dominds 1.23.5 → 1.23.6

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 (109) hide show
  1. package/dist/dialog-display-state.js +79 -30
  2. package/dist/dialog-interruption.js +2 -0
  3. package/dist/llm/kernel-driver/flow.js +140 -32
  4. package/dist/minds/system-prompt.js +4 -4
  5. package/dist/persistence.js +100 -14
  6. package/dist/runtime/inter-dialog-format.js +4 -4
  7. package/dist/runtime/reply-prompt-copy.js +4 -4
  8. package/dist/tools/prompts/control/en/principles.md +1 -1
  9. package/dist/tools/prompts/control/en/scenarios.md +1 -1
  10. package/dist/tools/prompts/control/en/tools.md +1 -1
  11. package/dist/tools/prompts/control/zh/principles.md +1 -1
  12. package/dist/tools/prompts/control/zh/scenarios.md +1 -1
  13. package/dist/tools/prompts/control/zh/tools.md +1 -1
  14. package/package.json +3 -3
  15. package/webapp/dist/assets/{_basePickBy-DMD1UhXs.js → _basePickBy-528dB5Tu.js} +3 -3
  16. package/webapp/dist/assets/{_basePickBy-DMD1UhXs.js.map → _basePickBy-528dB5Tu.js.map} +1 -1
  17. package/webapp/dist/assets/{_baseUniq-CsE8Qvwt.js → _baseUniq-DkdKmFUs.js} +2 -2
  18. package/webapp/dist/assets/{_baseUniq-CsE8Qvwt.js.map → _baseUniq-DkdKmFUs.js.map} +1 -1
  19. package/webapp/dist/assets/{arc-0h8sV6e1.js → arc-BXvXVeL_.js} +2 -2
  20. package/webapp/dist/assets/{arc-0h8sV6e1.js.map → arc-BXvXVeL_.js.map} +1 -1
  21. package/webapp/dist/assets/{architectureDiagram-2XIMDMQ5-BbMESECO.js → architectureDiagram-2XIMDMQ5-Ck1IMDXl.js} +7 -7
  22. package/webapp/dist/assets/{architectureDiagram-2XIMDMQ5-BbMESECO.js.map → architectureDiagram-2XIMDMQ5-Ck1IMDXl.js.map} +1 -1
  23. package/webapp/dist/assets/{blockDiagram-WCTKOSBZ-DwkN-9a4.js → blockDiagram-WCTKOSBZ-DLRhkTKE.js} +7 -7
  24. package/webapp/dist/assets/{blockDiagram-WCTKOSBZ-DwkN-9a4.js.map → blockDiagram-WCTKOSBZ-DLRhkTKE.js.map} +1 -1
  25. package/webapp/dist/assets/{c4Diagram-IC4MRINW-CGYONEh1.js → c4Diagram-IC4MRINW-D2Hc1l7q.js} +3 -3
  26. package/webapp/dist/assets/{c4Diagram-IC4MRINW-CGYONEh1.js.map → c4Diagram-IC4MRINW-D2Hc1l7q.js.map} +1 -1
  27. package/webapp/dist/assets/{channel-DbSJhm5-.js → channel-DuagLVFr.js} +2 -2
  28. package/webapp/dist/assets/{channel-DbSJhm5-.js.map → channel-DuagLVFr.js.map} +1 -1
  29. package/webapp/dist/assets/{chunk-4BX2VUAB-D1inRfgf.js → chunk-4BX2VUAB-BVowxdVQ.js} +2 -2
  30. package/webapp/dist/assets/{chunk-4BX2VUAB-D1inRfgf.js.map → chunk-4BX2VUAB-BVowxdVQ.js.map} +1 -1
  31. package/webapp/dist/assets/{chunk-55IACEB6-DL1IDg_h.js → chunk-55IACEB6-DOqixome.js} +2 -2
  32. package/webapp/dist/assets/{chunk-55IACEB6-DL1IDg_h.js.map → chunk-55IACEB6-DOqixome.js.map} +1 -1
  33. package/webapp/dist/assets/{chunk-FMBD7UC4-CugIlRDV.js → chunk-FMBD7UC4-BQE3IRbI.js} +2 -2
  34. package/webapp/dist/assets/{chunk-FMBD7UC4-CugIlRDV.js.map → chunk-FMBD7UC4-BQE3IRbI.js.map} +1 -1
  35. package/webapp/dist/assets/{chunk-JSJVCQXG-DKHSdeu1.js → chunk-JSJVCQXG-BWvy_u2h.js} +2 -2
  36. package/webapp/dist/assets/{chunk-JSJVCQXG-DKHSdeu1.js.map → chunk-JSJVCQXG-BWvy_u2h.js.map} +1 -1
  37. package/webapp/dist/assets/{chunk-KX2RTZJC-DCU9tkq6.js → chunk-KX2RTZJC-DsSmqNSf.js} +2 -2
  38. package/webapp/dist/assets/{chunk-KX2RTZJC-DCU9tkq6.js.map → chunk-KX2RTZJC-DsSmqNSf.js.map} +1 -1
  39. package/webapp/dist/assets/{chunk-NQ4KR5QH-DN3O2s2M.js → chunk-NQ4KR5QH-B3jQt0DX.js} +4 -4
  40. package/webapp/dist/assets/{chunk-NQ4KR5QH-DN3O2s2M.js.map → chunk-NQ4KR5QH-B3jQt0DX.js.map} +1 -1
  41. package/webapp/dist/assets/{chunk-QZHKN3VN-e3ztIJg0.js → chunk-QZHKN3VN-CWST9WcY.js} +2 -2
  42. package/webapp/dist/assets/{chunk-QZHKN3VN-e3ztIJg0.js.map → chunk-QZHKN3VN-CWST9WcY.js.map} +1 -1
  43. package/webapp/dist/assets/{chunk-WL4C6EOR-Dv907NPM.js → chunk-WL4C6EOR-DjGCVqJN.js} +6 -6
  44. package/webapp/dist/assets/{chunk-WL4C6EOR-Dv907NPM.js.map → chunk-WL4C6EOR-DjGCVqJN.js.map} +1 -1
  45. package/webapp/dist/assets/{classDiagram-VBA2DB6C-DOTXtxYZ.js → classDiagram-VBA2DB6C-BnjkPcus.js} +7 -7
  46. package/webapp/dist/assets/{classDiagram-VBA2DB6C-DOTXtxYZ.js.map → classDiagram-VBA2DB6C-BnjkPcus.js.map} +1 -1
  47. package/webapp/dist/assets/{classDiagram-v2-RAHNMMFH-DOTXtxYZ.js → classDiagram-v2-RAHNMMFH-BnjkPcus.js} +7 -7
  48. package/webapp/dist/assets/{classDiagram-v2-RAHNMMFH-DOTXtxYZ.js.map → classDiagram-v2-RAHNMMFH-BnjkPcus.js.map} +1 -1
  49. package/webapp/dist/assets/{clone-6lYQMWpu.js → clone-BlToIURl.js} +2 -2
  50. package/webapp/dist/assets/{clone-6lYQMWpu.js.map → clone-BlToIURl.js.map} +1 -1
  51. package/webapp/dist/assets/{cose-bilkent-S5V4N54A-DoJeDXV0.js → cose-bilkent-S5V4N54A-BDVnPWt2.js} +2 -2
  52. package/webapp/dist/assets/{cose-bilkent-S5V4N54A-DoJeDXV0.js.map → cose-bilkent-S5V4N54A-BDVnPWt2.js.map} +1 -1
  53. package/webapp/dist/assets/{dagre-KLK3FWXG-F_n_vhV9.js → dagre-KLK3FWXG-aEZUtpHt.js} +7 -7
  54. package/webapp/dist/assets/{dagre-KLK3FWXG-F_n_vhV9.js.map → dagre-KLK3FWXG-aEZUtpHt.js.map} +1 -1
  55. package/webapp/dist/assets/{diagram-E7M64L7V-Crwhgyjv.js → diagram-E7M64L7V-CvNVSxxk.js} +8 -8
  56. package/webapp/dist/assets/{diagram-E7M64L7V-Crwhgyjv.js.map → diagram-E7M64L7V-CvNVSxxk.js.map} +1 -1
  57. package/webapp/dist/assets/{diagram-IFDJBPK2-CIt1nnn5.js → diagram-IFDJBPK2-Cvwaoava.js} +7 -7
  58. package/webapp/dist/assets/{diagram-IFDJBPK2-CIt1nnn5.js.map → diagram-IFDJBPK2-Cvwaoava.js.map} +1 -1
  59. package/webapp/dist/assets/{diagram-P4PSJMXO-qowipEfV.js → diagram-P4PSJMXO-ffnT7Lr_.js} +7 -7
  60. package/webapp/dist/assets/{diagram-P4PSJMXO-qowipEfV.js.map → diagram-P4PSJMXO-ffnT7Lr_.js.map} +1 -1
  61. package/webapp/dist/assets/{erDiagram-INFDFZHY-DV2BcYNa.js → erDiagram-INFDFZHY-DvGIVeJS.js} +5 -5
  62. package/webapp/dist/assets/{erDiagram-INFDFZHY-DV2BcYNa.js.map → erDiagram-INFDFZHY-DvGIVeJS.js.map} +1 -1
  63. package/webapp/dist/assets/{flowDiagram-PKNHOUZH-CAbWV161.js → flowDiagram-PKNHOUZH-BkQUpSc9.js} +7 -7
  64. package/webapp/dist/assets/{flowDiagram-PKNHOUZH-CAbWV161.js.map → flowDiagram-PKNHOUZH-BkQUpSc9.js.map} +1 -1
  65. package/webapp/dist/assets/{ganttDiagram-A5KZAMGK-CfdR7FRr.js → ganttDiagram-A5KZAMGK-BlG96EZZ.js} +3 -3
  66. package/webapp/dist/assets/{ganttDiagram-A5KZAMGK-CfdR7FRr.js.map → ganttDiagram-A5KZAMGK-BlG96EZZ.js.map} +1 -1
  67. package/webapp/dist/assets/{gitGraphDiagram-K3NZZRJ6-DuJFTELz.js → gitGraphDiagram-K3NZZRJ6-CnyjUBR4.js} +8 -8
  68. package/webapp/dist/assets/{gitGraphDiagram-K3NZZRJ6-DuJFTELz.js.map → gitGraphDiagram-K3NZZRJ6-CnyjUBR4.js.map} +1 -1
  69. package/webapp/dist/assets/{graph-cjRyzujT.js → graph-D-OO7MVR.js} +3 -3
  70. package/webapp/dist/assets/{graph-cjRyzujT.js.map → graph-D-OO7MVR.js.map} +1 -1
  71. package/webapp/dist/assets/{index-DgfF56L4.js → index-DvqI98wY.js} +39 -33
  72. package/webapp/dist/assets/{index-DgfF56L4.js.map → index-DvqI98wY.js.map} +1 -1
  73. package/webapp/dist/assets/{infoDiagram-LFFYTUFH-3wx-7AdD.js → infoDiagram-LFFYTUFH-Bid564Un.js} +6 -6
  74. package/webapp/dist/assets/{infoDiagram-LFFYTUFH-3wx-7AdD.js.map → infoDiagram-LFFYTUFH-Bid564Un.js.map} +1 -1
  75. package/webapp/dist/assets/{ishikawaDiagram-PHBUUO56-g6CMb1Qc.js → ishikawaDiagram-PHBUUO56-BoU1GXkx.js} +2 -2
  76. package/webapp/dist/assets/{ishikawaDiagram-PHBUUO56-g6CMb1Qc.js.map → ishikawaDiagram-PHBUUO56-BoU1GXkx.js.map} +1 -1
  77. package/webapp/dist/assets/{journeyDiagram-4ABVD52K-DdCcmOBO.js → journeyDiagram-4ABVD52K-C-JJRe4y.js} +5 -5
  78. package/webapp/dist/assets/{journeyDiagram-4ABVD52K-DdCcmOBO.js.map → journeyDiagram-4ABVD52K-C-JJRe4y.js.map} +1 -1
  79. package/webapp/dist/assets/{kanban-definition-K7BYSVSG-BFw2emGl.js → kanban-definition-K7BYSVSG-BPGHC2fL.js} +3 -3
  80. package/webapp/dist/assets/{kanban-definition-K7BYSVSG-BFw2emGl.js.map → kanban-definition-K7BYSVSG-BPGHC2fL.js.map} +1 -1
  81. package/webapp/dist/assets/{layout-Clazq06r.js → layout-BFpoiNr0.js} +5 -5
  82. package/webapp/dist/assets/{layout-Clazq06r.js.map → layout-BFpoiNr0.js.map} +1 -1
  83. package/webapp/dist/assets/{linear-jdsBGgvD.js → linear-BwnDVwt9.js} +2 -2
  84. package/webapp/dist/assets/{linear-jdsBGgvD.js.map → linear-BwnDVwt9.js.map} +1 -1
  85. package/webapp/dist/assets/{mindmap-definition-YRQLILUH-DLSZrW6l.js → mindmap-definition-YRQLILUH-D4aHh1Ye.js} +4 -4
  86. package/webapp/dist/assets/{mindmap-definition-YRQLILUH-DLSZrW6l.js.map → mindmap-definition-YRQLILUH-D4aHh1Ye.js.map} +1 -1
  87. package/webapp/dist/assets/{pieDiagram-SKSYHLDU-Uj-Zpci6.js → pieDiagram-SKSYHLDU-DIp7yy6V.js} +8 -8
  88. package/webapp/dist/assets/{pieDiagram-SKSYHLDU-Uj-Zpci6.js.map → pieDiagram-SKSYHLDU-DIp7yy6V.js.map} +1 -1
  89. package/webapp/dist/assets/{quadrantDiagram-337W2JSQ-DO7Sl1nV.js → quadrantDiagram-337W2JSQ-uKOhvCPR.js} +3 -3
  90. package/webapp/dist/assets/{quadrantDiagram-337W2JSQ-DO7Sl1nV.js.map → quadrantDiagram-337W2JSQ-uKOhvCPR.js.map} +1 -1
  91. package/webapp/dist/assets/{requirementDiagram-Z7DCOOCP-WrurrDKQ.js → requirementDiagram-Z7DCOOCP-Da_5DlcQ.js} +4 -4
  92. package/webapp/dist/assets/{requirementDiagram-Z7DCOOCP-WrurrDKQ.js.map → requirementDiagram-Z7DCOOCP-Da_5DlcQ.js.map} +1 -1
  93. package/webapp/dist/assets/{sankeyDiagram-WA2Y5GQK-gcxbxuZB.js → sankeyDiagram-WA2Y5GQK-P3UD1XYS.js} +2 -2
  94. package/webapp/dist/assets/{sankeyDiagram-WA2Y5GQK-gcxbxuZB.js.map → sankeyDiagram-WA2Y5GQK-P3UD1XYS.js.map} +1 -1
  95. package/webapp/dist/assets/{sequenceDiagram-2WXFIKYE-B98U2Npa.js → sequenceDiagram-2WXFIKYE-jY-eNlAg.js} +4 -4
  96. package/webapp/dist/assets/{sequenceDiagram-2WXFIKYE-B98U2Npa.js.map → sequenceDiagram-2WXFIKYE-jY-eNlAg.js.map} +1 -1
  97. package/webapp/dist/assets/{stateDiagram-RAJIS63D-BUgfHMbd.js → stateDiagram-RAJIS63D-HMXNbLUd.js} +9 -9
  98. package/webapp/dist/assets/{stateDiagram-RAJIS63D-BUgfHMbd.js.map → stateDiagram-RAJIS63D-HMXNbLUd.js.map} +1 -1
  99. package/webapp/dist/assets/{stateDiagram-v2-FVOUBMTO-C8gH0rSW.js → stateDiagram-v2-FVOUBMTO-C-50Qbn8.js} +5 -5
  100. package/webapp/dist/assets/{stateDiagram-v2-FVOUBMTO-C8gH0rSW.js.map → stateDiagram-v2-FVOUBMTO-C-50Qbn8.js.map} +1 -1
  101. package/webapp/dist/assets/{timeline-definition-YZTLITO2-DnVikX3B.js → timeline-definition-YZTLITO2-CkiLYjSG.js} +3 -3
  102. package/webapp/dist/assets/{timeline-definition-YZTLITO2-DnVikX3B.js.map → timeline-definition-YZTLITO2-CkiLYjSG.js.map} +1 -1
  103. package/webapp/dist/assets/{treemap-KZPCXAKY-BjhjT1IM.js → treemap-KZPCXAKY-DKYYu8t-.js} +5 -5
  104. package/webapp/dist/assets/{treemap-KZPCXAKY-BjhjT1IM.js.map → treemap-KZPCXAKY-DKYYu8t-.js.map} +1 -1
  105. package/webapp/dist/assets/{vennDiagram-LZ73GAT5-CXjPMxrl.js → vennDiagram-LZ73GAT5-n9k6D3Up.js} +2 -2
  106. package/webapp/dist/assets/{vennDiagram-LZ73GAT5-CXjPMxrl.js.map → vennDiagram-LZ73GAT5-n9k6D3Up.js.map} +1 -1
  107. package/webapp/dist/assets/{xychartDiagram-JWTSCODW-ByKmk3Cb.js → xychartDiagram-JWTSCODW-DJZb5SW1.js} +3 -3
  108. package/webapp/dist/assets/{xychartDiagram-JWTSCODW-ByKmk3Cb.js.map → xychartDiagram-JWTSCODW-DJZb5SW1.js.map} +1 -1
  109. package/webapp/dist/index.html +1 -1
