@tea-agent/loop-agent 0.38.0 → 0.38.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/dist/application/dag/generate-task-dag.js +9 -0
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/commands/init-upgrade.js +97 -19
  5. package/dist/executors/dag-pi-executor.js +42 -4
  6. package/dist/executors/shell-executor.js +269 -15
  7. package/dist/infrastructure/harness/atomic-write.js +2 -1
  8. package/dist/shared/openspec-spec.js +76 -0
  9. package/dist/shared/operator/capabilities.js +149 -27
  10. package/dist/task/config-types.js +43 -0
  11. package/dist/task/frontend-project-capability.js +292 -38
  12. package/dist/worker/console/chat/deferred-turn.js +91 -0
  13. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  14. package/dist/worker/console/chat/pi-runtime.js +165 -83
  15. package/dist/worker/console/chat/repo-walk.js +10 -1
  16. package/dist/worker/console/chat/routes.js +477 -55
  17. package/dist/worker/console/chat/semantic-activity.js +20 -8
  18. package/dist/worker/console/chat/session-store.js +146 -2
  19. package/dist/worker/console/chat/turn-process.js +17 -30
  20. package/dist/worker/console/harness-pi-model-matrix.js +137 -0
  21. package/dist/worker/console/operator-actions.js +128 -4
  22. package/dist/worker/console/operator-user-error.js +19 -0
  23. package/dist/worker/console/pi-readiness.js +8 -1
  24. package/dist/worker/console/recovery-cta.js +4 -4
  25. package/dist/worker/console/recovery-error-copy.js +4 -4
  26. package/dist/worker/console/security.js +20 -1
  27. package/dist/worker/console/server.js +6 -1
  28. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-C4bmo3iY.js} +1 -1
  29. package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-SAD7McOS.js} +1 -1
  30. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-DAVLUdK1.js} +1 -1
  31. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-CPPdv4vj.js} +1 -1
  32. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-D1GRGZax.js} +1 -1
  33. package/dist/worker/console/static/assets/channel-R6oQIDkw.js +1 -0
  34. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-D_Bq2ZqK.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-CPLVtamK.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-DnaZWcL3.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BRTmFgnq.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-BRlfYZoH.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-CjNybD4u.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-D3e-74fG.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-Br3ir6f2.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-1NgVuRbh.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-BTYuvuWN.js} +1 -1
  44. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +1 -0
  45. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +1 -0
  46. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-BC6Bgosf.js} +1 -1
  47. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-DV-HypfL.js} +1 -1
  48. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DhTaddO-.js} +1 -1
  49. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-BnEcOmiI.js} +1 -1
  50. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-xphGRVKp.js} +1 -1
  51. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-CrdD1mRG.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-D4YdLsaF.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-LCKyGZ5U.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-BeiTe7t0.js} +1 -1
  55. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-BO2cq8GS.js} +1 -1
  56. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-CmWww3Nt.js} +1 -1
  57. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-C2kkUYg5.js} +1 -1
  58. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-DY_JZ2iQ.js} +1 -1
  59. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-BizhCSw9.js} +1 -1
  60. package/dist/worker/console/static/assets/index-BXN89VPX.js +325 -0
  61. package/dist/worker/console/static/assets/index-CuH5CNFu.css +1 -0
  62. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-Dlo2eK_d.js} +1 -1
  63. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js} +1 -1
  64. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-DiMDe5Yh.js} +1 -1
  65. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-sN553oIN.js} +1 -1
  66. package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-D9Py31Ld.js} +1 -1
  67. package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-D7Occzk_.js} +5 -5
  68. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-DjDRboiR.js} +1 -1
  69. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-Cgc4qlzp.js} +1 -1
  70. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-DxZj8pKd.js} +1 -1
  71. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-CJ50MngR.js} +1 -1
  72. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-BY-OD8Tr.js} +1 -1
  73. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-CQFButfK.js} +1 -1
  74. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-BB_QUK_b.js} +1 -1
  75. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-CqhXZjyx.js} +1 -1
  76. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-2l1U4rgJ.js} +1 -1
  77. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-BX1FbrhJ.js} +1 -1
  78. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +1 -0
  79. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DOSnT7hb.js} +2 -2
  80. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +8 -0
  81. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-DVJQA7AN.js} +1 -1
  82. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-74uxl9gx.js} +1 -1
  83. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-DsBxlcg_.js} +1 -1
  84. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-snuW9Mjw.js} +1 -1
  85. package/dist/worker/console/static/index.html +2 -2
  86. package/dist/worker/console/static-src/app/console-types.js +39 -0
  87. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  88. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  89. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +82 -22
  90. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  91. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  92. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  93. package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
  94. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
  95. package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
  96. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  97. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  98. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +755 -51
  99. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  100. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  101. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  102. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  103. package/dist/workflows/dag/backend-test-layout.js +133 -0
  104. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  105. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  106. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  107. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  108. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  109. package/dist/workflows/dag/backend-test-scenario-partitions.js +249 -0
  110. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  111. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  112. package/dist/workflows/dag/convergence/controller.js +141 -0
  113. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  114. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  115. package/dist/workflows/dag/failure-routing.js +9 -4
  116. package/dist/workflows/dag/frontend-implementation-contract.js +386 -0
  117. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  118. package/dist/workflows/dag/frontend-prewrite-gate.js +479 -28
  119. package/dist/workflows/dag/init-hybrid.js +920 -110
  120. package/dist/workflows/dag/lifecycle.js +4 -0
  121. package/dist/workflows/dag/node-execution.js +77 -8
  122. package/dist/workflows/dag/prompt.js +45 -0
  123. package/dist/workflows/dag/report.js +6 -0
  124. package/dist/workflows/dag/rerun-feedback.js +212 -0
  125. package/dist/workflows/dag/rerun-plan.js +93 -27
  126. package/dist/workflows/dag/rerun-run.js +3 -0
  127. package/dist/workflows/dag/rerun-task.js +61 -2
  128. package/dist/workflows/dag/retry-policy.js +33 -0
  129. package/dist/workflows/dag/types.js +148 -4
  130. package/dist/workflows/dag/validate.js +7 -2
  131. package/docs/templates/backend-test-dag.json +60 -35
  132. package/docs/templates/frontend-design-contract.md +40 -14
  133. package/docs/templates/frontend-implementation-contract.schema.json +9 -2
  134. package/docs/templates/frontend-task-constraints.md +40 -13
  135. package/docs/templates/frontend-task-requirement.md +54 -20
  136. package/harness.json +3 -1
  137. package/package.json +1 -1
  138. package/skills/frontend-design-review/references/review-checklist.md +2 -0
  139. package/skills/frontend-implementation/references/design-spec.md +16 -19
  140. package/skills/frontend-implementation/references/node-contracts.md +15 -11
  141. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  142. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  143. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  144. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  145. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  146. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  147. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
