@byok-sdk/client 0.12.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 (48) hide show
  1. package/README.md +93 -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 +433 -33
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/adapters/pi/events.d.ts +1 -1
  7. package/dist/adapters/pi/pi-adapter.d.ts +3 -0
  8. package/dist/adapters/pi/rpc-client.d.ts +47 -1
  9. package/dist/adapters/pi/subagents-policy-extension.js +1 -1
  10. package/dist/adapters/pi/subagents-policy-extension.js.map +1 -1
  11. package/dist/adapters/pi/team-interaction-extension.d.ts +24 -0
  12. package/dist/adapters/pi/team-interaction-extension.js +81 -0
  13. package/dist/adapters/pi/team-interaction-extension.js.map +1 -0
  14. package/dist/adapters/process-tree.d.ts +74 -4
  15. package/dist/adapters/provider-credential-environment.d.ts +1 -1
  16. package/dist/adapters/win32-job-object.d.ts +101 -0
  17. package/dist/agent-home.d.ts +74 -0
  18. package/dist/agent-memory/index.d.ts +1 -1
  19. package/dist/agent-memory/index.js.map +1 -1
  20. package/dist/bin/byok-agent-memory-mcp.js.map +1 -1
  21. package/dist/bin/byok-agent-message-mcp.js.map +1 -1
  22. package/dist/bin/byok-agent-team-mcp.js.map +1 -1
  23. package/dist/bin/byok-agent.js +9571 -8272
  24. package/dist/bin/byok-agent.js.map +1 -1
  25. package/dist/bin/byok-approval-mcp.js.map +1 -1
  26. package/dist/bin/commands/team-pi-relay.d.ts +24 -0
  27. package/dist/bin/commands/team-relay.d.ts +11 -0
  28. package/dist/bin/team-codex-relay.d.ts +32 -0
  29. package/dist/bin/team-notification-relay.d.ts +40 -0
  30. package/dist/bin/team-pi-session.d.ts +59 -0
  31. package/dist/daemon/agent-egress-policy.d.ts +8 -0
  32. package/dist/daemon/connection-manager.d.ts +66 -224
  33. package/dist/daemon/control-protocol.d.ts +10 -1
  34. package/dist/daemon/create-daemon.d.ts +58 -13
  35. package/dist/daemon/event-spill.d.ts +90 -0
  36. package/dist/daemon/journal/journal.d.ts +15 -3
  37. package/dist/daemon/journal/sqlite-journal.d.ts +7 -2
  38. package/dist/daemon/long-poll-transport.d.ts +28 -70
  39. package/dist/daemon/observer.d.ts +1 -1
  40. package/dist/daemon/task-runner.d.ts +31 -0
  41. package/dist/daemon/team-workspace.d.ts +9 -0
  42. package/dist/daemon/url.d.ts +10 -12
  43. package/dist/diagnostics/support-bundle.d.ts +1 -1
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.js +1334 -696
  46. package/dist/index.js.map +1 -1
  47. package/package.json +9 -8
  48. package/dist/daemon/ws-transport.d.ts +0 -139
@@ -1,10 +1,9 @@
1
1
  import type { AgentEgressPolicy, RuntimeId } from '@byok-sdk/protocol';
2
2
  import type { PermissionPolicy } from '@byok-sdk/protocol';
3
3
  import type { RuntimeAdapter, GitWorkspaceConfig, McpToolsetConfig, McpToolsetObservation, McpToolsetRegistryStatus, McpToolsetReloadReceipt } from '../types';
4
- import { type AgentHomeProjection } from '../agent-home';
4
+ import { type AgentHomeExecutionStatus, type AgentHomeProjection } from '../agent-home';
5
5
  import type { AgentRef } from '../agent-home';
6
6
  import { type LocalAgentReleaseIdentity } from '../release-identity';
7
- import type { BackoffOptions, LivenessOptions } from './ws-transport';
8
7
  import { type OperationalHealthSnapshot } from './operational-health';
9
8
  import { type DaemonEventListener, type DaemonTaskInfo, type Unsubscribe } from './observer';
10
9
  import { GitWorkspaceManager } from './git-workspace';
