@tea-agent/loop-agent 0.34.0 → 0.34.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/application/task-lifecycle/advance.js +57 -7
  3. package/dist/application/task-lifecycle/plan-transitions.js +13 -21
  4. package/dist/executors/dag-pi-executor.js +18 -3
  5. package/dist/shared/operator/capabilities.js +26 -2
  6. package/dist/shared/timeout-policy.js +5 -0
  7. package/dist/task/source-prepare/prepare.js +49 -2
  8. package/dist/worker/cli.js +16 -0
  9. package/dist/worker/console/interview/grill-me.js +7 -6
  10. package/dist/worker/console/operator-actions.js +231 -22
  11. package/dist/worker/console/prd-intake-bridge.js +393 -0
  12. package/dist/worker/console/recovery-cta.js +35 -6
  13. package/dist/worker/console/server.js +28 -0
  14. package/dist/worker/console/static/assets/index-BpuHmlSP.js +29 -0
  15. package/dist/worker/console/static/index.html +1 -1
  16. package/dist/worker/console/static-src/app/console-types.js +1 -0
  17. package/dist/worker/console/static-src/app/usePrdImport.js +2 -1
  18. package/dist/worker/console/static-src/app/useRecoveryActions.js +24 -1
  19. package/dist/worker/console/static-src/app/useRecoveryConsole.js +19 -1
  20. package/dist/worker/console/static-src/app/useTaskWizard.js +57 -2
  21. package/dist/worker/observability/read-model.js +3 -0
  22. package/dist/workflows/dag/failure-category.js +3 -0
  23. package/dist/workflows/dag/init-hybrid.js +15 -9
  24. package/dist/workflows/dag/liveness-policy.js +2 -1
  25. package/dist/workflows/dag/node-execution.js +3 -2
  26. package/dist/workflows/dag/retry-policy.js +44 -11
  27. package/dist/workflows/dag/runner.js +6 -0
  28. package/dist/workflows/dag/scheduler.js +49 -1
  29. package/dist/workflows/dag/validate.js +16 -2
  30. package/docs/templates/agent-dag.schema.json +4 -4
  31. package/package.json +1 -1
  32. package/dist/worker/console/static/assets/index-CMHovlqG.js +0 -32
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Console PRD Intake Bridge (design 2026-08-11-console-prd-intake-bridge).
3
+ * Maps task-side intake artifacts / projected source into Console DraftStore
4
+ * fields. Does not run Semantic Intake itself — that stays in task advance.
5
+ */
6
+ import { access, readFile } from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { getTaskPaths } from "../../task/runtime.js";
9
+ import { SEMANTIC_INTAKE_ARTIFACT_REL, } from "../../task/source-prepare/semantic-intake.js";
10
+ import { extractRequirementFactsFromMarkdown } from "../../task/source-prepare/parse-intent.js";
11
+ import { emptyDraft } from "./interview/grill-me.js";
12
+ import { normalizeConsoleWorkflowKind } from "./workflow-kinds.js";
13
+ function asStringList(value) {
14
+ if (!Array.isArray(value))
15
+ return [];
16
+ return value
17
+ .map((item) => {
18
+ if (typeof item === "string")
19
+ return item.trim();
20
+ if (item && typeof item === "object") {
21
+ const record = item;
22
+ if (typeof record.text === "string")
23
+ return record.text.trim();
24
+ if (typeof record.message === "string")
25
+ return record.message.trim();
26
+ }
27
+ return String(item ?? "").trim();
28
+ })
29
+ .filter(Boolean);
30
+ }
31
+ function asAcceptance(value) {
32
+ if (!Array.isArray(value))
33
+ return [];
34
+ const out = [];
35
+ for (let i = 0; i < value.length; i += 1) {
36
+ const item = value[i];
37
+ if (typeof item === "string" && item.trim()) {
38
+ out.push({ id: `AC-${i + 1}`, text: item.trim() });
39
+ continue;
40
+ }
41
+ if (item && typeof item === "object") {
42
+ const record = item;
43
+ const text = typeof record.text === "string" ? record.text.trim() : "";
44
+ if (!text)
45
+ continue;
46
+ const id = typeof record.id === "string" && record.id.trim()
47
+ ? record.id.trim()
48
+ : `AC-${i + 1}`;
49
+ out.push({ id, text });
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+ function warningList(json) {
55
+ if (!json || typeof json !== "object")
56
+ return [];
57
+ const root = json;
58
+ const raw = Array.isArray(root.warnings)
59
+ ? root.warnings
60
+ : Array.isArray(root.result?.warnings)
61
+ ? root.result?.warnings
62
+ : [];
63
+ const out = [];
64
+ for (const item of raw) {
65
+ if (!item || typeof item !== "object")
66
+ continue;
67
+ const record = item;
68
+ const code = typeof record.code === "string" && record.code.trim()
69
+ ? record.code.trim()
70
+ : "UNKNOWN";
71
+ const message = typeof record.message === "string" ? record.message : code;
72
+ out.push({ code, message });
73
+ }
74
+ return out;
75
+ }
76
+ function lifecycleFromAdvanceJson(json) {
77
+ if (!json || typeof json !== "object")
78
+ return undefined;
79
+ const root = json;
80
+ const value = root.result?.lifecycleState ?? root.lifecycleState;
81
+ return typeof value === "string" ? value : undefined;
82
+ }
83
+ async function readJsonIfExists(filePath) {
84
+ try {
85
+ await access(filePath);
86
+ const raw = await readFile(filePath, "utf8");
87
+ return JSON.parse(raw);
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ function hasRequirementStructure(draft) {
94
+ const req = draft.requirement ?? {};
95
+ return Boolean(typeof req.objective === "string" &&
96
+ req.objective.trim() &&
97
+ Array.isArray(req.scope) &&
98
+ req.scope.length > 0 &&
99
+ Array.isArray(req.acceptanceCriteria) &&
100
+ req.acceptanceCriteria.length > 0);
101
+ }
102
+ /** Normalize path / verify lists from operator params (arrays or newline/comma text). */
103
+ export function parseEngineeringBoundaryFromParams(p) {
104
+ const asList = (value) => {
105
+ if (Array.isArray(value)) {
106
+ return value
107
+ .map((item) => (typeof item === "string" ? item.trim() : ""))
108
+ .filter(Boolean);
109
+ }
110
+ if (typeof value === "string") {
111
+ return value
112
+ .split(/[\n,;,;]+/)
113
+ .map((s) => s.trim())
114
+ .filter(Boolean);
115
+ }
116
+ return [];
117
+ };
118
+ const allowedPaths = asList(p.allowedPaths ?? p.allowedPath);
119
+ const forbiddenPaths = asList(p.forbiddenPaths ?? p.forbiddenPath);
120
+ const verifyRaw = p.verifyCommands ?? p.verify;
121
+ const verifyCommands = [];
122
+ if (Array.isArray(verifyRaw)) {
123
+ for (let i = 0; i < verifyRaw.length; i += 1) {
124
+ const item = verifyRaw[i];
125
+ if (typeof item === "string" && item.trim()) {
126
+ const trimmed = item.trim();
127
+ const idx = trimmed.indexOf(":");
128
+ if (idx > 0 && idx < trimmed.length - 1 && !trimmed.slice(0, idx).includes(" ")) {
129
+ verifyCommands.push({
130
+ label: trimmed.slice(0, idx),
131
+ command: trimmed.slice(idx + 1).trim(),
132
+ });
133
+ }
134
+ else {
135
+ verifyCommands.push({
136
+ label: `verify-${i + 1}`,
137
+ command: trimmed,
138
+ });
139
+ }
140
+ }
141
+ else if (item && typeof item === "object") {
142
+ const rec = item;
143
+ const command = typeof rec.command === "string" ? rec.command.trim() : "";
144
+ if (!command)
145
+ continue;
146
+ verifyCommands.push({
147
+ label: typeof rec.label === "string" && rec.label.trim()
148
+ ? rec.label.trim()
149
+ : `verify-${i + 1}`,
150
+ command,
151
+ ...(typeof rec.timeoutMs === "number"
152
+ ? { timeoutMs: rec.timeoutMs }
153
+ : {}),
154
+ });
155
+ }
156
+ }
157
+ }
158
+ else if (typeof verifyRaw === "string" && verifyRaw.trim()) {
159
+ for (const [i, line] of asList(verifyRaw).entries()) {
160
+ const idx = line.indexOf(":");
161
+ if (idx > 0 && idx < line.length - 1 && !line.slice(0, idx).includes(" ")) {
162
+ verifyCommands.push({
163
+ label: line.slice(0, idx),
164
+ command: line.slice(idx + 1).trim(),
165
+ });
166
+ }
167
+ else {
168
+ verifyCommands.push({ label: `verify-${i + 1}`, command: line });
169
+ }
170
+ }
171
+ }
172
+ return {
173
+ ...(allowedPaths.length > 0 ? { allowedPaths } : {}),
174
+ ...(forbiddenPaths.length > 0 ? { forbiddenPaths } : {}),
175
+ ...(verifyCommands.length > 0 ? { verifyCommands } : {}),
176
+ };
177
+ }
178
+ export function appendEngineeringCliArgs(args, boundary) {
179
+ for (const pathGlob of boundary.allowedPaths ?? []) {
180
+ args.push("--allowed-path", pathGlob);
181
+ }
182
+ for (const pathGlob of boundary.forbiddenPaths ?? []) {
183
+ args.push("--forbidden-path", pathGlob);
184
+ }
185
+ for (const cmd of boundary.verifyCommands ?? []) {
186
+ args.push("--verify", `${cmd.label}:${cmd.command}`);
187
+ }
188
+ }
189
+ export function applyEngineeringBoundaryToDraft(draft, boundary) {
190
+ if (!(boundary.allowedPaths?.length) &&
191
+ !(boundary.forbiddenPaths?.length) &&
192
+ !(boundary.verifyCommands?.length)) {
193
+ return draft;
194
+ }
195
+ return {
196
+ ...draft,
197
+ constraints: {
198
+ invariants: draft.constraints?.invariants ?? [],
199
+ allowedPaths: boundary.allowedPaths?.length
200
+ ? boundary.allowedPaths
201
+ : (draft.constraints?.allowedPaths ?? []),
202
+ forbiddenPaths: boundary.forbiddenPaths?.length
203
+ ? boundary.forbiddenPaths
204
+ : (draft.constraints?.forbiddenPaths ?? []),
205
+ },
206
+ verification: {
207
+ commands: boundary.verifyCommands?.length
208
+ ? boundary.verifyCommands
209
+ : (draft.verification?.commands ?? []),
210
+ },
211
+ };
212
+ }
213
+ export async function buildDraftFromTaskIntake(input) {
214
+ const taskKind = normalizeConsoleWorkflowKind(input.taskKind, "standard");
215
+ const paths = getTaskPaths(input.repoRoot, input.taskId);
216
+ const warnings = warningList(input.advanceJson);
217
+ const lifecycleState = lifecycleFromAdvanceJson(input.advanceJson);
218
+ const semanticApplied = warnings.some((w) => w.code === "SEMANTIC_INTAKE_APPLIED");
219
+ const semanticFailed = warnings.find((w) => w.code === "SEMANTIC_INTAKE_PI_FAILED" ||
220
+ w.code === "SEMANTIC_INTAKE_INVALID_OUTPUT" ||
221
+ w.code === "SEMANTIC_INTAKE_FAILED" ||
222
+ w.code === "SEMANTIC_INTAKE_NO_DOCUMENTS" ||
223
+ w.code === "SEMANTIC_INTAKE_REFUSED_NON_EXECUTABLE");
224
+ const semanticSkipped = warnings.find((w) => w.code === "SEMANTIC_INTAKE_SKIPPED");
225
+ const artifactPath = path.join(paths.taskDir, SEMANTIC_INTAKE_ARTIFACT_REL);
226
+ const artifact = await readJsonIfExists(artifactPath);
227
+ let draft = {
228
+ ...emptyDraft(input.taskId, input.title, { taskKind }),
229
+ requirement: {
230
+ objective: input.title,
231
+ },
232
+ };
233
+ let parseStatus = "pending";
234
+ let semanticIntake = {
235
+ applied: false,
236
+ code: semanticFailed?.code ?? semanticSkipped?.code,
237
+ message: semanticFailed?.message ?? semanticSkipped?.message,
238
+ };
239
+ const semanticReq = artifact?.draft?.requirement ??
240
+ (artifact?.semantic
241
+ ? {
242
+ objective: artifact.semantic.objective,
243
+ scope: artifact.semantic.scope,
244
+ nonGoals: artifact.semantic.nonGoals,
245
+ acceptanceCriteria: artifact.semantic.acceptanceCriteria,
246
+ }
247
+ : undefined);
248
+ // An artifact is authoritative only when this exact advance invocation reports
249
+ // SEMANTIC_INTAKE_APPLIED. Tasks may import newer immutable PRD revisions while
250
+ // an older per-task semantic artifact remains on disk; never project it as the
251
+ // current Console draft.
252
+ if (semanticApplied && semanticReq?.objective?.trim()) {
253
+ const title = artifact?.draft?.title?.trim() ||
254
+ artifact?.semantic?.title?.trim() ||
255
+ input.title;
256
+ draft = {
257
+ ...draft,
258
+ title,
259
+ requirement: {
260
+ objective: semanticReq.objective.trim(),
261
+ scope: asStringList(semanticReq.scope),
262
+ nonGoals: asStringList(semanticReq.nonGoals),
263
+ acceptanceCriteria: asAcceptance(semanticReq.acceptanceCriteria),
264
+ },
265
+ openQuestions: asStringList(artifact?.draft?.openQuestions ?? artifact?.semantic?.openQuestions) || undefined,
266
+ assumptions: asStringList(artifact?.draft?.assumptions ?? artifact?.semantic?.assumptions) || undefined,
267
+ references: artifact?.draft?.references,
268
+ };
269
+ parseStatus = "semantic";
270
+ semanticIntake = {
271
+ applied: true,
272
+ code: "SEMANTIC_INTAKE_APPLIED",
273
+ message: "structured from artifacts/intake/semantic-draft.json",
274
+ };
275
+ }
276
+ else {
277
+ // Deterministic path. When the current intake is incomplete/failed/skipped,
278
+ // prefer the newest imported reference over an older projected 需求.md.
279
+ // Successful deterministic projection still prefers canonical 需求.md.
280
+ const projectedRequirementPath = path.join(paths.sourceDir, "需求.md");
281
+ const referenceCandidates = [];
282
+ try {
283
+ const manifestPath = path.join(paths.sourceDir, "source-manifest.json");
284
+ const manifest = await readJsonIfExists(manifestPath);
285
+ for (const doc of manifest?.documents ?? []) {
286
+ if ((doc.role === "requirement" || doc.role === "acceptance") &&
287
+ typeof doc.materializedPath === "string") {
288
+ referenceCandidates.push({
289
+ filePath: path.join(paths.sourceDir, doc.materializedPath),
290
+ importedAt: typeof doc.importedAt === "string" ? doc.importedAt : "",
291
+ });
292
+ }
293
+ }
294
+ }
295
+ catch {
296
+ // ignore manifest read errors
297
+ }
298
+ referenceCandidates.sort((left, right) => right.importedAt.localeCompare(left.importedAt));
299
+ const preferImportedReferences = Boolean(semanticFailed ||
300
+ semanticSkipped ||
301
+ lifecycleState === "intake-incomplete");
302
+ const candidates = preferImportedReferences
303
+ ? [
304
+ ...referenceCandidates.map((candidate) => candidate.filePath),
305
+ projectedRequirementPath,
306
+ ]
307
+ : [
308
+ projectedRequirementPath,
309
+ ...referenceCandidates.map((candidate) => candidate.filePath),
310
+ ];
311
+ for (const filePath of candidates) {
312
+ try {
313
+ const markdown = await readFile(filePath, "utf8");
314
+ const facts = extractRequirementFactsFromMarkdown(markdown);
315
+ if (!facts.objective?.trim())
316
+ continue;
317
+ draft = {
318
+ ...draft,
319
+ title: facts.title?.trim() || input.title,
320
+ requirement: {
321
+ objective: facts.objective.trim(),
322
+ scope: facts.scope,
323
+ nonGoals: facts.nonGoals,
324
+ acceptanceCriteria: facts.acceptanceCriteria,
325
+ },
326
+ openQuestions: facts.openQuestions.length > 0
327
+ ? facts.openQuestions
328
+ : undefined,
329
+ assumptions: facts.assumptions.length > 0
330
+ ? facts.assumptions
331
+ : undefined,
332
+ };
333
+ parseStatus = hasRequirementStructure(draft)
334
+ ? "deterministic"
335
+ : "incomplete";
336
+ if (hasRequirementStructure(draft))
337
+ break;
338
+ }
339
+ catch {
340
+ // try next candidate
341
+ }
342
+ }
343
+ // Fallback: title-only objective (never invent scope/AC templates).
344
+ if (!draft.requirement?.objective?.trim()) {
345
+ draft = {
346
+ ...draft,
347
+ requirement: { objective: input.title },
348
+ };
349
+ parseStatus = "incomplete";
350
+ }
351
+ else if (parseStatus === "pending") {
352
+ parseStatus = hasRequirementStructure(draft)
353
+ ? "deterministic"
354
+ : "incomplete";
355
+ }
356
+ }
357
+ // Engineering gaps remain as draft gaps; do not invent paths/AC templates.
358
+ const gaps = warnings.filter((w) => w.code === "EMPTY_ALLOWED_PATHS" ||
359
+ w.code === "EMPTY_ACCEPTANCE" ||
360
+ w.code === "EMPTY_OBJECTIVE" ||
361
+ w.code === "EMPTY_FORBIDDEN_PATHS" ||
362
+ w.code === "INTAKE_INCOMPLETE" ||
363
+ w.code === "BOUNDARY_REQUIRED" ||
364
+ w.code.startsWith("SEMANTIC_INTAKE_") ||
365
+ w.code === "NO_PARSEABLE_REQUIREMENT");
366
+ if (semanticFailed && !semanticIntake.applied) {
367
+ if (!hasRequirementStructure(draft)) {
368
+ parseStatus = "failed";
369
+ }
370
+ semanticIntake = {
371
+ applied: false,
372
+ code: semanticFailed.code,
373
+ message: semanticFailed.message,
374
+ };
375
+ }
376
+ else if (!hasRequirementStructure(draft) && parseStatus !== "semantic") {
377
+ parseStatus = "incomplete";
378
+ }
379
+ if (input.engineering) {
380
+ draft = applyEngineeringBoundaryToDraft(draft, input.engineering);
381
+ }
382
+ return {
383
+ draft,
384
+ parseStatus,
385
+ semanticIntake,
386
+ gaps,
387
+ lifecycleState,
388
+ };
389
+ }
390
+ /** True when requirement structure is filled enough that Skip should not invent AC/scope. */
391
+ export function requirementStructurallyReady(draft) {
392
+ return hasRequirementStructure(draft);
393
+ }
@@ -37,9 +37,10 @@ const CTA = {
37
37
  id: "reconcile",
38
38
  label: "对账 / reconcile",
39
39
  requiresReason: true,
40
+ action: "dagReconcileRun",
40
41
  lane: "dag",
41
- guide: "状态不一致时先对账,再决定是否重跑(需原因)",
42
- commandHint: "loop-agent dag reconcile-run --run-id <run-id> --reason <structured-reason>",
42
+ guide: "Orphan/中断时先对账归档(默认 supersede);runner 仍存活时勿盲 supersede",
43
+ commandHint: "loop-agent dag reconcile-run --run-id <run-id> --action supersede --reason <structured-reason>",
43
44
  },
44
45
  regenerate: {
45
46
  id: "regenerate",
@@ -97,7 +98,9 @@ const MATRIX = {
97
98
  "resume",
98
99
  "regenerate",
99
100
  ],
100
- "orphan-unknown": ["report", "doctor", "reconcile"],
101
+ // Stall/quiet/unknown: observe + doctor only — do not offer supersede while
102
+ // runner may still be alive (eligibility fail-closes, but UI should not tease).
103
+ "orphan-unknown": ["report", "doctor"],
101
104
  "terminal-failed": [
102
105
  "report",
103
106
  "doctor",
@@ -137,8 +140,9 @@ export function partitionRecoveryCtas(ctas) {
137
140
  }
138
141
  /**
139
142
  * Console only shows CTAs that actually dispatch an operator action.
140
- * Pure CLI-hint rows (regenerate / decision / resume / reconcile) stay in the
141
- * policy matrix for docs, but must not appear as dead buttons.
143
+ * Pure CLI-hint rows (regenerate / decision / resume) stay in the policy
144
+ * matrix for docs, but must not appear as dead buttons. reconcile is wired
145
+ * to dagReconcileRun (structured reason required).
142
146
  */
143
147
  export function isConsoleExecutableRecoveryCta(cta) {
144
148
  return typeof cta.action === "string" && cta.action.trim().length > 0;
@@ -167,6 +171,8 @@ export function recommendedRecoveryCtaId(factClass) {
167
171
  case "pause-on-human":
168
172
  return pick("doctor", "report");
169
173
  case "needs-reconcile":
174
+ // Prefer doctor first; reconcile is available when operator confirms reason.
175
+ return pick("doctor", "reconcile", "report");
170
176
  case "runtime-mismatch":
171
177
  case "contract-drift":
172
178
  return pick("doctor", "report");
@@ -201,6 +207,7 @@ export function classifyRecoveryFact(input) {
201
207
  const code = (input.errorCode ?? "").toLowerCase();
202
208
  const state = (input.operationState ?? "").toLowerCase();
203
209
  const dag = (input.dagStatus ?? "").toLowerCase();
210
+ const failure = (input.failureCategory ?? "").toLowerCase();
204
211
  if (code.includes("validation") ||
205
212
  code.includes("invalid") ||
206
213
  state.includes("validation")) {
@@ -225,15 +232,37 @@ export function classifyRecoveryFact(input) {
225
232
  if (code.includes("verif") || state.includes("verif")) {
226
233
  return "verification-failed";
227
234
  }
235
+ // Controller interrupt / orphaned mid-run: reconcile first (not blind rerun).
236
+ if (failure === "controller-interrupted" ||
237
+ code.includes("controller-interrupted") ||
238
+ code.includes("interrupted") ||
239
+ dag === "interrupted" ||
240
+ dag === "orphaned" ||
241
+ state.includes("controller-interrupted") ||
242
+ state.includes("interrupted")) {
243
+ return "needs-reconcile";
244
+ }
245
+ // Live stall / quiet while runner lease may still be fresh — observe/doctor only
246
+ // (Console executable CTAs: report + doctor; reconcile stays CLI-hint).
247
+ if (dag === "running-suspected-stall" ||
248
+ dag === "running-quiet" ||
249
+ dag === "suspected-stall" ||
250
+ dag.includes("suspected-stall") ||
251
+ state.includes("suspected-stall")) {
252
+ return "orphan-unknown";
253
+ }
228
254
  if (state === "succeeded" ||
229
255
  state === "completed" ||
256
+ state === "finished" ||
230
257
  dag === "succeeded" ||
231
- dag === "completed") {
258
+ dag === "completed" ||
259
+ dag === "finished") {
232
260
  return "terminal-succeeded";
233
261
  }
234
262
  if (state === "failed" ||
235
263
  state.includes("fail") ||
236
264
  dag === "failed" ||
265
+ dag === "partial_failed" ||
237
266
  code.includes("fail")) {
238
267
  return "terminal-failed";
239
268
  }
@@ -4,6 +4,7 @@ import { createServer } from "node:http";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { LoopAgentClient } from "../loop-agent/loop-agent-client.js";
7
+ import { DAG_ABSOLUTE_MAX_WALL_CLOCK_MS } from "../../shared/timeout-policy.js";
7
8
  import { openConsoleAppData } from "./app-data.js";
8
9
  import { DagConfirmationStore } from "./dag-confirmation.js";
9
10
  import { DagExecutionReceiptStore } from "./dag-execution-receipt.js";
@@ -24,6 +25,30 @@ import { resolveSiblingLoopAgentBin } from "./sibling-controller.js";
24
25
  import { NightAuxTicker } from "./night-aux-ticker.js";
25
26
  import { deriveObserveRouteCapabilities } from "../observe/health.js";
26
27
  import { createObserveRouteContext, defaultObserveStaticDir, handleMatchedObserveRoute, matchObserveGetRoute, ROUTES, serveObserveStatic, } from "../observe/routes.js";
28
+ /**
29
+ * Default hard timeout for the Console-owned LoopAgentClient.
30
+ *
31
+ * Console task-nav operations (`dag execute`, `task advance --approve-gate`,
32
+ * etc.) spawn a long-lived sibling runner via the operation runner. The
33
+ * worker client hard-kills children after `defaultTimeoutMs` (default 120s),
34
+ * which is far below multi-node DAG wall clocks and leaves `state.json`
35
+ * stuck in `running` with no error. Align with shared `DAG_ABSOLUTE_MAX_WALL_CLOCK_MS`
36
+ * (4h) so the client is a safety net, not a false deadline.
37
+ */
38
+ export const DEFAULT_CONSOLE_CLIENT_TIMEOUT_MS = DAG_ABSOLUTE_MAX_WALL_CLOCK_MS;
39
+ /**
40
+ * Resolve the Console LoopAgentClient default timeout.
41
+ * `0` disables the client hard timeout (DAG kernel owns wall clock).
42
+ */
43
+ export function resolveConsoleDefaultTimeoutMs(override) {
44
+ if (override === undefined) {
45
+ return DEFAULT_CONSOLE_CLIENT_TIMEOUT_MS;
46
+ }
47
+ if (!Number.isFinite(override) || override < 0) {
48
+ throw new Error(`console serve: invalid defaultTimeoutMs ${String(override)} (expected >= 0)`);
49
+ }
50
+ return override;
51
+ }
27
52
  /** Built static assets live next to the compiled server under dist/worker/console/static. */
28
53
  export function defaultConsoleStaticDir(fromFileUrl = import.meta.url) {
29
54
  return path.join(path.dirname(fileURLToPath(fromFileUrl)), "static");
@@ -74,6 +99,9 @@ export async function createConsoleServer(options) {
74
99
  loopAgentBin: resolveSiblingLoopAgentBin(),
75
100
  artifactRoot,
76
101
  resolveIdentity: true,
102
+ // See DEFAULT_CONSOLE_CLIENT_TIMEOUT_MS: dag execute / task advance
103
+ // must not inherit the 120s worker-command default.
104
+ defaultTimeoutMs: resolveConsoleDefaultTimeoutMs(options.defaultTimeoutMs),
77
105
  });
78
106
  if (!options.skipReconcile) {
79
107
  await operations.reconcileOnBoot();