Files
orca/src/shared/remote-runtime-shared-control-frame-handler.ts
T
Brennan Benson cd8c66551a fix(agent-hooks): resumed Claude Code session gets its sidebar agent row at SessionStart (STA-3386) (#12859)
* fix(agent-hooks): give resumed Claude sessions a sidebar row at SessionStart (STA-3386)

Claude's hook set never registered SessionStart and normalizeClaudeEvent
dropped it at ingest, so a resumed session that idled produced zero hook
traffic and earned no sidebar agent row until the first prompt.

- Register SessionStart in CLAUDE_EVENTS (local + remote installs).
- Map lead SessionStart (startup/resume/clear) to an idle 'done' row,
  resetting stale roster/task/cron/tool/prompt state like the Codex path;
  compact restarts and child-attributed SessionStart stay dropped.
- Thread hookEventName through the agent-status IPC payload so the
  completion coordinator can tell a session connect from a turn result;
  a SessionStart 'done' no longer raises agent-task-complete.

* fix(agent-hooks): mark SessionStart rows as session boundaries, not completions (STA-3386)

Review follow-up: represent the idle connect as a first-class
sessionBoundary flag on the status payload instead of gating one
renderer consumer on hookEventName.

- sessionBoundary rides AgentStatusPayload/AgentStatusEntry (done-only,
  clamped like interrupted); drops the hookEventName IPC threading.
- Completion-reactive consumers ignore session boundaries: the
  completion coordinator (task-complete notifications), automation
  dispatch observers (a connecting agent no longer completes the run
  and closes its tab), activity unread counts, and the dashboard
  finished timestamp; the status slice keeps boundaries out of
  stateHistory and preserves the flag across done->done repaints.
- SessionStart sources are allowlisted (startup/resume/clear) so
  compact restarts or unknown sources fail closed mid-turn.
- A live SessionStart now un-retires a reusable pane like a fresh
  prompt, so resume-in-reused-pane earns its row too.

* fix(agent-hooks): keep session-boundary dones out of teardown and completion history (STA-3386)

Review round 2:
- A boundary done no longer deletes the pane's launch-config registry
  entry, so a resumed idle TUI keeps its registered-launch-agent
  identity evidence.
- A boundary landing on a REAL done pushes that completion into
  stateHistory so the finished timestamp and unread badge survive a
  resume//clear right after a finish.
- The done->done flag carry yields to turn evidence (assistant message
  or changed prompt) so a genuine completion can never be suppressed.
- Star-nag value-moment observer and the server's OSC-equivalence
  dedupe now discriminate the flag.

* fix(agent-hooks): keep a displaced completion unread in the sidebar badge (STA-3386)

Review round 3: sidebar-badge mode counts only the live entry, so a
session boundary landing on an unacknowledged completion silently
dropped the sidebar badge while the agent-events count kept it. Count
the displaced completion from history for boundary rows, and pin the
behavior with countActivityUnread tests.

* fix(agent-hooks): prevent SessionStart completion side effects (STA-3386)

* fix(agent-hooks): preserve SessionStart through renderer IPC (STA-3386)
2026-08-05 22:06:36 -07:00

81 lines
2.7 KiB
TypeScript

import { parseAuthenticatedFrame, parseReadyFrame } from './remote-runtime-request-frames'
import type { RemoteRuntimeClientError } from './remote-runtime-client-error'
import {
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY
} from './protocol-version'
import { dispatchSharedControlFrame } from './remote-runtime-shared-control-frame-dispatch'
import { parseSharedControlFrame } from './remote-runtime-shared-control-protocol'
import type { SharedControlRetiredRequestIds } from './remote-runtime-shared-control-retired-request-ids'
import { resolveSharedControlReadyWaiters } from './remote-runtime-shared-control-state'
import type {
SharedControlConnectionState,
SharedControlLogicalSubscription,
SharedControlPendingRequest,
SharedControlReadyWaiter
} from './remote-runtime-shared-control-types'
export function handleSharedControlTextFrame(args: {
frame: string
state: SharedControlConnectionState
sharedKey: Uint8Array | null
deviceToken: string
environmentId?: string
pendingRequests: Map<string, SharedControlPendingRequest<unknown>>
subscriptions: Map<string, SharedControlLogicalSubscription<unknown>>
retiredRequestIds: SharedControlRetiredRequestIds
readyWaiters: SharedControlReadyWaiter[]
setState: (state: SharedControlConnectionState) => void
handleSocketClosed: (error: RemoteRuntimeClientError) => void
sendEncrypted: (payload: unknown) => boolean
markReady: () => void
replaySubscriptions: () => void
}): void {
if (args.state === 'awaiting_ready') {
const error = parseReadyFrame(args.frame)
if (error) {
args.handleSocketClosed(error)
return
}
args.setState('awaiting_authenticated')
args.sendEncrypted({
type: 'e2ee_auth',
deviceToken: args.deviceToken,
clientCapabilities: [
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY
]
})
return
}
const parsed = parseSharedControlFrame(args.frame, args.sharedKey, args.state)
if (parsed.type === 'auth') {
const error = parseAuthenticatedFrame(parsed.plaintext)
if (error) {
args.handleSocketClosed(error)
return
}
args.setState('ready')
args.markReady()
resolveSharedControlReadyWaiters(args.readyWaiters)
args.replaySubscriptions()
return
}
if (parsed.type === 'error') {
args.handleSocketClosed(parsed.error)
return
}
dispatchSharedControlFrame({
environmentId: args.environmentId,
frame: parsed.frame,
pendingRequests: args.pendingRequests,
subscriptions: args.subscriptions,
retiredRequestIds: args.retiredRequestIds,
deviceToken: args.deviceToken,
send: args.sendEncrypted
})
}