@private.me/xcontinuity 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/AGENTS.md +123 -0
  2. package/LICENSE.md +26 -0
  3. package/MIGRATING.md +77 -0
  4. package/README.md +601 -0
  5. package/dist/adjudicator.d.ts +75 -0
  6. package/dist/adjudicator.js +184 -0
  7. package/dist/cascade.d.ts +157 -0
  8. package/dist/cascade.js +323 -0
  9. package/dist/chronicle.d.ts +76 -0
  10. package/dist/chronicle.js +173 -0
  11. package/dist/cjs/adjudicator.js +189 -0
  12. package/dist/cjs/cascade.js +328 -0
  13. package/dist/cjs/chronicle.js +178 -0
  14. package/dist/cjs/enforcement.js +108 -0
  15. package/dist/cjs/errors.js +72 -0
  16. package/dist/cjs/index.js +108 -0
  17. package/dist/cjs/memory-runtime.js +129 -0
  18. package/dist/cjs/memory-session.js +134 -0
  19. package/dist/cjs/mission.js +178 -0
  20. package/dist/cjs/package.json +1 -0
  21. package/dist/cjs/provenance.js +192 -0
  22. package/dist/cjs/ratification.js +322 -0
  23. package/dist/cjs/reverse-xorida.js +506 -0
  24. package/dist/cjs/session.js +273 -0
  25. package/dist/cjs/state-serializer.js +300 -0
  26. package/dist/cjs/store-memory.js +33 -0
  27. package/dist/cjs/trust.js +133 -0
  28. package/dist/cjs/types.js +59 -0
  29. package/dist/enforcement.d.ts +40 -0
  30. package/dist/enforcement.js +104 -0
  31. package/dist/errors.d.ts +25 -0
  32. package/dist/errors.js +68 -0
  33. package/dist/index.d.ts +34 -0
  34. package/dist/index.js +43 -0
  35. package/dist/memory-runtime.d.ts +36 -0
  36. package/dist/memory-runtime.js +125 -0
  37. package/dist/memory-session.d.ts +38 -0
  38. package/dist/memory-session.js +97 -0
  39. package/dist/mission.d.ts +68 -0
  40. package/dist/mission.js +172 -0
  41. package/dist/provenance.d.ts +54 -0
  42. package/dist/provenance.js +182 -0
  43. package/dist/ratification.d.ts +113 -0
  44. package/dist/ratification.js +317 -0
  45. package/dist/reverse-xorida.d.ts +174 -0
  46. package/dist/reverse-xorida.js +490 -0
  47. package/dist/session.d.ts +102 -0
  48. package/dist/session.js +269 -0
  49. package/dist/state-serializer.d.ts +37 -0
  50. package/dist/state-serializer.js +294 -0
  51. package/dist/store-memory.d.ts +18 -0
  52. package/dist/store-memory.js +29 -0
  53. package/dist/trust.d.ts +76 -0
  54. package/dist/trust.js +121 -0
  55. package/dist/types.d.ts +320 -0
  56. package/dist/types.js +56 -0
  57. package/llms.txt +43 -0
  58. package/package.json +125 -0
  59. package/share1.dat +0 -0
