dominds 1.25.3 → 1.25.5

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 (167) hide show
  1. package/dist/dialog-display-state.js +6 -7
  2. package/dist/docs/dialog-system.md +15 -29
  3. package/dist/docs/dialog-system.zh.md +15 -29
  4. package/dist/evt-registry.js +2 -0
  5. package/dist/llm/kernel-driver/drive.js +137 -7
  6. package/dist/llm/kernel-driver/flow.js +46 -20
  7. package/dist/llm/kernel-driver/reply-guidance.js +5 -5
  8. package/dist/minds/system-prompt-parts.js +11 -0
  9. package/dist/persistence-errors.d.ts +1 -1
  10. package/dist/persistence.d.ts +50 -1
  11. package/dist/persistence.js +549 -4
  12. package/dist/runtime/driver-messages.d.ts +7 -1
  13. package/dist/runtime/driver-messages.js +33 -11
  14. package/dist/runtime/interjection-pause-stop.js +8 -7
  15. package/dist/server/dominds-self-update.js +133 -38
  16. package/dist/server/websocket-handler.js +73 -6
  17. package/dist/tools/cmd-runner-protocol.d.ts +1 -0
  18. package/dist/tools/cmd-runner-protocol.js +6 -0
  19. package/dist/tools/cmd-runner.js +1 -0
  20. package/dist/tools/os.d.ts +2 -4
  21. package/dist/tools/os.js +79 -4
  22. package/package.json +3 -3
  23. package/webapp/dist/assets/{_basePickBy-CePmq6WU.js → _basePickBy-fZ31r-iF.js} +3 -3
  24. package/webapp/dist/assets/_basePickBy-fZ31r-iF.js.map +1 -0
  25. package/webapp/dist/assets/{_baseUniq-DAB2FR9P.js → _baseUniq-CI1keLoL.js} +2 -2
  26. package/webapp/dist/assets/_baseUniq-CI1keLoL.js.map +1 -0
  27. package/webapp/dist/assets/{arc-BmAGUHtw.js → arc-1NeUqzoV.js} +2 -2
  28. package/webapp/dist/assets/arc-1NeUqzoV.js.map +1 -0
  29. package/webapp/dist/assets/{architectureDiagram-VXUJARFQ-BBsOS4fu.js → architectureDiagram-2XIMDMQ5-C7SdzYIh.js} +26 -8
  30. package/webapp/dist/assets/architectureDiagram-2XIMDMQ5-C7SdzYIh.js.map +1 -0
  31. package/webapp/dist/assets/{blockDiagram-VD42YOAC-Cuxh8em4.js → blockDiagram-WCTKOSBZ-Cmpu3kIy.js} +187 -170
  32. package/webapp/dist/assets/blockDiagram-WCTKOSBZ-Cmpu3kIy.js.map +1 -0
  33. package/webapp/dist/assets/{c4Diagram-YG6GDRKO-L56ctb3A.js → c4Diagram-IC4MRINW-ChGTHIor.js} +4 -4
  34. package/webapp/dist/assets/c4Diagram-IC4MRINW-ChGTHIor.js.map +1 -0
  35. package/webapp/dist/assets/{channel-Ic6QamsR.js → channel-ndS-XTQQ.js} +2 -2
  36. package/webapp/dist/assets/channel-ndS-XTQQ.js.map +1 -0
  37. package/webapp/dist/assets/{chunk-4BX2VUAB-B8UiHrZa.js → chunk-4BX2VUAB-Cw5FtBd_.js} +2 -2
  38. package/webapp/dist/assets/chunk-4BX2VUAB-Cw5FtBd_.js.map +1 -0
  39. package/webapp/dist/assets/{chunk-55IACEB6-9QqsT66L.js → chunk-55IACEB6-D3G71JdA.js} +2 -2
  40. package/webapp/dist/assets/chunk-55IACEB6-D3G71JdA.js.map +1 -0
  41. package/webapp/dist/assets/{chunk-FMBD7UC4-DimBaUep.js → chunk-FMBD7UC4-BcugPyy7.js} +2 -2
  42. package/webapp/dist/assets/chunk-FMBD7UC4-BcugPyy7.js.map +1 -0
  43. package/webapp/dist/assets/{chunk-TZMSLE5B-DtHl61is.js → chunk-JSJVCQXG-DvuxYuNq.js} +14 -6
  44. package/webapp/dist/assets/chunk-JSJVCQXG-DvuxYuNq.js.map +1 -0
  45. package/webapp/dist/assets/{chunk-QN33PNHL-HUOaMARz.js → chunk-KX2RTZJC-BQ42xd4s.js} +2 -2
  46. package/webapp/dist/assets/chunk-KX2RTZJC-BQ42xd4s.js.map +1 -0
  47. package/webapp/dist/assets/{chunk-DI55MBZ5-uGc9dcaR.js → chunk-NQ4KR5QH-CGEMYTch.js} +9 -7
  48. package/webapp/dist/assets/chunk-NQ4KR5QH-CGEMYTch.js.map +1 -0
  49. package/webapp/dist/assets/{chunk-QZHKN3VN-pR62PJlL.js → chunk-QZHKN3VN-CxNTVm5w.js} +2 -2
  50. package/webapp/dist/assets/chunk-QZHKN3VN-CxNTVm5w.js.map +1 -0
  51. package/webapp/dist/assets/{chunk-B4BG7PRW-Cs-W2Q5y.js → chunk-WL4C6EOR-CIwdSg5q.js} +171 -121
  52. package/webapp/dist/assets/chunk-WL4C6EOR-CIwdSg5q.js.map +1 -0
  53. package/webapp/dist/assets/{classDiagram-2ON5EDUG-DoZGDGgs.js → classDiagram-VBA2DB6C-Dl05PJAP.js} +7 -6
  54. package/webapp/dist/assets/classDiagram-VBA2DB6C-Dl05PJAP.js.map +1 -0
  55. package/webapp/dist/assets/{classDiagram-v2-WZHVMYZB-DoZGDGgs.js → classDiagram-v2-RAHNMMFH-Dl05PJAP.js} +7 -6
  56. package/webapp/dist/assets/classDiagram-v2-RAHNMMFH-Dl05PJAP.js.map +1 -0
  57. package/webapp/dist/assets/{clone-WTiKKPxQ.js → clone-KCj1-QMr.js} +2 -2
  58. package/webapp/dist/assets/clone-KCj1-QMr.js.map +1 -0
  59. package/webapp/dist/assets/{cose-bilkent-S5V4N54A-CQdrsAwh.js → cose-bilkent-S5V4N54A-BttciCz5.js} +2 -2
  60. package/webapp/dist/assets/cose-bilkent-S5V4N54A-BttciCz5.js.map +1 -0
  61. package/webapp/dist/assets/cytoscape.esm-Bm8DJGmZ.js.map +1 -1
  62. package/webapp/dist/assets/{dagre-6UL2VRFP-CUMu8kzn.js → dagre-KLK3FWXG-Dm5yE9k2.js} +7 -7
  63. package/webapp/dist/assets/dagre-KLK3FWXG-Dm5yE9k2.js.map +1 -0
  64. package/webapp/dist/assets/defaultLocale-B2RvLBDe.js.map +1 -1
  65. package/webapp/dist/assets/{diagram-PSM6KHXK-DB6XicOz.js → diagram-E7M64L7V-C80ZsiBG.js} +10 -10
  66. package/webapp/dist/assets/diagram-E7M64L7V-C80ZsiBG.js.map +1 -0
  67. package/webapp/dist/assets/{diagram-QEK2KX5R-BaeG3mbJ.js → diagram-IFDJBPK2-CqKidH4n.js} +9 -8
  68. package/webapp/dist/assets/diagram-IFDJBPK2-CqKidH4n.js.map +1 -0
  69. package/webapp/dist/assets/{diagram-S2PKOQOG-DF7TLMvc.js → diagram-P4PSJMXO-EHVjfzUV.js} +8 -8
  70. package/webapp/dist/assets/diagram-P4PSJMXO-EHVjfzUV.js.map +1 -0
  71. package/webapp/dist/assets/{erDiagram-Q2GNP2WA-BQr1dJmi.js → erDiagram-INFDFZHY-7MrtsOIt.js} +96 -75
  72. package/webapp/dist/assets/erDiagram-INFDFZHY-7MrtsOIt.js.map +1 -0
  73. package/webapp/dist/assets/{flowDiagram-NV44I4VS-BPOTtmjM.js → flowDiagram-PKNHOUZH-D6hXqCy7.js} +98 -81
  74. package/webapp/dist/assets/flowDiagram-PKNHOUZH-D6hXqCy7.js.map +1 -0
  75. package/webapp/dist/assets/{ganttDiagram-JELNMOA3-DsUkIvyS.js → ganttDiagram-A5KZAMGK-DLO0XCDk.js} +28 -3
  76. package/webapp/dist/assets/ganttDiagram-A5KZAMGK-DLO0XCDk.js.map +1 -0
  77. package/webapp/dist/assets/{gitGraphDiagram-V2S2FVAM-CnXo7Mx_.js → gitGraphDiagram-K3NZZRJ6-BmVD_nQP.js} +38 -46
  78. package/webapp/dist/assets/gitGraphDiagram-K3NZZRJ6-BmVD_nQP.js.map +1 -0
  79. package/webapp/dist/assets/graph-DQTj8O0Q.js +782 -0
  80. package/webapp/dist/assets/graph-DQTj8O0Q.js.map +1 -0
  81. package/webapp/dist/assets/{index-yycTJNYb.css → index-BQoNJEGT.css} +1 -1
  82. package/webapp/dist/assets/{index-Dpmu_hYq.js → index-BXjq-k48.js} +1378 -1094
  83. package/webapp/dist/assets/{index-Dpmu_hYq.js.map → index-BXjq-k48.js.map} +1 -1
  84. package/webapp/dist/assets/{infoDiagram-HS3SLOUP-OgTfggdn.js → infoDiagram-LFFYTUFH-D2fqM9Fn.js} +7 -7
  85. package/webapp/dist/assets/infoDiagram-LFFYTUFH-D2fqM9Fn.js.map +1 -0
  86. package/webapp/dist/assets/init-ZxktEp_H.js.map +1 -1
  87. package/webapp/dist/assets/ishikawaDiagram-PHBUUO56-kp7lB6-f.js +966 -0
  88. package/webapp/dist/assets/ishikawaDiagram-PHBUUO56-kp7lB6-f.js.map +1 -0
  89. package/webapp/dist/assets/{journeyDiagram-XKPGCS4Q-5a_zmNY7.js → journeyDiagram-4ABVD52K-DmbA52J-.js} +5 -5
  90. package/webapp/dist/assets/journeyDiagram-4ABVD52K-DmbA52J-.js.map +1 -0
  91. package/webapp/dist/assets/{kanban-definition-3W4ZIXB7-DJTdbtlF.js → kanban-definition-K7BYSVSG-DRUw-P-d.js} +5 -3
  92. package/webapp/dist/assets/kanban-definition-K7BYSVSG-DRUw-P-d.js.map +1 -0
  93. package/webapp/dist/assets/{layout-BrRMWeju.js → layout-BUoHHXzn.js} +5 -5
  94. package/webapp/dist/assets/layout-BUoHHXzn.js.map +1 -0
  95. package/webapp/dist/assets/{linear-Dc9z6NyJ.js → linear-xVSAq8rI.js} +2 -2
  96. package/webapp/dist/assets/linear-xVSAq8rI.js.map +1 -0
  97. package/webapp/dist/assets/{mindmap-definition-VGOIOE7T-Dhltq6n6.js → mindmap-definition-YRQLILUH-BcajbGmW.js} +7 -5
  98. package/webapp/dist/assets/mindmap-definition-YRQLILUH-BcajbGmW.js.map +1 -0
  99. package/webapp/dist/assets/ordinal-CxptdPJm.js.map +1 -1
  100. package/webapp/dist/assets/{pieDiagram-ADFJNKIX-C-YmzhGj.js → pieDiagram-SKSYHLDU-Bu8kmCW6.js} +8 -8
  101. package/webapp/dist/assets/pieDiagram-SKSYHLDU-Bu8kmCW6.js.map +1 -0
  102. package/webapp/dist/assets/{quadrantDiagram-AYHSOK5B-BxqMSMrz.js → quadrantDiagram-337W2JSQ-DIBSNUGa.js} +3 -3
  103. package/webapp/dist/assets/quadrantDiagram-337W2JSQ-DIBSNUGa.js.map +1 -0
  104. package/webapp/dist/assets/{requirementDiagram-UZGBJVZJ-DzAytLM4.js → requirementDiagram-Z7DCOOCP-CkrDkbBt.js} +16 -6
  105. package/webapp/dist/assets/requirementDiagram-Z7DCOOCP-CkrDkbBt.js.map +1 -0
  106. package/webapp/dist/assets/{sankeyDiagram-TZEHDZUN-C1YODWlP.js → sankeyDiagram-WA2Y5GQK-BZ-1R8pc.js} +2 -2
  107. package/webapp/dist/assets/sankeyDiagram-WA2Y5GQK-BZ-1R8pc.js.map +1 -0
  108. package/webapp/dist/assets/{sequenceDiagram-WL72ISMW-CuqvCm6T.js → sequenceDiagram-2WXFIKYE-DYoNARdO.js} +601 -201
  109. package/webapp/dist/assets/sequenceDiagram-2WXFIKYE-DYoNARdO.js.map +1 -0
  110. package/webapp/dist/assets/{stateDiagram-FKZM4ZOC-Bdl_IDuo.js → stateDiagram-RAJIS63D-D9v19nvh.js} +9 -9
  111. package/webapp/dist/assets/stateDiagram-RAJIS63D-D9v19nvh.js.map +1 -0
  112. package/webapp/dist/assets/{stateDiagram-v2-4FDKWEC3-CApGpGJP.js → stateDiagram-v2-FVOUBMTO-BYw5ZwtH.js} +5 -5
  113. package/webapp/dist/assets/stateDiagram-v2-FVOUBMTO-BYw5ZwtH.js.map +1 -0
  114. package/webapp/dist/assets/{timeline-definition-IT6M3QCI-D58v81nI.js → timeline-definition-YZTLITO2-BT10dRIJ.js} +3 -3
  115. package/webapp/dist/assets/timeline-definition-YZTLITO2-BT10dRIJ.js.map +1 -0
  116. package/webapp/dist/assets/{treemap-GDKQZRPO-DAFtS5Nq.js → treemap-KZPCXAKY-yPoPC5hc.js} +37 -24
  117. package/webapp/dist/assets/treemap-KZPCXAKY-yPoPC5hc.js.map +1 -0
  118. package/webapp/dist/assets/vennDiagram-LZ73GAT5-tryg3OaY.js +2487 -0
  119. package/webapp/dist/assets/vennDiagram-LZ73GAT5-tryg3OaY.js.map +1 -0
  120. package/webapp/dist/assets/{xychartDiagram-PRI3JC2R-Dfairu24.js → xychartDiagram-JWTSCODW-BWLUzo9S.js} +4 -4
  121. package/webapp/dist/assets/xychartDiagram-JWTSCODW-BWLUzo9S.js.map +1 -0
  122. package/webapp/dist/index.html +2 -2
  123. package/webapp/dist/assets/_basePickBy-CePmq6WU.js.map +0 -1
  124. package/webapp/dist/assets/_baseUniq-DAB2FR9P.js.map +0 -1
  125. package/webapp/dist/assets/arc-BmAGUHtw.js.map +0 -1
  126. package/webapp/dist/assets/architectureDiagram-VXUJARFQ-BBsOS4fu.js.map +0 -1
  127. package/webapp/dist/assets/blockDiagram-VD42YOAC-Cuxh8em4.js.map +0 -1
  128. package/webapp/dist/assets/c4Diagram-YG6GDRKO-L56ctb3A.js.map +0 -1
  129. package/webapp/dist/assets/channel-Ic6QamsR.js.map +0 -1
  130. package/webapp/dist/assets/chunk-4BX2VUAB-B8UiHrZa.js.map +0 -1
  131. package/webapp/dist/assets/chunk-55IACEB6-9QqsT66L.js.map +0 -1
  132. package/webapp/dist/assets/chunk-B4BG7PRW-Cs-W2Q5y.js.map +0 -1
  133. package/webapp/dist/assets/chunk-DI55MBZ5-uGc9dcaR.js.map +0 -1
  134. package/webapp/dist/assets/chunk-FMBD7UC4-DimBaUep.js.map +0 -1
  135. package/webapp/dist/assets/chunk-QN33PNHL-HUOaMARz.js.map +0 -1
  136. package/webapp/dist/assets/chunk-QZHKN3VN-pR62PJlL.js.map +0 -1
  137. package/webapp/dist/assets/chunk-TZMSLE5B-DtHl61is.js.map +0 -1
  138. package/webapp/dist/assets/classDiagram-2ON5EDUG-DoZGDGgs.js.map +0 -1
  139. package/webapp/dist/assets/classDiagram-v2-WZHVMYZB-DoZGDGgs.js.map +0 -1
  140. package/webapp/dist/assets/clone-WTiKKPxQ.js.map +0 -1
  141. package/webapp/dist/assets/cose-bilkent-S5V4N54A-CQdrsAwh.js.map +0 -1
  142. package/webapp/dist/assets/dagre-6UL2VRFP-CUMu8kzn.js.map +0 -1
  143. package/webapp/dist/assets/diagram-PSM6KHXK-DB6XicOz.js.map +0 -1
  144. package/webapp/dist/assets/diagram-QEK2KX5R-BaeG3mbJ.js.map +0 -1
  145. package/webapp/dist/assets/diagram-S2PKOQOG-DF7TLMvc.js.map +0 -1
  146. package/webapp/dist/assets/erDiagram-Q2GNP2WA-BQr1dJmi.js.map +0 -1
  147. package/webapp/dist/assets/flowDiagram-NV44I4VS-BPOTtmjM.js.map +0 -1
  148. package/webapp/dist/assets/ganttDiagram-JELNMOA3-DsUkIvyS.js.map +0 -1
  149. package/webapp/dist/assets/gitGraphDiagram-V2S2FVAM-CnXo7Mx_.js.map +0 -1
  150. package/webapp/dist/assets/graph-C2Cpmq_d.js +0 -425
  151. package/webapp/dist/assets/graph-C2Cpmq_d.js.map +0 -1
  152. package/webapp/dist/assets/infoDiagram-HS3SLOUP-OgTfggdn.js.map +0 -1
  153. package/webapp/dist/assets/journeyDiagram-XKPGCS4Q-5a_zmNY7.js.map +0 -1
  154. package/webapp/dist/assets/kanban-definition-3W4ZIXB7-DJTdbtlF.js.map +0 -1
  155. package/webapp/dist/assets/layout-BrRMWeju.js.map +0 -1
  156. package/webapp/dist/assets/linear-Dc9z6NyJ.js.map +0 -1
  157. package/webapp/dist/assets/mindmap-definition-VGOIOE7T-Dhltq6n6.js.map +0 -1
  158. package/webapp/dist/assets/pieDiagram-ADFJNKIX-C-YmzhGj.js.map +0 -1
  159. package/webapp/dist/assets/quadrantDiagram-AYHSOK5B-BxqMSMrz.js.map +0 -1
  160. package/webapp/dist/assets/requirementDiagram-UZGBJVZJ-DzAytLM4.js.map +0 -1
  161. package/webapp/dist/assets/sankeyDiagram-TZEHDZUN-C1YODWlP.js.map +0 -1
  162. package/webapp/dist/assets/sequenceDiagram-WL72ISMW-CuqvCm6T.js.map +0 -1
  163. package/webapp/dist/assets/stateDiagram-FKZM4ZOC-Bdl_IDuo.js.map +0 -1
  164. package/webapp/dist/assets/stateDiagram-v2-4FDKWEC3-CApGpGJP.js.map +0 -1
  165. package/webapp/dist/assets/timeline-definition-IT6M3QCI-D58v81nI.js.map +0 -1
  166. package/webapp/dist/assets/treemap-GDKQZRPO-DAFtS5Nq.js.map +0 -1
  167. package/webapp/dist/assets/xychartDiagram-PRI3JC2R-Dfairu24.js.map +0 -1