@@ -132,6 +131,26 @@ export interface DaemonConfig {
132
131
  * after the SDK-owned Agent home has passed construction-time preflight.
133
132
  */
134
133
  strictAgentOnly?: boolean;
134
+ /**
135
+ * WP0: how many Attempts this daemon lets execute CONCURRENTLY in one
136
+ * canonical Agent home, across every lane and every session. Default
137
+ * {@link DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME} (1).
138
+ *
139
+ * The canonical home is every Agent session's cwd, so each concurrent
140
+ * Attempt in it is another writer of the same `MEMORY.md`, `notes/` and
141
+ * `.git`. At the default, a second offer for a home that already has an
142
+ * active Attempt is declined retryably before adapter preparation, the
143
+ * claim, or any process side effect — the busy-home contract downstream
144
+ * hosts already depend on.
145
+ *
146
+ * Raising it above 1 is an explicit host choice that re-enables the
147
+ * 0.12.0 concurrent-session behaviour, including its co-writing exposure;
148
+ * the SDK never falls back to it on its own. Validated up front, the same
149
+ * way `maxTaskOutputBytes` is: a positive safe integer, so `0`, a negative
150
+ * number, `NaN` and a non-integer are construction errors rather than a
151
+ * silently reinterpreted "unlimited".
152
+ */
153
+ maxConcurrentMutableSessionsPerAgentHome?: number;
135
154
  /**
136
155
  * Explicit Agent-local/cloud egress selection. Omission still enforces the
137
156
  * SDK metadata/status projection, but does not advertise or admit the new
@@ -257,7 +276,7 @@ export interface DaemonConfig {
257
276
  /**
258
277
  * M5: explicit escape hatch for `url.ts`'s `assertServerUrlAllowed` — see
259
278
  * that function's own doc comment for the full allow/deny rule. Default
260
- * (unset/`false`): a `serverUrl` using plaintext `ws:`/`http:` is only
279
+ * (unset/`false`): a `serverUrl` using plaintext `http:` is only
261
280
  * accepted when its host is loopback (`localhost`/`*.localhost`,
262
281
  * `127.0.0.0/8`, `::1`); anything else over plaintext throws a typed
263
282
  * `InsecureServerUrlError` from `pair()`/`start()` below, BEFORE any
@@ -268,7 +287,7 @@ export interface DaemonConfig {
268
287
  * server) — doing so also logs a loud `console.warn` (see
269
288
  * `checkServerUrl`, this file) every time it actually changes the
270
289
  * outcome. Never overrides an unsupported scheme (anything other than
271
- * `http:`/`https:`/`ws:`/`wss:`), which is refused unconditionally.
290
+ * `http:`/`https:`), which is refused unconditionally.
272
291
  */
273
292
  dangerouslyAllowInsecureRemote?: boolean;
274
293
  /**
@@ -288,6 +307,36 @@ export interface DaemonConfig {
288
307
  * explicitly instead to opt out of enforcement altogether.
289
308
  */
290
309
  maxTaskOutputBytes?: number;
310
+ /**
311
+ * Per-EVENT inline ceiling (default {@link DEFAULT_MAX_INLINE_EVENT_BYTES},
312
+ * 64 KiB) for the two `AgentEvent` fields a runtime authors freely:
313
+ * `tool_use.input` and `tool_result.output`. An event whose serialization
314
+ * exceeds this leaves `TaskRunner.pump` with that field replaced by a
315
+ * UTF-8-safe head/tail preview (`{ preview: { head, tail } }`) and an
316
+ * additive `spill` descriptor; the full JSON serialization is uploaded to
317
+ * the blob plane under an idempotent, content-addressed key, and
318
+ * `spill.blob` is where a consumer reads it back. If the upload fails the
319
+ * preview still ships, carrying `spill.unstoredReason` instead — omission
320
+ * is always described, never silent.
321
+ *
322
+ * This is a per-event bound, orthogonal to `maxTaskOutputBytes` (a
323
+ * whole-task total, counted AFTER spilling) and to
324
+ * `progressBatch.maxBatchBytes` (a per-batch wire budget).
325
+ *
326
+ * **Consumer contract:** `spill`'s presence is the only signal that the
327
+ * inline field is a preview. A consumer that renders `tool_result.output`
328
+ * without checking `spill` renders a truncation as the whole result.
329
+ *
330
+ * Must be a positive safe integer of at least
331
+ * {@link MIN_MAX_INLINE_EVENT_BYTES} (4096) — below that a legitimate
332
+ * `spill` descriptor no longer fits inside the cap it exists to enforce.
333
+ * Anything else (0, negative, non-integer, `NaN`,
334
+ * `Number.POSITIVE_INFINITY`) is a config validation error thrown
335
+ * synchronously from `createDaemonWithAdapters`/`createDaemon`; there is
336
+ * no opt-out, because "unbounded event" is exactly the state this exists
337
+ * to prevent.
338
+ */
339
+ maxInlineEventBytes?: number;
291
340
  /**
292
341
  * Host-owned batching policy for normalized `task.progress` events.
293
342
  * `maxBatchBytes`, when set, measures exactly the UTF-8 bytes of
@@ -475,8 +524,6 @@ export interface DaemonStatus {
475
524
  localAgentRelease: Readonly<LocalAgentReleaseIdentity>;
476
525
  paired: boolean;
477
526
  connected: boolean;
478
- /** True once the connection has fallen back to long-poll (protocol §8) — transport info only (finding F6): long-poll is a full transport, so work still proceeds normally while this holds; outbound envelopes POST to /byok/messages instead of going out over WS. */
479
- degraded: boolean;
480
527
  /** True once the server has revoked this device (401 on challenge/token, protocol §6.3). The only recourse is calling `pair()` again — the daemon does not keep retrying on its own. */
481
528
  revoked: boolean;
482
529
  deviceId?: string;
@@ -489,6 +536,8 @@ export interface DaemonStatus {
489
536
  toolsets: McpToolsetRegistryStatus;
490
537
  /** Content-free egress lane watermarks and typed last-drop facts. */
491
538
  egress: AgentEgressStatus;
539
+ /** WP0: per-canonical-Agent-home execution serialization — see {@link AgentHomeExecutionStatus}. */
540
+ agentHomeExecution: AgentHomeExecutionStatus;
492
541
  }
493
542
  export interface Daemon {
494
543
  /** Pairing result is intentionally credential-blind. */
@@ -538,10 +587,10 @@ export interface Daemon {
538
587
  /** M3-2a: same as {@link approve} but rejects — see that method's doc comment. */
539
588
  reject(taskId: string, reason?: string): Promise<void>;
540
589
  }
541
- /** Internal seam so tests can substitute stub adapters / faster backoff+batch+liveness+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. */
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. */
542
591
  export interface DaemonOverrides {
543
- backoff?: BackoffOptions;
544
- liveness?: LivenessOptions;
592
+ /** Test-only synchronous kill points; never supplied by production configuration. */
593
+ executionRecoveryFault?: (step: 'terminal:before-send' | 'terminal:queued' | 'outbound:before-post' | 'outbound:after-ack') => void;
545
594
  /** 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. */
546
595
  approvalTimeoutMs?: number;
547
596
  /** 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. */
@@ -552,10 +601,6 @@ export interface DaemonOverrides {
552
601
  outboxDrainTimeoutMs?: number;
553
602
  };
554
603
  longPoll?: {
555
- /** Consecutive never-acked WS connect failures before falling back to long-poll. Default 3. */
556
- wsFailureThreshold?: number;
557
- /** While long-polling, how often to retry establishing WS. Default 5 minutes. */
558
- wsRetryIntervalMs?: number;
559
604
  /** Backoff between failed long-poll HTTP attempts. Default 2s. */
560
605
  retryDelayMs?: number;
561
606
  /** Minimum delay before the next long-poll request after an empty (no-events) response — avoids busy-looping against a server that responds instantly. Default 250ms. */
@@ -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
@@ -33,71 +33,24 @@ export interface LongPollClientOptions {
33
33
  serverUrl: string;
34
34
  auth: AuthManager;
35
35
  getCursor: () => number | undefined;
36
- onEnvelope: (envelope: Envelope) => void;
36
+ /** Returns false when the envelope was a local duplicate and no handler was queued. */
37
+ onEnvelope: (envelope: Envelope) => boolean | void;
37
38
  /**
38
39
  * Capabilities advertised by the server that produced the current poll
39
40
  * response. Called before any envelopes from that response are delivered.
40
41
  * An older responder omitting the additive field is reported as `[]`.
41
42
  */
42
43
  onServerCapabilities?: (capabilities: string[]) => void;
44
+ /** Called when a failed poll invalidates the preceding response's capability snapshot. */
45
+ onServerCapabilitiesInvalidated?: () => void;
46
+ /** Called after a poll fails and before the retry delay begins. */
47
+ onPollFailure?: () => void;
43
48
  /** Called once the device is found to be revoked (401 surfaced through {@link AuthManager}) — the loop stops itself rather than retrying. */
44
49
  onRevoked?: () => void;
45
50
  /** Called when the server cannot replay the durable cursor supplied to this poll. */
46
51
  onReplayCursorTooOld?: (error: ReplayCursorTooOldError) => void;
47
- /**
48
- * M4 Phase 4 (version-negotiation drill fix), scope narrowed by finding F1:
49
- * called ONLY for a batch entry that failed to parse because its `type`
50
- * is entirely unrecognized (`parseMessage` throwing
51
- * {@link UnknownMessageTypeError} — mirrors `ws-transport.ts`'s identical
52
- * per-frame tolerance for that SPECIFIC failure) and which still carries a
53
- * numeric envelope-level `seq` AND a recognizably task-class `type` (a
54
- * `task.` prefix — see `extractSkippableSeq`'s own doc comment for why a
55
- * `conn.*`-shaped or type-less entry is deliberately excluded, mirroring
56
- * F2's "conn.* is never cursor-tracked" rule), so the caller can advance
57
- * its cursor/watermark past it even though there is no real `Envelope` to
58
- * hand to `onEnvelope`. Without this, a persistently-redelivered
59
- * unrecognized-type entry (the real server retains and redelivers an
60
- * un-acked envelope, protocol §9) would keep reappearing at the same
61
- * cursor position forever.
62
- *
63
- * Finding F1: a RECOGNIZED type that fails schema validation
64
- * ({@link EnvelopeValidationError} — e.g. a `task.offer` whose
65
- * `PermissionPolicy` rejects an unknown constraint) is deliberately NOT
66
- * reported here. That failure is a genuinely malformed control message,
67
- * not forward-compat tolerance — forwarding its `seq` here would
68
- * permanently ack a message the daemon never actually understood (the
69
- * server would stop redelivering it, silently stranding whatever it was
70
- * offering). The WS path never had this hazard (an unparseable WS frame
71
- * has no skip-side cursor bookkeeping at all — see
72
- * `ws-transport.ts` — so it simply gets redelivered later); this callback
73
- * being scoped to `UnknownMessageTypeError` only is what makes long-poll
74
- * match that same "no silent permanent ack" property for real. Optional
75
- * only for constructor/test convenience — `ConnectionManager` always
76
- * supplies it.
77
- */
78
- onSkippedSeq?: (seq: number) => void;
79
- /**
80
- * Finding R1 (cross-model re-review — the F1 fix alone was NOT-CLOSED):
81
- * called for a batch entry whose `type` WAS recognized but whose payload
82
- * failed schema validation ({@link EnvelopeValidationError}) — a genuine
83
- * delivery failure at that specific seq, not forward-compat tolerance
84
- * (contrast {@link onSkippedSeq}, which is scoped to the opposite case,
85
- * an entirely unrecognized type). F1's own fix — simply not forwarding
86
- * this seq to `onSkippedSeq` — turned out to be insufficient on its own:
87
- * a LATER valid envelope in the same or a later batch would still
88
- * silently advance the durable cursor PAST this seq once its own handler
89
- * succeeded, since nothing had told `ConnectionManager` this seq needed
90
- * the same stall treatment a thrown handler failure already gets — an
91
- * INDIRECT permanent ack, one hop removed from the exact bug F1 set out
92
- * to fix. `ConnectionManager` (`noteValidationFailure`) engages
93
- * `stalledAtSeq` for this seq the same way `process()`'s own catch block
94
- * does for a real thrown handler — freezing `dedupWatermark()` at the
95
- * durable cursor (so the server's retain-and-redeliver semantics,
96
- * protocol §9, keep this seq alive) and, via that SAME existing
97
- * machinery, holding back the cursor for anything else delivered after it
98
- * in the same batch too, exactly as a real handler failure already would.
99
- * Optional only for constructor/test convenience — `ConnectionManager`
100
- * 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.
101
54
  */
102
55
  onValidationFailedSeq?: (seq: number) => void;
103
56
  /**
@@ -114,7 +67,7 @@ export interface LongPollClientOptions {
114
67
  * `ConnectionManager` always supplies it.
115
68
  */
116
69
  isStalled?: () => boolean;
117
- /** Backoff between failed poll attempts (network/HTTP errors), AND between cycles that made no cursor progress while stalled (finding P2/Fix 2a — see {@link isStalled}). The reference server holds each successful, non-stalled request open ~50s itself (protocol §8), so this only matters when a request errors outright or is stalled. Default 2s. */
70
+ /** Backoff between failed poll attempts (network/HTTP errors), stalled cycles, and duplicate-only cycles that made no cursor progress. The reference server holds a genuinely idle request open ~50s itself (protocol §8). Default 2s. */
118
71
  retryDelayMs?: number;
119
72
  /** Deterministic delay authority for automatic failed/stalled cycles. */
120
73
  retryDelayForAttempt?: (attempt: number, baseDelayMs: number) => number;
@@ -128,24 +81,29 @@ export interface LongPollClientOptions {
128
81
  */
129
82
  idleDelayMs?: number;
130
83
  }
84
+ /** A fully-read frozen-v1 count acknowledgement for the batch that was posted. */
85
+ export interface MessageBatchPostResult {
86
+ readonly accepted: number;
87
+ readonly rejected?: number;
88
+ }
131
89
  /**
132
- * Protocol §8 long-poll fallback: `GET /byok/events?cursor=N` in a loop,
133
- * used while WS connectivity is unavailable (see `ConnectionManager`), plus
134
- * `POST /byok/messages` for the daemon's own outbound envelopes while in
135
- * this mode (finding F6 — long-poll is a full transport, not receive-only:
136
- * see docs/protocol.md §8).
90
+ * Protocol §8 long-poll transport: `GET /byok/events?cursor=N` in a loop,
91
+ * plus `POST /byok/messages` for the daemon's own outbound envelopes
92
+ * (finding F6 long-poll is a full transport, not receive-only: see
93
+ * docs/protocol.md §8).
137
94
  *
138
- * Design B (finding N4): this is a stateless drainer, symmetric with
139
- * `WsTransport.sendNow` it holds no outbound queue of its own.
140
- * `ConnectionManager` owns the single shared outbox both transports drain
141
- * from (so a transport switch never strands a queued envelope);
142
- * `postBatch` is a single POST attempt, reporting back whether the server
143
- * accepted it. All retry/backoff policy (and re-checking which transport is
144
- * currently active) lives in the caller (`ConnectionManager.drainOutbox`).
95
+ * Design B (finding N4): this is a stateless drainer; it holds no outbound
96
+ * queue of its own. `ConnectionManager` owns the single shared outbox;
97
+ * `postBatch` is a single POST attempt, reporting only frozen-v1 accepted and
98
+ * rejected counts after its response body has been read and validated. All
99
+ * retry/backoff and rejection isolation policy lives in the caller
100
+ * (`ConnectionManager.drainOutbox`).
145
101
  */
146
102
  export declare class LongPollClient {
147
103
  private readonly opts;
148
104
  private running;
105
+ /** Owns exactly one active loop generation, including its held GET and retry delays. */
106
+ private loopAbortController;
149
107
  /**
150
108
  * Finding R1: seqs this loop has already `console.warn`'d about for a
151
109
  * validation-failed (recognized-type, invalid-payload) entry — a poison
@@ -204,8 +162,8 @@ export declare class LongPollClient {
204
162
  * (`ConnectionHub.handleInbound`), so a resend of the SAME batch (same
205
163
  * envelope `id`s — the caller must never rebuild them) is deduped
206
164
  * server-side into a safe no-op rather than reprocessed (§9). Returns
207
- * `true` once the server has accepted the batch.
165
+ * validated frozen-v1 counts only after a readable response body.
208
166
  */
209
- postBatch(envelopes: Envelope[]): Promise<boolean>;
167
+ postBatch(envelopes: Envelope[]): Promise<MessageBatchPostResult | undefined>;
210
168
  private loop;
211
169
  }
@@ -1,5 +1,5 @@
1
1
  import { type AgentEvent, type BlobRef, type Envelope, type RuntimeInfo, type TaskState } from '@byok-sdk/protocol';
2
- import type { ConnectionState } from './ws-transport';
2
+ import type { ConnectionState } from './connection-manager';
3
3
  /**
4
4
  * M3-2a: local observability for the daemon — the seam a CLI (M3-2b) drives a
5
5
  * live task feed, a task list, and approve/reject/unpair from, all LOCALLY
@@ -177,6 +177,15 @@ export type ResultDocumentExtractor = (finalOutput: string, task: ResultDocument
177
177
  * opt-out pin.
178
178
  */
179
179
  export declare const DEFAULT_MAX_TASK_OUTPUT_BYTES: number;
180
+ /**
181
+ * WP0: default number of Attempts allowed to execute concurrently in one
182
+ * canonical Agent home. One — the canonical home is every Agent session's
183
+ * cwd, so a second concurrent Attempt is a second writer of the same
184
+ * `MEMORY.md`, `notes/` and `.git`. Raising it is an explicit host choice
185
+ * (`DaemonConfig.maxConcurrentMutableSessionsPerAgentHome`) that re-enables
186
+ * the 0.12.0 co-writing exposure; there is no implicit fallback to it.
187
+ */
188
+ export declare const DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME = 1;
180
189
  export interface TaskRunnerDeps {
181
190
  adapters: RuntimeAdapter[];
182
191
  runtimeAllowlist?: string[];
@@ -204,6 +213,13 @@ export interface TaskRunnerDeps {
204
213
  agentHome?: AgentHomeManager;
205
214
  /** Local authority: legacy offers are declined after journal/dedup/cancel precedence. */
206
215
  strictAgentOnly?: boolean;
216
+ /**
217
+ * WP0: how many Attempts may execute concurrently in ONE canonical Agent
218
+ * home — see `DaemonConfig.maxConcurrentMutableSessionsPerAgentHome`'s own
219
+ * doc comment (`create-daemon.ts`) for the validated contract. Unset
220
+ * defaults to {@link DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME}.
221
+ */
222
+ maxConcurrentMutableSessionsPerAgentHome?: number;
207
223
  /** Exact host-selected policy accepted by `task.offer_for_agent_with_egress`. */
208
224
  agentEgressPolicy?: Readonly<AgentEgressPolicy>;
209
225
  /** Always-present projection/sanitizer consumer; it defaults to metadata-only. */
@@ -212,6 +228,8 @@ export interface TaskRunnerDeps {
212
228
  agentSessionHandoffs?: AgentSessionHandoffStore;
213
229
  deviceId: string;
214
230
  send: (envelope: Envelope) => void;
231
+ /** Fsync the execution commitment before claim/runtime side effects. */
232
+ beforeClaim?: (taskId: string, runtime: string) => Promise<void>;
215
233
  blobClient: BlobResolver;
216
234
  batcherOptions?: ProgressBatcherOptions;
217
235
  /**
@@ -314,6 +332,15 @@ export interface TaskRunnerDeps {
314
332
  * interface (`shutdownInterruptTimeoutMs`, `approvalTimeoutMs`).
315
333
  */
316
334
  maxTaskOutputBytes?: number;
335
+ /**
336
+ * Per-event inline ceiling for `tool_use.input` / `tool_result.output` —
337
+ * see `DaemonConfig.maxInlineEventBytes` (`create-daemon.ts`) for the full
338
+ * contract and {@link DEFAULT_MAX_INLINE_EVENT_BYTES} for the default.
339
+ * Validated (positive safe integer, at least
340
+ * `MIN_MAX_INLINE_EVENT_BYTES`) at the `DaemonConfig` layer, not here —
341
+ * this seam trusts its caller, same as `maxTaskOutputBytes` above.
342
+ */
343
+ maxInlineEventBytes?: number;
317
344
  /**
318
345
  * M4 (additive-minor, `task.approval_resolved`): the capabilities advertised
319
346
  * by the CURRENT transport's server (`conn.ack` on WS, the latest successful
@@ -553,6 +580,10 @@ export declare class TaskRunner {
553
580
  usesAgentEgress(taskId: string): boolean;
554
581
  /** M5 batch-3 (workstream 2): effective `maxTaskOutputBytes` cap for this daemon — see {@link DEFAULT_MAX_TASK_OUTPUT_BYTES}'s own doc comment. */
555
582
  private get maxTaskOutputBytes();
583
+ /** Effective per-event inline ceiling for this daemon — see `DaemonConfig.maxInlineEventBytes`. */
584
+ private get maxInlineEventBytes();
585
+ /** WP0: effective per-canonical-Agent-home Attempt cap — see {@link DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME}. */
586
+ private get maxConcurrentMutableSessionsPerAgentHome();
556
587
  /**
557
588
  * M4 Phase 4 (part B.3, observability): per-active-task queue watermarks
558
589
  * for the control socket's `status` result — see
@@ -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;
@@ -1,7 +1,5 @@
1
- /** Normalize a configured `serverUrl` (http/https/ws/wss, any path) to an http(s) base with no path/query. */
1
+ /** Normalize a configured HTTP(S) `serverUrl` (any path) to a base with no path/query. */
2
2
  export declare function toHttpBase(serverUrl: string): string;
3
- /** Derive the `/byok/ws` WebSocket URL from a configured `serverUrl`. */
4
- export declare function toWsUrl(serverUrl: string): string;
5
3
  /**
6
4
  * Which route a transport diagnostic is about, in the only two fields that
7
5
  * are safe to keep: the host (with port) and the path.
@@ -15,7 +13,7 @@ export declare function toWsUrl(serverUrl: string): string;
15
13
  * by formatting a raw URL of its own.
16
14
  */
17
15
  export interface TransportEndpoint {
18
- readonly transport: 'ws' | 'long-poll';
16
+ readonly transport: 'long-poll';
19
17
  /** `URL.host` — hostname plus port when non-default. Never userinfo. */
20
18
  readonly host: string;
21
19
  /** `URL.pathname` — no query, no fragment. */
@@ -43,7 +41,7 @@ export declare class InsecureServerUrlError extends Error {
43
41
  }
44
42
  export interface AssertServerUrlAllowedOptions {
45
43
  /**
46
- * Explicit escape hatch: when `true`, a `http:`/`ws:` `serverUrl` whose
44
+ * Explicit escape hatch: when `true`, an `http:` `serverUrl` whose
47
45
  * host is NOT loopback is allowed through instead of throwing. Does
48
46
  * nothing for an unsupported scheme (see {@link assertServerUrlAllowed}'s
49
47
  * own doc comment) — that rejection is unconditional. Threaded from
@@ -57,25 +55,25 @@ export interface AssertServerUrlAllowedOptions {
57
55
  }
58
56
  /**
59
57
  * M5: transport-security gate for a configured `serverUrl` — refuses
60
- * plaintext (`ws:`/`http:`) transport to any non-loopback host, so a device
58
+ * plaintext (`http:`) transport to any non-loopback host, so a device
61
59
  * can never be talked into pairing with (and sending its pairing code /
62
60
  * device credentials to) a remote host in the clear. Call this ONCE at each
63
61
  * real entry point a raw, operator-supplied `serverUrl` first enters the
64
62
  * client (`create-daemon.ts`'s `pair()`/`start()`) rather than inside
65
- * `toHttpBase`/`toWsUrl` themselves: ws-transport, the long-poll fallback,
66
- * and blob-client all read `serverUrl` from that SAME `DaemonConfig`, never
63
+ * `toHttpBase` itself: long-poll and blob-client both read `serverUrl` from
64
+ * that same `DaemonConfig`, never
67
65
  * an independently-supplied URL of their own, so those two call sites are
68
66
  * already the single common path every one of them goes through.
69
67
  *
70
68
  * Rules, checked in order:
71
- * - `https:`/`wss:` — always allowed, any host (TLS is the actual
69
+ * - `https:` — always allowed, any host (TLS is the actual
72
70
  * plaintext-network defense; this gate has nothing further to add there).
73
- * - `http:`/`ws:` — allowed only when the hostname is loopback: exactly
71
+ * - `http:` — allowed only when the hostname is loopback: exactly
74
72
  * `localhost` or any `*.localhost` subdomain, an IPv4 literal in
75
73
  * `127.0.0.0/8`, or the IPv6 loopback `::1` — see {@link isLoopbackHostname}.
76
- * - `http:`/`ws:` to any other host — refused with a clear, typed
74
+ * - `http:` to any other host — refused with a clear, typed
77
75
  * {@link InsecureServerUrlError} naming the redacted scheme/host/path and the fix
78
- * (use `wss:`/`https:`, or pass `dangerouslyAllowInsecureRemote: true` if
76
+ * (use `https:`, or pass `dangerouslyAllowInsecureRemote: true` if
79
77
  * this is a deliberate, understood exception) — UNLESS
80
78
  * `opts.dangerouslyAllowInsecureRemote` is `true`.
81
79
  * - Any other scheme (or a `serverUrl` that fails to parse as a URL at
@@ -28,7 +28,7 @@ export interface SupportBundle {
28
28
  status: 'online';
29
29
  pid: number;
30
30
  uptimeMs: number;
31
- transport: 'connecting' | 'open' | 'closed' | 'degraded' | 'revoked' | 'unavailable';
31
+ transport: 'connecting' | 'open' | 'closed' | 'revoked' | 'unavailable';
32
32
  activeTaskCount: number;
33
33
  pendingApprovalCount: number;
34
34
  operationalHealth: {
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export type { AgentRef } from './agent-home';
3
3
  export { AgentHomeError, AgentRefValidationError, AgentHomeResolutionError, AgentHomeCollisionError, AgentHomeBusyError, AgentHomeLeaseCorruptError, AgentHomeLayout, AgentHomeLeaseManager, AgentHomeManager, createAgentHomeProjection, createAgentHomeProjectionConsumer, AGENT_HOME_PROJECTION_STATE_FILE, stableAgentHomeOwnerId, validateAgentRef, } from './agent-home';
4
4
  export { AgentSessionHandoffStore, AgentSessionHandoffStoreError, AgentSessionHandoffCorruptError, AgentSessionHandoffMismatchError, } from './daemon/agent-session-handoff-store';
5
5
  export type { AgentSessionHandoff, AgentSessionHandoffMatch, AgentTaskTerminalEvidence, AgentTaskTerminalMatch, AgentTerminalCause, } from './daemon/agent-session-handoff-store';
6
- export type { AgentHomeResolution, AgentHomeProjection, AgentHomeProjectionInput, AgentHomeProjectionApplyInput, AgentHomeProjectionFunction, AgentHomeProjectionApplyFunction, AgentHomeLease, AgentHomeBinding, AgentHomeExecutionLease, AgentHomeExecutionBinding, } from './agent-home';
6
+ export type { AgentHomeResolution, AgentHomeProjection, AgentHomeProjectionInput, AgentHomeProjectionApplyInput, AgentHomeProjectionFunction, AgentHomeProjectionApplyFunction, AgentHomeLease, AgentHomeBinding, AgentHomeExecutionLease, AgentHomeExecutionBinding, AgentHomeExecutionStatus, } from './agent-home';
7
7
  export { localStateRelocation, LocalStateRelocationError, LocalStateRelocationBusyError, LocalStateRelocationIntegrityError, } from './local-state-relocation';
8
8
  export type { LocalStateRelocationInput, LocalStateRelocationLease, } from './local-state-relocation';
9
9
  export { PolicyUnsupportedError, SteerUnsupportedError, freezeRuntimeAdapterDescriptor, sealRuntimeOperationManifest } from './types';
@@ -61,7 +61,7 @@ export { SKILL_PACKS_CAPABILITY, SKILL_PACKS_DIRNAME, SKILL_PACK_AUDIT_FILENAME,
61
61
  export type { InstallSkillPacksOptions, InstalledSkillPack, ProjectedSkillPack, SkillPackInstallErrorCode, SkillPackInstallResult, SkillPackLock, } from './daemon/skill-pack-installer';
62
62
  export { TruthMemoryClient, TruthMemoryClientError } from './daemon/truth-memory-client';
63
63
  export type { LocalMemoryFilter, MemorySelector, TruthManifestQueryInput, TruthManifestRecord, TruthMemoryClientErrorCode, TruthMemoryClientOptions, TruthMemoryMetric, TruthSnapshotCandidateInput, TruthSnapshotWriteInput, TruthTerminalWriteInput, TruthWriteBody, TruthWriteResult, VerifiedTruthRecord, } from './daemon/truth-memory-client';
64
- export type { ConnectionState } from './daemon/ws-transport';
64
+ export type { ConnectionState } from './daemon/connection-manager';
65
65
  export { ReplayCursorTooOldError } from './daemon/replay-cursor';
66
66
  export { BlobClient, BlobRequestAbortedError } from './daemon/blob-client';
67
67
  export type { BlobClientOptions, BlobRequestAbortReason, BlobRequestOptions, BlobResolver } from './daemon/blob-client';