deadbyte-mcp 0.15.2 → 0.15.3

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 (56) hide show
  1. package/MANIFEST.SHA256 +54 -33
  2. package/package.json +88 -88
  3. package/scripts/autonomous-build.ps1 +1 -0
  4. package/scripts/autonomous-tests.ps1 +1 -0
  5. package/scripts/check-agent-backend-r46.mjs +77 -0
  6. package/scripts/check-atomic-resource-r46.mjs +84 -0
  7. package/scripts/check-autonomous-race-r46.mjs +160 -0
  8. package/scripts/check-coding-task-deadline-r46.mjs +59 -0
  9. package/scripts/check-deadline-cancellation-r46.mjs +49 -0
  10. package/scripts/check-process-deadline-r46.mjs +52 -0
  11. package/scripts/check-r46-background.mjs +5 -0
  12. package/scripts/extract-r46-failures.mjs +5 -0
  13. package/scripts/full-regression-r46.mjs +20 -12
  14. package/scripts/gate-windows-r46.ps1 +2 -0
  15. package/scripts/inspect-r46-active.mjs +2 -0
  16. package/scripts/r47-core-failures.mjs +1 -0
  17. package/scripts/release-manifest.mjs +1 -1
  18. package/scripts/run-r46-full-regression-background.mjs +7 -0
  19. package/scripts/run-r46-windows-gate-background.mjs +6 -0
  20. package/scripts/run-r46-windows-gate-background.ps1 +10 -0
  21. package/scripts/windows-gate-evidence-r46.mjs +4 -3
  22. package/scripts/workload-contract-regression-r47.mjs +14 -0
  23. package/scripts/workload-stream-regression.mjs +16 -0
  24. package/scripts/workload-substrate-r47-regression.mjs +12 -0
  25. package/src/agent-backend-r44.mjs +75 -10
  26. package/src/atomic-mutation.mjs +55 -0
  27. package/src/autonomous-mcp-tools.mjs +4 -3
  28. package/src/autonomous-output-contracts.mjs +16 -3
  29. package/src/autonomous-runtime.mjs +83 -30
  30. package/src/coding-mcp-tools.mjs +2 -2
  31. package/src/coding-output-contracts.mjs +18 -4
  32. package/src/coding-plane.mjs +3 -24
  33. package/src/coding-search.mjs +11 -2
  34. package/src/coding-task-runtime.mjs +41 -6
  35. package/src/deadline-cancellation.mjs +83 -0
  36. package/src/machine-fs-smoke-client.mjs +1 -1
  37. package/src/machine-fs.mjs +3 -23
  38. package/src/mcp-server.mjs +6 -0
  39. package/src/observation-smoke-client.mjs +1 -1
  40. package/src/process-mcp-tools.mjs +11 -3
  41. package/src/process-output-contracts.mjs +23 -0
  42. package/src/process-runtime.mjs +54 -5
  43. package/src/process-smoke-client.mjs +1 -1
  44. package/src/resource-identity.mjs +59 -0
  45. package/src/runtime-update.mjs +526 -162
  46. package/src/workload-substrate.mjs +87 -0
  47. package/test/coding-runtime.test.mjs +12 -0
  48. package/test/coding-search-r36.test.mjs +7 -0
  49. package/test/full-max-catalog-r46.test.mjs +2 -2
  50. package/test/helpers/autonomous-r34-fixture.mjs +10 -1
  51. package/test/process-mcp-tools.test.mjs +5 -2
  52. package/test/process-runtime.test.mjs +18 -1
  53. package/test/r45-finalization.test.mjs +1 -1
  54. package/test/release-version.test.mjs +1 -1
  55. package/test/runtime-self-update.test.mjs +118 -1
  56. package/test/workload-substrate.test.mjs +22 -0
@@ -8,9 +8,19 @@ const hex64 = z.string().regex(HEX64_RE);
8
8
  const receiptId = z.string().regex(HEX24_RE);
9
9
  const taskId = z.string().regex(TASK_ID_RE);
10
10
  const stepId = z.string().regex(STEP_ID_RE);
11
+ const streamCoverage = z.strictObject({
12
+ logical_first_seq:z.number().int().min(1), logical_last_seq:z.number().int().min(0),
13
+ retained_first_seq:z.number().int().min(1), retained_last_seq:z.number().int().min(0),
14
+ complete:z.boolean(), truncated:z.boolean(),
15
+ missing_ranges:z.array(z.strictObject({from:z.number().int().min(1),to:z.number().int().min(0)})).max(100000),
16
+ logical_item_count:z.number().int().min(0), retained_item_count:z.number().int().min(0),
17
+ logical_bytes:z.number().int().min(0), retained_bytes:z.number().int().min(0),
18
+ dropped_items:z.number().int().min(0), dropped_bytes:z.number().int().min(0), status:z.enum(['open','closed'])
19
+ });
11
20
  const jsonObject = z.record(z.string(), z.json());
12
21
  const taskState = z.enum(['pending', 'running', 'paused', 'failed', 'cancelled', 'succeeded']);
13
22
  const codingKind = z.enum(['checkpoint', 'write', 'replace', 'command', 'benchmark']);
23
+ const deadline = z.strictObject({ schema:z.literal('deadbyte.deadline.v1'), id:z.string().nullable(), created_at_ms:z.number(), timeout_ms:z.number().int().min(1).nullable(), deadline_ms:z.number().nullable() });
14
24
 
15
25
  function requireFields(value, ctx, fields) {
16
26
  for (const field of fields) if (value[field] === undefined) ctx.addIssue({ code: 'custom', path: [field], message: `${field} is required` });
@@ -87,7 +97,9 @@ export const codingSearchOutputSchema = statusContract({
87
97
  binary_files_skipped: z.number().int().min(0).optional(), truncated: z.boolean().optional(),
88
98
  truncation_reason: z.enum(['tree_limit','scan_byte_limit','max_results']).nullable().optional(),
89
99
  query_sha256: hex64.optional(), snapshot_sha256: hex64.optional(), page_sha256: hex64.optional(),
90
- next_cursor: z.string().max(8192).nullable().optional() },
100
+ next_cursor: z.string().max(8192).nullable().optional(),
101
+ stream_id: hex64.optional(), stream_generation: z.number().int().min(1).optional(), stream_next_seq: z.number().int().min(1).optional(),
102
+ stream_coverage: streamCoverage.optional() },
91
103
  successStatuses: ['ok'], successRequired: ['root_id','path','pattern','target','mode','case_sensitive','matches','files_scanned','bytes_scanned','binary_files_skipped','truncated','truncation_reason','query_sha256','snapshot_sha256','page_sha256','next_cursor']
92
104
  });
