@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
@@ -0,0 +1,137 @@
1
+ // The lease watchdog on a thread of its own. A store commit is synchronous, so a FUSE daemon that stops
2
+ // answering blocks the instance's main thread inside the kernel, and with it every timer the main thread owns. This
3
+ // thread never touches the mount: it reads a clock reading the main thread shares, /proc and the cgroup, and sends
4
+ // signals. Once the lease has lapsed it kills the instance's commands on every look, until the main thread runs again,
5
+ // sees the flag and takes the normal fence path (exit 75).
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import { Worker } from "node:worker_threads";
9
+ /** Slots of the shared BigInt64Array. Clock readings are `process.hrtime.bigint()`, monotonic and the same on every thread. */
10
+ export const SLOT_BEAT = 0; // when the last successful heartbeat write started
11
+ export const SLOT_LIMIT = 1; // expiry minus margin, in ns
12
+ export const SLOT_LAPSED = 2; // 0 while the lease holds; else how old the last beat was when this thread found it lapsed
13
+ export const SLOT_KILLED = 3; // signals sent so far
14
+ const SLOTS = 4;
15
+ // The watchdog's code is plain CommonJS JavaScript in strings this module carries, and the worker is started from that
16
+ // source (`eval: true`), never from this module's URL: bundled into an app, this module's URL is the app's bundle, and a
17
+ // worker loaded from it would run the whole app again. A bundler leaves string contents alone, so the worker runs the
18
+ // same text whatever bundles the module. It must stay free of ESM syntax (`import`, `export`, `import.meta`): Node then
19
+ // runs it as CommonJS, with `require`. The main thread compiles the same LIB text for `lapsed` and `sweep`, so the tests
20
+ // exercise exactly the code the worker runs.
21
+ const LIB = String.raw `
22
+ const SLOT_BEAT = 0, SLOT_LIMIT = 1, SLOT_LAPSED = 2, SLOT_KILLED = 3;
23
+
24
+ function lapsed(shared, now) {
25
+ if (Atomics.load(shared, SLOT_LAPSED) !== 0n) return true;
26
+ const age = now - Atomics.load(shared, SLOT_BEAT);
27
+ if (age <= Atomics.load(shared, SLOT_LIMIT)) return false;
28
+ Atomics.compareExchange(shared, SLOT_LAPSED, 0n, age);
29
+ return true;
30
+ }
31
+
32
+ function stat(proc, pid) {
33
+ try {
34
+ const text = fs.readFileSync(path.join(proc, String(pid), "stat"), "utf8");
35
+ const fields = text.slice(text.lastIndexOf(")") + 2).split(" ");
36
+ return { ppid: Number(fields[1]), pgrp: Number(fields[2]) };
37
+ } catch {
38
+ return undefined;
39
+ }
40
+ }
41
+
42
+ function cgroupPids(instance, proc, sys) {
43
+ try {
44
+ const line = fs.readFileSync(path.join(proc, String(instance), "cgroup"), "utf8").split("\n").find((l) => l.startsWith("0::"));
45
+ if (line === undefined) return [];
46
+ return fs.readFileSync(path.join(sys, line.slice(3), "cgroup.procs"), "utf8").split("\n").filter(Boolean).map(Number);
47
+ } catch {
48
+ return [];
49
+ }
50
+ }
51
+
52
+ function sweep(instance, options = {}) {
53
+ const proc = options.proc ?? "/proc";
54
+ const kill = options.kill ?? ((pid, signal) => process.kill(pid, signal));
55
+ const own = stat(proc, instance);
56
+ if (own === undefined) return 0;
57
+ const targets = new Set();
58
+ for (const entry of fs.readdirSync(proc)) {
59
+ const pid = Number(entry);
60
+ if (!Number.isInteger(pid) || pid <= 0 || pid === instance) continue;
61
+ const child = stat(proc, pid);
62
+ if (child === undefined || child.ppid !== instance) continue;
63
+ targets.add(child.pgrp === own.pgrp || child.pgrp <= 1 ? pid : -child.pgrp);
64
+ }
65
+ if (options.ownCgroup) {
66
+ for (const pid of cgroupPids(instance, proc, options.sys ?? "/sys/fs/cgroup")) if (pid !== instance && pid > 0) targets.add(pid);
67
+ }
68
+ let sent = 0;
69
+ for (const target of targets) {
70
+ try {
71
+ kill(target, "SIGKILL");
72
+ sent++;
73
+ } catch {
74
+ // gone already, or not ours to signal
75
+ }
76
+ }
77
+ return sent;
78
+ }
79
+ `;
80
+ const WORKER_SOURCE = `"use strict";
81
+ const fs = require("node:fs");
82
+ const path = require("node:path");
83
+ const { workerData } = require("node:worker_threads");
84
+ ${LIB}
85
+ const shared = new BigInt64Array(workerData.buffer);
86
+ setInterval(() => {
87
+ if (!lapsed(shared, process.hrtime.bigint())) return;
88
+ Atomics.add(shared, SLOT_KILLED, BigInt(sweep(workerData.instance, { ownCgroup: workerData.ownCgroup })));
89
+ }, workerData.checkMs);
90
+ `;
91
+ const compiled = new Function("fs", "path", `"use strict";${LIB}\nreturn { lapsed, sweep };`)(fs, path);
92
+ /** One look at the shared state at `now`. True once the lease has lapsed, for good; the first look records the age. */
93
+ export const lapsed = compiled.lapsed;
94
+ /**
95
+ * SIGKILL the instance's commands: the process group of each direct child (pi starts each command as its own group's
96
+ * leader), or the child alone when it shares the instance's group; with `ownCgroup`, every other process of the cgroup.
97
+ * Never the instance or its group. A process this user may not signal (sudo's archil helpers) is skipped. Returns the
98
+ * number of signals sent. `stat` lines are `pid (comm) state ppid pgrp ...`, read after the last `)` since comm may hold
99
+ * spaces and parentheses.
100
+ */
101
+ export const sweep = compiled.sweep;
102
+ /** The main thread's handle on the watchdog thread. */
103
+ export class LeaseWatchdog {
104
+ #shared;
105
+ #worker;
106
+ /** Starts the thread; `beatNs` is when the last successful heartbeat write started. */
107
+ constructor(options) {
108
+ const buffer = new SharedArrayBuffer(SLOTS * 8);
109
+ this.#shared = new BigInt64Array(buffer);
110
+ Atomics.store(this.#shared, SLOT_BEAT, options.beatNs);
111
+ Atomics.store(this.#shared, SLOT_LIMIT, BigInt(Math.round(options.limitMs * 1e6)));
112
+ const data = { buffer, checkMs: options.checkMs, instance: process.pid, ownCgroup: options.ownCgroup === true };
113
+ // No inherited Node flags: the source is self-contained CommonJS and needs none of the host's, and some would make Node
114
+ // read it as ESM (`--input-type=module`, a host running inline ESM through -e or stdin), where `require` is undefined.
115
+ this.#worker = new Worker(WORKER_SOURCE, { eval: true, workerData: data, execArgv: [] });
116
+ this.#worker.unref();
117
+ }
118
+ /** A heartbeat write that started at `startNs` succeeded. */
119
+ beat(startNs) {
120
+ for (;;) {
121
+ const current = Atomics.load(this.#shared, SLOT_BEAT);
122
+ if (startNs <= current || Atomics.compareExchange(this.#shared, SLOT_BEAT, current, startNs) === current)
123
+ return;
124
+ }
125
+ }
126
+ /** Once the watchdog found the lease lapsed: how long before that the last successful heartbeat started, in ms. */
127
+ get lapsedAfterMs() {
128
+ const age = Atomics.load(this.#shared, SLOT_LAPSED);
129
+ return age === 0n ? undefined : Number(age) / 1e6;
130
+ }
131
+ get killed() {
132
+ return Number(Atomics.load(this.#shared, SLOT_KILLED));
133
+ }
134
+ stop() {
135
+ void this.#worker.terminate();
136
+ }
137
+ }
package/package.json CHANGED
@@ -1,6 +1,89 @@
1
1
  {
2
2
  "name": "@parcha/pi-durable-disk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-beta.13",
4
+ "description": "A durable pi host on a network disk (Archil): one owner per run, a fence, durable commits, and a supervisor.",
5
+ "keywords": [
6
+ "pi",
7
+ "pi-durable",
8
+ "archil",
9
+ "disk",
10
+ "durable",
11
+ "agent",
12
+ "fence",
13
+ "lease",
14
+ "supervisor"
15
+ ],
16
+ "type": "module",
17
+ "license": "Apache-2.0",
18
+ "author": "Parcha Labs, Inc.",
19
+ "engines": {
20
+ "node": ">=22.19.0"
21
+ },
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ },
29
+ "./lease": {
30
+ "types": "./dist/lease.d.ts",
31
+ "default": "./dist/lease.js"
32
+ },
33
+ "./bin/archil-scoped": "./bin/archil-scoped",
34
+ "./package.json": "./package.json"
35
+ },
36
+ "bin": {
37
+ "pi-durable-disk": "./dist/cli.js"
38
+ },
39
+ "files": [
40
+ "dist",
41
+ "bin/archil-scoped",
42
+ "README.md",
43
+ "CHANGELOG.md",
44
+ "LICENSE",
45
+ "NOTICE"
46
+ ],
47
+ "sideEffects": false,
48
+ "scripts": {
49
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/rewrite-dts.mjs dist",
50
+ "prepack": "npm run build",
51
+ "typecheck": "tsc --noEmit && tsc --noEmit -p examples",
52
+ "test": "node --test --test-timeout=240000 'test/*.test.ts' 'test/examples/*.test.ts'",
53
+ "test:acceptance": "node --test --test-concurrency=1 'test/acceptance/*.test.ts'",
54
+ "test:live": "node --test --test-concurrency=1 'test/live/*.test.ts'",
55
+ "test:live:claim": "node --test --test-concurrency=1 test/live/claim.live.test.ts",
56
+ "test:live:env": "node --test --test-concurrency=1 test/live/env.live.test.ts",
57
+ "test:live:lifecycle": "node --test --test-concurrency=1 test/live/lifecycle.live.test.ts",
58
+ "test:live:run": "node --test --test-concurrency=1 test/live/run.live.test.ts",
59
+ "test:live:store": "node --test --test-concurrency=1 test/live/store.live.test.ts",
60
+ "test:live:supervise": "node --test --test-concurrency=1 test/live/supervise.live.test.ts",
61
+ "test:live:docker": "PDA_LIVE_DOCKER=1 node --test --test-concurrency=1 test/live/docker.live.test.ts test/live/docker-park.live.test.ts",
62
+ "verify:tarball": "node scripts/verify-tarball.mjs"
63
+ },
64
+ "peerDependencies": {
65
+ "@earendil-works/chord": ">=1.0.4 <1.3.0",
66
+ "@earendil-works/pi-durable": ">=1.0.4 <1.3.0"
67
+ },
68
+ "dependencies": {
69
+ "disk": "1.7.0"
70
+ },
71
+ "devDependencies": {
72
+ "@earendil-works/chord": "1.1.0",
73
+ "@earendil-works/pi-ai": "1.1.0",
74
+ "@earendil-works/pi-durable": "1.1.0"
75
+ },
76
+ "repository": {
77
+ "type": "git",
78
+ "url": "git+https://github.com/Parcha-ai/agentrun.git",
79
+ "directory": "packages/pi-durable-disk"
80
+ },
81
+ "bugs": {
82
+ "url": "https://github.com/Parcha-ai/agentrun/issues"
83
+ },
84
+ "homepage": "https://agentrun.ai",
85
+ "publishConfig": {
86
+ "access": "public",
87
+ "tag": "beta"
88
+ }
89
+ }