@deepstrike/sdk 0.2.39 → 0.2.40

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 (102) hide show
  1. package/README.md +61 -31
  2. package/dist/collaboration/contract.d.ts +2 -2
  3. package/dist/collaboration/contract.js +2 -2
  4. package/dist/collaboration/handoff.d.ts +2 -14
  5. package/dist/collaboration/handoff.js +1 -17
  6. package/dist/collaboration/harness.d.ts +16 -47
  7. package/dist/collaboration/harness.js +57 -158
  8. package/dist/collaboration/index.d.ts +2 -2
  9. package/dist/collaboration/index.js +1 -2
  10. package/dist/collaboration/modes/creator-verifier.js +42 -6
  11. package/dist/collaboration/pool.d.ts +8 -0
  12. package/dist/collaboration/pool.js +56 -3
  13. package/dist/harness/harness.d.ts +97 -97
  14. package/dist/harness/harness.js +190 -144
  15. package/dist/harness/judge.d.ts +3 -2
  16. package/dist/harness/judge.js +7 -3
  17. package/dist/harness/public.d.ts +5 -3
  18. package/dist/harness/public.js +3 -2
  19. package/dist/index.d.ts +22 -7
  20. package/dist/index.js +8 -1
  21. package/dist/kernel.d.ts +12 -39
  22. package/dist/memory/agent.d.ts +8 -41
  23. package/dist/memory/agent.js +34 -117
  24. package/dist/memory/extraction.d.ts +4 -0
  25. package/dist/memory/extraction.js +79 -0
  26. package/dist/memory/in-memory-store.d.ts +28 -15
  27. package/dist/memory/in-memory-store.js +74 -25
  28. package/dist/memory/protocols.d.ts +56 -56
  29. package/dist/memory/protocols.js +1 -0
  30. package/dist/memory/public.d.ts +3 -1
  31. package/dist/memory/public.js +1 -0
  32. package/dist/memory/ranking.d.ts +33 -0
  33. package/dist/memory/ranking.js +77 -0
  34. package/dist/memory/retention.d.ts +17 -0
  35. package/dist/memory/retention.js +54 -0
  36. package/dist/os/public.d.ts +3 -3
  37. package/dist/os/public.js +1 -1
  38. package/dist/providers/base.d.ts +8 -0
  39. package/dist/providers/base.js +0 -0
  40. package/dist/providers/gemini.js +12 -0
  41. package/dist/providers/ollama.js +3 -1
  42. package/dist/providers/openai-responses.js +9 -2
  43. package/dist/providers/profiles.d.ts +5 -3
  44. package/dist/providers/profiles.js +3 -3
  45. package/dist/runtime/context-policy.d.ts +35 -0
  46. package/dist/runtime/context-policy.js +66 -0
  47. package/dist/runtime/eval.d.ts +6 -2
  48. package/dist/runtime/eval.js +2 -2
  49. package/dist/runtime/event-stream.d.ts +9 -0
  50. package/dist/runtime/event-stream.js +25 -11
  51. package/dist/runtime/execution-plane.d.ts +5 -1
  52. package/dist/runtime/execution-plane.js +9 -3
  53. package/dist/runtime/facade.js +2 -1
  54. package/dist/runtime/kernel-event-log.d.ts +0 -2
  55. package/dist/runtime/kernel-event-log.js +61 -13
  56. package/dist/runtime/kernel-primitives-dashboard.js +1 -1
  57. package/dist/runtime/kernel-rebuild.d.ts +13 -0
  58. package/dist/runtime/kernel-rebuild.js +75 -0
  59. package/dist/runtime/kernel-step.d.ts +157 -8
  60. package/dist/runtime/kernel-step.js +220 -7
  61. package/dist/runtime/kernel-transaction-log.d.ts +61 -0
  62. package/dist/runtime/kernel-transaction-log.js +149 -0
  63. package/dist/runtime/large-result-spool.d.ts +3 -1
  64. package/dist/runtime/large-result-spool.js +24 -5
  65. package/dist/runtime/loop-driver.d.ts +1 -1
  66. package/dist/runtime/loop-driver.js +2 -7
  67. package/dist/runtime/mcp-proxy-plane.d.ts +1 -0
  68. package/dist/runtime/mcp-proxy-plane.js +23 -6
  69. package/dist/runtime/os-profile.d.ts +9 -10
  70. package/dist/runtime/os-profile.js +14 -10
  71. package/dist/runtime/os-snapshot.d.ts +19 -0
  72. package/dist/runtime/os-snapshot.js +33 -3
  73. package/dist/runtime/process-sandbox-plane.js +16 -11
  74. package/dist/runtime/reaction-checkpoint.d.ts +51 -0
  75. package/dist/runtime/reaction-checkpoint.js +83 -0
  76. package/dist/runtime/reactive-session.d.ts +9 -3
  77. package/dist/runtime/reactive-session.js +44 -14
  78. package/dist/runtime/reliability.d.ts +48 -0
  79. package/dist/runtime/reliability.js +86 -0
  80. package/dist/runtime/remote-vpc-plane.js +4 -3
  81. package/dist/runtime/run-group.d.ts +38 -35
  82. package/dist/runtime/run-group.js +97 -54
  83. package/dist/runtime/runner.d.ts +117 -78
  84. package/dist/runtime/runner.js +1516 -1197
  85. package/dist/runtime/session-log.d.ts +71 -12
  86. package/dist/runtime/session-log.js +192 -32
  87. package/dist/runtime/session-repair.d.ts +11 -7
  88. package/dist/runtime/session-repair.js +11 -8
  89. package/dist/runtime/sub-agent-orchestrator.d.ts +6 -2
  90. package/dist/runtime/sub-agent-orchestrator.js +45 -18
  91. package/dist/signals/gateway.d.ts +23 -10
  92. package/dist/signals/gateway.js +81 -17
  93. package/dist/signals/scheduled.js +0 -1
  94. package/dist/signals/types.d.ts +21 -13
  95. package/dist/skills/loader.js +12 -2
  96. package/dist/tools/index.d.ts +2 -0
  97. package/dist/types/agent.d.ts +40 -1
  98. package/dist/types/agent.js +61 -1
  99. package/dist/types.d.ts +6 -1
  100. package/dist/workflow/public.d.ts +2 -3
  101. package/dist/workflow/public.js +0 -1
  102. package/package.json +2 -2
