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.
- package/MANIFEST.SHA256 +81 -59
- package/README.txt +13 -14
- package/bin/WORKER-REGISTRY.txt +3 -3
- package/bin/appcontainer-stage.exe +0 -0
- package/bin/appcontainer-stage.obj +0 -0
- package/bin/bootstrap-advapi32.exe +0 -0
- package/bin/bootstrap-advapi32.obj +0 -0
- package/bin/bootstrap-exitcode.exe +0 -0
- package/bin/bootstrap-exitcode.obj +0 -0
- package/bin/bootstrap-kernel32.exe +0 -0
- package/bin/bootstrap-kernel32.obj +0 -0
- package/bin/child-control-probe.exe +0 -0
- package/bin/child-control-probe.obj +0 -0
- package/bin/child-control-stage.exe +0 -0
- package/bin/child-control-stage.obj +0 -0
- package/bin/contained-reverse-worker.exe +0 -0
- package/bin/contained-reverse-worker.obj +0 -0
- package/bin/contained-transform-worker.exe +0 -0
- package/bin/contained-transform-worker.obj +0 -0
- package/bin/containment-probe.exe +0 -0
- package/bin/containment-probe.obj +0 -0
- package/bin/deadbyte-contain.exe +0 -0
- package/bin/deadbyte-contain.obj +0 -0
- package/bin/deadbyte-exec.exe +0 -0
- package/bin/deadbyte-exec.obj +0 -0
- package/bin/deadbyte-process-host.exe +0 -0
- package/bin/deadbyte-process-host.obj +0 -0
- package/bin/deadbyte-tunnel-host.exe +0 -0
- package/bin/deadbyte-tunnel-host.obj +0 -0
- package/controller/README.TXT +3 -3
- package/controller/deadbyte-controller.ps1 +1 -1
- package/controller/deadbyte-process-policy.json +1 -1
- package/docs/ARCHITECTURE.md +1 -1
- package/package.json +1 -1
- package/scripts/build-containment.ps1 +15 -15
- package/scripts/deferred-slot-operation-r37-0110.mjs +124 -0
- package/scripts/deferred-slot-operation.mjs +1 -1
- package/scripts/final-closure-r36.mjs +113 -0
- package/scripts/final-closure-r37-0110.mjs +113 -0
- package/scripts/final-closure-verify-r36.mjs +208 -0
- package/scripts/final-closure-verify-r37-0110.mjs +208 -0
- package/scripts/final-closure-verify.mjs +48 -48
- package/scripts/final-closure.mjs +13 -13
- package/scripts/gate-windows-r36.ps1 +42 -0
- package/scripts/gate-windows-r37-0110.ps1 +42 -0
- package/scripts/gate-windows.ps1 +3 -3
- package/scripts/release-parity-r36.mjs +200 -0
- package/scripts/release-parity-r37-0110.mjs +200 -0
- package/scripts/release-parity-tests.ps1 +2 -2
- package/scripts/release-parity.mjs +37 -37
- package/scripts/windows-gate-evidence-r36.mjs +52 -0
- package/scripts/windows-gate-evidence-r37-0110.mjs +52 -0
- package/scripts/windows-gate-evidence.mjs +5 -5
- package/src/agent-memory-plane.mjs +98 -0
- package/src/autonomous-output-contracts.mjs +5 -2
- package/src/autonomous-runtime.mjs +155 -31
- package/src/autonomous-timeout-recovery.mjs +27 -0
- package/src/deadbyte-cli.mjs +90 -94
- package/src/effect-ledger.mjs +47 -0
- package/src/observation-lease.mjs +32 -0
- package/src/remote-live-log.mjs +81 -0
- package/src/remote-mcp-autonomous-smoke-client.mjs +15 -9
- package/src/remote-result-journal.mjs +23 -4
- package/src/version.mjs +1 -1
- package/src/worker-scheduler.mjs +53 -0
- package/test/autonomous-output-contract.test.mjs +1 -1
- package/test/cli-entrypoint.test.mjs +9 -0
- package/test/containment.test.mjs +18 -0
- package/test/core.test.mjs +1 -1
- package/test/production-docs.test.mjs +5 -5
- package/test/r33-closeout-regression.test.mjs +4 -4
- package/test/r33-finalization.test.mjs +6 -6
- package/test/r36-finalization.test.mjs +5 -5
- package/test/r36-release-identity.test.mjs +12 -19
- package/test/r37-0110-history.test.mjs +20 -0
- package/test/r37-agent-fabric.test.mjs +92 -0
- package/test/r37-autonomous-timeout-recovery.test.mjs +60 -0
- package/test/r37-finalization.test.mjs +74 -0
- package/test/r37-remote-tool-call-timeline.test.mjs +94 -0
- package/test/release-version.test.mjs +35 -36
- package/test/remote-live-log-r37.test.mjs +50 -0
- package/test/remote-session-cli.test.mjs +86 -68
package/src/deadbyte-cli.mjs
CHANGED
|
@@ -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 {
|
|
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
|
|
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 #
|
|
38
|
-
' npx deadbyte-mcp@latest remote --
|
|
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
|
|
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(
|
|
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,{
|
|
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
|
-
|
|
87
|
-
|
|
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
|
|
104
|
+
return formatRuntimeDiagnosticLine(file,line);
|
|
110
105
|
}
|
|
111
106
|
function remoteResultJournalPath(root,sessionId){return path.join(root,'runtime','remote-results',`${sessionId}.json`);}
|
|
112
|
-
function
|
|
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(
|
|
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
|
-
|
|
118
|
-
|
|
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
|
|
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(
|
|
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,{
|
|
155
|
-
process.stdout.write(
|
|
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
|
|
168
|
+
const timeline=auditTimelineRecord(file,line);
|
|
166
169
|
let recorded=null;
|
|
167
|
-
if(
|
|
168
|
-
|
|
169
|
-
|
|
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(
|
|
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
|
|
183
|
+
async function cleanupRemoteSession(ctx){
|
|
179
184
|
for(const command of SESSION_CLEANUP){
|
|
180
185
|
try{
|
|
181
|
-
const result=invokeController(ctx,command,
|
|
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'&¤t.owner_pid===marker.owner_pid&¤t.session_nonce===marker.session_nonce;
|
|
260
263
|
if(same) await rm(remoteSessionMarker(ctx.root),{force:true});
|
|
261
264
|
}
|
|
262
|
-
async function runRemoteSession({
|
|
265
|
+
async function runRemoteSession({resumeCursor=null}={}){
|
|
263
266
|
const ctx=await remoteContext();
|
|
264
|
-
if(resumeCursor!==null) await replayRemoteResults(ctx.root,resumeCursor
|
|
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=
|
|
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
|
-
|
|
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,{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
325
|
-
await cleanupRemoteSession(ctx
|
|
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
|
|
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
|
+
}
|
package/src/remote-live-log.mjs
CHANGED
|
@@ -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
|
-
|
|
36
|
-
if (
|
|
36
|
+
const disposition=classifyAutonomousTimeoutRecovery(status,{beforeJournalHead});
|
|
37
|
+
if (disposition.kind === 'completed') return { kind:'completed', status };
|
|
38
|
+
if (disposition.kind === 'terminal_failure') {
|
|
37
39
|
throw new Error(`autonomous goal became terminal after request timeout: ${JSON.stringify(status)}`);
|
|
38
40
|
}
|
|
39
|
-
if (
|
|
40
|
-
throw new Error(`autonomous goal returned unexpected recovery state: ${JSON.stringify(status)}`);
|
|
41
|
-
}
|
|
41
|
+
if (disposition.kind === 'resumable') return { kind:'resumable', status };
|
|
42
42
|
await sleep(AUTONOMOUS_TIMEOUT_RECOVERY_POLL_MS);
|
|
43
43
|
}
|
|
44
|
-
throw new Error('autonomous goal did not reach a durable terminal
|
|
44
|
+
throw new Error('autonomous goal did not reach a durable terminal or resumable boundary after request timeout');
|
|
45
45
|
}
|
|
46
46
|
function messageText(content) {
|
|
47
47
|
if (typeof content === 'string') return content;
|
|
@@ -137,6 +137,10 @@ try {
|
|
|
137
137
|
let ran = null;
|
|
138
138
|
let recoveredRequestTimeout = false;
|
|
139
139
|
for (let requestRound = 0; requestRound < AUTONOMOUS_RUN_MAX_REQUESTS; requestRound += 1) {
|
|
140
|
+
const beforeRun=structured(await client.callTool(
|
|
141
|
+
{ name:'autonomous_goal_status', arguments:{ goal_id:goalId } },
|
|
142
|
+
{ timeout:30000, maxTotalTimeout:30000 }
|
|
143
|
+
), 'autonomous_goal_status');
|
|
140
144
|
try {
|
|
141
145
|
ran = structured(await client.callTool(
|
|
142
146
|
{ name:'autonomous_goal_run', arguments:{ goal_id:goalId, max_cycles:AUTONOMOUS_RUN_MAX_CYCLES_PER_REQUEST } },
|
|
@@ -144,8 +148,10 @@ try {
|
|
|
144
148
|
), 'autonomous_goal_run');
|
|
145
149
|
} catch (error) {
|
|
146
150
|
if (String(error?.code ?? '') !== 'REQUEST_TIMEOUT') throw error;
|
|
147
|
-
const recovered = await recoverTimedOutGoal(client, goalId);
|
|
148
|
-
ran =
|
|
151
|
+
const recovered = await recoverTimedOutGoal(client, goalId, beforeRun.journal_head_sha256);
|
|
152
|
+
ran = recovered.kind === 'completed'
|
|
153
|
+
? { status:'completed', state:recovered.status.state, goal_id:goalId, recovered_from_timeout:true }
|
|
154
|
+
: { status:'yielded', state:recovered.status.state, goal_id:goalId, recovered_from_timeout:true };
|
|
149
155
|
recoveredRequestTimeout = true;
|
|
150
156
|
}
|
|
151
157
|
if (ran.status === 'completed' && ran.state === 'succeeded') break;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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