@byok-sdk/client 0.2.0 → 0.4.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 +58 -5
- package/dist/adapters/claude/claude-adapter.d.ts +6 -19
- package/dist/adapters/claude/events.d.ts +3 -0
- package/dist/adapters/claude/process-client.d.ts +9 -1
- package/dist/adapters/codex/codex-adapter.d.ts +4 -15
- package/dist/adapters/codex/process-runner.d.ts +4 -1
- package/dist/adapters/index.d.ts +4 -2
- package/dist/adapters/index.js +1081 -258
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/pi-adapter.d.ts +24 -15
- package/dist/adapters/pi/rpc-client.d.ts +9 -1
- package/dist/adapters/process-tree.d.ts +19 -0
- package/dist/adapters/provider-credential-environment.d.ts +18 -0
- package/dist/bin/audit-log.d.ts +12 -0
- package/dist/bin/byok-agent.js +2686 -912
- package/dist/bin/byok-agent.js.map +1 -1
- package/dist/bin/byok-approval-mcp.js +2 -2
- package/dist/bin/byok-approval-mcp.js.map +1 -1
- package/dist/bin/commands/workspaces.d.ts +11 -0
- package/dist/bin/format.d.ts +13 -0
- package/dist/bin/runtime-probe.d.ts +1 -1
- package/dist/bin/tasks-view.d.ts +13 -0
- package/dist/daemon/approvals.d.ts +2 -2
- package/dist/daemon/assertion-client.d.ts +68 -0
- package/dist/daemon/capabilities-client.d.ts +48 -0
- package/dist/daemon/connection-manager.d.ts +4 -2
- package/dist/daemon/control-protocol.d.ts +81 -4
- package/dist/daemon/control-server.d.ts +18 -1
- package/dist/daemon/create-daemon.d.ts +171 -3
- package/dist/daemon/daemon-owner.d.ts +37 -0
- package/dist/daemon/device-assertion-signer.d.ts +41 -0
- package/dist/daemon/device-keys.d.ts +15 -13
- package/dist/daemon/environment.d.ts +9 -9
- package/dist/daemon/git-workspace.d.ts +21 -0
- package/dist/daemon/observer.d.ts +81 -3
- package/dist/daemon/presence-publisher.d.ts +98 -0
- package/dist/daemon/runtime-capabilities.d.ts +1 -1
- package/dist/daemon/skill-pack-installer.d.ts +116 -0
- package/dist/daemon/task-runner.d.ts +156 -37
- package/dist/daemon/ws-transport.d.ts +3 -1
- package/dist/index.d.ts +25 -4
- package/dist/index.js +2972 -597
- package/dist/index.js.map +1 -1
- package/dist/runtime-failure.d.ts +64 -0
- package/dist/types.d.ts +114 -58
- package/package.json +4 -4
|
@@ -20,8 +20,8 @@ import type { ConnectionState } from './ws-transport';
|
|
|
20
20
|
* - `ConnectionManagerOptions.onEnvelope`/`onStateChange` — likewise already
|
|
21
21
|
* `create-daemon.ts`'s own closures (`(envelope) =>
|
|
22
22
|
* runner?.handleEnvelope(envelope)`, `(state) => { connectionState = state;
|
|
23
|
-
* }`). `onEnvelope` additionally exposes the raw INBOUND
|
|
24
|
-
* one event with no corresponding outbound envelope of its own — see
|
|
23
|
+
* }`). `onEnvelope` additionally exposes the raw INBOUND offer variants —
|
|
24
|
+
* the one event class with no corresponding outbound envelope of its own — see
|
|
25
25
|
* `handleInboundEnvelope`.
|
|
26
26
|
*
|
|
27
27
|
* Neither seam required adding anything to `TaskRunnerDeps`/`TaskRunner`
|
|
@@ -188,6 +188,57 @@ export type DaemonEvent = {
|
|
|
188
188
|
conflicted: number;
|
|
189
189
|
};
|
|
190
190
|
errorCategory?: string;
|
|
191
|
+
} | {
|
|
192
|
+
kind: 'runtime-disposal-failed';
|
|
193
|
+
ts: string;
|
|
194
|
+
taskId: string;
|
|
195
|
+
runtimeId: string;
|
|
196
|
+
stage: 'signal' | 'quiescence' | 'cleanup';
|
|
197
|
+
reason: string;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Plan `device-assertion-broker`: one `assertion.issue` control call
|
|
201
|
+
* resolved — either an assertion was minted (`issued`) or one of the six
|
|
202
|
+
* fail-closed gates refused (`denied`, with `reason` naming which one; see
|
|
203
|
+
* `ASSERTION_ISSUE_ERROR_CODES`, `control-protocol.ts`).
|
|
204
|
+
*
|
|
205
|
+
* ONE kind for both outcomes, so an operator reading the feed sees the
|
|
206
|
+
* issuance rate and the refusal rate on the same line shape rather than
|
|
207
|
+
* having to correlate two.
|
|
208
|
+
*
|
|
209
|
+
* The signature and the envelope bytes are NOT fields of this event and
|
|
210
|
+
* never can be. That is structural, not a redaction rule: `bin/audit-log.ts`
|
|
211
|
+
* can only drop what it is handed, and an event type that cannot carry a
|
|
212
|
+
* signature cannot leak one into a durable file no matter how the audit
|
|
213
|
+
* projection is later edited. What IS carried is the metadata an incident
|
|
214
|
+
* needs — which audience, which `jti` (so a suspect assertion presented to
|
|
215
|
+
* the host's cloud can be traced back to the exact local call that minted
|
|
216
|
+
* it), and when it expires.
|
|
217
|
+
*
|
|
218
|
+
* codex round-2 F4 — the union is split by `result` for the same structural
|
|
219
|
+
* reason: on the ISSUED path `audience` came from this daemon's own
|
|
220
|
+
* configured allowlist (operator-authored, safe verbatim), but on the DENIED
|
|
221
|
+
* path it is whatever the CALLER sent — free text that could be a PEM, a
|
|
222
|
+
* signature, or any secret shaped as an audience. So the denied variant has
|
|
223
|
+
* NO raw `audience` field at all; it carries only `audienceSize` (a byte
|
|
224
|
+
* count), computed at event-construction time (`noteDeviceAssertion`). The
|
|
225
|
+
* raw denied audience therefore never reaches the observer feed, `format.ts`,
|
|
226
|
+
* daemon stdout, or the audit file — there is no field to carry it, rather
|
|
227
|
+
* than a redactor that has to remember to strip it.
|
|
228
|
+
*/
|
|
229
|
+
| {
|
|
230
|
+
kind: 'device-assertion';
|
|
231
|
+
ts: string;
|
|
232
|
+
result: 'issued';
|
|
233
|
+
audience: string;
|
|
234
|
+
jti: string;
|
|
235
|
+
expiresAt: string;
|
|
236
|
+
} | {
|
|
237
|
+
kind: 'device-assertion';
|
|
238
|
+
ts: string;
|
|
239
|
+
result: 'denied';
|
|
240
|
+
reason: string;
|
|
241
|
+
audienceSize?: number;
|
|
191
242
|
};
|
|
192
243
|
export type DaemonEventListener = (event: DaemonEvent) => void;
|
|
193
244
|
export type Unsubscribe = () => void;
|
|
@@ -246,7 +297,7 @@ export declare class DaemonObserver {
|
|
|
246
297
|
tasks(): DaemonTaskInfo[];
|
|
247
298
|
/**
|
|
248
299
|
* Feed a raw INBOUND (server -> daemon) envelope. Deliberately narrow: only
|
|
249
|
-
*
|
|
300
|
+
* either offer variant produces a local event here — every other inbound type
|
|
250
301
|
* (`task.cancel`/`task.steer`/`task.approve`/`task.reject`) is a
|
|
251
302
|
* best-effort notification whose OWN observable effect already surfaces
|
|
252
303
|
* through the daemon's outbound envelopes (`task.cancelled`, `task.progress`
|
|
@@ -269,6 +320,27 @@ export declare class DaemonObserver {
|
|
|
269
320
|
noteShutdownRequested(reason: string): void;
|
|
270
321
|
/** M4 Phase 2: see the `shutdown-complete` `DaemonEvent` variant's own doc comment (finding F5(b): `undeliveredOutboxCount`). */
|
|
271
322
|
noteShutdownComplete(reason: string, undeliveredOutboxCount?: number): void;
|
|
323
|
+
/**
|
|
324
|
+
* Plan `device-assertion-broker`: see the `device-assertion` `DaemonEvent`
|
|
325
|
+
* variant's own doc comment. The parameter type is what keeps the signature
|
|
326
|
+
* out — there is no field to pass one through.
|
|
327
|
+
*
|
|
328
|
+
* codex round-2 F4: the DENIED caller can pass its raw `audience` here, but
|
|
329
|
+
* it is converted to a byte SIZE the instant the event is constructed and the
|
|
330
|
+
* raw string is dropped — it is never placed on the emitted `DaemonEvent`, so
|
|
331
|
+
* it cannot reach a subscriber, `format.ts`, stdout, or the audit file. The
|
|
332
|
+
* ISSUED `audience` came from the allowlist and is kept verbatim.
|
|
333
|
+
*/
|
|
334
|
+
noteDeviceAssertion(event: {
|
|
335
|
+
result: 'issued';
|
|
336
|
+
audience: string;
|
|
337
|
+
jti: string;
|
|
338
|
+
expiresAt: string;
|
|
339
|
+
} | {
|
|
340
|
+
result: 'denied';
|
|
341
|
+
reason: string;
|
|
342
|
+
audience?: string;
|
|
343
|
+
}): void;
|
|
272
344
|
/** M4 Phase 3 hardening: see the `stale-approval-decision` `DaemonEvent` variant's own doc comment. */
|
|
273
345
|
noteStaleApprovalDecision(taskId: string, decision: 'approve' | 'reject', reason?: string): void;
|
|
274
346
|
noteGitWorkspace(event: {
|
|
@@ -285,6 +357,12 @@ export declare class DaemonObserver {
|
|
|
285
357
|
};
|
|
286
358
|
errorCategory?: string;
|
|
287
359
|
}): void;
|
|
360
|
+
noteRuntimeDisposalFailure(event: {
|
|
361
|
+
taskId: string;
|
|
362
|
+
runtimeId: string;
|
|
363
|
+
stage: 'signal' | 'quiescence' | 'cleanup';
|
|
364
|
+
reason: string;
|
|
365
|
+
}): void;
|
|
288
366
|
/**
|
|
289
367
|
* Finding F4: wired from `TaskRunnerDeps.onApprovalDispatched`, called
|
|
290
368
|
* synchronously by `TaskRunner.dispatchApproval` BEFORE its own
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presence producer (§12.3): a periodic `{ level: 'online' }` heartbeat to
|
|
3
|
+
* `PUT /byok/presence`, started only when the deployment's capability
|
|
4
|
+
* declaration contains `presence.hints` (ADR-010 — see `capabilities-client.ts`).
|
|
5
|
+
*
|
|
6
|
+
* Three properties this module deliberately holds:
|
|
7
|
+
*
|
|
8
|
+
* - **Only `online`.** The five levels exist in core, but nothing downstream
|
|
9
|
+
* consumes a `thinking`/`working`/`error` mapping yet, so none is invented
|
|
10
|
+
* here. That also keeps the rule below trivially true.
|
|
11
|
+
* - **It never reads or writes task state.** Presence is a lossy, unsigned,
|
|
12
|
+
* TTL-bounded hint that must never become coordination or execution state
|
|
13
|
+
* (`@byok-sdk/core`'s `presence.ts` module doc). This publisher's only input
|
|
14
|
+
* is a clock.
|
|
15
|
+
* - **Stopping IS the offline signal.** No explicit `offline` publish on
|
|
16
|
+
* shutdown: the hosted hint carries a TTL and expiry means *absence*, so a
|
|
17
|
+
* daemon that stops beating disappears on its own. Publishing `offline` too
|
|
18
|
+
* would express the same fact through a second channel that a crashed daemon
|
|
19
|
+
* could never use anyway.
|
|
20
|
+
*
|
|
21
|
+
* Auth reuses the daemon's one device-token lifecycle: {@link authedFetch}
|
|
22
|
+
* attaches the current bearer and, on a 401, renews once through `AuthManager`
|
|
23
|
+
* and retries exactly once. A revoked device (`DeviceRevokedError`) stops this
|
|
24
|
+
* publisher permanently — there is no recourse but a fresh `pair()`, so
|
|
25
|
+
* retrying would be a pure spin.
|
|
26
|
+
*/
|
|
27
|
+
import { type ToolsetId } from '@byok-sdk/protocol';
|
|
28
|
+
import type { AuthManager } from './auth-manager';
|
|
29
|
+
/**
|
|
30
|
+
* Client-side defaults, chosen against the hosted defaults
|
|
31
|
+
* (`@byok-sdk/cloud`'s `DEFAULT_PRESENCE_TTL_MS` = 90s,
|
|
32
|
+
* `DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS` = 5s) and core §12.7.5's suggested
|
|
33
|
+
* 60-120s presence TTL. Spelled out here rather than imported: the daemon must
|
|
34
|
+
* not depend on the hosted package, and these are this producer's own cadence
|
|
35
|
+
* expectations, not a mirror of any deployment's configuration.
|
|
36
|
+
*
|
|
37
|
+
* The invariant that matters is `minimumIntervalMs < intervalMs < ttlMs`,
|
|
38
|
+
* asserted in the constructor: beat faster than the store's throttle and every
|
|
39
|
+
* other beat is rejected as `hint_rate_limited`; beat slower than the TTL and
|
|
40
|
+
* the device flickers offline between beats.
|
|
41
|
+
*/
|
|
42
|
+
export declare const DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS = 30000;
|
|
43
|
+
export declare const DEFAULT_PRESENCE_TTL_MS = 90000;
|
|
44
|
+
export declare const DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS = 5000;
|
|
45
|
+
/**
|
|
46
|
+
* The cadence invariant, as a function so the two places that need it — this
|
|
47
|
+
* module's constructor and `create-daemon.ts`'s synchronous config validation —
|
|
48
|
+
* share one authority instead of two spellings of the same band.
|
|
49
|
+
*
|
|
50
|
+
* @throws {Error} when the cadence is outside `minimumIntervalMs < intervalMs < ttlMs`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function assertPresenceHeartbeatCadence(cadence: {
|
|
53
|
+
intervalMs: number;
|
|
54
|
+
ttlMs: number;
|
|
55
|
+
minimumIntervalMs: number;
|
|
56
|
+
}): void;
|
|
57
|
+
export interface PresencePublisherOptions {
|
|
58
|
+
serverUrl: string;
|
|
59
|
+
auth: AuthManager;
|
|
60
|
+
/** Sorted logical IDs only. Executable MCP definitions and credentials remain device-local. */
|
|
61
|
+
configuredToolsets?: readonly ToolsetId[];
|
|
62
|
+
/** Heartbeat cadence. Must sit strictly between {@link PresencePublisherOptions.minimumIntervalMs} and {@link PresencePublisherOptions.ttlMs}. */
|
|
63
|
+
intervalMs?: number;
|
|
64
|
+
/** The deployment's presence hint TTL, as this daemon understands it. Only used to validate the cadence. */
|
|
65
|
+
ttlMs?: number;
|
|
66
|
+
/** The deployment's minimum accepted interval between publications, as this daemon understands it. Only used to validate the cadence. */
|
|
67
|
+
minimumIntervalMs?: number;
|
|
68
|
+
/**
|
|
69
|
+
* Observable degradation sink — one line per publish failure and one for the
|
|
70
|
+
* permanent stop. The daemon passes its `console.warn` convention; tests read
|
|
71
|
+
* it directly instead of scraping stdout.
|
|
72
|
+
*/
|
|
73
|
+
onDegraded?: (reason: string) => void;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Periodic `online` heartbeat. Construct once per `start()`, {@link stop} it in
|
|
77
|
+
* the shutdown sequence.
|
|
78
|
+
*
|
|
79
|
+
* The loop is a self-rescheduling `setTimeout` rather than `setInterval`: a slow
|
|
80
|
+
* publish must delay the next beat, not queue a second one behind it.
|
|
81
|
+
*/
|
|
82
|
+
export declare class PresencePublisher {
|
|
83
|
+
private readonly opts;
|
|
84
|
+
private readonly url;
|
|
85
|
+
private readonly intervalMs;
|
|
86
|
+
private timer;
|
|
87
|
+
private running;
|
|
88
|
+
/** Set once a revoked device is observed. Terminal: `start()` will not restart this instance. */
|
|
89
|
+
private stoppedPermanently;
|
|
90
|
+
constructor(opts: PresencePublisherOptions);
|
|
91
|
+
/** Publishes immediately, then every `intervalMs`. Idempotent; a no-op after a permanent stop. */
|
|
92
|
+
start(): void;
|
|
93
|
+
/** Stops the cadence. Idempotent, and the only "offline" signal this producer emits — the hint's TTL does the rest. */
|
|
94
|
+
stop(): void;
|
|
95
|
+
private schedule;
|
|
96
|
+
private beat;
|
|
97
|
+
private stopPermanently;
|
|
98
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RuntimeCapabilities as ProtocolRuntimeCapabilities } from '@byok-sdk/protocol';
|
|
2
2
|
import type { RuntimeCapabilities } from '../types';
|
|
3
3
|
/**
|
|
4
|
-
* Maps a `
|
|
4
|
+
* Maps a frozen `RuntimeAdapterDescriptor`'s internal `capabilities` value
|
|
5
5
|
* (`../types.ts`'s `RuntimeCapabilities` — `{steer, resume,
|
|
6
6
|
* approvalInteractive, permissionModes}`, always-required fields) onto the
|
|
7
7
|
* wire's `RuntimeCapabilities` shape (`@byok-sdk/protocol` — the same field names,
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { type CapabilityDeclaration } from '@byok-sdk/core';
|
|
2
|
+
import type { AuthManager } from './auth-manager';
|
|
3
|
+
/**
|
|
4
|
+
* The hosted capability this pipeline gates on. A plain string, not an import
|
|
5
|
+
* from `@byok-sdk/cloud`: the daemon must not gain a dependency on the hosted
|
|
6
|
+
* implementation, and ADR-010 makes capability names deployment vocabulary that
|
|
7
|
+
* core validates the shape of but never the meaning of. Same rule the presence
|
|
8
|
+
* producer's `PRESENCE_HINTS_CAPABILITY` follows.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SKILL_PACKS_CAPABILITY = "skills.pack";
|
|
11
|
+
/** Directory under `dataDir` this module owns end to end. */
|
|
12
|
+
export declare const SKILL_PACKS_DIRNAME = "skill-packs";
|
|
13
|
+
/** Per-pack pointer at the installed content-addressed revision. */
|
|
14
|
+
export declare const SKILL_PACK_LOCK_FILENAME = "lock.json";
|
|
15
|
+
/** Append-only install/refusal record, beside the packs it describes. */
|
|
16
|
+
export declare const SKILL_PACK_AUDIT_FILENAME = "audit.jsonl";
|
|
17
|
+
export declare const SKILL_PACK_LOCK_SCHEMA = "byok-skill-pack-lock-v1";
|
|
18
|
+
/**
|
|
19
|
+
* Ceiling on a single HTTP response this pipeline will read into memory.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately derived from the pack cap rather than chosen independently: the
|
|
22
|
+
* largest legitimate manifest list is bounded by what a pack may declare, and a
|
|
23
|
+
* response beyond that is refused before it is parsed rather than after. A
|
|
24
|
+
* transfer cap that is not evaluated is the exact failure this plan set out not
|
|
25
|
+
* to repeat.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SKILL_PACK_RESPONSE_MAX_BYTES: number;
|
|
28
|
+
export declare const SKILL_PACK_INSTALL_ERROR_CODES: readonly ['capability_unavailable', 'transport_failed', 'response_invalid', 'response_too_large', 'manifest_invalid', 'content_rejected', 'store_unsafe'];
|
|
29
|
+
export type SkillPackInstallErrorCode = (typeof SKILL_PACK_INSTALL_ERROR_CODES)[number];
|
|
30
|
+
/**
|
|
31
|
+
* Every way an install can be refused, as one type with a `code`.
|
|
32
|
+
*
|
|
33
|
+
* Code-based branching rather than a class per failure — the same idiom
|
|
34
|
+
* `@byok-sdk/core` uses — because a caller only ever needs two decisions from
|
|
35
|
+
* this: "was the channel unavailable" (retry later, or the deployment simply
|
|
36
|
+
* does not offer it) versus "was the content refused" (a publication problem
|
|
37
|
+
* nobody on this device can fix by retrying).
|
|
38
|
+
*/
|
|
39
|
+
export declare class SkillPackInstallError extends Error {
|
|
40
|
+
readonly code: SkillPackInstallErrorCode;
|
|
41
|
+
readonly packName: string | undefined;
|
|
42
|
+
constructor(code: SkillPackInstallErrorCode, message: string, options?: {
|
|
43
|
+
cause?: unknown;
|
|
44
|
+
packName?: string;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** What a device recorded about one installed pack. Mirrors `lock.json` on disk. */
|
|
48
|
+
export interface SkillPackLock {
|
|
49
|
+
readonly schema: typeof SKILL_PACK_LOCK_SCHEMA;
|
|
50
|
+
readonly name: string;
|
|
51
|
+
readonly version: string;
|
|
52
|
+
readonly description: string;
|
|
53
|
+
readonly content_hash: string;
|
|
54
|
+
/** The deployment the pack came from, normalized to its http(s) origin. Never a token, never a path. */
|
|
55
|
+
readonly source: string;
|
|
56
|
+
readonly installed_at: string;
|
|
57
|
+
readonly files: readonly {
|
|
58
|
+
readonly path: string;
|
|
59
|
+
readonly sha256: string;
|
|
60
|
+
readonly bytes: number;
|
|
61
|
+
}[];
|
|
62
|
+
}
|
|
63
|
+
export interface InstalledSkillPack {
|
|
64
|
+
readonly name: string;
|
|
65
|
+
readonly lock: SkillPackLock;
|
|
66
|
+
/** Absolute path of the content-addressed revision `lock.json` points at. */
|
|
67
|
+
readonly directory: string;
|
|
68
|
+
}
|
|
69
|
+
export interface InstallSkillPacksOptions {
|
|
70
|
+
/** The SDK data directory. A daemon passes its `storeDir`; the pack tree lives beside its other private state. */
|
|
71
|
+
readonly dataDir: string;
|
|
72
|
+
readonly serverUrl: string;
|
|
73
|
+
readonly auth: AuthManager;
|
|
74
|
+
/** The declaration read from `GET /byok/capabilities`. Never re-derived here, and never assumed. */
|
|
75
|
+
readonly declaration: CapabilityDeclaration;
|
|
76
|
+
readonly signal?: AbortSignal;
|
|
77
|
+
}
|
|
78
|
+
export interface SkillPackInstallResult {
|
|
79
|
+
readonly installed: readonly InstalledSkillPack[];
|
|
80
|
+
/** Packs already present at the same content hash. Re-installing is a no-op, not a rewrite. */
|
|
81
|
+
readonly unchanged: readonly string[];
|
|
82
|
+
}
|
|
83
|
+
/** The store root this module owns. */
|
|
84
|
+
export declare function skillPacksRoot(dataDir: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* Fetches, verifies and installs every pack this deployment offers.
|
|
87
|
+
*
|
|
88
|
+
* @throws {SkillPackInstallError} `capability_unavailable` before any request
|
|
89
|
+
* is issued when the declaration does not name `skills.pack`.
|
|
90
|
+
*/
|
|
91
|
+
export declare function installSkillPacks(options: InstallSkillPacksOptions): Promise<SkillPackInstallResult>;
|
|
92
|
+
/**
|
|
93
|
+
* Every pack this device has a valid lock for, sorted by name.
|
|
94
|
+
*
|
|
95
|
+
* Reads only the locks — never the pack bytes — so a caller listing what is
|
|
96
|
+
* installed pays nothing for packs it is not about to project.
|
|
97
|
+
*/
|
|
98
|
+
export declare function listInstalledSkillPacks(dataDir: string): Promise<readonly InstalledSkillPack[]>;
|
|
99
|
+
export interface ProjectedSkillPack {
|
|
100
|
+
readonly name: string;
|
|
101
|
+
readonly contentHash: string;
|
|
102
|
+
readonly targetDir: string;
|
|
103
|
+
readonly files: readonly string[];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Copies an installed pack's files into a host-chosen directory.
|
|
107
|
+
*
|
|
108
|
+
* Copy, not symlink, and re-verified on the way out: the store is on the same
|
|
109
|
+
* machine as whatever else runs there, so the bytes are hashed again and
|
|
110
|
+
* compared against the lock before they are handed to a runtime. An install
|
|
111
|
+
* that was verified last week is not evidence about the file on disk today.
|
|
112
|
+
*
|
|
113
|
+
* @throws {SkillPackInstallError} `store_unsafe` for a missing pack, a symlink
|
|
114
|
+
* anywhere in the pack, or a file whose bytes no longer match the lock.
|
|
115
|
+
*/
|
|
116
|
+
export declare function projectSkillPack(dataDir: string, name: string, targetDir: string): Promise<ProjectedSkillPack>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type Envelope, type PermissionPolicy, type RuntimeId, type TaskOfferPayload } from '@byok-sdk/protocol';
|
|
2
|
-
import { type RuntimeAdapter } from '../types';
|
|
1
|
+
import { type Envelope, type PermissionPolicy, type RuntimeId, type TaskOfferPayload, type TaskOfferWithToolsetsPayload } from '@byok-sdk/protocol';
|
|
2
|
+
import { type McpToolsetConfig, type RuntimeAdapter } from '../types';
|
|
3
|
+
import { type RuntimeDisposalStage } from '../runtime-failure';
|
|
3
4
|
import { type ApprovalDecision, type ApprovalOrigin, type ApprovalRegistry } from './approvals';
|
|
4
5
|
import type { BlobResolver } from './blob-client';
|
|
5
6
|
import type { TaskQueueWatermark } from './control-protocol';
|
|
@@ -101,6 +102,56 @@ export declare const MAX_TRACKED_TASK_IDS = 2000;
|
|
|
101
102
|
export declare const MAX_DURATION_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxDurationMs";
|
|
102
103
|
/** M5 batch-3 (workstream 2): same contract as {@link MAX_DURATION_EXCEEDED_REASON_PREFIX}, for `DaemonConfig.maxTaskOutputBytes` — see `TaskRunner.pump`'s own per-event byte counting. */
|
|
103
104
|
export declare const MAX_OUTPUT_BYTES_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxTaskOutputBytes";
|
|
105
|
+
/**
|
|
106
|
+
* additive-minor (`task.complete.document`): same stable-PREFIX contract as
|
|
107
|
+
* {@link MAX_DURATION_EXCEEDED_REASON_PREFIX} above, carried by every
|
|
108
|
+
* `task.fail` this daemon reports because a configured
|
|
109
|
+
* `DaemonConfig.resultDocument` extractor produced a document that could not
|
|
110
|
+
* be delivered — over the cap, not JSON-serializable, or destined for a
|
|
111
|
+
* server that never advertised the `result-document` capability. All three
|
|
112
|
+
* are `retryable: false`: none of them can come out differently on a retry
|
|
113
|
+
* against the same server with the same extractor. Everything after the
|
|
114
|
+
* prefix is human-readable detail (including the measured size), not part of
|
|
115
|
+
* the stable shape.
|
|
116
|
+
*
|
|
117
|
+
* There is deliberately no "send it anyway" or "send it truncated" path.
|
|
118
|
+
* A document is the task's PRIMARY structured result, so quietly dropping or
|
|
119
|
+
* mangling it would report success while destroying the thing the task
|
|
120
|
+
* existed to produce.
|
|
121
|
+
*/
|
|
122
|
+
export declare const RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX = "result document undeliverable";
|
|
123
|
+
/**
|
|
124
|
+
* The task identity handed to a {@link ResultDocumentExtractor} alongside the
|
|
125
|
+
* final output text. Deliberately minimal — identity only, no session
|
|
126
|
+
* handle, no workspace path, no adapter: this seam exists to turn text the
|
|
127
|
+
* runtime already produced into the product's own JSON, not to become a
|
|
128
|
+
* general-purpose end-of-task callback with access to the daemon's innards.
|
|
129
|
+
*/
|
|
130
|
+
export interface ResultDocumentTask {
|
|
131
|
+
readonly taskId: string;
|
|
132
|
+
readonly sessionRef: string;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Host-supplied glue that turns a finished task's final output into the
|
|
136
|
+
* product's structured terminal result (`task.complete.document`). Returning
|
|
137
|
+
* `undefined` means "this task has no structured result" and completes the
|
|
138
|
+
* task exactly as it would have without an extractor configured at all.
|
|
139
|
+
*
|
|
140
|
+
* SYNCHRONOUS by contract, like every other single-purpose callback on
|
|
141
|
+
* `TaskRunnerDeps`, and the runtime ENFORCES that rather than trusting it:
|
|
142
|
+
* the returned value is treated as data and JSON-encoded as-is, never
|
|
143
|
+
* awaited, so a returned promise would encode to an empty document (`{}`) —
|
|
144
|
+
* a well-formed, under-cap, and completely WRONG result. A thenable return
|
|
145
|
+
* is therefore rejected exactly like a throw (`task.fail`, `retryable:
|
|
146
|
+
* false`), because delivering a confidently wrong terminal result is worse
|
|
147
|
+
* than delivering none.
|
|
148
|
+
*
|
|
149
|
+
* Throwing is a real outcome, not a nuisance: it fails the task
|
|
150
|
+
* (`retryable: false`) rather than completing it without the result the
|
|
151
|
+
* extractor was supposed to produce — see {@link
|
|
152
|
+
* RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}.
|
|
153
|
+
*/
|
|
154
|
+
export type ResultDocumentExtractor = (finalOutput: string, task: ResultDocumentTask) => unknown;
|
|
104
155
|
/**
|
|
105
156
|
* M5 batch-3 (workstream 2): default cap (64 MiB) on accumulated
|
|
106
157
|
* (approximate) agent-event output bytes this daemon tolerates for a single
|
|
@@ -130,6 +181,8 @@ export interface TaskRunnerDeps {
|
|
|
130
181
|
runtimeEnvironment?: Record<string, {
|
|
131
182
|
allow?: string[];
|
|
132
183
|
}>;
|
|
184
|
+
/** Validated, device-local registry keyed by wire-level logical toolset id. */
|
|
185
|
+
mcpToolsets?: ReadonlyMap<string, McpToolsetConfig>;
|
|
133
186
|
permissionDefaults?: PermissionPolicy;
|
|
134
187
|
workspaceRoot: string;
|
|
135
188
|
deviceId: string;
|
|
@@ -153,13 +206,20 @@ export interface TaskRunnerDeps {
|
|
|
153
206
|
observation?: GitWorkspaceObservation;
|
|
154
207
|
errorCategory?: string;
|
|
155
208
|
}) => void;
|
|
209
|
+
/** Local-only evidence that a semantic terminal outcome could not yet release its runtime ownership. */
|
|
210
|
+
onRuntimeDisposalFailure?: (event: {
|
|
211
|
+
taskId: string;
|
|
212
|
+
runtimeId: string;
|
|
213
|
+
stage: RuntimeDisposalStage;
|
|
214
|
+
reason: string;
|
|
215
|
+
}) => void;
|
|
156
216
|
/**
|
|
157
217
|
* M4 Phase 3: this daemon's control-socket identity + the shared registry
|
|
158
218
|
* backing the control socket's own `approvals.list`/`approvals.resolve`
|
|
159
219
|
* methods (`create-daemon.ts` constructs ONE `ApprovalRegistry` and passes
|
|
160
220
|
* the SAME instance here) — see `requestApproval`'s own doc comment for
|
|
161
221
|
* why `TaskRunner` needs a handle on all three. `storeDir`/`productId` are
|
|
162
|
-
* copied verbatim into every
|
|
222
|
+
* copied verbatim into every prepared operation's approval channel.
|
|
163
223
|
*/
|
|
164
224
|
approvalRegistry: ApprovalRegistry;
|
|
165
225
|
storeDir: string;
|
|
@@ -192,7 +252,7 @@ export interface TaskRunnerDeps {
|
|
|
192
252
|
* to supply one — mirrors `onStaleApprovalDecision`'s own contract.
|
|
193
253
|
*/
|
|
194
254
|
onApprovalDispatched?: (taskId: string, approvalId: string) => void;
|
|
195
|
-
/**
|
|
255
|
+
/** Overrides the bounded soft-interrupt window before authoritative `Session.close()` disposal begins. */
|
|
196
256
|
shutdownInterruptTimeoutMs?: number;
|
|
197
257
|
/**
|
|
198
258
|
* M5 batch-3 (workstream 2): overrides {@link DEFAULT_MAX_TASK_OUTPUT_BYTES}
|
|
@@ -240,8 +300,22 @@ export interface TaskRunnerDeps {
|
|
|
240
300
|
*/
|
|
241
301
|
admissionGuard?: (offer: {
|
|
242
302
|
readonly taskId: string;
|
|
243
|
-
readonly payload:
|
|
303
|
+
readonly payload: AcceptedOfferPayload;
|
|
244
304
|
}) => AdmissionGuardDecision;
|
|
305
|
+
/**
|
|
306
|
+
* additive-minor (`task.complete.document`): the host's structured-result
|
|
307
|
+
* extractor, consulted once per task at the moment `task.complete` is
|
|
308
|
+
* built — see {@link ResultDocumentExtractor} and `DaemonConfig
|
|
309
|
+
* .resultDocument` (`create-daemon.ts`) for the full contract.
|
|
310
|
+
*
|
|
311
|
+
* Optional, and absent by default: with no extractor supplied, the
|
|
312
|
+
* completion path is byte-identical to what it was before this seam
|
|
313
|
+
* existed — no document is computed, no capability is consulted, and
|
|
314
|
+
* `task.complete` carries exactly the fields it always did.
|
|
315
|
+
*/
|
|
316
|
+
resultDocument?: {
|
|
317
|
+
readonly extract: ResultDocumentExtractor;
|
|
318
|
+
};
|
|
245
319
|
}
|
|
246
320
|
/** See {@link TaskRunnerDeps.admissionGuard}. */
|
|
247
321
|
export type AdmissionGuardDecision = {
|
|
@@ -251,9 +325,10 @@ export type AdmissionGuardDecision = {
|
|
|
251
325
|
readonly reason: string;
|
|
252
326
|
readonly retryable: boolean;
|
|
253
327
|
};
|
|
328
|
+
type AcceptedOfferPayload = TaskOfferPayload | TaskOfferWithToolsetsPayload;
|
|
254
329
|
/**
|
|
255
|
-
* Per-connection task orchestration: offer -> (decline |
|
|
256
|
-
*
|
|
330
|
+
* Per-connection task orchestration: offer -> (decline | prepare -> seal ->
|
|
331
|
+
* claim -> prepared operation -> started) -> seq-ordered progress batches -> complete/fail/
|
|
257
332
|
* cancelled, plus approve/reject/cancel/steer handling.
|
|
258
333
|
*
|
|
259
334
|
* M1 rework (docs/protocol.md §3, §5, §10 — `packages/protocol` is frozen,
|
|
@@ -273,13 +348,13 @@ export declare class TaskRunner {
|
|
|
273
348
|
* Finding F4 (cancel lost during the offer-processing window): a
|
|
274
349
|
* `task.cancel` for a taskId that hasn't finished `handleOffer` yet (still
|
|
275
350
|
* awaiting adapter detection / instruction resolution / workspace setup /
|
|
276
|
-
* `
|
|
351
|
+
* prepared operation `start()`) has no `this.tasks` entry to land on — it used to be
|
|
277
352
|
* silently dropped, and the runtime session `handleOffer` was about to
|
|
278
353
|
* register would then run an unsupervised ("zombie") turn nobody asked
|
|
279
354
|
* for anymore. Recording the taskId here lets `handleOffer` consult it at
|
|
280
355
|
* the two points where it can still safely react (see its body): before
|
|
281
356
|
* claiming at all (decline instead of ever starting a session), and right
|
|
282
|
-
* after
|
|
357
|
+
* after the prepared operation resolves but before this task is registered as
|
|
283
358
|
* active (tear the just-started session down immediately, before its
|
|
284
359
|
* event loop ever pumps a single event). Consumed (deleted) at whichever
|
|
285
360
|
* checkpoint handles it; a cancel for a taskId that's already active,
|
|
@@ -305,12 +380,12 @@ export declare class TaskRunner {
|
|
|
305
380
|
* checkpoint-2 cancel-teardown, or successful registration into
|
|
306
381
|
* `this.tasks`). Bounded eviction on `pendingCancelled` (below) must never
|
|
307
382
|
* remove an entry for a taskId in this set: doing so is exactly the bug —
|
|
308
|
-
* block task A in `
|
|
383
|
+
* block task A in prepared-operation `start()`, deliver A's own `task.cancel` (so
|
|
309
384
|
* `pendingCancelled` gets an entry for A while A is still in-flight),
|
|
310
385
|
* then deliver `MAX_TRACKED_TASK_IDS` more cancels for unrelated taskIds
|
|
311
386
|
* nobody ever offered — under naive oldest-wins eviction, A's entry (the
|
|
312
387
|
* single oldest) gets evicted purely because of unrelated churn, so when
|
|
313
|
-
*
|
|
388
|
+
* the prepared operation finally resolves, checkpoint 2 finds no cancel marker
|
|
314
389
|
* and the already-cancelled task starts a real session. See
|
|
315
390
|
* `evictPendingCancelled` below for the fix, and
|
|
316
391
|
* `task-runner-bounded-collections.test.ts` for a test mirroring this
|
|
@@ -330,7 +405,7 @@ export declare class TaskRunner {
|
|
|
330
405
|
* explicitly relies on redelivered handlers being idempotent for exactly
|
|
331
406
|
* this reason). `handleOffer` must treat a redelivered offer for a taskId
|
|
332
407
|
* that's already active (`this.tasks`) or already finished (this set) as
|
|
333
|
-
* a no-op — never a second `
|
|
408
|
+
* a no-op — never a second prepared-operation `start()` call, which would orphan the
|
|
334
409
|
* first session.
|
|
335
410
|
*
|
|
336
411
|
* M3-B: unbounded otherwise — a long-lived daemon that's finished many
|
|
@@ -388,10 +463,9 @@ export declare class TaskRunner {
|
|
|
388
463
|
/** M4 Phase 2: stop claiming any FUTURE `task.offer` — see `stoppingOffers`'s own doc comment. Idempotent. */
|
|
389
464
|
stopAcceptingOffers(): void;
|
|
390
465
|
/**
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
* terminal message is sent either way) but reports `task.fail` rather than
|
|
466
|
+
* Shutdown of every currently ACTIVE task for the control socket's
|
|
467
|
+
* `shutdown` RPC. Soft interrupt remains bounded, but each task's
|
|
468
|
+
* authoritative close receipt must settle successfully. Reports `task.fail` rather than
|
|
395
469
|
* `task.cancelled` — these tasks aren't ending because the SERVER
|
|
396
470
|
* cancelled them, they're ending because this device is shutting down.
|
|
397
471
|
* `retryable: true` throughout: nothing about the task/policy itself was
|
|
@@ -442,28 +516,13 @@ export declare class TaskRunner {
|
|
|
442
516
|
* unconditionally, so a hung `interrupt()` (a misbehaving adapter) can
|
|
443
517
|
* never block `task.fail` from being sent at all.
|
|
444
518
|
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
* far more here than it used to for the pre-existing graceful-shutdown-only
|
|
450
|
-
* caller, since THAT path is additionally bounded by an outer deadline
|
|
451
|
-
* (`SHUTDOWN_TASK_TEARDOWN_DEADLINE_MS`/`DaemonConfig.shutdownGraceMs`,
|
|
452
|
-
* `create-daemon.ts`), while resource-limit enforcement fires during
|
|
453
|
-
* ordinary operation with no such outer bound watching it). `close()` is
|
|
454
|
-
* every adapter's harder teardown primitive — an actual process-level kill
|
|
455
|
-
* (SIGTERM, or `taskkill /F` on Windows — see e.g.
|
|
456
|
-
* `ClaudeProcessClient.kill()`/`PiRpcClient.kill()`) as opposed to pi's own
|
|
457
|
-
* soft in-band `interrupt()` (an RPC `abort` message that leaves the
|
|
458
|
-
* process alive and resumable) — so escalating to it is the closest thing
|
|
459
|
-
* to a "hard kill" the `Session` interface exposes. `finish()` below calls
|
|
460
|
-
* `session.close()` again regardless (documented idempotent) — this isn't
|
|
461
|
-
* a substitute for that, only an earlier, bounded attempt at actually
|
|
462
|
-
* stopping a stuck runtime before this method gives up and reports failure
|
|
463
|
-
* anyway.
|
|
519
|
+
* After the bounded soft interrupt, `finish()` always awaits the authoritative
|
|
520
|
+
* `Session.close()` receipt. A failed receipt retains active/Git ownership;
|
|
521
|
+
* shutdown surfaces the rejection while resource enforcement leaves local
|
|
522
|
+
* evidence for a later retry.
|
|
464
523
|
*
|
|
465
524
|
* Re-checks task identity (`this.tasks.get(...) === active`) immediately
|
|
466
|
-
* before sending `task.fail`: the interrupt
|
|
525
|
+
* before sending `task.fail`: the interrupt race above has await
|
|
467
526
|
* points during which a DIFFERENT path (a racing `task.cancel`/
|
|
468
527
|
* `task.reject`, or the session completing normally on its own) may have
|
|
469
528
|
* already finished this exact task and sent its own terminal message.
|
|
@@ -508,6 +567,8 @@ export declare class TaskRunner {
|
|
|
508
567
|
private handleOffer;
|
|
509
568
|
/** Protocol §7: an instruction too large to inline arrives as a `blobRef` — resolve it via the blob client rather than failing closed. */
|
|
510
569
|
private resolveInstruction;
|
|
570
|
+
/** Resolve every requested logical id locally and reject missing/colliding server authority before claim. */
|
|
571
|
+
private resolveMcpServers;
|
|
511
572
|
private pump;
|
|
512
573
|
/**
|
|
513
574
|
* Protocol §7: an `artifact` `AgentEvent` only names a file the runtime
|
|
@@ -554,7 +615,7 @@ export declare class TaskRunner {
|
|
|
554
615
|
*
|
|
555
616
|
* `inFlightOffers` is naturally tiny (bounded by this device's real
|
|
556
617
|
* concurrent-offer-processing count — normally single digits, driven by
|
|
557
|
-
* how many `task.offer`s are simultaneously mid
|
|
618
|
+
* how many `task.offer`s are simultaneously mid-prepared-operation start() — nowhere
|
|
558
619
|
* near `MAX_TRACKED_TASK_IDS`), so this scan is cheap in practice: it
|
|
559
620
|
* finds a safe entry at or near the front almost always. The only case
|
|
560
621
|
* where NO entry is safe to evict is every single tracked cancel
|
|
@@ -824,9 +885,66 @@ export declare class TaskRunner {
|
|
|
824
885
|
/** Pre-claim, fail-closed rejection (protocol §3.2) — never claims first. */
|
|
825
886
|
private decline;
|
|
826
887
|
private fail;
|
|
888
|
+
/**
|
|
889
|
+
* additive-minor (`task.complete.document`): the whole daemon-side gate
|
|
890
|
+
* between a configured {@link ResultDocumentExtractor} and the wire —
|
|
891
|
+
* called once, from the `turn_end` completion path, immediately before
|
|
892
|
+
* `task.complete` is built.
|
|
893
|
+
*
|
|
894
|
+
* `{deliver: true}` means "go on and send `task.complete`", carrying the
|
|
895
|
+
* document when there is one. `{deliver: false}` means this method has
|
|
896
|
+
* ALREADY reported `task.fail` and finished the task; the caller must
|
|
897
|
+
* return without sending anything further.
|
|
898
|
+
*
|
|
899
|
+
* Four fail-closed branches, all `retryable: false` (see
|
|
900
|
+
* {@link RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX} for why none of them
|
|
901
|
+
* can succeed on a retry):
|
|
902
|
+
*
|
|
903
|
+
* 1. The extractor threw — its error is surfaced, never swallowed.
|
|
904
|
+
* 2. The extractor returned a thenable, violating the synchronous
|
|
905
|
+
* contract in the one way that would otherwise ship a wrong answer.
|
|
906
|
+
* 3. The document is over the cap, not JSON-serializable, or not plain
|
|
907
|
+
* JSON data, per `checkResultDocument` — the protocol's OWN check,
|
|
908
|
+
* imported rather than reimplemented, so this gate and the server's
|
|
909
|
+
* schema validation can never disagree about what is legal.
|
|
910
|
+
* 4. The connected server never advertised `result-document`. Its
|
|
911
|
+
* tolerant `z.object()` would silently strip the field on arrival
|
|
912
|
+
* (`version.ts`'s own flag doc comment), so "send anyway" is not a
|
|
913
|
+
* degraded-but-working path — it is the task's primary structured
|
|
914
|
+
* result being deleted in transit with nothing reported anywhere.
|
|
915
|
+
*
|
|
916
|
+
* The capability is checked LAST, deliberately: a document that is itself
|
|
917
|
+
* invalid is the host's own bug and is worth reporting as such even when
|
|
918
|
+
* the connected server could not have accepted any document at all. It is
|
|
919
|
+
* then re-checked once more by the caller after its own last await, since
|
|
920
|
+
* a reconnect can invalidate this answer in between (F3).
|
|
921
|
+
*
|
|
922
|
+
* **Residual window (bounded, deliberately not hacked around).** Even the
|
|
923
|
+
* caller's re-check happens before `ConnectionManager.send` hands the
|
|
924
|
+
* envelope to a transport, and a queued envelope can outlive the
|
|
925
|
+
* connection it was queued for: a reconnect between `send()` and the
|
|
926
|
+
* outbox actually draining could still deliver this `task.complete` to a
|
|
927
|
+
* rolled-back N-1 server that strips the document. Closing that would
|
|
928
|
+
* mean teaching the transport outbox to inspect payload semantics and
|
|
929
|
+
* mint a substitute `task.fail` for a task this runner already finished —
|
|
930
|
+
* a second authority over terminal outcomes living in the queue, which is
|
|
931
|
+
* worse than the window it closes. Documented instead, here and in
|
|
932
|
+
* docs/protocol.md §7.2.
|
|
933
|
+
*/
|
|
934
|
+
private resolveResultDocument;
|
|
935
|
+
/**
|
|
936
|
+
* Whether the CURRENTLY connected server advertised `result-document` —
|
|
937
|
+
* read fresh on every call, never captured, because the answer changes
|
|
938
|
+
* across a reconnect (`ConnectionManager.getServerCapabilities` returns
|
|
939
|
+
* `[]` from the moment an acked connection closes until a fresh
|
|
940
|
+
* `conn.ack` repopulates it). An absent `getServerCapabilities` seam is
|
|
941
|
+
* "no capabilities", the fail-closed reading.
|
|
942
|
+
*/
|
|
943
|
+
private hasResultDocumentCapability;
|
|
827
944
|
private observeGit;
|
|
828
945
|
private updateGitPhaseBestEffort;
|
|
829
946
|
private finish;
|
|
947
|
+
private reserveSemanticTerminal;
|
|
830
948
|
/** M3-B: bounded insert for `finishedTaskIds` — see its class-level doc comment and `MAX_TRACKED_TASK_IDS`. Evicts the oldest (first-inserted) entry once over cap, same idiom as `ConnectionHub.checkAndRecordDuplicate` (packages/server/src/hub.ts). */
|
|
831
949
|
private addFinishedTaskId;
|
|
832
950
|
/** `reuseDir`, when set (a known sessionRef's recorded workspace), is used verbatim instead of a fresh `workspaceRoot/<taskId>` directory — `mkdir recursive` is idempotent either way, so ensuring-exists is safe to do unconditionally. */
|
|
@@ -860,3 +978,4 @@ export declare class TaskRunner {
|
|
|
860
978
|
*/
|
|
861
979
|
private pickAdapter;
|
|
862
980
|
}
|
|
981
|
+
export {};
|