@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,559 +0,0 @@
1
- import { watch } from "node:fs";
2
- import { mkdir, readFile } from "node:fs/promises";
3
- import { hostname as localHostname } from "node:os";
4
- import path from "node:path";
5
- import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
6
- import { assessDagRunLiveness, getDagRunDir, locateDagRun, readDagRunState, } from "./lifecycle.js";
7
- export const INTERRUPT_ARTIFACT_REL = path.posix.join(".runtime", "interrupt.json");
8
- export const INTERRUPT_REASON_CODES = [
9
- "requirements-changed",
10
- "suspected-runaway",
11
- "resource-protection",
12
- "operator-request",
13
- "other",
14
- ];
15
- export const INTERRUPT_ERROR_CODES = [
16
- "RUN_NOT_ACTIVE",
17
- "RUN_NOT_CONSOLE_OWNED",
18
- "RUNNER_IDENTITY_MISMATCH",
19
- "INTERRUPT_ALREADY_PENDING",
20
- "RUN_ALREADY_TERMINAL",
21
- "INTERRUPT_NEEDS_RECONCILE",
22
- "INVALID_INPUT",
23
- ];
24
- export const INTERRUPT_ERROR_MESSAGES = {
25
- RUN_NOT_ACTIVE: "这次运行不在本机活动目录,或已经不是 running,不能协作中止。",
26
- RUN_NOT_CONSOLE_OWNED: "此运行不由当前 Console 持有,暂不支持协作中止。请用 Doctor / 报告查看。",
27
- RUNNER_IDENTITY_MISMATCH: "运行身份已变化,不能中止这份过期请求。请刷新资格后再试。",
28
- INTERRUPT_ALREADY_PENDING: "已有一条中止请求等待结算。请等待当前节点安全收敛,或用 Doctor 对账。",
29
- RUN_ALREADY_TERMINAL: "运行已经结束,没有可中止的活 runner。",
30
- INTERRUPT_NEEDS_RECONCILE: "中止请求已登记,但无法确认 runner 已处理。请用 Doctor 对账,不要当成已经中止。",
31
- INVALID_INPUT: "中止请求参数不完整或格式不正确。",
32
- };
33
- export class DagInterruptError extends Error {
34
- code;
35
- constructor(code, message) {
36
- super(message ?? INTERRUPT_ERROR_MESSAGES[code]);
37
- this.name = "DagInterruptError";
38
- this.code = code;
39
- }
40
- }
41
- export function interruptArtifactPath(runDir) {
42
- return path.join(runDir, ".runtime", "interrupt.json");
43
- }
44
- export function isInterruptReasonCode(value) {
45
- return INTERRUPT_REASON_CODES.includes(value);
46
- }
47
- export function normalizeInterruptReasonDetail(raw) {
48
- const detail = raw.trim();
49
- if (detail.length < 10 || detail.length > 500) {
50
- throw new DagInterruptError("INVALID_INPUT", "reasonDetail 去首尾空白后必须是 10–500 个字符,且不能是无意义占位。");
51
- }
52
- return detail;
53
- }
54
- export function runnerIdentityFromState(state) {
55
- const runner = state.runner;
56
- const fingerprint = state.controllerIdentityRef?.packageFingerprint?.trim();
57
- if (!runner || !fingerprint)
58
- return undefined;
59
- if (!Number.isInteger(runner.pid) ||
60
- runner.pid <= 0 ||
61
- !runner.hostname?.trim() ||
62
- !runner.startedAt?.trim()) {
63
- return undefined;
64
- }
65
- return {
66
- pid: runner.pid,
67
- hostname: runner.hostname,
68
- startedAt: runner.startedAt,
69
- controllerFingerprint: fingerprint,
70
- };
71
- }
72
- export function runnerIdentitiesMatch(left, right) {
73
- return (left.pid === right.pid &&
74
- left.hostname === right.hostname &&
75
- left.startedAt === right.startedAt &&
76
- left.controllerFingerprint === right.controllerFingerprint);
77
- }
78
- export function parseInterruptRequest(raw) {
79
- if (!raw || typeof raw !== "object") {
80
- throw new DagInterruptError("INVALID_INPUT", "interrupt.json 不是对象");
81
- }
82
- const rec = raw;
83
- if (rec.schemaVersion !== 1) {
84
- throw new DagInterruptError("INVALID_INPUT", "interrupt.json schemaVersion 必须为 1");
85
- }
86
- const identity = rec.expectedRunnerIdentity;
87
- if (!identity || typeof identity !== "object") {
88
- throw new DagInterruptError("INVALID_INPUT", "interrupt.json 缺少 expectedRunnerIdentity");
89
- }
90
- const ident = identity;
91
- const reasonCode = String(rec.reasonCode ?? "");
92
- if (!isInterruptReasonCode(reasonCode)) {
93
- throw new DagInterruptError("INVALID_INPUT", "interrupt.json reasonCode 非法");
94
- }
95
- const status = String(rec.status ?? "");
96
- if (status !== "requested" &&
97
- status !== "acknowledged" &&
98
- status !== "settled" &&
99
- status !== "needs-reconcile") {
100
- throw new DagInterruptError("INVALID_INPUT", "interrupt.json status 非法");
101
- }
102
- const requestedBy = rec.requestedBy;
103
- if (!requestedBy || typeof requestedBy !== "object") {
104
- throw new DagInterruptError("INVALID_INPUT", "interrupt.json 缺少 requestedBy");
105
- }
106
- const by = requestedBy;
107
- const source = by.source === "cli" ? "cli" : "operator-console";
108
- const pid = Number(ident.pid);
109
- if (!Number.isInteger(pid) || pid <= 0) {
110
- throw new DagInterruptError("INVALID_INPUT", "interrupt.json expectedRunnerIdentity.pid 非法");
111
- }
112
- return {
113
- schemaVersion: 1,
114
- revision: Number(rec.revision) || 1,
115
- requestId: String(rec.requestId ?? ""),
116
- runId: String(rec.runId ?? ""),
117
- targetOperationId: String(rec.targetOperationId ?? ""),
118
- reasonCode,
119
- reasonDetail: String(rec.reasonDetail ?? ""),
120
- requestedAt: String(rec.requestedAt ?? ""),
121
- requestedBy: {
122
- source,
123
- ...(typeof by.operatorSessionId === "string" && by.operatorSessionId.trim()
124
- ? { operatorSessionId: by.operatorSessionId.trim() }
125
- : {}),
126
- },
127
- expectedRunnerIdentity: {
128
- pid,
129
- hostname: String(ident.hostname ?? ""),
130
- startedAt: String(ident.startedAt ?? ""),
131
- controllerFingerprint: String(ident.controllerFingerprint ?? ""),
132
- },
133
- status,
134
- ...(typeof rec.acknowledgedAt === "string"
135
- ? { acknowledgedAt: rec.acknowledgedAt }
136
- : {}),
137
- ...(typeof rec.settledAt === "string" ? { settledAt: rec.settledAt } : {}),
138
- ...(typeof rec.outcome === "string" ? { outcome: rec.outcome } : {}),
139
- };
140
- }
141
- export async function readInterruptRequest(runDir) {
142
- try {
143
- const raw = JSON.parse(await readFile(interruptArtifactPath(runDir), "utf-8"));
144
- return parseInterruptRequest(raw);
145
- }
146
- catch (error) {
147
- if (error &&
148
- typeof error === "object" &&
149
- "code" in error &&
150
- error.code === "ENOENT") {
151
- return undefined;
152
- }
153
- if (error instanceof DagInterruptError)
154
- throw error;
155
- throw new DagInterruptError("INVALID_INPUT", `无法读取 interrupt.json: ${error instanceof Error ? error.message : String(error)}`);
156
- }
157
- }
158
- async function writeInterruptRequest(runDir, request) {
159
- await mkdir(path.join(runDir, ".runtime"), { recursive: true });
160
- await writeJsonAtomic(interruptArtifactPath(runDir), request);
161
- }
162
- function requestPayloadKey(request) {
163
- const ident = request.expectedRunnerIdentity;
164
- return [
165
- request.runId,
166
- request.targetOperationId,
167
- request.reasonCode,
168
- request.reasonDetail,
169
- ident.pid,
170
- ident.hostname,
171
- ident.startedAt,
172
- ident.controllerFingerprint,
173
- ].join("\0");
174
- }
175
- function assertKernelRunEligible(input) {
176
- if (input.lifecycle !== "active") {
177
- if (input.lifecycle === "completed" ||
178
- input.lifecycle === "paused" ||
179
- isTerminalRunStatus(input.state.status)) {
180
- throw new DagInterruptError("RUN_ALREADY_TERMINAL");
181
- }
182
- throw new DagInterruptError("RUN_NOT_ACTIVE");
183
- }
184
- if (input.state.status !== "running") {
185
- if (isTerminalRunStatus(input.state.status) || input.state.status === "paused") {
186
- throw new DagInterruptError("RUN_ALREADY_TERMINAL");
187
- }
188
- throw new DagInterruptError("RUN_NOT_ACTIVE");
189
- }
190
- if (input.state.workerAssociation) {
191
- throw new DagInterruptError("RUN_NOT_CONSOLE_OWNED");
192
- }
193
- const live = runnerIdentityFromState(input.state);
194
- if (!live || !runnerIdentitiesMatch(live, input.expected)) {
195
- throw new DagInterruptError("RUNNER_IDENTITY_MISMATCH");
196
- }
197
- const liveness = assessDagRunLiveness({ state: input.state });
198
- if (liveness.status === "orphaned" || liveness.status === "unknown-host") {
199
- throw new DagInterruptError("RUN_NOT_ACTIVE");
200
- }
201
- }
202
- function isTerminalRunStatus(status) {
203
- return (status === "finished" ||
204
- status === "failed" ||
205
- status === "partial_failed" ||
206
- status === "superseded" ||
207
- status === "abandoned");
208
- }
209
- export async function requestDagInterrupt(input) {
210
- const requestId = input.requestId.trim();
211
- const targetOperationId = input.targetOperationId.trim();
212
- const runId = input.runId.trim();
213
- if (!requestId || !targetOperationId || !runId) {
214
- throw new DagInterruptError("INVALID_INPUT");
215
- }
216
- const reasonDetail = normalizeInterruptReasonDetail(input.reasonDetail);
217
- const located = await locateDagRun(input.cwd, runId);
218
- if (!located) {
219
- throw new DagInterruptError("RUN_NOT_ACTIVE");
220
- }
221
- const state = await readDagRunState(located.runDir);
222
- if (state.runId !== runId) {
223
- throw new DagInterruptError("RUNNER_IDENTITY_MISMATCH");
224
- }
225
- assertKernelRunEligible({
226
- lifecycle: located.lifecycle,
227
- state,
228
- expected: input.expectedRunnerIdentity,
229
- });
230
- const existing = await readInterruptRequest(located.runDir);
231
- const next = {
232
- schemaVersion: 1,
233
- revision: (existing?.revision ?? 0) + 1,
234
- requestId,
235
- runId,
236
- targetOperationId,
237
- reasonCode: input.reasonCode,
238
- reasonDetail,
239
- requestedAt: (input.now ?? new Date()).toISOString(),
240
- requestedBy: input.requestedBy ?? { source: "cli" },
241
- expectedRunnerIdentity: input.expectedRunnerIdentity,
242
- status: "requested",
243
- };
244
- if (existing) {
245
- if (existing.status === "requested" ||
246
- existing.status === "acknowledged") {
247
- if (existing.requestId === requestId &&
248
- requestPayloadKey(existing) === requestPayloadKey(next)) {
249
- return {
250
- ok: true,
251
- interruptStatus: "requested",
252
- request: existing,
253
- runDir: located.runDir,
254
- lifecycle: located.lifecycle,
255
- };
256
- }
257
- if (existing.requestId === requestId) {
258
- throw new DagInterruptError("INVALID_INPUT", "同一 requestId 不能绑定不同的中止内容。");
259
- }
260
- throw new DagInterruptError("INTERRUPT_ALREADY_PENDING");
261
- }
262
- if (existing.status === "needs-reconcile") {
263
- throw new DagInterruptError("INTERRUPT_NEEDS_RECONCILE");
264
- }
265
- }
266
- await writeInterruptRequest(located.runDir, next);
267
- const confirmed = await readInterruptRequest(located.runDir);
268
- if (!confirmed ||
269
- confirmed.requestId !== requestId ||
270
- requestPayloadKey(confirmed) !== requestPayloadKey(next)) {
271
- throw new DagInterruptError("INTERRUPT_ALREADY_PENDING");
272
- }
273
- return {
274
- ok: true,
275
- interruptStatus: "requested",
276
- request: confirmed,
277
- runDir: located.runDir,
278
- lifecycle: located.lifecycle,
279
- };
280
- }
281
- export async function acknowledgeDagInterrupt(input) {
282
- const existing = await readInterruptRequest(input.runDir);
283
- if (!existing)
284
- return undefined;
285
- if (existing.runId !== input.runId) {
286
- throw new DagInterruptError("RUNNER_IDENTITY_MISMATCH");
287
- }
288
- if (existing.status === "settled" || existing.status === "needs-reconcile") {
289
- return existing;
290
- }
291
- if (!runnerIdentitiesMatch(existing.expectedRunnerIdentity, input.expectedRunnerIdentity)) {
292
- throw new DagInterruptError("RUNNER_IDENTITY_MISMATCH");
293
- }
294
- if (existing.status === "acknowledged")
295
- return existing;
296
- const next = {
297
- ...existing,
298
- revision: existing.revision + 1,
299
- status: "acknowledged",
300
- acknowledgedAt: (input.now ?? new Date()).toISOString(),
301
- };
302
- await writeInterruptRequest(input.runDir, next);
303
- return next;
304
- }
305
- export async function settleDagInterrupt(input) {
306
- const existing = await readInterruptRequest(input.runDir);
307
- if (!existing)
308
- return undefined;
309
- if (existing.status === "settled")
310
- return existing;
311
- if (existing.status === "requested") {
312
- const next = {
313
- ...existing,
314
- revision: existing.revision + 1,
315
- status: "needs-reconcile",
316
- outcome: "runner-did-not-acknowledge",
317
- };
318
- await writeInterruptRequest(input.runDir, next);
319
- return next;
320
- }
321
- const next = {
322
- ...existing,
323
- revision: existing.revision + 1,
324
- status: "settled",
325
- settledAt: (input.now ?? new Date()).toISOString(),
326
- outcome: input.outcome ?? "controller-interrupted",
327
- };
328
- await writeInterruptRequest(input.runDir, next);
329
- return next;
330
- }
331
- export async function markInterruptNeedsReconcile(input) {
332
- const existing = await readInterruptRequest(input.runDir);
333
- if (!existing)
334
- return undefined;
335
- if (existing.status === "settled")
336
- return existing;
337
- const next = {
338
- ...existing,
339
- revision: existing.revision + 1,
340
- status: "needs-reconcile",
341
- outcome: input.outcome,
342
- };
343
- await writeInterruptRequest(input.runDir, next);
344
- return next;
345
- }
346
- export function projectedInterruptStatus(request) {
347
- return request?.status ?? "none";
348
- }
349
- export function mergeAbortSignals(left, right) {
350
- if (!left)
351
- return right;
352
- if (!right)
353
- return left;
354
- const anyFn = AbortSignal.any;
355
- if (typeof anyFn === "function")
356
- return anyFn([left, right]);
357
- const merged = new AbortController();
358
- const onAbort = () => {
359
- if (!merged.signal.aborted)
360
- merged.abort();
361
- };
362
- if (left.aborted || right.aborted) {
363
- merged.abort();
364
- return merged.signal;
365
- }
366
- left.addEventListener("abort", onAbort, { once: true });
367
- right.addEventListener("abort", onAbort, { once: true });
368
- return merged.signal;
369
- }
370
- export function startInterruptWatcher(input) {
371
- let stopped = false;
372
- const tick = () => {
373
- if (stopped || input.controller.signal.aborted)
374
- return;
375
- void (async () => {
376
- try {
377
- const existing = await readInterruptRequest(input.runDir);
378
- if (!existing || existing.runId !== input.runId)
379
- return;
380
- if (existing.status !== "requested" && existing.status !== "acknowledged") {
381
- return;
382
- }
383
- const acknowledged = await acknowledgeDagInterrupt({
384
- runDir: input.runDir,
385
- runId: input.runId,
386
- expectedRunnerIdentity: input.expectedRunnerIdentity,
387
- });
388
- if (acknowledged &&
389
- (acknowledged.status === "acknowledged" ||
390
- acknowledged.status === "settled") &&
391
- !input.controller.signal.aborted) {
392
- input.controller.abort(acknowledged.reasonCode);
393
- }
394
- }
395
- catch {
396
- // Identity drift or corrupt artifact: do not abort. Leave requested
397
- // for doctor / needs-reconcile at terminal.
398
- }
399
- })();
400
- };
401
- void mkdir(path.join(input.runDir, ".runtime"), { recursive: true }).then(() => {
402
- if (!stopped)
403
- tick();
404
- }, () => {
405
- if (!stopped)
406
- tick();
407
- });
408
- const timer = setInterval(tick, 400);
409
- timer.unref();
410
- let watcher;
411
- try {
412
- watcher = watch(path.join(input.runDir, ".runtime"), () => tick());
413
- watcher.unref?.();
414
- }
415
- catch {
416
- // Polling is the guaranteed path; fs.watch is best-effort.
417
- }
418
- return {
419
- stop: () => {
420
- stopped = true;
421
- clearInterval(timer);
422
- watcher?.close();
423
- },
424
- };
425
- }
426
- function readFlag(args, index, name) {
427
- const arg = args[index.value];
428
- if (arg === `--${name}`) {
429
- index.value += 1;
430
- return args[index.value];
431
- }
432
- if (arg?.startsWith(`--${name}=`)) {
433
- return arg.slice(`--${name}=`.length);
434
- }
435
- return undefined;
436
- }
437
- export function parseDagRequestInterruptArgs(args) {
438
- let runId;
439
- let requestId;
440
- let targetOperationId;
441
- let reasonCode;
442
- let reason;
443
- let pid;
444
- let hostname;
445
- let startedAt;
446
- let fingerprint;
447
- for (let i = 0; i < args.length; i += 1) {
448
- const cursor = { value: i };
449
- const arg = args[i];
450
- const runIdValue = readFlag(args, cursor, "run-id");
451
- if (runIdValue !== undefined) {
452
- runId = runIdValue;
453
- i = cursor.value;
454
- continue;
455
- }
456
- const requestIdValue = readFlag(args, cursor, "request-id");
457
- if (requestIdValue !== undefined) {
458
- requestId = requestIdValue;
459
- i = cursor.value;
460
- continue;
461
- }
462
- const opValue = readFlag(args, cursor, "target-operation-id");
463
- if (opValue !== undefined) {
464
- targetOperationId = opValue;
465
- i = cursor.value;
466
- continue;
467
- }
468
- const reasonCodeValue = readFlag(args, cursor, "reason-code");
469
- if (reasonCodeValue !== undefined) {
470
- reasonCode = reasonCodeValue;
471
- i = cursor.value;
472
- continue;
473
- }
474
- const reasonValue = readFlag(args, cursor, "reason");
475
- if (reasonValue !== undefined) {
476
- reason = reasonValue;
477
- i = cursor.value;
478
- continue;
479
- }
480
- const pidValue = readFlag(args, cursor, "expected-runner-pid");
481
- if (pidValue !== undefined) {
482
- pid = pidValue;
483
- i = cursor.value;
484
- continue;
485
- }
486
- const hostValue = readFlag(args, cursor, "expected-runner-hostname");
487
- if (hostValue !== undefined) {
488
- hostname = hostValue;
489
- i = cursor.value;
490
- continue;
491
- }
492
- const startedValue = readFlag(args, cursor, "expected-runner-started-at");
493
- if (startedValue !== undefined) {
494
- startedAt = startedValue;
495
- i = cursor.value;
496
- continue;
497
- }
498
- const fpValue = readFlag(args, cursor, "expected-controller-fingerprint");
499
- if (fpValue !== undefined) {
500
- fingerprint = fpValue;
501
- i = cursor.value;
502
- continue;
503
- }
504
- if (arg === "--json")
505
- continue;
506
- if (arg?.startsWith("-")) {
507
- throw new DagInterruptError("INVALID_INPUT", `unknown dag request-interrupt flag: ${arg}`);
508
- }
509
- throw new DagInterruptError("INVALID_INPUT", `unexpected positional argument: ${arg}`);
510
- }
511
- const parsedPid = Number(pid);
512
- if (!runId?.trim() ||
513
- !requestId?.trim() ||
514
- !targetOperationId?.trim() ||
515
- !reasonCode ||
516
- !reason ||
517
- !hostname?.trim() ||
518
- !startedAt?.trim() ||
519
- !fingerprint?.trim() ||
520
- !Number.isInteger(parsedPid) ||
521
- parsedPid <= 0) {
522
- throw new DagInterruptError("INVALID_INPUT", "dag request-interrupt requires --run-id, --request-id, --target-operation-id, --reason-code, --reason, --expected-runner-pid, --expected-runner-hostname, --expected-runner-started-at, --expected-controller-fingerprint");
523
- }
524
- if (!isInterruptReasonCode(reasonCode)) {
525
- throw new DagInterruptError("INVALID_INPUT", `reason-code must be one of ${INTERRUPT_REASON_CODES.join("|")}`);
526
- }
527
- return {
528
- runId: runId.trim(),
529
- requestId: requestId.trim(),
530
- targetOperationId: targetOperationId.trim(),
531
- reasonCode,
532
- reason,
533
- expectedRunnerIdentity: {
534
- pid: parsedPid,
535
- hostname: hostname.trim(),
536
- startedAt: startedAt.trim(),
537
- controllerFingerprint: fingerprint.trim(),
538
- },
539
- };
540
- }
541
- export async function executeDagRequestInterrupt(repoRoot, rawArgs) {
542
- const parsed = parseDagRequestInterruptArgs(rawArgs);
543
- return requestDagInterrupt({
544
- cwd: repoRoot,
545
- runId: parsed.runId,
546
- requestId: parsed.requestId,
547
- targetOperationId: parsed.targetOperationId,
548
- reasonCode: parsed.reasonCode,
549
- reasonDetail: parsed.reason,
550
- expectedRunnerIdentity: parsed.expectedRunnerIdentity,
551
- requestedBy: { source: "cli" },
552
- });
553
- }
554
- export function defaultLocalHostname() {
555
- return localHostname();
556
- }
557
- export function getActiveRunDir(cwd, runId) {
558
- return getDagRunDir(cwd, "active", runId);
559
- }