@tea-agent/loop-agent 0.38.1 → 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 (127) hide show
  1. package/CHANGELOG.md +109 -13
  2. package/dist/application/dag/generate-task-dag.js +9 -0
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/commands/init.js +2 -14
  5. package/dist/executors/dag-pi-executor.js +41 -4
  6. package/dist/executors/shell-executor.js +269 -15
  7. package/dist/governance/manifest-types.js +0 -2
  8. package/dist/shared/operator/capabilities.js +87 -22
  9. package/dist/task/config-types.js +22 -0
  10. package/dist/worker/console/chat/deferred-turn.js +91 -0
  11. package/dist/worker/console/chat/pi-runtime.js +10 -5
  12. package/dist/worker/console/chat/routes.js +458 -47
  13. package/dist/worker/console/chat/semantic-activity.js +13 -13
  14. package/dist/worker/console/chat/session-store.js +146 -2
  15. package/dist/worker/console/harness-pi-model-matrix.js +2 -11
  16. package/dist/worker/console/operator-actions.js +5 -4
  17. package/dist/worker/console/pi-readiness.js +1 -0
  18. package/dist/worker/console/recovery-cta.js +4 -4
  19. package/dist/worker/console/recovery-error-copy.js +4 -4
  20. package/dist/worker/console/server.js +6 -1
  21. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Hfsot6rP.js → abnfDiagram-N423BO3Z-C4bmo3iY.js} +1 -1
  22. package/dist/worker/console/static/assets/{arc-YGwEASin.js → arc-SAD7McOS.js} +1 -1
  23. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CmOM0F92.js → architectureDiagram-T3A2C74G-DAVLUdK1.js} +1 -1
  24. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DDLuaHC5.js → blockDiagram-VBNYF7ZC-CPPdv4vj.js} +1 -1
  25. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B0Um43W4.js → c4Diagram-5PPSVZJV-D1GRGZax.js} +1 -1
  26. package/dist/worker/console/static/assets/channel-R6oQIDkw.js +1 -0
  27. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DvBN4JZc.js → chunk-2GRJ4B5K-D_Bq2ZqK.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B1UVgW4E.js → chunk-2Q5K7J3B-CPLVtamK.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Cf0GgXzX.js → chunk-5RXB4S5H-DnaZWcL3.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DbNA24sj.js → chunk-5VM5RSS4-BRTmFgnq.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bi_0Be06.js → chunk-6Q2QTUOP-BRlfYZoH.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C8pPGvRd.js → chunk-GF5L2VYU-CjNybD4u.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BitQY8Z-.js → chunk-JWPE2WC7-D3e-74fG.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-U67GCiEo.js → chunk-KBJHAD2P-Br3ir6f2.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-IoJwXPBA.js → chunk-RYQCIY6F-1NgVuRbh.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-YDZ_UHgE.js → chunk-XXDRQBXY-BTYuvuWN.js} +1 -1
  37. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +1 -0
  38. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +1 -0
  39. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CUZgsiRC.js → cose-bilkent-JH36ORCC-BC6Bgosf.js} +1 -1
  40. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-COmCh4pT.js → cynefin-VYW2F7L2-DV-HypfL.js} +1 -1
  41. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Ta6FSm68.js → cynefinDiagram-MW4NZA55-DhTaddO-.js} +1 -1
  42. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UdcR-VW9.js → dagre-VZM6K2ZE-BnEcOmiI.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DybcO3Et.js → diagram-7IWD3JNH-xphGRVKp.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-wErDFWLw.js → diagram-B4RE2ZJO-CrdD1mRG.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DkVHZWqz.js → diagram-LBJQPF4R-D4YdLsaF.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DdqWs7QN.js → diagram-Q27KOJAE-LCKyGZ5U.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-UB23O5K3-C9dMVmDo.js → diagram-UB23O5K3-BeiTe7t0.js} +1 -1
  48. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-B2T2WcBK.js → ebnfDiagram-BXEA7PRR-BO2cq8GS.js} +1 -1
  49. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bt9rjDFq.js → erDiagram-JOGREHBK-CmWww3Nt.js} +1 -1
  50. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-U8ic_tvi.js → flowDiagram-UKHOOZJN-C2kkUYg5.js} +1 -1
  51. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BfQJdQoO.js → ganttDiagram-PKOTCBZU-DY_JZ2iQ.js} +1 -1
  52. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CpMftH4O.js → gitGraphDiagram-DS77QQ5N-BizhCSw9.js} +1 -1
  53. package/dist/worker/console/static/assets/index-BXN89VPX.js +325 -0
  54. package/dist/worker/console/static/assets/index-CuH5CNFu.css +1 -0
  55. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DzqWXNO_.js → infoDiagram-6WML65LV-Dlo2eK_d.js} +1 -1
  56. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CJmGQ5Yp.js → ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js} +1 -1
  57. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-ChPtKFLz.js → journeyDiagram-NVQOT4AX-DiMDe5Yh.js} +1 -1
  58. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PRe7wB5u.js → kanban-definition-27J2QSJJ-sN553oIN.js} +1 -1
  59. package/dist/worker/console/static/assets/{linear-DpKX6QNp.js → linear-D9Py31Ld.js} +1 -1
  60. package/dist/worker/console/static/assets/{mermaid.core-Dnl76b7Y.js → mermaid.core-D7Occzk_.js} +5 -5
  61. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-S9PCXc3J.js → mindmap-definition-FAOFIHXS-DjDRboiR.js} +1 -1
  62. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Du8e-FSX.js → pegDiagram-VL7TDLO6-Cgc4qlzp.js} +1 -1
  63. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CY6OmHTc.js → pieDiagram-7S7Q4E2Y-DxZj8pKd.js} +1 -1
  64. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C65GPhpj.js → quadrantDiagram-CIZ2JOQS-CJ50MngR.js} +1 -1
  65. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-U6IUiTCL.js → railroadDiagram-AXF67PYL-BY-OD8Tr.js} +1 -1
  66. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-I_9Qhi.js → requirementDiagram-LRYGKXZP-CQFButfK.js} +1 -1
  67. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B57Fhed0.js → sankeyDiagram-W5VNT64P-BB_QUK_b.js} +1 -1
  68. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CJOtiw21.js → sequenceDiagram-SI44F4Z6-CqhXZjyx.js} +1 -1
  69. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-FyX8zxSh.js → sizeCapture-X5ZJPWSS-2l1U4rgJ.js} +1 -1
  70. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-eF-pbs9t.js → stateDiagram-OKZ733FA-BX1FbrhJ.js} +1 -1
  71. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +1 -0
  72. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CcfkA-Dq.js → swimlanes-SLNWSIFB-DOSnT7hb.js} +2 -2
  73. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +8 -0
  74. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DH8FjnOK.js → timeline-definition-Z64GVDOM-DVJQA7AN.js} +1 -1
  75. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-beoTagJg.js → vennDiagram-T6HMQDX7-74uxl9gx.js} +1 -1
  76. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CQSfw4cK.js → wardleyDiagram-T6FBY63Y-DsBxlcg_.js} +1 -1
  77. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CQgzQ6jn.js → xychartDiagram-ELKLHX3M-snuW9Mjw.js} +1 -1
  78. package/dist/worker/console/static/index.html +2 -2
  79. package/dist/worker/console/static-src/app/console-types.js +0 -12
  80. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  81. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  82. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +38 -1
  83. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  84. package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
  85. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
  86. package/dist/worker/console/static-src/operator-chat/useChatStream.js +204 -11
  87. package/dist/worker/console/static-src/operator-chat/useChatThread.js +23 -1
  88. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +755 -51
  89. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  90. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  91. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  92. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  93. package/dist/workflows/dag/backend-test-layout.js +133 -0
  94. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  95. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  96. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  97. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  98. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  99. package/dist/workflows/dag/backend-test-scenario-partitions.js +249 -0
  100. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  101. package/dist/workflows/dag/contract-validator-registrations.js +2 -1
  102. package/dist/workflows/dag/convergence/controller.js +141 -0
  103. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  104. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  105. package/dist/workflows/dag/frontend-implementation-contract.js +236 -8
  106. package/dist/workflows/dag/frontend-prewrite-gate.js +95 -35
  107. package/dist/workflows/dag/init-hybrid.js +652 -87
  108. package/dist/workflows/dag/node-execution.js +45 -18
  109. package/dist/workflows/dag/prompt.js +45 -0
  110. package/dist/workflows/dag/rerun-feedback.js +212 -0
  111. package/dist/workflows/dag/rerun-plan.js +93 -27
  112. package/dist/workflows/dag/rerun-run.js +3 -0
  113. package/dist/workflows/dag/rerun-task.js +61 -2
  114. package/dist/workflows/dag/retry-policy.js +18 -0
  115. package/dist/workflows/dag/types.js +98 -0
  116. package/docs/templates/backend-test-dag.json +60 -35
  117. package/docs/templates/harness.schema.json +0 -8
  118. package/harness.json +1 -8
  119. package/package.json +1 -1
  120. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  121. package/dist/worker/console/static/assets/channel-Dp_2Aqf1.js +0 -1
  122. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL--CODy37j.js +0 -1
  123. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE--CODy37j.js +0 -1
  124. package/dist/worker/console/static/assets/index-BA5-N5ER.css +0 -1
  125. package/dist/worker/console/static/assets/index-DKPXBrdc.js +0 -325
  126. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B-mvnLEc.js +0 -1
  127. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-JtpBmIv4.js +0 -8
