deadbyte-mcp 0.10.0 → 0.11.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/MANIFEST.SHA256 +69 -55
- package/README.txt +13 -14
- package/bin/WORKER-REGISTRY.txt +3 -3
- package/bin/appcontainer-stage.exe +0 -0
- package/bin/appcontainer-stage.obj +0 -0
- package/bin/bootstrap-advapi32.exe +0 -0
- package/bin/bootstrap-advapi32.obj +0 -0
- package/bin/bootstrap-exitcode.exe +0 -0
- package/bin/bootstrap-exitcode.obj +0 -0
- package/bin/bootstrap-kernel32.exe +0 -0
- package/bin/bootstrap-kernel32.obj +0 -0
- package/bin/child-control-probe.exe +0 -0
- package/bin/child-control-probe.obj +0 -0
- package/bin/child-control-stage.exe +0 -0
- package/bin/child-control-stage.obj +0 -0
- package/bin/contained-reverse-worker.exe +0 -0
- package/bin/contained-reverse-worker.obj +0 -0
- package/bin/contained-transform-worker.exe +0 -0
- package/bin/contained-transform-worker.obj +0 -0
- package/bin/containment-probe.exe +0 -0
- package/bin/containment-probe.obj +0 -0
- package/bin/deadbyte-contain.exe +0 -0
- package/bin/deadbyte-contain.obj +0 -0
- package/bin/deadbyte-exec.exe +0 -0
- package/bin/deadbyte-exec.obj +0 -0
- package/bin/deadbyte-process-host.exe +0 -0
- package/bin/deadbyte-process-host.obj +0 -0
- package/bin/deadbyte-tunnel-host.exe +0 -0
- package/bin/deadbyte-tunnel-host.obj +0 -0
- package/controller/README.TXT +3 -3
- package/controller/deadbyte-controller.ps1 +1 -1
- package/controller/deadbyte-process-policy.json +1 -1
- package/docs/ARCHITECTURE.md +1 -1
- package/package.json +1 -1
- package/scripts/deferred-slot-operation.mjs +1 -1
- package/scripts/final-closure-r36.mjs +113 -0
- package/scripts/final-closure-verify-r36.mjs +208 -0
- package/scripts/final-closure-verify.mjs +48 -48
- package/scripts/final-closure.mjs +13 -13
- package/scripts/gate-windows-r36.ps1 +42 -0
- package/scripts/gate-windows.ps1 +3 -3
- package/scripts/release-parity-r36.mjs +200 -0
- package/scripts/release-parity-tests.ps1 +2 -2
- package/scripts/release-parity.mjs +37 -37
- package/scripts/windows-gate-evidence-r36.mjs +52 -0
- package/scripts/windows-gate-evidence.mjs +5 -5
- package/src/agent-memory-plane.mjs +98 -0
- package/src/autonomous-output-contracts.mjs +5 -2
- package/src/autonomous-runtime.mjs +155 -31
- package/src/autonomous-timeout-recovery.mjs +27 -0
- package/src/deadbyte-cli.mjs +37 -76
- package/src/effect-ledger.mjs +47 -0
- package/src/observation-lease.mjs +32 -0
- package/src/remote-live-log.mjs +59 -0
- package/src/remote-mcp-autonomous-smoke-client.mjs +15 -9
- package/src/version.mjs +1 -1
- package/src/worker-scheduler.mjs +53 -0
- package/test/autonomous-output-contract.test.mjs +1 -1
- package/test/core.test.mjs +1 -1
- package/test/production-docs.test.mjs +5 -5
- package/test/r33-closeout-regression.test.mjs +4 -4
- package/test/r33-finalization.test.mjs +6 -6
- package/test/r36-finalization.test.mjs +5 -5
- package/test/r36-release-identity.test.mjs +12 -19
- package/test/r37-agent-fabric.test.mjs +92 -0
- package/test/r37-autonomous-timeout-recovery.test.mjs +60 -0
- package/test/r37-finalization.test.mjs +74 -0
- package/test/release-version.test.mjs +42 -36
- package/test/remote-live-log-r37.test.mjs +47 -0
- package/test/remote-session-cli.test.mjs +27 -50
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from './canonical-json.mjs';
|
|
3
|
+
|
|
4
|
+
export const EFFECT_LEDGER_SCHEMA='deadbyte.effect-ledger.v1';
|
|
5
|
+
const HEX64=/^[0-9a-f]{64}$/;
|
|
6
|
+
const ID=/^[A-Za-z0-9._-]{1,128}$/;
|
|
7
|
+
const sha=value=>createHash('sha256').update(Buffer.from(canonicalJson(value),'utf8')).digest('hex');
|
|
8
|
+
|
|
9
|
+
function assertHex(name,value){if(!HEX64.test(value??'')) throw new Error(`${name} must be sha256`);}
|
|
10
|
+
function assertId(name,value){if(!ID.test(value??'')) throw new Error(`${name} invalid`);}
|
|
11
|
+
|
|
12
|
+
export function effectIdentity({goalId,planId,stepId,mutationEpoch,action,observationHeadSha256=null}){
|
|
13
|
+
assertHex('goalId',goalId);
|
|
14
|
+
assertHex('planId',planId);
|
|
15
|
+
assertId('stepId',stepId);
|
|
16
|
+
if(!Number.isInteger(mutationEpoch)||mutationEpoch<0) throw new Error('mutationEpoch invalid');
|
|
17
|
+
if(!action||typeof action!=='object'||Array.isArray(action)||typeof action.kind!=='string') throw new Error('action invalid');
|
|
18
|
+
if(observationHeadSha256!==null) assertHex('observationHeadSha256',observationHeadSha256);
|
|
19
|
+
const effectShape={schema:EFFECT_LEDGER_SCHEMA,kind:action.kind,mutation_epoch:mutationEpoch,
|
|
20
|
+
observation_head_sha256:observationHeadSha256,action};
|
|
21
|
+
const fingerprint=sha(effectShape);
|
|
22
|
+
const identity={schema:EFFECT_LEDGER_SCHEMA,goal_id:goalId,plan_id:planId,step_id:stepId,
|
|
23
|
+
mutation_epoch:mutationEpoch,fingerprint};
|
|
24
|
+
return Object.freeze({...identity,effect_id:sha(identity)});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function loopGuardDisposition(events,candidate,{maxRepeats=3,window=24}={}){
|
|
28
|
+
if(!Array.isArray(events)) throw new Error('effect events must be array');
|
|
29
|
+
if(!candidate||!HEX64.test(candidate.fingerprint??'')) throw new Error('effect candidate invalid');
|
|
30
|
+
if(!Number.isInteger(maxRepeats)||maxRepeats<1) throw new Error('maxRepeats invalid');
|
|
31
|
+
if(!Number.isInteger(window)||window<1) throw new Error('window invalid');
|
|
32
|
+
const recent=events.slice(-window);
|
|
33
|
+
const started=recent.filter(event=>event?.type==='effect_started'&&event.payload?.fingerprint===candidate.fingerprint);
|
|
34
|
+
const terminal=new Set(recent.filter(event=>['effect_completed','effect_failed'].includes(event?.type)&&HEX64.test(event.payload?.effect_id??''))
|
|
35
|
+
.map(event=>event.payload.effect_id));
|
|
36
|
+
const repeated=started.filter(event=>terminal.has(event.payload?.effect_id)).length;
|
|
37
|
+
return Object.freeze({allowed:repeated<maxRepeats,reason:repeated<maxRepeats?'allowed':'effect_repeat_limit',
|
|
38
|
+
repeated_count:repeated,max_repeats:maxRepeats,fingerprint:candidate.fingerprint});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function bindEffectCompletion(effect,outcome){
|
|
42
|
+
if(!effect||!HEX64.test(effect.effect_id??'')||!HEX64.test(effect.fingerprint??'')) throw new Error('effect identity invalid');
|
|
43
|
+
if(!outcome||typeof outcome!=='object'||Array.isArray(outcome)) throw new Error('effect outcome invalid');
|
|
44
|
+
const result_sha256=sha(outcome);
|
|
45
|
+
return Object.freeze({schema:'deadbyte.effect-completion.v1',effect_id:effect.effect_id,
|
|
46
|
+
fingerprint:effect.fingerprint,result_sha256});
|
|
47
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from './canonical-json.mjs';
|
|
3
|
+
|
|
4
|
+
export const OBSERVATION_LEASE_SCHEMA='deadbyte.observation-lease.v1';
|
|
5
|
+
const HEX64=/^[0-9a-f]{64}$/;
|
|
6
|
+
const sha=value=>createHash('sha256').update(Buffer.from(canonicalJson(value),'utf8')).digest('hex');
|
|
7
|
+
|
|
8
|
+
export function issueObservationLease({goalId,mutationEpoch,observations,ttlMs=30000,nowMs=Date.now()}){
|
|
9
|
+
if(!HEX64.test(goalId??'')) throw new Error('goalId must be sha256');
|
|
10
|
+
if(!Number.isInteger(mutationEpoch)||mutationEpoch<0) throw new Error('mutationEpoch invalid');
|
|
11
|
+
if(!Array.isArray(observations)||observations.length<1) throw new Error('observations required');
|
|
12
|
+
if(!Number.isInteger(ttlMs)||ttlMs<100) throw new Error('ttlMs invalid');
|
|
13
|
+
if(!Number.isFinite(nowMs)||nowMs<0) throw new Error('nowMs invalid');
|
|
14
|
+
const latest=[...observations].reverse().find(item=>HEX64.test(item?.event_sha256??'')||HEX64.test(item?.receipt_sha256??'')||HEX64.test(item?.result_head_sha256??''));
|
|
15
|
+
if(!latest) throw new Error('observation identity unavailable');
|
|
16
|
+
const observation_head_sha256=HEX64.test(latest.result_head_sha256??'')?latest.result_head_sha256:
|
|
17
|
+
HEX64.test(latest.receipt_sha256??'')?latest.receipt_sha256:latest.event_sha256;
|
|
18
|
+
const body={schema:OBSERVATION_LEASE_SCHEMA,goal_id:goalId,mutation_epoch:mutationEpoch,observation_head_sha256,
|
|
19
|
+
issued_at_ms:nowMs,expires_at_ms:nowMs+ttlMs};
|
|
20
|
+
return Object.freeze({...body,lease_id:sha(body)});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function verifyObservationLease(lease,{goalId,currentMutationEpoch,nowMs=Date.now(),observationHeadSha256=null}){
|
|
24
|
+
if(!lease||lease.schema!==OBSERVATION_LEASE_SCHEMA||!HEX64.test(lease.lease_id??'')) throw new Error('observation lease invalid');
|
|
25
|
+
const {lease_id,...body}=lease;
|
|
26
|
+
if(sha(body)!==lease_id) throw new Error('observation lease identity mismatch');
|
|
27
|
+
if(lease.goal_id!==goalId) throw new Error('observation lease goal mismatch');
|
|
28
|
+
if(lease.mutation_epoch!==currentMutationEpoch) throw new Error('observation lease stale mutation epoch');
|
|
29
|
+
if(nowMs>lease.expires_at_ms) throw new Error('observation lease expired');
|
|
30
|
+
if(observationHeadSha256!==null&&lease.observation_head_sha256!==observationHeadSha256) throw new Error('observation lease head mismatch');
|
|
31
|
+
return Object.freeze({ok:true,lease_id:lease.lease_id,observation_head_sha256:lease.observation_head_sha256,mutation_epoch:lease.mutation_epoch});
|
|
32
|
+
}
|
package/src/remote-live-log.mjs
CHANGED
|
@@ -74,3 +74,62 @@ export function createLiveActivityTracker(){
|
|
|
74
74
|
}
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
function shortSpanId(requestId){
|
|
80
|
+
return typeof requestId==='string'&&requestId.length?requestId.slice(0,8):'????????';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createLiveTimelineTracker({clock=()=>Date.now()}={}){
|
|
84
|
+
if(typeof clock!=='function') throw new Error('live timeline clock must be a function');
|
|
85
|
+
const pending=new Map();
|
|
86
|
+
const completed=new Set();
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
observe(event){
|
|
89
|
+
if(!event||typeof event!=='object'||Array.isArray(event)) return null;
|
|
90
|
+
const requestId=typeof event.request_id==='string'&&event.request_id?event.request_id:null;
|
|
91
|
+
const tool=typeof event.tool==='string'&&event.tool?event.tool:null;
|
|
92
|
+
if(event.event_type==='tool.request'&&requestId&&tool){
|
|
93
|
+
if(pending.has(requestId)||completed.has(requestId)) return null;
|
|
94
|
+
pending.set(requestId,{request_id:requestId,tool,started_at_ms:clock()});
|
|
95
|
+
return `▶ [${shortSpanId(requestId)}] RUN ${tool}${summarizeArgs(event.args)}`;
|
|
96
|
+
}
|
|
97
|
+
if(event.event_type==='tool.result'&&requestId&&tool){
|
|
98
|
+
if(completed.has(requestId)) return null;
|
|
99
|
+
const span=pending.get(requestId)??null;
|
|
100
|
+
pending.delete(requestId);completed.add(requestId);
|
|
101
|
+
const error=event.outcome?.is_error===true;
|
|
102
|
+
const duration=Number.isFinite(Number(event.outcome?.duration_ms))
|
|
103
|
+
? formatDuration(event.outcome.duration_ms)
|
|
104
|
+
: span?formatDuration(Math.max(0,clock()-span.started_at_ms)):'';
|
|
105
|
+
const marker=span?(error?'✗':'✓'):'↩';
|
|
106
|
+
const status=error?'ERR':'OK ';
|
|
107
|
+
const suffix=span?'':' (request before live window)';
|
|
108
|
+
return `${marker} [${shortSpanId(requestId)}] ${status} ${tool}${duration?` ${duration}`:''}${suffix}`;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
},
|
|
112
|
+
pending(){return [...pending.values()].map(({request_id,tool})=>({request_id,tool}));},
|
|
113
|
+
reconcileInterrupted(reason='session_end'){
|
|
114
|
+
const suffix=reason==='recovery'?'recovery ended before terminal result':
|
|
115
|
+
reason==='session_end'?'session ended before terminal result':'stream ended before terminal result';
|
|
116
|
+
const lines=[...pending.values()].map(item=>`⚠ [${shortSpanId(item.request_id)}] INTERRUPTED ${item.tool} — ${suffix}`);
|
|
117
|
+
pending.clear();
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
export function formatRuntimeDiagnosticLine(file,line){
|
|
125
|
+
if(typeof file!=='string'||typeof line!=='string'||!line) return null;
|
|
126
|
+
const label=file.split(/[\\/]/).at(-1).replace(/\.log$/i,'').toUpperCase();
|
|
127
|
+
const input=line.match(/\[TOOL IN \]\s+\S+\s+#\d+\s+([A-Za-z0-9_.-]+)/);
|
|
128
|
+
if(input) return `[${label}] IN ${input[1]}`;
|
|
129
|
+
const output=line.match(/\[TOOL OUT\]\s+\S+\s+#\d+\s+([A-Za-z0-9_.-]+)/);
|
|
130
|
+
if(output){
|
|
131
|
+
if(/"is_error"\s*:\s*true/.test(line)) return `[${label}] ERR ${output[1]}`;
|
|
132
|
+
return `[${label}] OUT ${output[1]}`;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
|
|
6
6
|
import { R36_COMPACT_GATEWAYS, R36_COMPACT_TOOL_LIMIT } from './remote-mcp-r36-parity-client.mjs';
|
|
7
7
|
import { promptContexts, promptObservations, promptMutationEpoch } from './autonomous-live-state.mjs';
|
|
8
|
+
import { classifyAutonomousTimeoutRecovery } from './autonomous-timeout-recovery.mjs';
|
|
8
9
|
|
|
9
10
|
const endpointText = process.argv[2];
|
|
10
11
|
if (!endpointText) throw new Error('usage: node src/remote-mcp-autonomous-smoke-client.mjs https://host/mcp/path');
|
|
@@ -26,22 +27,21 @@ function structured(result, label) {
|
|
|
26
27
|
if (!result.structuredContent || typeof result.structuredContent !== 'object') throw new Error(`${label} returned no structured object`);
|
|
27
28
|
return result.structuredContent;
|
|
28
29
|
}
|
|
29
|
-
async function recoverTimedOutGoal(client, goalId) {
|
|
30
|
+
async function recoverTimedOutGoal(client, goalId, beforeJournalHead) {
|
|
30
31
|
for (let poll = 0; poll < AUTONOMOUS_TIMEOUT_RECOVERY_MAX_POLLS; poll += 1) {
|
|
31
32
|
const status = structured(await client.callTool(
|
|
32
33
|
{ name:'autonomous_goal_status', arguments:{ goal_id:goalId } },
|
|
33
34
|
{ timeout:30000, maxTotalTimeout:30000 }
|
|
34
35
|
), 'autonomous_goal_status');
|
|
35
|
-
|
|
36
|
-
if (
|
|
36
|
+
const disposition=classifyAutonomousTimeoutRecovery(status,{beforeJournalHead});
|
|
37
|
+
if (disposition.kind === 'completed') return { kind:'completed', status };
|
|
38
|
+
if (disposition.kind === 'terminal_failure') {
|
|
37
39
|
throw new Error(`autonomous goal became terminal after request timeout: ${JSON.stringify(status)}`);
|
|
38
40
|
}
|
|
39
|
-
if (
|
|
40
|
-
throw new Error(`autonomous goal returned unexpected recovery state: ${JSON.stringify(status)}`);
|
|
41
|
-
}
|
|
41
|
+
if (disposition.kind === 'resumable') return { kind:'resumable', status };
|
|
42
42
|
await sleep(AUTONOMOUS_TIMEOUT_RECOVERY_POLL_MS);
|
|
43
43
|
}
|
|
44
|
-
throw new Error('autonomous goal did not reach a durable terminal
|
|
44
|
+
throw new Error('autonomous goal did not reach a durable terminal or resumable boundary after request timeout');
|
|
45
45
|
}
|
|
46
46
|
function messageText(content) {
|
|
47
47
|
if (typeof content === 'string') return content;
|
|
@@ -137,6 +137,10 @@ try {
|
|
|
137
137
|
let ran = null;
|
|
138
138
|
let recoveredRequestTimeout = false;
|
|
139
139
|
for (let requestRound = 0; requestRound < AUTONOMOUS_RUN_MAX_REQUESTS; requestRound += 1) {
|
|
140
|
+
const beforeRun=structured(await client.callTool(
|
|
141
|
+
{ name:'autonomous_goal_status', arguments:{ goal_id:goalId } },
|
|
142
|
+
{ timeout:30000, maxTotalTimeout:30000 }
|
|
143
|
+
), 'autonomous_goal_status');
|
|
140
144
|
try {
|
|
141
145
|
ran = structured(await client.callTool(
|
|
142
146
|
{ name:'autonomous_goal_run', arguments:{ goal_id:goalId, max_cycles:AUTONOMOUS_RUN_MAX_CYCLES_PER_REQUEST } },
|
|
@@ -144,8 +148,10 @@ try {
|
|
|
144
148
|
), 'autonomous_goal_run');
|
|
145
149
|
} catch (error) {
|
|
146
150
|
if (String(error?.code ?? '') !== 'REQUEST_TIMEOUT') throw error;
|
|
147
|
-
const recovered = await recoverTimedOutGoal(client, goalId);
|
|
148
|
-
ran =
|
|
151
|
+
const recovered = await recoverTimedOutGoal(client, goalId, beforeRun.journal_head_sha256);
|
|
152
|
+
ran = recovered.kind === 'completed'
|
|
153
|
+
? { status:'completed', state:recovered.status.state, goal_id:goalId, recovered_from_timeout:true }
|
|
154
|
+
: { status:'yielded', state:recovered.status.state, goal_id:goalId, recovered_from_timeout:true };
|
|
149
155
|
recoveredRequestTimeout = true;
|
|
150
156
|
}
|
|
151
157
|
if (ran.status === 'completed' && ran.state === 'succeeded') break;
|
package/src/version.mjs
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from './canonical-json.mjs';
|
|
3
|
+
|
|
4
|
+
export const WORKER_SCHEDULER_SCHEMA='deadbyte.worker-scheduler.v1';
|
|
5
|
+
const sha=value=>createHash('sha256').update(Buffer.from(canonicalJson(value),'utf8')).digest('hex');
|
|
6
|
+
|
|
7
|
+
export function createWorkerScheduler({maxConcurrent=1,totalCpuUnits=1,totalMemoryMb=1024,leaseMs=60000,clock=()=>Date.now()}={}){
|
|
8
|
+
if(!Number.isInteger(maxConcurrent)||maxConcurrent<1) throw new Error('maxConcurrent invalid');
|
|
9
|
+
if(!Number.isInteger(totalCpuUnits)||totalCpuUnits<1) throw new Error('totalCpuUnits invalid');
|
|
10
|
+
if(!Number.isInteger(totalMemoryMb)||totalMemoryMb<1) throw new Error('totalMemoryMb invalid');
|
|
11
|
+
if(!Number.isInteger(leaseMs)||leaseMs<100) throw new Error('leaseMs invalid');
|
|
12
|
+
const active=new Map(),attempts=new Map(),history=[];
|
|
13
|
+
function reclaim(){
|
|
14
|
+
const now=clock();
|
|
15
|
+
for(const [id,lease] of active) if(lease.expires_at_ms<=now){
|
|
16
|
+
active.delete(id);history.push({...lease,status:'expired',released_at_ms:now});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function usage(){
|
|
20
|
+
return [...active.values()].reduce((x,l)=>({cpu:x.cpu+l.cpu_units,memory:x.memory+l.memory_mb}),{cpu:0,memory:0});
|
|
21
|
+
}
|
|
22
|
+
function snapshot(){
|
|
23
|
+
reclaim();
|
|
24
|
+
const used=usage();
|
|
25
|
+
return Object.freeze({schema:WORKER_SCHEDULER_SCHEMA,max_concurrent:maxConcurrent,total_cpu_units:totalCpuUnits,total_memory_mb:totalMemoryMb,
|
|
26
|
+
active:[...active.values()].sort((a,b)=>a.lease_id.localeCompare(b.lease_id,'en')),
|
|
27
|
+
history:[...history],used_cpu_units:used.cpu,used_memory_mb:used.memory});
|
|
28
|
+
}
|
|
29
|
+
function acquire({taskId,workerClass,cpuUnits=1,memoryMb=128}){
|
|
30
|
+
reclaim();
|
|
31
|
+
if(typeof taskId!=='string'||!taskId) throw new Error('taskId invalid');
|
|
32
|
+
if(typeof workerClass!=='string'||!workerClass) throw new Error('workerClass invalid');
|
|
33
|
+
if(!Number.isInteger(cpuUnits)||cpuUnits<1||!Number.isInteger(memoryMb)||memoryMb<1) throw new Error('worker resources invalid');
|
|
34
|
+
const used=usage();
|
|
35
|
+
if(active.size>=maxConcurrent||used.cpu+cpuUnits>totalCpuUnits||used.memory+memoryMb>totalMemoryMb) throw new Error('worker scheduler capacity exceeded');
|
|
36
|
+
const attempt=(attempts.get(taskId)??0)+1;attempts.set(taskId,attempt);
|
|
37
|
+
const issued=clock(),expires=issued+leaseMs;
|
|
38
|
+
const body={schema:'deadbyte.worker-lease.v1',task_id:taskId,worker_class:workerClass,attempt,cpu_units:cpuUnits,memory_mb:memoryMb,issued_at_ms:issued,expires_at_ms:expires};
|
|
39
|
+
const lease=Object.freeze({...body,lease_id:sha(body)});
|
|
40
|
+
active.set(lease.lease_id,lease);
|
|
41
|
+
return lease;
|
|
42
|
+
}
|
|
43
|
+
function heartbeat(leaseId){
|
|
44
|
+
reclaim();const current=active.get(leaseId);if(!current) throw new Error('worker lease unavailable');
|
|
45
|
+
const next=Object.freeze({...current,expires_at_ms:clock()+leaseMs});active.set(leaseId,next);return next;
|
|
46
|
+
}
|
|
47
|
+
function release(leaseId,{status='completed'}={}){
|
|
48
|
+
reclaim();const lease=active.get(leaseId);if(!lease) throw new Error('worker lease unavailable');
|
|
49
|
+
active.delete(leaseId);const item={...lease,status,released_at_ms:clock()};history.push(item);
|
|
50
|
+
return Object.freeze({status:'released',lease:item});
|
|
51
|
+
}
|
|
52
|
+
return Object.freeze({acquire,heartbeat,release,snapshot});
|
|
53
|
+
}
|
|
@@ -48,7 +48,7 @@ test('R34 run/status output contracts accept only the sanitized public loop proj
|
|
|
48
48
|
assert.equal(run.structuredContent.loop.phase,'ACTING');
|
|
49
49
|
const status = autonomousStructuredResult(autonomousGoalStatusOutputSchema,{
|
|
50
50
|
status:'ok',goal_id:'1'.repeat(64),goal_sha256:'2'.repeat(64),title:'x',root_id:'project',journal_verified:true,
|
|
51
|
-
|
|
51
|
+
run_busy:false,run_owner_pid:null,run_lock_created_at_utc:null,...baseDerived(),loop:publicLoop
|
|
52
52
|
});
|
|
53
53
|
assert.equal(status.structuredContent.loop.pending_action.kind,'patch');
|
|
54
54
|
const approved = autonomousStructuredResult(autonomousGoalApproveOutputSchema,{
|
package/test/core.test.mjs
CHANGED
|
@@ -105,7 +105,7 @@ test('proof execution creates deterministic output and signed verifiable receipt
|
|
|
105
105
|
assert.equal(result.receipt.request.schema, 'deadbyte.request.v1');
|
|
106
106
|
assert.match(result.receipt.request.nonce, /^[0-9a-f]{64}$/);
|
|
107
107
|
assert.match(result.receipt.request.sha256, /^[0-9a-f]{64}$/);
|
|
108
|
-
assert.equal(result.receipt.runtime.package_version, '0.
|
|
108
|
+
assert.equal(result.receipt.runtime.package_version, '0.11.0');
|
|
109
109
|
assert.match(result.receipt.runtime.mcp_ingress_adapter, /^(?:none|tunnel-stdio-probe-compat-v1)$/);
|
|
110
110
|
assert.match(result.receipt.runtime.mcp_ingress_adapter_sha256, /^[0-9a-f]{64}$/);
|
|
111
111
|
assert.match(result.receipt.runtime.mcp_ingress_mode_source_sha256, /^[0-9a-f]{64}$/);
|
|
@@ -9,9 +9,9 @@ const readme=()=>readFile(path.join(root,'README.txt'),'utf8');
|
|
|
9
9
|
|
|
10
10
|
test('production README documents one-command stdio configs for major MCP clients',async()=>{
|
|
11
11
|
const text=await readme();
|
|
12
|
-
assert.match(text,/npx -y deadbyte-mcp@0\.
|
|
12
|
+
assert.match(text,/npx -y deadbyte-mcp@0\.11\.0 mcp/);
|
|
13
13
|
assert.match(text,/CLAUDE DESKTOP/);
|
|
14
|
-
assert.match(text,/"mcpServers"[\s\S]*?"command"\s*:\s*"npx"[\s\S]*?"-y"[\s\S]*?"deadbyte-mcp@0\.
|
|
14
|
+
assert.match(text,/"mcpServers"[\s\S]*?"command"\s*:\s*"npx"[\s\S]*?"-y"[\s\S]*?"deadbyte-mcp@0\.11\.0"[\s\S]*?"mcp"/);
|
|
15
15
|
assert.match(text,/CURSOR/);
|
|
16
16
|
assert.match(text,/\.cursor\/mcp\.json/);
|
|
17
17
|
assert.match(text,/VS CODE/);
|
|
@@ -20,7 +20,7 @@ test('production README documents one-command stdio configs for major MCP client
|
|
|
20
20
|
assert.match(text,/CODEX/);
|
|
21
21
|
assert.match(text,/\[mcp_servers\.deadbyte\]/);
|
|
22
22
|
assert.match(text,/command\s*=\s*"npx"/);
|
|
23
|
-
assert.match(text,/args\s*=\s*\["-y",\s*"deadbyte-mcp@0\.
|
|
23
|
+
assert.match(text,/args\s*=\s*\["-y",\s*"deadbyte-mcp@0\.11\.0",\s*"mcp"\]/);
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
test('production README records tracer bullet, semantic scope, remote UX, safety and document extension',async()=>{
|
|
@@ -29,8 +29,8 @@ test('production README records tracer bullet, semantic scope, remote UX, safety
|
|
|
29
29
|
for(const token of ['coding_files_read','coding_symbol_outline','coding_symbol_definition','coding_symbol_references','coding_symbol_replace']) assert.match(text,new RegExp(token));
|
|
30
30
|
assert.match(text,/JavaScript\/TypeScript|JS\/TS/);
|
|
31
31
|
assert.match(text,/does not claim|not claim|limited to/i);
|
|
32
|
-
assert.
|
|
33
|
-
assert.match(text,/
|
|
32
|
+
assert.doesNotMatch(text,/remote --verbose/);
|
|
33
|
+
assert.match(text,/unified live timeline/i);
|
|
34
34
|
assert.match(text,/workspace allowlist|coding-policy root/i);
|
|
35
35
|
assert.match(text,/destructive|standing grant|authority plane/i);
|
|
36
36
|
assert.match(text,/document adapter/i);
|
|
@@ -86,7 +86,7 @@ test('audit restart reconciles an orphan request with an explicit interrupted re
|
|
|
86
86
|
}finally{await rm(stateRoot,{recursive:true,force:true});}
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
test('Windows
|
|
89
|
+
test('Windows R37 gate emits manifest-bound machine-readable evidence outside the candidate tree', async()=>{
|
|
90
90
|
const stateRoot=await temp('deadbyte-r33-windows-evidence-');
|
|
91
91
|
try{
|
|
92
92
|
const gate=await readFile(path.join(root,'scripts','gate-windows.ps1'),'utf8');
|
|
@@ -99,12 +99,12 @@ test('Windows R36 gate emits manifest-bound machine-readable evidence outside th
|
|
|
99
99
|
assert.equal(summary.status,'written');
|
|
100
100
|
const evidence=JSON.parse(await readFile(summary.path,'utf8'));
|
|
101
101
|
const manifestSha=sha256(await readFile(path.join(root,'MANIFEST.SHA256')));
|
|
102
|
-
assert.equal(evidence.schema,'deadbyte.
|
|
102
|
+
assert.equal(evidence.schema,'deadbyte.r37-windows-gate.v1');
|
|
103
103
|
assert.equal(evidence.status,'passed');
|
|
104
104
|
assert.equal(evidence.exit_code,0);
|
|
105
|
-
assert.equal(evidence.version,'0.
|
|
105
|
+
assert.equal(evidence.version,'0.11.0');
|
|
106
106
|
assert.equal(evidence.manifest_sha256,manifestSha);
|
|
107
|
-
assert.equal(evidence.marker,'DEADBYTE V0.
|
|
107
|
+
assert.equal(evidence.marker,'DEADBYTE V0.11.0 / R37 WINDOWS BATCH GATE: PASS');
|
|
108
108
|
assert.ok(summary.path.startsWith(path.join(stateRoot,'evidence','release')));
|
|
109
109
|
}finally{await rm(stateRoot,{recursive:true,force:true});}
|
|
110
110
|
});
|
|
@@ -26,17 +26,17 @@ test('release parity and final verifier are package entry points', async () => {
|
|
|
26
26
|
assert.equal(typeof verifyFinalClosureObject, 'function');
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
test('
|
|
29
|
+
test('R37 final closure verifier is implementation-independent from production helpers', async () => {
|
|
30
30
|
const verifier = await text('scripts/final-closure-verify.mjs');
|
|
31
31
|
const writer = await text('scripts/final-closure.mjs');
|
|
32
32
|
assert.doesNotMatch(verifier, /from ['"]\.\.\/src\//);
|
|
33
33
|
assert.match(verifier, /DEADBYTE-R27-SLOT-UPDATE-V1/);
|
|
34
34
|
assert.match(verifier, /DEADBYTE-MACHINE-PING-V1/);
|
|
35
|
-
assert.match(verifier, /DEADBYTE-
|
|
36
|
-
assert.match(writer, /pointer\.version==='0\.
|
|
37
|
-
assert.match(verifier, /envelope\.body\.version==='0\.
|
|
38
|
-
assert.match(verifier, /release_label==='
|
|
39
|
-
assert.match(verifier, /
|
|
35
|
+
assert.match(verifier, /DEADBYTE-R37-FINAL-CLOSURE-V1/);
|
|
36
|
+
assert.match(writer, /pointer\.version==='0\.11\.0'/);
|
|
37
|
+
assert.match(verifier, /envelope\.body\.version==='0\.11\.0'/);
|
|
38
|
+
assert.match(verifier, /release_label==='R37 FINAL MACHINE BASELINE'/);
|
|
39
|
+
assert.match(verifier, /8d1e5f09745e764796b60e0a8a0168be7334ed4bf834fad5974a80eba295b4e8/);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
test('final closure verifier canonicalizes traversal order before exact-tree comparison', async () => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
|
-
import { validateParityEvidence, R35_PREDECESSOR_RELEASE_ID, R35_PREDECESSOR_MANIFEST } from '../scripts/release-parity.mjs';
|
|
5
|
-
import { verifyFinalClosureObject } from '../scripts/final-closure-verify.mjs';
|
|
4
|
+
import { validateParityEvidence, R35_PREDECESSOR_RELEASE_ID, R35_PREDECESSOR_MANIFEST } from '../scripts/release-parity-r36.mjs';
|
|
5
|
+
import { verifyFinalClosureObject } from '../scripts/final-closure-verify-r36.mjs';
|
|
6
6
|
|
|
7
7
|
const root=new URL('../',import.meta.url);
|
|
8
8
|
const text=rel=>readFile(new URL(rel,root),'utf8');
|
|
@@ -45,7 +45,7 @@ test('R36 compact disarmed probe carries signed machine status/ping evidence int
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
test('R36 parity captures exact successor bridge before rollback and reuses it for restore',async()=>{
|
|
48
|
-
const parity=await text('scripts/release-parity.mjs');
|
|
48
|
+
const parity=await text('scripts/release-parity-r36.mjs');
|
|
49
49
|
assert.match(parity,/loadPowerShellDataFile/);
|
|
50
50
|
assert.match(parity,/successorBridge/);
|
|
51
51
|
assert.match(parity,/Bridge\?\.WorkingDirectory/);
|
|
@@ -72,7 +72,7 @@ test('R36 compact parity treats stale machine-fs precondition denial as expected
|
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
test('R36 final closure writer binds compact public evidence and R36 release evidence domains',async()=>{
|
|
75
|
-
const writer=await text('scripts/final-closure.mjs');
|
|
75
|
+
const writer=await text('scripts/final-closure-r36.mjs');
|
|
76
76
|
assert.match(writer,/DEADBYTE-R36-FINAL-CLOSURE-V1/);
|
|
77
77
|
assert.match(writer,/deadbyte\.r36-final-closure\.v1/);
|
|
78
78
|
assert.match(writer,/pointer\.version==='0\.10\.0'/);
|
|
@@ -86,7 +86,7 @@ test('R36 final closure writer binds compact public evidence and R36 release evi
|
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
test('R36 independent closure verifier binds full, compact, predecessor, restore, Windows gate and immutable slot',async()=>{
|
|
89
|
-
const verifier=await text('scripts/final-closure-verify.mjs');
|
|
89
|
+
const verifier=await text('scripts/final-closure-verify-r36.mjs');
|
|
90
90
|
assert.equal(typeof verifyFinalClosureObject,'function');
|
|
91
91
|
assert.doesNotMatch(verifier,/from ['"]\.\.\/src\//);
|
|
92
92
|
assert.match(verifier,/DEADBYTE-R36-FINAL-CLOSURE-V1/);
|
|
@@ -6,35 +6,28 @@ const root=new URL('../',import.meta.url);
|
|
|
6
6
|
const text=rel=>readFile(new URL(rel,root),'utf8');
|
|
7
7
|
const R35_RELEASE_ID='v0.9.0-b95e1ec1d9a7e26a';
|
|
8
8
|
const R35_MANIFEST='b95e1ec1d9a7e26a96ef492306c1be4df18c883ff03ab3b6015f677d7429ec8d';
|
|
9
|
+
const R36_RELEASE_ID='v0.10.0-8d1e5f09745e7647';
|
|
10
|
+
const R36_MANIFEST='8d1e5f09745e764796b60e0a8a0168be7334ed4bf834fad5974a80eba295b4e8';
|
|
9
11
|
|
|
10
|
-
test('R36
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
text('
|
|
14
|
-
text('
|
|
15
|
-
text('scripts/deferred-slot-operation.mjs'),text('scripts/release-parity.mjs'),text('scripts/final-closure.mjs'),
|
|
16
|
-
text('scripts/final-closure-verify.mjs'),text('scripts/windows-gate-evidence.mjs')
|
|
12
|
+
test('sealed R36 historical proof scripts preserve 0.10.0 / R36 and exact R35 predecessor',async()=>{
|
|
13
|
+
const [gate,parity,closure,closureVerify,windowsGate,currentParity]=await Promise.all([
|
|
14
|
+
text('scripts/gate-windows-r36.ps1'),text('scripts/release-parity-r36.mjs'),
|
|
15
|
+
text('scripts/final-closure-r36.mjs'),text('scripts/final-closure-verify-r36.mjs'),
|
|
16
|
+
text('scripts/windows-gate-evidence-r36.mjs'),text('scripts/release-parity.mjs')
|
|
17
17
|
]);
|
|
18
|
-
assert.equal(pkg.version,'0.10.0');
|
|
19
|
-
assert.match(version,/DEADBYTE_VERSION = '0\.10\.0'/);
|
|
20
|
-
assert.match(controller,/CONTROLLER V0\.10\.0 \/ R36/);
|
|
21
18
|
assert.match(gate,/DEADBYTE V0\.10\.0 \/ R36 WINDOWS BATCH GATE: PASS/);
|
|
22
|
-
assert.match(readme,/^DEADBYTE MCP V0\.10\.0 \/ R36 PRODUCTION BASELINE$/m);
|
|
23
|
-
assert.match(architecture,/^# DEADBYTE MCP V0\.10\.0 \/ R36 ARCHITECTURE$/m);
|
|
24
|
-
assert.match(controllerReadme,/^DEADBYTE MCP V0\.10\.0 \/ R36 CONTROLLER$/m);
|
|
25
|
-
assert.match(deferred,/--expected-version','0\.10\.0'/);
|
|
26
19
|
assert.match(parity,/deadbyte\.r36-parity\.v1/);
|
|
27
20
|
assert.match(parity,new RegExp(R35_RELEASE_ID.replaceAll('.','\\.')));
|
|
28
21
|
assert.match(parity,new RegExp(R35_MANIFEST));
|
|
29
22
|
assert.match(closure,/DEADBYTE-R36-FINAL-CLOSURE-V1/);
|
|
30
23
|
assert.match(closure,/version:'0\.10\.0'/);
|
|
31
|
-
assert.match(closure,/release_label:'R36 FINAL MACHINE BASELINE'/);
|
|
32
24
|
assert.match(closureVerify,/DEADBYTE-R36-FINAL-CLOSURE-V1/);
|
|
33
|
-
assert.match(
|
|
34
|
-
assert.match(
|
|
25
|
+
assert.match(windowsGate,/deadbyte\.r36-windows-gate\.v1/);
|
|
26
|
+
assert.match(currentParity,new RegExp(R36_RELEASE_ID.replaceAll('.','\\.')));
|
|
27
|
+
assert.match(currentParity,new RegExp(R36_MANIFEST));
|
|
35
28
|
});
|
|
36
29
|
|
|
37
|
-
test('
|
|
30
|
+
test('current R37 controller still binds production remote to compact profile and restores parent environment',async()=>{
|
|
38
31
|
const controller=await text('controller/deadbyte-controller.ps1');
|
|
39
32
|
assert.match(controller,/Test-Path Env:DEADBYTE_CAPABILITY_PROFILE/);
|
|
40
33
|
assert.match(controller,/\$previousCapabilityProfileEnv = \$env:DEADBYTE_CAPABILITY_PROFILE/);
|
|
@@ -43,7 +36,7 @@ test('R36 controller binds production remote to compact profile and restores par
|
|
|
43
36
|
assert.match(controller,/\$env:DEADBYTE_CAPABILITY_PROFILE = \$previousCapabilityProfileEnv/);
|
|
44
37
|
});
|
|
45
38
|
|
|
46
|
-
test('R36
|
|
39
|
+
test('R36 surface contract remains exact full 76 plus compact gateway closure for R37',async()=>{
|
|
47
40
|
const parity=await text('src/remote-mcp-r36-parity-client.mjs');
|
|
48
41
|
assert.match(parity,/R36_EXPECTED_TOOLS/);
|
|
49
42
|
assert.match(parity,/R36_EXPECTED_TOOLS\.length !== 76|tool_count !== 76|length === 76/);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { effectIdentity, loopGuardDisposition, bindEffectCompletion } from '../src/effect-ledger.mjs';
|
|
7
|
+
import { deriveAgentMemory, queryAgentMemory } from '../src/agent-memory-plane.mjs';
|
|
8
|
+
import { createWorkerScheduler } from '../src/worker-scheduler.mjs';
|
|
9
|
+
import { issueObservationLease, verifyObservationLease } from '../src/observation-lease.mjs';
|
|
10
|
+
|
|
11
|
+
const repo=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
12
|
+
|
|
13
|
+
test('R37 effect ledger detects semantically identical repeated effects across planner wording',()=>{
|
|
14
|
+
const base={goalId:'a'.repeat(64),planId:'b'.repeat(64),stepId:'fix',mutationEpoch:4,
|
|
15
|
+
action:{kind:'patch',operations:[{kind:'replace',path:'src/a.mjs',old_string:'x',new_string:'y'}]},
|
|
16
|
+
observationHeadSha256:'c'.repeat(64)};
|
|
17
|
+
const first=effectIdentity(base);
|
|
18
|
+
const second=effectIdentity({...base,plannerSummary:'different prose'});
|
|
19
|
+
assert.equal(first.fingerprint,second.fingerprint);
|
|
20
|
+
const events=[
|
|
21
|
+
{type:'effect_started',payload:{fingerprint:first.fingerprint,effect_id:first.effect_id}},
|
|
22
|
+
{type:'effect_completed',payload:{fingerprint:first.fingerprint,effect_id:first.effect_id}},
|
|
23
|
+
{type:'effect_started',payload:{fingerprint:first.fingerprint,effect_id:'d'.repeat(64)}},
|
|
24
|
+
{type:'effect_failed',payload:{fingerprint:first.fingerprint,effect_id:'d'.repeat(64)}}
|
|
25
|
+
];
|
|
26
|
+
const guard=loopGuardDisposition(events,second,{maxRepeats:2,window:16});
|
|
27
|
+
assert.equal(guard.allowed,false);
|
|
28
|
+
assert.equal(guard.reason,'effect_repeat_limit');
|
|
29
|
+
assert.equal(guard.repeated_count,2);
|
|
30
|
+
const completion=bindEffectCompletion(first,{status:'passed',receipt_sha256:'e'.repeat(64)});
|
|
31
|
+
assert.equal(completion.effect_id,first.effect_id);
|
|
32
|
+
assert.match(completion.result_sha256,/^[0-9a-f]{64}$/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('R37 memory plane derives deterministic core archival and graph views from journal',()=>{
|
|
36
|
+
const events=[
|
|
37
|
+
{seq:1,type:'goal_created',event_sha256:'1'.repeat(64),payload:{acceptance:'all gates pass'}},
|
|
38
|
+
{seq:2,type:'profile_completed',event_sha256:'2'.repeat(64),payload:{profile_id:'quick-test',passed:true,receipt_sha256:'a'.repeat(64)}},
|
|
39
|
+
{seq:3,type:'native_diff_committed',event_sha256:'3'.repeat(64),payload:{paths:['src/a.mjs'],diff_receipt_sha256:'b'.repeat(64),mutation_epoch:1}},
|
|
40
|
+
{seq:4,type:'planner_rejected',event_sha256:'4'.repeat(64),payload:{reason:'stale approach'}},
|
|
41
|
+
{seq:5,type:'waiting_entered',event_sha256:'5'.repeat(64),payload:{reason:'needs evidence'}}
|
|
42
|
+
];
|
|
43
|
+
const one=deriveAgentMemory(events);
|
|
44
|
+
const two=deriveAgentMemory(structuredClone(events));
|
|
45
|
+
assert.equal(one.memory_sha256,two.memory_sha256);
|
|
46
|
+
assert.deepEqual(one.core.acceptance_constraints,['all gates pass']);
|
|
47
|
+
assert.ok(one.archival.some(x=>x.type==='planner_rejected'));
|
|
48
|
+
assert.ok(one.graph.nodes.some(x=>x.id==='file:src/a.mjs'));
|
|
49
|
+
const local=queryAgentMemory(one,{query:'src a mjs',mode:'local',limit:8});
|
|
50
|
+
assert.ok(local.results.some(x=>JSON.stringify(x).includes('src/a.mjs')));
|
|
51
|
+
const global=queryAgentMemory(one,{query:'gates profiles blockers',mode:'global',limit:8});
|
|
52
|
+
assert.ok(global.results.length>0);
|
|
53
|
+
const drift=queryAgentMemory(one,{query:'src/a.mjs',mode:'drift',limit:8});
|
|
54
|
+
assert.ok(drift.results.length>0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('R37 worker scheduler enforces resources, leases and expiry recovery',()=>{
|
|
58
|
+
let now=1000;
|
|
59
|
+
const scheduler=createWorkerScheduler({maxConcurrent:2,totalCpuUnits:2,totalMemoryMb:1024,leaseMs:5000,clock:()=>now});
|
|
60
|
+
const a=scheduler.acquire({taskId:'test-a',workerClass:'test',cpuUnits:1,memoryMb:512});
|
|
61
|
+
const b=scheduler.acquire({taskId:'test-b',workerClass:'test',cpuUnits:1,memoryMb:512});
|
|
62
|
+
assert.equal(scheduler.snapshot().active.length,2);
|
|
63
|
+
assert.throws(()=>scheduler.acquire({taskId:'test-c',workerClass:'test',cpuUnits:1,memoryMb:128}),/capacity/i);
|
|
64
|
+
now=7000;
|
|
65
|
+
const c=scheduler.acquire({taskId:'test-c',workerClass:'test',cpuUnits:2,memoryMb:1024});
|
|
66
|
+
assert.equal(scheduler.snapshot().active.length,1);
|
|
67
|
+
assert.equal(c.task_id,'test-c');
|
|
68
|
+
assert.equal(scheduler.release(c.lease_id,{status:'passed'}).status,'released');
|
|
69
|
+
assert.equal(scheduler.snapshot().active.length,0);
|
|
70
|
+
assert.notEqual(a.lease_id,b.lease_id);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('R37 observation leases bind effects to fresh observed state and mutation epoch',()=>{
|
|
74
|
+
const lease=issueObservationLease({goalId:'a'.repeat(64),mutationEpoch:3,
|
|
75
|
+
observations:[{event_sha256:'b'.repeat(64),receipt_sha256:'c'.repeat(64),result_head_sha256:'d'.repeat(64)}],
|
|
76
|
+
ttlMs:1000,nowMs:5000});
|
|
77
|
+
assert.equal(verifyObservationLease(lease,{goalId:'a'.repeat(64),currentMutationEpoch:3,nowMs:5500}).ok,true);
|
|
78
|
+
assert.throws(()=>verifyObservationLease(lease,{goalId:'a'.repeat(64),currentMutationEpoch:4,nowMs:5500}),/stale mutation epoch/i);
|
|
79
|
+
assert.throws(()=>verifyObservationLease(lease,{goalId:'a'.repeat(64),currentMutationEpoch:3,nowMs:7000}),/expired/i);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('R37 autonomous runtime integrates memory, effect guard, worker scheduler and observation lease',async()=>{
|
|
83
|
+
const source=await readFile(path.join(repo,'src','autonomous-runtime.mjs'),'utf8');
|
|
84
|
+
assert.match(source,/deriveAgentMemory/);
|
|
85
|
+
assert.match(source,/effectIdentity/);
|
|
86
|
+
assert.match(source,/loopGuardDisposition/);
|
|
87
|
+
assert.match(source,/createWorkerScheduler/);
|
|
88
|
+
assert.match(source,/issueObservationLease/);
|
|
89
|
+
assert.match(source,/project_memory:memory/);
|
|
90
|
+
assert.match(source,/effect_guard_blocked/);
|
|
91
|
+
assert.match(source,/worker_lease_acquired/);
|
|
92
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { classifyAutonomousTimeoutRecovery } from '../src/autonomous-timeout-recovery.mjs';
|
|
4
|
+
|
|
5
|
+
const before='a'.repeat(64);
|
|
6
|
+
const after='b'.repeat(64);
|
|
7
|
+
const base={status:'ok',state:'running',reason:null,journal_head_sha256:after,pending_request:null,pending_decision:null,run_busy:false,run_owner_pid:null,run_lock_created_at_utc:null};
|
|
8
|
+
|
|
9
|
+
test('R37 timeout recovery accepts signed durable terminal success',()=>{
|
|
10
|
+
assert.deepEqual(classifyAutonomousTimeoutRecovery({...base,state:'succeeded'},{beforeJournalHead:before}),
|
|
11
|
+
{kind:'completed',state:'succeeded',journal_head_sha256:after});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('R37 timeout recovery surfaces paused or cancelled as terminal failure',()=>{
|
|
15
|
+
assert.equal(classifyAutonomousTimeoutRecovery({...base,state:'paused',reason:'benchmark_regression'},{beforeJournalHead:before}).kind,'terminal_failure');
|
|
16
|
+
assert.equal(classifyAutonomousTimeoutRecovery({...base,state:'cancelled'},{beforeJournalHead:before}).kind,'terminal_failure');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('R37 timeout recovery waits while planner or decision is still in flight',()=>{
|
|
20
|
+
assert.equal(classifyAutonomousTimeoutRecovery({...base,pending_request:{request_id:'x'}},{beforeJournalHead:before}).kind,'wait');
|
|
21
|
+
assert.equal(classifyAutonomousTimeoutRecovery({...base,pending_decision:{event_sha256:'c'.repeat(64)}},{beforeJournalHead:before}).kind,'wait');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('R37 timeout recovery resumes only after durable head advance and cleared pending state',()=>{
|
|
25
|
+
assert.deepEqual(classifyAutonomousTimeoutRecovery(base,{beforeJournalHead:before}),{
|
|
26
|
+
kind:'resumable',state:'running',reason:'durable_cycle_completed',journal_head_sha256:after
|
|
27
|
+
});
|
|
28
|
+
assert.equal(classifyAutonomousTimeoutRecovery({...base,journal_head_sha256:before},{beforeJournalHead:before}).kind,'wait');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
test('R37 public autonomous smoke binds timeout recovery to pre-run journal head and resumes yielded work',async()=>{
|
|
33
|
+
const {readFile}=await import('node:fs/promises');
|
|
34
|
+
const src=await readFile(new URL('../src/remote-mcp-autonomous-smoke-client.mjs',import.meta.url),'utf8');
|
|
35
|
+
assert.match(src,/classifyAutonomousTimeoutRecovery/);
|
|
36
|
+
assert.match(src,/const beforeRun=structured\(await client\.callTool/);
|
|
37
|
+
assert.match(src,/recoverTimedOutGoal\(client, goalId, beforeRun\.journal_head_sha256\)/);
|
|
38
|
+
assert.match(src,/recovered\.kind === 'completed'/);
|
|
39
|
+
assert.match(src,/status:'yielded'/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
test('R37 timeout recovery waits until server-side run lock is released even after durable progress',()=>{
|
|
44
|
+
const busy={...base,run_busy:true,run_owner_pid:2904,run_lock_created_at_utc:'2026-09-18T14:22:02.000Z'};
|
|
45
|
+
const result=classifyAutonomousTimeoutRecovery(busy,{beforeJournalHead:before});
|
|
46
|
+
assert.equal(result.kind,'wait');
|
|
47
|
+
assert.equal(result.reason,'server_run_in_flight');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('R37 autonomous goal status exposes run-lock state for timeout-safe clients',async()=>{
|
|
51
|
+
const {readFile}=await import('node:fs/promises');
|
|
52
|
+
const [runtime,contracts]=await Promise.all([
|
|
53
|
+
readFile(new URL('../src/autonomous-runtime.mjs',import.meta.url),'utf8'),
|
|
54
|
+
readFile(new URL('../src/autonomous-output-contracts.mjs',import.meta.url),'utf8')
|
|
55
|
+
]);
|
|
56
|
+
assert.match(runtime,/runLockStatus/);
|
|
57
|
+
assert.match(runtime,/run_busy/);
|
|
58
|
+
assert.match(contracts,/run_busy:z\.boolean/);
|
|
59
|
+
assert.match(contracts,/run_owner_pid:z\.number\(\)\.int\(\)\.min\(1\)\.nullable/);
|
|
60
|
+
});
|