@elpapi42/pi-fleet 0.1.0-beta.1 → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runtime.mjs CHANGED
@@ -64,7 +64,7 @@ var PiProcess = class _PiProcess {
64
64
  #responses = /* @__PURE__ */ new Map();
65
65
  #listeners = /* @__PURE__ */ new Set();
66
66
  #exitListeners = /* @__PURE__ */ new Set();
67
- #stdoutBuffer = "";
67
+ #stdoutBuffer = Buffer.alloc(0);
68
68
  #stderr = "";
69
69
  #stopping = false;
70
70
  #maxStdoutFrameBytes;
@@ -80,7 +80,6 @@ var PiProcess = class _PiProcess {
80
80
  stdio: ["pipe", "pipe", "pipe"]
81
81
  }
82
82
  );
83
- this.#child.stdout.setEncoding("utf8");
84
83
  this.#child.stderr.setEncoding("utf8");
85
84
  this.#child.stdout.on("data", (chunk) => this.#consumeStdout(chunk));
86
85
  this.#child.stderr.on("data", (chunk) => {
@@ -136,11 +135,20 @@ var PiProcess = class _PiProcess {
136
135
  const response = new Promise((resolveResponse, rejectResponse) => {
137
136
  const timer = setTimeout(() => {
138
137
  this.#responses.delete(id);
139
- rejectResponse(new Error(`Pi RPC request timed out. stderr: ${this.#stderr}`));
138
+ rejectResponse(new Error("Pi RPC request timed out"));
140
139
  }, timeoutMs);
141
140
  this.#responses.set(id, { resolve: resolveResponse, reject: rejectResponse, timer });
142
141
  });
143
- await this.#write({ ...command, id });
142
+ try {
143
+ await this.#write({ ...command, id });
144
+ } catch (error) {
145
+ const waiter = this.#responses.get(id);
146
+ if (waiter !== void 0) {
147
+ clearTimeout(waiter.timer);
148
+ this.#responses.delete(id);
149
+ waiter.reject(error instanceof Error ? error : new Error("Pi RPC write failed"));
150
+ }
151
+ }
144
152
  const frame = await response;
145
153
  if (frame.success !== true)
146
154
  throw new Error(frame.error ?? `Pi rejected ${String(command.type)}`);
@@ -169,27 +177,29 @@ var PiProcess = class _PiProcess {
169
177
  await once(this.#child.stdin, "drain");
170
178
  }
171
179
  #consumeStdout(chunk) {
172
- this.#stdoutBuffer += chunk;
180
+ this.#stdoutBuffer = Buffer.concat([this.#stdoutBuffer, chunk]);
173
181
  while (true) {
174
- const newline = this.#stdoutBuffer.indexOf("\n");
182
+ const newline = this.#stdoutBuffer.indexOf(10);
175
183
  if (newline < 0) {
176
- if (Buffer.byteLength(this.#stdoutBuffer) > this.#maxStdoutFrameBytes) {
184
+ if (this.#stdoutBuffer.length > this.#maxStdoutFrameBytes) {
177
185
  signalProcessTree(this.pid, "SIGTERM");
178
186
  }
179
187
  return;
180
188
  }
181
- if (Buffer.byteLength(this.#stdoutBuffer.slice(0, newline)) > this.#maxStdoutFrameBytes) {
189
+ if (newline > this.#maxStdoutFrameBytes) {
182
190
  signalProcessTree(this.pid, "SIGTERM");
183
191
  return;
184
192
  }
185
- const line = this.#stdoutBuffer.slice(0, newline).replace(/\r$/, "");
186
- this.#stdoutBuffer = this.#stdoutBuffer.slice(newline + 1);
187
- if (line.length === 0) continue;
193
+ let lineBytes = this.#stdoutBuffer.subarray(0, newline);
194
+ this.#stdoutBuffer = this.#stdoutBuffer.subarray(newline + 1);
195
+ if (lineBytes.at(-1) === 13) lineBytes = lineBytes.subarray(0, -1);
196
+ if (lineBytes.length === 0) continue;
188
197
  let frame;
189
198
  try {
199
+ const line = new TextDecoder("utf-8", { fatal: true }).decode(lineBytes);
190
200
  frame = JSON.parse(line);
191
201
  } catch {
192
- this.#child.kill("SIGTERM");
202
+ signalProcessTree(this.pid, "SIGTERM");
193
203
  return;
194
204
  }
195
205
  if (frame.type === "extension_ui_request" && typeof frame.id === "string" && ["select", "confirm", "input", "editor"].includes(String(frame.method))) {
@@ -6533,12 +6543,12 @@ function handleConnection(socket, service, maxFrameBytes) {
6533
6543
  stopReading();
6534
6544
  void service.then((readyService) => dispatch(value, readyService, socket, abort.signal, maxFrameBytes)).catch((error) => {
6535
6545
  if (socket.destroyed) return;
6536
- const message = error instanceof Error ? error.message : "Internal runtime error";
6546
+ const invalidRequest = error instanceof InvalidRequestError;
6537
6547
  writeJsonLine(socket, {
6538
6548
  v: PROTOCOL_VERSION,
6539
6549
  requestId: requestIdFrom(value),
6540
6550
  ok: false,
6541
- error: { code: "invalid_request", message }
6551
+ error: invalidRequest ? { code: "invalid_request", message: error.message } : { code: "internal_error", message: "Pi Fleet encountered an internal error." }
6542
6552
  });
6543
6553
  socket.end();
6544
6554
  });
@@ -6556,7 +6566,14 @@ function handleConnection(socket, service, maxFrameBytes) {
6556
6566
  );
6557
6567
  }
6558
6568
  async function dispatch(value, service, socket, connectionSignal, maxFrameBytes) {
6559
- const request = parseProtocolRequest(value);
6569
+ let request;
6570
+ try {
6571
+ request = parseProtocolRequest(value);
6572
+ } catch (error) {
6573
+ throw new InvalidRequestError(
6574
+ error instanceof Error ? error.message : "Invalid protocol request"
6575
+ );
6576
+ }
6560
6577
  const operationId = request.operation?.operationId;
6561
6578
  let result;
6562
6579
  switch (request.method) {
@@ -6616,7 +6633,7 @@ async function dispatch(value, service, socket, connectionSignal, maxFrameBytes)
6616
6633
  });
6617
6634
  socket.end();
6618
6635
  }
6619
- } catch (error) {
6636
+ } catch {
6620
6637
  if (!socket.destroyed) {
6621
6638
  writeJsonLine(socket, {
6622
6639
  v: PROTOCOL_VERSION,
@@ -6624,7 +6641,7 @@ async function dispatch(value, service, socket, connectionSignal, maxFrameBytes)
6624
6641
  stream: "error",
6625
6642
  error: {
6626
6643
  code: "session_changed",
6627
- message: error instanceof Error ? error.message : "Session watch failed."
6644
+ message: "Pi session changed while watching."
6628
6645
  }
6629
6646
  });
6630
6647
  socket.end();
@@ -6648,10 +6665,10 @@ function asCreateInput(params) {
6648
6665
  const instructions = params.instructions;
6649
6666
  const piArgv = params.piArgv;
6650
6667
  if (instructions !== void 0 && typeof instructions !== "string") {
6651
- throw new Error("instructions must be a string");
6668
+ throw new InvalidRequestError("instructions must be a string");
6652
6669
  }
6653
6670
  if (!Array.isArray(piArgv) || !piArgv.every((token) => typeof token === "string")) {
6654
- throw new Error("piArgv must be an array of strings");
6671
+ throw new InvalidRequestError("piArgv must be an array of strings");
6655
6672
  }
6656
6673
  return { name, cwd, piArgv, ...instructions === void 0 ? {} : { instructions } };
6657
6674
  }
@@ -6663,13 +6680,17 @@ function asNamedInput(params) {
6663
6680
  }
6664
6681
  async function receiveWithTimeout(service, input, timeoutMs, connectionSignal) {
6665
6682
  const abort = new AbortController();
6683
+ let timedOut = false;
6666
6684
  const onConnectionAbort = () => abort.abort();
6667
6685
  connectionSignal.addEventListener("abort", onConnectionAbort, { once: true });
6668
- const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => abort.abort(), timeoutMs);
6686
+ const timer = timeoutMs === void 0 ? void 0 : setTimeout(() => {
6687
+ timedOut = true;
6688
+ abort.abort();
6689
+ }, timeoutMs);
6669
6690
  try {
6670
6691
  return await service.receive(input, abort.signal);
6671
6692
  } catch (error) {
6672
- if (abort.signal.aborted) {
6693
+ if (timedOut) {
6673
6694
  return err({ code: "timeout", message: "Agent did not become idle before timeout." });
6674
6695
  }
6675
6696
  throw error;
@@ -6682,19 +6703,23 @@ function numberParam(params, key) {
6682
6703
  const value = params[key];
6683
6704
  if (value === void 0) return void 0;
6684
6705
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
6685
- throw new Error(`${key} must be a non-negative number`);
6706
+ throw new InvalidRequestError(`${key} must be a non-negative number`);
6686
6707
  }
6687
6708
  return value;
6688
6709
  }
6689
6710
  function stringParam(params, key) {
6690
6711
  const value = params[key];
6691
- if (typeof value !== "string") throw new Error(`${key} must be a string`);
6712
+ if (typeof value !== "string") throw new InvalidRequestError(`${key} must be a string`);
6692
6713
  return value;
6693
6714
  }
6694
6715
  function requireOperation(operationId) {
6695
- if (operationId === void 0) throw new Error("Mutation requires operation identity");
6716
+ if (operationId === void 0)
6717
+ throw new InvalidRequestError("Mutation requires operation identity");
6696
6718
  return operationId;
6697
6719
  }
6720
+ var InvalidRequestError = class extends Error {
6721
+ name = "InvalidRequestError";
6722
+ };
6698
6723
  function requestIdFrom(value) {
6699
6724
  if (typeof value !== "object" || value === null || !("requestId" in value)) return "unknown";
6700
6725
  return typeof value.requestId === "string" ? value.requestId : "unknown";
@@ -6918,16 +6943,17 @@ var AgentCoordinator = class {
6918
6943
  this.now = now;
6919
6944
  this.onProcessExit = onProcessExit;
6920
6945
  this.#unsubscribeFrame = process2.onFrame((frame) => {
6921
- if (frame.type === "agent_start") void this.#enqueue(() => this.#markWorking());
6922
- if (frame.type === "agent_settled") void this.#enqueue(() => this.#markIdle());
6946
+ if (frame.type === "agent_start") this.#queueEvent(() => this.#markWorking());
6947
+ if (frame.type === "agent_settled") this.#queueEvent(() => this.#markIdle());
6923
6948
  });
6924
6949
  this.#unsubscribeExit = process2.onExit((error) => {
6925
- void this.#enqueue(() => this.#handleProcessExit(error));
6950
+ this.#queueEvent(() => this.#handleProcessExit(error));
6926
6951
  });
6927
6952
  }
6928
6953
  #idleWaiters = /* @__PURE__ */ new Set();
6929
6954
  #lane = Promise.resolve();
6930
6955
  #stopReason = null;
6956
+ #handlingFailure = false;
6931
6957
  #unsubscribeFrame;
6932
6958
  #unsubscribeExit;
6933
6959
  get storedAgent() {
@@ -7101,6 +7127,17 @@ var AgentCoordinator = class {
7101
7127
  }
7102
7128
  this.#idleWaiters.clear();
7103
7129
  }
7130
+ #queueEvent(operation) {
7131
+ void this.#enqueue(operation).catch((error) => this.#handleEventFailure(error));
7132
+ }
7133
+ async #handleEventFailure(error) {
7134
+ if (this.#handlingFailure) return;
7135
+ this.#handlingFailure = true;
7136
+ const failure = error instanceof Error ? error : new Error("Agent coordinator failed");
7137
+ this.#rejectIdleWaiters(failure);
7138
+ await this.process.stop().catch(() => void 0);
7139
+ this.onProcessExit(failure);
7140
+ }
7104
7141
  #enqueue(operation) {
7105
7142
  const result = this.#lane.then(operation, operation);
7106
7143
  this.#lane = result.then(
@@ -7253,6 +7290,7 @@ var FleetService = class {
7253
7290
  #coordinators = /* @__PURE__ */ new Map();
7254
7291
  #watchers = /* @__PURE__ */ new Map();
7255
7292
  #processSlots = /* @__PURE__ */ new Set();
7293
+ #agentLanes = /* @__PURE__ */ new Map();
7256
7294
  #sendLanes = /* @__PURE__ */ new Map();
7257
7295
  #launcher;
7258
7296
  #now;
@@ -7262,7 +7300,7 @@ var FleetService = class {
7262
7300
  operationId,
7263
7301
  "create",
7264
7302
  input,
7265
- () => this.#createImpl(input, operationId)
7303
+ () => this.#enqueueAgent(input.name, () => this.#createImpl(input, operationId))
7266
7304
  );
7267
7305
  }
7268
7306
  async #createImpl(input, operationId) {
@@ -7462,14 +7500,19 @@ var FleetService = class {
7462
7500
  const code = cleanupUncertain ? "incarnation_cleanup_uncertain" : deliveryAmbiguous ? "delivery_uncertain" : "pi_start_failed";
7463
7501
  const result = err({
7464
7502
  code,
7465
- message: error instanceof Error ? error.message : "Pi failed to start."
7503
+ message: code === "delivery_uncertain" ? "Pi may have accepted the initial instructions; Fleet will not replay them automatically." : code === "incarnation_cleanup_uncertain" ? "Fleet could not prove the Pi process group was removed." : "Pi failed to start."
7466
7504
  });
7467
7505
  await this.#remember(operationId, "create", input, result);
7468
7506
  return result;
7469
7507
  }
7470
7508
  }
7471
7509
  send(input, operationId) {
7472
- return this.#runOperation(operationId, "send", input, () => this.#sendImpl(input, operationId));
7510
+ return this.#runOperation(
7511
+ operationId,
7512
+ "send",
7513
+ input,
7514
+ () => this.#enqueueAgent(input.name, () => this.#sendImpl(input, operationId))
7515
+ );
7473
7516
  }
7474
7517
  async #sendImpl(input, operationId) {
7475
7518
  const replay = await this.#operation(operationId, "send", input);
@@ -7569,7 +7612,22 @@ var FleetService = class {
7569
7612
  }
7570
7613
  async receive(input, signal) {
7571
7614
  const coordinator = this.#coordinators.get(input.name);
7572
- const agent = coordinator === void 0 ? await this.store.getAgent(input.name) : await coordinator.waitForIdle(signal);
7615
+ let agent;
7616
+ try {
7617
+ agent = coordinator === void 0 ? await this.store.getAgent(input.name) : await coordinator.waitForIdle(signal);
7618
+ } catch (error) {
7619
+ if (signal?.aborted === true) throw error;
7620
+ if (error instanceof Error && error.message === "Agent destroyed") {
7621
+ return err({ code: "agent_destroyed", message: `Agent ${input.name} was destroyed.` });
7622
+ }
7623
+ if (error instanceof Error && error.message === "Pi work was interrupted") {
7624
+ return err({
7625
+ code: "runtime_interrupted",
7626
+ message: `Agent ${input.name} was interrupted before becoming idle.`
7627
+ });
7628
+ }
7629
+ throw error;
7630
+ }
7573
7631
  if (agent === null) return this.#notFound(input.name);
7574
7632
  if (agent.latestAssistantText === null || agent.responseObservedAt === null) {
7575
7633
  return err({
@@ -7634,7 +7692,7 @@ var FleetService = class {
7634
7692
  operationId,
7635
7693
  "destroy",
7636
7694
  input,
7637
- () => this.#destroyImpl(input, operationId)
7695
+ () => this.#enqueueAgent(input.name, () => this.#destroyImpl(input, operationId))
7638
7696
  );
7639
7697
  }
7640
7698
  async #destroyImpl(input, operationId) {
@@ -7781,7 +7839,7 @@ var FleetService = class {
7781
7839
  agent: { id: agent.summary.id, name: agent.summary.name },
7782
7840
  acceptedAt
7783
7841
  });
7784
- } catch (error) {
7842
+ } catch {
7785
7843
  if (!this.#coordinators.has(input.name)) this.#releaseProcessSlot(input.name);
7786
7844
  if (incarnationId !== null && !this.#coordinators.has(input.name)) {
7787
7845
  await this.store.putIncarnation({
@@ -7801,7 +7859,7 @@ var FleetService = class {
7801
7859
  });
7802
7860
  return err({
7803
7861
  code: "delivery_uncertain",
7804
- message: error instanceof Error ? error.message : "Pi delivery became uncertain."
7862
+ message: "Pi may have accepted the message; Fleet will not replay it automatically."
7805
7863
  });
7806
7864
  }
7807
7865
  }
@@ -7830,6 +7888,9 @@ var FleetService = class {
7830
7888
  this.#coordinators.set(agent.summary.name, coordinator);
7831
7889
  return coordinator;
7832
7890
  }
7891
+ #enqueueAgent(name, operation) {
7892
+ return enqueueNamed(this.#agentLanes, name, operation);
7893
+ }
7833
7894
  #enqueueSend(name, operation) {
7834
7895
  const previous = this.#sendLanes.get(name) ?? Promise.resolve();
7835
7896
  const result = previous.then(operation, operation);
@@ -7955,6 +8016,18 @@ var FleetService = class {
7955
8016
  }
7956
8017
  return null;
7957
8018
  } else {
8019
+ const send = await this.store.getSend(operationId);
8020
+ if (send === null) {
8021
+ await this.store.deleteOperation(operationId);
8022
+ await this.store.putOperation({
8023
+ operationId,
8024
+ method,
8025
+ fingerprint,
8026
+ state: "pending",
8027
+ result: null
8028
+ });
8029
+ return null;
8030
+ }
7958
8031
  return err({
7959
8032
  code: "operation_in_progress",
7960
8033
  message: `Operation ${operationId} is still pending.`
@@ -7998,6 +8071,19 @@ var FleetService = class {
7998
8071
  });
7999
8072
  }
8000
8073
  };
8074
+ function enqueueNamed(lanes, name, operation) {
8075
+ const previous = lanes.get(name) ?? Promise.resolve();
8076
+ const result = previous.then(operation, operation);
8077
+ const settled = result.then(
8078
+ () => void 0,
8079
+ () => void 0
8080
+ );
8081
+ lanes.set(name, settled);
8082
+ void settled.finally(() => {
8083
+ if (lanes.get(name) === settled) lanes.delete(name);
8084
+ });
8085
+ return result;
8086
+ }
8001
8087
  async function* watchWithCleanup(subscription, cleanup) {
8002
8088
  try {
8003
8089
  yield* subscription;
@@ -8013,19 +8099,24 @@ var WorkerFleetStore = class {
8013
8099
  #worker;
8014
8100
  #pending = /* @__PURE__ */ new Map();
8015
8101
  #closed = false;
8102
+ #failure = null;
8016
8103
  constructor(path, workerUrl = new URL("./sqlite-worker.mjs", import.meta.url)) {
8017
8104
  this.#worker = new Worker(workerUrl, { workerData: { path } });
8018
- this.#worker.on("message", (response) => {
8019
- const pending = this.#pending.get(response.id);
8105
+ this.#worker.on("message", (value) => {
8106
+ if (!isWorkerResponse(value)) {
8107
+ this.#fail(new Error("SQLite worker returned a malformed response"));
8108
+ void this.#worker.terminate();
8109
+ return;
8110
+ }
8111
+ const pending = this.#pending.get(value.id);
8020
8112
  if (pending === void 0) return;
8021
- this.#pending.delete(response.id);
8022
- if (response.ok) pending.resolve(response.value);
8023
- else pending.reject(new Error(response.error ?? "SQLite worker failed"));
8113
+ this.#pending.delete(value.id);
8114
+ if (value.ok) pending.resolve(value.value);
8115
+ else pending.reject(new Error(value.error ?? "SQLite worker failed"));
8024
8116
  });
8025
- this.#worker.on("error", (error) => this.#rejectAll(error));
8117
+ this.#worker.on("error", (error) => this.#fail(error));
8026
8118
  this.#worker.on("exit", (code) => {
8027
- if (!this.#closed)
8028
- this.#rejectAll(new Error(`SQLite worker exited unexpectedly with ${code}`));
8119
+ if (!this.#closed) this.#fail(new Error(`SQLite worker exited unexpectedly with ${code}`));
8029
8120
  });
8030
8121
  }
8031
8122
  createAgent(agent) {
@@ -8075,12 +8166,13 @@ var WorkerFleetStore = class {
8075
8166
  }
8076
8167
  async close(cleanShutdown = true) {
8077
8168
  if (this.#closed) return;
8078
- await this.#call("close", [cleanShutdown]);
8169
+ if (this.#failure === null) await this.#call("close", [cleanShutdown]);
8079
8170
  this.#closed = true;
8080
8171
  await this.#worker.terminate();
8081
8172
  }
8082
8173
  #call(method, args) {
8083
8174
  if (this.#closed) return Promise.reject(new Error("Fleet store is closed"));
8175
+ if (this.#failure !== null) return Promise.reject(this.#failure);
8084
8176
  const id = randomUUID3();
8085
8177
  const result = new Promise((resolveCall, rejectCall) => {
8086
8178
  this.#pending.set(id, {
@@ -8088,14 +8180,25 @@ var WorkerFleetStore = class {
8088
8180
  reject: rejectCall
8089
8181
  });
8090
8182
  });
8091
- this.#worker.postMessage({ id, method, args });
8183
+ try {
8184
+ this.#worker.postMessage({ id, method, args });
8185
+ } catch (error) {
8186
+ const failure = error instanceof Error ? error : new Error("SQLite worker request failed");
8187
+ this.#fail(failure);
8188
+ }
8092
8189
  return result;
8093
8190
  }
8094
- #rejectAll(error) {
8095
- for (const pending of this.#pending.values()) pending.reject(error);
8191
+ #fail(error) {
8192
+ this.#failure ??= error;
8193
+ for (const pending of this.#pending.values()) pending.reject(this.#failure);
8096
8194
  this.#pending.clear();
8097
8195
  }
8098
8196
  };
8197
+ function isWorkerResponse(value) {
8198
+ if (typeof value !== "object" || value === null) return false;
8199
+ const response = value;
8200
+ return typeof response.id === "string" && typeof response.ok === "boolean" && (response.error === void 0 || typeof response.error === "string");
8201
+ }
8099
8202
 
8100
8203
  // src/entry/runtime.ts
8101
8204
  async function runRuntime() {