@polpo-ai/core 0.15.144 → 0.16.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/README.md +30 -0
- package/dist/__tests__/custom-tool-runner.test.d.ts +2 -0
- package/dist/__tests__/custom-tool-runner.test.d.ts.map +1 -0
- package/dist/__tests__/custom-tool-runner.test.js +121 -0
- package/dist/__tests__/custom-tool-runner.test.js.map +1 -0
- package/dist/__tests__/mcp-runner-client.test.d.ts +2 -0
- package/dist/__tests__/mcp-runner-client.test.d.ts.map +1 -0
- package/dist/__tests__/mcp-runner-client.test.js +144 -0
- package/dist/__tests__/mcp-runner-client.test.js.map +1 -0
- package/dist/__tests__/mission-execution.test.d.ts +2 -0
- package/dist/__tests__/mission-execution.test.d.ts.map +1 -0
- package/dist/__tests__/mission-execution.test.js +68 -0
- package/dist/__tests__/mission-execution.test.js.map +1 -0
- package/dist/__tests__/run-state-monitor.test.d.ts +2 -0
- package/dist/__tests__/run-state-monitor.test.d.ts.map +1 -0
- package/dist/__tests__/run-state-monitor.test.js +59 -0
- package/dist/__tests__/run-state-monitor.test.js.map +1 -0
- package/dist/__tests__/runtime-metadata.test.d.ts +2 -0
- package/dist/__tests__/runtime-metadata.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-metadata.test.js +17 -0
- package/dist/__tests__/runtime-metadata.test.js.map +1 -0
- package/dist/__tests__/task-execution-router.test.js +1 -1
- package/dist/__tests__/task-execution-router.test.js.map +1 -1
- package/dist/__tests__/task-runner-agent-lifetime.test.d.ts +2 -0
- package/dist/__tests__/task-runner-agent-lifetime.test.d.ts.map +1 -0
- package/dist/__tests__/task-runner-agent-lifetime.test.js +136 -0
- package/dist/__tests__/task-runner-agent-lifetime.test.js.map +1 -0
- package/dist/adapter.d.ts +18 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/agent-manager.d.ts +3 -0
- package/dist/agent-manager.d.ts.map +1 -1
- package/dist/agent-manager.js +28 -2
- package/dist/agent-manager.js.map +1 -1
- package/dist/agent-store.d.ts +70 -1
- package/dist/agent-store.d.ts.map +1 -1
- package/dist/agent-store.js +103 -1
- package/dist/agent-store.js.map +1 -1
- package/dist/approval-manager.d.ts +24 -4
- package/dist/approval-manager.d.ts.map +1 -1
- package/dist/approval-manager.js +160 -112
- package/dist/approval-manager.js.map +1 -1
- package/dist/approval-manager.test.d.ts +2 -0
- package/dist/approval-manager.test.d.ts.map +1 -0
- package/dist/approval-manager.test.js +116 -0
- package/dist/approval-manager.test.js.map +1 -0
- package/dist/assessment-orchestrator.d.ts +12 -1
- package/dist/assessment-orchestrator.d.ts.map +1 -1
- package/dist/assessment-orchestrator.js +181 -83
- package/dist/assessment-orchestrator.js.map +1 -1
- package/dist/custom-tool-runner.d.ts +56 -0
- package/dist/custom-tool-runner.d.ts.map +1 -0
- package/dist/custom-tool-runner.js +197 -0
- package/dist/custom-tool-runner.js.map +1 -0
- package/dist/escalation-manager.d.ts +2 -0
- package/dist/escalation-manager.d.ts.map +1 -1
- package/dist/escalation-manager.js +79 -25
- package/dist/escalation-manager.js.map +1 -1
- package/dist/events.d.ts +4 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +6 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +19 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-capability.d.ts +40 -0
- package/dist/mcp-capability.d.ts.map +1 -0
- package/dist/mcp-capability.js +2 -0
- package/dist/mcp-capability.js.map +1 -0
- package/dist/mcp-runner.d.ts +45 -0
- package/dist/mcp-runner.d.ts.map +1 -0
- package/dist/mcp-runner.js +238 -0
- package/dist/mcp-runner.js.map +1 -0
- package/dist/mission-cancellation.d.ts +14 -0
- package/dist/mission-cancellation.d.ts.map +1 -0
- package/dist/mission-cancellation.js +43 -0
- package/dist/mission-cancellation.js.map +1 -0
- package/dist/mission-execution.d.ts +103 -0
- package/dist/mission-execution.d.ts.map +1 -0
- package/dist/mission-execution.js +196 -0
- package/dist/mission-execution.js.map +1 -0
- package/dist/mission-executor.d.ts +18 -16
- package/dist/mission-executor.d.ts.map +1 -1
- package/dist/mission-executor.js +339 -203
- package/dist/mission-executor.js.map +1 -1
- package/dist/mission-gating.d.ts +11 -15
- package/dist/mission-gating.d.ts.map +1 -1
- package/dist/mission-gating.js +25 -33
- package/dist/mission-gating.js.map +1 -1
- package/dist/mission-inspection.d.ts +29 -0
- package/dist/mission-inspection.d.ts.map +1 -0
- package/dist/mission-inspection.js +99 -0
- package/dist/mission-inspection.js.map +1 -0
- package/dist/mission-store.d.ts +7 -0
- package/dist/mission-store.d.ts.map +1 -1
- package/dist/mission-store.js +8 -0
- package/dist/mission-store.js.map +1 -1
- package/dist/orchestrator-context.d.ts +3 -0
- package/dist/orchestrator-context.d.ts.map +1 -1
- package/dist/orchestrator-engine.d.ts +39 -39
- package/dist/orchestrator-engine.d.ts.map +1 -1
- package/dist/orchestrator-engine.js +124 -24
- package/dist/orchestrator-engine.js.map +1 -1
- package/dist/run-state-monitor.d.ts +14 -0
- package/dist/run-state-monitor.d.ts.map +1 -0
- package/dist/run-state-monitor.js +63 -0
- package/dist/run-state-monitor.js.map +1 -0
- package/dist/run-store.d.ts +15 -0
- package/dist/run-store.d.ts.map +1 -1
- package/dist/runner-capability.d.ts +43 -0
- package/dist/runner-capability.d.ts.map +1 -0
- package/dist/runner-capability.js +55 -0
- package/dist/runner-capability.js.map +1 -0
- package/dist/runtime-metadata.d.ts +5 -0
- package/dist/runtime-metadata.d.ts.map +1 -0
- package/dist/runtime-metadata.js +21 -0
- package/dist/runtime-metadata.js.map +1 -0
- package/dist/scheduler.d.ts +2 -1
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +16 -6
- package/dist/scheduler.js.map +1 -1
- package/dist/schemas.d.ts +8 -6
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +10 -0
- package/dist/schemas.js.map +1 -1
- package/dist/spawner.d.ts +12 -0
- package/dist/spawner.d.ts.map +1 -1
- package/dist/steering.d.ts +4 -1
- package/dist/steering.d.ts.map +1 -1
- package/dist/steering.js.map +1 -1
- package/dist/task-approval-coordinator.d.ts +29 -0
- package/dist/task-approval-coordinator.d.ts.map +1 -0
- package/dist/task-approval-coordinator.js +216 -0
- package/dist/task-approval-coordinator.js.map +1 -0
- package/dist/task-dispatch.d.ts +40 -0
- package/dist/task-dispatch.d.ts.map +1 -0
- package/dist/task-dispatch.js +136 -0
- package/dist/task-dispatch.js.map +1 -0
- package/dist/task-execution.d.ts +146 -0
- package/dist/task-execution.d.ts.map +1 -0
- package/dist/task-execution.js +357 -0
- package/dist/task-execution.js.map +1 -0
- package/dist/task-manager.d.ts +31 -22
- package/dist/task-manager.d.ts.map +1 -1
- package/dist/task-manager.js +74 -7
- package/dist/task-manager.js.map +1 -1
- package/dist/task-runner.d.ts +30 -3
- package/dist/task-runner.d.ts.map +1 -1
- package/dist/task-runner.js +429 -90
- package/dist/task-runner.js.map +1 -1
- package/dist/task-store.d.ts +9 -3
- package/dist/task-store.d.ts.map +1 -1
- package/dist/tool-invocation.d.ts +4 -0
- package/dist/tool-invocation.d.ts.map +1 -1
- package/dist/tool-invocation.js +8 -1
- package/dist/tool-invocation.js.map +1 -1
- package/dist/tool-invocation.test.d.ts +2 -0
- package/dist/tool-invocation.test.d.ts.map +1 -0
- package/dist/tool-invocation.test.js +40 -0
- package/dist/tool-invocation.test.js.map +1 -0
- package/dist/types/agent.d.ts +4 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/agent.js.map +1 -1
- package/dist/types/config.d.ts +3 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/mission.d.ts +23 -1
- package/dist/types/mission.d.ts.map +1 -1
- package/dist/types/task.d.ts +10 -0
- package/dist/types/task.d.ts.map +1 -1
- package/package.json +17 -1
package/dist/task-runner.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { nanoid } from "nanoid";
|
|
2
|
-
import {
|
|
2
|
+
import { isVersionedAgentStore, agentIdentityFromSnapshot, assertAgentIdentity, AgentIdentityError } from "./agent-store.js";
|
|
3
|
+
import { resolveMissionStore, resolveMissionForTask, resolveMissionExecutionStore } from "./mission-store.js";
|
|
4
|
+
import { MissionExecutionError } from "./mission-execution.js";
|
|
3
5
|
import { agentMemoryScope } from "./memory-store.js";
|
|
4
6
|
import { resolveExecutionMode } from "./execution-mode.js";
|
|
5
7
|
import { resolveRuntimeSandboxOptions } from "./runtime-sandbox.js";
|
|
8
|
+
import { normalizeRuntimeMetadata } from "./runtime-metadata.js";
|
|
6
9
|
import { resolveConfiguredModelSelection } from "./model-profiles.js";
|
|
7
10
|
import { createRuntimePromptContextSegment, resolveRuntimeContext, replacesLegacyAgentMemory, replacesLegacySharedMemory, } from "./runtime-context/index.js";
|
|
8
11
|
import { createExecutionRouteResolvedEvent, resolveTaskExecutionRoute, } from "./execution-router.js";
|
|
12
|
+
import { isConditionalTaskExecutionStore, isTaskExecutionStore, taskExecutionReference, TaskExecutionError } from "./task-execution.js";
|
|
13
|
+
import { claimTaskDispatch, authorizeTaskDispatch, isTaskDispatchActive } from "./task-dispatch.js";
|
|
9
14
|
/**
|
|
10
15
|
* Durable turns: max age of a resume checkpoint before orphan recovery
|
|
11
16
|
* ignores it and falls back to retry-from-zero. Crash/deploy restarts are
|
|
@@ -53,6 +58,131 @@ function pathDirname(p) {
|
|
|
53
58
|
*/
|
|
54
59
|
export class TaskRunner {
|
|
55
60
|
ctx;
|
|
61
|
+
closing = false;
|
|
62
|
+
closePromise;
|
|
63
|
+
preparations = new Set();
|
|
64
|
+
owners = new Map();
|
|
65
|
+
pendingSpawns = new Map();
|
|
66
|
+
/** Interrupted preparations retain their receipt until failure persistence
|
|
67
|
+
* and acknowledgement finish, or ownership transfers to a SpawnResult. */
|
|
68
|
+
interruptedPreparations = new Set();
|
|
69
|
+
/** Stop admission synchronously; in-flight preparation remains owned. */
|
|
70
|
+
stop() { this.closing = true; }
|
|
71
|
+
/** Drain only locally spawned owners. A terminal store row is not an exit
|
|
72
|
+
* receipt. Hosts may bound this wait, but must keep stores open on timeout. */
|
|
73
|
+
close() {
|
|
74
|
+
this.stop();
|
|
75
|
+
if (this.closePromise)
|
|
76
|
+
return this.closePromise;
|
|
77
|
+
const drain = this.drainOwners();
|
|
78
|
+
this.closePromise = drain;
|
|
79
|
+
void drain.catch(() => { if (this.closePromise === drain)
|
|
80
|
+
this.closePromise = undefined; });
|
|
81
|
+
return drain;
|
|
82
|
+
}
|
|
83
|
+
async drainOwners() {
|
|
84
|
+
const initial = new Set(this.owners.keys());
|
|
85
|
+
const stopping = Promise.allSettled([...initial].map(id => this.stopOwner(id)));
|
|
86
|
+
// A pending spawn may need cancellation to finish acquisition/cleanup. Do
|
|
87
|
+
// not wait for its preparation before notifying the exact local owner.
|
|
88
|
+
// Report a failed notification immediately so close() can be retried even
|
|
89
|
+
// while a sibling cancellation is pending. Each sibling retains its own
|
|
90
|
+
// single-flight promise; rejecting this drain does not abandon that work.
|
|
91
|
+
await Promise.all([...this.pendingSpawns.keys()].map(id => this.stopPendingSpawn(id)));
|
|
92
|
+
const stopped = await stopping;
|
|
93
|
+
const stopFailure = stopped.find(result => result.status === "rejected");
|
|
94
|
+
if (stopFailure?.status === "rejected")
|
|
95
|
+
throw stopFailure.reason;
|
|
96
|
+
await Promise.allSettled([...this.preparations]);
|
|
97
|
+
const late = await Promise.allSettled([...this.owners.keys()].filter(id => !initial.has(id)).map(id => this.stopOwner(id)));
|
|
98
|
+
const failed = late.find(result => result.status === "rejected");
|
|
99
|
+
if (failed?.status === "rejected")
|
|
100
|
+
throw failed.reason;
|
|
101
|
+
// spawn() rejection is a drained preparation, not a fabricated process
|
|
102
|
+
// exit. The host must settle its owned effects before rejecting spawn().
|
|
103
|
+
for (const runId of this.interruptedPreparations) {
|
|
104
|
+
const run = await this.ctx.runStore.getRun(runId);
|
|
105
|
+
if (run?.status === "killed" && run.result?.stderr === "Killed during shutdown")
|
|
106
|
+
await this.acknowledgeRun(run);
|
|
107
|
+
this.interruptedPreparations.delete(runId);
|
|
108
|
+
}
|
|
109
|
+
const owners = [...this.owners.values()];
|
|
110
|
+
if (owners.some(owner => !owner.exited))
|
|
111
|
+
throw new Error("Runner shutdown requires an onExit receipt from the spawner");
|
|
112
|
+
await Promise.all(owners.map(owner => owner.exited));
|
|
113
|
+
}
|
|
114
|
+
stopPendingSpawn(runId) {
|
|
115
|
+
const pending = this.pendingSpawns.get(runId);
|
|
116
|
+
if (!pending)
|
|
117
|
+
return Promise.resolve();
|
|
118
|
+
if (pending.stopping)
|
|
119
|
+
return pending.stopping;
|
|
120
|
+
const stopping = (async () => {
|
|
121
|
+
if (!this.ctx.spawner.cancelRun)
|
|
122
|
+
throw new Error("Pending runner shutdown requires owner-scoped cancelRun from the spawner");
|
|
123
|
+
this.interruptedPreparations.add(runId);
|
|
124
|
+
await this.ctx.runStore.completeRun(runId, "killed", {
|
|
125
|
+
exitCode: 1, stdout: "", stderr: "Killed during shutdown", duration: 0,
|
|
126
|
+
});
|
|
127
|
+
await this.ctx.spawner.cancelRun(runId);
|
|
128
|
+
})();
|
|
129
|
+
pending.stopping = stopping;
|
|
130
|
+
void stopping.catch(() => { if (pending.stopping === stopping)
|
|
131
|
+
pending.stopping = undefined; });
|
|
132
|
+
return stopping;
|
|
133
|
+
}
|
|
134
|
+
stopOwner(runId) {
|
|
135
|
+
const owner = this.owners.get(runId);
|
|
136
|
+
if (!owner)
|
|
137
|
+
return Promise.resolve();
|
|
138
|
+
if (owner.stopping)
|
|
139
|
+
return owner.stopping;
|
|
140
|
+
const stopping = this.drainOwner(runId, owner);
|
|
141
|
+
owner.stopping = stopping;
|
|
142
|
+
void stopping.catch(() => { if (owner.stopping === stopping)
|
|
143
|
+
owner.stopping = undefined; });
|
|
144
|
+
return stopping;
|
|
145
|
+
}
|
|
146
|
+
async drainOwner(runId, owner) {
|
|
147
|
+
if (!owner.exited || !this.ctx.spawner.cancelRun) {
|
|
148
|
+
throw new Error("Runner shutdown requires owner-scoped cancelRun and an onExit receipt from the spawner");
|
|
149
|
+
}
|
|
150
|
+
// Keep the receipt after exit until post-exit persistence is confirmed.
|
|
151
|
+
// A failed acknowledgement must be retried before storage can close.
|
|
152
|
+
owner.shutdownPending = true;
|
|
153
|
+
await this.ctx.runStore.completeRun(runId, "killed", {
|
|
154
|
+
exitCode: 1, stdout: "", stderr: "Killed during shutdown", duration: 0,
|
|
155
|
+
});
|
|
156
|
+
if (!owner.exitConfirmed)
|
|
157
|
+
await this.ctx.spawner.cancelRun(runId);
|
|
158
|
+
await owner.exited;
|
|
159
|
+
const run = await this.ctx.runStore.getRun(runId);
|
|
160
|
+
// A competing successful/failed completion wins and remains collectable.
|
|
161
|
+
if (run?.status === "killed" && run.result?.stderr === "Killed during shutdown")
|
|
162
|
+
await this.acknowledgeRun(run);
|
|
163
|
+
if (this.owners.get(runId) === owner)
|
|
164
|
+
this.owners.delete(runId);
|
|
165
|
+
}
|
|
166
|
+
ownSpawn(runId, taskId, handle) {
|
|
167
|
+
const owner = { exitConfirmed: false, shutdownPending: this.closing };
|
|
168
|
+
this.owners.set(runId, owner);
|
|
169
|
+
if (handle.onExit)
|
|
170
|
+
owner.exited = new Promise(resolve => {
|
|
171
|
+
handle.onExit(() => {
|
|
172
|
+
owner.exitConfirmed = true;
|
|
173
|
+
if (!owner.shutdownPending && this.owners.get(runId) === owner)
|
|
174
|
+
this.owners.delete(runId);
|
|
175
|
+
try {
|
|
176
|
+
this.ctx.emitter.emit("run:exited", { taskId, runId, pid: handle.pid });
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
resolve();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
approvals;
|
|
185
|
+
setApprovalManager(manager) { this.approvals = manager; }
|
|
56
186
|
staleWarned = new Set();
|
|
57
187
|
/** Last known activity snapshot per taskId, used to diff and emit SSE events */
|
|
58
188
|
lastActivity = new Map();
|
|
@@ -69,6 +199,8 @@ export class TaskRunner {
|
|
|
69
199
|
constructor(ctx) {
|
|
70
200
|
this.ctx = ctx;
|
|
71
201
|
}
|
|
202
|
+
prepareMissionExecution;
|
|
203
|
+
setMissionExecutionPreparer(prepare) { this.prepareMissionExecution = prepare; }
|
|
72
204
|
async resolveTaskExecutionRoute(task, agent) {
|
|
73
205
|
const route = await resolveTaskExecutionRoute({ task, agent }, {
|
|
74
206
|
getProjectLoop: this.ctx.getProjectLoop,
|
|
@@ -84,12 +216,56 @@ export class TaskRunner {
|
|
|
84
216
|
* The callback is typically the assessment pipeline (handleResult).
|
|
85
217
|
*/
|
|
86
218
|
async collectResults(onResult) {
|
|
219
|
+
if (this.closing)
|
|
220
|
+
return;
|
|
87
221
|
const terminalRuns = await this.ctx.runStore.getTerminalRuns();
|
|
88
222
|
for (const run of terminalRuns) {
|
|
223
|
+
// Shutdown interruptions belong to recovery, not ordinary assessment.
|
|
224
|
+
// An assessment already admitted before stop() is still drained by the host.
|
|
225
|
+
if (this.closing)
|
|
226
|
+
return;
|
|
227
|
+
let execution;
|
|
228
|
+
const sid = run.sessionId ?? run.activity.sessionId;
|
|
229
|
+
if (run.taskExecutionRevision !== undefined) {
|
|
230
|
+
if (!isTaskExecutionStore(this.ctx.taskStore)) {
|
|
231
|
+
throw new TaskExecutionError("task_execution_unsupported", "Result collection requires execution persistence");
|
|
232
|
+
}
|
|
233
|
+
execution = { taskId: run.taskId, revision: run.taskExecutionRevision, runId: run.id };
|
|
234
|
+
try {
|
|
235
|
+
await this.ctx.taskStore.mutateTaskExecution(execution, { type: "patch", updates: {
|
|
236
|
+
...(sid ? { sessionId: sid } : {}), ...(run.outcomes ? { outcomes: run.outcomes } : {}),
|
|
237
|
+
} });
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (!(error instanceof TaskExecutionError) || error.code !== "task_execution_conflict")
|
|
241
|
+
throw error;
|
|
242
|
+
await this.acknowledgeRun(run);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else if (isTaskExecutionStore(this.ctx.taskStore)) {
|
|
247
|
+
const task = await this.ctx.taskStore.getTask(run.taskId);
|
|
248
|
+
if (!task || task.executionRevision !== undefined || task.activeRunId !== undefined) {
|
|
249
|
+
// An old, unversioned writer cannot attach its result to a new attempt.
|
|
250
|
+
await this.acknowledgeRun(run);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
execution = taskExecutionReference(task);
|
|
254
|
+
try {
|
|
255
|
+
await this.ctx.taskStore.mutateTaskExecution(execution, { type: "patch", updates: {
|
|
256
|
+
...(sid ? { sessionId: sid } : {}), ...(run.outcomes ? { outcomes: run.outcomes } : {}),
|
|
257
|
+
} });
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (!(error instanceof TaskExecutionError) || error.code !== "task_execution_conflict")
|
|
261
|
+
throw error;
|
|
262
|
+
await this.acknowledgeRun(run);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
89
266
|
await this.emitGuardrailEvents(run);
|
|
90
267
|
// Persist sessionId on the task before acknowledging the run.
|
|
91
|
-
|
|
92
|
-
if (sid) {
|
|
268
|
+
if (!execution && sid) {
|
|
93
269
|
try {
|
|
94
270
|
await this.ctx.taskStore.updateTask(run.taskId, { sessionId: sid });
|
|
95
271
|
}
|
|
@@ -98,13 +274,13 @@ export class TaskRunner {
|
|
|
98
274
|
// Persist auto-collected outcomes on the task.
|
|
99
275
|
// REPLACE (not append) — each execution produces its own definitive outcomes.
|
|
100
276
|
// Appending caused "exponential outcome" accumulation across retries/fix cycles.
|
|
101
|
-
if (run.outcomes && run.outcomes.length > 0) {
|
|
277
|
+
if (!execution && run.outcomes && run.outcomes.length > 0) {
|
|
102
278
|
try {
|
|
103
279
|
await this.ctx.taskStore.updateTask(run.taskId, { outcomes: run.outcomes });
|
|
104
280
|
}
|
|
105
281
|
catch { /* task may already be gone */ }
|
|
106
282
|
}
|
|
107
|
-
const result = run.result
|
|
283
|
+
const result = run.result ? { ...run.result } : {
|
|
108
284
|
exitCode: 1,
|
|
109
285
|
stdout: "",
|
|
110
286
|
stderr: `Run ${run.id} ended with status ${run.status} without a result`,
|
|
@@ -126,19 +302,21 @@ export class TaskRunner {
|
|
|
126
302
|
result.stderr = (result.stderr ? result.stderr + "\n" : "") + diagnosis;
|
|
127
303
|
}
|
|
128
304
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (this.ctx.runStore.markRunCollected) {
|
|
134
|
-
await this.ctx.runStore.markRunCollected(run.id);
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
await this.ctx.runStore.deleteRun(run.id);
|
|
305
|
+
if (execution)
|
|
306
|
+
await onResult(run.taskId, result, execution);
|
|
307
|
+
else
|
|
308
|
+
await onResult(run.taskId, result);
|
|
138
309
|
}
|
|
139
|
-
this.
|
|
310
|
+
await this.acknowledgeRun(run);
|
|
140
311
|
}
|
|
141
312
|
}
|
|
313
|
+
async acknowledgeRun(run) {
|
|
314
|
+
if (this.ctx.runStore.markRunCollected)
|
|
315
|
+
await this.ctx.runStore.markRunCollected(run.id);
|
|
316
|
+
else
|
|
317
|
+
await this.ctx.runStore.deleteRun(run.id);
|
|
318
|
+
this.staleWarned.delete(run.taskId);
|
|
319
|
+
}
|
|
142
320
|
async emitGuardrailEvents(run) {
|
|
143
321
|
const byId = new Map();
|
|
144
322
|
const sid = run.sessionId ?? run.activity.sessionId;
|
|
@@ -383,9 +561,38 @@ export class TaskRunner {
|
|
|
383
561
|
* Resets orphaned tasks to pending WITHOUT burning retry count.
|
|
384
562
|
*/
|
|
385
563
|
async recoverOrphanedTasks() {
|
|
564
|
+
let recovered = 0;
|
|
565
|
+
const orphanStates = new Set(["assigned", "in_progress", "review"]);
|
|
386
566
|
// Check RunStore active runs first
|
|
387
567
|
const activeRuns = await this.ctx.runStore.getActiveRuns();
|
|
388
568
|
for (const run of activeRuns) {
|
|
569
|
+
if (run.taskExecutionRevision !== undefined) {
|
|
570
|
+
const store = this.ctx.taskStore;
|
|
571
|
+
if (!isTaskExecutionStore(store))
|
|
572
|
+
throw new TaskExecutionError("task_execution_unsupported", "Recovery requires execution persistence");
|
|
573
|
+
const task = await store.getTask(run.taskId);
|
|
574
|
+
if (!task || task.activeRunId !== run.id || task.executionRevision !== run.taskExecutionRevision
|
|
575
|
+
|| !orphanStates.has(task.status) || this.isProcessAlive(run.pid))
|
|
576
|
+
continue;
|
|
577
|
+
try {
|
|
578
|
+
const mutation = { type: "requeue", countRetry: false, reason: "Orphan recovery", onlyIfRunRunning: true };
|
|
579
|
+
const changed = isConditionalTaskExecutionStore(store)
|
|
580
|
+
? await store.mutateTaskExecutionConditionally(taskExecutionReference(task), mutation, [{ taskId: task.id, snapshot: task }])
|
|
581
|
+
: await store.mutateTaskExecution(taskExecutionReference(task), mutation);
|
|
582
|
+
if (changed.run)
|
|
583
|
+
await this.acknowledgeRun(changed.run);
|
|
584
|
+
const checkpoint = usableCheckpoint(changed.run?.resumeState);
|
|
585
|
+
if (checkpoint)
|
|
586
|
+
this.pendingResume.set(task.id, { state: checkpoint, agentIdentity: changed.run?.config?.agentIdentity });
|
|
587
|
+
this.ctx.emitter.emit("task:recovered", { taskId: task.id, title: task.title, previousStatus: task.status });
|
|
588
|
+
recovered++;
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
if (!(error instanceof TaskExecutionError) || error.code !== "task_execution_conflict")
|
|
592
|
+
throw error;
|
|
593
|
+
}
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
389
596
|
if (this.isProcessAlive(run.pid)) {
|
|
390
597
|
// Runner still alive — leave it running, work is NOT lost!
|
|
391
598
|
this.ctx.emitter.emit("log", { level: "info", message: `Runner PID ${run.pid} still alive for task ${run.taskId} — reconnecting` });
|
|
@@ -395,7 +602,7 @@ export class TaskRunner {
|
|
|
395
602
|
// so the respawn resumes at turn + 1 instead of starting over.
|
|
396
603
|
const checkpoint = usableCheckpoint(run.resumeState);
|
|
397
604
|
if (checkpoint) {
|
|
398
|
-
this.pendingResume.set(run.taskId, checkpoint);
|
|
605
|
+
this.pendingResume.set(run.taskId, { state: checkpoint, agentIdentity: run.config?.agentIdentity });
|
|
399
606
|
this.ctx.emitter.emit("log", {
|
|
400
607
|
level: "info",
|
|
401
608
|
message: `[${run.taskId}] Runner died mid-run — checkpoint at turn ${checkpoint.turn + 1} saved for resume`,
|
|
@@ -415,30 +622,53 @@ export class TaskRunner {
|
|
|
415
622
|
}
|
|
416
623
|
}
|
|
417
624
|
}
|
|
418
|
-
//
|
|
419
|
-
|
|
420
|
-
for (const proc of state.processes) {
|
|
421
|
-
if (proc.pid > 0 && proc.alive) {
|
|
422
|
-
this.killOrphanProcess(proc.pid, proc.agentName);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
625
|
+
// Persisted process-list PIDs are not local ownership receipts. Do not
|
|
626
|
+
// signal them: a PID may have been reused or belong to another supervisor.
|
|
425
627
|
const tasks = await this.ctx.taskStore.listTasks();
|
|
426
|
-
const orphanStates = new Set(["assigned", "in_progress", "review"]);
|
|
427
|
-
let recovered = 0;
|
|
428
628
|
for (const task of tasks) {
|
|
429
629
|
if (!orphanStates.has(task.status))
|
|
430
630
|
continue;
|
|
431
631
|
// Check if there's a live runner for this task
|
|
432
|
-
const run = await this.ctx.runStore.
|
|
433
|
-
|
|
632
|
+
const run = task.activeRunId ? await this.ctx.runStore.getRun(task.activeRunId)
|
|
633
|
+
: await this.ctx.runStore.getRunByTaskId(task.id);
|
|
634
|
+
const matches = run && (run.taskExecutionRevision === undefined
|
|
635
|
+
? task.executionRevision === undefined && task.activeRunId === undefined
|
|
636
|
+
: run.taskExecutionRevision === task.executionRevision && run.id === task.activeRunId);
|
|
637
|
+
// Completed work must still traverse assessment and completion approvals.
|
|
638
|
+
// Preserve failed results too: their retry policy is not orphan recovery.
|
|
639
|
+
if (matches && run.status !== "running" && !run.collectedAt)
|
|
640
|
+
continue;
|
|
641
|
+
if (matches && run.status === "running" && this.isProcessAlive(run.pid)) {
|
|
434
642
|
// Runner still working — skip recovery for this task
|
|
435
643
|
continue;
|
|
436
644
|
}
|
|
437
645
|
// Recover: reset to pending WITHOUT incrementing retries.
|
|
438
646
|
// Shutdown interrupts are not real failures — unsafeSetStatus bypasses
|
|
439
647
|
// transition(failed → pending) which would burn a retry.
|
|
648
|
+
const store = this.ctx.taskStore;
|
|
649
|
+
try {
|
|
650
|
+
if (isTaskExecutionStore(store)) {
|
|
651
|
+
const mutation = { type: "requeue", countRetry: false, reason: "Orphan recovery", ...(run?.status === "running" ? { onlyIfRunRunning: true } : {}) };
|
|
652
|
+
if (isConditionalTaskExecutionStore(store))
|
|
653
|
+
await store.mutateTaskExecutionConditionally(taskExecutionReference(task), mutation, [{ taskId: task.id, snapshot: task }]);
|
|
654
|
+
else
|
|
655
|
+
await store.mutateTaskExecution(taskExecutionReference(task), mutation);
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
await store.unsafeSetStatus(task.id, "pending", "orphan recovery — shutdown interrupt");
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
if (error instanceof TaskExecutionError && error.code === "task_execution_conflict")
|
|
663
|
+
continue;
|
|
664
|
+
throw error;
|
|
665
|
+
}
|
|
666
|
+
if (matches && run.collectedAt) {
|
|
667
|
+
const checkpoint = usableCheckpoint(run.resumeState);
|
|
668
|
+
if (checkpoint)
|
|
669
|
+
this.pendingResume.set(task.id, { state: checkpoint, agentIdentity: run.config?.agentIdentity });
|
|
670
|
+
}
|
|
440
671
|
this.ctx.emitter.emit("task:recovered", { taskId: task.id, title: task.title, previousStatus: task.status });
|
|
441
|
-
await this.ctx.taskStore.unsafeSetStatus(task.id, "pending", "orphan recovery — shutdown interrupt");
|
|
442
672
|
recovered++;
|
|
443
673
|
}
|
|
444
674
|
// Clear stale process list
|
|
@@ -459,7 +689,11 @@ export class TaskRunner {
|
|
|
459
689
|
return this.ctx.spawner.isAlive(pid);
|
|
460
690
|
}
|
|
461
691
|
/** Persist failures that happen before a host process/sandbox can start. */
|
|
462
|
-
async failBeforeSpawn(task, message) {
|
|
692
|
+
async failBeforeSpawn(task, message, claimedRun) {
|
|
693
|
+
if (claimedRun) {
|
|
694
|
+
await this.ctx.runStore.completeRun(claimedRun.id, "failed", { exitCode: 1, stdout: "", stderr: message, duration: 0 });
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
463
697
|
await this.ctx.taskStore.transition(task.id, "assigned");
|
|
464
698
|
await this.ctx.taskStore.transition(task.id, "in_progress");
|
|
465
699
|
await this.ctx.taskStore.updateTask(task.id, { phase: "execution" });
|
|
@@ -488,50 +722,124 @@ export class TaskRunner {
|
|
|
488
722
|
duration: 0,
|
|
489
723
|
});
|
|
490
724
|
}
|
|
491
|
-
|
|
492
|
-
|
|
725
|
+
spawnForTask(task, overrides) {
|
|
726
|
+
if (this.closing)
|
|
727
|
+
return Promise.reject(new Error("Task runner is closing"));
|
|
728
|
+
const work = this.dispatchTask(task, overrides);
|
|
729
|
+
this.preparations.add(work);
|
|
730
|
+
void work.then(() => this.preparations.delete(work), () => this.preparations.delete(work));
|
|
731
|
+
return work;
|
|
732
|
+
}
|
|
733
|
+
async dispatchTask(task, overrides) {
|
|
734
|
+
if (task.missionExecutionId && !this.prepareMissionExecution) {
|
|
735
|
+
throw new MissionExecutionError("mission_execution_unsupported", "Mission execution must be prepared before dispatch");
|
|
736
|
+
}
|
|
737
|
+
const executions = this.ctx.taskStore;
|
|
738
|
+
if (!this.approvals && (task.approvalState || this.ctx.config.settings.approvalGates?.some(gate => gate.hook === "task:spawn"))) {
|
|
739
|
+
throw new TaskExecutionError("task_execution_unsupported", "Task approval coordinator is required");
|
|
740
|
+
}
|
|
741
|
+
if (!isTaskExecutionStore(executions)) {
|
|
742
|
+
if (task.activeRunId !== undefined || task.executionRevision !== undefined || task.approvalState !== undefined) {
|
|
743
|
+
throw new TaskExecutionError("task_execution_unsupported", "Task execution persistence is required for this Task");
|
|
744
|
+
}
|
|
745
|
+
try {
|
|
746
|
+
await this.prepareAndSpawn(task, undefined, overrides);
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
await this.failBeforeSpawn(task, error instanceof Error ? error.message : "Task preparation failed");
|
|
750
|
+
}
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
// Claim before any asynchronous preparation. The stored Task is the source
|
|
754
|
+
// of the principal, assignment and input; the scheduler's snapshot may lag.
|
|
755
|
+
const claimed = await claimTaskDispatch(this.ctx, task, nanoid(), this.approvals);
|
|
756
|
+
try {
|
|
757
|
+
await this.prepareAndSpawn(claimed.task, claimed.run, overrides ?? claimed.overrides);
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
await this.ctx.runStore.completeRun(claimed.run.id, "failed", {
|
|
761
|
+
exitCode: 1, stdout: "", stderr: error instanceof Error ? error.message : "Task preparation failed", duration: 0,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
async claimIsActive(task, run) {
|
|
766
|
+
return isTaskDispatchActive(this.ctx, task, run);
|
|
767
|
+
}
|
|
768
|
+
async prepareAndSpawn(task, initialRun, overrides) {
|
|
769
|
+
const claimed = initialRun !== undefined;
|
|
770
|
+
const callerContext = { user: task.user, metadata: normalizeRuntimeMetadata(task.metadata) };
|
|
771
|
+
const executionIdentity = claimed ? { id: task.id, executionRevision: task.executionRevision, activeRunId: task.activeRunId } : {};
|
|
772
|
+
task = { ...task, ...callerContext, ...executionIdentity };
|
|
773
|
+
if (task.missionExecutionId) {
|
|
774
|
+
if (!this.prepareMissionExecution)
|
|
775
|
+
throw new MissionExecutionError("mission_execution_unsupported", "Mission execution must be prepared before dispatch");
|
|
776
|
+
await this.prepareMissionExecution(task);
|
|
777
|
+
}
|
|
778
|
+
const store = this.ctx.agentStore;
|
|
779
|
+
const versioned = isVersionedAgentStore(store);
|
|
780
|
+
const snapshot = versioned ? await store.getAgentSnapshot(task.assignTo) : undefined;
|
|
781
|
+
let agent = versioned ? snapshot?.agent : await store.getAgent(task.assignTo);
|
|
493
782
|
if (!agent) {
|
|
494
783
|
const message = `No agent "${task.assignTo}" for task "${task.title}"`;
|
|
495
784
|
this.ctx.emitter.emit("log", { level: "error", message });
|
|
496
|
-
await this.failBeforeSpawn(task, message);
|
|
785
|
+
await this.failBeforeSpawn(task, message, initialRun);
|
|
497
786
|
return;
|
|
498
787
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
}
|
|
788
|
+
const agentIdentity = snapshot ? agentIdentityFromSnapshot(snapshot) : undefined;
|
|
789
|
+
const pendingResume = this.pendingResume.get(task.id);
|
|
790
|
+
try {
|
|
791
|
+
if (agent.name !== task.assignTo)
|
|
792
|
+
throw new AgentIdentityError();
|
|
793
|
+
if (pendingResume && (versioned || pendingResume.agentIdentity))
|
|
794
|
+
assertAgentIdentity(pendingResume.agentIdentity, snapshot);
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
this.pendingResume.delete(task.id);
|
|
798
|
+
await this.failBeforeSpawn(task, error instanceof Error ? error.message : "Agent identity check failed", initialRun);
|
|
799
|
+
return;
|
|
512
800
|
}
|
|
513
|
-
|
|
514
|
-
const
|
|
515
|
-
|
|
801
|
+
let executionRoute;
|
|
802
|
+
const invocation = await authorizeTaskDispatch(this.ctx, {
|
|
803
|
+
source: task, run: initialRun, invocation: { task, agent, agentIdentity, overrides },
|
|
804
|
+
prepare: async (effective) => {
|
|
805
|
+
// Resolve the effective model, loop and sandbox BEFORE consent. The
|
|
806
|
+
// returned values are used unchanged after approval, including resume.
|
|
807
|
+
if (effective.agent.model && this.ctx.validateProviderKeys) {
|
|
808
|
+
const resolved = resolveConfiguredModelSelection(effective.agent.model, this.ctx.config.settings, effective.agent.allowedModelProfiles);
|
|
809
|
+
const missing = this.ctx.validateProviderKeys([...resolved.policy.candidates]);
|
|
810
|
+
if (missing.length)
|
|
811
|
+
throw new Error(`Missing API key for ${missing.map(m => `${m.provider} (${m.modelSpec})`).join(", ")}`);
|
|
812
|
+
}
|
|
813
|
+
executionRoute = await this.resolveTaskExecutionRoute(effective.task, effective.agent);
|
|
814
|
+
return { ...effective, task: { ...effective.task,
|
|
815
|
+
...(executionRoute?.mode === "loop" ? { loop: executionRoute.loop } : {}),
|
|
816
|
+
executionMode: resolveExecutionMode(effective.task, effective.agent, this.ctx.config.settings),
|
|
817
|
+
sandbox: resolveRuntimeSandboxOptions(this.ctx.config.settings, effective.agent, effective.task),
|
|
818
|
+
} };
|
|
819
|
+
},
|
|
820
|
+
}, this.approvals);
|
|
821
|
+
task = invocation.task;
|
|
822
|
+
agent = invocation.agent;
|
|
823
|
+
if (!invocation.allowed) {
|
|
516
824
|
this.ctx.emitter.emit("log", {
|
|
517
825
|
level: "info",
|
|
518
|
-
message: `[${task.id}] Spawn blocked by hook: ${
|
|
826
|
+
message: `[${task.id}] Spawn blocked by hook: ${invocation.reason ?? "no reason"}`,
|
|
519
827
|
});
|
|
520
|
-
return; //
|
|
828
|
+
return; // Approval, veto, or cancellation prevents provider execution.
|
|
521
829
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
830
|
+
if (!claimed) {
|
|
831
|
+
await this.ctx.taskStore.transition(task.id, "assigned");
|
|
832
|
+
await this.ctx.taskStore.transition(task.id, "in_progress");
|
|
833
|
+
if (!task.phase)
|
|
834
|
+
await this.ctx.taskStore.updateTask(task.id, { phase: "execution" });
|
|
527
835
|
}
|
|
528
|
-
const runId = nanoid();
|
|
836
|
+
const runId = initialRun?.id ?? nanoid();
|
|
529
837
|
// Create per-task output directory for deliverables
|
|
530
838
|
const outputDir = pathJoin(this.ctx.polpoDir, "output", task.id);
|
|
531
839
|
// Establish the durable Run before memory, mission context, storage or
|
|
532
840
|
// sandbox preparation. Any subsequent failure is therefore inspectable.
|
|
533
|
-
const startedAt = new Date().toISOString();
|
|
534
|
-
|
|
841
|
+
const startedAt = initialRun?.startedAt ?? new Date().toISOString();
|
|
842
|
+
initialRun ??= {
|
|
535
843
|
id: runId,
|
|
536
844
|
taskId: task.id,
|
|
537
845
|
pid: 0,
|
|
@@ -547,8 +855,11 @@ export class TaskRunner {
|
|
|
547
855
|
engine: "agent",
|
|
548
856
|
delivery: "background",
|
|
549
857
|
};
|
|
550
|
-
|
|
858
|
+
if (!claimed)
|
|
859
|
+
await this.ctx.runStore.upsertRun(initialRun);
|
|
551
860
|
try {
|
|
861
|
+
if (claimed && !await this.claimIsActive(task, initialRun))
|
|
862
|
+
return;
|
|
552
863
|
let runtimeContext;
|
|
553
864
|
try {
|
|
554
865
|
runtimeContext = await resolveRuntimeContext(this.ctx.runtimeContext, {
|
|
@@ -563,7 +874,8 @@ export class TaskRunner {
|
|
|
563
874
|
catch {
|
|
564
875
|
throw new Error("Runtime context retrieval failed");
|
|
565
876
|
}
|
|
566
|
-
|
|
877
|
+
if (claimed && !await this.claimIsActive(task, initialRun))
|
|
878
|
+
return;
|
|
567
879
|
const contextTrust = this.ctx.config.settings.contextTrust === "enforce"
|
|
568
880
|
? "enforce"
|
|
569
881
|
: "off";
|
|
@@ -571,12 +883,7 @@ export class TaskRunner {
|
|
|
571
883
|
// metadata structural; disabled runs preserve the historical task prompt.
|
|
572
884
|
const promptContextSegments = [];
|
|
573
885
|
const legacyContextParts = [];
|
|
574
|
-
const taskWithContext =
|
|
575
|
-
...task,
|
|
576
|
-
...(executionRoute?.mode === "loop"
|
|
577
|
-
? { loop: executionRoute.loop }
|
|
578
|
-
: {}),
|
|
579
|
-
};
|
|
886
|
+
const taskWithContext = task;
|
|
580
887
|
// 1. Shared memory (persistent cross-session knowledge, visible to all agents)
|
|
581
888
|
const sharedMemory = replacesLegacySharedMemory(runtimeContext)
|
|
582
889
|
? ""
|
|
@@ -611,7 +918,9 @@ export class TaskRunner {
|
|
|
611
918
|
if (task.group) {
|
|
612
919
|
try {
|
|
613
920
|
// Resolve mission via direct ID (preferred) or group name (legacy fallback)
|
|
614
|
-
const mission =
|
|
921
|
+
const mission = task.missionExecutionId
|
|
922
|
+
? (await resolveMissionExecutionStore(this.ctx)?.getMissionExecutionById(task.missionExecutionId))?.expected
|
|
923
|
+
: await resolveMissionForTask(resolveMissionStore(this.ctx), task);
|
|
615
924
|
const legacyMissionParts = [];
|
|
616
925
|
// Original user prompt that generated this mission (the "why")
|
|
617
926
|
if (mission?.prompt) {
|
|
@@ -627,7 +936,7 @@ export class TaskRunner {
|
|
|
627
936
|
}
|
|
628
937
|
// Sibling tasks — just titles and statuses for awareness, not full descriptions
|
|
629
938
|
const allTasks = await this.ctx.taskStore.listTasks();
|
|
630
|
-
const siblings = allTasks.filter(t => t.group === task.group && t.id !== task.id);
|
|
939
|
+
const siblings = allTasks.filter(t => (task.missionExecutionId ? t.missionExecutionId === task.missionExecutionId : t.group === task.group) && t.id !== task.id);
|
|
631
940
|
if (siblings.length > 0) {
|
|
632
941
|
const missionParts = [`Other tasks in this mission:`];
|
|
633
942
|
for (const s of siblings) {
|
|
@@ -662,7 +971,7 @@ export class TaskRunner {
|
|
|
662
971
|
// Durable turns: consume (one-shot) a checkpoint harvested by orphan
|
|
663
972
|
// recovery — the runner resumes the conversation at turn + 1 instead
|
|
664
973
|
// of redoing completed work. Absent checkpoint = spawn from zero.
|
|
665
|
-
const resumeState =
|
|
974
|
+
const resumeState = pendingResume?.state;
|
|
666
975
|
if (resumeState) {
|
|
667
976
|
this.pendingResume.delete(task.id);
|
|
668
977
|
const from = resumeState.pipelineName
|
|
@@ -674,8 +983,8 @@ export class TaskRunner {
|
|
|
674
983
|
});
|
|
675
984
|
}
|
|
676
985
|
// Adaptive isolation: resolve WHERE this run executes (task > agent > settings)
|
|
677
|
-
const executionMode =
|
|
678
|
-
const sandbox =
|
|
986
|
+
const executionMode = task.executionMode;
|
|
987
|
+
const sandbox = task.sandbox;
|
|
679
988
|
const runnerConfig = {
|
|
680
989
|
runId,
|
|
681
990
|
taskId: task.id,
|
|
@@ -688,6 +997,7 @@ export class TaskRunner {
|
|
|
688
997
|
runtimeContext,
|
|
689
998
|
executionRoute,
|
|
690
999
|
agent,
|
|
1000
|
+
...(agentIdentity ? { agentIdentity } : {}),
|
|
691
1001
|
task: taskForRun,
|
|
692
1002
|
polpoDir: this.ctx.polpoDir,
|
|
693
1003
|
cwd: this.ctx.agentWorkDir,
|
|
@@ -704,13 +1014,54 @@ export class TaskRunner {
|
|
|
704
1014
|
};
|
|
705
1015
|
// Persist the complete input before handing control to the host. This is
|
|
706
1016
|
// also the recovery/debug snapshot when host acquisition fails.
|
|
707
|
-
await this.
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
|
|
1017
|
+
if (claimed && !await this.claimIsActive(task, initialRun))
|
|
1018
|
+
return;
|
|
1019
|
+
if (this.ctx.runStore.updateRunConfig) {
|
|
1020
|
+
if (!await this.ctx.runStore.updateRunConfig(runId, runnerConfig, initialRun.configPath))
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
else {
|
|
1024
|
+
// Compatibility for third-party stores predating updateRunConfig. This
|
|
1025
|
+
// legacy registration path cannot provide the update-only fence; hosts
|
|
1026
|
+
// requiring that guarantee must require the optional store capability.
|
|
1027
|
+
await this.ctx.runStore.upsertRun({
|
|
1028
|
+
...initialRun,
|
|
1029
|
+
executionMode,
|
|
1030
|
+
config: runnerConfig,
|
|
1031
|
+
updatedAt: new Date().toISOString(),
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
if (claimed && !await this.claimIsActive(task, initialRun))
|
|
1035
|
+
return;
|
|
1036
|
+
if (this.closing) {
|
|
1037
|
+
await this.ctx.runStore.completeRun(runId, "killed", { exitCode: 1, stdout: "", stderr: "Killed during shutdown", duration: 0 });
|
|
1038
|
+
const interrupted = await this.ctx.runStore.getRun(runId);
|
|
1039
|
+
if (interrupted?.status === "killed" && interrupted.result?.stderr === "Killed during shutdown")
|
|
1040
|
+
await this.acknowledgeRun(interrupted);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const pending = {};
|
|
1044
|
+
this.pendingSpawns.set(runId, pending);
|
|
1045
|
+
let spawnResult;
|
|
1046
|
+
try {
|
|
1047
|
+
spawnResult = await this.ctx.spawner.spawn(runnerConfig);
|
|
1048
|
+
this.ownSpawn(runId, task.id, spawnResult);
|
|
1049
|
+
this.interruptedPreparations.delete(runId);
|
|
1050
|
+
}
|
|
1051
|
+
finally {
|
|
1052
|
+
if (this.pendingSpawns.get(runId) === pending)
|
|
1053
|
+
this.pendingSpawns.delete(runId);
|
|
1054
|
+
}
|
|
1055
|
+
if (this.closing)
|
|
1056
|
+
await this.stopOwner(runId);
|
|
1057
|
+
// A host may finish acquisition after cancellation. Only this spawn's
|
|
1058
|
+
// returned handle is local authority to kill; a persisted foreign PID is not.
|
|
1059
|
+
if (claimed && (await this.ctx.runStore.getRun(runId))?.status === "killed") {
|
|
1060
|
+
if (this.ctx.spawner.cancelRun)
|
|
1061
|
+
await this.ctx.spawner.cancelRun(runId);
|
|
1062
|
+
else
|
|
1063
|
+
this.ctx.spawner.kill(spawnResult.pid);
|
|
1064
|
+
}
|
|
714
1065
|
// Never upsert the whole record here: an in-process runner can complete
|
|
715
1066
|
// before spawn() returns. Updating only spawn metadata cannot resurrect
|
|
716
1067
|
// that terminal Run back to "running".
|
|
@@ -728,12 +1079,6 @@ export class TaskRunner {
|
|
|
728
1079
|
});
|
|
729
1080
|
}
|
|
730
1081
|
}
|
|
731
|
-
// Wake the supervisor as soon as the runner process exits so results
|
|
732
|
-
// are collected immediately (poll interval remains the safety net for
|
|
733
|
-
// spawners that don't track process lifecycle).
|
|
734
|
-
spawnResult.onExit?.(() => {
|
|
735
|
-
this.ctx.emitter.emit("run:exited", { taskId: task.id, runId, pid: spawnResult.pid });
|
|
736
|
-
});
|
|
737
1082
|
this.ctx.emitter.emit("agent:spawned", {
|
|
738
1083
|
taskId: task.id,
|
|
739
1084
|
agentName: agent.name,
|
|
@@ -751,11 +1096,5 @@ export class TaskRunner {
|
|
|
751
1096
|
});
|
|
752
1097
|
}
|
|
753
1098
|
}
|
|
754
|
-
killOrphanProcess(pid, agentName) {
|
|
755
|
-
if (!this.ctx.spawner.isAlive(pid))
|
|
756
|
-
return;
|
|
757
|
-
this.ctx.emitter.emit("log", { level: "warn", message: `Killing orphan process PID ${pid} (${agentName})` });
|
|
758
|
-
this.ctx.spawner.kill(pid);
|
|
759
|
-
}
|
|
760
1099
|
}
|
|
761
1100
|
//# sourceMappingURL=task-runner.js.map
|