@@ -17,6 +17,7 @@ import { dispatchOperatorAction } from "../operator-actions.js";
17
17
  import { authorizeOperatorChatTool } from "./tools.js";
18
18
  import { projectOperationForChat } from "./chat-event-store.js";
19
19
  import { scrubSecrets } from "./explore-tools.js";
20
+ import { hashDeferredTurnPayload, parseDeferredTurn, } from "./deferred-turn.js";
20
21
  const CHAT_TITLE_MAX_CHARS = 80;
21
22
  /** Safe, one-line bounded title used for placeholders and model output. */
22
23
  export function cleanChatTitle(value) {
@@ -53,6 +54,7 @@ export class ChatSessionStore {
53
54
  const raw = await readJsonIfExists(recordPath(this.appData, sessionId));
54
55
  if (!raw || raw.sessionId !== sessionId)
55
56
  return undefined;
57
+ const deferredTurn = parseDeferredTurn(raw.deferredTurn);
56
58
  return {
57
59
  schemaVersion: 2,
58
60
  sessionId,
@@ -67,6 +69,7 @@ export class ChatSessionStore {
67
69
  ...(typeof raw.modelProvider === "string" ? { modelProvider: raw.modelProvider } : {}),
68
70
  ...(typeof raw.modelId === "string" ? { modelId: raw.modelId } : {}),
69
71
  ...(typeof raw.thinkingLevel === "string" ? { thinkingLevel: raw.thinkingLevel } : {}),
72
+ ...(deferredTurn ? { deferredTurn } : {}),
70
73
  messages: Array.isArray(raw.messages) ? raw.messages : [],
71
74
  toolInvocations: Array.isArray(raw.toolInvocations) ? raw.toolInvocations : [],
72
75
  };
@@ -147,6 +150,7 @@ export class ChatSessionStore {
147
150
  ...(input.targetSessionFile ? { sessionFile: input.targetSessionFile } : { sessionFile: undefined }),
148
151
  createdAt: now,
149
152
  updatedAt: now,
153
+ deferredTurn: undefined,
150
154
  messages: structuredClone(source.messages),
151
155
  toolInvocations: structuredClone(source.toolInvocations),
152
156
  };
@@ -216,6 +220,146 @@ export class ChatSessionStore {
216
220
  return true;
217
221
  });
218
222
  }
223
+ async putDeferredTurn(sessionId, input) {
224
+ return this.serialize(sessionId, async () => {
225
+ const record = await this.get(sessionId);
226
+ if (!record)
227
+ return { ok: false, code: "NOT_FOUND" };
228
+ const clientRequestId = input.clientRequestId.trim();
229
+ const blockedByTurnId = input.blockedByTurnId.trim();
230
+ const text = input.text.trim() || (input.images.length > 0 ? "请查看附件图片。" : "");
231
+ if (!clientRequestId || !blockedByTurnId || !text) {
232
+ return { ok: false, code: "INVALID_INPUT" };
233
+ }
234
+ const payloadHash = hashDeferredTurnPayload({ text, images: input.images });
235
+ const existing = record.deferredTurn;
236
+ if (existing) {
237
+ if (existing.clientRequestId === clientRequestId) {
238
+ if (existing.payloadHash !== payloadHash) {
239
+ return {
240
+ ok: false,
241
+ code: "REQUEST_ID_REUSE_CONFLICT",
242
+ deferredTurn: existing,
243
+ };
244
+ }
245
+ return { ok: true, deferredTurn: existing, idempotent: true };
246
+ }
247
+ if (existing.state !== "queued") {
248
+ return {
249
+ ok: false,
250
+ code: "NOT_REPLACEABLE",
251
+ deferredTurn: existing,
252
+ };
253
+ }
254
+ if (input.expectedRevision !== existing.revision) {
255
+ return {
256
+ ok: false,
257
+ code: "REVISION_CONFLICT",
258
+ deferredTurn: existing,
259
+ };
260
+ }
261
+ }
262
+ else if (input.expectedRevision !== undefined &&
263
+ input.expectedRevision !== 0) {
264
+ return { ok: false, code: "REVISION_CONFLICT" };
265
+ }
266
+ const now = new Date().toISOString();
267
+ const deferredTurn = {
268
+ schemaVersion: 1,
269
+ revision: (existing?.revision ?? 0) + 1,
270
+ clientRequestId,
271
+ payloadHash,
272
+ blockedByTurnId,
273
+ text,
274
+ images: structuredClone(input.images),
275
+ state: "queued",
276
+ createdAt: existing?.createdAt ?? now,
277
+ updatedAt: now,
278
+ };
279
+ record.deferredTurn = deferredTurn;
280
+ record.updatedAt = now;
281
+ await writeSecureJson(recordPath(this.appData, sessionId), record);
282
+ return { ok: true, deferredTurn, idempotent: false };
283
+ });
284
+ }
285
+ async claimDeferredTurn(sessionId, expectedRevision) {
286
+ return this.serialize(sessionId, async () => {
287
+ const record = await this.get(sessionId);
288
+ const existing = record?.deferredTurn;
289
+ if (!record || !existing)
290
+ return { ok: false, code: "NOT_FOUND" };
291
+ if (existing.revision !== expectedRevision) {
292
+ return { ok: false, code: "REVISION_CONFLICT", deferredTurn: existing };
293
+ }
294
+ if (existing.state !== "queued") {
295
+ return { ok: false, code: "NOT_CLAIMABLE", deferredTurn: existing };
296
+ }
297
+ const next = {
298
+ ...existing,
299
+ revision: existing.revision + 1,
300
+ state: "dispatching",
301
+ updatedAt: new Date().toISOString(),
302
+ };
303
+ record.deferredTurn = next;
304
+ record.updatedAt = next.updatedAt;
305
+ await writeSecureJson(recordPath(this.appData, sessionId), record);
306
+ return { ok: true, deferredTurn: next };
307
+ });
308
+ }
309
+ async transitionDeferredTurn(sessionId, input) {
310
+ return this.serialize(sessionId, async () => {
311
+ const record = await this.get(sessionId);
312
+ const existing = record?.deferredTurn;
313
+ if (!record || !existing || existing.revision !== input.expectedRevision)
314
+ return undefined;
315
+ const next = {
316
+ ...existing,
317
+ revision: existing.revision + 1,
318
+ state: input.state,
319
+ updatedAt: new Date().toISOString(),
320
+ ...(input.acceptedTurnId ? { acceptedTurnId: input.acceptedTurnId } : {}),
321
+ ...(input.lastError ? { lastError: input.lastError } : {}),
322
+ };
323
+ if (!input.lastError)
324
+ delete next.lastError;
325
+ record.deferredTurn = next;
326
+ record.updatedAt = next.updatedAt;
327
+ await writeSecureJson(recordPath(this.appData, sessionId), record);
328
+ return next;
329
+ });
330
+ }
331
+ async deleteDeferredTurn(sessionId, input) {
332
+ return this.serialize(sessionId, async () => {
333
+ const record = await this.get(sessionId);
334
+ const existing = record?.deferredTurn;
335
+ if (!record || !existing || existing.clientRequestId !== input.clientRequestId) {
336
+ return { ok: false, code: "NOT_FOUND" };
337
+ }
338
+ if (existing.state !== "queued" && existing.state !== "failed") {
339
+ return { ok: false, code: "NOT_CANCELLABLE", deferredTurn: existing };
340
+ }
341
+ if (existing.revision !== input.expectedRevision) {
342
+ return { ok: false, code: "REVISION_CONFLICT", deferredTurn: existing };
343
+ }
344
+ delete record.deferredTurn;
345
+ record.updatedAt = new Date().toISOString();
346
+ await writeSecureJson(recordPath(this.appData, sessionId), record);
347
+ return { ok: true, deferredTurn: existing };
348
+ });
349
+ }
350
+ async clearDeferredTurnForRequest(sessionId, clientRequestId) {
351
+ return this.serialize(sessionId, async () => {
352
+ const record = await this.get(sessionId);
353
+ const existing = record?.deferredTurn;
354
+ if (!record || !existing || existing.clientRequestId !== clientRequestId) {
355
+ return undefined;
356
+ }
357
+ delete record.deferredTurn;
358
+ record.updatedAt = new Date().toISOString();
359
+ await writeSecureJson(recordPath(this.appData, sessionId), record);
360
+ return { deferredTurn: existing, clearedRevision: existing.revision + 1 };
361
+ });
362
+ }
219
363
  async appendMessage(sessionId, message) {
220
364
  return this.serialize(sessionId, async () => {
221
365
  const record = await this.get(sessionId);
@@ -340,10 +484,10 @@ export async function runChatTurn(options) {
340
484
  },
341
485
  };
342
486
  }
343
- await store.appendMessage(sessionId, { role: "user", text });
487
+ const userMessage = await store.appendMessage(sessionId, { role: "user", text });
344
488
  const afterUserMessage = await store.get(sessionId);
345
489
  if (afterUserMessage)
346
- await options.onUserMessagePersisted?.(afterUserMessage);
490
+ await options.onUserMessagePersisted?.(afterUserMessage, userMessage);
347
491
  let turnError;
348
492
  // Collect persistence promises so runChatTurn does not resolve before the
349
493
  // chat history / audit trail is durably written (also makes the outcome
@@ -92,36 +92,25 @@ function sortToolsStable(tools) {
92
92
  });
93
93
  }