@@ -17,6 +17,7 @@ import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_
17
17
  import { buildModelCallableToolSchemas } from "./tool-adapter.js";
18
18
  import { cleanChatTitle, acceptedOperationIdFromResult } from "./session-store.js";
19
19
  import { hashChatTurnPayload, projectChatToolEventPayload, projectCompactSnapshot, } from "./chat-event-store.js";
20
+ import { projectDeferredTurnSummary, } from "./deferred-turn.js";
20
21
  import { THINKING_LEVELS, RuntimeSnapshotUnavailableError, } from "./pi-runtime.js";
21
22
  import { ResourcePreferencesError, setSkillAutoInvocationPreference, } from "./resource-preferences-store.js";
22
23
  import { ComposerDraftStore } from "./composer-draft-store.js";
@@ -645,19 +646,195 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
645
646
  const composerDraft = await new ComposerDraftStore(deps.appData).get(sessionId);
646
647
  const snapshot = deps.events.snapshot(sessionId);
647
648
  const lastEvent = snapshot.at(-1);
649
+ const activeTurn = deps.events.getActiveTurn(sessionId);
648
650
  sendJson(res, 200, {
649
651
  ok: true,
650
652
  eventEpoch: deps.events.epoch(sessionId),
651
653
  session: {
652
654
  ...record,
653
655
  composerDraft,
654
- activeTurnId: deps.events.getActiveTurn(sessionId)?.turnId,
656
+ activeTurnId: activeTurn?.turnId,
657
+ turnPhase: activeTurn
658
+ ? activeTurn.abortRequestedAt
659
+ ? "stopping"
660
+ : "running"
661
+ : "idle",
655
662
  },
656
663
  hasBash: true,
657
664
  lastEventId: lastEvent?.eventId ?? null,
658
665
  lastEventSeq: lastEvent?.seq ?? 0,
659
666
  });
