@byok-sdk/client 0.13.0 → 0.15.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 (69) hide show
  1. package/README.md +95 -0
  2. package/dist/adapters/claude/process-client.d.ts +24 -0
  3. package/dist/adapters/codex/codex-adapter.d.ts +2 -0
  4. package/dist/adapters/codex/process-runner.d.ts +62 -18
  5. package/dist/adapters/detect-outcome.d.ts +18 -0
  6. package/dist/adapters/index.d.ts +1 -1
  7. package/dist/adapters/index.js +546 -76
  8. package/dist/adapters/index.js.map +1 -1
  9. package/dist/adapters/pi/events.d.ts +1 -1
  10. package/dist/adapters/pi/rpc-client.d.ts +47 -1
  11. package/dist/adapters/pi/subagents-policy-extension.js +1 -1
  12. package/dist/adapters/pi/subagents-policy-extension.js.map +1 -1
  13. package/dist/adapters/pi/team-interaction-extension.d.ts +24 -0
  14. package/dist/adapters/pi/team-interaction-extension.js +81 -0
  15. package/dist/adapters/pi/team-interaction-extension.js.map +1 -0
  16. package/dist/adapters/process-tree.d.ts +74 -4
  17. package/dist/adapters/provider-credential-environment.d.ts +1 -1
  18. package/dist/adapters/win32-job-object.d.ts +101 -0
  19. package/dist/bin/byok-agent-memory-mcp.js +29 -0
  20. package/dist/bin/byok-agent-memory-mcp.js.map +1 -1
  21. package/dist/bin/byok-agent-message-mcp.js +29 -0
  22. package/dist/bin/byok-agent-message-mcp.js.map +1 -1
  23. package/dist/bin/byok-agent-team-mcp.js +29 -0
  24. package/dist/bin/byok-agent-team-mcp.js.map +1 -1
  25. package/dist/bin/byok-agent.js +20274 -18057
  26. package/dist/bin/byok-agent.js.map +1 -1
  27. package/dist/bin/byok-approval-mcp.js +29 -0
  28. package/dist/bin/byok-approval-mcp.js.map +1 -1
  29. package/dist/bin/byok-mcp-env.d.ts +2 -0
  30. package/dist/bin/byok-mcp-env.js +36 -0
  31. package/dist/bin/byok-mcp-env.js.map +1 -0
  32. package/dist/bin/commands/doctor.d.ts +3 -0
  33. package/dist/bin/commands/team-pi-relay.d.ts +24 -0
  34. package/dist/bin/commands/team-relay.d.ts +11 -0
  35. package/dist/bin/mcp-env-launcher.d.ts +5 -0
  36. package/dist/bin/runtime-probe.d.ts +6 -7
  37. package/dist/bin/team-codex-relay.d.ts +32 -0
  38. package/dist/bin/team-notification-relay.d.ts +40 -0
  39. package/dist/bin/team-pi-session.d.ts +59 -0
  40. package/dist/daemon/admission-wait.d.ts +2 -0
  41. package/dist/daemon/agent-egress-policy.d.ts +8 -0
  42. package/dist/daemon/agent-egress-spool.d.ts +1 -0
  43. package/dist/daemon/agent-message-outbox.d.ts +15 -3
  44. package/dist/daemon/artifact-read.d.ts +7 -0
  45. package/dist/daemon/connection-manager.d.ts +23 -176
  46. package/dist/daemon/control-protocol.d.ts +2 -0
  47. package/dist/daemon/create-daemon.d.ts +43 -1
  48. package/dist/daemon/event-spill.d.ts +90 -0
  49. package/dist/daemon/journal/journal.d.ts +15 -3
  50. package/dist/daemon/journal/sqlite-journal.d.ts +7 -2
  51. package/dist/daemon/long-poll-transport.d.ts +2 -50
  52. package/dist/daemon/runtime-start.d.ts +3 -0
  53. package/dist/daemon/store.d.ts +2 -0
  54. package/dist/daemon/task-runner.d.ts +36 -0
  55. package/dist/daemon/team-workspace.d.ts +9 -0
  56. package/dist/daemon/terminal-commit-queue.d.ts +21 -0
  57. package/dist/daemon/terminal-identity.d.ts +6 -0
  58. package/dist/diagnostics/device-doctor.d.ts +46 -0
  59. package/dist/diagnostics/diagnostics.d.ts +3 -80
  60. package/dist/diagnostics/types.d.ts +82 -0
  61. package/dist/index.d.ts +3 -1
  62. package/dist/index.js +2824 -649
  63. package/dist/index.js.map +1 -1
  64. package/dist/runtime-detection.d.ts +3 -0
  65. package/dist/runtime-failure.d.ts +6 -0
  66. package/dist/sdk-reserved-helper-host.d.ts +1 -1
  67. package/dist/types.d.ts +20 -10
  68. package/dist/util/durable-jsonl.d.ts +12 -0
  69. package/package.json +8 -5