@@ -0,0 +1,149 @@
1
+ import { createHash } from "node:crypto";
2
+ export const KERNEL_LOG_RECORD_VERSION = 1;
3
+ export class KernelLogConflictError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "KernelLogConflictError";
7
+ }
8
+ }
9
+ export class KernelLogIntegrityError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "KernelLogIntegrityError";
13
+ }
14
+ }
15
+ export function canonicalKernelJson(value) {
16
+ if (value === null)
17
+ return "null";
18
+ if (typeof value === "string" || typeof value === "boolean")
19
+ return JSON.stringify(value);
20
+ if (typeof value === "number") {
21
+ if (!Number.isFinite(value))
22
+ throw new KernelLogIntegrityError("canonical records require finite numbers");
23
+ if (Number.isSafeInteger(value))
24
+ return JSON.stringify(Object.is(value, -0) ? 0 : value);
25
+ if (Number.isInteger(value)) {
26
+ throw new KernelLogIntegrityError("canonical record integer exceeds the cross-SDK safe range");
27
+ }
28
+ const bytes = new Uint8Array(8);
29
+ new DataView(bytes.buffer).setFloat64(0, value, false);
30
+ return `f64:${Array.from(bytes, byte => byte.toString(16).padStart(2, "0")).join("")}`;
31
+ }
32
+ if (Array.isArray(value))
33
+ return `[${value.map(canonicalKernelJson).join(",")}]`;
34
+ if (typeof value === "object") {
35
+ const object = value;
36
+ const keys = Object.keys(object).sort();
37
+ return `{${keys.map(key => `${JSON.stringify(key)}:${canonicalKernelJson(object[key])}`).join(",")}}`;
38
+ }
39
+ throw new KernelLogIntegrityError(`unsupported canonical record value: ${typeof value}`);
40
+ }
41
+ export function kernelRecordDigest(value) {
42
+ return createHash("sha256").update(canonicalKernelJson(value), "utf8").digest("hex");
43
+ }
44
+ export async function createKernelOperationGenesis(input) {
45
+ const body = {
46
+ record_version: KERNEL_LOG_RECORD_VERSION,
47
+ ...input,
48
+ };
49
+ validateGenesisBody(body);
50
+ return { ...body, genesis_digest: kernelRecordDigest(body) };
51
+ }
52
+ export async function createKernelTransaction(input) {
53
+ const body = {
54
+ record_version: KERNEL_LOG_RECORD_VERSION,
55
+ operation_id: input.operation_id,
56
+ step_seq: input.step_seq,
57
+ base_generation: input.base_generation,
58
+ input: input.input,
59
+ input_digest: kernelRecordDigest(input.input),
60
+ previous_transaction_digest: input.previous_transaction_digest,
61
+ step_digest: kernelRecordDigest(input.step),
62
+ };
63
+ validateTransactionBody(body);
64
+ return { ...body, transaction_digest: kernelRecordDigest(body) };
65
+ }
66
+ export function verifyKernelOperationGenesis(genesis) {
67
+ const { genesis_digest, ...body } = genesis;
68
+ validateGenesisBody(body);
69
+ if (kernelRecordDigest(body) !== genesis_digest) {
70
+ throw new KernelLogIntegrityError("kernel genesis digest does not match its canonical body");
71
+ }
72
+ }
73
+ export function verifyKernelTransaction(transaction) {
74
+ const { transaction_digest, ...body } = transaction;
75
+ validateTransactionBody(body);
76
+ if (kernelRecordDigest(body.input) !== body.input_digest) {
77
+ throw new KernelLogIntegrityError("kernel transaction input digest does not match its input");
78
+ }
79
+ if (kernelRecordDigest(body) !== transaction_digest) {
80
+ throw new KernelLogIntegrityError("kernel transaction digest does not match its canonical body");
81
+ }
82
+ }
83
+ export function verifyKernelTransactionSuccessor(previous, transaction) {
84
+ const expectedStepSeq = previous ? previous.step_seq + 1 : 1;
85
+ const expectedGeneration = previous ? previous.base_generation + 1 : 0;
86
+ if (transaction.step_seq !== expectedStepSeq) {
87
+ throw new KernelLogIntegrityError(`kernel transaction step_seq ${transaction.step_seq} does not follow ${expectedStepSeq - 1}`);
88
+ }
89
+ if (transaction.base_generation !== expectedGeneration) {
90
+ throw new KernelLogIntegrityError(`kernel transaction base_generation ${transaction.base_generation} does not match ${expectedGeneration}`);
91
+ }
92
+ if (transaction.input.operation_id !== transaction.operation_id) {
93
+ throw new KernelLogIntegrityError("kernel transaction input operation_id does not match its envelope");
94
+ }
95
+ }
96
+ /** Validate one complete authoritative operation stream and derive its next wire cursor. */
97
+ export function verifyKernelTransactionStream(genesis, transactions) {
98
+ verifyKernelOperationGenesis(genesis);
99
+ let previous;
100
+ let head = genesis.genesis_digest;
101
+ for (const transaction of transactions) {
102
+ verifyKernelTransaction(transaction);
103
+ if (transaction.operation_id !== genesis.operation_id) {
104
+ throw new KernelLogIntegrityError("kernel transaction operation_id does not match genesis");
105
+ }
106
+ if (transaction.previous_transaction_digest !== head) {
107
+ throw new KernelLogIntegrityError("kernel transaction digest chain is not continuous");
108
+ }
109
+ verifyKernelTransactionSuccessor(previous, transaction);
110
+ previous = transaction;
111
+ head = transaction.transaction_digest;
112
+ }
113
+ const nextSequence = (previous?.step_seq ?? 0) + 1;
114
+ return {
115
+ operation_id: genesis.operation_id,
116
+ next_event_sequence: nextSequence,
117
+ next_step_seq: nextSequence,
118
+ transaction_head_digest: head,
119
+ };
120
+ }
121
+ function validateGenesisBody(genesis) {
122
+ if (genesis.record_version !== KERNEL_LOG_RECORD_VERSION) {
123
+ throw new KernelLogIntegrityError("unsupported kernel genesis record version");
124
+ }
125
+ if (!Number.isSafeInteger(genesis.abi_version) || genesis.abi_version <= 0) {
126
+ throw new KernelLogIntegrityError("kernel genesis abi_version must be a positive safe integer");
127
+ }
128
+ if (!genesis.operation_id)
129
+ throw new KernelLogIntegrityError("kernel genesis operation_id is required");
130
+ if (!Number.isSafeInteger(genesis.default_policy_version) || genesis.default_policy_version <= 0) {
131
+ throw new KernelLogIntegrityError("kernel genesis default_policy_version must be a positive safe integer");
132
+ }
133
+ }
134
+ function validateTransactionBody(transaction) {
135
+ if (transaction.record_version !== KERNEL_LOG_RECORD_VERSION) {
136
+ throw new KernelLogIntegrityError("unsupported kernel transaction record version");
137
+ }
138
+ if (!transaction.operation_id)
139
+ throw new KernelLogIntegrityError("kernel transaction operation_id is required");
140
+ if (!Number.isSafeInteger(transaction.step_seq) || transaction.step_seq <= 0) {
141
+ throw new KernelLogIntegrityError("kernel transaction step_seq must be a positive safe integer");
142
+ }
143
+ if (!Number.isSafeInteger(transaction.base_generation) || transaction.base_generation < 0) {
144
+ throw new KernelLogIntegrityError("kernel transaction base_generation must be a non-negative safe integer");
145
+ }
146
+ if (!transaction.previous_transaction_digest) {
147
+ throw new KernelLogIntegrityError("kernel transaction previous_transaction_digest is required");
148
+ }
149
+ }
@@ -57,6 +57,8 @@ export declare class LargeResultSpool {
57
57
  * Get spool file path for a hash.
58
58
  */
59
59
  private getSpoolPath;
60
+ private callKey;
61
+ private atomicWrite;
60
62
  /**
61
63
  * Write large result to disk.
62
64
  */
@@ -80,7 +82,7 @@ export declare class LargeResultSpool {
80
82
  /**
81
83
  * O7: locate a spooled output by the tool call's id (the `read_result` meta-tool only knows
82
84
  * `call_id`, not the content-hashed file name `persistOutput` chose). Scans the spool directory
83
- * for the `${callId}-*.txt` naming convention; returns `undefined` if nothing was ever spooled
85
+ * for the hashed call-key prefix; returns `undefined` if nothing was ever spooled
84
86
  * for that call (e.g. it never actually exceeded the threshold, or the spool dir was cleaned up).
85
87
  */
86
88
  findByCallId(callId: string): Promise<string | undefined>;
@@ -46,6 +46,25 @@ export class LargeResultSpool {
46
46
  getSpoolPath(hash) {
47
47
  return path.join(this.spoolDir, `${hash}.txt`);
48
48
  }
49
+ callKey(callId) {
50
+ return this.hashContent(callId).slice(0, 32);
51
+ }
52
+ async atomicWrite(spoolPath, content) {
53
+ const tempPath = `${spoolPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
54
+ let handle;
55
+ try {
56
+ handle = await fs.open(tempPath, 'wx');
57
+ await handle.writeFile(content, 'utf-8');
58
+ await handle.sync();
59
+ await handle.close();
60
+ handle = undefined;
61
+ await fs.rename(tempPath, spoolPath);
62
+ }
63
+ finally {
64
+ await handle?.close().catch(() => undefined);
65
+ await fs.unlink(tempPath).catch(() => undefined);
66
+ }
67
+ }
49
68
  /**
50
69
  * Write large result to disk.
51
70
  */
@@ -56,7 +75,7 @@ export class LargeResultSpool {
56
75
  promise = (async () => {
57
76
  try {
58
77
  await fs.mkdir(this.spoolDir, { recursive: true });
59
- await fs.writeFile(spoolPath, content, 'utf-8');
78
+ await this.atomicWrite(spoolPath, content);
60
79
  return spoolPath;
61
80
  }
62
81
  finally {
@@ -111,13 +130,13 @@ omitted: ${omitted} chars
111
130
  */
112
131
  async persistOutput(callId, content) {
113
132
  const hash = this.hashContent(content);
114
- const spoolPath = this.getSpoolPath(`${callId}-${hash.slice(0, 16)}`);
133
+ const spoolPath = this.getSpoolPath(`${this.callKey(callId)}-${hash.slice(0, 16)}`);
115
134
  let promise = this.activeWrites.get(spoolPath);
116
135
  if (!promise) {
117
136
  promise = (async () => {
118
137
  try {
119
138
  await fs.mkdir(this.spoolDir, { recursive: true });
120
- await fs.writeFile(spoolPath, content, 'utf-8');
139
+ await this.atomicWrite(spoolPath, content);
121
140
  return spoolPath;
122
141
  }
123
142
  finally {
@@ -143,7 +162,7 @@ omitted: ${omitted} chars
143
162
  /**
144
163
  * O7: locate a spooled output by the tool call's id (the `read_result` meta-tool only knows
145
164
  * `call_id`, not the content-hashed file name `persistOutput` chose). Scans the spool directory
146
- * for the `${callId}-*.txt` naming convention; returns `undefined` if nothing was ever spooled
165
+ * for the hashed call-key prefix; returns `undefined` if nothing was ever spooled
147
166
  * for that call (e.g. it never actually exceeded the threshold, or the spool dir was cleaned up).
148
167
  */
149
168
  async findByCallId(callId) {
@@ -154,7 +173,7 @@ omitted: ${omitted} chars
154
173
  catch {
155
174
  return undefined;
156
175
  }
157
- const prefix = `${callId}-`;
176
+ const prefix = `${this.callKey(callId)}-`;
158
177
  const match = files.find(f => f.startsWith(prefix) && f.endsWith('.txt'));
159
178
  if (!match)
160
179
  return undefined;
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * Durable pacing: every round appends `round_started` / `round_paced` to the loop's
14
14
  * session log, so `LoopDriver.resume()`-style recovery is a fold over the log
15
- * (the SessionLogGroupBudgetStore pattern) — zero new storage. A stateless host
15
+ * — zero new storage. A stateless host
16
16
  * reads `wake_at_ms` from the fold and re-arms via its own cron/queue; an
17
17
  * in-process host lets `run()` sleep inline.
18
18
  */
@@ -108,9 +108,8 @@ export class LoopDriver {
108
108
  defaultAction: this.spec.defaultAction,
109
109
  },
110
110
  };
111
- // With a RunGroup configured, run() seeds the kernel trap's round base from the
112
- // group ledger (the driver charges rounds:1 per round below) — max_rounds coercion
113
- // then happens IN-KERNEL; the check above is the ungrouped backstop.
111
+ // With a RunGroup configured, run() reserves one round and the kernel reports its
112
+ // correlated local usage; the check above remains the ungrouped backstop.
114
113
  try {
115
114
  for await (const evt of this.runner.run({
116
115
  sessionId: loopId,
@@ -164,10 +163,6 @@ export class LoopDriver {
164
163
  reason: finalDecision.reason,
165
164
  ...(finalDecision.coercedFrom ? { coerced_from: finalDecision.coercedFrom } : {}),
166
165
  });
167
- // Lifetime governance: one round = one group charge on the rounds axis.
168
- const group = this.runner.hostOptions.runGroup;
169
- if (group)
170
- await group.budgetStore.charge(group.id, { rounds: 1 });
171
166
  if (finalDecision.action === "stop") {
172
167
  return {
173
168
  loopId, roundsCompleted: round, stopped: true, state: "stopped",
@@ -39,6 +39,7 @@ export declare class McpProxyPlane implements ExecutionPlane {
39
39
  constructor(opts: {
40
40
  servers: Record<string, McpServerConfig>;
41
41
  vault: CredentialVault;
42
+ timeoutMs?: number;
42
43
  });
43
44
  /** Start all configured MCP server processes and discover their tool schemas. */
44
45
  connect(): Promise<void>;
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { createInterface } from "node:readline";
3
3
  import { LocalExecutionPlane } from "./execution-plane.js";
4
4
  import { formatToolError } from "../tools/errors.js";
5
+ import { operationAbortSignal } from "./reliability.js";
5
6
  class McpConnection {
6
7
  serverName;
7
8
  config;
@@ -63,14 +64,28 @@ class McpConnection {
63
64
  this.schemaNames.add(t.name);
64
65
  }
65
66
  }
66
- request(method, params) {
67
+ request(method, params, signal) {
67
68
  return new Promise((resolve, reject) => {
68
69
  if (!this.child?.stdin) {
69
70
  reject(new Error(`MCP server "${this.serverName}" not running`));
70
71
  return;
71
72
  }
72
73
  const id = this.nextId++;
73
- this.pending.set(id, { resolve, reject });
74
+ const cleanup = () => signal?.removeEventListener("abort", abort);
75
+ const abort = () => {
76
+ this.pending.delete(id);
77
+ cleanup();
78
+ reject(signal?.reason instanceof Error ? signal.reason : new Error("operation cancelled"));
79
+ };
80
+ this.pending.set(id, {
81
+ resolve: result => { cleanup(); resolve(result); },
82
+ reject: error => { cleanup(); reject(error); },
83
+ });
84
+ signal?.addEventListener("abort", abort, { once: true });
85
+ if (signal?.aborted) {
86
+ abort();
87
+ return;
88
+ }
74
89
  const msg = { jsonrpc: "2.0", method, id };
75
90
  if (params !== undefined)
76
91
  msg.params = params;
@@ -87,10 +102,10 @@ class McpConnection {
87
102
  }
88
103
  schemas() { return this._schemas; }
89
104
  hasSchema(name) { return this.schemaNames.has(name); }
90
- async execute(call) {
105
+ async execute(call, signal) {
91
106
  try {
92
107
  const args = JSON.parse(call.arguments || "{}");
93
- const result = await this.request("tools/call", { name: call.name, arguments: args });
108
+ const result = await this.request("tools/call", { name: call.name, arguments: args }, signal);
94
109
  const text = (result.content ?? [])
95
110
  .filter(c => c.type === "text")
96
111
  .map(c => c.text ?? "")
@@ -192,8 +207,10 @@ export class McpProxyPlane {
192
207
  }
193
208
  const tasks = Array.from(groups.entries()).map(async ([conn, serverCalls]) => {
194
209
  const results = [];
195
- for (const call of serverCalls)
196
- results.push({ call, result: await conn.execute(call) });
210
+ for (const call of serverCalls) {
211
+ const signal = operationAbortSignal(ctx.operation, this.opts.timeoutMs ?? 30_000);
212
+ results.push({ call, result: await conn.execute(call, signal) });
213
+ }
197
214
  return results;
198
215
  });
199
216
  for (const settled of await Promise.all(tasks)) {
@@ -1,16 +1,17 @@
1
1
  import type { GovernancePolicy } from "../governance.js";
2
2
  export type OsProfileId = "native";
3
+ export interface SignalPolicy {
4
+ queueMax: number;
5
+ ttlMs?: number;
6
+ deadlineEscalation?: boolean;
7
+ }
3
8
  export interface NativeOsProfile {
4
9
  id: OsProfileId;
5
- attentionPolicy: {
6
- maxQueueSize?: number;
7
- };
10
+ signalPolicy: SignalPolicy;
8
11
  governancePolicy: GovernancePolicy;
9
12
  }
10
- /** Default attention policy for native profile smoke tests. */
11
- export declare const DEFAULT_NATIVE_ATTENTION_POLICY: {
12
- maxQueueSize: number;
13
- };
13
+ /** Default signal policy for native profile smoke tests. */
14
+ export declare const DEFAULT_NATIVE_SIGNAL_POLICY: SignalPolicy;
14
15
  /** Permissive governance policy for native runs that do not need AskUser. */
15
16
  export declare const DEFAULT_NATIVE_GOVERNANCE_POLICY: GovernancePolicy;
16
17
  /** Default restrictive sandbox policy template requiring confirmation for modification/execution. */
@@ -22,9 +23,7 @@ export declare function assertNativeProfile(profile?: OsProfileId | NativeOsProf
22
23
  /**
23
24
  * Validates the declarative policies statically to prevent runtime crashes when loaded into the microkernel.
24
25
  */
25
- export declare function validateDeclarativePolicy(govPolicy?: GovernancePolicy, attentionPolicy?: {
26
- maxQueueSize?: number;
27
- }): {
26
+ export declare function validateDeclarativePolicy(govPolicy?: GovernancePolicy, signalPolicy?: SignalPolicy): {
28
27
  valid: boolean;
29
28
  errors: string[];
30
29
  };
@@ -1,5 +1,5 @@
1
- /** Default attention policy for native profile smoke tests. */
2
- export const DEFAULT_NATIVE_ATTENTION_POLICY = { maxQueueSize: 64 };
1
+ /** Default signal policy for native profile smoke tests. */
2
+ export const DEFAULT_NATIVE_SIGNAL_POLICY = { queueMax: 64 };
3
3
  /** Permissive governance policy for native runs that do not need AskUser. */
4
4
  export const DEFAULT_NATIVE_GOVERNANCE_POLICY = {
5
5
  rules: [{ pattern: "*", action: "allow" }],
@@ -21,7 +21,7 @@ export function osProfile(profile = "native") {
21
21
  throw new Error(`Unsupported OS profile: ${profile}`);
22
22
  return {
23
23
  id: "native",
24
- attentionPolicy: DEFAULT_NATIVE_ATTENTION_POLICY,
24
+ signalPolicy: DEFAULT_NATIVE_SIGNAL_POLICY,
25
25
  governancePolicy: DEFAULT_NATIVE_GOVERNANCE_POLICY,
26
26
  };
27
27
  }
@@ -31,7 +31,7 @@ export function assertNativeProfile(profile = "native") {
31
31
  if (resolved.id !== "native") {
32
32
  throw new Error(`Unsupported OS profile: ${resolved.id}`);
33
33
  }
34
- const validation = validateDeclarativePolicy(resolved.governancePolicy, resolved.attentionPolicy);
34
+ const validation = validateDeclarativePolicy(resolved.governancePolicy, resolved.signalPolicy);
35
35
  if (!validation.valid) {
36
36
  throw new Error(`Invalid native OS profile: ${validation.errors.join("; ")}`);
37
37
  }
@@ -40,7 +40,7 @@ export function assertNativeProfile(profile = "native") {
40
40
  /**
41
41
  * Validates the declarative policies statically to prevent runtime crashes when loaded into the microkernel.
42
42
  */
43
- export function validateDeclarativePolicy(govPolicy, attentionPolicy) {
43
+ export function validateDeclarativePolicy(govPolicy, signalPolicy) {
44
44
  const errors = [];
45
45
  if (govPolicy) {
46
46
  if (!Array.isArray(govPolicy.rules)) {
@@ -57,11 +57,15 @@ export function validateDeclarativePolicy(govPolicy, attentionPolicy) {
57
57
  });
58
58
  }
59
59
  }
60
- if (attentionPolicy) {
61
- if (attentionPolicy.maxQueueSize !== undefined) {
62
- if (typeof attentionPolicy.maxQueueSize !== "number" || attentionPolicy.maxQueueSize <= 0) {
63
- errors.push("AttentionPolicy maxQueueSize must be a positive integer");
64
- }
60
+ if (signalPolicy) {
61
+ if (!Number.isInteger(signalPolicy.queueMax) || signalPolicy.queueMax <= 0) {
62
+ errors.push("SignalPolicy queueMax must be a positive integer");
63
+ }
64
+ if (signalPolicy.ttlMs !== undefined && (!Number.isInteger(signalPolicy.ttlMs) || signalPolicy.ttlMs <= 0)) {
65
+ errors.push("SignalPolicy ttlMs must be a positive integer");
66
+ }
67
+ if (signalPolicy.deadlineEscalation !== undefined && typeof signalPolicy.deadlineEscalation !== "boolean") {
68
+ errors.push("SignalPolicy deadlineEscalation must be a boolean");
65
69
  }
66
70
  }
67
71
  return {
@@ -14,10 +14,29 @@ export interface OsSnapshot {
14
14
  }>;
15
15
  budgetExceeded: Array<{
16
16
  turn: number;
17
+ operation_id: string;
18
+ reservation_id?: string;
17
19
  budget: string;
18
20
  }>;
21
+ budgetUsageReported: Array<{
22
+ turn: number;
23
+ operation_id: string;
24
+ reservation_id: string;
25
+ tokens: number;
26
+ subagents: number;
27
+ rounds: number;
28
+ }>;
29
+ cancellations: Array<{
30
+ turn: number;
31
+ operation_id: string;
32
+ reason: "user" | "deadline" | "lease_lost" | "host_shutdown";
33
+ pending_call_ids: string[];
34
+ }>;
19
35
  signals: Array<{
20
36
  turn: number;
37
+ operation_id: string;
38
+ delivery_id: string;
39
+ attempt: number;
21
40
  signal_id: string;
22
41
  disposition: string;
23
42
  queue_depth: number;
@@ -8,8 +8,10 @@ const KERNEL_KINDS = new Set([
8
8
  "suspended",
9
9
  "resumed",
10
10
  "tool_gated",
11
- "signal_disposed",
11
+ "signal_delivery_disposed",
12
12
  "budget_exceeded",
13
+ "budget_usage_reported",
14
+ "operation_cancelled",
13
15
  "checkpoint_taken",
14
16
  "rollbacked",
15
17
  "agent_process_changed",
@@ -23,6 +25,8 @@ export function rebuildOsSnapshotFromSessionEvents(events) {
23
25
  const snap = {
24
26
  processByAgent: [],
25
27
  budgetExceeded: [],
28
+ budgetUsageReported: [],
29
+ cancellations: [],
26
30
  signals: [],
27
31
  pageOutCount: 0,
28
32
  pageInCount: 0,
@@ -73,11 +77,37 @@ export function rebuildOsSnapshotFromSessionEvents(events) {
73
77
  break;
74
78
  }
75
79
  case "budget_exceeded":
76
- snap.budgetExceeded.push({ turn: event.turn, budget: event.budget });
80
+ snap.budgetExceeded.push({
81
+ turn: event.turn,
82
+ operation_id: event.operation_id,
83
+ ...(event.reservation_id ? { reservation_id: event.reservation_id } : {}),
84
+ budget: event.budget,
85
+ });
86
+ break;
87
+ case "budget_usage_reported":
88
+ snap.budgetUsageReported.push({
89
+ turn: event.turn,
90
+ operation_id: event.operation_id,
91
+ reservation_id: event.reservation_id,
92
+ tokens: event.tokens,
93
+ subagents: event.subagents,
94
+ rounds: event.rounds,
95
+ });
96
+ break;
97
+ case "operation_cancelled":
98
+ snap.cancellations.push({
99
+ turn: event.turn,
100
+ operation_id: event.operation_id,
101
+ reason: event.reason,
102
+ pending_call_ids: event.pending_call_ids,
103
+ });
77
104
  break;
78
- case "signal_disposed":
105
+ case "signal_delivery_disposed":
79
106
  snap.signals.push({
80
107
  turn: event.turn,
108
+ operation_id: event.operation_id,
109
+ delivery_id: event.delivery_id,
110
+ attempt: event.attempt,
81
111
  signal_id: event.signal_id,
82
112
  disposition: event.disposition,
83
113
  queue_depth: event.queue_depth,
@@ -3,6 +3,7 @@ import { mkdir } from "node:fs/promises";
3
3
  import { tool } from "../tools/index.js";
4
4
  import { LocalExecutionPlane } from "./execution-plane.js";
5
5
  import { formatToolError } from "../tools/errors.js";
6
+ import { operationAbortSignal } from "./reliability.js";
6
7
  /**
7
8
  * ExecutionPlane that runs subprocesses with a sandbox directory as cwd.
8
9
  * Extends LocalExecutionPlane with two built-in tools:
@@ -39,7 +40,7 @@ export class ProcessSandboxPlane extends LocalExecutionPlane {
39
40
  }
40
41
  return env;
41
42
  }
42
- runSubprocess(cmd, argv, cwd) {
43
+ runSubprocess(cmd, argv, cwd, operation) {
43
44
  return new Promise(resolve => {
44
45
  const chunks = [];
45
46
  let totalBytes = 0;
@@ -48,7 +49,7 @@ export class ProcessSandboxPlane extends LocalExecutionPlane {
48
49
  if (settled)
49
50
  return;
50
51
  settled = true;
51
- clearTimeout(timer);
52
+ signal.removeEventListener("abort", abort);
52
53
  resolve({ output, isError });
53
54
  };
54
55
  const child = spawn(cmd, argv, {
@@ -56,10 +57,14 @@ export class ProcessSandboxPlane extends LocalExecutionPlane {
56
57
  env: this.buildEnv(),
57
58
  stdio: ["ignore", "pipe", "pipe"],
58
59
  });
59
- const timer = setTimeout(() => {
60
+ const signal = operationAbortSignal(operation, this.timeoutMs);
61
+ const abort = () => {
60
62
  child.kill("SIGKILL");
61
- settle(`timed out after ${this.timeoutMs}ms`, true);
62
- }, this.timeoutMs);
63
+ settle(signal.reason instanceof Error ? signal.reason.message : "operation cancelled", true);
64
+ };
65
+ signal.addEventListener("abort", abort, { once: true });
66
+ if (signal.aborted)
67
+ abort();
63
68
  const capture = (chunk) => {
64
69
  if (settled)
65
70
  return;
@@ -89,9 +94,9 @@ export class ProcessSandboxPlane extends LocalExecutionPlane {
89
94
  const cwd = ctx?.cwd ?? this.sandboxDir;
90
95
  if (!ctx?.cwd)
91
96
  await mkdir(this.sandboxDir, { recursive: true });
92
- const { output, isError } = await this.runSubprocess("bash", ["-c", String(args.command)], cwd);
93
- if (isError && !output.trim())
94
- return "Process exited with non-zero status and produced no output.";
97
+ const { output, isError } = await this.runSubprocess("bash", ["-c", String(args.command)], cwd, ctx?.operation);
98
+ if (isError)
99
+ throw new Error(output.trim() || "Process exited with non-zero status and produced no output.");
95
100
  return output || "(no output)";
96
101
  });
97
102
  }
@@ -107,9 +112,9 @@ export class ProcessSandboxPlane extends LocalExecutionPlane {
107
112
  const cwd = ctx?.cwd ?? this.sandboxDir;
108
113
  if (!ctx?.cwd)
109
114
  await mkdir(this.sandboxDir, { recursive: true });
110
- const { output, isError } = await this.runSubprocess("node", ["-e", String(args.code)], cwd);
111
- if (isError && !output.trim())
112
- return "Script exited with non-zero status and produced no output.";
115
+ const { output, isError } = await this.runSubprocess("node", ["-e", String(args.code)], cwd, ctx?.operation);
116
+ if (isError)
117
+ throw new Error(output.trim() || "Script exited with non-zero status and produced no output.");
113
118
  return output || "(no output)";
114
119
  });
115
120
  }
@@ -0,0 +1,51 @@
1
+ export interface ReactionRecord {
2
+ personaId: string;
3
+ output: string;
4
+ }
5
+ export interface ReactionCheckpointReceipt {
6
+ checkpointKey: string;
7
+ leaseToken: string;
8
+ }
9
+ export interface ReactionCheckpointClaim extends ReactionCheckpointReceipt {
10
+ leaseExpiresAtMs: number;
11
+ plan?: string[];
12
+ outputs: Record<string, string>;
13
+ }
14
+ export type ReactionCheckpointClaimResult = {
15
+ status: "claimed";
16
+ claim: ReactionCheckpointClaim;
17
+ } | {
18
+ status: "completed";
19
+ reactions: ReactionRecord[];
20
+ } | {
21
+ status: "busy";
22
+ };
23
+ export interface ReactionCheckpointStore {
24
+ claim(checkpointKey: string, leaseMs?: number): Promise<ReactionCheckpointClaimResult>;
25
+ savePlan(receipt: ReactionCheckpointReceipt, personaIds: string[]): Promise<string[] | null>;
26
+ record(receipt: ReactionCheckpointReceipt, reaction: ReactionRecord): Promise<boolean>;
27
+ complete(receipt: ReactionCheckpointReceipt): Promise<boolean>;
28
+ release(receipt: ReactionCheckpointReceipt): Promise<boolean>;
29
+ }
30
+ export interface InMemoryReactionCheckpointStoreOptions {
31
+ now?: () => number;
32
+ defaultLeaseMs?: number;
33
+ }
34
+ /** Process-local reference implementation; durable stores implement the same atomic contract. */
35
+ export declare class InMemoryReactionCheckpointStore implements ReactionCheckpointStore {
36
+ private readonly opts;
37
+ private readonly states;
38
+ private leaseSeq;
39
+ constructor(opts?: InMemoryReactionCheckpointStoreOptions);
40
+ claim(checkpointKey: string, leaseMs?: number): Promise<ReactionCheckpointClaimResult>;
41
+ savePlan(receipt: ReactionCheckpointReceipt, personaIds: string[]): Promise<string[] | null>;
42
+ record(receipt: ReactionCheckpointReceipt, reaction: ReactionRecord): Promise<boolean>;
43
+ complete(receipt: ReactionCheckpointReceipt): Promise<boolean>;
44
+ release(receipt: ReactionCheckpointReceipt): Promise<boolean>;
45
+ private current;
46
+ private reactions;
47
+ }
48
+ export declare class ReactionInProgressError extends Error {
49
+ readonly checkpointKey: string;
50
+ constructor(checkpointKey: string);
51
+ }