dominds 1.16.1 → 1.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dialog-display-state.d.ts +4 -0
- package/dist/dialog-display-state.js +138 -19
- package/dist/dialog-fork.js +13 -5
- package/dist/dialog-interruption.d.ts +18 -0
- package/dist/dialog-interruption.js +34 -0
- package/dist/docs/dominds-agent-collaboration.zh.md +401 -0
- package/dist/llm/api-quirks.d.ts +11 -0
- package/dist/llm/api-quirks.js +322 -24
- package/dist/llm/defaults.yaml +2 -1
- package/dist/llm/gen/mock.d.ts +1 -1
- package/dist/llm/gen/mock.js +12 -0
- package/dist/llm/gen/openai-compatible.d.ts +1 -1
- package/dist/llm/gen/openai-compatible.js +9 -1
- package/dist/llm/gen/openai.d.ts +1 -1
- package/dist/llm/gen/openai.js +16 -2
- package/dist/llm/gen.d.ts +10 -0
- package/dist/llm/gen.js +10 -0
- package/dist/llm/kernel-driver/drive.js +994 -866
- package/dist/llm/kernel-driver/runtime.d.ts +16 -2
- package/dist/llm/kernel-driver/runtime.js +75 -10
- package/dist/llm/stop-reason-i18n.d.ts +9 -0
- package/dist/llm/stop-reason-i18n.js +114 -0
- package/dist/persistence-errors.d.ts +26 -0
- package/dist/persistence-errors.js +68 -0
- package/dist/persistence.d.ts +119 -82
- package/dist/persistence.js +1055 -426
- package/dist/server/api-routes.js +174 -58
- package/dist/server/websocket-handler.js +125 -21
- package/dist/tools/team_mgmt.js +2 -2
- package/package.json +3 -3
- package/webapp/dist/assets/{_basePickBy-C0MXVhMo.js → _basePickBy-BJWCmtUm.js} +3 -3
- package/webapp/dist/assets/_basePickBy-BJWCmtUm.js.map +1 -0
- package/webapp/dist/assets/{_baseUniq-CRygl41M.js → _baseUniq-D6k_lGfs.js} +2 -2
- package/webapp/dist/assets/_baseUniq-D6k_lGfs.js.map +1 -0
- package/webapp/dist/assets/{arc-BeHJtnt2.js → arc-DYDaESgj.js} +2 -2
- package/webapp/dist/assets/arc-DYDaESgj.js.map +1 -0
- package/webapp/dist/assets/{architectureDiagram-2XIMDMQ5-CGdo5TVi.js → architectureDiagram-VXUJARFQ-DZMT7dqc.js} +8 -26
- package/webapp/dist/assets/architectureDiagram-VXUJARFQ-DZMT7dqc.js.map +1 -0
- package/webapp/dist/assets/{blockDiagram-WCTKOSBZ-C59VaSGw.js → blockDiagram-VD42YOAC-CABpgVAa.js} +170 -187
- package/webapp/dist/assets/blockDiagram-VD42YOAC-CABpgVAa.js.map +1 -0
- package/webapp/dist/assets/{c4Diagram-IC4MRINW-B3YCBmTx.js → c4Diagram-YG6GDRKO-COLZS8Ul.js} +4 -4
- package/webapp/dist/assets/c4Diagram-YG6GDRKO-COLZS8Ul.js.map +1 -0
- package/webapp/dist/assets/{channel-Dn4i8EAY.js → channel-CYFm9Cri.js} +2 -2
- package/webapp/dist/assets/channel-CYFm9Cri.js.map +1 -0
- package/webapp/dist/assets/{chunk-4BX2VUAB-CQbB3HlW.js → chunk-4BX2VUAB-CX_-XbaN.js} +2 -2
- package/webapp/dist/assets/chunk-4BX2VUAB-CX_-XbaN.js.map +1 -0
- package/webapp/dist/assets/{chunk-55IACEB6-CkQR_Qu9.js → chunk-55IACEB6-ByD-NdBC.js} +2 -2
- package/webapp/dist/assets/chunk-55IACEB6-ByD-NdBC.js.map +1 -0
- package/webapp/dist/assets/{chunk-WL4C6EOR-Bjl-j3bf.js → chunk-B4BG7PRW-BANemsDD.js} +121 -171
- package/webapp/dist/assets/chunk-B4BG7PRW-BANemsDD.js.map +1 -0
- package/webapp/dist/assets/{chunk-NQ4KR5QH-DBxY8LL2.js → chunk-DI55MBZ5-DV7sdJmQ.js} +7 -9
- package/webapp/dist/assets/chunk-DI55MBZ5-DV7sdJmQ.js.map +1 -0
- package/webapp/dist/assets/{chunk-FMBD7UC4-YSIOuaST.js → chunk-FMBD7UC4-DYGviJnf.js} +2 -2
- package/webapp/dist/assets/chunk-FMBD7UC4-DYGviJnf.js.map +1 -0
- package/webapp/dist/assets/{chunk-KX2RTZJC-DFiwgnFf.js → chunk-QN33PNHL-B8DWRL9f.js} +2 -2
- package/webapp/dist/assets/chunk-QN33PNHL-B8DWRL9f.js.map +1 -0
- package/webapp/dist/assets/{chunk-QZHKN3VN-BAdR6lLc.js → chunk-QZHKN3VN-B1aYIzMR.js} +2 -2
- package/webapp/dist/assets/chunk-QZHKN3VN-B1aYIzMR.js.map +1 -0
- package/webapp/dist/assets/{chunk-JSJVCQXG-CWDLRawz.js → chunk-TZMSLE5B-_HISzxl3.js} +6 -14
- package/webapp/dist/assets/chunk-TZMSLE5B-_HISzxl3.js.map +1 -0
- package/webapp/dist/assets/{classDiagram-VBA2DB6C-KBeS6WXD.js → classDiagram-2ON5EDUG-CpyYj1Rc.js} +6 -7
- package/webapp/dist/assets/classDiagram-2ON5EDUG-CpyYj1Rc.js.map +1 -0
- package/webapp/dist/assets/{classDiagram-v2-RAHNMMFH-KBeS6WXD.js → classDiagram-v2-WZHVMYZB-CpyYj1Rc.js} +6 -7
- package/webapp/dist/assets/classDiagram-v2-WZHVMYZB-CpyYj1Rc.js.map +1 -0
- package/webapp/dist/assets/{clone-C7-aS2vm.js → clone-B_9AxWIU.js} +2 -2
- package/webapp/dist/assets/clone-B_9AxWIU.js.map +1 -0
- package/webapp/dist/assets/{cose-bilkent-S5V4N54A-Bo5IsL-5.js → cose-bilkent-S5V4N54A-17ECLfPR.js} +2 -2
- package/webapp/dist/assets/cose-bilkent-S5V4N54A-17ECLfPR.js.map +1 -0
- package/webapp/dist/assets/cytoscape.esm-Bm8DJGmZ.js.map +1 -1
- package/webapp/dist/assets/{dagre-KLK3FWXG-C3FeHj7D.js → dagre-6UL2VRFP-DhRqcF1o.js} +7 -7
- package/webapp/dist/assets/dagre-6UL2VRFP-DhRqcF1o.js.map +1 -0
- package/webapp/dist/assets/defaultLocale-B2RvLBDe.js.map +1 -1
- package/webapp/dist/assets/{diagram-E7M64L7V-0HfiRW0u.js → diagram-PSM6KHXK-lpDT6Wwb.js} +10 -10
- package/webapp/dist/assets/diagram-PSM6KHXK-lpDT6Wwb.js.map +1 -0
- package/webapp/dist/assets/{diagram-IFDJBPK2-DJwh7SMO.js → diagram-QEK2KX5R-ohxbPpDH.js} +8 -9
- package/webapp/dist/assets/diagram-QEK2KX5R-ohxbPpDH.js.map +1 -0
- package/webapp/dist/assets/{diagram-P4PSJMXO-Y0RbvUZX.js → diagram-S2PKOQOG-DAFFGfch.js} +8 -8
- package/webapp/dist/assets/diagram-S2PKOQOG-DAFFGfch.js.map +1 -0
- package/webapp/dist/assets/{erDiagram-INFDFZHY-B183xyLa.js → erDiagram-Q2GNP2WA-BH-7rI43.js} +75 -96
- package/webapp/dist/assets/erDiagram-Q2GNP2WA-BH-7rI43.js.map +1 -0
- package/webapp/dist/assets/{flowDiagram-PKNHOUZH-C_BR3Y3J.js → flowDiagram-NV44I4VS-CdEjFbz4.js} +81 -98
- package/webapp/dist/assets/flowDiagram-NV44I4VS-CdEjFbz4.js.map +1 -0
- package/webapp/dist/assets/{ganttDiagram-A5KZAMGK-CHLgkuJT.js → ganttDiagram-JELNMOA3-Cju2t-lK.js} +3 -28
- package/webapp/dist/assets/ganttDiagram-JELNMOA3-Cju2t-lK.js.map +1 -0
- package/webapp/dist/assets/{gitGraphDiagram-K3NZZRJ6-D6C_HU1q.js → gitGraphDiagram-V2S2FVAM-CUJ5oxCg.js} +46 -38
- package/webapp/dist/assets/gitGraphDiagram-V2S2FVAM-CUJ5oxCg.js.map +1 -0
- package/webapp/dist/assets/graph-mhcc7ldf.js +425 -0
- package/webapp/dist/assets/graph-mhcc7ldf.js.map +1 -0
- package/webapp/dist/assets/{index-CLoNHmSQ.js → index-BLnM-uON.js} +2098 -1841
- package/webapp/dist/assets/{index-CLoNHmSQ.js.map → index-BLnM-uON.js.map} +1 -1
- package/webapp/dist/assets/{index-YaxF76or.css → index-xvYYeHuy.css} +1 -1
- package/webapp/dist/assets/{infoDiagram-LFFYTUFH-mnMS61NO.js → infoDiagram-HS3SLOUP-Df8p0okQ.js} +7 -7
- package/webapp/dist/assets/infoDiagram-HS3SLOUP-Df8p0okQ.js.map +1 -0
- package/webapp/dist/assets/init-ZxktEp_H.js.map +1 -1
- package/webapp/dist/assets/{journeyDiagram-4ABVD52K-BdeERn0r.js → journeyDiagram-XKPGCS4Q-BXMl8H-d.js} +5 -5
- package/webapp/dist/assets/journeyDiagram-XKPGCS4Q-BXMl8H-d.js.map +1 -0
- package/webapp/dist/assets/{kanban-definition-K7BYSVSG-MLeCGcYg.js → kanban-definition-3W4ZIXB7-Cc5RwCEh.js} +3 -5
- package/webapp/dist/assets/kanban-definition-3W4ZIXB7-Cc5RwCEh.js.map +1 -0
- package/webapp/dist/assets/{layout-DY9VEQ9M.js → layout-BB2SvQcE.js} +5 -5
- package/webapp/dist/assets/layout-BB2SvQcE.js.map +1 -0
- package/webapp/dist/assets/{linear-D-T1xRsr.js → linear-Cj50lA0a.js} +2 -2
- package/webapp/dist/assets/linear-Cj50lA0a.js.map +1 -0
- package/webapp/dist/assets/{mindmap-definition-YRQLILUH-CrVm8vj2.js → mindmap-definition-VGOIOE7T-BLBklJVX.js} +5 -7
- package/webapp/dist/assets/mindmap-definition-VGOIOE7T-BLBklJVX.js.map +1 -0
- package/webapp/dist/assets/ordinal-CxptdPJm.js.map +1 -1
- package/webapp/dist/assets/{pieDiagram-SKSYHLDU-GtLLV0r1.js → pieDiagram-ADFJNKIX-BQrOgSc-.js} +8 -8
- package/webapp/dist/assets/pieDiagram-ADFJNKIX-BQrOgSc-.js.map +1 -0
- package/webapp/dist/assets/{quadrantDiagram-337W2JSQ-BCgk_2LP.js → quadrantDiagram-AYHSOK5B-C62TxtsO.js} +3 -3
- package/webapp/dist/assets/quadrantDiagram-AYHSOK5B-C62TxtsO.js.map +1 -0
- package/webapp/dist/assets/{requirementDiagram-Z7DCOOCP-DYUKHWKR.js → requirementDiagram-UZGBJVZJ-Is6Q3osM.js} +6 -16
- package/webapp/dist/assets/requirementDiagram-UZGBJVZJ-Is6Q3osM.js.map +1 -0
- package/webapp/dist/assets/{sankeyDiagram-WA2Y5GQK-CFAH8OaY.js → sankeyDiagram-TZEHDZUN-CCAW8Dr2.js} +2 -2
- package/webapp/dist/assets/sankeyDiagram-TZEHDZUN-CCAW8Dr2.js.map +1 -0
- package/webapp/dist/assets/{sequenceDiagram-2WXFIKYE-BTnhbv6w.js → sequenceDiagram-WL72ISMW-DpODpbl6.js} +201 -601
- package/webapp/dist/assets/sequenceDiagram-WL72ISMW-DpODpbl6.js.map +1 -0
- package/webapp/dist/assets/{stateDiagram-RAJIS63D-DWUnJTwO.js → stateDiagram-FKZM4ZOC-B8LHaf5T.js} +9 -9
- package/webapp/dist/assets/stateDiagram-FKZM4ZOC-B8LHaf5T.js.map +1 -0
- package/webapp/dist/assets/{stateDiagram-v2-FVOUBMTO-B-fc52bp.js → stateDiagram-v2-4FDKWEC3-4FzTkpgz.js} +5 -5
- package/webapp/dist/assets/stateDiagram-v2-4FDKWEC3-4FzTkpgz.js.map +1 -0
- package/webapp/dist/assets/{timeline-definition-YZTLITO2-BMZ52QaG.js → timeline-definition-IT6M3QCI-Ckxc4qZe.js} +3 -3
- package/webapp/dist/assets/timeline-definition-IT6M3QCI-Ckxc4qZe.js.map +1 -0
- package/webapp/dist/assets/{treemap-KZPCXAKY-fgSrze0S.js → treemap-GDKQZRPO-QQa4vKMv.js} +24 -37
- package/webapp/dist/assets/treemap-GDKQZRPO-QQa4vKMv.js.map +1 -0
- package/webapp/dist/assets/{xychartDiagram-JWTSCODW-D6aviLtC.js → xychartDiagram-PRI3JC2R-Ba2uJcs6.js} +4 -4
- package/webapp/dist/assets/xychartDiagram-PRI3JC2R-Ba2uJcs6.js.map +1 -0
- package/webapp/dist/index.html +2 -2
- package/webapp/dist/assets/_basePickBy-C0MXVhMo.js.map +0 -1
- package/webapp/dist/assets/_baseUniq-CRygl41M.js.map +0 -1
- package/webapp/dist/assets/arc-BeHJtnt2.js.map +0 -1
- package/webapp/dist/assets/architectureDiagram-2XIMDMQ5-CGdo5TVi.js.map +0 -1
- package/webapp/dist/assets/blockDiagram-WCTKOSBZ-C59VaSGw.js.map +0 -1
- package/webapp/dist/assets/c4Diagram-IC4MRINW-B3YCBmTx.js.map +0 -1
- package/webapp/dist/assets/channel-Dn4i8EAY.js.map +0 -1
- package/webapp/dist/assets/chunk-4BX2VUAB-CQbB3HlW.js.map +0 -1
- package/webapp/dist/assets/chunk-55IACEB6-CkQR_Qu9.js.map +0 -1
- package/webapp/dist/assets/chunk-FMBD7UC4-YSIOuaST.js.map +0 -1
- package/webapp/dist/assets/chunk-JSJVCQXG-CWDLRawz.js.map +0 -1
- package/webapp/dist/assets/chunk-KX2RTZJC-DFiwgnFf.js.map +0 -1
- package/webapp/dist/assets/chunk-NQ4KR5QH-DBxY8LL2.js.map +0 -1
- package/webapp/dist/assets/chunk-QZHKN3VN-BAdR6lLc.js.map +0 -1
- package/webapp/dist/assets/chunk-WL4C6EOR-Bjl-j3bf.js.map +0 -1
- package/webapp/dist/assets/classDiagram-VBA2DB6C-KBeS6WXD.js.map +0 -1
- package/webapp/dist/assets/classDiagram-v2-RAHNMMFH-KBeS6WXD.js.map +0 -1
- package/webapp/dist/assets/clone-C7-aS2vm.js.map +0 -1
- package/webapp/dist/assets/cose-bilkent-S5V4N54A-Bo5IsL-5.js.map +0 -1
- package/webapp/dist/assets/dagre-KLK3FWXG-C3FeHj7D.js.map +0 -1
- package/webapp/dist/assets/diagram-E7M64L7V-0HfiRW0u.js.map +0 -1
- package/webapp/dist/assets/diagram-IFDJBPK2-DJwh7SMO.js.map +0 -1
- package/webapp/dist/assets/diagram-P4PSJMXO-Y0RbvUZX.js.map +0 -1
- package/webapp/dist/assets/erDiagram-INFDFZHY-B183xyLa.js.map +0 -1
- package/webapp/dist/assets/flowDiagram-PKNHOUZH-C_BR3Y3J.js.map +0 -1
- package/webapp/dist/assets/ganttDiagram-A5KZAMGK-CHLgkuJT.js.map +0 -1
- package/webapp/dist/assets/gitGraphDiagram-K3NZZRJ6-D6C_HU1q.js.map +0 -1
- package/webapp/dist/assets/graph-D0hgB-X8.js +0 -782
- package/webapp/dist/assets/graph-D0hgB-X8.js.map +0 -1
- package/webapp/dist/assets/infoDiagram-LFFYTUFH-mnMS61NO.js.map +0 -1
- package/webapp/dist/assets/ishikawaDiagram-PHBUUO56-XFIJsnJj.js +0 -966
- package/webapp/dist/assets/ishikawaDiagram-PHBUUO56-XFIJsnJj.js.map +0 -1
- package/webapp/dist/assets/journeyDiagram-4ABVD52K-BdeERn0r.js.map +0 -1
- package/webapp/dist/assets/kanban-definition-K7BYSVSG-MLeCGcYg.js.map +0 -1
- package/webapp/dist/assets/layout-DY9VEQ9M.js.map +0 -1
- package/webapp/dist/assets/linear-D-T1xRsr.js.map +0 -1
- package/webapp/dist/assets/mindmap-definition-YRQLILUH-CrVm8vj2.js.map +0 -1
- package/webapp/dist/assets/pieDiagram-SKSYHLDU-GtLLV0r1.js.map +0 -1
- package/webapp/dist/assets/quadrantDiagram-337W2JSQ-BCgk_2LP.js.map +0 -1
- package/webapp/dist/assets/requirementDiagram-Z7DCOOCP-DYUKHWKR.js.map +0 -1
- package/webapp/dist/assets/sankeyDiagram-WA2Y5GQK-CFAH8OaY.js.map +0 -1
- package/webapp/dist/assets/sequenceDiagram-2WXFIKYE-BTnhbv6w.js.map +0 -1
- package/webapp/dist/assets/stateDiagram-RAJIS63D-DWUnJTwO.js.map +0 -1
- package/webapp/dist/assets/stateDiagram-v2-FVOUBMTO-B-fc52bp.js.map +0 -1
- package/webapp/dist/assets/timeline-definition-YZTLITO2-BMZ52QaG.js.map +0 -1
- package/webapp/dist/assets/treemap-KZPCXAKY-fgSrze0S.js.map +0 -1
- package/webapp/dist/assets/vennDiagram-LZ73GAT5-D5DmDY9l.js +0 -2487
- package/webapp/dist/assets/vennDiagram-LZ73GAT5-D5DmDY9l.js.map +0 -1
- package/webapp/dist/assets/xychartDiagram-JWTSCODW-D6aviLtC.js.map +0 -1
package/dist/persistence.js
CHANGED
|
@@ -40,6 +40,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
40
40
|
})();
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
42
|
exports.DialogPersistence = exports.DiskFileDialogStore = void 0;
|
|
43
|
+
exports.setDialogsQuarantinedBroadcaster = setDialogsQuarantinedBroadcaster;
|
|
44
|
+
exports.setPrepareDialogQuarantineHook = setPrepareDialogQuarantineHook;
|
|
45
|
+
exports.setFinalizeDialogQuarantineHook = setFinalizeDialogQuarantineHook;
|
|
43
46
|
const language_1 = require("@longrun-ai/kernel/types/language");
|
|
44
47
|
const storage_1 = require("@longrun-ai/kernel/types/storage");
|
|
45
48
|
const time_1 = require("@longrun-ai/kernel/utils/time");
|
|
@@ -48,12 +51,45 @@ const node_crypto_1 = require("node:crypto");
|
|
|
48
51
|
const path = __importStar(require("path"));
|
|
49
52
|
const yaml = __importStar(require("yaml"));
|
|
50
53
|
const dialog_1 = require("./dialog");
|
|
54
|
+
const dialog_interruption_1 = require("./dialog-interruption");
|
|
51
55
|
const evt_registry_1 = require("./evt-registry");
|
|
52
56
|
const log_1 = require("./log");
|
|
57
|
+
const persistence_errors_1 = require("./persistence-errors");
|
|
53
58
|
const async_fifo_mutex_1 = require("./runtime/async-fifo-mutex");
|
|
54
59
|
const reply_prompt_copy_1 = require("./runtime/reply-prompt-copy");
|
|
55
60
|
const tool_1 = require("./tool");
|
|
56
61
|
const registry_1 = require("./tools/registry");
|
|
62
|
+
let dialogsQuarantinedBroadcaster = null;
|
|
63
|
+
let prepareDialogQuarantineHook = null;
|
|
64
|
+
let finalizeDialogQuarantineHook = null;
|
|
65
|
+
const quarantiningRootDialogs = new Set();
|
|
66
|
+
const PERSISTABLE_DIALOG_STATUSES = ['running', 'completed', 'archived'];
|
|
67
|
+
const RUN_STATUS_DIR = 'run';
|
|
68
|
+
const DONE_STATUS_DIR = 'done';
|
|
69
|
+
const ARCHIVE_STATUS_DIR = 'archive';
|
|
70
|
+
function assertPersistableDialogStatus(status, context) {
|
|
71
|
+
if (status === 'quarantining') {
|
|
72
|
+
throw new Error(`${context} does not support status 'quarantining'`);
|
|
73
|
+
}
|
|
74
|
+
return status;
|
|
75
|
+
}
|
|
76
|
+
function getPersistableStatusDirName(status, context) {
|
|
77
|
+
const persistableStatus = assertPersistableDialogStatus(status, context);
|
|
78
|
+
if (persistableStatus === 'running')
|
|
79
|
+
return RUN_STATUS_DIR;
|
|
80
|
+
if (persistableStatus === 'completed')
|
|
81
|
+
return DONE_STATUS_DIR;
|
|
82
|
+
return ARCHIVE_STATUS_DIR;
|
|
83
|
+
}
|
|
84
|
+
function setDialogsQuarantinedBroadcaster(fn) {
|
|
85
|
+
dialogsQuarantinedBroadcaster = fn;
|
|
86
|
+
}
|
|
87
|
+
function setPrepareDialogQuarantineHook(fn) {
|
|
88
|
+
prepareDialogQuarantineHook = fn;
|
|
89
|
+
}
|
|
90
|
+
function setFinalizeDialogQuarantineHook(fn) {
|
|
91
|
+
finalizeDialogQuarantineHook = fn;
|
|
92
|
+
}
|
|
57
93
|
function isTellaskBusinessCallName(value) {
|
|
58
94
|
return (value === 'tellask' ||
|
|
59
95
|
value === 'tellaskSessionless' ||
|
|
@@ -83,6 +119,19 @@ function isSuppressedTellaskPlaceholderFuncResult(args) {
|
|
|
83
119
|
}
|
|
84
120
|
return false;
|
|
85
121
|
}
|
|
122
|
+
function isYamlUnexpectedEofLikeError(error) {
|
|
123
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
124
|
+
return (message.includes('unexpected eof') ||
|
|
125
|
+
message.includes('unexpected end') ||
|
|
126
|
+
message.includes('end of the stream') ||
|
|
127
|
+
message.includes('unexpected end of document'));
|
|
128
|
+
}
|
|
129
|
+
function isJsonUnexpectedEofLikeError(error) {
|
|
130
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
131
|
+
return (message.includes('unterminated string in json') ||
|
|
132
|
+
message.includes('unexpected end of json input') ||
|
|
133
|
+
message.includes('unexpected eof'));
|
|
134
|
+
}
|
|
86
135
|
function buildTellaskResultRoute(route, fallback) {
|
|
87
136
|
const effective = route ?? fallback;
|
|
88
137
|
if (!effective)
|
|
@@ -133,6 +182,79 @@ function getErrorCode(error) {
|
|
|
133
182
|
const maybeCode = error.code;
|
|
134
183
|
return typeof maybeCode === 'string' ? maybeCode : undefined;
|
|
135
184
|
}
|
|
185
|
+
function isGenericUnexpectedEofLikeError(error) {
|
|
186
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
187
|
+
return message.includes('unexpected eof') || message.includes('unexpected end');
|
|
188
|
+
}
|
|
189
|
+
function isPersistenceFileUnexpectedEofLikeError(error, format) {
|
|
190
|
+
if (format === 'yaml') {
|
|
191
|
+
return isYamlUnexpectedEofLikeError(error) || isGenericUnexpectedEofLikeError(error);
|
|
192
|
+
}
|
|
193
|
+
if (format === 'json' || format === 'jsonl') {
|
|
194
|
+
return isJsonUnexpectedEofLikeError(error) || isGenericUnexpectedEofLikeError(error);
|
|
195
|
+
}
|
|
196
|
+
return isGenericUnexpectedEofLikeError(error);
|
|
197
|
+
}
|
|
198
|
+
function buildInvalidPersistenceFileError(args) {
|
|
199
|
+
return new persistence_errors_1.DomindsPersistenceFileError({
|
|
200
|
+
message: `Invalid ${path.basename(args.filePath)} in ${args.filePath}`,
|
|
201
|
+
source: args.source,
|
|
202
|
+
operation: 'parse',
|
|
203
|
+
format: args.format,
|
|
204
|
+
filePath: args.filePath,
|
|
205
|
+
eofLike: args.cause === undefined
|
|
206
|
+
? false
|
|
207
|
+
: isPersistenceFileUnexpectedEofLikeError(args.cause, args.format),
|
|
208
|
+
...(args.lineNumber !== undefined ? { lineNumber: args.lineNumber } : {}),
|
|
209
|
+
...(args.cause !== undefined ? { cause: args.cause } : {}),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async function readPersistenceTextFile(args) {
|
|
213
|
+
try {
|
|
214
|
+
return await fs.promises.readFile(args.filePath, 'utf-8');
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
if (getErrorCode(error) === 'ENOENT') {
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
throw new persistence_errors_1.DomindsPersistenceFileError({
|
|
221
|
+
message: `Failed to read ${path.basename(args.filePath)} in ${args.filePath}`,
|
|
222
|
+
source: args.source,
|
|
223
|
+
operation: 'read',
|
|
224
|
+
format: args.format,
|
|
225
|
+
filePath: args.filePath,
|
|
226
|
+
eofLike: isPersistenceFileUnexpectedEofLikeError(error, args.format),
|
|
227
|
+
cause: error,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function parsePersistenceYaml(args) {
|
|
232
|
+
try {
|
|
233
|
+
return yaml.parse(args.content);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
throw buildInvalidPersistenceFileError({
|
|
237
|
+
source: args.source,
|
|
238
|
+
format: 'yaml',
|
|
239
|
+
filePath: args.filePath,
|
|
240
|
+
cause: error,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function parsePersistenceJson(args) {
|
|
245
|
+
try {
|
|
246
|
+
return JSON.parse(args.content);
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
throw buildInvalidPersistenceFileError({
|
|
250
|
+
source: args.source,
|
|
251
|
+
format: args.lineNumber === undefined ? 'json' : 'jsonl',
|
|
252
|
+
filePath: args.filePath,
|
|
253
|
+
cause: error,
|
|
254
|
+
...(args.lineNumber !== undefined ? { lineNumber: args.lineNumber } : {}),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
136
258
|
function isRecord(value) {
|
|
137
259
|
return typeof value === 'object' && value !== null;
|
|
138
260
|
}
|
|
@@ -164,6 +286,21 @@ function parseDialogRetryDisplay(value) {
|
|
|
164
286
|
summaryTextI18n,
|
|
165
287
|
};
|
|
166
288
|
}
|
|
289
|
+
function parseDialogLlmRetryRecoveryAction(value) {
|
|
290
|
+
if (value === undefined) {
|
|
291
|
+
return { kind: 'none' };
|
|
292
|
+
}
|
|
293
|
+
if (!isRecord(value) || typeof value.kind !== 'string')
|
|
294
|
+
return null;
|
|
295
|
+
switch (value.kind) {
|
|
296
|
+
case 'none':
|
|
297
|
+
return { kind: 'none' };
|
|
298
|
+
case 'diligence_push_once':
|
|
299
|
+
return { kind: 'diligence_push_once' };
|
|
300
|
+
default:
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
167
304
|
function parseDialogInterruptionReason(value) {
|
|
168
305
|
if (!isRecord(value) || typeof value.kind !== 'string')
|
|
169
306
|
return null;
|
|
@@ -180,7 +317,12 @@ function parseDialogInterruptionReason(value) {
|
|
|
180
317
|
const detail = value.detail;
|
|
181
318
|
if (typeof detail !== 'string')
|
|
182
319
|
return null;
|
|
183
|
-
|
|
320
|
+
const i18nStopReason = parseDisplayTextI18n(value.i18nStopReason);
|
|
321
|
+
if (i18nStopReason === null)
|
|
322
|
+
return null;
|
|
323
|
+
return i18nStopReason
|
|
324
|
+
? { kind: 'system_stop', detail, i18nStopReason }
|
|
325
|
+
: { kind: 'system_stop', detail };
|
|
184
326
|
}
|
|
185
327
|
case 'llm_retry_stopped': {
|
|
186
328
|
const error = value.error;
|
|
@@ -189,10 +331,14 @@ function parseDialogInterruptionReason(value) {
|
|
|
189
331
|
const display = parseDialogRetryDisplay(value.display);
|
|
190
332
|
if (display === null)
|
|
191
333
|
return null;
|
|
334
|
+
const recoveryAction = parseDialogLlmRetryRecoveryAction(value.recoveryAction);
|
|
335
|
+
if (recoveryAction === null)
|
|
336
|
+
return null;
|
|
192
337
|
return {
|
|
193
338
|
kind: 'llm_retry_stopped',
|
|
194
339
|
error,
|
|
195
340
|
display,
|
|
341
|
+
recoveryAction,
|
|
196
342
|
};
|
|
197
343
|
}
|
|
198
344
|
default:
|
|
@@ -200,7 +346,7 @@ function parseDialogInterruptionReason(value) {
|
|
|
200
346
|
}
|
|
201
347
|
}
|
|
202
348
|
function resolveStoppedContinueEnabled(reason) {
|
|
203
|
-
return reason
|
|
349
|
+
return (0, dialog_interruption_1.isInterruptionReasonManualResumeEligible)(reason);
|
|
204
350
|
}
|
|
205
351
|
function serializeReminderSnapshot(reminder) {
|
|
206
352
|
return {
|
|
@@ -948,6 +1094,67 @@ function isSubdialogResponseRecord(value) {
|
|
|
948
1094
|
return false;
|
|
949
1095
|
return true;
|
|
950
1096
|
}
|
|
1097
|
+
function isReminderPriority(value) {
|
|
1098
|
+
return value === 'high' || value === 'medium' || value === 'low';
|
|
1099
|
+
}
|
|
1100
|
+
function isReminderScope(value) {
|
|
1101
|
+
return value === 'dialog' || value === 'personal' || value === 'agent_shared';
|
|
1102
|
+
}
|
|
1103
|
+
function isReminderStateFile(value) {
|
|
1104
|
+
if (!isRecord(value))
|
|
1105
|
+
return false;
|
|
1106
|
+
if (!Array.isArray(value.reminders))
|
|
1107
|
+
return false;
|
|
1108
|
+
if (typeof value.updatedAt !== 'string')
|
|
1109
|
+
return false;
|
|
1110
|
+
return value.reminders.every((entry) => {
|
|
1111
|
+
if (!isRecord(entry))
|
|
1112
|
+
return false;
|
|
1113
|
+
if (typeof entry.id !== 'string')
|
|
1114
|
+
return false;
|
|
1115
|
+
if (typeof entry.content !== 'string')
|
|
1116
|
+
return false;
|
|
1117
|
+
if (entry.ownerName !== undefined && typeof entry.ownerName !== 'string')
|
|
1118
|
+
return false;
|
|
1119
|
+
if (entry.echoback !== undefined && typeof entry.echoback !== 'boolean')
|
|
1120
|
+
return false;
|
|
1121
|
+
if (entry.scope !== undefined && !isReminderScope(entry.scope))
|
|
1122
|
+
return false;
|
|
1123
|
+
if (typeof entry.createdAt !== 'string')
|
|
1124
|
+
return false;
|
|
1125
|
+
if (!isReminderPriority(entry.priority))
|
|
1126
|
+
return false;
|
|
1127
|
+
return true;
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
function isHumanQuestion(value) {
|
|
1131
|
+
if (!isRecord(value))
|
|
1132
|
+
return false;
|
|
1133
|
+
if (typeof value.id !== 'string')
|
|
1134
|
+
return false;
|
|
1135
|
+
if (typeof value.tellaskContent !== 'string')
|
|
1136
|
+
return false;
|
|
1137
|
+
if (typeof value.askedAt !== 'string')
|
|
1138
|
+
return false;
|
|
1139
|
+
if (typeof value.callId !== 'string')
|
|
1140
|
+
return false;
|
|
1141
|
+
if (!isRecord(value.callSiteRef))
|
|
1142
|
+
return false;
|
|
1143
|
+
if (typeof value.callSiteRef.course !== 'number')
|
|
1144
|
+
return false;
|
|
1145
|
+
if (typeof value.callSiteRef.messageIndex !== 'number')
|
|
1146
|
+
return false;
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
function isQuestions4HumanFile(value) {
|
|
1150
|
+
if (!isRecord(value))
|
|
1151
|
+
return false;
|
|
1152
|
+
if (!Array.isArray(value.questions))
|
|
1153
|
+
return false;
|
|
1154
|
+
if (typeof value.updatedAt !== 'string')
|
|
1155
|
+
return false;
|
|
1156
|
+
return value.questions.every(isHumanQuestion);
|
|
1157
|
+
}
|
|
951
1158
|
// Remove old type definitions - now using kernel/types/storage.ts
|
|
952
1159
|
const id_1 = require("./utils/id");
|
|
953
1160
|
function parseReplayTellaskCall(record) {
|
|
@@ -1115,7 +1322,6 @@ class DiskFileDialogStore extends dialog_1.DialogStore {
|
|
|
1115
1322
|
},
|
|
1116
1323
|
};
|
|
1117
1324
|
await DialogPersistence.saveSubdialogMetadata(subdialogId, metadata);
|
|
1118
|
-
await DialogPersistence.saveDialogMetadata(subdialogId, metadata);
|
|
1119
1325
|
const rootAnchor = resolveRootGenerationAnchor(callerDialog);
|
|
1120
1326
|
const parentCourse = callerDialog.activeGenCourseOrUndefined ?? callerDialog.currentCourse;
|
|
1121
1327
|
const subdialogCreatedRecord = {
|
|
@@ -2876,6 +3082,15 @@ class DiskFileDialogStore extends dialog_1.DialogStore {
|
|
|
2876
3082
|
}
|
|
2877
3083
|
}
|
|
2878
3084
|
exports.DiskFileDialogStore = DiskFileDialogStore;
|
|
3085
|
+
class DialogWriteBackCanceledError extends Error {
|
|
3086
|
+
constructor(token, phase) {
|
|
3087
|
+
super(`Dialog writeback canceled for ${token.rootDialogId} (${token.status}) during ${phase}`);
|
|
3088
|
+
this.name = 'DialogWriteBackCanceledError';
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
function isDialogWriteBackCanceledError(error) {
|
|
3092
|
+
return error instanceof DialogWriteBackCanceledError;
|
|
3093
|
+
}
|
|
2879
3094
|
/**
|
|
2880
3095
|
* Utility class for managing dialog persistence
|
|
2881
3096
|
*/
|
|
@@ -2926,6 +3141,71 @@ class DialogPersistence {
|
|
|
2926
3141
|
static getPendingSubdialogsWriteBackKey(rootDialogId, status) {
|
|
2927
3142
|
return `${this.getDialogsRootDir()}|${status}|${rootDialogId.valueOf()}|pending-subdialogs`;
|
|
2928
3143
|
}
|
|
3144
|
+
static getRootDialogWriteBackCancelScopeKey(rootDialogId, status) {
|
|
3145
|
+
return `${this.getDialogsRootDir()}|${status}|${rootDialogId.selfId}|writeback-cancel`;
|
|
3146
|
+
}
|
|
3147
|
+
static createRootDialogWriteBackCancellationToken(dialogId, status) {
|
|
3148
|
+
const rootDialogId = dialogId.rootId === dialogId.selfId ? dialogId : new dialog_1.DialogID(dialogId.rootId);
|
|
3149
|
+
const scopeKey = this.getRootDialogWriteBackCancelScopeKey(rootDialogId, status);
|
|
3150
|
+
return {
|
|
3151
|
+
scopeKey,
|
|
3152
|
+
generation: this.rootDialogWriteBackCancelGenerations.get(scopeKey) ?? 0,
|
|
3153
|
+
rootDialogId: rootDialogId.selfId,
|
|
3154
|
+
status,
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
3157
|
+
static assertRootDialogWriteBackNotCanceled(token, phase) {
|
|
3158
|
+
if (this.quarantinedRootDialogScopes.has(token.scopeKey)) {
|
|
3159
|
+
throw new DialogWriteBackCanceledError(token, phase);
|
|
3160
|
+
}
|
|
3161
|
+
const currentGeneration = this.rootDialogWriteBackCancelGenerations.get(token.scopeKey) ?? 0;
|
|
3162
|
+
if (currentGeneration !== token.generation) {
|
|
3163
|
+
throw new DialogWriteBackCanceledError(token, phase);
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
static async rethrowWriteBackPathMissingAsCanceled(error, dialogPath, cancellationToken, phase) {
|
|
3167
|
+
if (getErrorCode(error) !== 'ENOENT') {
|
|
3168
|
+
throw error;
|
|
3169
|
+
}
|
|
3170
|
+
if (cancellationToken) {
|
|
3171
|
+
this.assertRootDialogWriteBackNotCanceled(cancellationToken, phase);
|
|
3172
|
+
if (!(await this.pathExists(dialogPath))) {
|
|
3173
|
+
throw new DialogWriteBackCanceledError(cancellationToken, `${phase}:dialog-path-missing`);
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
throw error;
|
|
3177
|
+
}
|
|
3178
|
+
static cancelRootDialogWriteBacks(rootDialogId, status) {
|
|
3179
|
+
const scopeKey = this.getRootDialogWriteBackCancelScopeKey(rootDialogId, status);
|
|
3180
|
+
const nextGeneration = (this.rootDialogWriteBackCancelGenerations.get(scopeKey) ?? 0) + 1;
|
|
3181
|
+
this.rootDialogWriteBackCancelGenerations.set(scopeKey, nextGeneration);
|
|
3182
|
+
this.clearWriteBackEntriesForRootDialog(rootDialogId, status);
|
|
3183
|
+
}
|
|
3184
|
+
static getDialogMetadataPath(dialogId, status) {
|
|
3185
|
+
const dialogPath = dialogId.rootId === dialogId.selfId
|
|
3186
|
+
? this.getRootDialogPath(dialogId, status)
|
|
3187
|
+
: this.getSubdialogPath(dialogId, status);
|
|
3188
|
+
return path.join(dialogPath, 'dialog.yaml');
|
|
3189
|
+
}
|
|
3190
|
+
static async assertDialogMetadataExistsForAppend(dialogId, status, cancellationToken, phase) {
|
|
3191
|
+
this.assertRootDialogWriteBackNotCanceled(cancellationToken, phase);
|
|
3192
|
+
const metadataPath = this.getDialogMetadataPath(dialogId, status);
|
|
3193
|
+
try {
|
|
3194
|
+
await fs.promises.access(metadataPath);
|
|
3195
|
+
}
|
|
3196
|
+
catch (error) {
|
|
3197
|
+
if (getErrorCode(error) === 'ENOENT') {
|
|
3198
|
+
this.assertRootDialogWriteBackNotCanceled(cancellationToken, `${phase}:metadata-missing`);
|
|
3199
|
+
throw new Error(`Refusing to append events for dialog ${dialogId.valueOf()}: missing dialog metadata at ${metadataPath}`);
|
|
3200
|
+
}
|
|
3201
|
+
throw error;
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
static async cleanupCanceledAppendPlaceholder(dialogId, status) {
|
|
3205
|
+
const rootDialogId = dialogId.rootId === dialogId.selfId ? dialogId : new dialog_1.DialogID(dialogId.rootId);
|
|
3206
|
+
const rootPath = this.getRootDialogPath(rootDialogId, status);
|
|
3207
|
+
await fs.promises.rm(rootPath, { recursive: true, force: true });
|
|
3208
|
+
}
|
|
2929
3209
|
static clonePendingSubdialogRecords(records) {
|
|
2930
3210
|
return records.map((record) => ({
|
|
2931
3211
|
...record,
|
|
@@ -3000,58 +3280,6 @@ class DialogPersistence {
|
|
|
3000
3280
|
static getDialogsRootDir() {
|
|
3001
3281
|
return path.join(process.cwd(), this.DIALOGS_DIR);
|
|
3002
3282
|
}
|
|
3003
|
-
/**
|
|
3004
|
-
* Save dialog state to JSON file for persistence (internal use only)
|
|
3005
|
-
*/
|
|
3006
|
-
static async saveDialogState(state) {
|
|
3007
|
-
try {
|
|
3008
|
-
const dialogPath = await this.ensureRootDialogDirectory(new dialog_1.DialogID(state.metadata.id));
|
|
3009
|
-
// Save state as JSON file
|
|
3010
|
-
const stateFile = path.join(dialogPath, 'state.json');
|
|
3011
|
-
await fs.promises.writeFile(stateFile, JSON.stringify({
|
|
3012
|
-
metadata: state.metadata,
|
|
3013
|
-
currentCourse: state.currentCourse,
|
|
3014
|
-
messages: state.messages,
|
|
3015
|
-
reminders: state.reminders,
|
|
3016
|
-
savedAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
|
|
3017
|
-
}, null, 2), 'utf-8');
|
|
3018
|
-
}
|
|
3019
|
-
catch (error) {
|
|
3020
|
-
log_1.log.error(`Failed to save dialog state for ${state.metadata.id}:`, error);
|
|
3021
|
-
throw error;
|
|
3022
|
-
}
|
|
3023
|
-
}
|
|
3024
|
-
/**
|
|
3025
|
-
* Load dialog state from JSON file
|
|
3026
|
-
*/
|
|
3027
|
-
static async loadDialogState(dialogId) {
|
|
3028
|
-
try {
|
|
3029
|
-
const dialogPath = this.getRootDialogPath(dialogId, 'running');
|
|
3030
|
-
const stateFile = path.join(dialogPath, 'state.json');
|
|
3031
|
-
// Check if state file exists
|
|
3032
|
-
try {
|
|
3033
|
-
await fs.promises.access(stateFile);
|
|
3034
|
-
}
|
|
3035
|
-
catch {
|
|
3036
|
-
log_1.log.warn(`No state file found for dialog ${dialogId.selfId}, returning null`);
|
|
3037
|
-
return null;
|
|
3038
|
-
}
|
|
3039
|
-
const stateData = JSON.parse(await fs.promises.readFile(stateFile, 'utf-8'));
|
|
3040
|
-
const currentCourse = typeof stateData.currentCourse === 'number'
|
|
3041
|
-
? stateData.currentCourse
|
|
3042
|
-
: 1;
|
|
3043
|
-
return {
|
|
3044
|
-
metadata: stateData.metadata,
|
|
3045
|
-
currentCourse,
|
|
3046
|
-
messages: stateData.messages,
|
|
3047
|
-
reminders: stateData.reminders || [],
|
|
3048
|
-
};
|
|
3049
|
-
}
|
|
3050
|
-
catch (error) {
|
|
3051
|
-
log_1.log.error(`Failed to load dialog state for root ${dialogId.selfId}:`, error);
|
|
3052
|
-
return null;
|
|
3053
|
-
}
|
|
3054
|
-
}
|
|
3055
3283
|
/**
|
|
3056
3284
|
* Get the full path for a dialog directory
|
|
3057
3285
|
*/
|
|
@@ -3059,16 +3287,7 @@ class DialogPersistence {
|
|
|
3059
3287
|
if (dialogId.rootId !== dialogId.selfId) {
|
|
3060
3288
|
throw new Error('Expected root dialog id');
|
|
3061
3289
|
}
|
|
3062
|
-
|
|
3063
|
-
if (status === 'running') {
|
|
3064
|
-
statusDir = this.RUN_DIR;
|
|
3065
|
-
}
|
|
3066
|
-
else if (status === 'completed') {
|
|
3067
|
-
statusDir = this.DONE_DIR;
|
|
3068
|
-
}
|
|
3069
|
-
else {
|
|
3070
|
-
statusDir = this.ARCHIVE_DIR;
|
|
3071
|
-
}
|
|
3290
|
+
const statusDir = getPersistableStatusDirName(status, 'DialogPersistence.getRootDialogPath');
|
|
3072
3291
|
return path.join(this.getDialogsRootDir(), statusDir, dialogId.selfId);
|
|
3073
3292
|
}
|
|
3074
3293
|
/**
|
|
@@ -3092,11 +3311,6 @@ class DialogPersistence {
|
|
|
3092
3311
|
const rootPath = this.getRootDialogPath(new dialog_1.DialogID(dialogId.rootId), status);
|
|
3093
3312
|
return path.join(rootPath, this.SUBDIALOGS_DIR, dialogId.selfId);
|
|
3094
3313
|
}
|
|
3095
|
-
static isMalformedDialogLoadError(error) {
|
|
3096
|
-
return (error instanceof Error &&
|
|
3097
|
-
(error.message.startsWith('Invalid latest.yaml in ') ||
|
|
3098
|
-
error.message.startsWith('Invalid dialog metadata in ')));
|
|
3099
|
-
}
|
|
3100
3314
|
static getMalformedRootDialogPath(dialogId, status) {
|
|
3101
3315
|
if (dialogId.rootId !== dialogId.selfId) {
|
|
3102
3316
|
throw new Error('Expected root dialog id');
|
|
@@ -3104,6 +3318,82 @@ class DialogPersistence {
|
|
|
3104
3318
|
void status;
|
|
3105
3319
|
return path.join(this.getDialogsRootDir(), this.MALFORMED_DIR, dialogId.selfId);
|
|
3106
3320
|
}
|
|
3321
|
+
static inferRootDialogIdFromMetadataRelativeDir(relativeDir) {
|
|
3322
|
+
const dir = relativeDir.trim();
|
|
3323
|
+
if (dir === '' || dir === '.' || dir === path.sep) {
|
|
3324
|
+
return null;
|
|
3325
|
+
}
|
|
3326
|
+
const segments = dir.split(path.sep).filter((seg) => seg.length > 0 && seg !== '.');
|
|
3327
|
+
if (segments.length === 0) {
|
|
3328
|
+
return null;
|
|
3329
|
+
}
|
|
3330
|
+
const subdialogsIndex = segments.indexOf(this.SUBDIALOGS_DIR);
|
|
3331
|
+
const rootSegments = subdialogsIndex === -1 ? segments : segments.slice(0, subdialogsIndex);
|
|
3332
|
+
if (rootSegments.length === 0) {
|
|
3333
|
+
return null;
|
|
3334
|
+
}
|
|
3335
|
+
return new dialog_1.DialogID(rootSegments.join('/'));
|
|
3336
|
+
}
|
|
3337
|
+
static inferExpectedDialogIdFromMetadataRelativeDir(relativeDir) {
|
|
3338
|
+
const dir = relativeDir.trim();
|
|
3339
|
+
if (dir === '' || dir === '.' || dir === path.sep) {
|
|
3340
|
+
return null;
|
|
3341
|
+
}
|
|
3342
|
+
const segments = dir.split(path.sep).filter((seg) => seg.length > 0 && seg !== '.');
|
|
3343
|
+
if (segments.length === 0) {
|
|
3344
|
+
return null;
|
|
3345
|
+
}
|
|
3346
|
+
const subdialogsIndex = segments.indexOf(this.SUBDIALOGS_DIR);
|
|
3347
|
+
if (subdialogsIndex === -1) {
|
|
3348
|
+
return segments.join('/');
|
|
3349
|
+
}
|
|
3350
|
+
const subdialogSegments = segments.slice(subdialogsIndex + 1);
|
|
3351
|
+
if (subdialogSegments.length === 0) {
|
|
3352
|
+
return null;
|
|
3353
|
+
}
|
|
3354
|
+
return subdialogSegments.join('/');
|
|
3355
|
+
}
|
|
3356
|
+
static async listSubdialogIdsUnderRoot(rootDialogId, status) {
|
|
3357
|
+
const subdialogsPath = path.join(this.getRootDialogPath(rootDialogId, status), this.SUBDIALOGS_DIR);
|
|
3358
|
+
const subdialogIds = new Set();
|
|
3359
|
+
const visit = async (dirPath, relativePath = '') => {
|
|
3360
|
+
let entries;
|
|
3361
|
+
try {
|
|
3362
|
+
entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
3363
|
+
}
|
|
3364
|
+
catch (error) {
|
|
3365
|
+
if (getErrorCode(error) === 'ENOENT') {
|
|
3366
|
+
return;
|
|
3367
|
+
}
|
|
3368
|
+
throw error;
|
|
3369
|
+
}
|
|
3370
|
+
for (const entry of entries) {
|
|
3371
|
+
if (!entry.isDirectory()) {
|
|
3372
|
+
continue;
|
|
3373
|
+
}
|
|
3374
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
3375
|
+
const entryRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
3376
|
+
const dialogYamlPath = path.join(fullPath, 'dialog.yaml');
|
|
3377
|
+
try {
|
|
3378
|
+
await fs.promises.access(dialogYamlPath);
|
|
3379
|
+
const inferredId = this.inferExpectedDialogIdFromMetadataRelativeDir(path.join(this.SUBDIALOGS_DIR, entryRelativePath));
|
|
3380
|
+
if (!inferredId) {
|
|
3381
|
+
throw new Error(`Failed to infer subdialog id from relative path ${entryRelativePath} under root ${rootDialogId.selfId}`);
|
|
3382
|
+
}
|
|
3383
|
+
subdialogIds.add(inferredId);
|
|
3384
|
+
continue;
|
|
3385
|
+
}
|
|
3386
|
+
catch (error) {
|
|
3387
|
+
if (getErrorCode(error) !== 'ENOENT') {
|
|
3388
|
+
throw error;
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
await visit(fullPath, entryRelativePath);
|
|
3392
|
+
}
|
|
3393
|
+
};
|
|
3394
|
+
await visit(subdialogsPath);
|
|
3395
|
+
return [...subdialogIds];
|
|
3396
|
+
}
|
|
3107
3397
|
static async pathExists(targetPath) {
|
|
3108
3398
|
try {
|
|
3109
3399
|
await fs.promises.access(targetPath);
|
|
@@ -3125,40 +3415,139 @@ class DialogPersistence {
|
|
|
3125
3415
|
this.latestWriteBack.delete(key);
|
|
3126
3416
|
this.latestWriteBackMutexes.delete(key);
|
|
3127
3417
|
}
|
|
3128
|
-
static
|
|
3129
|
-
|
|
3130
|
-
|
|
3418
|
+
static clearWriteBackEntriesForRootDialog(rootDialogId, status) {
|
|
3419
|
+
const basePrefix = `${this.getDialogsRootDir()}|${status}|${rootDialogId.selfId}`;
|
|
3420
|
+
const matchesRootDialogKey = (key) => key === basePrefix || key.startsWith(`${basePrefix}#`) || key.startsWith(`${basePrefix}|`);
|
|
3421
|
+
for (const [key, entry] of this.latestWriteBack.entries()) {
|
|
3422
|
+
if (!matchesRootDialogKey(key))
|
|
3423
|
+
continue;
|
|
3424
|
+
if (entry.kind === 'scheduled') {
|
|
3425
|
+
clearTimeout(entry.timer);
|
|
3426
|
+
}
|
|
3427
|
+
this.latestWriteBack.delete(key);
|
|
3131
3428
|
}
|
|
3132
|
-
const
|
|
3133
|
-
|
|
3134
|
-
|
|
3429
|
+
for (const key of this.latestWriteBackMutexes.keys()) {
|
|
3430
|
+
if (matchesRootDialogKey(key)) {
|
|
3431
|
+
this.latestWriteBackMutexes.delete(key);
|
|
3432
|
+
}
|
|
3135
3433
|
}
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3434
|
+
for (const [key, entry] of this.q4hWriteBack.entries()) {
|
|
3435
|
+
if (!matchesRootDialogKey(key))
|
|
3436
|
+
continue;
|
|
3437
|
+
if (entry.kind === 'scheduled') {
|
|
3438
|
+
clearTimeout(entry.timer);
|
|
3439
|
+
}
|
|
3440
|
+
this.q4hWriteBack.delete(key);
|
|
3441
|
+
}
|
|
3442
|
+
for (const key of this.q4hWriteBackMutexes.keys()) {
|
|
3443
|
+
if (matchesRootDialogKey(key)) {
|
|
3444
|
+
this.q4hWriteBackMutexes.delete(key);
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
for (const [key, entry] of this.pendingSubdialogsWriteBack.entries()) {
|
|
3448
|
+
if (!matchesRootDialogKey(key))
|
|
3449
|
+
continue;
|
|
3450
|
+
if (entry.kind === 'scheduled') {
|
|
3451
|
+
clearTimeout(entry.timer);
|
|
3452
|
+
}
|
|
3453
|
+
this.pendingSubdialogsWriteBack.delete(key);
|
|
3454
|
+
}
|
|
3455
|
+
for (const key of this.pendingSubdialogsWriteBackMutexes.keys()) {
|
|
3456
|
+
if (matchesRootDialogKey(key)) {
|
|
3457
|
+
this.pendingSubdialogsWriteBackMutexes.delete(key);
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
for (const key of this.courseAppendMutexes.keys()) {
|
|
3461
|
+
if (matchesRootDialogKey(key)) {
|
|
3462
|
+
this.courseAppendMutexes.delete(key);
|
|
3463
|
+
}
|
|
3139
3464
|
}
|
|
3140
|
-
await fs.promises.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
3141
|
-
await fs.promises.rename(sourcePath, destinationPath);
|
|
3142
|
-
this.clearLatestWriteBackState(dialogId, status);
|
|
3143
|
-
log_1.log.warn(`Quarantined malformed root dialog ${dialogId.selfId}`, undefined, {
|
|
3144
|
-
status,
|
|
3145
|
-
reason,
|
|
3146
|
-
sourcePath,
|
|
3147
|
-
destinationPath,
|
|
3148
|
-
errorMessage: error.message,
|
|
3149
|
-
});
|
|
3150
3465
|
}
|
|
3151
|
-
static
|
|
3152
|
-
|
|
3466
|
+
static async quarantineMalformedDialog(dialogId, status, reason, error) {
|
|
3467
|
+
const rootDialogId = dialogId.rootId === dialogId.selfId ? dialogId : new dialog_1.DialogID(dialogId.rootId);
|
|
3468
|
+
const quarantineKey = `${status}|${rootDialogId.selfId}`;
|
|
3469
|
+
if (quarantiningRootDialogs.has(quarantineKey)) {
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
quarantiningRootDialogs.add(quarantineKey);
|
|
3473
|
+
let quarantined = false;
|
|
3153
3474
|
try {
|
|
3154
|
-
|
|
3475
|
+
await prepareDialogQuarantineHook?.({
|
|
3476
|
+
dialogId,
|
|
3477
|
+
rootDialogId,
|
|
3478
|
+
status,
|
|
3479
|
+
reason,
|
|
3480
|
+
error,
|
|
3481
|
+
});
|
|
3482
|
+
this.quarantinedRootDialogScopes.add(this.getRootDialogWriteBackCancelScopeKey(rootDialogId, status));
|
|
3483
|
+
this.cancelRootDialogWriteBacks(rootDialogId, status);
|
|
3484
|
+
const sourcePath = this.getRootDialogPath(rootDialogId, status);
|
|
3485
|
+
if (!(await this.pathExists(sourcePath))) {
|
|
3486
|
+
return;
|
|
3487
|
+
}
|
|
3488
|
+
let destinationPath = this.getMalformedRootDialogPath(rootDialogId, status);
|
|
3489
|
+
if (await this.pathExists(destinationPath)) {
|
|
3490
|
+
destinationPath = path.join(this.getDialogsRootDir(), this.MALFORMED_DIR, `${rootDialogId.selfId}__${(0, node_crypto_1.randomUUID)()}`);
|
|
3491
|
+
}
|
|
3492
|
+
await fs.promises.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
3493
|
+
await fs.promises.rename(sourcePath, destinationPath);
|
|
3494
|
+
quarantined = true;
|
|
3495
|
+
log_1.log.warn(`Quarantined malformed dialog ${rootDialogId.selfId}`, undefined, {
|
|
3496
|
+
status,
|
|
3497
|
+
reason,
|
|
3498
|
+
sourcePath,
|
|
3499
|
+
destinationPath,
|
|
3500
|
+
errorMessage: error.message,
|
|
3501
|
+
dialogId: dialogId.valueOf(),
|
|
3502
|
+
rootDialogId: rootDialogId.valueOf(),
|
|
3503
|
+
});
|
|
3504
|
+
dialogsQuarantinedBroadcaster?.({
|
|
3505
|
+
type: 'dialogs_quarantined',
|
|
3506
|
+
status: 'quarantining',
|
|
3507
|
+
fromStatus: assertPersistableDialogStatus(status, 'DialogPersistence.quarantineMalformedDialog(fromStatus)'),
|
|
3508
|
+
rootId: rootDialogId.selfId,
|
|
3509
|
+
dialogId: dialogId.selfId,
|
|
3510
|
+
reason,
|
|
3511
|
+
timestamp: (0, time_1.formatUnifiedTimestamp)(new Date()),
|
|
3512
|
+
});
|
|
3155
3513
|
}
|
|
3156
|
-
|
|
3157
|
-
|
|
3514
|
+
finally {
|
|
3515
|
+
try {
|
|
3516
|
+
await finalizeDialogQuarantineHook?.({
|
|
3517
|
+
dialogId,
|
|
3518
|
+
rootDialogId,
|
|
3519
|
+
status,
|
|
3520
|
+
reason,
|
|
3521
|
+
error,
|
|
3522
|
+
quarantined,
|
|
3523
|
+
});
|
|
3524
|
+
}
|
|
3525
|
+
finally {
|
|
3526
|
+
quarantiningRootDialogs.delete(quarantineKey);
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
static async rethrowAfterQuarantiningDialogPersistenceProblem(dialogId, status, reason, error) {
|
|
3531
|
+
const persistenceError = (0, persistence_errors_1.findDomindsPersistenceFileError)(error);
|
|
3532
|
+
if (persistenceError) {
|
|
3533
|
+
await this.quarantineMalformedDialog(dialogId, status, reason, persistenceError);
|
|
3534
|
+
throw persistenceError;
|
|
3158
3535
|
}
|
|
3536
|
+
throw error;
|
|
3537
|
+
}
|
|
3538
|
+
static parseDialogLatestYaml(content, latestFilePath) {
|
|
3539
|
+
const parsed = parsePersistenceYaml({
|
|
3540
|
+
content,
|
|
3541
|
+
filePath: latestFilePath,
|
|
3542
|
+
source: 'dialog_latest',
|
|
3543
|
+
});
|
|
3159
3544
|
const latest = parseDialogLatestFile(parsed);
|
|
3160
3545
|
if (!latest) {
|
|
3161
|
-
throw
|
|
3546
|
+
throw buildInvalidPersistenceFileError({
|
|
3547
|
+
source: 'dialog_latest',
|
|
3548
|
+
format: 'yaml',
|
|
3549
|
+
filePath: latestFilePath,
|
|
3550
|
+
});
|
|
3162
3551
|
}
|
|
3163
3552
|
return latest;
|
|
3164
3553
|
}
|
|
@@ -3212,16 +3601,17 @@ class DialogPersistence {
|
|
|
3212
3601
|
}
|
|
3213
3602
|
}
|
|
3214
3603
|
/**
|
|
3215
|
-
* List
|
|
3604
|
+
* List candidate root dialog IDs by scanning `dialog.yaml`.
|
|
3605
|
+
*
|
|
3606
|
+
* This scanner intentionally stays lightweight: it only validates the path<->id identity needed
|
|
3607
|
+
* for safe enumeration, and leaves full metadata shape validation to the subsequent lazy-load
|
|
3608
|
+
* step. Callers iterating these candidate IDs must therefore tolerate per-dialog load failures
|
|
3609
|
+
* and continue after a malformed dialog is quarantined.
|
|
3216
3610
|
*/
|
|
3217
3611
|
static async listDialogs(status = 'running') {
|
|
3218
3612
|
try {
|
|
3219
3613
|
const statusDir = this.getDialogsRootDir();
|
|
3220
|
-
const specificDir = path.join(statusDir, status
|
|
3221
|
-
? this.RUN_DIR
|
|
3222
|
-
: status === 'completed'
|
|
3223
|
-
? this.DONE_DIR
|
|
3224
|
-
: this.ARCHIVE_DIR);
|
|
3614
|
+
const specificDir = path.join(statusDir, getPersistableStatusDirName(status, 'DialogPersistence.listDialogs'));
|
|
3225
3615
|
const validDialogIds = [];
|
|
3226
3616
|
// Recursively find all dialog.yaml files
|
|
3227
3617
|
const findDialogYamls = async (dirPath, relativePath = '') => {
|
|
@@ -3235,36 +3625,75 @@ class DialogPersistence {
|
|
|
3235
3625
|
await findDialogYamls(fullPath, entryRelativePath);
|
|
3236
3626
|
}
|
|
3237
3627
|
else if (entry.name === 'dialog.yaml') {
|
|
3238
|
-
// Found a dialog.yaml file
|
|
3628
|
+
// Found a dialog.yaml file. We only validate path<->id identity here; full metadata
|
|
3629
|
+
// shape remains lazy-loaded by the caller-specific read path.
|
|
3239
3630
|
try {
|
|
3240
|
-
const content = await
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3631
|
+
const content = await readPersistenceTextFile({
|
|
3632
|
+
filePath: fullPath,
|
|
3633
|
+
source: 'dialog_metadata',
|
|
3634
|
+
format: 'yaml',
|
|
3635
|
+
});
|
|
3636
|
+
const parsed = parsePersistenceYaml({
|
|
3637
|
+
content,
|
|
3638
|
+
filePath: fullPath,
|
|
3639
|
+
source: 'dialog_metadata',
|
|
3640
|
+
});
|
|
3641
|
+
if (!isRecord(parsed) || typeof parsed.id !== 'string' || parsed.id.trim() === '') {
|
|
3642
|
+
throw buildInvalidPersistenceFileError({
|
|
3643
|
+
source: 'dialog_metadata',
|
|
3644
|
+
format: 'yaml',
|
|
3645
|
+
filePath: fullPath,
|
|
3646
|
+
});
|
|
3244
3647
|
}
|
|
3648
|
+
const expectedDialogId = this.inferExpectedDialogIdFromMetadataRelativeDir(path.dirname(entryRelativePath));
|
|
3649
|
+
if (expectedDialogId === null || parsed.id !== expectedDialogId) {
|
|
3650
|
+
throw buildInvalidPersistenceFileError({
|
|
3651
|
+
source: 'dialog_metadata',
|
|
3652
|
+
format: 'yaml',
|
|
3653
|
+
filePath: fullPath,
|
|
3654
|
+
});
|
|
3655
|
+
}
|
|
3656
|
+
validDialogIds.push(parsed.id);
|
|
3245
3657
|
}
|
|
3246
3658
|
catch (yamlError) {
|
|
3659
|
+
const persistenceError = (0, persistence_errors_1.findDomindsPersistenceFileError)(yamlError);
|
|
3660
|
+
if (persistenceError) {
|
|
3661
|
+
const rootDialogId = this.inferRootDialogIdFromMetadataRelativeDir(path.dirname(entryRelativePath));
|
|
3662
|
+
if (rootDialogId) {
|
|
3663
|
+
await this.quarantineMalformedDialog(rootDialogId, status, 'listDialogs', persistenceError);
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3247
3666
|
log_1.log.warn(`🔍 listDialogs: Failed to parse dialog.yaml at ${fullPath}:`, yamlError);
|
|
3248
3667
|
}
|
|
3249
3668
|
}
|
|
3250
3669
|
}
|
|
3251
3670
|
}
|
|
3252
3671
|
catch (error) {
|
|
3672
|
+
// Directory enumeration failures are filesystem-level access/I/O problems, not evidence
|
|
3673
|
+
// that a specific dialog record is malformed. If we cannot even read this directory,
|
|
3674
|
+
// attempting to quarantine a child dialog via move/rename is unlikely to be reliable.
|
|
3253
3675
|
log_1.log.warn(`🔍 listDialogs: Error reading directory ${dirPath}:`, error);
|
|
3254
3676
|
}
|
|
3255
3677
|
};
|
|
3256
3678
|
try {
|
|
3257
|
-
//
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
.
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3679
|
+
// Only ENOENT means "status directory absent". Other stat failures are loud environment
|
|
3680
|
+
// errors and must flow into the warning path below instead of being silently downgraded.
|
|
3681
|
+
try {
|
|
3682
|
+
await fs.promises.stat(specificDir);
|
|
3683
|
+
}
|
|
3684
|
+
catch (error) {
|
|
3685
|
+
if (getErrorCode(error) === 'ENOENT') {
|
|
3686
|
+
return validDialogIds;
|
|
3687
|
+
}
|
|
3688
|
+
throw error;
|
|
3264
3689
|
}
|
|
3690
|
+
await findDialogYamls(specificDir);
|
|
3265
3691
|
return validDialogIds;
|
|
3266
3692
|
}
|
|
3267
3693
|
catch (error) {
|
|
3694
|
+
// Same rationale as the inner readdir catch above: keep the failure loud in logs, but do
|
|
3695
|
+
// not pretend we have actionable malformed-dialog evidence when the status directory itself
|
|
3696
|
+
// is not readable.
|
|
3268
3697
|
log_1.log.warn(`🔍 listDialogs: Error processing directory ${specificDir}:`, error);
|
|
3269
3698
|
return [];
|
|
3270
3699
|
}
|
|
@@ -3278,14 +3707,14 @@ class DialogPersistence {
|
|
|
3278
3707
|
* List all dialog IDs (root + subdialogs) together with their root IDs.
|
|
3279
3708
|
* This is the only safe way to enumerate subdialogs because their directory names
|
|
3280
3709
|
* are not guaranteed to be their selfId.
|
|
3710
|
+
*
|
|
3711
|
+
* Like `listDialogs()`, this is a candidate scanner rather than a full metadata validator.
|
|
3712
|
+
* Callers must treat later metadata/latest loads as lazy, per-dialog operations that can still
|
|
3713
|
+
* quarantine one dialog without invalidating the rest of the enumeration.
|
|
3281
3714
|
*/
|
|
3282
3715
|
static async listAllDialogIds(status = 'running') {
|
|
3283
3716
|
const statusDir = this.getDialogsRootDir();
|
|
3284
|
-
const specificDir = path.join(statusDir, status
|
|
3285
|
-
? this.RUN_DIR
|
|
3286
|
-
: status === 'completed'
|
|
3287
|
-
? this.DONE_DIR
|
|
3288
|
-
: this.ARCHIVE_DIR);
|
|
3717
|
+
const specificDir = path.join(statusDir, getPersistableStatusDirName(status, 'DialogPersistence.listAllDialogIds'));
|
|
3289
3718
|
const result = [];
|
|
3290
3719
|
const rootDialogIdByDialogYamlPath = new Map();
|
|
3291
3720
|
const readDialogYamlId = async (dialogYamlPath) => {
|
|
@@ -3293,22 +3722,44 @@ class DialogPersistence {
|
|
|
3293
3722
|
if (cached !== undefined)
|
|
3294
3723
|
return cached;
|
|
3295
3724
|
try {
|
|
3296
|
-
const content = await
|
|
3297
|
-
|
|
3725
|
+
const content = await readPersistenceTextFile({
|
|
3726
|
+
filePath: dialogYamlPath,
|
|
3727
|
+
source: 'dialog_metadata',
|
|
3728
|
+
format: 'yaml',
|
|
3729
|
+
});
|
|
3730
|
+
const parsed = parsePersistenceYaml({
|
|
3731
|
+
content,
|
|
3732
|
+
filePath: dialogYamlPath,
|
|
3733
|
+
source: 'dialog_metadata',
|
|
3734
|
+
});
|
|
3298
3735
|
if (typeof parsed !== 'object' || parsed === null) {
|
|
3299
|
-
|
|
3300
|
-
|
|
3736
|
+
throw buildInvalidPersistenceFileError({
|
|
3737
|
+
source: 'dialog_metadata',
|
|
3738
|
+
format: 'yaml',
|
|
3739
|
+
filePath: dialogYamlPath,
|
|
3740
|
+
});
|
|
3301
3741
|
}
|
|
3302
3742
|
const idValue = parsed.id;
|
|
3303
3743
|
if (typeof idValue !== 'string' || idValue.trim() === '') {
|
|
3304
|
-
|
|
3305
|
-
|
|
3744
|
+
throw buildInvalidPersistenceFileError({
|
|
3745
|
+
source: 'dialog_metadata',
|
|
3746
|
+
format: 'yaml',
|
|
3747
|
+
filePath: dialogYamlPath,
|
|
3748
|
+
});
|
|
3306
3749
|
}
|
|
3307
3750
|
const normalized = idValue.trim();
|
|
3308
3751
|
rootDialogIdByDialogYamlPath.set(dialogYamlPath, normalized);
|
|
3309
3752
|
return normalized;
|
|
3310
3753
|
}
|
|
3311
|
-
catch {
|
|
3754
|
+
catch (error) {
|
|
3755
|
+
const persistenceError = (0, persistence_errors_1.findDomindsPersistenceFileError)(error);
|
|
3756
|
+
if (persistenceError) {
|
|
3757
|
+
const relativeDir = path.relative(specificDir, path.dirname(dialogYamlPath));
|
|
3758
|
+
const rootDialogId = this.inferRootDialogIdFromMetadataRelativeDir(relativeDir);
|
|
3759
|
+
if (rootDialogId) {
|
|
3760
|
+
await this.quarantineMalformedDialog(rootDialogId, status, 'listAllDialogIds:readDialogYamlId', persistenceError);
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3312
3763
|
rootDialogIdByDialogYamlPath.set(dialogYamlPath, null);
|
|
3313
3764
|
return null;
|
|
3314
3765
|
}
|
|
@@ -3343,6 +3794,9 @@ class DialogPersistence {
|
|
|
3343
3794
|
entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
3344
3795
|
}
|
|
3345
3796
|
catch (err) {
|
|
3797
|
+
// This is an environment/filesystem failure rather than confirmed dialog corruption. We log
|
|
3798
|
+
// loudly and keep enumeration partial instead of fabricating a quarantine target that may
|
|
3799
|
+
// not even be reachable through the same broken directory path.
|
|
3346
3800
|
log_1.log.warn(`🔍 listAllDialogIds: Error reading directory ${dirPath}:`, err);
|
|
3347
3801
|
return;
|
|
3348
3802
|
}
|
|
@@ -3360,26 +3814,64 @@ class DialogPersistence {
|
|
|
3360
3814
|
if (!rootId)
|
|
3361
3815
|
continue;
|
|
3362
3816
|
try {
|
|
3363
|
-
const content = await
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3817
|
+
const content = await readPersistenceTextFile({
|
|
3818
|
+
filePath: fullPath,
|
|
3819
|
+
source: 'dialog_metadata',
|
|
3820
|
+
format: 'yaml',
|
|
3821
|
+
});
|
|
3822
|
+
const parsed = parsePersistenceYaml({
|
|
3823
|
+
content,
|
|
3824
|
+
filePath: fullPath,
|
|
3825
|
+
source: 'dialog_metadata',
|
|
3826
|
+
});
|
|
3827
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
3828
|
+
throw buildInvalidPersistenceFileError({
|
|
3829
|
+
source: 'dialog_metadata',
|
|
3830
|
+
format: 'yaml',
|
|
3831
|
+
filePath: fullPath,
|
|
3832
|
+
});
|
|
3833
|
+
}
|
|
3367
3834
|
const idValue = parsed.id;
|
|
3368
|
-
if (typeof idValue !== 'string' || idValue.trim() === '')
|
|
3369
|
-
|
|
3835
|
+
if (typeof idValue !== 'string' || idValue.trim() === '') {
|
|
3836
|
+
throw buildInvalidPersistenceFileError({
|
|
3837
|
+
source: 'dialog_metadata',
|
|
3838
|
+
format: 'yaml',
|
|
3839
|
+
filePath: fullPath,
|
|
3840
|
+
});
|
|
3841
|
+
}
|
|
3842
|
+
const expectedDialogId = this.inferExpectedDialogIdFromMetadataRelativeDir(relDir);
|
|
3843
|
+
if (expectedDialogId === null || idValue !== expectedDialogId) {
|
|
3844
|
+
throw buildInvalidPersistenceFileError({
|
|
3845
|
+
source: 'dialog_metadata',
|
|
3846
|
+
format: 'yaml',
|
|
3847
|
+
filePath: fullPath,
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3370
3850
|
result.push(new dialog_1.DialogID(idValue, rootId));
|
|
3371
3851
|
}
|
|
3372
3852
|
catch (yamlError) {
|
|
3853
|
+
const persistenceError = (0, persistence_errors_1.findDomindsPersistenceFileError)(yamlError);
|
|
3854
|
+
if (persistenceError) {
|
|
3855
|
+
const rootDialogId = this.inferRootDialogIdFromMetadataRelativeDir(relDir);
|
|
3856
|
+
if (rootDialogId) {
|
|
3857
|
+
await this.quarantineMalformedDialog(rootDialogId, status, 'listAllDialogIds', persistenceError);
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3373
3860
|
log_1.log.warn(`🔍 listAllDialogIds: Failed to parse dialog.yaml at ${fullPath}:`, yamlError);
|
|
3374
3861
|
}
|
|
3375
3862
|
}
|
|
3376
3863
|
};
|
|
3377
|
-
|
|
3378
|
-
.
|
|
3379
|
-
|
|
3380
|
-
.
|
|
3381
|
-
|
|
3382
|
-
|
|
3864
|
+
try {
|
|
3865
|
+
// Only ENOENT is the benign "no dialogs under this status" case. Permission/I/O failures
|
|
3866
|
+
// must stay loud so operators can tell the difference between "missing" and "inaccessible".
|
|
3867
|
+
await fs.promises.stat(specificDir);
|
|
3868
|
+
}
|
|
3869
|
+
catch (error) {
|
|
3870
|
+
if (getErrorCode(error) === 'ENOENT') {
|
|
3871
|
+
return [];
|
|
3872
|
+
}
|
|
3873
|
+
throw error;
|
|
3874
|
+
}
|
|
3383
3875
|
await findDialogYamls(specificDir);
|
|
3384
3876
|
return result;
|
|
3385
3877
|
}
|
|
@@ -3390,20 +3882,27 @@ class DialogPersistence {
|
|
|
3390
3882
|
static async appendEvents(dialogId, course, events, status = 'running') {
|
|
3391
3883
|
const appendMutexKey = this.getCourseAppendMutexKey(dialogId, course, status);
|
|
3392
3884
|
const release = await this.getCourseAppendMutex(appendMutexKey).acquire();
|
|
3885
|
+
const cancellationToken = this.createRootDialogWriteBackCancellationToken(dialogId, status);
|
|
3393
3886
|
try {
|
|
3394
3887
|
if (events.length === 0) {
|
|
3395
3888
|
return;
|
|
3396
3889
|
}
|
|
3890
|
+
await this.assertDialogMetadataExistsForAppend(dialogId, status, cancellationToken, 'appendEvents:start');
|
|
3397
3891
|
const dialogPath = this.getDialogEventsPath(dialogId, status);
|
|
3398
3892
|
const courseFilename = this.getCourseFilename(course);
|
|
3399
3893
|
const courseFilePath = path.join(dialogPath, courseFilename);
|
|
3400
|
-
// Ensure directory exists
|
|
3401
|
-
await fs.promises.mkdir(dialogPath, { recursive: true });
|
|
3402
3894
|
// Serialize appends per dialog+course file. Concurrent `appendFile` calls can interleave and
|
|
3403
3895
|
// corrupt JSONL lines (e.g. tool results appended in parallel), which later manifests as
|
|
3404
3896
|
// `Unterminated string in JSON ...` during resume.
|
|
3405
3897
|
const serialized = events.map((event) => JSON.stringify(event)).join('\n') + '\n';
|
|
3406
|
-
|
|
3898
|
+
try {
|
|
3899
|
+
await fs.promises.appendFile(courseFilePath, serialized, 'utf-8');
|
|
3900
|
+
}
|
|
3901
|
+
catch (error) {
|
|
3902
|
+
await this.rethrowWriteBackPathMissingAsCanceled(error, dialogPath, cancellationToken, 'appendEvents:append-file');
|
|
3903
|
+
throw error;
|
|
3904
|
+
}
|
|
3905
|
+
await this.assertDialogMetadataExistsForAppend(dialogId, status, cancellationToken, 'appendEvents:after-append');
|
|
3407
3906
|
// Update latest.yaml with new lastModified timestamp
|
|
3408
3907
|
await this.mutateDialogLatest(dialogId, () => ({
|
|
3409
3908
|
kind: 'patch',
|
|
@@ -3411,9 +3910,13 @@ class DialogPersistence {
|
|
|
3411
3910
|
lastModified: (0, time_1.formatUnifiedTimestamp)(new Date()),
|
|
3412
3911
|
currentCourse: course,
|
|
3413
3912
|
},
|
|
3414
|
-
}), status);
|
|
3913
|
+
}), status, cancellationToken);
|
|
3415
3914
|
}
|
|
3416
3915
|
catch (error) {
|
|
3916
|
+
if (isDialogWriteBackCanceledError(error)) {
|
|
3917
|
+
await this.cleanupCanceledAppendPlaceholder(dialogId, status);
|
|
3918
|
+
return;
|
|
3919
|
+
}
|
|
3417
3920
|
log_1.log.error(`Failed to append events to dialog ${dialogId} course ${course}:`, error);
|
|
3418
3921
|
throw error;
|
|
3419
3922
|
}
|
|
@@ -3518,7 +4021,11 @@ class DialogPersistence {
|
|
|
3518
4021
|
const dialogPath = this.getDialogEventsPath(dialogId, status);
|
|
3519
4022
|
const courseFilePath = path.join(dialogPath, this.getCourseFilename(course));
|
|
3520
4023
|
try {
|
|
3521
|
-
const content = await
|
|
4024
|
+
const content = await readPersistenceTextFile({
|
|
4025
|
+
filePath: courseFilePath,
|
|
4026
|
+
source: 'dialog_course_events',
|
|
4027
|
+
format: 'jsonl',
|
|
4028
|
+
});
|
|
3522
4029
|
const events = [];
|
|
3523
4030
|
const lines = content.split('\n');
|
|
3524
4031
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -3526,7 +4033,12 @@ class DialogPersistence {
|
|
|
3526
4033
|
if (!line.trim())
|
|
3527
4034
|
continue;
|
|
3528
4035
|
try {
|
|
3529
|
-
events.push(
|
|
4036
|
+
events.push(parsePersistenceJson({
|
|
4037
|
+
content: line,
|
|
4038
|
+
filePath: courseFilePath,
|
|
4039
|
+
source: 'dialog_course_events',
|
|
4040
|
+
lineNumber: i + 1,
|
|
4041
|
+
}));
|
|
3530
4042
|
}
|
|
3531
4043
|
catch (err) {
|
|
3532
4044
|
const isLastNonEmptyLine = (() => {
|
|
@@ -3536,16 +4048,27 @@ class DialogPersistence {
|
|
|
3536
4048
|
}
|
|
3537
4049
|
return true;
|
|
3538
4050
|
})();
|
|
4051
|
+
const persistenceFileError = err instanceof persistence_errors_1.DomindsPersistenceFileError ? err : undefined;
|
|
3539
4052
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3540
4053
|
// If the last JSONL line was truncated (e.g. process crash mid-append), ignore it so
|
|
3541
4054
|
// dialogs remain resumable. Do not mask corruption in the middle of the file.
|
|
3542
4055
|
if (isLastNonEmptyLine &&
|
|
3543
|
-
(
|
|
4056
|
+
(persistenceFileError?.eofLike === true ||
|
|
4057
|
+
msg.includes('Unterminated string in JSON') ||
|
|
3544
4058
|
msg.includes('Unexpected end of JSON input'))) {
|
|
3545
4059
|
log_1.log.warn(`Ignoring truncated JSONL tail for dialog ${dialogId} course ${course} at line ${i + 1}: ${msg}`);
|
|
3546
4060
|
break;
|
|
3547
4061
|
}
|
|
3548
|
-
|
|
4062
|
+
if (persistenceFileError) {
|
|
4063
|
+
throw persistenceFileError;
|
|
4064
|
+
}
|
|
4065
|
+
throw buildInvalidPersistenceFileError({
|
|
4066
|
+
source: 'dialog_course_events',
|
|
4067
|
+
format: 'jsonl',
|
|
4068
|
+
filePath: courseFilePath,
|
|
4069
|
+
lineNumber: i + 1,
|
|
4070
|
+
cause: err,
|
|
4071
|
+
});
|
|
3549
4072
|
}
|
|
3550
4073
|
}
|
|
3551
4074
|
return events;
|
|
@@ -3596,7 +4119,6 @@ class DialogPersistence {
|
|
|
3596
4119
|
static async _saveReminderState(dialogId, reminders, status = 'running') {
|
|
3597
4120
|
try {
|
|
3598
4121
|
const dialogPath = this.getDialogEventsPath(dialogId, status);
|
|
3599
|
-
await fs.promises.mkdir(dialogPath, { recursive: true });
|
|
3600
4122
|
const remindersFilePath = path.join(dialogPath, 'reminders.json');
|
|
3601
4123
|
const reminderState = {
|
|
3602
4124
|
reminders: reminders.map((r) => ({
|
|
@@ -3615,7 +4137,7 @@ class DialogPersistence {
|
|
|
3615
4137
|
const jsonContent = JSON.stringify(reminderState, null, 2);
|
|
3616
4138
|
const tempFile = path.join(dialogPath, `.${path.basename(remindersFilePath)}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`);
|
|
3617
4139
|
await fs.promises.writeFile(tempFile, jsonContent, 'utf-8');
|
|
3618
|
-
await this.renameWithRetry(tempFile, remindersFilePath
|
|
4140
|
+
await this.renameWithRetry(tempFile, remindersFilePath);
|
|
3619
4141
|
}
|
|
3620
4142
|
catch (error) {
|
|
3621
4143
|
log_1.log.error(`Failed to save reminder state for dialog ${dialogId}:`, error);
|
|
@@ -3630,8 +4152,23 @@ class DialogPersistence {
|
|
|
3630
4152
|
const dialogPath = this.getDialogEventsPath(dialogId, status);
|
|
3631
4153
|
const remindersFilePath = path.join(dialogPath, 'reminders.json');
|
|
3632
4154
|
try {
|
|
3633
|
-
const content = await
|
|
3634
|
-
|
|
4155
|
+
const content = await readPersistenceTextFile({
|
|
4156
|
+
filePath: remindersFilePath,
|
|
4157
|
+
source: 'reminder_state',
|
|
4158
|
+
format: 'json',
|
|
4159
|
+
});
|
|
4160
|
+
const reminderState = parsePersistenceJson({
|
|
4161
|
+
content,
|
|
4162
|
+
filePath: remindersFilePath,
|
|
4163
|
+
source: 'reminder_state',
|
|
4164
|
+
});
|
|
4165
|
+
if (!isReminderStateFile(reminderState)) {
|
|
4166
|
+
throw buildInvalidPersistenceFileError({
|
|
4167
|
+
source: 'reminder_state',
|
|
4168
|
+
format: 'json',
|
|
4169
|
+
filePath: remindersFilePath,
|
|
4170
|
+
});
|
|
4171
|
+
}
|
|
3635
4172
|
return reminderState.reminders.map((r) => {
|
|
3636
4173
|
const ownerNameFromFile = typeof r.ownerName === 'string' ? r.ownerName : undefined;
|
|
3637
4174
|
// Reminder metadata is owner-private. Rebind strictly through persisted ownerName.
|
|
@@ -3657,8 +4194,8 @@ class DialogPersistence {
|
|
|
3657
4194
|
}
|
|
3658
4195
|
}
|
|
3659
4196
|
catch (error) {
|
|
3660
|
-
|
|
3661
|
-
|
|
4197
|
+
await this.rethrowAfterQuarantiningDialogPersistenceProblem(dialogId, status, 'loadReminderState', error);
|
|
4198
|
+
throw new Error('unreachable after loadReminderState persistence rethrow');
|
|
3662
4199
|
}
|
|
3663
4200
|
}
|
|
3664
4201
|
/**
|
|
@@ -3683,82 +4220,32 @@ class DialogPersistence {
|
|
|
3683
4220
|
return await this.loadQuestions4HumanStateFromDisk(dialogId, status);
|
|
3684
4221
|
}
|
|
3685
4222
|
catch (error) {
|
|
3686
|
-
|
|
3687
|
-
|
|
4223
|
+
await this.rethrowAfterQuarantiningDialogPersistenceProblem(dialogId, status, 'loadQuestions4HumanState', error);
|
|
4224
|
+
throw new Error('unreachable after loadQuestions4HumanState persistence rethrow');
|
|
3688
4225
|
}
|
|
3689
4226
|
}
|
|
3690
4227
|
static async loadQuestions4HumanStateFromDisk(dialogId, status) {
|
|
3691
4228
|
const dialogPath = this.getDialogEventsPath(dialogId, status);
|
|
3692
4229
|
const questionsFilePath = path.join(dialogPath, 'q4h.yaml');
|
|
3693
4230
|
try {
|
|
3694
|
-
const content = await
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
4231
|
+
const content = await readPersistenceTextFile({
|
|
4232
|
+
filePath: questionsFilePath,
|
|
4233
|
+
source: 'questions4human_state',
|
|
4234
|
+
format: 'yaml',
|
|
4235
|
+
});
|
|
4236
|
+
const parsed = parsePersistenceYaml({
|
|
4237
|
+
content,
|
|
4238
|
+
filePath: questionsFilePath,
|
|
4239
|
+
source: 'questions4human_state',
|
|
4240
|
+
});
|
|
4241
|
+
if (!isQuestions4HumanFile(parsed)) {
|
|
4242
|
+
throw buildInvalidPersistenceFileError({
|
|
4243
|
+
source: 'questions4human_state',
|
|
4244
|
+
format: 'yaml',
|
|
3704
4245
|
filePath: questionsFilePath,
|
|
3705
4246
|
});
|
|
3706
|
-
return [];
|
|
3707
|
-
}
|
|
3708
|
-
catch (parseError) {
|
|
3709
|
-
// Attempt to auto-repair the common corruption pattern where extra trailing lines are appended
|
|
3710
|
-
// due to concurrent writers clobbering a shared temp file.
|
|
3711
|
-
const lines = content.split(/\r?\n/);
|
|
3712
|
-
let repairedQuestions = null;
|
|
3713
|
-
for (let cut = lines.length - 1; cut > 0 && lines.length - cut <= 12; cut--) {
|
|
3714
|
-
const candidate = lines.slice(0, cut).join('\n');
|
|
3715
|
-
if (candidate.trim() === '')
|
|
3716
|
-
continue;
|
|
3717
|
-
try {
|
|
3718
|
-
const candidateState = yaml.parse(candidate);
|
|
3719
|
-
if (typeof candidateState === 'object' &&
|
|
3720
|
-
candidateState !== null &&
|
|
3721
|
-
'questions' in candidateState &&
|
|
3722
|
-
Array.isArray(candidateState.questions)) {
|
|
3723
|
-
repairedQuestions = candidateState.questions;
|
|
3724
|
-
break;
|
|
3725
|
-
}
|
|
3726
|
-
}
|
|
3727
|
-
catch {
|
|
3728
|
-
// keep trimming
|
|
3729
|
-
}
|
|
3730
|
-
}
|
|
3731
|
-
if (repairedQuestions) {
|
|
3732
|
-
log_1.log.warn(`Repaired corrupted q4h.yaml for dialog ${dialogId}`, undefined, {
|
|
3733
|
-
filePath: questionsFilePath,
|
|
3734
|
-
});
|
|
3735
|
-
const repairedFile = {
|
|
3736
|
-
questions: repairedQuestions,
|
|
3737
|
-
updatedAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
|
|
3738
|
-
};
|
|
3739
|
-
try {
|
|
3740
|
-
await this.writeQ4HStateToDisk(dialogId, { kind: 'file', file: repairedFile }, status);
|
|
3741
|
-
}
|
|
3742
|
-
catch (repairSaveError) {
|
|
3743
|
-
log_1.log.warn(`Failed to persist repaired q4h.yaml for dialog ${dialogId}`, repairSaveError);
|
|
3744
|
-
}
|
|
3745
|
-
return repairedQuestions;
|
|
3746
|
-
}
|
|
3747
|
-
// Quarantine the bad file to avoid repeated parse errors, then treat as "no questions".
|
|
3748
|
-
try {
|
|
3749
|
-
const quarantinePath = `${questionsFilePath}.corrupt-${(0, node_crypto_1.randomUUID)()}`;
|
|
3750
|
-
await fs.promises.rename(questionsFilePath, quarantinePath);
|
|
3751
|
-
log_1.log.warn(`Quarantined corrupted q4h.yaml for dialog ${dialogId}`, undefined, {
|
|
3752
|
-
filePath: questionsFilePath,
|
|
3753
|
-
quarantinePath,
|
|
3754
|
-
});
|
|
3755
|
-
}
|
|
3756
|
-
catch (quarantineError) {
|
|
3757
|
-
log_1.log.warn(`Failed to quarantine corrupted q4h.yaml for dialog ${dialogId}`, quarantineError);
|
|
3758
|
-
}
|
|
3759
|
-
log_1.log.warn(`Failed to parse q4h.yaml for dialog ${dialogId}`, parseError);
|
|
3760
|
-
return [];
|
|
3761
4247
|
}
|
|
4248
|
+
return parsed.questions;
|
|
3762
4249
|
}
|
|
3763
4250
|
catch (error) {
|
|
3764
4251
|
if (getErrorCode(error) === 'ENOENT')
|
|
@@ -3886,7 +4373,8 @@ class DialogPersistence {
|
|
|
3886
4373
|
if (entry.kind !== 'scheduled')
|
|
3887
4374
|
return;
|
|
3888
4375
|
clearTimeout(entry.timer);
|
|
3889
|
-
const
|
|
4376
|
+
const cancellationToken = this.createRootDialogWriteBackCancellationToken(entry.dialogId, entry.status);
|
|
4377
|
+
const inFlight = this.writeQ4HStateToDisk(entry.dialogId, entry.state, entry.status, cancellationToken);
|
|
3890
4378
|
captured = {
|
|
3891
4379
|
dialogId: entry.dialogId,
|
|
3892
4380
|
status: entry.status,
|
|
@@ -3921,6 +4409,10 @@ class DialogPersistence {
|
|
|
3921
4409
|
return;
|
|
3922
4410
|
if (entry.inFlight !== captured.inFlight)
|
|
3923
4411
|
return;
|
|
4412
|
+
if (isDialogWriteBackCanceledError(error)) {
|
|
4413
|
+
this.q4hWriteBack.delete(key);
|
|
4414
|
+
return;
|
|
4415
|
+
}
|
|
3924
4416
|
const timer = setTimeout(() => {
|
|
3925
4417
|
void this.flushQ4HWriteBack(key);
|
|
3926
4418
|
}, this.Q4H_WRITEBACK_WINDOW_MS);
|
|
@@ -3965,18 +4457,26 @@ class DialogPersistence {
|
|
|
3965
4457
|
release();
|
|
3966
4458
|
}
|
|
3967
4459
|
}
|
|
3968
|
-
static async writeQ4HStateToDisk(dialogId, state, status) {
|
|
4460
|
+
static async writeQ4HStateToDisk(dialogId, state, status, cancellationToken) {
|
|
4461
|
+
if (cancellationToken) {
|
|
4462
|
+
this.assertRootDialogWriteBackNotCanceled(cancellationToken, 'writeQ4HStateToDisk:start');
|
|
4463
|
+
}
|
|
3969
4464
|
const dialogPath = this.getDialogEventsPath(dialogId, status);
|
|
3970
4465
|
const questionsFilePath = path.join(dialogPath, 'q4h.yaml');
|
|
3971
|
-
await fs.promises.mkdir(dialogPath, { recursive: true });
|
|
3972
4466
|
if (state.kind === 'deleted') {
|
|
3973
4467
|
await fs.promises.rm(questionsFilePath, { force: true });
|
|
3974
4468
|
return;
|
|
3975
4469
|
}
|
|
3976
4470
|
const yamlContent = yaml.stringify(state.file);
|
|
3977
4471
|
const tempFile = path.join(dialogPath, `.${path.basename(questionsFilePath)}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`);
|
|
3978
|
-
|
|
3979
|
-
|
|
4472
|
+
try {
|
|
4473
|
+
await fs.promises.writeFile(tempFile, yamlContent, 'utf-8');
|
|
4474
|
+
}
|
|
4475
|
+
catch (error) {
|
|
4476
|
+
await this.rethrowWriteBackPathMissingAsCanceled(error, dialogPath, cancellationToken, 'writeQ4HStateToDisk:write-temp');
|
|
4477
|
+
throw error;
|
|
4478
|
+
}
|
|
4479
|
+
await this.renameWithRetry(tempFile, questionsFilePath, 5, cancellationToken);
|
|
3980
4480
|
}
|
|
3981
4481
|
/**
|
|
3982
4482
|
* Load all Q4H questions from all running dialogs (for global Q4H display)
|
|
@@ -4057,8 +4557,8 @@ class DialogPersistence {
|
|
|
4057
4557
|
return await this.loadPendingSubdialogsFromDisk(rootDialogId, status);
|
|
4058
4558
|
}
|
|
4059
4559
|
catch (error) {
|
|
4060
|
-
|
|
4061
|
-
|
|
4560
|
+
await this.rethrowAfterQuarantiningDialogPersistenceProblem(rootDialogId, status, 'loadPendingSubdialogs', error);
|
|
4561
|
+
throw new Error('unreachable after loadPendingSubdialogs persistence rethrow');
|
|
4062
4562
|
}
|
|
4063
4563
|
}
|
|
4064
4564
|
static isPendingSubdialogRecord(value) {
|
|
@@ -4118,11 +4618,24 @@ class DialogPersistence {
|
|
|
4118
4618
|
const dialogPath = this.getDialogResponsesPath(rootDialogId, status);
|
|
4119
4619
|
const filePath = path.join(dialogPath, 'pending-subdialogs.json');
|
|
4120
4620
|
try {
|
|
4121
|
-
const content = await
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4621
|
+
const content = await readPersistenceTextFile({
|
|
4622
|
+
filePath,
|
|
4623
|
+
source: 'pending_subdialogs',
|
|
4624
|
+
format: 'json',
|
|
4625
|
+
});
|
|
4626
|
+
const parsed = parsePersistenceJson({
|
|
4627
|
+
content,
|
|
4628
|
+
filePath,
|
|
4629
|
+
source: 'pending_subdialogs',
|
|
4630
|
+
});
|
|
4631
|
+
if (!Array.isArray(parsed) || !parsed.every((item) => this.isPendingSubdialogRecord(item))) {
|
|
4632
|
+
throw buildInvalidPersistenceFileError({
|
|
4633
|
+
source: 'pending_subdialogs',
|
|
4634
|
+
format: 'json',
|
|
4635
|
+
filePath,
|
|
4636
|
+
});
|
|
4637
|
+
}
|
|
4638
|
+
return parsed;
|
|
4126
4639
|
}
|
|
4127
4640
|
catch (error) {
|
|
4128
4641
|
if (getErrorCode(error) === 'ENOENT')
|
|
@@ -4227,7 +4740,8 @@ class DialogPersistence {
|
|
|
4227
4740
|
if (entry.kind !== 'scheduled')
|
|
4228
4741
|
return;
|
|
4229
4742
|
clearTimeout(entry.timer);
|
|
4230
|
-
const
|
|
4743
|
+
const cancellationToken = this.createRootDialogWriteBackCancellationToken(entry.dialogId, entry.status);
|
|
4744
|
+
const inFlight = this.writePendingSubdialogsToDisk(entry.dialogId, entry.state, entry.status, cancellationToken);
|
|
4231
4745
|
captured = {
|
|
4232
4746
|
dialogId: entry.dialogId,
|
|
4233
4747
|
status: entry.status,
|
|
@@ -4252,7 +4766,7 @@ class DialogPersistence {
|
|
|
4252
4766
|
try {
|
|
4253
4767
|
await captured.inFlight;
|
|
4254
4768
|
}
|
|
4255
|
-
catch {
|
|
4769
|
+
catch (error) {
|
|
4256
4770
|
const release = await mutex.acquire();
|
|
4257
4771
|
try {
|
|
4258
4772
|
const entry = this.pendingSubdialogsWriteBack.get(key);
|
|
@@ -4262,6 +4776,10 @@ class DialogPersistence {
|
|
|
4262
4776
|
return;
|
|
4263
4777
|
if (entry.inFlight !== captured.inFlight)
|
|
4264
4778
|
return;
|
|
4779
|
+
if (isDialogWriteBackCanceledError(error)) {
|
|
4780
|
+
this.pendingSubdialogsWriteBack.delete(key);
|
|
4781
|
+
return;
|
|
4782
|
+
}
|
|
4265
4783
|
const timer = setTimeout(() => {
|
|
4266
4784
|
void this.flushPendingSubdialogsWriteBack(key);
|
|
4267
4785
|
}, this.PENDING_SUBDIALOGS_WRITEBACK_WINDOW_MS);
|
|
@@ -4306,9 +4824,11 @@ class DialogPersistence {
|
|
|
4306
4824
|
release();
|
|
4307
4825
|
}
|
|
4308
4826
|
}
|
|
4309
|
-
static async writePendingSubdialogsToDisk(rootDialogId, state, status) {
|
|
4827
|
+
static async writePendingSubdialogsToDisk(rootDialogId, state, status, cancellationToken) {
|
|
4828
|
+
if (cancellationToken) {
|
|
4829
|
+
this.assertRootDialogWriteBackNotCanceled(cancellationToken, 'writePendingSubdialogsToDisk:start');
|
|
4830
|
+
}
|
|
4310
4831
|
const dialogPath = this.getDialogResponsesPath(rootDialogId, status);
|
|
4311
|
-
await fs.promises.mkdir(dialogPath, { recursive: true });
|
|
4312
4832
|
const filePath = path.join(dialogPath, 'pending-subdialogs.json');
|
|
4313
4833
|
if (state.kind === 'deleted') {
|
|
4314
4834
|
await fs.promises.rm(filePath, { force: true });
|
|
@@ -4316,8 +4836,14 @@ class DialogPersistence {
|
|
|
4316
4836
|
}
|
|
4317
4837
|
const jsonContent = JSON.stringify(state.records, null, 2);
|
|
4318
4838
|
const tempFile = path.join(dialogPath, `.${path.basename(filePath)}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`);
|
|
4319
|
-
|
|
4320
|
-
|
|
4839
|
+
try {
|
|
4840
|
+
await fs.promises.writeFile(tempFile, jsonContent, 'utf-8');
|
|
4841
|
+
}
|
|
4842
|
+
catch (error) {
|
|
4843
|
+
await this.rethrowWriteBackPathMissingAsCanceled(error, dialogPath, cancellationToken, 'writePendingSubdialogsToDisk:write-temp');
|
|
4844
|
+
throw error;
|
|
4845
|
+
}
|
|
4846
|
+
await this.renameWithRetry(tempFile, filePath, 5, cancellationToken);
|
|
4321
4847
|
}
|
|
4322
4848
|
/**
|
|
4323
4849
|
* Get the path for storing subdialog responses (supports both root and subdialog parents).
|
|
@@ -4343,13 +4869,12 @@ class DialogPersistence {
|
|
|
4343
4869
|
static async saveSubdialogResponses(rootDialogId, responses, rootAnchor, status = 'running') {
|
|
4344
4870
|
try {
|
|
4345
4871
|
const dialogPath = this.getDialogResponsesPath(rootDialogId, status);
|
|
4346
|
-
await fs.promises.mkdir(dialogPath, { recursive: true });
|
|
4347
4872
|
const filePath = path.join(dialogPath, 'subdialog-responses.json');
|
|
4348
4873
|
// Atomic write operation
|
|
4349
4874
|
const jsonContent = JSON.stringify(responses, null, 2);
|
|
4350
4875
|
const tempFile = path.join(dialogPath, `.${path.basename(filePath)}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`);
|
|
4351
4876
|
await fs.promises.writeFile(tempFile, jsonContent, 'utf-8');
|
|
4352
|
-
await this.renameWithRetry(tempFile, filePath
|
|
4877
|
+
await this.renameWithRetry(tempFile, filePath);
|
|
4353
4878
|
if (rootAnchor) {
|
|
4354
4879
|
await this.appendSubdialogResponsesReconciledRecord(rootDialogId, responses, rootAnchorWriteTarget(rootAnchor), status);
|
|
4355
4880
|
}
|
|
@@ -4371,9 +4896,24 @@ class DialogPersistence {
|
|
|
4371
4896
|
const results = [];
|
|
4372
4897
|
const tryReadArray = async (p) => {
|
|
4373
4898
|
try {
|
|
4374
|
-
const content = await
|
|
4375
|
-
|
|
4376
|
-
|
|
4899
|
+
const content = await readPersistenceTextFile({
|
|
4900
|
+
filePath: p,
|
|
4901
|
+
source: 'subdialog_responses',
|
|
4902
|
+
format: 'json',
|
|
4903
|
+
});
|
|
4904
|
+
const parsed = parsePersistenceJson({
|
|
4905
|
+
content,
|
|
4906
|
+
filePath: p,
|
|
4907
|
+
source: 'subdialog_responses',
|
|
4908
|
+
});
|
|
4909
|
+
if (!Array.isArray(parsed)) {
|
|
4910
|
+
throw buildInvalidPersistenceFileError({
|
|
4911
|
+
source: 'subdialog_responses',
|
|
4912
|
+
format: 'json',
|
|
4913
|
+
filePath: p,
|
|
4914
|
+
});
|
|
4915
|
+
}
|
|
4916
|
+
return parsed;
|
|
4377
4917
|
}
|
|
4378
4918
|
catch (error) {
|
|
4379
4919
|
if (getErrorCode(error) === 'ENOENT') {
|
|
@@ -4385,9 +4925,14 @@ class DialogPersistence {
|
|
|
4385
4925
|
const primary = await tryReadArray(filePath);
|
|
4386
4926
|
const inflight = await tryReadArray(inflightPath);
|
|
4387
4927
|
for (const item of [...primary, ...inflight]) {
|
|
4388
|
-
if (isSubdialogResponseRecord(item)) {
|
|
4389
|
-
|
|
4928
|
+
if (!isSubdialogResponseRecord(item)) {
|
|
4929
|
+
throw buildInvalidPersistenceFileError({
|
|
4930
|
+
source: 'subdialog_responses',
|
|
4931
|
+
format: 'json',
|
|
4932
|
+
filePath,
|
|
4933
|
+
});
|
|
4390
4934
|
}
|
|
4935
|
+
results.push(item);
|
|
4391
4936
|
}
|
|
4392
4937
|
// Deduplicate by responseId (primary wins over inflight order is irrelevant)
|
|
4393
4938
|
const byId = new Map();
|
|
@@ -4404,26 +4949,39 @@ class DialogPersistence {
|
|
|
4404
4949
|
}
|
|
4405
4950
|
}
|
|
4406
4951
|
catch (error) {
|
|
4407
|
-
|
|
4408
|
-
|
|
4952
|
+
await this.rethrowAfterQuarantiningDialogPersistenceProblem(rootDialogId, status, 'loadSubdialogResponses', error);
|
|
4953
|
+
throw new Error('unreachable after loadSubdialogResponses persistence rethrow');
|
|
4409
4954
|
}
|
|
4410
4955
|
}
|
|
4411
4956
|
static async loadSubdialogResponsesQueue(dialogId, status = 'running') {
|
|
4412
4957
|
try {
|
|
4413
4958
|
const dialogPath = this.getDialogResponsesPath(dialogId, status);
|
|
4414
4959
|
const filePath = path.join(dialogPath, 'subdialog-responses.json');
|
|
4415
|
-
const content = await
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4960
|
+
const content = await readPersistenceTextFile({
|
|
4961
|
+
filePath,
|
|
4962
|
+
source: 'subdialog_responses',
|
|
4963
|
+
format: 'json',
|
|
4964
|
+
});
|
|
4965
|
+
const parsed = parsePersistenceJson({
|
|
4966
|
+
content,
|
|
4967
|
+
filePath,
|
|
4968
|
+
source: 'subdialog_responses',
|
|
4969
|
+
});
|
|
4970
|
+
if (!Array.isArray(parsed) || !parsed.every((item) => isSubdialogResponseRecord(item))) {
|
|
4971
|
+
throw buildInvalidPersistenceFileError({
|
|
4972
|
+
source: 'subdialog_responses',
|
|
4973
|
+
format: 'json',
|
|
4974
|
+
filePath,
|
|
4975
|
+
});
|
|
4419
4976
|
}
|
|
4420
|
-
return parsed
|
|
4977
|
+
return parsed;
|
|
4421
4978
|
}
|
|
4422
4979
|
catch (error) {
|
|
4423
4980
|
if (getErrorCode(error) === 'ENOENT') {
|
|
4424
4981
|
return [];
|
|
4425
4982
|
}
|
|
4426
|
-
|
|
4983
|
+
await this.rethrowAfterQuarantiningDialogPersistenceProblem(dialogId, status, 'loadSubdialogResponsesQueue', error);
|
|
4984
|
+
throw new Error('unreachable after loadSubdialogResponsesQueue persistence rethrow');
|
|
4427
4985
|
}
|
|
4428
4986
|
}
|
|
4429
4987
|
static async appendSubdialogResponse(dialogId, response, rootAnchor, status = 'running') {
|
|
@@ -4432,40 +4990,56 @@ class DialogPersistence {
|
|
|
4432
4990
|
await this.saveSubdialogResponses(dialogId, existing, rootAnchor, status);
|
|
4433
4991
|
}
|
|
4434
4992
|
static async takeSubdialogResponses(dialogId, status = 'running') {
|
|
4435
|
-
const dialogPath = this.getDialogResponsesPath(dialogId, status);
|
|
4436
|
-
await fs.promises.mkdir(dialogPath, { recursive: true });
|
|
4437
|
-
const filePath = path.join(dialogPath, 'subdialog-responses.json');
|
|
4438
|
-
const inflightPath = path.join(dialogPath, 'subdialog-responses.processing.json');
|
|
4439
|
-
// If a previous processing file exists, merge it back so it will be re-processed.
|
|
4440
|
-
try {
|
|
4441
|
-
await fs.promises.access(inflightPath);
|
|
4442
|
-
await this.rollbackTakenSubdialogResponses(dialogId, status);
|
|
4443
|
-
}
|
|
4444
|
-
catch {
|
|
4445
|
-
// no-op
|
|
4446
|
-
}
|
|
4447
4993
|
try {
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4994
|
+
const dialogPath = this.getDialogResponsesPath(dialogId, status);
|
|
4995
|
+
const filePath = path.join(dialogPath, 'subdialog-responses.json');
|
|
4996
|
+
const inflightPath = path.join(dialogPath, 'subdialog-responses.processing.json');
|
|
4997
|
+
// If a previous processing file exists, merge it back so it will be re-processed.
|
|
4998
|
+
try {
|
|
4999
|
+
await fs.promises.access(inflightPath);
|
|
4453
5000
|
}
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
const parsed = JSON.parse(raw);
|
|
4459
|
-
if (!Array.isArray(parsed)) {
|
|
4460
|
-
return [];
|
|
5001
|
+
catch (error) {
|
|
5002
|
+
if (getErrorCode(error) !== 'ENOENT') {
|
|
5003
|
+
throw error;
|
|
5004
|
+
}
|
|
4461
5005
|
}
|
|
4462
|
-
|
|
5006
|
+
if (await this.pathExists(inflightPath)) {
|
|
5007
|
+
await this.rollbackTakenSubdialogResponses(dialogId, status);
|
|
5008
|
+
}
|
|
5009
|
+
try {
|
|
5010
|
+
await fs.promises.rename(filePath, inflightPath);
|
|
5011
|
+
}
|
|
5012
|
+
catch (error) {
|
|
5013
|
+
if (getErrorCode(error) === 'ENOENT') {
|
|
5014
|
+
return [];
|
|
5015
|
+
}
|
|
5016
|
+
throw error;
|
|
5017
|
+
}
|
|
5018
|
+
const raw = await readPersistenceTextFile({
|
|
5019
|
+
filePath: inflightPath,
|
|
5020
|
+
source: 'subdialog_responses',
|
|
5021
|
+
format: 'json',
|
|
5022
|
+
});
|
|
5023
|
+
const parsed = parsePersistenceJson({
|
|
5024
|
+
content: raw,
|
|
5025
|
+
filePath: inflightPath,
|
|
5026
|
+
source: 'subdialog_responses',
|
|
5027
|
+
});
|
|
5028
|
+
if (!Array.isArray(parsed) || !parsed.every((item) => isSubdialogResponseRecord(item))) {
|
|
5029
|
+
throw buildInvalidPersistenceFileError({
|
|
5030
|
+
source: 'subdialog_responses',
|
|
5031
|
+
format: 'json',
|
|
5032
|
+
filePath: inflightPath,
|
|
5033
|
+
});
|
|
5034
|
+
}
|
|
5035
|
+
return parsed;
|
|
4463
5036
|
}
|
|
4464
5037
|
catch (error) {
|
|
4465
5038
|
if (getErrorCode(error) === 'ENOENT') {
|
|
4466
5039
|
return [];
|
|
4467
5040
|
}
|
|
4468
|
-
|
|
5041
|
+
await this.rethrowAfterQuarantiningDialogPersistenceProblem(dialogId, status, 'takeSubdialogResponses', error);
|
|
5042
|
+
throw new Error('unreachable after takeSubdialogResponses persistence rethrow');
|
|
4469
5043
|
}
|
|
4470
5044
|
}
|
|
4471
5045
|
static async commitTakenSubdialogResponses(dialogId, status = 'running') {
|
|
@@ -4474,44 +5048,79 @@ class DialogPersistence {
|
|
|
4474
5048
|
await fs.promises.rm(inflightPath, { force: true });
|
|
4475
5049
|
}
|
|
4476
5050
|
static async rollbackTakenSubdialogResponses(dialogId, status = 'running') {
|
|
4477
|
-
const dialogPath = this.getDialogResponsesPath(dialogId, status);
|
|
4478
|
-
await fs.promises.mkdir(dialogPath, { recursive: true });
|
|
4479
|
-
const filePath = path.join(dialogPath, 'subdialog-responses.json');
|
|
4480
|
-
const inflightPath = path.join(dialogPath, 'subdialog-responses.processing.json');
|
|
4481
|
-
let inflight = [];
|
|
4482
5051
|
try {
|
|
4483
|
-
const
|
|
4484
|
-
const
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
5052
|
+
const dialogPath = this.getDialogResponsesPath(dialogId, status);
|
|
5053
|
+
const filePath = path.join(dialogPath, 'subdialog-responses.json');
|
|
5054
|
+
const inflightPath = path.join(dialogPath, 'subdialog-responses.processing.json');
|
|
5055
|
+
let inflight = [];
|
|
5056
|
+
try {
|
|
5057
|
+
const raw = await readPersistenceTextFile({
|
|
5058
|
+
filePath: inflightPath,
|
|
5059
|
+
source: 'subdialog_responses',
|
|
5060
|
+
format: 'json',
|
|
5061
|
+
});
|
|
5062
|
+
const parsed = parsePersistenceJson({
|
|
5063
|
+
content: raw,
|
|
5064
|
+
filePath: inflightPath,
|
|
5065
|
+
source: 'subdialog_responses',
|
|
5066
|
+
});
|
|
5067
|
+
if (!Array.isArray(parsed) || !parsed.every((item) => isSubdialogResponseRecord(item))) {
|
|
5068
|
+
throw buildInvalidPersistenceFileError({
|
|
5069
|
+
source: 'subdialog_responses',
|
|
5070
|
+
format: 'json',
|
|
5071
|
+
filePath: inflightPath,
|
|
5072
|
+
});
|
|
5073
|
+
}
|
|
5074
|
+
inflight = parsed;
|
|
4490
5075
|
}
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
const raw = await fs.promises.readFile(filePath, 'utf-8');
|
|
4496
|
-
const parsed = JSON.parse(raw);
|
|
4497
|
-
primary = Array.isArray(parsed) ? parsed : [];
|
|
4498
|
-
}
|
|
4499
|
-
catch (error) {
|
|
4500
|
-
if (getErrorCode(error) !== 'ENOENT') {
|
|
5076
|
+
catch (error) {
|
|
5077
|
+
if (getErrorCode(error) === 'ENOENT') {
|
|
5078
|
+
return;
|
|
5079
|
+
}
|
|
4501
5080
|
throw error;
|
|
4502
5081
|
}
|
|
5082
|
+
let primary = [];
|
|
5083
|
+
try {
|
|
5084
|
+
const raw = await readPersistenceTextFile({
|
|
5085
|
+
filePath,
|
|
5086
|
+
source: 'subdialog_responses',
|
|
5087
|
+
format: 'json',
|
|
5088
|
+
});
|
|
5089
|
+
const parsed = parsePersistenceJson({
|
|
5090
|
+
content: raw,
|
|
5091
|
+
filePath,
|
|
5092
|
+
source: 'subdialog_responses',
|
|
5093
|
+
});
|
|
5094
|
+
if (!Array.isArray(parsed) || !parsed.every((item) => isSubdialogResponseRecord(item))) {
|
|
5095
|
+
throw buildInvalidPersistenceFileError({
|
|
5096
|
+
source: 'subdialog_responses',
|
|
5097
|
+
format: 'json',
|
|
5098
|
+
filePath,
|
|
5099
|
+
});
|
|
5100
|
+
}
|
|
5101
|
+
primary = parsed;
|
|
5102
|
+
}
|
|
5103
|
+
catch (error) {
|
|
5104
|
+
if (getErrorCode(error) !== 'ENOENT') {
|
|
5105
|
+
throw error;
|
|
5106
|
+
}
|
|
5107
|
+
}
|
|
5108
|
+
const merged = [...inflight, ...primary];
|
|
5109
|
+
const byId = new Map();
|
|
5110
|
+
for (const r of merged) {
|
|
5111
|
+
byId.set(r.responseId, r);
|
|
5112
|
+
}
|
|
5113
|
+
const result = Array.from(byId.values());
|
|
5114
|
+
const jsonContent = JSON.stringify(result, null, 2);
|
|
5115
|
+
const tempFile = path.join(dialogPath, `.${path.basename(filePath)}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`);
|
|
5116
|
+
await fs.promises.writeFile(tempFile, jsonContent, 'utf-8');
|
|
5117
|
+
await this.renameWithRetry(tempFile, filePath);
|
|
5118
|
+
await fs.promises.rm(inflightPath, { force: true });
|
|
4503
5119
|
}
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
byId.set(r.responseId, r);
|
|
5120
|
+
catch (error) {
|
|
5121
|
+
await this.rethrowAfterQuarantiningDialogPersistenceProblem(dialogId, status, 'rollbackTakenSubdialogResponses', error);
|
|
5122
|
+
throw new Error('unreachable after rollbackTakenSubdialogResponses persistence rethrow');
|
|
4508
5123
|
}
|
|
4509
|
-
const result = Array.from(byId.values());
|
|
4510
|
-
const jsonContent = JSON.stringify(result, null, 2);
|
|
4511
|
-
const tempFile = path.join(dialogPath, `.${path.basename(filePath)}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`);
|
|
4512
|
-
await fs.promises.writeFile(tempFile, jsonContent, 'utf-8');
|
|
4513
|
-
await this.renameWithRetry(tempFile, filePath, jsonContent);
|
|
4514
|
-
await fs.promises.rm(inflightPath, { force: true });
|
|
4515
5124
|
}
|
|
4516
5125
|
/**
|
|
4517
5126
|
* Save root dialog metadata (write-once pattern)
|
|
@@ -4526,7 +5135,7 @@ class DialogPersistence {
|
|
|
4526
5135
|
const yamlContent = yaml.stringify(metadata);
|
|
4527
5136
|
const tempFile = path.join(dialogPath, `.${path.basename(metadataFilePath)}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`);
|
|
4528
5137
|
await fs.promises.writeFile(tempFile, yamlContent, 'utf-8');
|
|
4529
|
-
await this.renameWithRetry(tempFile, metadataFilePath
|
|
5138
|
+
await this.renameWithRetry(tempFile, metadataFilePath);
|
|
4530
5139
|
}
|
|
4531
5140
|
catch (error) {
|
|
4532
5141
|
log_1.log.error(`Failed to save dialog YAML for dialog ${dialogId}:`, error);
|
|
@@ -4563,11 +5172,12 @@ class DialogPersistence {
|
|
|
4563
5172
|
try {
|
|
4564
5173
|
const subPath = this.getSubdialogPath(dialogId, status);
|
|
4565
5174
|
const metadataFilePath = path.join(subPath, 'dialog.yaml');
|
|
4566
|
-
|
|
5175
|
+
// Creation sites must ensure the directory exists first. Update paths intentionally do not
|
|
5176
|
+
// recreate missing directories so quarantine can use "directory disappeared" as cancellation.
|
|
4567
5177
|
const yamlContent = yaml.stringify(metadata);
|
|
4568
5178
|
const tempFile = path.join(subPath, `.${path.basename(metadataFilePath)}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`);
|
|
4569
5179
|
await fs.promises.writeFile(tempFile, yamlContent, 'utf-8');
|
|
4570
|
-
await this.renameWithRetry(tempFile, metadataFilePath
|
|
5180
|
+
await this.renameWithRetry(tempFile, metadataFilePath);
|
|
4571
5181
|
}
|
|
4572
5182
|
catch (error) {
|
|
4573
5183
|
log_1.log.error(`Failed to save subdialog YAML for ${dialogId.selfId} under root dialog ${dialogId.rootId}:`, error);
|
|
@@ -4576,7 +5186,6 @@ class DialogPersistence {
|
|
|
4576
5186
|
}
|
|
4577
5187
|
/**
|
|
4578
5188
|
* Update assignmentFromSup for an existing subdialog.
|
|
4579
|
-
* Persists both subdialog metadata locations for consistency.
|
|
4580
5189
|
*/
|
|
4581
5190
|
static async updateSubdialogAssignment(dialogId, assignment, status = 'running') {
|
|
4582
5191
|
if (dialogId.rootId === dialogId.selfId) {
|
|
@@ -4591,7 +5200,6 @@ class DialogPersistence {
|
|
|
4591
5200
|
assignmentFromSup: assignment,
|
|
4592
5201
|
};
|
|
4593
5202
|
await this.saveSubdialogMetadata(dialogId, next, status);
|
|
4594
|
-
await this.saveDialogMetadata(dialogId, next, status);
|
|
4595
5203
|
}
|
|
4596
5204
|
/**
|
|
4597
5205
|
* Load root dialog metadata
|
|
@@ -4601,21 +5209,30 @@ class DialogPersistence {
|
|
|
4601
5209
|
const dialogPath = this.getRootDialogPath(dialogId, status);
|
|
4602
5210
|
const metadataFilePath = path.join(dialogPath, 'dialog.yaml');
|
|
4603
5211
|
try {
|
|
4604
|
-
const content = await
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
}
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
5212
|
+
const content = await readPersistenceTextFile({
|
|
5213
|
+
filePath: metadataFilePath,
|
|
5214
|
+
source: 'dialog_metadata',
|
|
5215
|
+
format: 'yaml',
|
|
5216
|
+
});
|
|
5217
|
+
const parsed = parsePersistenceYaml({
|
|
5218
|
+
content,
|
|
5219
|
+
filePath: metadataFilePath,
|
|
5220
|
+
source: 'dialog_metadata',
|
|
5221
|
+
});
|
|
4612
5222
|
if (!isDialogMetadataFile(parsed)) {
|
|
4613
|
-
throw
|
|
5223
|
+
throw buildInvalidPersistenceFileError({
|
|
5224
|
+
source: 'dialog_metadata',
|
|
5225
|
+
format: 'yaml',
|
|
5226
|
+
filePath: metadataFilePath,
|
|
5227
|
+
});
|
|
4614
5228
|
}
|
|
4615
5229
|
// Validate that the ID in the file matches the expected dialogId
|
|
4616
5230
|
if (parsed.id !== dialogId.selfId) {
|
|
4617
|
-
|
|
4618
|
-
|
|
5231
|
+
throw buildInvalidPersistenceFileError({
|
|
5232
|
+
source: 'dialog_metadata',
|
|
5233
|
+
format: 'yaml',
|
|
5234
|
+
filePath: metadataFilePath,
|
|
5235
|
+
});
|
|
4619
5236
|
}
|
|
4620
5237
|
return parsed;
|
|
4621
5238
|
}
|
|
@@ -4623,16 +5240,13 @@ class DialogPersistence {
|
|
|
4623
5240
|
if (getErrorCode(error) === 'ENOENT') {
|
|
4624
5241
|
return null;
|
|
4625
5242
|
}
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
return null;
|
|
4629
|
-
}
|
|
4630
|
-
throw error;
|
|
5243
|
+
await this.rethrowAfterQuarantiningDialogPersistenceProblem(dialogId, status, 'loadRootDialogMetadata', error);
|
|
5244
|
+
throw new Error('unreachable after loadRootDialogMetadata persistence rethrow');
|
|
4631
5245
|
}
|
|
4632
5246
|
}
|
|
4633
5247
|
catch (error) {
|
|
4634
5248
|
log_1.log.error(`Failed to load dialog YAML for dialog ${dialogId.selfId}:`, error);
|
|
4635
|
-
|
|
5249
|
+
throw error;
|
|
4636
5250
|
}
|
|
4637
5251
|
}
|
|
4638
5252
|
/**
|
|
@@ -4648,10 +5262,22 @@ class DialogPersistence {
|
|
|
4648
5262
|
const subdialogPath = this.getSubdialogPath(dialogId, status);
|
|
4649
5263
|
const metadataFilePath = path.join(subdialogPath, 'dialog.yaml');
|
|
4650
5264
|
try {
|
|
4651
|
-
const content = await
|
|
4652
|
-
|
|
5265
|
+
const content = await readPersistenceTextFile({
|
|
5266
|
+
filePath: metadataFilePath,
|
|
5267
|
+
source: 'dialog_metadata',
|
|
5268
|
+
format: 'yaml',
|
|
5269
|
+
});
|
|
5270
|
+
const parsed = parsePersistenceYaml({
|
|
5271
|
+
content,
|
|
5272
|
+
filePath: metadataFilePath,
|
|
5273
|
+
source: 'dialog_metadata',
|
|
5274
|
+
});
|
|
4653
5275
|
if (!isDialogMetadataFile(parsed)) {
|
|
4654
|
-
throw
|
|
5276
|
+
throw buildInvalidPersistenceFileError({
|
|
5277
|
+
source: 'dialog_metadata',
|
|
5278
|
+
format: 'yaml',
|
|
5279
|
+
filePath: metadataFilePath,
|
|
5280
|
+
});
|
|
4655
5281
|
}
|
|
4656
5282
|
return parsed;
|
|
4657
5283
|
}
|
|
@@ -4659,28 +5285,39 @@ class DialogPersistence {
|
|
|
4659
5285
|
if (getErrorCode(error) === 'ENOENT') {
|
|
4660
5286
|
return null;
|
|
4661
5287
|
}
|
|
4662
|
-
|
|
5288
|
+
await this.rethrowAfterQuarantiningDialogPersistenceProblem(dialogId, status, 'loadDialogMetadata', error);
|
|
5289
|
+
throw new Error('unreachable after loadDialogMetadata persistence rethrow');
|
|
4663
5290
|
}
|
|
4664
5291
|
}
|
|
4665
5292
|
/**
|
|
4666
5293
|
* Save latest.yaml with current course and lastModified info
|
|
4667
5294
|
*/
|
|
4668
|
-
static async writeDialogLatestToDisk(dialogId, latest, status = 'running') {
|
|
5295
|
+
static async writeDialogLatestToDisk(dialogId, latest, status = 'running', cancellationToken) {
|
|
4669
5296
|
try {
|
|
5297
|
+
if (cancellationToken) {
|
|
5298
|
+
this.assertRootDialogWriteBackNotCanceled(cancellationToken, 'writeDialogLatestToDisk:start');
|
|
5299
|
+
}
|
|
4670
5300
|
const dialogPath = this.getDialogEventsPath(dialogId, status);
|
|
4671
5301
|
const latestFilePath = path.join(dialogPath, 'latest.yaml');
|
|
4672
|
-
// Ensure directory exists before writing (handles race conditions and new dialogs)
|
|
4673
|
-
await fs.promises.mkdir(dialogPath, { recursive: true });
|
|
4674
5302
|
// NOTE: Use a unique temp file name to avoid collisions when multiple updates
|
|
4675
5303
|
// happen concurrently for the same dialog (e.g., parallel tool responses).
|
|
4676
5304
|
const tempFile = path.join(dialogPath, `.${path.basename(latestFilePath)}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`);
|
|
4677
5305
|
const yamlContent = yaml.stringify(latest);
|
|
4678
|
-
|
|
5306
|
+
try {
|
|
5307
|
+
await fs.promises.writeFile(tempFile, yamlContent, 'utf-8');
|
|
5308
|
+
}
|
|
5309
|
+
catch (error) {
|
|
5310
|
+
await this.rethrowWriteBackPathMissingAsCanceled(error, dialogPath, cancellationToken, 'writeDialogLatestToDisk:write-temp');
|
|
5311
|
+
throw error;
|
|
5312
|
+
}
|
|
4679
5313
|
// Rename with retry logic for filesystem sync issues
|
|
4680
|
-
await this.renameWithRetry(tempFile, latestFilePath,
|
|
5314
|
+
await this.renameWithRetry(tempFile, latestFilePath, 5, cancellationToken);
|
|
4681
5315
|
// todo: publish CourseEvent here or where more suitable?
|
|
4682
5316
|
}
|
|
4683
5317
|
catch (error) {
|
|
5318
|
+
if (isDialogWriteBackCanceledError(error)) {
|
|
5319
|
+
throw error;
|
|
5320
|
+
}
|
|
4684
5321
|
log_1.log.error(`Failed to save latest.yaml for dialog ${dialogId.selfId}:`, error);
|
|
4685
5322
|
throw error;
|
|
4686
5323
|
}
|
|
@@ -4688,26 +5325,21 @@ class DialogPersistence {
|
|
|
4688
5325
|
/**
|
|
4689
5326
|
* Rename with retry logic to handle filesystem sync issues
|
|
4690
5327
|
*/
|
|
4691
|
-
static async renameWithRetry(source, destination,
|
|
5328
|
+
static async renameWithRetry(source, destination, maxRetries = 5, cancellationToken) {
|
|
4692
5329
|
let lastError;
|
|
4693
|
-
const destinationDir = path.dirname(destination);
|
|
4694
5330
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
4695
5331
|
try {
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
// Check if source file exists, re-create if missing
|
|
4699
|
-
try {
|
|
4700
|
-
await fs.promises.access(source);
|
|
4701
|
-
}
|
|
4702
|
-
catch {
|
|
4703
|
-
// Source file missing - re-create it
|
|
4704
|
-
await fs.promises.writeFile(source, yamlContent, 'utf-8');
|
|
5332
|
+
if (cancellationToken) {
|
|
5333
|
+
this.assertRootDialogWriteBackNotCanceled(cancellationToken, `renameWithRetry:${path.basename(destination)}:before-rename`);
|
|
4705
5334
|
}
|
|
4706
5335
|
await fs.promises.rename(source, destination);
|
|
4707
5336
|
return;
|
|
4708
5337
|
}
|
|
4709
5338
|
catch (error) {
|
|
4710
|
-
|
|
5339
|
+
if (isDialogWriteBackCanceledError(error)) {
|
|
5340
|
+
throw error;
|
|
5341
|
+
}
|
|
5342
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
4711
5343
|
if (getErrorCode(error) !== 'ENOENT' || attempt === maxRetries) {
|
|
4712
5344
|
throw error;
|
|
4713
5345
|
}
|
|
@@ -4729,18 +5361,19 @@ class DialogPersistence {
|
|
|
4729
5361
|
}
|
|
4730
5362
|
const dialogPath = this.getDialogEventsPath(dialogId, status);
|
|
4731
5363
|
const latestFilePath = path.join(dialogPath, 'latest.yaml');
|
|
4732
|
-
const content = await
|
|
5364
|
+
const content = await readPersistenceTextFile({
|
|
5365
|
+
filePath: latestFilePath,
|
|
5366
|
+
source: 'dialog_latest',
|
|
5367
|
+
format: 'yaml',
|
|
5368
|
+
});
|
|
4733
5369
|
return this.parseDialogLatestYaml(content, latestFilePath);
|
|
4734
5370
|
}
|
|
4735
5371
|
catch (error) {
|
|
4736
5372
|
if (getErrorCode(error) === 'ENOENT') {
|
|
4737
5373
|
return null;
|
|
4738
5374
|
}
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
return null;
|
|
4742
|
-
}
|
|
4743
|
-
throw error;
|
|
5375
|
+
await this.rethrowAfterQuarantiningDialogPersistenceProblem(dialogId, status, 'loadDialogLatest', error);
|
|
5376
|
+
throw new Error('unreachable after loadDialogLatest persistence rethrow');
|
|
4744
5377
|
}
|
|
4745
5378
|
}
|
|
4746
5379
|
/**
|
|
@@ -4750,11 +5383,13 @@ class DialogPersistence {
|
|
|
4750
5383
|
* staged state (or disk fallback). This avoids read-modify-write races in user code
|
|
4751
5384
|
* and allows lock-free-ish coalescing to disk via the write-back buffer.
|
|
4752
5385
|
*/
|
|
4753
|
-
static async mutateDialogLatest(dialogId, mutator, status = 'running') {
|
|
5386
|
+
static async mutateDialogLatest(dialogId, mutator, status = 'running', cancellationToken) {
|
|
4754
5387
|
const key = this.getLatestWriteBackKey(dialogId, status);
|
|
4755
5388
|
const mutex = this.getLatestWriteBackMutex(key);
|
|
5389
|
+
const effectiveCancellationToken = cancellationToken ?? this.createRootDialogWriteBackCancellationToken(dialogId, status);
|
|
4756
5390
|
const release = await mutex.acquire();
|
|
4757
5391
|
try {
|
|
5392
|
+
this.assertRootDialogWriteBackNotCanceled(effectiveCancellationToken, 'mutateDialogLatest');
|
|
4758
5393
|
const staged = this.latestWriteBack.get(key);
|
|
4759
5394
|
const existing = (staged
|
|
4760
5395
|
? staged.latest
|
|
@@ -4785,6 +5420,7 @@ class DialogPersistence {
|
|
|
4785
5420
|
const _exhaustive = mutation;
|
|
4786
5421
|
throw new Error(`Unhandled dialog latest mutation: ${String(_exhaustive)}`);
|
|
4787
5422
|
}
|
|
5423
|
+
this.assertRootDialogWriteBackNotCanceled(effectiveCancellationToken, 'mutateDialogLatest:before-stage');
|
|
4788
5424
|
const pending = this.latestWriteBack.get(key);
|
|
4789
5425
|
if (!pending) {
|
|
4790
5426
|
const timer = setTimeout(() => {
|
|
@@ -4814,18 +5450,19 @@ class DialogPersistence {
|
|
|
4814
5450
|
try {
|
|
4815
5451
|
const dialogPath = this.getDialogEventsPath(dialogId, status);
|
|
4816
5452
|
const latestFilePath = path.join(dialogPath, 'latest.yaml');
|
|
4817
|
-
const content = await
|
|
5453
|
+
const content = await readPersistenceTextFile({
|
|
5454
|
+
filePath: latestFilePath,
|
|
5455
|
+
source: 'dialog_latest',
|
|
5456
|
+
format: 'yaml',
|
|
5457
|
+
});
|
|
4818
5458
|
return this.parseDialogLatestYaml(content, latestFilePath);
|
|
4819
5459
|
}
|
|
4820
5460
|
catch (error) {
|
|
4821
5461
|
if (getErrorCode(error) === 'ENOENT') {
|
|
4822
5462
|
return null;
|
|
4823
5463
|
}
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
return null;
|
|
4827
|
-
}
|
|
4828
|
-
throw error;
|
|
5464
|
+
await this.rethrowAfterQuarantiningDialogPersistenceProblem(dialogId, status, 'loadDialogLatestFromDisk', error);
|
|
5465
|
+
throw new Error('unreachable after loadDialogLatestFromDisk persistence rethrow');
|
|
4829
5466
|
}
|
|
4830
5467
|
}
|
|
4831
5468
|
static async flushLatestWriteBack(key) {
|
|
@@ -4843,7 +5480,8 @@ class DialogPersistence {
|
|
|
4843
5480
|
return;
|
|
4844
5481
|
clearTimeout(entry.timer);
|
|
4845
5482
|
const latestToWrite = entry.latest;
|
|
4846
|
-
const
|
|
5483
|
+
const cancellationToken = this.createRootDialogWriteBackCancellationToken(entry.dialogId, entry.status);
|
|
5484
|
+
const inFlight = this.writeDialogLatestToDisk(entry.dialogId, latestToWrite, entry.status, cancellationToken);
|
|
4847
5485
|
captured = {
|
|
4848
5486
|
dialogId: entry.dialogId,
|
|
4849
5487
|
status: entry.status,
|
|
@@ -4878,6 +5516,10 @@ class DialogPersistence {
|
|
|
4878
5516
|
return;
|
|
4879
5517
|
if (entry.inFlight !== captured.inFlight)
|
|
4880
5518
|
return;
|
|
5519
|
+
if (isDialogWriteBackCanceledError(error)) {
|
|
5520
|
+
this.latestWriteBack.delete(key);
|
|
5521
|
+
return;
|
|
5522
|
+
}
|
|
4881
5523
|
const timer = setTimeout(() => {
|
|
4882
5524
|
void this.flushLatestWriteBack(key);
|
|
4883
5525
|
}, this.LATEST_WRITEBACK_WINDOW_MS);
|
|
@@ -4980,18 +5622,8 @@ class DialogPersistence {
|
|
|
4980
5622
|
*/
|
|
4981
5623
|
static async countAllSubdialogsUnderRoot(rootDialogId, status = 'running') {
|
|
4982
5624
|
try {
|
|
4983
|
-
const
|
|
4984
|
-
|
|
4985
|
-
try {
|
|
4986
|
-
const entries = await fs.promises.readdir(subdialogsPath, { withFileTypes: true });
|
|
4987
|
-
return entries.filter((entry) => entry.isDirectory()).length;
|
|
4988
|
-
}
|
|
4989
|
-
catch (error) {
|
|
4990
|
-
if (getErrorCode(error) === 'ENOENT') {
|
|
4991
|
-
return 0;
|
|
4992
|
-
}
|
|
4993
|
-
throw error;
|
|
4994
|
-
}
|
|
5625
|
+
const subdialogIds = await this.listSubdialogIdsUnderRoot(rootDialogId, status);
|
|
5626
|
+
return subdialogIds.length;
|
|
4995
5627
|
}
|
|
4996
5628
|
catch (error) {
|
|
4997
5629
|
log_1.log.error(`Failed to count all subdialogs under root ${rootDialogId.selfId}:`, error);
|
|
@@ -5010,18 +5642,7 @@ class DialogPersistence {
|
|
|
5010
5642
|
return null;
|
|
5011
5643
|
}
|
|
5012
5644
|
// Recursively restore subdialogs
|
|
5013
|
-
const
|
|
5014
|
-
const subdialogsPath = path.join(rootPath, this.SUBDIALOGS_DIR);
|
|
5015
|
-
let subdialogIds = [];
|
|
5016
|
-
try {
|
|
5017
|
-
const entries = await fs.promises.readdir(subdialogsPath, { withFileTypes: true });
|
|
5018
|
-
subdialogIds = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
5019
|
-
}
|
|
5020
|
-
catch (err) {
|
|
5021
|
-
if (getErrorCode(err) !== 'ENOENT') {
|
|
5022
|
-
throw err;
|
|
5023
|
-
}
|
|
5024
|
-
}
|
|
5645
|
+
const subdialogIds = await this.listSubdialogIdsUnderRoot(rootDialogId, status);
|
|
5025
5646
|
for (const subdialogId of subdialogIds) {
|
|
5026
5647
|
await this.restoreDialogTree(new dialog_1.DialogID(subdialogId, rootDialogId.rootId), status);
|
|
5027
5648
|
}
|
|
@@ -5278,27 +5899,19 @@ class DialogPersistence {
|
|
|
5278
5899
|
*/
|
|
5279
5900
|
static async moveDialogStatus(dialogId, fromStatus, toStatus) {
|
|
5280
5901
|
try {
|
|
5281
|
-
const fromPath = path.join(this.getDialogsRootDir(), fromStatus
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
await fs.promises.mkdir(toPath, { recursive: true });
|
|
5293
|
-
// Move all files and directories
|
|
5294
|
-
const entries = await fs.promises.readdir(fromPath, { withFileTypes: true });
|
|
5295
|
-
for (const entry of entries) {
|
|
5296
|
-
const srcPath = path.join(fromPath, entry.name);
|
|
5297
|
-
const destPath = path.join(toPath, entry.name);
|
|
5298
|
-
await fs.promises.rename(srcPath, destPath);
|
|
5902
|
+
const fromPath = path.join(this.getDialogsRootDir(), getPersistableStatusDirName(fromStatus, 'DialogPersistence.moveDialogStatus(fromStatus)'), dialogId.selfId);
|
|
5903
|
+
const toPath = path.join(this.getDialogsRootDir(), getPersistableStatusDirName(toStatus, 'DialogPersistence.moveDialogStatus(toStatus)'), dialogId.selfId);
|
|
5904
|
+
await fs.promises.mkdir(path.dirname(toPath), { recursive: true });
|
|
5905
|
+
try {
|
|
5906
|
+
await fs.promises.access(toPath);
|
|
5907
|
+
throw new Error(`Refusing to move dialog ${dialogId.valueOf()} from ${fromStatus} to ${toStatus}: destination already exists at ${toPath}`);
|
|
5908
|
+
}
|
|
5909
|
+
catch (error) {
|
|
5910
|
+
if (getErrorCode(error) !== 'ENOENT') {
|
|
5911
|
+
throw error;
|
|
5912
|
+
}
|
|
5299
5913
|
}
|
|
5300
|
-
|
|
5301
|
-
await fs.promises.rm(fromPath, { recursive: true, force: true });
|
|
5914
|
+
await fs.promises.rename(fromPath, toPath);
|
|
5302
5915
|
}
|
|
5303
5916
|
catch (error) {
|
|
5304
5917
|
log_1.log.error(`Failed to move dialog ${dialogId} from ${fromStatus} to ${toStatus}:`, error);
|
|
@@ -5318,12 +5931,8 @@ class DialogPersistence {
|
|
|
5318
5931
|
return false;
|
|
5319
5932
|
// Best-effort cleanup: remove the dialog from all status directories to avoid leaving behind
|
|
5320
5933
|
// orphaned placeholder paths (e.g. `run/<id>/latest.yaml`) after a delete.
|
|
5321
|
-
const
|
|
5322
|
-
|
|
5323
|
-
'completed',
|
|
5324
|
-
'archived',
|
|
5325
|
-
];
|
|
5326
|
-
for (const candidate of allStatuses) {
|
|
5934
|
+
for (const candidate of PERSISTABLE_DIALOG_STATUSES) {
|
|
5935
|
+
this.cancelRootDialogWriteBacks(rootDialogId, candidate);
|
|
5327
5936
|
const candidatePath = this.getRootDialogPath(rootDialogId, candidate);
|
|
5328
5937
|
await fs.promises.rm(candidatePath, { recursive: true, force: true });
|
|
5329
5938
|
}
|
|
@@ -5337,7 +5946,6 @@ class DialogPersistence {
|
|
|
5337
5946
|
try {
|
|
5338
5947
|
const dialogPath = this.getDialogResponsesPath(rootDialogId, status);
|
|
5339
5948
|
const registryFilePath = path.join(dialogPath, 'registry.yaml');
|
|
5340
|
-
await fs.promises.mkdir(dialogPath, { recursive: true });
|
|
5341
5949
|
const serializableEntries = entries.map((entry) => ({
|
|
5342
5950
|
key: entry.key,
|
|
5343
5951
|
subdialogId: entry.subdialogId.selfId,
|
|
@@ -5347,7 +5955,7 @@ class DialogPersistence {
|
|
|
5347
5955
|
const yamlContent = yaml.stringify({ entries: serializableEntries });
|
|
5348
5956
|
const tempFile = path.join(dialogPath, `.${path.basename(registryFilePath)}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`);
|
|
5349
5957
|
await fs.promises.writeFile(tempFile, yamlContent, 'utf-8');
|
|
5350
|
-
await this.renameWithRetry(tempFile, registryFilePath
|
|
5958
|
+
await this.renameWithRetry(tempFile, registryFilePath);
|
|
5351
5959
|
}
|
|
5352
5960
|
catch (error) {
|
|
5353
5961
|
log_1.log.error(`Failed to save subdialog registry for dialog ${rootDialogId}:`, error);
|
|
@@ -5361,15 +5969,34 @@ class DialogPersistence {
|
|
|
5361
5969
|
try {
|
|
5362
5970
|
const dialogPath = this.getDialogResponsesPath(rootDialogId, status);
|
|
5363
5971
|
const registryFilePath = path.join(dialogPath, 'registry.yaml');
|
|
5364
|
-
const content = await
|
|
5365
|
-
|
|
5972
|
+
const content = await readPersistenceTextFile({
|
|
5973
|
+
filePath: registryFilePath,
|
|
5974
|
+
source: 'subdialog_registry',
|
|
5975
|
+
format: 'yaml',
|
|
5976
|
+
});
|
|
5977
|
+
const parsed = parsePersistenceYaml({
|
|
5978
|
+
content,
|
|
5979
|
+
filePath: registryFilePath,
|
|
5980
|
+
source: 'subdialog_registry',
|
|
5981
|
+
});
|
|
5366
5982
|
if (!isRecord(parsed) || !Array.isArray(parsed.entries)) {
|
|
5367
|
-
|
|
5368
|
-
|
|
5983
|
+
throw buildInvalidPersistenceFileError({
|
|
5984
|
+
source: 'subdialog_registry',
|
|
5985
|
+
format: 'yaml',
|
|
5986
|
+
filePath: registryFilePath,
|
|
5987
|
+
});
|
|
5369
5988
|
}
|
|
5370
5989
|
const entries = parsed.entries.map((entry) => {
|
|
5371
|
-
if (!isRecord(entry)
|
|
5372
|
-
|
|
5990
|
+
if (!isRecord(entry) ||
|
|
5991
|
+
typeof entry.key !== 'string' ||
|
|
5992
|
+
typeof entry.subdialogId !== 'string' ||
|
|
5993
|
+
typeof entry.agentId !== 'string' ||
|
|
5994
|
+
(entry.sessionSlug !== undefined && typeof entry.sessionSlug !== 'string')) {
|
|
5995
|
+
throw buildInvalidPersistenceFileError({
|
|
5996
|
+
source: 'subdialog_registry',
|
|
5997
|
+
format: 'yaml',
|
|
5998
|
+
filePath: registryFilePath,
|
|
5999
|
+
});
|
|
5373
6000
|
}
|
|
5374
6001
|
return {
|
|
5375
6002
|
key: entry.key,
|
|
@@ -5384,8 +6011,8 @@ class DialogPersistence {
|
|
|
5384
6011
|
if (getErrorCode(error) === 'ENOENT') {
|
|
5385
6012
|
return [];
|
|
5386
6013
|
}
|
|
5387
|
-
|
|
5388
|
-
|
|
6014
|
+
await this.rethrowAfterQuarantiningDialogPersistenceProblem(rootDialogId, status, 'loadSubdialogRegistry', error);
|
|
6015
|
+
throw new Error('unreachable after loadSubdialogRegistry persistence rethrow');
|
|
5389
6016
|
}
|
|
5390
6017
|
}
|
|
5391
6018
|
}
|
|
@@ -5396,6 +6023,7 @@ DialogPersistence.RUN_DIR = 'run';
|
|
|
5396
6023
|
DialogPersistence.DONE_DIR = 'done';
|
|
5397
6024
|
DialogPersistence.ARCHIVE_DIR = 'archive';
|
|
5398
6025
|
DialogPersistence.SUBDIALOGS_DIR = 'subdialogs';
|
|
6026
|
+
DialogPersistence.quarantinedRootDialogScopes = new Set();
|
|
5399
6027
|
DialogPersistence.LATEST_WRITEBACK_WINDOW_MS = 300;
|
|
5400
6028
|
DialogPersistence.Q4H_WRITEBACK_WINDOW_MS = 300;
|
|
5401
6029
|
DialogPersistence.PENDING_SUBDIALOGS_WRITEBACK_WINDOW_MS = 300;
|
|
@@ -5406,3 +6034,4 @@ DialogPersistence.q4hWriteBack = new Map();
|
|
|
5406
6034
|
DialogPersistence.pendingSubdialogsWriteBackMutexes = new Map();
|
|
5407
6035
|
DialogPersistence.pendingSubdialogsWriteBack = new Map();
|
|
5408
6036
|
DialogPersistence.courseAppendMutexes = new Map();
|
|
6037
|
+
DialogPersistence.rootDialogWriteBackCancelGenerations = new Map();
|