deadbyte-mcp 0.10.0 → 0.11.1

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 (82) hide show
  1. package/MANIFEST.SHA256 +81 -59
  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/build-containment.ps1 +15 -15
  36. package/scripts/deferred-slot-operation-r37-0110.mjs +124 -0
  37. package/scripts/deferred-slot-operation.mjs +1 -1
  38. package/scripts/final-closure-r36.mjs +113 -0
  39. package/scripts/final-closure-r37-0110.mjs +113 -0
  40. package/scripts/final-closure-verify-r36.mjs +208 -0
  41. package/scripts/final-closure-verify-r37-0110.mjs +208 -0
  42. package/scripts/final-closure-verify.mjs +48 -48
  43. package/scripts/final-closure.mjs +13 -13
  44. package/scripts/gate-windows-r36.ps1 +42 -0
  45. package/scripts/gate-windows-r37-0110.ps1 +42 -0
  46. package/scripts/gate-windows.ps1 +3 -3
  47. package/scripts/release-parity-r36.mjs +200 -0
  48. package/scripts/release-parity-r37-0110.mjs +200 -0
  49. package/scripts/release-parity-tests.ps1 +2 -2
  50. package/scripts/release-parity.mjs +37 -37
  51. package/scripts/windows-gate-evidence-r36.mjs +52 -0
  52. package/scripts/windows-gate-evidence-r37-0110.mjs +52 -0
  53. package/scripts/windows-gate-evidence.mjs +5 -5
  54. package/src/agent-memory-plane.mjs +98 -0
  55. package/src/autonomous-output-contracts.mjs +5 -2
  56. package/src/autonomous-runtime.mjs +155 -31
  57. package/src/autonomous-timeout-recovery.mjs +27 -0
  58. package/src/deadbyte-cli.mjs +90 -94
  59. package/src/effect-ledger.mjs +47 -0
  60. package/src/observation-lease.mjs +32 -0
  61. package/src/remote-live-log.mjs +81 -0
  62. package/src/remote-mcp-autonomous-smoke-client.mjs +15 -9
  63. package/src/remote-result-journal.mjs +23 -4
  64. package/src/version.mjs +1 -1
  65. package/src/worker-scheduler.mjs +53 -0
  66. package/test/autonomous-output-contract.test.mjs +1 -1
  67. package/test/cli-entrypoint.test.mjs +9 -0
  68. package/test/containment.test.mjs +18 -0
  69. package/test/core.test.mjs +1 -1
  70. package/test/production-docs.test.mjs +5 -5
  71. package/test/r33-closeout-regression.test.mjs +4 -4
  72. package/test/r33-finalization.test.mjs +6 -6
  73. package/test/r36-finalization.test.mjs +5 -5
  74. package/test/r36-release-identity.test.mjs +12 -19
  75. package/test/r37-0110-history.test.mjs +20 -0
  76. package/test/r37-agent-fabric.test.mjs +92 -0
  77. package/test/r37-autonomous-timeout-recovery.test.mjs +60 -0
  78. package/test/r37-finalization.test.mjs +74 -0
  79. package/test/r37-remote-tool-call-timeline.test.mjs +94 -0
  80. package/test/release-version.test.mjs +35 -36
  81. package/test/remote-live-log-r37.test.mjs +50 -0
  82. package/test/remote-session-cli.test.mjs +86 -68
@@ -9,14 +9,17 @@ import { fileURLToPath } from 'node:url';
9
9
  import { defaultPublicKeyPath } from './trust.mjs';
10
10
  import { verifyAuditChain } from './audit-verify.mjs';
11
11
  import { openRemoteResultJournal, parseRemoteResultCursor } from './remote-result-journal.mjs';
12
- import { createLiveActivityTracker, formatCompactAuditEvent, formatRemoteLifecycle } from './remote-live-log.mjs';
12
+ import { DEADBYTE_VERSION } from './version.mjs';
13
+ import { createLiveTimelineTracker, formatCompactAuditEvent, formatRemoteLifecycle, formatRemoteToolCall, formatRuntimeDiagnosticLine } from './remote-live-log.mjs';
13
14
 
14
15
  const REMOTE_COMMANDS=new Set([
15
16
  'menu','start','resume','park','stop','restart','status','url','test','fulltest','toggle','clean','machinetest',
16
17
  'armprocess','disarmprocess','processstatus','armhost','disarmhost','hoststatus','hosttest',
17
18
  'armcoding','disarmcoding','codingstatus','codingtest','armautonomous','disarmautonomous','autonomousstatus','autonomoustest'
18
19
  ]);
