deadbyte-mcp 0.15.37 → 0.15.39

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 (42) hide show
  1. package/MANIFEST.SHA256 +41 -22
  2. package/PROVENANCE.json +55 -20
  3. package/README.txt +19 -19
  4. package/RELEASE.json +50 -21
  5. package/SBOM.cdx.json +2 -2
  6. package/TEST-MATRIX.json +67 -15
  7. package/bridge/package.json +1 -1
  8. package/bridge/src/bridge.mjs +4 -4
  9. package/bridge/src/r40-governor.mjs +109 -19
  10. package/controller/deadbyte-process-policy.json +4 -4
  11. package/docs/R61-TASK-CONTRACT.md +11 -0
  12. package/docs/r61-reference-evidence.md +18 -0
  13. package/package.json +5 -3
  14. package/scripts/gate-windows-r61-prepublish.ps1 +45 -0
  15. package/scripts/gate-windows-r62-prepublish.ps1 +45 -0
  16. package/scripts/npm-artifact-identity-r61.mjs +22 -0
  17. package/scripts/npm-release-r48.mjs +21 -5
  18. package/scripts/r61-mixed-lane-benchmark.mjs +98 -0
  19. package/scripts/r62-focused.ps1 +9 -0
  20. package/scripts/r62-full.ps1 +9 -0
  21. package/scripts/r62-live-transaction-audit.mjs +17 -0
  22. package/scripts/r62-unresolved-transaction-inspect.mjs +54 -0
  23. package/src/deadbyte-cli.mjs +9 -6
  24. package/src/doctor-transactions-r58.mjs +79 -8
  25. package/src/remote-live-log.mjs +28 -5
  26. package/src/runtime-update.mjs +28 -4
  27. package/src/version.mjs +1 -1
  28. package/test/bridge-runtime-r59.test.mjs +5 -5
  29. package/test/commander-schema-differential-r48.test.mjs +1 -1
  30. package/test/r58-doctor-transactions.test.mjs +2 -2
  31. package/test/r60-release-contract.test.mjs +4 -3
  32. package/test/r61-control-lane.test.mjs +154 -0
  33. package/test/r61-live-timeline-bounds.test.mjs +31 -0
  34. package/test/r61-npm-artifact-identity.test.mjs +17 -0
  35. package/test/r61-release-bridge-binding.test.mjs +32 -0
  36. package/test/r61-release-contract.test.mjs +45 -0
  37. package/test/r62-doctor-receipt-semantics.test.mjs +50 -0
  38. package/test/r62-doctor-transactions-reconcile.test.mjs +208 -0
  39. package/test/r62-release-contract.test.mjs +59 -0
  40. package/test/r62-rollback-failure-receipt.test.mjs +19 -0
  41. package/test/remote-live-log-r37.test.mjs +23 -6
  42. package/test/remote-session-cli.test.mjs +1 -1
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from 'node:child_process';
2
+ import { verifyPublishedTarballIdentity } from './npm-artifact-identity-r61.mjs';
2
3
  import { mkdtemp, readFile, rm, unlink } from 'node:fs/promises';
3
4
  import path from 'node:path';
4
5
  import os from 'node:os';
