@deepstrike/wasm 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 +2 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/runtime/canonical-kernel-step.d.ts +155 -0
- package/dist/runtime/canonical-kernel-step.js +1523 -0
- package/dist/runtime/execution-plane.d.ts +0 -3
- package/dist/runtime/execution-plane.js +0 -25
- package/dist/runtime/facade.js +3 -0
- package/dist/runtime/index.d.ts +7 -7
- package/dist/runtime/index.js +4 -3
- package/dist/runtime/kernel-event-log.d.ts +0 -1
- package/dist/runtime/kernel-event-log.js +7 -13
- package/dist/runtime/kernel-journal.d.ts +332 -0
- package/dist/runtime/kernel-journal.js +681 -0
- package/dist/runtime/kernel-step.d.ts +34 -89
- package/dist/runtime/kernel-step.js +67 -167
- package/dist/runtime/os-snapshot.d.ts +2 -2
- package/dist/runtime/os-snapshot.js +2 -6
- package/dist/runtime/payload-store.d.ts +30 -0
- package/dist/runtime/payload-store.js +66 -0
- package/dist/runtime/runner.d.ts +27 -75
- package/dist/runtime/runner.js +408 -632
- package/dist/runtime/session-log.d.ts +24 -27
- package/dist/runtime/session-log.js +11 -52
- 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 +2 -2
- package/dist/runtime/sub-agent-orchestrator.js +36 -40
- package/dist/runtime/types/agent.d.ts +13 -16
- package/dist/runtime/types/agent.js +16 -41
- package/package.json +4 -3
- package/dist/runtime/kernel-rebuild.d.ts +0 -8
- package/dist/runtime/kernel-rebuild.js +0 -65
- package/dist/runtime/kernel-transaction-log.d.ts +0 -61
- package/dist/runtime/kernel-transaction-log.js +0 -140
- package/dist/runtime/large-result-spool.d.ts +0 -57
- package/dist/runtime/large-result-spool.js +0 -169
|
@@ -2,7 +2,6 @@ import type { ToolCall, ToolSchema, StreamEvent, PermissionRequestEvent, Permiss
|
|
|
2
2
|
import type { RegisteredTool } from "../tools/index.js";
|
|
3
3
|
import type { DreamStore, MemoryScope } from "../memory/index.js";
|
|
4
4
|
import type { KnowledgeSource } from "../knowledge/index.js";
|
|
5
|
-
import { LargeResultSpool } from "./large-result-spool.js";
|
|
6
5
|
export interface ToolSuspendEvent {
|
|
7
6
|
type: "tool_suspend";
|
|
8
7
|
callId: string;
|
|
@@ -18,7 +17,6 @@ export interface RunContext {
|
|
|
18
17
|
knowledgeSource?: KnowledgeSource;
|
|
19
18
|
onToolSuspend?: (event: ToolSuspendEvent) => Promise<unknown> | unknown;
|
|
20
19
|
onPermissionRequest?: (event: PermissionRequestEvent) => Promise<PermissionResponse | boolean> | PermissionResponse | boolean;
|
|
21
|
-
resultSpool?: LargeResultSpool;
|
|
22
20
|
/** M3/G4: working directory a tool should run in. WASM has no filesystem, so this is carried for
|
|
23
21
|
* tool-ABI parity with Node/Python rather than consumed by a worktree plane. */
|
|
24
22
|
cwd?: string;
|
|
@@ -35,6 +33,5 @@ export declare class LocalExecutionPlane implements ExecutionPlane {
|
|
|
35
33
|
unregister(name: string): this;
|
|
36
34
|
schemas(): ToolSchema[];
|
|
37
35
|
executeAll(calls: ToolCall[], ctx: RunContext): AsyncIterable<StreamEvent>;
|
|
38
|
-
private tryReadSpooledArgument;
|
|
39
36
|
}
|
|
40
37
|
export declare function resolvePermissionRequest(request: PermissionRequestEvent, ctx: RunContext): Promise<PermissionResponse>;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { LargeResultSpool } from "./large-result-spool.js";
|
|
2
1
|
import { formatToolError } from "../tools/errors.js";
|
|
3
2
|
function stripFrontmatter(content) {
|
|
4
3
|
const s = content.trimStart();
|
|
@@ -62,11 +61,6 @@ export class LocalExecutionPlane {
|
|
|
62
61
|
yield { type: "tool_result", callId: c.id, name: c.name, content, isError: false };
|
|
63
62
|
}
|
|
64
63
|
for (const call of regularCalls) {
|
|
65
|
-
const spooledContent = await this.tryReadSpooledArgument(call, ctx);
|
|
66
|
-
if (spooledContent !== null) {
|
|
67
|
-
yield { type: "tool_result", callId: call.id, name: call.name, content: spooledContent, isError: false };
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
64
|
const registered = this.tools.get(call.name);
|
|
71
65
|
if (!registered) {
|
|
72
66
|
yield { type: "tool_result", callId: call.id, name: call.name, content: `unknown tool: ${call.name}`, isError: true, isFatal: false, errorKind: "recoverable" };
|
|
@@ -111,25 +105,6 @@ export class LocalExecutionPlane {
|
|
|
111
105
|
}
|
|
112
106
|
}
|
|
113
107
|
}
|
|
114
|
-
async tryReadSpooledArgument(call, ctx) {
|
|
115
|
-
const isReadTool = ["read", "read_file", "view_file", "read_spooled_result"].includes(call.name);
|
|
116
|
-
if (!isReadTool)
|
|
117
|
-
return null;
|
|
118
|
-
try {
|
|
119
|
-
const args = JSON.parse(call.arguments || "{}");
|
|
120
|
-
for (const val of Object.values(args)) {
|
|
121
|
-
if (typeof val === "string" && (val.startsWith(".spool/") || val.includes("/.spool/"))) {
|
|
122
|
-
const spool = ctx.resultSpool ?? new LargeResultSpool();
|
|
123
|
-
const content = await spool.readSpooledResult(val);
|
|
124
|
-
return content;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
// Ignore errors
|
|
130
|
-
}
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
108
|
}
|
|
134
109
|
function tryParseJson(s) {
|
|
135
110
|
try {
|
package/dist/runtime/facade.js
CHANGED
|
@@ -38,6 +38,9 @@ export async function runFanout(opts) {
|
|
|
38
38
|
if (opts.synthesisRole)
|
|
39
39
|
spec.nodes[spec.nodes.length - 1].role = opts.synthesisRole;
|
|
40
40
|
const outcome = await runner.runWorkflow(spec, opts.sessionId ? { sessionId: opts.sessionId } : undefined);
|
|
41
|
+
if (outcome.rejection) {
|
|
42
|
+
throw new Error(`workflow ${outcome.rejection.operation} rejected: ${outcome.rejection.reason}`);
|
|
43
|
+
}
|
|
41
44
|
const synthesisId = `wf-node${opts.tasks.length}`;
|
|
42
45
|
const completed = outcome.nodeOutcomes.filter(node => node.status === "completed" || node.status === "completed_partial");
|
|
43
46
|
const lastCompleted = completed[completed.length - 1]?.nodeId;
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
export type {
|
|
1
|
+
export type { SessionEvent, SessionLog } from "./session-log.js";
|
|
2
2
|
export { InMemorySessionLog } from "./session-log.js";
|
|
3
|
-
export
|
|
4
|
-
export {
|
|
5
|
-
export type { KernelRebuildResult } from "./kernel-rebuild.js";
|
|
3
|
+
export { DriverKernelJournal, InMemoryJournalDriver, InMemoryKernelJournal, JournalCasConflictError, JournalIntegrityError, JournalIoError, } from "./kernel-journal.js";
|
|
4
|
+
export type { CheckpointCandidate, InstalledCheckpoint, JournalAppendReceipt, JournalEntry, JournalHead, JournalPruneReceipt, JournalRecordInput, JournalStorageDriver, KernelJournal, } from "./kernel-journal.js";
|
|
6
5
|
export * from "./context-policy.js";
|
|
7
6
|
export type { RunContext, ExecutionPlane } from "./execution-plane.js";
|
|
8
7
|
export { LocalExecutionPlane } from "./execution-plane.js";
|
|
9
8
|
export type { MemoryPolicy, MemoryWriteRateLimit, OperationCancellationReason, PromptBudget, ResourceQuota, RuntimeOptions, SchedulerPolicy } from "./runner.js";
|
|
10
9
|
export { RuntimeRunner, collectText } from "./runner.js";
|
|
11
|
-
export {
|
|
12
|
-
export type {
|
|
13
|
-
export
|
|
10
|
+
export { MemoryPayloadDriver, PayloadStore } from "./payload-store.js";
|
|
11
|
+
export type { PayloadStorageDriver, PayloadStoreConfig } from "./payload-store.js";
|
|
12
|
+
export { CanonicalKernelHost, CanonicalKernelRejectedError, CanonicalKernelRebuildRequiredError, CanonicalRunnerRuntime, canonicalActionFromPlannedStep, sha256, } from "./canonical-kernel-step.js";
|
|
13
|
+
export type { CanonicalKernelInput, CanonicalPlannedStep, CanonicalRunnerRuntimeOptions, CanonicalTransition, CanonicalTransitionOptions, } from "./canonical-kernel-step.js";
|
|
14
14
|
export { runAgent, runFanout } from "./facade.js";
|
|
15
15
|
export type { RunAgentOptions, RunFanoutOptions } from "./facade.js";
|
|
16
16
|
export { builtinReducers, resolveReducer } from "./reducers.js";
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export { InMemorySessionLog } from "./session-log.js";
|
|
2
|
-
|
|
3
|
-
export {
|
|
2
|
+
// Durable transaction capability (Canonical Kernel ABI §9.1)
|
|
3
|
+
export { DriverKernelJournal, InMemoryJournalDriver, InMemoryKernelJournal, JournalCasConflictError, JournalIntegrityError, JournalIoError, } from "./kernel-journal.js";
|
|
4
4
|
export * from "./context-policy.js";
|
|
5
5
|
export { LocalExecutionPlane } from "./execution-plane.js";
|
|
6
6
|
export { RuntimeRunner, collectText } from "./runner.js";
|
|
7
|
-
export {
|
|
7
|
+
export { MemoryPayloadDriver, PayloadStore } from "./payload-store.js";
|
|
8
|
+
export { CanonicalKernelHost, CanonicalKernelRejectedError, CanonicalKernelRebuildRequiredError, CanonicalRunnerRuntime, canonicalActionFromPlannedStep, sha256, } from "./canonical-kernel-step.js";
|
|
8
9
|
export { runAgent, runFanout } from "./facade.js";
|
|
9
10
|
export { builtinReducers, resolveReducer } from "./reducers.js";
|
|
10
11
|
export { getKernel } from "./kernel.js";
|
|
@@ -12,7 +12,6 @@ export declare function kernelObservationToSessionEvent(obs: KernelObservation,
|
|
|
12
12
|
archiveRef?: string;
|
|
13
13
|
preservedRefs?: string[];
|
|
14
14
|
compressionAction?: (action?: string) => CompressionAction;
|
|
15
|
-
spoolRef?: string;
|
|
16
15
|
}): SessionEvent | null;
|
|
17
16
|
export type KernelPrimitive = "syscall" | "sched" | "mm";
|
|
18
17
|
export declare function primitiveForCategory(category: KernelEventCategory): KernelPrimitive;
|
|
@@ -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",
|
|
@@ -247,7 +236,12 @@ export function kernelObservationToSessionEvent(obs, turn, opts = {}) {
|
|
|
247
236
|
};
|
|
248
237
|
}
|
|
249
238
|
default:
|
|
250
|
-
return
|
|
239
|
+
return {
|
|
240
|
+
kind: "kernel_observation",
|
|
241
|
+
turn: t,
|
|
242
|
+
observation_kind: obs.kind,
|
|
243
|
+
raw: { ...obs },
|
|
244
|
+
};
|
|
251
245
|
}
|
|
252
246
|
}
|
|
253
247
|
export function primitiveForCategory(category) {
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `KernelJournal` — the durable transaction capability of the Canonical Kernel ABI (spec §9.1).
|
|
3
|
+
*
|
|
4
|
+
* The interface here is field-for-field the Node SDK's `node/src/runtime/kernel-journal.ts`, which is
|
|
5
|
+
* the authoritative shape for all four SDKs. Three rules give this file its shape:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Records are opaque.** core owns canonical serialization and hashing
|
|
8
|
+
* (`KernelRecord::record_bytes()` / `record_digest()` / `expected_head()`). The host stores the
|
|
9
|
+
* bytes verbatim and indexes them by the digest core handed it. A journal that re-serializes a
|
|
10
|
+
* record to recompute its hash would make "the host recomputed and disagreed" a reachable state;
|
|
11
|
+
* it is not one here.
|
|
12
|
+
* 2. **CAS is a storage-layer primitive, not a read-compare-write sequence.** §9.1 requires a real
|
|
13
|
+
* atomic operation (file lock / atomic-publish chained naming / conditional database update).
|
|
14
|
+
* `InMemoryKernelJournal` is atomic only within one process and says so in its own type;
|
|
15
|
+
* `DriverKernelJournal` is as atomic as the {@link JournalStorageDriver} the host injects.
|
|
16
|
+
* 3. **The journal's sequence space is `step_seq`** — the operation's record-chain position — and is
|
|
17
|
+
* completely independent of `SessionLog`'s business event `seq`. Pruning a journal prefix can
|
|
18
|
+
* never punch a hole in business event numbering (spec Task 8b, criterion 4).
|
|
19
|
+
*
|
|
20
|
+
* Failures are typed so a caller can tell "retry after rebuild" from "this storage is broken" from
|
|
21
|
+
* "someone handed me a corrupt chain" — a durable-step wrapper must never publish effects on any of
|
|
22
|
+
* them, and must never collapse them into one opaque `Error`.
|
|
23
|
+
*
|
|
24
|
+
* **Why this SDK has no file backend.** The WASM SDK targets browsers, Cloudflare Workers and Deno
|
|
25
|
+
* Deploy; it imports no host filesystem anywhere, so Node's `FileKernelJournal` (POSIX `link(2)` as
|
|
26
|
+
* the atomicity primitive) has nothing to stand on. The durable backend is therefore *injected*:
|
|
27
|
+
* the host supplies a {@link JournalStorageDriver} over whatever conditional-write primitive its
|
|
28
|
+
* platform actually has (R2/S3 `If-None-Match`, Durable Object storage, KV compare-and-set, OPFS),
|
|
29
|
+
* and {@link DriverKernelJournal} supplies the chain, checkpoint and prune protocol on top of it.
|
|
30
|
+
*/
|
|
31
|
+
/** Chain positions at or above this bound are rejected (parity with Node/Python/Rust). */
|
|
32
|
+
export declare const MAX_CHAIN_POSITION = 1000000000000;
|
|
33
|
+
/**
|
|
34
|
+
* The CAS precondition did not hold: the journal head (or checkpoint pointer) moved.
|
|
35
|
+
*
|
|
36
|
+
* Retryable — the protocol response is `abort(token)` → re-read head → rebuild → replay the input
|
|
37
|
+
* (spec §8.3, row "CAS conflict").
|
|
38
|
+
*/
|
|
39
|
+
export declare class JournalCasConflictError extends Error {
|
|
40
|
+
constructor(message: string);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The journal contents contradict themselves or the caller's claim: a broken digest chain, a
|
|
44
|
+
* `step_seq` that does not follow its predecessor, a checkpoint whose `covered_head` does not match
|
|
45
|
+
* the record at its `through_step_seq`. Never retryable — retrying replays the same contradiction.
|
|
46
|
+
*/
|
|
47
|
+
export declare class JournalIntegrityError extends Error {
|
|
48
|
+
constructor(message: string);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The storage layer failed (quota exceeded, network error, driver rejected the write). Distinct from
|
|
52
|
+
* both of the above: the journal state is *unknown*, not known-conflicting and not known-corrupt.
|
|
53
|
+
*/
|
|
54
|
+
export declare class JournalIoError extends Error {
|
|
55
|
+
constructor(message: string, options?: {
|
|
56
|
+
cause?: unknown;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** What a host hands the journal: core's opaque bytes plus the identity core assigned them. */
|
|
60
|
+
export interface JournalRecordInput {
|
|
61
|
+
/** Chain position. Genesis (`ConfigureOperation`) is 0; every later record is `head + 1`. */
|
|
62
|
+
step_seq: number;
|
|
63
|
+
/** core's `record_digest`. The journal indexes by it and never recomputes it. */
|
|
64
|
+
record_digest: string;
|
|
65
|
+
/** core's `record_bytes`, stored verbatim. */
|
|
66
|
+
record_bytes: Uint8Array;
|
|
67
|
+
}
|
|
68
|
+
/** A stored record. `previous_record_digest` is the CAS precondition it was appended under. */
|
|
69
|
+
export interface JournalEntry extends JournalRecordInput {
|
|
70
|
+
/** Absent on the genesis record only (spec §8.1: the first append's expected head is empty). */
|
|
71
|
+
previous_record_digest?: string;
|
|
72
|
+
}
|
|
73
|
+
/** The journal head: the last record of an operation's chain. */
|
|
74
|
+
export interface JournalHead {
|
|
75
|
+
step_seq: number;
|
|
76
|
+
record_digest: string;
|
|
77
|
+
}
|
|
78
|
+
export interface JournalAppendReceipt {
|
|
79
|
+
step_seq: number;
|
|
80
|
+
record_digest: string;
|
|
81
|
+
}
|
|
82
|
+
/** The host-persistable part of `kernel.checkpoint_candidate()` (spec §12.3). */
|
|
83
|
+
export interface CheckpointCandidate {
|
|
84
|
+
/** Stable identity of this checkpoint; the CAS token a later install names as its predecessor. */
|
|
85
|
+
checkpoint_id: string;
|
|
86
|
+
/** The chain position this checkpoint's logical state covers. */
|
|
87
|
+
through_step_seq: number;
|
|
88
|
+
/** core's digest of the logical state. Opaque to the journal. */
|
|
89
|
+
state_digest: string;
|
|
90
|
+
/** The serialized checkpoint blob, stored verbatim. */
|
|
91
|
+
checkpoint_bytes: Uint8Array;
|
|
92
|
+
}
|
|
93
|
+
export interface InstalledCheckpoint extends CheckpointCandidate {
|
|
94
|
+
/** Monotonic install ordinal. `previous_checkpoint_id === undefined` installs ordinal 0. */
|
|
95
|
+
ordinal: number;
|
|
96
|
+
/** The record digest at `through_step_seq` — verified at install, not required to still be head. */
|
|
97
|
+
covered_head: string;
|
|
98
|
+
previous_checkpoint_id?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Whether `ackCheckpoint` has run. Prefix reclamation is gated on this: a checkpoint that is
|
|
101
|
+
* installed but not acknowledged is recoverable-from but not prune-authorising (spec §12.3 rule 5/6).
|
|
102
|
+
*/
|
|
103
|
+
acknowledged: boolean;
|
|
104
|
+
}
|
|
105
|
+
export interface JournalPruneReceipt {
|
|
106
|
+
/** Highest `step_seq` no longer retained. `-1` when nothing has ever been pruned. */
|
|
107
|
+
pruned_through_step_seq: number;
|
|
108
|
+
pruned_count: number;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The durable transaction capability (spec §9.1). Deliberately *not* part of `SessionLog`: a custom
|
|
112
|
+
* business-projection log must never be forced to masquerade as a transactional journal (§9.4).
|
|
113
|
+
* One class may implement both — `InMemorySessionLog` holds one rather than inheriting it — but the
|
|
114
|
+
* interfaces stay separate.
|
|
115
|
+
*
|
|
116
|
+
* Guarantees an implementation owes (spec §9.1):
|
|
117
|
+
* - strict ordering within an operation;
|
|
118
|
+
* - a failed CAS never overwrites;
|
|
119
|
+
* - record bytes preserved verbatim;
|
|
120
|
+
* - checkpoint pointer advances monotonically;
|
|
121
|
+
* - the checkpoint store verifies `covered_head` against `through_step_seq` but does **not** require
|
|
122
|
+
* it to still be the current transaction head (§22.14);
|
|
123
|
+
* - checkpoint pointer and prefix pruning have an explicit acknowledgement boundary.
|
|
124
|
+
*/
|
|
125
|
+
export interface KernelJournal {
|
|
126
|
+
/**
|
|
127
|
+
* Atomically append `record` iff the operation's head is exactly `expectedHead`.
|
|
128
|
+
*
|
|
129
|
+
* @param expectedHead `undefined` starts the chain (genesis; `record.step_seq` must be 0).
|
|
130
|
+
* @throws {JournalCasConflictError} head moved, or a genesis already exists.
|
|
131
|
+
* @throws {JournalIntegrityError} `step_seq` does not follow the head.
|
|
132
|
+
* @throws {JournalIoError} storage failure.
|
|
133
|
+
*/
|
|
134
|
+
compareAndAppend(operationId: string, expectedHead: string | undefined, record: JournalRecordInput): Promise<JournalAppendReceipt>;
|
|
135
|
+
/** The current head, or `undefined` when the operation has no records and no pruned anchor. */
|
|
136
|
+
head(operationId: string): Promise<JournalHead | undefined>;
|
|
137
|
+
/** Records with `step_seq >= fromStepSeq` (default: the whole retained chain), in chain order. */
|
|
138
|
+
readFrom(operationId: string, fromStepSeq?: number): Promise<JournalEntry[]>;
|
|
139
|
+
/**
|
|
140
|
+
* Records strictly after the record whose digest is `afterHead` — the digest-anchored cursor
|
|
141
|
+
* §9.1 names `records_after(operation_id, checkpoint_head)`. `undefined` returns everything retained.
|
|
142
|
+
*
|
|
143
|
+
* @throws {JournalIntegrityError} `afterHead` names no retained record and no pruned anchor.
|
|
144
|
+
*/
|
|
145
|
+
recordsAfter(operationId: string, afterHead?: string): Promise<JournalEntry[]>;
|
|
146
|
+
/**
|
|
147
|
+
* Atomically install `checkpoint` iff the operation's checkpoint pointer is exactly
|
|
148
|
+
* `previousCheckpointId`, and `coveredHead` is the record digest at `checkpoint.through_step_seq`.
|
|
149
|
+
*
|
|
150
|
+
* Per §22.14 this deliberately does **not** require `coveredHead` to still be the current
|
|
151
|
+
* transaction head: transactions appended after the candidate was taken stay as tail.
|
|
152
|
+
*
|
|
153
|
+
* @param previousCheckpointId `undefined` installs the operation's first checkpoint.
|
|
154
|
+
* @throws {JournalCasConflictError} the checkpoint pointer moved.
|
|
155
|
+
* @throws {JournalIntegrityError} `coveredHead`/`through_step_seq` disagree with the chain, or
|
|
156
|
+
* `through_step_seq` would move the pointer backwards.
|
|
157
|
+
*/
|
|
158
|
+
compareAndInstallCheckpoint(operationId: string, previousCheckpointId: string | undefined, coveredHead: string, checkpoint: CheckpointCandidate): Promise<InstalledCheckpoint>;
|
|
159
|
+
/** The highest-ordinal installed checkpoint, acknowledged or not. */
|
|
160
|
+
latestCheckpoint(operationId: string): Promise<InstalledCheckpoint | undefined>;
|
|
161
|
+
/**
|
|
162
|
+
* Record the durable acknowledgement that opens the prefix-reclamation boundary. Idempotent.
|
|
163
|
+
*
|
|
164
|
+
* @throws {JournalIntegrityError} no checkpoint with that id is installed.
|
|
165
|
+
*/
|
|
166
|
+
ackCheckpoint(operationId: string, checkpointId: string): Promise<InstalledCheckpoint>;
|
|
167
|
+
/**
|
|
168
|
+
* Reclaim the record prefix covered by the latest **acknowledged** checkpoint. A no-op while no
|
|
169
|
+
* checkpoint is acknowledged. The pruned boundary is retained as an anchor so `head()` and the
|
|
170
|
+
* next CAS still resolve on a fully-pruned chain.
|
|
171
|
+
*/
|
|
172
|
+
pruneAckedPrefix(operationId: string): Promise<JournalPruneReceipt>;
|
|
173
|
+
/**
|
|
174
|
+
* Durably stage one outbound WireEnvelope JSON for `operationId` until the matching record is
|
|
175
|
+
* append-acked (or the attempt is abandoned). Overwrites any previous pending envelope.
|
|
176
|
+
*
|
|
177
|
+
* Required by Phase 6 host cutover (adjudication 5e.3): retries must replay byte-identical
|
|
178
|
+
* envelopes — reminting `observed_at_ms` after a crash produces `DuplicateInputConflict`.
|
|
179
|
+
*/
|
|
180
|
+
stageOutboundEnvelope(operationId: string, envelopeJson: string): Promise<void>;
|
|
181
|
+
/** Read the staged outbound envelope, if any. */
|
|
182
|
+
readOutboundEnvelope(operationId: string): Promise<string | undefined>;
|
|
183
|
+
/** Clear the staged outbound envelope. Idempotent. */
|
|
184
|
+
clearOutboundEnvelope(operationId: string): Promise<void>;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* **Single-process dev/test implementation.**
|
|
188
|
+
*
|
|
189
|
+
* CAS is genuinely atomic here — every check-then-mutate below runs to completion without an
|
|
190
|
+
* intervening `await`, so no other task can interleave on a single-threaded runtime — but that
|
|
191
|
+
* atomicity ends at the process boundary. Two workers/isolates sharing "the same" journal do not
|
|
192
|
+
* exist: each has its own `Map`. Production hosts must supply a `KernelJournal` whose CAS is a real
|
|
193
|
+
* storage-layer primitive (spec §9.1); {@link DriverKernelJournal} over a durable
|
|
194
|
+
* {@link JournalStorageDriver} is the reference for that.
|
|
195
|
+
*/
|
|
196
|
+
export declare class InMemoryKernelJournal implements KernelJournal {
|
|
197
|
+
private readonly operations;
|
|
198
|
+
private state;
|
|
199
|
+
private headOf;
|
|
200
|
+
compareAndAppend(operationId: string, expectedHead: string | undefined, record: JournalRecordInput): Promise<JournalAppendReceipt>;
|
|
201
|
+
head(operationId: string): Promise<JournalHead | undefined>;
|
|
202
|
+
readFrom(operationId: string, fromStepSeq?: number): Promise<JournalEntry[]>;
|
|
203
|
+
recordsAfter(operationId: string, afterHead?: string): Promise<JournalEntry[]>;
|
|
204
|
+
compareAndInstallCheckpoint(operationId: string, previousCheckpointId: string | undefined, coveredHead: string, checkpoint: CheckpointCandidate): Promise<InstalledCheckpoint>;
|
|
205
|
+
latestCheckpoint(operationId: string): Promise<InstalledCheckpoint | undefined>;
|
|
206
|
+
ackCheckpoint(operationId: string, checkpointId: string): Promise<InstalledCheckpoint>;
|
|
207
|
+
pruneAckedPrefix(operationId: string): Promise<JournalPruneReceipt>;
|
|
208
|
+
stageOutboundEnvelope(operationId: string, envelopeJson: string): Promise<void>;
|
|
209
|
+
readOutboundEnvelope(operationId: string): Promise<string | undefined>;
|
|
210
|
+
clearOutboundEnvelope(operationId: string): Promise<void>;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The storage primitive {@link DriverKernelJournal} needs from its host. This is the WASM SDK's
|
|
214
|
+
* substitute for Node's POSIX `link(2)`: this package runs where there is no filesystem, so the one
|
|
215
|
+
* operation that cannot be emulated in userspace — the atomic claim — is injected.
|
|
216
|
+
*
|
|
217
|
+
* **Driver contract.** Everything below is load-bearing; a driver that violates any of it turns
|
|
218
|
+
* §15.2 ("a CAS conflict never overwrites the journal") and §25.12 into unenforceable claims.
|
|
219
|
+
*
|
|
220
|
+
* 1. **`createIfAbsent` is THE atomic decision.** It must claim `key` and publish `bytes` in one
|
|
221
|
+
* indivisible storage-layer operation — R2/S3 `If-None-Match: *`, a Durable Object transaction,
|
|
222
|
+
* a KV compare-and-set, `INSERT ... ON CONFLICT DO NOTHING`, OPFS create-exclusive-then-rename.
|
|
223
|
+
* A read-then-write pair in the driver only moves the race; it does not decide it. The journal
|
|
224
|
+
* performs a *pre-check* before calling this, but that pre-check can only reject an append the
|
|
225
|
+
* driver would have accepted — never accept one the driver would have rejected.
|
|
226
|
+
* 2. **Keys are the collision domain, and may contain only CAS-precondition-derived identity**
|
|
227
|
+
* (spec Task 8b implementation correction (a)). {@link DriverKernelJournal} names records by
|
|
228
|
+
* zero-padded `step_seq` and checkpoints by zero-padded ordinal, both of which two racing
|
|
229
|
+
* writers *agree on* because both are derived from the predecessor they raced against. A driver
|
|
230
|
+
* must never rewrite, salt, or namespace keys per-writer: if racers end up at different keys,
|
|
231
|
+
* both writes succeed and the chain forks.
|
|
232
|
+
* 3. **Publication is all-or-nothing** (correction (b)). A partially written value must never
|
|
233
|
+
* become readable at `key`; stage first, publish second. A crash mid-write may leave residue
|
|
234
|
+
* under other keys, but never a half-value at a claimed key.
|
|
235
|
+
* 4. **`put` overwrites; use it only where the value is monotone.** The journal uses it solely for
|
|
236
|
+
* the pruned anchor `{through_step_seq, covered_head}`, which only ever moves forward, so a
|
|
237
|
+
* lost update is a stale-but-valid anchor rather than a corruption.
|
|
238
|
+
* 5. **`read` returns bytes verbatim**, and `undefined` (not a throw) for an absent key.
|
|
239
|
+
* 6. **`list(prefix)` returns every key beginning with `prefix`**, in any order. The journal
|
|
240
|
+
* ignores keys that do not match its own naming rule, so unrelated residue is harmless.
|
|
241
|
+
* 7. **Failures throw.** Any thrown error is wrapped as {@link JournalIoError}: the journal state
|
|
242
|
+
* is unknown, which is neither a conflict nor a corruption.
|
|
243
|
+
*/
|
|
244
|
+
export interface JournalStorageDriver {
|
|
245
|
+
/**
|
|
246
|
+
* Atomically publish `bytes` at `key` iff `key` is absent.
|
|
247
|
+
*
|
|
248
|
+
* @returns `false` when the key was already taken — i.e. a lost CAS race. An existing value must
|
|
249
|
+
* be left byte-identical.
|
|
250
|
+
*/
|
|
251
|
+
createIfAbsent(key: string, bytes: Uint8Array): Promise<boolean>;
|
|
252
|
+
/** The bytes at `key`, or `undefined` when absent. */
|
|
253
|
+
read(key: string): Promise<Uint8Array | undefined>;
|
|
254
|
+
/** Every key beginning with `prefix`, in any order. */
|
|
255
|
+
list(prefix: string): Promise<string[]>;
|
|
256
|
+
/** Atomically create-or-replace `key`. Only used for the monotone pruned anchor. */
|
|
257
|
+
put(key: string, bytes: Uint8Array): Promise<void>;
|
|
258
|
+
/** Remove `key`. Removing an absent key is not an error. */
|
|
259
|
+
remove(key: string): Promise<void>;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* **Single-process reference driver.** Backed by one `Map`, so `createIfAbsent` is atomic in the
|
|
263
|
+
* only sense a single isolate can offer: the check and the insert run with no `await` between them,
|
|
264
|
+
* so no other task interleaves. It is a complete, spec-conformant driver *within one isolate* — use
|
|
265
|
+
* it for dev, tests, and ephemeral Workers; use a durable driver anywhere state must outlive the
|
|
266
|
+
* isolate or be shared with another one.
|
|
267
|
+
*/
|
|
268
|
+
export declare class InMemoryJournalDriver implements JournalStorageDriver {
|
|
269
|
+
private readonly entries;
|
|
270
|
+
createIfAbsent(key: string, bytes: Uint8Array): Promise<boolean>;
|
|
271
|
+
read(key: string): Promise<Uint8Array | undefined>;
|
|
272
|
+
list(prefix: string): Promise<string[]>;
|
|
273
|
+
put(key: string, bytes: Uint8Array): Promise<void>;
|
|
274
|
+
remove(key: string): Promise<void>;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* **Durable reference implementation** of `KernelJournal` over a host-injected
|
|
278
|
+
* {@link JournalStorageDriver} (spec Task 8b) — the WASM SDK's counterpart to Node's
|
|
279
|
+
* `FileKernelJournal`.
|
|
280
|
+
*
|
|
281
|
+
* The atomicity primitive is `driver.createIfAbsent`. Content is published complete, under a name
|
|
282
|
+
* derived *only* from the CAS precondition, so a crash can never leave a half-written record and
|
|
283
|
+
* two racing writers can never land on different names.
|
|
284
|
+
*
|
|
285
|
+
* **Why a record's key is `<step_seq>.rec` and contains no digest.** The key *is* the collision
|
|
286
|
+
* domain. Two writers racing on the same head both compute the same next `step_seq`, so they
|
|
287
|
+
* contend for one key and exactly one wins. Folding a per-writer value (the new record's digest)
|
|
288
|
+
* into the key would give the racers *different* keys — both claims would succeed and the chain
|
|
289
|
+
* would fork. Only the predecessor-determined part of the identity may appear in the key.
|
|
290
|
+
*
|
|
291
|
+
* The pre-claim head check is not a TOCTOU hole: it can only *reject* an append the driver would
|
|
292
|
+
* have accepted (a stale `expectedHead` whose `step_seq` slot happens to be free), never accept one
|
|
293
|
+
* the driver would have rejected. Every acceptance is still decided by the atomic claim.
|
|
294
|
+
*
|
|
295
|
+
* Checkpoint installs use the same primitive on a separate ordinal space (`<ordinal>.ckpt`), so two
|
|
296
|
+
* writers installing on the same predecessor also contend for one key.
|
|
297
|
+
*/
|
|
298
|
+
export declare class DriverKernelJournal implements KernelJournal {
|
|
299
|
+
private readonly driver;
|
|
300
|
+
private readonly prefix;
|
|
301
|
+
constructor(driver: JournalStorageDriver, prefix?: string);
|
|
302
|
+
private operationPrefix;
|
|
303
|
+
private recordsPrefix;
|
|
304
|
+
private checkpointsPrefix;
|
|
305
|
+
private prunedKey;
|
|
306
|
+
private outboundKey;
|
|
307
|
+
/** Every driver call funnels through these three so a driver fault is always a `JournalIoError`. */
|
|
308
|
+
private claim;
|
|
309
|
+
private load;
|
|
310
|
+
private names;
|
|
311
|
+
/** Sorted `step_seq` values of the retained records. Anything not matching the rule is residue. */
|
|
312
|
+
private recordSeqs;
|
|
313
|
+
private readRecord;
|
|
314
|
+
private prunedAnchor;
|
|
315
|
+
head(operationId: string): Promise<JournalHead | undefined>;
|
|
316
|
+
compareAndAppend(operationId: string, expectedHead: string | undefined, record: JournalRecordInput): Promise<JournalAppendReceipt>;
|
|
317
|
+
readFrom(operationId: string, fromStepSeq?: number): Promise<JournalEntry[]>;
|
|
318
|
+
recordsAfter(operationId: string, afterHead?: string): Promise<JournalEntry[]>;
|
|
319
|
+
/** Sorted ordinals of installed checkpoints. */
|
|
320
|
+
private checkpointOrdinals;
|
|
321
|
+
private ackedOrdinals;
|
|
322
|
+
private readCheckpoint;
|
|
323
|
+
latestCheckpoint(operationId: string): Promise<InstalledCheckpoint | undefined>;
|
|
324
|
+
compareAndInstallCheckpoint(operationId: string, previousCheckpointId: string | undefined, coveredHead: string, checkpoint: CheckpointCandidate): Promise<InstalledCheckpoint>;
|
|
325
|
+
ackCheckpoint(operationId: string, checkpointId: string): Promise<InstalledCheckpoint>;
|
|
326
|
+
pruneAckedPrefix(operationId: string): Promise<JournalPruneReceipt>;
|
|
327
|
+
/** The anchor only ever moves forward, so an overwriting `put` is the right primitive. */
|
|
328
|
+
private writeAnchor;
|
|
329
|
+
stageOutboundEnvelope(operationId: string, envelopeJson: string): Promise<void>;
|
|
330
|
+
readOutboundEnvelope(operationId: string): Promise<string | undefined>;
|
|
331
|
+
clearOutboundEnvelope(operationId: string): Promise<void>;
|
|
332
|
+
}
|