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.
Files changed (70) hide show
  1. package/MANIFEST.SHA256 +69 -55
  2. package/README.txt +13 -14
  3. package/bin/WORKER-REGISTRY.txt +3 -3
  4. package/bin/appcontainer-stage.exe +0 -0
  5. package/bin/appcontainer-stage.obj +0 -0
  6. package/bin/bootstrap-advapi32.exe +0 -0
  7. package/bin/bootstrap-advapi32.obj +0 -0
  8. package/bin/bootstrap-exitcode.exe +0 -0
  9. package/bin/bootstrap-exitcode.obj +0 -0
  10. package/bin/bootstrap-kernel32.exe +0 -0
  11. package/bin/bootstrap-kernel32.obj +0 -0
  12. package/bin/child-control-probe.exe +0 -0
  13. package/bin/child-control-probe.obj +0 -0
  14. package/bin/child-control-stage.exe +0 -0
  15. package/bin/child-control-stage.obj +0 -0
  16. package/bin/contained-reverse-worker.exe +0 -0
  17. package/bin/contained-reverse-worker.obj +0 -0
  18. package/bin/contained-transform-worker.exe +0 -0
  19. package/bin/contained-transform-worker.obj +0 -0
  20. package/bin/containment-probe.exe +0 -0
  21. package/bin/containment-probe.obj +0 -0
  22. package/bin/deadbyte-contain.exe +0 -0
  23. package/bin/deadbyte-contain.obj +0 -0
  24. package/bin/deadbyte-exec.exe +0 -0
  25. package/bin/deadbyte-exec.obj +0 -0
  26. package/bin/deadbyte-process-host.exe +0 -0
  27. package/bin/deadbyte-process-host.obj +0 -0
  28. package/bin/deadbyte-tunnel-host.exe +0 -0
  29. package/bin/deadbyte-tunnel-host.obj +0 -0
  30. package/controller/README.TXT +3 -3
  31. package/controller/deadbyte-controller.ps1 +1 -1
  32. package/controller/deadbyte-process-policy.json +1 -1
  33. package/docs/ARCHITECTURE.md +1 -1
  34. package/package.json +1 -1
  35. package/scripts/deferred-slot-operation.mjs +1 -1
  36. package/scripts/final-closure-r36.mjs +113 -0
  37. package/scripts/final-closure-verify-r36.mjs +208 -0
  38. package/scripts/final-closure-verify.mjs +48 -48
  39. package/scripts/final-closure.mjs +13 -13
  40. package/scripts/gate-windows-r36.ps1 +42 -0
  41. package/scripts/gate-windows.ps1 +3 -3
  42. package/scripts/release-parity-r36.mjs +200 -0
  43. package/scripts/release-parity-tests.ps1 +2 -2
  44. package/scripts/release-parity.mjs +37 -37
  45. package/scripts/windows-gate-evidence-r36.mjs +52 -0
  46. package/scripts/windows-gate-evidence.mjs +5 -5
  47. package/src/agent-memory-plane.mjs +98 -0
  48. package/src/autonomous-output-contracts.mjs +5 -2
  49. package/src/autonomous-runtime.mjs +155 -31
  50. package/src/autonomous-timeout-recovery.mjs +27 -0
  51. package/src/deadbyte-cli.mjs +37 -76
  52. package/src/effect-ledger.mjs +47 -0
  53. package/src/observation-lease.mjs +32 -0
  54. package/src/remote-live-log.mjs +59 -0
  55. package/src/remote-mcp-autonomous-smoke-client.mjs +15 -9
  56. package/src/version.mjs +1 -1
  57. package/src/worker-scheduler.mjs +53 -0
  58. package/test/autonomous-output-contract.test.mjs +1 -1
  59. package/test/core.test.mjs +1 -1
  60. package/test/production-docs.test.mjs +5 -5
  61. package/test/r33-closeout-regression.test.mjs +4 -4
  62. package/test/r33-finalization.test.mjs +6 -6
  63. package/test/r36-finalization.test.mjs +5 -5
  64. package/test/r36-release-identity.test.mjs +12 -19
  65. package/test/r37-agent-fabric.test.mjs +92 -0
  66. package/test/r37-autonomous-timeout-recovery.test.mjs +60 -0
  67. package/test/r37-finalization.test.mjs +74 -0
  68. package/test/release-version.test.mjs +42 -36
  69. package/test/remote-live-log-r37.test.mjs +47 -0
  70. 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
+ }
@@ -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
- if (status.state === 'succeeded') return status;
36
- if (status.state === 'cancelled' || status.state === 'paused') {
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 (status.state !== 'pending' && status.state !== 'running') {
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 state after request timeout');
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 = { status:'completed', state:recovered.state, goal_id:goalId, recovered_from_timeout:true };
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
@@ -1,4 +1,4 @@
1
- export const DEADBYTE_VERSION = '0.10.0';
1
+ export const DEADBYTE_VERSION = '0.11.0';
2
2
  export const MCP_PROTOCOL_VERSION = '2026-07-28';
3
3
  export const RECEIPT_SCHEMA_V1 = 'deadbyte.receipt.v1';
4
4
  export const RECEIPT_SCHEMA_V2 = 'deadbyte.receipt.v2';
@@ -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
- ...baseDerived(),loop:publicLoop
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,{
@@ -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.10.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\.10\.0 mcp/);
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\.10\.0"[\s\S]*?"mcp"/);
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\.10\.0",\s*"mcp"\]/);
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.match(text,/remote --verbose/);
33
- assert.match(text,/compact/i);
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 R36 gate emits manifest-bound machine-readable evidence outside the candidate tree', async()=>{
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.r36-windows-gate.v1');
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.10.0');
105
+ assert.equal(evidence.version,'0.11.0');
106
106
  assert.equal(evidence.manifest_sha256,manifestSha);
107
- assert.equal(evidence.marker,'DEADBYTE V0.10.0 / R36 WINDOWS BATCH GATE: PASS');
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('R36 final closure verifier is implementation-independent from production helpers', async () => {
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-R36-FINAL-CLOSURE-V1/);
36
- assert.match(writer, /pointer\.version==='0\.10\.0'/);
37
- assert.match(verifier, /envelope\.body\.version==='0\.10\.0'/);
38
- assert.match(verifier, /release_label==='R36 FINAL MACHINE BASELINE'/);
39
- assert.match(verifier, /b95e1ec1d9a7e26a96ef492306c1be4df18c883ff03ab3b6015f677d7429ec8d/);
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 current release authority is consistently 0.10.0 / R36 and preserves exact R35 predecessor',async()=>{
11
- const pkg=JSON.parse(await text('package.json'));
12
- const [version,controller,gate,readme,architecture,controllerReadme,deferred,parity,closure,closureVerify,windowsGate]=await Promise.all([
13
- text('src/version.mjs'),text('controller/deadbyte-controller.ps1'),text('scripts/gate-windows.ps1'),
14
- text('README.txt'),text('docs/ARCHITECTURE.md'),text('controller/README.TXT'),
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(closureVerify,/version==='0\.10\.0'/);
34
- assert.match(windowsGate,/schema:'deadbyte\.r36-windows-gate\.v1'/);
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('R36 controller binds production remote to compact profile and restores parent environment',async()=>{
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 parity source defines exact full 76-tool authority plus compact gateway closure',async()=>{
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
+ });