@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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/dist/adapters/claude/claude-adapter.d.ts +155 -0
  4. package/dist/adapters/claude/events.d.ts +91 -0
  5. package/dist/adapters/claude/permission-mapping.d.ts +142 -0
  6. package/dist/adapters/claude/process-client.d.ts +103 -0
  7. package/dist/adapters/claude/resolve-approval-mcp-bin.d.ts +45 -0
  8. package/dist/adapters/claude/resolve-bin.d.ts +27 -0
  9. package/dist/adapters/codex/codex-adapter.d.ts +97 -0
  10. package/dist/adapters/codex/events.d.ts +41 -0
  11. package/dist/adapters/codex/permission-mapping.d.ts +107 -0
  12. package/dist/adapters/codex/process-runner.d.ts +86 -0
  13. package/dist/adapters/codex/resolve-bin.d.ts +25 -0
  14. package/dist/adapters/pi/events.d.ts +58 -0
  15. package/dist/adapters/pi/permission-mapping.d.ts +46 -0
  16. package/dist/adapters/pi/pi-adapter.d.ts +29 -0
  17. package/dist/adapters/pi/resolve-bin.d.ts +44 -0
  18. package/dist/adapters/pi/rpc-client.d.ts +101 -0
  19. package/dist/bin/approval-mcp-server.d.ts +69 -0
  20. package/dist/bin/audit-log.d.ts +212 -0
  21. package/dist/bin/byok-agent.d.ts +2 -0
  22. package/dist/bin/byok-agent.js +12347 -0
  23. package/dist/bin/byok-agent.js.map +1 -0
  24. package/dist/bin/byok-approval-mcp.d.ts +2 -0
  25. package/dist/bin/byok-approval-mcp.js +498 -0
  26. package/dist/bin/byok-approval-mcp.js.map +1 -0
  27. package/dist/bin/commands/approvals.d.ts +31 -0
  28. package/dist/bin/commands/approve-reject.d.ts +29 -0
  29. package/dist/bin/commands/doctor.d.ts +15 -0
  30. package/dist/bin/commands/pair.d.ts +7 -0
  31. package/dist/bin/commands/runtimes.d.ts +7 -0
  32. package/dist/bin/commands/service.d.ts +54 -0
  33. package/dist/bin/commands/start.d.ts +21 -0
  34. package/dist/bin/commands/status.d.ts +25 -0
  35. package/dist/bin/commands/support-bundle.d.ts +7 -0
  36. package/dist/bin/commands/tasks.d.ts +27 -0
  37. package/dist/bin/commands/unpair.d.ts +124 -0
  38. package/dist/bin/commands/workspaces.d.ts +16 -0
  39. package/dist/bin/config.d.ts +41 -0
  40. package/dist/bin/control-client.d.ts +50 -0
  41. package/dist/bin/format.d.ts +67 -0
  42. package/dist/bin/runtime-probe.d.ts +34 -0
  43. package/dist/bin/tasks-view.d.ts +49 -0
  44. package/dist/daemon/approvals.d.ts +87 -0
  45. package/dist/daemon/auth-manager.d.ts +52 -0
  46. package/dist/daemon/blob-client.d.ts +22 -0
  47. package/dist/daemon/connection-manager.d.ts +502 -0
  48. package/dist/daemon/control-protocol.d.ts +299 -0
  49. package/dist/daemon/control-server.d.ts +61 -0
  50. package/dist/daemon/create-daemon.d.ts +364 -0
  51. package/dist/daemon/cursor-store.d.ts +30 -0
  52. package/dist/daemon/daemon-owner.d.ts +24 -0
  53. package/dist/daemon/deterministic-jitter.d.ts +20 -0
  54. package/dist/daemon/device-keys.d.ts +45 -0
  55. package/dist/daemon/device-proof-signer.d.ts +41 -0
  56. package/dist/daemon/environment.d.ts +96 -0
  57. package/dist/daemon/git-workspace-store.d.ts +49 -0
  58. package/dist/daemon/git-workspace.d.ts +80 -0
  59. package/dist/daemon/http-client.d.ts +8 -0
  60. package/dist/daemon/journal/journal.d.ts +342 -0
  61. package/dist/daemon/journal/sqlite-journal.d.ts +173 -0
  62. package/dist/daemon/journal/sqlite-support.d.ts +96 -0
  63. package/dist/daemon/journal/storage-policy.d.ts +368 -0
  64. package/dist/daemon/long-poll-transport.d.ts +142 -0
  65. package/dist/daemon/observer.d.ts +345 -0
  66. package/dist/daemon/operational-health.d.ts +87 -0
  67. package/dist/daemon/policy.d.ts +16 -0
  68. package/dist/daemon/progress-batcher.d.ts +31 -0
  69. package/dist/daemon/runtime-capabilities.d.ts +36 -0
  70. package/dist/daemon/session-workspace-store.d.ts +75 -0
  71. package/dist/daemon/store.d.ts +54 -0
  72. package/dist/daemon/task-runner.d.ts +862 -0
  73. package/dist/daemon/truth-memory-client.d.ts +94 -0
  74. package/dist/daemon/url.d.ts +60 -0
  75. package/dist/daemon/ws-transport.d.ts +110 -0
  76. package/dist/diagnostics/diagnostics.d.ts +114 -0
  77. package/dist/diagnostics/support-bundle.d.ts +81 -0
  78. package/dist/index.d.ts +42 -0
  79. package/dist/index.js +10056 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/lifecycle/create-service-lifecycle.d.ts +59 -0
  82. package/dist/lifecycle/exec-runner.d.ts +141 -0
  83. package/dist/lifecycle/launchd.d.ts +55 -0
  84. package/dist/lifecycle/service-types.d.ts +177 -0
  85. package/dist/lifecycle/systemd.d.ts +46 -0
  86. package/dist/lifecycle/winsw.d.ts +64 -0
  87. package/dist/types.d.ts +178 -0
  88. package/dist/util/async-queue.d.ts +45 -0
  89. package/dist/util/atomic-write.d.ts +86 -0
  90. package/dist/util/secure-dir.d.ts +124 -0
  91. package/package.json +66 -0
