Repository navigation
feat(agent-status): add the pane agent identity resolver - #16157
Conversation
📝 WalkthroughWalkthroughAdded a shared Merge Risk: 🔵 Low · up to The new resolver’s diagnostic output can vary with evidence ordering even when the selected agent identity is unchanged. Because the module has no consumers yet, this is a bounded, non-blocking risk that should be addressed with deterministic ordering before dependent code adopts it. 🚥 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/pane-agent-identity-resolver.ts (1)
3-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReduce the implementation-history comment.
Keep the durable resolver contract in this file. Move the detailed history of prior ladders to design documentation or an issue.
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: 878f80a2-c183-4c35-90e8-a4ce391fe951
📒 Files selected for processing (2)
src/shared/pane-agent-identity-resolver.test.tssrc/shared/pane-agent-identity-resolver.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 1 remains after this review.
|
QA evidence (captured against PR #16237, brennanb2025/agent-group-routing-identity, stacked on this resolver; #16157 itself has zero importers and renders no behavior). The rendered Orca terminal ran the real group-routing tests: |
c5f9a63 to
8ac06a2
Compare
|
Codex review loop - status Reviewed head: Verdict: no release-blocking defects found. The resolver’s process/live-hook/launch/completed-hook ordering is defensible, run generations correctly separate stale hooks from pane reclaim, unknown run IDs preserve mixed-version rows, and the deliberately unwired module leaves migration behavior attributable. The residual design risk is that title fallback remains available to action consumers; the stacked caller fails closed on null, and a future trusted-source floor would make misuse impossible. Checks: resolver + observation tests 26 passed; mutation checks were non-vacuous (reversed rank failed 10/18, run filter removed failed 3/18); No additional implementation commits were needed after rebasing onto current main; final branch head is |
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.
**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.
8ac06a2 to
ce83494
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 665fa7b6-0aa8-4da7-9fe7-318dd085854c
📒 Files selected for processing (2)
src/shared/pane-agent-identity-resolver.test.tssrc/shared/pane-agent-identity-resolver.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.
| superseded.push(item.source) | ||
| return false |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make supersededSources deterministic.
Lines 144-145 append sources in input order. Reversing two stale evidence items changes the returned diagnostics, even when the resolved identity is unchanged. Return the diagnostics in SOURCE_RANK order and add a reversed-input stale-run test.
Proposed fix
const eligible = input.evidence.filter((item) => {
// ...
})
+ const supersededSources = [...superseded].sort(
+ (left, right) => SOURCE_RANK.indexOf(left) - SOURCE_RANK.indexOf(right)
+ )
for (const source of SOURCE_RANK) {
// ...
- return { agent: null, source: null, ambiguousAt: source, supersededSources: superseded }
+ return { agent: null, source: null, ambiguousAt: source, supersededSources }
}
- return { agent: matches[0].agent, source, supersededSources: superseded }
+ return { agent: matches[0].agent, source, supersededSources }
}
- return { agent: null, source: null, supersededSources: superseded }
+ return { agent: null, source: null, supersededSources }📝 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.
…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.
) * 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. (cherry picked from commit 4f525f1)
* 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
) * 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.
…#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)
) * 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.
…#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
Four different places in Orca each work out "which agent is in this pane" their own way, and they disagree with each other. Two of them read the terminal title before the launch record — so a string Orca parsed outranks a fact Orca owns.
This adds the one function that answers it. Nothing calls it yet.
What Changed
resolvePaneAgentIdentityranks evidence by how directly it observes the process:Two rules, and only one of them is an ordering:
runIdof the agent run it describes. Evidence from a superseded run is ineligible, not merely outranked.Why rule 2 exists
Rule 1 alone cannot fix this, and it is worth being precise about why.
A completed hook naming A plus a title naming B is either:
Identical signals. Opposite correct answers. No precedence order can separate them, which is what an earlier review of this problem established.
Run ids make them different facts: in the bug both belong to the current run, and in the reclaim the hook belongs to a previous one. That pair ships as an explicit assertion — the same evidence resolving two ways given only the run ids.
A run advances only on positive evidence that a new agent started: an accepted launch, a recognized command at a shell prompt, a host-confirmed foreground change, a new provider session. Never because a title changed, and never on transport loss.
Mixed-version behavior
runId: undefinedis eligible. Absence means "this peer does not publish run ids", not "this is stale" — treating unknown as superseded would blank every row arriving from an older host.currentRunId: undefineddisables run filtering entirely.Sibling evidence is opt-in (
allowSibling), so pane-scoped consumers — routing, native chat, keyboard encoding — cannot inherit a neighbouring pane's agent. Tab-level surfaces opt in.Returns
nullrather than guessing. An absent icon is recoverable; at the action surfaces (orchestration routing, mailbox delivery, prompt-cache timers) a wrong one is a misdelivery.Testing
18 assertions: title-is-last against every stronger source, the bug/reclaim pair, mixed-version absence in both directions, sibling scoping, input-order independence.
Verified non-vacuous by mutation, not by reading:
typecheck, oxlint, oxfmt clean.
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 is a pure function, 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/pane-agent-identity-resolver.test.ts— 18 passed.Verified non-vacuous by mutation rather than by reading: reversing the authority order fails 10 of 18 assertions, and removing the run filter fails 3. Restored, 18 pass.
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