@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
@@ -0,0 +1,11 @@
1
+ import type { DaemonConfig } from '../../daemon/create-daemon';
2
+ /** One foreground owner per room. Stale locks are never guessed away or stolen. */
3
+ export declare function acquireTeamRelayLock(storeDir: string, workspaceId: string): Promise<() => Promise<void>>;
4
+ export declare function runTeamRelayCommand(input: {
5
+ config: DaemonConfig;
6
+ workspaceId: string;
7
+ bindingsFile: string;
8
+ codexBin: string;
9
+ maxNotifications: number;
10
+ signal: AbortSignal;
11
+ }): Promise<void>;
@@ -0,0 +1,32 @@
1
+ import { type TeamMemberLease } from '../daemon/team-workspace';
2
+ export interface CodexTeamBinding {
3
+ readonly context: string;
4
+ readonly lease: TeamMemberLease;
5
+ readonly threadId: string;
6
+ readonly endpoint: string;
7
+ readonly afterSeq: number;
8
+ }
9
+ export interface TeamNotificationSnapshot {
10
+ workspaceId: string;
11
+ memberId: string;
12
+ registryRevision: string;
13
+ expiresAt: string;
14
+ acknowledgedThroughSeq: number;
15
+ latestPeerSeq: number | null;
16
+ }
17
+ /** Local endpoints only. Never select a default daemon or infer a thread from its name. */
18
+ export declare function validateCodexRelayEndpoint(value: unknown): asserts value is string;
19
+ export declare function parseCodexTeamBinding(binding: unknown, workspaceId: string): CodexTeamBinding;
20
+ export declare function parseCodexTeamBindings(value: unknown, workspaceId: string): readonly CodexTeamBinding[];
21
+ export declare function loadPrivateTeamDocument(file: string): Promise<unknown>;
22
+ export declare function loadCodexTeamBindings(file: string, workspaceId: string): Promise<readonly CodexTeamBinding[]>;
23
+ export declare function codexTeamNotification(workspaceId: string, throughSeq: number): string;
24
+ /** The native queue receipt contract is qualified against this CLI version. */
25
+ export declare function preflightCodexRelay(codexBin: string, signal: AbortSignal): Promise<string>;
26
+ /** Only a confirmed exact-thread queue receipt advances this epoch's notification watermark. */
27
+ export declare function queueCodexTeamNotification(input: {
28
+ codexBin: string;
29
+ binding: CodexTeamBinding;
30
+ throughSeq: number;
31
+ signal: AbortSignal;
32
+ }): Promise<string>;
@@ -0,0 +1,40 @@
1
+ import type { TeamMemberLease } from '../daemon/team-workspace';
2
+ export interface TeamRelayBinding {
3
+ readonly context: string;
4
+ readonly lease: TeamMemberLease;
5
+ readonly afterSeq: number;
6
+ }
7
+ export type TeamRelayState = 'running' | 'paused' | 'stopped' | 'budget_exhausted' | 'failed';
8
+ export declare class TeamNotificationRelay<T extends TeamRelayBinding> {
9
+ private readonly options;
10
+ private state;
11
+ private attempts;
12
+ private error;
13
+ private readonly watermarks;
14
+ private pending;
15
+ private readonly abort;
16
+ constructor(options: {
17
+ bindings: readonly T[];
18
+ maxNotifications: number;
19
+ snapshot: (binding: T, afterSeq: number) => Promise<unknown>;
20
+ describe: (binding: T) => Record<string, string>;
21
+ ready?: (binding: T) => Promise<boolean>;
22
+ enqueue: (binding: T, throughSeq: number, signal: AbortSignal) => Promise<string>;
23
+ });
24
+ status(): {
25
+ state: TeamRelayState;
26
+ attempts: number;
27
+ maxNotifications: number;
28
+ error?: "queue_delivery_unknown" | "snapshot_failed" | undefined;
29
+ bindings: {
30
+ workspaceId: string;
31
+ memberId: string;
32
+ notifiedThroughSeq: number | undefined;
33
+ }[];
34
+ };
35
+ pause(): void;
36
+ resume(): void;
37
+ stop(): void;
38
+ tick(): Promise<void>;
39
+ private performTick;
40
+ }
@@ -0,0 +1,59 @@
1
+ export interface PiInteractionResponse {
2
+ sessionId: string;
3
+ requestId: string;
4
+ response: {
5
+ cancelled: true;
6
+ } | {
7
+ confirmed: boolean;
8
+ } | {
9
+ value: string;
10
+ };
11
+ }
12
+ export interface PiTeamSessionOptions {
13
+ workspaceId: string;
14
+ cwd: string;
15
+ sessionDir: string;
16
+ provider: string;
17
+ model: string;
18
+ systemPrompt: string;
19
+ mcpConfig: Record<string, unknown>;
20
+ extensionPaths?: readonly string[];
21
+ onEvent: (event: Record<string, unknown>) => void;
22
+ }
23
+ /** One owned RPC child; GUI replies never share a model-controlled tool channel. */
24
+ export declare class PiTeamSession {
25
+ private readonly options;
26
+ private client;
27
+ private sessionId;
28
+ private revision;
29
+ private phase;
30
+ private readonly interactions;
31
+ private readonly replying;
32
+ private stopping;
33
+ private active;
34
+ private pendingInputs;
35
+ private readonly privateFiles;
36
+ private constructor();
37
+ static start(options: PiTeamSessionOptions): Promise<PiTeamSession>;
38
+ status(): {
39
+ sessionId: string | undefined;
40
+ phase: "closed" | "failed" | "open" | "starting" | "waiting";
41
+ revision: number;
42
+ pendingUi: {
43
+ id: string | undefined;
44
+ method: unknown;
45
+ responding: boolean;
46
+ }[];
47
+ };
48
+ private fail;
49
+ private onFrame;
50
+ private onInteraction;
51
+ private request;
52
+ private state;
53
+ ready(): Promise<boolean>;
54
+ notify(throughSeq: number, signal: AbortSignal): Promise<string>;
55
+ sendInput(message: string): Promise<string>;
56
+ respond(input: PiInteractionResponse): Promise<void>;
57
+ drain(signal: AbortSignal): Promise<void>;
58
+ stop(): Promise<void>;
59
+ }
@@ -31,6 +31,14 @@ export declare function resolveAgentEgressPolicy(policy: AgentEgressPolicy | und
31
31
  * Default activity projection. Every retained string is SDK-authored; no
32
32
  * runtime trajectory, tool, prompt, environment, argv, path, or credential
33
33
  * value survives this transformation.
34
+ *
35
+ * Each case CONSTRUCTS a fresh event from SDK-authored literals rather than
36
+ * editing the incoming one, which is what makes the guarantee total rather
37
+ * than a list of fields someone remembered to strip. `spill` on
38
+ * `tool_use`/`tool_result` is covered by exactly that: a `BlobRef` is a
39
+ * readable locator for the omitted tool payload — content, not metadata — so
40
+ * it never survives a metadata-status projection, and neither do the byte
41
+ * counts that would leak the payload's size.
34
42
  */
35
43
  export declare function metadataStatusEvent(event: AgentEvent): AgentEvent;
36
44
  export declare function eventBytes(event: AgentEvent): number;
@@ -3,18 +3,18 @@ import { AuthManager } from './auth-manager';
3
3
  import type { CursorStore } from './cursor-store';
4
4
  import { type FleetJitter } from './deterministic-jitter';
5
5
  import { ReplayCursorTooOldError } from './replay-cursor';
6
- import { type BackoffOptions, type ConnectionState, type LivenessOptions } from './ws-transport';
7
- export type { ConnectionState } from './ws-transport';
8
6
  export { ReplayCursorTooOldError } from './replay-cursor';
7
+ /** The lifecycle of the daemon's one long-poll connection. */
8
+ export type ConnectionState = 'connecting' | 'open' | 'closed' | 'revoked';
9
9
  export interface ConnectionManagerOptions {
10
10
  serverUrl: string;
11
11
  deviceId: string;
12
12
  productId: string;
13
13
  capabilities: CapabilityFlag[];
14
- /** U4a Local Agent release version; passed unchanged to both transports. */
14
+ /** U4a Local Agent release version, sent unchanged in `conn.hello`. */
15
15
  clientVersion?: string;
16
16
  runtimes: RuntimeInfo[];
17
- /** Reads current sorted logical IDs from the validated local registry for every WS hello. */
17
+ /** Reads current sorted logical IDs from the validated local registry for every `conn.hello`. */
18
18
  getConfiguredToolsets?: () => readonly ToolsetId[];
19
19
  auth: AuthManager;
20
20
  cursorStore: CursorStore;
@@ -25,12 +25,11 @@ export interface ConnectionManagerOptions {
25
25
  */
26
26
  onEnvelope: (envelope: Envelope) => void | Promise<void>;
27
27
  onStateChange?: (state: ConnectionState) => void;
28
- backoff?: BackoffOptions;
29
- liveness?: LivenessOptions;
30
- /** Consecutive never-acked WS connect failures before falling back to long-poll (protocol §8). Default 3. */
31
- wsFailureThreshold?: number;
32
- /** While long-polling, how often to retry establishing WS (protocol §8, "e.g. every 5 min"). Default 5 minutes. */
33
- wsRetryIntervalMs?: number;
28
+ /** Await durable disposition before retiring the exact accepted/rejected bytes. */
29
+ onOutboundAccepted?: (envelopes: readonly Envelope[]) => Promise<void>;
30
+ onOutboundRejected?: (envelope: Envelope) => Promise<void>;
31
+ onOutboundQueued?: (envelope: Envelope) => void;
32
+ beforeOutboundPost?: (envelopes: readonly Envelope[]) => void;
34
33
  /** Backoff between failed long-poll HTTP attempts. Default 2s. */
35
34
  longPollRetryDelayMs?: number;
36
35
  /** Minimum delay before the next long-poll request after an empty (no-events) response. Default 250ms. */
@@ -39,33 +38,27 @@ export interface ConnectionManagerOptions {
39
38
  onOperationalOutcome?: (outcome: 'success' | 'failure', source: 'reconnect' | 'upload') => void;
40
39
  onTerminalError?: (error: ReplayCursorTooOldError) => void;
41
40
  }
41
+ export interface RejectedOutboundEnvelope {
42
+ readonly envelope: Envelope;
43
+ readonly reason: 'inbound_rejected';
44
+ }
42
45
  /**
43
- * Owns the daemon's one logical connection to the server, which may be
44
- * backed by either transport the wire protocol defines: WS (the normal
45
- * path) or long-poll (protocol §8's fallback for environments where an
46
- * outbound WSS connection isn't viable). Both funnel every received
47
- * envelope through the same cursor-dedupe/persistence logic (protocol §9),
48
- * so redelivery is safe regardless of which transport happens to deliver a
49
- * given envelope — including during the brief overlap window when handing
50
- * off between them.
46
+ * Owns the daemon's one authenticated long-poll connection to the server.
47
+ * Every received envelope passes through the same cursor-dedupe/persistence
48
+ * logic (protocol §9), so redelivery remains safe after an HTTP retry or a
49
+ * daemon restart.
51
50
  *
52
51
  * `send()` (Design B, finding N4) pushes onto a single shared outbox this
53
- * class owns and drains through whichever transport is currently active —
54
- * WS raw-sends while acked, `POST /byok/messages` while long-polling
55
- * (finding F6, long-poll is a full transport, not receive-only; see
56
- * docs/protocol.md §8) — so a transport switch mid-flight never strands a
57
- * queued envelope. See `drainOutbox`.
52
+ * class owns and drains through `POST /byok/messages`; long-poll is a full
53
+ * bidirectional transport, not a receive-only path. See `drainOutbox`.
58
54
  */
59
55
  export declare class ConnectionManager {
60
56
  private readonly opts;
61
57
  private readonly fleetJitter;
62
- private readonly ws;
63
58
  private readonly longPoll;
64
- private mode;
65
- private consecutiveFailures;
66
- private wsRetryTimer;
67
- private wsProbeSequence;
68
59
  private uploadRetryAttempt;
60
+ private started;
61
+ private connected;
69
62
  private cursor;
70
63
  /**
71
64
  * Finding F3 (at-most-once redelivery): the lowest `task.*` envelope `seq`
@@ -87,10 +80,9 @@ export declare class ConnectionManager {
87
80
  * (see `advanceCursor`) — that semantics is unchanged. `deliveredSeq`
88
81
  * advances eagerly, the instant a `task.*` envelope is admitted past
89
82
  * dedup (see `deliver`/`noteDelivered`), independent of whether its
90
- * handler has even started, let alone succeeded. It exists so a
91
- * long-poll re-query (`LongPollClient`'s `getCursor`) doesn't re-pull an
92
- * envelope that's already been delivered once and is still in flight
93
- * `handleOffer` is NOT idempotent and must never be re-pulled while a
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
94
86
  * first attempt is still running. On WS this same field is written the
95
87
  * same way, but since a live WS connection only ever pushes a given `seq`
96
88
  * once, it never has an observable effect there beyond mirroring
@@ -101,18 +93,18 @@ export declare class ConnectionManager {
101
93
  /** Finding F3: serializes `onEnvelope` calls into a per-connection FIFO — one envelope's handler always fully settles before the next one starts. */
102
94
  private processingChain;
103
95
  /**
104
- * Design B (finding N4): the ONE outbound queue both transports drain
105
- * from — holds `Envelope` OBJECTS, never re-encoded/rebuilt strings, so a
96
+ * Design B (finding N4): the ONE outbound queue holds `Envelope` OBJECTS,
97
+ * never re-encoded/rebuilt strings, so a
106
98
  * resend after a failed send attempt is byte-identical to the original
107
99
  * (same `id`), which is what lets the server's per-(deviceId,id) dedup
108
100
  * (Wave 1) recognize it as a safe no-op retry rather than a second
109
- * application (protocol §9). A transport switch (long-poll <-> WS) never
110
- * touches this queue — see `drainOutbox` — so nothing queued while one
111
- * transport was active is ever stranded when the other takes over.
101
+ * application (protocol §9).
112
102
  */
113
103
  private readonly outbox;
104
+ /** Terminally rejected outbound envelopes, retained as a bounded observable quarantine. */
105
+ private readonly rejectedOutboundEnvelopes;
114
106
  /**
115
- * Finding F5(b): how many envelopes `drainOutbox`'s long-poll branch has
107
+ * Finding F5(b): how many envelopes `drainOutbox` has
116
108
  * currently spliced OUT of `this.outbox` for an in-flight (not yet
117
109
  * confirmed delivered) `postBatch` call — 0 the rest of the time. See
118
110
  * `outboxLength`'s own doc comment for why this needs to be tracked
@@ -165,33 +157,12 @@ export declare class ConnectionManager {
165
157
  */
166
158
  private cancelPendingDrainRetry;
167
159
  /**
168
- * The capabilities the CURRENT transport's server advertised — untyped
169
- * `string[]` for forward compatibility. WS populates it from `conn.ack`;
170
- * long-poll populates it from each successful events response. Empty until
171
- * the active transport supplies an advertisement.
172
- *
173
- * Finding R2 (cross-model re-review — was P1): strictly PER-CONNECTION,
174
- * not per-daemon-lifetime. Cleared to `[]` the instant the acked WS
175
- * connection ends for ANY reason — an ordinary disconnect (`onWsOutcome`'s
176
- * `acked` branch), `stop()`, or a transport switch to long-poll
177
- * (`enterLongPoll`) — and only repopulated by a fresh advertisement from
178
- * the transport that is still current.
179
- * The previous version of this doc comment claimed long-poll mode simply
180
- * "stays at whatever the last real WS `conn.ack` said" — that was the bug:
181
- * a daemon that once learned e.g. `approval_resolved` from an earlier WS
182
- * session kept believing it applied to whatever it's connected to NOW,
183
- * even after a disconnect/degrade where nothing has actually confirmed
184
- * that's still true (a reconnect could land on a DIFFERENT server behind a
185
- * load balancer). Concretely, `TaskRunner.sendApprovalResolved` gates
186
- * `task.approval_resolved` on this list — sending it to a server that
187
- * doesn't actually understand it over the long-poll path would get a
188
- * batch-level 400 from `MessagesSendRequestSchema` (protocol §8.2), which
189
- * `drainOutbox`'s retry-the-same-batch-forever loop then head-of-line
190
- * blocks EVERY envelope queued behind it on, permanently. Clearing this
191
- * eagerly means that gate reliably fails closed (falls back to the
192
- * pre-existing implicit-resume inference, unconditionally — see
193
- * `sendApprovalResolved`'s own doc comment) the moment the connection that
194
- * advertised the capability is no longer the one actually in use.
160
+ * The capabilities the current server response advertised — untyped
161
+ * `string[]` for forward compatibility. An advertisement is scoped to the
162
+ * current long-poll response stream and is cleared after an HTTP failure,
163
+ * terminal shutdown, or revocation. This keeps capability-gated outbound
164
+ * messages fail-closed until the current server has explicitly advertised
165
+ * support.
195
166
  */
196
167
  private serverCapabilities;
197
168
  constructor(opts: ConnectionManagerOptions);
@@ -202,17 +173,10 @@ export declare class ConnectionManager {
202
173
  * `drainOutbox`.
203
174
  */
204
175
  send(envelope: Envelope): void;
176
+ /** Publish a fresh local configuration snapshot while this daemon is running. */
177
+ refreshHello(): void;
205
178
  /**
206
- * Design B (finding N4): drain the shared outbox through whichever
207
- * transport is currently active, re-checking `this.mode` fresh on every
208
- * iteration so a transport switch mid-drain is picked up immediately
209
- * rather than fighting a stale decision made before the switch.
210
- *
211
- * WS: a synchronous, one-at-a-time `sendNow` per envelope while open+
212
- * acked; stops (without dropping anything — the remainder stays queued)
213
- * the moment it isn't, and is re-invoked once `onAcked` fires.
214
- *
215
- * Long-poll: POSTs the outbox in chunks of at most
179
+ * POSTs the outbox through long-poll in chunks of at most
216
180
  * `MAX_MESSAGES_PER_BATCH` (finding P1) — the server hard-caps a single
217
181
  * `/byok/messages` batch there (`MessagesSendRequestSchema`, protocol
218
182
  * §8.2) and 400s the WHOLE request if it's exceeded, which — before this
@@ -224,14 +188,9 @@ export declare class ConnectionManager {
224
188
  * failure that SAME chunk is unshifted back (order-preserving, same
225
189
  * Envelope objects/ids — never rebuilt, so a retry is exactly the resend
226
190
  * Wave 1's server-side dedup expects) and retried after a short backoff,
227
- * re-reading `this.mode` each time so a WS recovery that happens
228
- * mid-retry is honored on the very next loop iteration instead of only
229
- * after this attempt's backoff chain gives up.
230
- *
231
191
  * Re-entrancy is guarded by `draining`: a call arriving while a drain is
232
192
  * already in progress just returns — the in-progress loop's own
233
- * `while (this.outbox.length > 0)` check will pick up anything newly
234
- * pushed (or left over after a mode switch) on its very next iteration.
193
+ * `while (this.outbox.length > 0)` check picks up anything newly pushed.
235
194
  */
236
195
  private drainOutbox;
237
196
  /**
@@ -241,37 +200,29 @@ export declare class ConnectionManager {
241
200
  * in-flight wait immediately instead of leaving `drainOutbox` parked here
242
201
  * for up to the rest of the delay before it next checks `this.revoked` —
243
202
  * and (b) unref'd, so the timer never keeps the Node process alive by
244
- * itself while nothing else (a live long-poll GET, an open WS connection)
245
- * legitimately is.
203
+ * itself while nothing else (such as the live long-poll GET) legitimately is.
246
204
  */
247
205
  private drainRetryDelay;
248
- isTransportDegraded(): boolean;
249
206
  /**
250
- * The capabilities the CURRENT transport's server advertised: from
251
- * `conn.ack` on WS, or the latest successful `GET /byok/events` response
252
- * on long-poll. Empty before either transport has supplied its current
253
- * advertisement, and cleared across disconnect/switch boundaries.
207
+ * The capabilities the latest successful `GET /byok/events` response
208
+ * advertised. Empty before a successful response and after a failed one.
254
209
  */
255
210
  getServerCapabilities(): readonly string[];
256
211
  getTerminalError(): ReplayCursorTooOldError | undefined;
257
- getMode(): 'ws' | 'long-poll';
258
212
  isConnected(): boolean;
259
213
  isRevoked(): boolean;
260
214
  /**
261
- * Resolves once the connection has settled either a working, acked WS
262
- * connection, or the long-poll fallback taking over (protocol §8). This
263
- * lets `daemon.start()` return promptly even when WS is unavailable from
264
- * the very first attempt, rather than hanging until a WS `conn.ack` that
265
- * may never come.
215
+ * Resolves after the first successful long-poll response establishes the
216
+ * authenticated connection.
266
217
  *
267
218
  * Rejects with {@link DeviceRevokedError} — instead of hanging until
268
219
  * `timeoutMs` — if the device turns out to be revoked while settling (or
269
220
  * already was): a cold `daemon.start()` against an already-revoked device
270
221
  * must fail fast, not surface a generic timeout (protocol §6.3).
271
222
  */
272
- waitForAck(timeoutMs?: number): Promise<void>;
223
+ waitForConnection(timeoutMs?: number): Promise<void>;
273
224
  /**
274
- * Stops both transports and waits for every in-flight envelope handler
225
+ * Stops the long-poll transport and waits for every in-flight envelope handler
275
226
  * (the F3 FIFO chain) and the most recent cursor write to actually land on
276
227
  * disk — otherwise a `stop()` racing a just-processed envelope's
277
228
  * persistence could lose that cursor advance, or leave a handler running
@@ -280,14 +231,14 @@ export declare class ConnectionManager {
280
231
  * Finding F5(b) (cross-model adversarial review): `drainTimeoutMs`, when
281
232
  * passed, bounds how long this waits for the shared outbox (`this.outbox`
282
233
  * — Design B) to actually finish draining BEFORE flipping `this.stopped`
283
- * and closing the transports. Before this fix, `stop()` set `stopped`
234
+ * and stopping the transport. Before this fix, `stop()` set `stopped`
284
235
  * synchronously and never waited for `drainOutbox` at all: an envelope
285
236
  * `send()` had just pushed moments earlier (e.g. `TaskRunner.shutdownTask`'s
286
237
  * own `task.fail`, sent right before `create-daemon.ts`'s
287
238
  * `performControlShutdown` calls this) could still be sitting UNSENT in
288
239
  * `this.outbox` — mid long-poll retry backoff, or simply not yet picked up
289
240
  * by the fire-and-forget `drainOutbox()` `send()` kicked off — and this
290
- * method would happily proceed to `stopped = true` / `ws.close()` regardless,
241
+ * method would happily proceed to `stopped = true` regardless,
291
242
  * after which NOTHING ever drains it again: silently lost, even though
292
243
  * `TaskRunner` believed it had been sent. `drainTimeoutMs` omitted (the
293
244
  * default) preserves the EXACT prior behavior for every other existing
@@ -318,6 +269,8 @@ export declare class ConnectionManager {
318
269
  * for the one case (a hung POST) this finding exists to catch honestly.
319
270
  */
320
271
  outboxLength(): number;
272
+ /** A bounded terminal quarantine for operator inspection; these entries are never retried. */
273
+ rejectedOutbox(): readonly RejectedOutboundEnvelope[];
321
274
  /**
322
275
  * Finding F5(b): polls {@link outboxLength} (not `this.outbox.length`
323
276
  * alone — see that method's own doc comment for why a spliced-out,
@@ -325,15 +278,14 @@ export declare class ConnectionManager {
325
278
  * a single `drainOutbox()` promise directly — a drain in progress can
326
279
  * itself loop through multiple retry/backoff cycles (`drainRetryDelay`)
327
280
  * while the server is unreachable, and a fresh, INDEPENDENT
328
- * `drainOutbox()` call can also be triggered concurrently (`send()`, a
329
- * mode switch's own `void this.drainOutbox()`) — polling the one thing
281
+ * `drainOutbox()` call can also be triggered concurrently (`send()`)
282
+ * polling the one thing
330
283
  * that actually matters (is anything still undelivered) can never go
331
284
  * stale the way capturing one specific in-flight promise reference
332
285
  * could. Kicks off one more `drainOutbox()` attempt itself first
333
286
  * (harmless no-op if one is already running — see its own re-entrancy
334
- * guard) in case nothing is currently actively retrying (e.g. WS just
335
- * dropped and long-poll hasn't taken over yet), so this bounded wait
336
- * isn't just passively hoping something else happens to be making
287
+ * guard) in case nothing is currently actively retrying, so this bounded
288
+ * wait isn't just passively hoping something else happens to be making
337
289
  * progress.
338
290
  */
339
291
  private waitForOutboxDrained;
@@ -360,9 +312,10 @@ export declare class ConnectionManager {
360
312
  */
361
313
  private deliver;
362
314
  /**
363
- * Design A: the watermark `deliver()` dedupes inbound `task.*` envelopes
364
- * against, and the same value `LongPollClient` queries the next
365
- * `GET /byok/events` cursor with (see the constructor). Normally this is
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
366
319
  * `deliveredSeq` — which is always >= `cursor` (every envelope that
367
320
  * reaches `advanceCursor` already passed through `noteDelivered` first,
368
321
  * see `deliver`) — so this is the literal `max(cursor, deliveredSeq)` the
@@ -378,132 +331,21 @@ export declare class ConnectionManager {
378
331
  * whose outcome wasn't known yet. No separate "reset deliveredSeq on
379
332
  * reconnect" step is needed for this to be correct — collapsing to
380
333
  * `cursor` exactly while stalled already produces the right answer on
381
- * every redelivery path (long-poll re-query AND a WS reconnect's
382
- * backlog replay alike), and NOT resetting it unconditionally on every
383
- * reconnect is what lets `deliveredSeq` keep doing its job of not
384
- * re-pulling/re-dispatching something already in flight across a
385
- * reconnect that happens to land while a handler is still running.
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.
386
337
  */
387
338
  private dedupWatermark;
388
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. */
389
340
  private noteDelivered;
390
341
  private process;
391
- /**
392
- * M4 Phase 4 (version-negotiation drill fix): `LongPollClient` calls this
393
- * for a batch entry it could not parse into a known `Envelope` at all (an
394
- * unrecognized message type — mirrors `ws-transport.ts`'s identical
395
- * per-frame tolerance, see `long-poll-transport.ts`'s own doc comment on
396
- * `parseLooseEventsPollResponse`) but which still carried a numeric,
397
- * task-class envelope-level `seq` (the caller only invokes this for a
398
- * `task.`-prefixed type — see `long-poll-transport.ts`'s own
399
- * `extractSkippableSeq`; `conn.*`-shaped or type-less entries never reach
400
- * here at all, mirroring F2's "conn.* is never cursor-tracked" rule).
401
- * There is no real `Envelope` to hand to a handler — a genuinely
402
- * unrecognized type has nothing this build could ever act on.
403
- *
404
- * GATEKEEPER-CAUGHT REGRESSION (fixed here): this used to call
405
- * `advanceCursor(seq)` DIRECTLY, synchronously, the instant a skip was
406
- * detected in `LongPollClient.loop()`'s per-entry for-loop. That is NOT
407
- * "instantaneous and race-free" the way the previous version of this
408
- * comment claimed — the hazard was never the skip racing against itself,
409
- * it was the skip racing AHEAD of an EARLIER real envelope in the SAME
410
- * batch that is still in flight on `processingChain` (`deliver()`, above,
411
- * only ever CHAINS `process()` onto that promise chain — it never awaits
412
- * it before returning). Concretely, batch `[real seq1, unknown seq2]`:
413
- * `deliver(seq1)` chains `process(seq1)` but returns immediately without
414
- * running it; the for-loop then reaches `seq2` and (pre-fix) called
415
- * `advanceCursor(2)` synchronously, BEFORE `process(seq1)` had even
416
- * started, let alone failed. If `seq1`'s handler then failed,
417
- * `stalledAtSeq` became 1 — but the durable cursor was already 2, so
418
- * `dedupWatermark()` returned 2, and every future redelivery of seq1 was
419
- * dedup-dropped as "already past the cursor" forever: permanent envelope
420
- * loss, exactly the F3 bug class the whole `stalledAtSeq`/frozen-watermark
421
- * mechanism exists to prevent.
422
- *
423
- * Fix: the cursor-advancing half is now CHAINED onto `processingChain`
424
- * too, exactly like `process()`'s own post-handler bookkeeping — so it
425
- * only ever runs once every earlier envelope already queued ahead of it
426
- * has fully settled (success or failure), and can observe `stalledAtSeq`'s
427
- * REAL, up-to-date value rather than whatever it happened to be at the
428
- * instant the skip was first noticed. The guard mirrors `process()`'s own
429
- * success-path guard exactly: never advance past a still-unresolved
430
- * earlier failure, unless (degenerate, cannot really happen for a skip)
431
- * this exact seq IS the stalled one.
432
- *
433
- * `noteDelivered` (the eager, in-memory watermark) stays UNCHAINED —
434
- * called immediately, unconditionally, regardless of `stalledAtSeq` —
435
- * matching `deliver()`'s own eager, unconditional call for a real
436
- * envelope: its only job is "don't re-pull something already handed off,"
437
- * independent of outcome, and that property does not depend on FIFO
438
- * ordering the way the DURABLE cursor does.
439
- *
440
- * Deliberately NO top-level `dedupWatermark() <= seq` early-return before
441
- * queuing the chained callback (an earlier draft of this fix had one, and
442
- * it was itself subtly wrong): `deliveredSeq` can already reflect a seq
443
- * from the FIRST time it was ever seen, while the DURABLE cursor is still
444
- * behind it because a stall intervened before that seq's chained
445
- * advancement ran — a pre-check keyed on `deliveredSeq` would then
446
- * wrongly treat a LATER redelivery of the same seq (arriving once the
447
- * stall has since cleared) as "already accounted for" and never queue
448
- * another attempt, permanently stranding the cursor one seq short. Always
449
- * queuing is safe and cheap: `advanceCursor`'s own `seq <= this.cursor`
450
- * guard already makes a genuinely-redundant call a no-op, so there is no
451
- * correctness reason to short-circuit earlier, only a (here, unnecessary)
452
- * micro-optimization one.
453
- */
454
- private noteSkippedSeq;
455
- /**
456
- * Finding R1 (cross-model re-review — was NOT-CLOSED against F1):
457
- * `LongPollClient` calls this for a batch entry whose `type` it
458
- * recognized but whose payload failed schema validation
459
- * ({@link EnvelopeValidationError}) — a genuine delivery failure at that
460
- * seq, unlike `noteSkippedSeq`'s forward-compat case. Deliberately mirrors
461
- * `process()`'s own catch block (`if (tracked && this.stalledAtSeq ===
462
- * undefined) this.stalledAtSeq = envelope.seq;`) as closely as possible:
463
- * the SAME "only the lowest unresolved failure holds the stall" rule, the
464
- * SAME resulting freeze of `dedupWatermark()` at the durable cursor
465
- * (protocol §9 keeps this seq alive), and — because it's the SAME
466
- * `stalledAtSeq` field `process()`'s own post-success guard already
467
- * checks — anything ELSE delivered after this seq (same batch or a later
468
- * one) is automatically held back from advancing the cursor too, with
469
- * zero changes needed to `process()` itself.
470
- *
471
- * Chained onto `processingChain` for exactly the reason `noteSkippedSeq`
472
- * documents for its own identical chaining (see that method's sibling
473
- * doc comment on `LongPollClient`, "GATEKEEPER-CAUGHT REGRESSION"): an
474
- * EARLIER real envelope in the SAME batch may still be in flight on that
475
- * FIFO chain when this is called (`deliver()` only ever chains
476
- * `process()` onto it, never awaits before returning) — mutating
477
- * `stalledAtSeq` synchronously here could race ahead of that still-
478
- * unresolved earlier envelope. Chaining instead guarantees this only
479
- * takes effect once every earlier-queued envelope has already settled,
480
- * and reads `stalledAtSeq`'s real, up-to-date value rather than whatever
481
- * it happened to be the instant the failure was first noticed.
482
- *
483
- * No `noteDelivered` call here (contrast `noteSkippedSeq`, which does
484
- * call it): a validation-failed entry never becomes a real `Envelope` and
485
- * never reaches `deliver()`, so it was never "delivered" in the eager
486
- * in-memory-watermark sense that field tracks — there is nothing for it
487
- * to eagerly mark. Once a corrected redelivery of this exact seq DOES
488
- * arrive as a real envelope, it flows through the ordinary `deliver()`
489
- * path (which calls `noteDelivered` itself) and, on success, clears the
490
- * stall via `process()`'s own existing logic — no special-casing needed.
491
- */
342
+ /** Serialized with handler completion, so invalid work cannot be acked by later success. */
492
343
  private noteValidationFailure;
493
344
  private advanceCursor;
494
- /**
495
- * Fires the moment a connection attempt reaches `conn.ack` — independent
496
- * of whether/when it later closes. This is the ONLY place that can
497
- * reliably detect "WS is back up" while long-polling: a healthy
498
- * connection stays open indefinitely, so it never reaches `onWsOutcome`
499
- * (which is close-only) at all.
500
- */
501
- private onAcked;
502
- private onWsOutcome;
345
+ private quarantineRejectedOutbound;
503
346
  private notifySettled;
504
- private enterLongPoll;
347
+ private noteConnected;
348
+ private noteDisconnected;
505
349
  private enterReplayCursorTooOld;
506
- private exitLongPoll;
507
- private scheduleWsProbe;
508
350
  private enterRevoked;
509
351
  }
@@ -5,6 +5,7 @@ import type { StoragePressureState } from './journal/storage-policy';
5
5
  import type { OperationalHealthSnapshot } from './operational-health';
6
6
  import type { LocalAgentReleaseIdentity } from '../release-identity';
7
7
  import type { McpToolsetConfig, McpToolsetRegistryStatus } from '../types';
8
+ import type { AgentHomeExecutionStatus } from '../agent-home';
8
9
  /**
9
10
  * M4 Phase 2: shared local-IPC contract between the daemon's control server
10
11
  * (`control-server.ts`) and the CLI's control client (`bin/control-client.ts`)
@@ -252,7 +253,7 @@ export interface ControlStatusResult {
252
253
  uptimeMs: number;
253
254
  paired: boolean;
254
255
  deviceId?: string;
255
- /** The connection state machine's own current value (`ws-transport.ts`'s `ConnectionState`) — e.g. `'open'`, `'degraded'` (long-poll fallback), `'revoked'`, `'closed'`, `'connecting'`. */
256
+ /** The connection state machine's current value: `'open'`, `'revoked'`, `'closed'`, or `'connecting'`. */
256
257
  transport: string;
257
258
  activeTasks: ControlActiveTask[];
258
259
  runtimeIds: string[];
@@ -276,6 +277,12 @@ export interface ControlStatusResult {
276
277
  operationalHealth: OperationalHealthSnapshot;
277
278
  /** Redacted content-addressed status from the daemon's single local registry. */
278
279
  toolsets: McpToolsetRegistryStatus;
280
+ /**
281
+ * WP0: per-canonical-Agent-home execution serialization, counts only —
282
+ * see {@link AgentHomeExecutionStatus}. Absent only for an older control
283
+ * peer that predates the cap.
284
+ */
285
+ agentHomeExecution?: AgentHomeExecutionStatus;
279
286
  }
280
287
  export interface ToolsetsReloadParams {
281
288
  expectedRevision: string;
@@ -458,5 +465,7 @@ export declare function parseTeamWorkspaceJoinParams(value: unknown): TeamWorksp
458
465
  export declare function parseTeamContextParams(value: unknown): TeamContextParams | undefined;
459
466
  export declare function parseTeamMessagePostParams(value: unknown): TeamMessagePostParams | undefined;
460
467
  export declare function parseTeamMessageReadParams(value: unknown): TeamMessageReadParams | undefined;
468
+ /** Exact local operator RPC; the shared read-parameter shape has no model identity fields. */
469
+ export declare function parseTeamNotificationSnapshotParams(value: unknown): TeamMessageReadParams | undefined;
461
470
  export declare function parseTeamMessageAckParams(value: unknown): TeamMessageAckParams | undefined;
462
471
  export declare function parseTeamMessageInspectParams(value: unknown): TeamMessageInspectParams | undefined;