mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
refactor(agent-hooks): split remote and status application logic
This commit is contained in:
@@ -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, {
|
||||
|
||||
@@ -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<ReturnType<typeof normalizeAgentProviderSession>>
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user