@parcha/pi-durable-disk 0.1.0-beta.14 → 0.1.0-beta.15

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.
@@ -20,6 +20,7 @@ import { ARCHIL_SCOPED, DEAD_CONNECTION, DEFAULT_MOUNT_ROOT, runPath } from "../
20
20
  import { EXIT_FENCED, EXIT_HELD, PdaError } from "../errors.js";
21
21
  import { LOCAL_PARK_THRESHOLD_MS } from "../park.js";
22
22
  import { processTable, thisBoot } from "../process-table.js";
23
+ import { attemptEnv } from "./attempt-env.js";
23
24
  export class LocalHostError extends PdaError {
24
25
  constructor(code, message, options = {}) {
25
26
  super(code, message, options);
@@ -161,13 +162,13 @@ export function localHost(opts = {}) {
161
162
  return rm(path, { force: true });
162
163
  await root(["/bin/rm", "-f", path]);
163
164
  }
164
- async function startUnit(ref, token) {
165
+ async function startUnit(ref, token, extra) {
165
166
  const unit = `${prefix}${ref.id}-${Date.now().toString(36)}${Math.floor(Math.random() * 1296).toString(36).padStart(2, "0")}`;
166
167
  if (!UNIT_SAFE.test(unit))
167
168
  throw new LocalHostError("INVALID_ARGUMENT", `unit name ${unit} is not safe`);
168
169
  const mountpoint = mountpointOf(ref);
169
170
  const handle = { driver: "local", mode: "systemd", host: hostName, bootId, unit, mountpoint };
170
- const env = { ...opts.env, PATH: SAFE_PATH, PDA_HOLDER: holderEnv(handle) };
171
+ const env = { ...opts.env, ...extra, PATH: SAFE_PATH, PDA_HOLDER: holderEnv(handle) };
171
172
  const path = tokenFile(unit);
172
173
  await writeTokenFile(path, token);
173
174
  let started = false;
@@ -202,7 +203,7 @@ export function localHost(opts = {}) {
202
203
  await (started ? emptyTokenFile(path) : removeFile(path)).catch(() => { });
203
204
  }
204
205
  }
205
- async function startChild(ref, token) {
206
+ async function startChild(ref, token, extra) {
206
207
  const tag = `${prefix}${ref.id}-${Date.now().toString(36)}`;
207
208
  const mountpoint = mountpointOf(ref);
208
209
  const base = { driver: "local", mode: "child", host: hostName, bootId, mountpoint, tag };
@@ -211,7 +212,7 @@ export function localHost(opts = {}) {
211
212
  const { openSync } = await import("node:fs");
212
213
  out = openSync(join(opts.logDir, `${tag}.log`), "a", 0o600);
213
214
  }
214
- const env = { ...opts.env, PATH: SAFE_PATH, LANG: "C.UTF-8", HOME: process.env.HOME ?? "/", PDA_HOLDER: holderEnv(base) };
215
+ const env = { ...opts.env, ...extra, PATH: SAFE_PATH, LANG: "C.UTF-8", HOME: process.env.HOME ?? "/", PDA_HOLDER: holderEnv(base) };
215
216
  const child = spawn(command[0], [...command.slice(1), ...runFlags(ref), "--token-stdin"], { detached: true, stdio: ["pipe", out, out], env });
216
217
  await new Promise((resolve, reject) => {
217
218
  child.once("spawn", resolve);
@@ -288,9 +289,11 @@ export function localHost(opts = {}) {
288
289
  return {
289
290
  hostName,
290
291
  mountRoot,
291
- start(ref, token) {
292
+ kills: "process",
293
+ async start(ref, token, attempt) {
292
294
  runPath(ref.id);
293
- return mode === "systemd" ? startUnit(ref, token) : startChild(ref, token);
295
+ const extra = attemptEnv(attempt, (m) => new LocalHostError("INVALID_ARGUMENT", m));
296
+ return mode === "systemd" ? startUnit(ref, token, extra) : startChild(ref, token, extra);
294
297
  },
295
298
  async status(h) {
296
299
  if (!mine(h))
@@ -347,6 +350,31 @@ export function localHost(opts = {}) {
347
350
  await root([SYSTEMCTL, "reset-failed", `${h.unit}.service`]);
348
351
  await removeFile(tokenFile(h.unit));
349
352
  },
353
+ async kill(h) {
354
+ if (!mine(h) || h.bootId !== bootId)
355
+ return;
356
+ if (h.mode === "child") {
357
+ const pid = Number(h.pid);
358
+ // As in stop: only a real process, and only the one the handle names (its start ticks).
359
+ if (!Number.isInteger(pid) || pid <= 1 || typeof h.startTicks !== "number" || procStartTicks(pid) !== h.startTicks)
360
+ return;
361
+ try {
362
+ process.kill(-pid, "SIGKILL");
363
+ }
364
+ catch {
365
+ // gone already
366
+ }
367
+ return;
368
+ }
369
+ if (typeof h.unit !== "string" || !UNIT_SAFE.test(h.unit))
370
+ return;
371
+ // Every process of the unit's control group at once, the instance's commands included. Its FUSE daemon runs in a
372
+ // scope of its own and keeps the mount, as after an OOM kill; the unit restarts in place unless restart is off.
373
+ const r = await root([SYSTEMCTL, "kill", "--signal=SIGKILL", "--kill-whom=all", `${h.unit}.service`]);
374
+ if (!okRan(r) && unitStatus(await show(h.unit)) === "running") {
375
+ throw new LocalHostError("KILL_FAILED", `systemctl kill ${h.unit}: ${r.timedOut ? "timed out" : firstLine(r)}`);
376
+ }
377
+ },
350
378
  };
351
379
  }
352
380
  /**
package/dist/index.d.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  export { openDurableRun, RunError, StoreBehindSealError } from "./run.js";
2
- export type { BeforeResume, DurableRun, LeaseOptions, OpenDurableRunOptions, OpenRunLeaseOptions, OpenStep, ReleaseStep, RunClaim, RunEnv, RunErrorCode, RunLease, StoreHeadSource, StoreReads } from "./run.js";
2
+ export type { AfterClaim, BeforeResume, DurableRun, LeaseOptions, OpenDurableRunOptions, OpenRunLeaseOptions, OpenStep, ReleaseStep, RunClaim, RunEnv, RunErrorCode, RunLease, RunSeed, RunSeedDir, StoreHeadSource, StoreReads } from "./run.js";
3
3
  export { localRun } from "./local-run.js";
4
4
  export type { LocalRun } from "./local-run.js";
5
- export { CLAIM_UNMOUNTED, openClaimDir, RunRecordError } from "./status.js";
5
+ export { CLAIM_UNMOUNTED, openClaimDir, readLocalRunStatus, RunRecordError } from "./status.js";
6
6
  export type { ClaimDir, OpenClaimDirOptions, PersistRecord, RunHolder, RunRecord, RunStatus } from "./status.js";
7
7
  export { ArchilCodingTools, archilEnv, EnvClosedError, EnvError, markReadOnlySafe } from "./env.js";
8
8
  export type { ArchilEnvClaim, ArchilEnvFactory, ArchilEnvOptions, EnvErrorCode } from "./env.js";
9
9
  export { acquire, ARCHIL_SCOPED, revoke, takeOver } from "./claim.js";
10
10
  export type { AcquireOptions, ArchilHost, Claim, ControlApi, PathlessResolver, RunRef, Unmounted } from "./claim.js";
11
+ export { readReleased, withClaim } from "./with-claim.js";
12
+ export type { ClaimControl, HeldPath, WithClaimOptions } from "./with-claim.js";
11
13
  export { PathOutsideWorkError } from "./confine.js";
12
14
  export type { RefusalReason } from "./confine.js";
13
15
  export { ClaimError, FencedError, HeldError, PdaError } from "./errors.js";
@@ -25,6 +27,8 @@ export { DaytonaApiError, daytonaHost, DaytonaHostError, daytonaRest } from "./h
25
27
  export type { CreateSandboxBody, DaytonaClient, DaytonaHostErrorCode, DaytonaHostOptions, DaytonaRestOptions, ExecResult, SandboxInfo } from "./hosts/daytona.js";
26
28
  export { requestRun, ServeConfigError, ServeError, serveRun } from "./serve.js";
27
29
  export type { RequestOptions, RunResponse, RunServer, ServeConfigErrorCode, ServeErrorCode, ServeOptions } from "./serve.js";
30
+ export { archilPlace, localPlace, PlaceError } from "./place.js";
31
+ export type { ArchilPlaceOptions, LocalPlaceOptions, Place, PlaceContext, PlaceErrorCode, PlaceExit, PlaceKill, PlaceOutcome, PlacePaths, PlaceRecovery, PlaceRunner, PlaceSignal, PlaceStatus, RunnerOrder } from "./place.js";
28
32
  export { busyState, drain, leaseParkTarget, watchParking } from "./park.js";
29
33
  export type { BusyState, DrainOptions, Parking, ParkOptions, ParkResult, ParkTarget } from "./park.js";
30
34
  export { fork, ForkError, forkLocal } from "./fork.js";
package/dist/index.js CHANGED
@@ -4,9 +4,11 @@
4
4
  // The run: open it, on a disk or a local directory, with its environment, its claim and the errors it fails with.
5
5
  export { openDurableRun, RunError, StoreBehindSealError } from "./run.js";
6
6
  export { localRun } from "./local-run.js";
7
- export { CLAIM_UNMOUNTED, openClaimDir, RunRecordError } from "./status.js";
7
+ export { CLAIM_UNMOUNTED, openClaimDir, readLocalRunStatus, RunRecordError } from "./status.js";
8
8
  export { ArchilCodingTools, archilEnv, EnvClosedError, EnvError, markReadOnlySafe } from "./env.js";
9
9
  export { acquire, ARCHIL_SCOPED, revoke, takeOver } from "./claim.js";
10
+ // A short exclusive claim on any path of a disk, and a consistent S3 read of a released file (an agent home beside the runs).
11
+ export { readReleased, withClaim } from "./with-claim.js";
10
12
  export { PathOutsideWorkError } from "./confine.js";
11
13
  export { ClaimError, FencedError, HeldError, PdaError } from "./errors.js";
12
14
  // The app contract: what `--app` names.
@@ -18,6 +20,8 @@ export { dockerHost, DockerHostError } from "./hosts/docker.js";
18
20
  export { DaytonaApiError, daytonaHost, DaytonaHostError, daytonaRest } from "./hosts/daytona.js";
19
21
  // Serve: one HTTP request runs one turn.
20
22
  export { requestRun, ServeConfigError, ServeError, serveRun } from "./serve.js";
23
+ // Places: where a run lives for a supervisor that kills it and brings it back (agentrun's reference place seam).
24
+ export { archilPlace, localPlace, PlaceError } from "./place.js";
21
25
  // Parking: an idle run gives its host back.
22
26
  export { busyState, drain, leaseParkTarget, watchParking } from "./park.js";
23
27
  // Fork: a new run from a run's committed state.
package/dist/lease.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { EXIT_DATAERR, EXIT_SOFTWARE, LeaseLapsedError, openRunLease, OWNER_LOCK, RunError, StoreBehindSealError, storeHead, STORE_FILE } from "./run.js";
2
- export type { LeaseOptions, OpenRunLeaseOptions, OpenStep, ReleaseStep, RunClaim, RunErrorCode, RunLease, StoreHeadSource } from "./run.js";
2
+ export type { LeaseOptions, OpenRunLeaseOptions, OpenStep, ReleaseStep, RunClaim, RunErrorCode, RunLease, RunSeed, RunSeedDir, StoreHeadSource } from "./run.js";
3
3
  export * from "./errors.js";
4
4
  export { assertPragmas, Fence, FencedDatabase, openArchilStore, PROFILE_PRAGMAS, StoreBusyError, StoreFencedError, StorePragmaError } from "./store.js";
5
5
  export type { ArchilStore, OpenOptions, PragmaMismatch, Profile } from "./store.js";
@@ -0,0 +1,190 @@
1
+ import type { Delegation } from "disk";
2
+ import { PdaError } from "./errors.js";
3
+ import { type RunRecord } from "./status.js";
4
+ import { type EnsureOptions, type HostDriver, type SupervisorControl } from "./supervise.js";
5
+ export type PlaceKill = "process" | "box";
6
+ /** The run's paths in the place: its id, its directory, and the `work` and `store` under it. */
7
+ export type PlacePaths = {
8
+ id: string;
9
+ root: string;
10
+ work: string;
11
+ store: string;
12
+ };
13
+ /**
14
+ * How the run stood when a runner ended: the instance's own report, or `run.json` as it left it. `status` is the run's
15
+ * word: `complete` when it delivered, else `failed`.
16
+ */
17
+ export type PlaceOutcome = {
18
+ status: string;
19
+ generation: number | null;
20
+ code?: string | null;
21
+ message?: string;
22
+ record?: unknown;
23
+ storeSha256?: string;
24
+ };
25
+ /** How a runner ended. `exit`: the exit code or signal (local), or the host's status (`stopped`, `failed`, `gone`) or `SIGKILL` when the place killed it (archil). */
26
+ export type PlaceExit = {
27
+ exit: number | string | null;
28
+ signal: string | null;
29
+ outcome?: PlaceOutcome;
30
+ stderr: string;
31
+ };
32
+ /** One instance of the run in a place. `epoch`: 1 on a plain directory; on a disk, the generation the instance opens. */
33
+ export type PlaceRunner = {
34
+ epoch: number;
35
+ pid?: number;
36
+ exited: Promise<PlaceExit>;
37
+ };
38
+ /** What a runner is handed: its number in the run, its environment, and for a place with a token, the attempt's mount token. */
39
+ export type RunnerOrder = {
40
+ gen: number;
41
+ env: Record<string, string>;
42
+ stdin?: string;
43
+ };
44
+ /** What the place reads of the run, the way it reads it: `run.json`, and on a disk the run's delegations. */
45
+ export type PlaceStatus = {
46
+ record: RunRecord | null;
47
+ delegations?: Pick<Delegation, "clientId" | "inodeId" | "path" | "isOrphaned" | "isPending">[];
48
+ };
49
+ /** What a recovery did. A takeover on a disk lists what it revoked, and `ms` is the whole of it (revoke and start). */
50
+ export type PlaceRecovery = {
51
+ runner: PlaceRunner;
52
+ restored?: {
53
+ checkpoint: string;
54
+ head: number;
55
+ };
56
+ replayed?: {
57
+ from: number;
58
+ to: number;
59
+ };
60
+ revoked?: Pick<Delegation, "clientId" | "inodeId" | "path" | "isOrphaned">[];
61
+ ms?: number;
62
+ };
63
+ /**
64
+ * What a runner announces to its supervisor: a crossing it took, or a request to be killed at one (it parks there until
65
+ * the kill). `signals-failed` is the place's own last word when it could not read the requests: it ends the iterator,
66
+ * which never rejects. `code` is the failing read's error code when it has one (CONTROL_API_FAILED for a read that timed
67
+ * out), else SIGNALS_FAILED.
68
+ */
69
+ export type PlaceSignal = {
70
+ kind: "crossing" | "kill-request";
71
+ crossing: {
72
+ point: string;
73
+ n: number;
74
+ pid?: number;
75
+ info?: Record<string, unknown>;
76
+ };
77
+ /** The attempt's nonce the runner wrote into its request, lifted out of `crossing`: the supervisor honors a kill request only with the nonce it minted for that attempt. */
78
+ nonce?: string;
79
+ } | {
80
+ kind: "signals-failed";
81
+ code: string;
82
+ message: string;
83
+ };
84
+ /**
85
+ * What the supervisor hands a place factory: the run's paths as it computed them, how to take the boxes the run made,
86
+ * and where attempt n's runner writes its kill request (a file of the supervisor's machine).
87
+ */
88
+ export type PlaceContext = {
89
+ paths: PlacePaths;
90
+ self?: string;
91
+ destroyBoxes?: () => void | Promise<void>;
92
+ requestFile?: (attempt: number) => string;
93
+ };
94
+ export interface Place {
95
+ /** `local` or `archil`, for the recording's lines. */
96
+ readonly name: string;
97
+ readonly paths: PlacePaths;
98
+ /** A place whose instance opens a claim mints each attempt's mount token: the supervisor passes it as the runner's stdin, never in argv or env. */
99
+ token?(attempt: number): string | undefined | Promise<string | undefined>;
100
+ /** What this place can lose. */
101
+ readonly kills: ReadonlyArray<PlaceKill>;
102
+ start(order: RunnerOrder): Promise<PlaceRunner>;
103
+ /**
104
+ * Where attempt `attempt`'s runner writes its kill request, as a path in the place (the supervisor puts it in the
105
+ * runner's order). A place with `signals` has it, and `signals(attempt)` reads what is written there.
106
+ */
107
+ requestFile?(attempt: number): string;
108
+ /**
109
+ * The kill requests (and crossings) attempt `attempt`'s runner announces. The supervisor stops reading with `return()`
110
+ * once the runner has exited, and `return()` ends a pending `next()`.
111
+ */
112
+ signals?(attempt: number): AsyncIterable<PlaceSignal>;
113
+ /** Takes the runner away abruptly, as a crash would. Safe on a runner that is already gone. */
114
+ kill(runner: PlaceRunner, kind: PlaceKill): Promise<void>;
115
+ status?(): Promise<PlaceStatus>;
116
+ /** Kills `last` if it still runs, then starts the next runner. */
117
+ recover(last: PlaceRunner, kind: PlaceKill, order: RunnerOrder): Promise<PlaceRecovery>;
118
+ checkpoint?(): Promise<{
119
+ taken: boolean;
120
+ heldBy?: string;
121
+ }>;
122
+ move?(to: Place): Promise<Record<string, unknown>>;
123
+ /** Lets the run go: stops what still runs (its commands too) and removes what the place made for it (tokens), never the run's directory. */
124
+ release(): Promise<void>;
125
+ }
126
+ export type PlaceErrorCode = "INVALID_ARGUMENT" | "HELD" | "START_TIMED_OUT" | "STATUS_FAILED" | "KILL_UNSUPPORTED" | "RELEASE_FAILED";
127
+ export declare class PlaceError extends PdaError {
128
+ constructor(code: PlaceErrorCode, message: string, options?: {
129
+ cause?: unknown;
130
+ });
131
+ }
132
+ export interface LocalPlaceOptions {
133
+ /** The app module `run --app` loads. */
134
+ app: string;
135
+ /** The run's directory. Default: the context's `paths.root`. */
136
+ root?: string;
137
+ /** The instance program; `run` and its flags are appended. Default: this Node and the package's CLI. */
138
+ command?: string[];
139
+ /** Extra flags for `run` (heartbeat periods, a park threshold). */
140
+ runArgs?: string[];
141
+ }
142
+ /**
143
+ * The run on a directory of this machine: each runner is `pi-durable-disk run --dir <root> --id <id> --app <app>`, a
144
+ * child in its own process group, with `{ PATH, ...order.env }` and nothing else of this process's environment. No disk,
145
+ * no key and no token. A process kill is SIGKILL to the group, and the next runner resumes the run from the directory; a
146
+ * box kill also takes the boxes the run made (`destroyBoxes`), so `kills` has `box` only when the context gives that.
147
+ * A runner's outcome is the instance's `{"outcome":...}` line on stdout, or, without one, `run.json` as it left it.
148
+ */
149
+ export declare function localPlace(options: LocalPlaceOptions): (ctx: PlaceContext) => Place;
150
+ export interface ArchilPlaceOptions {
151
+ disk: string;
152
+ region: string;
153
+ /** The disk's control API (`archilControl`). The place holds it; no instance ever does. */
154
+ control: SupervisorControl;
155
+ /**
156
+ * The driver that runs the instance (`localHost`, `dockerHost`, `daytonaHost`), set up with the app the instance runs.
157
+ * Its `kill` and `kills` are the place's.
158
+ */
159
+ host: HostDriver;
160
+ /** The run's id on the disk. Default: the context's `paths.id`. */
161
+ id?: string;
162
+ /** Where the driver mounts runs, for `paths`. Default: the driver's `mountRoot`, else /mnt/archil. */
163
+ mountRoot?: string;
164
+ /** More for each start's `ensureRunning`: the run directory's owner (`create`), token prefix and TTL, timeouts. */
165
+ ensure?: Omit<EnsureOptions, "control" | "demand" | "mountToken">;
166
+ /** How often a runner's host status is read. Default 1 s. */
167
+ pollMs?: number;
168
+ /** How long a start waits on a mount another start has in flight. Default 60 s. */
169
+ startTimeoutMs?: number;
170
+ /** How often `signals` reads the attempt's kill request over S3. Default 500 ms (the runner waits at its crossing). */
171
+ signalPollMs?: number;
172
+ /**
173
+ * How long those reads may fail (anything but a missing object) without a break before the iterator yields one
174
+ * `signals-failed` and ends. Default 60 s.
175
+ */
176
+ signalFailMs?: number;
177
+ }
178
+ /**
179
+ * The run on an Archil disk, its instance run by a host driver. `token(attempt)` mints the attempt's mount token through
180
+ * the place's control, and the runner it is handed to mounts with it. A start is `ensureRunning` (it creates the run's
181
+ * directory with `ensure.create` and writes the start mark) with that token and the order's environment. A kill is the
182
+ * driver's `kill`: `process` or `box`, whichever the driver takes. A recovery is the takeover: the dead holder's
183
+ * delegations revoked (its lease counts as expired, since the place killed it) and the next instance started, which
184
+ * `revoked` and `ms` report. A runner writes its kill request in the run's directory (`requestFile`), and `signals` reads
185
+ * it over S3. A runner ends when the driver no longer reports it running, or when the place killed it; its
186
+ * outcome is `run.json` read over S3, with `code`, `message`, `record` and `storeSha256` from the detail of the last
187
+ * status the app set. `release` stops every instance it started and removes every token it minted; the run's directory
188
+ * stays (`deleteRunTree`).
189
+ */
190
+ export declare function archilPlace(options: ArchilPlaceOptions): (ctx: PlaceContext) => Place;