@byok-sdk/client 0.13.0 → 0.14.0

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 (39) hide show
  1. package/README.md +80 -0
  2. package/dist/adapters/claude/process-client.d.ts +24 -0
  3. package/dist/adapters/codex/process-runner.d.ts +46 -1
  4. package/dist/adapters/index.js +359 -28
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/adapters/pi/events.d.ts +1 -1
  7. package/dist/adapters/pi/rpc-client.d.ts +47 -1
  8. package/dist/adapters/pi/subagents-policy-extension.js +1 -1
  9. package/dist/adapters/pi/subagents-policy-extension.js.map +1 -1
  10. package/dist/adapters/pi/team-interaction-extension.d.ts +24 -0
  11. package/dist/adapters/pi/team-interaction-extension.js +81 -0
  12. package/dist/adapters/pi/team-interaction-extension.js.map +1 -0
  13. package/dist/adapters/process-tree.d.ts +74 -4
  14. package/dist/adapters/provider-credential-environment.d.ts +1 -1
  15. package/dist/adapters/win32-job-object.d.ts +101 -0
  16. package/dist/bin/byok-agent-memory-mcp.js.map +1 -1
  17. package/dist/bin/byok-agent-message-mcp.js.map +1 -1
  18. package/dist/bin/byok-agent-team-mcp.js.map +1 -1
  19. package/dist/bin/byok-agent.js +9305 -7988
  20. package/dist/bin/byok-agent.js.map +1 -1
  21. package/dist/bin/byok-approval-mcp.js.map +1 -1
  22. package/dist/bin/commands/team-pi-relay.d.ts +24 -0
  23. package/dist/bin/commands/team-relay.d.ts +11 -0
  24. package/dist/bin/team-codex-relay.d.ts +32 -0
  25. package/dist/bin/team-notification-relay.d.ts +40 -0
  26. package/dist/bin/team-pi-session.d.ts +59 -0
  27. package/dist/daemon/agent-egress-policy.d.ts +8 -0
  28. package/dist/daemon/connection-manager.d.ts +6 -100
  29. package/dist/daemon/control-protocol.d.ts +2 -0
  30. package/dist/daemon/create-daemon.d.ts +32 -0
  31. package/dist/daemon/event-spill.d.ts +90 -0
  32. package/dist/daemon/journal/journal.d.ts +15 -3
  33. package/dist/daemon/journal/sqlite-journal.d.ts +7 -2
  34. package/dist/daemon/long-poll-transport.d.ts +2 -50
  35. package/dist/daemon/task-runner.d.ts +13 -0
  36. package/dist/daemon/team-workspace.d.ts +9 -0
  37. package/dist/index.js +897 -241
  38. package/dist/index.js.map +1 -1
  39. package/package.json +8 -5
package/dist/index.js CHANGED
@@ -2,11 +2,11 @@ import { randomUUID, createHash, randomBytes, createPrivateKey, generateKeyPairS
2
2
  import { promises, mkdirSync, constants, existsSync, linkSync, fstatSync, lstatSync, unlinkSync, renameSync, writeFileSync, chmodSync, statSync, readdirSync, readFileSync, realpathSync } from 'fs';
3
3
  import * as path3 from 'path';
4
4
  import path3__default, { join, isAbsolute } from 'path';
5
- import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AgentRefSchema, AgentHomeProjectionPayloadSchema, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, partitionAgentEvents, BYOK_SKILL_PACKS_PATH, byokSkillPackFilePath, BYOK_RECORDS_PATH, byokRecordPath, TASK_TRANSITIONS, AgentEgressPolicySchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentContentReceiptPayloadSchema, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, TERMINAL_PROJECTION_SELECTION_CAPABILITY, PROVIDER_PROFILE_BINDING_CAPABILITY, STRICT_AGENT_ONLY_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, AgentHomeProjectionCompletionRequestSchema, byokAgentHomeProjectionCompletionPath, AgentHomeProjectionReadbackSchema, parseMessage, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, AgentMessagePublishPayloadSchema, AgentMessageDispositionPayloadSchema, RuntimeIdSchema, AGENT_MEMORY_PROJECTION_CAPABILITY, TERMINAL_INFERENCE_USAGE_MAX_DURATION_MS, TERMINAL_INFERENCE_USAGE_MAX_TOKENS, RESULT_DOCUMENT_MAX_BYTES, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, UnknownMessageTypeError, MESSAGE_TYPES, AGENT_MEMORY_PROJECTION_MAX_ORDERING_VALUE, AgentMemoryProjectionMutationSchema, AGENT_MEMORY_PROJECTION_MAX_REDACTED_BYTES } from '@byok-sdk/protocol';
5
+ import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AgentRefSchema, AgentHomeProjectionPayloadSchema, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, isTaskOfferType, partitionAgentEvents, BYOK_SKILL_PACKS_PATH, byokSkillPackFilePath, BYOK_RECORDS_PATH, byokRecordPath, TASK_TRANSITIONS, AgentEgressPolicySchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentContentReceiptPayloadSchema, decodeEnvelope, createEnvelope, encodeEnvelope, PROTOCOL_VERSION, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, TERMINAL_PROJECTION_SELECTION_CAPABILITY, PROVIDER_PROFILE_BINDING_CAPABILITY, STRICT_AGENT_ONLY_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, AgentHomeProjectionCompletionRequestSchema, byokAgentHomeProjectionCompletionPath, AgentHomeProjectionReadbackSchema, parseMessage, checkResultDocument, MAX_MESSAGES_PER_BATCH, isServerToDaemonType, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, AgentMessagePublishPayloadSchema, AgentMessageDispositionPayloadSchema, RuntimeIdSchema, AGENT_MEMORY_PROJECTION_CAPABILITY, TERMINAL_INFERENCE_USAGE_MAX_DURATION_MS, TERMINAL_INFERENCE_USAGE_MAX_TOKENS, RESULT_DOCUMENT_MAX_BYTES, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, MESSAGE_TYPES, AGENT_MEMORY_PROJECTION_MAX_ORDERING_VALUE, AgentMemoryProjectionMutationSchema, AGENT_MEMORY_PROJECTION_MAX_REDACTED_BYTES } from '@byok-sdk/protocol';
6
6
  import net2, { createServer, createConnection } from 'net';
7
7
  import * as os4 from 'os';
8
8
  import os4__default from 'os';
