@tea-agent/loop-agent 0.36.4-beta.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 (118) hide show
  1. package/AGENTS.md +2 -6
  2. package/CHANGELOG.md +72 -163
  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/infrastructure/harness/atomic-write.js +1 -2
  18. package/dist/shared/operator/capabilities.js +13 -200
  19. package/dist/shared/package-metadata.js +0 -42
  20. package/dist/task/frontend-project-capability.js +12 -162
  21. package/dist/task/source-prepare/completeness.js +0 -17
  22. package/dist/task/source-prepare/parse-intent.js +1 -15
  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 -61
  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 -86
  29. package/dist/worker/console/chat/routes.js +8 -36
  30. package/dist/worker/console/chat/session-store.js +0 -3
  31. package/dist/worker/console/chat/shortcuts.js +7 -9
  32. package/dist/worker/console/chat/turn-process.js +23 -97
  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 +13 -373
  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 -89
  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 +11 -30
  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 +1 -6
  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/failure-routing.js +4 -9
  70. package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
  71. package/dist/workflows/dag/frontend-prewrite-gate.js +62 -394
  72. package/dist/workflows/dag/frontend-repair.js +18 -219
  73. package/dist/workflows/dag/frontend-verification-trace.js +32 -47
  74. package/dist/workflows/dag/init-hybrid.js +86 -238
  75. package/dist/workflows/dag/lifecycle.js +0 -4
  76. package/dist/workflows/dag/node-execution.js +6 -105
  77. package/dist/workflows/dag/recovery-recommendation.js +0 -58
  78. package/dist/workflows/dag/report.js +0 -6
  79. package/dist/workflows/dag/retry-policy.js +0 -11
  80. package/dist/workflows/dag/runner.js +12 -314
  81. package/dist/workflows/dag/scheduler.js +3 -257
  82. package/dist/workflows/dag/types.js +9 -132
  83. package/dist/workflows/dag/validate.js +2 -7
  84. package/docs/README.md +3 -3
  85. package/docs/architecture/evolution.md +67 -102
  86. package/docs/templates/backend-test-dag.json +50 -32
  87. package/docs/templates/frontend-design-contract.md +14 -34
  88. package/docs/templates/frontend-task-constraints.md +13 -33
  89. package/docs/templates/frontend-task-requirement.md +20 -54
  90. package/docs/templates/init-managed-agents.md +2 -5
  91. package/harness.json +2 -2
  92. package/package.json +3 -4
  93. package/skills/loop-agent/SKILL.md +0 -1
  94. package/skills/loop-agent/references/command-reference.md +0 -3
  95. package/dist/build-stamp.json +0 -6
  96. package/dist/commands/dag-request-interrupt.js +0 -20
  97. package/dist/worker/console/chat/assistant-content.js +0 -121
  98. package/dist/worker/console/chat/semantic-activity.js +0 -471
  99. package/dist/worker/console/operation-run-facts.js +0 -190
  100. package/dist/worker/console/operation-wait.js +0 -355
  101. package/dist/worker/console/recovery-error-copy.js +0 -198
  102. package/dist/worker/console/static/assets/index-D83DYAFG.css +0 -1
  103. package/dist/worker/console/static/assets/index-IXm7oYjL.js +0 -59
  104. package/dist/worker/console/static-src/operator-chat/activity-journey.js +0 -125
  105. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  106. package/dist/worker/console/static-src/operator-chat/activity-references.js +0 -20
  107. package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +0 -24
  108. package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +0 -141
  109. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
  110. package/dist/worker/observability/interrupt-eligibility.js +0 -264
  111. package/dist/workflows/dag/contract-output-registry.js +0 -14
  112. package/dist/workflows/dag/contract-validator-registrations.js +0 -8
  113. package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
  114. package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
  115. package/dist/workflows/dag/frontend-recovery-run.js +0 -539
  116. package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
  117. package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
  118. package/dist/workflows/dag/interrupt-request.js +0 -559
@@ -6,8 +6,8 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <link rel="stylesheet" href="/inspect/operator-chrome.css" />
8
8
  <title>Loop 操作台 · Operator Console</title>
9
- <script type="module" crossorigin src="/assets/index-IXm7oYjL.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-D83DYAFG.css">
9
+ <script type="module" crossorigin src="/assets/index-M0BLEBfh.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-HX1pbOyl.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
@@ -1,4 +1,3 @@
1
- import { formatIneligibleRerunMessage, nextActionForIneligibleRerun, } from "../../recovery-error-copy.js";
2
1
  /** Legacy step ids from older sessions map onto the merged contract step. */