94
94
  /**
95
- * Flush buffered consecutive tools into process items. Semantic operator
96
- * tools stay as individual activity events; plain tools group into a
97
- * `tool-group` ("并行调用 · N 项") when two or more land back to back.
95
+ * Flush buffered consecutive tools into process items (pi-web aligned:
96
+ * strict sequential flow, no synthetic grouping). Semantic operator tools
97
+ * stay as individual activity events; plain tools each render as their own
98
+ * card in stable server order (eventSeq first, createdAt fallback).
98
99
  * Thinking/text/activity boundaries break the buffer upstream.
99
100
  */
100
101
  function flushToolBuffer(processItems, buffer) {
101
102
  if (buffer.length === 0)
102
103
  return;
103
104
  const sorted = sortToolsStable(buffer);
104
- let plainGroup = [];
105
- const flushPlain = () => {
106
- if (plainGroup.length === 1) {
107
- processItems.push({ kind: "tool", tool: plainGroup[0] });
108
- }
109
- else if (plainGroup.length > 1) {
110
- processItems.push({ kind: "tool-group", tools: plainGroup });
111
- }
112
- plainGroup = [];
113
- };
114
105
  for (const tool of sorted) {
115
106
  const activity = semanticActivityForTool(tool);
116
107
  if (activity) {
117
- flushPlain();
118
108
  processItems.push({ kind: "activity", activity });
119
109
  }
120
110
  else {
121
- plainGroup.push(tool);
111
+ processItems.push({ kind: "tool", tool });
122
112
  }
123
113
  }
124
- flushPlain();
125
114
  }