@@ -0,0 +1,96 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ /** Every post-construction boundary crossed before a journal handle is returned to its owner. */
3
+ export type JournalOpenStep = 'after-open' | 'after-auto-vacuum' | 'after-wal' | 'after-foreign-keys' | 'after-synchronous' | 'after-header-read';
4
+ /**
5
+ * Test-only fault seam for proving that every post-open initialization failure
6
+ * closes the native handle before control returns to the journal constructor.
7
+ */
8
+ export interface JournalOpenFaultSeam {
9
+ onStep?(step: JournalOpenStep): void;
10
+ /** Test-only close override; production always calls `DatabaseSync.close()`. */
11
+ close?(db: DatabaseSync): void;
12
+ }
13
+ /**
14
+ * Initialization failed after SQLite returned a native handle, and closing
15
+ * that handle also failed. Callers must retain their cross-process ownership
16
+ * lease because the helper can no longer prove that no writer remains alive.
17
+ */
18
+ export declare class JournalHandleCleanupError extends Error {
19
+ readonly failures: readonly unknown[];
20
+ constructor(message: string, failures: readonly unknown[]);
21
+ }
22
+ /**
23
+ * Whether `nodeVersion` (a `major.minor.patch` string shaped like
24
+ * `process.versions.node`) is new enough for `node:sqlite` to exist at all.
25
+ * A version-string heuristic only — `node:sqlite` shipped in 22.5.0 behind
26
+ * `--experimental-sqlite` and became usable unflagged later, so a runtime
27
+ * passing this can still fail to load the module. {@link isSqliteAvailable}
28
+ * is the authoritative check; this one only exists to turn the common
29
+ * "Node too old" case into a specific message. Unparsable input returns
30
+ * `true` (the `require` below is the real gate; don't false-negative on a
31
+ * version shape this hasn't seen).
32
+ */
33
+ export declare function isSqliteCapableNodeVersion(nodeVersion: string): boolean;
34
+ /**
35
+ * Synchronously load `node:sqlite` via `createRequire` rather than a dynamic
36
+ * `import()`. `DatabaseSync`'s entire API is synchronous, and the journal is
37
+ * constructed synchronously from `createDaemonWithAdapters` alongside every
38
+ * other daemon-local store (`DeviceStore`, `CursorStore`, ...) — an async
39
+ * factory here would leak into every one of those call sites for nothing.
40
+ * `node:sqlite` is a built-in, so `require` resolves it even from this
41
+ * `"type": "module"` package. Memoized: the throw is re-derived per call from
42
+ * the version check rather than cached, which keeps the failure message
43
+ * accurate without keeping a rejected value around.
44
+ */
45
+ export declare function loadSqliteModule(): typeof import('node:sqlite');
46
+ /**
47
+ * Whether `node:sqlite` can ACTUALLY be loaded right now. This is what the
48
+ * journal's own test suites use for `describe.skipIf(!isSqliteAvailable())`
49
+ * (the same idiom `@byok-sdk/server`'s SQLite suites already use), and what
50
+ * `createDaemonWithAdapters` calls before constructing a hosted journal so
51
+ * the refusal is a typed, up-front error instead of a cryptic
52
+ * `Cannot find module 'node:sqlite'` from deep inside the first append.
53
+ */
54
+ export declare function isSqliteAvailable(): boolean;
55
+ /**
56
+ * Open (or create) the journal database at `path` with the durability
57
+ * settings architecture §12.7.2 pins, in the order they have to be applied:
58
+ *
59
+ * 1. `auto_vacuum = INCREMENTAL` — MUST precede table creation. `auto_vacuum`
60
+ * is only settable on a database with no schema yet; setting it afterward
61
+ * is silently ignored, and `compact()`'s `PRAGMA incremental_vacuum` would
62
+ * then be a no-op that reports success.
63
+ * 2. `journal_mode = WAL` — a reader and the single writer proceed without
64
+ * blocking each other, and it is what makes "drop the instance, reopen the
65
+ * same file" (the entire crash-matrix method) reliable.
66
+ * 3. `foreign_keys = ON` — the journal's tables are a real graph
67
+ * (transition -> task -> envelope); an orphan row here is corrupted
68
+ * recovery evidence, not a tolerable inconsistency.
69
+ * 4. `synchronous = FULL` — every ack-critical transaction fsyncs before it
70
+ * reports committed. This is THE setting the "durable append then cursor
71
+ * ack" ordering rests on; NORMAL would let the WAL commit land in the OS
72
+ * page cache, so the cursor could advance over an envelope a power cut
73
+ * then erases. Applied database-wide rather than per-transaction: the
74
+ * journal's whole reason to exist is ack-critical writes, and the
75
+ * maintenance paths that do not need it (checkpoint, incremental vacuum)
76
+ * are off the hot path anyway.
77
+ *
78
+ * `busyTimeoutMs` bounds how long a write waits on the lock before throwing
79
+ * `SQLITE_BUSY` — bounded on purpose, so contention surfaces as an error the
80
+ * caller sees rather than an unbounded stall on the envelope path.
81
+ *
82
+ * Throws whatever `node:sqlite` throws (module load failure, a corrupt file,
83
+ * an unreadable directory). Every caller is `SqliteLocalTaskJournal`'s
84
+ * constructor, which turns each of those into its own typed error — see
85
+ * `sqlite-journal.ts`.
86
+ */
87
+ export declare function openJournalDatabase(path: string, busyTimeoutMs: number, faults?: JournalOpenFaultSeam): DatabaseSync;
88
+ /**
89
+ * Restrict the journal database and its WAL/SHM siblings to owner-only
90
+ * read/write. The journal holds raw task envelopes (instructions, policy) with
91
+ * no other access-control layer of its own, so it must not be left at whatever
92
+ * the process umask would give it. Call AFTER the schema exists, so the
93
+ * WAL/SHM files SQLite creates lazily on first write are already there; a
94
+ * sibling that does not exist yet is skipped rather than treated as an error.
95
+ */
96
+ export declare function secureJournalFilePermissions(dbPath: string): void;
@@ -0,0 +1,368 @@
1
+ /**
2
+ * `LocalStoragePolicy`, the watermark state machine, and the classified GC
3
+ * engine (architecture §12.7.2.1).
4
+ *
5
+ * §12.7.2 gives the daemon a database it must not lose. This file is the other
6
+ * half of that promise: a machine that runs out of disk cannot commit an
7
+ * ack-critical transaction, and a daemon that keeps acking mailbox rows it
8
+ * cannot durably record is losing tasks. So local storage stops being an
9
+ * operational afterthought and becomes an admission-control input.
10
+ *
11
+ * Three shapes here are load-bearing:
12
+ *
13
+ * - **The state machine is §12.7.2.1's table, not a threshold check.** Four
14
+ * states with distinct *behaviours*: `normal` runs unhurried maintenance;
15
+ * `pressure` alerts and cleans only what can be rebuilt; `hard-pressure`
16
+ * stops taking NEW work while everything that finishes existing work keeps
17
+ * running; `emergency` refuses to ack at all. The difference between
18
+ * `hard-pressure` and `emergency` is the whole design: one declines offers
19
+ * (a task the dispatcher can place elsewhere), the other freezes the cursor
20
+ * (a task the mailbox will redeliver). Neither deletes anything to make room.
21
+ * - **Cleanup is ordered and bounded, and it cannot name protected data.**
22
+ * The order is §12.7.2.1's 1-5, verbatim. The categories are
23
+ * {@link CleanableCategory}, which has no member for an unacked envelope, a
24
+ * `Running` task, an unconfirmed terminal, a recovery-marked row, a user
25
+ * workspace, or quarantine evidence — so no amount of pressure can express
26
+ * deleting one. Under pressure the order is TRUNCATED to its rebuildable
27
+ * prefix rather than extended: being short of disk is the worst moment to
28
+ * start deleting durable records, and the two cheap categories are the ones
29
+ * that give space back immediately.
30
+ * - **Everything measurable is injected.** Usage comes from the journal, free
31
+ * space from a provider (`fs.statfs` in production), time from a clock, and
32
+ * the cadence from a caller-driven `tick()`. There is no wall-clock race
33
+ * anywhere in this file's behaviour, which is what lets the S3.4 disk-pressure
34
+ * matrix assert state transitions instead of waiting for them.
35
+ */
36
+ import type { CategoryUsage, CleanableCategory, CleanupCandidate, CleanupResult, CompactResult, LocalStorageUsage, LocalTaskJournal, StorageCategory } from './journal';
37
+ /** §12.7.2.1's recommended soft watermark: "达到约 80% budget". */
38
+ export declare const DEFAULT_SOFT_BUDGET_RATIO = 0.8;
39
+ /** §12.7.2.1's recommended hard watermark: "达到约 90% budget". */
40
+ export declare const DEFAULT_HARD_BUDGET_RATIO = 0.9;
41
+ /**
42
+ * Free bytes below which one ack-critical transaction can no longer be
43
+ * guaranteed, i.e. §12.7.2.1's `emergency` trigger. Sized for a WAL frame
44
+ * batch plus the checkpoint headroom a `synchronous=FULL` commit needs, not
45
+ * for a single row — a commit that cannot grow the WAL fails as surely as one
46
+ * that cannot grow the database.
47
+ */
48
+ export declare const DEFAULT_ACK_CRITICAL_RESERVE_BYTES: number;
49
+ /** Upper bound on how many candidates one cleanup pass may act on. Bounded so a pass cannot monopolise the journal's single writer. */
50
+ export declare const DEFAULT_CLEANUP_BATCH_LIMIT = 64;
51
+ /** Upper bound on freelist pages one compaction pass returns to the filesystem. */
52
+ export declare const DEFAULT_INCREMENTAL_VACUUM_PAGES = 64;
53
+ /** Unhurried maintenance cadence while `normal` — §12.7.2.1's "常规低频 GC/compaction". */
54
+ export declare const DEFAULT_NORMAL_COMPACTION_INTERVAL_MS: number;
55
+ /** The accelerated cadence §12.7.2.1 asks for at `pressure` and above ("加快 journal compaction"). */
56
+ export declare const DEFAULT_PRESSURE_COMPACTION_INTERVAL_MS: number;
57
+ /**
58
+ * Per-category retention, as milliseconds between something becoming garbage
59
+ * and becoming ELIGIBLE for automatic cleanup (§12.7.5).
60
+ *
61
+ * `confirmed-journal` is deliberately the longest: pruning a journal row also
62
+ * drops its idempotency receipt, so its retention MUST outlast the mailbox
63
+ * redelivery window, or a very late redelivery would re-append an envelope
64
+ * this device already finished. `orphan-artifact` carries §12.7.5's 24-hour
65
+ * grace period, which is the reference scan's safety margin, not a guess.
66
+ */
67
+ export declare const DEFAULT_RETENTION_MS: Readonly<Record<CleanableCategory, number>>;
68
+ /**
69
+ * Log rotation parameters (§12.7.2.1 lists them among a `LocalStoragePolicy`'s
70
+ * minimum contents).
71
+ *
72
+ * This SDK does not own the daemon's log writer — the host does. These are
73
+ * therefore the CONTRACT a host's rotator reads, and the reason they live here
74
+ * rather than in host config is that the rotated files they produce become
75
+ * `rotated-log` cleanup candidates governed by the same retention above. Two
76
+ * separate numbers for "when to rotate" and "when to delete" is exactly how a
77
+ * disk fills up with files nobody owns.
78
+ */
79
+ export interface LogRotationPolicy {
80
+ /** Rotate the active log once it exceeds this size. */
81
+ readonly maxFileBytes: number;
82
+ /** How many rotated generations to keep before the oldest becomes a `rotated-log` cleanup candidate. */
83
+ readonly keepFiles: number;
84
+ }
85
+ export declare const DEFAULT_LOG_ROTATION: LogRotationPolicy;
86
+ /** Bounded compaction scheduling — see {@link LocalStoragePressureEngine.tick}. */
87
+ export interface CompactionPolicy {
88
+ readonly incrementalVacuumPages: number;
89
+ readonly normalIntervalMs: number;
90
+ readonly pressureIntervalMs: number;
91
+ }
92
+ /**
93
+ * The host-injected policy, resolved and validated (§12.7.2.1: "由 host/daemon
94
+ * config 注入,至少包含 `maxStoreBytes`、`minFreeBytes`、soft/hard watermark、
95
+ * 各数据类别的 retention、workspace policy 与 log rotation").
96
+ */
97
+ export interface LocalStoragePolicy {
98
+ /** Total bytes this daemon's store directory may occupy. The budget the soft/hard ratios apply to. */
99
+ readonly maxStoreBytes: number;
100
+ /** Free bytes on the store's filesystem below which this device is at HARD pressure regardless of its own budget. */
101
+ readonly minFreeBytes: number;
102
+ /** Free bytes below which this device is at SOFT pressure. Defaults to twice {@link minFreeBytes}, so pressure engages one doubling before the floor. */
103
+ readonly softMinFreeBytes: number;
104
+ readonly softBudgetRatio: number;
105
+ readonly hardBudgetRatio: number;
106
+ readonly ackCriticalReserveBytes: number;
107
+ readonly retentionMs: Readonly<Record<CleanableCategory, number>>;
108
+ readonly logRotation: LogRotationPolicy;
109
+ readonly cleanupBatchLimit: number;
110
+ readonly compaction: CompactionPolicy;
111
+ }
112
+ /** What a host actually writes. Everything but the two budget numbers has a §12.7.2.1 default. */
113
+ export interface LocalStoragePolicyInput {
114
+ maxStoreBytes: number;
115
+ minFreeBytes: number;
116
+ softMinFreeBytes?: number;
117
+ softBudgetRatio?: number;
118
+ hardBudgetRatio?: number;
119
+ ackCriticalReserveBytes?: number;
120
+ retentionMs?: Partial<Record<CleanableCategory, number>>;
121
+ logRotation?: Partial<LogRotationPolicy>;
122
+ cleanupBatchLimit?: number;
123
+ compaction?: Partial<CompactionPolicy>;
124
+ }
125
+ /**
126
+ * Thrown when a storage policy is internally inconsistent or out of range.
127
+ *
128
+ * Rejected at CONSTRUCTION, before a daemon exists, for the same reason
129
+ * `JournalUnavailableError` is: a policy whose hard watermark sits below its
130
+ * soft one, or whose budget is zero, produces a daemon that behaves plausibly
131
+ * until the day it matters. There is no clamping and no "closest sensible
132
+ * value" here — a misconfigured durability policy is a configuration bug to
133
+ * fix, not a number to guess at.
134
+ */
135
+ export declare class LocalStoragePolicyError extends Error {
136
+ constructor(field: string, reason: string);
137
+ }
138
+ /**
139
+ * Thrown by {@link LocalStoragePressureEngine.assertAckCriticalAllowed} while
140
+ * the device is in `emergency`.
141
+ *
142
+ * Thrown from the daemon's envelope handler, BEFORE the journal append, which
143
+ * is what makes it §12.7.2.1's "fail-closed,不 ack 新 mailbox row": the
144
+ * handler rejects, so `ConnectionManager` records a stall instead of advancing
145
+ * the cursor, so the mailbox keeps the row and redelivers it. The task is not
146
+ * lost — it is left where it is still safe, which is the cloud.
147
+ */
148
+ export declare class LocalStorageEmergencyError extends Error {
149
+ constructor(reason: string);
150
+ }
151
+ /**
152
+ * Validate and fill in a host's policy. The ONE place a `LocalStoragePolicy`
153
+ * comes into existence, so every consumer downstream can treat its fields as
154
+ * already checked.
155
+ */
156
+ export declare function resolveLocalStoragePolicy(input: LocalStoragePolicyInput): LocalStoragePolicy;
157
+ /**
158
+ * When something that became garbage at `since` becomes eligible for automatic
159
+ * cleanup. The ONE place retention turns into a timestamp, so a producer
160
+ * calling `enqueueCleanupCandidate` and this engine consuming it agree by
161
+ * construction rather than by two matching constants.
162
+ */
163
+ export declare function cleanupEligibleAt(policy: LocalStoragePolicy, category: CleanableCategory, since: Date): string;
164
+ /**
165
+ * §12.7.2.1's four states, verbatim:
166
+ *
167
+ * | state | trigger | behaviour |
168
+ * | --- | --- | --- |
169
+ * | `normal` | below soft | unhurried GC/compaction |
170
+ * | `pressure` | ≥ soft budget, or free below the soft minimum | alert; clean only rebuildable/expired categories; accelerate compaction |
171
+ * | `hard-pressure` | ≥ hard budget, or free below the hard minimum | stop admitting new ordinary tasks; terminal/truth flush, delete, export and recovery all continue |
172
+ * | `emergency` | one ack-critical transaction can no longer be guaranteed | fail closed: do not ack new mailbox rows; preserve existing recovery evidence |
173
+ */
174
+ export type StoragePressureState = 'normal' | 'pressure' | 'hard-pressure' | 'emergency';
175
+ export interface StorageMeasurement {
176
+ readonly usage: LocalStorageUsage;
177
+ readonly freeBytes: number;
178
+ }
179
+ /**
180
+ * The state machine itself — a pure function of policy, measurement, and
181
+ * whether an ack-critical write has already been observed to fail.
182
+ *
183
+ * Evaluated worst-first: `emergency` is not "very bad pressure", it is a
184
+ * different claim (the next commit may not land), so it is decided before any
185
+ * budget arithmetic. `latchedFailure` exists because the cheapest evidence
186
+ * that a transaction cannot complete is one that already did not: a disk-full
187
+ * error from an ack-critical write is a fact, where free-space arithmetic is
188
+ * an estimate.
189
+ */
190
+ export declare function computePressureState(policy: LocalStoragePolicy, measurement: StorageMeasurement, latchedFailure?: string): StoragePressureState;
191
+ /**
192
+ * §12.7.2.1's cleanup order, 1-5:
193
+ *
194
+ * 1. expired upload/download temp files and rebuildable caches;
195
+ * 2. rotated logs past their retention;
196
+ * 3. journal rows for tasks whose terminal the cloud confirmed and that carry
197
+ * no recovery marker — **compact first, then batch delete**;
198
+ * 4. host-marked ephemeral workspaces for tasks already terminal;
199
+ * 5. orphan artifacts, after a reference scan plus grace period.
200
+ *
201
+ * Under pressure the order is TRUNCATED to steps 1-2, not extended. Steps 3-5
202
+ * touch durable records, need a compaction or a reference scan first, and give
203
+ * their space back slowly; steps 1-2 are pure rebuildable garbage and give it
204
+ * back immediately. Deleting durable evidence is exactly the wrong reflex when
205
+ * the disk is nearly full, so it stays on the unhurried `normal` cadence where
206
+ * a mistake is recoverable.
207
+ */
208
+ export declare function cleanupOrderFor(state: StoragePressureState): readonly CleanableCategory[];
209
+ /** What a {@link CleanupExecutor} did with one candidate. Mirrors {@link CleanupResult} minus the bookkeeping the engine fills in. */
210
+ export interface CleanupExecution {
211
+ readonly outcome: 'deleted' | 'skipped' | 'failed';
212
+ readonly bytesReclaimed?: number;
213
+ readonly error?: string;
214
+ }
215
+ /**
216
+ * Performs one candidate's actual deletion.
217
+ *
218
+ * A seam rather than a method because the journal owns metadata and the
219
+ * filesystem owns bytes, and the crash window BETWEEN them (delete the file,
220
+ * die before marking; mark, die before deleting) is S3.4 point 12. Keeping
221
+ * them separate is what lets that window be tested at all.
222
+ *
223
+ * It receives a {@link CleanupCandidate}, whose `category` is a
224
+ * {@link CleanableCategory} — so an executor cannot be handed protected data
225
+ * even by a caller trying to.
226
+ */
227
+ export type CleanupExecutor = (candidate: CleanupCandidate) => Promise<CleanupExecution>;
228
+ /** `confirmed-journal` candidates address a journal task, not a path. This prefix is that distinction, spelled out. */
229
+ export declare const JOURNAL_TASK_REF_PREFIX = "task:";
230
+ /**
231
+ * The default cleanup worker: filesystem removal for the four path-addressed
232
+ * categories, and a delegated journal prune for `confirmed-journal`.
233
+ *
234
+ * Two deliberate behaviours:
235
+ *
236
+ * - **A missing path is `deleted`, not `failed`.** That is S3.4 point 12's
237
+ * first order (file gone, metadata not yet marked) converging on retry: the
238
+ * retry finds nothing to do and says so, rather than looping forever on a
239
+ * candidate whose work is already done.
240
+ * - **A relative `ref` is refused.** Cleanup resolves paths against nothing —
241
+ * a relative ref would delete whatever the daemon's cwd happens to make it,
242
+ * which is not a bug worth having once.
243
+ */
244
+ export declare function createFilesystemCleanupExecutor(options?: {
245
+ readonly pruneJournalTask?: (taskId: string) => Promise<boolean>;
246
+ }): CleanupExecutor;
247
+ /** What the status surface renders, and what a `tick()` hands back. */
248
+ export interface StorageStatusSnapshot {
249
+ readonly state: StoragePressureState;
250
+ readonly measuredAt: string;
251
+ readonly budgetBytes: number;
252
+ readonly usedBytes: number;
253
+ readonly freeBytes: number;
254
+ readonly categories: Readonly<Record<StorageCategory, CategoryUsage>>;
255
+ readonly lastCompaction?: CompactResult & {
256
+ readonly at: string;
257
+ };
258
+ }
259
+ export type StoragePressureEvent =
260
+ /** §12.7.2.1's "发出告警" — emitted on every transition, including back down to `normal`. */
261
+ {
262
+ readonly kind: 'state-changed';
263
+ readonly from: StoragePressureState;
264
+ readonly to: StoragePressureState;
265
+ readonly snapshot: StorageStatusSnapshot;
266
+ } | {
267
+ readonly kind: 'cleanup';
268
+ readonly result: CleanupResult;
269
+ readonly category: CleanableCategory;
270
+ } | {
271
+ readonly kind: 'compaction';
272
+ readonly result: CompactResult;
273
+ };
274
+ export interface StorageTickResult {
275
+ readonly state: StoragePressureState;
276
+ readonly snapshot: StorageStatusSnapshot;
277
+ readonly cleaned: readonly CleanupResult[];
278
+ readonly compaction?: CompactResult;
279
+ }
280
+ export interface TimerLike {
281
+ setInterval(handler: () => void, ms: number): unknown;
282
+ clearInterval(handle: unknown): void;
283
+ }
284
+ export interface LocalStoragePressureEngineOptions {
285
+ readonly policy: LocalStoragePolicy | LocalStoragePolicyInput;
286
+ readonly journal: LocalTaskJournal;
287
+ /** Free bytes on the store's filesystem. `fs.statfs` in production (see {@link createStatfsFreeBytesProvider}); a fixed number under test. */
288
+ readonly freeBytesProvider: () => number | Promise<number>;
289
+ /** Performs deletions. Defaults to {@link createFilesystemCleanupExecutor}. */
290
+ readonly executor?: CleanupExecutor;
291
+ readonly clock?: () => Date;
292
+ readonly onEvent?: (event: StoragePressureEvent) => void;
293
+ /** Reports only the scheduler's own maintenance pass outcome; never includes task-domain failures. */
294
+ readonly onMaintenanceOutcome?: (outcome: 'success' | 'failure') => void;
295
+ /** Injected so `start()`'s periodic driver is substitutable; the matrix never uses it and drives {@link LocalStoragePressureEngine.tick} directly. */
296
+ readonly timers?: TimerLike;
297
+ }
298
+ /**
299
+ * Drives §12.7.2.1: measure, decide the state, clean in order, compact within
300
+ * bounds — and answer the two questions the rest of the daemon asks
301
+ * ({@link admissionGuard}, {@link assertAckCriticalAllowed}).
302
+ *
303
+ * The cadence is a caller-driven {@link tick}. `start()` merely arranges for
304
+ * something to call it periodically, so a host with its own scheduler can skip
305
+ * it entirely, and the disk-pressure matrix can advance state deterministically
306
+ * with no timer at all. Nothing here runs on the envelope or task hot path:
307
+ * the only two things the hot path calls are the two synchronous questions,
308
+ * both of which read a field.
309
+ */
310
+ export declare class LocalStoragePressureEngine {
311
+ #private;
312
+ readonly policy: LocalStoragePolicy;
313
+ constructor(options: LocalStoragePressureEngineOptions);
314
+ get state(): StoragePressureState;
315
+ /** The last measured snapshot, or `undefined` before the first {@link tick}. */
316
+ snapshot(): StorageStatusSnapshot | undefined;
317
+ /**
318
+ * §12.7.2.1's hard-pressure row, as a `TaskRunnerDeps.admissionGuard`:
319
+ * "停止接收新的普通 task;仍允许 terminal/truth flush、删除、导出、doctor 与
320
+ * 恢复操作".
321
+ *
322
+ * The decline is RETRYABLE because pressure is a property of this device at
323
+ * this moment — a dispatcher re-routing the task to another device genuinely
324
+ * helps, and the same device may take it in a minute. Nothing in this method
325
+ * touches the disk; it reads the state the last tick computed, so an offer
326
+ * arriving between ticks is answered instantly.
327
+ */
328
+ admissionGuard(): {
329
+ readonly admit: true;
330
+ } | {
331
+ readonly admit: false;
332
+ readonly reason: string;
333
+ readonly retryable: boolean;
334
+ };
335
+ /** Throws {@link LocalStorageEmergencyError} while in `emergency`. Called immediately before every ack-critical journal append. */
336
+ assertAckCriticalAllowed(): void;
337
+ /**
338
+ * Latch `emergency` from an ack-critical write that ACTUALLY failed.
339
+ *
340
+ * A commit that returned `SQLITE_FULL` is stronger evidence than any free-space
341
+ * estimate, and it must not be forgotten on the next tick just because the
342
+ * arithmetic happens to look survivable. The latch clears only when a tick
343
+ * measures a genuinely `normal` device — not `pressure`, not `hard-pressure`:
344
+ * coming back from "the disk refused a write" requires actual headroom, not a
345
+ * borderline reading.
346
+ */
347
+ noteAckCriticalFailure(reason: string): void;
348
+ /**
349
+ * One maintenance pass: measure, transition, clean in order, compact if due.
350
+ *
351
+ * Ordered this way on purpose — the cleanup pass acts on the state THIS
352
+ * measurement produced, so a device that just crossed into pressure cleans on
353
+ * the same tick it alerts, rather than one cadence later.
354
+ */
355
+ tick(): Promise<StorageTickResult>;
356
+ /** Begin periodic ticking. A host with its own scheduler need never call this. */
357
+ start(): void;
358
+ stop(): Promise<void>;
359
+ }
360
+ /**
361
+ * Production free-space provider: bytes available to an unprivileged process
362
+ * on the filesystem holding `dir`.
363
+ *
364
+ * `bavail`, not `bfree` — the reserved blocks `bfree` includes are not space
365
+ * this daemon can write into, and treating them as headroom is how a device
366
+ * discovers it is out of disk at commit time instead of at measurement time.
367
+ */
368
+ export declare function createStatfsFreeBytesProvider(dir: string): () => Promise<number>;
@@ -0,0 +1,142 @@
1
+ import { type Envelope } from '@byok-sdk/protocol';
2
+ import { AuthManager } from './auth-manager';
3
+ export interface LongPollClientOptions {
4
+ serverUrl: string;
5
+ auth: AuthManager;
6
+ getCursor: () => number | undefined;
7
+ onEnvelope: (envelope: Envelope) => void;
8
+ /** Called once the device is found to be revoked (401 surfaced through {@link AuthManager}) — the loop stops itself rather than retrying. */
9
+ onRevoked?: () => void;
10
+ /**
11
+ * M4 Phase 4 (version-negotiation drill fix), scope narrowed by finding F1:
12
+ * called ONLY for a batch entry that failed to parse because its `type`
13
+ * is entirely unrecognized (`parseMessage` throwing
14
+ * {@link UnknownMessageTypeError} — mirrors `ws-transport.ts`'s identical
15
+ * per-frame tolerance for that SPECIFIC failure) and which still carries a
16
+ * numeric envelope-level `seq` AND a recognizably task-class `type` (a
17
+ * `task.` prefix — see `extractSkippableSeq`'s own doc comment for why a
18
+ * `conn.*`-shaped or type-less entry is deliberately excluded, mirroring
19
+ * F2's "conn.* is never cursor-tracked" rule), so the caller can advance
20
+ * its cursor/watermark past it even though there is no real `Envelope` to
21
+ * hand to `onEnvelope`. Without this, a persistently-redelivered
22
+ * unrecognized-type entry (the real server retains and redelivers an
23
+ * un-acked envelope, protocol §9) would keep reappearing at the same
24
+ * cursor position forever.
25
+ *
26
+ * Finding F1: a RECOGNIZED type that fails schema validation
27
+ * ({@link EnvelopeValidationError} — e.g. a `task.offer` whose
28
+ * `PermissionPolicy` rejects an unknown constraint) is deliberately NOT
29
+ * reported here. That failure is a genuinely malformed control message,
30
+ * not forward-compat tolerance — forwarding its `seq` here would
31
+ * permanently ack a message the daemon never actually understood (the
32
+ * server would stop redelivering it, silently stranding whatever it was
33
+ * offering). The WS path never had this hazard (an unparseable WS frame
34
+ * has no skip-side cursor bookkeeping at all — see
35
+ * `ws-transport.ts` — so it simply gets redelivered later); this callback
36
+ * being scoped to `UnknownMessageTypeError` only is what makes long-poll
37
+ * match that same "no silent permanent ack" property for real. Optional
38
+ * only for constructor/test convenience — `ConnectionManager` always
39
+ * supplies it.
40
+ */
41
+ onSkippedSeq?: (seq: number) => void;
42
+ /**
43
+ * Finding R1 (cross-model re-review — the F1 fix alone was NOT-CLOSED):
44
+ * called for a batch entry whose `type` WAS recognized but whose payload
45
+ * failed schema validation ({@link EnvelopeValidationError}) — a genuine
46
+ * delivery failure at that specific seq, not forward-compat tolerance
47
+ * (contrast {@link onSkippedSeq}, which is scoped to the opposite case,
48
+ * an entirely unrecognized type). F1's own fix — simply not forwarding
49
+ * this seq to `onSkippedSeq` — turned out to be insufficient on its own:
50
+ * a LATER valid envelope in the same or a later batch would still
51
+ * silently advance the durable cursor PAST this seq once its own handler
52
+ * succeeded, since nothing had told `ConnectionManager` this seq needed
53
+ * the same stall treatment a thrown handler failure already gets — an
54
+ * INDIRECT permanent ack, one hop removed from the exact bug F1 set out
55
+ * to fix. `ConnectionManager` (`noteValidationFailure`) engages
56
+ * `stalledAtSeq` for this seq the same way `process()`'s own catch block
57
+ * does for a real thrown handler — freezing `dedupWatermark()` at the
58
+ * durable cursor (so the server's retain-and-redeliver semantics,
59
+ * protocol §9, keep this seq alive) and, via that SAME existing
60
+ * machinery, holding back the cursor for anything else delivered after it
61
+ * in the same batch too, exactly as a real handler failure already would.
62
+ * Optional only for constructor/test convenience — `ConnectionManager`
63
+ * always supplies it.
64
+ */
65
+ onValidationFailedSeq?: (seq: number) => void;
66
+ /**
67
+ * Finding P2 (Fix 2a): true while a `task.*` envelope's handler has failed
68
+ * and hasn't yet been successfully reprocessed
69
+ * (`ConnectionManager.stalledAtSeq`). While true, `getCursor()` stays
70
+ * frozen below the actual delivery watermark (see
71
+ * `ConnectionManager.dedupWatermark`'s own doc comment) — so a non-empty
72
+ * response here doesn't mean "new events arrived", it can just as well
73
+ * mean "the whole post-cursor backlog got re-pulled again with no
74
+ * progress". Without a backoff for that case (distinct from "zero
75
+ * events"), a persistently-failing handler made this loop spin at RTT
76
+ * against the server. Optional only for constructor/test convenience —
77
+ * `ConnectionManager` always supplies it.
78
+ */
79
+ isStalled?: () => boolean;
80
+ /** Backoff between failed poll attempts (network/HTTP errors), AND between cycles that made no cursor progress while stalled (finding P2/Fix 2a — see {@link isStalled}). The reference server holds each successful, non-stalled request open ~50s itself (protocol §8), so this only matters when a request errors outright or is stalled. Default 2s. */
81
+ retryDelayMs?: number;
82
+ /** Deterministic delay authority for automatic failed/stalled cycles. */
83
+ retryDelayForAttempt?: (attempt: number, baseDelayMs: number) => number;
84
+ onOperationalOutcome?: (outcome: 'success' | 'failure') => void;
85
+ /**
86
+ * Minimum delay before the next request when a poll comes back with zero
87
+ * events. The reference server holds each request open ~50s waiting for
88
+ * something to happen, which throttles the loop for free; a server that
89
+ * (like this SDK's own test stub) responds immediately instead would
90
+ * otherwise make this a tight busy-loop. Default 250ms.
91
+ */
92
+ idleDelayMs?: number;
93
+ }
94
+ /**
95
+ * Protocol §8 long-poll fallback: `GET /byok/events?cursor=N` in a loop,
96
+ * used while WS connectivity is unavailable (see `ConnectionManager`), plus
97
+ * `POST /byok/messages` for the daemon's own outbound envelopes while in
98
+ * this mode (finding F6 — long-poll is a full transport, not receive-only:
99
+ * see docs/protocol.md §8).
100
+ *
101
+ * Design B (finding N4): this is a stateless drainer, symmetric with
102
+ * `WsTransport.sendNow` — it holds no outbound queue of its own.
103
+ * `ConnectionManager` owns the single shared outbox both transports drain
104
+ * from (so a transport switch never strands a queued envelope);
105
+ * `postBatch` is a single POST attempt, reporting back whether the server
106
+ * accepted it. All retry/backoff policy (and re-checking which transport is
107
+ * currently active) lives in the caller (`ConnectionManager.drainOutbox`).
108
+ */
109
+ export declare class LongPollClient {
110
+ private readonly opts;
111
+ private running;
112
+ /**
113
+ * Finding R1: seqs this loop has already `console.warn`'d about for a
114
+ * validation-failed (recognized-type, invalid-payload) entry — a poison
115
+ * entry is redelivered on every poll cycle for as long as it stalls the
116
+ * cursor (protocol §9), so without this the SAME warning would repeat
117
+ * every ~poll-interval, forever, for one persistently-malformed message.
118
+ * Never cleared: once a seq is fixed (a corrected redelivery is
119
+ * processed), the server never redelivers that seq again, so there is
120
+ * nothing left to re-warn about for it either. Soft-capped — this is a
121
+ * pathological/rare path (unlike a per-task hot structure), so on the
122
+ * rare chance a connection somehow accumulates an unreasonable number of
123
+ * distinct poisoned seqs, this is simply cleared outright (accepting a
124
+ * handful of possible re-warnings) rather than carrying any per-entry
125
+ * eviction bookkeeping for a case this unlikely.
126
+ */
127
+ private readonly warnedValidationFailureSeqs;
128
+ constructor(opts: LongPollClientOptions);
129
+ start(): void;
130
+ stop(): void;
131
+ /**
132
+ * POST one batch of envelopes to `/byok/messages` (finding F6/protocol
133
+ * §8.2) — a single attempt, no internal retry loop. Every envelope in
134
+ * `envelopes` is routed through the server's single inbound gate
135
+ * (`ConnectionHub.handleInbound`), so a resend of the SAME batch (same
136
+ * envelope `id`s — the caller must never rebuild them) is deduped
137
+ * server-side into a safe no-op rather than reprocessed (§9). Returns
138
+ * `true` once the server has accepted the batch.
139
+ */
140
+ postBatch(envelopes: Envelope[]): Promise<boolean>;
141
+ private loop;
142
+ }