fraim-hub 2.0.317 → 2.0.319

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 (32) hide show
  1. package/dist/src/ai-hub/atomic-json-file.js +20 -2
  2. package/dist/src/ai-hub/catalog.js +19 -2
  3. package/dist/src/ai-hub/conversation-history.js +363 -0
  4. package/dist/src/ai-hub/conversation-search-index.js +297 -171
  5. package/dist/src/ai-hub/conversation-search-projection.js +21 -38
  6. package/dist/src/ai-hub/conversation-search.js +68 -94
  7. package/dist/src/ai-hub/conversation-store-lock.js +35 -8
  8. package/dist/src/ai-hub/conversation-store.js +888 -120
  9. package/dist/src/ai-hub/conversation-ui-state.js +90 -0
  10. package/dist/src/ai-hub/custom-employees.js +1 -0
  11. package/dist/src/ai-hub/history-migration.js +517 -0
  12. package/dist/src/ai-hub/history-policy.js +45 -0
  13. package/dist/src/ai-hub/history-writer-compatibility.js +79 -0
  14. package/dist/src/ai-hub/host-output-stream.js +225 -0
  15. package/dist/src/ai-hub/hosts.js +153 -74
  16. package/dist/src/ai-hub/hub-app-materializer.js +2 -8
  17. package/dist/src/ai-hub/hub-runtime-file.js +1 -1
  18. package/dist/src/ai-hub/preferences.js +8 -2
  19. package/dist/src/ai-hub/server.js +1272 -302
  20. package/dist/src/ai-hub/stale-bucket-sweep.js +25 -4
  21. package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +11 -1
  22. package/dist/src/cli/utils/win32-cmd-spawn.js +38 -0
  23. package/dist/src/config/persona-capability-bundles.js +1 -1
  24. package/dist/src/core/quality-evidence.js +10 -0
  25. package/dist/src/core/resolve-phase-edge.js +20 -2
  26. package/dist/src/first-run/session-service.js +21 -6
  27. package/dist/src/local-mcp-server/learning-context-builder.js +8 -5
  28. package/package.json +5 -3
  29. package/public/ai-hub/index.html +133 -160
  30. package/public/ai-hub/review.css +2 -48
  31. package/public/ai-hub/script.js +1647 -1306
  32. package/public/ai-hub/styles.css +334 -65
@@ -14,8 +14,8 @@ const path_1 = __importDefault(require("path"));
14
14
  // same write-temp-then-rename dance, and the same Windows-specific retry around it. Keeping two
15
15
  // copies meant a future fix to the retry behaviour would land in one and silently not the other,
16
16
  // which is exactly the drift the store's #820 durability work exists to prevent.
17
- const RENAME_ATTEMPTS = 25;
18
- const RENAME_RETRY_DELAY_MS = 10;
17
+ const RENAME_ATTEMPTS = process.platform === 'win32' ? 250 : 25;
18
+ const RENAME_RETRY_DELAY_MS = process.platform === 'win32' ? 20 : 10;
19
19
  let tempSeq = 0;
