@byok-sdk/client 0.14.0 → 0.16.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 (47) hide show
  1. package/README.md +15 -0
  2. package/dist/adapters/codex/codex-adapter.d.ts +2 -0
  3. package/dist/adapters/codex/process-runner.d.ts +16 -17
  4. package/dist/adapters/detect-outcome.d.ts +18 -0
  5. package/dist/adapters/index.d.ts +1 -1
  6. package/dist/adapters/index.js +189 -50
  7. package/dist/adapters/index.js.map +1 -1
  8. package/dist/bin/byok-agent-memory-mcp.js +29 -0
  9. package/dist/bin/byok-agent-memory-mcp.js.map +1 -1
  10. package/dist/bin/byok-agent-message-mcp.js +29 -0
  11. package/dist/bin/byok-agent-message-mcp.js.map +1 -1
  12. package/dist/bin/byok-agent-team-mcp.js +29 -0
  13. package/dist/bin/byok-agent-team-mcp.js.map +1 -1
  14. package/dist/bin/byok-agent.js +3026 -2122
  15. package/dist/bin/byok-agent.js.map +1 -1
  16. package/dist/bin/byok-approval-mcp.js +29 -0
  17. package/dist/bin/byok-approval-mcp.js.map +1 -1
  18. package/dist/bin/byok-mcp-env.d.ts +2 -0
  19. package/dist/bin/byok-mcp-env.js +36 -0
  20. package/dist/bin/byok-mcp-env.js.map +1 -0
  21. package/dist/bin/commands/doctor.d.ts +3 -0
  22. package/dist/bin/mcp-env-launcher.d.ts +5 -0
  23. package/dist/bin/runtime-probe.d.ts +6 -7
  24. package/dist/daemon/admission-wait.d.ts +2 -0
  25. package/dist/daemon/agent-egress-spool.d.ts +1 -0
  26. package/dist/daemon/agent-message-outbox.d.ts +17 -3
  27. package/dist/daemon/artifact-read.d.ts +7 -0
  28. package/dist/daemon/connection-manager.d.ts +18 -77
  29. package/dist/daemon/create-daemon.d.ts +11 -1
  30. package/dist/daemon/runtime-start.d.ts +3 -0
  31. package/dist/daemon/store.d.ts +2 -0
  32. package/dist/daemon/task-runner.d.ts +23 -0
  33. package/dist/daemon/terminal-commit-queue.d.ts +21 -0
  34. package/dist/daemon/terminal-identity.d.ts +6 -0
  35. package/dist/diagnostics/device-doctor.d.ts +46 -0
  36. package/dist/diagnostics/diagnostics.d.ts +5 -90
  37. package/dist/diagnostics/operator-actions.d.ts +63 -0
  38. package/dist/diagnostics/types.d.ts +92 -0
  39. package/dist/index.d.ts +5 -1
  40. package/dist/index.js +2575 -470
  41. package/dist/index.js.map +1 -1
  42. package/dist/runtime-detection.d.ts +3 -0
  43. package/dist/runtime-failure.d.ts +6 -0
  44. package/dist/sdk-reserved-helper-host.d.ts +1 -1
  45. package/dist/types.d.ts +20 -10
  46. package/dist/util/durable-jsonl.d.ts +12 -0
  47. package/package.json +4 -4
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'child_process';
3
+
4
+ async function runMcpEnvLauncher() {
5
+ const key = process.env.BYOK_MCP_ENV_KEY;
6
+ if (!key || !/^BYOK_MCP_PAYLOAD_[A-F0-9]{32}$/.test(key)) throw new Error("invalid MCP environment binding");
7
+ const encoded = process.env[key];
8
+ if (!encoded) throw new Error("missing sealed MCP environment");
9
+ let config;
10
+ try {
11
+ config = JSON.parse(encoded);
12
+ if (!config || typeof config.command !== "string" || !config.command || config.args !== void 0 && (!Array.isArray(config.args) || config.args.some((value) => typeof value !== "string")) || config.env !== void 0 && (typeof config.env !== "object" || config.env === null || Array.isArray(config.env) || Object.values(config.env).some((value) => typeof value !== "string"))) throw new Error();
13
+ } catch {
14
+ throw new Error("invalid sealed MCP environment");
15
+ }
16
+ const env = { ...process.env };
17
+ delete env.BYOK_MCP_ENV_KEY;
18
+ for (const name of Object.keys(env)) if (name.startsWith("BYOK_MCP_PAYLOAD_")) delete env[name];
19
+ Object.assign(env, config.env);
20
+ await new Promise((resolve, reject) => {
21
+ const child = spawn(config.command, config.args ?? [], { env, stdio: "inherit", windowsHide: true });
22
+ child.once("error", () => reject(new Error("MCP child could not be spawned")));
23
+ child.once("close", (code) => {
24
+ if (code === 0) resolve();
25
+ else reject(new Error("MCP child exited unsuccessfully"));
26
+ });
27
+ });
28
+ }
29
+
30
+ // src/bin/byok-mcp-env.ts
31
+ runMcpEnvLauncher().catch(() => {
32
+ process.stderr.write("byok-mcp-env: MCP launch failed\n");
33
+ process.exitCode = 1;
34
+ });
35
+ //# sourceMappingURL=byok-mcp-env.js.map
36
+ //# sourceMappingURL=byok-mcp-env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/bin/mcp-env-launcher.ts","../../src/bin/byok-mcp-env.ts"],"names":[],"mappings":";;;AAMA,eAAsB,iBAAA,GAAmC;AACvD,EAAA,MAAM,GAAA,GAAM,QAAQ,GAAA,CAAI,gBAAA;AACxB,EAAA,IAAI,CAAC,GAAA,IAAO,CAAC,iCAAA,CAAkC,IAAA,CAAK,GAAG,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,iCAAiC,CAAA;AAC3G,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,GAAA,CAAI,GAAG,CAAA;AAC/B,EAAA,IAAI,CAAC,OAAA,EAAS,MAAM,IAAI,MAAM,gCAAgC,CAAA;AAC9D,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAA,GAAS,IAAA,CAAK,MAAM,OAAO,CAAA;AAC3B,IAAA,IAAI,CAAC,MAAA,IAAU,OAAO,MAAA,CAAO,OAAA,KAAY,YAAY,CAAC,MAAA,CAAO,OAAA,IACvD,MAAA,CAAO,IAAA,KAAS,KAAA,CAAA,KAAc,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAA,CAAO,IAAI,CAAA,IAAK,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,CAAA,KAAA,KAAS,OAAO,KAAA,KAAU,QAAQ,CAAA,CAAA,IAChH,MAAA,CAAO,QAAQ,KAAA,CAAA,KAAc,OAAO,MAAA,CAAO,GAAA,KAAQ,QAAA,IAAY,MAAA,CAAO,QAAQ,IAAA,IAAQ,KAAA,CAAM,OAAA,CAAQ,MAAA,CAAO,GAAG,CAAA,IAC7G,OAAO,MAAA,CAAO,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,CAAA,KAAA,KAAS,OAAO,KAAA,KAAU,QAAQ,CAAA,CAAA,EAAK,MAAM,IAAI,KAAA,EAAM;AAAA,EAC/F,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,MAAM,gCAAgC,CAAA;AAAA,EAClD;AACA,EAAA,MAAM,GAAA,GAAM,EAAE,GAAG,OAAA,CAAQ,GAAA,EAAI;AAC7B,EAAA,OAAO,GAAA,CAAI,gBAAA;AACX,EAAA,KAAA,MAAW,IAAA,IAAQ,MAAA,CAAO,IAAA,CAAK,GAAG,CAAA,EAAG,IAAI,IAAA,CAAK,UAAA,CAAW,mBAAmB,CAAA,EAAG,OAAO,GAAA,CAAI,IAAI,CAAA;AAC9F,EAAA,MAAA,CAAO,MAAA,CAAO,GAAA,EAAK,MAAA,CAAO,GAAG,CAAA;AAC7B,EAAA,MAAM,IAAI,OAAA,CAAc,CAAC,OAAA,EAAS,MAAA,KAAW;AAC3C,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,MAAA,CAAO,OAAA,EAAS,OAAO,IAAA,IAAQ,EAAC,EAAG,EAAE,GAAA,EAAK,KAAA,EAAO,SAAA,EAAW,WAAA,EAAa,MAAM,CAAA;AACnG,IAAA,KAAA,CAAM,IAAA,CAAK,SAAS,MAAM,MAAA,CAAO,IAAI,KAAA,CAAM,gCAAgC,CAAC,CAAC,CAAA;AAC7E,IAAA,KAAA,CAAM,IAAA,CAAK,SAAS,CAAA,IAAA,KAAQ;AAC1B,MAAA,IAAI,IAAA,KAAS,GAAG,OAAA,EAAQ;AAAA,WACnB,MAAA,CAAO,IAAI,KAAA,CAAM,iCAAiC,CAAC,CAAA;AAAA,IAC1D,CAAC,CAAA;AAAA,EACH,CAAC,CAAA;AACH;;;AC9BA,iBAAA,EAAkB,CAAE,MAAM,MAAM;AAC9B,EAAA,OAAA,CAAQ,MAAA,CAAO,MAAM,mCAAmC,CAAA;AACxD,EAAA,OAAA,CAAQ,QAAA,GAAW,CAAA;AACrB,CAAC,CAAA","file":"byok-mcp-env.js","sourcesContent":["import { spawn } from 'node:child_process';\n\n/** Project one sealed server's env through Codex's documented env_vars channel.\n * No secret or original server argument is carried in the launcher argv.\n * The child remains in Codex's owned process group / Windows Job Object.\n */\nexport async function runMcpEnvLauncher(): Promise<void> {\n const key = process.env.BYOK_MCP_ENV_KEY;\n if (!key || !/^BYOK_MCP_PAYLOAD_[A-F0-9]{32}$/.test(key)) throw new Error('invalid MCP environment binding');\n const encoded = process.env[key];\n if (!encoded) throw new Error('missing sealed MCP environment');\n let config: { command: string; args?: string[]; env?: Record<string, string> };\n try {\n config = JSON.parse(encoded);\n if (!config || typeof config.command !== 'string' || !config.command\n || (config.args !== undefined && (!Array.isArray(config.args) || config.args.some(value => typeof value !== 'string')))\n || (config.env !== undefined && (typeof config.env !== 'object' || config.env === null || Array.isArray(config.env)\n || Object.values(config.env).some(value => typeof value !== 'string')))) throw new Error();\n } catch {\n throw new Error('invalid sealed MCP environment');\n }\n const env = { ...process.env };\n delete env.BYOK_MCP_ENV_KEY;\n for (const name of Object.keys(env)) if (name.startsWith('BYOK_MCP_PAYLOAD_')) delete env[name];\n Object.assign(env, config.env);\n await new Promise<void>((resolve, reject) => {\n const child = spawn(config.command, config.args ?? [], { env, stdio: 'inherit', windowsHide: true });\n child.once('error', () => reject(new Error('MCP child could not be spawned')));\n child.once('close', code => {\n if (code === 0) resolve();\n else reject(new Error('MCP child exited unsuccessfully'));\n });\n });\n}\n","#!/usr/bin/env node\nimport { runMcpEnvLauncher } from './mcp-env-launcher';\n\nrunMcpEnvLauncher().catch(() => {\n process.stderr.write('byok-mcp-env: MCP launch failed\\n');\n process.exitCode = 1;\n});\n"]}
@@ -3,6 +3,9 @@ import { type CollectDiagnosticsOptions } from '../../diagnostics/diagnostics';
3
3
  export interface DoctorOptions extends CollectDiagnosticsOptions {
4
4
  json?: boolean;
5
5
  fix?: boolean;
6
+ repair?: string;
7
+ expectedDeviceId?: string;
8
+ expectedTenantId?: string;
6
9
  confirmed?: boolean;
7
10
  log?: (line: string) => void;
8
11
  }
