@mobileaidev/ai-app-bridge 0.2.15 → 0.3.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +306 -53
  4. package/bin/ai-app-bridge.js +51 -4504
  5. package/bin/android-permissions.js +152 -0
  6. package/bin/android-uia-xml.js +74 -0
  7. package/bin/artifact-paths.js +127 -1
  8. package/bin/bridge-forward.js +56 -0
  9. package/bin/command-discovery.js +86 -0
  10. package/bin/command-errors.js +60 -0
  11. package/bin/command-registry.js +504 -0
  12. package/bin/command-request.js +14 -0
  13. package/bin/command-router.js +80 -0
  14. package/bin/connection-cache.js +119 -0
  15. package/bin/device-provider.js +2934 -0
  16. package/bin/execution-host.js +596 -0
  17. package/bin/execution-runtime.js +101 -0
  18. package/bin/fact-codec.js +321 -0
  19. package/bin/fact-recorder.js +691 -0
  20. package/bin/fact-store.js +226 -0
  21. package/bin/feedback-probe.js +285 -0
  22. package/bin/intent/install-intent.js +267 -0
  23. package/bin/intent/intent-action-executor.js +129 -0
  24. package/bin/intent/intent-autonomous-adapter.js +46 -0
  25. package/bin/intent/intent-capture-port.js +64 -0
  26. package/bin/intent/intent-entry.js +226 -0
  27. package/bin/intent/intent-errors.js +28 -0
  28. package/bin/intent/intent-evidence-store.js +121 -0
  29. package/bin/intent/intent-lifetime.js +62 -0
  30. package/bin/intent/intent-observation-target.js +33 -0
  31. package/bin/intent/intent-observer.js +194 -0
  32. package/bin/intent/intent-production-adapter.js +334 -0
  33. package/bin/intent/intent-provider.js +27 -0
  34. package/bin/intent/intent-runtime.js +63 -0
  35. package/bin/intent/intent-worker.js +432 -0
  36. package/bin/intent/ios-intent-adapter.js +90 -0
  37. package/bin/intent/permission-intent.js +239 -0
  38. package/bin/intent/web-intent-adapter.js +31 -0
  39. package/bin/ios-device-outcome.js +47 -0
  40. package/bin/ios-execution.js +108 -0
  41. package/bin/ios-provider.js +497 -583
  42. package/bin/ios-runtime-binding.js +54 -0
  43. package/bin/ios-wda-execution.js +70 -0
  44. package/bin/ios-wda-port.js +98 -0
  45. package/bin/ios-wda-project.js +79 -0
  46. package/bin/mcp-server.js +83 -1040
  47. package/bin/mmap-scan-index.js +329 -0
  48. package/bin/observation-collector.js +862 -0
  49. package/bin/runtime-client.js +154 -0
  50. package/bin/runtime-directory.js +107 -0
  51. package/bin/runtime-protocol.js +36 -0
  52. package/bin/script/bounded-script-registry.js +103 -0
  53. package/bin/script/node-runtime-adapter.js +233 -0
  54. package/bin/script/progress-projector.js +63 -0
  55. package/bin/script/python-runtime-adapter.js +111 -0
  56. package/bin/script/rolling-summary.js +134 -0
  57. package/bin/script/script-agent-port.js +40 -0
  58. package/bin/script/script-assert.js +95 -0
  59. package/bin/script/script-capture-port.js +76 -0
  60. package/bin/script/script-catalog.js +85 -0
  61. package/bin/script/script-durable-restore.js +195 -0
  62. package/bin/script/script-entry-code.js +26 -0
  63. package/bin/script/script-entry-route.js +38 -0
  64. package/bin/script/script-entry.js +3 -0
  65. package/bin/script/script-errors.js +29 -0
  66. package/bin/script/script-evidence-store.js +22 -0
  67. package/bin/script/script-format-removed.js +26 -0
  68. package/bin/script/script-host-port.js +397 -0
  69. package/bin/script/script-ledger.js +64 -0
  70. package/bin/script/script-result.js +57 -0
  71. package/bin/script/script-sdk.js +152 -0
  72. package/bin/script/script-sdk.py +153 -0
  73. package/bin/script/script-session-channel.js +127 -0
  74. package/bin/script/script-spec.js +86 -0
  75. package/bin/script/script-supervisor.js +919 -0
  76. package/bin/script/templates/checkpoint-reentry.js +13 -0
  77. package/bin/segment-index.js +481 -0
  78. package/bin/segmented-fact-store.js +1571 -0
  79. package/bin/shared-kernel/android-h5-target.js +10 -0
  80. package/bin/shared-kernel/android-install-execution.js +176 -0
  81. package/bin/shared-kernel/android-sdk-endpoint.js +42 -0
  82. package/bin/shared-kernel/android-shell-execution.js +195 -0
  83. package/bin/shared-kernel/argument-schema.js +117 -0
  84. package/bin/shared-kernel/canonical-path.js +17 -0
  85. package/bin/shared-kernel/device-acknowledgements.js +53 -0
  86. package/bin/shared-kernel/device-completion-history.js +52 -0
  87. package/bin/shared-kernel/device-mutation-lease.js +219 -0
  88. package/bin/shared-kernel/device-ownership-recovery.js +95 -0
  89. package/bin/shared-kernel/device-ownership-store.js +94 -0
  90. package/bin/shared-kernel/evidence-adapters.js +251 -0
  91. package/bin/shared-kernel/evidence-archive.js +329 -0
  92. package/bin/shared-kernel/evidence-recording.js +131 -0
  93. package/bin/shared-kernel/evidence-schema.js +194 -0
  94. package/bin/shared-kernel/evidence-store.js +190 -0
  95. package/bin/shared-kernel/execution-admission.js +22 -0
  96. package/bin/shared-kernel/execution-contracts.js +171 -0
  97. package/bin/shared-kernel/execution-io.js +106 -0
  98. package/bin/shared-kernel/execution-ledger.js +125 -0
  99. package/bin/shared-kernel/execution-scope.js +87 -0
  100. package/bin/shared-kernel/execution-target.js +115 -0
  101. package/bin/shared-kernel/flutter-execution.js +13 -0
  102. package/bin/shared-kernel/flutter-h5-port.js +60 -0
  103. package/bin/shared-kernel/flutter-h5-target.js +9 -0
  104. package/bin/shared-kernel/flutter-target.js +75 -0
  105. package/bin/shared-kernel/h5-execution.js +11 -0
  106. package/bin/shared-kernel/h5-target.js +31 -0
  107. package/bin/shared-kernel/host-fact-store.js +49 -0
  108. package/bin/shared-kernel/ios-h5-target.js +9 -0
  109. package/bin/shared-kernel/ios-native-target.js +71 -0
  110. package/bin/shared-kernel/live-capture-query.js +115 -0
  111. package/bin/shared-kernel/managed-sdk-execution.js +78 -0
  112. package/bin/shared-kernel/native-execution.js +13 -0
  113. package/bin/shared-kernel/native-target.js +156 -0
  114. package/bin/shared-kernel/provider-command-contracts.js +55 -0
  115. package/bin/shared-kernel/recorded-payload-archive.js +195 -0
  116. package/bin/shared-kernel/request-context.js +35 -0
  117. package/bin/shared-kernel/semantic-node.js +55 -0
  118. package/bin/shared-kernel/summary-transformer.js +352 -0
  119. package/bin/shared-kernel/target-lease-protocol.js +47 -0
  120. package/bin/shared-kernel/text-wait.js +111 -0
  121. package/bin/shared-kernel/uia-execution.js +96 -0
  122. package/bin/shared-kernel/uia-protocol.js +214 -0
  123. package/bin/shared-kernel/uia-runtime-port.js +377 -0
  124. package/bin/shared-kernel/uia-target.js +39 -0
  125. package/bin/shared-kernel/web-dom-target.js +44 -0
  126. package/bin/shared-kernel/xml-attributes.js +25 -0
  127. package/bin/target-execution.js +275 -0
  128. package/bin/web/command-schema.js +60 -0
  129. package/bin/web/session-store.js +157 -0
  130. package/bin/web-provider.js +334 -553
  131. package/docs/COMMAND_CONTRACT.md +1563 -0
  132. package/docs/EVIDENCE_ARCHIVE.md +214 -0
  133. package/docs/INTENT_FOREGROUND.md +71 -0
  134. package/docs/INTENT_NATIVE_EDITING.md +79 -0
  135. package/docs/RELEASE.md +59 -0
  136. package/docs/SCRIPT_AUTHORING.md +489 -0
  137. package/node_modules/@mobileaidev/segmented-fact-store-native/LICENSE +201 -0
  138. package/node_modules/@mobileaidev/segmented-fact-store-native/NOTICE +7 -0
  139. package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +36 -0
  140. package/node_modules/@mobileaidev/segmented-fact-store-native/bindings/node/sfs_node.c +597 -0
  141. package/node_modules/@mobileaidev/segmented-fact-store-native/include/sfs.h +178 -0
  142. package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +5 -0
  143. package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +24 -0
  144. package/node_modules/@mobileaidev/segmented-fact-store-native/src/sfs.c +2349 -0
  145. package/package.json +60 -5
  146. package/runtime/ios-wda/AABWDABinding.h +19 -0
  147. package/runtime/ios-wda/AABWDABinding.m +97 -0
  148. package/runtime/ios-wda/AABWDAExecution.h +26 -0
  149. package/runtime/ios-wda/AABWDAExecution.m +172 -0
  150. package/runtime/ios-wda/AABWDAIntegration.h +71 -0
  151. package/runtime/ios-wda/AABWDAManagedRoutes.h +392 -0
  152. package/runtime/ios-wda/AABWDAReceiptStore.h +10 -0
  153. package/runtime/ios-wda/AABWDAReceiptStore.m +116 -0
  154. package/runtime/uia/ai-app-bridge-uia.jar +0 -0
  155. package/runtime/uia/manifest.json +22 -0
  156. package/skills/ai-app-bridge-use/SKILL.md +23 -360
@@ -0,0 +1,1571 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+
8
+ const { normalizePersistentFact } = require('./fact-codec');
9
+ const { MmapScanIndex } = require('./mmap-scan-index');
10
+ const { SegmentIndex } = require('./segment-index');
11
+
12
+ const MIB = 1024 * 1024;
13
+ const GIB = 1024 * MIB;
14
+ const MINIMUM_SAFETY_RESERVE = 256 * MIB;
15
+ const DEFAULT_PROJECTION_BUDGET_BYTES = GIB;
16
+ const PARTITION_ALL = 0xffff_ffff;
17
+ const NATIVE_SEGMENT_OVERHEAD_BYTES = 96;
18
+ const LARGE_FACT_CHUNK_MAGIC = Buffer.from('AIBCHN01', 'ascii');
19
+ const LARGE_FACT_CHUNK_VERSION = 1;
20
+ const LARGE_FACT_CHUNK_HEADER_BYTES = 72;
21
+ const LARGE_FACT_MANIFEST_KIND = 'ai-app-bridge.large-fact-manifest.v1';
22
+ const LARGE_FACT_LOGICAL_BYTES = '__aiAppBridgeLogicalPayloadBytes';
23
+ const LARGE_FACT_WIRE_V1 = Object.freeze({
24
+ chunkMagicAscii: LARGE_FACT_CHUNK_MAGIC.toString('ascii'),
25
+ chunkVersion: LARGE_FACT_CHUNK_VERSION,
26
+ chunkHeaderBytes: LARGE_FACT_CHUNK_HEADER_BYTES,
27
+ manifestMarkerField: '__aiAppBridgeInternal',
28
+ manifestMarkerValue: LARGE_FACT_MANIFEST_KIND,
29
+ });
30
+ const FACT_STORE_PROFILES = Object.freeze({
31
+ '1gb': 1024 * 1024 * 1024,
32
+ '512mb': 512 * 1024 * 1024,
33
+ '256mb': 256 * 1024 * 1024,
34
+ '64mb': 64 * 1024 * 1024,
35
+ });
36
+ const SEGMENT_SIZE_BY_PROFILE = Object.freeze({
37
+ '1gb': 4 * 1024 * 1024,
38
+ '512mb': 4 * 1024 * 1024,
39
+ '256mb': 2 * 1024 * 1024,
40
+ '64mb': 512 * 1024,
41
+ });
42
+ const PARTITIONS = Object.freeze([
43
+ 'network',
44
+ 'ui',
45
+ 'app-log',
46
+ 'device-log',
47
+ 'state-event',
48
+ 'action',
49
+ 'note',
50
+ 'index',
51
+ ]);
52
+ const PARTITION_IDS = Object.freeze(Object.fromEntries(PARTITIONS.map((name, index) => [name, index])));
53
+ // Ten percent remains outside mmap segment quotas for manifests, recovery and
54
+ // critical control operations. The Host query projection has a separate 1 GiB
55
+ // budget and is never charged to this reserve.
56
+ const PARTITION_WEIGHTS = Object.freeze({
57
+ network: 25,
58
+ ui: 18,
59
+ 'app-log': 10,
60
+ 'device-log': 7,
61
+ 'state-event': 10,
62
+ action: 13,
63
+ note: 5,
64
+ index: 2,
65
+ });
66
+ class NativeSegmentEngine {
67
+ constructor({
68
+ binding = loadNativeBinding(),
69
+ directory,
70
+ segmentSize,
71
+ partitionQuotas,
72
+ inheritExistingConfiguration = false,
73
+ }) {
74
+ this.binding = binding;
75
+ this.handle = binding.open(inheritExistingConfiguration
76
+ ? { directory, segmentSize: 0 }
77
+ : { directory, segmentSize, partitionQuotas });
78
+ this.closed = false;
79
+ }
80
+
81
+ append(partitionId, payload, options = {}) {
82
+ this.assertOpen();
83
+ return numericRecord(this.binding.append(
84
+ this.handle,
85
+ partitionId,
86
+ payload,
87
+ options.durability === 'sync',
88
+ ));
89
+ }
90
+
91
+ scan(cursor = {}) {
92
+ this.assertOpen();
93
+ const result = this.binding.scan(this.handle, cursor);
94
+ return {
95
+ ...result,
96
+ cursor: numericCursor(result.cursor),
97
+ ...(result.record ? { record: numericRecord(result.record) } : {}),
98
+ };
99
+ }
100
+
101
+ readAt(location) {
102
+ const result = this.scan({
103
+ partitionId: PARTITION_IDS[location.partition],
104
+ afterSequence: location.sequence - 1,
105
+ segmentId: location.segmentId,
106
+ offset: location.frameOffset,
107
+ });
108
+ if (result.done || result.record.sequence !== location.sequence) {
109
+ const error = new Error(`segment record ${location.sequence} is no longer available`);
110
+ error.code = 'segment_location_expired';
111
+ throw error;
112
+ }
113
+ return result;
114
+ }
115
+
116
+ status() {
117
+ this.assertOpen();
118
+ return numericTree(this.binding.status(this.handle));
119
+ }
120
+
121
+ flush() {
122
+ this.assertOpen();
123
+ this.binding.flush(this.handle);
124
+ }
125
+
126
+ close() {
127
+ if (this.closed) return;
128
+ this.binding.close(this.handle);
129
+ this.closed = true;
130
+ }
131
+
132
+ assertOpen() {
133
+ if (this.closed) throw new Error('NativeSegmentEngine is closed');
134
+ }
135
+ }
136
+
137
+ class DisabledSegmentedFactStoreAdapter {
138
+ constructor({ directory, storageCapacity, reason = 'insufficient-space' } = {}) {
139
+ this.directory = path.resolve(directory || defaultSegmentedFactStorePath());
140
+ this.storageCapacity = normalizeStorageCapacity(storageCapacity);
141
+ this.reason = reason;
142
+ this.closed = false;
143
+ this.droppedRecords = 0;
144
+ }
145
+
146
+ record() {
147
+ this.droppedRecords += 1;
148
+ return {
149
+ ok: false,
150
+ stored: false,
151
+ dropped: true,
152
+ error: 'fact_store_disabled',
153
+ reason: this.reason,
154
+ };
155
+ }
156
+
157
+ read() {
158
+ return {
159
+ ok: false,
160
+ error: 'fact_store_disabled',
161
+ message: `Segmented FactStore is disabled: ${this.reason}.`,
162
+ items: [],
163
+ count: 0,
164
+ gap: false,
165
+ cursorExpired: false,
166
+ hasMore: false,
167
+ };
168
+ }
169
+
170
+ status() {
171
+ return {
172
+ ok: true,
173
+ adapter: 'segmented-mmap-disabled',
174
+ authoritative: true,
175
+ persistence: false,
176
+ degraded: true,
177
+ profile: 'off-low-disk',
178
+ profileSelection: { mode: 'auto', ...this.storageCapacity },
179
+ budgetBytes: 0,
180
+ segmentSize: 0,
181
+ directory: this.directory,
182
+ disabledReason: this.reason,
183
+ droppedRecords: this.droppedRecords,
184
+ closed: this.closed,
185
+ };
186
+ }
187
+
188
+ close() {
189
+ this.closed = true;
190
+ }
191
+ }
192
+
193
+ class SegmentedFactStoreAdapter {
194
+ constructor(options = {}) {
195
+ this.directory = path.resolve(options.directory || defaultSegmentedFactStorePath());
196
+ const requestedProfile = normalizeProfile(options.profile);
197
+ const storageCapacity = requestedProfile === 'auto'
198
+ ? normalizeStorageCapacity(options.storageCapacity || readStorageCapacity(this.directory))
199
+ : null;
200
+ const capacityCandidate = requestedProfile === 'auto' ? selectAutomaticProfile(storageCapacity) : null;
201
+ this.profile = capacityCandidate || requestedProfile;
202
+ this.budgetBytes = positiveInteger(options.budgetBytes, FACT_STORE_PROFILES[this.profile], 'budgetBytes');
203
+ if (options.budgetBytes !== undefined && this.budgetBytes !== FACT_STORE_PROFILES[this.profile]) {
204
+ this.profile = 'custom';
205
+ }
206
+ this.profileSelection = requestedProfile === 'auto'
207
+ ? { mode: 'auto', ...storageCapacity }
208
+ : { mode: 'explicit' };
209
+ this.projectionBudgetBytes = positiveInteger(
210
+ options.projectionBudgetBytes,
211
+ DEFAULT_PROJECTION_BUDGET_BYTES,
212
+ 'projectionBudgetBytes',
213
+ );
214
+ this.segmentSize = positiveInteger(
215
+ options.segmentSize,
216
+ SEGMENT_SIZE_BY_PROFILE[this.profile] || Math.min(512 * 1024, this.budgetBytes),
217
+ 'segmentSize',
218
+ );
219
+ this.partitionQuotas = options.partitionQuotas
220
+ ? normalizePartitionQuotas(options.partitionQuotas, this.segmentSize)
221
+ : partitionQuotas(this.budgetBytes, this.segmentSize);
222
+ const allocatedBytes = this.partitionQuotas.reduce((sum, quota) => sum + quota, 0);
223
+ if (allocatedBytes > this.budgetBytes) {
224
+ throw new RangeError('partitionQuotas exceed budgetBytes');
225
+ }
226
+ this.segmentsDirectory = path.join(this.directory, 'segments');
227
+ const inheritExistingConfiguration = requestedProfile === 'auto'
228
+ && options.budgetBytes === undefined
229
+ && options.segmentSize === undefined
230
+ && options.partitionQuotas === undefined
231
+ && hasCommittedManifest(this.segmentsDirectory);
232
+ this.indexDirectory = path.join(this.directory, 'projection');
233
+ this.removeProjection = typeof options.removeProjection === 'function'
234
+ ? options.removeProjection
235
+ : () => fs.rmSync(this.indexDirectory, { recursive: true, force: true });
236
+ fs.mkdirSync(this.directory, { recursive: true, mode: 0o700 });
237
+ restrictDirectory(this.directory);
238
+ this.index = null;
239
+ this.engine = null;
240
+ this.ownsIndex = !options.index;
241
+ this.projectionFallback = null;
242
+ this.projectionInitializationError = null;
243
+ this.projectionCheckInterval = positiveInteger(
244
+ options.projectionCheckInterval,
245
+ 1,
246
+ 'projectionCheckInterval',
247
+ );
248
+ this.projectionWritesSinceCheck = 0;
249
+ this.lastPrunedFirstSequence = Array(PARTITIONS.length).fill(0);
250
+ this.closed = false;
251
+ try {
252
+ if (options.index) {
253
+ this.index = options.index;
254
+ } else {
255
+ try {
256
+ this.index = new SegmentIndex({
257
+ directory: this.indexDirectory,
258
+ budgetBytes: this.projectionBudgetBytes,
259
+ ...(Object.prototype.hasOwnProperty.call(options, 'sqliteModule')
260
+ ? { sqliteModule: options.sqliteModule }
261
+ : {}),
262
+ });
263
+ } catch (error) {
264
+ this.projectionFallback = {
265
+ code: error?.code || error?.name || 'sqlite_projection_unavailable',
266
+ message: String(error?.message || error),
267
+ };
268
+ this.projectionInitializationError = error;
269
+ this.index = null;
270
+ }
271
+ }
272
+ this.engine = options.engine || new NativeSegmentEngine({
273
+ binding: options.binding,
274
+ directory: this.segmentsDirectory,
275
+ segmentSize: this.segmentSize,
276
+ partitionQuotas: this.partitionQuotas,
277
+ inheritExistingConfiguration,
278
+ });
279
+ if (inheritExistingConfiguration) {
280
+ const engineStatus = this.engine.status();
281
+ const inheritedSegmentSize = positiveInteger(
282
+ engineStatus.segmentSize,
283
+ undefined,
284
+ 'existing segmentSize',
285
+ );
286
+ const inheritedQuotas = existingPartitionQuotas(engineStatus);
287
+ const inheritedProfile = profileForConfiguration(inheritedSegmentSize, inheritedQuotas);
288
+ this.segmentSize = inheritedSegmentSize;
289
+ this.partitionQuotas = inheritedQuotas;
290
+ this.profile = inheritedProfile || 'custom';
291
+ this.budgetBytes = inheritedProfile
292
+ ? FACT_STORE_PROFILES[inheritedProfile]
293
+ : inheritedQuotas.reduce((sum, quota) => sum + quota, 0);
294
+ this.profileSelection = {
295
+ mode: 'auto-existing',
296
+ capacityCandidate,
297
+ ...storageCapacity,
298
+ };
299
+ }
300
+ if (!this.index) {
301
+ this.activateMmapScanProjection(
302
+ this.projectionInitializationError,
303
+ 'startup-open',
304
+ { forceScanOnly: true },
305
+ );
306
+ } else try {
307
+ this.cursorSecret = this.index.metadata('cursor-secret', () => crypto.randomBytes(32).toString('base64url'));
308
+ this.storeId = this.index.metadata('store-id', () => crypto.randomUUID());
309
+ this.recovery = this.reconcileProjection();
310
+ this.maintainProjectionBudget({ force: true });
311
+ } catch (error) {
312
+ if (!this.ownsIndex) throw error;
313
+ this.activateMmapScanProjection(error, 'startup-reconcile', { forceScanOnly: true });
314
+ }
315
+ } catch (error) {
316
+ this.closed = true;
317
+ try {
318
+ this.engine?.close();
319
+ } catch (_) {
320
+ // Preserve the construction error.
321
+ }
322
+ try {
323
+ this.index?.close();
324
+ } catch (_) {
325
+ // Preserve the construction error.
326
+ }
327
+ throw error;
328
+ }
329
+ }
330
+
331
+ record(fact, options = {}) {
332
+ this.assertOpen();
333
+ const normalized = normalizePersistentFact(fact);
334
+ const partitionId = PARTITION_IDS[normalized.partition];
335
+ if (partitionId === undefined) {
336
+ throw new TypeError(`partition must be one of: ${PARTITIONS.join(', ')}`);
337
+ }
338
+ let projectionDegraded = false;
339
+ if (normalized.dedupeKey) {
340
+ let existing;
341
+ try {
342
+ existing = this.index.findDedupe(normalized.dedupeKey);
343
+ } catch (error) {
344
+ this.activateMmapScanProjection(error, 'dedupe-read', { forceScanOnly: true });
345
+ projectionDegraded = true;
346
+ existing = this.index.findDedupe(normalized.dedupeKey);
347
+ }
348
+ if (existing) {
349
+ return {
350
+ ok: true,
351
+ stored: true,
352
+ deduplicated: true,
353
+ globalSeq: existing.sequence,
354
+ partition: existing.partition,
355
+ targetKey: existing.targetKey,
356
+ runtimeEpoch: existing.runtimeEpoch,
357
+ actionId: existing.actionId,
358
+ sizeBytes: existing.payloadLength,
359
+ evictedCount: 0,
360
+ ...(projectionDegraded ? { projectionDegraded: true } : {}),
361
+ };
362
+ }
363
+ }
364
+ const payload = Buffer.from(JSON.stringify(normalized));
365
+ const critical = normalized.partition === 'action';
366
+ const durability = options.durability || (critical ? 'sync' : 'memory');
367
+ const location = this.appendLogicalFact(partitionId, normalized, payload, { durability });
368
+ let evictedCount = 0;
369
+ try {
370
+ const engineStatus = this.engine.status();
371
+ evictedCount = this.pruneEvictedProjection(engineStatus.partitions);
372
+ this.index.insert(normalized, {
373
+ sequence: location.sequence,
374
+ segmentId: location.segmentId,
375
+ frameOffset: location.frameOffset,
376
+ payloadLength: location.payloadLength,
377
+ });
378
+ this.updateProjectionCheckpoint(engineStatus);
379
+ const maintenance = this.maintainProjectionBudget();
380
+ projectionDegraded ||= maintenance.degraded;
381
+ } catch (error) {
382
+ try {
383
+ this.activateMmapScanProjection(error, 'post-commit', {
384
+ failedSequence: location.sequence,
385
+ });
386
+ projectionDegraded = true;
387
+ } catch (fallbackError) {
388
+ const wrapped = new Error(`fact committed but every query projection failed: ${fallbackError.message}`);
389
+ wrapped.code = 'segment_commit_index_unknown';
390
+ wrapped.sequence = location.sequence;
391
+ wrapped.cause = fallbackError;
392
+ throw wrapped;
393
+ }
394
+ }
395
+ return {
396
+ ok: true,
397
+ stored: true,
398
+ globalSeq: location.sequence,
399
+ partition: normalized.partition,
400
+ targetKey: normalized.targetKey,
401
+ runtimeEpoch: normalized.runtimeEpoch,
402
+ actionId: normalized.actionId,
403
+ sizeBytes: payload.length,
404
+ evictedCount,
405
+ durability,
406
+ segmentId: location.segmentId,
407
+ frameOffset: location.frameOffset,
408
+ ...(projectionDegraded ? {
409
+ projectionDegraded: true,
410
+ projectionFallbackCode: this.projectionFallback?.code,
411
+ } : {}),
412
+ };
413
+ }
414
+
415
+ appendLogicalFact(partitionId, normalized, payload, { durability }) {
416
+ const maxNativePayload = this.segmentSize - NATIVE_SEGMENT_OVERHEAD_BYTES;
417
+ if (payload.length <= maxNativePayload) {
418
+ return this.engine.append(partitionId, payload, { durability });
419
+ }
420
+ const maxChunkBytes = maxNativePayload - LARGE_FACT_CHUNK_HEADER_BYTES;
421
+ if (maxChunkBytes <= 0) {
422
+ throw new RangeError('segmentSize is too small for a large-fact chunk header');
423
+ }
424
+ const digest = crypto.createHash('sha256').update(payload).digest();
425
+ const chunkCount = Math.ceil(payload.length / maxChunkBytes);
426
+ const slices = [];
427
+ for (let ordinal = 0; ordinal < chunkCount; ordinal += 1) {
428
+ const start = ordinal * maxChunkBytes;
429
+ slices.push(payload.subarray(start, Math.min(payload.length, start + maxChunkBytes)));
430
+ }
431
+ const manifestUpperBound = Buffer.from(JSON.stringify(createLargeFactManifest({
432
+ normalized,
433
+ digest,
434
+ byteLength: payload.length,
435
+ chunks: slices.map((bytes, ordinal) => ({
436
+ ordinal,
437
+ sequence: Number.MAX_SAFE_INTEGER,
438
+ segmentId: Number.MAX_SAFE_INTEGER,
439
+ frameOffset: Number.MAX_SAFE_INTEGER,
440
+ payloadLength: LARGE_FACT_CHUNK_HEADER_BYTES + bytes.length,
441
+ byteLength: bytes.length,
442
+ })),
443
+ })));
444
+ if (manifestUpperBound.length > maxNativePayload) {
445
+ const error = new RangeError('large fact manifest does not fit in an empty segment');
446
+ error.code = 'large_fact_manifest_too_large';
447
+ throw error;
448
+ }
449
+ this.assertLargeFactCapacity(partitionId, [
450
+ ...slices.map((bytes) => LARGE_FACT_CHUNK_HEADER_BYTES + bytes.length),
451
+ manifestUpperBound.length,
452
+ ]);
453
+
454
+ const chunks = [];
455
+ for (let ordinal = 0; ordinal < slices.length; ordinal += 1) {
456
+ const bytes = slices[ordinal];
457
+ const chunkPayload = encodeLargeFactChunk({
458
+ digest,
459
+ ordinal,
460
+ chunkCount,
461
+ totalLength: payload.length,
462
+ bytes,
463
+ });
464
+ const location = this.engine.append(partitionId, chunkPayload, { durability: 'memory' });
465
+ chunks.push({
466
+ ordinal,
467
+ sequence: location.sequence,
468
+ segmentId: location.segmentId,
469
+ frameOffset: location.frameOffset,
470
+ payloadLength: location.payloadLength,
471
+ byteLength: bytes.length,
472
+ });
473
+ }
474
+ const manifestPayload = Buffer.from(JSON.stringify(createLargeFactManifest({
475
+ normalized,
476
+ digest,
477
+ byteLength: payload.length,
478
+ chunks,
479
+ })));
480
+ const manifestLocation = this.engine.append(partitionId, manifestPayload, { durability });
481
+ return {
482
+ ...manifestLocation,
483
+ payloadLength: payload.length,
484
+ };
485
+ }
486
+
487
+ assertLargeFactCapacity(partitionId, payloadLengths) {
488
+ const status = this.engine.status();
489
+ const partition = status.partitions[partitionId];
490
+ const maxSegments = Math.floor(this.partitionQuotas[partitionId] / this.segmentSize);
491
+ let hasSegment = Number(partition?.segmentCount || 0) > 0;
492
+ let offset = hasSegment ? Number(partition.activeWriteOffset) : 64;
493
+ let currentSegmentContainsFact = false;
494
+ let factSegments = 0;
495
+ for (const payloadLength of payloadLengths) {
496
+ const frameLength = nativeFrameLength(payloadLength);
497
+ if (!hasSegment || offset > this.segmentSize - frameLength) {
498
+ hasSegment = true;
499
+ offset = 64;
500
+ currentSegmentContainsFact = false;
501
+ }
502
+ if (!currentSegmentContainsFact) {
503
+ factSegments += 1;
504
+ currentSegmentContainsFact = true;
505
+ }
506
+ offset += frameLength;
507
+ }
508
+ if (factSegments > maxSegments) {
509
+ const error = new RangeError(
510
+ `large fact needs ${factSegments} segments but partition ${PARTITIONS[partitionId]} retains ${maxSegments}`,
511
+ );
512
+ error.code = 'large_fact_exceeds_partition_quota';
513
+ error.requiredSegments = factSegments;
514
+ error.availableSegments = maxSegments;
515
+ throw error;
516
+ }
517
+ }
518
+
519
+ read(query = {}) {
520
+ this.assertOpen();
521
+ const normalized = normalizeReadQuery(query);
522
+ const scope = cursorScope(normalized);
523
+ const decoded = normalized.cursor === null
524
+ ? { ok: true, sequence: 0 }
525
+ : decodeCursor(normalized.cursor, scope, this.cursorSecret, this.storeId);
526
+ if (!decoded.ok) {
527
+ return {
528
+ ok: false,
529
+ error: 'invalid_cursor',
530
+ message: decoded.message,
531
+ items: [],
532
+ count: 0,
533
+ gap: false,
534
+ cursorExpired: false,
535
+ };
536
+ }
537
+ const decodedThrough = normalized.throughCursor === null
538
+ ? { ok: true, sequence: null }
539
+ : decodeCursor(normalized.throughCursor, scope, this.cursorSecret, this.storeId);
540
+ if (!decodedThrough.ok) {
541
+ return {
542
+ ok: false,
543
+ error: 'invalid_through_cursor',
544
+ message: decodedThrough.message,
545
+ items: [],
546
+ count: 0,
547
+ gap: false,
548
+ cursorExpired: false,
549
+ };
550
+ }
551
+ const engineStatus = this.engine.status();
552
+ const gap = cursorGap(decoded.sequence, normalized.partitions, engineStatus.partitions);
553
+ try {
554
+ this.pruneEvictedProjection(engineStatus.partitions);
555
+ } catch (error) {
556
+ this.activateMmapScanProjection(error, 'query-prune', { forceScanOnly: true });
557
+ return this.read(query);
558
+ }
559
+ if (normalized.cursor !== null && gap) {
560
+ return {
561
+ ok: false,
562
+ error: 'cursor_expired',
563
+ message: 'Facts after this cursor have been evicted from one or more partitions.',
564
+ items: [],
565
+ count: 0,
566
+ gap: true,
567
+ cursorExpired: true,
568
+ evictedPartitions: gap,
569
+ };
570
+ }
571
+ let located;
572
+ try {
573
+ located = this.index.query({
574
+ afterSequence: decoded.sequence,
575
+ throughSequence: decodedThrough.sequence,
576
+ targetKey: normalized.targetKey,
577
+ partitions: normalized.partitions,
578
+ runtimeEpoch: normalized.runtimeEpoch,
579
+ ...(normalized.hasActionId ? { actionId: normalized.actionId } : {}),
580
+ fromObservedAtMs: normalized.fromObservedAtMs,
581
+ toObservedAtMs: normalized.toObservedAtMs,
582
+ limit: normalized.limit,
583
+ });
584
+ } catch (error) {
585
+ this.activateMmapScanProjection(error, 'query', { forceScanOnly: true });
586
+ return this.read(query);
587
+ }
588
+ const items = [];
589
+ try {
590
+ for (const location of located.items) {
591
+ const scanned = this.engine.readAt(location);
592
+ const decoded = decodeLogicalFact({
593
+ engine: this.engine,
594
+ partition: location.partition,
595
+ payload: scanned.payload,
596
+ sequence: location.sequence,
597
+ });
598
+ items.push({
599
+ ...decoded.fact,
600
+ globalSeq: location.sequence,
601
+ sizeBytes: decoded.byteLength,
602
+ });
603
+ }
604
+ } catch (error) {
605
+ if (error.code !== 'segment_location_expired') throw error;
606
+ return {
607
+ ok: false,
608
+ error: 'cursor_expired',
609
+ message: error.message,
610
+ items: [],
611
+ count: 0,
612
+ gap: true,
613
+ cursorExpired: true,
614
+ };
615
+ }
616
+ const latestSequence = Math.max(0, engineStatus.nextSequence - 1);
617
+ const cursorSequence = located.hasMore && items.length > 0
618
+ ? items.at(-1).globalSeq
619
+ : (decodedThrough.sequence ?? latestSequence);
620
+ return {
621
+ ok: true,
622
+ items,
623
+ count: items.length,
624
+ cursor: encodeCursor(cursorSequence, scope, this.cursorSecret, this.storeId),
625
+ gap: false,
626
+ cursorExpired: false,
627
+ hasMore: located.hasMore,
628
+ };
629
+ }
630
+
631
+ status() {
632
+ if (this.closed) return { ...this.lastStatus, closed: true };
633
+ const engine = this.engine.status();
634
+ const index = this.index.status();
635
+ const totalBytes = directoryBytes(this.directory);
636
+ const mmapBytes = Number(engine.segmentCount || 0) * this.segmentSize;
637
+ const projectionBytes = directoryBytes(this.indexDirectory);
638
+ return {
639
+ ok: true,
640
+ adapter: 'segmented-mmap',
641
+ authoritative: true,
642
+ persistence: true,
643
+ degraded: Boolean(this.projectionFallback),
644
+ profile: this.profile,
645
+ profileSelection: this.profileSelection,
646
+ budgetBytes: this.budgetBytes,
647
+ segmentSize: this.segmentSize,
648
+ directory: this.directory,
649
+ storeId: this.storeId,
650
+ quota: {
651
+ scope: 'host-mmap-store',
652
+ budgetBytes: this.budgetBytes,
653
+ emergencyReserveBytes: this.budgetBytes - this.partitionQuotas.reduce((sum, quota) => sum + quota, 0),
654
+ partitions: Object.fromEntries(PARTITIONS.map((partition, id) => [partition, {
655
+ id,
656
+ weight: PARTITION_WEIGHTS[partition],
657
+ hardQuotaBytes: this.partitionQuotas[id],
658
+ ...(engine.partitions[id] || {}),
659
+ }])),
660
+ },
661
+ storage: {
662
+ totalBytes,
663
+ // overQuota describes only the authoritative mmap store. The
664
+ // rebuildable SQLite projection has its own budget and
665
+ // can never trigger mmap fact eviction.
666
+ overQuota: mmapBytes > this.budgetBytes,
667
+ mmapBytes,
668
+ mmapBudgetBytes: this.budgetBytes,
669
+ mmapQuotaScope: 'host-mmap-store',
670
+ projectionBytes,
671
+ projectionBudgetBytes: this.projectionBudgetBytes,
672
+ projectionOverQuota: projectionBytes > this.projectionBudgetBytes,
673
+ projectionQuotaScope: 'host-sqlite-projection',
674
+ independentBudgets: true,
675
+ crossStoreEviction: false,
676
+ controlBytes: Math.max(0, totalBytes - mmapBytes - projectionBytes),
677
+ },
678
+ sequences: {
679
+ latestGlobalSeq: Math.max(0, Number(index.maxSequence || 0)),
680
+ nextGlobalSeq: engine.nextSequence,
681
+ },
682
+ recovery: {
683
+ ...this.recovery,
684
+ recoveredTail: engine.recoveredTail,
685
+ recoveryPartitionId: engine.recoveryPartitionId,
686
+ recoverySegmentId: engine.recoverySegmentId,
687
+ recoveryOffset: engine.recoveryOffset,
688
+ discardedBytes: engine.recoveryDiscardedBytes,
689
+ },
690
+ engine,
691
+ index,
692
+ projectionFallback: this.projectionFallback,
693
+ closed: false,
694
+ };
695
+ }
696
+
697
+ flush() {
698
+ this.assertOpen();
699
+ this.engine.flush();
700
+ }
701
+
702
+ close() {
703
+ if (this.closed) return;
704
+ let firstError = null;
705
+ for (const operation of [
706
+ () => this.engine.flush(),
707
+ () => { this.lastStatus = this.status(); },
708
+ () => this.index.close(),
709
+ () => this.engine.close(),
710
+ ]) {
711
+ try {
712
+ operation();
713
+ } catch (error) {
714
+ if (!firstError) firstError = error;
715
+ }
716
+ }
717
+ this.closed = true;
718
+ if (firstError) throw firstError;
719
+ }
720
+
721
+ reconcileProjection() {
722
+ const status = this.engine.status();
723
+ const engineMax = Math.max(0, status.nextSequence - 1);
724
+ const indexMax = this.index.maxSequence();
725
+ let rebuilt = false;
726
+ if (indexMax > engineMax) {
727
+ this.index.clear();
728
+ rebuilt = true;
729
+ }
730
+ const pruned = this.pruneEvictedProjection(status.partitions);
731
+ const indexedCount = Number(this.index.status().count || 0);
732
+ const checkpointCount = metadataNonNegativeInteger(this.index, 'logical-count');
733
+ const checkpointNextSequence = metadataNonNegativeInteger(this.index, 'projected-next-sequence');
734
+ const checkpointPhysicalCount = metadataNonNegativeInteger(this.index, 'projected-physical-count');
735
+ if (
736
+ !rebuilt
737
+ && checkpointCount === indexedCount
738
+ && checkpointNextSequence === Number(status.nextSequence)
739
+ && checkpointPhysicalCount === Number(status.recordCount)
740
+ ) {
741
+ return { rebuilt: false, recoveredRecords: 0, prunedRecords: pruned };
742
+ }
743
+ let recovered = this.scanLogicalRecordsIntoProjection();
744
+ if (this.index.status().count > recovered.logicalCount) {
745
+ this.index.clear();
746
+ rebuilt = true;
747
+ recovered = this.scanLogicalRecordsIntoProjection();
748
+ }
749
+ this.index.setMetadata('logical-count', String(recovered.logicalCount));
750
+ this.index.setMetadata('projected-next-sequence', String(status.nextSequence));
751
+ this.index.setMetadata('projected-physical-count', String(status.recordCount));
752
+ const recoveredRecords = recovered.recoveredRecords;
753
+ return { rebuilt, recoveredRecords, prunedRecords: pruned };
754
+ }
755
+
756
+ updateProjectionCheckpoint(engineStatus) {
757
+ if (this.index instanceof MmapScanIndex) return;
758
+ this.index.setMetadata('logical-count', String(this.index.status().count));
759
+ this.index.setMetadata('projected-next-sequence', String(engineStatus.nextSequence));
760
+ this.index.setMetadata('projected-physical-count', String(engineStatus.recordCount));
761
+ }
762
+
763
+ scanLogicalRecordsIntoProjection() {
764
+ const pending = [];
765
+ let cursor = { partitionId: PARTITION_ALL, afterSequence: 0 };
766
+ let logicalCount = 0;
767
+ let recoveredRecords = 0;
768
+ while (true) {
769
+ const scanned = this.engine.scan(cursor);
770
+ cursor = scanned.cursor;
771
+ if (scanned.done) break;
772
+ const fact = decodeFactPayload(scanned.payload, scanned.record.sequence);
773
+ if (!fact) continue;
774
+ logicalCount += 1;
775
+ if (this.index.locationForSequence(scanned.record.sequence)) continue;
776
+ pending.push({
777
+ fact,
778
+ location: {
779
+ sequence: scanned.record.sequence,
780
+ segmentId: scanned.record.segmentId,
781
+ frameOffset: scanned.record.frameOffset,
782
+ payloadLength: logicalPayloadLength(fact, scanned.record.payloadLength),
783
+ },
784
+ });
785
+ if (pending.length >= 256) {
786
+ this.index.insertBatch(pending.splice(0));
787
+ }
788
+ recoveredRecords += 1;
789
+ }
790
+ if (pending.length > 0) this.index.insertBatch(pending);
791
+ return { logicalCount, recoveredRecords };
792
+ }
793
+
794
+ pruneEvictedProjection(partitionStatuses = []) {
795
+ let pruned = 0;
796
+ for (let id = 0; id < PARTITIONS.length; id += 1) {
797
+ const status = partitionStatuses[id];
798
+ if (!status || Number(status.evictedRecords || 0) === 0 || Number(status.firstSequence || 0) <= 0) continue;
799
+ const firstSequence = Number(status.firstSequence);
800
+ if (firstSequence <= this.lastPrunedFirstSequence[id]) continue;
801
+ pruned += this.index.prunePartitionBefore(PARTITIONS[id], firstSequence);
802
+ this.lastPrunedFirstSequence[id] = firstSequence;
803
+ }
804
+ return pruned;
805
+ }
806
+
807
+ maintainProjectionBudget({ force = false } = {}) {
808
+ if (!this.ownsIndex) return { checked: false, degraded: false };
809
+ this.projectionWritesSinceCheck += 1;
810
+ if (!force && this.projectionWritesSinceCheck < this.projectionCheckInterval) {
811
+ return { checked: false, degraded: false };
812
+ }
813
+ this.projectionWritesSinceCheck = 0;
814
+ if (
815
+ this.index instanceof MmapScanIndex
816
+ && !this.index.base
817
+ && this.projectionFallback?.cleanupPending
818
+ ) {
819
+ this.projectionFallback.lastCleanupRetryAtMs = Date.now();
820
+ this.retryProjectionCleanup(this.projectionFallback);
821
+ }
822
+ const beforeBytes = directoryBytes(this.indexDirectory);
823
+ if (beforeBytes <= this.projectionBudgetBytes) {
824
+ return { checked: true, degraded: false, beforeBytes, afterBytes: beforeBytes };
825
+ }
826
+ let reclaimed;
827
+ try {
828
+ reclaimed = this.index.reclaim({ force: true });
829
+ } catch (error) {
830
+ this.activateMmapScanProjection(error, 'projection-reclaim', { forceScanOnly: true });
831
+ return {
832
+ checked: true,
833
+ degraded: true,
834
+ beforeBytes,
835
+ afterBytes: directoryBytes(this.indexDirectory),
836
+ };
837
+ }
838
+ const afterBytes = directoryBytes(this.indexDirectory);
839
+ if (afterBytes <= this.projectionBudgetBytes) {
840
+ return { checked: true, degraded: false, beforeBytes, afterBytes, reclaimed };
841
+ }
842
+ const error = new Error(
843
+ `SQLite projection uses ${afterBytes} bytes, exceeding its independent ${this.projectionBudgetBytes}-byte budget`,
844
+ );
845
+ error.code = 'sqlite_projection_budget_exceeded';
846
+ error.projectionBytes = afterBytes;
847
+ error.projectionBudgetBytes = this.projectionBudgetBytes;
848
+ this.activateMmapScanProjection(error, 'projection-budget', { forceScanOnly: true });
849
+ return {
850
+ checked: true,
851
+ degraded: true,
852
+ beforeBytes,
853
+ afterBytes: directoryBytes(this.indexDirectory),
854
+ reclaimed,
855
+ };
856
+ }
857
+
858
+ activateMmapScanProjection(
859
+ error,
860
+ phase,
861
+ { failedSequence = null, forceScanOnly = false } = {},
862
+ ) {
863
+ if (!this.ownsIndex) throw error;
864
+ const oldIndex = this.index;
865
+ if (oldIndex instanceof MmapScanIndex && !oldIndex.base) {
866
+ const fallback = this.projectionFallback || {
867
+ code: projectionErrorCode(error),
868
+ message: String(error?.message || error),
869
+ phase,
870
+ mode: 'mmap-scan',
871
+ activatedAtMs: Date.now(),
872
+ };
873
+ fallback.lastCleanupRetryAtMs = Date.now();
874
+ this.retryProjectionCleanup(fallback);
875
+ this.projectionFallback = fallback;
876
+ return fallback;
877
+ }
878
+ const cursorSecret = this.cursorSecret || crypto.randomBytes(32).toString('base64url');
879
+ const storeId = this.storeId || crypto.randomUUID();
880
+ const fallback = {
881
+ code: projectionErrorCode(error),
882
+ message: String(error?.message || error),
883
+ phase,
884
+ activatedAtMs: Date.now(),
885
+ };
886
+ let retainedBase = null;
887
+ let baseHighWater = 0;
888
+ if (!forceScanOnly && oldIndex && !(oldIndex instanceof MmapScanIndex)) {
889
+ try {
890
+ const engineStatus = this.engine.status();
891
+ const baseStatus = oldIndex.status();
892
+ const expectedMax = failedSequence === null
893
+ ? Math.max(0, Number(engineStatus.nextSequence || 1) - 1)
894
+ : Math.max(0, Number(failedSequence) - 1);
895
+ const expectedCount = failedSequence === null
896
+ ? Number(engineStatus.recordCount || 0)
897
+ : Math.max(0, Number(engineStatus.recordCount || 0) - 1);
898
+ oldIndex.query({ afterSequence: 0, limit: 1 });
899
+ if (
900
+ Number(baseStatus.maxSequence || 0) === expectedMax
901
+ && Number(baseStatus.count || 0) === expectedCount
902
+ && directoryBytes(this.indexDirectory) <= this.projectionBudgetBytes
903
+ ) {
904
+ retainedBase = oldIndex;
905
+ baseHighWater = expectedMax;
906
+ }
907
+ } catch (_) {
908
+ retainedBase = null;
909
+ baseHighWater = 0;
910
+ }
911
+ }
912
+
913
+ // Construct the bounded-memory candidate before touching the current
914
+ // projection. The pointer swap is therefore atomic from readers' view.
915
+ const candidate = new MmapScanIndex({
916
+ engine: this.engine,
917
+ decodeFact: decodeFactPayload,
918
+ base: retainedBase,
919
+ baseHighWater,
920
+ reason: fallback,
921
+ budgetBytes: this.projectionBudgetBytes,
922
+ });
923
+ candidate.setMetadata('cursor-secret', cursorSecret);
924
+ candidate.setMetadata('store-id', storeId);
925
+ this.index = candidate;
926
+ this.cursorSecret = cursorSecret;
927
+ this.storeId = storeId;
928
+ fallback.mode = retainedBase ? 'sqlite-base+mmap-tail' : 'mmap-scan';
929
+ this.projectionFallback = fallback;
930
+ this.projectionWritesSinceCheck = 0;
931
+ this.recovery = {
932
+ ...(this.recovery || {}),
933
+ rebuilt: false,
934
+ recoveredRecords: 0,
935
+ prunedRecords: 0,
936
+ projectionMode: fallback.mode,
937
+ };
938
+
939
+ if (!retainedBase) {
940
+ try {
941
+ oldIndex?.close();
942
+ } catch (closeError) {
943
+ fallback.closeError = String(closeError?.message || closeError);
944
+ }
945
+ try {
946
+ // Only the non-authoritative Host projection is removed. mmap facts
947
+ // and every mobile App sandbox are outside this directory.
948
+ this.retryProjectionCleanup(fallback);
949
+ } catch (_) {
950
+ // retryProjectionCleanup records the failure without affecting facts.
951
+ }
952
+ }
953
+ return fallback;
954
+ }
955
+
956
+ retryProjectionCleanup(fallback) {
957
+ try {
958
+ this.removeProjection();
959
+ delete fallback.removeError;
960
+ } catch (removeError) {
961
+ fallback.removeError = String(removeError?.message || removeError);
962
+ }
963
+ fallback.residualProjectionBytes = directoryBytes(this.indexDirectory);
964
+ fallback.cleanupPending = fallback.residualProjectionBytes > 0;
965
+ return !fallback.cleanupPending;
966
+ }
967
+
968
+ assertOpen() {
969
+ if (this.closed) throw new Error('SegmentedFactStoreAdapter is closed');
970
+ }
971
+ }
972
+
973
+ function projectionErrorCode(error) {
974
+ const message = String(error?.message || error || '');
975
+ if (/database or disk is full/i.test(message) || error?.code === 'SQLITE_FULL') {
976
+ return 'sqlite_projection_capacity';
977
+ }
978
+ return String(error?.code || error?.name || 'sqlite_projection_failure');
979
+ }
980
+
981
+ function createSegmentedFactStoreAdapter(options = {}) {
982
+ if (normalizeProfile(options.profile) === 'auto') {
983
+ const directory = path.resolve(options.directory || defaultSegmentedFactStorePath());
984
+ const storageCapacity = normalizeStorageCapacity(
985
+ options.storageCapacity || readStorageCapacity(directory),
986
+ );
987
+ if (storageCapacity.availableBytes < FACT_STORE_PROFILES['64mb'] + MINIMUM_SAFETY_RESERVE) {
988
+ return new DisabledSegmentedFactStoreAdapter({ directory, storageCapacity });
989
+ }
990
+ }
991
+ return new SegmentedFactStoreAdapter(options);
992
+ }
993
+
994
+ function loadNativeBinding() {
995
+ try {
996
+ return require('@mobileaidev/segmented-fact-store-native');
997
+ } catch (packageError) {
998
+ const localPath = path.resolve(__dirname, '../../../native/segmented-fact-store');
999
+ try {
1000
+ return require(localPath);
1001
+ } catch (localError) {
1002
+ const error = new Error(`segmented mmap native binding is unavailable: ${localError.message}`);
1003
+ error.code = 'segmented_native_unavailable';
1004
+ error.packageError = packageError;
1005
+ error.cause = localError;
1006
+ throw error;
1007
+ }
1008
+ }
1009
+ }
1010
+
1011
+ function partitionQuotas(budgetBytes, segmentSize) {
1012
+ const quotas = PARTITIONS.map((partition) => {
1013
+ const target = Math.floor(budgetBytes * PARTITION_WEIGHTS[partition] / 100);
1014
+ return Math.max(segmentSize, Math.floor(target / segmentSize) * segmentSize);
1015
+ });
1016
+ const allocated = quotas.reduce((sum, quota) => sum + quota, 0);
1017
+ if (allocated > budgetBytes) {
1018
+ throw new RangeError('budgetBytes is too small for one segment in every partition');
1019
+ }
1020
+ return quotas;
1021
+ }
1022
+
1023
+ function normalizePartitionQuotas(value, segmentSize) {
1024
+ if (!Array.isArray(value) || value.length !== PARTITIONS.length) {
1025
+ throw new TypeError(`partitionQuotas must contain ${PARTITIONS.length} entries`);
1026
+ }
1027
+ return value.map((quota, index) => {
1028
+ const number = positiveInteger(quota, undefined, `partitionQuotas[${index}]`);
1029
+ if (number < segmentSize || number % segmentSize !== 0) {
1030
+ throw new TypeError(`partitionQuotas[${index}] must be a segmentSize multiple`);
1031
+ }
1032
+ return number;
1033
+ });
1034
+ }
1035
+
1036
+ function normalizeReadQuery(query) {
1037
+ if (!query || typeof query !== 'object' || Array.isArray(query)) throw new TypeError('query must be an object');
1038
+ const partitions = query.partitions === undefined
1039
+ ? []
1040
+ : [...new Set((Array.isArray(query.partitions) ? query.partitions : [query.partitions]).map(String))].sort();
1041
+ for (const partition of partitions) {
1042
+ if (PARTITION_IDS[partition] === undefined) throw new TypeError(`unknown partition: ${partition}`);
1043
+ }
1044
+ const limit = positiveInteger(query.limit, 100, 'limit');
1045
+ if (limit > 1000) throw new TypeError('limit must not exceed 1000');
1046
+ return {
1047
+ cursor: query.cursor === undefined || query.cursor === null ? null : query.cursor,
1048
+ throughCursor: query.throughCursor === undefined || query.throughCursor === null
1049
+ ? null
1050
+ : query.throughCursor,
1051
+ targetKey: nullableString(query.targetKey),
1052
+ partitions,
1053
+ runtimeEpoch: nullableString(query.runtimeEpoch),
1054
+ hasActionId: Object.prototype.hasOwnProperty.call(query, 'actionId'),
1055
+ actionId: nullableString(query.actionId),
1056
+ fromObservedAtMs: nullableInteger(query.fromObservedAtMs, 'fromObservedAtMs'),
1057
+ toObservedAtMs: nullableInteger(query.toObservedAtMs, 'toObservedAtMs'),
1058
+ limit,
1059
+ };
1060
+ }
1061
+
1062
+ function cursorScope(query) {
1063
+ return crypto.createHash('sha256').update(JSON.stringify({
1064
+ targetKey: query.targetKey,
1065
+ partitions: query.partitions,
1066
+ runtimeEpoch: query.runtimeEpoch,
1067
+ hasActionId: query.hasActionId,
1068
+ actionId: query.actionId,
1069
+ })).digest('base64url').slice(0, 22);
1070
+ }
1071
+
1072
+ function encodeCursor(sequence, scope, secret, storeId) {
1073
+ const payload = Buffer.from(JSON.stringify({ v: 1, s: sequence, q: scope, i: storeId })).toString('base64url');
1074
+ const signature = crypto.createHmac('sha256', secret).update(payload).digest('base64url').slice(0, 22);
1075
+ return `fs1.${payload}.${signature}`;
1076
+ }
1077
+
1078
+ function decodeCursor(cursor, scope, secret, storeId) {
1079
+ if (typeof cursor !== 'string') return { ok: false, message: 'Cursor must be a string.' };
1080
+ const match = /^fs1\.([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)$/.exec(cursor);
1081
+ if (!match) return { ok: false, message: 'Cursor format is invalid.' };
1082
+ const expected = crypto.createHmac('sha256', secret).update(match[1]).digest('base64url').slice(0, 22);
1083
+ const expectedBuffer = Buffer.from(expected);
1084
+ const providedBuffer = Buffer.from(match[2]);
1085
+ if (expectedBuffer.length !== providedBuffer.length || !crypto.timingSafeEqual(expectedBuffer, providedBuffer)) {
1086
+ return { ok: false, message: 'Cursor signature is invalid.' };
1087
+ }
1088
+ try {
1089
+ const payload = JSON.parse(Buffer.from(match[1], 'base64url').toString('utf8'));
1090
+ if (
1091
+ payload.v !== 1
1092
+ || !Number.isSafeInteger(payload.s)
1093
+ || payload.s < 0
1094
+ || payload.q !== scope
1095
+ || payload.i !== storeId
1096
+ ) return { ok: false, message: 'Cursor scope is invalid.' };
1097
+ return { ok: true, sequence: payload.s };
1098
+ } catch (_) {
1099
+ return { ok: false, message: 'Cursor payload is invalid.' };
1100
+ }
1101
+ }
1102
+
1103
+ function cursorGap(sequence, requestedPartitions, statuses = []) {
1104
+ const selected = requestedPartitions.length > 0
1105
+ ? requestedPartitions
1106
+ : PARTITIONS;
1107
+ const gaps = [];
1108
+ for (const partition of selected) {
1109
+ const status = statuses[PARTITION_IDS[partition]];
1110
+ if (!status || Number(status.evictedRecords || 0) === 0) continue;
1111
+ const first = Number(status.firstSequence || 0);
1112
+ if (first > 0 && sequence < first - 1) {
1113
+ gaps.push({ partition, firstAvailableSequence: first, evictedRecords: Number(status.evictedRecords) });
1114
+ }
1115
+ }
1116
+ return gaps.length > 0 ? gaps : null;
1117
+ }
1118
+
1119
+ function createLargeFactManifest({ normalized, digest, byteLength, chunks }) {
1120
+ return {
1121
+ __aiAppBridgeInternal: LARGE_FACT_MANIFEST_KIND,
1122
+ index: factIndexMetadata(normalized),
1123
+ content: {
1124
+ encoding: 'json-utf8',
1125
+ byteLength,
1126
+ sha256: digest.toString('hex'),
1127
+ chunks,
1128
+ },
1129
+ };
1130
+ }
1131
+
1132
+ function nativeFrameLength(payloadLength) {
1133
+ return Math.ceil((24 + Number(payloadLength)) / 8) * 8 + 8;
1134
+ }
1135
+
1136
+ function encodeLargeFactChunk({ digest, ordinal, chunkCount, totalLength, bytes }) {
1137
+ const payload = Buffer.allocUnsafe(LARGE_FACT_CHUNK_HEADER_BYTES + bytes.length);
1138
+ LARGE_FACT_CHUNK_MAGIC.copy(payload, 0);
1139
+ payload.writeUInt32LE(LARGE_FACT_CHUNK_VERSION, 8);
1140
+ payload.writeUInt32LE(LARGE_FACT_CHUNK_HEADER_BYTES, 12);
1141
+ digest.copy(payload, 16);
1142
+ payload.writeUInt32LE(ordinal, 48);
1143
+ payload.writeUInt32LE(chunkCount, 52);
1144
+ payload.writeBigUInt64LE(BigInt(totalLength), 56);
1145
+ payload.writeUInt32LE(bytes.length, 64);
1146
+ payload.writeUInt32LE(0, 68);
1147
+ bytes.copy(payload, LARGE_FACT_CHUNK_HEADER_BYTES);
1148
+ return payload;
1149
+ }
1150
+
1151
+ function decodeLargeFactChunk(payload, sequence) {
1152
+ try {
1153
+ if (payload.length < LARGE_FACT_CHUNK_HEADER_BYTES) throw new Error('chunk header is truncated');
1154
+ if (!payload.subarray(0, LARGE_FACT_CHUNK_MAGIC.length).equals(LARGE_FACT_CHUNK_MAGIC)) {
1155
+ throw new Error('chunk magic is invalid');
1156
+ }
1157
+ if (payload.readUInt32LE(8) !== LARGE_FACT_CHUNK_VERSION) throw new Error('chunk version is unsupported');
1158
+ if (payload.readUInt32LE(12) !== LARGE_FACT_CHUNK_HEADER_BYTES) throw new Error('chunk header length is invalid');
1159
+ const totalLength = Number(payload.readBigUInt64LE(56));
1160
+ if (!Number.isSafeInteger(totalLength) || totalLength <= 0) throw new Error('chunk total length is invalid');
1161
+ const byteLength = payload.readUInt32LE(64);
1162
+ if (byteLength !== payload.length - LARGE_FACT_CHUNK_HEADER_BYTES) {
1163
+ throw new Error('chunk byte length does not match its payload');
1164
+ }
1165
+ if (payload.readUInt32LE(52) === 0) throw new Error('chunk count is invalid');
1166
+ if (payload.readUInt32LE(68) !== 0) throw new Error('chunk reserved bytes are not zero');
1167
+ return {
1168
+ digest: Buffer.from(payload.subarray(16, 48)),
1169
+ ordinal: payload.readUInt32LE(48),
1170
+ chunkCount: payload.readUInt32LE(52),
1171
+ totalLength,
1172
+ byteLength,
1173
+ bytes: payload.subarray(LARGE_FACT_CHUNK_HEADER_BYTES),
1174
+ };
1175
+ } catch (error) {
1176
+ throw invalidLargeFactError(sequence, `physical chunk is invalid: ${error.message}`, error);
1177
+ }
1178
+ }
1179
+
1180
+ function decodeFactPayload(payload, sequence) {
1181
+ const decoded = decodePhysicalFactPayload(payload, sequence);
1182
+ return decoded.kind === 'chunk' ? null : decoded.fact;
1183
+ }
1184
+
1185
+ function decodePhysicalFactPayload(payload, sequence) {
1186
+ if (
1187
+ payload.length >= LARGE_FACT_CHUNK_MAGIC.length
1188
+ && payload.subarray(0, LARGE_FACT_CHUNK_MAGIC.length).equals(LARGE_FACT_CHUNK_MAGIC)
1189
+ ) {
1190
+ return { kind: 'chunk' };
1191
+ }
1192
+ try {
1193
+ const fact = JSON.parse(payload.toString('utf8'));
1194
+ if (fact?.__aiAppBridgeInternal === LARGE_FACT_MANIFEST_KIND) {
1195
+ const manifest = validateLargeFactManifest(fact, sequence);
1196
+ const indexFact = { ...manifest.index, timestamps: { ...manifest.index.timestamps } };
1197
+ Object.defineProperty(indexFact, LARGE_FACT_LOGICAL_BYTES, {
1198
+ configurable: false,
1199
+ enumerable: false,
1200
+ value: manifest.content.byteLength,
1201
+ writable: false,
1202
+ });
1203
+ return {
1204
+ kind: 'manifest',
1205
+ fact: indexFact,
1206
+ manifest,
1207
+ byteLength: manifest.content.byteLength,
1208
+ };
1209
+ }
1210
+ validateFactShape(fact);
1211
+ return { kind: 'fact', fact, byteLength: payload.length };
1212
+ } catch (error) {
1213
+ if (error?.code === 'large_fact_invalid') throw error;
1214
+ const wrapped = new Error(`segment payload at sequence ${sequence} is invalid: ${error.message}`);
1215
+ wrapped.code = 'segment_payload_invalid';
1216
+ wrapped.sequence = sequence;
1217
+ wrapped.cause = error;
1218
+ throw wrapped;
1219
+ }
1220
+ }
1221
+
1222
+ function decodeLogicalFact({ engine, partition, payload, sequence }) {
1223
+ const decoded = decodePhysicalFactPayload(payload, sequence);
1224
+ if (decoded.kind === 'chunk') {
1225
+ throw invalidLargeFactError(sequence, 'query projection points to a physical chunk');
1226
+ }
1227
+ if (decoded.kind === 'fact') return { fact: decoded.fact, byteLength: decoded.byteLength };
1228
+
1229
+ const { manifest } = decoded;
1230
+ const expectedDigest = Buffer.from(manifest.content.sha256, 'hex');
1231
+ const buffers = [];
1232
+ let totalLength = 0;
1233
+ for (const expected of manifest.content.chunks) {
1234
+ let scanned;
1235
+ try {
1236
+ scanned = engine.readAt({
1237
+ partition,
1238
+ sequence: expected.sequence,
1239
+ segmentId: expected.segmentId,
1240
+ frameOffset: expected.frameOffset,
1241
+ });
1242
+ } catch (error) {
1243
+ if (error?.code === 'segment_location_expired') throw error;
1244
+ throw invalidLargeFactError(sequence, `chunk ${expected.ordinal} cannot be read: ${error.message}`, error);
1245
+ }
1246
+ if (scanned.record.payloadLength !== expected.payloadLength) {
1247
+ throw invalidLargeFactError(sequence, `chunk ${expected.ordinal} physical length changed`);
1248
+ }
1249
+ const chunk = decodeLargeFactChunk(scanned.payload, expected.sequence);
1250
+ if (
1251
+ chunk.ordinal !== expected.ordinal
1252
+ || chunk.chunkCount !== manifest.content.chunks.length
1253
+ || chunk.totalLength !== manifest.content.byteLength
1254
+ || chunk.byteLength !== expected.byteLength
1255
+ || !chunk.digest.equals(expectedDigest)
1256
+ ) {
1257
+ throw invalidLargeFactError(sequence, `chunk ${expected.ordinal} metadata does not match the manifest`);
1258
+ }
1259
+ buffers.push(chunk.bytes);
1260
+ totalLength += chunk.byteLength;
1261
+ }
1262
+ if (totalLength !== manifest.content.byteLength) {
1263
+ throw invalidLargeFactError(sequence, 'chunk lengths do not match the manifest total');
1264
+ }
1265
+ const assembled = Buffer.concat(buffers, totalLength);
1266
+ const actualDigest = crypto.createHash('sha256').update(assembled).digest();
1267
+ if (!actualDigest.equals(expectedDigest)) {
1268
+ throw invalidLargeFactError(sequence, 'assembled fact SHA-256 does not match the manifest');
1269
+ }
1270
+ const logical = decodePhysicalFactPayload(assembled, sequence);
1271
+ if (logical.kind !== 'fact') {
1272
+ throw invalidLargeFactError(sequence, 'assembled payload is not a canonical fact');
1273
+ }
1274
+ if (JSON.stringify(factIndexMetadata(logical.fact)) !== JSON.stringify(manifest.index)) {
1275
+ throw invalidLargeFactError(sequence, 'assembled fact metadata does not match the manifest');
1276
+ }
1277
+ return { fact: logical.fact, byteLength: assembled.length };
1278
+ }
1279
+
1280
+ function validateLargeFactManifest(value, sequence) {
1281
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
1282
+ throw invalidLargeFactError(sequence, 'manifest must be an object');
1283
+ }
1284
+ validateFactShape(value.index);
1285
+ const content = value.content;
1286
+ if (!content || typeof content !== 'object' || Array.isArray(content)) {
1287
+ throw invalidLargeFactError(sequence, 'manifest content is missing');
1288
+ }
1289
+ if (content.encoding !== 'json-utf8') throw invalidLargeFactError(sequence, 'manifest encoding is invalid');
1290
+ if (!Number.isSafeInteger(content.byteLength) || content.byteLength <= 0) {
1291
+ throw invalidLargeFactError(sequence, 'manifest byte length is invalid');
1292
+ }
1293
+ if (typeof content.sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(content.sha256)) {
1294
+ throw invalidLargeFactError(sequence, 'manifest SHA-256 is invalid');
1295
+ }
1296
+ if (!Array.isArray(content.chunks) || content.chunks.length === 0) {
1297
+ throw invalidLargeFactError(sequence, 'manifest chunks are missing');
1298
+ }
1299
+ let priorSequence = 0;
1300
+ let totalLength = 0;
1301
+ const chunks = content.chunks.map((chunk, ordinal) => {
1302
+ if (!chunk || typeof chunk !== 'object' || Array.isArray(chunk)) {
1303
+ throw invalidLargeFactError(sequence, `manifest chunk ${ordinal} is invalid`);
1304
+ }
1305
+ const normalized = {
1306
+ ordinal: nonNegativeInteger(chunk.ordinal, undefined, `manifest chunk ${ordinal} ordinal`),
1307
+ sequence: positiveInteger(chunk.sequence, undefined, `manifest chunk ${ordinal} sequence`),
1308
+ segmentId: nonNegativeInteger(chunk.segmentId, undefined, `manifest chunk ${ordinal} segmentId`),
1309
+ frameOffset: nonNegativeInteger(chunk.frameOffset, undefined, `manifest chunk ${ordinal} frameOffset`),
1310
+ payloadLength: positiveInteger(chunk.payloadLength, undefined, `manifest chunk ${ordinal} payloadLength`),
1311
+ byteLength: positiveInteger(chunk.byteLength, undefined, `manifest chunk ${ordinal} byteLength`),
1312
+ };
1313
+ if (normalized.ordinal !== ordinal) throw invalidLargeFactError(sequence, `manifest chunk ordinal ${ordinal} is missing`);
1314
+ if (normalized.sequence <= priorSequence || normalized.sequence >= sequence) {
1315
+ throw invalidLargeFactError(sequence, `manifest chunk ${ordinal} sequence is out of order`);
1316
+ }
1317
+ priorSequence = normalized.sequence;
1318
+ totalLength += normalized.byteLength;
1319
+ return normalized;
1320
+ });
1321
+ if (totalLength !== content.byteLength) {
1322
+ throw invalidLargeFactError(sequence, 'manifest chunk lengths do not match its total');
1323
+ }
1324
+ return {
1325
+ __aiAppBridgeInternal: LARGE_FACT_MANIFEST_KIND,
1326
+ index: factIndexMetadata(value.index),
1327
+ content: {
1328
+ encoding: 'json-utf8',
1329
+ byteLength: content.byteLength,
1330
+ sha256: content.sha256,
1331
+ chunks,
1332
+ },
1333
+ };
1334
+ }
1335
+
1336
+ function validateFactShape(fact) {
1337
+ if (!fact || typeof fact !== 'object' || Array.isArray(fact) || !fact.timestamps) {
1338
+ throw new Error('required fact fields are missing');
1339
+ }
1340
+ for (const key of ['partition', 'targetKey', 'runtimeEpoch']) {
1341
+ if (fact[key] === undefined || fact[key] === null || String(fact[key]).length === 0) {
1342
+ throw new Error('required fact fields are missing');
1343
+ }
1344
+ }
1345
+ for (const key of ['occurredAtMs', 'observedAtMs', 'ingestedAtMs']) {
1346
+ if (!Number.isSafeInteger(Number(fact.timestamps[key]))) {
1347
+ throw new Error(`fact timestamp ${key} is invalid`);
1348
+ }
1349
+ }
1350
+ }
1351
+
1352
+ function factIndexMetadata(fact) {
1353
+ validateFactShape(fact);
1354
+ return {
1355
+ partition: String(fact.partition),
1356
+ targetKey: String(fact.targetKey),
1357
+ runtimeEpoch: String(fact.runtimeEpoch),
1358
+ actionId: fact.actionId === undefined || fact.actionId === null ? null : String(fact.actionId),
1359
+ dedupeKey: fact.dedupeKey === undefined || fact.dedupeKey === null ? null : String(fact.dedupeKey),
1360
+ timestamps: {
1361
+ occurredAtMs: Number(fact.timestamps.occurredAtMs),
1362
+ observedAtMs: Number(fact.timestamps.observedAtMs),
1363
+ ingestedAtMs: Number(fact.timestamps.ingestedAtMs),
1364
+ },
1365
+ };
1366
+ }
1367
+
1368
+ function logicalPayloadLength(fact, fallback) {
1369
+ const value = Number(fact?.[LARGE_FACT_LOGICAL_BYTES]);
1370
+ return Number.isSafeInteger(value) && value > 0 ? value : Number(fallback);
1371
+ }
1372
+
1373
+ function metadataNonNegativeInteger(index, key) {
1374
+ const raw = index.metadata(key);
1375
+ if (raw === undefined || raw === null || raw === '') return null;
1376
+ const value = Number(raw);
1377
+ return Number.isSafeInteger(value) && value >= 0 ? value : null;
1378
+ }
1379
+
1380
+ function invalidLargeFactError(sequence, message, cause) {
1381
+ const error = new Error(`large fact at sequence ${sequence} is invalid: ${message}`);
1382
+ error.code = 'large_fact_invalid';
1383
+ error.sequence = sequence;
1384
+ if (cause) error.cause = cause;
1385
+ return error;
1386
+ }
1387
+
1388
+ function numericRecord(record) {
1389
+ return {
1390
+ ...record,
1391
+ sequence: safeNumber(record.sequence, 'sequence'),
1392
+ segmentId: safeNumber(record.segmentId, 'segmentId'),
1393
+ frameOffset: safeNumber(record.frameOffset, 'frameOffset'),
1394
+ gapFirstSequence: safeNumber(record.gapFirstSequence, 'gapFirstSequence'),
1395
+ gapLastSequence: safeNumber(record.gapLastSequence, 'gapLastSequence'),
1396
+ };
1397
+ }
1398
+
1399
+ function numericCursor(cursor) {
1400
+ return {
1401
+ ...cursor,
1402
+ afterSequence: safeNumber(cursor.afterSequence, 'afterSequence'),
1403
+ segmentId: safeNumber(cursor.segmentId, 'segmentId'),
1404
+ offset: safeNumber(cursor.offset, 'offset'),
1405
+ };
1406
+ }
1407
+
1408
+ function numericTree(value) {
1409
+ if (typeof value === 'bigint') return safeNumber(value, 'native status');
1410
+ if (Array.isArray(value)) return value.map(numericTree);
1411
+ if (!value || typeof value !== 'object') return value;
1412
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, numericTree(child)]));
1413
+ }
1414
+
1415
+ function safeNumber(value, name) {
1416
+ const number = typeof value === 'bigint' ? Number(value) : Number(value || 0);
1417
+ if (!Number.isSafeInteger(number) || number < 0) throw new RangeError(`${name} exceeds JavaScript safe integer range`);
1418
+ return number;
1419
+ }
1420
+
1421
+ function normalizeProfile(value) {
1422
+ const raw = value === undefined || value === null ? 'auto' : String(value).trim().toLowerCase();
1423
+ const profile = {
1424
+ default: '1gb',
1425
+ 1024: '1gb',
1426
+ '1024m': '1gb',
1427
+ '1g': '1gb',
1428
+ 512: '512mb',
1429
+ '512m': '512mb',
1430
+ 256: '256mb',
1431
+ '256m': '256mb',
1432
+ 64: '64mb',
1433
+ '64m': '64mb',
1434
+ }[raw] || raw;
1435
+ if (profile === 'auto') return profile;
1436
+ if (!FACT_STORE_PROFILES[profile]) throw new TypeError('profile must be auto, 1gb, 512mb, 256mb, or 64mb');
1437
+ return profile;
1438
+ }
1439
+
1440
+ function selectAutomaticProfile(storage) {
1441
+ if (storage.totalBytes >= 32 * GIB && storage.availableBytes >= 8 * GIB) return '1gb';
1442
+ if (storage.totalBytes >= 16 * GIB && storage.availableBytes >= 4 * GIB) return '512mb';
1443
+ if (storage.totalBytes >= 4 * GIB && storage.availableBytes >= 2 * GIB) return '256mb';
1444
+ return '64mb';
1445
+ }
1446
+
1447
+ function hasCommittedManifest(segmentsDirectory) {
1448
+ try {
1449
+ const status = fs.statSync(path.join(segmentsDirectory, '.sfs-manifest'));
1450
+ return status.isFile() && status.size > 0;
1451
+ } catch (error) {
1452
+ if (error?.code === 'ENOENT') return false;
1453
+ throw error;
1454
+ }
1455
+ }
1456
+
1457
+ function existingPartitionQuotas(engineStatus) {
1458
+ if (!Array.isArray(engineStatus?.partitions) || engineStatus.partitions.length !== PARTITIONS.length) {
1459
+ throw new Error('existing segmented store has an invalid partition layout');
1460
+ }
1461
+ return engineStatus.partitions.map((partition, index) => nonNegativeInteger(
1462
+ partition?.quotaBytes,
1463
+ undefined,
1464
+ `existing partitionQuotas[${index}]`,
1465
+ ));
1466
+ }
1467
+
1468
+ function profileForConfiguration(segmentSize, quotas) {
1469
+ for (const [profile, budgetBytes] of Object.entries(FACT_STORE_PROFILES)) {
1470
+ const expectedSegmentSize = SEGMENT_SIZE_BY_PROFILE[profile];
1471
+ if (segmentSize !== expectedSegmentSize) continue;
1472
+ const expectedQuotas = partitionQuotas(budgetBytes, expectedSegmentSize);
1473
+ if (expectedQuotas.every((quota, index) => quota === quotas[index])) return profile;
1474
+ }
1475
+ return null;
1476
+ }
1477
+
1478
+ function readStorageCapacity(directory) {
1479
+ let probe = path.resolve(directory);
1480
+ while (!fs.existsSync(probe)) {
1481
+ const parent = path.dirname(probe);
1482
+ if (parent === probe) throw new Error(`No existing parent for segmented FactStore directory: ${directory}`);
1483
+ probe = parent;
1484
+ }
1485
+ const stats = fs.statfsSync(probe);
1486
+ const blockSize = Number(stats.bsize);
1487
+ return { totalBytes: Number(stats.blocks) * blockSize, availableBytes: Number(stats.bavail) * blockSize };
1488
+ }
1489
+
1490
+ function normalizeStorageCapacity(value) {
1491
+ if (!value || typeof value !== 'object') throw new TypeError('storageCapacity must be an object');
1492
+ const totalBytes = positiveInteger(value.totalBytes, undefined, 'storageCapacity.totalBytes');
1493
+ const availableBytes = nonNegativeInteger(value.availableBytes, undefined, 'storageCapacity.availableBytes');
1494
+ if (availableBytes > totalBytes) throw new TypeError('storageCapacity.availableBytes cannot exceed totalBytes');
1495
+ return { totalBytes, availableBytes };
1496
+ }
1497
+
1498
+ function defaultSegmentedFactStorePath() {
1499
+ if (process.env.AI_APP_BRIDGE_FACT_STORE_PATH) return path.resolve(process.env.AI_APP_BRIDGE_FACT_STORE_PATH);
1500
+ if (process.platform === 'win32') {
1501
+ const root = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
1502
+ return path.join(root, 'ai-app-bridge', 'fact-store-v1');
1503
+ }
1504
+ if (process.platform === 'darwin') return path.join(os.homedir(), 'Library', 'Caches', 'ai-app-bridge', 'fact-store-v1');
1505
+ return path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'), 'ai-app-bridge', 'fact-store-v1');
1506
+ }
1507
+
1508
+ function directoryBytes(directory) {
1509
+ let total = 0;
1510
+ const pending = [directory];
1511
+ while (pending.length > 0) {
1512
+ const current = pending.pop();
1513
+ let entries;
1514
+ try {
1515
+ entries = fs.readdirSync(current, { withFileTypes: true });
1516
+ } catch (error) {
1517
+ if (error?.code === 'ENOENT') continue;
1518
+ throw error;
1519
+ }
1520
+ for (const entry of entries) {
1521
+ const child = path.join(current, entry.name);
1522
+ if (entry.isDirectory()) pending.push(child);
1523
+ else if (entry.isFile()) total += fs.statSync(child).size;
1524
+ }
1525
+ }
1526
+ return total;
1527
+ }
1528
+
1529
+ function restrictDirectory(directory) {
1530
+ if (process.platform !== 'win32') fs.chmodSync(directory, 0o700);
1531
+ }
1532
+
1533
+ function positiveInteger(value, fallback, name) {
1534
+ const number = value === undefined ? fallback : Number(value);
1535
+ if (!Number.isSafeInteger(number) || number <= 0) throw new TypeError(`${name} must be a positive safe integer`);
1536
+ return number;
1537
+ }
1538
+
1539
+ function nonNegativeInteger(value, fallback, name) {
1540
+ const number = value === undefined ? fallback : Number(value);
1541
+ if (!Number.isSafeInteger(number) || number < 0) throw new TypeError(`${name} must be a non-negative safe integer`);
1542
+ return number;
1543
+ }
1544
+
1545
+ function nullableString(value) {
1546
+ return value === undefined || value === null ? null : String(value);
1547
+ }
1548
+
1549
+ function nullableInteger(value, name) {
1550
+ if (value === undefined || value === null) return null;
1551
+ const number = Number(value);
1552
+ if (!Number.isSafeInteger(number)) throw new TypeError(`${name} must be a safe integer`);
1553
+ return number;
1554
+ }
1555
+
1556
+ module.exports = {
1557
+ DEFAULT_PROJECTION_BUDGET_BYTES,
1558
+ DisabledSegmentedFactStoreAdapter,
1559
+ FACT_STORE_PROFILES,
1560
+ LARGE_FACT_WIRE_V1,
1561
+ NativeSegmentEngine,
1562
+ PARTITIONS,
1563
+ PARTITION_IDS,
1564
+ PARTITION_WEIGHTS,
1565
+ SegmentedFactStoreAdapter,
1566
+ createSegmentedFactStoreAdapter,
1567
+ createLargeFactManifest,
1568
+ defaultSegmentedFactStorePath,
1569
+ encodeLargeFactChunk,
1570
+ partitionQuotas,
1571
+ };