@parcha/pi-durable-disk 0.0.0-stage → 0.1.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +329 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +695 -2
  5. package/bin/archil-scoped +165 -0
  6. package/dist/app.d.ts +40 -0
  7. package/dist/app.js +47 -0
  8. package/dist/claim.d.ts +222 -0
  9. package/dist/claim.js +722 -0
  10. package/dist/cli.d.ts +64 -0
  11. package/dist/cli.js +664 -0
  12. package/dist/commands.d.ts +67 -0
  13. package/dist/commands.js +299 -0
  14. package/dist/confine.d.ts +91 -0
  15. package/dist/confine.js +341 -0
  16. package/dist/env.d.ts +129 -0
  17. package/dist/env.js +608 -0
  18. package/dist/errors.d.ts +47 -0
  19. package/dist/errors.js +55 -0
  20. package/dist/fork.d.ts +41 -0
  21. package/dist/fork.js +181 -0
  22. package/dist/hosts/daytona-launch.d.ts +52 -0
  23. package/dist/hosts/daytona-launch.js +270 -0
  24. package/dist/hosts/daytona.d.ts +147 -0
  25. package/dist/hosts/daytona.js +368 -0
  26. package/dist/hosts/docker.d.ts +103 -0
  27. package/dist/hosts/docker.js +362 -0
  28. package/dist/hosts/local-host.d.ts +71 -0
  29. package/dist/hosts/local-host.js +356 -0
  30. package/dist/index.d.ts +16 -0
  31. package/dist/index.js +17 -0
  32. package/dist/lease.d.ts +7 -0
  33. package/dist/lease.js +6 -0
  34. package/dist/local-run.d.ts +10 -0
  35. package/dist/local-run.js +63 -0
  36. package/dist/park.d.ts +92 -0
  37. package/dist/park.js +229 -0
  38. package/dist/run.d.ts +290 -0
  39. package/dist/run.js +785 -0
  40. package/dist/serve.d.ts +101 -0
  41. package/dist/serve.js +393 -0
  42. package/dist/status.d.ts +123 -0
  43. package/dist/status.js +264 -0
  44. package/dist/store.d.ts +103 -0
  45. package/dist/store.js +217 -0
  46. package/dist/supervise.d.ts +340 -0
  47. package/dist/supervise.js +528 -0
  48. package/dist/watchdog.d.ts +39 -0
  49. package/dist/watchdog.js +137 -0
  50. package/package.json +87 -4
