@useorgx/openclaw-plugin 0.4.9 → 0.7.2
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 +77 -11
- package/dashboard/dist/assets/6mILZQ2a.js +1 -0
- package/dashboard/dist/assets/6mILZQ2a.js.br +0 -0
- package/dashboard/dist/assets/6mILZQ2a.js.gz +0 -0
- package/dashboard/dist/assets/8dksYiq4.js +2 -0
- package/dashboard/dist/assets/8dksYiq4.js.br +0 -0
- package/dashboard/dist/assets/8dksYiq4.js.gz +0 -0
- package/dashboard/dist/assets/B5zYRHc3.js +1 -0
- package/dashboard/dist/assets/B5zYRHc3.js.br +0 -0
- package/dashboard/dist/assets/B5zYRHc3.js.gz +0 -0
- package/dashboard/dist/assets/B6wPWJ35.js +1 -0
- package/dashboard/dist/assets/B6wPWJ35.js.br +0 -0
- package/dashboard/dist/assets/B6wPWJ35.js.gz +0 -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/BWEwjt1W.js +1 -0
- package/dashboard/dist/assets/BWEwjt1W.js.br +0 -0
- package/dashboard/dist/assets/BWEwjt1W.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/BzRbDCAD.css +1 -0
- package/dashboard/dist/assets/BzRbDCAD.css.br +0 -0
- package/dashboard/dist/assets/BzRbDCAD.css.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/C8uM3AX8.js +1 -0
- package/dashboard/dist/assets/C8uM3AX8.js.br +0 -0
- package/dashboard/dist/assets/C8uM3AX8.js.gz +0 -0
- package/dashboard/dist/assets/C9jy61eu.js +212 -0
- package/dashboard/dist/assets/C9jy61eu.js.br +0 -0
- package/dashboard/dist/assets/C9jy61eu.js.gz +0 -0
- package/dashboard/dist/assets/CC63EwFD.js +1 -0
- package/dashboard/dist/assets/CC63EwFD.js.br +0 -0
- package/dashboard/dist/assets/CC63EwFD.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/CZaT3ob_.js +1 -0
- package/dashboard/dist/assets/CZaT3ob_.js.br +0 -0
- package/dashboard/dist/assets/CZaT3ob_.js.gz +0 -0
- package/dashboard/dist/assets/CgaottFX.js +1 -0
- package/dashboard/dist/assets/CgaottFX.js.br +0 -0
- package/dashboard/dist/assets/CgaottFX.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/CzCxAZlW.js +1 -0
- package/dashboard/dist/assets/CzCxAZlW.js.br +0 -0
- package/dashboard/dist/assets/CzCxAZlW.js.gz +0 -0
- package/dashboard/dist/assets/D3iMTYEj.js +1 -0
- package/dashboard/dist/assets/D3iMTYEj.js.br +0 -0
- package/dashboard/dist/assets/D3iMTYEj.js.gz +0 -0
- package/dashboard/dist/assets/D8JNX8kq.js +2 -0
- package/dashboard/dist/assets/D8JNX8kq.js.br +0 -0
- package/dashboard/dist/assets/D8JNX8kq.js.gz +0 -0
- package/dashboard/dist/assets/DnA8dpj6.js +1 -0
- package/dashboard/dist/assets/DnA8dpj6.js.br +0 -0
- package/dashboard/dist/assets/DnA8dpj6.js.gz +0 -0
- package/dashboard/dist/assets/IUexzymk.js +1 -0
- package/dashboard/dist/assets/IUexzymk.js.br +0 -0
- package/dashboard/dist/assets/IUexzymk.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/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/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/rttbDbEx.js +1 -0
- package/dashboard/dist/assets/rttbDbEx.js.br +0 -0
- package/dashboard/dist/assets/rttbDbEx.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/practice-exercise-schema.d.ts +216 -0
- package/dist/contracts/practice-exercise-schema.js +314 -0
- 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 +199 -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 +247 -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/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 +3145 -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 +328 -25
- package/dist/http/helpers/autopilot-slice-utils.d.ts +18 -0
- package/dist/http/helpers/autopilot-slice-utils.js +514 -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 +104 -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 +877 -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 +1420 -105
- package/dist/http/routes/agent-suite.d.ts +9 -0
- package/dist/http/routes/agent-suite.js +294 -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 +232 -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 +49 -2
- package/dist/http/routes/live-snapshot.js +653 -23
- package/dist/http/routes/live-terminal.d.ts +11 -0
- package/dist/http/routes/live-terminal.js +154 -0
- package/dist/http/routes/live-triage.d.ts +61 -0
- package/dist/http/routes/live-triage.js +192 -0
- package/dist/http/routes/mission-control-actions.d.ts +49 -1
- package/dist/http/routes/mission-control-actions.js +1246 -84
- package/dist/http/routes/mission-control-read.d.ts +48 -3
- package/dist/http/routes/mission-control-read.js +1658 -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.d.ts +3 -0
- package/dist/mcp-http-handler.js +34 -60
- 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 +36 -5
- package/dist/reporting/rollups.d.ts +41 -0
- package/dist/reporting/rollups.js +113 -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 +20 -6
- 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,27 +1,876 @@
|
|
|
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
|
+
const questionAutoAnswerPolicyByScope = new Map();
|
|
38
|
+
const pendingQuestionAutoAnswerByScope = new Map();
|
|
39
|
+
const QUESTION_AUTO_ANSWER_DEFAULT_TIMEOUT_SECONDS = readBudgetEnvNumber("ORGX_QUESTION_AUTO_ANSWER_TIMEOUT_SEC", readBudgetEnvNumber("ORGX_QUESTION_AUTO_ANSWER_DELAY_SECONDS", 60, {
|
|
40
|
+
min: 1,
|
|
41
|
+
max: 900,
|
|
42
|
+
}), { min: 1, max: 3600 });
|
|
43
|
+
const QUESTION_AUTO_ANSWER_DEFAULT_ENABLED = String(process.env.ORGX_QUESTION_AUTO_ANSWER_ENABLED ?? "true")
|
|
44
|
+
.trim()
|
|
45
|
+
.toLowerCase() !== "false";
|
|
46
|
+
const QUESTION_AUTO_ANSWER_DEFAULT_MODE = String(process.env.ORGX_QUESTION_AUTO_ANSWER_POLICY ?? "contextual")
|
|
47
|
+
.trim()
|
|
48
|
+
.toLowerCase() === "approve_non_blocking"
|
|
49
|
+
? "approve_non_blocking"
|
|
50
|
+
: String(process.env.ORGX_QUESTION_AUTO_ANSWER_POLICY ?? "contextual")
|
|
51
|
+
.trim()
|
|
52
|
+
.toLowerCase() === "defer_non_blocking"
|
|
53
|
+
? "defer_non_blocking"
|
|
54
|
+
: "contextual";
|
|
55
|
+
const QUESTION_BLOCKING_BEHAVIOR_DEFAULT = String(process.env.ORGX_QUESTION_BLOCKING_BEHAVIOR ?? "require_human")
|
|
56
|
+
.trim()
|
|
57
|
+
.toLowerCase() === "guarded_auto_resolve_then_human"
|
|
58
|
+
? "guarded_auto_resolve_then_human"
|
|
59
|
+
: "require_human";
|
|
60
|
+
const QUESTION_AUTO_ANSWER_DEFAULT_ACTION = String(process.env.ORGX_QUESTION_AUTO_ANSWER_ACTION ?? "approve")
|
|
61
|
+
.trim()
|
|
62
|
+
.toLowerCase() === "reject"
|
|
63
|
+
? "reject"
|
|
64
|
+
: "approve";
|
|
65
|
+
const autoContinueSliceRuns = new Map();
|
|
66
|
+
/** Spread into any metadata object to flag mock-worker activity. */
|
|
67
|
+
function mockMeta(slice) {
|
|
68
|
+
return slice.isMockWorker ? { mock: true } : {};
|
|
69
|
+
}
|
|
70
|
+
function normalizeRuntimeSourceClient(value) {
|
|
71
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
72
|
+
if (!normalized)
|
|
73
|
+
return "unknown";
|
|
74
|
+
if (normalized === "codex")
|
|
75
|
+
return "codex";
|
|
76
|
+
if (normalized === "claude-code" || normalized === "claude_code")
|
|
77
|
+
return "claude-code";
|
|
78
|
+
if (normalized === "openclaw")
|
|
79
|
+
return "openclaw";
|
|
80
|
+
if (normalized === "api")
|
|
81
|
+
return "api";
|
|
82
|
+
return "unknown";
|
|
83
|
+
}
|
|
84
|
+
const normalizeQuestionAutoAnswerPolicy = (runtimeSettings) => {
|
|
85
|
+
const workspaceDefaults = runtimeSettings?.workspace_question_defaults &&
|
|
86
|
+
typeof runtimeSettings.workspace_question_defaults === "object"
|
|
87
|
+
? runtimeSettings.workspace_question_defaults
|
|
88
|
+
: null;
|
|
89
|
+
const enabledRaw = runtimeSettings?.question_auto_answer_enabled;
|
|
90
|
+
const timeoutRaw = runtimeSettings?.question_auto_answer_timeout_sec ??
|
|
91
|
+
runtimeSettings?.question_auto_answer_delay_seconds;
|
|
92
|
+
const workspaceTimeoutRaw = workspaceDefaults?.question_auto_answer_timeout_sec ??
|
|
93
|
+
workspaceDefaults
|
|
94
|
+
?.question_auto_answer_delay_seconds;
|
|
95
|
+
const actionRaw = runtimeSettings?.question_auto_answer_action;
|
|
96
|
+
const modeRaw = runtimeSettings?.question_auto_answer_policy;
|
|
97
|
+
const workspaceModeRaw = workspaceDefaults?.question_auto_answer_policy;
|
|
98
|
+
const blockingBehaviorRaw = runtimeSettings?.question_blocking_behavior;
|
|
99
|
+
const workspaceBlockingBehaviorRaw = workspaceDefaults?.question_blocking_behavior;
|
|
100
|
+
const policyVersionRaw = runtimeSettings?.question_policy_version;
|
|
101
|
+
const timeoutSeconds = typeof timeoutRaw === "number" && Number.isFinite(timeoutRaw)
|
|
102
|
+
? Math.max(1, Math.min(3600, Math.floor(timeoutRaw)))
|
|
103
|
+
: typeof workspaceTimeoutRaw === "number" && Number.isFinite(workspaceTimeoutRaw)
|
|
104
|
+
? Math.max(1, Math.min(3600, Math.floor(workspaceTimeoutRaw)))
|
|
105
|
+
: QUESTION_AUTO_ANSWER_DEFAULT_TIMEOUT_SECONDS;
|
|
106
|
+
const mode = modeRaw === "approve_non_blocking" ||
|
|
107
|
+
modeRaw === "defer_non_blocking" ||
|
|
108
|
+
modeRaw === "contextual"
|
|
109
|
+
? modeRaw
|
|
110
|
+
: workspaceModeRaw === "approve_non_blocking" ||
|
|
111
|
+
workspaceModeRaw === "defer_non_blocking" ||
|
|
112
|
+
workspaceModeRaw === "contextual"
|
|
113
|
+
? workspaceModeRaw
|
|
114
|
+
: QUESTION_AUTO_ANSWER_DEFAULT_MODE;
|
|
115
|
+
const action = actionRaw === "reject" || actionRaw === "approve"
|
|
116
|
+
? actionRaw
|
|
117
|
+
: mode === "defer_non_blocking"
|
|
118
|
+
? "reject"
|
|
119
|
+
: QUESTION_AUTO_ANSWER_DEFAULT_ACTION;
|
|
120
|
+
const blockingBehavior = blockingBehaviorRaw === "guarded_auto_resolve_then_human" ||
|
|
121
|
+
blockingBehaviorRaw === "require_human"
|
|
122
|
+
? blockingBehaviorRaw
|
|
123
|
+
: workspaceBlockingBehaviorRaw === "guarded_auto_resolve_then_human" ||
|
|
124
|
+
workspaceBlockingBehaviorRaw === "require_human"
|
|
125
|
+
? workspaceBlockingBehaviorRaw
|
|
126
|
+
: QUESTION_BLOCKING_BEHAVIOR_DEFAULT;
|
|
127
|
+
const enabled = typeof enabledRaw === "boolean"
|
|
128
|
+
? enabledRaw
|
|
129
|
+
: typeof workspaceDefaults?.question_auto_answer_enabled === "boolean"
|
|
130
|
+
? workspaceDefaults.question_auto_answer_enabled
|
|
131
|
+
: QUESTION_AUTO_ANSWER_DEFAULT_ENABLED;
|
|
132
|
+
const policyVersion = typeof policyVersionRaw === "number" && Number.isFinite(policyVersionRaw)
|
|
133
|
+
? Math.max(1, Math.min(10, Math.floor(policyVersionRaw)))
|
|
134
|
+
: 1;
|
|
135
|
+
return {
|
|
136
|
+
enabled,
|
|
137
|
+
timeoutSeconds,
|
|
138
|
+
mode,
|
|
139
|
+
action,
|
|
140
|
+
blockingBehavior,
|
|
141
|
+
policyVersion,
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
const questionScopeKey = (initiativeId, workstreamId) => {
|
|
145
|
+
const normalizedInitiativeId = (initiativeId ?? "").trim() || "unknown_initiative";
|
|
146
|
+
const normalizedWorkstreamId = (workstreamId ?? "").trim() || "all_workstreams";
|
|
147
|
+
return `${normalizedInitiativeId}::${normalizedWorkstreamId}`;
|
|
148
|
+
};
|
|
149
|
+
const resolveQuestionPolicy = (initiativeId, workstreamId) => {
|
|
150
|
+
const scoped = questionAutoAnswerPolicyByScope.get(questionScopeKey(initiativeId, workstreamId));
|
|
151
|
+
if (scoped)
|
|
152
|
+
return scoped;
|
|
153
|
+
const initiativeWide = questionAutoAnswerPolicyByScope.get(questionScopeKey(initiativeId, null));
|
|
154
|
+
if (initiativeWide)
|
|
155
|
+
return initiativeWide;
|
|
156
|
+
return {
|
|
157
|
+
enabled: QUESTION_AUTO_ANSWER_DEFAULT_ENABLED,
|
|
158
|
+
timeoutSeconds: QUESTION_AUTO_ANSWER_DEFAULT_TIMEOUT_SECONDS,
|
|
159
|
+
mode: QUESTION_AUTO_ANSWER_DEFAULT_MODE,
|
|
160
|
+
action: QUESTION_AUTO_ANSWER_DEFAULT_ACTION,
|
|
161
|
+
blockingBehavior: QUESTION_BLOCKING_BEHAVIOR_DEFAULT,
|
|
162
|
+
policyVersion: 1,
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
const clearQuestionAutoAnswerStateForInitiative = (initiativeId) => {
|
|
166
|
+
const normalizedInitiativeId = (initiativeId ?? "").trim();
|
|
167
|
+
if (!normalizedInitiativeId)
|
|
168
|
+
return;
|
|
169
|
+
for (const [key, pending] of pendingQuestionAutoAnswerByScope.entries()) {
|
|
170
|
+
if ((pending.initiativeId ?? "").trim() !== normalizedInitiativeId)
|
|
171
|
+
continue;
|
|
172
|
+
if (pending.timer) {
|
|
173
|
+
clearTimeout(pending.timer);
|
|
174
|
+
}
|
|
175
|
+
pendingQuestionAutoAnswerByScope.delete(key);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const processQuestionAutoAnswer = async (key, pending) => {
|
|
179
|
+
pendingQuestionAutoAnswerByScope.delete(key);
|
|
180
|
+
const note = pending.action === "approve"
|
|
181
|
+
? "Auto-approved after timeout: no human answer received within configured delay."
|
|
182
|
+
: "Auto-rejected after timeout: no human answer received within configured delay.";
|
|
183
|
+
const decisionIds = pending.decisionIds;
|
|
184
|
+
if (decisionIds.length === 0) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
await emitActivitySafe({
|
|
188
|
+
initiativeId: pending.initiativeId,
|
|
189
|
+
runId: pending.sourceRunId,
|
|
190
|
+
correlationId: pending.sourceRunId,
|
|
191
|
+
phase: "review",
|
|
192
|
+
level: "info",
|
|
193
|
+
progressPct: 0,
|
|
194
|
+
nextStep: "Applying question answer policy to unresolved items.",
|
|
195
|
+
message: "Question auto-answered after timeout; applying decision updates.",
|
|
196
|
+
metadata: {
|
|
197
|
+
event: "question_auto_answered",
|
|
198
|
+
action_type: normalizeActivityActionType("question_auto_answered"),
|
|
199
|
+
action_phase: normalizeActivityActionPhase("review"),
|
|
200
|
+
initiative_id: pending.initiativeId,
|
|
201
|
+
workstream_id: pending.workstreamId,
|
|
202
|
+
source_run_id: pending.sourceRunId,
|
|
203
|
+
source_client: pending.sourceClient,
|
|
204
|
+
decision_ids: decisionIds,
|
|
205
|
+
decision_count: decisionIds.length,
|
|
206
|
+
decision_action: pending.action,
|
|
207
|
+
timeout_seconds_applied: pending.timeoutSeconds,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
let applied = 0;
|
|
211
|
+
let failed = 0;
|
|
212
|
+
const failures = [];
|
|
213
|
+
for (const decisionId of decisionIds) {
|
|
214
|
+
try {
|
|
215
|
+
await client.decideDecision(decisionId, pending.action, { note });
|
|
216
|
+
applied += 1;
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
failed += 1;
|
|
220
|
+
failures.push({
|
|
221
|
+
id: decisionId,
|
|
222
|
+
error: safeErrorMessage(err),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
await emitActivitySafe({
|
|
227
|
+
initiativeId: pending.initiativeId,
|
|
228
|
+
runId: pending.sourceRunId,
|
|
229
|
+
correlationId: pending.sourceRunId,
|
|
230
|
+
phase: failed > 0 ? "blocked" : "review",
|
|
231
|
+
level: failed > 0 ? "warn" : "info",
|
|
232
|
+
progressPct: 100,
|
|
233
|
+
nextStep: failed > 0
|
|
234
|
+
? "Review failed auto-answer decisions and resolve manually."
|
|
235
|
+
: "Decision queue was auto-resolved; run can continue.",
|
|
236
|
+
message: failed > 0
|
|
237
|
+
? `Question answers processed (${applied} applied, ${failed} failed).`
|
|
238
|
+
: `Question answer ${pending.action} applied to ${applied} queued items.`,
|
|
239
|
+
metadata: {
|
|
240
|
+
event: failed > 0 ? "question_answer_failed" : "question_answer_applied",
|
|
241
|
+
action_type: normalizeActivityActionType(failed > 0 ? "question_answer_failed" : "question_answer_applied"),
|
|
242
|
+
action_phase: normalizeActivityActionPhase(failed > 0 ? "blocked" : "review"),
|
|
243
|
+
initiative_id: pending.initiativeId,
|
|
244
|
+
workstream_id: pending.workstreamId,
|
|
245
|
+
source_run_id: pending.sourceRunId,
|
|
246
|
+
source_client: pending.sourceClient,
|
|
247
|
+
question_policy_mode: pending.mode,
|
|
248
|
+
question_policy_version: pending.policyVersion,
|
|
249
|
+
decision_action: pending.action,
|
|
250
|
+
decision_ids: decisionIds,
|
|
251
|
+
decision_count: decisionIds.length,
|
|
252
|
+
applied_count: applied,
|
|
253
|
+
failed_count: failed,
|
|
254
|
+
resolution_source: "policy_timeout",
|
|
255
|
+
timeout_seconds_applied: pending.timeoutSeconds,
|
|
256
|
+
failures,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
const armQuestionAutoAnswerTimer = (key, pending, delaySeconds) => {
|
|
261
|
+
if (pending.timer) {
|
|
262
|
+
clearTimeout(pending.timer);
|
|
263
|
+
}
|
|
264
|
+
pending.timer = setTimeout(() => {
|
|
265
|
+
void (async () => {
|
|
266
|
+
try {
|
|
267
|
+
await emitActivitySafe({
|
|
268
|
+
initiativeId: pending.initiativeId,
|
|
269
|
+
runId: pending.sourceRunId,
|
|
270
|
+
correlationId: pending.sourceRunId,
|
|
271
|
+
phase: "review",
|
|
272
|
+
level: "info",
|
|
273
|
+
progressPct: 100,
|
|
274
|
+
nextStep: "Applying configured decision action sequentially.",
|
|
275
|
+
message: "Question timeout reached; applying auto-answer policy.",
|
|
276
|
+
metadata: {
|
|
277
|
+
event: "question_timeout_started",
|
|
278
|
+
action_type: normalizeActivityActionType("question_timeout_started"),
|
|
279
|
+
action_phase: normalizeActivityActionPhase("review"),
|
|
280
|
+
initiative_id: pending.initiativeId,
|
|
281
|
+
workstream_id: pending.workstreamId,
|
|
282
|
+
source_run_id: pending.sourceRunId,
|
|
283
|
+
source_client: pending.sourceClient,
|
|
284
|
+
decision_ids: pending.decisionIds,
|
|
285
|
+
decision_count: pending.decisionIds.length,
|
|
286
|
+
decision_action: pending.action,
|
|
287
|
+
question_policy_mode: pending.mode,
|
|
288
|
+
question_policy_version: pending.policyVersion,
|
|
289
|
+
timeout_seconds_applied: pending.timeoutSeconds,
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
await processQuestionAutoAnswer(key, pending);
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
await emitActivitySafe({
|
|
296
|
+
initiativeId: pending.initiativeId,
|
|
297
|
+
runId: pending.sourceRunId,
|
|
298
|
+
correlationId: pending.sourceRunId,
|
|
299
|
+
phase: "blocked",
|
|
300
|
+
level: "warn",
|
|
301
|
+
progressPct: 100,
|
|
302
|
+
nextStep: "Review and resolve the queued question manually.",
|
|
303
|
+
message: "Question auto-answer failed before apply.",
|
|
304
|
+
metadata: {
|
|
305
|
+
event: "question_answer_failed",
|
|
306
|
+
action_type: normalizeActivityActionType("question_answer_failed"),
|
|
307
|
+
action_phase: normalizeActivityActionPhase("blocked"),
|
|
308
|
+
initiative_id: pending.initiativeId,
|
|
309
|
+
workstream_id: pending.workstreamId,
|
|
310
|
+
source_run_id: pending.sourceRunId,
|
|
311
|
+
source_client: pending.sourceClient,
|
|
312
|
+
decision_ids: pending.decisionIds,
|
|
313
|
+
decision_count: pending.decisionIds.length,
|
|
314
|
+
decision_action: pending.action,
|
|
315
|
+
failed_count: pending.decisionIds.length,
|
|
316
|
+
resolution_source: "policy_timeout",
|
|
317
|
+
timeout_seconds_applied: pending.timeoutSeconds,
|
|
318
|
+
question_policy_mode: pending.mode,
|
|
319
|
+
question_policy_version: pending.policyVersion,
|
|
320
|
+
error: safeErrorMessage(err),
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
})();
|
|
325
|
+
}, delaySeconds * 1_000);
|
|
326
|
+
pending.timer.unref?.();
|
|
327
|
+
};
|
|
328
|
+
const scheduleQuestionAutoAnswer = async (input) => {
|
|
329
|
+
const decisionIds = dedupeStrings(input.decisionIds
|
|
330
|
+
.map((entry) => (entry ?? "").trim())
|
|
331
|
+
.filter(Boolean));
|
|
332
|
+
if (decisionIds.length === 0)
|
|
333
|
+
return;
|
|
334
|
+
const policy = resolveQuestionPolicy(input.initiativeId, input.workstreamId);
|
|
335
|
+
await emitActivitySafe({
|
|
336
|
+
initiativeId: input.initiativeId,
|
|
337
|
+
runId: input.sourceRunId,
|
|
338
|
+
correlationId: input.sourceRunId,
|
|
339
|
+
phase: "review",
|
|
340
|
+
level: "info",
|
|
341
|
+
progressPct: 0,
|
|
342
|
+
nextStep: input.blocking
|
|
343
|
+
? "Blocking question requires human review."
|
|
344
|
+
: `Auto-answer in ${policy.timeoutSeconds}s unless human responds.`,
|
|
345
|
+
message: input.blocking
|
|
346
|
+
? "Blocking question surfaced for human decision."
|
|
347
|
+
: "Question surfaced and queued for timeout policy.",
|
|
348
|
+
metadata: {
|
|
349
|
+
event: "question_asked",
|
|
350
|
+
action_type: normalizeActivityActionType("question_asked"),
|
|
351
|
+
action_phase: normalizeActivityActionPhase("review"),
|
|
352
|
+
initiative_id: input.initiativeId,
|
|
353
|
+
workstream_id: input.workstreamId,
|
|
354
|
+
source_run_id: input.sourceRunId,
|
|
355
|
+
source_client: input.sourceClient,
|
|
356
|
+
decision_ids: decisionIds,
|
|
357
|
+
decision_count: decisionIds.length,
|
|
358
|
+
blocking: input.blocking,
|
|
359
|
+
question_policy_mode: policy.mode,
|
|
360
|
+
question_policy_version: policy.policyVersion,
|
|
361
|
+
timeout_seconds_applied: policy.timeoutSeconds,
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
if (input.blocking) {
|
|
365
|
+
await emitActivitySafe({
|
|
366
|
+
initiativeId: input.initiativeId,
|
|
367
|
+
runId: input.sourceRunId,
|
|
368
|
+
correlationId: input.sourceRunId,
|
|
369
|
+
phase: "blocked",
|
|
370
|
+
level: "info",
|
|
371
|
+
progressPct: 0,
|
|
372
|
+
nextStep: policy.blockingBehavior === "guarded_auto_resolve_then_human" &&
|
|
373
|
+
decisionAutoResolveGuardedEnabled
|
|
374
|
+
? "Awaiting guarded remediation and/or human decision."
|
|
375
|
+
: "Awaiting human decision response.",
|
|
376
|
+
message: policy.blockingBehavior === "guarded_auto_resolve_then_human" &&
|
|
377
|
+
decisionAutoResolveGuardedEnabled
|
|
378
|
+
? "Blocking question requires human decision after guarded remediation."
|
|
379
|
+
: "Blocking question requires human decision.",
|
|
380
|
+
metadata: {
|
|
381
|
+
event: "review_item_created",
|
|
382
|
+
action_type: normalizeActivityActionType("review_item_created"),
|
|
383
|
+
action_phase: normalizeActivityActionPhase("blocked"),
|
|
384
|
+
initiative_id: input.initiativeId,
|
|
385
|
+
workstream_id: input.workstreamId,
|
|
386
|
+
source_run_id: input.sourceRunId,
|
|
387
|
+
source_client: input.sourceClient,
|
|
388
|
+
decision_ids: decisionIds,
|
|
389
|
+
decision_count: decisionIds.length,
|
|
390
|
+
blocking: true,
|
|
391
|
+
reason: "blocking_question_requires_human",
|
|
392
|
+
question_policy_mode: policy.mode,
|
|
393
|
+
question_policy_version: policy.policyVersion,
|
|
394
|
+
question_blocking_behavior: policy.blockingBehavior,
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (!policy.enabled) {
|
|
400
|
+
await emitActivitySafe({
|
|
401
|
+
initiativeId: input.initiativeId,
|
|
402
|
+
runId: input.sourceRunId,
|
|
403
|
+
correlationId: input.sourceRunId,
|
|
404
|
+
phase: "review",
|
|
405
|
+
level: "info",
|
|
406
|
+
progressPct: 0,
|
|
407
|
+
nextStep: "Awaiting human decision response.",
|
|
408
|
+
message: "Question auto-answer is disabled for this agent policy.",
|
|
409
|
+
metadata: {
|
|
410
|
+
event: "review_item_created",
|
|
411
|
+
action_type: normalizeActivityActionType("review_item_created"),
|
|
412
|
+
action_phase: normalizeActivityActionPhase("review"),
|
|
413
|
+
initiative_id: input.initiativeId,
|
|
414
|
+
workstream_id: input.workstreamId,
|
|
415
|
+
source_run_id: input.sourceRunId,
|
|
416
|
+
source_client: input.sourceClient,
|
|
417
|
+
decision_ids: decisionIds,
|
|
418
|
+
reason: "policy_disabled",
|
|
419
|
+
question_policy_mode: policy.mode,
|
|
420
|
+
question_policy_version: policy.policyVersion,
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const key = questionScopeKey(input.initiativeId, input.workstreamId);
|
|
426
|
+
const dueAtEpoch = Date.now() + policy.timeoutSeconds * 1_000;
|
|
427
|
+
const existing = pendingQuestionAutoAnswerByScope.get(key);
|
|
428
|
+
if (existing) {
|
|
429
|
+
existing.decisionIds = dedupeStrings([...existing.decisionIds, ...decisionIds]);
|
|
430
|
+
existing.sourceRunId = input.sourceRunId ?? existing.sourceRunId;
|
|
431
|
+
existing.sourceClient = input.sourceClient || existing.sourceClient;
|
|
432
|
+
existing.action = policy.action;
|
|
433
|
+
existing.mode = policy.mode;
|
|
434
|
+
existing.policyVersion = policy.policyVersion;
|
|
435
|
+
existing.timeoutSeconds = policy.timeoutSeconds;
|
|
436
|
+
existing.dueAt = new Date(dueAtEpoch).toISOString();
|
|
437
|
+
armQuestionAutoAnswerTimer(key, existing, policy.timeoutSeconds);
|
|
438
|
+
await emitActivitySafe({
|
|
439
|
+
initiativeId: input.initiativeId,
|
|
440
|
+
runId: input.sourceRunId,
|
|
441
|
+
correlationId: input.sourceRunId,
|
|
442
|
+
phase: "review",
|
|
443
|
+
level: "info",
|
|
444
|
+
progressPct: 0,
|
|
445
|
+
nextStep: `Auto-answer in ${policy.timeoutSeconds}s unless human responds.`,
|
|
446
|
+
message: "Extended timeout for queued unanswered decision(s).",
|
|
447
|
+
metadata: {
|
|
448
|
+
event: "question_timeout_started",
|
|
449
|
+
action_type: normalizeActivityActionType("question_timeout_started"),
|
|
450
|
+
action_phase: normalizeActivityActionPhase("review"),
|
|
451
|
+
initiative_id: input.initiativeId,
|
|
452
|
+
workstream_id: input.workstreamId,
|
|
453
|
+
source_run_id: input.sourceRunId,
|
|
454
|
+
source_client: input.sourceClient,
|
|
455
|
+
decision_ids: existing.decisionIds,
|
|
456
|
+
decision_count: existing.decisionIds.length,
|
|
457
|
+
decision_action: existing.action,
|
|
458
|
+
timeout_seconds_applied: policy.timeoutSeconds,
|
|
459
|
+
question_policy_mode: policy.mode,
|
|
460
|
+
question_policy_version: policy.policyVersion,
|
|
461
|
+
due_at: existing.dueAt,
|
|
462
|
+
reason: input.reason,
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const pending = {
|
|
468
|
+
key,
|
|
469
|
+
initiativeId: input.initiativeId ?? "",
|
|
470
|
+
workstreamId: input.workstreamId ?? null,
|
|
471
|
+
sourceRunId: input.sourceRunId ?? null,
|
|
472
|
+
sourceClient: input.sourceClient,
|
|
473
|
+
action: policy.action,
|
|
474
|
+
mode: policy.mode,
|
|
475
|
+
policyVersion: policy.policyVersion,
|
|
476
|
+
timeoutSeconds: policy.timeoutSeconds,
|
|
477
|
+
dueAt: new Date(dueAtEpoch).toISOString(),
|
|
478
|
+
timer: null,
|
|
479
|
+
decisionIds,
|
|
480
|
+
};
|
|
481
|
+
armQuestionAutoAnswerTimer(key, pending, policy.timeoutSeconds);
|
|
482
|
+
pendingQuestionAutoAnswerByScope.set(key, pending);
|
|
483
|
+
await emitActivitySafe({
|
|
484
|
+
initiativeId: input.initiativeId,
|
|
485
|
+
runId: input.sourceRunId,
|
|
486
|
+
correlationId: input.sourceRunId,
|
|
487
|
+
phase: "review",
|
|
488
|
+
level: "info",
|
|
489
|
+
progressPct: 0,
|
|
490
|
+
nextStep: `Auto-answer in ${policy.timeoutSeconds}s unless human responds.`,
|
|
491
|
+
message: "Queued unanswered decision(s) for timeout auto-answer.",
|
|
492
|
+
metadata: {
|
|
493
|
+
event: "question_timeout_started",
|
|
494
|
+
action_type: normalizeActivityActionType("question_timeout_started"),
|
|
495
|
+
action_phase: normalizeActivityActionPhase("review"),
|
|
496
|
+
initiative_id: input.initiativeId,
|
|
497
|
+
workstream_id: input.workstreamId,
|
|
498
|
+
source_run_id: input.sourceRunId,
|
|
499
|
+
source_client: input.sourceClient,
|
|
500
|
+
decision_ids: decisionIds,
|
|
501
|
+
decision_count: decisionIds.length,
|
|
502
|
+
decision_action: policy.action,
|
|
503
|
+
timeout_seconds_applied: policy.timeoutSeconds,
|
|
504
|
+
question_policy_mode: policy.mode,
|
|
505
|
+
question_policy_version: policy.policyVersion,
|
|
506
|
+
due_at: pending.dueAt,
|
|
507
|
+
reason: input.reason,
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
};
|
|
511
|
+
const requestDecisionQueued = async (input) => {
|
|
512
|
+
const asRecord = (value) => {
|
|
513
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
514
|
+
return null;
|
|
515
|
+
return value;
|
|
516
|
+
};
|
|
517
|
+
const normalizeId = (value) => {
|
|
518
|
+
if (typeof value !== "string")
|
|
519
|
+
return null;
|
|
520
|
+
const trimmed = value.trim();
|
|
521
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
522
|
+
};
|
|
523
|
+
const normalizeLower = (value) => {
|
|
524
|
+
if (typeof value !== "string")
|
|
525
|
+
return "";
|
|
526
|
+
return value.trim().toLowerCase();
|
|
527
|
+
};
|
|
528
|
+
const recoverQueuedDecisionIds = async (recoverInput) => {
|
|
529
|
+
try {
|
|
530
|
+
const pending = await client.getLiveDecisions({ status: "pending", limit: 100 });
|
|
531
|
+
const rows = Array.isArray(pending?.decisions) ? pending.decisions : [];
|
|
532
|
+
const wantedTitle = normalizeLower(recoverInput.title);
|
|
533
|
+
const wantedRunId = normalizeLower(recoverInput.sourceRunId ?? "");
|
|
534
|
+
const wantedWorkstreamId = normalizeLower(recoverInput.workstreamId ?? "");
|
|
535
|
+
const recentThreshold = Date.now() - 10 * 60 * 1_000;
|
|
536
|
+
const ids = [];
|
|
537
|
+
const seen = new Set();
|
|
538
|
+
for (const row of rows) {
|
|
539
|
+
const record = asRecord(row);
|
|
540
|
+
if (!record)
|
|
541
|
+
continue;
|
|
542
|
+
const id = normalizeId(record.id) ??
|
|
543
|
+
normalizeId(record.entity_id) ??
|
|
544
|
+
normalizeId(record.decision_id);
|
|
545
|
+
if (!id || seen.has(id))
|
|
546
|
+
continue;
|
|
547
|
+
const metadata = asRecord(record.metadata);
|
|
548
|
+
const sourceRef = asRecord(record.source_ref) ?? asRecord(metadata?.source_ref);
|
|
549
|
+
const rowWorkstreamId = normalizeLower(record.workstream_id) ||
|
|
550
|
+
normalizeLower(record.workstreamId) ||
|
|
551
|
+
normalizeLower(metadata?.source_stream_id) ||
|
|
552
|
+
normalizeLower(sourceRef?.workstream_id) ||
|
|
553
|
+
normalizeLower(sourceRef?.stream_id);
|
|
554
|
+
const rowRunId = normalizeLower(record.source_run_id) ||
|
|
555
|
+
normalizeLower(record.sourceRunId) ||
|
|
556
|
+
normalizeLower(metadata?.run_id) ||
|
|
557
|
+
normalizeLower(metadata?.correlation_id) ||
|
|
558
|
+
normalizeLower(sourceRef?.run_id);
|
|
559
|
+
const rowTitle = normalizeLower(record.title) || normalizeLower(metadata?.title);
|
|
560
|
+
const updatedAtRaw = normalizeId(record.updated_at) ?? normalizeId(record.created_at);
|
|
561
|
+
const updatedAtEpoch = updatedAtRaw ? Date.parse(updatedAtRaw) : NaN;
|
|
562
|
+
const recentEnough = !Number.isFinite(updatedAtEpoch) || updatedAtEpoch >= recentThreshold;
|
|
563
|
+
const workstreamMatches = !wantedWorkstreamId || rowWorkstreamId === wantedWorkstreamId;
|
|
564
|
+
const runMatches = Boolean(wantedRunId) && rowRunId === wantedRunId;
|
|
565
|
+
const titleMatches = Boolean(wantedTitle) && rowTitle === wantedTitle;
|
|
566
|
+
if (!workstreamMatches)
|
|
567
|
+
continue;
|
|
568
|
+
if (!(runMatches || (titleMatches && recentEnough)))
|
|
569
|
+
continue;
|
|
570
|
+
seen.add(id);
|
|
571
|
+
ids.push(id);
|
|
572
|
+
}
|
|
573
|
+
return ids;
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
return [];
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
const inferredRunId = (typeof input.sourceRunId === "string" && input.sourceRunId.trim().length > 0
|
|
580
|
+
? input.sourceRunId.trim()
|
|
581
|
+
: null) ??
|
|
582
|
+
(typeof input.correlationId === "string" && input.correlationId.trim().length > 0
|
|
583
|
+
? input.correlationId.trim()
|
|
584
|
+
: null);
|
|
585
|
+
const inferredSessionId = (typeof input.sourceSessionId === "string" && input.sourceSessionId.trim().length > 0
|
|
586
|
+
? input.sourceSessionId.trim()
|
|
587
|
+
: null) ?? inferredRunId;
|
|
588
|
+
const inferredStreamId = (typeof input.sourceStreamId === "string" && input.sourceStreamId.trim().length > 0
|
|
589
|
+
? input.sourceStreamId.trim()
|
|
590
|
+
: null) ??
|
|
591
|
+
(typeof input.workstreamId === "string" && input.workstreamId.trim().length > 0
|
|
592
|
+
? input.workstreamId.trim()
|
|
593
|
+
: null);
|
|
594
|
+
const sourceRefBase = input.sourceRef && typeof input.sourceRef === "object" && !Array.isArray(input.sourceRef)
|
|
595
|
+
? input.sourceRef
|
|
596
|
+
: {};
|
|
597
|
+
const metadataBase = input.metadata && typeof input.metadata === "object" && !Array.isArray(input.metadata)
|
|
598
|
+
? input.metadata
|
|
599
|
+
: {};
|
|
600
|
+
const metadataSourceClient = (typeof metadataBase.source_client === "string" && metadataBase.source_client.trim().length > 0
|
|
601
|
+
? metadataBase.source_client.trim()
|
|
602
|
+
: null) ??
|
|
603
|
+
(typeof metadataBase.sourceClient === "string" && metadataBase.sourceClient.trim().length > 0
|
|
604
|
+
? metadataBase.sourceClient.trim()
|
|
605
|
+
: null);
|
|
606
|
+
const inferredSourceClient = normalizeRuntimeSourceClient(metadataSourceClient ??
|
|
607
|
+
process.env.ORGX_AUTOPILOT_EXECUTOR ??
|
|
608
|
+
process.env.ORGX_AUTOPILOT_WORKER_KIND);
|
|
609
|
+
const normalizedInput = {
|
|
610
|
+
...input,
|
|
611
|
+
sourceRunId: inferredRunId,
|
|
612
|
+
sourceSessionId: inferredSessionId,
|
|
613
|
+
sourceStreamId: inferredStreamId,
|
|
614
|
+
sourceRef: {
|
|
615
|
+
...sourceRefBase,
|
|
616
|
+
run_id: sourceRefBase.run_id ?? inferredRunId,
|
|
617
|
+
session_id: sourceRefBase.session_id ?? inferredSessionId,
|
|
618
|
+
stream_id: sourceRefBase.stream_id ?? inferredStreamId,
|
|
619
|
+
workstream_id: sourceRefBase.workstream_id ?? input.workstreamId ?? null,
|
|
620
|
+
source_client: sourceRefBase.source_client ??
|
|
621
|
+
sourceRefBase.sourceClient ??
|
|
622
|
+
inferredSourceClient,
|
|
623
|
+
},
|
|
624
|
+
metadata: {
|
|
625
|
+
...metadataBase,
|
|
626
|
+
source_system: input.sourceSystem ?? null,
|
|
627
|
+
conflict_source: input.conflictSource ?? null,
|
|
628
|
+
source_client: metadataBase.source_client ??
|
|
629
|
+
metadataBase.sourceClient ??
|
|
630
|
+
inferredSourceClient,
|
|
631
|
+
},
|
|
632
|
+
};
|
|
633
|
+
const linkedSlice = inferredRunId ? autoContinueSliceRuns.get(inferredRunId) ?? null : null;
|
|
634
|
+
const sourceClientFromInput = typeof normalizedInput.metadata?.source_client === "string"
|
|
635
|
+
? normalizedInput.metadata.source_client
|
|
636
|
+
: null;
|
|
637
|
+
const sourceClient = sourceClientFromInput ??
|
|
638
|
+
linkedSlice?.sourceClient ??
|
|
639
|
+
"unknown";
|
|
640
|
+
const scopedWorkstreamId = ((typeof normalizedInput.workstreamId === "string" &&
|
|
641
|
+
normalizedInput.workstreamId.trim().length > 0
|
|
642
|
+
? normalizedInput.workstreamId.trim()
|
|
643
|
+
: null) ??
|
|
644
|
+
inferredStreamId ??
|
|
645
|
+
linkedSlice?.workstreamId ??
|
|
646
|
+
null);
|
|
647
|
+
const result = await requestDecisionSafe(normalizedInput);
|
|
648
|
+
if (typeof result === "boolean") {
|
|
649
|
+
return { queued: result, decisionIds: [] };
|
|
650
|
+
}
|
|
651
|
+
if (result && typeof result === "object" && "queued" in result) {
|
|
652
|
+
const record = result;
|
|
653
|
+
let decisionIds = Array.isArray(record.decisionIds)
|
|
654
|
+
? record.decisionIds
|
|
655
|
+
.filter((entry) => typeof entry === "string")
|
|
656
|
+
.map((entry) => entry.trim())
|
|
657
|
+
.filter(Boolean)
|
|
658
|
+
: [];
|
|
659
|
+
if (Boolean(record.queued) && decisionIds.length === 0) {
|
|
660
|
+
decisionIds = await recoverQueuedDecisionIds({
|
|
661
|
+
title: normalizedInput.title,
|
|
662
|
+
sourceRunId: inferredRunId,
|
|
663
|
+
workstreamId: scopedWorkstreamId,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
if (Boolean(record.queued) && decisionIds.length > 0) {
|
|
667
|
+
await scheduleQuestionAutoAnswer({
|
|
668
|
+
initiativeId: normalizedInput.initiativeId,
|
|
669
|
+
workstreamId: scopedWorkstreamId,
|
|
670
|
+
sourceRunId: inferredRunId,
|
|
671
|
+
sourceClient,
|
|
672
|
+
decisionIds,
|
|
673
|
+
blocking: Boolean(normalizedInput.blocking),
|
|
674
|
+
reason: typeof normalizedInput.conflictSource === "string"
|
|
675
|
+
? normalizedInput.conflictSource
|
|
676
|
+
: null,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
queued: Boolean(record.queued),
|
|
681
|
+
decisionIds,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
return { queued: false, decisionIds: [] };
|
|
685
|
+
};
|
|
16
686
|
const __filename = deps.filename;
|
|
17
687
|
const autoContinueRuns = new Map();
|
|
18
688
|
const localInitiativeStatusOverrides = new Map();
|
|
689
|
+
const localTaskStatusOverrides = new Map();
|
|
690
|
+
const localMilestoneStatusOverrides = new Map();
|
|
19
691
|
let autoContinueTickInFlight = null;
|
|
20
692
|
const AUTO_CONTINUE_TICK_MS = readBudgetEnvNumber("ORGX_AUTO_CONTINUE_TICK_MS", 2_500, {
|
|
21
693
|
min: 250,
|
|
22
694
|
max: 60_000,
|
|
23
695
|
});
|
|
24
|
-
const
|
|
696
|
+
const AUTO_CONTINUE_PARALLEL_MIN = 1;
|
|
697
|
+
const AUTO_CONTINUE_PARALLEL_MAX = 5;
|
|
698
|
+
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 }))));
|
|
699
|
+
const normalizeParallelMode = (_value) => "iwmt";
|
|
700
|
+
const normalizeMaxParallelSlices = (value, fallback) => {
|
|
701
|
+
const normalizedFallback = Math.max(AUTO_CONTINUE_PARALLEL_MIN, Math.min(AUTO_CONTINUE_PARALLEL_MAX, Math.round(fallback || AUTO_CONTINUE_PARALLEL_MIN)));
|
|
702
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
703
|
+
const parsed = Math.round(value);
|
|
704
|
+
return Math.max(AUTO_CONTINUE_PARALLEL_MIN, Math.min(AUTO_CONTINUE_PARALLEL_MAX, parsed));
|
|
705
|
+
}
|
|
706
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
707
|
+
const parsed = Number(value);
|
|
708
|
+
if (Number.isFinite(parsed)) {
|
|
709
|
+
const rounded = Math.round(parsed);
|
|
710
|
+
return Math.max(AUTO_CONTINUE_PARALLEL_MIN, Math.min(AUTO_CONTINUE_PARALLEL_MAX, rounded));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return normalizedFallback;
|
|
714
|
+
};
|
|
715
|
+
const buildSliceEnrichment = (input) => {
|
|
716
|
+
const eventName = typeof input.event === "string" && input.event.trim().length > 0
|
|
717
|
+
? input.event.trim().toLowerCase()
|
|
718
|
+
: null;
|
|
719
|
+
const inferredActionType = (() => {
|
|
720
|
+
if (!eventName)
|
|
721
|
+
return "execute_task";
|
|
722
|
+
if (eventName === "orchestrator_dispatch")
|
|
723
|
+
return "orchestrator_dispatch";
|
|
724
|
+
if (eventName.includes("slice_dispatched"))
|
|
725
|
+
return "dispatch_slice";
|
|
726
|
+
if (eventName.includes("slice_started") || eventName === "session_start") {
|
|
727
|
+
return "run_started";
|
|
728
|
+
}
|
|
729
|
+
if (eventName.includes("slice_heartbeat") || eventName === "heartbeat") {
|
|
730
|
+
return "run_heartbeat";
|
|
731
|
+
}
|
|
732
|
+
if (eventName.includes("slice_handoff"))
|
|
733
|
+
return "slice_handoff";
|
|
734
|
+
if (eventName.includes("spawn_guard_rate_limited"))
|
|
735
|
+
return "spawn_guard_rate_limited";
|
|
736
|
+
if (eventName.includes("spawn_guard_blocked"))
|
|
737
|
+
return "spawn_guard_blocked";
|
|
738
|
+
if (eventName.includes("status_updates_buffered"))
|
|
739
|
+
return "status_updates_buffered";
|
|
740
|
+
if (eventName.includes("status_updates"))
|
|
741
|
+
return "status_updates_applied";
|
|
742
|
+
if (eventName.includes("artifact_registered"))
|
|
743
|
+
return "artifact_registered";
|
|
744
|
+
if (eventName.includes("question_asked"))
|
|
745
|
+
return "question_asked";
|
|
746
|
+
if (eventName.includes("question_timeout_started"))
|
|
747
|
+
return "question_timeout_started";
|
|
748
|
+
if (eventName.includes("question_auto_answered"))
|
|
749
|
+
return "question_auto_answered";
|
|
750
|
+
if (eventName.includes("question_answer_applied"))
|
|
751
|
+
return "question_answer_applied";
|
|
752
|
+
if (eventName.includes("question_answer_failed"))
|
|
753
|
+
return "question_answer_failed";
|
|
754
|
+
if (eventName.includes("review_item_created"))
|
|
755
|
+
return "review_item_created";
|
|
756
|
+
if (eventName.includes("review_item_resolved"))
|
|
757
|
+
return "review_item_resolved";
|
|
758
|
+
if (eventName.includes("decision_requested"))
|
|
759
|
+
return "decision_requested";
|
|
760
|
+
if (eventName.includes("decision_resolved"))
|
|
761
|
+
return "decision_resolved";
|
|
762
|
+
if (eventName === "auto_continue_started")
|
|
763
|
+
return "auto_continue_started";
|
|
764
|
+
if (eventName === "auto_continue_stopped")
|
|
765
|
+
return "auto_continue_stopped";
|
|
766
|
+
if (eventName.includes("behavior_config") || eventName.includes("behavior_automation")) {
|
|
767
|
+
return "behavior_config_review";
|
|
768
|
+
}
|
|
769
|
+
if (eventName.includes("transition"))
|
|
770
|
+
return "run_state_transition";
|
|
771
|
+
if (eventName.includes("auto_fix"))
|
|
772
|
+
return "auto_fix";
|
|
773
|
+
if (eventName.includes("milestone_completed"))
|
|
774
|
+
return "milestone_completed";
|
|
775
|
+
if (eventName.includes("error") || eventName.includes("failed"))
|
|
776
|
+
return "run_failed";
|
|
777
|
+
if (eventName.includes("result") || eventName.includes("completed"))
|
|
778
|
+
return "run_completed";
|
|
779
|
+
return eventName.replace(/[^a-z0-9]+/g, "_");
|
|
780
|
+
})();
|
|
781
|
+
const actionType = normalizeActivityActionType(input.actionType ?? inferredActionType) ?? inferredActionType;
|
|
782
|
+
const inferredActionPhase = (() => {
|
|
783
|
+
if (!eventName)
|
|
784
|
+
return "execution";
|
|
785
|
+
if (eventName === "orchestrator_dispatch" || eventName.includes("slice_dispatched")) {
|
|
786
|
+
return "dispatch";
|
|
787
|
+
}
|
|
788
|
+
if (eventName.includes("handoff"))
|
|
789
|
+
return "handoff";
|
|
790
|
+
if (eventName.includes("heartbeat"))
|
|
791
|
+
return "execution";
|
|
792
|
+
if (eventName.includes("decision_"))
|
|
793
|
+
return "review";
|
|
794
|
+
if (eventName.includes("blocked") ||
|
|
795
|
+
eventName.includes("rate_limited") ||
|
|
796
|
+
eventName.includes("stall") ||
|
|
797
|
+
eventName.includes("timeout")) {
|
|
798
|
+
return "blocked";
|
|
799
|
+
}
|
|
800
|
+
if (eventName.includes("error") || eventName.includes("failed"))
|
|
801
|
+
return "error";
|
|
802
|
+
if (eventName.includes("result") || eventName.includes("completed") || eventName === "auto_continue_stopped") {
|
|
803
|
+
return "completed";
|
|
804
|
+
}
|
|
805
|
+
if (eventName === "auto_continue_started")
|
|
806
|
+
return "intent";
|
|
807
|
+
return "execution";
|
|
808
|
+
})();
|
|
809
|
+
const actionPhase = normalizeActivityActionPhase(input.actionPhase ?? inferredActionPhase) ??
|
|
810
|
+
inferredActionPhase;
|
|
811
|
+
const workstreamId = (input.workstreamId ?? input.slice?.workstreamId ?? "").trim() || null;
|
|
812
|
+
const taskId = (input.taskId ?? input.slice?.taskIds?.[0] ?? "").trim() || null;
|
|
813
|
+
const requiredSkills = Array.isArray(input.requiredSkills)
|
|
814
|
+
? input.requiredSkills
|
|
815
|
+
: input.slice?.requiredSkills ?? null;
|
|
816
|
+
return {
|
|
817
|
+
event: input.event ?? null,
|
|
818
|
+
action_type: actionType,
|
|
819
|
+
action_phase: actionPhase,
|
|
820
|
+
initiative_id: input.run.initiativeId,
|
|
821
|
+
requested_by_agent_id: input.run.agentId,
|
|
822
|
+
requested_by_agent_name: input.run.agentName,
|
|
823
|
+
requester_agent_id: input.run.agentId,
|
|
824
|
+
requester_agent_name: input.run.agentName,
|
|
825
|
+
agent_id: input.slice?.agentId ?? null,
|
|
826
|
+
agent_name: input.slice?.agentName ?? null,
|
|
827
|
+
executor_agent_id: input.slice?.agentId ?? null,
|
|
828
|
+
executor_agent_name: input.slice?.agentName ?? null,
|
|
829
|
+
source_run_id: input.slice?.runId ?? null,
|
|
830
|
+
source_session_id: input.slice?.runId ?? null,
|
|
831
|
+
source_stream_id: workstreamId,
|
|
832
|
+
run_id: input.slice?.runId ?? null,
|
|
833
|
+
slice_run_id: input.slice?.runId ?? null,
|
|
834
|
+
correlation_id: input.slice?.runId ?? null,
|
|
835
|
+
source_client: input.slice?.sourceClient ?? "unknown",
|
|
836
|
+
runtime_client: input.slice?.sourceClient ?? "unknown",
|
|
837
|
+
workstream_id: workstreamId,
|
|
838
|
+
workstream_title: input.workstreamTitle ?? input.slice?.workstreamTitle ?? null,
|
|
839
|
+
task_id: taskId,
|
|
840
|
+
task_title: input.taskTitle ?? null,
|
|
841
|
+
milestone_ids: input.slice?.milestoneIds ?? null,
|
|
842
|
+
task_ids: input.slice?.taskIds ?? null,
|
|
843
|
+
domain: input.domain ?? input.slice?.domain ?? null,
|
|
844
|
+
required_skills: requiredSkills,
|
|
845
|
+
skill_pack: requiredSkills,
|
|
846
|
+
model_tier: input.modelTier ?? null,
|
|
847
|
+
scope: input.slice?.scope ?? input.run.scope,
|
|
848
|
+
actors: {
|
|
849
|
+
requester: {
|
|
850
|
+
agent_id: input.run.agentId ?? null,
|
|
851
|
+
agent_name: input.run.agentName ?? null,
|
|
852
|
+
},
|
|
853
|
+
dispatcher: {
|
|
854
|
+
agent_id: input.run.agentId ?? null,
|
|
855
|
+
agent_name: input.run.agentName ?? null,
|
|
856
|
+
},
|
|
857
|
+
executor: {
|
|
858
|
+
agent_id: input.slice?.agentId ?? null,
|
|
859
|
+
agent_name: input.slice?.agentName ?? null,
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
scope_context: {
|
|
863
|
+
initiative_id: input.run.initiativeId,
|
|
864
|
+
workstream_id: workstreamId,
|
|
865
|
+
task_id: taskId,
|
|
866
|
+
task_ids: input.slice?.taskIds ?? null,
|
|
867
|
+
milestone_ids: input.slice?.milestoneIds ?? null,
|
|
868
|
+
},
|
|
869
|
+
next_actions: input.nextActions ?? null,
|
|
870
|
+
user_summary: input.userSummary ?? null,
|
|
871
|
+
...(input.extra ?? {}),
|
|
872
|
+
};
|
|
873
|
+
};
|
|
25
874
|
// Keep child handles alive so stdout/stderr capture remains reliable even when the process is detached.
|
|
26
875
|
const autoContinueSliceChildren = new Map();
|
|
27
876
|
const autoContinueSliceLastHeartbeatMs = new Map();
|
|
@@ -32,7 +881,6 @@ export function createAutoContinueEngine(deps) {
|
|
|
32
881
|
autoContinueSliceChildren.delete(id);
|
|
33
882
|
autoContinueSliceLastHeartbeatMs.delete(id);
|
|
34
883
|
};
|
|
35
|
-
const AUTO_CONTINUE_SLICE_MAX_TASKS = 6;
|
|
36
884
|
const AUTO_CONTINUE_SLICE_TIMEOUT_MS = readBudgetEnvNumber("ORGX_AUTOPILOT_SLICE_TIMEOUT_MS", 55 * 60_000,
|
|
37
885
|
// Keep test runs fast; real-world defaults are still ~1h unless overridden.
|
|
38
886
|
{ min: 250, max: 6 * 60 * 60_000 });
|
|
@@ -42,6 +890,302 @@ export function createAutoContinueEngine(deps) {
|
|
|
42
890
|
const AUTO_CONTINUE_SLICE_HEARTBEAT_MS = 12_000;
|
|
43
891
|
const AUTO_CONTINUE_SLICE_SCHEMA_FILENAME = "autopilot-slice-schema.json";
|
|
44
892
|
const AUTO_CONTINUE_SLICE_LOG_DIRNAME = "autopilot-logs";
|
|
893
|
+
// Prune old autopilot logs on engine init (7-day TTL, 50 MB cap).
|
|
894
|
+
const AUTOPILOT_LOG_TTL_MS = 7 * 24 * 60 * 60_000;
|
|
895
|
+
const AUTOPILOT_LOG_MAX_BYTES = 50 * 1024 * 1024;
|
|
896
|
+
(async () => {
|
|
897
|
+
try {
|
|
898
|
+
const logsDir = join(getOrgxPluginConfigDir(), AUTO_CONTINUE_SLICE_LOG_DIRNAME);
|
|
899
|
+
if (!existsSync(logsDir))
|
|
900
|
+
return;
|
|
901
|
+
const entries = await readdir(logsDir);
|
|
902
|
+
const now = Date.now();
|
|
903
|
+
const fileStats = [];
|
|
904
|
+
for (const name of entries) {
|
|
905
|
+
if (!name.endsWith(".log") && !name.endsWith(".output.json"))
|
|
906
|
+
continue;
|
|
907
|
+
const filePath = join(logsDir, name);
|
|
908
|
+
try {
|
|
909
|
+
const s = await stat(filePath);
|
|
910
|
+
if (s.mtimeMs < now - AUTOPILOT_LOG_TTL_MS) {
|
|
911
|
+
await unlink(filePath);
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
fileStats.push({ name, path: filePath, mtimeMs: s.mtimeMs, size: s.size });
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
catch { /* skip */ }
|
|
918
|
+
}
|
|
919
|
+
// Enforce total size cap by deleting oldest first.
|
|
920
|
+
fileStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
921
|
+
let totalSize = fileStats.reduce((sum, f) => sum + f.size, 0);
|
|
922
|
+
for (const f of fileStats) {
|
|
923
|
+
if (totalSize <= AUTOPILOT_LOG_MAX_BYTES)
|
|
924
|
+
break;
|
|
925
|
+
try {
|
|
926
|
+
await unlink(f.path);
|
|
927
|
+
}
|
|
928
|
+
catch { /* skip */ }
|
|
929
|
+
totalSize -= f.size;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
catch { /* best effort */ }
|
|
933
|
+
})();
|
|
934
|
+
const AUTO_FIX_DEFAULT_GRACE_MS = readBudgetEnvNumber("ORGX_AUTOPILOT_AUTOFIX_GRACE_MS", 10_000, { min: 1_000, max: 120_000 });
|
|
935
|
+
const AUTO_CONTINUE_SPAWN_GUARD_RETRY_MS = readBudgetEnvNumber("ORGX_AUTO_CONTINUE_SPAWN_GUARD_RETRY_MS", 15_000, { min: 1_000, max: 15 * 60_000 });
|
|
936
|
+
const autoFixByScope = new Map();
|
|
937
|
+
const autoContinueSpawnGuardRetryByTask = new Map();
|
|
938
|
+
const getSpawnGuardRetryAtMs = (initiativeId, taskId) => {
|
|
939
|
+
const taskKey = taskId.trim();
|
|
940
|
+
if (!taskKey)
|
|
941
|
+
return 0;
|
|
942
|
+
const entry = autoContinueSpawnGuardRetryByTask.get(taskKey);
|
|
943
|
+
if (!entry)
|
|
944
|
+
return 0;
|
|
945
|
+
if (entry.initiativeId !== initiativeId || entry.retryAtMs <= Date.now()) {
|
|
946
|
+
autoContinueSpawnGuardRetryByTask.delete(taskKey);
|
|
947
|
+
return 0;
|
|
948
|
+
}
|
|
949
|
+
return entry.retryAtMs;
|
|
950
|
+
};
|
|
951
|
+
const clearSpawnGuardRetryStateForInitiative = (initiativeId) => {
|
|
952
|
+
for (const [taskId, entry] of autoContinueSpawnGuardRetryByTask.entries()) {
|
|
953
|
+
if (entry.initiativeId !== initiativeId)
|
|
954
|
+
continue;
|
|
955
|
+
autoContinueSpawnGuardRetryByTask.delete(taskId);
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
const normalizeStatusValue = (value) => {
|
|
959
|
+
if (typeof value !== "string")
|
|
960
|
+
return "";
|
|
961
|
+
return value.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
962
|
+
};
|
|
963
|
+
const listActiveSliceRunIds = (run) => {
|
|
964
|
+
ensureRunInternals(run);
|
|
965
|
+
const ids = new Set();
|
|
966
|
+
for (const id of run.activeSliceRunIds ?? []) {
|
|
967
|
+
const normalized = (id ?? "").trim();
|
|
968
|
+
if (normalized)
|
|
969
|
+
ids.add(normalized);
|
|
970
|
+
}
|
|
971
|
+
for (const lane of Object.values(run.laneByWorkstreamId ?? {})) {
|
|
972
|
+
const activeRunId = (lane.activeRunId ?? "").trim();
|
|
973
|
+
if (activeRunId)
|
|
974
|
+
ids.add(activeRunId);
|
|
975
|
+
}
|
|
976
|
+
return Array.from(ids);
|
|
977
|
+
};
|
|
978
|
+
const upsertLane = (run, workstreamId, patch) => {
|
|
979
|
+
const normalizedWorkstreamId = workstreamId.trim();
|
|
980
|
+
if (!normalizedWorkstreamId) {
|
|
981
|
+
throw new Error("workstreamId is required");
|
|
982
|
+
}
|
|
983
|
+
const existing = run.laneByWorkstreamId[normalizedWorkstreamId] ?? {
|
|
984
|
+
workstreamId: normalizedWorkstreamId,
|
|
985
|
+
state: "idle",
|
|
986
|
+
activeRunId: null,
|
|
987
|
+
activeTaskIds: [],
|
|
988
|
+
blockedReason: null,
|
|
989
|
+
waitingOnWorkstreamIds: [],
|
|
990
|
+
retryAt: null,
|
|
991
|
+
updatedAt: new Date().toISOString(),
|
|
992
|
+
};
|
|
993
|
+
const next = {
|
|
994
|
+
...existing,
|
|
995
|
+
...patch,
|
|
996
|
+
workstreamId: normalizedWorkstreamId,
|
|
997
|
+
updatedAt: patch.updatedAt ?? new Date().toISOString(),
|
|
998
|
+
activeTaskIds: Array.isArray(patch.activeTaskIds)
|
|
999
|
+
? dedupeStrings(patch.activeTaskIds.map((id) => (id ?? "").trim()).filter(Boolean))
|
|
1000
|
+
: existing.activeTaskIds,
|
|
1001
|
+
waitingOnWorkstreamIds: Array.isArray(patch.waitingOnWorkstreamIds)
|
|
1002
|
+
? dedupeStrings(patch.waitingOnWorkstreamIds.map((id) => (id ?? "").trim()).filter(Boolean))
|
|
1003
|
+
: existing.waitingOnWorkstreamIds,
|
|
1004
|
+
};
|
|
1005
|
+
run.laneByWorkstreamId[normalizedWorkstreamId] = next;
|
|
1006
|
+
return next;
|
|
1007
|
+
};
|
|
1008
|
+
const setLaneState = (run, input) => {
|
|
1009
|
+
return upsertLane(run, input.workstreamId, {
|
|
1010
|
+
state: input.state,
|
|
1011
|
+
activeRunId: input.activeRunId === undefined ? undefined : (input.activeRunId ?? "").trim() || null,
|
|
1012
|
+
activeTaskIds: input.activeTaskIds,
|
|
1013
|
+
blockedReason: input.blockedReason === undefined
|
|
1014
|
+
? undefined
|
|
1015
|
+
: (input.blockedReason ?? "").trim() || null,
|
|
1016
|
+
waitingOnWorkstreamIds: input.waitingOnWorkstreamIds,
|
|
1017
|
+
retryAt: input.retryAt === undefined ? undefined : input.retryAt,
|
|
1018
|
+
});
|
|
1019
|
+
};
|
|
1020
|
+
const removeActiveSliceFromRun = (run, input) => {
|
|
1021
|
+
const sliceRunId = input.sliceRunId.trim();
|
|
1022
|
+
if (!sliceRunId)
|
|
1023
|
+
return;
|
|
1024
|
+
run.activeSliceRunIds = run.activeSliceRunIds.filter((id) => id !== sliceRunId);
|
|
1025
|
+
const taskIds = new Set(Array.isArray(input.taskIds)
|
|
1026
|
+
? input.taskIds.map((id) => (id ?? "").trim()).filter(Boolean)
|
|
1027
|
+
: []);
|
|
1028
|
+
if (taskIds.size > 0) {
|
|
1029
|
+
run.activeTaskIds = run.activeTaskIds.filter((id) => !taskIds.has(id));
|
|
1030
|
+
}
|
|
1031
|
+
const normalizedWorkstreamId = (input.workstreamId ?? "").trim();
|
|
1032
|
+
if (normalizedWorkstreamId) {
|
|
1033
|
+
const lane = run.laneByWorkstreamId[normalizedWorkstreamId];
|
|
1034
|
+
if (lane && lane.activeRunId === sliceRunId) {
|
|
1035
|
+
setLaneState(run, {
|
|
1036
|
+
workstreamId: normalizedWorkstreamId,
|
|
1037
|
+
state: lane.state === "blocked" ? "blocked" : "idle",
|
|
1038
|
+
activeRunId: null,
|
|
1039
|
+
activeTaskIds: [],
|
|
1040
|
+
retryAt: lane.retryAt ?? null,
|
|
1041
|
+
waitingOnWorkstreamIds: lane.waitingOnWorkstreamIds ?? [],
|
|
1042
|
+
blockedReason: lane.state === "blocked" ? lane.blockedReason : null,
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
const syncLegacyRunPointers = (run) => {
|
|
1048
|
+
ensureRunInternals(run);
|
|
1049
|
+
const activeIds = listActiveSliceRunIds(run);
|
|
1050
|
+
run.activeSliceRunIds = activeIds;
|
|
1051
|
+
run.activeTaskIds = dedupeStrings((run.activeTaskIds ?? []).map((id) => (id ?? "").trim()).filter(Boolean));
|
|
1052
|
+
run.activeRunId = activeIds[0] ?? null;
|
|
1053
|
+
run.activeTaskId = run.activeTaskIds[0] ?? null;
|
|
1054
|
+
if (!run.activeRunId) {
|
|
1055
|
+
run.activeTaskTokenEstimate = null;
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
const ensureRunInternals = (run) => {
|
|
1059
|
+
if (!Array.isArray(run.activeSliceRunIds))
|
|
1060
|
+
run.activeSliceRunIds = [];
|
|
1061
|
+
if (!Array.isArray(run.activeTaskIds))
|
|
1062
|
+
run.activeTaskIds = [];
|
|
1063
|
+
if (!run.laneByWorkstreamId || typeof run.laneByWorkstreamId !== "object") {
|
|
1064
|
+
run.laneByWorkstreamId = {};
|
|
1065
|
+
}
|
|
1066
|
+
if (!Array.isArray(run.blockedWorkstreamIds))
|
|
1067
|
+
run.blockedWorkstreamIds = [];
|
|
1068
|
+
run.maxParallelSlices = normalizeMaxParallelSlices(run.maxParallelSlices, AUTO_CONTINUE_MAX_PARALLEL_DEFAULT);
|
|
1069
|
+
run.parallelMode = normalizeParallelMode(run.parallelMode);
|
|
1070
|
+
run.tokenBudget = normalizeTokenBudget(run.tokenBudget, defaultAutoContinueTokenBudget());
|
|
1071
|
+
};
|
|
1072
|
+
const recordLocalStatusOverrides = (input) => {
|
|
1073
|
+
const initiativeId = input.initiativeId.trim();
|
|
1074
|
+
if (!initiativeId)
|
|
1075
|
+
return;
|
|
1076
|
+
if (input.taskUpdates.length > 0) {
|
|
1077
|
+
const scoped = localTaskStatusOverrides.get(initiativeId) ?? new Map();
|
|
1078
|
+
for (const update of input.taskUpdates) {
|
|
1079
|
+
const taskId = update.taskId.trim();
|
|
1080
|
+
const status = normalizeStatusValue(update.status);
|
|
1081
|
+
if (!taskId || !status)
|
|
1082
|
+
continue;
|
|
1083
|
+
scoped.set(taskId, {
|
|
1084
|
+
status,
|
|
1085
|
+
updatedAt: input.updatedAt,
|
|
1086
|
+
reason: update.reason,
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
if (scoped.size > 0) {
|
|
1090
|
+
localTaskStatusOverrides.set(initiativeId, scoped);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (input.milestoneUpdates.length > 0) {
|
|
1094
|
+
const scoped = localMilestoneStatusOverrides.get(initiativeId) ?? new Map();
|
|
1095
|
+
for (const update of input.milestoneUpdates) {
|
|
1096
|
+
const milestoneId = update.milestoneId.trim();
|
|
1097
|
+
const status = normalizeStatusValue(update.status);
|
|
1098
|
+
if (!milestoneId || !status)
|
|
1099
|
+
continue;
|
|
1100
|
+
scoped.set(milestoneId, {
|
|
1101
|
+
status,
|
|
1102
|
+
updatedAt: input.updatedAt,
|
|
1103
|
+
reason: update.reason,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
if (scoped.size > 0) {
|
|
1107
|
+
localMilestoneStatusOverrides.set(initiativeId, scoped);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
const applyLocalStatusOverridesToGraph = (initiativeId, nodeById) => {
|
|
1112
|
+
const scopedTaskOverrides = localTaskStatusOverrides.get(initiativeId) ?? null;
|
|
1113
|
+
if (scopedTaskOverrides) {
|
|
1114
|
+
for (const [taskId, override] of scopedTaskOverrides.entries()) {
|
|
1115
|
+
const node = nodeById.get(taskId);
|
|
1116
|
+
if (!node || node.type !== "task")
|
|
1117
|
+
continue;
|
|
1118
|
+
const remoteStatus = normalizeStatusValue(node.status);
|
|
1119
|
+
node.status = override.status;
|
|
1120
|
+
if (remoteStatus === override.status) {
|
|
1121
|
+
scopedTaskOverrides.delete(taskId);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (scopedTaskOverrides.size === 0) {
|
|
1125
|
+
localTaskStatusOverrides.delete(initiativeId);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const scopedMilestoneOverrides = localMilestoneStatusOverrides.get(initiativeId) ?? null;
|
|
1129
|
+
if (scopedMilestoneOverrides) {
|
|
1130
|
+
for (const [milestoneId, override] of scopedMilestoneOverrides.entries()) {
|
|
1131
|
+
const node = nodeById.get(milestoneId);
|
|
1132
|
+
if (!node || node.type !== "milestone")
|
|
1133
|
+
continue;
|
|
1134
|
+
const remoteStatus = normalizeStatusValue(node.status);
|
|
1135
|
+
node.status = override.status;
|
|
1136
|
+
if (remoteStatus === override.status) {
|
|
1137
|
+
scopedMilestoneOverrides.delete(milestoneId);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (scopedMilestoneOverrides.size === 0) {
|
|
1141
|
+
localMilestoneStatusOverrides.delete(initiativeId);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
const isPendingDecisionStatus = (value) => {
|
|
1146
|
+
const normalized = normalizeStatusValue(value);
|
|
1147
|
+
if (!normalized)
|
|
1148
|
+
return false;
|
|
1149
|
+
return (normalized === "pending" ||
|
|
1150
|
+
normalized === "open" ||
|
|
1151
|
+
normalized === "requested" ||
|
|
1152
|
+
normalized === "awaiting_review" ||
|
|
1153
|
+
normalized === "awaiting_approval" ||
|
|
1154
|
+
normalized === "queued");
|
|
1155
|
+
};
|
|
1156
|
+
const decisionMatchesWorkstream = (record, workstreamId, runId) => {
|
|
1157
|
+
const directWorkstream = pickString(record, ["workstream_id", "workstreamId"])?.trim() ?? "";
|
|
1158
|
+
if (directWorkstream && directWorkstream === workstreamId)
|
|
1159
|
+
return true;
|
|
1160
|
+
const correlationId = pickString(record, ["correlation_id", "correlationId"])?.trim() ?? "";
|
|
1161
|
+
if (runId && correlationId && correlationId === runId)
|
|
1162
|
+
return true;
|
|
1163
|
+
const metadataRaw = record.metadata;
|
|
1164
|
+
const metadata = metadataRaw && typeof metadataRaw === "object" && !Array.isArray(metadataRaw)
|
|
1165
|
+
? metadataRaw
|
|
1166
|
+
: null;
|
|
1167
|
+
if (!metadata)
|
|
1168
|
+
return false;
|
|
1169
|
+
const nestedWorkstream = pickString(metadata, ["workstream_id", "workstreamId"])?.trim() ?? "";
|
|
1170
|
+
if (nestedWorkstream && nestedWorkstream === workstreamId)
|
|
1171
|
+
return true;
|
|
1172
|
+
const nestedCorrelation = pickString(metadata, ["correlation_id", "correlationId"])?.trim() ?? "";
|
|
1173
|
+
if (runId && nestedCorrelation && nestedCorrelation === runId)
|
|
1174
|
+
return true;
|
|
1175
|
+
return false;
|
|
1176
|
+
};
|
|
1177
|
+
const decisionIsBlocking = (record) => {
|
|
1178
|
+
const direct = record.blocking;
|
|
1179
|
+
if (typeof direct === "boolean")
|
|
1180
|
+
return direct;
|
|
1181
|
+
const metadataRaw = record.metadata;
|
|
1182
|
+
if (metadataRaw && typeof metadataRaw === "object" && !Array.isArray(metadataRaw)) {
|
|
1183
|
+
const nested = metadataRaw.blocking;
|
|
1184
|
+
if (typeof nested === "boolean")
|
|
1185
|
+
return nested;
|
|
1186
|
+
}
|
|
1187
|
+
return true;
|
|
1188
|
+
};
|
|
45
1189
|
const setLocalInitiativeStatusOverride = (initiativeId, status) => {
|
|
46
1190
|
const normalizedId = initiativeId.trim();
|
|
47
1191
|
if (!normalizedId)
|
|
@@ -104,27 +1248,52 @@ export function createAutoContinueEngine(deps) {
|
|
|
104
1248
|
: node),
|
|
105
1249
|
};
|
|
106
1250
|
};
|
|
107
|
-
function
|
|
1251
|
+
function parseTokenBudget(value) {
|
|
108
1252
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1253
|
+
if (value <= 0)
|
|
1254
|
+
return null;
|
|
109
1255
|
return Math.max(1_000, Math.round(value));
|
|
110
1256
|
}
|
|
111
|
-
if (typeof value === "string"
|
|
112
|
-
const
|
|
1257
|
+
if (typeof value === "string") {
|
|
1258
|
+
const trimmed = value.trim();
|
|
1259
|
+
if (!trimmed)
|
|
1260
|
+
return null;
|
|
1261
|
+
const normalized = trimmed.toLowerCase();
|
|
1262
|
+
if (normalized === "0" ||
|
|
1263
|
+
normalized === "off" ||
|
|
1264
|
+
normalized === "none" ||
|
|
1265
|
+
normalized === "false" ||
|
|
1266
|
+
normalized === "unlimited" ||
|
|
1267
|
+
normalized === "null") {
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
const parsed = Number(trimmed);
|
|
113
1271
|
if (Number.isFinite(parsed)) {
|
|
1272
|
+
if (parsed <= 0)
|
|
1273
|
+
return null;
|
|
114
1274
|
return Math.max(1_000, Math.round(parsed));
|
|
115
1275
|
}
|
|
116
1276
|
}
|
|
117
|
-
return
|
|
1277
|
+
return null;
|
|
1278
|
+
}
|
|
1279
|
+
function normalizeTokenBudget(value, fallback) {
|
|
1280
|
+
const parsed = parseTokenBudget(value);
|
|
1281
|
+
if (parsed !== null)
|
|
1282
|
+
return parsed;
|
|
1283
|
+
return fallback;
|
|
118
1284
|
}
|
|
119
1285
|
function defaultAutoContinueTokenBudget() {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return
|
|
1286
|
+
const explicitBudget = parseTokenBudget(process.env.ORGX_AUTO_CONTINUE_TOKEN_BUDGET);
|
|
1287
|
+
if (explicitBudget !== null)
|
|
1288
|
+
return explicitBudget;
|
|
1289
|
+
// Token budget guardrails are now explicit-only: either pass a budget when starting
|
|
1290
|
+
// auto-continue or set ORGX_AUTO_CONTINUE_TOKEN_BUDGET directly.
|
|
1291
|
+
// Legacy fallback toggles (for example ORGX_AUTO_CONTINUE_ENFORCE_TOKEN_BUDGET)
|
|
1292
|
+
// are intentionally ignored to prevent hidden auto-stop behavior.
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
function defaultAutoContinueMaxParallelSlices() {
|
|
1296
|
+
return AUTO_CONTINUE_MAX_PARALLEL_DEFAULT;
|
|
128
1297
|
}
|
|
129
1298
|
function estimateTokensForDurationHours(durationHours) {
|
|
130
1299
|
if (!Number.isFinite(durationHours) || durationHours <= 0)
|
|
@@ -136,7 +1305,8 @@ export function createAutoContinueEngine(deps) {
|
|
|
136
1305
|
}
|
|
137
1306
|
// Helpers used by previous task-level auto-continue implementation were removed in v2.
|
|
138
1307
|
// readOpenClawSessionSummary was used by the previous task-level auto-continue implementation.
|
|
139
|
-
// Autopilot v2 dispatches workstream slices via
|
|
1308
|
+
// Autopilot v2 dispatches workstream slices via runtime workers (codex/claude-code)
|
|
1309
|
+
// and does not rely on OpenClaw session JSONL.
|
|
140
1310
|
async function fetchInitiativeEntity(initiativeId) {
|
|
141
1311
|
try {
|
|
142
1312
|
const list = await client.listEntities("initiative", { limit: 200 });
|
|
@@ -159,7 +1329,18 @@ export function createAutoContinueEngine(deps) {
|
|
|
159
1329
|
await client.updateEntity("initiative", initiativeId, { metadata: nextMeta });
|
|
160
1330
|
}
|
|
161
1331
|
async function updateInitiativeAutoContinueState(input) {
|
|
1332
|
+
syncLegacyRunPointers(input.run);
|
|
162
1333
|
const now = new Date().toISOString();
|
|
1334
|
+
const laneStates = Object.values(input.run.laneByWorkstreamId ?? {}).map((lane) => ({
|
|
1335
|
+
workstream_id: lane.workstreamId,
|
|
1336
|
+
state: lane.state,
|
|
1337
|
+
active_run_id: lane.activeRunId,
|
|
1338
|
+
active_task_ids: lane.activeTaskIds,
|
|
1339
|
+
blocked_reason: lane.blockedReason,
|
|
1340
|
+
waiting_on_workstream_ids: lane.waitingOnWorkstreamIds,
|
|
1341
|
+
retry_at: lane.retryAt,
|
|
1342
|
+
updated_at: lane.updatedAt,
|
|
1343
|
+
}));
|
|
163
1344
|
const patch = {
|
|
164
1345
|
auto_continue_enabled: input.run.status === "running" || input.run.status === "stopping",
|
|
165
1346
|
auto_continue_status: input.run.status,
|
|
@@ -171,29 +1352,63 @@ export function createAutoContinueEngine(deps) {
|
|
|
171
1352
|
auto_continue_tokens_used: input.run.tokensUsed,
|
|
172
1353
|
auto_continue_active_task_id: input.run.activeTaskId,
|
|
173
1354
|
auto_continue_active_run_id: input.run.activeRunId,
|
|
1355
|
+
auto_continue_active_task_ids: input.run.activeTaskIds,
|
|
1356
|
+
auto_continue_active_run_ids: input.run.activeSliceRunIds,
|
|
174
1357
|
auto_continue_active_task_token_estimate: input.run.activeTaskTokenEstimate,
|
|
175
1358
|
auto_continue_last_task_id: input.run.lastTaskId,
|
|
176
1359
|
auto_continue_last_run_id: input.run.lastRunId,
|
|
177
1360
|
auto_continue_include_verification: input.run.includeVerification,
|
|
178
1361
|
auto_continue_workstream_filter: input.run.allowedWorkstreamIds,
|
|
1362
|
+
auto_continue_parallel_mode: input.run.parallelMode,
|
|
1363
|
+
auto_continue_max_parallel: input.run.maxParallelSlices,
|
|
1364
|
+
auto_continue_lane_states: laneStates,
|
|
1365
|
+
auto_continue_blocked_workstream_ids: input.run.blockedWorkstreamIds,
|
|
1366
|
+
auto_continue_ignore_spawn_guard_rate_limit: input.run.ignoreSpawnGuardRateLimit,
|
|
179
1367
|
...(input.run.lastError ? { auto_continue_last_error: input.run.lastError } : {}),
|
|
180
1368
|
};
|
|
181
1369
|
await updateInitiativeMetadata(input.initiativeId, patch);
|
|
182
1370
|
}
|
|
183
1371
|
async function stopAutoContinueRun(input) {
|
|
1372
|
+
const decisionRequired = input.reason === "blocked" && input.decisionRequired === true;
|
|
1373
|
+
const decisionIds = Array.isArray(input.decisionIds)
|
|
1374
|
+
? input.decisionIds
|
|
1375
|
+
.filter((entry) => typeof entry === "string")
|
|
1376
|
+
.map((entry) => entry.trim())
|
|
1377
|
+
.filter(Boolean)
|
|
1378
|
+
: [];
|
|
1379
|
+
const preserveQuestionAutoAnswerState = input.reason === "blocked" && decisionRequired && decisionIds.length > 0;
|
|
184
1380
|
const now = new Date().toISOString();
|
|
185
|
-
|
|
1381
|
+
ensureRunInternals(input.run);
|
|
1382
|
+
const activeRunIds = listActiveSliceRunIds(input.run);
|
|
186
1383
|
input.run.status = "stopped";
|
|
187
1384
|
input.run.stopReason = input.reason;
|
|
188
1385
|
input.run.stoppedAt = now;
|
|
189
1386
|
input.run.updatedAt = now;
|
|
190
1387
|
input.run.stopRequested = false;
|
|
1388
|
+
input.run.activeSliceRunIds = [];
|
|
1389
|
+
input.run.activeTaskIds = [];
|
|
191
1390
|
input.run.activeRunId = null;
|
|
192
1391
|
input.run.activeTaskId = null;
|
|
193
1392
|
input.run.activeTaskTokenEstimate = null;
|
|
1393
|
+
for (const lane of Object.values(input.run.laneByWorkstreamId ?? {})) {
|
|
1394
|
+
if (lane.activeRunId || lane.activeTaskIds.length > 0) {
|
|
1395
|
+
setLaneState(input.run, {
|
|
1396
|
+
workstreamId: lane.workstreamId,
|
|
1397
|
+
state: lane.state === "blocked" ? "blocked" : "idle",
|
|
1398
|
+
activeRunId: null,
|
|
1399
|
+
activeTaskIds: [],
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
194
1403
|
if (input.error)
|
|
195
1404
|
input.run.lastError = input.error;
|
|
196
|
-
|
|
1405
|
+
clearSpawnGuardRetryStateForInitiative(input.run.initiativeId);
|
|
1406
|
+
if (!preserveQuestionAutoAnswerState) {
|
|
1407
|
+
clearQuestionAutoAnswerStateForInitiative(input.run.initiativeId);
|
|
1408
|
+
}
|
|
1409
|
+
for (const runId of activeRunIds) {
|
|
1410
|
+
clearAutoContinueSliceTransientState(runId);
|
|
1411
|
+
}
|
|
197
1412
|
// Only pause the initiative on non-terminal stops (error, blocked, user-requested).
|
|
198
1413
|
// Completed / budget-exhausted runs should not override the initiative status.
|
|
199
1414
|
if (input.reason !== "completed" && input.reason !== "budget_exhausted") {
|
|
@@ -215,17 +1430,22 @@ export function createAutoContinueEngine(deps) {
|
|
|
215
1430
|
catch {
|
|
216
1431
|
// best effort
|
|
217
1432
|
}
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
1433
|
+
const primaryActiveRunId = activeRunIds[0] ?? null;
|
|
1434
|
+
const scopedWorkstreamId = Array.isArray(input.run.allowedWorkstreamIds) && input.run.allowedWorkstreamIds.length === 1
|
|
1435
|
+
? input.run.allowedWorkstreamIds[0]
|
|
1436
|
+
: null;
|
|
1437
|
+
const scopeSuffix = scopedWorkstreamId ? ` [workstream ${scopedWorkstreamId}]` : "";
|
|
1438
|
+
const budgetValue = typeof input.run.tokenBudget === "number" ? input.run.tokenBudget : "unbounded";
|
|
221
1439
|
const message = input.reason === "completed"
|
|
222
1440
|
? `Autopilot stopped: current dispatch scope completed${scopeSuffix}.`
|
|
223
1441
|
: input.reason === "budget_exhausted"
|
|
224
|
-
? `Autopilot stopped: token budget exhausted (${input.run.tokensUsed}/${
|
|
1442
|
+
? `Autopilot stopped: token budget exhausted (${input.run.tokensUsed}/${budgetValue}).`
|
|
225
1443
|
: input.reason === "stopped"
|
|
226
1444
|
? `Autopilot stopped by user request${scopeSuffix}.`
|
|
227
1445
|
: input.reason === "blocked"
|
|
228
|
-
?
|
|
1446
|
+
? decisionRequired
|
|
1447
|
+
? `Autopilot stopped: blocked awaiting decision${scopeSuffix}.`
|
|
1448
|
+
: `Autopilot stopped: blocked${scopeSuffix}.`
|
|
229
1449
|
: `Autopilot stopped due to error${scopeSuffix}.`;
|
|
230
1450
|
const phase = input.reason === "completed"
|
|
231
1451
|
? "completed"
|
|
@@ -237,26 +1457,83 @@ export function createAutoContinueEngine(deps) {
|
|
|
237
1457
|
: input.reason === "budget_exhausted" || input.reason === "stopped"
|
|
238
1458
|
? "warn"
|
|
239
1459
|
: "error";
|
|
1460
|
+
const errorLocation = input.reason === "blocked"
|
|
1461
|
+
? "mission-control.auto-continue.engine.blocked"
|
|
1462
|
+
: input.reason === "error"
|
|
1463
|
+
? "mission-control.auto-continue.engine.error"
|
|
1464
|
+
: null;
|
|
1465
|
+
const stopRunContext = {
|
|
1466
|
+
initiativeId: input.run.initiativeId,
|
|
1467
|
+
agentId: input.run.agentId,
|
|
1468
|
+
agentName: input.run.agentName,
|
|
1469
|
+
scope: input.run.scope,
|
|
1470
|
+
};
|
|
240
1471
|
await emitActivitySafe({
|
|
241
1472
|
initiativeId: input.run.initiativeId,
|
|
242
|
-
runId:
|
|
243
|
-
correlationId:
|
|
1473
|
+
runId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
|
|
1474
|
+
correlationId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
|
|
244
1475
|
phase,
|
|
245
1476
|
level,
|
|
1477
|
+
progressPct: input.reason === "completed" ? 100 : input.reason === "blocked" ? 65 : 0,
|
|
1478
|
+
nextStep: input.reason === "completed"
|
|
1479
|
+
? "Select the next queue item or enable autoplay for continuous dispatch."
|
|
1480
|
+
: input.reason === "blocked"
|
|
1481
|
+
? "Resolve blocker decisions, then resume or restart autoplay."
|
|
1482
|
+
: input.reason === "budget_exhausted"
|
|
1483
|
+
? "Increase token budget or scope down work before restarting autoplay."
|
|
1484
|
+
: input.reason === "stopped"
|
|
1485
|
+
? "Restart autoplay when ready."
|
|
1486
|
+
: "Inspect error details and relaunch once fixed.",
|
|
246
1487
|
message,
|
|
247
1488
|
metadata: {
|
|
248
|
-
|
|
1489
|
+
...buildSliceEnrichment({
|
|
1490
|
+
run: stopRunContext,
|
|
1491
|
+
workstreamId: scopedWorkstreamId,
|
|
1492
|
+
event: "auto_continue_stopped",
|
|
1493
|
+
}),
|
|
249
1494
|
stop_reason: input.reason,
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
active_run_id: activeRunId,
|
|
1495
|
+
active_run_id: primaryActiveRunId,
|
|
1496
|
+
active_run_ids: activeRunIds,
|
|
253
1497
|
last_run_id: input.run.lastRunId,
|
|
254
1498
|
token_budget: input.run.tokenBudget,
|
|
255
1499
|
tokens_used: input.run.tokensUsed,
|
|
256
1500
|
allowed_workstream_ids: input.run.allowedWorkstreamIds,
|
|
1501
|
+
max_parallel_slices: input.run.maxParallelSlices,
|
|
1502
|
+
scope_workstream_id: scopedWorkstreamId,
|
|
1503
|
+
decision_required: decisionRequired,
|
|
1504
|
+
decision_ids: decisionIds,
|
|
1505
|
+
decision_count: decisionIds.length,
|
|
257
1506
|
last_error: input.run.lastError,
|
|
1507
|
+
error_location: errorLocation,
|
|
258
1508
|
},
|
|
259
1509
|
});
|
|
1510
|
+
// Emit autopilot_transition event for state observers.
|
|
1511
|
+
try {
|
|
1512
|
+
await emitActivitySafe({
|
|
1513
|
+
initiativeId: input.run.initiativeId,
|
|
1514
|
+
runId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
|
|
1515
|
+
correlationId: primaryActiveRunId ?? input.run.lastRunId ?? undefined,
|
|
1516
|
+
phase,
|
|
1517
|
+
level: "info",
|
|
1518
|
+
progressPct: input.reason === "completed" ? 100 : input.reason === "blocked" ? 65 : 0,
|
|
1519
|
+
message: `Autopilot state: running → ${input.reason === "completed" ? "idle" : input.reason === "stopped" ? "idle" : input.reason}.`,
|
|
1520
|
+
metadata: {
|
|
1521
|
+
...buildSliceEnrichment({
|
|
1522
|
+
run: stopRunContext,
|
|
1523
|
+
workstreamId: scopedWorkstreamId,
|
|
1524
|
+
event: "autopilot_transition",
|
|
1525
|
+
actionType: "run_state_transition",
|
|
1526
|
+
}),
|
|
1527
|
+
old_state: "running",
|
|
1528
|
+
new_state: input.reason === "completed" || input.reason === "stopped" ? "idle" : input.reason === "blocked" ? "blocked" : input.reason === "error" ? "error" : "idle",
|
|
1529
|
+
reason: input.reason,
|
|
1530
|
+
workspace_id: input.run.allowedWorkstreamIds?.[0] ?? null,
|
|
1531
|
+
},
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
catch {
|
|
1535
|
+
// best effort
|
|
1536
|
+
}
|
|
260
1537
|
}
|
|
261
1538
|
const codexBinResolver = createCodexBinResolver();
|
|
262
1539
|
const resolveCodexBinInfo = () => codexBinResolver.resolveCodexBinInfo();
|
|
@@ -274,11 +1551,15 @@ export function createAutoContinueEngine(deps) {
|
|
|
274
1551
|
if (run.status !== "running" && run.status !== "stopping")
|
|
275
1552
|
return;
|
|
276
1553
|
const now = new Date().toISOString();
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
1554
|
+
syncLegacyRunPointers(run);
|
|
1555
|
+
// 1) Reconcile each active slice lane and register outcomes when complete.
|
|
1556
|
+
const activeRunIdsForTick = listActiveSliceRunIds(run);
|
|
1557
|
+
for (const activeRunIdForTick of activeRunIdsForTick) {
|
|
1558
|
+
run.activeRunId = activeRunIdForTick;
|
|
1559
|
+
const slice = autoContinueSliceRuns.get(activeRunIdForTick) ?? null;
|
|
280
1560
|
if (!slice) {
|
|
281
1561
|
// Legacy/unknown pointer; clear so we can continue.
|
|
1562
|
+
removeActiveSliceFromRun(run, { sliceRunId: activeRunIdForTick });
|
|
282
1563
|
run.activeRunId = null;
|
|
283
1564
|
run.activeTaskId = null;
|
|
284
1565
|
run.updatedAt = now;
|
|
@@ -327,12 +1608,18 @@ export function createAutoContinueEngine(deps) {
|
|
|
327
1608
|
requested_by_agent_name: run.agentName,
|
|
328
1609
|
domain: slice.domain,
|
|
329
1610
|
required_skills: slice.requiredSkills,
|
|
1611
|
+
behavior_config_id: slice.behaviorConfigId,
|
|
1612
|
+
behavior_config_version: slice.behaviorConfigVersion,
|
|
1613
|
+
behavior_config_hash: slice.behaviorConfigHash,
|
|
1614
|
+
policy_source: slice.behaviorPolicySource,
|
|
1615
|
+
behavior_automation_level: slice.behaviorAutomationLevel,
|
|
330
1616
|
workstream_id: slice.workstreamId,
|
|
331
1617
|
workstream_title: slice.workstreamTitle ?? null,
|
|
332
1618
|
task_ids: slice.taskIds,
|
|
333
1619
|
milestone_ids: slice.milestoneIds,
|
|
334
1620
|
log_path: slice.logPath,
|
|
335
1621
|
output_path: slice.outputPath,
|
|
1622
|
+
...mockMeta(slice),
|
|
336
1623
|
},
|
|
337
1624
|
});
|
|
338
1625
|
}
|
|
@@ -344,8 +1631,10 @@ export function createAutoContinueEngine(deps) {
|
|
|
344
1631
|
const startedAtEpochMs = Date.parse(slice.startedAt);
|
|
345
1632
|
const fallbackEpochMs = Number.isFinite(startedAtEpochMs) ? startedAtEpochMs : nowMs;
|
|
346
1633
|
const outputUpdatedAtEpochMs = fileUpdatedAtEpochMs(slice.outputPath, fallbackEpochMs);
|
|
347
|
-
|
|
348
|
-
|
|
1634
|
+
const logUpdatedAtEpochMs = fileUpdatedAtEpochMs(slice.logPath, fallbackEpochMs);
|
|
1635
|
+
// Some codex runs only materialize output.json at process exit. Treat recent log activity
|
|
1636
|
+
// as liveness signal so active slices are not falsely marked as stalled.
|
|
1637
|
+
const stallUpdatedAtEpochMs = Math.max(outputUpdatedAtEpochMs, logUpdatedAtEpochMs);
|
|
349
1638
|
const logTail = readFileTailSafe(slice.logPath, 64_000);
|
|
350
1639
|
const mcpHandshake = detectMcpHandshakeFailure(logTail);
|
|
351
1640
|
if (mcpHandshake) {
|
|
@@ -369,21 +1658,28 @@ export function createAutoContinueEngine(deps) {
|
|
|
369
1658
|
correlationId: slice.runId,
|
|
370
1659
|
phase: "blocked",
|
|
371
1660
|
level: "error",
|
|
1661
|
+
progressPct: 55,
|
|
1662
|
+
nextStep: "Review MCP diagnostics, then choose retry, skip, or pause for investigation.",
|
|
372
1663
|
message: `Autopilot slice MCP failed: ${slice.workstreamTitle ?? slice.workstreamId}.`,
|
|
373
1664
|
metadata: {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
1665
|
+
...buildSliceEnrichment({
|
|
1666
|
+
run,
|
|
1667
|
+
slice,
|
|
1668
|
+
workstreamId: slice.workstreamId,
|
|
1669
|
+
workstreamTitle: slice.workstreamTitle ?? null,
|
|
1670
|
+
domain: slice.domain,
|
|
1671
|
+
requiredSkills: slice.requiredSkills,
|
|
1672
|
+
event: "autopilot_slice_mcp_handshake_failed",
|
|
1673
|
+
}),
|
|
1674
|
+
error_location: "mission-control.auto-continue.engine.slice.mcp-handshake",
|
|
377
1675
|
mcp_server: mcpHandshake.server,
|
|
378
1676
|
mcp_line: mcpHandshake.line,
|
|
379
|
-
workstream_id: slice.workstreamId,
|
|
380
|
-
task_ids: slice.taskIds,
|
|
381
|
-
milestone_ids: slice.milestoneIds,
|
|
382
1677
|
log_path: slice.logPath,
|
|
383
1678
|
output_path: slice.outputPath,
|
|
1679
|
+
...mockMeta(slice),
|
|
384
1680
|
},
|
|
385
1681
|
});
|
|
386
|
-
await
|
|
1682
|
+
const decisionResult = await requestDecisionQueued({
|
|
387
1683
|
initiativeId: run.initiativeId,
|
|
388
1684
|
correlationId: slice.runId,
|
|
389
1685
|
title: `Autopilot slice MCP failed: ${slice.workstreamTitle ?? slice.workstreamId}`,
|
|
@@ -395,19 +1691,63 @@ export function createAutoContinueEngine(deps) {
|
|
|
395
1691
|
"Skip this workstream for now",
|
|
396
1692
|
],
|
|
397
1693
|
blocking: true,
|
|
1694
|
+
decisionType: "autopilot_failure",
|
|
1695
|
+
workstreamId: slice.workstreamId,
|
|
1696
|
+
agentId: slice.agentId,
|
|
1697
|
+
sourceSystem: "orgx-autopilot",
|
|
1698
|
+
conflictSource: "mcp_handshake_failure",
|
|
1699
|
+
dedupeKey: [
|
|
1700
|
+
"autopilot",
|
|
1701
|
+
run.initiativeId,
|
|
1702
|
+
slice.workstreamId,
|
|
1703
|
+
"mcp_handshake_failure",
|
|
1704
|
+
mcpHandshake.server ?? "unknown",
|
|
1705
|
+
].join(":"),
|
|
1706
|
+
recommendedAction: "Retry once. If it fails again, pause autopilot and inspect MCP server configuration.",
|
|
1707
|
+
sourceRunId: slice.runId,
|
|
1708
|
+
sourceRef: {
|
|
1709
|
+
run_id: slice.runId,
|
|
1710
|
+
workstream_id: slice.workstreamId,
|
|
1711
|
+
mcp_server: mcpHandshake.server ?? null,
|
|
1712
|
+
},
|
|
1713
|
+
evidenceRefs: [
|
|
1714
|
+
{
|
|
1715
|
+
evidence_type: "mcp_diagnostic",
|
|
1716
|
+
title: "MCP handshake failure",
|
|
1717
|
+
summary: `MCP handshake failed${mcpHandshake.server ? ` for ${mcpHandshake.server}` : ""}.`,
|
|
1718
|
+
source_pointer: slice.logPath,
|
|
1719
|
+
payload: {
|
|
1720
|
+
mcp_server: mcpHandshake.server ?? null,
|
|
1721
|
+
mcp_line: mcpHandshake.line ?? null,
|
|
1722
|
+
output_path: slice.outputPath,
|
|
1723
|
+
},
|
|
1724
|
+
},
|
|
1725
|
+
],
|
|
1726
|
+
});
|
|
1727
|
+
setLaneState(run, {
|
|
1728
|
+
workstreamId: slice.workstreamId,
|
|
1729
|
+
state: "blocked",
|
|
1730
|
+
activeRunId: null,
|
|
1731
|
+
activeTaskIds: [],
|
|
1732
|
+
blockedReason: slice.lastError,
|
|
1733
|
+
waitingOnWorkstreamIds: [],
|
|
1734
|
+
retryAt: null,
|
|
398
1735
|
});
|
|
399
1736
|
await stopAutoContinueRun({
|
|
400
1737
|
run,
|
|
401
1738
|
reason: "blocked",
|
|
402
1739
|
error: slice.lastError,
|
|
1740
|
+
decisionRequired: decisionResult.queued,
|
|
1741
|
+
decisionIds: decisionResult.decisionIds,
|
|
403
1742
|
});
|
|
404
1743
|
return;
|
|
405
1744
|
}
|
|
1745
|
+
const scopeTimeoutMs = AUTO_CONTINUE_SLICE_TIMEOUT_MS * SLICE_SCOPE_TIMEOUT_MULTIPLIER[slice.scope ?? "task"];
|
|
406
1746
|
const killDecision = shouldKillWorker({
|
|
407
1747
|
nowEpochMs: nowMs,
|
|
408
1748
|
startedAtEpochMs: fallbackEpochMs,
|
|
409
1749
|
logUpdatedAtEpochMs: stallUpdatedAtEpochMs,
|
|
410
|
-
}, { timeoutMs:
|
|
1750
|
+
}, { timeoutMs: scopeTimeoutMs, stallMs: AUTO_CONTINUE_SLICE_LOG_STALL_MS });
|
|
411
1751
|
if (killDecision.kill) {
|
|
412
1752
|
try {
|
|
413
1753
|
await stopProcess(pid);
|
|
@@ -420,7 +1760,7 @@ export function createAutoContinueEngine(deps) {
|
|
|
420
1760
|
slice.updatedAt = now;
|
|
421
1761
|
slice.lastError =
|
|
422
1762
|
killDecision.kind === "timeout"
|
|
423
|
-
? `Autopilot slice timed out after ${Math.round(
|
|
1763
|
+
? `Autopilot slice timed out after ${Math.round(scopeTimeoutMs / 60_000)} minutes.`
|
|
424
1764
|
: `Autopilot slice stalled (no output) for ${Math.round(AUTO_CONTINUE_SLICE_LOG_STALL_MS / 60_000)} minutes.`;
|
|
425
1765
|
autoContinueSliceRuns.set(slice.runId, slice);
|
|
426
1766
|
run.lastError = slice.lastError;
|
|
@@ -434,22 +1774,31 @@ export function createAutoContinueEngine(deps) {
|
|
|
434
1774
|
correlationId: slice.runId,
|
|
435
1775
|
phase: "blocked",
|
|
436
1776
|
level: "error",
|
|
1777
|
+
progressPct: 55,
|
|
1778
|
+
nextStep: "Open logs/output, decide retry or pause, and capture blocker context for handoff.",
|
|
437
1779
|
message: `Autopilot slice ${humanLabel}: ${slice.workstreamTitle ?? slice.workstreamId}.`,
|
|
438
1780
|
metadata: {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
1781
|
+
...buildSliceEnrichment({
|
|
1782
|
+
run,
|
|
1783
|
+
slice,
|
|
1784
|
+
workstreamId: slice.workstreamId,
|
|
1785
|
+
workstreamTitle: slice.workstreamTitle ?? null,
|
|
1786
|
+
domain: slice.domain,
|
|
1787
|
+
requiredSkills: slice.requiredSkills,
|
|
1788
|
+
event,
|
|
1789
|
+
}),
|
|
1790
|
+
error_location: killDecision.kind === "timeout"
|
|
1791
|
+
? "mission-control.auto-continue.engine.slice.timeout"
|
|
1792
|
+
: "mission-control.auto-continue.engine.slice.stall",
|
|
445
1793
|
log_path: slice.logPath,
|
|
446
1794
|
output_path: slice.outputPath,
|
|
447
1795
|
reason: killDecision.reason,
|
|
448
1796
|
elapsed_ms: killDecision.elapsedMs,
|
|
449
1797
|
idle_ms: killDecision.idleMs,
|
|
1798
|
+
...mockMeta(slice),
|
|
450
1799
|
},
|
|
451
1800
|
});
|
|
452
|
-
await
|
|
1801
|
+
const decisionResult = await requestDecisionQueued({
|
|
453
1802
|
initiativeId: run.initiativeId,
|
|
454
1803
|
correlationId: slice.runId,
|
|
455
1804
|
title: `Autopilot slice ${humanLabel}: ${slice.workstreamTitle ?? slice.workstreamId}`,
|
|
@@ -461,11 +1810,63 @@ export function createAutoContinueEngine(deps) {
|
|
|
461
1810
|
"Skip this workstream for now",
|
|
462
1811
|
],
|
|
463
1812
|
blocking: true,
|
|
1813
|
+
decisionType: "autopilot_failure",
|
|
1814
|
+
workstreamId: slice.workstreamId,
|
|
1815
|
+
agentId: slice.agentId,
|
|
1816
|
+
sourceSystem: "orgx-autopilot",
|
|
1817
|
+
conflictSource: killDecision.kind === "timeout"
|
|
1818
|
+
? "slice_timeout"
|
|
1819
|
+
: "slice_stall_no_output",
|
|
1820
|
+
dedupeKey: [
|
|
1821
|
+
"autopilot",
|
|
1822
|
+
run.initiativeId,
|
|
1823
|
+
slice.workstreamId,
|
|
1824
|
+
killDecision.kind === "timeout"
|
|
1825
|
+
? "slice_timeout"
|
|
1826
|
+
: "slice_stall_no_output",
|
|
1827
|
+
].join(":"),
|
|
1828
|
+
recommendedAction: "Review logs and output, then retry once. If repeated, pause autopilot and investigate worker/runtime health.",
|
|
1829
|
+
sourceRunId: slice.runId,
|
|
1830
|
+
sourceRef: {
|
|
1831
|
+
run_id: slice.runId,
|
|
1832
|
+
workstream_id: slice.workstreamId,
|
|
1833
|
+
kill_kind: killDecision.kind,
|
|
1834
|
+
elapsed_ms: killDecision.elapsedMs,
|
|
1835
|
+
idle_ms: killDecision.idleMs,
|
|
1836
|
+
},
|
|
1837
|
+
evidenceRefs: [
|
|
1838
|
+
{
|
|
1839
|
+
evidence_type: killDecision.kind === "timeout"
|
|
1840
|
+
? "timeout_diagnostic"
|
|
1841
|
+
: "stall_diagnostic",
|
|
1842
|
+
title: killDecision.kind === "timeout"
|
|
1843
|
+
? "Slice timed out"
|
|
1844
|
+
: "Slice stalled",
|
|
1845
|
+
summary: killDecision.reason,
|
|
1846
|
+
source_pointer: slice.logPath,
|
|
1847
|
+
payload: {
|
|
1848
|
+
elapsed_ms: killDecision.elapsedMs,
|
|
1849
|
+
idle_ms: killDecision.idleMs,
|
|
1850
|
+
output_path: slice.outputPath,
|
|
1851
|
+
},
|
|
1852
|
+
},
|
|
1853
|
+
],
|
|
1854
|
+
});
|
|
1855
|
+
setLaneState(run, {
|
|
1856
|
+
workstreamId: slice.workstreamId,
|
|
1857
|
+
state: "blocked",
|
|
1858
|
+
activeRunId: null,
|
|
1859
|
+
activeTaskIds: [],
|
|
1860
|
+
blockedReason: slice.lastError,
|
|
1861
|
+
waitingOnWorkstreamIds: [],
|
|
1862
|
+
retryAt: null,
|
|
464
1863
|
});
|
|
465
1864
|
await stopAutoContinueRun({
|
|
466
1865
|
run,
|
|
467
1866
|
reason: "blocked",
|
|
468
1867
|
error: slice.lastError,
|
|
1868
|
+
decisionRequired: decisionResult.queued,
|
|
1869
|
+
decisionIds: decisionResult.decisionIds,
|
|
469
1870
|
});
|
|
470
1871
|
return;
|
|
471
1872
|
}
|
|
@@ -478,7 +1879,7 @@ export function createAutoContinueEngine(deps) {
|
|
|
478
1879
|
}
|
|
479
1880
|
}
|
|
480
1881
|
if (!outputComplete)
|
|
481
|
-
|
|
1882
|
+
continue;
|
|
482
1883
|
}
|
|
483
1884
|
}
|
|
484
1885
|
// Slice finished.
|
|
@@ -486,13 +1887,21 @@ export function createAutoContinueEngine(deps) {
|
|
|
486
1887
|
const parsed = raw ? parseSliceResult(raw) : null;
|
|
487
1888
|
const parsedStatus = parsed?.status ?? "error";
|
|
488
1889
|
const defaultDecisionBlocking = parsedStatus === "completed" ? false : true;
|
|
489
|
-
const
|
|
1890
|
+
const allDecisions = Array.isArray(parsed?.decisions_needed)
|
|
490
1891
|
? (parsed?.decisions_needed ?? [])
|
|
491
1892
|
.filter((item) => Boolean(item && typeof item.question === "string" && item.question.trim()))
|
|
492
1893
|
: [];
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
1894
|
+
const isParserSyntheticFallbackDecision = (item) => {
|
|
1895
|
+
const question = String(item?.question ?? "").trim().toLowerCase();
|
|
1896
|
+
const summary = String(item?.summary ?? "").trim().toLowerCase();
|
|
1897
|
+
return ((question.includes("missing required blocking decision") ||
|
|
1898
|
+
summary.includes("parser inserted a blocking decision")) &&
|
|
1899
|
+
item?.blocking === true);
|
|
1900
|
+
};
|
|
1901
|
+
const decisions = allDecisions.filter((item) => !isParserSyntheticFallbackDecision(item));
|
|
1902
|
+
const normalizedBlockingDecisionCount = allDecisions.filter((item) => typeof item.blocking === "boolean" ? item.blocking : defaultDecisionBlocking).length;
|
|
1903
|
+
const normalizedNonBlockingDecisionCount = Math.max(0, allDecisions.length - normalizedBlockingDecisionCount);
|
|
1904
|
+
const effectiveParsedStatus = parsedStatus === "completed" && normalizedBlockingDecisionCount > 0
|
|
496
1905
|
? "needs_decision"
|
|
497
1906
|
: parsedStatus;
|
|
498
1907
|
slice.status =
|
|
@@ -510,32 +1919,157 @@ export function createAutoContinueEngine(deps) {
|
|
|
510
1919
|
autoContinueSliceRuns.set(slice.runId, slice);
|
|
511
1920
|
clearAutoContinueSliceTransientState(slice.runId);
|
|
512
1921
|
// Token accounting: codex CLI doesn't provide tokens here; use the modeled estimate.
|
|
513
|
-
const modeledTokens = slice.tokenEstimate ??
|
|
1922
|
+
const modeledTokens = slice.tokenEstimate ?? 0;
|
|
514
1923
|
run.tokensUsed += Math.max(0, modeledTokens);
|
|
515
1924
|
run.activeTaskTokenEstimate = null;
|
|
516
1925
|
const artifacts = Array.isArray(parsed?.artifacts)
|
|
517
1926
|
? (parsed?.artifacts ?? [])
|
|
518
1927
|
.filter((item) => Boolean(item && typeof item.name === "string" && item.name.trim()))
|
|
519
1928
|
: [];
|
|
520
|
-
const
|
|
521
|
-
|
|
1929
|
+
const artifactEvidenceRefs = artifacts.map((artifact) => ({
|
|
1930
|
+
evidence_type: "artifact",
|
|
1931
|
+
title: artifact.name.trim(),
|
|
1932
|
+
summary: artifact.description?.trim() || "Slice artifact output",
|
|
1933
|
+
source_pointer: artifact.url ?? slice.outputPath,
|
|
1934
|
+
payload: {
|
|
1935
|
+
artifact_type: artifact.artifact_type ?? null,
|
|
1936
|
+
confidence_score: artifact.confidence_score ?? null,
|
|
1937
|
+
task_ids: Array.isArray(artifact.task_ids) && artifact.task_ids.length > 0
|
|
1938
|
+
? artifact.task_ids
|
|
1939
|
+
: slice.taskIds,
|
|
1940
|
+
milestone_id: artifact.milestone_id ?? slice.milestoneIds[0] ?? null,
|
|
1941
|
+
},
|
|
1942
|
+
}));
|
|
1943
|
+
const nextActions = Array.isArray(parsed?.next_actions)
|
|
1944
|
+
? parsed.next_actions
|
|
1945
|
+
.filter((item) => typeof item === "string")
|
|
1946
|
+
.map((item) => item.trim())
|
|
1947
|
+
.filter(Boolean)
|
|
1948
|
+
: [];
|
|
1949
|
+
const userSummary = (typeof parsed?.summary === "string" && parsed.summary.trim().length > 0
|
|
1950
|
+
? parsed.summary.trim()
|
|
1951
|
+
: null) ??
|
|
1952
|
+
nextActions[0] ??
|
|
1953
|
+
(slice.status === "completed"
|
|
1954
|
+
? `Slice completed for ${slice.workstreamTitle ?? slice.workstreamId}.`
|
|
1955
|
+
: `Slice blocked for ${slice.workstreamTitle ?? slice.workstreamId}.`);
|
|
1956
|
+
const nextStepHint = nextActions[0] ??
|
|
1957
|
+
(slice.status === "completed"
|
|
1958
|
+
? "No follow-up action returned by worker."
|
|
1959
|
+
: "Resolve blocker to continue execution.");
|
|
1960
|
+
const skillEvidence = Array.isArray(parsed?.skill_evidence)
|
|
1961
|
+
? parsed.skill_evidence
|
|
1962
|
+
.map((item) => ({
|
|
1963
|
+
skill: typeof item?.skill === "string"
|
|
1964
|
+
? item.skill.trim()
|
|
1965
|
+
: "",
|
|
1966
|
+
skill_file: typeof item?.skill_file === "string"
|
|
1967
|
+
? item.skill_file.trim()
|
|
1968
|
+
: null,
|
|
1969
|
+
skill_sha256: typeof item?.skill_sha256 === "string"
|
|
1970
|
+
? item.skill_sha256.trim().toLowerCase()
|
|
1971
|
+
: null,
|
|
1972
|
+
skill_heading: typeof item?.skill_heading === "string"
|
|
1973
|
+
? item.skill_heading.trim()
|
|
1974
|
+
: null,
|
|
1975
|
+
}))
|
|
1976
|
+
.filter((item) => item.skill.length > 0)
|
|
1977
|
+
: [];
|
|
1978
|
+
const reportedSkillNames = Array.from(new Set(skillEvidence
|
|
1979
|
+
.map((entry) => entry.skill.replace(/^\$/, "").trim())
|
|
1980
|
+
.filter(Boolean)));
|
|
1981
|
+
const reportedSkillSha256Count = skillEvidence.filter((entry) => typeof entry.skill_sha256 === "string" && entry.skill_sha256.length > 0).length;
|
|
1982
|
+
const taskUpdates = Array.isArray(parsed?.task_updates)
|
|
1983
|
+
? parsed.task_updates
|
|
522
1984
|
: [];
|
|
523
1985
|
const milestoneUpdates = Array.isArray(parsed?.milestone_updates)
|
|
524
1986
|
? parsed.milestone_updates
|
|
525
1987
|
: [];
|
|
1988
|
+
const resultEnvelope = {
|
|
1989
|
+
summary: userSummary,
|
|
1990
|
+
parsed_status: effectiveParsedStatus,
|
|
1991
|
+
task_updates: taskUpdates,
|
|
1992
|
+
milestone_updates: milestoneUpdates,
|
|
1993
|
+
next_actions: nextActions,
|
|
1994
|
+
artifacts: artifacts.map((artifact) => ({
|
|
1995
|
+
name: artifact.name,
|
|
1996
|
+
artifact_type: artifact.artifact_type ?? null,
|
|
1997
|
+
url: artifact.url ?? null,
|
|
1998
|
+
})),
|
|
1999
|
+
};
|
|
2000
|
+
const evidenceEnvelope = {
|
|
2001
|
+
artifacts: artifacts.map((artifact) => ({
|
|
2002
|
+
name: artifact.name,
|
|
2003
|
+
artifact_type: artifact.artifact_type ?? null,
|
|
2004
|
+
source_pointer: artifact.url ?? null,
|
|
2005
|
+
})),
|
|
2006
|
+
files: [slice.outputPath, slice.logPath].filter(Boolean),
|
|
2007
|
+
logs: [slice.logPath].filter(Boolean),
|
|
2008
|
+
};
|
|
2009
|
+
let blockingDecisionQueued = false;
|
|
2010
|
+
const blockingDecisionIds = [];
|
|
2011
|
+
const nonBlockingDecisionIds = [];
|
|
526
2012
|
for (const decision of decisions) {
|
|
527
|
-
|
|
2013
|
+
const isBlocking = typeof decision.blocking === "boolean" ? decision.blocking : defaultDecisionBlocking;
|
|
2014
|
+
const normalizedQuestion = decision.question.trim();
|
|
2015
|
+
const decisionResult = await requestDecisionQueued({
|
|
528
2016
|
initiativeId: run.initiativeId,
|
|
529
2017
|
correlationId: slice.runId,
|
|
530
|
-
title:
|
|
2018
|
+
title: normalizedQuestion,
|
|
531
2019
|
summary: decision.summary ?? parsed?.summary ?? null,
|
|
532
2020
|
urgency: decision.urgency ?? "high",
|
|
533
2021
|
options: Array.isArray(decision.options)
|
|
534
2022
|
? decision.options.filter((opt) => typeof opt === "string" && opt.trim())
|
|
535
2023
|
: [],
|
|
536
|
-
blocking:
|
|
2024
|
+
blocking: isBlocking,
|
|
2025
|
+
decisionType: isBlocking
|
|
2026
|
+
? "autopilot_blocking_decision"
|
|
2027
|
+
: "autopilot_followup_decision",
|
|
2028
|
+
workstreamId: slice.workstreamId,
|
|
2029
|
+
agentId: slice.agentId,
|
|
2030
|
+
sourceSystem: "orgx-autopilot",
|
|
2031
|
+
conflictSource: parsedStatus === "needs_decision"
|
|
2032
|
+
? "slice_needs_decision"
|
|
2033
|
+
: "slice_reported_decision",
|
|
2034
|
+
dedupeKey: [
|
|
2035
|
+
"autopilot",
|
|
2036
|
+
run.initiativeId,
|
|
2037
|
+
slice.workstreamId,
|
|
2038
|
+
"slice_reported_decision",
|
|
2039
|
+
normalizedQuestion.toLowerCase(),
|
|
2040
|
+
].join(":"),
|
|
2041
|
+
recommendedAction: nextActions[0] ??
|
|
2042
|
+
"Resolve this decision to continue the slice or safely defer workstream execution.",
|
|
2043
|
+
sourceRunId: slice.runId,
|
|
2044
|
+
sourceRef: {
|
|
2045
|
+
run_id: slice.runId,
|
|
2046
|
+
workstream_id: slice.workstreamId,
|
|
2047
|
+
parsed_status: parsedStatus,
|
|
2048
|
+
},
|
|
2049
|
+
evidenceRefs: [
|
|
2050
|
+
{
|
|
2051
|
+
evidence_type: "slice_output_summary",
|
|
2052
|
+
title: "Slice requested a decision",
|
|
2053
|
+
summary: decision.summary ?? parsed?.summary ?? "Decision required by slice output.",
|
|
2054
|
+
source_pointer: slice.outputPath,
|
|
2055
|
+
payload: {
|
|
2056
|
+
log_path: slice.logPath,
|
|
2057
|
+
blocking: isBlocking,
|
|
2058
|
+
},
|
|
2059
|
+
},
|
|
2060
|
+
...artifactEvidenceRefs,
|
|
2061
|
+
],
|
|
537
2062
|
});
|
|
2063
|
+
if (decisionResult.queued && isBlocking)
|
|
2064
|
+
blockingDecisionQueued = true;
|
|
2065
|
+
if (decisionResult.decisionIds.length > 0) {
|
|
2066
|
+
if (isBlocking)
|
|
2067
|
+
blockingDecisionIds.push(...decisionResult.decisionIds);
|
|
2068
|
+
else
|
|
2069
|
+
nonBlockingDecisionIds.push(...decisionResult.decisionIds);
|
|
2070
|
+
}
|
|
538
2071
|
}
|
|
2072
|
+
const decisionIds = Array.from(new Set([...blockingDecisionIds, ...nonBlockingDecisionIds]));
|
|
539
2073
|
for (const artifact of artifacts) {
|
|
540
2074
|
await registerArtifactSafe({
|
|
541
2075
|
initiativeId: run.initiativeId,
|
|
@@ -543,16 +2077,86 @@ export function createAutoContinueEngine(deps) {
|
|
|
543
2077
|
agentId: slice.agentId,
|
|
544
2078
|
agentName: slice.agentName,
|
|
545
2079
|
workstreamId: slice.workstreamId,
|
|
2080
|
+
fallbackMilestoneId: slice.milestoneIds[0] ?? null,
|
|
2081
|
+
fallbackTaskIds: slice.taskIds,
|
|
546
2082
|
artifact,
|
|
2083
|
+
isMockWorker: slice.isMockWorker,
|
|
547
2084
|
});
|
|
548
2085
|
}
|
|
2086
|
+
// --- Proof ladder gate: check completion tasks for proof readiness ---
|
|
2087
|
+
// Phase 1: warn-only. Does not block status transitions but creates
|
|
2088
|
+
// a decision request when proof is missing for done/completed tasks.
|
|
2089
|
+
const doneTaskUpdates = taskUpdates.filter((tu) => tu.status === "done" || tu.status === "completed");
|
|
2090
|
+
if (doneTaskUpdates.length > 0 && !slice.isMockWorker) {
|
|
2091
|
+
const proofStrictness = process.env.ORGX_PROOF_STRICTNESS ?? "warn";
|
|
2092
|
+
for (const dtu of doneTaskUpdates) {
|
|
2093
|
+
try {
|
|
2094
|
+
const qp = new URLSearchParams({ task_id: dtu.task_id });
|
|
2095
|
+
const proofResult = await client.rawRequest("GET", `/api/flywheel/proof-status?${qp.toString()}`).catch(() => null);
|
|
2096
|
+
// If proof API unavailable, skip gracefully (phase 1)
|
|
2097
|
+
if (!proofResult)
|
|
2098
|
+
continue;
|
|
2099
|
+
const overallPassed = proofResult?.overall_passed === true;
|
|
2100
|
+
if (!overallPassed && proofStrictness === "block") {
|
|
2101
|
+
// Hard block: downgrade to needs_review
|
|
2102
|
+
dtu.status = "needs_review";
|
|
2103
|
+
const reasonCodes = Array.isArray(proofResult?.reason_codes)
|
|
2104
|
+
? proofResult.reason_codes.join(", ")
|
|
2105
|
+
: "incomplete_proof";
|
|
2106
|
+
await requestDecisionSafe({
|
|
2107
|
+
initiativeId: run.initiativeId,
|
|
2108
|
+
correlationId: slice.runId,
|
|
2109
|
+
title: `Task ${dtu.task_id} missing proof for completion`,
|
|
2110
|
+
summary: `Proof chain incomplete (${reasonCodes}). Task held in needs_review until proof is resolved.`,
|
|
2111
|
+
urgency: "high",
|
|
2112
|
+
blocking: true,
|
|
2113
|
+
decisionType: "proof_incomplete",
|
|
2114
|
+
workstreamId: slice.workstreamId,
|
|
2115
|
+
agentId: slice.agentId,
|
|
2116
|
+
sourceRunId: slice.runId,
|
|
2117
|
+
dedupeKey: `proof-gate:${dtu.task_id}:${slice.runId}`,
|
|
2118
|
+
metadata: { proof_result: proofResult },
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
else if (!overallPassed) {
|
|
2122
|
+
// Warn-only: emit activity but allow transition
|
|
2123
|
+
await emitActivitySafe({
|
|
2124
|
+
initiativeId: run.initiativeId,
|
|
2125
|
+
runId: slice.runId,
|
|
2126
|
+
correlationId: slice.runId,
|
|
2127
|
+
phase: "review",
|
|
2128
|
+
level: "warn",
|
|
2129
|
+
message: `Task ${dtu.task_id} completing with incomplete proof chain.`,
|
|
2130
|
+
metadata: {
|
|
2131
|
+
event: "proof_gate_warning",
|
|
2132
|
+
task_id: dtu.task_id,
|
|
2133
|
+
proof_result: proofResult,
|
|
2134
|
+
},
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
catch {
|
|
2139
|
+
// Best-effort proof check; don't block on transient failures
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
549
2143
|
const statusUpdateResult = await applyAgentStatusUpdatesSafe({
|
|
550
2144
|
initiativeId: run.initiativeId,
|
|
551
2145
|
runId: slice.runId,
|
|
552
2146
|
correlationId: slice.runId,
|
|
553
2147
|
taskUpdates,
|
|
554
2148
|
milestoneUpdates,
|
|
2149
|
+
isMockWorker: slice.isMockWorker,
|
|
555
2150
|
});
|
|
2151
|
+
if (statusUpdateResult.taskUpdates.length > 0 ||
|
|
2152
|
+
statusUpdateResult.milestoneUpdates.length > 0) {
|
|
2153
|
+
recordLocalStatusOverrides({
|
|
2154
|
+
initiativeId: run.initiativeId,
|
|
2155
|
+
updatedAt: now,
|
|
2156
|
+
taskUpdates: statusUpdateResult.taskUpdates,
|
|
2157
|
+
milestoneUpdates: statusUpdateResult.milestoneUpdates,
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
556
2160
|
try {
|
|
557
2161
|
writeRuntimeEvent({
|
|
558
2162
|
sourceClient: slice.sourceClient,
|
|
@@ -564,67 +2168,166 @@ export function createAutoContinueEngine(deps) {
|
|
|
564
2168
|
agentId: slice.agentId,
|
|
565
2169
|
agentName: slice.agentName ?? null,
|
|
566
2170
|
phase: slice.status === "completed" ? "completed" : "blocked",
|
|
567
|
-
message:
|
|
2171
|
+
message: userSummary ?? slice.lastError ?? "Autopilot slice finished.",
|
|
568
2172
|
metadata: {
|
|
569
2173
|
event: "autopilot_slice_finished",
|
|
2174
|
+
initiative_id: run.initiativeId,
|
|
2175
|
+
run_id: slice.runId,
|
|
2176
|
+
slice_run_id: slice.runId,
|
|
2177
|
+
workstream_id: slice.workstreamId,
|
|
2178
|
+
correlation_id: slice.runId,
|
|
570
2179
|
requested_by_agent_id: run.agentId,
|
|
571
2180
|
requested_by_agent_name: run.agentName,
|
|
572
2181
|
status: effectiveParsedStatus,
|
|
573
2182
|
artifacts: artifacts.length,
|
|
574
|
-
decisions:
|
|
575
|
-
blocking_decisions:
|
|
576
|
-
non_blocking_decisions:
|
|
2183
|
+
decisions: allDecisions.length,
|
|
2184
|
+
blocking_decisions: normalizedBlockingDecisionCount,
|
|
2185
|
+
non_blocking_decisions: normalizedNonBlockingDecisionCount,
|
|
577
2186
|
status_updates: statusUpdateResult.applied,
|
|
578
2187
|
status_updates_buffered: statusUpdateResult.buffered,
|
|
2188
|
+
reported_skill_evidence_count: skillEvidence.length,
|
|
2189
|
+
reported_skill_sha256_count: reportedSkillSha256Count,
|
|
2190
|
+
reported_skill_names: reportedSkillNames,
|
|
2191
|
+
action_type: normalizeActivityActionType("run_completed"),
|
|
2192
|
+
action_phase: normalizeActivityActionPhase(slice.status === "completed" ? "completed" : "blocked"),
|
|
2193
|
+
result: resultEnvelope,
|
|
2194
|
+
evidence: evidenceEnvelope,
|
|
2195
|
+
...mockMeta(slice),
|
|
2196
|
+
user_summary: userSummary,
|
|
2197
|
+
next_actions: nextActions,
|
|
579
2198
|
},
|
|
580
2199
|
});
|
|
581
2200
|
}
|
|
582
2201
|
catch {
|
|
583
2202
|
// best effort
|
|
584
2203
|
}
|
|
2204
|
+
if (slice.status === "completed") {
|
|
2205
|
+
await emitActivitySafe({
|
|
2206
|
+
initiativeId: run.initiativeId,
|
|
2207
|
+
runId: slice.runId,
|
|
2208
|
+
correlationId: slice.runId,
|
|
2209
|
+
phase: "handoff",
|
|
2210
|
+
level: "info",
|
|
2211
|
+
message: `Handoff ready for ${slice.workstreamTitle ?? slice.workstreamId}.`,
|
|
2212
|
+
progressPct: 80,
|
|
2213
|
+
nextStep: nextStepHint,
|
|
2214
|
+
metadata: buildSliceEnrichment({
|
|
2215
|
+
run,
|
|
2216
|
+
slice,
|
|
2217
|
+
workstreamId: slice.workstreamId,
|
|
2218
|
+
workstreamTitle: slice.workstreamTitle ?? null,
|
|
2219
|
+
domain: slice.domain,
|
|
2220
|
+
requiredSkills: slice.requiredSkills,
|
|
2221
|
+
nextActions,
|
|
2222
|
+
userSummary,
|
|
2223
|
+
event: "autopilot_slice_handoff",
|
|
2224
|
+
extra: {
|
|
2225
|
+
parsed_status: effectiveParsedStatus,
|
|
2226
|
+
artifacts: artifacts.length,
|
|
2227
|
+
decisions: decisions.length,
|
|
2228
|
+
decision_ids: decisionIds,
|
|
2229
|
+
output_path: slice.outputPath,
|
|
2230
|
+
log_path: slice.logPath,
|
|
2231
|
+
task_updates: taskUpdates,
|
|
2232
|
+
milestone_updates: milestoneUpdates,
|
|
2233
|
+
result: resultEnvelope,
|
|
2234
|
+
evidence: evidenceEnvelope,
|
|
2235
|
+
...mockMeta(slice),
|
|
2236
|
+
},
|
|
2237
|
+
}),
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
585
2240
|
await emitActivitySafe({
|
|
586
2241
|
initiativeId: run.initiativeId,
|
|
587
2242
|
runId: slice.runId,
|
|
588
2243
|
correlationId: slice.runId,
|
|
589
2244
|
phase: slice.status === "completed" ? "completed" : "blocked",
|
|
590
2245
|
level: slice.status === "completed" ? "info" : "warn",
|
|
2246
|
+
progressPct: slice.status === "completed" ? 100 : 65,
|
|
2247
|
+
nextStep: nextStepHint,
|
|
591
2248
|
message: slice.status === "completed"
|
|
592
2249
|
? `Autopilot slice completed for ${slice.workstreamTitle ?? slice.workstreamId} (${slice.taskIds.length} task${slice.taskIds.length === 1 ? "" : "s"}).`
|
|
593
2250
|
: `Autopilot slice blocked: ${slice.workstreamTitle ?? slice.workstreamId}.`,
|
|
594
2251
|
metadata: {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
2252
|
+
...buildSliceEnrichment({
|
|
2253
|
+
run,
|
|
2254
|
+
slice,
|
|
2255
|
+
workstreamId: slice.workstreamId,
|
|
2256
|
+
workstreamTitle: slice.workstreamTitle ?? null,
|
|
2257
|
+
domain: slice.domain,
|
|
2258
|
+
requiredSkills: slice.requiredSkills,
|
|
2259
|
+
nextActions,
|
|
2260
|
+
userSummary,
|
|
2261
|
+
event: "autopilot_slice_result",
|
|
2262
|
+
}),
|
|
2263
|
+
error_location: slice.status === "completed"
|
|
2264
|
+
? null
|
|
2265
|
+
: "mission-control.auto-continue.engine.slice.result",
|
|
2266
|
+
behavior_config_id: slice.behaviorConfigId,
|
|
2267
|
+
behavior_config_version: slice.behaviorConfigVersion,
|
|
2268
|
+
behavior_config_hash: slice.behaviorConfigHash,
|
|
2269
|
+
policy_source: slice.behaviorPolicySource,
|
|
2270
|
+
behavior_automation_level: slice.behaviorAutomationLevel,
|
|
605
2271
|
parsed_status: effectiveParsedStatus,
|
|
606
2272
|
has_output: Boolean(parsed),
|
|
607
2273
|
artifacts: artifacts.length,
|
|
608
|
-
decisions:
|
|
609
|
-
blocking_decisions:
|
|
610
|
-
non_blocking_decisions:
|
|
611
|
-
|
|
2274
|
+
decisions: allDecisions.length,
|
|
2275
|
+
blocking_decisions: normalizedBlockingDecisionCount,
|
|
2276
|
+
non_blocking_decisions: normalizedNonBlockingDecisionCount,
|
|
2277
|
+
decision_ids: decisionIds,
|
|
2278
|
+
blocking_decision_ids: Array.from(new Set(blockingDecisionIds)),
|
|
2279
|
+
non_blocking_decision_ids: Array.from(new Set(nonBlockingDecisionIds)),
|
|
2280
|
+
decision_required: blockingDecisionQueued || effectiveParsedStatus === "needs_decision",
|
|
612
2281
|
status_updates_applied: statusUpdateResult.applied,
|
|
613
2282
|
status_updates_buffered: statusUpdateResult.buffered,
|
|
2283
|
+
reported_skill_evidence_count: skillEvidence.length,
|
|
2284
|
+
reported_skill_sha256_count: reportedSkillSha256Count,
|
|
2285
|
+
reported_skill_names: reportedSkillNames,
|
|
614
2286
|
output_path: slice.outputPath,
|
|
615
2287
|
log_path: slice.logPath,
|
|
616
2288
|
error: slice.lastError,
|
|
2289
|
+
next_actions: nextActions,
|
|
2290
|
+
task_updates: taskUpdates,
|
|
2291
|
+
milestone_updates: milestoneUpdates,
|
|
2292
|
+
result: resultEnvelope,
|
|
2293
|
+
evidence: evidenceEnvelope,
|
|
2294
|
+
...mockMeta(slice),
|
|
2295
|
+
user_summary: userSummary,
|
|
617
2296
|
},
|
|
618
2297
|
});
|
|
2298
|
+
// Append to local team context for cross-agent awareness on subsequent slices.
|
|
2299
|
+
if (slice.status === "completed") {
|
|
2300
|
+
try {
|
|
2301
|
+
appendTeamCompletion(run.initiativeId, {
|
|
2302
|
+
domain: slice.domain ?? "unknown",
|
|
2303
|
+
task_title: slice.workstreamTitle ?? slice.workstreamId,
|
|
2304
|
+
summary: parsed?.summary ?? "Completed.",
|
|
2305
|
+
key_outputs: artifacts.map((a) => a.name).filter(Boolean).slice(0, 5),
|
|
2306
|
+
completed_at: new Date().toISOString(),
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
catch {
|
|
2310
|
+
// best effort: do not block the engine on store failure
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
619
2313
|
if (slice.status !== "completed") {
|
|
620
|
-
|
|
621
|
-
|
|
2314
|
+
let fallbackDecisionResult = {
|
|
2315
|
+
queued: false,
|
|
2316
|
+
decisionIds: [],
|
|
2317
|
+
};
|
|
2318
|
+
if (!blockingDecisionQueued) {
|
|
2319
|
+
const blockedLike = slice.status === "blocked";
|
|
2320
|
+
fallbackDecisionResult = await requestDecisionQueued({
|
|
622
2321
|
initiativeId: run.initiativeId,
|
|
623
2322
|
correlationId: slice.runId,
|
|
624
|
-
title:
|
|
2323
|
+
title: blockedLike
|
|
2324
|
+
? `Autopilot slice blocked: ${slice.workstreamTitle ?? slice.workstreamId}`
|
|
2325
|
+
: `Autopilot slice failed: ${slice.workstreamTitle ?? slice.workstreamId}`,
|
|
625
2326
|
summary: parsed?.summary ??
|
|
626
2327
|
slice.lastError ??
|
|
627
|
-
|
|
2328
|
+
(blockedLike
|
|
2329
|
+
? "The slice reported a blocked/decision-required state without a blocking decision payload. Review logs/output and decide whether to retry, unblock, or skip."
|
|
2330
|
+
: "The slice failed without producing a valid output contract. Review logs/output and decide whether to retry or pause autopilot."),
|
|
628
2331
|
urgency: "high",
|
|
629
2332
|
options: [
|
|
630
2333
|
"Retry this workstream slice",
|
|
@@ -632,14 +2335,66 @@ export function createAutoContinueEngine(deps) {
|
|
|
632
2335
|
"Skip this workstream for now",
|
|
633
2336
|
],
|
|
634
2337
|
blocking: true,
|
|
2338
|
+
decisionType: blockedLike ? "autopilot_blocked_without_decision" : "autopilot_failure",
|
|
2339
|
+
workstreamId: slice.workstreamId,
|
|
2340
|
+
agentId: slice.agentId,
|
|
2341
|
+
sourceSystem: "orgx-autopilot",
|
|
2342
|
+
conflictSource: blockedLike
|
|
2343
|
+
? "slice_missing_blocking_decision"
|
|
2344
|
+
: "slice_invalid_output",
|
|
2345
|
+
dedupeKey: [
|
|
2346
|
+
"autopilot",
|
|
2347
|
+
run.initiativeId,
|
|
2348
|
+
slice.workstreamId,
|
|
2349
|
+
blockedLike ? "slice_missing_blocking_decision" : "slice_invalid_output",
|
|
2350
|
+
].join(":"),
|
|
2351
|
+
recommendedAction: nextActions[0] ??
|
|
2352
|
+
"Review the output contract and logs, then retry or pause autopilot until the blocker is resolved.",
|
|
2353
|
+
sourceRunId: slice.runId,
|
|
2354
|
+
sourceRef: {
|
|
2355
|
+
run_id: slice.runId,
|
|
2356
|
+
workstream_id: slice.workstreamId,
|
|
2357
|
+
parsed_status: effectiveParsedStatus,
|
|
2358
|
+
},
|
|
2359
|
+
evidenceRefs: [
|
|
2360
|
+
{
|
|
2361
|
+
evidence_type: "slice_output_validation",
|
|
2362
|
+
title: "Slice output requires fallback decision",
|
|
2363
|
+
summary: parsed?.summary ??
|
|
2364
|
+
slice.lastError ??
|
|
2365
|
+
"Slice did not provide a blocking decision payload.",
|
|
2366
|
+
source_pointer: slice.outputPath,
|
|
2367
|
+
payload: {
|
|
2368
|
+
log_path: slice.logPath,
|
|
2369
|
+
parsed_status: effectiveParsedStatus,
|
|
2370
|
+
},
|
|
2371
|
+
},
|
|
2372
|
+
...artifactEvidenceRefs,
|
|
2373
|
+
],
|
|
635
2374
|
});
|
|
636
2375
|
}
|
|
2376
|
+
setLaneState(run, {
|
|
2377
|
+
workstreamId: slice.workstreamId,
|
|
2378
|
+
state: "blocked",
|
|
2379
|
+
activeRunId: null,
|
|
2380
|
+
activeTaskIds: [],
|
|
2381
|
+
blockedReason: parsed?.summary ??
|
|
2382
|
+
slice.lastError ??
|
|
2383
|
+
`Slice returned status: ${effectiveParsedStatus}`,
|
|
2384
|
+
waitingOnWorkstreamIds: [],
|
|
2385
|
+
retryAt: null,
|
|
2386
|
+
});
|
|
2387
|
+
if (!run.blockedWorkstreamIds.includes(slice.workstreamId)) {
|
|
2388
|
+
run.blockedWorkstreamIds.push(slice.workstreamId);
|
|
2389
|
+
}
|
|
637
2390
|
await stopAutoContinueRun({
|
|
638
2391
|
run,
|
|
639
2392
|
reason: slice.status === "error" ? "error" : "blocked",
|
|
640
2393
|
error: parsed?.summary ??
|
|
641
2394
|
slice.lastError ??
|
|
642
2395
|
`Slice returned status: ${effectiveParsedStatus}`,
|
|
2396
|
+
decisionRequired: blockingDecisionQueued || fallbackDecisionResult.queued,
|
|
2397
|
+
decisionIds: Array.from(new Set([...decisionIds, ...fallbackDecisionResult.decisionIds])),
|
|
643
2398
|
});
|
|
644
2399
|
return;
|
|
645
2400
|
}
|
|
@@ -654,7 +2409,7 @@ export function createAutoContinueEngine(deps) {
|
|
|
654
2409
|
const attentionSummary = completionHadNoOutcome
|
|
655
2410
|
? "The slice reported completion but did not produce artifacts or status updates. Decide whether to retry, request stronger output, or mark tasks manually."
|
|
656
2411
|
: "The slice exited without a valid output contract. Review logs/output and decide whether to retry or pause autopilot.";
|
|
657
|
-
await
|
|
2412
|
+
const decisionResult = await requestDecisionQueued({
|
|
658
2413
|
initiativeId: run.initiativeId,
|
|
659
2414
|
correlationId: slice.runId,
|
|
660
2415
|
title: attentionTitle,
|
|
@@ -666,7 +2421,61 @@ export function createAutoContinueEngine(deps) {
|
|
|
666
2421
|
"Skip this workstream for now",
|
|
667
2422
|
],
|
|
668
2423
|
blocking: true,
|
|
2424
|
+
decisionType: completionHadNoOutcome
|
|
2425
|
+
? "autopilot_completed_without_outcome"
|
|
2426
|
+
: "autopilot_failure",
|
|
2427
|
+
workstreamId: slice.workstreamId,
|
|
2428
|
+
agentId: slice.agentId,
|
|
2429
|
+
sourceSystem: "orgx-autopilot",
|
|
2430
|
+
conflictSource: completionHadNoOutcome
|
|
2431
|
+
? "slice_completed_without_outcome"
|
|
2432
|
+
: "slice_invalid_output",
|
|
2433
|
+
dedupeKey: [
|
|
2434
|
+
"autopilot",
|
|
2435
|
+
run.initiativeId,
|
|
2436
|
+
slice.workstreamId,
|
|
2437
|
+
completionHadNoOutcome
|
|
2438
|
+
? "slice_completed_without_outcome"
|
|
2439
|
+
: "slice_invalid_output",
|
|
2440
|
+
].join(":"),
|
|
2441
|
+
recommendedAction: nextActions[0] ??
|
|
2442
|
+
"Verify slice outputs and status updates, then retry once or pause for investigation.",
|
|
2443
|
+
sourceRunId: slice.runId,
|
|
2444
|
+
sourceRef: {
|
|
2445
|
+
run_id: slice.runId,
|
|
2446
|
+
workstream_id: slice.workstreamId,
|
|
2447
|
+
parsed_status: parsedStatus,
|
|
2448
|
+
},
|
|
2449
|
+
evidenceRefs: [
|
|
2450
|
+
{
|
|
2451
|
+
evidence_type: "slice_output_validation",
|
|
2452
|
+
title: "Slice output needs verification",
|
|
2453
|
+
summary: attentionSummary,
|
|
2454
|
+
source_pointer: slice.outputPath,
|
|
2455
|
+
payload: {
|
|
2456
|
+
log_path: slice.logPath,
|
|
2457
|
+
parsed_status: parsedStatus,
|
|
2458
|
+
completion_had_no_outcome: completionHadNoOutcome,
|
|
2459
|
+
},
|
|
2460
|
+
},
|
|
2461
|
+
...artifactEvidenceRefs,
|
|
2462
|
+
],
|
|
669
2463
|
});
|
|
2464
|
+
setLaneState(run, {
|
|
2465
|
+
workstreamId: slice.workstreamId,
|
|
2466
|
+
state: "blocked",
|
|
2467
|
+
activeRunId: null,
|
|
2468
|
+
activeTaskIds: [],
|
|
2469
|
+
blockedReason: slice.lastError ??
|
|
2470
|
+
(completionHadNoOutcome
|
|
2471
|
+
? "Slice completed without verifiable outcomes."
|
|
2472
|
+
: "Slice failed or returned invalid output."),
|
|
2473
|
+
waitingOnWorkstreamIds: [],
|
|
2474
|
+
retryAt: null,
|
|
2475
|
+
});
|
|
2476
|
+
if (!run.blockedWorkstreamIds.includes(slice.workstreamId)) {
|
|
2477
|
+
run.blockedWorkstreamIds.push(slice.workstreamId);
|
|
2478
|
+
}
|
|
670
2479
|
await stopAutoContinueRun({
|
|
671
2480
|
run,
|
|
672
2481
|
reason: completionHadNoOutcome ? "blocked" : "error",
|
|
@@ -674,13 +2483,31 @@ export function createAutoContinueEngine(deps) {
|
|
|
674
2483
|
(completionHadNoOutcome
|
|
675
2484
|
? "Slice completed without verifiable outcomes."
|
|
676
2485
|
: "Slice failed or returned invalid output."),
|
|
2486
|
+
decisionRequired: completionHadNoOutcome && decisionResult.queued,
|
|
2487
|
+
decisionIds: decisionResult.decisionIds,
|
|
677
2488
|
});
|
|
678
2489
|
return;
|
|
679
2490
|
}
|
|
680
2491
|
run.lastRunId = slice.runId;
|
|
681
|
-
run.lastTaskId =
|
|
682
|
-
run
|
|
683
|
-
|
|
2492
|
+
run.lastTaskId = slice.taskIds[0] ?? run.lastTaskId;
|
|
2493
|
+
removeActiveSliceFromRun(run, {
|
|
2494
|
+
sliceRunId: slice.runId,
|
|
2495
|
+
taskIds: slice.taskIds,
|
|
2496
|
+
workstreamId: slice.workstreamId,
|
|
2497
|
+
});
|
|
2498
|
+
setLaneState(run, {
|
|
2499
|
+
workstreamId: slice.workstreamId,
|
|
2500
|
+
state: "completed",
|
|
2501
|
+
activeRunId: null,
|
|
2502
|
+
activeTaskIds: [],
|
|
2503
|
+
blockedReason: null,
|
|
2504
|
+
waitingOnWorkstreamIds: [],
|
|
2505
|
+
retryAt: null,
|
|
2506
|
+
});
|
|
2507
|
+
run.blockedWorkstreamIds = run.blockedWorkstreamIds.filter((id) => id !== slice.workstreamId);
|
|
2508
|
+
syncLegacyRunPointers(run);
|
|
2509
|
+
// Do not keep prior rate-limit/runtime errors after a completed slice.
|
|
2510
|
+
run.lastError = null;
|
|
684
2511
|
run.updatedAt = now;
|
|
685
2512
|
try {
|
|
686
2513
|
await updateInitiativeAutoContinueState({
|
|
@@ -691,6 +2518,50 @@ export function createAutoContinueEngine(deps) {
|
|
|
691
2518
|
catch {
|
|
692
2519
|
// best effort
|
|
693
2520
|
}
|
|
2521
|
+
// Evaluate scope-level completion for milestone/workstream scopes.
|
|
2522
|
+
if (slice.scope && slice.scope !== "task") {
|
|
2523
|
+
try {
|
|
2524
|
+
const scopeGraph = applyLocalInitiativeOverrideToGraph(await buildMissionControlGraph(client, run.initiativeId));
|
|
2525
|
+
const scopeNodeById = new Map(scopeGraph.nodes.map((n) => [n.id, n]));
|
|
2526
|
+
const scopeResult = evaluateScopeCompletion({
|
|
2527
|
+
scope: slice.scope,
|
|
2528
|
+
milestoneIds: slice.scopeMilestoneIds ?? [],
|
|
2529
|
+
workstreamId: slice.workstreamId,
|
|
2530
|
+
nodeById: scopeNodeById,
|
|
2531
|
+
});
|
|
2532
|
+
if (scopeResult.scopeComplete) {
|
|
2533
|
+
await emitActivitySafe({
|
|
2534
|
+
initiativeId: run.initiativeId,
|
|
2535
|
+
runId: slice.runId,
|
|
2536
|
+
correlationId: slice.runId,
|
|
2537
|
+
phase: "completed",
|
|
2538
|
+
level: "info",
|
|
2539
|
+
progressPct: 100,
|
|
2540
|
+
nextStep: slice.scope === "milestone"
|
|
2541
|
+
? "Queue the next milestone-ready slice."
|
|
2542
|
+
: "Select the next dispatchable workstream from Next Up.",
|
|
2543
|
+
message: `${slice.scope === "milestone" ? "Milestone" : "Workstream"} scope completed for ${slice.workstreamTitle ?? slice.workstreamId}.`,
|
|
2544
|
+
metadata: {
|
|
2545
|
+
...buildSliceEnrichment({
|
|
2546
|
+
run,
|
|
2547
|
+
slice,
|
|
2548
|
+
workstreamId: slice.workstreamId,
|
|
2549
|
+
workstreamTitle: slice.workstreamTitle ?? null,
|
|
2550
|
+
domain: slice.domain,
|
|
2551
|
+
requiredSkills: slice.requiredSkills,
|
|
2552
|
+
event: "scope_completed",
|
|
2553
|
+
}),
|
|
2554
|
+
scope: slice.scope,
|
|
2555
|
+
milestone_ids: slice.scopeMilestoneIds,
|
|
2556
|
+
remaining_tasks: 0,
|
|
2557
|
+
},
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
catch {
|
|
2562
|
+
// best-effort scope completion check
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
694
2565
|
if (run.stopAfterSlice) {
|
|
695
2566
|
run.stopAfterSlice = false;
|
|
696
2567
|
await stopAutoContinueRun({ run, reason: "completed" });
|
|
@@ -702,17 +2573,26 @@ export function createAutoContinueEngine(deps) {
|
|
|
702
2573
|
}
|
|
703
2574
|
}
|
|
704
2575
|
}
|
|
2576
|
+
syncLegacyRunPointers(run);
|
|
705
2577
|
if (run.stopRequested) {
|
|
706
2578
|
run.status = "stopping";
|
|
707
2579
|
run.updatedAt = now;
|
|
708
2580
|
await stopAutoContinueRun({ run, reason: "stopped" });
|
|
709
2581
|
return;
|
|
710
2582
|
}
|
|
2583
|
+
const tokenBudgetValue = typeof run.tokenBudget === "number" && Number.isFinite(run.tokenBudget)
|
|
2584
|
+
? run.tokenBudget
|
|
2585
|
+
: null;
|
|
711
2586
|
// 2) Enforce token guardrail before starting a new slice.
|
|
712
|
-
if (run.tokensUsed >=
|
|
2587
|
+
if (tokenBudgetValue !== null && run.tokensUsed >= tokenBudgetValue) {
|
|
713
2588
|
await stopAutoContinueRun({ run, reason: "budget_exhausted" });
|
|
714
2589
|
return;
|
|
715
2590
|
}
|
|
2591
|
+
const activeSliceCount = listActiveSliceRunIds(run).length;
|
|
2592
|
+
if (activeSliceCount >= run.maxParallelSlices) {
|
|
2593
|
+
run.updatedAt = now;
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
716
2596
|
// 3) Pick next workstream slice and dispatch.
|
|
717
2597
|
let graph;
|
|
718
2598
|
try {
|
|
@@ -728,6 +2608,7 @@ export function createAutoContinueEngine(deps) {
|
|
|
728
2608
|
}
|
|
729
2609
|
const nodes = graph.nodes;
|
|
730
2610
|
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
2611
|
+
applyLocalStatusOverridesToGraph(run.initiativeId, nodeById);
|
|
731
2612
|
const taskNodes = nodes.filter((node) => node.type === "task");
|
|
732
2613
|
const todoTasks = taskNodes.filter((node) => isTodoStatus(node.status));
|
|
733
2614
|
if (todoTasks.length === 0) {
|
|
@@ -746,6 +2627,7 @@ export function createAutoContinueEngine(deps) {
|
|
|
746
2627
|
};
|
|
747
2628
|
// Select the next eligible workstream by scanning ordered todos.
|
|
748
2629
|
let selectedWorkstreamId = null;
|
|
2630
|
+
let deferredBySpawnGuardRateLimit = 0;
|
|
749
2631
|
for (const taskId of graph.recentTodos) {
|
|
750
2632
|
const node = nodeById.get(taskId);
|
|
751
2633
|
if (!node || node.type !== "task")
|
|
@@ -763,6 +2645,18 @@ export function createAutoContinueEngine(deps) {
|
|
|
763
2645
|
}
|
|
764
2646
|
if (!node.workstreamId)
|
|
765
2647
|
continue;
|
|
2648
|
+
if (run.blockedWorkstreamIds.includes(node.workstreamId))
|
|
2649
|
+
continue;
|
|
2650
|
+
const lane = run.laneByWorkstreamId[node.workstreamId] ?? null;
|
|
2651
|
+
if (lane?.state === "running" && lane.activeRunId)
|
|
2652
|
+
continue;
|
|
2653
|
+
if (lane?.state === "rate_limited" && lane.retryAt) {
|
|
2654
|
+
const retryAtMs = Date.parse(lane.retryAt);
|
|
2655
|
+
if (Number.isFinite(retryAtMs) && retryAtMs > Date.now()) {
|
|
2656
|
+
deferredBySpawnGuardRateLimit += 1;
|
|
2657
|
+
continue;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
766
2660
|
const ws = nodeById.get(node.workstreamId);
|
|
767
2661
|
if (ws && !isDispatchableWorkstreamStatus(ws.status))
|
|
768
2662
|
continue;
|
|
@@ -770,10 +2664,83 @@ export function createAutoContinueEngine(deps) {
|
|
|
770
2664
|
continue;
|
|
771
2665
|
if (taskHasBlockedParent(node))
|
|
772
2666
|
continue;
|
|
2667
|
+
const retryAtMs = getSpawnGuardRetryAtMs(run.initiativeId, node.id);
|
|
2668
|
+
if (retryAtMs > 0) {
|
|
2669
|
+
deferredBySpawnGuardRateLimit += 1;
|
|
2670
|
+
continue;
|
|
2671
|
+
}
|
|
773
2672
|
selectedWorkstreamId = node.workstreamId;
|
|
774
2673
|
break;
|
|
775
2674
|
}
|
|
776
2675
|
if (!selectedWorkstreamId) {
|
|
2676
|
+
const waitingByWorkstream = new Map();
|
|
2677
|
+
for (const task of taskNodes) {
|
|
2678
|
+
if (!isTodoStatus(task.status))
|
|
2679
|
+
continue;
|
|
2680
|
+
if (!run.includeVerification &&
|
|
2681
|
+
typeof task.title === "string" &&
|
|
2682
|
+
/^verification[ \t]+scenario/i.test(task.title)) {
|
|
2683
|
+
continue;
|
|
2684
|
+
}
|
|
2685
|
+
const workstreamId = (task.workstreamId ?? "").trim();
|
|
2686
|
+
if (!workstreamId)
|
|
2687
|
+
continue;
|
|
2688
|
+
if (Array.isArray(run.allowedWorkstreamIds) &&
|
|
2689
|
+
run.allowedWorkstreamIds.length > 0 &&
|
|
2690
|
+
!run.allowedWorkstreamIds.includes(workstreamId)) {
|
|
2691
|
+
continue;
|
|
2692
|
+
}
|
|
2693
|
+
if (run.blockedWorkstreamIds.includes(workstreamId)) {
|
|
2694
|
+
continue;
|
|
2695
|
+
}
|
|
2696
|
+
const blockedParents = taskHasBlockedParent(task);
|
|
2697
|
+
const unresolvedDepWorkstreamIds = task.dependencyIds
|
|
2698
|
+
.map((depId) => nodeById.get(depId))
|
|
2699
|
+
.filter((dep) => Boolean(dep && !isDoneStatus(dep.status)))
|
|
2700
|
+
.map((dep) => (dep.workstreamId ?? "").trim())
|
|
2701
|
+
.filter(Boolean);
|
|
2702
|
+
if (blockedParents || unresolvedDepWorkstreamIds.length > 0) {
|
|
2703
|
+
const existing = waitingByWorkstream.get(workstreamId) ?? [];
|
|
2704
|
+
waitingByWorkstream.set(workstreamId, dedupeStrings([...existing, ...unresolvedDepWorkstreamIds]));
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
for (const [workstreamId, waitingOnWorkstreamIds] of waitingByWorkstream.entries()) {
|
|
2708
|
+
setLaneState(run, {
|
|
2709
|
+
workstreamId,
|
|
2710
|
+
state: "waiting_dependency",
|
|
2711
|
+
activeRunId: null,
|
|
2712
|
+
activeTaskIds: [],
|
|
2713
|
+
blockedReason: null,
|
|
2714
|
+
waitingOnWorkstreamIds,
|
|
2715
|
+
retryAt: null,
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
if (listActiveSliceRunIds(run).length > 0) {
|
|
2719
|
+
run.updatedAt = now;
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
if (deferredBySpawnGuardRateLimit > 0) {
|
|
2723
|
+
run.updatedAt = now;
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
if (run.allowedWorkstreamIds && run.allowedWorkstreamIds.length > 0) {
|
|
2727
|
+
const scopedTodoCount = taskNodes.filter((node) => {
|
|
2728
|
+
if (!isTodoStatus(node.status))
|
|
2729
|
+
return false;
|
|
2730
|
+
if (!run.includeVerification &&
|
|
2731
|
+
typeof node.title === "string" &&
|
|
2732
|
+
/^verification[ \t]+scenario/i.test(node.title)) {
|
|
2733
|
+
return false;
|
|
2734
|
+
}
|
|
2735
|
+
if (!node.workstreamId)
|
|
2736
|
+
return false;
|
|
2737
|
+
return run.allowedWorkstreamIds?.includes(node.workstreamId) ?? false;
|
|
2738
|
+
}).length;
|
|
2739
|
+
if (scopedTodoCount === 0) {
|
|
2740
|
+
await stopAutoContinueRun({ run, reason: "completed" });
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
777
2744
|
await stopAutoContinueRun({ run, reason: "blocked" });
|
|
778
2745
|
return;
|
|
779
2746
|
}
|
|
@@ -781,19 +2748,21 @@ export function createAutoContinueEngine(deps) {
|
|
|
781
2748
|
const workstreamTitle = workstreamNode?.title ?? null;
|
|
782
2749
|
const initiativeNode = nodes.find((node) => node.type === "initiative") ?? null;
|
|
783
2750
|
const initiativeTitle = initiativeNode?.title ?? `Initiative ${run.initiativeId.slice(0, 8)}`;
|
|
784
|
-
const
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
!/^verification[ \t]+scenario/i.test(String(node.title ?? "")))))
|
|
794
|
-
.slice(0, AUTO_CONTINUE_SLICE_MAX_TASKS);
|
|
2751
|
+
const scopeSelection = selectSliceTasksByScope({
|
|
2752
|
+
scope: run.scope,
|
|
2753
|
+
workstreamId: selectedWorkstreamId,
|
|
2754
|
+
recentTodos: graph.recentTodos,
|
|
2755
|
+
nodeById,
|
|
2756
|
+
includeVerification: run.includeVerification,
|
|
2757
|
+
});
|
|
2758
|
+
const sliceTaskNodes = scopeSelection.tasks;
|
|
2759
|
+
const scopeMilestoneIds = scopeSelection.milestoneIds;
|
|
795
2760
|
const primaryTask = sliceTaskNodes[0] ?? null;
|
|
796
2761
|
if (!primaryTask) {
|
|
2762
|
+
if (listActiveSliceRunIds(run).length > 0) {
|
|
2763
|
+
run.updatedAt = now;
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
797
2766
|
await stopAutoContinueRun({ run, reason: "blocked" });
|
|
798
2767
|
return;
|
|
799
2768
|
}
|
|
@@ -803,14 +2772,14 @@ export function createAutoContinueEngine(deps) {
|
|
|
803
2772
|
? Math.max(0, t.expectedDurationHours)
|
|
804
2773
|
: 0), 0);
|
|
805
2774
|
let tokenEstimate = estimateTokensForDurationHours(expectedDurationHours);
|
|
806
|
-
const remainingTokens =
|
|
807
|
-
if (remainingTokens <= 0) {
|
|
2775
|
+
const remainingTokens = tokenBudgetValue !== null ? tokenBudgetValue - run.tokensUsed : null;
|
|
2776
|
+
if (remainingTokens !== null && remainingTokens <= 0) {
|
|
808
2777
|
await stopAutoContinueRun({ run, reason: "budget_exhausted" });
|
|
809
2778
|
return;
|
|
810
2779
|
}
|
|
811
2780
|
// If the modeled slice exceeds the remaining budget, shrink the slice to fit rather than
|
|
812
2781
|
// stopping immediately (Play should still dispatch at least the primary task when possible).
|
|
813
|
-
if (tokenEstimate > 0 && tokenEstimate > remainingTokens) {
|
|
2782
|
+
if (remainingTokens !== null && tokenEstimate > 0 && tokenEstimate > remainingTokens) {
|
|
814
2783
|
const nextSlice = [];
|
|
815
2784
|
let hours = 0;
|
|
816
2785
|
for (const task of sliceTaskNodes) {
|
|
@@ -832,12 +2801,300 @@ export function createAutoContinueEngine(deps) {
|
|
|
832
2801
|
expectedDurationHours = hours;
|
|
833
2802
|
tokenEstimate = estimateTokensForDurationHours(expectedDurationHours);
|
|
834
2803
|
}
|
|
835
|
-
if (tokenEstimate > 0 && tokenEstimate > remainingTokens) {
|
|
2804
|
+
if (remainingTokens !== null && tokenEstimate > 0 && tokenEstimate > remainingTokens) {
|
|
836
2805
|
await stopAutoContinueRun({ run, reason: "budget_exhausted" });
|
|
837
2806
|
return;
|
|
838
2807
|
}
|
|
839
2808
|
const executionPolicy = deriveExecutionPolicy(primaryTask, workstreamNode);
|
|
2809
|
+
const behaviorConfig = deriveBehaviorConfigContext(primaryTask, workstreamNode);
|
|
2810
|
+
const behaviorAutomationLevel = deriveBehaviorAutomationLevel(primaryTask, workstreamNode);
|
|
840
2811
|
const sliceRunId = randomUUID();
|
|
2812
|
+
await emitActivitySafe({
|
|
2813
|
+
initiativeId: run.initiativeId,
|
|
2814
|
+
runId: sliceRunId,
|
|
2815
|
+
correlationId: sliceRunId,
|
|
2816
|
+
phase: "intent",
|
|
2817
|
+
level: "info",
|
|
2818
|
+
progressPct: 5,
|
|
2819
|
+
message: `Orchestrator selected ${workstreamTitle ?? selectedWorkstreamId} for the next slice.`,
|
|
2820
|
+
nextStep: `Preparing dispatch checks before spawning ${executionPolicy.domain} execution.`,
|
|
2821
|
+
metadata: {
|
|
2822
|
+
...buildSliceEnrichment({
|
|
2823
|
+
run,
|
|
2824
|
+
taskId: primaryTask.id,
|
|
2825
|
+
taskTitle: primaryTask.title ?? null,
|
|
2826
|
+
workstreamId: selectedWorkstreamId,
|
|
2827
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
2828
|
+
domain: executionPolicy.domain,
|
|
2829
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
2830
|
+
event: "orchestrator_dispatch",
|
|
2831
|
+
}),
|
|
2832
|
+
scope: run.scope,
|
|
2833
|
+
candidate_task_count: sliceTaskNodes.length,
|
|
2834
|
+
},
|
|
2835
|
+
});
|
|
2836
|
+
const behaviorConfigDrift = detectBehaviorConfigDrift({
|
|
2837
|
+
taskNode: primaryTask,
|
|
2838
|
+
workstreamNode,
|
|
2839
|
+
behaviorConfig,
|
|
2840
|
+
behaviorAutomationLevel,
|
|
2841
|
+
});
|
|
2842
|
+
if (behaviorConfigDrift) {
|
|
2843
|
+
await emitActivitySafe({
|
|
2844
|
+
initiativeId: run.initiativeId,
|
|
2845
|
+
runId: sliceRunId,
|
|
2846
|
+
correlationId: sliceRunId,
|
|
2847
|
+
phase: "review",
|
|
2848
|
+
level: "warn",
|
|
2849
|
+
progressPct: 15,
|
|
2850
|
+
message: `Behavior config drift detected for ${workstreamTitle ?? selectedWorkstreamId}; ` +
|
|
2851
|
+
`runtime behavior differs from declared workstream config.`,
|
|
2852
|
+
metadata: {
|
|
2853
|
+
...buildSliceEnrichment({
|
|
2854
|
+
run,
|
|
2855
|
+
taskId: primaryTask.id,
|
|
2856
|
+
taskTitle: primaryTask.title ?? null,
|
|
2857
|
+
workstreamId: selectedWorkstreamId,
|
|
2858
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
2859
|
+
domain: executionPolicy.domain,
|
|
2860
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
2861
|
+
event: "auto_continue_behavior_config_drift_detected",
|
|
2862
|
+
}),
|
|
2863
|
+
drift_fields: behaviorConfigDrift.fields,
|
|
2864
|
+
declared_behavior_config_id: behaviorConfigDrift.declared.configId,
|
|
2865
|
+
declared_behavior_config_version: behaviorConfigDrift.declared.version,
|
|
2866
|
+
declared_behavior_config_hash: behaviorConfigDrift.declared.hash,
|
|
2867
|
+
declared_policy_source: behaviorConfigDrift.declared.policySource,
|
|
2868
|
+
declared_behavior_context: behaviorConfigDrift.declared.context,
|
|
2869
|
+
declared_behavior_automation_level: behaviorConfigDrift.declared.automationLevel,
|
|
2870
|
+
runtime_behavior_config_id: behaviorConfigDrift.runtime.configId,
|
|
2871
|
+
runtime_behavior_config_version: behaviorConfigDrift.runtime.version,
|
|
2872
|
+
runtime_behavior_config_hash: behaviorConfigDrift.runtime.hash,
|
|
2873
|
+
runtime_policy_source: behaviorConfigDrift.runtime.policySource,
|
|
2874
|
+
runtime_behavior_context: behaviorConfigDrift.runtime.context,
|
|
2875
|
+
runtime_behavior_automation_level: behaviorConfigDrift.runtime.automationLevel,
|
|
2876
|
+
error_location: "mission-control.auto-continue.engine.behavior-config.drift",
|
|
2877
|
+
},
|
|
2878
|
+
nextStep: "Review task/workstream behavior metadata and reconcile the declared config if override is unintended.",
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
if (behaviorConfig.requiresApproval) {
|
|
2882
|
+
const blockedReason = `Behavior config approval required before dispatch for ${workstreamTitle ?? selectedWorkstreamId}.`;
|
|
2883
|
+
await emitActivitySafe({
|
|
2884
|
+
initiativeId: run.initiativeId,
|
|
2885
|
+
runId: sliceRunId,
|
|
2886
|
+
correlationId: sliceRunId,
|
|
2887
|
+
phase: "blocked",
|
|
2888
|
+
level: "warn",
|
|
2889
|
+
progressPct: 20,
|
|
2890
|
+
message: blockedReason,
|
|
2891
|
+
metadata: {
|
|
2892
|
+
...buildSliceEnrichment({
|
|
2893
|
+
run,
|
|
2894
|
+
taskId: primaryTask.id,
|
|
2895
|
+
taskTitle: primaryTask.title ?? null,
|
|
2896
|
+
workstreamId: selectedWorkstreamId,
|
|
2897
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
2898
|
+
domain: executionPolicy.domain,
|
|
2899
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
2900
|
+
event: "auto_continue_behavior_config_approval_required",
|
|
2901
|
+
}),
|
|
2902
|
+
behavior_config_id: behaviorConfig.configId,
|
|
2903
|
+
behavior_config_version: behaviorConfig.version,
|
|
2904
|
+
behavior_config_hash: behaviorConfig.hash,
|
|
2905
|
+
behavior_approval_status: behaviorConfig.approvalStatus,
|
|
2906
|
+
behavior_approval_decision_id: behaviorConfig.approvalDecisionId,
|
|
2907
|
+
blocked_reason: blockedReason,
|
|
2908
|
+
error_location: "mission-control.auto-continue.engine.behavior-config.approval",
|
|
2909
|
+
},
|
|
2910
|
+
nextStep: "Approve the behavior config, then rerun Play/auto-continue for this workstream.",
|
|
2911
|
+
});
|
|
2912
|
+
const decisionResult = await requestDecisionQueued({
|
|
2913
|
+
initiativeId: run.initiativeId,
|
|
2914
|
+
correlationId: sliceRunId,
|
|
2915
|
+
title: `Approve behavior config for ${workstreamTitle ?? selectedWorkstreamId}`,
|
|
2916
|
+
summary: [
|
|
2917
|
+
`Autopilot paused before dispatch because behavior config requires approval.`,
|
|
2918
|
+
`Task: ${primaryTask.id}.`,
|
|
2919
|
+
behaviorConfig.configId ? `Config: ${behaviorConfig.configId}.` : "",
|
|
2920
|
+
behaviorConfig.version ? `Version: ${behaviorConfig.version}.` : "",
|
|
2921
|
+
behaviorConfig.approvalStatus ? `Approval status: ${behaviorConfig.approvalStatus}.` : "",
|
|
2922
|
+
]
|
|
2923
|
+
.filter(Boolean)
|
|
2924
|
+
.join(" "),
|
|
2925
|
+
urgency: "high",
|
|
2926
|
+
options: [
|
|
2927
|
+
"Approve config and continue execution",
|
|
2928
|
+
"Reject config and revise policy",
|
|
2929
|
+
"Pause this workstream",
|
|
2930
|
+
],
|
|
2931
|
+
blocking: true,
|
|
2932
|
+
decisionType: "autopilot_behavior_config_approval",
|
|
2933
|
+
workstreamId: selectedWorkstreamId,
|
|
2934
|
+
agentId: run.agentId,
|
|
2935
|
+
sourceSystem: "orgx-autopilot",
|
|
2936
|
+
conflictSource: "behavior_config_requires_approval",
|
|
2937
|
+
dedupeKey: [
|
|
2938
|
+
"autopilot",
|
|
2939
|
+
run.initiativeId,
|
|
2940
|
+
selectedWorkstreamId,
|
|
2941
|
+
"behavior_config_requires_approval",
|
|
2942
|
+
behaviorConfig.configId ?? "default",
|
|
2943
|
+
behaviorConfig.version ?? "unknown",
|
|
2944
|
+
].join(":"),
|
|
2945
|
+
recommendedAction: "Resolve approval state before allowing autopilot to spawn a worker.",
|
|
2946
|
+
sourceRunId: sliceRunId,
|
|
2947
|
+
sourceRef: {
|
|
2948
|
+
run_id: sliceRunId,
|
|
2949
|
+
workstream_id: selectedWorkstreamId,
|
|
2950
|
+
task_id: primaryTask.id,
|
|
2951
|
+
behavior_config_id: behaviorConfig.configId,
|
|
2952
|
+
behavior_approval_status: behaviorConfig.approvalStatus,
|
|
2953
|
+
behavior_approval_decision_id: behaviorConfig.approvalDecisionId,
|
|
2954
|
+
},
|
|
2955
|
+
});
|
|
2956
|
+
if (!run.blockedWorkstreamIds.includes(selectedWorkstreamId)) {
|
|
2957
|
+
run.blockedWorkstreamIds.push(selectedWorkstreamId);
|
|
2958
|
+
}
|
|
2959
|
+
setLaneState(run, {
|
|
2960
|
+
workstreamId: selectedWorkstreamId,
|
|
2961
|
+
state: "blocked",
|
|
2962
|
+
activeRunId: null,
|
|
2963
|
+
activeTaskIds: [],
|
|
2964
|
+
blockedReason,
|
|
2965
|
+
waitingOnWorkstreamIds: [],
|
|
2966
|
+
retryAt: null,
|
|
2967
|
+
});
|
|
2968
|
+
await stopAutoContinueRun({
|
|
2969
|
+
run,
|
|
2970
|
+
reason: "blocked",
|
|
2971
|
+
error: blockedReason,
|
|
2972
|
+
decisionRequired: decisionResult.queued,
|
|
2973
|
+
decisionIds: decisionResult.decisionIds,
|
|
2974
|
+
});
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2977
|
+
const isManualPlayDispatch = run.stopAfterSlice &&
|
|
2978
|
+
Array.isArray(run.allowedWorkstreamIds) &&
|
|
2979
|
+
run.allowedWorkstreamIds.length === 1;
|
|
2980
|
+
if (behaviorAutomationLevel === "manual" && !isManualPlayDispatch) {
|
|
2981
|
+
const blockedReason = `Automation level manual prevents auto-continue dispatch for ${workstreamTitle ?? selectedWorkstreamId}.`;
|
|
2982
|
+
await emitActivitySafe({
|
|
2983
|
+
initiativeId: run.initiativeId,
|
|
2984
|
+
runId: sliceRunId,
|
|
2985
|
+
correlationId: sliceRunId,
|
|
2986
|
+
phase: "blocked",
|
|
2987
|
+
level: "warn",
|
|
2988
|
+
progressPct: 20,
|
|
2989
|
+
message: blockedReason,
|
|
2990
|
+
metadata: {
|
|
2991
|
+
...buildSliceEnrichment({
|
|
2992
|
+
run,
|
|
2993
|
+
taskId: primaryTask.id,
|
|
2994
|
+
taskTitle: primaryTask.title ?? null,
|
|
2995
|
+
workstreamId: selectedWorkstreamId,
|
|
2996
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
2997
|
+
domain: executionPolicy.domain,
|
|
2998
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
2999
|
+
event: "auto_continue_behavior_automation_manual_blocked",
|
|
3000
|
+
}),
|
|
3001
|
+
behavior_config_id: behaviorConfig.configId,
|
|
3002
|
+
behavior_config_version: behaviorConfig.version,
|
|
3003
|
+
behavior_automation_level: behaviorAutomationLevel,
|
|
3004
|
+
blocked_reason: blockedReason,
|
|
3005
|
+
error_location: "mission-control.auto-continue.engine.behavior.automation.manual",
|
|
3006
|
+
},
|
|
3007
|
+
nextStep: "Use manual Play to dispatch this workstream slice.",
|
|
3008
|
+
});
|
|
3009
|
+
const decisionResult = await requestDecisionQueued({
|
|
3010
|
+
initiativeId: run.initiativeId,
|
|
3011
|
+
correlationId: sliceRunId,
|
|
3012
|
+
title: `Manual dispatch required for ${workstreamTitle ?? selectedWorkstreamId}`,
|
|
3013
|
+
summary: [
|
|
3014
|
+
"Autopilot paused because behavior automation level is manual.",
|
|
3015
|
+
`Task: ${primaryTask.id}.`,
|
|
3016
|
+
behaviorConfig.configId ? `Config: ${behaviorConfig.configId}.` : "",
|
|
3017
|
+
behaviorConfig.version ? `Version: ${behaviorConfig.version}.` : "",
|
|
3018
|
+
]
|
|
3019
|
+
.filter(Boolean)
|
|
3020
|
+
.join(" "),
|
|
3021
|
+
urgency: "high",
|
|
3022
|
+
options: [
|
|
3023
|
+
"Dispatch this workstream manually now",
|
|
3024
|
+
"Switch automation level to supervised",
|
|
3025
|
+
"Switch automation level to auto",
|
|
3026
|
+
],
|
|
3027
|
+
blocking: true,
|
|
3028
|
+
decisionType: "autopilot_behavior_manual_dispatch_required",
|
|
3029
|
+
workstreamId: selectedWorkstreamId,
|
|
3030
|
+
agentId: run.agentId,
|
|
3031
|
+
sourceSystem: "orgx-autopilot",
|
|
3032
|
+
conflictSource: "behavior_automation_level_manual",
|
|
3033
|
+
dedupeKey: [
|
|
3034
|
+
"autopilot",
|
|
3035
|
+
run.initiativeId,
|
|
3036
|
+
selectedWorkstreamId,
|
|
3037
|
+
"behavior_automation_level_manual",
|
|
3038
|
+
behaviorConfig.configId ?? "default",
|
|
3039
|
+
behaviorConfig.version ?? "unknown",
|
|
3040
|
+
].join(":"),
|
|
3041
|
+
recommendedAction: "Dispatch manually for this workstream, or switch behavior automation level before rerunning auto-continue.",
|
|
3042
|
+
sourceRunId: sliceRunId,
|
|
3043
|
+
sourceRef: {
|
|
3044
|
+
run_id: sliceRunId,
|
|
3045
|
+
workstream_id: selectedWorkstreamId,
|
|
3046
|
+
task_id: primaryTask.id,
|
|
3047
|
+
behavior_config_id: behaviorConfig.configId,
|
|
3048
|
+
behavior_automation_level: behaviorAutomationLevel,
|
|
3049
|
+
},
|
|
3050
|
+
});
|
|
3051
|
+
if (!run.blockedWorkstreamIds.includes(selectedWorkstreamId)) {
|
|
3052
|
+
run.blockedWorkstreamIds.push(selectedWorkstreamId);
|
|
3053
|
+
}
|
|
3054
|
+
setLaneState(run, {
|
|
3055
|
+
workstreamId: selectedWorkstreamId,
|
|
3056
|
+
state: "blocked",
|
|
3057
|
+
activeRunId: null,
|
|
3058
|
+
activeTaskIds: [],
|
|
3059
|
+
blockedReason,
|
|
3060
|
+
waitingOnWorkstreamIds: [],
|
|
3061
|
+
retryAt: null,
|
|
3062
|
+
});
|
|
3063
|
+
await stopAutoContinueRun({
|
|
3064
|
+
run,
|
|
3065
|
+
reason: "blocked",
|
|
3066
|
+
error: blockedReason,
|
|
3067
|
+
decisionRequired: decisionResult.queued,
|
|
3068
|
+
decisionIds: decisionResult.decisionIds,
|
|
3069
|
+
});
|
|
3070
|
+
return;
|
|
3071
|
+
}
|
|
3072
|
+
if (behaviorAutomationLevel === "supervised" && !run.stopAfterSlice) {
|
|
3073
|
+
run.stopAfterSlice = true;
|
|
3074
|
+
await emitActivitySafe({
|
|
3075
|
+
initiativeId: run.initiativeId,
|
|
3076
|
+
runId: sliceRunId,
|
|
3077
|
+
correlationId: sliceRunId,
|
|
3078
|
+
phase: "execution",
|
|
3079
|
+
level: "info",
|
|
3080
|
+
progressPct: 25,
|
|
3081
|
+
message: `Supervised automation level: dispatching one slice for ${workstreamTitle ?? selectedWorkstreamId}.`,
|
|
3082
|
+
metadata: {
|
|
3083
|
+
...buildSliceEnrichment({
|
|
3084
|
+
run,
|
|
3085
|
+
taskId: primaryTask.id,
|
|
3086
|
+
taskTitle: primaryTask.title ?? null,
|
|
3087
|
+
workstreamId: selectedWorkstreamId,
|
|
3088
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
3089
|
+
domain: executionPolicy.domain,
|
|
3090
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
3091
|
+
event: "auto_continue_behavior_automation_supervised_one_shot",
|
|
3092
|
+
}),
|
|
3093
|
+
behavior_automation_level: behaviorAutomationLevel,
|
|
3094
|
+
},
|
|
3095
|
+
nextStep: "Resume to dispatch the next slice after this one completes.",
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
841
3098
|
const spawnGuardResult = await checkSpawnGuardSafe({
|
|
842
3099
|
domain: executionPolicy.domain,
|
|
843
3100
|
taskId: primaryTask.id,
|
|
@@ -850,60 +3107,224 @@ export function createAutoContinueEngine(deps) {
|
|
|
850
3107
|
const allowed = spawnGuardResult.allowed;
|
|
851
3108
|
if (allowed === false) {
|
|
852
3109
|
const blockedReason = summarizeSpawnGuardBlockReason(spawnGuardResult);
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
}
|
|
860
|
-
try {
|
|
861
|
-
await syncParentRollupsForTask({
|
|
3110
|
+
const retryable = spawnGuardIsRateLimited(spawnGuardResult);
|
|
3111
|
+
const rateLimitOverrideRequested = retryable && run.ignoreSpawnGuardRateLimit;
|
|
3112
|
+
if (retryable && !rateLimitOverrideRequested) {
|
|
3113
|
+
const retryAtMs = Date.now() + AUTO_CONTINUE_SPAWN_GUARD_RETRY_MS;
|
|
3114
|
+
const retryAtIso = new Date(retryAtMs).toISOString();
|
|
3115
|
+
autoContinueSpawnGuardRetryByTask.set(primaryTask.id, {
|
|
862
3116
|
initiativeId: run.initiativeId,
|
|
863
|
-
|
|
3117
|
+
retryAtMs,
|
|
3118
|
+
});
|
|
3119
|
+
setLaneState(run, {
|
|
864
3120
|
workstreamId: selectedWorkstreamId,
|
|
865
|
-
|
|
3121
|
+
state: "rate_limited",
|
|
3122
|
+
activeRunId: null,
|
|
3123
|
+
activeTaskIds: [],
|
|
3124
|
+
blockedReason,
|
|
3125
|
+
waitingOnWorkstreamIds: [],
|
|
3126
|
+
retryAt: retryAtIso,
|
|
3127
|
+
});
|
|
3128
|
+
await emitActivitySafe({
|
|
3129
|
+
initiativeId: run.initiativeId,
|
|
3130
|
+
runId: sliceRunId,
|
|
866
3131
|
correlationId: sliceRunId,
|
|
3132
|
+
phase: "blocked",
|
|
3133
|
+
level: "warn",
|
|
3134
|
+
progressPct: 25,
|
|
3135
|
+
message: `Autopilot spawn guard rate-limited ${workstreamTitle ?? selectedWorkstreamId}; retrying shortly.`,
|
|
3136
|
+
metadata: {
|
|
3137
|
+
...buildSliceEnrichment({
|
|
3138
|
+
run,
|
|
3139
|
+
taskId: primaryTask.id,
|
|
3140
|
+
taskTitle: primaryTask.title ?? null,
|
|
3141
|
+
workstreamId: selectedWorkstreamId,
|
|
3142
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
3143
|
+
domain: executionPolicy.domain,
|
|
3144
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
3145
|
+
event: "auto_continue_spawn_guard_rate_limited",
|
|
3146
|
+
}),
|
|
3147
|
+
blocked_reason: blockedReason,
|
|
3148
|
+
error_location: "mission-control.auto-continue.engine.spawn-guard.rate-limited",
|
|
3149
|
+
next_retry_at: retryAtIso,
|
|
3150
|
+
next_retry_in_ms: AUTO_CONTINUE_SPAWN_GUARD_RETRY_MS,
|
|
3151
|
+
spawn_guard: spawnGuardResult,
|
|
3152
|
+
},
|
|
3153
|
+
nextStep: "Retry dispatch when spawn rate limits recover.",
|
|
867
3154
|
});
|
|
3155
|
+
run.lastError = blockedReason;
|
|
3156
|
+
run.updatedAt = now;
|
|
3157
|
+
syncLegacyRunPointers(run);
|
|
3158
|
+
try {
|
|
3159
|
+
await updateInitiativeAutoContinueState({
|
|
3160
|
+
initiativeId: run.initiativeId,
|
|
3161
|
+
run,
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
catch {
|
|
3165
|
+
// best effort
|
|
3166
|
+
}
|
|
3167
|
+
return;
|
|
868
3168
|
}
|
|
869
|
-
|
|
870
|
-
|
|
3169
|
+
if (rateLimitOverrideRequested) {
|
|
3170
|
+
const overrideMode = run.stopAfterSlice &&
|
|
3171
|
+
Array.isArray(run.allowedWorkstreamIds) &&
|
|
3172
|
+
run.allowedWorkstreamIds.length === 1
|
|
3173
|
+
? "Play"
|
|
3174
|
+
: "Auto-continue";
|
|
3175
|
+
await emitActivitySafe({
|
|
3176
|
+
initiativeId: run.initiativeId,
|
|
3177
|
+
runId: sliceRunId,
|
|
3178
|
+
correlationId: sliceRunId,
|
|
3179
|
+
phase: "execution",
|
|
3180
|
+
level: "warn",
|
|
3181
|
+
progressPct: 25,
|
|
3182
|
+
message: `${overrideMode} override: dispatching ${workstreamTitle ?? selectedWorkstreamId} despite spawn guard rate limit.`,
|
|
3183
|
+
metadata: {
|
|
3184
|
+
...buildSliceEnrichment({
|
|
3185
|
+
run,
|
|
3186
|
+
taskId: primaryTask.id,
|
|
3187
|
+
taskTitle: primaryTask.title ?? null,
|
|
3188
|
+
workstreamId: selectedWorkstreamId,
|
|
3189
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
3190
|
+
domain: executionPolicy.domain,
|
|
3191
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
3192
|
+
event: "auto_continue_spawn_guard_rate_limit_overridden",
|
|
3193
|
+
}),
|
|
3194
|
+
blocked_reason: blockedReason,
|
|
3195
|
+
error_location: "mission-control.auto-continue.engine.spawn-guard.override",
|
|
3196
|
+
spawn_guard: spawnGuardResult,
|
|
3197
|
+
},
|
|
3198
|
+
nextStep: "Manual Play requested immediate execution for this single workstream slice.",
|
|
3199
|
+
});
|
|
3200
|
+
run.lastError = null;
|
|
3201
|
+
run.updatedAt = now;
|
|
3202
|
+
setLaneState(run, {
|
|
3203
|
+
workstreamId: selectedWorkstreamId,
|
|
3204
|
+
state: "idle",
|
|
3205
|
+
activeRunId: null,
|
|
3206
|
+
activeTaskIds: [],
|
|
3207
|
+
blockedReason: null,
|
|
3208
|
+
waitingOnWorkstreamIds: [],
|
|
3209
|
+
retryAt: null,
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
else {
|
|
3213
|
+
// Maintain existing behavior: mark the primary task blocked when a quality gate denies dispatch.
|
|
3214
|
+
try {
|
|
3215
|
+
await client.updateEntity("task", primaryTask.id, { status: "blocked" });
|
|
3216
|
+
}
|
|
3217
|
+
catch {
|
|
3218
|
+
// best effort
|
|
3219
|
+
}
|
|
3220
|
+
try {
|
|
3221
|
+
await syncParentRollupsForTask({
|
|
3222
|
+
initiativeId: run.initiativeId,
|
|
3223
|
+
taskId: primaryTask.id,
|
|
3224
|
+
workstreamId: selectedWorkstreamId,
|
|
3225
|
+
milestoneId: primaryTask.milestoneId,
|
|
3226
|
+
correlationId: sliceRunId,
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
catch {
|
|
3230
|
+
// best effort
|
|
3231
|
+
}
|
|
3232
|
+
await emitActivitySafe({
|
|
3233
|
+
initiativeId: run.initiativeId,
|
|
3234
|
+
runId: sliceRunId,
|
|
3235
|
+
correlationId: sliceRunId,
|
|
3236
|
+
phase: "blocked",
|
|
3237
|
+
level: "error",
|
|
3238
|
+
progressPct: 25,
|
|
3239
|
+
message: `Autopilot blocked by spawn guard for ${workstreamTitle ?? selectedWorkstreamId}.`,
|
|
3240
|
+
metadata: {
|
|
3241
|
+
...buildSliceEnrichment({
|
|
3242
|
+
run,
|
|
3243
|
+
taskId: primaryTask.id,
|
|
3244
|
+
taskTitle: primaryTask.title ?? null,
|
|
3245
|
+
workstreamId: selectedWorkstreamId,
|
|
3246
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
3247
|
+
domain: executionPolicy.domain,
|
|
3248
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
3249
|
+
event: "auto_continue_spawn_guard_blocked",
|
|
3250
|
+
}),
|
|
3251
|
+
blocked_reason: blockedReason,
|
|
3252
|
+
error_location: "mission-control.auto-continue.engine.spawn-guard.blocked",
|
|
3253
|
+
spawn_guard: spawnGuardResult,
|
|
3254
|
+
},
|
|
3255
|
+
});
|
|
3256
|
+
const decisionResult = await requestDecisionQueued({
|
|
3257
|
+
initiativeId: run.initiativeId,
|
|
3258
|
+
correlationId: sliceRunId,
|
|
3259
|
+
title: `Unblock autopilot for ${workstreamTitle ?? selectedWorkstreamId}`,
|
|
3260
|
+
summary: [
|
|
3261
|
+
`Spawn guard denied dispatch for primary task ${primaryTask.id}.`,
|
|
3262
|
+
`Reason: ${blockedReason}`,
|
|
3263
|
+
`Domain: ${executionPolicy.domain}`,
|
|
3264
|
+
`Required skills: ${executionPolicy.requiredSkills.join(", ")}`,
|
|
3265
|
+
].join(" "),
|
|
3266
|
+
urgency: "high",
|
|
3267
|
+
options: [
|
|
3268
|
+
"Approve exception and continue",
|
|
3269
|
+
"Reassign slice/domain",
|
|
3270
|
+
"Pause and investigate quality gate",
|
|
3271
|
+
],
|
|
3272
|
+
blocking: true,
|
|
3273
|
+
decisionType: "autopilot_spawn_guard_block",
|
|
3274
|
+
workstreamId: selectedWorkstreamId,
|
|
3275
|
+
agentId: run.agentId,
|
|
3276
|
+
sourceSystem: "orgx-autopilot",
|
|
3277
|
+
conflictSource: "spawn_guard_blocked",
|
|
3278
|
+
dedupeKey: [
|
|
3279
|
+
"autopilot",
|
|
3280
|
+
run.initiativeId,
|
|
3281
|
+
selectedWorkstreamId,
|
|
3282
|
+
"spawn_guard_blocked",
|
|
3283
|
+
executionPolicy.domain,
|
|
3284
|
+
].join(":"),
|
|
3285
|
+
recommendedAction: "Choose exception, reassignment, or pause so dispatch can proceed safely.",
|
|
3286
|
+
sourceRunId: sliceRunId,
|
|
3287
|
+
sourceRef: {
|
|
3288
|
+
run_id: sliceRunId,
|
|
3289
|
+
workstream_id: selectedWorkstreamId,
|
|
3290
|
+
task_id: primaryTask.id,
|
|
3291
|
+
domain: executionPolicy.domain,
|
|
3292
|
+
},
|
|
3293
|
+
evidenceRefs: [
|
|
3294
|
+
{
|
|
3295
|
+
evidence_type: "spawn_guard_result",
|
|
3296
|
+
title: "Spawn guard denied dispatch",
|
|
3297
|
+
summary: blockedReason,
|
|
3298
|
+
source_pointer: null,
|
|
3299
|
+
payload: {
|
|
3300
|
+
spawn_guard: spawnGuardResult,
|
|
3301
|
+
task_id: primaryTask.id,
|
|
3302
|
+
domain: executionPolicy.domain,
|
|
3303
|
+
},
|
|
3304
|
+
},
|
|
3305
|
+
],
|
|
3306
|
+
});
|
|
3307
|
+
if (!run.blockedWorkstreamIds.includes(selectedWorkstreamId)) {
|
|
3308
|
+
run.blockedWorkstreamIds.push(selectedWorkstreamId);
|
|
3309
|
+
}
|
|
3310
|
+
setLaneState(run, {
|
|
3311
|
+
workstreamId: selectedWorkstreamId,
|
|
3312
|
+
state: "blocked",
|
|
3313
|
+
activeRunId: null,
|
|
3314
|
+
activeTaskIds: [],
|
|
3315
|
+
blockedReason,
|
|
3316
|
+
waitingOnWorkstreamIds: [],
|
|
3317
|
+
retryAt: null,
|
|
3318
|
+
});
|
|
3319
|
+
await stopAutoContinueRun({
|
|
3320
|
+
run,
|
|
3321
|
+
reason: "blocked",
|
|
3322
|
+
error: blockedReason,
|
|
3323
|
+
decisionRequired: decisionResult.queued,
|
|
3324
|
+
decisionIds: decisionResult.decisionIds,
|
|
3325
|
+
});
|
|
3326
|
+
return;
|
|
871
3327
|
}
|
|
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
3328
|
}
|
|
908
3329
|
}
|
|
909
3330
|
const milestoneIds = dedupeStrings(cappedSliceTaskNodes.map((t) => (t.milestoneId ?? "").trim()).filter(Boolean));
|
|
@@ -918,25 +3339,95 @@ export function createAutoContinueEngine(deps) {
|
|
|
918
3339
|
milestoneId: t.milestoneId ?? null,
|
|
919
3340
|
}));
|
|
920
3341
|
const schemaPath = ensureAutopilotSliceSchemaPath(AUTO_CONTINUE_SLICE_SCHEMA_FILENAME);
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
3342
|
+
// Try server KickoffContext (includes team context, acceptance criteria, etc.)
|
|
3343
|
+
let prompt;
|
|
3344
|
+
let kickoffContextHash = null;
|
|
3345
|
+
let kickoffRuntimeSettings = null;
|
|
3346
|
+
if (fetchKickoffContextSafeFn && renderKickoffMessageFn) {
|
|
3347
|
+
let kickoff = null;
|
|
3348
|
+
try {
|
|
3349
|
+
kickoff = await fetchKickoffContextSafeFn(client, {
|
|
3350
|
+
initiative_id: run.initiativeId,
|
|
3351
|
+
workstream_id: selectedWorkstreamId,
|
|
3352
|
+
task_id: primaryTask.id,
|
|
3353
|
+
domain: executionPolicy.domain,
|
|
3354
|
+
required_skills: executionPolicy.requiredSkills,
|
|
3355
|
+
agent_id: resolveOrgxAgentForDomain(executionPolicy.domain).id,
|
|
3356
|
+
});
|
|
3357
|
+
}
|
|
3358
|
+
catch {
|
|
3359
|
+
// best effort: fall back to local prompt
|
|
3360
|
+
}
|
|
3361
|
+
if (kickoff) {
|
|
3362
|
+
kickoffRuntimeSettings = kickoff.runtime_settings ?? null;
|
|
3363
|
+
const rendered = renderKickoffMessageFn({
|
|
3364
|
+
baseMessage: `Execute workstream slice for ${workstreamTitle ?? selectedWorkstreamId}`,
|
|
3365
|
+
kickoff,
|
|
3366
|
+
domain: executionPolicy.domain,
|
|
3367
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
3368
|
+
});
|
|
3369
|
+
const sliceInstructions = buildSliceOutputInstructions({
|
|
3370
|
+
runId: sliceRunId,
|
|
3371
|
+
schemaPath,
|
|
3372
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
3373
|
+
});
|
|
3374
|
+
prompt = rendered.message + "\n\n" + sliceInstructions;
|
|
3375
|
+
kickoffContextHash = rendered.contextHash;
|
|
3376
|
+
}
|
|
3377
|
+
else {
|
|
3378
|
+
// Fallback: existing local prompt (offline/degraded mode)
|
|
3379
|
+
prompt = buildWorkstreamSlicePrompt({
|
|
3380
|
+
initiativeTitle,
|
|
3381
|
+
initiativeId: run.initiativeId,
|
|
3382
|
+
workstreamId: selectedWorkstreamId,
|
|
3383
|
+
workstreamTitle: workstreamTitle ?? `Workstream ${selectedWorkstreamId.slice(0, 8)}`,
|
|
3384
|
+
milestoneSummaries,
|
|
3385
|
+
taskSummaries,
|
|
3386
|
+
executionPolicy,
|
|
3387
|
+
behaviorConfig,
|
|
3388
|
+
runId: sliceRunId,
|
|
3389
|
+
schemaPath,
|
|
3390
|
+
});
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
else {
|
|
3394
|
+
// No KickoffContext functions available: use local prompt
|
|
3395
|
+
prompt = buildWorkstreamSlicePrompt({
|
|
3396
|
+
initiativeTitle,
|
|
3397
|
+
initiativeId: run.initiativeId,
|
|
3398
|
+
workstreamId: selectedWorkstreamId,
|
|
3399
|
+
workstreamTitle: workstreamTitle ?? `Workstream ${selectedWorkstreamId.slice(0, 8)}`,
|
|
3400
|
+
milestoneSummaries,
|
|
3401
|
+
taskSummaries,
|
|
3402
|
+
executionPolicy,
|
|
3403
|
+
behaviorConfig,
|
|
3404
|
+
runId: sliceRunId,
|
|
3405
|
+
schemaPath,
|
|
3406
|
+
});
|
|
3407
|
+
}
|
|
3408
|
+
questionAutoAnswerPolicyByScope.set(questionScopeKey(run.initiativeId, selectedWorkstreamId), normalizeQuestionAutoAnswerPolicy(kickoffRuntimeSettings));
|
|
3409
|
+
// Append per-scope directive for milestone/workstream scopes.
|
|
3410
|
+
if (run.scope !== "task") {
|
|
3411
|
+
const msNodes = scopeMilestoneIds
|
|
3412
|
+
.map((id) => nodeById.get(id))
|
|
3413
|
+
.filter((n) => Boolean(n));
|
|
3414
|
+
const scopeDirective = buildScopeDirective(run.scope, {
|
|
3415
|
+
milestoneTitles: msNodes.map((n) => n.title),
|
|
3416
|
+
workstreamTitle: workstreamTitle ?? undefined,
|
|
3417
|
+
taskCount: cappedSliceTaskNodes.length,
|
|
3418
|
+
});
|
|
3419
|
+
if (scopeDirective) {
|
|
3420
|
+
prompt = prompt + "\n\n" + scopeDirective;
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
932
3423
|
const logsDir = join(getOrgxPluginConfigDir(), AUTO_CONTINUE_SLICE_LOG_DIRNAME);
|
|
933
3424
|
const logPath = join(logsDir, `${sliceRunId}.log`);
|
|
934
3425
|
const outputPath = join(logsDir, `${sliceRunId}.output.json`);
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
//
|
|
3426
|
+
const configuredWorkerCwd = (process.env.ORGX_AUTOPILOT_CWD ?? "").trim();
|
|
3427
|
+
let workerCwd = configuredWorkerCwd || resolveAutopilotDefaultCwd(__filename);
|
|
3428
|
+
// LaunchAgents sometimes start with cwd="/". Fall back to plugin root (or home if unresolved).
|
|
938
3429
|
if (!workerCwd || workerCwd === "/") {
|
|
939
|
-
workerCwd =
|
|
3430
|
+
workerCwd = resolveAutopilotDefaultCwd(__filename);
|
|
940
3431
|
}
|
|
941
3432
|
const sliceAgent = resolveOrgxAgentForDomain(executionPolicy.domain);
|
|
942
3433
|
const workerKind = (process.env.ORGX_AUTOPILOT_WORKER_KIND ?? "").trim().toLowerCase();
|
|
@@ -960,6 +3451,7 @@ export function createAutoContinueEngine(deps) {
|
|
|
960
3451
|
cwd: workerCwd,
|
|
961
3452
|
logPath,
|
|
962
3453
|
outputPath,
|
|
3454
|
+
outputSchemaPath: schemaPath,
|
|
963
3455
|
env: {
|
|
964
3456
|
ORGX_SOURCE_CLIENT: executorSourceClient,
|
|
965
3457
|
ORGX_RUN_ID: sliceRunId,
|
|
@@ -968,8 +3460,16 @@ export function createAutoContinueEngine(deps) {
|
|
|
968
3460
|
ORGX_WORKSTREAM_ID: selectedWorkstreamId,
|
|
969
3461
|
ORGX_WORKSTREAM_TITLE: workstreamTitle ?? undefined,
|
|
970
3462
|
ORGX_TASK_ID: primaryTask.id,
|
|
3463
|
+
ORGX_REQUIRED_SKILLS: executionPolicy.requiredSkills.join(","),
|
|
3464
|
+
ORGX_BEHAVIOR_CONFIG_ID: behaviorConfig.configId ?? undefined,
|
|
3465
|
+
ORGX_BEHAVIOR_CONFIG_VERSION: behaviorConfig.version ?? undefined,
|
|
3466
|
+
ORGX_BEHAVIOR_CONFIG_HASH: behaviorConfig.hash ?? undefined,
|
|
3467
|
+
ORGX_POLICY_SOURCE: behaviorConfig.policySource ?? undefined,
|
|
3468
|
+
ORGX_AUTOMATION_LEVEL: behaviorAutomationLevel,
|
|
3469
|
+
ORGX_BEHAVIOR_CONTEXT: behaviorConfig.context ?? undefined,
|
|
971
3470
|
ORGX_AGENT_ID: sliceAgent.id,
|
|
972
3471
|
ORGX_AGENT_NAME: sliceAgent.name,
|
|
3472
|
+
ORGX_KICKOFF_CONTEXT_HASH: kickoffContextHash ?? undefined,
|
|
973
3473
|
ORGX_OUTPUT_PATH: outputPath,
|
|
974
3474
|
ORGX_RUNTIME_HOOK_URL: runtimeHookUrl ?? undefined,
|
|
975
3475
|
ORGX_HOOK_TOKEN: runtimeHookToken ?? undefined,
|
|
@@ -985,6 +3485,11 @@ export function createAutoContinueEngine(deps) {
|
|
|
985
3485
|
agentName: sliceAgent.name,
|
|
986
3486
|
domain: executionPolicy.domain,
|
|
987
3487
|
requiredSkills: executionPolicy.requiredSkills,
|
|
3488
|
+
behaviorConfigId: behaviorConfig.configId,
|
|
3489
|
+
behaviorConfigVersion: behaviorConfig.version,
|
|
3490
|
+
behaviorConfigHash: behaviorConfig.hash,
|
|
3491
|
+
behaviorPolicySource: behaviorConfig.policySource,
|
|
3492
|
+
behaviorAutomationLevel,
|
|
988
3493
|
sourceClient: executorSourceClient,
|
|
989
3494
|
pid: spawned.pid,
|
|
990
3495
|
status: "running",
|
|
@@ -996,7 +3501,10 @@ export function createAutoContinueEngine(deps) {
|
|
|
996
3501
|
logPath,
|
|
997
3502
|
taskIds: cappedSliceTaskNodes.map((t) => t.id),
|
|
998
3503
|
milestoneIds,
|
|
3504
|
+
scope: run.scope,
|
|
3505
|
+
scopeMilestoneIds: scopeMilestoneIds,
|
|
999
3506
|
lastError: null,
|
|
3507
|
+
isMockWorker: workerKind === "mock",
|
|
1000
3508
|
};
|
|
1001
3509
|
autoContinueSliceRuns.set(sliceRunId, slice);
|
|
1002
3510
|
try {
|
|
@@ -1013,15 +3521,28 @@ export function createAutoContinueEngine(deps) {
|
|
|
1013
3521
|
message: `Autopilot slice started: ${workstreamTitle ?? selectedWorkstreamId}`,
|
|
1014
3522
|
metadata: {
|
|
1015
3523
|
event: "autopilot_slice_started",
|
|
3524
|
+
initiative_id: run.initiativeId,
|
|
3525
|
+
run_id: sliceRunId,
|
|
3526
|
+
slice_run_id: sliceRunId,
|
|
3527
|
+
workstream_id: selectedWorkstreamId,
|
|
3528
|
+
correlation_id: sliceRunId,
|
|
1016
3529
|
requested_by_agent_id: run.agentId,
|
|
1017
3530
|
requested_by_agent_name: run.agentName,
|
|
1018
3531
|
domain: executionPolicy.domain,
|
|
1019
3532
|
required_skills: executionPolicy.requiredSkills,
|
|
3533
|
+
behavior_config_id: behaviorConfig.configId,
|
|
3534
|
+
behavior_config_version: behaviorConfig.version,
|
|
3535
|
+
behavior_config_hash: behaviorConfig.hash,
|
|
3536
|
+
policy_source: behaviorConfig.policySource,
|
|
3537
|
+
behavior_automation_level: behaviorAutomationLevel,
|
|
1020
3538
|
task_ids: slice.taskIds,
|
|
1021
3539
|
initiative_title: initiativeTitle ?? null,
|
|
1022
3540
|
workstream_title: workstreamTitle ?? null,
|
|
3541
|
+
scope: slice.scope,
|
|
3542
|
+
scope_milestone_ids: slice.scopeMilestoneIds,
|
|
1023
3543
|
log_path: logPath,
|
|
1024
3544
|
output_path: outputPath,
|
|
3545
|
+
...mockMeta(slice),
|
|
1025
3546
|
},
|
|
1026
3547
|
});
|
|
1027
3548
|
}
|
|
@@ -1033,24 +3554,34 @@ export function createAutoContinueEngine(deps) {
|
|
|
1033
3554
|
initiativeId: run.initiativeId,
|
|
1034
3555
|
runId: sliceRunId,
|
|
1035
3556
|
correlationId: sliceRunId,
|
|
3557
|
+
progressPct: 10,
|
|
3558
|
+
nextStep: `Worker ${sliceAgent.name} is executing ${workstreamTitle ?? selectedWorkstreamId}.`,
|
|
1036
3559
|
phase: "execution",
|
|
1037
3560
|
level: "info",
|
|
1038
3561
|
message: `Autopilot dispatched slice for ${workstreamTitle ?? selectedWorkstreamId}.`,
|
|
1039
3562
|
metadata: {
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
3563
|
+
...buildSliceEnrichment({
|
|
3564
|
+
run,
|
|
3565
|
+
slice,
|
|
3566
|
+
taskId: primaryTask.id,
|
|
3567
|
+
taskTitle: primaryTask.title ?? null,
|
|
3568
|
+
workstreamId: selectedWorkstreamId,
|
|
3569
|
+
workstreamTitle: workstreamTitle ?? null,
|
|
3570
|
+
domain: executionPolicy.domain,
|
|
3571
|
+
requiredSkills: executionPolicy.requiredSkills,
|
|
3572
|
+
event: "autopilot_slice_dispatched",
|
|
3573
|
+
}),
|
|
3574
|
+
behavior_config_id: behaviorConfig.configId,
|
|
3575
|
+
behavior_config_version: behaviorConfig.version,
|
|
3576
|
+
behavior_config_hash: behaviorConfig.hash,
|
|
3577
|
+
policy_source: behaviorConfig.policySource,
|
|
3578
|
+
behavior_automation_level: behaviorAutomationLevel,
|
|
1047
3579
|
initiative_title: initiativeTitle ?? null,
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
task_ids: slice.taskIds,
|
|
1051
|
-
milestone_ids: milestoneIds,
|
|
3580
|
+
scope: slice.scope,
|
|
3581
|
+
scope_milestone_ids: slice.scopeMilestoneIds,
|
|
1052
3582
|
log_path: logPath,
|
|
1053
3583
|
output_path: outputPath,
|
|
3584
|
+
...mockMeta(slice),
|
|
1054
3585
|
},
|
|
1055
3586
|
});
|
|
1056
3587
|
upsertAgentContext({
|
|
@@ -1060,11 +3591,34 @@ export function createAutoContinueEngine(deps) {
|
|
|
1060
3591
|
workstreamId: selectedWorkstreamId,
|
|
1061
3592
|
taskId: primaryTask.id,
|
|
1062
3593
|
});
|
|
3594
|
+
upsertRunContext({
|
|
3595
|
+
runId: sliceRunId,
|
|
3596
|
+
agentId: slice.agentId,
|
|
3597
|
+
initiativeId: run.initiativeId,
|
|
3598
|
+
initiativeTitle: initiativeTitle ?? null,
|
|
3599
|
+
workstreamId: selectedWorkstreamId,
|
|
3600
|
+
taskId: primaryTask.id,
|
|
3601
|
+
});
|
|
1063
3602
|
run.lastTaskId = primaryTask.id;
|
|
1064
3603
|
run.lastRunId = sliceRunId;
|
|
1065
|
-
run.
|
|
1066
|
-
|
|
3604
|
+
run.activeSliceRunIds = dedupeStrings([
|
|
3605
|
+
...run.activeSliceRunIds,
|
|
3606
|
+
sliceRunId,
|
|
3607
|
+
]);
|
|
3608
|
+
run.activeTaskIds = dedupeStrings([...run.activeTaskIds, ...slice.taskIds]);
|
|
3609
|
+
setLaneState(run, {
|
|
3610
|
+
workstreamId: selectedWorkstreamId,
|
|
3611
|
+
state: "running",
|
|
3612
|
+
activeRunId: sliceRunId,
|
|
3613
|
+
activeTaskIds: slice.taskIds,
|
|
3614
|
+
blockedReason: null,
|
|
3615
|
+
waitingOnWorkstreamIds: [],
|
|
3616
|
+
retryAt: null,
|
|
3617
|
+
});
|
|
1067
3618
|
run.activeTaskTokenEstimate = tokenEstimate > 0 ? tokenEstimate : null;
|
|
3619
|
+
syncLegacyRunPointers(run);
|
|
3620
|
+
// Clear stale errors when a new slice dispatches successfully.
|
|
3621
|
+
run.lastError = null;
|
|
1068
3622
|
run.updatedAt = now;
|
|
1069
3623
|
try {
|
|
1070
3624
|
await client.updateEntity("initiative", run.initiativeId, { status: "active" });
|
|
@@ -1095,7 +3649,7 @@ export function createAutoContinueEngine(deps) {
|
|
|
1095
3649
|
}
|
|
1096
3650
|
catch (err) {
|
|
1097
3651
|
// Never let one loop crash the whole handler.
|
|
1098
|
-
run.lastError = safeErrorMessage(err)
|
|
3652
|
+
run.lastError = `[mission-control.auto-continue.engine.tick-all] ${safeErrorMessage(err)}`;
|
|
1099
3653
|
run.updatedAt = new Date().toISOString();
|
|
1100
3654
|
await stopAutoContinueRun({ run, reason: "error", error: run.lastError });
|
|
1101
3655
|
}
|
|
@@ -1123,12 +3677,324 @@ export function createAutoContinueEngine(deps) {
|
|
|
1123
3677
|
const run = autoContinueRuns.get(initiativeId) ?? null;
|
|
1124
3678
|
if (!run)
|
|
1125
3679
|
return null;
|
|
3680
|
+
ensureRunInternals(run);
|
|
1126
3681
|
if (run.status !== "running" && run.status !== "stopping")
|
|
1127
3682
|
return null;
|
|
1128
|
-
if (
|
|
3683
|
+
if (Array.isArray(run.allowedWorkstreamIds) &&
|
|
3684
|
+
run.allowedWorkstreamIds.length > 0 &&
|
|
3685
|
+
!run.allowedWorkstreamIds.includes(workstreamId)) {
|
|
3686
|
+
return null;
|
|
3687
|
+
}
|
|
3688
|
+
const lane = run.laneByWorkstreamId[workstreamId] ?? null;
|
|
3689
|
+
if (lane &&
|
|
3690
|
+
(lane.state === "running" ||
|
|
3691
|
+
lane.state === "blocked" ||
|
|
3692
|
+
lane.state === "waiting_dependency" ||
|
|
3693
|
+
lane.state === "rate_limited")) {
|
|
3694
|
+
return run;
|
|
3695
|
+
}
|
|
3696
|
+
if (Array.isArray(run.allowedWorkstreamIds) &&
|
|
3697
|
+
run.allowedWorkstreamIds.length > 0 &&
|
|
3698
|
+
run.allowedWorkstreamIds.includes(workstreamId) &&
|
|
3699
|
+
(run.status === "running" || run.status === "stopping")) {
|
|
1129
3700
|
return run;
|
|
1130
3701
|
}
|
|
1131
|
-
return
|
|
3702
|
+
return null;
|
|
3703
|
+
}
|
|
3704
|
+
function getAutoContinueLaneForWorkstream(initiativeId, workstreamId) {
|
|
3705
|
+
const run = autoContinueRuns.get(initiativeId) ?? null;
|
|
3706
|
+
if (!run)
|
|
3707
|
+
return null;
|
|
3708
|
+
ensureRunInternals(run);
|
|
3709
|
+
return run.laneByWorkstreamId[workstreamId] ?? null;
|
|
3710
|
+
}
|
|
3711
|
+
async function scheduleAutoFixForWorkstream(input) {
|
|
3712
|
+
const initiativeId = input.initiativeId.trim();
|
|
3713
|
+
const workstreamId = input.workstreamId.trim();
|
|
3714
|
+
if (!initiativeId || !workstreamId) {
|
|
3715
|
+
throw new Error("initiativeId and workstreamId are required");
|
|
3716
|
+
}
|
|
3717
|
+
const runId = (input.runId ?? "").trim() || null;
|
|
3718
|
+
const sourceEvent = (input.event ?? "").trim() || null;
|
|
3719
|
+
const requestedByAgentId = (input.requestedByAgentId ?? "").trim() || null;
|
|
3720
|
+
const requestedByAgentName = (input.requestedByAgentName ?? "").trim() || null;
|
|
3721
|
+
const providedGraceMs = typeof input.graceMs === "number" && Number.isFinite(input.graceMs)
|
|
3722
|
+
? Math.floor(input.graceMs)
|
|
3723
|
+
: null;
|
|
3724
|
+
const graceMs = Math.max(1_000, Math.min(120_000, providedGraceMs ?? AUTO_FIX_DEFAULT_GRACE_MS));
|
|
3725
|
+
const key = `${initiativeId}:${workstreamId}`;
|
|
3726
|
+
const existing = autoFixByScope.get(key);
|
|
3727
|
+
if (existing?.timer)
|
|
3728
|
+
clearTimeout(existing.timer);
|
|
3729
|
+
const scheduledAt = new Date().toISOString();
|
|
3730
|
+
const dueAt = new Date(Date.now() + graceMs).toISOString();
|
|
3731
|
+
const requestId = randomUUID();
|
|
3732
|
+
const resolveAutoFixRunContext = () => {
|
|
3733
|
+
const activeRun = autoContinueRuns.get(initiativeId) ?? null;
|
|
3734
|
+
return {
|
|
3735
|
+
initiativeId,
|
|
3736
|
+
agentId: activeRun?.agentId ?? requestedByAgentId ?? "main",
|
|
3737
|
+
agentName: activeRun?.agentName ?? requestedByAgentName ?? null,
|
|
3738
|
+
scope: activeRun?.scope ?? "task",
|
|
3739
|
+
};
|
|
3740
|
+
};
|
|
3741
|
+
const emitSkip = async (reason, details) => {
|
|
3742
|
+
await emitActivitySafe({
|
|
3743
|
+
initiativeId,
|
|
3744
|
+
runId: runId ?? undefined,
|
|
3745
|
+
correlationId: runId ?? undefined,
|
|
3746
|
+
phase: "review",
|
|
3747
|
+
level: reason === "error" ? "error" : "warn",
|
|
3748
|
+
message: reason === "paused_by_user"
|
|
3749
|
+
? `Auto-fix skipped for ${workstreamId}: paused during grace window.`
|
|
3750
|
+
: reason === "already_running"
|
|
3751
|
+
? `Auto-fix skipped for ${workstreamId}: workstream already running.`
|
|
3752
|
+
: reason === "missing_workstream"
|
|
3753
|
+
? `Auto-fix skipped for ${workstreamId}: workstream data unavailable.`
|
|
3754
|
+
: reason === "missing_scope"
|
|
3755
|
+
? `Auto-fix skipped: scope metadata was incomplete.`
|
|
3756
|
+
: `Auto-fix failed for ${workstreamId}.`,
|
|
3757
|
+
metadata: {
|
|
3758
|
+
...buildSliceEnrichment({
|
|
3759
|
+
run: resolveAutoFixRunContext(),
|
|
3760
|
+
workstreamId,
|
|
3761
|
+
event: "autopilot_autofix_skipped",
|
|
3762
|
+
actionType: "auto_fix",
|
|
3763
|
+
}),
|
|
3764
|
+
reason,
|
|
3765
|
+
run_id: runId,
|
|
3766
|
+
source_event: sourceEvent,
|
|
3767
|
+
grace_ms: graceMs,
|
|
3768
|
+
request_id: requestId,
|
|
3769
|
+
scheduled_at: scheduledAt,
|
|
3770
|
+
due_at: dueAt,
|
|
3771
|
+
...(details ?? {}),
|
|
3772
|
+
},
|
|
3773
|
+
});
|
|
3774
|
+
};
|
|
3775
|
+
const executeScheduledAutoFix = async () => {
|
|
3776
|
+
const pending = autoFixByScope.get(key);
|
|
3777
|
+
if (!pending || pending.requestId !== requestId)
|
|
3778
|
+
return;
|
|
3779
|
+
autoFixByScope.delete(key);
|
|
3780
|
+
const existingRun = autoContinueRuns.get(initiativeId) ?? null;
|
|
3781
|
+
if (existingRun &&
|
|
3782
|
+
(existingRun.stopRequested ||
|
|
3783
|
+
existingRun.status === "stopping" ||
|
|
3784
|
+
existingRun.stopReason === "stopped")) {
|
|
3785
|
+
await emitSkip("paused_by_user");
|
|
3786
|
+
return;
|
|
3787
|
+
}
|
|
3788
|
+
if (existingRun &&
|
|
3789
|
+
(existingRun.status === "running" || existingRun.status === "stopping") &&
|
|
3790
|
+
listActiveSliceRunIds(existingRun).length > 0) {
|
|
3791
|
+
const activeRunIds = listActiveSliceRunIds(existingRun);
|
|
3792
|
+
await emitSkip("already_running", {
|
|
3793
|
+
active_run_id: activeRunIds[0] ?? null,
|
|
3794
|
+
active_run_ids: activeRunIds,
|
|
3795
|
+
run_status: existingRun.status,
|
|
3796
|
+
});
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3799
|
+
let optionalDecisionsApproved = 0;
|
|
3800
|
+
if (decisionAutoResolveGuardedEnabled) {
|
|
3801
|
+
try {
|
|
3802
|
+
const decisionResult = await client.listEntities("decision", {
|
|
3803
|
+
initiative_id: initiativeId,
|
|
3804
|
+
status: "pending",
|
|
3805
|
+
limit: 500,
|
|
3806
|
+
});
|
|
3807
|
+
const decisionRows = Array.isArray(decisionResult?.data) ? decisionResult.data : [];
|
|
3808
|
+
for (const row of decisionRows) {
|
|
3809
|
+
if (!row || typeof row !== "object")
|
|
3810
|
+
continue;
|
|
3811
|
+
const record = row;
|
|
3812
|
+
const decisionId = pickString(record, ["id"])?.trim() ?? "";
|
|
3813
|
+
if (!decisionId)
|
|
3814
|
+
continue;
|
|
3815
|
+
if (!isPendingDecisionStatus(record.status ?? record.decision_status))
|
|
3816
|
+
continue;
|
|
3817
|
+
if (!decisionMatchesWorkstream(record, workstreamId, runId))
|
|
3818
|
+
continue;
|
|
3819
|
+
if (decisionIsBlocking(record))
|
|
3820
|
+
continue;
|
|
3821
|
+
const autoApprovalNote = "Auto-approved by OrgX auto-fix (non-blocking follow-up decision).";
|
|
3822
|
+
const autoApprovalSourceClient = normalizeRuntimeSourceClient(process.env.ORGX_AUTOPILOT_EXECUTOR ?? process.env.ORGX_AUTOPILOT_WORKER_KIND);
|
|
3823
|
+
if (typeof client.decideDecision === "function") {
|
|
3824
|
+
await client.decideDecision(decisionId, "approve", {
|
|
3825
|
+
note: autoApprovalNote,
|
|
3826
|
+
source_client: autoApprovalSourceClient,
|
|
3827
|
+
sourceClient: autoApprovalSourceClient,
|
|
3828
|
+
});
|
|
3829
|
+
}
|
|
3830
|
+
else {
|
|
3831
|
+
await client.updateEntity("decision", decisionId, {
|
|
3832
|
+
status: "approved",
|
|
3833
|
+
resolution_summary: autoApprovalNote,
|
|
3834
|
+
});
|
|
3835
|
+
}
|
|
3836
|
+
optionalDecisionsApproved += 1;
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
catch {
|
|
3840
|
+
// best effort
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
let resetTaskCount = 0;
|
|
3844
|
+
try {
|
|
3845
|
+
const taskResult = await client.listEntities("task", {
|
|
3846
|
+
initiative_id: initiativeId,
|
|
3847
|
+
workstream_id: workstreamId,
|
|
3848
|
+
limit: 100,
|
|
3849
|
+
});
|
|
3850
|
+
const taskRows = Array.isArray(taskResult?.data) ? taskResult.data : [];
|
|
3851
|
+
if (taskRows.length === 0) {
|
|
3852
|
+
await emitSkip("missing_workstream");
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
3855
|
+
for (const row of taskRows) {
|
|
3856
|
+
if (!row || typeof row !== "object")
|
|
3857
|
+
continue;
|
|
3858
|
+
const record = row;
|
|
3859
|
+
const taskId = pickString(record, ["id"])?.trim() ?? "";
|
|
3860
|
+
if (!taskId)
|
|
3861
|
+
continue;
|
|
3862
|
+
const status = normalizeStatusValue(record.status);
|
|
3863
|
+
if (!status || status === "todo" || status === "done" || status === "completed") {
|
|
3864
|
+
continue;
|
|
3865
|
+
}
|
|
3866
|
+
const shouldReset = status === "in_progress" ||
|
|
3867
|
+
status === "inprogress" ||
|
|
3868
|
+
status === "active" ||
|
|
3869
|
+
status === "running" ||
|
|
3870
|
+
status === "working" ||
|
|
3871
|
+
status === "planning" ||
|
|
3872
|
+
status === "dispatching" ||
|
|
3873
|
+
status === "pending" ||
|
|
3874
|
+
status === "blocked" ||
|
|
3875
|
+
status === "stalled" ||
|
|
3876
|
+
status === "failed" ||
|
|
3877
|
+
status === "error";
|
|
3878
|
+
if (!shouldReset)
|
|
3879
|
+
continue;
|
|
3880
|
+
await client.updateEntity("task", taskId, { status: "todo" });
|
|
3881
|
+
resetTaskCount += 1;
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
catch {
|
|
3885
|
+
// best effort
|
|
3886
|
+
}
|
|
3887
|
+
const latestRun = autoContinueRuns.get(initiativeId) ?? null;
|
|
3888
|
+
const dispatchAgentId = latestRun?.agentId ??
|
|
3889
|
+
requestedByAgentId ??
|
|
3890
|
+
"main";
|
|
3891
|
+
const dispatchAgentName = latestRun?.agentName ??
|
|
3892
|
+
requestedByAgentName ??
|
|
3893
|
+
null;
|
|
3894
|
+
const dispatchRun = await startAutoContinueRun({
|
|
3895
|
+
initiativeId,
|
|
3896
|
+
agentId: dispatchAgentId,
|
|
3897
|
+
agentName: dispatchAgentName,
|
|
3898
|
+
// Auto-fix retries should follow current defaults unless an operator explicitly
|
|
3899
|
+
// starts a run with a budget override.
|
|
3900
|
+
tokenBudget: null,
|
|
3901
|
+
includeVerification: latestRun?.includeVerification ?? false,
|
|
3902
|
+
allowedWorkstreamIds: [workstreamId],
|
|
3903
|
+
maxParallelSlices: 1,
|
|
3904
|
+
parallelMode: latestRun?.parallelMode ?? "iwmt",
|
|
3905
|
+
stopAfterSlice: true,
|
|
3906
|
+
ignoreSpawnGuardRateLimit: latestRun?.ignoreSpawnGuardRateLimit ?? false,
|
|
3907
|
+
});
|
|
3908
|
+
await tickAutoContinueRun(dispatchRun);
|
|
3909
|
+
await emitActivitySafe({
|
|
3910
|
+
initiativeId,
|
|
3911
|
+
runId: dispatchRun.activeRunId ?? runId ?? undefined,
|
|
3912
|
+
correlationId: dispatchRun.activeRunId ?? runId ?? undefined,
|
|
3913
|
+
phase: "execution",
|
|
3914
|
+
level: "info",
|
|
3915
|
+
message: `Auto-fix dispatched for ${workstreamId}.`,
|
|
3916
|
+
metadata: {
|
|
3917
|
+
...buildSliceEnrichment({
|
|
3918
|
+
run: {
|
|
3919
|
+
initiativeId,
|
|
3920
|
+
agentId: dispatchAgentId,
|
|
3921
|
+
agentName: dispatchAgentName,
|
|
3922
|
+
scope: dispatchRun.scope,
|
|
3923
|
+
},
|
|
3924
|
+
workstreamId,
|
|
3925
|
+
event: "autopilot_autofix_executed",
|
|
3926
|
+
actionType: "auto_fix",
|
|
3927
|
+
}),
|
|
3928
|
+
source_event: sourceEvent,
|
|
3929
|
+
run_id: runId,
|
|
3930
|
+
grace_ms: graceMs,
|
|
3931
|
+
request_id: requestId,
|
|
3932
|
+
scheduled_at: scheduledAt,
|
|
3933
|
+
due_at: dueAt,
|
|
3934
|
+
optional_decisions_auto_approved: optionalDecisionsApproved,
|
|
3935
|
+
reset_task_count: resetTaskCount,
|
|
3936
|
+
dispatched_run_id: dispatchRun.activeRunId,
|
|
3937
|
+
dispatch_agent_id: dispatchAgentId,
|
|
3938
|
+
dispatch_agent_name: dispatchAgentName,
|
|
3939
|
+
},
|
|
3940
|
+
});
|
|
3941
|
+
};
|
|
3942
|
+
const pending = {
|
|
3943
|
+
requestId,
|
|
3944
|
+
key,
|
|
3945
|
+
initiativeId,
|
|
3946
|
+
workstreamId,
|
|
3947
|
+
runId,
|
|
3948
|
+
sourceEvent,
|
|
3949
|
+
requestedByAgentId,
|
|
3950
|
+
requestedByAgentName,
|
|
3951
|
+
graceMs,
|
|
3952
|
+
scheduledAt,
|
|
3953
|
+
dueAt,
|
|
3954
|
+
timer: null,
|
|
3955
|
+
};
|
|
3956
|
+
const timer = setTimeout(() => {
|
|
3957
|
+
void executeScheduledAutoFix().catch(async (err) => {
|
|
3958
|
+
autoFixByScope.delete(key);
|
|
3959
|
+
await emitSkip("error", {
|
|
3960
|
+
error: safeErrorMessage(err),
|
|
3961
|
+
});
|
|
3962
|
+
});
|
|
3963
|
+
}, graceMs);
|
|
3964
|
+
pending.timer = timer;
|
|
3965
|
+
autoFixByScope.set(key, pending);
|
|
3966
|
+
await emitActivitySafe({
|
|
3967
|
+
initiativeId,
|
|
3968
|
+
runId: runId ?? undefined,
|
|
3969
|
+
correlationId: runId ?? undefined,
|
|
3970
|
+
phase: "review",
|
|
3971
|
+
level: "info",
|
|
3972
|
+
message: `Auto-fix scheduled for ${workstreamId} in ${Math.round(graceMs / 1000)}s.`,
|
|
3973
|
+
metadata: {
|
|
3974
|
+
...buildSliceEnrichment({
|
|
3975
|
+
run: resolveAutoFixRunContext(),
|
|
3976
|
+
workstreamId,
|
|
3977
|
+
event: "autopilot_autofix_scheduled",
|
|
3978
|
+
actionType: "auto_fix",
|
|
3979
|
+
}),
|
|
3980
|
+
source_event: sourceEvent,
|
|
3981
|
+
run_id: runId,
|
|
3982
|
+
grace_ms: graceMs,
|
|
3983
|
+
request_id: requestId,
|
|
3984
|
+
scheduled_at: scheduledAt,
|
|
3985
|
+
due_at: dueAt,
|
|
3986
|
+
},
|
|
3987
|
+
});
|
|
3988
|
+
return {
|
|
3989
|
+
requestId,
|
|
3990
|
+
initiativeId,
|
|
3991
|
+
workstreamId,
|
|
3992
|
+
runId,
|
|
3993
|
+
sourceEvent,
|
|
3994
|
+
graceMs,
|
|
3995
|
+
scheduledAt,
|
|
3996
|
+
dueAt,
|
|
3997
|
+
};
|
|
1132
3998
|
}
|
|
1133
3999
|
async function startAutoContinueRun(input) {
|
|
1134
4000
|
const now = new Date().toISOString();
|
|
@@ -1142,6 +4008,10 @@ export function createAutoContinueEngine(deps) {
|
|
|
1142
4008
|
includeVerification: false,
|
|
1143
4009
|
allowedWorkstreamIds: null,
|
|
1144
4010
|
stopAfterSlice: false,
|
|
4011
|
+
ignoreSpawnGuardRateLimit: false,
|
|
4012
|
+
maxParallelSlices: AUTO_CONTINUE_MAX_PARALLEL_DEFAULT,
|
|
4013
|
+
parallelMode: "iwmt",
|
|
4014
|
+
scope: "task",
|
|
1145
4015
|
tokenBudget: defaultAutoContinueTokenBudget(),
|
|
1146
4016
|
tokensUsed: 0,
|
|
1147
4017
|
status: "running",
|
|
@@ -1153,10 +4023,15 @@ export function createAutoContinueEngine(deps) {
|
|
|
1153
4023
|
lastError: null,
|
|
1154
4024
|
lastTaskId: null,
|
|
1155
4025
|
lastRunId: null,
|
|
4026
|
+
activeSliceRunIds: [],
|
|
4027
|
+
activeTaskIds: [],
|
|
4028
|
+
laneByWorkstreamId: {},
|
|
4029
|
+
blockedWorkstreamIds: [],
|
|
1156
4030
|
activeTaskId: null,
|
|
1157
4031
|
activeRunId: null,
|
|
1158
4032
|
activeTaskTokenEstimate: null,
|
|
1159
4033
|
};
|
|
4034
|
+
ensureRunInternals(run);
|
|
1160
4035
|
run.agentId = input.agentId;
|
|
1161
4036
|
run.agentName =
|
|
1162
4037
|
typeof input.agentName === "string" && input.agentName.trim().length > 0
|
|
@@ -1164,8 +4039,24 @@ export function createAutoContinueEngine(deps) {
|
|
|
1164
4039
|
: null;
|
|
1165
4040
|
run.includeVerification = input.includeVerification;
|
|
1166
4041
|
run.allowedWorkstreamIds = input.allowedWorkstreamIds;
|
|
4042
|
+
run.maxParallelSlices = normalizeMaxParallelSlices(input.maxParallelSlices, run.maxParallelSlices || AUTO_CONTINUE_MAX_PARALLEL_DEFAULT);
|
|
4043
|
+
run.parallelMode = normalizeParallelMode(input.parallelMode ?? run.parallelMode);
|
|
1167
4044
|
run.stopAfterSlice = Boolean(input.stopAfterSlice);
|
|
1168
|
-
run.
|
|
4045
|
+
run.ignoreSpawnGuardRateLimit = Boolean(input.ignoreSpawnGuardRateLimit);
|
|
4046
|
+
run.scope = input.scope ?? "task";
|
|
4047
|
+
const hasExplicitTokenBudgetInput = input.tokenBudget !== null &&
|
|
4048
|
+
input.tokenBudget !== undefined &&
|
|
4049
|
+
!(typeof input.tokenBudget === "string" && input.tokenBudget.trim().length === 0);
|
|
4050
|
+
if (hasExplicitTokenBudgetInput) {
|
|
4051
|
+
run.tokenBudget = normalizeTokenBudget(input.tokenBudget, defaultAutoContinueTokenBudget());
|
|
4052
|
+
}
|
|
4053
|
+
else {
|
|
4054
|
+
// On fresh restarts, reset to current defaults instead of inheriting stale prior limits.
|
|
4055
|
+
// While a run is live, keep its active budget unless explicitly overridden.
|
|
4056
|
+
run.tokenBudget = existingIsLive
|
|
4057
|
+
? normalizeTokenBudget(run.tokenBudget, defaultAutoContinueTokenBudget())
|
|
4058
|
+
: defaultAutoContinueTokenBudget();
|
|
4059
|
+
}
|
|
1169
4060
|
run.status = "running";
|
|
1170
4061
|
run.stopReason = null;
|
|
1171
4062
|
run.stopRequested = false;
|
|
@@ -1178,10 +4069,15 @@ export function createAutoContinueEngine(deps) {
|
|
|
1178
4069
|
run.startedAt = now;
|
|
1179
4070
|
run.lastTaskId = null;
|
|
1180
4071
|
run.lastRunId = null;
|
|
4072
|
+
run.activeSliceRunIds = [];
|
|
4073
|
+
run.activeTaskIds = [];
|
|
4074
|
+
run.blockedWorkstreamIds = [];
|
|
4075
|
+
run.laneByWorkstreamId = {};
|
|
1181
4076
|
run.activeTaskId = null;
|
|
1182
4077
|
run.activeRunId = null;
|
|
1183
4078
|
run.activeTaskTokenEstimate = null;
|
|
1184
4079
|
}
|
|
4080
|
+
syncLegacyRunPointers(run);
|
|
1185
4081
|
autoContinueRuns.set(input.initiativeId, run);
|
|
1186
4082
|
void client
|
|
1187
4083
|
.updateEntity("initiative", input.initiativeId, { status: "active" })
|
|
@@ -1194,6 +4090,66 @@ export function createAutoContinueEngine(deps) {
|
|
|
1194
4090
|
}).catch(() => {
|
|
1195
4091
|
// best effort
|
|
1196
4092
|
});
|
|
4093
|
+
if (!existingIsLive || forceFreshRun) {
|
|
4094
|
+
const startRunContext = {
|
|
4095
|
+
initiativeId: run.initiativeId,
|
|
4096
|
+
agentId: run.agentId,
|
|
4097
|
+
agentName: run.agentName,
|
|
4098
|
+
scope: run.scope,
|
|
4099
|
+
};
|
|
4100
|
+
try {
|
|
4101
|
+
await emitActivitySafe({
|
|
4102
|
+
initiativeId: input.initiativeId,
|
|
4103
|
+
runId: run.lastRunId ?? undefined,
|
|
4104
|
+
correlationId: run.lastRunId ?? undefined,
|
|
4105
|
+
phase: "intent",
|
|
4106
|
+
level: "info",
|
|
4107
|
+
message: "Autopilot enabled. Dispatch will continue from Next Up automatically.",
|
|
4108
|
+
metadata: {
|
|
4109
|
+
...buildSliceEnrichment({
|
|
4110
|
+
run: startRunContext,
|
|
4111
|
+
event: "auto_continue_started",
|
|
4112
|
+
}),
|
|
4113
|
+
token_budget: run.tokenBudget,
|
|
4114
|
+
include_verification: run.includeVerification,
|
|
4115
|
+
allowed_workstream_ids: run.allowedWorkstreamIds,
|
|
4116
|
+
max_parallel_slices: run.maxParallelSlices,
|
|
4117
|
+
parallel_mode: run.parallelMode,
|
|
4118
|
+
scope: run.scope,
|
|
4119
|
+
ignore_spawn_guard_rate_limit: run.ignoreSpawnGuardRateLimit,
|
|
4120
|
+
},
|
|
4121
|
+
nextStep: "Watch Activity for dispatch and slice-complete updates.",
|
|
4122
|
+
});
|
|
4123
|
+
}
|
|
4124
|
+
catch {
|
|
4125
|
+
// best effort
|
|
4126
|
+
}
|
|
4127
|
+
// Emit transition: idle → running
|
|
4128
|
+
try {
|
|
4129
|
+
await emitActivitySafe({
|
|
4130
|
+
initiativeId: input.initiativeId,
|
|
4131
|
+
runId: run.lastRunId ?? undefined,
|
|
4132
|
+
correlationId: run.lastRunId ?? undefined,
|
|
4133
|
+
phase: "intent",
|
|
4134
|
+
level: "info",
|
|
4135
|
+
message: "Autopilot state: idle → running.",
|
|
4136
|
+
metadata: {
|
|
4137
|
+
...buildSliceEnrichment({
|
|
4138
|
+
run: startRunContext,
|
|
4139
|
+
event: "autopilot_transition",
|
|
4140
|
+
actionType: "run_state_transition",
|
|
4141
|
+
}),
|
|
4142
|
+
old_state: "idle",
|
|
4143
|
+
new_state: "running",
|
|
4144
|
+
reason: "started",
|
|
4145
|
+
workspace_id: run.allowedWorkstreamIds?.[0] ?? null,
|
|
4146
|
+
},
|
|
4147
|
+
});
|
|
4148
|
+
}
|
|
4149
|
+
catch {
|
|
4150
|
+
// best effort
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
1197
4153
|
return run;
|
|
1198
4154
|
}
|
|
1199
4155
|
return {
|
|
@@ -1203,6 +4159,7 @@ export function createAutoContinueEngine(deps) {
|
|
|
1203
4159
|
writeRuntimeEvent,
|
|
1204
4160
|
autoContinueTickMs: AUTO_CONTINUE_TICK_MS,
|
|
1205
4161
|
defaultAutoContinueTokenBudget,
|
|
4162
|
+
defaultAutoContinueMaxParallelSlices,
|
|
1206
4163
|
setLocalInitiativeStatusOverride,
|
|
1207
4164
|
clearLocalInitiativeStatusOverride,
|
|
1208
4165
|
applyLocalInitiativeOverrides,
|
|
@@ -1213,6 +4170,8 @@ export function createAutoContinueEngine(deps) {
|
|
|
1213
4170
|
tickAllAutoContinue,
|
|
1214
4171
|
isInitiativeActiveStatus,
|
|
1215
4172
|
runningAutoContinueForWorkstream,
|
|
4173
|
+
getAutoContinueLaneForWorkstream,
|
|
4174
|
+
scheduleAutoFixForWorkstream,
|
|
1216
4175
|
startAutoContinueRun,
|
|
1217
4176
|
};
|
|
1218
4177
|
}
|