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