@exaudeus/workrail 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/config/feature-flags.js +1 -1
  2. package/dist/di/container.js +72 -0
  3. package/dist/di/tokens.d.ts +13 -0
  4. package/dist/di/tokens.js +13 -0
  5. package/dist/manifest.json +109 -93
  6. package/dist/mcp/error-mapper.d.ts +3 -8
  7. package/dist/mcp/error-mapper.js +41 -19
  8. package/dist/mcp/handlers/session.js +25 -11
  9. package/dist/mcp/handlers/v2-execution-helpers.d.ts +99 -0
  10. package/dist/mcp/handlers/v2-execution-helpers.js +249 -0
  11. package/dist/mcp/handlers/v2-execution.d.ts +4 -0
  12. package/dist/mcp/handlers/v2-execution.js +1061 -0
  13. package/dist/mcp/handlers/v2-workflow.js +7 -7
  14. package/dist/mcp/handlers/workflow.js +21 -12
  15. package/dist/mcp/index.d.ts +1 -1
  16. package/dist/mcp/index.js +4 -1
  17. package/dist/mcp/output-schemas.d.ts +411 -4
  18. package/dist/mcp/output-schemas.js +57 -1
  19. package/dist/mcp/server.d.ts +1 -1
  20. package/dist/mcp/server.js +57 -31
  21. package/dist/mcp/tool-descriptions.js +17 -0
  22. package/dist/mcp/tools.js +12 -0
  23. package/dist/mcp/types/tool-description-types.d.ts +1 -1
  24. package/dist/mcp/types/tool-description-types.js +2 -0
  25. package/dist/mcp/types.d.ts +38 -3
  26. package/dist/mcp/types.js +32 -3
  27. package/dist/mcp/v2/tool-registry.js +16 -1
  28. package/dist/mcp/v2/tools.d.ts +45 -0
  29. package/dist/mcp/v2/tools.js +21 -1
  30. package/dist/mcp/validation/workflow-next-prevalidate.d.ts +2 -3
  31. package/dist/mcp/validation/workflow-next-prevalidate.js +38 -27
  32. package/dist/v2/durable-core/ids/index.d.ts +2 -0
  33. package/dist/v2/durable-core/ids/index.js +4 -0
  34. package/dist/v2/durable-core/schemas/compiled-workflow/index.d.ts +100 -6
  35. package/dist/v2/durable-core/schemas/compiled-workflow/index.js +18 -3
  36. package/dist/v2/durable-core/schemas/session/events.d.ts +80 -50
  37. package/dist/v2/durable-core/schemas/session/events.js +27 -9
  38. package/dist/v2/durable-core/schemas/session/manifest.d.ts +2 -2
  39. package/dist/v2/durable-core/tokens/index.d.ts +2 -0
  40. package/dist/v2/durable-core/tokens/index.js +4 -1
  41. package/dist/v2/durable-core/tokens/payloads.d.ts +4 -4
  42. package/dist/v2/infra/local/pinned-workflow-store/index.d.ts +3 -3
  43. package/dist/v2/infra/local/pinned-workflow-store/index.js +1 -1
  44. package/dist/v2/infra/local/session-lock/index.js +1 -1
  45. package/dist/v2/infra/local/session-store/index.d.ts +0 -1
  46. package/dist/v2/infra/local/session-store/index.js +348 -280
  47. package/dist/v2/ports/pinned-workflow-store.port.d.ts +3 -3
  48. package/dist/v2/ports/session-event-log-store.port.d.ts +1 -1
  49. package/dist/v2/ports/session-lock.port.d.ts +1 -1
  50. package/dist/v2/read-only/v1-to-v2-shim.d.ts +6 -1
  51. package/dist/v2/read-only/v1-to-v2-shim.js +16 -4
  52. package/dist/v2/usecases/execution-session-gate.d.ts +3 -2
  53. package/dist/v2/usecases/execution-session-gate.js +98 -101
  54. package/package.json +2 -1
  55. package/workflows/coding-task-workflow-agentic.json +326 -69
  56. package/workflows/design-thinking-workflow-autonomous.agentic.json +1 -1
  57. package/workflows/design-thinking-workflow.json +1 -1
@@ -4,12 +4,6 @@ exports.LocalSessionEventLogStoreV2 = void 0;
4
4
  const neverthrow_1 = require("neverthrow");
5
5
  const jsonl_js_1 = require("../../../durable-core/canonical/jsonl.js");
6
6
  const index_js_1 = require("../../../durable-core/schemas/session/index.js");
7
- class StoreFailure extends Error {
8
- constructor(storeError) {
9
- super(storeError.message);
10
- this.storeError = storeError;
11
- }
12
- }
13
7
  class LocalSessionEventLogStoreV2 {
14
8
  constructor(dataDir, fs, sha256) {
15
9
  this.dataDir = dataDir;
@@ -23,286 +17,356 @@ class LocalSessionEventLogStoreV2 {
23
17
  message: 'WithHealthySessionLock used after gate callback ended (witness misuse-after-release)',
24
18
  });
25
19
  }
26
- return neverthrow_1.ResultAsync.fromPromise(this.appendImpl(lock.sessionId, plan), (e) => {
27
- if (e instanceof StoreFailure)
28
- return e.storeError;
29
- return { code: 'SESSION_STORE_IO_ERROR', message: e instanceof Error ? e.message : String(e) };
30
- });
20
+ return this.appendImpl(lock.sessionId, plan);
31
21
  }
32
22
  load(sessionId) {
33
- return neverthrow_1.ResultAsync.fromPromise(this.loadImpl(sessionId), (e) => {
34
- if (e instanceof StoreFailure)
35
- return e.storeError;
36
- return { code: 'SESSION_STORE_IO_ERROR', message: e instanceof Error ? e.message : String(e) };
37
- });
23
+ return this.loadImpl(sessionId);
38
24
  }
