Skip to content

Reduce Synapse query and polling tail amplification - #68

Open
ahrav wants to merge 25 commits into
mainfrom
feat/synapse-tail-amplification
Open

Reduce Synapse query and polling tail amplification#68
ahrav wants to merge 25 commits into
mainfrom
feat/synapse-tail-amplification

Conversation

@ahrav

@ahrav ahrav commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Problem

Concurrent embed.query traffic synchronized on a 100 ms client retry grid against a 1-permit fail-fast admission semaphore, so tail latency was ~100 ms x queue position over a sub-millisecond transport floor. The batch path quantized fast jobs on a 50 ms first-poll delay.

Changes

Host (mc-host)

  • Additive optional retry_after_ms on the wire error envelope; queue_full query rejections now carry a host-owned hint (SynapseLimits.query_retry_after_ms).
  • Bounded FIFO query waiting: query_admission = Semaphore::new(1 + max_waiting_queries), keeping try-only entry, explicit queue_full beyond K, and RAII permit release. K = 0 is byte-for-byte today's loss behavior and remains the default.
  • Startup validation charges waiters, queued jobs, and queued-job metadata (key/id/hash, via the runtime's own accounting) against the resident budget, so configs that would hit scratch queue_full early fail at startup instead.
  • The queued-waiter and awaiting-result timeout paths carry distinct messages so tests can discriminate which arm fired.

Plugin

  • Retry hygiene: finite attempt budget for queue_full, per-attempt deadline_ms rebuild, explicit cancelled classification (never condemns the lane), decorrelated jitter in [base, 3*base) honoring the served hint.
  • Fast-first escalating polls: first embed.result after ~1-2 ms, x1.6 escalation capped at the served retry_after_ms, 10 ms busy-poll floor, deadline clamp; per-job escalation state.

Benchmark harness (synapse_perf)

  • Batch/poll arm, closed-loop arm, injected engine delay, client-faithful retry/poll mirrors, conserved logical/attempt ledgers, exact drift-free open-loop scheduling for any rate, and client-faithful overload classification (module_restarted one-resubmission budget; correlation ids allocated under the writer lock).

Evidence

Frozen contract in docs/perf/synapse-tail-contract.md; raw run bundles are host-specific and gitignored (docs/perf/runs/). The definitive tiny-engine epoch (2,304/2,304 treatment positions, 2,288 valid, stable A/A) provisionally selects a+c with K=1:

  • Query 1.0x load p95: 585 ms -> 10.0 ms (5 ms service), 541 ms -> 28.5 ms (25 ms service); attempt amplification -> 1.0; terminal blocking -> 0; p95 permit wait 3.5-6.7 ms against the 100 ms budget.
  • Batch closed-1 p95: 51.6 ms -> 13.6 ms (50 ms poll step removed).
  • Overload behavior stays explicit queue_full with bounded amplification.

Selection is provisional: the production-bundle confirmatory run (criterion 8) is gate-blocked on magic-context-c50.8 and carried as magic-context-bux. Selection verified robust under the frozen 10% warmup rule via post-hoc re-analysis.

Verification

  • cargo test -p mc-host (25 suites), cargo clippy -p mc-host --all-targets -- -D warnings, cargo fmt --check: clean.
  • Plugin: 5,484 tests / 31,158 assertions pass; typecheck clean; lint clean of new issues.
  • Reviews applied: invariant-test-review, rust-code-reviewer, and reduce-complexity in parallel, then ponytail-review; all must-fix findings fixed, deferred structural items tracked in magic-context-qm0.

Summary by CodeRabbit

  • New Features
    • Added configurable Synapse query waiting with FIFO admission, bounded concurrency, cancellation, and retry guidance.
    • Added adaptive embedding retries and polling with jitter, backoff, deadline handling, and cancellation support.
    • Added a configurable Synapse performance load generator with query/batch modes and detailed metrics.
  • Bug Fixes
    • Improved error responses with optional retry timing metadata for clients.
    • Added startup validation for memory, scratch capacity, and task reservations, with clearer configuration errors.
  • Documentation
    • Documented retry behavior, capacity requirements, and Synapse performance measurement contracts.

ahrav added 19 commits August 26, 2026 17:01
module_restarted now mirrors the plugin's one-resubmission budget and
terminates as a rejected logical outcome instead of a harness-fatal
error. Correlation ids are allocated under the writer lock so concurrent
tasks cannot write inverted correlation order, which the host's
read-loop watermark answers by retiring the generation (the EOF storm).
Connection loss stays fatal but is now counted separately.
Startup scratch validation now includes the worst-case queued-job
key/id/hash charge using the runtime's own accounting, so non-default
configurations cannot pass validation and then hit scratch queue_full
before their configured capacities. The query-admission permit rule now
has a single derivation. Timeout messages distinguish the queued-waiter
arm from the awaiting-result backstop, and tests pin the accepted and
rejected sides of the waiter-memory boundary, runtime admission of
maximal texts at the boundary, and the exact resident-floor sizing rule.
SplitMix64 seeds disperse first draws so concurrent callers no longer
synchronize their first retry. Permit-wait summaries use successful
query attempts only. Restart resubmission gets a fresh per-submission
retry budget, matching the plugin. Late terminals after attempt timeouts
consume a bounded tombstone instead of poisoning the shared reader.
Ledger validation rejects duplicate and orphan records. Poll counts per
logical request gain a validity ceiling, and the harness pins a uniform
queued-request byte budget so admission cells fit the corrected startup
validation. Plugin tests pin exact poll schedules, the cancelled
classification's lane safety, and independent jitter across overlapping
calls.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 29 minutes.

View limit details

Limit details: You’ve used the included review currently available. Your 87 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8253f3f5-8671-4aea-bd11-897f65f38c4f

📥 Commits

Reviewing files that changed from the base of the PR and between 42b8ab1 and be17726.

📒 Files selected for processing (9)
  • crates/mc-host/examples/synapse_perf.rs
  • crates/mc-host/src/synapse/mod.rs
  • crates/mc-host/tests/perf_measurement.rs
  • crates/mc-host/tests/support/perf_measurement.rs
  • crates/mc-host/tests/synapse_protocol.rs
  • docs/mc-host-wire-protocol.md
  • docs/perf/synapse-tail-contract.md
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.test.ts
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.ts
📝 Walkthrough

Walkthrough

The change adds Synapse query admission limits, retry metadata, adaptive polling, exact open-loop scheduling, and a configurable query/batch performance benchmark. It also adds startup capacity validation, resource accounting, protocol updates, and extensive Rust and TypeScript tests.

Changes

Synapse runtime and retry flow

Layer / File(s) Summary
Error retry contract
crates/mc-host/src/handler.rs, crates/mc-host/src/dispatch.rs, packages/plugin/src/shared/mc-host-client/*
Errors can carry optional retry_after_ms metadata. The host serializes it, and the client validates and exposes it.
Admission and resource validation
crates/mc-host/src/synapse/*, crates/mc-host/src/runtime.rs, crates/mc-host/src/config.rs
Synapse supports FIFO query waiters. Startup validates scratch, resident, admission, and parked-task capacity.
Adaptive embedding retries and polling
packages/plugin/src/features/magic-context/memory/embedding-synapse.*
The provider uses injected timing and randomness, absolute retry deadlines, cancellation classification, retry hints, jitter, and adaptive polling.

Performance measurement

Layer / File(s) Summary
Exact open-loop scheduling
crates/mc-host/tests/support/perf_measurement.rs, crates/mc-host/benches/support/tcp.rs, crates/mc-host/examples/perf_load.rs
Open-loop workloads validate rates and calculate drift-free absolute slot offsets.
Synapse benchmark execution
crates/mc-host/examples/synapse_perf.rs
The benchmark supports query and batch workloads, retries, polling, concurrency modes, ledgers, resource metrics, and NDJSON output.
Measurement validation and contract
crates/mc-host/tests/perf_measurement.rs, crates/mc-host/tests/support/process_resources.rs, docs/perf/*
Tests cover ledgers, retry policies, polling schedules, task resources, and the Synapse performance contract.

Estimated code review effort: 5 (Critical) | ~120 minutes

Merge Risk: 🟡 Moderate · up to 42b8a

The PR improves retry, admission, and polling behavior, but the current head can still accept internally inconsistent benchmark results and may allow a batch page to run for up to twice its configured timeout; one admission test may also be nondeterministic. These bounded correctness and timeout risks should be fixed or explicitly accepted before merge.

Sequence Diagram(s)

sequenceDiagram
  participant EmbeddingProvider
  participant McHostClient
  participant SynapseHost
  participant PerformanceLedger
  EmbeddingProvider->>McHostClient: submit query or batch
  McHostClient->>SynapseHost: send request
  SynapseHost-->>McHostClient: result or retry_after_ms
  McHostClient-->>EmbeddingProvider: response
  EmbeddingProvider->>PerformanceLedger: record attempt and logical outcome
Loading

Poem

A rabbit checks the queue at dawn

Retry hints guide each hop along
Polls grow softly, clocks stay true
Ledgers count what tasks now do
Slots arrive in measured flight
Synapse rests within the bounds just right

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.15% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 226 functions across 31 files. (6 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: reducing Synapse query and polling tail amplification.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 41.15% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 226 functions across 31 files. (6 skipped: 5 unsupported, 1 too large.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

Comment thread crates/mc-host/examples/synapse_perf.rs Outdated
attempts += 1;
let mut params = constraints();
params["text"] = QUERY_TEXT.into();
params["deadline_ms"] = u64::try_from(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: deadline_ms can evaluate to 0 on sub-millisecond remaining durations, triggering schema_violation

When an attempt is dispatched or retried with less than 1 ms remaining before deadline, .as_millis() evaluates to 0. The host's parse_query strictly rejects deadline_ms: 0 with schema_violation: deadline_ms out of bounds. Because execute_query only expects timeout or queue_full, this causes a fatal harness error and terminates the run rather than recording a logical timeout. Clamping deadline_ms to at least 1 ms (or returning early with a timeout disposition if remaining duration is 0) ensures the request is accepted by the host parser.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already handled, in f06bfee — the harness takes the second option you list rather than clamping.

execute_query runs a pre-attempt guard before it ever sets the field:

let remaining_ms = u64::try_from(deadline.saturating_duration_since(Instant::now()).as_millis())...;
if remaining_ms == 0 {
    return Ok(terminal_record(..., LogicalDisposition::TimedOut, Some("timeout".to_owned()), ...));
}
params["deadline_ms"] = remaining_ms.into();

Clamping to 1 ms was the alternative, and it was rejected: it would send an attempt whose stated budget exceeds the real remaining time, so the host could serve work the client had already given up on and the attempt ledger would carry a deadline the run never actually offered. Returning the timeout disposition keeps the logical record truthful. The batch arm guards the same way before each poll.

let first = spawn_query(&host, &lane, "first", 30_000).await;
yield_until(|| engine.calls.load(std::sync::atomic::Ordering::SeqCst) == 1).await;
let second = spawn_query(&host, &lane, "second", 30_000).await;
tokio::task::yield_now().await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Single yield_now() may not give background query tasks enough turns to acquire permits

In bounded_query_waiters_are_fifo_and_reject_bound_plus_one, only a single tokio::task::yield_now().await; is called after spawning second and third. In paused virtual-time tests where background tasks handle socket I/O and semaphore acquisition, a single yield may not guarantee that second and third acquire waiter permits before fourth is spawned, potentially leading to intermittent test races. Other multi-task tests in this suite loop for _ in 0..100 { tokio::task::yield_now().await; } to ensure complete admission.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed in f847682. The reasoning is a bit different from a plain turn-count argument, which is worth recording.

Each waiter gets its own connection (spawn_query calls host.client().await per query), and the write happens inside the spawned task. So admission order follows when each task reaches the socket, not the spawn order in the test body — one yield is one scheduler turn, which need not carry a spawn through write, dispatch, and semaphore acquisition.

There is also nothing for yield_until to observe here: a waiter blocks before the engine, so engine.calls stays at 1 and cannot witness a waiter's arrival. That rules out the condition-based helper this suite otherwise prefers.

So the fix is the bounded drain the rest of the suite already uses in eight places, with a comment recording why the observable route is unavailable:

let second = spawn_query(&host, &lane, "second", 30_000).await;
for _ in 0..100 {
    tokio::task::yield_now().await;
}

Ran the test 5x plus the full synapse_protocol binary (23 tests) green.

"attempt ledger: {attempt_total} != {successes} + {retryable_rejections} + {attempt_timeouts} + {polls}"
));
}
for request in logical {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: $O(N \cdot M)$ nested scan across all attempts for each logical record in ledger validation

validate_synapse_ledgers filters the entire attempts slice for every request in logical. For large benchmark runs with tens of thousands of requests, this quadratic iteration adds significant post-run CPU delay. Pre-aggregating attempt counts by logical_id into a map in a single pass over attempts reduces validation time to $O(N + M)$.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in c2ffdf4 — validation is linear now. One pass over attempts builds both aggregates:

let mut first_method_by_logical: BTreeMap<u64, &'static str> = BTreeMap::new();
let mut attempts_by_logical: BTreeMap<u64, u64> = BTreeMap::new();
for attempt in attempts {
    first_method_by_logical.entry(attempt.logical_id).or_insert_with(|| attempt.method.wire_name());
    *attempts_by_logical.entry(attempt.logical_id).or_default() += 1;
    ...
}

The per-request loop then does a map lookup instead of a filter over the whole slice, so it is O(N + M).

f847682 extends that same pass with polls_by_logical for CodeRabbit's poll-count finding, deliberately reusing the existing pass rather than adding a second scan.

// never leaves the queue cannot poll without bound; each call is
// bounded by the remaining budget.
const deadlineAt = Date.now() + this.pageTimeoutMs;
const deadlineAt = this.now() + this.pageTimeoutMs;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: pollBatch re-anchors deadlineAt from pageTimeoutMs instead of deducting elapsed batch submission time

In embedItems, embed.batch is called with pageTimeoutMs. When polling starts, pollBatch sets deadlineAt = this.now() + this.pageTimeoutMs, resetting the overall timeout window rather than enforcing a shared absolute deadline across submission and polling. Propagating the remaining budget or an absolute deadlineAt into pollBatch makes deadline behavior consistent with collectJobPages.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed in f847682. CodeRabbit raised the same thing on 3869541863; the full write-up is there, summary here.

pollBatch now receives the caller's absolute deadline instead of computing this.now() + this.pageTimeoutMs, and embedItems establishes one page deadline that spans submission and polling:

const deadlineAt = this.now() + this.pageTimeoutMs;

This is the propagation you suggested rather than passing a remaining budget, so it matches collectJobPages, which already takes an absolute deadlineAt. The embed.batch submission is also clamped to the remaining budget so a module_restarted resubmission cannot restart the clock.

Confirmed the behaviour was real before the fix: with the re-anchoring in place a 1000 ms page budget settled at 1400 ms.

Comment thread docs/mc-host-wire-protocol.md Outdated
### 7.5 Synapse application protocol

Routed requests on the `synapse/management_surface` route are UTF-8 JSON objects (`binary = 0`) of the shape `{"method": string, "params": object}`. Successful responses are JSON objects whose operation payload lives under a `result` object. Failures are transport `Error` terminals with the canonical `{code, message}` body. The service implements exactly four methods — `models.list`, `embed.query`, `embed.batch`, and `embed.result` — and MUST NOT add job-management, health, cancellation, or model-management methods. Legacy field aliases (`entries`, `items`, `results`, `embedding`, `complete`, `cursor` as a response field) are TypeScript read compatibility only; the Rust host MUST NOT emit them.
Routed requests on the `synapse/management_surface` route are UTF-8 JSON objects (`binary = 0`) of the shape `{"method": string, "params": object}`. Successful responses are JSON objects whose operation payload lives under a `result` object. Failures are transport `Error` terminals with the canonical `{code, message,retry_after_ms?}` body. The service implements exactly four methods — `models.list`, `embed.query`, `embed.batch`, and `embed.result` — and MUST NOT add job-management, health, cancellation, or model-management methods. Legacy field aliases (`entries`, `items`, `results`, `embedding`, `complete`, `cursor` as a response field) are TypeScript read compatibility only; the Rust host MUST NOT emit them.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Formatting typo in error envelope summary

Suggested change
Routed requests on the `synapse/management_surface` route are UTF-8 JSON objects (`binary = 0`) of the shape `{"method": string, "params": object}`. Successful responses are JSON objects whose operation payload lives under a `result` object. Failures are transport `Error` terminals with the canonical `{code, message,retry_after_ms?}` body. The service implements exactly four methods — `models.list`, `embed.query`, `embed.batch`, and `embed.result` — and MUST NOT add job-management, health, cancellation, or model-management methods. Legacy field aliases (`entries`, `items`, `results`, `embedding`, `complete`, `cursor` as a response field) are TypeScript read compatibility only; the Rust host MUST NOT emit them.
Routed requests on the `synapse/management_surface` route are UTF-8 JSON objects (`binary = 0`) of the shape `{"method": string, "params": object}`. Successful responses are JSON objects whose operation payload lives under a `result` object. Failures are transport `Error` terminals with the canonical `{code, message, retry_after_ms?}` body. The service implements exactly four methods — `models.list`, `embed.query`, `embed.batch`, and `embed.result` — and MUST NOT add job-management, health, cancellation, or model-management methods. Legacy field aliases (`entries`, `items`, `results`, `embedding`, `complete`, `cursor` as a response field) are TypeScript read compatibility only; the Rust host MUST NOT emit them.

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c2ffdf4{code, message,retry_after_ms?} now reads {code, message, retry_after_ms?}. Thanks.

@kilo-code-bot

kilo-code-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (38 files)
  • .beads/interactions.jsonl
  • .beads/issues.jsonl
  • .gitignore
  • crates/mc-host/benches/support/tcp.rs
  • crates/mc-host/examples/perf_load.rs
  • crates/mc-host/examples/synapse_host.rs
  • crates/mc-host/examples/synapse_perf.rs
  • crates/mc-host/src/broca/mod.rs
  • crates/mc-host/src/composite.rs
  • crates/mc-host/src/config.rs
  • crates/mc-host/src/dispatch.rs
  • crates/mc-host/src/handler.rs
  • crates/mc-host/src/runtime.rs
  • crates/mc-host/src/synapse/bundle.rs
  • crates/mc-host/src/synapse/mod.rs
  • crates/mc-host/tests/composite_routing.rs
  • crates/mc-host/tests/dispatch.rs
  • crates/mc-host/tests/handler_contract.rs
  • crates/mc-host/tests/perf_measurement.rs
  • crates/mc-host/tests/support/echo_host.rs
  • crates/mc-host/tests/support/mod.rs
  • crates/mc-host/tests/support/perf_measurement.rs
  • crates/mc-host/tests/support/process_resources.rs
  • crates/mc-host/tests/support/raw_client.rs
  • crates/mc-host/tests/support/synapse.rs
  • crates/mc-host/tests/synapse_bundle.rs
  • crates/mc-host/tests/synapse_jobs.rs
  • crates/mc-host/tests/synapse_protocol.rs
  • crates/mc-module/examples/direct_host_fixture.rs
  • crates/mc-module/src/lib.rs
  • docs/mc-host-wire-protocol.md
  • docs/perf/mc-host-baseline.md
  • docs/perf/synapse-tail-contract.md
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.test.ts
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.ts
  • packages/plugin/src/shared/mc-host-client/client.test.ts
  • packages/plugin/src/shared/mc-host-client/client.ts
  • packages/plugin/src/shared/mc-host-client/errors.ts
Previous Review Summaries (3 snapshots, latest commit f847682)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit f847682)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (38 files)
  • .beads/interactions.jsonl
  • .beads/issues.jsonl
  • .gitignore
  • crates/mc-host/benches/support/tcp.rs
  • crates/mc-host/examples/perf_load.rs
  • crates/mc-host/examples/synapse_host.rs
  • crates/mc-host/examples/synapse_perf.rs
  • crates/mc-host/src/broca/mod.rs
  • crates/mc-host/src/composite.rs
  • crates/mc-host/src/config.rs
  • crates/mc-host/src/dispatch.rs
  • crates/mc-host/src/handler.rs
  • crates/mc-host/src/runtime.rs
  • crates/mc-host/src/synapse/bundle.rs
  • crates/mc-host/src/synapse/mod.rs
  • crates/mc-host/tests/composite_routing.rs
  • crates/mc-host/tests/dispatch.rs
  • crates/mc-host/tests/handler_contract.rs
  • crates/mc-host/tests/perf_measurement.rs
  • crates/mc-host/tests/support/echo_host.rs
  • crates/mc-host/tests/support/mod.rs
  • crates/mc-host/tests/support/perf_measurement.rs
  • crates/mc-host/tests/support/process_resources.rs
  • crates/mc-host/tests/support/raw_client.rs
  • crates/mc-host/tests/support/synapse.rs
  • crates/mc-host/tests/synapse_bundle.rs
  • crates/mc-host/tests/synapse_jobs.rs
  • crates/mc-host/tests/synapse_protocol.rs
  • crates/mc-module/examples/direct_host_fixture.rs
  • crates/mc-module/src/lib.rs
  • docs/mc-host-wire-protocol.md
  • docs/perf/mc-host-baseline.md
  • docs/perf/synapse-tail-contract.md
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.test.ts
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.ts
  • packages/plugin/src/shared/mc-host-client/client.test.ts
  • packages/plugin/src/shared/mc-host-client/client.ts
  • packages/plugin/src/shared/mc-host-client/errors.ts

Previous review (commit f06bfee)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
crates/mc-host/tests/synapse_protocol.rs 79 Single yield_now() may not give background query tasks enough turns to acquire permits
crates/mc-host/tests/support/perf_measurement.rs 437 $O(N \cdot M)$ nested scan across all attempts for each logical record in ledger validation

SUGGESTION

File Line Issue
packages/plugin/src/features/magic-context/memory/embedding-synapse.ts 1463 pollBatch re-anchors deadlineAt from pageTimeoutMs instead of deducting elapsed batch submission time
docs/mc-host-wire-protocol.md 414 Formatting typo in error envelope summary
Files Reviewed (38 files)
  • .beads/interactions.jsonl
  • .beads/issues.jsonl
  • .gitignore
  • crates/mc-host/benches/support/tcp.rs
  • crates/mc-host/examples/perf_load.rs
  • crates/mc-host/examples/synapse_host.rs
  • crates/mc-host/examples/synapse_perf.rs
  • crates/mc-host/src/broca/mod.rs
  • crates/mc-host/src/composite.rs
  • crates/mc-host/src/config.rs
  • crates/mc-host/src/dispatch.rs
  • crates/mc-host/src/handler.rs
  • crates/mc-host/src/runtime.rs
  • crates/mc-host/src/synapse/bundle.rs
  • crates/mc-host/src/synapse/mod.rs
  • crates/mc-host/tests/composite_routing.rs
  • crates/mc-host/tests/dispatch.rs
  • crates/mc-host/tests/handler_contract.rs
  • crates/mc-host/tests/perf_measurement.rs
  • crates/mc-host/tests/support/echo_host.rs
  • crates/mc-host/tests/support/mod.rs
  • crates/mc-host/tests/support/perf_measurement.rs - 1 issue
  • crates/mc-host/tests/support/process_resources.rs
  • crates/mc-host/tests/support/raw_client.rs
  • crates/mc-host/tests/support/synapse.rs
  • crates/mc-host/tests/synapse_bundle.rs
  • crates/mc-host/tests/synapse_jobs.rs
  • crates/mc-host/tests/synapse_protocol.rs - 1 issue
  • crates/mc-module/examples/direct_host_fixture.rs
  • crates/mc-module/src/lib.rs
  • docs/mc-host-wire-protocol.md - 1 issue
  • docs/perf/mc-host-baseline.md
  • docs/perf/synapse-tail-contract.md
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.test.ts
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.ts - 1 issue
  • packages/plugin/src/shared/mc-host-client/client.test.ts
  • packages/plugin/src/shared/mc-host-client/client.ts
  • packages/plugin/src/shared/mc-host-client/errors.ts

Fix these issues in Kilo Cloud

Previous review (commit b4362c7)

Status: 5 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
crates/mc-host/examples/synapse_perf.rs 731 deadline_ms can evaluate to 0 on sub-millisecond remaining durations, triggering schema_violation
crates/mc-host/tests/synapse_protocol.rs 79 Single yield_now() may not give background query tasks enough turns to acquire permits
crates/mc-host/tests/support/perf_measurement.rs 437 $O(N \cdot M)$ nested scan across all attempts for each logical record in ledger validation

SUGGESTION

File Line Issue
packages/plugin/src/features/magic-context/memory/embedding-synapse.ts 1440 pollBatch re-anchors deadlineAt from pageTimeoutMs instead of deducting elapsed batch submission time
docs/mc-host-wire-protocol.md 414 Formatting typo in error envelope summary
Files Reviewed (36 files)
  • .beads/interactions.jsonl
  • .beads/issues.jsonl
  • .gitignore
  • crates/mc-host/benches/support/tcp.rs
  • crates/mc-host/examples/perf_load.rs
  • crates/mc-host/examples/synapse_host.rs
  • crates/mc-host/examples/synapse_perf.rs - 1 issue
  • crates/mc-host/src/broca/mod.rs
  • crates/mc-host/src/composite.rs
  • crates/mc-host/src/config.rs
  • crates/mc-host/src/dispatch.rs
  • crates/mc-host/src/handler.rs
  • crates/mc-host/src/synapse/bundle.rs
  • crates/mc-host/src/synapse/mod.rs
  • crates/mc-host/tests/composite_routing.rs
  • crates/mc-host/tests/handler_contract.rs
  • crates/mc-host/tests/perf_measurement.rs
  • crates/mc-host/tests/support/echo_host.rs
  • crates/mc-host/tests/support/mod.rs
  • crates/mc-host/tests/support/perf_measurement.rs - 1 issue
  • crates/mc-host/tests/support/process_resources.rs
  • crates/mc-host/tests/support/raw_client.rs
  • crates/mc-host/tests/support/synapse.rs
  • crates/mc-host/tests/synapse_bundle.rs
  • crates/mc-host/tests/synapse_jobs.rs
  • crates/mc-host/tests/synapse_protocol.rs - 1 issue
  • crates/mc-module/examples/direct_host_fixture.rs
  • crates/mc-module/src/lib.rs
  • docs/mc-host-wire-protocol.md - 1 issue
  • docs/perf/mc-host-baseline.md
  • docs/perf/synapse-tail-contract.md
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.test.ts
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.ts - 1 issue
  • packages/plugin/src/shared/mc-host-client/client.test.ts
  • packages/plugin/src/shared/mc-host-client/client.ts
  • packages/plugin/src/shared/mc-host-client/errors.ts

Fix these issues in Kilo Cloud


Reviewed by gemini-3.7-flash · Input: 212.4K · Output: 19.6K · Cached: 1.6M

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b4362c775c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/mc-host/examples/synapse_perf.rs Outdated
Comment on lines +1430 to +1434
let ledger = perf_measurement::validate_synapse_ledgers(&logical, &attempts);
let attempt_latency =
LatencySummary::from_unsorted(attempts.iter().map(|attempt| attempt.latency_ns).collect());
let logical_latency =
LatencySummary::from_unsorted(logical.iter().map(|request| request.latency_ns).collect());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude the frozen warmup window from summaries

Every benchmark cell feeds all scheduled records directly into the ledger and latency summaries, even though docs/perf/synapse-tail-contract.md requires the first 10% of each hold window to remain raw-only and not enter estimates. The emitted records also lack the required warmup marker, so consumers cannot reliably distinguish them without reconstructing the window externally; consequently the executable's headline latency, amplification, rejection, and timeout summaries are biased by warmup and are inadmissible under the frozen experiment contract.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed in f847682. Both halves of this (the discard and the missing marker) are addressed.

Each repetition derives a window from its scheduled span, then stamps both ledgers before anything is estimated:

window.stamp(&mut records, &mut attempts);
let (logical_estimates, logical_warmup) = perf_measurement::partition_warmup(&logical, |r| r.warmup);
let (attempt_estimates, attempt_warmup) = perf_measurement::partition_warmup(&attempts, |a| a.warmup);

Only the estimate set feeds validate_synapse_ledgers, the latency/permit-wait/poll summaries, and the rates. Raw evidence keeps every row.

Details worth flagging:

  • warmup is on both LogicalRecord and AttemptRecord, and attempts inherit the marker from the owning logical request, so the two ledgers are discarded together and stay reconcilable.
  • The field is deliberately not #[serde(default)]: evidence written without the marker is not contract-conformant, so it fails to parse rather than silently reading as post-warmup.
  • The window boundaries (hold_window_start_ns, warmup_end_ns, hold_window_end_ns) and the discarded counts (warmup_offered, warmup_attempts) are emitted, so the discard is re-derivable from the summary alone.
  • The prefix is the first 10% exclusive of its end; a request opening exactly on the boundary is post-warmup. Pinned in the_hold_window_marks_warmup_and_censors_unsettled_requests.

Comment thread crates/mc-host/examples/synapse_perf.rs Outdated
Comment on lines +1222 to +1224
let lag =
u64::try_from(now.duration_since(scheduled).as_nanos()).unwrap_or(u64::MAX);
send_lag_max_ns = send_lag_max_ns.max(lag);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Measure open-loop lag at the actual wire send

Under generator or runtime saturation, this lag is sampled before the request task is spawned, so it measures only the pacer loop's wake-up delay. The spawned task can then wait arbitrarily long in Tokio's run queue or on the shared writer mutex before actual_first_send_ns, while missed_slots remains zero; this allows an overloaded, non-open-loop repetition to pass the validity gate and corrupt the tail comparison. Compute the gate from the recorded actual first send relative to the intended scheduled timestamp.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed in f847682. The gate is computed from the recorded send against the intended schedule, exactly as you describe:

fn open_loop_send_lag(records: &[LogicalRecord], rate: u64) -> (u64, u64) {
    ...
    let lag_ns = record.actual_first_send_ns.saturating_sub(scheduled_ns);
    ...
    if slot_gap_ns != 0 && lag_ns >= slot_gap_ns { missed_slots += 1; }
}

One related change was needed to make this meaningful: scheduled_ns was previously reconstructed from the pacer's observed lag (ctx.wire.elapsed_ns().saturating_sub(lag)), which bakes the very quantity being measured into the baseline. It is now the intended slot offset from the run's start on the wire clock (start_ns + offset_ns), so time a request spends queued after the pacer releases it now shows up in the lag instead of cancelling out.

The per-slot threshold stays each slot's own gap rather than a global constant.

Comment thread crates/mc-host/examples/synapse_perf.rs Outdated
Comment on lines +1121 to +1126
let served_delay = result["retry_after_ms"].as_u64().unwrap_or(served_poll_cap);
poll_delay_ms = ctx
.opts
.variant
.pending_poll_delay_ms(poll_delay_ms, served_delay);
tokio::time::sleep(Duration::from_secs_f64(poll_delay_ms / 1_000.0)).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clamp pending-poll sleeps to the batch deadline

When a job remains pending with less time left than poll_delay_ms, this unconditional sleep crosses the logical deadline. The next iteration still enters record_call; its zero remaining budget is passed to RoutedWire::call, which writes the request before timing out its receiver, so the harness records and sends an extra post-deadline poll. This inflates poll amplification and host load precisely in slow or overloaded cells and no longer mirrors the plugin, which clamps every pending delay to the remaining deadline.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in c2ffdf4 — the pending wait is clamped and an exhausted deadline terminates the request:

let remaining = deadline.saturating_duration_since(Instant::now());
tokio::time::sleep(Duration::from_secs_f64(delay_ms / 1_000.0).min(remaining)).await;
if Instant::now() >= deadline {
    return Ok(terminal_record(..., LogicalDisposition::TimedOut, Some("timeout".to_owned()), ...));
}

Your sibling comment about the paged path (3869598724) was the remaining gap — that continue had no equivalent check and is fixed in f847682.

Comment on lines +1237 to +1239
let mut records = Vec::with_capacity(offered as usize);
while let Some(result) = tasks.join_next().await {
match result {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve in-flight requests at the measurement boundary

For an open-loop cell with requests still outstanding when the scheduled hold window ends, this drain waits for every task to settle—potentially until the 3-second query or 120-second batch deadline—and records those later outcomes as completions or timeouts. The frozen ledger requires such requests to be counted as in flight at the window end, so the current behavior understates censoring and lets post-window completions inflate completed rate when downstream analysis uses the configured seconds window.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed in f847682. A request the window closed on is censored rather than awaited into a completion:

if record.terminal_ns > self.end_ns && record.disposition != LogicalDisposition::InFlight {
    record.disposition = LogicalDisposition::InFlight;
    record.terminal_code = Some(IN_FLIGHT_AT_WINDOW_END_CODE.to_owned());
}

The window end comes from the scheduled span, not from observed completions, so it does not drift with the outcome it is judging.

Note the drain itself is retained on purpose: the task still settles and its attempt rows keep whatever wire outcome actually arrived, so nothing is lost from raw evidence. What changed is that the logical row records that the request had not settled when measurement ended, which is what the frozen ledger asks for. That keeps in flight at window end a real term in the count identity instead of a term that was always zero.

Covered by the_hold_window_marks_warmup_and_censors_unsettled_requests.

Comment on lines +574 to +575
pub fn uses_served_query_hint(self) -> bool {
matches!(self, Self::B | Self::APlusC)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep candidate B's retry hint out of A+C

The frozen design defines A+C as A's bounded server waiting combined with C's fast polling, but this predicate also enables B's served query-retry hint for APlusC. As a result, the selected A+C treatment differs from its declared composition and its query results cannot be attributed to A+C rather than the unlabelled addition of B; only B should select the served hint unless the treatment and contract are renamed consistently.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in c2ffdf4 — only B selects the served hint now:

/// Only candidate B reads the host's served `query_retry_after_ms`. The
/// frozen matrix declares `a+c` as A's bounded server waiting plus C's
/// fast polling, so letting it read the hint too would make its query
/// results unattributable between A+C and an unlabelled B.
pub fn uses_served_query_hint(self) -> bool {
    matches!(self, Self::B)
}

needs_waiting_queries still matches A | APlusC and fast_polls still matches C | APlusC, so A+C keeps exactly its declared composition. Guarded by variant_policy_keeps_control_arms_isolated_from_landed_hints.

Comment on lines +359 to +363
let queue_full = attempt.code.as_deref() == Some("queue_full");
if !queue_full {
*admitted_by_method
.entry(attempt.method.wire_name().to_owned())
.or_default() += 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude outcome-unknown timeouts from admitted counts

When RoutedWire::call times out before receiving a terminal, the attempt is recorded with code = "attempt_timeout", and this complement-of-queue_full logic counts it as admitted even though the harness has no wire evidence that the host acquired an admission permit or job slot. Under overload or transport delay this inflates admitted_by_method, which is subsequently used as measured λ_adm for capacity and K analysis; outcome-unknown attempts need a separate censored category rather than being treated as admitted.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in c2ffdf4 — outcome-unknown attempts have their own subtotal and are excluded from both admitted and rejected:

let outcome_unknown = attempt.code.as_deref() == Some(ATTEMPT_TIMEOUT_CODE);
if outcome_unknown {
    *outcome_unknown_by_method.entry(...).or_default() += 1;
} else if !queue_full {
    *admitted_by_method.entry(...).or_default() += 1;
}

admitted_by_method is the measured λ_adm, so it now counts only attempts with wire evidence that the host took a permit.

f847682 adds outcome_unknown_attempts_are_neither_admitted_nor_rejected, which pins all three buckets (served / queue_full / no-terminal) against one logical request so a future change cannot quietly fold the third back into the first.

Comment on lines +513 to +517
Err(_) => {
self.pending.lock().await.remove(&corr);
// Leave a tombstone so the reply that may still arrive is
// discarded instead of being read as an unknown correlation.
let mut tombstones = self.tombstones.lock().await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make timeout tombstone installation atomic

When a reply arrives at the timeout boundary, the timeout task can remove corr from pending here and be descheduled before it acquires the tombstone lock. The reader then finds neither a pending sender nor a tombstone and reports terminal for unknown correlation, poisoning the shared connection and every other in-flight logical request even though this was an expected late terminal. Install the tombstone and remove the pending entry under one synchronization protocol that the reader also follows.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in c2ffdf4 — the ordering is inverted so the correlation is never absent from both maps:

// Install the tombstone *before* dropping the pending entry.
// The reader looks up `pending` first and only then consults
// the tombstones, so this order keeps `corr` present in
// `pending ∪ tombstones` at every instant.
let mut tombstones = self.tombstones.lock().await;
...
tombstones.push_back(corr);
drop(tombstones);
self.pending.lock().await.remove(&corr);

The invariant is stated in terms of the reader's lookup order, which is the part that makes it hold without a shared lock: reader checks pending then tombstones, writer publishes to tombstones then retracts from pending. A reader interleaved anywhere sees the correlation in at least one map, so an expected late terminal can no longer poison the shared connection.

The residue case is bounded too — a terminal that already consumed the sender leaves the tombstone unclaimed, and FIFO eviction at TOMBSTONE_CAP caps that.

Comment thread crates/mc-host/examples/synapse_perf.rs Outdated
Comment on lines +609 to +610
let code = json["code"].as_str().map(str::to_owned);
let retry_after_ms = json["retry_after_ms"].as_u64();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record retry hints from successful result envelopes

For successful embed.batch descriptors and pending embed.result replies, retry_after_ms lives under json.result, not at the top level. This lookup therefore records None for every successful batch/poll hint even though those served values drive the subsequent poll schedule, leaving the raw attempt ledger unable to audit whether the client-faithful policy honored the host's cap. Read the nested result field as well as the top-level error-envelope field.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in c2ffdf4 — the recorded attempt reads both locations:

// Error envelopes carry the hint at the top level; a served
// batch descriptor and a pending poll reply carry it under
// `result`.
let retry_after_ms = json["retry_after_ms"]
    .as_u64()
    .or_else(|| json["result"]["retry_after_ms"].as_u64());

So the raw attempt ledger can now audit whether the poll schedule honored the served cap, which was the point. Covered by raw_error_surfaces_retry_after_ms.

Comment on lines +1291 to +1295
seed: u64,
engine_delay_ms: u64,
max_waiting_queries: usize,
query_retry_after_ms: u64,
ledger: perf_measurement::SynapseLedgerSummary,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit the transport-floor input with each summary

The CLI's transport_floor_ns directly changes every reported permit-wait sample, but it is omitted from the serialized summary while the other treatment inputs are retained. Two runs with identical emitted configuration can therefore produce different derived wait distributions solely because of an unrecorded subtraction, making the raw evidence non-reproducible and preventing downstream analysis from verifying the calculation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed in f847682. transport_floor_ns is now a field on the emitted Summary, alongside the other treatment inputs:

/// Subtracted from every permit-wait sample, so two runs with otherwise
/// identical emitted configuration derive different wait distributions
/// when it differs. Emitted with the other treatment inputs to keep that
/// subtraction reproducible from the summary alone.
transport_floor_ns: u64,

Two runs that report the same configuration now cannot disagree on the derived wait distribution, and downstream analysis can re-check the subtraction.

Host:
- Reserve waiter headroom in SCRATCH_RESERVED_BYTES so max_waiting_queries
  up to 4 is feasible at default limits (the selected K=1 was previously
  rejected at startup); repin the boundary tests (K boundary 0 -> 4 at
  defaults, 28 -> 32 with the 8 MiB harness queue budget).
- Fail initialization on infeasible SynapseLimits instead of silently
  disabling the lane while the host reports healthy; artifact faults still
  degrade. Split validate_limits out of validate_serving_limits.
- Validate parked query slots against handler-task capacity: components
  declare a general_task_hold_bound, startup refuses a configuration whose
  parked tasks (1 + max_waiting_queries) leave no free general slot.
- Name the resident floor's composition in the below-minimum config error
  and document the floor/default increase as an upgrade note.
- Remove the unreachable permit-count error in ready_with_engine.

Plugin:
- Budget queue_full retries by the caller's deadline (64-attempt safety
  cap) instead of the generic four-attempt cap, which abandoned bursts
  with most of the deadline unspent under the default fail-fast admission.
- Retry the host's shutdown "cancelled" rejection as transport-class so
  in-flight embeds survive a host restart; only local aborts stay in the
  never-retried cancelled class.
- Issue the first embed.result immediately and consume the fast-first
  seed on the first pending reply, removing the unconditional 1-2 ms
  pre-sleep that regressed sub-millisecond batch p50.

Harness:
- Clamp deadline_ms >= 1 before sending (a truncated 0 was rejected by
  the host and turned a near-deadline retry into a fatal run error).
- Bound the pacer spin window by half the slot gap so high offered rates
  cannot busy-spin a worker core in the SUT's process.
- Hoist the shared poll-policy constants, derive MAX_POLLS_PER_LOGICAL
  from the busy-poll floor, mirror the immediate-first-poll and split
  retry budgets, and pin the unsaturated 10 -> 16 escalation step.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f06bfee20c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// Declaring the bound lets startup refuse a `max_waiting_queries`
// that could park away every general handler-task slot.
crate::handler::ResourceDeclaration {
general_task_hold_bound: self.inner.limits.max_waiting_queries.saturating_add(1),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not reserve query tasks for a disabled component

When SynapseComponent::new(None) is used, the component cannot bind or execute queries, but this declaration still reserves one parked general task because the default max_waiting_queries is zero. A valid host configuration that intentionally leaves one general handler slot (for example, reserved Broca slots plus one) now fails startup with “declared parked handler tasks ... leave no free general handler-task slot,” even though disabled Synapse can never consume that slot. Return a zero hold bound for the disabled/no-bundle state while retaining the bound for configured or pre-readied lanes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in c2ffdf4 — a disabled lane declares no hold:

// A component with no bundle configuration and no ready lane can
// never reach the parking path: `bind` rejects every route and
// `handle` answers `artifact_invalid` without touching admission,
// and only `initialize` over a `SynapseConfig` can publish a lane.
if self.inner.config.is_none() && self.ready_lane().is_none() {
    return crate::handler::ResourceDeclaration::default();
}

The predicate is config.is_none() && ready_lane().is_none(), so a configured lane and a pre-readied lane both keep their bound — only the state that provably cannot park drops it, which is the distinction you asked for.

Comment thread crates/mc-host/examples/synapse_perf.rs Outdated
Comment on lines +618 to +623
} else if reply.frame.ty == raw_client::TY_ERROR
&& code.as_deref() == Some("timeout")
{
AttemptDisposition::Timeout
} else {
AttemptDisposition::Success

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not classify unexpected error terminals as successes

When a non-poll call receives any Error other than queue_full, module_restarted, or timeout—for example artifact_invalid, schema_violation, or cancelled—this fallback records the attempt as Success. The caller later marks the logical operation as a harness error and invalidates the repetition, but the emitted raw attempt and summary still claim a successful wire call, corrupting the retained evidence needed to diagnose that invalid run and violating the ledger definition that successes are successful non-poll calls. Give other Error terminals a rejection/failure disposition rather than falling through to success.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in c2ffdf4 — those terminals get a Failure disposition instead of falling through:

} else if is_error {
    // Any other error terminal is outside the client policy's
    // vocabulary. The caller turns it into a harness error and
    // invalidates the repetition, so the retained attempt row
    // must not claim a successful wire call.
    AttemptDisposition::Failure
} else {
    AttemptDisposition::Success
};

The validator also treats any nonzero failures as a ledger error, so the repetition is reported inadmissible rather than silently reshaping the rates — and the four-way attempt identity stays intact for every admissible run.

f847682 adds an_error_outside_the_client_vocabulary_is_not_a_success, which asserts successes == 0, failures == 1, and !valid for a schema_violation terminal.

ahrav added 2 commits August 27, 2026 06:44
Address review findings on the tail-amplification harness and the Synapse
component's startup declaration.

Attempt ledger. A non-poll wire call answered with an error outside the
client policy's vocabulary (artifact_invalid, schema_violation, cancelled)
fell through to the Success disposition, so the retained evidence claimed a
successful call for the very run the caller then invalidated. Those
terminals now record a Failure disposition, and the ledger validator reports
any nonzero count as an error, which keeps the frozen four-way identity
intact for every admissible repetition. Retry hints served under a batch
descriptor's or a pending poll's `result` object are now recorded alongside
the top-level error-envelope field, so the ledger can audit whether the poll
schedule honored the served cap.

Admitted counts. An attempt whose client deadline fired before any terminal
arrived carries no wire evidence that the host took an admission permit, yet
the complement-of-queue_full test counted it as admitted and that subtotal
feeds the measured admitted rate. Outcome-unknown attempts now have their
own subtotal and are excluded from both admitted and rejected counts.

Ledger validation was quadratic: it rescanned every attempt once per logical
request to derive method subtotals and attempt ownership. One pass over the
attempts now builds both aggregates.

Treatment composition. The frozen matrix declares a+c as A's bounded server
waiting plus C's fast polling, but the served-query-hint predicate also
selected a+c, folding B's mechanism into the arm and making its query
results unattributable. Only B reads the hint now.

Wire races. The timeout path removed its pending entry before installing the
tombstone, so a reader running concurrently could find the correlation in
neither map, report an unknown correlation, and poison the shared connection
for every other in-flight request over an expected late terminal. The
tombstone is now installed first, and both sides document the ordering that
keeps a live correlation present in one map or the other at every instant.

Pending polls slept the full escalated delay without clamping it to the
remaining budget, so the next iteration wrote a post-deadline poll before its
zero-budget receiver expired, inflating poll amplification in exactly the
slow cells under measurement. The wait is now clamped and an exhausted
deadline terminates the request, mirroring the plugin's pendingPollDelay.

Startup declaration. A Synapse component with no bundle configuration and no
ready lane rejects every bind and never parks a handler task on admission,
but it still declared a one-task hold, which could fail startup for a host
that reserves all but one general handler-task slot. A disabled lane now
declares no hold while configured and pre-readied lanes keep theirs.
Three files conflicted.

`crates/mc-host/examples/synapse_perf.rs` takes this branch's version. Main's
only change to the file since the merge base was a `cargo fmt` reflow of a
statement inside the pre-rewrite harness body, and this branch replaced that
body; the reflowed identifiers (`lag_ns`, `interval_ns`) no longer exist here,
so nothing from main is dropped.

`.gitignore` and `.beads/interactions.jsonl` were both append conflicts and
take the union. The interaction log is an append-only event stream, so the
three added records are restored to `created_at` order and every id stays
distinct.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

Inline comments:
In `@crates/mc-host/tests/support/perf_measurement.rs`:
- Around line 492-503: Extend the existing aggregation pass to count owned
AttemptDisposition::Poll entries by logical_id, then update the validation loop
over logical to compare each request.polls against that poll count and push an
error on mismatch. Preserve the current attempts validation and ledger validity
behavior.

In `@docs/perf/synapse-tail-contract.md`:
- Around line 293-300: Update the rate equation in the section around “λ_off” so
all terms use consistent units: divide terminal rejections, timeouts, and
in-flight counts by the measured reporting window, or relabel the equation as a
count equality. Keep the manifest’s starts-versus-settled convention and
raw-count reconciliation guidance aligned with the chosen formulation.

In `@packages/plugin/src/features/magic-context/memory/embedding-synapse.ts`:
- Around line 1458-1466: Update pollBatch to accept and reuse the absolute
deadline computed by its caller instead of creating a new deadline from
this.now() and pageTimeoutMs. In embedItems, pass that page deadline through to
pollBatch, matching the existing deadline propagation used by collectJobPages
and keeping the total page timeout bounded across submission and polling.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3235db30-53b6-4a24-b0fa-3dbce83385c0

📥 Commits

Reviewing files that changed from the base of the PR and between 94143b6 and 42b8ab1.

📒 Files selected for processing (38)
  • .beads/interactions.jsonl
  • .beads/issues.jsonl
  • .gitignore
  • crates/mc-host/benches/support/tcp.rs
  • crates/mc-host/examples/perf_load.rs
  • crates/mc-host/examples/synapse_host.rs
  • crates/mc-host/examples/synapse_perf.rs
  • crates/mc-host/src/broca/mod.rs
  • crates/mc-host/src/composite.rs
  • crates/mc-host/src/config.rs
  • crates/mc-host/src/dispatch.rs
  • crates/mc-host/src/handler.rs
  • crates/mc-host/src/runtime.rs
  • crates/mc-host/src/synapse/bundle.rs
  • crates/mc-host/src/synapse/mod.rs
  • crates/mc-host/tests/composite_routing.rs
  • crates/mc-host/tests/dispatch.rs
  • crates/mc-host/tests/handler_contract.rs
  • crates/mc-host/tests/perf_measurement.rs
  • crates/mc-host/tests/support/echo_host.rs
  • crates/mc-host/tests/support/mod.rs
  • crates/mc-host/tests/support/perf_measurement.rs
  • crates/mc-host/tests/support/process_resources.rs
  • crates/mc-host/tests/support/raw_client.rs
  • crates/mc-host/tests/support/synapse.rs
  • crates/mc-host/tests/synapse_bundle.rs
  • crates/mc-host/tests/synapse_jobs.rs
  • crates/mc-host/tests/synapse_protocol.rs
  • crates/mc-module/examples/direct_host_fixture.rs
  • crates/mc-module/src/lib.rs
  • docs/mc-host-wire-protocol.md
  • docs/perf/mc-host-baseline.md
  • docs/perf/synapse-tail-contract.md
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.test.ts
  • packages/plugin/src/features/magic-context/memory/embedding-synapse.ts
  • packages/plugin/src/shared/mc-host-client/client.test.ts
  • packages/plugin/src/shared/mc-host-client/client.ts
  • packages/plugin/src/shared/mc-host-client/errors.ts

Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Comment thread crates/mc-host/tests/support/perf_measurement.rs
Comment thread docs/perf/synapse-tail-contract.md Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 42b8ab1c40

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if !cursor.is_string() {
return Err(format!("non-final page omitted cursor: {json}"));
}
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check the deadline before polling the next vector page

When processing a non-final vector page finishes at or just after BATCH_DEADLINE, this unconditional continue starts another poll without the deadline check used by the pending-reply path. record_call then computes a zero budget, but RoutedWire::call writes the request before timing out its receiver, so paged batches can emit and record a post-deadline poll, inflating amplification and host load in slow cells. Check the remaining deadline before continuing to the next page.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed in f847682. The paged path now gates on the deadline before continuing, which the pending path already did:

// Paged fetches carry no pending delay, so this is the only
// place the page loop can observe an exhausted deadline.
if Instant::now() >= deadline {
    return Ok(terminal_record(..., LogicalDisposition::TimedOut, Some("timeout".to_owned()), ...));
}
continue;

Your mechanism is right and worth restating: record_call would compute a zero budget, but RoutedWire::call writes before timing out its receiver, so the post-deadline poll is genuinely emitted and recorded — it inflates both measured amplification and real host load, in exactly the slow cells under measurement.

Comment thread crates/mc-host/examples/synapse_perf.rs Outdated
return Ok(terminal_record(
logical_id,
scheduled_start_ns,
first_send.unwrap_or(now),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the first timed-out attempt's send timestamp

When the first query attempt times out, record_call has already recorded its real sent_ns, but this path leaves first_send unset and substitutes the timeout timestamp itself. A terminal timeout therefore gets near-zero closed-loop logical latency, while a later successful retry starts latency at the retry rather than the original attempt, omitting both the first attempt and retry delay. The batch timeout paths use the same fallback, so retain the first send timestamp even when the wire call returns WireCallError::Timeout.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed in f847682, and the retry half of this was the worse half.

record_call was discarding sent_ns because it flattened the timeout into a sentinel string (Err("attempt timeout")) that callers compared with ==. It now returns a typed error that carries the timestamp:

enum CallError {
    Timeout { sent_ns: u64 },
    /// Transport loss or an unparsable response. The caller cannot act on it.
    Fatal(String),
}

Both affected paths anchor on it before deciding whether to retry:

Err(CallError::Timeout { sent_ns }) => {
    // The attempt reached the wire, so it anchors this request's
    // latency whether or not a retry follows.
    first_send.get_or_insert(sent_ns);
    ...
}

That fixes both symptoms you identified: a terminal timeout no longer reports near-zero logical latency (it was first_send.unwrap_or(now) with now == terminal_ns), and a later successful retry no longer re-anchors latency past the first attempt and the retry delay, because get_or_insert runs on the timeout path before continue.

The poll path already had first_send set by the submission, so it keeps its expect and just documents why.

Comment on lines +1145 to +1149
for vector in vectors {
collected.push(
vector["id"]
.as_str()
.ok_or_else(|| format!("vector omitted id: {json}"))?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate batch vector payloads before marking completion

For batch result pages, this loop validates only each item ID; it never checks content_sha256, vector dimensions or finite values, or the served model/fingerprint/epoch metadata before returning LogicalDisposition::Completed. A host or engine regression can therefore return the expected IDs with corrupted vectors or the wrong lane identity and still contribute to completed rate and latency summaries, unlike the query arm's validate_vectors gate. Validate every page's identity and vector payload before adding it to the completed ledger.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed in f847682. Batch pages now go through a payload gate before joining the ledger, closing the asymmetry with the query arm's validate_vectors.

validate_batch_page checks, per page: lane identity (model, fingerprint, table_epoch, dims), and per item: content_sha256 against the request's own value for that id, plus the vector's length and contents.

Two notes on the shape:

  • done is deliberately not checked there, unlike validate_vectors. It is a paging state rather than a payload property — the caller distinguishes a final page from a continuation, and asserting it inside the validator would reject every non-final page.
  • The expected id -> content_sha256 lookup is built once per logical request rather than per page, so validation does not reintroduce a quadratic scan over items.

An unrequested id is also now rejected explicitly, since the existing exactly-once comparison only inspects the collected order and would not attribute that failure clearly.

Address the remaining review findings on the tail-amplification harness, the
plugin's batch page budget, and the frozen contract's rate equation.

Measurement window. Each repetition now derives a hold window from its
scheduled span and stamps both ledgers before anything is estimated. The
window's first tenth is marked warmup and held out of the ledger, the rates,
and the percentiles while staying in raw evidence, and a request that had not
settled when the window closed is censored as in flight rather than awaited
into a completion that inflates the completed rate. The window boundaries and
the discarded counts are emitted so both decisions are re-derivable.

Open-loop validity. The lag gate sampled the pacer's wake-up delay before the
request task was spawned, so a task that then waited on the runtime queue or
the shared writer still reported zero missed slots. The gate now compares each
request's recorded first send against its intended scheduled start, which is
the quantity the frozen offered rate is defined by.

Logical latency. A first attempt that timed out left `first_send` unset, so a
terminal timeout reported near-zero logical latency and a later retry
re-anchored latency past both the first attempt and the retry delay. The
recorded call now surfaces its send timestamp through a typed error instead of
a sentinel string, and the timeout paths anchor on it.

Batch payload validation. The query arm gates every reply on lane identity and
vector contents, but batch result pages checked only item ids, so a page with
corrupted vectors, a mismatched content hash, or the wrong lane identity could
still reach Completed. Every page is now validated before it joins the ledger,
and the paged path checks the deadline before fetching the next page, which
the pending path already did.

Poll accounting. A logical row claiming zero polls while owning a poll attempt
balanced whenever its attempt total matched, so the emitted poll distribution
could be wrong while the ledger reported valid. The aggregation pass now counts
polls per request and the validator rejects a mismatch.

Waiter admission test. Each waiter in the FIFO test travels its own
connection, and a waiter blocks before the engine, so no counter can witness
its arrival and a single yield need not carry a spawn through write, dispatch,
and semaphore acquisition. The test drains the queue the way the rest of the
suite does so the ordering assertion does not depend on that path's await
count.

Page budget. `pollBatch` re-anchored its deadline from `pageTimeoutMs` after
`embed.batch` had already spent up to that budget, so one page could take twice
the configured page timeout. Submission and polling now share one absolute page
deadline, and a resubmission after `module_restarted` inherits what is left of
it.

Contract. The rate identity equated two rates with three raw counts. It is now
divided through by the post-warmup measured window so every term is a rate, and
names the window the emitted boundaries delimit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f847682aa3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1729 to +1732
logical_estimates
.iter()
.map(|request| request.latency_ns)
.collect(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude censored requests from latency percentiles

When a request settles after the hold-window boundary, HoldWindow::stamp changes its disposition to InFlight but leaves its eventual post-window latency_ns unchanged; this summary then includes that value because it consumes every non-warmup row. Fresh evidence beyond the earlier boundary finding is that the updated censoring code now fixes the completion count but still lets work drained seconds or minutes later enter p50/p95/p99, so overloaded cells can report tail estimates dominated by observations that the ledger simultaneously declares right-censored. Restrict the latency sample to terminal, non-censored rows or report censored durations separately.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 00cd8df.

logical_estimates was every non-warmup row, and stamp rewrites a late row's
disposition to InFlight while leaving its latency_ns measured to whenever
it actually settled — so the value the ledger declared right-censored was still
feeding p50/p95/p99. Worse, the contamination scales with the treatment, since a
saturated cell drains for longer than its control.

Percentiles now cover terminal, non-censored rows only, with is_censored
sharing one definition with censored_count so the reported censoring rate
always describes exactly the rows the percentiles omit.

Measured on a saturated cell (--concurrency 12 --max-waiting-queries 4 --engine-delay-ms 40): offered 73, completed 61, censored_per_mille 164.4,
and logical_latency.count is now 61 rather than 73 — 16% of the old sample was
censored observations.

Comment on lines +1515 to +1517
while Instant::now() < end {
let id = next_id.fetch_add(1, Ordering::Relaxed);
records.push(execute(&worker_ctx, id, None).await);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop closed-loop starts at the wire window boundary

In closed-loop cells, a worker can pass this pre-call time check just before end and then make its actual first wire send after end because dispatch or the shared writer is delayed. HoldWindow::opened_ns correctly identifies that post-window start, but stamp only marks it in flight and the estimate partition removes only warmup rows, so these requests still inflate offered counts, amplification, and censoring despite never starting in the measured window. Gate the actual send against the boundary or exclude rows whose opened_ns >= window.end_ns from estimates.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 00cd8df.

The closed-loop worker tests Instant::now() < end before it dispatches, so it
can pass the test and still land its first wire send after the window closed.
opened_ns identified that instant correctly, but nothing excluded the row:
stamp only set warmup and the partition dropped warmup alone, so such a
request still counted toward offered, amplification, and censoring.

Rather than add a second exclusion flag beside warmup, LogicalRecord and
AttemptRecord now carry one window: WindowClass of Warmup / Measured /
AfterWindow, and only Measured enters estimates. Every consumer asks the same
question — is this row in the measured set — and one class cannot encode the
contradictory answers two independent booleans can.

Two details worth flagging:

  • The censoring rewrite now applies only to measured rows. An after-window row is
    excluded outright, so rewriting its disposition would destroy a true outcome
    from raw evidence and change no estimate; it also keeps
    in_flight_at_window_end meaning exactly "the measured window closed on this
    request".
  • An attempt whose logical row is missing is classified AfterWindow rather than
    inheriting the constructed default, so an unattributable row cannot reach
    estimates while the ledger validator reports the orphan.

Covered by the_hold_window_marks_warmup_and_censors_unsettled_requests (both
boundaries half-open, plus the after-window row keeping its outcome) and a new
an_orphan_attempt_cannot_enter_the_measured_set.

I did not manage to make after_window_offered non-zero in a smoke run — it
needs dispatch delayed past the boundary — so the classification is proven by
unit test rather than by observation here.

Comment on lines +1593 to +1595
if (this.now() + delay >= deadlineAtMs) throw classified;
attempt += 1;
await wait(delay);
await this.sleep(delay);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Race retry sleeps with request cancellation

If the caller aborts while a retry delay is sleeping, this unconditional await does not observe the AbortSignal until the entire delay finishes. The expanded queue_full policy can now retry 64 times and jitter a 2-second served or fallback delay to nearly 6 seconds, so an aborted embedding or batch operation can remain pending for several seconds before returning and can delay shutdown or replacement work. Make the delay abortable, while preserving the injectable sleep path used by tests.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 00cd8df.

The abort was checked at the top of the retry loop but not during the wait, so an
aborted caller was held for the full delay — and with the widened queue_full
ladder that is seconds, not milliseconds. The same shape was in the two poll
pending delays, so all three now go through one delay(ms, signal) helper that
races the injected sleep against the signal. The injectable seam is preserved:
sleep still drives the timing, so tests that supply their own keep full control.

Worth recording, because it is the same class of bug you reported: my first
version of the helper had a race of its own. It called this.sleep(ms) as the
first element of Promise.race and registered the abort listener in the second,
and array elements evaluate left to right — so an abort that fired while the
sleep was starting was missed by the { once: true } listener entirely. The new
test hung instead of passing, which is how I caught it. The listener is now
registered first and signal.aborted is re-tested inside the executor, so an
abort landing between the check and the registration cannot be lost.

New test ends a retry delay as soon as the caller aborts: the injected sleep
never settles, so only the abort can end the race — a delay that ignored the
signal hangs the test rather than returning a wrong value. Asserts the second
attempt is never dispatched.

Comment thread crates/mc-host/src/synapse/mod.rs Outdated
Comment on lines +593 to +595
tokio::task::yield_now().await;
match rx.try_recv() {
Ok(result) => result,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject inference results received after the query deadline

When the deadline arm wins for a running query, this yield creates a new unbounded scheduling interval before try_recv; if inference completes during that interval, Ok(result) is accepted and returned as a successful response even though the deadline already expired. Under runtime saturation the yield can last materially longer than one scheduler turn, breaking the request deadline precisely in the tail conditions this change targets. After the deadline arm fires, only consume the worker's queued-timeout verdict needed for message attribution; do not accept a late successful engine result.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 00cd8df.

Traced it: once the worker has taken the CPU permit its own deadline arm is gone,
so spawn_blocking runs to completion regardless. If it finished inside the
yield_now() interval, try_recv returned Ok(vectors) and the handler answered
successfully — after the caller's deadline had passed. And as you note, the yield
is an unbounded scheduling interval under saturation, so the window is widest in
exactly the tail conditions this PR targets.

The deadline arm now consumes only the worker's queued-timeout verdict, and only
for attribution:

return match rx.try_recv() {
    Ok(Err(QueryFault::Timeout)) => {
        app_error("timeout", "the query deadline expired while queued")
    }
    _ => app_error("timeout", "the query deadline expired awaiting the result"),
};

The arm no longer has any path that returns vectors, so this is checkable by
reading the match rather than by winning the race. A late Invariant fault loses
nothing: settle_inference already marked the lane failing before sending, so the
next request sees artifact_invalid regardless.

I did not add a test for it. Hitting the accepting path deterministically needs a
seam controlling when the worker sends, which does not exist; the existing
expired_waiter_releases_its_slot_without_engine_work and the queued-timeout
message assertion in synapse_protocol.rs both still pass, and the message
attribution is unchanged on every reachable path.

Also documented in the wire protocol: a deadline that expires while the engine
call is already running fails as timeout, and a vector produced after the
deadline is discarded rather than returned.

Comment thread crates/mc-host/examples/synapse_perf.rs Outdated
Comment on lines +1761 to +1764
let service_samples = service_ns.lock().expect("service samples").clone();
let service_time = LatencySummary::from_unsorted(service_samples.clone());
let service_time_mean_ns = mean(&service_samples);
let service_time_cv = coefficient_of_variation(&service_samples);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove warmup engine calls from service-time estimates

The engine sample buffer is cleared before the load but has no timestamps, logical IDs, or warmup markers, and this code summarizes the entire buffer unchanged. Consequently calls executed during the frozen first 10% warmup—and calls drained after the hold boundary—enter mean S, CV, capacity, and service-time percentiles even though logical and attempt estimates exclude that warmup cohort. Because the emitted raw service rows also lack enough attribution to repair this downstream, record service samples with their owning logical request or window timestamp and partition them before computing these estimates.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 00cd8df.

You were right that the buffer clear only removes the untimed warm() calls —
the in-window warmup prefix and any post-boundary drain were still summarized —
and right that the emitted rows could not repair it downstream, since they were
bare durations with only an array index.

DelayEngine now records ServiceSample { started_ns, service_ns, window } on
the wire clock, so the same HoldWindow::classify that partitions logical rows
partitions service samples, and S, its CV, and the service percentiles are all
computed over the measured cohort only. Sharing the clock required hoisting
origin above engine construction; every timestamp is window-relative, so the
extra startup offset is common to all of them and cancels.

The raw synapse_perf_service rows now carry the whole sample, so the partition
is re-derivable from evidence, and the summary reports
service_measured_samples / service_excluded_samples.

Smoke run (--concurrency 12 --engine-delay-ms 40 --seconds 3):
service_excluded_samples 19, matching warmup_offered 19 exactly — those 19
engine calls were previously in mean S and the capacity estimate while the
logical and attempt ledgers excluded the very same cohort.

Comment thread crates/mc-host/examples/synapse_perf.rs Outdated
Comment on lines +1699 to +1701
let task_after =
process_resources::observe_tasks(std::process::id()).map_err(|error| error.to_string())?;
let task_deltas = process_resources::task_deltas(&task_before, &task_after);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Measure task counters only over the post-warmup hold window

The before snapshot is taken at the start of run_load, while this after snapshot is taken only after every logical task has drained. The resulting CPU and context-switch deltas therefore include the discarded first 10% warmup and, for outstanding batch/query work, potentially seconds or minutes after the hold-window boundary; overloaded variants receive more extra accounting time than controls. This corrupts the resource-shift and no-busy-poll evidence, so take snapshots at warmup_end_ns and hold_window_end_ns rather than around the full generation-and-drain interval.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 00cd8df.

The snapshots did bracket the whole generate-and-drain interval, and your point
about the bias direction is the load-bearing one: the extra accounting time is
not noise, it tracks the treatment, because the overloaded arm drains longest.

run_load now spawns observe_task_window, which sleeps to warmup_end and to
end — both derived from the HoldWindow itself rather than recomputed from
seconds, so the observation boundaries cannot drift from the boundaries the
estimates are partitioned by.

Two things I added beyond the report:

  • A saturated harness can overshoot a boundary, so the instants the observations
    actually landed on are emitted as task_window_start_ns /
    task_window_end_ns. The covered span is auditable instead of assumed. In the
    smoke run they landed 0.49ms and 0.87ms past their boundaries.
  • A failed observation leaves task_deltas absent and records a fatal error,
    rather than degrading to a wider span. The counters are evidence for the
    no-busy-poll claim, so a sample covering a different interval in each arm is
    worse than no sample.

deltas and the span they cover are one TaskWindow value rather than two
independently-optional fields, so they cannot disagree.

Comment on lines 268 to 270
cpu: Arc::new(tokio::sync::Semaphore::new(1)),
query_admission: Arc::new(tokio::sync::Semaphore::new(1)),
query_admission: Arc::new(tokio::sync::Semaphore::new(query_admission_permits)),
tracker: TaskTracker::new(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve FIFO order for admitted query waiters

With max_waiting_queries > 0, increasing this semaphore only reserves a bounded number of query slots; it does not establish their order. Each admitted handler subsequently spawns an independent tracker task, and Tokio's one-permit CPU semaphore orders those tasks by when they are first polled, so a later admitted query can register before and overtake an earlier one under concurrent scheduling. This violates the documented FIFO waiter behavior and makes per-request tail latency nondeterministic; enqueue admitted queries through an explicit ordered queue or ticket before they contend for the CPU permit.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified the mechanism — it is real — but declining the ordered queue and fixing
the documentation instead. Reasoning, since this is a disagreement about the
claim rather than the code:

Admission is try_acquire_owned, i.e. non-blocking, so it only decides whether
a query may wait; ordering happens later, when each spawned worker registers on
the one-permit cpu semaphore. Tokio's semaphore is fair — permits are handed
out in the order they were requested — so the discipline is FIFO over
registrations, and the first-poll order of independently spawned tasks is the
scheduler's to choose. That much is exactly as you describe.

Where I disagree is that this violates a documented guarantee:

  1. An admission-time ticket would not be a stronger promise. Admission runs in
    the handler task, so the ticket order is itself decided by when each handler
    happened to be polled. Concurrent in-flight requests have no wire-level total
    order, so there is no ground truth for a new queue to enforce — it would put a
    second queue in front of an already-fair semaphore and still order by
    scheduler chance, at the cost of touching the query path's core concurrency.
  2. What the FIFO claim is load-bearing for is bounding a waiter's wait and ruling
    out starvation, which registration-order fairness already provides.
    mixed_batch_and_query_waiters_share_fifo_cpu_without_starvation is the test
    that covers it.

So the defect is the doc over-claiming, and that is fixed in 00cd8df. The cpu
and query_admission field docs now say waiters are served in registration
order, that this bounds the wait and rules out starvation, and that it is
explicitly not a claim that it matches admission order. The wire protocol says
the same and drops the bare "FIFO waiters".

Supporting evidence that registration-order was the intended contract all along:
bounded_query_waiters_are_fifo_and_reject_bound_plus_one already documents this
distinction in its own comment — "admission order follows when its task reaches
the socket rather than the spawn order here" — and drains the queue between
spawns precisely so its assertion does not depend on how many await points the
write/dispatch/acquire path contains.

Happy to reopen if you have a caller that needs admission-order service; it would
need a wire-level ordering guarantee to be defined first.

Six review findings shared one root cause on the measurement side: the
frozen hold window was applied to logical dispositions but not to the
observations built on top of them, so the discarded warmup prefix and any
post-window drain leaked into estimates. The leak is correlated with the
treatment, because an overloaded cell drains for longer than its control.

Replace the `warmup` boolean on logical and attempt records with a single
`window` class of warmup, measured, or after-window. One field rather than a
second exclusion flag: every consumer asks whether a row is in the measured
set, and one class cannot encode the contradictory answers two independent
booleans can. The new after-window class closes a real gap — a closed-loop
worker tests the boundary before it dispatches, so it can pass the test and
still reach the wire after the window has closed, which previously counted
as offered work.

Scope the remaining estimates to that class:

- Latency percentiles now cover terminal, non-censored requests. A censored
  row's duration runs to whenever it actually settled, so admitting it let
  the reported tail be dominated by observations the same ledger declares
  right-censored. The censoring rate is reported beside the percentiles.
- Engine service samples carry the instant the call began, so the same
  classification partitions them. Without a timestamp, calls made under the
  warmup prefix entered mean S, its coefficient of variation, and the
  capacity estimate while the ledgers excluded that cohort.
- Task counters are observed at the window's own boundaries instead of
  around the whole generate-and-drain interval. The instants the two
  observations landed on are emitted, so the covered span is auditable
  rather than assumed, and the deltas are absent rather than widened when
  an observation fails.

Two host-side fixes:

- A query whose deadline expires no longer returns a vector that arrived
  late. The yield after the deadline arm exists only to let the worker's
  same-instant queued verdict land for attribution; it is an unbounded
  scheduling interval under saturation, and any value landing in it was
  accepted, so an engine call finishing late answered a request whose
  budget had already expired.
- Retry and poll delays observe the caller's abort signal. A queue_full
  ladder can wait seconds, and an aborted caller was held for the full
  delay. The injectable sleep seam is preserved.

Correct the FIFO claim in both the field docs and the wire protocol. The
CPU semaphore serves waiters in the order they register, which bounds each
wait and rules out starvation; admission is a non-blocking count that
decides whether a query may wait, never where it lands. Concurrent
requests have no wire-level total order for service order to match.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 00cd8df174

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +449 to +452
attempt.window = classes
.get(&attempt.logical_id)
.copied()
.unwrap_or(WindowClass::AfterWindow);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Classify attempts from their own send timestamps

Fresh evidence after the earlier window-marker fix is that stamp assigns every attempt the owning logical request's class instead of classifying actual_send_ns, contrary to the per-observation window rule in docs/perf/synapse-tail-contract.md:114-120. When a measured request continues retrying or polling after hold_window_end_ns—potentially for the 120-second batch deadline—those post-window calls still enter admitted/rejected rates, attempt latency, and amplification; conversely, attempts sent after warmup by a warmup request are discarded. Classify each attempt using its own send timestamp while retaining the parent ID for attribution.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on the inconsistency — but the doc is what is wrong here, not the
classification. Fixed in be17726.

You are right that stamp assigns the owning request's class rather than
classifying actual_send_ns, and right that this contradicts the paragraph I
added at synapse-tail-contract.md:114-120. That sentence claims per-observation
classification for attempts, which was sloppy of me: it is true of logical
requests and service samples, not of attempts.

Declining the prescription, because classifying attempts independently breaks the
identity the contract makes authoritative. validate_synapse_ledgers checks
reconciliation per request, not just in aggregate:

if actual != request.attempts {
    errors.push(format!(
        "logical {} records {} attempts but owns {actual}", ...
    ));
}

So for a measured request still polling past hold_window_end_ns — your own
example, a batch on its 120s deadline — the post-window attempt rows would leave
attempt_estimates while the logical row counting them stayed in
logical_estimates, and the validator would report logical N records 3 attempts but owns 1. ledger.valid gates the repetition, so an ordinary saturated
repetition would be declared inadmissible. That is the failure mode the change
would actually produce.

The amplification argument also points the other way from the report. A is
attempts per logical request, so numerator and denominator have to cover the same
requests; truncating a censored request's attempts at the boundary understates
the amplification this study exists to measure, rather than removing an inflation.

What I did instead:

  • The contract now separates the two rules explicitly — requests and service
    samples by their own opening instant, attempts by ownership — and gives the
    reason plus the consequence in plain terms: a measured request's post-window
    wire calls do enter the admitted and rejected counts, attempt latency, and A,
    as calls that request actually cost, and a warmup request's later attempts are
    discarded with it. That second half is your "conversely" case, now stated as
    intended behavior rather than left implicit.
  • AttemptRecord::window and the stamp loop carry the same rationale, so the
    next reader does not have to infer it from the validator.
  • New test attempts_follow_their_request_so_the_per_request_ledger_reconciles
    pins it from both sides: a measured request whose two polls land after the
    window end keeps them, a warmup request's post-warmup poll is discarded with
    it, and the per-attempt alternative is asserted to fail validation with
    records 3 attempts but owns 1 rather than quietly shrinking the ledger.

If you think per-attempt classification is still right, the thing to resolve
first is what should happen to the per-request reconciliation check — that is the
constraint that decides it.

The window-class paragraph said every logical request, attempt, and service
sample is classified against the frozen boundaries. That is true of requests
and service samples but not of attempts, which take the class of the request
that owns them, so the contract described a rule the code does not implement.

Ownership is the correct rule, not a shortcut. An attempt is one wire call of
a logical request rather than an independent observation, and validation
rejects a repetition whose logical row disagrees with the attempts it owns.
Classifying by `actual_send_ns` would drop the post-window attempts of any
measured request that keeps retrying or polling past the window end while the
logical row counting them stayed, so an ordinary saturated repetition would
be reported inadmissible. It would also understate amplification, which needs
attempts and requests measured over the same set.

Say so in the contract, in the field docs, and in `stamp`, including the
consequence that follows: a measured request's post-window wire calls do enter
the admitted and rejected counts, attempt latency, and `A`, and a warmup
request's later attempts are discarded with it.

Pin the decision with a test covering both directions of the ownership rule
and asserting that the per-attempt alternative fails validation with
"records 3 attempts but owns 1" rather than quietly shrinking the ledger.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: be17726878

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +587 to +590
biased;
result = &mut rx => match result {
Err(_) => return app_error("internal_error", "the inference task was lost"),
Ok(result) => result,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prefer the deadline over a simultaneously ready result

Under runtime saturation, the handler can remain descheduled while an already-running inference crosses its deadline and sends a result; when the handler resumes, both rx and the deadline timer are ready, and this biased selection always chooses rx, returning the late result as a success. Fresh evidence beyond the earlier yield-path fix is that the receiver-first ordering still bypasses the deadline without entering that fixed arm. Check the deadline before accepting a received result or otherwise make the expired timer authoritative.

Useful? React with 👍 / 👎.

Comment on lines +999 to +1001
let (reply, json) = match ctx
.record_call(logical_id, SynapseMethod::Batch, body.clone(), deadline)
.await

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recheck the batch deadline before each retry send

When a timed-out or queue_full batch submission schedules a retry, Tokio may wake the retry sleep after BATCH_DEADLINE; this loop then enters record_call without another deadline check. record_call passes a zero duration to RoutedWire::call, but that function writes the frame before timing out the receiver, so saturated cells can send and record an extra post-deadline submission, unlike the plugin's per-attempt remaining-budget guard. Check the deadline at the top of the submission loop before issuing the next wire call.

Useful? React with 👍 / 👎.

Comment on lines +1828 to +1829
let origin = Instant::now();
let wire = RoutedWire::connect(&publication, origin).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep engine and wire timestamps on the same origin

This second origin is created after host startup, while DelayEngine retained the earlier origin created before startup. Consequently every service sample's started_ns includes the startup interval but HoldWindow boundaries do not, so window.classify(sample.started_ns) admits part of the logical warmup and excludes an equal tail interval from service-time estimates. Fresh evidence beyond the earlier service-window fix is that the new timestamps are present but expressed on different clocks; pass the original origin to RoutedWire instead of shadowing it.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant