@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,49 @@
1
+ import type { GitErrorCategory, GitWorkspaceObservation } from './git-workspace';
2
+ export type GitWorkspacePhase = 'preparing' | 'active' | 'completed' | 'failed' | 'cancelled' | 'interrupted' | 'salvage';
3
+ export interface GitWorkspaceLedgerRecord {
4
+ workspaceId: string;
5
+ taskId: string;
6
+ workspaceDir: string;
7
+ sessionRef?: string;
8
+ phase: GitWorkspacePhase;
9
+ baseline?: string;
10
+ current?: string;
11
+ commitsSinceBaseline: number;
12
+ staged: number;
13
+ unstaged: number;
14
+ untracked: number;
15
+ conflicted: number;
16
+ createdAt: string;
17
+ updatedAt: string;
18
+ errorCategory?: GitErrorCategory;
19
+ }
20
+ export interface GitWorkspaceLedger {
21
+ version: 1;
22
+ records: GitWorkspaceLedgerRecord[];
23
+ }
24
+ /** Private, versioned, serialized recovery ledger for local Git workspaces. */
25
+ export declare class GitWorkspaceStore {
26
+ readonly storeDir: string;
27
+ readonly filePath: string;
28
+ private queue;
29
+ private readonly maxRecords;
30
+ constructor(storeDir: string, options?: {
31
+ maxRecords?: number;
32
+ });
33
+ initialize(): Promise<void>;
34
+ list(): Promise<GitWorkspaceLedgerRecord[]>;
35
+ get(workspaceId: string): Promise<GitWorkspaceLedgerRecord | undefined>;
36
+ findBySession(sessionRef: string): Promise<GitWorkspaceLedgerRecord | undefined>;
37
+ findBySessionAnyPhase(sessionRef: string): Promise<GitWorkspaceLedgerRecord | undefined>;
38
+ attachSession(workspaceId: string, sessionRef: string): Promise<void>;
39
+ upsert(record: GitWorkspaceLedgerRecord): Promise<void>;
40
+ updateObservation(workspaceId: string, observation: GitWorkspaceObservation, phase?: GitWorkspacePhase, errorCategory?: GitErrorCategory): Promise<void>;
41
+ /** Marks old preparation/active records interrupted without reviving protocol tasks. */
42
+ reconcile(validate?: (record: GitWorkspaceLedgerRecord) => Promise<boolean>): Promise<void>;
43
+ private prune;
44
+ private enqueue;
45
+ private load;
46
+ private save;
47
+ }
48
+ export declare const GIT_WORKSPACE_LEDGER_FILE = "git-workspaces.json";
49
+ export declare const GIT_WORKSPACE_LEDGER_MAX_RECORDS = 500;
@@ -0,0 +1,80 @@
1
+ export interface GitWorkspaceConfig {
2
+ mode: 'local-checkpoints';
3
+ }
4
+ export type GitErrorCategory = 'git-unavailable' | 'git-timeout' | 'git-output-limit' | 'git-command-failed' | 'workspace-root-invalid' | 'workspace-root-conflict' | 'workspace-not-owned' | 'repository-root-mismatch' | 'repository-invalid' | 'lease-busy' | 'ledger-invalid';
5
+ export declare class GitWorkspaceError extends Error {
6
+ readonly category: GitErrorCategory;
7
+ constructor(category: GitErrorCategory, message?: string);
8
+ }
9
+ export interface GitCommandResult {
10
+ code: number;
11
+ stdout: string;
12
+ stderr: string;
13
+ }
14
+ export interface GitCommandOptions {
15
+ cwd?: string;
16
+ timeout?: number;
17
+ maxBuffer?: number;
18
+ env?: NodeJS.ProcessEnv;
19
+ }
20
+ export type GitRunner = (args: readonly string[], options?: GitCommandOptions) => Promise<GitCommandResult>;
21
+ declare const DEFAULT_TIMEOUT_MS = 5000;
22
+ declare const DEFAULT_MAX_OUTPUT_BYTES: number;
23
+ export declare function stableGitWorkspaceOwnerId(storeDir: string, productId: string): string;
24
+ /** A bounded, no-shell runner for the small Git command allowlist. */
25
+ export declare const defaultGitRunner: GitRunner;
26
+ export interface GitWorkspaceOptions {
27
+ run?: GitRunner;
28
+ timeoutMs?: number;
29
+ maxOutputBytes?: number;
30
+ platform?: NodeJS.Platform;
31
+ ownerId?: string;
32
+ }
33
+ export interface GitWorkspaceObservation {
34
+ workspaceDir: string;
35
+ head?: string;
36
+ baseline?: string;
37
+ headChanged: boolean;
38
+ commitsSinceBaseline: number;
39
+ staged: number;
40
+ unstaged: number;
41
+ untracked: number;
42
+ conflicted: number;
43
+ }
44
+ export interface GitWorkspaceLease {
45
+ readonly workspaceDir: string;
46
+ readonly sessionRef?: string;
47
+ release(): void;
48
+ }
49
+ /** Local Git checkpoint manager. It never invokes a shell or mutating Git command other than init. */
50
+ export declare class GitWorkspaceManager {
51
+ readonly workspaceRoot: string;
52
+ readonly ownerId: string;
53
+ private readonly run;
54
+ private readonly timeoutMs;
55
+ private readonly maxOutputBytes;
56
+ private readonly platform;
57
+ constructor(workspaceRoot: string, options?: GitWorkspaceOptions);
58
+ static validateConfig(value: unknown): GitWorkspaceConfig | undefined;
59
+ preflight(): Promise<void>;
60
+ ensureOwnerMarker(): Promise<void>;
61
+ prepareFresh(workspaceDir: string): Promise<GitWorkspaceObservation>;
62
+ /** Validate an already prepared repository without creating directories or running mutating Git commands. */
63
+ validateExisting(workspaceDir: string): Promise<GitWorkspaceObservation>;
64
+ observe(workspaceDir: string, baseline?: string): Promise<GitWorkspaceObservation>;
65
+ acquireLease(workspaceDir: string, sessionRef?: string): Promise<GitWorkspaceLease>;
66
+ static guidance(): string;
67
+ static prependGuidance(instruction: string): string;
68
+ private commandOptions;
69
+ private read;
70
+ private readTopLevel;
71
+ private assertTaskRoot;
72
+ private assertExistingAncestry;
73
+ private assertExistingTaskRoot;
74
+ }
75
+ export declare const GIT_WORKSPACE_OWNER_MARKER = ".byok-git-workspace-owner.json";
76
+ export declare const LOCAL_GIT_WORKSPACE_GUIDANCE: string;
77
+ export declare function prependGitWorkspaceGuidance(instruction: string): string;
78
+ export declare function isGitWorkspaceConfig(value: unknown): value is GitWorkspaceConfig;
79
+ export declare function canonicalWorkspaceRoot(value: string): Promise<string>;
80
+ export { DEFAULT_MAX_OUTPUT_BYTES as GIT_WORKSPACE_MAX_OUTPUT_BYTES, DEFAULT_TIMEOUT_MS as GIT_WORKSPACE_TIMEOUT_MS };
@@ -0,0 +1,8 @@
1
+ import type { AuthManager } from './auth-manager';
2
+ /**
3
+ * `fetch` with the current device bearer token attached. On a 401, renews
4
+ * the token once (reactive renewal, protocol §6.2) and retries exactly
5
+ * once — never loops. A second 401 (or {@link AuthManager.handleUnauthorized}
6
+ * throwing `DeviceRevokedError`) propagates to the caller.
7
+ */
8
+ export declare function authedFetch(url: string | URL, init: RequestInit, auth: AuthManager): Promise<Response>;
@@ -0,0 +1,342 @@
1
+ /**
2
+ * The daemon's durable local journal port (sprint S3.3 / architecture
3
+ * §12.7.2).
4
+ *
5
+ * The journal is NOT a debug log. It is the correctness precondition for
6
+ * acking the hosted mailbox: the cloud retires a mailbox row when the daemon's
7
+ * next poll carries a cursor past it (§12.7.3 — "领走即弃" means *cursor moved
8
+ * past*, not *read*), so the moment the cursor advances over an envelope, that
9
+ * envelope exists nowhere but this machine. Everything in this file exists to
10
+ * make "the bytes are durable" a fact the cursor can be gated on.
11
+ *
12
+ * Two shapes here are load-bearing and deliberate:
13
+ *
14
+ * - **Bounded records.** The journal stores reliability metadata and small
15
+ * bounded bytes. Prompts, tool output, artifacts, workspaces, and blobs stay
16
+ * on the filesystem and are referenced, never inlined (§12.7.2). An oversized
17
+ * record is rejected with {@link JournalRecordTooLargeError} rather than
18
+ * silently truncated — a truncated envelope is not the envelope, and a
19
+ * journal that quietly stores something else than what arrived is worse than
20
+ * one that refuses.
21
+ * - **Cleanable categories are a closed set that excludes protected data.**
22
+ * §12.7.2.1's never-auto-delete list (unacked envelopes, Running/
23
+ * AwaitApproval tasks, unconfirmed terminals, anything carrying a recovery
24
+ * marker, user workspaces, provider secrets, quarantine evidence) is
25
+ * enforced by {@link CleanableCategory} not having names for those things,
26
+ * so the cleanup path cannot ask for one. A runtime filter would put the
27
+ * never-delete list one bug away from deleting recovery evidence exactly
28
+ * once, in production.
29
+ */
30
+ /**
31
+ * The journal's one hash function, shared by every producer so a stored digest
32
+ * and a recomputed one are comparable without a second convention. `sha256:`
33
+ * prefixed because the algorithm has to travel with the value — an unprefixed
34
+ * hex string is a hash of unknown provenance the day this changes.
35
+ */
36
+ export declare function journalHash(bytes: string): string;
37
+ /**
38
+ * Who this daemon is, as recorded on every journal row (§12.7.2's minimum
39
+ * fact set, first three entries). Carried explicitly rather than captured at
40
+ * construction because `deviceId` is only known after `AuthManager`
41
+ * `loadExisting()` resolves, strictly after the journal is built.
42
+ */
43
+ export interface JournalIdentity {
44
+ readonly tenantId: string;
45
+ readonly productId: string;
46
+ readonly deviceId: string;
47
+ }
48
+ /**
49
+ * One inbound envelope, as received from the mailbox and about to be made
50
+ * durable. `bytes` is the canonical v1 encoding (`encodeEnvelope`) of the
51
+ * envelope this daemon parsed — the frozen codec's round-trip guarantee is
52
+ * what makes that a faithful record — and `bytesHash` is its digest, so a
53
+ * later reader can tell a redelivery of the same envelope from a different
54
+ * envelope reusing an id.
55
+ */
56
+ export interface ReceivedEnvelopeRecord {
57
+ readonly identity: JournalIdentity;
58
+ readonly envelopeId: string;
59
+ /** The envelope's `task_id`, when it carries one. Required whenever {@link opensTask} is true. */
60
+ readonly taskId?: string;
61
+ /** The mailbox `seq` this envelope arrived at — the received cursor (§12.7.2's minimum fact set). */
62
+ readonly seq: number;
63
+ /** Canonical v1 bytes. Bounded — see {@link JournalRecordTooLargeError}. */
64
+ readonly bytes: string;
65
+ /** `sha256:<hex>` over {@link bytes}. */
66
+ readonly bytesHash: string;
67
+ readonly receivedAt: string;
68
+ /**
69
+ * Whether this envelope OPENS a local task record, i.e. whether the same
70
+ * transaction must also create the `journal_task` row §12.7.3 requires
71
+ * ("v1 bytes 与本机 task record 已在同一个本机 transaction durable append").
72
+ * The caller decides this from the envelope's own type — it is the one
73
+ * party that already knows, and re-deriving it inside the journal would put
74
+ * a second copy of protocol semantics somewhere it does not belong.
75
+ */
76
+ readonly opensTask: boolean;
77
+ }
78
+ /**
79
+ * What {@link LocalTaskJournal.appendEnvelope} hands back. The cursor may
80
+ * advance past {@link seq} once — and only once — this exists.
81
+ */
82
+ export interface JournalReceipt {
83
+ readonly envelopeId: string;
84
+ readonly seq: number;
85
+ readonly bytesHash: string;
86
+ readonly committedAt: string;
87
+ /**
88
+ * `false` when this exact envelope id was ALREADY durable and this call
89
+ * changed nothing — the redelivery case (§8.3's at-least-once wire means
90
+ * every envelope can arrive twice; a crash between commit and ack
91
+ * guarantees at least one will). A `false` here is a successful, correct
92
+ * outcome, not an error: it says the bytes are durable, which is all the
93
+ * cursor needed to know.
94
+ */
95
+ readonly created: boolean;
96
+ }
97
+ /** The admission decision for an offered task, as recorded on `journal_task`. */
98
+ export interface AdmissionRecord {
99
+ readonly taskId: string;
100
+ readonly admitted: boolean;
101
+ readonly reason?: string;
102
+ readonly retryable?: boolean;
103
+ readonly claimedRuntime?: string;
104
+ /** Digest of the effective policy this task runs under — the policy itself is not stored (bounded records). */
105
+ readonly effectivePolicyHash?: string;
106
+ /** Filesystem reference to the task's workspace. A path, never its contents. */
107
+ readonly workspaceRef?: string;
108
+ readonly decidedAt: string;
109
+ }
110
+ /**
111
+ * One local execution-state transition. Idempotent by {@link transitionId}:
112
+ * a replayed transition (recovery re-running a step, a redelivered envelope
113
+ * driving the same move) records nothing new.
114
+ */
115
+ export interface LocalTransitionRecord {
116
+ readonly transitionId: string;
117
+ readonly taskId: string;
118
+ readonly from?: string;
119
+ readonly to: string;
120
+ readonly occurredAt: string;
121
+ /** Small bounded free-form note. Not a place for tool output. */
122
+ readonly detail?: string;
123
+ }
124
+ /** Whether the cloud has confirmed the terminal this daemon produced (§12.7.3's "terminal 生成后、truth 写入前" window). */
125
+ export type TerminalTruthState = 'pending' | 'confirmed' | 'failed';
126
+ /**
127
+ * A task's terminal, as it exists locally. The PAYLOAD is not stored — only
128
+ * its hash, plus enough retry state to know whether the cloud has taken it.
129
+ */
130
+ export interface LocalTerminalRecord {
131
+ readonly taskId: string;
132
+ readonly terminalType: 'complete' | 'failed' | 'cancelled';
133
+ readonly payloadHash: string;
134
+ readonly truthState: TerminalTruthState;
135
+ /** How many times delivery to the cloud has been attempted. */
136
+ readonly attempt: number;
137
+ readonly lastError?: string;
138
+ readonly recordedAt: string;
139
+ }
140
+ /** A task the journal knows about that has no terminal and no recovery marker — i.e. one this daemon was in the middle of when it stopped. */
141
+ export interface RecoverableTask {
142
+ readonly taskId: string;
143
+ readonly envelopeId: string;
144
+ readonly seq: number;
145
+ readonly identity: JournalIdentity;
146
+ readonly localState: string;
147
+ readonly claimedRuntime?: string;
148
+ readonly workspaceRef?: string;
149
+ readonly updatedAt: string;
150
+ }
151
+ /**
152
+ * What recovery decided about a task. `interrupted` is the honest default for
153
+ * a daemon restart: local runtime sessions do not survive the process, so the
154
+ * task is recorded as interrupted rather than pretended back into life — the
155
+ * same semantics `GitWorkspaceStore.reconcile()` already applies to a lease
156
+ * whose owner is gone. Nothing here deletes anything; a row that has been
157
+ * through recovery carries a marker, and §12.7.2.1 forbids auto-deleting
158
+ * those.
159
+ */
160
+ export type RecoveryDisposition = 'resumed' | 'interrupted' | 'abandoned';
161
+ export interface RecoveryOutcome {
162
+ readonly disposition: RecoveryDisposition;
163
+ readonly reason?: string;
164
+ readonly occurredAt?: string;
165
+ }
166
+ /**
167
+ * The five storage categories §12.7.2.1 requires to be measured SEPARATELY —
168
+ * the cleanup order and the never-delete list are both category-scoped, so a
169
+ * single "bytes used" number cannot drive either.
170
+ */
171
+ export type StorageCategory = 'journal' | 'cache' | 'log' | 'workspace' | 'quarantine';
172
+ export interface CategoryUsage {
173
+ readonly bytes: number;
174
+ /** `true` when this is a host-reported or sampled figure rather than a measured one. */
175
+ readonly approximate: boolean;
176
+ }
177
+ export interface LocalStorageUsage {
178
+ readonly measuredAt: string;
179
+ readonly totalBytes: number;
180
+ readonly categories: Readonly<Record<StorageCategory, CategoryUsage>>;
181
+ }
182
+ /**
183
+ * The categories automatic cleanup may act on, in §12.7.2.1's order. This
184
+ * union is the never-auto-delete list's enforcement: `quarantine`,
185
+ * unacked envelopes, live tasks, unconfirmed terminals, recovery-marked rows,
186
+ * and user-designated workspaces have NO name here, so no cleanup call can
187
+ * name one. Adding a member is therefore a deliberate, reviewable act rather
188
+ * than a filter that silently stops matching.
189
+ */
190
+ export type CleanableCategory =
191
+ /** Expired upload/download temp files and rebuildable caches. */
192
+ 'expired-temp'
193
+ /** Rotated logs past their retention. */
194
+ | 'rotated-log'
195
+ /** Journal rows for tasks whose terminal the cloud has confirmed and that carry no recovery marker. */
196
+ | 'confirmed-journal'
197
+ /** Generated workspaces the HOST explicitly marked ephemeral, for tasks already terminal. */
198
+ | 'ephemeral-workspace'
199
+ /** Local artifacts with no remaining reference, after a reference scan plus grace period. */
200
+ | 'orphan-artifact';
201
+ export interface CleanupCandidate {
202
+ readonly candidateId: string;
203
+ readonly category: CleanableCategory;
204
+ /** What to act on — a filesystem path, or a journal key. Interpreted by the cleanup worker, never by the journal. */
205
+ readonly ref: string;
206
+ readonly eligibleAt: string;
207
+ readonly reason: string;
208
+ readonly attempts: number;
209
+ readonly lastError?: string;
210
+ }
211
+ export interface CleanupResult {
212
+ readonly candidateId: string;
213
+ readonly outcome: 'deleted' | 'skipped' | 'failed';
214
+ readonly bytesReclaimed?: number;
215
+ readonly error?: string;
216
+ readonly at: string;
217
+ }
218
+ export interface CompactOptions {
219
+ /**
220
+ * `passive` never blocks a concurrent reader/writer and may checkpoint
221
+ * nothing; `truncate` waits for the WAL to be fully applied and resets it.
222
+ * Both are maintenance calls and belong off the active-task hot path
223
+ * (§12.7.2).
224
+ */
225
+ readonly checkpoint?: 'passive' | 'truncate';
226
+ /** Upper bound on freelist pages returned to the filesystem this pass. Bounded so compaction cannot monopolise the single writer. */
227
+ readonly incrementalVacuumPages?: number;
228
+ }
229
+ export interface CompactResult {
230
+ readonly checkpointed: boolean;
231
+ /** WAL frames still outstanding after the checkpoint attempt — `0` means the WAL was fully applied. */
232
+ readonly walFramesRemaining: number;
233
+ readonly pagesVacuumed: number;
234
+ readonly durationMs: number;
235
+ }
236
+ /**
237
+ * The daemon-local durable journal (sprint S3.3's minimum API, verbatim,
238
+ * plus {@link close}).
239
+ *
240
+ * A port, not an implementation detail: architecture §12.7.2 lets a host
241
+ * inject its own backend, but only one that meets the same durability
242
+ * contract. `SqliteLocalTaskJournal` is the shipped production implementation;
243
+ * a plain JSON/JSONL store is explicitly NOT an acceptable substitute in
244
+ * hosted mode, because it cannot honour the one property this interface exists
245
+ * for — that `appendEnvelope` resolving means the bytes survived a power cut.
246
+ */
247
+ export interface LocalTaskJournal {
248
+ /**
249
+ * Make one inbound envelope durable. Resolving means committed and fsynced;
250
+ * only then may the mailbox cursor advance past `record.seq`.
251
+ *
252
+ * Idempotent by envelope id: appending the same envelope twice returns the
253
+ * original receipt with `created: false` and writes nothing new. That is
254
+ * what makes redelivery — which the at-least-once wire guarantees will
255
+ * happen after any crash between commit and ack — a no-op rather than a
256
+ * second side effect.
257
+ */
258
+ appendEnvelope(record: ReceivedEnvelopeRecord): Promise<JournalReceipt>;
259
+ /** Record the admission decision for a task whose offer envelope is already durable. */
260
+ recordAdmission(record: AdmissionRecord): Promise<void>;
261
+ /** Record one local execution-state transition. Idempotent by transition id. */
262
+ recordTransition(record: LocalTransitionRecord): Promise<void>;
263
+ /** Record (or update the retry state of) a task's terminal. Idempotent by task id: a replay with the same payload hash is a no-op beyond retry bookkeeping. */
264
+ recordTerminal(record: LocalTerminalRecord): Promise<void>;
265
+ /** Tasks with no terminal and no recovery marker — what this daemon was in the middle of when it last stopped. */
266
+ listRecoverable(): Promise<RecoverableTask[]>;
267
+ /** Close out one recoverable task by writing its recovery marker. Never deletes; a marked row is on §12.7.2.1's never-auto-delete list. */
268
+ markRecovered(taskId: string, outcome: RecoveryOutcome): Promise<void>;
269
+ /** Per-category storage usage (§12.7.2.1) — the input to the watermark state machine. */
270
+ measureUsage(): Promise<LocalStorageUsage>;
271
+ /** Cleanup candidates eligible at `now`, oldest first, at most `limit`. Only {@link CleanableCategory} members can ever appear. */
272
+ listCleanupCandidates(now: Date, limit: number): Promise<CleanupCandidate[]>;
273
+ /** Record what the cleanup worker actually did with a candidate. */
274
+ markCleanupResult(result: CleanupResult): Promise<void>;
275
+ /** Bounded WAL checkpoint + incremental vacuum. Maintenance only; never on the envelope path. */
276
+ compact(options: CompactOptions): Promise<CompactResult>;
277
+ /**
278
+ * Release the underlying handle. Beyond S3.3's minimum API — the daemon
279
+ * owns this object's lifetime and has to be able to end it, and the crash
280
+ * matrix needs an explicit "clean shutdown" to contrast against dropping
281
+ * the reference without one.
282
+ */
283
+ close(): Promise<void>;
284
+ }
285
+ /**
286
+ * Thrown when hosted journal mode is configured on a runtime that cannot
287
+ * provide a real SQLite backend.
288
+ *
289
+ * This is architecture §12.7.2's no-silent-downgrade rule made executable:
290
+ * "为兼容 Node 20 而退回的普通文件实现不能冒充 production durability." A daemon
291
+ * that acks a mailbox on the strength of a journal that does not fsync loses
292
+ * tasks only under power-cut timings — it passes every happy-path test it will
293
+ * ever be given. Refusing to start is the only honest behaviour, so this is
294
+ * thrown from CONSTRUCTION, before a single envelope has been accepted.
295
+ */
296
+ export declare class JournalUnavailableError extends Error {
297
+ constructor(reason: string, options?: {
298
+ cause?: unknown;
299
+ });
300
+ }
301
+ /**
302
+ * Thrown when the journal database is unreadable and has been moved aside.
303
+ *
304
+ * The database is renamed (with its WAL/SHM siblings) into
305
+ * `<storeDir>/quarantine/`, timestamped, alongside a manifest naming the
306
+ * reason — and then this is thrown. Nothing is deleted and nothing is
307
+ * silently rebuilt: a fresh empty database opened over a corrupt one would
308
+ * report "no recoverable tasks" for a machine that has some, and destroy the
309
+ * only evidence of why. §12.7.2.1 puts quarantine on the never-auto-delete
310
+ * list; clearing it is an explicit operator action.
311
+ */
312
+ export declare class JournalCorruptError extends Error {
313
+ readonly dbPath: string;
314
+ readonly quarantinePath: string;
315
+ constructor(dbPath: string, quarantinePath: string, reason: string, options?: {
316
+ cause?: unknown;
317
+ });
318
+ }
319
+ /**
320
+ * Thrown when a record exceeds the journal's per-record byte bound.
321
+ *
322
+ * The journal holds reliability metadata and small bounded bytes; large
323
+ * payloads live on the filesystem and are referenced (§12.7.2). Rejecting is
324
+ * the point — truncating would make the stored envelope a different envelope
325
+ * than the one acked, which is precisely the failure this whole subsystem
326
+ * exists to rule out.
327
+ */
328
+ export declare class JournalRecordTooLargeError extends Error {
329
+ readonly field: string;
330
+ readonly actualBytes: number;
331
+ readonly limitBytes: number;
332
+ constructor(field: string, actualBytes: number, limitBytes: number);
333
+ }
334
+ /** Thrown when a record references a task the journal has no row for — a transition or terminal for a task whose offer envelope was never appended. Fail-closed: the alternative is inventing the missing task row, which would fabricate recovery evidence. */
335
+ export declare class JournalUnknownTaskError extends Error {
336
+ readonly taskId: string;
337
+ constructor(taskId: string, operation: string);
338
+ }
339
+ /** Thrown when the journal is used after {@link LocalTaskJournal.close}. */
340
+ export declare class JournalClosedError extends Error {
341
+ constructor(operation: string);
342
+ }
@@ -0,0 +1,173 @@
1
+ import { type AdmissionRecord, type CategoryUsage, type CleanableCategory, type CleanupCandidate, type CleanupResult, type CompactOptions, type CompactResult, type JournalReceipt, type LocalStorageUsage, type LocalTaskJournal, type LocalTerminalRecord, type LocalTransitionRecord, type RecoverableTask, type RecoveryOutcome, type ReceivedEnvelopeRecord, type StorageCategory } from './journal';
2
+ import { JournalHandleCleanupError, type JournalOpenFaultSeam } from './sqlite-support';
3
+ export { JournalHandleCleanupError };
4
+ /** The single database file, per §12.7.2's "建议单库 `<storeDir>/daemon.db`". */
5
+ export declare const JOURNAL_DB_FILENAME = "daemon.db";
6
+ /** Where a database that failed to open is moved to. On §12.7.2.1's never-auto-delete list. */
7
+ export declare const JOURNAL_QUARANTINE_DIRNAME = "quarantine";
8
+ /**
9
+ * Default per-record byte bound. Generous next to a real `task.offer` (an
10
+ * instruction plus a policy object — kilobytes), tight enough that an
11
+ * envelope carrying an inlined artifact is refused instead of turning the
12
+ * journal into a blob store. See {@link JournalRecordTooLargeError}.
13
+ */
14
+ export declare const DEFAULT_MAX_RECORD_BYTES: number;
15
+ /** Default bound on how long a write waits for the write lock before failing. */
16
+ export declare const DEFAULT_JOURNAL_BUSY_TIMEOUT_MS = 5000;
17
+ /**
18
+ * Named points inside a write where {@link JournalFaultSeam} may throw.
19
+ *
20
+ * These exist so the crash matrix can put a failure at an exact ordering
21
+ * boundary — "after the envelope row, before the task row", "after the receipt,
22
+ * before the commit" — deterministically, with no wall clock and no real
23
+ * process kill. Same DI shape as `EnsureSecureDirOptions.run`
24
+ * (`util/secure-dir.ts`): a seam the production path never supplies, exercised
25
+ * from any host.
26
+ */
27
+ export type JournalFaultStep = 'append:before-begin' | 'append:after-envelope' | 'append:after-task' | 'append:after-receipt' | 'append:before-commit' | 'admission:before-commit' | 'transition:before-commit' | 'terminal:before-commit' | 'recovery:before-commit' | 'cleanup:before-commit' | 'prune:before-commit';
28
+ export interface JournalFaultSeam {
29
+ /** Throw to simulate a crash or IO error at exactly this step. Return normally to proceed. */
30
+ onStep?(step: JournalFaultStep): void;
31
+ }
32
+ export interface SqliteLocalTaskJournalOptions {
33
+ /** The daemon's store directory. The database lives at `<storeDir>/daemon.db` and quarantine at `<storeDir>/quarantine/`. */
34
+ readonly storeDir: string;
35
+ /** Bound on waiting for the write lock. Default {@link DEFAULT_JOURNAL_BUSY_TIMEOUT_MS}. */
36
+ readonly busyTimeoutMs?: number;
37
+ /** Per-record byte bound. Default {@link DEFAULT_MAX_RECORD_BYTES}. */
38
+ readonly maxRecordBytes?: number;
39
+ /** Test seam — see {@link JournalFaultSeam}. Never supplied in production. */
40
+ readonly faults?: JournalFaultSeam;
41
+ /** Test seam for post-open/pre-return SQLite initialization failures. Never supplied in production. */
42
+ readonly openFaults?: JournalOpenFaultSeam;
43
+ /** Injected clock, so receipt/quarantine timestamps are deterministic under test. Defaults to the real one. */
44
+ readonly clock?: () => Date;
45
+ }
46
+ /** The categories `measureUsage` reports. Host-reported ones default to zero-approximate rather than being guessed at. */
47
+ declare const HOST_REPORTED_CATEGORIES: readonly StorageCategory[];
48
+ export declare class SqliteLocalTaskJournal implements LocalTaskJournal {
49
+ #private;
50
+ constructor(options: SqliteLocalTaskJournalOptions);
51
+ /**
52
+ * The ack-critical path. Envelope bytes, the task record it opens, and the
53
+ * idempotency receipt all land in ONE transaction, or none of them do.
54
+ *
55
+ * The receipt lookup happens INSIDE that transaction rather than as a cheap
56
+ * pre-check outside it: `BEGIN IMMEDIATE` already holds the write lock by
57
+ * then, so "is this a redelivery?" and "write it" cannot be separated by
58
+ * another writer. The duplicate answer is served from the stored receipt,
59
+ * so a redelivery returns the ORIGINAL commit time and hash — the caller
60
+ * learns the bytes are durable and that this call changed nothing, which is
61
+ * exactly what the cursor needed.
62
+ */
63
+ appendEnvelope(record: ReceivedEnvelopeRecord): Promise<JournalReceipt>;
64
+ recordAdmission(record: AdmissionRecord): Promise<void>;
65
+ /**
66
+ * Idempotent by transition id. `INSERT OR IGNORE` decides it: when the row
67
+ * already exists nothing is inserted AND the task's `local_state` is left
68
+ * alone, so replaying an old transition cannot drag a task's state
69
+ * backwards — which is precisely what recovery replaying a stored sequence
70
+ * would otherwise do.
71
+ */
72
+ recordTransition(record: LocalTransitionRecord): Promise<void>;
73
+ /**
74
+ * First terminal wins, matching the cloud's own receipt rule (§12.6.4:
75
+ * 不覆写第一份事实). A repeat carrying the SAME payload hash is a delivery
76
+ * retry and updates only the retry bookkeeping (`truth_state`, `attempt`,
77
+ * `last_error`); a repeat carrying a DIFFERENT hash is a second, distinct
78
+ * terminal for a task that already has one, and is recorded nowhere — the
79
+ * first fact stands.
80
+ */
81
+ recordTerminal(record: LocalTerminalRecord): Promise<void>;
82
+ /**
83
+ * What this daemon was in the middle of: a task whose offer envelope is
84
+ * durable, that has no terminal, that was not declined, and that recovery
85
+ * has not already closed out.
86
+ *
87
+ * A task with a terminal is NOT here even when the cloud has not confirmed
88
+ * it — that is terminal REDELIVERY (§12.7.3's "terminal 生成后、truth 写入前"
89
+ * row), a different track with different evidence, and folding the two
90
+ * together would have recovery re-admit a task that already finished.
91
+ */
92
+ listRecoverable(): Promise<RecoverableTask[]>;
93
+ /**
94
+ * Writes the recovery marker. Idempotent and additive: a task already
95
+ * marked is left exactly as it was (the FIRST recovery decision is the real
96
+ * one), and no path here deletes anything — §12.7.2.1 puts recovery-marked
97
+ * records on the never-auto-delete list, and this is what puts them there.
98
+ */
99
+ markRecovered(taskId: string, outcome: RecoveryOutcome): Promise<void>;
100
+ /**
101
+ * Per-category bytes (§12.7.2.1). `journal` and `quarantine` are MEASURED
102
+ * off the filesystem — they are this object's own footprint, so guessing
103
+ * would be inexcusable. `cache`/`log`/`workspace` are owned by other
104
+ * subsystems, so they come from whatever they last reported into
105
+ * `local_storage_usage`; with nothing reported the answer is an explicit
106
+ * zero-marked-approximate, never a fabricated estimate.
107
+ */
108
+ measureUsage(): Promise<LocalStorageUsage>;
109
+ /**
110
+ * Record what another subsystem measured for a category it owns. Beyond
111
+ * S3.3's minimum API and deliberately restricted to the three categories
112
+ * this journal does NOT measure itself — reporting a number for `journal`
113
+ * or `quarantine` would let a stale figure override a measured one.
114
+ */
115
+ reportCategoryUsage(category: (typeof HOST_REPORTED_CATEGORIES)[number], usage: CategoryUsage): Promise<void>;
116
+ /**
117
+ * Register something the cleanup worker may act on. Beyond S3.3's minimum
118
+ * API (which has no producer for the candidate table), and the only way a
119
+ * row gets in: `category` is typed {@link CleanableCategory}, so protected
120
+ * data has no spelling that would let it be enqueued in the first place.
121
+ */
122
+ enqueueCleanupCandidate(candidate: {
123
+ readonly candidateId: string;
124
+ readonly category: CleanableCategory;
125
+ readonly ref: string;
126
+ readonly eligibleAt: string;
127
+ readonly reason: string;
128
+ }): Promise<void>;
129
+ listCleanupCandidates(now: Date, limit: number): Promise<CleanupCandidate[]>;
130
+ /**
131
+ * `deleted`/`skipped` resolve the candidate; `failed` does NOT — it bumps
132
+ * the attempt count and records the error, leaving the row eligible again.
133
+ * That asymmetry is what makes a cleanup worker crash safe in either order:
134
+ * crash after deleting the file but before marking, and the candidate is
135
+ * retried against a file that is already gone (a no-op the worker reports as
136
+ * `deleted`); crash after marking but before deleting, and the metadata says
137
+ * resolved for something still on disk, which the next reference scan
138
+ * re-enqueues. Neither order can lose protected data, because protected data
139
+ * cannot be a candidate at all.
140
+ */
141
+ markCleanupResult(result: CleanupResult): Promise<void>;
142
+ /**
143
+ * §12.7.2.1's cleanup step 3: drop one task's journal rows, but ONLY when
144
+ * the cloud has confirmed its terminal and it carries no recovery marker.
145
+ *
146
+ * Beyond S3.3's minimum API, and the ONLY deletion path this journal has.
147
+ * The eligibility test is the first statement INSIDE the transaction that
148
+ * would delete, so "is this row protected?" and "delete it" cannot be
149
+ * separated by another writer marking it recovered in between — a check
150
+ * performed outside the write lock is a check that can go stale, and the row
151
+ * it goes stale on is recovery evidence.
152
+ *
153
+ * The idempotency receipt goes with the rows. That is deliberate and it is
154
+ * what `retentionMs['confirmed-journal']` exists to protect: once the receipt
155
+ * is gone, a redelivery of that envelope would be appended as new, so the
156
+ * candidate's retention MUST outlast the mailbox redelivery window
157
+ * (`storage-policy.ts`'s `DEFAULT_RETENTION_MS` carries the longest default
158
+ * of the five for exactly this reason). Keeping the receipt forever would
159
+ * trade that risk for an unbounded table, which is the growth this step is
160
+ * here to stop.
161
+ *
162
+ * Returns `false` — having deleted nothing — when the guard refuses.
163
+ */
164
+ pruneConfirmedJournalTask(taskId: string): Promise<boolean>;
165
+ /**
166
+ * WAL checkpoint plus a BOUNDED incremental vacuum. Maintenance only — it
167
+ * goes through the same single-writer queue as everything else, so it can
168
+ * never run concurrently with an ack-critical append, and `incrementalVacuumPages`
169
+ * caps how long one pass can hold that queue.
170
+ */
171
+ compact(options: CompactOptions): Promise<CompactResult>;
172
+ close(): Promise<void>;
173
+ }