diff --git a/CHANGELOG.md b/CHANGELOG.md index 90cb5e94..0f2678cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,7 +68,7 @@ ### Architecture - **ADR 0013** — persist the `beta1m` fact, derive the session window (expert-panel design: Kleppmann/Hickey/Helland/Bailis). -- **ADR 0010 rewritten by #350** — per-conversation coreHash ownership replaces per-turn early-exit. +- **ADR 0010 rewritten by #350** — per-conversation coreHash ownership replaces the per-turn early-exit in the **batch** path (`wfInferLanes`) only; the live path and turn list keep the per-turn form. ## 2.0.0 diff --git a/CLAUDE.md b/CLAUDE.md index 3243ae43..004cf2af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,7 @@ These constraints are enforced by structural encapsulation where possible, and g - **Use `_wfIsMainLane(lane)` for main/orchestrator detection, never `!lane.spawnParent`** (`spawnParent` is always `null`, in every lane object, everywhere) — @docs/decisions/0007-wf-is-main-lane-not-spawn-parent.md - **Temporal overlap overrides agentKey for lane placement** — the main lane is strictly serial (no temporally-overlapping turns); same-convId overlap turns (jitter/re-send/rewind) ride main's `overlapEntries` as event markers, never a separate lane (#364); different-convId overlaps still create `parallel-*`/`agent-*` lanes; null-convId non-main lanes retain strict no-overlap split. `_wfSeqRetroMove` must rebuild `mainConvIds` and migrate stale markers after every retro-move — @docs/decisions/0008-temporal-overlap-overrides-agent-key.md - **Sequential-interleave classification goes through the shared seq tracker in both files** (`wfCreateSeqTracker` in `workflow-timeline.js`; instances in `wfInferLanes`, `wfAddEntry`, and `entry-rendering.js` `addEntry`), and the tracker never consults `isCompacted` (fan-out first-turns carry a false flag) — @docs/decisions/0009-sequential-interleave-conv-bracketing.md -- **coreHash lane ownership is per-CONVERSATION, never per-turn** — in `wfInferLanes` a main-agentKey turn is a subagent iff NONE of its conversation's *plurality-tied* coreHashes (`_wfComputeConvIdentity`, `convMaxHashes` = the hashes sharing the max count) is in `mainCoreHashSet` (the seed conversation's dominant only — routing is per-conversation, so a mixed `/model` conversation already rides main as a unit; adding a main conversation's *secondary* coreHashes over-merges a separate subagent sharing that hash — real c6e1ddaa). A lone seed/blip turn carrying main's coreHash is a minority so it cannot flip its conversation into main (add88512); a genuine plurality tie resolves toward main-membership so a `/model`-switching compaction successor is not ejected. Seed = the earliest-starting main-agentKey CONVERSATION that has a coreHash (not the earliest coreHash-bearing turn — else a subagent could seize the seed when main opens coreHash-less). This REWRITES ADR 0010's per-turn early-exit for the batch path; the live `wfAddEntry` + turn-list still use the per-turn form pending A2 (#350) — @docs/decisions/0010-corehash-identity-routing.md +- **coreHash lane ownership is per-CONVERSATION, never per-turn** — in `wfInferLanes` a main-agentKey turn is a subagent iff NONE of its conversation's *plurality-tied* coreHashes (`_wfComputeConvIdentity`, `convMaxHashes` = the hashes sharing the max count) is in `mainCoreHashSet` (the seed conversation's dominant only — routing is per-conversation, so a mixed `/model` conversation already rides main as a unit; adding a main conversation's *secondary* coreHashes over-merges a separate subagent sharing that hash — real c6e1ddaa). A lone seed/blip turn carrying main's coreHash is a minority so it cannot flip its conversation into main (add88512); a genuine plurality tie resolves toward main-membership so a `/model`-switching compaction successor is not ejected. Seed = the earliest-starting main-agentKey CONVERSATION that has a coreHash (not the earliest coreHash-bearing turn — else a subagent could seize the seed when main opens coreHash-less). This REWRITES ADR 0010's per-turn early-exit for the batch path; the live `wfAddEntry` + turn-list keep the per-turn form as an accepted bounded divergence (no open issue tracks a mirror) — @docs/decisions/0010-corehash-identity-routing.md - **context% denominator: display folds a per-session window, classification never does** — display sites (session card, its window label, timeline minimap, swimlane) divide by `sessionCtxWindow(sid)` (or the `_wfWinByTurn` lane fold): 1M if any main turn has `beta1m===true` (authoritative, `SUPPORTS_1M`-gated at write time), else the largest `maxContext` fossil (heals legacy), else 200K. Classification (`isCompacted`, per-turn `severity`, lane placement) keeps dividing by raw per-turn `maxContext` — never the fold (feeding a future-dependent latch into a prefix-local classifier reintroduces the reverted live≠batch divergence). The fold is stateless-at-render (never a stored `sess.maxWindow` → can't go stale); `beta1m` is persisted add-only so it's rebuildable. Never cross the two — @docs/decisions/0013-beta1m-persist-session-window-derive.md - **persisted weather carries a derivation revision; all three writers stamp it through `_assignWeather`** — the load-time probe tests field EXISTENCE and `reconcile` compares COUNTS, so neither notices a change in HOW weather is derived; a semantics change without a `WEATHER_REV` bump leaves every cold session card rendering the deleted rule with all checks green. An unstamped writer (restore's step-6 `setWeather` overwrites the rebuild's output) turns the probe into a permanent rebuild loop — @docs/decisions/0013-beta1m-persist-session-window-derive.md - **cost-worker lifecycle: imported mode is side-effect free, executed mode exits by drain** — R1 no handle may ref the loop past the final stdout write; R2 no `process.exit()` on success; R3 one result protocol (stdout JSON); R4 adding an env-derived root obliges updating `isolatedEnv()` in every test that forks the worker — @docs/decisions/0015-cost-worker-lifecycle-drain-exit.md diff --git a/docs/decisions/0010-corehash-identity-routing.md b/docs/decisions/0010-corehash-identity-routing.md index acfd0dac..fba087d3 100644 --- a/docs/decisions/0010-corehash-identity-routing.md +++ b/docs/decisions/0010-corehash-identity-routing.md @@ -2,7 +2,8 @@ - Status: Accepted — **batch routing rule REWRITTEN by #350 (2026-07-24), see "## Rewritten by #350" below**; the live path (`wfAddEntry`) + turn-list - (`entry-rendering.js`) still use the original per-turn early-exit pending A2. + (`entry-rendering.js`) keep the original per-turn early-exit as an ACCEPTED + bounded divergence — see "A1/A2 boundary" below; no open issue tracks a mirror. - Date: 2026-07-16 - Related: #258 / #257 / #350 / ADR 0005 / ADR 0008 @@ -227,8 +228,9 @@ fix any *completed* session on cold-load+rebuild, which is how the dashboard renders `4b15c248`. `wfBuildState` re-derives `mainCoreHash`/`mainConvIds` from the now-correctly-composed final main lane, so the live `wfAddEntry` and turn-list `entry-rendering.js` paths inherit a correct main identity for new -turns; their per-turn early-exit is left in place. A2 (#350) mirrors the +turns; their per-turn early-exit is left in place. Mirroring the per-conversation rule + dominant-coreHash flip/trunk rebuild into those two -sites. Until A2, the only residual divergence is a *live* seed turn arriving on -an actively-viewed session — bounded and self-correcting on the next full -rebuild. See `docs/solutions/same-convid-lane-classification.md`. +sites — once tracked as "A2" — was never implemented, and #350 is closed, so +nothing tracks it. The residual divergence is therefore ACCEPTED, not pending: +a *live* seed turn arriving on an actively-viewed session — bounded and +self-correcting on the next full rebuild. See `docs/solutions/same-convid-lane-classification.md`. diff --git a/public/workflow-timeline.js b/public/workflow-timeline.js index af2586cd..e19699ce 100644 --- a/public/workflow-timeline.js +++ b/public/workflow-timeline.js @@ -1097,7 +1097,8 @@ function wfAddEntry(entry) { // derived from the (now correctly composed) final main lane — so a fresh // cold-load/rebuild of a completed session is already correct, and the only // residual divergence is a *live* seed turn on an actively-viewed session. - // A2 mirrors the per-conversation rule + dominant-coreHash flip/rebuild here. + // Mirroring the per-conversation rule here (once "A2") was never implemented + // and #350 is closed — this per-turn form is the accepted bounded divergence. if (entry.agentKey && WF_MAIN_AGENT_KEYS[entry.agentKey] && wfState.mainCoreHash && entry.coreHash && entry.coreHash !== wfState.mainCoreHash && entry.convId && wfState.mainConvIds && !wfState.mainConvIds.has(entry.convId)) {