@@ -58,6 +59,8 @@ const allowedUnmanifested = new Set([
58
59
 
59
60
  async function main() {
60
61
  const isVerifyOnly = process.argv.includes('--verify-only') || process.argv.includes('--dry-run');
62
+ const publishOnly = process.argv.includes('--publish-only');
63
+ if (isVerifyOnly && publishOnly) throw new Error('--publish-only conflicts with verify-only mode');
61
64
 
62
65
  console.log(`[1/7] Reading package.json and checking registry publication status... ${isVerifyOnly ? '(MODE: VERIFY-ONLY / DRY-RUN)' : '(MODE: PUBLISH)'}`);
63
66
  const pkg = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
@@ -180,6 +183,8 @@ async function main() {
180
183
  return;
181
184
  }
182
185
 
186
+ const localBytes = await readFile(tarball);
187
+
183
188
  console.log('[6/7] Publishing artifact to npm registry...');
184
189
  publishAttempted = true;
185
190
  let publishOut;
@@ -193,8 +198,15 @@ async function main() {
193
198
  }
194
199
  throw publishError;
195
200
  }
196
- const published = parseJson(runNpm(['view', `deadbyte-mcp@${version}`, 'version', 'dist.integrity', 'dist.tarball', '--json'], { timeout: 120000 }));
201
+ const viewed = parseJson(runNpm(['view', `deadbyte-mcp@${version}`, 'version', 'dist.integrity', 'dist.shasum', 'dist.tarball', '--json'], { timeout: 120000 }));
202
+ const published = Array.isArray(viewed) ? viewed[0] : viewed;
203
+ const dist = published?.dist ?? published;
204
+ if(typeof dist?.tarball !== 'string')throw new Error('published registry tarball URL missing');
205
+ const registryResponse = await fetch(dist.tarball,{signal:AbortSignal.timeout(120000)});
206
+ if(!registryResponse.ok) throw new Error(`registry tarball fetch failed status=${registryResponse.status}`);
207
+ const registryBytes = Buffer.from(await registryResponse.arrayBuffer());
197
208
  const tags = parseJson(runNpm(['view', 'deadbyte-mcp', 'dist-tags', '--json'], { timeout: 120000 }));
209
+ const identity=verifyPublishedTarballIdentity({version,localBytes,registryBytes,published,tags});
198
210
 
199
211
  console.log('[7/7] Post-publish external registry smoke test (executed outside repo in temp)...');
200
212
  const tempDir = os.tmpdir();
@@ -211,16 +223,20 @@ async function main() {
211
223
  timeout: 300000,
212
224
  });
213
225
 
214
- console.log('[8/8] Activating the published release into the live immutable slot and waiting for verified handoff...');
215
- run(node, [path.join(root, 'scripts', 'deploy-r48.mjs')], { cwd: root, timeout: 900000 });
226
+ if(!publishOnly){
227
+ console.log('[8/8] Activating the published release into the live immutable slot and waiting for verified handoff...');
228
+ run(node, [path.join(root, 'scripts', 'deploy-r48.mjs')], { cwd: root, timeout: 900000 });
229
+ }else console.log('[8/8] Publication verified; activation deliberately deferred to the separate production gate.');
216
230
 
217
231
  releaseSucceeded = true;
218
232
  console.log('\nRELEASE SUCCESSFUL:');
219
233
  console.log(JSON.stringify({
220
- status: 'published',
234
+ status: publishOnly?'published_verified_activation_pending':'published',
221
235
  name: pkg.name,
222
236
  version,
223
237
  tarball: filename,
238
+ tarball_sha256: identity.tarball_sha256,
239
+ npm_integrity: identity.npm_integrity,
224
240
  published,
225
241
  published_output_tail: publishOut.slice(-2000),
226
242
  dist_tags: tags,
@@ -231,7 +247,7 @@ async function main() {
231
247
  }, null, 2));
232
248
 
233
249
  } finally {
234
- if (releaseSucceeded || !publishAttempted) {
250
+ if ((releaseSucceeded && !publishOnly) || !publishAttempted) {
235
251
  await unlink(tarball).catch(() => {});
236
252
  } else {
237
253
  console.error('Release artifact preserved after failure: ' + tarball);
@@ -0,0 +1,98 @@
1
+ import {mkdtemp,mkdir,readFile,readdir,stat,writeFile,open,rm} from 'node:fs/promises';
2
+ import {spawn} from 'node:child_process';
3
+ import {createHash} from 'node:crypto';
4
+ import {monitorEventLoopDelay,performance} from 'node:perf_hooks';
5
+ import {pathToFileURL} from 'node:url';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+
9
+ const roots=process.argv.slice(2);
10
+ if(roots.length!==2||roots.some(item=>!path.isAbsolute(item)))throw new Error('two absolute governor module paths required');
11
+ const levels=(process.env.DEADBYTE_R61_BENCH_LEVELS??'1,2,4,8,16,32').split(',').map(Number);
12
+ if(!levels.length||levels.some(level=>![1,2,4,8,16,32].includes(level))||new Set(levels).size!==levels.length)throw new Error('benchmark levels invalid');
13
+ const ROUNDS=Number(process.env.DEADBYTE_R61_BENCH_ROUNDS??3),OPS=160;
14
+ if(!Number.isInteger(ROUNDS)||ROUNDS<3||ROUNDS>15)throw new Error('benchmark rounds must be 3..15');
15
+ const scratchRoot=process.env.DEADBYTE_R61_SCRATCH_ROOT;
16
+ if(!scratchRoot||!path.isAbsolute(scratchRoot))throw new Error('explicit absolute DEADBYTE_R61_SCRATCH_ROOT required');
17
+ const quant=(values,p)=>{const ordered=[...values].sort((a,b)=>a-b);return ordered[Math.max(0,Math.ceil(ordered.length*p)-1)]};
18
+ const sha=value=>createHash('sha256').update(value).digest('hex');
19
+
20
+ async function childOutput(){
21
+ const child=spawn(process.execPath,['-e','process.stdout.write("R61_CHILD_OK")'],{windowsHide:true,stdio:['ignore','pipe','pipe']});
22
+ let output='';for await(const chunk of child.stdout)output+=chunk;
23
+ const exit=await new Promise(resolve=>child.once('close',resolve));
24
+ if(exit!==0||output!=='R61_CHILD_OK')throw new Error('bounded child process oracle failed');
25
+ }
26
+
27
+ async function runCase(modulePath,level,round){
28
+ const {createBridgeGovernor}=await import(pathToFileURL(modulePath).href);
29
+ await mkdir(scratchRoot,{recursive:true});
30
+ const scratch=await mkdtemp(path.join(scratchRoot,'deadbyte-r61-bench-'));
31
+ const fixture=path.join(scratch,'fixture'),audit=path.join(scratch,'presentation.jsonl');
32
+ await mkdir(fixture);
33
+ const sample='deadbyte bounded corpus สวัสดี\n'.repeat(512);
34
+ for(let i=0;i<16;i++)await writeFile(path.join(fixture,`f${i}.txt`),sample);
35
+ const governor=createBridgeGovernor();
36
+ const calls=[],control=[],counts={error:0,unknown:0},peak={rss:process.memoryUsage().rss,queue:0,active:0};
37
+ const delay=monitorEventLoopDelay({resolution:20});delay.enable();
38
+ let next=0;
39
+ const started=performance.now();
40
+ async function worker(){
41
+ while(next<OPS){
42
+ const i=next++;
43
+ const kind=i%16;
44
+ const tool=kind<3?(kind===0?'machine_ping':'machine_status'):
45
+ kind<5?'capability_manifest':kind<8?'machine_fs_read':kind<10?'coding_search':
46
+ kind<12?'machine_fs_write':kind===12?'process_session_start':kind<15?'coding_file_read':'machine_process_list';
47
+ const t=performance.now();let ticket;
48
+ try{
49
+ ticket=await governor.acquire(tool);
50
+ if(tool==='machine_ping')sha(`nonce-${round}-${i}`);
51
+ else if(tool==='machine_status')await stat(fixture);
52
+ else if(tool==='capability_manifest')sha(await readFile(roots[0]));
53
+ else if(tool==='coding_search'){
54
+ const names=await readdir(fixture);
55
+ for(const name of names)if((await readFile(path.join(fixture,name),'utf8')).includes('bounded'))break;
56
+ }else if(tool==='machine_fs_write')await writeFile(path.join(scratch,`out-${i}.txt`),sample.slice(0,128));
57
+ else if(tool==='process_session_start')await childOutput();
58
+ else if(tool==='machine_process_list')os.cpus().length;
59
+ else await readFile(path.join(fixture,`f${i%16}.txt`));
60
+ const line=JSON.stringify({kind,at:i,hash:sha(String(i))})+'\n';
61
+ const handle=await open(audit,'a');try{await handle.writeFile(line);await handle.sync();}finally{await handle.close();}
62
+ }catch(error){counts.error++;throw error;}
63
+ finally{
64
+ ticket?.release();
65
+ const elapsed=performance.now()-t;
66
+ calls.push(elapsed);if(kind<3)control.push(elapsed);
67
+ const state=governor.snapshot();
68
+ peak.rss=Math.max(peak.rss,process.memoryUsage().rss);
69
+ peak.queue=Math.max(peak.queue,...Object.values(state).map(value=>value.queued));
70
+ peak.active=Math.max(peak.active,...Object.values(state).map(value=>value.active));
71
+ }
72
+ }
73
+ }
74
+ try{
75
+ await Promise.all(Array.from({length:level},()=>worker()));
76
+ await governor.close();
77
+ const total=performance.now()-started;
78
+ return {level,round,n:calls.length,throughput_ops_s:OPS*1000/total,p50_ms:quant(calls,.5),p95_ms:quant(calls,.95),
79
+ p99_ms:quant(calls,.99),max_ms:Math.max(...calls),control_p95_ms:quant(control,.95),
80
+ event_loop_p95_ms:delay.percentile(95)/1e6,peak_rss_bytes:peak.rss,peak_queue:peak.queue,
81
+ peak_active:peak.active,errors:counts.error,unknown:counts.unknown};
82
+ }finally{delay.disable();await rm(scratch,{recursive:true,force:true});}
83
+ }
84
+
85
+ const results=[];
86
+ for(const level of levels)for(let round=0;round<ROUNDS;round++){
87
+ const order=round%2===0?[0,1]:[1,0];
88
+ for(const index of order)results.push({target:index,...await runCase(roots[index],level,round)});
89
+ }
90
+ const report={schema:'deadbyte.r61.mixed-lane-benchmark.v1',node:process.version,platform:process.platform,
91
+ governor_sha256:await Promise.all(roots.map(async value=>sha(await readFile(value)))),levels,rounds:ROUNDS,operations_per_run:OPS,
92
+ limitations:['direct governor and real scratch I/O, not full MCP transport','presentation traffic is fsynced fixture data, not signed audit'],results};
93
+ const outputPath=process.env.DEADBYTE_R61_BENCH_OUTPUT;
94
+ if(outputPath){
95
+ if(!path.isAbsolute(outputPath))throw new Error('benchmark output must be absolute');
96
+ await writeFile(outputPath,JSON.stringify(report,null,2)+'\n',{flag:'wx'});
97
+ console.log(JSON.stringify({schema:report.schema,output_path:outputPath,result_count:results.length,governor_sha256:report.governor_sha256}));
98
+ }else console.log(JSON.stringify(report,null,2));
@@ -0,0 +1,9 @@
1
+ $ErrorActionPreference='Stop'
2
+ $root=Split-Path -Parent $PSScriptRoot
3
+ Push-Location $root
4
+ try {
5
+ & npm.cmd run r62:focused:test
6
+ if($LASTEXITCODE -ne 0){ exit $LASTEXITCODE }
7
+ } finally {
8
+ Pop-Location
9
+ }
@@ -0,0 +1,9 @@
1
+ $ErrorActionPreference='Stop'
2
+ $root=Split-Path -Parent $PSScriptRoot
3
+ Push-Location $root
4
+ try {
5
+ & npm.cmd test
6
+ $code=$LASTEXITCODE
7
+ Write-Host ("R62_FULL_EXIT="+$code)
8
+ exit $code
9
+ } finally { Pop-Location }
@@ -0,0 +1,17 @@
1
+ import {inspectDoctorTransactions} from '../src/doctor-transactions-r58.mjs';
2
+
3
+ const result=await inspectDoctorTransactions();
4
+ const summary=result.findings.find(item=>item.evidence?.receipt_verification_performed===true)?.evidence??null;
5
+ const unresolved=result.findings
6
+ .filter(item=>item.level==='WARN'&&item.evidence?.recorded_phase)
7
+ .map(item=>({schema:item.evidence.schema,phase:item.evidence.recorded_phase,state_sha256:item.evidence.state_sha256}));
8
+ const phases={};
9
+ for(const item of unresolved) phases[item.phase]=(phases[item.phase]??0)+1;
10
+ console.log(JSON.stringify({
11
+ status:result.status,
12
+ summary,
13
+ unresolved_count:unresolved.length,
14
+ unresolved_phases:phases,
15
+ fail_count:result.findings.filter(item=>item.level==='FAIL').length,
16
+ inventory_warning:result.findings.some(item=>item.evidence?.inventory_complete===false)
17
+ },null,2));
@@ -0,0 +1,54 @@
1
+ import {opendir,lstat,readFile} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import {createHash} from 'node:crypto';
5
+
6
+ const state=path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));
7
+ const root=path.join(state,'evidence','updates');
8
+ const sha=b=>createHash('sha256').update(b).digest('hex');
9
+ const terminal=new Set(['completed','superseded','failed_receipted']);
10
+ const rows=[];
11
+ for await(const entry of await opendir(root)){
12
+ if(!entry.isDirectory()||entry.isSymbolicLink()) continue;
13
+ const dir=path.join(root,entry.name),stateFile=path.join(dir,'state.json');
14
+ let stateBytes,body;
15
+ try{
16
+ const info=await lstat(stateFile);
17
+ if(!info.isFile()||info.isSymbolicLink()||info.size>1048576) continue;
18
+ stateBytes=await readFile(stateFile);
19
+ body=JSON.parse(stateBytes.toString('utf8').replace(/^\uFEFF/,''));
20
+ }catch{continue;}
21
+ if(!['deadbyte.r27-slot-update-state.v1','deadbyte.r31-slot-rollback-state.v1'].includes(body?.schema)) continue;
22
+ const has=async name=>{try{const st=await lstat(path.join(dir,name));return st.isFile()&&!st.isSymbolicLink();}catch{return false;}};
23
+ if(terminal.has(body.phase)||await has('receipt.json')||await has('failure-receipt.json')) continue;
24
+ const handoff=await has('handoff.json');
25
+ let handoffSummary=null;
26
+ if(handoff){
27
+ try{
28
+ const h=JSON.parse((await readFile(path.join(dir,'handoff.json'),'utf8')).replace(/^\uFEFF/,''));
29
+ handoffSummary={schema:h?.body?.schema??h?.schema??null,body:h?.body??h};
30
+ }catch(error){handoffSummary={parse_error:String(error?.message??error)};}
31
+ }
32
+ rows.push({
33
+ entry:entry.name,
34
+ entry_sha256:sha(Buffer.from(entry.name,'utf8')),
35
+ state_sha256:sha(stateBytes),
36
+ schema:body.schema,
37
+ phase:body.phase,
38
+ update_id:body.update_id??null,
39
+ rollback_id:body.rollback_id??null,
40
+ prior_pointer:body.prior_pointer??null,
41
+ new_pointer:body.new_pointer??null,
42
+ active_pointer:body.active_pointer??null,
43
+ old_runtime:body.old_runtime??null,
44
+ new_runtime:body.new_runtime??null,
45
+ error:body.error??null,
46
+ recovery:body.recovery??null,
47
+ has_handoff:handoff,
48
+ handoff:handoffSummary
49
+ });
50
+ }
51
+ rows.sort((a,b)=>a.entry.localeCompare(b.entry));
52
+ let active=null;
53
+ try{active=JSON.parse(await readFile(path.join(state,'active-release.json'),'utf8'));}catch{}
54
+ console.log(JSON.stringify({state_root:state,active_pointer:active,unresolved_count:rows.length,rows},null,2));
@@ -226,12 +226,12 @@ function parseAuditLogLine(file,line){
226
226
  if(!/^segment-\d{8}\.jsonl$/.test(path.basename(file))||!line) return null;
227
227
  try{return JSON.parse(line);}catch{return null;}
228
228
  }
229
- function formatLogLine(file,line,{auditEvent=null,activityTracker=null,sequence=null,verbose=false,displayMode='unicode'}={}){
229
+ function formatLogLine(file,line,{auditEvent=null,activityTracker=null,sequence=null,sourceSequence=null,verbose=false,displayMode='unicode'}={}){
230
230
  if(!line) return null;
231
231
  if(/^segment-\d{8}\.jsonl$/.test(path.basename(file))){
232
232
  const event=auditEvent??parseAuditLogLine(file,line);
233
233
  if(!event) return null;
234
- return activityTracker?activityTracker.observe(event,{sequence}):formatCompactAuditEvent(event);
234
+ return activityTracker?activityTracker.observe(event,{sequence,sourceSequence}):formatCompactAuditEvent(event);
235
235
  }
236
236
  return formatRuntimeDiagnosticLine(file,line,{verbose,displayMode});
237
237
  }
@@ -328,7 +328,7 @@ function timelineFilterEvent(timeline){
328
328
  async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTracker=null,timelineFilter=null,timelineWriter=null,verbose=false,displayMode='unicode'}={}){
329
329
  process.stdout.write(verbose
330
330
  ? '[LOG] Verbose live timeline. Full sanitized request detail enabled; Ctrl+C disconnects and revokes authority.\n'
331
- : '[LOG] Human live timeline. Transport IN/OUT hidden; full replay/audit detail remains on disk. Ctrl+C disconnects and revokes authority.\n');
331
+ : '[LOG] Human live timeline. Transport IN/OUT hidden; full replay/audit detail remains on disk. START/DONE retained; heartbeat bursts folded; Ctrl+C disconnects and revokes authority.\n');
332
332
  const activeOffsets=offsets??await snapshotRemoteLogOffsets(root);
333
333
  const partialLines=new Map();
334
334
  let liveFiles=[...activeOffsets.keys()];
@@ -358,7 +358,8 @@ async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTra
358
358
  : await journal.recordTerminal(timeline);
359
359
  if(recorded?.duplicate) continue;
360
360
  const accepted=timelineFilter&&timeline?timelineFilter.accept(timelineFilterEvent(timeline)):true;
361
- const formatted=formatLogLine(file,line,{auditEvent,activityTracker,sequence:recorded?.event?.seq??null,verbose,displayMode});
361
+ const formatted=formatLogLine(file,line,{auditEvent,activityTracker,sequence:recorded?.event?.seq??null,
362
+ sourceSequence:Number.isSafeInteger(auditEvent?.seq)?auditEvent.seq:null,verbose,displayMode});
362
363
  if(!formatted||!accepted) continue;
363
364
  const output=Array.isArray(formatted)?formatted:[formatted];
364
365
  for(const item of output) if(item){
@@ -512,9 +513,9 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
512
513
  filePath:remoteResultJournalPath(ctx.root,marker.result_session_id),sessionId:marker.result_session_id,maxEntries:2048
513
514
  });
514
515
  const rootPaths=await readLiveRootPaths(ctx.root);
515
- const activityTracker=createLiveTimelineTracker({verbose,displayMode,rootPaths,trace});
516
+ const activityTracker=createLiveTimelineTracker({verbose,displayMode,rootPaths,trace,foldHeartbeats:!verbose});
516
517
  const timelineFilter=filterExpression?createLiveTimelineFilter(filterExpression):null;
517
- const timelineWriter=createLiveTimelineWriter({stream:process.stdout,displayMode,rewrite:!verbose});
518
+ const timelineWriter=createLiveTimelineWriter({stream:process.stdout,displayMode,rewrite:false});
518
519
  const aborter=new AbortController();
519
520
  const abort=()=>aborter.abort();
520
521
  const watchStdin=!process.stdin.isTTY;
@@ -588,6 +589,8 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
588
589
  if(logTask) await logTask.catch(()=>{});
589
590
  timelineWriter.flush();
590
591
  for(const line of activityTracker.reconcileInterrupted('session_end')) process.stdout.write(`${line}\n`);
592
+ const filterRetention=timelineFilter?.retention();
593
+ if(filterRetention?.evicted>0) process.stdout.write(`[WARN] LIVELOG filter evicted ${filterRetention.evicted} presentation identities; full audit remains on disk.\n`);
591
594
  process.stdout.write(`${activityTracker.summary()}\n`);
592
595
  process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);
