From 602b49881c8d66e68df927962155deff115281e9 Mon Sep 17 00:00:00 2001 From: Neil Date: Sat, 19 Sep 2026 07:20:07 -0700 Subject: [PATCH] refactor(agent-hooks): split remote and status application logic --- .../server/server-ingest-remote.ts | 73 ++++------------ .../server-remote-envelope-normalization.ts | 84 +++++++++++++++++++ .../server/server-status-application.ts | 70 ++++++++++++++++ .../server/server-status-update.ts | 71 ---------------- 4 files changed, 172 insertions(+), 126 deletions(-) create mode 100644 src/main/agent-hooks/server/server-remote-envelope-normalization.ts diff --git a/src/main/agent-hooks/server/server-ingest-remote.ts b/src/main/agent-hooks/server/server-ingest-remote.ts index 7c03888cabe..0bc8d7947a9 100644 --- a/src/main/agent-hooks/server/server-ingest-remote.ts +++ b/src/main/agent-hooks/server/server-ingest-remote.ts @@ -1,11 +1,8 @@ import { track } from '../../telemetry/client' import { normalizeAgentStatusPayload } from '../../../shared/agent-status-types' -import { normalizeAgentProviderSession } from '../../../shared/agent-session-resume' -import { isAgentHookSource, restoreShedStatusFields } from '../../../shared/agent-hook-relay' +import { restoreShedStatusFields } from '../../../shared/agent-hook-relay' import { MAX_PANE_KEY_LEN, - normalizeClaudePromptId, - normalizeGrokPromptId, warnOnHookEnvOrVersionMismatch } from '../../../shared/agent-hook-listener/listener-limits' import { @@ -23,6 +20,7 @@ import { olderPeerAgentStatusLegacyMode } from '../../../shared/agent-status-legacy-adapter' import { isValidPiProviderSessionOnly } from './server-status-identity' +import { normalizeRemoteEnvelopeFields } from './server-remote-envelope-normalization' import { AgentHookServerIngestStructured } from './server-ingest-structured' export abstract class AgentHookServerIngestRemote extends AgentHookServerIngestStructured { @@ -124,50 +122,20 @@ export abstract class AgentHookServerIngestRemote extends AgentHookServerIngestS return } let tabId = paneKey !== physicalPaneKey ? parsedPaneKey.tabId : reportedTabId - const hookEventName = - typeof envelope.hookEventName === 'string' && envelope.hookEventName.trim().length > 0 - ? envelope.hookEventName.trim() - : undefined - const source = isAgentHookSource(envelope.source) ? envelope.source : undefined - const providerPromptId = - source === 'claude' - ? normalizeClaudePromptId(envelope.providerPromptId) - : source === 'grok' - ? normalizeGrokPromptId(envelope.providerPromptId) - : undefined - const grokPromptBoundary = - source === 'grok' && envelope.grokPromptBoundary === true ? true : undefined - const compactTrigger = - source === 'claude' && - (envelope.compactTrigger === 'manual' || envelope.compactTrigger === 'auto') - ? envelope.compactTrigger - : undefined - const worktreeId = - envelope.worktreeId !== undefined && envelope.worktreeId.trim().length > 0 - ? envelope.worktreeId.trim() - : undefined - const promptInteractionKey = - typeof envelope.promptInteractionKey === 'string' && - envelope.promptInteractionKey.trim().length > 0 - ? envelope.promptInteractionKey.trim() - : undefined - const toolUseId = - typeof envelope.toolUseId === 'string' && envelope.toolUseId.trim().length > 0 - ? envelope.toolUseId.trim() - : undefined - const toolAgentId = - typeof envelope.toolAgentId === 'string' && envelope.toolAgentId.trim().length > 0 - ? envelope.toolAgentId.trim() - : undefined - const teammateName = - typeof envelope.teammateName === 'string' && envelope.teammateName.trim().length > 0 - ? envelope.teammateName.trim() - : undefined - const toolAgentType = - typeof envelope.toolAgentType === 'string' && envelope.toolAgentType.trim().length > 0 - ? envelope.toolAgentType.trim() - : undefined - const providerSession = normalizeAgentProviderSession(envelope.providerSession) ?? undefined + const { + hookEventName, + source, + providerPromptId, + grokPromptBoundary, + compactTrigger, + worktreeId, + promptInteractionKey, + toolUseId, + toolAgentId, + teammateName, + toolAgentType, + providerSession + } = normalizeRemoteEnvelopeFields(envelope) // Why: relay crosses a trust boundary — re-run the canonical normalizer to enforce caps/invariants (returns null on malformed). const validatedPayload = normalizeAgentStatusPayload(envelope.payload) if (!validatedPayload) { @@ -261,17 +229,13 @@ export abstract class AgentHookServerIngestRemote extends AgentHookServerIngestS paneKey, providerPromptId ) - // Why: an older relay built this payload before the boundary flag existed, so it arrives as a - // plain `done` — which every completion-reactive consumer reads as a finished turn. Stamp the - // boundary here so a compact stays silent regardless of which relay normalized it. + // Older relays omit the boundary flag; stamp it so compact completion stays silent. if (normalizedPayload.sessionBoundary !== true) { normalizedPayload = { ...normalizedPayload, sessionBoundary: true } } acceptedCompactCompletion = true } - // Why: keyed on "did we accept a completion", not on the trigger surviving the wire — the - // trigger-stripped replay is exactly the shape that arrives without one, and it is still the - // compact's own promptless event, so it still needs the summarized turn's label. + // Accepted compact completions retain the summarized turn label, including trigger-stripped replays. if ( source === 'claude' && (compactTrigger !== undefined || acceptedCompactCompletion) && @@ -283,7 +247,6 @@ export abstract class AgentHookServerIngestRemote extends AgentHookServerIngestS const applyClaudeBackgroundWork = normalizedPayload.agentType === 'claude' && typeof envelope.claudeRunningNonAgentTask === 'boolean' && - // Why: reconnect replay may seed a restarted listener, but cannot override any observation made by this runtime. (envelope.isReplay !== true || !this.runtimeObservedStatusPaneKeys.has(paneKey)) // Why: run the HTTP path's warn-once version/env-mismatch diagnostics with this.env as expected. warnOnHookEnvOrVersionMismatch(this.state, { diff --git a/src/main/agent-hooks/server/server-remote-envelope-normalization.ts b/src/main/agent-hooks/server/server-remote-envelope-normalization.ts new file mode 100644 index 00000000000..3765c8c0a50 --- /dev/null +++ b/src/main/agent-hooks/server/server-remote-envelope-normalization.ts @@ -0,0 +1,84 @@ +import { normalizeAgentProviderSession } from '../../../shared/agent-session-resume' +import { + normalizeClaudePromptId, + normalizeGrokPromptId +} from '../../../shared/agent-hook-listener/listener-limits' +import { isAgentHookSource, type AgentHookSource } from '../../../shared/agent-hook-relay' + +export type RemoteEnvelopeFields = { + hookEventName?: string + source?: AgentHookSource + providerPromptId?: string + grokPromptBoundary?: true + compactTrigger?: 'manual' | 'auto' + worktreeId?: string + promptInteractionKey?: string + toolUseId?: string + toolAgentId?: string + teammateName?: string + toolAgentType?: string + providerSession?: NonNullable> +} + +export function normalizeRemoteEnvelopeFields(envelope: { + hookEventName?: string + source?: unknown + providerPromptId?: unknown + grokPromptBoundary?: unknown + compactTrigger?: unknown + worktreeId?: string + promptInteractionKey?: string + toolUseId?: string + toolAgentId?: string + teammateName?: string + toolAgentType?: string + providerSession?: unknown +}): RemoteEnvelopeFields { + const source = isAgentHookSource(envelope.source) ? envelope.source : undefined + return { + hookEventName: + typeof envelope.hookEventName === 'string' && envelope.hookEventName.trim().length > 0 + ? envelope.hookEventName.trim() + : undefined, + source, + providerPromptId: + source === 'claude' + ? normalizeClaudePromptId(envelope.providerPromptId) + : source === 'grok' + ? normalizeGrokPromptId(envelope.providerPromptId) + : undefined, + grokPromptBoundary: + source === 'grok' && envelope.grokPromptBoundary === true ? true : undefined, + compactTrigger: + source === 'claude' && + (envelope.compactTrigger === 'manual' || envelope.compactTrigger === 'auto') + ? envelope.compactTrigger + : undefined, + worktreeId: + envelope.worktreeId !== undefined && envelope.worktreeId.trim().length > 0 + ? envelope.worktreeId.trim() + : undefined, + promptInteractionKey: + typeof envelope.promptInteractionKey === 'string' && + envelope.promptInteractionKey.trim().length > 0 + ? envelope.promptInteractionKey.trim() + : undefined, + toolUseId: + typeof envelope.toolUseId === 'string' && envelope.toolUseId.trim().length > 0 + ? envelope.toolUseId.trim() + : undefined, + toolAgentId: + typeof envelope.toolAgentId === 'string' && envelope.toolAgentId.trim().length > 0 + ? envelope.toolAgentId.trim() + : undefined, + teammateName: + typeof envelope.teammateName === 'string' && envelope.teammateName.trim().length > 0 + ? envelope.teammateName.trim() + : undefined, + toolAgentType: + typeof envelope.toolAgentType === 'string' && envelope.toolAgentType.trim().length > 0 + ? envelope.toolAgentType.trim() + : undefined, + providerSession: normalizeAgentProviderSession(envelope.providerSession) ?? undefined + } +} diff --git a/src/main/agent-hooks/server/server-status-application.ts b/src/main/agent-hooks/server/server-status-application.ts index 025c431dff6..8863dc178b9 100644 --- a/src/main/agent-hooks/server/server-status-application.ts +++ b/src/main/agent-hooks/server/server-status-application.ts @@ -9,6 +9,8 @@ import type { AgentStatusObservation, AgentStatusObservationOrigin } from '../../../shared/agent-status-observation' +import { AGENT_STATUS_2A_CURRENT_PRODUCER_MODE } from '../../../shared/agent-status-legacy-adapter' +import { admitLegacyAgentStatus } from '../../../shared/agent-hook-listener/listener-state' import type { EnrichedAgentHookEventPayload } from './server-types' import { agentTypeToPromptSentAgentKind } from './server-status-identity' import { AgentHookServerStatusDisposition } from './server-status-disposition' @@ -17,6 +19,74 @@ import { AgentHookServerStatusDisposition } from './server-status-disposition' const MAX_REMEMBERED_EVIDENCE_OBSERVATIONS = 1024 export abstract class AgentHookServerStatusApplication extends AgentHookServerStatusDisposition { + protected refreshTerminalStatusEvidence( + previous: EnrichedAgentHookEventPayload, + mutationBefore?: EnrichedAgentHookEventPayload, + emitEnrichedStatus = false + ): void { + if (!this.canWriteLegacyStatusRow(previous)) { + return + } + const connectionClearWatermark = previous.connectionId + ? this.connectionTimestampWatermarkById.get(previous.connectionId) + : undefined + const now = Math.max(Date.now(), (connectionClearWatermark ?? -1) + 1) + if (previous.connectionId) { + this.connectionTimestampWatermarkById.set(previous.connectionId, now) + } + const { + receivedAt: _receivedAt, + evidenceObservedAt: _evidenceObservedAt, + stateStartedAt, + observation: _observation, + restoredUnconfirmed: _restoredUnconfirmed, + isReplay: _isReplay, + ...payload + } = previous + const refreshed: EnrichedAgentHookEventPayload = { + ...payload, + receivedAt: now, + evidenceObservedAt: now, + stateStartedAt, + observation: this.stampObservation(payload, 'osc', now) + } + const firstRuntimeObservation = !this.runtimeObservedStatusPaneKeys.has(refreshed.paneKey) + this.runtimeObservedStatusPaneKeys.add(refreshed.paneKey) + if (!this.writeLegacyStatusRow(refreshed)) { + return + } + this.commitStatusRowMutation(mutationBefore ?? previous, refreshed) + this.scheduleStatusPersist() + // A dismissed row may retain only provider resume identity. Its preserved payload can still + // read `working`, but it is deliberately hidden from live readers and must not renew awake or + // mobile freshness leases. + if (refreshed.providerSessionOnly === true) { + return + } + if (firstRuntimeObservation) { + this.notifyStatusChangeListeners() + } + this.emitStatusFreshnessObservation({ + paneKey: refreshed.paneKey, + state: refreshed.payload.state, + receivedAt: refreshed.receivedAt, + observedInCurrentRuntime: true, + ...(refreshed.worktreeId ? { worktreeId: refreshed.worktreeId } : {}), + ...(refreshed.terminalHandle ? { terminalHandle: refreshed.terminalHandle } : {}) + }) + if (emitEnrichedStatus) { + this.emitEnrichedStatus(refreshed) + } + } + + protected writeLegacyStatusRow(entry: EnrichedAgentHookEventPayload): boolean { + return admitLegacyAgentStatus( + this.state, + 'main-status-update', + entry, + AGENT_STATUS_2A_CURRENT_PRODUCER_MODE + ) + } /** `observedAt` is the producer's own clock for evidence that has one (a session journal); it * stamps the evidence and state-start times while `receivedAt` keeps delivery order. */ protected attachStatusTiming( diff --git a/src/main/agent-hooks/server/server-status-update.ts b/src/main/agent-hooks/server/server-status-update.ts index b5a4d547dc4..1b4a0e75960 100644 --- a/src/main/agent-hooks/server/server-status-update.ts +++ b/src/main/agent-hooks/server/server-status-update.ts @@ -10,8 +10,6 @@ import { INTERRUPTED_DONE_LATE_WORKING_SUPPRESSION_MS } from './server-constants import type { EnrichedAgentHookEventPayload } from './server-types' import type { AgentHookEventPayload } from '../../../shared/agent-hook-listener/listener-event' import type { AgentStatusObservationOrigin } from '../../../shared/agent-status-observation' -import { AGENT_STATUS_2A_CURRENT_PRODUCER_MODE } from '../../../shared/agent-status-legacy-adapter' -import { admitLegacyAgentStatus } from '../../../shared/agent-hook-listener/listener-state' import { attachClaudeChildOnlyBoundary, attachClaudePermissionToolUseId, @@ -249,73 +247,4 @@ export abstract class AgentHookServerStatusUpdate extends AgentHookServerStatusA ) return enriched } - - protected refreshTerminalStatusEvidence( - previous: EnrichedAgentHookEventPayload, - mutationBefore?: EnrichedAgentHookEventPayload, - emitEnrichedStatus = false - ): void { - if (!this.canWriteLegacyStatusRow(previous)) { - return - } - const connectionClearWatermark = previous.connectionId - ? this.connectionTimestampWatermarkById.get(previous.connectionId) - : undefined - const now = Math.max(Date.now(), (connectionClearWatermark ?? -1) + 1) - if (previous.connectionId) { - this.connectionTimestampWatermarkById.set(previous.connectionId, now) - } - const { - receivedAt: _receivedAt, - evidenceObservedAt: _evidenceObservedAt, - stateStartedAt, - observation: _observation, - restoredUnconfirmed: _restoredUnconfirmed, - isReplay: _isReplay, - ...payload - } = previous - const refreshed: EnrichedAgentHookEventPayload = { - ...payload, - receivedAt: now, - evidenceObservedAt: now, - stateStartedAt, - observation: this.stampObservation(payload, 'osc', now) - } - const firstRuntimeObservation = !this.runtimeObservedStatusPaneKeys.has(refreshed.paneKey) - this.runtimeObservedStatusPaneKeys.add(refreshed.paneKey) - if (!this.writeLegacyStatusRow(refreshed)) { - return - } - this.commitStatusRowMutation(mutationBefore ?? previous, refreshed) - this.scheduleStatusPersist() - // A dismissed row may retain only provider resume identity. Its preserved payload can still - // read `working`, but it is deliberately hidden from live readers and must not renew awake or - // mobile freshness leases. - if (refreshed.providerSessionOnly === true) { - return - } - if (firstRuntimeObservation) { - this.notifyStatusChangeListeners() - } - this.emitStatusFreshnessObservation({ - paneKey: refreshed.paneKey, - state: refreshed.payload.state, - receivedAt: refreshed.receivedAt, - observedInCurrentRuntime: true, - ...(refreshed.worktreeId ? { worktreeId: refreshed.worktreeId } : {}), - ...(refreshed.terminalHandle ? { terminalHandle: refreshed.terminalHandle } : {}) - }) - if (emitEnrichedStatus) { - this.emitEnrichedStatus(refreshed) - } - } - - private writeLegacyStatusRow(entry: EnrichedAgentHookEventPayload): boolean { - return admitLegacyAgentStatus( - this.state, - 'main-status-update', - entry, - AGENT_STATUS_2A_CURRENT_PRODUCER_MODE - ) - } }