agent-relay-server 0.124.0 → 0.126.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/openapi.json +1 -1
- package/package.json +9 -5
- package/public/assets/{MessageBubble-SQK8-Mzp.js → MessageBubble-B6YgulnX.js} +2 -2
- package/public/assets/{MessageBubble-SQK8-Mzp.js.map → MessageBubble-B6YgulnX.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-CS-aow4S.js → PlanGraphPanel--DNxHn1T.js} +2 -2
- package/public/assets/{PlanGraphPanel-CS-aow4S.js.map → PlanGraphPanel--DNxHn1T.js.map} +1 -1
- package/public/assets/{activity-W31XvhC2.js → activity-Fo5aLYD6.js} +2 -2
- package/public/assets/{activity-W31XvhC2.js.map → activity-Fo5aLYD6.js.map} +1 -1
- package/public/assets/agent-profiles-C3p0xclG.js +2 -0
- package/public/assets/{agent-profiles-DXysA9To.js.map → agent-profiles-C3p0xclG.js.map} +1 -1
- package/public/assets/{agent-type-icon-BxUopALO.js → agent-type-icon-BmBR-9CH.js} +2 -2
- package/public/assets/{agent-type-icon-BxUopALO.js.map → agent-type-icon-BmBR-9CH.js.map} +1 -1
- package/public/assets/{agents-B5GRNRnN.js → agents-_GBQyF5b.js} +2 -2
- package/public/assets/{agents-B5GRNRnN.js.map → agents-_GBQyF5b.js.map} +1 -1
- package/public/assets/{analytics-DqAuUAwX.js → analytics-BZtRycyL.js} +2 -2
- package/public/assets/{analytics-DqAuUAwX.js.map → analytics-BZtRycyL.js.map} +1 -1
- package/public/assets/{automation-DYlgHYcb.js → automation-Cf_Ewi9f.js} +2 -2
- package/public/assets/{automation-DYlgHYcb.js.map → automation-Cf_Ewi9f.js.map} +1 -1
- package/public/assets/{badge-XEru6wto.js → badge-BCvp7LK3.js} +2 -2
- package/public/assets/{badge-XEru6wto.js.map → badge-BCvp7LK3.js.map} +1 -1
- package/public/assets/{branch-state-badge-Mf0Ow77K.js → branch-state-badge-Cscjqf-E.js} +2 -2
- package/public/assets/{branch-state-badge-Mf0Ow77K.js.map → branch-state-badge-Cscjqf-E.js.map} +1 -1
- package/public/assets/{button-C3CQSr4p.js → button-DyHjsHY3.js} +2 -2
- package/public/assets/{button-C3CQSr4p.js.map → button-DyHjsHY3.js.map} +1 -1
- package/public/assets/{card-C5XTIPvq.js → card-BoqYxpVW.js} +2 -2
- package/public/assets/{card-C5XTIPvq.js.map → card-BoqYxpVW.js.map} +1 -1
- package/public/assets/{channels-DVRkVeky.js → channels-DAtOnA6o.js} +2 -2
- package/public/assets/{channels-DVRkVeky.js.map → channels-DAtOnA6o.js.map} +1 -1
- package/public/assets/chat-BdbIWZpQ.js +2 -0
- package/public/assets/chat-BdbIWZpQ.js.map +1 -0
- package/public/assets/{connectors-Cqubkw9C.js → connectors-B2grC-hl.js} +2 -2
- package/public/assets/{connectors-Cqubkw9C.js.map → connectors-B2grC-hl.js.map} +1 -1
- package/public/assets/{copy-button-Bil2CjzT.js → copy-button-C67FUG3E.js} +2 -2
- package/public/assets/{copy-button-Bil2CjzT.js.map → copy-button-C67FUG3E.js.map} +1 -1
- package/public/assets/{display-GFbGUG0O.js → display-aTnwsqQh.js} +3 -3
- package/public/assets/{display-GFbGUG0O.js.map → display-aTnwsqQh.js.map} +1 -1
- package/public/assets/{dist-V3wu3BHw.js → dist-BD4Xc13v.js} +2 -2
- package/public/assets/{dist-V3wu3BHw.js.map → dist-BD4Xc13v.js.map} +1 -1
- package/public/assets/{dist-C5z05Vkz.js → dist-BFDfbfNC.js} +2 -2
- package/public/assets/{dist-C5z05Vkz.js.map → dist-BFDfbfNC.js.map} +1 -1
- package/public/assets/{dist-CjyfQThq.js → dist-BJKxqIQd.js} +2 -2
- package/public/assets/{dist-CjyfQThq.js.map → dist-BJKxqIQd.js.map} +1 -1
- package/public/assets/{dist-B8tD9ZcR.js → dist-BNB3fTO-.js} +2 -2
- package/public/assets/{dist-B8tD9ZcR.js.map → dist-BNB3fTO-.js.map} +1 -1
- package/public/assets/{dist-CFtFIhu3.js → dist-CHKra2JV.js} +2 -2
- package/public/assets/{dist-CFtFIhu3.js.map → dist-CHKra2JV.js.map} +1 -1
- package/public/assets/{dist-r8wyUTkK.js → dist-CQMQ5iku.js} +2 -2
- package/public/assets/{dist-r8wyUTkK.js.map → dist-CQMQ5iku.js.map} +1 -1
- package/public/assets/{dist-DKmTXfpP.js → dist-C_VOBSaK.js} +2 -2
- package/public/assets/{dist-DKmTXfpP.js.map → dist-C_VOBSaK.js.map} +1 -1
- package/public/assets/{dist-CGaHpxnN.js → dist-D5w04z0l.js} +2 -2
- package/public/assets/{dist-CGaHpxnN.js.map → dist-D5w04z0l.js.map} +1 -1
- package/public/assets/{dist-4VwhnLtj.js → dist-DG75bvPp.js} +2 -2
- package/public/assets/{dist-4VwhnLtj.js.map → dist-DG75bvPp.js.map} +1 -1
- package/public/assets/{dist-CrUl2aQp.js → dist-G8YBD-wp.js} +2 -2
- package/public/assets/{dist-CrUl2aQp.js.map → dist-G8YBD-wp.js.map} +1 -1
- package/public/assets/{dist-BJaV2BD4.js → dist-f6MW74eX.js} +2 -2
- package/public/assets/{dist-BJaV2BD4.js.map → dist-f6MW74eX.js.map} +1 -1
- package/public/assets/{es2015-5DTJe35n.js → es2015-a6fysEVz.js} +2 -2
- package/public/assets/{es2015-5DTJe35n.js.map → es2015-a6fysEVz.js.map} +1 -1
- package/public/assets/{formatted-body-impl-L1EMnNW6.js → formatted-body-impl-gOLBKeyW.js} +2 -2
- package/public/assets/{formatted-body-impl-L1EMnNW6.js.map → formatted-body-impl-gOLBKeyW.js.map} +1 -1
- package/public/assets/formatted-body-jjhFXTap.js +3 -0
- package/public/assets/{formatted-body-BYQ-SPtj.js.map → formatted-body-jjhFXTap.js.map} +1 -1
- package/public/assets/index-CgH1BZwq.js +26 -0
- package/public/assets/index-CgH1BZwq.js.map +1 -0
- package/public/assets/index-DHzXsQot.css +2 -0
- package/public/assets/{input-CXMbJ848.js → input-Rc4oWv4q.js} +2 -2
- package/public/assets/{input-CXMbJ848.js.map → input-Rc4oWv4q.js.map} +1 -1
- package/public/assets/{insights-DwZxq1Dn.js → insights-CTW85An9.js} +2 -2
- package/public/assets/{insights-DwZxq1Dn.js.map → insights-CTW85An9.js.map} +1 -1
- package/public/assets/{integrations-9G5TJnnU.js → integrations-9c6IdRU4.js} +2 -2
- package/public/assets/{integrations-9G5TJnnU.js.map → integrations-9c6IdRU4.js.map} +1 -1
- package/public/assets/{lib-BcUW1l-W.js → lib-BZ6WPg2t.js} +2 -2
- package/public/assets/{lib-BcUW1l-W.js.map → lib-BZ6WPg2t.js.map} +1 -1
- package/public/assets/lucide-react-NXHPIMsX.js +10 -0
- package/public/assets/lucide-react-NXHPIMsX.js.map +1 -0
- package/public/assets/{maintenance-COcmUDnf.js → maintenance-D5PEygZz.js} +2 -2
- package/public/assets/{maintenance-COcmUDnf.js.map → maintenance-D5PEygZz.js.map} +1 -1
- package/public/assets/{managed-agents-D9-dgMeX.js → managed-agents-pkpAzZy8.js} +2 -2
- package/public/assets/{managed-agents-D9-dgMeX.js.map → managed-agents-pkpAzZy8.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-DH372bL-.js → markdown-preview-impl-DMzFE-Lo.js} +2 -2
- package/public/assets/{markdown-preview-impl-DH372bL-.js.map → markdown-preview-impl-DMzFE-Lo.js.map} +1 -1
- package/public/assets/{memory-B9hRK8sk.js → memory-BnquVYQz.js} +2 -2
- package/public/assets/{memory-B9hRK8sk.js.map → memory-BnquVYQz.js.map} +1 -1
- package/public/assets/{messages-BHkHlXzF.js → messages-DgxLlndC.js} +2 -2
- package/public/assets/{messages-BHkHlXzF.js.map → messages-DgxLlndC.js.map} +1 -1
- package/public/assets/{orchestrators-CXy3SDSn.js → orchestrators-BaWxeAlK.js} +2 -2
- package/public/assets/{orchestrators-CXy3SDSn.js.map → orchestrators-BaWxeAlK.js.map} +1 -1
- package/public/assets/{overview-D2yRyCQq.js → overview-Kvs-j2gg.js} +2 -2
- package/public/assets/{overview-D2yRyCQq.js.map → overview-Kvs-j2gg.js.map} +1 -1
- package/public/assets/{pairs-5tXdHylI.js → pairs-eDzbqcER.js} +2 -2
- package/public/assets/{pairs-5tXdHylI.js.map → pairs-eDzbqcER.js.map} +1 -1
- package/public/assets/{projects-CquHTMo1.js → projects-iDkxAGBW.js} +2 -2
- package/public/assets/{projects-CquHTMo1.js.map → projects-iDkxAGBW.js.map} +1 -1
- package/public/assets/{prompt-settings-Bn5mmi9w.js → prompt-settings-CjcqBG_r.js} +2 -2
- package/public/assets/{prompt-settings-Bn5mmi9w.js.map → prompt-settings-CjcqBG_r.js.map} +1 -1
- package/public/assets/{react-dom-BQICbUuF.js → react-dom-8yaD9iJG.js} +2 -2
- package/public/assets/{react-dom-BQICbUuF.js.map → react-dom-8yaD9iJG.js.map} +1 -1
- package/public/assets/{registry-C3sB7qv5.js → registry-C1DSnjjg.js} +2 -2
- package/public/assets/{registry-C3sB7qv5.js.map → registry-C1DSnjjg.js.map} +1 -1
- package/public/assets/{security-CmD-YE-_.js → security-VbEe_jwm.js} +2 -2
- package/public/assets/{security-CmD-YE-_.js.map → security-VbEe_jwm.js.map} +1 -1
- package/public/assets/{select-C0UlhK2k.js → select-CVfwgfKl.js} +2 -2
- package/public/assets/{select-C0UlhK2k.js.map → select-CVfwgfKl.js.map} +1 -1
- package/public/assets/{settings-DrqMG5nG.js → settings-ujA9hZW1.js} +4 -4
- package/public/assets/{settings-DrqMG5nG.js.map → settings-ujA9hZW1.js.map} +1 -1
- package/public/assets/store-D-Frk_EC.js +9 -0
- package/public/assets/store-D-Frk_EC.js.map +1 -0
- package/public/assets/{task-chip-BHM-e3I8.js → task-chip-CeujtXYT.js} +2 -2
- package/public/assets/{task-chip-BHM-e3I8.js.map → task-chip-CeujtXYT.js.map} +1 -1
- package/public/assets/{tasks-ChQjtXHu.js → tasks-BtbKaa-b.js} +2 -2
- package/public/assets/{tasks-ChQjtXHu.js.map → tasks-BtbKaa-b.js.map} +1 -1
- package/public/assets/{teams-itjNlDLp.js → teams-CrVDQ99v.js} +2 -2
- package/public/assets/{teams-itjNlDLp.js.map → teams-CrVDQ99v.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-B9M6s2vB.js → terminal-viewer-impl-Aays_e7-.js} +2 -2
- package/public/assets/{terminal-viewer-impl-B9M6s2vB.js.map → terminal-viewer-impl-Aays_e7-.js.map} +1 -1
- package/public/assets/{types-CRptbSYh.js → types-BqeSkWrv.js} +2 -2
- package/public/assets/types-BqeSkWrv.js.map +1 -0
- package/public/assets/{use-keyboard-viewport-CAa7IQTm.js → use-keyboard-viewport-cGEmrGE7.js} +2 -2
- package/public/assets/{use-keyboard-viewport-CAa7IQTm.js.map → use-keyboard-viewport-cGEmrGE7.js.map} +1 -1
- package/public/assets/{user-avatar-kYPfLgXp.js → user-avatar-qbo-B3dQ.js} +2 -2
- package/public/assets/{user-avatar-kYPfLgXp.js.map → user-avatar-qbo-B3dQ.js.map} +1 -1
- package/public/assets/{work-queue-Kw0uMkEF.js → work-queue-DDSGZO4Z.js} +2 -2
- package/public/assets/{work-queue-Kw0uMkEF.js.map → work-queue-DDSGZO4Z.js.map} +1 -1
- package/public/assets/{workspaces-CR6f7G0R.js → workspaces-CoDz-iZd.js} +2 -2
- package/public/assets/{workspaces-CR6f7G0R.js.map → workspaces-CoDz-iZd.js.map} +1 -1
- package/public/index.html +26 -26
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/hooks/relay-status.sh +11 -1
- package/runner/plugins/claude/hooks/stop.sh +6 -1
- package/runner/plugins/claude/hooks/user-prompt-submit.sh +6 -1
- package/runner/src/adapter.ts +5 -1
- package/runner/src/adapters/claude-transcript.ts +52 -58
- package/runner/src/adapters/codex-agent-message-capture.ts +33 -31
- package/runner/src/adapters/codex.ts +30 -12
- package/runner/src/control-server.ts +9 -4
- package/runner/src/response-capture-report.ts +4 -1
- package/runner/src/runner-core.ts +108 -46
- package/runner/src/runner-helpers.ts +31 -0
- package/src/agent-lifecycle-events.ts +8 -0
- package/src/bus.ts +0 -24
- package/src/db/agents.ts +15 -1
- package/src/db/messages.ts +150 -43
- package/src/maintenance/jobs.ts +8 -2
- package/src/mcp/tools-workspace.ts +4 -0
- package/src/routes/agent-sessions.ts +10 -7
- package/src/routes/agents.ts +0 -4
- package/src/routes/orchestrator.ts +5 -2
- package/src/routes/settings.ts +31 -2
- package/src/services/restart-instructions.ts +36 -0
- package/src/services/send-message.ts +7 -18
- package/src/services/shutdown-agent.ts +6 -0
- package/src/services/task-entity.ts +4 -0
- package/src/services/transient-agent-reaper.ts +40 -1
- package/src/services/work-finality.ts +25 -0
- package/src/settings/registry.ts +14 -3
- package/public/assets/agent-profiles-DXysA9To.js +0 -2
- package/public/assets/chat-Clz6qOtn.js +0 -2
- package/public/assets/chat-Clz6qOtn.js.map +0 -1
- package/public/assets/formatted-body-BYQ-SPtj.js +0 -3
- package/public/assets/index-0uIGXT14.js +0 -25
- package/public/assets/index-0uIGXT14.js.map +0 -1
- package/public/assets/index-DOJ7KidA.css +0 -2
- package/public/assets/lucide-react-CoU_Xts2.js +0 -10
- package/public/assets/lucide-react-CoU_Xts2.js.map +0 -1
- package/public/assets/store-zFLzd0Jd.js +0 -9
- package/public/assets/store-zFLzd0Jd.js.map +0 -1
- package/public/assets/types-CRptbSYh.js.map +0 -1
|
@@ -16,7 +16,7 @@ import { readRunnerContextProbeState } from "./context-probe-state";
|
|
|
16
16
|
import { startControlServer, type ControlServer, type ResolvedPermissionPrompt } from "./control-server";
|
|
17
17
|
import { ReplyObligationCache, obligationRequiresExplicitReply, responseCaptureReplyRoute } from "./reply-obligation-cache";
|
|
18
18
|
import { Outbox, type OutboxRecord } from "./outbox";
|
|
19
|
-
import { extractLastAssistantTurn, extractLastAssistantTurnAfterEntry,
|
|
19
|
+
import { extractLastAssistantTurn, extractLastAssistantTurnAfterEntry, extractAssistantTurnForPrompt, countTranscriptEntries, extractHookAssistantMessage, extractLatestTurnSteps, stepDedupKeys, transcriptLooksComplete } from "./adapters/claude-transcript";
|
|
20
20
|
import { IncrementalClaudeTranscriptTail } from "./adapters/claude-transcript-tail";
|
|
21
21
|
import { getManifest } from "agent-relay-providers";
|
|
22
22
|
import { profileUsesProviderHostGlobals } from "./profile-home";
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
runnerAgentStatus,
|
|
52
52
|
runnerBusErrorAction,
|
|
53
53
|
runnerMessageMatches,
|
|
54
|
-
runnerMessageShouldDirtyObligations,
|
|
54
|
+
runnerMessageShouldDirtyObligations, restartOptionsFromCommand,
|
|
55
55
|
runnerShouldResolveProviderExit,
|
|
56
56
|
runnerShouldRestartUnexpectedProviderExit,
|
|
57
57
|
runtimeProviderCapabilities,
|
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
shouldLogDeliveryFailure,
|
|
61
61
|
stripRunnerClaimedGuidance,
|
|
62
62
|
taskIdFromMessage,
|
|
63
|
-
type ProbeModelInfo,
|
|
63
|
+
type ProbeModelInfo, type RestartProviderOptions,
|
|
64
64
|
} from "./runner-helpers";
|
|
65
65
|
|
|
66
66
|
// Pre-destroy work is best-effort and must never hang teardown. Capping it keeps a slow
|
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
// short so a wedged/down server can't stall an operator-requested shutdown for long; a
|
|
70
70
|
// row that still can't land is logged, not silently dropped.
|
|
71
71
|
const OUTBOX_FLUSH_TIMEOUT_MS = 3_000;
|
|
72
|
+
const STALE_PROVIDER_TURN_BUSY_MS = 10 * 60_000;
|
|
72
73
|
|
|
73
74
|
interface RunnerOptions {
|
|
74
75
|
provider: string;
|
|
@@ -535,7 +536,7 @@ export class AgentRunner {
|
|
|
535
536
|
return mode === "isolated";
|
|
536
537
|
}
|
|
537
538
|
|
|
538
|
-
private async spawnProvider(opts:
|
|
539
|
+
private async spawnProvider(opts: RestartProviderOptions = {}): Promise<ManagedProcess> {
|
|
539
540
|
this.providerSessionId = crypto.randomUUID();
|
|
540
541
|
this.lastTranscriptPath = undefined;
|
|
541
542
|
const includeProviderGlobals = profileUsesProviderHostGlobals(this.options);
|
|
@@ -586,7 +587,7 @@ export class AgentRunner {
|
|
|
586
587
|
...(this.options.profile ? { profile: this.options.profile } : {}),
|
|
587
588
|
...(this.options.agentProfile ? { agentProfile: this.options.agentProfile } : {}),
|
|
588
589
|
...(this.options.prompt && !opts.resumeId && !opts.suppressPrompt ? { prompt: this.options.prompt } : {}),
|
|
589
|
-
...(this.options.systemPromptAppend ? { systemPromptAppend: this.options.systemPromptAppend } : {}),
|
|
590
|
+
...((opts.systemPromptAppend ?? this.options.systemPromptAppend) ? { systemPromptAppend: opts.systemPromptAppend ?? this.options.systemPromptAppend } : {}),
|
|
590
591
|
...(this.options.tmuxSession ? { tmuxSession: this.options.tmuxSession } : {}),
|
|
591
592
|
providerArgs: opts.resumeId ? [...this.options.providerArgs, "--resume", opts.resumeId] : this.options.providerArgs,
|
|
592
593
|
providerConfig: this.options.providerConfig,
|
|
@@ -604,7 +605,7 @@ export class AgentRunner {
|
|
|
604
605
|
};
|
|
605
606
|
const launchSeededPrompt = this.options.adapter.seedsInitialPromptAtLaunch && !opts.resumeId && !opts.suppressPrompt ? this.options.prompt?.trim() : ""; if (launchSeededPrompt) this.recordInjectedPrompt(launchSeededPrompt);
|
|
606
607
|
const managedProcess = await this.options.adapter.spawn(config);
|
|
607
|
-
const injectionEvents = runnerLaunchInjectionSessionEvents({ carried: this.options.relayInjectionEvents, provider: this.options.provider, config, providerConfig: this.options.providerConfig, agentId: this.agentId, providerSessionId: this.providerSessionId });
|
|
608
|
+
const injectionEvents = runnerLaunchInjectionSessionEvents({ carried: opts.relayInjectionEvents ?? this.options.relayInjectionEvents, provider: this.options.provider, config, providerConfig: this.options.providerConfig, agentId: this.agentId, providerSessionId: this.providerSessionId });
|
|
608
609
|
for (const event of injectionEvents) {
|
|
609
610
|
this.publishSessionEvent(event);
|
|
610
611
|
}
|
|
@@ -777,7 +778,7 @@ export class AgentRunner {
|
|
|
777
778
|
else this.lifecycleAction = undefined;
|
|
778
779
|
this.publishStatus();
|
|
779
780
|
let providerResult: Record<string, unknown> | void = undefined;
|
|
780
|
-
if (type === "agent.restart") await this.restartProvider();
|
|
781
|
+
if (type === "agent.restart") await this.restartProvider(restartOptionsFromCommand(params));
|
|
781
782
|
else if (type === "agent.reconnect") this.publishStatus();
|
|
782
783
|
else if (type === "agent.compact") {
|
|
783
784
|
if (!this.options.adapter.compact || !this.process) throw new Error("provider does not support compact");
|
|
@@ -927,7 +928,7 @@ export class AgentRunner {
|
|
|
927
928
|
return attachmentText ? `${body}\n\n${attachmentText}` : body;
|
|
928
929
|
}
|
|
929
930
|
|
|
930
|
-
private async restartProvider(opts:
|
|
931
|
+
private async restartProvider(opts: RestartProviderOptions = {}): Promise<void> {
|
|
931
932
|
this.restartInProgress = true;
|
|
932
933
|
try {
|
|
933
934
|
if (this.process) {
|
|
@@ -1204,7 +1205,7 @@ export class AgentRunner {
|
|
|
1204
1205
|
const providerSessionId = typeof update === "string" ? undefined : update.providerSessionId;
|
|
1205
1206
|
if (providerSessionId && providerSessionId !== this.providerSessionId) return;
|
|
1206
1207
|
const reason = typeof update === "string" ? "provider-turn" : update.reason ?? "provider-turn";
|
|
1207
|
-
|
|
1208
|
+
let id = typeof update === "string" ? reason : update.id ?? reason;
|
|
1208
1209
|
if (typeof update !== "string" && update.timeline) {
|
|
1209
1210
|
this.pendingTimelineEvent = {
|
|
1210
1211
|
status: update.timeline.status,
|
|
@@ -1291,13 +1292,19 @@ export class AgentRunner {
|
|
|
1291
1292
|
}
|
|
1292
1293
|
if (status === "busy" && reason === "provider-turn") {
|
|
1293
1294
|
if (!this.currentTurnId) {
|
|
1295
|
+
// For Claude, `update.id` carries the native prompt_id the UserPromptSubmit
|
|
1296
|
+
// hook reported (relay-status.sh → /status) — the same id the Stop hook and
|
|
1297
|
+
// transcript use for this turn. Falls back to a random mint only when no
|
|
1298
|
+
// native id was supplied (e.g. a provider/path that doesn't report one).
|
|
1294
1299
|
this.currentTurnId = typeof update !== "string" && update.id ? update.id : crypto.randomUUID();
|
|
1295
1300
|
this.currentTurnStartedAt = Date.now();
|
|
1296
1301
|
this.compactionMidTurn = false;
|
|
1297
1302
|
this.sessionLog(`turn started (turn ${this.currentTurnId})`);
|
|
1298
1303
|
}
|
|
1304
|
+
id = this.currentTurnId;
|
|
1299
1305
|
this.busyReconciler.arm();
|
|
1300
1306
|
} else if (status === "idle" && reason === "provider-turn") {
|
|
1307
|
+
id = typeof update !== "string" && update.id ? update.id : this.currentTurnId ?? reason;
|
|
1301
1308
|
if (this.currentTurnId) { this.completedProviderTurns += 1; this.sessionLog(`turn ended via provider idle (turn ${this.currentTurnId})`); }
|
|
1302
1309
|
this.currentTurnId = undefined;
|
|
1303
1310
|
this.currentTurnStartedAt = undefined;
|
|
@@ -1342,7 +1349,8 @@ export class AgentRunner {
|
|
|
1342
1349
|
}
|
|
1343
1350
|
} else if (status === "idle") {
|
|
1344
1351
|
this.claims.clearTerminalStatus();
|
|
1345
|
-
this.claims.
|
|
1352
|
+
if (reason === "provider-turn") this.claims.clearWorkKind("provider-turn");
|
|
1353
|
+
else this.claims.finishWork(reason, id);
|
|
1346
1354
|
if (reason === "provider-turn") void this.completeObservedTaskClaims();
|
|
1347
1355
|
}
|
|
1348
1356
|
else if (status === "offline" || status === "error") this.claims.setTerminalStatus(status);
|
|
@@ -1423,7 +1431,7 @@ export class AgentRunner {
|
|
|
1423
1431
|
// isPreFlush: true (#435/#499) — mid-turn PreToolUse boundary. Drain the live
|
|
1424
1432
|
// tail and force-flush session messages before showing the blocking control;
|
|
1425
1433
|
// the entry cursor lets Stop capture skip already-emitted text.
|
|
1426
|
-
private async publishSessionTurn(input: { transcriptPath: string; lastAssistantMessage?: unknown; isPreFlush?: boolean }): Promise<void> {
|
|
1434
|
+
private async publishSessionTurn(input: { transcriptPath: string; lastAssistantMessage?: unknown; promptId?: string; isPreFlush?: boolean }): Promise<void> {
|
|
1427
1435
|
if (input.transcriptPath) this.lastTranscriptPath = input.transcriptPath;
|
|
1428
1436
|
|
|
1429
1437
|
if (input.isPreFlush) {
|
|
@@ -1443,7 +1451,13 @@ export class AgentRunner {
|
|
|
1443
1451
|
return;
|
|
1444
1452
|
}
|
|
1445
1453
|
|
|
1446
|
-
|
|
1454
|
+
// Claude's Stop hook reports its own native `prompt_id` — the same UUID the
|
|
1455
|
+
// transcript stamps on every entry of this turn (extractAssistantTurnForPrompt /
|
|
1456
|
+
// resolveEntryPromptId in claude-transcript.ts). It's the authoritative turn
|
|
1457
|
+
// identifier; currentTurnId should already equal it (seeded from the same
|
|
1458
|
+
// prompt_id at turn start — see setProviderStatus / handleUserPrompt), but this
|
|
1459
|
+
// prefers the freshest source so finalization is correct even if the two drift.
|
|
1460
|
+
const turnId = input.promptId ?? this.currentTurnId;
|
|
1447
1461
|
this.stopReasoningTail();
|
|
1448
1462
|
// Optional correlation for threading + obligation clearing — never a capture gate.
|
|
1449
1463
|
let replyToMessageId: number | undefined, replyTarget = "user";
|
|
@@ -1464,32 +1478,41 @@ export class AgentRunner {
|
|
|
1464
1478
|
const flushCount = this.narrativeFlushEntryCount;
|
|
1465
1479
|
this.narrativeFlushEntryCount = 0;
|
|
1466
1480
|
|
|
1467
|
-
//
|
|
1468
|
-
//
|
|
1469
|
-
//
|
|
1470
|
-
//
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
//
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
if (
|
|
1481
|
+
// PRIMARY capture source: the Stop hook's own last_assistant_message. Claude
|
|
1482
|
+
// reports it synchronously in the hook payload itself, so there is no
|
|
1483
|
+
// transcript file-read race to retry against — this text is authoritative and
|
|
1484
|
+
// complete the moment the hook fires. The transcript is read at most ONCE
|
|
1485
|
+
// (never retried/looped) and only to enrich "full" mode with the turn's
|
|
1486
|
+
// intermediate narration; if that single read comes up empty (the file simply
|
|
1487
|
+
// hasn't caught up yet) the hook payload text still carries the turn.
|
|
1488
|
+
let body = extractHookAssistantMessage(input.lastAssistantMessage);
|
|
1489
|
+
if (this.options.providerConfig.chatCaptureMode === "full" && input.transcriptPath) {
|
|
1490
|
+
let jsonl: string | undefined;
|
|
1491
|
+
try { jsonl = await readFile(input.transcriptPath, "utf8"); } catch { jsonl = undefined; }
|
|
1492
|
+
if (jsonl !== undefined && transcriptLooksComplete(jsonl)) {
|
|
1493
|
+
// Deterministic slicing via prompt_id→parentUuid chaining when the native id is
|
|
1494
|
+
// known; only fall back to the legacy "last real user prompt" heuristic when that
|
|
1495
|
+
// deterministic slice comes up EMPTY, not merely when turnId is falsy — an old
|
|
1496
|
+
// Claude (<2.1.196) mints a truthy random turnId that never resolves via promptId
|
|
1497
|
+
// chaining, so branching on truthiness alone left this fallback unreachable and
|
|
1498
|
+
// silently dropped intermediate narration in full mode (#1086 review, Finding F1).
|
|
1499
|
+
let full = turnId ? extractAssistantTurnForPrompt(jsonl, turnId, flushCount) : "";
|
|
1500
|
+
if (!full) full = flushCount > 0 ? extractLastAssistantTurnAfterEntry(jsonl, flushCount) : extractLastAssistantTurn(jsonl);
|
|
1501
|
+
if (full) body = full;
|
|
1487
1502
|
}
|
|
1488
1503
|
}
|
|
1489
|
-
//
|
|
1490
|
-
//
|
|
1491
|
-
|
|
1492
|
-
|
|
1504
|
+
// Last-resort safety net for the default "final" mode (#1086 review, Finding F2):
|
|
1505
|
+
// last_assistant_message is an undocumented Claude hook field. If a future Claude
|
|
1506
|
+
// version renames or drops it, do ONE (never retried — same one-shot discipline as the
|
|
1507
|
+
// "full" mode enrichment above) transcript read rather than silently losing every
|
|
1508
|
+
// capture in the mode nearly every provider config runs in.
|
|
1509
|
+
if (!body && this.options.providerConfig.chatCaptureMode === "final" && input.transcriptPath) {
|
|
1510
|
+
let jsonl: string | undefined;
|
|
1511
|
+
try { jsonl = await readFile(input.transcriptPath, "utf8"); } catch { jsonl = undefined; }
|
|
1512
|
+
if (jsonl !== undefined) {
|
|
1513
|
+
const fallback = turnId ? extractAssistantTurnForPrompt(jsonl, turnId, flushCount) : "";
|
|
1514
|
+
body = fallback || (flushCount > 0 ? extractLastAssistantTurnAfterEntry(jsonl, flushCount) : extractLastAssistantTurn(jsonl));
|
|
1515
|
+
}
|
|
1493
1516
|
}
|
|
1494
1517
|
// A pure tool-use turn with no closing text is fine to skip — its reasoning and
|
|
1495
1518
|
// tool steps already carried the visibility into chat.
|
|
@@ -1499,7 +1522,18 @@ export class AgentRunner {
|
|
|
1499
1522
|
}
|
|
1500
1523
|
|
|
1501
1524
|
this.sessionLog(`response captured for turn ${turnId ?? "?"} (${body.length} chars${replyToMessageId ? `, replyTo #${replyToMessageId}` : ", no replyTo"})`);
|
|
1502
|
-
await publishCapturedResponse({
|
|
1525
|
+
await publishCapturedResponse({
|
|
1526
|
+
publishSessionEvent: (event) => this.publishSessionEvent(event),
|
|
1527
|
+
outbox: this.sessionOutbox,
|
|
1528
|
+
provider: this.options.provider,
|
|
1529
|
+
from: this.agentId,
|
|
1530
|
+
to: replyTarget,
|
|
1531
|
+
body,
|
|
1532
|
+
replyTo: replyToMessageId,
|
|
1533
|
+
// A Stop-hook-driven capture is by construction a definitive, provider-native
|
|
1534
|
+
// turn completion — Claude's own lifecycle hook fired, not a reconstructed guess.
|
|
1535
|
+
session: { type: "response", origin: "provider", final: true, confidence: "definitive", endedBy: "completed", ...(turnId ? { turnId } : {}) },
|
|
1536
|
+
});
|
|
1503
1537
|
// The agent's reply may have cleared an obligation — refresh the snapshot so the next
|
|
1504
1538
|
// turn-end doesn't re-prompt for a message already answered (#196).
|
|
1505
1539
|
if (replyToMessageId) this.obligationCache.markDirty();
|
|
@@ -1609,10 +1643,15 @@ export class AgentRunner {
|
|
|
1609
1643
|
// it into the dashboard chat so both surfaces stay in sync, and kick off reasoning
|
|
1610
1644
|
// tailing for the turn. Skips prompts the runner itself injected (chat box, relay
|
|
1611
1645
|
// deliveries) so those aren't double-posted.
|
|
1612
|
-
private async handleUserPrompt(input: { prompt: string; transcriptPath?: string }): Promise<void> {
|
|
1646
|
+
private async handleUserPrompt(input: { prompt: string; transcriptPath?: string; promptId?: string }): Promise<void> {
|
|
1613
1647
|
if (input.transcriptPath) this.lastTranscriptPath = input.transcriptPath;
|
|
1614
1648
|
if (!this.currentTurnId) {
|
|
1615
|
-
|
|
1649
|
+
// Claude's own prompt_id (from the UserPromptSubmit hook payload) is the
|
|
1650
|
+
// native, stable turn identifier — the same id the Stop hook reports and the
|
|
1651
|
+
// transcript stamps on every entry of this turn. Falls back to a random mint
|
|
1652
|
+
// only for the rare case prompt_id wasn't available (see the ZS base schema
|
|
1653
|
+
// note: absent until the very first user input of the process lifetime).
|
|
1654
|
+
this.currentTurnId = input.promptId ?? crypto.randomUUID();
|
|
1616
1655
|
this.currentTurnStartedAt = Date.now();
|
|
1617
1656
|
}
|
|
1618
1657
|
const text = input.prompt.trim();
|
|
@@ -1739,6 +1778,14 @@ export class AgentRunner {
|
|
|
1739
1778
|
// Dashboard prompt injection is already answered by this captured App Server
|
|
1740
1779
|
// response. User obligations still belong to the session mirror; spawner
|
|
1741
1780
|
// obligations (#413) route to the spawn-parent with replyTo to clear the debt.
|
|
1781
|
+
// #1083: a turn-final capture is ALWAYS mirrored, regardless of reply route — an
|
|
1782
|
+
// autonomous Codex turn with no pending prompt and no eligible obligation used to
|
|
1783
|
+
// hit `if (!route) return` here and vanish before ever reaching publishCapturedResponse,
|
|
1784
|
+
// so it was neither shown in the dashboard nor eligible for report-up. The route only
|
|
1785
|
+
// decides whether an explicit reply/obligation-clear also happens; when there's none,
|
|
1786
|
+
// replyToMessageId stays undefined and the capture still lands as a plain session-mirror
|
|
1787
|
+
// response (enqueueCapturedResponseReport's own `!replyTo` gate then correctly skips
|
|
1788
|
+
// report-up — there's no spawner obligation to report up against).
|
|
1742
1789
|
let replyToMessageId: number | undefined, replyTarget = "user";
|
|
1743
1790
|
const pendingPrompt = this.pendingPromptMessageId;
|
|
1744
1791
|
if (pendingPrompt) {
|
|
@@ -1746,10 +1793,9 @@ export class AgentRunner {
|
|
|
1746
1793
|
this.pendingPromptMessageId = undefined;
|
|
1747
1794
|
} else {
|
|
1748
1795
|
const route = responseCaptureReplyRoute(this.obligationCache.get(), this.currentTurnStartedAt, { suppressUser: true });
|
|
1749
|
-
if (
|
|
1750
|
-
replyToMessageId = route.replyToMessageId; replyTarget = route.to;
|
|
1796
|
+
if (route) { replyToMessageId = route.replyToMessageId; replyTarget = route.to; }
|
|
1751
1797
|
}
|
|
1752
|
-
await publishCapturedResponse({ publishSessionEvent: (sessionEvent) => this.publishSessionEvent(sessionEvent), outbox: this.sessionOutbox, provider: this.options.provider, from: this.agentId, to: replyTarget, body, replyTo: replyToMessageId, session: { type: "response", origin: event.origin ?? "provider", ...(turnId ? { turnId } : {}) } });
|
|
1798
|
+
await publishCapturedResponse({ publishSessionEvent: (sessionEvent) => this.publishSessionEvent(sessionEvent), outbox: this.sessionOutbox, provider: this.options.provider, from: this.agentId, to: replyTarget, body, replyTo: replyToMessageId, session: { type: "response", origin: event.origin ?? "provider", ...(turnId ? { turnId } : {}), ...(event.confidence ? { confidence: event.confidence } : {}), ...(event.endedBy ? { endedBy: event.endedBy } : {}) } });
|
|
1753
1799
|
if (replyToMessageId) this.obligationCache.markDirty();
|
|
1754
1800
|
return;
|
|
1755
1801
|
}
|
|
@@ -1965,15 +2011,25 @@ export class AgentRunner {
|
|
|
1965
2011
|
});
|
|
1966
2012
|
}
|
|
1967
2013
|
|
|
2014
|
+
private isStaleProviderTurnOnlyBusy(
|
|
2015
|
+
status: SemanticStatus,
|
|
2016
|
+
activeWork: Array<{ kind: string }>,
|
|
2017
|
+
busyReasons: string[],
|
|
2018
|
+
liveness: { hasLiveWork: boolean; lastProgressAt: number },
|
|
2019
|
+
): boolean {
|
|
2020
|
+
if (status !== "busy") return false;
|
|
2021
|
+
if (activeWork.length === 0 || activeWork.some((work) => work.kind !== "provider-turn")) return false;
|
|
2022
|
+
if (busyReasons.some((reason) => reason !== "provider-turn")) return false;
|
|
2023
|
+
if (liveness.hasLiveWork || !liveness.lastProgressAt) return false;
|
|
2024
|
+
return Date.now() - liveness.lastProgressAt >= STALE_PROVIDER_TURN_BUSY_MS;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
1968
2027
|
private publishStatus(): void {
|
|
1969
2028
|
this.claims.expire();
|
|
1970
2029
|
const status = this.claims.currentStatus();
|
|
1971
|
-
const agentStatus = runnerAgentStatus(status);
|
|
1972
2030
|
const activeWork = this.claims.activeWork();
|
|
1973
2031
|
const activeSubagents = activeWork.filter((item) => item.kind === "subagent");
|
|
1974
2032
|
const terminalFailure = this.terminalFailure;
|
|
1975
|
-
const providerState = terminalFailure?.providerState ?? this.rateLimitHold ?? providerStateFromActiveWork(activeWork);
|
|
1976
|
-
this.bus.setSemanticStatus(status === "offline" || status === "error" ? "idle" : status);
|
|
1977
2033
|
const timelineEvent = this.pendingTimelineEvent;
|
|
1978
2034
|
this.pendingTimelineEvent = undefined;
|
|
1979
2035
|
// §4.1 Runner↔provider liveness contract (#725 / #746): fold the raw transport/timeline/
|
|
@@ -1989,6 +2045,12 @@ export class AgentRunner {
|
|
|
1989
2045
|
const liveness = this.options.adapter.liveness
|
|
1990
2046
|
? this.options.adapter.liveness(livenessInputs)
|
|
1991
2047
|
: computeLivenessSignal(livenessInputs);
|
|
2048
|
+
const busyReasons = this.claims.reasons();
|
|
2049
|
+
const staleProviderTurnBusy = this.isStaleProviderTurnOnlyBusy(status, activeWork, busyReasons, liveness);
|
|
2050
|
+
const effectiveStatus = staleProviderTurnBusy ? "idle" : status;
|
|
2051
|
+
const agentStatus = runnerAgentStatus(effectiveStatus);
|
|
2052
|
+
const providerState = terminalFailure?.providerState ?? this.rateLimitHold ?? (staleProviderTurnBusy ? null : providerStateFromActiveWork(activeWork));
|
|
2053
|
+
this.bus.setSemanticStatus(effectiveStatus === "offline" || effectiveStatus === "error" ? "idle" : effectiveStatus);
|
|
1992
2054
|
this.bus.status({
|
|
1993
2055
|
agentStatus,
|
|
1994
2056
|
ready: agentStatus !== "offline" && !this.stopped,
|
|
@@ -2020,7 +2082,7 @@ export class AgentRunner {
|
|
|
2020
2082
|
? { lastError: `Claude provider exited; resume id ${this.terminalProviderExit.claudeResumeId} captured for manual recovery` }
|
|
2021
2083
|
: { lastError: "Claude provider exited; manual intervention required" }),
|
|
2022
2084
|
} : {}),
|
|
2023
|
-
busyReasons:
|
|
2085
|
+
busyReasons: staleProviderTurnBusy ? busyReasons.filter((reason) => reason !== "provider-turn") : busyReasons,
|
|
2024
2086
|
completedProviderTurns: this.completedProviderTurns,
|
|
2025
2087
|
activeWork,
|
|
2026
2088
|
activeSubagents,
|
|
@@ -5,6 +5,7 @@ import { getManifest } from "agent-relay-providers";
|
|
|
5
5
|
import type { BooleanCapabilityDefault, ProviderCapabilityCondition } from "agent-relay-providers";
|
|
6
6
|
import { targetMatchesAgentIdentity } from "agent-relay-sdk/agent-target";
|
|
7
7
|
import type { ProviderAdapter, ProviderConfig, RunnerSpawnConfig, SemanticStatus } from "./adapter";
|
|
8
|
+
import type { RunnerRelayInjectionEvent } from "./relay-injection-events";
|
|
8
9
|
import { agentProfileProjectionReport } from "./profile-projection";
|
|
9
10
|
import { assembleLaunch } from "./launch-assembly";
|
|
10
11
|
|
|
@@ -27,6 +28,13 @@ export interface ProbeModelInfo {
|
|
|
27
28
|
effort?: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
export interface RestartProviderOptions {
|
|
32
|
+
resumeId?: string;
|
|
33
|
+
suppressPrompt?: boolean;
|
|
34
|
+
systemPromptAppend?: string;
|
|
35
|
+
relayInjectionEvents?: RunnerRelayInjectionEvent[];
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
export function runnerMessageMatches(message: Pick<Message, "to" | "resolvedToAgent">, target: {
|
|
31
39
|
agentId: string;
|
|
32
40
|
label?: string;
|
|
@@ -62,6 +70,29 @@ export function stripRunnerClaimedGuidance(body: string): string {
|
|
|
62
70
|
return body.replace(RUNNER_CLAIMED_GUIDANCE_RE, "");
|
|
63
71
|
}
|
|
64
72
|
|
|
73
|
+
const RELAY_INJECTION_CATEGORIES = new Set(["memory", "instructions", "tools", "comms", "knowledge", "context"]);
|
|
74
|
+
|
|
75
|
+
function relayInjectionEventsFromParam(value: unknown): RunnerRelayInjectionEvent[] | undefined {
|
|
76
|
+
if (!Array.isArray(value)) return undefined;
|
|
77
|
+
const events = value.flatMap((item): RunnerRelayInjectionEvent[] => {
|
|
78
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return [];
|
|
79
|
+
const record = item as Record<string, unknown>;
|
|
80
|
+
const detail = record.detail;
|
|
81
|
+
if (typeof record.category !== "string" || !RELAY_INJECTION_CATEGORIES.has(record.category)) return [];
|
|
82
|
+
if (typeof record.summary !== "string" || !record.summary.trim()) return [];
|
|
83
|
+
if (!detail || typeof detail !== "object" || Array.isArray(detail)) return [];
|
|
84
|
+
return [{ category: record.category as RunnerRelayInjectionEvent["category"], summary: record.summary, detail: detail as Record<string, unknown> }];
|
|
85
|
+
});
|
|
86
|
+
return events.length ? events : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function restartOptionsFromCommand(params: Record<string, unknown>): RestartProviderOptions {
|
|
90
|
+
return {
|
|
91
|
+
systemPromptAppend: typeof params.systemPromptAppend === "string" ? params.systemPromptAppend : undefined,
|
|
92
|
+
relayInjectionEvents: relayInjectionEventsFromParam(params.relayInjectionEvents),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
65
96
|
// Bound a runner's INITIAL relay registration. `connect` resolves on the first `registered`
|
|
66
97
|
// frame and otherwise loops forever, so a token the relay rejects (401 at the WS upgrade)
|
|
67
98
|
// hangs here silently. Race it against `timeoutMs`; on timeout, surface a fatal reason, tear
|
|
@@ -4,6 +4,7 @@ import { LIFECYCLE_STATE_WORKING, childSignalKey, stampLifecycleSignal, taskSign
|
|
|
4
4
|
import { deliverableTaskIdsBySpawnRequestId, getAgent, markAgentReadyNotified, recordCrashReport, recordTerminalEvent } from "./db";
|
|
5
5
|
import { crashReportFor } from "./agent-death-diagnosis";
|
|
6
6
|
import { renderTemplate } from "./prompt-resolver";
|
|
7
|
+
import { markAgentWorkFinal } from "./services/work-finality";
|
|
7
8
|
import type { AgentCard, AgentDeathDiagnosis } from "./types";
|
|
8
9
|
|
|
9
10
|
/** One-line cause tag appended to a death notification body, e.g. " · why: Clean exit (exit code 0)". */
|
|
@@ -187,6 +188,13 @@ export function notifyAgentOffline(childId: string, reason: string, diagnosis?:
|
|
|
187
188
|
const child = getAgent(childId);
|
|
188
189
|
const parent = child?.spawnedBy;
|
|
189
190
|
if (!child || !parent) return;
|
|
191
|
+
// #1084 crash-final-loss fix-forward — this is the platform confirming the child is gone for
|
|
192
|
+
// good (past the reconnect/stale grace, or an explicit exit-diagnostics report). A genuine final
|
|
193
|
+
// report captured+held in the ring before the crash must go out now or it is lost forever: no
|
|
194
|
+
// later signal (relay_workspace_ready, task→done) is coming from a process that no longer exists.
|
|
195
|
+
// Idempotent (flushWorkFinalReports no-ops once the ring is empty), so a redundant call from a
|
|
196
|
+
// second offline classification of the same agent is harmless.
|
|
197
|
+
markAgentWorkFinal(childId, "edge-fallback");
|
|
190
198
|
// Classify from the live set OR the durable readiness stamp. The `delete` always runs (cleanup of
|
|
191
199
|
// the in-memory set); the persisted stamp covers a child that became ready before a relay restart.
|
|
192
200
|
const wasReady = readyFired.delete(childId) || everReady(child);
|
package/src/bus.ts
CHANGED
|
@@ -15,7 +15,6 @@ import { agentProviderTimelineEvent, auditRunnerTimelineEvent } from "./provider
|
|
|
15
15
|
import { ServiceAuthError } from "./services/errors";
|
|
16
16
|
import { ShutdownAuthError, ShutdownTargetError, shutdownAgent, shutdownInputFromBusFrame } from "./services/shutdown-agent";
|
|
17
17
|
import { flushQueuedDirectMessages } from "./services/managed-running";
|
|
18
|
-
import { flushIdleLineageReport } from "./services/send-message";
|
|
19
18
|
import { isAgentUnavailable } from "./db/agent-predicates";
|
|
20
19
|
import { applyCommandToRecipe } from "./recipe-runner";
|
|
21
20
|
import { enqueueContinuationInjectionAfterSelfResume } from "./self-resume";
|
|
@@ -106,10 +105,6 @@ export function busHandleClose(ws: BusWebSocket): void {
|
|
|
106
105
|
if (!conn.transportReconnect) {
|
|
107
106
|
setStatus(conn.agentId, "stale", { instanceId: conn.instanceId, epoch: conn.epoch });
|
|
108
107
|
emitAgentStatusEvent(conn.agentId);
|
|
109
|
-
// #1040 — a WS close is a busy→exit edge (the runner crashed/dropped before its stop-hook
|
|
110
|
-
// status frame). Flush any held turn-final report-up so it still reaches the spawner instead
|
|
111
|
-
// of dying with the socket. No-op when nothing was deferred.
|
|
112
|
-
flushIdleLineageReport(conn.agentId);
|
|
113
108
|
}
|
|
114
109
|
}
|
|
115
110
|
}
|
|
@@ -134,9 +129,6 @@ export function expireStaleBusAgents(graceMs = busStaleGraceMs(), lagGraceMs = 0
|
|
|
134
129
|
// (a fresh instanceId → epoch bump), never on a heartbeat lapse.
|
|
135
130
|
setStatus(row.id, "offline");
|
|
136
131
|
emitAgentStatusEvent(row.id);
|
|
137
|
-
// #1040 — the stale sweep is the terminal busy→offline edge for a runner that dropped without a
|
|
138
|
-
// clean idle transition. Flush any held turn-final report-up before the agent ages out.
|
|
139
|
-
flushIdleLineageReport(row.id);
|
|
140
132
|
const tasks = orphanTasksForAgent(row.id);
|
|
141
133
|
for (const task of tasks) {
|
|
142
134
|
orphanedTasks.push(task);
|
|
@@ -172,16 +164,6 @@ function handleFrame(ws: BusWebSocket, frame: ReturnType<typeof validateClientFr
|
|
|
172
164
|
heartbeat(conn.agentId, { instanceId: conn.instanceId, epoch: conn.epoch }, runtime);
|
|
173
165
|
if (frame.payload.status) {
|
|
174
166
|
setStatus(conn.agentId, frame.payload.status, { instanceId: conn.instanceId, epoch: conn.epoch });
|
|
175
|
-
// #1040 — a heartbeat frame can carry the busy→idle transition instead of a `status`
|
|
176
|
-
// frame. Flush the held report-up on THIS edge too, else `before.status` is already
|
|
177
|
-
// non-busy at the next status frame → the flush never fires → the child deadlocks idle
|
|
178
|
-
// with its verdict held and the coordinator waits forever.
|
|
179
|
-
// #1037 (re-regression) — gate on a genuine at-rest `idle`, NOT merely "not busy". A
|
|
180
|
-
// real turn-end always reports `idle`; a transient non-idle status (a stray `online`,
|
|
181
|
-
// a malformed frame) is NOT the end of the child's work and must not flush its held
|
|
182
|
-
// mid-work capture upstream. Terminal exit edges (WS-close/stale sweep) keep their
|
|
183
|
-
// unconditional flush for the #1040 delivery guarantee — this only tightens the LIVE edge.
|
|
184
|
-
if (current?.status === "busy" && frame.payload.status === "idle") flushIdleLineageReport(conn.agentId);
|
|
185
167
|
}
|
|
186
168
|
emitAgentStatusEvent(conn.agentId);
|
|
187
169
|
// #416 — flush queued messages if the agent transitioned from unavailable to available.
|
|
@@ -226,12 +208,6 @@ function handleFrame(ws: BusWebSocket, frame: ReturnType<typeof validateClientFr
|
|
|
226
208
|
emitAgentStatusEvent(conn.agentId);
|
|
227
209
|
// #237 stop-hook → instant ⚪↔🟡 changes detection on the turn-end edge.
|
|
228
210
|
probeWorkspaceOnTurnEnd(before, after);
|
|
229
|
-
// #1037 — the same busy→idle turn-end edge flushes a quiet child's deferred turn-final
|
|
230
|
-
// report-up to its spawner (held back while the child was mid-work). Gate on a genuine
|
|
231
|
-
// at-rest `idle`: a real turn-end reports `idle`, whereas a transient non-idle status
|
|
232
|
-
// (stray `online`, reconnect) is not the child going to rest and must not flush its held
|
|
233
|
-
// mid-work capture. Terminal exit edges keep the #1040 unconditional flush.
|
|
234
|
-
if (before?.status === "busy" && after && after.status === "idle") flushIdleLineageReport(conn.agentId);
|
|
235
211
|
// #633 — silent clean-exit: a runner that stays alive after its provider terminally exited
|
|
236
212
|
// marks the offline edge with `terminalProviderExit`; the owning service turns that into a
|
|
237
213
|
// terminal event + #636 diagnosis (and clears the hold on a genuine resume).
|
package/src/db/agents.ts
CHANGED
|
@@ -566,7 +566,20 @@ export function reapStaleAgents(ttlMs: number = STALE_TTL_MS, lagGraceMs = 0): s
|
|
|
566
566
|
|
|
567
567
|
export { settleSingleTargetOnDemandTasks } from "./task-maintenance.ts";
|
|
568
568
|
|
|
569
|
-
export function pruneOfflineAgents(
|
|
569
|
+
export function pruneOfflineAgents(
|
|
570
|
+
maxOfflineMs: number = DAY_MS,
|
|
571
|
+
opts?: {
|
|
572
|
+
/**
|
|
573
|
+
* #1084 crash-final-loss fix-forward — defense-in-depth for a held work-final ring that
|
|
574
|
+
* somehow survived to prune time (the primary flush is `notifyAgentOffline`, fired when the
|
|
575
|
+
* agent first goes offline). Called per pruned agent id BEFORE its row is deleted, so a
|
|
576
|
+
* caller can flush any still-held final report while `getAgent(id)` can still resolve it —
|
|
577
|
+
* kept a plain callback (not a direct import) so this db-layer module stays free of
|
|
578
|
+
* service/SSE-layer dependencies.
|
|
579
|
+
*/
|
|
580
|
+
beforeDelete?: (agentId: string) => void;
|
|
581
|
+
},
|
|
582
|
+
): string[] {
|
|
570
583
|
const cutoff = Date.now() - maxOfflineMs;
|
|
571
584
|
return getDb().transaction(() => {
|
|
572
585
|
const rows = getDb()
|
|
@@ -597,6 +610,7 @@ export function pruneOfflineAgents(maxOfflineMs: number = DAY_MS): string[] {
|
|
|
597
610
|
|
|
598
611
|
for (const row of rows) {
|
|
599
612
|
// Offline past the prune horizon and being deleted — a hard kill, not recoverable.
|
|
613
|
+
opts?.beforeDelete?.(row.id);
|
|
600
614
|
revokeRuntimeTokensForAgent(row.id, now, "pruned");
|
|
601
615
|
closeOpenPairsForAgent(row.id, now);
|
|
602
616
|
removeSubscriptionsForAgent(row.id);
|