diff --git a/packages/agent/docs/mobile-handoff/01-harness/01-delta/FINDINGS.md b/packages/agent/docs/mobile-handoff/01-harness/01-delta/FINDINGS.md index f913c11cd..396aadcc7 100644 --- a/packages/agent/docs/mobile-handoff/01-harness/01-delta/FINDINGS.md +++ b/packages/agent/docs/mobile-handoff/01-harness/01-delta/FINDINGS.md @@ -1,6 +1,6 @@ # 01-delta: known defects and measured findings -Everything here was found **after** `delta.md` and the prototype implementation beside it were written. None of it is applied to the code in this directory. Production now lives in `packages/chord/src/delta/index.ts`: its flush-time dirty-tree design resolves D1, while D2 remains relevant to rolling-window producers that assign sliced strings, but its profile predates flush-time tracking and must be re-measured against production Chord. Each item below remains the original problem, evidence, and candidate-fix record. +Everything here was found **after** `delta.md` and the prototype implementation beside it were written. None of it is applied to the code in this directory. Production now lives in `packages/chord/src/delta/index.ts`: flush-time dirty tracking resolves D1, and production remeasurement closes D2 without a new API. See [`append-decision.md`](append-decision.md). Each item below remains the original problem, evidence, and candidate-fix record. Every number was measured with **`node --experimental-strip-types`**, not `tsx`. That matters: tsx transpiles through esbuild and inflated the same benchmark by @@ -88,9 +88,9 @@ writes. --- -## D2. `overlap()` is 94.5% of tracker time +## D2. `overlap()` was 94.5% of prototype tracker time — closed -**Severity: high.** Everything else in this file is noise beside it. +**Historical severity: high. Production decision:** do not add an explicit append/truncate API. The prototype profile was dominated by a slow `startsWith` path; production uses a flattened-slice comparison and measured 17.8–18.7 µs per 200 KB assistant append flush and 2.43–2.46 µs per 50 KB rolling-window flush locally. See [`append-decision.md`](append-decision.md). The original evidence follows. ### Problem @@ -166,7 +166,7 @@ benchmarks disagree in a way that points at path depth: cache key by joining the path, so a read of `content[0].text` allocates on every access. -**Re-profile after D2 lands.** This may be the next bottleneck or it may be +**Re-profile only if production delta cost becomes material.** This may be the next bottleneck or it may be nothing; the current numbers cannot distinguish them because `overlap` swamps both. --- @@ -179,8 +179,7 @@ profile above. **A path trie replacing `JSON.stringify(path)` keys.** Genuinely faster in isolation — 6x on the `LaneSnapshot` shape, 93x at 500 distinct paths — because stringify cost scales with path depth while a trie walk is one `Map.get` per -segment. But the whole slot layer is 0.5% of runtime. Build it only if a profile -after D2 says so. +segment. But the whole slot layer is 0.5% of runtime. Build it only if a current production profile says so. **Trie as storage vs trie as index.** If a trie is ever built, it should map path to a **position in the ops array**, not hold the ops. Holding them loses first-touch diff --git a/packages/agent/docs/mobile-handoff/01-harness/01-delta/append-decision.md b/packages/agent/docs/mobile-handoff/01-harness/01-delta/append-decision.md new file mode 100644 index 000000000..ff29bbfeb --- /dev/null +++ b/packages/agent/docs/mobile-handoff/01-harness/01-delta/append-decision.md @@ -0,0 +1,39 @@ +# Decision: no explicit text append/truncate API + +**Status: closed.** Do not add `appendText`, explicit truncate, a text-specific dirty-node kind, or proxy-to-tracker lookup machinery. + +## Why it was considered + +The tracker marks dirty paths and compares them with its accepted baseline at `flush()`. For a growing or rolling string, confirming the relationship can scan the retained string even though the producer already knows it appended or evicted text. + +The original benchmark made this look expensive because its append fast path used `after.startsWith(before)`. V8 walked the producer's cons string character by character. Production Chord now uses: + +```ts +if (after.length > before.length && after.slice(0, before.length) === before) { + // emit append +} +``` + +The slice flattens once and comparison uses the native string path. Flush-time dirty tracking also removed the prototype's retained-op amplification. + +## Local confirmation + +Measured 2026-09-01 against `origin/dev` at `1a7bc80e7`, using `packages/chord/src/delta/index.ts` directly under Node 26.0.0 on an Apple M5 Max. Each workload used 3,000 warmups followed by 11 samples of 10,000 mutation-plus-flush iterations; a second process reproduced the result. + +| Workload | Median µs/flush, run 1 | Median µs/flush, run 2 | +| --- | ---: | ---: | +| 200 KB assistant string, append 8 characters | 18.68 | 17.81 | +| 50 KB rolling window, slide 32 varied characters | 2.46 | 2.43 | +| Transcript push, one small entry | 0.74 | 0.78 | + +The assistant tracker started with 200,000 varied characters, appended `" abcdef"`, and flushed each append. The rolling tracker started with 50,000 varied characters, then assigned `text.slice(32) + chunk` using a distinct 32-character `chunk::durable-stream` value on every flush; every flush was asserted to emit `t` + `a`. The transcript tracker pushed `{ id: "e", text: "message " }` and asserted one `p` per flush. Setup and garbage collection were outside each timed loop. + +At 100 assistant updates per second, the growing-string case consumes about 1.8 ms/s, approximately 0.18% of one core. The measured rolling-window cost is itself too small to justify the abandoned explicit API. + +## Why the API is rejected + +The prototype required a text-specific dirty-node state, proxy-to-tracker lookup, repeated-append/drop folding, and interaction rules for later whole-value replacement. It produced subtle silent failures: one implementation fell through to the ordinary differ while tests still passed, and another drifted when drops reached into earlier appends. + +That complexity does not justify saving microseconds below the surrounding replication, isolation, and rendering costs. Keep ordinary string mutation and the generic Chord op vocabulary. + +Reopen only when a production profile shows delta flush time is a meaningful fraction of a real workload. Re-measure the generic fast path first; a regression there is cheaper and safer to fix than adding a producer-specific API. diff --git a/packages/agent/docs/mobile-handoff/01-harness/01-delta/delta.md b/packages/agent/docs/mobile-handoff/01-harness/01-delta/delta.md index 5ba7ab8f5..de102e539 100644 --- a/packages/agent/docs/mobile-handoff/01-harness/01-delta/delta.md +++ b/packages/agent/docs/mobile-handoff/01-harness/01-delta/delta.md @@ -2,7 +2,7 @@ > **Production status:** landed in `packages/chord/src/delta/index.ts`, with tests in `packages/chord/test/delta.test.ts`. Chord owns the dependency-free `Op`/`WireOp`, tracker, applier, codec, and validation boundary; Session storage, the Harness, and facets consume it. The implementation and tests beside this document are historical prototype and benchmark evidence, not production source. > -> The landed tracker computes deltas at `flush()` from a dirty tree and baseline rather than retaining one op per mutation. That resolves FINDINGS D1. FINDINGS D2 remains relevant to producers that repeatedly assign sliced rolling strings, but its profile predates flush-time tracking; re-measure it against production Chord and add an explicit append/truncate producer API before the tool-output hot path only if it remains hot. +> The landed tracker computes deltas at `flush()` from a dirty tree and baseline rather than retaining one op per mutation. That resolves FINDINGS D1. Production remeasurement also closes FINDINGS D2: the generic string path is below surrounding replication/rendering costs, so an explicit append/truncate API is rejected ([decision](append-decision.md)). One mechanism covers assistant partials, tool output, tool details, lane state, and arbitrary facet state — on the wire and in durable storage. @@ -262,7 +262,7 @@ Two callers need this: - `sort` / `reverse` / `fill` / `copyWithin` mark the array dirty and emit the resulting structural/index changes rather than preserving the producer's method intent. Add a dedicated op only if a measured workload needs one. - A manual index-shift loop (`for (…) a[i] = a[i+1]`) can still cost O(n) sets. Correct, not minimal, and unavoidable — the producer genuinely wrote every element. -- Rolling-window string assignment still runs overlap discovery at flush. Re-measure FINDINGS D2 against the landed tracker, then add an explicit append/truncate producer API if it remains hot. +- Rolling-window string assignment still runs overlap discovery at flush, deliberately: production measurements do not justify text-specific tracker state or API surface ([decision](append-decision.md)). ### 3.4 Constraints the string algorithm must hold @@ -608,7 +608,6 @@ the beginning. - Prefix interning (a trie over path heads) if a workload emerges with many distinct paths sharing long prefixes. Second-use interning (§4) removes the pathological case; this would go further. -- Re-measure rolling-window overlap under flush-time tracking (FINDINGS D2); if it remains hot, add an explicit append/truncate producer API so the producer can state what changed. - Whether array reordering needs a dedicated op; the landed tracker currently marks the array dirty and emits the resulting structural/index changes. - Cross-language replicas: the applier is a page of code in any language, but the interning tables and the wire framing are not specified for a non-JS consumer. diff --git a/packages/agent/docs/mobile-handoff/01-harness/04-tool-output/bash-worked-example.md b/packages/agent/docs/mobile-handoff/01-harness/04-tool-output/bash-worked-example.md index 964a77dfa..0cf268866 100644 --- a/packages/agent/docs/mobile-handoff/01-harness/04-tool-output/bash-worked-example.md +++ b/packages/agent/docs/mobile-handoff/01-harness/04-tool-output/bash-worked-example.md @@ -107,7 +107,7 @@ consecutive ops on it omit the id entirely: [["t",0,4096],["a","cc -c src/z.c\n"],["s",["truncation","totalBytes"],262144]] ``` -Details are one `s`, in one flush out of twenty. The `t` + `a` pair is the window slide. The current tracker recovers it by verified overlap; delta FINDINGS D2 requires re-measurement against production Chord before this becomes a hot tool-output loop; add an explicit append/truncate producer path if overlap remains dominant. +Details are one `s`, in one flush out of twenty. The `t` + `a` pair is the window slide. The current tracker recovers it by verified overlap; production remeasurement shows the generic path is already negligible, so no explicit append/truncate producer API is added ([decision](../01-delta/append-decision.md)). ## 5. Harness events diff --git a/packages/agent/docs/mobile-handoff/01-harness/04-tool-output/harness-tools.md b/packages/agent/docs/mobile-handoff/01-harness/04-tool-output/harness-tools.md index e5d040caa..f58432e84 100644 --- a/packages/agent/docs/mobile-handoff/01-harness/04-tool-output/harness-tools.md +++ b/packages/agent/docs/mobile-handoff/01-harness/04-tool-output/harness-tools.md @@ -351,7 +351,7 @@ writes: [appendList(address, wire, isBase(ops) ? "base" : undefined)]; `isBase` comes from Chord. It checks for the root replacement op `r`; ordinary nested sets use `s`. Classification lives beside the vocabulary so the comparison is written once. -The landed tracker emits structural ops unconditionally. There is no serialized-size comparison or adaptive replacement heuristic. A producer requests a base batch explicitly with `rebase()`; the output sink's cap bounds that replacement, while periodic rebasing bounds recovery work. Before the rolling tool-output hot path lands, re-measure delta FINDINGS D2 against production Chord; if overlap discovery remains hot, add an explicit append/truncate producer API. +The landed tracker emits structural ops unconditionally. There is no serialized-size comparison or adaptive replacement heuristic. A producer requests a base batch explicitly with `rebase()`; the output sink's cap bounds that replacement, while periodic rebasing bounds recovery work. Production remeasurement rejects a text-specific append/truncate API: the generic path measured 2.43–2.46 µs per 50 KB rolling-window flush locally, below surrounding costs. Keep ordinary tracked string mutation; see the [decision record](../01-delta/append-decision.md). **Checkpointing is not deleted.** An earlier draft called for removing `BASH_CHECKPOINT_INTERVAL_MS`; that was wrong. Checkpoint frequency is the knob diff --git a/packages/agent/docs/mobile-handoff/README.md b/packages/agent/docs/mobile-handoff/README.md index 000e35ded..2ba287c69 100644 --- a/packages/agent/docs/mobile-handoff/README.md +++ b/packages/agent/docs/mobile-handoff/README.md @@ -22,7 +22,7 @@ Base everything on a clean checkout of `origin/dev`. **Three units ship working code. Four are specifications.** The table above says which. Do not assume a doc describes something that exists. -**`01-delta/FINDINGS.md` is not optional.** The production implementation now lives in `packages/chord/src/delta/index.ts`. Its flush-time dirty-tree design fixes D1 (interleaved paths no longer retain one op per write); D2 remains relevant to rolling-window producers that assign sliced strings, but its 94.5% figure predates flush-time tracking. Re-measure it against production Chord and add an explicit append/truncate producer API before `04-tool-output` if it remains hot. The code beside the handoff is historical prototype evidence, not production source. +**`01-delta/FINDINGS.md` is historical evidence, not an implementation queue.** Production lives in `packages/chord/src/delta/index.ts`: flush-time dirty tracking fixed D1, and production remeasurement closed D2. The explicit append/truncate API is rejected; see [`01-harness/01-delta/append-decision.md`](01-harness/01-delta/append-decision.md). The code beside the handoff remains prototype and benchmark evidence. **If a doc and the code disagree, the code wins** — fix the doc and say so in the commit. @@ -39,7 +39,7 @@ five more measurement traps, each of which produced a confident wrong conclusion | unit | ships | state | | --- | --- | --- | -| **01-delta** | production implementation and tests in `packages/chord`; prototype evidence here | landed; D1 fixed by flush-time tracking, D2 remains for rolling-window producers | +| **01-delta** | production implementation and tests in `packages/chord`; prototype evidence here | landed; D1 fixed, explicit text API rejected after production remeasurement | | **02-scopes** | spec + `scopes.variance.ts` | spec aligned to code; not implemented. The variance check compiles. | | **03-execenv** | impl, rewritten bash, 2 diffs, spec | ran green (29 tests). **Its checkpoint policy is wrong** — see `04-tool-output/rate-limiting.md` §5 | | **04-tool-output** | spec + design notes | **not built.** The piece every measurement of the op encoding depends on | @@ -49,11 +49,10 @@ five more measurement traps, each of which produced a confident wrong conclusion ## Suggested order -1. **`01-delta` D2** — re-measure against production Chord; if it remains hot, add the explicit append/truncate producer API before the rolling tool-output path depends on it. -2. **`03-execenv`** — land the shipped code, minus the checkpoint policy and the two redundant knobs (`rate-limiting.md` §5). -3. **`02-scopes`** — the compiler finds the call sites for you once `Write` is invariant. -4. **`04-tool-output`** — decide the cadence question first (`rate-limiting.md` §4.3 lists what is undecided), then build. -5. **05**, then **02-plugins**. +1. **`03-execenv`** — land the shipped code, minus the checkpoint policy and the two redundant knobs (`rate-limiting.md` §5). +2. **`02-scopes`** — the compiler finds the call sites for you once `Write` is invariant. +3. **`04-tool-output`** — decide the cadence question first (`rate-limiting.md` §4.3 lists what is undecided), then build. +4. **05**, then **02-plugins**. ## Live bugs on `origin/dev`, independent of this design