39
25
  loadValidatedPrefix(sessionId) {
40
- return neverthrow_1.ResultAsync.fromPromise(this.loadValidatedPrefixImpl(sessionId), (e) => {
41
- if (e instanceof StoreFailure)
42
- return e.storeError;
43
- return { code: 'SESSION_STORE_IO_ERROR', message: e instanceof Error ? e.message : String(e) };
44
- });
26
+ return this.loadValidatedPrefixImpl(sessionId);
45
27
  }
46
- async appendImpl(sessionId, plan) {
28
+ appendImpl(sessionId, plan) {
47
29
  const sessionDir = this.dataDir.sessionDir(sessionId);
48
30
  const eventsDir = this.dataDir.sessionEventsDir(sessionId);
49
31
  const manifestPath = this.dataDir.sessionManifestPath(sessionId);
50
- await this.unwrap(this.fs.mkdirp(eventsDir), mapFsToStoreError);
51
- const { manifest, events: existingEvents } = await this.loadTruthOrEmpty(sessionId);
52
- validateManifestContiguityOrThrow(manifest);
53
- const existingByDedupeKey = new Set(existingEvents.map((e) => e.dedupeKey));
54
- const allExist = plan.events.every((e) => existingByDedupeKey.has(e.dedupeKey));
55
- if (allExist) {
56
- return;
57
- }
58
- const anyExist = plan.events.some((e) => existingByDedupeKey.has(e.dedupeKey));
59
- if (anyExist && !allExist) {
60
- throw new StoreFailure({
61
- code: 'SESSION_STORE_INVARIANT_VIOLATION',
62
- message: 'Partial dedupeKey collision detected (some events exist, some do not); this is an invariant violation',
63
- });
64
- }
65
- const expectedFirstEventIndex = nextEventIndexFromManifest(manifest);
66
- validateAppendPlanOrThrow(sessionId, plan, expectedFirstEventIndex);
67
- const first = plan.events[0].eventIndex;
68
- const last = plan.events[plan.events.length - 1].eventIndex;
69
- const segmentRelPath = segmentRelPathFor(first, last);
70
- const segmentPath = `${sessionDir}/${segmentRelPath}`;
71
- const tmpPath = `${segmentPath}.tmp`;
72
- const segmentBytes = concatJsonlRecords(plan.events);
73
- const tmpHandle = await this.unwrap(this.fs.openWriteTruncate(tmpPath), mapFsToStoreError);
74
- await this.unwrap(this.fs.writeAll(tmpHandle.fd, segmentBytes), mapFsToStoreError);
75
- await this.unwrap(this.fs.fsyncFile(tmpHandle.fd), mapFsToStoreError);
76
- await this.unwrap(this.fs.closeFile(tmpHandle.fd), mapFsToStoreError);
77
- await this.unwrap(this.fs.rename(tmpPath, segmentPath), mapFsToStoreError);
78
- await this.unwrap(this.fs.fsyncDir(eventsDir), mapFsToStoreError);
79
- const digest = this.sha256.sha256(segmentBytes);
80
- const segClosed = {
81
- v: 1,
82
- manifestIndex: nextManifestIndex(manifest),
83
- sessionId,
84
- kind: 'segment_closed',
85
- firstEventIndex: first,
86
- lastEventIndex: last,
87
- segmentRelPath,
88
- sha256: digest,
89
- bytes: segmentBytes.length,
90
- };
91
- await this.appendManifestRecords(manifestPath, [segClosed]);
92
- const pins = sortedPins(plan.snapshotPins);
93
- if (pins.length > 0) {
94
- const startIndex = segClosed.manifestIndex + 1;
95
- const records = pins.map((p, i) => ({
96
- v: 1,
97
- manifestIndex: startIndex + i,
98
- sessionId,
99
- kind: 'snapshot_pinned',
100
- eventIndex: p.eventIndex,
101
- snapshotRef: p.snapshotRef,
102
- createdByEventId: p.createdByEventId,
103
- }));
104
- await this.appendManifestRecords(manifestPath, records);
105
- }
106
- }
107
- async loadImpl(sessionId) {
108
- const sessionDir = this.dataDir.sessionDir(sessionId);
109
- const manifest = await this.readManifestOrEmpty(sessionId);
110
- validateManifestContiguityOrThrow(manifest);
111
- validateSegmentClosedContiguityOrThrow(manifest);
112
- const segments = manifest.filter((m) => m.kind === 'segment_closed');
113
- const events = [];
114
- for (const seg of segments) {
115
- const segmentPath = `${sessionDir}/${seg.segmentRelPath}`;
116
- const bytes = await this.fs.readFileBytes(segmentPath).match((v) => v, (e) => {
117
- if (e.code === 'FS_NOT_FOUND') {
118
- throw new StoreFailure({
119
- code: 'SESSION_STORE_CORRUPTION_DETECTED',
120
- location: 'tail',
121
- reason: { code: 'missing_attested_segment', message: `Missing attested segment: ${seg.segmentRelPath}` },
122
- message: `Missing attested segment: ${seg.segmentRelPath}`,
123
- });
124
- }
125
- throw new StoreFailure(mapFsToStoreError(e));
126
- });
127
- const actual = this.sha256.sha256(bytes);
128
- if (actual !== seg.sha256) {
129
- throw new StoreFailure({
130
- code: 'SESSION_STORE_CORRUPTION_DETECTED',
131
- location: 'tail',
132
- reason: { code: 'digest_mismatch', message: `Segment digest mismatch: ${seg.segmentRelPath}` },
133
- message: `Segment digest mismatch: ${seg.segmentRelPath}`,
134
- });
32
+ return this.fs.mkdirp(eventsDir)
33
+ .mapErr(mapFsToStoreError)
34
+ .andThen(() => this.loadTruthOrEmpty(sessionId))
35
+ .andThen(({ manifest, events: existingEvents }) => {
36
+ const contiguityRes = validateManifestContiguity(manifest);
37
+ if (contiguityRes.isErr())
38
+ return (0, neverthrow_1.errAsync)(contiguityRes.error);
39
+ const existingByDedupeKey = new Set(existingEvents.map((e) => e.dedupeKey));
40
+ const allExist = plan.events.every((e) => existingByDedupeKey.has(e.dedupeKey));
41
+ if (allExist) {
42
+ return (0, neverthrow_1.okAsync)(undefined);
135
43
  }
136
- const parsed = parseJsonlLines(bytes, index_js_1.DomainEventV1Schema);
137
- if (parsed.length === 0) {
138
- throw new StoreFailure({
139
- code: 'SESSION_STORE_CORRUPTION_DETECTED',
140
- location: 'tail',
141
- reason: { code: 'non_contiguous_indices', message: `Empty segment referenced by manifest: ${seg.segmentRelPath}` },
142
- message: `Empty segment referenced by manifest: ${seg.segmentRelPath}`,
44
+ const anyExist = plan.events.some((e) => existingByDedupeKey.has(e.dedupeKey));
45
+ if (anyExist && !allExist) {
46
+ return (0, neverthrow_1.errAsync)({
47
+ code: 'SESSION_STORE_INVARIANT_VIOLATION',
48
+ message: 'Partial dedupeKey collision detected (some events exist, some do not); this is an invariant violation',
143
49
  });
144
50
  }
145
- if (parsed[0].eventIndex !== seg.firstEventIndex || parsed[parsed.length - 1].eventIndex !== seg.lastEventIndex) {
146
- throw new StoreFailure({
147
- code: 'SESSION_STORE_CORRUPTION_DETECTED',
148
- location: 'tail',
149
- reason: { code: 'non_contiguous_indices', message: `Segment bounds mismatch: ${seg.segmentRelPath}` },
150
- message: `Segment bounds mismatch: ${seg.segmentRelPath}`,
51
+ const expectedFirstEventIndex = nextEventIndexFromManifest(manifest);
52
+ const planRes = validateAppendPlan(sessionId, plan, expectedFirstEventIndex);
53
+ if (planRes.isErr())
54
+ return (0, neverthrow_1.errAsync)(planRes.error);
55
+ const first = plan.events[0].eventIndex;
56
+ const last = plan.events[plan.events.length - 1].eventIndex;
57
+ const segmentRelPath = segmentRelPathFor(first, last);
58
+ const segmentPath = `${sessionDir}/${segmentRelPath}`;
59
+ const tmpPath = `${segmentPath}.tmp`;
60
+ const segmentBytesRes = concatJsonlRecords(plan.events);
61
+ if (segmentBytesRes.isErr())
62
+ return (0, neverthrow_1.errAsync)(segmentBytesRes.error);
63
+ const segmentBytes = segmentBytesRes.value;
64
+ return this.fs.openWriteTruncate(tmpPath)
65
+ .mapErr(mapFsToStoreError)
66
+ .andThen((tmpHandle) => this.fs.writeAll(tmpHandle.fd, segmentBytes).mapErr(mapFsToStoreError)
67
+ .andThen(() => this.fs.fsyncFile(tmpHandle.fd).mapErr(mapFsToStoreError))
68
+ .andThen(() => this.fs.closeFile(tmpHandle.fd).mapErr(mapFsToStoreError)))
69
+ .andThen(() => this.fs.rename(tmpPath, segmentPath).mapErr(mapFsToStoreError))
70
+ .andThen(() => this.fs.fsyncDir(eventsDir).mapErr(mapFsToStoreError))
71
+ .andThen(() => {
72
+ const digest = this.sha256.sha256(segmentBytes);
73
+ const segClosed = {
74
+ v: 1,
75
+ manifestIndex: nextManifestIndex(manifest),
76
+ sessionId,
77
+ kind: 'segment_closed',
78
+ firstEventIndex: first,
79
+ lastEventIndex: last,
80
+ segmentRelPath,
81
+ sha256: digest,
82
+ bytes: segmentBytes.length,
83
+ };
84
+ return this.appendManifestRecords(manifestPath, [segClosed])
85
+ .andThen(() => {
86
+ const pins = sortedPins(plan.snapshotPins);
87
+ if (pins.length === 0)
88
+ return (0, neverthrow_1.okAsync)(undefined);
89
+ const startIndex = segClosed.manifestIndex + 1;
90
+ const records = pins.map((p, i) => ({
91
+ v: 1,
92
+ manifestIndex: startIndex + i,
93
+ sessionId,
94
+ kind: 'snapshot_pinned',
95
+ eventIndex: p.eventIndex,
96
+ snapshotRef: p.snapshotRef,
97
+ createdByEventId: p.createdByEventId,
98
+ }));
99
+ return this.appendManifestRecords(manifestPath, records);
151
100
  });
152
- }
153
- for (let i = 1; i < parsed.length; i++) {
154
- if (parsed[i].eventIndex !== parsed[i - 1].eventIndex + 1) {
155
- throw new StoreFailure({
156
- code: 'SESSION_STORE_CORRUPTION_DETECTED',
157
- location: 'tail',
158
- reason: { code: 'non_contiguous_indices', message: `Non-contiguous eventIndex inside segment: ${seg.segmentRelPath}` },
159
- message: `Non-contiguous eventIndex inside segment: ${seg.segmentRelPath}`,
160
- });
101
+ });
102
+ });
103
+ }
104
+ loadImpl(sessionId) {
105
+ const sessionDir = this.dataDir.sessionDir(sessionId);
106
+ return this.readManifestOrEmpty(sessionId)
107
+ .andThen((manifest) => {
108
+ const contRes = validateManifestContiguity(manifest);
109
+ if (contRes.isErr())
110
+ return (0, neverthrow_1.errAsync)(contRes.error);
111
+ const segRes = validateSegmentClosedContiguity(manifest);
112
+ if (segRes.isErr())
113
+ return (0, neverthrow_1.errAsync)(segRes.error);
114
+ const segments = manifest.filter((m) => m.kind === 'segment_closed');
115
+ const loadSegments = (segs) => {
116
+ if (segs.length === 0)
117
+ return (0, neverthrow_1.okAsync)([]);
118
+ const [head, ...tail] = segs;
119
+ const segmentPath = `${sessionDir}/${head.segmentRelPath}`;
120
+ return this.fs.readFileBytes(segmentPath)
121
+ .mapErr((e) => {
122
+ if (e.code === 'FS_NOT_FOUND') {
123
+ return {
124
+ code: 'SESSION_STORE_CORRUPTION_DETECTED',
125
+ location: 'tail',
126
+ reason: { code: 'missing_attested_segment', message: `Missing attested segment: ${head.segmentRelPath}` },
127
+ message: `Missing attested segment: ${head.segmentRelPath}`,
128
+ };
129
+ }
130
+ return mapFsToStoreError(e);
131
+ })
132
+ .andThen((bytes) => {
133
+ const actual = this.sha256.sha256(bytes);
134
+ if (actual !== head.sha256) {
135
+ return (0, neverthrow_1.errAsync)({
136
+ code: 'SESSION_STORE_CORRUPTION_DETECTED',
137
+ location: 'tail',
138
+ reason: { code: 'digest_mismatch', message: `Segment digest mismatch: ${head.segmentRelPath}` },
139
+ message: `Segment digest mismatch: ${head.segmentRelPath}`,
140
+ });
141
+ }
142
+ const parsedRes = parseJsonlLines(bytes, index_js_1.DomainEventV1Schema);
143
+ if (parsedRes.isErr())
144
+ return (0, neverthrow_1.errAsync)(parsedRes.error);
145
+ const parsed = parsedRes.value;
146
+ if (parsed.length === 0) {
147
+ return (0, neverthrow_1.errAsync)({
148
+ code: 'SESSION_STORE_CORRUPTION_DETECTED',
149
+ location: 'tail',
150
+ reason: { code: 'non_contiguous_indices', message: `Empty segment referenced by manifest: ${head.segmentRelPath}` },
151
+ message: `Empty segment referenced by manifest: ${head.segmentRelPath}`,
152
+ });
153
+ }
154
+ if (parsed[0].eventIndex !== head.firstEventIndex || parsed[parsed.length - 1].eventIndex !== head.lastEventIndex) {
155
+ return (0, neverthrow_1.errAsync)({
156
+ code: 'SESSION_STORE_CORRUPTION_DETECTED',
157
+ location: 'tail',
158
+ reason: { code: 'non_contiguous_indices', message: `Segment bounds mismatch: ${head.segmentRelPath}` },
159
+ message: `Segment bounds mismatch: ${head.segmentRelPath}`,
160
+ });
161
+ }
162
+ for (let i = 1; i < parsed.length; i++) {
163
+ if (parsed[i].eventIndex !== parsed[i - 1].eventIndex + 1) {
164
+ return (0, neverthrow_1.errAsync)({
165
+ code: 'SESSION_STORE_CORRUPTION_DETECTED',
166
+ location: 'tail',
167
+ reason: { code: 'non_contiguous_indices', message: `Non-contiguous eventIndex inside segment: ${head.segmentRelPath}` },
168
+ message: `Non-contiguous eventIndex inside segment: ${head.segmentRelPath}`,
169
+ });
170
+ }
171
+ }
172
+ return (0, neverthrow_1.okAsync)(parsed);
173
+ })
174
+ .andThen((events) => loadSegments(tail).map((rest) => [...events, ...rest]));
175
+ };
176
+ return loadSegments(segments).andThen((events) => {
177
+ const expectedPins = extractSnapshotPinsFromEvents(events);
178
+ const actualPins = new Set(manifest
179
+ .filter((m) => m.kind === 'snapshot_pinned')
180
+ .map((p) => `${p.eventIndex}:${p.createdByEventId}:${p.snapshotRef}`));
181
+ for (const ep of expectedPins) {
182
+ const key = `${ep.eventIndex}:${ep.createdByEventId}:${ep.snapshotRef}`;
183
+ if (!actualPins.has(key)) {
184
+ return (0, neverthrow_1.errAsync)({
185
+ code: 'SESSION_STORE_CORRUPTION_DETECTED',
186
+ location: 'tail',
187
+ reason: { code: 'missing_attested_segment', message: `Missing snapshot_pinned for introduced snapshotRef: ${key}` },
188
+ message: `Missing snapshot_pinned for introduced snapshotRef: ${key}`,
189
+ });
190
+ }
161
191
  }
162
- }
163
- events.push(...parsed);
164
- }
165
- const expectedPins = extractSnapshotPinsFromEvents(events);
166
- const actualPins = new Set(manifest
167
- .filter((m) => m.kind === 'snapshot_pinned')
168
- .map((p) => `${p.eventIndex}:${p.createdByEventId}:${p.snapshotRef}`));
169
- for (const ep of expectedPins) {
170
- const key = `${ep.eventIndex}:${ep.createdByEventId}:${ep.snapshotRef}`;
171
- if (!actualPins.has(key)) {
172
- throw new StoreFailure({
173
- code: 'SESSION_STORE_CORRUPTION_DETECTED',
174
- location: 'tail',
175
- reason: { code: 'missing_attested_segment', message: `Missing snapshot_pinned for introduced snapshotRef: ${key}` },
176
- message: `Missing snapshot_pinned for introduced snapshotRef: ${key}`,
177
- });
178
- }
179
- }
180
- return { manifest, events };
192
+ return (0, neverthrow_1.okAsync)({ manifest, events });
193
+ });
194
+ });
181
195
  }
182
- async readManifestOrEmpty(sessionId) {
196
+ readManifestOrEmpty(sessionId) {
183
197
  const manifestPath = this.dataDir.sessionManifestPath(sessionId);
184
- const raw = await this.fs.readFileUtf8(manifestPath).match((v) => v, (e) => {
185
- if (e.code === 'FS_NOT_FOUND')
186
- return '';
187
- throw new StoreFailure(mapFsToStoreError(e));
198
+ return this.fs
199
+ .readFileUtf8(manifestPath)
200
+ .orElse((e) => (e.code === 'FS_NOT_FOUND' ? (0, neverthrow_1.okAsync)('') : (0, neverthrow_1.errAsync)(mapFsToStoreError(e))))
201
+ .andThen((raw) => {
202
+ if (raw.trim() === '')
203
+ return (0, neverthrow_1.okAsync)([]);
204
+ const parsed = parseJsonlText(raw, index_js_1.ManifestRecordV1Schema);
205
+ return parsed.isErr() ? (0, neverthrow_1.errAsync)(parsed.error) : (0, neverthrow_1.okAsync)(parsed.value);
188
206
  });
189
- if (raw.trim() === '')
190
- return [];
191
- return parseJsonlText(raw, index_js_1.ManifestRecordV1Schema);
192
207
  }
193
- async loadValidatedPrefixImpl(sessionId) {
208
+ loadValidatedPrefixImpl(sessionId) {
194
209
  const sessionDir = this.dataDir.sessionDir(sessionId);
195
210
  const manifestPath = this.dataDir.sessionManifestPath(sessionId);
196
- const raw = await this.fs.readFileUtf8(manifestPath).match((v) => v, (e) => {
197
- if (e.code === 'FS_NOT_FOUND')
198
- return '';
199
- throw new StoreFailure(mapFsToStoreError(e));
200
- });
201
- if (raw.trim() === '')
202
- return { truth: { manifest: [], events: [] }, isComplete: true, tailReason: null };
203
- const lines = raw.split('\n').filter((l) => l.trim() !== '');
204
- const manifest = [];
205
- let isComplete = true;
206
- let tailReason = null;
207
- for (let i = 0; i < lines.length; i++) {
208
- const line = lines[i];
209
- let parsed;
210
- try {
211
- parsed = JSON.parse(line);
211
+ return this.fs
212
+ .readFileUtf8(manifestPath)
213
+ .orElse((e) => (e.code === 'FS_NOT_FOUND' ? (0, neverthrow_1.okAsync)('') : (0, neverthrow_1.errAsync)(mapFsToStoreError(e))))
214
+ .andThen((raw) => {
215
+ if (raw.trim() === '') {
216
+ return (0, neverthrow_1.okAsync)({ truth: { manifest: [], events: [] }, isComplete: true, tailReason: null });
212
217
  }
213
- catch {
214
- isComplete = false;
215
- tailReason = { code: 'non_contiguous_indices', message: 'Invalid JSON in manifest (corrupt tail)' };
216
- break;
217
- }
218
- const validated = index_js_1.ManifestRecordV1Schema.safeParse(parsed);
219
- if (!validated.success) {
220
- isComplete = false;
221
- tailReason = { code: 'unknown_schema_version', message: 'Unknown manifest schema version (corrupt tail)' };
222
- break;
223
- }
224
- if (validated.data.manifestIndex !== i) {
225
- isComplete = false;
226
- tailReason = { code: 'non_contiguous_indices', message: 'Non-contiguous manifestIndex in prefix (corrupt tail)' };
227
- break;
228
- }
229
- manifest.push(validated.data);
230
- }
231
- if (manifest.length === 0) {
232
- throw new StoreFailure({
233
- code: 'SESSION_STORE_CORRUPTION_DETECTED',
234
- location: 'head',
235
- reason: { code: 'non_contiguous_indices', message: 'No validated manifest prefix' },
236
- message: 'No validated manifest prefix',
237
- });
238
- }
239
- const segments = manifest.filter((m) => m.kind === 'segment_closed');
240
- const events = [];
241
- for (const seg of segments) {
242
- const segmentPath = `${sessionDir}/${seg.segmentRelPath}`;
243
- const bytes = await this.fs.readFileBytes(segmentPath).match((v) => v, (e) => {
244
- if (e.code === 'FS_NOT_FOUND')
245
- return null;
246
- throw new StoreFailure(mapFsToStoreError(e));
247
- });
248
- if (bytes === null) {
249
- isComplete = false;
250
- tailReason ?? (tailReason = { code: 'missing_attested_segment', message: `Missing attested segment: ${seg.segmentRelPath}` });
251
- break;
252
- }
253
- const actual = this.sha256.sha256(bytes);
254
- if (actual !== seg.sha256) {
255
- isComplete = false;
256
- tailReason ?? (tailReason = { code: 'digest_mismatch', message: `Segment digest mismatch: ${seg.segmentRelPath}` });
257
- break;
258
- }
259
- const parsed = parseJsonlLines(bytes, index_js_1.DomainEventV1Schema);
260
- if (parsed.length === 0) {
261
- isComplete = false;
262
- tailReason ?? (tailReason = { code: 'non_contiguous_indices', message: `Empty segment referenced by manifest: ${seg.segmentRelPath}` });
263
- break;
264
- }
265
- if (parsed[0].eventIndex !== seg.firstEventIndex || parsed[parsed.length - 1].eventIndex !== seg.lastEventIndex) {
266
- isComplete = false;
267
- tailReason ?? (tailReason = { code: 'non_contiguous_indices', message: `Segment bounds mismatch: ${seg.segmentRelPath}` });
268
- break;
269
- }
270
- for (let i = 1; i < parsed.length; i++) {
271
- if (parsed[i].eventIndex !== parsed[i - 1].eventIndex + 1) {
218
+ const lines = raw.split('\n').filter((l) => l.trim() !== '');
219
+ const manifest = [];
220
+ let isComplete = true;
221
+ let tailReason = null;
222
+ for (let i = 0; i < lines.length; i++) {
223
+ const line = lines[i];
224
+ let parsed;
225
+ try {
226
+ parsed = JSON.parse(line);
227
+ }
228
+ catch {
229
+ isComplete = false;
230
+ tailReason ?? (tailReason = { code: 'non_contiguous_indices', message: 'Invalid JSON in manifest (corrupt tail)' });
231
+ break;
232
+ }
233
+ const validated = index_js_1.ManifestRecordV1Schema.safeParse(parsed);
234
+ if (!validated.success) {
235
+ isComplete = false;
236
+ tailReason ?? (tailReason = { code: 'unknown_schema_version', message: 'Unknown manifest schema version (corrupt tail)' });
237
+ break;
238
+ }
239
+ if (validated.data.manifestIndex !== i) {
272
240
  isComplete = false;
273
- tailReason ?? (tailReason = { code: 'non_contiguous_indices', message: `Non-contiguous eventIndex inside segment: ${seg.segmentRelPath}` });
241
+ tailReason ?? (tailReason = { code: 'non_contiguous_indices', message: 'Non-contiguous manifestIndex in prefix (corrupt tail)' });
274
242
  break;
275
243
  }
244
+ manifest.push(validated.data);
276
245
  }
277
- events.push(...parsed);
278
- }
279
- return { truth: { manifest, events }, isComplete, tailReason };
280
- }
281
- async loadTruthOrEmpty(sessionId) {
282
- const manifest = await this.readManifestOrEmpty(sessionId);
283
- if (manifest.length === 0)
284
- return { manifest: [], events: [] };
285
- const segments = manifest.filter((m) => m.kind === 'segment_closed');
286
- const sessionDir = this.dataDir.sessionDir(sessionId);
287
- const events = [];
288
- for (const seg of segments) {
289
- const segmentPath = `${sessionDir}/${seg.segmentRelPath}`;
290
- const bytes = await this.unwrap(this.fs.readFileBytes(segmentPath), mapFsToStoreError);
291
- const parsed = parseJsonlLines(bytes, index_js_1.DomainEventV1Schema);
292
- events.push(...parsed);
293
- }
294
- return { manifest, events };
246
+ if (manifest.length === 0) {
247
+ return (0, neverthrow_1.errAsync)({
248
+ code: 'SESSION_STORE_CORRUPTION_DETECTED',
249
+ location: 'head',
250
+ reason: { code: 'non_contiguous_indices', message: 'No validated manifest prefix' },
251
+ message: 'No validated manifest prefix',
252
+ });
253
+ }
254
+ const segments = manifest.filter((m) => m.kind === 'segment_closed');
255
+ const initial = { events: [], isComplete, tailReason, done: false };
256
+ const processSegment = (seg, state) => {
257
+ if (state.done)
258
+ return (0, neverthrow_1.okAsync)(state);
259
+ const segmentPath = `${sessionDir}/${seg.segmentRelPath}`;
260
+ return this.fs
261
+ .readFileBytes(segmentPath)
262
+ .map((bytes) => ({ kind: 'present', bytes }))
263
+ .orElse((e) => (e.code === 'FS_NOT_FOUND' ? (0, neverthrow_1.okAsync)({ kind: 'missing' }) : (0, neverthrow_1.errAsync)(mapFsToStoreError(e))))
264
+ .andThen((res) => {
265
+ if (res.kind === 'missing') {
266
+ return (0, neverthrow_1.okAsync)({
267
+ ...state,
268
+ isComplete: false,
269
+ tailReason: state.tailReason ?? { code: 'missing_attested_segment', message: `Missing attested segment: ${seg.segmentRelPath}` },
270
+ done: true,
271
+ });
272
+ }
273
+ const bytes = res.bytes;
274
+ const actual = this.sha256.sha256(bytes);
275
+ if (actual !== seg.sha256) {
276
+ return (0, neverthrow_1.okAsync)({
277
+ ...state,
278
+ isComplete: false,
279
+ tailReason: state.tailReason ?? { code: 'digest_mismatch', message: `Segment digest mismatch: ${seg.segmentRelPath}` },
280
+ done: true,
281
+ });
282
+ }
283
+ const parsedRes = parseJsonlLines(bytes, index_js_1.DomainEventV1Schema);
284
+ if (parsedRes.isErr()) {
285
+ return (0, neverthrow_1.okAsync)({
286
+ ...state,
287
+ isComplete: false,
288
+ tailReason: state.tailReason ?? { code: 'non_contiguous_indices', message: `Invalid JSONL inside segment: ${seg.segmentRelPath}` },
289
+ done: true,
290
+ });
291
+ }
292
+ const parsed = parsedRes.value;
293
+ if (parsed.length === 0) {
294
+ return (0, neverthrow_1.okAsync)({
295
+ ...state,
296
+ isComplete: false,
297
+ tailReason: state.tailReason ?? { code: 'non_contiguous_indices', message: `Empty segment referenced by manifest: ${seg.segmentRelPath}` },
298
+ done: true,
299
+ });
300
+ }
301
+ if (parsed[0].eventIndex !== seg.firstEventIndex || parsed[parsed.length - 1].eventIndex !== seg.lastEventIndex) {
302
+ return (0, neverthrow_1.okAsync)({
303
+ ...state,
304
+ isComplete: false,
305
+ tailReason: state.tailReason ?? { code: 'non_contiguous_indices', message: `Segment bounds mismatch: ${seg.segmentRelPath}` },
306
+ done: true,
307
+ });
308
+ }
309
+ for (let i = 1; i < parsed.length; i++) {
310
+ if (parsed[i].eventIndex !== parsed[i - 1].eventIndex + 1) {
311
+ return (0, neverthrow_1.okAsync)({
312
+ ...state,
313
+ isComplete: false,
314
+ tailReason: state.tailReason ?? { code: 'non_contiguous_indices', message: `Non-contiguous eventIndex inside segment: ${seg.segmentRelPath}` },
315
+ done: true,
316
+ });
317
+ }
318
+ }
319
+ return (0, neverthrow_1.okAsync)({
320
+ ...state,
321
+ events: [...state.events, ...parsed],
322
+ });
323
+ });
324
+ };
325
+ return segments
326
+ .reduce((acc, seg) => acc.andThen((s) => processSegment(seg, s)), (0, neverthrow_1.okAsync)(initial))
327
+ .map((final) => ({
328
+ truth: { manifest, events: final.events },
329
+ isComplete: final.isComplete,
330
+ tailReason: final.tailReason,
331
+ }));
332
+ });
295
333
  }
296
- async appendManifestRecords(manifestPath, records) {
297
- const handle = await this.unwrap(this.fs.openAppend(manifestPath), mapFsToStoreError);
298
- const bytes = concatJsonlRecords(records);
299
- await this.unwrap(this.fs.writeAll(handle.fd, bytes), mapFsToStoreError);
300
- await this.unwrap(this.fs.fsyncFile(handle.fd), mapFsToStoreError);
301
- await this.unwrap(this.fs.closeFile(handle.fd), mapFsToStoreError);
334
+ loadTruthOrEmpty(sessionId) {
335
+ return this.readManifestOrEmpty(sessionId)
336
+ .andThen((manifest) => {
337
+ if (manifest.length === 0)
338
+ return (0, neverthrow_1.okAsync)({ manifest: [], events: [] });
339
+ const segments = manifest.filter((m) => m.kind === 'segment_closed');
340
+ const sessionDir = this.dataDir.sessionDir(sessionId);
341
+ const loadSegments = (segs) => {
342
+ if (segs.length === 0)
343
+ return (0, neverthrow_1.okAsync)([]);
344
+ const [head, ...tail] = segs;
345
+ const segmentPath = `${sessionDir}/${head.segmentRelPath}`;
346
+ return this.fs.readFileBytes(segmentPath)
347
+ .mapErr(mapFsToStoreError)
348
+ .andThen((bytes) => {
349
+ const parsedRes = parseJsonlLines(bytes, index_js_1.DomainEventV1Schema);
350
+ if (parsedRes.isErr())
351
+ return (0, neverthrow_1.errAsync)(parsedRes.error);
352
+ return (0, neverthrow_1.okAsync)(parsedRes.value);
353
+ })
354
+ .andThen((events) => loadSegments(tail).map((rest) => [...events, ...rest]));
355
+ };
356
+ return loadSegments(segments).map((events) => ({ manifest, events }));
357
+ });
302
358
  }
303
- async unwrap(ra, map) {
304
- return ra.match((v) => v, (e) => {
305
- throw new StoreFailure(map(e));
359
+ appendManifestRecords(manifestPath, records) {
360
+ return this.fs.openAppend(manifestPath)
361
+ .mapErr(mapFsToStoreError)
362
+ .andThen((handle) => {
363
+ const bytesRes = concatJsonlRecords(records);
364
+ if (bytesRes.isErr())
365
+ return (0, neverthrow_1.errAsync)(bytesRes.error);
366
+ const bytes = bytesRes.value;
367
+ return this.fs.writeAll(handle.fd, bytes).mapErr(mapFsToStoreError)
368
+ .andThen(() => this.fs.fsyncFile(handle.fd).mapErr(mapFsToStoreError))
369
+ .andThen(() => this.fs.closeFile(handle.fd).mapErr(mapFsToStoreError));
306
370
  });
307
371
  }
308
372
  }
@@ -321,11 +385,11 @@ function nextEventIndexFromManifest(manifest) {
321
385
  return 0;
322
386
  return segments[segments.length - 1].lastEventIndex + 1;
323
387
  }
324
- function validateManifestContiguityOrThrow(manifest) {
388
+ function validateManifestContiguity(manifest) {
325
389
  for (let i = 0; i < manifest.length; i++) {
326
390
  const expected = i;
327
391
  if (manifest[i].manifestIndex !== expected) {
328
- throw new StoreFailure({
392
+ return (0, neverthrow_1.err)({
329
393
  code: 'SESSION_STORE_CORRUPTION_DETECTED',
330
394
  location: i === 0 ? 'head' : 'tail',
331
395
  reason: {
@@ -336,14 +400,15 @@ function validateManifestContiguityOrThrow(manifest) {
336
400
  });
337
401
  }
338
402
  }
403
+ return (0, neverthrow_1.ok)(undefined);
339
404
  }
340
- function validateSegmentClosedContiguityOrThrow(manifest) {
405
+ function validateSegmentClosedContiguity(manifest) {
341
406
  const segments = manifest.filter((m) => m.kind === 'segment_closed');
342
407
  for (let i = 1; i < segments.length; i++) {
343
408
  const prev = segments[i - 1];
344
409
  const cur = segments[i];
345
410
  if (cur.firstEventIndex !== prev.lastEventIndex + 1) {
346
- throw new StoreFailure({
411
+ return (0, neverthrow_1.err)({
347
412
  code: 'SESSION_STORE_CORRUPTION_DETECTED',
348
413
  location: 'tail',
349
414
  reason: {
@@ -354,14 +419,15 @@ function validateSegmentClosedContiguityOrThrow(manifest) {
354
419
  });
355
420
  }
356
421
  }
422
+ return (0, neverthrow_1.ok)(undefined);
357
423
  }
358
- function validateAppendPlanOrThrow(sessionId, plan, expectedFirstEventIndex) {
424
+ function validateAppendPlan(sessionId, plan, expectedFirstEventIndex) {
359
425
  if (plan.events.length === 0) {
360
- throw new StoreFailure({ code: 'SESSION_STORE_INVARIANT_VIOLATION', message: 'AppendPlan.events must be non-empty' });
426
+ return (0, neverthrow_1.err)({ code: 'SESSION_STORE_INVARIANT_VIOLATION', message: 'AppendPlan.events must be non-empty' });
361
427
  }
362
428
  const first = plan.events[0];
363
429
  if (first.eventIndex !== expectedFirstEventIndex) {
364
- throw new StoreFailure({
430
+ return (0, neverthrow_1.err)({
365
431
  code: 'SESSION_STORE_INVARIANT_VIOLATION',
366
432
  message: `AppendPlan.eventIndex must start at ${expectedFirstEventIndex} (got ${first.eventIndex})`,
367
433
  });
@@ -369,16 +435,16 @@ function validateAppendPlanOrThrow(sessionId, plan, expectedFirstEventIndex) {
369
435
  for (let i = 0; i < plan.events.length; i++) {
370
436
  const e = index_js_1.DomainEventV1Schema.safeParse(plan.events[i]);
371
437
  if (!e.success) {
372
- throw new StoreFailure({ code: 'SESSION_STORE_INVARIANT_VIOLATION', message: `Invalid domain event at index ${i}` });
438
+ return (0, neverthrow_1.err)({ code: 'SESSION_STORE_INVARIANT_VIOLATION', message: `Invalid domain event at index ${i}` });
373
439
  }
374
440
  if (e.data.sessionId !== sessionId) {
375
- throw new StoreFailure({
441
+ return (0, neverthrow_1.err)({
376
442
  code: 'SESSION_STORE_INVARIANT_VIOLATION',
377
443
  message: `Domain event sessionId mismatch at index ${i}`,
378
444
  });
379
445
  }
380
446
  if (i > 0 && plan.events[i].eventIndex !== plan.events[i - 1].eventIndex + 1) {
381
- throw new StoreFailure({
447
+ return (0, neverthrow_1.err)({
382
448
  code: 'SESSION_STORE_INVARIANT_VIOLATION',
383
449
  message: `Non-contiguous eventIndex in AppendPlan at index ${i}`,
384
450
  });
@@ -387,12 +453,13 @@ function validateAppendPlanOrThrow(sessionId, plan, expectedFirstEventIndex) {
387
453
  const pins = sortedPins(plan.snapshotPins);
388
454
  for (const p of pins) {
389
455
  if (p.eventIndex < first.eventIndex || p.eventIndex > plan.events[plan.events.length - 1].eventIndex) {
390
- throw new StoreFailure({
456
+ return (0, neverthrow_1.err)({
391
457
  code: 'SESSION_STORE_INVARIANT_VIOLATION',
392
458
  message: `SnapshotPin.eventIndex must refer to an event in the appended segment`,
393
459
  });
394
460
  }
395
461
  }
462
+ return (0, neverthrow_1.ok)(undefined);
396
463
  }
397
464
  function sortedPins(pins) {
398
465
  return [...pins].sort((a, b) => {
@@ -414,11 +481,12 @@ function concatJsonlRecords(records) {
414
481
  const parts = [];
415
482
  let total = 0;
416
483
  for (const r of records) {
417
- const encoded = (0, jsonl_js_1.toJsonlLineBytes)(r).match((v) => v, (e) => {
418
- throw new StoreFailure({ code: 'SESSION_STORE_INVARIANT_VIOLATION', message: e.message });
419
- });
420
- parts.push(encoded);
421
- total += encoded.length;
484
+ const encoded = (0, jsonl_js_1.toJsonlLineBytes)(r);
485
+ if (encoded.isErr())
486
+ return (0, neverthrow_1.err)({ code: 'SESSION_STORE_INVARIANT_VIOLATION', message: encoded.error.message });
487
+ const val = encoded.value;
488
+ parts.push(val);
489
+ total += val.length;
422
490
  }
423
491
  const out = new Uint8Array(total);
424
492
  let offset = 0;
@@ -426,7 +494,7 @@ function concatJsonlRecords(records) {
426
494
  out.set(p, offset);
427
495
  offset += p.length;
428
496
  }
429
- return out;
497
+ return (0, neverthrow_1.ok)(out);
430
498
  }
431
499
  function parseJsonlText(text, schema) {
432
500
  const lines = text.split('\n').filter((l) => l.trim() !== '');
@@ -438,7 +506,7 @@ function parseJsonlText(text, schema) {
438
506
  parsed = JSON.parse(raw);
439
507
  }
440
508
  catch {
441
- throw new StoreFailure({
509
+ return (0, neverthrow_1.err)({
442
510
  code: 'SESSION_STORE_CORRUPTION_DETECTED',
443
511
  location: i === 0 ? 'head' : 'tail',
444
512
  reason: { code: 'non_contiguous_indices', message: `Invalid JSONL at line ${i}` },
@@ -447,7 +515,7 @@ function parseJsonlText(text, schema) {
447
515
  }
448
516
  const validated = schema.safeParse(parsed);
449
517
  if (!validated.success) {
450
- throw new StoreFailure({
518
+ return (0, neverthrow_1.err)({
451
519
  code: 'SESSION_STORE_CORRUPTION_DETECTED',
452
520
  location: i === 0 ? 'head' : 'tail',
453
521
  reason: { code: 'unknown_schema_version', message: `Invalid record at line ${i}` },
@@ -456,7 +524,7 @@ function parseJsonlText(text, schema) {
456
524
  }
457
525
  out.push(validated.data);
458
526
  }
459
- return out;
527
+ return (0, neverthrow_1.ok)(out);
460
528
  }
461
529
  function parseJsonlLines(bytes, schema) {
462
530
  const text = new TextDecoder().decode(bytes);