@@ -0,0 +1,269 @@
1
+ /**
2
+ * @private.me/xcontinuity — Session Manager
3
+ *
4
+ * Stateful controller combining all layers:
5
+ * - RuntimeMemory for ephemeral in-process state
6
+ * - SessionMemory for XorIDA-persisted state
7
+ * - Chronicle for ordered state history
8
+ *
9
+ * Status transitions: active -> suspended -> active (resume), active -> closed (terminal).
10
+ */
11
+ import { ok, err } from '@private.me/shared';
12
+ import { generateUUID } from '@private.me/crypto';
13
+ import { DEFAULT_SPLIT_CONFIG } from './types.js';
14
+ import { continuityError } from './errors.js';
15
+ import { SessionMemory } from './memory-session.js';
16
+ import { Chronicle } from './chronicle.js';
17
+ export class SessionManager {
18
+ sessionRecord;
19
+ state;
20
+ memory;
21
+ chronicle;
22
+ trustStore;
23
+ missionGuard;
24
+ enforcementLoop;
25
+ sequenceCounter;
26
+ constructor(sessionRecord, memory, chronicle, trustStore, missionGuard, enforcementLoop) {
27
+ this.sessionRecord = sessionRecord;
28
+ this.state = {};
29
+ this.memory = memory;
30
+ this.chronicle = chronicle;
31
+ this.trustStore = trustStore;
32
+ this.missionGuard = missionGuard;
33
+ this.enforcementLoop = enforcementLoop;
34
+ this.sequenceCounter = 0;
35
+ }
36
+ /**
37
+ * Create a new continuity session.
38
+ *
39
+ * @param config - Session configuration (agentId, store, optional splitConfig)
40
+ * @returns SessionManager instance
41
+ */
42
+ static create(config) {
43
+ const sessionId = generateUUID();
44
+ const now = Date.now();
45
+ const splitConfig = config.splitConfig ?? DEFAULT_SPLIT_CONFIG;
46
+ const sessionRecord = {
47
+ sessionId,
48
+ agentId: config.agentId,
49
+ status: 'active',
50
+ splitConfig,
51
+ createdAt: now,
52
+ updatedAt: now,
53
+ snapshotCount: 0,
54
+ };
55
+ const memory = new SessionMemory(config.store, splitConfig);
56
+ const chronicle = new Chronicle();
57
+ // Extract trust substrate components (optional)
58
+ const trustConfig = config;
59
+ return new SessionManager(sessionRecord, memory, chronicle, trustConfig.trustStore, trustConfig.missionGuard, trustConfig.enforcementLoop);
60
+ }
61
+ /** Get the current session record. */
62
+ get session() {
63
+ return this.sessionRecord;
64
+ }
65
+ /** Get a copy of the current agent state. */
66
+ get currentState() {
67
+ return { ...this.state };
68
+ }
69
+ /**
70
+ * Merge a partial update into the current state.
71
+ * Only works when session is active.
72
+ */
73
+ updateState(patch) {
74
+ if (this.sessionRecord.status !== 'active') {
75
+ return err(continuityError(this.sessionRecord.status === 'closed' ? 'SESSION_CLOSED' : 'SESSION_SUSPENDED', `Cannot update state: session is ${this.sessionRecord.status}`));
76
+ }
77
+ // If enforcement loop is active, check each key in the patch
78
+ if (this.enforcementLoop) {
79
+ for (const [key, value] of Object.entries(patch)) {
80
+ const checkResult = this.enforcementLoop.check({
81
+ author: this.sessionRecord.agentId,
82
+ type: 'write',
83
+ key,
84
+ value: value,
85
+ });
86
+ if (checkResult.ok && checkResult.value.decision !== 'allow') {
87
+ if (checkResult.value.decision === 'rewrite' && checkResult.value.rewrittenAction) {
88
+ // Apply rewritten value instead
89
+ patch = { ...patch, [key]: checkResult.value.rewrittenAction.value };
90
+ }
91
+ else {
92
+ return err(continuityError('CONSTRAINT_VIOLATION', `State update blocked for key "${key}": ${checkResult.value.reason}`));
93
+ }
94
+ }
95
+ }
96
+ }
97
+ // If trust store is present, route writes through it
98
+ if (this.trustStore) {
99
+ for (const [key, value] of Object.entries(patch)) {
100
+ this.trustStore.write(key, value);
101
+ }
102
+ }
103
+ this.state = { ...this.state, ...patch };
104
+ this.updateTimestamp();
105
+ return ok(undefined);
106
+ }
107
+ /**
108
+ * Replace the entire current state.
109
+ * Only works when session is active.
110
+ */
111
+ setState(state) {
112
+ if (this.sessionRecord.status !== 'active') {
113
+ return err(continuityError(this.sessionRecord.status === 'closed' ? 'SESSION_CLOSED' : 'SESSION_SUSPENDED', `Cannot set state: session is ${this.sessionRecord.status}`));
114
+ }
115
+ this.state = { ...state };
116
+ this.updateTimestamp();
117
+ return ok(undefined);
118
+ }
119
+ /**
120
+ * Persist current state as a XorIDA-split snapshot.
121
+ * Adds a chronicle entry for the snapshot.
122
+ *
123
+ * @param description - Optional human-readable description
124
+ * @param tags - Optional tags for filtering
125
+ * @returns StateSnapshot with stateId for later retrieval
126
+ */
127
+ async snapshot(description, tags) {
128
+ if (this.sessionRecord.status !== 'active') {
129
+ return err(continuityError(this.sessionRecord.status === 'closed' ? 'SESSION_CLOSED' : 'SESSION_SUSPENDED', `Cannot snapshot: session is ${this.sessionRecord.status}`));
130
+ }
131
+ const sequence = this.sequenceCounter++;
132
+ const snapshotTags = tags ?? [];
133
+ const parentStateId = this.chronicle.latest()?.stateId;
134
+ const metadata = {
135
+ agentId: this.sessionRecord.agentId,
136
+ sessionId: this.sessionRecord.sessionId,
137
+ sequenceNumber: sequence,
138
+ createdAt: Date.now(),
139
+ description,
140
+ tags: snapshotTags,
141
+ };
142
+ const result = await this.memory.save(this.state, metadata);
143
+ if (!result.ok)
144
+ return result;
145
+ // Add chronicle entry
146
+ const entry = {
147
+ stateId: result.value.stateId,
148
+ sessionId: this.sessionRecord.sessionId,
149
+ sequence,
150
+ timestamp: metadata.createdAt,
151
+ description,
152
+ tags: snapshotTags,
153
+ parentStateId,
154
+ };
155
+ this.chronicle.append(entry);
156
+ // Update session record
157
+ this.sessionRecord = {
158
+ ...this.sessionRecord,
159
+ snapshotCount: this.sessionRecord.snapshotCount + 1,
160
+ updatedAt: Date.now(),
161
+ };
162
+ return result;
163
+ }
164
+ /**
165
+ * Restore state from a specific snapshot.
166
+ * Sets the current state to the reconstructed state.
167
+ *
168
+ * @param stateId - The state ID to restore
169
+ * @returns Reconstructed AgentState
170
+ */
171
+ async restore(stateId) {
172
+ if (this.sessionRecord.status === 'closed') {
173
+ return err(continuityError('SESSION_CLOSED', 'Cannot restore: session is closed'));
174
+ }
175
+ const result = await this.memory.load(stateId);
176
+ if (!result.ok)
177
+ return result;
178
+ this.state = result.value;
179
+ this.updateTimestamp();
180
+ // If suspended, transition back to active
181
+ if (this.sessionRecord.status === 'suspended') {
182
+ this.sessionRecord = { ...this.sessionRecord, status: 'active' };
183
+ }
184
+ return ok({ ...result.value });
185
+ }
186
+ /**
187
+ * Restore the latest snapshot.
188
+ * Convenience method for restoring the most recent state.
189
+ */
190
+ async restoreLatest() {
191
+ const latest = this.chronicle.latest();
192
+ if (!latest) {
193
+ return err(continuityError('NO_SNAPSHOTS', 'No snapshots available to restore'));
194
+ }
195
+ return this.restore(latest.stateId);
196
+ }
197
+ /**
198
+ * Suspend the session. Persists current state before suspending.
199
+ * Can be resumed later.
200
+ */
201
+ async suspend() {
202
+ if (this.sessionRecord.status !== 'active') {
203
+ return err(continuityError(this.sessionRecord.status === 'closed' ? 'SESSION_CLOSED' : 'SESSION_SUSPENDED', `Cannot suspend: session is ${this.sessionRecord.status}`));
204
+ }
205
+ // Auto-snapshot before suspending
206
+ const snapResult = await this.snapshot('auto-suspend', ['suspend']);
207
+ if (!snapResult.ok)
208
+ return snapResult;
209
+ this.sessionRecord = {
210
+ ...this.sessionRecord,
211
+ status: 'suspended',
212
+ updatedAt: Date.now(),
213
+ };
214
+ return ok(undefined);
215
+ }
216
+ /**
217
+ * Resume a suspended session. Restores the latest snapshot.
218
+ */
219
+ async resume() {
220
+ if (this.sessionRecord.status !== 'suspended') {
221
+ if (this.sessionRecord.status === 'closed') {
222
+ return err(continuityError('SESSION_CLOSED', 'Cannot resume: session is closed'));
223
+ }
224
+ return err(continuityError('SESSION_ACTIVE', 'Session is already active'));
225
+ }
226
+ this.sessionRecord = {
227
+ ...this.sessionRecord,
228
+ status: 'active',
229
+ updatedAt: Date.now(),
230
+ };
231
+ return this.restoreLatest();
232
+ }
233
+ /**
234
+ * Close the session permanently. No further operations allowed.
235
+ */
236
+ close() {
237
+ if (this.sessionRecord.status === 'closed') {
238
+ return err(continuityError('SESSION_CLOSED', 'Session is already closed'));
239
+ }
240
+ this.sessionRecord = {
241
+ ...this.sessionRecord,
242
+ status: 'closed',
243
+ updatedAt: Date.now(),
244
+ };
245
+ return ok(undefined);
246
+ }
247
+ /** Get the chronicle for state history navigation. */
248
+ getChronicle() {
249
+ return this.chronicle;
250
+ }
251
+ /** Get the trust store (if configured). */
252
+ getTrustStore() {
253
+ return this.trustStore;
254
+ }
255
+ /** Get the mission guard (if configured). */
256
+ getMissionGuard() {
257
+ return this.missionGuard;
258
+ }
259
+ /** Get the enforcement loop (if configured). */
260
+ getEnforcementLoop() {
261
+ return this.enforcementLoop;
262
+ }
263
+ updateTimestamp() {
264
+ this.sessionRecord = {
265
+ ...this.sessionRecord,
266
+ updatedAt: Date.now(),
267
+ };
268
+ }
269
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @private.me/xcontinuity — State Serialization
3
+ *
4
+ * Pipeline: AgentState -> TLV byte stream -> SHA-256 checksum -> StateSnapshot
5
+ *
6
+ * TLV format: [Type:1][Length:4 BE][Value:N] per entry.
7
+ * Key-value pairs: [keyLen:2 BE][key:UTF8][valueType:1][value:typed]
8
+ * Value types: STRING(0x01), NUMBER(0x02/float64 BE), BOOLEAN(0x03/1byte),
9
+ * BYTES(0x04), NULL(0x05), JSON(0x06/UTF8).
10
+ */
11
+ import type { Result } from '@private.me/shared';
12
+ import type { AgentState, StateMetadata, StateSnapshot } from './types.js';
13
+ import type { ContinuityError } from './errors.js';
14
+ /**
15
+ * Serialize agent state and metadata into a TLV byte stream with SHA-256 checksum.
16
+ *
17
+ * @param state - Flat key-value state map
18
+ * @param metadata - State metadata (agentId, sessionId, sequence, etc.)
19
+ * @returns StateSnapshot with stateId, serialized bytes, and checksum
20
+ */
21
+ export declare function serializeState(state: AgentState, metadata: StateMetadata): Promise<Result<StateSnapshot, ContinuityError>>;
22
+ /**
23
+ * Deserialize a state snapshot back into an AgentState.
24
+ * Verifies SHA-256 checksum before returning.
25
+ *
26
+ * @param snapshot - StateSnapshot to deserialize
27
+ * @returns Reconstructed AgentState
28
+ */
29
+ export declare function deserializeState(snapshot: StateSnapshot): Promise<Result<AgentState, ContinuityError>>;
30
+ /**
31
+ * Compute SHA-256 checksum of data using Web Crypto API.
32
+ */
33
+ export declare function computeChecksum(data: Uint8Array): Promise<Uint8Array>;
34
+ /**
35
+ * Compare two AgentStates for equality by comparing sorted serialized keys and values.
36
+ */
37
+ export declare function statesEqual(a: AgentState, b: AgentState): boolean;
@@ -0,0 +1,294 @@
1
+ /**
2
+ * @private.me/xcontinuity — State Serialization
3
+ *
4
+ * Pipeline: AgentState -> TLV byte stream -> SHA-256 checksum -> StateSnapshot
5
+ *
6
+ * TLV format: [Type:1][Length:4 BE][Value:N] per entry.
7
+ * Key-value pairs: [keyLen:2 BE][key:UTF8][valueType:1][value:typed]
8
+ * Value types: STRING(0x01), NUMBER(0x02/float64 BE), BOOLEAN(0x03/1byte),
9
+ * BYTES(0x04), NULL(0x05), JSON(0x06/UTF8).
10
+ */
11
+ import { ok, err } from '@private.me/shared';
12
+ import { generateUUID } from '@private.me/crypto';
13
+ import { CONTINUITY_TLV, VALUE_TYPE } from './types.js';
14
+ import { continuityError } from './errors.js';
15
+ const TEXT_ENCODER = new TextEncoder();
16
+ const TEXT_DECODER = new TextDecoder();
17
+ /* ── TLV Primitives ── */
18
+ function encodeTlv(type, value) {
19
+ const entry = new Uint8Array(5 + value.length);
20
+ entry[0] = type;
21
+ const view = new DataView(entry.buffer, 1, 4);
22
+ view.setUint32(0, value.length);
23
+ entry.set(value, 5);
24
+ return entry;
25
+ }
26
+ function encodeUint32(n) {
27
+ const buf = new Uint8Array(4);
28
+ new DataView(buf.buffer).setUint32(0, n);
29
+ return buf;
30
+ }
31
+ function encodeFloat64(n) {
32
+ const buf = new Uint8Array(8);
33
+ new DataView(buf.buffer).setFloat64(0, n);
34
+ return buf;
35
+ }
36
+ function encodeString(s) {
37
+ return TEXT_ENCODER.encode(s);
38
+ }
39
+ function encodeKeyLen(key) {
40
+ const keyBytes = TEXT_ENCODER.encode(key);
41
+ const buf = new Uint8Array(2 + keyBytes.length);
42
+ new DataView(buf.buffer).setUint16(0, keyBytes.length);
43
+ buf.set(keyBytes, 2);
44
+ return buf;
45
+ }
46
+ /* ── Value Encoding ── */
47
+ function encodeValue(value) {
48
+ if (value === null) {
49
+ return new Uint8Array([VALUE_TYPE.NULL]);
50
+ }
51
+ if (typeof value === 'string') {
52
+ const strBytes = TEXT_ENCODER.encode(value);
53
+ const buf = new Uint8Array(1 + strBytes.length);
54
+ buf[0] = VALUE_TYPE.STRING;
55
+ buf.set(strBytes, 1);
56
+ return buf;
57
+ }
58
+ if (typeof value === 'number') {
59
+ const buf = new Uint8Array(9);
60
+ buf[0] = VALUE_TYPE.NUMBER;
61
+ new DataView(buf.buffer).setFloat64(1, value);
62
+ return buf;
63
+ }
64
+ if (typeof value === 'boolean') {
65
+ return new Uint8Array([VALUE_TYPE.BOOLEAN, value ? 1 : 0]);
66
+ }
67
+ if (value instanceof Uint8Array) {
68
+ const buf = new Uint8Array(1 + value.length);
69
+ buf[0] = VALUE_TYPE.BYTES;
70
+ buf.set(value, 1);
71
+ return buf;
72
+ }
73
+ // Record<string, unknown> -> JSON
74
+ const jsonBytes = TEXT_ENCODER.encode(JSON.stringify(value));
75
+ const buf = new Uint8Array(1 + jsonBytes.length);
76
+ buf[0] = VALUE_TYPE.JSON;
77
+ buf.set(jsonBytes, 1);
78
+ return buf;
79
+ }
80
+ /* ── Value Decoding ── */
81
+ function decodeValue(data) {
82
+ if (data.length === 0) {
83
+ return err(continuityError('INVALID_VALUE_TYPE', 'Empty value data'));
84
+ }
85
+ const typeCode = data[0];
86
+ const payload = data.subarray(1);
87
+ switch (typeCode) {
88
+ case VALUE_TYPE.NULL:
89
+ return ok(null);
90
+ case VALUE_TYPE.STRING:
91
+ return ok(TEXT_DECODER.decode(payload));
92
+ case VALUE_TYPE.NUMBER: {
93
+ if (payload.length < 8) {
94
+ return err(continuityError('DESERIALIZE_FAILED', 'Number value too short'));
95
+ }
96
+ return ok(new DataView(payload.buffer, payload.byteOffset, 8).getFloat64(0));
97
+ }
98
+ case VALUE_TYPE.BOOLEAN:
99
+ return ok(payload[0] === 1);
100
+ case VALUE_TYPE.BYTES:
101
+ return ok(new Uint8Array(payload));
102
+ case VALUE_TYPE.JSON: {
103
+ try {
104
+ return ok(JSON.parse(TEXT_DECODER.decode(payload)));
105
+ }
106
+ catch {
107
+ return err(continuityError('DESERIALIZE_FAILED', 'Invalid JSON value'));
108
+ }
109
+ }
110
+ default:
111
+ return err(continuityError('INVALID_VALUE_TYPE', `Unknown value type: 0x${typeCode.toString(16)}`));
112
+ }
113
+ }
114
+ /* ── Entry Encoding ── */
115
+ function encodeEntry(key, value) {
116
+ const keyPart = encodeKeyLen(key);
117
+ const valuePart = encodeValue(value);
118
+ const combined = new Uint8Array(keyPart.length + valuePart.length);
119
+ combined.set(keyPart, 0);
120
+ combined.set(valuePart, keyPart.length);
121
+ return combined;
122
+ }
123
+ /* ── Metadata Encoding ── */
124
+ function encodeMetadata(metadata) {
125
+ const parts = [];
126
+ parts.push(encodeTlv(CONTINUITY_TLV.AGENT_ID, encodeString(metadata.agentId)));
127
+ parts.push(encodeTlv(CONTINUITY_TLV.SESSION_ID, encodeString(metadata.sessionId)));
128
+ parts.push(encodeTlv(CONTINUITY_TLV.SEQUENCE_NUMBER, encodeUint32(metadata.sequenceNumber)));
129
+ parts.push(encodeTlv(CONTINUITY_TLV.TIMESTAMP, encodeFloat64(metadata.createdAt)));
130
+ if (metadata.description) {
131
+ parts.push(encodeTlv(CONTINUITY_TLV.DESCRIPTION, encodeString(metadata.description)));
132
+ }
133
+ for (const tag of metadata.tags) {
134
+ parts.push(encodeTlv(CONTINUITY_TLV.TAG, encodeString(tag)));
135
+ }
136
+ return concatParts(parts);
137
+ }
138
+ /* ── Public API ── */
139
+ /**
140
+ * Serialize agent state and metadata into a TLV byte stream with SHA-256 checksum.
141
+ *
142
+ * @param state - Flat key-value state map
143
+ * @param metadata - State metadata (agentId, sessionId, sequence, etc.)
144
+ * @returns StateSnapshot with stateId, serialized bytes, and checksum
145
+ */
146
+ export async function serializeState(state, metadata) {
147
+ try {
148
+ const parts = [];
149
+ // Encode metadata header
150
+ const metaBytes = encodeMetadata(metadata);
151
+ parts.push(encodeTlv(CONTINUITY_TLV.STATE_METADATA, metaBytes));
152
+ // Encode state entries (sorted by key for deterministic output)
153
+ const sortedKeys = Object.keys(state).sort();
154
+ for (const key of sortedKeys) {
155
+ const value = state[key];
156
+ if (value === undefined)
157
+ continue;
158
+ const entryBytes = encodeEntry(key, value);
159
+ parts.push(encodeTlv(CONTINUITY_TLV.STATE_ENTRY, entryBytes));
160
+ }
161
+ const serializedBytes = concatParts(parts);
162
+ const checksum = await computeChecksum(serializedBytes);
163
+ const stateId = generateUUID();
164
+ return ok({
165
+ stateId,
166
+ metadata,
167
+ serializedBytes,
168
+ checksum,
169
+ });
170
+ }
171
+ catch (e) {
172
+ const msg = e instanceof Error ? e.message : 'Unknown serialization error';
173
+ return err(continuityError('SERIALIZE_FAILED', msg));
174
+ }
175
+ }
176
+ /**
177
+ * Deserialize a state snapshot back into an AgentState.
178
+ * Verifies SHA-256 checksum before returning.
179
+ *
180
+ * @param snapshot - StateSnapshot to deserialize
181
+ * @returns Reconstructed AgentState
182
+ */
183
+ export async function deserializeState(snapshot) {
184
+ // Verify checksum
185
+ const computed = await computeChecksum(snapshot.serializedBytes);
186
+ if (!bytesEqual(computed, snapshot.checksum)) {
187
+ return err(continuityError('CHECKSUM_MISMATCH', 'SHA-256 checksum verification failed'));
188
+ }
189
+ const state = {};
190
+ const data = snapshot.serializedBytes;
191
+ let offset = 0;
192
+ while (offset < data.length) {
193
+ if (offset + 5 > data.length) {
194
+ return err(continuityError('INVALID_TLV', 'Truncated TLV header'));
195
+ }
196
+ const type = data[offset];
197
+ const view = new DataView(data.buffer, data.byteOffset + offset + 1, 4);
198
+ const length = view.getUint32(0);
199
+ offset += 5;
200
+ if (offset + length > data.length) {
201
+ return err(continuityError('INVALID_TLV', `TLV value exceeds data at type 0x${type.toString(16)}`));
202
+ }
203
+ const value = data.subarray(offset, offset + length);
204
+ offset += length;
205
+ if (type === CONTINUITY_TLV.STATE_ENTRY) {
206
+ const entryResult = decodeEntry(value);
207
+ if (!entryResult.ok)
208
+ return entryResult;
209
+ const [key, val] = entryResult.value;
210
+ state[key] = val;
211
+ }
212
+ // Skip metadata and other TLV types during state reconstruction
213
+ }
214
+ return ok(state);
215
+ }
216
+ /**
217
+ * Compute SHA-256 checksum of data using Web Crypto API.
218
+ */
219
+ export async function computeChecksum(data) {
220
+ // Copy to a clean ArrayBuffer to satisfy Web Crypto API typing
221
+ const copy = new Uint8Array(data.length);
222
+ copy.set(data);
223
+ const hash = await crypto.subtle.digest('SHA-256', copy.buffer);
224
+ return new Uint8Array(hash);
225
+ }
226
+ /**
227
+ * Compare two AgentStates for equality by comparing sorted serialized keys and values.
228
+ */
229
+ export function statesEqual(a, b) {
230
+ const keysA = Object.keys(a).sort();
231
+ const keysB = Object.keys(b).sort();
232
+ if (keysA.length !== keysB.length)
233
+ return false;
234
+ for (let i = 0; i < keysA.length; i++) {
235
+ if (keysA[i] !== keysB[i])
236
+ return false;
237
+ const valA = a[keysA[i]];
238
+ const valB = b[keysB[i]];
239
+ if (!valuesEqual(valA, valB))
240
+ return false;
241
+ }
242
+ return true;
243
+ }
244
+ /* ── Helpers ── */
245
+ function decodeEntry(data) {
246
+ if (data.length < 3) {
247
+ return err(continuityError('DESERIALIZE_FAILED', 'Entry too short'));
248
+ }
249
+ const keyLen = new DataView(data.buffer, data.byteOffset, 2).getUint16(0);
250
+ if (2 + keyLen > data.length) {
251
+ return err(continuityError('DESERIALIZE_FAILED', 'Key length exceeds entry'));
252
+ }
253
+ const key = TEXT_DECODER.decode(data.subarray(2, 2 + keyLen));
254
+ const valueData = data.subarray(2 + keyLen);
255
+ const valueResult = decodeValue(valueData);
256
+ if (!valueResult.ok)
257
+ return valueResult;
258
+ return ok([key, valueResult.value]);
259
+ }
260
+ function concatParts(parts) {
261
+ const totalLen = parts.reduce((sum, p) => sum + p.length, 0);
262
+ const result = new Uint8Array(totalLen);
263
+ let offset = 0;
264
+ for (const part of parts) {
265
+ result.set(part, offset);
266
+ offset += part.length;
267
+ }
268
+ return result;
269
+ }
270
+ function bytesEqual(a, b) {
271
+ if (a.length !== b.length)
272
+ return false;
273
+ for (let i = 0; i < a.length; i++) {
274
+ if (a[i] !== b[i])
275
+ return false;
276
+ }
277
+ return true;
278
+ }
279
+ function valuesEqual(a, b) {
280
+ if (a === b)
281
+ return true;
282
+ if (a === undefined || b === undefined)
283
+ return false;
284
+ if (a === null || b === null)
285
+ return a === b;
286
+ if (a instanceof Uint8Array && b instanceof Uint8Array) {
287
+ return bytesEqual(a, b);
288
+ }
289
+ if (typeof a === 'object' && typeof b === 'object' &&
290
+ !(a instanceof Uint8Array) && !(b instanceof Uint8Array)) {
291
+ return JSON.stringify(a) === JSON.stringify(b);
292
+ }
293
+ return a === b;
294
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @private.me/xcontinuity — In-Memory StateStore
3
+ *
4
+ * Simple Map-based implementation of the StateStore interface.
5
+ * Async interface allows future disk/network backends without API changes.
6
+ */
7
+ import type { StateStore, SplitState } from './types.js';
8
+ export declare class MemoryStateStore implements StateStore {
9
+ private readonly store;
10
+ putShares(splitState: SplitState): Promise<void>;
11
+ getShares(stateId: string): Promise<SplitState | null>;
12
+ deleteShares(stateId: string): Promise<void>;
13
+ listStateIds(): Promise<string[]>;
14
+ /** Number of stored split states (useful for testing). */
15
+ get size(): number;
16
+ /** Clear all stored split states. */
17
+ clear(): void;
18
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @private.me/xcontinuity — In-Memory StateStore
3
+ *
4
+ * Simple Map-based implementation of the StateStore interface.
5
+ * Async interface allows future disk/network backends without API changes.
6
+ */
7
+ export class MemoryStateStore {
8
+ store = new Map();
9
+ async putShares(splitState) {
10
+ this.store.set(splitState.stateId, splitState);
11
+ }
12
+ async getShares(stateId) {
13
+ return this.store.get(stateId) ?? null;
14
+ }
15
+ async deleteShares(stateId) {
16
+ this.store.delete(stateId);
17
+ }
18
+ async listStateIds() {
19
+ return Array.from(this.store.keys());
20
+ }
21
+ /** Number of stored split states (useful for testing). */
22
+ get size() {
23
+ return this.store.size;
24
+ }
25
+ /** Clear all stored split states. */
26
+ clear() {
27
+ this.store.clear();
28
+ }
29
+ }