@elpapi42/pi-fleet 0.1.0-beta.10 → 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.
- package/CHANGELOG.md +28 -1
- package/README.md +99 -167
- package/dist/cli-meta.json +313 -315
- package/dist/cli.mjs +2504 -1812
- package/dist/cli.mjs.map +4 -4
- package/dist/client/agent-target.d.ts +33 -0
- package/dist/client/contracts.d.ts +81 -0
- package/dist/client/fleet-client.d.ts +118 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/sdk-connector.d.ts +7 -0
- package/dist/client/sdk-facade.d.ts +84 -0
- package/dist/client/sdk-transport.d.ts +24 -0
- package/dist/client/shared-client.d.ts +18 -0
- package/dist/client/socket-fleet-client.d.ts +21 -0
- package/dist/client-meta.json +5736 -0
- package/dist/client.mjs +4434 -0
- package/dist/client.mjs.map +7 -0
- package/dist/installer-meta.json +7 -7
- package/dist/installer.mjs +47 -13
- package/dist/installer.mjs.map +2 -2
- package/dist/journal-sqlite-worker-meta.json +149 -0
- package/dist/journal-sqlite-worker.mjs +1110 -0
- package/dist/journal-sqlite-worker.mjs.map +7 -0
- package/dist/pi/external-installation.d.ts +23 -0
- package/dist/platform/client/start-runtime.d.ts +25 -0
- package/dist/platform/install/runtime-release.d.ts +8 -0
- package/dist/platform/install/tree-integrity.d.ts +7 -0
- package/dist/platform/shared/paths.d.ts +8 -0
- package/dist/platform/shared/runtime-ownership.d.ts +14 -0
- package/dist/protocol/jsonl.d.ts +3 -0
- package/dist/protocol/pi-identity.d.ts +16 -0
- package/dist/protocol/semantic-segmentation.d.ts +22 -0
- package/dist/protocol/version.d.ts +2 -0
- package/dist/runtime/semantic-events.d.ts +43 -0
- package/dist/runtime-manifest.json +156 -31
- package/dist/runtime-meta.json +378 -53
- package/dist/runtime.mjs +3161 -581
- package/dist/runtime.mjs.map +4 -4
- package/dist/shared/result.d.ts +9 -0
- package/package.json +14 -1
- package/dist/sqlite-worker-meta.json +0 -84
- package/dist/sqlite-worker.mjs +0 -359
- package/dist/sqlite-worker.mjs.map +0 -7
package/dist/runtime.mjs
CHANGED
|
@@ -5,8 +5,191 @@ var __export = (target, all) => {
|
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
// src/entry/runtime.ts
|
|
8
|
+
import { existsSync, readdirSync as readdirSync2, statSync } from "node:fs";
|
|
9
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
8
10
|
import { pathToFileURL } from "node:url";
|
|
9
11
|
|
|
12
|
+
// src/runtime/receive-pager.ts
|
|
13
|
+
var ReceiveBoundaryError = class extends Error {
|
|
14
|
+
constructor(code) {
|
|
15
|
+
super("Receive cursor cannot be used.");
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.name = "ReceiveBoundaryError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var OpaqueReceiveCursorCodec = class {
|
|
21
|
+
version = 1;
|
|
22
|
+
encode(boundary) {
|
|
23
|
+
assertPosition(boundary.position);
|
|
24
|
+
assertPosition(boundary.epoch);
|
|
25
|
+
const payload = {
|
|
26
|
+
v: this.version,
|
|
27
|
+
a: boundary.agentId,
|
|
28
|
+
e: boundary.epoch,
|
|
29
|
+
p: boundary.position,
|
|
30
|
+
...boundary.kind === "continuation" ? { k: "continuation" } : {}
|
|
31
|
+
};
|
|
32
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
33
|
+
}
|
|
34
|
+
decode(cursor) {
|
|
35
|
+
let value;
|
|
36
|
+
try {
|
|
37
|
+
value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
38
|
+
} catch {
|
|
39
|
+
throw new ReceiveBoundaryError("cursor_invalid");
|
|
40
|
+
}
|
|
41
|
+
if (!isCursorPayload(value)) throw new ReceiveBoundaryError("cursor_invalid");
|
|
42
|
+
return {
|
|
43
|
+
agentId: value.a,
|
|
44
|
+
epoch: value.e,
|
|
45
|
+
position: value.p,
|
|
46
|
+
kind: value.k === "continuation" ? "continuation" : "position"
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var ReceiveObservationUncertainError = class extends Error {
|
|
51
|
+
constructor(lastSafeCursor, continuationCursor) {
|
|
52
|
+
super("Receive observation continuity is uncertain");
|
|
53
|
+
this.lastSafeCursor = lastSafeCursor;
|
|
54
|
+
this.continuationCursor = continuationCursor;
|
|
55
|
+
this.name = "ReceiveObservationUncertainError";
|
|
56
|
+
}
|
|
57
|
+
code = "observation_uncertain";
|
|
58
|
+
};
|
|
59
|
+
var ReceivePager = class {
|
|
60
|
+
constructor(store, cursors, wakeup, limits, activity = {}) {
|
|
61
|
+
this.store = store;
|
|
62
|
+
this.cursors = cursors;
|
|
63
|
+
this.wakeup = wakeup;
|
|
64
|
+
this.limits = limits;
|
|
65
|
+
this.activity = activity;
|
|
66
|
+
assertPositiveInteger(limits.maxRows, "Receive replay row limit");
|
|
67
|
+
assertPositiveInteger(limits.maxBytes, "Receive replay byte limit");
|
|
68
|
+
assertPositiveInteger(limits.maxEventBytes, "Receive semantic event limit");
|
|
69
|
+
}
|
|
70
|
+
async open(agentId, start, signal) {
|
|
71
|
+
throwIfAborted(signal);
|
|
72
|
+
const snapshot = await this.store.openReceive(agentId);
|
|
73
|
+
if (snapshot === null) throw codedError("agent_not_found", "Agent generation was not found");
|
|
74
|
+
const boundary = this.#boundary(snapshot, start);
|
|
75
|
+
const initialCursor = this.cursors.encode(boundary);
|
|
76
|
+
return {
|
|
77
|
+
cursor: initialCursor,
|
|
78
|
+
[Symbol.asyncIterator]: () => this.#events(agentId, boundary.epoch, boundary.position, signal)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
#boundary(snapshot, start) {
|
|
82
|
+
const latest = snapshot.epochs.at(-1);
|
|
83
|
+
if (latest === void 0) throw codedError("state_corrupt", "Agent has no continuity epoch");
|
|
84
|
+
if (start.kind === "live") {
|
|
85
|
+
return {
|
|
86
|
+
agentId: snapshot.agent.agentId,
|
|
87
|
+
epoch: latest.epoch,
|
|
88
|
+
position: snapshot.highWater.eventPosition
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (start.kind === "start") {
|
|
92
|
+
return {
|
|
93
|
+
agentId: snapshot.agent.agentId,
|
|
94
|
+
epoch: snapshot.epochs[0]?.epoch ?? latest.epoch,
|
|
95
|
+
position: 0
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const decoded = this.cursors.decode(start.cursor);
|
|
99
|
+
if (decoded.agentId !== snapshot.agent.agentId) {
|
|
100
|
+
throw new ReceiveBoundaryError("cursor_wrong_agent");
|
|
101
|
+
}
|
|
102
|
+
const epochIndex = snapshot.epochs.findIndex((epoch) => epoch.epoch === decoded.epoch);
|
|
103
|
+
if (epochIndex < 0) throw new ReceiveBoundaryError("cursor_expired");
|
|
104
|
+
if (decoded.position > snapshot.highWater.eventPosition) {
|
|
105
|
+
throw new ReceiveBoundaryError("cursor_invalid");
|
|
106
|
+
}
|
|
107
|
+
const selectedEpoch = snapshot.epochs[epochIndex];
|
|
108
|
+
if (selectedEpoch.state === "closed" && decoded.kind === "position" && decoded.position > selectedEpoch.lastSafeEventPosition) {
|
|
109
|
+
throw new ReceiveBoundaryError("cursor_invalid");
|
|
110
|
+
}
|
|
111
|
+
if (decoded.kind === "continuation") {
|
|
112
|
+
const previous = snapshot.epochs[epochIndex - 1];
|
|
113
|
+
if (previous === void 0 || previous.state !== "closed" || previous.reason !== "observation_uncertain" || previous.lastSafeEventPosition !== decoded.position) {
|
|
114
|
+
throw new ReceiveBoundaryError("cursor_invalid");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return decoded;
|
|
118
|
+
}
|
|
119
|
+
async *#events(agentId, initialEpoch, initialPosition, signal) {
|
|
120
|
+
let epoch = initialEpoch;
|
|
121
|
+
let position = initialPosition;
|
|
122
|
+
while (true) {
|
|
123
|
+
throwIfAborted(signal);
|
|
124
|
+
this.activity.onReadStart?.();
|
|
125
|
+
const page = await this.store.readEvents({
|
|
126
|
+
agentId,
|
|
127
|
+
epoch,
|
|
128
|
+
afterPosition: position,
|
|
129
|
+
limit: this.limits.maxRows,
|
|
130
|
+
maxBytes: this.limits.maxBytes,
|
|
131
|
+
maxEventBytes: this.limits.maxEventBytes
|
|
132
|
+
}).finally(() => this.activity.onReadEnd?.());
|
|
133
|
+
if (page.length > 0) {
|
|
134
|
+
for (const stored of page) {
|
|
135
|
+
const cursor = this.cursors.encode({
|
|
136
|
+
agentId,
|
|
137
|
+
epoch: stored.event.epoch,
|
|
138
|
+
position: stored.position
|
|
139
|
+
});
|
|
140
|
+
position = stored.position;
|
|
141
|
+
yield { ...stored.event, cursor };
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const snapshot = await this.store.openReceive(agentId);
|
|
146
|
+
if (snapshot === null) throw codedError("agent_destroyed", "Agent was destroyed");
|
|
147
|
+
const epochIndex = snapshot.epochs.findIndex((candidate) => candidate.epoch === epoch);
|
|
148
|
+
if (epochIndex < 0) throw new ReceiveBoundaryError("cursor_expired");
|
|
149
|
+
const current = snapshot.epochs[epochIndex];
|
|
150
|
+
const next = snapshot.epochs[epochIndex + 1];
|
|
151
|
+
if (current.state === "closed" && position >= current.lastSafeEventPosition) {
|
|
152
|
+
if (current.reason === "observation_uncertain") {
|
|
153
|
+
const lastSafeCursor = this.cursors.encode({
|
|
154
|
+
agentId,
|
|
155
|
+
epoch: current.epoch,
|
|
156
|
+
position: current.lastSafeEventPosition
|
|
157
|
+
});
|
|
158
|
+
const continuationCursor = next === void 0 ? null : this.cursors.encode({
|
|
159
|
+
agentId,
|
|
160
|
+
epoch: next.epoch,
|
|
161
|
+
position: current.lastSafeEventPosition,
|
|
162
|
+
kind: "continuation"
|
|
163
|
+
});
|
|
164
|
+
throw new ReceiveObservationUncertainError(lastSafeCursor, continuationCursor);
|
|
165
|
+
}
|
|
166
|
+
if (next !== void 0) {
|
|
167
|
+
epoch = next.epoch;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
await this.wakeup.waitForEvents(agentId, position, signal);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
function codedError(code, message) {
|
|
176
|
+
return Object.assign(new Error(message), { code });
|
|
177
|
+
}
|
|
178
|
+
function isCursorPayload(value) {
|
|
179
|
+
if (typeof value !== "object" || value === null) return false;
|
|
180
|
+
const payload = value;
|
|
181
|
+
return payload.v === 1 && (payload.k === void 0 || payload.k === "continuation") && typeof payload.a === "string" && payload.a.length > 0 && Number.isSafeInteger(payload.e) && (payload.e ?? -1) >= 0 && Number.isSafeInteger(payload.p) && (payload.p ?? -1) >= 0;
|
|
182
|
+
}
|
|
183
|
+
function assertPosition(value, label = "Receive cursor position") {
|
|
184
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label} must be non-negative`);
|
|
185
|
+
}
|
|
186
|
+
function assertPositiveInteger(value, label) {
|
|
187
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${label} must be positive`);
|
|
188
|
+
}
|
|
189
|
+
function throwIfAborted(signal) {
|
|
190
|
+
if (signal.aborted) throw signal.reason ?? new Error("Receive cancelled");
|
|
191
|
+
}
|
|
192
|
+
|
|
10
193
|
// node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
|
|
11
194
|
var value_exports = {};
|
|
12
195
|
__export(value_exports, {
|
|
@@ -1056,7 +1239,7 @@ function Literal(value, options) {
|
|
|
1056
1239
|
}
|
|
1057
1240
|
|
|
1058
1241
|
// node_modules/@sinclair/typebox/build/esm/type/boolean/boolean.mjs
|
|
1059
|
-
function
|
|
1242
|
+
function Boolean2(options) {
|
|
1060
1243
|
return CreateType({ [Kind]: "Boolean", type: "boolean" }, options);
|
|
1061
1244
|
}
|
|
1062
1245
|
|
|
@@ -1078,7 +1261,7 @@ function String2(options) {
|
|
|
1078
1261
|
// node_modules/@sinclair/typebox/build/esm/type/template-literal/syntax.mjs
|
|
1079
1262
|
function* FromUnion(syntax) {
|
|
1080
1263
|
const trim = syntax.trim().replace(/"|'/g, "");
|
|
1081
|
-
return trim === "boolean" ? yield
|
|
1264
|
+
return trim === "boolean" ? yield Boolean2() : trim === "number" ? yield Number2() : trim === "bigint" ? yield BigInt2() : trim === "string" ? yield String2() : yield (() => {
|
|
1082
1265
|
const literals = trim.split("|").map((literal) => Literal(literal.trim()));
|
|
1083
1266
|
return literals.length === 0 ? Never() : literals.length === 1 ? literals[0] : UnionEvaluated(literals);
|
|
1084
1267
|
})();
|
|
@@ -2694,7 +2877,7 @@ __export(type_exports3, {
|
|
|
2694
2877
|
AsyncIterator: () => AsyncIterator,
|
|
2695
2878
|
Awaited: () => Awaited,
|
|
2696
2879
|
BigInt: () => BigInt2,
|
|
2697
|
-
Boolean: () =>
|
|
2880
|
+
Boolean: () => Boolean2,
|
|
2698
2881
|
Capitalize: () => Capitalize,
|
|
2699
2882
|
Composite: () => Composite,
|
|
2700
2883
|
Const: () => Const,
|
|
@@ -2808,6 +2991,16 @@ var ExternalPiResolutionError = class extends Error {
|
|
|
2808
2991
|
this.name = "ExternalPiResolutionError";
|
|
2809
2992
|
}
|
|
2810
2993
|
};
|
|
2994
|
+
function installationIdentity(installation) {
|
|
2995
|
+
return {
|
|
2996
|
+
mode: "external",
|
|
2997
|
+
selectedPath: installation.selectedPath,
|
|
2998
|
+
nodePath: installation.nodePath,
|
|
2999
|
+
realPath: installation.realPath,
|
|
3000
|
+
version: installation.version,
|
|
3001
|
+
fingerprint: installation.fingerprint
|
|
3002
|
+
};
|
|
3003
|
+
}
|
|
2811
3004
|
async function resolveExternalPiInstallation(options = {}) {
|
|
2812
3005
|
const env = options.env ?? process.env;
|
|
2813
3006
|
const selectedPath = await resolveSelectedPath(env);
|
|
@@ -3058,10 +3251,10 @@ async function waitForProcessGroupExit(processGroupId, timeoutMs = 1e3) {
|
|
|
3058
3251
|
}
|
|
3059
3252
|
function readLinuxProcess(pid) {
|
|
3060
3253
|
try {
|
|
3061
|
-
const
|
|
3062
|
-
const commandEnd =
|
|
3254
|
+
const stat3 = readFileSync(`/proc/${String(pid)}/stat`, "utf8");
|
|
3255
|
+
const commandEnd = stat3.lastIndexOf(")");
|
|
3063
3256
|
if (commandEnd < 0) return void 0;
|
|
3064
|
-
const fields =
|
|
3257
|
+
const fields = stat3.slice(commandEnd + 2).trim().split(/\s+/);
|
|
3065
3258
|
const state = fields[0];
|
|
3066
3259
|
const processGroupId = Number(fields[2]);
|
|
3067
3260
|
if (state === void 0 || !Number.isSafeInteger(processGroupId)) return void 0;
|
|
@@ -3091,8 +3284,76 @@ async function waitUntilGone(isAlive, timeoutMs) {
|
|
|
3091
3284
|
return !isAlive();
|
|
3092
3285
|
}
|
|
3093
3286
|
|
|
3287
|
+
// src/pi/rpc-record-framer.ts
|
|
3288
|
+
var RpcPartialRecordLimitError = class extends Error {
|
|
3289
|
+
constructor(pendingBytes, limitBytes) {
|
|
3290
|
+
super(`Pi RPC unterminated record exceeded ${String(limitBytes)} bytes`);
|
|
3291
|
+
this.pendingBytes = pendingBytes;
|
|
3292
|
+
this.limitBytes = limitBytes;
|
|
3293
|
+
this.name = "RpcPartialRecordLimitError";
|
|
3294
|
+
}
|
|
3295
|
+
};
|
|
3296
|
+
var RpcRecordFramer = class {
|
|
3297
|
+
constructor(maxPartialRecordBytes) {
|
|
3298
|
+
this.maxPartialRecordBytes = maxPartialRecordBytes;
|
|
3299
|
+
if (!Number.isSafeInteger(maxPartialRecordBytes) || maxPartialRecordBytes <= 0) {
|
|
3300
|
+
throw new Error("maxPartialRecordBytes must be a positive safe integer");
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
#segments = [];
|
|
3304
|
+
#pendingBytes = 0;
|
|
3305
|
+
#failed = false;
|
|
3306
|
+
get pendingBytes() {
|
|
3307
|
+
return this.#pendingBytes;
|
|
3308
|
+
}
|
|
3309
|
+
push(chunk) {
|
|
3310
|
+
if (this.#failed) throw new Error("Pi RPC record framer has failed");
|
|
3311
|
+
if (chunk.length === 0) return [];
|
|
3312
|
+
const records = [];
|
|
3313
|
+
let offset = 0;
|
|
3314
|
+
while (offset < chunk.length) {
|
|
3315
|
+
const newline = chunk.indexOf(10, offset);
|
|
3316
|
+
if (newline === -1) {
|
|
3317
|
+
this.#appendPartial(chunk.subarray(offset));
|
|
3318
|
+
break;
|
|
3319
|
+
}
|
|
3320
|
+
const finalSegment = chunk.subarray(offset, newline + 1);
|
|
3321
|
+
if (this.#pendingBytes === 0) {
|
|
3322
|
+
records.push(Buffer.from(finalSegment));
|
|
3323
|
+
} else {
|
|
3324
|
+
this.#segments.push(finalSegment);
|
|
3325
|
+
this.#pendingBytes += finalSegment.length;
|
|
3326
|
+
records.push(Buffer.concat(this.#segments, this.#pendingBytes));
|
|
3327
|
+
this.#segments.length = 0;
|
|
3328
|
+
this.#pendingBytes = 0;
|
|
3329
|
+
}
|
|
3330
|
+
offset = newline + 1;
|
|
3331
|
+
}
|
|
3332
|
+
return records;
|
|
3333
|
+
}
|
|
3334
|
+
/** Returns, but never promotes, an interrupted trailing fragment. */
|
|
3335
|
+
finish() {
|
|
3336
|
+
if (this.#pendingBytes === 0) return null;
|
|
3337
|
+
const trailing = Buffer.concat(this.#segments, this.#pendingBytes);
|
|
3338
|
+
this.#segments.length = 0;
|
|
3339
|
+
this.#pendingBytes = 0;
|
|
3340
|
+
return trailing;
|
|
3341
|
+
}
|
|
3342
|
+
#appendPartial(segment) {
|
|
3343
|
+
if (segment.length === 0) return;
|
|
3344
|
+
const nextBytes = this.#pendingBytes + segment.length;
|
|
3345
|
+
if (nextBytes > this.maxPartialRecordBytes) {
|
|
3346
|
+
this.#failed = true;
|
|
3347
|
+
throw new RpcPartialRecordLimitError(nextBytes, this.maxPartialRecordBytes);
|
|
3348
|
+
}
|
|
3349
|
+
this.#segments.push(Buffer.from(segment));
|
|
3350
|
+
this.#pendingBytes = nextBytes;
|
|
3351
|
+
}
|
|
3352
|
+
};
|
|
3353
|
+
|
|
3094
3354
|
// src/pi/process.ts
|
|
3095
3355
|
var DEFAULT_MAX_STDOUT_FRAME_BYTES = 8 * 1024 * 1024;
|
|
3356
|
+
var DEFAULT_MAX_PARTIAL_RECORD_BYTES = 64 * 1024 * 1024;
|
|
3096
3357
|
var PiRpcRejectedError = class extends Error {
|
|
3097
3358
|
constructor(message) {
|
|
3098
3359
|
super(message);
|
|
@@ -3106,6 +3367,13 @@ var PiCompactionError = class extends Error {
|
|
|
3106
3367
|
this.name = "PiCompactionError";
|
|
3107
3368
|
}
|
|
3108
3369
|
};
|
|
3370
|
+
var PiResponseCommitDelayError = class extends Error {
|
|
3371
|
+
code = "storage_unavailable";
|
|
3372
|
+
constructor() {
|
|
3373
|
+
super("Pi responded, but pi-fleet could not durably commit the response in time");
|
|
3374
|
+
this.name = "PiResponseCommitDelayError";
|
|
3375
|
+
}
|
|
3376
|
+
};
|
|
3109
3377
|
var PiCleanupUncertainError = class extends Error {
|
|
3110
3378
|
constructor(pid, startupError, cleanupError) {
|
|
3111
3379
|
super(`Pi process group ${String(pid)} could not be cleaned up after startup failed`);
|
|
@@ -3115,23 +3383,57 @@ var PiCleanupUncertainError = class extends Error {
|
|
|
3115
3383
|
this.name = "PiCleanupUncertainError";
|
|
3116
3384
|
}
|
|
3117
3385
|
};
|
|
3386
|
+
var PiResponseAdmission = class {
|
|
3387
|
+
#state = "awaiting_response";
|
|
3388
|
+
get state() {
|
|
3389
|
+
return this.#state;
|
|
3390
|
+
}
|
|
3391
|
+
admit() {
|
|
3392
|
+
if (this.#state !== "awaiting_response") {
|
|
3393
|
+
throw new Error(`Cannot admit response while ${this.#state}`);
|
|
3394
|
+
}
|
|
3395
|
+
this.#state = "admitted_pending_commit";
|
|
3396
|
+
}
|
|
3397
|
+
commit() {
|
|
3398
|
+
if (this.#state !== "admitted_pending_commit") {
|
|
3399
|
+
throw new Error(`Cannot commit response while ${this.#state}`);
|
|
3400
|
+
}
|
|
3401
|
+
this.#state = "committed";
|
|
3402
|
+
}
|
|
3403
|
+
fail() {
|
|
3404
|
+
if (this.#state === "committed") {
|
|
3405
|
+
throw new Error("Cannot fail a committed response");
|
|
3406
|
+
}
|
|
3407
|
+
this.#state = "failed";
|
|
3408
|
+
}
|
|
3409
|
+
};
|
|
3118
3410
|
var PiProcess = class _PiProcess {
|
|
3119
3411
|
#child;
|
|
3120
3412
|
#responses = /* @__PURE__ */ new Map();
|
|
3121
3413
|
#listeners = /* @__PURE__ */ new Set();
|
|
3122
3414
|
#exitListeners = /* @__PURE__ */ new Set();
|
|
3123
|
-
#
|
|
3415
|
+
#stdoutFramer;
|
|
3124
3416
|
#stderr = "";
|
|
3125
3417
|
#stopping = false;
|
|
3126
3418
|
#handledExit = false;
|
|
3127
3419
|
#exitHandled;
|
|
3128
3420
|
#resolveExitHandled = () => void 0;
|
|
3129
3421
|
#maxStdoutFrameBytes;
|
|
3422
|
+
#onStdoutRecord;
|
|
3423
|
+
#stdoutLane = Promise.resolve();
|
|
3424
|
+
#stdoutFailure = null;
|
|
3130
3425
|
constructor(options) {
|
|
3131
|
-
this.#exitHandled = new Promise((
|
|
3132
|
-
this.#resolveExitHandled =
|
|
3426
|
+
this.#exitHandled = new Promise((resolve3) => {
|
|
3427
|
+
this.#resolveExitHandled = resolve3;
|
|
3133
3428
|
});
|
|
3134
3429
|
this.#maxStdoutFrameBytes = options.maxStdoutFrameBytes ?? DEFAULT_MAX_STDOUT_FRAME_BYTES;
|
|
3430
|
+
this.#stdoutFramer = new RpcRecordFramer(
|
|
3431
|
+
options.maxPartialRecordBytes ?? DEFAULT_MAX_PARTIAL_RECORD_BYTES
|
|
3432
|
+
);
|
|
3433
|
+
this.#onStdoutRecord = async (record) => {
|
|
3434
|
+
if (options.onStdoutRecord !== void 0) await options.onStdoutRecord(record);
|
|
3435
|
+
else options.onStdoutBytes?.(record);
|
|
3436
|
+
};
|
|
3135
3437
|
this.#child = spawn2(
|
|
3136
3438
|
options.executable,
|
|
3137
3439
|
[...options.argvPrefix ?? [], "--mode", "rpc", ...options.piArgv],
|
|
@@ -3144,14 +3446,18 @@ var PiProcess = class _PiProcess {
|
|
|
3144
3446
|
);
|
|
3145
3447
|
this.#child.stderr.setEncoding("utf8");
|
|
3146
3448
|
this.#child.stdout.on("data", (chunk) => {
|
|
3147
|
-
|
|
3148
|
-
this.#consumeStdout(chunk)
|
|
3449
|
+
this.#child.stdout.pause();
|
|
3450
|
+
this.#stdoutLane = this.#stdoutLane.then(() => this.#consumeStdout(chunk)).catch((error) => this.#failStdout(error)).finally(() => {
|
|
3451
|
+
if (this.#child.exitCode === null && this.#stdoutFailure === null) {
|
|
3452
|
+
this.#child.stdout.resume();
|
|
3453
|
+
}
|
|
3454
|
+
});
|
|
3149
3455
|
});
|
|
3150
3456
|
this.#child.stderr.on("data", (chunk) => {
|
|
3151
3457
|
this.#stderr = `${this.#stderr}${chunk}`.slice(-65536);
|
|
3152
3458
|
});
|
|
3153
|
-
this.#child.once("exit", (code, signal) => this.#handleExit(code, signal));
|
|
3154
|
-
this.#child.once("error", (error) => this.#handleExit(null, null, error));
|
|
3459
|
+
this.#child.once("exit", (code, signal) => void this.#handleExit(code, signal));
|
|
3460
|
+
this.#child.once("error", (error) => void this.#handleExit(null, null, error));
|
|
3155
3461
|
}
|
|
3156
3462
|
static async start(options) {
|
|
3157
3463
|
const process2 = new _PiProcess(options);
|
|
@@ -3190,6 +3496,9 @@ var PiProcess = class _PiProcess {
|
|
|
3190
3496
|
async prompt(message) {
|
|
3191
3497
|
await this.request({ type: "prompt", message, streamingBehavior: "steer" });
|
|
3192
3498
|
}
|
|
3499
|
+
async followUp(message) {
|
|
3500
|
+
await this.request({ type: "follow_up", message });
|
|
3501
|
+
}
|
|
3193
3502
|
async compact() {
|
|
3194
3503
|
let frame;
|
|
3195
3504
|
try {
|
|
@@ -3216,12 +3525,23 @@ var PiProcess = class _PiProcess {
|
|
|
3216
3525
|
async request(command, timeoutMs = 15e3) {
|
|
3217
3526
|
if (this.#child.exitCode !== null) throw new Error("Pi process is not running");
|
|
3218
3527
|
const id = randomUUID();
|
|
3528
|
+
const admission = new PiResponseAdmission();
|
|
3219
3529
|
const response = new Promise((resolveResponse, rejectResponse) => {
|
|
3220
3530
|
const timer = setTimeout(() => {
|
|
3531
|
+
if (admission.state === "admitted_pending_commit") {
|
|
3532
|
+
this.#failStdout(new PiResponseCommitDelayError());
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3221
3535
|
this.#responses.delete(id);
|
|
3536
|
+
admission.fail();
|
|
3222
3537
|
rejectResponse(new Error("Pi RPC request timed out"));
|
|
3223
3538
|
}, timeoutMs);
|
|
3224
|
-
this.#responses.set(id, {
|
|
3539
|
+
this.#responses.set(id, {
|
|
3540
|
+
resolve: resolveResponse,
|
|
3541
|
+
reject: rejectResponse,
|
|
3542
|
+
timer,
|
|
3543
|
+
admission
|
|
3544
|
+
});
|
|
3225
3545
|
});
|
|
3226
3546
|
try {
|
|
3227
3547
|
await this.#write({ ...command, id });
|
|
@@ -3230,6 +3550,7 @@ var PiProcess = class _PiProcess {
|
|
|
3230
3550
|
if (waiter !== void 0) {
|
|
3231
3551
|
clearTimeout(waiter.timer);
|
|
3232
3552
|
this.#responses.delete(id);
|
|
3553
|
+
waiter.admission.fail();
|
|
3233
3554
|
waiter.reject(error instanceof Error ? error : new Error("Pi RPC write failed"));
|
|
3234
3555
|
}
|
|
3235
3556
|
}
|
|
@@ -3266,55 +3587,68 @@ var PiProcess = class _PiProcess {
|
|
|
3266
3587
|
`)) return;
|
|
3267
3588
|
await once(this.#child.stdin, "drain");
|
|
3268
3589
|
}
|
|
3269
|
-
#consumeStdout(chunk) {
|
|
3270
|
-
this.#
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
signalProcessTree(this.pid, "SIGTERM");
|
|
3276
|
-
}
|
|
3277
|
-
return;
|
|
3278
|
-
}
|
|
3279
|
-
if (newline > this.#maxStdoutFrameBytes) {
|
|
3280
|
-
signalProcessTree(this.pid, "SIGTERM");
|
|
3281
|
-
return;
|
|
3590
|
+
async #consumeStdout(chunk) {
|
|
3591
|
+
for (const record of this.#stdoutFramer.push(chunk)) {
|
|
3592
|
+
const commit = this.#onStdoutRecord(record);
|
|
3593
|
+
if (record.length - 1 > this.#maxStdoutFrameBytes) {
|
|
3594
|
+
await commit;
|
|
3595
|
+
throw new Error("Pi RPC record exceeds the configured parser limit");
|
|
3282
3596
|
}
|
|
3283
|
-
let lineBytes =
|
|
3284
|
-
this.#stdoutBuffer = this.#stdoutBuffer.subarray(newline + 1);
|
|
3597
|
+
let lineBytes = record.subarray(0, record.length - 1);
|
|
3285
3598
|
if (lineBytes.at(-1) === 13) lineBytes = lineBytes.subarray(0, -1);
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3599
|
+
let frame = null;
|
|
3600
|
+
if (lineBytes.length > 0) {
|
|
3601
|
+
try {
|
|
3602
|
+
const line = new TextDecoder("utf-8", { fatal: true }).decode(lineBytes);
|
|
3603
|
+
frame = JSON.parse(line);
|
|
3604
|
+
} catch {
|
|
3605
|
+
await commit;
|
|
3606
|
+
throw new Error("Pi emitted a malformed RPC record");
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
const waiter = frame?.type === "response" && typeof frame.id === "string" ? this.#responses.get(frame.id) : void 0;
|
|
3610
|
+
if (waiter !== void 0) waiter.admission.admit();
|
|
3611
|
+
await commit;
|
|
3612
|
+
if (waiter !== void 0) {
|
|
3613
|
+
if (waiter.admission.state !== "admitted_pending_commit") continue;
|
|
3614
|
+
waiter.admission.commit();
|
|
3615
|
+
clearTimeout(waiter.timer);
|
|
3616
|
+
this.#responses.delete(frame.id);
|
|
3617
|
+
waiter.resolve(frame);
|
|
3294
3618
|
}
|
|
3619
|
+
if (frame === null) continue;
|
|
3295
3620
|
if (frame.type === "extension_ui_request" && typeof frame.id === "string" && ["select", "confirm", "input", "editor"].includes(String(frame.method))) {
|
|
3296
3621
|
void this.#write({ type: "extension_ui_response", id: frame.id, cancelled: true }).catch(
|
|
3297
3622
|
() => void 0
|
|
3298
3623
|
);
|
|
3299
3624
|
}
|
|
3300
|
-
if (frame.type === "response" && frame.id !== void 0) {
|
|
3301
|
-
const waiter = this.#responses.get(frame.id);
|
|
3302
|
-
if (waiter !== void 0) {
|
|
3303
|
-
clearTimeout(waiter.timer);
|
|
3304
|
-
this.#responses.delete(frame.id);
|
|
3305
|
-
waiter.resolve(frame);
|
|
3306
|
-
}
|
|
3307
|
-
}
|
|
3308
3625
|
for (const listener of this.#listeners) listener(frame);
|
|
3309
3626
|
}
|
|
3310
3627
|
}
|
|
3311
|
-
|
|
3628
|
+
async drainStdout() {
|
|
3629
|
+
await this.#stdoutLane;
|
|
3630
|
+
}
|
|
3631
|
+
#failStdout(error) {
|
|
3632
|
+
if (this.#stdoutFailure !== null) return;
|
|
3633
|
+
this.#stdoutFailure = error instanceof Error ? error : new Error("Pi stdout persistence failed");
|
|
3634
|
+
for (const waiter of this.#responses.values()) {
|
|
3635
|
+
clearTimeout(waiter.timer);
|
|
3636
|
+
if (waiter.admission.state !== "committed") waiter.admission.fail();
|
|
3637
|
+
waiter.reject(this.#stdoutFailure);
|
|
3638
|
+
}
|
|
3639
|
+
this.#responses.clear();
|
|
3640
|
+
signalProcessTree(this.pid, "SIGTERM");
|
|
3641
|
+
}
|
|
3642
|
+
async #handleExit(code, signal, cause) {
|
|
3312
3643
|
if (this.#handledExit) return;
|
|
3313
3644
|
this.#handledExit = true;
|
|
3314
|
-
|
|
3645
|
+
await this.#stdoutLane.catch(() => void 0);
|
|
3646
|
+
const trailingPartial = this.#stdoutFramer.finish();
|
|
3647
|
+
const error = this.#stdoutFailure ?? (trailingPartial !== null ? new Error("Pi exited with an unterminated RPC record") : this.#stopping && (code === 0 || signal === "SIGTERM") ? null : cause ?? new Error(`Pi exited unexpectedly (code=${String(code)}, signal=${String(signal)})`));
|
|
3315
3648
|
try {
|
|
3316
3649
|
for (const waiter of this.#responses.values()) {
|
|
3317
3650
|
clearTimeout(waiter.timer);
|
|
3651
|
+
if (waiter.admission.state !== "committed") waiter.admission.fail();
|
|
3318
3652
|
waiter.reject(error ?? new Error("Pi stopped before responding"));
|
|
3319
3653
|
}
|
|
3320
3654
|
this.#responses.clear();
|
|
@@ -3342,7 +3676,7 @@ var RealPiLauncher = class {
|
|
|
3342
3676
|
preflight() {
|
|
3343
3677
|
return this.options.preflight?.() ?? Promise.resolve();
|
|
3344
3678
|
}
|
|
3345
|
-
async start(profile, restore, onSpawn,
|
|
3679
|
+
async start(profile, restore, onSpawn, onStdoutRecord) {
|
|
3346
3680
|
const piArgv = restore ? profile.restorePiArgv : profile.userPiArgv;
|
|
3347
3681
|
if (piArgv === null) throw new Error("Agent has no observed Pi session to restore");
|
|
3348
3682
|
const process2 = await PiProcess.start({
|
|
@@ -3352,8 +3686,9 @@ var RealPiLauncher = class {
|
|
|
3352
3686
|
cwd: profile.cwd,
|
|
3353
3687
|
...this.options.env === void 0 ? {} : { env: this.options.env },
|
|
3354
3688
|
...this.options.maxStdoutFrameBytes === void 0 ? {} : { maxStdoutFrameBytes: this.options.maxStdoutFrameBytes },
|
|
3689
|
+
...this.options.maxPartialRecordBytes === void 0 ? {} : { maxPartialRecordBytes: this.options.maxPartialRecordBytes },
|
|
3355
3690
|
...onSpawn === void 0 ? {} : { onSpawn },
|
|
3356
|
-
...
|
|
3691
|
+
...onStdoutRecord === void 0 ? {} : { onStdoutRecord }
|
|
3357
3692
|
});
|
|
3358
3693
|
this.options.onStart?.(process2.pid);
|
|
3359
3694
|
return process2;
|
|
@@ -3361,7 +3696,7 @@ var RealPiLauncher = class {
|
|
|
3361
3696
|
};
|
|
3362
3697
|
|
|
3363
3698
|
// src/pi/external-target.ts
|
|
3364
|
-
async function createExternalPiTarget(env, maxStdoutFrameBytes) {
|
|
3699
|
+
async function createExternalPiTarget(env, maxStdoutFrameBytes, maxPartialRecordBytes) {
|
|
3365
3700
|
const selectedPath = env.PIFLEET_PI_EXECUTABLE;
|
|
3366
3701
|
const nodePath = env.PIFLEET_PI_NODE;
|
|
3367
3702
|
if (selectedPath === void 0 || nodePath === void 0) {
|
|
@@ -3386,6 +3721,7 @@ async function createExternalPiTarget(env, maxStdoutFrameBytes) {
|
|
|
3386
3721
|
artifactId: "external-pi",
|
|
3387
3722
|
env: externalPiExecutionEnvironment(env, initial.selectedPath, initial.nodePath),
|
|
3388
3723
|
...maxStdoutFrameBytes === void 0 ? {} : { maxStdoutFrameBytes },
|
|
3724
|
+
...maxPartialRecordBytes === void 0 ? {} : { maxPartialRecordBytes },
|
|
3389
3725
|
preflight: async () => {
|
|
3390
3726
|
let current;
|
|
3391
3727
|
try {
|
|
@@ -3403,16 +3739,6 @@ async function createExternalPiTarget(env, maxStdoutFrameBytes) {
|
|
|
3403
3739
|
});
|
|
3404
3740
|
return { launcher, identity };
|
|
3405
3741
|
}
|
|
3406
|
-
function installationIdentity(installation) {
|
|
3407
|
-
return {
|
|
3408
|
-
mode: "external",
|
|
3409
|
-
selectedPath: installation.selectedPath,
|
|
3410
|
-
nodePath: installation.nodePath,
|
|
3411
|
-
realPath: installation.realPath,
|
|
3412
|
-
version: installation.version,
|
|
3413
|
-
fingerprint: installation.fingerprint
|
|
3414
|
-
};
|
|
3415
|
-
}
|
|
3416
3742
|
function unavailableTarget(selectedPath, nodePath, code) {
|
|
3417
3743
|
const launcher = {
|
|
3418
3744
|
artifactId: "external-pi",
|
|
@@ -3470,11 +3796,102 @@ function resolveFleetPaths(env = process.env) {
|
|
|
3470
3796
|
};
|
|
3471
3797
|
}
|
|
3472
3798
|
|
|
3799
|
+
// src/platform/shared/state-security.ts
|
|
3800
|
+
import { chmodSync, constants as constants2, lstatSync } from "node:fs";
|
|
3801
|
+
import { chmod, lstat, mkdir, open } from "node:fs/promises";
|
|
3802
|
+
import { dirname as dirname2, join as join3, parse, resolve as resolve2, sep } from "node:path";
|
|
3803
|
+
var PRIVATE_DIRECTORY_MODE = 448;
|
|
3804
|
+
var PRIVATE_FILE_MODE = 384;
|
|
3805
|
+
var GROUP_OR_OTHER_WRITE = 18;
|
|
3806
|
+
var STICKY_BIT = 512;
|
|
3807
|
+
var SAFE_ROOT_STICKY_DIRECTORIES = /* @__PURE__ */ new Set(["/tmp", "/var/tmp"]);
|
|
3808
|
+
async function prepareFleetPathSecurity(paths) {
|
|
3809
|
+
await preparePrivateDirectory(paths.runtimeRoot);
|
|
3810
|
+
if (paths.stateRoot !== paths.runtimeRoot) await preparePrivateDirectory(paths.stateRoot);
|
|
3811
|
+
await preparePrivateDatabase(paths.databasePath);
|
|
3812
|
+
}
|
|
3813
|
+
async function preparePrivateDirectory(path) {
|
|
3814
|
+
const absolutePath = resolve2(path);
|
|
3815
|
+
await prepareDirectoryComponents(absolutePath);
|
|
3816
|
+
const stats = await lstat(absolutePath);
|
|
3817
|
+
assertCurrentUser(absolutePath, stats.uid);
|
|
3818
|
+
await chmod(absolutePath, PRIVATE_DIRECTORY_MODE);
|
|
3819
|
+
}
|
|
3820
|
+
async function preparePrivateDatabase(path) {
|
|
3821
|
+
const absolutePath = resolve2(path);
|
|
3822
|
+
await preparePrivateDirectory(dirname2(absolutePath));
|
|
3823
|
+
await assertPrivateRegularFileIfPresent(absolutePath);
|
|
3824
|
+
await assertPrivateRegularFileIfPresent(`${absolutePath}-wal`);
|
|
3825
|
+
await assertPrivateRegularFileIfPresent(`${absolutePath}-shm`);
|
|
3826
|
+
const handle = await open(
|
|
3827
|
+
absolutePath,
|
|
3828
|
+
constants2.O_CREAT | constants2.O_APPEND | constants2.O_WRONLY | constants2.O_NOFOLLOW,
|
|
3829
|
+
384
|
|
3830
|
+
);
|
|
3831
|
+
await handle.chmod(PRIVATE_FILE_MODE);
|
|
3832
|
+
await handle.close();
|
|
3833
|
+
}
|
|
3834
|
+
async function prepareDirectoryComponents(absolutePath) {
|
|
3835
|
+
const root = parse(absolutePath).root;
|
|
3836
|
+
const rootStats = await lstat(root);
|
|
3837
|
+
assertTrustedDirectoryAncestor(root, rootStats.uid, rootStats.mode);
|
|
3838
|
+
const components = absolutePath.slice(root.length).split(sep).filter(Boolean);
|
|
3839
|
+
let current = root;
|
|
3840
|
+
for (const component of components) {
|
|
3841
|
+
current = join3(current, component);
|
|
3842
|
+
let stats = await lstat(current).catch((error) => {
|
|
3843
|
+
if (error.code === "ENOENT") return null;
|
|
3844
|
+
throw error;
|
|
3845
|
+
});
|
|
3846
|
+
if (stats === null) {
|
|
3847
|
+
await mkdir(current, { mode: PRIVATE_DIRECTORY_MODE }).catch(
|
|
3848
|
+
(error) => {
|
|
3849
|
+
if (error.code !== "EEXIST") throw error;
|
|
3850
|
+
}
|
|
3851
|
+
);
|
|
3852
|
+
stats = await lstat(current);
|
|
3853
|
+
}
|
|
3854
|
+
if (stats.isSymbolicLink()) {
|
|
3855
|
+
throw new Error(`Refusing unsafe pi-fleet directory symlink ${current}`);
|
|
3856
|
+
}
|
|
3857
|
+
if (!stats.isDirectory()) {
|
|
3858
|
+
throw new Error(`Refusing unsafe pi-fleet directory ${current}`);
|
|
3859
|
+
}
|
|
3860
|
+
assertTrustedDirectoryAncestor(current, stats.uid, stats.mode);
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
function assertTrustedDirectoryAncestor(path, ownerUid, mode, currentUid = process.getuid?.()) {
|
|
3864
|
+
if (currentUid === void 0 || ownerUid === currentUid) return;
|
|
3865
|
+
if (ownerUid === 0) {
|
|
3866
|
+
const writable = (mode & GROUP_OR_OTHER_WRITE) !== 0;
|
|
3867
|
+
if (!writable) return;
|
|
3868
|
+
if ((mode & STICKY_BIT) !== 0 && SAFE_ROOT_STICKY_DIRECTORIES.has(path)) return;
|
|
3869
|
+
}
|
|
3870
|
+
throw new Error(`Refusing untrusted pi-fleet directory ancestor: ${path}`);
|
|
3871
|
+
}
|
|
3872
|
+
async function assertPrivateRegularFileIfPresent(path, harden = false) {
|
|
3873
|
+
const stats = await lstat(path).catch((error) => {
|
|
3874
|
+
if (error.code === "ENOENT") return null;
|
|
3875
|
+
throw error;
|
|
3876
|
+
});
|
|
3877
|
+
if (stats === null) return;
|
|
3878
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
3879
|
+
throw new Error(`Refusing unsafe pi-fleet state file ${path}`);
|
|
3880
|
+
}
|
|
3881
|
+
assertCurrentUser(path, stats.uid);
|
|
3882
|
+
if (harden || (stats.mode & 63) !== 0) await chmod(path, PRIVATE_FILE_MODE);
|
|
3883
|
+
}
|
|
3884
|
+
function assertCurrentUser(path, ownerUid) {
|
|
3885
|
+
const uid = process.getuid?.();
|
|
3886
|
+
if (uid !== void 0 && ownerUid !== uid) {
|
|
3887
|
+
throw new Error(`Refusing pi-fleet path not owned by the current user: ${path}`);
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3473
3891
|
// src/runtime/control-server.ts
|
|
3474
|
-
import {
|
|
3475
|
-
import {
|
|
3476
|
-
import {
|
|
3477
|
-
import { dirname as dirname2 } from "node:path";
|
|
3892
|
+
import { chmod as chmod2, mkdir as mkdir2, unlink } from "node:fs/promises";
|
|
3893
|
+
import { createServer } from "node:net";
|
|
3894
|
+
import { dirname as dirname3 } from "node:path";
|
|
3478
3895
|
|
|
3479
3896
|
// node_modules/@sinclair/typebox/build/esm/errors/function.mjs
|
|
3480
3897
|
function DefaultErrorFunction(error) {
|
|
@@ -6811,7 +7228,7 @@ __export(value_exports2, {
|
|
|
6811
7228
|
});
|
|
6812
7229
|
|
|
6813
7230
|
// src/protocol/version.ts
|
|
6814
|
-
var PROTOCOL_VERSION =
|
|
7231
|
+
var PROTOCOL_VERSION = 3;
|
|
6815
7232
|
var MAX_PROTOCOL_FRAME_BYTES = 1024 * 1024;
|
|
6816
7233
|
|
|
6817
7234
|
// src/protocol/envelope.ts
|
|
@@ -6829,7 +7246,6 @@ var RequestSchema = Type.Object(
|
|
|
6829
7246
|
Type.Literal("agent.receive"),
|
|
6830
7247
|
Type.Literal("agent.status"),
|
|
6831
7248
|
Type.Literal("agent.list"),
|
|
6832
|
-
Type.Literal("agent.watch"),
|
|
6833
7249
|
Type.Literal("agent.destroy"),
|
|
6834
7250
|
Type.Literal("agent.compact")
|
|
6835
7251
|
]),
|
|
@@ -6889,6 +7305,53 @@ function writeJsonLine(socket, value) {
|
|
|
6889
7305
|
`);
|
|
6890
7306
|
}
|
|
6891
7307
|
|
|
7308
|
+
// src/protocol/semantic-segmentation.ts
|
|
7309
|
+
function segmentSemanticEvent(event2, precedingCursor, maxFrameBytes, maxSegments = 4096) {
|
|
7310
|
+
assertPositiveInteger2(maxFrameBytes, "Semantic frame limit");
|
|
7311
|
+
assertPositiveInteger2(maxSegments, "Semantic segment limit");
|
|
7312
|
+
const payload = Buffer.from(JSON.stringify(event2));
|
|
7313
|
+
let low = 1;
|
|
7314
|
+
let high = payload.length;
|
|
7315
|
+
let accepted = null;
|
|
7316
|
+
while (low <= high) {
|
|
7317
|
+
const chunkBytes = Math.floor((low + high) / 2);
|
|
7318
|
+
const candidate = makeFrames(event2, precedingCursor, payload, chunkBytes);
|
|
7319
|
+
if (candidate.every((frame) => serializedFrameBytes(frame) <= maxFrameBytes)) {
|
|
7320
|
+
accepted = candidate;
|
|
7321
|
+
low = chunkBytes + 1;
|
|
7322
|
+
} else {
|
|
7323
|
+
high = chunkBytes - 1;
|
|
7324
|
+
}
|
|
7325
|
+
}
|
|
7326
|
+
if (accepted === null) throw new Error("Semantic frame limit is too small for metadata");
|
|
7327
|
+
if (accepted.length > maxSegments) {
|
|
7328
|
+
throw new Error("Semantic event requires too many segments");
|
|
7329
|
+
}
|
|
7330
|
+
return accepted;
|
|
7331
|
+
}
|
|
7332
|
+
function serializedFrameBytes(frame) {
|
|
7333
|
+
return Buffer.byteLength(JSON.stringify(frame));
|
|
7334
|
+
}
|
|
7335
|
+
function makeFrames(event2, precedingCursor, payload, chunkBytes) {
|
|
7336
|
+
const count = Math.ceil(payload.length / chunkBytes);
|
|
7337
|
+
const frames = [];
|
|
7338
|
+
for (let index = 0; index < count; index += 1) {
|
|
7339
|
+
frames.push({
|
|
7340
|
+
type: "semantic.segment",
|
|
7341
|
+
eventId: event2.id,
|
|
7342
|
+
precedingCursor,
|
|
7343
|
+
eventCursor: event2.cursor,
|
|
7344
|
+
index,
|
|
7345
|
+
count,
|
|
7346
|
+
data: payload.subarray(index * chunkBytes, (index + 1) * chunkBytes).toString("base64")
|
|
7347
|
+
});
|
|
7348
|
+
}
|
|
7349
|
+
return frames;
|
|
7350
|
+
}
|
|
7351
|
+
function assertPositiveInteger2(value, label) {
|
|
7352
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${label} must be positive`);
|
|
7353
|
+
}
|
|
7354
|
+
|
|
6892
7355
|
// src/shared/runtime-limits.ts
|
|
6893
7356
|
var KIBIBYTE = 1024;
|
|
6894
7357
|
var MEBIBYTE = 1024 * KIBIBYTE;
|
|
@@ -6897,16 +7360,44 @@ var DEFAULT_RUNTIME_LIMITS = Object.freeze({
|
|
|
6897
7360
|
maxMessageBytes: 512 * KIBIBYTE,
|
|
6898
7361
|
maxProtocolFrameBytes: MEBIBYTE,
|
|
6899
7362
|
maxPiFrameBytes: 8 * MEBIBYTE,
|
|
6900
|
-
|
|
6901
|
-
|
|
7363
|
+
maxJournalPendingRecords: 4096,
|
|
7364
|
+
maxJournalPendingBytes: 32 * MEBIBYTE,
|
|
7365
|
+
maxJournalPendingBytesPerAgent: 4 * MEBIBYTE,
|
|
7366
|
+
maxJournalPartialRecordBytes: 8 * MEBIBYTE,
|
|
7367
|
+
maxJournalBatchRecords: 128,
|
|
7368
|
+
maxJournalBatchBytes: 512 * KIBIBYTE,
|
|
7369
|
+
maxJournalBatchAgeMs: 25,
|
|
7370
|
+
journalCheckpointCommitInterval: 128,
|
|
7371
|
+
journalReclaimPagesPerPass: 32,
|
|
7372
|
+
maxOpenProjectorActivities: 1024,
|
|
7373
|
+
maxReceiveStreams: 128,
|
|
7374
|
+
maxReceiveReplayRows: 256,
|
|
7375
|
+
maxReceiveReplayBytes: 4 * MEBIBYTE,
|
|
7376
|
+
maxSemanticFrameBytes: MEBIBYTE,
|
|
7377
|
+
maxSemanticSegments: 4096,
|
|
7378
|
+
maxSocketWriteMs: 3e4
|
|
6902
7379
|
});
|
|
6903
7380
|
var ENV_KEYS = {
|
|
6904
7381
|
maxResidentProcesses: "PIFLEET_MAX_RESIDENT_PROCESSES",
|
|
6905
7382
|
maxMessageBytes: "PIFLEET_MAX_MESSAGE_BYTES",
|
|
6906
7383
|
maxProtocolFrameBytes: "PIFLEET_MAX_PROTOCOL_FRAME_BYTES",
|
|
6907
7384
|
maxPiFrameBytes: "PIFLEET_MAX_PI_FRAME_BYTES",
|
|
6908
|
-
|
|
6909
|
-
|
|
7385
|
+
maxJournalPendingRecords: "PIFLEET_MAX_JOURNAL_PENDING_RECORDS",
|
|
7386
|
+
maxJournalPendingBytes: "PIFLEET_MAX_JOURNAL_PENDING_BYTES",
|
|
7387
|
+
maxJournalPendingBytesPerAgent: "PIFLEET_MAX_JOURNAL_PENDING_BYTES_PER_AGENT",
|
|
7388
|
+
maxJournalPartialRecordBytes: "PIFLEET_MAX_JOURNAL_PARTIAL_RECORD_BYTES",
|
|
7389
|
+
maxJournalBatchRecords: "PIFLEET_MAX_JOURNAL_BATCH_RECORDS",
|
|
7390
|
+
maxJournalBatchBytes: "PIFLEET_MAX_JOURNAL_BATCH_BYTES",
|
|
7391
|
+
maxJournalBatchAgeMs: "PIFLEET_MAX_JOURNAL_BATCH_AGE_MS",
|
|
7392
|
+
journalCheckpointCommitInterval: "PIFLEET_JOURNAL_CHECKPOINT_COMMIT_INTERVAL",
|
|
7393
|
+
journalReclaimPagesPerPass: "PIFLEET_JOURNAL_RECLAIM_PAGES_PER_PASS",
|
|
7394
|
+
maxOpenProjectorActivities: "PIFLEET_MAX_OPEN_PROJECTOR_ACTIVITIES",
|
|
7395
|
+
maxReceiveStreams: "PIFLEET_MAX_RECEIVE_STREAMS",
|
|
7396
|
+
maxReceiveReplayRows: "PIFLEET_MAX_RECEIVE_REPLAY_ROWS",
|
|
7397
|
+
maxReceiveReplayBytes: "PIFLEET_MAX_RECEIVE_REPLAY_BYTES",
|
|
7398
|
+
maxSemanticFrameBytes: "PIFLEET_MAX_SEMANTIC_FRAME_BYTES",
|
|
7399
|
+
maxSemanticSegments: "PIFLEET_MAX_SEMANTIC_SEGMENTS",
|
|
7400
|
+
maxSocketWriteMs: "PIFLEET_MAX_SOCKET_WRITE_MS"
|
|
6910
7401
|
};
|
|
6911
7402
|
function runtimeLimitsFromEnv(env = process.env) {
|
|
6912
7403
|
return {
|
|
@@ -6914,8 +7405,22 @@ function runtimeLimitsFromEnv(env = process.env) {
|
|
|
6914
7405
|
maxMessageBytes: positiveInteger(env, "maxMessageBytes"),
|
|
6915
7406
|
maxProtocolFrameBytes: positiveInteger(env, "maxProtocolFrameBytes"),
|
|
6916
7407
|
maxPiFrameBytes: positiveInteger(env, "maxPiFrameBytes"),
|
|
6917
|
-
|
|
6918
|
-
|
|
7408
|
+
maxJournalPendingRecords: positiveInteger(env, "maxJournalPendingRecords"),
|
|
7409
|
+
maxJournalPendingBytes: positiveInteger(env, "maxJournalPendingBytes"),
|
|
7410
|
+
maxJournalPendingBytesPerAgent: positiveInteger(env, "maxJournalPendingBytesPerAgent"),
|
|
7411
|
+
maxJournalPartialRecordBytes: positiveInteger(env, "maxJournalPartialRecordBytes"),
|
|
7412
|
+
maxJournalBatchRecords: positiveInteger(env, "maxJournalBatchRecords"),
|
|
7413
|
+
maxJournalBatchBytes: positiveInteger(env, "maxJournalBatchBytes"),
|
|
7414
|
+
maxJournalBatchAgeMs: positiveInteger(env, "maxJournalBatchAgeMs"),
|
|
7415
|
+
journalCheckpointCommitInterval: positiveInteger(env, "journalCheckpointCommitInterval"),
|
|
7416
|
+
journalReclaimPagesPerPass: positiveInteger(env, "journalReclaimPagesPerPass"),
|
|
7417
|
+
maxOpenProjectorActivities: positiveInteger(env, "maxOpenProjectorActivities"),
|
|
7418
|
+
maxReceiveStreams: positiveInteger(env, "maxReceiveStreams"),
|
|
7419
|
+
maxReceiveReplayRows: positiveInteger(env, "maxReceiveReplayRows"),
|
|
7420
|
+
maxReceiveReplayBytes: positiveInteger(env, "maxReceiveReplayBytes"),
|
|
7421
|
+
maxSemanticFrameBytes: positiveInteger(env, "maxSemanticFrameBytes"),
|
|
7422
|
+
maxSemanticSegments: positiveInteger(env, "maxSemanticSegments"),
|
|
7423
|
+
maxSocketWriteMs: positiveInteger(env, "maxSocketWriteMs")
|
|
6919
7424
|
};
|
|
6920
7425
|
}
|
|
6921
7426
|
function positiveInteger(env, key) {
|
|
@@ -6929,160 +7434,93 @@ function positiveInteger(env, key) {
|
|
|
6929
7434
|
return value;
|
|
6930
7435
|
}
|
|
6931
7436
|
|
|
6932
|
-
// src/shared/
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
}
|
|
6939
|
-
|
|
6940
|
-
// src/runtime/rpc-watch-hub.ts
|
|
6941
|
-
var RpcWatchError = class extends Error {
|
|
6942
|
-
constructor(code, message = code) {
|
|
7437
|
+
// src/platform/shared/runtime-ownership.ts
|
|
7438
|
+
import { lstat as lstat2 } from "node:fs/promises";
|
|
7439
|
+
import { createConnection } from "node:net";
|
|
7440
|
+
var RuntimeOwnershipBlockedError = class extends Error {
|
|
7441
|
+
code = "runtime_upgrade_deferred";
|
|
7442
|
+
constructor(message) {
|
|
6943
7443
|
super(message);
|
|
6944
|
-
this.
|
|
6945
|
-
this.name = "RpcWatchError";
|
|
7444
|
+
this.name = "RuntimeOwnershipBlockedError";
|
|
6946
7445
|
}
|
|
6947
7446
|
};
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
const subscription = {
|
|
6959
|
-
agentName,
|
|
6960
|
-
signal,
|
|
6961
|
-
queue: [],
|
|
6962
|
-
maxQueuedBytes: this.limits.maxQueuedBytes,
|
|
6963
|
-
queuedBytes: 0,
|
|
6964
|
-
incarnationId: this.#activeIncarnations.get(agentName) ?? null,
|
|
6965
|
-
ended: false,
|
|
6966
|
-
terminalError: null,
|
|
6967
|
-
wake: null,
|
|
6968
|
-
onAbort: () => this.#abort(subscription)
|
|
6969
|
-
};
|
|
6970
|
-
signal.addEventListener("abort", subscription.onAbort, { once: true });
|
|
6971
|
-
if (signal.aborted) subscription.onAbort();
|
|
6972
|
-
else {
|
|
6973
|
-
const subscriptions = this.#subscriptions.get(agentName) ?? /* @__PURE__ */ new Set();
|
|
6974
|
-
subscriptions.add(subscription);
|
|
6975
|
-
this.#subscriptions.set(agentName, subscriptions);
|
|
6976
|
-
}
|
|
6977
|
-
return { [Symbol.asyncIterator]: () => this.#iterator(subscription) };
|
|
6978
|
-
}
|
|
6979
|
-
beginIncarnation(agentName, incarnationId) {
|
|
6980
|
-
this.#activeIncarnations.set(agentName, incarnationId);
|
|
6981
|
-
for (const subscription of this.#subscriptions.get(agentName) ?? []) {
|
|
6982
|
-
if (subscription.incarnationId === null && !subscription.ended) {
|
|
6983
|
-
subscription.incarnationId = incarnationId;
|
|
6984
|
-
}
|
|
6985
|
-
}
|
|
6986
|
-
return (bytes) => this.#publish(agentName, incarnationId, bytes);
|
|
6987
|
-
}
|
|
6988
|
-
endIncarnation(agentName, incarnationId, error = null) {
|
|
6989
|
-
if (this.#activeIncarnations.get(agentName) === incarnationId) {
|
|
6990
|
-
this.#activeIncarnations.delete(agentName);
|
|
6991
|
-
}
|
|
6992
|
-
for (const subscription of [...this.#subscriptions.get(agentName) ?? []]) {
|
|
6993
|
-
if (subscription.incarnationId === incarnationId) this.#endAfterDrain(subscription, error);
|
|
6994
|
-
}
|
|
6995
|
-
}
|
|
6996
|
-
closeAgent(agentName, error = null) {
|
|
6997
|
-
this.#activeIncarnations.delete(agentName);
|
|
6998
|
-
for (const subscription of [...this.#subscriptions.get(agentName) ?? []]) {
|
|
6999
|
-
this.#endAfterDrain(subscription, error);
|
|
7000
|
-
}
|
|
7447
|
+
async function inspectControlSocketOwnership(socketPath, probe = probeControlSocket) {
|
|
7448
|
+
const stats = await lstat2(socketPath).catch((error) => {
|
|
7449
|
+
if (error.code === "ENOENT") return null;
|
|
7450
|
+
throw error;
|
|
7451
|
+
});
|
|
7452
|
+
if (stats === null) return "absent";
|
|
7453
|
+
if (!stats.isSocket()) {
|
|
7454
|
+
throw new RuntimeOwnershipBlockedError(
|
|
7455
|
+
`Refusing to replace non-socket pi-fleet control path ${socketPath}`
|
|
7456
|
+
);
|
|
7001
7457
|
}
|
|
7002
|
-
|
|
7003
|
-
|
|
7458
|
+
return probe(socketPath);
|
|
7459
|
+
}
|
|
7460
|
+
async function preflightRuntimeStartup(options) {
|
|
7461
|
+
const ownership = await (options.inspectOwnership ?? inspectControlSocketOwnership)(
|
|
7462
|
+
options.socketPath
|
|
7463
|
+
);
|
|
7464
|
+
if (ownership === "responsive" || ownership === "uncertain") {
|
|
7465
|
+
throw new RuntimeOwnershipBlockedError(
|
|
7466
|
+
ownership === "responsive" ? `A responsive pi-fleet runtime already owns ${options.socketPath}` : `pi-fleet control socket ownership is uncertain for ${options.socketPath}`
|
|
7467
|
+
);
|
|
7004
7468
|
}
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
continue;
|
|
7011
|
-
}
|
|
7012
|
-
subscription.queue.push(Buffer.from(bytes));
|
|
7013
|
-
subscription.queuedBytes += bytes.byteLength;
|
|
7014
|
-
subscription.wake?.();
|
|
7015
|
-
subscription.wake = null;
|
|
7469
|
+
if (options.destructive) {
|
|
7470
|
+
if (options.assertOwnedProcessTreesAbsent === void 0) {
|
|
7471
|
+
throw new RuntimeOwnershipBlockedError(
|
|
7472
|
+
"Destructive pi-fleet startup requires proof that prior runtime and Pi process trees are absent."
|
|
7473
|
+
);
|
|
7016
7474
|
}
|
|
7475
|
+
await options.assertOwnedProcessTreesAbsent();
|
|
7017
7476
|
}
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
subscription.ended = true;
|
|
7039
|
-
subscription.terminalError = error;
|
|
7040
|
-
subscription.wake?.();
|
|
7041
|
-
subscription.wake = null;
|
|
7042
|
-
}
|
|
7043
|
-
#finish(subscription, error = null) {
|
|
7044
|
-
if (subscription.ended) return;
|
|
7045
|
-
subscription.ended = true;
|
|
7046
|
-
subscription.terminalError = error;
|
|
7047
|
-
subscription.wake?.();
|
|
7048
|
-
subscription.wake = null;
|
|
7049
|
-
}
|
|
7050
|
-
#abort(subscription) {
|
|
7051
|
-
subscription.queue.length = 0;
|
|
7052
|
-
subscription.queuedBytes = 0;
|
|
7053
|
-
subscription.ended = true;
|
|
7054
|
-
subscription.terminalError = null;
|
|
7055
|
-
this.#remove(subscription);
|
|
7056
|
-
subscription.wake?.();
|
|
7057
|
-
subscription.wake = null;
|
|
7058
|
-
}
|
|
7059
|
-
#remove(subscription) {
|
|
7060
|
-
subscription.signal.removeEventListener("abort", subscription.onAbort);
|
|
7061
|
-
const subscriptions = this.#subscriptions.get(subscription.agentName);
|
|
7062
|
-
if (subscriptions === void 0) return;
|
|
7063
|
-
subscriptions.delete(subscription);
|
|
7064
|
-
if (subscriptions.size === 0) this.#subscriptions.delete(subscription.agentName);
|
|
7065
|
-
}
|
|
7066
|
-
#count() {
|
|
7067
|
-
let count = 0;
|
|
7068
|
-
for (const subscriptions of this.#subscriptions.values()) count += subscriptions.size;
|
|
7069
|
-
return count;
|
|
7070
|
-
}
|
|
7071
|
-
};
|
|
7477
|
+
return ownership;
|
|
7478
|
+
}
|
|
7479
|
+
function probeControlSocket(socketPath) {
|
|
7480
|
+
return new Promise((resolveProbe) => {
|
|
7481
|
+
const socket = createConnection(socketPath);
|
|
7482
|
+
let settled = false;
|
|
7483
|
+
const settle = (result) => {
|
|
7484
|
+
if (settled) return;
|
|
7485
|
+
settled = true;
|
|
7486
|
+
clearTimeout(timer);
|
|
7487
|
+
socket.destroy();
|
|
7488
|
+
resolveProbe(result);
|
|
7489
|
+
};
|
|
7490
|
+
const timer = setTimeout(() => settle("uncertain"), 200);
|
|
7491
|
+
socket.once("connect", () => settle("responsive"));
|
|
7492
|
+
socket.once("error", (error) => {
|
|
7493
|
+
settle(error.code === "ECONNREFUSED" || error.code === "ENOENT" ? "stale" : "uncertain");
|
|
7494
|
+
});
|
|
7495
|
+
});
|
|
7496
|
+
}
|
|
7072
7497
|
|
|
7073
7498
|
// src/runtime/control-server.ts
|
|
7074
7499
|
async function startControlServer(options) {
|
|
7075
|
-
await
|
|
7500
|
+
await mkdir2(dirname3(options.socketPath), { recursive: true, mode: 448 });
|
|
7076
7501
|
await prepareSocketPath(options.socketPath);
|
|
7077
7502
|
const service = Promise.resolve(options.service);
|
|
7078
|
-
const
|
|
7079
|
-
|
|
7503
|
+
const limits = {
|
|
7504
|
+
maxProtocolFrameBytes: options.limits?.maxProtocolFrameBytes ?? DEFAULT_RUNTIME_LIMITS.maxProtocolFrameBytes,
|
|
7505
|
+
maxSemanticFrameBytes: options.limits?.maxSemanticFrameBytes ?? DEFAULT_RUNTIME_LIMITS.maxSemanticFrameBytes,
|
|
7506
|
+
maxSemanticSegments: options.limits?.maxSemanticSegments ?? DEFAULT_RUNTIME_LIMITS.maxSemanticSegments,
|
|
7507
|
+
maxSemanticEventBytes: options.limits?.maxPiFrameBytes ?? DEFAULT_RUNTIME_LIMITS.maxPiFrameBytes,
|
|
7508
|
+
maxSocketWriteMs: options.limits?.maxSocketWriteMs ?? DEFAULT_RUNTIME_LIMITS.maxSocketWriteMs
|
|
7509
|
+
};
|
|
7510
|
+
const server = createServer(
|
|
7511
|
+
(socket) => handleConnection(
|
|
7512
|
+
socket,
|
|
7513
|
+
service,
|
|
7514
|
+
options.journal ?? (() => Promise.reject(new Error("Semantic receive is unavailable"))),
|
|
7515
|
+
limits
|
|
7516
|
+
)
|
|
7517
|
+
);
|
|
7080
7518
|
server.listen(options.socketPath);
|
|
7081
7519
|
await new Promise((resolveListen, rejectListen) => {
|
|
7082
7520
|
server.once("listening", resolveListen);
|
|
7083
7521
|
server.once("error", rejectListen);
|
|
7084
7522
|
});
|
|
7085
|
-
await
|
|
7523
|
+
await chmod2(options.socketPath, 384);
|
|
7086
7524
|
return {
|
|
7087
7525
|
socketPath: options.socketPath,
|
|
7088
7526
|
async close() {
|
|
@@ -7093,7 +7531,7 @@ async function startControlServer(options) {
|
|
|
7093
7531
|
}
|
|
7094
7532
|
};
|
|
7095
7533
|
}
|
|
7096
|
-
function handleConnection(socket, service,
|
|
7534
|
+
function handleConnection(socket, service, journal, limits) {
|
|
7097
7535
|
let handled = false;
|
|
7098
7536
|
const abort = new AbortController();
|
|
7099
7537
|
socket.once("close", () => abort.abort());
|
|
@@ -7103,31 +7541,15 @@ function handleConnection(socket, service, maxFrameBytes) {
|
|
|
7103
7541
|
if (handled) return;
|
|
7104
7542
|
handled = true;
|
|
7105
7543
|
stopReading();
|
|
7106
|
-
void service.then(
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
writeJsonLine(socket, {
|
|
7110
|
-
v: PROTOCOL_VERSION,
|
|
7111
|
-
requestId: requestIdFrom(value),
|
|
7112
|
-
ok: false,
|
|
7113
|
-
error: invalidRequest ? { code: "invalid_request", message: error.message } : { code: "internal_error", message: "pi-fleet encountered an internal error." }
|
|
7114
|
-
});
|
|
7115
|
-
socket.end();
|
|
7116
|
-
});
|
|
7117
|
-
},
|
|
7118
|
-
(error) => {
|
|
7119
|
-
writeJsonLine(socket, {
|
|
7120
|
-
v: PROTOCOL_VERSION,
|
|
7121
|
-
requestId: "unknown",
|
|
7122
|
-
ok: false,
|
|
7123
|
-
error: { code: "invalid_request", message: error.message }
|
|
7124
|
-
});
|
|
7125
|
-
socket.end();
|
|
7544
|
+
void service.then(
|
|
7545
|
+
(readyService) => dispatch(value, readyService, journal, socket, abort.signal, limits)
|
|
7546
|
+
).catch((error) => writeConnectionError(socket, value, error));
|
|
7126
7547
|
},
|
|
7127
|
-
|
|
7548
|
+
(error) => writeConnectionError(socket, void 0, new InvalidRequestError(error.message)),
|
|
7549
|
+
limits.maxProtocolFrameBytes
|
|
7128
7550
|
);
|
|
7129
7551
|
}
|
|
7130
|
-
async function dispatch(value, service, socket, connectionSignal,
|
|
7552
|
+
async function dispatch(value, service, journal, socket, connectionSignal, limits) {
|
|
7131
7553
|
let request;
|
|
7132
7554
|
try {
|
|
7133
7555
|
request = parseProtocolRequest(value);
|
|
@@ -7154,86 +7576,178 @@ async function dispatch(value, service, socket, connectionSignal, maxFrameBytes)
|
|
|
7154
7576
|
);
|
|
7155
7577
|
break;
|
|
7156
7578
|
case "agent.receive":
|
|
7157
|
-
|
|
7579
|
+
await streamReceive(
|
|
7580
|
+
request.requestId,
|
|
7581
|
+
asReceiveInput(request.params),
|
|
7158
7582
|
service,
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
connectionSignal
|
|
7583
|
+
await journal(),
|
|
7584
|
+
socket,
|
|
7585
|
+
connectionSignal,
|
|
7586
|
+
limits
|
|
7162
7587
|
);
|
|
7163
|
-
|
|
7588
|
+
return;
|
|
7164
7589
|
case "agent.status":
|
|
7165
7590
|
result = await service.status(asNamedInput(request.params));
|
|
7166
7591
|
break;
|
|
7167
7592
|
case "agent.list":
|
|
7168
7593
|
result = await service.list();
|
|
7169
7594
|
break;
|
|
7170
|
-
case "agent.watch": {
|
|
7171
|
-
const watch = await service.openWatch(asNamedInput(request.params), connectionSignal);
|
|
7172
|
-
if (!watch.ok) {
|
|
7173
|
-
writeJsonLine(socket, {
|
|
7174
|
-
v: PROTOCOL_VERSION,
|
|
7175
|
-
requestId: request.requestId,
|
|
7176
|
-
stream: "error",
|
|
7177
|
-
error: watch.error
|
|
7178
|
-
});
|
|
7179
|
-
socket.end();
|
|
7180
|
-
return;
|
|
7181
|
-
}
|
|
7182
|
-
writeJsonLine(socket, { v: PROTOCOL_VERSION, requestId: request.requestId, stream: "ready" });
|
|
7183
|
-
try {
|
|
7184
|
-
const outgoingFrameLimit = Math.min(maxFrameBytes, MAX_PROTOCOL_FRAME_BYTES);
|
|
7185
|
-
const maxRawChunkBytes = Math.max(1, Math.floor((outgoingFrameLimit - 1024) * 0.75));
|
|
7186
|
-
for await (const chunk of watch.value) {
|
|
7187
|
-
for (let offset = 0; offset < chunk.length; offset += maxRawChunkBytes) {
|
|
7188
|
-
const part = chunk.subarray(offset, offset + maxRawChunkBytes);
|
|
7189
|
-
const writable = writeJsonLine(socket, {
|
|
7190
|
-
v: PROTOCOL_VERSION,
|
|
7191
|
-
requestId: request.requestId,
|
|
7192
|
-
stream: "chunk",
|
|
7193
|
-
data: part.toString("base64")
|
|
7194
|
-
});
|
|
7195
|
-
if (!writable) await once2(socket, "drain");
|
|
7196
|
-
}
|
|
7197
|
-
}
|
|
7198
|
-
if (!socket.destroyed) {
|
|
7199
|
-
writeJsonLine(socket, {
|
|
7200
|
-
v: PROTOCOL_VERSION,
|
|
7201
|
-
requestId: request.requestId,
|
|
7202
|
-
stream: "end"
|
|
7203
|
-
});
|
|
7204
|
-
socket.end();
|
|
7205
|
-
}
|
|
7206
|
-
} catch (error) {
|
|
7207
|
-
if (!socket.destroyed) {
|
|
7208
|
-
const watchError = error instanceof RpcWatchError ? { code: error.code, message: error.message } : { code: "internal_error", message: "Raw Pi RPC watch failed." };
|
|
7209
|
-
writeJsonLine(socket, {
|
|
7210
|
-
v: PROTOCOL_VERSION,
|
|
7211
|
-
requestId: request.requestId,
|
|
7212
|
-
stream: "error",
|
|
7213
|
-
error: watchError
|
|
7214
|
-
});
|
|
7215
|
-
socket.end();
|
|
7216
|
-
}
|
|
7217
|
-
}
|
|
7218
|
-
return;
|
|
7219
|
-
}
|
|
7220
7595
|
case "agent.destroy":
|
|
7221
|
-
result = await service.destroy(
|
|
7596
|
+
result = await service.destroy(asBoundInput(request.params), requireOperation(operationId));
|
|
7222
7597
|
break;
|
|
7223
7598
|
case "agent.compact":
|
|
7224
7599
|
result = await service.compact(
|
|
7225
|
-
|
|
7600
|
+
asBoundInput(request.params),
|
|
7226
7601
|
requireOperation(operationId),
|
|
7227
7602
|
requirePiIdentity(request.runtime?.pi)
|
|
7228
7603
|
);
|
|
7229
7604
|
break;
|
|
7230
7605
|
}
|
|
7231
|
-
|
|
7606
|
+
await writeFrame(
|
|
7232
7607
|
socket,
|
|
7233
|
-
result.ok ? { v: PROTOCOL_VERSION, requestId: request.requestId, ok: true, result: result.value } : { v: PROTOCOL_VERSION, requestId: request.requestId, ok: false, error: result.error }
|
|
7608
|
+
result.ok ? { v: PROTOCOL_VERSION, requestId: request.requestId, ok: true, result: result.value } : { v: PROTOCOL_VERSION, requestId: request.requestId, ok: false, error: result.error },
|
|
7609
|
+
limits.maxSocketWriteMs
|
|
7234
7610
|
);
|
|
7235
7611
|
socket.end();
|
|
7236
7612
|
}
|
|
7613
|
+
async function streamReceive(requestId, input, service, journal, socket, connectionSignal, limits) {
|
|
7614
|
+
const abort = new AbortController();
|
|
7615
|
+
const onConnectionAbort = () => abort.abort(connectionSignal.reason);
|
|
7616
|
+
connectionSignal.addEventListener("abort", onConnectionAbort, { once: true });
|
|
7617
|
+
let lastCursor = null;
|
|
7618
|
+
try {
|
|
7619
|
+
const prepared = await service.prepareReceive(
|
|
7620
|
+
input,
|
|
7621
|
+
input.start ?? { kind: "live" },
|
|
7622
|
+
input.untilIdle === true,
|
|
7623
|
+
abort.signal
|
|
7624
|
+
);
|
|
7625
|
+
if (!prepared.ok) {
|
|
7626
|
+
await writeStreamError(socket, requestId, prepared.error, limits.maxSocketWriteMs);
|
|
7627
|
+
return;
|
|
7628
|
+
}
|
|
7629
|
+
const { stream, idle } = prepared.value;
|
|
7630
|
+
await writeFrame(
|
|
7631
|
+
socket,
|
|
7632
|
+
{
|
|
7633
|
+
v: PROTOCOL_VERSION,
|
|
7634
|
+
requestId,
|
|
7635
|
+
stream: "ready",
|
|
7636
|
+
cursor: stream.cursor,
|
|
7637
|
+
limits: {
|
|
7638
|
+
maxEventBytes: limits.maxSemanticEventBytes,
|
|
7639
|
+
maxSegments: limits.maxSemanticSegments
|
|
7640
|
+
}
|
|
7641
|
+
},
|
|
7642
|
+
limits.maxSocketWriteMs,
|
|
7643
|
+
limits.maxProtocolFrameBytes
|
|
7644
|
+
);
|
|
7645
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
7646
|
+
let precedingCursor = stream.cursor;
|
|
7647
|
+
lastCursor = stream.cursor;
|
|
7648
|
+
let lastPosition = journal.decodeCursorPosition(stream.cursor);
|
|
7649
|
+
let idleHighWater = null;
|
|
7650
|
+
let pending = iterator.next();
|
|
7651
|
+
while (!abort.signal.aborted) {
|
|
7652
|
+
if (idle !== null && idleHighWater === null) {
|
|
7653
|
+
const raced = await Promise.race([
|
|
7654
|
+
pending.then((next2) => ({ kind: "event", next: next2 })),
|
|
7655
|
+
idle.then((settled) => ({ kind: "idle", settled }))
|
|
7656
|
+
]);
|
|
7657
|
+
if (raced.kind === "idle") {
|
|
7658
|
+
if (!raced.settled.ok) {
|
|
7659
|
+
await writeStreamError(socket, requestId, raced.settled.error, limits.maxSocketWriteMs);
|
|
7660
|
+
return;
|
|
7661
|
+
}
|
|
7662
|
+
idleHighWater = raced.settled.value.idleEventPosition;
|
|
7663
|
+
if (lastPosition >= idleHighWater) break;
|
|
7664
|
+
continue;
|
|
7665
|
+
}
|
|
7666
|
+
if (raced.next.done) break;
|
|
7667
|
+
const event3 = raced.next.value;
|
|
7668
|
+
await writeSemanticEvent(socket, requestId, event3, precedingCursor, limits);
|
|
7669
|
+
precedingCursor = event3.cursor;
|
|
7670
|
+
lastCursor = event3.cursor;
|
|
7671
|
+
lastPosition = journal.decodeCursorPosition(event3.cursor);
|
|
7672
|
+
pending = iterator.next();
|
|
7673
|
+
continue;
|
|
7674
|
+
}
|
|
7675
|
+
if (idleHighWater !== null && lastPosition >= idleHighWater) break;
|
|
7676
|
+
const next = await pending;
|
|
7677
|
+
if (next.done) break;
|
|
7678
|
+
const event2 = next.value;
|
|
7679
|
+
await writeSemanticEvent(socket, requestId, event2, precedingCursor, limits);
|
|
7680
|
+
precedingCursor = event2.cursor;
|
|
7681
|
+
lastCursor = event2.cursor;
|
|
7682
|
+
lastPosition = journal.decodeCursorPosition(event2.cursor);
|
|
7683
|
+
pending = iterator.next();
|
|
7684
|
+
}
|
|
7685
|
+
abort.abort();
|
|
7686
|
+
await pending.catch(() => ({ done: true, value: void 0 }));
|
|
7687
|
+
await iterator.return?.();
|
|
7688
|
+
if (!socket.destroyed) {
|
|
7689
|
+
await writeFrame(
|
|
7690
|
+
socket,
|
|
7691
|
+
{ v: PROTOCOL_VERSION, requestId, stream: "end" },
|
|
7692
|
+
limits.maxSocketWriteMs
|
|
7693
|
+
);
|
|
7694
|
+
socket.end();
|
|
7695
|
+
}
|
|
7696
|
+
} catch (error) {
|
|
7697
|
+
if (socket.destroyed || connectionSignal.aborted) return;
|
|
7698
|
+
const failure = error instanceof ReceiveObservationUncertainError ? {
|
|
7699
|
+
code: error.code,
|
|
7700
|
+
message: error.message,
|
|
7701
|
+
details: {
|
|
7702
|
+
lastSafeCursor: error.lastSafeCursor,
|
|
7703
|
+
continuationCursor: error.continuationCursor
|
|
7704
|
+
}
|
|
7705
|
+
} : error instanceof Error && "code" in error && typeof error.code === "string" ? {
|
|
7706
|
+
code: error.code,
|
|
7707
|
+
message: "Receive stream failed.",
|
|
7708
|
+
...lastCursor === null ? {} : { details: { lastSafeCursor: lastCursor } }
|
|
7709
|
+
} : error instanceof Error && error.message.startsWith("Semantic") ? {
|
|
7710
|
+
code: "semantic_event_too_large",
|
|
7711
|
+
message: "A semantic event exceeds stream limits."
|
|
7712
|
+
} : { code: "internal_error", message: "Receive stream failed." };
|
|
7713
|
+
await writeStreamError(socket, requestId, failure, limits.maxSocketWriteMs);
|
|
7714
|
+
} finally {
|
|
7715
|
+
connectionSignal.removeEventListener("abort", onConnectionAbort);
|
|
7716
|
+
}
|
|
7717
|
+
}
|
|
7718
|
+
async function writeSemanticEvent(socket, requestId, event2, precedingCursor, limits) {
|
|
7719
|
+
let segmentLimit = Math.max(
|
|
7720
|
+
1,
|
|
7721
|
+
Math.min(limits.maxSemanticFrameBytes, limits.maxProtocolFrameBytes)
|
|
7722
|
+
);
|
|
7723
|
+
let frames = null;
|
|
7724
|
+
while (frames === null) {
|
|
7725
|
+
const candidates = segmentSemanticEvent(
|
|
7726
|
+
event2,
|
|
7727
|
+
precedingCursor,
|
|
7728
|
+
segmentLimit,
|
|
7729
|
+
limits.maxSemanticSegments
|
|
7730
|
+
).map((segment) => ({
|
|
7731
|
+
v: PROTOCOL_VERSION,
|
|
7732
|
+
requestId,
|
|
7733
|
+
stream: "semantic.segment",
|
|
7734
|
+
segment
|
|
7735
|
+
}));
|
|
7736
|
+
if (candidates.every(
|
|
7737
|
+
(frame) => Buffer.byteLength(JSON.stringify(frame)) + 1 <= limits.maxProtocolFrameBytes
|
|
7738
|
+
)) {
|
|
7739
|
+
frames = candidates;
|
|
7740
|
+
break;
|
|
7741
|
+
}
|
|
7742
|
+
if (segmentLimit === 1) {
|
|
7743
|
+
throw new Error("Semantic event envelope exceeds the configured protocol limit");
|
|
7744
|
+
}
|
|
7745
|
+
segmentLimit = Math.max(1, Math.floor(segmentLimit * 0.75));
|
|
7746
|
+
}
|
|
7747
|
+
for (const frame of frames) {
|
|
7748
|
+
await writeFrame(socket, frame, limits.maxSocketWriteMs, limits.maxProtocolFrameBytes);
|
|
7749
|
+
}
|
|
7750
|
+
}
|
|
7237
7751
|
function asCreateInput(params) {
|
|
7238
7752
|
const name = stringParam(params, "name");
|
|
7239
7753
|
const cwd = stringParam(params, "cwd");
|
|
@@ -7248,39 +7762,63 @@ function asCreateInput(params) {
|
|
|
7248
7762
|
return { name, cwd, piArgv, ...instructions === void 0 ? {} : { instructions } };
|
|
7249
7763
|
}
|
|
7250
7764
|
function asSendInput(params) {
|
|
7251
|
-
|
|
7765
|
+
const delivery = params.delivery;
|
|
7766
|
+
if (delivery !== void 0 && delivery !== "steer" && delivery !== "followUp") {
|
|
7767
|
+
throw new InvalidRequestError("delivery must be steer or followUp");
|
|
7768
|
+
}
|
|
7769
|
+
return {
|
|
7770
|
+
...asBoundInput(params),
|
|
7771
|
+
message: stringParam(params, "message"),
|
|
7772
|
+
...delivery === void 0 ? {} : { delivery }
|
|
7773
|
+
};
|
|
7252
7774
|
}
|
|
7253
|
-
function
|
|
7254
|
-
|
|
7775
|
+
function asReceiveInput(params) {
|
|
7776
|
+
const named = asBoundInput(params);
|
|
7777
|
+
if (params.untilIdle !== void 0 && typeof params.untilIdle !== "boolean") {
|
|
7778
|
+
throw new InvalidRequestError("untilIdle must be a boolean");
|
|
7779
|
+
}
|
|
7780
|
+
const start = receiveStart(params);
|
|
7781
|
+
if (params.untilIdle === true && start.kind !== "live") {
|
|
7782
|
+
throw new InvalidRequestError("untilIdle uses a live boundary and cannot include history");
|
|
7783
|
+
}
|
|
7784
|
+
return {
|
|
7785
|
+
...named,
|
|
7786
|
+
start,
|
|
7787
|
+
...params.untilIdle === true ? { untilIdle: true } : {}
|
|
7788
|
+
};
|
|
7255
7789
|
}
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
return err({ code: "timeout", message: "Agent did not become idle before timeout." });
|
|
7270
|
-
}
|
|
7271
|
-
throw error;
|
|
7272
|
-
} finally {
|
|
7273
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
7274
|
-
connectionSignal.removeEventListener("abort", onConnectionAbort);
|
|
7790
|
+
function receiveStart(params) {
|
|
7791
|
+
if (params.fromStart !== void 0 && typeof params.fromStart !== "boolean") {
|
|
7792
|
+
throw new InvalidRequestError("fromStart must be a boolean");
|
|
7793
|
+
}
|
|
7794
|
+
if (params.after !== void 0 && typeof params.after !== "string") {
|
|
7795
|
+
throw new InvalidRequestError("after must be a string cursor");
|
|
7796
|
+
}
|
|
7797
|
+
if (params.fromStart === true && params.after !== void 0) {
|
|
7798
|
+
throw new InvalidRequestError("after and fromStart cannot be combined");
|
|
7799
|
+
}
|
|
7800
|
+
if (params.fromStart === true) return { kind: "start" };
|
|
7801
|
+
if (typeof params.after === "string") {
|
|
7802
|
+
return { kind: "after", cursor: params.after };
|
|
7275
7803
|
}
|
|
7804
|
+
return { kind: "live" };
|
|
7276
7805
|
}
|
|
7277
|
-
function
|
|
7278
|
-
const
|
|
7279
|
-
if (
|
|
7280
|
-
|
|
7281
|
-
throw new InvalidRequestError(`${key} must be a non-negative number`);
|
|
7806
|
+
function asNamedInput(params) {
|
|
7807
|
+
const expectedAgentId = params.expectedAgentId;
|
|
7808
|
+
if (expectedAgentId !== void 0 && typeof expectedAgentId !== "string") {
|
|
7809
|
+
throw new InvalidRequestError("expectedAgentId must be a string");
|
|
7282
7810
|
}
|
|
7283
|
-
return
|
|
7811
|
+
return {
|
|
7812
|
+
name: stringParam(params, "name"),
|
|
7813
|
+
...expectedAgentId === void 0 ? {} : { expectedAgentId }
|
|
7814
|
+
};
|
|
7815
|
+
}
|
|
7816
|
+
function asBoundInput(params) {
|
|
7817
|
+
const input = asNamedInput(params);
|
|
7818
|
+
if (input.expectedAgentId === void 0) {
|
|
7819
|
+
throw new InvalidRequestError("expectedAgentId is required for agent-scoped operations");
|
|
7820
|
+
}
|
|
7821
|
+
return { ...input, expectedAgentId: input.expectedAgentId };
|
|
7284
7822
|
}
|
|
7285
7823
|
function stringParam(params, key) {
|
|
7286
7824
|
const value = params[key];
|
|
@@ -7297,6 +7835,56 @@ function requirePiIdentity(identity) {
|
|
|
7297
7835
|
throw new InvalidRequestError("Mutation requires Pi runtime identity");
|
|
7298
7836
|
return identity;
|
|
7299
7837
|
}
|
|
7838
|
+
async function writeStreamError(socket, requestId, error, timeoutMs) {
|
|
7839
|
+
if (socket.destroyed) return;
|
|
7840
|
+
await writeFrame(socket, { v: PROTOCOL_VERSION, requestId, stream: "error", error }, timeoutMs);
|
|
7841
|
+
socket.end();
|
|
7842
|
+
}
|
|
7843
|
+
async function writeFrame(socket, value, timeoutMs, maxBytes) {
|
|
7844
|
+
if (socket.destroyed) throw new Error("Control socket is closed");
|
|
7845
|
+
if (maxBytes !== void 0 && Buffer.byteLength(JSON.stringify(value)) + 1 > maxBytes) {
|
|
7846
|
+
throw new Error("Control protocol frame exceeds the configured limit");
|
|
7847
|
+
}
|
|
7848
|
+
if (writeJsonLine(socket, value)) return;
|
|
7849
|
+
await new Promise((resolve3, reject) => {
|
|
7850
|
+
const cleanup = () => {
|
|
7851
|
+
clearTimeout(timer);
|
|
7852
|
+
socket.off("drain", onDrain);
|
|
7853
|
+
socket.off("error", onError);
|
|
7854
|
+
socket.off("close", onClose);
|
|
7855
|
+
};
|
|
7856
|
+
const onDrain = () => {
|
|
7857
|
+
cleanup();
|
|
7858
|
+
resolve3();
|
|
7859
|
+
};
|
|
7860
|
+
const onError = (error) => {
|
|
7861
|
+
cleanup();
|
|
7862
|
+
reject(error);
|
|
7863
|
+
};
|
|
7864
|
+
const onClose = () => {
|
|
7865
|
+
cleanup();
|
|
7866
|
+
reject(new Error("Control socket closed during write"));
|
|
7867
|
+
};
|
|
7868
|
+
const timer = setTimeout(() => {
|
|
7869
|
+
cleanup();
|
|
7870
|
+
reject(new Error("Control socket write timed out"));
|
|
7871
|
+
}, timeoutMs);
|
|
7872
|
+
socket.once("drain", onDrain);
|
|
7873
|
+
socket.once("error", onError);
|
|
7874
|
+
socket.once("close", onClose);
|
|
7875
|
+
});
|
|
7876
|
+
}
|
|
7877
|
+
function writeConnectionError(socket, value, error) {
|
|
7878
|
+
if (socket.destroyed) return;
|
|
7879
|
+
const invalid = error instanceof InvalidRequestError;
|
|
7880
|
+
writeJsonLine(socket, {
|
|
7881
|
+
v: PROTOCOL_VERSION,
|
|
7882
|
+
requestId: requestIdFrom(value),
|
|
7883
|
+
ok: false,
|
|
7884
|
+
error: invalid ? { code: "invalid_request", message: error.message } : { code: "internal_error", message: "pi-fleet encountered an internal error." }
|
|
7885
|
+
});
|
|
7886
|
+
socket.end();
|
|
7887
|
+
}
|
|
7300
7888
|
var InvalidRequestError = class extends Error {
|
|
7301
7889
|
name = "InvalidRequestError";
|
|
7302
7890
|
};
|
|
@@ -7305,32 +7893,15 @@ function requestIdFrom(value) {
|
|
|
7305
7893
|
return typeof value.requestId === "string" ? value.requestId : "unknown";
|
|
7306
7894
|
}
|
|
7307
7895
|
async function prepareSocketPath(socketPath) {
|
|
7308
|
-
const
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
}
|
|
7318
|
-
async function canConnect(socketPath) {
|
|
7319
|
-
return new Promise((resolveConnect) => {
|
|
7320
|
-
const socket = createConnection(socketPath);
|
|
7321
|
-
const timer = setTimeout(() => {
|
|
7322
|
-
socket.destroy();
|
|
7323
|
-
resolveConnect(false);
|
|
7324
|
-
}, 200);
|
|
7325
|
-
socket.once("connect", () => {
|
|
7326
|
-
clearTimeout(timer);
|
|
7327
|
-
socket.destroy();
|
|
7328
|
-
resolveConnect(true);
|
|
7329
|
-
});
|
|
7330
|
-
socket.once("error", () => {
|
|
7331
|
-
clearTimeout(timer);
|
|
7332
|
-
resolveConnect(false);
|
|
7333
|
-
});
|
|
7896
|
+
const ownership = await inspectControlSocketOwnership(socketPath);
|
|
7897
|
+
if (ownership === "absent") return;
|
|
7898
|
+
if (ownership !== "stale") {
|
|
7899
|
+
throw new RuntimeOwnershipBlockedError(
|
|
7900
|
+
ownership === "responsive" ? `A responsive pi-fleet runtime already owns ${socketPath}` : `pi-fleet control socket ownership is uncertain for ${socketPath}`
|
|
7901
|
+
);
|
|
7902
|
+
}
|
|
7903
|
+
await unlink(socketPath).catch((error) => {
|
|
7904
|
+
if (error.code !== "ENOENT") throw error;
|
|
7334
7905
|
});
|
|
7335
7906
|
}
|
|
7336
7907
|
async function closeServer(server) {
|
|
@@ -7341,7 +7912,56 @@ async function closeServer(server) {
|
|
|
7341
7912
|
}
|
|
7342
7913
|
|
|
7343
7914
|
// src/runtime/fleet-service.ts
|
|
7344
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7915
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
7916
|
+
|
|
7917
|
+
// src/client/contracts.ts
|
|
7918
|
+
var PI_FLEET_ERROR_CODES = [
|
|
7919
|
+
"agent_busy",
|
|
7920
|
+
"agent_destroyed",
|
|
7921
|
+
"agent_destroying",
|
|
7922
|
+
"agent_not_found",
|
|
7923
|
+
"capacity_exceeded",
|
|
7924
|
+
"cancelled",
|
|
7925
|
+
"compaction_failed",
|
|
7926
|
+
"compaction_uncertain",
|
|
7927
|
+
"cursor_expired",
|
|
7928
|
+
"cursor_invalid",
|
|
7929
|
+
"cursor_wrong_agent",
|
|
7930
|
+
"delivery_uncertain",
|
|
7931
|
+
"destroy_incomplete",
|
|
7932
|
+
"incarnation_cleanup_uncertain",
|
|
7933
|
+
"internal_error",
|
|
7934
|
+
"invalid_arguments",
|
|
7935
|
+
"invalid_request",
|
|
7936
|
+
"name_taken",
|
|
7937
|
+
"nothing_to_compact",
|
|
7938
|
+
"observation_uncertain",
|
|
7939
|
+
"operation_conflict",
|
|
7940
|
+
"operation_in_progress",
|
|
7941
|
+
"pi_installation_changed",
|
|
7942
|
+
"pi_not_executable",
|
|
7943
|
+
"pi_not_found",
|
|
7944
|
+
"pi_runtime_mismatch",
|
|
7945
|
+
"pi_service_mismatch",
|
|
7946
|
+
"pi_start_failed",
|
|
7947
|
+
"pi_version_unavailable",
|
|
7948
|
+
"pi_version_unsupported",
|
|
7949
|
+
"protocol_error",
|
|
7950
|
+
"protocol_incompatible",
|
|
7951
|
+
"receive_resource_exhausted",
|
|
7952
|
+
"runtime_interrupted",
|
|
7953
|
+
"runtime_unavailable",
|
|
7954
|
+
"runtime_upgrade_deferred",
|
|
7955
|
+
"semantic_event_too_large",
|
|
7956
|
+
"stale_agent",
|
|
7957
|
+
"state_corrupt",
|
|
7958
|
+
"storage_unavailable",
|
|
7959
|
+
"timeout"
|
|
7960
|
+
];
|
|
7961
|
+
var piFleetErrorCodeSet = new Set(PI_FLEET_ERROR_CODES);
|
|
7962
|
+
function isPiFleetErrorCode(value) {
|
|
7963
|
+
return typeof value === "string" && piFleetErrorCodeSet.has(value);
|
|
7964
|
+
}
|
|
7345
7965
|
|
|
7346
7966
|
// src/pi/session-selector.ts
|
|
7347
7967
|
var VALUE_SELECTORS = /* @__PURE__ */ new Map([
|
|
@@ -7514,15 +8134,24 @@ function observeSession(profile, session) {
|
|
|
7514
8134
|
};
|
|
7515
8135
|
}
|
|
7516
8136
|
|
|
8137
|
+
// src/shared/result.ts
|
|
8138
|
+
function ok(value) {
|
|
8139
|
+
return { ok: true, value };
|
|
8140
|
+
}
|
|
8141
|
+
function err(error) {
|
|
8142
|
+
return { ok: false, error };
|
|
8143
|
+
}
|
|
8144
|
+
|
|
7517
8145
|
// src/runtime/agent-coordinator.ts
|
|
7518
8146
|
var AgentCoordinator = class {
|
|
7519
|
-
constructor(store, agent, process2, incarnationId, now, onProcessExit) {
|
|
8147
|
+
constructor(store, agent, process2, incarnationId, now, onProcessExit, onIdle) {
|
|
7520
8148
|
this.store = store;
|
|
7521
8149
|
this.agent = agent;
|
|
7522
8150
|
this.process = process2;
|
|
7523
8151
|
this.incarnationId = incarnationId;
|
|
7524
8152
|
this.now = now;
|
|
7525
8153
|
this.onProcessExit = onProcessExit;
|
|
8154
|
+
this.onIdle = onIdle;
|
|
7526
8155
|
this.#unsubscribeFrame = process2.onFrame((frame) => {
|
|
7527
8156
|
if (frame.type === "agent_start") this.#queueEvent(() => this.#markWorking());
|
|
7528
8157
|
if (frame.type === "agent_settled") this.#queueEvent(() => this.#markIdle());
|
|
@@ -7536,22 +8165,50 @@ var AgentCoordinator = class {
|
|
|
7536
8165
|
#stopReason = null;
|
|
7537
8166
|
#handlingFailure = false;
|
|
7538
8167
|
#mayBeWorking = false;
|
|
8168
|
+
#idleEventPosition = null;
|
|
7539
8169
|
#unsubscribeFrame;
|
|
7540
8170
|
#unsubscribeExit;
|
|
7541
8171
|
get storedAgent() {
|
|
7542
8172
|
return this.agent;
|
|
7543
8173
|
}
|
|
7544
|
-
|
|
8174
|
+
get idleEventPosition() {
|
|
8175
|
+
return this.#idleEventPosition;
|
|
8176
|
+
}
|
|
8177
|
+
/**
|
|
8178
|
+
* Delivers caller input to Pi.
|
|
8179
|
+
*
|
|
8180
|
+
* Follow-up delivery means "wait until the current run finishes", which Pi only
|
|
8181
|
+
* honours while a turn is active: a follow-up queued against an authoritatively
|
|
8182
|
+
* idle session would wait for a turn that may never start. Idle follow-up input
|
|
8183
|
+
* is therefore delivered as an ordinary prompt, matching what typing into an idle
|
|
8184
|
+
* Pi session does, while active follow-up keeps its queued semantics.
|
|
8185
|
+
*/
|
|
8186
|
+
send(message, delivery = "steer") {
|
|
7545
8187
|
return this.#enqueue(async () => {
|
|
8188
|
+
const queueFollowUp = delivery === "followUp" && await this.#isActive();
|
|
7546
8189
|
this.#mayBeWorking = true;
|
|
7547
8190
|
try {
|
|
7548
|
-
await this.process.
|
|
8191
|
+
if (queueFollowUp) await this.process.followUp(message);
|
|
8192
|
+
else await this.process.prompt(message);
|
|
7549
8193
|
} catch (error) {
|
|
7550
8194
|
this.#mayBeWorking = false;
|
|
7551
8195
|
throw error;
|
|
7552
8196
|
}
|
|
7553
8197
|
});
|
|
7554
8198
|
}
|
|
8199
|
+
async #isActive() {
|
|
8200
|
+
if (this.#mayBeWorking) return true;
|
|
8201
|
+
const state = await this.process.getState();
|
|
8202
|
+
return state.isStreaming || state.isCompacting || state.pendingMessageCount !== 0;
|
|
8203
|
+
}
|
|
8204
|
+
reconcileState() {
|
|
8205
|
+
return this.#enqueue(async () => {
|
|
8206
|
+
const state = await this.process.getState();
|
|
8207
|
+
this.#applyObservedState(state);
|
|
8208
|
+
await this.store.putAgent(this.agent);
|
|
8209
|
+
return state;
|
|
8210
|
+
});
|
|
8211
|
+
}
|
|
7555
8212
|
compact() {
|
|
7556
8213
|
return this.#enqueue(async () => {
|
|
7557
8214
|
const state = await this.process.getState();
|
|
@@ -7580,15 +8237,21 @@ var AgentCoordinator = class {
|
|
|
7580
8237
|
}
|
|
7581
8238
|
});
|
|
7582
8239
|
}
|
|
7583
|
-
async
|
|
8240
|
+
async registerIdleWaiter(signal) {
|
|
7584
8241
|
let wait = null;
|
|
7585
8242
|
await this.#enqueue(async () => {
|
|
7586
|
-
if (!this.#mayBeWorking && this.agent.summary.state === "idle") return;
|
|
7587
8243
|
if (signal?.aborted === true) throw new Error("Receive cancelled");
|
|
8244
|
+
if (!this.#mayBeWorking && this.agent.summary.state === "idle" && this.#idleEventPosition !== null) {
|
|
8245
|
+
wait = Promise.resolve({
|
|
8246
|
+
agent: this.agent,
|
|
8247
|
+
idleEventPosition: this.#idleEventPosition
|
|
8248
|
+
});
|
|
8249
|
+
return;
|
|
8250
|
+
}
|
|
7588
8251
|
let resolveIdle;
|
|
7589
8252
|
let rejectIdle;
|
|
7590
|
-
const pending = new Promise((
|
|
7591
|
-
resolveIdle =
|
|
8253
|
+
const pending = new Promise((resolve3, reject) => {
|
|
8254
|
+
resolveIdle = resolve3;
|
|
7592
8255
|
rejectIdle = reject;
|
|
7593
8256
|
});
|
|
7594
8257
|
void pending.catch(() => void 0);
|
|
@@ -7614,14 +8277,18 @@ var AgentCoordinator = class {
|
|
|
7614
8277
|
await this.#markIdle();
|
|
7615
8278
|
}
|
|
7616
8279
|
});
|
|
7617
|
-
if (wait
|
|
7618
|
-
|
|
7619
|
-
|
|
8280
|
+
if (wait === null) throw new Error("Idle waiter registration failed");
|
|
8281
|
+
return { completion: wait };
|
|
8282
|
+
}
|
|
8283
|
+
async waitForIdle(signal) {
|
|
8284
|
+
const registered = await this.registerIdleWaiter(signal);
|
|
8285
|
+
return registered.completion;
|
|
7620
8286
|
}
|
|
7621
8287
|
async stop(reason) {
|
|
7622
8288
|
this.#stopReason = reason;
|
|
7623
8289
|
await this.store.putIncarnation({
|
|
7624
8290
|
incarnationId: this.incarnationId,
|
|
8291
|
+
agentId: this.agent.summary.id,
|
|
7625
8292
|
agentName: this.agent.summary.name,
|
|
7626
8293
|
pid: this.process.pid,
|
|
7627
8294
|
state: "stopping"
|
|
@@ -7642,6 +8309,7 @@ var AgentCoordinator = class {
|
|
|
7642
8309
|
await this.store.putAgent(this.agent);
|
|
7643
8310
|
await this.store.putIncarnation({
|
|
7644
8311
|
incarnationId: this.incarnationId,
|
|
8312
|
+
agentId: this.agent.summary.id,
|
|
7645
8313
|
agentName: this.agent.summary.name,
|
|
7646
8314
|
pid: this.process.pid,
|
|
7647
8315
|
state: "cleanup_uncertain"
|
|
@@ -7669,6 +8337,7 @@ var AgentCoordinator = class {
|
|
|
7669
8337
|
await this.store.putAgent(this.agent);
|
|
7670
8338
|
await this.store.putIncarnation({
|
|
7671
8339
|
incarnationId: this.incarnationId,
|
|
8340
|
+
agentId: this.agent.summary.id,
|
|
7672
8341
|
agentName: this.agent.summary.name,
|
|
7673
8342
|
pid: this.process.pid,
|
|
7674
8343
|
state: "cleanup_uncertain"
|
|
@@ -7679,7 +8348,7 @@ var AgentCoordinator = class {
|
|
|
7679
8348
|
}
|
|
7680
8349
|
const wasActive = this.agent.summary.state === "working" || this.agent.summary.state === "restoring";
|
|
7681
8350
|
const destroyed = this.#stopReason === "destroy";
|
|
7682
|
-
const interrupted = error !== null && this.#stopReason === null || wasActive && this.#stopReason !== "destroy";
|
|
8351
|
+
const interrupted = this.agent.summary.state === "failed" || error !== null && this.#stopReason === null || wasActive && this.#stopReason !== "destroy";
|
|
7683
8352
|
const state = destroyed ? "destroying" : interrupted ? "failed" : "idle";
|
|
7684
8353
|
this.agent = {
|
|
7685
8354
|
...this.agent,
|
|
@@ -7693,6 +8362,7 @@ var AgentCoordinator = class {
|
|
|
7693
8362
|
await this.store.putAgent(this.agent);
|
|
7694
8363
|
await this.store.putIncarnation({
|
|
7695
8364
|
incarnationId: this.incarnationId,
|
|
8365
|
+
agentId: this.agent.summary.id,
|
|
7696
8366
|
agentName: this.agent.summary.name,
|
|
7697
8367
|
pid: this.process.pid,
|
|
7698
8368
|
state: "gone"
|
|
@@ -7706,6 +8376,7 @@ var AgentCoordinator = class {
|
|
|
7706
8376
|
}
|
|
7707
8377
|
async #markWorking() {
|
|
7708
8378
|
this.#mayBeWorking = true;
|
|
8379
|
+
this.#idleEventPosition = null;
|
|
7709
8380
|
this.agent = {
|
|
7710
8381
|
...this.agent,
|
|
7711
8382
|
summary: {
|
|
@@ -7718,12 +8389,11 @@ var AgentCoordinator = class {
|
|
|
7718
8389
|
await this.store.putAgent(this.agent);
|
|
7719
8390
|
}
|
|
7720
8391
|
async #markIdle() {
|
|
7721
|
-
const
|
|
7722
|
-
|
|
7723
|
-
this
|
|
8392
|
+
const state = await this.process.getState();
|
|
8393
|
+
if (state.isStreaming || state.isCompacting || state.pendingMessageCount !== 0) return;
|
|
8394
|
+
this.#applyObservedState(state);
|
|
8395
|
+
const idleAgent = {
|
|
7724
8396
|
...this.agent,
|
|
7725
|
-
latestAssistantText,
|
|
7726
|
-
responseObservedAt: latestAssistantText === null ? this.agent.responseObservedAt : this.now(),
|
|
7727
8397
|
summary: {
|
|
7728
8398
|
...this.agent.summary,
|
|
7729
8399
|
state: "idle",
|
|
@@ -7731,15 +8401,50 @@ var AgentCoordinator = class {
|
|
|
7731
8401
|
error: void 0
|
|
7732
8402
|
}
|
|
7733
8403
|
};
|
|
7734
|
-
|
|
7735
|
-
|
|
8404
|
+
try {
|
|
8405
|
+
const idleEventPosition = await this.onIdle?.(idleAgent) ?? null;
|
|
8406
|
+
await this.store.putAgent(idleAgent);
|
|
8407
|
+
this.agent = idleAgent;
|
|
8408
|
+
this.#idleEventPosition = idleEventPosition;
|
|
8409
|
+
this.#mayBeWorking = false;
|
|
8410
|
+
this.#resolveIdleWaiters();
|
|
8411
|
+
} catch (error) {
|
|
8412
|
+
const failure = error instanceof Error ? error : new Error("Idle durability failed");
|
|
8413
|
+
this.agent = {
|
|
8414
|
+
...idleAgent,
|
|
8415
|
+
summary: {
|
|
8416
|
+
...idleAgent.summary,
|
|
8417
|
+
state: "failed",
|
|
8418
|
+
error: { code: "storage_unavailable" }
|
|
8419
|
+
}
|
|
8420
|
+
};
|
|
8421
|
+
await this.store.putAgent(this.agent).catch(() => void 0);
|
|
8422
|
+
this.#rejectIdleWaiters(failure);
|
|
8423
|
+
throw failure;
|
|
8424
|
+
}
|
|
8425
|
+
}
|
|
8426
|
+
#applyObservedState(state) {
|
|
8427
|
+
this.agent = {
|
|
8428
|
+
...this.agent,
|
|
8429
|
+
launch: observeSession(this.agent.launch, {
|
|
8430
|
+
path: state.sessionFile ?? null,
|
|
8431
|
+
id: state.sessionId
|
|
8432
|
+
}),
|
|
8433
|
+
summary: {
|
|
8434
|
+
...this.agent.summary,
|
|
8435
|
+
session: { path: state.sessionFile ?? null, id: state.sessionId }
|
|
8436
|
+
}
|
|
8437
|
+
};
|
|
7736
8438
|
}
|
|
7737
8439
|
#resolveIdleWaiters() {
|
|
7738
8440
|
for (const waiter of this.#idleWaiters) {
|
|
7739
8441
|
if (waiter.signal !== void 0 && waiter.onAbort !== void 0) {
|
|
7740
8442
|
waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
7741
8443
|
}
|
|
7742
|
-
waiter.resolve(
|
|
8444
|
+
waiter.resolve({
|
|
8445
|
+
agent: this.agent,
|
|
8446
|
+
idleEventPosition: this.#idleEventPosition ?? 0
|
|
8447
|
+
});
|
|
7743
8448
|
}
|
|
7744
8449
|
this.#idleWaiters.clear();
|
|
7745
8450
|
}
|
|
@@ -7784,10 +8489,9 @@ var FleetService = class {
|
|
|
7784
8489
|
...DEFAULT_RUNTIME_LIMITS,
|
|
7785
8490
|
...typeof options === "function" ? {} : options.limits
|
|
7786
8491
|
};
|
|
7787
|
-
this.#
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
});
|
|
8492
|
+
this.#journal = typeof options === "function" ? void 0 : options.journal;
|
|
8493
|
+
this.#journalStore = typeof options === "function" ? void 0 : options.journalStore;
|
|
8494
|
+
this.#onAgentDestroyed = typeof options === "function" ? () => void 0 : options.onAgentDestroyed ?? (() => void 0);
|
|
7791
8495
|
}
|
|
7792
8496
|
#operations = /* @__PURE__ */ new Map();
|
|
7793
8497
|
#inflightOperations = /* @__PURE__ */ new Map();
|
|
@@ -7800,9 +8504,37 @@ var FleetService = class {
|
|
|
7800
8504
|
#launcher;
|
|
7801
8505
|
#now;
|
|
7802
8506
|
#limits;
|
|
7803
|
-
#
|
|
8507
|
+
#journal;
|
|
8508
|
+
#journalStore;
|
|
8509
|
+
#journalSinks = /* @__PURE__ */ new Map();
|
|
8510
|
+
#onAgentDestroyed;
|
|
7804
8511
|
#piIdentity;
|
|
8512
|
+
#storageFailure = null;
|
|
7805
8513
|
#closing = false;
|
|
8514
|
+
failStorage(error) {
|
|
8515
|
+
this.#storageFailure ??= error;
|
|
8516
|
+
for (const coordinator of this.#coordinators.values()) {
|
|
8517
|
+
void coordinator.process.stop().catch(() => void 0);
|
|
8518
|
+
}
|
|
8519
|
+
}
|
|
8520
|
+
failAgent(agentId, error) {
|
|
8521
|
+
const coordinator = [...this.#coordinators.values()].find(
|
|
8522
|
+
(candidate) => candidate.storedAgent.summary.id === agentId
|
|
8523
|
+
);
|
|
8524
|
+
if (coordinator === void 0) return;
|
|
8525
|
+
const agent = coordinator.storedAgent;
|
|
8526
|
+
void this.store.putAgent({
|
|
8527
|
+
...agent,
|
|
8528
|
+
summary: {
|
|
8529
|
+
...agent.summary,
|
|
8530
|
+
state: "failed",
|
|
8531
|
+
error: {
|
|
8532
|
+
code: error.message.includes("capacity") ? "storage_unavailable" : "state_corrupt"
|
|
8533
|
+
}
|
|
8534
|
+
}
|
|
8535
|
+
}).catch(() => void 0);
|
|
8536
|
+
void coordinator.process.stop().catch(() => void 0);
|
|
8537
|
+
}
|
|
7806
8538
|
create(input, operationId, callerPiIdentity) {
|
|
7807
8539
|
return this.#runOperation(
|
|
7808
8540
|
operationId,
|
|
@@ -7812,8 +8544,12 @@ var FleetService = class {
|
|
|
7812
8544
|
);
|
|
7813
8545
|
}
|
|
7814
8546
|
async #createImpl(input, operationId, callerPiIdentity) {
|
|
8547
|
+
if (this.#storageFailure !== null) {
|
|
8548
|
+
return err({ code: "storage_unavailable", message: "pi-fleet storage is unavailable." });
|
|
8549
|
+
}
|
|
7815
8550
|
const replay = await this.#operation(operationId, "create", input);
|
|
7816
8551
|
if (replay !== null) return replay;
|
|
8552
|
+
if (this.#closing) return this.#runtimeUnavailable();
|
|
7817
8553
|
let profile;
|
|
7818
8554
|
try {
|
|
7819
8555
|
profile = createLaunchProfile({
|
|
@@ -7864,11 +8600,15 @@ var FleetService = class {
|
|
|
7864
8600
|
process: { state: this.#launcher === void 0 ? "resident" : "starting" },
|
|
7865
8601
|
session: { path: null, id: null }
|
|
7866
8602
|
},
|
|
7867
|
-
launch: profile
|
|
7868
|
-
latestAssistantText: null,
|
|
7869
|
-
responseObservedAt: null
|
|
8603
|
+
launch: profile
|
|
7870
8604
|
};
|
|
7871
|
-
|
|
8605
|
+
const pendingCreate = await this.store.getOperation(operationId);
|
|
8606
|
+
if (pendingCreate === null) throw new Error("Create operation receipt is missing");
|
|
8607
|
+
if (!await this.store.createAgent(agent, {
|
|
8608
|
+
...pendingCreate,
|
|
8609
|
+
targetName: input.name,
|
|
8610
|
+
targetAgent: { id: agent.summary.id, name: input.name }
|
|
8611
|
+
})) {
|
|
7872
8612
|
const result = err({
|
|
7873
8613
|
code: "name_taken",
|
|
7874
8614
|
message: `Agent ${input.name} already exists.`
|
|
@@ -7876,14 +8616,22 @@ var FleetService = class {
|
|
|
7876
8616
|
await this.#remember(operationId, "create", input, result);
|
|
7877
8617
|
return result;
|
|
7878
8618
|
}
|
|
8619
|
+
if (this.#journalStore !== void 0) {
|
|
8620
|
+
await this.#journalStore.putEpoch({
|
|
8621
|
+
agentId: agent.summary.id,
|
|
8622
|
+
epoch: 0,
|
|
8623
|
+
state: "open",
|
|
8624
|
+
lastSafeEventPosition: 0,
|
|
8625
|
+
openedAt: this.#now()
|
|
8626
|
+
});
|
|
8627
|
+
}
|
|
7879
8628
|
await this.#recordOperationTarget(operationId, agent);
|
|
7880
8629
|
if (this.#launcher !== void 0 && this.#reserveProcessSlot(input.name) !== "acquired") {
|
|
7881
|
-
await this.store.deleteAgent(input.name);
|
|
7882
8630
|
const result = err({
|
|
7883
8631
|
code: "capacity_exceeded",
|
|
7884
8632
|
message: `pi-fleet has reached its ${String(this.#limits.maxResidentProcesses)} process limit.`
|
|
7885
8633
|
});
|
|
7886
|
-
await this.#
|
|
8634
|
+
await this.#rollbackProvisionalCreate(agent, operationId, result);
|
|
7887
8635
|
return result;
|
|
7888
8636
|
}
|
|
7889
8637
|
let incarnationId = null;
|
|
@@ -7892,26 +8640,29 @@ var FleetService = class {
|
|
|
7892
8640
|
incarnationId = randomUUID2();
|
|
7893
8641
|
await this.store.putIncarnation({
|
|
7894
8642
|
incarnationId,
|
|
8643
|
+
agentId: agent.summary.id,
|
|
7895
8644
|
agentName: input.name,
|
|
7896
8645
|
pid: null,
|
|
7897
8646
|
state: "starting"
|
|
7898
8647
|
});
|
|
7899
|
-
const
|
|
8648
|
+
const journalSink = await this.#openJournalSink(agent, incarnationId);
|
|
7900
8649
|
const process2 = await this.#launcher.start(
|
|
7901
8650
|
profile,
|
|
7902
8651
|
false,
|
|
7903
8652
|
async (pid) => {
|
|
7904
8653
|
await this.store.putIncarnation({
|
|
7905
8654
|
incarnationId,
|
|
8655
|
+
agentId: agent.summary.id,
|
|
7906
8656
|
agentName: input.name,
|
|
7907
8657
|
pid,
|
|
7908
8658
|
state: "starting"
|
|
7909
8659
|
});
|
|
7910
8660
|
},
|
|
7911
|
-
|
|
8661
|
+
journalSink?.pushRecord
|
|
7912
8662
|
);
|
|
7913
8663
|
await this.store.putIncarnation({
|
|
7914
8664
|
incarnationId,
|
|
8665
|
+
agentId: agent.summary.id,
|
|
7915
8666
|
agentName: input.name,
|
|
7916
8667
|
pid: process2.pid,
|
|
7917
8668
|
state: "live"
|
|
@@ -7921,6 +8672,7 @@ var FleetService = class {
|
|
|
7921
8672
|
path: state.sessionFile ?? null,
|
|
7922
8673
|
id: state.sessionId
|
|
7923
8674
|
});
|
|
8675
|
+
await this.#journal?.markIdle(agent.summary.id);
|
|
7924
8676
|
agent = {
|
|
7925
8677
|
...agent,
|
|
7926
8678
|
launch: observedProfile,
|
|
@@ -7939,36 +8691,47 @@ var FleetService = class {
|
|
|
7939
8691
|
const ordinal = await this.store.nextSendOrdinal(input.name);
|
|
7940
8692
|
await this.store.putSend({
|
|
7941
8693
|
sendId,
|
|
8694
|
+
agentId: agent.summary.id,
|
|
7942
8695
|
agentName: input.name,
|
|
7943
8696
|
ordinal,
|
|
7944
8697
|
message: input.instructions,
|
|
8698
|
+
delivery: "steer",
|
|
7945
8699
|
state: "pending",
|
|
7946
8700
|
acceptedAt
|
|
7947
8701
|
});
|
|
7948
8702
|
await this.store.putSend({
|
|
7949
8703
|
sendId,
|
|
8704
|
+
agentId: agent.summary.id,
|
|
7950
8705
|
agentName: input.name,
|
|
7951
8706
|
ordinal,
|
|
7952
8707
|
message: input.instructions,
|
|
8708
|
+
delivery: "steer",
|
|
7953
8709
|
state: "dispatching",
|
|
7954
8710
|
acceptedAt
|
|
7955
8711
|
});
|
|
7956
8712
|
try {
|
|
7957
|
-
await this.#enqueueSend(input.name, () =>
|
|
8713
|
+
await this.#enqueueSend(input.name, async () => {
|
|
8714
|
+
await coordinator.send(input.instructions);
|
|
8715
|
+
await coordinator.reconcileState();
|
|
8716
|
+
});
|
|
7958
8717
|
await this.store.putSend({
|
|
7959
8718
|
sendId,
|
|
8719
|
+
agentId: agent.summary.id,
|
|
7960
8720
|
agentName: input.name,
|
|
7961
8721
|
ordinal,
|
|
7962
8722
|
message: input.instructions,
|
|
8723
|
+
delivery: "steer",
|
|
7963
8724
|
state: "acknowledged",
|
|
7964
8725
|
acceptedAt
|
|
7965
8726
|
});
|
|
7966
8727
|
} catch (error) {
|
|
7967
8728
|
await this.store.putSend({
|
|
7968
8729
|
sendId,
|
|
8730
|
+
agentId: agent.summary.id,
|
|
7969
8731
|
agentName: input.name,
|
|
7970
8732
|
ordinal,
|
|
7971
8733
|
message: input.instructions,
|
|
8734
|
+
delivery: "steer",
|
|
7972
8735
|
state: "uncertain",
|
|
7973
8736
|
acceptedAt
|
|
7974
8737
|
});
|
|
@@ -7996,11 +8759,7 @@ var FleetService = class {
|
|
|
7996
8759
|
} catch (error) {
|
|
7997
8760
|
const coordinator = this.#coordinators.get(input.name);
|
|
7998
8761
|
if (incarnationId !== null && coordinator === void 0) {
|
|
7999
|
-
this.#
|
|
8000
|
-
input.name,
|
|
8001
|
-
incarnationId,
|
|
8002
|
-
new RpcWatchError("pi_start_failed", "Pi failed to start")
|
|
8003
|
-
);
|
|
8762
|
+
this.#finishJournalSink(incarnationId);
|
|
8004
8763
|
}
|
|
8005
8764
|
const deliveryAmbiguous = (await this.store.getSend(`${operationId}:initial`))?.state === "uncertain";
|
|
8006
8765
|
let cleanupUncertain = error instanceof PiCleanupUncertainError;
|
|
@@ -8039,34 +8798,49 @@ var FleetService = class {
|
|
|
8039
8798
|
if (incarnationId !== null) {
|
|
8040
8799
|
await this.store.putIncarnation({
|
|
8041
8800
|
incarnationId,
|
|
8801
|
+
agentId: agent.summary.id,
|
|
8042
8802
|
agentName: input.name,
|
|
8043
8803
|
pid: error instanceof PiCleanupUncertainError ? error.pid : null,
|
|
8044
8804
|
state: "gone"
|
|
8045
8805
|
});
|
|
8046
8806
|
}
|
|
8047
8807
|
this.#releaseProcessSlot(input.name);
|
|
8048
|
-
await this.store.deleteAgent(input.name);
|
|
8049
8808
|
}
|
|
8050
8809
|
const code = cleanupUncertain ? "incarnation_cleanup_uncertain" : deliveryAmbiguous ? "delivery_uncertain" : "pi_start_failed";
|
|
8051
8810
|
const result = err({
|
|
8052
8811
|
code,
|
|
8053
8812
|
message: code === "delivery_uncertain" ? "Pi may have accepted the initial instructions; pi-fleet will not replay them automatically." : code === "incarnation_cleanup_uncertain" ? "pi-fleet could not prove the Pi process group was removed." : "Pi failed to start."
|
|
8054
8813
|
});
|
|
8055
|
-
|
|
8814
|
+
if (cleanupUncertain || deliveryAmbiguous) {
|
|
8815
|
+
await this.#remember(operationId, "create", input, result);
|
|
8816
|
+
} else {
|
|
8817
|
+
await this.#rollbackProvisionalCreate(agent, operationId, result);
|
|
8818
|
+
}
|
|
8056
8819
|
return result;
|
|
8057
8820
|
}
|
|
8058
8821
|
}
|
|
8059
8822
|
send(input, operationId, callerPiIdentity) {
|
|
8823
|
+
const normalizedInput = {
|
|
8824
|
+
...input,
|
|
8825
|
+
delivery: input.delivery ?? "steer"
|
|
8826
|
+
};
|
|
8060
8827
|
return this.#runOperation(
|
|
8061
8828
|
operationId,
|
|
8062
8829
|
"send",
|
|
8063
|
-
|
|
8064
|
-
() => this.#enqueueAgent(
|
|
8830
|
+
normalizedInput,
|
|
8831
|
+
() => this.#enqueueAgent(
|
|
8832
|
+
normalizedInput.name,
|
|
8833
|
+
() => this.#sendImpl(normalizedInput, operationId, callerPiIdentity)
|
|
8834
|
+
)
|
|
8065
8835
|
);
|
|
8066
8836
|
}
|
|
8067
8837
|
async #sendImpl(input, operationId, callerPiIdentity) {
|
|
8838
|
+
if (this.#storageFailure !== null) {
|
|
8839
|
+
return err({ code: "storage_unavailable", message: "pi-fleet storage is unavailable." });
|
|
8840
|
+
}
|
|
8068
8841
|
const replay = await this.#operation(operationId, "send", input);
|
|
8069
8842
|
if (replay !== null) return replay;
|
|
8843
|
+
if (this.#closing) return this.#runtimeUnavailable();
|
|
8070
8844
|
if (this.#destroyingAgents.has(input.name)) {
|
|
8071
8845
|
const result2 = err({
|
|
8072
8846
|
code: "agent_destroying",
|
|
@@ -8085,6 +8859,12 @@ var FleetService = class {
|
|
|
8085
8859
|
}
|
|
8086
8860
|
const agent = await this.store.getAgent(input.name);
|
|
8087
8861
|
if (agent === null) return this.#rememberNotFound(operationId, "send", input);
|
|
8862
|
+
const staleTarget = this.#staleTarget(input.expectedAgentId, agent);
|
|
8863
|
+
if (staleTarget !== null) {
|
|
8864
|
+
await this.#remember(operationId, "send", input, staleTarget);
|
|
8865
|
+
return staleTarget;
|
|
8866
|
+
}
|
|
8867
|
+
await this.#recordOperationTarget(operationId, agent);
|
|
8088
8868
|
if (agent.summary.process.state === "cleanup_uncertain") {
|
|
8089
8869
|
const result2 = err({
|
|
8090
8870
|
code: "incarnation_cleanup_uncertain",
|
|
@@ -8110,9 +8890,11 @@ var FleetService = class {
|
|
|
8110
8890
|
const ordinal = await this.store.nextSendOrdinal(input.name);
|
|
8111
8891
|
await this.store.putSend({
|
|
8112
8892
|
sendId: operationId,
|
|
8893
|
+
agentId: agent.summary.id,
|
|
8113
8894
|
agentName: input.name,
|
|
8114
8895
|
ordinal,
|
|
8115
8896
|
message: input.message,
|
|
8897
|
+
delivery: input.delivery ?? "steer",
|
|
8116
8898
|
state: "pending",
|
|
8117
8899
|
acceptedAt
|
|
8118
8900
|
});
|
|
@@ -8130,8 +8912,12 @@ var FleetService = class {
|
|
|
8130
8912
|
);
|
|
8131
8913
|
}
|
|
8132
8914
|
async #compactImpl(input, operationId, callerPiIdentity) {
|
|
8915
|
+
if (this.#storageFailure !== null) {
|
|
8916
|
+
return err({ code: "storage_unavailable", message: "pi-fleet storage is unavailable." });
|
|
8917
|
+
}
|
|
8133
8918
|
const replay = await this.#operation(operationId, "compact", input);
|
|
8134
8919
|
if (replay !== null) return replay;
|
|
8920
|
+
if (this.#closing) return this.#runtimeUnavailable();
|
|
8135
8921
|
const requestedAt = this.#now();
|
|
8136
8922
|
let agent = await this.store.getAgent(input.name);
|
|
8137
8923
|
if (agent === null) {
|
|
@@ -8140,6 +8926,11 @@ var FleetService = class {
|
|
|
8140
8926
|
message: `Agent ${input.name} was not found.`
|
|
8141
8927
|
});
|
|
8142
8928
|
}
|
|
8929
|
+
const staleTarget = this.#staleTarget(input.expectedAgentId, agent);
|
|
8930
|
+
if (staleTarget !== null) {
|
|
8931
|
+
await this.#remember(operationId, "compact", input, staleTarget);
|
|
8932
|
+
return staleTarget;
|
|
8933
|
+
}
|
|
8143
8934
|
await this.#recordOperationTarget(operationId, agent);
|
|
8144
8935
|
if (agent.summary.process.state === "cleanup_uncertain") {
|
|
8145
8936
|
return this.#rememberCompactFailure(operationId, input, requestedAt, {
|
|
@@ -8163,6 +8954,7 @@ var FleetService = class {
|
|
|
8163
8954
|
}
|
|
8164
8955
|
await this.store.putCompact({
|
|
8165
8956
|
compactId: operationId,
|
|
8957
|
+
agentId: agent.summary.id,
|
|
8166
8958
|
agentName: input.name,
|
|
8167
8959
|
state: "pending",
|
|
8168
8960
|
requestedAt
|
|
@@ -8177,6 +8969,7 @@ var FleetService = class {
|
|
|
8177
8969
|
try {
|
|
8178
8970
|
await this.store.putCompact({
|
|
8179
8971
|
compactId: operationId,
|
|
8972
|
+
agentId: agent.summary.id,
|
|
8180
8973
|
agentName: input.name,
|
|
8181
8974
|
state: "dispatching",
|
|
8182
8975
|
requestedAt
|
|
@@ -8192,6 +8985,7 @@ var FleetService = class {
|
|
|
8192
8985
|
}
|
|
8193
8986
|
await this.store.putCompact({
|
|
8194
8987
|
compactId: operationId,
|
|
8988
|
+
agentId: agent.summary.id,
|
|
8195
8989
|
agentName: input.name,
|
|
8196
8990
|
state: "completed",
|
|
8197
8991
|
requestedAt,
|
|
@@ -8235,6 +9029,7 @@ var FleetService = class {
|
|
|
8235
9029
|
const terminalFailure = busy || preDispatch || compactionError !== null;
|
|
8236
9030
|
await this.store.putCompact({
|
|
8237
9031
|
compactId: operationId,
|
|
9032
|
+
agentId: agent.summary.id,
|
|
8238
9033
|
agentName: input.name,
|
|
8239
9034
|
state: terminalFailure ? "failed" : "uncertain",
|
|
8240
9035
|
requestedAt,
|
|
@@ -8263,26 +9058,29 @@ var FleetService = class {
|
|
|
8263
9058
|
try {
|
|
8264
9059
|
await this.store.putIncarnation({
|
|
8265
9060
|
incarnationId,
|
|
9061
|
+
agentId: agent.summary.id,
|
|
8266
9062
|
agentName: agent.summary.name,
|
|
8267
9063
|
pid: null,
|
|
8268
9064
|
state: "starting"
|
|
8269
9065
|
});
|
|
8270
|
-
const
|
|
9066
|
+
const journalSink = await this.#openJournalSink(restoring, incarnationId);
|
|
8271
9067
|
process2 = await this.#launcher.start(
|
|
8272
9068
|
restoring.launch,
|
|
8273
9069
|
true,
|
|
8274
9070
|
async (pid) => {
|
|
8275
9071
|
await this.store.putIncarnation({
|
|
8276
9072
|
incarnationId,
|
|
9073
|
+
agentId: agent.summary.id,
|
|
8277
9074
|
agentName: agent.summary.name,
|
|
8278
9075
|
pid,
|
|
8279
9076
|
state: "starting"
|
|
8280
9077
|
});
|
|
8281
9078
|
},
|
|
8282
|
-
|
|
9079
|
+
journalSink?.pushRecord
|
|
8283
9080
|
);
|
|
8284
9081
|
await this.store.putIncarnation({
|
|
8285
9082
|
incarnationId,
|
|
9083
|
+
agentId: agent.summary.id,
|
|
8286
9084
|
agentName: agent.summary.name,
|
|
8287
9085
|
pid: process2.pid,
|
|
8288
9086
|
state: "live"
|
|
@@ -8305,11 +9103,7 @@ var FleetService = class {
|
|
|
8305
9103
|
await this.store.putAgent(restored);
|
|
8306
9104
|
return this.#attachCoordinator(restored, process2, incarnationId);
|
|
8307
9105
|
} catch (error) {
|
|
8308
|
-
this.#
|
|
8309
|
-
agent.summary.name,
|
|
8310
|
-
incarnationId,
|
|
8311
|
-
new RpcWatchError("pi_start_failed", "Pi failed to start")
|
|
8312
|
-
);
|
|
9106
|
+
this.#finishJournalSink(incarnationId);
|
|
8313
9107
|
let cleanupUncertain = error instanceof PiCleanupUncertainError;
|
|
8314
9108
|
let pid = error instanceof PiCleanupUncertainError ? error.pid : null;
|
|
8315
9109
|
if (process2 !== null) {
|
|
@@ -8331,6 +9125,7 @@ var FleetService = class {
|
|
|
8331
9125
|
});
|
|
8332
9126
|
await this.store.putIncarnation({
|
|
8333
9127
|
incarnationId,
|
|
9128
|
+
agentId: agent.summary.id,
|
|
8334
9129
|
agentName: agent.summary.name,
|
|
8335
9130
|
pid,
|
|
8336
9131
|
state: cleanupUncertain ? "cleanup_uncertain" : "gone"
|
|
@@ -8342,11 +9137,12 @@ var FleetService = class {
|
|
|
8342
9137
|
async reconcile() {
|
|
8343
9138
|
const nonterminalCompacts = await this.store.listNonterminalCompacts();
|
|
8344
9139
|
const nonterminalSends = await this.store.listNonterminalSends();
|
|
9140
|
+
const activeIncarnations = await this.store.listActiveIncarnations();
|
|
8345
9141
|
const activeWorkAgents = /* @__PURE__ */ new Set([
|
|
8346
9142
|
...nonterminalSends.map((send) => send.agentName),
|
|
8347
9143
|
...nonterminalCompacts.filter((compact) => compact.state === "dispatching").map((compact) => compact.agentName)
|
|
8348
9144
|
]);
|
|
8349
|
-
for (const incarnation of
|
|
9145
|
+
for (const incarnation of activeIncarnations) {
|
|
8350
9146
|
if (incarnation.state !== "cleanup_uncertain") continue;
|
|
8351
9147
|
this.#processSlots.add(incarnation.agentName);
|
|
8352
9148
|
if (incarnation.pid === null || !await waitForProcessGroupExit(incarnation.pid)) continue;
|
|
@@ -8380,7 +9176,15 @@ var FleetService = class {
|
|
|
8380
9176
|
}
|
|
8381
9177
|
}
|
|
8382
9178
|
for (const send of nonterminalSends) {
|
|
8383
|
-
|
|
9179
|
+
if (send.agentId === void 0) {
|
|
9180
|
+
throw new Error(`Durable send ${send.sendId} is missing its agent generation`);
|
|
9181
|
+
}
|
|
9182
|
+
const input = {
|
|
9183
|
+
name: send.agentName,
|
|
9184
|
+
expectedAgentId: send.agentId,
|
|
9185
|
+
message: send.message,
|
|
9186
|
+
delivery: send.delivery ?? "steer"
|
|
9187
|
+
};
|
|
8384
9188
|
if (send.state === "dispatching") {
|
|
8385
9189
|
const result2 = err({
|
|
8386
9190
|
code: "delivery_uncertain",
|
|
@@ -8409,62 +9213,75 @@ var FleetService = class {
|
|
|
8409
9213
|
}
|
|
8410
9214
|
for (const operation of await this.store.listPendingOperations()) {
|
|
8411
9215
|
if (operation.method === "send") continue;
|
|
8412
|
-
const
|
|
9216
|
+
const target = operation.targetAgent;
|
|
8413
9217
|
if (operation.method === "create") {
|
|
8414
|
-
|
|
8415
|
-
|
|
9218
|
+
const agent = await this.store.getAgent(operation.targetName);
|
|
9219
|
+
const hasActiveIncarnation = activeIncarnations.some(
|
|
9220
|
+
(incarnation) => incarnation.agentId === target?.id
|
|
9221
|
+
);
|
|
9222
|
+
const startFailed = err({
|
|
9223
|
+
code: "pi_start_failed",
|
|
9224
|
+
message: `Creation of ${operation.targetName} stopped before Pi was safely dispatched.`
|
|
9225
|
+
});
|
|
9226
|
+
if (target !== void 0 && agent?.summary.id === target.id && agent.summary.state === "restoring" && !hasActiveIncarnation) {
|
|
9227
|
+
await this.#rollbackProvisionalCreate(agent, operation.operationId, startFailed);
|
|
9228
|
+
} else if (target !== void 0 && agent?.summary.id === target.id) {
|
|
9229
|
+
const result = agent.summary.state === "idle" ? ok({
|
|
9230
|
+
schemaVersion: 1,
|
|
9231
|
+
type: "agent.created",
|
|
9232
|
+
agent: agent.summary
|
|
9233
|
+
}) : err({
|
|
9234
|
+
code: publicAgentFailureCode(agent.summary.error?.code, "runtime_interrupted"),
|
|
9235
|
+
message: `Creation of ${operation.targetName} did not settle successfully.`
|
|
9236
|
+
});
|
|
9237
|
+
await this.store.putOperation({ ...operation, state: "completed", result });
|
|
9238
|
+
} else if (target === void 0 && agent === null && isCreateRequest(operation.request)) {
|
|
9239
|
+
if (piExecutionFailure === null)
|
|
9240
|
+
await this.create(operation.request, operation.operationId);
|
|
9241
|
+
} else {
|
|
9242
|
+
await this.store.putOperation({ ...operation, state: "completed", result: startFailed });
|
|
9243
|
+
}
|
|
8416
9244
|
} else if (operation.method === "destroy") {
|
|
8417
|
-
|
|
9245
|
+
if (target === void 0) {
|
|
9246
|
+
await this.store.putOperation({
|
|
9247
|
+
...operation,
|
|
9248
|
+
state: "completed",
|
|
9249
|
+
result: this.#notFound(operation.targetName)
|
|
9250
|
+
});
|
|
9251
|
+
} else {
|
|
9252
|
+
await this.destroy(
|
|
9253
|
+
{ name: target.name, expectedAgentId: target.id },
|
|
9254
|
+
operation.operationId
|
|
9255
|
+
);
|
|
9256
|
+
}
|
|
8418
9257
|
} else if (operation.method === "compact") {
|
|
8419
|
-
if (
|
|
8420
|
-
await this.
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
if (error instanceof Error && error.message === "Agent destroyed") {
|
|
8432
|
-
return err({ code: "agent_destroyed", message: `Agent ${input.name} was destroyed.` });
|
|
8433
|
-
}
|
|
8434
|
-
if (error instanceof Error && error.message === "Pi work was interrupted") {
|
|
8435
|
-
return err({
|
|
8436
|
-
code: "runtime_interrupted",
|
|
8437
|
-
message: `Agent ${input.name} was interrupted before becoming idle.`
|
|
8438
|
-
});
|
|
9258
|
+
if (target === void 0) {
|
|
9259
|
+
await this.store.putOperation({
|
|
9260
|
+
...operation,
|
|
9261
|
+
state: "completed",
|
|
9262
|
+
result: this.#notFound(operation.targetName)
|
|
9263
|
+
});
|
|
9264
|
+
} else if (piExecutionFailure === null) {
|
|
9265
|
+
await this.compact(
|
|
9266
|
+
{ name: target.name, expectedAgentId: target.id },
|
|
9267
|
+
operation.operationId
|
|
9268
|
+
);
|
|
9269
|
+
}
|
|
8439
9270
|
}
|
|
8440
|
-
throw error;
|
|
8441
|
-
}
|
|
8442
|
-
if (agent === null) return this.#notFound(input.name);
|
|
8443
|
-
if (agent.summary.state === "failed") {
|
|
8444
|
-
const code = agent.summary.error?.code ?? "agent_failed";
|
|
8445
|
-
return err({
|
|
8446
|
-
code,
|
|
8447
|
-
message: `Agent ${input.name} is failed (${code}) and has no current successful response.`
|
|
8448
|
-
});
|
|
8449
|
-
}
|
|
8450
|
-
if (agent.latestAssistantText === null || agent.responseObservedAt === null) {
|
|
8451
|
-
return err({
|
|
8452
|
-
code: "no_response",
|
|
8453
|
-
message: `Agent ${input.name} has no assistant response.`
|
|
8454
|
-
});
|
|
8455
9271
|
}
|
|
8456
|
-
return ok({
|
|
8457
|
-
schemaVersion: 1,
|
|
8458
|
-
type: "response",
|
|
8459
|
-
agent: { id: agent.summary.id, name: agent.summary.name },
|
|
8460
|
-
response: { text: agent.latestAssistantText, observedAt: agent.responseObservedAt }
|
|
8461
|
-
});
|
|
8462
9272
|
}
|
|
8463
9273
|
async status(input) {
|
|
9274
|
+
if (this.#storageFailure !== null) {
|
|
9275
|
+
return err({ code: "storage_unavailable", message: "pi-fleet storage is unavailable." });
|
|
9276
|
+
}
|
|
8464
9277
|
const agent = await this.store.getAgent(input.name);
|
|
8465
|
-
|
|
9278
|
+
if (agent === null) return this.#notFound(input.name);
|
|
9279
|
+
return this.#staleTarget(input.expectedAgentId, agent) ?? ok({ schemaVersion: 1, type: "agent.status", agent: agent.summary });
|
|
8466
9280
|
}
|
|
8467
9281
|
async list() {
|
|
9282
|
+
if (this.#storageFailure !== null) {
|
|
9283
|
+
return err({ code: "storage_unavailable", message: "pi-fleet storage is unavailable." });
|
|
9284
|
+
}
|
|
8468
9285
|
const agents = await this.store.listAgents();
|
|
8469
9286
|
return ok({
|
|
8470
9287
|
schemaVersion: 1,
|
|
@@ -8472,29 +9289,86 @@ var FleetService = class {
|
|
|
8472
9289
|
agents: agents.map((agent) => agent.summary)
|
|
8473
9290
|
});
|
|
8474
9291
|
}
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
if (
|
|
8478
|
-
|
|
9292
|
+
async waitForIdle(input, signal) {
|
|
9293
|
+
const current = await this.status(input);
|
|
9294
|
+
if (!current.ok || current.value.agent.state === "idle") return current;
|
|
9295
|
+
if (current.value.agent.state === "failed") {
|
|
9296
|
+
return err({
|
|
9297
|
+
code: publicAgentFailureCode(current.value.agent.error?.code),
|
|
9298
|
+
message: `Agent ${input.name} failed before reaching idle.`
|
|
9299
|
+
});
|
|
9300
|
+
}
|
|
9301
|
+
const coordinator = this.#coordinators.get(input.name);
|
|
9302
|
+
if (coordinator === void 0) {
|
|
9303
|
+
return err({ code: "state_corrupt", message: `Agent ${input.name} has no active process.` });
|
|
9304
|
+
}
|
|
9305
|
+
try {
|
|
9306
|
+
const boundary = await coordinator.waitForIdle(signal);
|
|
9307
|
+
return this.#staleTarget(input.expectedAgentId, boundary.agent) ?? ok({ schemaVersion: 1, type: "agent.status", agent: boundary.agent.summary });
|
|
9308
|
+
} catch (error) {
|
|
9309
|
+
if (signal.aborted) throw error;
|
|
9310
|
+
return err({
|
|
9311
|
+
code: "runtime_interrupted",
|
|
9312
|
+
message: `Agent ${input.name} was interrupted before becoming idle.`
|
|
9313
|
+
});
|
|
9314
|
+
}
|
|
9315
|
+
}
|
|
9316
|
+
prepareReceive(input, start, untilIdle, signal) {
|
|
8479
9317
|
return this.#enqueueAgent(input.name, async () => {
|
|
8480
9318
|
if (this.#closing) return this.#runtimeUnavailable();
|
|
8481
|
-
if (this.#
|
|
9319
|
+
if (this.#storageFailure !== null) {
|
|
9320
|
+
return err({ code: "storage_unavailable", message: "pi-fleet storage is unavailable." });
|
|
9321
|
+
}
|
|
9322
|
+
if (this.#journal === void 0) {
|
|
9323
|
+
return err({ code: "protocol_incompatible", message: "Semantic receive is unavailable." });
|
|
9324
|
+
}
|
|
8482
9325
|
const agent = await this.store.getAgent(input.name);
|
|
8483
9326
|
if (agent === null) return this.#notFound(input.name);
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
9327
|
+
const stale = this.#staleTarget(input.expectedAgentId, agent);
|
|
9328
|
+
if (stale !== null) return stale;
|
|
9329
|
+
if (agent.summary.state === "failed") {
|
|
9330
|
+
return err({
|
|
9331
|
+
code: publicAgentFailureCode(agent.summary.error?.code),
|
|
9332
|
+
message: `Agent ${input.name} is failed.`
|
|
9333
|
+
});
|
|
9334
|
+
}
|
|
9335
|
+
const agentId = agent.summary.id;
|
|
9336
|
+
const stream = await this.#journal.openReceive(agentId, start, signal);
|
|
9337
|
+
if (!untilIdle) return ok({ agentId, stream, idle: null });
|
|
9338
|
+
const coordinator = this.#coordinators.get(input.name);
|
|
9339
|
+
if (coordinator !== void 0) {
|
|
9340
|
+
const { completion } = await coordinator.registerIdleWaiter(signal);
|
|
9341
|
+
const idle = completion.then(
|
|
9342
|
+
(boundary) => ok({ idleEventPosition: boundary.idleEventPosition }),
|
|
9343
|
+
() => err({
|
|
9344
|
+
code: "runtime_interrupted",
|
|
9345
|
+
message: `Agent ${input.name} was interrupted before becoming idle.`
|
|
9346
|
+
})
|
|
9347
|
+
);
|
|
9348
|
+
return ok({ agentId, stream, idle });
|
|
8494
9349
|
}
|
|
9350
|
+
if (agent.summary.state !== "idle") {
|
|
9351
|
+
return err({
|
|
9352
|
+
code: "state_corrupt",
|
|
9353
|
+
message: `Agent ${input.name} has no active process.`
|
|
9354
|
+
});
|
|
9355
|
+
}
|
|
9356
|
+
const idleEventPosition = await this.#journal.idleEventHighWater(agentId) ?? await this.#journal.markIdle(agentId);
|
|
9357
|
+
return ok({
|
|
9358
|
+
agentId,
|
|
9359
|
+
stream,
|
|
9360
|
+
idle: Promise.resolve(ok({ idleEventPosition }))
|
|
9361
|
+
});
|
|
8495
9362
|
});
|
|
8496
9363
|
}
|
|
8497
9364
|
destroy(input, operationId) {
|
|
9365
|
+
if (this.#closing) return Promise.resolve(this.#runtimeUnavailable());
|
|
9366
|
+
if (this.#storageFailure !== null) {
|
|
9367
|
+
void this.#coordinators.get(input.name)?.process.stop().catch(() => void 0);
|
|
9368
|
+
return Promise.resolve(
|
|
9369
|
+
err({ code: "storage_unavailable", message: "pi-fleet storage is unavailable." })
|
|
9370
|
+
);
|
|
9371
|
+
}
|
|
8498
9372
|
return this.#runOperation(operationId, "destroy", input, async () => {
|
|
8499
9373
|
this.#destroyingAgents.add(input.name);
|
|
8500
9374
|
try {
|
|
@@ -8517,6 +9391,13 @@ var FleetService = class {
|
|
|
8517
9391
|
const replay = await this.#operation(operationId, "destroy", input);
|
|
8518
9392
|
if (replay !== null) return replay;
|
|
8519
9393
|
const stored = await this.store.getAgent(input.name);
|
|
9394
|
+
if (stored !== null) {
|
|
9395
|
+
const staleTarget = this.#staleTarget(input.expectedAgentId, stored);
|
|
9396
|
+
if (staleTarget !== null) {
|
|
9397
|
+
await this.#remember(operationId, "destroy", input, staleTarget);
|
|
9398
|
+
return staleTarget;
|
|
9399
|
+
}
|
|
9400
|
+
}
|
|
8520
9401
|
if (stored?.summary.process.state === "cleanup_uncertain") {
|
|
8521
9402
|
const result2 = err({
|
|
8522
9403
|
code: "destroy_incomplete",
|
|
@@ -8527,11 +9408,27 @@ var FleetService = class {
|
|
|
8527
9408
|
}
|
|
8528
9409
|
if (stored !== null) await this.#recordOperationTarget(operationId, stored);
|
|
8529
9410
|
const coordinator = this.#coordinators.get(input.name);
|
|
8530
|
-
if (coordinator !== void 0)
|
|
9411
|
+
if (coordinator !== void 0) {
|
|
9412
|
+
try {
|
|
9413
|
+
await coordinator.stop("destroy");
|
|
9414
|
+
} catch {
|
|
9415
|
+
const result2 = err({
|
|
9416
|
+
code: "destroy_incomplete",
|
|
9417
|
+
message: `pi-fleet could not prove the Pi process for ${input.name} was removed.`
|
|
9418
|
+
});
|
|
9419
|
+
await this.#remember(operationId, "destroy", input, result2);
|
|
9420
|
+
return result2;
|
|
9421
|
+
}
|
|
9422
|
+
}
|
|
8531
9423
|
this.#coordinators.delete(input.name);
|
|
8532
|
-
this
|
|
8533
|
-
const agent = await this.store.deleteAgent(input.name
|
|
9424
|
+
const pending = await this.store.getOperation(operationId);
|
|
9425
|
+
const agent = await this.store.deleteAgent(input.name, {
|
|
9426
|
+
operationId,
|
|
9427
|
+
fingerprint: pending?.fingerprint ?? JSON.stringify(input),
|
|
9428
|
+
destroyedAt: this.#now()
|
|
9429
|
+
});
|
|
8534
9430
|
if (agent === null) return this.#rememberNotFound(operationId, "destroy", input);
|
|
9431
|
+
this.#onAgentDestroyed(agent.summary.id);
|
|
8535
9432
|
const result = ok({
|
|
8536
9433
|
schemaVersion: 1,
|
|
8537
9434
|
type: "agent.destroyed",
|
|
@@ -8546,20 +9443,37 @@ var FleetService = class {
|
|
|
8546
9443
|
await coordinator.stop("idle_release");
|
|
8547
9444
|
this.#coordinators.delete(name);
|
|
8548
9445
|
}
|
|
8549
|
-
|
|
9446
|
+
beginShutdown() {
|
|
8550
9447
|
this.#closing = true;
|
|
9448
|
+
}
|
|
9449
|
+
async drainStdoutAndJournal() {
|
|
9450
|
+
await Promise.all(
|
|
9451
|
+
[...this.#coordinators.values()].map((coordinator) => {
|
|
9452
|
+
const process2 = coordinator.process;
|
|
9453
|
+
return process2.drainStdout?.() ?? Promise.resolve();
|
|
9454
|
+
})
|
|
9455
|
+
);
|
|
9456
|
+
await this.#journal?.ingestion.drain();
|
|
9457
|
+
}
|
|
9458
|
+
async stopProcessTrees() {
|
|
8551
9459
|
const stops = await Promise.allSettled(
|
|
8552
9460
|
[...this.#coordinators.values()].map((coordinator) => coordinator.stop("runtime_shutdown"))
|
|
8553
9461
|
);
|
|
8554
9462
|
this.#coordinators.clear();
|
|
8555
|
-
this.#
|
|
8556
|
-
|
|
8557
|
-
|
|
9463
|
+
for (const incarnationId of [...this.#journalSinks.keys()]) {
|
|
9464
|
+
this.#finishJournalSink(incarnationId);
|
|
9465
|
+
}
|
|
9466
|
+
await this.#journal?.closeIngestion();
|
|
8558
9467
|
const failure = stops.find(
|
|
8559
9468
|
(result) => result.status === "rejected"
|
|
8560
9469
|
);
|
|
8561
9470
|
if (failure !== void 0) throw failure.reason;
|
|
8562
9471
|
}
|
|
9472
|
+
async close() {
|
|
9473
|
+
this.beginShutdown();
|
|
9474
|
+
await this.drainStdoutAndJournal();
|
|
9475
|
+
await this.stopProcessTrees();
|
|
9476
|
+
}
|
|
8563
9477
|
async #dispatchSend(input, operationId, acceptedAt, initialAgent, ordinal) {
|
|
8564
9478
|
let agent = initialAgent;
|
|
8565
9479
|
let incarnationId = null;
|
|
@@ -8578,9 +9492,11 @@ var FleetService = class {
|
|
|
8578
9492
|
} else if (reservation === "full") {
|
|
8579
9493
|
await this.store.putSend({
|
|
8580
9494
|
sendId: operationId,
|
|
9495
|
+
agentId: agent.summary.id,
|
|
8581
9496
|
agentName: input.name,
|
|
8582
9497
|
ordinal,
|
|
8583
9498
|
message: input.message,
|
|
9499
|
+
delivery: input.delivery ?? "steer",
|
|
8584
9500
|
state: "failed",
|
|
8585
9501
|
acceptedAt
|
|
8586
9502
|
});
|
|
@@ -8594,27 +9510,30 @@ var FleetService = class {
|
|
|
8594
9510
|
incarnationId = randomUUID2();
|
|
8595
9511
|
await this.store.putIncarnation({
|
|
8596
9512
|
incarnationId,
|
|
9513
|
+
agentId: agent.summary.id,
|
|
8597
9514
|
agentName: input.name,
|
|
8598
9515
|
pid: null,
|
|
8599
9516
|
state: "starting"
|
|
8600
9517
|
});
|
|
8601
|
-
const
|
|
9518
|
+
const journalSink = await this.#openJournalSink(agent, incarnationId);
|
|
8602
9519
|
startingProcess = await this.#launcher.start(
|
|
8603
9520
|
agent.launch,
|
|
8604
9521
|
true,
|
|
8605
9522
|
async (pid) => {
|
|
8606
9523
|
await this.store.putIncarnation({
|
|
8607
9524
|
incarnationId,
|
|
9525
|
+
agentId: agent.summary.id,
|
|
8608
9526
|
agentName: input.name,
|
|
8609
9527
|
pid,
|
|
8610
9528
|
state: "starting"
|
|
8611
9529
|
});
|
|
8612
9530
|
},
|
|
8613
|
-
|
|
9531
|
+
journalSink?.pushRecord
|
|
8614
9532
|
);
|
|
8615
9533
|
const process2 = startingProcess;
|
|
8616
9534
|
await this.store.putIncarnation({
|
|
8617
9535
|
incarnationId,
|
|
9536
|
+
agentId: agent.summary.id,
|
|
8618
9537
|
agentName: input.name,
|
|
8619
9538
|
pid: process2.pid,
|
|
8620
9539
|
state: "live"
|
|
@@ -8642,26 +9561,25 @@ var FleetService = class {
|
|
|
8642
9561
|
}
|
|
8643
9562
|
await this.store.putSend({
|
|
8644
9563
|
sendId: operationId,
|
|
9564
|
+
agentId: agent.summary.id,
|
|
8645
9565
|
agentName: input.name,
|
|
8646
9566
|
ordinal,
|
|
8647
9567
|
message: input.message,
|
|
9568
|
+
delivery: input.delivery ?? "steer",
|
|
8648
9569
|
state: "dispatching",
|
|
8649
9570
|
acceptedAt
|
|
8650
9571
|
});
|
|
8651
|
-
if (coordinator
|
|
8652
|
-
await
|
|
8653
|
-
|
|
8654
|
-
latestAssistantText: `Fake response to: ${input.message}`,
|
|
8655
|
-
responseObservedAt: acceptedAt
|
|
8656
|
-
});
|
|
8657
|
-
} else {
|
|
8658
|
-
await coordinator.send(input.message);
|
|
9572
|
+
if (coordinator !== void 0) {
|
|
9573
|
+
await coordinator.send(input.message, input.delivery ?? "steer");
|
|
9574
|
+
await coordinator.reconcileState();
|
|
8659
9575
|
}
|
|
8660
9576
|
await this.store.putSend({
|
|
8661
9577
|
sendId: operationId,
|
|
9578
|
+
agentId: agent.summary.id,
|
|
8662
9579
|
agentName: input.name,
|
|
8663
9580
|
ordinal,
|
|
8664
9581
|
message: input.message,
|
|
9582
|
+
delivery: input.delivery ?? "steer",
|
|
8665
9583
|
state: "acknowledged",
|
|
8666
9584
|
acceptedAt
|
|
8667
9585
|
});
|
|
@@ -8684,11 +9602,7 @@ var FleetService = class {
|
|
|
8684
9602
|
}
|
|
8685
9603
|
}
|
|
8686
9604
|
const code = cleanupUncertain ? "incarnation_cleanup_uncertain" : "pi_start_failed";
|
|
8687
|
-
this.#
|
|
8688
|
-
input.name,
|
|
8689
|
-
incarnationId,
|
|
8690
|
-
new RpcWatchError("pi_start_failed", "Pi failed to restore")
|
|
8691
|
-
);
|
|
9605
|
+
this.#finishJournalSink(incarnationId);
|
|
8692
9606
|
agent = {
|
|
8693
9607
|
...agent,
|
|
8694
9608
|
summary: {
|
|
@@ -8701,15 +9615,18 @@ var FleetService = class {
|
|
|
8701
9615
|
await this.store.putAgent(agent);
|
|
8702
9616
|
await this.store.putIncarnation({
|
|
8703
9617
|
incarnationId,
|
|
9618
|
+
agentId: agent.summary.id,
|
|
8704
9619
|
agentName: input.name,
|
|
8705
9620
|
pid: cleanupPid,
|
|
8706
9621
|
state: cleanupUncertain ? "cleanup_uncertain" : "gone"
|
|
8707
9622
|
});
|
|
8708
9623
|
await this.store.putSend({
|
|
8709
9624
|
sendId: operationId,
|
|
9625
|
+
agentId: agent.summary.id,
|
|
8710
9626
|
agentName: input.name,
|
|
8711
9627
|
ordinal,
|
|
8712
9628
|
message: input.message,
|
|
9629
|
+
delivery: input.delivery ?? "steer",
|
|
8713
9630
|
state: "failed",
|
|
8714
9631
|
acceptedAt
|
|
8715
9632
|
});
|
|
@@ -8723,6 +9640,7 @@ var FleetService = class {
|
|
|
8723
9640
|
if (incarnationId !== null && !this.#coordinators.has(input.name)) {
|
|
8724
9641
|
await this.store.putIncarnation({
|
|
8725
9642
|
incarnationId,
|
|
9643
|
+
agentId: agent.summary.id,
|
|
8726
9644
|
agentName: input.name,
|
|
8727
9645
|
pid: null,
|
|
8728
9646
|
state: "cleanup_uncertain"
|
|
@@ -8730,9 +9648,11 @@ var FleetService = class {
|
|
|
8730
9648
|
}
|
|
8731
9649
|
await this.store.putSend({
|
|
8732
9650
|
sendId: operationId,
|
|
9651
|
+
agentId: agent.summary.id,
|
|
8733
9652
|
agentName: input.name,
|
|
8734
9653
|
ordinal,
|
|
8735
9654
|
message: input.message,
|
|
9655
|
+
delivery: input.delivery ?? "steer",
|
|
8736
9656
|
state: "uncertain",
|
|
8737
9657
|
acceptedAt
|
|
8738
9658
|
});
|
|
@@ -8758,21 +9678,46 @@ var FleetService = class {
|
|
|
8758
9678
|
incarnationId,
|
|
8759
9679
|
this.#now,
|
|
8760
9680
|
() => {
|
|
8761
|
-
|
|
8762
|
-
this.#watchHub.endIncarnation(
|
|
8763
|
-
agent.summary.name,
|
|
8764
|
-
incarnationId,
|
|
8765
|
-
interrupted ? new RpcWatchError("runtime_interrupted", "Pi process was interrupted") : null
|
|
8766
|
-
);
|
|
9681
|
+
this.#finishJournalSink(incarnationId);
|
|
8767
9682
|
if (this.#coordinators.get(agent.summary.name) === coordinator) {
|
|
8768
9683
|
this.#coordinators.delete(agent.summary.name);
|
|
8769
9684
|
this.#releaseProcessSlot(agent.summary.name);
|
|
8770
9685
|
}
|
|
8771
|
-
}
|
|
9686
|
+
},
|
|
9687
|
+
async (idleAgent) => await this.#journal?.markIdle(idleAgent.summary.id) ?? 0
|
|
8772
9688
|
);
|
|
8773
9689
|
this.#coordinators.set(agent.summary.name, coordinator);
|
|
8774
9690
|
return coordinator;
|
|
8775
9691
|
}
|
|
9692
|
+
async #openJournalSink(agent, incarnationId) {
|
|
9693
|
+
if (this.#journal === void 0 || this.#journalStore === void 0) return void 0;
|
|
9694
|
+
const agentId = agent.summary.id;
|
|
9695
|
+
const epochs = await this.#journalStore.getEpochs(agentId);
|
|
9696
|
+
const last = epochs.at(-1);
|
|
9697
|
+
const epoch = last?.state === "open" ? last.epoch : (last?.epoch ?? -1) + 1;
|
|
9698
|
+
if (last?.state !== "open") {
|
|
9699
|
+
await this.#journalStore.putEpoch({
|
|
9700
|
+
agentId,
|
|
9701
|
+
epoch,
|
|
9702
|
+
state: "open",
|
|
9703
|
+
lastSafeEventPosition: (await this.#journalStore.getHighWater(agentId))?.eventPosition ?? 0,
|
|
9704
|
+
openedAt: this.#now()
|
|
9705
|
+
});
|
|
9706
|
+
}
|
|
9707
|
+
const sink = await this.#journal.openIncarnation({
|
|
9708
|
+
agentId,
|
|
9709
|
+
incarnationId,
|
|
9710
|
+
epoch
|
|
9711
|
+
});
|
|
9712
|
+
this.#journalSinks.set(incarnationId, sink);
|
|
9713
|
+
return sink;
|
|
9714
|
+
}
|
|
9715
|
+
#finishJournalSink(incarnationId) {
|
|
9716
|
+
const sink = this.#journalSinks.get(incarnationId);
|
|
9717
|
+
if (sink === void 0) return;
|
|
9718
|
+
this.#journalSinks.delete(incarnationId);
|
|
9719
|
+
sink.finish();
|
|
9720
|
+
}
|
|
8776
9721
|
#enqueueAgent(name, operation) {
|
|
8777
9722
|
return enqueueNamed(this.#agentLanes, name, operation);
|
|
8778
9723
|
}
|
|
@@ -8794,7 +9739,7 @@ var FleetService = class {
|
|
|
8794
9739
|
while (Date.now() < deadline) {
|
|
8795
9740
|
const coordinator = this.#coordinators.get(name);
|
|
8796
9741
|
if (coordinator !== void 0) return coordinator;
|
|
8797
|
-
await new Promise((
|
|
9742
|
+
await new Promise((resolve3) => setTimeout(resolve3, 10));
|
|
8798
9743
|
}
|
|
8799
9744
|
return this.#coordinators.get(name);
|
|
8800
9745
|
}
|
|
@@ -8807,30 +9752,38 @@ var FleetService = class {
|
|
|
8807
9752
|
#releaseProcessSlot(name) {
|
|
8808
9753
|
this.#processSlots.delete(name);
|
|
8809
9754
|
}
|
|
8810
|
-
#
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
9755
|
+
#staleTarget(expectedAgentId, agent) {
|
|
9756
|
+
if (expectedAgentId === void 0 || expectedAgentId === agent.summary.id) return null;
|
|
9757
|
+
return err({
|
|
9758
|
+
code: "stale_agent",
|
|
9759
|
+
message: `Agent ${agent.summary.name} was recreated; this operation targets an older generation.`
|
|
9760
|
+
});
|
|
8815
9761
|
}
|
|
8816
9762
|
#runtimeUnavailable() {
|
|
8817
9763
|
return err({ code: "runtime_unavailable", message: "pi-fleet runtime is shutting down." });
|
|
8818
9764
|
}
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
compactId: operationId,
|
|
8822
|
-
agentName: input.name,
|
|
8823
|
-
state: "failed",
|
|
8824
|
-
requestedAt,
|
|
8825
|
-
error: failure
|
|
8826
|
-
});
|
|
8827
|
-
const result = err(failure);
|
|
8828
|
-
await this.#remember(operationId, "compact", input, result);
|
|
8829
|
-
return result;
|
|
9765
|
+
#notFound(name) {
|
|
9766
|
+
return err({ code: "agent_not_found", message: `Agent ${name} was not found.` });
|
|
8830
9767
|
}
|
|
8831
|
-
#
|
|
8832
|
-
|
|
8833
|
-
|
|
9768
|
+
async #rememberCompactFailure(operationId, input, requestedAt, failure) {
|
|
9769
|
+
const agent = await this.store.getAgent(input.name);
|
|
9770
|
+
if (agent !== null) {
|
|
9771
|
+
await this.store.putCompact({
|
|
9772
|
+
compactId: operationId,
|
|
9773
|
+
agentId: agent.summary.id,
|
|
9774
|
+
agentName: input.name,
|
|
9775
|
+
state: "failed",
|
|
9776
|
+
requestedAt,
|
|
9777
|
+
error: failure
|
|
9778
|
+
});
|
|
9779
|
+
}
|
|
9780
|
+
const result = err(failure);
|
|
9781
|
+
await this.#remember(operationId, "compact", input, result);
|
|
9782
|
+
return result;
|
|
9783
|
+
}
|
|
9784
|
+
#piIdentityFailure(callerIdentity) {
|
|
9785
|
+
if (callerIdentity === void 0 || samePiRuntimeIdentity(callerIdentity, this.#piIdentity)) {
|
|
9786
|
+
return null;
|
|
8834
9787
|
}
|
|
8835
9788
|
return {
|
|
8836
9789
|
code: "pi_runtime_mismatch",
|
|
@@ -8860,7 +9813,7 @@ var FleetService = class {
|
|
|
8860
9813
|
return result;
|
|
8861
9814
|
}
|
|
8862
9815
|
#runOperation(operationId, method, payload, operation) {
|
|
8863
|
-
const fingerprint =
|
|
9816
|
+
const fingerprint = fingerprintPayload(payload);
|
|
8864
9817
|
const inflight = this.#inflightOperations.get(operationId);
|
|
8865
9818
|
if (inflight !== void 0) {
|
|
8866
9819
|
if (inflight.method !== method || inflight.fingerprint !== fingerprint) {
|
|
@@ -8894,7 +9847,7 @@ var FleetService = class {
|
|
|
8894
9847
|
result: stored.result
|
|
8895
9848
|
} : void 0);
|
|
8896
9849
|
if (recorded === void 0) {
|
|
8897
|
-
const fingerprint =
|
|
9850
|
+
const fingerprint = fingerprintPayload(payload);
|
|
8898
9851
|
if (stored !== null) {
|
|
8899
9852
|
if (stored.method !== method || stored.fingerprint !== fingerprint) {
|
|
8900
9853
|
return err({
|
|
@@ -8905,8 +9858,22 @@ var FleetService = class {
|
|
|
8905
9858
|
const name = "name" in payload ? String(payload.name) : "";
|
|
8906
9859
|
const agent = name.length === 0 ? null : await this.store.getAgent(name);
|
|
8907
9860
|
if (method === "create") {
|
|
8908
|
-
if (
|
|
8909
|
-
|
|
9861
|
+
if (stored.targetAgent === void 0) {
|
|
9862
|
+
if (agent === null) return null;
|
|
9863
|
+
const result = err({
|
|
9864
|
+
code: "name_taken",
|
|
9865
|
+
message: `Agent ${name} already exists.`
|
|
9866
|
+
});
|
|
9867
|
+
await this.#remember(operationId, method, payload, result);
|
|
9868
|
+
return result;
|
|
9869
|
+
}
|
|
9870
|
+
if (agent === null || agent.summary.id !== stored.targetAgent.id) {
|
|
9871
|
+
const result = err({
|
|
9872
|
+
code: "stale_agent",
|
|
9873
|
+
message: `Creation operation ${operationId} targets an unavailable agent generation.`
|
|
9874
|
+
});
|
|
9875
|
+
await this.#remember(operationId, method, payload, result);
|
|
9876
|
+
return result;
|
|
8910
9877
|
} else if (agent.summary.state === "idle" || agent.summary.state === "working") {
|
|
8911
9878
|
const result = ok({
|
|
8912
9879
|
schemaVersion: 1,
|
|
@@ -8917,7 +9884,7 @@ var FleetService = class {
|
|
|
8917
9884
|
return result;
|
|
8918
9885
|
} else if (agent.summary.state === "failed") {
|
|
8919
9886
|
const result = err({
|
|
8920
|
-
code: agent.summary.error?.code
|
|
9887
|
+
code: publicAgentFailureCode(agent.summary.error?.code, "pi_start_failed"),
|
|
8921
9888
|
message: `Creation of ${name} did not complete safely.`
|
|
8922
9889
|
});
|
|
8923
9890
|
await this.#remember(operationId, method, payload, result);
|
|
@@ -8929,11 +9896,19 @@ var FleetService = class {
|
|
|
8929
9896
|
});
|
|
8930
9897
|
}
|
|
8931
9898
|
} else if (method === "destroy") {
|
|
8932
|
-
if (
|
|
9899
|
+
if (stored.targetAgent === void 0) {
|
|
9900
|
+
const result = this.#notFound(name);
|
|
9901
|
+
await this.#remember(operationId, method, payload, result);
|
|
9902
|
+
return result;
|
|
9903
|
+
}
|
|
9904
|
+
if (agent === null) {
|
|
8933
9905
|
const result = ok({
|
|
8934
9906
|
schemaVersion: 1,
|
|
8935
9907
|
type: "agent.destroyed",
|
|
8936
|
-
agent:
|
|
9908
|
+
agent: {
|
|
9909
|
+
id: stored.targetAgent.id,
|
|
9910
|
+
name: stored.targetAgent.name || String("name" in payload ? payload.name : "")
|
|
9911
|
+
}
|
|
8937
9912
|
});
|
|
8938
9913
|
await this.#remember(operationId, method, payload, result);
|
|
8939
9914
|
return result;
|
|
@@ -8941,14 +9916,21 @@ var FleetService = class {
|
|
|
8941
9916
|
return null;
|
|
8942
9917
|
} else if (method === "send") {
|
|
8943
9918
|
const send = await this.store.getSend(operationId);
|
|
9919
|
+
if (stored.targetAgent === void 0) {
|
|
9920
|
+
const result = this.#notFound(name);
|
|
9921
|
+
await this.#remember(operationId, method, payload, result);
|
|
9922
|
+
return result;
|
|
9923
|
+
}
|
|
8944
9924
|
if (send === null) {
|
|
8945
9925
|
await this.store.deleteOperation(operationId);
|
|
8946
9926
|
await this.store.putOperation({
|
|
8947
9927
|
operationId,
|
|
8948
9928
|
method,
|
|
8949
9929
|
fingerprint,
|
|
9930
|
+
targetName: String("name" in payload ? payload.name : ""),
|
|
8950
9931
|
state: "pending",
|
|
8951
|
-
result: null
|
|
9932
|
+
result: null,
|
|
9933
|
+
request: payload
|
|
8952
9934
|
});
|
|
8953
9935
|
return null;
|
|
8954
9936
|
}
|
|
@@ -8958,6 +9940,11 @@ var FleetService = class {
|
|
|
8958
9940
|
});
|
|
8959
9941
|
} else {
|
|
8960
9942
|
const compact = await this.store.getCompact(operationId);
|
|
9943
|
+
if (stored.targetAgent === void 0) {
|
|
9944
|
+
const result = this.#notFound(name);
|
|
9945
|
+
await this.#remember(operationId, method, payload, result);
|
|
9946
|
+
return result;
|
|
9947
|
+
}
|
|
8961
9948
|
if (compact?.state === "completed" && compact.result !== void 0) {
|
|
8962
9949
|
const target = stored.targetAgent;
|
|
8963
9950
|
if (target === void 0) {
|
|
@@ -9001,12 +9988,14 @@ var FleetService = class {
|
|
|
9001
9988
|
operationId,
|
|
9002
9989
|
method,
|
|
9003
9990
|
fingerprint,
|
|
9991
|
+
targetName: String("name" in payload ? payload.name : ""),
|
|
9004
9992
|
state: "pending",
|
|
9005
|
-
result: null
|
|
9993
|
+
result: null,
|
|
9994
|
+
request: payload
|
|
9006
9995
|
});
|
|
9007
9996
|
return null;
|
|
9008
9997
|
}
|
|
9009
|
-
if (recorded.method !== method || recorded.fingerprint !==
|
|
9998
|
+
if (recorded.method !== method || recorded.fingerprint !== fingerprintPayload(payload)) {
|
|
9010
9999
|
return err({
|
|
9011
10000
|
code: "operation_conflict",
|
|
9012
10001
|
message: `Operation ${operationId} was already used with a different request.`
|
|
@@ -9014,26 +10003,63 @@ var FleetService = class {
|
|
|
9014
10003
|
}
|
|
9015
10004
|
return recorded.result;
|
|
9016
10005
|
}
|
|
10006
|
+
async #rollbackProvisionalCreate(agent, operationId, result) {
|
|
10007
|
+
const operation = await this.store.getOperation(operationId);
|
|
10008
|
+
if (operation === null || operation.method !== "create" || operation.state !== "pending" || operation.targetAgent?.id !== agent.summary.id) {
|
|
10009
|
+
throw new Error("Provisional create operation receipt is missing or inconsistent");
|
|
10010
|
+
}
|
|
10011
|
+
await this.store.rollbackProvisionalCreate(agent.summary.name, {
|
|
10012
|
+
operationId: operation.operationId,
|
|
10013
|
+
method: operation.method,
|
|
10014
|
+
fingerprint: operation.fingerprint,
|
|
10015
|
+
state: "completed",
|
|
10016
|
+
result,
|
|
10017
|
+
targetName: agent.summary.name
|
|
10018
|
+
});
|
|
10019
|
+
}
|
|
9017
10020
|
async #recordOperationTarget(operationId, agent) {
|
|
9018
10021
|
const operation = await this.store.getOperation(operationId);
|
|
9019
10022
|
if (operation === null || operation.state !== "pending") return;
|
|
9020
10023
|
await this.store.putOperation({
|
|
9021
10024
|
...operation,
|
|
10025
|
+
targetName: agent.summary.name,
|
|
9022
10026
|
targetAgent: { id: agent.summary.id, name: agent.summary.name }
|
|
9023
10027
|
});
|
|
9024
10028
|
}
|
|
9025
10029
|
async #remember(operationId, method, payload, result) {
|
|
9026
|
-
const fingerprint =
|
|
10030
|
+
const fingerprint = fingerprintPayload(payload);
|
|
9027
10031
|
this.#operations.set(operationId, { method, fingerprint, result });
|
|
10032
|
+
const existing = await this.store.getOperation(operationId);
|
|
9028
10033
|
await this.store.putOperation({
|
|
9029
10034
|
operationId,
|
|
9030
10035
|
method,
|
|
9031
10036
|
fingerprint,
|
|
10037
|
+
targetName: existing?.targetName ?? String("name" in payload ? payload.name : ""),
|
|
9032
10038
|
state: "completed",
|
|
9033
|
-
result
|
|
10039
|
+
result,
|
|
10040
|
+
...existing?.targetAgent === void 0 ? {} : { targetAgent: existing.targetAgent }
|
|
9034
10041
|
});
|
|
9035
10042
|
}
|
|
9036
10043
|
};
|
|
10044
|
+
function isCreateRequest(request) {
|
|
10045
|
+
return typeof request === "object" && request !== null && "name" in request && typeof request.name === "string";
|
|
10046
|
+
}
|
|
10047
|
+
function publicAgentFailureCode(code, fallback = "runtime_interrupted") {
|
|
10048
|
+
return isPiFleetErrorCode(code) ? code : fallback;
|
|
10049
|
+
}
|
|
10050
|
+
function fingerprintPayload(payload) {
|
|
10051
|
+
const durable = { ...payload };
|
|
10052
|
+
delete durable.expectedAgentId;
|
|
10053
|
+
const canonical = JSON.stringify(canonicalizeFingerprintValue(durable));
|
|
10054
|
+
return `sha256:${createHash2("sha256").update(canonical).digest("hex")}`;
|
|
10055
|
+
}
|
|
10056
|
+
function canonicalizeFingerprintValue(value) {
|
|
10057
|
+
if (Array.isArray(value)) return value.map((item) => canonicalizeFingerprintValue(item));
|
|
10058
|
+
if (value === null || typeof value !== "object") return value;
|
|
10059
|
+
return Object.fromEntries(
|
|
10060
|
+
Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonicalizeFingerprintValue(item)])
|
|
10061
|
+
);
|
|
10062
|
+
}
|
|
9037
10063
|
function enqueueNamed(lanes, name, operation) {
|
|
9038
10064
|
const previous = lanes.get(name) ?? Promise.resolve();
|
|
9039
10065
|
const result = previous.then(operation, operation);
|
|
@@ -9048,38 +10074,1260 @@ function enqueueNamed(lanes, name, operation) {
|
|
|
9048
10074
|
return result;
|
|
9049
10075
|
}
|
|
9050
10076
|
|
|
9051
|
-
// src/
|
|
10077
|
+
// src/runtime/journal-ingestion.ts
|
|
10078
|
+
var JournalIngestionScheduler = class {
|
|
10079
|
+
#limits;
|
|
10080
|
+
#commit;
|
|
10081
|
+
#pauseAgent;
|
|
10082
|
+
#resumeAgent;
|
|
10083
|
+
#failAgentCallback;
|
|
10084
|
+
#now;
|
|
10085
|
+
#queues = /* @__PURE__ */ new Map();
|
|
10086
|
+
#pendingBytesByAgent = /* @__PURE__ */ new Map();
|
|
10087
|
+
#pausedAgents = /* @__PURE__ */ new Set();
|
|
10088
|
+
#failedAgents = /* @__PURE__ */ new Map();
|
|
10089
|
+
#pendingRecords = 0;
|
|
10090
|
+
#pendingBytes = 0;
|
|
10091
|
+
#roundRobinOffset = 0;
|
|
10092
|
+
#timer = null;
|
|
10093
|
+
#flushPromise = null;
|
|
10094
|
+
#activeOldestEnqueuedAt = null;
|
|
10095
|
+
#closePromise = null;
|
|
10096
|
+
#closed = false;
|
|
10097
|
+
constructor(options) {
|
|
10098
|
+
validateLimits(options.limits);
|
|
10099
|
+
this.#limits = options.limits;
|
|
10100
|
+
this.#commit = options.commit;
|
|
10101
|
+
this.#pauseAgent = options.pauseAgent ?? (() => void 0);
|
|
10102
|
+
this.#resumeAgent = options.resumeAgent ?? (() => void 0);
|
|
10103
|
+
this.#failAgentCallback = options.failAgent ?? (() => void 0);
|
|
10104
|
+
this.#now = options.now ?? Date.now;
|
|
10105
|
+
}
|
|
10106
|
+
get pendingRecords() {
|
|
10107
|
+
return this.#pendingRecords;
|
|
10108
|
+
}
|
|
10109
|
+
get pendingBytes() {
|
|
10110
|
+
return this.#pendingBytes;
|
|
10111
|
+
}
|
|
10112
|
+
get oldestPendingAgeMs() {
|
|
10113
|
+
let oldest = this.#activeOldestEnqueuedAt ?? Number.POSITIVE_INFINITY;
|
|
10114
|
+
for (const queue of this.#queues.values()) {
|
|
10115
|
+
for (const pending of queue) oldest = Math.min(oldest, pending.enqueuedAt);
|
|
10116
|
+
}
|
|
10117
|
+
return oldest === Number.POSITIVE_INFINITY ? 0 : Math.max(0, this.#now() - oldest);
|
|
10118
|
+
}
|
|
10119
|
+
get pausedAgentCount() {
|
|
10120
|
+
return this.#pausedAgents.size;
|
|
10121
|
+
}
|
|
10122
|
+
get failedAgentCount() {
|
|
10123
|
+
return this.#failedAgents.size;
|
|
10124
|
+
}
|
|
10125
|
+
enqueue(record) {
|
|
10126
|
+
if (this.#closed) return Promise.reject(new Error("Journal ingestion is closed"));
|
|
10127
|
+
const priorFailure = this.#failedAgents.get(record.agentId);
|
|
10128
|
+
if (priorFailure !== void 0) return Promise.reject(priorFailure);
|
|
10129
|
+
const ownedRecord = { ...record, bytes: Buffer.from(record.bytes) };
|
|
10130
|
+
const agentBytes = this.#pendingBytesByAgent.get(record.agentId) ?? 0;
|
|
10131
|
+
if (ownedRecord.bytes.length > this.#limits.maxPendingBytesPerAgent || agentBytes + ownedRecord.bytes.length > this.#limits.maxPendingBytesPerAgent || this.#pendingRecords + 1 > this.#limits.maxPendingRecords || this.#pendingBytes + ownedRecord.bytes.length > this.#limits.maxPendingBytes) {
|
|
10132
|
+
const error = new Error("Journal ingestion capacity exceeded");
|
|
10133
|
+
this.#failAgent(record.agentId, error);
|
|
10134
|
+
return Promise.reject(error);
|
|
10135
|
+
}
|
|
10136
|
+
const completion = new Promise((resolve3, reject) => {
|
|
10137
|
+
const queue = this.#queues.get(record.agentId) ?? [];
|
|
10138
|
+
queue.push({ record: ownedRecord, enqueuedAt: this.#now(), resolve: resolve3, reject });
|
|
10139
|
+
this.#queues.set(record.agentId, queue);
|
|
10140
|
+
});
|
|
10141
|
+
this.#pendingRecords += 1;
|
|
10142
|
+
this.#pendingBytes += ownedRecord.bytes.length;
|
|
10143
|
+
const nextAgentBytes = agentBytes + ownedRecord.bytes.length;
|
|
10144
|
+
this.#pendingBytesByAgent.set(record.agentId, nextAgentBytes);
|
|
10145
|
+
this.#refreshPauses();
|
|
10146
|
+
this.#scheduleFlush();
|
|
10147
|
+
return completion;
|
|
10148
|
+
}
|
|
10149
|
+
async drain() {
|
|
10150
|
+
this.#clearTimer();
|
|
10151
|
+
while (this.#pendingRecords > 0) {
|
|
10152
|
+
await this.#flush();
|
|
10153
|
+
}
|
|
10154
|
+
}
|
|
10155
|
+
close() {
|
|
10156
|
+
if (this.#closePromise !== null) return this.#closePromise;
|
|
10157
|
+
this.#closed = true;
|
|
10158
|
+
this.#closePromise = this.drain();
|
|
10159
|
+
return this.#closePromise;
|
|
10160
|
+
}
|
|
10161
|
+
#scheduleFlush() {
|
|
10162
|
+
if (this.#flushPromise !== null) return;
|
|
10163
|
+
if (this.#pendingRecords >= this.#limits.maxBatchRecords || this.#pendingBytes >= this.#limits.maxBatchBytes) {
|
|
10164
|
+
queueMicrotask(() => void this.#flush());
|
|
10165
|
+
return;
|
|
10166
|
+
}
|
|
10167
|
+
if (this.#timer === null) {
|
|
10168
|
+
this.#timer = setTimeout(() => {
|
|
10169
|
+
this.#timer = null;
|
|
10170
|
+
void this.#flush();
|
|
10171
|
+
}, this.#limits.maxBatchAgeMs);
|
|
10172
|
+
}
|
|
10173
|
+
}
|
|
10174
|
+
#flush() {
|
|
10175
|
+
if (this.#flushPromise !== null) return this.#flushPromise;
|
|
10176
|
+
this.#clearTimer();
|
|
10177
|
+
const batch = this.#takeBatch();
|
|
10178
|
+
if (batch.length === 0) return Promise.resolve();
|
|
10179
|
+
this.#activeOldestEnqueuedAt = Math.min(...batch.map((pending) => pending.enqueuedAt));
|
|
10180
|
+
const flushing = Promise.resolve().then(() => this.#commit(batch.map((pending) => pending.record))).then(() => {
|
|
10181
|
+
for (const pending of batch) this.#settle(pending, null);
|
|
10182
|
+
}).catch((cause) => {
|
|
10183
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
10184
|
+
const agents = new Set(batch.map((pending) => pending.record.agentId));
|
|
10185
|
+
for (const pending of batch) this.#settle(pending, error, false);
|
|
10186
|
+
for (const agentId of agents) this.#failAgent(agentId, error);
|
|
10187
|
+
}).finally(() => {
|
|
10188
|
+
this.#flushPromise = null;
|
|
10189
|
+
this.#activeOldestEnqueuedAt = null;
|
|
10190
|
+
if (this.#pendingRecords > 0) queueMicrotask(() => void this.#flush());
|
|
10191
|
+
});
|
|
10192
|
+
this.#flushPromise = flushing;
|
|
10193
|
+
return flushing;
|
|
10194
|
+
}
|
|
10195
|
+
#takeBatch() {
|
|
10196
|
+
const agentIds = [...this.#queues.keys()].filter(
|
|
10197
|
+
(agentId2) => (this.#queues.get(agentId2)?.length ?? 0) > 0
|
|
10198
|
+
);
|
|
10199
|
+
if (agentIds.length === 0) return [];
|
|
10200
|
+
const index = this.#roundRobinOffset % agentIds.length;
|
|
10201
|
+
const agentId = agentIds[index];
|
|
10202
|
+
this.#roundRobinOffset = (index + 1) % agentIds.length;
|
|
10203
|
+
const queue = this.#queues.get(agentId);
|
|
10204
|
+
const batchKey = queue[0]?.record.batchKey ?? agentId;
|
|
10205
|
+
const batch = [];
|
|
10206
|
+
let batchBytes = 0;
|
|
10207
|
+
while (batch.length < this.#limits.maxBatchRecords && queue.length > 0) {
|
|
10208
|
+
const next = queue[0];
|
|
10209
|
+
if ((next.record.batchKey ?? agentId) !== batchKey) break;
|
|
10210
|
+
if (batch.length > 0 && batchBytes + next.record.bytes.length > this.#limits.maxBatchBytes) {
|
|
10211
|
+
break;
|
|
10212
|
+
}
|
|
10213
|
+
queue.shift();
|
|
10214
|
+
batch.push(next);
|
|
10215
|
+
batchBytes += next.record.bytes.length;
|
|
10216
|
+
}
|
|
10217
|
+
return batch;
|
|
10218
|
+
}
|
|
10219
|
+
#settle(pending, error, refreshPauses = true) {
|
|
10220
|
+
const { agentId, bytes } = pending.record;
|
|
10221
|
+
this.#pendingRecords -= 1;
|
|
10222
|
+
this.#pendingBytes -= bytes.length;
|
|
10223
|
+
const agentBytes = (this.#pendingBytesByAgent.get(agentId) ?? bytes.length) - bytes.length;
|
|
10224
|
+
if (agentBytes <= 0) this.#pendingBytesByAgent.delete(agentId);
|
|
10225
|
+
else this.#pendingBytesByAgent.set(agentId, agentBytes);
|
|
10226
|
+
if (refreshPauses) this.#refreshPauses();
|
|
10227
|
+
if (error === null) pending.resolve();
|
|
10228
|
+
else pending.reject(error);
|
|
10229
|
+
}
|
|
10230
|
+
#failAgent(agentId, error) {
|
|
10231
|
+
if (this.#failedAgents.has(agentId)) return;
|
|
10232
|
+
this.#failedAgents.set(agentId, error);
|
|
10233
|
+
const queued = this.#queues.get(agentId) ?? [];
|
|
10234
|
+
this.#queues.delete(agentId);
|
|
10235
|
+
for (const pending of queued) this.#settle(pending, error);
|
|
10236
|
+
this.#refreshPauses();
|
|
10237
|
+
this.#failAgentCallback(agentId, error);
|
|
10238
|
+
}
|
|
10239
|
+
#refreshPauses() {
|
|
10240
|
+
const globalHigh = this.#pendingBytes >= Math.max(1, Math.floor(this.#limits.maxPendingBytes * 0.75)) || this.#pendingRecords >= Math.max(1, Math.floor(this.#limits.maxPendingRecords * 0.75));
|
|
10241
|
+
const globalLow = this.#pendingBytes <= Math.floor(this.#limits.maxPendingBytes * 0.5) && this.#pendingRecords <= Math.floor(this.#limits.maxPendingRecords * 0.5);
|
|
10242
|
+
const candidates = /* @__PURE__ */ new Set([...this.#pendingBytesByAgent.keys(), ...this.#pausedAgents]);
|
|
10243
|
+
for (const agentId of candidates) {
|
|
10244
|
+
if (this.#failedAgents.has(agentId)) {
|
|
10245
|
+
this.#pausedAgents.delete(agentId);
|
|
10246
|
+
continue;
|
|
10247
|
+
}
|
|
10248
|
+
const agentBytes = this.#pendingBytesByAgent.get(agentId) ?? 0;
|
|
10249
|
+
const agentHigh = agentBytes >= Math.max(1, Math.floor(this.#limits.maxPendingBytesPerAgent * 0.75));
|
|
10250
|
+
const agentLow = agentBytes <= Math.floor(this.#limits.maxPendingBytesPerAgent * 0.5);
|
|
10251
|
+
if ((globalHigh || agentHigh) && !this.#pausedAgents.has(agentId)) {
|
|
10252
|
+
this.#pausedAgents.add(agentId);
|
|
10253
|
+
this.#pauseAgent(agentId);
|
|
10254
|
+
} else if (globalLow && agentLow && this.#pausedAgents.delete(agentId)) {
|
|
10255
|
+
this.#resumeAgent(agentId);
|
|
10256
|
+
}
|
|
10257
|
+
}
|
|
10258
|
+
}
|
|
10259
|
+
#clearTimer() {
|
|
10260
|
+
if (this.#timer === null) return;
|
|
10261
|
+
clearTimeout(this.#timer);
|
|
10262
|
+
this.#timer = null;
|
|
10263
|
+
}
|
|
10264
|
+
};
|
|
10265
|
+
function validateLimits(limits) {
|
|
10266
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
10267
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
10268
|
+
throw new Error(`${name} must be a positive safe integer`);
|
|
10269
|
+
}
|
|
10270
|
+
}
|
|
10271
|
+
}
|
|
10272
|
+
|
|
10273
|
+
// src/runtime/lifecycle-projector.ts
|
|
10274
|
+
function initialProjectorState() {
|
|
10275
|
+
return { version: 1, messageSequence: 0, finishedThinkingIndexes: [], openActivities: [] };
|
|
10276
|
+
}
|
|
10277
|
+
function projectLifecycleRecord(state, input, maxOpenActivities) {
|
|
10278
|
+
if (!Number.isSafeInteger(maxOpenActivities) || maxOpenActivities <= 0) {
|
|
10279
|
+
throw new Error("maxOpenActivities must be a positive safe integer");
|
|
10280
|
+
}
|
|
10281
|
+
const frame = asObject(input.frame);
|
|
10282
|
+
if (frame === null) return { events: [], state };
|
|
10283
|
+
if (frame.type === "message_update") {
|
|
10284
|
+
return projectMessageUpdate(state, input, frame, maxOpenActivities);
|
|
10285
|
+
}
|
|
10286
|
+
if (frame.type === "message_end") return projectMessageEnd(state, input, frame);
|
|
10287
|
+
if (frame.type === "tool_execution_start") {
|
|
10288
|
+
return projectToolStart(state, input, frame, maxOpenActivities);
|
|
10289
|
+
}
|
|
10290
|
+
if (frame.type === "tool_execution_end") return projectToolEnd(state, input, frame);
|
|
10291
|
+
return { events: [], state };
|
|
10292
|
+
}
|
|
10293
|
+
function projectMessageUpdate(state, input, frame, maxOpen) {
|
|
10294
|
+
const update = asObject(frame.assistantMessageEvent ?? frame.event);
|
|
10295
|
+
if (update === null) return { events: [], state };
|
|
10296
|
+
const contentIndex = integer(update.contentIndex);
|
|
10297
|
+
if (contentIndex === null) return { events: [], state };
|
|
10298
|
+
if (update.type === "thinking_delta") {
|
|
10299
|
+
return projectThinkingDelta(state, input, contentIndex, text(update.delta), maxOpen);
|
|
10300
|
+
}
|
|
10301
|
+
if (update.type === "thinking_end") {
|
|
10302
|
+
return projectThinkingEnd(state, input, contentIndex, text(update.content), maxOpen);
|
|
10303
|
+
}
|
|
10304
|
+
if (update.type === "text_delta") {
|
|
10305
|
+
return projectTextStart(state, input, text(update.delta), maxOpen);
|
|
10306
|
+
}
|
|
10307
|
+
if (update.type === "text_end") {
|
|
10308
|
+
return projectTextStart(state, input, text(update.content), maxOpen);
|
|
10309
|
+
}
|
|
10310
|
+
return { events: [], state };
|
|
10311
|
+
}
|
|
10312
|
+
function projectThinkingDelta(state, input, contentIndex, delta, maxOpen) {
|
|
10313
|
+
if (delta === null) return { events: [], state };
|
|
10314
|
+
if (state.finishedThinkingIndexes.includes(contentIndex)) return { events: [], state };
|
|
10315
|
+
const existing = findThinking(state, contentIndex);
|
|
10316
|
+
if (existing !== void 0) {
|
|
10317
|
+
return {
|
|
10318
|
+
events: [],
|
|
10319
|
+
state: replaceActivity(state, existing, { ...existing, text: existing.text + delta })
|
|
10320
|
+
};
|
|
10321
|
+
}
|
|
10322
|
+
if (!meaningful(delta)) return { events: [], state };
|
|
10323
|
+
const activityId = activityIdFor(input, `thinking:${state.messageSequence}:${contentIndex}`);
|
|
10324
|
+
const activity = {
|
|
10325
|
+
kind: "thinking",
|
|
10326
|
+
activityId,
|
|
10327
|
+
messageSequence: state.messageSequence,
|
|
10328
|
+
contentIndex,
|
|
10329
|
+
text: delta
|
|
10330
|
+
};
|
|
10331
|
+
const next = addActivity(state, activity, maxOpen);
|
|
10332
|
+
return { events: [event(input, activityId, 0, "assistant.thinking.started")], state: next };
|
|
10333
|
+
}
|
|
10334
|
+
function projectThinkingEnd(state, input, contentIndex, completeText, maxOpen) {
|
|
10335
|
+
if (state.finishedThinkingIndexes.includes(contentIndex)) return { events: [], state };
|
|
10336
|
+
const existing = findThinking(state, contentIndex);
|
|
10337
|
+
if (!meaningful(completeText)) {
|
|
10338
|
+
if (existing !== void 0) {
|
|
10339
|
+
throw new Error("Lifecycle projector finalized thinking contradicts its started activity");
|
|
10340
|
+
}
|
|
10341
|
+
return { events: [], state: markThinkingFinished(state, contentIndex, maxOpen) };
|
|
10342
|
+
}
|
|
10343
|
+
const activityId = existing?.activityId ?? activityIdFor(input, `thinking:${state.messageSequence}:${contentIndex}`);
|
|
10344
|
+
const started = existing === void 0 ? [event(input, activityId, 0, "assistant.thinking.started")] : [];
|
|
10345
|
+
const finished = event(input, activityId, started.length, "assistant.thinking.finished", {
|
|
10346
|
+
text: completeText
|
|
10347
|
+
});
|
|
10348
|
+
return {
|
|
10349
|
+
events: [...started, finished],
|
|
10350
|
+
state: finishThinking(state, activityId, contentIndex, maxOpen)
|
|
10351
|
+
};
|
|
10352
|
+
}
|
|
10353
|
+
function projectTextStart(state, input, content, maxOpen) {
|
|
10354
|
+
if (!meaningful(content) || findMessage(state) !== void 0) return { events: [], state };
|
|
10355
|
+
const activityId = activityIdFor(input, `message:${state.messageSequence}`);
|
|
10356
|
+
const activity = {
|
|
10357
|
+
kind: "message",
|
|
10358
|
+
activityId,
|
|
10359
|
+
messageSequence: state.messageSequence
|
|
10360
|
+
};
|
|
10361
|
+
return {
|
|
10362
|
+
events: [event(input, activityId, 0, "assistant.message.started")],
|
|
10363
|
+
state: addActivity(state, activity, maxOpen)
|
|
10364
|
+
};
|
|
10365
|
+
}
|
|
10366
|
+
function projectMessageEnd(state, input, frame) {
|
|
10367
|
+
const completeText = assistantText(frame.message ?? asObject(frame.data)?.message);
|
|
10368
|
+
const existing = findMessage(state);
|
|
10369
|
+
const unfinishedThinking = state.openActivities.find(
|
|
10370
|
+
(activity) => activity.kind === "thinking" && activity.messageSequence === state.messageSequence
|
|
10371
|
+
);
|
|
10372
|
+
if (unfinishedThinking !== void 0) {
|
|
10373
|
+
throw new Error("Lifecycle projector completed message omits a finished thinking activity");
|
|
10374
|
+
}
|
|
10375
|
+
if (existing !== void 0 && !meaningful(completeText)) {
|
|
10376
|
+
throw new Error("Lifecycle projector finalized message contradicts its started activity");
|
|
10377
|
+
}
|
|
10378
|
+
const events = [];
|
|
10379
|
+
let subposition = 0;
|
|
10380
|
+
let activityId = existing?.activityId;
|
|
10381
|
+
if (meaningful(completeText)) {
|
|
10382
|
+
if (activityId === void 0) {
|
|
10383
|
+
activityId = activityIdFor(input, `message:${state.messageSequence}`);
|
|
10384
|
+
events.push(event(input, activityId, subposition++, "assistant.message.started"));
|
|
10385
|
+
}
|
|
10386
|
+
events.push(
|
|
10387
|
+
event(input, activityId, subposition, "assistant.message.finished", { text: completeText })
|
|
10388
|
+
);
|
|
10389
|
+
}
|
|
10390
|
+
return {
|
|
10391
|
+
events,
|
|
10392
|
+
state: {
|
|
10393
|
+
version: 1,
|
|
10394
|
+
messageSequence: state.messageSequence + 1,
|
|
10395
|
+
finishedThinkingIndexes: [],
|
|
10396
|
+
openActivities: state.openActivities.filter(
|
|
10397
|
+
(activity) => activity.kind === "tool" || activity.messageSequence !== state.messageSequence
|
|
10398
|
+
)
|
|
10399
|
+
}
|
|
10400
|
+
};
|
|
10401
|
+
}
|
|
10402
|
+
function projectToolStart(state, input, frame, maxOpen) {
|
|
10403
|
+
const callId = text(frame.toolCallId);
|
|
10404
|
+
const name = text(frame.toolName);
|
|
10405
|
+
if (callId === null || name === null || findTool(state, callId) !== void 0) {
|
|
10406
|
+
return { events: [], state };
|
|
10407
|
+
}
|
|
10408
|
+
const activityId = activityIdFor(input, `tool:${callId}`);
|
|
10409
|
+
const activity = {
|
|
10410
|
+
kind: "tool",
|
|
10411
|
+
activityId,
|
|
10412
|
+
callId,
|
|
10413
|
+
name,
|
|
10414
|
+
input: frame.args
|
|
10415
|
+
};
|
|
10416
|
+
return {
|
|
10417
|
+
events: [
|
|
10418
|
+
event(input, activityId, 0, "tool.execution.started", {
|
|
10419
|
+
tool: { callId, name, input: frame.args }
|
|
10420
|
+
})
|
|
10421
|
+
],
|
|
10422
|
+
state: addActivity(state, activity, maxOpen)
|
|
10423
|
+
};
|
|
10424
|
+
}
|
|
10425
|
+
function projectToolEnd(state, input, frame) {
|
|
10426
|
+
const callId = text(frame.toolCallId);
|
|
10427
|
+
if (callId === null) return { events: [], state };
|
|
10428
|
+
const existing = findTool(state, callId);
|
|
10429
|
+
if (existing === void 0) return { events: [], state };
|
|
10430
|
+
return {
|
|
10431
|
+
events: [
|
|
10432
|
+
event(input, existing.activityId, 0, "tool.execution.finished", {
|
|
10433
|
+
tool: {
|
|
10434
|
+
callId,
|
|
10435
|
+
name: existing.name,
|
|
10436
|
+
input: existing.input,
|
|
10437
|
+
output: frame.result,
|
|
10438
|
+
isError: frame.isError === true
|
|
10439
|
+
}
|
|
10440
|
+
})
|
|
10441
|
+
],
|
|
10442
|
+
state: removeActivity(state, existing.activityId)
|
|
10443
|
+
};
|
|
10444
|
+
}
|
|
10445
|
+
function event(input, activityId, subposition, type, payload = {}) {
|
|
10446
|
+
const id = `${input.agentId}:${input.epoch}:${input.rawPosition}:${subposition}`;
|
|
10447
|
+
return {
|
|
10448
|
+
id,
|
|
10449
|
+
activityId,
|
|
10450
|
+
agentId: input.agentId,
|
|
10451
|
+
cursor: id,
|
|
10452
|
+
epoch: input.epoch,
|
|
10453
|
+
sourceRawPosition: input.rawPosition,
|
|
10454
|
+
observedAt: input.observedAt,
|
|
10455
|
+
type,
|
|
10456
|
+
...payload
|
|
10457
|
+
};
|
|
10458
|
+
}
|
|
10459
|
+
function activityIdFor(input, suffix) {
|
|
10460
|
+
return `${input.agentId}:${input.epoch}:${suffix}:${input.rawPosition}`;
|
|
10461
|
+
}
|
|
10462
|
+
function findThinking(state, contentIndex) {
|
|
10463
|
+
return state.openActivities.find(
|
|
10464
|
+
(activity) => activity.kind === "thinking" && activity.messageSequence === state.messageSequence && activity.contentIndex === contentIndex
|
|
10465
|
+
);
|
|
10466
|
+
}
|
|
10467
|
+
function findMessage(state) {
|
|
10468
|
+
return state.openActivities.find(
|
|
10469
|
+
(activity) => activity.kind === "message" && activity.messageSequence === state.messageSequence
|
|
10470
|
+
);
|
|
10471
|
+
}
|
|
10472
|
+
function findTool(state, callId) {
|
|
10473
|
+
return state.openActivities.find(
|
|
10474
|
+
(activity) => activity.kind === "tool" && activity.callId === callId
|
|
10475
|
+
);
|
|
10476
|
+
}
|
|
10477
|
+
function addActivity(state, activity, maxOpen) {
|
|
10478
|
+
if (state.openActivities.length >= maxOpen) {
|
|
10479
|
+
throw new Error("Lifecycle projector open activity capacity exceeded");
|
|
10480
|
+
}
|
|
10481
|
+
return { ...state, openActivities: [...state.openActivities, activity] };
|
|
10482
|
+
}
|
|
10483
|
+
function replaceActivity(state, current, replacement) {
|
|
10484
|
+
return {
|
|
10485
|
+
...state,
|
|
10486
|
+
openActivities: state.openActivities.map(
|
|
10487
|
+
(activity) => activity.activityId === current.activityId ? replacement : activity
|
|
10488
|
+
)
|
|
10489
|
+
};
|
|
10490
|
+
}
|
|
10491
|
+
function removeActivity(state, activityId) {
|
|
10492
|
+
return {
|
|
10493
|
+
...state,
|
|
10494
|
+
openActivities: state.openActivities.filter((activity) => activity.activityId !== activityId)
|
|
10495
|
+
};
|
|
10496
|
+
}
|
|
10497
|
+
function finishThinking(state, activityId, contentIndex, maxTracked) {
|
|
10498
|
+
return markThinkingFinished(removeActivity(state, activityId), contentIndex, maxTracked);
|
|
10499
|
+
}
|
|
10500
|
+
function markThinkingFinished(state, contentIndex, maxTracked) {
|
|
10501
|
+
if (state.finishedThinkingIndexes.length >= maxTracked) {
|
|
10502
|
+
throw new Error("Lifecycle projector completed thinking capacity exceeded");
|
|
10503
|
+
}
|
|
10504
|
+
return {
|
|
10505
|
+
...state,
|
|
10506
|
+
finishedThinkingIndexes: [...state.finishedThinkingIndexes, contentIndex]
|
|
10507
|
+
};
|
|
10508
|
+
}
|
|
10509
|
+
function meaningful(value) {
|
|
10510
|
+
return value !== null && /\S/u.test(value);
|
|
10511
|
+
}
|
|
10512
|
+
function assistantText(value) {
|
|
10513
|
+
const message = asObject(value);
|
|
10514
|
+
if (message === null || message.role !== "assistant" || !Array.isArray(message.content))
|
|
10515
|
+
return null;
|
|
10516
|
+
return message.content.map((part) => asObject(part)).filter((part) => part?.type === "text").map((part) => text(part.text) ?? "").join("");
|
|
10517
|
+
}
|
|
10518
|
+
function asObject(value) {
|
|
10519
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
10520
|
+
}
|
|
10521
|
+
function text(value) {
|
|
10522
|
+
return typeof value === "string" ? value : null;
|
|
10523
|
+
}
|
|
10524
|
+
function integer(value) {
|
|
10525
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
10526
|
+
}
|
|
10527
|
+
|
|
10528
|
+
// src/runtime/runtime-diagnostics.ts
|
|
10529
|
+
import { stat as stat2 } from "node:fs/promises";
|
|
10530
|
+
async function collectRuntimeJournalDiagnostics(options) {
|
|
10531
|
+
const storage = await options.store.getDiagnostics();
|
|
10532
|
+
const [databaseBytes, walBytes, shmBytes] = await Promise.all([
|
|
10533
|
+
fileBytes(options.databasePath),
|
|
10534
|
+
fileBytes(`${options.databasePath}-wal`),
|
|
10535
|
+
fileBytes(`${options.databasePath}-shm`)
|
|
10536
|
+
]);
|
|
10537
|
+
return {
|
|
10538
|
+
files: { databaseBytes, walBytes, shmBytes },
|
|
10539
|
+
retained: {
|
|
10540
|
+
agentCount: storage.agentCount,
|
|
10541
|
+
rawRecordCount: storage.rawRecordCount,
|
|
10542
|
+
rawBytes: storage.rawBytes,
|
|
10543
|
+
semanticEventCount: storage.semanticEventCount
|
|
10544
|
+
},
|
|
10545
|
+
retainedByAgent: storage.retainedByAgent,
|
|
10546
|
+
ingestion: {
|
|
10547
|
+
pendingRecords: options.ingestion.pendingRecords,
|
|
10548
|
+
pendingBytes: options.ingestion.pendingBytes,
|
|
10549
|
+
oldestPendingAgeMs: options.ingestion.oldestPendingAgeMs,
|
|
10550
|
+
pausedAgentCount: options.ingestion.pausedAgentCount,
|
|
10551
|
+
failedAgentCount: options.ingestion.failedAgentCount
|
|
10552
|
+
},
|
|
10553
|
+
append: {
|
|
10554
|
+
state: storage.storageState,
|
|
10555
|
+
lastCommitAt: storage.lastCommitAt,
|
|
10556
|
+
lastDurationMs: storage.lastAppendDurationMs
|
|
10557
|
+
},
|
|
10558
|
+
openProjectorActivities: storage.openProjectorActivities,
|
|
10559
|
+
activeReceiveStreams: options.activeReceiveStreams,
|
|
10560
|
+
activeReplayReads: options.activeReplayReads,
|
|
10561
|
+
checkpoint: storage.maintenance,
|
|
10562
|
+
continuityGapCount: storage.continuityGapCount,
|
|
10563
|
+
continuityUncertain: storage.continuityGapCount > 0
|
|
10564
|
+
};
|
|
10565
|
+
}
|
|
10566
|
+
async function fileBytes(path) {
|
|
10567
|
+
const value = await stat2(path).catch((error) => {
|
|
10568
|
+
if (error.code === "ENOENT") return null;
|
|
10569
|
+
throw error;
|
|
10570
|
+
});
|
|
10571
|
+
return value?.size ?? 0;
|
|
10572
|
+
}
|
|
10573
|
+
|
|
10574
|
+
// src/runtime/journal-runtime.ts
|
|
10575
|
+
var JournalRuntimeComposition = class {
|
|
10576
|
+
receive;
|
|
10577
|
+
ingestion;
|
|
10578
|
+
#framers = /* @__PURE__ */ new Map();
|
|
10579
|
+
#activeIncarnationsByAgent = /* @__PURE__ */ new Map();
|
|
10580
|
+
#store;
|
|
10581
|
+
#limits;
|
|
10582
|
+
#now;
|
|
10583
|
+
#notifyEvents;
|
|
10584
|
+
#cursors;
|
|
10585
|
+
#activeReceiveStreams = 0;
|
|
10586
|
+
#activeReplayReads = 0;
|
|
10587
|
+
constructor(options) {
|
|
10588
|
+
this.#store = options.store;
|
|
10589
|
+
this.#limits = options.limits;
|
|
10590
|
+
this.#now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
10591
|
+
this.#notifyEvents = options.notifyEvents ?? defaultNotifyEvents(options.wakeup);
|
|
10592
|
+
this.#cursors = options.cursors;
|
|
10593
|
+
this.ingestion = new JournalIngestionScheduler({
|
|
10594
|
+
limits: journalIngestionLimitsFromRuntime(options.limits),
|
|
10595
|
+
commit: (records) => this.#commit(records.map((record) => record.value)),
|
|
10596
|
+
...options.pauseAgent === void 0 ? {} : { pauseAgent: options.pauseAgent },
|
|
10597
|
+
...options.resumeAgent === void 0 ? {} : { resumeAgent: options.resumeAgent },
|
|
10598
|
+
...options.failAgent === void 0 ? {} : { failAgent: options.failAgent }
|
|
10599
|
+
});
|
|
10600
|
+
this.receive = new ReceivePager(
|
|
10601
|
+
options.store,
|
|
10602
|
+
options.cursors,
|
|
10603
|
+
options.wakeup,
|
|
10604
|
+
receivePagerLimitsFromRuntime(options.limits),
|
|
10605
|
+
{
|
|
10606
|
+
onReadStart: () => {
|
|
10607
|
+
this.#activeReplayReads += 1;
|
|
10608
|
+
},
|
|
10609
|
+
onReadEnd: () => {
|
|
10610
|
+
this.#activeReplayReads -= 1;
|
|
10611
|
+
}
|
|
10612
|
+
}
|
|
10613
|
+
);
|
|
10614
|
+
}
|
|
10615
|
+
async openIncarnation(options) {
|
|
10616
|
+
if (this.#framers.has(options.incarnationId)) {
|
|
10617
|
+
throw new Error("Journal incarnation sink already exists");
|
|
10618
|
+
}
|
|
10619
|
+
await this.#store.beginIncarnation(
|
|
10620
|
+
options.agentId,
|
|
10621
|
+
options.incarnationId,
|
|
10622
|
+
options.epoch,
|
|
10623
|
+
initialProjectorState()
|
|
10624
|
+
);
|
|
10625
|
+
const framer = new RpcRecordFramer(this.#limits.maxJournalPartialRecordBytes);
|
|
10626
|
+
this.#framers.set(options.incarnationId, framer);
|
|
10627
|
+
this.#activeIncarnationsByAgent.set(options.agentId, {
|
|
10628
|
+
incarnationId: options.incarnationId,
|
|
10629
|
+
epoch: options.epoch
|
|
10630
|
+
});
|
|
10631
|
+
let finished = false;
|
|
10632
|
+
const pushRecord = async (record) => {
|
|
10633
|
+
if (finished) throw new Error("Journal incarnation sink is finished");
|
|
10634
|
+
if (record.length === 0 || record.at(-1) !== 10) {
|
|
10635
|
+
throw new Error("Journal incarnation sink requires a complete LF-terminated record");
|
|
10636
|
+
}
|
|
10637
|
+
const bytes = Buffer.from(record);
|
|
10638
|
+
await this.ingestion.enqueue({
|
|
10639
|
+
agentId: options.agentId,
|
|
10640
|
+
batchKey: `${options.agentId}:${options.epoch}:${options.incarnationId}`,
|
|
10641
|
+
bytes,
|
|
10642
|
+
value: {
|
|
10643
|
+
...options,
|
|
10644
|
+
observedAt: this.#now(),
|
|
10645
|
+
bytes
|
|
10646
|
+
}
|
|
10647
|
+
});
|
|
10648
|
+
};
|
|
10649
|
+
return {
|
|
10650
|
+
push: async (chunk) => {
|
|
10651
|
+
await Promise.all(framer.push(chunk).map((record) => pushRecord(record)));
|
|
10652
|
+
},
|
|
10653
|
+
pushRecord,
|
|
10654
|
+
finish: () => {
|
|
10655
|
+
if (finished) throw new Error("Journal incarnation sink is finished");
|
|
10656
|
+
finished = true;
|
|
10657
|
+
this.#framers.delete(options.incarnationId);
|
|
10658
|
+
const active = this.#activeIncarnationsByAgent.get(options.agentId);
|
|
10659
|
+
if (active?.incarnationId === options.incarnationId) {
|
|
10660
|
+
this.#activeIncarnationsByAgent.delete(options.agentId);
|
|
10661
|
+
}
|
|
10662
|
+
return framer.finish();
|
|
10663
|
+
}
|
|
10664
|
+
};
|
|
10665
|
+
}
|
|
10666
|
+
async openReceive(agentId, start, signal) {
|
|
10667
|
+
if (this.#activeReceiveStreams >= this.#limits.maxReceiveStreams) {
|
|
10668
|
+
const error = new Error("Receive stream capacity exceeded");
|
|
10669
|
+
error.code = "receive_resource_exhausted";
|
|
10670
|
+
throw error;
|
|
10671
|
+
}
|
|
10672
|
+
const stream = await this.receive.open(agentId, start, signal);
|
|
10673
|
+
this.#activeReceiveStreams += 1;
|
|
10674
|
+
let released = false;
|
|
10675
|
+
let iteratorCreated = false;
|
|
10676
|
+
const release = () => {
|
|
10677
|
+
if (released) return;
|
|
10678
|
+
released = true;
|
|
10679
|
+
signal.removeEventListener("abort", release);
|
|
10680
|
+
this.#activeReceiveStreams -= 1;
|
|
10681
|
+
};
|
|
10682
|
+
signal.addEventListener("abort", release, { once: true });
|
|
10683
|
+
if (signal.aborted) release();
|
|
10684
|
+
return {
|
|
10685
|
+
cursor: stream.cursor,
|
|
10686
|
+
[Symbol.asyncIterator]: () => {
|
|
10687
|
+
if (iteratorCreated) throw new Error("Receive stream can only be iterated once");
|
|
10688
|
+
iteratorCreated = true;
|
|
10689
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
10690
|
+
return {
|
|
10691
|
+
next: async () => {
|
|
10692
|
+
try {
|
|
10693
|
+
const result = await iterator.next();
|
|
10694
|
+
if (result.done) release();
|
|
10695
|
+
return result;
|
|
10696
|
+
} catch (error) {
|
|
10697
|
+
release();
|
|
10698
|
+
throw error;
|
|
10699
|
+
}
|
|
10700
|
+
},
|
|
10701
|
+
return: async () => {
|
|
10702
|
+
release();
|
|
10703
|
+
return iterator.return?.() ?? { done: true, value: void 0 };
|
|
10704
|
+
},
|
|
10705
|
+
throw: async (error) => {
|
|
10706
|
+
release();
|
|
10707
|
+
if (iterator.throw !== void 0) return iterator.throw(error);
|
|
10708
|
+
throw error;
|
|
10709
|
+
}
|
|
10710
|
+
};
|
|
10711
|
+
}
|
|
10712
|
+
};
|
|
10713
|
+
}
|
|
10714
|
+
decodeCursorPosition(cursor) {
|
|
10715
|
+
return this.#cursors.decode(cursor).position;
|
|
10716
|
+
}
|
|
10717
|
+
async eventHighWater(agentId) {
|
|
10718
|
+
return (await this.#store.getHighWater(agentId))?.eventPosition ?? 0;
|
|
10719
|
+
}
|
|
10720
|
+
async idleEventHighWater(agentId) {
|
|
10721
|
+
return (await this.#store.getHighWater(agentId))?.idleEventPosition ?? null;
|
|
10722
|
+
}
|
|
10723
|
+
async markIdle(agentId) {
|
|
10724
|
+
const epochs = await this.#store.getEpochs(agentId);
|
|
10725
|
+
const epoch = epochs.findLast((candidate) => candidate.state === "open");
|
|
10726
|
+
if (epoch === void 0) throw new Error("Agent has no open continuity epoch");
|
|
10727
|
+
const idleEventPosition = await this.#store.markIdle(agentId, epoch.epoch);
|
|
10728
|
+
this.#notifyEvents(agentId, idleEventPosition);
|
|
10729
|
+
return idleEventPosition;
|
|
10730
|
+
}
|
|
10731
|
+
get activeReceiveStreams() {
|
|
10732
|
+
return this.#activeReceiveStreams;
|
|
10733
|
+
}
|
|
10734
|
+
get activeReplayReads() {
|
|
10735
|
+
return this.#activeReplayReads;
|
|
10736
|
+
}
|
|
10737
|
+
diagnostics(databasePath) {
|
|
10738
|
+
return collectRuntimeJournalDiagnostics({
|
|
10739
|
+
store: this.#store,
|
|
10740
|
+
databasePath,
|
|
10741
|
+
ingestion: this.ingestion,
|
|
10742
|
+
activeReceiveStreams: this.#activeReceiveStreams,
|
|
10743
|
+
activeReplayReads: this.#activeReplayReads
|
|
10744
|
+
});
|
|
10745
|
+
}
|
|
10746
|
+
closeIngestion() {
|
|
10747
|
+
return this.ingestion.close();
|
|
10748
|
+
}
|
|
10749
|
+
async #commit(candidates) {
|
|
10750
|
+
const first = candidates[0];
|
|
10751
|
+
if (first === void 0) return;
|
|
10752
|
+
if (candidates.some(
|
|
10753
|
+
(candidate) => candidate.agentId !== first.agentId || candidate.epoch !== first.epoch || candidate.incarnationId !== first.incarnationId
|
|
10754
|
+
)) {
|
|
10755
|
+
throw new Error("Journal ingestion batch crosses an agent, incarnation, or continuity epoch");
|
|
10756
|
+
}
|
|
10757
|
+
const current = await this.#store.getHighWater(first.agentId) ?? { rawPosition: 0, eventPosition: 0, idleEventPosition: null };
|
|
10758
|
+
let nextState = await this.#store.getProjectorState(first.agentId, first.incarnationId, first.epoch) ?? initialProjectorState();
|
|
10759
|
+
let rawPosition = current.rawPosition;
|
|
10760
|
+
let eventPosition = current.eventPosition;
|
|
10761
|
+
const records = [];
|
|
10762
|
+
const events = [];
|
|
10763
|
+
let projectionError = null;
|
|
10764
|
+
for (const candidate of candidates) {
|
|
10765
|
+
rawPosition += 1;
|
|
10766
|
+
records.push({
|
|
10767
|
+
agentId: candidate.agentId,
|
|
10768
|
+
incarnationId: candidate.incarnationId,
|
|
10769
|
+
position: rawPosition,
|
|
10770
|
+
observedAt: candidate.observedAt,
|
|
10771
|
+
bytes: candidate.bytes
|
|
10772
|
+
});
|
|
10773
|
+
if (projectionError !== null) continue;
|
|
10774
|
+
try {
|
|
10775
|
+
const result = projectLifecycleRecord(
|
|
10776
|
+
nextState,
|
|
10777
|
+
{
|
|
10778
|
+
agentId: candidate.agentId,
|
|
10779
|
+
epoch: candidate.epoch,
|
|
10780
|
+
rawPosition,
|
|
10781
|
+
observedAt: candidate.observedAt,
|
|
10782
|
+
frame: decodeRecord(candidate.bytes)
|
|
10783
|
+
},
|
|
10784
|
+
this.#limits.maxOpenProjectorActivities
|
|
10785
|
+
);
|
|
10786
|
+
if (result.events.some(
|
|
10787
|
+
(event2) => Buffer.byteLength(JSON.stringify(event2)) > this.#limits.maxPiFrameBytes
|
|
10788
|
+
)) {
|
|
10789
|
+
throw new Error("Semantic event exceeds the configured storage limit");
|
|
10790
|
+
}
|
|
10791
|
+
nextState = result.state;
|
|
10792
|
+
for (const event2 of result.events) {
|
|
10793
|
+
eventPosition += 1;
|
|
10794
|
+
events.push({ agentId: candidate.agentId, position: eventPosition, event: event2 });
|
|
10795
|
+
}
|
|
10796
|
+
} catch (error) {
|
|
10797
|
+
projectionError = error instanceof Error ? error : new Error(String(error));
|
|
10798
|
+
}
|
|
10799
|
+
}
|
|
10800
|
+
const append = {
|
|
10801
|
+
agentId: first.agentId,
|
|
10802
|
+
incarnationId: first.incarnationId,
|
|
10803
|
+
epoch: first.epoch,
|
|
10804
|
+
records,
|
|
10805
|
+
events,
|
|
10806
|
+
projectorState: nextState,
|
|
10807
|
+
highWater: {
|
|
10808
|
+
rawPosition,
|
|
10809
|
+
eventPosition,
|
|
10810
|
+
idleEventPosition: current.idleEventPosition
|
|
10811
|
+
}
|
|
10812
|
+
};
|
|
10813
|
+
await this.#store.append(append);
|
|
10814
|
+
if (events.length > 0) this.#notifyEvents(first.agentId, eventPosition);
|
|
10815
|
+
if (projectionError !== null) throw projectionError;
|
|
10816
|
+
}
|
|
10817
|
+
};
|
|
10818
|
+
function journalIngestionLimitsFromRuntime(limits) {
|
|
10819
|
+
return {
|
|
10820
|
+
maxPendingRecords: limits.maxJournalPendingRecords,
|
|
10821
|
+
maxPendingBytes: limits.maxJournalPendingBytes,
|
|
10822
|
+
maxPendingBytesPerAgent: limits.maxJournalPendingBytesPerAgent,
|
|
10823
|
+
maxBatchRecords: limits.maxJournalBatchRecords,
|
|
10824
|
+
maxBatchBytes: limits.maxJournalBatchBytes,
|
|
10825
|
+
maxBatchAgeMs: limits.maxJournalBatchAgeMs
|
|
10826
|
+
};
|
|
10827
|
+
}
|
|
10828
|
+
function receivePagerLimitsFromRuntime(limits) {
|
|
10829
|
+
return {
|
|
10830
|
+
maxRows: limits.maxReceiveReplayRows,
|
|
10831
|
+
maxBytes: limits.maxReceiveReplayBytes,
|
|
10832
|
+
maxEventBytes: limits.maxPiFrameBytes
|
|
10833
|
+
};
|
|
10834
|
+
}
|
|
10835
|
+
function decodeRecord(bytes) {
|
|
10836
|
+
let end = bytes.length;
|
|
10837
|
+
if (end === 0 || bytes[end - 1] !== 10) throw new Error("RPC record is not LF terminated");
|
|
10838
|
+
end -= 1;
|
|
10839
|
+
if (end > 0 && bytes[end - 1] === 13) end -= 1;
|
|
10840
|
+
const text2 = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, end));
|
|
10841
|
+
return JSON.parse(text2);
|
|
10842
|
+
}
|
|
10843
|
+
function defaultNotifyEvents(wakeup) {
|
|
10844
|
+
const candidate = wakeup;
|
|
10845
|
+
if (typeof candidate.notify !== "function") {
|
|
10846
|
+
throw new Error("Journal composition requires a notifiable receive wakeup");
|
|
10847
|
+
}
|
|
10848
|
+
const notify = candidate.notify.bind(wakeup);
|
|
10849
|
+
return (agentId, position) => notify(agentId, position);
|
|
10850
|
+
}
|
|
10851
|
+
|
|
10852
|
+
// src/runtime/journal-wakeup.ts
|
|
10853
|
+
var JournalWakeup = class {
|
|
10854
|
+
#positions = /* @__PURE__ */ new Map();
|
|
10855
|
+
#waiters = /* @__PURE__ */ new Map();
|
|
10856
|
+
#failures = /* @__PURE__ */ new Map();
|
|
10857
|
+
#closedError = null;
|
|
10858
|
+
notify(agentId, position) {
|
|
10859
|
+
if (this.#closedError !== null) return;
|
|
10860
|
+
this.#positions.set(agentId, Math.max(this.#positions.get(agentId) ?? 0, position));
|
|
10861
|
+
const waiters = this.#waiters.get(agentId);
|
|
10862
|
+
if (waiters === void 0) return;
|
|
10863
|
+
for (const waiter of [...waiters]) {
|
|
10864
|
+
if (position <= waiter.afterPosition) continue;
|
|
10865
|
+
waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
10866
|
+
waiters.delete(waiter);
|
|
10867
|
+
waiter.resolve();
|
|
10868
|
+
}
|
|
10869
|
+
if (waiters.size === 0) this.#waiters.delete(agentId);
|
|
10870
|
+
}
|
|
10871
|
+
waitForEvents(agentId, afterPosition, signal) {
|
|
10872
|
+
if (this.#closedError !== null) return Promise.reject(this.#closedError);
|
|
10873
|
+
const failure = this.#failures.get(agentId);
|
|
10874
|
+
if (failure !== void 0) return Promise.reject(failure);
|
|
10875
|
+
if ((this.#positions.get(agentId) ?? 0) > afterPosition) return Promise.resolve();
|
|
10876
|
+
if (signal.aborted) return Promise.reject(new Error("Receive cancelled"));
|
|
10877
|
+
return new Promise((resolve3, reject) => {
|
|
10878
|
+
const waiters = this.#waiters.get(agentId) ?? /* @__PURE__ */ new Set();
|
|
10879
|
+
const waiter = {
|
|
10880
|
+
afterPosition,
|
|
10881
|
+
resolve: resolve3,
|
|
10882
|
+
reject,
|
|
10883
|
+
signal,
|
|
10884
|
+
onAbort: () => {
|
|
10885
|
+
waiters.delete(waiter);
|
|
10886
|
+
if (waiters.size === 0) this.#waiters.delete(agentId);
|
|
10887
|
+
reject(new Error("Receive cancelled"));
|
|
10888
|
+
}
|
|
10889
|
+
};
|
|
10890
|
+
waiters.add(waiter);
|
|
10891
|
+
this.#waiters.set(agentId, waiters);
|
|
10892
|
+
signal.addEventListener("abort", waiter.onAbort, { once: true });
|
|
10893
|
+
const racedFailure = this.#failures.get(agentId);
|
|
10894
|
+
if (racedFailure !== void 0) {
|
|
10895
|
+
signal.removeEventListener("abort", waiter.onAbort);
|
|
10896
|
+
waiters.delete(waiter);
|
|
10897
|
+
if (waiters.size === 0) this.#waiters.delete(agentId);
|
|
10898
|
+
reject(racedFailure);
|
|
10899
|
+
} else if ((this.#positions.get(agentId) ?? 0) > afterPosition) {
|
|
10900
|
+
signal.removeEventListener("abort", waiter.onAbort);
|
|
10901
|
+
waiters.delete(waiter);
|
|
10902
|
+
if (waiters.size === 0) this.#waiters.delete(agentId);
|
|
10903
|
+
resolve3();
|
|
10904
|
+
}
|
|
10905
|
+
});
|
|
10906
|
+
}
|
|
10907
|
+
failAgent(agentId, error) {
|
|
10908
|
+
this.#failures.set(agentId, error);
|
|
10909
|
+
const waiters = this.#waiters.get(agentId);
|
|
10910
|
+
if (waiters === void 0) return;
|
|
10911
|
+
for (const waiter of waiters) {
|
|
10912
|
+
waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
10913
|
+
waiter.reject(error);
|
|
10914
|
+
}
|
|
10915
|
+
this.#waiters.delete(agentId);
|
|
10916
|
+
}
|
|
10917
|
+
close(error = new Error("Runtime unavailable")) {
|
|
10918
|
+
if (this.#closedError !== null) return;
|
|
10919
|
+
this.#closedError = error;
|
|
10920
|
+
for (const waiters of this.#waiters.values()) {
|
|
10921
|
+
for (const waiter of waiters) {
|
|
10922
|
+
waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
10923
|
+
waiter.reject(error);
|
|
10924
|
+
}
|
|
10925
|
+
}
|
|
10926
|
+
this.#waiters.clear();
|
|
10927
|
+
}
|
|
10928
|
+
};
|
|
10929
|
+
|
|
10930
|
+
// src/runtime/storage-health.ts
|
|
10931
|
+
var CleanDrainCoordinator = class {
|
|
10932
|
+
constructor(steps) {
|
|
10933
|
+
this.steps = steps;
|
|
10934
|
+
}
|
|
10935
|
+
#closing = null;
|
|
10936
|
+
close() {
|
|
10937
|
+
this.#closing ??= this.#run();
|
|
10938
|
+
return this.#closing;
|
|
10939
|
+
}
|
|
10940
|
+
async #run() {
|
|
10941
|
+
const steps = [
|
|
10942
|
+
() => this.steps.stopAdmission(),
|
|
10943
|
+
() => this.steps.drainStdoutAndJournal(),
|
|
10944
|
+
() => this.steps.closeReceivers(),
|
|
10945
|
+
() => this.steps.stopProcessTrees(),
|
|
10946
|
+
() => this.steps.closeServer(),
|
|
10947
|
+
() => this.steps.closeStore()
|
|
10948
|
+
];
|
|
10949
|
+
let firstError;
|
|
10950
|
+
for (const step of steps) {
|
|
10951
|
+
try {
|
|
10952
|
+
await step();
|
|
10953
|
+
} catch (error) {
|
|
10954
|
+
firstError ??= error;
|
|
10955
|
+
}
|
|
10956
|
+
}
|
|
10957
|
+
if (firstError !== void 0) throw firstError;
|
|
10958
|
+
}
|
|
10959
|
+
};
|
|
10960
|
+
|
|
10961
|
+
// src/store/journal-fleet-store-adapter.ts
|
|
10962
|
+
var JournalFleetStoreAdapter = class {
|
|
10963
|
+
constructor(journal) {
|
|
10964
|
+
this.journal = journal;
|
|
10965
|
+
}
|
|
10966
|
+
async createAgent(agent, operation) {
|
|
10967
|
+
if (operation === void 0) return this.journal.createAgent(toJournalAgent(agent));
|
|
10968
|
+
return this.journal.createAgentWithOperation(
|
|
10969
|
+
toJournalAgent(agent),
|
|
10970
|
+
toJournalOperation(operation, agent.summary.id)
|
|
10971
|
+
);
|
|
10972
|
+
}
|
|
10973
|
+
async getAgent(name) {
|
|
10974
|
+
return fromJournalAgent(await this.journal.getAgentByName(name));
|
|
10975
|
+
}
|
|
10976
|
+
async listAgents() {
|
|
10977
|
+
return (await this.journal.listAgents()).map((agent) => fromJournalAgent(agent));
|
|
10978
|
+
}
|
|
10979
|
+
putAgent(agent) {
|
|
10980
|
+
return this.journal.putAgent(toJournalAgent(agent));
|
|
10981
|
+
}
|
|
10982
|
+
async rollbackProvisionalCreate(name, completedOperation) {
|
|
10983
|
+
const existing = await this.journal.getAgentByName(name);
|
|
10984
|
+
if (existing === null) return null;
|
|
10985
|
+
return fromJournalAgent(
|
|
10986
|
+
await this.journal.rollbackProvisionalCreate(
|
|
10987
|
+
existing.agentId,
|
|
10988
|
+
toJournalOperation(completedOperation, void 0)
|
|
10989
|
+
)
|
|
10990
|
+
);
|
|
10991
|
+
}
|
|
10992
|
+
async deleteAgent(name, receipt) {
|
|
10993
|
+
const existing = await this.journal.getAgentByName(name);
|
|
10994
|
+
if (existing === null) return null;
|
|
10995
|
+
const destroyed = await this.journal.destroyAgent(existing.agentId, {
|
|
10996
|
+
operationId: receipt?.operationId ?? `rollback:${existing.agentId}`,
|
|
10997
|
+
agentId: existing.agentId,
|
|
10998
|
+
agentName: existing.name,
|
|
10999
|
+
fingerprint: receipt?.fingerprint ?? "rollback",
|
|
11000
|
+
destroyedAt: receipt?.destroyedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
11001
|
+
status: "destroyed"
|
|
11002
|
+
});
|
|
11003
|
+
return fromJournalAgent(destroyed);
|
|
11004
|
+
}
|
|
11005
|
+
async getOperation(operationId) {
|
|
11006
|
+
const operation = await this.journal.getOperation(operationId);
|
|
11007
|
+
if (operation !== null) return fromJournalOperation(operation);
|
|
11008
|
+
const receipt = await this.journal.getDestroyReceipt(operationId);
|
|
11009
|
+
if (receipt === null) return null;
|
|
11010
|
+
return {
|
|
11011
|
+
operationId,
|
|
11012
|
+
method: "destroy",
|
|
11013
|
+
fingerprint: receipt.fingerprint,
|
|
11014
|
+
state: "completed",
|
|
11015
|
+
result: {
|
|
11016
|
+
ok: true,
|
|
11017
|
+
value: {
|
|
11018
|
+
schemaVersion: 1,
|
|
11019
|
+
type: "agent.destroyed",
|
|
11020
|
+
agent: { id: receipt.agentId, name: receipt.agentName }
|
|
11021
|
+
}
|
|
11022
|
+
},
|
|
11023
|
+
targetName: receipt.agentName,
|
|
11024
|
+
targetAgent: { id: receipt.agentId, name: receipt.agentName }
|
|
11025
|
+
};
|
|
11026
|
+
}
|
|
11027
|
+
async putOperation(operation) {
|
|
11028
|
+
if (operation.method === "destroy" && operation.state === "completed" && await this.journal.getDestroyReceipt(operation.operationId) !== null) {
|
|
11029
|
+
return;
|
|
11030
|
+
}
|
|
11031
|
+
await this.journal.putOperation(
|
|
11032
|
+
toJournalOperation(operation, operation.targetAgent?.id)
|
|
11033
|
+
);
|
|
11034
|
+
}
|
|
11035
|
+
async listPendingOperations() {
|
|
11036
|
+
return (await this.journal.listPendingOperations()).map(
|
|
11037
|
+
(operation) => fromJournalOperation(operation)
|
|
11038
|
+
);
|
|
11039
|
+
}
|
|
11040
|
+
async deleteOperation(operationId) {
|
|
11041
|
+
await this.journal.deleteOperation(operationId);
|
|
11042
|
+
}
|
|
11043
|
+
async getSend(sendId) {
|
|
11044
|
+
return fromJournalSend(
|
|
11045
|
+
await this.journal.getSend(sendId),
|
|
11046
|
+
await this.#agentNameForSend(sendId)
|
|
11047
|
+
);
|
|
11048
|
+
}
|
|
11049
|
+
async nextSendOrdinal(agentName) {
|
|
11050
|
+
const agent = await this.#requireAgentByName(agentName);
|
|
11051
|
+
return this.journal.nextSendOrdinal(agent.agentId);
|
|
11052
|
+
}
|
|
11053
|
+
async putSend(send) {
|
|
11054
|
+
const agent = await this.#requireAgentByName(send.agentName);
|
|
11055
|
+
if (send.agentId === void 0 || agent.agentId !== send.agentId) {
|
|
11056
|
+
throw new Error("Send is missing its immutable agent generation");
|
|
11057
|
+
}
|
|
11058
|
+
await this.journal.putSend({
|
|
11059
|
+
sendId: send.sendId,
|
|
11060
|
+
agentId: send.agentId,
|
|
11061
|
+
ordinal: send.ordinal ?? await this.journal.nextSendOrdinal(agent.agentId),
|
|
11062
|
+
message: send.message,
|
|
11063
|
+
delivery: send.delivery ?? "steer",
|
|
11064
|
+
state: send.state,
|
|
11065
|
+
acceptedAt: send.acceptedAt
|
|
11066
|
+
});
|
|
11067
|
+
}
|
|
11068
|
+
async listNonterminalSends() {
|
|
11069
|
+
const sends = await this.journal.listNonterminalSends();
|
|
11070
|
+
return Promise.all(
|
|
11071
|
+
sends.map(async (send) => {
|
|
11072
|
+
const agent = await this.journal.getAgentById(send.agentId);
|
|
11073
|
+
if (agent === null) throw new Error("Send targets a deleted agent generation");
|
|
11074
|
+
return fromJournalSend(send, agent.name);
|
|
11075
|
+
})
|
|
11076
|
+
);
|
|
11077
|
+
}
|
|
11078
|
+
async getCompact(compactId) {
|
|
11079
|
+
const compact = await this.journal.getCompact(compactId);
|
|
11080
|
+
if (compact === null) return null;
|
|
11081
|
+
const agent = await this.journal.getAgentById(compact.agentId);
|
|
11082
|
+
if (agent === null) return null;
|
|
11083
|
+
return fromJournalCompact(compact, agent.name);
|
|
11084
|
+
}
|
|
11085
|
+
async putCompact(compact) {
|
|
11086
|
+
const agent = await this.#requireAgentByName(compact.agentName);
|
|
11087
|
+
if (compact.agentId === void 0 || agent.agentId !== compact.agentId) {
|
|
11088
|
+
throw new Error("Compaction is missing its immutable agent generation");
|
|
11089
|
+
}
|
|
11090
|
+
await this.journal.putCompact({
|
|
11091
|
+
compactId: compact.compactId,
|
|
11092
|
+
agentId: compact.agentId,
|
|
11093
|
+
state: compact.state,
|
|
11094
|
+
requestedAt: compact.requestedAt,
|
|
11095
|
+
...compact.result === void 0 ? {} : { result: compact.result },
|
|
11096
|
+
...compact.error === void 0 ? {} : { error: compact.error }
|
|
11097
|
+
});
|
|
11098
|
+
}
|
|
11099
|
+
async listNonterminalCompacts() {
|
|
11100
|
+
const compacts = await this.journal.listNonterminalCompacts();
|
|
11101
|
+
return Promise.all(
|
|
11102
|
+
compacts.map(async (compact) => {
|
|
11103
|
+
const agent = await this.journal.getAgentById(compact.agentId);
|
|
11104
|
+
if (agent === null) throw new Error("Compaction targets a deleted agent generation");
|
|
11105
|
+
return fromJournalCompact(compact, agent.name);
|
|
11106
|
+
})
|
|
11107
|
+
);
|
|
11108
|
+
}
|
|
11109
|
+
async putIncarnation(incarnation) {
|
|
11110
|
+
const agent = await this.#requireAgentByName(incarnation.agentName);
|
|
11111
|
+
if (incarnation.agentId === void 0 || agent.agentId !== incarnation.agentId) {
|
|
11112
|
+
throw new Error("Incarnation is missing its immutable agent generation");
|
|
11113
|
+
}
|
|
11114
|
+
await this.journal.putIncarnation({
|
|
11115
|
+
incarnationId: incarnation.incarnationId,
|
|
11116
|
+
agentId: incarnation.agentId,
|
|
11117
|
+
pid: incarnation.pid,
|
|
11118
|
+
state: incarnation.state
|
|
11119
|
+
});
|
|
11120
|
+
}
|
|
11121
|
+
async listActiveIncarnations() {
|
|
11122
|
+
const incarnations = await this.journal.listActiveIncarnations();
|
|
11123
|
+
return Promise.all(
|
|
11124
|
+
incarnations.map(async (incarnation) => {
|
|
11125
|
+
const agent = await this.journal.getAgentById(incarnation.agentId);
|
|
11126
|
+
if (agent === null) throw new Error("Incarnation targets a deleted agent generation");
|
|
11127
|
+
return {
|
|
11128
|
+
incarnationId: incarnation.incarnationId,
|
|
11129
|
+
agentId: incarnation.agentId,
|
|
11130
|
+
agentName: agent.name,
|
|
11131
|
+
pid: incarnation.pid,
|
|
11132
|
+
state: incarnation.state
|
|
11133
|
+
};
|
|
11134
|
+
})
|
|
11135
|
+
);
|
|
11136
|
+
}
|
|
11137
|
+
close() {
|
|
11138
|
+
return this.journal.close();
|
|
11139
|
+
}
|
|
11140
|
+
async #requireAgentByName(name) {
|
|
11141
|
+
const agent = await this.journal.getAgentByName(name);
|
|
11142
|
+
if (agent === null) throw new Error(`Agent ${name} no longer exists`);
|
|
11143
|
+
return agent;
|
|
11144
|
+
}
|
|
11145
|
+
async #agentNameForSend(sendId) {
|
|
11146
|
+
const send = await this.journal.getSend(sendId);
|
|
11147
|
+
if (send === null) return null;
|
|
11148
|
+
return (await this.journal.getAgentById(send.agentId))?.name ?? null;
|
|
11149
|
+
}
|
|
11150
|
+
};
|
|
11151
|
+
function toJournalAgent(agent) {
|
|
11152
|
+
return {
|
|
11153
|
+
agentId: agent.summary.id,
|
|
11154
|
+
name: agent.summary.name,
|
|
11155
|
+
summary: agent.summary,
|
|
11156
|
+
launch: agent.launch
|
|
11157
|
+
};
|
|
11158
|
+
}
|
|
11159
|
+
function fromJournalAgent(agent) {
|
|
11160
|
+
if (agent === null) return null;
|
|
11161
|
+
return {
|
|
11162
|
+
summary: agent.summary,
|
|
11163
|
+
launch: agent.launch
|
|
11164
|
+
};
|
|
11165
|
+
}
|
|
11166
|
+
function fromJournalOperation(operation) {
|
|
11167
|
+
return {
|
|
11168
|
+
operationId: operation.operationId,
|
|
11169
|
+
method: operation.method,
|
|
11170
|
+
fingerprint: operation.fingerprint,
|
|
11171
|
+
state: operation.state,
|
|
11172
|
+
result: operation.result,
|
|
11173
|
+
targetName: operation.targetName,
|
|
11174
|
+
...operation.agentId === null ? {} : { targetAgent: { id: operation.agentId, name: operation.targetName } },
|
|
11175
|
+
...operation.state === "pending" && operation.request !== void 0 ? { request: operation.request } : {}
|
|
11176
|
+
};
|
|
11177
|
+
}
|
|
11178
|
+
function toJournalOperation(operation, targetId) {
|
|
11179
|
+
return {
|
|
11180
|
+
operationId: operation.operationId,
|
|
11181
|
+
agentId: targetId ?? null,
|
|
11182
|
+
targetName: operation.targetName ?? operation.targetAgent?.name ?? "",
|
|
11183
|
+
method: operation.method,
|
|
11184
|
+
fingerprint: operation.fingerprint,
|
|
11185
|
+
state: operation.state,
|
|
11186
|
+
result: operation.result,
|
|
11187
|
+
...operation.state === "pending" && operation.request !== void 0 ? { request: operation.request } : {}
|
|
11188
|
+
};
|
|
11189
|
+
}
|
|
11190
|
+
function fromJournalSend(send, name) {
|
|
11191
|
+
if (send === null || name === null) return null;
|
|
11192
|
+
return {
|
|
11193
|
+
sendId: send.sendId,
|
|
11194
|
+
agentId: send.agentId,
|
|
11195
|
+
agentName: name,
|
|
11196
|
+
ordinal: send.ordinal,
|
|
11197
|
+
message: send.message,
|
|
11198
|
+
delivery: send.delivery,
|
|
11199
|
+
state: send.state,
|
|
11200
|
+
acceptedAt: send.acceptedAt
|
|
11201
|
+
};
|
|
11202
|
+
}
|
|
11203
|
+
function fromJournalCompact(compact, name) {
|
|
11204
|
+
return {
|
|
11205
|
+
compactId: compact.compactId,
|
|
11206
|
+
agentId: compact.agentId,
|
|
11207
|
+
agentName: name,
|
|
11208
|
+
state: compact.state,
|
|
11209
|
+
requestedAt: compact.requestedAt,
|
|
11210
|
+
...compact.result === void 0 ? {} : { result: compact.result },
|
|
11211
|
+
...compact.error === void 0 ? {} : { error: compact.error }
|
|
11212
|
+
};
|
|
11213
|
+
}
|
|
11214
|
+
|
|
11215
|
+
// src/store/sqlite-journal-store.ts
|
|
11216
|
+
import { DatabaseSync } from "node:sqlite";
|
|
11217
|
+
var JOURNAL_SCHEMA_VERSION = 3;
|
|
11218
|
+
var JOURNAL_SCHEMA_CHECKSUM = "003_uuid_journal_v3";
|
|
11219
|
+
var LEGACY_JOURNAL_SCHEMA_CHECKSUMS = /* @__PURE__ */ new Map([
|
|
11220
|
+
[1, "001_initial_v1"],
|
|
11221
|
+
[2, "002_compact_v1"]
|
|
11222
|
+
]);
|
|
11223
|
+
function classifyJournalMigrationRows(rows) {
|
|
11224
|
+
if (rows.length === 0) return "fresh";
|
|
11225
|
+
for (const row of rows) {
|
|
11226
|
+
if (!Number.isSafeInteger(row.version) || typeof row.checksum !== "string") {
|
|
11227
|
+
throw new Error("pi-fleet database migration ledger is malformed");
|
|
11228
|
+
}
|
|
11229
|
+
}
|
|
11230
|
+
if (rows.length === 1 && rows[0]?.version === JOURNAL_SCHEMA_VERSION) {
|
|
11231
|
+
if (rows[0].checksum !== JOURNAL_SCHEMA_CHECKSUM) {
|
|
11232
|
+
throw new Error(
|
|
11233
|
+
`pi-fleet database migration ${String(JOURNAL_SCHEMA_VERSION)} checksum mismatch`
|
|
11234
|
+
);
|
|
11235
|
+
}
|
|
11236
|
+
return "current";
|
|
11237
|
+
}
|
|
11238
|
+
const legacyOne = rows.length === 1 && rows[0]?.version === 1;
|
|
11239
|
+
const legacyTwo = rows.length === 2 && rows[0]?.version === 1 && rows[1]?.version === 2;
|
|
11240
|
+
if (legacyOne || legacyTwo) {
|
|
11241
|
+
for (const row of rows) {
|
|
11242
|
+
if (LEGACY_JOURNAL_SCHEMA_CHECKSUMS.get(row.version) !== row.checksum) {
|
|
11243
|
+
throw new Error(`pi-fleet database migration ${String(row.version)} checksum mismatch`);
|
|
11244
|
+
}
|
|
11245
|
+
}
|
|
11246
|
+
return "legacy";
|
|
11247
|
+
}
|
|
11248
|
+
const newer = rows.find((row) => row.version > JOURNAL_SCHEMA_VERSION);
|
|
11249
|
+
if (newer !== void 0) {
|
|
11250
|
+
throw new Error(`pi-fleet database schema ${String(newer.version)} is newer than this runtime`);
|
|
11251
|
+
}
|
|
11252
|
+
throw new Error("pi-fleet database migration ledger sequence is invalid");
|
|
11253
|
+
}
|
|
11254
|
+
|
|
11255
|
+
// src/store/worker-journal-store.ts
|
|
9052
11256
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
9053
11257
|
import { Worker } from "node:worker_threads";
|
|
9054
|
-
|
|
11258
|
+
|
|
11259
|
+
// src/store/journal-worker-protocol.ts
|
|
11260
|
+
function isJournalWorkerResponse(value) {
|
|
11261
|
+
if (typeof value !== "object" || value === null) return false;
|
|
11262
|
+
const response = value;
|
|
11263
|
+
return typeof response.id === "string" && typeof response.ok === "boolean" && (response.error === void 0 || typeof response.error === "string");
|
|
11264
|
+
}
|
|
11265
|
+
|
|
11266
|
+
// src/store/worker-journal-store.ts
|
|
11267
|
+
var WRITE_METHODS = /* @__PURE__ */ new Set([
|
|
11268
|
+
"createAgent",
|
|
11269
|
+
"createAgentWithOperation",
|
|
11270
|
+
"rollbackProvisionalCreate",
|
|
11271
|
+
"putAgent",
|
|
11272
|
+
"putOperation",
|
|
11273
|
+
"deleteOperation",
|
|
11274
|
+
"putSend",
|
|
11275
|
+
"putCompact",
|
|
11276
|
+
"putIncarnation",
|
|
11277
|
+
"putEpoch",
|
|
11278
|
+
"beginIncarnation",
|
|
11279
|
+
"append",
|
|
11280
|
+
"markIdle",
|
|
11281
|
+
"destroyAgent",
|
|
11282
|
+
"maintain",
|
|
11283
|
+
"close"
|
|
11284
|
+
]);
|
|
11285
|
+
var WorkerJournalStore = class {
|
|
9055
11286
|
#worker;
|
|
9056
|
-
#
|
|
11287
|
+
#maxPending;
|
|
11288
|
+
#onHealthFailure;
|
|
11289
|
+
#writes = [];
|
|
11290
|
+
#reads = [];
|
|
11291
|
+
#readerIds = /* @__PURE__ */ new Set();
|
|
11292
|
+
#active = null;
|
|
11293
|
+
#closing = false;
|
|
9057
11294
|
#closed = false;
|
|
11295
|
+
#closePromise = null;
|
|
11296
|
+
#terminationPromise = null;
|
|
9058
11297
|
#failure = null;
|
|
9059
|
-
constructor(path,
|
|
9060
|
-
this.#
|
|
9061
|
-
this.#
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
11298
|
+
constructor(path, options = {}) {
|
|
11299
|
+
this.#maxPending = options.maxPending ?? DEFAULT_RUNTIME_LIMITS.maxJournalPendingRecords;
|
|
11300
|
+
if (!Number.isSafeInteger(this.#maxPending) || this.#maxPending <= 0) {
|
|
11301
|
+
throw new Error("Journal worker maxPending must be a positive integer");
|
|
11302
|
+
}
|
|
11303
|
+
this.#onHealthFailure = options.onHealthFailure;
|
|
11304
|
+
this.#worker = options.worker ?? new Worker(options.workerUrl ?? new URL("./journal-sqlite-worker.mjs", import.meta.url), {
|
|
11305
|
+
workerData: {
|
|
11306
|
+
path,
|
|
11307
|
+
checkpointCommitInterval: options.checkpointCommitInterval ?? DEFAULT_RUNTIME_LIMITS.journalCheckpointCommitInterval,
|
|
11308
|
+
reclaimPagesPerPass: options.reclaimPagesPerPass ?? DEFAULT_RUNTIME_LIMITS.journalReclaimPagesPerPass
|
|
9066
11309
|
}
|
|
9067
|
-
const pending = this.#pending.get(value.id);
|
|
9068
|
-
if (pending === void 0) return;
|
|
9069
|
-
this.#pending.delete(value.id);
|
|
9070
|
-
if (value.ok) pending.resolve(value.value);
|
|
9071
|
-
else pending.reject(new Error(value.error ?? "SQLite worker failed"));
|
|
9072
11310
|
});
|
|
11311
|
+
this.#worker.on("message", (value) => this.#handleResponse(value));
|
|
9073
11312
|
this.#worker.on("error", (error) => this.#fail(error));
|
|
9074
11313
|
this.#worker.on("exit", (code) => {
|
|
9075
|
-
if (!this.#closed) this.#fail(new Error(`
|
|
11314
|
+
if (!this.#closed) this.#fail(new Error(`Journal worker exited unexpectedly with ${code}`));
|
|
9076
11315
|
});
|
|
9077
11316
|
}
|
|
9078
11317
|
createAgent(agent) {
|
|
9079
11318
|
return this.#call("createAgent", [agent]);
|
|
9080
11319
|
}
|
|
9081
|
-
|
|
9082
|
-
return this.#call("
|
|
11320
|
+
createAgentWithOperation(agent, operation) {
|
|
11321
|
+
return this.#call("createAgentWithOperation", [agent, operation]);
|
|
11322
|
+
}
|
|
11323
|
+
rollbackProvisionalCreate(agentId, completedOperation) {
|
|
11324
|
+
return this.#call("rollbackProvisionalCreate", [agentId, completedOperation]);
|
|
11325
|
+
}
|
|
11326
|
+
getAgentByName(name) {
|
|
11327
|
+
return this.#call("getAgentByName", [name]);
|
|
11328
|
+
}
|
|
11329
|
+
getAgentById(agentId) {
|
|
11330
|
+
return this.#call("getAgentById", [agentId]);
|
|
9083
11331
|
}
|
|
9084
11332
|
listAgents() {
|
|
9085
11333
|
return this.#call("listAgents", []);
|
|
@@ -9087,36 +11335,36 @@ var WorkerFleetStore = class {
|
|
|
9087
11335
|
async putAgent(agent) {
|
|
9088
11336
|
await this.#call("putAgent", [agent]);
|
|
9089
11337
|
}
|
|
9090
|
-
getOperation(operationId) {
|
|
9091
|
-
return this.#call("getOperation", [operationId]);
|
|
9092
|
-
}
|
|
9093
11338
|
async putOperation(operation) {
|
|
9094
11339
|
await this.#call("putOperation", [operation]);
|
|
9095
11340
|
}
|
|
11341
|
+
getOperation(operationId) {
|
|
11342
|
+
return this.#call("getOperation", [operationId]);
|
|
11343
|
+
}
|
|
9096
11344
|
listPendingOperations() {
|
|
9097
11345
|
return this.#call("listPendingOperations", []);
|
|
9098
11346
|
}
|
|
9099
11347
|
async deleteOperation(operationId) {
|
|
9100
11348
|
await this.#call("deleteOperation", [operationId]);
|
|
9101
11349
|
}
|
|
11350
|
+
async putSend(send) {
|
|
11351
|
+
await this.#call("putSend", [send]);
|
|
11352
|
+
}
|
|
9102
11353
|
getSend(sendId) {
|
|
9103
11354
|
return this.#call("getSend", [sendId]);
|
|
9104
11355
|
}
|
|
9105
|
-
nextSendOrdinal(
|
|
9106
|
-
return this.#call("nextSendOrdinal", [
|
|
9107
|
-
}
|
|
9108
|
-
async putSend(send) {
|
|
9109
|
-
await this.#call("putSend", [send]);
|
|
11356
|
+
nextSendOrdinal(agentId) {
|
|
11357
|
+
return this.#call("nextSendOrdinal", [agentId]);
|
|
9110
11358
|
}
|
|
9111
11359
|
listNonterminalSends() {
|
|
9112
11360
|
return this.#call("listNonterminalSends", []);
|
|
9113
11361
|
}
|
|
9114
|
-
getCompact(compactId) {
|
|
9115
|
-
return this.#call("getCompact", [compactId]);
|
|
9116
|
-
}
|
|
9117
11362
|
async putCompact(compact) {
|
|
9118
11363
|
await this.#call("putCompact", [compact]);
|
|
9119
11364
|
}
|
|
11365
|
+
getCompact(compactId) {
|
|
11366
|
+
return this.#call("getCompact", [compactId]);
|
|
11367
|
+
}
|
|
9120
11368
|
listNonterminalCompacts() {
|
|
9121
11369
|
return this.#call("listNonterminalCompacts", []);
|
|
9122
11370
|
}
|
|
@@ -9126,94 +11374,426 @@ var WorkerFleetStore = class {
|
|
|
9126
11374
|
listActiveIncarnations() {
|
|
9127
11375
|
return this.#call("listActiveIncarnations", []);
|
|
9128
11376
|
}
|
|
9129
|
-
|
|
9130
|
-
|
|
11377
|
+
async putEpoch(epoch) {
|
|
11378
|
+
await this.#call("putEpoch", [epoch]);
|
|
9131
11379
|
}
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
await this.#
|
|
11380
|
+
getEpochs(agentId) {
|
|
11381
|
+
return this.#call("getEpochs", [agentId]);
|
|
11382
|
+
}
|
|
11383
|
+
async beginIncarnation(agentId, incarnationId, epoch, projectorState) {
|
|
11384
|
+
await this.#call("beginIncarnation", [agentId, incarnationId, epoch, projectorState]);
|
|
11385
|
+
}
|
|
11386
|
+
async append(batch) {
|
|
11387
|
+
const { args, transferList } = prepareAppend(batch);
|
|
11388
|
+
await this.#call("append", args, { transferList });
|
|
11389
|
+
}
|
|
11390
|
+
openReceive(agentId) {
|
|
11391
|
+
return this.#call("openReceive", [agentId]);
|
|
11392
|
+
}
|
|
11393
|
+
readEvents(range) {
|
|
11394
|
+
return this.readEventsForReader("default", range);
|
|
9137
11395
|
}
|
|
9138
|
-
|
|
9139
|
-
if (this.#
|
|
11396
|
+
readEventsForReader(readerId, range) {
|
|
11397
|
+
if (this.#readerIds.has(readerId)) {
|
|
11398
|
+
return Promise.reject(new Error(`Reader ${readerId} already has an outstanding range read`));
|
|
11399
|
+
}
|
|
11400
|
+
this.#readerIds.add(readerId);
|
|
11401
|
+
return this.#call("readEvents", [range], { readerId }).catch(
|
|
11402
|
+
(error) => {
|
|
11403
|
+
this.#readerIds.delete(readerId);
|
|
11404
|
+
throw error;
|
|
11405
|
+
}
|
|
11406
|
+
);
|
|
11407
|
+
}
|
|
11408
|
+
getProjectorState(agentId, incarnationId, epoch) {
|
|
11409
|
+
return this.#call("getProjectorState", [agentId, incarnationId, epoch]);
|
|
11410
|
+
}
|
|
11411
|
+
getHighWater(agentId) {
|
|
11412
|
+
return this.#call("getHighWater", [agentId]);
|
|
11413
|
+
}
|
|
11414
|
+
markIdle(agentId, epoch) {
|
|
11415
|
+
return this.#call("markIdle", [agentId, epoch]);
|
|
11416
|
+
}
|
|
11417
|
+
getRawRecords(agentId, afterPosition, limit) {
|
|
11418
|
+
return this.#call("getRawRecords", [agentId, afterPosition, limit]);
|
|
11419
|
+
}
|
|
11420
|
+
destroyAgent(agentId, receipt) {
|
|
11421
|
+
return this.#call("destroyAgent", [agentId, receipt]);
|
|
11422
|
+
}
|
|
11423
|
+
getDestroyReceipt(operationId) {
|
|
11424
|
+
return this.#call("getDestroyReceipt", [operationId]);
|
|
11425
|
+
}
|
|
11426
|
+
maintain(reclaimPages) {
|
|
11427
|
+
return this.#call("maintain", reclaimPages === void 0 ? [] : [reclaimPages]);
|
|
11428
|
+
}
|
|
11429
|
+
getDiagnostics() {
|
|
11430
|
+
return this.#call("getDiagnostics", []);
|
|
11431
|
+
}
|
|
11432
|
+
close() {
|
|
11433
|
+
if (this.#closePromise !== null) return this.#closePromise;
|
|
11434
|
+
if (this.#closed) return Promise.resolve();
|
|
11435
|
+
this.#closing = true;
|
|
11436
|
+
this.#rejectQueued(new Error("pi-fleet journal store is closing"));
|
|
11437
|
+
this.#closePromise = (async () => {
|
|
11438
|
+
let closeError = null;
|
|
11439
|
+
try {
|
|
11440
|
+
if (this.#failure === null) await this.#enqueueClose();
|
|
11441
|
+
} catch (error) {
|
|
11442
|
+
closeError = error;
|
|
11443
|
+
}
|
|
11444
|
+
this.#closed = true;
|
|
11445
|
+
try {
|
|
11446
|
+
await this.#terminateWorker();
|
|
11447
|
+
} catch (error) {
|
|
11448
|
+
if (closeError === null) closeError = error;
|
|
11449
|
+
}
|
|
11450
|
+
if (closeError !== null) throw closeError;
|
|
11451
|
+
})();
|
|
11452
|
+
return this.#closePromise;
|
|
11453
|
+
}
|
|
11454
|
+
#call(method, args, options = {}) {
|
|
11455
|
+
if (this.#closing || this.#closed) {
|
|
11456
|
+
if (options.readerId !== void 0) this.#readerIds.delete(options.readerId);
|
|
11457
|
+
return Promise.reject(
|
|
11458
|
+
new Error(`pi-fleet journal store is ${this.#closed ? "closed" : "closing"}`)
|
|
11459
|
+
);
|
|
11460
|
+
}
|
|
9140
11461
|
if (this.#failure !== null) return Promise.reject(this.#failure);
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
11462
|
+
if (this.#pendingCount() >= this.#maxPending) {
|
|
11463
|
+
if (options.readerId !== void 0) this.#readerIds.delete(options.readerId);
|
|
11464
|
+
return Promise.reject(new Error("Journal worker pending capacity exceeded"));
|
|
11465
|
+
}
|
|
11466
|
+
return new Promise((resolve3, reject) => {
|
|
11467
|
+
const call = {
|
|
11468
|
+
request: { id: randomUUID3(), method, args },
|
|
11469
|
+
transferList: options.transferList ?? [],
|
|
11470
|
+
priority: WRITE_METHODS.has(method) ? "write" : "read",
|
|
11471
|
+
...options.readerId === void 0 ? {} : { readerId: options.readerId },
|
|
11472
|
+
resolve: (value) => resolve3(value),
|
|
11473
|
+
reject
|
|
11474
|
+
};
|
|
11475
|
+
(call.priority === "write" ? this.#writes : this.#reads).push(call);
|
|
11476
|
+
this.#pump();
|
|
11477
|
+
});
|
|
11478
|
+
}
|
|
11479
|
+
#enqueueClose() {
|
|
11480
|
+
return new Promise((resolve3, reject) => {
|
|
11481
|
+
this.#writes.unshift({
|
|
11482
|
+
request: { id: randomUUID3(), method: "close", args: [] },
|
|
11483
|
+
transferList: [],
|
|
11484
|
+
priority: "write",
|
|
11485
|
+
resolve: () => resolve3(),
|
|
11486
|
+
reject
|
|
9146
11487
|
});
|
|
11488
|
+
this.#pump();
|
|
9147
11489
|
});
|
|
11490
|
+
}
|
|
11491
|
+
#pump() {
|
|
11492
|
+
if (this.#active !== null || this.#failure !== null || this.#closed) return;
|
|
11493
|
+
const next = this.#writes.shift() ?? this.#reads.shift();
|
|
11494
|
+
if (next === void 0) return;
|
|
11495
|
+
this.#active = next;
|
|
9148
11496
|
try {
|
|
9149
|
-
this.#worker.postMessage(
|
|
11497
|
+
this.#worker.postMessage(next.request, next.transferList);
|
|
9150
11498
|
} catch (error) {
|
|
9151
|
-
|
|
9152
|
-
this.#fail(failure);
|
|
11499
|
+
this.#fail(error instanceof Error ? error : new Error("Journal worker request failed"));
|
|
9153
11500
|
}
|
|
9154
|
-
|
|
11501
|
+
}
|
|
11502
|
+
#handleResponse(value) {
|
|
11503
|
+
if (!isJournalWorkerResponse(value)) {
|
|
11504
|
+
this.#fail(new Error("Journal worker returned a malformed response"));
|
|
11505
|
+
return;
|
|
11506
|
+
}
|
|
11507
|
+
const active = this.#active;
|
|
11508
|
+
if (active === null || active.request.id !== value.id) {
|
|
11509
|
+
this.#fail(new Error("Journal worker returned an unexpected response"));
|
|
11510
|
+
return;
|
|
11511
|
+
}
|
|
11512
|
+
if (!value.ok && active.priority === "write") {
|
|
11513
|
+
this.#fail(new Error(value.error ?? "Journal worker durability write failed"));
|
|
11514
|
+
return;
|
|
11515
|
+
}
|
|
11516
|
+
this.#active = null;
|
|
11517
|
+
if (active.readerId !== void 0) this.#readerIds.delete(active.readerId);
|
|
11518
|
+
if (value.ok) active.resolve(value.value);
|
|
11519
|
+
else active.reject(new Error(value.error ?? "Journal worker failed"));
|
|
11520
|
+
this.#pump();
|
|
9155
11521
|
}
|
|
9156
11522
|
#fail(error) {
|
|
9157
|
-
this.#failure
|
|
9158
|
-
|
|
9159
|
-
this.#
|
|
11523
|
+
if (this.#failure !== null) return;
|
|
11524
|
+
this.#failure = error;
|
|
11525
|
+
this.#onHealthFailure?.(error);
|
|
11526
|
+
void this.#terminateWorker();
|
|
11527
|
+
const calls = [this.#active, ...this.#writes, ...this.#reads].filter(
|
|
11528
|
+
(call) => call !== null
|
|
11529
|
+
);
|
|
11530
|
+
this.#active = null;
|
|
11531
|
+
this.#writes.length = 0;
|
|
11532
|
+
this.#reads.length = 0;
|
|
11533
|
+
this.#readerIds.clear();
|
|
11534
|
+
for (const call of calls) call.reject(error);
|
|
11535
|
+
}
|
|
11536
|
+
#terminateWorker() {
|
|
11537
|
+
this.#terminationPromise ??= this.#worker.terminate();
|
|
11538
|
+
return this.#terminationPromise;
|
|
11539
|
+
}
|
|
11540
|
+
#rejectQueued(error) {
|
|
11541
|
+
const queued = [...this.#writes, ...this.#reads];
|
|
11542
|
+
this.#writes.length = 0;
|
|
11543
|
+
this.#reads.length = 0;
|
|
11544
|
+
for (const call of queued) {
|
|
11545
|
+
if (call.readerId !== void 0) this.#readerIds.delete(call.readerId);
|
|
11546
|
+
call.reject(error);
|
|
11547
|
+
}
|
|
11548
|
+
}
|
|
11549
|
+
#pendingCount() {
|
|
11550
|
+
return (this.#active === null ? 0 : 1) + this.#writes.length + this.#reads.length;
|
|
9160
11551
|
}
|
|
9161
11552
|
};
|
|
9162
|
-
function
|
|
9163
|
-
|
|
9164
|
-
const
|
|
9165
|
-
|
|
11553
|
+
function prepareAppend(batch) {
|
|
11554
|
+
const transferList = [];
|
|
11555
|
+
const records = batch.records.map((record) => {
|
|
11556
|
+
const bytes = Uint8Array.from(record.bytes);
|
|
11557
|
+
transferList.push(bytes.buffer);
|
|
11558
|
+
return { ...record, bytes };
|
|
11559
|
+
});
|
|
11560
|
+
return { args: [{ ...batch, records }], transferList };
|
|
9166
11561
|
}
|
|
9167
11562
|
|
|
9168
11563
|
// src/entry/runtime.ts
|
|
9169
11564
|
async function runRuntime() {
|
|
9170
11565
|
const paths = resolveFleetPaths();
|
|
9171
11566
|
const limits = runtimeLimitsFromEnv();
|
|
11567
|
+
await preflightRuntimeStartup({ socketPath: paths.socketPath, destructive: false });
|
|
11568
|
+
await prepareFleetPathSecurity(paths);
|
|
11569
|
+
const schemaState = inspectJournalSchema(paths.databasePath);
|
|
11570
|
+
if (schemaState === "legacy") {
|
|
11571
|
+
await preflightRuntimeStartup({
|
|
11572
|
+
socketPath: paths.socketPath,
|
|
11573
|
+
destructive: true,
|
|
11574
|
+
assertOwnedProcessTreesAbsent: () => assertLegacyProcessTreesAbsent(paths.databasePath)
|
|
11575
|
+
});
|
|
11576
|
+
}
|
|
9172
11577
|
let resolveService;
|
|
9173
11578
|
let rejectService;
|
|
9174
11579
|
const serviceReady = new Promise((resolveReady, rejectReady) => {
|
|
9175
11580
|
resolveService = resolveReady;
|
|
9176
11581
|
rejectService = rejectReady;
|
|
9177
11582
|
});
|
|
11583
|
+
const wakeup = new JournalWakeup();
|
|
11584
|
+
let journal;
|
|
9178
11585
|
const server = await startControlServer({
|
|
9179
11586
|
socketPath: paths.socketPath,
|
|
9180
11587
|
service: serviceReady,
|
|
11588
|
+
journal: async () => {
|
|
11589
|
+
if (journal === void 0) throw new Error("Journal runtime is not ready");
|
|
11590
|
+
return journal;
|
|
11591
|
+
},
|
|
9181
11592
|
limits
|
|
9182
11593
|
});
|
|
9183
|
-
let
|
|
11594
|
+
let journalStore;
|
|
9184
11595
|
let service;
|
|
9185
11596
|
try {
|
|
9186
|
-
|
|
9187
|
-
|
|
11597
|
+
journalStore = new WorkerJournalStore(paths.databasePath, {
|
|
11598
|
+
maxPending: limits.maxJournalPendingRecords,
|
|
11599
|
+
checkpointCommitInterval: limits.journalCheckpointCommitInterval,
|
|
11600
|
+
reclaimPagesPerPass: limits.journalReclaimPagesPerPass,
|
|
11601
|
+
onHealthFailure: (error) => {
|
|
11602
|
+
const failure = Object.assign(error, { code: "storage_unavailable" });
|
|
11603
|
+
wakeup.close(failure);
|
|
11604
|
+
service?.failStorage(failure);
|
|
11605
|
+
}
|
|
11606
|
+
});
|
|
11607
|
+
await reconcileObservationContinuity(journalStore, (/* @__PURE__ */ new Date()).toISOString());
|
|
11608
|
+
journal = new JournalRuntimeComposition({
|
|
11609
|
+
store: journalStore,
|
|
11610
|
+
limits,
|
|
11611
|
+
cursors: new OpaqueReceiveCursorCodec(),
|
|
11612
|
+
wakeup,
|
|
11613
|
+
notifyEvents: (agentId, position) => wakeup.notify(agentId, position),
|
|
11614
|
+
failAgent: (agentId, error) => service?.failAgent(agentId, error)
|
|
11615
|
+
});
|
|
11616
|
+
const store = new JournalFleetStoreAdapter(journalStore);
|
|
11617
|
+
const piTarget = await createExternalPiTarget(
|
|
11618
|
+
process.env,
|
|
11619
|
+
limits.maxPiFrameBytes,
|
|
11620
|
+
limits.maxJournalPartialRecordBytes
|
|
11621
|
+
);
|
|
9188
11622
|
service = new FleetService(store, {
|
|
9189
11623
|
launcher: piTarget.launcher,
|
|
9190
11624
|
piIdentity: piTarget.identity,
|
|
9191
|
-
limits
|
|
11625
|
+
limits,
|
|
11626
|
+
journal,
|
|
11627
|
+
journalStore,
|
|
11628
|
+
onAgentDestroyed: (agentId) => {
|
|
11629
|
+
const error = new Error("Agent destroyed");
|
|
11630
|
+
error.code = "agent_destroyed";
|
|
11631
|
+
wakeup.failAgent(agentId, error);
|
|
11632
|
+
}
|
|
9192
11633
|
});
|
|
9193
11634
|
await service.reconcile();
|
|
9194
11635
|
resolveService(service);
|
|
9195
11636
|
} catch (error) {
|
|
9196
11637
|
rejectService(error);
|
|
9197
|
-
|
|
11638
|
+
wakeup.close();
|
|
11639
|
+
await Promise.allSettled([service?.close() ?? Promise.resolve(), server.close()]);
|
|
11640
|
+
await journalStore?.close().catch(() => void 0);
|
|
9198
11641
|
throw error;
|
|
9199
11642
|
}
|
|
11643
|
+
const unavailable = Object.assign(new Error("Runtime unavailable"), {
|
|
11644
|
+
code: "runtime_unavailable"
|
|
11645
|
+
});
|
|
11646
|
+
const drain = new CleanDrainCoordinator({
|
|
11647
|
+
stopAdmission: () => service.beginShutdown(),
|
|
11648
|
+
drainStdoutAndJournal: () => service.drainStdoutAndJournal(),
|
|
11649
|
+
closeReceivers: async () => wakeup.close(unavailable),
|
|
11650
|
+
stopProcessTrees: () => service.stopProcessTrees(),
|
|
11651
|
+
closeServer: () => server.close(),
|
|
11652
|
+
closeStore: () => journalStore.close()
|
|
11653
|
+
});
|
|
9200
11654
|
await new Promise((resolveShutdown) => {
|
|
9201
11655
|
let closing = false;
|
|
9202
11656
|
const shutdown = () => {
|
|
9203
11657
|
if (closing) return;
|
|
9204
11658
|
closing = true;
|
|
9205
|
-
|
|
9206
|
-
const serverClosing = server.close();
|
|
9207
|
-
void Promise.allSettled([serviceClosing, serverClosing]).then(() => store.close(true)).finally(resolveShutdown);
|
|
11659
|
+
void drain.close().finally(resolveShutdown);
|
|
9208
11660
|
};
|
|
9209
11661
|
process.once("SIGTERM", shutdown);
|
|
9210
11662
|
process.once("SIGINT", shutdown);
|
|
9211
11663
|
});
|
|
9212
11664
|
}
|
|
11665
|
+
async function reconcileObservationContinuity(store, now) {
|
|
11666
|
+
for (const incarnation of await store.listActiveIncarnations()) {
|
|
11667
|
+
let gone = incarnation.pid !== null && await waitForProcessGroupExit(incarnation.pid, 0);
|
|
11668
|
+
const epochs = await store.getEpochs(incarnation.agentId);
|
|
11669
|
+
const open2 = epochs.findLast((epoch) => epoch.state === "open");
|
|
11670
|
+
if (open2 !== void 0) {
|
|
11671
|
+
const highWater = await store.getHighWater(incarnation.agentId);
|
|
11672
|
+
await store.putEpoch({
|
|
11673
|
+
...open2,
|
|
11674
|
+
state: "closed",
|
|
11675
|
+
lastSafeEventPosition: highWater?.eventPosition ?? open2.lastSafeEventPosition,
|
|
11676
|
+
closedAt: now,
|
|
11677
|
+
reason: "observation_uncertain"
|
|
11678
|
+
});
|
|
11679
|
+
}
|
|
11680
|
+
if (!gone && incarnation.pid !== null) {
|
|
11681
|
+
signalProcessTree(incarnation.pid, "SIGTERM");
|
|
11682
|
+
gone = await waitForProcessGroupExit(incarnation.pid, 1e3);
|
|
11683
|
+
}
|
|
11684
|
+
if (!gone && incarnation.pid !== null) {
|
|
11685
|
+
signalProcessTree(incarnation.pid, "SIGKILL");
|
|
11686
|
+
gone = await waitForProcessGroupExit(incarnation.pid, 1e3);
|
|
11687
|
+
}
|
|
11688
|
+
await store.putIncarnation({
|
|
11689
|
+
...incarnation,
|
|
11690
|
+
state: gone ? "gone" : "cleanup_uncertain"
|
|
11691
|
+
});
|
|
11692
|
+
const agent = await store.getAgentById(incarnation.agentId);
|
|
11693
|
+
if (agent === null) continue;
|
|
11694
|
+
const wasActive = agent.summary.state === "working" || agent.summary.state === "restoring";
|
|
11695
|
+
await store.putAgent({
|
|
11696
|
+
...agent,
|
|
11697
|
+
summary: {
|
|
11698
|
+
...agent.summary,
|
|
11699
|
+
state: gone && !wasActive ? "idle" : "failed",
|
|
11700
|
+
process: { state: gone ? "absent" : "cleanup_uncertain" },
|
|
11701
|
+
...!gone ? { error: { code: "incarnation_cleanup_uncertain" } } : wasActive ? { error: { code: "runtime_interrupted" } } : { error: void 0 }
|
|
11702
|
+
}
|
|
11703
|
+
});
|
|
11704
|
+
}
|
|
11705
|
+
}
|
|
11706
|
+
function inspectJournalSchema(path) {
|
|
11707
|
+
if (!existsSync(path) || statSync(path).size === 0) return "fresh";
|
|
11708
|
+
let database;
|
|
11709
|
+
try {
|
|
11710
|
+
database = new DatabaseSync2(path, { readOnly: true });
|
|
11711
|
+
} catch (error) {
|
|
11712
|
+
throw new Error("pi-fleet database ownership is uncertain", { cause: error });
|
|
11713
|
+
}
|
|
11714
|
+
try {
|
|
11715
|
+
const table = database.prepare("SELECT type FROM sqlite_master WHERE name = 'schema_migrations'").get();
|
|
11716
|
+
if (table?.type !== "table") throw new Error("pi-fleet database migration ledger is missing");
|
|
11717
|
+
const rows = database.prepare("SELECT version, checksum FROM schema_migrations ORDER BY version").all();
|
|
11718
|
+
const state = classifyJournalMigrationRows(rows);
|
|
11719
|
+
if (state === "fresh") throw new Error("pi-fleet database migration ledger is empty");
|
|
11720
|
+
return state;
|
|
11721
|
+
} finally {
|
|
11722
|
+
database.close();
|
|
11723
|
+
}
|
|
11724
|
+
}
|
|
11725
|
+
async function assertLegacyProcessTreesAbsent(path) {
|
|
11726
|
+
if (!existsSync(path)) return;
|
|
11727
|
+
const database = new DatabaseSync2(path, { readOnly: true });
|
|
11728
|
+
let pids = [];
|
|
11729
|
+
try {
|
|
11730
|
+
const rows = database.prepare(
|
|
11731
|
+
`SELECT json_extract(data_json, '$.pid') AS pid
|
|
11732
|
+
FROM incarnations
|
|
11733
|
+
WHERE state IN ('starting', 'live', 'stopping', 'cleanup_uncertain')`
|
|
11734
|
+
).all();
|
|
11735
|
+
if (rows.some(
|
|
11736
|
+
(row) => typeof row.pid !== "number" || !Number.isSafeInteger(row.pid) || row.pid <= 0
|
|
11737
|
+
)) {
|
|
11738
|
+
throw new Error("Legacy Pi process ownership is uncertain");
|
|
11739
|
+
}
|
|
11740
|
+
pids = rows.map((row) => row.pid);
|
|
11741
|
+
} finally {
|
|
11742
|
+
database.close();
|
|
11743
|
+
}
|
|
11744
|
+
for (const pid of pids) {
|
|
11745
|
+
if (!await waitForProcessGroupExit(pid, 0)) {
|
|
11746
|
+
throw new Error(`Legacy Pi process group ${String(pid)} is still present`);
|
|
11747
|
+
}
|
|
11748
|
+
}
|
|
11749
|
+
assertNoOtherProcessHasDatabaseOpen(path);
|
|
11750
|
+
}
|
|
11751
|
+
function assertNoOtherProcessHasDatabaseOpen(path, procRoot = "/proc") {
|
|
11752
|
+
if (process.platform !== "linux" || !existsSync(path)) {
|
|
11753
|
+
throw new Error("Legacy runtime ownership proof is supported only on Linux");
|
|
11754
|
+
}
|
|
11755
|
+
const databaseIdentity = statSync(path);
|
|
11756
|
+
const currentUid = process.getuid?.();
|
|
11757
|
+
for (const entry of readdirSync2(procRoot)) {
|
|
11758
|
+
if (!/^\d+$/.test(entry) || Number(entry) === process.pid) continue;
|
|
11759
|
+
const processPath = `${procRoot}/${entry}`;
|
|
11760
|
+
let owner;
|
|
11761
|
+
try {
|
|
11762
|
+
owner = statSync(processPath).uid;
|
|
11763
|
+
} catch {
|
|
11764
|
+
continue;
|
|
11765
|
+
}
|
|
11766
|
+
if (currentUid !== void 0 && owner !== currentUid) continue;
|
|
11767
|
+
let descriptors;
|
|
11768
|
+
try {
|
|
11769
|
+
descriptors = readdirSync2(`${processPath}/fd`);
|
|
11770
|
+
} catch (error) {
|
|
11771
|
+
throw new Error(`Cannot prove legacy runtime ${entry} released pi-fleet state`, {
|
|
11772
|
+
cause: error
|
|
11773
|
+
});
|
|
11774
|
+
}
|
|
11775
|
+
for (const descriptor of descriptors) {
|
|
11776
|
+
try {
|
|
11777
|
+
const opened = statSync(`${processPath}/fd/${descriptor}`);
|
|
11778
|
+
if (opened.dev === databaseIdentity.dev && opened.ino === databaseIdentity.ino) {
|
|
11779
|
+
throw new Error(`Legacy runtime process ${entry} still owns the pi-fleet database`);
|
|
11780
|
+
}
|
|
11781
|
+
} catch (error) {
|
|
11782
|
+
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "EBADF")) {
|
|
11783
|
+
continue;
|
|
11784
|
+
}
|
|
11785
|
+
throw error;
|
|
11786
|
+
}
|
|
11787
|
+
}
|
|
11788
|
+
}
|
|
11789
|
+
}
|
|
9213
11790
|
if (process.argv[1] !== void 0 && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
9214
11791
|
await runRuntime();
|
|
9215
11792
|
}
|
|
9216
11793
|
export {
|
|
11794
|
+
assertNoOtherProcessHasDatabaseOpen,
|
|
11795
|
+
inspectJournalSchema,
|
|
11796
|
+
reconcileObservationContinuity,
|
|
9217
11797
|
runRuntime
|
|
9218
11798
|
};
|
|
9219
11799
|
//# sourceMappingURL=runtime.mjs.map
|