@useorgx/openclaw-plugin 0.4.9 → 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 +34 -8
- package/dist/agent-context-store.js +79 -17
- package/dist/agent-run-store.js +44 -3
- package/dist/agent-suite.d.ts +9 -0
- package/dist/agent-suite.js +149 -9
- 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/chat-store.d.ts +157 -0
- package/dist/chat-store.js +586 -0
- package/dist/cli/orgx.js +11 -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 +159 -0
- package/dist/contracts/shared-types.js +177 -1
- 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 +227 -2
- package/dist/entities/auto-assignment.js +43 -17
- 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/hooks/post-reporting-event.mjs +1 -5
- package/dist/http/helpers/activity-headline.js +13 -132
- package/dist/http/helpers/auto-continue-engine.d.ts +198 -10
- package/dist/http/helpers/auto-continue-engine.js +2531 -186
- package/dist/http/helpers/autopilot-operations.d.ts +19 -0
- package/dist/http/helpers/autopilot-operations.js +182 -31
- package/dist/http/helpers/autopilot-runtime.d.ts +1 -0
- package/dist/http/helpers/autopilot-runtime.js +308 -20
- package/dist/http/helpers/autopilot-slice-utils.d.ts +18 -0
- package/dist/http/helpers/autopilot-slice-utils.js +516 -93
- package/dist/http/helpers/decision-mapper.d.ts +40 -0
- package/dist/http/helpers/decision-mapper.js +223 -7
- package/dist/http/helpers/dispatch-lifecycle.d.ts +19 -2
- package/dist/http/helpers/dispatch-lifecycle.js +242 -37
- package/dist/http/helpers/kickoff-context.js +74 -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 +102 -3
- package/dist/http/helpers/mission-control.js +498 -9
- 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.js +7 -2
- package/dist/http/helpers/workspace-scope.d.ts +15 -0
- package/dist/http/helpers/workspace-scope.js +170 -0
- package/dist/http/index.js +1354 -97
- package/dist/http/routes/agent-suite.d.ts +9 -0
- package/dist/http/routes/agent-suite.js +207 -8
- package/dist/http/routes/agents-catalog.js +64 -19
- package/dist/http/routes/chat.d.ts +19 -0
- package/dist/http/routes/chat.js +522 -0
- package/dist/http/routes/decision-actions.d.ts +8 -1
- package/dist/http/routes/decision-actions.js +42 -5
- 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 +16 -0
- package/dist/http/routes/entities.js +294 -6
- package/dist/http/routes/live-legacy.d.ts +5 -0
- package/dist/http/routes/live-legacy.js +23 -509
- package/dist/http/routes/live-misc.d.ts +12 -0
- package/dist/http/routes/live-misc.js +251 -31
- package/dist/http/routes/live-snapshot.d.ts +48 -2
- package/dist/http/routes/live-snapshot.js +638 -19
- 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 +49 -1
- package/dist/http/routes/mission-control-actions.js +1334 -84
- package/dist/http/routes/mission-control-read.d.ts +48 -3
- package/dist/http/routes/mission-control-read.js +1593 -20
- 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 +5 -2
- package/dist/http/routes/run-control.js +10 -0
- package/dist/http/routes/sentinels-catalog.d.ts +7 -0
- package/dist/http/routes/sentinels-catalog.js +24 -0
- package/dist/http/routes/summary.js +10 -3
- package/dist/http/routes/usage.d.ts +24 -0
- package/dist/http/routes/usage.js +362 -0
- package/dist/http/routes/work-artifacts.js +28 -9
- package/dist/index.js +165 -27
- 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 +89 -7
- 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/services/background.d.ts +11 -0
- package/dist/services/background.js +22 -0
- package/dist/services/experiment-randomization.d.ts +21 -0
- package/dist/services/experiment-randomization.js +63 -0
- package/dist/skill-pack-state.d.ts +36 -5
- package/dist/skill-pack-state.js +273 -29
- package/dist/sync/local-agent-telemetry.d.ts +13 -0
- package/dist/sync/local-agent-telemetry.js +128 -0
- package/dist/sync/outbox-replay.js +131 -24
- 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 +10 -14
- package/dist/tools/core-tools.js +1289 -24
- 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/B5NEElEI.css +0 -1
- package/dashboard/dist/assets/BhapSNAs.js +0 -215
- package/dashboard/dist/assets/iFdvE7lx.js +0 -1
- package/dashboard/dist/assets/jRJsmpYM.js +0 -1
- package/dashboard/dist/assets/sAhvFnpk.js +0 -4
|
@@ -1,26 +1,271 @@
|
|
|
1
1
|
import { randomUUID as randomUuidFn } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readdir, stat, unlink } from "node:fs/promises";
|
|
2
4
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import {
|
|
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";
|
|
5
9
|
import { readOpenClawGatewayPort, readOpenClawSettingsSnapshot, } from "../../openclaw-settings.js";
|
|
6
10
|
import { resolveRuntimeHookToken, } from "../../runtime-instance-store.js";
|
|
7
11
|
import { detectMcpHandshakeFailure, shouldKillWorker } from "../../worker-supervisor.js";
|
|
8
12
|
import { getOrgxPluginConfigDir } from "../../paths.js";
|
|
9
|
-
import { buildMissionControlGraph, DEFAULT_TOKEN_BUDGET_ASSUMPTIONS, dedupeStrings, deriveExecutionPolicy, isDispatchableWorkstreamStatus, isDoneStatus, isTodoStatus, readBudgetEnvNumber, summarizeSpawnGuardBlockReason, } from "./mission-control.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";
|
|
10
14
|
import { createAutopilotRuntime } from "./autopilot-runtime.js";
|
|
11
|
-
import { buildWorkstreamSlicePrompt, createCodexBinResolver, ensureAutopilotSliceSchemaPath, fileUpdatedAtEpochMs, parseSliceResult, readFileTailSafe, readSliceOutputFile, } from "./autopilot-slice-utils.js";
|
|
15
|
+
import { buildScopeDirective, buildSliceOutputInstructions, buildWorkstreamSlicePrompt, createCodexBinResolver, ensureAutopilotSliceSchemaPath, fileUpdatedAtEpochMs, parseSliceResult, readFileTailSafe, readSliceOutputFile, } from "./autopilot-slice-utils.js";
|
|
12
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
|
+
}
|
|
13
29
|
export function createAutoContinueEngine(deps) {
|
|
14
30
|
const { client, safeErrorMessage, pidAlive, stopProcess, resolveOrgxAgentForDomain, checkSpawnGuardSafe, syncParentRollupsForTask, emitActivitySafe, requestDecisionSafe, registerArtifactSafe, applyAgentStatusUpdatesSafe, upsertRuntimeInstanceFromHook, broadcastRuntimeSse, clearSnapshotResponseCache, resolveByokEnvOverrides, } = deps;
|
|
15
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
|
+
};
|
|
16
97
|
const __filename = deps.filename;
|
|
17
98
|
const autoContinueRuns = new Map();
|
|
18
99
|
const localInitiativeStatusOverrides = new Map();
|
|
100
|
+
const localTaskStatusOverrides = new Map();
|
|
101
|
+
const localMilestoneStatusOverrides = new Map();
|
|
19
102
|
let autoContinueTickInFlight = null;
|
|
20
103
|
const AUTO_CONTINUE_TICK_MS = readBudgetEnvNumber("ORGX_AUTO_CONTINUE_TICK_MS", 2_500, {
|
|
21
104
|
min: 250,
|
|
22
105
|
max: 60_000,
|
|
23
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
|
+
};
|
|
24
269
|
const autoContinueSliceRuns = new Map();
|
|
25
270
|
// Keep child handles alive so stdout/stderr capture remains reliable even when the process is detached.
|
|
26
271
|
const autoContinueSliceChildren = new Map();
|
|
@@ -32,7 +277,6 @@ export function createAutoContinueEngine(deps) {
|
|
|
32
277
|
autoContinueSliceChildren.delete(id);
|
|
33
278
|
autoContinueSliceLastHeartbeatMs.delete(id);
|
|
34
279
|
};
|
|
35
|
-
const AUTO_CONTINUE_SLICE_MAX_TASKS = 6;
|
|
36
280
|
const AUTO_CONTINUE_SLICE_TIMEOUT_MS = readBudgetEnvNumber("ORGX_AUTOPILOT_SLICE_TIMEOUT_MS", 55 * 60_000,
|
|
37
281
|
// Keep test runs fast; real-world defaults are still ~1h unless overridden.
|
|
38
282
|
{ min: 250, max: 6 * 60 * 60_000 });
|
|
@@ -42,6 +286,302 @@ export function createAutoContinueEngine(deps) {
|
|
|
42
286
|
const AUTO_CONTINUE_SLICE_HEARTBEAT_MS = 12_000;
|
|
43
287
|
const AUTO_CONTINUE_SLICE_SCHEMA_FILENAME = "autopilot-slice-schema.json";
|
|
44
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
|
+
};
|
|
45
585
|
const setLocalInitiativeStatusOverride = (initiativeId, status) => {
|
|
46
586
|
const normalizedId = initiativeId.trim();
|
|
47
587
|
if (!normalizedId)
|
|
@@ -104,27 +644,52 @@ export function createAutoContinueEngine(deps) {
|
|
|
104
644
|
: node),
|
|
105
645
|
};
|
|
106
646
|
};
|
|
107
|
-
function
|
|
647
|
+
function parseTokenBudget(value) {
|
|
108
648
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
649
|
+
if (value <= 0)
|
|
650
|
+
return null;
|
|
109
651
|
return Math.max(1_000, Math.round(value));
|
|
110
652
|
}
|
|
111
|
-
if (typeof value === "string"
|
|
112
|
-
const
|
|
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);
|
|
113
667
|
if (Number.isFinite(parsed)) {
|
|
668
|
+
if (parsed <= 0)
|
|
669
|
+
return null;
|
|
114
670
|
return Math.max(1_000, Math.round(parsed));
|
|
115
671
|
}
|
|
116
672
|
}
|
|
117
|
-
return
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
function normalizeTokenBudget(value, fallback) {
|
|
676
|
+
const parsed = parseTokenBudget(value);
|
|
677
|
+
if (parsed !== null)
|
|
678
|
+
return parsed;
|
|
679
|
+
return fallback;
|
|
118
680
|
}
|
|
119
681
|
function defaultAutoContinueTokenBudget() {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return
|
|
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;
|
|
128
693
|
}
|
|
129
694
|
function estimateTokensForDurationHours(durationHours) {
|
|
130
695
|
if (!Number.isFinite(durationHours) || durationHours <= 0)
|
|
@@ -136,7 +701,8 @@ export function createAutoContinueEngine(deps) {
|
|
|
136
701
|
}
|
|
137
702
|
// Helpers used by previous task-level auto-continue implementation were removed in v2.
|
|
138
703
|
// readOpenClawSessionSummary was used by the previous task-level auto-continue implementation.
|
|
139
|
-
// Autopilot v2 dispatches workstream slices via
|
|
704
|
+
// Autopilot v2 dispatches workstream slices via runtime workers (codex/claude-code)
|
|
705
|
+
// and does not rely on OpenClaw session JSONL.
|
|
140
706
|
async function fetchInitiativeEntity(initiativeId) {
|
|
141
707
|
try {
|
|
142
708
|
const list = await client.listEntities("initiative", { limit: 200 });
|
|
@@ -159,7 +725,18 @@ export function createAutoContinueEngine(deps) {
|
|
|
159
725
|
await client.updateEntity("initiative", initiativeId, { metadata: nextMeta });
|
|
160
726
|
}
|
|
161
727
|
async function updateInitiativeAutoContinueState(input) {
|
|
728
|
+
syncLegacyRunPointers(input.run);
|
|
162
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
|
+
}));
|
|
163
740
|
const patch = {
|
|
164
741
|
auto_continue_enabled: input.run.status === "running" || input.run.status === "stopping",
|
|
165
742
|
auto_continue_status: input.run.status,
|
|
@@ -171,29 +748,52 @@ export function createAutoContinueEngine(deps) {
|
|
|
171
748
|
auto_continue_tokens_used: input.run.tokensUsed,
|
|
172
749
|
auto_continue_active_task_id: input.run.activeTaskId,
|
|
173
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,
|
|
174
753
|
auto_continue_active_task_token_estimate: input.run.activeTaskTokenEstimate,
|
|
175
754
|
auto_continue_last_task_id: input.run.lastTaskId,
|
|
176
755
|
auto_continue_last_run_id: input.run.lastRunId,
|
|
177
756
|
auto_continue_include_verification: input.run.includeVerification,
|
|
178
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,
|
|
179
763
|
...(input.run.lastError ? { auto_continue_last_error: input.run.lastError } : {}),
|
|
180
764
|
};
|
|
181
765
|
await updateInitiativeMetadata(input.initiativeId, patch);
|
|
182
766
|
}
|
|
183
767
|
async function stopAutoContinueRun(input) {
|
|
184
768
|
const now = new Date().toISOString();
|
|
185
|
-
|
|
769
|
+
ensureRunInternals(input.run);
|
|
770
|
+
const activeRunIds = listActiveSliceRunIds(input.run);
|
|
186
771
|
input.run.status = "stopped";
|
|
187
772
|
input.run.stopReason = input.reason;
|
|
188
773
|
input.run.stoppedAt = now;
|
|
189
774
|
input.run.updatedAt = now;
|
|
190
775
|
input.run.stopRequested = false;
|
|
776
|
+
input.run.activeSliceRunIds = [];
|
|
777
|
+
input.run.activeTaskIds = [];
|
|
191
778
|
input.run.activeRunId = null;
|
|
192
779
|
input.run.activeTaskId = null;
|
|
193
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
|
+
}
|
|
194
791
|
if (input.error)
|
|
195
792
|
input.run.lastError = input.error;
|
|
196
|
-
|
|
793
|
+
clearSpawnGuardRetryStateForInitiative(input.run.initiativeId);
|
|
794
|
+
for (const runId of activeRunIds) {
|
|
795
|
+
clearAutoContinueSliceTransientState(runId);
|
|
796
|
+
}
|
|
197
797
|
// Only pause the initiative on non-terminal stops (error, blocked, user-requested).
|
|
198
798
|
// Completed / budget-exhausted runs should not override the initiative status.
|
|
199
799
|
if (input.reason !== "completed" && input.reason !== "budget_exhausted") {
|
|
@@ -215,17 +815,29 @@ export function createAutoContinueEngine(deps) {
|
|
|
215
815
|
catch {
|
|
216
816
|
// best effort
|
|
217
817
|
}
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
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";
|
|
221
831
|
const message = input.reason === "completed"
|
|
222
832
|
? `Autopilot stopped: current dispatch scope completed${scopeSuffix}.`
|
|
223
833
|
: input.reason === "budget_exhausted"
|
|
224
|
-
? `Autopilot stopped: token budget exhausted (${input.run.tokensUsed}/${
|
|
834
|
+
? `Autopilot stopped: token budget exhausted (${input.run.tokensUsed}/${budgetValue}).`
|
|
225
835
|
: input.reason === "stopped"
|
|
226
836
|
? `Autopilot stopped by user request${scopeSuffix}.`
|
|
227
837
|
: input.reason === "blocked"
|
|
228
|
-
?
|
|
838
|
+
? decisionRequired
|
|
839
|
+
? `Autopilot stopped: blocked awaiting decision${scopeSuffix}.`
|
|
840
|
+
: `Autopilot stopped: blocked${scopeSuffix}.`
|
|
229
841
|
: `Autopilot stopped due to error${scopeSuffix}.`;
|
|
230
842
|
const phase = input.reason === "completed"
|
|
231
843
|
? "completed"
|
|
@@ -237,26 +849,83 @@ export function createAutoContinueEngine(deps) {
|
|
|
237
849
|
: input.reason === "budget_exhausted" || input.reason === "stopped"
|
|
238
850
|
? "warn"
|
|
239
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
|
+
};
|
|
240
863
|
await emitActivitySafe({
|
|
241
864
|
initiativeId: input.run.initiativeId,
|
|
242
|
-
runId:
|
|
243
|
-
correlationId:
|
|
865
|
+
runId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
|
|
866
|
+
correlationId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
|
|
244
867
|
phase,
|
|
245
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.",
|
|
246
879
|
message,
|
|
247
880
|
metadata: {
|
|
248
|
-
|
|
881
|
+
...buildSliceEnrichment({
|
|
882
|
+
run: stopRunContext,
|
|
883
|
+
workstreamId: scopedWorkstreamId,
|
|
884
|
+
event: "auto_continue_stopped",
|
|
885
|
+
}),
|
|
249
886
|
stop_reason: input.reason,
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
active_run_id: activeRunId,
|
|
887
|
+
active_run_id: primaryActiveRunId,
|
|
888
|
+
active_run_ids: activeRunIds,
|
|
253
889
|
last_run_id: input.run.lastRunId,
|
|
254
890
|
token_budget: input.run.tokenBudget,
|
|
255
891
|
tokens_used: input.run.tokensUsed,
|
|
256
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,
|
|
257
898
|
last_error: input.run.lastError,
|
|
899
|
+
error_location: errorLocation,
|
|
258
900
|
},
|
|
259
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
|
+
}
|
|
260
929
|
}
|
|
261
930
|
const codexBinResolver = createCodexBinResolver();
|
|
262
931
|
const resolveCodexBinInfo = () => codexBinResolver.resolveCodexBinInfo();
|
|
@@ -274,11 +943,15 @@ export function createAutoContinueEngine(deps) {
|
|
|
274
943
|
if (run.status !== "running" && run.status !== "stopping")
|
|
275
944
|
return;
|
|
276
945
|
const now = new Date().toISOString();
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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;
|
|
280
952
|
if (!slice) {
|
|
281
953
|
// Legacy/unknown pointer; clear so we can continue.
|
|
954
|
+
removeActiveSliceFromRun(run, { sliceRunId: activeRunIdForTick });
|
|
282
955
|
run.activeRunId = null;
|
|
283
956
|
run.activeTaskId = null;
|
|
284
957
|
run.updatedAt = now;
|
|
@@ -327,12 +1000,18 @@ export function createAutoContinueEngine(deps) {
|
|
|
327
1000
|
requested_by_agent_name: run.agentName,
|
|
328
1001
|
domain: slice.domain,
|
|
329
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,
|
|
330
1008
|
workstream_id: slice.workstreamId,
|
|
331
1009
|
workstream_title: slice.workstreamTitle ?? null,
|
|
332
1010
|
task_ids: slice.taskIds,
|
|
333
1011
|
milestone_ids: slice.milestoneIds,
|
|
334
1012
|
log_path: slice.logPath,
|
|
335
1013
|
output_path: slice.outputPath,
|
|
1014
|
+
...mockMeta(slice),
|
|
336
1015
|
},
|
|
337
1016
|
});
|
|
338
1017
|
}
|
|
@@ -344,8 +1023,10 @@ export function createAutoContinueEngine(deps) {
|
|
|
344
1023
|
const startedAtEpochMs = Date.parse(slice.startedAt);
|
|
345
1024
|
const fallbackEpochMs = Number.isFinite(startedAtEpochMs) ? startedAtEpochMs : nowMs;
|
|
346
1025
|
const outputUpdatedAtEpochMs = fileUpdatedAtEpochMs(slice.outputPath, fallbackEpochMs);
|
|
347
|
-
|
|
348
|
-
|
|
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);
|
|
349
1030
|
const logTail = readFileTailSafe(slice.logPath, 64_000);
|
|
350
1031
|
const mcpHandshake = detectMcpHandshakeFailure(logTail);
|
|
351
1032
|
if (mcpHandshake) {
|
|
@@ -369,21 +1050,28 @@ export function createAutoContinueEngine(deps) {
|
|
|
369
1050
|
correlationId: slice.runId,
|
|
370
1051
|
phase: "blocked",
|
|
371
1052
|
level: "error",
|
|
1053
|
+
progressPct: 55,
|
|
1054
|
+
nextStep: "Review MCP diagnostics, then choose retry, skip, or pause for investigation.",
|
|
372
1055
|
message: `Autopilot slice MCP failed: ${slice.workstreamTitle ?? slice.workstreamId}.`,
|
|
373
1056
|
metadata: {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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",
|
|
377
1067
|
mcp_server: mcpHandshake.server,
|
|
378
1068
|
mcp_line: mcpHandshake.line,
|
|
379
|
-
workstream_id: slice.workstreamId,
|
|
380
|
-
task_ids: slice.taskIds,
|
|
381
|
-
milestone_ids: slice.milestoneIds,
|
|
382
1069
|
log_path: slice.logPath,
|
|
383
1070
|
output_path: slice.outputPath,
|
|
1071
|
+
...mockMeta(slice),
|
|
384
1072
|
},
|
|
385
1073
|
});
|
|
386
|
-
await
|
|
1074
|
+
const decisionResult = await requestDecisionQueued({
|
|
387
1075
|
initiativeId: run.initiativeId,
|
|
388
1076
|
correlationId: slice.runId,
|
|
389
1077
|
title: `Autopilot slice MCP failed: ${slice.workstreamTitle ?? slice.workstreamId}`,
|
|
@@ -395,19 +1083,63 @@ export function createAutoContinueEngine(deps) {
|
|
|
395
1083
|
"Skip this workstream for now",
|
|
396
1084
|
],
|
|
397
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,
|
|
398
1127
|
});
|
|
399
1128
|
await stopAutoContinueRun({
|
|
400
1129
|
run,
|
|
401
1130
|
reason: "blocked",
|
|
402
1131
|
error: slice.lastError,
|
|
1132
|
+
decisionRequired: decisionResult.queued,
|
|
1133
|
+
decisionIds: decisionResult.decisionIds,
|
|
403
1134
|
});
|
|
404
1135
|
return;
|
|
405
1136
|
}
|
|
1137
|
+
const scopeTimeoutMs = AUTO_CONTINUE_SLICE_TIMEOUT_MS * SLICE_SCOPE_TIMEOUT_MULTIPLIER[slice.scope ?? "task"];
|
|
406
1138
|
const killDecision = shouldKillWorker({
|
|
407
1139
|
nowEpochMs: nowMs,
|
|
408
1140
|
startedAtEpochMs: fallbackEpochMs,
|
|
409
1141
|
logUpdatedAtEpochMs: stallUpdatedAtEpochMs,
|
|
410
|
-
}, { timeoutMs:
|
|
1142
|
+
}, { timeoutMs: scopeTimeoutMs, stallMs: AUTO_CONTINUE_SLICE_LOG_STALL_MS });
|
|
411
1143
|
if (killDecision.kill) {
|
|
412
1144
|
try {
|
|
413
1145
|
await stopProcess(pid);
|
|
@@ -420,7 +1152,7 @@ export function createAutoContinueEngine(deps) {
|
|
|
420
1152
|
slice.updatedAt = now;
|
|
421
1153
|
slice.lastError =
|
|
422
1154
|
killDecision.kind === "timeout"
|
|
423
|
-
? `Autopilot slice timed out after ${Math.round(
|
|
1155
|
+
? `Autopilot slice timed out after ${Math.round(scopeTimeoutMs / 60_000)} minutes.`
|
|
424
1156
|
: `Autopilot slice stalled (no output) for ${Math.round(AUTO_CONTINUE_SLICE_LOG_STALL_MS / 60_000)} minutes.`;
|
|
425
1157
|
autoContinueSliceRuns.set(slice.runId, slice);
|
|
426
1158
|
run.lastError = slice.lastError;
|
|
@@ -434,22 +1166,31 @@ export function createAutoContinueEngine(deps) {
|
|
|
434
1166
|
correlationId: slice.runId,
|
|
435
1167
|
phase: "blocked",
|
|
436
1168
|
level: "error",
|
|
1169
|
+
progressPct: 55,
|
|
1170
|
+
nextStep: "Open logs/output, decide retry or pause, and capture blocker context for handoff.",
|
|
437
1171
|
message: `Autopilot slice ${humanLabel}: ${slice.workstreamTitle ?? slice.workstreamId}.`,
|
|
438
1172
|
metadata: {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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",
|
|
445
1185
|
log_path: slice.logPath,
|
|
446
1186
|
output_path: slice.outputPath,
|
|
447
1187
|
reason: killDecision.reason,
|
|
448
1188
|
elapsed_ms: killDecision.elapsedMs,
|
|
449
1189
|
idle_ms: killDecision.idleMs,
|
|
1190
|
+
...mockMeta(slice),
|
|
450
1191
|
},
|
|
451
1192
|
});
|
|
452
|
-
await
|
|
1193
|
+
const decisionResult = await requestDecisionQueued({
|
|
453
1194
|
initiativeId: run.initiativeId,
|
|
454
1195
|
correlationId: slice.runId,
|
|
455
1196
|
title: `Autopilot slice ${humanLabel}: ${slice.workstreamTitle ?? slice.workstreamId}`,
|
|
@@ -461,11 +1202,63 @@ export function createAutoContinueEngine(deps) {
|
|
|
461
1202
|
"Skip this workstream for now",
|
|
462
1203
|
],
|
|
463
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,
|
|
464
1255
|
});
|
|
465
1256
|
await stopAutoContinueRun({
|
|
466
1257
|
run,
|
|
467
1258
|
reason: "blocked",
|
|
468
1259
|
error: slice.lastError,
|
|
1260
|
+
decisionRequired: decisionResult.queued,
|
|
1261
|
+
decisionIds: decisionResult.decisionIds,
|
|
469
1262
|
});
|
|
470
1263
|
return;
|
|
471
1264
|
}
|
|
@@ -478,7 +1271,7 @@ export function createAutoContinueEngine(deps) {
|
|
|
478
1271
|
}
|
|
479
1272
|
}
|
|
480
1273
|
if (!outputComplete)
|
|
481
|
-
|
|
1274
|
+
continue;
|
|
482
1275
|
}
|
|
483
1276
|
}
|
|
484
1277
|
// Slice finished.
|
|
@@ -486,15 +1279,25 @@ export function createAutoContinueEngine(deps) {
|
|
|
486
1279
|
const parsed = raw ? parseSliceResult(raw) : null;
|
|
487
1280
|
const parsedStatus = parsed?.status ?? "error";
|
|
488
1281
|
const defaultDecisionBlocking = parsedStatus === "completed" ? false : true;
|
|
489
|
-
const
|
|
1282
|
+
const allDecisions = Array.isArray(parsed?.decisions_needed)
|
|
490
1283
|
? (parsed?.decisions_needed ?? [])
|
|
491
1284
|
.filter((item) => Boolean(item && typeof item.question === "string" && item.question.trim()))
|
|
492
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));
|
|
493
1294
|
const blockingDecisionCount = decisions.filter((item) => typeof item.blocking === "boolean" ? item.blocking : defaultDecisionBlocking).length;
|
|
494
1295
|
const nonBlockingDecisionCount = Math.max(0, decisions.length - blockingDecisionCount);
|
|
495
1296
|
const effectiveParsedStatus = parsedStatus === "completed" && blockingDecisionCount > 0
|
|
496
1297
|
? "needs_decision"
|
|
497
|
-
: parsedStatus
|
|
1298
|
+
: parsedStatus === "needs_decision" && blockingDecisionCount === 0
|
|
1299
|
+
? "completed"
|
|
1300
|
+
: parsedStatus;
|
|
498
1301
|
slice.status =
|
|
499
1302
|
effectiveParsedStatus === "completed"
|
|
500
1303
|
? "completed"
|
|
@@ -510,32 +1313,157 @@ export function createAutoContinueEngine(deps) {
|
|
|
510
1313
|
autoContinueSliceRuns.set(slice.runId, slice);
|
|
511
1314
|
clearAutoContinueSliceTransientState(slice.runId);
|
|
512
1315
|
// Token accounting: codex CLI doesn't provide tokens here; use the modeled estimate.
|
|
513
|
-
const modeledTokens = slice.tokenEstimate ??
|
|
1316
|
+
const modeledTokens = slice.tokenEstimate ?? 0;
|
|
514
1317
|
run.tokensUsed += Math.max(0, modeledTokens);
|
|
515
1318
|
run.activeTaskTokenEstimate = null;
|
|
516
1319
|
const artifacts = Array.isArray(parsed?.artifacts)
|
|
517
1320
|
? (parsed?.artifacts ?? [])
|
|
518
1321
|
.filter((item) => Boolean(item && typeof item.name === "string" && item.name.trim()))
|
|
519
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;
|
|
520
1376
|
const taskUpdates = Array.isArray(parsed?.task_updates)
|
|
521
1377
|
? parsed.task_updates
|
|
522
1378
|
: [];
|
|
523
1379
|
const milestoneUpdates = Array.isArray(parsed?.milestone_updates)
|
|
524
1380
|
? parsed.milestone_updates
|
|
525
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 = [];
|
|
526
1406
|
for (const decision of decisions) {
|
|
527
|
-
|
|
1407
|
+
const isBlocking = typeof decision.blocking === "boolean" ? decision.blocking : defaultDecisionBlocking;
|
|
1408
|
+
const normalizedQuestion = decision.question.trim();
|
|
1409
|
+
const decisionResult = await requestDecisionQueued({
|
|
528
1410
|
initiativeId: run.initiativeId,
|
|
529
1411
|
correlationId: slice.runId,
|
|
530
|
-
title:
|
|
1412
|
+
title: normalizedQuestion,
|
|
531
1413
|
summary: decision.summary ?? parsed?.summary ?? null,
|
|
532
1414
|
urgency: decision.urgency ?? "high",
|
|
533
1415
|
options: Array.isArray(decision.options)
|
|
534
1416
|
? decision.options.filter((opt) => typeof opt === "string" && opt.trim())
|
|
535
1417
|
: [],
|
|
536
|
-
blocking:
|
|
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
|
+
],
|
|
537
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
|
+
}
|
|
538
1465
|
}
|
|
1466
|
+
const decisionIds = Array.from(new Set([...blockingDecisionIds, ...nonBlockingDecisionIds]));
|
|
539
1467
|
for (const artifact of artifacts) {
|
|
540
1468
|
await registerArtifactSafe({
|
|
541
1469
|
initiativeId: run.initiativeId,
|
|
@@ -543,16 +1471,86 @@ export function createAutoContinueEngine(deps) {
|
|
|
543
1471
|
agentId: slice.agentId,
|
|
544
1472
|
agentName: slice.agentName,
|
|
545
1473
|
workstreamId: slice.workstreamId,
|
|
1474
|
+
fallbackMilestoneId: slice.milestoneIds[0] ?? null,
|
|
1475
|
+
fallbackTaskIds: slice.taskIds,
|
|
546
1476
|
artifact,
|
|
1477
|
+
isMockWorker: slice.isMockWorker,
|
|
547
1478
|
});
|
|
548
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
|
+
}
|
|
549
1537
|
const statusUpdateResult = await applyAgentStatusUpdatesSafe({
|
|
550
1538
|
initiativeId: run.initiativeId,
|
|
551
1539
|
runId: slice.runId,
|
|
552
1540
|
correlationId: slice.runId,
|
|
553
1541
|
taskUpdates,
|
|
554
1542
|
milestoneUpdates,
|
|
1543
|
+
isMockWorker: slice.isMockWorker,
|
|
555
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
|
+
}
|
|
556
1554
|
try {
|
|
557
1555
|
writeRuntimeEvent({
|
|
558
1556
|
sourceClient: slice.sourceClient,
|
|
@@ -564,9 +1562,14 @@ export function createAutoContinueEngine(deps) {
|
|
|
564
1562
|
agentId: slice.agentId,
|
|
565
1563
|
agentName: slice.agentName ?? null,
|
|
566
1564
|
phase: slice.status === "completed" ? "completed" : "blocked",
|
|
567
|
-
message:
|
|
1565
|
+
message: userSummary ?? slice.lastError ?? "Autopilot slice finished.",
|
|
568
1566
|
metadata: {
|
|
569
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,
|
|
570
1573
|
requested_by_agent_id: run.agentId,
|
|
571
1574
|
requested_by_agent_name: run.agentName,
|
|
572
1575
|
status: effectiveParsedStatus,
|
|
@@ -576,55 +1579,149 @@ export function createAutoContinueEngine(deps) {
|
|
|
576
1579
|
non_blocking_decisions: nonBlockingDecisionCount,
|
|
577
1580
|
status_updates: statusUpdateResult.applied,
|
|
578
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,
|
|
579
1592
|
},
|
|
580
1593
|
});
|
|
581
1594
|
}
|
|
582
1595
|
catch {
|
|
583
1596
|
// best effort
|
|
584
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
|
+
}
|
|
585
1634
|
await emitActivitySafe({
|
|
586
1635
|
initiativeId: run.initiativeId,
|
|
587
1636
|
runId: slice.runId,
|
|
588
1637
|
correlationId: slice.runId,
|
|
589
1638
|
phase: slice.status === "completed" ? "completed" : "blocked",
|
|
590
1639
|
level: slice.status === "completed" ? "info" : "warn",
|
|
1640
|
+
progressPct: slice.status === "completed" ? 100 : 65,
|
|
1641
|
+
nextStep: nextStepHint,
|
|
591
1642
|
message: slice.status === "completed"
|
|
592
1643
|
? `Autopilot slice completed for ${slice.workstreamTitle ?? slice.workstreamId} (${slice.taskIds.length} task${slice.taskIds.length === 1 ? "" : "s"}).`
|
|
593
1644
|
: `Autopilot slice blocked: ${slice.workstreamTitle ?? slice.workstreamId}.`,
|
|
594
1645
|
metadata: {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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,
|
|
605
1665
|
parsed_status: effectiveParsedStatus,
|
|
606
1666
|
has_output: Boolean(parsed),
|
|
607
1667
|
artifacts: artifacts.length,
|
|
608
1668
|
decisions: decisions.length,
|
|
609
1669
|
blocking_decisions: blockingDecisionCount,
|
|
610
1670
|
non_blocking_decisions: nonBlockingDecisionCount,
|
|
611
|
-
|
|
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,
|
|
612
1675
|
status_updates_applied: statusUpdateResult.applied,
|
|
613
1676
|
status_updates_buffered: statusUpdateResult.buffered,
|
|
1677
|
+
reported_skill_evidence_count: skillEvidence.length,
|
|
1678
|
+
reported_skill_sha256_count: reportedSkillSha256Count,
|
|
1679
|
+
reported_skill_names: reportedSkillNames,
|
|
614
1680
|
output_path: slice.outputPath,
|
|
615
1681
|
log_path: slice.logPath,
|
|
616
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,
|
|
617
1690
|
},
|
|
618
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
|
+
}
|
|
619
1707
|
if (slice.status !== "completed") {
|
|
620
|
-
|
|
621
|
-
|
|
1708
|
+
let fallbackDecisionResult = {
|
|
1709
|
+
queued: false,
|
|
1710
|
+
decisionIds: [],
|
|
1711
|
+
};
|
|
1712
|
+
if (!blockingDecisionQueued) {
|
|
1713
|
+
const blockedLike = slice.status === "blocked";
|
|
1714
|
+
fallbackDecisionResult = await requestDecisionQueued({
|
|
622
1715
|
initiativeId: run.initiativeId,
|
|
623
1716
|
correlationId: slice.runId,
|
|
624
|
-
title:
|
|
1717
|
+
title: blockedLike
|
|
1718
|
+
? `Autopilot slice blocked: ${slice.workstreamTitle ?? slice.workstreamId}`
|
|
1719
|
+
: `Autopilot slice failed: ${slice.workstreamTitle ?? slice.workstreamId}`,
|
|
625
1720
|
summary: parsed?.summary ??
|
|
626
1721
|
slice.lastError ??
|
|
627
|
-
|
|
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."),
|
|
628
1725
|
urgency: "high",
|
|
629
1726
|
options: [
|
|
630
1727
|
"Retry this workstream slice",
|
|
@@ -632,14 +1729,66 @@ export function createAutoContinueEngine(deps) {
|
|
|
632
1729
|
"Skip this workstream for now",
|
|
633
1730
|
],
|
|
634
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
|
+
],
|
|
635
1768
|
});
|
|
636
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
|
+
}
|
|
637
1784
|
await stopAutoContinueRun({
|
|
638
1785
|
run,
|
|
639
1786
|
reason: slice.status === "error" ? "error" : "blocked",
|
|
640
1787
|
error: parsed?.summary ??
|
|
641
1788
|
slice.lastError ??
|
|
642
1789
|
`Slice returned status: ${effectiveParsedStatus}`,
|
|
1790
|
+
decisionRequired: blockingDecisionQueued || fallbackDecisionResult.queued,
|
|
1791
|
+
decisionIds: Array.from(new Set([...decisionIds, ...fallbackDecisionResult.decisionIds])),
|
|
643
1792
|
});
|
|
644
1793
|
return;
|
|
645
1794
|
}
|
|
@@ -654,7 +1803,7 @@ export function createAutoContinueEngine(deps) {
|
|
|
654
1803
|
const attentionSummary = completionHadNoOutcome
|
|
655
1804
|
? "The slice reported completion but did not produce artifacts or status updates. Decide whether to retry, request stronger output, or mark tasks manually."
|
|
656
1805
|
: "The slice exited without a valid output contract. Review logs/output and decide whether to retry or pause autopilot.";
|
|
657
|
-
await
|
|
1806
|
+
const decisionResult = await requestDecisionQueued({
|
|
658
1807
|
initiativeId: run.initiativeId,
|
|
659
1808
|
correlationId: slice.runId,
|
|
660
1809
|
title: attentionTitle,
|
|
@@ -666,7 +1815,61 @@ export function createAutoContinueEngine(deps) {
|
|
|
666
1815
|
"Skip this workstream for now",
|
|
667
1816
|
],
|
|
668
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,
|
|
669
1869
|
});
|
|
1870
|
+
if (!run.blockedWorkstreamIds.includes(slice.workstreamId)) {
|
|
1871
|
+
run.blockedWorkstreamIds.push(slice.workstreamId);
|
|
1872
|
+
}
|
|
670
1873
|
await stopAutoContinueRun({
|
|
671
1874
|
run,
|
|
672
1875
|
reason: completionHadNoOutcome ? "blocked" : "error",
|
|
@@ -674,13 +1877,31 @@ export function createAutoContinueEngine(deps) {
|
|
|
674
1877
|
(completionHadNoOutcome
|
|
675
1878
|
? "Slice completed without verifiable outcomes."
|
|
676
1879
|
: "Slice failed or returned invalid output."),
|
|
1880
|
+
decisionRequired: completionHadNoOutcome && decisionResult.queued,
|
|
1881
|
+
decisionIds: decisionResult.decisionIds,
|
|
677
1882
|
});
|
|
678
1883
|
return;
|
|
679
1884
|
}
|
|
680
1885
|
run.lastRunId = slice.runId;
|
|
681
|
-
run.lastTaskId =
|
|
682
|
-
run
|
|
683
|
-
|
|
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;
|
|
684
1905
|
run.updatedAt = now;
|
|
685
1906
|
try {
|
|
686
1907
|
await updateInitiativeAutoContinueState({
|
|
@@ -691,6 +1912,50 @@ export function createAutoContinueEngine(deps) {
|
|
|
691
1912
|
catch {
|
|
692
1913
|
// best effort
|
|
693
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
|
+
}
|
|
694
1959
|
if (run.stopAfterSlice) {
|
|
695
1960
|
run.stopAfterSlice = false;
|
|
696
1961
|
await stopAutoContinueRun({ run, reason: "completed" });
|
|
@@ -702,17 +1967,26 @@ export function createAutoContinueEngine(deps) {
|
|
|
702
1967
|
}
|
|
703
1968
|
}
|
|
704
1969
|
}
|
|
1970
|
+
syncLegacyRunPointers(run);
|
|
705
1971
|
if (run.stopRequested) {
|
|
706
1972
|
run.status = "stopping";
|
|
707
1973
|
run.updatedAt = now;
|
|
708
1974
|
await stopAutoContinueRun({ run, reason: "stopped" });
|
|
709
1975
|
return;
|
|
710
1976
|
}
|
|
1977
|
+
const tokenBudgetValue = typeof run.tokenBudget === "number" && Number.isFinite(run.tokenBudget)
|
|
1978
|
+
? run.tokenBudget
|
|
1979
|
+
: null;
|
|
711
1980
|
// 2) Enforce token guardrail before starting a new slice.
|
|
712
|
-
if (run.tokensUsed >=
|
|
1981
|
+
if (tokenBudgetValue !== null && run.tokensUsed >= tokenBudgetValue) {
|
|
713
1982
|
await stopAutoContinueRun({ run, reason: "budget_exhausted" });
|
|
714
1983
|
return;
|
|
715
1984
|
}
|
|
1985
|
+
const activeSliceCount = listActiveSliceRunIds(run).length;
|
|
1986
|
+
if (activeSliceCount >= run.maxParallelSlices) {
|
|
1987
|
+
run.updatedAt = now;
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
716
1990
|
// 3) Pick next workstream slice and dispatch.
|
|
717
1991
|
let graph;
|
|
718
1992
|
try {
|
|
@@ -728,6 +2002,7 @@ export function createAutoContinueEngine(deps) {
|
|
|
728
2002
|
}
|
|
729
2003
|
const nodes = graph.nodes;
|
|
730
2004
|
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
2005
|
+
applyLocalStatusOverridesToGraph(run.initiativeId, nodeById);
|
|
731
2006
|
const taskNodes = nodes.filter((node) => node.type === "task");
|
|
732
2007
|
const todoTasks = taskNodes.filter((node) => isTodoStatus(node.status));
|
|
733
2008
|
if (todoTasks.length === 0) {
|
|
@@ -746,6 +2021,7 @@ export function createAutoContinueEngine(deps) {
|
|
|
746
2021
|
};
|
|
747
2022
|
// Select the next eligible workstream by scanning ordered todos.
|
|
748
2023
|
let selectedWorkstreamId = null;
|
|
2024
|
+
let deferredBySpawnGuardRateLimit = 0;
|
|
749
2025
|
for (const taskId of graph.recentTodos) {
|
|
750
2026
|
const node = nodeById.get(taskId);
|
|
751
2027
|
if (!node || node.type !== "task")
|
|
@@ -763,6 +2039,18 @@ export function createAutoContinueEngine(deps) {
|
|
|
763
2039
|
}
|
|
764
2040
|
if (!node.workstreamId)
|
|
765
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
|
+
}
|
|
766
2054
|
const ws = nodeById.get(node.workstreamId);
|
|
767
2055
|
if (ws && !isDispatchableWorkstreamStatus(ws.status))
|
|
768
2056
|
continue;
|
|
@@ -770,32 +2058,107 @@ export function createAutoContinueEngine(deps) {
|
|
|
770
2058
|
continue;
|
|
771
2059
|
if (taskHasBlockedParent(node))
|
|
772
2060
|
continue;
|
|
2061
|
+
const retryAtMs = getSpawnGuardRetryAtMs(run.initiativeId, node.id);
|
|
2062
|
+
if (retryAtMs > 0) {
|
|
2063
|
+
deferredBySpawnGuardRateLimit += 1;
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
773
2066
|
selectedWorkstreamId = node.workstreamId;
|
|
774
2067
|
break;
|
|
775
2068
|
}
|
|
776
2069
|
if (!selectedWorkstreamId) {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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;
|
|
799
2162
|
}
|
|
800
2163
|
let cappedSliceTaskNodes = sliceTaskNodes;
|
|
801
2164
|
let expectedDurationHours = cappedSliceTaskNodes.reduce((acc, t) => acc +
|
|
@@ -803,14 +2166,14 @@ export function createAutoContinueEngine(deps) {
|
|
|
803
2166
|
? Math.max(0, t.expectedDurationHours)
|
|
804
2167
|
: 0), 0);
|
|
805
2168
|
let tokenEstimate = estimateTokensForDurationHours(expectedDurationHours);
|
|
806
|
-
const remainingTokens =
|
|
807
|
-
if (remainingTokens <= 0) {
|
|
2169
|
+
const remainingTokens = tokenBudgetValue !== null ? tokenBudgetValue - run.tokensUsed : null;
|
|
2170
|
+
if (remainingTokens !== null && remainingTokens <= 0) {
|
|
808
2171
|
await stopAutoContinueRun({ run, reason: "budget_exhausted" });
|
|
809
2172
|
return;
|
|
810
2173
|
}
|
|
811
2174
|
// If the modeled slice exceeds the remaining budget, shrink the slice to fit rather than
|
|
812
2175
|
// stopping immediately (Play should still dispatch at least the primary task when possible).
|
|
813
|
-
if (tokenEstimate > 0 && tokenEstimate > remainingTokens) {
|
|
2176
|
+
if (remainingTokens !== null && tokenEstimate > 0 && tokenEstimate > remainingTokens) {
|
|
814
2177
|
const nextSlice = [];
|
|
815
2178
|
let hours = 0;
|
|
816
2179
|
for (const task of sliceTaskNodes) {
|
|
@@ -832,12 +2195,300 @@ export function createAutoContinueEngine(deps) {
|
|
|
832
2195
|
expectedDurationHours = hours;
|
|
833
2196
|
tokenEstimate = estimateTokensForDurationHours(expectedDurationHours);
|
|
834
2197
|
}
|
|
835
|
-
if (tokenEstimate > 0 && tokenEstimate > remainingTokens) {
|
|
2198
|
+
if (remainingTokens !== null && tokenEstimate > 0 && tokenEstimate > remainingTokens) {
|
|
836
2199
|
await stopAutoContinueRun({ run, reason: "budget_exhausted" });
|
|
837
2200
|
return;
|
|
838
2201
|
}
|
|
839
2202
|
const executionPolicy = deriveExecutionPolicy(primaryTask, workstreamNode);
|
|
2203
|
+
const behaviorConfig = deriveBehaviorConfigContext(primaryTask, workstreamNode);
|
|
2204
|
+
const behaviorAutomationLevel = deriveBehaviorAutomationLevel(primaryTask, workstreamNode);
|
|
840
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
|
+
}
|
|
841
2492
|
const spawnGuardResult = await checkSpawnGuardSafe({
|
|
842
2493
|
domain: executionPolicy.domain,
|
|
843
2494
|
taskId: primaryTask.id,
|
|
@@ -850,60 +2501,224 @@ export function createAutoContinueEngine(deps) {
|
|
|
850
2501
|
const allowed = spawnGuardResult.allowed;
|
|
851
2502
|
if (allowed === false) {
|
|
852
2503
|
const blockedReason = summarizeSpawnGuardBlockReason(spawnGuardResult);
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
}
|
|
860
|
-
try {
|
|
861
|
-
await syncParentRollupsForTask({
|
|
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, {
|
|
862
2510
|
initiativeId: run.initiativeId,
|
|
863
|
-
|
|
2511
|
+
retryAtMs,
|
|
2512
|
+
});
|
|
2513
|
+
setLaneState(run, {
|
|
864
2514
|
workstreamId: selectedWorkstreamId,
|
|
865
|
-
|
|
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,
|
|
866
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.",
|
|
867
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;
|
|
868
2562
|
}
|
|
869
|
-
|
|
870
|
-
|
|
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;
|
|
871
2721
|
}
|
|
872
|
-
await emitActivitySafe({
|
|
873
|
-
initiativeId: run.initiativeId,
|
|
874
|
-
runId: sliceRunId,
|
|
875
|
-
correlationId: sliceRunId,
|
|
876
|
-
phase: "blocked",
|
|
877
|
-
level: "error",
|
|
878
|
-
message: `Autopilot blocked by spawn guard for ${workstreamTitle ?? selectedWorkstreamId}.`,
|
|
879
|
-
metadata: {
|
|
880
|
-
event: "auto_continue_spawn_guard_blocked",
|
|
881
|
-
task_id: primaryTask.id,
|
|
882
|
-
workstream_id: selectedWorkstreamId,
|
|
883
|
-
blocked_reason: blockedReason,
|
|
884
|
-
spawn_guard: spawnGuardResult,
|
|
885
|
-
},
|
|
886
|
-
});
|
|
887
|
-
await requestDecisionSafe({
|
|
888
|
-
initiativeId: run.initiativeId,
|
|
889
|
-
correlationId: sliceRunId,
|
|
890
|
-
title: `Unblock autopilot for ${workstreamTitle ?? selectedWorkstreamId}`,
|
|
891
|
-
summary: [
|
|
892
|
-
`Spawn guard denied dispatch for primary task ${primaryTask.id}.`,
|
|
893
|
-
`Reason: ${blockedReason}`,
|
|
894
|
-
`Domain: ${executionPolicy.domain}`,
|
|
895
|
-
`Required skills: ${executionPolicy.requiredSkills.join(", ")}`,
|
|
896
|
-
].join(" "),
|
|
897
|
-
urgency: "high",
|
|
898
|
-
options: [
|
|
899
|
-
"Approve exception and continue",
|
|
900
|
-
"Reassign slice/domain",
|
|
901
|
-
"Pause and investigate quality gate",
|
|
902
|
-
],
|
|
903
|
-
blocking: true,
|
|
904
|
-
});
|
|
905
|
-
await stopAutoContinueRun({ run, reason: "blocked", error: blockedReason });
|
|
906
|
-
return;
|
|
907
2722
|
}
|
|
908
2723
|
}
|
|
909
2724
|
const milestoneIds = dedupeStrings(cappedSliceTaskNodes.map((t) => (t.milestoneId ?? "").trim()).filter(Boolean));
|
|
@@ -918,25 +2733,92 @@ export function createAutoContinueEngine(deps) {
|
|
|
918
2733
|
milestoneId: t.milestoneId ?? null,
|
|
919
2734
|
}));
|
|
920
2735
|
const schemaPath = ensureAutopilotSliceSchemaPath(AUTO_CONTINUE_SLICE_SCHEMA_FILENAME);
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
+
}
|
|
932
2814
|
const logsDir = join(getOrgxPluginConfigDir(), AUTO_CONTINUE_SLICE_LOG_DIRNAME);
|
|
933
2815
|
const logPath = join(logsDir, `${sliceRunId}.log`);
|
|
934
2816
|
const outputPath = join(logsDir, `${sliceRunId}.output.json`);
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
//
|
|
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).
|
|
938
2820
|
if (!workerCwd || workerCwd === "/") {
|
|
939
|
-
workerCwd =
|
|
2821
|
+
workerCwd = resolveAutopilotDefaultCwd(__filename);
|
|
940
2822
|
}
|
|
941
2823
|
const sliceAgent = resolveOrgxAgentForDomain(executionPolicy.domain);
|
|
942
2824
|
const workerKind = (process.env.ORGX_AUTOPILOT_WORKER_KIND ?? "").trim().toLowerCase();
|
|
@@ -960,6 +2842,7 @@ export function createAutoContinueEngine(deps) {
|
|
|
960
2842
|
cwd: workerCwd,
|
|
961
2843
|
logPath,
|
|
962
2844
|
outputPath,
|
|
2845
|
+
outputSchemaPath: schemaPath,
|
|
963
2846
|
env: {
|
|
964
2847
|
ORGX_SOURCE_CLIENT: executorSourceClient,
|
|
965
2848
|
ORGX_RUN_ID: sliceRunId,
|
|
@@ -968,8 +2851,16 @@ export function createAutoContinueEngine(deps) {
|
|
|
968
2851
|
ORGX_WORKSTREAM_ID: selectedWorkstreamId,
|
|
969
2852
|
ORGX_WORKSTREAM_TITLE: workstreamTitle ?? undefined,
|
|
970
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,
|
|
971
2861
|
ORGX_AGENT_ID: sliceAgent.id,
|
|
972
2862
|
ORGX_AGENT_NAME: sliceAgent.name,
|
|
2863
|
+
ORGX_KICKOFF_CONTEXT_HASH: kickoffContextHash ?? undefined,
|
|
973
2864
|
ORGX_OUTPUT_PATH: outputPath,
|
|
974
2865
|
ORGX_RUNTIME_HOOK_URL: runtimeHookUrl ?? undefined,
|
|
975
2866
|
ORGX_HOOK_TOKEN: runtimeHookToken ?? undefined,
|
|
@@ -985,6 +2876,11 @@ export function createAutoContinueEngine(deps) {
|
|
|
985
2876
|
agentName: sliceAgent.name,
|
|
986
2877
|
domain: executionPolicy.domain,
|
|
987
2878
|
requiredSkills: executionPolicy.requiredSkills,
|
|
2879
|
+
behaviorConfigId: behaviorConfig.configId,
|
|
2880
|
+
behaviorConfigVersion: behaviorConfig.version,
|
|
2881
|
+
behaviorConfigHash: behaviorConfig.hash,
|
|
2882
|
+
behaviorPolicySource: behaviorConfig.policySource,
|
|
2883
|
+
behaviorAutomationLevel,
|
|
988
2884
|
sourceClient: executorSourceClient,
|
|
989
2885
|
pid: spawned.pid,
|
|
990
2886
|
status: "running",
|
|
@@ -996,7 +2892,10 @@ export function createAutoContinueEngine(deps) {
|
|
|
996
2892
|
logPath,
|
|
997
2893
|
taskIds: cappedSliceTaskNodes.map((t) => t.id),
|
|
998
2894
|
milestoneIds,
|
|
2895
|
+
scope: run.scope,
|
|
2896
|
+
scopeMilestoneIds: scopeMilestoneIds,
|
|
999
2897
|
lastError: null,
|
|
2898
|
+
isMockWorker: workerKind === "mock",
|
|
1000
2899
|
};
|
|
1001
2900
|
autoContinueSliceRuns.set(sliceRunId, slice);
|
|
1002
2901
|
try {
|
|
@@ -1013,15 +2912,28 @@ export function createAutoContinueEngine(deps) {
|
|
|
1013
2912
|
message: `Autopilot slice started: ${workstreamTitle ?? selectedWorkstreamId}`,
|
|
1014
2913
|
metadata: {
|
|
1015
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,
|
|
1016
2920
|
requested_by_agent_id: run.agentId,
|
|
1017
2921
|
requested_by_agent_name: run.agentName,
|
|
1018
2922
|
domain: executionPolicy.domain,
|
|
1019
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,
|
|
1020
2929
|
task_ids: slice.taskIds,
|
|
1021
2930
|
initiative_title: initiativeTitle ?? null,
|
|
1022
2931
|
workstream_title: workstreamTitle ?? null,
|
|
2932
|
+
scope: slice.scope,
|
|
2933
|
+
scope_milestone_ids: slice.scopeMilestoneIds,
|
|
1023
2934
|
log_path: logPath,
|
|
1024
2935
|
output_path: outputPath,
|
|
2936
|
+
...mockMeta(slice),
|
|
1025
2937
|
},
|
|
1026
2938
|
});
|
|
1027
2939
|
}
|
|
@@ -1033,24 +2945,34 @@ export function createAutoContinueEngine(deps) {
|
|
|
1033
2945
|
initiativeId: run.initiativeId,
|
|
1034
2946
|
runId: sliceRunId,
|
|
1035
2947
|
correlationId: sliceRunId,
|
|
2948
|
+
progressPct: 10,
|
|
2949
|
+
nextStep: `Worker ${sliceAgent.name} is executing ${workstreamTitle ?? selectedWorkstreamId}.`,
|
|
1036
2950
|
phase: "execution",
|
|
1037
2951
|
level: "info",
|
|
1038
2952
|
message: `Autopilot dispatched slice for ${workstreamTitle ?? selectedWorkstreamId}.`,
|
|
1039
2953
|
metadata: {
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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,
|
|
1047
2970
|
initiative_title: initiativeTitle ?? null,
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
task_ids: slice.taskIds,
|
|
1051
|
-
milestone_ids: milestoneIds,
|
|
2971
|
+
scope: slice.scope,
|
|
2972
|
+
scope_milestone_ids: slice.scopeMilestoneIds,
|
|
1052
2973
|
log_path: logPath,
|
|
1053
2974
|
output_path: outputPath,
|
|
2975
|
+
...mockMeta(slice),
|
|
1054
2976
|
},
|
|
1055
2977
|
});
|
|
1056
2978
|
upsertAgentContext({
|
|
@@ -1060,11 +2982,34 @@ export function createAutoContinueEngine(deps) {
|
|
|
1060
2982
|
workstreamId: selectedWorkstreamId,
|
|
1061
2983
|
taskId: primaryTask.id,
|
|
1062
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
|
+
});
|
|
1063
2993
|
run.lastTaskId = primaryTask.id;
|
|
1064
2994
|
run.lastRunId = sliceRunId;
|
|
1065
|
-
run.
|
|
1066
|
-
|
|
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
|
+
});
|
|
1067
3009
|
run.activeTaskTokenEstimate = tokenEstimate > 0 ? tokenEstimate : null;
|
|
3010
|
+
syncLegacyRunPointers(run);
|
|
3011
|
+
// Clear stale errors when a new slice dispatches successfully.
|
|
3012
|
+
run.lastError = null;
|
|
1068
3013
|
run.updatedAt = now;
|
|
1069
3014
|
try {
|
|
1070
3015
|
await client.updateEntity("initiative", run.initiativeId, { status: "active" });
|
|
@@ -1095,7 +3040,7 @@ export function createAutoContinueEngine(deps) {
|
|
|
1095
3040
|
}
|
|
1096
3041
|
catch (err) {
|
|
1097
3042
|
// Never let one loop crash the whole handler.
|
|
1098
|
-
run.lastError = safeErrorMessage(err)
|
|
3043
|
+
run.lastError = `[mission-control.auto-continue.engine.tick-all] ${safeErrorMessage(err)}`;
|
|
1099
3044
|
run.updatedAt = new Date().toISOString();
|
|
1100
3045
|
await stopAutoContinueRun({ run, reason: "error", error: run.lastError });
|
|
1101
3046
|
}
|
|
@@ -1123,12 +3068,319 @@ export function createAutoContinueEngine(deps) {
|
|
|
1123
3068
|
const run = autoContinueRuns.get(initiativeId) ?? null;
|
|
1124
3069
|
if (!run)
|
|
1125
3070
|
return null;
|
|
3071
|
+
ensureRunInternals(run);
|
|
1126
3072
|
if (run.status !== "running" && run.status !== "stopping")
|
|
1127
3073
|
return null;
|
|
1128
|
-
if (
|
|
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")) {
|
|
1129
3091
|
return run;
|
|
1130
3092
|
}
|
|
1131
|
-
return
|
|
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
|
+
};
|
|
1132
3384
|
}
|
|
1133
3385
|
async function startAutoContinueRun(input) {
|
|
1134
3386
|
const now = new Date().toISOString();
|
|
@@ -1142,6 +3394,10 @@ export function createAutoContinueEngine(deps) {
|
|
|
1142
3394
|
includeVerification: false,
|
|
1143
3395
|
allowedWorkstreamIds: null,
|
|
1144
3396
|
stopAfterSlice: false,
|
|
3397
|
+
ignoreSpawnGuardRateLimit: false,
|
|
3398
|
+
maxParallelSlices: AUTO_CONTINUE_MAX_PARALLEL_DEFAULT,
|
|
3399
|
+
parallelMode: "iwmt",
|
|
3400
|
+
scope: "task",
|
|
1145
3401
|
tokenBudget: defaultAutoContinueTokenBudget(),
|
|
1146
3402
|
tokensUsed: 0,
|
|
1147
3403
|
status: "running",
|
|
@@ -1153,10 +3409,15 @@ export function createAutoContinueEngine(deps) {
|
|
|
1153
3409
|
lastError: null,
|
|
1154
3410
|
lastTaskId: null,
|
|
1155
3411
|
lastRunId: null,
|
|
3412
|
+
activeSliceRunIds: [],
|
|
3413
|
+
activeTaskIds: [],
|
|
3414
|
+
laneByWorkstreamId: {},
|
|
3415
|
+
blockedWorkstreamIds: [],
|
|
1156
3416
|
activeTaskId: null,
|
|
1157
3417
|
activeRunId: null,
|
|
1158
3418
|
activeTaskTokenEstimate: null,
|
|
1159
3419
|
};
|
|
3420
|
+
ensureRunInternals(run);
|
|
1160
3421
|
run.agentId = input.agentId;
|
|
1161
3422
|
run.agentName =
|
|
1162
3423
|
typeof input.agentName === "string" && input.agentName.trim().length > 0
|
|
@@ -1164,8 +3425,24 @@ export function createAutoContinueEngine(deps) {
|
|
|
1164
3425
|
: null;
|
|
1165
3426
|
run.includeVerification = input.includeVerification;
|
|
1166
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);
|
|
1167
3430
|
run.stopAfterSlice = Boolean(input.stopAfterSlice);
|
|
1168
|
-
run.
|
|
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
|
+
}
|
|
1169
3446
|
run.status = "running";
|
|
1170
3447
|
run.stopReason = null;
|
|
1171
3448
|
run.stopRequested = false;
|
|
@@ -1178,10 +3455,15 @@ export function createAutoContinueEngine(deps) {
|
|
|
1178
3455
|
run.startedAt = now;
|
|
1179
3456
|
run.lastTaskId = null;
|
|
1180
3457
|
run.lastRunId = null;
|
|
3458
|
+
run.activeSliceRunIds = [];
|
|
3459
|
+
run.activeTaskIds = [];
|
|
3460
|
+
run.blockedWorkstreamIds = [];
|
|
3461
|
+
run.laneByWorkstreamId = {};
|
|
1181
3462
|
run.activeTaskId = null;
|
|
1182
3463
|
run.activeRunId = null;
|
|
1183
3464
|
run.activeTaskTokenEstimate = null;
|
|
1184
3465
|
}
|
|
3466
|
+
syncLegacyRunPointers(run);
|
|
1185
3467
|
autoContinueRuns.set(input.initiativeId, run);
|
|
1186
3468
|
void client
|
|
1187
3469
|
.updateEntity("initiative", input.initiativeId, { status: "active" })
|
|
@@ -1194,6 +3476,66 @@ export function createAutoContinueEngine(deps) {
|
|
|
1194
3476
|
}).catch(() => {
|
|
1195
3477
|
// best effort
|
|
1196
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
|
+
}
|
|
1197
3539
|
return run;
|
|
1198
3540
|
}
|
|
1199
3541
|
return {
|
|
@@ -1203,6 +3545,7 @@ export function createAutoContinueEngine(deps) {
|
|
|
1203
3545
|
writeRuntimeEvent,
|
|
1204
3546
|
autoContinueTickMs: AUTO_CONTINUE_TICK_MS,
|
|
1205
3547
|
defaultAutoContinueTokenBudget,
|
|
3548
|
+
defaultAutoContinueMaxParallelSlices,
|
|
1206
3549
|
setLocalInitiativeStatusOverride,
|
|
1207
3550
|
clearLocalInitiativeStatusOverride,
|
|
1208
3551
|
applyLocalInitiativeOverrides,
|
|
@@ -1213,6 +3556,8 @@ export function createAutoContinueEngine(deps) {
|
|
|
1213
3556
|
tickAllAutoContinue,
|
|
1214
3557
|
isInitiativeActiveStatus,
|
|
1215
3558
|
runningAutoContinueForWorkstream,
|
|
3559
|
+
getAutoContinueLaneForWorkstream,
|
|
3560
|
+
scheduleAutoFixForWorkstream,
|
|
1216
3561
|
startAutoContinueRun,
|
|
1217
3562
|
};
|
|
1218
3563
|
}
|