diff --git a/ATTRIBUTION-child-clock.md b/ATTRIBUTION-child-clock.md index 1330a65352b..f36709a1b54 100644 --- a/ATTRIBUTION-child-clock.md +++ b/ATTRIBUTION-child-clock.md @@ -89,3 +89,76 @@ Say which category your change is in. does not. 4. Propose a mechanism; prefer architecture over a guard. State whether it removes the bug class. 5. Mark every claim VERIFIED or UNVERIFIED. Cite file:line. + +--- + +# Reference findings + +Five independent implementations were reviewed. Projects are deliberately not named here; describe +any conclusion in repo-native terms only. Every mechanism below was read at source and verified. + +## Converged — all five agree + +1. **A child's activity never touches the parent row's lifecycle state, label, or timestamp.** +2. **Attribution happens at ingestion (the producer boundary), never as a reader-side filter.** +3. **Exactly one writer of a row's status/recency.** None of the five keeps two writers apart with + publication filters. They make the second writer *impossible*. +4. **Child -> parent rollups do exist** — but they roll up *status* or a *count*, never a label and + never a timestamp, and each is explicit and documented. +5. **The row label is a stored title derived from the user's own prompt** — never a backward scan + for the newest assistant message. + +## Closest architectural match (same platform: Electron, host-service process + renderer sidebar +## that nests parent and subagent rows) + +- Subagent hook events are routed to a **separate roster at ingestion, before any store write**. + The ingestion comment reads: *"Subagent activity is not the terminal's lifecycle: no chime, no + status change, no session id capture."* +- The store method recording subagent events is documented *"Never touches the parent binding's + lifecycle state."* +- The renderer **derives** status on every read inside a `useMemo` and **stores nothing**. +- The state-entry stamp is preserved server-side as + `prior !== undefined && !sessionChanged ? prior.startedAt : occurredAt`. + **There is no done-state special case.** That is the shape of Orca's *main* canonical writer + (`server-ingest-structured.ts:65`), not the renderer bridge's. + +## How the references make a second writer impossible + +- One reference's generic update API **structurally omits** the status/recency fields, so the type + itself forbids a second writer; a lint rule confines cache writes to owner modules. +- One consolidated *"ten independent writers across six slices"* into a single registry writer and + recorded the decision as an architecture decision record. +- One gates client updates behind a monotonic sequence check and replaces **whole rows**, so a + client cannot mutate an individual field. + +## Where the references DIVERGE — do not claim precedent here + +The child clock (defect C) has **no single answer**: +- one shows **no timestamp at all** on the desktop row, carrying liveness via status + derived counts; +- one shows a **live-ticking elapsed duration** that freezes at completion; +- one carries a true **advancing per-child progress field**. + +All three do quarantine the spawn stamp from display. So C has several legitimate answers — choose +one with stated reasons; you may not justify it by precedent alone. + +## Material deviation to disclose (repo-native terms, for the eventual PR body) + +> Orca keeps two real writers of one status row apart with publication filters. Every reference +> implementation examined removes the possibility of a second writer rather than suppressing its +> traffic. + +## Feasibility fact established in Orca + +The producer already has the attribution and discards it: +- `src/main/claude/claude-structured-item-translation.ts:57` already parses + `parentToolUseId: claudeText(frame.parent_tool_use_id)`. +- `src/main/claude/claude-structured-journal-translation.ts:143-144` uses it only for + `observeChildActivity(...)`, then drops it; the item is journaled with no producer trace. +- The journal schemas carry **no producer field at all** — zero hits for + `parentToolUseId|agentId|parentAgentId|producer|subagentId` in + `src/shared/agent-session-journal-schemas.ts` and `src/shared/agent-session-journal-types.ts`. +- A canonical subagent id already exists next door: + `src/main/claude/claude-subagent-roster.ts:121` (`this.ids.canonical(parentToolUseId)`). + +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.