20
20
  /**
21
21
  * Rename, retrying the Windows contention codes.
@@ -66,7 +66,25 @@ function writeFileAtomic(filePath, data, encoding) {
66
66
  fs_1.default.writeFileSync(tempPath, data);
67
67
  }
68
68
  try {
69
+ const fd = fs_1.default.openSync(tempPath, 'r+');
70
+ try {
71
+ fs_1.default.fsyncSync(fd);
72
+ }
73
+ finally {
74
+ fs_1.default.closeSync(fd);
75
+ }
69
76
  renameWithRetry(tempPath, filePath);
77
+ // POSIX supports directory fsync for rename durability. Windows does not expose
78
+ // a directory flush through Node; file data is flushed, rename is atomic.
79
+ if (process.platform !== 'win32') {
80
+ const directory = fs_1.default.openSync(path_1.default.dirname(filePath), 'r');
81
+ try {
82
+ fs_1.default.fsyncSync(directory);
83
+ }
84
+ finally {
85
+ fs_1.default.closeSync(directory);
86
+ }
87
+ }
70
88
  }
71
89
  catch (error) {
72
90
  // Do not leave the temp file behind for a scanner to trip over.
@@ -492,7 +492,11 @@ function loadJobPhases(jobId, projectPath, discriminant = 'feature') {
492
492
  const phaseDef = fm.phases[cursor];
493
493
  if (!phaseDef)
494
494
  break;
495
- cursor = (0, resolve_phase_edge_1.resolvePhaseEdge)(fm.phases[cursor]?.onSuccess, discriminant);
495
+ if ((0, resolve_phase_edge_1.isRecurrencePhase)(phaseDef)) {
496
+ ordered.splice(ordered.length - 1, 1, phaseDef.setup.phase, phaseDef.recur.phase, phaseDef.done.phase);
497
+ break;
498
+ }
499
+ cursor = (0, resolve_phase_edge_1.resolvePhaseEdge)(phaseDef.onSuccess, discriminant);
496
500
  }
497
501
  const labels = fm.phaseLabels || {};
498
502
  return ordered.map((id) => ({ id, label: friendlyPhaseLabel(id, labels[id]) }));
@@ -513,6 +517,8 @@ function resolveJobPhaseTransition(jobId, projectPath, phaseId, outcome, discrim
513
517
  const phaseDef = fm.phases[phaseId];
514
518
  if (!phaseDef)
515
519
  return null;
520
+ if ((0, resolve_phase_edge_1.isRecurrencePhase)(phaseDef))
521
+ return outcome === 'complete' ? phaseDef.setup.phase : null;
516
522
  const edge = outcome === 'complete' ? phaseDef.onSuccess : phaseDef.onFailure;
517
523
  return (0, resolve_phase_edge_1.resolvePhaseEdge)(edge, discriminant);
518
524
  }
@@ -525,7 +531,18 @@ function loadAllJobPhaseIds(jobId, projectPath) {
525
531
  const phases = loadJobPhasesFromSteps(declaredPath);
526
532
  return new Set(phases.map((p) => p.id));
527
533
  }
528
- return new Set(Object.keys(fm.phases));
534
+ const ids = new Set();
535
+ for (const [phaseId, phase] of Object.entries(fm.phases)) {
536
+ if ((0, resolve_phase_edge_1.isRecurrencePhase)(phase)) {
537
+ ids.add(phase.setup.phase);
538
+ ids.add(phase.recur.phase);
539
+ ids.add(phase.done.phase);
540
+ }
541
+ else {
542
+ ids.add(phaseId);
543
+ }
544
+ }
545
+ return ids;
529
546
  }
530
547
  // Issue #347 — public exposure of the friendly-label rule so callers
531
548
  // outside catalog.ts can render labels for phases that exist in the
@@ -0,0 +1,363 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.historyDirectory = exports.historyHash = void 0;
7
+ exports.historySequenceRange = historySequenceRange;
8
+ exports.historyOf = historyOf;
9
+ exports.jsonBytes = jsonBytes;
10
+ exports.historyMessagesNewest = historyMessagesNewest;
11
+ exports.prepareConversationHead = prepareConversationHead;
12
+ exports.messagePage = messagePage;
13
+ const crypto_1 = __importDefault(require("crypto"));
14
+ const fs_1 = __importDefault(require("fs"));
15
+ const path_1 = __importDefault(require("path"));
16
+ const atomic_json_file_1 = require("./atomic-json-file");
17
+ const history_policy_1 = require("./history-policy");
18
+ const conversation_ui_state_1 = require("./conversation-ui-state");
19
+ const REFERENCE_WINDOW = 100;
20
+ function referenceDirectory(bucket, id) { return path_1.default.join((0, exports.historyDirectory)(bucket, id), 'pages'); }
21
+ function readReferencePage(bucket, id, window, revision) {
22
+ const directory = referenceDirectory(bucket, id);
23
+ try {
24
+ let hash = JSON.parse(fs_1.default.readFileSync(path_1.default.join(directory, `${window}.head.json`), 'utf8')).hash;
25
+ for (let hops = 0; hash && hops < 10000; hops++) {
26
+ if (!/^[a-f0-9]{64}$/.test(hash))
27
+ return null;
28
+ const file = path_1.default.join(directory, `${hash}.json`);
29
+ if (fs_1.default.statSync(file).size > 32 * 1024)
30
+ return null;
31
+ const text = fs_1.default.readFileSync(file, 'utf8');
32
+ if ((0, exports.historyHash)(text) !== hash)
33
+ return null;
34
+ const page = JSON.parse(text);
35
+ if (page.revision <= revision) {
36
+ const commit = JSON.parse(fs_1.default.readFileSync(path_1.default.join((0, exports.historyDirectory)(bucket, id), 'commits', `${page.revision}.json`), 'utf8'));
37
+ if (commit.id === page.commitId)
38
+ return { hash, page };
39
+ }
40
+ hash = page.previous;
41
+ }
42
+ }
43
+ catch { /* derived page missing/corrupt: caller reads the authoritative chunks */ }
44
+ return null;
45
+ }
46
+ function indexChunk(bucket, id, hash, messages, revision, commitId) {
47
+ const windows = new Map();
48
+ messages.forEach((message, index) => {
49
+ const window = Math.floor((message.sequence - 1) / REFERENCE_WINDOW);
50
+ if (!windows.has(window))
51
+ windows.set(window, []);
52
+ windows.get(window).push([message.sequence, { chunk: hash, index }]);
53
+ });
54
+ for (const [window, additions] of windows) {
55
+ const previous = readReferencePage(bucket, id, window, revision);
56
+ const entries = { ...previous?.page.entries };
57
+ for (const [sequence, ref] of additions)
58
+ entries[sequence] = ref;
59
+ const page = { revision, commitId, previous: previous?.hash ?? null, entries };
60
+ const text = JSON.stringify(page), pageHash = (0, exports.historyHash)(text), directory = referenceDirectory(bucket, id);
61
+ writeImmutable(path_1.default.join(directory, `${pageHash}.json`), text);
62
+ (0, atomic_json_file_1.writeFileAtomic)(path_1.default.join(directory, `${window}.head.json`), JSON.stringify({ hash: pageHash }), 'utf8');
63
+ }
64
+ }
65
+ /** Resolve a sequence range with one small reference page and one chunk cache at a time. */
66
+ function historySequenceRange(bucket, record, first, last) {
67
+ const resident = new Map((record.messages || []).map(message => [message.sequence, message]));
68
+ const found = new Map();
69
+ let window = -1, references, chunkHash = '', chunk, missing = false;
70
+ for (let sequence = first; sequence <= last; sequence++) {
71
+ const inline = resident.get(sequence);
72
+ if (inline) {
73
+ found.set(sequence, inline);
74
+ continue;
75
+ }
76
+ const nextWindow = Math.floor((sequence - 1) / REFERENCE_WINDOW);
77
+ if (window !== nextWindow) {
78
+ window = nextWindow;
79
+ references = readReferencePage(bucket, record.id, window, Number(record.revision || 0))?.page;
80
+ }
81
+ const ref = references?.entries[String(sequence)];
82
+ if (!ref) {
83
+ missing = true;
84
+ continue;
85
+ }
86
+ if (chunkHash !== ref.chunk) {
87
+ chunkHash = ref.chunk;
88
+ chunk = readChunk(bucket, record.id, ref.chunk);
89
+ }
90
+ const message = chunk?.messages[ref.index];
91
+ if (!message || message.sequence !== sequence) {
92
+ missing = true;
93
+ continue;
94
+ }
95
+ found.set(sequence, message);
96
+ }
97
+ if (missing) {
98
+ // Never silently omit data when a rebuildable index is absent. One bounded
99
+ // range is resolved from the linked source, newest correction first.
100
+ found.clear();
101
+ for (const message of historyMessagesNewest(bucket, record))
102
+ if (message.sequence >= first && message.sequence <= last && !found.has(message.sequence))
103
+ found.set(message.sequence, message);
104
+ }
105
+ if (found.size !== last - first + 1)
106
+ throw new history_policy_1.HistoryStoreError('history_unavailable');
107
+ return [...found.values()].sort((a, b) => a.sequence - b.sequence);
108
+ }
109
+ const historyHash = (value) => crypto_1.default.createHash('sha256').update(value).digest('hex');
110
+ exports.historyHash = historyHash;
111
+ const historyDirectory = (bucket, id) => path_1.default.join(bucket, 'history', (0, exports.historyHash)(id).slice(0, 40));
112
+ exports.historyDirectory = historyDirectory;
113
+ function historyOf(record) {
114
+ const h = record?.history;
115
+ return h?.version === 3 ? h : undefined;
116
+ }
117
+ /** Account JSON bytes recursively before constructing a potentially enormous string. */
118
+ function jsonBytes(value, max = Number.MAX_SAFE_INTEGER) {
119
+ let bytes = 0;
120
+ const add = (n) => { bytes += n; if (bytes > max)
121
+ throw new history_policy_1.HistoryStoreError('history_payload_too_large', 413); };
122
+ const visit = (v) => {
123
+ if (v === undefined) {
124
+ add(4);
125
+ return;
126
+ }
127
+ if (typeof v === 'string') {
128
+ add(2 + Buffer.byteLength(v));
129
+ for (let i = 0; i < v.length; i++) {
130
+ const code = v.charCodeAt(i);
131
+ if (code < 32)
132
+ add(code === 8 || code === 9 || code === 10 || code === 12 || code === 13 ? 1 : 5);
133
+ else if (code === 34 || code === 92)
134
+ add(1);
135
+ }
136
+ }
137
+ else if (Array.isArray(v)) {
138
+ add(2);
139
+ v.forEach((x, i) => { if (i)
140
+ add(1); visit(x); });
141
+ }
142
+ else if (v && typeof v === 'object') {
143
+ add(2);
144
+ let n = 0;
145
+ for (const [k, x] of Object.entries(v)) {
146
+ if (x === undefined)
147
+ continue;
148
+ if (n++)
149
+ add(1);
150
+ visit(k);
151
+ add(1);
152
+ visit(x);
153
+ }
154
+ }
155
+ else
156
+ add(JSON.stringify(v)?.length ?? 4);
157
+ };
158
+ visit(value);
159
+ return bytes;
160
+ }
161
+ function readChunk(bucket, id, hash) {
162
+ if (!/^[a-f0-9]{64}$/.test(hash))
163
+ throw new history_policy_1.HistoryStoreError('history_unavailable');
164
+ try {
165
+ const file = path_1.default.join((0, exports.historyDirectory)(bucket, id), `${hash}.json`);
166
+ if (fs_1.default.statSync(file).size > history_policy_1.HISTORY_POLICY.chunkBytes)
167
+ throw new Error('oversized chunk');
168
+ const text = fs_1.default.readFileSync(file, 'utf8');
169
+ if ((0, exports.historyHash)(text) !== hash)
170
+ throw new Error('checksum');
171
+ const chunk = JSON.parse(text);
172
+ if (chunk.version !== 3 || chunk.conversationId !== id || !Array.isArray(chunk.messages))
173
+ throw new Error('invalid chunk');
174
+ return chunk;
175
+ }
176
+ catch {
177
+ throw new history_policy_1.HistoryStoreError('history_unavailable');
178
+ }
179
+ }
180
+ function writeImmutable(file, content) {
181
+ if (!fs_1.default.existsSync(file))
182
+ (0, atomic_json_file_1.writeFileAtomic)(file, content, 'utf8');
183
+ }
184
+ function inlineMessage(bucket, id, raw, sequence) {
185
+ const result = { ...raw, id: typeof raw.id === 'string' ? raw.id : (0, exports.historyHash)(`${sequence}:${raw.role}:${raw.at ?? raw.createdAt}:${raw.text}`), sequence };
186
+ if (typeof result.text === 'string' && Buffer.byteLength(result.text) > history_policy_1.HISTORY_POLICY.inlineTextBytes) {
187
+ const hash = (0, exports.historyHash)(result.text);
188
+ const bytes = Buffer.byteLength(result.text);
189
+ writeImmutable(path_1.default.join((0, exports.historyDirectory)(bucket, id), 'blobs', hash), result.text);
190
+ result.contentRef = { hash, bytes };
191
+ result.text = Buffer.from(result.text).subarray(0, 4096).toString('utf8');
192
+ }
193
+ jsonBytes(result, history_policy_1.HISTORY_POLICY.chunkBytes - 1024);
194
+ return result;
195
+ }
196
+ function appendChunks(bucket, id, messages, tail, revision, commitId) {
197
+ let pending = [];
198
+ let bytes = 512;
199
+ const flush = () => {
200
+ if (!pending.length)
201
+ return;
202
+ const chunk = { version: 3, conversationId: id, previous: tail, messages: pending };
203
+ const text = JSON.stringify(chunk);
204
+ const hash = (0, exports.historyHash)(text);
205
+ writeImmutable(path_1.default.join((0, exports.historyDirectory)(bucket, id), `${hash}.json`), text);
206
+ indexChunk(bucket, id, hash, pending, revision, commitId);
207
+ tail = hash;
208
+ pending = [];
209
+ bytes = 512;
210
+ };
211
+ for (const message of messages) {
212
+ const size = jsonBytes(message) + 1;
213
+ if (bytes + size > history_policy_1.HISTORY_POLICY.chunkBytes)
214
+ flush();
215
+ pending.push(message);
216
+ bytes += size;
217
+ }
218
+ flush();
219
+ return tail;
220
+ }
221
+ /** A newest-revision-first traversal makes corrections authoritative without a growing manifest. */
222
+ function* historyMessagesNewest(bucket, record) {
223
+ const resident = (record.messages || []);
224
+ for (let i = resident.length - 1; i >= 0; i--)
225
+ yield resident[i];
226
+ let tail = historyOf(record)?.archiveTail;
227
+ let hops = 0;
228
+ while (tail) {
229
+ if (++hops > 1_000_000)
230
+ throw new history_policy_1.HistoryStoreError('history_unavailable');
231
+ const chunk = readChunk(bucket, record.id, tail);
232
+ for (let i = chunk.messages.length - 1; i >= 0; i--)
233
+ yield chunk.messages[i];
234
+ tail = chunk.previous;
235
+ }
236
+ }
237
+ function prepareConversationHead(bucket, incoming, existing) {
238
+ const revision = Number(existing?.revision || 0) + 1, commitId = crypto_1.default.randomUUID();
239
+ // Reusing a revision after a failed head publication must not authorize orphaned
240
+ // reference pages from the failed transaction. A new nonce invalidates those pages.
241
+ (0, atomic_json_file_1.writeFileAtomic)(path_1.default.join((0, exports.historyDirectory)(bucket, incoming.id), 'commits', `${revision}.json`), JSON.stringify({ id: commitId }), 'utf8');
242
+ const previousHistory = historyOf(existing);
243
+ const oldResident = (existing?.messages || []);
244
+ const messages = incoming.messages ?? oldResident;
245
+ let lastSequence = previousHistory?.lastSequence ?? 0;
246
+ const bySequence = new Map();
247
+ for (const message of oldResident)
248
+ if (message.sequence)
249
+ bySequence.set(message.sequence, message);
250
+ const byId = new Map(oldResident.map(m => [m.id, m]));
251
+ const archivedCorrections = [];
252
+ const hasSequencedInput = messages.some(message => Number.isSafeInteger(message.sequence));
253
+ for (let i = 0; i < messages.length; i++) {
254
+ let raw = messages[i];
255
+ let previous = typeof raw.id === 'string' ? byId.get(raw.id) : undefined;
256
+ if (typeof raw.id !== 'string' && !Number.isSafeInteger(raw.sequence)) {
257
+ const sequence = i + 1;
258
+ if (existing && previousHistory && sequence <= previousHistory.lastSequence)
259
+ previous = historySequenceRange(bucket, existing, sequence, sequence)[0];
260
+ raw = { ...raw, sequence, ...(previous?.id ? { id: previous.id } : {}) };
261
+ }
262
+ if (!previous && !hasSequencedInput && existing && previousHistory && typeof raw.id === 'string' && !raw.sequence) {
263
+ // Legacy callers can resend a complete transcript. Resolve IDs against cold history
264
+ // one chunk at a time, rather than inventing duplicate sequence numbers.
265
+ for (const old of historyMessagesNewest(bucket, existing))
266
+ if (old.id === raw.id) {
267
+ previous = old;
268
+ break;
269
+ }
270
+ }
271
+ const supplied = Number(raw.sequence);
272
+ const sequence = previous?.sequence ?? (Number.isSafeInteger(supplied) && supplied > 0 && supplied <= lastSequence ? supplied : ++lastSequence);
273
+ const message = inlineMessage(bucket, incoming.id, raw, sequence);
274
+ if (previousHistory && sequence < previousHistory.firstResidentSequence) {
275
+ if (!previous || JSON.stringify(previous) !== JSON.stringify(message))
276
+ archivedCorrections.push(message);
277
+ }
278
+ else
279
+ bySequence.set(sequence, message);
280
+ }
281
+ const sorted = [...bySequence.values()].sort((a, b) => a.sequence - b.sequence);
282
+ let bytes = 2, cut = sorted.length;
283
+ while (cut > 0 && sorted.length - cut < history_policy_1.HISTORY_POLICY.residentMessages) {
284
+ const size = jsonBytes(sorted[cut - 1]) + 1;
285
+ if (bytes + size > history_policy_1.HISTORY_POLICY.residentMessageBytes)
286
+ break;
287
+ bytes += size;
288
+ cut--;
289
+ }
290
+ const tail = appendChunks(bucket, incoming.id, [...sorted.slice(0, cut), ...archivedCorrections], previousHistory?.archiveTail ?? null, revision, commitId);
291
+ const resident = sorted.slice(cut);
292
+ const result = {
293
+ ...incoming,
294
+ revision,
295
+ messages: resident,
296
+ history: { version: 3, firstResidentSequence: resident[0]?.sequence ?? lastSequence + 1, lastSequence, archiveTail: tail },
297
+ messageCount: lastSequence,
298
+ hasMessageBlobs: Boolean(existing?.hasMessageBlobs || sorted.some(m => m.contentRef || m.rawContentRef) || archivedCorrections.some(m => m.contentRef || m.rawContentRef)),
299
+ hasReviewHandoff: Object.prototype.hasOwnProperty.call(incoming, 'reviewHandoff') ? incoming.reviewHandoff?.reviewRequired === true : Boolean(incoming.hasReviewHandoff),
300
+ hasDelegation: Object.prototype.hasOwnProperty.call(incoming, 'delegation') ? Boolean(incoming.delegation) : Boolean(incoming.hasDelegation),
301
+ uiState: (0, conversation_ui_state_1.conversationUiState)(incoming),
302
+ };
303
+ if (result.run) {
304
+ result.run = { ...result.run };
305
+ delete result.run.messages;
306
+ }
307
+ // Preserve large historical/control values in immutable documents; full consumers must
308
+ // request the named document, while the interactive body remains bounded.
309
+ const refs = { ...(existing?.documentRefs || {}) };
310
+ const storeDocument = (key, value) => {
311
+ jsonBytes(value, history_policy_1.HISTORY_POLICY.controlDocumentBytes);
312
+ const text = JSON.stringify(value), hash = (0, exports.historyHash)(text);
313
+ writeImmutable(path_1.default.join((0, exports.historyDirectory)(bucket, incoming.id), 'documents', hash), text);
314
+ refs[key] = { hash, bytes: Buffer.byteLength(text) };
315
+ };
316
+ if (result.run)
317
+ for (const [key, value] of Object.entries(result.run)) {
318
+ if (value !== undefined && jsonBytes(value, history_policy_1.HISTORY_POLICY.controlDocumentBytes) > 32 * 1024) {
319
+ storeDocument(`run.${key}`, value);
320
+ delete result.run[key];
321
+ }
322
+ else
323
+ delete refs[`run.${key}`];
324
+ }
325
+ for (const [key, value] of Object.entries(result)) {
326
+ if (['messages', 'history', 'documentRefs'].includes(key) || value === undefined)
327
+ continue;
328
+ const size = jsonBytes(value, history_policy_1.HISTORY_POLICY.controlDocumentBytes);
329
+ if (size > 64 * 1024) {
330
+ storeDocument(key, value);
331
+ delete result[key];
332
+ }
333
+ else
334
+ delete refs[key];
335
+ }
336
+ if (Object.keys(refs).length)
337
+ result.documentRefs = refs;
338
+ else
339
+ delete result.documentRefs;
340
+ jsonBytes(result, history_policy_1.HISTORY_POLICY.headBytes - 1024);
341
+ return result;
342
+ }
343
+ function messagePage(bucket, record, options = {}) {
344
+ const limit = (0, history_policy_1.historyPageLimit)(options.limit);
345
+ const before = options.beforeSequence ?? Number.MAX_SAFE_INTEGER;
346
+ if (!Number.isSafeInteger(before) || before < 1)
347
+ throw new history_policy_1.HistoryStoreError('invalid_cursor', 400);
348
+ const last = Math.min(before - 1, historyOf(record)?.lastSequence ?? (record.messages?.length || 0));
349
+ const firstSequence = Math.max(1, last - limit);
350
+ const sorted = last >= firstSequence ? historySequenceRange(bucket, record, firstSequence, last).reverse() : [];
351
+ const messages = [];
352
+ let bytes = 512;
353
+ for (const message of sorted.slice(0, limit)) {
354
+ const size = jsonBytes(message);
355
+ if (bytes + size > history_policy_1.HISTORY_POLICY.responseBytes)
356
+ break;
357
+ messages.push(message);
358
+ bytes += size;
359
+ }
360
+ messages.reverse();
361
+ const first = messages[0]?.sequence ?? before;
362
+ return { messages, hasEarlier: sorted.length > messages.length || first > 1, nextBeforeSequence: first > 1 ? first : null, revision: Number(record.revision || 0) };
363
+ }