19
- const SESSION_START=['start','armprocess','armcoding','armautonomous'];
20
+ const SESSION_ARM=['armprocess','armcoding','armautonomous'];
21
+ const REMOTE_STATUS_TIMEOUT_MS=30000;
22
+ const REMOTE_START_TIMEOUT_MS=150000;
20
23
  const SESSION_STATUS=[
21
24
  ['processstatus',/^Process plane\s*:\s*ARMED\b/m],
22
25
  ['codingstatus',/^Coding plane\s*:\s*ARMED\b/m],
@@ -34,9 +37,8 @@ function printHelp(){
34
37
  process.stdout.write([
35
38
  'DEADBYTE MCP CLI','',
36
39
  ' npx deadbyte-mcp@latest mcp # stdio MCP server for local clients',
37
- ' npx deadbyte-mcp@latest remote # compact foreground device session',
38
- ' npx deadbyte-mcp@latest remote --verbose # detailed audit + bridge logs',
39
- ' npx deadbyte-mcp@latest remote --resume <cursor> # replay missed compact results, then reconnect',
40
+ ' npx deadbyte-mcp@latest remote # unified foreground device session + live timeline',
41
+ ' npx deadbyte-mcp@latest remote --resume <cursor> # replay missed results, then reconnect',
40
42
  ' npx deadbyte-mcp@latest remote status',
41
43
  ' npx deadbyte-mcp@latest remote restart',
42
44
  ' npx deadbyte-mcp@latest supervisor status',
@@ -53,11 +55,16 @@ async function remoteContext(){
53
55
  const powershell=await requireRegular(path.join(systemRoot,'System32','WindowsPowerShell','v1.0','powershell.exe'),'Windows PowerShell');
54
56
  return {root,launcher,powershell};
55
57
  }
56
- function invokeController(ctx,command,{capture=false,allowFailure=false,echoCaptured=true}={}){
57
- const result=spawnSync(ctx.powershell,['-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',ctx.launcher,command],{
58
+ function invokeController(ctx,command,{capture=false,allowFailure=false,echoCaptured=true,timeoutMs=null}={}){
59
+ const options={
58
60
  stdio:capture?['ignore','pipe','pipe']:'inherit',encoding:capture?'utf8':undefined,windowsHide:false,shell:false
59
- });
60
- if(result.error) throw result.error;
61
+ };
62
+ if(Number.isInteger(timeoutMs)&&timeoutMs>0) options.timeout=timeoutMs;
63
+ const result=spawnSync(ctx.powershell,['-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',ctx.launcher,command],options);
64
+ if(result.error){
65
+ if(result.error?.code==='ETIMEDOUT') throw new Error(`remote controller '${command}' timed out after ${timeoutMs}ms`);
66
+ throw result.error;
67
+ }
61
68
  const status=Number.isInteger(result.status)?result.status:1;
62
69
  const stdout=capture?String(result.stdout||''):'';
63
70
  const stderr=capture?String(result.stderr||''):'';
@@ -65,6 +72,14 @@ function invokeController(ctx,command,{capture=false,allowFailure=false,echoCapt
65
72
  if(status!==0&&!allowFailure) throw new Error(`remote controller '${command}' failed exit=${status}`);
66
73
  return {status,stdout,stderr};
67
74
  }
75
+ function controllerRuntimeReady(text){
76
+ const value=String(text||'');
77
+ return /^DEADBYTE MCP\s*=\s*ONLINE\s*$/m.test(value)&&
78
+ /^Bridge PID\s*:\s*\d+\s+\[verified\]\s*$/m.test(value)&&
79
+ /^Cloud PID\s*:\s*\d+\s+\[verified\]\s*$/m.test(value)&&
80
+ /^Local ready\s*:\s*True\s*$/m.test(value)&&
81
+ /^Loop supervisor:\s*RUNNING\s+\[verified\]\s*$/m.test(value);
82
+ }
68
83
  async function listLogFiles(root){
69
84
  const files=[];
70
85
  const audit=path.join(root,'audit');
@@ -78,46 +93,30 @@ async function listLogFiles(root){
78
93
  }
79
94
  return files;
80
95
  }
81
- function formatLogLine(file,line,{verbose=false,activityTracker=null}={}){
96
+ function formatLogLine(file,line,{activityTracker=null}={}){
82
97
  if(!line) return null;
83
98
  if(/^segment-\d{8}\.jsonl$/.test(path.basename(file))){
84
99
  try{
85
100
  const event=JSON.parse(line);
86
- if(!verbose) return activityTracker?activityTracker.observe(event):formatCompactAuditEvent(event);
87
- const seq=Number.isInteger(event.seq)?`#${event.seq}`:'#?';
88
- const request=typeof event.request_id==='string'?` ${event.request_id.slice(0,8)}`:'';
89
- const tool=typeof event.tool==='string'?` ${event.tool}`:'';
90
- let marker='';
91
- if(event.event_type==='tool.request') marker='📥 ';
92
- else if(event.event_type==='tool.result'){
93
- if(event.outcome?.is_error===true) marker='📤❌ ';
94
- else if(event.outcome?.is_error===false) marker='📤✅ ';
95
- else marker='📤 ';
96
- }
97
- return `${marker}[AUDIT ${seq}] ${event.event_type||'event'}${tool}${request}`;
98
- }catch{return verbose?`[AUDIT] ${line}`:null;}
99
- }
100
- if(!verbose) return null;
101
- const label=path.basename(file).replace(/\.log$/i,'').toUpperCase();
102
- let marker='';
103
- if(/\[TOOL IN \]/.test(line)) marker='📥 ';
104
- else if(/\[TOOL OUT\]/.test(line)){
105
- if(/"is_error"\s*:\s*true/.test(line)) marker='📤❌ ';
106
- else if(/"is_error"\s*:\s*false/.test(line)) marker='📤✅ ';
107
- else marker='📤 ';
101
+ return activityTracker?activityTracker.observe(event):formatCompactAuditEvent(event);
102
+ }catch{return null;}
108
103
  }
109
- return `${marker}[${label}] ${line}`;
104
+ return formatRuntimeDiagnosticLine(file,line);
110
105
  }
111
106
  function remoteResultJournalPath(root,sessionId){return path.join(root,'runtime','remote-results',`${sessionId}.json`);}
112
- function auditTerminalResult(file,line){
107
+ function auditTimelineRecord(file,line){
113
108
  if(!/^segment-\d{8}\.jsonl$/.test(path.basename(file))||!line) return null;
114
109
  let event;try{event=JSON.parse(line);}catch{return null;}
115
- if(event?.event_type!=='tool.result'||typeof event.request_id!=='string'||!event.request_id||typeof event.tool!=='string'||!event.tool) return null;
110
+ if(typeof event?.request_id!=='string'||!event.request_id||typeof event.tool!=='string'||!event.tool) return null;
116
111
  const observed=Number.isFinite(Date.parse(event.created_at_utc))?new Date(Date.parse(event.created_at_utc)).toISOString():new Date().toISOString();
117
- return {request_id:event.request_id,tool:event.tool,status:event.outcome?.is_error===true?'error':'ok',observed_at_utc:observed,
118
- payload_sha256:createHash('sha256').update(Buffer.from(line,'utf8')).digest('hex')};
112
+ const payload_sha256=createHash('sha256').update(Buffer.from(line,'utf8')).digest('hex');
113
+ if(event.event_type==='tool.request') return {event_type:'tool.call',request_id:event.request_id,tool:event.tool,
114
+ observed_at_utc:observed,arguments:event.args??{},payload_sha256};
115
+ if(event.event_type==='tool.result') return {event_type:'tool.result',request_id:event.request_id,tool:event.tool,
116
+ status:event.outcome?.is_error===true?'error':'ok',observed_at_utc:observed,payload_sha256};
117
+ return null;
119
118
  }
120
- async function replayRemoteResults(root,cursor,{verbose=false}={}){
119
+ async function replayRemoteResults(root,cursor){
121
120
  const parsed=parseRemoteResultCursor(cursor);
122
121
  const file=remoteResultJournalPath(root,parsed.session_id);
123
122
  await requireRegular(file,'remote result journal');
@@ -126,8 +125,14 @@ async function replayRemoteResults(root,cursor,{verbose=false}={}){
126
125
  for(;;){
127
126
  const page=await journal.replayAfter(next,{limit:200});
128
127
  for(const event of page.events){
128
+ if(event.event_type==='tool.call'){
129
+ const line=formatRemoteToolCall({event_type:'tool.request',tool:event.tool,request_id:event.request_id,
130
+ observed_at_utc:event.observed_at_utc,args:event.arguments});
131
+ process.stdout.write(`${line} | REPLAY cursor r1:${event.session_id}:${event.seq}\n`);
132
+ continue;
133
+ }
129
134
  const marker=event.status==='error'?'[ERR]':'[OK]';
130
- process.stdout.write(`${verbose?'[REPLAY] ': '[REPLAY] '}${marker} ${event.tool} [cursor r1:${event.session_id}:${event.seq}]\n`);
135
+ process.stdout.write(`[REPLAY] ${marker} ${event.tool} [cursor r1:${event.session_id}:${event.seq}]\n`);
131
136
  }
132
137
  next=page.next_cursor;
133
138
  if(!page.has_more) return next;
@@ -151,10 +156,8 @@ async function snapshotRemoteLogOffsets(root){
151
156
  }
152
157
  return offsets;
153
158
  }
154
- async function tailRemoteLogs(root,signal,{verbose=false,journal=null,offsets=null,activityTracker=null}={}){
155
- process.stdout.write(verbose
156
- ?'📜 [LOG] Live logs active. Press Ctrl+C to disconnect and revoke authority.\n'
157
- :'[LOG] Live activity. Ctrl+C disconnects and revokes authority.\n');
159
+ async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTracker=null}={}){
160
+ process.stdout.write('[LOG] Unified live timeline. Ctrl+C disconnects and revokes authority.\n');
158
161
  const activeOffsets=offsets??await snapshotRemoteLogOffsets(root);
159
162
  while(!signal.aborted){
160
163
  for(const file of await listLogFiles(root)){
@@ -162,25 +165,25 @@ async function tailRemoteLogs(root,signal,{verbose=false,journal=null,offsets=nu
162
165
  const next=await readAppend(file,prior);
163
166
  activeOffsets.set(file,next.offset);
164
167
  for(const line of next.text.split(/\r?\n/)){
165
- const terminal=auditTerminalResult(file,line);
168
+ const timeline=auditTimelineRecord(file,line);
166
169
  let recorded=null;
167
- if(terminal&&journal) recorded=await journal.recordTerminal(terminal);
168
- if(recorded?.duplicate&&!verbose) continue;
169
- const formatted=formatLogLine(file,line,{verbose,activityTracker});
170
+ if(timeline&&journal) recorded=timeline.event_type==='tool.call'
171
+ ? await journal.recordCall(timeline)
172
+ : await journal.recordTerminal(timeline);
173
+ if(recorded?.duplicate) continue;
174
+ const formatted=formatLogLine(file,line,{activityTracker});
170
175
  if(!formatted) continue;
171
- if(!verbose&&recorded) process.stdout.write(`${formatted} [cursor ${recorded.cursor}]\n`);
176
+ if(recorded) process.stdout.write(`${formatted} [cursor ${recorded.cursor}]\n`);
172
177
  else process.stdout.write(`${formatted}\n`);
173
178
  }
174
179
  }
175
180
  try{await sleep(250,null,{signal});}catch(error){if(error?.name!=='AbortError') throw error;}
176
181
  }
177
182
  }
178
- async function cleanupRemoteSession(ctx,{verbose=false}={}){
183
+ async function cleanupRemoteSession(ctx){
179
184
  for(const command of SESSION_CLEANUP){
180
185
  try{
181
- const result=invokeController(ctx,command,verbose
182
- ?{allowFailure:true}
183
- :{allowFailure:true,capture:true,echoCaptured:false});
186
+ const result=invokeController(ctx,command,{allowFailure:true,capture:true,echoCaptured:false});
184
187
  if(result.status!==0) process.stderr.write(`⚠️ [WARN] cleanup ${command} exit=${result.status}\n`);
185
188
  }
186
189
  catch(error){process.stderr.write(`⚠️ [WARN] cleanup ${command}: ${String(error?.message||error)}\n`);}
@@ -259,15 +262,15 @@ async function releaseRemoteSession(ctx,marker){
259
262
  const same=current?.schema==='deadbyte.remote-session.v2'&&current.owner_pid===marker.owner_pid&&current.session_nonce===marker.session_nonce;
260
263
  if(same) await rm(remoteSessionMarker(ctx.root),{force:true});
261
264
  }
262
- async function runRemoteSession({verbose=false,resumeCursor=null}={}){
265
+ async function runRemoteSession({resumeCursor=null}={}){
263
266
  const ctx=await remoteContext();
264
- if(resumeCursor!==null) await replayRemoteResults(ctx.root,resumeCursor,{verbose});
267
+ if(resumeCursor!==null) await replayRemoteResults(ctx.root,resumeCursor);
265
268
  await recoverRemoteSession(ctx);
266
269
  const marker=await claimRemoteSession(ctx);
267
270
  const resultJournal=await openRemoteResultJournal({
268
271
  filePath:remoteResultJournalPath(ctx.root,marker.result_session_id),sessionId:marker.result_session_id,maxEntries:2048
269
272
  });
270
- const activityTracker=createLiveActivityTracker();
273
+ const activityTracker=createLiveTimelineTracker();
271
274
  const aborter=new AbortController();
272
275
  const abort=()=>aborter.abort();
273
276
  const watchStdin=!process.stdin.isTTY;
@@ -276,67 +279,56 @@ async function runRemoteSession({verbose=false,resumeCursor=null}={}){
276
279
  let ready=false;
277
280
  let logTask=null;
278
281
  try{
279
- if(verbose) process.stdout.write('DEADBYTE MCP DEVICE SESSION\n');
280
- else process.stdout.write('DEADBYTE MCP\n');
282
+ process.stdout.write('DEADBYTE MCP\n');
281
283
  process.stdout.write(`${formatRemoteLifecycle('device_start')}\n`);
282
284
  process.stdout.write(`${formatRemoteLifecycle(resumeCursor!==null?'session_restored':'session_new')}\n`);
283
285
  const logOffsets=await snapshotRemoteLogOffsets(ctx.root);
284
- logTask=tailRemoteLogs(ctx.root,aborter.signal,{verbose,journal:resultJournal,offsets:logOffsets,activityTracker});
285
- if(verbose) process.stdout.write('[INFO] Starting local + remote runtime...\n');
286
- else process.stdout.write('[BOOT] Starting local + remote runtime...\n');
287
- const started=[];
288
- for(const command of SESSION_START){
289
- if(!verbose){
290
- if(command==='start') process.stdout.write('[BOOT] Starting bridge/runtime...\n');
291
- else process.stdout.write(`🔐 [AUTH] Arming ${command.slice(3)}...\n`);
292
- }
293
- const controllerResult=invokeController(ctx,command,verbose?{}:{capture:true,echoCaptured:false});
294
- started.push(controllerResult);
295
- if(command==='start'){
296
- process.stdout.write(`${formatRemoteLifecycle('bridge_connected')}\n`);
297
- process.stdout.write(`${formatRemoteLifecycle('cloud_connected')}\n`);
298
- }
286
+ logTask=tailRemoteLogs(ctx.root,aborter.signal,{journal:resultJournal,offsets:logOffsets,activityTracker});
287
+ process.stdout.write('[BOOT] Starting local + remote runtime...\n');
288
+ const preflight=invokeController(ctx,'status',{capture:true,allowFailure:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
289
+ let startText=preflight.stdout||'';
290
+ if(preflight.status===0&&controllerRuntimeReady(preflight.stdout)){
291
+ process.stdout.write('[BOOT] Runtime already ONLINE; reusing verified bridge/cloud/supervisor.\n');
292
+ }else{
293
+ process.stdout.write('[BOOT] Starting bridge/runtime...\n');
294
+ const started=invokeController(ctx,'start',{capture:true,echoCaptured:false,timeoutMs:REMOTE_START_TIMEOUT_MS});
295
+ startText=started.stdout||'';
296
+ }
297
+ process.stdout.write(`${formatRemoteLifecycle('bridge_connected')}\n`);
298
+ process.stdout.write(`${formatRemoteLifecycle('cloud_connected')}\n`);
299
+ for(const command of SESSION_ARM){
300
+ process.stdout.write(`🔐 [AUTH] Arming ${command.slice(3)}...\n`);
301
+ invokeController(ctx,command,{capture:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
299
302
  }
300
- if(!verbose){
301
- const startText=started[0]?.stdout||'';
302
- const url=startText.match(/^URL\s*:\s*(\S+)/m)?.[1]||'';
303
- process.stdout.write('[OK] ONLINE\n');
304
- process.stdout.write(`${formatRemoteLifecycle('ready')}\n`);
305
- if(url) process.stdout.write(`URL: ${url}\n`);
306
- process.stdout.write('🔐 [AUTH] Verifying process, coding, autonomous; host must stay DISARMED...\n');
307
- }else process.stdout.write('🛡️ [AUTHORITY] Verifying runtime planes; host must stay DISARMED...\n');
303
+ const url=startText.match(/^URL\s*:\s*(\S+)/m)?.[1]||'';
304
+ process.stdout.write('[OK] ONLINE\n');
305
+ process.stdout.write(`${formatRemoteLifecycle('ready')}\n`);
306
+ if(url) process.stdout.write(`URL: ${url}\n`);
307
+ process.stdout.write('🔐 [AUTH] Verifying process, coding, autonomous; host must stay DISARMED...\n');
308
308
  for(const [command,pattern] of SESSION_STATUS){
309
- const result=invokeController(ctx,command,{capture:true,echoCaptured:verbose});
309
+ const result=invokeController(ctx,command,{capture:true,echoCaptured:false});
310
310
  if(!pattern.test(result.stdout)) throw new Error(`remote session authority verification failed: ${command} state mismatch`);
311
311
  }
312
312
  ready=true;
313
- if(verbose) process.stdout.write('[DEVICE] DEADBYTE MCP online; process/coding/autonomous ARMED; host DISARMED.\n');
314
- else process.stdout.write('🔐 [AUTH] ARMED: process, coding, autonomous | host DISARMED\n');
313
+ process.stdout.write('🔐 [AUTH] ARMED: process, coding, autonomous | host DISARMED\n');
315
314
  await logTask;
316
315
  }finally{
317
316
  const wasAborted=aborter.signal.aborted;
318
317
  aborter.abort();
319
318
  if(logTask) await logTask.catch(()=>{});
320
- if(!verbose){for(const line of activityTracker.reconcileInterrupted('session_end')) process.stdout.write(`${line}\n`);}
319
+ for(const line of activityTracker.reconcileInterrupted('session_end')) process.stdout.write(`${line}\n`);
321
320
  process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);
322
321
  if(watchStdin){process.stdin.removeListener('end',abort);process.stdin.removeListener('close',abort);process.stdin.pause();}
323
322
  process.stdout.write(`${formatRemoteLifecycle('cleanup')}\n`);
324
- if(ready||!wasAborted) process.stdout.write(verbose?'[INFO] Closing remote device session and revoking authority...\n':'[INFO] Disconnecting; revoking authority...\n');
325
- await cleanupRemoteSession(ctx,{verbose});
323
+ if(ready||!wasAborted) process.stdout.write('[INFO] Disconnecting; revoking authority...\n');
324
+ await cleanupRemoteSession(ctx);
326
325
  await releaseRemoteSession(ctx,marker);
327
326
  }
328
327
  }
329
328
  async function runRemote(args){
330
329
  if(args.length===0){await runRemoteSession();return;}
331
- if(args.length===1&&args[0]==='--verbose'){await runRemoteSession({verbose:true});return;}
332
330
  if(args.length===2&&args[0]==='--resume'){await runRemoteSession({resumeCursor:args[1]});return;}
333
- if(args.length===3&&args[0]==='--verbose'&&args[1]==='--resume'){
334
- await runRemoteSession({verbose:true,resumeCursor:args[2]});return;
335
- }
336
- if(args.length===3&&args[0]==='--resume'&&args[2]==='--verbose'){
337
- await runRemoteSession({verbose:true,resumeCursor:args[1]});return;
338
- }
339
- if(args.length>1) throw new Error('remote accepts one controller command, --verbose, or --resume <cursor>');
331
+ if(args.length>1) throw new Error('remote accepts one controller command or --resume <cursor>');
340
332
  const command=args[0];if(!REMOTE_COMMANDS.has(command)) throw new Error(`unsupported remote command '${command}'`);
341
333
  const ctx=await remoteContext();
342
334
  const result=invokeController(ctx,command,{allowFailure:true});
@@ -359,6 +351,10 @@ async function runAudit(args){
359
351
  }
360
352
  async function main(argv){
361
353
  const [command,...rest]=argv;
354
+ if(command==='--version'||command==='-v'||command==='version'){
355
+ if(rest.length) throw new Error('version accepts no arguments');
356
+ process.stdout.write(`${DEADBYTE_VERSION}\n`);return;
357
+ }
362
358
  if(!command||command==='help'||command==='--help'||command==='-h'){printHelp();return;}
363
359
  if(command==='mcp'){
364
360
  if(rest.length) throw new Error('mcp accepts no arguments');
@@ -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
+ }
@@ -1,3 +1,6 @@
1
+ import { canonicalJson } from './canonical-json.mjs';
2
+ import { sanitizeAuditArgs } from './tool-audit.mjs';
3
+
1
4
  const SAFE_ARG_KEYS=Object.freeze(['root_id','path','pattern','command_id','profile_id','session_id','goal_id','task_id','step_id','mode']);
2
5
  const LIFECYCLE=Object.freeze({
3
6
  device_start:'🚀 [DEVICE] Starting DEADBYTE MCP...',
@@ -30,6 +33,25 @@ function summarizeArgs(args){
30
33
  return parts.length?` ${parts.join(' ')}`:'';
31
34
  }
32
35
 
36
+ const TOOL_COLUMN_WIDTH=32;
37
+
38
+ function eventTimestamp(event,clock){
39
+ for(const value of [event?.created_at_utc,event?.observed_at_utc]){
40
+ const parsed=Date.parse(value);
41
+ if(typeof value==='string'&&Number.isFinite(parsed)) return new Date(parsed).toISOString();
42
+ }
43
+ const value=Number(clock());
44
+ return new Date(Number.isFinite(value)?value:Date.now()).toISOString();
45
+ }
46
+
47
+ export function formatRemoteToolCall(event,{clock=()=>Date.now()}={}){
48
+ if(!event||typeof event!=='object'||Array.isArray(event)||event.event_type!=='tool.request'||
49
+ typeof event.tool!=='string'||!event.tool) return null;
50
+ if(typeof clock!=='function') throw new Error('remote tool-call clock must be a function');
51
+ const args=sanitizeAuditArgs(event.args??{});
52
+ return `${eventTimestamp(event,clock)} | ${event.tool.padEnd(TOOL_COLUMN_WIDTH)} | Arguments: ${canonicalJson(args)}`;
53
+ }
54
+
33
55
  function formatDuration(ms){
34
56
  const value=Number(ms);
35
57
  if(!Number.isFinite(value)||value<0) return '';
@@ -74,3 +96,62 @@ export function createLiveActivityTracker(){
74
96
  }
75
97
  });
76
98
  }
99
+
100
+
101
+ function shortSpanId(requestId){
102
+ return typeof requestId==='string'&&requestId.length?requestId.slice(0,8):'????????';
103
+ }
104
+
105
+ export function createLiveTimelineTracker({clock=()=>Date.now()}={}){
106
+ if(typeof clock!=='function') throw new Error('live timeline clock must be a function');
107
+ const pending=new Map();
108
+ const completed=new Set();
109
+ return Object.freeze({
110
+ observe(event){
111
+ if(!event||typeof event!=='object'||Array.isArray(event)) return null;
112
+ const requestId=typeof event.request_id==='string'&&event.request_id?event.request_id:null;
113
+ const tool=typeof event.tool==='string'&&event.tool?event.tool:null;
114
+ if(event.event_type==='tool.request'&&requestId&&tool){
115
+ if(pending.has(requestId)||completed.has(requestId)) return null;
116
+ pending.set(requestId,{request_id:requestId,tool,started_at_ms:clock()});
117
+ return formatRemoteToolCall(event,{clock});
118
+ }
119
+ if(event.event_type==='tool.result'&&requestId&&tool){
120
+ if(completed.has(requestId)) return null;
121
+ const span=pending.get(requestId)??null;
122
+ pending.delete(requestId);completed.add(requestId);
123
+ const error=event.outcome?.is_error===true;
124
+ const duration=Number.isFinite(Number(event.outcome?.duration_ms))
125
+ ? formatDuration(event.outcome.duration_ms)
126
+ : span?formatDuration(Math.max(0,clock()-span.started_at_ms)):'';
127
+ const marker=span?(error?'✗':'✓'):'↩';
128
+ const status=error?'ERR':'OK ';
129
+ const suffix=span?'':' (request before live window)';
130
+ return `${marker} [${shortSpanId(requestId)}] ${status} ${tool}${duration?` ${duration}`:''}${suffix}`;
131
+ }
132
+ return null;
133
+ },
134
+ pending(){return [...pending.values()].map(({request_id,tool})=>({request_id,tool}));},
135
+ reconcileInterrupted(reason='session_end'){
136
+ const suffix=reason==='recovery'?'recovery ended before terminal result':
137
+ reason==='session_end'?'session ended before terminal result':'stream ended before terminal result';
138
+ const lines=[...pending.values()].map(item=>`⚠ [${shortSpanId(item.request_id)}] INTERRUPTED ${item.tool} — ${suffix}`);
139
+ pending.clear();
140
+ return lines;
141
+ }
142
+ });
143
+ }
144
+
145
+
146
+ export function formatRuntimeDiagnosticLine(file,line){
147
+ if(typeof file!=='string'||typeof line!=='string'||!line) return null;
148
+ const label=file.split(/[\\/]/).at(-1).replace(/\.log$/i,'').toUpperCase();
149
+ const input=line.match(/\[TOOL IN \]\s+\S+\s+#\d+\s+([A-Za-z0-9_.-]+)/);
150
+ if(input) return `[${label}] IN ${input[1]}`;
151
+ const output=line.match(/\[TOOL OUT\]\s+\S+\s+#\d+\s+([A-Za-z0-9_.-]+)/);
152
+ if(output){
153
+ if(/"is_error"\s*:\s*true/.test(line)) return `[${label}] ERR ${output[1]}`;
154
+ return `[${label}] OUT ${output[1]}`;
155
+ }
156
+ return null;
157
+ }
@@ -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;
@@ -1,10 +1,12 @@
1
1
  import { lstat, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { sanitizeAuditArgs } from './tool-audit.mjs';
3
4
 
4
5
  export const REMOTE_RESULT_JOURNAL_SCHEMA='deadbyte.remote-result-journal.v1';
5
6
  const SESSION=/^[0-9a-f]{24}$/;
6
7
  const HASH=/^[0-9a-f]{64}$/;
7
8
  const STATUS=new Set(['ok','error']);
9
+ const EVENT_TYPES=new Set(['tool.call','tool.result']);
8
10
 
9
11
  function text(value,label,max){
10
12
  if(typeof value!=='string'||value.length<1||value.length>max||value.includes('\0')) throw new Error(`${label} invalid`);
@@ -26,14 +28,22 @@ function validateEvent(raw,index=null){
26
28
  const sessionId=validateSession(raw.session_id);
27
29
  const requestId=text(raw.request_id,'remote result request_id',256);
28
30
  const tool=text(raw.tool,'remote result tool',128);
29
- if(!STATUS.has(raw.status)) throw new Error('remote result status invalid');
31
+ const eventType=raw.event_type===undefined?'tool.result':raw.event_type;
32
+ if(!EVENT_TYPES.has(eventType)) throw new Error('remote result event_type invalid');
30
33
  const observed=iso(raw.observed_at_utc,'remote result observed_at_utc');
31
34
  if(typeof raw.payload_sha256!=='string'||!HASH.test(raw.payload_sha256)) throw new Error('remote result payload_sha256 invalid');
32
35
  if(index!==null&&seq!==index) throw new Error('remote result sequence mismatch');
33
- return Object.freeze({session_id:sessionId,seq,request_id:requestId,tool,status:raw.status,
36
+ if(eventType==='tool.call'){
37
+ const rawArgs=raw.arguments??{};
38
+ if(!rawArgs||typeof rawArgs!=='object'||Array.isArray(rawArgs)) throw new Error('remote result call arguments invalid');
39
+ return Object.freeze({session_id:sessionId,seq,event_type:eventType,request_id:requestId,tool,
40
+ observed_at_utc:observed,payload_sha256:raw.payload_sha256,arguments:sanitizeAuditArgs(rawArgs)});
41
+ }
42
+ if(!STATUS.has(raw.status)) throw new Error('remote result status invalid');
43
+ return Object.freeze({session_id:sessionId,seq,event_type:eventType,request_id:requestId,tool,status:raw.status,
34
44
  observed_at_utc:observed,payload_sha256:raw.payload_sha256});
35
45
  }
36
- function identity(item){return `${item.request_id}\0${item.status}\0${item.payload_sha256}`;}
46
+ function identity(item){return `${item.event_type}\0${item.request_id}\0${item.status??''}\0${item.payload_sha256}`;}
37
47
  export function parseRemoteResultCursor(cursor){
38
48
  if(typeof cursor!=='string') throw new Error('remote result cursor invalid');
39
49
  const match=/^r1:([0-9a-f]{24}):(0|[1-9][0-9]{0,15})$/.exec(cursor);
@@ -89,8 +99,17 @@ export async function openRemoteResultJournal({filePath,sessionId,maxEntries=204
89
99
  return Object.freeze({
90
100
  file_path:file,session_id:sid,
91
101
  cursor(){return cursorFor(sid,state.next_seq-1);},
102
+ async recordCall(input){
103
+ const candidate=validateEvent({session_id:sid,seq:state.next_seq,event_type:'tool.call',...input});
104
+ const key=identity(candidate);
105
+ const existing=state.events.find(event=>identity(event)===key);
106
+ if(existing) return {duplicate:true,event:existing,cursor:cursorFor(sid,state.next_seq-1)};
107
+ const event=candidate;
108
+ state.events.push(event);state.next_seq+=1;await persist();
109
+ return {duplicate:false,event,cursor:cursorFor(sid,event.seq)};
110
+ },
92
111
  async recordTerminal(input){
93
- const candidate=validateEvent({session_id:sid,seq:state.next_seq,...input});
112
+ const candidate=validateEvent({session_id:sid,seq:state.next_seq,event_type:'tool.result',...input});
94
113
  const key=identity(candidate);
95
114
  const existing=state.events.find(event=>identity(event)===key);
96
115
  if(existing) return {duplicate:true,event:existing,cursor:cursorFor(sid,state.next_seq-1)};
package/src/version.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const DEADBYTE_VERSION = '0.10.0';
1
+ export const DEADBYTE_VERSION = '0.11.1';
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';