@sema-agent/core 1.451.0 → 2.0.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/dist/agents/agent-transcript-tool.d.ts +15 -0
- package/dist/agents/agent-transcript-tool.js +122 -0
- package/dist/agents/cascade.js +5 -8
- package/dist/agents/observer.js +3 -0
- package/dist/agents/repair-loop.d.ts +1 -1
- package/dist/agents/repair-loop.js +1 -1
- package/dist/agents/retain-ledger.d.ts +60 -0
- package/dist/agents/retain-ledger.js +225 -0
- package/dist/agents/roster-store.d.ts +1 -0
- package/dist/agents/roster-store.js +9 -0
- package/dist/agents/send-message-tool.d.ts +41 -0
- package/dist/agents/send-message-tool.js +469 -0
- package/dist/agents/subagent.d.ts +33 -108
- package/dist/agents/subagent.js +40 -813
- package/dist/agents/teacher.d.ts +0 -1
- package/dist/agents/teacher.js +0 -9
- package/dist/bin/sema-tb.js +3 -3
- package/dist/brain/anthropic.js +15 -40
- package/dist/brain/openai.js +15 -40
- package/dist/brain/reasoning.d.ts +1 -1
- package/dist/brain/stream-engine.js +2 -2
- package/dist/brain/stream-shared.d.ts +17 -0
- package/dist/brain/stream-shared.js +50 -0
- package/dist/brain/timeout.d.ts +1 -1
- package/dist/brain/timeout.js +1 -1
- package/dist/config/catalog.js +1 -3
- package/dist/config/defaults.d.ts +6 -0
- package/dist/config/defaults.js +6 -0
- package/dist/core/ask-question.d.ts +1 -1
- package/dist/core/ask-question.js +5 -5
- package/dist/core/background-agent-store.d.ts +2 -0
- package/dist/core/background-agent-store.js +5 -1
- package/dist/core/background-shell.d.ts +1 -1
- package/dist/core/checkpoint-store.d.ts +0 -1
- package/dist/core/exec-gate.d.ts +1 -1
- package/dist/core/exec-output-tail.d.ts +19 -1
- package/dist/core/exec-output-tail.js +44 -5
- package/dist/core/file-snapshot-store.d.ts +1 -1
- package/dist/core/fs-write-gate-policy.d.ts +1 -1
- package/dist/core/git-worktree-env.d.ts +1 -1
- package/dist/core/hooks.d.ts +1 -1
- package/dist/core/image-downsample.d.ts +44 -0
- package/dist/core/image-downsample.js +97 -0
- package/dist/core/lsp-session.d.ts +1 -0
- package/dist/core/lsp-session.js +24 -6
- package/dist/core/lsp.d.ts +11 -1
- package/dist/core/lsp.js +63 -6
- package/dist/core/mailbox-store.d.ts +3 -2
- package/dist/core/mailbox-store.js +19 -4
- package/dist/core/mcp.d.ts +3 -45
- package/dist/core/mcp.js +2 -97
- package/dist/core/media-byte-cap.d.ts +1 -1
- package/dist/core/memory-recall.d.ts +0 -12
- package/dist/core/memory-recall.js +0 -222
- package/dist/core/memory.d.ts +0 -6
- package/dist/core/memory.js +0 -88
- package/dist/core/message-utils.d.ts +1 -1
- package/dist/core/oracle-isolation.d.ts +1 -1
- package/dist/core/remote-env.d.ts +1 -1
- package/dist/core/roles.d.ts +1 -1
- package/dist/core/runner/active-skill-scope.d.ts +1 -1
- package/dist/core/runner/assemble-result.d.ts +0 -2
- package/dist/core/runner/memory-consolidation.d.ts +1 -1
- package/dist/core/runner/prepare-memory.d.ts +16 -0
- package/dist/core/runner/prepare-memory.js +191 -0
- package/dist/core/runner/prepare-task.d.ts +4 -28
- package/dist/core/runner/prepare-task.js +29 -208
- package/dist/core/runner/prompt-suggestions.d.ts +1 -1
- package/dist/core/runner/runtask.d.ts +2 -3
- package/dist/core/runner/runtask.js +337 -529
- package/dist/core/runner/session-rule-policy.d.ts +1 -1
- package/dist/core/runner/synthetic-tools.d.ts +5 -5
- package/dist/core/runner/synthetic-tools.js +4 -4
- package/dist/core/runner/tool-disclosure.d.ts +3 -3
- package/dist/core/runner/tool-disclosure.js +2 -2
- package/dist/core/runner/tool-output-projection.d.ts +6 -0
- package/dist/core/runner/tool-output-projection.js +81 -0
- package/dist/core/runtime.d.ts +1 -1
- package/dist/core/scheduler.d.ts +1 -1
- package/dist/core/sensitive-path-policy.d.ts +1 -1
- package/dist/core/session-reconcile.d.ts +1 -1
- package/dist/core/session-reconcile.js +5 -2
- package/dist/core/session-store.d.ts +2 -1
- package/dist/core/session-store.js +2 -1
- package/dist/core/session.d.ts +0 -1
- package/dist/core/task-notification.d.ts +1 -0
- package/dist/core/task-registry-agent.d.ts +115 -0
- package/dist/core/task-registry-agent.js +1163 -0
- package/dist/core/task-registry-monitor.d.ts +13 -0
- package/dist/core/task-registry-monitor.js +347 -0
- package/dist/core/task-registry-shared.d.ts +294 -0
- package/dist/core/task-registry-shared.js +271 -0
- package/dist/core/task-registry-workflow.d.ts +5 -0
- package/dist/core/task-registry-workflow.js +143 -0
- package/dist/core/task-registry.d.ts +54 -213
- package/dist/core/task-registry.js +190 -1766
- package/dist/core/task-tool-shape.d.ts +32 -0
- package/dist/core/task-tool-shape.js +83 -0
- package/dist/core/tool-policy.d.ts +1 -2
- package/dist/core/tool-result-budget.d.ts +1 -1
- package/dist/core/tool-result-store.d.ts +2 -2
- package/dist/core/tool-result-store.js +3 -3
- package/dist/core/tools.d.ts +7 -3
- package/dist/core/tools.js +3 -8
- package/dist/core/trace.d.ts +2 -2
- package/dist/core/types.d.ts +1 -29
- package/dist/core/workflow-journal-store.d.ts +2 -0
- package/dist/core/workflow-journal-store.js +14 -0
- package/dist/core/workflow-run-store-contract.d.ts +1 -1
- package/dist/core/workflow-run-store-contract.js +19 -19
- package/dist/engine/execution-env/node-execution-env.d.ts +14 -0
- package/dist/engine/execution-env/node-execution-env.js +184 -44
- package/dist/engine/lsp/frame-decoder.d.ts +1 -1
- package/dist/engine/lsp/frame-decoder.js +1 -1
- package/dist/engine/lsp/node-lsp-manager.d.ts +4 -2
- package/dist/engine/lsp/node-lsp-manager.js +22 -5
- package/dist/engine/lsp/stdio-lsp-transport.d.ts +1 -1
- package/dist/engine/lsp/stdio-lsp-transport.js +19 -8
- package/dist/index.d.ts +18 -16
- package/dist/index.js +15 -13
- package/dist/internal/harness-types.d.ts +7 -0
- package/dist/internal/harness-types.js +1 -0
- package/dist/internal/harness.d.ts +1 -7
- package/dist/internal/harness.js +1 -0
- package/dist/orchestration/goal.js +2 -2
- package/dist/orchestration/run-workflow-tool.d.ts +2 -1
- package/dist/orchestration/run-workflow-tool.js +29 -6
- package/dist/orchestration/workflow-types.d.ts +193 -0
- package/dist/orchestration/workflow-types.js +54 -0
- package/dist/orchestration/workflow.d.ts +3 -184
- package/dist/orchestration/workflow.js +87 -64
- package/dist/prompt-assembly/assemble.d.ts +3 -3
- package/dist/prompt-assembly/assemble.js +0 -16
- package/dist/prompt-assembly/explain.d.ts +1 -1
- package/dist/prompt-assembly/tool-catalog.d.ts +1 -1
- package/dist/prompts/default.d.ts +1 -6
- package/dist/prompts/default.js +3 -11
- package/dist/scenarios/env.d.ts +1 -1
- package/dist/scenarios/full-body.d.ts +0 -3
- package/dist/scenarios/full-body.js +0 -2
- package/dist/stores/cc/mailbox-store.js +6 -1
- package/dist/stores/file/background-agent-store.d.ts +2 -15
- package/dist/stores/file/background-agent-store.js +33 -102
- package/dist/stores/file/checkpoint-store.d.ts +1 -14
- package/dist/stores/file/checkpoint-store.js +47 -119
- package/dist/stores/file/file-snapshot-store.d.ts +1 -1
- package/dist/stores/file/mailbox-store.d.ts +1 -1
- package/dist/stores/file/mailbox-store.js +9 -7
- package/dist/stores/file/shared-ledger.d.ts +35 -0
- package/dist/stores/file/shared-ledger.js +94 -0
- package/dist/stores/file/workflow-run-store.d.ts +1 -14
- package/dist/stores/file/workflow-run-store.js +24 -93
- package/dist/tools/fs/bash-readonly-classifier.d.ts +8 -0
- package/dist/tools/fs/bash-readonly-classifier.js +166 -0
- package/dist/tools/fs/encoding.d.ts +5 -0
- package/dist/tools/fs/encoding.js +6 -0
- package/dist/tools/fs/fs-bash.d.ts +31 -0
- package/dist/tools/fs/fs-bash.js +672 -0
- package/dist/tools/fs/fs-pdf.d.ts +22 -0
- package/dist/tools/fs/fs-pdf.js +236 -0
- package/dist/tools/fs/fs-read.d.ts +8 -0
- package/dist/tools/fs/fs-read.js +286 -0
- package/dist/tools/fs/fs-search-tools.d.ts +5 -0
- package/dist/tools/fs/fs-search-tools.js +173 -0
- package/dist/tools/fs/fs-shared.d.ts +55 -0
- package/dist/tools/fs/fs-shared.js +144 -0
- package/dist/tools/fs/fs-write.d.ts +8 -0
- package/dist/tools/fs/fs-write.js +423 -0
- package/dist/tools/fs/index.d.ts +12 -65
- package/dist/tools/fs/index.js +26 -2026
- package/dist/tools/fs/notebook.d.ts +43 -0
- package/dist/tools/fs/notebook.js +141 -0
- package/dist/tools/fs/pdf.d.ts +1 -1
- package/dist/tools/fs/repo-map.d.ts +3 -3
- package/dist/tools/fs/repo-map.js +3 -3
- package/dist/tools/fs/safety.d.ts +1 -1
- package/dist/tools/fs/search.d.ts +1 -1
- package/dist/tools/fs/search.js +141 -12
- package/dist/tools/gitea-issue.js +4 -2
- package/dist/tools/monitor.d.ts +1 -1
- package/dist/tools/monitor.js +12 -8
- package/dist/tools/scheduler-tools.d.ts +1 -1
- package/dist/tools/scheduler-tools.js +16 -16
- package/dist/tools/task-list.js +34 -12
- package/dist/tools/web.d.ts +4 -2
- package/dist/tools/web.js +106 -20
- package/dist/tools/worktree.d.ts +1 -1
- package/dist/tools/worktree.js +14 -14
- package/package.json +1 -1
- package/dist/core/dynamic-recall.d.ts +0 -32
- package/dist/core/dynamic-recall.js +0 -76
|
@@ -1,129 +1,36 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { defineTool } from "./tools.js";
|
|
2
|
+
import { defineTool, errorResult } from "./tools.js";
|
|
4
3
|
import { hasBackgroundShell } from "./background-shell.js";
|
|
5
4
|
import { shutdownDebug } from "./shutdown-debug.js";
|
|
6
|
-
import {
|
|
7
|
-
import { canAccessAgentRecord, BackgroundAgentStoreError, } from "./background-agent-store.js";
|
|
5
|
+
import { canAccessAgentRecord, } from "./background-agent-store.js";
|
|
8
6
|
import { delimitUntrusted } from "./untrusted-text.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
},
|
|
17
|
-
clearInterval: (h) => clearInterval(h),
|
|
18
|
-
now: () => Date.now(),
|
|
19
|
-
};
|
|
7
|
+
import { registerMonitorLane, pollMonitorLane, stopMonitorLane } from "./task-registry-monitor.js";
|
|
8
|
+
import { registerWorkflowLane, pollWorkflowLane, stopWorkflowLane } from "./task-registry-workflow.js";
|
|
9
|
+
import { mintCompletionId, canAccessWorkflowRun, formatWorkflowRun, clipTaskOutput, assertOwnership, sleepPollStep, statusFromBackground, rollSpoolText, accountDroppedBytes, droppedGapNote, alreadyTerminalStopNote, TASK_OUTPUT_MAX_CHARS, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccess, DURABLE_AGENT_HANDLE_RE, } from "./task-registry-shared.js";
|
|
10
|
+
export { normalizeAgentName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR } from "./task-registry-shared.js";
|
|
11
|
+
import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_ALIASES, TASK_STOP_ALIASES, TASK_OUTPUT_CONTRACT, TASK_STOP_CONTRACT, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, REGISTRY_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "./task-tool-shape.js";
|
|
12
|
+
import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane, endDurableClaimLane, reapDurableAgentsLane, releaseDurableTranscriptAnchorLane, bindBackgroundAgentSessionLane, registerBackgroundAgentLane, parkBackgroundAgentLane, reconcileParkedAgentsLane, claimParkedAgentLane, rollbackParkedClaimLane, consumeParkedFlipLane, finalizeParkedResumeLane, settleBackgroundAgentLane, abortBackgroundAgentsForOwnerLane, serveDurableAgentRowLane, resolveBackgroundAgentByNameLane, markRetainedContinuationLane, reviveBackgroundAgentLane, settleRevivedAgentLane, unmarkRetainedContinuationLane, attachAgentNotifyLane, deliverToRunningAgentLane, runningBackgroundAgentLabelsLane, pollBackgroundAgentLane, stopBackgroundAgentLane, } from "./task-registry-agent.js";
|
|
13
|
+
export { canAccessWorkflowRun, clipTaskOutput, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE };
|
|
20
14
|
const BLOCK_DEFAULT_TIMEOUT_MS = 30_000;
|
|
21
15
|
const BLOCK_MAX_TIMEOUT_MS = 600_000;
|
|
22
|
-
const BLOCK_POLL_INTERVAL_MS = 400;
|
|
23
16
|
function blockDeadline(timeoutMs, now) {
|
|
24
17
|
const t = Math.min(BLOCK_MAX_TIMEOUT_MS, Math.max(1, Math.floor(timeoutMs ?? BLOCK_DEFAULT_TIMEOUT_MS)));
|
|
25
18
|
return now + t;
|
|
26
19
|
}
|
|
27
|
-
const sleepPollStep = (deadline, signal) => sleepMs(deadline === undefined ? BLOCK_POLL_INTERVAL_MS : Math.max(1, Math.min(BLOCK_POLL_INTERVAL_MS, deadline - Date.now())), signal);
|
|
28
|
-
const sleepMs = (ms, signal) => new Promise((resolve) => {
|
|
29
|
-
const t = setTimeout(done, ms);
|
|
30
|
-
function done() {
|
|
31
|
-
clearTimeout(t);
|
|
32
|
-
signal?.removeEventListener("abort", done);
|
|
33
|
-
resolve();
|
|
34
|
-
}
|
|
35
|
-
signal?.addEventListener("abort", done, { once: true });
|
|
36
|
-
});
|
|
37
20
|
const BLOCK_WAIT_WRITEOUT_RESERVE_MS = 5_000;
|
|
38
|
-
export const DURABLE_AGENT_HEARTBEAT_MS = 60_000;
|
|
39
|
-
export const DURABLE_AGENT_HANDLE_RE = /^a[0-9a-f]{6,32}$/;
|
|
40
21
|
const TASK_PREFIX = {
|
|
41
22
|
background_bash: "b",
|
|
42
23
|
workflow: "w",
|
|
43
24
|
background_agent: "a",
|
|
44
25
|
monitor: "m",
|
|
45
26
|
};
|
|
46
|
-
export const BG_AGENT_REAP_STOP_ERROR = "stopped (parent task ended or TaskStop)";
|
|
47
|
-
export const MONITOR_BATCH_WINDOW_MS = 200;
|
|
48
|
-
export const MONITOR_DEFAULT_TIMEOUT_MS = 300_000;
|
|
49
|
-
export const MONITOR_MAX_TIMEOUT_MS = 3_600_000;
|
|
50
|
-
export const MONITOR_MAX_BATCHES_PER_MINUTE = 50;
|
|
51
|
-
const MONITOR_EVENT_RESULT_CAP = 2_000;
|
|
52
|
-
const TASK_OUTPUT_DEFAULT_CHARS = 32_000;
|
|
53
|
-
const TASK_OUTPUT_MAX_CHARS = 160_000;
|
|
54
|
-
const TASK_OUTPUT_ALIASES = ["BashOutput", "AgentOutputTool", "BashOutputTool", "AgentOutput", "WorkflowStatus"];
|
|
55
|
-
const TASK_STOP_ALIASES = ["KillShell", "KillBash"];
|
|
56
|
-
const TASK_OUTPUT_MIN_CHARS = 512;
|
|
57
|
-
function taskMaxOutputChars() {
|
|
58
|
-
const raw = process.env.TASK_MAX_OUTPUT_LENGTH;
|
|
59
|
-
if (!raw)
|
|
60
|
-
return TASK_OUTPUT_DEFAULT_CHARS;
|
|
61
|
-
if (!/^\d+$/.test(raw.trim()))
|
|
62
|
-
return TASK_OUTPUT_DEFAULT_CHARS;
|
|
63
|
-
const n = parseInt(raw.trim(), 10);
|
|
64
|
-
if (!Number.isFinite(n) || n <= 0)
|
|
65
|
-
return TASK_OUTPUT_DEFAULT_CHARS;
|
|
66
|
-
return Math.min(Math.max(n, TASK_OUTPUT_MIN_CHARS), TASK_OUTPUT_MAX_CHARS);
|
|
67
|
-
}
|
|
68
|
-
export function clipTaskOutput(s, fullOutputPath) {
|
|
69
|
-
return clipWithFilePointer(s, taskMaxOutputChars(), fullOutputPath);
|
|
70
|
-
}
|
|
71
|
-
function firstString(...values) {
|
|
72
|
-
for (const value of values) {
|
|
73
|
-
if (typeof value === "string" && value.trim() !== "")
|
|
74
|
-
return value.trim();
|
|
75
|
-
}
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
78
27
|
function noTask(taskId) {
|
|
79
28
|
return {
|
|
80
29
|
content: JSON.stringify({ error: "task not found" }),
|
|
81
30
|
details: { task_id: taskId, type: "unknown", status: "not_found", retrieval_status: "not_ready", error: "not_found" },
|
|
31
|
+
isError: true,
|
|
82
32
|
};
|
|
83
33
|
}
|
|
84
|
-
export function normalizeAgentName(s) {
|
|
85
|
-
return s
|
|
86
|
-
.normalize("NFKC")
|
|
87
|
-
.replace(/[\p{Cc}\p{Cf}]/gu, (t) => (/\s/.test(t) ? t : ""))
|
|
88
|
-
.trim()
|
|
89
|
-
.toLowerCase()
|
|
90
|
-
.replace(/\s+/g, "-");
|
|
91
|
-
}
|
|
92
|
-
function editDistance(a, b) {
|
|
93
|
-
if (a === b)
|
|
94
|
-
return 0;
|
|
95
|
-
const m = a.length;
|
|
96
|
-
const n = b.length;
|
|
97
|
-
const d = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
|
|
98
|
-
for (let i = 1; i <= m; i++) {
|
|
99
|
-
for (let j = 1; j <= n; j++) {
|
|
100
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
101
|
-
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
102
|
-
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
103
|
-
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return d[m][n];
|
|
108
|
-
}
|
|
109
|
-
function closestName(query, candidates) {
|
|
110
|
-
let best;
|
|
111
|
-
let bestDist = 3;
|
|
112
|
-
const seen = new Set();
|
|
113
|
-
for (const c of candidates) {
|
|
114
|
-
if (seen.has(c))
|
|
115
|
-
continue;
|
|
116
|
-
seen.add(c);
|
|
117
|
-
if (Math.abs(c.length - query.length) > 2)
|
|
118
|
-
continue;
|
|
119
|
-
const dist = editDistance(query, c);
|
|
120
|
-
if (dist <= 2 && dist < bestDist) {
|
|
121
|
-
bestDist = dist;
|
|
122
|
-
best = c;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return best;
|
|
126
|
-
}
|
|
127
34
|
function noTaskForStop(taskId, enrich) {
|
|
128
35
|
let content = `No task found with ID: ${taskId}`;
|
|
129
36
|
if (enrich?.suggestion !== undefined)
|
|
@@ -133,6 +40,7 @@ function noTaskForStop(taskId, enrich) {
|
|
|
133
40
|
return {
|
|
134
41
|
content,
|
|
135
42
|
details: { task_id: taskId, type: "unknown", status: "not_found", retrieval_status: "not_ready", error: "not_found" },
|
|
43
|
+
isError: true,
|
|
136
44
|
};
|
|
137
45
|
}
|
|
138
46
|
function bashTerminalResult(spool) {
|
|
@@ -140,24 +48,6 @@ function bashTerminalResult(spool) {
|
|
|
140
48
|
const err = spool.stderr.slice(-2_000);
|
|
141
49
|
return err.length > 0 ? `${out}${out.length > 0 ? "\n" : ""}--- stderr (tail) ---\n${err}` : out;
|
|
142
50
|
}
|
|
143
|
-
function statusFromBackground(status, exitCode) {
|
|
144
|
-
if (status === "running")
|
|
145
|
-
return "running";
|
|
146
|
-
if (status === "killed")
|
|
147
|
-
return "killed";
|
|
148
|
-
if (status === "failed")
|
|
149
|
-
return "failed";
|
|
150
|
-
if (status === "exited")
|
|
151
|
-
return exitCode === 0 ? "completed" : "failed";
|
|
152
|
-
return "failed";
|
|
153
|
-
}
|
|
154
|
-
function rollSpoolText(spool, s, cap) {
|
|
155
|
-
if (s.length <= cap)
|
|
156
|
-
return s;
|
|
157
|
-
const half = Math.floor(cap / 2);
|
|
158
|
-
spool.rolledChars += s.length - 2 * half;
|
|
159
|
-
return s.slice(0, half) + s.slice(s.length - half);
|
|
160
|
-
}
|
|
161
51
|
function bashMirrorGapNote(handle) {
|
|
162
52
|
return handle.outputFile !== undefined && handle.spool?.mirrorFailed === true
|
|
163
53
|
? " [!] The output file is INCOMPLETE (one or more writes to it failed) — trust the result text / TaskOutput over the file."
|
|
@@ -166,362 +56,112 @@ function bashMirrorGapNote(handle) {
|
|
|
166
56
|
function monitorInTimeoutWindow(handle) {
|
|
167
57
|
return handle.status === "running" && handle.deadlineAt !== undefined && handle.timers.now() < handle.deadlineAt;
|
|
168
58
|
}
|
|
169
|
-
function canAccess(handle, access) {
|
|
170
|
-
if (handle.scope === undefined || access.scope === undefined || access.scope !== handle.scope)
|
|
171
|
-
return false;
|
|
172
|
-
if (handle.owner === undefined)
|
|
173
|
-
return false;
|
|
174
|
-
if (access.owner === handle.owner)
|
|
175
|
-
return true;
|
|
176
|
-
if (handle.sessionScoped && access.sessionId !== undefined && access.sessionId === handle.owner)
|
|
177
|
-
return true;
|
|
178
|
-
if (handle.type === "workflow" && access.sessionId !== undefined && handle.originatingSessionId === access.sessionId)
|
|
179
|
-
return true;
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
function assertOwnership(input, site) {
|
|
183
|
-
if (input.owner === undefined || input.scope === undefined) {
|
|
184
|
-
throw new Error(`TaskRegistry.${site}: owner AND scope are both required (access is default-deny on both axes; use scope "default" for a single-tenant deployment)`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
export function canAccessWorkflowRun(run, access) {
|
|
188
|
-
if (access.scope === undefined || run.scope !== access.scope)
|
|
189
|
-
return false;
|
|
190
|
-
if (access.owner !== undefined && run.sourceTaskId !== undefined && run.sourceTaskId === access.owner)
|
|
191
|
-
return true;
|
|
192
|
-
if (access.sessionId !== undefined && run.originatingSessionId !== undefined && run.originatingSessionId === access.sessionId)
|
|
193
|
-
return true;
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
const WORKFLOW_PARTIAL_RESULTS_MAX = 5;
|
|
197
|
-
const WORKFLOW_AGENT_ROWS_MAX = 60;
|
|
198
|
-
function workflowAgentRow(a, ordinal) {
|
|
199
|
-
return {
|
|
200
|
-
ordinal,
|
|
201
|
-
label: a.label,
|
|
202
|
-
...(a.phase !== undefined ? { phase: a.phase } : {}),
|
|
203
|
-
status: a.status,
|
|
204
|
-
...(a.errorCode !== undefined ? { errorCode: a.errorCode } : {}),
|
|
205
|
-
...(a.errorMessage !== undefined
|
|
206
|
-
? { error: delimitUntrusted(`workflow agent ${a.label} error`, boundedRedactedSummary(a.errorMessage, 200)) }
|
|
207
|
-
: {}),
|
|
208
|
-
...(a.startedAt !== undefined && a.endedAt !== undefined ? { elapsedMs: a.endedAt - a.startedAt } : {}),
|
|
209
|
-
...(a.replayed === true ? { replayed: true } : {}),
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
function formatWorkflowRun(run) {
|
|
213
|
-
const summary = summarizeWorkflowRun(run);
|
|
214
|
-
const done = run.agents.filter((a) => a.status === "completed" || a.status === "failed").length;
|
|
215
|
-
const failed = run.agents.filter((a) => a.status === "failed").length;
|
|
216
|
-
const allRows = run.agents.map((a, i) => ({ a, i }));
|
|
217
|
-
const overCap = allRows.length > WORKFLOW_AGENT_ROWS_MAX;
|
|
218
|
-
const keptRows = overCap
|
|
219
|
-
? (() => {
|
|
220
|
-
const urgent = allRows.filter((r) => r.a.status !== "completed");
|
|
221
|
-
const budget = Math.max(0, WORKFLOW_AGENT_ROWS_MAX - urgent.length);
|
|
222
|
-
const fill = allRows.filter((r) => r.a.status === "completed").slice(0, budget);
|
|
223
|
-
return [...urgent, ...fill].sort((x, y) => x.i - y.i);
|
|
224
|
-
})()
|
|
225
|
-
: allRows;
|
|
226
|
-
const agentRows = keptRows.map((r) => workflowAgentRow(r.a, r.i));
|
|
227
|
-
const finishedAgents = run.status === "failed" ? run.agents.filter((a) => a.status === "completed" && a.output !== undefined && a.output !== "") : [];
|
|
228
|
-
return {
|
|
229
|
-
content: JSON.stringify({
|
|
230
|
-
task_id: run.id,
|
|
231
|
-
runId: run.id,
|
|
232
|
-
type: "workflow",
|
|
233
|
-
status: run.status,
|
|
234
|
-
...(summary.name ? { name: summary.name } : {}),
|
|
235
|
-
...(summary.currentPhase ? { currentPhase: summary.currentPhase } : {}),
|
|
236
|
-
agents: { done, total: run.agents.length, failed },
|
|
237
|
-
agent_runs: agentRows,
|
|
238
|
-
...(overCap ? { agent_runs_note: `showing ${agentRows.length} of ${run.agents.length} agent rows (every non-completed row kept; completed rows trimmed).` } : {}),
|
|
239
|
-
startedAt: run.startedAt,
|
|
240
|
-
...(run.endedAt !== undefined ? { endedAt: run.endedAt, elapsedMs: run.endedAt - run.startedAt } : {}),
|
|
241
|
-
...(run.status === "failed" && run.error ? { error: delimitUntrusted("workflow error", boundedRedactedSummary(run.error, 500)) } : {}),
|
|
242
|
-
...(run.status === "completed" && run.result !== undefined
|
|
243
|
-
? { result: delimitUntrusted("workflow result", run.resultFull ?? run.result) }
|
|
244
|
-
: {}),
|
|
245
|
-
...(finishedAgents.length > 0
|
|
246
|
-
? {
|
|
247
|
-
partial_results: finishedAgents.slice(0, WORKFLOW_PARTIAL_RESULTS_MAX).map((a) => ({
|
|
248
|
-
agent: a.label,
|
|
249
|
-
output: delimitUntrusted(`workflow agent ${a.label} output`, a.output),
|
|
250
|
-
})),
|
|
251
|
-
...(finishedAgents.length > WORKFLOW_PARTIAL_RESULTS_MAX
|
|
252
|
-
? { partial_results_note: `showing ${WORKFLOW_PARTIAL_RESULTS_MAX} of ${finishedAgents.length} completed agent outputs; the full set remains retrievable from the workflow run record (getWorkflowRun / the workflow journal).` }
|
|
253
|
-
: {}),
|
|
254
|
-
}
|
|
255
|
-
: {}),
|
|
256
|
-
note: run.status === "running"
|
|
257
|
-
? "still running — poll again shortly."
|
|
258
|
-
: run.status === "completed"
|
|
259
|
-
? run.result !== undefined
|
|
260
|
-
? "completed — `result` above is the workflow's return value."
|
|
261
|
-
: "completed — this run predates result persistence; its result was delivered on the completion notification."
|
|
262
|
-
: finishedAgents.length > 0
|
|
263
|
-
? `failed — see error. ${finishedAgents.length} agent(s) had already completed before the run ended; their outputs are inlined under \`partial_results\`.`
|
|
264
|
-
: "failed — see error.",
|
|
265
|
-
}),
|
|
266
|
-
details: {
|
|
267
|
-
task_id: run.id,
|
|
268
|
-
type: "workflow",
|
|
269
|
-
status: run.status,
|
|
270
|
-
retrieval_status: "success",
|
|
271
|
-
details: summary,
|
|
272
|
-
},
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
59
|
export class TaskRegistry {
|
|
276
60
|
handles = new Map();
|
|
277
61
|
sessionReapHooks = new Set();
|
|
278
62
|
bgQuiescenceWatchers = new Map();
|
|
279
63
|
legacyToTaskId = new Map();
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
liveLanes++;
|
|
297
|
-
this.durableAgentWrite(bh, {});
|
|
298
|
-
}
|
|
299
|
-
else if (bh.durable.flushFailed) {
|
|
300
|
-
if ((bh.durable.redrives = (bh.durable.redrives ?? 0) + 1) > 60) {
|
|
301
|
-
bh.durable.poisoned = true;
|
|
302
|
-
process.emitWarning(`sema durable-agents: terminal flush for ${bh.durable.record.handle} unrecoverable after 60 heartbeat re-drives — durable writes for this row stop here`);
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
liveLanes++;
|
|
306
|
-
this.durableAgentWrite(bh, {});
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
if (liveLanes === 0) {
|
|
310
|
-
clearInterval(t);
|
|
311
|
-
this.durableHeartbeatTimer = undefined;
|
|
312
|
-
}
|
|
313
|
-
}, DURABLE_AGENT_HEARTBEAT_MS);
|
|
314
|
-
t.unref?.();
|
|
315
|
-
this.durableHeartbeatTimer = t;
|
|
316
|
-
}
|
|
317
|
-
durableAgentWrite(handle, patch, clear) {
|
|
318
|
-
const lane = handle.durable;
|
|
319
|
-
if (!lane || lane.poisoned)
|
|
320
|
-
return;
|
|
321
|
-
lane.chain = lane.chain.then(async () => {
|
|
322
|
-
if (lane.poisoned)
|
|
323
|
-
return;
|
|
324
|
-
Object.assign(lane.record, patch);
|
|
325
|
-
if (clear !== undefined)
|
|
326
|
-
for (const k of clear)
|
|
327
|
-
delete lane.record[k];
|
|
328
|
-
lane.record.updatedAt = Date.now();
|
|
329
|
-
const poison = (why) => {
|
|
330
|
-
lane.poisoned = true;
|
|
331
|
-
process.emitWarning(`sema durable-agents: ${why} for ${lane.record.handle} — durable writes for this row stop here`);
|
|
332
|
-
};
|
|
333
|
-
const adoptOwnRow = async () => {
|
|
334
|
-
try {
|
|
335
|
-
const live = await lane.store.get(lane.record.handle, lane.record.scope);
|
|
336
|
-
if (!live)
|
|
337
|
-
return "gone";
|
|
338
|
-
if (live.writerId !== lane.record.writerId)
|
|
339
|
-
return "foreign";
|
|
340
|
-
if (live.writerEpoch !== lane.record.writerEpoch)
|
|
341
|
-
return "foreign";
|
|
342
|
-
lane.record.rev = live.rev;
|
|
343
|
-
lane.written = true;
|
|
344
|
-
return "adopted";
|
|
345
|
-
}
|
|
346
|
-
catch {
|
|
347
|
-
return "unreadable";
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
351
|
-
if (lane.poisoned)
|
|
352
|
-
return;
|
|
353
|
-
if (attempt > 0) {
|
|
354
|
-
await new Promise((r) => {
|
|
355
|
-
const t = setTimeout(r, 50 * attempt);
|
|
356
|
-
t.unref?.();
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
try {
|
|
360
|
-
if (!lane.written) {
|
|
361
|
-
await lane.store.put(structuredClone(lane.record));
|
|
362
|
-
lane.written = true;
|
|
363
|
-
lane.flushFailed = false;
|
|
364
|
-
lane.redrives = 0;
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
const ok = await lane.store.update(lane.record.handle, lane.record.scope, structuredClone(lane.record), {
|
|
368
|
-
rev: lane.record.rev,
|
|
369
|
-
});
|
|
370
|
-
if (ok) {
|
|
371
|
-
lane.record.rev += 1;
|
|
372
|
-
lane.flushFailed = false;
|
|
373
|
-
lane.redrives = 0;
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
const verdict = await adoptOwnRow();
|
|
377
|
-
if (lane.poisoned)
|
|
378
|
-
return;
|
|
379
|
-
if (verdict === "adopted")
|
|
380
|
-
continue;
|
|
381
|
-
if (verdict === "unreadable")
|
|
382
|
-
continue;
|
|
383
|
-
poison(verdict === "gone" ? "row reaped mid-write" : "CAS lost to a foreign writer");
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
catch (e) {
|
|
387
|
-
if (e instanceof BackgroundAgentStoreError && e.code === "agent_record.already_exists") {
|
|
388
|
-
const verdict = await adoptOwnRow();
|
|
389
|
-
if (lane.poisoned)
|
|
390
|
-
return;
|
|
391
|
-
if (verdict === "adopted")
|
|
392
|
-
continue;
|
|
393
|
-
if (verdict === "unreadable")
|
|
394
|
-
continue;
|
|
395
|
-
if (verdict === "gone")
|
|
396
|
-
continue;
|
|
397
|
-
poison("handle collision (foreign durable row left untouched)");
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
shutdownDebug(`durable-agents write failed for ${lane.record.handle} (attempt ${attempt + 1}/3)`, { error: String(e) });
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
lane.flushFailed = true;
|
|
404
|
-
});
|
|
405
|
-
}
|
|
64
|
+
core = (() => {
|
|
65
|
+
const self = this;
|
|
66
|
+
return {
|
|
67
|
+
handles: self.handles,
|
|
68
|
+
mintTaskId: (type) => self.mintTaskId(type),
|
|
69
|
+
markStopSource: (id, source) => self.markStopSource(id, source),
|
|
70
|
+
notifyTerminalOnce: (handle, send) => self.notifyTerminalOnce(handle, send),
|
|
71
|
+
pokeBgQuiescence: (owner) => self.pokeBgQuiescence(owner),
|
|
72
|
+
get writerId() { return self.writerId; },
|
|
73
|
+
get reapingHandles() { return self.reapingHandles; },
|
|
74
|
+
get claimingHandles() { return self.claimingHandles; },
|
|
75
|
+
get durableHeartbeatTimer() { return self.durableHeartbeatTimer; },
|
|
76
|
+
set durableHeartbeatTimer(v) { self.durableHeartbeatTimer = v; },
|
|
77
|
+
get deliverConfirmTimeoutMs() { return self.deliverConfirmTimeoutMs; },
|
|
78
|
+
};
|
|
79
|
+
})();
|
|
406
80
|
durableAgentArmed(id) {
|
|
407
|
-
|
|
408
|
-
if (!h || h.type !== "background_agent")
|
|
409
|
-
return false;
|
|
410
|
-
const lane = h.durable;
|
|
411
|
-
return lane !== undefined && !lane.poisoned;
|
|
81
|
+
return durableAgentArmedLane(this.core, id);
|
|
412
82
|
}
|
|
413
83
|
durableAgentRowProbe(id) {
|
|
414
|
-
|
|
415
|
-
if (!h || h.type !== "background_agent")
|
|
416
|
-
return undefined;
|
|
417
|
-
const lane = h.durable;
|
|
418
|
-
if (!lane)
|
|
419
|
-
return undefined;
|
|
420
|
-
return async () => {
|
|
421
|
-
await lane.chain.catch(() => undefined);
|
|
422
|
-
return lane.written && !lane.poisoned && !lane.flushFailed;
|
|
423
|
-
};
|
|
84
|
+
return durableAgentRowProbeLane(this.core, id);
|
|
424
85
|
}
|
|
425
86
|
beginDurableClaim(id) {
|
|
426
|
-
|
|
427
|
-
if (live !== undefined && (live.status === "running" || live.status === "pending"))
|
|
428
|
-
return false;
|
|
429
|
-
if (this.reapingHandles.has(id) || this.claimingHandles.has(id))
|
|
430
|
-
return false;
|
|
431
|
-
this.claimingHandles.add(id);
|
|
432
|
-
return true;
|
|
87
|
+
return beginDurableClaimLane(this.core, id);
|
|
433
88
|
}
|
|
434
89
|
endDurableClaim(id) {
|
|
435
|
-
this.
|
|
90
|
+
return endDurableClaimLane(this.core, id);
|
|
436
91
|
}
|
|
437
92
|
async reapDurableAgents(scope, deps, policy) {
|
|
438
|
-
|
|
439
|
-
if (policy.staleRunningMaxAgeMs !== undefined) {
|
|
440
|
-
await deps.store.reap(scope, now, { staleRunningMaxAgeMs: policy.staleRunningMaxAgeMs });
|
|
441
|
-
}
|
|
442
|
-
if (policy.maxAgeMs === undefined && policy.keep === undefined)
|
|
443
|
-
return { rowsReaped: 0, sessionsReleased: 0, skippedNoSessions: 0 };
|
|
444
|
-
const terminal = (await deps.store.listByScope(scope)).filter((r) => r.status !== "running" && r.status !== "parked");
|
|
445
|
-
terminal.sort((a, b) => b.spawnedAt - a.spawnedAt);
|
|
446
|
-
const doomed = new Map();
|
|
447
|
-
if (policy.maxAgeMs !== undefined) {
|
|
448
|
-
for (const r of terminal) {
|
|
449
|
-
if ((r.settledAt ?? r.spawnedAt) < now - policy.maxAgeMs)
|
|
450
|
-
doomed.set(r.handle, r);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
if (policy.keep !== undefined) {
|
|
454
|
-
for (const r of terminal.slice(Math.max(0, policy.keep)))
|
|
455
|
-
doomed.set(r.handle, r);
|
|
456
|
-
}
|
|
457
|
-
let rowsReaped = 0;
|
|
458
|
-
let sessionsReleased = 0;
|
|
459
|
-
let skippedNoSessions = 0;
|
|
460
|
-
for (const r of doomed.values()) {
|
|
461
|
-
if (this.claimingHandles.has(r.handle))
|
|
462
|
-
continue;
|
|
463
|
-
this.reapingHandles.add(r.handle);
|
|
464
|
-
try {
|
|
465
|
-
let live;
|
|
466
|
-
try {
|
|
467
|
-
live = await deps.store.get(r.handle, scope);
|
|
468
|
-
}
|
|
469
|
-
catch {
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
472
|
-
if (!live || live.status === "running" || live.status === "parked")
|
|
473
|
-
continue;
|
|
474
|
-
const inProc = this.handles.get(r.handle);
|
|
475
|
-
if (inProc !== undefined && (inProc.status === "running" || inProc.status === "pending" || inProc.status === "parked"))
|
|
476
|
-
continue;
|
|
477
|
-
if (live.sessionId !== undefined && deps.sessions === undefined) {
|
|
478
|
-
skippedNoSessions++;
|
|
479
|
-
continue;
|
|
480
|
-
}
|
|
481
|
-
try {
|
|
482
|
-
if (!(await deps.store.delete(r.handle, scope, { rev: live.rev })))
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
catch {
|
|
486
|
-
continue;
|
|
487
|
-
}
|
|
488
|
-
rowsReaped++;
|
|
489
|
-
if (deps.mailbox !== undefined) {
|
|
490
|
-
try {
|
|
491
|
-
await deps.mailbox.drop(scope, r.handle);
|
|
492
|
-
}
|
|
493
|
-
catch {
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
if (live.sessionId !== undefined && deps.sessions !== undefined) {
|
|
497
|
-
try {
|
|
498
|
-
await deps.sessions.unpin?.(live.sessionId);
|
|
499
|
-
await deps.sessions.release(live.sessionId);
|
|
500
|
-
sessionsReleased++;
|
|
501
|
-
}
|
|
502
|
-
catch {
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
finally {
|
|
507
|
-
this.reapingHandles.delete(r.handle);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
return { rowsReaped, sessionsReleased, skippedNoSessions };
|
|
93
|
+
return reapDurableAgentsLane(this.core, scope, deps, policy);
|
|
511
94
|
}
|
|
512
95
|
releaseDurableTranscriptAnchor(id) {
|
|
513
|
-
|
|
514
|
-
if (!handle || handle.type !== "background_agent")
|
|
515
|
-
return;
|
|
516
|
-
this.durableAgentWrite(handle, {}, ["sessionId"]);
|
|
96
|
+
return releaseDurableTranscriptAnchorLane(this.core, id);
|
|
517
97
|
}
|
|
518
98
|
bindBackgroundAgentSession(id, sessionId) {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
99
|
+
return bindBackgroundAgentSessionLane(this.core, id, sessionId);
|
|
100
|
+
}
|
|
101
|
+
registerBackgroundAgent(input) {
|
|
102
|
+
return registerBackgroundAgentLane(this.core, input);
|
|
103
|
+
}
|
|
104
|
+
parkBackgroundAgent(id, park) {
|
|
105
|
+
return parkBackgroundAgentLane(this.core, id, park);
|
|
106
|
+
}
|
|
107
|
+
async reconcileParkedAgents(stores, scope, now, opts) {
|
|
108
|
+
return reconcileParkedAgentsLane(this.core, stores, scope, now, opts);
|
|
109
|
+
}
|
|
110
|
+
async claimParkedAgent(stores, handle, access) {
|
|
111
|
+
return claimParkedAgentLane(stores, handle, access);
|
|
112
|
+
}
|
|
113
|
+
async rollbackParkedClaim(stores, ticket) {
|
|
114
|
+
return rollbackParkedClaimLane(this.core, stores, ticket);
|
|
115
|
+
}
|
|
116
|
+
async consumeParkedFlip(id, stores, ticket) {
|
|
117
|
+
return consumeParkedFlipLane(this.core, id, stores, ticket);
|
|
118
|
+
}
|
|
119
|
+
finalizeParkedResume(id) {
|
|
120
|
+
return finalizeParkedResumeLane(this.core, id);
|
|
121
|
+
}
|
|
122
|
+
settleBackgroundAgent(id, outcome) {
|
|
123
|
+
return settleBackgroundAgentLane(this.core, id, outcome);
|
|
524
124
|
}
|
|
125
|
+
abortBackgroundAgentsForOwner(access, opts) {
|
|
126
|
+
return abortBackgroundAgentsForOwnerLane(this.core, access, opts);
|
|
127
|
+
}
|
|
128
|
+
serveDurableAgentRow(row) {
|
|
129
|
+
return serveDurableAgentRowLane(row);
|
|
130
|
+
}
|
|
131
|
+
resolveBackgroundAgentByName(name, access, opts) {
|
|
132
|
+
return resolveBackgroundAgentByNameLane(this.core, name, access, opts);
|
|
133
|
+
}
|
|
134
|
+
markRetainedContinuation(id) {
|
|
135
|
+
return markRetainedContinuationLane(this.core, id);
|
|
136
|
+
}
|
|
137
|
+
reviveBackgroundAgent(id, access, abort) {
|
|
138
|
+
return reviveBackgroundAgentLane(this.core, id, access, abort);
|
|
139
|
+
}
|
|
140
|
+
settleRevivedAgent(id, cycle, outcome) {
|
|
141
|
+
return settleRevivedAgentLane(this.core, id, cycle, outcome);
|
|
142
|
+
}
|
|
143
|
+
unmarkRetainedContinuation(id) {
|
|
144
|
+
return unmarkRetainedContinuationLane(this.core, id);
|
|
145
|
+
}
|
|
146
|
+
attachAgentNotify(id, notify, cycle) {
|
|
147
|
+
return attachAgentNotifyLane(this.core, id, notify, cycle);
|
|
148
|
+
}
|
|
149
|
+
async deliverToRunningAgent(id, access, notification, opts) {
|
|
150
|
+
return deliverToRunningAgentLane(this.core, id, access, notification, opts);
|
|
151
|
+
}
|
|
152
|
+
runningBackgroundAgentLabels(access) {
|
|
153
|
+
return runningBackgroundAgentLabelsLane(this.core, access);
|
|
154
|
+
}
|
|
155
|
+
async pollBackgroundAgent(handle, deadline, signal) {
|
|
156
|
+
return pollBackgroundAgentLane(handle, deadline, signal);
|
|
157
|
+
}
|
|
158
|
+
async stopBackgroundAgent(handle) {
|
|
159
|
+
return stopBackgroundAgentLane(this.core, handle);
|
|
160
|
+
}
|
|
161
|
+
writerId = randomBytes(8).toString("hex");
|
|
162
|
+
reapingHandles = new Set();
|
|
163
|
+
claimingHandles = new Set();
|
|
164
|
+
durableHeartbeatTimer;
|
|
525
165
|
mintTaskId(type) {
|
|
526
166
|
const prefix = TASK_PREFIX[type];
|
|
527
167
|
for (let i = 0; i < 32; i++) {
|
|
@@ -571,99 +211,6 @@ export class TaskRegistry {
|
|
|
571
211
|
}
|
|
572
212
|
return false;
|
|
573
213
|
}
|
|
574
|
-
registerBackgroundAgent(input) {
|
|
575
|
-
assertOwnership(input, "registerBackgroundAgent");
|
|
576
|
-
if (input.id !== undefined && !DURABLE_AGENT_HANDLE_RE.test(input.id)) {
|
|
577
|
-
throw new Error(`TaskRegistry: background-agent id ${JSON.stringify(input.id)} does not match the a<hex> handle form`);
|
|
578
|
-
}
|
|
579
|
-
const id = input.id ?? this.mintTaskId("background_agent");
|
|
580
|
-
if (input.durableSeed !== undefined) {
|
|
581
|
-
const prior = this.handles.get(id);
|
|
582
|
-
if (prior !== undefined) {
|
|
583
|
-
const parkedHandoff = prior.status === "parked" && input.initialStatus === "parked";
|
|
584
|
-
if (prior.type !== "background_agent" || prior.status === "running" || prior.status === "pending" || (prior.status === "parked" && !parkedHandoff)) {
|
|
585
|
-
throw new Error(`TaskRegistry: task id already registered: ${id}`);
|
|
586
|
-
}
|
|
587
|
-
const priorLane = prior.durable;
|
|
588
|
-
if (priorLane !== undefined)
|
|
589
|
-
priorLane.poisoned = true;
|
|
590
|
-
this.handles.delete(id);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
if (this.handles.has(id))
|
|
594
|
-
throw new Error(`TaskRegistry: task id already registered: ${id}`);
|
|
595
|
-
const now = input.now ?? Date.now();
|
|
596
|
-
this.handles.set(id, {
|
|
597
|
-
id,
|
|
598
|
-
type: "background_agent",
|
|
599
|
-
status: input.initialStatus ?? "running",
|
|
600
|
-
...(input.parkedCheckpointToken !== undefined ? { parkedCheckpointToken: input.parkedCheckpointToken } : {}),
|
|
601
|
-
...(input.resolveParkedStop !== undefined ? { resolveParkedStop: input.resolveParkedStop } : {}),
|
|
602
|
-
...(input.sessionScoped ? { sessionScoped: true } : {}),
|
|
603
|
-
...(input.description !== undefined ? { description: input.description } : {}),
|
|
604
|
-
...(input.name !== undefined ? { name: input.name } : {}),
|
|
605
|
-
...(input.agentType !== undefined ? { agentType: input.agentType } : {}),
|
|
606
|
-
...(input.parentTaskId !== undefined ? { parentTaskId: input.parentTaskId } : {}),
|
|
607
|
-
...(input.parentSessionId !== undefined ? { parentSessionId: input.parentSessionId } : {}),
|
|
608
|
-
...(input.rootSessionId !== undefined ? { rootSessionId: input.rootSessionId } : {}),
|
|
609
|
-
...(input.deliveryChannel !== undefined ? { channelState: input.deliveryChannel } : {}),
|
|
610
|
-
...(input.retainedContinuation !== undefined ? { retainedContinuation: input.retainedContinuation } : {}),
|
|
611
|
-
...(input.owner !== undefined ? { owner: input.owner } : {}),
|
|
612
|
-
...(input.scope !== undefined ? { scope: input.scope } : {}),
|
|
613
|
-
...(input.toolUseId !== undefined ? { toolUseId: input.toolUseId } : {}),
|
|
614
|
-
cycleSeq: input.durableSeed !== undefined ? (input.durableSeed.record.seq ?? 1) : 1,
|
|
615
|
-
createdAt: now,
|
|
616
|
-
updatedAt: now,
|
|
617
|
-
abort: input.abort,
|
|
618
|
-
});
|
|
619
|
-
if (input.store) {
|
|
620
|
-
const handle = this.handles.get(id);
|
|
621
|
-
if (input.durableSeed !== undefined) {
|
|
622
|
-
const seeded = structuredClone(input.durableSeed.record);
|
|
623
|
-
seeded.rev = input.durableSeed.rev;
|
|
624
|
-
handle.durable = {
|
|
625
|
-
store: input.store,
|
|
626
|
-
record: seeded,
|
|
627
|
-
chain: Promise.resolve(),
|
|
628
|
-
written: true,
|
|
629
|
-
poisoned: false,
|
|
630
|
-
flushFailed: false,
|
|
631
|
-
};
|
|
632
|
-
this.ensureDurableHeartbeat();
|
|
633
|
-
return id;
|
|
634
|
-
}
|
|
635
|
-
handle.durable = {
|
|
636
|
-
store: input.store,
|
|
637
|
-
record: {
|
|
638
|
-
handle: id,
|
|
639
|
-
scope: input.scope,
|
|
640
|
-
owner: input.owner,
|
|
641
|
-
sessionScoped: input.sessionScoped === true,
|
|
642
|
-
...(input.parentSessionId !== undefined ? { parentSessionId: input.parentSessionId } : {}),
|
|
643
|
-
...(input.rootSessionId !== undefined ? { rootSessionId: input.rootSessionId } : {}),
|
|
644
|
-
...(input.parentTaskId !== undefined ? { parentTaskId: input.parentTaskId } : {}),
|
|
645
|
-
...(input.name !== undefined ? { name: input.name } : {}),
|
|
646
|
-
...(input.agentType !== undefined ? { agentType: input.agentType } : {}),
|
|
647
|
-
...(input.description !== undefined ? { description: input.description } : {}),
|
|
648
|
-
...(input.model !== undefined ? { model: input.model } : {}),
|
|
649
|
-
...(input.teamName !== undefined ? { teamName: input.teamName } : {}),
|
|
650
|
-
writerId: this.writerId,
|
|
651
|
-
writerEpoch: 1,
|
|
652
|
-
spawnedAt: now,
|
|
653
|
-
updatedAt: now,
|
|
654
|
-
status: "running",
|
|
655
|
-
rev: 0,
|
|
656
|
-
},
|
|
657
|
-
chain: Promise.resolve(),
|
|
658
|
-
written: false,
|
|
659
|
-
poisoned: false,
|
|
660
|
-
flushFailed: false,
|
|
661
|
-
};
|
|
662
|
-
this.durableAgentWrite(handle, {});
|
|
663
|
-
this.ensureDurableHeartbeat();
|
|
664
|
-
}
|
|
665
|
-
return id;
|
|
666
|
-
}
|
|
667
214
|
getAccessibleTask(taskId, access) {
|
|
668
215
|
const h = this.handles.get(taskId);
|
|
669
216
|
if (!h || !canAccess(h, access))
|
|
@@ -762,388 +309,8 @@ export class TaskRegistry {
|
|
|
762
309
|
return undefined;
|
|
763
310
|
return handle.stoppedBy ?? handle.stopSource ?? "system";
|
|
764
311
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
if (!handle || handle.type !== "background_agent")
|
|
768
|
-
return undefined;
|
|
769
|
-
if (handle.status !== "running")
|
|
770
|
-
return undefined;
|
|
771
|
-
handle.status = "parked";
|
|
772
|
-
handle.parkedCheckpointToken = park.checkpointToken;
|
|
773
|
-
handle.resolveParkedStop = park.resolveStop;
|
|
774
|
-
handle.notify = undefined;
|
|
775
|
-
for (const [, , qResolve] of handle.preAttachQueue ?? [])
|
|
776
|
-
qResolve?.({ ok: false, reason: "not_running" });
|
|
777
|
-
handle.preAttachQueue = undefined;
|
|
778
|
-
for (const r of [...(handle.inFlightDirect ?? [])])
|
|
779
|
-
r({ ok: false, reason: "not_running" });
|
|
780
|
-
handle.inFlightDirect = undefined;
|
|
781
|
-
handle.updatedAt = Date.now();
|
|
782
|
-
if (park.seq !== undefined)
|
|
783
|
-
handle.cycleSeq = park.seq;
|
|
784
|
-
this.pokeBgQuiescence(handle.owner);
|
|
785
|
-
this.durableAgentWrite(handle, {
|
|
786
|
-
status: "parked",
|
|
787
|
-
parkedCheckpointToken: park.checkpointToken,
|
|
788
|
-
parkedAt: Date.now(),
|
|
789
|
-
...(park.seq !== undefined ? { seq: park.seq } : {}),
|
|
790
|
-
}, ["parkClaimId"]);
|
|
791
|
-
return "parked";
|
|
792
|
-
}
|
|
793
|
-
async reconcileParkedAgents(stores, scope, now, opts) {
|
|
794
|
-
let liveFailed = 0;
|
|
795
|
-
for (const handle of this.handles.values()) {
|
|
796
|
-
if (handle.type !== "background_agent" || handle.status !== "parked")
|
|
797
|
-
continue;
|
|
798
|
-
if (handle.scope !== undefined && handle.scope !== scope)
|
|
799
|
-
continue;
|
|
800
|
-
const token = handle.parkedCheckpointToken;
|
|
801
|
-
let state;
|
|
802
|
-
if (token === undefined)
|
|
803
|
-
state = "missing";
|
|
804
|
-
else {
|
|
805
|
-
try {
|
|
806
|
-
const cp = await stores.checkpointStore.get(token);
|
|
807
|
-
state = cp === null ? "missing" : cp.status;
|
|
808
|
-
}
|
|
809
|
-
catch {
|
|
810
|
-
state = "unreadable";
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
if (state !== "expired" && state !== "missing")
|
|
814
|
-
continue;
|
|
815
|
-
if (handle.status !== "parked")
|
|
816
|
-
continue;
|
|
817
|
-
handle.status = "failed";
|
|
818
|
-
handle.error = `approval checkpoint ${state} while parked (reconciled)`;
|
|
819
|
-
handle.parkedCheckpointToken = undefined;
|
|
820
|
-
handle.resolveParkedStop = undefined;
|
|
821
|
-
handle.updatedAt = now;
|
|
822
|
-
this.pokeBgQuiescence(handle.owner);
|
|
823
|
-
this.durableAgentWrite(handle, { status: "failed", error: handle.error, settledAt: now, stoppedBy: "system" }, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
|
|
824
|
-
liveFailed++;
|
|
825
|
-
}
|
|
826
|
-
const { reconcileParkedAgents: storeReconcile } = await import("./background-agent-store.js");
|
|
827
|
-
const liveHandles = new Set();
|
|
828
|
-
for (const h of this.handles.values())
|
|
829
|
-
if (h.type === "background_agent")
|
|
830
|
-
liveHandles.add(h.id);
|
|
831
|
-
const storeHalf = await storeReconcile(stores, scope, now, {
|
|
832
|
-
...(opts?.staleClaimMaxAgeMs !== undefined ? { staleClaimMaxAgeMs: opts.staleClaimMaxAgeMs } : {}),
|
|
833
|
-
excludeHandles: liveHandles,
|
|
834
|
-
});
|
|
835
|
-
return { failed: storeHalf.failed + liveFailed, rolledBack: storeHalf.rolledBack };
|
|
836
|
-
}
|
|
837
|
-
async claimParkedAgent(stores, handle, access) {
|
|
838
|
-
if (access.scope === undefined)
|
|
839
|
-
return { ok: false, reason: "not_found" };
|
|
840
|
-
let row;
|
|
841
|
-
try {
|
|
842
|
-
row = await stores.agentStore.get(handle, access.scope);
|
|
843
|
-
}
|
|
844
|
-
catch {
|
|
845
|
-
return { ok: false, reason: "store_unreachable" };
|
|
846
|
-
}
|
|
847
|
-
if (!row || !canAccessAgentRecord(row, access))
|
|
848
|
-
return { ok: false, reason: "not_found" };
|
|
849
|
-
if (row.status !== "parked")
|
|
850
|
-
return { ok: false, reason: "not_parked" };
|
|
851
|
-
const token = row.parkedCheckpointToken;
|
|
852
|
-
if (token === undefined || row.sessionId === undefined)
|
|
853
|
-
return { ok: false, reason: "binding_broken" };
|
|
854
|
-
let cp;
|
|
855
|
-
try {
|
|
856
|
-
cp = (await stores.checkpointStore.get(token));
|
|
857
|
-
}
|
|
858
|
-
catch {
|
|
859
|
-
return { ok: false, reason: "store_unreachable" };
|
|
860
|
-
}
|
|
861
|
-
if (cp === null || cp.sessionId !== row.sessionId)
|
|
862
|
-
return { ok: false, reason: "binding_broken" };
|
|
863
|
-
if (cp.status !== "pending")
|
|
864
|
-
return { ok: false, reason: "checkpoint_not_pending" };
|
|
865
|
-
const claimId = randomBytes(8).toString("hex");
|
|
866
|
-
const reserved = structuredClone(row);
|
|
867
|
-
reserved.parkClaimId = claimId;
|
|
868
|
-
reserved.updatedAt = Date.now();
|
|
869
|
-
let won;
|
|
870
|
-
try {
|
|
871
|
-
won = await stores.agentStore.updateIf(handle, access.scope, reserved, {
|
|
872
|
-
rev: row.rev,
|
|
873
|
-
status: "parked",
|
|
874
|
-
parkedCheckpointToken: token,
|
|
875
|
-
parkClaimId: null,
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
catch {
|
|
879
|
-
return { ok: false, reason: "store_unreachable" };
|
|
880
|
-
}
|
|
881
|
-
if (!won)
|
|
882
|
-
return { ok: false, reason: "claim_lost" };
|
|
883
|
-
return {
|
|
884
|
-
ok: true,
|
|
885
|
-
ticket: { handle, scope: access.scope, token, claimId, reservedRev: row.rev + 1, row: structuredClone(row) },
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
|
-
async rollbackParkedClaim(stores, ticket) {
|
|
889
|
-
let cpStatus;
|
|
890
|
-
try {
|
|
891
|
-
const cp = (await stores.checkpointStore.get(ticket.token));
|
|
892
|
-
cpStatus = cp === null ? "missing" : cp.status;
|
|
893
|
-
}
|
|
894
|
-
catch {
|
|
895
|
-
return "retry";
|
|
896
|
-
}
|
|
897
|
-
const now = Date.now();
|
|
898
|
-
const settleLocalParkedHandle = (error) => {
|
|
899
|
-
const h = this.handles.get(ticket.handle);
|
|
900
|
-
if (h !== undefined && h.type === "background_agent" && h.status === "parked") {
|
|
901
|
-
h.status = "failed";
|
|
902
|
-
h.error = error;
|
|
903
|
-
h.parkedCheckpointToken = undefined;
|
|
904
|
-
h.resolveParkedStop = undefined;
|
|
905
|
-
h.updatedAt = now;
|
|
906
|
-
this.pokeBgQuiescence(h.owner);
|
|
907
|
-
}
|
|
908
|
-
};
|
|
909
|
-
let liveRow;
|
|
910
|
-
try {
|
|
911
|
-
liveRow = await stores.agentStore.get(ticket.handle, ticket.scope);
|
|
912
|
-
}
|
|
913
|
-
catch {
|
|
914
|
-
return "retry";
|
|
915
|
-
}
|
|
916
|
-
if (!liveRow || liveRow.parkClaimId !== ticket.claimId)
|
|
917
|
-
return "lost";
|
|
918
|
-
if (cpStatus === "pending") {
|
|
919
|
-
const restored = structuredClone(liveRow);
|
|
920
|
-
delete restored.parkClaimId;
|
|
921
|
-
restored.updatedAt = now;
|
|
922
|
-
restored.writerEpoch = (liveRow.writerEpoch ?? 0) + 1;
|
|
923
|
-
const liveHandle = this.handles.get(ticket.handle);
|
|
924
|
-
if (liveHandle?.type === "background_agent" &&
|
|
925
|
-
liveHandle.durable !== undefined &&
|
|
926
|
-
liveHandle.durable.record.writerId === liveRow.writerId &&
|
|
927
|
-
(liveHandle.durable.record.writerEpoch ?? 0) === (liveRow.writerEpoch ?? 0)) {
|
|
928
|
-
liveHandle.durable.record.writerEpoch = restored.writerEpoch;
|
|
929
|
-
}
|
|
930
|
-
try {
|
|
931
|
-
const won = await stores.agentStore.updateIf(ticket.handle, ticket.scope, restored, {
|
|
932
|
-
rev: liveRow.rev,
|
|
933
|
-
status: "parked",
|
|
934
|
-
parkedCheckpointToken: ticket.token,
|
|
935
|
-
parkClaimId: ticket.claimId,
|
|
936
|
-
});
|
|
937
|
-
if (!won)
|
|
938
|
-
return "lost";
|
|
939
|
-
try {
|
|
940
|
-
const cp2 = (await stores.checkpointStore.get(ticket.token));
|
|
941
|
-
if (cp2 !== null && cp2.status === "pending")
|
|
942
|
-
return "rolled_back";
|
|
943
|
-
const post = await stores.agentStore.get(ticket.handle, ticket.scope);
|
|
944
|
-
if (post && post.status === "parked") {
|
|
945
|
-
const term = structuredClone(post);
|
|
946
|
-
term.status = "failed";
|
|
947
|
-
term.error = `approval checkpoint ${cp2 === null ? "missing" : cp2.status} under the resume claim (claim rollback)`;
|
|
948
|
-
term.stoppedBy = "system";
|
|
949
|
-
term.settledAt = now;
|
|
950
|
-
term.updatedAt = now;
|
|
951
|
-
delete term.parkedCheckpointToken;
|
|
952
|
-
delete term.parkClaimId;
|
|
953
|
-
delete term.parkedAt;
|
|
954
|
-
if (await stores.agentStore.updateIf(ticket.handle, ticket.scope, term, { rev: post.rev, status: "parked" })) {
|
|
955
|
-
settleLocalParkedHandle(term.error);
|
|
956
|
-
return "failed";
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
return "lost";
|
|
960
|
-
}
|
|
961
|
-
catch {
|
|
962
|
-
return "rolled_back";
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
catch {
|
|
966
|
-
return "retry";
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
const error = cpStatus === "resolved"
|
|
970
|
-
? "approval consumed but the resume could not proceed — outcome unknown (claim rollback)"
|
|
971
|
-
: `approval checkpoint ${cpStatus} under the resume claim (claim rollback)`;
|
|
972
|
-
if (liveRow.status !== "parked")
|
|
973
|
-
return "lost";
|
|
974
|
-
const failed = structuredClone(liveRow);
|
|
975
|
-
failed.status = "failed";
|
|
976
|
-
failed.error = error;
|
|
977
|
-
failed.stoppedBy = "system";
|
|
978
|
-
failed.settledAt = now;
|
|
979
|
-
failed.updatedAt = now;
|
|
980
|
-
delete failed.parkedCheckpointToken;
|
|
981
|
-
delete failed.parkClaimId;
|
|
982
|
-
delete failed.parkedAt;
|
|
983
|
-
try {
|
|
984
|
-
const won = await stores.agentStore.updateIf(ticket.handle, ticket.scope, failed, {
|
|
985
|
-
rev: liveRow.rev,
|
|
986
|
-
status: "parked",
|
|
987
|
-
parkClaimId: ticket.claimId,
|
|
988
|
-
});
|
|
989
|
-
if (won)
|
|
990
|
-
settleLocalParkedHandle(error);
|
|
991
|
-
return won ? "failed" : "lost";
|
|
992
|
-
}
|
|
993
|
-
catch {
|
|
994
|
-
return "retry";
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
async consumeParkedFlip(id, stores, ticket) {
|
|
998
|
-
const handle = this.handles.get(id);
|
|
999
|
-
if (!handle || handle.type !== "background_agent" || handle.status !== "parked")
|
|
1000
|
-
return false;
|
|
1001
|
-
const lane = handle.durable;
|
|
1002
|
-
const doFlip = async () => {
|
|
1003
|
-
let live;
|
|
1004
|
-
try {
|
|
1005
|
-
live = await stores.agentStore.get(ticket.handle, ticket.scope);
|
|
1006
|
-
}
|
|
1007
|
-
catch {
|
|
1008
|
-
return undefined;
|
|
1009
|
-
}
|
|
1010
|
-
if (!live || live.status !== "parked" || live.parkClaimId !== ticket.claimId || live.parkedCheckpointToken !== ticket.token)
|
|
1011
|
-
return undefined;
|
|
1012
|
-
const flipped = structuredClone(live);
|
|
1013
|
-
flipped.status = "running";
|
|
1014
|
-
flipped.writerId = this.writerId;
|
|
1015
|
-
flipped.writerEpoch = (live.writerEpoch ?? 0) + 1;
|
|
1016
|
-
flipped.updatedAt = Date.now();
|
|
1017
|
-
flipped.seq = (live.seq ?? 1) + 1;
|
|
1018
|
-
delete flipped.parkedCheckpointToken;
|
|
1019
|
-
delete flipped.parkedAt;
|
|
1020
|
-
try {
|
|
1021
|
-
const won = await stores.agentStore.updateIf(ticket.handle, ticket.scope, flipped, {
|
|
1022
|
-
rev: live.rev,
|
|
1023
|
-
status: "parked",
|
|
1024
|
-
parkedCheckpointToken: ticket.token,
|
|
1025
|
-
parkClaimId: ticket.claimId,
|
|
1026
|
-
});
|
|
1027
|
-
if (!won)
|
|
1028
|
-
return undefined;
|
|
1029
|
-
}
|
|
1030
|
-
catch {
|
|
1031
|
-
return undefined;
|
|
1032
|
-
}
|
|
1033
|
-
flipped.rev = live.rev + 1;
|
|
1034
|
-
return flipped;
|
|
1035
|
-
};
|
|
1036
|
-
let flipped;
|
|
1037
|
-
if (lane !== undefined && !lane.poisoned) {
|
|
1038
|
-
const p = lane.chain.then(doFlip, doFlip);
|
|
1039
|
-
lane.chain = p.then((f) => {
|
|
1040
|
-
if (f !== undefined) {
|
|
1041
|
-
lane.record = structuredClone(f);
|
|
1042
|
-
lane.written = true;
|
|
1043
|
-
}
|
|
1044
|
-
});
|
|
1045
|
-
flipped = await p;
|
|
1046
|
-
}
|
|
1047
|
-
else {
|
|
1048
|
-
flipped = await doFlip();
|
|
1049
|
-
}
|
|
1050
|
-
if (flipped === undefined)
|
|
1051
|
-
return false;
|
|
1052
|
-
handle.status = "running";
|
|
1053
|
-
handle.parkedCheckpointToken = undefined;
|
|
1054
|
-
handle.resolveParkedStop = undefined;
|
|
1055
|
-
handle.updatedAt = Date.now();
|
|
1056
|
-
if (flipped.seq !== undefined)
|
|
1057
|
-
handle.cycleSeq = flipped.seq;
|
|
1058
|
-
return true;
|
|
1059
|
-
}
|
|
1060
|
-
finalizeParkedResume(id) {
|
|
1061
|
-
const handle = this.handles.get(id);
|
|
1062
|
-
if (!handle || handle.type !== "background_agent")
|
|
1063
|
-
return;
|
|
1064
|
-
this.durableAgentWrite(handle, {}, ["parkClaimId"]);
|
|
1065
|
-
}
|
|
1066
|
-
settleBackgroundAgent(id, outcome) {
|
|
1067
|
-
const handle = this.handles.get(id);
|
|
1068
|
-
if (!handle || handle.type !== "background_agent")
|
|
1069
|
-
return undefined;
|
|
1070
|
-
if (handle.status !== "running") {
|
|
1071
|
-
if (handle.status === "killed") {
|
|
1072
|
-
let backfilled = false;
|
|
1073
|
-
if (handle.result === undefined && outcome.result !== undefined && outcome.result !== "") {
|
|
1074
|
-
handle.result = outcome.result;
|
|
1075
|
-
handle.resultIsPartial = true;
|
|
1076
|
-
backfilled = true;
|
|
1077
|
-
if (outcome.resultFull !== undefined)
|
|
1078
|
-
handle.resultFull = outcome.resultFull;
|
|
1079
|
-
}
|
|
1080
|
-
if (handle.error === undefined && outcome.error !== undefined && outcome.error !== BG_AGENT_REAP_STOP_ERROR) {
|
|
1081
|
-
handle.error = outcome.error;
|
|
1082
|
-
backfilled = true;
|
|
1083
|
-
}
|
|
1084
|
-
if (backfilled) {
|
|
1085
|
-
handle.updatedAt = Date.now();
|
|
1086
|
-
this.durableAgentWrite(handle, {
|
|
1087
|
-
...(handle.result !== undefined ? { finalOutput: handle.result } : {}),
|
|
1088
|
-
...(handle.resultFull !== undefined ? { finalOutputFull: handle.resultFull } : {}),
|
|
1089
|
-
...(handle.resultIsPartial ? { resultIsPartial: true } : {}),
|
|
1090
|
-
...(handle.error !== undefined ? { error: handle.error } : {}),
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
return handle.status === "completed" || handle.status === "failed" || handle.status === "killed" ? handle.status : undefined;
|
|
1095
|
-
}
|
|
1096
|
-
handle.status = outcome.status;
|
|
1097
|
-
handle.notify = undefined;
|
|
1098
|
-
for (const [, , qResolve] of handle.preAttachQueue ?? [])
|
|
1099
|
-
qResolve?.({ ok: false, reason: "not_running" });
|
|
1100
|
-
handle.preAttachQueue = undefined;
|
|
1101
|
-
for (const r of [...(handle.inFlightDirect ?? [])])
|
|
1102
|
-
r({ ok: false, reason: "not_running" });
|
|
1103
|
-
handle.inFlightDirect = undefined;
|
|
1104
|
-
if (outcome.status === "killed" && handle.stoppedBy === undefined) {
|
|
1105
|
-
handle.stoppedBy = outcome.stoppedBy ?? handle.stopSource ?? "system";
|
|
1106
|
-
}
|
|
1107
|
-
if (outcome.result !== undefined)
|
|
1108
|
-
handle.result = outcome.result;
|
|
1109
|
-
if (outcome.resultFull !== undefined)
|
|
1110
|
-
handle.resultFull = outcome.resultFull;
|
|
1111
|
-
if (outcome.status === "killed" && outcome.result !== undefined && outcome.result !== "") {
|
|
1112
|
-
handle.resultIsPartial = true;
|
|
1113
|
-
}
|
|
1114
|
-
if (outcome.error !== undefined && !(outcome.status === "killed" && outcome.error === BG_AGENT_REAP_STOP_ERROR)) {
|
|
1115
|
-
handle.error = outcome.error;
|
|
1116
|
-
}
|
|
1117
|
-
handle.updatedAt = Date.now();
|
|
1118
|
-
if (outcome.seq !== undefined)
|
|
1119
|
-
handle.cycleSeq = outcome.seq;
|
|
1120
|
-
this.pokeBgQuiescence(handle.owner);
|
|
1121
|
-
this.durableAgentWrite(handle, {
|
|
1122
|
-
status: outcome.status,
|
|
1123
|
-
...(outcome.seq !== undefined ? { seq: outcome.seq } : {}),
|
|
1124
|
-
settledAt: Date.now(),
|
|
1125
|
-
...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
|
|
1126
|
-
...(handle.result !== undefined ? { finalOutput: handle.result } : {}),
|
|
1127
|
-
...(handle.resultFull !== undefined ? { finalOutputFull: handle.resultFull } : {}),
|
|
1128
|
-
...(handle.resultIsPartial ? { resultIsPartial: true } : {}),
|
|
1129
|
-
...(handle.error !== undefined ? { error: handle.error } : {}),
|
|
1130
|
-
}, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
|
|
1131
|
-
return outcome.status;
|
|
1132
|
-
}
|
|
1133
|
-
abortBackgroundAgentsForOwner(access, opts) {
|
|
1134
|
-
let aborted = 0;
|
|
1135
|
-
for (const handle of this.handles.values()) {
|
|
1136
|
-
if (handle.type !== "background_agent" || handle.status !== "running" || !canAccess(handle, access))
|
|
1137
|
-
continue;
|
|
1138
|
-
if (opts?.skipSessionScoped && handle.sessionScoped)
|
|
1139
|
-
continue;
|
|
1140
|
-
if (opts?.sessionScopedOnly && !handle.sessionScoped)
|
|
1141
|
-
continue;
|
|
1142
|
-
this.markStopSource(handle.id, "parent");
|
|
1143
|
-
handle.abort.abort();
|
|
1144
|
-
aborted++;
|
|
1145
|
-
}
|
|
1146
|
-
return aborted;
|
|
312
|
+
getCompletionId(id) {
|
|
313
|
+
return this.handles.get(id)?.completionId;
|
|
1147
314
|
}
|
|
1148
315
|
markStopSourceForOwner(access, source, opts) {
|
|
1149
316
|
let marked = 0;
|
|
@@ -1195,6 +362,7 @@ export class TaskRegistry {
|
|
|
1195
362
|
handle.status = "killed";
|
|
1196
363
|
handle.stoppedBy = handle.stopSource ?? source;
|
|
1197
364
|
handle.updatedAt = Date.now();
|
|
365
|
+
mintCompletionId(handle);
|
|
1198
366
|
if (handle.outputFile !== undefined) {
|
|
1199
367
|
try {
|
|
1200
368
|
const drained = await handle.env.pollBackground(handle.shellId);
|
|
@@ -1221,6 +389,7 @@ export class TaskRegistry {
|
|
|
1221
389
|
stoppedBy: handle.stoppedBy,
|
|
1222
390
|
summary: `${(handle.description ?? "background command").slice(0, 200)} — killed before completion (${clause(handle.stoppedBy)})${killFailed ? ` — kill attempt reported ${kill.ok ? "" : kill.error.code}: the process may still be terminating; the environment teardown retries` : ""}${bashMirrorGapNote(handle)}`,
|
|
1223
391
|
...(resultText.length > 0 ? { result: resultText, partial: true } : {}),
|
|
392
|
+
...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
|
|
1224
393
|
}));
|
|
1225
394
|
}
|
|
1226
395
|
else {
|
|
@@ -1235,6 +404,7 @@ export class TaskRegistry {
|
|
|
1235
404
|
handle.status = "killed";
|
|
1236
405
|
handle.stoppedBy = handle.stopSource ?? source;
|
|
1237
406
|
handle.updatedAt = Date.now();
|
|
407
|
+
mintCompletionId(handle);
|
|
1238
408
|
const lastLines = handle.lineBuf !== "" ? [handle.lineBuf] : [];
|
|
1239
409
|
handle.lineBuf = "";
|
|
1240
410
|
this.notifyTerminalOnce(handle, () => handle.onEvent?.({
|
|
@@ -1245,6 +415,7 @@ export class TaskRegistry {
|
|
|
1245
415
|
stoppedBy: handle.stoppedBy,
|
|
1246
416
|
summary: `${(handle.description ?? "monitor").slice(0, 200)} — killed before completion (${clause(handle.stoppedBy)}); watch ended.${killFailed ? ` Kill attempt reported ${kill.ok ? "" : kill.error.code}: the process may still be terminating; the environment teardown retries.` : ""}`,
|
|
1247
417
|
...(lastLines.length > 0 ? { lines: lastLines, result: lastLines.join("\n"), partial: true } : {}),
|
|
418
|
+
...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
|
|
1248
419
|
}));
|
|
1249
420
|
}
|
|
1250
421
|
settled++;
|
|
@@ -1270,6 +441,7 @@ export class TaskRegistry {
|
|
|
1270
441
|
handle.status = "killed";
|
|
1271
442
|
handle.stoppedBy = handle.stopSource ?? "system";
|
|
1272
443
|
handle.updatedAt = Date.now();
|
|
444
|
+
mintCompletionId(handle);
|
|
1273
445
|
reaped++;
|
|
1274
446
|
continue;
|
|
1275
447
|
}
|
|
@@ -1286,6 +458,7 @@ export class TaskRegistry {
|
|
|
1286
458
|
handle.status = "killed";
|
|
1287
459
|
handle.stoppedBy = handle.stopSource ?? "system";
|
|
1288
460
|
handle.updatedAt = Date.now();
|
|
461
|
+
mintCompletionId(handle);
|
|
1289
462
|
reaped++;
|
|
1290
463
|
continue;
|
|
1291
464
|
}
|
|
@@ -1442,6 +615,7 @@ export class TaskRegistry {
|
|
|
1442
615
|
return;
|
|
1443
616
|
handle.status = "failed";
|
|
1444
617
|
handle.updatedAt = Date.now();
|
|
618
|
+
mintCompletionId(handle);
|
|
1445
619
|
stop();
|
|
1446
620
|
const lostResult = bashTerminalResult(handle.spool);
|
|
1447
621
|
this.notifyTerminalOnce(handle, () => onTerminal({
|
|
@@ -1452,11 +626,13 @@ export class TaskRegistry {
|
|
|
1452
626
|
status: "failed",
|
|
1453
627
|
summary: `${(handle.description ?? "background command").slice(0, 200)} — lost its background process (${r.error.message}); no further output will arrive.${bashMirrorGapNote(handle)}`,
|
|
1454
628
|
...(lostResult.length > 0 ? { result: lostResult } : {}),
|
|
629
|
+
...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
|
|
1455
630
|
}));
|
|
1456
631
|
return;
|
|
1457
632
|
}
|
|
1458
633
|
pollFailures = 0;
|
|
1459
634
|
const spool = handle.spool;
|
|
635
|
+
accountDroppedBytes(spool, r.value);
|
|
1460
636
|
spool.stdout = roll(spool.stdout + r.value.stdout);
|
|
1461
637
|
spool.stderr = roll(spool.stderr + r.value.stderr);
|
|
1462
638
|
if (handle.outputFile !== undefined && (r.value.stdout.length > 0 || r.value.stderr.length > 0)) {
|
|
@@ -1470,6 +646,7 @@ export class TaskRegistry {
|
|
|
1470
646
|
}
|
|
1471
647
|
handle.updatedAt = Date.now();
|
|
1472
648
|
if (r.value.status !== "running") {
|
|
649
|
+
mintCompletionId(handle);
|
|
1473
650
|
spool.exitCode = r.value.exitCode;
|
|
1474
651
|
if (r.value.timedOut === true) {
|
|
1475
652
|
spool.timedOut = true;
|
|
@@ -1496,9 +673,12 @@ export class TaskRegistry {
|
|
|
1496
673
|
? r.value.timeoutSec !== undefined
|
|
1497
674
|
? `${desc} — timed out after ${r.value.timeoutSec}s (its own background time budget); the process was killed`
|
|
1498
675
|
: `${desc} — timed out (its own background time budget); the process was killed`
|
|
1499
|
-
: `${desc} — ${r.value.status}`) +
|
|
676
|
+
: `${desc} — ${r.value.status}`) +
|
|
677
|
+
bashMirrorGapNote(handle) +
|
|
678
|
+
droppedGapNote(spool),
|
|
1500
679
|
...(resultText.length > 0 ? { result: resultText } : {}),
|
|
1501
680
|
...(handle.status === "killed" && resultText.length > 0 ? { partial: true } : {}),
|
|
681
|
+
...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
|
|
1502
682
|
}));
|
|
1503
683
|
}
|
|
1504
684
|
}
|
|
@@ -1509,6 +689,7 @@ export class TaskRegistry {
|
|
|
1509
689
|
return;
|
|
1510
690
|
handle.status = "failed";
|
|
1511
691
|
handle.updatedAt = Date.now();
|
|
692
|
+
mintCompletionId(handle);
|
|
1512
693
|
stop();
|
|
1513
694
|
const crashedResult = bashTerminalResult(handle.spool);
|
|
1514
695
|
this.notifyTerminalOnce(handle, () => onTerminal({
|
|
@@ -1519,6 +700,7 @@ export class TaskRegistry {
|
|
|
1519
700
|
status: "failed",
|
|
1520
701
|
summary: `${(handle.description ?? "background command").slice(0, 200)} — lost its background process (poll failed: ${e instanceof Error ? e.message : String(e)}); no further output will arrive.${bashMirrorGapNote(handle)}`,
|
|
1521
702
|
...(crashedResult.length > 0 ? { result: crashedResult } : {}),
|
|
703
|
+
...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
|
|
1522
704
|
}));
|
|
1523
705
|
}
|
|
1524
706
|
finally {
|
|
@@ -1555,229 +737,12 @@ export class TaskRegistry {
|
|
|
1555
737
|
}
|
|
1556
738
|
return out;
|
|
1557
739
|
}
|
|
1558
|
-
registerMonitor(input) {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
const timeoutMs = persistent
|
|
1565
|
-
? undefined
|
|
1566
|
-
: Math.min(MONITOR_MAX_TIMEOUT_MS, Math.max(1, Math.floor(input.timeoutMs ?? MONITOR_DEFAULT_TIMEOUT_MS)));
|
|
1567
|
-
const handle = {
|
|
1568
|
-
id,
|
|
1569
|
-
type: "monitor",
|
|
1570
|
-
status: "running",
|
|
1571
|
-
...(input.description !== undefined ? { description: input.description } : {}),
|
|
1572
|
-
...(input.owner !== undefined ? { owner: input.owner } : {}),
|
|
1573
|
-
...(input.scope !== undefined ? { scope: input.scope } : {}),
|
|
1574
|
-
...(input.toolUseId !== undefined ? { toolUseId: input.toolUseId } : {}),
|
|
1575
|
-
...(persistent ? { persistent: true } : {}),
|
|
1576
|
-
...(input.sessionScoped ? { sessionScoped: true } : {}),
|
|
1577
|
-
createdAt: now,
|
|
1578
|
-
updatedAt: now,
|
|
1579
|
-
env: input.env,
|
|
1580
|
-
shellId: input.shellId,
|
|
1581
|
-
spool: { stdout: "", stderr: "", rolledChars: 0 },
|
|
1582
|
-
lineBuf: "",
|
|
1583
|
-
seq: 0,
|
|
1584
|
-
batchTimes: [],
|
|
1585
|
-
batchWindowMs: Math.max(1, Math.floor(input.batchWindowMs ?? MONITOR_BATCH_WINDOW_MS)),
|
|
1586
|
-
maxBatchesPerMinute: Math.max(1, Math.floor(input.maxBatchesPerMinute ?? MONITOR_MAX_BATCHES_PER_MINUTE)),
|
|
1587
|
-
...(timeoutMs !== undefined ? { timeoutMs, deadlineAt: timers.now() + timeoutMs } : {}),
|
|
1588
|
-
timers,
|
|
1589
|
-
...(input.onEvent !== undefined ? { onEvent: input.onEvent } : {}),
|
|
1590
|
-
};
|
|
1591
|
-
this.handles.set(id, handle);
|
|
1592
|
-
this.startMonitorWatcher(id);
|
|
1593
|
-
return id;
|
|
1594
|
-
}
|
|
1595
|
-
startMonitorWatcher(id) {
|
|
1596
|
-
const handle = this.handles.get(id);
|
|
1597
|
-
if (!handle || handle.type !== "monitor")
|
|
1598
|
-
return;
|
|
1599
|
-
const timers = handle.timers;
|
|
1600
|
-
const ROLL_CAP = 2 * TASK_OUTPUT_MAX_CHARS;
|
|
1601
|
-
const roll = (s) => {
|
|
1602
|
-
if (s.length <= ROLL_CAP)
|
|
1603
|
-
return s;
|
|
1604
|
-
const half = Math.floor(ROLL_CAP / 2);
|
|
1605
|
-
handle.spool.rolledChars += s.length - 2 * half;
|
|
1606
|
-
return s.slice(0, half) + s.slice(s.length - half);
|
|
1607
|
-
};
|
|
1608
|
-
let ticking = false;
|
|
1609
|
-
const MAX_RETRYABLE_POLL_FAILURES = 3;
|
|
1610
|
-
let pollFailures = 0;
|
|
1611
|
-
const stop = () => {
|
|
1612
|
-
if (handle.watcher !== undefined) {
|
|
1613
|
-
timers.clearInterval(handle.watcher);
|
|
1614
|
-
handle.watcher = undefined;
|
|
1615
|
-
}
|
|
1616
|
-
};
|
|
1617
|
-
const emit = (n) => {
|
|
1618
|
-
try {
|
|
1619
|
-
handle.onEvent?.(n);
|
|
1620
|
-
}
|
|
1621
|
-
catch {
|
|
1622
|
-
}
|
|
1623
|
-
};
|
|
1624
|
-
const clipEvent = (s) => s.length <= MONITOR_EVENT_RESULT_CAP ? s : `${s.slice(0, MONITOR_EVENT_RESULT_CAP)}\n…[+${s.length - MONITOR_EVENT_RESULT_CAP} chars truncated]`;
|
|
1625
|
-
const pendingFinalLines = (lines) => {
|
|
1626
|
-
const out = handle.lineBuf !== "" ? [...lines, handle.lineBuf] : lines;
|
|
1627
|
-
handle.lineBuf = "";
|
|
1628
|
-
return out;
|
|
1629
|
-
};
|
|
1630
|
-
const terminal = (status, summary, finalLines) => {
|
|
1631
|
-
handle.updatedAt = Date.now();
|
|
1632
|
-
stop();
|
|
1633
|
-
this.notifyTerminalOnce(handle, () => emit({
|
|
1634
|
-
task_id: id,
|
|
1635
|
-
task_type: "monitor",
|
|
1636
|
-
...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
|
|
1637
|
-
status,
|
|
1638
|
-
...(status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
|
|
1639
|
-
summary: `${(handle.description ?? "monitor").slice(0, 200)} — ${summary}`,
|
|
1640
|
-
...(finalLines.length > 0 ? { lines: finalLines, result: clipEvent(finalLines.join("\n")), ...(status === "killed" ? { partial: true } : {}) } : {}),
|
|
1641
|
-
}));
|
|
1642
|
-
};
|
|
1643
|
-
const tick = async () => {
|
|
1644
|
-
if (ticking)
|
|
1645
|
-
return;
|
|
1646
|
-
ticking = true;
|
|
1647
|
-
try {
|
|
1648
|
-
if (this.handles.get(id) !== handle)
|
|
1649
|
-
return stop();
|
|
1650
|
-
if (handle.status !== "running")
|
|
1651
|
-
return stop();
|
|
1652
|
-
const r = await handle.env.pollBackground(handle.shellId);
|
|
1653
|
-
if (this.handles.get(id) !== handle || handle.status !== "running")
|
|
1654
|
-
return stop();
|
|
1655
|
-
if (!r.ok) {
|
|
1656
|
-
if ((r.error.code === "timeout" || r.error.code === "io") && ++pollFailures <= MAX_RETRYABLE_POLL_FAILURES)
|
|
1657
|
-
return;
|
|
1658
|
-
handle.status = "failed";
|
|
1659
|
-
return terminal("failed", `monitor lost its process (${r.error.message}); watch ended.`, pendingFinalLines([]));
|
|
1660
|
-
}
|
|
1661
|
-
pollFailures = 0;
|
|
1662
|
-
const spool = handle.spool;
|
|
1663
|
-
spool.stdout = roll(spool.stdout + r.value.stdout);
|
|
1664
|
-
spool.stderr = roll(spool.stderr + r.value.stderr);
|
|
1665
|
-
handle.lineBuf += r.value.stdout;
|
|
1666
|
-
const parts = handle.lineBuf.split(/\r?\n/);
|
|
1667
|
-
handle.lineBuf = parts.pop() ?? "";
|
|
1668
|
-
const lines = parts.filter((l) => l !== "");
|
|
1669
|
-
handle.status = statusFromBackground(r.value.status, r.value.exitCode);
|
|
1670
|
-
if (r.value.status !== "running") {
|
|
1671
|
-
spool.exitCode = r.value.exitCode;
|
|
1672
|
-
if (handle.status === "killed" && handle.stoppedBy === undefined)
|
|
1673
|
-
handle.stoppedBy = handle.stopSource ?? "system";
|
|
1674
|
-
const label = r.value.status === "exited"
|
|
1675
|
-
? `exited(code ${r.value.exitCode}); watch ended.`
|
|
1676
|
-
: `${r.value.status}; watch ended.`;
|
|
1677
|
-
return terminal(handle.status === "completed" ? "completed" : handle.status === "killed" ? "killed" : "failed", label, pendingFinalLines(lines));
|
|
1678
|
-
}
|
|
1679
|
-
const now = timers.now();
|
|
1680
|
-
if (handle.deadlineAt !== undefined && now >= handle.deadlineAt) {
|
|
1681
|
-
this.markStopSource(id, "timeout");
|
|
1682
|
-
const k = await handle.env.killBackground(handle.shellId);
|
|
1683
|
-
if (this.handles.get(id) !== handle || handle.status !== "running")
|
|
1684
|
-
return stop();
|
|
1685
|
-
if (!k.ok && k.error.code !== "not_found") {
|
|
1686
|
-
handle.status = "failed";
|
|
1687
|
-
return terminal("failed", `timed out after ${handle.timeoutMs}ms, but the kill failed (${k.error.message}); the process may still be running. Watch ended.`, pendingFinalLines(lines));
|
|
1688
|
-
}
|
|
1689
|
-
handle.status = "killed";
|
|
1690
|
-
handle.stoppedBy = handle.stopSource ?? "timeout";
|
|
1691
|
-
return terminal("killed", `timed out after ${handle.timeoutMs}ms; the process was killed and the watch ended.`, pendingFinalLines(lines));
|
|
1692
|
-
}
|
|
1693
|
-
if (lines.length === 0) {
|
|
1694
|
-
if (r.value.stdout.length > 0 || r.value.stderr.length > 0)
|
|
1695
|
-
handle.updatedAt = Date.now();
|
|
1696
|
-
return;
|
|
1697
|
-
}
|
|
1698
|
-
const cutoff = now - 60_000;
|
|
1699
|
-
handle.batchTimes = handle.batchTimes.filter((t) => t > cutoff);
|
|
1700
|
-
handle.batchTimes.push(now);
|
|
1701
|
-
if (handle.batchTimes.length > handle.maxBatchesPerMinute) {
|
|
1702
|
-
this.markStopSource(id, "system");
|
|
1703
|
-
const k = await handle.env.killBackground(handle.shellId);
|
|
1704
|
-
if (this.handles.get(id) !== handle || handle.status !== "running")
|
|
1705
|
-
return stop();
|
|
1706
|
-
if (!k.ok && k.error.code !== "not_found") {
|
|
1707
|
-
handle.status = "failed";
|
|
1708
|
-
return terminal("failed", `auto-stop triggered (more than ${handle.maxBatchesPerMinute} notification batches within a minute), but the kill failed (${k.error.message}); the process may still be running. Watch ended.`, pendingFinalLines(lines));
|
|
1709
|
-
}
|
|
1710
|
-
handle.status = "killed";
|
|
1711
|
-
handle.stoppedBy = handle.stopSource ?? "system";
|
|
1712
|
-
return terminal("killed", `auto-stopped: it produced too many events (more than ${handle.maxBatchesPerMinute} notification batches within a minute). ` +
|
|
1713
|
-
`The process was killed; its full output remains readable via TaskOutput. Restart it with a quieter command (filter its output) if you still need the watch.`, pendingFinalLines(lines));
|
|
1714
|
-
}
|
|
1715
|
-
handle.seq += 1;
|
|
1716
|
-
handle.updatedAt = Date.now();
|
|
1717
|
-
emit({
|
|
1718
|
-
task_id: id,
|
|
1719
|
-
task_type: "monitor",
|
|
1720
|
-
...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
|
|
1721
|
-
status: "event",
|
|
1722
|
-
seq: handle.seq,
|
|
1723
|
-
lines,
|
|
1724
|
-
summary: `${(handle.description ?? "monitor").slice(0, 200)} — ${lines.length} new output line(s)`,
|
|
1725
|
-
result: clipEvent(lines.join("\n")),
|
|
1726
|
-
});
|
|
1727
|
-
}
|
|
1728
|
-
catch (e) {
|
|
1729
|
-
if (this.handles.get(id) !== handle || handle.status !== "running")
|
|
1730
|
-
return stop();
|
|
1731
|
-
if (++pollFailures <= MAX_RETRYABLE_POLL_FAILURES)
|
|
1732
|
-
return;
|
|
1733
|
-
handle.status = "failed";
|
|
1734
|
-
terminal("failed", `monitor watcher crashed (${e instanceof Error ? e.message : String(e)}); watch ended.`, pendingFinalLines([]));
|
|
1735
|
-
}
|
|
1736
|
-
finally {
|
|
1737
|
-
ticking = false;
|
|
1738
|
-
}
|
|
1739
|
-
};
|
|
1740
|
-
handle.watcher = timers.setInterval(tick, handle.batchWindowMs);
|
|
1741
|
-
}
|
|
1742
|
-
registerWorkflow(input) {
|
|
1743
|
-
assertOwnership(input, "registerWorkflow");
|
|
1744
|
-
const id = input.runId;
|
|
1745
|
-
if (this.handles.has(id))
|
|
1746
|
-
throw new Error(`TaskRegistry: task id already registered: ${id}`);
|
|
1747
|
-
const now = input.now ?? Date.now();
|
|
1748
|
-
const originatingSessionId = input.originatingSessionId;
|
|
1749
|
-
const handle = {
|
|
1750
|
-
id,
|
|
1751
|
-
type: "workflow",
|
|
1752
|
-
status: "running",
|
|
1753
|
-
...(input.description !== undefined ? { description: input.description } : {}),
|
|
1754
|
-
...(input.owner !== undefined ? { owner: input.owner } : {}),
|
|
1755
|
-
...(input.scope !== undefined ? { scope: input.scope } : {}),
|
|
1756
|
-
...(input.toolUseId !== undefined ? { toolUseId: input.toolUseId } : {}),
|
|
1757
|
-
createdAt: now,
|
|
1758
|
-
updatedAt: now,
|
|
1759
|
-
runId: id,
|
|
1760
|
-
handle: input.handle,
|
|
1761
|
-
...(input.store !== undefined ? { store: input.store } : {}),
|
|
1762
|
-
...(originatingSessionId !== undefined ? { originatingSessionId, sessionScoped: true } : {}),
|
|
1763
|
-
...(input.onServedTerminal !== undefined ? { onServedTerminal: input.onServedTerminal } : {}),
|
|
1764
|
-
};
|
|
1765
|
-
this.handles.set(id, handle);
|
|
1766
|
-
void input.handle.done.then((res) => {
|
|
1767
|
-
handle.status = "completed";
|
|
1768
|
-
const run = res?.run;
|
|
1769
|
-
const r = run?.resultFull ?? run?.result;
|
|
1770
|
-
if (typeof r === "string")
|
|
1771
|
-
handle.result = r;
|
|
1772
|
-
handle.updatedAt = Date.now();
|
|
1773
|
-
}, (err) => {
|
|
1774
|
-
if (handle.status !== "cancelled")
|
|
1775
|
-
handle.status = "failed";
|
|
1776
|
-
handle.error = boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500);
|
|
1777
|
-
handle.updatedAt = Date.now();
|
|
1778
|
-
});
|
|
1779
|
-
return id;
|
|
1780
|
-
}
|
|
740
|
+
registerMonitor(input) {
|
|
741
|
+
return registerMonitorLane(this.core, input);
|
|
742
|
+
}
|
|
743
|
+
registerWorkflow(input) {
|
|
744
|
+
return registerWorkflowLane(this.core, input);
|
|
745
|
+
}
|
|
1781
746
|
async pollTask(id, access, opts = {}) {
|
|
1782
747
|
const taskId = id.trim();
|
|
1783
748
|
const deadline = opts.block ? blockDeadline(opts.timeoutMs, Date.now()) : undefined;
|
|
@@ -1825,6 +790,7 @@ export class TaskRegistry {
|
|
|
1825
790
|
The durable record is still marked running (last update ${new Date(row.updatedAt).toISOString()}), but this engine instance is not executing it — the outcome is unknown here. If its host process is gone, a retention sweep (staleRunningMaxAgeMs) settles the record as interrupted.`;
|
|
1826
791
|
return {
|
|
1827
792
|
content: delimitUntrusted(`TaskOutput ${row.handle}`, body),
|
|
793
|
+
isError: true,
|
|
1828
794
|
details: {
|
|
1829
795
|
task_id: row.handle,
|
|
1830
796
|
type: "background_agent",
|
|
@@ -1839,37 +805,6 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
|
|
|
1839
805
|
}
|
|
1840
806
|
return noTask(taskId);
|
|
1841
807
|
}
|
|
1842
|
-
serveDurableAgentRow(row) {
|
|
1843
|
-
if (row.status === "parked") {
|
|
1844
|
-
return {
|
|
1845
|
-
content: delimitUntrusted(`TaskOutput ${row.handle}`, `status: parked
|
|
1846
|
-
The agent is durably suspended, waiting for an approval decision. It resumes when the pending approval is decided (durable approval inbox), or lands failed if the approval expires.`),
|
|
1847
|
-
details: {
|
|
1848
|
-
task_id: row.handle,
|
|
1849
|
-
type: "background_agent",
|
|
1850
|
-
status: "parked",
|
|
1851
|
-
retrieval_status: "success",
|
|
1852
|
-
...(row.seq !== undefined ? { seq: row.seq } : {}),
|
|
1853
|
-
},
|
|
1854
|
-
};
|
|
1855
|
-
}
|
|
1856
|
-
const body = `status: ${row.status}
|
|
1857
|
-
${row.error ? `error: ${row.error}
|
|
1858
|
-
` : ""}${row.finalOutput ? `--- result${row.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
|
|
1859
|
-
${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}`;
|
|
1860
|
-
return {
|
|
1861
|
-
content: delimitUntrusted(`TaskOutput ${row.handle}`, body),
|
|
1862
|
-
details: {
|
|
1863
|
-
task_id: row.handle,
|
|
1864
|
-
type: "background_agent",
|
|
1865
|
-
status: row.status,
|
|
1866
|
-
retrieval_status: "success",
|
|
1867
|
-
...(row.status === "killed" && row.stoppedBy !== undefined ? { stoppedBy: row.stoppedBy } : {}),
|
|
1868
|
-
...(row.seq !== undefined ? { seq: row.seq } : {}),
|
|
1869
|
-
...(row.resultIsPartial ? { partial_result: true } : {}),
|
|
1870
|
-
},
|
|
1871
|
-
};
|
|
1872
|
-
}
|
|
1873
808
|
async stopTask(id, access, opts = {}) {
|
|
1874
809
|
const taskId = id.trim();
|
|
1875
810
|
const handle = this.resolveInternal(taskId, access);
|
|
@@ -1917,7 +852,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
1917
852
|
? `${row.handle} is not attached to this engine instance; cannot stop it here. If its host process is gone, a retention sweep (staleRunningMaxAgeMs) settles it as interrupted.`
|
|
1918
853
|
: row.status === "parked"
|
|
1919
854
|
? `${row.handle} is parked on a pending approval — stop it by deciding (deny/expire) the pending approval in the durable approval inbox.`
|
|
1920
|
-
:
|
|
855
|
+
: alreadyTerminalStopNote(row.handle, row.status),
|
|
1921
856
|
details: {
|
|
1922
857
|
task_id: row.handle,
|
|
1923
858
|
type: "background_agent",
|
|
@@ -1929,13 +864,14 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
1929
864
|
};
|
|
1930
865
|
}
|
|
1931
866
|
}
|
|
1932
|
-
const byName = this.resolveBackgroundAgentByName(taskId, access);
|
|
867
|
+
const byName = this.resolveBackgroundAgentByName(taskId, access, { preferRunning: true });
|
|
1933
868
|
if (byName.status === "found")
|
|
1934
869
|
return this.stopBackgroundAgent(byName.handle);
|
|
1935
870
|
if (byName.status === "ambiguous") {
|
|
1936
871
|
return {
|
|
1937
872
|
content: byName.message,
|
|
1938
873
|
details: { task_id: taskId, retrieval_status: "not_ready", error: "ambiguous" },
|
|
874
|
+
isError: true,
|
|
1939
875
|
};
|
|
1940
876
|
}
|
|
1941
877
|
return noTaskForStop(taskId, {
|
|
@@ -1943,235 +879,6 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
1943
879
|
runningAgents: this.runningBackgroundAgentLabels(access),
|
|
1944
880
|
});
|
|
1945
881
|
}
|
|
1946
|
-
resolveBackgroundAgentByName(name, access) {
|
|
1947
|
-
const key = normalizeAgentName(name);
|
|
1948
|
-
if (key === "")
|
|
1949
|
-
return { status: "not_found" };
|
|
1950
|
-
const latestOf = (pool) => {
|
|
1951
|
-
let best;
|
|
1952
|
-
for (const h of pool)
|
|
1953
|
-
if (best === undefined || h.createdAt >= best.createdAt)
|
|
1954
|
-
best = h;
|
|
1955
|
-
return best;
|
|
1956
|
-
};
|
|
1957
|
-
const named = [];
|
|
1958
|
-
for (const h of this.handles.values()) {
|
|
1959
|
-
if (h.type !== "background_agent" || h.name === undefined || !canAccess(h, access))
|
|
1960
|
-
continue;
|
|
1961
|
-
if (h.name === name || normalizeAgentName(h.name) === key)
|
|
1962
|
-
named.push(h);
|
|
1963
|
-
}
|
|
1964
|
-
const namedWinner = latestOf(named);
|
|
1965
|
-
if (namedWinner !== undefined)
|
|
1966
|
-
return { status: "found", handle: namedWinner };
|
|
1967
|
-
const exact = [];
|
|
1968
|
-
for (const h of this.handles.values()) {
|
|
1969
|
-
if (h.type !== "background_agent" || !canAccess(h, access))
|
|
1970
|
-
continue;
|
|
1971
|
-
if (h.description === name)
|
|
1972
|
-
exact.push(h);
|
|
1973
|
-
}
|
|
1974
|
-
const exactRunning = exact.filter((h) => h.status === "running");
|
|
1975
|
-
const exactPool = exactRunning.length > 0 ? exactRunning : exact;
|
|
1976
|
-
if (exactPool.length === 1)
|
|
1977
|
-
return { status: "found", handle: exactPool[0] };
|
|
1978
|
-
if (exactPool.length > 1) {
|
|
1979
|
-
return {
|
|
1980
|
-
status: "ambiguous",
|
|
1981
|
-
message: `Multiple background agents match "${name}": ${exactPool.map((h) => (h.description ? `${h.id} (${h.description})` : h.id)).join(", ")}. Use the task ID.`,
|
|
1982
|
-
};
|
|
1983
|
-
}
|
|
1984
|
-
const matches = [];
|
|
1985
|
-
for (const h of this.handles.values()) {
|
|
1986
|
-
if (h.type !== "background_agent" || !canAccess(h, access))
|
|
1987
|
-
continue;
|
|
1988
|
-
if (h.description !== undefined && normalizeAgentName(h.description) === key)
|
|
1989
|
-
matches.push(h);
|
|
1990
|
-
}
|
|
1991
|
-
const running = matches.filter((h) => h.status === "running");
|
|
1992
|
-
const pool = running.length > 0 ? running : matches;
|
|
1993
|
-
if (pool.length === 1)
|
|
1994
|
-
return { status: "found", handle: pool[0] };
|
|
1995
|
-
if (pool.length > 1) {
|
|
1996
|
-
return {
|
|
1997
|
-
status: "ambiguous",
|
|
1998
|
-
message: `Multiple background agents match "${name}": ${pool.map((h) => (h.description ? `${h.id} (${h.description})` : h.id)).join(", ")}. Use the task ID.`,
|
|
1999
|
-
};
|
|
2000
|
-
}
|
|
2001
|
-
const byKey = new Map();
|
|
2002
|
-
for (const h of this.handles.values()) {
|
|
2003
|
-
if (h.type !== "background_agent" || h.status !== "running" || !canAccess(h, access))
|
|
2004
|
-
continue;
|
|
2005
|
-
for (const label of [h.name, h.description]) {
|
|
2006
|
-
if (label === undefined)
|
|
2007
|
-
continue;
|
|
2008
|
-
const k = normalizeAgentName(label);
|
|
2009
|
-
if (k !== "" && !byKey.has(k))
|
|
2010
|
-
byKey.set(k, label);
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
const closest = closestName(key, byKey.keys());
|
|
2014
|
-
return { status: "not_found", ...(closest !== undefined ? { suggestion: byKey.get(closest) } : {}) };
|
|
2015
|
-
}
|
|
2016
|
-
markRetainedContinuation(id) {
|
|
2017
|
-
const handle = this.handles.get(id);
|
|
2018
|
-
if (handle && handle.type === "background_agent")
|
|
2019
|
-
handle.retainedContinuation = true;
|
|
2020
|
-
}
|
|
2021
|
-
reviveBackgroundAgent(id, access, abort) {
|
|
2022
|
-
if (this.reapingHandles.has(id) || this.claimingHandles.has(id))
|
|
2023
|
-
return { ok: false, reason: "not_found" };
|
|
2024
|
-
const handle = this.handles.get(id);
|
|
2025
|
-
if (!handle || handle.type !== "background_agent" || !canAccess(handle, access))
|
|
2026
|
-
return { ok: false, reason: "not_found" };
|
|
2027
|
-
if (handle.status === "running" || handle.status === "pending")
|
|
2028
|
-
return { ok: false, reason: "still_running" };
|
|
2029
|
-
if (handle.status === "parked")
|
|
2030
|
-
return { ok: false, reason: "still_running" };
|
|
2031
|
-
handle.status = "running";
|
|
2032
|
-
handle.channelState = "attaching";
|
|
2033
|
-
handle.notify = undefined;
|
|
2034
|
-
if (abort !== undefined)
|
|
2035
|
-
handle.abort = abort;
|
|
2036
|
-
handle.result = undefined;
|
|
2037
|
-
handle.resultFull = undefined;
|
|
2038
|
-
handle.error = undefined;
|
|
2039
|
-
handle.resultIsPartial = undefined;
|
|
2040
|
-
handle.stopSource = undefined;
|
|
2041
|
-
handle.reviveCycle = (handle.reviveCycle ?? 0) + 1;
|
|
2042
|
-
handle.cycleSeq = (handle.cycleSeq ?? 1) + 1;
|
|
2043
|
-
handle.updatedAt = Date.now();
|
|
2044
|
-
this.durableAgentWrite(handle, { status: "running" }, ["settledAt", "stoppedBy", "finalOutput", "finalOutputFull", "error", "resultIsPartial", "summary", "recentSteps", "editedFiles", "usage"]);
|
|
2045
|
-
return { ok: true, cycle: handle.reviveCycle };
|
|
2046
|
-
}
|
|
2047
|
-
settleRevivedAgent(id, cycle, outcome) {
|
|
2048
|
-
const handle = this.handles.get(id);
|
|
2049
|
-
if (!handle || handle.type !== "background_agent")
|
|
2050
|
-
return undefined;
|
|
2051
|
-
if ((handle.reviveCycle ?? 0) !== cycle)
|
|
2052
|
-
return undefined;
|
|
2053
|
-
return this.settleBackgroundAgent(id, outcome);
|
|
2054
|
-
}
|
|
2055
|
-
unmarkRetainedContinuation(id) {
|
|
2056
|
-
const handle = this.handles.get(id);
|
|
2057
|
-
if (handle && handle.type === "background_agent")
|
|
2058
|
-
handle.retainedContinuation = false;
|
|
2059
|
-
}
|
|
2060
|
-
attachAgentNotify(id, notify, cycle) {
|
|
2061
|
-
const handle = this.handles.get(id);
|
|
2062
|
-
if (!handle || handle.type !== "background_agent" || handle.status !== "running")
|
|
2063
|
-
return;
|
|
2064
|
-
if (cycle !== undefined && (handle.reviveCycle ?? 0) !== cycle)
|
|
2065
|
-
return;
|
|
2066
|
-
handle.notify = notify;
|
|
2067
|
-
handle.channelState = "attaching";
|
|
2068
|
-
const live = handle.preAttachQueue;
|
|
2069
|
-
if (live !== undefined && live.length > 0) {
|
|
2070
|
-
void (async () => {
|
|
2071
|
-
while (handle.preAttachQueue === live && live.length > 0) {
|
|
2072
|
-
const [qPayload, qOpts, qResolve] = live[0];
|
|
2073
|
-
let result;
|
|
2074
|
-
try {
|
|
2075
|
-
const d = await notify(qPayload, qOpts);
|
|
2076
|
-
result =
|
|
2077
|
-
d === "parked" && handle.retainedContinuation !== true
|
|
2078
|
-
? { ok: false, reason: "not_running" }
|
|
2079
|
-
: d === "parked"
|
|
2080
|
-
? { ok: true, disposition: "parked" }
|
|
2081
|
-
: { ok: true, disposition: "queued" };
|
|
2082
|
-
}
|
|
2083
|
-
catch {
|
|
2084
|
-
result = { ok: false, reason: "not_running" };
|
|
2085
|
-
}
|
|
2086
|
-
if (handle.preAttachQueue !== live)
|
|
2087
|
-
return;
|
|
2088
|
-
live.shift();
|
|
2089
|
-
qResolve?.(result);
|
|
2090
|
-
}
|
|
2091
|
-
if (handle.preAttachQueue === live) {
|
|
2092
|
-
handle.preAttachQueue = undefined;
|
|
2093
|
-
handle.channelState = "attached";
|
|
2094
|
-
}
|
|
2095
|
-
})();
|
|
2096
|
-
}
|
|
2097
|
-
else {
|
|
2098
|
-
handle.preAttachQueue = undefined;
|
|
2099
|
-
handle.channelState = "attached";
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
async deliverToRunningAgent(id, access, notification, opts) {
|
|
2103
|
-
const handle = this.handles.get(id);
|
|
2104
|
-
if (!handle || handle.type !== "background_agent" || !canAccess(handle, access))
|
|
2105
|
-
return { ok: false, reason: "not_found" };
|
|
2106
|
-
if (handle.status !== "running")
|
|
2107
|
-
return { ok: false, reason: "not_running" };
|
|
2108
|
-
const bounded = (p, fallback, onTimeout) => new Promise((resolve) => {
|
|
2109
|
-
const t = setTimeout(() => {
|
|
2110
|
-
try {
|
|
2111
|
-
onTimeout?.();
|
|
2112
|
-
}
|
|
2113
|
-
catch {
|
|
2114
|
-
}
|
|
2115
|
-
resolve(fallback);
|
|
2116
|
-
}, this.deliverConfirmTimeoutMs);
|
|
2117
|
-
t.unref?.();
|
|
2118
|
-
void p.then((v) => {
|
|
2119
|
-
clearTimeout(t);
|
|
2120
|
-
resolve(v);
|
|
2121
|
-
}, () => {
|
|
2122
|
-
clearTimeout(t);
|
|
2123
|
-
resolve(fallback);
|
|
2124
|
-
});
|
|
2125
|
-
});
|
|
2126
|
-
if (handle.notify === undefined || handle.channelState === "attaching") {
|
|
2127
|
-
if (handle.channelState !== "attaching")
|
|
2128
|
-
return { ok: false, reason: "no_channel" };
|
|
2129
|
-
const q = (handle.preAttachQueue ??= []);
|
|
2130
|
-
if (q.length >= 8)
|
|
2131
|
-
return { ok: false, reason: "no_channel" };
|
|
2132
|
-
return bounded(new Promise((resolve) => {
|
|
2133
|
-
q.push([notification, opts, resolve]);
|
|
2134
|
-
}), { ok: true, disposition: "pending" });
|
|
2135
|
-
}
|
|
2136
|
-
let detachOnTimeout;
|
|
2137
|
-
return await bounded(new Promise((resolve) => {
|
|
2138
|
-
const inflight = (handle.inFlightDirect ??= new Set());
|
|
2139
|
-
const settleOnce = (r) => {
|
|
2140
|
-
if (!inflight.has(settleOnce))
|
|
2141
|
-
return;
|
|
2142
|
-
inflight.delete(settleOnce);
|
|
2143
|
-
resolve(r);
|
|
2144
|
-
};
|
|
2145
|
-
inflight.add(settleOnce);
|
|
2146
|
-
detachOnTimeout = () => inflight.delete(settleOnce);
|
|
2147
|
-
void (async () => {
|
|
2148
|
-
let result;
|
|
2149
|
-
try {
|
|
2150
|
-
const d = await handle.notify?.(notification, opts);
|
|
2151
|
-
if (d === "parked") {
|
|
2152
|
-
result = handle.retainedContinuation === true ? { ok: true, disposition: "parked" } : { ok: false, reason: "not_running" };
|
|
2153
|
-
}
|
|
2154
|
-
else {
|
|
2155
|
-
result = { ok: true, disposition: "queued" };
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
catch {
|
|
2159
|
-
result = { ok: false, reason: "not_running" };
|
|
2160
|
-
}
|
|
2161
|
-
settleOnce(result);
|
|
2162
|
-
})();
|
|
2163
|
-
}), { ok: true, disposition: "pending" }, () => detachOnTimeout?.());
|
|
2164
|
-
}
|
|
2165
|
-
runningBackgroundAgentLabels(access) {
|
|
2166
|
-
const out = [];
|
|
2167
|
-
for (const h of this.handles.values()) {
|
|
2168
|
-
if (h.type !== "background_agent" || h.status !== "running" || !canAccess(h, access))
|
|
2169
|
-
continue;
|
|
2170
|
-
const label = h.name ?? h.description;
|
|
2171
|
-
out.push(label ? `${h.id} (${label})` : h.id);
|
|
2172
|
-
}
|
|
2173
|
-
return out;
|
|
2174
|
-
}
|
|
2175
882
|
deliverConfirmTimeoutMs = 10_000;
|
|
2176
883
|
lastGcAt = 0;
|
|
2177
884
|
list(access, opts = {}) {
|
|
@@ -2277,6 +984,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
2277
984
|
return {
|
|
2278
985
|
content: `Error (TaskOutput): invalid filter regex: ${filter}`,
|
|
2279
986
|
details: { task_id: handle.id, type: "background_bash", status: handle.status, retrieval_status: "not_ready" },
|
|
987
|
+
isError: true,
|
|
2280
988
|
};
|
|
2281
989
|
}
|
|
2282
990
|
}
|
|
@@ -2293,7 +1001,13 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
2293
1001
|
const timedOutClause = spool.timedOut === true ? ` (timed out after ${spool.timeoutSec !== undefined ? `${spool.timeoutSec}s` : "its background time budget"})` : "";
|
|
2294
1002
|
const displayStatus = (spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : running ? "running" : handle.status) + timedOutClause;
|
|
2295
1003
|
const rolledNote = spool.rolledChars > 0 ? `[!] ${spool.rolledChars} earlier char(s) dropped from the spool (memory bound)\n` : "";
|
|
2296
|
-
const
|
|
1004
|
+
const droppedNote = (spool.droppedBytes ?? 0) > 0
|
|
1005
|
+
? `[!] ${spool.droppedBytes} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n`
|
|
1006
|
+
: spool.dropUnknown === true
|
|
1007
|
+
? "[!] an unknown amount of earlier output was permanently dropped before this window (buffer evicted)\n"
|
|
1008
|
+
: "";
|
|
1009
|
+
const stdoutHeading = droppedNote !== "" ? "stdout (re-readable spool — INCOMPLETE, see note above)" : "stdout (full, re-readable)";
|
|
1010
|
+
const body = `status: ${displayStatus}\n${rolledNote}${droppedNote}--- ${stdoutHeading} ---\n${shape(spool.stdout) || "(none)"}\n` +
|
|
2297
1011
|
`--- stderr ---\n${shape(spool.stderr) || "(none)"}`;
|
|
2298
1012
|
return {
|
|
2299
1013
|
content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
|
|
@@ -2303,9 +1017,11 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
2303
1017
|
status: handle.status,
|
|
2304
1018
|
retrieval_status: retrieval,
|
|
2305
1019
|
...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
|
|
1020
|
+
...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
|
|
2306
1021
|
details: {
|
|
2307
1022
|
status: running ? "running" : handle.status,
|
|
2308
1023
|
exitCode: spool.exitCode,
|
|
1024
|
+
...(spool.droppedBytes !== undefined ? { bytesDroppedBeforeCursor: spool.droppedBytes } : {}),
|
|
2309
1025
|
...(spool.timedOut === true ? { timedOut: true, ...(spool.timeoutSec !== undefined ? { timeoutSec: spool.timeoutSec } : {}) } : {}),
|
|
2310
1026
|
},
|
|
2311
1027
|
},
|
|
@@ -2335,6 +1051,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
2335
1051
|
return {
|
|
2336
1052
|
content: `Error (TaskOutput): ${r.error.message}${salvaged}`,
|
|
2337
1053
|
details: { task_id: handle.id, type: "background_bash", status: handle.status, retrieval_status, error: r.error.code },
|
|
1054
|
+
isError: true,
|
|
2338
1055
|
};
|
|
2339
1056
|
}
|
|
2340
1057
|
stdout = roll(stdout + r.value.stdout);
|
|
@@ -2345,6 +1062,8 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
2345
1062
|
handle.stoppedBy = handle.stopSource ?? (r.value.timedOut === true ? "timeout" : "system");
|
|
2346
1063
|
handle.updatedAt = Date.now();
|
|
2347
1064
|
const running = r.value.status === "running";
|
|
1065
|
+
if (!running)
|
|
1066
|
+
mintCompletionId(handle);
|
|
2348
1067
|
if (!running || deadline === undefined || Date.now() >= deadline || signal?.aborted) {
|
|
2349
1068
|
const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
|
|
2350
1069
|
const shape = (text) => clipTaskOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text, handle.outputFile);
|
|
@@ -2362,6 +1081,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
2362
1081
|
status: handle.status,
|
|
2363
1082
|
retrieval_status: retrieval,
|
|
2364
1083
|
...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
|
|
1084
|
+
...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
|
|
2365
1085
|
details: {
|
|
2366
1086
|
status: r.value.status,
|
|
2367
1087
|
exitCode: r.value.exitCode,
|
|
@@ -2374,71 +1094,11 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
2374
1094
|
await sleepPollStep(deadline, signal);
|
|
2375
1095
|
}
|
|
2376
1096
|
}
|
|
2377
|
-
fireServedTerminal(handle) {
|
|
2378
|
-
const cb = handle.onServedTerminal;
|
|
2379
|
-
if (!cb)
|
|
2380
|
-
return;
|
|
2381
|
-
handle.onServedTerminal = undefined;
|
|
2382
|
-
try {
|
|
2383
|
-
cb();
|
|
2384
|
-
}
|
|
2385
|
-
catch {
|
|
2386
|
-
}
|
|
2387
|
-
}
|
|
2388
1097
|
async pollWorkflow(handle, access, deadline, signal) {
|
|
2389
|
-
|
|
2390
|
-
if (store) {
|
|
2391
|
-
for (;;) {
|
|
2392
|
-
const run = await store.get(handle.runId);
|
|
2393
|
-
if (!run || !canAccessWorkflowRun(run, access))
|
|
2394
|
-
break;
|
|
2395
|
-
if (run.status !== "running" || deadline === undefined || Date.now() >= deadline || signal?.aborted) {
|
|
2396
|
-
const out = formatWorkflowRun(run);
|
|
2397
|
-
if (run.status === "running" && deadline !== undefined && Date.now() >= deadline)
|
|
2398
|
-
out.details.retrieval_status = "timeout";
|
|
2399
|
-
else if (run.status !== "running")
|
|
2400
|
-
this.fireServedTerminal(handle);
|
|
2401
|
-
return out;
|
|
2402
|
-
}
|
|
2403
|
-
await sleepPollStep(deadline, signal);
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
|
-
while (handle.status === "running" && deadline !== undefined && Date.now() < deadline && !signal?.aborted) {
|
|
2407
|
-
await sleepPollStep(deadline, signal);
|
|
2408
|
-
}
|
|
2409
|
-
if (handle.status !== "running")
|
|
2410
|
-
this.fireServedTerminal(handle);
|
|
2411
|
-
return {
|
|
2412
|
-
content: JSON.stringify({
|
|
2413
|
-
task_id: handle.id,
|
|
2414
|
-
runId: handle.id,
|
|
2415
|
-
type: "workflow",
|
|
2416
|
-
status: handle.status,
|
|
2417
|
-
...(handle.status === "completed" && handle.result !== undefined
|
|
2418
|
-
? { result: delimitUntrusted("workflow result", handle.result) }
|
|
2419
|
-
: {}),
|
|
2420
|
-
...(handle.status === "failed" && handle.error !== undefined
|
|
2421
|
-
? { error: delimitUntrusted("workflow error", handle.error) }
|
|
2422
|
-
: {}),
|
|
2423
|
-
note: handle.status === "running"
|
|
2424
|
-
? "still running — poll again shortly."
|
|
2425
|
-
: handle.status === "completed"
|
|
2426
|
-
? handle.result !== undefined
|
|
2427
|
-
? "completed — `result` above is the workflow's return value."
|
|
2428
|
-
: "completed — its result was delivered on the completion notification."
|
|
2429
|
-
: handle.status === "failed" && handle.error !== undefined
|
|
2430
|
-
? "failed — see error."
|
|
2431
|
-
: "workflow reached a terminal state.",
|
|
2432
|
-
}),
|
|
2433
|
-
details: {
|
|
2434
|
-
task_id: handle.id,
|
|
2435
|
-
type: "workflow",
|
|
2436
|
-
status: handle.status,
|
|
2437
|
-
retrieval_status: handle.status === "running" && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success",
|
|
2438
|
-
},
|
|
2439
|
-
};
|
|
1098
|
+
return pollWorkflowLane(handle, access, deadline, signal);
|
|
2440
1099
|
}
|
|
2441
1100
|
async stopBackgroundBash(handle) {
|
|
1101
|
+
const wasRunning = handle.status === "running";
|
|
2442
1102
|
if (handle.watcher !== undefined) {
|
|
2443
1103
|
clearInterval(handle.watcher);
|
|
2444
1104
|
handle.watcher = undefined;
|
|
@@ -2447,8 +1107,15 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
2447
1107
|
const r = await handle.env.killBackground(handle.shellId);
|
|
2448
1108
|
if (handle.status !== "running") {
|
|
2449
1109
|
return {
|
|
2450
|
-
content: `Terminated ${handle.id}
|
|
2451
|
-
details: {
|
|
1110
|
+
content: wasRunning ? `Terminated ${handle.id}.` : alreadyTerminalStopNote(handle.id, handle.status),
|
|
1111
|
+
details: {
|
|
1112
|
+
task_id: handle.id,
|
|
1113
|
+
type: "background_bash",
|
|
1114
|
+
status: handle.status,
|
|
1115
|
+
retrieval_status: "success",
|
|
1116
|
+
...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
|
|
1117
|
+
...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
|
|
1118
|
+
},
|
|
2452
1119
|
};
|
|
2453
1120
|
}
|
|
2454
1121
|
if (!r.ok) {
|
|
@@ -2457,10 +1124,18 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
2457
1124
|
handle.stoppedBy = handle.stopSource ?? "parent";
|
|
2458
1125
|
handle.status = "killed";
|
|
2459
1126
|
handle.updatedAt = Date.now();
|
|
1127
|
+
mintCompletionId(handle);
|
|
2460
1128
|
this.pokeBgQuiescence(handle.owner);
|
|
2461
1129
|
return {
|
|
2462
1130
|
content: `Terminated ${handle.id} (the process was already gone).`,
|
|
2463
|
-
details: {
|
|
1131
|
+
details: {
|
|
1132
|
+
task_id: handle.id,
|
|
1133
|
+
type: "background_bash",
|
|
1134
|
+
status: "killed",
|
|
1135
|
+
retrieval_status: "success",
|
|
1136
|
+
stoppedBy: handle.stoppedBy,
|
|
1137
|
+
...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
|
|
1138
|
+
},
|
|
2464
1139
|
};
|
|
2465
1140
|
}
|
|
2466
1141
|
const restored = handle.onTerminal !== undefined;
|
|
@@ -2471,301 +1146,52 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
2471
1146
|
content: `Error (TaskStop): ${r.error.message} — the kill did not land; the process may still be running.` +
|
|
2472
1147
|
(restored ? " Its watcher keeps running: TaskOutput still works and the completion notification will still fire." : " TaskOutput still reflects its live status."),
|
|
2473
1148
|
details: { task_id: handle.id, type: "background_bash", status: handle.status, retrieval_status, error: r.error.code },
|
|
1149
|
+
isError: true,
|
|
2474
1150
|
};
|
|
2475
1151
|
}
|
|
2476
1152
|
if (handle.stoppedBy === undefined)
|
|
2477
1153
|
handle.stoppedBy = handle.stopSource ?? "parent";
|
|
2478
1154
|
handle.status = "killed";
|
|
2479
1155
|
handle.updatedAt = Date.now();
|
|
1156
|
+
mintCompletionId(handle);
|
|
2480
1157
|
this.pokeBgQuiescence(handle.owner);
|
|
2481
|
-
return {
|
|
2482
|
-
content: `Terminated ${handle.id}.`,
|
|
2483
|
-
details: { task_id: handle.id, type: "background_bash", status: "killed", retrieval_status: "success", stoppedBy: handle.stoppedBy },
|
|
2484
|
-
};
|
|
2485
|
-
}
|
|
2486
|
-
async pollMonitor(handle, filter, deadline, signal) {
|
|
2487
|
-
let rx;
|
|
2488
|
-
if (filter !== undefined) {
|
|
2489
|
-
try {
|
|
2490
|
-
rx = new RegExp(filter);
|
|
2491
|
-
}
|
|
2492
|
-
catch {
|
|
2493
|
-
return {
|
|
2494
|
-
content: `Error (TaskOutput): invalid filter regex: ${filter}`,
|
|
2495
|
-
details: { task_id: handle.id, type: "monitor", status: handle.status, retrieval_status: "not_ready" },
|
|
2496
|
-
};
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
while (handle.status === "running" && deadline !== undefined && Date.now() < deadline && !signal?.aborted) {
|
|
2500
|
-
await sleepPollStep(deadline, signal);
|
|
2501
|
-
}
|
|
2502
|
-
const spool = handle.spool;
|
|
2503
|
-
const shape = (text) => clipTaskOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text, handle.outputFile);
|
|
2504
|
-
const running = handle.status === "running";
|
|
2505
|
-
const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
|
|
2506
|
-
const displayStatus = spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : running ? "running" : handle.status;
|
|
2507
|
-
const rolledNote = spool.rolledChars > 0 ? `[!] ${spool.rolledChars} earlier char(s) dropped from the spool (memory bound)\n` : "";
|
|
2508
|
-
const body = `status: ${displayStatus}\n${rolledNote}--- stdout (full, re-readable) ---\n${shape(spool.stdout) || "(none)"}\n` +
|
|
2509
|
-
`--- stderr ---\n${shape(spool.stderr) || "(none)"}`;
|
|
2510
|
-
return {
|
|
2511
|
-
content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
|
|
2512
|
-
details: {
|
|
2513
|
-
task_id: handle.id,
|
|
2514
|
-
type: "monitor",
|
|
2515
|
-
status: handle.status,
|
|
2516
|
-
retrieval_status: retrieval,
|
|
2517
|
-
...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
|
|
2518
|
-
details: { status: running ? "running" : handle.status, exitCode: spool.exitCode },
|
|
2519
|
-
},
|
|
2520
|
-
};
|
|
2521
|
-
}
|
|
2522
|
-
async stopMonitor(handle) {
|
|
2523
|
-
if (handle.watcher !== undefined) {
|
|
2524
|
-
handle.timers.clearInterval(handle.watcher);
|
|
2525
|
-
handle.watcher = undefined;
|
|
2526
|
-
}
|
|
2527
|
-
this.markStopSource(handle.id, "parent");
|
|
2528
|
-
const r = await handle.env.killBackground(handle.shellId);
|
|
2529
|
-
if (handle.status !== "running") {
|
|
2530
|
-
return {
|
|
2531
|
-
content: `Terminated ${handle.id}.`,
|
|
2532
|
-
details: { task_id: handle.id, type: "monitor", status: handle.status, retrieval_status: "success", ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}) },
|
|
2533
|
-
};
|
|
2534
|
-
}
|
|
2535
|
-
if (!r.ok) {
|
|
2536
|
-
if (r.error.code === "not_found") {
|
|
2537
|
-
if (handle.stoppedBy === undefined)
|
|
2538
|
-
handle.stoppedBy = handle.stopSource ?? "parent";
|
|
2539
|
-
handle.status = "killed";
|
|
2540
|
-
handle.updatedAt = Date.now();
|
|
2541
|
-
this.pokeBgQuiescence(handle.owner);
|
|
2542
|
-
return {
|
|
2543
|
-
content: `Terminated ${handle.id} (the process was already gone).`,
|
|
2544
|
-
details: { task_id: handle.id, type: "monitor", status: "killed", retrieval_status: "success", stoppedBy: handle.stoppedBy },
|
|
2545
|
-
};
|
|
2546
|
-
}
|
|
2547
|
-
if (handle.watcher === undefined)
|
|
2548
|
-
this.startMonitorWatcher(handle.id);
|
|
2549
|
-
const retrieval_status = r.error.code === "timeout" ? "timeout" : "not_ready";
|
|
2550
|
-
return {
|
|
2551
|
-
content: `Error (TaskStop): ${r.error.message} — the kill did not land; the process may still be running. The watch keeps running: events and the terminal notification still fire.`,
|
|
2552
|
-
details: { task_id: handle.id, type: "monitor", status: handle.status, retrieval_status, error: r.error.code },
|
|
2553
|
-
};
|
|
2554
|
-
}
|
|
2555
|
-
if (handle.status === "running") {
|
|
2556
|
-
if (handle.stoppedBy === undefined)
|
|
2557
|
-
handle.stoppedBy = handle.stopSource ?? "parent";
|
|
2558
|
-
handle.status = "killed";
|
|
2559
|
-
handle.updatedAt = Date.now();
|
|
2560
|
-
this.pokeBgQuiescence(handle.owner);
|
|
2561
|
-
}
|
|
2562
1158
|
return {
|
|
2563
1159
|
content: `Terminated ${handle.id}.`,
|
|
2564
1160
|
details: {
|
|
2565
1161
|
task_id: handle.id,
|
|
2566
|
-
type: "
|
|
2567
|
-
status:
|
|
1162
|
+
type: "background_bash",
|
|
1163
|
+
status: "killed",
|
|
2568
1164
|
retrieval_status: "success",
|
|
2569
|
-
|
|
1165
|
+
stoppedBy: handle.stoppedBy,
|
|
1166
|
+
...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
|
|
2570
1167
|
},
|
|
2571
1168
|
};
|
|
2572
1169
|
}
|
|
2573
|
-
async
|
|
2574
|
-
|
|
2575
|
-
await sleepPollStep(deadline, signal);
|
|
2576
|
-
}
|
|
2577
|
-
const running = handle.status === "running";
|
|
2578
|
-
const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
|
|
2579
|
-
if (handle.status === "parked") {
|
|
2580
|
-
return {
|
|
2581
|
-
content: delimitUntrusted(`TaskOutput ${handle.id}`, `status: parked
|
|
2582
|
-
The agent is durably suspended, waiting for an approval decision. It resumes when the pending approval is decided (durable approval inbox), or lands failed if the approval expires.`),
|
|
2583
|
-
details: {
|
|
2584
|
-
task_id: handle.id,
|
|
2585
|
-
type: "background_agent",
|
|
2586
|
-
status: "parked",
|
|
2587
|
-
retrieval_status: "success",
|
|
2588
|
-
...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
|
|
2589
|
-
},
|
|
2590
|
-
};
|
|
2591
|
-
}
|
|
2592
|
-
const body = running
|
|
2593
|
-
? `status: running
|
|
2594
|
-
The agent is still working — you will be notified when it completes.`
|
|
2595
|
-
: `status: ${handle.status}
|
|
2596
|
-
${handle.error ? `error: ${handle.error}
|
|
2597
|
-
` : ""}${handle.result ? `--- result${handle.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
|
|
2598
|
-
${clipTaskOutput(handle.resultFull ?? handle.result, handle.outputFile)}` : "(no result text)"}`;
|
|
2599
|
-
return {
|
|
2600
|
-
content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
|
|
2601
|
-
details: {
|
|
2602
|
-
task_id: handle.id,
|
|
2603
|
-
type: "background_agent",
|
|
2604
|
-
status: handle.status,
|
|
2605
|
-
retrieval_status: retrieval,
|
|
2606
|
-
...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
|
|
2607
|
-
...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
|
|
2608
|
-
...(handle.resultIsPartial ? { partial_result: true } : {}),
|
|
2609
|
-
},
|
|
2610
|
-
};
|
|
1170
|
+
async pollMonitor(handle, filter, deadline, signal) {
|
|
1171
|
+
return pollMonitorLane(handle, filter, deadline, signal);
|
|
2611
1172
|
}
|
|
2612
|
-
async
|
|
2613
|
-
|
|
2614
|
-
const arbiter = handle.resolveParkedStop;
|
|
2615
|
-
if (arbiter === undefined) {
|
|
2616
|
-
return {
|
|
2617
|
-
content: `${handle.id} is parked on a pending approval — stop it by deciding (deny/expire) the pending approval in the durable approval inbox.`,
|
|
2618
|
-
details: { task_id: handle.id, type: "background_agent", status: "parked", retrieval_status: "not_ready", error: "parked_pending_approval" },
|
|
2619
|
-
};
|
|
2620
|
-
}
|
|
2621
|
-
let stopWon = false;
|
|
2622
|
-
let arbiterUnreachable = false;
|
|
2623
|
-
try {
|
|
2624
|
-
stopWon = await arbiter();
|
|
2625
|
-
}
|
|
2626
|
-
catch {
|
|
2627
|
-
stopWon = false;
|
|
2628
|
-
arbiterUnreachable = true;
|
|
2629
|
-
}
|
|
2630
|
-
if (stopWon && handle.status === "parked") {
|
|
2631
|
-
this.markStopSource(handle.id, "parent");
|
|
2632
|
-
handle.status = "killed";
|
|
2633
|
-
handle.stoppedBy = handle.stopSource ?? "parent";
|
|
2634
|
-
handle.updatedAt = Date.now();
|
|
2635
|
-
handle.parkedCheckpointToken = undefined;
|
|
2636
|
-
handle.resolveParkedStop = undefined;
|
|
2637
|
-
this.pokeBgQuiescence(handle.owner);
|
|
2638
|
-
{
|
|
2639
|
-
const lane = handle.durable;
|
|
2640
|
-
if (lane !== undefined) {
|
|
2641
|
-
try {
|
|
2642
|
-
const live = await lane.store.get(handle.id, lane.record.scope);
|
|
2643
|
-
if (live && (live.status === "parked" || live.status === "running")) {
|
|
2644
|
-
const term = structuredClone(live);
|
|
2645
|
-
term.status = "killed";
|
|
2646
|
-
if (handle.stoppedBy !== undefined)
|
|
2647
|
-
term.stoppedBy = handle.stoppedBy;
|
|
2648
|
-
term.settledAt = Date.now();
|
|
2649
|
-
term.updatedAt = Date.now();
|
|
2650
|
-
delete term.parkedCheckpointToken;
|
|
2651
|
-
delete term.parkClaimId;
|
|
2652
|
-
delete term.parkedAt;
|
|
2653
|
-
await lane.store.update(handle.id, lane.record.scope, term, { rev: live.rev });
|
|
2654
|
-
}
|
|
2655
|
-
}
|
|
2656
|
-
catch {
|
|
2657
|
-
}
|
|
2658
|
-
}
|
|
2659
|
-
}
|
|
2660
|
-
this.durableAgentWrite(handle, { status: "killed", settledAt: Date.now(), ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}) }, ["parkedCheckpointToken", "parkClaimId"]);
|
|
2661
|
-
return {
|
|
2662
|
-
content: `Terminated ${handle.id} (pending approval expired).`,
|
|
2663
|
-
details: {
|
|
2664
|
-
task_id: handle.id,
|
|
2665
|
-
type: "background_agent",
|
|
2666
|
-
status: "killed",
|
|
2667
|
-
retrieval_status: "success",
|
|
2668
|
-
...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
|
|
2669
|
-
...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
|
|
2670
|
-
},
|
|
2671
|
-
};
|
|
2672
|
-
}
|
|
2673
|
-
if (arbiterUnreachable) {
|
|
2674
|
-
return {
|
|
2675
|
-
content: `${handle.id} was not stopped: the approval arbitration store was unreachable — the row stays parked; retry the stop.`,
|
|
2676
|
-
details: { task_id: handle.id, type: "background_agent", status: handle.status, retrieval_status: "not_ready", error: "park_arbiter_unreachable" },
|
|
2677
|
-
};
|
|
2678
|
-
}
|
|
2679
|
-
return {
|
|
2680
|
-
content: `${handle.id} was not stopped: its pending approval was already consumed (a resume won the arbitration).`,
|
|
2681
|
-
details: { task_id: handle.id, type: "background_agent", status: handle.status, retrieval_status: "not_ready", error: "park_resume_won" },
|
|
2682
|
-
};
|
|
2683
|
-
}
|
|
2684
|
-
if (handle.status === "running") {
|
|
2685
|
-
this.markStopSource(handle.id, "parent");
|
|
2686
|
-
handle.abort.abort();
|
|
2687
|
-
handle.status = "killed";
|
|
2688
|
-
handle.stoppedBy = handle.stopSource ?? "parent";
|
|
2689
|
-
handle.updatedAt = Date.now();
|
|
2690
|
-
handle.notify = undefined;
|
|
2691
|
-
for (const [, , qResolve] of handle.preAttachQueue ?? [])
|
|
2692
|
-
qResolve?.({ ok: false, reason: "not_running" });
|
|
2693
|
-
handle.preAttachQueue = undefined;
|
|
2694
|
-
for (const r of [...(handle.inFlightDirect ?? [])])
|
|
2695
|
-
r({ ok: false, reason: "not_running" });
|
|
2696
|
-
handle.inFlightDirect = undefined;
|
|
2697
|
-
this.pokeBgQuiescence(handle.owner);
|
|
2698
|
-
this.durableAgentWrite(handle, {
|
|
2699
|
-
status: "killed",
|
|
2700
|
-
settledAt: Date.now(),
|
|
2701
|
-
...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
|
|
2702
|
-
});
|
|
2703
|
-
}
|
|
2704
|
-
return {
|
|
2705
|
-
content: `Terminated ${handle.id}.`,
|
|
2706
|
-
details: {
|
|
2707
|
-
task_id: handle.id,
|
|
2708
|
-
type: "background_agent",
|
|
2709
|
-
status: handle.status,
|
|
2710
|
-
retrieval_status: "success",
|
|
2711
|
-
...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
|
|
2712
|
-
...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
|
|
2713
|
-
},
|
|
2714
|
-
};
|
|
1173
|
+
async stopMonitor(handle) {
|
|
1174
|
+
return stopMonitorLane(this.core, handle);
|
|
2715
1175
|
}
|
|
2716
1176
|
async stopWorkflow(handle) {
|
|
2717
|
-
|
|
2718
|
-
return {
|
|
2719
|
-
content: JSON.stringify({
|
|
2720
|
-
task_id: handle.id,
|
|
2721
|
-
runId: handle.id,
|
|
2722
|
-
type: "workflow",
|
|
2723
|
-
status: handle.status,
|
|
2724
|
-
stopped: false,
|
|
2725
|
-
note: `workflow already ${handle.status} — nothing to stop; poll TaskOutput for its final state.`,
|
|
2726
|
-
}),
|
|
2727
|
-
details: { task_id: handle.id, type: "workflow", status: handle.status, retrieval_status: "success" },
|
|
2728
|
-
};
|
|
2729
|
-
}
|
|
2730
|
-
handle.status = "cancelled";
|
|
2731
|
-
handle.updatedAt = Date.now();
|
|
2732
|
-
handle.handle.cancel("TaskStop requested");
|
|
2733
|
-
return {
|
|
2734
|
-
content: JSON.stringify({
|
|
2735
|
-
task_id: handle.id,
|
|
2736
|
-
runId: handle.id,
|
|
2737
|
-
type: "workflow",
|
|
2738
|
-
status: "cancelled",
|
|
2739
|
-
stopped: true,
|
|
2740
|
-
note: "workflow cancellation requested; durable run status maps cancellation to failed when it finalizes.",
|
|
2741
|
-
}),
|
|
2742
|
-
details: { task_id: handle.id, type: "workflow", status: "cancelled", retrieval_status: "success" },
|
|
2743
|
-
};
|
|
1177
|
+
return stopWorkflowLane(handle);
|
|
2744
1178
|
}
|
|
2745
1179
|
}
|
|
2746
1180
|
export const defaultTaskRegistry = new TaskRegistry();
|
|
2747
1181
|
export function createTaskOutputTool(opts) {
|
|
1182
|
+
const caps = { ...REGISTRY_TASK_TOOL_CAPS, notification: opts.notificationWired !== false };
|
|
2748
1183
|
return defineTool({
|
|
2749
|
-
name:
|
|
1184
|
+
name: TASK_OUTPUT_TOOL_NAME,
|
|
2750
1185
|
aliases: [...TASK_OUTPUT_ALIASES],
|
|
2751
|
-
contract:
|
|
2752
|
-
description:
|
|
2753
|
-
|
|
2754
|
-
"Retrieve output or status for a background task by task_id. Supports background Bash tasks and workflow " +
|
|
2755
|
-
"tasks returned by RunWorkflow. Returns only the caller's own task scope; unknown and out-of-scope ids are " +
|
|
2756
|
-
"reported the same way. Legacy bash_id and runId arguments are accepted for compatibility.",
|
|
2757
|
-
parameters: Type.Object({
|
|
2758
|
-
task_id: Type.Optional(Type.String({ description: "The task_id returned by Bash(run_in_background) or RunWorkflow." })),
|
|
2759
|
-
block: Type.Optional(Type.Boolean({ description: "Whether to wait for completion (default true — the call waits until the task finishes or `timeout`, saving re-poll turns). Pass false for an instantaneous snapshot." })),
|
|
2760
|
-
timeout: Type.Optional(Type.Number({ description: "Optional timeout in milliseconds when block is true (default 30000, max 600000)." })),
|
|
2761
|
-
filter: Type.Optional(Type.String({ description: "Optional regex filter for background Bash output (sema extension)." })),
|
|
2762
|
-
}),
|
|
1186
|
+
contract: TASK_OUTPUT_CONTRACT,
|
|
1187
|
+
description: composeTaskOutputDescription(caps),
|
|
1188
|
+
parameters: composeTaskOutputParams(caps),
|
|
2763
1189
|
effect: "read",
|
|
2764
1190
|
execute: async (rawArgs, ctx) => {
|
|
2765
1191
|
const args = rawArgs;
|
|
2766
|
-
const id =
|
|
1192
|
+
const id = resolveTaskIdArg(args.task_id, args.bash_id, args.runId);
|
|
2767
1193
|
if (!id)
|
|
2768
|
-
return
|
|
1194
|
+
return errorResult(TASK_OUTPUT_MISSING_ID_MESSAGE);
|
|
2769
1195
|
const requestedMs = typeof args.timeout === "number" ? args.timeout : undefined;
|
|
2770
1196
|
let effectiveTimeoutMs = requestedMs;
|
|
2771
1197
|
let waitClampNote = "";
|
|
@@ -2792,35 +1218,33 @@ export function createTaskOutputTool(opts) {
|
|
|
2792
1218
|
signal: ctx.signal,
|
|
2793
1219
|
});
|
|
2794
1220
|
const { type: taskType, ...rest } = r.details;
|
|
2795
|
-
return {
|
|
1221
|
+
return {
|
|
1222
|
+
content: r.content + waitClampNote,
|
|
1223
|
+
details: { type: "task-output", ...(taskType !== undefined ? { taskType } : {}), ...(waitClampNote !== "" ? { waitClamped: true } : {}), ...rest },
|
|
1224
|
+
...(r.isError ? { isError: true } : {}),
|
|
1225
|
+
};
|
|
2796
1226
|
},
|
|
2797
1227
|
});
|
|
2798
1228
|
}
|
|
2799
1229
|
export function createTaskStopTool(opts) {
|
|
2800
1230
|
return defineTool({
|
|
2801
|
-
name:
|
|
1231
|
+
name: TASK_STOP_TOOL_NAME,
|
|
2802
1232
|
aliases: [...TASK_STOP_ALIASES],
|
|
2803
|
-
contract:
|
|
2804
|
-
description:
|
|
2805
|
-
|
|
2806
|
-
"- To stop a background agent spawned with a name, pass that name as task_id\n" +
|
|
2807
|
-
"- Returns a success or failure status\n" +
|
|
2808
|
-
"- Use this tool when you need to terminate a long-running task",
|
|
2809
|
-
parameters: Type.Object({
|
|
2810
|
-
task_id: Type.Optional(Type.String({ description: "The ID of the background task to stop" })),
|
|
2811
|
-
shell_id: Type.Optional(Type.String({ description: "Deprecated: use task_id instead" })),
|
|
2812
|
-
}),
|
|
1233
|
+
contract: TASK_STOP_CONTRACT,
|
|
1234
|
+
description: composeTaskStopDescription(REGISTRY_TASK_TOOL_CAPS),
|
|
1235
|
+
parameters: TASK_STOP_PARAMS,
|
|
2813
1236
|
effect: "write",
|
|
2814
1237
|
execute: async (rawArgs, ctx) => {
|
|
2815
1238
|
const args = rawArgs;
|
|
2816
|
-
const id =
|
|
1239
|
+
const id = resolveTaskIdArg(args.task_id, args.shell_id, args.runId);
|
|
2817
1240
|
if (!id)
|
|
2818
|
-
return
|
|
1241
|
+
return errorResult(TASK_STOP_MISSING_ID_MESSAGE);
|
|
2819
1242
|
const r = await opts.registry.stopTask(id, { owner: ctx.taskId ?? opts.owner, scope: ctx.principal ?? opts.scope, ...((ctx.sessionId ?? opts.sessionId) !== undefined ? { sessionId: ctx.sessionId ?? opts.sessionId } : {}) }, { workflowStore: opts.workflowStore, agentStore: opts.agentStore });
|
|
2820
1243
|
const { type: taskType, ...rest } = r.details;
|
|
2821
1244
|
return {
|
|
2822
1245
|
content: r.content,
|
|
2823
1246
|
details: { type: "task-stop", message: r.content, task_type: taskType ?? "unknown", ...rest },
|
|
1247
|
+
...(r.isError ? { isError: true } : {}),
|
|
2824
1248
|
};
|
|
2825
1249
|
},
|
|
2826
1250
|
});
|