@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,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
|
+
}
|