Repository navigation
feat(agent-status): add an order-independent title evidence parser - #16148
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/shared/agent-title-evidence.ts (1)
18-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReduce long TypeScript comments.
Keep comments brief and limited to non-obvious constraints. Move detailed rationale and corpus context to documentation or focused test names.
src/shared/agent-title-evidence.ts#L18-L34: Reduce the multi-paragraph evidence-model explanation to a brief contract comment.src/shared/agent-title-evidence.test.ts#L98-L103: Reduce the spinner explanation to one brief test-specific comment.src/shared/agent-title-identity-characterization.test.ts#L4-L16: Move the corpus and historical behavior narrative out of the file header.As per coding guidelines: “Comments must be concise, non-obvious, and brief—prefer one line; do not explain obvious behavior or walk through code.”
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 20fec4ae-2535-484b-ad59-edd30d172ce0
📒 Files selected for processing (3)
src/shared/agent-title-evidence.test.tssrc/shared/agent-title-evidence.tssrc/shared/agent-title-identity-characterization.test.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.
a662578 to
565d177
Compare
|
Status update: rebased onto current main and pushed the focused parser/test changes to the PR head. Validation is green: full |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/shared/agent-title-evidence.ts (1)
25-40: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShorten the introductory block comment.
Lines 25-40 restate the implementation and include examples. Keep a short description of the evidence-ranking contract.
Proposed change
-/** - * Order-independent identity evidence from a terminal title. - * - * The chain this replaces is a first-match-wins scan of substring predicates, so its answer is - * decided by list position rather than by how strong the evidence is. That is why a Grok pane - * whose task text mentions Codex reads as Codex, and why fixing one collision by hoisting a - * branch breaks another. Here every signal is collected first and ranked afterwards, by class: - * - * vendor marker — a control sequence or sigil the agent itself emits. Task text cannot forge it. - * anchored name — a name in a position some grammar reserves for identity (Orca's `- <agent>` - * owner suffix, or the whole undecorated remainder). - * free-text name — a name anywhere else. Anyone can type it. - * - * A free-text name never becomes identity on its own, even when it is the only name present: an - * absent icon is recoverable, a confidently wrong one is not. Callers that only need "is this pane - * busy" want activity parsing, which lives elsewhere and does not go through here. - */ +/** Collects terminal-title identity evidence and resolves it by evidence strength. */As per coding guidelines: “Comments must be concise, non-obvious, and brief—prefer one line; do not explain obvious behavior or walk through code.”
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 2f0f6ee4-e003-4a95-818e-9c6460558669
📥 Commits
Reviewing files that changed from the base of the PR and between a662578 and 565d17767986b26eced2bd08ed312ad07b6834bb.
📒 Files selected for processing (5)
src/shared/agent-title-core.tssrc/shared/agent-title-evidence.test.tssrc/shared/agent-title-evidence.tssrc/shared/synthetic-agent-title.tssrc/shared/terminal-title-agent-type.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.
05c3dc7 to
e905d0d
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/shared/agent-title-evidence.ts (1)
26-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShorten the module documentation.
Keep a short contract summary here. Move extended design rationale to focused tests or project documentation.
As per coding guidelines, comments must be concise, non-obvious, and brief.
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 408e3a23-411a-41cd-849b-c145f71f4638
📥 Commits
Reviewing files that changed from the base of the PR and between 05c3dc778647f96e6ff0105052eecae43564c503 and 06ce7a0c07563fe55d1d06be7ecf31e45aaa31a6.
📒 Files selected for processing (2)
src/shared/agent-title-evidence.test.tssrc/shared/agent-title-evidence.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.
06ce7a0 to
50d57c1
Compare
|
Follow-up status: addressed the final review findings by hardening wrapper traversal against leading-separator hangs, keeping Claude-sigil titles from overclaiming bare agent names, restricting owner suffixes to recognized agent IDs, and honoring opted-out synthetic profiles. Rebased again onto current |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/shared/agent-title-evidence.ts (1)
26-42: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReduce multi-line explanatory comments. Keep only concise, non-obvious behavior notes.
src/shared/agent-title-evidence.ts#L26-L42: replace implementation history with a short API contract.src/shared/agent-title-evidence.test.ts#L281-L286: replace the fixture walkthrough with one behavior statement.As per coding guidelines, comments must be concise, non-obvious, and brief—prefer one line.
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 52a0c0b8-d2aa-41b8-82d0-445cf0669f56
📥 Commits
Reviewing files that changed from the base of the PR and between 06ce7a0c07563fe55d1d06be7ecf31e45aaa31a6 and 587455b.
📒 Files selected for processing (2)
src/shared/agent-title-evidence.test.tssrc/shared/agent-title-evidence.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 2 remain after this review.
| it('lets an anchored name outrank a foreign vendor marker', () => { | ||
| expect(agentFor('✳ agy')).toBe('claude') | ||
| expect(reasonFor('✳ agy')).toBe('vendor-marker') | ||
| expect(agentFor('✳ codex')).toBe('claude') | ||
| expect(reasonFor('✳ codex')).toBe('vendor-marker') |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Correct the ✳ agy expectation.
AGY_AGENT_NAME_RE identifies agy as Antigravity. The collector strips the Claude sigil and records anchored Antigravity evidence. Anchored evidence has precedence over the Claude vendor marker. Lines 89-90 therefore fail.
Proposed fix
- expect(agentFor('✳ agy')).toBe('claude')
- expect(reasonFor('✳ agy')).toBe('vendor-marker')
+ expect(agentFor('✳ agy')).toBe('antigravity')
+ expect(reasonFor('✳ agy')).toBe('anchored')📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('lets an anchored name outrank a foreign vendor marker', () => { | |
| expect(agentFor('✳ agy')).toBe('claude') | |
| expect(reasonFor('✳ agy')).toBe('vendor-marker') | |
| expect(agentFor('✳ codex')).toBe('claude') | |
| expect(reasonFor('✳ codex')).toBe('vendor-marker') | |
| it('lets an anchored name outrank a foreign vendor marker', () => { | |
| expect(agentFor('✳ agy')).toBe('antigravity') | |
| expect(reasonFor('✳ agy')).toBe('anchored') | |
| expect(agentFor('✳ codex')).toBe('claude') | |
| expect(reasonFor('✳ codex')).toBe('vendor-marker') |
587455b to
e5bf637
Compare
The chain this will replace is a first-match-wins scan of substring predicates, so its answer
is decided by list position rather than by how strong the evidence is. Four real recorded Grok
panes read as Codex today because Codex is checked first and their task text mentions it.
Reordering cannot fix that: whichever branch is hoisted, some other pair breaks.
collectAgentTitleEvidence collects every signal first and ranks by class afterwards:
vendor marker — a sigil or control sequence the agent itself emits; task text cannot forge it
anchored name — a name where a grammar reserves the position for identity (Orca's `- <agent>`
owner suffix, or the whole undecorated remainder)
free-text name — a name anywhere else
Anchored beats vendor marker, so `✳ agy` is Antigravity rather than Claude. Free text never
beats either, and never becomes identity on its own even as the only name present — an absent
icon is recoverable, a confidently wrong one is not. Conflicts within a class resolve to null.
No consumer imports this yet. Swapping getAgentLabel in place would move ~20 call sites at
once; each migrates behind the resolver, with its own evidence.
Measured against the live recorded-title corpus (747 distinct titles), 598 agree with the
current chain and 149 differ:
144 claude -> null spinner-only titles. Braille and quarter-circle frames are emitted by
many agents, so they prove the pane is busy and nothing about who it
is. These are answered by stronger signals once the resolver lands,
which is why the parser is not wired up on its own.
4 codex -> grok the misattributed Grok panes, fixed.
1 codex -> null a spinner-prefixed Claude pane whose task text names Codex.
Three parser defects were found and fixed by reviewing that delta rather than reasoning about
it: the OpenCode envelope was treated as a vetoable marker instead of an owning grammar, free
text was allowed to veto a vendor marker (which blinded 13 real Claude titles), and the owner
suffix matched the tail of a hyphenated worktree name (`review-14600-codex`).
|
Manual QA captured against PR #16237 head ( Rendered Orca evidence from the #16237 build (attached here; it shows the real app surface and confirms the intended worktree instance): The parser itself is not claimed to be exercised by these screenshots; they document the real caller build and the host-level routing evidence. |
e5bf637 to
212d935
Compare
|
Codex review loop - status Verdict: CLEAN / PASS WITH NO PROVEN IN-SCOPE ISSUES. This status covers head
Residual risk is intentional: #16148 adds an unwired parser, so its parser output is not itself rendered on this branch; the real caller behavior is validated on #16237 as documented above. |
…r routing Rebuilt on current main so this carries the hardened parser from #16148 and the corrected resolver from #16157 (authority-scoped run keys, no order-dependent duplicate resolution). Applies the resolver's new `minimumSource` floor at the publisher. What this publishes authorizes an action — routing decides which real agent pane receives a message — so ranking title last is not enough; the floor removes it from consideration entirely, and no amount of reordering by a caller can bring it back. The trade, stated because it is a real capability loss: a hook-less agent over SSH that Orca did not launch, and whose foreground process the host cannot read, is no longer addressable by @agent. Accepted because a message delivered into the wrong agent's prompt is unrecoverable while an undelivered one is visible — the sender sees zero recipients. Whether real panes actually carry launch/foreground evidence is the open question, and is what live validation must answer.
…tablyai#16148) * feat(agent-status): add an order-independent title evidence parser The chain this will replace is a first-match-wins scan of substring predicates, so its answer is decided by list position rather than by how strong the evidence is. Four real recorded Grok panes read as Codex today because Codex is checked first and their task text mentions it. Reordering cannot fix that: whichever branch is hoisted, some other pair breaks. collectAgentTitleEvidence collects every signal first and ranks by class afterwards: vendor marker — a sigil or control sequence the agent itself emits; task text cannot forge it anchored name — a name where a grammar reserves the position for identity (Orca's `- <agent>` owner suffix, or the whole undecorated remainder) free-text name — a name anywhere else Anchored beats vendor marker, so `✳ agy` is Antigravity rather than Claude. Free text never beats either, and never becomes identity on its own even as the only name present — an absent icon is recoverable, a confidently wrong one is not. Conflicts within a class resolve to null. No consumer imports this yet. Swapping getAgentLabel in place would move ~20 call sites at once; each migrates behind the resolver, with its own evidence. Measured against the live recorded-title corpus (747 distinct titles), 598 agree with the current chain and 149 differ: 144 claude -> null spinner-only titles. Braille and quarter-circle frames are emitted by many agents, so they prove the pane is busy and nothing about who it is. These are answered by stronger signals once the resolver lands, which is why the parser is not wired up on its own. 4 codex -> grok the misattributed Grok panes, fixed. 1 codex -> null a spinner-prefixed Claude pane whose task text names Codex. Three parser defects were found and fixed by reviewing that delta rather than reasoning about it: the OpenCode envelope was treated as a vetoable marker instead of an owning grammar, free text was allowed to veto a vendor marker (which blinded 13 real Claude titles), and the owner suffix matched the tail of a hyphenated worktree name (`review-14600-codex`). * fix(agent-status): harden title evidence collection * fix(agent-status): limit emitted display evidence * fix(agent-status): anchor explicit title evidence * test(agent-status): cover anchored token filtering * fix(agent-status): complete explicit marker evidence * fix(agent-status): recognize reserved owner ids * fix(agent-status): reject cwd path titles * fix(agent-status): avoid bare-name identity guesses * fix(agent-status): require spinner for working labels * fix(agent-status): honor opted-out synthetic profiles * fix(agent-status): harden wrapper evidence boundaries (cherry picked from commit 5261206)
* feat(agent-status): measure identity evidence before migrating any consumer PR 1 of the identity migration. It changes no displayed or routed identity — it only measures. Why measure first: the hierarchy shipped in #16148/#16157 has zero consumers, while ~31 sites still derive identity independently. Every migration decision after this is currently a guess, including the one that matters most — how often a real pane has no evidence at all. A live P0 reports "No Claude status shown", and this design trades toward showing nothing when uncertain, so the blank rate has to be a number before any surface moves. - `pane-agent-identity-evidence.ts` — one assembler that gathers a pane's evidence, so consumers stop each inventing their own ladder. - `pane-agent-identity-census.ts` — shadow-only counters keyed by host kind (native / wsl-host / wsl-distro / ssh / relay) and launch mode (typed / orca-launch / resume). Records a bitmask of which sources were present and whether the resolver returned null or ambiguous. No titles, prompts, paths, handles, or agent text. - `pane-agent-identity-inventory.test.ts` — a ratchet that fails when a legacy identity helper gains a new production caller, so the surface cannot grow while the migration runs. Three review findings are encoded rather than deferred: launch stays above run-key-less completed hooks (promoting the hook lets a stale record hijack a pane); OMP/Pi evidence is owner-normalized before assembly, since OMP emits Pi-compatible frames and a wrapper's hook would otherwise be read as the agent it wraps; and Windows-side `wsl.exe` is rejected as process evidence, because the host observes the distro wrapper rather than the agent inside it. The census cannot be completed from a worktree. It needs representative native, SSH, WSL and relay cohorts collected from real use, and that review is the gate on PR 3 — not this PR. * test(agent-status): keep identity migration inventory-only * test(agent-status): reuse reliable source scanner * test(agent-status): bound inventory scan work * test(agent-status): avoid inventory path false negatives * test(agent-status): refresh identity inventory after base repair * test(agent-status): correct inventory classifications * test(agent-status): correct action boundary inventory * test(agent-status): pin inventory occurrence counts * test(agent-status): fail closed on scanner desync
…tablyai#16148) * feat(agent-status): add an order-independent title evidence parser The chain this will replace is a first-match-wins scan of substring predicates, so its answer is decided by list position rather than by how strong the evidence is. Four real recorded Grok panes read as Codex today because Codex is checked first and their task text mentions it. Reordering cannot fix that: whichever branch is hoisted, some other pair breaks. collectAgentTitleEvidence collects every signal first and ranks by class afterwards: vendor marker — a sigil or control sequence the agent itself emits; task text cannot forge it anchored name — a name where a grammar reserves the position for identity (Orca's `- <agent>` owner suffix, or the whole undecorated remainder) free-text name — a name anywhere else Anchored beats vendor marker, so `✳ agy` is Antigravity rather than Claude. Free text never beats either, and never becomes identity on its own even as the only name present — an absent icon is recoverable, a confidently wrong one is not. Conflicts within a class resolve to null. No consumer imports this yet. Swapping getAgentLabel in place would move ~20 call sites at once; each migrates behind the resolver, with its own evidence. Measured against the live recorded-title corpus (747 distinct titles), 598 agree with the current chain and 149 differ: 144 claude -> null spinner-only titles. Braille and quarter-circle frames are emitted by many agents, so they prove the pane is busy and nothing about who it is. These are answered by stronger signals once the resolver lands, which is why the parser is not wired up on its own. 4 codex -> grok the misattributed Grok panes, fixed. 1 codex -> null a spinner-prefixed Claude pane whose task text names Codex. Three parser defects were found and fixed by reviewing that delta rather than reasoning about it: the OpenCode envelope was treated as a vetoable marker instead of an owning grammar, free text was allowed to veto a vendor marker (which blinded 13 real Claude titles), and the owner suffix matched the tail of a hyphenated worktree name (`review-14600-codex`). * fix(agent-status): harden title evidence collection * fix(agent-status): limit emitted display evidence * fix(agent-status): anchor explicit title evidence * test(agent-status): cover anchored token filtering * fix(agent-status): complete explicit marker evidence * fix(agent-status): recognize reserved owner ids * fix(agent-status): reject cwd path titles * fix(agent-status): avoid bare-name identity guesses * fix(agent-status): require spinner for working labels * fix(agent-status): honor opted-out synthetic profiles * fix(agent-status): harden wrapper evidence boundaries
…#16575) * feat(agent-status): measure identity evidence before migrating any consumer PR 1 of the identity migration. It changes no displayed or routed identity — it only measures. Why measure first: the hierarchy shipped in stablyai#16148/stablyai#16157 has zero consumers, while ~31 sites still derive identity independently. Every migration decision after this is currently a guess, including the one that matters most — how often a real pane has no evidence at all. A live P0 reports "No Claude status shown", and this design trades toward showing nothing when uncertain, so the blank rate has to be a number before any surface moves. - `pane-agent-identity-evidence.ts` — one assembler that gathers a pane's evidence, so consumers stop each inventing their own ladder. - `pane-agent-identity-census.ts` — shadow-only counters keyed by host kind (native / wsl-host / wsl-distro / ssh / relay) and launch mode (typed / orca-launch / resume). Records a bitmask of which sources were present and whether the resolver returned null or ambiguous. No titles, prompts, paths, handles, or agent text. - `pane-agent-identity-inventory.test.ts` — a ratchet that fails when a legacy identity helper gains a new production caller, so the surface cannot grow while the migration runs. Three review findings are encoded rather than deferred: launch stays above run-key-less completed hooks (promoting the hook lets a stale record hijack a pane); OMP/Pi evidence is owner-normalized before assembly, since OMP emits Pi-compatible frames and a wrapper's hook would otherwise be read as the agent it wraps; and Windows-side `wsl.exe` is rejected as process evidence, because the host observes the distro wrapper rather than the agent inside it. The census cannot be completed from a worktree. It needs representative native, SSH, WSL and relay cohorts collected from real use, and that review is the gate on PR 3 — not this PR. * test(agent-status): keep identity migration inventory-only * test(agent-status): reuse reliable source scanner * test(agent-status): bound inventory scan work * test(agent-status): avoid inventory path false negatives * test(agent-status): refresh identity inventory after base repair * test(agent-status): correct inventory classifications * test(agent-status): correct action boundary inventory * test(agent-status): pin inventory occurrence counts * test(agent-status): fail closed on scanner desync
…#16575) * feat(agent-status): measure identity evidence before migrating any consumer PR 1 of the identity migration. It changes no displayed or routed identity — it only measures. Why measure first: the hierarchy shipped in stablyai#16148/stablyai#16157 has zero consumers, while ~31 sites still derive identity independently. Every migration decision after this is currently a guess, including the one that matters most — how often a real pane has no evidence at all. A live P0 reports "No Claude status shown", and this design trades toward showing nothing when uncertain, so the blank rate has to be a number before any surface moves. - `pane-agent-identity-evidence.ts` — one assembler that gathers a pane's evidence, so consumers stop each inventing their own ladder. - `pane-agent-identity-census.ts` — shadow-only counters keyed by host kind (native / wsl-host / wsl-distro / ssh / relay) and launch mode (typed / orca-launch / resume). Records a bitmask of which sources were present and whether the resolver returned null or ambiguous. No titles, prompts, paths, handles, or agent text. - `pane-agent-identity-inventory.test.ts` — a ratchet that fails when a legacy identity helper gains a new production caller, so the surface cannot grow while the migration runs. Three review findings are encoded rather than deferred: launch stays above run-key-less completed hooks (promoting the hook lets a stale record hijack a pane); OMP/Pi evidence is owner-normalized before assembly, since OMP emits Pi-compatible frames and a wrapper's hook would otherwise be read as the agent it wraps; and Windows-side `wsl.exe` is rejected as process evidence, because the host observes the distro wrapper rather than the agent inside it. The census cannot be completed from a worktree. It needs representative native, SSH, WSL and relay cohorts collected from real use, and that review is the gate on PR 3 — not this PR. * test(agent-status): keep identity migration inventory-only * test(agent-status): reuse reliable source scanner * test(agent-status): bound inventory scan work * test(agent-status): avoid inventory path false negatives * test(agent-status): refresh identity inventory after base repair * test(agent-status): correct inventory classifications * test(agent-status): correct action boundary inventory * test(agent-status): pin inventory occurrence counts * test(agent-status): fail closed on scanner desync
…erminal title (#16237) * feat(agent-status): add the pane agent identity resolver Four ladders answer "which agent is in this pane" independently — the tab icon, the open-tab/search occupant, the sidebar title rows, and the sidebar hook-row fallback — and they disagree. Two consult the terminal title before the launch record, so a string Orca parsed outranks a fact Orca owns. resolvePaneAgentIdentity is the single ranked answer. Two rules, one of which is not an ordering: 1. Evidence is ranked by how directly it observes the process; a display title is last. 2. Each observation carries the runId of the agent run it describes. Evidence from a superseded run is INELIGIBLE, not merely outranked. Rule 2 is the part reordering could never supply. A completed hook naming A plus a title naming B is either a bug (hook right, title stale) or a legitimate pane reclaim (title right) — identical signals, opposite correct answers. Run ids make them different facts: in the bug both belong to the current run; in the reclaim the hook belongs to a previous one. That pair ships as a test asserting the two produce opposite answers from the same evidence. Missing run ids are treated as eligible. Absence means "this peer does not publish them", not "this is stale", so an old host's rows are never blanked. Sibling evidence is opt-in so pane-scoped consumers cannot inherit another pane's agent. No consumer imports this yet; each migrates separately with its own evidence. Verified non-vacuous: reversing the authority order fails 10 of 18 assertions and removing the run filter fails 3. * fix(agent-status): close three resolver contract holes found in review **Duplicate evidence of one source resolved by array order.** `eligible.find(...)` returned the first match, so two live hooks naming different agents were settled by input position — the exact property this resolver exists to remove. The original order-independence test only used DISTINCT sources, so it never exercised it. Conflicting same-class evidence now returns null with `ambiguousAt`, and does NOT fall through to a weaker source: letting a title answer whenever two hooks disagree is worse than saying nothing. **A bare numeric runId collided across authority restarts.** `incarnation` is a total order only within one `authorityId` (agent-status-observation.ts states this), and the id is regenerated per authority instance, so a restarted host counting from its own floor would report `1` and match an unrelated live run 1. The run key now carries its authority, and evidence from a DIFFERENT authority is treated as incomparable — kept, like an absent key — rather than as stale. **Title stayed reachable by consumers that authorize writes.** Ranking it last makes misuse unlikely; `minimumSource` makes it impossible. An action consumer passes `'launch'` and weaker evidence is dropped before ranking, so routing or delivery cannot name a target from a parsed string even by reordering its inputs. Display surfaces omit it and are unaffected. Also restores the generic agent-vocabulary parameter, which lives on the routing branch and was lost when this branch was rebased. Each fix is mutation-verified: first-match restored fails 3, ignoring authority fails 1, dropping the floor fails 2. The authority test was itself vacuous on the first attempt — both sides used `incarnation: 1`, so a resolver ignoring authority still passed on the numeric compare. It now uses differing incarnations. The remaining review finding, that `process > launch` has no freshness bound, is NOT fixed here: it needs an observation timestamp the evidence type does not yet carry. Recorded rather than silently dropped. * fix(orchestration): route @agent messages by resolved identity, not terminal title `@claude` picked its recipients with `buildAgentNameRe('claude').test(title)`, so any pane whose TITLE contained the word received Claude's messages. Terminal titles carry task text, and people describe agent work in them, so this is the ordinary case rather than a contrived one: the recorded title "Switch Claude and Codex off the load balancer… - grok" is a Grok pane that received both @claude and @codex. Misdelivered instructions, not a cosmetic slip. The cause is that `RuntimeTerminalSummary` carried no identity at all — `title` was the only identity-ish field on it, so routing by title was the only option available. Fix the input: - `RuntimeTerminalSummary.agentIdentity?: TuiAgent` — optional, host-resolved from launch and foreground-process evidence the host owns, with the title ranked last and contributing only when the evidence parser finds an unambiguous name. A title that merely mentions an agent yields no evidence, which is the whole point. - `resolvePublishedPaneAgentIdentity` in `src/shared` rather than inside the runtime class, so the decision is testable without a runtime and so routing, delivery and the UI cannot drift. - Groups match `agentIdentity`; the title matcher and its bespoke Cursor predicate are deleted. Unknown fails closed. `agentIdentity` is absent when the host predates the field or had no evidence beyond the title, and delivery is an action: not delivering is visible and recoverable (the sender sees no recipients), while delivering to the wrong agent is neither. The optional field is additive, so an old client simply ignores it (wire rule 1). This is also the first real caller of the evidence parser and the identity resolver. Tests: 27 in groups, 8 for the publisher, 3 RPC fan-out cases updated to the new contract. The `@cursor`-must-not-match-"text cursor blink" hazard is now excluded structurally instead of by a per-agent predicate. Verified non-vacuous by mutation: swapping the process/title ranks fails 2 publisher assertions, and reverting groups to title matching fails 15 of 27. One earlier mutation silently failed to apply after formatting reflowed the block — the file was checked before trusting the result. * perf(runtime): reuse terminal title during summary build * fix(orchestration): refuse title evidence when publishing identity for routing Rebuilt on current main so this carries the hardened parser from #16148 and the corrected resolver from #16157 (authority-scoped run keys, no order-dependent duplicate resolution). Applies the resolver's new `minimumSource` floor at the publisher. What this publishes authorizes an action — routing decides which real agent pane receives a message — so ranking title last is not enough; the floor removes it from consideration entirely, and no amount of reordering by a caller can bring it back. The trade, stated because it is a real capability loss: a hook-less agent over SSH that Orca did not launch, and whose foreground process the host cannot read, is no longer addressable by @agent. Accepted because a message delivered into the wrong agent's prompt is unrecoverable while an undelivered one is visible — the sender sees zero recipients. Whether real panes actually carry launch/foreground evidence is the open question, and is what live validation must answer. * fix(pty): preserve agent identity on daemon reattach * fix(runtime): retire stale pane agent identity * chore: normalize runtime types formatting * fix(agent-status): identify a pane from its own hook, not from how it was started Two defects, one cause: identity was inferred from the outside instead of read from the agent. **Hook evidence was never plumbed in.** The publisher considered `process`, `launch` and `title` and contained zero hook references — while the resolver ranks `live-hook` first. The top rung of the ladder was never connected. That made identity depend on Orca having launched the agent. Most agents are started by typing `claude` or `codex` at a shell, which leaves no launch record. On macOS the foreground process still names them, so the gap was invisible. On WSL the Windows host reads the foreground process as `wsl.exe` — the distro wrapper, not the agent inside it — so those panes had no signal at all and became unaddressable by `@agent`. A hook is the agent reporting itself, so it survives both: no launch record needed, and no dependency on reading a process across the WSL boundary. **`launch` outranked `completed-hook`.** Ranking is now by TENSE rather than by how authoritative a source sounds: present: live-hook > process past: completed-hook > launch > sleeping-session > sibling > title A launch record is an event, not a state — it stays true after the agent exits, which is why a pane reused after closing its agent kept reading as the old one. A completed hook at least proves the agent actually ran in that pane; a launch record only proves Orca tried to start one. Neither rank was covered: all 392 existing tests passed unchanged after reordering. Mutation now fails 2 on the old order and 4 with hook evidence removed. Known remaining gap, deliberately not papered over: a hand-started WSL agent with no managed hooks has no identity signal at all. Restoring a title guess there would reinstate the misdelivery this PR exists to prevent. * fix(orchestration): restore title as the last resort, not a forbidden source An earlier revision passed `minimumSource: 'launch'` so routing could not see a title at any rank, reasoning that a display string must never authorize a write. That conflated the evidence parser with the raw substring match it replaced. `buildAgentNameRe('claude').test(title)` was the misdelivery. `collectAgentTitleEvidence` returns null on exactly those shapes: "Review the Claude session-history fix" on a Codex pane yields nothing, and "Switch Claude and Codex off the load balancer… - grok" yields grok from its owner suffix. Ranking title last is therefore sufficient; refusing it is not necessary. Refusing it had a real cost. An agent a user starts by hand inside an Orca WSL terminal has no launch record, no readable foreground process (the Windows host sees `wsl.exe`, not the agent in the distro), and — until managed Codex hooks install there — no hook either. An unambiguous title was the only thing left, and dropping it made that pane unaddressable by @agent where the previous code could reach it. That is a regression, and most agents are started that way. End-to-end coverage added at the routing layer with title allowed: @claude still does not reach a Codex pane whose task text names Claude, @codex still does not reach a Grok pane whose task text names Codex, and a pane identified only by an unambiguous title is reachable again. * revert(agent-status): keep launch above completed-hook until run keys exist Reverts the tense-based reorder from this branch. The reasoning behind it was sound as far as it went — a launch record is a past event, not an observation, which is why a reused pane kept reading as its previous agent — but it fixed one staleness by opening a worse one. A completed hook is past tense too, and without an agent-run key it never expires at all. Ranking it above `launch` lets a stale hook from a previous agent outrank the launch record Orca stamped for the process running NOW. pane-agent-owner.ts already says this in its own comment: "Ranking launch/live-hook above the completed/sleeping records keeps a genuine pane on its real agent and stops a stale record from hijacking it." The reorder belongs with authority-scoped run generation, which is what makes any past-tense evidence expire. It is staged in the migration plan rather than shipped here. What this branch keeps: hook evidence feeding pane identity (so an agent a user starts by hand is identified from its own report rather than needing a launch record), and title restored as a genuine last resort behind the evidence parser. * fix(runtime): guard the pane key so terminal.list survives a non-UUID leaf `makePaneKey` throws on a leaf id that is not a UUID. The hook-evidence lookup called it unguarded inside `buildTerminalSummary`, so a single such leaf took down `terminal.list` for the whole list rather than degrading that one pane — 136 tests across 5 files, and the native code-quality gate tripped separately on a duplicate test title. Both were mine, and both were caught by CI rather than by me: I ran the focused suites before pushing instead of the affected directories. * fix(runtime): declare published terminal agent identity * fix(runtime): demote completed hook identity evidence
…#16575) * feat(agent-status): measure identity evidence before migrating any consumer PR 1 of the identity migration. It changes no displayed or routed identity — it only measures. Why measure first: the hierarchy shipped in stablyai#16148/stablyai#16157 has zero consumers, while ~31 sites still derive identity independently. Every migration decision after this is currently a guess, including the one that matters most — how often a real pane has no evidence at all. A live P0 reports "No Claude status shown", and this design trades toward showing nothing when uncertain, so the blank rate has to be a number before any surface moves. - `pane-agent-identity-evidence.ts` — one assembler that gathers a pane's evidence, so consumers stop each inventing their own ladder. - `pane-agent-identity-census.ts` — shadow-only counters keyed by host kind (native / wsl-host / wsl-distro / ssh / relay) and launch mode (typed / orca-launch / resume). Records a bitmask of which sources were present and whether the resolver returned null or ambiguous. No titles, prompts, paths, handles, or agent text. - `pane-agent-identity-inventory.test.ts` — a ratchet that fails when a legacy identity helper gains a new production caller, so the surface cannot grow while the migration runs. Three review findings are encoded rather than deferred: launch stays above run-key-less completed hooks (promoting the hook lets a stale record hijack a pane); OMP/Pi evidence is owner-normalized before assembly, since OMP emits Pi-compatible frames and a wrapper's hook would otherwise be read as the agent it wraps; and Windows-side `wsl.exe` is rejected as process evidence, because the host observes the distro wrapper rather than the agent inside it. The census cannot be completed from a worktree. It needs representative native, SSH, WSL and relay cohorts collected from real use, and that review is the gate on PR 3 — not this PR. * test(agent-status): keep identity migration inventory-only * test(agent-status): reuse reliable source scanner * test(agent-status): bound inventory scan work * test(agent-status): avoid inventory path false negatives * test(agent-status): refresh identity inventory after base repair * test(agent-status): correct inventory classifications * test(agent-status): correct action boundary inventory * test(agent-status): pin inventory occurrence counts * test(agent-status): fail closed on scanner desync (cherry picked from commit a624e7c)
…erminal title (stablyai#16237) * feat(agent-status): add the pane agent identity resolver Four ladders answer "which agent is in this pane" independently — the tab icon, the open-tab/search occupant, the sidebar title rows, and the sidebar hook-row fallback — and they disagree. Two consult the terminal title before the launch record, so a string Orca parsed outranks a fact Orca owns. resolvePaneAgentIdentity is the single ranked answer. Two rules, one of which is not an ordering: 1. Evidence is ranked by how directly it observes the process; a display title is last. 2. Each observation carries the runId of the agent run it describes. Evidence from a superseded run is INELIGIBLE, not merely outranked. Rule 2 is the part reordering could never supply. A completed hook naming A plus a title naming B is either a bug (hook right, title stale) or a legitimate pane reclaim (title right) — identical signals, opposite correct answers. Run ids make them different facts: in the bug both belong to the current run; in the reclaim the hook belongs to a previous one. That pair ships as a test asserting the two produce opposite answers from the same evidence. Missing run ids are treated as eligible. Absence means "this peer does not publish them", not "this is stale", so an old host's rows are never blanked. Sibling evidence is opt-in so pane-scoped consumers cannot inherit another pane's agent. No consumer imports this yet; each migrates separately with its own evidence. Verified non-vacuous: reversing the authority order fails 10 of 18 assertions and removing the run filter fails 3. * fix(agent-status): close three resolver contract holes found in review **Duplicate evidence of one source resolved by array order.** `eligible.find(...)` returned the first match, so two live hooks naming different agents were settled by input position — the exact property this resolver exists to remove. The original order-independence test only used DISTINCT sources, so it never exercised it. Conflicting same-class evidence now returns null with `ambiguousAt`, and does NOT fall through to a weaker source: letting a title answer whenever two hooks disagree is worse than saying nothing. **A bare numeric runId collided across authority restarts.** `incarnation` is a total order only within one `authorityId` (agent-status-observation.ts states this), and the id is regenerated per authority instance, so a restarted host counting from its own floor would report `1` and match an unrelated live run 1. The run key now carries its authority, and evidence from a DIFFERENT authority is treated as incomparable — kept, like an absent key — rather than as stale. **Title stayed reachable by consumers that authorize writes.** Ranking it last makes misuse unlikely; `minimumSource` makes it impossible. An action consumer passes `'launch'` and weaker evidence is dropped before ranking, so routing or delivery cannot name a target from a parsed string even by reordering its inputs. Display surfaces omit it and are unaffected. Also restores the generic agent-vocabulary parameter, which lives on the routing branch and was lost when this branch was rebased. Each fix is mutation-verified: first-match restored fails 3, ignoring authority fails 1, dropping the floor fails 2. The authority test was itself vacuous on the first attempt — both sides used `incarnation: 1`, so a resolver ignoring authority still passed on the numeric compare. It now uses differing incarnations. The remaining review finding, that `process > launch` has no freshness bound, is NOT fixed here: it needs an observation timestamp the evidence type does not yet carry. Recorded rather than silently dropped. * fix(orchestration): route @agent messages by resolved identity, not terminal title `@claude` picked its recipients with `buildAgentNameRe('claude').test(title)`, so any pane whose TITLE contained the word received Claude's messages. Terminal titles carry task text, and people describe agent work in them, so this is the ordinary case rather than a contrived one: the recorded title "Switch Claude and Codex off the load balancer… - grok" is a Grok pane that received both @claude and @codex. Misdelivered instructions, not a cosmetic slip. The cause is that `RuntimeTerminalSummary` carried no identity at all — `title` was the only identity-ish field on it, so routing by title was the only option available. Fix the input: - `RuntimeTerminalSummary.agentIdentity?: TuiAgent` — optional, host-resolved from launch and foreground-process evidence the host owns, with the title ranked last and contributing only when the evidence parser finds an unambiguous name. A title that merely mentions an agent yields no evidence, which is the whole point. - `resolvePublishedPaneAgentIdentity` in `src/shared` rather than inside the runtime class, so the decision is testable without a runtime and so routing, delivery and the UI cannot drift. - Groups match `agentIdentity`; the title matcher and its bespoke Cursor predicate are deleted. Unknown fails closed. `agentIdentity` is absent when the host predates the field or had no evidence beyond the title, and delivery is an action: not delivering is visible and recoverable (the sender sees no recipients), while delivering to the wrong agent is neither. The optional field is additive, so an old client simply ignores it (wire rule 1). This is also the first real caller of the evidence parser and the identity resolver. Tests: 27 in groups, 8 for the publisher, 3 RPC fan-out cases updated to the new contract. The `@cursor`-must-not-match-"text cursor blink" hazard is now excluded structurally instead of by a per-agent predicate. Verified non-vacuous by mutation: swapping the process/title ranks fails 2 publisher assertions, and reverting groups to title matching fails 15 of 27. One earlier mutation silently failed to apply after formatting reflowed the block — the file was checked before trusting the result. * perf(runtime): reuse terminal title during summary build * fix(orchestration): refuse title evidence when publishing identity for routing Rebuilt on current main so this carries the hardened parser from stablyai#16148 and the corrected resolver from stablyai#16157 (authority-scoped run keys, no order-dependent duplicate resolution). Applies the resolver's new `minimumSource` floor at the publisher. What this publishes authorizes an action — routing decides which real agent pane receives a message — so ranking title last is not enough; the floor removes it from consideration entirely, and no amount of reordering by a caller can bring it back. The trade, stated because it is a real capability loss: a hook-less agent over SSH that Orca did not launch, and whose foreground process the host cannot read, is no longer addressable by @agent. Accepted because a message delivered into the wrong agent's prompt is unrecoverable while an undelivered one is visible — the sender sees zero recipients. Whether real panes actually carry launch/foreground evidence is the open question, and is what live validation must answer. * fix(pty): preserve agent identity on daemon reattach * fix(runtime): retire stale pane agent identity * chore: normalize runtime types formatting * fix(agent-status): identify a pane from its own hook, not from how it was started Two defects, one cause: identity was inferred from the outside instead of read from the agent. **Hook evidence was never plumbed in.** The publisher considered `process`, `launch` and `title` and contained zero hook references — while the resolver ranks `live-hook` first. The top rung of the ladder was never connected. That made identity depend on Orca having launched the agent. Most agents are started by typing `claude` or `codex` at a shell, which leaves no launch record. On macOS the foreground process still names them, so the gap was invisible. On WSL the Windows host reads the foreground process as `wsl.exe` — the distro wrapper, not the agent inside it — so those panes had no signal at all and became unaddressable by `@agent`. A hook is the agent reporting itself, so it survives both: no launch record needed, and no dependency on reading a process across the WSL boundary. **`launch` outranked `completed-hook`.** Ranking is now by TENSE rather than by how authoritative a source sounds: present: live-hook > process past: completed-hook > launch > sleeping-session > sibling > title A launch record is an event, not a state — it stays true after the agent exits, which is why a pane reused after closing its agent kept reading as the old one. A completed hook at least proves the agent actually ran in that pane; a launch record only proves Orca tried to start one. Neither rank was covered: all 392 existing tests passed unchanged after reordering. Mutation now fails 2 on the old order and 4 with hook evidence removed. Known remaining gap, deliberately not papered over: a hand-started WSL agent with no managed hooks has no identity signal at all. Restoring a title guess there would reinstate the misdelivery this PR exists to prevent. * fix(orchestration): restore title as the last resort, not a forbidden source An earlier revision passed `minimumSource: 'launch'` so routing could not see a title at any rank, reasoning that a display string must never authorize a write. That conflated the evidence parser with the raw substring match it replaced. `buildAgentNameRe('claude').test(title)` was the misdelivery. `collectAgentTitleEvidence` returns null on exactly those shapes: "Review the Claude session-history fix" on a Codex pane yields nothing, and "Switch Claude and Codex off the load balancer… - grok" yields grok from its owner suffix. Ranking title last is therefore sufficient; refusing it is not necessary. Refusing it had a real cost. An agent a user starts by hand inside an Orca WSL terminal has no launch record, no readable foreground process (the Windows host sees `wsl.exe`, not the agent in the distro), and — until managed Codex hooks install there — no hook either. An unambiguous title was the only thing left, and dropping it made that pane unaddressable by @agent where the previous code could reach it. That is a regression, and most agents are started that way. End-to-end coverage added at the routing layer with title allowed: @claude still does not reach a Codex pane whose task text names Claude, @codex still does not reach a Grok pane whose task text names Codex, and a pane identified only by an unambiguous title is reachable again. * revert(agent-status): keep launch above completed-hook until run keys exist Reverts the tense-based reorder from this branch. The reasoning behind it was sound as far as it went — a launch record is a past event, not an observation, which is why a reused pane kept reading as its previous agent — but it fixed one staleness by opening a worse one. A completed hook is past tense too, and without an agent-run key it never expires at all. Ranking it above `launch` lets a stale hook from a previous agent outrank the launch record Orca stamped for the process running NOW. pane-agent-owner.ts already says this in its own comment: "Ranking launch/live-hook above the completed/sleeping records keeps a genuine pane on its real agent and stops a stale record from hijacking it." The reorder belongs with authority-scoped run generation, which is what makes any past-tense evidence expire. It is staged in the migration plan rather than shipped here. What this branch keeps: hook evidence feeding pane identity (so an agent a user starts by hand is identified from its own report rather than needing a launch record), and title restored as a genuine last resort behind the evidence parser. * fix(runtime): guard the pane key so terminal.list survives a non-UUID leaf `makePaneKey` throws on a leaf id that is not a UUID. The hook-evidence lookup called it unguarded inside `buildTerminalSummary`, so a single such leaf took down `terminal.list` for the whole list rather than degrading that one pane — 136 tests across 5 files, and the native code-quality gate tripped separately on a duplicate test title. Both were mine, and both were caught by CI rather than by me: I ran the focused suites before pushing instead of the affected directories. * fix(runtime): declare published terminal agent identity * fix(runtime): demote completed hook identity evidence (cherry picked from commit 6ba6d58)
…tablyai#16148) * feat(agent-status): add an order-independent title evidence parser The chain this will replace is a first-match-wins scan of substring predicates, so its answer is decided by list position rather than by how strong the evidence is. Four real recorded Grok panes read as Codex today because Codex is checked first and their task text mentions it. Reordering cannot fix that: whichever branch is hoisted, some other pair breaks. collectAgentTitleEvidence collects every signal first and ranks by class afterwards: vendor marker — a sigil or control sequence the agent itself emits; task text cannot forge it anchored name — a name where a grammar reserves the position for identity (Orca's `- <agent>` owner suffix, or the whole undecorated remainder) free-text name — a name anywhere else Anchored beats vendor marker, so `✳ agy` is Antigravity rather than Claude. Free text never beats either, and never becomes identity on its own even as the only name present — an absent icon is recoverable, a confidently wrong one is not. Conflicts within a class resolve to null. No consumer imports this yet. Swapping getAgentLabel in place would move ~20 call sites at once; each migrates behind the resolver, with its own evidence. Measured against the live recorded-title corpus (747 distinct titles), 598 agree with the current chain and 149 differ: 144 claude -> null spinner-only titles. Braille and quarter-circle frames are emitted by many agents, so they prove the pane is busy and nothing about who it is. These are answered by stronger signals once the resolver lands, which is why the parser is not wired up on its own. 4 codex -> grok the misattributed Grok panes, fixed. 1 codex -> null a spinner-prefixed Claude pane whose task text names Codex. Three parser defects were found and fixed by reviewing that delta rather than reasoning about it: the OpenCode envelope was treated as a vetoable marker instead of an owning grammar, free text was allowed to veto a vendor marker (which blinded 13 real Claude titles), and the owner suffix matched the tail of a hyphenated worktree name (`review-14600-codex`). * fix(agent-status): harden title evidence collection * fix(agent-status): limit emitted display evidence * fix(agent-status): anchor explicit title evidence * test(agent-status): cover anchored token filtering * fix(agent-status): complete explicit marker evidence * fix(agent-status): recognize reserved owner ids * fix(agent-status): reject cwd path titles * fix(agent-status): avoid bare-name identity guesses * fix(agent-status): require spinner for working labels * fix(agent-status): honor opted-out synthetic profiles * fix(agent-status): harden wrapper evidence boundaries
…#16575) * feat(agent-status): measure identity evidence before migrating any consumer PR 1 of the identity migration. It changes no displayed or routed identity — it only measures. Why measure first: the hierarchy shipped in stablyai#16148/stablyai#16157 has zero consumers, while ~31 sites still derive identity independently. Every migration decision after this is currently a guess, including the one that matters most — how often a real pane has no evidence at all. A live P0 reports "No Claude status shown", and this design trades toward showing nothing when uncertain, so the blank rate has to be a number before any surface moves. - `pane-agent-identity-evidence.ts` — one assembler that gathers a pane's evidence, so consumers stop each inventing their own ladder. - `pane-agent-identity-census.ts` — shadow-only counters keyed by host kind (native / wsl-host / wsl-distro / ssh / relay) and launch mode (typed / orca-launch / resume). Records a bitmask of which sources were present and whether the resolver returned null or ambiguous. No titles, prompts, paths, handles, or agent text. - `pane-agent-identity-inventory.test.ts` — a ratchet that fails when a legacy identity helper gains a new production caller, so the surface cannot grow while the migration runs. Three review findings are encoded rather than deferred: launch stays above run-key-less completed hooks (promoting the hook lets a stale record hijack a pane); OMP/Pi evidence is owner-normalized before assembly, since OMP emits Pi-compatible frames and a wrapper's hook would otherwise be read as the agent it wraps; and Windows-side `wsl.exe` is rejected as process evidence, because the host observes the distro wrapper rather than the agent inside it. The census cannot be completed from a worktree. It needs representative native, SSH, WSL and relay cohorts collected from real use, and that review is the gate on PR 3 — not this PR. * test(agent-status): keep identity migration inventory-only * test(agent-status): reuse reliable source scanner * test(agent-status): bound inventory scan work * test(agent-status): avoid inventory path false negatives * test(agent-status): refresh identity inventory after base repair * test(agent-status): correct inventory classifications * test(agent-status): correct action boundary inventory * test(agent-status): pin inventory occurrence counts * test(agent-status): fail closed on scanner desync
…erminal title (stablyai#16237) * feat(agent-status): add the pane agent identity resolver Four ladders answer "which agent is in this pane" independently — the tab icon, the open-tab/search occupant, the sidebar title rows, and the sidebar hook-row fallback — and they disagree. Two consult the terminal title before the launch record, so a string Orca parsed outranks a fact Orca owns. resolvePaneAgentIdentity is the single ranked answer. Two rules, one of which is not an ordering: 1. Evidence is ranked by how directly it observes the process; a display title is last. 2. Each observation carries the runId of the agent run it describes. Evidence from a superseded run is INELIGIBLE, not merely outranked. Rule 2 is the part reordering could never supply. A completed hook naming A plus a title naming B is either a bug (hook right, title stale) or a legitimate pane reclaim (title right) — identical signals, opposite correct answers. Run ids make them different facts: in the bug both belong to the current run; in the reclaim the hook belongs to a previous one. That pair ships as a test asserting the two produce opposite answers from the same evidence. Missing run ids are treated as eligible. Absence means "this peer does not publish them", not "this is stale", so an old host's rows are never blanked. Sibling evidence is opt-in so pane-scoped consumers cannot inherit another pane's agent. No consumer imports this yet; each migrates separately with its own evidence. Verified non-vacuous: reversing the authority order fails 10 of 18 assertions and removing the run filter fails 3. * fix(agent-status): close three resolver contract holes found in review **Duplicate evidence of one source resolved by array order.** `eligible.find(...)` returned the first match, so two live hooks naming different agents were settled by input position — the exact property this resolver exists to remove. The original order-independence test only used DISTINCT sources, so it never exercised it. Conflicting same-class evidence now returns null with `ambiguousAt`, and does NOT fall through to a weaker source: letting a title answer whenever two hooks disagree is worse than saying nothing. **A bare numeric runId collided across authority restarts.** `incarnation` is a total order only within one `authorityId` (agent-status-observation.ts states this), and the id is regenerated per authority instance, so a restarted host counting from its own floor would report `1` and match an unrelated live run 1. The run key now carries its authority, and evidence from a DIFFERENT authority is treated as incomparable — kept, like an absent key — rather than as stale. **Title stayed reachable by consumers that authorize writes.** Ranking it last makes misuse unlikely; `minimumSource` makes it impossible. An action consumer passes `'launch'` and weaker evidence is dropped before ranking, so routing or delivery cannot name a target from a parsed string even by reordering its inputs. Display surfaces omit it and are unaffected. Also restores the generic agent-vocabulary parameter, which lives on the routing branch and was lost when this branch was rebased. Each fix is mutation-verified: first-match restored fails 3, ignoring authority fails 1, dropping the floor fails 2. The authority test was itself vacuous on the first attempt — both sides used `incarnation: 1`, so a resolver ignoring authority still passed on the numeric compare. It now uses differing incarnations. The remaining review finding, that `process > launch` has no freshness bound, is NOT fixed here: it needs an observation timestamp the evidence type does not yet carry. Recorded rather than silently dropped. * fix(orchestration): route @agent messages by resolved identity, not terminal title `@claude` picked its recipients with `buildAgentNameRe('claude').test(title)`, so any pane whose TITLE contained the word received Claude's messages. Terminal titles carry task text, and people describe agent work in them, so this is the ordinary case rather than a contrived one: the recorded title "Switch Claude and Codex off the load balancer… - grok" is a Grok pane that received both @claude and @codex. Misdelivered instructions, not a cosmetic slip. The cause is that `RuntimeTerminalSummary` carried no identity at all — `title` was the only identity-ish field on it, so routing by title was the only option available. Fix the input: - `RuntimeTerminalSummary.agentIdentity?: TuiAgent` — optional, host-resolved from launch and foreground-process evidence the host owns, with the title ranked last and contributing only when the evidence parser finds an unambiguous name. A title that merely mentions an agent yields no evidence, which is the whole point. - `resolvePublishedPaneAgentIdentity` in `src/shared` rather than inside the runtime class, so the decision is testable without a runtime and so routing, delivery and the UI cannot drift. - Groups match `agentIdentity`; the title matcher and its bespoke Cursor predicate are deleted. Unknown fails closed. `agentIdentity` is absent when the host predates the field or had no evidence beyond the title, and delivery is an action: not delivering is visible and recoverable (the sender sees no recipients), while delivering to the wrong agent is neither. The optional field is additive, so an old client simply ignores it (wire rule 1). This is also the first real caller of the evidence parser and the identity resolver. Tests: 27 in groups, 8 for the publisher, 3 RPC fan-out cases updated to the new contract. The `@cursor`-must-not-match-"text cursor blink" hazard is now excluded structurally instead of by a per-agent predicate. Verified non-vacuous by mutation: swapping the process/title ranks fails 2 publisher assertions, and reverting groups to title matching fails 15 of 27. One earlier mutation silently failed to apply after formatting reflowed the block — the file was checked before trusting the result. * perf(runtime): reuse terminal title during summary build * fix(orchestration): refuse title evidence when publishing identity for routing Rebuilt on current main so this carries the hardened parser from stablyai#16148 and the corrected resolver from stablyai#16157 (authority-scoped run keys, no order-dependent duplicate resolution). Applies the resolver's new `minimumSource` floor at the publisher. What this publishes authorizes an action — routing decides which real agent pane receives a message — so ranking title last is not enough; the floor removes it from consideration entirely, and no amount of reordering by a caller can bring it back. The trade, stated because it is a real capability loss: a hook-less agent over SSH that Orca did not launch, and whose foreground process the host cannot read, is no longer addressable by @agent. Accepted because a message delivered into the wrong agent's prompt is unrecoverable while an undelivered one is visible — the sender sees zero recipients. Whether real panes actually carry launch/foreground evidence is the open question, and is what live validation must answer. * fix(pty): preserve agent identity on daemon reattach * fix(runtime): retire stale pane agent identity * chore: normalize runtime types formatting * fix(agent-status): identify a pane from its own hook, not from how it was started Two defects, one cause: identity was inferred from the outside instead of read from the agent. **Hook evidence was never plumbed in.** The publisher considered `process`, `launch` and `title` and contained zero hook references — while the resolver ranks `live-hook` first. The top rung of the ladder was never connected. That made identity depend on Orca having launched the agent. Most agents are started by typing `claude` or `codex` at a shell, which leaves no launch record. On macOS the foreground process still names them, so the gap was invisible. On WSL the Windows host reads the foreground process as `wsl.exe` — the distro wrapper, not the agent inside it — so those panes had no signal at all and became unaddressable by `@agent`. A hook is the agent reporting itself, so it survives both: no launch record needed, and no dependency on reading a process across the WSL boundary. **`launch` outranked `completed-hook`.** Ranking is now by TENSE rather than by how authoritative a source sounds: present: live-hook > process past: completed-hook > launch > sleeping-session > sibling > title A launch record is an event, not a state — it stays true after the agent exits, which is why a pane reused after closing its agent kept reading as the old one. A completed hook at least proves the agent actually ran in that pane; a launch record only proves Orca tried to start one. Neither rank was covered: all 392 existing tests passed unchanged after reordering. Mutation now fails 2 on the old order and 4 with hook evidence removed. Known remaining gap, deliberately not papered over: a hand-started WSL agent with no managed hooks has no identity signal at all. Restoring a title guess there would reinstate the misdelivery this PR exists to prevent. * fix(orchestration): restore title as the last resort, not a forbidden source An earlier revision passed `minimumSource: 'launch'` so routing could not see a title at any rank, reasoning that a display string must never authorize a write. That conflated the evidence parser with the raw substring match it replaced. `buildAgentNameRe('claude').test(title)` was the misdelivery. `collectAgentTitleEvidence` returns null on exactly those shapes: "Review the Claude session-history fix" on a Codex pane yields nothing, and "Switch Claude and Codex off the load balancer… - grok" yields grok from its owner suffix. Ranking title last is therefore sufficient; refusing it is not necessary. Refusing it had a real cost. An agent a user starts by hand inside an Orca WSL terminal has no launch record, no readable foreground process (the Windows host sees `wsl.exe`, not the agent in the distro), and — until managed Codex hooks install there — no hook either. An unambiguous title was the only thing left, and dropping it made that pane unaddressable by @agent where the previous code could reach it. That is a regression, and most agents are started that way. End-to-end coverage added at the routing layer with title allowed: @claude still does not reach a Codex pane whose task text names Claude, @codex still does not reach a Grok pane whose task text names Codex, and a pane identified only by an unambiguous title is reachable again. * revert(agent-status): keep launch above completed-hook until run keys exist Reverts the tense-based reorder from this branch. The reasoning behind it was sound as far as it went — a launch record is a past event, not an observation, which is why a reused pane kept reading as its previous agent — but it fixed one staleness by opening a worse one. A completed hook is past tense too, and without an agent-run key it never expires at all. Ranking it above `launch` lets a stale hook from a previous agent outrank the launch record Orca stamped for the process running NOW. pane-agent-owner.ts already says this in its own comment: "Ranking launch/live-hook above the completed/sleeping records keeps a genuine pane on its real agent and stops a stale record from hijacking it." The reorder belongs with authority-scoped run generation, which is what makes any past-tense evidence expire. It is staged in the migration plan rather than shipped here. What this branch keeps: hook evidence feeding pane identity (so an agent a user starts by hand is identified from its own report rather than needing a launch record), and title restored as a genuine last resort behind the evidence parser. * fix(runtime): guard the pane key so terminal.list survives a non-UUID leaf `makePaneKey` throws on a leaf id that is not a UUID. The hook-evidence lookup called it unguarded inside `buildTerminalSummary`, so a single such leaf took down `terminal.list` for the whole list rather than degrading that one pane — 136 tests across 5 files, and the native code-quality gate tripped separately on a duplicate test title. Both were mine, and both were caught by CI rather than by me: I ran the focused suites before pushing instead of the affected directories. * fix(runtime): declare published terminal agent identity * fix(runtime): demote completed hook identity evidence


ELI5
Orca decides which agent is in a pane by scanning the terminal title for agent names in a fixed order — first match wins. So a Grok pane whose task text happens to mention Codex reads as Codex, because Codex is checked first. Four real recorded titles are wrong today for exactly that reason.
This adds a parser that gathers all the evidence first and ranks it by how trustworthy the signal is instead of by list position. Nothing uses it yet.
What Changed
One new module plus its tests. No consumer imports it —
grepforcollectAgentTitleEvidenceoutside the module returns nothing.collectAgentTitleEvidence(title)sorts every signal into three classes:✳, the four Gemini glyphs, theOC |envelope)- <agent>owner suffix, or the whole undecorated remainderAnchored beats vendor marker, so
✳ agyresolves to Antigravity rather than Claude — the strongest possible name evidence wins instead of losing to a sigil by position. Free text beats neither, and never becomes identity on its own even when it is the only name present: an absent icon is recoverable, a confidently wrong one is not. Conflicts inside a class resolve tonull.Antigravity's
Gemini <n.n> <Name>models are handled by reading identity-plus-model as a grammar, so the gemini token is metadata. That is the general rule rather than an exception living inside the Gemini detector.Why not swap
getAgentLabeldirectlyIt has ~20 importers and they include write-authorizing paths, not just icons. Replacing it in place moves all of them in one commit with no way to attribute a regression. Each consumer migrates behind the resolver, separately, with its own evidence.
Testing
24 assertions covering the recorded misclassifications, the
✳ agyand Antigravity-model cases, hyphenated worktree names, spinner-only titles, and same-class conflicts — plus an order-independence property test: swapping the two agent names in a title must not change the answer, which is precisely what fails on the current chain.Measured against the live recorded-title corpus (747 distinct titles): 598 agree, 149 differ.
That 144 is the load-bearing number in this PR: it is why the parser ships dark. On its own it would blank real Claude rows; behind the resolver those panes are identified by hook, launch, and process evidence long before the title is consulted.
Reviewing the delta found three parser defects that reasoning had missed:
OC |envelope was a vetoable marker instead of an owning grammar, so 4 real titles lost their identity to session text naming another agent;✳ <task text naming Codex>Claude titles;review-14600-codex).typecheck, oxlint, oxfmt clean. Existing title/status suites: 59 files, 665 tests, all passing.
Linked Issue
Part of the agent-identity rearchitecture tracked by #9236. No single issue is closed by this PR on its own — it is one stage of a staged change, and behavior only moves once consumers migrate.
Visual Proof
N/A— this module has zero importers and renders nothing, so there is no user-visible behavior to capture. Screenshotting it would imply coverage that does not exist. The manual-validation arm was deliberately skipped for that reason.Testing
pnpm exec vitest run --config config/vitest.config.ts src/shared/agent-title-evidence.test.ts— 24 passed.Measured against the live recorded-title corpus (747 distinct titles): 598 agree with the current chain, 149 differ, and every delta group is enumerated above. Reviewing that delta — rather than reasoning about the code — is what found three defects during implementation.
Agent skill upstream boundary
Notes
src/shared, no platform branches, no paths, no shell.Checklist
N/Awith reasontypecheck,oxlint,oxfmtand focused tests pass locally; CI covers the rest