@byok-sdk/client 0.1.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/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/adapters/claude/claude-adapter.d.ts +155 -0
- package/dist/adapters/claude/events.d.ts +91 -0
- package/dist/adapters/claude/permission-mapping.d.ts +142 -0
- package/dist/adapters/claude/process-client.d.ts +103 -0
- package/dist/adapters/claude/resolve-approval-mcp-bin.d.ts +45 -0
- package/dist/adapters/claude/resolve-bin.d.ts +27 -0
- package/dist/adapters/codex/codex-adapter.d.ts +97 -0
- package/dist/adapters/codex/events.d.ts +41 -0
- package/dist/adapters/codex/permission-mapping.d.ts +107 -0
- package/dist/adapters/codex/process-runner.d.ts +86 -0
- package/dist/adapters/codex/resolve-bin.d.ts +25 -0
- package/dist/adapters/pi/events.d.ts +58 -0
- package/dist/adapters/pi/permission-mapping.d.ts +46 -0
- package/dist/adapters/pi/pi-adapter.d.ts +29 -0
- package/dist/adapters/pi/resolve-bin.d.ts +44 -0
- package/dist/adapters/pi/rpc-client.d.ts +101 -0
- package/dist/bin/approval-mcp-server.d.ts +69 -0
- package/dist/bin/audit-log.d.ts +212 -0
- package/dist/bin/byok-agent.d.ts +2 -0
- package/dist/bin/byok-agent.js +12347 -0
- package/dist/bin/byok-agent.js.map +1 -0
- package/dist/bin/byok-approval-mcp.d.ts +2 -0
- package/dist/bin/byok-approval-mcp.js +498 -0
- package/dist/bin/byok-approval-mcp.js.map +1 -0
- package/dist/bin/commands/approvals.d.ts +31 -0
- package/dist/bin/commands/approve-reject.d.ts +29 -0
- package/dist/bin/commands/doctor.d.ts +15 -0
- package/dist/bin/commands/pair.d.ts +7 -0
- package/dist/bin/commands/runtimes.d.ts +7 -0
- package/dist/bin/commands/service.d.ts +54 -0
- package/dist/bin/commands/start.d.ts +21 -0
- package/dist/bin/commands/status.d.ts +25 -0
- package/dist/bin/commands/support-bundle.d.ts +7 -0
- package/dist/bin/commands/tasks.d.ts +27 -0
- package/dist/bin/commands/unpair.d.ts +124 -0
- package/dist/bin/commands/workspaces.d.ts +16 -0
- package/dist/bin/config.d.ts +41 -0
- package/dist/bin/control-client.d.ts +50 -0
- package/dist/bin/format.d.ts +67 -0
- package/dist/bin/runtime-probe.d.ts +34 -0
- package/dist/bin/tasks-view.d.ts +49 -0
- package/dist/daemon/approvals.d.ts +87 -0
- package/dist/daemon/auth-manager.d.ts +52 -0
- package/dist/daemon/blob-client.d.ts +22 -0
- package/dist/daemon/connection-manager.d.ts +502 -0
- package/dist/daemon/control-protocol.d.ts +299 -0
- package/dist/daemon/control-server.d.ts +61 -0
- package/dist/daemon/create-daemon.d.ts +364 -0
- package/dist/daemon/cursor-store.d.ts +30 -0
- package/dist/daemon/daemon-owner.d.ts +24 -0
- package/dist/daemon/deterministic-jitter.d.ts +20 -0
- package/dist/daemon/device-keys.d.ts +45 -0
- package/dist/daemon/device-proof-signer.d.ts +41 -0
- package/dist/daemon/environment.d.ts +96 -0
- package/dist/daemon/git-workspace-store.d.ts +49 -0
- package/dist/daemon/git-workspace.d.ts +80 -0
- package/dist/daemon/http-client.d.ts +8 -0
- package/dist/daemon/journal/journal.d.ts +342 -0
- package/dist/daemon/journal/sqlite-journal.d.ts +173 -0
- package/dist/daemon/journal/sqlite-support.d.ts +96 -0
- package/dist/daemon/journal/storage-policy.d.ts +368 -0
- package/dist/daemon/long-poll-transport.d.ts +142 -0
- package/dist/daemon/observer.d.ts +345 -0
- package/dist/daemon/operational-health.d.ts +87 -0
- package/dist/daemon/policy.d.ts +16 -0
- package/dist/daemon/progress-batcher.d.ts +31 -0
- package/dist/daemon/runtime-capabilities.d.ts +36 -0
- package/dist/daemon/session-workspace-store.d.ts +75 -0
- package/dist/daemon/store.d.ts +54 -0
- package/dist/daemon/task-runner.d.ts +862 -0
- package/dist/daemon/truth-memory-client.d.ts +94 -0
- package/dist/daemon/url.d.ts +60 -0
- package/dist/daemon/ws-transport.d.ts +110 -0
- package/dist/diagnostics/diagnostics.d.ts +114 -0
- package/dist/diagnostics/support-bundle.d.ts +81 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +10056 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle/create-service-lifecycle.d.ts +59 -0
- package/dist/lifecycle/exec-runner.d.ts +141 -0
- package/dist/lifecycle/launchd.d.ts +55 -0
- package/dist/lifecycle/service-types.d.ts +177 -0
- package/dist/lifecycle/systemd.d.ts +46 -0
- package/dist/lifecycle/winsw.d.ts +64 -0
- package/dist/types.d.ts +178 -0
- package/dist/util/async-queue.d.ts +45 -0
- package/dist/util/atomic-write.d.ts +86 -0
- package/dist/util/secure-dir.d.ts +124 -0
- package/package.json +66 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { type AgentEvent, type BlobRef, type Envelope, type RuntimeInfo, type TaskState } from '@byok-sdk/protocol';
|
|
2
|
+
import type { ConnectionState } from './ws-transport';
|
|
3
|
+
/**
|
|
4
|
+
* M3-2a: local observability for the daemon — the seam a CLI (M3-2b) drives a
|
|
5
|
+
* live task feed, a task list, and approve/reject/unpair from, all LOCALLY
|
|
6
|
+
* against a running daemon (no SaaS-side polling required).
|
|
7
|
+
*
|
|
8
|
+
* Sourcing, without editing `task-runner.ts`: `TaskRunner` never calls
|
|
9
|
+
* anything in this file directly and has no notion it exists. Every
|
|
10
|
+
* task-lifecycle transition it makes is already observable from OUTSIDE the
|
|
11
|
+
* class through two seams `create-daemon.ts` itself owns and constructs:
|
|
12
|
+
*
|
|
13
|
+
* - `TaskRunnerDeps.send` (the callback `TaskRunner` calls for every
|
|
14
|
+
* `task.claim` / `task.started` / `task.progress` / `task.artifact` /
|
|
15
|
+
* `task.await_approval` / `task.complete` / `task.fail` / `task.decline` /
|
|
16
|
+
* `task.cancelled` it ever emits) — `create-daemon.ts` already builds this
|
|
17
|
+
* closure itself (`send: (envelope) => connection?.send(envelope)`); this
|
|
18
|
+
* module just gets a chance to look at the same envelope before it goes
|
|
19
|
+
* out. See `handleOutboundEnvelope`.
|
|
20
|
+
* - `ConnectionManagerOptions.onEnvelope`/`onStateChange` — likewise already
|
|
21
|
+
* `create-daemon.ts`'s own closures (`(envelope) =>
|
|
22
|
+
* runner?.handleEnvelope(envelope)`, `(state) => { connectionState = state;
|
|
23
|
+
* }`). `onEnvelope` additionally exposes the raw INBOUND `task.offer` — the
|
|
24
|
+
* one event with no corresponding outbound envelope of its own — see
|
|
25
|
+
* `handleInboundEnvelope`.
|
|
26
|
+
*
|
|
27
|
+
* Neither seam required adding anything to `TaskRunnerDeps`/`TaskRunner`
|
|
28
|
+
* itself: both were already plain functions `create-daemon.ts` constructs,
|
|
29
|
+
* so wrapping them (call this module first, then the real behavior,
|
|
30
|
+
* unchanged) is the entire integration.
|
|
31
|
+
*/
|
|
32
|
+
/** Every local event kind this daemon can emit — see {@link DaemonEvent}. */
|
|
33
|
+
export type DaemonEventKind = DaemonEvent['kind'];
|
|
34
|
+
export type DaemonEvent = {
|
|
35
|
+
kind: 'offered';
|
|
36
|
+
ts: string;
|
|
37
|
+
taskId: string;
|
|
38
|
+
runtime?: string;
|
|
39
|
+
} | {
|
|
40
|
+
kind: 'claimed';
|
|
41
|
+
ts: string;
|
|
42
|
+
taskId: string;
|
|
43
|
+
claimedRuntime?: string;
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'started';
|
|
46
|
+
ts: string;
|
|
47
|
+
taskId: string;
|
|
48
|
+
}
|
|
49
|
+
/** One per normalized `AgentEvent` (not one per `task.progress` batch) — matches "a live task feed" better than re-exposing the wire's own batching. */
|
|
50
|
+
| {
|
|
51
|
+
kind: 'progress';
|
|
52
|
+
ts: string;
|
|
53
|
+
taskId: string;
|
|
54
|
+
event: AgentEvent;
|
|
55
|
+
} | {
|
|
56
|
+
kind: 'artifact';
|
|
57
|
+
ts: string;
|
|
58
|
+
taskId: string;
|
|
59
|
+
name: string;
|
|
60
|
+
contentType: string;
|
|
61
|
+
inline?: string;
|
|
62
|
+
blobRef?: BlobRef;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Finding F4: `approvalId` is populated whenever `TaskRunner` dispatched
|
|
66
|
+
* this approval THROUGH the normal `requestApproval`/`onApprovalDispatched`
|
|
67
|
+
* path (see `noteApprovalDispatched`) — `undefined` only for a
|
|
68
|
+
* hypothetical caller of `handleOutboundEnvelope` that never went through
|
|
69
|
+
* that hook (there is no other producer of `task.await_approval` today).
|
|
70
|
+
* This is the one piece of information an operator needs to actually
|
|
71
|
+
* call `approve`/`reject` (or the `approvals` CLI command) against this
|
|
72
|
+
* specific pending decision — before this fix, nothing surfaced it
|
|
73
|
+
* outside the control socket's `approvals.list`/`approvals.request`
|
|
74
|
+
* internals.
|
|
75
|
+
*/
|
|
76
|
+
| {
|
|
77
|
+
kind: 'awaiting-approval';
|
|
78
|
+
ts: string;
|
|
79
|
+
taskId: string;
|
|
80
|
+
summary: string;
|
|
81
|
+
approvalId?: string;
|
|
82
|
+
} | {
|
|
83
|
+
kind: 'completed';
|
|
84
|
+
ts: string;
|
|
85
|
+
taskId: string;
|
|
86
|
+
summary: string;
|
|
87
|
+
sessionRef: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Covers BOTH a post-claim `task.fail` and a pre-claim `task.decline` —
|
|
91
|
+
* `preClaim` distinguishes the two. This mirrors the protocol's own
|
|
92
|
+
* `Offered -> Failed` convention (docs/protocol.md "Declined vs. Failed";
|
|
93
|
+
* `TASK_TRANSITIONS`, `@byok-sdk/protocol`): a decline and a failure are the
|
|
94
|
+
* same outcome from a dispatcher's point of view, so this module doesn't
|
|
95
|
+
* invent a parallel `declined` kind the wire model itself doesn't have.
|
|
96
|
+
*/
|
|
97
|
+
| {
|
|
98
|
+
kind: 'failed';
|
|
99
|
+
ts: string;
|
|
100
|
+
taskId: string;
|
|
101
|
+
reason: string;
|
|
102
|
+
retryable: boolean;
|
|
103
|
+
preClaim?: boolean;
|
|
104
|
+
} | {
|
|
105
|
+
kind: 'cancelled';
|
|
106
|
+
ts: string;
|
|
107
|
+
taskId: string;
|
|
108
|
+
reason?: string;
|
|
109
|
+
} | {
|
|
110
|
+
kind: 'connection';
|
|
111
|
+
ts: string;
|
|
112
|
+
state: ConnectionState;
|
|
113
|
+
} | {
|
|
114
|
+
kind: 'paired';
|
|
115
|
+
ts: string;
|
|
116
|
+
deviceId: string;
|
|
117
|
+
} | {
|
|
118
|
+
kind: 'unpaired';
|
|
119
|
+
ts: string;
|
|
120
|
+
} | {
|
|
121
|
+
kind: 'runtimes-detected';
|
|
122
|
+
ts: string;
|
|
123
|
+
runtimes: RuntimeInfo[];
|
|
124
|
+
}
|
|
125
|
+
/** M4 Phase 2: the control socket's `shutdown` RPC was invoked — emitted once, before this daemon starts tearing itself down, so it lands in the audit log via the exact same subscribe->append plumbing every other event already uses (see `bin/commands/start.ts`). Informational only — do NOT gate any teardown decision on this event; see `shutdown-complete`. */
|
|
126
|
+
| {
|
|
127
|
+
kind: 'shutdown-requested';
|
|
128
|
+
ts: string;
|
|
129
|
+
reason: string;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* M4 Phase 2: emitted once, AFTER the control-socket-driven shutdown
|
|
133
|
+
* sequence has fully finished — active tasks reported failed over the
|
|
134
|
+
* (at that point still-open) connection, then the connection/control
|
|
135
|
+
* socket actually closed (see `create-daemon.ts`'s `performControlShutdown`,
|
|
136
|
+
* which calls this last). This is the event `bin/commands/start.ts` must
|
|
137
|
+
* wait for before treating the daemon as done: reacting to
|
|
138
|
+
* `shutdown-requested` instead would race `daemon.stop()` against the
|
|
139
|
+
* still-in-flight `task.fail` send and silently drop it (confirmed via a
|
|
140
|
+
* real regression — see `daemon-control-socket.test.ts`).
|
|
141
|
+
*
|
|
142
|
+
* Finding F5(b): `undeliveredOutboxCount`, when defined, is the honest
|
|
143
|
+
* post-drain read of `ConnectionManager.outboxLength()` — 0 means the
|
|
144
|
+
* bounded outbox drain (see `ConnectionManager.stop`) genuinely finished
|
|
145
|
+
* before the connection closed; a positive number means that many
|
|
146
|
+
* envelopes (almost certainly including a `task.fail`
|
|
147
|
+
* `shutdownActiveTasks` just enqueued) never actually left the outbox.
|
|
148
|
+
* `undefined` only for a hypothetical caller of `noteShutdownComplete`
|
|
149
|
+
* that didn't pass one — `create-daemon.ts`'s own `performControlShutdown`
|
|
150
|
+
* (the only real producer of this event) always does.
|
|
151
|
+
*/
|
|
152
|
+
| {
|
|
153
|
+
kind: 'shutdown-complete';
|
|
154
|
+
ts: string;
|
|
155
|
+
reason: string;
|
|
156
|
+
undeliveredOutboxCount?: number;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* M4 Phase 3 hardening: a wire `task.approve`/`task.reject` (or this
|
|
160
|
+
* device's own redelivered copy of one) arrived for an out-of-band
|
|
161
|
+
* approval a DIFFERENT, faster path (a racing local `approvals.resolve`
|
|
162
|
+
* over the control socket, or this exact decision arriving twice) had
|
|
163
|
+
* already resolved — `TaskRunner.handleApprove`/`handleReject`
|
|
164
|
+
* (`task-runner.ts`) emit this instead of failing the task a second time.
|
|
165
|
+
* Audit-only: never gates any teardown/task-state decision, purely a
|
|
166
|
+
* record that a stale message was seen and correctly ignored — see
|
|
167
|
+
* `NoPendingApprovalError`'s own doc comment (`task-runner.ts`) for the
|
|
168
|
+
* full race this closes.
|
|
169
|
+
*/
|
|
170
|
+
| {
|
|
171
|
+
kind: 'stale-approval-decision';
|
|
172
|
+
ts: string;
|
|
173
|
+
taskId: string;
|
|
174
|
+
decision: 'approve' | 'reject';
|
|
175
|
+
reason?: string;
|
|
176
|
+
} | {
|
|
177
|
+
kind: 'git-workspace';
|
|
178
|
+
ts: string;
|
|
179
|
+
taskId: string;
|
|
180
|
+
workspaceId: string;
|
|
181
|
+
phase: string;
|
|
182
|
+
headChanged?: boolean;
|
|
183
|
+
commitsSinceBaseline?: number;
|
|
184
|
+
dirty?: {
|
|
185
|
+
staged: number;
|
|
186
|
+
unstaged: number;
|
|
187
|
+
untracked: number;
|
|
188
|
+
conflicted: number;
|
|
189
|
+
};
|
|
190
|
+
errorCategory?: string;
|
|
191
|
+
};
|
|
192
|
+
export type DaemonEventListener = (event: DaemonEvent) => void;
|
|
193
|
+
export type Unsubscribe = () => void;
|
|
194
|
+
/** `daemon.tasks()`'s per-task view — current local state + whatever summary/outcome was last reported for it. */
|
|
195
|
+
export interface DaemonTaskInfo {
|
|
196
|
+
taskId: string;
|
|
197
|
+
state: TaskState;
|
|
198
|
+
runtime?: string;
|
|
199
|
+
/** Actual runtime selected for this task, as reported by the outbound `task.claim` payload. Distinct from `runtime`, which remains the requested offer runtime. */
|
|
200
|
+
claimedRuntime?: string;
|
|
201
|
+
/** Last known human-readable text for this task's current state: an await-approval summary, a complete summary, or a fail/cancel reason — whichever was most recently reported. */
|
|
202
|
+
summary?: string;
|
|
203
|
+
/** Only set once this task has actually reached `task.complete`. */
|
|
204
|
+
sessionRef?: string;
|
|
205
|
+
/** `true` only when `state === 'Failed'` resulted from a pre-claim `task.decline` rather than a post-claim `task.fail` — see the `DaemonEvent` `failed` variant's doc comment. */
|
|
206
|
+
declined?: boolean;
|
|
207
|
+
updatedAt: string;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* M3-B parity (see `task-runner.ts`'s `MAX_TRACKED_TASK_IDS` doc comment,
|
|
211
|
+
* same rationale): this daemon is meant to run as a long-lived background
|
|
212
|
+
* service, so a per-task registry that only ever grows is a slow memory
|
|
213
|
+
* leak. Only TERMINAL entries (`Complete`/`Failed`/`Cancelled`) are ever
|
|
214
|
+
* evicted, oldest first — active entries are never removed here, the same
|
|
215
|
+
* way `TaskRunner.tasks` itself is bounded by real concurrency rather than
|
|
216
|
+
* an explicit cap.
|
|
217
|
+
*/
|
|
218
|
+
export declare const MAX_TRACKED_TASKS = 2000;
|
|
219
|
+
/**
|
|
220
|
+
* Owns both halves of the local observability surface: the pub/sub
|
|
221
|
+
* (`subscribe`/`emit`) and the derived per-task registry (`tasks`). A plain
|
|
222
|
+
* `Set<listener>` with `subscribe` returning its own removal closure — not
|
|
223
|
+
* `node:events`' `EventEmitter` — keeps unsubscribe trivially leak-free
|
|
224
|
+
* (delete-by-reference, no string event names, no listener-count footguns)
|
|
225
|
+
* and keeps `DaemonEvent` a single typed union instead of a per-event-name
|
|
226
|
+
* overload table.
|
|
227
|
+
*/
|
|
228
|
+
export declare class DaemonObserver {
|
|
229
|
+
private readonly listeners;
|
|
230
|
+
private readonly taskInfo;
|
|
231
|
+
/**
|
|
232
|
+
* Finding F4: `approvalId` for a taskId's NEXT `task.await_approval`,
|
|
233
|
+
* stashed by `noteApprovalDispatched` and consumed (read + deleted) the
|
|
234
|
+
* moment `handleOutboundEnvelope`'s `task.await_approval` case actually
|
|
235
|
+
* emits the corresponding `awaiting-approval` event — see that hook's own
|
|
236
|
+
* doc comment for why this is always populated first (both calls happen
|
|
237
|
+
* synchronously, in that order, from `TaskRunner.dispatchApproval`).
|
|
238
|
+
* Read-and-delete keeps this self-bounding: an entry never outlives the
|
|
239
|
+
* one event it was stashed for, so this can never leak across a
|
|
240
|
+
* long-lived daemon's lifetime the way an evict-on-schedule cache would
|
|
241
|
+
* need explicit bookkeeping to avoid.
|
|
242
|
+
*/
|
|
243
|
+
private readonly pendingApprovalIdByTask;
|
|
244
|
+
subscribe(listener: DaemonEventListener): Unsubscribe;
|
|
245
|
+
/** Current locally-known tasks, in first-seen order. */
|
|
246
|
+
tasks(): DaemonTaskInfo[];
|
|
247
|
+
/**
|
|
248
|
+
* Feed a raw INBOUND (server -> daemon) envelope. Deliberately narrow: only
|
|
249
|
+
* `task.offer` produces a local event here — every other inbound type
|
|
250
|
+
* (`task.cancel`/`task.steer`/`task.approve`/`task.reject`) is a
|
|
251
|
+
* best-effort notification whose OWN observable effect already surfaces
|
|
252
|
+
* through the daemon's outbound envelopes (`task.cancelled`, `task.progress`
|
|
253
|
+
* resuming, `task.fail`, ...) — see `handleOutboundEnvelope`, which is
|
|
254
|
+
* where those are actually reported from.
|
|
255
|
+
*/
|
|
256
|
+
handleInboundEnvelope(envelope: Envelope): void;
|
|
257
|
+
/**
|
|
258
|
+
* Feed a raw OUTBOUND (daemon -> server) envelope — this is where every
|
|
259
|
+
* task-lifecycle local event actually comes from: `TaskRunner` already
|
|
260
|
+
* calls `deps.send(...)` for each of these at exactly the moment its own
|
|
261
|
+
* state machine decides the transition happened.
|
|
262
|
+
*/
|
|
263
|
+
handleOutboundEnvelope(envelope: Envelope): void;
|
|
264
|
+
noteConnectionState(state: ConnectionState): void;
|
|
265
|
+
notePaired(deviceId: string): void;
|
|
266
|
+
noteUnpaired(): void;
|
|
267
|
+
noteRuntimesDetected(runtimes: RuntimeInfo[]): void;
|
|
268
|
+
/** M4 Phase 2: see the `shutdown-requested` `DaemonEvent` variant's own doc comment. */
|
|
269
|
+
noteShutdownRequested(reason: string): void;
|
|
270
|
+
/** M4 Phase 2: see the `shutdown-complete` `DaemonEvent` variant's own doc comment (finding F5(b): `undeliveredOutboxCount`). */
|
|
271
|
+
noteShutdownComplete(reason: string, undeliveredOutboxCount?: number): void;
|
|
272
|
+
/** M4 Phase 3 hardening: see the `stale-approval-decision` `DaemonEvent` variant's own doc comment. */
|
|
273
|
+
noteStaleApprovalDecision(taskId: string, decision: 'approve' | 'reject', reason?: string): void;
|
|
274
|
+
noteGitWorkspace(event: {
|
|
275
|
+
taskId: string;
|
|
276
|
+
workspaceId: string;
|
|
277
|
+
phase: string;
|
|
278
|
+
headChanged?: boolean;
|
|
279
|
+
commitsSinceBaseline?: number;
|
|
280
|
+
dirty?: {
|
|
281
|
+
staged: number;
|
|
282
|
+
unstaged: number;
|
|
283
|
+
untracked: number;
|
|
284
|
+
conflicted: number;
|
|
285
|
+
};
|
|
286
|
+
errorCategory?: string;
|
|
287
|
+
}): void;
|
|
288
|
+
/**
|
|
289
|
+
* Finding F4: wired from `TaskRunnerDeps.onApprovalDispatched`, called
|
|
290
|
+
* synchronously by `TaskRunner.dispatchApproval` BEFORE its own
|
|
291
|
+
* `deps.send(createEnvelope('task.await_approval', ...))` — stashes
|
|
292
|
+
* `approvalId` so `handleOutboundEnvelope`'s `task.await_approval` case
|
|
293
|
+
* (triggered by that very `send` call) can attach it to the
|
|
294
|
+
* `awaiting-approval` `DaemonEvent` it emits. Never emits anything
|
|
295
|
+
* itself — purely a handoff, same as `upsertTask`'s bookkeeping role for
|
|
296
|
+
* other events.
|
|
297
|
+
*/
|
|
298
|
+
noteApprovalDispatched(taskId: string, approvalId: string): void;
|
|
299
|
+
private upsertTask;
|
|
300
|
+
/**
|
|
301
|
+
* Finding P2/#11 (observer half): the "no terminal entry to evict" branch
|
|
302
|
+
* used to just give up and leave the registry unbounded for as long as
|
|
303
|
+
* every tracked task stayed nonterminal (many concurrent/stuck offers that
|
|
304
|
+
* never reach Complete/Failed/Cancelled — the exact case this cap exists
|
|
305
|
+
* for, since a normal quickly-resolving workload always has terminal
|
|
306
|
+
* entries to evict well before this). Falling back to "never evict" here
|
|
307
|
+
* defeats the whole point of `MAX_TRACKED_TASKS`. Fix: fall back to
|
|
308
|
+
* evicting the OLDEST entry regardless of state (same insertion-order
|
|
309
|
+
* idiom as everywhere else this file/`task-runner.ts` bound a collection),
|
|
310
|
+
* logged since it's a real, if rare, observability loss — this registry is
|
|
311
|
+
* a local READ-MODEL only (see the module doc comment), never consulted by
|
|
312
|
+
* `TaskRunner`'s own state machine, so evicting a still-active task's
|
|
313
|
+
* entry here can't affect that task's actual execution — it only means
|
|
314
|
+
* `tasks()`/a CLI's task list can no longer show it until it reports
|
|
315
|
+
* another transition (which re-inserts it via `upsertTask`).
|
|
316
|
+
*/
|
|
317
|
+
private evictIfNeeded;
|
|
318
|
+
/**
|
|
319
|
+
* Listener errors are caught here so a broken subscriber (e.g. a CLI's
|
|
320
|
+
* rendering bug) can never propagate back into the real send/onEnvelope
|
|
321
|
+
* path this module wraps — see this file's own module doc comment.
|
|
322
|
+
*
|
|
323
|
+
* Finding #6: `DaemonEventListener` is typed `(event: DaemonEvent) =>
|
|
324
|
+
* void`, but TypeScript's structural typing does not stop a caller from
|
|
325
|
+
* subscribing an `async` function (or anything else returning a promise)
|
|
326
|
+
* where a void-returning callback is expected — nothing here ever
|
|
327
|
+
* validates that at runtime. The `try`/`catch` below only ever catches a
|
|
328
|
+
* SYNCHRONOUS throw; an async listener doesn't throw synchronously, it
|
|
329
|
+
* RETURNS an already-rejected (or later-rejecting) promise, which sails
|
|
330
|
+
* straight past that catch. Left unhandled, that promise's rejection
|
|
331
|
+
* becomes an `unhandledRejection` on the process — which, depending on
|
|
332
|
+
* the host's Node version/flags, can crash the entire daemon over one
|
|
333
|
+
* subscriber's bug (e.g. a CLI's own progress renderer awaiting something
|
|
334
|
+
* that throws). Fix: treat the listener's return value as "possibly a
|
|
335
|
+
* promise" regardless of its declared type, and attach a `.catch` so a
|
|
336
|
+
* later rejection is caught and logged exactly like a synchronous throw,
|
|
337
|
+
* never left to become unhandled. This call stays synchronous itself
|
|
338
|
+
* (never `await`s a listener) — `emit` is invoked from the send/cursor
|
|
339
|
+
* path (`handleOutboundEnvelope`/`handleInboundEnvelope`, wired in
|
|
340
|
+
* directly from `create-daemon.ts`'s `send`/`onEnvelope` closures) and
|
|
341
|
+
* must never make a subscriber's own async work a precondition for
|
|
342
|
+
* cursor advancement or the next envelope being processed.
|
|
343
|
+
*/
|
|
344
|
+
private emit;
|
|
345
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
export type OperationalHealthState = 'healthy' | 'degraded' | 'recovering';
|
|
3
|
+
export type OperationalFailureSource = 'reconnect' | 'upload' | 'maintenance' | 'lifecycle';
|
|
4
|
+
export interface OperationalCrashRecord {
|
|
5
|
+
detectedAt: string;
|
|
6
|
+
previousRunStartedAt: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const OPERATIONAL_HEALTH_FILENAME = "operational-health.json";
|
|
9
|
+
export declare const MAX_OPERATIONAL_HEALTH_FILE_BYTES: number;
|
|
10
|
+
export interface OpenOperationalHealthFile {
|
|
11
|
+
handle: Awaited<ReturnType<typeof fs.open>>;
|
|
12
|
+
stat: import('node:fs').BigIntStats;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Bind the pathname to a regular-file handle before any bytes are read.
|
|
16
|
+
* POSIX additionally gets O_NOFOLLOW/O_NONBLOCK. On Windows those flags are
|
|
17
|
+
* unavailable, so the pre-open lstat plus handle/path identity checks are the
|
|
18
|
+
* fail-closed authority: a static reparse point is rejected before open, and
|
|
19
|
+
* a raced replacement cannot match the already-observed inode/file state.
|
|
20
|
+
*/
|
|
21
|
+
export declare function openOperationalHealthFile(storeDir: string): Promise<OpenOperationalHealthFile | undefined>;
|
|
22
|
+
export type OperationalHealthFileInspection = {
|
|
23
|
+
status: 'missing';
|
|
24
|
+
} | {
|
|
25
|
+
status: 'valid';
|
|
26
|
+
sizeBytes: number;
|
|
27
|
+
state: OperationalHealthState;
|
|
28
|
+
failureCount: number;
|
|
29
|
+
crashCount: number;
|
|
30
|
+
currentRunStartedAt?: string;
|
|
31
|
+
lastCrashAt?: string;
|
|
32
|
+
} | {
|
|
33
|
+
status: 'corrupt';
|
|
34
|
+
sizeBytes: number;
|
|
35
|
+
reason: string;
|
|
36
|
+
} | {
|
|
37
|
+
status: 'unavailable';
|
|
38
|
+
sizeBytes?: number;
|
|
39
|
+
reason: string;
|
|
40
|
+
};
|
|
41
|
+
export type OperationalHealthSnapshot = {
|
|
42
|
+
availability: 'available';
|
|
43
|
+
state: OperationalHealthState;
|
|
44
|
+
failureCount: number;
|
|
45
|
+
windowMs: number;
|
|
46
|
+
failureThreshold: number;
|
|
47
|
+
crashCount: number;
|
|
48
|
+
lastCrashAt?: string;
|
|
49
|
+
currentRunStartedAt?: string;
|
|
50
|
+
} | {
|
|
51
|
+
availability: 'unavailable';
|
|
52
|
+
reason: string;
|
|
53
|
+
};
|
|
54
|
+
export interface OperationalHealthOptions {
|
|
55
|
+
windowMs?: number;
|
|
56
|
+
failureThreshold?: number;
|
|
57
|
+
maxFailures?: number;
|
|
58
|
+
maxCrashes?: number;
|
|
59
|
+
clock?: () => Date;
|
|
60
|
+
runId?: () => string;
|
|
61
|
+
pid?: number;
|
|
62
|
+
}
|
|
63
|
+
export declare class OperationalHealthTracker {
|
|
64
|
+
#private;
|
|
65
|
+
constructor(storeDir: string, options?: OperationalHealthOptions);
|
|
66
|
+
startRun(): Promise<OperationalHealthSnapshot>;
|
|
67
|
+
recordFailure(source: OperationalFailureSource): Promise<void>;
|
|
68
|
+
recordSuccess(_source: OperationalFailureSource): Promise<void>;
|
|
69
|
+
markCleanStop(): Promise<void>;
|
|
70
|
+
snapshot(): OperationalHealthSnapshot;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Read-only S7-b inspection seam. Unlike `OperationalHealthTracker.startRun`,
|
|
74
|
+
* this never writes a run marker, prunes history, or repairs malformed bytes;
|
|
75
|
+
* doctor can therefore report on an offline daemon without changing the very
|
|
76
|
+
* evidence it is inspecting.
|
|
77
|
+
*/
|
|
78
|
+
declare function inspectOperationalHealthHandle(handle: Awaited<ReturnType<typeof fs.open>>, expected?: import('node:fs').BigIntStats): Promise<Exclude<OperationalHealthFileInspection, {
|
|
79
|
+
status: 'missing';
|
|
80
|
+
}>>;
|
|
81
|
+
/**
|
|
82
|
+
* Open a pathname-bound regular-file handle before inspecting. POSIX uses
|
|
83
|
+
* no-follow/non-blocking flags; Windows uses the pre-open and post-open
|
|
84
|
+
* pathname/handle identity checks in openOperationalHealthFile.
|
|
85
|
+
*/
|
|
86
|
+
export declare function inspectOperationalHealthFile(storeDir: string): Promise<OperationalHealthFileInspection>;
|
|
87
|
+
export { inspectOperationalHealthHandle };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PermissionPolicy } from '@byok-sdk/protocol';
|
|
2
|
+
export interface PolicyDecision {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
/** The merged, effective policy. Only meaningful when `ok` is true. */
|
|
5
|
+
policy: PermissionPolicy;
|
|
6
|
+
/** Present when `ok` is false. */
|
|
7
|
+
reason?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Merge an offered task policy against the daemon operator's configured
|
|
11
|
+
* ceiling, fail-closed. This is a runtime-agnostic safety gate independent
|
|
12
|
+
* of whether the eventually-chosen adapter can even express the result — see
|
|
13
|
+
* each adapter's own permission mapping for that second, adapter-specific
|
|
14
|
+
* gate (e.g. `mapPermissionPolicyToPiArgs`).
|
|
15
|
+
*/
|
|
16
|
+
export declare function computeEffectivePolicy(offered: PermissionPolicy, ceiling: PermissionPolicy | undefined): PolicyDecision;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AgentEvent } from '@byok-sdk/protocol';
|
|
2
|
+
export type ProgressEmitter = (seq: number, events: AgentEvent[]) => void;
|
|
3
|
+
export interface ProgressBatcherOptions {
|
|
4
|
+
/** Flush immediately once this many events are buffered. Default 10. */
|
|
5
|
+
maxBatchSize?: number;
|
|
6
|
+
/** Otherwise flush at most this often (ms) while events are pending. Default 250 (~4/sec). */
|
|
7
|
+
flushIntervalMs?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Coalesces a task's `AgentEvent`s into seq-ordered `task.progress` batches:
|
|
11
|
+
* flush immediately at `maxBatchSize` events, otherwise at most every
|
|
12
|
+
* `flushIntervalMs` while anything is buffered. One instance per task —
|
|
13
|
+
* `seq` is a per-task monotonic counter starting at 1.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ProgressBatcher {
|
|
16
|
+
private readonly emit;
|
|
17
|
+
private buffer;
|
|
18
|
+
private seq;
|
|
19
|
+
private timer;
|
|
20
|
+
private readonly maxBatchSize;
|
|
21
|
+
private readonly flushIntervalMs;
|
|
22
|
+
constructor(emit: ProgressEmitter, options?: ProgressBatcherOptions);
|
|
23
|
+
push(event: AgentEvent): void;
|
|
24
|
+
/** M4 Phase 4 (part B.3, observability): events buffered right now, not yet flushed as a `task.progress` batch — a cheap per-task queue-depth watermark for the daemon's control-socket `status` result (see `task-runner.ts`'s `getQueueWatermarks`). */
|
|
25
|
+
get pendingCount(): number;
|
|
26
|
+
flush(): void;
|
|
27
|
+
/** Stop the pending flush timer without flushing (used on teardown). */
|
|
28
|
+
stop(): void;
|
|
29
|
+
private ensureTimer;
|
|
30
|
+
private clearTimer;
|
|
31
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RuntimeCapabilities as ProtocolRuntimeCapabilities } from '@byok-sdk/protocol';
|
|
2
|
+
import type { RuntimeCapabilities } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Maps a `RuntimeAdapter`'s own internal `capabilities()` result
|
|
5
|
+
* (`../types.ts`'s `RuntimeCapabilities` — `{steer, resume,
|
|
6
|
+
* approvalInteractive, permissionModes}`, always-required fields) onto the
|
|
7
|
+
* wire's `RuntimeCapabilities` shape (`@byok-sdk/protocol` — the same field names,
|
|
8
|
+
* but all-optional).
|
|
9
|
+
*
|
|
10
|
+
* Every field, `approvalInteractive` included, is a pure passthrough of the
|
|
11
|
+
* adapter's own self-report: the adapter is the single source of truth for
|
|
12
|
+
* what its runtime can do, and this function does no interpretation of its
|
|
13
|
+
* own. Previously `approvalInteractive` was hardcoded `false` at the sole
|
|
14
|
+
* caller, which had gone stale — as of M4 Phase 3 claude genuinely does
|
|
15
|
+
* support interactive approval, via `--permission-prompt-tool` routing
|
|
16
|
+
* `ClaudeSession.resolveApproval` into the local out-of-process MCP approval
|
|
17
|
+
* channel (`bin/byok-approval-mcp.ts`) under `policy.mode: 'confirm'` (see
|
|
18
|
+
* docs/protocol.md §5.1/§11.2) — so the wire advertised a capability claim no
|
|
19
|
+
* adapter owned. pi and codex report `false` for the same honest reason they
|
|
20
|
+
* always did: neither has any notion of pausing for approval
|
|
21
|
+
* (`resolveApproval()` throws unconditionally for both — see `../types.ts`'s
|
|
22
|
+
* `Session.resolveApproval` doc comment).
|
|
23
|
+
*
|
|
24
|
+
* This lives in its own module, rather than beside either of its two callers,
|
|
25
|
+
* because those two callers sit on opposite sides of an import edge:
|
|
26
|
+
* `create-daemon.ts` imports `task-runner.ts`, so exporting the mapper from
|
|
27
|
+
* `create-daemon.ts` would make `task-runner.ts` import back into it and close
|
|
28
|
+
* a cycle. A leaf module with no local imports beyond `../types` is the one
|
|
29
|
+
* place both can reach.
|
|
30
|
+
*
|
|
31
|
+
* The connection-level `interactive-approval` flag (`CAPABILITY_FLAGS`,
|
|
32
|
+
* `@byok-sdk/protocol`) is a separate, connection-scoped signal and is NOT derived
|
|
33
|
+
* from this: `create-daemon.ts`'s `computeCapabilities` keeps its own
|
|
34
|
+
* semantics.
|
|
35
|
+
*/
|
|
36
|
+
export declare function toRuntimeInfoCapabilities(caps: RuntimeCapabilities): ProtocolRuntimeCapabilities;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** What's recoverable for a given `sessionRef` — see the class doc comment. */
|
|
2
|
+
export interface SessionWorkspaceRecord {
|
|
3
|
+
workspaceDir: string;
|
|
4
|
+
/**
|
|
5
|
+
* The underlying runtime's own resumable session identifier.
|
|
6
|
+
*/
|
|
7
|
+
runtimeSessionId: string;
|
|
8
|
+
/** Additive workspace kind; missing legacy values remain plain. */
|
|
9
|
+
workspaceKind?: 'plain' | 'git';
|
|
10
|
+
/** Opaque private Git workspace ledger identifier. */
|
|
11
|
+
gitWorkspaceId?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Persists `sessionRef -> {workspaceDir, runtimeSessionId}` across daemon
|
|
15
|
+
* restarts (finding #3 from the 2026-07-16 live GLM run): a `task.offer`
|
|
16
|
+
* carrying a `sessionRef` this device has previously reported (via a prior
|
|
17
|
+
* task's `task.complete.sessionRef`) reuses that exact workspace directory
|
|
18
|
+
* as the new task's cwd — which is what lets a runtime adapter's own resume
|
|
19
|
+
* mechanism (e.g. pi's `--session <id>`, scoped to the cwd/project a session
|
|
20
|
+
* was created under — see pi-adapter.ts) actually find the session again.
|
|
21
|
+
* An unknown or absent `sessionRef` is simply not in this map, and
|
|
22
|
+
* `task-runner.ts` treats that identically to "no sessionRef was ever
|
|
23
|
+
* offered" — fresh workspace, fresh session.
|
|
24
|
+
*
|
|
25
|
+
* One JSON file under `storeDir`, mirroring `DeviceStore`/`CursorStore`'s
|
|
26
|
+
* own persistence style: always read/write straight through to disk, no
|
|
27
|
+
* in-memory cache that could go stale or need invalidating across multiple
|
|
28
|
+
* `TaskRunner`/daemon instances sharing the same `storeDir` (exactly the
|
|
29
|
+
* "map persisted across daemon restart" requirement).
|
|
30
|
+
*
|
|
31
|
+
* Two correctness properties `task-runner.ts` depends on, neither of which
|
|
32
|
+
* a bare `fs.writeFile` + independent `fs.readFile` calls actually gives
|
|
33
|
+
* you:
|
|
34
|
+
*
|
|
35
|
+
* 1. **No torn reads.** `record()` is deliberately fire-and-forgotten from
|
|
36
|
+
* `handleOffer` (see its call site) — it must never block `task.started`
|
|
37
|
+
* on a disk write. But a bare `fs.writeFile` is not atomic: it truncates
|
|
38
|
+
* the file before writing the new bytes, so a `get()` racing an
|
|
39
|
+
* in-flight `record()` on another task's offer can `JSON.parse` a
|
|
40
|
+
* half-written file, fail, and silently fall back to `{}` (see `load()`)
|
|
41
|
+
* — resolving what should have been a resume to a fresh workspace.
|
|
42
|
+
* `save()` below instead writes to a private temp file in the same
|
|
43
|
+
* directory and `fs.rename`s it onto the real path, which POSIX
|
|
44
|
+
* guarantees is atomic when both paths share a filesystem (true here —
|
|
45
|
+
* same directory): any concurrent reader sees either the fully-old or
|
|
46
|
+
* fully-new bytes, never a partial write.
|
|
47
|
+
* 2. **No lost updates.** Two `record()` calls for two different
|
|
48
|
+
* `sessionRef`s that overlap (both `load()` the same on-disk snapshot,
|
|
49
|
+
* each mutate their own key into their own in-memory copy, then `save()`
|
|
50
|
+
* back to back) would otherwise let the second `save()` overwrite the
|
|
51
|
+
* first's key with a snapshot that never saw it — each individual
|
|
52
|
+
* `save()` being atomic does not prevent this. `enqueue()` below chains
|
|
53
|
+
* every `get()`/`record()` through one serial promise queue per
|
|
54
|
+
* instance, so no two load-modify-save cycles (or a read and a
|
|
55
|
+
* concurrent write) ever interleave.
|
|
56
|
+
*/
|
|
57
|
+
export declare class SessionWorkspaceStore {
|
|
58
|
+
private readonly filePath;
|
|
59
|
+
/**
|
|
60
|
+
* Serial "mutex" queue: every `get()`/`record()` chains its work off this
|
|
61
|
+
* promise and replaces it, so operations on this instance always run
|
|
62
|
+
* one-at-a-time, in call order — never interleaved. The queue's own tail
|
|
63
|
+
* is never allowed to reject (a failed task must not wedge every
|
|
64
|
+
* subsequent caller behind a rejected promise); the failure still
|
|
65
|
+
* propagates to that call's own caller via the returned promise.
|
|
66
|
+
*/
|
|
67
|
+
private queue;
|
|
68
|
+
constructor(storeDir: string);
|
|
69
|
+
get(sessionRef: string): Promise<SessionWorkspaceRecord | undefined>;
|
|
70
|
+
record(sessionRef: string, entry: SessionWorkspaceRecord): Promise<void>;
|
|
71
|
+
/** See the class doc comment's "no lost updates" property. */
|
|
72
|
+
private enqueue;
|
|
73
|
+
private load;
|
|
74
|
+
private save;
|
|
75
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type EnsureSecureDirOptions } from '../util/secure-dir';
|
|
2
|
+
export interface DeviceRecord {
|
|
3
|
+
deviceId: string;
|
|
4
|
+
/** Current access token (JWT), renewed via challenge/token without re-pairing (protocol §6.2). */
|
|
5
|
+
accessToken: string;
|
|
6
|
+
/** ISO-8601 expiry for `accessToken` (our best knowledge of it — see auth-manager.ts for how this is derived after `/byok/pair`, which reports no explicit expiry itself). */
|
|
7
|
+
expiresAt: string;
|
|
8
|
+
/** Ed25519 private key, PKCS8 PEM. Never leaves this device. Stored as a 0600 file; OS keychain integration is deferred (tracked as a future roadmap item, not promised for any specific milestone). */
|
|
9
|
+
devicePrivateKeyPem: string;
|
|
10
|
+
/** Ed25519 public key, base64url — re-sent verbatim on a post-revocation re-pair (protocol §6.3). */
|
|
11
|
+
devicePublicKey: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Persists the device identity issued by `pair()` — deviceId, current
|
|
15
|
+
* access token + its expiry, and the device's own Ed25519 keypair. This is
|
|
16
|
+
* the ONLY credential material the daemon itself ever holds — never a
|
|
17
|
+
* runtime's own credentials (see the credential-isolation rule on
|
|
18
|
+
* `RuntimeAdapter`). Stored 0600 under `storeDir` (default
|
|
19
|
+
* `~/.byok/<productId>/`); OS keychain storage is deferred (tracked as a
|
|
20
|
+
* future roadmap item, not promised for any specific milestone).
|
|
21
|
+
*/
|
|
22
|
+
export declare class DeviceStore {
|
|
23
|
+
private readonly secureDirOptions?;
|
|
24
|
+
private readonly filePath;
|
|
25
|
+
/**
|
|
26
|
+
* `secureDirOptions` is a test-only DI seam (mirrors `EnsureSecureDirOptions`'s
|
|
27
|
+
* own `run`/`platform` overrides) — every real caller omits it, getting
|
|
28
|
+
* real `ensureSecureDir(storeDir)` behavior unchanged. It exists so
|
|
29
|
+
* finding R4's fail-closed contract ("on win32, an `icacls` failure makes
|
|
30
|
+
* `save()` — and thus `AuthManager.pair()` — reject with a clear typed
|
|
31
|
+
* `SecureDirHardeningError` instead of silently persisting an
|
|
32
|
+
* ACL-unprotected credential") is verifiable from a real `darwin`/`linux`
|
|
33
|
+
* CI/dev machine, not just asserted.
|
|
34
|
+
*/
|
|
35
|
+
constructor(storeDir: string, secureDirOptions?: EnsureSecureDirOptions | undefined);
|
|
36
|
+
static defaultDir(productId: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the one store pathname every daemon/CLI component must share.
|
|
39
|
+
* A configured relative path is anchored once at process entry rather than
|
|
40
|
+
* being reinterpreted after a diagnostics operation temporarily changes
|
|
41
|
+
* cwd to pin a quarantine directory inode.
|
|
42
|
+
*/
|
|
43
|
+
static resolveDir(productId: string, configured?: string): string;
|
|
44
|
+
load(): Promise<DeviceRecord | undefined>;
|
|
45
|
+
/**
|
|
46
|
+
* Read and remove the exact bounded, no-follow device record under the
|
|
47
|
+
* caller's mutation lease. The hard-link guard keeps the inspected inode
|
|
48
|
+
* identifiable until the synchronous pathname check and unlink complete.
|
|
49
|
+
*/
|
|
50
|
+
remove(): Promise<DeviceRecord | undefined>;
|
|
51
|
+
save(record: DeviceRecord): Promise<void>;
|
|
52
|
+
clear(): Promise<void>;
|
|
53
|
+
private openBounded;
|
|
54
|
+
}
|