@useorgx/openclaw-plugin 0.4.8 → 0.4.9
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/dashboard/dist/assets/B5NEElEI.css +1 -0
- package/dashboard/dist/assets/BhapSNAs.js +215 -0
- package/dashboard/dist/assets/{BNeJ0kpF.js → iFdvE7lx.js} +1 -1
- package/dashboard/dist/assets/{CUV9IHHi.js → jRJsmpYM.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/activity-store.js +4 -18
- package/dist/agent-context-store.js +5 -25
- package/dist/agent-run-store.js +5 -25
- package/dist/agent-suite.js +1 -8
- package/dist/auth/flows.d.ts +47 -0
- package/dist/auth/flows.js +169 -0
- package/dist/auth-store.js +6 -26
- package/dist/byok-store.js +5 -19
- package/dist/cli/orgx.d.ts +66 -0
- package/dist/cli/orgx.js +91 -0
- package/dist/config/refresh.d.ts +32 -0
- package/dist/config/refresh.js +55 -0
- package/dist/config/resolution.d.ts +37 -0
- package/dist/config/resolution.js +178 -0
- package/dist/contracts/shared-types.d.ts +147 -0
- package/dist/contracts/shared-types.js +3 -0
- package/dist/contracts/types.d.ts +1 -134
- package/dist/contracts/types.js +5 -0
- package/dist/entities/auto-assignment.d.ts +36 -0
- package/dist/entities/auto-assignment.js +115 -0
- package/dist/entity-comment-store.js +5 -25
- package/dist/hash-utils.d.ts +2 -0
- package/dist/hash-utils.js +12 -0
- package/dist/http/helpers/activity-headline.d.ts +10 -0
- package/dist/http/helpers/activity-headline.js +192 -0
- package/dist/http/helpers/artifact-fallback.d.ts +13 -0
- package/dist/http/helpers/artifact-fallback.js +148 -0
- package/dist/http/helpers/auto-continue-engine.d.ts +298 -0
- package/dist/http/helpers/auto-continue-engine.js +1218 -0
- package/dist/http/helpers/autopilot-operations.d.ts +157 -0
- package/dist/http/helpers/autopilot-operations.js +403 -0
- package/dist/http/helpers/autopilot-runtime.d.ts +42 -0
- package/dist/http/helpers/autopilot-runtime.js +319 -0
- package/dist/http/helpers/autopilot-slice-utils.d.ts +38 -0
- package/dist/http/helpers/autopilot-slice-utils.js +476 -0
- package/dist/http/helpers/decision-mapper.d.ts +12 -0
- package/dist/http/helpers/decision-mapper.js +44 -0
- package/dist/http/helpers/dispatch-lifecycle.d.ts +102 -0
- package/dist/http/helpers/dispatch-lifecycle.js +604 -0
- package/dist/http/helpers/hash-utils.d.ts +1 -0
- package/dist/http/helpers/hash-utils.js +1 -0
- package/dist/http/helpers/kickoff-context.d.ts +12 -0
- package/dist/http/helpers/kickoff-context.js +154 -0
- package/dist/http/helpers/mission-control.d.ts +94 -0
- package/dist/http/helpers/mission-control.js +894 -0
- package/dist/http/helpers/openclaw-cli.d.ts +37 -0
- package/dist/http/helpers/openclaw-cli.js +283 -0
- package/dist/http/helpers/runtime-sse.d.ts +20 -0
- package/dist/http/helpers/runtime-sse.js +110 -0
- package/dist/http/helpers/value-utils.d.ts +6 -0
- package/dist/http/helpers/value-utils.js +67 -0
- package/dist/http/index.d.ts +88 -0
- package/dist/http/index.js +2353 -0
- package/dist/http/router.d.ts +23 -0
- package/dist/http/router.js +23 -0
- package/dist/http/routes/agent-control.d.ts +79 -0
- package/dist/http/routes/agent-control.js +684 -0
- package/dist/http/routes/agent-suite.d.ts +29 -0
- package/dist/http/routes/agent-suite.js +198 -0
- package/dist/http/routes/agents-catalog.d.ts +40 -0
- package/dist/http/routes/agents-catalog.js +83 -0
- package/dist/http/routes/billing.d.ts +23 -0
- package/dist/http/routes/billing.js +55 -0
- package/dist/http/routes/debug.d.ts +14 -0
- package/dist/http/routes/debug.js +21 -0
- package/dist/http/routes/decision-actions.d.ts +13 -0
- package/dist/http/routes/decision-actions.js +66 -0
- package/dist/http/routes/delegation.d.ts +19 -0
- package/dist/http/routes/delegation.js +32 -0
- package/dist/http/routes/entities.d.ts +47 -0
- package/dist/http/routes/entities.js +152 -0
- package/dist/http/routes/entity-dynamic.d.ts +25 -0
- package/dist/http/routes/entity-dynamic.js +191 -0
- package/dist/http/routes/health.d.ts +22 -0
- package/dist/http/routes/health.js +49 -0
- package/dist/http/routes/live-legacy.d.ts +110 -0
- package/dist/http/routes/live-legacy.js +598 -0
- package/dist/http/routes/live-misc.d.ts +69 -0
- package/dist/http/routes/live-misc.js +206 -0
- package/dist/http/routes/live-snapshot.d.ts +90 -0
- package/dist/http/routes/live-snapshot.js +297 -0
- package/dist/http/routes/mission-control-actions.d.ts +83 -0
- package/dist/http/routes/mission-control-actions.js +541 -0
- package/dist/http/routes/mission-control-read.d.ts +28 -0
- package/dist/http/routes/mission-control-read.js +67 -0
- package/dist/http/routes/onboarding.d.ts +34 -0
- package/dist/http/routes/onboarding.js +101 -0
- package/dist/http/routes/run-control.d.ts +24 -0
- package/dist/http/routes/run-control.js +86 -0
- package/dist/http/routes/runtime-hooks.d.ts +69 -0
- package/dist/http/routes/runtime-hooks.js +437 -0
- package/dist/http/routes/settings-byok.d.ts +23 -0
- package/dist/http/routes/settings-byok.js +163 -0
- package/dist/http/routes/summary.d.ts +18 -0
- package/dist/http/routes/summary.js +42 -0
- package/dist/http/routes/work-artifacts.d.ts +9 -0
- package/dist/http/routes/work-artifacts.js +36 -0
- package/dist/http/shared-state.d.ts +16 -0
- package/dist/http/shared-state.js +1 -0
- package/dist/http-handler.d.ts +1 -88
- package/dist/http-handler.js +1 -10605
- package/dist/index.js +108 -2243
- package/dist/json-utils.d.ts +1 -0
- package/dist/json-utils.js +8 -0
- package/dist/next-up-queue-store.js +4 -18
- package/dist/runtime-instance-store.js +5 -31
- package/dist/services/background.d.ts +23 -0
- package/dist/services/background.js +23 -0
- package/dist/services/instrumentation.d.ts +29 -0
- package/dist/services/instrumentation.js +136 -0
- package/dist/snapshot-store.js +5 -25
- package/dist/stores/json-store.d.ts +11 -0
- package/dist/stores/json-store.js +42 -0
- package/dist/sync/outbox-replay.d.ts +55 -0
- package/dist/sync/outbox-replay.js +514 -0
- package/dist/tools/core-tools.d.ts +76 -0
- package/dist/tools/core-tools.js +1005 -0
- package/package.json +1 -1
- package/dashboard/dist/assets/BzkiMPmM.js +0 -215
- package/dashboard/dist/assets/Ie7d9Iq2.css +0 -1
|
@@ -0,0 +1,1218 @@
|
|
|
1
|
+
import { randomUUID as randomUuidFn } from "node:crypto";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { upsertAgentContext } from "../../agent-context-store.js";
|
|
5
|
+
import { readOpenClawGatewayPort, readOpenClawSettingsSnapshot, } from "../../openclaw-settings.js";
|
|
6
|
+
import { resolveRuntimeHookToken, } from "../../runtime-instance-store.js";
|
|
7
|
+
import { detectMcpHandshakeFailure, shouldKillWorker } from "../../worker-supervisor.js";
|
|
8
|
+
import { getOrgxPluginConfigDir } from "../../paths.js";
|
|
9
|
+
import { buildMissionControlGraph, DEFAULT_TOKEN_BUDGET_ASSUMPTIONS, dedupeStrings, deriveExecutionPolicy, isDispatchableWorkstreamStatus, isDoneStatus, isTodoStatus, readBudgetEnvNumber, summarizeSpawnGuardBlockReason, } from "./mission-control.js";
|
|
10
|
+
import { createAutopilotRuntime } from "./autopilot-runtime.js";
|
|
11
|
+
import { buildWorkstreamSlicePrompt, createCodexBinResolver, ensureAutopilotSliceSchemaPath, fileUpdatedAtEpochMs, parseSliceResult, readFileTailSafe, readSliceOutputFile, } from "./autopilot-slice-utils.js";
|
|
12
|
+
import { pickString } from "./value-utils.js";
|
|
13
|
+
export function createAutoContinueEngine(deps) {
|
|
14
|
+
const { client, safeErrorMessage, pidAlive, stopProcess, resolveOrgxAgentForDomain, checkSpawnGuardSafe, syncParentRollupsForTask, emitActivitySafe, requestDecisionSafe, registerArtifactSafe, applyAgentStatusUpdatesSafe, upsertRuntimeInstanceFromHook, broadcastRuntimeSse, clearSnapshotResponseCache, resolveByokEnvOverrides, } = deps;
|
|
15
|
+
const randomUUID = deps.randomUUID ?? randomUuidFn;
|
|
16
|
+
const __filename = deps.filename;
|
|
17
|
+
const autoContinueRuns = new Map();
|
|
18
|
+
const localInitiativeStatusOverrides = new Map();
|
|
19
|
+
let autoContinueTickInFlight = null;
|
|
20
|
+
const AUTO_CONTINUE_TICK_MS = readBudgetEnvNumber("ORGX_AUTO_CONTINUE_TICK_MS", 2_500, {
|
|
21
|
+
min: 250,
|
|
22
|
+
max: 60_000,
|
|
23
|
+
});
|
|
24
|
+
const autoContinueSliceRuns = new Map();
|
|
25
|
+
// Keep child handles alive so stdout/stderr capture remains reliable even when the process is detached.
|
|
26
|
+
const autoContinueSliceChildren = new Map();
|
|
27
|
+
const autoContinueSliceLastHeartbeatMs = new Map();
|
|
28
|
+
const clearAutoContinueSliceTransientState = (sliceRunId) => {
|
|
29
|
+
const id = (sliceRunId ?? "").trim();
|
|
30
|
+
if (!id)
|
|
31
|
+
return;
|
|
32
|
+
autoContinueSliceChildren.delete(id);
|
|
33
|
+
autoContinueSliceLastHeartbeatMs.delete(id);
|
|
34
|
+
};
|
|
35
|
+
const AUTO_CONTINUE_SLICE_MAX_TASKS = 6;
|
|
36
|
+
const AUTO_CONTINUE_SLICE_TIMEOUT_MS = readBudgetEnvNumber("ORGX_AUTOPILOT_SLICE_TIMEOUT_MS", 55 * 60_000,
|
|
37
|
+
// Keep test runs fast; real-world defaults are still ~1h unless overridden.
|
|
38
|
+
{ min: 250, max: 6 * 60 * 60_000 });
|
|
39
|
+
const AUTO_CONTINUE_SLICE_LOG_STALL_MS = readBudgetEnvNumber("ORGX_AUTOPILOT_SLICE_LOG_STALL_MS", 6 * 60_000,
|
|
40
|
+
// Stall detection is only enforced when explicitly overridden; keep lower bound permissive for tests.
|
|
41
|
+
{ min: 20, max: 60 * 60_000 });
|
|
42
|
+
const AUTO_CONTINUE_SLICE_HEARTBEAT_MS = 12_000;
|
|
43
|
+
const AUTO_CONTINUE_SLICE_SCHEMA_FILENAME = "autopilot-slice-schema.json";
|
|
44
|
+
const AUTO_CONTINUE_SLICE_LOG_DIRNAME = "autopilot-logs";
|
|
45
|
+
const setLocalInitiativeStatusOverride = (initiativeId, status) => {
|
|
46
|
+
const normalizedId = initiativeId.trim();
|
|
47
|
+
if (!normalizedId)
|
|
48
|
+
return;
|
|
49
|
+
localInitiativeStatusOverrides.set(normalizedId, {
|
|
50
|
+
status,
|
|
51
|
+
updatedAt: new Date().toISOString(),
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
const clearLocalInitiativeStatusOverride = (initiativeId) => {
|
|
55
|
+
const normalizedId = initiativeId.trim();
|
|
56
|
+
if (!normalizedId)
|
|
57
|
+
return;
|
|
58
|
+
localInitiativeStatusOverrides.delete(normalizedId);
|
|
59
|
+
};
|
|
60
|
+
const applyLocalInitiativeOverrides = (rows) => {
|
|
61
|
+
const seenIds = new Set();
|
|
62
|
+
const next = rows.map((row) => {
|
|
63
|
+
const id = pickString(row, ["id"]);
|
|
64
|
+
if (!id)
|
|
65
|
+
return row;
|
|
66
|
+
seenIds.add(id);
|
|
67
|
+
const override = localInitiativeStatusOverrides.get(id);
|
|
68
|
+
if (!override)
|
|
69
|
+
return row;
|
|
70
|
+
return {
|
|
71
|
+
...row,
|
|
72
|
+
status: override.status,
|
|
73
|
+
updated_at: pickString(row, ["updated_at", "updatedAt"]) ?? override.updatedAt,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
for (const [id, override] of localInitiativeStatusOverrides.entries()) {
|
|
77
|
+
if (seenIds.has(id))
|
|
78
|
+
continue;
|
|
79
|
+
next.push({
|
|
80
|
+
id,
|
|
81
|
+
title: `Initiative ${id.slice(0, 8)}`,
|
|
82
|
+
name: `Initiative ${id.slice(0, 8)}`,
|
|
83
|
+
summary: null,
|
|
84
|
+
status: override.status,
|
|
85
|
+
progress_pct: null,
|
|
86
|
+
created_at: override.updatedAt,
|
|
87
|
+
updated_at: override.updatedAt,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return next;
|
|
91
|
+
};
|
|
92
|
+
const applyLocalInitiativeOverrideToGraph = (graph) => {
|
|
93
|
+
const override = localInitiativeStatusOverrides.get(graph.initiative.id) ?? null;
|
|
94
|
+
if (!override)
|
|
95
|
+
return graph;
|
|
96
|
+
return {
|
|
97
|
+
...graph,
|
|
98
|
+
initiative: {
|
|
99
|
+
...graph.initiative,
|
|
100
|
+
status: override.status,
|
|
101
|
+
},
|
|
102
|
+
nodes: graph.nodes.map((node) => node.type === "initiative" && node.id === graph.initiative.id
|
|
103
|
+
? { ...node, status: override.status }
|
|
104
|
+
: node),
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
function normalizeTokenBudget(value, fallback) {
|
|
108
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
109
|
+
return Math.max(1_000, Math.round(value));
|
|
110
|
+
}
|
|
111
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
112
|
+
const parsed = Number(value);
|
|
113
|
+
if (Number.isFinite(parsed)) {
|
|
114
|
+
return Math.max(1_000, Math.round(parsed));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return Math.max(1_000, Math.round(fallback));
|
|
118
|
+
}
|
|
119
|
+
function defaultAutoContinueTokenBudget() {
|
|
120
|
+
const hours = readBudgetEnvNumber("ORGX_AUTO_CONTINUE_BUDGET_HOURS", 4, {
|
|
121
|
+
min: 0.05,
|
|
122
|
+
max: 24,
|
|
123
|
+
});
|
|
124
|
+
const fallback = DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.tokensPerHour *
|
|
125
|
+
hours *
|
|
126
|
+
DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.contingencyMultiplier;
|
|
127
|
+
return normalizeTokenBudget(process.env.ORGX_AUTO_CONTINUE_TOKEN_BUDGET, fallback);
|
|
128
|
+
}
|
|
129
|
+
function estimateTokensForDurationHours(durationHours) {
|
|
130
|
+
if (!Number.isFinite(durationHours) || durationHours <= 0)
|
|
131
|
+
return 0;
|
|
132
|
+
const raw = durationHours *
|
|
133
|
+
DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.tokensPerHour *
|
|
134
|
+
DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.contingencyMultiplier;
|
|
135
|
+
return Math.max(0, Math.round(raw));
|
|
136
|
+
}
|
|
137
|
+
// Helpers used by previous task-level auto-continue implementation were removed in v2.
|
|
138
|
+
// readOpenClawSessionSummary was used by the previous task-level auto-continue implementation.
|
|
139
|
+
// Autopilot v2 dispatches workstream slices via codex and does not rely on OpenClaw session JSONL.
|
|
140
|
+
async function fetchInitiativeEntity(initiativeId) {
|
|
141
|
+
try {
|
|
142
|
+
const list = await client.listEntities("initiative", { limit: 200 });
|
|
143
|
+
const match = list.data.find((candidate) => String(candidate?.id ?? "") === initiativeId);
|
|
144
|
+
return match ?? null;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function updateInitiativeMetadata(initiativeId, patch) {
|
|
151
|
+
const existing = await fetchInitiativeEntity(initiativeId);
|
|
152
|
+
const existingMetaRaw = existing && typeof existing === "object"
|
|
153
|
+
? existing.metadata
|
|
154
|
+
: null;
|
|
155
|
+
const existingMeta = existingMetaRaw && typeof existingMetaRaw === "object" && !Array.isArray(existingMetaRaw)
|
|
156
|
+
? existingMetaRaw
|
|
157
|
+
: {};
|
|
158
|
+
const nextMeta = { ...existingMeta, ...patch };
|
|
159
|
+
await client.updateEntity("initiative", initiativeId, { metadata: nextMeta });
|
|
160
|
+
}
|
|
161
|
+
async function updateInitiativeAutoContinueState(input) {
|
|
162
|
+
const now = new Date().toISOString();
|
|
163
|
+
const patch = {
|
|
164
|
+
auto_continue_enabled: input.run.status === "running" || input.run.status === "stopping",
|
|
165
|
+
auto_continue_status: input.run.status,
|
|
166
|
+
auto_continue_stop_reason: input.run.stopReason,
|
|
167
|
+
auto_continue_started_at: input.run.startedAt,
|
|
168
|
+
auto_continue_stopped_at: input.run.stoppedAt,
|
|
169
|
+
auto_continue_updated_at: now,
|
|
170
|
+
auto_continue_token_budget: input.run.tokenBudget,
|
|
171
|
+
auto_continue_tokens_used: input.run.tokensUsed,
|
|
172
|
+
auto_continue_active_task_id: input.run.activeTaskId,
|
|
173
|
+
auto_continue_active_run_id: input.run.activeRunId,
|
|
174
|
+
auto_continue_active_task_token_estimate: input.run.activeTaskTokenEstimate,
|
|
175
|
+
auto_continue_last_task_id: input.run.lastTaskId,
|
|
176
|
+
auto_continue_last_run_id: input.run.lastRunId,
|
|
177
|
+
auto_continue_include_verification: input.run.includeVerification,
|
|
178
|
+
auto_continue_workstream_filter: input.run.allowedWorkstreamIds,
|
|
179
|
+
...(input.run.lastError ? { auto_continue_last_error: input.run.lastError } : {}),
|
|
180
|
+
};
|
|
181
|
+
await updateInitiativeMetadata(input.initiativeId, patch);
|
|
182
|
+
}
|
|
183
|
+
async function stopAutoContinueRun(input) {
|
|
184
|
+
const now = new Date().toISOString();
|
|
185
|
+
const activeRunId = input.run.activeRunId;
|
|
186
|
+
input.run.status = "stopped";
|
|
187
|
+
input.run.stopReason = input.reason;
|
|
188
|
+
input.run.stoppedAt = now;
|
|
189
|
+
input.run.updatedAt = now;
|
|
190
|
+
input.run.stopRequested = false;
|
|
191
|
+
input.run.activeRunId = null;
|
|
192
|
+
input.run.activeTaskId = null;
|
|
193
|
+
input.run.activeTaskTokenEstimate = null;
|
|
194
|
+
if (input.error)
|
|
195
|
+
input.run.lastError = input.error;
|
|
196
|
+
clearAutoContinueSliceTransientState(activeRunId);
|
|
197
|
+
// Only pause the initiative on non-terminal stops (error, blocked, user-requested).
|
|
198
|
+
// Completed / budget-exhausted runs should not override the initiative status.
|
|
199
|
+
if (input.reason !== "completed" && input.reason !== "budget_exhausted") {
|
|
200
|
+
try {
|
|
201
|
+
await client.updateEntity("initiative", input.run.initiativeId, {
|
|
202
|
+
status: "paused",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// best effort
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
await updateInitiativeAutoContinueState({
|
|
211
|
+
initiativeId: input.run.initiativeId,
|
|
212
|
+
run: input.run,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// best effort
|
|
217
|
+
}
|
|
218
|
+
const scopeSuffix = Array.isArray(input.run.allowedWorkstreamIds) && input.run.allowedWorkstreamIds.length === 1
|
|
219
|
+
? ` (${input.run.allowedWorkstreamIds[0]})`
|
|
220
|
+
: "";
|
|
221
|
+
const message = input.reason === "completed"
|
|
222
|
+
? `Autopilot stopped: current dispatch scope completed${scopeSuffix}.`
|
|
223
|
+
: input.reason === "budget_exhausted"
|
|
224
|
+
? `Autopilot stopped: token budget exhausted (${input.run.tokensUsed}/${input.run.tokenBudget}).`
|
|
225
|
+
: input.reason === "stopped"
|
|
226
|
+
? `Autopilot stopped by user request${scopeSuffix}.`
|
|
227
|
+
: input.reason === "blocked"
|
|
228
|
+
? `Autopilot stopped: blocked pending decision${scopeSuffix}.`
|
|
229
|
+
: `Autopilot stopped due to error${scopeSuffix}.`;
|
|
230
|
+
const phase = input.reason === "completed"
|
|
231
|
+
? "completed"
|
|
232
|
+
: input.reason === "blocked" || input.reason === "error"
|
|
233
|
+
? "blocked"
|
|
234
|
+
: "review";
|
|
235
|
+
const level = input.reason === "completed"
|
|
236
|
+
? "info"
|
|
237
|
+
: input.reason === "budget_exhausted" || input.reason === "stopped"
|
|
238
|
+
? "warn"
|
|
239
|
+
: "error";
|
|
240
|
+
await emitActivitySafe({
|
|
241
|
+
initiativeId: input.run.initiativeId,
|
|
242
|
+
runId: activeRunId ?? input.run.lastRunId ?? undefined,
|
|
243
|
+
correlationId: activeRunId ?? input.run.lastRunId ?? undefined,
|
|
244
|
+
phase,
|
|
245
|
+
level,
|
|
246
|
+
message,
|
|
247
|
+
metadata: {
|
|
248
|
+
event: "auto_continue_stopped",
|
|
249
|
+
stop_reason: input.reason,
|
|
250
|
+
requested_by_agent_id: input.run.agentId,
|
|
251
|
+
requested_by_agent_name: input.run.agentName,
|
|
252
|
+
active_run_id: activeRunId,
|
|
253
|
+
last_run_id: input.run.lastRunId,
|
|
254
|
+
token_budget: input.run.tokenBudget,
|
|
255
|
+
tokens_used: input.run.tokensUsed,
|
|
256
|
+
allowed_workstream_ids: input.run.allowedWorkstreamIds,
|
|
257
|
+
last_error: input.run.lastError,
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const codexBinResolver = createCodexBinResolver();
|
|
262
|
+
const resolveCodexBinInfo = () => codexBinResolver.resolveCodexBinInfo();
|
|
263
|
+
const { spawnCodexSliceWorker, writeRuntimeEvent } = createAutopilotRuntime({
|
|
264
|
+
filename: __filename,
|
|
265
|
+
autoContinueSliceChildren,
|
|
266
|
+
resolveByokEnvOverrides,
|
|
267
|
+
safeErrorMessage,
|
|
268
|
+
resolveCodexBinInfo,
|
|
269
|
+
upsertRuntimeInstanceFromHook,
|
|
270
|
+
broadcastRuntimeSse,
|
|
271
|
+
clearSnapshotResponseCache,
|
|
272
|
+
});
|
|
273
|
+
async function tickAutoContinueRun(run) {
|
|
274
|
+
if (run.status !== "running" && run.status !== "stopping")
|
|
275
|
+
return;
|
|
276
|
+
const now = new Date().toISOString();
|
|
277
|
+
// 1) If we have an active slice, wait for it to finish and then register outcomes.
|
|
278
|
+
if (run.activeRunId) {
|
|
279
|
+
const slice = autoContinueSliceRuns.get(run.activeRunId) ?? null;
|
|
280
|
+
if (!slice) {
|
|
281
|
+
// Legacy/unknown pointer; clear so we can continue.
|
|
282
|
+
run.activeRunId = null;
|
|
283
|
+
run.activeTaskId = null;
|
|
284
|
+
run.updatedAt = now;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
const pid = slice.pid;
|
|
288
|
+
if (pid && pidAlive(pid)) {
|
|
289
|
+
const nowMs = Date.now();
|
|
290
|
+
const outputTail = readFileTailSafe(slice.outputPath, 240_000);
|
|
291
|
+
const outputParsed = outputTail
|
|
292
|
+
? parseSliceResult(outputTail)
|
|
293
|
+
: null;
|
|
294
|
+
const outputComplete = Boolean(outputParsed &&
|
|
295
|
+
typeof outputParsed.status === "string" &&
|
|
296
|
+
typeof outputParsed.summary === "string");
|
|
297
|
+
if (outputComplete) {
|
|
298
|
+
// Some platforms can report a just-finished detached process as still "alive" (zombie).
|
|
299
|
+
// Best-effort stop, then clear pid so we can proceed to parse the output contract below.
|
|
300
|
+
try {
|
|
301
|
+
await stopProcess(pid);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// best effort
|
|
305
|
+
}
|
|
306
|
+
slice.pid = null;
|
|
307
|
+
autoContinueSliceRuns.set(slice.runId, slice);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
const lastHeartbeat = autoContinueSliceLastHeartbeatMs.get(slice.runId) ?? 0;
|
|
311
|
+
if (nowMs - lastHeartbeat >= AUTO_CONTINUE_SLICE_HEARTBEAT_MS) {
|
|
312
|
+
try {
|
|
313
|
+
writeRuntimeEvent({
|
|
314
|
+
sourceClient: slice.sourceClient,
|
|
315
|
+
event: "heartbeat",
|
|
316
|
+
runId: slice.runId,
|
|
317
|
+
initiativeId: slice.initiativeId,
|
|
318
|
+
workstreamId: slice.workstreamId,
|
|
319
|
+
taskId: slice.taskIds[0] ?? null,
|
|
320
|
+
agentId: slice.agentId,
|
|
321
|
+
agentName: slice.agentName,
|
|
322
|
+
phase: "execution",
|
|
323
|
+
message: `Autopilot slice running: ${slice.workstreamTitle ?? slice.workstreamId}`,
|
|
324
|
+
metadata: {
|
|
325
|
+
event: "autopilot_slice_heartbeat",
|
|
326
|
+
requested_by_agent_id: run.agentId,
|
|
327
|
+
requested_by_agent_name: run.agentName,
|
|
328
|
+
domain: slice.domain,
|
|
329
|
+
required_skills: slice.requiredSkills,
|
|
330
|
+
workstream_id: slice.workstreamId,
|
|
331
|
+
workstream_title: slice.workstreamTitle ?? null,
|
|
332
|
+
task_ids: slice.taskIds,
|
|
333
|
+
milestone_ids: slice.milestoneIds,
|
|
334
|
+
log_path: slice.logPath,
|
|
335
|
+
output_path: slice.outputPath,
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// best effort
|
|
341
|
+
}
|
|
342
|
+
autoContinueSliceLastHeartbeatMs.set(slice.runId, nowMs);
|
|
343
|
+
}
|
|
344
|
+
const startedAtEpochMs = Date.parse(slice.startedAt);
|
|
345
|
+
const fallbackEpochMs = Number.isFinite(startedAtEpochMs) ? startedAtEpochMs : nowMs;
|
|
346
|
+
const outputUpdatedAtEpochMs = fileUpdatedAtEpochMs(slice.outputPath, fallbackEpochMs);
|
|
347
|
+
// Treat stdout/output freshness as progress; stderr noise should not prevent stall detection.
|
|
348
|
+
const stallUpdatedAtEpochMs = outputUpdatedAtEpochMs;
|
|
349
|
+
const logTail = readFileTailSafe(slice.logPath, 64_000);
|
|
350
|
+
const mcpHandshake = detectMcpHandshakeFailure(logTail);
|
|
351
|
+
if (mcpHandshake) {
|
|
352
|
+
try {
|
|
353
|
+
await stopProcess(pid);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// best effort
|
|
357
|
+
}
|
|
358
|
+
slice.status = "error";
|
|
359
|
+
slice.finishedAt = now;
|
|
360
|
+
slice.updatedAt = now;
|
|
361
|
+
slice.lastError = `Autopilot slice failed to initialize MCP server${mcpHandshake.server ? ` (${mcpHandshake.server})` : ""}.`;
|
|
362
|
+
autoContinueSliceRuns.set(slice.runId, slice);
|
|
363
|
+
run.lastError = slice.lastError;
|
|
364
|
+
run.updatedAt = now;
|
|
365
|
+
clearAutoContinueSliceTransientState(slice.runId);
|
|
366
|
+
await emitActivitySafe({
|
|
367
|
+
initiativeId: run.initiativeId,
|
|
368
|
+
runId: slice.runId,
|
|
369
|
+
correlationId: slice.runId,
|
|
370
|
+
phase: "blocked",
|
|
371
|
+
level: "error",
|
|
372
|
+
message: `Autopilot slice MCP failed: ${slice.workstreamTitle ?? slice.workstreamId}.`,
|
|
373
|
+
metadata: {
|
|
374
|
+
event: "autopilot_slice_mcp_handshake_failed",
|
|
375
|
+
requested_by_agent_id: run.agentId,
|
|
376
|
+
requested_by_agent_name: run.agentName,
|
|
377
|
+
mcp_server: mcpHandshake.server,
|
|
378
|
+
mcp_line: mcpHandshake.line,
|
|
379
|
+
workstream_id: slice.workstreamId,
|
|
380
|
+
task_ids: slice.taskIds,
|
|
381
|
+
milestone_ids: slice.milestoneIds,
|
|
382
|
+
log_path: slice.logPath,
|
|
383
|
+
output_path: slice.outputPath,
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
await requestDecisionSafe({
|
|
387
|
+
initiativeId: run.initiativeId,
|
|
388
|
+
correlationId: slice.runId,
|
|
389
|
+
title: `Autopilot slice MCP failed: ${slice.workstreamTitle ?? slice.workstreamId}`,
|
|
390
|
+
summary: `MCP handshake failed${mcpHandshake.server ? ` for ${mcpHandshake.server}` : ""}. Review logs/output and decide whether to retry or pause autopilot.`,
|
|
391
|
+
urgency: "high",
|
|
392
|
+
options: [
|
|
393
|
+
"Retry this workstream slice",
|
|
394
|
+
"Pause autopilot and investigate",
|
|
395
|
+
"Skip this workstream for now",
|
|
396
|
+
],
|
|
397
|
+
blocking: true,
|
|
398
|
+
});
|
|
399
|
+
await stopAutoContinueRun({
|
|
400
|
+
run,
|
|
401
|
+
reason: "blocked",
|
|
402
|
+
error: slice.lastError,
|
|
403
|
+
});
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const killDecision = shouldKillWorker({
|
|
407
|
+
nowEpochMs: nowMs,
|
|
408
|
+
startedAtEpochMs: fallbackEpochMs,
|
|
409
|
+
logUpdatedAtEpochMs: stallUpdatedAtEpochMs,
|
|
410
|
+
}, { timeoutMs: AUTO_CONTINUE_SLICE_TIMEOUT_MS, stallMs: AUTO_CONTINUE_SLICE_LOG_STALL_MS });
|
|
411
|
+
if (killDecision.kill) {
|
|
412
|
+
try {
|
|
413
|
+
await stopProcess(pid);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// best effort
|
|
417
|
+
}
|
|
418
|
+
slice.status = "error";
|
|
419
|
+
slice.finishedAt = now;
|
|
420
|
+
slice.updatedAt = now;
|
|
421
|
+
slice.lastError =
|
|
422
|
+
killDecision.kind === "timeout"
|
|
423
|
+
? `Autopilot slice timed out after ${Math.round(AUTO_CONTINUE_SLICE_TIMEOUT_MS / 60_000)} minutes.`
|
|
424
|
+
: `Autopilot slice stalled (no output) for ${Math.round(AUTO_CONTINUE_SLICE_LOG_STALL_MS / 60_000)} minutes.`;
|
|
425
|
+
autoContinueSliceRuns.set(slice.runId, slice);
|
|
426
|
+
run.lastError = slice.lastError;
|
|
427
|
+
run.updatedAt = now;
|
|
428
|
+
clearAutoContinueSliceTransientState(slice.runId);
|
|
429
|
+
const event = killDecision.kind === "timeout" ? "autopilot_slice_timeout" : "autopilot_slice_log_stall";
|
|
430
|
+
const humanLabel = killDecision.kind === "timeout" ? "timed out" : "stalled";
|
|
431
|
+
await emitActivitySafe({
|
|
432
|
+
initiativeId: run.initiativeId,
|
|
433
|
+
runId: slice.runId,
|
|
434
|
+
correlationId: slice.runId,
|
|
435
|
+
phase: "blocked",
|
|
436
|
+
level: "error",
|
|
437
|
+
message: `Autopilot slice ${humanLabel}: ${slice.workstreamTitle ?? slice.workstreamId}.`,
|
|
438
|
+
metadata: {
|
|
439
|
+
event,
|
|
440
|
+
requested_by_agent_id: run.agentId,
|
|
441
|
+
requested_by_agent_name: run.agentName,
|
|
442
|
+
workstream_id: slice.workstreamId,
|
|
443
|
+
task_ids: slice.taskIds,
|
|
444
|
+
milestone_ids: slice.milestoneIds,
|
|
445
|
+
log_path: slice.logPath,
|
|
446
|
+
output_path: slice.outputPath,
|
|
447
|
+
reason: killDecision.reason,
|
|
448
|
+
elapsed_ms: killDecision.elapsedMs,
|
|
449
|
+
idle_ms: killDecision.idleMs,
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
await requestDecisionSafe({
|
|
453
|
+
initiativeId: run.initiativeId,
|
|
454
|
+
correlationId: slice.runId,
|
|
455
|
+
title: `Autopilot slice ${humanLabel}: ${slice.workstreamTitle ?? slice.workstreamId}`,
|
|
456
|
+
summary: "The slice was terminated because it stopped making progress. Review logs/output and decide whether to retry or pause autopilot.",
|
|
457
|
+
urgency: "high",
|
|
458
|
+
options: [
|
|
459
|
+
"Retry this workstream slice",
|
|
460
|
+
"Pause autopilot and investigate",
|
|
461
|
+
"Skip this workstream for now",
|
|
462
|
+
],
|
|
463
|
+
blocking: true,
|
|
464
|
+
});
|
|
465
|
+
await stopAutoContinueRun({
|
|
466
|
+
run,
|
|
467
|
+
reason: "blocked",
|
|
468
|
+
error: slice.lastError,
|
|
469
|
+
});
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (run.stopRequested) {
|
|
473
|
+
try {
|
|
474
|
+
await stopProcess(pid);
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
// best effort
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (!outputComplete)
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Slice finished.
|
|
485
|
+
const raw = readSliceOutputFile(slice.outputPath);
|
|
486
|
+
const parsed = raw ? parseSliceResult(raw) : null;
|
|
487
|
+
const parsedStatus = parsed?.status ?? "error";
|
|
488
|
+
const defaultDecisionBlocking = parsedStatus === "completed" ? false : true;
|
|
489
|
+
const decisions = Array.isArray(parsed?.decisions_needed)
|
|
490
|
+
? (parsed?.decisions_needed ?? [])
|
|
491
|
+
.filter((item) => Boolean(item && typeof item.question === "string" && item.question.trim()))
|
|
492
|
+
: [];
|
|
493
|
+
const blockingDecisionCount = decisions.filter((item) => typeof item.blocking === "boolean" ? item.blocking : defaultDecisionBlocking).length;
|
|
494
|
+
const nonBlockingDecisionCount = Math.max(0, decisions.length - blockingDecisionCount);
|
|
495
|
+
const effectiveParsedStatus = parsedStatus === "completed" && blockingDecisionCount > 0
|
|
496
|
+
? "needs_decision"
|
|
497
|
+
: parsedStatus;
|
|
498
|
+
slice.status =
|
|
499
|
+
effectiveParsedStatus === "completed"
|
|
500
|
+
? "completed"
|
|
501
|
+
: effectiveParsedStatus === "blocked" || effectiveParsedStatus === "needs_decision"
|
|
502
|
+
? "blocked"
|
|
503
|
+
: "error";
|
|
504
|
+
slice.finishedAt = now;
|
|
505
|
+
slice.updatedAt = now;
|
|
506
|
+
slice.lastError =
|
|
507
|
+
slice.status === "error"
|
|
508
|
+
? slice.lastError ?? "Autopilot slice failed or returned invalid output."
|
|
509
|
+
: null;
|
|
510
|
+
autoContinueSliceRuns.set(slice.runId, slice);
|
|
511
|
+
clearAutoContinueSliceTransientState(slice.runId);
|
|
512
|
+
// Token accounting: codex CLI doesn't provide tokens here; use the modeled estimate.
|
|
513
|
+
const modeledTokens = slice.tokenEstimate ?? run.activeTaskTokenEstimate ?? 0;
|
|
514
|
+
run.tokensUsed += Math.max(0, modeledTokens);
|
|
515
|
+
run.activeTaskTokenEstimate = null;
|
|
516
|
+
const artifacts = Array.isArray(parsed?.artifacts)
|
|
517
|
+
? (parsed?.artifacts ?? [])
|
|
518
|
+
.filter((item) => Boolean(item && typeof item.name === "string" && item.name.trim()))
|
|
519
|
+
: [];
|
|
520
|
+
const taskUpdates = Array.isArray(parsed?.task_updates)
|
|
521
|
+
? parsed.task_updates
|
|
522
|
+
: [];
|
|
523
|
+
const milestoneUpdates = Array.isArray(parsed?.milestone_updates)
|
|
524
|
+
? parsed.milestone_updates
|
|
525
|
+
: [];
|
|
526
|
+
for (const decision of decisions) {
|
|
527
|
+
await requestDecisionSafe({
|
|
528
|
+
initiativeId: run.initiativeId,
|
|
529
|
+
correlationId: slice.runId,
|
|
530
|
+
title: decision.question.trim(),
|
|
531
|
+
summary: decision.summary ?? parsed?.summary ?? null,
|
|
532
|
+
urgency: decision.urgency ?? "high",
|
|
533
|
+
options: Array.isArray(decision.options)
|
|
534
|
+
? decision.options.filter((opt) => typeof opt === "string" && opt.trim())
|
|
535
|
+
: [],
|
|
536
|
+
blocking: typeof decision.blocking === "boolean" ? decision.blocking : defaultDecisionBlocking,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
for (const artifact of artifacts) {
|
|
540
|
+
await registerArtifactSafe({
|
|
541
|
+
initiativeId: run.initiativeId,
|
|
542
|
+
runId: slice.runId,
|
|
543
|
+
agentId: slice.agentId,
|
|
544
|
+
agentName: slice.agentName,
|
|
545
|
+
workstreamId: slice.workstreamId,
|
|
546
|
+
artifact,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
const statusUpdateResult = await applyAgentStatusUpdatesSafe({
|
|
550
|
+
initiativeId: run.initiativeId,
|
|
551
|
+
runId: slice.runId,
|
|
552
|
+
correlationId: slice.runId,
|
|
553
|
+
taskUpdates,
|
|
554
|
+
milestoneUpdates,
|
|
555
|
+
});
|
|
556
|
+
try {
|
|
557
|
+
writeRuntimeEvent({
|
|
558
|
+
sourceClient: slice.sourceClient,
|
|
559
|
+
event: slice.status === "error" ? "error" : "session_stop",
|
|
560
|
+
runId: slice.runId,
|
|
561
|
+
initiativeId: slice.initiativeId,
|
|
562
|
+
workstreamId: slice.workstreamId,
|
|
563
|
+
taskId: slice.taskIds[0] ?? null,
|
|
564
|
+
agentId: slice.agentId,
|
|
565
|
+
agentName: slice.agentName ?? null,
|
|
566
|
+
phase: slice.status === "completed" ? "completed" : "blocked",
|
|
567
|
+
message: parsed?.summary ?? slice.lastError ?? "Autopilot slice finished.",
|
|
568
|
+
metadata: {
|
|
569
|
+
event: "autopilot_slice_finished",
|
|
570
|
+
requested_by_agent_id: run.agentId,
|
|
571
|
+
requested_by_agent_name: run.agentName,
|
|
572
|
+
status: effectiveParsedStatus,
|
|
573
|
+
artifacts: artifacts.length,
|
|
574
|
+
decisions: decisions.length,
|
|
575
|
+
blocking_decisions: blockingDecisionCount,
|
|
576
|
+
non_blocking_decisions: nonBlockingDecisionCount,
|
|
577
|
+
status_updates: statusUpdateResult.applied,
|
|
578
|
+
status_updates_buffered: statusUpdateResult.buffered,
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
// best effort
|
|
584
|
+
}
|
|
585
|
+
await emitActivitySafe({
|
|
586
|
+
initiativeId: run.initiativeId,
|
|
587
|
+
runId: slice.runId,
|
|
588
|
+
correlationId: slice.runId,
|
|
589
|
+
phase: slice.status === "completed" ? "completed" : "blocked",
|
|
590
|
+
level: slice.status === "completed" ? "info" : "warn",
|
|
591
|
+
message: slice.status === "completed"
|
|
592
|
+
? `Autopilot slice completed for ${slice.workstreamTitle ?? slice.workstreamId} (${slice.taskIds.length} task${slice.taskIds.length === 1 ? "" : "s"}).`
|
|
593
|
+
: `Autopilot slice blocked: ${slice.workstreamTitle ?? slice.workstreamId}.`,
|
|
594
|
+
metadata: {
|
|
595
|
+
event: "autopilot_slice_result",
|
|
596
|
+
requested_by_agent_id: run.agentId,
|
|
597
|
+
requested_by_agent_name: run.agentName,
|
|
598
|
+
agent_id: slice.agentId,
|
|
599
|
+
agent_name: slice.agentName,
|
|
600
|
+
domain: slice.domain,
|
|
601
|
+
required_skills: slice.requiredSkills,
|
|
602
|
+
workstream_id: slice.workstreamId,
|
|
603
|
+
task_ids: slice.taskIds,
|
|
604
|
+
milestone_ids: slice.milestoneIds,
|
|
605
|
+
parsed_status: effectiveParsedStatus,
|
|
606
|
+
has_output: Boolean(parsed),
|
|
607
|
+
artifacts: artifacts.length,
|
|
608
|
+
decisions: decisions.length,
|
|
609
|
+
blocking_decisions: blockingDecisionCount,
|
|
610
|
+
non_blocking_decisions: nonBlockingDecisionCount,
|
|
611
|
+
decision_required: blockingDecisionCount > 0,
|
|
612
|
+
status_updates_applied: statusUpdateResult.applied,
|
|
613
|
+
status_updates_buffered: statusUpdateResult.buffered,
|
|
614
|
+
output_path: slice.outputPath,
|
|
615
|
+
log_path: slice.logPath,
|
|
616
|
+
error: slice.lastError,
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
if (slice.status !== "completed") {
|
|
620
|
+
if (slice.status === "error" && decisions.length === 0) {
|
|
621
|
+
await requestDecisionSafe({
|
|
622
|
+
initiativeId: run.initiativeId,
|
|
623
|
+
correlationId: slice.runId,
|
|
624
|
+
title: `Autopilot slice failed: ${slice.workstreamTitle ?? slice.workstreamId}`,
|
|
625
|
+
summary: parsed?.summary ??
|
|
626
|
+
slice.lastError ??
|
|
627
|
+
"The slice failed without producing a valid output contract. Review logs/output and decide whether to retry or pause autopilot.",
|
|
628
|
+
urgency: "high",
|
|
629
|
+
options: [
|
|
630
|
+
"Retry this workstream slice",
|
|
631
|
+
"Pause autopilot and investigate",
|
|
632
|
+
"Skip this workstream for now",
|
|
633
|
+
],
|
|
634
|
+
blocking: true,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
await stopAutoContinueRun({
|
|
638
|
+
run,
|
|
639
|
+
reason: slice.status === "error" ? "error" : "blocked",
|
|
640
|
+
error: parsed?.summary ??
|
|
641
|
+
slice.lastError ??
|
|
642
|
+
`Slice returned status: ${effectiveParsedStatus}`,
|
|
643
|
+
});
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const completionHadNoOutcome = parsedStatus === "completed" &&
|
|
647
|
+
artifacts.length === 0 &&
|
|
648
|
+
decisions.length === 0 &&
|
|
649
|
+
statusUpdateResult.applied === 0;
|
|
650
|
+
if (!parsed || parsedStatus === "error" || completionHadNoOutcome) {
|
|
651
|
+
const attentionTitle = completionHadNoOutcome
|
|
652
|
+
? `Autopilot slice needs verification: ${slice.workstreamTitle ?? slice.workstreamId}`
|
|
653
|
+
: `Autopilot slice failed: ${slice.workstreamTitle ?? slice.workstreamId}`;
|
|
654
|
+
const attentionSummary = completionHadNoOutcome
|
|
655
|
+
? "The slice reported completion but did not produce artifacts or status updates. Decide whether to retry, request stronger output, or mark tasks manually."
|
|
656
|
+
: "The slice exited without a valid output contract. Review logs/output and decide whether to retry or pause autopilot.";
|
|
657
|
+
await requestDecisionSafe({
|
|
658
|
+
initiativeId: run.initiativeId,
|
|
659
|
+
correlationId: slice.runId,
|
|
660
|
+
title: attentionTitle,
|
|
661
|
+
summary: attentionSummary,
|
|
662
|
+
urgency: "high",
|
|
663
|
+
options: [
|
|
664
|
+
"Retry this workstream slice",
|
|
665
|
+
"Pause autopilot and investigate",
|
|
666
|
+
"Skip this workstream for now",
|
|
667
|
+
],
|
|
668
|
+
blocking: true,
|
|
669
|
+
});
|
|
670
|
+
await stopAutoContinueRun({
|
|
671
|
+
run,
|
|
672
|
+
reason: completionHadNoOutcome ? "blocked" : "error",
|
|
673
|
+
error: slice.lastError ??
|
|
674
|
+
(completionHadNoOutcome
|
|
675
|
+
? "Slice completed without verifiable outcomes."
|
|
676
|
+
: "Slice failed or returned invalid output."),
|
|
677
|
+
});
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
run.lastRunId = slice.runId;
|
|
681
|
+
run.lastTaskId = run.activeTaskId ?? run.lastTaskId;
|
|
682
|
+
run.activeRunId = null;
|
|
683
|
+
run.activeTaskId = null;
|
|
684
|
+
run.updatedAt = now;
|
|
685
|
+
try {
|
|
686
|
+
await updateInitiativeAutoContinueState({
|
|
687
|
+
initiativeId: run.initiativeId,
|
|
688
|
+
run,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
// best effort
|
|
693
|
+
}
|
|
694
|
+
if (run.stopAfterSlice) {
|
|
695
|
+
run.stopAfterSlice = false;
|
|
696
|
+
await stopAutoContinueRun({ run, reason: "completed" });
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (run.stopRequested) {
|
|
700
|
+
await stopAutoContinueRun({ run, reason: "stopped" });
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (run.stopRequested) {
|
|
706
|
+
run.status = "stopping";
|
|
707
|
+
run.updatedAt = now;
|
|
708
|
+
await stopAutoContinueRun({ run, reason: "stopped" });
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
// 2) Enforce token guardrail before starting a new slice.
|
|
712
|
+
if (run.tokensUsed >= run.tokenBudget) {
|
|
713
|
+
await stopAutoContinueRun({ run, reason: "budget_exhausted" });
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
// 3) Pick next workstream slice and dispatch.
|
|
717
|
+
let graph;
|
|
718
|
+
try {
|
|
719
|
+
graph = applyLocalInitiativeOverrideToGraph(await buildMissionControlGraph(client, run.initiativeId));
|
|
720
|
+
}
|
|
721
|
+
catch (err) {
|
|
722
|
+
await stopAutoContinueRun({
|
|
723
|
+
run,
|
|
724
|
+
reason: "error",
|
|
725
|
+
error: safeErrorMessage(err),
|
|
726
|
+
});
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const nodes = graph.nodes;
|
|
730
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
731
|
+
const taskNodes = nodes.filter((node) => node.type === "task");
|
|
732
|
+
const todoTasks = taskNodes.filter((node) => isTodoStatus(node.status));
|
|
733
|
+
if (todoTasks.length === 0) {
|
|
734
|
+
await stopAutoContinueRun({ run, reason: "completed" });
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const taskIsReady = (task) => task.dependencyIds.every((depId) => {
|
|
738
|
+
const dependency = nodeById.get(depId);
|
|
739
|
+
return dependency ? isDoneStatus(dependency.status) : true;
|
|
740
|
+
});
|
|
741
|
+
const taskHasBlockedParent = (task) => {
|
|
742
|
+
const milestone = task.milestoneId ? nodeById.get(task.milestoneId) ?? null : null;
|
|
743
|
+
const workstream = task.workstreamId ? nodeById.get(task.workstreamId) ?? null : null;
|
|
744
|
+
return (milestone?.status?.toLowerCase() === "blocked" ||
|
|
745
|
+
workstream?.status?.toLowerCase() === "blocked");
|
|
746
|
+
};
|
|
747
|
+
// Select the next eligible workstream by scanning ordered todos.
|
|
748
|
+
let selectedWorkstreamId = null;
|
|
749
|
+
for (const taskId of graph.recentTodos) {
|
|
750
|
+
const node = nodeById.get(taskId);
|
|
751
|
+
if (!node || node.type !== "task")
|
|
752
|
+
continue;
|
|
753
|
+
if (!isTodoStatus(node.status))
|
|
754
|
+
continue;
|
|
755
|
+
if (!run.includeVerification &&
|
|
756
|
+
typeof node.title === "string" &&
|
|
757
|
+
/^verification[ \t]+scenario/i.test(node.title)) {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (run.allowedWorkstreamIds && node.workstreamId) {
|
|
761
|
+
if (!run.allowedWorkstreamIds.includes(node.workstreamId))
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (!node.workstreamId)
|
|
765
|
+
continue;
|
|
766
|
+
const ws = nodeById.get(node.workstreamId);
|
|
767
|
+
if (ws && !isDispatchableWorkstreamStatus(ws.status))
|
|
768
|
+
continue;
|
|
769
|
+
if (!taskIsReady(node))
|
|
770
|
+
continue;
|
|
771
|
+
if (taskHasBlockedParent(node))
|
|
772
|
+
continue;
|
|
773
|
+
selectedWorkstreamId = node.workstreamId;
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
if (!selectedWorkstreamId) {
|
|
777
|
+
await stopAutoContinueRun({ run, reason: "blocked" });
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const workstreamNode = nodeById.get(selectedWorkstreamId) ?? null;
|
|
781
|
+
const workstreamTitle = workstreamNode?.title ?? null;
|
|
782
|
+
const initiativeNode = nodes.find((node) => node.type === "initiative") ?? null;
|
|
783
|
+
const initiativeTitle = initiativeNode?.title ?? `Initiative ${run.initiativeId.slice(0, 8)}`;
|
|
784
|
+
const sliceTaskNodes = graph.recentTodos
|
|
785
|
+
.map((taskId) => nodeById.get(taskId))
|
|
786
|
+
.filter((node) => Boolean(node &&
|
|
787
|
+
node.type === "task" &&
|
|
788
|
+
node.workstreamId === selectedWorkstreamId &&
|
|
789
|
+
isTodoStatus(node.status) &&
|
|
790
|
+
taskIsReady(node) &&
|
|
791
|
+
!taskHasBlockedParent(node) &&
|
|
792
|
+
(run.includeVerification ||
|
|
793
|
+
!/^verification[ \t]+scenario/i.test(String(node.title ?? "")))))
|
|
794
|
+
.slice(0, AUTO_CONTINUE_SLICE_MAX_TASKS);
|
|
795
|
+
const primaryTask = sliceTaskNodes[0] ?? null;
|
|
796
|
+
if (!primaryTask) {
|
|
797
|
+
await stopAutoContinueRun({ run, reason: "blocked" });
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
let cappedSliceTaskNodes = sliceTaskNodes;
|
|
801
|
+
let expectedDurationHours = cappedSliceTaskNodes.reduce((acc, t) => acc +
|
|
802
|
+
(typeof t.expectedDurationHours === "number" && Number.isFinite(t.expectedDurationHours)
|
|
803
|
+
? Math.max(0, t.expectedDurationHours)
|
|
804
|
+
: 0), 0);
|
|
805
|
+
let tokenEstimate = estimateTokensForDurationHours(expectedDurationHours);
|
|
806
|
+
const remainingTokens = run.tokenBudget - run.tokensUsed;
|
|
807
|
+
if (remainingTokens <= 0) {
|
|
808
|
+
await stopAutoContinueRun({ run, reason: "budget_exhausted" });
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
// If the modeled slice exceeds the remaining budget, shrink the slice to fit rather than
|
|
812
|
+
// stopping immediately (Play should still dispatch at least the primary task when possible).
|
|
813
|
+
if (tokenEstimate > 0 && tokenEstimate > remainingTokens) {
|
|
814
|
+
const nextSlice = [];
|
|
815
|
+
let hours = 0;
|
|
816
|
+
for (const task of sliceTaskNodes) {
|
|
817
|
+
const taskHours = typeof task.expectedDurationHours === "number" && Number.isFinite(task.expectedDurationHours)
|
|
818
|
+
? Math.max(0, task.expectedDurationHours)
|
|
819
|
+
: 0;
|
|
820
|
+
if (nextSlice.length === 0) {
|
|
821
|
+
nextSlice.push(task);
|
|
822
|
+
hours += taskHours;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
const nextEstimate = estimateTokensForDurationHours(hours + taskHours);
|
|
826
|
+
if (nextEstimate > remainingTokens)
|
|
827
|
+
continue;
|
|
828
|
+
nextSlice.push(task);
|
|
829
|
+
hours += taskHours;
|
|
830
|
+
}
|
|
831
|
+
cappedSliceTaskNodes = nextSlice;
|
|
832
|
+
expectedDurationHours = hours;
|
|
833
|
+
tokenEstimate = estimateTokensForDurationHours(expectedDurationHours);
|
|
834
|
+
}
|
|
835
|
+
if (tokenEstimate > 0 && tokenEstimate > remainingTokens) {
|
|
836
|
+
await stopAutoContinueRun({ run, reason: "budget_exhausted" });
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const executionPolicy = deriveExecutionPolicy(primaryTask, workstreamNode);
|
|
840
|
+
const sliceRunId = randomUUID();
|
|
841
|
+
const spawnGuardResult = await checkSpawnGuardSafe({
|
|
842
|
+
domain: executionPolicy.domain,
|
|
843
|
+
taskId: primaryTask.id,
|
|
844
|
+
initiativeId: run.initiativeId,
|
|
845
|
+
correlationId: sliceRunId,
|
|
846
|
+
runId: sliceRunId,
|
|
847
|
+
targetLabel: "autopilot slice",
|
|
848
|
+
});
|
|
849
|
+
if (spawnGuardResult && typeof spawnGuardResult === "object") {
|
|
850
|
+
const allowed = spawnGuardResult.allowed;
|
|
851
|
+
if (allowed === false) {
|
|
852
|
+
const blockedReason = summarizeSpawnGuardBlockReason(spawnGuardResult);
|
|
853
|
+
// Maintain existing behavior: mark the primary task blocked when a quality gate denies dispatch.
|
|
854
|
+
try {
|
|
855
|
+
await client.updateEntity("task", primaryTask.id, { status: "blocked" });
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
// best effort
|
|
859
|
+
}
|
|
860
|
+
try {
|
|
861
|
+
await syncParentRollupsForTask({
|
|
862
|
+
initiativeId: run.initiativeId,
|
|
863
|
+
taskId: primaryTask.id,
|
|
864
|
+
workstreamId: selectedWorkstreamId,
|
|
865
|
+
milestoneId: primaryTask.milestoneId,
|
|
866
|
+
correlationId: sliceRunId,
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
// best effort
|
|
871
|
+
}
|
|
872
|
+
await emitActivitySafe({
|
|
873
|
+
initiativeId: run.initiativeId,
|
|
874
|
+
runId: sliceRunId,
|
|
875
|
+
correlationId: sliceRunId,
|
|
876
|
+
phase: "blocked",
|
|
877
|
+
level: "error",
|
|
878
|
+
message: `Autopilot blocked by spawn guard for ${workstreamTitle ?? selectedWorkstreamId}.`,
|
|
879
|
+
metadata: {
|
|
880
|
+
event: "auto_continue_spawn_guard_blocked",
|
|
881
|
+
task_id: primaryTask.id,
|
|
882
|
+
workstream_id: selectedWorkstreamId,
|
|
883
|
+
blocked_reason: blockedReason,
|
|
884
|
+
spawn_guard: spawnGuardResult,
|
|
885
|
+
},
|
|
886
|
+
});
|
|
887
|
+
await requestDecisionSafe({
|
|
888
|
+
initiativeId: run.initiativeId,
|
|
889
|
+
correlationId: sliceRunId,
|
|
890
|
+
title: `Unblock autopilot for ${workstreamTitle ?? selectedWorkstreamId}`,
|
|
891
|
+
summary: [
|
|
892
|
+
`Spawn guard denied dispatch for primary task ${primaryTask.id}.`,
|
|
893
|
+
`Reason: ${blockedReason}`,
|
|
894
|
+
`Domain: ${executionPolicy.domain}`,
|
|
895
|
+
`Required skills: ${executionPolicy.requiredSkills.join(", ")}`,
|
|
896
|
+
].join(" "),
|
|
897
|
+
urgency: "high",
|
|
898
|
+
options: [
|
|
899
|
+
"Approve exception and continue",
|
|
900
|
+
"Reassign slice/domain",
|
|
901
|
+
"Pause and investigate quality gate",
|
|
902
|
+
],
|
|
903
|
+
blocking: true,
|
|
904
|
+
});
|
|
905
|
+
await stopAutoContinueRun({ run, reason: "blocked", error: blockedReason });
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
const milestoneIds = dedupeStrings(cappedSliceTaskNodes.map((t) => (t.milestoneId ?? "").trim()).filter(Boolean));
|
|
910
|
+
const milestoneSummaries = milestoneIds
|
|
911
|
+
.map((id) => nodeById.get(id))
|
|
912
|
+
.filter((node) => Boolean(node && node.type === "milestone"))
|
|
913
|
+
.map((m) => ({ id: m.id, title: m.title, status: m.status }));
|
|
914
|
+
const taskSummaries = cappedSliceTaskNodes.map((t) => ({
|
|
915
|
+
id: t.id,
|
|
916
|
+
title: t.title,
|
|
917
|
+
status: t.status,
|
|
918
|
+
milestoneId: t.milestoneId ?? null,
|
|
919
|
+
}));
|
|
920
|
+
const schemaPath = ensureAutopilotSliceSchemaPath(AUTO_CONTINUE_SLICE_SCHEMA_FILENAME);
|
|
921
|
+
const prompt = buildWorkstreamSlicePrompt({
|
|
922
|
+
initiativeTitle,
|
|
923
|
+
initiativeId: run.initiativeId,
|
|
924
|
+
workstreamId: selectedWorkstreamId,
|
|
925
|
+
workstreamTitle: workstreamTitle ?? `Workstream ${selectedWorkstreamId.slice(0, 8)}`,
|
|
926
|
+
milestoneSummaries,
|
|
927
|
+
taskSummaries,
|
|
928
|
+
executionPolicy,
|
|
929
|
+
runId: sliceRunId,
|
|
930
|
+
schemaPath,
|
|
931
|
+
});
|
|
932
|
+
const logsDir = join(getOrgxPluginConfigDir(), AUTO_CONTINUE_SLICE_LOG_DIRNAME);
|
|
933
|
+
const logPath = join(logsDir, `${sliceRunId}.log`);
|
|
934
|
+
const outputPath = join(logsDir, `${sliceRunId}.output.json`);
|
|
935
|
+
let workerCwd = (process.env.ORGX_AUTOPILOT_CWD ?? "").trim() || process.cwd();
|
|
936
|
+
// LaunchAgents often start with cwd="/". Prefer a stable, user-owned directory
|
|
937
|
+
// so relative paths and codex sandboxing behave consistently.
|
|
938
|
+
if (!workerCwd || workerCwd === "/") {
|
|
939
|
+
workerCwd = homedir();
|
|
940
|
+
}
|
|
941
|
+
const sliceAgent = resolveOrgxAgentForDomain(executionPolicy.domain);
|
|
942
|
+
const workerKind = (process.env.ORGX_AUTOPILOT_WORKER_KIND ?? "").trim().toLowerCase();
|
|
943
|
+
const inferredExecutor = workerKind === "claude-code" || workerKind === "claude_code" ? "claude-code" : "codex";
|
|
944
|
+
const executorRaw = (process.env.ORGX_AUTOPILOT_EXECUTOR ?? "").trim().toLowerCase() || inferredExecutor;
|
|
945
|
+
const executorSourceClient = executorRaw === "claude-code" || executorRaw === "claude_code" ? "claude-code" : "codex";
|
|
946
|
+
let runtimeHookUrl = null;
|
|
947
|
+
let runtimeHookToken = null;
|
|
948
|
+
try {
|
|
949
|
+
const snapshot = readOpenClawSettingsSnapshot();
|
|
950
|
+
const port = readOpenClawGatewayPort(snapshot.raw);
|
|
951
|
+
runtimeHookUrl = `http://127.0.0.1:${port}/orgx/api/hooks/runtime`;
|
|
952
|
+
runtimeHookToken = resolveRuntimeHookToken();
|
|
953
|
+
}
|
|
954
|
+
catch {
|
|
955
|
+
// best effort
|
|
956
|
+
}
|
|
957
|
+
const spawned = spawnCodexSliceWorker({
|
|
958
|
+
runId: sliceRunId,
|
|
959
|
+
prompt,
|
|
960
|
+
cwd: workerCwd,
|
|
961
|
+
logPath,
|
|
962
|
+
outputPath,
|
|
963
|
+
env: {
|
|
964
|
+
ORGX_SOURCE_CLIENT: executorSourceClient,
|
|
965
|
+
ORGX_RUN_ID: sliceRunId,
|
|
966
|
+
ORGX_CORRELATION_ID: sliceRunId,
|
|
967
|
+
ORGX_INITIATIVE_ID: run.initiativeId,
|
|
968
|
+
ORGX_WORKSTREAM_ID: selectedWorkstreamId,
|
|
969
|
+
ORGX_WORKSTREAM_TITLE: workstreamTitle ?? undefined,
|
|
970
|
+
ORGX_TASK_ID: primaryTask.id,
|
|
971
|
+
ORGX_AGENT_ID: sliceAgent.id,
|
|
972
|
+
ORGX_AGENT_NAME: sliceAgent.name,
|
|
973
|
+
ORGX_OUTPUT_PATH: outputPath,
|
|
974
|
+
ORGX_RUNTIME_HOOK_URL: runtimeHookUrl ?? undefined,
|
|
975
|
+
ORGX_HOOK_TOKEN: runtimeHookToken ?? undefined,
|
|
976
|
+
},
|
|
977
|
+
});
|
|
978
|
+
const slice = {
|
|
979
|
+
runId: sliceRunId,
|
|
980
|
+
initiativeId: run.initiativeId,
|
|
981
|
+
initiativeTitle: initiativeTitle ?? null,
|
|
982
|
+
workstreamId: selectedWorkstreamId,
|
|
983
|
+
workstreamTitle,
|
|
984
|
+
agentId: sliceAgent.id,
|
|
985
|
+
agentName: sliceAgent.name,
|
|
986
|
+
domain: executionPolicy.domain,
|
|
987
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
988
|
+
sourceClient: executorSourceClient,
|
|
989
|
+
pid: spawned.pid,
|
|
990
|
+
status: "running",
|
|
991
|
+
startedAt: now,
|
|
992
|
+
finishedAt: null,
|
|
993
|
+
updatedAt: now,
|
|
994
|
+
tokenEstimate: tokenEstimate > 0 ? tokenEstimate : null,
|
|
995
|
+
outputPath,
|
|
996
|
+
logPath,
|
|
997
|
+
taskIds: cappedSliceTaskNodes.map((t) => t.id),
|
|
998
|
+
milestoneIds,
|
|
999
|
+
lastError: null,
|
|
1000
|
+
};
|
|
1001
|
+
autoContinueSliceRuns.set(sliceRunId, slice);
|
|
1002
|
+
try {
|
|
1003
|
+
writeRuntimeEvent({
|
|
1004
|
+
sourceClient: executorSourceClient,
|
|
1005
|
+
event: "session_start",
|
|
1006
|
+
runId: sliceRunId,
|
|
1007
|
+
initiativeId: run.initiativeId,
|
|
1008
|
+
workstreamId: selectedWorkstreamId,
|
|
1009
|
+
taskId: primaryTask.id,
|
|
1010
|
+
agentId: slice.agentId,
|
|
1011
|
+
agentName: sliceAgent.name,
|
|
1012
|
+
phase: "execution",
|
|
1013
|
+
message: `Autopilot slice started: ${workstreamTitle ?? selectedWorkstreamId}`,
|
|
1014
|
+
metadata: {
|
|
1015
|
+
event: "autopilot_slice_started",
|
|
1016
|
+
requested_by_agent_id: run.agentId,
|
|
1017
|
+
requested_by_agent_name: run.agentName,
|
|
1018
|
+
domain: executionPolicy.domain,
|
|
1019
|
+
required_skills: executionPolicy.requiredSkills,
|
|
1020
|
+
task_ids: slice.taskIds,
|
|
1021
|
+
initiative_title: initiativeTitle ?? null,
|
|
1022
|
+
workstream_title: workstreamTitle ?? null,
|
|
1023
|
+
log_path: logPath,
|
|
1024
|
+
output_path: outputPath,
|
|
1025
|
+
},
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
catch {
|
|
1029
|
+
// best effort
|
|
1030
|
+
}
|
|
1031
|
+
autoContinueSliceLastHeartbeatMs.set(sliceRunId, Date.now());
|
|
1032
|
+
await emitActivitySafe({
|
|
1033
|
+
initiativeId: run.initiativeId,
|
|
1034
|
+
runId: sliceRunId,
|
|
1035
|
+
correlationId: sliceRunId,
|
|
1036
|
+
phase: "execution",
|
|
1037
|
+
level: "info",
|
|
1038
|
+
message: `Autopilot dispatched slice for ${workstreamTitle ?? selectedWorkstreamId}.`,
|
|
1039
|
+
metadata: {
|
|
1040
|
+
event: "autopilot_slice_dispatched",
|
|
1041
|
+
requested_by_agent_id: run.agentId,
|
|
1042
|
+
requested_by_agent_name: run.agentName,
|
|
1043
|
+
agent_id: slice.agentId,
|
|
1044
|
+
agent_name: sliceAgent.name,
|
|
1045
|
+
domain: executionPolicy.domain,
|
|
1046
|
+
required_skills: executionPolicy.requiredSkills,
|
|
1047
|
+
initiative_title: initiativeTitle ?? null,
|
|
1048
|
+
workstream_id: selectedWorkstreamId,
|
|
1049
|
+
workstream_title: workstreamTitle ?? null,
|
|
1050
|
+
task_ids: slice.taskIds,
|
|
1051
|
+
milestone_ids: milestoneIds,
|
|
1052
|
+
log_path: logPath,
|
|
1053
|
+
output_path: outputPath,
|
|
1054
|
+
},
|
|
1055
|
+
});
|
|
1056
|
+
upsertAgentContext({
|
|
1057
|
+
agentId: slice.agentId,
|
|
1058
|
+
initiativeId: run.initiativeId,
|
|
1059
|
+
initiativeTitle: initiativeTitle ?? null,
|
|
1060
|
+
workstreamId: selectedWorkstreamId,
|
|
1061
|
+
taskId: primaryTask.id,
|
|
1062
|
+
});
|
|
1063
|
+
run.lastTaskId = primaryTask.id;
|
|
1064
|
+
run.lastRunId = sliceRunId;
|
|
1065
|
+
run.activeTaskId = primaryTask.id;
|
|
1066
|
+
run.activeRunId = sliceRunId;
|
|
1067
|
+
run.activeTaskTokenEstimate = tokenEstimate > 0 ? tokenEstimate : null;
|
|
1068
|
+
run.updatedAt = now;
|
|
1069
|
+
try {
|
|
1070
|
+
await client.updateEntity("initiative", run.initiativeId, { status: "active" });
|
|
1071
|
+
}
|
|
1072
|
+
catch {
|
|
1073
|
+
// best effort
|
|
1074
|
+
}
|
|
1075
|
+
try {
|
|
1076
|
+
await updateInitiativeAutoContinueState({
|
|
1077
|
+
initiativeId: run.initiativeId,
|
|
1078
|
+
run,
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
catch {
|
|
1082
|
+
// best effort
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
async function tickAllAutoContinue() {
|
|
1086
|
+
if (autoContinueTickInFlight) {
|
|
1087
|
+
// Wait for the in-flight tick to finish instead of silently dropping.
|
|
1088
|
+
await autoContinueTickInFlight.catch(() => { });
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
const work = (async () => {
|
|
1092
|
+
for (const run of autoContinueRuns.values()) {
|
|
1093
|
+
try {
|
|
1094
|
+
await tickAutoContinueRun(run);
|
|
1095
|
+
}
|
|
1096
|
+
catch (err) {
|
|
1097
|
+
// Never let one loop crash the whole handler.
|
|
1098
|
+
run.lastError = safeErrorMessage(err);
|
|
1099
|
+
run.updatedAt = new Date().toISOString();
|
|
1100
|
+
await stopAutoContinueRun({ run, reason: "error", error: run.lastError });
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
})();
|
|
1104
|
+
autoContinueTickInFlight = work;
|
|
1105
|
+
try {
|
|
1106
|
+
await work;
|
|
1107
|
+
}
|
|
1108
|
+
finally {
|
|
1109
|
+
autoContinueTickInFlight = null;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
function isInitiativeActiveStatus(status) {
|
|
1113
|
+
const normalized = (status ?? "").trim().toLowerCase();
|
|
1114
|
+
if (!normalized)
|
|
1115
|
+
return false;
|
|
1116
|
+
return !(normalized === "completed" ||
|
|
1117
|
+
normalized === "done" ||
|
|
1118
|
+
normalized === "archived" ||
|
|
1119
|
+
normalized === "deleted" ||
|
|
1120
|
+
normalized === "cancelled");
|
|
1121
|
+
}
|
|
1122
|
+
function runningAutoContinueForWorkstream(initiativeId, workstreamId) {
|
|
1123
|
+
const run = autoContinueRuns.get(initiativeId) ?? null;
|
|
1124
|
+
if (!run)
|
|
1125
|
+
return null;
|
|
1126
|
+
if (run.status !== "running" && run.status !== "stopping")
|
|
1127
|
+
return null;
|
|
1128
|
+
if (!Array.isArray(run.allowedWorkstreamIds) || run.allowedWorkstreamIds.length === 0) {
|
|
1129
|
+
return run;
|
|
1130
|
+
}
|
|
1131
|
+
return run.allowedWorkstreamIds.includes(workstreamId) ? run : null;
|
|
1132
|
+
}
|
|
1133
|
+
async function startAutoContinueRun(input) {
|
|
1134
|
+
const now = new Date().toISOString();
|
|
1135
|
+
const existing = autoContinueRuns.get(input.initiativeId) ?? null;
|
|
1136
|
+
const existingIsLive = existing?.status === "running" || existing?.status === "stopping";
|
|
1137
|
+
const run = existing ??
|
|
1138
|
+
{
|
|
1139
|
+
initiativeId: input.initiativeId,
|
|
1140
|
+
agentId: input.agentId,
|
|
1141
|
+
agentName: input.agentName ?? null,
|
|
1142
|
+
includeVerification: false,
|
|
1143
|
+
allowedWorkstreamIds: null,
|
|
1144
|
+
stopAfterSlice: false,
|
|
1145
|
+
tokenBudget: defaultAutoContinueTokenBudget(),
|
|
1146
|
+
tokensUsed: 0,
|
|
1147
|
+
status: "running",
|
|
1148
|
+
stopReason: null,
|
|
1149
|
+
stopRequested: false,
|
|
1150
|
+
startedAt: now,
|
|
1151
|
+
stoppedAt: null,
|
|
1152
|
+
updatedAt: now,
|
|
1153
|
+
lastError: null,
|
|
1154
|
+
lastTaskId: null,
|
|
1155
|
+
lastRunId: null,
|
|
1156
|
+
activeTaskId: null,
|
|
1157
|
+
activeRunId: null,
|
|
1158
|
+
activeTaskTokenEstimate: null,
|
|
1159
|
+
};
|
|
1160
|
+
run.agentId = input.agentId;
|
|
1161
|
+
run.agentName =
|
|
1162
|
+
typeof input.agentName === "string" && input.agentName.trim().length > 0
|
|
1163
|
+
? input.agentName.trim()
|
|
1164
|
+
: null;
|
|
1165
|
+
run.includeVerification = input.includeVerification;
|
|
1166
|
+
run.allowedWorkstreamIds = input.allowedWorkstreamIds;
|
|
1167
|
+
run.stopAfterSlice = Boolean(input.stopAfterSlice);
|
|
1168
|
+
run.tokenBudget = normalizeTokenBudget(input.tokenBudget, run.tokenBudget || defaultAutoContinueTokenBudget());
|
|
1169
|
+
run.status = "running";
|
|
1170
|
+
run.stopReason = null;
|
|
1171
|
+
run.stopRequested = false;
|
|
1172
|
+
run.stoppedAt = null;
|
|
1173
|
+
run.updatedAt = now;
|
|
1174
|
+
run.lastError = null;
|
|
1175
|
+
const forceFreshRun = Boolean(input.stopAfterSlice);
|
|
1176
|
+
if (!existingIsLive || forceFreshRun) {
|
|
1177
|
+
run.tokensUsed = 0;
|
|
1178
|
+
run.startedAt = now;
|
|
1179
|
+
run.lastTaskId = null;
|
|
1180
|
+
run.lastRunId = null;
|
|
1181
|
+
run.activeTaskId = null;
|
|
1182
|
+
run.activeRunId = null;
|
|
1183
|
+
run.activeTaskTokenEstimate = null;
|
|
1184
|
+
}
|
|
1185
|
+
autoContinueRuns.set(input.initiativeId, run);
|
|
1186
|
+
void client
|
|
1187
|
+
.updateEntity("initiative", input.initiativeId, { status: "active" })
|
|
1188
|
+
.catch(() => {
|
|
1189
|
+
// best effort
|
|
1190
|
+
});
|
|
1191
|
+
void updateInitiativeAutoContinueState({
|
|
1192
|
+
initiativeId: input.initiativeId,
|
|
1193
|
+
run,
|
|
1194
|
+
}).catch(() => {
|
|
1195
|
+
// best effort
|
|
1196
|
+
});
|
|
1197
|
+
return run;
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
autoContinueRuns,
|
|
1201
|
+
autoContinueSliceRuns,
|
|
1202
|
+
localInitiativeStatusOverrides,
|
|
1203
|
+
writeRuntimeEvent,
|
|
1204
|
+
autoContinueTickMs: AUTO_CONTINUE_TICK_MS,
|
|
1205
|
+
defaultAutoContinueTokenBudget,
|
|
1206
|
+
setLocalInitiativeStatusOverride,
|
|
1207
|
+
clearLocalInitiativeStatusOverride,
|
|
1208
|
+
applyLocalInitiativeOverrides,
|
|
1209
|
+
applyLocalInitiativeOverrideToGraph,
|
|
1210
|
+
updateInitiativeAutoContinueState,
|
|
1211
|
+
stopAutoContinueRun,
|
|
1212
|
+
tickAutoContinueRun,
|
|
1213
|
+
tickAllAutoContinue,
|
|
1214
|
+
isInitiativeActiveStatus,
|
|
1215
|
+
runningAutoContinueForWorkstream,
|
|
1216
|
+
startAutoContinueRun,
|
|
1217
|
+
};
|
|
1218
|
+
}
|