package/dist/park.js ADDED
@@ -0,0 +1,229 @@
1
+ import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
2
+ import { FencedError } from "./errors.js";
3
+ /**
4
+ * The park target of a Harness the host opened over `lease`: the wake goes through `lease.setStatus`, and the release
5
+ * closes the Harness (running work is aborted with no outcome recorded) and then releases the lease. A rejecting close
6
+ * is a fence, as in `DurableRun.release`.
7
+ */
8
+ export function leaseParkTarget(lease, harness, context = BACKGROUND_CONTEXT) {
9
+ return {
10
+ harness,
11
+ get fenced() {
12
+ return lease.fenced;
13
+ },
14
+ setStatus: (status, detail, options) => lease.setStatus(status, detail, options),
15
+ async release() {
16
+ try {
17
+ await harness.close(context);
18
+ }
19
+ catch (error) {
20
+ throw lease.fence(error instanceof FencedError ? error : new FencedError("closing the Harness failed, so the claim cannot be trusted", { cause: error }));
21
+ }
22
+ await lease.release();
23
+ },
24
+ };
25
+ }
26
+ /** localHost's threshold, far above twice its start (about 1.5 s); Cloudflare and Rivet park pi after 60 s too. */
27
+ export const LOCAL_PARK_THRESHOLD_MS = 60_000;
28
+ /**
29
+ * The deadline of a task that is only sleeping: `until` in pi's retry phase (generation and compaction), `pollAt` in
30
+ * its deferred poll phase. This is pi-durable's internal checkpoint shape, the same in 1.0.4 and 1.1.0
31
+ * (harness/generation.js, compaction.js), read here and nowhere else. Any other shape is no deadline, which counts as
32
+ * busy, so a pi that changes the shape keeps instances up rather than parking them mid-work. pi#10325 asks for this as
33
+ * a contract.
34
+ */
35
+ export function waitDeadline(checkpoint) {
36
+ if (typeof checkpoint !== "object" || checkpoint === null || Array.isArray(checkpoint))
37
+ return undefined;
38
+ const at = checkpoint.phase === "retry" ? checkpoint.until : checkpoint.phase === "poll" ? checkpoint.pollAt : undefined;
39
+ return typeof at === "number" && Number.isFinite(at) ? at : undefined;
40
+ }
41
+ /**
42
+ * Whether the run's live work needs this instance. A task that runs or is ready to run does, unless it only sleeps
43
+ * until a deadline more than `thresholdMs` away. Waiting and completing tasks depend on other tasks, which decide; a
44
+ * blocked task (no registered definition can take it) never runs, so it never keeps the instance up. A closing
45
+ * Harness, a task state this code does not know, and unsettled submissions with no live task all count as busy.
46
+ */
47
+ export function busyState(inspection, now, thresholdMs) {
48
+ if (inspection.scheduling === "closing")
49
+ return { kind: "busy" };
50
+ let until;
51
+ let live = false;
52
+ for (const task of inspection.tasks) {
53
+ const kind = task.state.kind;
54
+ if (kind === "waiting" || kind === "completing" || kind === "blocked")
55
+ continue;
56
+ if (kind !== "running" && kind !== "ready")
57
+ return { kind: "busy" };
58
+ live = true;
59
+ const deadline = waitDeadline(task.record.state.checkpoint);
60
+ if (deadline === undefined || deadline - now <= thresholdMs)
61
+ return { kind: "busy" };
62
+ until = Math.min(until ?? deadline, deadline);
63
+ }
64
+ if (until !== undefined)
65
+ return { kind: "waiting", until };
66
+ if (!live && inspection.tasks.length === 0 && inspection.submissions.length > 0)
67
+ return { kind: "busy" };
68
+ return { kind: "idle" };
69
+ }
70
+ /** The default wake: run.json `sleeping` with `wakeAt` (null: idle, started on demand), written durably. */
71
+ export function recordWake(run, at) {
72
+ return run.setStatus("sleeping", { reason: at === null ? "idle" : "waiting" }, { wakeAt: at === null ? null : new Date(at).toISOString() });
73
+ }
74
+ /**
75
+ * Watch `run` and park it when its work allows. Checks after every commit (pi's listener must not call pi,
76
+ * so the check runs after it returns), on `check()`, and when an idle period ends. Parking stops admission, checks
77
+ * once more, records the wake, then releases: the wake is always written before the claim goes.
78
+ */
79
+ export function watchParking(run, options) {
80
+ const now = options.now ?? Date.now;
81
+ const wake = options.wake ?? recordWake;
82
+ const log = options.log ?? (() => { });
83
+ const idleMs = options.idleMs ?? Number.POSITIVE_INFINITY;
84
+ let checking = false;
85
+ let dirty = false;
86
+ let stopped = false;
87
+ let idleSince;
88
+ let idleTimer;
89
+ let attempt;
90
+ const settled = {};
91
+ settled.promise = new Promise((resolve, reject) => Object.assign(settled, { resolve, reject }));
92
+ const classify = async () => {
93
+ const inspection = await run.harness.inspect(BACKGROUND_CONTEXT);
94
+ const blocked = inspection.tasks.filter((t) => t.state.kind === "blocked");
95
+ return { state: busyState(inspection, now(), options.thresholdMs), blocked };
96
+ };
97
+ const armIdle = (state) => {
98
+ if (state.kind !== "idle" || !Number.isFinite(idleMs)) {
99
+ idleSince = undefined;
100
+ clearTimeout(idleTimer);
101
+ return false;
102
+ }
103
+ idleSince ??= now();
104
+ const left = idleSince + idleMs - now();
105
+ if (left <= 0)
106
+ return true;
107
+ clearTimeout(idleTimer);
108
+ // Not unref'd: a park that is due is pending work, and nothing else may be keeping the process alive.
109
+ idleTimer = setTimeout(check, left);
110
+ return false;
111
+ };
112
+ const park = async () => {
113
+ const undo = options.quiesce?.() ?? (() => { });
114
+ const { state, blocked } = await classify();
115
+ const due = state.kind === "waiting" || (state.kind === "idle" && armIdle(state));
116
+ if (stopped || !due || options.keepAwake?.()) {
117
+ undo();
118
+ return undefined;
119
+ }
120
+ for (const t of blocked) {
121
+ log("blocked task does not keep the instance up", { task: t.record.id, kind: t.record.kind, reason: t.state.kind === "blocked" ? t.state.reason : null });
122
+ }
123
+ const at = state.kind === "waiting" ? state.until : null;
124
+ try {
125
+ await wake(run, at);
126
+ }
127
+ catch (error) {
128
+ undo();
129
+ // A fence already ends the instance; anything else leaves the wait to pi's own timer.
130
+ if (!(error instanceof FencedError) && !run.fenced)
131
+ log("wake not recorded; staying up through the wait", { wakeAt: at, error: error.message ?? String(error) });
132
+ return undefined;
133
+ }
134
+ stopped = true;
135
+ unsubscribe();
136
+ clearTimeout(idleTimer);
137
+ try {
138
+ await run.release();
139
+ }
140
+ catch (error) {
141
+ settled.reject(error);
142
+ throw error;
143
+ }
144
+ const result = { wakeAt: at, blocked: blocked.length };
145
+ settled.resolve(result);
146
+ return result;
147
+ };
148
+ async function loop() {
149
+ if (checking) {
150
+ dirty = true;
151
+ return;
152
+ }
153
+ checking = true;
154
+ try {
155
+ do {
156
+ dirty = false;
157
+ if (stopped || run.fenced)
158
+ return;
159
+ if (options.keepAwake?.()) {
160
+ // An open request is activity: an idle period starts again after it.
161
+ armIdle({ kind: "busy" });
162
+ return;
163
+ }
164
+ const { state } = await classify();
165
+ if (stopped)
166
+ return;
167
+ if (state.kind === "waiting" || armIdle(state)) {
168
+ attempt = park();
169
+ if (await attempt)
170
+ return;
171
+ }
172
+ } while (dirty && !stopped);
173
+ }
174
+ catch (error) {
175
+ if (!run.fenced)
176
+ log("park check failed", { error: error.message ?? String(error) });
177
+ }
178
+ finally {
179
+ checking = false;
180
+ }
181
+ }
182
+ function check() {
183
+ queueMicrotask(() => void loop());
184
+ }
185
+ const unsubscribe = run.harness.subscribeCommits(() => check());
186
+ settled.promise.catch(() => { });
187
+ check();
188
+ return {
189
+ parked: settled.promise,
190
+ check,
191
+ async stop() {
192
+ const inFlight = attempt;
193
+ stopped = true;
194
+ unsubscribe();
195
+ clearTimeout(idleTimer);
196
+ return inFlight ? inFlight.catch(() => undefined) : undefined;
197
+ },
198
+ };
199
+ }
200
+ /**
201
+ * Wait until no task needs this instance, or until `deadline`, rechecking after every commit.
202
+ * Any sleeping task counts as waiting here, however short its wait: closing ends the sleep and the next open resumes it.
203
+ * Returns the last classification: `busy` means the deadline passed with work running, which the release interrupts
204
+ * and the next open resumes (safe tools rerun, unsafe ones report the interruption).
205
+ */
206
+ export async function drain(run, options) {
207
+ const now = options.now ?? Date.now;
208
+ let changed = () => { };
209
+ let timedOut = false;
210
+ const unsubscribe = run.harness.subscribeCommits(() => changed());
211
+ const timer = setTimeout(() => {
212
+ timedOut = true;
213
+ changed();
214
+ }, Math.max(0, options.deadline - Date.now()));
215
+ try {
216
+ for (;;) {
217
+ // Armed before the check, so a commit during the check is not missed.
218
+ const next = new Promise((resolve) => (changed = resolve));
219
+ const state = busyState(await run.harness.inspect(BACKGROUND_CONTEXT), now(), 0);
220
+ if (state.kind !== "busy" || timedOut)
221
+ return state;
222
+ await next;
223
+ }
224
+ }
225
+ finally {
226
+ clearTimeout(timer);
227
+ unsubscribe();
228
+ }
229
+ }
package/dist/run.d.ts ADDED
@@ -0,0 +1,290 @@
1
+ import type { Context, JsonValue } from "@earendil-works/chord";
2
+ import { Harness } from "@earendil-works/pi-durable";
3
+ import type { HarnessOptions, Seq, Storage, ToolRegistration } from "@earendil-works/pi-durable";
4
+ import type { SqliteDatabase, SqliteExecutor } from "@earendil-works/pi-durable/storage/sqlite";
5
+ import { type AcquireOptions, type ArchilHost, type Claim, type RunRef } from "./claim.js";
6
+ import { FencedError, PdaError } from "./errors.js";
7
+ import { type ArchilStore, type Profile } from "./store.js";
8
+ import { type ClaimDir, type PersistRecord, type RunRecord, type RunStatus } from "./status.js";
9
+ /** The same-host owner lock under the run's root. */
10
+ export declare const OWNER_LOCK = "owner.lock";
11
+ /** pi's store under the claim's `store/` directory. */
12
+ export declare const STORE_FILE = "run.sqlite";
13
+ /** EX_DATAERR: the run's data refuses to open (a store behind its seal, an unreadable `run.json`); a restart cannot help. */
14
+ export declare const EXIT_DATAERR = 65;
15
+ /** EX_SOFTWARE: this package cannot read the head of pi's store (pi's schema moved); a restart cannot help. */
16
+ export declare const EXIT_SOFTWARE = 70;
17
+ export type RunErrorCode = "INVALID_ARGUMENT" | "STORE_HEAD_UNREADABLE" | "RUN_RELEASED";
18
+ /**
19
+ * Bad options, a call on a released run, or a store whose head cannot be read (STORE_HEAD_UNREADABLE: the run is marked
20
+ * `failed` and released, exit 70).
21
+ */
22
+ export declare class RunError extends PdaError {
23
+ constructor(code: RunErrorCode, message: string, options?: {
24
+ cause?: unknown;
25
+ });
26
+ }
27
+ /**
28
+ * The store's last committed sequence is below the `sealedSeq` the last clean release wrote: an acknowledged commit is
29
+ * missing. The run is refused before pi commits anything, and marked `failed` in `run.json`. Exit 65.
30
+ */
31
+ export declare class StoreBehindSealError extends PdaError {
32
+ readonly sealedSeq: number;
33
+ /** The store's last committed sequence. */
34
+ readonly head: number;
35
+ constructor(file: string, sealedSeq: number, head: number);
36
+ }
37
+ /**
38
+ * The self-fence rule: the last heartbeat that succeeded started longer ago than the lease expiry minus its margin. `by`
39
+ * names who saw it: the main thread's timer, or the watchdog thread (which had already killed the commands).
40
+ */
41
+ export declare class LeaseLapsedError extends FencedError {
42
+ readonly sinceMs: number;
43
+ readonly by: "timer" | "watchdog";
44
+ constructor(sinceMs: number, limitMs: number, by?: "timer" | "watchdog");
45
+ }
46
+ /** What the run needs of its environment: pi's `env` option plus a way to kill every command it started. */
47
+ export type RunEnv = NonNullable<HarnessOptions["env"]> & {
48
+ cleanup(context: Context): Promise<void>;
49
+ };
50
+ export interface LeaseOptions {
51
+ /** Heartbeat period; default 20 s. */
52
+ readonly heartbeatMs?: number;
53
+ /** The lease the supervisor grants; default 90 s. */
54
+ readonly expiryMs?: number;
55
+ /** The instance fences itself at `expiryMs - marginMs` after the last successful heartbeat started; default 15 s. */
56
+ readonly marginMs?: number;
57
+ /** How often the self-fence timer looks; default 1 s. */
58
+ readonly checkMs?: number;
59
+ }
60
+ export type OpenStep = "acquire" | "owner-lock" | "run.json" | "heartbeat" | "layout" | "commands" | "store" | "seal" | "harness" | "live" | "before-resume" | "resume";
61
+ export type ReleaseStep = "close" | "cleanup" | "barrier" | "seal" | "unlock" | "unmount";
62
+ export interface OpenRunLeaseOptions {
63
+ /**
64
+ * The mount token from the host driver (reusable, 24 h); travels only on the archil wrapper's stdin, never in an
65
+ * environment. Required to mount an Archil disk (the default `acquire`); a run whose `acquire` is replaced (a local
66
+ * directory: `localRun`) needs none, and its `acquire` is handed an empty token.
67
+ */
68
+ readonly mountToken?: string;
69
+ /** Default /mnt/archil. */
70
+ readonly mountRoot?: string;
71
+ /** Mount with `--force`: the takeover fallback when the supervisor could not revoke. */
72
+ readonly force?: boolean;
73
+ readonly host?: ArchilHost;
74
+ /**
75
+ * The host driver's handle, written as run.json's `holder` with this process's pid and `since` added; `driver`
76
+ * defaults to `local`, `host` and `bootId` to this machine's. The supervisor hands it back to the driver to stop it.
77
+ */
78
+ readonly holder?: Readonly<Record<string, JsonValue>>;
79
+ readonly lease?: LeaseOptions;
80
+ /**
81
+ * The instance runs in a cgroup of its own (a systemd unit, a container). When the lease lapses, the watchdog then
82
+ * kills every other process in that cgroup too, beyond the instance's direct children and their process groups.
83
+ * Leave it off in a shared cgroup (a login session): its other processes are not the run's.
84
+ */
85
+ readonly ownCgroup?: boolean;
86
+ /**
87
+ * Kills every command the app started (pi starts each as its own process group, so the instance's own group does not
88
+ * reach them). Runs first on any fence, before `onFenced`, and as the first step of `release()`. Default: nothing.
89
+ */
90
+ readonly cleanup?: (context: Context) => Promise<void>;
91
+ /** Called once, after `cleanup`, on any fence. Default (and if it throws): a line on stderr, exit 75. */
92
+ readonly onFenced?: (error: FencedError) => void;
93
+ readonly context?: Context;
94
+ /** Each lifecycle step as it completes, with its duration. */
95
+ readonly onStep?: (step: OpenStep | ReleaseStep, ms: number) => void;
96
+ /** Replaceable for tests: how the claim is taken (default `acquire`). */
97
+ readonly acquire?: (options: AcquireOptions) => Promise<Claim>;
98
+ /** Replaceable for fault injection: how `run.json` is written (default `persistRecord`). */
99
+ readonly persist?: PersistRecord;
100
+ /**
101
+ * Replaceable for tests: how the claim's root is held open (default `openClaimDir(root)`, which requires the root to be
102
+ * the mount point of an Archil mount). A run root on a local directory passes `(root) => openClaimDir(root, { fstype: null })`.
103
+ */
104
+ readonly claimDir?: (root: string) => Promise<ClaimDir>;
105
+ /** Replaceable for tests: the monotonic clock the self-fence measures with, in ms (default `performance.now`). */
106
+ readonly clock?: () => number;
107
+ }
108
+ export interface OpenDurableRunOptions<Tool extends ToolRegistration = ToolRegistration> extends Omit<OpenRunLeaseOptions, "cleanup"> {
109
+ /** Default `exclusive`. */
110
+ readonly profile?: Profile;
111
+ /** pi's Harness options; the registry holds every extension before the Harness opens (recovered tasks need them). */
112
+ readonly harness: Omit<HarnessOptions<Tool>, "env">;
113
+ /** The run's environment, built on the claim; default `archilEnv(claim)`. Its `cleanup` is the lease's. */
114
+ readonly env?: (claim: Claim) => RunEnv;
115
+ /** Wraps the raw database inside the store's fence (instrumentation, fault injection). */
116
+ readonly decorateStore?: (database: SqliteDatabase) => SqliteDatabase;
117
+ /**
118
+ * Awaited once the Harness is open (its registry installed) and the run is live, before the Harness resumes: no
119
+ * resumed task takes a step until it settles, and what it admits (a submission, a write) is committed before anything
120
+ * a resumed task's steps commit. A host reconciles here what a resumed tool depends on (a browser, a mount). A rejection
121
+ * fails the open as any step does (`before-resume`), and the claim is released.
122
+ */
123
+ readonly beforeResume?: (run: BeforeResume) => void | Promise<void>;
124
+ }
125
+ /**
126
+ * What `beforeResume` sees: the open Harness, not yet resumed, the open's context, the incarnation it belongs to, and the
127
+ * store the Harness was opened over with its head (a recorder snapshots every document at `head` here).
128
+ */
129
+ export interface BeforeResume {
130
+ readonly harness: Harness;
131
+ readonly context: Context;
132
+ readonly generation: number;
133
+ readonly record: RunRecord;
134
+ readonly claim: RunClaim;
135
+ /** The store the Harness was opened over, read-only: four reads on an object of their own, and no way to a write. */
136
+ readonly storage: StoreReads;
137
+ /**
138
+ * The store's last committed sequence once the Harness is open, read just before the hook runs. Until the hook commits
139
+ * something, the store's current state is its state at `head`: a document that keeps no history (`latest`, and session
140
+ * and task documents) reads only at "current", a `rewindable` one at `head` too.
141
+ */
142
+ readonly head: Seq;
143
+ }
144
+ /** The reads of a store that `beforeResume` gets. */
145
+ export type StoreReads = Pick<Storage, "scanConversations" | "scanTasks" | "scanDocuments" | "document">;
146
+ /** The claim as the app sees it: paths, the fenced flag, and a barrier whose failure fences the run. */
147
+ export interface RunClaim {
148
+ readonly ref: RunRef;
149
+ readonly disk: string;
150
+ readonly root: string;
151
+ readonly work: string;
152
+ readonly store: string;
153
+ readonly fenced: boolean;
154
+ /** `archil sync` of the mount: the workspace is durable before a result commits and before a terminal status; a failure fences the run. */
155
+ barrier(): Promise<void>;
156
+ }
157
+ /**
158
+ * The claim's lifecycle for an app that owns its store and Harness (`openDurableRun` without the store and the Harness). Open
159
+ * order, all done by `openRunLease`: acquire and verify the claim, take the owner lock, write `run.json` (`running`,
160
+ * generation + 1, this holder, a heartbeat, the previous seal kept), start the heartbeat and the self-fence (a timer and
161
+ * a watchdog thread), create `store/`, `work/` and `tmp/`. The app then opens its store, calls `checkSeal` before
162
+ * anything commits, opens its Harness over `observe(storage)`, and calls `live()`. Once fenced, every member that
163
+ * writes rejects with the first fence; nothing is retried.
164
+ */
165
+ export interface RunLease {
166
+ readonly ref: RunRef;
167
+ readonly claim: RunClaim;
168
+ /** This incarnation's generation in `run.json` (the previous one + 1). */
169
+ readonly generation: number;
170
+ /** `run.json` as last written. */
171
+ readonly record: RunRecord;
172
+ /** The seal the previous incarnation's clean release wrote; null on a first run and after a crash. */
173
+ readonly sealedSeq: number | null;
174
+ readonly fenced: boolean;
175
+ /**
176
+ * The seal check on the app's store, before anything commits: the store's head (`storeHead`) below
177
+ * `sealedSeq` is refused with StoreBehindSealError (65), and a head that cannot be read with RunError
178
+ * STORE_HEAD_UNREADABLE (70). Either way `run.json` is marked `failed` first, keeping the seal, and the app then calls
179
+ * `abandon` (never `release`, which would seal the lower head).
180
+ */
181
+ checkSeal(store: StoreHeadSource): Promise<void>;
182
+ /**
183
+ * `storage` reporting to the lease: every resolved commit's sequence counts toward the seal `release()` writes; a
184
+ * commit is refused once the watchdog found the lease lapsed, and one stuck meanwhile ends in the fence; a FencedError
185
+ * from a commit fences the lease. Wrap every Storage a Harness of the run opens over.
186
+ */
187
+ observe(storage: Storage): Storage;
188
+ /** The store is open and checked: clear the seal in `run.json` (a heartbeat write). */
189
+ live(): Promise<void>;
190
+ /**
191
+ * Fence the run, once: refuse every later `run.json` write (one in flight never renames), stop the timers and the
192
+ * watchdog, mark the claim fenced, all synchronously; then `cleanup`, then `onFenced`. Returns the first fence. The
193
+ * app's store facade calls it from its `onFenced`.
194
+ */
195
+ fence(error: FencedError): FencedError;
196
+ /** Resolves once a fence's cleanup and `onFenced` ran (when `onFenced` returns). */
197
+ fenceSettled(): Promise<void>;
198
+ /** Write a status transition. `done` and `failed` run the barrier first, so a terminal status never names workspace files that are not durable. */
199
+ setStatus(status: RunStatus, detail?: JsonValue | null, options?: {
200
+ readonly wakeAt?: string | null;
201
+ }): Promise<void>;
202
+ /** `archil sync` of the mount: the workspace is durable before a result commits and before a terminal status; a failure fences. */
203
+ barrier(): Promise<void>;
204
+ /**
205
+ * After the app closed every Harness and store connection: `cleanup`, barrier, seal `run.json` with the store's last
206
+ * sequence and the final status (`running` seals as `paused`), release the owner lock, unmount. Heartbeats continue
207
+ * until the seal and the self-fence past it, so a release that hangs still ends in exit 75. A failed barrier is a
208
+ * fence: nothing is sealed.
209
+ */
210
+ release(): Promise<void>;
211
+ /**
212
+ * Undo an open that failed after `openRunLease` (the app's store, the seal, its Harness): nothing is sealed. A fence
213
+ * goes to the fence path. Otherwise the timers stop, pending writes settle, `close` (the app closing what it opened)
214
+ * runs, the owner lock is released and the claim unmounted, except after a HeldError, whose mount another process on
215
+ * this host holds.
216
+ */
217
+ abandon(error: unknown, close?: () => Promise<void>): Promise<void>;
218
+ }
219
+ export interface DurableRun {
220
+ readonly ref: RunRef;
221
+ /** pi-durable's Harness, resumed. */
222
+ readonly harness: Harness;
223
+ readonly claim: RunClaim;
224
+ readonly env: RunEnv;
225
+ readonly store: ArchilStore;
226
+ readonly generation: number;
227
+ /** `run.json` as last written. */
228
+ readonly record: RunRecord;
229
+ readonly fenced: boolean;
230
+ /** Write a status transition. `done` and `failed` run the barrier first, so a terminal status never names workspace files that are not durable. */
231
+ setStatus(status: RunStatus, detail?: JsonValue | null, options?: {
232
+ readonly wakeAt?: string | null;
233
+ }): Promise<void>;
234
+ /**
235
+ * Close the Harness (running work is aborted with no outcome recorded, so the next open reports it interrupted),
236
+ * kill the run's commands, barrier, seal `run.json` with the store's last sequence and the final status (`running`
237
+ * seals as `paused`), release the owner lock, unmount. A rejecting close or barrier is a fence: nothing is sealed.
238
+ */
239
+ release(): Promise<void>;
240
+ }
241
+ export interface OwnerLock {
242
+ readonly file: string;
243
+ /** Drop the lock. An error is a fence (`ownWriteFenced`). */
244
+ release(): void;
245
+ }
246
+ /**
247
+ * Take the run's same-host owner lock for the life of this process. Node has no flock(2), so the lock is
248
+ * SQLite's exclusive POSIX lock on `<root>/owner.lock` (`locking_mode = EXCLUSIVE`, then `BEGIN EXCLUSIVE`), which the
249
+ * kernel drops when the process dies, however it dies. The journal stays in memory, so `owner.lock` is the only file
250
+ * the lock makes. Another process holding it is HeldError("owner-lock") (76); any other error is a fence, since the
251
+ * file is the claim's.
252
+ *
253
+ * Only this function may open `owner.lock` in the process: a POSIX lock is dropped when any descriptor the process holds
254
+ * on the file is closed, so a stray open and close elsewhere would silently release it.
255
+ */
256
+ export declare function takeOwnerLock(root: string): OwnerLock;
257
+ /** What `storeHead` reads: an ArchilStore, or any connection to a pi store with the file it opened. */
258
+ export interface StoreHeadSource {
259
+ readonly database: Pick<SqliteExecutor, "get">;
260
+ readonly file: string;
261
+ }
262
+ /**
263
+ * The last sequence the store committed: the one read of pi's internal schema in this package. pi-durable's public
264
+ * `Storage` has no head query (1.0.4 and 1.1.0), and the seal check must run before `Harness.open` can commit (its
265
+ * recovery commit would reuse a lost sequence number). pi's SqliteStorage gives each commit `durable_metadata.next_seq`
266
+ * and stores the successor in the same transaction, so the head is `next_seq - 1`. The query only reads. A store
267
+ * without that table, row or column fails closed (STORE_HEAD_UNREADABLE), so a pi whose schema moved never skips the
268
+ * check silently; a unit test pins the read to the sequence pi's commits return.
269
+ *
270
+ * `database` is the store's fence-aware facade (`FencedDatabase`), which turns an I/O-class error into a fence; through
271
+ * a raw connection such an error would read as STORE_HEAD_UNREADABLE.
272
+ */
273
+ export declare function storeHead(store: StoreHeadSource): Promise<number>;
274
+ /**
275
+ * Open the claim's lifecycle for run `ref` (see `RunLease`): acquire and verify the claim, take the owner lock, write
276
+ * `run.json` and start the heartbeat and the self-fence, create `store/`, `work/` and `tmp/`. Errors are typed as for
277
+ * `openDurableRun`: HeldError (76), FencedError (75, also passed to `onFenced`), RunRecordError (65), RunError
278
+ * INVALID_ARGUMENT before any claim.
279
+ */
280
+ export declare function openRunLease(ref: RunRef, options: OpenRunLeaseOptions): Promise<RunLease>;
281
+ /**
282
+ * Open run `ref` on this host: the lease (`openRunLease`: claim, owner lock, `run.json`, heartbeat and
283
+ * self-fence, layout), then the store, the seal check before anything commits, pi's Harness with the app's registry,
284
+ * the seal cleared in `run.json`, resume.
285
+ *
286
+ * Errors are typed: HeldError (76) when another client, another process on this host or another connection holds the
287
+ * run; FencedError (75) when the claim cannot be trusted (also passed to `onFenced`); StoreBehindSealError (65) and
288
+ * RunError STORE_HEAD_UNREADABLE (70), each after marking the run `failed` and releasing the claim.
289
+ */
290
+ export declare function openDurableRun<Tool extends ToolRegistration = ToolRegistration>(ref: RunRef, options: OpenDurableRunOptions<Tool>): Promise<DurableRun>;