@@ -182,9 +182,8 @@ async function getRunControlCountsSnapshot() {
182
182
  isStoppedReasonResumable(latest.executionMarker.reason)) {
183
183
  // Keep run-control counts aligned with actual Continue affordance:
184
184
  // - ordinary interrupted dialogs count as resumable only when no Q4H suspension remains
185
- // - interjection-paused dialogs still count as resumable even if Q4H remains,
186
- // because the intended UX is that Continue exits the temporary paused projection
187
- // and re-evaluates the original task from fresh facts
185
+ // - legacy interjection-paused dialogs still count as resumable even if Q4H remains,
186
+ // because Continue only re-evaluates the original task from fresh facts
188
187
  if ((0, interjection_pause_stop_1.isUserInterjectionPauseStopReason)(latest.executionMarker.reason)) {
189
188
  resumable++;
190
189
  }
@@ -615,10 +614,10 @@ async function refreshRunControlProjectionFromPersistenceFacts(dialogId, trigger
615
614
  if (latest.executionMarker?.kind === 'interrupted' &&
616
615
  (0, interjection_pause_stop_1.isUserInterjectionPauseStopReason)(latest.executionMarker.reason)) {
617
616
  // WARNING:
618
- // This is the one place where the projection intentionally preserves the paused-interjection
619
- // stopped state ahead of the current suspension facts. That is not a bug: after a user
620
- // interjection we want the UI to keep showing "original task paused; click Continue" even if
621
- // the underlying dialog is still waiting on Q4H.
617
+ // This is the one place where the projection intentionally preserves legacy
618
+ // paused-interjection stopped state ahead of the current suspension facts. That is not a bug:
619
+ // the UI may still need to show that the original task was paused even if the underlying
620
+ // dialog is now waiting on Q4H.
622
621
  //
623
622
  // The true source-of-truth decision about what Continue should do next lives in `flow.ts`'s
624
623
  // resume path, which performs a fresh fact scan at resume time and then either restores the
@@ -155,52 +155,38 @@ Dialog state is persisted to storage at key points:
155
155
 
156
156
  This ensures crash recovery and enables the backend to resume from any persisted state without depending on frontend state.
157
157
 
158
- ### User Interjection Pause And Continue Semantics
158
+ ### User Interjection And A2H Semantics
159
159
 
160
160
  When a dialog still carries an inter-dialog reply obligation, but the user temporarily interjects and asks it to handle a local question first, the system must distinguish between the **UI projection** and the **true driving source state**.
161
161
 
162
+ Plainly: the system should answer the user's interjection first. Once the user receives a visible answer, the backend records that answer as A2H (Answer to Human) in Human Attention so the user can find and acknowledge it even if the dialog immediately continues automatically.
163
+
162
164
  **Normative semantics**:
163
165
 
164
166
  1. Every user interjection message is driven as a complete normal round.
165
- 2. If that round needs tools, the system MUST finish the full tool round and any post-tool follow-up before pausing.
166
- 3. The system only projects the original task as resumable `stopped` when this interjection has actually parked an original task that still needs explicit restoration.
167
- 4. If there is no parked original task to resume afterwards (for example, no inter-dialog reply obligation needs reassertion), the interjection round should simply finish and return to the true underlying state without showing this special `stopped` panel.
168
- 5. As long as the user keeps sending new messages, the dialog stays in temporary interjection-chat handling, and that paused projection remains in place only if it was established in the first place.
169
- 6. Only an explicit UI `Continue` attempts to restore the original task.
167
+ 2. If that round needs tools, the system MUST finish the full tool round and any post-tool follow-up before treating the interjection as answered.
168
+ 3. A visible assistant `saying` settles the pending user-interjection reply only when no same-round function/tellask call remains after that `saying`.
169
+ 4. Settling the interjection appends an A2H item to the dialog's `a2h.yaml`. A2H is an acknowledgement queue, not a problem report and not durable drive work.
170
+ 5. If an inter-dialog reply obligation still exists after the interjection is answered, the backend automatically reasserts that obligation and continues. The user should not need to click `Continue` merely because the interjection answer completed.
171
+ 6. A2H disappears when the user acknowledges it. This is intentionally "read then burn"; the canonical answer remains in the dialog transcript at `answerRef`.
172
+ 7. The Human Attention panel shows Q4H and A2H together. Q4H waits for a human answer; A2H waits only for human acknowledgement.
170
173
 
171
174
  **Strict boundary**: a formal `askHuman` answer is not part of this "user interjection" category. As soon as a prompt carries a real `q4hAnswerCallId`, it belongs to the askHuman reply channel and semantically continues an already-materialized question/answer chain; it must never be downgraded into temporary local side-chat.
172
175
 
173
- **Key point**: this `stopped` state is only a temporary run-control / UI projection. It is not the same as an ordinary system-stop failure, and it is not the final business source of truth. It also does not apply to every interjection; it exists only when there really is a parked original task to resume.
174
-
175
- After the user clicks `Continue`, the backend MUST re-evaluate fresh persistence facts and decide which true-source case now applies. It must not infer the result purely from the visible `displayState`:
176
-
177
- - **Case 1: the dialog no longer has a reply obligation**
178
- If there is also no user-wait fact such as Q4H, the dialog should simply continue driving. If it has already become ordinary idle-waiting-user, then `resume_dialog` is no longer actually resumable.
179
- - **Case 2: the dialog still has a reply obligation and is waiting for user input**
180
- The canonical example is pending Q4H. In this case, `Continue` should exit the interjection-paused projection and restore the true `blocked` state.
181
- - **Case 3: the dialog still has a reply obligation but is no longer suspended and is eligible to proceed**
182
- For example, the Q4H/user-wait fact has disappeared, or a queued prompt provides a valid continuation path. In this case, `Continue` must not first fall back to an intermediate placeholder `blocked/idle` state; it should keep driving immediately.
183
-
184
- **This leads to two implementation constraints**:
185
-
186
- - `refreshRunControlProjectionFromPersistenceFacts()` MUST preserve the special "interjection handled; original task paused" `stopped` projection until the user explicitly clicks `Continue`; otherwise the UI collapses back to ordinary `blocked` too early and breaks multi-turn interjection UX. Conversely, when there is no parked original task, this paused projection should not be created at all.
187
- - The actual outcome of `Continue` MUST be decided in the resume drive path by re-reading fresh persistence facts. "Continue is clickable" does not mean "the dialog will definitely enter proceeding immediately".
188
- - If `Continue` reveals that the true state is still `blocked`, the reply-obligation reassertion copy should be materialized immediately as a runtime guide in both `dlg.msgs` and persisted course history, while also surfacing as a frontend bubble. That lets the later real resume path rely on ordinary context replay instead of synthesizing a second duplicate runtime prompt.
189
- - The run-control toolbar's `resumable` count should align with "manual Continue attempt is meaningful". Therefore an interjection-paused `stopped` dialog still counts as resumable even when underlying user-wait facts remain, because the business meaning of `Continue` there is "exit the temporary paused projection and re-evaluate from source-of-truth facts".
176
+ **Key point**: pending user-interjection reply and inter-dialog reply obligation are independent business facts. Reminder/footer copy can use those two facts directly: if the interjection is still pending, prioritize answering the user; if it is settled and the reply obligation remains active, continue toward the required inter-dialog closure.
190
177
 
191
178
  **Mental-model warning**:
192
179
 
193
- - Do not reason about this flow from `displayState.kind === 'stopped'` alone.
194
- - Do not reason about it from user-wait facts alone and then wonder why the UI still shows `stopped`.
195
- - Do not reason about it from `resume_dialog` eligibility alone and assume resumption always means immediate running.
196
180
  - Do not flatten every `origin === 'user'` prompt into "interjection"; a non-empty `q4hAnswerCallId` means askHuman answer continuation and follows a different semantic path.
181
+ - Do not treat A2H as Q4H. A2H does not block drive and does not route input to an agent.
182
+ - Do not store A2H in the Problems panel. It belongs to Human Attention and is removed by Ack.
197
183
 
198
184
  You need all of the following together to understand the behavior correctly:
199
185
 
200
186
  - reply-guidance suppression / deferred reassertion for interjection turns
201
- - flow logic for "pause after local interjection reply" plus "fresh-fact second decision after Continue"
202
- - dialog-display-state projection preservation
203
- - websocket resume entry semantics distinguishing "allowed to attempt Continue" from "actually re-entered driving"
187
+ - pending user-interjection reply settlement after visible final `saying`
188
+ - A2H persistence and Ack flow
189
+ - automatic reply-obligation reassertion after the user-visible answer
204
190
 
205
191
  This is an intentionally cross-module semantic contract. Do not locally "simplify" one piece based only on its surface meaning.
206
192
 
@@ -154,52 +154,38 @@ askerDialog 可以在执行期间接收来自当前需向它回复的支线对
154
154
 
155
155
  这确保了崩溃恢复,并使后端能够从任何持久化状态恢复,而不依赖于前端状态。
156
156
 
157
- ### 用户插话暂停与 Continue 语义
157
+ ### 用户插话与 A2H 语义
158
158
 
159
159
  当某个对话仍带有跨对话回复义务,但用户临时插话要求它先处理本地问题时,系统必须区分**UI 投影**与**真实驱动源状态**。
160
160
 
161
+ 直白地说:先把用户这次插话接住并答完。用户看到可见答复后,后端把这条答复记录成 A2H(Answer to Human)放进“待人处理”,这样即使对话马上自动续推,用户也能找到并确认已阅。
162
+
161
163
  **规范语义**:
162
164
 
163
165
  1. 每条用户插话消息都按正常驱动轮完整执行。
164
- 2. 若该轮需要工具,则必须先完整跑完该工具轮及其 post-tool follow-up
165
- 3. 只有当这条插话确实打断了一个仍待恢复的“原任务”时,系统才把该原任务投影为可 `Continue` `stopped`,让用户先看到最后一条回复。
166
- 4. 若当前并不存在待恢复的原任务(例如没有待重申的跨对话回复义务),则插话轮结束后应直接回到真实 underlying state,而不显示这个特殊 `stopped` 面板。
167
- 5. 只要用户继续发送新消息,就继续作为插话临时对话处理;这个 paused projection 仅在它已被建立时持续保持。
168
- 6. 只有用户显式点击 UI `Continue`,系统才尝试恢复原任务。
166
+ 2. 若该轮需要工具,则必须先完整跑完该工具轮及其 post-tool follow-up,之后才可认为插话已答完。
167
+ 3. 只有当模型产生可见 `saying`,且该 `saying` 后同轮没有普通 function/tellask call 继续挂起时,才清除“待回复用户插话”。
168
+ 4. 清除该状态时,把答复追加到该对话的 `a2h.yaml`。A2H 是待人确认队列,不是 Problems,也不是持久驱动工作。
169
+ 5. 如果插话答完后仍有跨对话回复义务,后端自动重申并继续推进;用户不应仅因为插话答完而需要手工点 `Continue`。
170
+ 6. 用户 Ack 以后 A2H 立即消失,语义是“阅后即焚”;答复正文的真源仍在对话 transcript 中,由 `answerRef` 回链定位。
171
+ 7. “待人处理”面板同时显示 Q4H 与 A2H:Q4H 等人回答,A2H 只等人确认已阅。
169
172
 
170
173
  **严格边界**:`askHuman` 的正式回答不属于这里的“用户插话”。只要一条 prompt 带着真实的 `q4hAnswerCallId`,它就属于 askHuman 回复通道,语义上是在继续已 materialize 的提问/应答链路,绝不能被压入“本地临时插话聊天”。
171
174
 
172
- **关键点**:这里的 `stopped` 只是一个临时 run-control / UI 投影,不等于普通 system-stop 失败,也不是最终的业务真源;并且它不是所有插话都会出现,只在“确有一个待恢复的原任务被临时停靠”时出现。
173
-
174
- 点击 `Continue` 后,后端必须重新从 persistence 真源判定当前对话属于哪一种情况,而不能只根据表面的 `displayState` 做静态推断:
175
-
176
- - **情况 1:当前对话没有回复义务**
177
- 这时若也没有 Q4H 等用户等待事实,就应直接继续 drive;若已回到普通待用户输入态,则 `resume_dialog` 不应再被视为可继续。
178
- - **情况 2:当前对话仍有回复义务,但正在等待用户输入**
179
- 典型例子是 pending Q4H。此时 `Continue` 应退出插话 paused projection,并恢复成真实的 `blocked`。
180
- - **情况 3:当前对话仍有回复义务,但已不再 suspend,具备继续推进条件**
181
- 例如 Q4H/用户等待事实已消失,或存在允许继续的 queued prompt。此时 `Continue` 不应先落一个中间 `blocked/idle` 占位态,而应直接继续 drive。
182
-
183
- **因此有两个实现约束**:
184
-
185
- - `refreshRunControlProjectionFromPersistenceFacts()` 在用户尚未点击 `Continue` 前,必须保留这层“插话已处理;原任务已暂停”的 `stopped` 投影;否则 UI 会过早塌回普通 `blocked`,破坏多轮插话体验。反过来,如果当前其实没有待恢复原任务,则根本不应建立这层 paused projection。
186
- - 真正决定 `Continue` 结果的逻辑,必须在恢复驱动路径中重新读取 fresh persistence facts;不能把“可点 Continue”误解为“必然立即 proceeding”。
187
- - 若 `Continue` 后真源仍是 `blocked`,回复责任重申文案应当立即作为 runtime guide 同时进入 `dlg.msgs` 与持久化课程历史,并同步发前端气泡;这样后面真正恢复 drive 时只需正常读取上下文,不应再额外补发一条重复的 runtime prompt。
188
- - run-control 工具栏中的 `resumable` 计数,应与“是否允许手动 Continue 尝试”保持一致。因此,处于 interjection-paused `stopped` 的对话即便底层仍有用户等待事实,也应计入 `resumable`;因为 `Continue` 的业务语义正是“退出这层临时 paused projection,并从真源重判下一步”。
175
+ **关键点**:“是否还有用户插话待可见回复”和“是否还有跨对话回复义务”是两个独立业务事实。提醒项 footer 可以直接用这两个事实定制:插话尚未答复时优先答人;插话已答复且回复义务仍 active 时,继续按跨对话回贴要求收口。
189
176
 
190
177
  **心智模型提醒**:
191
178
 
192
- - 不能只看 `displayState.kind === 'stopped'` 就理解这条链路。
193
- - 不能只看用户等待事实就理解为什么 UI 仍显示 `stopped`。
194
- - 也不能只看 `resume_dialog` eligibility 就推断恢复后一定马上运行。
195
179
  - 更不能把所有 `origin === 'user'` 的输入都笼统视作“用户插话”;`q4hAnswerCallId` 非空的 prompt 是 askHuman answer continuation,必须按另一条语义链处理。
180
+ - 不能把 A2H 当成 Q4H;A2H 不阻塞 drive,也不把输入路由给智能体。
181
+ - 不能把 A2H 放进 Problems 面板;它属于“待人处理”,由 Ack 删除。
196
182
 
197
183
  必须把以下几块一起看,才能形成完整且精确的理解:
198
184
 
199
185
  - reply-guidance 中对插话轮的回复义务 suppression / deferred reassertion
200
- - flow 中“插话回复后停车”与“Continue fresh fact 二次判定”
201
- - dialog-display-state paused projection 的保留策略
202
- - websocket resume 入口对“可尝试 Continue”与“实际已恢复 drive”的区分
186
+ - 可见最终 `saying` 后对 pending user-interjection reply 的结算
187
+ - A2H 持久化与 Ack 流程
188
+ - 用户可见答复后的跨对话回复义务自动重申
203
189
 
204
190
  这是一条跨模块协同语义,不允许在单点上做“表面看起来更简单”的局部简化。
205
191
 
@@ -50,6 +50,8 @@ class DialogEventRegistryImpl {
50
50
  switch (evt.type) {
51
51
  case 'new_q4h_asked':
52
52
  case 'q4h_answered':
53
+ case 'new_a2h_answered':
54
+ case 'a2h_acknowledged':
53
55
  case 'sideDialog_created_evt':
54
56
  case 'dlg_background_callee_summary_evt':
55
57
  case 'dlg_touched_evt':
@@ -254,6 +254,22 @@ function resolveReminderContextFollowingDialogState(prompt, currentTurnDialogMsg
254
254
  return 'none';
255
255
  return prompt.origin === 'user' ? 'user_message' : 'runtime_notice';
256
256
  }
257
+ async function resolveReminderContextFooterState(args) {
258
+ const latest = await persistence_1.DialogPersistence.loadDialogLatest(args.dlg.id, args.dlg.status);
259
+ const deferredReplyReassertion = latest?.deferredReplyReassertion;
260
+ const activeReplyObligation = await persistence_1.DialogPersistence.loadActiveTellaskReplyObligation(args.dlg.id, args.dlg.status);
261
+ const pendingUserInterjectionReply = latest?.pendingUserInterjectionReply !== undefined;
262
+ const interDialogReplyObligation = deferredReplyReassertion?.reason === 'user_interjection_with_parked_original_task'
263
+ ? 'parked_by_user_interjection'
264
+ : activeReplyObligation !== undefined
265
+ ? 'active'
266
+ : 'none';
267
+ return {
268
+ followingDialogState: resolveReminderContextFollowingDialogState(args.prompt, args.currentTurnDialogMsgsForContext),
269
+ pendingUserInterjectionReply,
270
+ interDialogReplyObligation,
271
+ };
272
+ }
257
273
  function splitDialogMsgsForReminderInsertion(args) {
258
274
  const msgId = args.currentPrompt?.msgId;
259
275
  if (typeof msgId !== 'string' || msgId.trim() === '') {
@@ -281,6 +297,82 @@ function getUserOriginPromptMsgId(prompt) {
281
297
  ? prompt.msgId
282
298
  : undefined;
283
299
  }
300
+ function samePendingUserInterjectionMsg(pending, msgId) {
301
+ return pending.msgId === msgId;
302
+ }
303
+ async function maybeResolveAnsweredUserInterjection(args) {
304
+ if (args.userPromptMsgId === undefined ||
305
+ args.assistantSayingContent === null ||
306
+ args.assistantSayingContent.trim() === '' ||
307
+ args.assistantSayingGenseq === null) {
308
+ return;
309
+ }
310
+ for (const rawGenseq of args.functionCallGenseqs) {
311
+ if (!Number.isFinite(rawGenseq) || rawGenseq <= 0) {
312
+ continue;
313
+ }
314
+ if (args.assistantSayingGenseq <= Math.floor(rawGenseq)) {
315
+ return;
316
+ }
317
+ }
318
+ const latest = await persistence_1.DialogPersistence.loadDialogLatest(args.dlg.id, args.dlg.status);
319
+ const pending = latest?.pendingUserInterjectionReply;
320
+ if (pending === undefined || !samePendingUserInterjectionMsg(pending, args.userPromptMsgId)) {
321
+ return;
322
+ }
323
+ const course = args.dlg.activeGenCourseOrUndefined ?? args.dlg.currentCourse;
324
+ const answerIdSource = [
325
+ args.dlg.id.rootId,
326
+ args.dlg.id.selfId,
327
+ `c${String(course)}`,
328
+ `g${String(args.assistantSayingGenseq)}`,
329
+ pending.msgId,
330
+ ].join('|');
331
+ const answer = {
332
+ id: `a2h-${Buffer.from(answerIdSource).toString('base64url')}`,
333
+ content: args.assistantSayingContent,
334
+ answeredAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
335
+ userInterjection: {
336
+ msgId: pending.msgId,
337
+ course: pending.course,
338
+ genseq: pending.genseq,
339
+ },
340
+ answerRef: {
341
+ course,
342
+ genseq: args.assistantSayingGenseq,
343
+ },
344
+ };
345
+ const existingAnswers = await persistence_1.DialogPersistence.loadAnswersToHumanState(args.dlg.id, args.dlg.status);
346
+ if (!existingAnswers.some((item) => item.id === answer.id)) {
347
+ await persistence_1.DialogPersistence.appendAnswerToHumanState(args.dlg.id, answer, args.dlg.status);
348
+ const metadata = await persistence_1.DialogPersistence.loadDialogMetadata(args.dlg.id, args.dlg.status);
349
+ const taskDocPath = metadata?.taskDocPath ?? args.dlg.taskDocPath ?? '';
350
+ const event = {
351
+ type: 'new_a2h_answered',
352
+ answer: {
353
+ ...answer,
354
+ selfId: args.dlg.id.selfId,
355
+ rootId: args.dlg.id.rootId,
356
+ agentId: metadata?.agentId ?? args.dlg.agentId,
357
+ taskDocPath,
358
+ },
359
+ };
360
+ (0, evt_registry_1.postDialogEvent)(args.dlg, event);
361
+ }
362
+ await persistence_1.DialogPersistence.mutateDialogLatest(args.dlg.id, (previous) => {
363
+ const previousPending = previous.pendingUserInterjectionReply;
364
+ const userPromptMsgId = args.userPromptMsgId;
365
+ if (previousPending === undefined ||
366
+ userPromptMsgId === undefined ||
367
+ !samePendingUserInterjectionMsg(previousPending, userPromptMsgId)) {
368
+ return { kind: 'noop' };
369
+ }
370
+ return {
371
+ kind: 'patch',
372
+ patch: { pendingUserInterjectionReply: undefined },
373
+ };
374
+ }, args.dlg.status);
375
+ }
284
376
  async function persistAndEmitRuntimeGuide(dlg, content) {
285
377
  await dlg.addChatMessages({
286
378
  type: 'transient_guide_msg',
@@ -2009,16 +2101,16 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
2009
2101
  }
2010
2102
  lastBusinessContinuation = currentBusinessContinuation;
2011
2103
  let generationBodyError;
2104
+ const q4hAnswerCallId = normalizeQ4HAnswerCallId(currentPrompt?.q4hAnswerCallId);
2105
+ const isQ4HAnswerPrompt = q4hAnswerCallId !== undefined;
2012
2106
  try {
2013
2107
  if (currentPrompt) {
2014
2108
  if (currentPrompt.skipTaskdoc === true) {
2015
2109
  skipTaskdocForThisDrive = true;
2016
2110
  }
2017
2111
  const persistedUserLanguageCode = currentPrompt.userLanguageCode ?? dlg.getLastUserLanguageCode();
2018
- const q4hAnswerCallId = normalizeQ4HAnswerCallId(currentPrompt.q4hAnswerCallId);
2019
2112
  // `q4hAnswerCallId` marks a continuation input for an already-materialized askHuman
2020
2113
  // answer. It is not a second business-level user prompt that should re-enter transcript.
2021
- const isQ4HAnswerPrompt = q4hAnswerCallId !== undefined;
2022
2114
  const promptLanguage = persistedUserLanguageCode === 'zh' || persistedUserLanguageCode === 'en'
2023
2115
  ? persistedUserLanguageCode
2024
2116
  : (0, work_language_1.getWorkLanguage)();
@@ -2034,12 +2126,12 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
2034
2126
  // User interjection suppression is a reversible state transition, not a one-shot
2035
2127
  // latch. The normal cycle is:
2036
2128
  // - user interjects -> suppress reply obligation
2037
- // - operator clicks Continue -> restore reply obligation
2129
+ // - the visible local answer auto-reasserts the reply obligation
2038
2130
  // - user interjects again -> suppress it again
2039
2131
  //
2040
- // Therefore a repeated interjection after a blocked Continue MUST re-arm the deferred
2041
- // state and re-materialize the suppression guide, even when the underlying reply
2042
- // directive itself did not change.
2132
+ // Legacy blocked-Continue paths may also re-enter here. A repeated interjection MUST
2133
+ // re-arm the deferred state and re-materialize the suppression guide, even when the
2134
+ // underlying reply directive itself did not change.
2043
2135
  const existingDeferredReplyReassertion = await persistence_1.DialogPersistence.getDeferredReplyReassertion(dlg.id, dlg.status);
2044
2136
  const nextDeferredReplyReassertion = {
2045
2137
  reason: 'user_interjection_with_parked_original_task',
@@ -2077,6 +2169,18 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
2077
2169
  if (replyGuidance.promptContent === undefined) {
2078
2170
  throw new Error(`kernel-driver reply guidance invariant violation: missing prompt content for dialog=${dlg.id.valueOf()} msgId=${currentPrompt.msgId}`);
2079
2171
  }
2172
+ if (currentPrompt.origin === 'user' && !replyGuidance.isQ4HAnswerPrompt) {
2173
+ await persistence_1.DialogPersistence.mutateDialogLatest(dlg.id, () => ({
2174
+ kind: 'patch',
2175
+ patch: {
2176
+ pendingUserInterjectionReply: {
2177
+ msgId: currentPrompt.msgId,
2178
+ course: (0, storage_1.toDialogCourseNumber)(dlg.activeGenCourseOrUndefined ?? dlg.currentCourse),
2179
+ genseq: (0, storage_1.toCallSiteGenseqNo)(dlg.activeGenSeq),
2180
+ },
2181
+ },
2182
+ }), dlg.status);
2183
+ }
2080
2184
  const renderPromptAsRuntimeGuideBubble = currentPrompt.origin === 'runtime' &&
2081
2185
  (0, reply_prompt_copy_1.isStandaloneRuntimeGuidePromptContent)(replyGuidance.promptContent);
2082
2186
  if (currentRuntimeGuideMsg) {
@@ -2225,7 +2329,11 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
2225
2329
  {
2226
2330
  type: 'environment_msg',
2227
2331
  role: 'user',
2228
- content: (0, driver_messages_1.formatReminderContextFooter)((0, work_language_1.getWorkLanguage)(), resolveReminderContextFollowingDialogState(currentPrompt, splitDialogMsgs.currentTurnDialogMsgsForContext)),
2332
+ content: (0, driver_messages_1.formatReminderContextFooter)((0, work_language_1.getWorkLanguage)(), await resolveReminderContextFooterState({
2333
+ dlg,
2334
+ prompt: currentPrompt,
2335
+ currentTurnDialogMsgsForContext: splitDialogMsgs.currentTurnDialogMsgsForContext,
2336
+ })),
2229
2337
  },
2230
2338
  ]
2231
2339
  : renderedReminders;
@@ -2588,10 +2696,13 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
2588
2696
  }
2589
2697
  return { usage: res.usage, llmGenModel: res.llmGenModel };
2590
2698
  };
2699
+ const previousAssistantSayingGenseq = lastAssistantSayingGenseq;
2591
2700
  const llmOutput = await streamOrBatch();
2592
2701
  if (typeof llmOutput.llmGenModel === 'string' && llmOutput.llmGenModel.trim() !== '') {
2593
2702
  llmGenModelForGen = llmOutput.llmGenModel.trim();
2594
2703
  }
2704
+ let currentRoundAssistantSayingContent = null;
2705
+ let currentRoundAssistantSayingGenseq = null;
2595
2706
  contextHealthForGen = computeContextHealthSnapshot({
2596
2707
  providerCfg,
2597
2708
  model,
@@ -2618,6 +2729,8 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
2618
2729
  else {
2619
2730
  lastAssistantSayingContent = msg.content;
2620
2731
  lastAssistantSayingGenseq = msg.genseq;
2732
+ currentRoundAssistantSayingContent = msg.content;
2733
+ currentRoundAssistantSayingGenseq = msg.genseq;
2621
2734
  lastAssistantReplyTarget = currentReplyTarget;
2622
2735
  await emitAssistantSaying(dlg, msg.content);
2623
2736
  }
@@ -2735,11 +2848,13 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
2735
2848
  });
2736
2849
  streamedFuncCalls.length = 0;
2737
2850
  streamedFuncCalls.push(...normalizedStreamedFuncCalls);
2851
+ const currentRoundFunctionCallGenseqs = [];
2738
2852
  for (const call of streamedFuncCalls) {
2739
2853
  const rawCallGenseq = call.genseq;
2740
2854
  if (!Number.isFinite(rawCallGenseq) || rawCallGenseq <= 0)
2741
2855
  continue;
2742
2856
  const callGenseq = Math.floor(rawCallGenseq);
2857
+ currentRoundFunctionCallGenseqs.push(callGenseq);
2743
2858
  if (lastFunctionCallGenseq === null || callGenseq > lastFunctionCallGenseq) {
2744
2859
  lastFunctionCallGenseq = callGenseq;
2745
2860
  }
@@ -2761,6 +2876,21 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
2761
2876
  newMsgs.push(...routed.pairedMessages);
2762
2877
  }
2763
2878
  await dlg.addChatMessages(...newMsgs);
2879
+ const streamedCurrentRoundSayingContent = batchOutputs.length === 0 && lastAssistantSayingGenseq !== previousAssistantSayingGenseq
2880
+ ? lastAssistantSayingContent
2881
+ : null;
2882
+ const streamedCurrentRoundSayingGenseq = batchOutputs.length === 0 && lastAssistantSayingGenseq !== previousAssistantSayingGenseq
2883
+ ? lastAssistantSayingGenseq
2884
+ : null;
2885
+ if (currentPrompt?.origin === 'user' && !isQ4HAnswerPrompt) {
2886
+ await maybeResolveAnsweredUserInterjection({
2887
+ dlg,
2888
+ userPromptMsgId: currentPrompt.msgId,
2889
+ assistantSayingContent: currentRoundAssistantSayingContent ?? streamedCurrentRoundSayingContent,
2890
+ assistantSayingGenseq: currentRoundAssistantSayingGenseq ?? streamedCurrentRoundSayingGenseq,
2891
+ functionCallGenseqs: currentRoundFunctionCallGenseqs,
2892
+ });
2893
+ }
2764
2894
  const persistedFbrState = await loadDialogFbrState(dlg);
2765
2895
  if (persistedFbrState) {
2766
2896
  if (persistedFbrState.phase === 'finalization') {
@@ -1002,8 +1002,8 @@ async function maybeSurfaceDeferredReplyReassertionGuideForBlockedContinue(dialo
1002
1002
  // 3. emit runtime_guide_evt so the user immediately sees the reassertion bubble after Continue.
1003
1003
  //
1004
1004
  // Do not "optimize" this into only an event or only a deferred prompt. The whole point is that
1005
- // once blocked Continue is clicked, the guide becomes a first-class historical context fact and
1006
- // later real driving should need no special duplicate reassertion path.
1005
+ // once a legacy blocked Continue path surfaces the guide, it becomes a first-class historical
1006
+ // context fact and later real driving should need no special duplicate reassertion path.
1007
1007
  await surfaceRuntimeGuide(dialog, content);
1008
1008
  await persistence_1.DialogPersistence.setDeferredReplyReassertion(dialog.id, {
1009
1009
  reason: 'user_interjection_with_parked_original_task',
@@ -1399,9 +1399,9 @@ async function executeDriveRound(args) {
1399
1399
  if (!humanPrompt) {
1400
1400
  // WARNING:
1401
1401
  // `allowResumeFromInterrupted` covers multiple stop reasons, but the interjection-pause case
1402
- // is semantically special. Clicking Continue here does NOT mean "blindly clear stopped and
1403
- // drive". We must re-read the fresh persistence facts first because there are three distinct
1404
- // true-source cases behind the same visible resumption panel:
1402
+ // is semantically special. Continue here does NOT mean "blindly clear stopped and drive". We
1403
+ // must re-read the fresh persistence facts first because there are three distinct true-source
1404
+ // cases behind the same visible resumption panel:
1405
1405
  // - no active reply obligation / not suspended anymore -> continue real driving now
1406
1406
  // - active reply obligation + suspended -> restore the true suspension state
1407
1407
  // - active reply obligation + still proceeding continuation (for example queued prompt) ->
@@ -1623,6 +1623,15 @@ async function executeDriveRound(args) {
1623
1623
  !replyGuidance.isQ4HAnswerPrompt &&
1624
1624
  replyGuidance.suppressInterDialogReplyGuidance &&
1625
1625
  replyGuidance.deferredReplyReassertionDirective !== undefined;
1626
+ if (effectivePrompt?.origin === 'user' &&
1627
+ !replyGuidance.isQ4HAnswerPrompt &&
1628
+ !replyGuidance.suppressInterDialogReplyGuidance &&
1629
+ replyGuidance.deferredReplyReassertionDirective !== undefined) {
1630
+ await persistence_1.DialogPersistence.setDeferredReplyReassertion(dialog.id, {
1631
+ reason: 'user_interjection_with_parked_original_task',
1632
+ directive: replyGuidance.deferredReplyReassertionDirective,
1633
+ }, dialog.status);
1634
+ }
1626
1635
  activeTellaskReplyDirective =
1627
1636
  replyGuidance.activeReplyDirective ?? replyContinuationScope.directive();
1628
1637
  activePromptWasReplyToolReminder = isReplyToolReminderPrompt(effectivePrompt);
@@ -1927,6 +1936,36 @@ async function executeDriveRound(args) {
1927
1936
  }
1928
1937
  }
1929
1938
  }
1939
+ if (shouldPauseAfterLocalUserInterjection &&
1940
+ !interruptedBySignal &&
1941
+ followUp === undefined &&
1942
+ driveResult?.lastAssistantSayingContent !== null) {
1943
+ const deferredReplyReassertion = await persistence_1.DialogPersistence.getDeferredReplyReassertion(dialog.id, dialog.status);
1944
+ if (deferredReplyReassertion?.reason === 'user_interjection_with_parked_original_task') {
1945
+ const language = (0, work_language_1.getWorkLanguage)();
1946
+ const prompt = await (0, reply_guidance_1.buildReplyObligationReassertionPrompt)({
1947
+ dlg: dialog,
1948
+ directive: deferredReplyReassertion.directive,
1949
+ language,
1950
+ });
1951
+ followUp = {
1952
+ kind: 'runtime_reply_reminder',
1953
+ prompt,
1954
+ msgId: (0, id_1.generateShortId)(),
1955
+ grammar: 'markdown',
1956
+ origin: 'runtime',
1957
+ userLanguageCode: language,
1958
+ tellaskReplyDirective: deferredReplyReassertion.directive,
1959
+ };
1960
+ await persistence_1.DialogPersistence.setDeferredReplyReassertion(dialog.id, undefined, dialog.status);
1961
+ log_1.log.debug('kernel-driver queued automatic reply-obligation reassertion after user interjection answer', undefined, {
1962
+ dialogId: dialog.id.valueOf(),
1963
+ rootId: dialog.id.rootId,
1964
+ selfId: dialog.id.selfId,
1965
+ targetCallId: deferredReplyReassertion.directive.targetCallId,
1966
+ });
1967
+ }
1968
+ }
1930
1969
  if (followUp) {
1931
1970
  if (followUp.kind === 'runtime_reply_reminder' ||
1932
1971
  followUp.kind === 'runtime_sideDialog_reply_reminder') {
@@ -2016,21 +2055,8 @@ async function executeDriveRound(args) {
2016
2055
  shouldDriveQueuedPromptAfterCore = true;
2017
2056
  return driveResult;
2018
2057
  }
2019
- if (shouldPauseAfterLocalUserInterjection &&
2020
- !interruptedBySignal &&
2021
- followUp === undefined &&
2022
- driveResult?.lastAssistantSayingContent !== null) {
2023
- const pauseReason = (0, interjection_pause_stop_1.buildUserInterjectionPauseStopReason)();
2024
- await (0, dialog_display_state_1.setDialogDisplayState)(dialog.id, {
2025
- kind: 'stopped',
2026
- reason: pauseReason,
2027
- continueEnabled: true,
2028
- });
2029
- (0, dialog_display_state_1.broadcastDisplayStateMarker)(dialog.id, {
2030
- kind: 'interrupted',
2031
- reason: pauseReason,
2032
- });
2033
- log_1.log.debug('kernel-driver paused original task after local user interjection reply', undefined, {
2058
+ if (shouldPauseAfterLocalUserInterjection && !interruptedBySignal && followUp === undefined) {
2059
+ log_1.log.debug('kernel-driver observed local user interjection pause condition, but continuation is now fully automatic', undefined, {
2034
2060
  dialogId: dialog.id.valueOf(),
2035
2061
  rootId: dialog.id.rootId,
2036
2062
  selfId: dialog.id.selfId,
@@ -196,15 +196,15 @@ async function shouldSuppressInterDialogReplyGuidanceForUserInterjection(args) {
196
196
  // This suppression decision is not a cosmetic prompt tweak. It is one leg of the full
197
197
  // interjection-pause state machine:
198
198
  // 1. user interjection suppresses the live reply obligation here;
199
- // 2. `flow.ts` answers locally and parks the original task in a resumable stopped state;
200
- // 3. manual Continue later decides from fresh persistence facts whether the dialog should stay
201
- // blocked or resume real driving.
199
+ // 2. `flow.ts` answers locally and stores a deferred reply reassertion;
200
+ // 3. after a visible local answer, the driver reasserts the reply obligation automatically.
202
201
  //
203
202
  // Do not "simplify" this into a pure display-state check or a pure active-callee check.
204
203
  // Proceeding dialogs with a still-active reply obligation are part of the same rule: a fresh
205
204
  // user interjection should still suppress the live reply obligation and answer locally first.
206
- // The business anchor is the deferred reply reassertion, while the paused execution marker keeps
207
- // repeated interjection turns behaving as local side conversation until explicit Continue.
205
+ // The business anchor is the deferred reply reassertion. A stale paused execution marker still
206
+ // means a repeated interjection should behave as local side conversation, but it is not durable
207
+ // drive work and must not force a manual Continue after the answer is visible.
208
208
  const prompt = args.prompt;
209
209
  if (!prompt) {
210
210
  return false;
@@ -204,6 +204,11 @@ function getMemoryPromptCopy(ctx) {
204
204
  ? `工作流:先做事 → 再提炼(\`update_reminder\`;必要时整理差遣牒追加条目/更新提案并诉请 \`@${ctx.taskdocMaintainerId}\` 合并写入)→ 然后 \`clear_mind\` 清空噪音。`
205
205
  : '工作流:停止扩张上下文 → 维护足够详尽的接续包提醒项(`add_reminder` 或 `update_reminder`,长度没有技术限制)→ 然后 `clear_mind` 开启新一程。',
206
206
  mainDialogWorkflowLine: '工作流:先做事 -> 再提炼(`update_reminder` + `mind_more(progress)`;需要压缩/删旧时先 `recall_taskdoc` 取得 `content_hash`,再用带 `previous_content_hash` 的 `change_mind`;要删除整章文件时用 `never_mind`)-> 然后 `clear_mind` 清空噪音。',
207
+ ...(ctx.isSideDialog
208
+ ? {}
209
+ : {
210
+ progressVcsOrderLine: '硬性顺序:先补 `progress`,再动 git。只要这次代码/文档改动准备进 git(add / commit / push),先确认 `progress` 已经写清当前状态、决策、阻塞和下一步;如果还没写清,就先补写。简单判断:改动会进仓库 + `progress` 还没跟上 = 先别动 git。`git status` 只用来确认,不是提交动作。不要把“提交了某个 commit”“push 了”写进 `progress`。',
211
+ }),
207
212
  contextHealthLine: contextHealthLineZh,
208
213
  taskdocLogLine: taskdocLogLineZh,
209
214
  };
@@ -239,6 +244,11 @@ function getMemoryPromptCopy(ctx) {
239
244
  ? `Workflow: do work → distill (\`update_reminder\`; when Taskdoc needs updates, draft append entries, a merged replacement, or a section deletion and ask \`@${ctx.taskdocMaintainerId}\`) → then \`clear_mind\` to drop noise.`
240
245
  : 'Workflow: stop expanding context → maintain sufficiently detailed continuation-package reminders (`add_reminder` or `update_reminder`, with no technical length limit) → then `clear_mind` to start a new course.',
241
246
  mainDialogWorkflowLine: 'Workflow: do work -> distill (`update_reminder` + `mind_more(progress)`; when compression/deletion is needed, first use `recall_taskdoc` to get `content_hash`, then use `change_mind` with `previous_content_hash`; use `never_mind` when removing a whole section file) -> then `clear_mind` to drop noise.',
247
+ ...(ctx.isSideDialog
248
+ ? {}
249
+ : {
250
+ progressVcsOrderLine: 'Hard order: update `progress` first, then use git. If this code/docs change is going into git (add / commit / push), make sure `progress` already says the current state, decisions, blockers, and next step; if not, write it first. Simple check: change will enter the repo + `progress` is behind = stop and update `progress` first. `git status` is only for checking, not a commit step. Do not write git events like “committed X” or “ran git push” into progress.',
251
+ }),
242
252
  contextHealthLine: contextHealthLineEn,
243
253
  taskdocLogLine: taskdocLogLineEn,
244
254
  };
@@ -264,6 +274,7 @@ function buildMemorySystemPrompt(ctx) {
264
274
  ...(ctx.agentHasTeamMemoryTools ? [copy.teamMemoryHintLine] : []),
265
275
  ...(ctx.agentHasPersonalMemoryTools ? [copy.personalMemoryHintLine] : []),
266
276
  ctx.isSideDialog ? copy.sideDialogWorkflowLine : copy.mainDialogWorkflowLine,
277
+ ...(copy.progressVcsOrderLine === undefined ? [] : [copy.progressVcsOrderLine]),
267
278
  copy.contextHealthLine,
268
279
  copy.taskdocLogLine,
269
280
  ].join('\n');
@@ -1,4 +1,4 @@
1
- export type DomindsPersistenceFileSource = 'dialog_state' | 'dialog_latest' | 'dialog_metadata' | 'dialog_asker_stack' | 'dialog_course_events' | 'reminder_state' | 'questions4human_state' | 'active_callees' | 'wake_queue' | 'sideDialog_responses' | 'sideDialog_registry';
1
+ export type DomindsPersistenceFileSource = 'dialog_state' | 'dialog_latest' | 'dialog_metadata' | 'dialog_asker_stack' | 'dialog_course_events' | 'reminder_state' | 'questions4human_state' | 'answers2human_state' | 'active_callees' | 'wake_queue' | 'sideDialog_responses' | 'sideDialog_registry';
2
2
  export type DomindsPersistenceFileOperation = 'read' | 'parse';
3
3
  export type DomindsPersistenceFileFormat = 'yaml' | 'json' | 'jsonl';
4
4
  type DomindsPersistenceFileErrorArgs = {