660
667
  }
668
+ function parseDeferredImages(value) {
669
+ const parsed = parseChatImages(value);
670
+ const raw = Array.isArray(value) ? value : [];
671
+ return parsed.map((image, index) => {
672
+ const candidate = raw[index];
673
+ const name = candidate &&
674
+ typeof candidate === "object" &&
675
+ typeof candidate.name === "string"
676
+ ? candidate.name.trim().slice(0, 180)
677
+ : `image-${index + 1}`;
678
+ return { ...image, name: name || `image-${index + 1}` };
679
+ });
680
+ }
681
+ export async function handlePutDeferredTurn(req, res, deps, sessionId) {
682
+ const gate = gateMutation(req, deps);
683
+ if (!gate.ok) {
684
+ sendJson(res, gate.status, {
685
+ ok: false,
686
+ accepted: false,
687
+ error: { code: gate.code, message: gate.message },
688
+ });
689
+ return;
690
+ }
691
+ let body;
692
+ try {
693
+ body = await readJsonBody(req);
694
+ }
695
+ catch (error) {
696
+ sendJson(res, 400, {
697
+ ok: false,
698
+ accepted: false,
699
+ error: {
700
+ code: "INVALID_INPUT",
701
+ message: error instanceof Error ? error.message : String(error),
702
+ },
703
+ });
704
+ return;
705
+ }
706
+ const clientRequestId = typeof body.clientRequestId === "string" ? body.clientRequestId.trim() : "";
707
+ const blockedByTurnId = typeof body.blockedByTurnId === "string" ? body.blockedByTurnId.trim() : "";
708
+ const text = typeof body.text === "string" ? body.text : "";
709
+ const expectedRevision = typeof body.expectedRevision === "number" &&
710
+ Number.isSafeInteger(body.expectedRevision) &&
711
+ body.expectedRevision >= 0
712
+ ? body.expectedRevision
713
+ : undefined;
714
+ let images;
715
+ try {
716
+ images = parseDeferredImages(body.images);
717
+ }
718
+ catch (error) {
719
+ sendJson(res, 400, {
720
+ ok: false,
721
+ accepted: false,
722
+ error: {
723
+ code: "INVALID_ATTACHMENT",
724
+ message: error instanceof Error ? error.message : String(error),
725
+ },
726
+ });
727
+ return;
728
+ }
729
+ const active = deps.events.getActiveTurn(sessionId);
730
+ if (!active || !active.abortRequestedAt || active.turnId !== blockedByTurnId) {
731
+ sendJson(res, 409, {
732
+ ok: false,
733
+ accepted: false,
734
+ error: {
735
+ code: "TURN_NOT_STOPPING",
736
+ message: "只有仍在停止中的当前 Turn 可以保存停止后发送消息",
737
+ },
738
+ activeTurn: active ? turnSummary(active) : null,
739
+ });
740
+ return;
741
+ }
742
+ const result = await deps.store.putDeferredTurn(sessionId, {
743
+ clientRequestId,
744
+ blockedByTurnId,
745
+ text,
746
+ images,
747
+ ...(expectedRevision !== undefined ? { expectedRevision } : {}),
748
+ });
749
+ if (!result.ok) {
750
+ const status = result.code === "NOT_FOUND"
751
+ ? 404
752
+ : result.code === "INVALID_INPUT"
753
+ ? 400
754
+ : 409;
755
+ sendJson(res, status, {
756
+ ok: false,
757
+ accepted: false,
758
+ error: {
759
+ code: result.code,
760
+ message: result.code === "REVISION_CONFLICT"
761
+ ? "待发送消息已在其他页面更新,请使用服务端最新状态"
762
+ : result.code === "NOT_REPLACEABLE"
763
+ ? "待发送消息已经开始发送,不能再覆盖"
764
+ : result.code === "REQUEST_ID_REUSE_CONFLICT"
765
+ ? "clientRequestId 已绑定到不同消息"
766
+ : "待发送消息无效或会话不存在",
767
+ },
768
+ deferredTurn: result.deferredTurn ?? null,
769
+ summary: projectDeferredTurnSummary(result.deferredTurn),
770
+ });
771
+ return;
772
+ }
773
+ emitDeferredTurnUpdate(deps, sessionId, blockedByTurnId, result.deferredTurn, result.deferredTurn.revision);
774
+ sendJson(res, 200, {
775
+ ok: true,
776
+ accepted: true,
777
+ idempotent: result.idempotent,
778
+ state: result.deferredTurn.state,
779
+ revision: result.deferredTurn.revision,
780
+ deferredTurn: result.deferredTurn,
781
+ summary: projectDeferredTurnSummary(result.deferredTurn),
782
+ });
783
+ void dispatchDeferredTurn(deps, sessionId);
784
+ }
785
+ export async function handleDeleteDeferredTurn(req, res, deps, sessionId) {
786
+ const gate = gateMutation(req, deps);
787
+ if (!gate.ok) {
788
+ sendJson(res, gate.status, {
789
+ ok: false,
790
+ error: { code: gate.code, message: gate.message },
791
+ });
792
+ return;
793
+ }
794
+ let body;
795
+ try {
796
+ body = await readJsonBody(req);
797
+ }
798
+ catch (error) {
799
+ sendJson(res, 400, {
800
+ ok: false,
801
+ error: {
802
+ code: "INVALID_INPUT",
803
+ message: error instanceof Error ? error.message : String(error),
804
+ },
805
+ });
806
+ return;
807
+ }
808
+ const clientRequestId = typeof body.clientRequestId === "string" ? body.clientRequestId.trim() : "";
809
+ const expectedRevision = typeof body.expectedRevision === "number" && Number.isSafeInteger(body.expectedRevision)
810
+ ? body.expectedRevision
811
+ : -1;
812
+ const result = await deps.store.deleteDeferredTurn(sessionId, {
813
+ clientRequestId,
814
+ expectedRevision,
815
+ });
816
+ if (!result.ok) {
817
+ sendJson(res, result.code === "NOT_FOUND" ? 404 : 409, {
818
+ ok: false,
819
+ error: {
820
+ code: result.code,
821
+ message: result.code === "NOT_CANCELLABLE"
822
+ ? "消息已经开始发送,不能再撤回"
823
+ : "待发送消息已变化,请刷新后重试",
824
+ },
825
+ deferredTurn: result.deferredTurn ?? null,
826
+ });
827
+ return;
828
+ }
829
+ const clearedRevision = result.deferredTurn.revision + 1;
830
+ emitDeferredTurnUpdate(deps, sessionId, result.deferredTurn.blockedByTurnId, undefined, clearedRevision);
831
+ sendJson(res, 200, {
832
+ ok: true,
833
+ cancelled: true,
834
+ deferredTurn: result.deferredTurn,
835
+ revision: clearedRevision,
836
+ });
837
+ }
661
838
  /** Lightweight busy-truth endpoint for the client reconcile loop (pi-web
662
839
  * get_state equivalent): exposes the active turn + the latest turn's durable
663
840
  * outcome WITHOUT the full message payload, so the UI can poll it on an
@@ -684,6 +861,14 @@ export async function handleGetChatSessionState(_req, res, deps, sessionId) {
684
861
  isCompacting: false,
685
862
  isBashRunning: false,
686
863
  };
864
+ const runtimeBusy = Object.values(runtimeFacts).some((value) => value === true);
865
+ const turnPhase = active
866
+ ? active.abortRequestedAt
867
+ ? "stopping"
868
+ : "running"
869
+ : runtimeBusy
870
+ ? "running"
871
+ : "idle";
687
872
  sendJson(res, 200, {
688
873
  ok: true,
689
874
  state: {
@@ -695,6 +880,8 @@ export async function handleGetChatSessionState(_req, res, deps, sessionId) {
695
880
  activeTurn: active ? turnSummary(active) : null,
696
881
  latestTurn: latest ? turnSummary(latest) : null,
697
882
  runtime: runtimeFacts,
883
+ turnPhase,
884
+ deferredTurn: projectDeferredTurnSummary(record.deferredTurn),
698
885
  },
699
886
  });
700
887
  }
@@ -703,10 +890,19 @@ export async function handleListChatSessions(_req, res, deps) {
703
890
  sendJson(res, 200, {
704
891
  ok: true,
705
892
  schemaVersion: 2,
706
- sessions: sessions.map((session) => ({
707
- ...session,
708
- activeTurnId: deps.events.getActiveTurn(session.sessionId)?.turnId,
709
- })),
893
+ sessions: sessions.map((session) => {
894
+ const active = deps.events.getActiveTurn(session.sessionId);
895
+ return {
896
+ ...session,
897
+ activeTurnId: active?.turnId,
898
+ turnPhase: active?.abortRequestedAt
899
+ ? "stopping"
900
+ : active
901
+ ? "running"
902
+ : "idle",
903
+ deferredTurn: projectDeferredTurnSummary(session.deferredTurn),
904
+ };
905
+ }),
710
906
  hasBash: true,
711
907
  });
712
908
  }
@@ -1057,49 +1253,63 @@ export async function handleChatPrompt(req, res, deps, sessionId) {
1057
1253
  * Pi prompt continues independently of this HTTP response and publishes to the
1058
1254
  * persisted event ring consumed by GET /events.
1059
1255
  */