@@ -307,6 +307,43 @@ 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;
317
+ /**
318
+ * Per-EVENT inline ceiling (default {@link DEFAULT_MAX_INLINE_EVENT_BYTES},
319
+ * 64 KiB) for the two `AgentEvent` fields a runtime authors freely:
320
+ * `tool_use.input` and `tool_result.output`. An event whose serialization
321
+ * exceeds this leaves `TaskRunner.pump` with that field replaced by a
322
+ * UTF-8-safe head/tail preview (`{ preview: { head, tail } }`) and an
323
+ * additive `spill` descriptor; the full JSON serialization is uploaded to
324
+ * the blob plane under an idempotent, content-addressed key, and
325
+ * `spill.blob` is where a consumer reads it back. If the upload fails the
326
+ * preview still ships, carrying `spill.unstoredReason` instead — omission
327
+ * is always described, never silent.
328
+ *
329
+ * This is a per-event bound, orthogonal to `maxTaskOutputBytes` (a
330
+ * whole-task total, counted AFTER spilling) and to
331
+ * `progressBatch.maxBatchBytes` (a per-batch wire budget).
332
+ *
333
+ * **Consumer contract:** `spill`'s presence is the only signal that the
334
+ * inline field is a preview. A consumer that renders `tool_result.output`
335
+ * without checking `spill` renders a truncation as the whole result.
336
+ *
337
+ * Must be a positive safe integer of at least
338
+ * {@link MIN_MAX_INLINE_EVENT_BYTES} (4096) — below that a legitimate
339
+ * `spill` descriptor no longer fits inside the cap it exists to enforce.
340
+ * Anything else (0, negative, non-integer, `NaN`,
341
+ * `Number.POSITIVE_INFINITY`) is a config validation error thrown
342
+ * synchronously from `createDaemonWithAdapters`/`createDaemon`; there is
343
+ * no opt-out, because "unbounded event" is exactly the state this exists
344
+ * to prevent.
345
+ */
346
+ maxInlineEventBytes?: number;
310
347
  /**
311
348
  * Host-owned batching policy for normalized `task.progress` events.
312
349
  * `maxBatchBytes`, when set, measures exactly the UTF-8 bytes of
@@ -498,6 +535,8 @@ export interface DaemonStatus {
498
535
  revoked: boolean;
499
536
  deviceId?: string;
500
537
  activeTaskCount: number;
538
+ /** Exact terminal results retained for journal retry; nonzero requires recovery. */
539
+ pendingTerminalCommits: number;
501
540
  /** Passthrough of `DaemonConfig.branding` — `undefined` when the product configured none. See `DaemonBranding`. */
502
541
  branding?: DaemonBranding;
503
542
  /** Local lifecycle/retry budget, separate from transport fallback state. */
