@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.
Files changed (125) hide show
  1. package/dashboard/dist/assets/B5NEElEI.css +1 -0
  2. package/dashboard/dist/assets/BhapSNAs.js +215 -0
  3. package/dashboard/dist/assets/{BNeJ0kpF.js → iFdvE7lx.js} +1 -1
  4. package/dashboard/dist/assets/{CUV9IHHi.js → jRJsmpYM.js} +1 -1
  5. package/dashboard/dist/index.html +2 -2
  6. package/dist/activity-store.js +4 -18
  7. package/dist/agent-context-store.js +5 -25
  8. package/dist/agent-run-store.js +5 -25
  9. package/dist/agent-suite.js +1 -8
  10. package/dist/auth/flows.d.ts +47 -0
  11. package/dist/auth/flows.js +169 -0
  12. package/dist/auth-store.js +6 -26
  13. package/dist/byok-store.js +5 -19
  14. package/dist/cli/orgx.d.ts +66 -0
  15. package/dist/cli/orgx.js +91 -0
  16. package/dist/config/refresh.d.ts +32 -0
  17. package/dist/config/refresh.js +55 -0
  18. package/dist/config/resolution.d.ts +37 -0
  19. package/dist/config/resolution.js +178 -0
  20. package/dist/contracts/shared-types.d.ts +147 -0
  21. package/dist/contracts/shared-types.js +3 -0
  22. package/dist/contracts/types.d.ts +1 -134
  23. package/dist/contracts/types.js +5 -0
  24. package/dist/entities/auto-assignment.d.ts +36 -0
  25. package/dist/entities/auto-assignment.js +115 -0
  26. package/dist/entity-comment-store.js +5 -25
  27. package/dist/hash-utils.d.ts +2 -0
  28. package/dist/hash-utils.js +12 -0
  29. package/dist/http/helpers/activity-headline.d.ts +10 -0
  30. package/dist/http/helpers/activity-headline.js +192 -0
  31. package/dist/http/helpers/artifact-fallback.d.ts +13 -0
  32. package/dist/http/helpers/artifact-fallback.js +148 -0
  33. package/dist/http/helpers/auto-continue-engine.d.ts +298 -0
  34. package/dist/http/helpers/auto-continue-engine.js +1218 -0
  35. package/dist/http/helpers/autopilot-operations.d.ts +157 -0
  36. package/dist/http/helpers/autopilot-operations.js +403 -0
  37. package/dist/http/helpers/autopilot-runtime.d.ts +42 -0
  38. package/dist/http/helpers/autopilot-runtime.js +319 -0
  39. package/dist/http/helpers/autopilot-slice-utils.d.ts +38 -0
  40. package/dist/http/helpers/autopilot-slice-utils.js +476 -0
  41. package/dist/http/helpers/decision-mapper.d.ts +12 -0
  42. package/dist/http/helpers/decision-mapper.js +44 -0
  43. package/dist/http/helpers/dispatch-lifecycle.d.ts +102 -0
  44. package/dist/http/helpers/dispatch-lifecycle.js +604 -0
  45. package/dist/http/helpers/hash-utils.d.ts +1 -0
  46. package/dist/http/helpers/hash-utils.js +1 -0
  47. package/dist/http/helpers/kickoff-context.d.ts +12 -0
  48. package/dist/http/helpers/kickoff-context.js +154 -0
  49. package/dist/http/helpers/mission-control.d.ts +94 -0
  50. package/dist/http/helpers/mission-control.js +894 -0
  51. package/dist/http/helpers/openclaw-cli.d.ts +37 -0
  52. package/dist/http/helpers/openclaw-cli.js +283 -0
  53. package/dist/http/helpers/runtime-sse.d.ts +20 -0
  54. package/dist/http/helpers/runtime-sse.js +110 -0
  55. package/dist/http/helpers/value-utils.d.ts +6 -0
  56. package/dist/http/helpers/value-utils.js +67 -0
  57. package/dist/http/index.d.ts +88 -0
  58. package/dist/http/index.js +2353 -0
  59. package/dist/http/router.d.ts +23 -0
  60. package/dist/http/router.js +23 -0
  61. package/dist/http/routes/agent-control.d.ts +79 -0
  62. package/dist/http/routes/agent-control.js +684 -0
  63. package/dist/http/routes/agent-suite.d.ts +29 -0
  64. package/dist/http/routes/agent-suite.js +198 -0
  65. package/dist/http/routes/agents-catalog.d.ts +40 -0
  66. package/dist/http/routes/agents-catalog.js +83 -0
  67. package/dist/http/routes/billing.d.ts +23 -0
  68. package/dist/http/routes/billing.js +55 -0
  69. package/dist/http/routes/debug.d.ts +14 -0
  70. package/dist/http/routes/debug.js +21 -0
  71. package/dist/http/routes/decision-actions.d.ts +13 -0
  72. package/dist/http/routes/decision-actions.js +66 -0
  73. package/dist/http/routes/delegation.d.ts +19 -0
  74. package/dist/http/routes/delegation.js +32 -0
  75. package/dist/http/routes/entities.d.ts +47 -0
  76. package/dist/http/routes/entities.js +152 -0
  77. package/dist/http/routes/entity-dynamic.d.ts +25 -0
  78. package/dist/http/routes/entity-dynamic.js +191 -0
  79. package/dist/http/routes/health.d.ts +22 -0
  80. package/dist/http/routes/health.js +49 -0
  81. package/dist/http/routes/live-legacy.d.ts +110 -0
  82. package/dist/http/routes/live-legacy.js +598 -0
  83. package/dist/http/routes/live-misc.d.ts +69 -0
  84. package/dist/http/routes/live-misc.js +206 -0
  85. package/dist/http/routes/live-snapshot.d.ts +90 -0
  86. package/dist/http/routes/live-snapshot.js +297 -0
  87. package/dist/http/routes/mission-control-actions.d.ts +83 -0
  88. package/dist/http/routes/mission-control-actions.js +541 -0
  89. package/dist/http/routes/mission-control-read.d.ts +28 -0
  90. package/dist/http/routes/mission-control-read.js +67 -0
  91. package/dist/http/routes/onboarding.d.ts +34 -0
  92. package/dist/http/routes/onboarding.js +101 -0
  93. package/dist/http/routes/run-control.d.ts +24 -0
  94. package/dist/http/routes/run-control.js +86 -0
  95. package/dist/http/routes/runtime-hooks.d.ts +69 -0
  96. package/dist/http/routes/runtime-hooks.js +437 -0
  97. package/dist/http/routes/settings-byok.d.ts +23 -0
  98. package/dist/http/routes/settings-byok.js +163 -0
  99. package/dist/http/routes/summary.d.ts +18 -0
  100. package/dist/http/routes/summary.js +42 -0
  101. package/dist/http/routes/work-artifacts.d.ts +9 -0
  102. package/dist/http/routes/work-artifacts.js +36 -0
  103. package/dist/http/shared-state.d.ts +16 -0
  104. package/dist/http/shared-state.js +1 -0
  105. package/dist/http-handler.d.ts +1 -88
  106. package/dist/http-handler.js +1 -10605
  107. package/dist/index.js +108 -2243
  108. package/dist/json-utils.d.ts +1 -0
  109. package/dist/json-utils.js +8 -0
  110. package/dist/next-up-queue-store.js +4 -18
  111. package/dist/runtime-instance-store.js +5 -31
  112. package/dist/services/background.d.ts +23 -0
  113. package/dist/services/background.js +23 -0
  114. package/dist/services/instrumentation.d.ts +29 -0
  115. package/dist/services/instrumentation.js +136 -0
  116. package/dist/snapshot-store.js +5 -25
  117. package/dist/stores/json-store.d.ts +11 -0
  118. package/dist/stores/json-store.js +42 -0
  119. package/dist/sync/outbox-replay.d.ts +55 -0
  120. package/dist/sync/outbox-replay.js +514 -0
  121. package/dist/tools/core-tools.d.ts +76 -0
  122. package/dist/tools/core-tools.js +1005 -0
  123. package/package.json +1 -1
  124. package/dashboard/dist/assets/BzkiMPmM.js +0 -215
  125. 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
+ }