593
596
  if(watchStdin){process.stdin.removeListener('end',abort);process.stdin.removeListener('close',abort);process.stdin.pause();}
@@ -1,10 +1,37 @@
1
1
  import {lstat,opendir,readFile} from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import {createHash} from 'node:crypto';
3
+ import {createHash,createPublicKey} from 'node:crypto';
4
4
  import {resolveStateRoot} from './state-root.mjs';
5
+ import {verifySlotUpdateEnvelope} from './runtime-update.mjs';
5
6
 
6
- export async function inspectDoctorTransactions(explicitRoot,{maxEntries=256}={}){
7
- if(!Number.isSafeInteger(maxEntries)||maxEntries<1||maxEntries>256)throw new Error('doctor transaction budget invalid');
7
+ const UPDATE_RECEIPT='deadbyte.r27-slot-update-receipt.v1';
8
+ const FAILURE_RECEIPT='deadbyte.r27-slot-update-failure.v1';
9
+ const ROLLBACK_RECEIPT='deadbyte.r31-slot-rollback-receipt.v1';
10
+ const ROLLBACK_FAILURE_RECEIPT='deadbyte.r31-slot-rollback-failure.v1';
11
+
12
+ async function readReceipt(file){
13
+ const info=await lstat(file);
14
+ if(!info.isFile()||info.isSymbolicLink()||info.size>1048576)throw new Error('unsafe transaction receipt');
15
+ const bytes=await readFile(file);
16
+ if(bytes.length>1048576)throw new Error('transaction receipt budget');
17
+ return JSON.parse(bytes.toString('utf8').replace(/^\uFEFF/,''));
18
+ }
19
+
20
+ function receiptSubjectMatches(state,envelope,kind){
21
+ const body=envelope?.body;
22
+ if(state.schema==='deadbyte.r27-slot-update-state.v1'){
23
+ if(kind==='success')return body?.schema===UPDATE_RECEIPT&&body?.update_id===state.update_id&&body?.rollback_performed===false;
24
+ return body?.schema===FAILURE_RECEIPT&&body?.update_id===state.update_id;
25
+ }
26
+ if(state.schema==='deadbyte.r31-slot-rollback-state.v1'){
27
+ if(kind==='success')return body?.schema===ROLLBACK_RECEIPT&&body?.rollback_id===state.rollback_id&&body?.rollback_performed===true;
28
+ return body?.schema===ROLLBACK_FAILURE_RECEIPT&&body?.rollback_id===state.rollback_id;
29
+ }
30
+ return false;
31
+ }
32
+
33
+ export async function inspectDoctorTransactions(explicitRoot,{maxEntries=1024}={}){
34
+ if(!Number.isSafeInteger(maxEntries)||maxEntries<1||maxEntries>1024)throw new Error('doctor transaction budget invalid');
8
35
  const state=resolveStateRoot(explicitRoot),root=path.join(state,'evidence','updates'),findings=[];
9
36
  const next='Retain transaction evidence; inspect the exact signed receipt/handoff and current pointer before any supported recovery. Doctor never launches recovery.';
10
37
  const add=(level,what,evidence)=>findings.push({id:'transaction_residue',level,what,evidence,next_action:level==='PASS'?null:next});
@@ -17,24 +44,68 @@ export async function inspectDoctorTransactions(explicitRoot,{maxEntries=256}={}
17
44
  else add('FAIL','Update evidence directory cannot be safely inspected.',{error_code:error?.code??'VALIDATION_FAILED'});
18
45
  return {status:findings[0].level,findings};
19
46
  }
20
- let entries=0,checked=0,terminal=0;
47
+ let publicKey=null;
48
+ const loadPublicKey=async()=>{
49
+ if(publicKey)return publicKey;
50
+ const keyFile=path.join(state,'trust','ed25519-public.pem');
51
+ const info=await lstat(keyFile);
52
+ if(!info.isFile()||info.isSymbolicLink()||info.size>65536)throw new Error('unsafe transaction trust key');
53
+ publicKey=createPublicKey(await readFile(keyFile));
54
+ if(publicKey.asymmetricKeyType!=='ed25519')throw new Error('transaction trust key must be Ed25519');
55
+ return publicKey;
56
+ };
57
+ let entries=0,checked=0,terminal=0,verifiedTerminalReceipts=0,verifiedSuccessReceipts=0,verifiedFailureReceipts=0,unresolved=0,inventoryComplete=true;
21
58
  try{
22
59
  for await(const entry of directory){
23
- if(++entries>maxEntries){add('WARN','Transaction inventory budget reached; unobserved entries remain.',{max_entries:maxEntries,states_checked:checked,inventory_complete:false});break;}
60
+ if(++entries>maxEntries){inventoryComplete=false;add('WARN','Transaction inventory budget reached; unobserved entries remain.',{max_entries:maxEntries,states_checked:checked,inventory_complete:false});break;}
24
61
  if(entry.isSymbolicLink()){add('FAIL','Transaction evidence contains a link; it was not followed.',{entry_name_sha256:createHash('sha256').update(entry.name).digest('hex')});continue;}
25
62
  if(!entry.isDirectory())continue;
26
- const file=path.join(root,entry.name,'state.json');let bytes;
63
+ const dir=path.join(root,entry.name),file=path.join(dir,'state.json');let bytes;
27
64
  try{
28
65
  const info=await lstat(file);if(!info.isFile()||info.isSymbolicLink()||info.size>1048576)throw new Error('unsafe transaction state');
29
66
  bytes=await readFile(file);if(bytes.length>1048576)throw new Error('transaction state budget');
30
67
  const body=JSON.parse(bytes.toString('utf8').replace(/^\uFEFF/,''));checked++;
31
68
  const digest=createHash('sha256').update(bytes).digest('hex');
32
69
  if(!['deadbyte.r27-slot-update-state.v1','deadbyte.r31-slot-rollback-state.v1'].includes(body?.schema)||typeof body.phase!=='string'||!/^[a-z_]{1,64}$/.test(body.phase)){add('FAIL','Transaction state schema or phase is invalid.',{state_sha256:digest,check:'transaction_schema'});continue;}
33
- if(['completed','superseded','failed_receipted'].includes(body.phase)){terminal++;continue;}
70
+ const receiptPath=path.join(dir,'receipt.json'),failurePath=path.join(dir,'failure-receipt.json');
71
+ let receiptExists=false,failureExists=false;
72
+ try{const st=await lstat(receiptPath);receiptExists=st.isFile()&&!st.isSymbolicLink();}catch(error){if(error?.code!=='ENOENT')throw error;}
73
+ try{const st=await lstat(failurePath);failureExists=st.isFile()&&!st.isSymbolicLink();}catch(error){if(error?.code!=='ENOENT')throw error;}
74
+ if(receiptExists&&failureExists){add('FAIL','Transaction contains conflicting terminal receipts.',{state_sha256:digest,check:'terminal_receipt_conflict'});continue;}
75
+ const terminalPath=receiptExists?receiptPath:failureExists?failurePath:null;
76
+ if(terminalPath){
77
+ const kind=receiptExists?'success':'failure';
78
+ const envelope=await readReceipt(terminalPath);
79
+ const key=await loadPublicKey();
80
+ if(!verifySlotUpdateEnvelope(envelope,key)||!receiptSubjectMatches(body,envelope,kind)){
81
+ add('FAIL','Transaction terminal receipt is invalid or not subject-bound to its journal.',{state_sha256:digest,check:'terminal_receipt_verification',receipt_kind:kind});
82
+ continue;
83
+ }
84
+ terminal++;verifiedTerminalReceipts++;
85
+ if(kind==='success')verifiedSuccessReceipts++;else verifiedFailureReceipts++;
86
+ continue;
87
+ }
88
+
89
+ if(body.phase==='superseded'){terminal++;continue;}
90
+ if(body.phase==='failed_receipted'){
91
+ add('FAIL','Transaction journal claims a signed failure receipt but the receipt is missing.',{state_sha256:digest,check:'missing_failure_receipt',recorded_phase:body.phase});
92
+ continue;
93
+ }
94
+ unresolved++;
34
95
  add('WARN','Nonterminal or unresolved transaction record remains; liveness/outcome is not inferred from its age or PID.',{state_sha256:digest,schema:body.schema,recorded_phase:body.phase});
35
96
  }catch(error){if(error?.code==='ENOENT')continue;add('FAIL','Transaction state cannot be safely parsed or inspected.',{error_code:error?.code??'VALIDATION_FAILED'});}
36
97
  }
37
98
  }catch(error){add('FAIL','Transaction inventory observation failed.',{error_code:error?.code??'OBSERVATION_FAILED'});}
38
- add('PASS','Recorded terminal transaction phases counted; signatures and desired outcomes are not established by this inventory.',{states_checked:checked,recorded_terminal_states:terminal,receipt_verification_performed:false});
99
+ add('PASS','Recorded terminal transaction outcomes counted; signed receipts are verified when they reconcile stale journal phases.',{
100
+ states_checked:checked,
101
+ recorded_terminal_states:terminal,
102
+ verified_terminal_receipts:verifiedTerminalReceipts,
103
+ verified_success_receipts:verifiedSuccessReceipts,
104
+ verified_failure_receipts:verifiedFailureReceipts,
105
+ unresolved_transactions:unresolved,
106
+ inventory_complete:inventoryComplete,
107
+ max_entries:maxEntries,
108
+ receipt_verification_performed:true
109
+ });
39
110
  return {status:findings.some(f=>f.level==='FAIL')?'FAIL':findings.some(f=>f.level==='WARN')?'WARN':'PASS',findings};
40
111
  }
@@ -275,6 +275,8 @@ function toolFilterMatch(pattern,tool){
275
275
  export function createLiveTimelineFilter(expression=''){
276
276
  const spec=parseFilterExpression(expression);
277
277
  const pending=new Map();
278
+ const MAX_FILTER_IDENTITIES=4096;
279
+ let evicted=0;
278
280
  const baseMatch=meta=>(!spec.tool||toolFilterMatch(spec.tool,meta.tool))&&(!spec.category||spec.category===meta.category)&&(!spec.trace||spec.trace===meta.trace_id);
279
281
  return Object.freeze({
280
282
  accept(event){
@@ -284,6 +286,8 @@ export function createLiveTimelineFilter(expression=''){
284
286
  if(event.event_type==='tool.request'){
285
287
  const args=event.args??{};const tool=displayToolName(rawTool,args);const trace=traceIdentity(event,args);
286
288
  const meta={tool,category:categoryForTool(tool),trace_id:trace.trace_id};
289
+ if(pending.has(requestId))pending.delete(requestId);
290
+ if(pending.size>=MAX_FILTER_IDENTITIES){pending.delete(pending.keys().next().value);evicted++;}
287
291
  pending.set(requestId,meta);
288
292
  return spec.status?false:baseMatch(meta);
289
293
  }
@@ -294,7 +298,8 @@ export function createLiveTimelineFilter(expression=''){
294
298
  }
295
299
  return false;
296
300
  },
297
- spec
301
+ spec,
302
+ retention(){return Object.freeze({pending:pending.size,evicted});}
298
303
  });
299
304
  }
300
305
  export function createLiveTimelineWriter({stream=process.stdout,displayMode='unicode',rewrite=true}={}){
@@ -330,10 +335,12 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
330
335
  if(!Number.isInteger(slowCallMs)||slowCallMs<100||slowCallMs>600000) throw new Error('live timeline slowCallMs invalid');
331
336
  const pending=new Map();
332
337
  const completed=new Set();
338
+ const MAX_PRESENTATION_IDENTITIES=4096;
333
339
  const toolCounts=new Map();
334
340
  const durationSamples=[];
335
341
  let resultCount=0,okCount=0,errorCount=0,unknownCount=0,mutationCount=0,totalDurationMs=0,slowest=null,burst=null,foldedHeartbeatLines=0,peakInFlight=0;
336
342
  let lastCapability=null,suppressedCapability=0,suppressedCapabilityReported=false;
343
+ let lastSourceSequence=0,pendingOverflow=0;
337
344
 
338
345
  function eventPrefix(event,sequence,{displayTool=null,icon=null}={}){
339
346
  const seq=Number.isSafeInteger(sequence)&&sequence>0?'#'+sequence+' ':'';
@@ -362,10 +369,12 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
362
369
  error?(displayMode==='ascii'?'ERR':'✗'):
363
370
  ['write','edit','run'].includes(category)?(displayMode==='ascii'?'SUCCEEDED':'✅ SUCCEEDED'):
364
371
  (displayMode==='ascii'?'OK':'✓');
372
+ const rawStatus=safeAtom(event.outcome?.status);
373
+ const statusDetail=rawStatus&&!['ok','error'].includes(rawStatus)?' status='+rawStatus:'';
365
374
  const prefixIcon=unknown?(displayMode==='ascii'?'WARN':'⚠️'):null;
366
375
  const evidence=receipt?' '+(displayMode==='ascii'?'RECEIPT':'🧾 receipt')+(displayMode==='ascii'?'=':'=')+receipt:'';
367
376
  const reasonSep=displayMode==='ascii'?' - ':' — ';
368
- return remoteDisplayText(eventPrefix(event,sequence,{displayTool,icon:prefixIcon})+traceSuffix(span?.trace_meta)+(summary?' '+summary:'')+(duration?' '+duration:'')+slow+' '+marker+evidence+(reason?reasonSep+reason:''),displayMode);
377
+ return remoteDisplayText(eventPrefix(event,sequence,{displayTool,icon:prefixIcon})+traceSuffix(span?.trace_meta)+(summary?' '+summary:'')+(duration?' '+duration:'')+slow+' '+marker+statusDetail+evidence+(reason?reasonSep+reason:''),displayMode);
369
378
  }
370
379
  function burstLine(value){
371
380
  if(!value) return null;
@@ -386,7 +395,8 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
386
395
  function addStats(event,span){
387
396
  resultCount++;
388
397
  const displayTool=span?.display_tool||event.tool;
389
- toolCounts.set(displayTool,(toolCounts.get(displayTool)??0)+1);
398
+ const countKey=toolCounts.has(displayTool)||toolCounts.size<256?displayTool:'[other]';
399
+ toolCounts.set(countKey,(toolCounts.get(countKey)??0)+1);
390
400
  const outcome=outcomeClass(event);
391
401
  if(outcome==='unknown') unknownCount++;else if(outcome==='error') errorCount++;else okCount++;
392
402
  if(['write','edit','run'].includes(categoryForTool(displayTool))) mutationCount++;
@@ -397,8 +407,12 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
397
407
  if(!slowest||duration>slowest.duration_ms) slowest={tool:displayTool,duration_ms:duration};
398
408
  }
399
409
  }
400
- function observe(event,{sequence=null}={}){
410
+ function observe(event,{sequence=null,sourceSequence=null}={}){
401
411
  if(!event||typeof event!=='object'||Array.isArray(event)) return null;
412
+ if(Number.isSafeInteger(sourceSequence)&&sourceSequence>0){
413
+ if(sourceSequence<=lastSourceSequence)return null;
414
+ lastSourceSequence=sourceSequence;
415
+ }
402
416
  const requestId=typeof event.request_id==='string'&&event.request_id?event.request_id:null;
403
417
  const tool=typeof event.tool==='string'&&event.tool?event.tool:null;
404
418
  if(event.event_type==='tool.request'&&requestId&&tool){
@@ -407,10 +421,17 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
407
421
  const displayTool=displayToolName(tool,args);
408
422
  const summary=summarizeLiveToolArgs(displayTool,args,{rootPaths});
409
423
  const traceMeta=traceIdentity(event,args);
424
+ if(pending.size>=MAX_PRESENTATION_IDENTITIES){
425
+ pendingOverflow++;
426
+ return pendingOverflow===1?remoteDisplayText('LIVELOG pending span cap reached; full audit remains on disk',displayMode):null;
427
+ }
410
428
  pending.set(requestId,{request_id:requestId,tool,display_tool:displayTool,summary,trace_meta:traceMeta,started_at_ms:clock()});
411
429
  peakInFlight=Math.max(peakInFlight,pending.size);
412
430
  if(verbose) return formatRemoteToolCall({...event,tool:displayTool},{clock,displayMode})+traceSuffix(traceMeta);
413
- if(visibleStart(displayTool)) return eventPrefix(event,sequence,{displayTool})+traceSuffix(traceMeta)+(summary?' '+summary:'')+' …';
431
+ if(visibleStart(displayTool)){
432
+ const startMarker=displayMode==='ascii'?'START':'▶ START';
433
+ return remoteDisplayText(eventPrefix(event,sequence,{displayTool})+traceSuffix(traceMeta)+' '+startMarker+(summary?' '+summary:''),displayMode);
434
+ }
414
435
  return null;
415
436
  }
416
437
  if(event.event_type==='tool.result'&&requestId&&tool){
@@ -418,6 +439,7 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
418
439
  const span=pending.get(requestId)??null;
419
440
  pending.delete(requestId);
420
441
  completed.add(requestId);
442
+ if(completed.size>MAX_PRESENTATION_IDENTITIES)completed.delete(completed.values().next().value);
421
443
  addStats(event,span);
422
444
  const error=event.outcome?.is_error===true;
423
445
  const nowMs=eventMs(event,clock);
@@ -487,6 +509,7 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
487
509
  }
488
510
  return Object.freeze({
489
511
  observe,
512
+ retention(){return Object.freeze({pending:pending.size,completed:completed.size,pending_overflow:pendingOverflow,last_source_sequence:lastSourceSequence});},
490
513
  pending(){return [...pending.values()].map(({request_id,tool})=>({request_id,tool}));},
491
514
  reconcileInterrupted,
492
515
  summary
@@ -26,6 +26,7 @@ const UPDATE_SCHEMA='deadbyte.r27-slot-update-receipt.v1';
26
26
  const HANDOFF_SCHEMA='deadbyte.r27-slot-update-handoff.v1';
27
27
  const FAILURE_SCHEMA='deadbyte.r27-slot-update-failure.v1';
28
28
  const ROLLBACK_SCHEMA='deadbyte.r31-slot-rollback-receipt.v1';
29
+ const ROLLBACK_FAILURE_SCHEMA='deadbyte.r31-slot-rollback-failure.v1';
29
30
  const DEFAULT_STATE_ROOT=undefined;
30
31
  const TOP_LAUNCHER=path.join(os.homedir(),'Downloads','DEADBYTE-MCP.cmd');
31
32
  const RUNTIME_STABILITY_MS=12000;
@@ -394,6 +395,18 @@ export async function prepareBridgeSpec(config,bootstrap){
394
395
  return next;
395
396
  }
396
397
 
398
+ export async function prepareReleaseBridgeSpec(config,bootstrap,releaseRoot){
399
+ if(!bootstrap) return structuredClone(config);
400
+ if(typeof releaseRoot!=='string'||!path.isAbsolute(releaseRoot))throw new Error('immutable release root required for Bridge binding');
401
+ // Verify the requested development input, then bind runtime paths only to
402
+ // the manifest-verified immutable slot produced from that same candidate.
403
+ const verifiedSource=await prepareBridgeSpec(config,bootstrap);
404
+ return prepareBridgeSpec(verifiedSource,{
405
+ root:path.join(path.resolve(releaseRoot),'bridge'),
406
+ scriptSha256:verifiedSource.Bridge.ScriptSha256
407
+ });
408
+ }
409
+
397
410
  export async function prepareAutonomousSupervisorSpec(config,{releaseRoot,enable=false}={}){
398
411
  const next=structuredClone(config);
399
412
  if(!enable) return next;
@@ -1682,7 +1695,7 @@ export async function performSlotUpdate({stage,developmentRoot,expectedVersion,e
1682
1695
  if(!control.desktopPolicy) throw new Error('DesktopRuntimeEnabled requires pinned desktop policy bootstrap or existing policy');
1683
1696
  control.config.DesktopRuntimeEnabled=true;
1684
1697
  }
1685
- control.config=await prepareBridgeSpec(control.config,bridgeBootstrap);
1698
+ control.config=await prepareReleaseBridgeSpec(control.config,bridgeBootstrap,slot.release_root);
1686
1699
  control.config=await prepareNamedTunnelCloudSpec(control.config,{releaseRoot:slot.release_root,bootstrap:cloudTransport});
1687
1700
  control.config=await prepareAutonomousSupervisorSpec(control.config,{releaseRoot:slot.release_root,enable:enableAutonomousSupervisor});
1688
1701
  authorityPreparation=await prepareAuthorityPolicies({hostPolicy:control.hostPolicy,codingPolicy:control.codingPolicy},{commandPins:hostCommandPins});
@@ -1750,7 +1763,7 @@ export async function performSlotRollback({activationReceipt,stateRoot=DEFAULT_S
1750
1763
  const currentCtx=slotContext(state,pointerInfo.pointer),currentRuntime=await captureRuntime(currentCtx);
1751
1764
  const revoked=await revokeAuthority(currentCtx,currentRuntime.status);
1752
1765
  const rollbackId=`r31-rollback-${activationEnvelope.receipt_sha256.slice(0,16)}-${Date.now().toString(36)}-${randomBytes(4).toString('hex')}`;
1753
- const evidenceDir=path.join(state,'evidence','updates',rollbackId),statePath=path.join(evidenceDir,'state.json'),receiptPath=path.join(evidenceDir,'receipt.json');
1766
+ const evidenceDir=path.join(state,'evidence','updates',rollbackId),statePath=path.join(evidenceDir,'state.json'),receiptPath=path.join(evidenceDir,'receipt.json'),failurePath=path.join(evidenceDir,'failure-receipt.json');
1754
1767
  await mkdir(evidenceDir,{recursive:true});
1755
1768
  const journal={schema:'deadbyte.r31-slot-rollback-state.v1',rollback_id:rollbackId,activation_receipt:path.resolve(receiptFile),
1756
1769
  activation_receipt_sha256:activationEnvelope.receipt_sha256,handoff_path:handoffPath,
@@ -1795,8 +1808,19 @@ export async function performSlotRollback({activationReceipt,stateRoot=DEFAULT_S
1795
1808
  }
1796
1809
  try { await startAndVerify(currentCtx,currentRuntime); recovery.successor_runtime_restarted=true; } catch(recoveryError){ recovery.errors.push(`restart-successor:${recoveryError.message}`); }
1797
1810
  }
1798
- await writeState(statePath,journal,'failed',{error:{name:error?.name??'Error',message:String(error?.message??error)},recovery}).catch(()=>{});
1799
- const wrapped=new Error(`R31 slot rollback failed: ${error?.message??error}`); wrapped.cause=error; wrapped.statePath=statePath; throw wrapped;
1811
+ const failure={schema:ROLLBACK_FAILURE_SCHEMA,rollback_id:rollbackId,activation_receipt_sha256:activationEnvelope.receipt_sha256,
1812
+ failed_at_utc:new Date().toISOString(),phase:journal.phase??'unknown',from_pointer:publicPointer(activePointer),to_pointer:publicPointer(priorPointer),
1813
+ error:{name:error?.name??'Error',message:String(error?.message??error)},recovery};
1814
+ await writeState(statePath,journal,'failed',{error:failure.error,recovery}).catch(()=>{});
1815
+ let failureReceipt=null;
1816
+ try{
1817
+ failureReceipt=await writeEnvelope(failurePath,failure,key);
1818
+ await writeState(statePath,journal,'failed_receipted',{failure_receipt_path:failurePath,failure_receipt_sha256:failureReceipt.receipt_sha256});
1819
+ }catch(receiptError){
1820
+ recovery.errors.push('failure-receipt:'+receiptError.message);
1821
+ await writeState(statePath,journal,'failed_unreceipted',{recovery,failure_receipt_error:String(receiptError?.message??receiptError)}).catch(()=>{});
1822
+ }
1823
+ const wrapped=new Error(`R31 slot rollback failed: ${error?.message??error}`); wrapped.cause=error; wrapped.statePath=statePath; wrapped.failureReceiptPath=failureReceipt?failurePath:null; throw wrapped;
1800
1824
  }
1801
1825
  }
1802
1826
 
package/src/version.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const DEADBYTE_VERSION = '0.15.37';
1
+ export const DEADBYTE_VERSION = '0.15.39';
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';