3
2
  export function normalizeWizardStep(step) {
4
3
  if (step === "validate" || step === "apply" || step === "spine") {
@@ -58,7 +57,6 @@ export const RECOVERY_FACT_LABEL = {
58
57
  "terminal-failed": "已终止(失败)",
59
58
  "terminal-succeeded": "已终止(成功)",
60
59
  "needs-reconcile": "需要对账",
61
- "running-active": "正在运行",
62
60
  };
63
61
  export const LAYER_VALUE_LABEL = {
64
62
  idle: "空闲",
@@ -110,20 +108,18 @@ export const LONG_RUNNING_ACTIONS = new Set([
110
108
  "runDag",
111
109
  "dagRerun",
112
110
  "dagReconcileRun",
113
- "dagRequestInterrupt",
114
111
  "standaloneTaskRerun",
115
112
  ]);
116
- export function suggestedRecoveryAfterIneligibleRerun(reasons) {
117
- return nextActionForIneligibleRerun(reasons);
118
- }
119
- export function formatDagRerunIneligible(plan, fallbackMessage) {
120
- return formatIneligibleRerunMessage({
121
- blockedReasons: plan.blockedReasons,
122
- blockingNodes: plan.blockingNodes,
123
- fallbackMessage,
124
- });
113
+ export function formatDagRerunIneligible(plan) {
114
+ const fromBlocked = (plan.blockedReasons ?? []).map(String);
115
+ const fromNodes = (plan.blockingNodes ?? []).map((n) => [n.nodeId, n.reasonCode].filter(Boolean).join(":"));
116
+ const reasons = [...fromBlocked, ...fromNodes].filter(Boolean);
117
+ if (reasons.length === 0) {
118
+ return "从节点重跑不具备安全资格(子图含 writer / unsafe shell / decision-gate 等)。可改用「完整重跑任务」,或另选只读安全子图的起始节点。";
119
+ }
120
+ return `从节点重跑不具备安全资格:${reasons.slice(0, 6).join("; ")}${reasons.length > 6 ? "…" : ""}。可改用「完整重跑任务」。`;
125
121
  }
126
- export async function waitForOperation(operationId, timeoutMs = 300_000) {
122
+ export async function waitForOperation(operationId, timeoutMs = 90_000) {
127
123
  const started = Date.now();
128
124
  while (Date.now() - started < timeoutMs) {
129
125
  const res = await fetch(`/api/operator/v1/operations/${encodeURIComponent(operationId)}`, { credentials: "same-origin" });
@@ -6,8 +6,8 @@ import { confirmationToken, CONFIRMATION_HEADER, LONG_RUNNING_ACTIONS, postOpera
6
6
  * seconds server-side (imports + intake). Prevents the "page looks frozen"
7
7
  * gap between click and the terminal flash message. */
8
8
  const ACTION_PROGRESS_COPY = {
9
- bootstrapFromPrd: "正在导入 PRD 并创建任务:归档文档 → 确定性解析 → 语义结构化(MED→HIGH→LOW,每档最多 300 秒,请勿关闭页面)",
10
- importPrd: "正在导入 PRD:归档文档 → 确定性解析 → 语义结构化(MED→HIGH→LOW,每档最多 300 秒,请勿关闭页面)",
9
+ bootstrapFromPrd: "正在导入 PRD 并创建任务:归档文档 → 确定性解析 → 语义结构化(约 10–60 秒,请勿关闭页面)",
10
+ importPrd: "正在导入 PRD:归档文档 → 确定性解析 → 语义结构化(约 10–60 秒,请勿关闭页面)",
11
11
  interviewStart: "正在启动需求访谈…",
12
12
  interviewTurn: "正在提交访谈回答…",
13
13
  interviewSkip: "正在跳过访谈并生成评估…",
@@ -16,8 +16,6 @@ const ACTION_PROGRESS_COPY = {
16
16
  contractApply: "正在应用契约(写入需求与执行约束)…",
17
17
  spineCheck: "正在运行 Spine 审计…",
18
18
  dagGenerate: "正在生成 DAG…",
19
- standaloneTaskRerun: "正在完整重跑任务:服务端校验资格后启动新 run…",
20
- workerTaskRetry: "正在重新排队 Worker Task…",
21
19
  };
22
20
  /** Central operator-action dispatcher: POST /api/operator/v1/operations with
23
21
  * polling, long-running background waits, interview payload handoff and the
@@ -1,35 +1,10 @@
1
1
  import { useCallback } from "react";
2
2
  import { isAutoRecoveryReason } from "../../recovery-selection.js";
3
- import { formatOperatorUserError } from "../../operator-user-error.js";
4
3
  import { formatDagRerunIneligible, postOperator, requestId, } from "./console-types.js";
5
- /** Destructive reconcile still consumes a browser Human Gate receipt.
6
- * Same-task rerun / Worker retry are autonomous and must not enter here. */
7
- async function consumeMutationGateAndRun(input) {
8
- const prep = await input.runAction("prepareMutationGate", {
9
- action: input.action,
10
- actionParams: input.actionParams,
11
- });
12
- if (!input.isActionSuccess(prep))
13
- return;
14
- const receiptSource = (prep?.result ?? prep);
15
- const receipt = receiptSource.receiptId
16
- ? receiptSource
17
- : (receiptSource.result ?? receiptSource);
18
- if (!receipt.receiptId || !receipt.humanGateToken) {
19
- input.setOpError("prepareMutationGate 未返回可消费的 Human Gate receipt");
20
- return;
21
- }
22
- await input.runAction(input.action, {
23
- ...input.actionParams,
24
- confirmationId: receipt.receiptId,
25
- humanGateToken: receipt.humanGateToken,
26
- }, { clientRequestId: input.clientRequestId });
27
- }
28
4
  /** Recovery CTA dispatch: diagnose/report/inspect shortcuts, reason-gated
29
- * confirm flow with dagRerunPlan preflight, autonomous rerun/retry, and
30
- * Human Gate only for destructive reconcile. */
5
+ * confirm flow with dagRerunPlan preflight, and gated reruns/retries. */
31
6
  export function useRecoveryActions(params) {
32
- const { shell, actions, taskId, dagRunId, fromNodeId, featureId, recoveryReason, recoveryReasonTouched, recoveryConfirm, recoveryPlanSummary, interruptReasonCode, setRecoveryConfirm, setRecoveryPlanSummary, setRecoveryReport, setRecoveryReason, setRecoveryReasonTouched, buildCurrentRecoveryReason, openFullCanvas, } = params;
7
+ const { shell, actions, taskId, dagRunId, fromNodeId, featureId, recoveryReason, recoveryReasonTouched, recoveryConfirm, recoveryPlanSummary, setRecoveryConfirm, setRecoveryPlanSummary, setRecoveryReport, setRecoveryReason, setRecoveryReasonTouched, buildCurrentRecoveryReason, openFullCanvas, } = params;
33
8
  const { setOpError, setOpErrorCode, setOpMessage, setBusy } = shell;
34
9
  const { runAction, isActionSuccess } = actions;
35
10
  const handleRecoveryCta = useCallback(async (cta) => {
@@ -108,7 +83,7 @@ export function useRecoveryActions(params) {
108
83
  setOpMessage(null);
109
84
  }
110
85
  else if (plan.eligible === false) {
111
- setOpError(formatOperatorUserError(formatDagRerunIneligible(plan, body.error?.message)));
86
+ setOpError(body.error?.message ?? formatDagRerunIneligible(plan));
112
87
  }
113
88
  else {
114
89
  setOpMessage(body.error?.message ??
@@ -175,7 +150,7 @@ export function useRecoveryActions(params) {
175
150
  planHash = plan.planHash?.trim() || planHash;
176
151
  planEligible = plan.eligible;
177
152
  if (plan.eligible === false) {
178
- setOpError(formatOperatorUserError(formatDagRerunIneligible(plan, planBody?.error?.message)));
153
+ setOpError(planBody?.error?.message ?? formatDagRerunIneligible(plan));
179
154
  return;
180
155
  }
181
156
  if (planBody && planBody.ok === false && !planHash) {
@@ -185,7 +160,7 @@ export function useRecoveryActions(params) {
185
160
  }
186
161
  }
187
162
  if (planEligible === false) {
188
- setOpError(formatOperatorUserError(formatDagRerunIneligible(recoveryPlanSummary ?? {})));
163
+ setOpError(formatDagRerunIneligible(recoveryPlanSummary ?? {}));
189
164
  return;
190
165
  }
191
166
  if (!planHash) {
@@ -201,10 +176,26 @@ export function useRecoveryActions(params) {
201
176
  setOpError("完整重跑任务需要 DAG 运行 ID");
202
177
  return;
203
178
  }
204
- await runAction("standaloneTaskRerun", {
179
+ const actionParams = {
205
180
  runId,
206
181
  reason,
207
182
  ...(taskId.trim() ? { taskId: taskId.trim() } : {}),
183
+ };
184
+ const prep = await runAction("prepareMutationGate", {
185
+ action: "standaloneTaskRerun",
186
+ actionParams,
187
+ });
188
+ if (!isActionSuccess(prep))
189
+ return;
190
+ const receipt = (prep?.result ?? prep);
191
+ if (!receipt.receiptId || !receipt.humanGateToken) {
192
+ setOpError("prepareMutationGate 未返回可消费的 Human Gate receipt");
193
+ return;
194
+ }
195
+ await runAction("standaloneTaskRerun", {
196
+ ...actionParams,
197
+ confirmationId: receipt.receiptId,
198
+ humanGateToken: receipt.humanGateToken,
208
199
  }, { clientRequestId });
209
200
  return;
210
201
  }
@@ -214,35 +205,27 @@ export function useRecoveryActions(params) {
214
205
  setOpError("重新排队 Worker Task 需要任务 ID");
215
206
  return;
216
207
  }
217
- await runAction("workerTaskRetry", {
208
+ const actionParams = {
218
209
  taskId: id,
219
210
  reason,
220
211
  ...(featureId.trim() ? { featureId: featureId.trim() } : {}),
221
- }, { clientRequestId });
222
- return;
223
- }
224
- if (cta.id === "interrupt") {
225
- const runId = dagRunId.trim();
226
- if (!runId) {
227
- setOpError("中止运行需要 DAG 运行 ID");
212
+ };
213
+ const prep = await runAction("prepareMutationGate", {
214
+ action: "workerTaskRetry",
215
+ actionParams,
216
+ });
217
+ if (!isActionSuccess(prep))
228
218
  return;
229
- }
230
- if (reason.trim().length < 10 || reason.trim().length > 500) {
231
- setOpError("中止原因去首尾空白后必须是 10–500 个字符,请手写说明,不要用占位句。");
219
+ const receipt = (prep?.result ?? prep);
220
+ if (!receipt.receiptId || !receipt.humanGateToken) {
221
+ setOpError("prepareMutationGate 未返回可消费的 Human Gate receipt");
232
222
  return;
233
223
  }
234
- await consumeMutationGateAndRun({
235
- runAction,
236
- isActionSuccess,
237
- setOpError,
238
- action: "dagRequestInterrupt",
239
- actionParams: {
240
- runId,
241
- reasonCode: interruptReasonCode.trim() || "operator-request",
242
- reasonDetail: reason,
243
- },
244
- clientRequestId,
245
- });
224
+ await runAction("workerTaskRetry", {
225
+ ...actionParams,
226
+ confirmationId: receipt.receiptId,
227
+ humanGateToken: receipt.humanGateToken,
228
+ }, { clientRequestId });
246
229
  return;
247
230
  }
248
231
  if (cta.id === "reconcile") {
@@ -254,18 +237,27 @@ export function useRecoveryActions(params) {
254
237
  // Default supersede: orphan/interrupted runs should not be resumed.
255
238
  // The browser Human Gate binds the destructive action + reason; the CLI
256
239
  // still fail-closes when runner is not proven dead/stopped.
257
- await consumeMutationGateAndRun({
258
- runAction,
259
- isActionSuccess,
260
- setOpError,
240
+ const actionParams = {
241
+ runId,
242
+ reason,
243
+ reconcileAction: "supersede",
244
+ };
245
+ const prep = await runAction("prepareMutationGate", {
261
246
  action: "dagReconcileRun",
262
- actionParams: {
263
- runId,
264
- reason,
265
- reconcileAction: "supersede",
266
- },
267
- clientRequestId,
247
+ actionParams,
268
248
  });
249
+ if (!isActionSuccess(prep))
250
+ return;
251
+ const receipt = (prep?.result ?? prep);
252
+ if (!receipt.receiptId || !receipt.humanGateToken) {
253
+ setOpError("prepareMutationGate 未返回可消费的 Human Gate receipt");
254
+ return;
255
+ }
256
+ await runAction("dagReconcileRun", {
257
+ ...actionParams,
258
+ confirmationId: receipt.receiptId,
259
+ humanGateToken: receipt.humanGateToken,
260
+ }, { clientRequestId });
269
261
  return;
270
262
  }
271
263
  setOpMessage(`${cta.label}:请经 CLI — ${cta.commandHint ?? ""}`);
@@ -276,7 +268,6 @@ export function useRecoveryActions(params) {
276
268
  recoveryConfirm,
277
269
  recoveryPlanSummary?.planHash,
278
270
  recoveryReason,
279
- interruptReasonCode,
280
271
  runAction,
281
272
  isActionSuccess,
282
273
  setOpError,
@@ -16,16 +16,12 @@ export function useRecoveryConsole(params) {
16
16
  const [recoveryReasonTouched, setRecoveryReasonTouched] = useState(false);
17
17
  const [recoveryPlanSummary, setRecoveryPlanSummary] = useState(null);
18
18
  const [recoveryConfirm, setRecoveryConfirm] = useState(null);
19
- const [interruptIntent, setInterruptIntent] = useState(false);
20
- const [interruptReasonCode, setInterruptReasonCode] = useState("operator-request");
21
- const [interruptEligibility, setInterruptEligibility] = useState(null);
22
19
  const [recoveryRuns, setRecoveryRuns] = useState([]);
23
20
  const [recoveryNodes, setRecoveryNodes] = useState([]);
24
21
  const [recoveryRunsLoading, setRecoveryRunsLoading] = useState(false);
25
22
  const [recoveryNodesLoading, setRecoveryNodesLoading] = useState(false);
26
23
  const [recoveryRunStatus, setRecoveryRunStatus] = useState(null);
27
24
  const [recoveryFailureCategory, setRecoveryFailureCategory] = useState(null);
28
- const [frontendRecovery, setFrontendRecovery] = useState(null);
29
25
  const [manualRunId, setManualRunId] = useState(false);
30
26
  const [manualNodeId, setManualNodeId] = useState(false);
31
27
  const [recoveryReport, setRecoveryReport] = useState(null);
@@ -128,7 +124,6 @@ export function useRecoveryConsole(params) {
128
124
  setRecoveryNodes([]);
129
125
  setRecoveryRunStatus(null);
130
126
  setRecoveryFailureCategory(null);
131
- setFrontendRecovery(null);
132
127
  return;
133
128
  }
134
129
  setRecoveryNodesLoading(true);
@@ -172,13 +167,11 @@ export function useRecoveryConsole(params) {
172
167
  n.failureCategory &&
173
168
  n.failureCategory !== "success")?.failureCategory;
174
169
  setRecoveryFailureCategory(runFailure ?? nodeFailure ?? null);
175
- setFrontendRecovery(body.frontendRecovery ?? null);
176
170
  }
177
171
  catch {
178
172
  setRecoveryNodes([]);
179
173
  setRecoveryRunStatus(null);
180
174
  setRecoveryFailureCategory(null);
181
- setFrontendRecovery(null);
182
175
  }
183
176
  finally {
184
177
  setRecoveryNodesLoading(false);
@@ -211,37 +204,6 @@ export function useRecoveryConsole(params) {
211
204
  return;
212
205
  void loadRecoveryNodes(dagRunId);
213
206
  }, [nav, dagRunId, loadRecoveryNodes]);
214
- useEffect(() => {
215
- if (nav !== "recovery")
216
- return;
217
- const runId = dagRunId.trim();
218
- if (!runId) {
219
- setInterruptEligibility(null);
220
- return;
221
- }
222
- let cancelled = false;
223
- const load = async () => {
224
- const { body } = await postOperator("dagInterruptEligibility", { runId });
225
- if (cancelled)
226
- return;
227
- const result = (body.result ?? body);
228
- setInterruptEligibility({
229
- eligible: result.eligible === true,
230
- ownerKind: result.ownerKind,
231
- message: result.message,
232
- interruptStatus: result.interruptStatus,
233
- reasonCodes: result.reasonCodes,
234
- targetOperationId: result.targetOperationId,
235
- targetOperationState: result.targetOperationState,
236
- });
237
- };
238
- void load();
239
- const timer = window.setInterval(() => void load(), 4000);
240
- return () => {
241
- cancelled = true;
242
- window.clearInterval(timer);
243
- };
244
- }, [nav, dagRunId]);
245
207
  useEffect(() => {
246
208
  if (nav !== "recovery")
247
209
  return;
@@ -267,14 +229,7 @@ export function useRecoveryConsole(params) {
267
229
  dagStatus: recoveryRunStatus,
268
230
  failureCategory: recoveryFailureCategory,
269
231
  });
270
- const recoveryCtas = useMemo(() => {
271
- const ctas = consoleRecoveryCtas(recoveryFact);
272
- if (recoveryFact !== "running-active")
273
- return ctas;
274
- if (interruptEligibility?.eligible)
275
- return ctas;
276
- return ctas.filter((cta) => cta.id !== "interrupt");
277
- }, [recoveryFact, interruptEligibility?.eligible]);
232
+ const recoveryCtas = useMemo(() => consoleRecoveryCtas(recoveryFact), [recoveryFact]);
278
233
  const recoveryLanes = useMemo(() => partitionRecoveryCtas(recoveryCtas), [recoveryCtas]);
279
234
  const recommendedCtaId = useMemo(() => recommendedRecoveryCtaId(recoveryFact), [recoveryFact]);
280
235
  const selectedRecoveryNode = useMemo(() => recoveryNodes.find((n) => n.nodeId === fromNodeId.trim()), [recoveryNodes, fromNodeId]);
@@ -340,7 +295,6 @@ export function useRecoveryConsole(params) {
340
295
  recoveryReasonTouched,
341
296
  recoveryConfirm,
342
297
  recoveryPlanSummary,
343
- interruptReasonCode,
344
298
  setRecoveryConfirm,
345
299
  setRecoveryPlanSummary,
346
300
  setRecoveryReport,
@@ -349,25 +303,6 @@ export function useRecoveryConsole(params) {
349
303
  buildCurrentRecoveryReason,
350
304
  openFullCanvas,
351
305
  });
352
- useEffect(() => {
353
- if (nav !== "recovery" || !interruptIntent)
354
- return;
355
- if (!interruptEligibility?.eligible)
356
- return;
357
- const interruptCta = recoveryCtas.find((cta) => cta.id === "interrupt");
358
- if (!interruptCta)
359
- return;
360
- setRecoveryConfirm(interruptCta);
361
- setRecoveryReason(buildCurrentRecoveryReason(interruptCta.label));
362
- setRecoveryReasonTouched(false);
363
- setInterruptIntent(false);
364
- }, [
365
- nav,
366
- interruptIntent,
367
- interruptEligibility?.eligible,
368
- recoveryCtas,
369
- buildCurrentRecoveryReason,
370
- ]);
371
306
  return {
372
307
  dagRunId,
373
308
  setDagRunId,
@@ -388,8 +323,6 @@ export function useRecoveryConsole(params) {
388
323
  recoveryRunsLoading,
389
324
  recoveryNodesLoading,
390
325
  recoveryRunStatus,
391
- recoveryFailureCategory,
392
- frontendRecovery,
393
326
  manualRunId,
394
327
  setManualRunId,
395
328
  manualNodeId,
@@ -412,11 +345,6 @@ export function useRecoveryConsole(params) {
412
345
  recoveryCtas,
413
346
  recoveryLanes,
414
347
  recommendedCtaId,
415
- interruptIntent,
416
- setInterruptIntent,
417
- interruptReasonCode,
418
- setInterruptReasonCode,
419
- interruptEligibility,
420
348
  recoveryConfirmRef,
421
349
  buildCurrentRecoveryReason,
422
350
  handleRecoveryCta,
@@ -2,7 +2,6 @@ import { artifactFromEventPayload, mergeArtifactCardState, } from "../../chat/ar
2
2
  import { humanGateCardFromEventPayload, mergeHumanGateCardState, } from "../../chat/human-gate-card.js";
3
3
  import { mergeOperationCardState, operationFromEventPayload, } from "../../chat/operation-card.js";
4
4
  import { requestId } from "./format.js";
5
- import { parseAssistantContent } from "../../chat/assistant-content.js";
6
5
  /** Shared handler for a single parsed Chat SSE event (M1 reconnect).
7
6
  *
8
7
  * Used by BOTH the inline prompt POST SSE and the background reconnect
@@ -25,30 +24,25 @@ export function createChatSseEventApplier(deps) {
25
24
  // global error; the snapshot replay frames that follow are still applied
26
25
  // as ordinary blocks (deduped by eventId, replay text skipped).
27
26
  const reason = typeof data.reason === "string" ? data.reason : "cursor_expired";
28
- const reconcileInfo = {
27
+ onReconcile?.({
29
28
  reason,
30
29
  ...(typeof data.minSeq === "number" ? { minSeq: data.minSeq } : {}),
31
30
  ...(typeof data.epoch === "string" ? { epoch: data.epoch } : {}),
32
31
  ...(typeof data.latestTurnId === "string"
33
32
  ? { latestTurnId: data.latestTurnId }
34
33
  : {}),
35
- };
36
- if (typeof data.latestEventId === "string" || data.latestEventId === null) {
37
- reconcileInfo.latestEventId = data.latestEventId;
38
- }
39
- onReconcile?.(reconcileInfo);
34
+ ...(typeof data.latestEventId === "string"
35
+ ? { latestEventId: data.latestEventId }
36
+ : data.latestEventId === null
37
+ ? { latestEventId: null }
38
+ : {}),
39
+ });
40
40
  return;
41
41
  }
42
42
  const payload = data.data && typeof data.data === "object"
43
43
  ? data.data
44
44
  : data;
45
45
  const turnId = typeof data.turnId === "string" ? data.turnId : undefined;
46
- const eventSeq = typeof data.seq === "number" && Number.isFinite(data.seq)
47
- ? data.seq
48
- : undefined;
49
- const eventAt = typeof data.at === "string" && Number.isFinite(Date.parse(data.at))
50
- ? Date.parse(data.at)
51
- : Date.now();
52
46
  if (turnId &&
53
47
  (eventName === "agent_start" || eventName === "message_update") &&
54
48
  refs.settledTurnIdsRef.current.has(turnId)) {
@@ -113,26 +107,14 @@ export function createChatSseEventApplier(deps) {
113
107
  }
114
108
  if (eventName === "human-gate-card") {
115
109
  const card = humanGateCardFromEventPayload(payload);
116
- if (card) {
117
- setHumanGateCards((current) => mergeHumanGateCardState(current, {
118
- ...card,
119
- ...(eventSeq === undefined ? {} : { eventSeq }),
120
- ...(turnId ? { turnId } : {}),
121
- activityRef: `gate:${card.cardId}`,
122
- }));
123
- }
110
+ if (card)
111
+ setHumanGateCards((current) => mergeHumanGateCardState(current, card));
124
112
  return;
125
113
  }
126
114
  if (eventName === "artifact-ref") {
127
115
  const artifact = artifactFromEventPayload(payload);
128
- if (artifact) {
129
- setArtifactCards((current) => mergeArtifactCardState(current, {
130
- ...artifact,
131
- ...(eventSeq === undefined ? {} : { eventSeq }),
132
- ...(turnId ? { turnId } : {}),
133
- activityRef: `artifact:${artifact.artifactId}`,
134
- }));
135
- }
116
+ if (artifact)
117
+ setArtifactCards((current) => mergeArtifactCardState(current, artifact));
136
118
  return;
137
119
  }
138
120
  if (eventName === "agent_start") {
@@ -189,18 +171,7 @@ export function createChatSseEventApplier(deps) {
189
171
  if (eventName === "operation-ref" || eventName === "operation-status") {
190
172
  const operation = operationFromEventPayload(payload);
191
173
  if (operation) {
192
- const toolCallId = typeof payload.toolCallId === "string"
193
- ? payload.toolCallId
194
- : undefined;
195
- setOperationCards((current) => mergeOperationCardState(current, {
196
- ...operation,
197
- ...(eventSeq === undefined
198
- ? {}
199
- : { eventSeq, updatedSeq: eventSeq }),
200
- ...(turnId ? { turnId } : {}),
201
- ...(toolCallId ? { toolCallId } : {}),
202
- activityRef: `operation:${operation.operationId}`,
203
- }));
174
+ setOperationCards((current) => mergeOperationCardState(current, operation));
204
175
  }
205
176
  return;
206
177
  }
@@ -242,46 +213,24 @@ export function createChatSseEventApplier(deps) {
242
213
  // a duplicate bubble.
243
214
  return;
244
215
  }
245
- const sealedIds = refs.sealedAssistantIdsRef.current;
246
- if (eventName === "message_update" &&
247
- sealedIds.has(resolvedAssistantId)) {
248
- const nextId = requestId("a");
249
- if (turnId)
250
- refs.turnAssistantIdsRef.current.set(turnId, nextId);
251
- resolvedAssistantId = nextId;
252
- setStreamingAssistantId(nextId);
253
- }
254
216
  const text = typeof payload.text === "string" ? payload.text : "";
255
- const content = parseAssistantContent(payload.content);
256
- const assistantIdForEvent = resolvedAssistantId;
257
217
  setMessages((m) => {
258
- const exists = m.some((msg) => msg.id === assistantIdForEvent);
218
+ const exists = m.some((msg) => msg.id === resolvedAssistantId);
259
219
  if (exists) {
260
- return m.map((msg) => {
261
- if (msg.id !== assistantIdForEvent)
262
- return msg;
263
- return {
264
- ...msg,
265
- text,
266
- ...(content ? { content } : {}),
267
- };
268
- });
220
+ return m.map((msg) => msg.id === resolvedAssistantId ? { ...msg, text } : msg);
269
221
  }
270
222
  // Reconnect path: the assistant message may not exist yet
271
223
  // (e.g. only tool_call events arrived). Append a placeholder.
272
224
  return [
273
225
  ...m,
274
226
  {
275
- id: assistantIdForEvent,
227
+ id: resolvedAssistantId,
276
228
  role: "assistant",
277
229
  text,
278
- createdAt: eventAt,
279
- ...(content ? { content } : {}),
230
+ createdAt: Date.now(),
280
231
  },
281
232
  ];
282
233
  });
283
- if (eventName === "message_end")
284
- sealedIds.add(assistantIdForEvent);
285
234
  }
286
235
  else if (eventName === "tool_call") {
287
236
  const id = typeof payload.toolCallId === "string"
@@ -289,20 +238,18 @@ export function createChatSseEventApplier(deps) {
289
238
  : requestId("t");
290
239
  const toolName = typeof payload.toolName === "string" ? payload.toolName : "tool";
291
240
  const argsPreview = typeof payload.argsPreview === "string" ? payload.argsPreview : "{}";
241
+ const now = Date.now();
292
242
  setToolCalls((prev) => {
293
243
  if (prev.some((t) => t.id === id)) {
294
- return prev.map((t) => {
295
- if (t.id !== id)
296
- return t;
297
- return {
244
+ return prev.map((t) => t.id === id
245
+ ? {
298
246
  ...t,
299
247
  args: argsPreview,
300
248
  status: "running",
301
249
  turnId: turnId ?? t.turnId,
302
250
  assistantMessageId: resolvedAssistantId,
303
- ...(eventSeq === undefined ? {} : { eventSeq }),
304
- };
305
- });
251
+ }
252
+ : t);
306
253
  }
307
254
  return [
308
255
  ...prev,
@@ -311,8 +258,7 @@ export function createChatSseEventApplier(deps) {
311
258
  toolName,
312
259
  args: argsPreview,
313
260
  status: "running",
314
- createdAt: eventAt,
315
- ...(eventSeq === undefined ? {} : { eventSeq }),
261
+ createdAt: now,
316
262
  turnId,
317
263
  assistantMessageId: resolvedAssistantId,
318
264
  },
@@ -328,23 +274,21 @@ export function createChatSseEventApplier(deps) {
328
274
  const resultText = typeof payload.resultPreview === "string"
329
275
  ? payload.resultPreview
330
276
  : "{}";
277
+ const now = Date.now();
331
278
  setToolCalls((prev) => {
332
279
  const exists = prev.find((t) => t.id === id);
333
280
  if (exists) {
334
- return prev.map((t) => {
335
- if (t.id !== id)
336
- return t;
337
- return {
281
+ return prev.map((t) => t.id === id
282
+ ? {
338
283
  ...t,
339
284
  result: resultText,
340
285
  resultText,
341
286
  status: isError ? "error" : "ok",
342
- finishedAt: eventAt,
343
- ...(eventSeq === undefined ? {} : { finishedSeq: eventSeq }),
287
+ finishedAt: now,
344
288
  turnId: turnId ?? t.turnId,
345
289
  assistantMessageId: t.assistantMessageId ?? resolvedAssistantId,
346
- };
347
- });
290
+ }
291
+ : t);
348
292
  }
349
293
  return [
350
294
  ...prev,
@@ -354,11 +298,8 @@ export function createChatSseEventApplier(deps) {
354
298
  result: resultText,
355
299
  resultText,
356
300
  status: isError ? "error" : "ok",
357
- createdAt: eventAt,
358
- finishedAt: eventAt,
359
- ...(eventSeq === undefined
360
- ? {}
361
- : { eventSeq, finishedSeq: eventSeq }),
301
+ createdAt: now,
302
+ finishedAt: now,
362
303
  turnId,
363
304
  assistantMessageId: resolvedAssistantId,
364
305
  },
@@ -12,7 +12,6 @@ export function useChatRefs() {
12
12
  const settledTurnIdsRef = useRef(new Set());
13
13
  const seenEventIdsRef = useRef(new Set());
14
14
  const turnAssistantIdsRef = useRef(new Map());
15
- const sealedAssistantIdsRef = useRef(new Set());
16
15
  const reconnectAbortRef = useRef(null);
17
16
  const activeTurnIdRef = useRef(null);
18
17
  const submitInFlightRef = useRef(false);
@@ -41,7 +40,6 @@ export function useChatRefs() {
41
40
  settledTurnIdsRef,
42
41
  seenEventIdsRef,
43
42
  turnAssistantIdsRef,
44
- sealedAssistantIdsRef,
45
43
  reconnectAbortRef,
46
44
  activeTurnIdRef,
47
45
  submitInFlightRef,
@@ -68,7 +66,6 @@ export function useChatRefs() {
68
66
  settledTurnIdsRef,
69
67
  seenEventIdsRef,
70
68
  turnAssistantIdsRef,
71
- sealedAssistantIdsRef,
72
69
  reconnectAbortRef,
73
70
  activeTurnIdRef,
74
71
  submitInFlightRef,