entroly-wasm 1.0.78 → 1.0.80
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/data/tuning_defaults.json +5 -5
- package/index.d.ts +8 -0
- package/index.js +90 -1
- package/js/continuity_contracts.d.ts +86 -0
- package/js/continuity_contracts.js +81 -0
- package/js/trust_engine.d.ts +18 -0
- package/js/trust_engine.js +31 -0
- package/js/work_context_snapshot_store.d.ts +43 -0
- package/js/work_context_snapshot_store.js +280 -0
- package/js/work_graph.d.ts +225 -0
- package/js/work_graph.js +191 -0
- package/js/work_graph_content_digest.js +151 -0
- package/js/work_graph_continuity.d.ts +54 -0
- package/js/work_graph_continuity.js +128 -0
- package/js/work_graph_repo.d.ts +26 -0
- package/js/work_graph_repo.js +364 -0
- package/js/work_graph_store.d.ts +85 -0
- package/js/work_graph_store.js +364 -0
- package/package.json +18 -3
- package/pkg/entroly_wasm.d.ts +208 -0
- package/pkg/entroly_wasm.js +1710 -2
- package/pkg/entroly_wasm_bg.wasm +0 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Host persistence mechanics only. The v1 verified-context commitment rules
|
|
4
|
+
// live in entroly-engine and are reached here through the generated WASM
|
|
5
|
+
// binding. This file owns repository isolation, bounds, atomic persistence and
|
|
6
|
+
// byte preservation; it does not define what makes a context snapshot valid.
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { verifiedContextSnapshotVerifyBytes } = require('../pkg/entroly_wasm');
|
|
11
|
+
const { WorkGraphStateError, WorkGraphStore } = require('./work_graph_store');
|
|
12
|
+
|
|
13
|
+
const CONTEXT_SNAPSHOT_TOKEN_PREFIX = 'wctx1.';
|
|
14
|
+
const DEFAULT_MAX_CONTEXT_BYTES = 512 * 1024;
|
|
15
|
+
const DEFAULT_MAX_SNAPSHOTS = 8192;
|
|
16
|
+
const DEFAULT_MAX_TOTAL_BYTES = 256 * 1024 * 1024;
|
|
17
|
+
const DIGEST_RE = /^[0-9a-f]{64}$/;
|
|
18
|
+
|
|
19
|
+
class WorkContextSnapshotError extends WorkGraphStateError {}
|
|
20
|
+
|
|
21
|
+
function positiveInteger(value, name, minimum = 1) {
|
|
22
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
23
|
+
throw new WorkContextSnapshotError(`${name} must be a safe integer >= ${minimum}`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function secureDirectory(target) {
|
|
29
|
+
fs.mkdirSync(target, { recursive: true, mode: 0o700 });
|
|
30
|
+
const info = fs.lstatSync(target);
|
|
31
|
+
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
32
|
+
throw new WorkContextSnapshotError(`unsafe context snapshot directory: ${target}`);
|
|
33
|
+
}
|
|
34
|
+
if (process.platform !== 'win32') fs.chmodSync(target, 0o700);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fsyncDirectory(target) {
|
|
38
|
+
if (process.platform === 'win32') return;
|
|
39
|
+
let fd;
|
|
40
|
+
try {
|
|
41
|
+
fd = fs.openSync(target, fs.constants.O_RDONLY | (fs.constants.O_DIRECTORY || 0));
|
|
42
|
+
fs.fsyncSync(fd);
|
|
43
|
+
} catch (_) {
|
|
44
|
+
// Directory fsync is not supported by every filesystem/runtime. The Python
|
|
45
|
+
// store has the same best-effort boundary; file data itself is fsynced.
|
|
46
|
+
} finally {
|
|
47
|
+
if (fd !== undefined) {
|
|
48
|
+
try { fs.closeSync(fd); } catch (_) {}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function asBuffer(value) {
|
|
54
|
+
if (Buffer.isBuffer(value)) return Buffer.from(value);
|
|
55
|
+
if (value instanceof Uint8Array) return Buffer.from(value);
|
|
56
|
+
if (typeof value === 'string') return Buffer.from(value, 'utf8');
|
|
57
|
+
throw new WorkContextSnapshotError('context snapshot must be bytes or UTF-8 text');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Verify exact snapshot bytes through the Rust semantic kernel.
|
|
62
|
+
*
|
|
63
|
+
* The byte stream is intentionally not normalized or reserialized before
|
|
64
|
+
* verification. Python v1 commits its compact ensure-ASCII representation and
|
|
65
|
+
* valid number lexemes such as `1.0` must survive a Python -> Node -> Python
|
|
66
|
+
* handoff byte-for-byte.
|
|
67
|
+
*/
|
|
68
|
+
function verifyCanonicalSnapshotBytes(
|
|
69
|
+
value,
|
|
70
|
+
expectedCommitment,
|
|
71
|
+
maxBytes = DEFAULT_MAX_CONTEXT_BYTES,
|
|
72
|
+
) {
|
|
73
|
+
const bytes = asBuffer(value);
|
|
74
|
+
positiveInteger(maxBytes, 'maxBytes', 1024);
|
|
75
|
+
if (bytes.length > maxBytes) {
|
|
76
|
+
throw new WorkContextSnapshotError(`context snapshot exceeds ${maxBytes} bytes`);
|
|
77
|
+
}
|
|
78
|
+
const expected = String(expectedCommitment || '');
|
|
79
|
+
try {
|
|
80
|
+
const commitment = verifiedContextSnapshotVerifyBytes(bytes, expected);
|
|
81
|
+
// Parsing is presentation only and happens after Rust has accepted the exact
|
|
82
|
+
// bytes. The parsed object is never reserialized for commitment verification.
|
|
83
|
+
const payload = JSON.parse(bytes.toString('ascii'));
|
|
84
|
+
return { payload, commitment, bytes };
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof WorkContextSnapshotError) throw error;
|
|
87
|
+
const detail = error && error.message ? error.message : String(error);
|
|
88
|
+
throw new WorkContextSnapshotError(detail);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
class WorkContextSnapshotStore {
|
|
93
|
+
constructor(graphStore, options = {}) {
|
|
94
|
+
if (!(graphStore instanceof WorkGraphStore)) {
|
|
95
|
+
throw new TypeError('graphStore must be a WorkGraphStore');
|
|
96
|
+
}
|
|
97
|
+
this.graphStore = graphStore;
|
|
98
|
+
this.maxContextBytes = positiveInteger(
|
|
99
|
+
options.maxContextBytes ?? DEFAULT_MAX_CONTEXT_BYTES,
|
|
100
|
+
'maxContextBytes',
|
|
101
|
+
1024,
|
|
102
|
+
);
|
|
103
|
+
this.maxSnapshots = positiveInteger(
|
|
104
|
+
options.maxSnapshots ?? DEFAULT_MAX_SNAPSHOTS,
|
|
105
|
+
'maxSnapshots',
|
|
106
|
+
);
|
|
107
|
+
if (this.maxSnapshots > 100000) {
|
|
108
|
+
throw new WorkContextSnapshotError('maxSnapshots must not exceed 100000');
|
|
109
|
+
}
|
|
110
|
+
this.maxTotalBytes = positiveInteger(
|
|
111
|
+
options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES,
|
|
112
|
+
'maxTotalBytes',
|
|
113
|
+
this.maxContextBytes,
|
|
114
|
+
);
|
|
115
|
+
this.contextDir = path.join(graphStore.repoDir, 'context-snapshots');
|
|
116
|
+
secureDirectory(this.contextDir);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static tokenForCommitment(commitment) {
|
|
120
|
+
const digest = String(commitment || '');
|
|
121
|
+
if (!DIGEST_RE.test(digest)) {
|
|
122
|
+
throw new WorkContextSnapshotError('context commitment is not a sha256 digest');
|
|
123
|
+
}
|
|
124
|
+
return CONTEXT_SNAPSHOT_TOKEN_PREFIX + digest;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
static digestFromToken(token) {
|
|
128
|
+
const value = String(token || '');
|
|
129
|
+
if (!value.startsWith(CONTEXT_SNAPSHOT_TOKEN_PREFIX)) {
|
|
130
|
+
throw new WorkContextSnapshotError('unsupported context snapshot token');
|
|
131
|
+
}
|
|
132
|
+
const digest = value.slice(CONTEXT_SNAPSHOT_TOKEN_PREFIX.length);
|
|
133
|
+
if (!DIGEST_RE.test(digest)) {
|
|
134
|
+
throw new WorkContextSnapshotError('context snapshot token has an invalid digest');
|
|
135
|
+
}
|
|
136
|
+
return digest;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_snapshotPath(digest) {
|
|
140
|
+
return path.join(this.contextDir, `${digest}.json`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_readUnlocked(target, expectedDigest) {
|
|
144
|
+
let info;
|
|
145
|
+
try { info = fs.lstatSync(target); }
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error && error.code === 'ENOENT') {
|
|
148
|
+
throw new WorkContextSnapshotError('context snapshot is unavailable');
|
|
149
|
+
}
|
|
150
|
+
throw new WorkContextSnapshotError(`cannot inspect context snapshot: ${error.message}`);
|
|
151
|
+
}
|
|
152
|
+
if (info.isSymbolicLink() || !info.isFile()) {
|
|
153
|
+
throw new WorkContextSnapshotError('unsafe context snapshot path');
|
|
154
|
+
}
|
|
155
|
+
if (info.size > this.maxContextBytes) {
|
|
156
|
+
throw new WorkContextSnapshotError('context snapshot exceeds its byte bound');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
|
|
160
|
+
let fd;
|
|
161
|
+
try {
|
|
162
|
+
fd = fs.openSync(target, flags);
|
|
163
|
+
const current = fs.fstatSync(fd);
|
|
164
|
+
if (!current.isFile() || current.size > this.maxContextBytes) {
|
|
165
|
+
throw new WorkContextSnapshotError('unsafe or oversized context snapshot');
|
|
166
|
+
}
|
|
167
|
+
const bytes = fs.readFileSync(fd);
|
|
168
|
+
const verified = verifyCanonicalSnapshotBytes(bytes, expectedDigest, this.maxContextBytes);
|
|
169
|
+
return verified.bytes;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error instanceof WorkContextSnapshotError) throw error;
|
|
172
|
+
throw new WorkContextSnapshotError(`cannot open context snapshot safely: ${error.message}`);
|
|
173
|
+
} finally {
|
|
174
|
+
if (fd !== undefined) {
|
|
175
|
+
try { fs.closeSync(fd); } catch (_) {}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
_usageUnlocked() {
|
|
181
|
+
let count = 0;
|
|
182
|
+
let totalBytes = 0;
|
|
183
|
+
for (const name of fs.readdirSync(this.contextDir)) {
|
|
184
|
+
if (name.startsWith('.context-') && name.endsWith('.tmp')) continue;
|
|
185
|
+
if (!name.endsWith('.json')) continue;
|
|
186
|
+
const target = path.join(this.contextDir, name);
|
|
187
|
+
const info = fs.lstatSync(target);
|
|
188
|
+
if (info.isSymbolicLink() || !info.isFile()) {
|
|
189
|
+
throw new WorkContextSnapshotError('unsafe context snapshot entry');
|
|
190
|
+
}
|
|
191
|
+
count += 1;
|
|
192
|
+
totalBytes += info.size;
|
|
193
|
+
if (count > this.maxSnapshots || totalBytes > this.maxTotalBytes) break;
|
|
194
|
+
}
|
|
195
|
+
return { count, totalBytes };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
putCanonicalBytes(value, expectedCommitment) {
|
|
199
|
+
const expected = String(expectedCommitment || '');
|
|
200
|
+
if (!DIGEST_RE.test(expected)) {
|
|
201
|
+
throw new WorkContextSnapshotError('expectedCommitment must be a sha256 digest');
|
|
202
|
+
}
|
|
203
|
+
const verified = verifyCanonicalSnapshotBytes(value, expected, this.maxContextBytes);
|
|
204
|
+
const token = WorkContextSnapshotStore.tokenForCommitment(expected);
|
|
205
|
+
const target = this._snapshotPath(expected);
|
|
206
|
+
|
|
207
|
+
return this.graphStore.withLock(() => {
|
|
208
|
+
secureDirectory(this.contextDir);
|
|
209
|
+
if (fs.existsSync(target)) {
|
|
210
|
+
const existing = this._readUnlocked(target, expected);
|
|
211
|
+
if (!existing.equals(verified.bytes)) {
|
|
212
|
+
throw new WorkContextSnapshotError(
|
|
213
|
+
'context commitment maps to conflicting stable snapshot bytes',
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return token;
|
|
217
|
+
}
|
|
218
|
+
const usage = this._usageUnlocked();
|
|
219
|
+
if (usage.count >= this.maxSnapshots) {
|
|
220
|
+
throw new WorkContextSnapshotError('context snapshot store reached its bounded entry limit');
|
|
221
|
+
}
|
|
222
|
+
if (usage.totalBytes + verified.bytes.length > this.maxTotalBytes) {
|
|
223
|
+
throw new WorkContextSnapshotError('context snapshot store reached its bounded byte limit');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const temp = path.join(
|
|
227
|
+
this.contextDir,
|
|
228
|
+
`.context-${process.pid}-${crypto.randomUUID().replace(/-/g, '')}.tmp`,
|
|
229
|
+
);
|
|
230
|
+
let fd;
|
|
231
|
+
try {
|
|
232
|
+
fd = fs.openSync(temp, 'wx', 0o600);
|
|
233
|
+
fs.writeFileSync(fd, verified.bytes);
|
|
234
|
+
fs.fsyncSync(fd);
|
|
235
|
+
fs.closeSync(fd);
|
|
236
|
+
fd = undefined;
|
|
237
|
+
if (fs.existsSync(target)) {
|
|
238
|
+
const existing = this._readUnlocked(target, expected);
|
|
239
|
+
if (!existing.equals(verified.bytes)) {
|
|
240
|
+
throw new WorkContextSnapshotError(
|
|
241
|
+
'context commitment maps to conflicting stable snapshot bytes',
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
fs.renameSync(temp, target);
|
|
246
|
+
if (process.platform !== 'win32') fs.chmodSync(target, 0o600);
|
|
247
|
+
fsyncDirectory(this.contextDir);
|
|
248
|
+
}
|
|
249
|
+
} finally {
|
|
250
|
+
if (fd !== undefined) {
|
|
251
|
+
try { fs.closeSync(fd); } catch (_) {}
|
|
252
|
+
}
|
|
253
|
+
try { fs.unlinkSync(temp); } catch (_) {}
|
|
254
|
+
}
|
|
255
|
+
return token;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
getCanonicalBytes(token) {
|
|
260
|
+
const digest = WorkContextSnapshotStore.digestFromToken(token);
|
|
261
|
+
return this.graphStore.withLock(() => {
|
|
262
|
+
secureDirectory(this.contextDir);
|
|
263
|
+
return this._readUnlocked(this._snapshotPath(digest), digest);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
getJSON(token) {
|
|
268
|
+
return JSON.parse(this.getCanonicalBytes(token).toString('ascii'));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
CONTEXT_SNAPSHOT_TOKEN_PREFIX,
|
|
274
|
+
DEFAULT_MAX_CONTEXT_BYTES,
|
|
275
|
+
DEFAULT_MAX_SNAPSHOTS,
|
|
276
|
+
DEFAULT_MAX_TOTAL_BYTES,
|
|
277
|
+
WorkContextSnapshotError,
|
|
278
|
+
WorkContextSnapshotStore,
|
|
279
|
+
verifyCanonicalSnapshotBytes,
|
|
280
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { RepositoryDiscoveryOptions } from "./work_graph_repo";
|
|
2
|
+
|
|
3
|
+
export type WorkGraphTrust = "untrusted" | "inferred" | "observed" | "verified";
|
|
4
|
+
export type WorkGraphStatus =
|
|
5
|
+
| "unknown"
|
|
6
|
+
| "planned"
|
|
7
|
+
| "in_progress"
|
|
8
|
+
| "blocked"
|
|
9
|
+
| "needs_verification"
|
|
10
|
+
| "completed"
|
|
11
|
+
| "abandoned";
|
|
12
|
+
|
|
13
|
+
export interface WorkGraphSummary {
|
|
14
|
+
schema_version: number;
|
|
15
|
+
repo_id: string;
|
|
16
|
+
revision: number;
|
|
17
|
+
graph_commitment: string;
|
|
18
|
+
event_count: number;
|
|
19
|
+
node_count: number;
|
|
20
|
+
edge_count: number;
|
|
21
|
+
evidence_count: number;
|
|
22
|
+
unfinished_count: number;
|
|
23
|
+
blocked_count: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WorkGraphWorkItem {
|
|
27
|
+
node_id: string;
|
|
28
|
+
kind: string;
|
|
29
|
+
label: string;
|
|
30
|
+
status: WorkGraphStatus;
|
|
31
|
+
trust: WorkGraphTrust;
|
|
32
|
+
updated_at_ms: number;
|
|
33
|
+
task_ids: string[];
|
|
34
|
+
agent_ids: string[];
|
|
35
|
+
changed_paths: string[];
|
|
36
|
+
symbol_ids: string[];
|
|
37
|
+
commit_ids: string[];
|
|
38
|
+
decision_ids: string[];
|
|
39
|
+
failure_ids: string[];
|
|
40
|
+
verification_ids: string[];
|
|
41
|
+
evidence_ids: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WorkGraphEvidence {
|
|
45
|
+
evidence_id: string;
|
|
46
|
+
kind: string;
|
|
47
|
+
source_ref: string;
|
|
48
|
+
digest: string;
|
|
49
|
+
locator: string;
|
|
50
|
+
trust: WorkGraphTrust;
|
|
51
|
+
observed_at_ms: number;
|
|
52
|
+
attributes: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WorkGraphResumeView {
|
|
56
|
+
repo_id: string;
|
|
57
|
+
graph_revision: number;
|
|
58
|
+
graph_commitment: string;
|
|
59
|
+
selected_workstream: WorkGraphWorkItem;
|
|
60
|
+
task_labels: string[];
|
|
61
|
+
agents: string[];
|
|
62
|
+
decisions: string[];
|
|
63
|
+
failures: string[];
|
|
64
|
+
verification: string[];
|
|
65
|
+
changed_paths: string[];
|
|
66
|
+
commits: string[];
|
|
67
|
+
evidence: WorkGraphEvidence[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface WorkGraphContextScope {
|
|
71
|
+
repo_id: string;
|
|
72
|
+
graph_revision: number;
|
|
73
|
+
graph_commitment: string;
|
|
74
|
+
workstream_id: string;
|
|
75
|
+
task_ids: string[];
|
|
76
|
+
task_ids_total: number;
|
|
77
|
+
task_ids_commitment: string;
|
|
78
|
+
agent_ids: string[];
|
|
79
|
+
agent_ids_total: number;
|
|
80
|
+
agent_ids_commitment: string;
|
|
81
|
+
changed_paths: string[];
|
|
82
|
+
changed_paths_total: number;
|
|
83
|
+
changed_paths_commitment: string;
|
|
84
|
+
symbol_ids: string[];
|
|
85
|
+
symbol_ids_total: number;
|
|
86
|
+
symbol_ids_commitment: string;
|
|
87
|
+
commit_ids: string[];
|
|
88
|
+
commit_ids_total: number;
|
|
89
|
+
commit_ids_commitment: string;
|
|
90
|
+
evidence_ids: string[];
|
|
91
|
+
evidence_ids_total: number;
|
|
92
|
+
evidence_ids_commitment: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface WorkGraphCoordinationConflict {
|
|
96
|
+
lease_a: string;
|
|
97
|
+
lease_b: string;
|
|
98
|
+
agent_a: string;
|
|
99
|
+
agent_b: string;
|
|
100
|
+
task_a: string;
|
|
101
|
+
task_b: string;
|
|
102
|
+
overlapping_paths: string[];
|
|
103
|
+
overlapping_symbols: string[];
|
|
104
|
+
reason: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface WorkGraphCoordinationReport {
|
|
108
|
+
as_of_ms: number;
|
|
109
|
+
active_leases: number;
|
|
110
|
+
conflicts: WorkGraphCoordinationConflict[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface WorkGraphHandoffReceipt {
|
|
114
|
+
schema_version: number;
|
|
115
|
+
repo_id: string;
|
|
116
|
+
graph_revision: number;
|
|
117
|
+
graph_commitment: string;
|
|
118
|
+
workstream_id: string;
|
|
119
|
+
from_agent: string;
|
|
120
|
+
to_agent: string;
|
|
121
|
+
generated_at_ms: number;
|
|
122
|
+
node_ids: string[];
|
|
123
|
+
edge_ids: string[];
|
|
124
|
+
evidence_ids: string[];
|
|
125
|
+
payload_commitment: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface WorkContinuationManifest {
|
|
129
|
+
context_receipt_commitments?: string[];
|
|
130
|
+
routing_commitments?: string[];
|
|
131
|
+
execution_outcome_commitments?: string[];
|
|
132
|
+
verification_commitments?: string[];
|
|
133
|
+
memory_commitments?: string[];
|
|
134
|
+
outstanding_work_refs?: string[];
|
|
135
|
+
recovery_handle_ids?: string[];
|
|
136
|
+
created_at_ms: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface WorkContinuationProof {
|
|
140
|
+
schema_version: number;
|
|
141
|
+
proof_id: string;
|
|
142
|
+
repository_id: string;
|
|
143
|
+
graph_revision: number;
|
|
144
|
+
graph_commitment: string;
|
|
145
|
+
workstream_id: string;
|
|
146
|
+
from_agent: string;
|
|
147
|
+
to_agent: string;
|
|
148
|
+
handoff_commitment: string;
|
|
149
|
+
context_receipt_commitments: string[];
|
|
150
|
+
routing_commitments: string[];
|
|
151
|
+
execution_outcome_commitments: string[];
|
|
152
|
+
verification_commitments: string[];
|
|
153
|
+
memory_commitments: string[];
|
|
154
|
+
outstanding_work_refs: string[];
|
|
155
|
+
recovery_handle_ids: string[];
|
|
156
|
+
created_at_ms: number;
|
|
157
|
+
proof_commitment: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type WorkGraphObservation = Record<string, unknown> & {
|
|
161
|
+
repo_id: string;
|
|
162
|
+
observed_at_ms: number;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export class WorkGraph {
|
|
166
|
+
constructor(repoId: string);
|
|
167
|
+
static fromJSON(serialized: string | Record<string, unknown>): WorkGraph;
|
|
168
|
+
static fromRepository(path?: string, options?: RepositoryDiscoveryOptions): WorkGraph;
|
|
169
|
+
static verifyHandoffIntegrity(receipt: string | WorkGraphHandoffReceipt): boolean;
|
|
170
|
+
readonly repoId: string;
|
|
171
|
+
readonly revision: number;
|
|
172
|
+
readonly graphCommitment: string;
|
|
173
|
+
readonly eventCount: number;
|
|
174
|
+
applyEvent(event: string | Record<string, unknown>): string;
|
|
175
|
+
observeRepository(observation: string | WorkGraphObservation): string;
|
|
176
|
+
refreshRepository(path?: string, options?: RepositoryDiscoveryOptions): string;
|
|
177
|
+
merge(other: WorkGraph | string | Record<string, unknown>): number;
|
|
178
|
+
exportJSON(pretty?: boolean): string;
|
|
179
|
+
exportState(): Record<string, unknown>;
|
|
180
|
+
summary(): WorkGraphSummary;
|
|
181
|
+
snapshot(pretty?: boolean): Record<string, unknown>;
|
|
182
|
+
unfinished(pretty?: boolean): WorkGraphWorkItem[];
|
|
183
|
+
resume(workstreamId?: string | null, maxEvidence?: number, pretty?: boolean): WorkGraphResumeView;
|
|
184
|
+
contextScope(workstreamId?: string | null, maxEvidence?: number, pretty?: boolean): WorkGraphContextScope;
|
|
185
|
+
recordContextReceipt(
|
|
186
|
+
receipt: string | Record<string, unknown>,
|
|
187
|
+
agentId?: string,
|
|
188
|
+
sessionId?: string,
|
|
189
|
+
): string;
|
|
190
|
+
recordMemory(
|
|
191
|
+
memory: string | Record<string, unknown>,
|
|
192
|
+
nowMs?: number,
|
|
193
|
+
supersededIds?: string[],
|
|
194
|
+
): string;
|
|
195
|
+
recordExecutionChain(
|
|
196
|
+
route: string | Record<string, unknown>,
|
|
197
|
+
outcome: string | Record<string, unknown>,
|
|
198
|
+
verification: string | Record<string, unknown>,
|
|
199
|
+
invalidatedCommitments?: string[],
|
|
200
|
+
): string;
|
|
201
|
+
continuationProof(
|
|
202
|
+
handoff: string | WorkGraphHandoffReceipt,
|
|
203
|
+
manifest: WorkContinuationManifest,
|
|
204
|
+
): WorkContinuationProof;
|
|
205
|
+
reconstructedContinuationProof(
|
|
206
|
+
workstreamId: string,
|
|
207
|
+
toAgent: string,
|
|
208
|
+
manifest: WorkContinuationManifest,
|
|
209
|
+
): WorkContinuationProof;
|
|
210
|
+
coordination(nowMs?: number, pretty?: boolean): WorkGraphCoordinationReport;
|
|
211
|
+
handoff(
|
|
212
|
+
workstreamId: string,
|
|
213
|
+
fromAgent: string,
|
|
214
|
+
toAgent: string,
|
|
215
|
+
generatedAtMs?: number,
|
|
216
|
+
pretty?: boolean,
|
|
217
|
+
): WorkGraphHandoffReceipt;
|
|
218
|
+
verifyHandoff(receipt: string | WorkGraphHandoffReceipt): boolean;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export {
|
|
222
|
+
RepositoryDiscoveryError,
|
|
223
|
+
discoverRepositoryIdentity,
|
|
224
|
+
discoverRepositoryObservation,
|
|
225
|
+
} from "./work_graph_repo";
|
package/js/work_graph.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Thin npm orchestration over the shared Rust/WASM AI Work Graph. No task-state
|
|
4
|
+
// inference, trust upgrades, coordination logic, or handoff verification lives
|
|
5
|
+
// here; those semantics are single-source-of-truth in entroly-engine.
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
RepositoryDiscoveryError,
|
|
9
|
+
discoverRepositoryIdentity,
|
|
10
|
+
discoverRepositoryObservation,
|
|
11
|
+
} = require('./work_graph_repo');
|
|
12
|
+
|
|
13
|
+
function wasmWorkGraph() {
|
|
14
|
+
const { WasmWorkGraph } = require('../pkg/entroly_wasm');
|
|
15
|
+
if (!WasmWorkGraph) {
|
|
16
|
+
throw new Error('Rust Work Graph is unavailable; rebuild entroly-wasm');
|
|
17
|
+
}
|
|
18
|
+
return WasmWorkGraph;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toJSONText(value) {
|
|
22
|
+
if (typeof value === 'string') return value;
|
|
23
|
+
return JSON.stringify(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fromJSONText(value) {
|
|
27
|
+
return JSON.parse(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function requireSafeInteger(value, name, { min = Number.MIN_SAFE_INTEGER } = {}) {
|
|
31
|
+
const number = Number(value);
|
|
32
|
+
if (!Number.isSafeInteger(number) || number < min) {
|
|
33
|
+
throw new TypeError(`${name} must be a safe integer${min === 0 ? ' >= 0' : ''}`);
|
|
34
|
+
}
|
|
35
|
+
return number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class WorkGraph {
|
|
39
|
+
constructor(repoId, inner = null) {
|
|
40
|
+
const Native = wasmWorkGraph();
|
|
41
|
+
this._inner = inner || new Native(String(repoId || ''));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static fromJSON(serialized) {
|
|
45
|
+
const Native = wasmWorkGraph();
|
|
46
|
+
return new WorkGraph('', Native.fromJSON(toJSONText(serialized)));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static fromRepository(repoPath = '.', options = {}) {
|
|
50
|
+
const observation = discoverRepositoryObservation(repoPath, options);
|
|
51
|
+
const graph = new WorkGraph(observation.repo_id);
|
|
52
|
+
graph.observeRepository(observation);
|
|
53
|
+
return graph;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static verifyHandoffIntegrity(receipt) {
|
|
57
|
+
return wasmWorkGraph().verifyHandoffIntegrityJSON(toJSONText(receipt));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get repoId() { return this._inner.repoId; }
|
|
61
|
+
get revision() { return Number(this._inner.revision); }
|
|
62
|
+
get graphCommitment() { return this._inner.graphCommitment; }
|
|
63
|
+
get eventCount() { return Number(this._inner.eventCount); }
|
|
64
|
+
|
|
65
|
+
applyEvent(event) {
|
|
66
|
+
return this._inner.applyEventJSON(toJSONText(event));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
observeRepository(observation) {
|
|
70
|
+
return this._inner.observeRepositoryJSON(toJSONText(observation));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
refreshRepository(repoPath = '.', options = {}) {
|
|
74
|
+
const observation = discoverRepositoryObservation(repoPath, options);
|
|
75
|
+
if (observation.repo_id !== this.repoId) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`repository identity changed: expected ${this.repoId}, got ${observation.repo_id}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return this.observeRepository(observation);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
merge(other) {
|
|
84
|
+
const payload = other instanceof WorkGraph ? other.exportJSON(false) : toJSONText(other);
|
|
85
|
+
return Number(this._inner.mergeJSON(payload));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
exportJSON(pretty = false) {
|
|
89
|
+
return this._inner.exportJSON(Boolean(pretty));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
exportState() {
|
|
93
|
+
return fromJSONText(this.exportJSON(false));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
summary() {
|
|
97
|
+
return fromJSONText(this._inner.summaryJSON());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
snapshot(pretty = false) {
|
|
101
|
+
return fromJSONText(this._inner.snapshotJSON(Boolean(pretty)));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
unfinished(pretty = false) {
|
|
105
|
+
return fromJSONText(this._inner.unfinishedJSON(Boolean(pretty)));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
resume(workstreamId = null, maxEvidence = 128, pretty = false) {
|
|
109
|
+
const id = workstreamId == null ? undefined : String(workstreamId);
|
|
110
|
+
return fromJSONText(this._inner.resumeJSON(
|
|
111
|
+
id,
|
|
112
|
+
requireSafeInteger(maxEvidence, 'maxEvidence', { min: 0 }),
|
|
113
|
+
Boolean(pretty),
|
|
114
|
+
));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
contextScope(workstreamId = null, maxEvidence = 128, pretty = false) {
|
|
118
|
+
const id = workstreamId == null ? undefined : String(workstreamId);
|
|
119
|
+
return fromJSONText(this._inner.contextScopeJSON(
|
|
120
|
+
id,
|
|
121
|
+
requireSafeInteger(maxEvidence, 'maxEvidence', { min: 0 }),
|
|
122
|
+
Boolean(pretty),
|
|
123
|
+
));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
recordContextReceipt(receipt, agentId = '', sessionId = '') {
|
|
127
|
+
return this._inner.recordContextReceiptJSON(
|
|
128
|
+
toJSONText(receipt), String(agentId), String(sessionId),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
recordMemory(memory, nowMs = Date.now(), supersededIds = []) {
|
|
133
|
+
return this._inner.recordMemoryJSON(
|
|
134
|
+
toJSONText(memory),
|
|
135
|
+
requireSafeInteger(nowMs, 'nowMs'),
|
|
136
|
+
toJSONText(supersededIds),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
recordExecutionChain(route, outcome, verification, invalidatedCommitments = []) {
|
|
141
|
+
return this._inner.recordExecutionChainJSON(
|
|
142
|
+
toJSONText(route),
|
|
143
|
+
toJSONText(outcome),
|
|
144
|
+
toJSONText(verification),
|
|
145
|
+
toJSONText(invalidatedCommitments),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
continuationProof(handoff, manifest) {
|
|
150
|
+
return fromJSONText(this._inner.continuationProofJSON(
|
|
151
|
+
toJSONText(handoff),
|
|
152
|
+
toJSONText(manifest),
|
|
153
|
+
));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
reconstructedContinuationProof(workstreamId, toAgent, manifest) {
|
|
157
|
+
return fromJSONText(this._inner.reconstructedContinuationProofJSON(
|
|
158
|
+
String(workstreamId),
|
|
159
|
+
String(toAgent),
|
|
160
|
+
toJSONText(manifest),
|
|
161
|
+
));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
coordination(nowMs = Date.now(), pretty = false) {
|
|
165
|
+
return fromJSONText(this._inner.coordinationJSON(
|
|
166
|
+
requireSafeInteger(nowMs, 'nowMs'),
|
|
167
|
+
Boolean(pretty),
|
|
168
|
+
));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
handoff(workstreamId, fromAgent, toAgent, generatedAtMs = Date.now(), pretty = false) {
|
|
172
|
+
return fromJSONText(this._inner.handoffJSON(
|
|
173
|
+
String(workstreamId),
|
|
174
|
+
String(fromAgent),
|
|
175
|
+
String(toAgent),
|
|
176
|
+
requireSafeInteger(generatedAtMs, 'generatedAtMs'),
|
|
177
|
+
Boolean(pretty),
|
|
178
|
+
));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
verifyHandoff(receipt) {
|
|
182
|
+
return this._inner.verifyHandoffJSON(toJSONText(receipt));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
WorkGraph,
|
|
188
|
+
RepositoryDiscoveryError,
|
|
189
|
+
discoverRepositoryIdentity,
|
|
190
|
+
discoverRepositoryObservation,
|
|
191
|
+
};
|