pi-daddy 0.45.0 → 0.46.0
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/CHANGELOG.md +4 -0
- package/README.md +355 -45
- package/dist/executors/captured-execution.d.ts +2 -2
- package/dist/executors/captured-execution.d.ts.map +1 -1
- package/dist/executors/captured-execution.js +2 -2
- package/dist/executors/captured-execution.js.map +1 -1
- package/dist/executors/executor.d.ts.map +1 -1
- package/dist/executors/executor.js +4 -2
- package/dist/executors/executor.js.map +1 -1
- package/dist/executors/herdr-cli.d.ts +2 -0
- package/dist/executors/herdr-cli.d.ts.map +1 -1
- package/dist/executors/herdr-cli.js +2 -2
- package/dist/executors/herdr-cli.js.map +1 -1
- package/dist/executors/herdr-launcher.d.ts +2 -0
- package/dist/executors/herdr-launcher.d.ts.map +1 -0
- package/dist/executors/herdr-launcher.js +123 -0
- package/dist/executors/herdr-launcher.js.map +1 -0
- package/dist/executors/herdr-owned.d.ts +13 -0
- package/dist/executors/herdr-owned.d.ts.map +1 -0
- package/dist/executors/herdr-owned.js +229 -0
- package/dist/executors/herdr-owned.js.map +1 -0
- package/dist/executors/herdr-qualification.d.ts +6 -0
- package/dist/executors/herdr-qualification.d.ts.map +1 -0
- package/dist/executors/herdr-qualification.js +33 -0
- package/dist/executors/herdr-qualification.js.map +1 -0
- package/dist/executors/herdr-wire.d.ts +5 -0
- package/dist/executors/herdr-wire.d.ts.map +1 -0
- package/dist/executors/herdr-wire.js +47 -0
- package/dist/executors/herdr-wire.js.map +1 -0
- package/dist/governance/runtime-settlement-lock.d.ts +5 -0
- package/dist/governance/runtime-settlement-lock.d.ts.map +1 -0
- package/dist/governance/runtime-settlement-lock.js +72 -0
- package/dist/governance/runtime-settlement-lock.js.map +1 -0
- package/dist/governance/runtime-settlement.d.ts +22 -0
- package/dist/governance/runtime-settlement.d.ts.map +1 -0
- package/dist/governance/runtime-settlement.js +236 -0
- package/dist/governance/runtime-settlement.js.map +1 -0
- package/extensions/execute-child.ts +98 -74
- package/extensions/executor-session.ts +2 -1
- package/extensions/grants.ts +3 -0
- package/extensions/reload-environment.ts +8 -0
- package/extensions/runtime-snapshot.ts +123 -0
- package/extensions/session.ts +1 -1
- package/package.json +1 -1
- package/src/executors/captured-execution.ts +2 -1
- package/src/executors/executor.ts +4 -2
- package/src/executors/herdr-cli.ts +19 -12
- package/src/executors/herdr-launcher.ts +124 -0
- package/src/executors/herdr-owned.ts +260 -0
- package/src/executors/herdr-qualification.ts +33 -0
- package/src/executors/herdr-wire.ts +43 -0
- package/src/governance/runtime-settlement-lock.ts +76 -0
- package/src/governance/runtime-settlement.ts +271 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Bounded local framing; this channel carries private execution input only between its two owners. */
|
|
2
|
+
import type { Socket } from "node:net";
|
|
3
|
+
export const HERDR_WIRE_LIMIT = 8 * 1024 * 1024;
|
|
4
|
+
export function wire(socket: Socket, receive: (value: any) => void, failed: (error: Error) => void) {
|
|
5
|
+
let pending = "";
|
|
6
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
7
|
+
const fail = (error: unknown) => {
|
|
8
|
+
failed(error instanceof Error ? error : new Error(String(error)));
|
|
9
|
+
socket.destroy();
|
|
10
|
+
};
|
|
11
|
+
socket.on("data", (bytes) => {
|
|
12
|
+
try {
|
|
13
|
+
pending += decoder.decode(bytes, { stream: true });
|
|
14
|
+
let end: number;
|
|
15
|
+
while ((end = pending.indexOf("\n")) >= 0) {
|
|
16
|
+
if (Buffer.byteLength(pending.slice(0, end)) > HERDR_WIRE_LIMIT)
|
|
17
|
+
throw Error("Herdr transport record exceeds bound");
|
|
18
|
+
const line = pending.slice(0, end);
|
|
19
|
+
pending = pending.slice(end + 1);
|
|
20
|
+
receive(JSON.parse(line));
|
|
21
|
+
}
|
|
22
|
+
if (Buffer.byteLength(pending) > HERDR_WIRE_LIMIT) throw Error("Herdr transport record exceeds bound");
|
|
23
|
+
} catch (error) {
|
|
24
|
+
fail(error);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
socket.on("end", () => {
|
|
28
|
+
try {
|
|
29
|
+
pending += decoder.decode();
|
|
30
|
+
if (pending) throw Error("Herdr transport record is incomplete");
|
|
31
|
+
} catch (error) {
|
|
32
|
+
fail(error);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
socket.on("error", (error) => failed(error));
|
|
36
|
+
return (value: unknown) => {
|
|
37
|
+
const text = JSON.stringify(value) + "\n";
|
|
38
|
+
if (Buffer.byteLength(text) > HERDR_WIRE_LIMIT || socket.writableLength > HERDR_WIRE_LIMIT * 2)
|
|
39
|
+
throw Error("Herdr transport buffering exceeds bound");
|
|
40
|
+
if (socket.destroyed || !socket.writable) throw Error("Herdr transport is closed");
|
|
41
|
+
socket.write(text);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** An OS-held single-writer lock; parent pipe loss releases it without stale-age guesses. */
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { constants, openSync, closeSync, fstatSync } from "node:fs";
|
|
4
|
+
export async function lockRuntimeSettlement(path: string): Promise<{ healthy(): boolean; release(): Promise<void> }> {
|
|
5
|
+
const fd = openSync(path, constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW, 0o600);
|
|
6
|
+
const info = fstatSync(fd);
|
|
7
|
+
if (!info.isFile() || info.uid !== process.getuid!() || info.mode & 0o077) {
|
|
8
|
+
closeSync(fd);
|
|
9
|
+
throw Error("runtime lock is not a private ordinary file");
|
|
10
|
+
}
|
|
11
|
+
const child = spawn(
|
|
12
|
+
"/usr/bin/flock",
|
|
13
|
+
[
|
|
14
|
+
"--no-fork",
|
|
15
|
+
"--wait",
|
|
16
|
+
"1",
|
|
17
|
+
"/proc/self/fd/3",
|
|
18
|
+
process.execPath,
|
|
19
|
+
"-e",
|
|
20
|
+
'process.stdout.write("locked\\n");process.stdin.resume();process.stdin.on("end",()=>process.exit(0));',
|
|
21
|
+
],
|
|
22
|
+
{ stdio: ["pipe", "pipe", "ignore", fd] },
|
|
23
|
+
);
|
|
24
|
+
closeSync(fd);
|
|
25
|
+
let healthy = false,
|
|
26
|
+
line = "";
|
|
27
|
+
await new Promise<void>((resolve, reject) => {
|
|
28
|
+
const timer = setTimeout(() => {
|
|
29
|
+
child.stdin?.destroy();
|
|
30
|
+
child.kill();
|
|
31
|
+
reject(Error("runtime ownership lock timed out"));
|
|
32
|
+
}, 3000);
|
|
33
|
+
child.on("error", (error) => {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
reject(error);
|
|
36
|
+
});
|
|
37
|
+
child.on("exit", () => {
|
|
38
|
+
healthy = false;
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
reject(Error("another live runtime owns this Pi session"));
|
|
41
|
+
});
|
|
42
|
+
child.stdout?.on("data", (bytes) => {
|
|
43
|
+
line += bytes.toString();
|
|
44
|
+
if (line === "locked\n") {
|
|
45
|
+
healthy = true;
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
resolve();
|
|
48
|
+
} else if (line.length > 16) {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
child.stdin?.destroy();
|
|
51
|
+
reject(Error("runtime lock returned malformed readiness"));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
child.unref();
|
|
56
|
+
(child.stdin as import("node:net").Socket).unref();
|
|
57
|
+
(child.stdout as import("node:net").Socket).unref();
|
|
58
|
+
let releasing: Promise<void> | undefined;
|
|
59
|
+
return {
|
|
60
|
+
healthy: () => healthy && child.exitCode === null && child.signalCode === null,
|
|
61
|
+
release: () =>
|
|
62
|
+
(releasing ??= new Promise<void>((resolve) => {
|
|
63
|
+
healthy = false;
|
|
64
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
65
|
+
resolve();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 1000);
|
|
69
|
+
child.once("close", () => {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
resolve();
|
|
72
|
+
});
|
|
73
|
+
child.stdin?.end();
|
|
74
|
+
})),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/** Local runtime facts for explicitly armed resume; no task text, grants, approval or model verdicts. */
|
|
2
|
+
import { lockRuntimeSettlement } from "./runtime-settlement-lock.ts";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import {
|
|
5
|
+
mkdirSync,
|
|
6
|
+
realpathSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
openSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
fsyncSync,
|
|
11
|
+
closeSync,
|
|
12
|
+
renameSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
constants,
|
|
15
|
+
readFileSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { readBoundedTextSync } from "../kernel/bounded-read-sync.ts";
|
|
19
|
+
import type { CapturedWorkerCleanup, CapturedWorkerIdentity } from "../kernel/captured-worker-contract.ts";
|
|
20
|
+
import { isCapturedWorkerIdentity, readCapturedWorkerReceipt, sameWorkerIdentity } from "./captured-worker-record.ts";
|
|
21
|
+
const MAX_RECORD = 4 * 1024 * 1024;
|
|
22
|
+
interface Execution {
|
|
23
|
+
id: string;
|
|
24
|
+
state: "pending" | "settled" | "not-started" | "unknown";
|
|
25
|
+
identity?: CapturedWorkerIdentity;
|
|
26
|
+
receiptSha256?: string;
|
|
27
|
+
problem?: string;
|
|
28
|
+
}
|
|
29
|
+
interface Journal {
|
|
30
|
+
version: 1;
|
|
31
|
+
sessionId: string;
|
|
32
|
+
cwd: string;
|
|
33
|
+
ownerScope: string;
|
|
34
|
+
ownerId: string;
|
|
35
|
+
executions: Execution[];
|
|
36
|
+
}
|
|
37
|
+
export const settlementHash = (value: unknown): string =>
|
|
38
|
+
createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
39
|
+
const ownerId = (): string => {
|
|
40
|
+
const stat = readFileSync("/proc/self/stat", "utf8");
|
|
41
|
+
return [
|
|
42
|
+
readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim(),
|
|
43
|
+
process.pid,
|
|
44
|
+
stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19],
|
|
45
|
+
].join(":");
|
|
46
|
+
};
|
|
47
|
+
function ownerAlive(id: string): boolean {
|
|
48
|
+
const [boot, pid, ticks] = id.split(":");
|
|
49
|
+
if (!/^\d+$/.test(pid ?? "") || !/^\d+$/.test(ticks ?? "")) throw Error("invalid runtime journal owner");
|
|
50
|
+
if (readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim() !== boot) return false;
|
|
51
|
+
try {
|
|
52
|
+
const text = readFileSync("/proc/" + pid + "/stat", "utf8");
|
|
53
|
+
return text.slice(text.lastIndexOf(")") + 2).split(" ")[19] === ticks;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function validate(value: unknown, sessionId: string, cwd: string): Journal {
|
|
60
|
+
const j = value as Journal;
|
|
61
|
+
if (
|
|
62
|
+
!j ||
|
|
63
|
+
j.version !== 1 ||
|
|
64
|
+
j.sessionId !== sessionId ||
|
|
65
|
+
j.cwd !== cwd ||
|
|
66
|
+
!/^[a-f0-9-]{36}$/.test(j.ownerScope) ||
|
|
67
|
+
typeof j.ownerId !== "string" ||
|
|
68
|
+
!Array.isArray(j.executions) ||
|
|
69
|
+
j.executions.length > 10000
|
|
70
|
+
)
|
|
71
|
+
throw Error("invalid runtime journal identity");
|
|
72
|
+
const ids = new Set<string>();
|
|
73
|
+
for (const e of j.executions) {
|
|
74
|
+
if (
|
|
75
|
+
!e ||
|
|
76
|
+
typeof e.id !== "string" ||
|
|
77
|
+
!e.id ||
|
|
78
|
+
e.id.length > 512 ||
|
|
79
|
+
ids.has(e.id) ||
|
|
80
|
+
!["pending", "settled", "not-started", "unknown"].includes(e.state) ||
|
|
81
|
+
(e.identity !== undefined && (!isCapturedWorkerIdentity(e.identity) || e.identity.executionId !== e.id)) ||
|
|
82
|
+
(e.receiptSha256 !== undefined && !/^[a-f0-9]{64}$/.test(e.receiptSha256)) ||
|
|
83
|
+
(e.state === "settled" && (!e.identity || !e.receiptSha256)) ||
|
|
84
|
+
(e.state === "not-started" && e.identity) ||
|
|
85
|
+
(e.problem !== undefined && typeof e.problem !== "string")
|
|
86
|
+
)
|
|
87
|
+
throw Error("invalid runtime journal execution");
|
|
88
|
+
ids.add(e.id);
|
|
89
|
+
}
|
|
90
|
+
return j;
|
|
91
|
+
}
|
|
92
|
+
export interface RuntimeSettlement {
|
|
93
|
+
readonly ownerScope: string;
|
|
94
|
+
readonly ownerId: string;
|
|
95
|
+
readonly sessionId: string;
|
|
96
|
+
readonly cwd: string;
|
|
97
|
+
begin(id: string): void;
|
|
98
|
+
bind(identity: CapturedWorkerIdentity): void;
|
|
99
|
+
finish(id: string, cleanup: CapturedWorkerCleanup, problem?: string): Promise<void>;
|
|
100
|
+
snapshot(): Promise<{
|
|
101
|
+
ownerScope: string;
|
|
102
|
+
ownerId: string;
|
|
103
|
+
evidenceDigest: string;
|
|
104
|
+
state: "idle" | "busy" | "unknown";
|
|
105
|
+
settledExecutionIds: string[];
|
|
106
|
+
outstandingExecutionIds: string[];
|
|
107
|
+
reason?: string;
|
|
108
|
+
}>;
|
|
109
|
+
}
|
|
110
|
+
export async function openRuntimeSettlement(
|
|
111
|
+
directory: string,
|
|
112
|
+
sessionId: string,
|
|
113
|
+
cwd: string,
|
|
114
|
+
): Promise<RuntimeSettlement> {
|
|
115
|
+
if (process.platform !== "linux" || process.arch !== "x64") throw Error("runtime settlement requires Linux x64");
|
|
116
|
+
if (!sessionId || sessionId.length > 512 || cwd !== realpathSync(cwd))
|
|
117
|
+
throw Error("runtime session/cwd identity is unavailable");
|
|
118
|
+
const root = join(realpathSync(directory), "pi-daddy", "runtime-settlement");
|
|
119
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
120
|
+
if (realpathSync(root) !== root) throw Error("runtime settlement root must not contain symlinks");
|
|
121
|
+
const info = lstatSync(root);
|
|
122
|
+
if (!info.isDirectory() || info.uid !== process.getuid!() || info.mode & 0o077)
|
|
123
|
+
throw Error("runtime settlement root must be private");
|
|
124
|
+
const path = join(root, settlementHash([sessionId, cwd]) + ".json");
|
|
125
|
+
const lock = await lockRuntimeSettlement(path + ".lock");
|
|
126
|
+
try {
|
|
127
|
+
let journal: Journal;
|
|
128
|
+
try {
|
|
129
|
+
journal = validate(JSON.parse(readBoundedTextSync(path, { maxBytes: MAX_RECORD }).text), sessionId, cwd);
|
|
130
|
+
if (ownerAlive(journal.ownerId)) throw Error("prior runtime owner is still alive");
|
|
131
|
+
journal.ownerId = ownerId();
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
134
|
+
journal = { version: 1, sessionId, cwd, ownerScope: randomUUID(), ownerId: ownerId(), executions: [] };
|
|
135
|
+
}
|
|
136
|
+
let failed: string | undefined,
|
|
137
|
+
active = new Set<string>();
|
|
138
|
+
const save = () => {
|
|
139
|
+
let temporary: string | undefined, fd: number | undefined;
|
|
140
|
+
try {
|
|
141
|
+
if (!lock.healthy()) throw Error("runtime ownership lock was lost");
|
|
142
|
+
const text = JSON.stringify(journal) + "\n";
|
|
143
|
+
if (Buffer.byteLength(text) > MAX_RECORD) throw Error("runtime settlement journal exceeds its bound");
|
|
144
|
+
temporary = path + "." + randomUUID();
|
|
145
|
+
fd = openSync(
|
|
146
|
+
temporary,
|
|
147
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
|
|
148
|
+
0o600,
|
|
149
|
+
);
|
|
150
|
+
writeFileSync(fd, text);
|
|
151
|
+
fsyncSync(fd);
|
|
152
|
+
closeSync(fd);
|
|
153
|
+
fd = undefined;
|
|
154
|
+
renameSync(temporary, path);
|
|
155
|
+
const parent = openSync(root, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
156
|
+
try {
|
|
157
|
+
fsyncSync(parent);
|
|
158
|
+
} finally {
|
|
159
|
+
closeSync(parent);
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
failed = String(error);
|
|
163
|
+
throw error;
|
|
164
|
+
} finally {
|
|
165
|
+
if (fd !== undefined) closeSync(fd);
|
|
166
|
+
if (temporary)
|
|
167
|
+
try {
|
|
168
|
+
unlinkSync(temporary);
|
|
169
|
+
} catch {}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const entry = (id: string) => {
|
|
173
|
+
const e = journal.executions.find((e) => e.id === id);
|
|
174
|
+
if (!e) throw Error("runtime execution was not registered");
|
|
175
|
+
return e;
|
|
176
|
+
};
|
|
177
|
+
save();
|
|
178
|
+
return {
|
|
179
|
+
ownerScope: journal.ownerScope,
|
|
180
|
+
ownerId: journal.ownerId,
|
|
181
|
+
sessionId,
|
|
182
|
+
cwd,
|
|
183
|
+
begin(id) {
|
|
184
|
+
if (failed) throw Error(failed);
|
|
185
|
+
if (
|
|
186
|
+
typeof id !== "string" ||
|
|
187
|
+
!id ||
|
|
188
|
+
id.length > 512 ||
|
|
189
|
+
journal.executions.some((e) => e.id === id) ||
|
|
190
|
+
journal.executions.length >= 10000
|
|
191
|
+
)
|
|
192
|
+
throw Error("runtime execution id must be fresh and bounded");
|
|
193
|
+
journal.executions.push({ id, state: "pending" });
|
|
194
|
+
active.add(id);
|
|
195
|
+
save();
|
|
196
|
+
},
|
|
197
|
+
bind(identity) {
|
|
198
|
+
const e = entry(identity.executionId);
|
|
199
|
+
if (e.state !== "pending" || (e.identity && !sameWorkerIdentity(e.identity, identity)))
|
|
200
|
+
throw Error("runtime ownership changed");
|
|
201
|
+
e.identity = { ...identity };
|
|
202
|
+
save();
|
|
203
|
+
},
|
|
204
|
+
async finish(id, cleanup, problem) {
|
|
205
|
+
const e = entry(id);
|
|
206
|
+
if (problem) e.problem = problem.slice(0, 2000);
|
|
207
|
+
try {
|
|
208
|
+
if (cleanup.state === "not-started" && !e.identity) e.state = "not-started";
|
|
209
|
+
else if (cleanup.state === "settled" && e.identity && sameWorkerIdentity(e.identity, cleanup.identity)) {
|
|
210
|
+
const receipt = await readCapturedWorkerReceipt(e.identity);
|
|
211
|
+
if (receipt) {
|
|
212
|
+
e.state = "settled";
|
|
213
|
+
e.receiptSha256 = settlementHash(receipt);
|
|
214
|
+
} else e.state = "unknown";
|
|
215
|
+
} else e.state = "unknown";
|
|
216
|
+
save();
|
|
217
|
+
} finally {
|
|
218
|
+
active.delete(id);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
async snapshot() {
|
|
222
|
+
if (!lock.healthy()) failed = "runtime ownership lock was lost";
|
|
223
|
+
let changed = false;
|
|
224
|
+
for (const e of journal.executions) {
|
|
225
|
+
if (active.has(e.id) || !e.identity) continue;
|
|
226
|
+
const receipt = await readCapturedWorkerReceipt(e.identity);
|
|
227
|
+
if (!receipt || (e.receiptSha256 && e.receiptSha256 !== settlementHash(receipt))) {
|
|
228
|
+
if (e.state === "settled") {
|
|
229
|
+
e.state = "unknown";
|
|
230
|
+
changed = true;
|
|
231
|
+
}
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (e.state !== "settled") {
|
|
235
|
+
e.state = "settled";
|
|
236
|
+
e.receiptSha256 = settlementHash(receipt);
|
|
237
|
+
changed = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (changed) {
|
|
241
|
+
try {
|
|
242
|
+
save();
|
|
243
|
+
} catch {}
|
|
244
|
+
}
|
|
245
|
+
const outstanding = journal.executions.filter(
|
|
246
|
+
(e) => !["settled", "not-started"].includes(e.state) || e.problem,
|
|
247
|
+
);
|
|
248
|
+
const state =
|
|
249
|
+
failed || outstanding.some((e) => !active.has(e.id))
|
|
250
|
+
? "unknown"
|
|
251
|
+
: active.size
|
|
252
|
+
? "busy"
|
|
253
|
+
: outstanding.length
|
|
254
|
+
? "unknown"
|
|
255
|
+
: "idle";
|
|
256
|
+
return {
|
|
257
|
+
ownerScope: journal.ownerScope,
|
|
258
|
+
ownerId: journal.ownerId,
|
|
259
|
+
evidenceDigest: settlementHash({ ownerScope: journal.ownerScope, executions: journal.executions }),
|
|
260
|
+
state,
|
|
261
|
+
settledExecutionIds: journal.executions.filter((e) => e.state === "settled").map((e) => e.id),
|
|
262
|
+
outstandingExecutionIds: outstanding.map((e) => e.id),
|
|
263
|
+
...(failed ? { reason: failed } : {}),
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
} catch (error) {
|
|
268
|
+
await lock.release();
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|