@@ -96,6 +96,25 @@ function isSideDialogResponseAnchor(record) {
96
96
  function isNonIdleDisplayProjection(state) {
97
97
  return state !== undefined && state.kind !== 'idle_waiting_user';
98
98
  }
99
+ function pendingReplyObligationDisplayState() {
100
+ return {
101
+ kind: 'stopped',
102
+ reason: { kind: 'pending_reply_obligation' },
103
+ continueEnabled: true,
104
+ };
105
+ }
106
+ function blockerDisplayState(args) {
107
+ if (args.hasQ4H && args.hasSideDialogs) {
108
+ return { kind: 'blocked', reason: { kind: 'needs_human_input_and_sideDialogs' } };
109
+ }
110
+ if (args.hasQ4H) {
111
+ return { kind: 'blocked', reason: { kind: 'needs_human_input' } };
112
+ }
113
+ if (args.hasSideDialogs) {
114
+ return { kind: 'blocked', reason: { kind: 'waiting_for_sideDialogs' } };
115
+ }
116
+ return undefined;
117
+ }
99
118
  async function hasSideDialogFinalResponseAnchor(dialogId, latest) {
100
119
  if (dialogId.selfId === dialogId.rootId) {
101
120
  return false;
@@ -111,6 +130,35 @@ async function hasSideDialogFinalResponseAnchor(dialogId, latest) {
111
130
  }
112
131
  return false;
113
132
  }
133
+ async function hasActiveSideDialogReplyObligation(dialogId) {
134
+ if (dialogId.selfId === dialogId.rootId) {
135
+ return false;
136
+ }
137
+ const activeObligation = await persistence_1.DialogPersistence.loadActiveTellaskReplyObligation(dialogId, 'running');
138
+ return activeObligation !== undefined;
139
+ }
140
+ async function coerceIdleDisplayStateForActiveSideDialogReplyObligation(dialogId, displayState) {
141
+ if (displayState.kind !== 'idle_waiting_user') {
142
+ return displayState;
143
+ }
144
+ if (!(await hasActiveSideDialogReplyObligation(dialogId))) {
145
+ return displayState;
146
+ }
147
+ const q4h = await persistence_1.DialogPersistence.loadQuestions4HumanState(dialogId, 'running');
148
+ const pendingSideDialogs = await persistence_1.DialogPersistence.loadPendingSideDialogs(dialogId, 'running');
149
+ const blocked = blockerDisplayState({
150
+ hasQ4H: q4h.length > 0,
151
+ hasSideDialogs: pendingSideDialogs.length > 0,
152
+ });
153
+ const healedDisplayState = blocked ?? pendingReplyObligationDisplayState();
154
+ log.warn('Prevented sideDialog with active reply obligation from entering idle display state', new Error('sideDialog idle display-state invariant violation'), {
155
+ dialogId: dialogId.valueOf(),
156
+ rootId: dialogId.rootId,
157
+ selfId: dialogId.selfId,
158
+ healedDisplayState,
159
+ });
160
+ return healedDisplayState;
161
+ }
114
162
  function classifyRunControlBucket(state) {
115
163
  if (!state)
116
164
  return 'none';
@@ -345,6 +393,7 @@ async function clearDialogInterruptedExecutionMarker(dialogId) {
345
393
  await setDialogExecutionMarker(dialogId, undefined);
346
394
  }
347
395
  async function setDialogDisplayState(dialogId, displayState) {
396
+ displayState = await coerceIdleDisplayStateForActiveSideDialogReplyObligation(dialogId, displayState);
348
397
  if (displayState.kind === 'dead' && dialogId.selfId === dialogId.rootId) {
349
398
  log.warn('Rejecting dead displayState for main dialog (main dialogs must not be dead)', undefined, {
350
399
  dialogId: dialogId.valueOf(),
@@ -441,7 +490,8 @@ async function computeIdleDisplayState(dlg) {
441
490
  latest.executionMarker.kind === 'dead') {
442
491
  return { kind: 'dead', reason: latest.executionMarker.reason };
443
492
  }
444
- if (latest?.executionMarker?.kind === 'interrupted') {
493
+ if (latest?.executionMarker?.kind === 'interrupted' &&
494
+ latest.executionMarker.reason.kind !== 'pending_reply_obligation') {
445
495
  return {
446
496
  kind: 'stopped',
447
497
  reason: latest.executionMarker.reason,
@@ -457,14 +507,12 @@ async function computeIdleDisplayState(dlg) {
457
507
  }
458
508
  const hasQ4H = await dlg.hasPendingQ4H();
459
509
  const hasSideDialogs = await dlg.hasPendingSideDialogs();
460
- if (hasQ4H && hasSideDialogs) {
461
- return { kind: 'blocked', reason: { kind: 'needs_human_input_and_sideDialogs' } };
510
+ const blocked = blockerDisplayState({ hasQ4H, hasSideDialogs });
511
+ if (blocked) {
512
+ return blocked;
462
513
  }
463
- if (hasQ4H) {
464
- return { kind: 'blocked', reason: { kind: 'needs_human_input' } };
465
- }
466
- if (hasSideDialogs) {
467
- return { kind: 'blocked', reason: { kind: 'waiting_for_sideDialogs' } };
514
+ if (await hasActiveSideDialogReplyObligation(dlg.id)) {
515
+ return pendingReplyObligationDisplayState();
468
516
  }
469
517
  return { kind: 'idle_waiting_user' };
470
518
  }
@@ -480,7 +528,8 @@ async function computeIdleDisplayStateFromPersistence(dialogId) {
480
528
  latest.executionMarker.kind === 'dead') {
481
529
  return { kind: 'dead', reason: latest.executionMarker.reason };
482
530
  }
483
- if (latest?.executionMarker?.kind === 'interrupted') {
531
+ if (latest?.executionMarker?.kind === 'interrupted' &&
532
+ latest.executionMarker.reason.kind !== 'pending_reply_obligation') {
484
533
  return {
485
534
  kind: 'stopped',
486
535
  reason: latest.executionMarker.reason,
@@ -494,21 +543,19 @@ async function computeIdleDisplayStateFromPersistence(dialogId) {
494
543
  continueEnabled: true,
495
544
  };
496
545
  }
497
- if (latest && (await hasSideDialogFinalResponseAnchor(dialogId, latest))) {
498
- return { kind: 'idle_waiting_user' };
499
- }
500
546
  const q4h = await persistence_1.DialogPersistence.loadQuestions4HumanState(dialogId, 'running');
501
547
  const pendingSideDialogs = await persistence_1.DialogPersistence.loadPendingSideDialogs(dialogId, 'running');
502
548
  const hasQ4H = q4h.length > 0;
503
549
  const hasSideDialogs = pendingSideDialogs.length > 0;
504
- if (hasQ4H && hasSideDialogs) {
505
- return { kind: 'blocked', reason: { kind: 'needs_human_input_and_sideDialogs' } };
550
+ const blocked = blockerDisplayState({ hasQ4H, hasSideDialogs });
551
+ if (blocked) {
552
+ return blocked;
506
553
  }
507
- if (hasQ4H) {
508
- return { kind: 'blocked', reason: { kind: 'needs_human_input' } };
554
+ if (await hasActiveSideDialogReplyObligation(dialogId)) {
555
+ return pendingReplyObligationDisplayState();
509
556
  }
510
- if (hasSideDialogs) {
511
- return { kind: 'blocked', reason: { kind: 'waiting_for_sideDialogs' } };
557
+ if (latest && (await hasSideDialogFinalResponseAnchor(dialogId, latest))) {
558
+ return { kind: 'idle_waiting_user' };
512
559
  }
513
560
  return { kind: 'idle_waiting_user' };
514
561
  }
@@ -602,23 +649,22 @@ async function refreshRunControlProjectionFromPersistenceFacts(dialogId, trigger
602
649
  continueEnabled: true,
603
650
  };
604
651
  }
605
- if (await hasSideDialogFinalResponseAnchor(dialogId, latest)) {
606
- return { kind: 'idle_waiting_user' };
607
- }
608
652
  const q4h = await persistence_1.DialogPersistence.loadQuestions4HumanState(dialogId, 'running');
609
653
  const pendingSideDialogs = await persistence_1.DialogPersistence.loadPendingSideDialogs(dialogId, 'running');
610
654
  const hasQ4H = q4h.length > 0;
611
655
  const hasSideDialogs = pendingSideDialogs.length > 0;
612
- if (hasQ4H && hasSideDialogs) {
613
- return { kind: 'blocked', reason: { kind: 'needs_human_input_and_sideDialogs' } };
656
+ const blocked = blockerDisplayState({ hasQ4H, hasSideDialogs });
657
+ if (blocked) {
658
+ return blocked;
614
659
  }
615
- if (hasQ4H) {
616
- return { kind: 'blocked', reason: { kind: 'needs_human_input' } };
660
+ if (await hasActiveSideDialogReplyObligation(dialogId)) {
661
+ return pendingReplyObligationDisplayState();
617
662
  }
618
- if (hasSideDialogs) {
619
- return { kind: 'blocked', reason: { kind: 'waiting_for_sideDialogs' } };
663
+ if (await hasSideDialogFinalResponseAnchor(dialogId, latest)) {
664
+ return { kind: 'idle_waiting_user' };
620
665
  }
621
- if (latest.executionMarker?.kind === 'interrupted') {
666
+ if (latest.executionMarker?.kind === 'interrupted' &&
667
+ latest.executionMarker.reason.kind !== 'pending_reply_obligation') {
622
668
  return {
623
669
  kind: 'stopped',
624
670
  reason: latest.executionMarker.reason,
@@ -673,7 +719,9 @@ function isRecoverableGeneratingLatest(latest) {
673
719
  if (marker.kind === 'dead') {
674
720
  return false;
675
721
  }
676
- return marker.kind !== 'interrupted' || marker.reason.kind === 'pending_course_start';
722
+ return (marker.kind !== 'interrupted' ||
723
+ marker.reason.kind === 'pending_course_start' ||
724
+ marker.reason.kind === 'pending_reply_obligation');
677
725
  }
678
726
  async function reconcileDisplayStatesAfterRestart() {
679
727
  const dialogIds = await persistence_1.DialogPersistence.listAllDialogIds('running');
@@ -743,7 +791,8 @@ async function reconcileDisplayStatesAfterRestart() {
743
791
  needsDrive: true,
744
792
  displayState: { kind: 'proceeding' },
745
793
  executionMarker: existingMarker?.kind === 'interrupted' &&
746
- existingMarker.reason.kind === 'pending_course_start'
794
+ (existingMarker.reason.kind === 'pending_course_start' ||
795
+ existingMarker.reason.kind === 'pending_reply_obligation')
747
796
  ? undefined
748
797
  : existingMarker,
749
798
  },
@@ -24,6 +24,7 @@ function isInterruptionReasonManualResumeEligible(reason) {
24
24
  case 'emergency_stop':
25
25
  case 'server_restart':
26
26
  case 'pending_course_start':
27
+ case 'pending_reply_obligation':
27
28
  case 'fork_continue_ready':
28
29
  case 'system_stop':
29
30
  case 'llm_retry_stopped':
@@ -37,6 +38,7 @@ function isInterruptionReasonManualResumeEligible(reason) {
37
38
  function doesInterruptionReasonRequireExplicitResume(reason) {
38
39
  switch (reason.kind) {
39
40
  case 'pending_course_start':
41
+ case 'pending_reply_obligation':
40
42
  return false;
41
43
  case 'user_stop':
42
44
  case 'emergency_stop':
@@ -23,6 +23,26 @@ const idle_reminder_wake_1 = require("./idle-reminder-wake");
23
23
  const reply_guidance_1 = require("./reply-guidance");
24
24
  const sideDialog_1 = require("./sideDialog");
25
25
  const tellask_special_1 = require("./tellask-special");
26
+ function buildRuntimeReplyReminderFollowUp(args) {
27
+ const common = {
28
+ prompt: args.prompt,
29
+ msgId: (0, id_1.generateShortId)(),
30
+ grammar: 'markdown',
31
+ origin: 'runtime',
32
+ userLanguageCode: args.language,
33
+ tellaskReplyDirective: args.directive,
34
+ };
35
+ return args.sideDialogReplyTarget === undefined
36
+ ? {
37
+ kind: 'runtime_reply_reminder',
38
+ ...common,
39
+ }
40
+ : {
41
+ kind: 'runtime_sideDialog_reply_reminder',
42
+ ...common,
43
+ sideDialogReplyTarget: args.sideDialogReplyTarget,
44
+ };
45
+ }
26
46
  async function queueReplyReminderFollowUp(args) {
27
47
  if (args.followUp.kind === 'runtime_sideDialog_reply_reminder') {
28
48
  await args.dialog.queueRuntimeSideDialogPrompt({
@@ -74,6 +94,10 @@ function resolveDirectFallbackResponse(args) {
74
94
  source: 'saying',
75
95
  };
76
96
  }
97
+ // Thinking-only output is intentionally a fallback candidate: some providers/models can finish a
98
+ // Side Dialog with useful content in thinking and no public saying. This helper only extracts the
99
+ // candidate; callers below must still reject it when a same-round function/tellask call needs
100
+ // auto-continuation, when the dialog is suspended, or when another follow-up prompt is queued.
77
101
  if (args.driveResult.lastAssistantThinkingContent !== null &&
78
102
  args.driveResult.lastAssistantThinkingContent.trim() !== '') {
79
103
  if (typeof args.driveResult.lastAssistantThinkingGenseq !== 'number' ||
@@ -185,6 +209,79 @@ async function loadPendingDiagnosticsSnapshot(args) {
185
209
  };
186
210
  }
187
211
  }
212
+ async function hasAssistantOutputAfterAssignmentAnchor(args) {
213
+ const events = await persistence_1.DialogPersistence.loadCourseEvents(args.dialog.id, args.dialog.currentCourse, args.dialog.status);
214
+ let assignmentGenseq;
215
+ for (const event of events) {
216
+ if (event.type === 'tellask_anchor_record' &&
217
+ event.anchorRole === 'assignment' &&
218
+ event.callId === args.callId) {
219
+ assignmentGenseq = event.genseq;
220
+ continue;
221
+ }
222
+ if (assignmentGenseq !== undefined &&
223
+ (event.type === 'agent_thought_record' || event.type === 'agent_words_record') &&
224
+ event.genseq >= assignmentGenseq &&
225
+ event.content.trim() !== '') {
226
+ return true;
227
+ }
228
+ }
229
+ return false;
230
+ }
231
+ async function resolveStrandedSideDialogReplyReminderFollowUp(args) {
232
+ const latest = await persistence_1.DialogPersistence.loadDialogLatest(args.dialog.id, args.dialog.status);
233
+ const displayState = latest?.displayState;
234
+ const isRecoverableProjection = displayState?.kind === 'idle_waiting_user' ||
235
+ (displayState?.kind === 'stopped' && displayState.reason.kind === 'pending_reply_obligation');
236
+ if (!latest ||
237
+ !isRecoverableProjection ||
238
+ latest.pendingCourseStartPrompt !== undefined ||
239
+ latest.executionMarker?.kind === 'dead') {
240
+ return undefined;
241
+ }
242
+ const directive = await (0, tellask_special_1.loadActiveTellaskReplyDirective)(args.dialog);
243
+ if (!directive) {
244
+ return undefined;
245
+ }
246
+ const ownerDialogId = directive.targetDialogId.trim();
247
+ if (ownerDialogId === '') {
248
+ throw new Error(`stranded sideDialog reply recovery invariant violation: empty targetDialogId ` +
249
+ `(dialogId=${args.dialog.id.valueOf()}, targetCallId=${directive.targetCallId})`);
250
+ }
251
+ const pending = await persistence_1.DialogPersistence.loadPendingSideDialogs(new dialog_1.DialogID(ownerDialogId, args.dialog.id.rootId), args.dialog.status);
252
+ const pendingRecord = pending.find((record) => record.sideDialogId === args.dialog.id.selfId && record.callId === directive.targetCallId);
253
+ if (!pendingRecord) {
254
+ return undefined;
255
+ }
256
+ if (!(await hasAssistantOutputAfterAssignmentAnchor({
257
+ dialog: args.dialog,
258
+ callId: pendingRecord.callId,
259
+ }))) {
260
+ return undefined;
261
+ }
262
+ const language = (0, work_language_1.getWorkLanguage)();
263
+ const sideDialogReplyTarget = {
264
+ ownerDialogId,
265
+ callType: pendingRecord.callType,
266
+ callId: pendingRecord.callId,
267
+ callSiteCourse: pendingRecord.callSiteCourse,
268
+ callSiteGenseq: pendingRecord.callSiteGenseq,
269
+ };
270
+ return {
271
+ kind: 'runtime_sideDialog_reply_reminder',
272
+ prompt: await buildReplyToolReminderPrompt({
273
+ dlg: args.dialog,
274
+ directive,
275
+ language,
276
+ }),
277
+ msgId: (0, id_1.generateShortId)(),
278
+ grammar: 'markdown',
279
+ origin: 'runtime',
280
+ userLanguageCode: language,
281
+ tellaskReplyDirective: directive,
282
+ sideDialogReplyTarget,
283
+ };
284
+ }
188
285
  async function clearConsumedDeferredRootQueueIfIdle(dialog) {
189
286
  if (dialog.id.selfId !== dialog.id.rootId) {
190
287
  return;
@@ -366,6 +463,8 @@ async function inspectNoPromptSideDialogDrive(args) {
366
463
  const inProgressGenerationResumeAllowed = args.driveOptions?.resumeInProgressGeneration === true;
367
464
  const supplyResponseParentReviveAllowed = source === 'kernel_driver_supply_response_parent_revive' &&
368
465
  hasNoPromptSideDialogResumeEntitlement(args.dialog, args.driveOptions);
466
+ const pendingReplyObligationResumeAllowed = latest?.executionMarker?.kind === 'interrupted' &&
467
+ latest.executionMarker.reason.kind === 'pending_reply_obligation';
369
468
  if (lastEvent?.type === 'tellask_anchor_record' && lastEvent.anchorRole === 'response') {
370
469
  return {
371
470
  shouldReject: true,
@@ -378,7 +477,8 @@ async function inspectNoPromptSideDialogDrive(args) {
378
477
  }
379
478
  if (!explicitInterruptedResumeAllowed &&
380
479
  !inProgressGenerationResumeAllowed &&
381
- !supplyResponseParentReviveAllowed) {
480
+ !supplyResponseParentReviveAllowed &&
481
+ !pendingReplyObligationResumeAllowed) {
382
482
  return {
383
483
  shouldReject: true,
384
484
  source,
@@ -640,6 +740,29 @@ async function executeDriveRound(args) {
640
740
  // suspended by pending Q4H or sideDialogs. This prevents duplicate generations when
641
741
  // multiple wake-ups race around the same sideDialog completion boundary.
642
742
  if (!humanPrompt) {
743
+ if (dialog instanceof dialog_1.SideDialog && !dialog.hasUpNext()) {
744
+ const strandedReplyReminder = await resolveStrandedSideDialogReplyReminderFollowUp({
745
+ dialog,
746
+ });
747
+ if (strandedReplyReminder !== undefined) {
748
+ await queueReplyReminderFollowUp({ dialog, followUp: strandedReplyReminder });
749
+ args.scheduleDrive(dialog, {
750
+ waitInQue: true,
751
+ driveOptions: {
752
+ source: 'kernel_driver_follow_up',
753
+ reason: 'follow_up_prompt',
754
+ },
755
+ });
756
+ log_1.log.warn('kernel-driver recovered stranded sideDialog reply obligation by queueing reply reminder', undefined, {
757
+ dialogId: dialog.id.valueOf(),
758
+ rootId: dialog.id.rootId,
759
+ selfId: dialog.id.selfId,
760
+ targetCallId: strandedReplyReminder.tellaskReplyDirective.targetCallId,
761
+ targetOwnerDialogId: strandedReplyReminder.sideDialogReplyTarget.ownerDialogId,
762
+ });
763
+ return;
764
+ }
765
+ }
643
766
  if (dialog instanceof dialog_1.SideDialog && !dialog.hasUpNext()) {
644
767
  try {
645
768
  const inspection = await inspectNoPromptSideDialogDrive({ dialog, driveOptions });
@@ -916,8 +1039,10 @@ async function executeDriveRound(args) {
916
1039
  driveResult.lastFunctionCallGenseq > 0 &&
917
1040
  directFallbackResponse.responseGenseq <= driveResult.lastFunctionCallGenseq;
918
1041
  if (hasInProgressFunctionCall) {
919
- // Any function call means execution is still in-progress. Only supply when the tellaskee
920
- // has produced a newer assistant saying after the latest function call.
1042
+ // A candidate direct fallback, including thinking-only output, must be newer than the
1043
+ // latest same-round function/tellask call. Otherwise the call is still the active move
1044
+ // and may auto-continue; the candidate is merely pre-tool reasoning/progress, not final
1045
+ // tellasker delivery.
921
1046
  log_1.log.debug('kernel-driver skip sideDialog response supply because latest assistant output is not after function calls', undefined, {
922
1047
  rootId: dialog.id.rootId,
923
1048
  selfId: dialog.id.selfId,
@@ -948,35 +1073,16 @@ async function executeDriveRound(args) {
948
1073
  else {
949
1074
  if (!activePromptWasReplyToolReminder) {
950
1075
  const language = (0, work_language_1.getWorkLanguage)();
951
- followUp =
952
- sideDialogReplyTarget === undefined
953
- ? {
954
- kind: 'runtime_reply_reminder',
955
- prompt: await buildReplyToolReminderPrompt({
956
- dlg: dialog,
957
- directive: activeTellaskReplyDirective,
958
- language,
959
- }),
960
- msgId: (0, id_1.generateShortId)(),
961
- grammar: 'markdown',
962
- origin: 'runtime',
963
- userLanguageCode: language,
964
- tellaskReplyDirective: activeTellaskReplyDirective,
965
- }
966
- : {
967
- kind: 'runtime_sideDialog_reply_reminder',
968
- prompt: await buildReplyToolReminderPrompt({
969
- dlg: dialog,
970
- directive: activeTellaskReplyDirective,
971
- language,
972
- }),
973
- msgId: (0, id_1.generateShortId)(),
974
- grammar: 'markdown',
975
- origin: 'runtime',
976
- userLanguageCode: language,
977
- tellaskReplyDirective: activeTellaskReplyDirective,
978
- sideDialogReplyTarget,
979
- };
1076
+ followUp = buildRuntimeReplyReminderFollowUp({
1077
+ directive: activeTellaskReplyDirective,
1078
+ prompt: await buildReplyToolReminderPrompt({
1079
+ dlg: dialog,
1080
+ directive: activeTellaskReplyDirective,
1081
+ language,
1082
+ }),
1083
+ language,
1084
+ sideDialogReplyTarget,
1085
+ });
980
1086
  log_1.log.debug('kernel-driver queued sideDialog replyTellask reminder after plain reply', undefined, {
981
1087
  dialogId: dialog.id.valueOf(),
982
1088
  targetCallId: activeTellaskReplyDirective.targetCallId,
@@ -1064,6 +1170,8 @@ async function executeDriveRound(args) {
1064
1170
  Number.isFinite(driveResult.lastFunctionCallGenseq) &&
1065
1171
  driveResult.lastFunctionCallGenseq > 0 &&
1066
1172
  directFallbackResponse.responseGenseq <= driveResult.lastFunctionCallGenseq;
1173
+ // Same rule as Side Dialog final delivery: direct fallback is allowed only after the
1174
+ // candidate content is known to be post-tool and no same-round call is waiting to continue.
1067
1175
  if (!hasInProgressFunctionCall) {
1068
1176
  if (!activePromptWasReplyToolReminder) {
1069
1177
  const language = (0, work_language_1.getWorkLanguage)();
@@ -130,7 +130,7 @@ function buildSideDialogTellaskerReplyMarkerRules(language) {
130
130
  '- 当前支线未完成时,不要默认直接 `tellaskBack`。先判断当前团队规程/SOP/职责卡能否明确负责人:若能明确且属于执行性处理,直接 `tellask` / `tellaskSessionless` 对应负责人;只有当必须向诉请者补充需求、做业务裁决、澄清验收口径、提供缺失输入,或现有规程无法明确判责时,才发起 `tellaskBack({ tellaskContent: "..." })`,并在 `tellaskContent` 中给出具体问题。',
131
131
  '- 是否存在“待你收口的跨对话回复义务”、以及精确该调用哪个 reply 函数,均由运行时程序化判断;运行时会在 assignment 或最新 runtime/user 提示里直接点名。',
132
132
  '- 若运行时点名了精确 reply 函数名,你只需调用那个被点名的函数;不要自己判断 `reply*` 变体。禁止调用 `tellaskBack` 发送最终结果,也禁止用 `tellask` 向诉请者发送最终结果。',
133
- `- 只有在运行时当前明确点名了某个精确 reply 函数,且你通过那个函数回复时,运行时才会把该回复作为完成结果投递给诉请者,并在传递正文中添加 ${runtimeMarkers.finalCompleted}。`,
133
+ `- 正式完成路径:只有在运行时当前明确点名了某个精确 reply 函数,且你通过那个函数回复时,运行时才会把该回复作为完成结果投递给诉请者,并在传递正文中添加 ${runtimeMarkers.finalCompleted}。不要依赖 direct-reply fallback;它只是运行时临时过渡兜底,不是正式回复机制。`,
134
134
  '- 若运行时当前明确提示“没有待完成的跨对话回复义务”,就直接继续当前本地对话;不要凭记忆再次调用 `reply*`。',
135
135
  '- "不得发普通文本中间汇报"只针对未完成态;若你已经完成任务并能给出最终交付,就应使用运行时当前点名的精确 reply 函数,不要使用 `tellaskBack` 或 `tellask`。',
136
136
  '- 例外:FBR 支线为工具禁用模式(不得调用 `tellaskBack`);其回贴标记(`' +
@@ -145,7 +145,7 @@ function buildSideDialogTellaskerReplyMarkerRules(language) {
145
145
  '- If the current Side Dialog is unfinished, do not default to `tellaskBack`. First judge whether current team SOP / role ownership already identifies the responsible executor: if yes and the issue is execution work, directly use `tellask` / `tellaskSessionless` for that owner; use `tellaskBack({ tellaskContent: "..." })` only when the tellasker must provide clarification, business decision, acceptance-criteria confirmation, missing input, or when existing SOP cannot determine ownership. Put concrete questions in `tellaskContent`.',
146
146
  '- Runtime programmatically decides whether there is an active inter-dialog reply obligation for you, and which exact reply function name applies; runtime will state that directly in the assignment or the latest runtime/user prompt.',
147
147
  '- If runtime names an exact reply function, call that named function and do not choose a `reply*` variant by yourself. Do not use `tellaskBack` or `tellask` to send final delivery.',
148
- `- Only replies sent through the exact reply function currently named by runtime are delivered to the tellasker as completion results and marked with ${runtimeMarkers.finalCompleted}.`,
148
+ `- Formal completion path: only replies sent through the exact reply function currently named by runtime are delivered to the tellasker as completion results and marked with ${runtimeMarkers.finalCompleted}. Do not rely on direct-reply fallback; it is only a temporary runtime transition safeguard, not the formal reply mechanism.`,
149
149
  '- If runtime explicitly tells you there is no active inter-dialog reply obligation right now, just continue the current local conversation; do not call `reply*` again from memory.',
150
150
  '- "Do not post a plain-text progress update" only applies to unfinished states; if the task is done and you can deliver the final result, use the exact reply function currently named by runtime instead of `tellaskBack` or `tellask`.',
151
151
  '- Exception: FBR Side Dialog is tool-less (no \`tellaskBack\`); its reply markers (`' +
@@ -169,7 +169,7 @@ function buildTellaskReplyMarkerScopePolicy(language, dialogScope) {
169
169
  '- `tellaskBack` 只允许用于回问诉请者;仅当必须向诉请者补需求/澄清/裁决/缺失输入,或现有团队规程无法明确判责时才使用。禁止用 `tellaskBack` 发送最终结果。',
170
170
  '- 当前支线未完成时,不得把“阻塞/不确定”机械等同于 `tellaskBack`;若团队规程/SOP/职责卡已明确负责人,应直接 `tellask` / `tellaskSessionless` 对应负责人,不得发普通文本中间汇报。',
171
171
  `- ${(0, reply_prompt_copy_1.buildSideDialogCompletionRule)('zh')}`,
172
- `- 仅当运行时当前明确点名了某个精确 reply 函数,且你通过那个函数回复时,运行时才会把该回复投递给诉请者并标注 ${runtimeMarkers.finalCompleted}。`,
172
+ `- 正式完成路径中,仅当运行时当前明确点名了某个精确 reply 函数,且你通过那个函数回复时,运行时才会把该回复投递给诉请者并标注 ${runtimeMarkers.finalCompleted};不要依赖 direct-reply fallback,它只是运行时临时过渡兜底,不是正式回复机制。`,
173
173
  '- 若运行时当前明确提示“没有待完成的跨对话回复义务”,说明这轮不是待你收口的跨对话回复义务;不要重复调用 `reply*`。',
174
174
  ],
175
175
  en: [
@@ -178,7 +178,7 @@ function buildTellaskReplyMarkerScopePolicy(language, dialogScope) {
178
178
  '- `tellaskBack` is only for asking the tellasker back; use it only when tellasker clarification / decision / missing input is required, or current team SOP cannot determine ownership. Do not use `tellaskBack` to send final results.',
179
179
  '- If the current Side Dialog is unfinished, do not mechanically map “blocked / uncertain” to `tellaskBack`; when team SOP / role ownership already identifies the responsible owner, directly use `tellask` / `tellaskSessionless` for that owner instead of posting a plain-text progress update.',
180
180
  `- ${(0, reply_prompt_copy_1.buildSideDialogCompletionRule)('en')}`,
181
- `- Runtime marks ${runtimeMarkers.finalCompleted} and delivers to the tellasker only when runtime currently names an exact reply function and you reply through that named function.`,
181
+ `- In the formal completion path, runtime marks ${runtimeMarkers.finalCompleted} and delivers to the tellasker only when runtime currently names an exact reply function and you reply through that named function; do not rely on direct-reply fallback, which is only a temporary runtime transition safeguard, not the formal reply mechanism.`,
182
182
  '- If runtime currently tells you there is no active inter-dialog reply obligation, then this turn is not awaiting another inter-dialog closure from you; do not call `reply*` again.',
183
183
  ],
184
184
  }),
@@ -220,6 +220,67 @@ function normalizeGeneratingDisplayStateMismatch(dialogId, status, previous, lat
220
220
  executionMarker: hasInterruptedExecutionMarker ? undefined : latest.executionMarker,
221
221
  };
222
222
  }
223
+ function hasActiveReplyObligationInAskerStackState(state) {
224
+ const top = state?.askerStack[state.askerStack.length - 1];
225
+ return top?.tellaskReplyObligation !== undefined;
226
+ }
227
+ function blockerDisplayState(args) {
228
+ if (args.hasQ4H && args.hasSideDialogs) {
229
+ return { kind: 'blocked', reason: { kind: 'needs_human_input_and_sideDialogs' } };
230
+ }
231
+ if (args.hasQ4H) {
232
+ return { kind: 'blocked', reason: { kind: 'needs_human_input' } };
233
+ }
234
+ if (args.hasSideDialogs) {
235
+ return { kind: 'blocked', reason: { kind: 'waiting_for_sideDialogs' } };
236
+ }
237
+ return undefined;
238
+ }
239
+ async function normalizeSideDialogIdleWhileReplyObligationPending(dialogId, status, previous, latest, askerStackState, context) {
240
+ if (status !== 'running' || dialogId.selfId === dialogId.rootId) {
241
+ return latest;
242
+ }
243
+ if (latest.displayState?.kind !== 'idle_waiting_user') {
244
+ return latest;
245
+ }
246
+ if (!hasActiveReplyObligationInAskerStackState(askerStackState)) {
247
+ return latest;
248
+ }
249
+ const blockerState = blockerDisplayState({
250
+ hasQ4H: (await DialogPersistence.loadQuestions4HumanState(dialogId, status)).length > 0,
251
+ hasSideDialogs: (await DialogPersistence.loadPendingSideDialogs(dialogId, status)).length > 0,
252
+ });
253
+ const top = askerStackState?.askerStack[askerStackState.askerStack.length - 1];
254
+ const healedDisplayState = blockerState ?? pendingReplyObligationDisplayState();
255
+ const healedExecutionMarker = healedDisplayState.kind === 'stopped' ? pendingReplyObligationExecutionMarker() : undefined;
256
+ emitInvariantWarning('Dialog latest projection invariant warning: sideDialog with active reply obligation attempted to enter idle displayState; healing from persistence facts', {
257
+ trigger: context.trigger,
258
+ mutationKind: context.mutationKind,
259
+ latestSource: context.latestSource,
260
+ latestWriteBackKey: context.latestWriteBackKey,
261
+ patchSummary: context.patchSummary,
262
+ dialogId: dialogId.valueOf(),
263
+ rootId: dialogId.rootId,
264
+ selfId: dialogId.selfId,
265
+ status,
266
+ targetCallId: top?.tellaskReplyObligation?.targetCallId ?? null,
267
+ blockedByQ4H: blockerState?.kind === 'blocked' && blockerState.reason.kind !== 'waiting_for_sideDialogs',
268
+ blockedBySideDialogs: blockerState?.kind === 'blocked' && blockerState.reason.kind !== 'needs_human_input',
269
+ before: summarizeLatestProjectionState(previous),
270
+ afterBeforeHealing: summarizeLatestProjectionState(latest),
271
+ healedTo: {
272
+ displayState: healedDisplayState,
273
+ executionMarker: healedExecutionMarker,
274
+ },
275
+ callStack: captureInvariantWarningStack(),
276
+ });
277
+ return {
278
+ ...latest,
279
+ lastModified: (0, time_1.formatUnifiedTimestamp)(new Date()),
280
+ displayState: healedDisplayState,
281
+ executionMarker: healedDisplayState.kind === 'stopped' ? healedExecutionMarker : undefined,
282
+ };
283
+ }
223
284
  const quarantiningMainDialogs = new Set();
224
285
  const PERSISTABLE_DIALOG_STATUSES = ['running', 'completed', 'archived'];
225
286
  const RUN_STATUS_DIR = 'run';
@@ -475,6 +536,8 @@ function parseDialogInterruptionReason(value) {
475
536
  return { kind: 'server_restart' };
476
537
  case 'pending_course_start':
477
538
  return { kind: 'pending_course_start' };
539
+ case 'pending_reply_obligation':
540
+ return { kind: 'pending_reply_obligation' };
478
541
  case 'fork_continue_ready':
479
542
  return { kind: 'fork_continue_ready' };
480
543
  case 'system_stop': {
@@ -509,6 +572,19 @@ function parseDialogInterruptionReason(value) {
509
572
  return null;
510
573
  }
511
574
  }
575
+ function pendingReplyObligationDisplayState() {
576
+ return {
577
+ kind: 'stopped',
578
+ reason: { kind: 'pending_reply_obligation' },
579
+ continueEnabled: true,
580
+ };
581
+ }
582
+ function pendingReplyObligationExecutionMarker() {
583
+ return {
584
+ kind: 'interrupted',
585
+ reason: { kind: 'pending_reply_obligation' },
586
+ };
587
+ }
512
588
  function resolveStoppedContinueEnabled(reason) {
513
589
  return (0, dialog_interruption_1.isInterruptionReasonManualResumeEligible)(reason);
514
590
  }
@@ -1810,6 +1886,7 @@ class DiskFileDialogStore extends dialog_1.DialogStore {
1810
1886
  },
1811
1887
  };
1812
1888
  await this.appendEvent(askerDialog, parentCourse, sideDialogCreatedRecord);
1889
+ const initialSideDialogDisplayState = pendingReplyObligationDisplayState();
1813
1890
  // Initialize latest.yaml via the mutation API (write-back will flush).
1814
1891
  await DialogPersistence.mutateDialogLatest(sideDialogId, () => ({
1815
1892
  kind: 'replace',
@@ -1820,7 +1897,8 @@ class DiskFileDialogStore extends dialog_1.DialogStore {
1820
1897
  messageCount: 0,
1821
1898
  functionCallCount: 0,
1822
1899
  sideDialogCount: 0,
1823
- displayState: { kind: 'idle_waiting_user' },
1900
+ displayState: initialSideDialogDisplayState,
1901
+ executionMarker: pendingReplyObligationExecutionMarker(),
1824
1902
  disableDiligencePush: false,
1825
1903
  },
1826
1904
  }));
@@ -1857,7 +1935,7 @@ class DiskFileDialogStore extends dialog_1.DialogStore {
1857
1935
  currentCourse: 1,
1858
1936
  createdAt: nowTs,
1859
1937
  lastModified: nowTs,
1860
- displayState: { kind: 'idle_waiting_user' },
1938
+ displayState: initialSideDialogDisplayState,
1861
1939
  sessionSlug: options.sessionSlug,
1862
1940
  assignmentFromAsker: {
1863
1941
  callName: options.callName,
@@ -6469,10 +6547,27 @@ class DialogPersistence {
6469
6547
  lastModified: (0, time_1.formatUnifiedTimestamp)(new Date()),
6470
6548
  status: 'active',
6471
6549
  };
6550
+ const askerStackState = status === 'running' && dialogId.selfId !== dialogId.rootId
6551
+ ? await this.loadSideDialogAskerStackState(dialogId, status)
6552
+ : null;
6472
6553
  const mutation = mutator(existing);
6554
+ const mutationContext = {
6555
+ trigger: 'mutateDialogLatest',
6556
+ mutationKind: mutation.kind,
6557
+ patchSummary: mutation.kind === 'patch'
6558
+ ? summarizeLatestMutationPatch(mutation.patch)
6559
+ : mutation.kind === 'replace'
6560
+ ? summarizeLatestProjectionState(mutation.next)
6561
+ : null,
6562
+ latestSource: staged ? 'staged' : latestFromDisk ? 'disk' : 'default_bootstrap',
6563
+ latestWriteBackKey: key,
6564
+ };
6473
6565
  let updated;
6474
6566
  if (mutation.kind === 'noop') {
6475
- return existing;
6567
+ updated = await normalizeSideDialogIdleWhileReplyObligationPending(dialogId, status, existing, existing, askerStackState, mutationContext);
6568
+ if (updated === existing) {
6569
+ return existing;
6570
+ }
6476
6571
  }
6477
6572
  else if (mutation.kind === 'replace') {
6478
6573
  updated = {
@@ -6491,17 +6586,8 @@ class DialogPersistence {
6491
6586
  const _exhaustive = mutation;
6492
6587
  throw new Error(`Unhandled dialog latest mutation: ${String(_exhaustive)}`);
6493
6588
  }
6494
- updated = normalizeGeneratingDisplayStateMismatch(dialogId, status, existing, updated, {
6495
- trigger: 'mutateDialogLatest',
6496
- mutationKind: mutation.kind,
6497
- patchSummary: mutation.kind === 'patch'
6498
- ? summarizeLatestMutationPatch(mutation.patch)
6499
- : mutation.kind === 'replace'
6500
- ? summarizeLatestProjectionState(mutation.next)
6501
- : null,
6502
- latestSource: staged ? 'staged' : latestFromDisk ? 'disk' : 'default_bootstrap',
6503
- latestWriteBackKey: key,
6504
- });
6589
+ updated = normalizeGeneratingDisplayStateMismatch(dialogId, status, existing, updated, mutationContext);
6590
+ updated = await normalizeSideDialogIdleWhileReplyObligationPending(dialogId, status, existing, updated, askerStackState, mutationContext);
6505
6591
  this.assertMainDialogWriteBackNotCanceled(effectiveCancellationToken, 'mutateDialogLatest:before-stage');
6506
6592
  const pending = this.latestWriteBack.get(key);
6507
6593
  if (!pending) {