1060
- export async function handleCreateChatTurn(req, res, deps, sessionId) {
1061
- const gate = gateMutation(req, deps);
1062
- if (!gate.ok) {
1063
- sendJson(res, gate.status, {
1064
- ok: false,
1065
- accepted: false,
1066
- error: { code: gate.code, message: gate.message },
1067
- });
1068
- return;
1069
- }
1070
- let body;
1071
- try {
1072
- body = await readJsonBody(req);
1073
- }
1074
- catch (error) {
1075
- sendJson(res, 400, {
1076
- ok: false,
1077
- accepted: false,
1078
- error: {
1079
- code: "INVALID_INPUT",
1080
- message: error instanceof Error ? error.message : String(error),
1081
- },
1082
- });
1083
- return;
1084
- }
1085
- const text = typeof body.text === "string" ? body.text.trim() : "";
1086
- const clientRequestId = typeof body.clientRequestId === "string" && body.clientRequestId.trim()
1087
- ? body.clientRequestId.trim()
1088
- : undefined;
1256
+ export async function handleCreateChatTurn(req, res, deps, sessionId, trustedPayload) {
1257
+ let text;
1258
+ let clientRequestId;
1089
1259
  let images;
1090
- try {
1091
- images = parseChatImages(body.images);
1260
+ if (trustedPayload) {
1261
+ text = trustedPayload.text.trim();
1262
+ clientRequestId = trustedPayload.clientRequestId.trim();
1263
+ images = trustedPayload.images.map(({ type, mimeType, data }) => ({
1264
+ type,
1265
+ mimeType,
1266
+ data,
1267
+ }));
1092
1268
  }
1093
- catch (error) {
1094
- sendJson(res, 400, {
1095
- ok: false,
1096
- accepted: false,
1097
- error: {
1098
- code: "INVALID_ATTACHMENT",
1099
- message: error instanceof Error ? error.message : String(error),
1100
- },
1101
- });
1102
- return;
1269
+ else {
1270
+ const gate = gateMutation(req, deps);
1271
+ if (!gate.ok) {
1272
+ sendJson(res, gate.status, {
1273
+ ok: false,
1274
+ accepted: false,
1275
+ error: { code: gate.code, message: gate.message },
1276
+ });
1277
+ return;
1278
+ }
1279
+ let body;
1280
+ try {
1281
+ body = await readJsonBody(req);
1282
+ }
1283
+ catch (error) {
1284
+ sendJson(res, 400, {
1285
+ ok: false,
1286
+ accepted: false,
1287
+ error: {
1288
+ code: "INVALID_INPUT",
1289
+ message: error instanceof Error ? error.message : String(error),
1290
+ },
1291
+ });
1292
+ return;
1293
+ }
1294
+ text = typeof body.text === "string" ? body.text.trim() : "";
1295
+ clientRequestId =
1296
+ typeof body.clientRequestId === "string" && body.clientRequestId.trim()
1297
+ ? body.clientRequestId.trim()
1298
+ : undefined;
1299
+ try {
1300
+ images = parseChatImages(body.images);
1301
+ }
1302
+ catch (error) {
1303
+ sendJson(res, 400, {
1304
+ ok: false,
1305
+ accepted: false,
1306
+ error: {
1307
+ code: "INVALID_ATTACHMENT",
1308
+ message: error instanceof Error ? error.message : String(error),
1309
+ },
1310
+ });
1311
+ return;
1312
+ }
1103
1313
  }
1104
1314
  if (!text) {
1105
1315
  sendJson(res, 400, {
@@ -1235,7 +1445,8 @@ export async function handleCreateChatTurn(req, res, deps, sessionId) {
1235
1445
  text,
1236
1446
  images,
1237
1447
  signal: controller.signal,
1238
- onUserMessagePersisted: (record) => {
1448
+ onUserMessagePersisted: async (record, userMessage) => {
1449
+ await trustedPayload?.onUserMessagePersisted?.(turn.turnId, userMessage);
1239
1450
  const users = record.messages.filter((message) => message.role === "user");
1240
1451
  if (users.length !== 1)
1241
1452
  return;
@@ -1312,9 +1523,200 @@ export async function handleCreateChatTurn(req, res, deps, sessionId) {
1312
1523
  error: terminalError,
1313
1524
  });
1314
1525
  deps.turnExecutions?.clear(sessionId, turn.turnId);
1526
+ void dispatchDeferredTurn(deps, sessionId);
1315
1527
  }
1316
1528
  })();
1317
1529
  }
1530
+ function emitDeferredTurnUpdate(deps, sessionId, turnId, deferredTurn, revision) {
1531
+ deps.events.append(sessionId, turnId, {
1532
+ kind: "queue_update",
1533
+ data: {
1534
+ deferredTurn: projectDeferredTurnSummary(deferredTurn),
1535
+ revision,
1536
+ },
1537
+ });
1538
+ }
1539
+ async function startTrustedChatTurn(deps, sessionId, payload) {
1540
+ let status = 500;
1541
+ let raw = "";
1542
+ const response = {
1543
+ writeHead(code) {
1544
+ status = code;
1545
+ return this;
1546
+ },
1547
+ end(chunk) {
1548
+ if (chunk !== undefined)
1549
+ raw += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;
1550
+ return this;
1551
+ },
1552
+ };
1553
+ await handleCreateChatTurn({}, response, deps, sessionId, payload);
1554
+ let body = {};
1555
+ try {
1556
+ body = raw ? JSON.parse(raw) : {};
1557
+ }
1558
+ catch {
1559
+ body = {};
1560
+ }
1561
+ return { status, body };
1562
+ }
1563
+ const deferredDispatchingSessions = new Set();
1564
+ /** Dispatch one durable follow-up through the exact normal Turn creation path. */
1565
+ export async function dispatchDeferredTurn(deps, sessionId) {
1566
+ if (deferredDispatchingSessions.has(sessionId))
1567
+ return;
1568
+ deferredDispatchingSessions.add(sessionId);
1569
+ try {
1570
+ let record = await deps.store.get(sessionId);
1571
+ let deferred = record?.deferredTurn;
1572
+ if (!deferred)
1573
+ return;
1574
+ if (deferred.state === "accepted")
1575
+ return;
1576
+ if (deferred.state === "failed")
1577
+ return;
1578
+ if (deferred.state === "dispatching") {
1579
+ const existing = deps.events.findTurnByClientRequestId(sessionId, deferred.clientRequestId);
1580
+ if (existing) {
1581
+ const accepted = await deps.store.transitionDeferredTurn(sessionId, {
1582
+ expectedRevision: deferred.revision,
1583
+ state: "accepted",
1584
+ acceptedTurnId: existing.turnId,
1585
+ });
1586
+ if (accepted)
1587
+ emitDeferredTurnUpdate(deps, sessionId, existing.turnId, accepted, accepted.revision);
1588
+ return;
1589
+ }
1590
+ if (deps.events.getActiveTurn(sessionId))
1591
+ return;
1592
+ const requeued = await deps.store.transitionDeferredTurn(sessionId, {
1593
+ expectedRevision: deferred.revision,
1594
+ state: "queued",
1595
+ });
1596
+ if (!requeued)
1597
+ return;
1598
+ deferred = requeued;
1599
+ }
1600
+ const runtimeFacts = deps.runtime.getSessionBusyFacts?.(sessionId);
1601
+ const runtimeBusy = Boolean(runtimeFacts && Object.values(runtimeFacts).some((value) => value === true));
1602
+ if (deps.events.getActiveTurn(sessionId) || runtimeBusy)
1603
+ return;
1604
+ const claim = await deps.store.claimDeferredTurn(sessionId, deferred.revision);
1605
+ if (!claim.ok)
1606
+ return;
1607
+ deferred = claim.deferredTurn;
1608
+ emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, deferred, deferred.revision);
1609
+ const started = await startTrustedChatTurn(deps, sessionId, {
1610
+ clientRequestId: deferred.clientRequestId,
1611
+ text: deferred.text,
1612
+ images: deferred.images,
1613
+ onUserMessagePersisted: async (turnId, userMessage) => {
1614
+ deps.events.append(sessionId, turnId, {
1615
+ kind: "user_message",
1616
+ data: {
1617
+ clientRequestId: deferred.clientRequestId,
1618
+ message: {
1619
+ id: userMessage.id,
1620
+ text: userMessage.text,
1621
+ at: userMessage.at,
1622
+ },
1623
+ },
1624
+ });
1625
+ const cleared = await deps.store.clearDeferredTurnForRequest(sessionId, deferred.clientRequestId);
1626
+ if (cleared)
1627
+ emitDeferredTurnUpdate(deps, sessionId, turnId, undefined, cleared.clearedRevision);
1628
+ },
1629
+ });
1630
+ const accepted = started.body.accepted === true;
1631
+ const turn = started.body.turn && typeof started.body.turn === "object"
1632
+ ? started.body.turn
1633
+ : undefined;
1634
+ if (accepted && turn?.turnId) {
1635
+ const next = await deps.store.transitionDeferredTurn(sessionId, {
1636
+ expectedRevision: deferred.revision,
1637
+ state: "accepted",
1638
+ acceptedTurnId: turn.turnId,
1639
+ });
1640
+ if (next)
1641
+ emitDeferredTurnUpdate(deps, sessionId, turn.turnId, next, next.revision);
1642
+ return;
1643
+ }
1644
+ const error = started.body.error && typeof started.body.error === "object"
1645
+ ? started.body.error
1646
+ : {};
1647
+ if (error.code === "TURN_ACTIVE") {
1648
+ const next = await deps.store.transitionDeferredTurn(sessionId, {
1649
+ expectedRevision: deferred.revision,
1650
+ state: "queued",
1651
+ });
1652
+ if (next)
1653
+ emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, next, next.revision);
1654
+ return;
1655
+ }
1656
+ const failed = await deps.store.transitionDeferredTurn(sessionId, {
1657
+ expectedRevision: deferred.revision,
1658
+ state: "failed",
1659
+ lastError: {
1660
+ code: error.code ?? `HTTP_${started.status}`,
1661
+ message: error.message ?? "停止后的消息未能自动发送",
1662
+ },
1663
+ });
1664
+ if (failed)
1665
+ emitDeferredTurnUpdate(deps, sessionId, deferred.blockedByTurnId, failed, failed.revision);
1666
+ }
1667
+ catch (error) {
1668
+ // Keep fire-and-forget callers safe and make a dispatch failure visible and
1669
+ // recoverable from the composer instead of leaving the slot indefinitely
1670
+ // in an ambiguous queued/dispatching state.
1671
+ try {
1672
+ const current = (await deps.store.get(sessionId))?.deferredTurn;
1673
+ if (current && (current.state === "queued" || current.state === "dispatching")) {
1674
+ const failed = await deps.store.transitionDeferredTurn(sessionId, {
1675
+ expectedRevision: current.revision,
1676
+ state: "failed",
1677
+ lastError: {
1678
+ code: "DEFERRED_TURN_DISPATCH_FAILED",
1679
+ message: error instanceof Error ? error.message : String(error),
1680
+ },
1681
+ });
1682
+ if (failed)
1683
+ emitDeferredTurnUpdate(deps, sessionId, failed.blockedByTurnId, failed, failed.revision);
1684
+ }
1685
+ }
1686
+ catch {
1687
+ // The durable store itself is unavailable. Leave the original record in
1688
+ // place so a later Console boot can recover it.
1689
+ }
1690
+ }
1691
+ finally {
1692
+ deferredDispatchingSessions.delete(sessionId);
1693
+ }
1694
+ }
1695
+ export async function recoverDeferredTurns(deps) {
1696
+ const sessions = await deps.store.list({ includeArchived: false });
1697
+ await Promise.all(sessions
1698
+ .filter((session) => session.deferredTurn
1699
+ ? session.deferredTurn.state === "queued" ||
1700
+ session.deferredTurn.state === "dispatching"
1701
+ : false)
1702
+ .map(async (session) => {
1703
+ const deferred = session.deferredTurn;
1704
+ const active = deps.events.getActiveTurn(session.sessionId);
1705
+ // A process restart cannot retain AbortController ownership. When the
1706
+ // durable blocker was already stopping and no execution owns it now,
1707
+ // close that orphan before dispatching the server-owned follow-up.
1708
+ if (active?.turnId === deferred.blockedByTurnId &&
1709
+ active.abortRequestedAt &&
1710
+ deps.turnExecutions &&
1711
+ !deps.turnExecutions.get(session.sessionId, active.turnId)) {
1712
+ deps.events.setTurnState(session.sessionId, active.turnId, "failed", {
1713
+ code: "CHAT_TURN_OWNER_LOST",
1714
+ message: "turn ownership was lost during Console restart",
1715
+ });
1716
+ }
1717
+ await dispatchDeferredTurn(deps, session.sessionId);
1718
+ }));
1719
+ }
1318
1720
  /** Real server-side abort: invokes Pi session.abort via the execution registry. */
1319
1721
  export async function handleAbortChatTurn(req, res, deps, sessionId, turnId) {
1320
1722
  const gate = gateMutation(req, deps);
@@ -3054,6 +3456,15 @@ export async function handleChatRequest(req, res, deps, pathname) {
3054
3456
  await handleCreateChatTurn(req, res, deps, decodeURIComponent(sessionTurn[1]));
3055
3457
  return true;
3056
3458
  }
3459
+ const deferredTurn = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/deferred-turn$/);
3460
+ if (method === "PUT" && deferredTurn) {
3461
+ await handlePutDeferredTurn(req, res, deps, decodeURIComponent(deferredTurn[1]));
3462
+ return true;
3463
+ }
3464
+ if (method === "DELETE" && deferredTurn) {
3465
+ await handleDeleteDeferredTurn(req, res, deps, decodeURIComponent(deferredTurn[1]));
3466
+ return true;
3467
+ }
3057
3468
  const sessionTurnAbort = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/turns\/([^/]+)\/abort$/);
3058
3469
  if (method === "POST" && sessionTurnAbort) {
3059
3470
  await handleAbortChatTurn(req, res, deps, decodeURIComponent(sessionTurnAbort[1]), decodeURIComponent(sessionTurnAbort[2]));
@@ -38,10 +38,10 @@ const OPERATION_TITLES = {
38
38
  dagStatus: "执行状态",
39
39
  dagDoctor: "执行诊断",
40
40
  dagReport: "执行结果",
41
- dagRerunPlan: "恢复评估",
42
- dagRerun: "从节点续跑",
41
+ dagRerunPlan: "节点恢复评估",
42
+ dagRerun: "从失败节点恢复",
43
43
  dagInterruptEligibility: "中止资格",
44
- standaloneTaskRerun: "完整重跑任务",
44
+ standaloneTaskRerun: "带反馈重新执行",
45
45
  workerTaskRetry: "重试 Worker 任务",
46
46
  workerPoolDoctor: "任务池诊断",
47
47
  promoteRun: "任务推进",
@@ -89,24 +89,24 @@ const TOOL_SEMANTICS = {
89
89
  error: "读取执行结果时遇到问题",
90
90
  },
91
91
  dagRerunPlan: {
92
- running: "正在评估能否从失败位置继续",
93
- success: "恢复条件评估已经完成",
92
+ running: "正在评估能否从失败节点安全恢复",
93
+ success: "节点恢复条件评估已经完成",
94
94
  error: "暂时无法确定安全的恢复方式",
95
95
  },
96
+ dagRerun: {
97
+ running: "正在创建节点恢复 run",
98
+ success: "节点恢复 run 已经启动",
99
+ error: "从失败节点恢复时遇到问题",
100
+ },
96
101
  dagInterruptEligibility: {
97
102
  running: "正在检查能否协作中止此运行",
98
103
  success: "中止资格已经确认",
99
104
  error: "当前运行不能协作中止",
100
105
  },
101
- dagRerun: {
102
- running: "正在从失败节点续跑",
103
- success: "已经从失败节点继续执行",
104
- error: "从失败节点续跑时遇到问题",
105
- },
106
106
  standaloneTaskRerun: {
107
- running: "正在完整重跑整单任务",
108
- success: "已经启动完整重跑",
109
- error: "完整重跑任务时遇到问题",
107
+ running: "正在创建新的完整 DAG,并带入上一轮未解决反馈",
108
+ success: "新的完整 DAG 已经按上一轮反馈重新执行",
109
+ error: "带反馈重新执行任务时遇到问题",
110
110
  },
111
111
  workerPoolDoctor: {
112
112
  running: "正在读取任务池状态",