@tea-agent/loop-agent 0.37.0 → 0.37.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -6
- package/CHANGELOG.md +71 -144
- package/README.md +1 -1
- package/bin/loop-agent.js +1 -37
- package/dist/application/task-lifecycle/advance.js +0 -1
- package/dist/application/task-lifecycle/observe.js +0 -15
- package/dist/application/task-lifecycle/plan-transitions.js +0 -15
- package/dist/cli/command-definitions.js +0 -7
- package/dist/cli/program.js +3 -8
- package/dist/commands/init-upgrade.js +19 -351
- package/dist/commands/init.js +67 -14
- package/dist/commands/run-dag-progress.js +0 -14
- package/dist/commands/task-advance.js +3 -33
- package/dist/executors/dag-pi-executor.js +30 -47
- package/dist/executors/pi-sdk-executor.js +0 -34
- package/dist/executors/shell-executor.js +95 -6
- package/dist/shared/operator/capabilities.js +13 -200
- package/dist/shared/package-metadata.js +0 -42
- package/dist/task/contract/project.js +0 -4
- package/dist/task/source-prepare/completeness.js +0 -53
- package/dist/task/source-prepare/parse-intent.js +12 -82
- package/dist/task/source-prepare/prepare.js +5 -102
- package/dist/task/source-prepare/semantic-intake.js +23 -144
- package/dist/worker/console/chat/artifact-card.js +1 -8
- package/dist/worker/console/chat/chat-event-store.js +0 -152
- package/dist/worker/console/chat/human-gate-card.js +1 -9
- package/dist/worker/console/chat/operation-card.js +2 -71
- package/dist/worker/console/chat/pi-runtime.js +62 -111
- package/dist/worker/console/chat/routes.js +10 -43
- package/dist/worker/console/chat/session-store.js +1 -9
- package/dist/worker/console/chat/shortcuts.js +7 -9
- package/dist/worker/console/chat/turn-process.js +23 -204
- package/dist/worker/console/chat/workspace-landing.js +1 -2
- package/dist/worker/console/operation-runner.js +6 -155
- package/dist/worker/console/operator-actions.js +14 -391
- package/dist/worker/console/operator-user-error.js +0 -4
- package/dist/worker/console/recovery-cta.js +3 -50
- package/dist/worker/console/static/assets/index-HX1pbOyl.css +1 -0
- package/dist/worker/console/static/assets/index-M0BLEBfh.js +56 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +9 -13
- package/dist/worker/console/static-src/app/useOperatorActions.js +2 -4
- package/dist/worker/console/static-src/app/useRecoveryActions.js +56 -65
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +1 -73
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +30 -114
- package/dist/worker/console/static-src/operator-chat/refs.js +0 -3
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +13 -67
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -12
- package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -45
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -7
- package/dist/worker/loop-agent/loop-agent-client.js +3 -17
- package/dist/worker/observability/read-model.js +0 -20
- package/dist/worker/observe/spec-evidence.js +8 -3
- package/dist/worker/observe/static/operator-chrome.d.ts +0 -1
- package/dist/worker/observe/static/operator-chrome.js +3 -10
- package/dist/worker/observe/static/views/dag-inspector.js +71 -6
- package/dist/worker/observe/static/views/dag.js +0 -12
- package/dist/worker/preflight.js +1 -2
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +707 -37
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +109 -7
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
- package/dist/workflows/dag/backend-test-scenario-param.js +890 -164
- package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
- package/dist/workflows/dag/frontend-prewrite-gate.js +61 -364
- package/dist/workflows/dag/frontend-repair.js +18 -219
- package/dist/workflows/dag/frontend-verification-trace.js +32 -47
- package/dist/workflows/dag/init-hybrid.js +82 -165
- package/dist/workflows/dag/node-execution.js +0 -89
- package/dist/workflows/dag/recovery-recommendation.js +0 -58
- package/dist/workflows/dag/runner.js +15 -333
- package/dist/workflows/dag/scheduler.js +3 -257
- package/dist/workflows/dag/types.js +9 -130
- package/docs/README.md +3 -3
- package/docs/architecture/evolution.md +67 -102
- package/docs/templates/backend-test-dag.json +50 -32
- package/docs/templates/init-managed-agents.md +2 -5
- package/harness.json +2 -2
- package/package.json +3 -9
- package/skills/loop-agent/SKILL.md +0 -1
- package/skills/loop-agent/references/command-reference.md +0 -4
- package/dist/build-stamp.json +0 -6
- package/dist/commands/dag-request-interrupt.js +0 -20
- package/dist/worker/console/chat/assistant-content.js +0 -121
- package/dist/worker/console/chat/semantic-activity.js +0 -477
- package/dist/worker/console/operation-run-facts.js +0 -190
- package/dist/worker/console/operation-wait.js +0 -355
- package/dist/worker/console/recovery-error-copy.js +0 -198
- package/dist/worker/console/static/assets/abnfDiagram-N423BO3Z-DSvF6RXQ.js +0 -1
- package/dist/worker/console/static/assets/arc-DuI4ogtJ.js +0 -1
- package/dist/worker/console/static/assets/architectureDiagram-T3A2C74G-DdBbgD4y.js +0 -36
- package/dist/worker/console/static/assets/blockDiagram-VBNYF7ZC-xQC2HELm.js +0 -132
- package/dist/worker/console/static/assets/c4Diagram-5PPSVZJV-DPMSgwIZ.js +0 -10
- package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
- package/dist/worker/console/static/assets/chunk-2GRJ4B5K-WTEKTxin.js +0 -1
- package/dist/worker/console/static/assets/chunk-2Q5K7J3B-B6vZ0Pv_.js +0 -1
- package/dist/worker/console/static/assets/chunk-5RXB4S5H-BrerNAix.js +0 -231
- package/dist/worker/console/static/assets/chunk-5VM5RSS4-D_ch2aaI.js +0 -15
- package/dist/worker/console/static/assets/chunk-6Q2QTUOP-VvfacNrq.js +0 -88
- package/dist/worker/console/static/assets/chunk-GF5L2VYU-B-uGFIg2.js +0 -206
- package/dist/worker/console/static/assets/chunk-JWPE2WC7-BlysCtod.js +0 -1
- package/dist/worker/console/static/assets/chunk-KBJHAD2P-BjAUAfcT.js +0 -1
- package/dist/worker/console/static/assets/chunk-RYQCIY6F-BeZnBmb-.js +0 -1
- package/dist/worker/console/static/assets/chunk-XXDRQBXY-Bbfe3UJG.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
- package/dist/worker/console/static/assets/cose-bilkent-JH36ORCC-BKJQnYVM.js +0 -1
- package/dist/worker/console/static/assets/cynefin-VYW2F7L2-B1wtI3hU.js +0 -166
- package/dist/worker/console/static/assets/cynefinDiagram-MW4NZA55-DCF6DBSu.js +0 -62
- package/dist/worker/console/static/assets/cytoscape.esm-yzknjiTM.js +0 -321
- package/dist/worker/console/static/assets/dagre-VZM6K2ZE-BJyNGRlP.js +0 -4
- package/dist/worker/console/static/assets/defaultLocale-DX6XiGOO.js +0 -1
- package/dist/worker/console/static/assets/diagram-7IWD3JNH-B7j1-VsZ.js +0 -30
- package/dist/worker/console/static/assets/diagram-B4RE2ZJO-BREJ140a.js +0 -3
- package/dist/worker/console/static/assets/diagram-LBJQPF4R-CDPHmHGH.js +0 -24
- package/dist/worker/console/static/assets/diagram-Q27KOJAE-BtZLrez1.js +0 -24
- package/dist/worker/console/static/assets/diagram-UB23O5K3-mzybu8oU.js +0 -41
- package/dist/worker/console/static/assets/ebnfDiagram-BXEA7PRR-Dk3I2uO6.js +0 -1
- package/dist/worker/console/static/assets/erDiagram-JOGREHBK-BT97u_tQ.js +0 -85
- package/dist/worker/console/static/assets/flowDiagram-UKHOOZJN-CMOtYzui.js +0 -156
- package/dist/worker/console/static/assets/ganttDiagram-PKOTCBZU-DiNydkuY.js +0 -292
- package/dist/worker/console/static/assets/gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js +0 -106
- package/dist/worker/console/static/assets/graph-DOmOIIwC.js +0 -1
- package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
- package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
- package/dist/worker/console/static/assets/infoDiagram-6WML65LV-AsGIGKKB.js +0 -2
- package/dist/worker/console/static/assets/init-Gi6I4Gst.js +0 -1
- package/dist/worker/console/static/assets/ishikawaDiagram-WSZJBQD7-BL8ITZh3.js +0 -70
- package/dist/worker/console/static/assets/journeyDiagram-NVQOT4AX-VPKAUX8M.js +0 -139
- package/dist/worker/console/static/assets/kanban-definition-27J2QSJJ-BL56hfGp.js +0 -89
- package/dist/worker/console/static/assets/layout-D-LzfAck.js +0 -1
- package/dist/worker/console/static/assets/linear-BmSG3nIZ.js +0 -1
- package/dist/worker/console/static/assets/map-DxJ2ADlA.js +0 -1
- package/dist/worker/console/static/assets/mermaid.core-DWGK9kwP.js +0 -308
- package/dist/worker/console/static/assets/mindmap-definition-FAOFIHXS--vvKLjo0.js +0 -96
- package/dist/worker/console/static/assets/ordinal-Cboi1Yqb.js +0 -1
- package/dist/worker/console/static/assets/pegDiagram-VL7TDLO6-B41mxb2-.js +0 -1
- package/dist/worker/console/static/assets/pieDiagram-7S7Q4E2Y-DTlu77sw.js +0 -39
- package/dist/worker/console/static/assets/quadrantDiagram-CIZ2JOQS-Bk5A7alv.js +0 -7
- package/dist/worker/console/static/assets/railroadDiagram-AXF67PYL-Csf9jzQy.js +0 -1
- package/dist/worker/console/static/assets/requirementDiagram-LRYGKXZP-CeptoSNx.js +0 -84
- package/dist/worker/console/static/assets/sankeyDiagram-W5VNT64P-A-DLCLer.js +0 -40
- package/dist/worker/console/static/assets/sequenceDiagram-SI44F4Z6-22VbPdOI.js +0 -162
- package/dist/worker/console/static/assets/sizeCapture-X5ZJPWSS-DWvAyBC9.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-OKZ733FA-qICZ75yT.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
- package/dist/worker/console/static/assets/swimlanes-SLNWSIFB-xi7lcMyp.js +0 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
- package/dist/worker/console/static/assets/timeline-definition-Z64GVDOM-Bu6Nl6O2.js +0 -120
- package/dist/worker/console/static/assets/vennDiagram-T6HMQDX7-BzMz4Bn9.js +0 -34
- package/dist/worker/console/static/assets/wardleyDiagram-T6FBY63Y-CxaJnzc5.js +0 -78
- package/dist/worker/console/static/assets/xychartDiagram-ELKLHX3M-5JK-omQK.js +0 -7
- package/dist/worker/console/static/fonts/katex/KaTeX_AMS-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Main-Bold.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Main-Italic.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Main-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Math-Italic.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Script-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Size1-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Size2-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Size3-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Size4-Regular.woff2 +0 -0
- package/dist/worker/console/static/fonts/katex/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/dist/worker/console/static-src/operator-chat/activity-journey.js +0 -125
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/activity-references.js +0 -20
- package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +0 -24
- package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +0 -141
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
- package/dist/worker/observability/interrupt-eligibility.js +0 -264
- package/dist/workflows/dag/contract-output-registry.js +0 -14
- package/dist/workflows/dag/contract-validator-registrations.js +0 -8
- package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
- package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
- package/dist/workflows/dag/frontend-recovery-run.js +0 -539
- package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
- package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
- package/dist/workflows/dag/interrupt-request.js +0 -559
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Trusted run-facts extraction + safe run summary projection
|
|
3
|
-
* (2026-08-14 Operator Chat DAG recovery P0).
|
|
4
|
-
*
|
|
5
|
-
* Invariants:
|
|
6
|
-
* - Only deterministic structured sources are trusted: the standard
|
|
7
|
-
* `OperatorCommandResultV1.result.runSummary` envelope, a structured CLI
|
|
8
|
-
* `runSummary` object, or the rerun standard `newRunId`. Free-text stdout /
|
|
9
|
-
* non-standard JSON is NEVER scanned or guessed for run ids.
|
|
10
|
-
* - The projection uses an independent allowlist with bounded fields/arrays
|
|
11
|
-
* and redacted text; it must never leak raw results, actionParams, CLI
|
|
12
|
-
* args, absolute artifact paths or secrets.
|
|
13
|
-
* - A run summary is a projection only: it can never change the terminal
|
|
14
|
-
* operation state decided by the command envelope/exit facts.
|
|
15
|
-
*/
|
|
16
|
-
import { scrubSecrets } from "./chat/explore-tools.js";
|
|
17
|
-
const RUN_SUMMARY_MAX_NODES = 20;
|
|
18
|
-
const RUN_SUMMARY_MAX_TEXT_CHARS = 600;
|
|
19
|
-
const ABSOLUTE_PATH_REDACTION = "[REDACTED-ABS-PATH]";
|
|
20
|
-
function asRecord(value) {
|
|
21
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
22
|
-
return null;
|
|
23
|
-
return value;
|
|
24
|
-
}
|
|
25
|
-
function redactAbsolutePathSubstrings(value) {
|
|
26
|
-
let redacted = "";
|
|
27
|
-
let cursor = 0;
|
|
28
|
-
while (cursor < value.length) {
|
|
29
|
-
const current = value[cursor] ?? "";
|
|
30
|
-
const next = value[cursor + 1] ?? "";
|
|
31
|
-
const previous = value[cursor - 1] ?? "";
|
|
32
|
-
const windowsDrive = /[A-Za-z]/.test(current) &&
|
|
33
|
-
next === ":" &&
|
|
34
|
-
/[\\/]/.test(value[cursor + 2] ?? "") &&
|
|
35
|
-
(cursor === 0 || !/[A-Za-z0-9_]/.test(previous));
|
|
36
|
-
const uncPath = current === "\\" && next === "\\";
|
|
37
|
-
const posixPath = current === "/" &&
|
|
38
|
-
next !== "/" &&
|
|
39
|
-
(cursor === 0 || (!/[A-Za-z0-9_]/.test(previous) && previous !== "/"));
|
|
40
|
-
if (!windowsDrive && !uncPath && !posixPath) {
|
|
41
|
-
redacted += current;
|
|
42
|
-
cursor += 1;
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
const quote = previous === '"' || previous === "'" ? previous : undefined;
|
|
46
|
-
let end = cursor;
|
|
47
|
-
while (end < value.length) {
|
|
48
|
-
const char = value[end] ?? "";
|
|
49
|
-
if (quote ? char === quote : /["'<>()[\]{},;]/.test(char))
|
|
50
|
-
break;
|
|
51
|
-
end += 1;
|
|
52
|
-
}
|
|
53
|
-
redacted += ABSOLUTE_PATH_REDACTION;
|
|
54
|
-
cursor = end;
|
|
55
|
-
}
|
|
56
|
-
return redacted;
|
|
57
|
-
}
|
|
58
|
-
function boundedRedactedText(value, maxChars = RUN_SUMMARY_MAX_TEXT_CHARS) {
|
|
59
|
-
if (typeof value !== "string" || !value.trim())
|
|
60
|
-
return undefined;
|
|
61
|
-
const { scrubbed } = scrubSecrets(value);
|
|
62
|
-
const pathRedacted = redactAbsolutePathSubstrings(scrubbed);
|
|
63
|
-
return pathRedacted.length <= maxChars
|
|
64
|
-
? pathRedacted
|
|
65
|
-
: `${pathRedacted.slice(0, maxChars)}…[truncated]`;
|
|
66
|
-
}
|
|
67
|
-
function safeNode(value) {
|
|
68
|
-
const rec = asRecord(value);
|
|
69
|
-
if (!rec)
|
|
70
|
-
return undefined;
|
|
71
|
-
const id = boundedRedactedText(rec.id, 200);
|
|
72
|
-
const status = boundedRedactedText(rec.status, 100);
|
|
73
|
-
if (!id || !status)
|
|
74
|
-
return undefined;
|
|
75
|
-
return { id, status };
|
|
76
|
-
}
|
|
77
|
-
/** Independent allowlist projection of a structured run summary. */
|
|
78
|
-
export function safeRunSummaryForProjection(value) {
|
|
79
|
-
const rec = asRecord(value);
|
|
80
|
-
if (!rec)
|
|
81
|
-
return undefined;
|
|
82
|
-
const runId = boundedRedactedText(rec.runId, 200);
|
|
83
|
-
if (!runId)
|
|
84
|
-
return undefined;
|
|
85
|
-
const status = boundedRedactedText(rec.status, 100);
|
|
86
|
-
if (!status)
|
|
87
|
-
return undefined;
|
|
88
|
-
const nodes = [];
|
|
89
|
-
if (Array.isArray(rec.nodes)) {
|
|
90
|
-
for (const raw of rec.nodes) {
|
|
91
|
-
if (nodes.length >= RUN_SUMMARY_MAX_NODES)
|
|
92
|
-
break;
|
|
93
|
-
const node = safeNode(raw);
|
|
94
|
-
if (node)
|
|
95
|
-
nodes.push(node);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
const outOfBounds = [];
|
|
99
|
-
if (Array.isArray(rec.outOfBounds)) {
|
|
100
|
-
for (const raw of rec.outOfBounds.slice(0, RUN_SUMMARY_MAX_NODES)) {
|
|
101
|
-
const entry = asRecord(raw);
|
|
102
|
-
if (!entry)
|
|
103
|
-
continue;
|
|
104
|
-
const nodeId = boundedRedactedText(entry.nodeId, 200);
|
|
105
|
-
const failureCategory = boundedRedactedText(entry.failureCategory, 200);
|
|
106
|
-
if (nodeId && failureCategory)
|
|
107
|
-
outOfBounds.push({ nodeId, failureCategory });
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
let next;
|
|
111
|
-
const nextRec = asRecord(rec.next);
|
|
112
|
-
if (nextRec) {
|
|
113
|
-
const kind = boundedRedactedText(nextRec.kind, 100);
|
|
114
|
-
if (kind) {
|
|
115
|
-
const description = boundedRedactedText(nextRec.description);
|
|
116
|
-
next = { kind, ...(description ? { description } : {}) };
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
const verdict = boundedRedactedText(rec.verdict);
|
|
120
|
-
const failureCategory = boundedRedactedText(rec.failureCategory);
|
|
121
|
-
return {
|
|
122
|
-
runId,
|
|
123
|
-
status,
|
|
124
|
-
nodes,
|
|
125
|
-
...(verdict !== undefined ? { verdict } : {}),
|
|
126
|
-
...(failureCategory !== undefined ? { failureCategory } : {}),
|
|
127
|
-
...(outOfBounds.length > 0 ? { outOfBounds } : {}),
|
|
128
|
-
...(next ? { next } : {}),
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Deterministic trusted-source extraction:
|
|
133
|
-
* 1. `OperatorCommandResultV1.result.runSummary` (schemaVersion=1 envelope);
|
|
134
|
-
* 2. structured CLI JSON `runSummary` object (task advance / report shapes);
|
|
135
|
-
* 3. rerun standard `newRunId` (optionally with its own `runSummary`).
|
|
136
|
-
*
|
|
137
|
-
* Candidates are checked in order; a rerun `newRunId` that disagrees with the
|
|
138
|
-
* accompanying `runSummary.runId` is rejected (no guessing). Free text is
|
|
139
|
-
* never scanned.
|
|
140
|
-
*/
|
|
141
|
-
export function extractTrustedRunFacts(result) {
|
|
142
|
-
const root = asRecord(result.json);
|
|
143
|
-
if (!root)
|
|
144
|
-
return undefined;
|
|
145
|
-
// Envelope: { schemaVersion: 1, ok, result: { runSummary } }
|
|
146
|
-
if (root.schemaVersion === 1 && typeof root.ok === "boolean") {
|
|
147
|
-
const payload = asRecord(root.result);
|
|
148
|
-
const runSummaryRaw = payload ? asRecord(payload.runSummary) : null;
|
|
149
|
-
if (runSummaryRaw) {
|
|
150
|
-
const summary = safeRunSummaryForProjection(runSummaryRaw);
|
|
151
|
-
if (summary)
|
|
152
|
-
return { runId: summary.runId, runSummary: summary };
|
|
153
|
-
}
|
|
154
|
-
// Rerun standard newRunId inside the envelope result.
|
|
155
|
-
if (payload && typeof payload.newRunId === "string" && payload.newRunId.trim()) {
|
|
156
|
-
const runId = payload.newRunId.trim();
|
|
157
|
-
const inner = asRecord(payload.runSummary);
|
|
158
|
-
const summary = inner ? safeRunSummaryForProjection(inner) : undefined;
|
|
159
|
-
if (summary && summary.runId !== runId)
|
|
160
|
-
return undefined;
|
|
161
|
-
return summary ? { runId, runSummary: summary } : { runId };
|
|
162
|
-
}
|
|
163
|
-
return undefined;
|
|
164
|
-
}
|
|
165
|
-
// Structured CLI JSON with a top-level runSummary object.
|
|
166
|
-
const directSummary = asRecord(root.runSummary);
|
|
167
|
-
if (directSummary) {
|
|
168
|
-
const summary = safeRunSummaryForProjection(directSummary);
|
|
169
|
-
if (summary) {
|
|
170
|
-
// A rerun newRunId must agree with the summary runId.
|
|
171
|
-
if (typeof root.newRunId === "string" &&
|
|
172
|
-
root.newRunId.trim() &&
|
|
173
|
-
root.newRunId.trim() !== summary.runId) {
|
|
174
|
-
return undefined;
|
|
175
|
-
}
|
|
176
|
-
return { runId: summary.runId, runSummary: summary };
|
|
177
|
-
}
|
|
178
|
-
return undefined;
|
|
179
|
-
}
|
|
180
|
-
// Rerun standard newRunId (top level, non-envelope).
|
|
181
|
-
if (typeof root.newRunId === "string" && root.newRunId.trim()) {
|
|
182
|
-
const runId = root.newRunId.trim();
|
|
183
|
-
const inner = asRecord(root.runSummary);
|
|
184
|
-
const summary = inner ? safeRunSummaryForProjection(inner) : undefined;
|
|
185
|
-
if (summary && summary.runId !== runId)
|
|
186
|
-
return undefined;
|
|
187
|
-
return summary ? { runId, runSummary: summary } : { runId };
|
|
188
|
-
}
|
|
189
|
-
return undefined;
|
|
190
|
-
}
|
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
import { projectOperationEventSummary, projectOperationForChat, } from "./chat/chat-event-store.js";
|
|
2
|
-
import { isTerminalOperationState, } from "./operation-store.js";
|
|
3
|
-
/**
|
|
4
|
-
* P2: read-only event-driven long poll on the canonical operation event ring
|
|
5
|
-
* (2026-08-13 Operator Chat long-run supervision). Server contract bounds:
|
|
6
|
-
* the model-facing `maxWaitMs` is clamped to [minWaitMs, maxWaitMsBound];
|
|
7
|
-
* defaults allow the 60–180s model supervision cadence and tests may inject
|
|
8
|
-
* short bounds.
|
|
9
|
-
*/
|
|
10
|
-
export const DEFAULT_OPERATION_WAIT_MIN_MS = 60_000;
|
|
11
|
-
export const DEFAULT_OPERATION_WAIT_MAX_MS = 180_000;
|
|
12
|
-
export class OperationWaitError extends Error {
|
|
13
|
-
code;
|
|
14
|
-
constructor(code, message) {
|
|
15
|
-
super(message);
|
|
16
|
-
this.code = code;
|
|
17
|
-
this.name = "OperationWaitError";
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Pure wake classification: heartbeats are liveness telemetry, not progress.
|
|
22
|
-
* Everything else (state/result/stdout/stderr/error/reconcile) is meaningful.
|
|
23
|
-
*
|
|
24
|
-
* Exception (2026-08-15): stderr/stdout events whose payload is a DAG-runner
|
|
25
|
-
* telemetry log line (`[dag execute] heartbeat ...` / `[task advance] heartbeat
|
|
26
|
-
* ...` / bare `heartbeat ...`) are also liveness, not progress. Without this,
|
|
27
|
-
* a long run's periodic stderr heartbeat logs settle every operationWait
|
|
28
|
-
* immediately (55/55 observed instant returns) and force the model back into
|
|
29
|
-
* high-frequency bash polling — defeating the 60-180s backoff contract.
|
|
30
|
-
*/
|
|
31
|
-
const RUNNER_TELEMETRY_LOG = /^\s*\[?(?:dag execute|task advance|run-dag)\]?\s*heartbeat\b/i;
|
|
32
|
-
const BARE_HEARTBEAT_LOG = /^\s*heartbeat\b/i;
|
|
33
|
-
export function isMeaningfulOperationEvent(event) {
|
|
34
|
-
if (event.kind === "heartbeat")
|
|
35
|
-
return false;
|
|
36
|
-
if (event.kind === "stderr" || event.kind === "stdout") {
|
|
37
|
-
const message = typeof event.message === "string"
|
|
38
|
-
? event.message
|
|
39
|
-
: "";
|
|
40
|
-
// Multi-line chunks: only telemetry-only chunks are classified liveness.
|
|
41
|
-
const lines = message.split("\n").filter((line) => line.trim() !== "");
|
|
42
|
-
if (lines.length > 0 && lines.every((line) => RUNNER_TELEMETRY_LOG.test(line) || BARE_HEARTBEAT_LOG.test(line))) {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
function isFocusedOperation(operation) {
|
|
49
|
-
return (isTerminalOperationState(operation.state) ||
|
|
50
|
-
operation.state === "needs-reconcile");
|
|
51
|
-
}
|
|
52
|
-
function settledSummary(input) {
|
|
53
|
-
const projectedEvents = input.newEvents.length > 0
|
|
54
|
-
? input.newEvents.map(projectOperationEventSummary)
|
|
55
|
-
: [];
|
|
56
|
-
// Cursor safety: nextSeq always covers every observed seq so heartbeats
|
|
57
|
-
// filtered out of the payload are never replayed on the next round.
|
|
58
|
-
const nextSeq = Math.max(input.afterSeq, input.observedSeq);
|
|
59
|
-
return {
|
|
60
|
-
operationId: input.operationId,
|
|
61
|
-
state: input.operation.state,
|
|
62
|
-
changed: input.changed,
|
|
63
|
-
timedOut: input.timedOut,
|
|
64
|
-
events: projectedEvents,
|
|
65
|
-
nextSeq,
|
|
66
|
-
recommendedNextCall: {
|
|
67
|
-
operationId: input.operationId,
|
|
68
|
-
afterSeq: nextSeq,
|
|
69
|
-
wakeOn: "meaningful",
|
|
70
|
-
},
|
|
71
|
-
operation: projectOperationForChat(input.operation),
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Deterministic read-only wait over the canonical operation event stream
|
|
76
|
-
* (AC-003 / AC-004). Completion paths (wakeOn=meaningful default):
|
|
77
|
-
* - existing meaningful events (seq > afterSeq) → immediate (changed: true);
|
|
78
|
-
* - operation terminal/needs-reconcile → immediate flush of meaningful events;
|
|
79
|
-
* - first subscribed meaningful event → immediate settle;
|
|
80
|
-
* - maxWaitMs elapsed without meaningful change → timedOut summary whose
|
|
81
|
-
* nextSeq still covers every observed (heartbeat) seq.
|
|
82
|
-
*
|
|
83
|
-
* wakeOn=all preserves the legacy diagnostic semantics: any first event,
|
|
84
|
-
* heartbeat included, settles and is returned.
|
|
85
|
-
*
|
|
86
|
-
* Race safety: the listener is registered BEFORE listFrom, closing the
|
|
87
|
-
* listFrom/subscribe gap; every path settles exactly once through a guarded
|
|
88
|
-
* `finish`, and listener + timer are always cleaned up on settle.
|
|
89
|
-
*/
|
|
90
|
-
export async function waitForOperationChange(input) {
|
|
91
|
-
const operationId = input.operationId?.trim();
|
|
92
|
-
if (!operationId) {
|
|
93
|
-
throw new OperationWaitError("INVALID_INPUT", "operationId is required");
|
|
94
|
-
}
|
|
95
|
-
if (!Number.isInteger(input.afterSeq) || input.afterSeq < 0) {
|
|
96
|
-
throw new OperationWaitError("INVALID_INPUT", "afterSeq must be a non-negative integer");
|
|
97
|
-
}
|
|
98
|
-
const wakeOn = input.wakeOn ?? "meaningful";
|
|
99
|
-
if (wakeOn !== "meaningful" && wakeOn !== "all") {
|
|
100
|
-
throw new OperationWaitError("INVALID_INPUT", "wakeOn must be 'meaningful' or 'all'");
|
|
101
|
-
}
|
|
102
|
-
const minWaitMs = input.minWaitMs ?? DEFAULT_OPERATION_WAIT_MIN_MS;
|
|
103
|
-
const maxWaitMsBound = input.maxWaitMsBound ?? DEFAULT_OPERATION_WAIT_MAX_MS;
|
|
104
|
-
if (!Number.isFinite(minWaitMs) ||
|
|
105
|
-
!Number.isFinite(maxWaitMsBound) ||
|
|
106
|
-
minWaitMs < 0 ||
|
|
107
|
-
maxWaitMsBound < minWaitMs) {
|
|
108
|
-
throw new OperationWaitError("INVALID_INPUT", "invalid wait bounds");
|
|
109
|
-
}
|
|
110
|
-
const rawMaxWaitMs = input.maxWaitMs ?? maxWaitMsBound;
|
|
111
|
-
if (!Number.isFinite(rawMaxWaitMs) || rawMaxWaitMs < 0) {
|
|
112
|
-
throw new OperationWaitError("INVALID_INPUT", "maxWaitMs must be a non-negative number");
|
|
113
|
-
}
|
|
114
|
-
const maxWaitMs = Math.max(minWaitMs, Math.min(maxWaitMsBound, Math.floor(rawMaxWaitMs)));
|
|
115
|
-
const afterSeq = input.afterSeq;
|
|
116
|
-
/** Wake filter per policy: meaningful drops heartbeat payload entirely. */
|
|
117
|
-
const wakeFilter = (events) => wakeOn === "meaningful"
|
|
118
|
-
? events.filter((event) => isMeaningfulOperationEvent(event))
|
|
119
|
-
: events;
|
|
120
|
-
const operation = await input.operations.get(operationId);
|
|
121
|
-
if (!operation) {
|
|
122
|
-
throw new OperationWaitError("NOT_FOUND", `operation not found: ${operationId}`);
|
|
123
|
-
}
|
|
124
|
-
const events = input.events;
|
|
125
|
-
if (isFocusedOperation(operation)) {
|
|
126
|
-
// Flush unconsumed events before the terminal summary: the caller's
|
|
127
|
-
// cursor must advance past every retained canonical event (observed
|
|
128
|
-
// seq includes heartbeats even when their payload is filtered out).
|
|
129
|
-
const listed = events.listFrom(operationId, afterSeq);
|
|
130
|
-
if ("error" in listed) {
|
|
131
|
-
throw new OperationWaitError("EVENT_CURSOR_EXPIRED", "event cursor expired; re-read operation snapshot");
|
|
132
|
-
}
|
|
133
|
-
const wake = wakeFilter(listed.events);
|
|
134
|
-
const observedSeq = listed.events.length > 0
|
|
135
|
-
? listed.events[listed.events.length - 1].seq
|
|
136
|
-
: afterSeq;
|
|
137
|
-
if (wake.length > 0) {
|
|
138
|
-
return settledSummary({
|
|
139
|
-
operationId,
|
|
140
|
-
operation,
|
|
141
|
-
afterSeq,
|
|
142
|
-
changed: true,
|
|
143
|
-
timedOut: false,
|
|
144
|
-
newEvents: wake,
|
|
145
|
-
observedSeq,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
return settledSummary({
|
|
149
|
-
operationId,
|
|
150
|
-
operation,
|
|
151
|
-
afterSeq,
|
|
152
|
-
changed: false,
|
|
153
|
-
timedOut: false,
|
|
154
|
-
newEvents: [],
|
|
155
|
-
observedSeq,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
return new Promise((resolve, reject) => {
|
|
159
|
-
let settled = false;
|
|
160
|
-
let timer;
|
|
161
|
-
let pollTimer;
|
|
162
|
-
let unsubscribe;
|
|
163
|
-
/** Highest seq observed via listener/listFrom during this wait. */
|
|
164
|
-
let observedSeq = afterSeq;
|
|
165
|
-
const cleanup = () => {
|
|
166
|
-
if (timer !== undefined) {
|
|
167
|
-
clearTimeout(timer);
|
|
168
|
-
timer = undefined;
|
|
169
|
-
}
|
|
170
|
-
if (pollTimer !== undefined) {
|
|
171
|
-
clearInterval(pollTimer);
|
|
172
|
-
pollTimer = undefined;
|
|
173
|
-
}
|
|
174
|
-
if (unsubscribe) {
|
|
175
|
-
unsubscribe();
|
|
176
|
-
unsubscribe = undefined;
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
const finish = (result) => {
|
|
180
|
-
if (settled)
|
|
181
|
-
return;
|
|
182
|
-
settled = true;
|
|
183
|
-
cleanup();
|
|
184
|
-
if (result instanceof OperationWaitError)
|
|
185
|
-
reject(result);
|
|
186
|
-
else
|
|
187
|
-
resolve(result);
|
|
188
|
-
};
|
|
189
|
-
const settleWithEvent = (event) => {
|
|
190
|
-
// Future-cursor guard: events at or below the caller's afterSeq are
|
|
191
|
-
// already consumed and must not settle this wait.
|
|
192
|
-
if (event.seq <= afterSeq)
|
|
193
|
-
return;
|
|
194
|
-
// Heartbeats only advance the observed cursor in meaningful mode.
|
|
195
|
-
if (wakeOn === "meaningful" && !isMeaningfulOperationEvent(event)) {
|
|
196
|
-
observedSeq = Math.max(observedSeq, event.seq);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
observedSeq = Math.max(observedSeq, event.seq);
|
|
200
|
-
// Listener path: async re-read of the operation for a fresh summary.
|
|
201
|
-
void (async () => {
|
|
202
|
-
try {
|
|
203
|
-
const current = await input.operations.get(operationId);
|
|
204
|
-
finish(settledSummary({
|
|
205
|
-
operationId,
|
|
206
|
-
operation: current ?? operation,
|
|
207
|
-
afterSeq,
|
|
208
|
-
changed: true,
|
|
209
|
-
timedOut: false,
|
|
210
|
-
newEvents: [event],
|
|
211
|
-
observedSeq,
|
|
212
|
-
}));
|
|
213
|
-
}
|
|
214
|
-
catch (error) {
|
|
215
|
-
finish(error instanceof OperationWaitError
|
|
216
|
-
? error
|
|
217
|
-
: new OperationWaitError("INVALID_INPUT", error instanceof Error ? error.message : String(error)));
|
|
218
|
-
}
|
|
219
|
-
})();
|
|
220
|
-
};
|
|
221
|
-
// Terminal recheck before subscribing: events landing during this await
|
|
222
|
-
// are still in the ring and are caught by listFrom below.
|
|
223
|
-
void (async () => {
|
|
224
|
-
try {
|
|
225
|
-
const current = await input.operations.get(operationId);
|
|
226
|
-
if (current && isFocusedOperation(current)) {
|
|
227
|
-
// Same terminal flush as the initial path: events landing during
|
|
228
|
-
// the await are still in the retained ring.
|
|
229
|
-
const listed = events.listFrom(operationId, afterSeq);
|
|
230
|
-
if ("error" in listed) {
|
|
231
|
-
finish(new OperationWaitError("EVENT_CURSOR_EXPIRED", "event cursor expired; re-read operation snapshot"));
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
const wake = wakeFilter(listed.events);
|
|
235
|
-
observedSeq = Math.max(observedSeq, listed.events.length > 0
|
|
236
|
-
? listed.events[listed.events.length - 1].seq
|
|
237
|
-
: afterSeq);
|
|
238
|
-
if (wake.length > 0) {
|
|
239
|
-
finish(settledSummary({
|
|
240
|
-
operationId,
|
|
241
|
-
operation: current,
|
|
242
|
-
afterSeq,
|
|
243
|
-
changed: true,
|
|
244
|
-
timedOut: false,
|
|
245
|
-
newEvents: wake,
|
|
246
|
-
observedSeq,
|
|
247
|
-
}));
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
finish(settledSummary({
|
|
251
|
-
operationId,
|
|
252
|
-
operation: current,
|
|
253
|
-
afterSeq,
|
|
254
|
-
changed: false,
|
|
255
|
-
timedOut: false,
|
|
256
|
-
newEvents: [],
|
|
257
|
-
observedSeq,
|
|
258
|
-
}));
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
// Subscribe BEFORE listFrom: any event appended after this point
|
|
262
|
-
// reaches the listener, closing the listFrom/subscribe race.
|
|
263
|
-
unsubscribe = events.subscribe(operationId, settleWithEvent);
|
|
264
|
-
const listed = events.listFrom(operationId, afterSeq);
|
|
265
|
-
if ("error" in listed) {
|
|
266
|
-
finish(new OperationWaitError("EVENT_CURSOR_EXPIRED", "event cursor expired; re-read operation snapshot"));
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
observedSeq = Math.max(observedSeq, listed.events.length > 0
|
|
270
|
-
? listed.events[listed.events.length - 1].seq
|
|
271
|
-
: afterSeq);
|
|
272
|
-
const wake = wakeFilter(listed.events);
|
|
273
|
-
if (wake.length > 0) {
|
|
274
|
-
finish(settledSummary({
|
|
275
|
-
operationId,
|
|
276
|
-
operation: current ?? operation,
|
|
277
|
-
afterSeq,
|
|
278
|
-
changed: true,
|
|
279
|
-
timedOut: false,
|
|
280
|
-
newEvents: wake,
|
|
281
|
-
observedSeq,
|
|
282
|
-
}));
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
// No wake events: arm the bounded wait; the listener settles on
|
|
286
|
-
// the first new wake event, the timer settles on timeout. The
|
|
287
|
-
// timeout summary returns observedSeq so heartbeat-only progress
|
|
288
|
-
// advances the cursor without replaying heartbeat payloads.
|
|
289
|
-
timer = setTimeout(() => {
|
|
290
|
-
void (async () => {
|
|
291
|
-
try {
|
|
292
|
-
const latest = await input.operations.get(operationId);
|
|
293
|
-
finish(settledSummary({
|
|
294
|
-
operationId,
|
|
295
|
-
operation: latest ?? operation,
|
|
296
|
-
afterSeq,
|
|
297
|
-
changed: false,
|
|
298
|
-
timedOut: true,
|
|
299
|
-
newEvents: [],
|
|
300
|
-
observedSeq,
|
|
301
|
-
}));
|
|
302
|
-
}
|
|
303
|
-
catch (error) {
|
|
304
|
-
finish(error instanceof OperationWaitError
|
|
305
|
-
? error
|
|
306
|
-
: new OperationWaitError("INVALID_INPUT", error instanceof Error ? error.message : String(error)));
|
|
307
|
-
}
|
|
308
|
-
})();
|
|
309
|
-
}, maxWaitMs);
|
|
310
|
-
timer.unref?.();
|
|
311
|
-
if (input.poll) {
|
|
312
|
-
pollTimer = setInterval(() => {
|
|
313
|
-
void (async () => {
|
|
314
|
-
if (settled)
|
|
315
|
-
return;
|
|
316
|
-
try {
|
|
317
|
-
const changed = await input.poll?.();
|
|
318
|
-
if (!changed || settled)
|
|
319
|
-
return;
|
|
320
|
-
const relisted = events.listFrom(operationId, afterSeq);
|
|
321
|
-
if ("error" in relisted)
|
|
322
|
-
return;
|
|
323
|
-
const nextWake = wakeFilter(relisted.events);
|
|
324
|
-
observedSeq = Math.max(observedSeq, relisted.events.length > 0
|
|
325
|
-
? relisted.events[relisted.events.length - 1].seq
|
|
326
|
-
: afterSeq);
|
|
327
|
-
if (nextWake.length === 0)
|
|
328
|
-
return;
|
|
329
|
-
const latest = await input.operations.get(operationId);
|
|
330
|
-
finish(settledSummary({
|
|
331
|
-
operationId,
|
|
332
|
-
operation: latest ?? operation,
|
|
333
|
-
afterSeq,
|
|
334
|
-
changed: true,
|
|
335
|
-
timedOut: false,
|
|
336
|
-
newEvents: nextWake,
|
|
337
|
-
observedSeq,
|
|
338
|
-
}));
|
|
339
|
-
}
|
|
340
|
-
catch {
|
|
341
|
-
// Poll is best-effort; the wait still times out.
|
|
342
|
-
}
|
|
343
|
-
})();
|
|
344
|
-
}, 1_000);
|
|
345
|
-
pollTimer.unref?.();
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
catch (error) {
|
|
349
|
-
finish(error instanceof OperationWaitError
|
|
350
|
-
? error
|
|
351
|
-
: new OperationWaitError("INVALID_INPUT", error instanceof Error ? error.message : String(error)));
|
|
352
|
-
}
|
|
353
|
-
})();
|
|
354
|
-
});
|
|
355
|
-
}
|