@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +99 -167
  3. package/dist/cli-meta.json +313 -315
  4. package/dist/cli.mjs +2504 -1812
  5. package/dist/cli.mjs.map +4 -4
  6. package/dist/client/agent-target.d.ts +33 -0
  7. package/dist/client/contracts.d.ts +81 -0
  8. package/dist/client/fleet-client.d.ts +118 -0
  9. package/dist/client/index.d.ts +4 -0
  10. package/dist/client/sdk-connector.d.ts +7 -0
  11. package/dist/client/sdk-facade.d.ts +84 -0
  12. package/dist/client/sdk-transport.d.ts +24 -0
  13. package/dist/client/shared-client.d.ts +18 -0
  14. package/dist/client/socket-fleet-client.d.ts +21 -0
  15. package/dist/client-meta.json +5736 -0
  16. package/dist/client.mjs +4434 -0
  17. package/dist/client.mjs.map +7 -0
  18. package/dist/installer-meta.json +7 -7
  19. package/dist/installer.mjs +47 -13
  20. package/dist/installer.mjs.map +2 -2
  21. package/dist/journal-sqlite-worker-meta.json +149 -0
  22. package/dist/journal-sqlite-worker.mjs +1110 -0
  23. package/dist/journal-sqlite-worker.mjs.map +7 -0
  24. package/dist/pi/external-installation.d.ts +23 -0
  25. package/dist/platform/client/start-runtime.d.ts +25 -0
  26. package/dist/platform/install/runtime-release.d.ts +8 -0
  27. package/dist/platform/install/tree-integrity.d.ts +7 -0
  28. package/dist/platform/shared/paths.d.ts +8 -0
  29. package/dist/platform/shared/runtime-ownership.d.ts +14 -0
  30. package/dist/protocol/jsonl.d.ts +3 -0
  31. package/dist/protocol/pi-identity.d.ts +16 -0
  32. package/dist/protocol/semantic-segmentation.d.ts +22 -0
  33. package/dist/protocol/version.d.ts +2 -0
  34. package/dist/runtime/semantic-events.d.ts +43 -0
  35. package/dist/runtime-manifest.json +156 -31
  36. package/dist/runtime-meta.json +378 -53
  37. package/dist/runtime.mjs +3161 -581
  38. package/dist/runtime.mjs.map +4 -4
  39. package/dist/shared/result.d.ts +9 -0
  40. package/package.json +14 -1
  41. package/dist/sqlite-worker-meta.json +0 -84
  42. package/dist/sqlite-worker.mjs +0 -359
  43. 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 Boolean(options) {
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 Boolean() : trim === "number" ? yield Number2() : trim === "bigint" ? yield BigInt2() : trim === "string" ? yield String2() : 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: () => 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 stat2 = readFileSync(`/proc/${String(pid)}/stat`, "utf8");
3062
- const commandEnd = stat2.lastIndexOf(")");
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 = stat2.slice(commandEnd + 2).trim().split(/\s+/);
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
- #stdoutBuffer = Buffer.alloc(0);
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((resolve2) => {
3132
- this.#resolveExitHandled = resolve2;
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
- options.onStdoutBytes?.(chunk);
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, { resolve: resolveResponse, reject: rejectResponse, timer });
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.#stdoutBuffer = Buffer.concat([this.#stdoutBuffer, chunk]);
3271
- while (true) {
3272
- const newline = this.#stdoutBuffer.indexOf(10);
3273
- if (newline < 0) {
3274
- if (this.#stdoutBuffer.length > this.#maxStdoutFrameBytes) {
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 = this.#stdoutBuffer.subarray(0, newline);
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
- if (lineBytes.length === 0) continue;
3287
- let frame;
3288
- try {
3289
- const line = new TextDecoder("utf-8", { fatal: true }).decode(lineBytes);
3290
- frame = JSON.parse(line);
3291
- } catch {
3292
- signalProcessTree(this.pid, "SIGTERM");
3293
- return;
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
- #handleExit(code, signal, cause) {
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
- const error = this.#stopping && (code === 0 || signal === "SIGTERM") ? null : cause ?? new Error(`Pi exited unexpectedly (code=${String(code)}, signal=${String(signal)})`);
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, onStdoutBytes) {
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
- ...onStdoutBytes === void 0 ? {} : { onStdoutBytes }
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 { once as once2 } from "node:events";
3475
- import { chmod, lstat, mkdir, unlink } from "node:fs/promises";
3476
- import { createConnection, createServer } from "node:net";
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 = 2;
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
- maxWatchers: 128,
6901
- maxWatchQueuedBytes: MEBIBYTE
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
- maxWatchers: "PIFLEET_MAX_WATCHERS",
6909
- maxWatchQueuedBytes: "PIFLEET_MAX_WATCH_QUEUED_BYTES"
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
- maxWatchers: positiveInteger(env, "maxWatchers"),
6918
- maxWatchQueuedBytes: positiveInteger(env, "maxWatchQueuedBytes")
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/result.ts
6933
- function ok(value) {
6934
- return { ok: true, value };
6935
- }
6936
- function err(error) {
6937
- return { ok: false, error };
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.code = code;
6945
- this.name = "RpcWatchError";
7444
+ this.name = "RuntimeOwnershipBlockedError";
6946
7445
  }
6947
7446
  };
6948
- var RpcWatchHub = class {
6949
- constructor(limits) {
6950
- this.limits = limits;
6951
- }
6952
- #subscriptions = /* @__PURE__ */ new Map();
6953
- #activeIncarnations = /* @__PURE__ */ new Map();
6954
- subscribe(agentName, signal) {
6955
- if (this.#count() >= this.limits.maxWatchers) {
6956
- throw new RpcWatchError("watcher_capacity_exceeded");
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
- closeAll(error = null) {
7003
- for (const agentName of [...this.#subscriptions.keys()]) this.closeAgent(agentName, error);
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
- #publish(agentName, incarnationId, bytes) {
7006
- for (const subscription of [...this.#subscriptions.get(agentName) ?? []]) {
7007
- if (subscription.incarnationId !== incarnationId || subscription.ended) continue;
7008
- if (subscription.queuedBytes + bytes.byteLength > subscription.maxQueuedBytes) {
7009
- this.#finish(subscription, new RpcWatchError("watcher_lagged"));
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
- async *#iterator(subscription) {
7019
- try {
7020
- while (true) {
7021
- while (subscription.queue.length > 0) {
7022
- const bytes = subscription.queue.shift();
7023
- subscription.queuedBytes -= bytes.byteLength;
7024
- yield bytes;
7025
- }
7026
- if (subscription.terminalError !== null) throw subscription.terminalError;
7027
- if (subscription.ended) return;
7028
- await new Promise((resolve2) => {
7029
- subscription.wake = resolve2;
7030
- });
7031
- }
7032
- } finally {
7033
- this.#abort(subscription);
7034
- }
7035
- }
7036
- #endAfterDrain(subscription, error = null) {
7037
- if (subscription.ended) return;
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 mkdir(dirname2(options.socketPath), { recursive: true, mode: 448 });
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 maxFrameBytes = options.limits?.maxProtocolFrameBytes ?? DEFAULT_RUNTIME_LIMITS.maxProtocolFrameBytes;
7079
- const server = createServer((socket) => handleConnection(socket, service, maxFrameBytes));
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 chmod(options.socketPath, 384);
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, maxFrameBytes) {
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((readyService) => dispatch(value, readyService, socket, abort.signal, maxFrameBytes)).catch((error) => {
7107
- if (socket.destroyed) return;
7108
- const invalidRequest = error instanceof InvalidRequestError;
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
- maxFrameBytes
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, maxFrameBytes) {
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
- result = await receiveWithTimeout(
7579
+ await streamReceive(
7580
+ request.requestId,
7581
+ asReceiveInput(request.params),
7158
7582
  service,
7159
- asNamedInput(request.params),
7160
- numberParam(request.params, "timeoutMs"),
7161
- connectionSignal
7583
+ await journal(),
7584
+ socket,
7585
+ connectionSignal,
7586
+ limits
7162
7587
  );
7163
- break;
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(asNamedInput(request.params), requireOperation(operationId));
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
- asNamedInput(request.params),
7600
+ asBoundInput(request.params),
7226
7601
  requireOperation(operationId),
7227
7602
  requirePiIdentity(request.runtime?.pi)
7228
7603
  );
7229
7604
  break;
7230
7605
  }
7231
- writeJsonLine(
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
- return { name: stringParam(params, "name"), message: stringParam(params, "message") };
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 asNamedInput(params) {
7254
- return { name: stringParam(params, "name") };
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
- async function receiveWithTimeout(service, input, timeoutMs, connectionSignal) {
7257
- const abort = new AbortController();
7258
- let timedOut = false;
7259
- const onConnectionAbort = () => abort.abort();
7260
- connectionSignal.addEventListener("abort", onConnectionAbort, { once: true });
7261
- const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => {
7262
- timedOut = true;
7263
- abort.abort();
7264
- }, timeoutMs);
7265
- try {
7266
- return await service.receive(input, abort.signal);
7267
- } catch (error) {
7268
- if (timedOut) {
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 numberParam(params, key) {
7278
- const value = params[key];
7279
- if (value === void 0) return void 0;
7280
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
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 value;
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 stats = await lstat(socketPath).catch((error) => {
7309
- if (error.code === "ENOENT") return null;
7310
- throw error;
7311
- });
7312
- if (stats === null) return;
7313
- if (!stats.isSocket()) throw new Error(`Refusing to replace non-socket path ${socketPath}`);
7314
- if (await canConnect(socketPath))
7315
- throw new Error(`A pi-fleet runtime already owns ${socketPath}`);
7316
- await unlink(socketPath);
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
- send(message) {
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.prompt(message);
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 waitForIdle(signal) {
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((resolve2, reject) => {
7591
- resolveIdle = resolve2;
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 !== null) await wait;
7618
- await this.#lane;
7619
- return this.agent;
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 latestAssistantText = await this.process.getLastAssistantText();
7722
- this.#mayBeWorking = false;
7723
- this.agent = {
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
- await this.store.putAgent(this.agent);
7735
- this.#resolveIdleWaiters();
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.#watchHub = new RpcWatchHub({
7788
- maxWatchers: this.#limits.maxWatchers,
7789
- maxQueuedBytes: this.#limits.maxWatchQueuedBytes
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
- #watchHub;
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
- if (!await this.store.createAgent(agent)) {
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.#remember(operationId, "create", input, result);
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 publishStdout = this.#watchHub.beginIncarnation(input.name, incarnationId);
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
- publishStdout
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, () => coordinator.send(input.instructions));
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.#watchHub.endIncarnation(
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
- await this.#remember(operationId, "create", input, result);
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
- input,
8064
- () => this.#enqueueAgent(input.name, () => this.#sendImpl(input, operationId, callerPiIdentity))
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 publishStdout = this.#watchHub.beginIncarnation(agent.summary.name, incarnationId);
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
- publishStdout
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.#watchHub.endIncarnation(
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 await this.store.listActiveIncarnations()) {
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
- const input = { name: send.agentName, message: send.message };
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 payload = JSON.parse(operation.fingerprint);
9216
+ const target = operation.targetAgent;
8413
9217
  if (operation.method === "create") {
8414
- if (piExecutionFailure === null)
8415
- await this.create(payload, operation.operationId);
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
- await this.destroy(payload, operation.operationId);
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 (piExecutionFailure === null)
8420
- await this.compact(payload, operation.operationId);
8421
- }
8422
- }
8423
- }
8424
- async receive(input, signal) {
8425
- const coordinator = this.#coordinators.get(input.name);
8426
- let agent;
8427
- try {
8428
- agent = coordinator === void 0 ? await this.store.getAgent(input.name) : await coordinator.waitForIdle(signal);
8429
- } catch (error) {
8430
- if (signal?.aborted === true) throw error;
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
- return agent === null ? this.#notFound(input.name) : ok({ schemaVersion: 1, type: "agent.status", agent: agent.summary });
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
- openWatch(input, connectionSignal) {
8476
- if (this.#closing) return Promise.resolve(this.#runtimeUnavailable());
8477
- if (this.#destroyingAgents.has(input.name))
8478
- return Promise.resolve(this.#agentDestroying(input.name));
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.#destroyingAgents.has(input.name)) return this.#agentDestroying(input.name);
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
- try {
8485
- return ok(this.#watchHub.subscribe(input.name, connectionSignal));
8486
- } catch (error) {
8487
- if (error instanceof RpcWatchError && error.code === "watcher_capacity_exceeded") {
8488
- return err({
8489
- code: "capacity_exceeded",
8490
- message: `pi-fleet has reached its ${String(this.#limits.maxWatchers)} watcher limit.`
8491
- });
8492
- }
8493
- throw error;
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) await coordinator.stop("destroy");
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.#watchHub.closeAgent(input.name);
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
- async close() {
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.#watchHub.closeAll(
8556
- new RpcWatchError("runtime_unavailable", "pi-fleet runtime is shutting down")
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 publishStdout = this.#watchHub.beginIncarnation(input.name, incarnationId);
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
- publishStdout
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 === void 0) {
8652
- await this.store.putAgent({
8653
- ...agent,
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.#watchHub.endIncarnation(
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
- const interrupted = coordinator.storedAgent.summary.state === "failed";
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((resolve2) => setTimeout(resolve2, 10));
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
- #agentDestroying(name) {
8811
- return err({ code: "agent_destroying", message: `Agent ${name} is being destroyed.` });
8812
- }
8813
- #notFound(name) {
8814
- return err({ code: "agent_not_found", message: `Agent ${name} was not found.` });
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
- async #rememberCompactFailure(operationId, input, requestedAt, failure) {
8820
- await this.store.putCompact({
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
- #piIdentityFailure(callerIdentity) {
8832
- if (callerIdentity === void 0 || samePiRuntimeIdentity(callerIdentity, this.#piIdentity)) {
8833
- return null;
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 = JSON.stringify(payload);
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 = JSON.stringify(payload);
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 (agent === null) {
8909
- await this.store.deleteOperation(operationId);
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 ?? "pi_start_failed",
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 (agent === null && stored.targetAgent !== void 0) {
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: stored.targetAgent
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 !== JSON.stringify(payload)) {
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 = JSON.stringify(payload);
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/store/worker-store.ts
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
- var WorkerFleetStore = class {
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
- #pending = /* @__PURE__ */ new Map();
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, workerUrl = new URL("./sqlite-worker.mjs", import.meta.url)) {
9060
- this.#worker = new Worker(workerUrl, { workerData: { path } });
9061
- this.#worker.on("message", (value) => {
9062
- if (!isWorkerResponse(value)) {
9063
- this.#fail(new Error("SQLite worker returned a malformed response"));
9064
- void this.#worker.terminate();
9065
- return;
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(`SQLite worker exited unexpectedly with ${code}`));
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
- getAgent(name) {
9082
- return this.#call("getAgent", [name]);
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(agentName) {
9106
- return this.#call("nextSendOrdinal", [agentName]);
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
- deleteAgent(name) {
9130
- return this.#call("deleteAgent", [name]);
11377
+ async putEpoch(epoch) {
11378
+ await this.#call("putEpoch", [epoch]);
9131
11379
  }
9132
- async close(cleanShutdown = true) {
9133
- if (this.#closed) return;
9134
- if (this.#failure === null) await this.#call("close", [cleanShutdown]);
9135
- this.#closed = true;
9136
- await this.#worker.terminate();
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
- #call(method, args) {
9139
- if (this.#closed) return Promise.reject(new Error("pi-fleet store is closed"));
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
- const id = randomUUID3();
9142
- const result = new Promise((resolveCall, rejectCall) => {
9143
- this.#pending.set(id, {
9144
- resolve: (value) => resolveCall(value),
9145
- reject: rejectCall
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({ id, method, args });
11497
+ this.#worker.postMessage(next.request, next.transferList);
9150
11498
  } catch (error) {
9151
- const failure = error instanceof Error ? error : new Error("SQLite worker request failed");
9152
- this.#fail(failure);
11499
+ this.#fail(error instanceof Error ? error : new Error("Journal worker request failed"));
9153
11500
  }
9154
- return result;
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 ??= error;
9158
- for (const pending of this.#pending.values()) pending.reject(this.#failure);
9159
- this.#pending.clear();
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 isWorkerResponse(value) {
9163
- if (typeof value !== "object" || value === null) return false;
9164
- const response = value;
9165
- return typeof response.id === "string" && typeof response.ok === "boolean" && (response.error === void 0 || typeof response.error === "string");
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 store;
11594
+ let journalStore;
9184
11595
  let service;
9185
11596
  try {
9186
- store = new WorkerFleetStore(paths.databasePath);
9187
- const piTarget = await createExternalPiTarget(process.env, limits.maxPiFrameBytes);
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
- await server.close();
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
- const serviceClosing = service.close();
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