93
105
  const semanticLocationSchema = z.strictObject({
@@ -222,7 +234,7 @@ export const codingTaskSubmitOutputSchema = statusContract({
222
234
  successStatuses: ['accepted', 'existing'], successRequired: ['task_id', 'plan_sha256', 'state']
223
235
  });
224
236
  export const codingTaskRunOutputSchema = z.strictObject({
225
- status: z.enum(['terminal', 'paused', 'completed', 'cancelled', 'failed', 'yielded', 'succeeded']), error: z.string().optional(), task_id: taskId.optional(),
237
+ status: z.enum(['terminal', 'paused', 'completed', 'cancelled', 'failed', 'yielded', 'succeeded']), error: z.string().optional(), task_id: taskId.optional(), deadline: deadline.optional(),
226
238
  ...Object.fromEntries(Object.entries(derivedShape).map(([key, schema]) => [key, schema.optional()]))
227
239
  }).superRefine((value, ctx) => {
228
240
  if (value.error !== undefined) { if (value.status !== 'failed') ctx.addIssue({ code: 'custom', path: ['error'], message: 'error wrapper requires status=failed' }); return; }
@@ -231,13 +243,15 @@ export const codingTaskRunOutputSchema = z.strictObject({
231
243
  export const codingTaskStatusOutputSchema = statusContract({
232
244
  statuses: ['ok', 'failed'],
233
245
  shape: { task_id: taskId.optional(), plan_sha256: hex64.optional(),
234
- ...Object.fromEntries(Object.entries(derivedShape).map(([key, schema]) => [key, schema.optional()])), journal_verified: z.literal(true).optional() },
246
+ ...Object.fromEntries(Object.entries(derivedShape).map(([key, schema]) => [key, schema.optional()])), journal_verified: z.literal(true).optional(),
247
+ stream_id: taskId.optional(), stream_generation: z.number().int().min(1).optional(), stream_next_seq: z.number().int().min(1).optional(), stream_coverage: streamCoverage.optional() },
235
248
  successStatuses: ['ok'], successRequired: ['task_id', 'plan_sha256', ...Object.keys(derivedShape), 'journal_verified']
236
249
  });
237
250
  export const codingTaskEventsOutputSchema = statusContract({
238
251
  statuses: ['ok', 'failed'],
239
252
  shape: { task_id: taskId.optional(), plan_sha256: hex64.optional(), journal_verified: z.literal(true).optional(),
240
- journal_head_sha256: hex64.optional(), events: z.array(eventSchema).max(200).optional() },
253
+ journal_head_sha256: hex64.optional(), events: z.array(eventSchema).max(200).optional(),
254
+ stream_id: taskId.optional(), stream_generation: z.number().int().min(1).optional(), stream_next_seq: z.number().int().min(1).optional(), stream_coverage: streamCoverage.optional() },
241
255
  successStatuses: ['ok'], successRequired: ['task_id', 'plan_sha256', 'journal_verified', 'journal_head_sha256', 'events']
242
256
  });
243
257
  export const codingTaskCancelOutputSchema = statusContract({
@@ -10,6 +10,7 @@ import { isInside, validatePortableRelativePath } from './path-policy.mjs';
10
10
  import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
11
11
  import { createCodingSearch } from './coding-search.mjs';
12
12
  import { createSearchCursorCodec } from './search-cursor.mjs';
13
+ import { atomicMutation } from './atomic-mutation.mjs';
13
14
 
14
15
  export const CODING_POLICY_SCHEMA = 'deadbyte.coding-policy.v1';
15
16
  export const CODING_LEASE_SCHEMA = 'deadbyte.coding-lease.v1';
@@ -261,30 +262,8 @@ function decodeContent(content, encoding) {
261
262
  throw new Error('encoding must be utf8 or base64');
262
263
  }
263
264
  async function atomicWrite(target, bytes, expectedSha256, authorityCheck = null) {
264
- const before = await targetState(target);
265
- checkExpected(before, expectedSha256);
266
- const temp = path.join(path.dirname(target), `.${path.basename(target)}.deadbyte-coding-${process.pid}-${randomBytes(8).toString('hex')}.tmp`);
267
- let handle;
268
- try {
269
- if (authorityCheck) await authorityCheck();
270
- handle = await open(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);
271
- await handle.writeFile(bytes);
272
- await handle.sync();
273
- await handle.close();
274
- handle = null;
275
- const race = await targetState(target);
276
- checkExpected(race, expectedSha256);
277
- if (authorityCheck) await authorityCheck();
278
- await rename(temp, target);
279
- } catch (error) {
280
- if (handle) await handle.close().catch(() => {});
281
- await rm(temp, { force: true }).catch(() => {});
282
- throw error;
283
- }
284
- const after = await targetState(target);
285
- const expectedAfter = sha256Bytes(bytes);
286
- if (!after.exists || after.sha256 !== expectedAfter) throw new Error('coding write post-commit hash mismatch');
287
- return { before, after };
265
+ return atomicMutation({ target, bytes, expected: expectedSha256, readState: targetState,
266
+ assertExpected: checkExpected, authorityCheck, tempTag: 'deadbyte-coding' });
288
267
  }
289
268
  function codingEnvironment() {
290
269
  const allowed = new Set([
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { canonicalJson } from './canonical-json.mjs';
3
3
  import { decodeUtf8Strict } from './text-policy.mjs';
4
+ import { createSequenceStream } from './workload-substrate.mjs';
4
5
 
5
6
  const sha256Object=value=>createHash('sha256').update(canonicalJson(value),'utf8').digest('hex');
6
7
  const sha256Bytes=bytes=>createHash('sha256').update(bytes).digest('hex');
@@ -121,18 +122,26 @@ export function createCodingSearch({tree,fileRead,cursorCodec}){
121
122
 
122
123
  const snapshotSha=sha256Object({schema:'deadbyte.coding-search-snapshot.v2',query_sha256:querySha,entries:snapshotEntries});
123
124
  if(decodedCursor&&decodedCursor.snapshot_sha256!==snapshotSha) throw new Error('coding search cursor stale snapshot mismatch');
125
+ const stream=createSequenceStream({streamId:querySha,maxItems:Math.max(1,maxResults),maxBytes:Math.max(1024, maxResults*4096)});
126
+ for(const match of matches) stream.append(match,{producer:'coding-search',channel:'match'});
124
127
  const offset=decodedCursor?.offset??0;
125
128
  if(offset>matches.length) throw new Error('coding search cursor offset exceeds retained results');
126
- const pageMatches=matches.slice(offset,offset+pageSize);
129
+ const page=stream.read(offset+1,pageSize);
130
+ if(page.missing) throw new Error('coding search cursor offset is outside retained stream coverage');
131
+ const pageMatches=page.items.map(item=>item.value);
127
132
  const nextOffset=offset+pageMatches.length;
128
133
  const hasMore=nextOffset<matches.length;
129
134
  const nextCursor=hasMore?await cursorCodec.encode({query_sha256:querySha,snapshot_sha256:snapshotSha,offset:nextOffset}):null;
130
135
  const pageSha=sha256Object({schema:'deadbyte.coding-search-page.v2',query_sha256:querySha,snapshot_sha256:snapshotSha,offset,matches:pageMatches});
136
+ const coverage=stream.coverage();
137
+ const streamSnapshot=stream.snapshot();
131
138
  return {
132
139
  status:'ok',root_id:rootId,path,pattern,target,mode,case_sensitive:caseSensitive,
133
140
  matches:pageMatches,files_scanned:filesScanned,bytes_scanned:bytesScanned,binary_files_skipped:binaryFilesSkipped,
134
141
  truncated:truncated||hasMore,truncation_reason:truncationReason,query_sha256:querySha,snapshot_sha256:snapshotSha,
135
- page_sha256:pageSha,next_cursor:nextCursor
142
+ page_sha256:pageSha,next_cursor:nextCursor,
143
+ stream_id:stream.streamId,stream_generation:stream.generation,stream_next_seq:streamSnapshot.next_seq,
144
+ stream_coverage:coverage
136
145
  };
137
146
  };
138
147
  }
@@ -4,6 +4,8 @@ import path from 'node:path';
4
4
  import { canonicalJson } from './canonical-json.mjs';
5
5
  import { validatePortableRelativePath } from './path-policy.mjs';
6
6
  import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
7
+ import { createJournalStream } from './workload-substrate.mjs';
8
+ import { createDeadline, deadlineState } from './deadline-cancellation.mjs';
7
9
 
8
10
  export const CODING_TASK_PLAN_SCHEMA = 'deadbyte.coding-task-plan.v1';
9
11
  export const CODING_TASK_EVENT_SCHEMA = 'deadbyte.coding-task-event.v1';
@@ -525,7 +527,11 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
525
527
  const ctx = taskContext(policy, taskId);
526
528
  const plan = await loadPlan(ctx, policy, verifyKeyPath);
527
529
  const events = await readEvents(ctx, verifyKeyPath);
528
- return { status: 'ok', task_id: taskId, plan_sha256: plan.plan_sha256, ...deriveState(plan, events), journal_verified: true };
530
+ const stream = createJournalStream({streamId:taskId,events,maxItems:MAX_EVENTS,maxBytes:8*1024*1024,producer:'coding-task',channel:'event'});
531
+ const snapshot = stream.snapshot();
532
+ return { status: 'ok', task_id: taskId, plan_sha256: plan.plan_sha256, ...deriveState(plan, events), journal_verified: true,
533
+ stream_id:snapshot.stream_id, stream_generation:snapshot.generation, stream_next_seq:snapshot.next_seq,
534
+ stream_coverage:stream.coverage() };
529
535
  }
530
536
 
531
537
  async function events({ taskId, fromSeq = 1, limit = 100 }) {
@@ -534,10 +540,15 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
534
540
  const ctx = taskContext(policy, taskId);
535
541
  const plan = await loadPlan(ctx, policy, verifyKeyPath);
536
542
  const all = await readEvents(ctx, verifyKeyPath);
543
+ const stream = createJournalStream({streamId:taskId,events:all,maxItems:MAX_EVENTS,maxBytes:8*1024*1024,producer:'coding-task',channel:'event'});
544
+ const page = stream.read(fromSeq,limit);
545
+ const snapshot = stream.snapshot();
537
546
  return {
538
547
  status: 'ok', task_id: taskId, plan_sha256: plan.plan_sha256, journal_verified: true,
539
548
  journal_head_sha256: all.at(-1)?.event_sha256 ?? ZERO_HASH,
540
- events: all.slice(fromSeq - 1, fromSeq - 1 + limit)
549
+ stream_id: snapshot.stream_id, stream_generation: snapshot.generation, stream_next_seq: snapshot.next_seq,
550
+ stream_coverage: stream.coverage(),
551
+ events: page.items.map(item=>item.value)
541
552
  };
542
553
  }
543
554
 
@@ -590,8 +601,18 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
590
601
  const journal = await readEvents(ctx, verifyKeyPath);
591
602
  return verifyTaskReceipt(ctx, plan, journal, policy, verifyKeyPath, codingRuntime);
592
603
  }
593
- async function run({ taskId, maxSteps = 8 }) {
604
+ async function run({ taskId, maxSteps = 8, timeoutMs = null }) {
594
605
  if (!Number.isInteger(maxSteps) || maxSteps < 1 || maxSteps > MAX_STEPS) throw new Error(`max_steps must be in [1, ${MAX_STEPS}]`);
606
+ if (timeoutMs !== null && (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 900000)) throw new Error('timeout_ms must be null or in [100,900000]');
607
+ const deadline = createDeadline({ timeoutMs });
608
+ const deadlineExpired = () => timeoutMs !== null && deadlineState(deadline).expired;
609
+ const failDeadline = async (ctx, journal, plan, stepId = null) => {
610
+ await appendEvent(ctx, journal, signingKeyPath, 'task_failed', {
611
+ stepId,
612
+ payload: { reason: 'deadline_exceeded', deadline }
613
+ });
614
+ return deriveState(plan, journal);
615
+ };
595
616
  const session = await codingRuntime.sessionStatus();
596
617
  if (!session.armed) throw new Error(`coding plane is disarmed: ${session.reason}`);
597
618
  const ctx = taskContext(policy, taskId);
@@ -610,10 +631,14 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
610
631
  if (await consumeCancel(ctx, journal, signingKeyPath, verifyKeyPath)) {
611
632
  derived = deriveState(plan, journal); return { status: 'cancelled', task_id: taskId, ...derived };
612
633
  }
613
- await appendEvent(ctx, journal, signingKeyPath, 'run_started', { payload: { max_steps: maxSteps } });
634
+ await appendEvent(ctx, journal, signingKeyPath, 'run_started', { payload: { max_steps: maxSteps, deadline } });
614
635
  derived = deriveState(plan, journal);
615
636
  let consumed = 0;
616
637
  while (consumed < maxSteps) {
638
+ if (deadlineExpired()) {
639
+ derived = await failDeadline(ctx, journal, plan);
640
+ return { status: 'failed', task_id: taskId, ...derived, deadline };
641
+ }
617
642
  if (await consumeCancel(ctx, journal, signingKeyPath, verifyKeyPath)) {
618
643
  derived = deriveState(plan, journal); return { status: 'cancelled', task_id: taskId, ...derived };
619
644
  }
@@ -638,7 +663,9 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
638
663
  derived = deriveState(plan, journal);
639
664
  consumed += 1;
640
665
  try {
641
- const executed = await executeStep(codingRuntime, plan, next, attempt);
666
+ const remainingMs = timeoutMs === null ? null : Math.max(100, Math.floor(deadlineState(deadline).remaining_ms));
667
+ const boundedStep = remainingMs === null ? next : { ...next, timeout_ms: next.timeout_ms === null || next.timeout_ms === undefined ? remainingMs : Math.min(next.timeout_ms, remainingMs) };
668
+ const executed = await executeStep(codingRuntime, plan, boundedStep, attempt);
642
669
  if (!executed.outcome.valid) {
643
670
  await appendEvent(ctx, journal, signingKeyPath, 'task_paused', {
644
671
  stepId: next.step_id, payload: { reason: 'post_execute_receipt_mismatch', detail: executed.outcome.reason }
@@ -646,6 +673,10 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
646
673
  derived = deriveState(plan, journal);
647
674
  return { status: 'paused', task_id: taskId, ...derived };
648
675
  }
676
+ if (deadlineExpired()) {
677
+ derived = await failDeadline(ctx, journal, plan, next.step_id);
678
+ return { status: 'failed', task_id: taskId, ...derived, deadline };
679
+ }
649
680
  if (executed.outcome.success) {
650
681
  await appendEvent(ctx, journal, signingKeyPath, 'step_succeeded', {
651
682
  stepId: next.step_id, payload: { ...executed.outcome.payload, attempt, recovered: false }
@@ -664,6 +695,10 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
664
695
  }
665
696
  derived = deriveState(plan, journal);
666
697
  } catch (error) {
698
+ if (deadlineExpired()) {
699
+ derived = await failDeadline(ctx, journal, plan, next.step_id);
700
+ return { status: 'failed', task_id: taskId, ...derived, deadline };
701
+ }
667
702
  const checked = await codingRuntime.verifyReceiptDetailed(receiptId);
668
703
  if (checked.ok) {
669
704
  const outcome = receiptOutcome(next, checked, requestSha256);
@@ -717,7 +752,7 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
717
752
  }
718
753
  }
719
754
  }
720
- return { status: 'yielded', task_id: taskId, ...derived };
755
+ return { status: 'yielded', task_id: taskId, ...derived, deadline };
721
756
  } finally { await releaseLock(ctx); }
722
757
  }
723
758
 
@@ -0,0 +1,83 @@
1
+ const ID=/^[A-Za-z0-9._:-]{1,128}$/;
2
+
3
+ export function createDeadline({timeoutMs=null, nowMs=Date.now(), id=null}={}) {
4
+ if(timeoutMs!==null && (!Number.isInteger(timeoutMs)||timeoutMs<1)) throw new Error('deadline timeout_ms invalid');
5
+ if(!Number.isFinite(nowMs)) throw new Error('deadline now_ms invalid');
6
+ const deadlineMs=timeoutMs===null?null:nowMs+timeoutMs;
7
+ return Object.freeze({
8
+ schema:'deadbyte.deadline.v1',
9
+ id:id===null?null:validateId(id),
10
+ created_at_ms:nowMs,
11
+ timeout_ms:timeoutMs,
12
+ deadline_ms:deadlineMs
13
+ });
14
+ }
15
+
16
+ export function deadlineState(deadline,{nowMs=Date.now()}={}) {
17
+ if(!deadline||deadline.schema!=='deadbyte.deadline.v1') throw new Error('deadline invalid');
18
+ if(!Number.isFinite(nowMs)) throw new Error('deadline now_ms invalid');
19
+ if(deadline.deadline_ms===null) return Object.freeze({expired:false,remaining_ms:null});
20
+ const remainingMs=deadline.deadline_ms-nowMs;
21
+ return Object.freeze({expired:remainingMs<=0,remaining_ms:Math.max(0,remainingMs)});
22
+ }
23
+
24
+ export function createCancellationToken({id=null}={}) {
25
+ const controller=new AbortController();
26
+ let requestedAtMs=null;
27
+ let reason=null;
28
+ const request=(value='cancelled')=>{
29
+ if(controller.signal.aborted)return false;
30
+ requestedAtMs=Date.now();
31
+ reason=String(value);
32
+ controller.abort(reason);
33
+ return true;
34
+ };
35
+ return Object.freeze({
36
+ schema:'deadbyte.cancellation.v1',
37
+ id:id===null?null:validateId(id),
38
+ signal:controller.signal,
39
+ request,
40
+ snapshot:()=>Object.freeze({
41
+ requested:controller.signal.aborted,
42
+ requested_at_ms:requestedAtMs,
43
+ reason
44
+ })
45
+ });
46
+ }
47
+
48
+ export function composeAbortSignal({deadline=null,cancellation=null}={}) {
49
+ const controller=new AbortController();
50
+ let timeout=null;
51
+ const abort=(reason)=>{
52
+ if(!controller.signal.aborted)controller.abort(reason);
53
+ };
54
+ if(cancellation?.signal){
55
+ if(cancellation.signal.aborted)abort(cancellation.signal.reason??'cancelled');
56
+ else cancellation.signal.addEventListener('abort',()=>abort(cancellation.signal.reason??'cancelled'),{once:true});
57
+ }
58
+ if(deadline?.deadline_ms!==null&&deadline?.deadline_ms!==undefined){
59
+ const remaining=Math.max(0,deadline.deadline_ms-Date.now());
60
+ if(remaining===0)abort('deadline_exceeded');
61
+ else timeout=setTimeout(()=>abort('deadline_exceeded'),remaining);
62
+ timeout?.unref?.();
63
+ }
64
+ const close=()=>{if(timeout)clearTimeout(timeout);};
65
+ return Object.freeze({signal:controller.signal,close});
66
+ }
67
+
68
+ export function classifyTermination({deadlineExceeded=false,cancellationRequested=false,physicallyTerminated=false,verified=false}={}) {
69
+ return Object.freeze({
70
+ deadline_exceeded:deadlineExceeded===true,
71
+ cancellation_requested:cancellationRequested===true,
72
+ physical_termination:physicallyTerminated===true,
73
+ verified:verified===true,
74
+ terminal_semantics:
75
+ verified ? (physicallyTerminated ? 'verified_termination' : 'verified_stop')
76
+ : physicallyTerminated ? 'termination_unverified'
77
+ : cancellationRequested ? 'cancellation_requested'
78
+ : deadlineExceeded ? 'deadline_exceeded'
79
+ : 'running'
80
+ });
81
+ }
82
+
83
+ function validateId(id){if(typeof id!=='string'||!ID.test(id))throw new Error('deadline/cancellation id invalid');return id;}
@@ -56,7 +56,7 @@ const transport = new StdioClientTransport({
56
56
  });
57
57
  const stderrChunks = [];
58
58
  transport.stderr?.on('data', chunk => stderrChunks.push(Buffer.from(chunk)));
59
- const baseTools = ['contained_transform','evidence_verify','file_read','proof_execute','skill_catalog','workspace_open'];
59
+ const baseTools = ['contained_transform','evidence_verify','file_read','host_exec','host_file_read','host_file_write','proof_execute','skill_catalog','workspace_open'];
60
60
  const codingTools = [
61
61
  'coding_benchmark_run','coding_command_run','coding_file_read','coding_file_write','coding_files_read',
62
62
  'coding_receipt_verify','coding_search','coding_session_status','coding_stat',
@@ -9,6 +9,7 @@ import { assertCodingArmed, recheckCodingAuthority } from './coding-plane.mjs';
9
9
  import { isInside, validatePortableRelativePath } from './path-policy.mjs';
10
10
  import { decodeUtf8Strict } from './text-policy.mjs';
11
11
  import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
12
+ import { atomicMutation } from './atomic-mutation.mjs';
12
13
 
13
14
  export const MACHINE_FS_RECEIPT_SCHEMA = 'deadbyte.machine-fs-receipt.v1';
14
15
  const RECEIPT_DOMAIN = Buffer.from('DEADBYTE-MACHINE-FS-RECEIPT-V1\0', 'utf8');
@@ -271,29 +272,8 @@ function requestSha(operation, args) {
271
272
  }
272
273
 
273
274
  async function atomicWrite(target, bytes, expectedSha256, authorityCheck = null) {
274
- const before = await regularState(target);
275
- checkExpectedFile(before, expectedSha256);
276
- const temp = path.join(path.dirname(target), `.${path.basename(target)}.deadbyte-machine-fs-${process.pid}-${randomBytes(8).toString('hex')}.tmp`);
277
- let handle;
278
- try {
279
- if (authorityCheck) await authorityCheck();
280
- handle = await open(temp, 'wx', 0o600);
281
- await handle.writeFile(bytes);
282
- await handle.sync();
283
- await handle.close();
284
- handle = null;
285
- checkExpectedFile(await regularState(target), expectedSha256);
286
- if (authorityCheck) await authorityCheck();
287
- await rename(temp, target);
288
- } catch (error) {
289
- if (handle) await handle.close().catch(() => {});
290
- await rm(temp, { force: true }).catch(() => {});
291
- throw error;
292
- }
293
- const after = await regularState(target);
294
- const expectedAfter = sha256Bytes(bytes);
295
- if (!after.exists || after.sha256 !== expectedAfter) throw new Error('machine fs post-commit hash mismatch');
296
- return { before, after };
275
+ return atomicMutation({ target, bytes, expected: expectedSha256, readState: regularState,
276
+ assertExpected: checkExpectedFile, authorityCheck, tempTag: 'deadbyte-machine-fs' });
297
277
  }
298
278
 
299
279
  export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath, readWorkerPath = machineFsReadWorkerPath }) {
@@ -15,6 +15,7 @@ import { DEADBYTE_VERSION } from './version.mjs';
15
15
  import { currentIngressAdapterMode, TUNNEL_STDIO_PROBE_COMPAT_MODE } from './ingress-mode.mjs';
16
16
  import { installTunnelStdioProbeCompat } from './tunnel-stdio-compat.mjs';
17
17
  import { installStdioConsoleGuard } from './stdio-console-guard.mjs';
18
+ import { recoverInterruptedSlotUpdates } from './runtime-update.mjs';
18
19
  import { decodeUtf8Strict } from './text-policy.mjs';
19
20
  import { assertHostArmed, hostExec, hostExecInline, hostExecNative, hostFileRead, hostFileWrite, loadHostPolicy } from './host-plane.mjs';
20
21
  import { createMachineRuntime } from './machine-identity.mjs';
@@ -74,6 +75,11 @@ const verifyKeyPath = process.env.DEADBYTE_VERIFY_KEY ?? defaultPublicKeyPath();
74
75
  const auditRuntimeMode = process.env.DEADBYTE_AUDIT_RUNTIME ?? '0';
75
76
  if (!['0','1'].includes(auditRuntimeMode)) throw new Error('DEADBYTE_AUDIT_RUNTIME must be 0 or 1');
76
77
  const runtimeStateRoot = path.resolve(process.env.DEADBYTE_STATE_ROOT || path.join(os.homedir(),'.deadbyte-mcp'));
78
+ const interruptedUpdateRecovery = await recoverInterruptedSlotUpdates({ stateRoot: runtimeStateRoot });
79
+ if (interruptedUpdateRecovery.launched > 0) {
80
+ console.error(`[runtime-update-recovery] launched ${interruptedUpdateRecovery.launched} detached supervisor(s)`);
81
+ }
82
+
77
83
  const surfaceProfile = capabilityProfileFromEnv(process.env);
78
84
  const localPrivilegedPolicyPath = path.join(packageRoot, 'controller', 'deadbyte-host-policy.json');
79
85
  const privilegedPolicyExplicit = Object.hasOwn(process.env, 'DEADBYTE_PRIVILEGED_POLICY');
@@ -51,7 +51,7 @@ const transport=new StdioClientTransport({command:process.execPath,args:[serverP
51
51
  env:{...getDefaultEnvironment(),DEADBYTE_MACHINE_RUNTIME:'1',DEADBYTE_CODING_RUNTIME:'1',DEADBYTE_OBSERVATION_RUNTIME:'1',
52
52
  DEADBYTE_CODING_POLICY:policyPath,DEADBYTE_STATE_ROOT:state,DEADBYTE_SIGNING_KEY:keys.privateKeyPath,DEADBYTE_VERIFY_KEY:keys.publicKeyPath},stderr:'pipe'});
53
53
  const stderr=[]; transport.stderr?.on('data',chunk=>stderr.push(Buffer.from(chunk)));
54
- const base=['contained_transform','evidence_verify','file_read','proof_execute','skill_catalog','workspace_open'];
54
+ const base=['contained_transform','evidence_verify','file_read','host_exec','host_file_read','host_file_write','proof_execute','skill_catalog','workspace_open'];
55
55
  const machine=['machine_list','machine_ping','machine_revoke','machine_status'];
56
56
  const coding=['coding_benchmark_run','coding_command_run','coding_file_read','coding_file_write','coding_files_read','coding_receipt_verify','coding_search',
57
57
  'coding_session_status','coding_stat','coding_symbol_definition','coding_symbol_diagnostics','coding_symbol_implementations','coding_symbol_outline','coding_symbol_references','coding_symbol_rename_preview','coding_symbol_replace',
@@ -22,10 +22,10 @@ export function registerProcessTools(server, { runtime }) {
22
22
  description: 'Start one locally registered process profile inside an approved root. Executable identity comes only from local policy.',
23
23
  inputSchema: z.strictObject({
24
24
  profile_id: id, root_id: id, cwd: z.string().min(1).max(1024).default('.'),
25
- args: z.array(z.string().max(8192)).max(64).default([])
25
+ args: z.array(z.string().max(8192)).max(64).default([]), timeout_ms: z.number().int().min(100).max(900000).nullable().default(null)
26
26
  }), outputSchema: processStartOutputSchema
27
- }, async ({ profile_id, root_id, cwd, args }) => {
28
- try { return ok(processStartOutputSchema, { status: 'ok', session: await runtime.start({ profileId: profile_id, rootId: root_id, cwd, args }) }); }
27
+ }, async ({ profile_id, root_id, cwd, args, timeout_ms }) => {
28
+ try { return ok(processStartOutputSchema, { status: 'ok', session: await runtime.start({ profileId: profile_id, rootId: root_id, cwd, args, timeoutMs: timeout_ms }) }); }
29
29
  catch (error) { return fail(processStartOutputSchema, 'denied', error); }
30
30
  });
31
31
  server.registerTool('process_session_read', {
@@ -66,6 +66,14 @@ export function registerProcessTools(server, { runtime }) {
66
66
  try { return ok(processListOutputSchema, { status: 'ok', ...(await runtime.list()) }); }
67
67
  catch (error) { return fail(processListOutputSchema, 'failed', error); }
68
68
  });
69
+ server.registerTool('process_session_cancel', {
70
+ description: 'Request cooperative cancellation of one DEADBYTE-owned process session. Cancellation is recorded separately from physical termination.',
71
+ inputSchema: z.strictObject({ session_id: hex64, reason: z.string().min(1).max(256).default('operator_request') }), outputSchema: processTerminateOutputSchema
72
+ }, async ({ session_id, reason }) => {
73
+ try { return ok(processTerminateOutputSchema, { status: 'ok', session: await runtime.cancel({ sessionId: session_id, reason }) }); }
74
+ catch (error) { return fail(processTerminateOutputSchema, 'denied', error); }
75
+ });
76
+
69
77
  server.registerTool('process_session_terminate', {
70
78
  description: 'Terminate one DEADBYTE-owned process session through its owned host boundary. Numeric PID is never an input.',
71
79
  inputSchema: z.strictObject({ session_id: hex64 }), outputSchema: processTerminateOutputSchema
@@ -4,6 +4,23 @@ import { structuredToolResult } from './output-contracts.mjs';
4
4
  const hex64 = z.string().regex(/^[0-9a-f]{64}$/);
5
5
  const digits = z.string().regex(/^[0-9]+$/);
6
6
  const processState = z.enum(['running','exited','terminated']);
7
+ const termination = z.strictObject({
8
+ deadline_exceeded:z.boolean(), cancellation_requested:z.boolean(), physical_termination:z.boolean(), verified:z.boolean(),
9
+ terminal_semantics:z.string().min(1).max(64)
10
+ });
11
+ const deadline = z.strictObject({
12
+ schema:z.literal('deadbyte.deadline.v1'), id:hex64.nullable(), created_at_ms:z.number(), timeout_ms:z.number().int().positive().nullable(), deadline_ms:z.number().nullable()
13
+ });
14
+ const streamCoverage = z.strictObject({
15
+ logical_first_seq:z.number().int().min(1), logical_last_seq:z.number().int().min(0),
16
+ retained_first_seq:z.number().int().min(1), retained_last_seq:z.number().int().min(0),
17
+ complete:z.boolean(), truncated:z.boolean(),
18
+ missing_ranges:z.array(z.strictObject({from:z.number().int().min(1),to:z.number().int().min(0)})).max(100000),
19
+ logical_item_count:z.number().int().min(0), retained_item_count:z.number().int().min(0),
20
+ logical_bytes:z.number().int().min(0), retained_bytes:z.number().int().min(0),
21
+ dropped_items:z.number().int().min(0), dropped_bytes:z.number().int().min(0),
22
+ status:z.enum(['open','closed'])
23
+ });
7
24
 
8
25
  export const processSessionRecordSchema = z.strictObject({
9
26
  session_id: hex64,
@@ -17,6 +34,12 @@ export const processSessionRecordSchema = z.strictObject({
17
34
  earliest_offset: z.number().int().nonnegative(),
18
35
  stream_head: z.number().int().nonnegative(),
19
36
  stream_sha256: hex64,
37
+ stream_id: hex64.nullable(),
38
+ stream_generation: z.number().int().min(1).nullable(),
39
+ stream_next_seq: z.number().int().min(1).nullable(),
40
+ stream_coverage: streamCoverage.nullable(),
41
+ termination: termination.nullable(),
42
+ deadline: deadline.nullable(),
20
43
  journal_head_sha256: hex64
21
44
  });
22
45
 
@@ -6,6 +6,8 @@ import { canonicalJson } from './canonical-json.mjs';
6
6
  import { isInside, validatePortableRelativePath } from './path-policy.mjs';
7
7
  import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
8
8
  import { assertProcessArmed, recheckProcessAuthority } from './process-policy.mjs';
9
+ import { createSequenceStream } from './workload-substrate.mjs';
10
+ import { classifyTermination, composeAbortSignal, createCancellationToken, createDeadline } from './deadline-cancellation.mjs';
9
11
 
10
12
  const EVENT_SCHEMA = 'deadbyte.process-event.v1';
11
13
  const EVENT_DOMAIN = Buffer.from('DEADBYTE-PROCESS-EVENT-V1\0', 'utf8');
@@ -52,11 +54,18 @@ async function resolveCwd(policy, grant, profile, rootId, cwd) {
52
54
  }
53
55
 
54
56
  function publicSession(session) {
57
+ const stream = session.recovered_terminal ? null : session.output_stream;
55
58
  return {
56
59
  session_id: session.session_id, profile_id: session.profile_id, root_id: session.root_id,
57
60
  cwd: session.cwd_relative, child_pid: session.child_pid, creation_filetime: session.creation_filetime,
58
61
  state: session.state, exit_code: session.exit_code, earliest_offset: session.earliest_offset,
59
62
  stream_head: session.stream_head, stream_sha256: session.stream_sha256,
63
+ stream_id: stream?.streamId ?? null,
64
+ stream_generation: stream?.generation ?? null,
65
+ stream_next_seq: stream?.snapshot().next_seq ?? null,
66
+ stream_coverage: stream?.coverage() ?? null,
67
+ termination: session.termination ?? null,
68
+ deadline: session.deadline ?? null,
60
69
  journal_head_sha256: session.journal_head_sha256
61
70
  };
62
71
  }
@@ -93,6 +102,7 @@ function retainedIndexOf(session, needle, begin = 0) {
93
102
  function appendOutput(session, chunk) {
94
103
  if (!chunk?.length) return;
95
104
  const bytes = Buffer.from(chunk);
105
+ session.output_stream?.append(null, { producer: 'process-host', channel: 'merged-output', sizeBytes: bytes.length });
96
106
  session.stream_sha256 = sha256Bytes(Buffer.concat([
97
107
  Buffer.from(session.stream_sha256, 'hex'), bytes
98
108
  ]));
@@ -207,7 +217,8 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
207
217
  earliest_offset:terminal.detail.earliest_offset, stream_head:terminal.detail.stream_head,
208
218
  stream_sha256:terminal.detail.stream_sha256, output_chunks:[output], retained_output_bytes:output.length, control_buffer:Buffer.alloc(0),
209
219
  journal_head_sha256:checked.head_sha256, journal_path:path.join(policy.state_dir,name), event_count:checked.event_count,
210
- max_retained_output_bytes:profile.max_retained_output_bytes, stderr:'', terminate_requested:terminal.type==='terminated',
220
+ max_retained_output_bytes:profile.max_retained_output_bytes, output_stream:createSequenceStream({streamId:sessionId,maxItems:1000000,maxBytes:profile.max_retained_output_bytes}), stderr:'', terminate_requested:terminal.type==='terminated',
221
+ termination:terminal.detail?.termination ?? null, deadline:terminal.detail?.deadline ?? null, cancellation:null,
211
222
  protocol_error:null, transport:profile.transport, win32_input_mode:false, host:null, close_promise:Promise.resolve(),
212
223
  recovered_terminal:true
213
224
  });
@@ -281,7 +292,8 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
281
292
  }
282
293
  }
283
294
 
284
- async function start({ profileId, rootId, cwd = '.', args = [] }) {
295
+ async function start({ profileId, rootId, cwd = '.', args = [], timeoutMs = null }) {
296
+ if (timeoutMs !== null && (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 900000)) throw new Error('process timeout_ms must be in [100, 900000] or null');
285
297
  await hydrateTerminalSessions();
286
298
  const grant = await assertProcessArmed(policy, profileId, rootId);
287
299
  const profile = policy.profiles[profileId];
@@ -303,16 +315,35 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
303
315
  cwd_relative: cwdInfo.relative, child_pid: null, creation_filetime: null, state: 'starting', exit_code: null,
304
316
  earliest_offset: 0, stream_head: 0, stream_sha256: ZERO_SHA256, output_chunks: [], retained_output_bytes: 0, control_buffer: Buffer.alloc(0),
305
317
  journal_head_sha256: ZERO_SHA256, journal_path: path.join(policy.state_dir, `${sessionId}.jsonl`), event_count: 0,
306
- max_retained_output_bytes: profile.max_retained_output_bytes, stderr: '', terminate_requested: false, protocol_error: null,
318
+ max_retained_output_bytes: profile.max_retained_output_bytes, output_stream: createSequenceStream({streamId: sessionId, maxItems: 1000000, maxBytes: profile.max_retained_output_bytes}), stderr: '', terminate_requested: false, protocol_error: null,
319
+ deadline: createDeadline({ timeoutMs, id: sessionId }),
320
+ cancellation: createCancellationToken({ id: sessionId }),
321
+ termination: classifyTermination(),
307
322
  transport: profile.transport,
308
323
  win32_input_mode: false
309
324
  };
310
325
  await recheckProcessAuthority(policy, grant, profileId, rootId);
326
+ const composed = composeAbortSignal({ deadline: session.deadline, cancellation: session.cancellation });
327
+ session.abort_signal = composed;
311
328
  const host = spawn(policy.native_host.path,
312
329
  [profile.executable, cwdInfo.resolved, session.job_id, `--deadbyte-transport=${profile.transport}`, ...profile.prefix_args, ...args],
313
330
  { cwd: cwdInfo.resolved, env: processEnvironment(profile), windowsHide: true, shell: false, stdio: ['pipe','pipe','pipe'] });
314
331
  session.host = host;
315
332
  sessions.set(sessionId, session);
333
+ const abortProcess = () => {
334
+ const reason = String(composed.signal.reason ?? 'cancelled');
335
+ const snapshot = session.cancellation.snapshot();
336
+ session.termination = classifyTermination({
337
+ deadlineExceeded: reason === 'deadline_exceeded',
338
+ cancellationRequested: snapshot.requested,
339
+ physicallyTerminated: false,
340
+ verified: false
341
+ });
342
+ session.terminate_requested = true;
343
+ if (!host.killed) host.kill();
344
+ };
345
+ composed.signal.addEventListener('abort', abortProcess, { once: true });
346
+ if (composed.signal.aborted) abortProcess();
316
347
  const ready = new Promise((resolve, reject) => { session.ready_resolve = resolve; session.ready_reject = reject; });
317
348
  host.stdout.on('data', chunk => parseHostOutput(session, chunk));
318
349
  host.stderr.on('data', chunk => { session.stderr = (session.stderr + chunk.toString('utf8')).slice(-16384); });
@@ -322,6 +353,13 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
322
353
  parseHostOutput(session, null, true);
323
354
  if (session.state === 'starting') session.ready_reject?.(new Error(`process host exited before READY: ${session.stderr}`));
324
355
  session.exit_code ??= Number.isInteger(code) ? code : null;
356
+ const snapshot = session.cancellation.snapshot();
357
+ session.termination = classifyTermination({
358
+ deadlineExceeded: session.deadline.deadline_ms !== null && Date.now() >= session.deadline.deadline_ms && session.terminate_requested,
359
+ cancellationRequested: snapshot.requested,
360
+ physicallyTerminated: session.terminate_requested,
361
+ verified: true
362
+ });
325
363
  const terminalState = session.terminate_requested ? 'terminated' : 'exited';
326
364
  try {
327
365
  const outputFile = `${session.session_id}.stream.bin`;
@@ -336,10 +374,13 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
336
374
  stream_sha256:session.stream_sha256,
337
375
  retained_output_bytes:session.retained_output_bytes,
338
376
  retained_output_sha256:retainedOutputSha256,
377
+ termination:session.termination,
378
+ deadline:session.deadline,
339
379
  output_file:outputFile
340
380
  });
341
381
  session.state = terminalState;
342
382
  } catch {}
383
+ composed.close();
343
384
  resolve();
344
385
  });
345
386
  });
@@ -428,11 +469,19 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
428
469
  await appendEvent(runtime, session, 'action', { action_id: actionId, logical_bytes: logicalBytes.length, logical_sha256: logicalSha256 });
429
470
  return { action_id: actionId, logical_bytes: logicalBytes.length, logical_sha256: logicalSha256, session: publicSession(session) };
430
471
  }
472
+ async function cancel({ sessionId, reason = 'operator_request' }) {
473
+ const session = await sessionFor(sessionId);
474
+ if (typeof reason !== 'string' || reason.length < 1 || reason.length > 256) throw new Error('process cancellation reason invalid');
475
+ if (['exited','terminated'].includes(session.state)) return publicSession(session);
476
+ session.cancellation.request(reason);
477
+ await session.close_promise;
478
+ return publicSession(session);
479
+ }
431
480
  async function terminate({ sessionId }) {
432
481
  const session = await sessionFor(sessionId);
433
482
  if (['exited','terminated'].includes(session.state)) return publicSession(session);
434
483
  session.terminate_requested = true;
435
- session.host.kill();
484
+ if (!session.host.killed) session.host.kill();
436
485
  await session.close_promise;
437
486
  return publicSession(session);
438
487
  }
@@ -463,5 +512,5 @@ export function createProcessRuntime({ policy, signingKeyPath, verifyKeyPath })
463
512
  return { ok: true, event_count: sequence, head_sha256: previous };
464
513
  }
465
514
 
466
- return { policy, start, read, write, status, list, action, terminate, verifyEventJournal };
515
+ return { policy, start, read, write, status, list, action, cancel, terminate, verifyEventJournal };
467
516
  }