@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.
- package/README.md +93 -0
- package/dist/adapters/claude/process-client.d.ts +24 -0
- package/dist/adapters/codex/process-runner.d.ts +46 -1
- package/dist/adapters/index.js +433 -33
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/events.d.ts +1 -1
- package/dist/adapters/pi/pi-adapter.d.ts +3 -0
- package/dist/adapters/pi/rpc-client.d.ts +47 -1
- package/dist/adapters/pi/subagents-policy-extension.js +1 -1
- package/dist/adapters/pi/subagents-policy-extension.js.map +1 -1
- package/dist/adapters/pi/team-interaction-extension.d.ts +24 -0
- package/dist/adapters/pi/team-interaction-extension.js +81 -0
- package/dist/adapters/pi/team-interaction-extension.js.map +1 -0
- package/dist/adapters/process-tree.d.ts +74 -4
- package/dist/adapters/provider-credential-environment.d.ts +1 -1
- package/dist/adapters/win32-job-object.d.ts +101 -0
- 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 +9571 -8272
- package/dist/bin/byok-agent.js.map +1 -1
- package/dist/bin/byok-approval-mcp.js.map +1 -1
- package/dist/bin/commands/team-pi-relay.d.ts +24 -0
- package/dist/bin/commands/team-relay.d.ts +11 -0
- package/dist/bin/team-codex-relay.d.ts +32 -0
- package/dist/bin/team-notification-relay.d.ts +40 -0
- package/dist/bin/team-pi-session.d.ts +59 -0
- package/dist/daemon/agent-egress-policy.d.ts +8 -0
- package/dist/daemon/connection-manager.d.ts +66 -224
- package/dist/daemon/control-protocol.d.ts +10 -1
- package/dist/daemon/create-daemon.d.ts +58 -13
- package/dist/daemon/event-spill.d.ts +90 -0
- package/dist/daemon/journal/journal.d.ts +15 -3
- package/dist/daemon/journal/sqlite-journal.d.ts +7 -2
- package/dist/daemon/long-poll-transport.d.ts +28 -70
- package/dist/daemon/observer.d.ts +1 -1
- package/dist/daemon/task-runner.d.ts +31 -0
- package/dist/daemon/team-workspace.d.ts +9 -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 +1334 -696
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
- 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
|
|
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,11 @@ export interface ConnectionManagerOptions {
|
|
|
25
25
|
*/
|
|
26
26
|
onEnvelope: (envelope: Envelope) => void | Promise<void>;
|
|
27
27
|
onStateChange?: (state: ConnectionState) => void;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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.
|
|
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
|
|
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`.
|
|
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
|
-
*
|
|
92
|
-
*
|
|
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
|
|
105
|
-
*
|
|
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).
|
|
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`
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
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
|
|
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 (
|
|
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
|
|
251
|
-
*
|
|
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
|
|
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.
|
|
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
|
-
|
|
223
|
+
waitForConnection(timeoutMs?: number): Promise<void>;
|
|
273
224
|
/**
|
|
274
|
-
* Stops
|
|
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
|
|
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`
|
|
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()
|
|
329
|
-
*
|
|
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
|
|
335
|
-
*
|
|
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
|
-
*
|
|
364
|
-
* against
|
|
365
|
-
*
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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;
|