9
- import { execFile, spawn } from 'child_process';
9
+ import { execFile, spawn, spawnSync } from 'child_process';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { createInterface } from 'readline';
12
12
  import { parseDeviceAssertionEnvelope, SKILL_PACK_MAX_BYTES, hasCapability, parseSkillPackManifest, checkSkillPackManifest, skillPackContentHashInput, checkSkillPackFileContent, SKILL_PACK_ENTRY_PATH, checkSkillPackEntry, isSkillPackPathSafe, DEVICE_PROOF_HEADER, contentHash as contentHash$1, TRUTH_RECORD_KINDS, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, CONTENT_HASH_PATTERN, isTenantId, nonceSigningBytes, CapabilityDeclarationSchema, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
@@ -14,7 +14,130 @@ import { promisify } from 'util';
14
14
  import * as fs14 from 'fs/promises';
15
15
  import { createRequire } from 'module';
16
16
 
17
- // src/agent-home.ts
17
+ var __defProp = Object.defineProperty;
18
+ var __getOwnPropNames = Object.getOwnPropertyNames;
19
+ var __esm = (fn, res) => function __init() {
20
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
21
+ };
22
+ var __export = (target, all) => {
23
+ for (var name in all)
24
+ __defProp(target, name, { get: all[name], enumerable: true });
25
+ };
26
+
27
+ // src/adapters/win32-job-object.ts
28
+ var win32_job_object_exports = {};
29
+ __export(win32_job_object_exports, {
30
+ Win32JobObjectFailure: () => Win32JobObjectFailure,
31
+ assignOwnedProcessToJob: () => assignOwnedProcessToJob,
32
+ isWin32JobObjectFailure: () => isWin32JobObjectFailure,
33
+ loadBindings: () => loadBindings
34
+ });
35
+ function isWin32JobObjectFailure(value) {
36
+ return value instanceof Error && value.name === "Win32JobObjectFailure";
37
+ }
38
+ function isNullHandle(value) {
39
+ return value === null || value === void 0 || value === 0n || value === 0;
40
+ }
41
+ async function loadBindings() {
42
+ if (cachedBindings !== void 0) return cachedBindings;
43
+ let koffi;
44
+ try {
45
+ const imported = await import('koffi');
46
+ koffi = imported.default ?? imported;
47
+ } catch (cause) {
48
+ throw new Win32JobObjectFailure({
49
+ step: "loadKoffi",
50
+ reason: "win32 runtime process ownership requires the koffi optional dependency, which could not be loaded"
51
+ }, { cause });
52
+ }
53
+ const kernel32 = koffi.load("kernel32.dll");
54
+ const PVOID = koffi.pointer("void");
55
+ const bind = (name, result, args) => kernel32.func("__stdcall", name, result, args);
56
+ cachedBindings = {
57
+ createJobObjectW: bind("CreateJobObjectW", PVOID, [PVOID, "str16"]),
58
+ setInformationJobObject: bind("SetInformationJobObject", "int", [PVOID, "int", PVOID, "uint32"]),
59
+ openProcess: bind("OpenProcess", PVOID, ["uint32", "int", "uint32"]),
60
+ assignProcessToJobObject: bind("AssignProcessToJobObject", "int", [PVOID, PVOID]),
61
+ closeHandle: bind("CloseHandle", "int", [PVOID]),
62
+ getLastError: bind("GetLastError", "uint32", [])
63
+ };
64
+ return cachedBindings;
65
+ }
66
+ function killOnCloseLimitInformation() {
67
+ const information = Buffer.alloc(JOBOBJECT_EXTENDED_LIMIT_SIZE);
68
+ information.writeUInt32LE(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, JOBOBJECT_EXTENDED_LIMIT_FLAGS_OFFSET);
69
+ return information;
70
+ }
71
+ function daemonJob(bindings) {
72
+ const existing = jobsByBindings.get(bindings);
73
+ if (existing !== void 0) return existing;
74
+ const job = bindings.createJobObjectW(null, null);
75
+ if (isNullHandle(job)) {
76
+ throw new Win32JobObjectFailure({
77
+ step: "CreateJobObjectW",
78
+ reason: "win32 runtime process ownership could not create the daemon kill-on-close job object",
79
+ win32Code: bindings.getLastError()
80
+ });
81
+ }
82
+ const information = killOnCloseLimitInformation();
83
+ if (bindings.setInformationJobObject(job, JobObjectExtendedLimitInformation, information, information.length) === 0) {
84
+ const win32Code = bindings.getLastError();
85
+ bindings.closeHandle(job);
86
+ throw new Win32JobObjectFailure({
87
+ step: "SetInformationJobObject",
88
+ reason: "win32 runtime process ownership could not make the daemon job object kill-on-close",
89
+ win32Code
90
+ });
91
+ }
92
+ jobsByBindings.set(bindings, job);
93
+ return job;
94
+ }
95
+ async function assignOwnedProcessToJob(pid, options) {
96
+ const bindings = options?.bindings ?? await loadBindings();
97
+ const job = daemonJob(bindings);
98
+ const processHandle = bindings.openProcess(OWNED_PROCESS_ACCESS, 0, pid);
99
+ if (isNullHandle(processHandle)) {
100
+ throw new Win32JobObjectFailure({
101
+ step: "OpenProcess",
102
+ reason: `win32 runtime process ownership could not open process ${pid} for job assignment`,
103
+ win32Code: bindings.getLastError()
104
+ });
105
+ }
106
+ try {
107
+ if (bindings.assignProcessToJobObject(job, processHandle) === 0) {
108
+ throw new Win32JobObjectFailure({
109
+ step: "AssignProcessToJobObject",
110
+ reason: `win32 runtime process ownership could not assign process ${pid} to the daemon kill-on-close job object`,
111
+ win32Code: bindings.getLastError()
112
+ });
113
+ }
114
+ } finally {
115
+ bindings.closeHandle(processHandle);
116
+ }
117
+ }
118
+ var JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, JobObjectExtendedLimitInformation, JOBOBJECT_EXTENDED_LIMIT_SIZE, JOBOBJECT_EXTENDED_LIMIT_FLAGS_OFFSET, PROCESS_SET_QUOTA, PROCESS_TERMINATE, OWNED_PROCESS_ACCESS, Win32JobObjectFailure, cachedBindings, jobsByBindings;
119
+ var init_win32_job_object = __esm({
120
+ "src/adapters/win32-job-object.ts"() {
121
+ JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 8192;
122
+ JobObjectExtendedLimitInformation = 9;
123
+ JOBOBJECT_EXTENDED_LIMIT_SIZE = 144;
124
+ JOBOBJECT_EXTENDED_LIMIT_FLAGS_OFFSET = 16;
125
+ PROCESS_SET_QUOTA = 256;
126
+ PROCESS_TERMINATE = 1;
127
+ OWNED_PROCESS_ACCESS = PROCESS_SET_QUOTA | PROCESS_TERMINATE;
128
+ Win32JobObjectFailure = class extends Error {
129
+ step;
130
+ win32Code;
131
+ constructor(input, options) {
132
+ super(input.reason, options);
133
+ this.name = "Win32JobObjectFailure";
134
+ this.step = input.step;
135
+ this.win32Code = input.win32Code;
136
+ }
137
+ };
138
+ jobsByBindings = /* @__PURE__ */ new WeakMap();
139
+ }
140
+ });
18
141
  var tmpSeq = 0;
19
142
  async function atomicWriteFile(filePath, data, options = {}) {
20
143
  const tmpPath = `${filePath}.${process.pid}-${tmpSeq++}.tmp`;
@@ -2378,6 +2501,9 @@ function parseTeamMessageReadParams(value) {
2378
2501
  if (!isRecord2(value) || !exactKeys(value, ["context", "afterSeq"]) || !contextString(value.context) || value.afterSeq !== void 0 && !safeInteger(value.afterSeq)) return void 0;
2379
2502
  return { context: value.context, ...value.afterSeq === void 0 ? {} : { afterSeq: value.afterSeq } };
2380
2503
  }
2504
+ function parseTeamNotificationSnapshotParams(value) {
2505
+ return parseTeamMessageReadParams(value);
2506
+ }
2381
2507
  function parseTeamMessageAckParams(value) {
2382
2508
  if (!isRecord2(value) || !exactKeys(value, ["context", "throughSeq"]) || !contextString(value.context) || !safeInteger(value.throughSeq)) return void 0;
2383
2509
  return { context: value.context, throughSeq: value.throughSeq };
@@ -4038,6 +4164,55 @@ function withOwnedProcessTree(options) {
4038
4164
  ...process.platform === "win32" ? { windowsHide: true } : { detached: true }
4039
4165
  };
4040
4166
  }
4167
+ var hostExitTargets = /* @__PURE__ */ new Map();
4168
+ var hostExitHandlerInstalled = false;
4169
+ function terminateOwnedTreesForHostExit() {
4170
+ for (const target of hostExitTargets.values()) {
4171
+ try {
4172
+ if (target.platform === "win32") {
4173
+ target.spawnSyncFn("taskkill", ["/PID", String(target.pid), "/T", "/F"], {
4174
+ stdio: "ignore",
4175
+ windowsHide: true
4176
+ });
4177
+ } else {
4178
+ target.kill(-target.pid, "SIGKILL");
4179
+ }
4180
+ } catch {
4181
+ }
4182
+ }
4183
+ hostExitTargets.clear();
4184
+ }
4185
+ function ensureHostExitHandler() {
4186
+ if (hostExitHandlerInstalled) return;
4187
+ hostExitHandlerInstalled = true;
4188
+ process.prependListener("exit", terminateOwnedTreesForHostExit);
4189
+ }
4190
+ var defaultJobObject = {
4191
+ async assign(pid) {
4192
+ const { assignOwnedProcessToJob: assignOwnedProcessToJob2 } = await Promise.resolve().then(() => (init_win32_job_object(), win32_job_object_exports));
4193
+ await assignOwnedProcessToJob2(pid);
4194
+ }
4195
+ };
4196
+ async function adoptOwnedProcessTree(options) {
4197
+ const pid = positivePid(options.child, options.label);
4198
+ if (pid === void 0) return;
4199
+ const platform = options.platform ?? process.platform;
4200
+ ensureHostExitHandler();
4201
+ hostExitTargets.set(options.child, {
4202
+ pid,
4203
+ label: options.label,
4204
+ platform,
4205
+ kill: options.killFn ?? defaultKill,
4206
+ spawnSyncFn: options.spawnSyncFn ?? spawnSync
4207
+ });
4208
+ const forget = () => {
4209
+ hostExitTargets.delete(options.child);
4210
+ };
4211
+ options.child.once("close", forget);
4212
+ options.child.once("exit", forget);
4213
+ if (platform !== "win32") return;
4214
+ await (options.jobObject ?? defaultJobObject).assign(pid);
4215
+ }
4041
4216
  function positivePid(child, label) {
4042
4217
  const pid = child.pid;
4043
4218
  if (pid === void 0) return void 0;
@@ -4249,21 +4424,8 @@ async function disposeOwnedProcessTree(options) {
4249
4424
  var STDERR_RING_CAPACITY = 20;
4250
4425
  var DIALOG_UI_METHODS = /* @__PURE__ */ new Set(["select", "confirm", "input", "editor"]);
4251
4426
  var PiRpcClient = class {
4252
- child;
4253
- buffer = "";
4254
- nextId = 1;
4255
- pending = /* @__PURE__ */ new Map();
4256
- eventQueue = new AsyncQueue();
4257
- closed = false;
4258
- exitError;
4259
- closedPromise;
4260
- resolveClosed;
4261
- disposalAttempt;
4262
- /** Bounded tail of recent stderr lines — pi discarded this entirely before (nothing ever read `child.stderr`), which is exactly why finding #1 (`Error: Unknown option: --session-id`, exit 1) had to be root-caused by hand instead of reading it off a thrown error. See `buildExitError`. */
4263
- stderrRing = [];
4264
- /** Count of pi RPC message types `PiSession` (pi-adapter.ts) has told us have no `AgentEvent` mapping and aren't routine bookkeeping — see `recordUnmappedFrame`. */
4265
- unmappedFrameCounts = /* @__PURE__ */ new Map();
4266
4427
  constructor(options) {
4428
+ this.options = options;
4267
4429
  const spawnFn = options.spawnFn ?? spawn;
4268
4430
  this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
4269
4431
  cwd: options.cwd,
@@ -4273,6 +4435,9 @@ var PiRpcClient = class {
4273
4435
  this.closedPromise = new Promise((resolve) => {
4274
4436
  this.resolveClosed = resolve;
4275
4437
  });
4438
+ this.adopted = this.adoptOwnedTree(options);
4439
+ this.adopted.catch(() => {
4440
+ });
4276
4441
  this.child.stdout.setEncoding("utf8");
4277
4442
  this.child.stdout.on("data", (chunk) => this.onData(chunk));
4278
4443
  this.child.stderr.setEncoding("utf8");
@@ -4284,10 +4449,37 @@ var PiRpcClient = class {
4284
4449
  this.onClosed(err);
4285
4450
  });
4286
4451
  }
4287
- /** Send a command, resolved with its correlated `response` message. */
4288
- send(command) {
4452
+ options;
4453
+ child;
4454
+ buffer = "";
4455
+ nextId = 1;
4456
+ pending = /* @__PURE__ */ new Map();
4457
+ eventQueue = new AsyncQueue();
4458
+ closed = false;
4459
+ exitError;
4460
+ closedPromise;
4461
+ resolveClosed;
4462
+ disposalAttempt;
4463
+ /** Resolves once this tree is backstopped (see `adoptOwnedProcessTree`); rejects with the adoption failure, having already terminated the tree. */
4464
+ adopted;
4465
+ /** Set before the fail-closed termination starts, so it — not the exit code of the kill we ourselves requested — becomes this client's exit error. */
4466
+ adoptionFailure;
4467
+ /** Bounded tail of recent stderr lines — pi discarded this entirely before (nothing ever read `child.stderr`), which is exactly why finding #1 (`Error: Unknown option: --session-id`, exit 1) had to be root-caused by hand instead of reading it off a thrown error. See `buildExitError`. */
4468
+ stderrRing = [];
4469
+ /** Count of pi RPC message types `PiSession` (pi-adapter.ts) has told us have no `AgentEvent` mapping and aren't routine bookkeeping — see `recordUnmappedFrame`. */
4470
+ unmappedFrameCounts = /* @__PURE__ */ new Map();
4471
+ /**
4472
+ * Send a command, resolved with its correlated `response` message.
4473
+ *
4474
+ * Adoption is awaited FIRST — this is `PiAdapter.start()`'s first awaited
4475
+ * operation, so an unbackstopped tree never receives the initial prompt and
4476
+ * never yields a session. The wait costs one settled-promise turn once the
4477
+ * tree is adopted; every later command sees it already resolved.
4478
+ */
4479
+ async send(command) {
4480
+ await this.adopted;
4289
4481
  if (this.closed) {
4290
- return Promise.reject(this.exitError ?? new Error("pi process is closed"));
4482
+ throw this.exitError ?? new Error("pi process is closed");
4291
4483
  }
4292
4484
  const id = command.id ?? `req-${this.nextId++}`;
4293
4485
  const full = { ...command, id };
@@ -4360,6 +4552,32 @@ var PiRpcClient = class {
4360
4552
  label: "pi"
4361
4553
  };
4362
4554
  }
4555
+ /**
4556
+ * Backstop this tree, or tear it down. Adoption failure is a start-time
4557
+ * precondition, not a degraded mode: the child is terminated through the one
4558
+ * disposal authority and the failure is re-thrown, which is what makes the
4559
+ * first `send()` — and therefore `PiAdapter.start()` — fail before any
4560
+ * session is published. Both cleanup attempts are best-effort because the
4561
+ * adoption failure, not a terminator's own complaint, is the reason to report.
4562
+ */
4563
+ async adoptOwnedTree(options) {
4564
+ try {
4565
+ await adoptOwnedProcessTree({
4566
+ child: this.child,
4567
+ label: "pi",
4568
+ platform: options.platform,
4569
+ jobObject: options.jobObject
4570
+ });
4571
+ } catch (cause) {
4572
+ const failure = cause instanceof Error ? cause : new Error(String(cause));
4573
+ this.adoptionFailure = failure;
4574
+ await requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
4575
+ });
4576
+ await disposeOwnedProcessTree(this.processTreeOptions()).catch(() => {
4577
+ });
4578
+ throw failure;
4579
+ }
4580
+ }
4363
4581
  onData(chunk) {
4364
4582
  this.buffer += chunk;
4365
4583
  let newlineIndex = this.buffer.indexOf("\n");
@@ -4378,6 +4596,7 @@ var PiRpcClient = class {
4378
4596
  } catch {
4379
4597
  return;
4380
4598
  }
4599
+ this.options.onFrame?.(msg);
4381
4600
  if (msg.type === "response" && typeof msg.id === "string" && this.pending.has(msg.id)) {
4382
4601
  const waiter = this.pending.get(msg.id);
4383
4602
  this.pending.delete(msg.id);
@@ -4385,11 +4604,20 @@ var PiRpcClient = class {
4385
4604
  return;
4386
4605
  }
4387
4606
  if (msg.type === "extension_ui_request" && typeof msg.id === "string" && typeof msg.method === "string") {
4388
- this.respondToExtensionUiRequest(msg.id, msg.method);
4607
+ if (this.options.extensionUi?.mode === "hold") this.options.extensionUi.onRequest(msg);
4608
+ else this.respondToExtensionUiRequest(msg.id, msg.method);
4389
4609
  return;
4390
4610
  }
4391
4611
  this.eventQueue.push(msg);
4392
4612
  }
4613
+ /** Write an explicit host-owned response; completion is a transport receipt only. */
4614
+ async respondExtensionUi(response) {
4615
+ if (this.options.extensionUi?.mode !== "hold") throw new Error("Pi interactions are not host-owned");
4616
+ await this.adopted;
4617
+ if (this.closed) throw new Error("Pi process is closed");
4618
+ await new Promise((resolve, reject) => this.child.stdin.write(`${JSON.stringify({ type: "extension_ui_response", ...response })}
4619
+ `, (error) => error ? reject(error) : resolve()));
4620
+ }
4393
4621
  /**
4394
4622
  * Answer pi's extension-UI blocking protocol headlessly (rpc.md's
4395
4623
  * "Extension UI Protocol"). Fail-closed policy, stated explicitly because
@@ -4443,9 +4671,10 @@ var PiRpcClient = class {
4443
4671
  onClosed(err) {
4444
4672
  if (this.closed) return;
4445
4673
  this.closed = true;
4446
- this.exitError = err;
4674
+ const terminal = this.adoptionFailure ?? err;
4675
+ this.exitError = terminal;
4447
4676
  this.resolveClosed();
4448
- for (const [, waiter] of this.pending) waiter.reject(err);
4677
+ for (const [, waiter] of this.pending) waiter.reject(terminal);
4449
4678
  this.pending.clear();
4450
4679
  this.eventQueue.end();
4451
4680
  }
@@ -4479,6 +4708,8 @@ var PROVIDER_CREDENTIAL_ENV_DENY_NAMES = [
4479
4708
  "TOGETHER_API_KEY",
4480
4709
  "BASETEN_API_KEY",
4481
4710
  "KIMI_API_KEY",
4711
+ "HF_TOKEN",
4712
+ "MOONSHOT_API_KEY",
4482
4713
  "MINIMAX_API_KEY",
4483
4714
  "MINIMAX_CN_API_KEY",
4484
4715
  "QWEN_TOKEN_PLAN_API_KEY",
@@ -5300,6 +5531,10 @@ var ClaudeProcessClient = class {
5300
5531
  closedPromise;
5301
5532
  resolveClosed;
5302
5533
  disposalAttempt;
5534
+ /** Resolves once this tree is backstopped (see `adoptOwnedProcessTree`); rejects with the adoption failure, having already terminated the tree. */
5535
+ adopted;
5536
+ /** Set before the fail-closed termination starts, so it — not the exit code of the kill we ourselves requested — becomes this client's exit error. */
5537
+ adoptionFailure;
5303
5538
  stderrRing = [];
5304
5539
  unmappedFrameCounts = /* @__PURE__ */ new Map();
5305
5540
  sessionId;
@@ -5314,6 +5549,9 @@ var ClaudeProcessClient = class {
5314
5549
  this.closedPromise = new Promise((resolve) => {
5315
5550
  this.resolveClosed = resolve;
5316
5551
  });
5552
+ this.adopted = this.adoptOwnedTree(options);
5553
+ this.adopted.catch(() => {
5554
+ });
5317
5555
  this.child.stdout.setEncoding("utf8");
5318
5556
  this.child.stdout.on("data", (chunk) => this.onData(chunk));
5319
5557
  this.child.stderr.setEncoding("utf8");
@@ -5357,9 +5595,10 @@ var ClaudeProcessClient = class {
5357
5595
  * same id; if the process already closed before init ever arrived,
5358
5596
  * every call rejects with that same exit error.
5359
5597
  */
5360
- waitForInit() {
5361
- if (this.sessionId !== void 0) return Promise.resolve(this.sessionId);
5362
- if (this.closed) return Promise.reject(this.exitError ?? new Error("claude process is closed"));
5598
+ async waitForInit() {
5599
+ await this.adopted;
5600
+ if (this.sessionId !== void 0) return this.sessionId;
5601
+ if (this.closed) throw this.exitError ?? new Error("claude process is closed");
5363
5602
  return new Promise((resolve, reject) => {
5364
5603
  this.initWaiter = { resolve, reject };
5365
5604
  });
@@ -5423,6 +5662,32 @@ var ClaudeProcessClient = class {
5423
5662
  label: "claude"
5424
5663
  };
5425
5664
  }
5665
+ /**
5666
+ * Backstop this tree, or tear it down. Adoption failure is a start-time
5667
+ * precondition, not a degraded mode: the child is terminated through the one
5668
+ * disposal authority and the failure is re-thrown, which is what makes
5669
+ * `waitForInit()` — and therefore `ClaudeAdapter.start()` — fail before any
5670
+ * session is published. Both cleanup attempts are best-effort because the
5671
+ * adoption failure, not a terminator's own complaint, is the reason to report.
5672
+ */
5673
+ async adoptOwnedTree(options) {
5674
+ try {
5675
+ await adoptOwnedProcessTree({
5676
+ child: this.child,
5677
+ label: "claude",
5678
+ platform: options.platform,
5679
+ jobObject: options.jobObject
5680
+ });
5681
+ } catch (cause) {
5682
+ const failure = cause instanceof Error ? cause : new Error(String(cause));
5683
+ this.adoptionFailure = failure;
5684
+ await requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
5685
+ });
5686
+ await disposeOwnedProcessTree(this.processTreeOptions()).catch(() => {
5687
+ });
5688
+ throw failure;
5689
+ }
5690
+ }
5426
5691
  onData(chunk) {
5427
5692
  this.buffer += chunk;
5428
5693
  let newlineIndex = this.buffer.indexOf("\n");
@@ -5471,9 +5736,10 @@ var ClaudeProcessClient = class {
5471
5736
  onClosed(err) {
5472
5737
  if (this.closed) return;
5473
5738
  this.closed = true;
5474
- this.exitError = err;
5739
+ const terminal = this.adoptionFailure ?? err;
5740
+ this.exitError = terminal;
5475
5741
  this.resolveClosed();
5476
- this.initWaiter?.reject(err);
5742
+ this.initWaiter?.reject(terminal);
5477
5743
  this.initWaiter = void 0;
5478
5744
  this.eventQueue.end();
5479
5745
  }
@@ -6340,6 +6606,19 @@ var CodexProcessRunner = class {
6340
6606
  closedPromise;
6341
6607
  resolveClosed;
6342
6608
  disposalAttempt;
6609
+ /** Resolves once this tree is backstopped (see `adoptOwnedProcessTree`); rejects with the adoption failure, having already terminated the tree. */
6610
+ adopted;
6611
+ /** Set before the fail-closed termination starts; `buildExitError` reports it instead of the exit status of the kill we ourselves requested. */
6612
+ adoptionFailure;
6613
+ adoption = "pending";
6614
+ /**
6615
+ * Lines parsed before adoption settled. Unlike pi and claude, this runner has
6616
+ * no first awaited operation of its own to gate on — its caller reads the
6617
+ * FIRST event as the authoritative thread id. Holding events until the tree
6618
+ * is backstopped is what keeps that caller from publishing a session for a
6619
+ * tree the job object never took.
6620
+ */
6621
+ deferredEvents = [];
6343
6622
  constructor(options) {
6344
6623
  this.onEvent = options.onEvent;
6345
6624
  const spawnFn = options.spawnFn ?? spawn;
@@ -6351,6 +6630,9 @@ var CodexProcessRunner = class {
6351
6630
  this.closedPromise = new Promise((resolve) => {
6352
6631
  this.resolveClosed = resolve;
6353
6632
  });
6633
+ this.adopted = this.adoptOwnedTree(options);
6634
+ this.adopted.catch(() => {
6635
+ });
6354
6636
  this.child.stdout.setEncoding("utf8");
6355
6637
  this.child.stdout.on("data", (chunk) => this.onData(chunk));
6356
6638
  this.child.stderr.setEncoding("utf8");
@@ -6416,8 +6698,58 @@ var CodexProcessRunner = class {
6416
6698
  label: "codex"
6417
6699
  };
6418
6700
  }
6419
- /** Builds a descriptive error folding in the exit code/signal and the stderr tail — mirrors `PiRpcClient.buildExitError`'s reasoning: a post-mortem on a failed start/resume should never need separately re-running codex by hand with a raw JSONL logger to learn why. */
6701
+ /**
6702
+ * Backstop this tree, or tear it down. Adoption failure is a start-time
6703
+ * precondition, not a degraded mode: the child is terminated through the one
6704
+ * disposal authority, every parsed line is dropped instead of delivered, and
6705
+ * the resulting close makes the caller's own `waitClosed()` race reject with
6706
+ * the adoption failure (`buildExitError`) before a thread id is published.
6707
+ * Both cleanup attempts are best-effort because the adoption failure, not a
6708
+ * terminator's own complaint, is the reason to report.
6709
+ */
6710
+ async adoptOwnedTree(options) {
6711
+ try {
6712
+ await adoptOwnedProcessTree({
6713
+ child: this.child,
6714
+ label: "codex",
6715
+ platform: options.platform,
6716
+ jobObject: options.jobObject
6717
+ });
6718
+ } catch (cause) {
6719
+ const failure = cause instanceof Error ? cause : new Error(String(cause));
6720
+ this.adoptionFailure = failure;
6721
+ this.adoption = "failed";
6722
+ this.deferredEvents.length = 0;
6723
+ await requestOwnedProcessTreeTermination(this.processTreeOptions()).catch(() => {
6724
+ });
6725
+ await disposeOwnedProcessTree(this.processTreeOptions()).catch(() => {
6726
+ });
6727
+ throw failure;
6728
+ }
6729
+ this.adoption = "adopted";
6730
+ for (const evt of this.deferredEvents.splice(0)) this.onEvent(evt);
6731
+ }
6732
+ /** Arrival-order delivery, held back until the tree is backstopped (see `deferredEvents`). */
6733
+ deliver(evt) {
6734
+ if (this.adoption === "failed") return;
6735
+ if (this.adoption === "pending") {
6736
+ this.deferredEvents.push(evt);
6737
+ return;
6738
+ }
6739
+ this.onEvent(evt);
6740
+ }
6741
+ /**
6742
+ * Builds a descriptive error folding in the exit code/signal and the stderr
6743
+ * tail — mirrors `PiRpcClient.buildExitError`'s reasoning: a post-mortem on a
6744
+ * failed start/resume should never need separately re-running codex by hand
6745
+ * with a raw JSONL logger to learn why.
6746
+ *
6747
+ * A tree this runner could not backstop is the one exception: that process
6748
+ * exited because THIS runner killed it, so `exit code=null, signal=SIGKILL`
6749
+ * plus an empty stderr tail would bury the only reason anyone can act on.
6750
+ */
6420
6751
  buildExitError(context) {
6752
+ if (this.adoptionFailure !== void 0) return this.adoptionFailure;
6421
6753
  const parts = [`${context} (exit code=${this.exitCode}, signal=${this.exitSignal})`];
6422
6754
  if (this.stderrRing.length > 0) {
6423
6755
  parts.push(`stderr: ${this.stderrRing.join(" | ")}`);
@@ -6445,7 +6777,7 @@ var CodexProcessRunner = class {
6445
6777
  if (!parsed || typeof parsed !== "object" || typeof parsed.type !== "string") {
6446
6778
  return;
6447
6779
  }
6448
- this.onEvent(parsed);
6780
+ this.deliver(parsed);
6449
6781
  }
6450
6782
  onStderr(chunk) {
6451
6783
  for (const rawLine of chunk.split("\n")) {
@@ -9478,6 +9810,28 @@ var LocalTeamWorkspace = class {
9478
9810
  return Object.freeze({ accepted: true, durable: true, workspaceId: state.definition.workspaceId, memberId: input.lease.memberId, seq, messageId: message.messageId, message: publicValue });
9479
9811
  });
9480
9812
  }
9813
+ /** Metadata-only operator notification view. Never advances delivery or acknowledgement. */
9814
+ async notificationSnapshot(input) {
9815
+ assertLeaseShape(input?.lease);
9816
+ if (input.afterSeq !== void 0) assertAfterSeq(input.afterSeq);
9817
+ return this.enqueue(async () => {
9818
+ const state = await this.resolveLease(await this.load(), input.lease);
9819
+ const acknowledgedThroughSeq = state.receipts[input.lease.memberId].acknowledgedThroughSeq;
9820
+ const after = Math.max(input.afterSeq ?? 0, acknowledgedThroughSeq);
9821
+ let latestPeerSeq = null;
9822
+ for (const message of state.messages) {
9823
+ if (message.seq > after && message.senderMemberId !== input.lease.memberId) latestPeerSeq = message.seq;
9824
+ }
9825
+ return Object.freeze({
9826
+ workspaceId: state.definition.workspaceId,
9827
+ memberId: input.lease.memberId,
9828
+ registryRevision: state.definition.revision,
9829
+ expiresAt: input.lease.expiresAt,
9830
+ acknowledgedThroughSeq,
9831
+ latestPeerSeq
9832
+ });
9833
+ });
9834
+ }
9481
9835
  async readMessages(input) {
9482
9836
  assertLeaseShape(input?.lease);
9483
9837
  if (input?.afterSeq !== void 0) assertAfterSeq(input.afterSeq);
@@ -9893,9 +10247,10 @@ function isSafeNonnegativeInteger(value) {
9893
10247
  function validateTrustedEventsPage(events, requestedCursor, pageCursor) {
9894
10248
  let previousTaskSeq;
9895
10249
  for (const raw of events) {
9896
- if (typeof raw !== "object" || raw === null) continue;
10250
+ if (typeof raw !== "object" || raw === null) throw new Error("events page contains an unidentifiable message");
9897
10251
  const { type, seq } = raw;
9898
- if (typeof type !== "string" || !type.startsWith("task.")) continue;
10252
+ if (typeof type !== "string") throw new Error("events page message has no authoritative type");
10253
+ if (type.startsWith("conn.")) continue;
9899
10254
  if (!isSafeNonnegativeInteger(seq)) {
9900
10255
  throw new Error("events poll task seq is not a safe nonnegative integer");
9901
10256
  }
@@ -9913,10 +10268,10 @@ function validateTrustedEventsPage(events, requestedCursor, pageCursor) {
9913
10268
  previousTaskSeq = seq;
9914
10269
  }
9915
10270
  }
9916
- function extractSkippableSeq(raw) {
10271
+ function extractExecutableSeq(raw) {
9917
10272
  if (typeof raw !== "object" || raw === null) return void 0;
9918
10273
  const { type, seq } = raw;
9919
- if (typeof type !== "string" || !type.startsWith("task.")) return void 0;
10274
+ if (typeof type !== "string" || type.startsWith("conn.")) return void 0;
9920
10275
  return isSafeNonnegativeInteger(seq) ? seq : void 0;
9921
10276
  }
9922
10277
  var LongPollClient = class {
@@ -10103,27 +10458,14 @@ var LongPollClient = class {
10103
10458
  try {
10104
10459
  envelope = parseMessage(raw);
10105
10460
  } catch (err) {
10106
- if (err instanceof UnknownMessageTypeError) {
10107
- const skippableSeq = extractSkippableSeq(raw);
10108
- if (skippableSeq !== void 0) {
10109
- this.opts.onSkippedSeq?.(skippableSeq);
10110
- acceptedAnyEntry = true;
10111
- }
10112
- } else {
10113
- const failedSeq = extractSkippableSeq(raw);
10114
- if (failedSeq !== void 0) {
10115
- hadValidationFailureThisBatch = true;
10116
- this.opts.onValidationFailedSeq?.(failedSeq);
10117
- if (!this.warnedValidationFailureSeqs.has(failedSeq)) {
10118
- if (this.warnedValidationFailureSeqs.size > MAX_TRACKED_VALIDATION_FAILURE_WARNINGS) {
10119
- this.warnedValidationFailureSeqs.clear();
10120
- }
10121
- this.warnedValidationFailureSeqs.add(failedSeq);
10122
- console.warn(
10123
- `[byok/client] long-poll: a recognized message type at seq=${failedSeq} failed payload validation \u2014 skipped for this batch, cursor frozen so the server keeps redelivering it until a corrected version arrives:`,
10124
- err
10125
- );
10126
- }
10461
+ const failedSeq = extractExecutableSeq(raw);
10462
+ if (failedSeq !== void 0) {
10463
+ hadValidationFailureThisBatch = true;
10464
+ this.opts.onValidationFailedSeq?.(failedSeq);
10465
+ if (!this.warnedValidationFailureSeqs.has(failedSeq)) {
10466
+ if (this.warnedValidationFailureSeqs.size > MAX_TRACKED_VALIDATION_FAILURE_WARNINGS) this.warnedValidationFailureSeqs.clear();
10467
+ this.warnedValidationFailureSeqs.add(failedSeq);
10468
+ console.warn(`[byok/client] long-poll: unknown or invalid executable message at seq=${failedSeq}; cursor frozen without durable disposition`, err);
10127
10469
  }
10128
10470
  }
10129
10471
  continue;
@@ -10184,7 +10526,7 @@ function sleep(ms, signal) {
10184
10526
 
10185
10527
  // src/daemon/connection-manager.ts
10186
10528
  function isCursorEnvelopeType(type) {
10187
- return type.startsWith("task.") || type === "agent.egress.ack" || type === "agent.content.read" || type === "agent.home.projection";
10529
+ return isServerToDaemonType(type) && !type.startsWith("conn.");
10188
10530
  }
10189
10531
  function createConnectionHelloEnvelope(opts, getCursor) {
10190
10532
  const configuredToolsets = opts.getConfiguredToolsets?.();
@@ -10224,16 +10566,7 @@ var ConnectionManager = class {
10224
10566
  onPollFailure: () => this.noteDisconnected(),
10225
10567
  onRevoked: () => this.enterRevoked(),
10226
10568
  onReplayCursorTooOld: (error) => this.enterReplayCursorTooOld(error),
10227
- // M4 Phase 4 (version-negotiation drill fix): a batch entry
10228
- // LongPollClient couldn't parse into a known Envelope at all (an
10229
- // unrecognized message type) still needs its cursor/watermark
10230
- // advanced past it, exactly like a successfully-processed envelope
10231
- // would — see `noteSkippedSeq`'s own doc comment.
10232
- onSkippedSeq: (seq) => this.noteSkippedSeq(seq),
10233
- // Finding R1: a batch entry LongPollClient recognized the TYPE of but
10234
- // whose payload failed validation — a genuine delivery failure at
10235
- // that seq, engaged the same way a thrown handler failure is (see
10236
- // `noteValidationFailure`'s own doc comment).
10569
+ // Unknown and malformed executable work use the same ordered stall.
10237
10570
  onValidationFailedSeq: (seq) => this.noteValidationFailure(seq),
10238
10571
  // Finding P2 (Fix 2a): lets the long-poll loop distinguish "this
10239
10572
  // non-empty batch was a stalled backlog re-pull, no cursor progress
@@ -10375,6 +10708,7 @@ var ConnectionManager = class {
10375
10708
  */
10376
10709
  send(envelope) {
10377
10710
  this.outbox.push(envelope);
10711
+ this.opts.onOutboundQueued?.(envelope);
10378
10712
  void this.drainOutbox();
10379
10713
  }
10380
10714
  /** Publish a fresh local configuration snapshot while this daemon is running. */
@@ -10407,12 +10741,22 @@ var ConnectionManager = class {
10407
10741
  while (pending.length > 0) {
10408
10742
  if (this.stopped || this.revoked) return pending.flat();
10409
10743
  const segment = pending.shift();
10410
- const result = await this.longPoll.postBatch(segment);
10411
- if (result === void 0) return [...segment, ...pending.flat()];
10412
- if ((result.rejected ?? 0) === 0) continue;
10413
- if (segment.length === 1) {
10414
- this.quarantineRejectedOutbound(segment[0], "inbound_rejected");
10415
- continue;
10744
+ try {
10745
+ this.opts.beforeOutboundPost?.(segment);
10746
+ const result = await this.longPoll.postBatch(segment);
10747
+ if (result === void 0) return [...segment, ...pending.flat()];
10748
+ if ((result.rejected ?? 0) === 0) {
10749
+ await this.opts.onOutboundAccepted?.(segment);
10750
+ continue;
10751
+ }
10752
+ if (segment.length === 1) {
10753
+ await this.opts.onOutboundRejected?.(segment[0]);
10754
+ this.quarantineRejectedOutbound(segment[0], "inbound_rejected");
10755
+ continue;
10756
+ }
10757
+ } catch (error) {
10758
+ console.error("[byok/client] outbound durable disposition failed; retaining exact batch", error);
10759
+ return [...segment, ...pending.flat()];
10416
10760
  }
10417
10761
  const midpoint = Math.floor(segment.length / 2);
10418
10762
  pending.unshift(segment.slice(0, midpoint), segment.slice(midpoint));
@@ -10632,6 +10976,10 @@ var ConnectionManager = class {
10632
10976
  * documented idempotent (protocol §9).
10633
10977
  */
10634
10978
  deliver(envelope) {
10979
+ if (!isServerToDaemonType(envelope.type)) {
10980
+ if (typeof envelope.seq === "number") this.noteValidationFailure(envelope.seq);
10981
+ return false;
10982
+ }
10635
10983
  const tracked = isCursorEnvelopeType(envelope.type) && typeof envelope.seq === "number";
10636
10984
  const watermark = this.dedupWatermark();
10637
10985
  if (tracked && watermark !== void 0 && envelope.seq <= watermark) return false;
@@ -10701,122 +11049,7 @@ var ConnectionManager = class {
10701
11049
  if (tracked) this.inFlightSeqs.delete(seq);
10702
11050
  }
10703
11051
  }
10704
- /**
10705
- * M4 Phase 4 (version-negotiation drill fix): `LongPollClient` calls this
10706
- * for a batch entry it could not parse into a known `Envelope` at all (an
10707
- * unrecognized message type (see `long-poll-transport.ts`'s own doc
10708
- * comment on `parseLooseEventsPollResponse`) but which still carried a numeric,
10709
- * task-class envelope-level `seq` (the caller only invokes this for a
10710
- * `task.`-prefixed type — see `long-poll-transport.ts`'s own
10711
- * `extractSkippableSeq`; `conn.*`-shaped or type-less entries never reach
10712
- * here at all, mirroring F2's "conn.* is never cursor-tracked" rule).
10713
- * There is no real `Envelope` to hand to a handler — a genuinely
10714
- * unrecognized type has nothing this build could ever act on.
10715
- *
10716
- * GATEKEEPER-CAUGHT REGRESSION (fixed here): this used to call
10717
- * `advanceCursor(seq)` DIRECTLY, synchronously, the instant a skip was
10718
- * detected in `LongPollClient.loop()`'s per-entry for-loop. That is NOT
10719
- * "instantaneous and race-free" the way the previous version of this
10720
- * comment claimed — the hazard was never the skip racing against itself,
10721
- * it was the skip racing AHEAD of an EARLIER real envelope in the SAME
10722
- * batch that is still in flight on `processingChain` (`deliver()`, above,
10723
- * only ever CHAINS `process()` onto that promise chain — it never awaits
10724
- * it before returning). Concretely, batch `[real seq1, unknown seq2]`:
10725
- * `deliver(seq1)` chains `process(seq1)` but returns immediately without
10726
- * running it; the for-loop then reaches `seq2` and (pre-fix) called
10727
- * `advanceCursor(2)` synchronously, BEFORE `process(seq1)` had even
10728
- * started, let alone failed. If `seq1`'s handler then failed,
10729
- * `stalledAtSeq` became 1 — but the durable cursor was already 2, so
10730
- * `dedupWatermark()` returned 2, and every future redelivery of seq1 was
10731
- * dedup-dropped as "already past the cursor" forever: permanent envelope
10732
- * loss, exactly the F3 bug class the whole `stalledAtSeq`/frozen-watermark
10733
- * mechanism exists to prevent.
10734
- *
10735
- * Fix: the cursor-advancing half is now CHAINED onto `processingChain`
10736
- * too, exactly like `process()`'s own post-handler bookkeeping — so it
10737
- * only ever runs once every earlier envelope already queued ahead of it
10738
- * has fully settled (success or failure), and can observe `stalledAtSeq`'s
10739
- * REAL, up-to-date value rather than whatever it happened to be at the
10740
- * instant the skip was first noticed. The guard mirrors `process()`'s own
10741
- * success-path guard exactly: never advance past a still-unresolved
10742
- * earlier failure, unless (degenerate, cannot really happen for a skip)
10743
- * this exact seq IS the stalled one.
10744
- *
10745
- * `noteDelivered` (the eager, in-memory watermark) stays UNCHAINED —
10746
- * called immediately, unconditionally, regardless of `stalledAtSeq` —
10747
- * matching `deliver()`'s own eager, unconditional call for a real
10748
- * envelope: its only job is "don't re-dispatch something already handed off,"
10749
- * independent of outcome, and that property does not depend on FIFO
10750
- * ordering the way the DURABLE cursor does.
10751
- *
10752
- * Deliberately NO top-level `dedupWatermark() <= seq` early-return before
10753
- * queuing the chained callback (an earlier draft of this fix had one, and
10754
- * it was itself subtly wrong): `deliveredSeq` can already reflect a seq
10755
- * from the FIRST time it was ever seen, while the DURABLE cursor is still
10756
- * behind it because a stall intervened before that seq's chained
10757
- * advancement ran — a pre-check keyed on `deliveredSeq` would then
10758
- * wrongly treat a LATER redelivery of the same seq (arriving once the
10759
- * stall has since cleared) as "already accounted for" and never queue
10760
- * another attempt, permanently stranding the cursor one seq short. Always
10761
- * queuing is safe and cheap: `advanceCursor`'s own `seq <= this.cursor`
10762
- * guard already makes a genuinely-redundant call a no-op, so there is no
10763
- * correctness reason to short-circuit earlier, only a (here, unnecessary)
10764
- * micro-optimization one.
10765
- */
10766
- noteSkippedSeq(seq) {
10767
- this.noteDelivered(seq);
10768
- this.processingChain = this.processingChain.then(async () => {
10769
- if (this.stalledAtSeq === void 0 || seq === this.stalledAtSeq) {
10770
- try {
10771
- await this.advanceCursor(seq);
10772
- if (this.stalledAtSeq === seq) this.stalledAtSeq = void 0;
10773
- } catch (err) {
10774
- if (this.stalledAtSeq === void 0) this.stalledAtSeq = seq;
10775
- console.error(
10776
- `[byok/client] unknown task seq=${seq} could not durably persist its cursor; cursor left unadvanced for redelivery:`,
10777
- err
10778
- );
10779
- }
10780
- }
10781
- });
10782
- }
10783
- /**
10784
- * Finding R1 (cross-model re-review — was NOT-CLOSED against F1):
10785
- * `LongPollClient` calls this for a batch entry whose `type` it
10786
- * recognized but whose payload failed schema validation
10787
- * ({@link EnvelopeValidationError}) — a genuine delivery failure at that
10788
- * seq, unlike `noteSkippedSeq`'s forward-compat case. Deliberately mirrors
10789
- * `process()`'s own catch block (`if (tracked && this.stalledAtSeq ===
10790
- * undefined) this.stalledAtSeq = envelope.seq;`) as closely as possible:
10791
- * the SAME "only the lowest unresolved failure holds the stall" rule, the
10792
- * SAME resulting freeze of `dedupWatermark()` at the durable cursor
10793
- * (protocol §9 keeps this seq alive), and — because it's the SAME
10794
- * `stalledAtSeq` field `process()`'s own post-success guard already
10795
- * checks — anything ELSE delivered after this seq (same batch or a later
10796
- * one) is automatically held back from advancing the cursor too, with
10797
- * zero changes needed to `process()` itself.
10798
- *
10799
- * Chained onto `processingChain` for exactly the reason `noteSkippedSeq`
10800
- * documents for its own identical chaining (see that method's sibling
10801
- * doc comment on `LongPollClient`, "GATEKEEPER-CAUGHT REGRESSION"): an
10802
- * EARLIER real envelope in the SAME batch may still be in flight on that
10803
- * FIFO chain when this is called (`deliver()` only ever chains
10804
- * `process()` onto it, never awaits before returning) — mutating
10805
- * `stalledAtSeq` synchronously here could race ahead of that still-
10806
- * unresolved earlier envelope. Chaining instead guarantees this only
10807
- * takes effect once every earlier-queued envelope has already settled,
10808
- * and reads `stalledAtSeq`'s real, up-to-date value rather than whatever
10809
- * it happened to be the instant the failure was first noticed.
10810
- *
10811
- * No `noteDelivered` call here (contrast `noteSkippedSeq`, which does
10812
- * call it): a validation-failed entry never becomes a real `Envelope` and
10813
- * never reaches `deliver()`, so it was never "delivered" in the eager
10814
- * in-memory-watermark sense that field tracks — there is nothing for it
10815
- * to eagerly mark. Once a corrected redelivery of this exact seq DOES
10816
- * arrive as a real envelope, it flows through the ordinary `deliver()`
10817
- * path (which calls `noteDelivered` itself) and, on success, clears the
10818
- * stall via `process()`'s own existing logic — no special-casing needed.
10819
- */
11052
+ /** Serialized with handler completion, so invalid work cannot be acked by later success. */
10820
11053
  noteValidationFailure(seq) {
10821
11054
  this.processingChain = this.processingChain.then(() => {
10822
11055
  if (this.stalledAtSeq === void 0) this.stalledAtSeq = seq;
@@ -11203,11 +11436,12 @@ var DaemonObserver = class {
11203
11436
  * where those are actually reported from.
11204
11437
  */
11205
11438
  handleInboundEnvelope(envelope) {
11206
- if (envelope.type !== "task.offer" && envelope.type !== "task.offer_with_toolsets" && envelope.type !== "task.offer_for_agent") return;
11207
- const taskId = envelope.task_id;
11439
+ if (!isTaskOfferType(envelope.type)) return;
11440
+ const offer = envelope;
11441
+ const taskId = offer.task_id;
11208
11442
  if (this.taskInfo.has(taskId)) return;
11209
- this.upsertTask(taskId, { state: "Offered", runtime: envelope.payload.runtime });
11210
- this.emit({ kind: "offered", ts: nowIso(), taskId, runtime: envelope.payload.runtime });
11443
+ this.upsertTask(taskId, { state: "Offered", runtime: offer.payload.runtime });
11444
+ this.emit({ kind: "offered", ts: nowIso(), taskId, runtime: offer.payload.runtime });
11211
11445
  }
11212
11446
  /**
11213
11447
  * Feed a raw OUTBOUND (daemon -> server) envelope — this is where every
@@ -11736,6 +11970,7 @@ CREATE INDEX IF NOT EXISTS journal_transition_task ON journal_transition (task_i
11736
11970
  CREATE TABLE IF NOT EXISTS journal_terminal (
11737
11971
  task_id TEXT PRIMARY KEY REFERENCES journal_task (task_id),
11738
11972
  terminal_type TEXT NOT NULL,
11973
+ bytes TEXT NOT NULL,
11739
11974
  payload_hash TEXT NOT NULL,
11740
11975
  truth_state TEXT NOT NULL,
11741
11976
  attempt INTEGER NOT NULL,
@@ -11890,6 +12125,11 @@ var SqliteLocalTaskJournal = class {
11890
12125
  const quarantinePath = quarantineDatabase(options.storeDir, this.#dbPath, reason, this.#clock());
11891
12126
  throw new JournalCorruptError(this.#dbPath, quarantinePath, reason, { cause: err });
11892
12127
  }
12128
+ const terminalColumns = db.prepare("PRAGMA table_info(journal_terminal)").all();
12129
+ if (!terminalColumns.some((column) => column.name === "bytes")) {
12130
+ db.close();
12131
+ throw new JournalUnavailableError("hash-only journal format cannot replay terminals; preserve/export this journal and resolve it before starting the new execution-recovery format");
12132
+ }
11893
12133
  this.#db = db;
11894
12134
  try {
11895
12135
  secureJournalFilePermissions(this.#dbPath);
@@ -11979,10 +12219,13 @@ var SqliteLocalTaskJournal = class {
11979
12219
  }
11980
12220
  return this.#enqueue("appendEnvelope", () => {
11981
12221
  this.#fault("append:before-begin");
11982
- return this.#transaction(() => {
12222
+ const committed = this.#transaction(() => {
11983
12223
  const existing = this.#db.prepare("SELECT receipt FROM journal_idempotency WHERE scope = ? AND key = ?").get("envelope", record3.envelopeId);
11984
12224
  if (existing) {
11985
12225
  const prior = JSON.parse(existing.receipt);
12226
+ if (prior.bytesHash !== record3.bytesHash) throw new Error("journal envelope identity collision");
12227
+ const priorIdentity = this.#db.prepare("SELECT tenant_id, product_id, device_id FROM journal_envelope WHERE envelope_id = ?").get(record3.envelopeId);
12228
+ this.#assertIdentity(priorIdentity, record3.identity);
11986
12229
  return { ...prior, created: false };
11987
12230
  }
11988
12231
  const committedAt = this.#clock().toISOString();
@@ -12004,6 +12247,16 @@ var SqliteLocalTaskJournal = class {
12004
12247
  );
12005
12248
  this.#fault("append:after-envelope");
12006
12249
  if (record3.opensTask && record3.taskId !== void 0) {
12250
+ const task = this.#db.prepare("SELECT * FROM journal_task WHERE task_id = ?").get(record3.taskId);
12251
+ if (task) {
12252
+ this.#assertIdentity(task, record3.identity);
12253
+ const original = this.#db.prepare("SELECT bytes FROM journal_envelope WHERE envelope_id = ?").get(task.envelope_id);
12254
+ const oldOffer = JSON.parse(original.bytes);
12255
+ const newOffer = JSON.parse(record3.bytes);
12256
+ if (oldOffer.type !== newOffer.type || JSON.stringify(oldOffer.payload) !== JSON.stringify(newOffer.payload)) {
12257
+ throw new Error("journal task identity reused with a different execution contract");
12258
+ }
12259
+ }
12007
12260
  this.#db.prepare(
12008
12261
  `INSERT OR IGNORE INTO journal_task
12009
12262
  (task_id, envelope_id, tenant_id, product_id, device_id, seq, local_state, created_at, updated_at)
@@ -12032,6 +12285,8 @@ var SqliteLocalTaskJournal = class {
12032
12285
  this.#fault("append:before-commit");
12033
12286
  return { ...receipt, created: true };
12034
12287
  });
12288
+ this.#fault("append:after-commit");
12289
+ return committed;
12035
12290
  });
12036
12291
  }
12037
12292
  recordAdmission(record3) {
@@ -12102,17 +12357,22 @@ var SqliteLocalTaskJournal = class {
12102
12357
  */
12103
12358
  recordTerminal(record3) {
12104
12359
  return this.#enqueue("recordTerminal", () => {
12360
+ if (typeof record3.bytes !== "string" || journalHash(record3.bytes) !== record3.payloadHash) {
12361
+ throw new Error("terminal bytes do not match their canonical hash");
12362
+ }
12363
+ if (byteLength2(record3.bytes) > this.#maxRecordBytes) {
12364
+ throw new JournalRecordTooLargeError("terminal bytes", byteLength2(record3.bytes), this.#maxRecordBytes);
12365
+ }
12105
12366
  this.#transaction(() => {
12106
12367
  this.#requireTask(record3.taskId, "recordTerminal");
12107
- const existing = this.#db.prepare("SELECT payload_hash FROM journal_terminal WHERE task_id = ?").get(record3.taskId);
12108
- if (existing === void 0) {
12109
- this.#db.prepare(
12110
- `INSERT INTO journal_terminal
12111
- (task_id, terminal_type, payload_hash, truth_state, attempt, last_error, recorded_at, updated_at)
12112
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
12113
- ).run(
12368
+ const existing = this.#db.prepare("SELECT * FROM journal_terminal WHERE task_id = ?").get(record3.taskId);
12369
+ if (!existing) {
12370
+ this.#db.prepare(`INSERT INTO journal_terminal
12371
+ (task_id, terminal_type, bytes, payload_hash, truth_state, attempt, last_error, recorded_at, updated_at)
12372
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
12114
12373
  record3.taskId,
12115
12374
  record3.terminalType,
12375
+ record3.bytes,
12116
12376
  record3.payloadHash,
12117
12377
  record3.truthState,
12118
12378
  record3.attempt,
@@ -12121,11 +12381,98 @@ var SqliteLocalTaskJournal = class {
12121
12381
  record3.recordedAt
12122
12382
  );
12123
12383
  this.#db.prepare("UPDATE journal_task SET local_state = ?, updated_at = ? WHERE task_id = ?").run(`terminal:${record3.terminalType}`, record3.recordedAt, record3.taskId);
12124
- } else if (existing.payload_hash === record3.payloadHash) {
12384
+ } else if (existing.payload_hash === record3.payloadHash && existing.bytes === record3.bytes && existing.truth_state !== "confirmed") {
12125
12385
  this.#db.prepare("UPDATE journal_terminal SET truth_state = ?, attempt = ?, last_error = ?, updated_at = ? WHERE task_id = ?").run(record3.truthState, record3.attempt, record3.lastError ?? null, record3.recordedAt, record3.taskId);
12126
12386
  }
12387
+ if (record3.recovery !== void 0 && !existing) {
12388
+ this.#db.prepare("UPDATE journal_task SET recovery_marker = ?, updated_at = ? WHERE task_id = ?").run(JSON.stringify({ ...record3.recovery, occurredAt: record3.recovery.occurredAt ?? record3.recordedAt }), record3.recordedAt, record3.taskId);
12389
+ this.#fault("recovery:before-commit");
12390
+ }
12127
12391
  this.#fault("terminal:before-commit");
12128
12392
  });
12393
+ if (record3.recovery !== void 0) this.#fault("recovery:after-commit");
12394
+ this.#fault("terminal:after-commit");
12395
+ });
12396
+ }
12397
+ #assertIdentity(row, identity) {
12398
+ if (row.tenant_id !== identity.tenantId || row.product_id !== identity.productId || row.device_id !== identity.deviceId) {
12399
+ throw new Error("journal enrollment identity mismatch; refusing cross-enrollment execution or terminal replay");
12400
+ }
12401
+ }
12402
+ #taskRow(row) {
12403
+ if (typeof row.envelope_bytes !== "string" || journalHash(row.envelope_bytes) !== row.envelope_hash) {
12404
+ throw new Error("durable offer bytes/hash mismatch");
12405
+ }
12406
+ return {
12407
+ taskId: row.task_id,
12408
+ envelopeId: row.envelope_id,
12409
+ seq: Number(row.seq),
12410
+ identity: { tenantId: row.tenant_id, productId: row.product_id, deviceId: row.device_id },
12411
+ localState: row.recovery_marker !== null && !String(row.local_state).startsWith("terminal:") ? "interrupted" : row.local_state,
12412
+ ...row.claimed_runtime == null ? {} : { claimedRuntime: row.claimed_runtime },
12413
+ ...row.workspace_ref == null ? {} : { workspaceRef: row.workspace_ref },
12414
+ updatedAt: row.updated_at,
12415
+ envelopeBytes: row.envelope_bytes
12416
+ };
12417
+ }
12418
+ readTask(taskId, identity) {
12419
+ return this.#enqueue("readTask", () => {
12420
+ const row = this.#db.prepare(`SELECT t.*, e.bytes AS envelope_bytes, e.bytes_hash AS envelope_hash FROM journal_task t
12421
+ JOIN journal_envelope e ON e.envelope_id = t.envelope_id WHERE t.task_id = ?`).get(taskId);
12422
+ if (!row) return void 0;
12423
+ this.#assertIdentity(row, identity);
12424
+ return this.#taskRow(row);
12425
+ });
12426
+ }
12427
+ listRecoveryTasks(identity) {
12428
+ return this.#enqueue("listRecoveryTasks", () => {
12429
+ const rows = this.#db.prepare(`SELECT t.*, e.bytes AS envelope_bytes, e.bytes_hash AS envelope_hash FROM journal_task t
12430
+ JOIN journal_envelope e ON e.envelope_id = t.envelope_id
12431
+ LEFT JOIN journal_terminal term ON term.task_id = t.task_id
12432
+ WHERE term.task_id IS NULL ORDER BY t.seq, t.task_id`).all();
12433
+ return rows.map((row) => {
12434
+ this.#assertIdentity(row, identity);
12435
+ return this.#taskRow(row);
12436
+ });
12437
+ });
12438
+ }
12439
+ listPendingTerminals(identity) {
12440
+ return this.#enqueue("listPendingTerminals", () => {
12441
+ const rows = this.#db.prepare(`SELECT term.*, t.tenant_id, t.product_id, t.device_id FROM journal_terminal term
12442
+ JOIN journal_task t ON t.task_id = term.task_id WHERE term.truth_state <> 'confirmed'
12443
+ ORDER BY term.recorded_at, term.task_id`).all();
12444
+ return rows.map((row) => {
12445
+ this.#assertIdentity(row, identity);
12446
+ if (typeof row.bytes !== "string" || journalHash(row.bytes) !== row.payload_hash) throw new Error("durable terminal bytes/hash mismatch");
12447
+ return {
12448
+ taskId: row.task_id,
12449
+ terminalType: row.terminal_type,
12450
+ bytes: row.bytes,
12451
+ payloadHash: row.payload_hash,
12452
+ truthState: row.truth_state,
12453
+ attempt: Number(row.attempt),
12454
+ recordedAt: row.recorded_at,
12455
+ ...row.last_error === null ? {} : { lastError: row.last_error }
12456
+ };
12457
+ });
12458
+ });
12459
+ }
12460
+ confirmTerminal(taskId, payloadHash) {
12461
+ return this.#terminalDisposition(taskId, payloadHash, "confirmed");
12462
+ }
12463
+ rejectTerminal(taskId, payloadHash, reason) {
12464
+ return this.#terminalDisposition(taskId, payloadHash, "failed", reason);
12465
+ }
12466
+ #terminalDisposition(taskId, payloadHash, state, reason) {
12467
+ return this.#enqueue("terminalDisposition", () => {
12468
+ this.#transaction(() => {
12469
+ const row = this.#db.prepare("SELECT payload_hash FROM journal_terminal WHERE task_id = ?").get(taskId);
12470
+ if (!row || row.payload_hash !== payloadHash) throw new Error("terminal acknowledgement does not match durable terminal");
12471
+ this.#db.prepare(`UPDATE journal_terminal SET truth_state = ?, last_error = ?, updated_at = ?
12472
+ WHERE task_id = ? AND truth_state <> 'confirmed'`).run(state, reason ?? null, this.#clock().toISOString(), taskId);
12473
+ this.#fault("confirm:before-commit");
12474
+ });
12475
+ this.#fault("confirm:after-commit");
12129
12476
  });
12130
12477
  }
12131
12478
  /**
@@ -12142,8 +12489,9 @@ var SqliteLocalTaskJournal = class {
12142
12489
  return this.#enqueue("listRecoverable", () => {
12143
12490
  const rows = this.#db.prepare(
12144
12491
  `SELECT t.task_id, t.envelope_id, t.seq, t.tenant_id, t.product_id, t.device_id,
12145
- t.local_state, t.claimed_runtime, t.workspace_ref, t.updated_at
12492
+ t.local_state, t.claimed_runtime, t.workspace_ref, t.updated_at, e.bytes AS envelope_bytes, e.bytes_hash AS envelope_hash
12146
12493
  FROM journal_task t
12494
+ JOIN journal_envelope e ON e.envelope_id = t.envelope_id
12147
12495
  LEFT JOIN journal_terminal term ON term.task_id = t.task_id
12148
12496
  WHERE t.recovery_marker IS NULL
12149
12497
  AND term.task_id IS NULL
@@ -12165,7 +12513,8 @@ var SqliteLocalTaskJournal = class {
12165
12513
  localState: row.local_state,
12166
12514
  ...claimedRuntime === null ? {} : { claimedRuntime },
12167
12515
  ...workspaceRef === null ? {} : { workspaceRef },
12168
- updatedAt: row.updated_at
12516
+ updatedAt: row.updated_at,
12517
+ envelopeBytes: row.envelope_bytes
12169
12518
  };
12170
12519
  });
12171
12520
  });
@@ -12822,6 +13171,204 @@ function createStatfsFreeBytesProvider(dir) {
12822
13171
  return Number(stats.bavail) * Number(stats.bsize);
12823
13172
  };
12824
13173
  }
13174
+ var DEFAULT_MAX_INLINE_EVENT_BYTES = 64 * 1024;
13175
+ var MIN_MAX_INLINE_EVENT_BYTES = 4096;
13176
+ var MAX_UNSTORED_REASON_BYTES = 512;
13177
+ var MAX_SPILL_DESCRIPTOR_BYTES = 768;
13178
+ function byteLength3(value) {
13179
+ return Buffer.byteLength(value, "utf8");
13180
+ }
13181
+ function safeStringify(value) {
13182
+ try {
13183
+ return JSON.stringify(value);
13184
+ } catch {
13185
+ return void 0;
13186
+ }
13187
+ }
13188
+ function utf8Width(codePoint) {
13189
+ if (codePoint < 128) return 1;
13190
+ if (codePoint < 2048) return 2;
13191
+ if (codePoint < 65536) return 3;
13192
+ return 4;
13193
+ }
13194
+ function utf8Prefix(value, maxBytes) {
13195
+ if (maxBytes <= 0) return "";
13196
+ let bytes = 0;
13197
+ let index = 0;
13198
+ while (index < value.length) {
13199
+ const codePoint = value.codePointAt(index);
13200
+ const width = utf8Width(codePoint);
13201
+ if (bytes + width > maxBytes) break;
13202
+ bytes += width;
13203
+ index += codePoint > 65535 ? 2 : 1;
13204
+ }
13205
+ return value.slice(0, index);
13206
+ }
13207
+ function utf8Suffix(value, maxBytes) {
13208
+ if (maxBytes <= 0) return "";
13209
+ let bytes = 0;
13210
+ let index = value.length;
13211
+ while (index > 0) {
13212
+ let step = 1;
13213
+ const last = value.charCodeAt(index - 1);
13214
+ if (last >= 56320 && last <= 57343 && index >= 2) {
13215
+ const previous = value.charCodeAt(index - 2);
13216
+ if (previous >= 55296 && previous <= 56319) step = 2;
13217
+ }
13218
+ const codePoint = value.codePointAt(index - step);
13219
+ const width = utf8Width(codePoint);
13220
+ if (bytes + width > maxBytes) break;
13221
+ bytes += width;
13222
+ index -= step;
13223
+ }
13224
+ return value.slice(index);
13225
+ }
13226
+ function boundReasonText(raw) {
13227
+ const collapsed = raw.replace(/\s+/g, " ").trim();
13228
+ const fallback = "blob upload failed with a non-descriptive error";
13229
+ if (collapsed.length === 0) return fallback;
13230
+ let budget = MAX_UNSTORED_REASON_BYTES;
13231
+ for (; ; ) {
13232
+ const reason = utf8Prefix(collapsed, budget);
13233
+ if (reason.length === 0) return fallback;
13234
+ const encoded = byteLength3(JSON.stringify(reason)) - 2;
13235
+ if (encoded <= MAX_UNSTORED_REASON_BYTES) return reason;
13236
+ budget = Math.min(Math.floor(budget * MAX_UNSTORED_REASON_BYTES / encoded), budget - 1);
13237
+ }
13238
+ }
13239
+ function boundedUnstoredReason(error) {
13240
+ const raw = error instanceof Error && error.message.length > 0 ? `${error.name}: ${error.message}` : String(error);
13241
+ return boundReasonText(raw);
13242
+ }
13243
+ async function spillOversizedEvent(event, deps) {
13244
+ if (event.type !== "tool_use" && event.type !== "tool_result") return event;
13245
+ const serializedEvent = safeStringify(event);
13246
+ if (serializedEvent === void 0) {
13247
+ deps.log?.(
13248
+ `task ${deps.taskId}: ${event.type} event cannot be JSON-serialized (a BigInt, a cycle, or a throwing toJSON in its payload); forwarding it unchanged and unbounded`
13249
+ );
13250
+ return event;
13251
+ }
13252
+ if (byteLength3(serializedEvent) <= deps.maxInlineBytes) return event;
13253
+ const spillable = event;
13254
+ const field = spillable.type === "tool_use" ? "input" : "output";
13255
+ const value = spillable.type === "tool_use" ? spillable.input : spillable.output;
13256
+ if (value === void 0) {
13257
+ deps.log?.(
13258
+ `task ${deps.taskId}: ${spillable.type} event is ${byteLength3(serializedEvent)} bytes, over the ${deps.maxInlineBytes}-byte inline cap, but carries no ${field} to spill; forwarding unchanged`
13259
+ );
13260
+ return event;
13261
+ }
13262
+ const serialized = safeStringify(value);
13263
+ if (serialized === void 0) {
13264
+ deps.log?.(
13265
+ `task ${deps.taskId}: ${spillable.type}.${field} is not JSON-serializable; forwarding the event unchanged`
13266
+ );
13267
+ return event;
13268
+ }
13269
+ const totalBytes = byteLength3(serialized);
13270
+ const skeleton = safeStringify({ ...spillable, [field]: { preview: { head: "", tail: "" } } });
13271
+ if (skeleton === void 0) {
13272
+ deps.log?.(
13273
+ `task ${deps.taskId}: ${spillable.type} event cannot be JSON-serialized once its ${field} is replaced by a preview; forwarding it unchanged and unbounded`
13274
+ );
13275
+ return event;
13276
+ }
13277
+ if (byteLength3(skeleton) + MAX_SPILL_DESCRIPTOR_BYTES > deps.maxInlineBytes) {
13278
+ deps.log?.(
13279
+ `task ${deps.taskId}: ${spillable.type} event exceeds the ${deps.maxInlineBytes}-byte inline cap independently of its ${field} (${byteLength3(skeleton)} bytes without it, plus up to ${MAX_SPILL_DESCRIPTOR_BYTES} bytes of spill descriptor); forwarding unchanged`
13280
+ );
13281
+ return event;
13282
+ }
13283
+ const digest4 = createHash("sha256").update(serialized, "utf8").digest("hex");
13284
+ let stored;
13285
+ let storedBlobId;
13286
+ try {
13287
+ const blob = await deps.blobClient.uploadArtifact(serialized, "application/json", {
13288
+ idempotencyKey: `spill_${deps.taskId}_${digest4}`,
13289
+ ...deps.signal === void 0 ? {} : { signal: deps.signal }
13290
+ });
13291
+ stored = { blob };
13292
+ storedBlobId = blob.blobId;
13293
+ } catch (error) {
13294
+ const unstoredReason = boundedUnstoredReason(error);
13295
+ stored = { unstoredReason };
13296
+ deps.log?.(
13297
+ `task ${deps.taskId}: ${totalBytes}-byte ${spillable.type}.${field} could not be stored, only a preview will reach the wire \u2014 ${unstoredReason}`
13298
+ );
13299
+ }
13300
+ const descriptorFor = (omittedBytes, half) => ({
13301
+ field,
13302
+ totalBytes,
13303
+ omittedBytes,
13304
+ contentType: "application/json",
13305
+ ...half
13306
+ });
13307
+ const bound = (half) => {
13308
+ const overheadJson = safeStringify({
13309
+ ...spillable,
13310
+ [field]: { preview: { head: "", tail: "" } },
13311
+ spill: descriptorFor(totalBytes, half)
13312
+ });
13313
+ if (overheadJson === void 0) return void 0;
13314
+ const overhead = byteLength3(overheadJson);
13315
+ let headBudget = Math.max(0, Math.ceil((deps.maxInlineBytes - overhead) / 2));
13316
+ let tailBudget = Math.max(0, Math.floor((deps.maxInlineBytes - overhead) / 2));
13317
+ for (; ; ) {
13318
+ const head = utf8Prefix(serialized, headBudget);
13319
+ const tail = utf8Suffix(serialized.slice(head.length), tailBudget);
13320
+ const retained = byteLength3(head) + byteLength3(tail);
13321
+ const result = {
13322
+ ...spillable,
13323
+ [field]: { preview: { head, tail } },
13324
+ spill: descriptorFor(totalBytes - retained, half)
13325
+ };
13326
+ const resultJson = safeStringify(result);
13327
+ if (resultJson === void 0) return void 0;
13328
+ const resultBytes = byteLength3(resultJson);
13329
+ if (resultBytes <= deps.maxInlineBytes) return { result, resultBytes, overhead };
13330
+ const headBytes = byteLength3(head);
13331
+ const tailBytes = byteLength3(tail);
13332
+ if (headBytes === 0 && tailBytes === 0) return { result, resultBytes, overhead };
13333
+ const excess = resultBytes - deps.maxInlineBytes;
13334
+ const dropHead = headBytes === 0 ? 0 : Math.min(headBytes, Math.max(1, Math.ceil(excess / 2)));
13335
+ const dropTail = tailBytes === 0 ? 0 : Math.min(tailBytes, Math.max(dropHead === 0 ? 1 : 0, excess - dropHead));
13336
+ if (dropHead === 0 && dropTail === 0) return { result, resultBytes, overhead };
13337
+ headBudget = headBytes - dropHead;
13338
+ tailBudget = tailBytes - dropTail;
13339
+ }
13340
+ };
13341
+ let bounded2 = bound(stored);
13342
+ if (bounded2 === void 0) {
13343
+ deps.log?.(
13344
+ `task ${deps.taskId}: ${spillable.type} event cannot be JSON-serialized once its ${field} is replaced by a preview and a spill descriptor; forwarding it unchanged and unbounded`
13345
+ );
13346
+ return event;
13347
+ }
13348
+ if (bounded2.resultBytes > deps.maxInlineBytes && storedBlobId !== void 0) {
13349
+ const unstoredReason = boundReasonText(
13350
+ `spill locator too large for maxInlineBytes: blobId ${storedBlobId.length} chars`
13351
+ );
13352
+ deps.log?.(
13353
+ `task ${deps.taskId}: ${totalBytes}-byte ${spillable.type}.${field} was stored, but its spill locator does not fit the ${deps.maxInlineBytes}-byte inline cap (descriptor overhead ${bounded2.overhead} bytes); shipping the preview with unstoredReason instead \u2014 ${unstoredReason}`
13354
+ );
13355
+ stored = { unstoredReason };
13356
+ storedBlobId = void 0;
13357
+ bounded2 = bound(stored);
13358
+ if (bounded2 === void 0) {
13359
+ deps.log?.(
13360
+ `task ${deps.taskId}: ${spillable.type} event cannot be JSON-serialized once its ${field} is replaced by a preview and a spill descriptor; forwarding it unchanged and unbounded`
13361
+ );
13362
+ return event;
13363
+ }
13364
+ }
13365
+ if (bounded2.resultBytes > deps.maxInlineBytes) {
13366
+ throw new Error(
13367
+ `event spill failed to bound a ${spillable.type} event: replacement is ${bounded2.resultBytes} bytes against a ${deps.maxInlineBytes}-byte cap (${field} was ${totalBytes} bytes, descriptor overhead ${bounded2.overhead} bytes)`
13368
+ );
13369
+ }
13370
+ return bounded2.result;
13371
+ }
12825
13372
 
12826
13373
  // src/daemon/environment.ts
12827
13374
  var BASE_PLATFORM_ALLOWLIST = [
@@ -14448,6 +14995,10 @@ var TaskRunner = class {
14448
14995
  get maxTaskOutputBytes() {
14449
14996
  return this.deps.maxTaskOutputBytes ?? DEFAULT_MAX_TASK_OUTPUT_BYTES;
14450
14997
  }
14998
+ /** Effective per-event inline ceiling for this daemon — see `DaemonConfig.maxInlineEventBytes`. */
14999
+ get maxInlineEventBytes() {
15000
+ return this.deps.maxInlineEventBytes ?? DEFAULT_MAX_INLINE_EVENT_BYTES;
15001
+ }
14451
15002
  /** WP0: effective per-canonical-Agent-home Attempt cap — see {@link DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME}. */
14452
15003
  get maxConcurrentMutableSessionsPerAgentHome() {
14453
15004
  return this.deps.maxConcurrentMutableSessionsPerAgentHome ?? DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME;
@@ -15128,6 +15679,7 @@ var TaskRunner = class {
15128
15679
  },
15129
15680
  forwardedEnvironmentNames: Object.freeze(Object.keys(env).sort())
15130
15681
  });
15682
+ await this.deps.beforeClaim?.(taskId, manifest.descriptor.id);
15131
15683
  this.deps.send(
15132
15684
  createEnvelope(
15133
15685
  "task.claim",
@@ -15614,9 +16166,17 @@ var TaskRunner = class {
15614
16166
  }
15615
16167
  async pump(active) {
15616
16168
  try {
15617
- for await (const event of active.session.events) {
16169
+ for await (let event of active.session.events) {
15618
16170
  if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
15619
16171
  if (this.tasks.get(active.taskId) !== active) return;
16172
+ event = await spillOversizedEvent(event, {
16173
+ maxInlineBytes: this.maxInlineEventBytes,
16174
+ blobClient: this.deps.blobClient,
16175
+ taskId: active.taskId,
16176
+ signal: active.blobAbort.signal,
16177
+ log: (message) => console.error(`[byok/client] ${message}`)
16178
+ });
16179
+ if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
15620
16180
  active.outputBytesSoFar += estimateEventBytes(event);
15621
16181
  if (active.outputBytesSoFar > this.maxTaskOutputBytes) {
15622
16182
  active.batcher.flush();
@@ -18536,6 +19096,7 @@ function delay2(ms) {
18536
19096
  });
18537
19097
  }
18538
19098
  function terminalKindOf(type) {
19099
+ if (type === "task.decline") return "declined";
18539
19100
  if (type === "task.complete") return "complete";
18540
19101
  if (type === "task.fail") return "failed";
18541
19102
  if (type === "task.cancelled") return "cancelled";
@@ -18551,7 +19112,7 @@ function toJournalEnvelopeRecord(envelope, identity) {
18551
19112
  bytes,
18552
19113
  bytesHash: journalHash(bytes),
18553
19114
  receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
18554
- opensTask: envelope.type === "task.offer" || envelope.type === "task.offer_with_toolsets" || envelope.type === "task.offer_for_agent"
19115
+ opensTask: isTaskOfferType(envelope.type)
18555
19116
  };
18556
19117
  }
18557
19118
  function isRuntimeId(id) {
@@ -18740,6 +19301,11 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
18740
19301
  `DaemonConfig.maxTaskOutputBytes must be a positive number (or omitted to use the ${DEFAULT_MAX_TASK_OUTPUT_BYTES}-byte default) \u2014 got ${config.maxTaskOutputBytes}. Pass Number.POSITIVE_INFINITY to explicitly disable the cap; 0 or a negative number is rejected rather than silently treated as "disabled".`
18741
19302
  );
18742
19303
  }
19304
+ if (config.maxInlineEventBytes !== void 0 && !(Number.isSafeInteger(config.maxInlineEventBytes) && config.maxInlineEventBytes >= MIN_MAX_INLINE_EVENT_BYTES)) {
19305
+ throw new Error(
19306
+ `DaemonConfig.maxInlineEventBytes must be a safe integer of at least ${MIN_MAX_INLINE_EVENT_BYTES} (or omitted to use the ${DEFAULT_MAX_INLINE_EVENT_BYTES}-byte default) \u2014 got ${config.maxInlineEventBytes}. A smaller cap cannot hold the spill descriptor that documents the omission, and there is no "disabled" value: an unbounded tool_use.input/tool_result.output is what this cap exists to prevent.`
19307
+ );
19308
+ }
18743
19309
  validateProgressBatcherOptions(config.progressBatch);
18744
19310
  if (config.agentEgress !== void 0 && config.agentHome === void 0) {
18745
19311
  throw new Error("DaemonConfig.agentEgress requires DaemonConfig.agentHome for the per-Agent local spool");
@@ -19034,19 +19600,31 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
19034
19600
  `[byok/client] WARNING: permissionDefaults.workspaceRoot ("${config.permissionDefaults.workspaceRoot}") is configured, but no bundled runtime adapter (pi/claude/codex) enforces PermissionPolicy.workspaceRoot \u2014 every adapter confines a task to ctx.workspaceDir instead. This ceiling value has no enforcement effect; see docs/security.md's "Workspace confinement is a convention, not a sandbox" section.`
19035
19601
  );
19036
19602
  }
19037
- if (activeJournal) {
19038
- const recoverable = await activeJournal.listRecoverable();
19039
- for (const task of recoverable) {
19040
- await activeJournal.markRecovered(task.taskId, {
19041
- disposition: "interrupted",
19042
- reason: `daemon restarted while this task was in local state "${task.localState}"; local runtime sessions do not survive the process`
19603
+ const journalIdentity = activeJournal ? { tenantId: record3.tenantId, productId: config.productId, deviceId: record3.deviceId } : void 0;
19604
+ if (activeJournal && journalIdentity) {
19605
+ const durableCursor = await cursorStore.load(config.serverUrl, record3.deviceId) ?? 0;
19606
+ for (const task of await activeJournal.listRecoveryTasks(journalIdentity)) {
19607
+ if (task.localState === "received" && task.seq > durableCursor) continue;
19608
+ const offer = decodeEnvelope(task.envelopeBytes);
19609
+ if (!isTaskOfferType(offer.type) || offer.task_id !== task.taskId) throw new Error("invalid durable recovery offer binding");
19610
+ const payload = offer.payload;
19611
+ const terminal = createEnvelope("task.fail", {
19612
+ reason: "daemon_interrupted",
19613
+ retryable: false,
19614
+ ...payload.agentRef === void 0 ? {} : { agentRef: payload.agentRef }
19615
+ }, { taskId: task.taskId });
19616
+ const bytes = encodeEnvelope(terminal);
19617
+ await activeJournal.recordTerminal({
19618
+ taskId: task.taskId,
19619
+ terminalType: "failed",
19620
+ bytes,
19621
+ payloadHash: journalHash(bytes),
19622
+ truthState: "pending",
19623
+ attempt: 1,
19624
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
19625
+ recovery: { disposition: "interrupted", reason: "daemon_interrupted" }
19043
19626
  });
19044
19627
  }
19045
- if (recoverable.length > 0) {
19046
- console.warn(
19047
- `[byok/client] local journal recovery: ${recoverable.length} task(s) were interrupted by the previous shutdown and have been marked (not resumed, not deleted): ${recoverable.map((task) => task.taskId).join(", ")}`
19048
- );
19049
- }
19050
19628
  }
19051
19629
  if (activeOwnedPressureEngine) {
19052
19630
  await activeOwnedPressureEngine.tick();
@@ -19082,35 +19660,64 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
19082
19660
  tenantId: record3.tenantId,
19083
19661
  deviceId: record3.deviceId
19084
19662
  }) : void 0;
19085
- const journalIdentity = config.hostedJournal ? { tenantId: record3.tenantId, productId: config.productId, deviceId: record3.deviceId } : void 0;
19086
- const sendSanitizedEnvelope = activeJournal && journalIdentity ? (envelope) => {
19087
- observer.handleOutboundEnvelope(envelope);
19663
+ const sendSanitizedEnvelope = (envelope) => {
19088
19664
  const terminalKind = terminalKindOf(envelope.type);
19089
- const taskId = envelope.task_id;
19090
- if (terminalKind === void 0 || taskId === void 0) {
19665
+ if (!activeJournal || !journalIdentity || terminalKind === void 0 || envelope.task_id === void 0) {
19666
+ observer.handleOutboundEnvelope(envelope);
19091
19667
  connection?.send(envelope);
19092
19668
  return;
19093
19669
  }
19094
- const payloadHash = journalHash(encodeEnvelope(envelope));
19095
- journalTerminalTail = journalTerminalTail.then(
19096
- () => activeJournal.recordTerminal({
19097
- taskId,
19098
- terminalType: terminalKind,
19099
- payloadHash,
19100
- truthState: "pending",
19101
- attempt: 1,
19102
- recordedAt: (/* @__PURE__ */ new Date()).toISOString()
19103
- })
19104
- ).catch((err) => {
19105
- console.warn(
19106
- `[byok/client] could not record task ${taskId}'s terminal in the local journal (sending it anyway): ${err instanceof Error ? err.message : String(err)}`
19107
- );
19108
- }).then(() => {
19109
- connection?.send(envelope);
19670
+ const taskId = envelope.task_id;
19671
+ const bytes = encodeEnvelope(envelope);
19672
+ journalTerminalTail = journalTerminalTail.catch(() => void 0).then(async () => {
19673
+ try {
19674
+ await activeJournal.recordTerminal({
19675
+ taskId,
19676
+ terminalType: terminalKind,
19677
+ bytes,
19678
+ payloadHash: journalHash(bytes),
19679
+ truthState: "pending",
19680
+ attempt: 1,
19681
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString()
19682
+ });
19683
+ const terminal = (await activeJournal.listPendingTerminals(journalIdentity)).find((row) => row.taskId === taskId);
19684
+ if (!terminal || terminal.truthState !== "pending") return;
19685
+ const durable = decodeEnvelope(terminal.bytes);
19686
+ overrides.executionRecoveryFault?.("terminal:before-send");
19687
+ observer.handleOutboundEnvelope(durable);
19688
+ connection?.send(durable);
19689
+ } catch (error) {
19690
+ if (!(error instanceof JournalRecordTooLargeError)) throw error;
19691
+ if (!activeJournal || !journalIdentity) throw error;
19692
+ const task = await activeJournal.readTask(taskId, journalIdentity);
19693
+ if (!task) throw new Error(`missing durable task ${taskId}`);
19694
+ const offer = decodeEnvelope(task.envelopeBytes);
19695
+ if (!isTaskOfferType(offer.type) || offer.task_id !== taskId) throw new Error(`invalid durable offer binding for ${taskId}`);
19696
+ const offerPayload = offer.payload;
19697
+ const failure = createEnvelope("task.fail", {
19698
+ reason: "terminal_result_too_large",
19699
+ retryable: false,
19700
+ ...offerPayload.agentRef === void 0 ? {} : { agentRef: offerPayload.agentRef }
19701
+ }, { taskId });
19702
+ const failureBytes = encodeEnvelope(failure);
19703
+ await activeJournal.recordTerminal({
19704
+ taskId,
19705
+ terminalType: "failed",
19706
+ bytes: failureBytes,
19707
+ payloadHash: journalHash(failureBytes),
19708
+ truthState: "pending",
19709
+ attempt: 1,
19710
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString()
19711
+ });
19712
+ const durableFailure = (await activeJournal.listPendingTerminals(journalIdentity)).find((row) => row.taskId === taskId);
19713
+ if (!durableFailure || durableFailure.truthState !== "pending") return;
19714
+ const decoded = decodeEnvelope(durableFailure.bytes);
19715
+ observer.handleOutboundEnvelope(decoded);
19716
+ connection?.send(decoded);
19717
+ }
19718
+ }).catch((error) => {
19719
+ console.error("[byok/client] terminal durability failed; execution remains unsettled", error);
19110
19720
  });
19111
- } : (envelope) => {
19112
- observer.handleOutboundEnvelope(envelope);
19113
- connection?.send(envelope);
19114
19721
  };
19115
19722
  const sendEnvelope = (candidate) => {
19116
19723
  if (candidate.task_id === void 0 || runner?.usesAgentEgress(candidate.task_id) !== true) {
@@ -19149,6 +19756,11 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
19149
19756
  // before is the entire integration; `task-runner.ts` itself is
19150
19757
  // untouched. See `observer.ts`'s module doc comment.
19151
19758
  send: sendEnvelope,
19759
+ ...activeJournal === void 0 ? {} : {
19760
+ beforeClaim: async (taskId, runtime) => {
19761
+ await activeJournal.recordAdmission({ taskId, admitted: true, claimedRuntime: runtime, decidedAt: (/* @__PURE__ */ new Date()).toISOString() });
19762
+ }
19763
+ },
19152
19764
  blobClient,
19153
19765
  batcherOptions: egressBatcherOptions,
19154
19766
  agentEgress,
@@ -19187,6 +19799,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
19187
19799
  shutdownInterruptTimeoutMs: overrides.shutdown?.taskInterruptTimeoutMs,
19188
19800
  // M5 batch-3 (workstream 2): see DaemonConfig.maxTaskOutputBytes's own doc comment — already validated above.
19189
19801
  maxTaskOutputBytes: config.maxTaskOutputBytes,
19802
+ // See DaemonConfig.maxInlineEventBytes's own doc comment — already validated above.
19803
+ maxInlineEventBytes: config.maxInlineEventBytes,
19190
19804
  // additive-minor (`task.complete.document`): passed through verbatim,
19191
19805
  // absent when unconfigured — see `DaemonConfig.resultDocument`'s own
19192
19806
  // doc comment. Spread rather than assigned so an unconfigured daemon
@@ -19406,7 +20020,13 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
19406
20020
  if (await handleAgentContentReadEnvelope(envelope)) return;
19407
20021
  activePressureEngine?.assertAckCriticalAllowed();
19408
20022
  await activeJournal.appendEnvelope(toJournalEnvelopeRecord(envelope, journalIdentity));
19409
- return runner?.handleEnvelope(envelope) ?? Promise.resolve();
20023
+ if (isTaskOfferType(envelope.type) && envelope.task_id !== void 0) {
20024
+ const task = await activeJournal.readTask(envelope.task_id, journalIdentity);
20025
+ if (!task) throw new Error("durable offer has no execution record");
20026
+ if (task.localState !== "received") return;
20027
+ }
20028
+ await runner?.handleEnvelope(envelope);
20029
+ await journalTerminalTail;
19410
20030
  } : (envelope) => {
19411
20031
  if (tenantRebinding) {
19412
20032
  return Promise.reject(new Error("tenant enrollment is being re-paired; inbound work is blocked until restart"));
@@ -19418,6 +20038,25 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
19418
20038
  if (envelope.type === "agent.content.read") return handleAgentContentReadEnvelope(envelope).then(() => void 0);
19419
20039
  return runner?.handleEnvelope(envelope) ?? Promise.resolve();
19420
20040
  },
20041
+ onOutboundQueued: (envelope) => {
20042
+ if (terminalKindOf(envelope.type) !== void 0) overrides.executionRecoveryFault?.("terminal:queued");
20043
+ },
20044
+ beforeOutboundPost: (envelopes) => {
20045
+ if (envelopes.some((envelope) => terminalKindOf(envelope.type) !== void 0)) overrides.executionRecoveryFault?.("outbound:before-post");
20046
+ },
20047
+ onOutboundAccepted: async (envelopes) => {
20048
+ if (!activeJournal) return;
20049
+ for (const envelope of envelopes) {
20050
+ if (terminalKindOf(envelope.type) === void 0 || envelope.task_id === void 0) continue;
20051
+ overrides.executionRecoveryFault?.("outbound:after-ack");
20052
+ await activeJournal.confirmTerminal(envelope.task_id, journalHash(encodeEnvelope(envelope)));
20053
+ }
20054
+ },
20055
+ onOutboundRejected: async (envelope) => {
20056
+ if (activeJournal && terminalKindOf(envelope.type) !== void 0 && envelope.task_id !== void 0) {
20057
+ await activeJournal.rejectTerminal(envelope.task_id, journalHash(encodeEnvelope(envelope)), "inbound_rejected");
20058
+ }
20059
+ },
19421
20060
  onStateChange: (state) => {
19422
20061
  const wasSettled = connectionState === "open";
19423
20062
  connectionState = state;
@@ -19435,6 +20074,18 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
19435
20074
  });
19436
20075
  }
19437
20076
  });
20077
+ if (activeJournal && journalIdentity) {
20078
+ for (const terminal of await activeJournal.listPendingTerminals(journalIdentity)) {
20079
+ if (terminal.truthState !== "pending") continue;
20080
+ const envelope = decodeEnvelope(terminal.bytes);
20081
+ if (envelope.task_id !== terminal.taskId || terminalKindOf(envelope.type) !== terminal.terminalType) {
20082
+ throw new Error("durable terminal execution binding mismatch");
20083
+ }
20084
+ overrides.executionRecoveryFault?.("terminal:before-send");
20085
+ observer.handleOutboundEnvelope(envelope);
20086
+ connection.send(envelope);
20087
+ }
20088
+ }
19438
20089
  await connection.start();
19439
20090
  await connection.waitForConnection();
19440
20091
  runner.retryRecoveredAgentMessages();
@@ -19822,6 +20473,11 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
19822
20473
  if (!parsed) throw new ControlError("bad_request", "team_messages.ack requires exactly {context,throughSeq}");
19823
20474
  return teamWorkspaces.ackMessages({ lease: decodeTeamMemberContext(parsed.context), throughSeq: parsed.throughSeq });
19824
20475
  },
20476
+ "team_notifications.snapshot": (params) => {
20477
+ const parsed = parseTeamNotificationSnapshotParams(params);
20478
+ if (!parsed) throw new ControlError("bad_request", "team_notifications.snapshot requires exactly {context,afterSeq?}");
20479
+ return teamWorkspaces.notificationSnapshot({ lease: decodeTeamMemberContext(parsed.context), ...parsed.afterSeq === void 0 ? {} : { afterSeq: parsed.afterSeq } });
20480
+ },
19825
20481
  "team_messages.inspect": (params) => {
19826
20482
  const parsed = parseTeamMessageInspectParams(params);
19827
20483
  if (!parsed) throw new ControlError("bad_request", "team_messages.inspect requires exactly {workspaceId,afterSeq?}");