@deepstrike/sdk 0.2.50 → 0.2.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -60
- package/dist/index.d.ts +5 -7
- package/dist/index.js +3 -3
- package/dist/kernel.d.ts +61 -31
- package/dist/runtime/canonical-kernel-step.d.ts +152 -0
- package/dist/runtime/canonical-kernel-step.js +1483 -0
- package/dist/runtime/execution-plane.d.ts +0 -3
- package/dist/runtime/execution-plane.js +0 -24
- package/dist/runtime/facade.js +3 -0
- package/dist/runtime/kernel-event-log.js +7 -13
- package/dist/runtime/kernel-journal.d.ts +264 -0
- package/dist/runtime/kernel-journal.js +741 -0
- package/dist/runtime/kernel-primitives-dashboard.d.ts +0 -2
- package/dist/runtime/kernel-primitives-dashboard.js +1 -8
- package/dist/runtime/kernel-step.d.ts +29 -109
- package/dist/runtime/kernel-step.js +47 -317
- package/dist/runtime/os-snapshot.d.ts +2 -2
- package/dist/runtime/os-snapshot.js +2 -6
- package/dist/runtime/payload-store.d.ts +16 -0
- package/dist/runtime/payload-store.js +80 -0
- package/dist/runtime/runner.d.ts +31 -114
- package/dist/runtime/runner.js +689 -774
- package/dist/runtime/session-log.d.ts +34 -32
- package/dist/runtime/session-log.js +21 -131
- package/dist/runtime/session-repair.d.ts +2 -36
- package/dist/runtime/session-repair.js +2 -47
- package/dist/runtime/sub-agent-orchestrator.d.ts +1 -1
- package/dist/runtime/sub-agent-orchestrator.js +42 -40
- package/dist/types/agent.d.ts +22 -19
- package/dist/types/agent.js +26 -42
- package/dist/workflow/public.d.ts +1 -1
- package/dist/workflow/public.js +1 -1
- package/package.json +2 -2
- package/dist/runtime/kernel-rebuild.d.ts +0 -13
- package/dist/runtime/kernel-rebuild.js +0 -75
- package/dist/runtime/kernel-transaction-log.d.ts +0 -61
- package/dist/runtime/kernel-transaction-log.js +0 -149
- package/dist/runtime/large-result-spool.d.ts +0 -93
- package/dist/runtime/large-result-spool.js +0 -214
|
@@ -2,7 +2,6 @@ import type { ToolCall, ToolSchema, StreamEvent, ToolSuspendEvent, PermissionReq
|
|
|
2
2
|
import type { RegisteredTool } from "../tools/index.js";
|
|
3
3
|
import type { DreamStore, MemoryScope } from "../memory/protocols.js";
|
|
4
4
|
import type { KnowledgeSource } from "../knowledge/source.js";
|
|
5
|
-
import { LargeResultSpool } from "./large-result-spool.js";
|
|
6
5
|
import type { OperationContext } from "./reliability.js";
|
|
7
6
|
export interface RunContext {
|
|
8
7
|
/** Immutable identity, deadline, and cancellation boundary for this operation. */
|
|
@@ -14,7 +13,6 @@ export interface RunContext {
|
|
|
14
13
|
knowledgeSource?: KnowledgeSource;
|
|
15
14
|
onToolSuspend?: (event: ToolSuspendEvent) => Promise<unknown> | unknown;
|
|
16
15
|
onPermissionRequest?: (event: PermissionRequestEvent) => Promise<PermissionResponse | boolean> | PermissionResponse | boolean;
|
|
17
|
-
resultSpool?: LargeResultSpool;
|
|
18
16
|
/** M3/G4 worktree isolation: the working directory a sub-agent's tools should run in (the git
|
|
19
17
|
* worktree created for an `isolation: "worktree"` node). Injected by `WorktreeExecutionPlane`; a
|
|
20
18
|
* cwd-aware execution plane / tool reads it to scope filesystem + subprocess work. Undefined ⇒
|
|
@@ -38,7 +36,6 @@ export declare class LocalExecutionPlane implements ExecutionPlane {
|
|
|
38
36
|
unregister(name: string): this;
|
|
39
37
|
schemas(): ToolSchema[];
|
|
40
38
|
executeAll(calls: ToolCall[], ctx: RunContext): AsyncIterable<StreamEvent>;
|
|
41
|
-
private tryReadSpooledArgument;
|
|
42
39
|
private executeSingle;
|
|
43
40
|
}
|
|
44
41
|
export declare function resolvePermissionRequest(request: PermissionRequestEvent, ctx: RunContext): Promise<PermissionResponse>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { isAsyncIterable, maybeWarnFailureShapedChunk, normalizeToolChunk, toolChunkText, validateToolArguments } from "../tools/index.js";
|
|
2
2
|
import { formatToolError } from "../tools/errors.js";
|
|
3
3
|
import { readSkillFile } from "../skills/loader.js";
|
|
4
|
-
import { LargeResultSpool } from "./large-result-spool.js";
|
|
5
4
|
export class LocalExecutionPlane {
|
|
6
5
|
tools = new Map();
|
|
7
6
|
register(...tools) {
|
|
@@ -81,30 +80,7 @@ export class LocalExecutionPlane {
|
|
|
81
80
|
}
|
|
82
81
|
}
|
|
83
82
|
}
|
|
84
|
-
async tryReadSpooledArgument(call, ctx) {
|
|
85
|
-
const isReadTool = ["read", "read_file", "view_file", "read_spooled_result"].includes(call.name);
|
|
86
|
-
if (!isReadTool)
|
|
87
|
-
return null;
|
|
88
|
-
try {
|
|
89
|
-
const args = JSON.parse(call.arguments || "{}");
|
|
90
|
-
for (const val of Object.values(args)) {
|
|
91
|
-
if (typeof val === "string" && (val.startsWith(".spool/") || val.includes("/.spool/"))) {
|
|
92
|
-
const spool = ctx.resultSpool ?? new LargeResultSpool();
|
|
93
|
-
const content = await spool.readSpooledResult(val);
|
|
94
|
-
return content;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
// Ignore errors
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
83
|
async *executeSingle(call, ctx) {
|
|
104
|
-
const spooledContent = await this.tryReadSpooledArgument(call, ctx);
|
|
105
|
-
if (spooledContent !== null) {
|
|
106
|
-
return { callId: call.id, output: spooledContent, isError: false };
|
|
107
|
-
}
|
|
108
84
|
const registered = this.tools.get(call.name);
|
|
109
85
|
if (!registered)
|
|
110
86
|
return { callId: call.id, output: `unknown tool: ${call.name}`, isError: true };
|
package/dist/runtime/facade.js
CHANGED
|
@@ -58,6 +58,9 @@ export async function runFanout(opts) {
|
|
|
58
58
|
if (opts.synthesisRole)
|
|
59
59
|
spec.nodes[spec.nodes.length - 1].role = opts.synthesisRole;
|
|
60
60
|
const outcome = await runner.runWorkflow(spec, opts.sessionId ? { sessionId: opts.sessionId } : undefined);
|
|
61
|
+
if (outcome.rejection) {
|
|
62
|
+
throw new Error(`workflow ${outcome.rejection.operation} rejected: ${outcome.rejection.reason}`);
|
|
63
|
+
}
|
|
61
64
|
// The synthesis node is the last spec node; the kernel ids nodes `wf-node{index}`. Prefer that id,
|
|
62
65
|
// but fall back to the last completed node's output so a kernel id-scheme change can't silently
|
|
63
66
|
// return an empty synthesis.
|
|
@@ -9,7 +9,6 @@ export function categoryForKind(kind) {
|
|
|
9
9
|
case "page_in_requested":
|
|
10
10
|
case "renewed":
|
|
11
11
|
case "context_renewed":
|
|
12
|
-
case "large_result_spooled":
|
|
13
12
|
case "memory_written":
|
|
14
13
|
case "memory_queried":
|
|
15
14
|
case "memory_validation_failed":
|
|
@@ -111,7 +110,7 @@ export function kernelObservationToSessionEvent(obs, turn, opts = {}) {
|
|
|
111
110
|
kind: "agent_process_changed",
|
|
112
111
|
turn: t,
|
|
113
112
|
agent_id: obs.agent_id ?? "",
|
|
114
|
-
|
|
113
|
+
parent_task_id: obs.parent_task_id ?? "",
|
|
115
114
|
role: obs.role ?? "",
|
|
116
115
|
isolation: obs.isolation ?? "",
|
|
117
116
|
context_inheritance: obs.context_inheritance ?? "",
|
|
@@ -182,16 +181,6 @@ export function kernelObservationToSessionEvent(obs, turn, opts = {}) {
|
|
|
182
181
|
};
|
|
183
182
|
case "page_in_requested":
|
|
184
183
|
return null;
|
|
185
|
-
case "large_result_spooled":
|
|
186
|
-
return {
|
|
187
|
-
kind: "large_result_spooled",
|
|
188
|
-
turn: t,
|
|
189
|
-
call_id: obs.call_id ?? "",
|
|
190
|
-
tool: obs.tool ?? "",
|
|
191
|
-
original_size: obs.original_size ?? 0,
|
|
192
|
-
preview_size: obs.preview_size ?? 0,
|
|
193
|
-
spool_ref: obs.spool_ref,
|
|
194
|
-
};
|
|
195
184
|
case "page_out_archived":
|
|
196
185
|
return {
|
|
197
186
|
kind: "page_out",
|
|
@@ -264,7 +253,12 @@ export function kernelObservationToSessionEvent(obs, turn, opts = {}) {
|
|
|
264
253
|
};
|
|
265
254
|
}
|
|
266
255
|
default:
|
|
267
|
-
return
|
|
256
|
+
return {
|
|
257
|
+
kind: "kernel_observation",
|
|
258
|
+
turn: t,
|
|
259
|
+
observation_kind: obs.kind,
|
|
260
|
+
raw: { ...obs },
|
|
261
|
+
};
|
|
268
262
|
}
|
|
269
263
|
}
|
|
270
264
|
export function primitiveForCategory(category) {
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `KernelJournal` — the durable transaction capability of the Canonical Kernel ABI (spec §9.1).
|
|
3
|
+
*
|
|
4
|
+
* This is the *authoritative* interface shape for all four SDKs (Node first, then Python/WASM/Rust).
|
|
5
|
+
*
|
|
6
|
+
* Three rules give this file its shape:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Records are opaque.** core owns canonical serialization and hashing
|
|
9
|
+
* (`KernelRecord::record_bytes()` / `record_digest()` / `expected_head()`). The host stores the
|
|
10
|
+
* bytes verbatim and indexes them by the digest core handed it. A journal that re-serializes a
|
|
11
|
+
* record to recompute its hash would make "the host recomputed and disagreed" a reachable state;
|
|
12
|
+
* it is not one here.
|
|
13
|
+
* 2. **CAS is a storage-layer primitive, not a read-compare-write sequence.** §9.1 requires a real
|
|
14
|
+
* atomic operation (file lock / `O_EXCL` chained naming / conditional database update).
|
|
15
|
+
* `InMemoryKernelJournal` is atomic only within one process and says so in its own type;
|
|
16
|
+
* `FileKernelJournal` is atomic across processes (see its class docs).
|
|
17
|
+
* 3. **The journal's sequence space is `step_seq`** — the operation's record-chain position — and is
|
|
18
|
+
* completely independent of `SessionLog`'s business event `seq`. Pruning a journal prefix can
|
|
19
|
+
* never punch a hole in business event numbering (spec Task 8b, criterion 4).
|
|
20
|
+
*
|
|
21
|
+
* Failures are typed so a caller can tell "retry after rebuild" from "this storage is broken" from
|
|
22
|
+
* "someone handed me a corrupt chain" — a durable-step wrapper must never publish effects on any of
|
|
23
|
+
* them, and must never collapse them into one opaque `Error`.
|
|
24
|
+
*/
|
|
25
|
+
export declare const MAX_CHAIN_POSITION = 1000000000000;
|
|
26
|
+
/**
|
|
27
|
+
* The CAS precondition did not hold: the journal head (or checkpoint pointer) moved.
|
|
28
|
+
*
|
|
29
|
+
* Retryable — the protocol response is `abort(token)` → re-read head → rebuild → replay the input
|
|
30
|
+
* (spec §8.3, row "CAS conflict").
|
|
31
|
+
*/
|
|
32
|
+
export declare class JournalCasConflictError extends Error {
|
|
33
|
+
constructor(message: string);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The journal contents contradict themselves or the caller's claim: a broken digest chain, a
|
|
37
|
+
* `step_seq` that does not follow its predecessor, a checkpoint whose `covered_head` does not match
|
|
38
|
+
* the record at its `through_step_seq`. Never retryable — retrying replays the same contradiction.
|
|
39
|
+
*/
|
|
40
|
+
export declare class JournalIntegrityError extends Error {
|
|
41
|
+
constructor(message: string);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The storage layer failed (disk full, permission denied, hard links unsupported). Distinct from
|
|
45
|
+
* both of the above: the journal state is *unknown*, not known-conflicting and not known-corrupt.
|
|
46
|
+
*/
|
|
47
|
+
export declare class JournalIoError extends Error {
|
|
48
|
+
constructor(message: string, options?: {
|
|
49
|
+
cause?: unknown;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** What a host hands the journal: core's opaque bytes plus the identity core assigned them. */
|
|
53
|
+
export interface JournalRecordInput {
|
|
54
|
+
/** Chain position. Genesis (`ConfigureOperation`) is 0; every later record is `head + 1`. */
|
|
55
|
+
step_seq: number;
|
|
56
|
+
/** core's `record_digest`. The journal indexes by it and never recomputes it. */
|
|
57
|
+
record_digest: string;
|
|
58
|
+
/** core's `record_bytes`, stored verbatim. */
|
|
59
|
+
record_bytes: Uint8Array;
|
|
60
|
+
}
|
|
61
|
+
/** A stored record. `previous_record_digest` is the CAS precondition it was appended under. */
|
|
62
|
+
export interface JournalEntry extends JournalRecordInput {
|
|
63
|
+
/** Absent on the genesis record only (spec §8.1: the first append's expected head is empty). */
|
|
64
|
+
previous_record_digest?: string;
|
|
65
|
+
}
|
|
66
|
+
/** The journal head: the last record of an operation's chain. */
|
|
67
|
+
export interface JournalHead {
|
|
68
|
+
step_seq: number;
|
|
69
|
+
record_digest: string;
|
|
70
|
+
}
|
|
71
|
+
export interface JournalAppendReceipt {
|
|
72
|
+
step_seq: number;
|
|
73
|
+
record_digest: string;
|
|
74
|
+
}
|
|
75
|
+
/** The host-persistable part of `kernel.checkpoint_candidate()` (spec §12.3). */
|
|
76
|
+
export interface CheckpointCandidate {
|
|
77
|
+
/** Stable identity of this checkpoint; the CAS token a later install names as its predecessor. */
|
|
78
|
+
checkpoint_id: string;
|
|
79
|
+
/** The chain position this checkpoint's logical state covers. */
|
|
80
|
+
through_step_seq: number;
|
|
81
|
+
/** core's digest of the logical state. Opaque to the journal. */
|
|
82
|
+
state_digest: string;
|
|
83
|
+
/** The serialized checkpoint blob, stored verbatim. */
|
|
84
|
+
checkpoint_bytes: Uint8Array;
|
|
85
|
+
}
|
|
86
|
+
export interface InstalledCheckpoint extends CheckpointCandidate {
|
|
87
|
+
/** Monotonic install ordinal. `previous_checkpoint_id === undefined` installs ordinal 0. */
|
|
88
|
+
ordinal: number;
|
|
89
|
+
/** The record digest at `through_step_seq` — verified at install, not required to still be head. */
|
|
90
|
+
covered_head: string;
|
|
91
|
+
previous_checkpoint_id?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Whether `ackCheckpoint` has run. Prefix reclamation is gated on this: a checkpoint that is
|
|
94
|
+
* installed but not acknowledged is recoverable-from but not prune-authorising (spec §12.3 rule 5/6).
|
|
95
|
+
*/
|
|
96
|
+
acknowledged: boolean;
|
|
97
|
+
}
|
|
98
|
+
export interface JournalPruneReceipt {
|
|
99
|
+
/** Highest `step_seq` no longer retained. `-1` when nothing has ever been pruned. */
|
|
100
|
+
pruned_through_step_seq: number;
|
|
101
|
+
pruned_count: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The durable transaction capability (spec §9.1). Deliberately *not* part of `SessionLog`: a custom
|
|
105
|
+
* business-projection log must never be forced to masquerade as a transactional journal (§9.4).
|
|
106
|
+
* One class may implement both — `InMemorySessionLog` does — but the interfaces stay separate.
|
|
107
|
+
*
|
|
108
|
+
* Guarantees an implementation owes (spec §9.1):
|
|
109
|
+
* - strict ordering within an operation;
|
|
110
|
+
* - a failed CAS never overwrites;
|
|
111
|
+
* - record bytes preserved verbatim;
|
|
112
|
+
* - checkpoint pointer advances monotonically;
|
|
113
|
+
* - the checkpoint store verifies `covered_head` against `through_step_seq` but does **not** require
|
|
114
|
+
* it to still be the current transaction head (§22.14);
|
|
115
|
+
* - checkpoint pointer and prefix pruning have an explicit acknowledgement boundary.
|
|
116
|
+
*/
|
|
117
|
+
export interface KernelJournal {
|
|
118
|
+
/**
|
|
119
|
+
* Atomically append `record` iff the operation's head is exactly `expectedHead`.
|
|
120
|
+
*
|
|
121
|
+
* @param expectedHead `undefined` starts the chain (genesis; `record.step_seq` must be 0).
|
|
122
|
+
* @throws {JournalCasConflictError} head moved, or a genesis already exists.
|
|
123
|
+
* @throws {JournalIntegrityError} `step_seq` does not follow the head.
|
|
124
|
+
* @throws {JournalIoError} storage failure.
|
|
125
|
+
*/
|
|
126
|
+
compareAndAppend(operationId: string, expectedHead: string | undefined, record: JournalRecordInput): Promise<JournalAppendReceipt>;
|
|
127
|
+
/** The current head, or `undefined` when the operation has no records and no pruned anchor. */
|
|
128
|
+
head(operationId: string): Promise<JournalHead | undefined>;
|
|
129
|
+
/** Records with `step_seq >= fromStepSeq` (default: the whole retained chain), in chain order. */
|
|
130
|
+
readFrom(operationId: string, fromStepSeq?: number): Promise<JournalEntry[]>;
|
|
131
|
+
/**
|
|
132
|
+
* Records strictly after the record whose digest is `afterHead` — the digest-anchored cursor
|
|
133
|
+
* §9.1 names `records_after(operation_id, checkpoint_head)`. `undefined` returns everything retained.
|
|
134
|
+
*
|
|
135
|
+
* @throws {JournalIntegrityError} `afterHead` names no retained record and no pruned anchor.
|
|
136
|
+
*/
|
|
137
|
+
recordsAfter(operationId: string, afterHead?: string): Promise<JournalEntry[]>;
|
|
138
|
+
/**
|
|
139
|
+
* Atomically install `checkpoint` iff the operation's checkpoint pointer is exactly
|
|
140
|
+
* `previousCheckpointId`, and `coveredHead` is the record digest at `checkpoint.through_step_seq`.
|
|
141
|
+
*
|
|
142
|
+
* Per §22.14 this deliberately does **not** require `coveredHead` to still be the current
|
|
143
|
+
* transaction head: transactions appended after the candidate was taken stay as tail.
|
|
144
|
+
*
|
|
145
|
+
* @param previousCheckpointId `undefined` installs the operation's first checkpoint.
|
|
146
|
+
* @throws {JournalCasConflictError} the checkpoint pointer moved.
|
|
147
|
+
* @throws {JournalIntegrityError} `coveredHead`/`through_step_seq` disagree with the chain, or
|
|
148
|
+
* `through_step_seq` would move the pointer backwards.
|
|
149
|
+
*/
|
|
150
|
+
compareAndInstallCheckpoint(operationId: string, previousCheckpointId: string | undefined, coveredHead: string, checkpoint: CheckpointCandidate): Promise<InstalledCheckpoint>;
|
|
151
|
+
/** The highest-ordinal installed checkpoint, acknowledged or not. */
|
|
152
|
+
latestCheckpoint(operationId: string): Promise<InstalledCheckpoint | undefined>;
|
|
153
|
+
/**
|
|
154
|
+
* Record the durable acknowledgement that opens the prefix-reclamation boundary. Idempotent.
|
|
155
|
+
*
|
|
156
|
+
* @throws {JournalIntegrityError} no checkpoint with that id is installed.
|
|
157
|
+
*/
|
|
158
|
+
ackCheckpoint(operationId: string, checkpointId: string): Promise<InstalledCheckpoint>;
|
|
159
|
+
/**
|
|
160
|
+
* Reclaim the record prefix covered by the latest **acknowledged** checkpoint. A no-op while no
|
|
161
|
+
* checkpoint is acknowledged. The pruned boundary is retained as an anchor so `head()` and the
|
|
162
|
+
* next CAS still resolve on a fully-pruned chain.
|
|
163
|
+
*/
|
|
164
|
+
pruneAckedPrefix(operationId: string): Promise<JournalPruneReceipt>;
|
|
165
|
+
/**
|
|
166
|
+
* Durably stage one outbound WireEnvelope JSON for `operationId` until the matching record is
|
|
167
|
+
* append-acked (or the attempt is abandoned). Overwrites any previous pending envelope.
|
|
168
|
+
*
|
|
169
|
+
* Required by Phase 6 host cutover (adjudication 5e.3): retries must replay byte-identical
|
|
170
|
+
* envelopes — reminting `observed_at_ms` after a crash produces `DuplicateInputConflict`.
|
|
171
|
+
*/
|
|
172
|
+
stageOutboundEnvelope(operationId: string, envelopeJson: string): Promise<void>;
|
|
173
|
+
/** Read the staged outbound envelope, if any. */
|
|
174
|
+
readOutboundEnvelope(operationId: string): Promise<string | undefined>;
|
|
175
|
+
/** Clear the staged outbound envelope. Idempotent. */
|
|
176
|
+
clearOutboundEnvelope(operationId: string): Promise<void>;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* **Single-process dev/test implementation.**
|
|
180
|
+
*
|
|
181
|
+
* CAS is genuinely atomic here — every check-then-mutate below runs to completion without an
|
|
182
|
+
* intervening `await`, so no other task can interleave on a single-threaded runtime — but that
|
|
183
|
+
* atomicity ends at the process boundary. Two processes sharing "the same" journal do not exist:
|
|
184
|
+
* each has its own `Map`. Production hosts must supply a `KernelJournal` whose CAS is a real
|
|
185
|
+
* storage-layer primitive (spec §9.1); `FileKernelJournal` is the reference for that.
|
|
186
|
+
*/
|
|
187
|
+
export declare class InMemoryKernelJournal implements KernelJournal {
|
|
188
|
+
private readonly operations;
|
|
189
|
+
private state;
|
|
190
|
+
private headOf;
|
|
191
|
+
compareAndAppend(operationId: string, expectedHead: string | undefined, record: JournalRecordInput): Promise<JournalAppendReceipt>;
|
|
192
|
+
head(operationId: string): Promise<JournalHead | undefined>;
|
|
193
|
+
readFrom(operationId: string, fromStepSeq?: number): Promise<JournalEntry[]>;
|
|
194
|
+
recordsAfter(operationId: string, afterHead?: string): Promise<JournalEntry[]>;
|
|
195
|
+
compareAndInstallCheckpoint(operationId: string, previousCheckpointId: string | undefined, coveredHead: string, checkpoint: CheckpointCandidate): Promise<InstalledCheckpoint>;
|
|
196
|
+
latestCheckpoint(operationId: string): Promise<InstalledCheckpoint | undefined>;
|
|
197
|
+
ackCheckpoint(operationId: string, checkpointId: string): Promise<InstalledCheckpoint>;
|
|
198
|
+
pruneAckedPrefix(operationId: string): Promise<JournalPruneReceipt>;
|
|
199
|
+
stageOutboundEnvelope(operationId: string, envelopeJson: string): Promise<void>;
|
|
200
|
+
readOutboundEnvelope(operationId: string): Promise<string | undefined>;
|
|
201
|
+
clearOutboundEnvelope(operationId: string): Promise<void>;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* **Cross-process atomic reference implementation** of `KernelJournal` (spec Task 8b).
|
|
205
|
+
*
|
|
206
|
+
* The atomicity primitive is POSIX `link(2)`: content is written to a private temp file and fsynced,
|
|
207
|
+
* then hard-linked into its final name. `link` fails with `EEXIST` if the name is taken, and it
|
|
208
|
+
* publishes already-complete content — so a crash can never leave a half-written `.rec`, only an
|
|
209
|
+
* orphan temp file that the naming rule ignores. The journal root must therefore live on a
|
|
210
|
+
* filesystem that supports hard links; that requirement is the price of real CAS.
|
|
211
|
+
*
|
|
212
|
+
* **Why the record filename is `<step_seq>.rec` and contains no digest.** The filename *is* the
|
|
213
|
+
* collision domain. Two writers racing on the same head both compute the same next `step_seq`, so
|
|
214
|
+
* they contend for one name and exactly one wins. Folding a per-writer value (the new record's
|
|
215
|
+
* digest) into the name would give the racers *different* names — both `link`s would succeed and the
|
|
216
|
+
* chain would fork. Only the predecessor-determined part of the identity may appear in the name.
|
|
217
|
+
*
|
|
218
|
+
* The pre-`link` head check is not a TOCTOU hole: it can only *reject* an append that `link` would
|
|
219
|
+
* have accepted (a stale `expectedHead` whose `step_seq` slot happens to be free), never accept one
|
|
220
|
+
* `link` would have rejected. Every acceptance is still decided by the atomic `link`.
|
|
221
|
+
*
|
|
222
|
+
* Checkpoint installs use the same primitive on a separate ordinal space
|
|
223
|
+
* (`<ordinal>.ckpt`), so two processes installing on the same predecessor also contend for one name.
|
|
224
|
+
*/
|
|
225
|
+
export declare class FileKernelJournal implements KernelJournal {
|
|
226
|
+
private readonly root;
|
|
227
|
+
constructor(root: string);
|
|
228
|
+
private operationDir;
|
|
229
|
+
private recordsDir;
|
|
230
|
+
private checkpointsDir;
|
|
231
|
+
private tmpDir;
|
|
232
|
+
private prunedPath;
|
|
233
|
+
/**
|
|
234
|
+
* Write `payload` to a temp file, fsync it, then atomically claim `target` by hard link.
|
|
235
|
+
*
|
|
236
|
+
* @returns `false` when the name was already taken — i.e. a lost CAS race.
|
|
237
|
+
*/
|
|
238
|
+
private publish;
|
|
239
|
+
private syncDir;
|
|
240
|
+
/** Sorted `step_seq` values of the retained records. Anything not matching the rule is residue. */
|
|
241
|
+
private recordSeqs;
|
|
242
|
+
private readRecord;
|
|
243
|
+
private prunedAnchor;
|
|
244
|
+
head(operationId: string): Promise<JournalHead | undefined>;
|
|
245
|
+
compareAndAppend(operationId: string, expectedHead: string | undefined, record: JournalRecordInput): Promise<JournalAppendReceipt>;
|
|
246
|
+
readFrom(operationId: string, fromStepSeq?: number): Promise<JournalEntry[]>;
|
|
247
|
+
recordsAfter(operationId: string, afterHead?: string): Promise<JournalEntry[]>;
|
|
248
|
+
/** Sorted ordinals of installed checkpoints. */
|
|
249
|
+
private checkpointOrdinals;
|
|
250
|
+
private ackedOrdinals;
|
|
251
|
+
private readCheckpoint;
|
|
252
|
+
latestCheckpoint(operationId: string): Promise<InstalledCheckpoint | undefined>;
|
|
253
|
+
compareAndInstallCheckpoint(operationId: string, previousCheckpointId: string | undefined, coveredHead: string, checkpoint: CheckpointCandidate): Promise<InstalledCheckpoint>;
|
|
254
|
+
ackCheckpoint(operationId: string, checkpointId: string): Promise<InstalledCheckpoint>;
|
|
255
|
+
pruneAckedPrefix(operationId: string): Promise<JournalPruneReceipt>;
|
|
256
|
+
/** The anchor only ever moves forward, so an atomic overwriting `rename` is the right primitive. */
|
|
257
|
+
private writeAnchor;
|
|
258
|
+
private outboundPath;
|
|
259
|
+
/** Overwriteable durable blob — same write-tmp→fsync→rename pattern as the pruned anchor. */
|
|
260
|
+
private writeReplaceable;
|
|
261
|
+
stageOutboundEnvelope(operationId: string, envelopeJson: string): Promise<void>;
|
|
262
|
+
readOutboundEnvelope(operationId: string): Promise<string | undefined>;
|
|
263
|
+
clearOutboundEnvelope(operationId: string): Promise<void>;
|
|
264
|
+
}
|