@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.
Files changed (192) hide show
  1. package/AGENTS.md +2 -6
  2. package/CHANGELOG.md +71 -144
  3. package/README.md +1 -1
  4. package/bin/loop-agent.js +1 -37
  5. package/dist/application/task-lifecycle/advance.js +0 -1
  6. package/dist/application/task-lifecycle/observe.js +0 -15
  7. package/dist/application/task-lifecycle/plan-transitions.js +0 -15
  8. package/dist/cli/command-definitions.js +0 -7
  9. package/dist/cli/program.js +3 -8
  10. package/dist/commands/init-upgrade.js +19 -351
  11. package/dist/commands/init.js +67 -14
  12. package/dist/commands/run-dag-progress.js +0 -14
  13. package/dist/commands/task-advance.js +3 -33
  14. package/dist/executors/dag-pi-executor.js +30 -47
  15. package/dist/executors/pi-sdk-executor.js +0 -34
  16. package/dist/executors/shell-executor.js +95 -6
  17. package/dist/shared/operator/capabilities.js +13 -200
  18. package/dist/shared/package-metadata.js +0 -42
  19. package/dist/task/contract/project.js +0 -4
  20. package/dist/task/source-prepare/completeness.js +0 -53
  21. package/dist/task/source-prepare/parse-intent.js +12 -82
  22. package/dist/task/source-prepare/prepare.js +5 -102
  23. package/dist/task/source-prepare/semantic-intake.js +23 -144
  24. package/dist/worker/console/chat/artifact-card.js +1 -8
  25. package/dist/worker/console/chat/chat-event-store.js +0 -152
  26. package/dist/worker/console/chat/human-gate-card.js +1 -9
  27. package/dist/worker/console/chat/operation-card.js +2 -71
  28. package/dist/worker/console/chat/pi-runtime.js +62 -111
  29. package/dist/worker/console/chat/routes.js +10 -43
  30. package/dist/worker/console/chat/session-store.js +1 -9
  31. package/dist/worker/console/chat/shortcuts.js +7 -9
  32. package/dist/worker/console/chat/turn-process.js +23 -204
  33. package/dist/worker/console/chat/workspace-landing.js +1 -2
  34. package/dist/worker/console/operation-runner.js +6 -155
  35. package/dist/worker/console/operator-actions.js +14 -391
  36. package/dist/worker/console/operator-user-error.js +0 -4
  37. package/dist/worker/console/recovery-cta.js +3 -50
  38. package/dist/worker/console/static/assets/index-HX1pbOyl.css +1 -0
  39. package/dist/worker/console/static/assets/index-M0BLEBfh.js +56 -0
  40. package/dist/worker/console/static/index.html +2 -2
  41. package/dist/worker/console/static-src/app/console-types.js +9 -13
  42. package/dist/worker/console/static-src/app/useOperatorActions.js +2 -4
  43. package/dist/worker/console/static-src/app/useRecoveryActions.js +56 -65
  44. package/dist/worker/console/static-src/app/useRecoveryConsole.js +1 -73
  45. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +30 -114
  46. package/dist/worker/console/static-src/operator-chat/refs.js +0 -3
  47. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  48. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +13 -67
  49. package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -12
  50. package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -45
  51. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -7
  52. package/dist/worker/loop-agent/loop-agent-client.js +3 -17
  53. package/dist/worker/observability/read-model.js +0 -20
  54. package/dist/worker/observe/spec-evidence.js +8 -3
  55. package/dist/worker/observe/static/operator-chrome.d.ts +0 -1
  56. package/dist/worker/observe/static/operator-chrome.js +3 -10
  57. package/dist/worker/observe/static/views/dag-inspector.js +71 -6
  58. package/dist/worker/observe/static/views/dag.js +0 -12
  59. package/dist/worker/preflight.js +1 -2
  60. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +707 -37
  61. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  62. package/dist/workflows/dag/backend-test-markdown-workflow.js +109 -7
  63. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  64. package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
  65. package/dist/workflows/dag/backend-test-scenario-param.js +890 -164
  66. package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
  67. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  68. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
  69. package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
  70. package/dist/workflows/dag/frontend-prewrite-gate.js +61 -364
  71. package/dist/workflows/dag/frontend-repair.js +18 -219
  72. package/dist/workflows/dag/frontend-verification-trace.js +32 -47
  73. package/dist/workflows/dag/init-hybrid.js +82 -165
  74. package/dist/workflows/dag/node-execution.js +0 -89
  75. package/dist/workflows/dag/recovery-recommendation.js +0 -58
  76. package/dist/workflows/dag/runner.js +15 -333
  77. package/dist/workflows/dag/scheduler.js +3 -257
  78. package/dist/workflows/dag/types.js +9 -130
  79. package/docs/README.md +3 -3
  80. package/docs/architecture/evolution.md +67 -102
  81. package/docs/templates/backend-test-dag.json +50 -32
  82. package/docs/templates/init-managed-agents.md +2 -5
  83. package/harness.json +2 -2
  84. package/package.json +3 -9
  85. package/skills/loop-agent/SKILL.md +0 -1
  86. package/skills/loop-agent/references/command-reference.md +0 -4
  87. package/dist/build-stamp.json +0 -6
  88. package/dist/commands/dag-request-interrupt.js +0 -20
  89. package/dist/worker/console/chat/assistant-content.js +0 -121
  90. package/dist/worker/console/chat/semantic-activity.js +0 -477
  91. package/dist/worker/console/operation-run-facts.js +0 -190
  92. package/dist/worker/console/operation-wait.js +0 -355
  93. package/dist/worker/console/recovery-error-copy.js +0 -198
  94. package/dist/worker/console/static/assets/abnfDiagram-N423BO3Z-DSvF6RXQ.js +0 -1
  95. package/dist/worker/console/static/assets/arc-DuI4ogtJ.js +0 -1
  96. package/dist/worker/console/static/assets/architectureDiagram-T3A2C74G-DdBbgD4y.js +0 -36
  97. package/dist/worker/console/static/assets/blockDiagram-VBNYF7ZC-xQC2HELm.js +0 -132
  98. package/dist/worker/console/static/assets/c4Diagram-5PPSVZJV-DPMSgwIZ.js +0 -10
  99. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  100. package/dist/worker/console/static/assets/chunk-2GRJ4B5K-WTEKTxin.js +0 -1
  101. package/dist/worker/console/static/assets/chunk-2Q5K7J3B-B6vZ0Pv_.js +0 -1
  102. package/dist/worker/console/static/assets/chunk-5RXB4S5H-BrerNAix.js +0 -231
  103. package/dist/worker/console/static/assets/chunk-5VM5RSS4-D_ch2aaI.js +0 -15
  104. package/dist/worker/console/static/assets/chunk-6Q2QTUOP-VvfacNrq.js +0 -88
  105. package/dist/worker/console/static/assets/chunk-GF5L2VYU-B-uGFIg2.js +0 -206
  106. package/dist/worker/console/static/assets/chunk-JWPE2WC7-BlysCtod.js +0 -1
  107. package/dist/worker/console/static/assets/chunk-KBJHAD2P-BjAUAfcT.js +0 -1
  108. package/dist/worker/console/static/assets/chunk-RYQCIY6F-BeZnBmb-.js +0 -1
  109. package/dist/worker/console/static/assets/chunk-XXDRQBXY-Bbfe3UJG.js +0 -1
  110. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  111. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  112. package/dist/worker/console/static/assets/cose-bilkent-JH36ORCC-BKJQnYVM.js +0 -1
  113. package/dist/worker/console/static/assets/cynefin-VYW2F7L2-B1wtI3hU.js +0 -166
  114. package/dist/worker/console/static/assets/cynefinDiagram-MW4NZA55-DCF6DBSu.js +0 -62
  115. package/dist/worker/console/static/assets/cytoscape.esm-yzknjiTM.js +0 -321
  116. package/dist/worker/console/static/assets/dagre-VZM6K2ZE-BJyNGRlP.js +0 -4
  117. package/dist/worker/console/static/assets/defaultLocale-DX6XiGOO.js +0 -1
  118. package/dist/worker/console/static/assets/diagram-7IWD3JNH-B7j1-VsZ.js +0 -30
  119. package/dist/worker/console/static/assets/diagram-B4RE2ZJO-BREJ140a.js +0 -3
  120. package/dist/worker/console/static/assets/diagram-LBJQPF4R-CDPHmHGH.js +0 -24
  121. package/dist/worker/console/static/assets/diagram-Q27KOJAE-BtZLrez1.js +0 -24
  122. package/dist/worker/console/static/assets/diagram-UB23O5K3-mzybu8oU.js +0 -41
  123. package/dist/worker/console/static/assets/ebnfDiagram-BXEA7PRR-Dk3I2uO6.js +0 -1
  124. package/dist/worker/console/static/assets/erDiagram-JOGREHBK-BT97u_tQ.js +0 -85
  125. package/dist/worker/console/static/assets/flowDiagram-UKHOOZJN-CMOtYzui.js +0 -156
  126. package/dist/worker/console/static/assets/ganttDiagram-PKOTCBZU-DiNydkuY.js +0 -292
  127. package/dist/worker/console/static/assets/gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js +0 -106
  128. package/dist/worker/console/static/assets/graph-DOmOIIwC.js +0 -1
  129. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  130. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  131. package/dist/worker/console/static/assets/infoDiagram-6WML65LV-AsGIGKKB.js +0 -2
  132. package/dist/worker/console/static/assets/init-Gi6I4Gst.js +0 -1
  133. package/dist/worker/console/static/assets/ishikawaDiagram-WSZJBQD7-BL8ITZh3.js +0 -70
  134. package/dist/worker/console/static/assets/journeyDiagram-NVQOT4AX-VPKAUX8M.js +0 -139
  135. package/dist/worker/console/static/assets/kanban-definition-27J2QSJJ-BL56hfGp.js +0 -89
  136. package/dist/worker/console/static/assets/layout-D-LzfAck.js +0 -1
  137. package/dist/worker/console/static/assets/linear-BmSG3nIZ.js +0 -1
  138. package/dist/worker/console/static/assets/map-DxJ2ADlA.js +0 -1
  139. package/dist/worker/console/static/assets/mermaid.core-DWGK9kwP.js +0 -308
  140. package/dist/worker/console/static/assets/mindmap-definition-FAOFIHXS--vvKLjo0.js +0 -96
  141. package/dist/worker/console/static/assets/ordinal-Cboi1Yqb.js +0 -1
  142. package/dist/worker/console/static/assets/pegDiagram-VL7TDLO6-B41mxb2-.js +0 -1
  143. package/dist/worker/console/static/assets/pieDiagram-7S7Q4E2Y-DTlu77sw.js +0 -39
  144. package/dist/worker/console/static/assets/quadrantDiagram-CIZ2JOQS-Bk5A7alv.js +0 -7
  145. package/dist/worker/console/static/assets/railroadDiagram-AXF67PYL-Csf9jzQy.js +0 -1
  146. package/dist/worker/console/static/assets/requirementDiagram-LRYGKXZP-CeptoSNx.js +0 -84
  147. package/dist/worker/console/static/assets/sankeyDiagram-W5VNT64P-A-DLCLer.js +0 -40
  148. package/dist/worker/console/static/assets/sequenceDiagram-SI44F4Z6-22VbPdOI.js +0 -162
  149. package/dist/worker/console/static/assets/sizeCapture-X5ZJPWSS-DWvAyBC9.js +0 -1
  150. package/dist/worker/console/static/assets/stateDiagram-OKZ733FA-qICZ75yT.js +0 -1
  151. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  152. package/dist/worker/console/static/assets/swimlanes-SLNWSIFB-xi7lcMyp.js +0 -2
  153. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
  154. package/dist/worker/console/static/assets/timeline-definition-Z64GVDOM-Bu6Nl6O2.js +0 -120
  155. package/dist/worker/console/static/assets/vennDiagram-T6HMQDX7-BzMz4Bn9.js +0 -34
  156. package/dist/worker/console/static/assets/wardleyDiagram-T6FBY63Y-CxaJnzc5.js +0 -78
  157. package/dist/worker/console/static/assets/xychartDiagram-ELKLHX3M-5JK-omQK.js +0 -7
  158. package/dist/worker/console/static/fonts/katex/KaTeX_AMS-Regular.woff2 +0 -0
  159. package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Bold.woff2 +0 -0
  160. package/dist/worker/console/static/fonts/katex/KaTeX_Caligraphic-Regular.woff2 +0 -0
  161. package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Bold.woff2 +0 -0
  162. package/dist/worker/console/static/fonts/katex/KaTeX_Fraktur-Regular.woff2 +0 -0
  163. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Bold.woff2 +0 -0
  164. package/dist/worker/console/static/fonts/katex/KaTeX_Main-BoldItalic.woff2 +0 -0
  165. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Italic.woff2 +0 -0
  166. package/dist/worker/console/static/fonts/katex/KaTeX_Main-Regular.woff2 +0 -0
  167. package/dist/worker/console/static/fonts/katex/KaTeX_Math-BoldItalic.woff2 +0 -0
  168. package/dist/worker/console/static/fonts/katex/KaTeX_Math-Italic.woff2 +0 -0
  169. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Bold.woff2 +0 -0
  170. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Italic.woff2 +0 -0
  171. package/dist/worker/console/static/fonts/katex/KaTeX_SansSerif-Regular.woff2 +0 -0
  172. package/dist/worker/console/static/fonts/katex/KaTeX_Script-Regular.woff2 +0 -0
  173. package/dist/worker/console/static/fonts/katex/KaTeX_Size1-Regular.woff2 +0 -0
  174. package/dist/worker/console/static/fonts/katex/KaTeX_Size2-Regular.woff2 +0 -0
  175. package/dist/worker/console/static/fonts/katex/KaTeX_Size3-Regular.woff2 +0 -0
  176. package/dist/worker/console/static/fonts/katex/KaTeX_Size4-Regular.woff2 +0 -0
  177. package/dist/worker/console/static/fonts/katex/KaTeX_Typewriter-Regular.woff2 +0 -0
  178. package/dist/worker/console/static-src/operator-chat/activity-journey.js +0 -125
  179. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  180. package/dist/worker/console/static-src/operator-chat/activity-references.js +0 -20
  181. package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +0 -24
  182. package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +0 -141
  183. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
  184. package/dist/worker/observability/interrupt-eligibility.js +0 -264
  185. package/dist/workflows/dag/contract-output-registry.js +0 -14
  186. package/dist/workflows/dag/contract-validator-registrations.js +0 -8
  187. package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
  188. package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
  189. package/dist/workflows/dag/frontend-recovery-run.js +0 -539
  190. package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
  191. package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
  192. 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
- }