@useorgx/openclaw-plugin 0.4.8 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/dashboard/dist/assets/BJgZIVUQ.js +53 -0
- package/dashboard/dist/assets/BJgZIVUQ.js.br +0 -0
- package/dashboard/dist/assets/BJgZIVUQ.js.gz +0 -0
- package/dashboard/dist/assets/BXWDRGm-.js +1 -0
- package/dashboard/dist/assets/BXWDRGm-.js.br +0 -0
- package/dashboard/dist/assets/BXWDRGm-.js.gz +0 -0
- package/dashboard/dist/assets/BgOYB78t.js +4 -0
- package/dashboard/dist/assets/BgOYB78t.js.br +0 -0
- package/dashboard/dist/assets/BgOYB78t.js.gz +0 -0
- package/dashboard/dist/assets/C-KIc3Wc.js.br +0 -0
- package/dashboard/dist/assets/C-KIc3Wc.js.gz +0 -0
- package/dashboard/dist/assets/CE38zU4U.js +1 -0
- package/dashboard/dist/assets/CE38zU4U.js.br +0 -0
- package/dashboard/dist/assets/CE38zU4U.js.gz +0 -0
- package/dashboard/dist/assets/CFGKRAzG.js +1 -0
- package/dashboard/dist/assets/CFGKRAzG.js.br +0 -0
- package/dashboard/dist/assets/CFGKRAzG.js.gz +0 -0
- package/dashboard/dist/assets/CGGR2GZh.js +1 -0
- package/dashboard/dist/assets/CGGR2GZh.js.br +0 -0
- package/dashboard/dist/assets/CGGR2GZh.js.gz +0 -0
- package/dashboard/dist/assets/CL_wXqR7.js +1 -0
- package/dashboard/dist/assets/CL_wXqR7.js.br +0 -0
- package/dashboard/dist/assets/CL_wXqR7.js.gz +0 -0
- package/dashboard/dist/assets/CPFiTmlw.js +8 -0
- package/dashboard/dist/assets/CPFiTmlw.js.br +0 -0
- package/dashboard/dist/assets/CPFiTmlw.js.gz +0 -0
- package/dashboard/dist/assets/CZZTvkQZ.js +1 -0
- package/dashboard/dist/assets/CZZTvkQZ.js.br +0 -0
- package/dashboard/dist/assets/CZZTvkQZ.js.gz +0 -0
- package/dashboard/dist/assets/{CpJsfbXo.js → CxQ08qFN.js} +2 -2
- package/dashboard/dist/assets/CxQ08qFN.js.br +0 -0
- package/dashboard/dist/assets/CxQ08qFN.js.gz +0 -0
- package/dashboard/dist/assets/D-bf6hEI.js +213 -0
- package/dashboard/dist/assets/D-bf6hEI.js.br +0 -0
- package/dashboard/dist/assets/D-bf6hEI.js.gz +0 -0
- package/dashboard/dist/assets/DG6y9wJI.js +2 -0
- package/dashboard/dist/assets/DG6y9wJI.js.br +0 -0
- package/dashboard/dist/assets/DG6y9wJI.js.gz +0 -0
- package/dashboard/dist/assets/DNxKz-GV.js +1 -0
- package/dashboard/dist/assets/DNxKz-GV.js.br +0 -0
- package/dashboard/dist/assets/DNxKz-GV.js.gz +0 -0
- package/dashboard/dist/assets/DW_rKUic.js +11 -0
- package/dashboard/dist/assets/DW_rKUic.js.br +0 -0
- package/dashboard/dist/assets/DW_rKUic.js.gz +0 -0
- package/dashboard/dist/assets/DbNoijHm.js +1 -0
- package/dashboard/dist/assets/DbNoijHm.js.br +0 -0
- package/dashboard/dist/assets/DbNoijHm.js.gz +0 -0
- package/dashboard/dist/assets/DjcdE6jC.js +2 -0
- package/dashboard/dist/assets/DjcdE6jC.js.br +0 -0
- package/dashboard/dist/assets/DjcdE6jC.js.gz +0 -0
- package/dashboard/dist/assets/FZYuCDnt.js +1 -0
- package/dashboard/dist/assets/FZYuCDnt.js.br +0 -0
- package/dashboard/dist/assets/FZYuCDnt.js.gz +0 -0
- package/dashboard/dist/assets/PAUiij_z.js +1 -0
- package/dashboard/dist/assets/PAUiij_z.js.br +0 -0
- package/dashboard/dist/assets/PAUiij_z.js.gz +0 -0
- package/dashboard/dist/assets/cNrhgGc1.js +8 -0
- package/dashboard/dist/assets/cNrhgGc1.js.br +0 -0
- package/dashboard/dist/assets/cNrhgGc1.js.gz +0 -0
- package/dashboard/dist/assets/h5biQs2I.css +1 -0
- package/dashboard/dist/assets/h5biQs2I.css.br +0 -0
- package/dashboard/dist/assets/h5biQs2I.css.gz +0 -0
- package/dashboard/dist/assets/ic2FaMnh.js +1 -0
- package/dashboard/dist/assets/ic2FaMnh.js.br +0 -0
- package/dashboard/dist/assets/ic2FaMnh.js.gz +0 -0
- package/dashboard/dist/assets/nByHNHoW.js +1 -0
- package/dashboard/dist/assets/nByHNHoW.js.br +0 -0
- package/dashboard/dist/assets/nByHNHoW.js.gz +0 -0
- package/dashboard/dist/assets/qm8xLgv-.css +1 -0
- package/dashboard/dist/assets/qm8xLgv-.css.br +0 -0
- package/dashboard/dist/assets/qm8xLgv-.css.gz +0 -0
- package/dashboard/dist/assets/tS9mbYZi.js +1 -0
- package/dashboard/dist/assets/tS9mbYZi.js.br +0 -0
- package/dashboard/dist/assets/tS9mbYZi.js.gz +0 -0
- package/dashboard/dist/brand/anthropic-mark.svg.br +0 -0
- package/dashboard/dist/brand/anthropic-mark.svg.gz +0 -0
- package/dashboard/dist/brand/openai-mark.svg.br +0 -0
- package/dashboard/dist/brand/openai-mark.svg.gz +0 -0
- package/dashboard/dist/brand/openclaw-mark.svg.br +0 -0
- package/dashboard/dist/brand/openclaw-mark.svg.gz +0 -0
- package/dashboard/dist/brand/xandy-orchestrator.png +0 -0
- package/dashboard/dist/index.html +7 -5
- package/dashboard/dist/index.html.br +0 -0
- package/dashboard/dist/index.html.gz +0 -0
- package/dist/activity-actor-fields.js +26 -4
- package/dist/activity-store.js +38 -26
- package/dist/agent-context-store.js +84 -42
- package/dist/agent-run-store.js +49 -28
- package/dist/agent-suite.d.ts +9 -0
- package/dist/agent-suite.js +150 -17
- package/dist/artifacts/artifact-domain-schemas.d.ts +66 -0
- package/dist/artifacts/artifact-domain-schemas.js +357 -0
- package/dist/artifacts/register-artifact.d.ts +4 -3
- package/dist/artifacts/register-artifact.js +170 -57
- 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/chat-store.d.ts +157 -0
- package/dist/chat-store.js +586 -0
- package/dist/cli/orgx.d.ts +66 -0
- package/dist/cli/orgx.js +102 -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/client.d.ts +43 -3
- package/dist/contracts/client.js +159 -30
- package/dist/contracts/retro-schema.d.ts +81 -0
- package/dist/contracts/retro-schema.js +80 -0
- package/dist/contracts/shared-types.d.ts +306 -0
- package/dist/contracts/shared-types.js +179 -0
- package/dist/contracts/skill-pack-schema.d.ts +192 -0
- package/dist/contracts/skill-pack-schema.js +180 -0
- package/dist/contracts/types.d.ts +224 -132
- package/dist/contracts/types.js +5 -0
- package/dist/entities/auto-assignment.d.ts +36 -0
- package/dist/entities/auto-assignment.js +141 -0
- package/dist/entity-comment-store.js +5 -25
- package/dist/event-sanitization.d.ts +11 -0
- package/dist/event-sanitization.js +113 -0
- package/dist/fs-utils.js +13 -1
- package/dist/gateway-watchdog.d.ts +5 -0
- package/dist/gateway-watchdog.js +50 -0
- package/dist/hash-utils.d.ts +2 -0
- package/dist/hash-utils.js +12 -0
- package/dist/hooks/post-reporting-event.mjs +1 -5
- package/dist/http/helpers/activity-headline.d.ts +10 -0
- package/dist/http/helpers/activity-headline.js +73 -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 +486 -0
- package/dist/http/helpers/auto-continue-engine.js +3563 -0
- package/dist/http/helpers/autopilot-operations.d.ts +176 -0
- package/dist/http/helpers/autopilot-operations.js +554 -0
- package/dist/http/helpers/autopilot-runtime.d.ts +43 -0
- package/dist/http/helpers/autopilot-runtime.js +607 -0
- package/dist/http/helpers/autopilot-slice-utils.d.ts +56 -0
- package/dist/http/helpers/autopilot-slice-utils.js +899 -0
- package/dist/http/helpers/decision-mapper.d.ts +52 -0
- package/dist/http/helpers/decision-mapper.js +260 -0
- package/dist/http/helpers/dispatch-lifecycle.d.ts +119 -0
- package/dist/http/helpers/dispatch-lifecycle.js +809 -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 +228 -0
- package/dist/http/helpers/llm-client.d.ts +47 -0
- package/dist/http/helpers/llm-client.js +256 -0
- package/dist/http/helpers/mission-control.d.ts +193 -0
- package/dist/http/helpers/mission-control.js +1383 -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/sentinel-catalog.d.ts +23 -0
- package/dist/http/helpers/sentinel-catalog.js +193 -0
- package/dist/http/helpers/session-classification.d.ts +9 -0
- package/dist/http/helpers/session-classification.js +564 -0
- package/dist/http/helpers/slice-experience-v2.d.ts +137 -0
- package/dist/http/helpers/slice-experience-v2.js +677 -0
- package/dist/http/helpers/slice-run-projections.d.ts +72 -0
- package/dist/http/helpers/slice-run-projections.js +860 -0
- package/dist/http/helpers/triage-mapper.d.ts +43 -0
- package/dist/http/helpers/triage-mapper.js +549 -0
- package/dist/http/helpers/value-utils.d.ts +6 -0
- package/dist/http/helpers/value-utils.js +72 -0
- package/dist/http/helpers/workspace-scope.d.ts +15 -0
- package/dist/http/helpers/workspace-scope.js +170 -0
- package/dist/http/index.d.ts +88 -0
- package/dist/http/index.js +3610 -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 +38 -0
- package/dist/http/routes/agent-suite.js +397 -0
- package/dist/http/routes/agents-catalog.d.ts +40 -0
- package/dist/http/routes/agents-catalog.js +128 -0
- package/dist/http/routes/billing.d.ts +23 -0
- package/dist/http/routes/billing.js +55 -0
- package/dist/http/routes/chat.d.ts +19 -0
- package/dist/http/routes/chat.js +522 -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 +20 -0
- package/dist/http/routes/decision-actions.js +103 -0
- package/dist/http/routes/delegation.d.ts +19 -0
- package/dist/http/routes/delegation.js +32 -0
- package/dist/http/routes/dispatch-gateway-envelope.d.ts +25 -0
- package/dist/http/routes/dispatch-gateway-envelope.js +26 -0
- package/dist/http/routes/entities.d.ts +63 -0
- package/dist/http/routes/entities.js +440 -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 +115 -0
- package/dist/http/routes/live-legacy.js +112 -0
- package/dist/http/routes/live-misc.d.ts +81 -0
- package/dist/http/routes/live-misc.js +426 -0
- package/dist/http/routes/live-snapshot.d.ts +136 -0
- package/dist/http/routes/live-snapshot.js +916 -0
- package/dist/http/routes/live-terminal.d.ts +11 -0
- package/dist/http/routes/live-terminal.js +261 -0
- package/dist/http/routes/live-triage.d.ts +61 -0
- package/dist/http/routes/live-triage.js +248 -0
- package/dist/http/routes/mission-control-actions.d.ts +131 -0
- package/dist/http/routes/mission-control-actions.js +1791 -0
- package/dist/http/routes/mission-control-read.d.ts +73 -0
- package/dist/http/routes/mission-control-read.js +1640 -0
- package/dist/http/routes/onboarding.d.ts +34 -0
- package/dist/http/routes/onboarding.js +101 -0
- package/dist/http/routes/realtime-orchestrator.d.ts +10 -0
- package/dist/http/routes/realtime-orchestrator.js +74 -0
- package/dist/http/routes/run-control.d.ts +27 -0
- package/dist/http/routes/run-control.js +96 -0
- package/dist/http/routes/runtime-hooks.d.ts +69 -0
- package/dist/http/routes/runtime-hooks.js +437 -0
- package/dist/http/routes/sentinels-catalog.d.ts +7 -0
- package/dist/http/routes/sentinels-catalog.js +24 -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 +49 -0
- package/dist/http/routes/usage.d.ts +24 -0
- package/dist/http/routes/usage.js +362 -0
- package/dist/http/routes/work-artifacts.d.ts +9 -0
- package/dist/http/routes/work-artifacts.js +55 -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 +287 -2284
- package/dist/json-utils.d.ts +1 -0
- package/dist/json-utils.js +8 -0
- package/dist/local-openclaw.js +29 -6
- package/dist/mcp-client-setup.js +3 -3
- package/dist/mcp-http-handler.js +33 -59
- package/dist/next-up-queue-store.d.ts +16 -1
- package/dist/next-up-queue-store.js +93 -25
- package/dist/outbox.d.ts +5 -0
- package/dist/outbox.js +113 -9
- package/dist/paths.js +24 -5
- package/dist/reporting/rollups.d.ts +53 -0
- package/dist/reporting/rollups.js +148 -0
- package/dist/retro/domain-templates.d.ts +45 -0
- package/dist/retro/domain-templates.js +297 -0
- package/dist/retro/quality-rubric.d.ts +33 -0
- package/dist/retro/quality-rubric.js +213 -0
- package/dist/runtime-cleanup.d.ts +18 -0
- package/dist/runtime-cleanup.js +87 -0
- package/dist/runtime-instance-store.js +5 -31
- package/dist/services/background.d.ts +34 -0
- package/dist/services/background.js +45 -0
- package/dist/services/experiment-randomization.d.ts +21 -0
- package/dist/services/experiment-randomization.js +63 -0
- package/dist/services/instrumentation.d.ts +29 -0
- package/dist/services/instrumentation.js +136 -0
- package/dist/skill-pack-state.d.ts +36 -5
- package/dist/skill-pack-state.js +273 -29
- 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/local-agent-telemetry.d.ts +13 -0
- package/dist/sync/local-agent-telemetry.js +128 -0
- package/dist/sync/outbox-replay.d.ts +55 -0
- package/dist/sync/outbox-replay.js +621 -0
- package/dist/team-context-store.d.ts +23 -0
- package/dist/team-context-store.js +116 -0
- package/dist/telemetry/posthog.js +4 -2
- package/dist/tools/core-tools.d.ts +72 -0
- package/dist/tools/core-tools.js +2270 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +2 -0
- package/dist/worker-supervisor.js +23 -0
- package/package.json +14 -4
- package/dashboard/dist/assets/B3ziCA02.js +0 -8
- package/dashboard/dist/assets/BNeJ0kpF.js +0 -1
- package/dashboard/dist/assets/BzkiMPmM.js +0 -215
- package/dashboard/dist/assets/CUV9IHHi.js +0 -1
- package/dashboard/dist/assets/Ie7d9Iq2.css +0 -1
- package/dashboard/dist/assets/sAhvFnpk.js +0 -4
|
@@ -0,0 +1,3563 @@
|
|
|
1
|
+
import { randomUUID as randomUuidFn } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readdir, stat, unlink } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { normalizeActivityActionPhase, normalizeActivityActionType, } from "../../contracts/shared-types.js";
|
|
7
|
+
import { upsertAgentContext, upsertRunContext } from "../../agent-context-store.js";
|
|
8
|
+
import { appendTeamCompletion } from "../../team-context-store.js";
|
|
9
|
+
import { readOpenClawGatewayPort, readOpenClawSettingsSnapshot, } from "../../openclaw-settings.js";
|
|
10
|
+
import { resolveRuntimeHookToken, } from "../../runtime-instance-store.js";
|
|
11
|
+
import { detectMcpHandshakeFailure, shouldKillWorker } from "../../worker-supervisor.js";
|
|
12
|
+
import { getOrgxPluginConfigDir } from "../../paths.js";
|
|
13
|
+
import { buildMissionControlGraph, DEFAULT_TOKEN_BUDGET_ASSUMPTIONS, dedupeStrings, detectBehaviorConfigDrift, deriveBehaviorAutomationLevel, deriveBehaviorConfigContext, deriveExecutionPolicy, evaluateScopeCompletion, isDispatchableWorkstreamStatus, isDoneStatus, isTodoStatus, readBudgetEnvNumber, selectSliceTasksByScope, SLICE_SCOPE_TIMEOUT_MULTIPLIER, spawnGuardIsRateLimited, summarizeSpawnGuardBlockReason, } from "./mission-control.js";
|
|
14
|
+
import { createAutopilotRuntime } from "./autopilot-runtime.js";
|
|
15
|
+
import { buildScopeDirective, buildSliceOutputInstructions, buildWorkstreamSlicePrompt, createCodexBinResolver, ensureAutopilotSliceSchemaPath, fileUpdatedAtEpochMs, parseSliceResult, readFileTailSafe, readSliceOutputFile, } from "./autopilot-slice-utils.js";
|
|
16
|
+
import { pickString } from "./value-utils.js";
|
|
17
|
+
function resolveAutopilotDefaultCwd(filename) {
|
|
18
|
+
let cursor = dirname(filename);
|
|
19
|
+
for (let i = 0; i < 12; i += 1) {
|
|
20
|
+
if (existsSync(join(cursor, "package.json")))
|
|
21
|
+
return cursor;
|
|
22
|
+
const parent = dirname(cursor);
|
|
23
|
+
if (!parent || parent === cursor)
|
|
24
|
+
break;
|
|
25
|
+
cursor = parent;
|
|
26
|
+
}
|
|
27
|
+
return homedir();
|
|
28
|
+
}
|
|
29
|
+
export function createAutoContinueEngine(deps) {
|
|
30
|
+
const { client, safeErrorMessage, pidAlive, stopProcess, resolveOrgxAgentForDomain, checkSpawnGuardSafe, syncParentRollupsForTask, emitActivitySafe, requestDecisionSafe, registerArtifactSafe, applyAgentStatusUpdatesSafe, upsertRuntimeInstanceFromHook, broadcastRuntimeSse, clearSnapshotResponseCache, resolveByokEnvOverrides, } = deps;
|
|
31
|
+
const randomUUID = deps.randomUUID ?? randomUuidFn;
|
|
32
|
+
const fetchKickoffContextSafeFn = deps.fetchKickoffContextSafe ?? null;
|
|
33
|
+
const renderKickoffMessageFn = deps.renderKickoffMessage ?? null;
|
|
34
|
+
const decisionAutoResolveGuardedEnabled = String(process.env.DECISION_AUTO_RESOLVE_GUARDED_ENABLED ?? "true")
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase() !== "false";
|
|
37
|
+
/** Spread into any metadata object to flag mock-worker activity. */
|
|
38
|
+
function mockMeta(slice) {
|
|
39
|
+
return slice.isMockWorker ? { mock: true } : {};
|
|
40
|
+
}
|
|
41
|
+
const requestDecisionQueued = async (input) => {
|
|
42
|
+
const inferredRunId = (typeof input.sourceRunId === "string" && input.sourceRunId.trim().length > 0
|
|
43
|
+
? input.sourceRunId.trim()
|
|
44
|
+
: null) ??
|
|
45
|
+
(typeof input.correlationId === "string" && input.correlationId.trim().length > 0
|
|
46
|
+
? input.correlationId.trim()
|
|
47
|
+
: null);
|
|
48
|
+
const inferredSessionId = (typeof input.sourceSessionId === "string" && input.sourceSessionId.trim().length > 0
|
|
49
|
+
? input.sourceSessionId.trim()
|
|
50
|
+
: null) ?? inferredRunId;
|
|
51
|
+
const inferredStreamId = (typeof input.sourceStreamId === "string" && input.sourceStreamId.trim().length > 0
|
|
52
|
+
? input.sourceStreamId.trim()
|
|
53
|
+
: null) ??
|
|
54
|
+
(typeof input.workstreamId === "string" && input.workstreamId.trim().length > 0
|
|
55
|
+
? input.workstreamId.trim()
|
|
56
|
+
: null);
|
|
57
|
+
const sourceRefBase = input.sourceRef && typeof input.sourceRef === "object" && !Array.isArray(input.sourceRef)
|
|
58
|
+
? input.sourceRef
|
|
59
|
+
: {};
|
|
60
|
+
const normalizedInput = {
|
|
61
|
+
...input,
|
|
62
|
+
sourceRunId: inferredRunId,
|
|
63
|
+
sourceSessionId: inferredSessionId,
|
|
64
|
+
sourceStreamId: inferredStreamId,
|
|
65
|
+
sourceRef: {
|
|
66
|
+
...sourceRefBase,
|
|
67
|
+
run_id: sourceRefBase.run_id ?? inferredRunId,
|
|
68
|
+
session_id: sourceRefBase.session_id ?? inferredSessionId,
|
|
69
|
+
stream_id: sourceRefBase.stream_id ?? inferredStreamId,
|
|
70
|
+
workstream_id: sourceRefBase.workstream_id ?? input.workstreamId ?? null,
|
|
71
|
+
},
|
|
72
|
+
metadata: {
|
|
73
|
+
...(input.metadata ?? {}),
|
|
74
|
+
source_system: input.sourceSystem ?? null,
|
|
75
|
+
conflict_source: input.conflictSource ?? null,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
const result = await requestDecisionSafe(normalizedInput);
|
|
79
|
+
if (typeof result === "boolean") {
|
|
80
|
+
return { queued: result, decisionIds: [] };
|
|
81
|
+
}
|
|
82
|
+
if (result && typeof result === "object" && "queued" in result) {
|
|
83
|
+
const record = result;
|
|
84
|
+
const decisionIds = Array.isArray(record.decisionIds)
|
|
85
|
+
? record.decisionIds
|
|
86
|
+
.filter((entry) => typeof entry === "string")
|
|
87
|
+
.map((entry) => entry.trim())
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
: [];
|
|
90
|
+
return {
|
|
91
|
+
queued: Boolean(record.queued),
|
|
92
|
+
decisionIds,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return { queued: false, decisionIds: [] };
|
|
96
|
+
};
|
|
97
|
+
const __filename = deps.filename;
|
|
98
|
+
const autoContinueRuns = new Map();
|
|
99
|
+
const localInitiativeStatusOverrides = new Map();
|
|
100
|
+
const localTaskStatusOverrides = new Map();
|
|
101
|
+
const localMilestoneStatusOverrides = new Map();
|
|
102
|
+
let autoContinueTickInFlight = null;
|
|
103
|
+
const AUTO_CONTINUE_TICK_MS = readBudgetEnvNumber("ORGX_AUTO_CONTINUE_TICK_MS", 2_500, {
|
|
104
|
+
min: 250,
|
|
105
|
+
max: 60_000,
|
|
106
|
+
});
|
|
107
|
+
const AUTO_CONTINUE_PARALLEL_MIN = 1;
|
|
108
|
+
const AUTO_CONTINUE_PARALLEL_MAX = 5;
|
|
109
|
+
const AUTO_CONTINUE_MAX_PARALLEL_DEFAULT = Math.max(AUTO_CONTINUE_PARALLEL_MIN, Math.min(AUTO_CONTINUE_PARALLEL_MAX, Math.round(readBudgetEnvNumber("ORGX_AUTO_CONTINUE_MAX_PARALLEL_DEFAULT", 5, { min: AUTO_CONTINUE_PARALLEL_MIN, max: AUTO_CONTINUE_PARALLEL_MAX }))));
|
|
110
|
+
const normalizeParallelMode = (_value) => "iwmt";
|
|
111
|
+
const normalizeMaxParallelSlices = (value, fallback) => {
|
|
112
|
+
const normalizedFallback = Math.max(AUTO_CONTINUE_PARALLEL_MIN, Math.min(AUTO_CONTINUE_PARALLEL_MAX, Math.round(fallback || AUTO_CONTINUE_PARALLEL_MIN)));
|
|
113
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
114
|
+
const parsed = Math.round(value);
|
|
115
|
+
return Math.max(AUTO_CONTINUE_PARALLEL_MIN, Math.min(AUTO_CONTINUE_PARALLEL_MAX, parsed));
|
|
116
|
+
}
|
|
117
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
118
|
+
const parsed = Number(value);
|
|
119
|
+
if (Number.isFinite(parsed)) {
|
|
120
|
+
const rounded = Math.round(parsed);
|
|
121
|
+
return Math.max(AUTO_CONTINUE_PARALLEL_MIN, Math.min(AUTO_CONTINUE_PARALLEL_MAX, rounded));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return normalizedFallback;
|
|
125
|
+
};
|
|
126
|
+
const buildSliceEnrichment = (input) => {
|
|
127
|
+
const eventName = typeof input.event === "string" && input.event.trim().length > 0
|
|
128
|
+
? input.event.trim().toLowerCase()
|
|
129
|
+
: null;
|
|
130
|
+
const inferredActionType = (() => {
|
|
131
|
+
if (!eventName)
|
|
132
|
+
return "execute_task";
|
|
133
|
+
if (eventName === "orchestrator_dispatch")
|
|
134
|
+
return "orchestrator_dispatch";
|
|
135
|
+
if (eventName.includes("slice_dispatched"))
|
|
136
|
+
return "dispatch_slice";
|
|
137
|
+
if (eventName.includes("slice_started") || eventName === "session_start") {
|
|
138
|
+
return "run_started";
|
|
139
|
+
}
|
|
140
|
+
if (eventName.includes("slice_heartbeat") || eventName === "heartbeat") {
|
|
141
|
+
return "run_heartbeat";
|
|
142
|
+
}
|
|
143
|
+
if (eventName.includes("slice_handoff"))
|
|
144
|
+
return "slice_handoff";
|
|
145
|
+
if (eventName.includes("spawn_guard_rate_limited"))
|
|
146
|
+
return "spawn_guard_rate_limited";
|
|
147
|
+
if (eventName.includes("spawn_guard_blocked"))
|
|
148
|
+
return "spawn_guard_blocked";
|
|
149
|
+
if (eventName.includes("status_updates_buffered"))
|
|
150
|
+
return "status_updates_buffered";
|
|
151
|
+
if (eventName.includes("status_updates"))
|
|
152
|
+
return "status_updates_applied";
|
|
153
|
+
if (eventName.includes("artifact_registered"))
|
|
154
|
+
return "artifact_registered";
|
|
155
|
+
if (eventName.includes("decision_requested"))
|
|
156
|
+
return "decision_requested";
|
|
157
|
+
if (eventName.includes("decision_resolved"))
|
|
158
|
+
return "decision_resolved";
|
|
159
|
+
if (eventName === "auto_continue_started")
|
|
160
|
+
return "auto_continue_started";
|
|
161
|
+
if (eventName === "auto_continue_stopped")
|
|
162
|
+
return "auto_continue_stopped";
|
|
163
|
+
if (eventName.includes("behavior_config") || eventName.includes("behavior_automation")) {
|
|
164
|
+
return "behavior_config_review";
|
|
165
|
+
}
|
|
166
|
+
if (eventName.includes("transition"))
|
|
167
|
+
return "run_state_transition";
|
|
168
|
+
if (eventName.includes("auto_fix"))
|
|
169
|
+
return "auto_fix";
|
|
170
|
+
if (eventName.includes("milestone_completed"))
|
|
171
|
+
return "milestone_completed";
|
|
172
|
+
if (eventName.includes("error") || eventName.includes("failed"))
|
|
173
|
+
return "run_failed";
|
|
174
|
+
if (eventName.includes("result") || eventName.includes("completed"))
|
|
175
|
+
return "run_completed";
|
|
176
|
+
return eventName.replace(/[^a-z0-9]+/g, "_");
|
|
177
|
+
})();
|
|
178
|
+
const actionType = normalizeActivityActionType(input.actionType ?? inferredActionType) ?? inferredActionType;
|
|
179
|
+
const inferredActionPhase = (() => {
|
|
180
|
+
if (!eventName)
|
|
181
|
+
return "execution";
|
|
182
|
+
if (eventName === "orchestrator_dispatch" || eventName.includes("slice_dispatched")) {
|
|
183
|
+
return "dispatch";
|
|
184
|
+
}
|
|
185
|
+
if (eventName.includes("handoff"))
|
|
186
|
+
return "handoff";
|
|
187
|
+
if (eventName.includes("heartbeat"))
|
|
188
|
+
return "execution";
|
|
189
|
+
if (eventName.includes("decision_"))
|
|
190
|
+
return "review";
|
|
191
|
+
if (eventName.includes("blocked") ||
|
|
192
|
+
eventName.includes("rate_limited") ||
|
|
193
|
+
eventName.includes("stall") ||
|
|
194
|
+
eventName.includes("timeout")) {
|
|
195
|
+
return "blocked";
|
|
196
|
+
}
|
|
197
|
+
if (eventName.includes("error") || eventName.includes("failed"))
|
|
198
|
+
return "error";
|
|
199
|
+
if (eventName.includes("result") || eventName.includes("completed") || eventName === "auto_continue_stopped") {
|
|
200
|
+
return "completed";
|
|
201
|
+
}
|
|
202
|
+
if (eventName === "auto_continue_started")
|
|
203
|
+
return "intent";
|
|
204
|
+
return "execution";
|
|
205
|
+
})();
|
|
206
|
+
const actionPhase = normalizeActivityActionPhase(input.actionPhase ?? inferredActionPhase) ??
|
|
207
|
+
inferredActionPhase;
|
|
208
|
+
const workstreamId = (input.workstreamId ?? input.slice?.workstreamId ?? "").trim() || null;
|
|
209
|
+
const taskId = (input.taskId ?? input.slice?.taskIds?.[0] ?? "").trim() || null;
|
|
210
|
+
const requiredSkills = Array.isArray(input.requiredSkills)
|
|
211
|
+
? input.requiredSkills
|
|
212
|
+
: input.slice?.requiredSkills ?? null;
|
|
213
|
+
return {
|
|
214
|
+
event: input.event ?? null,
|
|
215
|
+
action_type: actionType,
|
|
216
|
+
action_phase: actionPhase,
|
|
217
|
+
initiative_id: input.run.initiativeId,
|
|
218
|
+
requested_by_agent_id: input.run.agentId,
|
|
219
|
+
requested_by_agent_name: input.run.agentName,
|
|
220
|
+
requester_agent_id: input.run.agentId,
|
|
221
|
+
requester_agent_name: input.run.agentName,
|
|
222
|
+
agent_id: input.slice?.agentId ?? null,
|
|
223
|
+
agent_name: input.slice?.agentName ?? null,
|
|
224
|
+
executor_agent_id: input.slice?.agentId ?? null,
|
|
225
|
+
executor_agent_name: input.slice?.agentName ?? null,
|
|
226
|
+
source_run_id: input.slice?.runId ?? null,
|
|
227
|
+
source_session_id: input.slice?.runId ?? null,
|
|
228
|
+
source_stream_id: workstreamId,
|
|
229
|
+
run_id: input.slice?.runId ?? null,
|
|
230
|
+
slice_run_id: input.slice?.runId ?? null,
|
|
231
|
+
correlation_id: input.slice?.runId ?? null,
|
|
232
|
+
workstream_id: workstreamId,
|
|
233
|
+
workstream_title: input.workstreamTitle ?? input.slice?.workstreamTitle ?? null,
|
|
234
|
+
task_id: taskId,
|
|
235
|
+
task_title: input.taskTitle ?? null,
|
|
236
|
+
milestone_ids: input.slice?.milestoneIds ?? null,
|
|
237
|
+
task_ids: input.slice?.taskIds ?? null,
|
|
238
|
+
domain: input.domain ?? input.slice?.domain ?? null,
|
|
239
|
+
required_skills: requiredSkills,
|
|
240
|
+
skill_pack: requiredSkills,
|
|
241
|
+
model_tier: input.modelTier ?? null,
|
|
242
|
+
scope: input.slice?.scope ?? input.run.scope,
|
|
243
|
+
actors: {
|
|
244
|
+
requester: {
|
|
245
|
+
agent_id: input.run.agentId ?? null,
|
|
246
|
+
agent_name: input.run.agentName ?? null,
|
|
247
|
+
},
|
|
248
|
+
dispatcher: {
|
|
249
|
+
agent_id: input.run.agentId ?? null,
|
|
250
|
+
agent_name: input.run.agentName ?? null,
|
|
251
|
+
},
|
|
252
|
+
executor: {
|
|
253
|
+
agent_id: input.slice?.agentId ?? null,
|
|
254
|
+
agent_name: input.slice?.agentName ?? null,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
scope_context: {
|
|
258
|
+
initiative_id: input.run.initiativeId,
|
|
259
|
+
workstream_id: workstreamId,
|
|
260
|
+
task_id: taskId,
|
|
261
|
+
task_ids: input.slice?.taskIds ?? null,
|
|
262
|
+
milestone_ids: input.slice?.milestoneIds ?? null,
|
|
263
|
+
},
|
|
264
|
+
next_actions: input.nextActions ?? null,
|
|
265
|
+
user_summary: input.userSummary ?? null,
|
|
266
|
+
...(input.extra ?? {}),
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
const autoContinueSliceRuns = new Map();
|
|
270
|
+
// Keep child handles alive so stdout/stderr capture remains reliable even when the process is detached.
|
|
271
|
+
const autoContinueSliceChildren = new Map();
|
|
272
|
+
const autoContinueSliceLastHeartbeatMs = new Map();
|
|
273
|
+
const clearAutoContinueSliceTransientState = (sliceRunId) => {
|
|
274
|
+
const id = (sliceRunId ?? "").trim();
|
|
275
|
+
if (!id)
|
|
276
|
+
return;
|
|
277
|
+
autoContinueSliceChildren.delete(id);
|
|
278
|
+
autoContinueSliceLastHeartbeatMs.delete(id);
|
|
279
|
+
};
|
|
280
|
+
const AUTO_CONTINUE_SLICE_TIMEOUT_MS = readBudgetEnvNumber("ORGX_AUTOPILOT_SLICE_TIMEOUT_MS", 55 * 60_000,
|
|
281
|
+
// Keep test runs fast; real-world defaults are still ~1h unless overridden.
|
|
282
|
+
{ min: 250, max: 6 * 60 * 60_000 });
|
|
283
|
+
const AUTO_CONTINUE_SLICE_LOG_STALL_MS = readBudgetEnvNumber("ORGX_AUTOPILOT_SLICE_LOG_STALL_MS", 6 * 60_000,
|
|
284
|
+
// Stall detection is only enforced when explicitly overridden; keep lower bound permissive for tests.
|
|
285
|
+
{ min: 20, max: 60 * 60_000 });
|
|
286
|
+
const AUTO_CONTINUE_SLICE_HEARTBEAT_MS = 12_000;
|
|
287
|
+
const AUTO_CONTINUE_SLICE_SCHEMA_FILENAME = "autopilot-slice-schema.json";
|
|
288
|
+
const AUTO_CONTINUE_SLICE_LOG_DIRNAME = "autopilot-logs";
|
|
289
|
+
// Prune old autopilot logs on engine init (7-day TTL, 50 MB cap).
|
|
290
|
+
const AUTOPILOT_LOG_TTL_MS = 7 * 24 * 60 * 60_000;
|
|
291
|
+
const AUTOPILOT_LOG_MAX_BYTES = 50 * 1024 * 1024;
|
|
292
|
+
(async () => {
|
|
293
|
+
try {
|
|
294
|
+
const logsDir = join(getOrgxPluginConfigDir(), AUTO_CONTINUE_SLICE_LOG_DIRNAME);
|
|
295
|
+
if (!existsSync(logsDir))
|
|
296
|
+
return;
|
|
297
|
+
const entries = await readdir(logsDir);
|
|
298
|
+
const now = Date.now();
|
|
299
|
+
const fileStats = [];
|
|
300
|
+
for (const name of entries) {
|
|
301
|
+
if (!name.endsWith(".log") && !name.endsWith(".output.json"))
|
|
302
|
+
continue;
|
|
303
|
+
const filePath = join(logsDir, name);
|
|
304
|
+
try {
|
|
305
|
+
const s = await stat(filePath);
|
|
306
|
+
if (s.mtimeMs < now - AUTOPILOT_LOG_TTL_MS) {
|
|
307
|
+
await unlink(filePath);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
fileStats.push({ name, path: filePath, mtimeMs: s.mtimeMs, size: s.size });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch { /* skip */ }
|
|
314
|
+
}
|
|
315
|
+
// Enforce total size cap by deleting oldest first.
|
|
316
|
+
fileStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
317
|
+
let totalSize = fileStats.reduce((sum, f) => sum + f.size, 0);
|
|
318
|
+
for (const f of fileStats) {
|
|
319
|
+
if (totalSize <= AUTOPILOT_LOG_MAX_BYTES)
|
|
320
|
+
break;
|
|
321
|
+
try {
|
|
322
|
+
await unlink(f.path);
|
|
323
|
+
}
|
|
324
|
+
catch { /* skip */ }
|
|
325
|
+
totalSize -= f.size;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch { /* best effort */ }
|
|
329
|
+
})();
|
|
330
|
+
const AUTO_FIX_DEFAULT_GRACE_MS = readBudgetEnvNumber("ORGX_AUTOPILOT_AUTOFIX_GRACE_MS", 10_000, { min: 1_000, max: 120_000 });
|
|
331
|
+
const AUTO_CONTINUE_SPAWN_GUARD_RETRY_MS = readBudgetEnvNumber("ORGX_AUTO_CONTINUE_SPAWN_GUARD_RETRY_MS", 15_000, { min: 1_000, max: 15 * 60_000 });
|
|
332
|
+
const autoFixByScope = new Map();
|
|
333
|
+
const autoContinueSpawnGuardRetryByTask = new Map();
|
|
334
|
+
const getSpawnGuardRetryAtMs = (initiativeId, taskId) => {
|
|
335
|
+
const taskKey = taskId.trim();
|
|
336
|
+
if (!taskKey)
|
|
337
|
+
return 0;
|
|
338
|
+
const entry = autoContinueSpawnGuardRetryByTask.get(taskKey);
|
|
339
|
+
if (!entry)
|
|
340
|
+
return 0;
|
|
341
|
+
if (entry.initiativeId !== initiativeId || entry.retryAtMs <= Date.now()) {
|
|
342
|
+
autoContinueSpawnGuardRetryByTask.delete(taskKey);
|
|
343
|
+
return 0;
|
|
344
|
+
}
|
|
345
|
+
return entry.retryAtMs;
|
|
346
|
+
};
|
|
347
|
+
const clearSpawnGuardRetryStateForInitiative = (initiativeId) => {
|
|
348
|
+
for (const [taskId, entry] of autoContinueSpawnGuardRetryByTask.entries()) {
|
|
349
|
+
if (entry.initiativeId !== initiativeId)
|
|
350
|
+
continue;
|
|
351
|
+
autoContinueSpawnGuardRetryByTask.delete(taskId);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const normalizeStatusValue = (value) => {
|
|
355
|
+
if (typeof value !== "string")
|
|
356
|
+
return "";
|
|
357
|
+
return value.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
358
|
+
};
|
|
359
|
+
const listActiveSliceRunIds = (run) => {
|
|
360
|
+
ensureRunInternals(run);
|
|
361
|
+
const ids = new Set();
|
|
362
|
+
for (const id of run.activeSliceRunIds ?? []) {
|
|
363
|
+
const normalized = (id ?? "").trim();
|
|
364
|
+
if (normalized)
|
|
365
|
+
ids.add(normalized);
|
|
366
|
+
}
|
|
367
|
+
for (const lane of Object.values(run.laneByWorkstreamId ?? {})) {
|
|
368
|
+
const activeRunId = (lane.activeRunId ?? "").trim();
|
|
369
|
+
if (activeRunId)
|
|
370
|
+
ids.add(activeRunId);
|
|
371
|
+
}
|
|
372
|
+
return Array.from(ids);
|
|
373
|
+
};
|
|
374
|
+
const upsertLane = (run, workstreamId, patch) => {
|
|
375
|
+
const normalizedWorkstreamId = workstreamId.trim();
|
|
376
|
+
if (!normalizedWorkstreamId) {
|
|
377
|
+
throw new Error("workstreamId is required");
|
|
378
|
+
}
|
|
379
|
+
const existing = run.laneByWorkstreamId[normalizedWorkstreamId] ?? {
|
|
380
|
+
workstreamId: normalizedWorkstreamId,
|
|
381
|
+
state: "idle",
|
|
382
|
+
activeRunId: null,
|
|
383
|
+
activeTaskIds: [],
|
|
384
|
+
blockedReason: null,
|
|
385
|
+
waitingOnWorkstreamIds: [],
|
|
386
|
+
retryAt: null,
|
|
387
|
+
updatedAt: new Date().toISOString(),
|
|
388
|
+
};
|
|
389
|
+
const next = {
|
|
390
|
+
...existing,
|
|
391
|
+
...patch,
|
|
392
|
+
workstreamId: normalizedWorkstreamId,
|
|
393
|
+
updatedAt: patch.updatedAt ?? new Date().toISOString(),
|
|
394
|
+
activeTaskIds: Array.isArray(patch.activeTaskIds)
|
|
395
|
+
? dedupeStrings(patch.activeTaskIds.map((id) => (id ?? "").trim()).filter(Boolean))
|
|
396
|
+
: existing.activeTaskIds,
|
|
397
|
+
waitingOnWorkstreamIds: Array.isArray(patch.waitingOnWorkstreamIds)
|
|
398
|
+
? dedupeStrings(patch.waitingOnWorkstreamIds.map((id) => (id ?? "").trim()).filter(Boolean))
|
|
399
|
+
: existing.waitingOnWorkstreamIds,
|
|
400
|
+
};
|
|
401
|
+
run.laneByWorkstreamId[normalizedWorkstreamId] = next;
|
|
402
|
+
return next;
|
|
403
|
+
};
|
|
404
|
+
const setLaneState = (run, input) => {
|
|
405
|
+
return upsertLane(run, input.workstreamId, {
|
|
406
|
+
state: input.state,
|
|
407
|
+
activeRunId: input.activeRunId === undefined ? undefined : (input.activeRunId ?? "").trim() || null,
|
|
408
|
+
activeTaskIds: input.activeTaskIds,
|
|
409
|
+
blockedReason: input.blockedReason === undefined
|
|
410
|
+
? undefined
|
|
411
|
+
: (input.blockedReason ?? "").trim() || null,
|
|
412
|
+
waitingOnWorkstreamIds: input.waitingOnWorkstreamIds,
|
|
413
|
+
retryAt: input.retryAt === undefined ? undefined : input.retryAt,
|
|
414
|
+
});
|
|
415
|
+
};
|
|
416
|
+
const removeActiveSliceFromRun = (run, input) => {
|
|
417
|
+
const sliceRunId = input.sliceRunId.trim();
|
|
418
|
+
if (!sliceRunId)
|
|
419
|
+
return;
|
|
420
|
+
run.activeSliceRunIds = run.activeSliceRunIds.filter((id) => id !== sliceRunId);
|
|
421
|
+
const taskIds = new Set(Array.isArray(input.taskIds)
|
|
422
|
+
? input.taskIds.map((id) => (id ?? "").trim()).filter(Boolean)
|
|
423
|
+
: []);
|
|
424
|
+
if (taskIds.size > 0) {
|
|
425
|
+
run.activeTaskIds = run.activeTaskIds.filter((id) => !taskIds.has(id));
|
|
426
|
+
}
|
|
427
|
+
const normalizedWorkstreamId = (input.workstreamId ?? "").trim();
|
|
428
|
+
if (normalizedWorkstreamId) {
|
|
429
|
+
const lane = run.laneByWorkstreamId[normalizedWorkstreamId];
|
|
430
|
+
if (lane && lane.activeRunId === sliceRunId) {
|
|
431
|
+
setLaneState(run, {
|
|
432
|
+
workstreamId: normalizedWorkstreamId,
|
|
433
|
+
state: lane.state === "blocked" ? "blocked" : "idle",
|
|
434
|
+
activeRunId: null,
|
|
435
|
+
activeTaskIds: [],
|
|
436
|
+
retryAt: lane.retryAt ?? null,
|
|
437
|
+
waitingOnWorkstreamIds: lane.waitingOnWorkstreamIds ?? [],
|
|
438
|
+
blockedReason: lane.state === "blocked" ? lane.blockedReason : null,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
const syncLegacyRunPointers = (run) => {
|
|
444
|
+
ensureRunInternals(run);
|
|
445
|
+
const activeIds = listActiveSliceRunIds(run);
|
|
446
|
+
run.activeSliceRunIds = activeIds;
|
|
447
|
+
run.activeTaskIds = dedupeStrings((run.activeTaskIds ?? []).map((id) => (id ?? "").trim()).filter(Boolean));
|
|
448
|
+
run.activeRunId = activeIds[0] ?? null;
|
|
449
|
+
run.activeTaskId = run.activeTaskIds[0] ?? null;
|
|
450
|
+
if (!run.activeRunId) {
|
|
451
|
+
run.activeTaskTokenEstimate = null;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
const ensureRunInternals = (run) => {
|
|
455
|
+
if (!Array.isArray(run.activeSliceRunIds))
|
|
456
|
+
run.activeSliceRunIds = [];
|
|
457
|
+
if (!Array.isArray(run.activeTaskIds))
|
|
458
|
+
run.activeTaskIds = [];
|
|
459
|
+
if (!run.laneByWorkstreamId || typeof run.laneByWorkstreamId !== "object") {
|
|
460
|
+
run.laneByWorkstreamId = {};
|
|
461
|
+
}
|
|
462
|
+
if (!Array.isArray(run.blockedWorkstreamIds))
|
|
463
|
+
run.blockedWorkstreamIds = [];
|
|
464
|
+
run.maxParallelSlices = normalizeMaxParallelSlices(run.maxParallelSlices, AUTO_CONTINUE_MAX_PARALLEL_DEFAULT);
|
|
465
|
+
run.parallelMode = normalizeParallelMode(run.parallelMode);
|
|
466
|
+
run.tokenBudget = normalizeTokenBudget(run.tokenBudget, defaultAutoContinueTokenBudget());
|
|
467
|
+
};
|
|
468
|
+
const recordLocalStatusOverrides = (input) => {
|
|
469
|
+
const initiativeId = input.initiativeId.trim();
|
|
470
|
+
if (!initiativeId)
|
|
471
|
+
return;
|
|
472
|
+
if (input.taskUpdates.length > 0) {
|
|
473
|
+
const scoped = localTaskStatusOverrides.get(initiativeId) ?? new Map();
|
|
474
|
+
for (const update of input.taskUpdates) {
|
|
475
|
+
const taskId = update.taskId.trim();
|
|
476
|
+
const status = normalizeStatusValue(update.status);
|
|
477
|
+
if (!taskId || !status)
|
|
478
|
+
continue;
|
|
479
|
+
scoped.set(taskId, {
|
|
480
|
+
status,
|
|
481
|
+
updatedAt: input.updatedAt,
|
|
482
|
+
reason: update.reason,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
if (scoped.size > 0) {
|
|
486
|
+
localTaskStatusOverrides.set(initiativeId, scoped);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (input.milestoneUpdates.length > 0) {
|
|
490
|
+
const scoped = localMilestoneStatusOverrides.get(initiativeId) ?? new Map();
|
|
491
|
+
for (const update of input.milestoneUpdates) {
|
|
492
|
+
const milestoneId = update.milestoneId.trim();
|
|
493
|
+
const status = normalizeStatusValue(update.status);
|
|
494
|
+
if (!milestoneId || !status)
|
|
495
|
+
continue;
|
|
496
|
+
scoped.set(milestoneId, {
|
|
497
|
+
status,
|
|
498
|
+
updatedAt: input.updatedAt,
|
|
499
|
+
reason: update.reason,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
if (scoped.size > 0) {
|
|
503
|
+
localMilestoneStatusOverrides.set(initiativeId, scoped);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
const applyLocalStatusOverridesToGraph = (initiativeId, nodeById) => {
|
|
508
|
+
const scopedTaskOverrides = localTaskStatusOverrides.get(initiativeId) ?? null;
|
|
509
|
+
if (scopedTaskOverrides) {
|
|
510
|
+
for (const [taskId, override] of scopedTaskOverrides.entries()) {
|
|
511
|
+
const node = nodeById.get(taskId);
|
|
512
|
+
if (!node || node.type !== "task")
|
|
513
|
+
continue;
|
|
514
|
+
const remoteStatus = normalizeStatusValue(node.status);
|
|
515
|
+
node.status = override.status;
|
|
516
|
+
if (remoteStatus === override.status) {
|
|
517
|
+
scopedTaskOverrides.delete(taskId);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (scopedTaskOverrides.size === 0) {
|
|
521
|
+
localTaskStatusOverrides.delete(initiativeId);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const scopedMilestoneOverrides = localMilestoneStatusOverrides.get(initiativeId) ?? null;
|
|
525
|
+
if (scopedMilestoneOverrides) {
|
|
526
|
+
for (const [milestoneId, override] of scopedMilestoneOverrides.entries()) {
|
|
527
|
+
const node = nodeById.get(milestoneId);
|
|
528
|
+
if (!node || node.type !== "milestone")
|
|
529
|
+
continue;
|
|
530
|
+
const remoteStatus = normalizeStatusValue(node.status);
|
|
531
|
+
node.status = override.status;
|
|
532
|
+
if (remoteStatus === override.status) {
|
|
533
|
+
scopedMilestoneOverrides.delete(milestoneId);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (scopedMilestoneOverrides.size === 0) {
|
|
537
|
+
localMilestoneStatusOverrides.delete(initiativeId);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
const isPendingDecisionStatus = (value) => {
|
|
542
|
+
const normalized = normalizeStatusValue(value);
|
|
543
|
+
if (!normalized)
|
|
544
|
+
return false;
|
|
545
|
+
return (normalized === "pending" ||
|
|
546
|
+
normalized === "open" ||
|
|
547
|
+
normalized === "requested" ||
|
|
548
|
+
normalized === "awaiting_review" ||
|
|
549
|
+
normalized === "awaiting_approval" ||
|
|
550
|
+
normalized === "queued");
|
|
551
|
+
};
|
|
552
|
+
const decisionMatchesWorkstream = (record, workstreamId, runId) => {
|
|
553
|
+
const directWorkstream = pickString(record, ["workstream_id", "workstreamId"])?.trim() ?? "";
|
|
554
|
+
if (directWorkstream && directWorkstream === workstreamId)
|
|
555
|
+
return true;
|
|
556
|
+
const correlationId = pickString(record, ["correlation_id", "correlationId"])?.trim() ?? "";
|
|
557
|
+
if (runId && correlationId && correlationId === runId)
|
|
558
|
+
return true;
|
|
559
|
+
const metadataRaw = record.metadata;
|
|
560
|
+
const metadata = metadataRaw && typeof metadataRaw === "object" && !Array.isArray(metadataRaw)
|
|
561
|
+
? metadataRaw
|
|
562
|
+
: null;
|
|
563
|
+
if (!metadata)
|
|
564
|
+
return false;
|
|
565
|
+
const nestedWorkstream = pickString(metadata, ["workstream_id", "workstreamId"])?.trim() ?? "";
|
|
566
|
+
if (nestedWorkstream && nestedWorkstream === workstreamId)
|
|
567
|
+
return true;
|
|
568
|
+
const nestedCorrelation = pickString(metadata, ["correlation_id", "correlationId"])?.trim() ?? "";
|
|
569
|
+
if (runId && nestedCorrelation && nestedCorrelation === runId)
|
|
570
|
+
return true;
|
|
571
|
+
return false;
|
|
572
|
+
};
|
|
573
|
+
const decisionIsBlocking = (record) => {
|
|
574
|
+
const direct = record.blocking;
|
|
575
|
+
if (typeof direct === "boolean")
|
|
576
|
+
return direct;
|
|
577
|
+
const metadataRaw = record.metadata;
|
|
578
|
+
if (metadataRaw && typeof metadataRaw === "object" && !Array.isArray(metadataRaw)) {
|
|
579
|
+
const nested = metadataRaw.blocking;
|
|
580
|
+
if (typeof nested === "boolean")
|
|
581
|
+
return nested;
|
|
582
|
+
}
|
|
583
|
+
return true;
|
|
584
|
+
};
|
|
585
|
+
const setLocalInitiativeStatusOverride = (initiativeId, status) => {
|
|
586
|
+
const normalizedId = initiativeId.trim();
|
|
587
|
+
if (!normalizedId)
|
|
588
|
+
return;
|
|
589
|
+
localInitiativeStatusOverrides.set(normalizedId, {
|
|
590
|
+
status,
|
|
591
|
+
updatedAt: new Date().toISOString(),
|
|
592
|
+
});
|
|
593
|
+
};
|
|
594
|
+
const clearLocalInitiativeStatusOverride = (initiativeId) => {
|
|
595
|
+
const normalizedId = initiativeId.trim();
|
|
596
|
+
if (!normalizedId)
|
|
597
|
+
return;
|
|
598
|
+
localInitiativeStatusOverrides.delete(normalizedId);
|
|
599
|
+
};
|
|
600
|
+
const applyLocalInitiativeOverrides = (rows) => {
|
|
601
|
+
const seenIds = new Set();
|
|
602
|
+
const next = rows.map((row) => {
|
|
603
|
+
const id = pickString(row, ["id"]);
|
|
604
|
+
if (!id)
|
|
605
|
+
return row;
|
|
606
|
+
seenIds.add(id);
|
|
607
|
+
const override = localInitiativeStatusOverrides.get(id);
|
|
608
|
+
if (!override)
|
|
609
|
+
return row;
|
|
610
|
+
return {
|
|
611
|
+
...row,
|
|
612
|
+
status: override.status,
|
|
613
|
+
updated_at: pickString(row, ["updated_at", "updatedAt"]) ?? override.updatedAt,
|
|
614
|
+
};
|
|
615
|
+
});
|
|
616
|
+
for (const [id, override] of localInitiativeStatusOverrides.entries()) {
|
|
617
|
+
if (seenIds.has(id))
|
|
618
|
+
continue;
|
|
619
|
+
next.push({
|
|
620
|
+
id,
|
|
621
|
+
title: `Initiative ${id.slice(0, 8)}`,
|
|
622
|
+
name: `Initiative ${id.slice(0, 8)}`,
|
|
623
|
+
summary: null,
|
|
624
|
+
status: override.status,
|
|
625
|
+
progress_pct: null,
|
|
626
|
+
created_at: override.updatedAt,
|
|
627
|
+
updated_at: override.updatedAt,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
return next;
|
|
631
|
+
};
|
|
632
|
+
const applyLocalInitiativeOverrideToGraph = (graph) => {
|
|
633
|
+
const override = localInitiativeStatusOverrides.get(graph.initiative.id) ?? null;
|
|
634
|
+
if (!override)
|
|
635
|
+
return graph;
|
|
636
|
+
return {
|
|
637
|
+
...graph,
|
|
638
|
+
initiative: {
|
|
639
|
+
...graph.initiative,
|
|
640
|
+
status: override.status,
|
|
641
|
+
},
|
|
642
|
+
nodes: graph.nodes.map((node) => node.type === "initiative" && node.id === graph.initiative.id
|
|
643
|
+
? { ...node, status: override.status }
|
|
644
|
+
: node),
|
|
645
|
+
};
|
|
646
|
+
};
|
|
647
|
+
function parseTokenBudget(value) {
|
|
648
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
649
|
+
if (value <= 0)
|
|
650
|
+
return null;
|
|
651
|
+
return Math.max(1_000, Math.round(value));
|
|
652
|
+
}
|
|
653
|
+
if (typeof value === "string") {
|
|
654
|
+
const trimmed = value.trim();
|
|
655
|
+
if (!trimmed)
|
|
656
|
+
return null;
|
|
657
|
+
const normalized = trimmed.toLowerCase();
|
|
658
|
+
if (normalized === "0" ||
|
|
659
|
+
normalized === "off" ||
|
|
660
|
+
normalized === "none" ||
|
|
661
|
+
normalized === "false" ||
|
|
662
|
+
normalized === "unlimited" ||
|
|
663
|
+
normalized === "null") {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
const parsed = Number(trimmed);
|
|
667
|
+
if (Number.isFinite(parsed)) {
|
|
668
|
+
if (parsed <= 0)
|
|
669
|
+
return null;
|
|
670
|
+
return Math.max(1_000, Math.round(parsed));
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
function normalizeTokenBudget(value, fallback) {
|
|
676
|
+
const parsed = parseTokenBudget(value);
|
|
677
|
+
if (parsed !== null)
|
|
678
|
+
return parsed;
|
|
679
|
+
return fallback;
|
|
680
|
+
}
|
|
681
|
+
function defaultAutoContinueTokenBudget() {
|
|
682
|
+
const explicitBudget = parseTokenBudget(process.env.ORGX_AUTO_CONTINUE_TOKEN_BUDGET);
|
|
683
|
+
if (explicitBudget !== null)
|
|
684
|
+
return explicitBudget;
|
|
685
|
+
// Token budget guardrails are now explicit-only: either pass a budget when starting
|
|
686
|
+
// auto-continue or set ORGX_AUTO_CONTINUE_TOKEN_BUDGET directly.
|
|
687
|
+
// Legacy fallback toggles (for example ORGX_AUTO_CONTINUE_ENFORCE_TOKEN_BUDGET)
|
|
688
|
+
// are intentionally ignored to prevent hidden auto-stop behavior.
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
function defaultAutoContinueMaxParallelSlices() {
|
|
692
|
+
return AUTO_CONTINUE_MAX_PARALLEL_DEFAULT;
|
|
693
|
+
}
|
|
694
|
+
function estimateTokensForDurationHours(durationHours) {
|
|
695
|
+
if (!Number.isFinite(durationHours) || durationHours <= 0)
|
|
696
|
+
return 0;
|
|
697
|
+
const raw = durationHours *
|
|
698
|
+
DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.tokensPerHour *
|
|
699
|
+
DEFAULT_TOKEN_BUDGET_ASSUMPTIONS.contingencyMultiplier;
|
|
700
|
+
return Math.max(0, Math.round(raw));
|
|
701
|
+
}
|
|
702
|
+
// Helpers used by previous task-level auto-continue implementation were removed in v2.
|
|
703
|
+
// readOpenClawSessionSummary was used by the previous task-level auto-continue implementation.
|
|
704
|
+
// Autopilot v2 dispatches workstream slices via runtime workers (codex/claude-code)
|
|
705
|
+
// and does not rely on OpenClaw session JSONL.
|
|
706
|
+
async function fetchInitiativeEntity(initiativeId) {
|
|
707
|
+
try {
|
|
708
|
+
const list = await client.listEntities("initiative", { limit: 200 });
|
|
709
|
+
const match = list.data.find((candidate) => String(candidate?.id ?? "") === initiativeId);
|
|
710
|
+
return match ?? null;
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
async function updateInitiativeMetadata(initiativeId, patch) {
|
|
717
|
+
const existing = await fetchInitiativeEntity(initiativeId);
|
|
718
|
+
const existingMetaRaw = existing && typeof existing === "object"
|
|
719
|
+
? existing.metadata
|
|
720
|
+
: null;
|
|
721
|
+
const existingMeta = existingMetaRaw && typeof existingMetaRaw === "object" && !Array.isArray(existingMetaRaw)
|
|
722
|
+
? existingMetaRaw
|
|
723
|
+
: {};
|
|
724
|
+
const nextMeta = { ...existingMeta, ...patch };
|
|
725
|
+
await client.updateEntity("initiative", initiativeId, { metadata: nextMeta });
|
|
726
|
+
}
|
|
727
|
+
async function updateInitiativeAutoContinueState(input) {
|
|
728
|
+
syncLegacyRunPointers(input.run);
|
|
729
|
+
const now = new Date().toISOString();
|
|
730
|
+
const laneStates = Object.values(input.run.laneByWorkstreamId ?? {}).map((lane) => ({
|
|
731
|
+
workstream_id: lane.workstreamId,
|
|
732
|
+
state: lane.state,
|
|
733
|
+
active_run_id: lane.activeRunId,
|
|
734
|
+
active_task_ids: lane.activeTaskIds,
|
|
735
|
+
blocked_reason: lane.blockedReason,
|
|
736
|
+
waiting_on_workstream_ids: lane.waitingOnWorkstreamIds,
|
|
737
|
+
retry_at: lane.retryAt,
|
|
738
|
+
updated_at: lane.updatedAt,
|
|
739
|
+
}));
|
|
740
|
+
const patch = {
|
|
741
|
+
auto_continue_enabled: input.run.status === "running" || input.run.status === "stopping",
|
|
742
|
+
auto_continue_status: input.run.status,
|
|
743
|
+
auto_continue_stop_reason: input.run.stopReason,
|
|
744
|
+
auto_continue_started_at: input.run.startedAt,
|
|
745
|
+
auto_continue_stopped_at: input.run.stoppedAt,
|
|
746
|
+
auto_continue_updated_at: now,
|
|
747
|
+
auto_continue_token_budget: input.run.tokenBudget,
|
|
748
|
+
auto_continue_tokens_used: input.run.tokensUsed,
|
|
749
|
+
auto_continue_active_task_id: input.run.activeTaskId,
|
|
750
|
+
auto_continue_active_run_id: input.run.activeRunId,
|
|
751
|
+
auto_continue_active_task_ids: input.run.activeTaskIds,
|
|
752
|
+
auto_continue_active_run_ids: input.run.activeSliceRunIds,
|
|
753
|
+
auto_continue_active_task_token_estimate: input.run.activeTaskTokenEstimate,
|
|
754
|
+
auto_continue_last_task_id: input.run.lastTaskId,
|
|
755
|
+
auto_continue_last_run_id: input.run.lastRunId,
|
|
756
|
+
auto_continue_include_verification: input.run.includeVerification,
|
|
757
|
+
auto_continue_workstream_filter: input.run.allowedWorkstreamIds,
|
|
758
|
+
auto_continue_parallel_mode: input.run.parallelMode,
|
|
759
|
+
auto_continue_max_parallel: input.run.maxParallelSlices,
|
|
760
|
+
auto_continue_lane_states: laneStates,
|
|
761
|
+
auto_continue_blocked_workstream_ids: input.run.blockedWorkstreamIds,
|
|
762
|
+
auto_continue_ignore_spawn_guard_rate_limit: input.run.ignoreSpawnGuardRateLimit,
|
|
763
|
+
...(input.run.lastError ? { auto_continue_last_error: input.run.lastError } : {}),
|
|
764
|
+
};
|
|
765
|
+
await updateInitiativeMetadata(input.initiativeId, patch);
|
|
766
|
+
}
|
|
767
|
+
async function stopAutoContinueRun(input) {
|
|
768
|
+
const now = new Date().toISOString();
|
|
769
|
+
ensureRunInternals(input.run);
|
|
770
|
+
const activeRunIds = listActiveSliceRunIds(input.run);
|
|
771
|
+
input.run.status = "stopped";
|
|
772
|
+
input.run.stopReason = input.reason;
|
|
773
|
+
input.run.stoppedAt = now;
|
|
774
|
+
input.run.updatedAt = now;
|
|
775
|
+
input.run.stopRequested = false;
|
|
776
|
+
input.run.activeSliceRunIds = [];
|
|
777
|
+
input.run.activeTaskIds = [];
|
|
778
|
+
input.run.activeRunId = null;
|
|
779
|
+
input.run.activeTaskId = null;
|
|
780
|
+
input.run.activeTaskTokenEstimate = null;
|
|
781
|
+
for (const lane of Object.values(input.run.laneByWorkstreamId ?? {})) {
|
|
782
|
+
if (lane.activeRunId || lane.activeTaskIds.length > 0) {
|
|
783
|
+
setLaneState(input.run, {
|
|
784
|
+
workstreamId: lane.workstreamId,
|
|
785
|
+
state: lane.state === "blocked" ? "blocked" : "idle",
|
|
786
|
+
activeRunId: null,
|
|
787
|
+
activeTaskIds: [],
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if (input.error)
|
|
792
|
+
input.run.lastError = input.error;
|
|
793
|
+
clearSpawnGuardRetryStateForInitiative(input.run.initiativeId);
|
|
794
|
+
for (const runId of activeRunIds) {
|
|
795
|
+
clearAutoContinueSliceTransientState(runId);
|
|
796
|
+
}
|
|
797
|
+
// Only pause the initiative on non-terminal stops (error, blocked, user-requested).
|
|
798
|
+
// Completed / budget-exhausted runs should not override the initiative status.
|
|
799
|
+
if (input.reason !== "completed" && input.reason !== "budget_exhausted") {
|
|
800
|
+
try {
|
|
801
|
+
await client.updateEntity("initiative", input.run.initiativeId, {
|
|
802
|
+
status: "paused",
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
catch {
|
|
806
|
+
// best effort
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
await updateInitiativeAutoContinueState({
|
|
811
|
+
initiativeId: input.run.initiativeId,
|
|
812
|
+
run: input.run,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
816
|
+
// best effort
|
|
817
|
+
}
|
|
818
|
+
const primaryActiveRunId = activeRunIds[0] ?? null;
|
|
819
|
+
const scopedWorkstreamId = Array.isArray(input.run.allowedWorkstreamIds) && input.run.allowedWorkstreamIds.length === 1
|
|
820
|
+
? input.run.allowedWorkstreamIds[0]
|
|
821
|
+
: null;
|
|
822
|
+
const scopeSuffix = scopedWorkstreamId ? ` [workstream ${scopedWorkstreamId}]` : "";
|
|
823
|
+
const decisionRequired = input.reason === "blocked" && input.decisionRequired === true;
|
|
824
|
+
const decisionIds = Array.isArray(input.decisionIds)
|
|
825
|
+
? input.decisionIds
|
|
826
|
+
.filter((entry) => typeof entry === "string")
|
|
827
|
+
.map((entry) => entry.trim())
|
|
828
|
+
.filter(Boolean)
|
|
829
|
+
: [];
|
|
830
|
+
const budgetValue = typeof input.run.tokenBudget === "number" ? input.run.tokenBudget : "unbounded";
|
|
831
|
+
const message = input.reason === "completed"
|
|
832
|
+
? `Autopilot stopped: current dispatch scope completed${scopeSuffix}.`
|
|
833
|
+
: input.reason === "budget_exhausted"
|
|
834
|
+
? `Autopilot stopped: token budget exhausted (${input.run.tokensUsed}/${budgetValue}).`
|
|
835
|
+
: input.reason === "stopped"
|
|
836
|
+
? `Autopilot stopped by user request${scopeSuffix}.`
|
|
837
|
+
: input.reason === "blocked"
|
|
838
|
+
? decisionRequired
|
|
839
|
+
? `Autopilot stopped: blocked awaiting decision${scopeSuffix}.`
|
|
840
|
+
: `Autopilot stopped: blocked${scopeSuffix}.`
|
|
841
|
+
: `Autopilot stopped due to error${scopeSuffix}.`;
|
|
842
|
+
const phase = input.reason === "completed"
|
|
843
|
+
? "completed"
|
|
844
|
+
: input.reason === "blocked" || input.reason === "error"
|
|
845
|
+
? "blocked"
|
|
846
|
+
: "review";
|
|
847
|
+
const level = input.reason === "completed"
|
|
848
|
+
? "info"
|
|
849
|
+
: input.reason === "budget_exhausted" || input.reason === "stopped"
|
|
850
|
+
? "warn"
|
|
851
|
+
: "error";
|
|
852
|
+
const errorLocation = input.reason === "blocked"
|
|
853
|
+
? "mission-control.auto-continue.engine.blocked"
|
|
854
|
+
: input.reason === "error"
|
|
855
|
+
? "mission-control.auto-continue.engine.error"
|
|
856
|
+
: null;
|
|
857
|
+
const stopRunContext = {
|
|
858
|
+
initiativeId: input.run.initiativeId,
|
|
859
|
+
agentId: input.run.agentId,
|
|
860
|
+
agentName: input.run.agentName,
|
|
861
|
+
scope: input.run.scope,
|
|
862
|
+
};
|
|
863
|
+
await emitActivitySafe({
|
|
864
|
+
initiativeId: input.run.initiativeId,
|
|
865
|
+
runId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
|
|
866
|
+
correlationId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
|
|
867
|
+
phase,
|
|
868
|
+
level,
|
|
869
|
+
progressPct: input.reason === "completed" ? 100 : input.reason === "blocked" ? 65 : 0,
|
|
870
|
+
nextStep: input.reason === "completed"
|
|
871
|
+
? "Select the next queue item or enable autoplay for continuous dispatch."
|
|
872
|
+
: input.reason === "blocked"
|
|
873
|
+
? "Resolve blocker decisions, then resume or restart autoplay."
|
|
874
|
+
: input.reason === "budget_exhausted"
|
|
875
|
+
? "Increase token budget or scope down work before restarting autoplay."
|
|
876
|
+
: input.reason === "stopped"
|
|
877
|
+
? "Restart autoplay when ready."
|
|
878
|
+
: "Inspect error details and relaunch once fixed.",
|
|
879
|
+
message,
|
|
880
|
+
metadata: {
|
|
881
|
+
...buildSliceEnrichment({
|
|
882
|
+
run: stopRunContext,
|
|
883
|
+
workstreamId: scopedWorkstreamId,
|
|
884
|
+
event: "auto_continue_stopped",
|
|
885
|
+
}),
|
|
886
|
+
stop_reason: input.reason,
|
|
887
|
+
active_run_id: primaryActiveRunId,
|
|
888
|
+
active_run_ids: activeRunIds,
|
|
889
|
+
last_run_id: input.run.lastRunId,
|
|
890
|
+
token_budget: input.run.tokenBudget,
|
|
891
|
+
tokens_used: input.run.tokensUsed,
|
|
892
|
+
allowed_workstream_ids: input.run.allowedWorkstreamIds,
|
|
893
|
+
max_parallel_slices: input.run.maxParallelSlices,
|
|
894
|
+
scope_workstream_id: scopedWorkstreamId,
|
|
895
|
+
decision_required: decisionRequired,
|
|
896
|
+
decision_ids: decisionIds,
|
|
897
|
+
decision_count: decisionIds.length,
|
|
898
|
+
last_error: input.run.lastError,
|
|
899
|
+
error_location: errorLocation,
|
|
900
|
+
},
|
|
901
|
+
});
|
|
902
|
+
// Emit autopilot_transition event for state observers.
|
|
903
|
+
try {
|
|
904
|
+
await emitActivitySafe({
|
|
905
|
+
initiativeId: input.run.initiativeId,
|
|
906
|
+
runId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
|
|
907
|
+
correlationId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
|
|
908
|
+
phase,
|
|
909
|
+
level: "info",
|
|
910
|
+
progressPct: input.reason === "completed" ? 100 : input.reason === "blocked" ? 65 : 0,
|
|
911
|
+
message: `Autopilot state: running → ${input.reason === "completed" ? "idle" : input.reason === "stopped" ? "idle" : input.reason}.`,
|
|
912
|
+
metadata: {
|
|
913
|
+
...buildSliceEnrichment({
|
|
914
|
+
run: stopRunContext,
|
|
915
|
+
workstreamId: scopedWorkstreamId,
|
|
916
|
+
event: "autopilot_transition",
|
|
917
|
+
actionType: "run_state_transition",
|
|
918
|
+
}),
|
|
919
|
+
old_state: "running",
|
|
920
|
+
new_state: input.reason === "completed" || input.reason === "stopped" ? "idle" : input.reason === "blocked" ? "blocked" : input.reason === "error" ? "error" : "idle",
|
|
921
|
+
reason: input.reason,
|
|
922
|
+
workspace_id: input.run.allowedWorkstreamIds?.[0] ?? null,
|
|
923
|
+
},
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
catch {
|
|
927
|
+
// best effort
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const codexBinResolver = createCodexBinResolver();
|
|
931
|
+
const resolveCodexBinInfo = () => codexBinResolver.resolveCodexBinInfo();
|
|
932
|
+
const { spawnCodexSliceWorker, writeRuntimeEvent } = createAutopilotRuntime({
|
|
933
|
+
filename: __filename,
|
|
934
|
+
autoContinueSliceChildren,
|
|
935
|
+
resolveByokEnvOverrides,
|
|
936
|
+
safeErrorMessage,
|
|
937
|
+
resolveCodexBinInfo,
|
|
938
|
+
upsertRuntimeInstanceFromHook,
|
|
939
|
+
broadcastRuntimeSse,
|
|
940
|
+
clearSnapshotResponseCache,
|
|
941
|
+
});
|
|
942
|
+
async function tickAutoContinueRun(run) {
|
|
943
|
+
if (run.status !== "running" && run.status !== "stopping")
|
|
944
|
+
return;
|
|
945
|
+
const now = new Date().toISOString();
|
|
946
|
+
syncLegacyRunPointers(run);
|
|
947
|
+
// 1) Reconcile each active slice lane and register outcomes when complete.
|
|
948
|
+
const activeRunIdsForTick = listActiveSliceRunIds(run);
|
|
949
|
+
for (const activeRunIdForTick of activeRunIdsForTick) {
|
|
950
|
+
run.activeRunId = activeRunIdForTick;
|
|
951
|
+
const slice = autoContinueSliceRuns.get(activeRunIdForTick) ?? null;
|
|
952
|
+
if (!slice) {
|
|
953
|
+
// Legacy/unknown pointer; clear so we can continue.
|
|
954
|
+
removeActiveSliceFromRun(run, { sliceRunId: activeRunIdForTick });
|
|
955
|
+
run.activeRunId = null;
|
|
956
|
+
run.activeTaskId = null;
|
|
957
|
+
run.updatedAt = now;
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
const pid = slice.pid;
|
|
961
|
+
if (pid && pidAlive(pid)) {
|
|
962
|
+
const nowMs = Date.now();
|
|
963
|
+
const outputTail = readFileTailSafe(slice.outputPath, 240_000);
|
|
964
|
+
const outputParsed = outputTail
|
|
965
|
+
? parseSliceResult(outputTail)
|
|
966
|
+
: null;
|
|
967
|
+
const outputComplete = Boolean(outputParsed &&
|
|
968
|
+
typeof outputParsed.status === "string" &&
|
|
969
|
+
typeof outputParsed.summary === "string");
|
|
970
|
+
if (outputComplete) {
|
|
971
|
+
// Some platforms can report a just-finished detached process as still "alive" (zombie).
|
|
972
|
+
// Best-effort stop, then clear pid so we can proceed to parse the output contract below.
|
|
973
|
+
try {
|
|
974
|
+
await stopProcess(pid);
|
|
975
|
+
}
|
|
976
|
+
catch {
|
|
977
|
+
// best effort
|
|
978
|
+
}
|
|
979
|
+
slice.pid = null;
|
|
980
|
+
autoContinueSliceRuns.set(slice.runId, slice);
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
const lastHeartbeat = autoContinueSliceLastHeartbeatMs.get(slice.runId) ?? 0;
|
|
984
|
+
if (nowMs - lastHeartbeat >= AUTO_CONTINUE_SLICE_HEARTBEAT_MS) {
|
|
985
|
+
try {
|
|
986
|
+
writeRuntimeEvent({
|
|
987
|
+
sourceClient: slice.sourceClient,
|
|
988
|
+
event: "heartbeat",
|
|
989
|
+
runId: slice.runId,
|
|
990
|
+
initiativeId: slice.initiativeId,
|
|
991
|
+
workstreamId: slice.workstreamId,
|
|
992
|
+
taskId: slice.taskIds[0] ?? null,
|
|
993
|
+
agentId: slice.agentId,
|
|
994
|
+
agentName: slice.agentName,
|
|
995
|
+
phase: "execution",
|
|
996
|
+
message: `Autopilot slice running: ${slice.workstreamTitle ?? slice.workstreamId}`,
|
|
997
|
+
metadata: {
|
|
998
|
+
event: "autopilot_slice_heartbeat",
|
|
999
|
+
requested_by_agent_id: run.agentId,
|
|
1000
|
+
requested_by_agent_name: run.agentName,
|
|
1001
|
+
domain: slice.domain,
|
|
1002
|
+
required_skills: slice.requiredSkills,
|
|
1003
|
+
behavior_config_id: slice.behaviorConfigId,
|
|
1004
|
+
behavior_config_version: slice.behaviorConfigVersion,
|
|
1005
|
+
behavior_config_hash: slice.behaviorConfigHash,
|
|
1006
|
+
policy_source: slice.behaviorPolicySource,
|
|
1007
|
+
behavior_automation_level: slice.behaviorAutomationLevel,
|
|
1008
|
+
workstream_id: slice.workstreamId,
|
|
1009
|
+
workstream_title: slice.workstreamTitle ?? null,
|
|
1010
|
+
task_ids: slice.taskIds,
|
|
1011
|
+
milestone_ids: slice.milestoneIds,
|
|
1012
|
+
log_path: slice.logPath,
|
|
1013
|
+
output_path: slice.outputPath,
|
|
1014
|
+
...mockMeta(slice),
|
|
1015
|
+
},
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
catch {
|
|
1019
|
+
// best effort
|
|
1020
|
+
}
|
|
1021
|
+
autoContinueSliceLastHeartbeatMs.set(slice.runId, nowMs);
|
|
1022
|
+
}
|
|
1023
|
+
const startedAtEpochMs = Date.parse(slice.startedAt);
|
|
1024
|
+
const fallbackEpochMs = Number.isFinite(startedAtEpochMs) ? startedAtEpochMs : nowMs;
|
|
1025
|
+
const outputUpdatedAtEpochMs = fileUpdatedAtEpochMs(slice.outputPath, fallbackEpochMs);
|
|
1026
|
+
const logUpdatedAtEpochMs = fileUpdatedAtEpochMs(slice.logPath, fallbackEpochMs);
|
|
1027
|
+
// Some codex runs only materialize output.json at process exit. Treat recent log activity
|
|
1028
|
+
// as liveness signal so active slices are not falsely marked as stalled.
|
|
1029
|
+
const stallUpdatedAtEpochMs = Math.max(outputUpdatedAtEpochMs, logUpdatedAtEpochMs);
|
|
1030
|
+
const logTail = readFileTailSafe(slice.logPath, 64_000);
|
|
1031
|
+
const mcpHandshake = detectMcpHandshakeFailure(logTail);
|
|
1032
|
+
if (mcpHandshake) {
|
|
1033
|
+
try {
|
|
1034
|
+
await stopProcess(pid);
|
|
1035
|
+
}
|
|
1036
|
+
catch {
|
|
1037
|
+
// best effort
|
|
1038
|
+
}
|
|
1039
|
+
slice.status = "error";
|
|
1040
|
+
slice.finishedAt = now;
|
|
1041
|
+
slice.updatedAt = now;
|
|
1042
|
+
slice.lastError = `Autopilot slice failed to initialize MCP server${mcpHandshake.server ? ` (${mcpHandshake.server})` : ""}.`;
|
|
1043
|
+
autoContinueSliceRuns.set(slice.runId, slice);
|
|
1044
|
+
run.lastError = slice.lastError;
|
|
1045
|
+
run.updatedAt = now;
|
|
1046
|
+
clearAutoContinueSliceTransientState(slice.runId);
|
|
1047
|
+
await emitActivitySafe({
|
|
1048
|
+
initiativeId: run.initiativeId,
|
|
1049
|
+
runId: slice.runId,
|
|
1050
|
+
correlationId: slice.runId,
|
|
1051
|
+
phase: "blocked",
|
|
1052
|
+
level: "error",
|
|
1053
|
+
progressPct: 55,
|
|
1054
|
+
nextStep: "Review MCP diagnostics, then choose retry, skip, or pause for investigation.",
|
|
1055
|
+
message: `Autopilot slice MCP failed: ${slice.workstreamTitle ?? slice.workstreamId}.`,
|
|
1056
|
+
metadata: {
|
|
1057
|
+
...buildSliceEnrichment({
|
|
1058
|
+
run,
|
|
1059
|
+
slice,
|
|
1060
|
+
workstreamId: slice.workstreamId,
|
|
1061
|
+
workstreamTitle: slice.workstreamTitle ?? null,
|
|
1062
|
+
domain: slice.domain,
|
|
1063
|
+
requiredSkills: slice.requiredSkills,
|
|
1064
|
+
event: "autopilot_slice_mcp_handshake_failed",
|
|
1065
|
+
}),
|
|
1066
|
+
error_location: "mission-control.auto-continue.engine.slice.mcp-handshake",
|
|
1067
|
+
mcp_server: mcpHandshake.server,
|
|
1068
|
+
mcp_line: mcpHandshake.line,
|
|
1069
|
+
log_path: slice.logPath,
|
|
1070
|
+
output_path: slice.outputPath,
|
|
1071
|
+
...mockMeta(slice),
|
|
1072
|
+
},
|
|
1073
|
+
});
|
|
1074
|
+
const decisionResult = await requestDecisionQueued({
|
|
1075
|
+
initiativeId: run.initiativeId,
|
|
1076
|
+
correlationId: slice.runId,
|
|
1077
|
+
title: `Autopilot slice MCP failed: ${slice.workstreamTitle ?? slice.workstreamId}`,
|
|
1078
|
+
summary: `MCP handshake failed${mcpHandshake.server ? ` for ${mcpHandshake.server}` : ""}. Review logs/output and decide whether to retry or pause autopilot.`,
|
|
1079
|
+
urgency: "high",
|
|
1080
|
+
options: [
|
|
1081
|
+
"Retry this workstream slice",
|
|
1082
|
+
"Pause autopilot and investigate",
|
|
1083
|
+
"Skip this workstream for now",
|
|
1084
|
+
],
|
|
1085
|
+
blocking: true,
|
|
1086
|
+
decisionType: "autopilot_failure",
|
|
1087
|
+
workstreamId: slice.workstreamId,
|
|
1088
|
+
agentId: slice.agentId,
|
|
1089
|
+
sourceSystem: "orgx-autopilot",
|
|
1090
|
+
conflictSource: "mcp_handshake_failure",
|
|
1091
|
+
dedupeKey: [
|
|
1092
|
+
"autopilot",
|
|
1093
|
+
run.initiativeId,
|
|
1094
|
+
slice.workstreamId,
|
|
1095
|
+
"mcp_handshake_failure",
|
|
1096
|
+
mcpHandshake.server ?? "unknown",
|
|
1097
|
+
].join(":"),
|
|
1098
|
+
recommendedAction: "Retry once. If it fails again, pause autopilot and inspect MCP server configuration.",
|
|
1099
|
+
sourceRunId: slice.runId,
|
|
1100
|
+
sourceRef: {
|
|
1101
|
+
run_id: slice.runId,
|
|
1102
|
+
workstream_id: slice.workstreamId,
|
|
1103
|
+
mcp_server: mcpHandshake.server ?? null,
|
|
1104
|
+
},
|
|
1105
|
+
evidenceRefs: [
|
|
1106
|
+
{
|
|
1107
|
+
evidence_type: "mcp_diagnostic",
|
|
1108
|
+
title: "MCP handshake failure",
|
|
1109
|
+
summary: `MCP handshake failed${mcpHandshake.server ? ` for ${mcpHandshake.server}` : ""}.`,
|
|
1110
|
+
source_pointer: slice.logPath,
|
|
1111
|
+
payload: {
|
|
1112
|
+
mcp_server: mcpHandshake.server ?? null,
|
|
1113
|
+
mcp_line: mcpHandshake.line ?? null,
|
|
1114
|
+
output_path: slice.outputPath,
|
|
1115
|
+
},
|
|
1116
|
+
},
|
|
1117
|
+
],
|
|
1118
|
+
});
|
|
1119
|
+
setLaneState(run, {
|
|
1120
|
+
workstreamId: slice.workstreamId,
|
|
1121
|
+
state: "blocked",
|
|
1122
|
+
activeRunId: null,
|
|
1123
|
+
activeTaskIds: [],
|
|
1124
|
+
blockedReason: slice.lastError,
|
|
1125
|
+
waitingOnWorkstreamIds: [],
|
|
1126
|
+
retryAt: null,
|
|
1127
|
+
});
|
|
1128
|
+
await stopAutoContinueRun({
|
|
1129
|
+
run,
|
|
1130
|
+
reason: "blocked",
|
|
1131
|
+
error: slice.lastError,
|
|
1132
|
+
decisionRequired: decisionResult.queued,
|
|
1133
|
+
decisionIds: decisionResult.decisionIds,
|
|
1134
|
+
});
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
const scopeTimeoutMs = AUTO_CONTINUE_SLICE_TIMEOUT_MS * SLICE_SCOPE_TIMEOUT_MULTIPLIER[slice.scope ?? "task"];
|
|
1138
|
+
const killDecision = shouldKillWorker({
|
|
1139
|
+
nowEpochMs: nowMs,
|
|
1140
|
+
startedAtEpochMs: fallbackEpochMs,
|
|
1141
|
+
logUpdatedAtEpochMs: stallUpdatedAtEpochMs,
|
|
1142
|
+
}, { timeoutMs: scopeTimeoutMs, stallMs: AUTO_CONTINUE_SLICE_LOG_STALL_MS });
|
|
1143
|
+
if (killDecision.kill) {
|
|
1144
|
+
try {
|
|
1145
|
+
await stopProcess(pid);
|
|
1146
|
+
}
|
|
1147
|
+
catch {
|
|
1148
|
+
// best effort
|
|
1149
|
+
}
|
|
1150
|
+
slice.status = "error";
|
|
1151
|
+
slice.finishedAt = now;
|
|
1152
|
+
slice.updatedAt = now;
|
|
1153
|
+
slice.lastError =
|
|
1154
|
+
killDecision.kind === "timeout"
|
|
1155
|
+
? `Autopilot slice timed out after ${Math.round(scopeTimeoutMs / 60_000)} minutes.`
|
|
1156
|
+
: `Autopilot slice stalled (no output) for ${Math.round(AUTO_CONTINUE_SLICE_LOG_STALL_MS / 60_000)} minutes.`;
|
|
1157
|
+
autoContinueSliceRuns.set(slice.runId, slice);
|
|
1158
|
+
run.lastError = slice.lastError;
|
|
1159
|
+
run.updatedAt = now;
|
|
1160
|
+
clearAutoContinueSliceTransientState(slice.runId);
|
|
1161
|
+
const event = killDecision.kind === "timeout" ? "autopilot_slice_timeout" : "autopilot_slice_log_stall";
|
|
1162
|
+
const humanLabel = killDecision.kind === "timeout" ? "timed out" : "stalled";
|
|
1163
|
+
await emitActivitySafe({
|
|
1164
|
+
initiativeId: run.initiativeId,
|
|
1165
|
+
runId: slice.runId,
|
|
1166
|
+
correlationId: slice.runId,
|
|
1167
|
+
phase: "blocked",
|
|
1168
|
+
level: "error",
|
|
1169
|
+
progressPct: 55,
|
|
1170
|
+
nextStep: "Open logs/output, decide retry or pause, and capture blocker context for handoff.",
|
|
1171
|
+
message: `Autopilot slice ${humanLabel}: ${slice.workstreamTitle ?? slice.workstreamId}.`,
|
|
1172
|
+
metadata: {
|
|
1173
|
+
...buildSliceEnrichment({
|
|
1174
|
+
run,
|
|
1175
|
+
slice,
|
|
1176
|
+
workstreamId: slice.workstreamId,
|
|
1177
|
+
workstreamTitle: slice.workstreamTitle ?? null,
|
|
1178
|
+
domain: slice.domain,
|
|
1179
|
+
requiredSkills: slice.requiredSkills,
|
|
1180
|
+
event,
|
|
1181
|
+
}),
|
|
1182
|
+
error_location: killDecision.kind === "timeout"
|
|
1183
|
+
? "mission-control.auto-continue.engine.slice.timeout"
|
|
1184
|
+
: "mission-control.auto-continue.engine.slice.stall",
|
|
1185
|
+
log_path: slice.logPath,
|
|
1186
|
+
output_path: slice.outputPath,
|
|
1187
|
+
reason: killDecision.reason,
|
|
1188
|
+
elapsed_ms: killDecision.elapsedMs,
|
|
1189
|
+
idle_ms: killDecision.idleMs,
|
|
1190
|
+
...mockMeta(slice),
|
|
1191
|
+
},
|
|
1192
|
+
});
|
|
1193
|
+
const decisionResult = await requestDecisionQueued({
|
|
1194
|
+
initiativeId: run.initiativeId,
|
|
1195
|
+
correlationId: slice.runId,
|
|
1196
|
+
title: `Autopilot slice ${humanLabel}: ${slice.workstreamTitle ?? slice.workstreamId}`,
|
|
1197
|
+
summary: "The slice was terminated because it stopped making progress. Review logs/output and decide whether to retry or pause autopilot.",
|
|
1198
|
+
urgency: "high",
|
|
1199
|
+
options: [
|
|
1200
|
+
"Retry this workstream slice",
|
|
1201
|
+
"Pause autopilot and investigate",
|
|
1202
|
+
"Skip this workstream for now",
|
|
1203
|
+
],
|
|
1204
|
+
blocking: true,
|
|
1205
|
+
decisionType: "autopilot_failure",
|
|
1206
|
+
workstreamId: slice.workstreamId,
|
|
1207
|
+
agentId: slice.agentId,
|
|
1208
|
+
sourceSystem: "orgx-autopilot",
|
|
1209
|
+
conflictSource: killDecision.kind === "timeout"
|
|
1210
|
+
? "slice_timeout"
|
|
1211
|
+
: "slice_stall_no_output",
|
|
1212
|
+
dedupeKey: [
|
|
1213
|
+
"autopilot",
|
|
1214
|
+
run.initiativeId,
|
|
1215
|
+
slice.workstreamId,
|
|
1216
|
+
killDecision.kind === "timeout"
|
|
1217
|
+
? "slice_timeout"
|
|
1218
|
+
: "slice_stall_no_output",
|
|
1219
|
+
].join(":"),
|
|
1220
|
+
recommendedAction: "Review logs and output, then retry once. If repeated, pause autopilot and investigate worker/runtime health.",
|
|
1221
|
+
sourceRunId: slice.runId,
|
|
1222
|
+
sourceRef: {
|
|
1223
|
+
run_id: slice.runId,
|
|
1224
|
+
workstream_id: slice.workstreamId,
|
|
1225
|
+
kill_kind: killDecision.kind,
|
|
1226
|
+
elapsed_ms: killDecision.elapsedMs,
|
|
1227
|
+
idle_ms: killDecision.idleMs,
|
|
1228
|
+
},
|
|
1229
|
+
evidenceRefs: [
|
|
1230
|
+
{
|
|
1231
|
+
evidence_type: killDecision.kind === "timeout"
|
|
1232
|
+
? "timeout_diagnostic"
|
|
1233
|
+
: "stall_diagnostic",
|
|
1234
|
+
title: killDecision.kind === "timeout"
|
|
1235
|
+
? "Slice timed out"
|
|
1236
|
+
: "Slice stalled",
|
|
1237
|
+
summary: killDecision.reason,
|
|
1238
|
+
source_pointer: slice.logPath,
|
|
1239
|
+
payload: {
|
|
1240
|
+
elapsed_ms: killDecision.elapsedMs,
|
|
1241
|
+
idle_ms: killDecision.idleMs,
|
|
1242
|
+
output_path: slice.outputPath,
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
],
|
|
1246
|
+
});
|
|
1247
|
+
setLaneState(run, {
|
|
1248
|
+
workstreamId: slice.workstreamId,
|
|
1249
|
+
state: "blocked",
|
|
1250
|
+
activeRunId: null,
|
|
1251
|
+
activeTaskIds: [],
|
|
1252
|
+
blockedReason: slice.lastError,
|
|
1253
|
+
waitingOnWorkstreamIds: [],
|
|
1254
|
+
retryAt: null,
|
|
1255
|
+
});
|
|
1256
|
+
await stopAutoContinueRun({
|
|
1257
|
+
run,
|
|
1258
|
+
reason: "blocked",
|
|
1259
|
+
error: slice.lastError,
|
|
1260
|
+
decisionRequired: decisionResult.queued,
|
|
1261
|
+
decisionIds: decisionResult.decisionIds,
|
|
1262
|
+
});
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
if (run.stopRequested) {
|
|
1266
|
+
try {
|
|
1267
|
+
await stopProcess(pid);
|
|
1268
|
+
}
|
|
1269
|
+
catch {
|
|
1270
|
+
// best effort
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (!outputComplete)
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
// Slice finished.
|
|
1278
|
+
const raw = readSliceOutputFile(slice.outputPath);
|
|
1279
|
+
const parsed = raw ? parseSliceResult(raw) : null;
|
|
1280
|
+
const parsedStatus = parsed?.status ?? "error";
|
|
1281
|
+
const defaultDecisionBlocking = parsedStatus === "completed" ? false : true;
|
|
1282
|
+
const allDecisions = Array.isArray(parsed?.decisions_needed)
|
|
1283
|
+
? (parsed?.decisions_needed ?? [])
|
|
1284
|
+
.filter((item) => Boolean(item && typeof item.question === "string" && item.question.trim()))
|
|
1285
|
+
: [];
|
|
1286
|
+
const isParserSyntheticFallbackDecision = (item) => {
|
|
1287
|
+
const question = String(item?.question ?? "").trim().toLowerCase();
|
|
1288
|
+
const summary = String(item?.summary ?? "").trim().toLowerCase();
|
|
1289
|
+
return ((question.includes("missing required blocking decision") ||
|
|
1290
|
+
summary.includes("parser inserted a blocking decision")) &&
|
|
1291
|
+
item?.blocking === true);
|
|
1292
|
+
};
|
|
1293
|
+
const decisions = allDecisions.filter((item) => !isParserSyntheticFallbackDecision(item));
|
|
1294
|
+
const blockingDecisionCount = decisions.filter((item) => typeof item.blocking === "boolean" ? item.blocking : defaultDecisionBlocking).length;
|
|
1295
|
+
const nonBlockingDecisionCount = Math.max(0, decisions.length - blockingDecisionCount);
|
|
1296
|
+
const effectiveParsedStatus = parsedStatus === "completed" && blockingDecisionCount > 0
|
|
1297
|
+
? "needs_decision"
|
|
1298
|
+
: parsedStatus === "needs_decision" && blockingDecisionCount === 0
|
|
1299
|
+
? "completed"
|
|
1300
|
+
: parsedStatus;
|
|
1301
|
+
slice.status =
|
|
1302
|
+
effectiveParsedStatus === "completed"
|
|
1303
|
+
? "completed"
|
|
1304
|
+
: effectiveParsedStatus === "blocked" || effectiveParsedStatus === "needs_decision"
|
|
1305
|
+
? "blocked"
|
|
1306
|
+
: "error";
|
|
1307
|
+
slice.finishedAt = now;
|
|
1308
|
+
slice.updatedAt = now;
|
|
1309
|
+
slice.lastError =
|
|
1310
|
+
slice.status === "error"
|
|
1311
|
+
? slice.lastError ?? "Autopilot slice failed or returned invalid output."
|
|
1312
|
+
: null;
|
|
1313
|
+
autoContinueSliceRuns.set(slice.runId, slice);
|
|
1314
|
+
clearAutoContinueSliceTransientState(slice.runId);
|
|
1315
|
+
// Token accounting: codex CLI doesn't provide tokens here; use the modeled estimate.
|
|
1316
|
+
const modeledTokens = slice.tokenEstimate ?? 0;
|
|
1317
|
+
run.tokensUsed += Math.max(0, modeledTokens);
|
|
1318
|
+
run.activeTaskTokenEstimate = null;
|
|
1319
|
+
const artifacts = Array.isArray(parsed?.artifacts)
|
|
1320
|
+
? (parsed?.artifacts ?? [])
|
|
1321
|
+
.filter((item) => Boolean(item && typeof item.name === "string" && item.name.trim()))
|
|
1322
|
+
: [];
|
|
1323
|
+
const artifactEvidenceRefs = artifacts.map((artifact) => ({
|
|
1324
|
+
evidence_type: "artifact",
|
|
1325
|
+
title: artifact.name.trim(),
|
|
1326
|
+
summary: artifact.description?.trim() || "Slice artifact output",
|
|
1327
|
+
source_pointer: artifact.url ?? slice.outputPath,
|
|
1328
|
+
payload: {
|
|
1329
|
+
artifact_type: artifact.artifact_type ?? null,
|
|
1330
|
+
confidence_score: artifact.confidence_score ?? null,
|
|
1331
|
+
task_ids: Array.isArray(artifact.task_ids) && artifact.task_ids.length > 0
|
|
1332
|
+
? artifact.task_ids
|
|
1333
|
+
: slice.taskIds,
|
|
1334
|
+
milestone_id: artifact.milestone_id ?? slice.milestoneIds[0] ?? null,
|
|
1335
|
+
},
|
|
1336
|
+
}));
|
|
1337
|
+
const nextActions = Array.isArray(parsed?.next_actions)
|
|
1338
|
+
? parsed.next_actions
|
|
1339
|
+
.filter((item) => typeof item === "string")
|
|
1340
|
+
.map((item) => item.trim())
|
|
1341
|
+
.filter(Boolean)
|
|
1342
|
+
: [];
|
|
1343
|
+
const userSummary = (typeof parsed?.summary === "string" && parsed.summary.trim().length > 0
|
|
1344
|
+
? parsed.summary.trim()
|
|
1345
|
+
: null) ??
|
|
1346
|
+
nextActions[0] ??
|
|
1347
|
+
(slice.status === "completed"
|
|
1348
|
+
? `Slice completed for ${slice.workstreamTitle ?? slice.workstreamId}.`
|
|
1349
|
+
: `Slice blocked for ${slice.workstreamTitle ?? slice.workstreamId}.`);
|
|
1350
|
+
const nextStepHint = nextActions[0] ??
|
|
1351
|
+
(slice.status === "completed"
|
|
1352
|
+
? "No follow-up action returned by worker."
|
|
1353
|
+
: "Resolve blocker to continue execution.");
|
|
1354
|
+
const skillEvidence = Array.isArray(parsed?.skill_evidence)
|
|
1355
|
+
? parsed.skill_evidence
|
|
1356
|
+
.map((item) => ({
|
|
1357
|
+
skill: typeof item?.skill === "string"
|
|
1358
|
+
? item.skill.trim()
|
|
1359
|
+
: "",
|
|
1360
|
+
skill_file: typeof item?.skill_file === "string"
|
|
1361
|
+
? item.skill_file.trim()
|
|
1362
|
+
: null,
|
|
1363
|
+
skill_sha256: typeof item?.skill_sha256 === "string"
|
|
1364
|
+
? item.skill_sha256.trim().toLowerCase()
|
|
1365
|
+
: null,
|
|
1366
|
+
skill_heading: typeof item?.skill_heading === "string"
|
|
1367
|
+
? item.skill_heading.trim()
|
|
1368
|
+
: null,
|
|
1369
|
+
}))
|
|
1370
|
+
.filter((item) => item.skill.length > 0)
|
|
1371
|
+
: [];
|
|
1372
|
+
const reportedSkillNames = Array.from(new Set(skillEvidence
|
|
1373
|
+
.map((entry) => entry.skill.replace(/^\$/, "").trim())
|
|
1374
|
+
.filter(Boolean)));
|
|
1375
|
+
const reportedSkillSha256Count = skillEvidence.filter((entry) => typeof entry.skill_sha256 === "string" && entry.skill_sha256.length > 0).length;
|
|
1376
|
+
const taskUpdates = Array.isArray(parsed?.task_updates)
|
|
1377
|
+
? parsed.task_updates
|
|
1378
|
+
: [];
|
|
1379
|
+
const milestoneUpdates = Array.isArray(parsed?.milestone_updates)
|
|
1380
|
+
? parsed.milestone_updates
|
|
1381
|
+
: [];
|
|
1382
|
+
const resultEnvelope = {
|
|
1383
|
+
summary: userSummary,
|
|
1384
|
+
parsed_status: effectiveParsedStatus,
|
|
1385
|
+
task_updates: taskUpdates,
|
|
1386
|
+
milestone_updates: milestoneUpdates,
|
|
1387
|
+
next_actions: nextActions,
|
|
1388
|
+
artifacts: artifacts.map((artifact) => ({
|
|
1389
|
+
name: artifact.name,
|
|
1390
|
+
artifact_type: artifact.artifact_type ?? null,
|
|
1391
|
+
url: artifact.url ?? null,
|
|
1392
|
+
})),
|
|
1393
|
+
};
|
|
1394
|
+
const evidenceEnvelope = {
|
|
1395
|
+
artifacts: artifacts.map((artifact) => ({
|
|
1396
|
+
name: artifact.name,
|
|
1397
|
+
artifact_type: artifact.artifact_type ?? null,
|
|
1398
|
+
source_pointer: artifact.url ?? null,
|
|
1399
|
+
})),
|
|
1400
|
+
files: [slice.outputPath, slice.logPath].filter(Boolean),
|
|
1401
|
+
logs: [slice.logPath].filter(Boolean),
|
|
1402
|
+
};
|
|
1403
|
+
let blockingDecisionQueued = false;
|
|
1404
|
+
const blockingDecisionIds = [];
|
|
1405
|
+
const nonBlockingDecisionIds = [];
|
|
1406
|
+
for (const decision of decisions) {
|
|
1407
|
+
const isBlocking = typeof decision.blocking === "boolean" ? decision.blocking : defaultDecisionBlocking;
|
|
1408
|
+
const normalizedQuestion = decision.question.trim();
|
|
1409
|
+
const decisionResult = await requestDecisionQueued({
|
|
1410
|
+
initiativeId: run.initiativeId,
|
|
1411
|
+
correlationId: slice.runId,
|
|
1412
|
+
title: normalizedQuestion,
|
|
1413
|
+
summary: decision.summary ?? parsed?.summary ?? null,
|
|
1414
|
+
urgency: decision.urgency ?? "high",
|
|
1415
|
+
options: Array.isArray(decision.options)
|
|
1416
|
+
? decision.options.filter((opt) => typeof opt === "string" && opt.trim())
|
|
1417
|
+
: [],
|
|
1418
|
+
blocking: isBlocking,
|
|
1419
|
+
decisionType: isBlocking
|
|
1420
|
+
? "autopilot_blocking_decision"
|
|
1421
|
+
: "autopilot_followup_decision",
|
|
1422
|
+
workstreamId: slice.workstreamId,
|
|
1423
|
+
agentId: slice.agentId,
|
|
1424
|
+
sourceSystem: "orgx-autopilot",
|
|
1425
|
+
conflictSource: parsedStatus === "needs_decision"
|
|
1426
|
+
? "slice_needs_decision"
|
|
1427
|
+
: "slice_reported_decision",
|
|
1428
|
+
dedupeKey: [
|
|
1429
|
+
"autopilot",
|
|
1430
|
+
run.initiativeId,
|
|
1431
|
+
slice.workstreamId,
|
|
1432
|
+
"slice_reported_decision",
|
|
1433
|
+
normalizedQuestion.toLowerCase(),
|
|
1434
|
+
].join(":"),
|
|
1435
|
+
recommendedAction: nextActions[0] ??
|
|
1436
|
+
"Resolve this decision to continue the slice or safely defer workstream execution.",
|
|
1437
|
+
sourceRunId: slice.runId,
|
|
1438
|
+
sourceRef: {
|
|
1439
|
+
run_id: slice.runId,
|
|
1440
|
+
workstream_id: slice.workstreamId,
|
|
1441
|
+
parsed_status: parsedStatus,
|
|
1442
|
+
},
|
|
1443
|
+
evidenceRefs: [
|
|
1444
|
+
{
|
|
1445
|
+
evidence_type: "slice_output_summary",
|
|
1446
|
+
title: "Slice requested a decision",
|
|
1447
|
+
summary: decision.summary ?? parsed?.summary ?? "Decision required by slice output.",
|
|
1448
|
+
source_pointer: slice.outputPath,
|
|
1449
|
+
payload: {
|
|
1450
|
+
log_path: slice.logPath,
|
|
1451
|
+
blocking: isBlocking,
|
|
1452
|
+
},
|
|
1453
|
+
},
|
|
1454
|
+
...artifactEvidenceRefs,
|
|
1455
|
+
],
|
|
1456
|
+
});
|
|
1457
|
+
if (decisionResult.queued && isBlocking)
|
|
1458
|
+
blockingDecisionQueued = true;
|
|
1459
|
+
if (decisionResult.decisionIds.length > 0) {
|
|
1460
|
+
if (isBlocking)
|
|
1461
|
+
blockingDecisionIds.push(...decisionResult.decisionIds);
|
|
1462
|
+
else
|
|
1463
|
+
nonBlockingDecisionIds.push(...decisionResult.decisionIds);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
const decisionIds = Array.from(new Set([...blockingDecisionIds, ...nonBlockingDecisionIds]));
|
|
1467
|
+
for (const artifact of artifacts) {
|
|
1468
|
+
await registerArtifactSafe({
|
|
1469
|
+
initiativeId: run.initiativeId,
|
|
1470
|
+
runId: slice.runId,
|
|
1471
|
+
agentId: slice.agentId,
|
|
1472
|
+
agentName: slice.agentName,
|
|
1473
|
+
workstreamId: slice.workstreamId,
|
|
1474
|
+
fallbackMilestoneId: slice.milestoneIds[0] ?? null,
|
|
1475
|
+
fallbackTaskIds: slice.taskIds,
|
|
1476
|
+
artifact,
|
|
1477
|
+
isMockWorker: slice.isMockWorker,
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
// --- Proof ladder gate: check completion tasks for proof readiness ---
|
|
1481
|
+
// Phase 1: warn-only. Does not block status transitions but creates
|
|
1482
|
+
// a decision request when proof is missing for done/completed tasks.
|
|
1483
|
+
const doneTaskUpdates = taskUpdates.filter((tu) => tu.status === "done" || tu.status === "completed");
|
|
1484
|
+
if (doneTaskUpdates.length > 0 && !slice.isMockWorker) {
|
|
1485
|
+
const proofStrictness = process.env.ORGX_PROOF_STRICTNESS ?? "warn";
|
|
1486
|
+
for (const dtu of doneTaskUpdates) {
|
|
1487
|
+
try {
|
|
1488
|
+
const qp = new URLSearchParams({ task_id: dtu.task_id });
|
|
1489
|
+
const proofResult = await client.rawRequest("GET", `/api/flywheel/proof-status?${qp.toString()}`).catch(() => null);
|
|
1490
|
+
// If proof API unavailable, skip gracefully (phase 1)
|
|
1491
|
+
if (!proofResult)
|
|
1492
|
+
continue;
|
|
1493
|
+
const overallPassed = proofResult?.overall_passed === true;
|
|
1494
|
+
if (!overallPassed && proofStrictness === "block") {
|
|
1495
|
+
// Hard block: downgrade to needs_review
|
|
1496
|
+
dtu.status = "needs_review";
|
|
1497
|
+
const reasonCodes = Array.isArray(proofResult?.reason_codes)
|
|
1498
|
+
? proofResult.reason_codes.join(", ")
|
|
1499
|
+
: "incomplete_proof";
|
|
1500
|
+
await requestDecisionSafe({
|
|
1501
|
+
initiativeId: run.initiativeId,
|
|
1502
|
+
correlationId: slice.runId,
|
|
1503
|
+
title: `Task ${dtu.task_id} missing proof for completion`,
|
|
1504
|
+
summary: `Proof chain incomplete (${reasonCodes}). Task held in needs_review until proof is resolved.`,
|
|
1505
|
+
urgency: "high",
|
|
1506
|
+
blocking: true,
|
|
1507
|
+
decisionType: "proof_incomplete",
|
|
1508
|
+
workstreamId: slice.workstreamId,
|
|
1509
|
+
agentId: slice.agentId,
|
|
1510
|
+
sourceRunId: slice.runId,
|
|
1511
|
+
dedupeKey: `proof-gate:${dtu.task_id}:${slice.runId}`,
|
|
1512
|
+
metadata: { proof_result: proofResult },
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
else if (!overallPassed) {
|
|
1516
|
+
// Warn-only: emit activity but allow transition
|
|
1517
|
+
await emitActivitySafe({
|
|
1518
|
+
initiativeId: run.initiativeId,
|
|
1519
|
+
runId: slice.runId,
|
|
1520
|
+
correlationId: slice.runId,
|
|
1521
|
+
phase: "review",
|
|
1522
|
+
level: "warn",
|
|
1523
|
+
message: `Task ${dtu.task_id} completing with incomplete proof chain.`,
|
|
1524
|
+
metadata: {
|
|
1525
|
+
event: "proof_gate_warning",
|
|
1526
|
+
task_id: dtu.task_id,
|
|
1527
|
+
proof_result: proofResult,
|
|
1528
|
+
},
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
catch {
|
|
1533
|
+
// Best-effort proof check; don't block on transient failures
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
const statusUpdateResult = await applyAgentStatusUpdatesSafe({
|
|
1538
|
+
initiativeId: run.initiativeId,
|
|
1539
|
+
runId: slice.runId,
|
|
1540
|
+
correlationId: slice.runId,
|
|
1541
|
+
taskUpdates,
|
|
1542
|
+
milestoneUpdates,
|
|
1543
|
+
isMockWorker: slice.isMockWorker,
|
|
1544
|
+
});
|
|
1545
|
+
if (statusUpdateResult.taskUpdates.length > 0 ||
|
|
1546
|
+
statusUpdateResult.milestoneUpdates.length > 0) {
|
|
1547
|
+
recordLocalStatusOverrides({
|
|
1548
|
+
initiativeId: run.initiativeId,
|
|
1549
|
+
updatedAt: now,
|
|
1550
|
+
taskUpdates: statusUpdateResult.taskUpdates,
|
|
1551
|
+
milestoneUpdates: statusUpdateResult.milestoneUpdates,
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
try {
|
|
1555
|
+
writeRuntimeEvent({
|
|
1556
|
+
sourceClient: slice.sourceClient,
|
|
1557
|
+
event: slice.status === "error" ? "error" : "session_stop",
|
|
1558
|
+
runId: slice.runId,
|
|
1559
|
+
initiativeId: slice.initiativeId,
|
|
1560
|
+
workstreamId: slice.workstreamId,
|
|
1561
|
+
taskId: slice.taskIds[0] ?? null,
|
|
1562
|
+
agentId: slice.agentId,
|
|
1563
|
+
agentName: slice.agentName ?? null,
|
|
1564
|
+
phase: slice.status === "completed" ? "completed" : "blocked",
|
|
1565
|
+
message: userSummary ?? slice.lastError ?? "Autopilot slice finished.",
|
|
1566
|
+
metadata: {
|
|
1567
|
+
event: "autopilot_slice_finished",
|
|
1568
|
+
initiative_id: run.initiativeId,
|
|
1569
|
+
run_id: slice.runId,
|
|
1570
|
+
slice_run_id: slice.runId,
|
|
1571
|
+
workstream_id: slice.workstreamId,
|
|
1572
|
+
correlation_id: slice.runId,
|
|
1573
|
+
requested_by_agent_id: run.agentId,
|
|
1574
|
+
requested_by_agent_name: run.agentName,
|
|
1575
|
+
status: effectiveParsedStatus,
|
|
1576
|
+
artifacts: artifacts.length,
|
|
1577
|
+
decisions: decisions.length,
|
|
1578
|
+
blocking_decisions: blockingDecisionCount,
|
|
1579
|
+
non_blocking_decisions: nonBlockingDecisionCount,
|
|
1580
|
+
status_updates: statusUpdateResult.applied,
|
|
1581
|
+
status_updates_buffered: statusUpdateResult.buffered,
|
|
1582
|
+
reported_skill_evidence_count: skillEvidence.length,
|
|
1583
|
+
reported_skill_sha256_count: reportedSkillSha256Count,
|
|
1584
|
+
reported_skill_names: reportedSkillNames,
|
|
1585
|
+
action_type: normalizeActivityActionType("run_completed"),
|
|
1586
|
+
action_phase: normalizeActivityActionPhase(slice.status === "completed" ? "completed" : "blocked"),
|
|
1587
|
+
result: resultEnvelope,
|
|
1588
|
+
evidence: evidenceEnvelope,
|
|
1589
|
+
...mockMeta(slice),
|
|
1590
|
+
user_summary: userSummary,
|
|
1591
|
+
next_actions: nextActions,
|
|
1592
|
+
},
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
catch {
|
|
1596
|
+
// best effort
|
|
1597
|
+
}
|
|
1598
|
+
if (slice.status === "completed") {
|
|
1599
|
+
await emitActivitySafe({
|
|
1600
|
+
initiativeId: run.initiativeId,
|
|
1601
|
+
runId: slice.runId,
|
|
1602
|
+
correlationId: slice.runId,
|
|
1603
|
+
phase: "handoff",
|
|
1604
|
+
level: "info",
|
|
1605
|
+
message: `Handoff ready for ${slice.workstreamTitle ?? slice.workstreamId}.`,
|
|
1606
|
+
progressPct: 80,
|
|
1607
|
+
nextStep: nextStepHint,
|
|
1608
|
+
metadata: buildSliceEnrichment({
|
|
1609
|
+
run,
|
|
1610
|
+
slice,
|
|
1611
|
+
workstreamId: slice.workstreamId,
|
|
1612
|
+
workstreamTitle: slice.workstreamTitle ?? null,
|
|
1613
|
+
domain: slice.domain,
|
|
1614
|
+
requiredSkills: slice.requiredSkills,
|
|
1615
|
+
nextActions,
|
|
1616
|
+
userSummary,
|
|
1617
|
+
event: "autopilot_slice_handoff",
|
|
1618
|
+
extra: {
|
|
1619
|
+
parsed_status: effectiveParsedStatus,
|
|
1620
|
+
artifacts: artifacts.length,
|
|
1621
|
+
decisions: decisions.length,
|
|
1622
|
+
decision_ids: decisionIds,
|
|
1623
|
+
output_path: slice.outputPath,
|
|
1624
|
+
log_path: slice.logPath,
|
|
1625
|
+
task_updates: taskUpdates,
|
|
1626
|
+
milestone_updates: milestoneUpdates,
|
|
1627
|
+
result: resultEnvelope,
|
|
1628
|
+
evidence: evidenceEnvelope,
|
|
1629
|
+
...mockMeta(slice),
|
|
1630
|
+
},
|
|
1631
|
+
}),
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
await emitActivitySafe({
|
|
1635
|
+
initiativeId: run.initiativeId,
|
|
1636
|
+
runId: slice.runId,
|
|
1637
|
+
correlationId: slice.runId,
|
|
1638
|
+
phase: slice.status === "completed" ? "completed" : "blocked",
|
|
1639
|
+
level: slice.status === "completed" ? "info" : "warn",
|
|
1640
|
+
progressPct: slice.status === "completed" ? 100 : 65,
|
|
1641
|
+
nextStep: nextStepHint,
|
|
1642
|
+
message: slice.status === "completed"
|
|
1643
|
+
? `Autopilot slice completed for ${slice.workstreamTitle ?? slice.workstreamId} (${slice.taskIds.length} task${slice.taskIds.length === 1 ? "" : "s"}).`
|
|
1644
|
+
: `Autopilot slice blocked: ${slice.workstreamTitle ?? slice.workstreamId}.`,
|
|
1645
|
+
metadata: {
|
|
1646
|
+
...buildSliceEnrichment({
|
|
1647
|
+
run,
|
|
1648
|
+
slice,
|
|
1649
|
+
workstreamId: slice.workstreamId,
|
|
1650
|
+
workstreamTitle: slice.workstreamTitle ?? null,
|
|
1651
|
+
domain: slice.domain,
|
|
1652
|
+
requiredSkills: slice.requiredSkills,
|
|
1653
|
+
nextActions,
|
|
1654
|
+
userSummary,
|
|
1655
|
+
event: "autopilot_slice_result",
|
|
1656
|
+
}),
|
|
1657
|
+
error_location: slice.status === "completed"
|
|
1658
|
+
? null
|
|
1659
|
+
: "mission-control.auto-continue.engine.slice.result",
|
|
1660
|
+
behavior_config_id: slice.behaviorConfigId,
|
|
1661
|
+
behavior_config_version: slice.behaviorConfigVersion,
|
|
1662
|
+
behavior_config_hash: slice.behaviorConfigHash,
|
|
1663
|
+
policy_source: slice.behaviorPolicySource,
|
|
1664
|
+
behavior_automation_level: slice.behaviorAutomationLevel,
|
|
1665
|
+
parsed_status: effectiveParsedStatus,
|
|
1666
|
+
has_output: Boolean(parsed),
|
|
1667
|
+
artifacts: artifacts.length,
|
|
1668
|
+
decisions: decisions.length,
|
|
1669
|
+
blocking_decisions: blockingDecisionCount,
|
|
1670
|
+
non_blocking_decisions: nonBlockingDecisionCount,
|
|
1671
|
+
decision_ids: decisionIds,
|
|
1672
|
+
blocking_decision_ids: Array.from(new Set(blockingDecisionIds)),
|
|
1673
|
+
non_blocking_decision_ids: Array.from(new Set(nonBlockingDecisionIds)),
|
|
1674
|
+
decision_required: blockingDecisionQueued,
|
|
1675
|
+
status_updates_applied: statusUpdateResult.applied,
|
|
1676
|
+
status_updates_buffered: statusUpdateResult.buffered,
|
|
1677
|
+
reported_skill_evidence_count: skillEvidence.length,
|
|
1678
|
+
reported_skill_sha256_count: reportedSkillSha256Count,
|
|
1679
|
+
reported_skill_names: reportedSkillNames,
|
|
1680
|
+
output_path: slice.outputPath,
|
|
1681
|
+
log_path: slice.logPath,
|
|
1682
|
+
error: slice.lastError,
|
|
1683
|
+
next_actions: nextActions,
|
|
1684
|
+
task_updates: taskUpdates,
|
|
1685
|
+
milestone_updates: milestoneUpdates,
|
|
1686
|
+
result: resultEnvelope,
|
|
1687
|
+
evidence: evidenceEnvelope,
|
|
1688
|
+
...mockMeta(slice),
|
|
1689
|
+
user_summary: userSummary,
|
|
1690
|
+
},
|
|
1691
|
+
});
|
|
1692
|
+
// Append to local team context for cross-agent awareness on subsequent slices.
|
|
1693
|
+
if (slice.status === "completed") {
|
|
1694
|
+
try {
|
|
1695
|
+
appendTeamCompletion(run.initiativeId, {
|
|
1696
|
+
domain: slice.domain ?? "unknown",
|
|
1697
|
+
task_title: slice.workstreamTitle ?? slice.workstreamId,
|
|
1698
|
+
summary: parsed?.summary ?? "Completed.",
|
|
1699
|
+
key_outputs: artifacts.map((a) => a.name).filter(Boolean).slice(0, 5),
|
|
1700
|
+
completed_at: new Date().toISOString(),
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
catch {
|
|
1704
|
+
// best effort: do not block the engine on store failure
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
if (slice.status !== "completed") {
|
|
1708
|
+
let fallbackDecisionResult = {
|
|
1709
|
+
queued: false,
|
|
1710
|
+
decisionIds: [],
|
|
1711
|
+
};
|
|
1712
|
+
if (!blockingDecisionQueued) {
|
|
1713
|
+
const blockedLike = slice.status === "blocked";
|
|
1714
|
+
fallbackDecisionResult = await requestDecisionQueued({
|
|
1715
|
+
initiativeId: run.initiativeId,
|
|
1716
|
+
correlationId: slice.runId,
|
|
1717
|
+
title: blockedLike
|
|
1718
|
+
? `Autopilot slice blocked: ${slice.workstreamTitle ?? slice.workstreamId}`
|
|
1719
|
+
: `Autopilot slice failed: ${slice.workstreamTitle ?? slice.workstreamId}`,
|
|
1720
|
+
summary: parsed?.summary ??
|
|
1721
|
+
slice.lastError ??
|
|
1722
|
+
(blockedLike
|
|
1723
|
+
? "The slice reported a blocked/decision-required state without a blocking decision payload. Review logs/output and decide whether to retry, unblock, or skip."
|
|
1724
|
+
: "The slice failed without producing a valid output contract. Review logs/output and decide whether to retry or pause autopilot."),
|
|
1725
|
+
urgency: "high",
|
|
1726
|
+
options: [
|
|
1727
|
+
"Retry this workstream slice",
|
|
1728
|
+
"Pause autopilot and investigate",
|
|
1729
|
+
"Skip this workstream for now",
|
|
1730
|
+
],
|
|
1731
|
+
blocking: true,
|
|
1732
|
+
decisionType: blockedLike ? "autopilot_blocked_without_decision" : "autopilot_failure",
|
|
1733
|
+
workstreamId: slice.workstreamId,
|
|
1734
|
+
agentId: slice.agentId,
|
|
1735
|
+
sourceSystem: "orgx-autopilot",
|
|
1736
|
+
conflictSource: blockedLike
|
|
1737
|
+
? "slice_missing_blocking_decision"
|
|
1738
|
+
: "slice_invalid_output",
|
|
1739
|
+
dedupeKey: [
|
|
1740
|
+
"autopilot",
|
|
1741
|
+
run.initiativeId,
|
|
1742
|
+
slice.workstreamId,
|
|
1743
|
+
blockedLike ? "slice_missing_blocking_decision" : "slice_invalid_output",
|
|
1744
|
+
].join(":"),
|
|
1745
|
+
recommendedAction: nextActions[0] ??
|
|
1746
|
+
"Review the output contract and logs, then retry or pause autopilot until the blocker is resolved.",
|
|
1747
|
+
sourceRunId: slice.runId,
|
|
1748
|
+
sourceRef: {
|
|
1749
|
+
run_id: slice.runId,
|
|
1750
|
+
workstream_id: slice.workstreamId,
|
|
1751
|
+
parsed_status: effectiveParsedStatus,
|
|
1752
|
+
},
|
|
1753
|
+
evidenceRefs: [
|
|
1754
|
+
{
|
|
1755
|
+
evidence_type: "slice_output_validation",
|
|
1756
|
+
title: "Slice output requires fallback decision",
|
|
1757
|
+
summary: parsed?.summary ??
|
|
1758
|
+
slice.lastError ??
|
|
1759
|
+
"Slice did not provide a blocking decision payload.",
|
|
1760
|
+
source_pointer: slice.outputPath,
|
|
1761
|
+
payload: {
|
|
1762
|
+
log_path: slice.logPath,
|
|
1763
|
+
parsed_status: effectiveParsedStatus,
|
|
1764
|
+
},
|
|
1765
|
+
},
|
|
1766
|
+
...artifactEvidenceRefs,
|
|
1767
|
+
],
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
setLaneState(run, {
|
|
1771
|
+
workstreamId: slice.workstreamId,
|
|
1772
|
+
state: "blocked",
|
|
1773
|
+
activeRunId: null,
|
|
1774
|
+
activeTaskIds: [],
|
|
1775
|
+
blockedReason: parsed?.summary ??
|
|
1776
|
+
slice.lastError ??
|
|
1777
|
+
`Slice returned status: ${effectiveParsedStatus}`,
|
|
1778
|
+
waitingOnWorkstreamIds: [],
|
|
1779
|
+
retryAt: null,
|
|
1780
|
+
});
|
|
1781
|
+
if (!run.blockedWorkstreamIds.includes(slice.workstreamId)) {
|
|
1782
|
+
run.blockedWorkstreamIds.push(slice.workstreamId);
|
|
1783
|
+
}
|
|
1784
|
+
await stopAutoContinueRun({
|
|
1785
|
+
run,
|
|
1786
|
+
reason: slice.status === "error" ? "error" : "blocked",
|
|
1787
|
+
error: parsed?.summary ??
|
|
1788
|
+
slice.lastError ??
|
|
1789
|
+
`Slice returned status: ${effectiveParsedStatus}`,
|
|
1790
|
+
decisionRequired: blockingDecisionQueued || fallbackDecisionResult.queued,
|
|
1791
|
+
decisionIds: Array.from(new Set([...decisionIds, ...fallbackDecisionResult.decisionIds])),
|
|
1792
|
+
});
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
const completionHadNoOutcome = parsedStatus === "completed" &&
|
|
1796
|
+
artifacts.length === 0 &&
|
|
1797
|
+
decisions.length === 0 &&
|
|
1798
|
+
statusUpdateResult.applied === 0;
|
|
1799
|
+
if (!parsed || parsedStatus === "error" || completionHadNoOutcome) {
|
|
1800
|
+
const attentionTitle = completionHadNoOutcome
|
|
1801
|
+
? `Autopilot slice needs verification: ${slice.workstreamTitle ?? slice.workstreamId}`
|
|
1802
|
+
: `Autopilot slice failed: ${slice.workstreamTitle ?? slice.workstreamId}`;
|
|
1803
|
+
const attentionSummary = completionHadNoOutcome
|
|
1804
|
+
? "The slice reported completion but did not produce artifacts or status updates. Decide whether to retry, request stronger output, or mark tasks manually."
|
|
1805
|
+
: "The slice exited without a valid output contract. Review logs/output and decide whether to retry or pause autopilot.";
|
|
1806
|
+
const decisionResult = await requestDecisionQueued({
|
|
1807
|
+
initiativeId: run.initiativeId,
|
|
1808
|
+
correlationId: slice.runId,
|
|
1809
|
+
title: attentionTitle,
|
|
1810
|
+
summary: attentionSummary,
|
|
1811
|
+
urgency: "high",
|
|
1812
|
+
options: [
|
|
1813
|
+
"Retry this workstream slice",
|
|
1814
|
+
"Pause autopilot and investigate",
|
|
1815
|
+
"Skip this workstream for now",
|
|
1816
|
+
],
|
|
1817
|
+
blocking: true,
|
|
1818
|
+
decisionType: completionHadNoOutcome
|
|
1819
|
+
? "autopilot_completed_without_outcome"
|
|
1820
|
+
: "autopilot_failure",
|
|
1821
|
+
workstreamId: slice.workstreamId,
|
|
1822
|
+
agentId: slice.agentId,
|
|
1823
|
+
sourceSystem: "orgx-autopilot",
|
|
1824
|
+
conflictSource: completionHadNoOutcome
|
|
1825
|
+
? "slice_completed_without_outcome"
|
|
1826
|
+
: "slice_invalid_output",
|
|
1827
|
+
dedupeKey: [
|
|
1828
|
+
"autopilot",
|
|
1829
|
+
run.initiativeId,
|
|
1830
|
+
slice.workstreamId,
|
|
1831
|
+
completionHadNoOutcome
|
|
1832
|
+
? "slice_completed_without_outcome"
|
|
1833
|
+
: "slice_invalid_output",
|
|
1834
|
+
].join(":"),
|
|
1835
|
+
recommendedAction: nextActions[0] ??
|
|
1836
|
+
"Verify slice outputs and status updates, then retry once or pause for investigation.",
|
|
1837
|
+
sourceRunId: slice.runId,
|
|
1838
|
+
sourceRef: {
|
|
1839
|
+
run_id: slice.runId,
|
|
1840
|
+
workstream_id: slice.workstreamId,
|
|
1841
|
+
parsed_status: parsedStatus,
|
|
1842
|
+
},
|
|
1843
|
+
evidenceRefs: [
|
|
1844
|
+
{
|
|
1845
|
+
evidence_type: "slice_output_validation",
|
|
1846
|
+
title: "Slice output needs verification",
|
|
1847
|
+
summary: attentionSummary,
|
|
1848
|
+
source_pointer: slice.outputPath,
|
|
1849
|
+
payload: {
|
|
1850
|
+
log_path: slice.logPath,
|
|
1851
|
+
parsed_status: parsedStatus,
|
|
1852
|
+
completion_had_no_outcome: completionHadNoOutcome,
|
|
1853
|
+
},
|
|
1854
|
+
},
|
|
1855
|
+
...artifactEvidenceRefs,
|
|
1856
|
+
],
|
|
1857
|
+
});
|
|
1858
|
+
setLaneState(run, {
|
|
1859
|
+
workstreamId: slice.workstreamId,
|
|
1860
|
+
state: "blocked",
|
|
1861
|
+
activeRunId: null,
|
|
1862
|
+
activeTaskIds: [],
|
|
1863
|
+
blockedReason: slice.lastError ??
|
|
1864
|
+
(completionHadNoOutcome
|
|
1865
|
+
? "Slice completed without verifiable outcomes."
|
|
1866
|
+
: "Slice failed or returned invalid output."),
|
|
1867
|
+
waitingOnWorkstreamIds: [],
|
|
1868
|
+
retryAt: null,
|
|
1869
|
+
});
|
|
1870
|
+
if (!run.blockedWorkstreamIds.includes(slice.workstreamId)) {
|
|
1871
|
+
run.blockedWorkstreamIds.push(slice.workstreamId);
|
|
1872
|
+
}
|
|
1873
|
+
await stopAutoContinueRun({
|
|
1874
|
+
run,
|
|
1875
|
+
reason: completionHadNoOutcome ? "blocked" : "error",
|
|
1876
|
+
error: slice.lastError ??
|
|
1877
|
+
(completionHadNoOutcome
|
|
1878
|
+
? "Slice completed without verifiable outcomes."
|
|
1879
|
+
: "Slice failed or returned invalid output."),
|
|
1880
|
+
decisionRequired: completionHadNoOutcome && decisionResult.queued,
|
|
1881
|
+
decisionIds: decisionResult.decisionIds,
|
|
1882
|
+
});
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
run.lastRunId = slice.runId;
|
|
1886
|
+
run.lastTaskId = slice.taskIds[0] ?? run.lastTaskId;
|
|
1887
|
+
removeActiveSliceFromRun(run, {
|
|
1888
|
+
sliceRunId: slice.runId,
|
|
1889
|
+
taskIds: slice.taskIds,
|
|
1890
|
+
workstreamId: slice.workstreamId,
|
|
1891
|
+
});
|
|
1892
|
+
setLaneState(run, {
|
|
1893
|
+
workstreamId: slice.workstreamId,
|
|
1894
|
+
state: "completed",
|
|
1895
|
+
activeRunId: null,
|
|
1896
|
+
activeTaskIds: [],
|
|
1897
|
+
blockedReason: null,
|
|
1898
|
+
waitingOnWorkstreamIds: [],
|
|
1899
|
+
retryAt: null,
|
|
1900
|
+
});
|
|
1901
|
+
run.blockedWorkstreamIds = run.blockedWorkstreamIds.filter((id) => id !== slice.workstreamId);
|
|
1902
|
+
syncLegacyRunPointers(run);
|
|
1903
|
+
// Do not keep prior rate-limit/runtime errors after a completed slice.
|
|
1904
|
+
run.lastError = null;
|
|
1905
|
+
run.updatedAt = now;
|
|
1906
|
+
try {
|
|
1907
|
+
await updateInitiativeAutoContinueState({
|
|
1908
|
+
initiativeId: run.initiativeId,
|
|
1909
|
+
run,
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
catch {
|
|
1913
|
+
// best effort
|
|
1914
|
+
}
|
|
1915
|
+
// Evaluate scope-level completion for milestone/workstream scopes.
|
|
1916
|
+
if (slice.scope && slice.scope !== "task") {
|
|
1917
|
+
try {
|
|
1918
|
+
const scopeGraph = applyLocalInitiativeOverrideToGraph(await buildMissionControlGraph(client, run.initiativeId));
|
|
1919
|
+
const scopeNodeById = new Map(scopeGraph.nodes.map((n) => [n.id, n]));
|
|
1920
|
+
const scopeResult = evaluateScopeCompletion({
|
|
1921
|
+
scope: slice.scope,
|
|
1922
|
+
milestoneIds: slice.scopeMilestoneIds ?? [],
|
|
1923
|
+
workstreamId: slice.workstreamId,
|
|
1924
|
+
nodeById: scopeNodeById,
|
|
1925
|
+
});
|
|
1926
|
+
if (scopeResult.scopeComplete) {
|
|
1927
|
+
await emitActivitySafe({
|
|
1928
|
+
initiativeId: run.initiativeId,
|
|
1929
|
+
runId: slice.runId,
|
|
1930
|
+
correlationId: slice.runId,
|
|
1931
|
+
phase: "completed",
|
|
1932
|
+
level: "info",
|
|
1933
|
+
progressPct: 100,
|
|
1934
|
+
nextStep: slice.scope === "milestone"
|
|
1935
|
+
? "Queue the next milestone-ready slice."
|
|
1936
|
+
: "Select the next dispatchable workstream from Next Up.",
|
|
1937
|
+
message: `${slice.scope === "milestone" ? "Milestone" : "Workstream"} scope completed for ${slice.workstreamTitle ?? slice.workstreamId}.`,
|
|
1938
|
+
metadata: {
|
|
1939
|
+
...buildSliceEnrichment({
|
|
1940
|
+
run,
|
|
1941
|
+
slice,
|
|
1942
|
+
workstreamId: slice.workstreamId,
|
|
1943
|
+
workstreamTitle: slice.workstreamTitle ?? null,
|
|
1944
|
+
domain: slice.domain,
|
|
1945
|
+
requiredSkills: slice.requiredSkills,
|
|
1946
|
+
event: "scope_completed",
|
|
1947
|
+
}),
|
|
1948
|
+
scope: slice.scope,
|
|
1949
|
+
milestone_ids: slice.scopeMilestoneIds,
|
|
1950
|
+
remaining_tasks: 0,
|
|
1951
|
+
},
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
catch {
|
|
1956
|
+
// best-effort scope completion check
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
if (run.stopAfterSlice) {
|
|
1960
|
+
run.stopAfterSlice = false;
|
|
1961
|
+
await stopAutoContinueRun({ run, reason: "completed" });
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
if (run.stopRequested) {
|
|
1965
|
+
await stopAutoContinueRun({ run, reason: "stopped" });
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
syncLegacyRunPointers(run);
|
|
1971
|
+
if (run.stopRequested) {
|
|
1972
|
+
run.status = "stopping";
|
|
1973
|
+
run.updatedAt = now;
|
|
1974
|
+
await stopAutoContinueRun({ run, reason: "stopped" });
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
const tokenBudgetValue = typeof run.tokenBudget === "number" && Number.isFinite(run.tokenBudget)
|
|
1978
|
+
? run.tokenBudget
|
|
1979
|
+
: null;
|
|
1980
|
+
// 2) Enforce token guardrail before starting a new slice.
|
|
1981
|
+
if (tokenBudgetValue !== null && run.tokensUsed >= tokenBudgetValue) {
|
|
1982
|
+
await stopAutoContinueRun({ run, reason: "budget_exhausted" });
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
const activeSliceCount = listActiveSliceRunIds(run).length;
|
|
1986
|
+
if (activeSliceCount >= run.maxParallelSlices) {
|
|
1987
|
+
run.updatedAt = now;
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
// 3) Pick next workstream slice and dispatch.
|
|
1991
|
+
let graph;
|
|
1992
|
+
try {
|
|
1993
|
+
graph = applyLocalInitiativeOverrideToGraph(await buildMissionControlGraph(client, run.initiativeId));
|
|
1994
|
+
}
|
|
1995
|
+
catch (err) {
|
|
1996
|
+
await stopAutoContinueRun({
|
|
1997
|
+
run,
|
|
1998
|
+
reason: "error",
|
|
1999
|
+
error: safeErrorMessage(err),
|
|
2000
|
+
});
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
const nodes = graph.nodes;
|
|
2004
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
2005
|
+
applyLocalStatusOverridesToGraph(run.initiativeId, nodeById);
|
|
2006
|
+
const taskNodes = nodes.filter((node) => node.type === "task");
|
|
2007
|
+
const todoTasks = taskNodes.filter((node) => isTodoStatus(node.status));
|
|
2008
|
+
if (todoTasks.length === 0) {
|
|
2009
|
+
await stopAutoContinueRun({ run, reason: "completed" });
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
const taskIsReady = (task) => task.dependencyIds.every((depId) => {
|
|
2013
|
+
const dependency = nodeById.get(depId);
|
|
2014
|
+
return dependency ? isDoneStatus(dependency.status) : true;
|
|
2015
|
+
});
|
|
2016
|
+
const taskHasBlockedParent = (task) => {
|
|
2017
|
+
const milestone = task.milestoneId ? nodeById.get(task.milestoneId) ?? null : null;
|
|
2018
|
+
const workstream = task.workstreamId ? nodeById.get(task.workstreamId) ?? null : null;
|
|
2019
|
+
return (milestone?.status?.toLowerCase() === "blocked" ||
|
|
2020
|
+
workstream?.status?.toLowerCase() === "blocked");
|
|
2021
|
+
};
|
|
2022
|
+
// Select the next eligible workstream by scanning ordered todos.
|
|
2023
|
+
let selectedWorkstreamId = null;
|
|
2024
|
+
let deferredBySpawnGuardRateLimit = 0;
|
|
2025
|
+
for (const taskId of graph.recentTodos) {
|
|
2026
|
+
const node = nodeById.get(taskId);
|
|
2027
|
+
if (!node || node.type !== "task")
|
|
2028
|
+
continue;
|
|
2029
|
+
if (!isTodoStatus(node.status))
|
|
2030
|
+
continue;
|
|
2031
|
+
if (!run.includeVerification &&
|
|
2032
|
+
typeof node.title === "string" &&
|
|
2033
|
+
/^verification[ \t]+scenario/i.test(node.title)) {
|
|
2034
|
+
continue;
|
|
2035
|
+
}
|
|
2036
|
+
if (run.allowedWorkstreamIds && node.workstreamId) {
|
|
2037
|
+
if (!run.allowedWorkstreamIds.includes(node.workstreamId))
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
if (!node.workstreamId)
|
|
2041
|
+
continue;
|
|
2042
|
+
if (run.blockedWorkstreamIds.includes(node.workstreamId))
|
|
2043
|
+
continue;
|
|
2044
|
+
const lane = run.laneByWorkstreamId[node.workstreamId] ?? null;
|
|
2045
|
+
if (lane?.state === "running" && lane.activeRunId)
|
|
2046
|
+
continue;
|
|
2047
|
+
if (lane?.state === "rate_limited" && lane.retryAt) {
|
|
2048
|
+
const retryAtMs = Date.parse(lane.retryAt);
|
|
2049
|
+
if (Number.isFinite(retryAtMs) && retryAtMs > Date.now()) {
|
|
2050
|
+
deferredBySpawnGuardRateLimit += 1;
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
const ws = nodeById.get(node.workstreamId);
|
|
2055
|
+
if (ws && !isDispatchableWorkstreamStatus(ws.status))
|
|
2056
|
+
continue;
|
|
2057
|
+
if (!taskIsReady(node))
|
|
2058
|
+
continue;
|
|
2059
|
+
if (taskHasBlockedParent(node))
|
|
2060
|
+
continue;
|
|
2061
|
+
const retryAtMs = getSpawnGuardRetryAtMs(run.initiativeId, node.id);
|
|
2062
|
+
if (retryAtMs > 0) {
|
|
2063
|
+
deferredBySpawnGuardRateLimit += 1;
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
2066
|
+
selectedWorkstreamId = node.workstreamId;
|
|
2067
|
+
break;
|
|
2068
|
+
}
|
|
2069
|
+
if (!selectedWorkstreamId) {
|
|
2070
|
+
const waitingByWorkstream = new Map();
|
|
2071
|
+
for (const task of taskNodes) {
|
|
2072
|
+
if (!isTodoStatus(task.status))
|
|
2073
|
+
continue;
|
|
2074
|
+
if (!run.includeVerification &&
|
|
2075
|
+
typeof task.title === "string" &&
|
|
2076
|
+
/^verification[ \t]+scenario/i.test(task.title)) {
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
const workstreamId = (task.workstreamId ?? "").trim();
|
|
2080
|
+
if (!workstreamId)
|
|
2081
|
+
continue;
|
|
2082
|
+
if (Array.isArray(run.allowedWorkstreamIds) &&
|
|
2083
|
+
run.allowedWorkstreamIds.length > 0 &&
|
|
2084
|
+
!run.allowedWorkstreamIds.includes(workstreamId)) {
|
|
2085
|
+
continue;
|
|
2086
|
+
}
|
|
2087
|
+
if (run.blockedWorkstreamIds.includes(workstreamId)) {
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
const blockedParents = taskHasBlockedParent(task);
|
|
2091
|
+
const unresolvedDepWorkstreamIds = task.dependencyIds
|
|
2092
|
+
.map((depId) => nodeById.get(depId))
|
|
2093
|
+
.filter((dep) => Boolean(dep && !isDoneStatus(dep.status)))
|
|
2094
|
+
.map((dep) => (dep.workstreamId ?? "").trim())
|
|
2095
|
+
.filter(Boolean);
|
|
2096
|
+
if (blockedParents || unresolvedDepWorkstreamIds.length > 0) {
|
|
2097
|
+
const existing = waitingByWorkstream.get(workstreamId) ?? [];
|
|
2098
|
+
waitingByWorkstream.set(workstreamId, dedupeStrings([...existing, ...unresolvedDepWorkstreamIds]));
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
for (const [workstreamId, waitingOnWorkstreamIds] of waitingByWorkstream.entries()) {
|
|
2102
|
+
setLaneState(run, {
|
|
2103
|
+
workstreamId,
|
|
2104
|
+
state: "waiting_dependency",
|
|
2105
|
+
activeRunId: null,
|
|
2106
|
+
activeTaskIds: [],
|
|
2107
|
+
blockedReason: null,
|
|
2108
|
+
waitingOnWorkstreamIds,
|
|
2109
|
+
retryAt: null,
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
if (listActiveSliceRunIds(run).length > 0) {
|
|
2113
|
+
run.updatedAt = now;
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
if (deferredBySpawnGuardRateLimit > 0) {
|
|
2117
|
+
run.updatedAt = now;
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
if (run.allowedWorkstreamIds && run.allowedWorkstreamIds.length > 0) {
|
|
2121
|
+
const scopedTodoCount = taskNodes.filter((node) => {
|
|
2122
|
+
if (!isTodoStatus(node.status))
|
|
2123
|
+
return false;
|
|
2124
|
+
if (!run.includeVerification &&
|
|
2125
|
+
typeof node.title === "string" &&
|
|
2126
|
+
/^verification[ \t]+scenario/i.test(node.title)) {
|
|
2127
|
+
return false;
|
|
2128
|
+
}
|
|
2129
|
+
if (!node.workstreamId)
|
|
2130
|
+
return false;
|
|
2131
|
+
return run.allowedWorkstreamIds?.includes(node.workstreamId) ?? false;
|
|
2132
|
+
}).length;
|
|
2133
|
+
if (scopedTodoCount === 0) {
|
|
2134
|
+
await stopAutoContinueRun({ run, reason: "completed" });
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
await stopAutoContinueRun({ run, reason: "blocked" });
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
const workstreamNode = nodeById.get(selectedWorkstreamId) ?? null;
|
|
2142
|
+
const workstreamTitle = workstreamNode?.title ?? null;
|
|
2143
|
+
const initiativeNode = nodes.find((node) => node.type === "initiative") ?? null;
|
|
2144
|
+
const initiativeTitle = initiativeNode?.title ?? `Initiative ${run.initiativeId.slice(0, 8)}`;
|
|
2145
|
+
const scopeSelection = selectSliceTasksByScope({
|
|
2146
|
+
scope: run.scope,
|
|
2147
|
+
workstreamId: selectedWorkstreamId,
|
|
2148
|
+
recentTodos: graph.recentTodos,
|
|
2149
|
+
nodeById,
|
|
2150
|
+
includeVerification: run.includeVerification,
|
|
2151
|
+
});
|
|
2152
|
+
const sliceTaskNodes = scopeSelection.tasks;
|
|
2153
|
+
const scopeMilestoneIds = scopeSelection.milestoneIds;
|
|
2154
|
+
const primaryTask = sliceTaskNodes[0] ?? null;
|
|
2155
|
+
if (!primaryTask) {
|
|
2156
|
+
if (listActiveSliceRunIds(run).length > 0) {
|
|
2157
|
+
run.updatedAt = now;
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
await stopAutoContinueRun({ run, reason: "blocked" });
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
let cappedSliceTaskNodes = sliceTaskNodes;
|
|
2164
|
+
let expectedDurationHours = cappedSliceTaskNodes.reduce((acc, t) => acc +
|
|
2165
|
+
(typeof t.expectedDurationHours === "number" && Number.isFinite(t.expectedDurationHours)
|
|
2166
|
+
? Math.max(0, t.expectedDurationHours)
|
|
2167
|
+
: 0), 0);
|
|
2168
|
+
let tokenEstimate = estimateTokensForDurationHours(expectedDurationHours);
|
|
2169
|
+
const remainingTokens = tokenBudgetValue !== null ? tokenBudgetValue - run.tokensUsed : null;
|
|
2170
|
+
if (remainingTokens !== null && remainingTokens <= 0) {
|
|
2171
|
+
await stopAutoContinueRun({ run, reason: "budget_exhausted" });
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
// If the modeled slice exceeds the remaining budget, shrink the slice to fit rather than
|
|
2175
|
+
// stopping immediately (Play should still dispatch at least the primary task when possible).
|
|
2176
|
+
if (remainingTokens !== null && tokenEstimate > 0 && tokenEstimate > remainingTokens) {
|
|
2177
|
+
const nextSlice = [];
|
|
2178
|
+
let hours = 0;
|
|
2179
|
+
for (const task of sliceTaskNodes) {
|
|
2180
|
+
const taskHours = typeof task.expectedDurationHours === "number" && Number.isFinite(task.expectedDurationHours)
|
|
2181
|
+
? Math.max(0, task.expectedDurationHours)
|
|
2182
|
+
: 0;
|
|
2183
|
+
if (nextSlice.length === 0) {
|
|
2184
|
+
nextSlice.push(task);
|
|
2185
|
+
hours += taskHours;
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
const nextEstimate = estimateTokensForDurationHours(hours + taskHours);
|
|
2189
|
+
if (nextEstimate > remainingTokens)
|
|
2190
|
+
continue;
|
|
2191
|
+
nextSlice.push(task);
|
|
2192
|
+
hours += taskHours;
|
|
2193
|
+
}
|
|
2194
|
+
cappedSliceTaskNodes = nextSlice;
|
|
2195
|
+
expectedDurationHours = hours;
|
|
2196
|
+
tokenEstimate = estimateTokensForDurationHours(expectedDurationHours);
|
|
2197
|
+
}
|
|
2198
|
+
if (remainingTokens !== null && tokenEstimate > 0 && tokenEstimate > remainingTokens) {
|
|
2199
|
+
await stopAutoContinueRun({ run, reason: "budget_exhausted" });
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
const executionPolicy = deriveExecutionPolicy(primaryTask, workstreamNode);
|
|
2203
|
+
const behaviorConfig = deriveBehaviorConfigContext(primaryTask, workstreamNode);
|
|
2204
|
+
const behaviorAutomationLevel = deriveBehaviorAutomationLevel(primaryTask, workstreamNode);
|
|
2205
|
+
const sliceRunId = randomUUID();
|
|
2206
|
+
await emitActivitySafe({
|
|
2207
|
+
initiativeId: run.initiativeId,
|
|
2208
|
+
runId: sliceRunId,
|
|
2209
|
+
correlationId: sliceRunId,
|
|
2210
|
+
phase: "intent",
|
|
2211
|
+
level: "info",
|
|
2212
|
+
progressPct: 5,
|
|
2213
|
+
message: `Orchestrator selected ${workstreamTitle ?? selectedWorkstreamId} for the next slice.`,
|
|
2214
|
+
nextStep: `Preparing dispatch checks before spawning ${executionPolicy.domain} execution.`,
|
|
2215
|
+
metadata: {
|
|
2216
|
+
...buildSliceEnrichment({
|
|
2217
|
+
run,
|
|
2218
|
+
taskId: primaryTask.id,
|
|
2219
|
+
taskTitle: primaryTask.title ?? null,
|
|
2220
|
+
workstreamId: selectedWorkstreamId,
|
|
2221
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
2222
|
+
domain: executionPolicy.domain,
|
|
2223
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
2224
|
+
event: "orchestrator_dispatch",
|
|
2225
|
+
}),
|
|
2226
|
+
scope: run.scope,
|
|
2227
|
+
candidate_task_count: sliceTaskNodes.length,
|
|
2228
|
+
},
|
|
2229
|
+
});
|
|
2230
|
+
const behaviorConfigDrift = detectBehaviorConfigDrift({
|
|
2231
|
+
taskNode: primaryTask,
|
|
2232
|
+
workstreamNode,
|
|
2233
|
+
behaviorConfig,
|
|
2234
|
+
behaviorAutomationLevel,
|
|
2235
|
+
});
|
|
2236
|
+
if (behaviorConfigDrift) {
|
|
2237
|
+
await emitActivitySafe({
|
|
2238
|
+
initiativeId: run.initiativeId,
|
|
2239
|
+
runId: sliceRunId,
|
|
2240
|
+
correlationId: sliceRunId,
|
|
2241
|
+
phase: "review",
|
|
2242
|
+
level: "warn",
|
|
2243
|
+
progressPct: 15,
|
|
2244
|
+
message: `Behavior config drift detected for ${workstreamTitle ?? selectedWorkstreamId}; ` +
|
|
2245
|
+
`runtime behavior differs from declared workstream config.`,
|
|
2246
|
+
metadata: {
|
|
2247
|
+
...buildSliceEnrichment({
|
|
2248
|
+
run,
|
|
2249
|
+
taskId: primaryTask.id,
|
|
2250
|
+
taskTitle: primaryTask.title ?? null,
|
|
2251
|
+
workstreamId: selectedWorkstreamId,
|
|
2252
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
2253
|
+
domain: executionPolicy.domain,
|
|
2254
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
2255
|
+
event: "auto_continue_behavior_config_drift_detected",
|
|
2256
|
+
}),
|
|
2257
|
+
drift_fields: behaviorConfigDrift.fields,
|
|
2258
|
+
declared_behavior_config_id: behaviorConfigDrift.declared.configId,
|
|
2259
|
+
declared_behavior_config_version: behaviorConfigDrift.declared.version,
|
|
2260
|
+
declared_behavior_config_hash: behaviorConfigDrift.declared.hash,
|
|
2261
|
+
declared_policy_source: behaviorConfigDrift.declared.policySource,
|
|
2262
|
+
declared_behavior_context: behaviorConfigDrift.declared.context,
|
|
2263
|
+
declared_behavior_automation_level: behaviorConfigDrift.declared.automationLevel,
|
|
2264
|
+
runtime_behavior_config_id: behaviorConfigDrift.runtime.configId,
|
|
2265
|
+
runtime_behavior_config_version: behaviorConfigDrift.runtime.version,
|
|
2266
|
+
runtime_behavior_config_hash: behaviorConfigDrift.runtime.hash,
|
|
2267
|
+
runtime_policy_source: behaviorConfigDrift.runtime.policySource,
|
|
2268
|
+
runtime_behavior_context: behaviorConfigDrift.runtime.context,
|
|
2269
|
+
runtime_behavior_automation_level: behaviorConfigDrift.runtime.automationLevel,
|
|
2270
|
+
error_location: "mission-control.auto-continue.engine.behavior-config.drift",
|
|
2271
|
+
},
|
|
2272
|
+
nextStep: "Review task/workstream behavior metadata and reconcile the declared config if override is unintended.",
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
if (behaviorConfig.requiresApproval) {
|
|
2276
|
+
const blockedReason = `Behavior config approval required before dispatch for ${workstreamTitle ?? selectedWorkstreamId}.`;
|
|
2277
|
+
await emitActivitySafe({
|
|
2278
|
+
initiativeId: run.initiativeId,
|
|
2279
|
+
runId: sliceRunId,
|
|
2280
|
+
correlationId: sliceRunId,
|
|
2281
|
+
phase: "blocked",
|
|
2282
|
+
level: "warn",
|
|
2283
|
+
progressPct: 20,
|
|
2284
|
+
message: blockedReason,
|
|
2285
|
+
metadata: {
|
|
2286
|
+
...buildSliceEnrichment({
|
|
2287
|
+
run,
|
|
2288
|
+
taskId: primaryTask.id,
|
|
2289
|
+
taskTitle: primaryTask.title ?? null,
|
|
2290
|
+
workstreamId: selectedWorkstreamId,
|
|
2291
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
2292
|
+
domain: executionPolicy.domain,
|
|
2293
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
2294
|
+
event: "auto_continue_behavior_config_approval_required",
|
|
2295
|
+
}),
|
|
2296
|
+
behavior_config_id: behaviorConfig.configId,
|
|
2297
|
+
behavior_config_version: behaviorConfig.version,
|
|
2298
|
+
behavior_config_hash: behaviorConfig.hash,
|
|
2299
|
+
behavior_approval_status: behaviorConfig.approvalStatus,
|
|
2300
|
+
behavior_approval_decision_id: behaviorConfig.approvalDecisionId,
|
|
2301
|
+
blocked_reason: blockedReason,
|
|
2302
|
+
error_location: "mission-control.auto-continue.engine.behavior-config.approval",
|
|
2303
|
+
},
|
|
2304
|
+
nextStep: "Approve the behavior config, then rerun Play/auto-continue for this workstream.",
|
|
2305
|
+
});
|
|
2306
|
+
const decisionResult = await requestDecisionQueued({
|
|
2307
|
+
initiativeId: run.initiativeId,
|
|
2308
|
+
correlationId: sliceRunId,
|
|
2309
|
+
title: `Approve behavior config for ${workstreamTitle ?? selectedWorkstreamId}`,
|
|
2310
|
+
summary: [
|
|
2311
|
+
`Autopilot paused before dispatch because behavior config requires approval.`,
|
|
2312
|
+
`Task: ${primaryTask.id}.`,
|
|
2313
|
+
behaviorConfig.configId ? `Config: ${behaviorConfig.configId}.` : "",
|
|
2314
|
+
behaviorConfig.version ? `Version: ${behaviorConfig.version}.` : "",
|
|
2315
|
+
behaviorConfig.approvalStatus ? `Approval status: ${behaviorConfig.approvalStatus}.` : "",
|
|
2316
|
+
]
|
|
2317
|
+
.filter(Boolean)
|
|
2318
|
+
.join(" "),
|
|
2319
|
+
urgency: "high",
|
|
2320
|
+
options: [
|
|
2321
|
+
"Approve config and continue execution",
|
|
2322
|
+
"Reject config and revise policy",
|
|
2323
|
+
"Pause this workstream",
|
|
2324
|
+
],
|
|
2325
|
+
blocking: true,
|
|
2326
|
+
decisionType: "autopilot_behavior_config_approval",
|
|
2327
|
+
workstreamId: selectedWorkstreamId,
|
|
2328
|
+
agentId: run.agentId,
|
|
2329
|
+
sourceSystem: "orgx-autopilot",
|
|
2330
|
+
conflictSource: "behavior_config_requires_approval",
|
|
2331
|
+
dedupeKey: [
|
|
2332
|
+
"autopilot",
|
|
2333
|
+
run.initiativeId,
|
|
2334
|
+
selectedWorkstreamId,
|
|
2335
|
+
"behavior_config_requires_approval",
|
|
2336
|
+
behaviorConfig.configId ?? "default",
|
|
2337
|
+
behaviorConfig.version ?? "unknown",
|
|
2338
|
+
].join(":"),
|
|
2339
|
+
recommendedAction: "Resolve approval state before allowing autopilot to spawn a worker.",
|
|
2340
|
+
sourceRunId: sliceRunId,
|
|
2341
|
+
sourceRef: {
|
|
2342
|
+
run_id: sliceRunId,
|
|
2343
|
+
workstream_id: selectedWorkstreamId,
|
|
2344
|
+
task_id: primaryTask.id,
|
|
2345
|
+
behavior_config_id: behaviorConfig.configId,
|
|
2346
|
+
behavior_approval_status: behaviorConfig.approvalStatus,
|
|
2347
|
+
behavior_approval_decision_id: behaviorConfig.approvalDecisionId,
|
|
2348
|
+
},
|
|
2349
|
+
});
|
|
2350
|
+
if (!run.blockedWorkstreamIds.includes(selectedWorkstreamId)) {
|
|
2351
|
+
run.blockedWorkstreamIds.push(selectedWorkstreamId);
|
|
2352
|
+
}
|
|
2353
|
+
setLaneState(run, {
|
|
2354
|
+
workstreamId: selectedWorkstreamId,
|
|
2355
|
+
state: "blocked",
|
|
2356
|
+
activeRunId: null,
|
|
2357
|
+
activeTaskIds: [],
|
|
2358
|
+
blockedReason,
|
|
2359
|
+
waitingOnWorkstreamIds: [],
|
|
2360
|
+
retryAt: null,
|
|
2361
|
+
});
|
|
2362
|
+
await stopAutoContinueRun({
|
|
2363
|
+
run,
|
|
2364
|
+
reason: "blocked",
|
|
2365
|
+
error: blockedReason,
|
|
2366
|
+
decisionRequired: decisionResult.queued,
|
|
2367
|
+
decisionIds: decisionResult.decisionIds,
|
|
2368
|
+
});
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
const isManualPlayDispatch = run.stopAfterSlice &&
|
|
2372
|
+
Array.isArray(run.allowedWorkstreamIds) &&
|
|
2373
|
+
run.allowedWorkstreamIds.length === 1;
|
|
2374
|
+
if (behaviorAutomationLevel === "manual" && !isManualPlayDispatch) {
|
|
2375
|
+
const blockedReason = `Automation level manual prevents auto-continue dispatch for ${workstreamTitle ?? selectedWorkstreamId}.`;
|
|
2376
|
+
await emitActivitySafe({
|
|
2377
|
+
initiativeId: run.initiativeId,
|
|
2378
|
+
runId: sliceRunId,
|
|
2379
|
+
correlationId: sliceRunId,
|
|
2380
|
+
phase: "blocked",
|
|
2381
|
+
level: "warn",
|
|
2382
|
+
progressPct: 20,
|
|
2383
|
+
message: blockedReason,
|
|
2384
|
+
metadata: {
|
|
2385
|
+
...buildSliceEnrichment({
|
|
2386
|
+
run,
|
|
2387
|
+
taskId: primaryTask.id,
|
|
2388
|
+
taskTitle: primaryTask.title ?? null,
|
|
2389
|
+
workstreamId: selectedWorkstreamId,
|
|
2390
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
2391
|
+
domain: executionPolicy.domain,
|
|
2392
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
2393
|
+
event: "auto_continue_behavior_automation_manual_blocked",
|
|
2394
|
+
}),
|
|
2395
|
+
behavior_config_id: behaviorConfig.configId,
|
|
2396
|
+
behavior_config_version: behaviorConfig.version,
|
|
2397
|
+
behavior_automation_level: behaviorAutomationLevel,
|
|
2398
|
+
blocked_reason: blockedReason,
|
|
2399
|
+
error_location: "mission-control.auto-continue.engine.behavior.automation.manual",
|
|
2400
|
+
},
|
|
2401
|
+
nextStep: "Use manual Play to dispatch this workstream slice.",
|
|
2402
|
+
});
|
|
2403
|
+
const decisionResult = await requestDecisionQueued({
|
|
2404
|
+
initiativeId: run.initiativeId,
|
|
2405
|
+
correlationId: sliceRunId,
|
|
2406
|
+
title: `Manual dispatch required for ${workstreamTitle ?? selectedWorkstreamId}`,
|
|
2407
|
+
summary: [
|
|
2408
|
+
"Autopilot paused because behavior automation level is manual.",
|
|
2409
|
+
`Task: ${primaryTask.id}.`,
|
|
2410
|
+
behaviorConfig.configId ? `Config: ${behaviorConfig.configId}.` : "",
|
|
2411
|
+
behaviorConfig.version ? `Version: ${behaviorConfig.version}.` : "",
|
|
2412
|
+
]
|
|
2413
|
+
.filter(Boolean)
|
|
2414
|
+
.join(" "),
|
|
2415
|
+
urgency: "high",
|
|
2416
|
+
options: [
|
|
2417
|
+
"Dispatch this workstream manually now",
|
|
2418
|
+
"Switch automation level to supervised",
|
|
2419
|
+
"Switch automation level to auto",
|
|
2420
|
+
],
|
|
2421
|
+
blocking: true,
|
|
2422
|
+
decisionType: "autopilot_behavior_manual_dispatch_required",
|
|
2423
|
+
workstreamId: selectedWorkstreamId,
|
|
2424
|
+
agentId: run.agentId,
|
|
2425
|
+
sourceSystem: "orgx-autopilot",
|
|
2426
|
+
conflictSource: "behavior_automation_level_manual",
|
|
2427
|
+
dedupeKey: [
|
|
2428
|
+
"autopilot",
|
|
2429
|
+
run.initiativeId,
|
|
2430
|
+
selectedWorkstreamId,
|
|
2431
|
+
"behavior_automation_level_manual",
|
|
2432
|
+
behaviorConfig.configId ?? "default",
|
|
2433
|
+
behaviorConfig.version ?? "unknown",
|
|
2434
|
+
].join(":"),
|
|
2435
|
+
recommendedAction: "Dispatch manually for this workstream, or switch behavior automation level before rerunning auto-continue.",
|
|
2436
|
+
sourceRunId: sliceRunId,
|
|
2437
|
+
sourceRef: {
|
|
2438
|
+
run_id: sliceRunId,
|
|
2439
|
+
workstream_id: selectedWorkstreamId,
|
|
2440
|
+
task_id: primaryTask.id,
|
|
2441
|
+
behavior_config_id: behaviorConfig.configId,
|
|
2442
|
+
behavior_automation_level: behaviorAutomationLevel,
|
|
2443
|
+
},
|
|
2444
|
+
});
|
|
2445
|
+
if (!run.blockedWorkstreamIds.includes(selectedWorkstreamId)) {
|
|
2446
|
+
run.blockedWorkstreamIds.push(selectedWorkstreamId);
|
|
2447
|
+
}
|
|
2448
|
+
setLaneState(run, {
|
|
2449
|
+
workstreamId: selectedWorkstreamId,
|
|
2450
|
+
state: "blocked",
|
|
2451
|
+
activeRunId: null,
|
|
2452
|
+
activeTaskIds: [],
|
|
2453
|
+
blockedReason,
|
|
2454
|
+
waitingOnWorkstreamIds: [],
|
|
2455
|
+
retryAt: null,
|
|
2456
|
+
});
|
|
2457
|
+
await stopAutoContinueRun({
|
|
2458
|
+
run,
|
|
2459
|
+
reason: "blocked",
|
|
2460
|
+
error: blockedReason,
|
|
2461
|
+
decisionRequired: decisionResult.queued,
|
|
2462
|
+
decisionIds: decisionResult.decisionIds,
|
|
2463
|
+
});
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
if (behaviorAutomationLevel === "supervised" && !run.stopAfterSlice) {
|
|
2467
|
+
run.stopAfterSlice = true;
|
|
2468
|
+
await emitActivitySafe({
|
|
2469
|
+
initiativeId: run.initiativeId,
|
|
2470
|
+
runId: sliceRunId,
|
|
2471
|
+
correlationId: sliceRunId,
|
|
2472
|
+
phase: "execution",
|
|
2473
|
+
level: "info",
|
|
2474
|
+
progressPct: 25,
|
|
2475
|
+
message: `Supervised automation level: dispatching one slice for ${workstreamTitle ?? selectedWorkstreamId}.`,
|
|
2476
|
+
metadata: {
|
|
2477
|
+
...buildSliceEnrichment({
|
|
2478
|
+
run,
|
|
2479
|
+
taskId: primaryTask.id,
|
|
2480
|
+
taskTitle: primaryTask.title ?? null,
|
|
2481
|
+
workstreamId: selectedWorkstreamId,
|
|
2482
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
2483
|
+
domain: executionPolicy.domain,
|
|
2484
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
2485
|
+
event: "auto_continue_behavior_automation_supervised_one_shot",
|
|
2486
|
+
}),
|
|
2487
|
+
behavior_automation_level: behaviorAutomationLevel,
|
|
2488
|
+
},
|
|
2489
|
+
nextStep: "Resume to dispatch the next slice after this one completes.",
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
const spawnGuardResult = await checkSpawnGuardSafe({
|
|
2493
|
+
domain: executionPolicy.domain,
|
|
2494
|
+
taskId: primaryTask.id,
|
|
2495
|
+
initiativeId: run.initiativeId,
|
|
2496
|
+
correlationId: sliceRunId,
|
|
2497
|
+
runId: sliceRunId,
|
|
2498
|
+
targetLabel: "autopilot slice",
|
|
2499
|
+
});
|
|
2500
|
+
if (spawnGuardResult && typeof spawnGuardResult === "object") {
|
|
2501
|
+
const allowed = spawnGuardResult.allowed;
|
|
2502
|
+
if (allowed === false) {
|
|
2503
|
+
const blockedReason = summarizeSpawnGuardBlockReason(spawnGuardResult);
|
|
2504
|
+
const retryable = spawnGuardIsRateLimited(spawnGuardResult);
|
|
2505
|
+
const rateLimitOverrideRequested = retryable && run.ignoreSpawnGuardRateLimit;
|
|
2506
|
+
if (retryable && !rateLimitOverrideRequested) {
|
|
2507
|
+
const retryAtMs = Date.now() + AUTO_CONTINUE_SPAWN_GUARD_RETRY_MS;
|
|
2508
|
+
const retryAtIso = new Date(retryAtMs).toISOString();
|
|
2509
|
+
autoContinueSpawnGuardRetryByTask.set(primaryTask.id, {
|
|
2510
|
+
initiativeId: run.initiativeId,
|
|
2511
|
+
retryAtMs,
|
|
2512
|
+
});
|
|
2513
|
+
setLaneState(run, {
|
|
2514
|
+
workstreamId: selectedWorkstreamId,
|
|
2515
|
+
state: "rate_limited",
|
|
2516
|
+
activeRunId: null,
|
|
2517
|
+
activeTaskIds: [],
|
|
2518
|
+
blockedReason,
|
|
2519
|
+
waitingOnWorkstreamIds: [],
|
|
2520
|
+
retryAt: retryAtIso,
|
|
2521
|
+
});
|
|
2522
|
+
await emitActivitySafe({
|
|
2523
|
+
initiativeId: run.initiativeId,
|
|
2524
|
+
runId: sliceRunId,
|
|
2525
|
+
correlationId: sliceRunId,
|
|
2526
|
+
phase: "blocked",
|
|
2527
|
+
level: "warn",
|
|
2528
|
+
progressPct: 25,
|
|
2529
|
+
message: `Autopilot spawn guard rate-limited ${workstreamTitle ?? selectedWorkstreamId}; retrying shortly.`,
|
|
2530
|
+
metadata: {
|
|
2531
|
+
...buildSliceEnrichment({
|
|
2532
|
+
run,
|
|
2533
|
+
taskId: primaryTask.id,
|
|
2534
|
+
taskTitle: primaryTask.title ?? null,
|
|
2535
|
+
workstreamId: selectedWorkstreamId,
|
|
2536
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
2537
|
+
domain: executionPolicy.domain,
|
|
2538
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
2539
|
+
event: "auto_continue_spawn_guard_rate_limited",
|
|
2540
|
+
}),
|
|
2541
|
+
blocked_reason: blockedReason,
|
|
2542
|
+
error_location: "mission-control.auto-continue.engine.spawn-guard.rate-limited",
|
|
2543
|
+
next_retry_at: retryAtIso,
|
|
2544
|
+
next_retry_in_ms: AUTO_CONTINUE_SPAWN_GUARD_RETRY_MS,
|
|
2545
|
+
spawn_guard: spawnGuardResult,
|
|
2546
|
+
},
|
|
2547
|
+
nextStep: "Retry dispatch when spawn rate limits recover.",
|
|
2548
|
+
});
|
|
2549
|
+
run.lastError = blockedReason;
|
|
2550
|
+
run.updatedAt = now;
|
|
2551
|
+
syncLegacyRunPointers(run);
|
|
2552
|
+
try {
|
|
2553
|
+
await updateInitiativeAutoContinueState({
|
|
2554
|
+
initiativeId: run.initiativeId,
|
|
2555
|
+
run,
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
catch {
|
|
2559
|
+
// best effort
|
|
2560
|
+
}
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
if (rateLimitOverrideRequested) {
|
|
2564
|
+
const overrideMode = run.stopAfterSlice &&
|
|
2565
|
+
Array.isArray(run.allowedWorkstreamIds) &&
|
|
2566
|
+
run.allowedWorkstreamIds.length === 1
|
|
2567
|
+
? "Play"
|
|
2568
|
+
: "Auto-continue";
|
|
2569
|
+
await emitActivitySafe({
|
|
2570
|
+
initiativeId: run.initiativeId,
|
|
2571
|
+
runId: sliceRunId,
|
|
2572
|
+
correlationId: sliceRunId,
|
|
2573
|
+
phase: "execution",
|
|
2574
|
+
level: "warn",
|
|
2575
|
+
progressPct: 25,
|
|
2576
|
+
message: `${overrideMode} override: dispatching ${workstreamTitle ?? selectedWorkstreamId} despite spawn guard rate limit.`,
|
|
2577
|
+
metadata: {
|
|
2578
|
+
...buildSliceEnrichment({
|
|
2579
|
+
run,
|
|
2580
|
+
taskId: primaryTask.id,
|
|
2581
|
+
taskTitle: primaryTask.title ?? null,
|
|
2582
|
+
workstreamId: selectedWorkstreamId,
|
|
2583
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
2584
|
+
domain: executionPolicy.domain,
|
|
2585
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
2586
|
+
event: "auto_continue_spawn_guard_rate_limit_overridden",
|
|
2587
|
+
}),
|
|
2588
|
+
blocked_reason: blockedReason,
|
|
2589
|
+
error_location: "mission-control.auto-continue.engine.spawn-guard.override",
|
|
2590
|
+
spawn_guard: spawnGuardResult,
|
|
2591
|
+
},
|
|
2592
|
+
nextStep: "Manual Play requested immediate execution for this single workstream slice.",
|
|
2593
|
+
});
|
|
2594
|
+
run.lastError = null;
|
|
2595
|
+
run.updatedAt = now;
|
|
2596
|
+
setLaneState(run, {
|
|
2597
|
+
workstreamId: selectedWorkstreamId,
|
|
2598
|
+
state: "idle",
|
|
2599
|
+
activeRunId: null,
|
|
2600
|
+
activeTaskIds: [],
|
|
2601
|
+
blockedReason: null,
|
|
2602
|
+
waitingOnWorkstreamIds: [],
|
|
2603
|
+
retryAt: null,
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
else {
|
|
2607
|
+
// Maintain existing behavior: mark the primary task blocked when a quality gate denies dispatch.
|
|
2608
|
+
try {
|
|
2609
|
+
await client.updateEntity("task", primaryTask.id, { status: "blocked" });
|
|
2610
|
+
}
|
|
2611
|
+
catch {
|
|
2612
|
+
// best effort
|
|
2613
|
+
}
|
|
2614
|
+
try {
|
|
2615
|
+
await syncParentRollupsForTask({
|
|
2616
|
+
initiativeId: run.initiativeId,
|
|
2617
|
+
taskId: primaryTask.id,
|
|
2618
|
+
workstreamId: selectedWorkstreamId,
|
|
2619
|
+
milestoneId: primaryTask.milestoneId,
|
|
2620
|
+
correlationId: sliceRunId,
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
catch {
|
|
2624
|
+
// best effort
|
|
2625
|
+
}
|
|
2626
|
+
await emitActivitySafe({
|
|
2627
|
+
initiativeId: run.initiativeId,
|
|
2628
|
+
runId: sliceRunId,
|
|
2629
|
+
correlationId: sliceRunId,
|
|
2630
|
+
phase: "blocked",
|
|
2631
|
+
level: "error",
|
|
2632
|
+
progressPct: 25,
|
|
2633
|
+
message: `Autopilot blocked by spawn guard for ${workstreamTitle ?? selectedWorkstreamId}.`,
|
|
2634
|
+
metadata: {
|
|
2635
|
+
...buildSliceEnrichment({
|
|
2636
|
+
run,
|
|
2637
|
+
taskId: primaryTask.id,
|
|
2638
|
+
taskTitle: primaryTask.title ?? null,
|
|
2639
|
+
workstreamId: selectedWorkstreamId,
|
|
2640
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
2641
|
+
domain: executionPolicy.domain,
|
|
2642
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
2643
|
+
event: "auto_continue_spawn_guard_blocked",
|
|
2644
|
+
}),
|
|
2645
|
+
blocked_reason: blockedReason,
|
|
2646
|
+
error_location: "mission-control.auto-continue.engine.spawn-guard.blocked",
|
|
2647
|
+
spawn_guard: spawnGuardResult,
|
|
2648
|
+
},
|
|
2649
|
+
});
|
|
2650
|
+
const decisionResult = await requestDecisionQueued({
|
|
2651
|
+
initiativeId: run.initiativeId,
|
|
2652
|
+
correlationId: sliceRunId,
|
|
2653
|
+
title: `Unblock autopilot for ${workstreamTitle ?? selectedWorkstreamId}`,
|
|
2654
|
+
summary: [
|
|
2655
|
+
`Spawn guard denied dispatch for primary task ${primaryTask.id}.`,
|
|
2656
|
+
`Reason: ${blockedReason}`,
|
|
2657
|
+
`Domain: ${executionPolicy.domain}`,
|
|
2658
|
+
`Required skills: ${executionPolicy.requiredSkills.join(", ")}`,
|
|
2659
|
+
].join(" "),
|
|
2660
|
+
urgency: "high",
|
|
2661
|
+
options: [
|
|
2662
|
+
"Approve exception and continue",
|
|
2663
|
+
"Reassign slice/domain",
|
|
2664
|
+
"Pause and investigate quality gate",
|
|
2665
|
+
],
|
|
2666
|
+
blocking: true,
|
|
2667
|
+
decisionType: "autopilot_spawn_guard_block",
|
|
2668
|
+
workstreamId: selectedWorkstreamId,
|
|
2669
|
+
agentId: run.agentId,
|
|
2670
|
+
sourceSystem: "orgx-autopilot",
|
|
2671
|
+
conflictSource: "spawn_guard_blocked",
|
|
2672
|
+
dedupeKey: [
|
|
2673
|
+
"autopilot",
|
|
2674
|
+
run.initiativeId,
|
|
2675
|
+
selectedWorkstreamId,
|
|
2676
|
+
"spawn_guard_blocked",
|
|
2677
|
+
executionPolicy.domain,
|
|
2678
|
+
].join(":"),
|
|
2679
|
+
recommendedAction: "Choose exception, reassignment, or pause so dispatch can proceed safely.",
|
|
2680
|
+
sourceRunId: sliceRunId,
|
|
2681
|
+
sourceRef: {
|
|
2682
|
+
run_id: sliceRunId,
|
|
2683
|
+
workstream_id: selectedWorkstreamId,
|
|
2684
|
+
task_id: primaryTask.id,
|
|
2685
|
+
domain: executionPolicy.domain,
|
|
2686
|
+
},
|
|
2687
|
+
evidenceRefs: [
|
|
2688
|
+
{
|
|
2689
|
+
evidence_type: "spawn_guard_result",
|
|
2690
|
+
title: "Spawn guard denied dispatch",
|
|
2691
|
+
summary: blockedReason,
|
|
2692
|
+
source_pointer: null,
|
|
2693
|
+
payload: {
|
|
2694
|
+
spawn_guard: spawnGuardResult,
|
|
2695
|
+
task_id: primaryTask.id,
|
|
2696
|
+
domain: executionPolicy.domain,
|
|
2697
|
+
},
|
|
2698
|
+
},
|
|
2699
|
+
],
|
|
2700
|
+
});
|
|
2701
|
+
if (!run.blockedWorkstreamIds.includes(selectedWorkstreamId)) {
|
|
2702
|
+
run.blockedWorkstreamIds.push(selectedWorkstreamId);
|
|
2703
|
+
}
|
|
2704
|
+
setLaneState(run, {
|
|
2705
|
+
workstreamId: selectedWorkstreamId,
|
|
2706
|
+
state: "blocked",
|
|
2707
|
+
activeRunId: null,
|
|
2708
|
+
activeTaskIds: [],
|
|
2709
|
+
blockedReason,
|
|
2710
|
+
waitingOnWorkstreamIds: [],
|
|
2711
|
+
retryAt: null,
|
|
2712
|
+
});
|
|
2713
|
+
await stopAutoContinueRun({
|
|
2714
|
+
run,
|
|
2715
|
+
reason: "blocked",
|
|
2716
|
+
error: blockedReason,
|
|
2717
|
+
decisionRequired: decisionResult.queued,
|
|
2718
|
+
decisionIds: decisionResult.decisionIds,
|
|
2719
|
+
});
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
const milestoneIds = dedupeStrings(cappedSliceTaskNodes.map((t) => (t.milestoneId ?? "").trim()).filter(Boolean));
|
|
2725
|
+
const milestoneSummaries = milestoneIds
|
|
2726
|
+
.map((id) => nodeById.get(id))
|
|
2727
|
+
.filter((node) => Boolean(node && node.type === "milestone"))
|
|
2728
|
+
.map((m) => ({ id: m.id, title: m.title, status: m.status }));
|
|
2729
|
+
const taskSummaries = cappedSliceTaskNodes.map((t) => ({
|
|
2730
|
+
id: t.id,
|
|
2731
|
+
title: t.title,
|
|
2732
|
+
status: t.status,
|
|
2733
|
+
milestoneId: t.milestoneId ?? null,
|
|
2734
|
+
}));
|
|
2735
|
+
const schemaPath = ensureAutopilotSliceSchemaPath(AUTO_CONTINUE_SLICE_SCHEMA_FILENAME);
|
|
2736
|
+
// Try server KickoffContext (includes team context, acceptance criteria, etc.)
|
|
2737
|
+
let prompt;
|
|
2738
|
+
let kickoffContextHash = null;
|
|
2739
|
+
if (fetchKickoffContextSafeFn && renderKickoffMessageFn) {
|
|
2740
|
+
let kickoff = null;
|
|
2741
|
+
try {
|
|
2742
|
+
kickoff = await fetchKickoffContextSafeFn(client, {
|
|
2743
|
+
initiative_id: run.initiativeId,
|
|
2744
|
+
workstream_id: selectedWorkstreamId,
|
|
2745
|
+
task_id: primaryTask.id,
|
|
2746
|
+
domain: executionPolicy.domain,
|
|
2747
|
+
required_skills: executionPolicy.requiredSkills,
|
|
2748
|
+
agent_id: resolveOrgxAgentForDomain(executionPolicy.domain).id,
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
catch {
|
|
2752
|
+
// best effort: fall back to local prompt
|
|
2753
|
+
}
|
|
2754
|
+
if (kickoff) {
|
|
2755
|
+
const rendered = renderKickoffMessageFn({
|
|
2756
|
+
baseMessage: `Execute workstream slice for ${workstreamTitle ?? selectedWorkstreamId}`,
|
|
2757
|
+
kickoff,
|
|
2758
|
+
domain: executionPolicy.domain,
|
|
2759
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
2760
|
+
});
|
|
2761
|
+
const sliceInstructions = buildSliceOutputInstructions({
|
|
2762
|
+
runId: sliceRunId,
|
|
2763
|
+
schemaPath,
|
|
2764
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
2765
|
+
});
|
|
2766
|
+
prompt = rendered.message + "\n\n" + sliceInstructions;
|
|
2767
|
+
kickoffContextHash = rendered.contextHash;
|
|
2768
|
+
}
|
|
2769
|
+
else {
|
|
2770
|
+
// Fallback: existing local prompt (offline/degraded mode)
|
|
2771
|
+
prompt = buildWorkstreamSlicePrompt({
|
|
2772
|
+
initiativeTitle,
|
|
2773
|
+
initiativeId: run.initiativeId,
|
|
2774
|
+
workstreamId: selectedWorkstreamId,
|
|
2775
|
+
workstreamTitle: workstreamTitle ?? `Workstream ${selectedWorkstreamId.slice(0, 8)}`,
|
|
2776
|
+
milestoneSummaries,
|
|
2777
|
+
taskSummaries,
|
|
2778
|
+
executionPolicy,
|
|
2779
|
+
behaviorConfig,
|
|
2780
|
+
runId: sliceRunId,
|
|
2781
|
+
schemaPath,
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
else {
|
|
2786
|
+
// No KickoffContext functions available: use local prompt
|
|
2787
|
+
prompt = buildWorkstreamSlicePrompt({
|
|
2788
|
+
initiativeTitle,
|
|
2789
|
+
initiativeId: run.initiativeId,
|
|
2790
|
+
workstreamId: selectedWorkstreamId,
|
|
2791
|
+
workstreamTitle: workstreamTitle ?? `Workstream ${selectedWorkstreamId.slice(0, 8)}`,
|
|
2792
|
+
milestoneSummaries,
|
|
2793
|
+
taskSummaries,
|
|
2794
|
+
executionPolicy,
|
|
2795
|
+
behaviorConfig,
|
|
2796
|
+
runId: sliceRunId,
|
|
2797
|
+
schemaPath,
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
// Append per-scope directive for milestone/workstream scopes.
|
|
2801
|
+
if (run.scope !== "task") {
|
|
2802
|
+
const msNodes = scopeMilestoneIds
|
|
2803
|
+
.map((id) => nodeById.get(id))
|
|
2804
|
+
.filter((n) => Boolean(n));
|
|
2805
|
+
const scopeDirective = buildScopeDirective(run.scope, {
|
|
2806
|
+
milestoneTitles: msNodes.map((n) => n.title),
|
|
2807
|
+
workstreamTitle: workstreamTitle ?? undefined,
|
|
2808
|
+
taskCount: cappedSliceTaskNodes.length,
|
|
2809
|
+
});
|
|
2810
|
+
if (scopeDirective) {
|
|
2811
|
+
prompt = prompt + "\n\n" + scopeDirective;
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
const logsDir = join(getOrgxPluginConfigDir(), AUTO_CONTINUE_SLICE_LOG_DIRNAME);
|
|
2815
|
+
const logPath = join(logsDir, `${sliceRunId}.log`);
|
|
2816
|
+
const outputPath = join(logsDir, `${sliceRunId}.output.json`);
|
|
2817
|
+
const configuredWorkerCwd = (process.env.ORGX_AUTOPILOT_CWD ?? "").trim();
|
|
2818
|
+
let workerCwd = configuredWorkerCwd || resolveAutopilotDefaultCwd(__filename);
|
|
2819
|
+
// LaunchAgents sometimes start with cwd="/". Fall back to plugin root (or home if unresolved).
|
|
2820
|
+
if (!workerCwd || workerCwd === "/") {
|
|
2821
|
+
workerCwd = resolveAutopilotDefaultCwd(__filename);
|
|
2822
|
+
}
|
|
2823
|
+
const sliceAgent = resolveOrgxAgentForDomain(executionPolicy.domain);
|
|
2824
|
+
const workerKind = (process.env.ORGX_AUTOPILOT_WORKER_KIND ?? "").trim().toLowerCase();
|
|
2825
|
+
const inferredExecutor = workerKind === "claude-code" || workerKind === "claude_code" ? "claude-code" : "codex";
|
|
2826
|
+
const executorRaw = (process.env.ORGX_AUTOPILOT_EXECUTOR ?? "").trim().toLowerCase() || inferredExecutor;
|
|
2827
|
+
const executorSourceClient = executorRaw === "claude-code" || executorRaw === "claude_code" ? "claude-code" : "codex";
|
|
2828
|
+
let runtimeHookUrl = null;
|
|
2829
|
+
let runtimeHookToken = null;
|
|
2830
|
+
try {
|
|
2831
|
+
const snapshot = readOpenClawSettingsSnapshot();
|
|
2832
|
+
const port = readOpenClawGatewayPort(snapshot.raw);
|
|
2833
|
+
runtimeHookUrl = `http://127.0.0.1:${port}/orgx/api/hooks/runtime`;
|
|
2834
|
+
runtimeHookToken = resolveRuntimeHookToken();
|
|
2835
|
+
}
|
|
2836
|
+
catch {
|
|
2837
|
+
// best effort
|
|
2838
|
+
}
|
|
2839
|
+
const spawned = spawnCodexSliceWorker({
|
|
2840
|
+
runId: sliceRunId,
|
|
2841
|
+
prompt,
|
|
2842
|
+
cwd: workerCwd,
|
|
2843
|
+
logPath,
|
|
2844
|
+
outputPath,
|
|
2845
|
+
outputSchemaPath: schemaPath,
|
|
2846
|
+
env: {
|
|
2847
|
+
ORGX_SOURCE_CLIENT: executorSourceClient,
|
|
2848
|
+
ORGX_RUN_ID: sliceRunId,
|
|
2849
|
+
ORGX_CORRELATION_ID: sliceRunId,
|
|
2850
|
+
ORGX_INITIATIVE_ID: run.initiativeId,
|
|
2851
|
+
ORGX_WORKSTREAM_ID: selectedWorkstreamId,
|
|
2852
|
+
ORGX_WORKSTREAM_TITLE: workstreamTitle ?? undefined,
|
|
2853
|
+
ORGX_TASK_ID: primaryTask.id,
|
|
2854
|
+
ORGX_REQUIRED_SKILLS: executionPolicy.requiredSkills.join(","),
|
|
2855
|
+
ORGX_BEHAVIOR_CONFIG_ID: behaviorConfig.configId ?? undefined,
|
|
2856
|
+
ORGX_BEHAVIOR_CONFIG_VERSION: behaviorConfig.version ?? undefined,
|
|
2857
|
+
ORGX_BEHAVIOR_CONFIG_HASH: behaviorConfig.hash ?? undefined,
|
|
2858
|
+
ORGX_POLICY_SOURCE: behaviorConfig.policySource ?? undefined,
|
|
2859
|
+
ORGX_AUTOMATION_LEVEL: behaviorAutomationLevel,
|
|
2860
|
+
ORGX_BEHAVIOR_CONTEXT: behaviorConfig.context ?? undefined,
|
|
2861
|
+
ORGX_AGENT_ID: sliceAgent.id,
|
|
2862
|
+
ORGX_AGENT_NAME: sliceAgent.name,
|
|
2863
|
+
ORGX_KICKOFF_CONTEXT_HASH: kickoffContextHash ?? undefined,
|
|
2864
|
+
ORGX_OUTPUT_PATH: outputPath,
|
|
2865
|
+
ORGX_RUNTIME_HOOK_URL: runtimeHookUrl ?? undefined,
|
|
2866
|
+
ORGX_HOOK_TOKEN: runtimeHookToken ?? undefined,
|
|
2867
|
+
},
|
|
2868
|
+
});
|
|
2869
|
+
const slice = {
|
|
2870
|
+
runId: sliceRunId,
|
|
2871
|
+
initiativeId: run.initiativeId,
|
|
2872
|
+
initiativeTitle: initiativeTitle ?? null,
|
|
2873
|
+
workstreamId: selectedWorkstreamId,
|
|
2874
|
+
workstreamTitle,
|
|
2875
|
+
agentId: sliceAgent.id,
|
|
2876
|
+
agentName: sliceAgent.name,
|
|
2877
|
+
domain: executionPolicy.domain,
|
|
2878
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
2879
|
+
behaviorConfigId: behaviorConfig.configId,
|
|
2880
|
+
behaviorConfigVersion: behaviorConfig.version,
|
|
2881
|
+
behaviorConfigHash: behaviorConfig.hash,
|
|
2882
|
+
behaviorPolicySource: behaviorConfig.policySource,
|
|
2883
|
+
behaviorAutomationLevel,
|
|
2884
|
+
sourceClient: executorSourceClient,
|
|
2885
|
+
pid: spawned.pid,
|
|
2886
|
+
status: "running",
|
|
2887
|
+
startedAt: now,
|
|
2888
|
+
finishedAt: null,
|
|
2889
|
+
updatedAt: now,
|
|
2890
|
+
tokenEstimate: tokenEstimate > 0 ? tokenEstimate : null,
|
|
2891
|
+
outputPath,
|
|
2892
|
+
logPath,
|
|
2893
|
+
taskIds: cappedSliceTaskNodes.map((t) => t.id),
|
|
2894
|
+
milestoneIds,
|
|
2895
|
+
scope: run.scope,
|
|
2896
|
+
scopeMilestoneIds: scopeMilestoneIds,
|
|
2897
|
+
lastError: null,
|
|
2898
|
+
isMockWorker: workerKind === "mock",
|
|
2899
|
+
};
|
|
2900
|
+
autoContinueSliceRuns.set(sliceRunId, slice);
|
|
2901
|
+
try {
|
|
2902
|
+
writeRuntimeEvent({
|
|
2903
|
+
sourceClient: executorSourceClient,
|
|
2904
|
+
event: "session_start",
|
|
2905
|
+
runId: sliceRunId,
|
|
2906
|
+
initiativeId: run.initiativeId,
|
|
2907
|
+
workstreamId: selectedWorkstreamId,
|
|
2908
|
+
taskId: primaryTask.id,
|
|
2909
|
+
agentId: slice.agentId,
|
|
2910
|
+
agentName: sliceAgent.name,
|
|
2911
|
+
phase: "execution",
|
|
2912
|
+
message: `Autopilot slice started: ${workstreamTitle ?? selectedWorkstreamId}`,
|
|
2913
|
+
metadata: {
|
|
2914
|
+
event: "autopilot_slice_started",
|
|
2915
|
+
initiative_id: run.initiativeId,
|
|
2916
|
+
run_id: sliceRunId,
|
|
2917
|
+
slice_run_id: sliceRunId,
|
|
2918
|
+
workstream_id: selectedWorkstreamId,
|
|
2919
|
+
correlation_id: sliceRunId,
|
|
2920
|
+
requested_by_agent_id: run.agentId,
|
|
2921
|
+
requested_by_agent_name: run.agentName,
|
|
2922
|
+
domain: executionPolicy.domain,
|
|
2923
|
+
required_skills: executionPolicy.requiredSkills,
|
|
2924
|
+
behavior_config_id: behaviorConfig.configId,
|
|
2925
|
+
behavior_config_version: behaviorConfig.version,
|
|
2926
|
+
behavior_config_hash: behaviorConfig.hash,
|
|
2927
|
+
policy_source: behaviorConfig.policySource,
|
|
2928
|
+
behavior_automation_level: behaviorAutomationLevel,
|
|
2929
|
+
task_ids: slice.taskIds,
|
|
2930
|
+
initiative_title: initiativeTitle ?? null,
|
|
2931
|
+
workstream_title: workstreamTitle ?? null,
|
|
2932
|
+
scope: slice.scope,
|
|
2933
|
+
scope_milestone_ids: slice.scopeMilestoneIds,
|
|
2934
|
+
log_path: logPath,
|
|
2935
|
+
output_path: outputPath,
|
|
2936
|
+
...mockMeta(slice),
|
|
2937
|
+
},
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2940
|
+
catch {
|
|
2941
|
+
// best effort
|
|
2942
|
+
}
|
|
2943
|
+
autoContinueSliceLastHeartbeatMs.set(sliceRunId, Date.now());
|
|
2944
|
+
await emitActivitySafe({
|
|
2945
|
+
initiativeId: run.initiativeId,
|
|
2946
|
+
runId: sliceRunId,
|
|
2947
|
+
correlationId: sliceRunId,
|
|
2948
|
+
progressPct: 10,
|
|
2949
|
+
nextStep: `Worker ${sliceAgent.name} is executing ${workstreamTitle ?? selectedWorkstreamId}.`,
|
|
2950
|
+
phase: "execution",
|
|
2951
|
+
level: "info",
|
|
2952
|
+
message: `Autopilot dispatched slice for ${workstreamTitle ?? selectedWorkstreamId}.`,
|
|
2953
|
+
metadata: {
|
|
2954
|
+
...buildSliceEnrichment({
|
|
2955
|
+
run,
|
|
2956
|
+
slice,
|
|
2957
|
+
taskId: primaryTask.id,
|
|
2958
|
+
taskTitle: primaryTask.title ?? null,
|
|
2959
|
+
workstreamId: selectedWorkstreamId,
|
|
2960
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
2961
|
+
domain: executionPolicy.domain,
|
|
2962
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
2963
|
+
event: "autopilot_slice_dispatched",
|
|
2964
|
+
}),
|
|
2965
|
+
behavior_config_id: behaviorConfig.configId,
|
|
2966
|
+
behavior_config_version: behaviorConfig.version,
|
|
2967
|
+
behavior_config_hash: behaviorConfig.hash,
|
|
2968
|
+
policy_source: behaviorConfig.policySource,
|
|
2969
|
+
behavior_automation_level: behaviorAutomationLevel,
|
|
2970
|
+
initiative_title: initiativeTitle ?? null,
|
|
2971
|
+
scope: slice.scope,
|
|
2972
|
+
scope_milestone_ids: slice.scopeMilestoneIds,
|
|
2973
|
+
log_path: logPath,
|
|
2974
|
+
output_path: outputPath,
|
|
2975
|
+
...mockMeta(slice),
|
|
2976
|
+
},
|
|
2977
|
+
});
|
|
2978
|
+
upsertAgentContext({
|
|
2979
|
+
agentId: slice.agentId,
|
|
2980
|
+
initiativeId: run.initiativeId,
|
|
2981
|
+
initiativeTitle: initiativeTitle ?? null,
|
|
2982
|
+
workstreamId: selectedWorkstreamId,
|
|
2983
|
+
taskId: primaryTask.id,
|
|
2984
|
+
});
|
|
2985
|
+
upsertRunContext({
|
|
2986
|
+
runId: sliceRunId,
|
|
2987
|
+
agentId: slice.agentId,
|
|
2988
|
+
initiativeId: run.initiativeId,
|
|
2989
|
+
initiativeTitle: initiativeTitle ?? null,
|
|
2990
|
+
workstreamId: selectedWorkstreamId,
|
|
2991
|
+
taskId: primaryTask.id,
|
|
2992
|
+
});
|
|
2993
|
+
run.lastTaskId = primaryTask.id;
|
|
2994
|
+
run.lastRunId = sliceRunId;
|
|
2995
|
+
run.activeSliceRunIds = dedupeStrings([
|
|
2996
|
+
...run.activeSliceRunIds,
|
|
2997
|
+
sliceRunId,
|
|
2998
|
+
]);
|
|
2999
|
+
run.activeTaskIds = dedupeStrings([...run.activeTaskIds, ...slice.taskIds]);
|
|
3000
|
+
setLaneState(run, {
|
|
3001
|
+
workstreamId: selectedWorkstreamId,
|
|
3002
|
+
state: "running",
|
|
3003
|
+
activeRunId: sliceRunId,
|
|
3004
|
+
activeTaskIds: slice.taskIds,
|
|
3005
|
+
blockedReason: null,
|
|
3006
|
+
waitingOnWorkstreamIds: [],
|
|
3007
|
+
retryAt: null,
|
|
3008
|
+
});
|
|
3009
|
+
run.activeTaskTokenEstimate = tokenEstimate > 0 ? tokenEstimate : null;
|
|
3010
|
+
syncLegacyRunPointers(run);
|
|
3011
|
+
// Clear stale errors when a new slice dispatches successfully.
|
|
3012
|
+
run.lastError = null;
|
|
3013
|
+
run.updatedAt = now;
|
|
3014
|
+
try {
|
|
3015
|
+
await client.updateEntity("initiative", run.initiativeId, { status: "active" });
|
|
3016
|
+
}
|
|
3017
|
+
catch {
|
|
3018
|
+
// best effort
|
|
3019
|
+
}
|
|
3020
|
+
try {
|
|
3021
|
+
await updateInitiativeAutoContinueState({
|
|
3022
|
+
initiativeId: run.initiativeId,
|
|
3023
|
+
run,
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
catch {
|
|
3027
|
+
// best effort
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
async function tickAllAutoContinue() {
|
|
3031
|
+
if (autoContinueTickInFlight) {
|
|
3032
|
+
// Wait for the in-flight tick to finish instead of silently dropping.
|
|
3033
|
+
await autoContinueTickInFlight.catch(() => { });
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
const work = (async () => {
|
|
3037
|
+
for (const run of autoContinueRuns.values()) {
|
|
3038
|
+
try {
|
|
3039
|
+
await tickAutoContinueRun(run);
|
|
3040
|
+
}
|
|
3041
|
+
catch (err) {
|
|
3042
|
+
// Never let one loop crash the whole handler.
|
|
3043
|
+
run.lastError = `[mission-control.auto-continue.engine.tick-all] ${safeErrorMessage(err)}`;
|
|
3044
|
+
run.updatedAt = new Date().toISOString();
|
|
3045
|
+
await stopAutoContinueRun({ run, reason: "error", error: run.lastError });
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
})();
|
|
3049
|
+
autoContinueTickInFlight = work;
|
|
3050
|
+
try {
|
|
3051
|
+
await work;
|
|
3052
|
+
}
|
|
3053
|
+
finally {
|
|
3054
|
+
autoContinueTickInFlight = null;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
function isInitiativeActiveStatus(status) {
|
|
3058
|
+
const normalized = (status ?? "").trim().toLowerCase();
|
|
3059
|
+
if (!normalized)
|
|
3060
|
+
return false;
|
|
3061
|
+
return !(normalized === "completed" ||
|
|
3062
|
+
normalized === "done" ||
|
|
3063
|
+
normalized === "archived" ||
|
|
3064
|
+
normalized === "deleted" ||
|
|
3065
|
+
normalized === "cancelled");
|
|
3066
|
+
}
|
|
3067
|
+
function runningAutoContinueForWorkstream(initiativeId, workstreamId) {
|
|
3068
|
+
const run = autoContinueRuns.get(initiativeId) ?? null;
|
|
3069
|
+
if (!run)
|
|
3070
|
+
return null;
|
|
3071
|
+
ensureRunInternals(run);
|
|
3072
|
+
if (run.status !== "running" && run.status !== "stopping")
|
|
3073
|
+
return null;
|
|
3074
|
+
if (Array.isArray(run.allowedWorkstreamIds) &&
|
|
3075
|
+
run.allowedWorkstreamIds.length > 0 &&
|
|
3076
|
+
!run.allowedWorkstreamIds.includes(workstreamId)) {
|
|
3077
|
+
return null;
|
|
3078
|
+
}
|
|
3079
|
+
const lane = run.laneByWorkstreamId[workstreamId] ?? null;
|
|
3080
|
+
if (lane &&
|
|
3081
|
+
(lane.state === "running" ||
|
|
3082
|
+
lane.state === "blocked" ||
|
|
3083
|
+
lane.state === "waiting_dependency" ||
|
|
3084
|
+
lane.state === "rate_limited")) {
|
|
3085
|
+
return run;
|
|
3086
|
+
}
|
|
3087
|
+
if (Array.isArray(run.allowedWorkstreamIds) &&
|
|
3088
|
+
run.allowedWorkstreamIds.length > 0 &&
|
|
3089
|
+
run.allowedWorkstreamIds.includes(workstreamId) &&
|
|
3090
|
+
(run.status === "running" || run.status === "stopping")) {
|
|
3091
|
+
return run;
|
|
3092
|
+
}
|
|
3093
|
+
return null;
|
|
3094
|
+
}
|
|
3095
|
+
function getAutoContinueLaneForWorkstream(initiativeId, workstreamId) {
|
|
3096
|
+
const run = autoContinueRuns.get(initiativeId) ?? null;
|
|
3097
|
+
if (!run)
|
|
3098
|
+
return null;
|
|
3099
|
+
ensureRunInternals(run);
|
|
3100
|
+
return run.laneByWorkstreamId[workstreamId] ?? null;
|
|
3101
|
+
}
|
|
3102
|
+
async function scheduleAutoFixForWorkstream(input) {
|
|
3103
|
+
const initiativeId = input.initiativeId.trim();
|
|
3104
|
+
const workstreamId = input.workstreamId.trim();
|
|
3105
|
+
if (!initiativeId || !workstreamId) {
|
|
3106
|
+
throw new Error("initiativeId and workstreamId are required");
|
|
3107
|
+
}
|
|
3108
|
+
const runId = (input.runId ?? "").trim() || null;
|
|
3109
|
+
const sourceEvent = (input.event ?? "").trim() || null;
|
|
3110
|
+
const requestedByAgentId = (input.requestedByAgentId ?? "").trim() || null;
|
|
3111
|
+
const requestedByAgentName = (input.requestedByAgentName ?? "").trim() || null;
|
|
3112
|
+
const providedGraceMs = typeof input.graceMs === "number" && Number.isFinite(input.graceMs)
|
|
3113
|
+
? Math.floor(input.graceMs)
|
|
3114
|
+
: null;
|
|
3115
|
+
const graceMs = Math.max(1_000, Math.min(120_000, providedGraceMs ?? AUTO_FIX_DEFAULT_GRACE_MS));
|
|
3116
|
+
const key = `${initiativeId}:${workstreamId}`;
|
|
3117
|
+
const existing = autoFixByScope.get(key);
|
|
3118
|
+
if (existing?.timer)
|
|
3119
|
+
clearTimeout(existing.timer);
|
|
3120
|
+
const scheduledAt = new Date().toISOString();
|
|
3121
|
+
const dueAt = new Date(Date.now() + graceMs).toISOString();
|
|
3122
|
+
const requestId = randomUUID();
|
|
3123
|
+
const resolveAutoFixRunContext = () => {
|
|
3124
|
+
const activeRun = autoContinueRuns.get(initiativeId) ?? null;
|
|
3125
|
+
return {
|
|
3126
|
+
initiativeId,
|
|
3127
|
+
agentId: activeRun?.agentId ?? requestedByAgentId ?? "main",
|
|
3128
|
+
agentName: activeRun?.agentName ?? requestedByAgentName ?? null,
|
|
3129
|
+
scope: activeRun?.scope ?? "task",
|
|
3130
|
+
};
|
|
3131
|
+
};
|
|
3132
|
+
const emitSkip = async (reason, details) => {
|
|
3133
|
+
await emitActivitySafe({
|
|
3134
|
+
initiativeId,
|
|
3135
|
+
runId: runId ?? undefined,
|
|
3136
|
+
correlationId: runId ?? undefined,
|
|
3137
|
+
phase: "review",
|
|
3138
|
+
level: reason === "error" ? "error" : "warn",
|
|
3139
|
+
message: reason === "paused_by_user"
|
|
3140
|
+
? `Auto-fix skipped for ${workstreamId}: paused during grace window.`
|
|
3141
|
+
: reason === "already_running"
|
|
3142
|
+
? `Auto-fix skipped for ${workstreamId}: workstream already running.`
|
|
3143
|
+
: reason === "missing_workstream"
|
|
3144
|
+
? `Auto-fix skipped for ${workstreamId}: workstream data unavailable.`
|
|
3145
|
+
: reason === "missing_scope"
|
|
3146
|
+
? `Auto-fix skipped: scope metadata was incomplete.`
|
|
3147
|
+
: `Auto-fix failed for ${workstreamId}.`,
|
|
3148
|
+
metadata: {
|
|
3149
|
+
...buildSliceEnrichment({
|
|
3150
|
+
run: resolveAutoFixRunContext(),
|
|
3151
|
+
workstreamId,
|
|
3152
|
+
event: "autopilot_autofix_skipped",
|
|
3153
|
+
actionType: "auto_fix",
|
|
3154
|
+
}),
|
|
3155
|
+
reason,
|
|
3156
|
+
run_id: runId,
|
|
3157
|
+
source_event: sourceEvent,
|
|
3158
|
+
grace_ms: graceMs,
|
|
3159
|
+
request_id: requestId,
|
|
3160
|
+
scheduled_at: scheduledAt,
|
|
3161
|
+
due_at: dueAt,
|
|
3162
|
+
...(details ?? {}),
|
|
3163
|
+
},
|
|
3164
|
+
});
|
|
3165
|
+
};
|
|
3166
|
+
const executeScheduledAutoFix = async () => {
|
|
3167
|
+
const pending = autoFixByScope.get(key);
|
|
3168
|
+
if (!pending || pending.requestId !== requestId)
|
|
3169
|
+
return;
|
|
3170
|
+
autoFixByScope.delete(key);
|
|
3171
|
+
const existingRun = autoContinueRuns.get(initiativeId) ?? null;
|
|
3172
|
+
if (existingRun &&
|
|
3173
|
+
(existingRun.stopRequested ||
|
|
3174
|
+
existingRun.status === "stopping" ||
|
|
3175
|
+
existingRun.stopReason === "stopped")) {
|
|
3176
|
+
await emitSkip("paused_by_user");
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
if (existingRun &&
|
|
3180
|
+
(existingRun.status === "running" || existingRun.status === "stopping") &&
|
|
3181
|
+
listActiveSliceRunIds(existingRun).length > 0) {
|
|
3182
|
+
const activeRunIds = listActiveSliceRunIds(existingRun);
|
|
3183
|
+
await emitSkip("already_running", {
|
|
3184
|
+
active_run_id: activeRunIds[0] ?? null,
|
|
3185
|
+
active_run_ids: activeRunIds,
|
|
3186
|
+
run_status: existingRun.status,
|
|
3187
|
+
});
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
let optionalDecisionsApproved = 0;
|
|
3191
|
+
if (decisionAutoResolveGuardedEnabled) {
|
|
3192
|
+
try {
|
|
3193
|
+
const decisionResult = await client.listEntities("decision", {
|
|
3194
|
+
initiative_id: initiativeId,
|
|
3195
|
+
status: "pending",
|
|
3196
|
+
limit: 500,
|
|
3197
|
+
});
|
|
3198
|
+
const decisionRows = Array.isArray(decisionResult?.data) ? decisionResult.data : [];
|
|
3199
|
+
for (const row of decisionRows) {
|
|
3200
|
+
if (!row || typeof row !== "object")
|
|
3201
|
+
continue;
|
|
3202
|
+
const record = row;
|
|
3203
|
+
const decisionId = pickString(record, ["id"])?.trim() ?? "";
|
|
3204
|
+
if (!decisionId)
|
|
3205
|
+
continue;
|
|
3206
|
+
if (!isPendingDecisionStatus(record.status ?? record.decision_status))
|
|
3207
|
+
continue;
|
|
3208
|
+
if (!decisionMatchesWorkstream(record, workstreamId, runId))
|
|
3209
|
+
continue;
|
|
3210
|
+
if (decisionIsBlocking(record))
|
|
3211
|
+
continue;
|
|
3212
|
+
const autoApprovalNote = "Auto-approved by OrgX auto-fix (non-blocking follow-up decision).";
|
|
3213
|
+
if (typeof client.decideDecision === "function") {
|
|
3214
|
+
await client.decideDecision(decisionId, "approve", { note: autoApprovalNote });
|
|
3215
|
+
}
|
|
3216
|
+
else {
|
|
3217
|
+
await client.updateEntity("decision", decisionId, {
|
|
3218
|
+
status: "approved",
|
|
3219
|
+
resolution_summary: autoApprovalNote,
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
3222
|
+
optionalDecisionsApproved += 1;
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
catch {
|
|
3226
|
+
// best effort
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
let resetTaskCount = 0;
|
|
3230
|
+
try {
|
|
3231
|
+
const taskResult = await client.listEntities("task", {
|
|
3232
|
+
initiative_id: initiativeId,
|
|
3233
|
+
workstream_id: workstreamId,
|
|
3234
|
+
limit: 100,
|
|
3235
|
+
});
|
|
3236
|
+
const taskRows = Array.isArray(taskResult?.data) ? taskResult.data : [];
|
|
3237
|
+
if (taskRows.length === 0) {
|
|
3238
|
+
await emitSkip("missing_workstream");
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
for (const row of taskRows) {
|
|
3242
|
+
if (!row || typeof row !== "object")
|
|
3243
|
+
continue;
|
|
3244
|
+
const record = row;
|
|
3245
|
+
const taskId = pickString(record, ["id"])?.trim() ?? "";
|
|
3246
|
+
if (!taskId)
|
|
3247
|
+
continue;
|
|
3248
|
+
const status = normalizeStatusValue(record.status);
|
|
3249
|
+
if (!status || status === "todo" || status === "done" || status === "completed") {
|
|
3250
|
+
continue;
|
|
3251
|
+
}
|
|
3252
|
+
const shouldReset = status === "in_progress" ||
|
|
3253
|
+
status === "inprogress" ||
|
|
3254
|
+
status === "active" ||
|
|
3255
|
+
status === "running" ||
|
|
3256
|
+
status === "working" ||
|
|
3257
|
+
status === "planning" ||
|
|
3258
|
+
status === "dispatching" ||
|
|
3259
|
+
status === "pending" ||
|
|
3260
|
+
status === "blocked" ||
|
|
3261
|
+
status === "stalled" ||
|
|
3262
|
+
status === "failed" ||
|
|
3263
|
+
status === "error";
|
|
3264
|
+
if (!shouldReset)
|
|
3265
|
+
continue;
|
|
3266
|
+
await client.updateEntity("task", taskId, { status: "todo" });
|
|
3267
|
+
resetTaskCount += 1;
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
catch {
|
|
3271
|
+
// best effort
|
|
3272
|
+
}
|
|
3273
|
+
const latestRun = autoContinueRuns.get(initiativeId) ?? null;
|
|
3274
|
+
const dispatchAgentId = latestRun?.agentId ??
|
|
3275
|
+
requestedByAgentId ??
|
|
3276
|
+
"main";
|
|
3277
|
+
const dispatchAgentName = latestRun?.agentName ??
|
|
3278
|
+
requestedByAgentName ??
|
|
3279
|
+
null;
|
|
3280
|
+
const dispatchRun = await startAutoContinueRun({
|
|
3281
|
+
initiativeId,
|
|
3282
|
+
agentId: dispatchAgentId,
|
|
3283
|
+
agentName: dispatchAgentName,
|
|
3284
|
+
// Auto-fix retries should follow current defaults unless an operator explicitly
|
|
3285
|
+
// starts a run with a budget override.
|
|
3286
|
+
tokenBudget: null,
|
|
3287
|
+
includeVerification: latestRun?.includeVerification ?? false,
|
|
3288
|
+
allowedWorkstreamIds: [workstreamId],
|
|
3289
|
+
maxParallelSlices: 1,
|
|
3290
|
+
parallelMode: latestRun?.parallelMode ?? "iwmt",
|
|
3291
|
+
stopAfterSlice: true,
|
|
3292
|
+
ignoreSpawnGuardRateLimit: latestRun?.ignoreSpawnGuardRateLimit ?? false,
|
|
3293
|
+
});
|
|
3294
|
+
await tickAutoContinueRun(dispatchRun);
|
|
3295
|
+
await emitActivitySafe({
|
|
3296
|
+
initiativeId,
|
|
3297
|
+
runId: dispatchRun.activeRunId ?? runId ?? undefined,
|
|
3298
|
+
correlationId: dispatchRun.activeRunId ?? runId ?? undefined,
|
|
3299
|
+
phase: "execution",
|
|
3300
|
+
level: "info",
|
|
3301
|
+
message: `Auto-fix dispatched for ${workstreamId}.`,
|
|
3302
|
+
metadata: {
|
|
3303
|
+
...buildSliceEnrichment({
|
|
3304
|
+
run: {
|
|
3305
|
+
initiativeId,
|
|
3306
|
+
agentId: dispatchAgentId,
|
|
3307
|
+
agentName: dispatchAgentName,
|
|
3308
|
+
scope: dispatchRun.scope,
|
|
3309
|
+
},
|
|
3310
|
+
workstreamId,
|
|
3311
|
+
event: "autopilot_autofix_executed",
|
|
3312
|
+
actionType: "auto_fix",
|
|
3313
|
+
}),
|
|
3314
|
+
source_event: sourceEvent,
|
|
3315
|
+
run_id: runId,
|
|
3316
|
+
grace_ms: graceMs,
|
|
3317
|
+
request_id: requestId,
|
|
3318
|
+
scheduled_at: scheduledAt,
|
|
3319
|
+
due_at: dueAt,
|
|
3320
|
+
optional_decisions_auto_approved: optionalDecisionsApproved,
|
|
3321
|
+
reset_task_count: resetTaskCount,
|
|
3322
|
+
dispatched_run_id: dispatchRun.activeRunId,
|
|
3323
|
+
dispatch_agent_id: dispatchAgentId,
|
|
3324
|
+
dispatch_agent_name: dispatchAgentName,
|
|
3325
|
+
},
|
|
3326
|
+
});
|
|
3327
|
+
};
|
|
3328
|
+
const pending = {
|
|
3329
|
+
requestId,
|
|
3330
|
+
key,
|
|
3331
|
+
initiativeId,
|
|
3332
|
+
workstreamId,
|
|
3333
|
+
runId,
|
|
3334
|
+
sourceEvent,
|
|
3335
|
+
requestedByAgentId,
|
|
3336
|
+
requestedByAgentName,
|
|
3337
|
+
graceMs,
|
|
3338
|
+
scheduledAt,
|
|
3339
|
+
dueAt,
|
|
3340
|
+
timer: null,
|
|
3341
|
+
};
|
|
3342
|
+
const timer = setTimeout(() => {
|
|
3343
|
+
void executeScheduledAutoFix().catch(async (err) => {
|
|
3344
|
+
autoFixByScope.delete(key);
|
|
3345
|
+
await emitSkip("error", {
|
|
3346
|
+
error: safeErrorMessage(err),
|
|
3347
|
+
});
|
|
3348
|
+
});
|
|
3349
|
+
}, graceMs);
|
|
3350
|
+
pending.timer = timer;
|
|
3351
|
+
autoFixByScope.set(key, pending);
|
|
3352
|
+
await emitActivitySafe({
|
|
3353
|
+
initiativeId,
|
|
3354
|
+
runId: runId ?? undefined,
|
|
3355
|
+
correlationId: runId ?? undefined,
|
|
3356
|
+
phase: "review",
|
|
3357
|
+
level: "info",
|
|
3358
|
+
message: `Auto-fix scheduled for ${workstreamId} in ${Math.round(graceMs / 1000)}s.`,
|
|
3359
|
+
metadata: {
|
|
3360
|
+
...buildSliceEnrichment({
|
|
3361
|
+
run: resolveAutoFixRunContext(),
|
|
3362
|
+
workstreamId,
|
|
3363
|
+
event: "autopilot_autofix_scheduled",
|
|
3364
|
+
actionType: "auto_fix",
|
|
3365
|
+
}),
|
|
3366
|
+
source_event: sourceEvent,
|
|
3367
|
+
run_id: runId,
|
|
3368
|
+
grace_ms: graceMs,
|
|
3369
|
+
request_id: requestId,
|
|
3370
|
+
scheduled_at: scheduledAt,
|
|
3371
|
+
due_at: dueAt,
|
|
3372
|
+
},
|
|
3373
|
+
});
|
|
3374
|
+
return {
|
|
3375
|
+
requestId,
|
|
3376
|
+
initiativeId,
|
|
3377
|
+
workstreamId,
|
|
3378
|
+
runId,
|
|
3379
|
+
sourceEvent,
|
|
3380
|
+
graceMs,
|
|
3381
|
+
scheduledAt,
|
|
3382
|
+
dueAt,
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
async function startAutoContinueRun(input) {
|
|
3386
|
+
const now = new Date().toISOString();
|
|
3387
|
+
const existing = autoContinueRuns.get(input.initiativeId) ?? null;
|
|
3388
|
+
const existingIsLive = existing?.status === "running" || existing?.status === "stopping";
|
|
3389
|
+
const run = existing ??
|
|
3390
|
+
{
|
|
3391
|
+
initiativeId: input.initiativeId,
|
|
3392
|
+
agentId: input.agentId,
|
|
3393
|
+
agentName: input.agentName ?? null,
|
|
3394
|
+
includeVerification: false,
|
|
3395
|
+
allowedWorkstreamIds: null,
|
|
3396
|
+
stopAfterSlice: false,
|
|
3397
|
+
ignoreSpawnGuardRateLimit: false,
|
|
3398
|
+
maxParallelSlices: AUTO_CONTINUE_MAX_PARALLEL_DEFAULT,
|
|
3399
|
+
parallelMode: "iwmt",
|
|
3400
|
+
scope: "task",
|
|
3401
|
+
tokenBudget: defaultAutoContinueTokenBudget(),
|
|
3402
|
+
tokensUsed: 0,
|
|
3403
|
+
status: "running",
|
|
3404
|
+
stopReason: null,
|
|
3405
|
+
stopRequested: false,
|
|
3406
|
+
startedAt: now,
|
|
3407
|
+
stoppedAt: null,
|
|
3408
|
+
updatedAt: now,
|
|
3409
|
+
lastError: null,
|
|
3410
|
+
lastTaskId: null,
|
|
3411
|
+
lastRunId: null,
|
|
3412
|
+
activeSliceRunIds: [],
|
|
3413
|
+
activeTaskIds: [],
|
|
3414
|
+
laneByWorkstreamId: {},
|
|
3415
|
+
blockedWorkstreamIds: [],
|
|
3416
|
+
activeTaskId: null,
|
|
3417
|
+
activeRunId: null,
|
|
3418
|
+
activeTaskTokenEstimate: null,
|
|
3419
|
+
};
|
|
3420
|
+
ensureRunInternals(run);
|
|
3421
|
+
run.agentId = input.agentId;
|
|
3422
|
+
run.agentName =
|
|
3423
|
+
typeof input.agentName === "string" && input.agentName.trim().length > 0
|
|
3424
|
+
? input.agentName.trim()
|
|
3425
|
+
: null;
|
|
3426
|
+
run.includeVerification = input.includeVerification;
|
|
3427
|
+
run.allowedWorkstreamIds = input.allowedWorkstreamIds;
|
|
3428
|
+
run.maxParallelSlices = normalizeMaxParallelSlices(input.maxParallelSlices, run.maxParallelSlices || AUTO_CONTINUE_MAX_PARALLEL_DEFAULT);
|
|
3429
|
+
run.parallelMode = normalizeParallelMode(input.parallelMode ?? run.parallelMode);
|
|
3430
|
+
run.stopAfterSlice = Boolean(input.stopAfterSlice);
|
|
3431
|
+
run.ignoreSpawnGuardRateLimit = Boolean(input.ignoreSpawnGuardRateLimit);
|
|
3432
|
+
run.scope = input.scope ?? "task";
|
|
3433
|
+
const hasExplicitTokenBudgetInput = input.tokenBudget !== null &&
|
|
3434
|
+
input.tokenBudget !== undefined &&
|
|
3435
|
+
!(typeof input.tokenBudget === "string" && input.tokenBudget.trim().length === 0);
|
|
3436
|
+
if (hasExplicitTokenBudgetInput) {
|
|
3437
|
+
run.tokenBudget = normalizeTokenBudget(input.tokenBudget, defaultAutoContinueTokenBudget());
|
|
3438
|
+
}
|
|
3439
|
+
else {
|
|
3440
|
+
// On fresh restarts, reset to current defaults instead of inheriting stale prior limits.
|
|
3441
|
+
// While a run is live, keep its active budget unless explicitly overridden.
|
|
3442
|
+
run.tokenBudget = existingIsLive
|
|
3443
|
+
? normalizeTokenBudget(run.tokenBudget, defaultAutoContinueTokenBudget())
|
|
3444
|
+
: defaultAutoContinueTokenBudget();
|
|
3445
|
+
}
|
|
3446
|
+
run.status = "running";
|
|
3447
|
+
run.stopReason = null;
|
|
3448
|
+
run.stopRequested = false;
|
|
3449
|
+
run.stoppedAt = null;
|
|
3450
|
+
run.updatedAt = now;
|
|
3451
|
+
run.lastError = null;
|
|
3452
|
+
const forceFreshRun = Boolean(input.stopAfterSlice);
|
|
3453
|
+
if (!existingIsLive || forceFreshRun) {
|
|
3454
|
+
run.tokensUsed = 0;
|
|
3455
|
+
run.startedAt = now;
|
|
3456
|
+
run.lastTaskId = null;
|
|
3457
|
+
run.lastRunId = null;
|
|
3458
|
+
run.activeSliceRunIds = [];
|
|
3459
|
+
run.activeTaskIds = [];
|
|
3460
|
+
run.blockedWorkstreamIds = [];
|
|
3461
|
+
run.laneByWorkstreamId = {};
|
|
3462
|
+
run.activeTaskId = null;
|
|
3463
|
+
run.activeRunId = null;
|
|
3464
|
+
run.activeTaskTokenEstimate = null;
|
|
3465
|
+
}
|
|
3466
|
+
syncLegacyRunPointers(run);
|
|
3467
|
+
autoContinueRuns.set(input.initiativeId, run);
|
|
3468
|
+
void client
|
|
3469
|
+
.updateEntity("initiative", input.initiativeId, { status: "active" })
|
|
3470
|
+
.catch(() => {
|
|
3471
|
+
// best effort
|
|
3472
|
+
});
|
|
3473
|
+
void updateInitiativeAutoContinueState({
|
|
3474
|
+
initiativeId: input.initiativeId,
|
|
3475
|
+
run,
|
|
3476
|
+
}).catch(() => {
|
|
3477
|
+
// best effort
|
|
3478
|
+
});
|
|
3479
|
+
if (!existingIsLive || forceFreshRun) {
|
|
3480
|
+
const startRunContext = {
|
|
3481
|
+
initiativeId: run.initiativeId,
|
|
3482
|
+
agentId: run.agentId,
|
|
3483
|
+
agentName: run.agentName,
|
|
3484
|
+
scope: run.scope,
|
|
3485
|
+
};
|
|
3486
|
+
try {
|
|
3487
|
+
await emitActivitySafe({
|
|
3488
|
+
initiativeId: input.initiativeId,
|
|
3489
|
+
runId: run.lastRunId ?? undefined,
|
|
3490
|
+
correlationId: run.lastRunId ?? undefined,
|
|
3491
|
+
phase: "intent",
|
|
3492
|
+
level: "info",
|
|
3493
|
+
message: "Autopilot enabled. Dispatch will continue from Next Up automatically.",
|
|
3494
|
+
metadata: {
|
|
3495
|
+
...buildSliceEnrichment({
|
|
3496
|
+
run: startRunContext,
|
|
3497
|
+
event: "auto_continue_started",
|
|
3498
|
+
}),
|
|
3499
|
+
token_budget: run.tokenBudget,
|
|
3500
|
+
include_verification: run.includeVerification,
|
|
3501
|
+
allowed_workstream_ids: run.allowedWorkstreamIds,
|
|
3502
|
+
max_parallel_slices: run.maxParallelSlices,
|
|
3503
|
+
parallel_mode: run.parallelMode,
|
|
3504
|
+
scope: run.scope,
|
|
3505
|
+
ignore_spawn_guard_rate_limit: run.ignoreSpawnGuardRateLimit,
|
|
3506
|
+
},
|
|
3507
|
+
nextStep: "Watch Activity for dispatch and slice-complete updates.",
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
catch {
|
|
3511
|
+
// best effort
|
|
3512
|
+
}
|
|
3513
|
+
// Emit transition: idle → running
|
|
3514
|
+
try {
|
|
3515
|
+
await emitActivitySafe({
|
|
3516
|
+
initiativeId: input.initiativeId,
|
|
3517
|
+
runId: run.lastRunId ?? undefined,
|
|
3518
|
+
correlationId: run.lastRunId ?? undefined,
|
|
3519
|
+
phase: "intent",
|
|
3520
|
+
level: "info",
|
|
3521
|
+
message: "Autopilot state: idle → running.",
|
|
3522
|
+
metadata: {
|
|
3523
|
+
...buildSliceEnrichment({
|
|
3524
|
+
run: startRunContext,
|
|
3525
|
+
event: "autopilot_transition",
|
|
3526
|
+
actionType: "run_state_transition",
|
|
3527
|
+
}),
|
|
3528
|
+
old_state: "idle",
|
|
3529
|
+
new_state: "running",
|
|
3530
|
+
reason: "started",
|
|
3531
|
+
workspace_id: run.allowedWorkstreamIds?.[0] ?? null,
|
|
3532
|
+
},
|
|
3533
|
+
});
|
|
3534
|
+
}
|
|
3535
|
+
catch {
|
|
3536
|
+
// best effort
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
return run;
|
|
3540
|
+
}
|
|
3541
|
+
return {
|
|
3542
|
+
autoContinueRuns,
|
|
3543
|
+
autoContinueSliceRuns,
|
|
3544
|
+
localInitiativeStatusOverrides,
|
|
3545
|
+
writeRuntimeEvent,
|
|
3546
|
+
autoContinueTickMs: AUTO_CONTINUE_TICK_MS,
|
|
3547
|
+
defaultAutoContinueTokenBudget,
|
|
3548
|
+
defaultAutoContinueMaxParallelSlices,
|
|
3549
|
+
setLocalInitiativeStatusOverride,
|
|
3550
|
+
clearLocalInitiativeStatusOverride,
|
|
3551
|
+
applyLocalInitiativeOverrides,
|
|
3552
|
+
applyLocalInitiativeOverrideToGraph,
|
|
3553
|
+
updateInitiativeAutoContinueState,
|
|
3554
|
+
stopAutoContinueRun,
|
|
3555
|
+
tickAutoContinueRun,
|
|
3556
|
+
tickAllAutoContinue,
|
|
3557
|
+
isInitiativeActiveStatus,
|
|
3558
|
+
runningAutoContinueForWorkstream,
|
|
3559
|
+
getAutoContinueLaneForWorkstream,
|
|
3560
|
+
scheduleAutoFixForWorkstream,
|
|
3561
|
+
startAutoContinueRun,
|
|
3562
|
+
};
|
|
3563
|
+
}
|