docs(attr-child-clock): settle the mechanism and write the implementation plan

This commit is contained in:
Brennan Benson
2026-09-17 12:07:21 -07:00
parent eeceea7270
commit 7af092adbd
+451
View File
@@ -162,3 +162,454 @@ The producer already has the attribution and discards it:
So attributing at the producer means **persisting a value we already compute and throw away three
lines later** — not plumbing a new one end to end.
---
# Review, mechanism decision, and implementation plan (2026-09-17)
Every claim below is marked **VERIFIED** (read at source in this worktree at the stated
file:line) or **UNVERIFIED**. Line numbers are against `origin/main` = `25dd70e6118b`.
## 1. Re-baseline and drift
**VERIFIED.** `git fetch origin main` puts `origin/main` at `25dd70e6118b`, exactly **one**
commit ahead of this document's base `779667c1e7`: `25dd70e611 Test: target question card title
by testid instead of text (#21153)`. It touches `NativeChatQuestionCard.tsx`,
`browser-history-match-budget.ts` and two tests. **No drift** on anything this defect cites —
every line the document quotes still reads as quoted, including
`worktree-subagent-child-rows.ts:41-47`.
## 2. The claims the document left unverified
### 2.1 Is `startedAt` deliberately preserved? Yes — and the source says so, in two places
**VERIFIED.** `src/main/claude/claude-settled-background-tasks.ts:28-30` documents the field on
the tracked row itself:
> `/** First-observed epoch ms; preserved across updates and roster replacement`
> ` * so clients can render elapsed and keep a stable first-seen sort. */`
That single comment names **both** load-bearing uses and settles the question the document asked
first. The preservation sites the document guessed at are real:
- **VERIFIED** `src/main/claude/claude-background-task-tracker.ts:245`
`startedAt: existing?.startedAt ?? this.now(),` inside `replaceAggregateRoster`.
- **VERIFIED** `src/main/claude/claude-background-task-tracker.ts:303`
`startedAt: existing.startedAt,` inside `upsert`.
The same discipline holds independently on the **hook** path, which the document did not look at:
- **VERIFIED** `src/shared/claude-subagent-roster.ts:73-84``upsertWorkingClaudeSubagent`'s
`existing` branch writes `state`, `agentType`, `description` and two authority flags, and
**never touches `startedAt`**. The only write is at creation, `:93`.
- **VERIFIED** `src/shared/codex-subagent-roster.ts:41-47` — identical shape; the only write is
`:56`.
Four further consumers make mutating it actively wrong, all **VERIFIED**:
1. `src/shared/claude-subagent-roster.ts:351-353` sorts snapshots by `startedAt`, with the stated
reason *"hook arrival order is not stable across reconciles; sort so equal rosters serialize
identically and downstream equality checks can dedupe."* Mutating it reshuffles the array on
every ping and defeats that dedupe. `src/shared/codex-subagent-roster.ts:122` does the same.
2. `src/renderer/src/components/sidebar/worktree-agent-row-order.ts:17-23`
`compareWorktreeAgentRows` uses `row.startedAt` as its **primary** sort key, and
`worktree-agent-rows.ts:273` applies it to every row including children. A mutating stamp
reorders sibling child rows under the user's cursor.
3. `src/shared/claude-subagent-roster.ts:99-113``evictOldestIdleClaudeSubagent` picks the
victim by smallest `startedAt`. Mutating it changes which teammate is dropped at the cap.
4. `src/shared/agent-status-types.ts:334``agentSubagentsEqual` compares `startedAt`, and
`src/shared/agent-hook-relay.ts:130-136` folds it into the shed-roster digest.
**Verdict: the stamp is load-bearing for stable first-seen ordering and for roster identity.
Mutating it is wrong. A second field is required.** This settles the question the document
told us to settle first.
### 2.2 Does the wire type have an activity field? No
**VERIFIED.** `src/shared/agent-session-background-task-wire.ts:18-35``AgentSessionBackgroundTask`
carries exactly `id`, `kind`, `description?`, `name?`, `state?`, `startedAt?`, `totalTokens?`,
`stoppable?`. No activity field. `:26` reads verbatim
*"Host epoch ms when the task was first observed, so clients render elapsed."*
`backgroundTaskFieldsEqual` at `:53-67` compares those eight and nothing else.
**VERIFIED.** `src/shared/agent-status-types.ts:72-82``AgentSubagentSnapshot` carries
`id`, `agentType?`, `model?`, `description?`, `state`, `startedAt`. `:80` reads
*"Timestamp (ms) when this subagent was first observed."* No activity field. The document's
`:80-81` citation is exact.
### 2.3 `lastEnteredDoneAt` for a child row
**VERIFIED, and stronger than reported.** `src/renderer/src/components/dashboard/agent-finished-timestamp.ts:14-16`
returns `null` when `rowSource === 'subagent' && state !== 'done'`. But
`worktree-subagent-child-rows.ts:39` maps `'done'` to `'idle'` before the row is built, so a
subagent row's `state` is one of `working | blocked | waiting | idle | unverifiable` and is
**never `'done'`**. The guard therefore fires on **every** child row, always. A child row can
never display a finish time; it always falls through to `startedAt`.
### 2.4 One claim the document did not make, which changes the design
**VERIFIED.** *The producer already computes the child's activity timestamp, already passes it
into the roster upsert, and the upsert discards it.* On all three paths:
- `src/shared/agent-hook-listener/providers/claude-events.ts:161-167` — every child-attributed
`PreToolUse` / `PostToolUse` / `PostToolUseFailure` calls
`upsertWorkingClaudeSubagent(roster, eventAgentId, {...}, Date.now())`. That `Date.now()` **is**
the child's own last-activity moment. `upsertWorkingClaudeSubagent`'s `existing` branch
(`claude-subagent-roster.ts:73-84`) never reads the `now` parameter.
- `src/shared/agent-hook-listener/providers/codex-events.ts:133-143` — identical, for every
child-attributed Codex hook event. `upsertCodexSubagent`'s `existing` branch
(`codex-subagent-roster.ts:41-47`) never reads `now`.
- `src/main/claude/claude-background-task-tracker.ts:134-142` — the provider sends a
per-child `task_progress` frame whose own comment says *"Progress `description` is the current
activity (\"Running <tool>\")"*. Orca takes `totalTokens` off it and **drops the frame entirely
when it carries no usage** (`:139-140`, `return false`). The arrival time — proof the child is
alive right now — is never recorded.
So this is not end-to-end plumbing. On every path the value is already in hand at the producer
boundary and thrown away within a few lines, exactly as the document's "Feasibility fact" says
of the journal — but it holds for the sidebar's actual producers, which the journal is not.
## 3. Framing: is this a missing field or a display bug? — **Both, and not where the document says**
### The time column is not a recency column. It is documented as bimodal, and it is already elapsed-for-active
**VERIFIED**, in two independent places:
- `src/renderer/src/components/dashboard/agent-finished-timestamp.ts:4-9`:
*"Shared by the left worktree sidebar and the pop-out dashboard so both time from the SAME
event: a finished agent reads 'N since it finished', an active one falls through to its start."*
- `src/shared/dashboard-snapshot.ts:125-130`: `startedAt` is *"'Started … ago' display"*;
`finishedAt` *"Drives the card's time column: finished cards read time-since-finish (parity with
the left worktree sidebar), active cards fall back to startedAt."*
And that is what the code does: `worktree-card-compact-agent-row.tsx:59-66` takes
`lastEnteredDoneAt(agent)` when non-null, else `agent.startedAt`, and renders it through
`formatShortTimeAgo` (`src/renderer/src/lib/short-time-ago.ts:1-16`, which emits a bare
`now / 14m / 3h / 2d` with no words).
**So "14m" on a child that has been working 14 minutes is correct, and it is an elapsed
duration — the same reading the parent row's own active-state number has.** The premise in
"Observed" — that the row *"sits in a column read as recency"* — is **refuted at source**. The
column reads as recency only for rows that have finished.
### Then why did it look wrong? Because of defect A, not C
The report is a *comparison*: the child read `14m` while its idle parent read `now`. Defect A's
document establishes (and its chain is independently consistent with what I read here) that the
parent's `now` was **false** — the child's own frames were restamping the parent's
`stateStartedAt`. Put an honest number on the parent and the child's honest `14m` stops looking
like the anomaly. **C should not be fixed by making the child's number match a parent number that
is itself wrong.**
### The honest rendering for a working child is a DURATION, not a recency. Reasons, not precedent
1. **A recency on a healthy child conveys nothing.** A working child emits a tool event every few
seconds (`claude-events.ts:161-167` fires on each one), so "last active 3s ago" is a constant.
The number a user acts on is the elapsed: *this has been running forty minutes, something is
wrong.* The duration carries the information; the recency carries a pulse.
2. **Orca would otherwise disagree with itself about the same child.** The pop-out dashboard's
subagent row is `{ id, name, dotState }`**no timestamp at all**
(`src/shared/dashboard-snapshot.ts:55-59`, VERIFIED). The sidebar's column is contractually
"start for active, finish for done". Introducing a third meaning for children only, in one of
the three surfaces, is the reader-side precedence rule
`docs/reference/agent-status-store.md:55-62` forbids.
3. **Where recency *is* the useful reading, Orca already has a dedicated surface for it, in
words.** `worktree-card-compact-agent-row.tsx:36-38` renders `agentNoUpdateLabel(entry, now)`
on the **secondary** line for an `unverifiable` row, producing `No update in 14m`
(`src/renderer/src/lib/agent-row-decay-state.ts:46-56`). Its own comment says it *"deliberately
says what Orca last heard rather than what the agent is doing."* That is the recency channel,
and it is reserved for rows whose liveness is in doubt — a derived judgement, not a second
stored clock. Adding a recency to the time column would be a second representation of liveness
sitting beside a derivation that already exists.
**Decision: `getCompactAgentTime` is not changed. No new field is needed for the time column.**
### The real, independent defect in C: the child's recency clock is its parent's
**VERIFIED.** The recency surface above reads `agentStatusEvidenceObservedAt(entry)` =
`mirroredEvidenceReceivedAt ?? evidenceObservedAt ?? updatedAt`
(`src/shared/agent-status-freshness.ts:13-29`). The synthetic child entry
(`worktree-subagent-child-rows.ts:43-60`) sets **no** `evidenceObservedAt` and sets
`updatedAt: args.parentEntry.updatedAt` (`:46`). So when a child row decays to `unverifiable`,
`No update in N` is **the parent's silence, reported as the child's**.
With exactly one child this reads correctly by coincidence — the child's activity is what advanced
the parent's `updatedAt` — which is why it survived. With **siblings** it is wrong: child X quiet
for ten minutes while child Y pings shows `No update in 0m` on X's row. A parent that speaks for
itself does the same. That is the attribution bug class this family is about, in its mirror form:
convergence finding #1 says a child's activity must never stamp the parent's row; its dual is that
the parent's must never stamp the child's.
**So the missing field is real — but it is a missing *recency* clock feeding an existing recency
label, not a missing number for the time column.**
## 4. Does defect B solve this for free? **No.** Three independent reasons
B's own document (read at `brennanb2025/attr-parent-label`, doc-only, no implementation) proposes
attributing **journal items** to their producing agent and scoping projections by it, and
speculates that this *"likely also resolves defects A and C."* For A that is plausible — A's chain
runs through `journal-reducer.ts:69`'s journal-wide `lastActivityAt`. For **C it does not hold**:
1. **The hook path has no journal at all.** Child rows for CLI panes come from
`claudeRosterToSnapshots` / `codexRosterToSnapshots` (`src/shared/claude-subagent-roster.ts:335`,
`src/shared/codex-subagent-roster.ts:108`), fed by hook lifecycle and tool events in
`src/shared/agent-hook-listener/providers/`. No journal item exists on that path to attribute.
**VERIFIED.**
2. **A backgrounded child emits no parent-attributed frames even on the structured path.**
`src/main/claude/claude-subagent-roster.ts:3-7` states it directly: *"a BACKGROUNDED subagent
emits no child frames at all, so a roster fed by `parent_tool_use_id` alone would leave every
one of them an unlabelled row forever."* Backgrounded children are precisely the long-running
ones this defect is about. A per-producer journal clock would be permanently silent for them.
**VERIFIED.**
3. **Even on the structured path the sidebar child row is not built from journal items.**
`StructuredAgentSessionStatusBridge.tsx:103-127` builds the child snapshots from
`summary.backgroundTasks` — the `ClaudeBackgroundTaskTracker` roster, fed from
`background_tasks` / `task_*` system frames (`claude-structured-session-adapter.ts:209`), not
from the journal. A journal-derived per-producer clock would still need new plumbing into that
roster. **VERIFIED.**
**Conclusion: B and C are independent. Do not sequence C behind B, and do not wait for B's
mechanism.** They are complementary — B attributes *content*, C attributes *liveness* — and they
touch disjoint files. Either may land first.
## 5. Mechanism
**Give the child its own `evidenceObservedAt`, stamped at the producer boundary from the
observation Orca already receives, and let the existing recency reader prefer it.**
Why this and not a bespoke field:
- **It reuses the field that already means exactly this.** `AgentStatusEntry.evidenceObservedAt`
is *"Timestamp (ms) the reported evidence was first observed. Separate from `updatedAt`, which
is the delivery/ordering clock"* (`agent-status-types.ts:95-98`). Use that name on the child
types too, and the reader edit becomes a single spread: `agentStatusEvidenceObservedAt` already
prefers it and already falls back to `updatedAt`. A grep for a second last-activity concept
stays empty.
- **It removes the bug class rather than guarding it.** The defect is a reader reading a clock
that belongs to a different producer. After this, the child row reads a clock stamped by the
child's own evidence, at the same boundary that already classifies the event as the child's
(`claude-events.ts:153-161` computes `subagentOriginId` for exactly this purpose). Attribution
happens at ingestion, which is convergence finding #2.
- **It does not add a second writer.** The producers named below are the only writers of a
subagent row today; each gains one assignment. No new writer, no new publication filter.
- **The time column is untouched**, so it cannot start disagreeing with the pop-out dashboard.
Honest limits of the claim, stated rather than left implicit:
- This does **not** make an idle-parked teammate's row time its idle-since moment; there is no
settle stamp on `AgentSubagentSnapshot`. Out of scope, listed in §10.
- On the structured path, `foldClaudeBackgroundTasksIntoRoster` stamps from the **lead's** Stop
inventory. That is still an observation *of the child* (the provider asserts it is running now),
not a borrow of the parent's clock — but it is a coarser observation and a reviewer should
confirm the distinction is acceptable. Marked for review in §9 Step 2.
## 6. Wire compatibility — **Rule 1 on two surfaces, with an explicit absence semantic**
Read `docs/reference/remote-wire-compatibility.md`. Two additive optional numeric fields:
| Surface | Type | Crosses | Category |
| --- | --- | --- | --- |
| `AgentSubagentSnapshot.evidenceObservedAt?: number` (`src/shared/agent-status-types.ts:72-82`) | hook status payload | agent-hook relay, WSL/SSH relay receivers (`src/shared/agent-hook-relay.ts`) | **Rule 1** |
| `AgentSessionBackgroundTask.evidenceObservedAt?: number` (`src/shared/agent-session-background-task-wire.ts:18-35`) | `agentSession.*` | paired desktop client ↔ remote Orca host | **Rule 1** |
- **Not Rule 2.** No new opcode, no new frame, no new journal item kind. Both are new keys on
frames that already exist, decoded by parsers that ignore unknown keys.
- **Not Rule 3.** Nothing the host already publishes changes meaning, units, nullability, or
population. `startedAt` keeps its exact value and its exact comment on both types. No content
stops being synthesized. One caveat below.
- **What an old client does:** ignores the key entirely. Its child rows keep borrowing the
parent's `updatedAt` for `No update in N`, which is today's behaviour — so the skew degrades to
the status quo, never to something worse.
- **What a new client against an old host does — this is the part to get right.** The field is
**absent**, and absence must read as *the host never reported it*, never as *this child has
never been active*. This is the `agentWait` worked example
(`remote-wire-compatibility.md:198-217`): collapsing absent into a value makes an old host
indistinguishable from a silent child. Concretely: `evidenceObservedAt` must be left
`undefined`**never normalized to `0`**, which `startedAt` does at
`agent-status-types.ts:295-296` and which would render `No update in 56 years`. The reader
then falls through to `updatedAt` through the existing helper, with no extra branch.
- **No client may ever require it.** Nothing gates on presence; the only reader prefers it when
present.
- **Rule-3 caveat to state in the PR body:** `claude-background-task-tracker.ts:139-140` must stop
dropping a usage-free `task_progress` frame. That changes **publish frequency** on an existing
path, not frame content. A `task_progress` frame that today produces no publish will produce
one. Old clients decode the identical frame shape and see a row whose fields are unchanged
except the key they ignore, so this is inside Rule 1 — but it is a behaviour change on a shared
path and belongs in the PR body, not buried.
- **Enforcement:** `tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts`
covers the `agentSession.*` surface and stays green for an added optional field by design
(`remote-wire-compatibility.md:128-134`). Add no `not.toHaveProperty` assertion anywhere —
`remote-wire-compatibility.md:136-144` explains why that rots on the next release cut.
- **Mobile is not affected. VERIFIED:** `rg` over `mobile/src/` finds **zero** non-test references
to `subagent` or `backgroundTask` in production code.
## 7. The pinned tests
Neither test needs to change, and both should be **kept and extended**. They pin the right thing.
- **`worktree-subagent-child-rows.test.ts:19-54`** asserts `row.startedAt === 20` across a
14-case freshness matrix, and also asserts `parentEntry.subagents` is unmutated. That is exactly
the invariant §2.1 proved load-bearing. **Keep verbatim.** Extend the same file with two new
cases: (a) a subagent carrying `evidenceObservedAt` produces a row whose
`entry.evidenceObservedAt` is the child's value and whose `startedAt` is still the spawn stamp;
(b) a subagent **without** it produces a row with `entry.evidenceObservedAt === undefined`, so
`agentStatusEvidenceObservedAt` falls back to the parent's `updatedAt` — the old-host skew,
pinned.
- **`useWorktreeAgentRows.test.ts:728-768`** asserts `startedAt: 1500` for a child spawned at
1500, plus lineage and label. **Keep verbatim.** Add one sibling case: two children, one pinged
recently and one long quiet, and assert their `entry.evidenceObservedAt` values **differ**. That
is the multi-child case that makes the parent borrow observably wrong, and it is the regression
test this defect most needs — today it is untestable because the field does not exist.
**VERIFIED** baseline: `pnpm test src/renderer/src/components/sidebar/worktree-subagent-child-rows.test.ts`
→ 14 passed, 147ms, at `origin/main` + this branch's docs.
## 8. Should the fix also stop the child borrowing the parent's `updatedAt`? — **Partly. Demote it, do not delete it.**
`worktree-subagent-child-rows.ts:46` sets `updatedAt: args.parentEntry.updatedAt`. After this
change it stays, but it is no longer the child's recency clock — it becomes the documented
**fallback for a host that predates the field**, which is precisely what Rule 1 requires of an
absent optional. `agentStatusEvidenceObservedAt` already encodes that fallback, so demoting the
borrow costs one added spread line and no new branch.
Deleting it is wrong. The alternatives are both worse:
- setting `updatedAt` to the spawn stamp makes every child on an old host read
`No update in <its entire lifetime>`;
- setting it to `now` fabricates freshness, which is the exact defect on the parent side (A).
The residual must be stated plainly: **while defect A is unfixed, the fallback clock is polluted**,
so a child row on an old host still inherits a wrong number. That pollution is A's to fix; C's
field is what lets a current host stop depending on it at all. Recording this here rather than
silently relying on A is the point.
## 9. Implementation plan
Independent of A and B; no shared files. Land in any order.
### Step 1 — wire types (additive only)
1. `src/shared/agent-status-types.ts`
- Add to `AgentSubagentSnapshot` (`:72-82`), below `startedAt`:
`evidenceObservedAt?: number` with a one-line comment: last moment the host observed this
child's own activity; absent means the host never reported it.
- `normalizeSubagentSnapshot` (`:271-300`): accept it as an optional finite number.
**Do not mirror the `startedAt` `?? 0` at `:295-296`** — absent must stay `undefined` (§6).
- `agentSubagentsEqual` (`:322-345`): add the comparison.
2. `src/shared/agent-hook-relay.ts:130-136` — add it to `subagentRosterDigest`'s stable tuple, so
a roster differing only in this field cannot collide with a shed digest.
3. `src/shared/agent-session-background-task-wire.ts` — add `evidenceObservedAt?: number` to
`AgentSessionBackgroundTask` (`:18-35`) and to `backgroundTaskFieldsEqual` (`:53-67`).
### Step 2 — producers stamp what they already compute
4. `src/shared/claude-subagent-roster.ts`
- `TrackedClaudeSubagent` (`:21-53`): add `evidenceObservedAt?: number`.
- `upsertWorkingClaudeSubagent` `existing` branch (`:73-84`): `existing.evidenceObservedAt = now`.
Create branch (`:91-96`): `evidenceObservedAt: now`. **Leave `startedAt` untouched at both.**
- `foldClaudeBackgroundTasksIntoRoster` (`:154-248`): stamp on the id-exact running match
(`:183-189`). **Flag for review** — this observation arrives on the lead's Stop payload
(§5 limits).
- `claudeRosterToSnapshots` (`:335-355`): emit it when defined. **Do not change the sort at
`:353`.**
5. `src/shared/agent-hook-listener/providers/claude-roster-state.ts:155-180`
`seedClaudeSubagentRosterFromSnapshots` carries `snapshot.evidenceObservedAt` through, so a
restored row does not read as freshly observed. Absent stays absent.
6. `src/shared/codex-subagent-roster.ts` — same three edits: `TrackedCodexSubagent` (`:14-20`),
`upsertCodexSubagent` existing (`:41-47`) and create (`:51-57`), `codexRosterToSnapshots`
(`:108-123`), and `seedCodexSubagentRoster` (`:86-105`).
**Two call sites must NOT stamp:** `setCodexSubagentModel` (`:70-84`), which its own comment
says never touches lifecycle; and `codex-subagent-transcript.ts:287-292`, which passes
`tracked.startedAt` — a **spawn** stamp — as `now`. Stamping recency there would backdate it to
the spawn. Give that call site an explicit observation argument or leave the field alone.
7. `src/main/claude/claude-settled-background-tasks.ts`
- `TrackedClaudeBackgroundTask` (`:18-32`): add `evidenceObservedAt?: number`.
- `claudeBackgroundTaskDetail` (`:34-49`): project it onto the wire row when defined.
8. `src/main/claude/claude-background-task-tracker.ts` — stamp `this.now()` on every frame that is
evidence the task is alive: `task_started` upsert (`:164-171`), `task_updated` upsert
(`:198-205`), `replaceAggregateRoster` (`:236-248`), and `upsert`'s existing branch
(`:295-307`). **Preserve `:245` and `:303` exactly as written.**
**The load-bearing edit in this slice:** `task_progress` (`:134-142`) currently returns `false`
and writes nothing when the frame carries no `totalTokens` (`:139-140`). A progress frame with
no usage is still proof the child is alive. Stamp before that early return, and return `true`
so the state refreshes. Everything else about that branch stays — in particular the comment's
rule that progress `description` must not overwrite the task's name.
### Step 3 — bridge
9. `src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.tsx:115-121`
carry `task.evidenceObservedAt` onto the snapshot when defined. The write gate at `:168` uses
`agentSubagentsEqual`, which Step 1 already widened.
### Step 4 — reader (presentation policy only)
10. `src/renderer/src/components/sidebar/worktree-subagent-child-rows.ts:43-60` — add exactly one
line to the synthetic entry:
`...(subagent.evidenceObservedAt !== undefined ? { evidenceObservedAt: subagent.evidenceObservedAt } : {})`
Keep `updatedAt: args.parentEntry.updatedAt` (`:46`) as the documented old-host fallback (§8).
Keep `stateStartedAt: startedAt` (`:47`) and `row.startedAt` (`:69`) unchanged.
11. **No change** to `worktree-card-compact-agent-row.tsx`, `agent-finished-timestamp.ts`,
`short-time-ago.ts`, `agent-row-decay-state.ts`, or `agent-status-child-work-freshness.ts`.
The recency label already prefers the new clock through
`agentStatusEvidenceObservedAt` (`agent-status-freshness.ts:25-29`). A reuse check on the
consumer side should come back empty — that is the success signal.
### Step 5 — tests
12. Extend the two pinned tests as in §7 (keep every existing assertion).
13. `src/shared/claude-subagent-roster.test.ts` / `codex-subagent-roster.test.ts`: a second upsert
for an existing id advances `evidenceObservedAt` and **leaves `startedAt` unchanged**, and the
snapshot sort order is unchanged across the advance. This is the ablation that proves the two
stamps are genuinely separate.
14. `src/main/claude/claude-background-task-tracker.test.ts`: a `task_progress` frame **with no
`usage`** advances `evidenceObservedAt`, preserves `startedAt`, preserves `name`, and reports
changed. Pin this explicitly — it is the branch that today returns early.
15. A relay test that a roster differing only in `evidenceObservedAt` produces a different shed
digest.
16. Gates: `pnpm tc`, `pnpm test` on the touched paths, `oxlint`,
`pnpm run check:code-quality:changed`, plus
`pnpm exec vitest run --config config/vitest.config.ts tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts`.
Restore `pnpm-lock.yaml` with `git checkout -- pnpm-lock.yaml` after any test run — that file
only, never a bare checkout.
## 10. Risks, and what is deliberately not fixed
1. **Publish/fanout churn.** `evidenceObservedAt` changes on every child tool event, so
`agentSubagentsEqual` stops returning true for consecutive pings.
- `agent-status-live-entry-builder.ts:260-262` uses it only for **array reference reuse**, not
as a fanout gate, so the cost is a new `subagents` array per child event and the React
re-renders that follow. **UNVERIFIED: not measured.** Measure before merging; if it bites, the
mitigation is to quantize the stamp to a coarse bucket at the producer, and that tradeoff
should be recorded rather than applied silently.
- `StructuredAgentSessionStatusBridge.tsx:160-180` **is** a write gate, but its chain already
includes `current.updatedAt === summary.updatedAt`, which moves on every publish anyway, so
the added term changes nothing there. **VERIFIED** by reading the gate.
2. **Frequency change on `task_progress`.** Step 2/8 makes usage-free progress frames produce a
publish where today they produce none. Rule-1 safe, but it is the one behavioural change on a
shared path (§6).
3. **Silent no-op if a provider sends no child-attributed events.** For a provider whose children
emit nothing, the field stays absent and the row keeps today's behaviour. That is the correct
degrade, but it means the fix is unobservable on such a provider — QA must pick a provider and
a child shape that actually emits, and say which.
4. **Not fixed: an idle-parked teammate's row still times from spawn.** There is no settle stamp
on `AgentSubagentSnapshot`. A parked teammate reads `14m` meaning "spawned 14m ago", not
"parked 14m ago". Separate defect; needs a settled-at field, not this one.
5. **Not fixed: the two-writer structure.** The material deviation this family must disclose —
Orca keeps two real writers of one status row apart with publication filters, where every
reference examined removes the possibility of a second writer — is untouched here. C adds no
writer and removes none. Say so in the PR body rather than implying the slice narrows it.
6. **Not fixed: defect A's pollution of the fallback clock** (§8).
## 11. Claim ledger
**VERIFIED at source in this worktree:** §1 drift; §2.1 all five preservation/consumer sites;
§2.2 both wire types; §2.3 the always-null guard; §2.4 all three discard sites; §3 both
column-contract docstrings, `formatShortTimeAgo`, the pop-out subagent shape, the
`agentNoUpdateLabel` surface, the `updatedAt` borrow, the `agentStatusEvidenceObservedAt`
fallback chain; §4 all three reasons; §6 the mobile-consumer absence and the `startedAt` `?? 0`
normalization hazard; §7 the baseline test run; §10.1 second bullet.
**UNVERIFIED:** §10.1 first bullet (fanout churn is reasoned from the code path, not measured);
the assertion that defect A's chain is the cause of the reported `now` on the parent (read from
A's document and consistent with what I read here, but A's journal-reducer line was not
re-read in this worktree); any statement about what B *will* do, since B is doc-only — its branch
contains one markdown file and no implementation.