@@ -0,0 +1,5 @@
1
+ /** Project one sealed server's env through Codex's documented env_vars channel.
2
+ * No secret or original server argument is carried in the launcher argv.
3
+ * The child remains in Codex's owned process group / Windows Job Object.
4
+ */
5
+ export declare function runMcpEnvLauncher(): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import type { RuntimeDetectResult } from '../types';
1
2
  import { type RuntimeAdapter } from '../index';
2
3
  export declare const RUNTIME_PROBE_TIMEOUT_MS = 5000;
3
4
  /** The bundled adapter set `byok-agent status`/`byok-agent runtimes` probe by default — same unset-vs-set allowlist contract as `createDaemon` itself. */
@@ -13,7 +14,9 @@ export declare function defaultRuntimeAdapters(runtimeAllowlist: string[] | unde
13
14
  */
14
15
  export interface ProbedRuntime {
15
16
  id: string;
17
+ /** Deterministic projection of outcome, never adapter-authored. */
16
18
  present: boolean;
19
+ outcome: RuntimeDetectResult['kind'];
17
20
  version?: string;
18
21
  authPresent?: boolean;
19
22
  steer: boolean;
@@ -21,13 +24,9 @@ export interface ProbedRuntime {
21
24
  permissionModes: string[];
22
25
  }
23
26
  /**
24
- * Runs `detect()` and reads each adapter's frozen descriptor, in parallel. Every
25
- * bundled adapter's own `detect()` already catches its own failures (e.g.
26
- * `pi-adapter.ts`'s `detect()` wraps its version probe in try/catch and
27
- * resolves `{present: false}` rather than rejecting) — the catch here is a
28
- * defensive backstop for a `RuntimeAdapter` that doesn't hold that
29
- * convention, not a workaround for an observed failure in the bundled
30
- * three.
27
+ * Fresh, parallel local observations. A custom adapter timeout is an observation
28
+ * deadline only: detect() has no cancellation contract. Never expose arbitrary
29
+ * adapter errors or interpret an old/malformed result as available.
31
30
  */
32
31
  export declare function probeRuntimes(adapters: readonly RuntimeAdapter[], options?: {
33
32
  timeoutMs?: number;
@@ -0,0 +1,2 @@
1
+ /** Only for side-effect-free admission. Never abandon an owned lease/session. */
2
+ export declare function awaitAdmission<T>(operation: () => Promise<T>, signal: AbortSignal): Promise<T>;
@@ -70,6 +70,7 @@ export declare class AgentReliableSpool {
70
70
  readonly homeDir: string;
71
71
  readonly spoolPath: string;
72
72
  private readonly pending;
73
+ private readonly file;
73
74
  private nextCursor;
74
75
  private logEntries;
75
76
  private writeTail;
@@ -20,12 +20,14 @@ export interface AgentMessageOutboxRecord {
20
20
  export declare class AgentMessageOutboxError extends Error {
21
21
  constructor(message: string);
22
22
  }
23
- /** Agent-local, append-before-send outbox. Only exact accepted disposition retires bytes. */
23
+ /** Agent-local append-before-send authority. Only acceptance or explicit terminal archival retires bytes. */
24
24
  export declare class AgentMessageOutbox {
25
25
  readonly homeDir: string;
26
26
  readonly outboxPath: string;
27
27
  private readonly pendingByTask;
28
+ private readonly revokedTasks;
28
29
  private readonly dispositionByTask;
30
+ private readonly file;
29
31
  private nextCursor;
30
32
  private logEntries;
31
33
  private writeTail;
@@ -34,7 +36,7 @@ export declare class AgentMessageOutbox {
34
36
  /** Re-open every existing Agent-local message outbox without following Agent-home symlinks. */
35
37
  static recover(agentsRoot: string, tenantId: string): Promise<readonly AgentMessageOutbox[]>;
36
38
  records(): readonly AgentMessageOutboxRecord[];
37
- /** Activated records with no exact disposition yet; only these may be transport-replayed. */
39
+ /** Records without disposition or local revoke; transport replay additionally requires session binding. */
38
40
  retryableRecords(): readonly AgentMessageOutboxRecord[];
39
41
  get(taskId: string): AgentMessageOutboxRecord | undefined;
40
42
  appendDraft(input: {
@@ -48,11 +50,23 @@ export declare class AgentMessageOutbox {
48
50
  readonly maxPendingEvents: number;
49
51
  readonly maxPendingBytes: number;
50
52
  }): Promise<AgentMessageOutboxRecord>;
51
- activate(taskId: string, sessionRef: string): Promise<AgentMessageOutboxRecord | undefined>;
53
+ activate(taskId: string, sessionRef: string, isCurrent?: () => boolean): Promise<AgentMessageOutboxRecord | undefined>;
54
+ /** Local lifecycle cancellation, never a synthesized consumer disposition. */
55
+ revoke(taskId: string): Promise<void>;
56
+ /** The same terminal classification used by operator receipts and archival. */
57
+ terminalRecords(): readonly AgentMessageOutboxRecord[];
58
+ private isTerminalEvidence;
52
59
  publishPayload(record: AgentMessageOutboxRecord): AgentMessagePublishPayload;
53
60
  applyDisposition(taskId: string, input: unknown): Promise<'accepted' | 'held' | 'refused' | 'mismatch' | 'unknown'>;
54
61
  private load;
55
62
  private appendEntry;
63
+ /**
64
+ * Explicit operator maintenance under the Agent-home single-writer lease.
65
+ * Copy complete terminal evidence durably before removing it from the live log.
66
+ * Archives are audit artifacts, never another replay input. held remains live.
67
+ */
68
+ archiveTerminalRecords(archiveDirectory: string): Promise<string | undefined>;
69
+ private snapshotEntries;
56
70
  private compact;
57
71
  private exclusive;
58
72
  }
@@ -0,0 +1,7 @@
1
+ import type { FileHandle } from 'node:fs/promises';
2
+ export declare const DEFAULT_ARTIFACT_LIMITS: Readonly<{
3
+ maxFileBytes: number;
4
+ maxTaskBytes: number;
5
+ }>;
6
+ /** The caller retains fd ownership; no pathname is reopened. */
7
+ export declare function readArtifactBytes(handle: FileHandle, maxBytes: number, signal: AbortSignal, account: (bytes: number) => void): Promise<Buffer>;
@@ -1,3 +1,4 @@
1
+ import type { HarnessInfo } from '@byok-sdk/protocol';
1
2
  import { type CapabilityFlag, type Envelope, type RuntimeInfo, type ToolsetId } from '@byok-sdk/protocol';
2
3
  import { AuthManager } from './auth-manager';
3
4
  import type { CursorStore } from './cursor-store';
@@ -14,6 +15,7 @@ export interface ConnectionManagerOptions {
14
15
  /** U4a Local Agent release version, sent unchanged in `conn.hello`. */
15
16
  clientVersion?: string;
16
17
  runtimes: RuntimeInfo[];
18
+ harnesses?: HarnessInfo[];
17
19
  /** Reads current sorted logical IDs from the validated local registry for every `conn.hello`. */
18
20
  getConfiguredToolsets?: () => readonly ToolsetId[];
19
21
  auth: AuthManager;
@@ -73,25 +75,11 @@ export declare class ConnectionManager {
73
75
  * guarantees in docs/protocol.md §9.
74
76
  */
75
77
  private stalledAtSeq;
76
- /**
77
- * Design A (Wave 2, F3-on-long-poll): the second, in-memory watermark
78
- * alongside the durable `cursor`. `cursor` only ever advances AFTER a
79
- * `task.*` handler's side effects resolve successfully, and is persisted
80
- * (see `advanceCursor`) — that semantics is unchanged. `deliveredSeq`
81
- * advances eagerly, the instant a `task.*` envelope is admitted past
82
- * dedup (see `deliver`/`noteDelivered`), independent of whether its
83
- * handler has even started, let alone succeeded. It exists so a repeated
84
- * read at the durable cursor does not re-dispatch an envelope already in
85
- * flight — `handleOffer` must not start a second adapter session while a
86
- * first attempt is still running. On WS this same field is written the
87
- * same way, but since a live WS connection only ever pushes a given `seq`
88
- * once, it never has an observable effect there beyond mirroring
89
- * `cursor` (see `dedupWatermark`'s doc comment for why redelivery
90
- * correctness doesn't depend on resetting it anywhere).
91
- */
92
- private deliveredSeq;
93
- /** Finding F3: serializes `onEnvelope` calls into a per-connection FIFO — one envelope's handler always fully settles before the next one starts. */
78
+ /** Drain barrier for independently running handlers; not an admission lock. */
94
79
  private processingChain;
80
+ private cursorInitialization;
81
+ private completionTail;
82
+ private readonly controlTails;
95
83
  /**
96
84
  * Design B (finding N4): the ONE outbound queue holds `Envelope` OBJECTS,
97
85
  * never re-encoded/rebuilt strings, so a
@@ -117,38 +105,13 @@ export declare class ConnectionManager {
117
105
  private terminalError;
118
106
  private settledWaiters;
119
107
  private pendingCursorSave;
120
- /**
121
- * Finding P2 (Fix 2b): seqs currently admitted into `processingChain` but
122
- * not yet settled — added in `deliver()` the moment a `task.*` envelope is
123
- * accepted past the ordinary watermark check, removed in `process()`'s
124
- * `finally` once that specific attempt resolves (success OR failure).
125
- * While stalled, `dedupWatermark()` deliberately stays frozen below
126
- * already-delivered seqs (see its own doc comment) so the failed seq's own
127
- * redelivery can get through — but that same frozen watermark also means
128
- * every OTHER seq above it rides along on every re-poll too. Without this,
129
- * a seq already mid-flight (e.g. a `task.offer` whose prepared operation start()
130
- * hasn't resolved yet) would be re-enqueued into `processingChain` on
131
- * every such re-poll, piling up duplicate copies that — once the first
132
- * finally resolves and the chain unwinds through them — run its handler
133
- * again; for `task.offer` specifically, a second adapter session
134
- * orphaning the first (`TaskRunner`'s own `this.tasks.has` guard, finding
135
- * P2c, is the second, independent layer against exactly that).
136
- */
108
+ /** Admission dedup while an individual handler is running. */
137
109
  private readonly inFlightSeqs;
138
- /**
139
- * Finding P2 (Fix 2b): seqs whose handler has already resolved
140
- * successfully at least once this session, tracked only while a stall is
141
- * in effect — cleared the moment `stalledAtSeq` itself clears (see
142
- * `process()`), since once unstalled the ordinary watermark check via
143
- * `deliveredSeq` already covers everything delivered so far, making this
144
- * redundant. Needed because the stall-gap-prevention rule in `process()`
145
- * deliberately does NOT advance `cursor` past a seq above the
146
- * still-unresolved `stalledAtSeq`, even once that seq's own handler
147
- * succeeds — so `dedupWatermark()` alone can't distinguish "already
148
- * succeeded, don't re-run" from "never yet attempted" for anything in
149
- * that gap.
150
- */
110
+ /** Successful side effects retained until their durable acknowledgement. */
151
111
  private readonly processedSeqs;
112
+ private readonly failedEnvelopes;
113
+ /** Received work lacking a successful handler receipt, including malformed frames. */
114
+ private readonly unresolvedSeqs;
152
115
  /**
153
116
  * Finding P3: the pending `drainOutbox` long-poll retry backoff, if any —
154
117
  * cancellable so `enterRevoked()` can unblock it immediately instead of
@@ -303,43 +266,21 @@ export declare class ConnectionManager {
303
266
  * - F3 (at-most-once): the old code persisted the cursor advance BEFORE
304
267
  * `onEnvelope` even ran (fire-and-forget) — a handler that then failed
305
268
  * left a redelivery-proof envelope permanently marked processed. Inbound
306
- * envelopes are now serialized through `processingChain` (one handler
307
- * fully settles before the next starts) and the cursor only advances
269
+ * handlers may run independently; their receipt commits are serialized.
270
+ * The cursor only advances
308
271
  * AFTER the handler resolves successfully; a rejection leaves the
309
272
  * cursor where it was (see `stalledAtSeq`), so a future reconnect's
310
273
  * redelivery re-attempts it — safe because every server->daemon type is
311
274
  * documented idempotent (protocol §9).
312
275
  */
313
276
  private deliver;
314
- /**
315
- * The local watermark `deliver()` dedupes inbound `task.*` envelopes
316
- * against. It is deliberately NOT the long-poll query cursor: that query
317
- * is the kernel acknowledgement and uses only the successfully processed
318
- * `cursor` (see the constructor). Normally this local watermark is
319
- * `deliveredSeq` — which is always >= `cursor` (every envelope that
320
- * reaches `advanceCursor` already passed through `noteDelivered` first,
321
- * see `deliver`) — so this is the literal `max(cursor, deliveredSeq)` the
322
- * design calls for, just expressed via that invariant rather than an
323
- * explicit `Math.max`.
324
- *
325
- * While `stalledAtSeq` is set, this collapses to the durable `cursor`
326
- * alone, deliberately ignoring however far `deliveredSeq` had already run
327
- * ahead before the failure was known: that's what lets the stalled
328
- * envelope's own redelivery (and everything after it, right up to a
329
- * fresh success) get past this same dedup check instead of being
330
- * self-deduped by the client's own earlier eager tracking of envelopes
331
- * whose outcome wasn't known yet. No separate "reset deliveredSeq on
332
- * reconnect" step is needed for this to be correct — collapsing to
333
- * `cursor` exactly while stalled already produces the right answer on
334
- * every long-poll retry path. NOT resetting it unconditionally on every
335
- * retry lets `deliveredSeq` keep doing its job of not re-dispatching
336
- * something already in flight while a handler is still running.
337
- */
277
+ /** A committed terminal can settle a failed offer receipt even if cloud
278
+ * cancellation has since filtered that offer from mailbox replay. */
279
+ retryTaskReceipts(taskId: string): void;
280
+ /** Only a durable acknowledgement can deduplicate a whole prefix. */
338
281
  private dedupWatermark;
339
- /** Design A: eagerly advance the in-memory delivery watermark — called for every `task.*` envelope `deliver()` admits past dedup, regardless of transport or of whether its handler has even started yet. */
340
- private noteDelivered;
341
282
  private process;
342
- /** Serialized with handler completion, so invalid work cannot be acked by later success. */
283
+ /** Recorded synchronously at receive, before a later successful receipt can commit. */
343
284
  private noteValidationFailure;
344
285
  private advanceCursor;
345
286
  private quarantineRejectedOutbound;
@@ -307,6 +307,13 @@ export interface DaemonConfig {
307
307
  * explicitly instead to opt out of enforcement altogether.
308
308
  */
309
309
  maxTaskOutputBytes?: number;
310
+ /** Legacy artifact bytes only: default 16 MiB/file and 64 MiB/task, independent of event output limits. */
311
+ artifactLimits?: {
312
+ maxFileBytes: number;
313
+ maxTaskBytes: number;
314
+ };
315
+ /** Admission and startup deadline, including pure detect/prepare waits; unresolved process owners remain quarantined. Default 30 seconds. */
316
+ startupTimeoutMs?: number;
310
317
  /**
311
318
  * Per-EVENT inline ceiling (default {@link DEFAULT_MAX_INLINE_EVENT_BYTES},
312
319
  * 64 KiB) for the two `AgentEvent` fields a runtime authors freely:
@@ -528,6 +535,8 @@ export interface DaemonStatus {
528
535
  revoked: boolean;
529
536
  deviceId?: string;
530
537
  activeTaskCount: number;
538
+ /** Exact terminal results retained for journal retry; nonzero requires recovery. */
539
+ pendingTerminalCommits: number;
531
540
  /** Passthrough of `DaemonConfig.branding` — `undefined` when the product configured none. See `DaemonBranding`. */
532
541
  branding?: DaemonBranding;
533
542
  /** Local lifecycle/retry budget, separate from transport fallback state. */
@@ -587,7 +596,8 @@ export interface Daemon {
587
596
  /** M3-2a: same as {@link approve} but rejects — see that method's doc comment. */
588
597
  reject(taskId: string, reason?: string): Promise<void>;
589
598
  }
590
- /** Internal seam so tests can substitute stub adapters / faster batch and long-poll timing. `createDaemonWithAdapters` (which takes this) is also the real entry point for products supplying a hand-built adapter set `createDaemon` can't construct on its own — e.g. custom adapter options, or an adapter that REPLACES a bundled runtime's implementation under the same id. Honest limit: an adapter id outside `pi`/`claude`/`codex` cannot pass wire validation today — `RuntimeIdSchema` (`@byok-sdk/protocol`) is a closed `z.enum(['pi', 'claude', 'codex'])`, and `isRuntimeId` filtering below (see `detectRuntimes`) drops any detected adapter outside that set before it ever reaches a wire-visible field. A genuinely fourth/namespaced runtime id is a future protocol change, not something this seam enables today. */
599
+ /** Custom adapter composition. Available custom descriptors are published through
600
+ * the capability-gated harness inventory; built-in RuntimeId remains closed. */
591
601
  export interface DaemonOverrides {
592
602
  /** Test-only synchronous kill points; never supplied by production configuration. */
593
603
  executionRecoveryFault?: (step: 'terminal:before-send' | 'terminal:queued' | 'outbound:before-post' | 'outbound:after-ack') => void;
@@ -0,0 +1,3 @@
1
+ import type { RuntimeOperationStartInput, Session } from '../types';
2
+ /** A deadline withdraws admission; it never invents a quiescence receipt. */
3
+ export declare function startOwnedRuntime(start: (input: RuntimeOperationStartInput) => Promise<Session>, input: RuntimeOperationStartInput, signal: AbortSignal, timeoutMs: number): Promise<Session>;
@@ -72,6 +72,8 @@ export declare class DeviceStore {
72
72
  * identifiable until the synchronous pathname check and unlink complete.
73
73
  */
74
74
  remove(): Promise<DeviceMetadata | undefined>;
75
+ /** Rebuild only the non-secret projection, while the caller owns the store lease. */
76
+ reconcileMetadata(authority: DeviceMetadata): Promise<boolean>;
75
77
  save(record: DeviceMetadata): Promise<void>;
76
78
  private openBounded;
77
79
  }
@@ -228,6 +228,7 @@ export interface TaskRunnerDeps {
228
228
  agentSessionHandoffs?: AgentSessionHandoffStore;
229
229
  deviceId: string;
230
230
  send: (envelope: Envelope) => void;
231
+ awaitTerminalCommit?: (taskId: string) => Promise<void>;
231
232
  /** Fsync the execution commitment before claim/runtime side effects. */
232
233
  beforeClaim?: (taskId: string, runtime: string) => Promise<void>;
233
234
  blobClient: BlobResolver;
@@ -323,6 +324,7 @@ export interface TaskRunnerDeps {
323
324
  onApprovalDispatched?: (taskId: string, approvalId: string) => void;
324
325
  /** Overrides the bounded soft-interrupt window before authoritative `Session.close()` disposal begins. */
325
326
  shutdownInterruptTimeoutMs?: number;
327
+ startupTimeoutMs?: number;
326
328
  /**
327
329
  * M5 batch-3 (workstream 2): overrides {@link DEFAULT_MAX_TASK_OUTPUT_BYTES}
328
330
  * — see that constant's own doc comment and `DaemonConfig.maxTaskOutputBytes`
@@ -332,6 +334,10 @@ export interface TaskRunnerDeps {
332
334
  * interface (`shutdownInterruptTimeoutMs`, `approvalTimeoutMs`).
333
335
  */
334
336
  maxTaskOutputBytes?: number;
337
+ artifactLimits?: {
338
+ maxFileBytes: number;
339
+ maxTaskBytes: number;
340
+ };
335
341
  /**
336
342
  * Per-event inline ceiling for `tool_use.input` / `tool_result.output` —
337
343
  * see `DaemonConfig.maxInlineEventBytes` (`create-daemon.ts`) for the full
@@ -627,8 +633,13 @@ export declare class TaskRunner {
627
633
  /** Restore activated, unaccepted message drafts before transport admission on daemon restart. */
628
634
  recoverAgentMessageOutboxes(agentsRoot: string): Promise<void>;
629
635
  private agentMessageOutbox;
636
+ /** A recovery terminal must not close first-message admission before this durable draft has a disposition. */
637
+ hasPendingRecoveredAgentMessage(taskId: string): boolean;
630
638
  /** Retry stable recovered records after a transport handshake/re-handshake. */
631
639
  retryRecoveredAgentMessages(): void;
640
+ private canPublishAgentMessage;
641
+ /** All authors and startup use the same queue-time and post-I/O authority check. */
642
+ private activateAgentMessage;
632
643
  private sendAgentMessageRecord;
633
644
  private handleAgentMessageDisposition;
634
645
  /** M4 Phase 2: stop claiming any FUTURE `task.offer` — see `stoppingOffers`'s own doc comment. Idempotent. */
@@ -670,6 +681,14 @@ export declare class TaskRunner {
670
681
  * ahead of this method — see `daemon-control-socket.test.ts`'s dedicated
671
682
  * regression test for the exact scenario.
672
683
  */
684
+ /** Startup resources have an owner even before a Session can become active. */
685
+ private startupRetryTimer;
686
+ private readonly claimedHarnesses;
687
+ private readonly homeReservations;
688
+ private readonly startupOwners;
689
+ private readonly startupDisposals;
690
+ private disposeStartupOwner;
691
+ private disposeStartupOwnerOnce;
673
692
  shutdownActiveTasks(reason: string): Promise<void>;
674
693
  /**
675
694
  * M5 batch-3 (workstream 2): the ONE shared per-task teardown sequence —
@@ -701,6 +720,7 @@ export declare class TaskRunner {
701
720
  * a genuine protocol bug, not a benign race — mirrors `pump()`'s own
702
721
  * identity-check guard for the same class of race.
703
722
  */
723
+ private interruptBounded;
704
724
  private teardownActiveTask;
705
725
  /** Graceful-shutdown caller of {@link teardownActiveTask} — see `shutdownActiveTasks`'s own doc comment. `retryable: true`: nothing about the task/policy itself was ever at fault, only this device's own availability right now. */
706
726
  private shutdownTask;
@@ -1151,7 +1171,10 @@ export declare class TaskRunner {
1151
1171
  private persistAgentTerminalEvidence;
1152
1172
  private retryAgentTerminalEvidence;
1153
1173
  private reportAgentTerminalEvidenceFailure;
1174
+ retryTerminalFinalization(taskId: string): Promise<void>;
1175
+ private readonly finalizationAttempts;
1154
1176
  private finish;
1177
+ private finishOnce;
1155
1178
  private reserveSemanticTerminal;
1156
1179
  /** M3-B: bounded insert for `finishedTaskIds` — see its class-level doc comment and `MAX_TRACKED_TASK_IDS`. Evicts the oldest (first-inserted) entry once over cap, same idiom as `ConnectionHub.checkAndRecordDuplicate` (packages/server/src/hub.ts). */
1157
1180
  private addFinishedTaskId;
@@ -0,0 +1,21 @@
1
+ /** Volatile ownership of exact terminal bytes until the journal accepts them.
2
+ * The journal remains the sole durable authority. Failed receipts stay rejected;
3
+ * only the internal scheduling tail recovers so another task can make progress.
4
+ */
5
+ export declare class TerminalCommitQueue {
6
+ private readonly committed;
7
+ private readonly pending;
8
+ private tail;
9
+ private timer;
10
+ private stopped;
11
+ constructor(committed: (taskId: string) => void);
12
+ get pendingCount(): number;
13
+ hasPending(taskId: string): boolean;
14
+ enqueue(taskId: string, commit: () => Promise<void>): void;
15
+ receipt(taskId: string): Promise<void>;
16
+ retry(taskId: string): Promise<void>;
17
+ resume(): void;
18
+ stop(): Promise<void>;
19
+ private attempt;
20
+ private armRetry;
21
+ }
@@ -0,0 +1,6 @@
1
+ import { type AgentRef } from '@byok-sdk/protocol';
2
+ /** Project sealed execution facts, never the offer's requested harness. */
3
+ export declare function terminalIdentity(runtimeId: string | undefined, agentRef?: AgentRef): {
4
+ harnessId?: string;
5
+ agentRef?: AgentRef;
6
+ };
@@ -0,0 +1,46 @@
1
+ import type { DaemonConfig } from '../daemon/create-daemon';
2
+ import type { RuntimeAdapter } from '../types';
3
+ import type { DiagnosticsSnapshot } from './types';
4
+ export type { DiagnosticsSnapshot, DiagnosticCheck, DiagnosticStatus } from './types';
5
+ export interface DiagnoseDeviceOptions {
6
+ /** The same adapters used by an embedded host; omitted uses bundled adapters. */
7
+ adapters?: RuntimeAdapter[];
8
+ runtimeProbeTimeoutMs?: number;
9
+ }
10
+ /** Read-only device observation; does not read OS credentials or prove Agent readiness. */
11
+ export declare function diagnoseDevice(config: DaemonConfig, options?: DiagnoseDeviceOptions): Promise<DiagnosticsSnapshot>;
12
+ export interface RepairDeviceEnrollmentMetadataInput {
13
+ confirmed: true;
14
+ /** Obtain both from the host's authorized enrollment target, never from a guessed default. */
15
+ expectedDeviceId: string;
16
+ expectedTenantId: string;
17
+ }
18
+ export interface DeviceMetadataRepairResult {
19
+ action: 'restore-enrollment-metadata';
20
+ scope: 'device';
21
+ /** Metadata readback only; does not imply renewed credentials or a running daemon. */
22
+ status: 'repaired' | 'not-needed';
23
+ }
24
+ declare const MESSAGES: {
25
+ readonly 'confirmation-required': 'Explicit confirmation is required for enrollment metadata repair.';
26
+ readonly 'invalid-target': 'An explicit expected tenant and device are required.';
27
+ readonly 'daemon-running': 'Stop the daemon before enrollment metadata repair.';
28
+ readonly 'store-busy': 'The store is owned by another operation; enrollment metadata repair refused.';
29
+ readonly 'authority-unavailable': 'The OS enrollment authority could not be read.';
30
+ readonly 'authority-missing': 'No complete OS enrollment exists; use explicit authenticated pairing.';
31
+ readonly 'target-mismatch': 'The OS enrollment does not match the expected tenant and device.';
32
+ readonly 'projection-unavailable': 'The enrollment metadata could not be read safely; repair refused.';
33
+ readonly 'repair-failed': 'Enrollment metadata repair did not complete; inspect the state before retrying.';
34
+ };
35
+ export type DeviceMetadataRepairErrorCode = keyof typeof MESSAGES;
36
+ /** Closed diagnostics only: no OS stderr, local paths, authority bytes or nested cause. */
37
+ export declare class DeviceMetadataRepairError extends Error {
38
+ readonly code: DeviceMetadataRepairErrorCode;
39
+ constructor(code: DeviceMetadataRepairErrorCode);
40
+ }
41
+ /**
42
+ * Explicitly restore missing/valid-stale device.json from its existing OS authority.
43
+ * Does not instantiate AuthManager, renew credentials, pair, or start a runtime.
44
+ * Ordinary doctor remains credential-blind; only this confirmed action opens the OS store.
45
+ */
46
+ export declare function repairDeviceEnrollmentMetadata(config: DaemonConfig, input: RepairDeviceEnrollmentMetadataInput): Promise<DeviceMetadataRepairResult>;
@@ -1,106 +1,21 @@
1
+ import type { DiagnosticsSnapshot } from './types';
1
2
  import type { DaemonConfig, RuntimeAdapter } from '../index';
2
3
  import { connectControlClient } from '../bin/control-client';
3
- import type { ControlStatusResult } from '../daemon/control-protocol';
4
- import { type OperationalHealthFileInspection, OPERATIONAL_HEALTH_FILENAME } from '../daemon/operational-health';
4
+ import { OPERATIONAL_HEALTH_FILENAME } from '../daemon/operational-health';
5
5
  export declare const MAX_QUARANTINE_ENTRIES = 100;
6
6
  export declare const MAX_QUARANTINE_SCAN_ENTRIES: number;
7
7
  export declare const MAX_QUARANTINE_READ_BYTES: number;
8
8
  export declare const MAX_DEVICE_RECORD_BYTES: number;
9
9
  export declare const MAX_JOURNAL_COPY_BYTES: number;
10
- export type DiagnosticStatus = 'pass' | 'warn' | 'fail';
11
- export interface DiagnosticCheck {
12
- id: 'config' | 'device' | 'runtimes' | 'control' | 'health' | 'journal' | 'workspace' | 'quarantine';
13
- status: DiagnosticStatus;
14
- summary: string;
15
- }
16
- export interface DiagnosticsSnapshot {
17
- version: 1;
18
- generatedAt: string;
19
- product: {
20
- nameHash: string;
21
- idHash: string;
22
- };
23
- system: {
24
- node: string;
25
- platform: NodeJS.Platform;
26
- arch: string;
27
- sqliteAvailable: boolean;
28
- };
29
- config: {
30
- serverProtocol: 'http' | 'https' | 'ws' | 'wss' | 'invalid' | 'unsupported';
31
- customStoreDir: boolean;
32
- hostedJournal: boolean;
33
- runtimeAllowlistCount?: number;
34
- };
35
- device: {
36
- status: 'paired' | 'unpaired' | 'unavailable';
37
- deviceIdHash?: string;
38
- };
39
- runtimes: Array<{
40
- idHash: string;
41
- present: boolean;
42
- versionPresent: boolean;
43
- authPresent?: boolean;
44
- steer: boolean;
45
- resume: boolean;
46
- permissionModeCount: number;
47
- }>;
48
- control: {
49
- status: 'offline';
50
- reason: string;
51
- } | {
52
- status: 'online';
53
- pid: number;
54
- uptimeMs: number;
55
- transport: string;
56
- activeTaskCount: number;
57
- pendingApprovalCount: number;
58
- operationalHealth: ControlStatusResult['operationalHealth'];
59
- storage?: ControlStatusResult['storage'];
60
- };
61
- health: OperationalHealthFileInspection;
62
- journal: {
63
- status: 'missing' | 'present' | 'corrupt' | 'unavailable';
64
- sizeBytes?: number;
65
- walBytes?: number;
66
- integrity?: 'ok' | 'not-checked';
67
- reason?: string;
68
- };
69
- workspace: {
70
- status: 'available' | 'missing' | 'unavailable';
71
- writable?: boolean;
72
- reason?: string;
73
- };
74
- quarantine: {
75
- status: 'available' | 'missing' | 'unavailable';
76
- count: number;
77
- scannedCount: number;
78
- truncated: boolean;
79
- entries: Array<{
80
- nameHash: string;
81
- sizeBytes: number;
82
- modifiedAt: string;
83
- }>;
84
- reason?: string;
85
- };
86
- checks: DiagnosticCheck[];
87
- }
10
+ export type { DiagnosticStatus, DiagnosticCheck, DiagnosticsSnapshot } from './types';
88
11
  export interface CollectDiagnosticsOptions {
89
12
  clock?: () => Date;
90
13
  adapters?: RuntimeAdapter[];
91
14
  connectControl?: typeof connectControlClient;
92
15
  runtimeProbeTimeoutMs?: number;
93
16
  }
94
- export type OperationalHealthFixResult = {
95
- status: 'not-needed';
96
- reason: 'missing' | 'valid';
97
- } | {
98
- status: 'quarantined';
99
- evidenceName: string;
100
- manifestName: string;
101
- sha256: string;
102
- sizeBytes: number;
103
- };
17
+ import type { OperationalHealthFixResult } from './types';
18
+ export type { OperationalHealthFixResult } from './types';
104
19
  export declare function collectDiagnostics(config: DaemonConfig, storeDir: string, options?: CollectDiagnosticsOptions): Promise<DiagnosticsSnapshot>;
105
20
  export declare function stableIdentifierHash(value: string): string;
106
21
  /**