@byok-sdk/client 0.12.0 → 0.13.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.
- package/README.md +13 -0
- package/dist/adapters/index.js +74 -5
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/pi-adapter.d.ts +3 -0
- package/dist/adapters/pi/subagents-policy-extension.js +1 -1
- package/dist/adapters/pi/subagents-policy-extension.js.map +1 -1
- package/dist/agent-home.d.ts +74 -0
- package/dist/agent-memory/index.d.ts +1 -1
- package/dist/agent-memory/index.js.map +1 -1
- package/dist/bin/byok-agent-memory-mcp.js.map +1 -1
- package/dist/bin/byok-agent-message-mcp.js.map +1 -1
- package/dist/bin/byok-agent-team-mcp.js.map +1 -1
- package/dist/bin/byok-agent.js +467 -485
- package/dist/bin/byok-agent.js.map +1 -1
- package/dist/bin/byok-approval-mcp.js.map +1 -1
- package/dist/daemon/connection-manager.d.ts +63 -127
- package/dist/daemon/control-protocol.d.ts +8 -1
- package/dist/daemon/create-daemon.d.ts +26 -13
- package/dist/daemon/long-poll-transport.d.ts +29 -23
- package/dist/daemon/observer.d.ts +1 -1
- package/dist/daemon/task-runner.d.ts +18 -0
- package/dist/daemon/url.d.ts +10 -12
- package/dist/diagnostics/support-bundle.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +465 -483
- package/dist/index.js.map +1 -1
- package/package.json +5 -7
- package/dist/daemon/ws-transport.d.ts +0 -139
|
@@ -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
|
|
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
|
|
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,6 @@ 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;
|
|
34
28
|
/** Backoff between failed long-poll HTTP attempts. Default 2s. */
|
|
35
29
|
longPollRetryDelayMs?: number;
|
|
36
30
|
/** Minimum delay before the next long-poll request after an empty (no-events) response. Default 250ms. */
|
|
@@ -39,33 +33,27 @@ export interface ConnectionManagerOptions {
|
|
|
39
33
|
onOperationalOutcome?: (outcome: 'success' | 'failure', source: 'reconnect' | 'upload') => void;
|
|
40
34
|
onTerminalError?: (error: ReplayCursorTooOldError) => void;
|
|
41
35
|
}
|
|
36
|
+
export interface RejectedOutboundEnvelope {
|
|
37
|
+
readonly envelope: Envelope;
|
|
38
|
+
readonly reason: 'inbound_rejected';
|
|
39
|
+
}
|
|
42
40
|
/**
|
|
43
|
-
* Owns the daemon's one
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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.
|
|
41
|
+
* Owns the daemon's one authenticated long-poll connection to the server.
|
|
42
|
+
* Every received envelope passes through the same cursor-dedupe/persistence
|
|
43
|
+
* logic (protocol §9), so redelivery remains safe after an HTTP retry or a
|
|
44
|
+
* daemon restart.
|
|
51
45
|
*
|
|
52
46
|
* `send()` (Design B, finding N4) pushes onto a single shared outbox this
|
|
53
|
-
* class owns and drains through
|
|
54
|
-
*
|
|
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`.
|
|
47
|
+
* class owns and drains through `POST /byok/messages`; long-poll is a full
|
|
48
|
+
* bidirectional transport, not a receive-only path. See `drainOutbox`.
|
|
58
49
|
*/
|
|
59
50
|
export declare class ConnectionManager {
|
|
60
51
|
private readonly opts;
|
|
61
52
|
private readonly fleetJitter;
|
|
62
|
-
private readonly ws;
|
|
63
53
|
private readonly longPoll;
|
|
64
|
-
private mode;
|
|
65
|
-
private consecutiveFailures;
|
|
66
|
-
private wsRetryTimer;
|
|
67
|
-
private wsProbeSequence;
|
|
68
54
|
private uploadRetryAttempt;
|
|
55
|
+
private started;
|
|
56
|
+
private connected;
|
|
69
57
|
private cursor;
|
|
70
58
|
/**
|
|
71
59
|
* Finding F3 (at-most-once redelivery): the lowest `task.*` envelope `seq`
|
|
@@ -87,10 +75,9 @@ export declare class ConnectionManager {
|
|
|
87
75
|
* (see `advanceCursor`) — that semantics is unchanged. `deliveredSeq`
|
|
88
76
|
* advances eagerly, the instant a `task.*` envelope is admitted past
|
|
89
77
|
* dedup (see `deliver`/`noteDelivered`), independent of whether its
|
|
90
|
-
* handler has even started, let alone succeeded. It exists so a
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* `handleOffer` is NOT idempotent and must never be re-pulled while a
|
|
78
|
+
* handler has even started, let alone succeeded. It exists so a repeated
|
|
79
|
+
* read at the durable cursor does not re-dispatch an envelope already in
|
|
80
|
+
* flight — `handleOffer` must not start a second adapter session while a
|
|
94
81
|
* first attempt is still running. On WS this same field is written the
|
|
95
82
|
* same way, but since a live WS connection only ever pushes a given `seq`
|
|
96
83
|
* once, it never has an observable effect there beyond mirroring
|
|
@@ -101,18 +88,18 @@ export declare class ConnectionManager {
|
|
|
101
88
|
/** Finding F3: serializes `onEnvelope` calls into a per-connection FIFO — one envelope's handler always fully settles before the next one starts. */
|
|
102
89
|
private processingChain;
|
|
103
90
|
/**
|
|
104
|
-
* Design B (finding N4): the ONE outbound queue
|
|
105
|
-
*
|
|
91
|
+
* Design B (finding N4): the ONE outbound queue holds `Envelope` OBJECTS,
|
|
92
|
+
* never re-encoded/rebuilt strings, so a
|
|
106
93
|
* resend after a failed send attempt is byte-identical to the original
|
|
107
94
|
* (same `id`), which is what lets the server's per-(deviceId,id) dedup
|
|
108
95
|
* (Wave 1) recognize it as a safe no-op retry rather than a second
|
|
109
|
-
* application (protocol §9).
|
|
110
|
-
* touches this queue — see `drainOutbox` — so nothing queued while one
|
|
111
|
-
* transport was active is ever stranded when the other takes over.
|
|
96
|
+
* application (protocol §9).
|
|
112
97
|
*/
|
|
113
98
|
private readonly outbox;
|
|
99
|
+
/** Terminally rejected outbound envelopes, retained as a bounded observable quarantine. */
|
|
100
|
+
private readonly rejectedOutboundEnvelopes;
|
|
114
101
|
/**
|
|
115
|
-
* Finding F5(b): how many envelopes `drainOutbox`
|
|
102
|
+
* Finding F5(b): how many envelopes `drainOutbox` has
|
|
116
103
|
* currently spliced OUT of `this.outbox` for an in-flight (not yet
|
|
117
104
|
* confirmed delivered) `postBatch` call — 0 the rest of the time. See
|
|
118
105
|
* `outboxLength`'s own doc comment for why this needs to be tracked
|
|
@@ -165,33 +152,12 @@ export declare class ConnectionManager {
|
|
|
165
152
|
*/
|
|
166
153
|
private cancelPendingDrainRetry;
|
|
167
154
|
/**
|
|
168
|
-
* The capabilities the
|
|
169
|
-
* `string[]` for forward compatibility.
|
|
170
|
-
* long-poll
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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.
|
|
155
|
+
* The capabilities the current server response advertised — untyped
|
|
156
|
+
* `string[]` for forward compatibility. An advertisement is scoped to the
|
|
157
|
+
* current long-poll response stream and is cleared after an HTTP failure,
|
|
158
|
+
* terminal shutdown, or revocation. This keeps capability-gated outbound
|
|
159
|
+
* messages fail-closed until the current server has explicitly advertised
|
|
160
|
+
* support.
|
|
195
161
|
*/
|
|
196
162
|
private serverCapabilities;
|
|
197
163
|
constructor(opts: ConnectionManagerOptions);
|
|
@@ -202,17 +168,10 @@ export declare class ConnectionManager {
|
|
|
202
168
|
* `drainOutbox`.
|
|
203
169
|
*/
|
|
204
170
|
send(envelope: Envelope): void;
|
|
171
|
+
/** Publish a fresh local configuration snapshot while this daemon is running. */
|
|
172
|
+
refreshHello(): void;
|
|
205
173
|
/**
|
|
206
|
-
*
|
|
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
|
|
174
|
+
* POSTs the outbox through long-poll in chunks of at most
|
|
216
175
|
* `MAX_MESSAGES_PER_BATCH` (finding P1) — the server hard-caps a single
|
|
217
176
|
* `/byok/messages` batch there (`MessagesSendRequestSchema`, protocol
|
|
218
177
|
* §8.2) and 400s the WHOLE request if it's exceeded, which — before this
|
|
@@ -224,14 +183,9 @@ export declare class ConnectionManager {
|
|
|
224
183
|
* failure that SAME chunk is unshifted back (order-preserving, same
|
|
225
184
|
* Envelope objects/ids — never rebuilt, so a retry is exactly the resend
|
|
226
185
|
* 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
186
|
* Re-entrancy is guarded by `draining`: a call arriving while a drain is
|
|
232
187
|
* already in progress just returns — the in-progress loop's own
|
|
233
|
-
* `while (this.outbox.length > 0)` check
|
|
234
|
-
* pushed (or left over after a mode switch) on its very next iteration.
|
|
188
|
+
* `while (this.outbox.length > 0)` check picks up anything newly pushed.
|
|
235
189
|
*/
|
|
236
190
|
private drainOutbox;
|
|
237
191
|
/**
|
|
@@ -241,37 +195,29 @@ export declare class ConnectionManager {
|
|
|
241
195
|
* in-flight wait immediately instead of leaving `drainOutbox` parked here
|
|
242
196
|
* for up to the rest of the delay before it next checks `this.revoked` —
|
|
243
197
|
* and (b) unref'd, so the timer never keeps the Node process alive by
|
|
244
|
-
* itself while nothing else (
|
|
245
|
-
* legitimately is.
|
|
198
|
+
* itself while nothing else (such as the live long-poll GET) legitimately is.
|
|
246
199
|
*/
|
|
247
200
|
private drainRetryDelay;
|
|
248
|
-
isTransportDegraded(): boolean;
|
|
249
201
|
/**
|
|
250
|
-
* The capabilities the
|
|
251
|
-
*
|
|
252
|
-
* on long-poll. Empty before either transport has supplied its current
|
|
253
|
-
* advertisement, and cleared across disconnect/switch boundaries.
|
|
202
|
+
* The capabilities the latest successful `GET /byok/events` response
|
|
203
|
+
* advertised. Empty before a successful response and after a failed one.
|
|
254
204
|
*/
|
|
255
205
|
getServerCapabilities(): readonly string[];
|
|
256
206
|
getTerminalError(): ReplayCursorTooOldError | undefined;
|
|
257
|
-
getMode(): 'ws' | 'long-poll';
|
|
258
207
|
isConnected(): boolean;
|
|
259
208
|
isRevoked(): boolean;
|
|
260
209
|
/**
|
|
261
|
-
* Resolves
|
|
262
|
-
* connection
|
|
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.
|
|
210
|
+
* Resolves after the first successful long-poll response establishes the
|
|
211
|
+
* authenticated connection.
|
|
266
212
|
*
|
|
267
213
|
* Rejects with {@link DeviceRevokedError} — instead of hanging until
|
|
268
214
|
* `timeoutMs` — if the device turns out to be revoked while settling (or
|
|
269
215
|
* already was): a cold `daemon.start()` against an already-revoked device
|
|
270
216
|
* must fail fast, not surface a generic timeout (protocol §6.3).
|
|
271
217
|
*/
|
|
272
|
-
|
|
218
|
+
waitForConnection(timeoutMs?: number): Promise<void>;
|
|
273
219
|
/**
|
|
274
|
-
* Stops
|
|
220
|
+
* Stops the long-poll transport and waits for every in-flight envelope handler
|
|
275
221
|
* (the F3 FIFO chain) and the most recent cursor write to actually land on
|
|
276
222
|
* disk — otherwise a `stop()` racing a just-processed envelope's
|
|
277
223
|
* persistence could lose that cursor advance, or leave a handler running
|
|
@@ -280,14 +226,14 @@ export declare class ConnectionManager {
|
|
|
280
226
|
* Finding F5(b) (cross-model adversarial review): `drainTimeoutMs`, when
|
|
281
227
|
* passed, bounds how long this waits for the shared outbox (`this.outbox`
|
|
282
228
|
* — Design B) to actually finish draining BEFORE flipping `this.stopped`
|
|
283
|
-
* and
|
|
229
|
+
* and stopping the transport. Before this fix, `stop()` set `stopped`
|
|
284
230
|
* synchronously and never waited for `drainOutbox` at all: an envelope
|
|
285
231
|
* `send()` had just pushed moments earlier (e.g. `TaskRunner.shutdownTask`'s
|
|
286
232
|
* own `task.fail`, sent right before `create-daemon.ts`'s
|
|
287
233
|
* `performControlShutdown` calls this) could still be sitting UNSENT in
|
|
288
234
|
* `this.outbox` — mid long-poll retry backoff, or simply not yet picked up
|
|
289
235
|
* by the fire-and-forget `drainOutbox()` `send()` kicked off — and this
|
|
290
|
-
* method would happily proceed to `stopped = true`
|
|
236
|
+
* method would happily proceed to `stopped = true` regardless,
|
|
291
237
|
* after which NOTHING ever drains it again: silently lost, even though
|
|
292
238
|
* `TaskRunner` believed it had been sent. `drainTimeoutMs` omitted (the
|
|
293
239
|
* default) preserves the EXACT prior behavior for every other existing
|
|
@@ -318,6 +264,8 @@ export declare class ConnectionManager {
|
|
|
318
264
|
* for the one case (a hung POST) this finding exists to catch honestly.
|
|
319
265
|
*/
|
|
320
266
|
outboxLength(): number;
|
|
267
|
+
/** A bounded terminal quarantine for operator inspection; these entries are never retried. */
|
|
268
|
+
rejectedOutbox(): readonly RejectedOutboundEnvelope[];
|
|
321
269
|
/**
|
|
322
270
|
* Finding F5(b): polls {@link outboxLength} (not `this.outbox.length`
|
|
323
271
|
* alone — see that method's own doc comment for why a spliced-out,
|
|
@@ -325,15 +273,14 @@ export declare class ConnectionManager {
|
|
|
325
273
|
* a single `drainOutbox()` promise directly — a drain in progress can
|
|
326
274
|
* itself loop through multiple retry/backoff cycles (`drainRetryDelay`)
|
|
327
275
|
* while the server is unreachable, and a fresh, INDEPENDENT
|
|
328
|
-
* `drainOutbox()` call can also be triggered concurrently (`send()
|
|
329
|
-
*
|
|
276
|
+
* `drainOutbox()` call can also be triggered concurrently (`send()`) —
|
|
277
|
+
* polling the one thing
|
|
330
278
|
* that actually matters (is anything still undelivered) can never go
|
|
331
279
|
* stale the way capturing one specific in-flight promise reference
|
|
332
280
|
* could. Kicks off one more `drainOutbox()` attempt itself first
|
|
333
281
|
* (harmless no-op if one is already running — see its own re-entrancy
|
|
334
|
-
* guard) in case nothing is currently actively retrying
|
|
335
|
-
*
|
|
336
|
-
* isn't just passively hoping something else happens to be making
|
|
282
|
+
* guard) in case nothing is currently actively retrying, so this bounded
|
|
283
|
+
* wait isn't just passively hoping something else happens to be making
|
|
337
284
|
* progress.
|
|
338
285
|
*/
|
|
339
286
|
private waitForOutboxDrained;
|
|
@@ -360,9 +307,10 @@ export declare class ConnectionManager {
|
|
|
360
307
|
*/
|
|
361
308
|
private deliver;
|
|
362
309
|
/**
|
|
363
|
-
*
|
|
364
|
-
* against
|
|
365
|
-
*
|
|
310
|
+
* The local watermark `deliver()` dedupes inbound `task.*` envelopes
|
|
311
|
+
* against. It is deliberately NOT the long-poll query cursor: that query
|
|
312
|
+
* is the kernel acknowledgement and uses only the successfully processed
|
|
313
|
+
* `cursor` (see the constructor). Normally this local watermark is
|
|
366
314
|
* `deliveredSeq` — which is always >= `cursor` (every envelope that
|
|
367
315
|
* reaches `advanceCursor` already passed through `noteDelivered` first,
|
|
368
316
|
* see `deliver`) — so this is the literal `max(cursor, deliveredSeq)` the
|
|
@@ -378,11 +326,9 @@ export declare class ConnectionManager {
|
|
|
378
326
|
* whose outcome wasn't known yet. No separate "reset deliveredSeq on
|
|
379
327
|
* reconnect" step is needed for this to be correct — collapsing to
|
|
380
328
|
* `cursor` exactly while stalled already produces the right answer on
|
|
381
|
-
* every
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
* re-pulling/re-dispatching something already in flight across a
|
|
385
|
-
* reconnect that happens to land while a handler is still running.
|
|
329
|
+
* every long-poll retry path. NOT resetting it unconditionally on every
|
|
330
|
+
* retry lets `deliveredSeq` keep doing its job of not re-dispatching
|
|
331
|
+
* something already in flight while a handler is still running.
|
|
386
332
|
*/
|
|
387
333
|
private dedupWatermark;
|
|
388
334
|
/** 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. */
|
|
@@ -391,9 +337,8 @@ export declare class ConnectionManager {
|
|
|
391
337
|
/**
|
|
392
338
|
* M4 Phase 4 (version-negotiation drill fix): `LongPollClient` calls this
|
|
393
339
|
* for a batch entry it could not parse into a known `Envelope` at all (an
|
|
394
|
-
* unrecognized message type
|
|
395
|
-
*
|
|
396
|
-
* `parseLooseEventsPollResponse`) but which still carried a numeric,
|
|
340
|
+
* unrecognized message type (see `long-poll-transport.ts`'s own doc
|
|
341
|
+
* comment on `parseLooseEventsPollResponse`) but which still carried a numeric,
|
|
397
342
|
* task-class envelope-level `seq` (the caller only invokes this for a
|
|
398
343
|
* `task.`-prefixed type — see `long-poll-transport.ts`'s own
|
|
399
344
|
* `extractSkippableSeq`; `conn.*`-shaped or type-less entries never reach
|
|
@@ -433,7 +378,7 @@ export declare class ConnectionManager {
|
|
|
433
378
|
* `noteDelivered` (the eager, in-memory watermark) stays UNCHAINED —
|
|
434
379
|
* called immediately, unconditionally, regardless of `stalledAtSeq` —
|
|
435
380
|
* matching `deliver()`'s own eager, unconditional call for a real
|
|
436
|
-
* envelope: its only job is "don't re-
|
|
381
|
+
* envelope: its only job is "don't re-dispatch something already handed off,"
|
|
437
382
|
* independent of outcome, and that property does not depend on FIFO
|
|
438
383
|
* ordering the way the DURABLE cursor does.
|
|
439
384
|
*
|
|
@@ -491,19 +436,10 @@ export declare class ConnectionManager {
|
|
|
491
436
|
*/
|
|
492
437
|
private noteValidationFailure;
|
|
493
438
|
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;
|
|
439
|
+
private quarantineRejectedOutbound;
|
|
503
440
|
private notifySettled;
|
|
504
|
-
private
|
|
441
|
+
private noteConnected;
|
|
442
|
+
private noteDisconnected;
|
|
505
443
|
private enterReplayCursorTooOld;
|
|
506
|
-
private exitLongPoll;
|
|
507
|
-
private scheduleWsProbe;
|
|
508
444
|
private enterRevoked;
|
|
509
445
|
}
|
|
@@ -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
|
|
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;
|
|
@@ -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 `
|
|
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
|
|
290
|
+
* `http:`/`https:`), which is refused unconditionally.
|
|
272
291
|
*/
|
|
273
292
|
dangerouslyAllowInsecureRemote?: boolean;
|
|
274
293
|
/**
|
|
@@ -475,8 +494,6 @@ export interface DaemonStatus {
|
|
|
475
494
|
localAgentRelease: Readonly<LocalAgentReleaseIdentity>;
|
|
476
495
|
paired: boolean;
|
|
477
496
|
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
497
|
/** 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
498
|
revoked: boolean;
|
|
482
499
|
deviceId?: string;
|
|
@@ -489,6 +506,8 @@ export interface DaemonStatus {
|
|
|
489
506
|
toolsets: McpToolsetRegistryStatus;
|
|
490
507
|
/** Content-free egress lane watermarks and typed last-drop facts. */
|
|
491
508
|
egress: AgentEgressStatus;
|
|
509
|
+
/** WP0: per-canonical-Agent-home execution serialization — see {@link AgentHomeExecutionStatus}. */
|
|
510
|
+
agentHomeExecution: AgentHomeExecutionStatus;
|
|
492
511
|
}
|
|
493
512
|
export interface Daemon {
|
|
494
513
|
/** Pairing result is intentionally credential-blind. */
|
|
@@ -538,10 +557,8 @@ export interface Daemon {
|
|
|
538
557
|
/** M3-2a: same as {@link approve} but rejects — see that method's doc comment. */
|
|
539
558
|
reject(taskId: string, reason?: string): Promise<void>;
|
|
540
559
|
}
|
|
541
|
-
/** Internal seam so tests can substitute stub adapters / faster
|
|
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. */
|
|
542
561
|
export interface DaemonOverrides {
|
|
543
|
-
backoff?: BackoffOptions;
|
|
544
|
-
liveness?: LivenessOptions;
|
|
545
562
|
/** 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
563
|
approvalTimeoutMs?: number;
|
|
547
564
|
/** 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 +569,6 @@ export interface DaemonOverrides {
|
|
|
552
569
|
outboxDrainTimeoutMs?: number;
|
|
553
570
|
};
|
|
554
571
|
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
572
|
/** Backoff between failed long-poll HTTP attempts. Default 2s. */
|
|
560
573
|
retryDelayMs?: number;
|
|
561
574
|
/** 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. */
|
|
@@ -33,13 +33,18 @@ export interface LongPollClientOptions {
|
|
|
33
33
|
serverUrl: string;
|
|
34
34
|
auth: AuthManager;
|
|
35
35
|
getCursor: () => number | undefined;
|
|
36
|
-
|
|
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. */
|
|
@@ -48,8 +53,7 @@ export interface LongPollClientOptions {
|
|
|
48
53
|
* M4 Phase 4 (version-negotiation drill fix), scope narrowed by finding F1:
|
|
49
54
|
* called ONLY for a batch entry that failed to parse because its `type`
|
|
50
55
|
* is entirely unrecognized (`parseMessage` throwing
|
|
51
|
-
* {@link UnknownMessageTypeError}
|
|
52
|
-
* per-frame tolerance for that SPECIFIC failure) and which still carries a
|
|
56
|
+
* {@link UnknownMessageTypeError}) and which still carries a
|
|
53
57
|
* numeric envelope-level `seq` AND a recognizably task-class `type` (a
|
|
54
58
|
* `task.` prefix — see `extractSkippableSeq`'s own doc comment for why a
|
|
55
59
|
* `conn.*`-shaped or type-less entry is deliberately excluded, mirroring
|
|
@@ -67,11 +71,8 @@ export interface LongPollClientOptions {
|
|
|
67
71
|
* not forward-compat tolerance — forwarding its `seq` here would
|
|
68
72
|
* permanently ack a message the daemon never actually understood (the
|
|
69
73
|
* server would stop redelivering it, silently stranding whatever it was
|
|
70
|
-
* offering).
|
|
71
|
-
*
|
|
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
|
|
74
|
+
* offering). This callback being scoped to `UnknownMessageTypeError` only
|
|
75
|
+
* preserves the no-silent-permanent-ack property. Optional
|
|
75
76
|
* only for constructor/test convenience — `ConnectionManager` always
|
|
76
77
|
* supplies it.
|
|
77
78
|
*/
|
|
@@ -114,7 +115,7 @@ export interface LongPollClientOptions {
|
|
|
114
115
|
* `ConnectionManager` always supplies it.
|
|
115
116
|
*/
|
|
116
117
|
isStalled?: () => boolean;
|
|
117
|
-
/** Backoff between failed poll attempts (network/HTTP errors),
|
|
118
|
+
/** 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
119
|
retryDelayMs?: number;
|
|
119
120
|
/** Deterministic delay authority for automatic failed/stalled cycles. */
|
|
120
121
|
retryDelayForAttempt?: (attempt: number, baseDelayMs: number) => number;
|
|
@@ -128,24 +129,29 @@ export interface LongPollClientOptions {
|
|
|
128
129
|
*/
|
|
129
130
|
idleDelayMs?: number;
|
|
130
131
|
}
|
|
132
|
+
/** A fully-read frozen-v1 count acknowledgement for the batch that was posted. */
|
|
133
|
+
export interface MessageBatchPostResult {
|
|
134
|
+
readonly accepted: number;
|
|
135
|
+
readonly rejected?: number;
|
|
136
|
+
}
|
|
131
137
|
/**
|
|
132
|
-
* Protocol §8 long-poll
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* see docs/protocol.md §8).
|
|
138
|
+
* Protocol §8 long-poll transport: `GET /byok/events?cursor=N` in a loop,
|
|
139
|
+
* plus `POST /byok/messages` for the daemon's own outbound envelopes
|
|
140
|
+
* (finding F6 — long-poll is a full transport, not receive-only: see
|
|
141
|
+
* docs/protocol.md §8).
|
|
137
142
|
*
|
|
138
|
-
* Design B (finding N4): this is a stateless drainer
|
|
139
|
-
*
|
|
140
|
-
* `
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* currently active) lives in the caller (`ConnectionManager.drainOutbox`).
|
|
143
|
+
* Design B (finding N4): this is a stateless drainer; it holds no outbound
|
|
144
|
+
* queue of its own. `ConnectionManager` owns the single shared outbox;
|
|
145
|
+
* `postBatch` is a single POST attempt, reporting only frozen-v1 accepted and
|
|
146
|
+
* rejected counts after its response body has been read and validated. All
|
|
147
|
+
* retry/backoff and rejection isolation policy lives in the caller
|
|
148
|
+
* (`ConnectionManager.drainOutbox`).
|
|
145
149
|
*/
|
|
146
150
|
export declare class LongPollClient {
|
|
147
151
|
private readonly opts;
|
|
148
152
|
private running;
|
|
153
|
+
/** Owns exactly one active loop generation, including its held GET and retry delays. */
|
|
154
|
+
private loopAbortController;
|
|
149
155
|
/**
|
|
150
156
|
* Finding R1: seqs this loop has already `console.warn`'d about for a
|
|
151
157
|
* validation-failed (recognized-type, invalid-payload) entry — a poison
|
|
@@ -204,8 +210,8 @@ export declare class LongPollClient {
|
|
|
204
210
|
* (`ConnectionHub.handleInbound`), so a resend of the SAME batch (same
|
|
205
211
|
* envelope `id`s — the caller must never rebuild them) is deduped
|
|
206
212
|
* server-side into a safe no-op rather than reprocessed (§9). Returns
|
|
207
|
-
*
|
|
213
|
+
* validated frozen-v1 counts only after a readable response body.
|
|
208
214
|
*/
|
|
209
|
-
postBatch(envelopes: Envelope[]): Promise<
|
|
215
|
+
postBatch(envelopes: Envelope[]): Promise<MessageBatchPostResult | undefined>;
|
|
210
216
|
private loop;
|
|
211
217
|
}
|
|
@@ -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 './
|
|
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
|