126
115
  function emitProcessBlocks(blocks, group, processItems, usedToolIds) {
127
116
  let buffer = [];
@@ -227,9 +216,6 @@ export function buildTurnProcessGroups(input) {
227
216
  current = startGroup(`assistant-${message.id}`);
228
217
  current.assistants.push(message);
229
218
  }
230
- if (groups.length === 0 && input.toolCalls.length > 0) {
231
- groups.push(emptyGroup("orphan-tools"));
232
- }
233
219
  const assigned = new Set();
234
220
  const assistantToGroup = new Map();
235
221
  for (const group of groups) {
@@ -276,17 +262,12 @@ export function buildTurnProcessGroups(input) {
276
262
  const end = next?.user?.createdAt ?? Number.POSITIVE_INFINITY;
277
263
  return tool.createdAt >= start && tool.createdAt < end;
278
264
  });
265
+ // Time window only. Never dump unmatched tools onto the latest group:
266
+ // that is how a previous turn's bash cards jump under a new prompt.
279
267
  if (windowGroup) {
280
268
  windowGroup.tools.push(tool);
281
269
  assigned.add(tool.id);
282
270
  }
283
- else {
284
- const lastGroup = groups.at(-1);
285
- if (lastGroup) {
286
- lastGroup.tools.push(tool);
287
- assigned.add(tool.id);
288
- }
289
- }
290
271
  }