@@ -557,8 +596,11 @@ export interface Daemon {
557
596
  /** M3-2a: same as {@link approve} but rejects — see that method's doc comment. */
558
597
  reject(taskId: string, reason?: string): Promise<void>;
559
598
  }
560
- /** 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. */
561
601
  export interface DaemonOverrides {
602
+ /** Test-only synchronous kill points; never supplied by production configuration. */
603
+ executionRecoveryFault?: (step: 'terminal:before-send' | 'terminal:queued' | 'outbound:before-post' | 'outbound:after-ack') => void;
562
604
  /** M4 Phase 3: overrides `TaskRunner`'s default out-of-band approval wait (`DEFAULT_APPROVAL_TIMEOUT_MS`, 10 minutes) before an unanswered `requestApproval` force-resolves as a fail-closed rejection. */
563
605
  approvalTimeoutMs?: number;
564
606
  /** Finding F5: overrides for the control-socket shutdown path's own bounded waits — see `TaskRunner.shutdownTask`'s and `ConnectionManager.stop`'s own doc comments. Both default to 5s; neither affects an ordinary (non-shutdown-RPC) `daemon.stop()` call. */
@@ -0,0 +1,90 @@
1
+ import type { AgentEvent } from '@byok-sdk/protocol';
2
+ import type { BlobResolver } from './blob-client';
3
+ /**
4
+ * Default per-event inline ceiling (64 KiB) for the two `AgentEvent` variants
5
+ * that carry runtime-authored payloads (`tool_use.input`,
6
+ * `tool_result.output`) — see `DaemonConfig.maxInlineEventBytes`
7
+ * (`create-daemon.ts`) for the host-facing contract.
8
+ *
9
+ * 64 KiB is the same threshold `sendArtifact` already uses to decide inline
10
+ * vs. blob for an artifact (`MAX_INLINE_ARTIFACT_BYTES`): one number for "a
11
+ * payload this daemon is willing to put on the activity wire".
12
+ */
13
+ export declare const DEFAULT_MAX_INLINE_EVENT_BYTES: number;
14
+ /**
15
+ * Smallest accepted `maxInlineEventBytes`. Below this a legitimate spill
16
+ * descriptor (`field` + byte counts + a `BlobRef` whose `blobId` is chosen by
17
+ * the server, plus the event's own `type`/`tool`/`toolCallId`) stops fitting
18
+ * inside the cap it is supposed to keep the event under, which would turn a
19
+ * host's configuration mistake into a per-event runtime invariant failure.
20
+ * Enforced up front at `DaemonConfig` validation, never here.
21
+ */
22
+ export declare const MIN_MAX_INLINE_EVENT_BYTES = 4096;
23
+ /**
24
+ * Worst-case JSON cost of the `,"spill":{…}` fragment when the descriptor
25
+ * carries an `unstoredReason` rather than a server-chosen `BlobRef`.
26
+ *
27
+ * Every part is bounded by construction, which a `BlobRef` is not — its
28
+ * `blobId` is an arbitrary-length server-chosen string:
29
+ *
30
+ * ```
31
+ * ,"spill": 9
32
+ * { 1
33
+ * "field":"output", 17 ("output" is the longer of the two)
34
+ * "totalBytes":<=16 digits>, 30
35
+ * "omittedBytes":<=16 digits>, 32 (never exceeds totalBytes)
36
+ * "contentType":"application/json", 33 (a module constant)
37
+ * "unstoredReason": 17
38
+ * "<=512 bytes of escaped reason>" 514 (MAX_UNSTORED_REASON_BYTES + 2 quotes)
39
+ * } 1
40
+ * ----
41
+ * 654
42
+ * ```
43
+ *
44
+ * Rounded up to 768 so digit-count growth cannot invalidate it. Because the
45
+ * event is spread first and `spill` written last, a bounded event is EXACTLY
46
+ * the empty-preview skeleton plus this fragment — so refusing to spill unless
47
+ * `skeleton + MAX_SPILL_DESCRIPTOR_BYTES <= maxInlineBytes` is what makes the
48
+ * final cap check unreachable rather than merely unlikely. `event-spill.test.ts`
49
+ * asserts this against a maximally escaping reason instead of trusting the
50
+ * comment.
51
+ */
52
+ export declare const MAX_SPILL_DESCRIPTOR_BYTES = 768;
53
+ export interface EventSpillDeps {
54
+ /** Effective inline ceiling for this daemon; already validated at the `DaemonConfig` layer. */
55
+ maxInlineBytes: number;
56
+ /** Only the upload half of `BlobResolver` is needed, so a test double stays minimal. */
57
+ blobClient: Pick<BlobResolver, 'uploadArtifact'>;
58
+ /** Scopes the upload's idempotency key to the task that produced the event. */
59
+ taskId: string;
60
+ /** Task lifecycle authority — aborting it stops the spill upload with the rest of the task's blob I/O. */
61
+ signal?: AbortSignal;
62
+ /** Diagnostic seam. Called only on a path that loses information (upload failure, or an event this policy cannot bound). */
63
+ log?: (message: string) => void;
64
+ }
65
+ /**
66
+ * Bound one normalized `AgentEvent` at the daemon's ingestion boundary
67
+ * (`TaskRunner.pump`).
68
+ *
69
+ * An event whose serialized form already fits `maxInlineBytes` is returned
70
+ * **as the same object reference** — the overwhelming majority of events pay
71
+ * exactly one `JSON.stringify` and nothing else, and no downstream identity
72
+ * comparison changes meaning.
73
+ *
74
+ * An oversized `tool_use` / `tool_result` has its runtime-authored field
75
+ * (`input` / `output`) uploaded to the blob plane in full and REPLACED inline
76
+ * by `{ preview: { head, tail } }`, with an additive `spill` descriptor
77
+ * carrying either the resulting `BlobRef` or a bounded `unstoredReason`. The
78
+ * replacement is *measured* against the cap, never assumed to fit: the
79
+ * preview budget is whatever is left after the rest of the event and the
80
+ * real descriptor, and it is shrunk until `JSON.stringify(result)` actually
81
+ * fits (JSON escaping can cost several bytes per source character, so the
82
+ * byte budget alone is not a bound).
83
+ *
84
+ * Storage failure is never silent and never fatal: the preview still ships,
85
+ * `unstoredReason` says why the omitted bytes are unreadable, and `log` is
86
+ * called. The runtime's own transcript still holds the content, so failing
87
+ * the task over a telemetry upload would trade a real result for an
88
+ * observability problem.
89
+ */
90
+ export declare function spillOversizedEvent(event: AgentEvent, deps: EventSpillDeps): Promise<AgentEvent>;
@@ -124,18 +124,21 @@ export interface LocalTransitionRecord {
124
124
  /** Whether the cloud has confirmed the terminal this daemon produced (§12.7.3's "terminal 生成后、truth 写入前" window). */
125
125
  export type TerminalTruthState = 'pending' | 'confirmed' | 'failed';
126
126
  /**
127
- * A task's terminal, as it exists locally. The PAYLOAD is not stored — only
128
- * its hash, plus enough retry state to know whether the cloud has taken it.
127
+ * One immutable canonical terminal and its delivery projection. Bytes are the
128
+ * replay authority; the hash is checked against them, never used as a substitute.
129
129
  */
130
130
  export interface LocalTerminalRecord {
131
131
  readonly taskId: string;
132
- readonly terminalType: 'complete' | 'failed' | 'cancelled';
132
+ readonly terminalType: 'complete' | 'failed' | 'cancelled' | 'declined';
133
+ readonly bytes: string;
133
134
  readonly payloadHash: string;
134
135
  readonly truthState: TerminalTruthState;
135
136
  /** How many times delivery to the cloud has been attempted. */
136
137
  readonly attempt: number;
137
138
  readonly lastError?: string;
138
139
  readonly recordedAt: string;
140
+ /** Committed atomically with the original interruption report. */
141
+ readonly recovery?: RecoveryOutcome;
139
142
  }
140
143
  /** A task the journal knows about that has no terminal and no recovery marker — i.e. one this daemon was in the middle of when it stopped. */
141
144
  export interface RecoverableTask {
@@ -147,6 +150,7 @@ export interface RecoverableTask {
147
150
  readonly claimedRuntime?: string;
148
151
  readonly workspaceRef?: string;
149
152
  readonly updatedAt: string;
153
+ readonly envelopeBytes: string;
150
154
  }
151
155
  /**
152
156
  * What recovery decided about a task. `interrupted` is the honest default for
@@ -262,6 +266,14 @@ export interface LocalTaskJournal {
262
266
  recordTransition(record: LocalTransitionRecord): Promise<void>;
263
267
  /** Record (or update the retry state of) a task's terminal. Idempotent by task id: a replay with the same payload hash is a no-op beyond retry bookkeeping. */
264
268
  recordTerminal(record: LocalTerminalRecord): Promise<void>;
269
+ /** Exact original terminal bytes, including rejected records, until acknowledged. */
270
+ listPendingTerminals(identity: JournalIdentity): Promise<LocalTerminalRecord[]>;
271
+ /** A successful authenticated transport disposition, bound to the original bytes. */
272
+ confirmTerminal(taskId: string, payloadHash: string): Promise<void>;
273
+ rejectTerminal(taskId: string, payloadHash: string, reason: string): Promise<void>;
274
+ /** Includes old interruption markers without reports; they must not hide pending work. */
275
+ listRecoveryTasks(identity: JournalIdentity): Promise<RecoverableTask[]>;
276
+ readTask(taskId: string, identity: JournalIdentity): Promise<RecoverableTask | undefined>;
265
277
  /** Tasks with no terminal and no recovery marker — what this daemon was in the middle of when it last stopped. */
266
278
  listRecoverable(): Promise<RecoverableTask[]>;
267
279
  /** Close out one recoverable task by writing its recovery marker. Never deletes; a marked row is on §12.7.2.1's never-auto-delete list. */
@@ -1,4 +1,4 @@
1
- import { type AdmissionRecord, type CategoryUsage, type CleanableCategory, type CleanupCandidate, type CleanupResult, type CompactOptions, type CompactResult, type JournalReceipt, type LocalStorageUsage, type LocalTaskJournal, type LocalTerminalRecord, type LocalTransitionRecord, type RecoverableTask, type RecoveryOutcome, type ReceivedEnvelopeRecord, type StorageCategory } from './journal';
1
+ import { type JournalIdentity, type AdmissionRecord, type CategoryUsage, type CleanableCategory, type CleanupCandidate, type CleanupResult, type CompactOptions, type CompactResult, type JournalReceipt, type LocalStorageUsage, type LocalTaskJournal, type LocalTerminalRecord, type LocalTransitionRecord, type RecoverableTask, type RecoveryOutcome, type ReceivedEnvelopeRecord, type StorageCategory } from './journal';
2
2
  import { JournalHandleCleanupError, type JournalOpenFaultSeam } from './sqlite-support';
3
3
  export { JournalHandleCleanupError };
4
4
  /** The single database file, per §12.7.2's "建议单库 `<storeDir>/daemon.db`". */
@@ -24,7 +24,7 @@ export declare const DEFAULT_JOURNAL_BUSY_TIMEOUT_MS = 5000;
24
24
  * (`util/secure-dir.ts`): a seam the production path never supplies, exercised
25
25
  * from any host.
26
26
  */
27
- export type JournalFaultStep = 'append:before-begin' | 'append:after-envelope' | 'append:after-task' | 'append:after-receipt' | 'append:before-commit' | 'admission:before-commit' | 'transition:before-commit' | 'terminal:before-commit' | 'recovery:before-commit' | 'cleanup:before-commit' | 'prune:before-commit';
27
+ export type JournalFaultStep = 'append:before-begin' | 'append:after-envelope' | 'append:after-task' | 'append:after-receipt' | 'append:before-commit' | 'append:after-commit' | 'admission:before-commit' | 'transition:before-commit' | 'terminal:before-commit' | 'terminal:after-commit' | 'recovery:after-commit' | 'confirm:before-commit' | 'confirm:after-commit' | 'recovery:before-commit' | 'cleanup:before-commit' | 'prune:before-commit';
28
28
  export interface JournalFaultSeam {
29
29
  /** Throw to simulate a crash or IO error at exactly this step. Return normally to proceed. */
30
30
  onStep?(step: JournalFaultStep): void;
@@ -79,6 +79,11 @@ export declare class SqliteLocalTaskJournal implements LocalTaskJournal {
79
79
  * first fact stands.
80
80
  */
81
81
  recordTerminal(record: LocalTerminalRecord): Promise<void>;
82
+ readTask(taskId: string, identity: JournalIdentity): Promise<RecoverableTask | undefined>;
83
+ listRecoveryTasks(identity: JournalIdentity): Promise<RecoverableTask[]>;
84
+ listPendingTerminals(identity: JournalIdentity): Promise<LocalTerminalRecord[]>;
85
+ confirmTerminal(taskId: string, payloadHash: string): Promise<void>;
86
+ rejectTerminal(taskId: string, payloadHash: string, reason: string): Promise<void>;
82
87
  /**
83
88
  * What this daemon was in the middle of: a task whose offer envelope is
84
89
  * durable, that has no terminal, that was not declined, and that recovery
@@ -49,56 +49,8 @@ export interface LongPollClientOptions {
49
49
  onRevoked?: () => void;
50
50
  /** Called when the server cannot replay the durable cursor supplied to this poll. */
51
51
  onReplayCursorTooOld?: (error: ReplayCursorTooOldError) => void;
52
- /**
53
- * M4 Phase 4 (version-negotiation drill fix), scope narrowed by finding F1:
54
- * called ONLY for a batch entry that failed to parse because its `type`
55
- * is entirely unrecognized (`parseMessage` throwing
56
- * {@link UnknownMessageTypeError}) and which still carries a
57
- * numeric envelope-level `seq` AND a recognizably task-class `type` (a
58
- * `task.` prefix — see `extractSkippableSeq`'s own doc comment for why a
59
- * `conn.*`-shaped or type-less entry is deliberately excluded, mirroring
60
- * F2's "conn.* is never cursor-tracked" rule), so the caller can advance
61
- * its cursor/watermark past it even though there is no real `Envelope` to
62
- * hand to `onEnvelope`. Without this, a persistently-redelivered
63
- * unrecognized-type entry (the real server retains and redelivers an
64
- * un-acked envelope, protocol §9) would keep reappearing at the same
65
- * cursor position forever.
66
- *
67
- * Finding F1: a RECOGNIZED type that fails schema validation
68
- * ({@link EnvelopeValidationError} — e.g. a `task.offer` whose
69
- * `PermissionPolicy` rejects an unknown constraint) is deliberately NOT
70
- * reported here. That failure is a genuinely malformed control message,
71
- * not forward-compat tolerance — forwarding its `seq` here would
72
- * permanently ack a message the daemon never actually understood (the
73
- * server would stop redelivering it, silently stranding whatever it was
74
- * offering). This callback being scoped to `UnknownMessageTypeError` only
75
- * preserves the no-silent-permanent-ack property. Optional
76
- * only for constructor/test convenience — `ConnectionManager` always
77
- * supplies it.
78
- */
79
- onSkippedSeq?: (seq: number) => void;
80
- /**
81
- * Finding R1 (cross-model re-review — the F1 fix alone was NOT-CLOSED):
82
- * called for a batch entry whose `type` WAS recognized but whose payload
83
- * failed schema validation ({@link EnvelopeValidationError}) — a genuine
84
- * delivery failure at that specific seq, not forward-compat tolerance
85
- * (contrast {@link onSkippedSeq}, which is scoped to the opposite case,
86
- * an entirely unrecognized type). F1's own fix — simply not forwarding
87
- * this seq to `onSkippedSeq` — turned out to be insufficient on its own:
88
- * a LATER valid envelope in the same or a later batch would still
89
- * silently advance the durable cursor PAST this seq once its own handler
90
- * succeeded, since nothing had told `ConnectionManager` this seq needed
91
- * the same stall treatment a thrown handler failure already gets — an
92
- * INDIRECT permanent ack, one hop removed from the exact bug F1 set out
93
- * to fix. `ConnectionManager` (`noteValidationFailure`) engages
94
- * `stalledAtSeq` for this seq the same way `process()`'s own catch block
95
- * does for a real thrown handler — freezing `dedupWatermark()` at the
96
- * durable cursor (so the server's retain-and-redeliver semantics,
97
- * protocol §9, keep this seq alive) and, via that SAME existing
98
- * machinery, holding back the cursor for anything else delivered after it
99
- * in the same batch too, exactly as a real handler failure already would.
100
- * Optional only for constructor/test convenience — `ConnectionManager`
101
- * always supplies it.
52
+ /** Unknown or malformed executable messages have no durable disposition.
53
+ * Freeze their sequence; a later valid message must not acknowledge them.
102
54
  */
103
55
  onValidationFailedSeq?: (seq: number) => void;
104
56
  /**
@@ -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,9 @@ export interface TaskRunnerDeps {
228
228
  agentSessionHandoffs?: AgentSessionHandoffStore;
229
229
  deviceId: string;
230
230
  send: (envelope: Envelope) => void;
231
+ awaitTerminalCommit?: (taskId: string) => Promise<void>;
232
+ /** Fsync the execution commitment before claim/runtime side effects. */
233
+ beforeClaim?: (taskId: string, runtime: string) => Promise<void>;
231
234
  blobClient: BlobResolver;
232
235
  batcherOptions?: ProgressBatcherOptions;
233
236
  /**
@@ -321,6 +324,7 @@ export interface TaskRunnerDeps {
321
324
  onApprovalDispatched?: (taskId: string, approvalId: string) => void;
322
325
  /** Overrides the bounded soft-interrupt window before authoritative `Session.close()` disposal begins. */
323
326
  shutdownInterruptTimeoutMs?: number;
327
+ startupTimeoutMs?: number;
324
328
  /**
325
329
  * M5 batch-3 (workstream 2): overrides {@link DEFAULT_MAX_TASK_OUTPUT_BYTES}
326
330
  * — see that constant's own doc comment and `DaemonConfig.maxTaskOutputBytes`
@@ -330,6 +334,19 @@ export interface TaskRunnerDeps {
330
334
  * interface (`shutdownInterruptTimeoutMs`, `approvalTimeoutMs`).
331
335
  */
332
336
  maxTaskOutputBytes?: number;
337
+ artifactLimits?: {
338
+ maxFileBytes: number;
339
+ maxTaskBytes: number;
340
+ };
341
+ /**
342
+ * Per-event inline ceiling for `tool_use.input` / `tool_result.output` —
343
+ * see `DaemonConfig.maxInlineEventBytes` (`create-daemon.ts`) for the full
344
+ * contract and {@link DEFAULT_MAX_INLINE_EVENT_BYTES} for the default.
345
+ * Validated (positive safe integer, at least
346
+ * `MIN_MAX_INLINE_EVENT_BYTES`) at the `DaemonConfig` layer, not here —
347
+ * this seam trusts its caller, same as `maxTaskOutputBytes` above.
348
+ */
349
+ maxInlineEventBytes?: number;
333
350
  /**
334
351
  * M4 (additive-minor, `task.approval_resolved`): the capabilities advertised
335
352
  * by the CURRENT transport's server (`conn.ack` on WS, the latest successful
@@ -569,6 +586,8 @@ export declare class TaskRunner {
569
586
  usesAgentEgress(taskId: string): boolean;
570
587
  /** M5 batch-3 (workstream 2): effective `maxTaskOutputBytes` cap for this daemon — see {@link DEFAULT_MAX_TASK_OUTPUT_BYTES}'s own doc comment. */
571
588
  private get maxTaskOutputBytes();
589
+ /** Effective per-event inline ceiling for this daemon — see `DaemonConfig.maxInlineEventBytes`. */
590
+ private get maxInlineEventBytes();
572
591
  /** WP0: effective per-canonical-Agent-home Attempt cap — see {@link DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME}. */
573
592
  private get maxConcurrentMutableSessionsPerAgentHome();
574
593
  /**
@@ -614,8 +633,13 @@ export declare class TaskRunner {
614
633
  /** Restore activated, unaccepted message drafts before transport admission on daemon restart. */
615
634
  recoverAgentMessageOutboxes(agentsRoot: string): Promise<void>;
616
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;
617
638
  /** Retry stable recovered records after a transport handshake/re-handshake. */
618
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;
619
643
  private sendAgentMessageRecord;
620
644
  private handleAgentMessageDisposition;
621
645
  /** M4 Phase 2: stop claiming any FUTURE `task.offer` — see `stoppingOffers`'s own doc comment. Idempotent. */
@@ -657,6 +681,14 @@ export declare class TaskRunner {
657
681
  * ahead of this method — see `daemon-control-socket.test.ts`'s dedicated
658
682
  * regression test for the exact scenario.
659
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;
660
692
  shutdownActiveTasks(reason: string): Promise<void>;
661
693
  /**
662
694
  * M5 batch-3 (workstream 2): the ONE shared per-task teardown sequence —
@@ -688,6 +720,7 @@ export declare class TaskRunner {
688
720
  * a genuine protocol bug, not a benign race — mirrors `pump()`'s own
689
721
  * identity-check guard for the same class of race.
690
722
  */
723
+ private interruptBounded;
691
724
  private teardownActiveTask;
692
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. */
693
726
  private shutdownTask;
@@ -1138,7 +1171,10 @@ export declare class TaskRunner {
1138
1171
  private persistAgentTerminalEvidence;
1139
1172
  private retryAgentTerminalEvidence;
1140
1173
  private reportAgentTerminalEvidenceFailure;
1174
+ retryTerminalFinalization(taskId: string): Promise<void>;
1175
+ private readonly finalizationAttempts;
1141
1176
  private finish;
1177
+ private finishOnce;
1142
1178
  private reserveSemanticTerminal;
1143
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). */
1144
1180
  private addFinishedTaskId;
@@ -191,6 +191,15 @@ export declare class LocalTeamWorkspace {
191
191
  expiresAt: string;
192
192
  }>>;
193
193
  postMessage(input: TeamPostMessageInput): Promise<TeamMessageAcceptedReceipt>;
194
+ /** Metadata-only operator notification view. Never advances delivery or acknowledgement. */
195
+ notificationSnapshot(input: TeamReadMessagesInput): Promise<{
196
+ workspaceId: string;
197
+ memberId: string;
198
+ registryRevision: TeamWorkspaceRevision;
199
+ expiresAt: string;
200
+ acknowledgedThroughSeq: number;
201
+ latestPeerSeq: number | null;
202
+ }>;
194
203
  readMessages(input: TeamReadMessagesInput): Promise<TeamReadMessagesResult>;
195
204
  ackMessages(input: TeamAckMessagesInput): Promise<TeamAckReceipt>;
196
205
  private resolveLease;
@@ -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,90 +1,13 @@
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[];