@spark-strategic/helix-core 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * End-to-end smoke of the durable chat turn store wired into the helix-core Connector
3
+ * (the @spark-strategic/helix-chat store, vendored into src/chat).
4
+ *
5
+ * Drives a REAL Connector against a throwaway in-process WebSocket server: the server
6
+ * plays agents-api (sends chat_start → chat_message, collects the connector's outbound
7
+ * frames), the Connector runs the turn through a test adapter, and we then read the
8
+ * on-disk chat-turn record to prove the turn was persisted as its frames streamed.
9
+ *
10
+ * The point is crash-durability: the on-disk record outlives the process. The key case
11
+ * is a FAILED turn — even then the partial streamed content and the terminal verdict are
12
+ * on disk, which is exactly the state a boot reconcile would read after a crash.
13
+ *
14
+ * Self-running (mirrors helix-os's test style): `ts-node connector-chat-durability.test.ts`.
15
+ */
16
+ export {};
17
+ //# sourceMappingURL=connector-chat-durability.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connector-chat-durability.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/connector-chat-durability.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
@@ -0,0 +1,259 @@
1
+ "use strict";
2
+ /**
3
+ * End-to-end smoke of the durable chat turn store wired into the helix-core Connector
4
+ * (the @spark-strategic/helix-chat store, vendored into src/chat).
5
+ *
6
+ * Drives a REAL Connector against a throwaway in-process WebSocket server: the server
7
+ * plays agents-api (sends chat_start → chat_message, collects the connector's outbound
8
+ * frames), the Connector runs the turn through a test adapter, and we then read the
9
+ * on-disk chat-turn record to prove the turn was persisted as its frames streamed.
10
+ *
11
+ * The point is crash-durability: the on-disk record outlives the process. The key case
12
+ * is a FAILED turn — even then the partial streamed content and the terminal verdict are
13
+ * on disk, which is exactly the state a boot reconcile would read after a crash.
14
+ *
15
+ * Self-running (mirrors helix-os's test style): `ts-node connector-chat-durability.test.ts`.
16
+ */
17
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ var desc = Object.getOwnPropertyDescriptor(m, k);
20
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
21
+ desc = { enumerable: true, get: function() { return m[k]; } };
22
+ }
23
+ Object.defineProperty(o, k2, desc);
24
+ }) : (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ o[k2] = m[k];
27
+ }));
28
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
29
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
30
+ }) : function(o, v) {
31
+ o["default"] = v;
32
+ });
33
+ var __importStar = (this && this.__importStar) || (function () {
34
+ var ownKeys = function(o) {
35
+ ownKeys = Object.getOwnPropertyNames || function (o) {
36
+ var ar = [];
37
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
38
+ return ar;
39
+ };
40
+ return ownKeys(o);
41
+ };
42
+ return function (mod) {
43
+ if (mod && mod.__esModule) return mod;
44
+ var result = {};
45
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
46
+ __setModuleDefault(result, mod);
47
+ return result;
48
+ };
49
+ })();
50
+ var __importDefault = (this && this.__importDefault) || function (mod) {
51
+ return (mod && mod.__esModule) ? mod : { "default": mod };
52
+ };
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ const node_assert_1 = __importDefault(require("node:assert"));
55
+ const fs = __importStar(require("node:fs"));
56
+ const os = __importStar(require("node:os"));
57
+ const path = __importStar(require("node:path"));
58
+ const ws_1 = require("ws");
59
+ const connector_1 = require("../connector");
60
+ const adapter_1 = require("../adapter");
61
+ const chat_store_1 = require("../chat/chat-store");
62
+ const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'helix-core-chat-'));
63
+ const silent = { debug() { }, info() { }, warn() { }, error() { } };
64
+ /** Run one turn against a fresh local server + Connector, return the collected outbound
65
+ * frames once the turn reaches a terminal (chat_done / chat_error). */
66
+ function runTurn(adapter, sessionId, message) {
67
+ return new Promise((resolve, reject) => {
68
+ const frames = [];
69
+ const wss = new ws_1.WebSocketServer({ port: 0 }, () => {
70
+ const port = wss.address().port;
71
+ const connector = new connector_1.Connector({
72
+ config: { agentsApiUrl: `http://127.0.0.1:${port}`, slug: 'test', key: 'k' },
73
+ adapter,
74
+ client: { name: 'test', version: '0.0.0' },
75
+ logger: silent,
76
+ dataDir: TMP,
77
+ });
78
+ const done = (err) => {
79
+ try {
80
+ connector.stop();
81
+ }
82
+ catch { /* ignore */ }
83
+ try {
84
+ wss.close();
85
+ }
86
+ catch { /* ignore */ }
87
+ if (err)
88
+ reject(err);
89
+ else
90
+ resolve(frames);
91
+ };
92
+ const timeout = setTimeout(() => done(new Error('turn did not terminate in time')), 5000);
93
+ wss.on('connection', (ws) => {
94
+ ws.on('message', (data) => {
95
+ let frame;
96
+ try {
97
+ frame = JSON.parse(data.toString());
98
+ }
99
+ catch {
100
+ return;
101
+ }
102
+ if (frame.type === 'connector_hello') {
103
+ // Kick the turn: enable the composer, then deliver the message.
104
+ ws.send(JSON.stringify({ type: 'chat_start', payload: { sessionId } }));
105
+ ws.send(JSON.stringify({ type: 'chat_message', payload: { sessionId, message, agentId: 'agent-x', model: 'test-model' } }));
106
+ return;
107
+ }
108
+ frames.push(frame);
109
+ if (frame.type === 'chat_done' || frame.type === 'chat_error') {
110
+ clearTimeout(timeout);
111
+ // Give the store's synchronous terminal flush a tick to settle, then finish.
112
+ setTimeout(() => done(), 50);
113
+ }
114
+ });
115
+ });
116
+ connector.start();
117
+ });
118
+ wss.on('error', reject);
119
+ });
120
+ }
121
+ /** Like runTurn, but after chat_done the server echoes a `chat_ack` carrying that frame's
122
+ * turnId — the real agents-api §3.2 round-trip — so we can prove the connector evicts its
123
+ * local record on ack. Resolves with the acked turnId once the ack has had a tick to land. */
124
+ function runTurnThenAck(adapter, sessionId, message) {
125
+ return new Promise((resolve, reject) => {
126
+ const wss = new ws_1.WebSocketServer({ port: 0 }, () => {
127
+ const port = wss.address().port;
128
+ const connector = new connector_1.Connector({
129
+ config: { agentsApiUrl: `http://127.0.0.1:${port}`, slug: 'test', key: 'k' },
130
+ adapter,
131
+ client: { name: 'test', version: '0.0.0' },
132
+ logger: silent,
133
+ dataDir: TMP,
134
+ });
135
+ const done = (err, turnId) => {
136
+ try {
137
+ connector.stop();
138
+ }
139
+ catch { /* ignore */ }
140
+ try {
141
+ wss.close();
142
+ }
143
+ catch { /* ignore */ }
144
+ if (err)
145
+ reject(err);
146
+ else
147
+ resolve({ turnId });
148
+ };
149
+ const timeout = setTimeout(() => done(new Error('turn did not terminate in time')), 5000);
150
+ wss.on('connection', (ws) => {
151
+ ws.on('message', (data) => {
152
+ let frame;
153
+ try {
154
+ frame = JSON.parse(data.toString());
155
+ }
156
+ catch {
157
+ return;
158
+ }
159
+ if (frame.type === 'connector_hello') {
160
+ ws.send(JSON.stringify({ type: 'chat_start', payload: { sessionId } }));
161
+ ws.send(JSON.stringify({ type: 'chat_message', payload: { sessionId, message, agentId: 'agent-x', model: 'test-model' } }));
162
+ return;
163
+ }
164
+ if (frame.type === 'chat_done') {
165
+ clearTimeout(timeout);
166
+ const turnId = typeof frame.turnId === 'string' ? frame.turnId : undefined;
167
+ // Echo the §3.2 ack exactly as agents-api's ackFinalWriteToNode does, keyed by
168
+ // the done frame's turnId, then give the connector's handler a tick to evict.
169
+ ws.send(JSON.stringify({ type: 'chat_ack', payload: { sessionId, turnId } }));
170
+ setTimeout(() => done(undefined, turnId), 100);
171
+ }
172
+ });
173
+ });
174
+ connector.start();
175
+ });
176
+ wss.on('error', reject);
177
+ });
178
+ }
179
+ function recordFor(sessionId) {
180
+ const recs = (0, chat_store_1.recordsForSession)(sessionId);
181
+ node_assert_1.default.equal(recs.length, 1, `exactly one durable record for ${sessionId} (got ${recs.length})`);
182
+ return recs[0];
183
+ }
184
+ /** Adapter that streams one delta then throws — a failed turn with partial output. */
185
+ class FailAfterDeltaAdapter {
186
+ name = 'fail';
187
+ version = '0.0.0';
188
+ async runTurn(_ctx, emit) {
189
+ emit.delta('partial answer before the crash');
190
+ await new Promise((r) => setTimeout(r, 10));
191
+ throw new Error('adapter blew up');
192
+ }
193
+ }
194
+ /** Adapter that runs one tool (call → result) then completes. */
195
+ class ToolAdapter {
196
+ name = 'tool';
197
+ version = '0.0.0';
198
+ async runTurn(_ctx, emit) {
199
+ emit.tool('lookup', 'call');
200
+ await new Promise((r) => setTimeout(r, 10));
201
+ emit.tool('lookup', 'result', true);
202
+ emit.delta('done');
203
+ }
204
+ }
205
+ async function main() {
206
+ // 1. Healthy turn: full content buffered, marked complete + done.
207
+ const okFrames = await runTurn(new adapter_1.EchoAdapter(), 'sess-ok', 'hello');
208
+ node_assert_1.default.ok(okFrames.some((f) => f.type === 'chat_done'), 'healthy turn emits chat_done');
209
+ const ok = recordFor('sess-ok');
210
+ node_assert_1.default.equal(ok.content, 'You said: hello', 'full streamed content is durable on disk');
211
+ node_assert_1.default.equal(ok.stream.complete, true, 'stream marked complete');
212
+ node_assert_1.default.equal(ok.final.written, true, 'final write flagged');
213
+ node_assert_1.default.equal(ok.terminalReason, 'done', 'terminal reason is done');
214
+ node_assert_1.default.ok(ok.stream.lastStreamSeq > 0, 'at least one stream frame recorded');
215
+ node_assert_1.default.equal(ok.model, 'test-model', 'model recorded from the inbound frame');
216
+ node_assert_1.default.equal(ok.agentId, 'agent-x', 'agentId recorded from the inbound frame');
217
+ console.log('✓ healthy turn: full content + complete + done persisted');
218
+ // 2. FAILED turn: the key crash-durability case — partial content AND the error
219
+ // verdict are on disk even though the turn never completed.
220
+ const failFrames = await runTurn(new FailAfterDeltaAdapter(), 'sess-fail', 'go');
221
+ node_assert_1.default.ok(failFrames.some((f) => f.type === 'chat_error'), 'failed turn emits chat_error');
222
+ const fail = recordFor('sess-fail');
223
+ node_assert_1.default.equal(fail.content, 'partial answer before the crash', 'partial streamed content survives a failure');
224
+ node_assert_1.default.equal(fail.stream.complete, false, 'stream NOT complete on failure');
225
+ node_assert_1.default.equal(fail.terminalReason, 'error', 'terminal reason is error');
226
+ node_assert_1.default.equal(fail.errorMessage, 'adapter blew up', 'error message persisted');
227
+ console.log('✓ failed turn: partial content + error verdict persisted (the crash-recovery coordinate)');
228
+ // 3. Tool turn: the ledger records the call and resolves it by name (no wire tool_use id).
229
+ const toolFrames = await runTurn(new ToolAdapter(), 'sess-tool', 'run tool');
230
+ node_assert_1.default.ok(toolFrames.some((f) => f.type === 'chat_done'), 'tool turn completes');
231
+ const tool = recordFor('sess-tool');
232
+ node_assert_1.default.equal(tool.tools.length, 1, 'one tool ledger entry');
233
+ node_assert_1.default.equal(tool.tools[0].name, 'lookup', 'tool name recorded');
234
+ node_assert_1.default.equal(tool.tools[0].state, 'executed', 'tool resolved to executed');
235
+ node_assert_1.default.equal(tool.tools[0].ok, true, 'tool result ok recorded');
236
+ console.log('✓ tool turn: ledger records call + result (name-resolved, fail-closed effect class)');
237
+ // 4. §3.2 ack round-trip: chat_done carries the turn's node-local store id; the server
238
+ // echoes it as chat_ack; the connector's handler calls markAcked → the local record
239
+ // flips acked/evicted and its heavy content is dropped (the server now owns the
240
+ // durable copy). This is the authoritative close, faithful to helix-os.
241
+ const { turnId } = await runTurnThenAck(new adapter_1.EchoAdapter(), 'sess-ack', 'ack me');
242
+ node_assert_1.default.ok(turnId, 'chat_done carried a turnId for the server to ack');
243
+ const acked = recordFor('sess-ack');
244
+ node_assert_1.default.equal(acked.final.acked, true, 'record flipped acked on chat_ack');
245
+ node_assert_1.default.equal(acked.final.evicted, true, 'record evicted on ack');
246
+ node_assert_1.default.equal(acked.content, '', 'heavy content dropped once the server owns the durable copy');
247
+ console.log('✓ ack round-trip: chat_done→chat_ack evicts the node-local record (§3.2)');
248
+ fs.rmSync(TMP, { recursive: true, force: true });
249
+ console.log('\n✓ connector chat durability: healthy + failed + tool + ack turns all persisted node-side');
250
+ }
251
+ main().catch((err) => {
252
+ console.error('✗ connector-chat-durability test failed:', err);
253
+ try {
254
+ fs.rmSync(TMP, { recursive: true, force: true });
255
+ }
256
+ catch { /* ignore */ }
257
+ process.exit(1);
258
+ });
259
+ //# sourceMappingURL=connector-chat-durability.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connector-chat-durability.test.js","sourceRoot":"","sources":["../../src/__tests__/connector-chat-durability.test.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEH,8DAAiC;AACjC,4CAA8B;AAC9B,4CAA8B;AAC9B,gDAAkC;AAClC,2BAA2D;AAC3D,4CAAsD;AACtD,wCAAgG;AAChG,mDAA4E;AAE5E,MAAM,GAAG,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC;AACvE,MAAM,MAAM,GAAW,EAAE,KAAK,KAAI,CAAC,EAAE,IAAI,KAAI,CAAC,EAAE,IAAI,KAAI,CAAC,EAAE,KAAK,KAAI,CAAC,EAAE,CAAC;AAExE;wEACwE;AACxE,SAAS,OAAO,CAAC,OAAqB,EAAE,SAAiB,EAAE,OAAe;IACxE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,MAAM,GAA8B,EAAE,CAAC;QAC7C,MAAM,GAAG,GAAG,IAAI,oBAAe,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE;YAChD,MAAM,IAAI,GAAI,GAAG,CAAC,OAAO,EAAuB,CAAC,IAAI,CAAC;YACtD,MAAM,SAAS,GAAG,IAAI,qBAAS,CAAC;gBAC9B,MAAM,EAAE,EAAE,YAAY,EAAE,oBAAoB,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE;gBAC5E,OAAO;gBACP,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE;gBAC1C,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,GAAG;aACb,CAAC,CAAC;YAEH,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,EAAE;gBAC3B,IAAI,CAAC;oBAAC,SAAS,CAAC,IAAI,EAAE,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;gBAChD,IAAI,CAAC;oBAAC,GAAG,CAAC,KAAK,EAAE,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;gBAC3C,IAAI,GAAG;oBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;;oBAAM,OAAO,CAAC,MAAM,CAAC,CAAC;YAC7C,CAAC,CAAC;YAEF,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YAE1F,GAAG,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,EAAM,EAAE,EAAE;gBAC9B,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE;oBACxB,IAAI,KAA8B,CAAC;oBACnC,IAAI,CAAC;wBAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;oBAAC,CAAC;oBAAC,MAAM,CAAC;wBAAC,OAAO;oBAAC,CAAC;oBAC9D,IAAI,KAAK,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;wBACrC,gEAAgE;wBAChE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;wBACxE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,EAAE,CAAC,CAAC,CAAC;wBAC5H,OAAO;oBACT,CAAC;oBACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;oBACnB,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;wBAC9D,YAAY,CAAC,OAAO,CAAC,CAAC;wBACtB,6EAA6E;wBAC7E,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;oBAC/B,CAAC;gBACH,CAAC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;YAEH,SAAS,CAAC,KAAK,EAAE,CAAC;QACpB,CAAC,CAAC,CAAC;QACH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;+FAE+F;AAC/F,SAAS,cAAc,CAAC,OAAqB,EAAE,SAAiB,EAAE,OAAe;IAC/E,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,GAAG,GAAG,IAAI,oBAAe,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE;YAChD,MAAM,IAAI,GAAI,GAAG,CAAC,OAAO,EAAuB,CAAC,IAAI,CAAC;YACtD,MAAM,SAAS,GAAG,IAAI,qBAAS,CAAC;gBAC9B,MAAM,EAAE,EAAE,YAAY,EAAE,oBAAoB,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE;gBAC5E,OAAO;gBACP,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE;gBAC1C,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,GAAG;aACb,CAAC,CAAC;YAEH,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,MAAe,EAAE,EAAE;gBAC5C,IAAI,CAAC;oBAAC,SAAS,CAAC,IAAI,EAAE,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;gBAChD,IAAI,CAAC;oBAAC,GAAG,CAAC,KAAK,EAAE,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;gBAC3C,IAAI,GAAG;oBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;;oBAAM,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;YACjD,CAAC,CAAC;YAEF,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;YAE1F,GAAG,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,EAAM,EAAE,EAAE;gBAC9B,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE;oBACxB,IAAI,KAA8B,CAAC;oBACnC,IAAI,CAAC;wBAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;oBAAC,CAAC;oBAAC,MAAM,CAAC;wBAAC,OAAO;oBAAC,CAAC;oBAC9D,IAAI,KAAK,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;wBACrC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;wBACxE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,EAAE,CAAC,CAAC,CAAC;wBAC5H,OAAO;oBACT,CAAC;oBACD,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;wBAC/B,YAAY,CAAC,OAAO,CAAC,CAAC;wBACtB,MAAM,MAAM,GAAG,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;wBAC3E,+EAA+E;wBAC/E,8EAA8E;wBAC9E,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;wBAC9E,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC;oBACjD,CAAC;gBACH,CAAC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;YAEH,SAAS,CAAC,KAAK,EAAE,CAAC;QACpB,CAAC,CAAC,CAAC;QACH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,SAAS,CAAC,SAAiB;IAClC,MAAM,IAAI,GAAG,IAAA,8BAAiB,EAAC,SAAS,CAAC,CAAC;IAC1C,qBAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,kCAAkC,SAAS,SAAS,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IACjG,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC;AAED,sFAAsF;AACtF,MAAM,qBAAqB;IAChB,IAAI,GAAG,MAAM,CAAC;IACd,OAAO,GAAG,OAAO,CAAC;IAC3B,KAAK,CAAC,OAAO,CAAC,IAAiB,EAAE,IAAiB;QAChD,IAAI,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC;QAC9C,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACrC,CAAC;CACF;AAED,iEAAiE;AACjE,MAAM,WAAW;IACN,IAAI,GAAG,MAAM,CAAC;IACd,OAAO,GAAG,OAAO,CAAC;IAC3B,KAAK,CAAC,OAAO,CAAC,IAAiB,EAAE,IAAiB;QAChD,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC5B,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QAC5C,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;QACpC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,CAAC;CACF;AAED,KAAK,UAAU,IAAI;IACjB,kEAAkE;IAClE,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,IAAI,qBAAW,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IACtE,qBAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,EAAE,8BAA8B,CAAC,CAAC;IACxF,MAAM,EAAE,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;IAChC,qBAAM,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,EAAE,0CAA0C,CAAC,CAAC;IACxF,qBAAM,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,wBAAwB,CAAC,CAAC;IACjE,qBAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,qBAAqB,CAAC,CAAC;IAC5D,qBAAM,CAAC,KAAK,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,yBAAyB,CAAC,CAAC;IACnE,qBAAM,CAAC,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,aAAa,GAAG,CAAC,EAAE,oCAAoC,CAAC,CAAC;IAC7E,qBAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,uCAAuC,CAAC,CAAC;IAC9E,qBAAM,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,yCAAyC,CAAC,CAAC;IAC/E,OAAO,CAAC,GAAG,CAAC,0DAA0D,CAAC,CAAC;IAExE,gFAAgF;IAChF,+DAA+D;IAC/D,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,IAAI,qBAAqB,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC;IACjF,qBAAM,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,YAAY,CAAC,EAAE,8BAA8B,CAAC,CAAC;IAC3F,MAAM,IAAI,GAAG,SAAS,CAAC,WAAW,CAAC,CAAC;IACpC,qBAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,iCAAiC,EAAE,6CAA6C,CAAC,CAAC;IAC7G,qBAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,EAAE,gCAAgC,CAAC,CAAC;IAC5E,qBAAM,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,EAAE,OAAO,EAAE,0BAA0B,CAAC,CAAC;IACvE,qBAAM,CAAC,KAAK,CAAC,IAAI,CAAC,YAAY,EAAE,iBAAiB,EAAE,yBAAyB,CAAC,CAAC;IAC9E,OAAO,CAAC,GAAG,CAAC,0FAA0F,CAAC,CAAC;IAExG,2FAA2F;IAC3F,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,IAAI,WAAW,EAAE,EAAE,WAAW,EAAE,UAAU,CAAC,CAAC;IAC7E,qBAAM,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,EAAE,qBAAqB,CAAC,CAAC;IACjF,MAAM,IAAI,GAAG,SAAS,CAAC,WAAW,CAAC,CAAC;IACpC,qBAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,uBAAuB,CAAC,CAAC;IAC5D,qBAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,oBAAoB,CAAC,CAAC;IACjE,qBAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,2BAA2B,CAAC,CAAC;IAC3E,qBAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,yBAAyB,CAAC,CAAC;IAChE,OAAO,CAAC,GAAG,CAAC,qFAAqF,CAAC,CAAC;IAEnG,uFAAuF;IACvF,uFAAuF;IACvF,mFAAmF;IACnF,2EAA2E;IAC3E,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,cAAc,CAAC,IAAI,qBAAW,EAAE,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IACjF,qBAAM,CAAC,EAAE,CAAC,MAAM,EAAE,kDAAkD,CAAC,CAAC;IACtE,MAAM,KAAK,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;IACpC,qBAAM,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,kCAAkC,CAAC,CAAC;IAC1E,qBAAM,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,uBAAuB,CAAC,CAAC;IACjE,qBAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,6DAA6D,CAAC,CAAC;IAC/F,OAAO,CAAC,GAAG,CAAC,0EAA0E,CAAC,CAAC;IAExF,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACjD,OAAO,CAAC,GAAG,CAAC,4FAA4F,CAAC,CAAC;AAC5G,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,0CAA0C,EAAE,GAAG,CAAC,CAAC;IAC/D,IAAI,CAAC;QAAC,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;IAChF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,44 @@
1
+ import { type ChatTurnRecord, type ReconcileDisposition, type ToolReconcileOutcome } from './chat-store';
2
+ /**
3
+ * The §5.1 policy table as a pure function: an effect class → its reconcile
4
+ * disposition. Unknown/undeclared classes fail closed to `needs-agent-review`.
5
+ * Pure w.r.t. IO — the deterministic heart the whole reconciler is built around.
6
+ */
7
+ export declare function dispositionForEffectClass(effectClass: string | undefined): ReconcileDisposition;
8
+ /** One turn's reconcile plan — the per-gap verdicts, computed purely from the record. */
9
+ export interface TurnReconcilePlan {
10
+ turnId: string;
11
+ sessionId: string;
12
+ agentId?: string;
13
+ outcomes: ToolReconcileOutcome[];
14
+ }
15
+ /**
16
+ * Compute (but do not write) the reconcile plan for one sealed turn: apply the policy
17
+ * table to every ledger gap. Pure — no IO, no mutation — so the policy is unit-testable
18
+ * in isolation from the boot scan.
19
+ */
20
+ export declare function planTurnReconcile(rec: ChatTurnRecord): TurnReconcilePlan;
21
+ export interface ReconcileSummary {
22
+ /** Turns that needed a pass (had an unreconciled gap). */
23
+ scanned: number;
24
+ /** Turns whose verdict was written durably this pass. */
25
+ reconciled: number;
26
+ /** Gap entries by disposition across all reconciled turns. */
27
+ resolvedSafe: number;
28
+ needsVerify: number;
29
+ needsAgentReview: number;
30
+ }
31
+ /**
32
+ * Boot reconcile: walk every interrupted turn still owing a §5.1 pass, apply the policy
33
+ * table to its ledger gaps, and write the verdict back durably. Returns a summary.
34
+ *
35
+ * Runs ONCE per boot (every record on disk predates this process, so a gap is always a
36
+ * prior life's interrupted turn, never a live one). A turn is visited at most once —
37
+ * `reconcileTurnRecord` stamps `reconciledAt` and the selector excludes anything stamped
38
+ * — so a crash-loop never re-reconciles. Each `needs-agent-review` / `needs-verify` gap
39
+ * is logged (agent + session + tool + class) so the residual is visible in the node log
40
+ * even before the agent's next turn consumes it. Fail-soft: a per-turn error is logged
41
+ * and skipped; the pass always returns.
42
+ */
43
+ export declare function reconcileInterruptedChatTurnsOnBoot(): ReconcileSummary;
44
+ //# sourceMappingURL=chat-reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-reconcile.d.ts","sourceRoot":"","sources":["../../src/chat/chat-reconcile.ts"],"names":[],"mappings":"AAmDA,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EAC1B,MAAM,cAAc,CAAC;AAMtB;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,oBAAoB,CAY/F;AAED,yFAAyF;AACzF,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,oBAAoB,EAAE,CAAC;CAClC;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,iBAAiB,CAaxE;AAED,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mCAAmC,IAAI,gBAAgB,CA0DtE"}
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ // ============================================================================
3
+ // GENERATED — DO NOT EDIT.
4
+ // Vendored from @spark-strategic/helix-chat (auxiliary/helix-chat/src/chat-reconcile.ts) by
5
+ // helix-core/scripts/vendor-helix-chat.js at build time. Edit the canonical file
6
+ // in @spark-strategic/helix-chat and rebuild; changes made directly here will be overwritten.
7
+ // ============================================================================
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.dispositionForEffectClass = dispositionForEffectClass;
10
+ exports.planTurnReconcile = planTurnReconcile;
11
+ exports.reconcileInterruptedChatTurnsOnBoot = reconcileInterruptedChatTurnsOnBoot;
12
+ /**
13
+ * §5.1 tool-call reconciler — the resume-path actor.
14
+ *
15
+ * The chat store (`chat-store.ts`) RECORDS the coordinates of an interrupted turn:
16
+ * each tool invocation writes `started` before `executed` (side-effect ordering), and
17
+ * a turn sealed without a terminal frame leaves its in-flight calls as a `started &&
18
+ * !executed` gap (`indeterminate` once sealed, or raw `started` after a hard crash
19
+ * that killed the stream loop before `seal` ran). This module is what READS that
20
+ * sealed ledger on node boot and applies the §5.1 policy table — the half the store
21
+ * deliberately left to "the resume path."
22
+ *
23
+ * The policy is DETERMINISTIC by the tool's declared effect class, never by asking the
24
+ * user (spec §5.1):
25
+ *
26
+ * | class | policy | disposition |
27
+ * |----------------------------|-------------------------------------------|---------------------|
28
+ * | pure / read | re-run freely (no side effect) | resolved-safe |
29
+ * | idempotent-keyed | re-run; tool collapses the dup by its key | resolved-safe |
30
+ * | verifiable | verify reality by key, re-run if absent | needs-verify |
31
+ * | irreversible-unverifiable | fail CLOSED — never auto-replay | needs-agent-review |
32
+ *
33
+ * DEFAULT IS FAIL-CLOSED. An undeclared/undeterminable class is read as
34
+ * `irreversible-unverifiable` → `needs-agent-review`: re-running is an opt-in a tool
35
+ * EARNS by declaring a safe class. This biases every ambiguous gap toward NOT repeating
36
+ * a side effect — a missed re-run is recoverable by the user, a duplicated irreversible
37
+ * side effect often is not.
38
+ *
39
+ * What this reconciler does NOT do, by design (spec §5.2 "node loss → close"): it does
40
+ * not resurrect or re-execute the interrupted turn. The turn is already sealed/closed;
41
+ * there is no handoff and no migration. The reconciler's job is to resolve the ledger
42
+ * into its correct final state and SURFACE the residual — the actual re-run / verify /
43
+ * resend happens when the AGENT next runs (§5.1 residual handling: it reads the
44
+ * agent-facing reconcile task and resolves it itself via a sibling verify tool, or the
45
+ * user's next action answers it, re-entering the normal consent gate — never a
46
+ * privileged rerun fast-path). A `needs-agent-review`/`needs-verify` disposition is the
47
+ * durable breadcrumb that reconcile task is built from; the §5.3 inspect command reads
48
+ * it per entry.
49
+ *
50
+ * Idempotent: a turn is stamped `reconciledAt` once visited, so a reboot loop never
51
+ * re-reconciles. Fail-soft throughout — reconcile is recovery instrumentation and must
52
+ * never break boot (the standing §0 principle the whole store honors).
53
+ */
54
+ const chat_store_context_1 = require("./chat-store-context");
55
+ const chat_store_1 = require("./chat-store");
56
+ /** The fail-closed default an undeclared/unknown effect class collapses to. Mirrors
57
+ * `local-tools.ts` DEFAULT_EFFECT_CLASS / the store's FAIL_CLOSED_EFFECT_CLASS. */
58
+ const FAIL_CLOSED_EFFECT_CLASS = 'irreversible-unverifiable';
59
+ /**
60
+ * The §5.1 policy table as a pure function: an effect class → its reconcile
61
+ * disposition. Unknown/undeclared classes fail closed to `needs-agent-review`.
62
+ * Pure w.r.t. IO — the deterministic heart the whole reconciler is built around.
63
+ */
64
+ function dispositionForEffectClass(effectClass) {
65
+ switch (effectClass) {
66
+ case 'pure':
67
+ case 'read':
68
+ case 'idempotent-keyed':
69
+ return 'resolved-safe';
70
+ case 'verifiable':
71
+ return 'needs-verify';
72
+ case 'irreversible-unverifiable':
73
+ default:
74
+ return 'needs-agent-review'; // fail closed: undeclared ⇒ assume irreversible
75
+ }
76
+ }
77
+ /**
78
+ * Compute (but do not write) the reconcile plan for one sealed turn: apply the policy
79
+ * table to every ledger gap. Pure — no IO, no mutation — so the policy is unit-testable
80
+ * in isolation from the boot scan.
81
+ */
82
+ function planTurnReconcile(rec) {
83
+ const outcomes = [];
84
+ for (const entry of rec.tools) {
85
+ if (!(0, chat_store_1.isReconcileGap)(entry))
86
+ continue; // executed ⇒ no gap to reconcile
87
+ const effectClass = entry.effectClass || FAIL_CLOSED_EFFECT_CLASS;
88
+ outcomes.push({
89
+ ordinal: entry.ordinal,
90
+ ...(entry.toolCallId ? { toolCallId: entry.toolCallId } : {}),
91
+ disposition: dispositionForEffectClass(effectClass),
92
+ effectClass,
93
+ });
94
+ }
95
+ return { turnId: rec.turnId, sessionId: rec.sessionId, agentId: rec.agentId, outcomes };
96
+ }
97
+ /**
98
+ * Boot reconcile: walk every interrupted turn still owing a §5.1 pass, apply the policy
99
+ * table to its ledger gaps, and write the verdict back durably. Returns a summary.
100
+ *
101
+ * Runs ONCE per boot (every record on disk predates this process, so a gap is always a
102
+ * prior life's interrupted turn, never a live one). A turn is visited at most once —
103
+ * `reconcileTurnRecord` stamps `reconciledAt` and the selector excludes anything stamped
104
+ * — so a crash-loop never re-reconciles. Each `needs-agent-review` / `needs-verify` gap
105
+ * is logged (agent + session + tool + class) so the residual is visible in the node log
106
+ * even before the agent's next turn consumes it. Fail-soft: a per-turn error is logged
107
+ * and skipped; the pass always returns.
108
+ */
109
+ function reconcileInterruptedChatTurnsOnBoot() {
110
+ const logger = (0, chat_store_context_1.getChatStoreContext)().logger;
111
+ const summary = {
112
+ scanned: 0, reconciled: 0, resolvedSafe: 0, needsVerify: 0, needsAgentReview: 0,
113
+ };
114
+ let turns;
115
+ try {
116
+ turns = (0, chat_store_1.listTurnsNeedingReconcile)();
117
+ }
118
+ catch (err) {
119
+ logger.warn('Chat reconcile: failed to list interrupted turns; skipping', { error: String(err) });
120
+ return summary;
121
+ }
122
+ for (const rec of turns) {
123
+ summary.scanned++;
124
+ try {
125
+ const plan = planTurnReconcile(rec);
126
+ if (plan.outcomes.length === 0)
127
+ continue; // gap vanished between list and read
128
+ const wrote = (0, chat_store_1.reconcileTurnRecord)(rec.turnId, plan.outcomes);
129
+ if (!wrote)
130
+ continue; // already reconciled by a racing pass / unknown — idempotent
131
+ summary.reconciled++;
132
+ for (const o of plan.outcomes) {
133
+ if (o.disposition === 'resolved-safe')
134
+ summary.resolvedSafe++;
135
+ else if (o.disposition === 'needs-verify')
136
+ summary.needsVerify++;
137
+ else
138
+ summary.needsAgentReview++;
139
+ }
140
+ // Surface the residual. `resolved-safe` gaps need no attention (a re-run would be
141
+ // free); the others are the agent-facing reconcile tasks the next turn resolves.
142
+ const residual = plan.outcomes.filter((o) => o.disposition !== 'resolved-safe');
143
+ if (residual.length > 0) {
144
+ const byOrdinal = new Map(rec.tools.map((t) => [t.ordinal, t.name]));
145
+ logger.warn('Chat reconcile: interrupted turn has unresolved tool side effects', {
146
+ turnId: rec.turnId,
147
+ sessionId: rec.sessionId,
148
+ agentId: rec.agentId,
149
+ residual: residual.map((o) => ({
150
+ tool: byOrdinal.get(o.ordinal) ?? `#${o.ordinal}`,
151
+ toolCallId: o.toolCallId,
152
+ effectClass: o.effectClass,
153
+ disposition: o.disposition,
154
+ })),
155
+ });
156
+ }
157
+ }
158
+ catch (err) {
159
+ logger.warn('Chat reconcile: failed to reconcile a turn; leaving for next boot', {
160
+ turnId: rec.turnId, error: String(err),
161
+ });
162
+ }
163
+ }
164
+ if (summary.reconciled > 0) {
165
+ logger.info('Chat reconcile: applied §5.1 policy to interrupted turns', {
166
+ scanned: summary.scanned,
167
+ reconciled: summary.reconciled,
168
+ resolvedSafe: summary.resolvedSafe,
169
+ needsVerify: summary.needsVerify,
170
+ needsAgentReview: summary.needsAgentReview,
171
+ });
172
+ }
173
+ return summary;
174
+ }
175
+ //# sourceMappingURL=chat-reconcile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-reconcile.js","sourceRoot":"","sources":["../../src/chat/chat-reconcile.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /** The minimal logger the store/reconciler use. Structurally satisfied by winston's
2
+ * `Logger` and by a plain console wrapper — the two hosts' loggers both fit. */
3
+ export interface ChatLogger {
4
+ debug(msg: string, meta?: unknown): void;
5
+ info(msg: string, meta?: unknown): void;
6
+ warn(msg: string, meta?: unknown): void;
7
+ error(msg: string, meta?: unknown): void;
8
+ }
9
+ /** What the host injects. Both members are consulted lazily (see module header). */
10
+ export interface ChatStoreContext {
11
+ /** Resolve the base data dir; the store writes under `<dataDir>/chat-turns`. Called
12
+ * per operation so a host that mutates its config (or a test that points it at a
13
+ * throwaway dir) is always honored at the moment of the write. */
14
+ resolveDataDir(): string;
15
+ /** Where the store's fail-soft diagnostics go. */
16
+ logger: ChatLogger;
17
+ }
18
+ /**
19
+ * Inject the host's data dir resolver + logger. Call once at boot. Idempotent — a later
20
+ * call simply replaces the context (useful for tests). Never throws.
21
+ */
22
+ export declare function configureChatStore(ctx: ChatStoreContext): void;
23
+ /** The active context, or the safe default if the host never configured. Never throws. */
24
+ export declare function getChatStoreContext(): ChatStoreContext;
25
+ //# sourceMappingURL=chat-store-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-store-context.d.ts","sourceRoot":"","sources":["../../src/chat/chat-store-context.ts"],"names":[],"mappings":"AAoCA;iFACiF;AACjF,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACzC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACxC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACxC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC1C;AAED,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAC/B;;uEAEmE;IACnE,cAAc,IAAI,MAAM,CAAC;IACzB,kDAAkD;IAClD,MAAM,EAAE,UAAU,CAAC;CACpB;AAgBD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAE9D;AAED,0FAA0F;AAC1F,wBAAgB,mBAAmB,IAAI,gBAAgB,CAEtD"}