291
272
  for (const group of groups) {
292
273
  group.tools.sort((a, b) => {
@@ -303,6 +284,14 @@ export function buildTurnProcessGroups(input) {
303
284
  }
304
285
  return groups;
305
286
  }
287
+ /** Keep the most recent complete turns. Slice groups, never raw messages. */
288
+ export function selectRecentTurnGroups(groups, limit) {
289
+ if (limit <= 0)
290
+ return [];
291
+ if (groups.length <= limit)
292
+ return [...groups];
293
+ return groups.slice(-limit);
294
+ }
306
295
  export function processGroupSummary(tools, options) {
307
296
  const count = tools.length;
308
297
  const thinkingCount = options?.thinkingCount ?? 0;
@@ -324,7 +313,7 @@ export function processGroupSummary(tools, options) {
324
313
  parts.push("运行中");
325
314
  return { count, errorCount, runningCount, stepCount, label: parts.join(" · ") };
326
315
  }
327
- /** Summary over rendered process items (tool-group tools count individually). */
316
+ /** Summary over rendered process items. */
328
317
  export function summarizeProcessItems(items) {
329
318
  const tools = [];
330
319
  let thinkingCount = 0;
@@ -344,10 +333,8 @@ export function summarizeProcessItems(items) {
344
333
  else if (item.activity.kind === "progress")
345
334
  activityRunningCount += 1;
346
335
  }
347
- else if (item.kind === "tool")
348
- tools.push(item.tool);
349
336
  else
350
- tools.push(...item.tools);
337
+ tools.push(item.tool);
351
338
  }
352
339
  return processGroupSummary(tools, {
353
340
  thinkingCount,
@@ -0,0 +1,137 @@
1
+ /**
2
+ * harness.json executors.pi LOW/MED/HIGH 模型矩阵的读写与校验。
3
+ *
4
+ * 该模块只服务于 Console「模型与认证」页的模型矩阵编辑器:
5
+ * - `validatePiModelMatrixTiers`:写入前拒绝空串 / default 哨兵 / 非字符串 /
6
+ * 畸形 provider/model(如 provider/、/model、a//b)。
7
+ * - `readPiModelMatrixProjection`:读全文档并投影三档的 provider/model 或裸
8
+ * model id(供 readiness 结构化投影 piModelMatrix 使用)。
9
+ * - `writePiModelMatrix`:读全文档 → 校验 → 原子写 tmp + fsync + rename,
10
+ * 保留 $schema 与其余所有字段。
11
+ *
12
+ * 本模块在 worker/console(Node 侧),不得被浏览器静态资源直接 import
13
+ * (依赖 node:fs / node:crypto / node:path)。
14
+ */
15
+ import { randomBytes } from "node:crypto";
16
+ import { open, readFile, rename, writeFile } from "node:fs/promises";
17
+ import path from "node:path";
18
+ import { normalizeExecutorTierValue } from "../../governance/manifest-types.js";
19
+ export const PI_MODEL_TIERS = ["LOW", "MED", "HIGH"];
20
+ /** 校验单档字符串值,返回错误文案;合法返回 undefined。 */
21
+ export function piModelTierValueError(value) {
22
+ if (typeof value !== "string")
23
+ return "模型值必须是字符串";
24
+ if (value.length === 0)
25
+ return "模型值不能为空";
26
+ if (value.trim() !== value)
27
+ return "模型值不能包含首尾空白";
28
+ if (value === "default")
29
+ return "default 是保留哨兵,不能作为模型值";
30
+ // 比 normalizeExecutorTierValue 更严格:拒绝 provider/、/model、a//b。
31
+ if (value.startsWith("/") || value.endsWith("/") || value.includes("//")) {
32
+ return "provider/model 格式非法:不允许 /model、provider/ 或 a//b";
33
+ }
34
+ return undefined;
35
+ }
36
+ /**
37
+ * 写入前校验三档字符串值。任何一档非法即整体拒绝,保证不落盘。
38
+ */
39
+ export function validatePiModelMatrixTiers(input) {
40
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
41
+ return {
42
+ ok: false,
43
+ code: "INVALID_INPUT",
44
+ message: "模型矩阵必须是 { LOW, MED, HIGH } 对象",
45
+ };
46
+ }
47
+ const record = input;
48
+ const tiers = {};
49
+ for (const tier of PI_MODEL_TIERS) {
50
+ const raw = record[tier];
51
+ const error = piModelTierValueError(raw);
52
+ if (error) {
53
+ return {
54
+ ok: false,
55
+ code: "INVALID_INPUT",
56
+ message: `${tier}: ${error}`,
57
+ tier,
58
+ };
59
+ }
60
+ tiers[tier] = raw;
61
+ }
62
+ return { ok: true, tiers };
63
+ }
64
+ async function readHarnessJson(repoRoot) {
65
+ try {
66
+ const raw = await readFile(path.join(repoRoot, "harness.json"), "utf8");
67
+ const parsed = JSON.parse(raw);
68
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
69
+ return undefined;
70
+ }
71
+ return parsed;
72
+ }
73
+ catch {
74
+ return undefined;
75
+ }
76
+ }
77
+ /**
78
+ * 投影 harness.json executors.pi 三档(provider/model 或裸 model id)。
79
+ * 缺文件 / 非法 JSON / 未配置档位均优雅降级为缺省,绝不抛错。
80
+ */
81
+ export async function readPiModelMatrixProjection(repoRoot) {
82
+ const manifest = await readHarnessJson(repoRoot);
83
+ const pi = manifest?.executors?.pi;
84
+ const projection = {};
85
+ if (!pi || typeof pi !== "object")
86
+ return projection;
87
+ for (const tier of PI_MODEL_TIERS) {
88
+ const selection = normalizeExecutorTierValue(pi[tier]);
89
+ if (!selection)
90
+ continue;
91
+ projection[tier] = {
92
+ model: selection.model,
93
+ ...(selection.provider ? { provider: selection.provider } : {}),
94
+ value: selection.provider
95
+ ? `${selection.provider}/${selection.model}`
96
+ : selection.model,
97
+ };
98
+ }
99
+ return projection;
100
+ }
101
+ /**
102
+ * 原子更新 harness.json executors.pi.LOW/MED/HIGH:
103
+ * 读全文档 → 校验 → 保留其余字段 → 写 tmp + fsync + rename。
104
+ */
105
+ export async function writePiModelMatrix(repoRoot, tiers) {
106
+ const validation = validatePiModelMatrixTiers(tiers);
107
+ if (!validation.ok) {
108
+ throw Object.assign(new Error(validation.message), {
109
+ code: validation.code,
110
+ });
111
+ }
112
+ const file = path.join(repoRoot, "harness.json");
113
+ const manifest = await readHarnessJson(repoRoot);
114
+ if (!manifest) {
115
+ throw Object.assign(new Error("harness.json 不存在或不是合法 JSON"), {
116
+ code: "HARNESS_NOT_FOUND",
117
+ });
118
+ }
119
+ const executors = (manifest.executors ?? {});
120
+ const pi = (executors.pi ?? {});
121
+ const nextPi = { ...pi };
122
+ for (const tier of PI_MODEL_TIERS) {
123
+ nextPi[tier] = validation.tiers[tier];
124
+ }
125
+ const nextManifest = { ...manifest, executors: { ...executors, pi: nextPi } };
126
+ const body = `${JSON.stringify(nextManifest, null, 2)}\n`;
127
+ const tmp = path.join(repoRoot, `.harness.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
128
+ await writeFile(tmp, body, { encoding: "utf8" });
129
+ const handle = await open(tmp, "r+");
130
+ try {
131
+ await handle.sync();
132
+ }
133
+ finally {
134
+ await handle.close();
135
+ }
136
+ await rename(tmp, file);
137
+ }
@@ -1,5 +1,5 @@
1
1
  import { access, readFile } from "node:fs/promises";
2
- import { buildOperatorCapabilitiesDocument } from "../../shared/operator/capabilities.js";
2
+ import { buildOperatorCapabilitiesDocument, OFFICIAL_TAIL_ACTION_IDS, } from "../../shared/operator/capabilities.js";
3
3
  import { buildOperatorResult, operatorFailed, operatorSucceeded, } from "../../shared/operator/envelope.js";
4
4
  import { CANONICAL_TASK_ID_PATTERN, formatLocalBusinessDate, getTaskPaths, loadTaskConfig, } from "../../task/runtime.js";
5
5
  import { diffTaskContract, validateDraftForTask, } from "../../task/contract/index.js";
@@ -11,6 +11,7 @@ import { resolveLatestDagRunIdForTask } from "./resolve-dag-run-for-task.js";
11
11
  import { repoFingerprintV1 } from "./repo-fingerprint.js";
12
12
  import { scheduleOperation, } from "./operation-runner.js";
13
13
  import { readinessGateFailure, } from "./pi-readiness.js";
14
+ import { validatePiModelMatrixTiers, writePiModelMatrix, } from "./harness-pi-model-matrix.js";
14
15
  import { isAssessmentFreshForDraft } from "./interview/assessment.js";
15
16
  import { applyGrillMeAnswer, draftSummary, emptyDraft, isDraftStructurallyComplete, nextGrillMeQuestion, } from "./interview/grill-me.js";
16
17
  import { toCanonicalDraftForCli, } from "./draft-store.js";
@@ -671,6 +672,80 @@ export async function dispatchOperatorAction(ctx, req) {
671
672
  const body = await runReadCli(ctx, ["dag", "report", "--run-id", runId, "--json"], "dag report");
672
673
  return { kind: "sync", status: body.ok ? 200 : 400, body };
673
674
  }
675
+ case "dagDoctor": {
676
+ const runId = str(p.runId) ?? str(p.dagRunId);
677
+ const args = ["dag", "doctor"];
678
+ if (runId)
679
+ args.push("--run-id", runId);
680
+ const body = await runReadCli(ctx, args, "dag doctor");
681
+ return { kind: "sync", status: body.ok ? 200 : 400, body };
682
+ }
683
+ case "dagStatus": {
684
+ const runId = str(p.runId) ?? str(p.dagRunId);
685
+ if (!runId)
686
+ return invalid(action, "runId is required");
687
+ const body = await runReadCli(ctx, ["dag", "status", "--run-id", runId], "dag status");
688
+ return { kind: "sync", status: body.ok ? 200 : 400, body };
689
+ }
690
+ case "workerPoolDoctor": {
691
+ const taskId = str(p.taskId);
692
+ const run = ctx.runCommand ??
693
+ ((args, options) => ctx.client.runExternal
694
+ ? ctx.client.runExternal(resolveSiblingAgentWorkerBin(), args, options)
695
+ : ctx.client.run(args, options));
696
+ let result;
697
+ try {
698
+ result = await run(["pool", "doctor", "--repo", ctx.repoRoot, "--json"], {
699
+ cwd: ctx.repoRoot,
700
+ artifactName: "read-pool-doctor",
701
+ expectJson: true,
702
+ });
703
+ }
704
+ catch (error) {
705
+ return {
706
+ kind: "error",
707
+ status: 400,
708
+ body: operatorFailed({
709
+ command: action,
710
+ outcome: "invalid",
711
+ code: "INVALID_INPUT",
712
+ message: `cannot read pool doctor facts: ${error instanceof Error ? error.message : String(error)}`,
713
+ }),
714
+ };
715
+ }
716
+ if (!result.ok || result.exitCode !== 0 || !result.json) {
717
+ return {
718
+ kind: "error",
719
+ status: 400,
720
+ body: operatorFailed({
721
+ command: action,
722
+ outcome: "invalid",
723
+ code: "INVALID_INPUT",
724
+ message: summarizeReadCliFailure(result),
725
+ }),
726
+ };
727
+ }
728
+ const inventory = result.json;
729
+ if (!taskId) {
730
+ return {
731
+ kind: "sync",
732
+ status: 200,
733
+ body: operatorSucceeded(action, inventory),
734
+ };
735
+ }
736
+ const v2 = (inventory.v2 ?? []).filter((entry) => entry.taskId === taskId);
737
+ const legacy = (inventory.legacy ?? []).filter((entry) => entry.taskId === taskId);
738
+ return {
739
+ kind: "sync",
740
+ status: 200,
741
+ body: operatorSucceeded(action, {
742
+ ...inventory,
743
+ v2,
744
+ legacy,
745
+ focusedTaskId: taskId,
746
+ }),
747
+ };
748
+ }
674
749
  case "status": {
675
750
  const taskId = str(p.taskId);
676
751
  if (!taskId) {
@@ -985,7 +1060,7 @@ export async function dispatchOperatorAction(ctx, req) {
985
1060
  }
986
1061
  if (operatorAction) {
987
1062
  const target = buildOperatorCapabilitiesDocument().actions.find((entry) => entry.action === operatorAction &&
988
- entry.description.startsWith("Official CLI action coverage") &&
1063
+ OFFICIAL_TAIL_ACTION_IDS.has(entry.action) &&
989
1064
  entry.humanConfirmation === "required");
990
1065
  if (!target)
991
1066
  return invalid(action, "operatorAction must name a required official action");
@@ -2483,12 +2558,61 @@ export async function dispatchOperatorAction(ctx, req) {
2483
2558
  }),
2484
2559
  };
2485
2560
  }
2561
+ case "savePiModelMatrix": {
2562
+ // 「模型与认证」页模型矩阵编辑器:三档字符串值经校验后原子写入
2563
+ // harness.json executors.pi,并自动重测 readiness。写路径本身由 HTTP
2564
+ // 层 evaluateMutationGate 拦截(cookie + confirmation 头 + origin)。
2565
+ const matrixInput = p.matrix ?? p;
2566
+ const validation = validatePiModelMatrixTiers(matrixInput);
2567
+ if (!validation.ok) {
2568
+ return {
2569
+ kind: "error",
2570
+ status: 400,
2571
+ body: operatorFailed({
2572
+ command: action,
2573
+ outcome: "invalid",
2574
+ code: validation.code,
2575
+ message: validation.message,
2576
+ ...(validation.tier ? { details: { tier: validation.tier } } : {}),
2577
+ }),
2578
+ };
2579
+ }
2580
+ try {
2581
+ await writePiModelMatrix(ctx.repoRoot, validation.tiers);
2582
+ }
2583
+ catch (error) {
2584
+ return {
2585
+ kind: "error",
2586
+ status: 500,
2587
+ body: operatorFailed({
2588
+ command: action,
2589
+ outcome: "blocked",
2590
+ code: "PI_MODEL_MATRIX_WRITE_FAILED",
2591
+ message: error instanceof Error ? error.message : String(error),
2592
+ }),
2593
+ };
2594
+ }
2595
+ // 保存成功自动重测 readiness(复用 probePiReadiness),让页面立即
2596
+ // 展示新的 modelsAvailable / modelsMessage / piModelMatrix(AC-4)。
2597
+ const report = ctx.refreshReadiness
2598
+ ? await ctx.refreshReadiness()
2599
+ : ctx.getReadiness?.() ?? ctx.readiness;
2600
+ return {
2601
+ kind: "sync",
2602
+ status: 200,
2603
+ body: operatorSucceeded("savePiModelMatrix", {
2604
+ tiers: validation.tiers,
2605
+ readiness: report,
2606
+ }),
2607
+ };
2608
+ }
2486
2609
  default: {
2487
2610
  // M5 official-command coverage uses one generic tail for newly registered
2488
2611
  // CLI actions. It remains inside dispatchOperatorAction (the sole dispatch
2489
2612
  // surface) and only accepts argv arrays declared by the capability schema.
2490
- const capability = buildOperatorCapabilitiesDocument().actions.find((entry) => entry.action === action &&
2491
- entry.description.startsWith("Official CLI action coverage"));
2613
+ // 2026-08-18: high-frequency entries got real descriptions; membership is
2614
+ // now derived from OFFICIAL_TAIL_ACTION_IDS, not description prefixes.
2615
+ const capability = buildOperatorCapabilitiesDocument().actions.find((entry) => entry.action === action && OFFICIAL_TAIL_ACTION_IDS.has(entry.action));
2492
2616
  if (capability) {
2493
2617
  const rawArgs = p.args;
2494
2618
  if (rawArgs !== undefined &&
@@ -1,4 +1,19 @@
1
1
  import { rewriteRecoveryOperatorError } from "./recovery-error-copy.js";
2
+ /** Mutation-gate raw English messages (worker/console/security.ts) → copy. */
3
+ const MUTATION_GATE_COPY = [
4
+ {
5
+ pattern: /invalid (boot capability|confirmation header) token/i,
6
+ copy: "页面已过期(控制台已重启),请刷新页面(F5)后重试。",
7
+ },
8
+ {
9
+ pattern: /mutation requires boot capability cookie|mutation requires confirmation header token/i,
10
+ copy: "页面状态异常,请刷新页面(F5)后重试。",
11
+ },
12
+ {
13
+ pattern: /Host header is required for non-loopback/i,
14
+ copy: "请求主机校验失败:非回环绑定时要求 Host 头,请刷新页面或检查访问地址。",
15
+ },
16
+ ];
2
17
  /** Map internal spawn/controller errors to actionable Chinese copy. */
3
18
  export function formatOperatorUserError(message) {
4
19
  const raw = (message ?? "").trim();
@@ -22,5 +37,9 @@ export function formatOperatorUserError(message) {
22
37
  if (/force-required|requires --force/i.test(raw)) {
23
38
  return "该终态需要强制 Discard。请确认后勾选强制再试。";
24
39
  }
40
+ for (const entry of MUTATION_GATE_COPY) {
41
+ if (entry.pattern.test(raw))
42
+ return entry.copy;
43
+ }
25
44
  return raw;
26
45
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import path from "node:path";
6
6
  import { mapSnapshotAvailableModels, resolveHarnessPiExecutorMatrix, } from "./chat/model-resolver.js";
7
+ import { readPiModelMatrixProjection, } from "./harness-pi-model-matrix.js";
7
8
  const SETUP_BLOCKED = Object.freeze([
8
9
  "newTask",
9
10
  "importPrd",
@@ -221,6 +222,7 @@ export async function discoverPiReadinessProbe(options) {
221
222
  authReady,
222
223
  modelsAvailable,
223
224
  interviewSessionOk,
225
+ piAvailableModels: availableModels,
224
226
  details: {
225
227
  sdkMessage: `Pi SDK 0.80.10 contract available (agentDir=${agentDir})`,
226
228
  authMessage: authReady
@@ -238,13 +240,18 @@ export async function discoverPiReadinessProbe(options) {
238
240
  * override is injected. Tests inject override for determinism.
239
241
  */
240
242
  export async function probePiReadiness(options) {
243
+ const repoRoot = options?.repoRoot ?? process.cwd();
244
+ // 模型矩阵投影与 SDK 探测解耦:即使 SDK 不可用 / 测试注入 override,仍要
245
+ // 投影 harness.json 三档值供模型矩阵编辑器预填(AC-1 / AC-4)。
246
+ const piModelMatrix = await readPiModelMatrixProjection(repoRoot);
241
247
  if (options?.override) {
242
- return buildPiReadinessReport(options.override);
248
+ return buildPiReadinessReport({ ...options.override, piModelMatrix });
243
249
  }
244
250
  try {
245
251
  const probe = await discoverPiReadinessProbe({
246
252
  repoRoot: options?.repoRoot,
247
253
  });
254
+ probe.piModelMatrix = piModelMatrix;
248
255
  if (options?.siblingControllerOk && !probe.details) {
249
256
  probe.details = {};
250
257
  }