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,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Node orchestration-only content identity for passive Work Graph snapshots.
|
|
4
|
+
// Rust decides whether observations are semantically duplicate; this helper
|
|
5
|
+
// merely supplies exact Git blob identity where it can do so without guessing.
|
|
6
|
+
// It never follows symlinks, reads special files, or reads an unbounded file.
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
const MAX_HASH_FILE_BYTES = 64 * 1024 * 1024;
|
|
13
|
+
const HASH_CHUNK_BYTES = 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
function gitEnv() {
|
|
16
|
+
return {
|
|
17
|
+
...process.env,
|
|
18
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
19
|
+
GIT_OPTIONAL_LOCKS: '0',
|
|
20
|
+
GIT_PAGER: 'cat',
|
|
21
|
+
LC_ALL: 'C',
|
|
22
|
+
LANG: 'C',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function runGitText(cwd, args) {
|
|
27
|
+
const result = spawnSync(
|
|
28
|
+
'git',
|
|
29
|
+
[
|
|
30
|
+
'-c', 'core.fsmonitor=false',
|
|
31
|
+
'-c', 'core.untrackedCache=false',
|
|
32
|
+
'-c', 'submodule.recurse=false',
|
|
33
|
+
'-C', String(cwd),
|
|
34
|
+
...args,
|
|
35
|
+
],
|
|
36
|
+
{
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
env: gitEnv(),
|
|
39
|
+
maxBuffer: 1024 * 1024,
|
|
40
|
+
timeout: 5000,
|
|
41
|
+
windowsHide: true,
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
if (result.error || result.status !== 0) return '';
|
|
45
|
+
return String(result.stdout || '').trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveRoot(repoPath) {
|
|
49
|
+
const candidate = path.resolve(String(repoPath || '.'));
|
|
50
|
+
const output = runGitText(candidate, ['rev-parse', '--show-toplevel']);
|
|
51
|
+
if (!output) throw new Error(`not a Git worktree: ${candidate}`);
|
|
52
|
+
return path.resolve(output);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function objectHashAlgorithm(root) {
|
|
56
|
+
const value = runGitText(root, ['rev-parse', '--show-object-format']).toLowerCase();
|
|
57
|
+
return value === 'sha1' || value === 'sha256' ? value : '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function relativeRegularPath(root, repoRel) {
|
|
61
|
+
const value = String(repoRel || '').replace(/\\/g, '/');
|
|
62
|
+
if (!value || value.startsWith('/') || /[\0\r\n]/.test(value)) return null;
|
|
63
|
+
const parts = value.split('/');
|
|
64
|
+
if (parts.some(part => !part || part === '.' || part === '..')) return null;
|
|
65
|
+
const candidate = path.join(root, ...parts);
|
|
66
|
+
let before;
|
|
67
|
+
try { before = fs.lstatSync(candidate, { bigint: true }); }
|
|
68
|
+
catch (_) { return null; }
|
|
69
|
+
if (!before.isFile() || before.size > BigInt(MAX_HASH_FILE_BYTES)) return null;
|
|
70
|
+
return { candidate, before };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function sameFile(left, right) {
|
|
74
|
+
if (!left.isFile() || !right.isFile()) return false;
|
|
75
|
+
// `dev` is compared only when both sides report one. Node on Windows returns
|
|
76
|
+
// dev=0 from lstat but a real device id from fstat for the same file, so
|
|
77
|
+
// requiring strict equality rejected every legitimate file and made
|
|
78
|
+
// gitBlobDigest return '' for all input -- content identity was silently
|
|
79
|
+
// dead on win32, which is the binding handoff/resume rely on. The anti-swap
|
|
80
|
+
// property is preserved: the file index (ino), size, and both timestamps
|
|
81
|
+
// must still match across lstat -> open -> lstat.
|
|
82
|
+
if (left.dev !== 0n && right.dev !== 0n && left.dev !== right.dev) return false;
|
|
83
|
+
return left.ino === right.ino
|
|
84
|
+
&& left.size === right.size
|
|
85
|
+
&& left.mtimeNs === right.mtimeNs
|
|
86
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function gitBlobDigest(root, repoRel, algorithm) {
|
|
90
|
+
const safe = relativeRegularPath(root, repoRel);
|
|
91
|
+
if (!safe) return '';
|
|
92
|
+
|
|
93
|
+
let fd;
|
|
94
|
+
try {
|
|
95
|
+
let flags = fs.constants.O_RDONLY;
|
|
96
|
+
if (typeof fs.constants.O_NOFOLLOW === 'number') flags |= fs.constants.O_NOFOLLOW;
|
|
97
|
+
fd = fs.openSync(safe.candidate, flags);
|
|
98
|
+
const opened = fs.fstatSync(fd, { bigint: true });
|
|
99
|
+
const pathAfterOpen = fs.lstatSync(safe.candidate, { bigint: true });
|
|
100
|
+
if (!sameFile(safe.before, opened) || !sameFile(opened, pathAfterOpen)) return '';
|
|
101
|
+
if (opened.size > BigInt(MAX_HASH_FILE_BYTES)) return '';
|
|
102
|
+
|
|
103
|
+
const hash = crypto.createHash(algorithm);
|
|
104
|
+
hash.update(Buffer.from(`blob ${opened.size.toString()}\0`, 'ascii'));
|
|
105
|
+
const buffer = Buffer.allocUnsafe(HASH_CHUNK_BYTES);
|
|
106
|
+
let offset = 0n;
|
|
107
|
+
while (offset < opened.size) {
|
|
108
|
+
const remaining = opened.size - offset;
|
|
109
|
+
const length = Number(remaining > BigInt(buffer.length) ? BigInt(buffer.length) : remaining);
|
|
110
|
+
const bytes = fs.readSync(fd, buffer, 0, length, Number(offset));
|
|
111
|
+
if (bytes <= 0) return '';
|
|
112
|
+
hash.update(buffer.subarray(0, bytes));
|
|
113
|
+
offset += BigInt(bytes);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
117
|
+
const pathAfterRead = fs.lstatSync(safe.candidate, { bigint: true });
|
|
118
|
+
if (!sameFile(opened, after) || !sameFile(after, pathAfterRead)) return '';
|
|
119
|
+
return `git-blob:${hash.digest('hex')}`;
|
|
120
|
+
} catch (_) {
|
|
121
|
+
return '';
|
|
122
|
+
} finally {
|
|
123
|
+
if (fd !== undefined) {
|
|
124
|
+
try { fs.closeSync(fd); } catch (_) {}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function enrichWorktreeContentDigests(repoPath, observation) {
|
|
130
|
+
const changes = observation && observation.changes;
|
|
131
|
+
if (!Array.isArray(changes)) return observation;
|
|
132
|
+
const root = resolveRoot(repoPath);
|
|
133
|
+
const algorithm = objectHashAlgorithm(root);
|
|
134
|
+
|
|
135
|
+
for (const change of changes) {
|
|
136
|
+
if (!change || typeof change !== 'object') continue;
|
|
137
|
+
change.content_digest = '';
|
|
138
|
+
if (change.staged || change.conflicted) continue;
|
|
139
|
+
if (String(change.kind || '') === 'deleted') {
|
|
140
|
+
change.content_digest = 'worktree:deleted';
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (!algorithm) continue;
|
|
144
|
+
change.content_digest = gitBlobDigest(root, change.path, algorithm);
|
|
145
|
+
}
|
|
146
|
+
return observation;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
enrichWorktreeContentDigests,
|
|
151
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WorkContinuationProof,
|
|
3
|
+
WorkGraphHandoffReceipt,
|
|
4
|
+
WorkGraphResumeView,
|
|
5
|
+
} from "./work_graph";
|
|
6
|
+
import type { RepositoryDiscoveryOptions } from "./work_graph_repo";
|
|
7
|
+
import type { WorkGraphStoreOptions } from "./work_graph_store";
|
|
8
|
+
|
|
9
|
+
export interface ResumeRepositoryOptions {
|
|
10
|
+
workstreamId?: string | null;
|
|
11
|
+
maxEvidence?: number;
|
|
12
|
+
storeOptions?: WorkGraphStoreOptions;
|
|
13
|
+
repositoryOptions?: RepositoryDiscoveryOptions;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface HandoffRepositoryOptions {
|
|
17
|
+
workstreamId: string;
|
|
18
|
+
fromAgent: string;
|
|
19
|
+
toAgent: string;
|
|
20
|
+
generatedAtMs?: number;
|
|
21
|
+
storeOptions?: WorkGraphStoreOptions;
|
|
22
|
+
repositoryOptions?: RepositoryDiscoveryOptions;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const MAX_RESUME_EVIDENCE: number;
|
|
26
|
+
export const MAX_WORK_ID_CHARS: number;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Refresh bounded durable repository/checkpoint facts and recover unfinished
|
|
30
|
+
* work from the shared Rust Work Graph. The previous agent does not need to
|
|
31
|
+
* have produced an explicit handoff.
|
|
32
|
+
*/
|
|
33
|
+
export function resumeRepository(
|
|
34
|
+
repoPath?: string,
|
|
35
|
+
options?: ResumeRepositoryOptions,
|
|
36
|
+
): WorkGraphResumeView;
|
|
37
|
+
|
|
38
|
+
/** Refresh durable facts exactly once, then seal a graph-bound handoff receipt. */
|
|
39
|
+
export function handoffRepository(
|
|
40
|
+
repoPath: string | undefined,
|
|
41
|
+
options: HandoffRepositoryOptions,
|
|
42
|
+
): WorkGraphHandoffReceipt;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Refresh durable facts once and return both the compatibility handoff receipt
|
|
46
|
+
* and the Rust-verified complete continuation proof.
|
|
47
|
+
*/
|
|
48
|
+
export function handoffRepositoryWithProof(
|
|
49
|
+
repoPath: string | undefined,
|
|
50
|
+
options: HandoffRepositoryOptions,
|
|
51
|
+
): {
|
|
52
|
+
handoff: WorkGraphHandoffReceipt;
|
|
53
|
+
continuation_proof: WorkContinuationProof;
|
|
54
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Thin Node orchestration for interrupted-agent recovery and fresh handoff.
|
|
4
|
+
// Repository observation and persistence happen here; all work-state meaning
|
|
5
|
+
// remains in shared Rust.
|
|
6
|
+
const { enrichWorktreeContentDigests } = require('./work_graph_content_digest');
|
|
7
|
+
const { discoverRepositoryObservation } = require('./work_graph_repo');
|
|
8
|
+
const { WorkGraphStateError, WorkGraphStore } = require('./work_graph_store');
|
|
9
|
+
|
|
10
|
+
const MAX_RESUME_EVIDENCE = 4096;
|
|
11
|
+
const MAX_WORK_ID_CHARS = 512;
|
|
12
|
+
|
|
13
|
+
function validateMaxEvidence(value) {
|
|
14
|
+
const number = Number(value);
|
|
15
|
+
if (!Number.isSafeInteger(number) || number < 0 || number > MAX_RESUME_EVIDENCE) {
|
|
16
|
+
throw new WorkGraphStateError(
|
|
17
|
+
`maxEvidence must be a safe integer between 0 and ${MAX_RESUME_EVIDENCE}`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function requiredId(value, name) {
|
|
24
|
+
const text = String(value == null ? '' : value).trim();
|
|
25
|
+
if (!text) throw new WorkGraphStateError(`${name} must not be empty`);
|
|
26
|
+
if (text.length > MAX_WORK_ID_CHARS || text.includes('\0')) {
|
|
27
|
+
throw new WorkGraphStateError(
|
|
28
|
+
`${name} may not exceed ${MAX_WORK_ID_CHARS} characters or contain NUL`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function optionalWorkstreamId(value) {
|
|
35
|
+
if (value == null || value === '') return null;
|
|
36
|
+
const text = String(value).trim();
|
|
37
|
+
if (text.length > MAX_WORK_ID_CHARS || text.includes('\0')) {
|
|
38
|
+
throw new WorkGraphStateError(
|
|
39
|
+
`workstreamId may not exceed ${MAX_WORK_ID_CHARS} characters or contain NUL`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return text || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function passiveObservation(repoPath, repositoryOptions) {
|
|
46
|
+
const observation = discoverRepositoryObservation(repoPath, repositoryOptions);
|
|
47
|
+
return enrichWorktreeContentDigests(repoPath, observation);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resumeRepository(repoPath = '.', options = {}) {
|
|
51
|
+
// Validate before constructing the store or observing Git. Invalid recovery
|
|
52
|
+
// requests must not mutate shared state.
|
|
53
|
+
const maxEvidence = validateMaxEvidence(options.maxEvidence ?? 128);
|
|
54
|
+
const workstreamId = optionalWorkstreamId(options.workstreamId);
|
|
55
|
+
const storeOptions = options.storeOptions || {};
|
|
56
|
+
const repositoryOptions = options.repositoryOptions || {};
|
|
57
|
+
|
|
58
|
+
const store = WorkGraphStore.forRepository(repoPath, storeOptions);
|
|
59
|
+
store.submitObservation(passiveObservation(repoPath, repositoryOptions));
|
|
60
|
+
return store.resume(workstreamId, maxEvidence);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function handoffRepository(repoPath = '.', options = {}) {
|
|
64
|
+
// A receipt is a state-sealing operation. Validate everything before touching
|
|
65
|
+
// disk, then refresh the bounded durable facts exactly once before Rust seals
|
|
66
|
+
// the handoff against the graph commitment.
|
|
67
|
+
const workstreamId = requiredId(options.workstreamId, 'workstreamId');
|
|
68
|
+
const fromAgent = requiredId(options.fromAgent, 'fromAgent');
|
|
69
|
+
const toAgent = requiredId(options.toAgent, 'toAgent');
|
|
70
|
+
const storeOptions = options.storeOptions || {};
|
|
71
|
+
const repositoryOptions = options.repositoryOptions || {};
|
|
72
|
+
const generatedAtMs = options.generatedAtMs == null ? Date.now() : Number(options.generatedAtMs);
|
|
73
|
+
if (!Number.isSafeInteger(generatedAtMs)) {
|
|
74
|
+
throw new WorkGraphStateError('generatedAtMs must be a JavaScript-safe integer');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const store = WorkGraphStore.forRepository(repoPath, storeOptions);
|
|
78
|
+
store.submitObservation(passiveObservation(repoPath, repositoryOptions));
|
|
79
|
+
return store.handoff(workstreamId, fromAgent, toAgent, generatedAtMs);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function continuationOutstandingRefs(view) {
|
|
83
|
+
const safeView = view && typeof view === 'object' ? view : {};
|
|
84
|
+
const selected = safeView.selected_workstream && typeof safeView.selected_workstream === 'object'
|
|
85
|
+
? safeView.selected_workstream
|
|
86
|
+
: {};
|
|
87
|
+
const values = [
|
|
88
|
+
...(Array.isArray(safeView.changed_paths) ? safeView.changed_paths : []),
|
|
89
|
+
...(Array.isArray(safeView.failures) ? safeView.failures : []),
|
|
90
|
+
...(Array.isArray(selected.task_ids) ? selected.task_ids : []),
|
|
91
|
+
];
|
|
92
|
+
return [...new Set(values
|
|
93
|
+
.filter((value) => typeof value === 'string' && value.trim())
|
|
94
|
+
.map((value) => value.trim()))];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function handoffRepositoryWithProof(repoPath = '.', options = {}) {
|
|
98
|
+
// Preserve the receipt-only API above for compatibility while making the
|
|
99
|
+
// complete continuation artifact available as one low-ceremony operation.
|
|
100
|
+
const workstreamId = requiredId(options.workstreamId, 'workstreamId');
|
|
101
|
+
const fromAgent = requiredId(options.fromAgent, 'fromAgent');
|
|
102
|
+
const toAgent = requiredId(options.toAgent, 'toAgent');
|
|
103
|
+
const storeOptions = options.storeOptions || {};
|
|
104
|
+
const repositoryOptions = options.repositoryOptions || {};
|
|
105
|
+
const generatedAtMs = options.generatedAtMs == null ? Date.now() : Number(options.generatedAtMs);
|
|
106
|
+
if (!Number.isSafeInteger(generatedAtMs)) {
|
|
107
|
+
throw new WorkGraphStateError('generatedAtMs must be a JavaScript-safe integer');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const store = WorkGraphStore.forRepository(repoPath, storeOptions);
|
|
111
|
+
store.submitObservation(passiveObservation(repoPath, repositoryOptions));
|
|
112
|
+
const handoff = store.handoff(workstreamId, fromAgent, toAgent, generatedAtMs);
|
|
113
|
+
const view = store.resume(workstreamId, 128);
|
|
114
|
+
const outstanding = continuationOutstandingRefs(view);
|
|
115
|
+
const continuationProof = store.continuationProof(handoff, {
|
|
116
|
+
outstanding_work_refs: outstanding,
|
|
117
|
+
created_at_ms: generatedAtMs,
|
|
118
|
+
});
|
|
119
|
+
return { handoff, continuation_proof: continuationProof };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
MAX_RESUME_EVIDENCE,
|
|
124
|
+
MAX_WORK_ID_CHARS,
|
|
125
|
+
handoffRepository,
|
|
126
|
+
handoffRepositoryWithProof,
|
|
127
|
+
resumeRepository,
|
|
128
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { WorkGraphObservation } from "./work_graph";
|
|
2
|
+
|
|
3
|
+
export interface RepositoryDiscoveryOptions {
|
|
4
|
+
agentId?: string;
|
|
5
|
+
sessionId?: string;
|
|
6
|
+
taskHint?: Record<string, unknown> | null;
|
|
7
|
+
defaultBranch?: string;
|
|
8
|
+
maxCommits?: number;
|
|
9
|
+
observedAtMs?: number;
|
|
10
|
+
includeCheckpoint?: boolean;
|
|
11
|
+
checkpointDir?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RepositoryIdentity {
|
|
15
|
+
repo_id: string;
|
|
16
|
+
root: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class RepositoryDiscoveryError extends Error {}
|
|
20
|
+
|
|
21
|
+
export function discoverRepositoryIdentity(path?: string): RepositoryIdentity;
|
|
22
|
+
|
|
23
|
+
export function discoverRepositoryObservation(
|
|
24
|
+
path?: string,
|
|
25
|
+
options?: RepositoryDiscoveryOptions,
|
|
26
|
+
): WorkGraphObservation;
|