@coreplane/switchboard 1.223.0 → 1.224.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -31681,7 +31681,7 @@ var init_bridge2 = __esm({
31681
31681
  return this.answerText ?? this.heldAnswer;
31682
31682
  }
31683
31683
  observe(event) {
31684
- const out = { replies: [], settled: false, turnEnded: false };
31684
+ const out = { replies: [], settled: false };
31685
31685
  const disposition = PI_EVENT_DISPOSITION[event.type];
31686
31686
  if (disposition === void 0) {
31687
31687
  this.note("harness_error", `pi emitted an event kind this build does not know: ${redactAndCap(event.type, 80)}`);
@@ -31698,9 +31698,6 @@ var init_bridge2 = __esm({
31698
31698
  case "agent_settled":
31699
31699
  out.settled = true;
31700
31700
  break;
31701
- case "turn_end":
31702
- out.turnEnded = true;
31703
- break;
31704
31701
  case "message_start":
31705
31702
  if (isRecord3(event.message) && event.message.role === "assistant") this.assistantStartedAt = this.deps.clock();
31706
31703
  break;
@@ -31893,6 +31890,7 @@ var init_bridge2 = __esm({
31893
31890
  });
31894
31891
 
31895
31892
  // ../../src/core/harness/pi/mirror.ts
31893
+ import { isDeepStrictEqual } from "node:util";
31896
31894
  function partsOf2(content) {
31897
31895
  if (typeof content === "string") return [{ type: "text", text: content }];
31898
31896
  if (!Array.isArray(content)) return [];
@@ -32042,11 +32040,14 @@ var init_mirror = __esm({
32042
32040
  constructor(deps) {
32043
32041
  this.deps = deps;
32044
32042
  this.idx = deps.seedLength;
32043
+ this.mirroredTail = deps.mirroredTail;
32045
32044
  }
32046
32045
  deps;
32047
32046
  idx;
32048
32047
  pendingUser = [];
32049
- seedEchoed = false;
32048
+ /** pi is about to echo the seed's prompt as a user message (see `expectSeedEcho`). */
32049
+ seedEchoPending = false;
32050
+ mirroredTail;
32050
32051
  iteration = 0;
32051
32052
  /** The highest ledger inbox seq folded in so far (run-history item 40). */
32052
32053
  inboxConsumedSeq = 0;
@@ -32054,19 +32055,43 @@ var init_mirror = __esm({
32054
32055
  get wired() {
32055
32056
  return this.deps.onStep !== void 0;
32056
32057
  }
32058
+ /** pi answered the seed's prompt: the first user message without results
32059
+ * that follows is its echo — the seed, on the ledger already — and is not
32060
+ * written twice. A continue prompt (harness-pi item 8) expects none: its
32061
+ * echo is a turn the model was told, folded in like a steer's text. */
32062
+ expectSeedEcho() {
32063
+ this.seedEchoPending = true;
32064
+ }
32065
+ /** The row about to be written, against the transcript's tail — compared
32066
+ * once, with the first row the mirror meets: equal means the ledger holds
32067
+ * it already (read again, harness-pi item 8), together with the results
32068
+ * pending before it, which were its step's user turn. */
32069
+ alreadyHeld(row) {
32070
+ const tail = this.mirroredTail;
32071
+ if (tail === void 0) return false;
32072
+ this.mirroredTail = void 0;
32073
+ if (!isDeepStrictEqual(row, tail)) return false;
32074
+ this.pendingUser = [];
32075
+ return true;
32076
+ }
32077
+ /** Feeds one finished message; answers whether the ledger now holds
32078
+ * everything up to it — a step written, or an assistant turn found already
32079
+ * there — so the harness can move the row's offset past it. A result or a
32080
+ * steer's text waits in memory for the next step, and the answer is no. */
32057
32081
  async onMessage(message3, turn) {
32058
- if (!this.deps.onStep) return;
32082
+ if (!this.deps.onStep) return false;
32059
32083
  const chat = chatMessageOf(message3);
32060
- if (!chat) return;
32061
- if (chat.content.length === 0) return;
32084
+ if (!chat) return false;
32085
+ if (chat.content.length === 0) return false;
32062
32086
  if (chat.role === "user") {
32063
- if (!this.seedEchoed && !chat.content.some((p) => p.type === "tool_result")) {
32064
- this.seedEchoed = true;
32065
- return;
32087
+ if (this.seedEchoPending && !chat.content.some((p) => p.type === "tool_result")) {
32088
+ this.seedEchoPending = false;
32089
+ return false;
32066
32090
  }
32067
32091
  this.pendingUser.push(...chat.content);
32068
- return;
32092
+ return false;
32069
32093
  }
32094
+ if (this.alreadyHeld({ turn: chat })) return true;
32070
32095
  const turns = [];
32071
32096
  if (this.pendingUser.length > 0) turns.push({ role: "user", content: this.pendingUser });
32072
32097
  this.pendingUser = [];
@@ -32083,13 +32108,16 @@ var init_mirror = __esm({
32083
32108
  };
32084
32109
  this.idx += turns.length;
32085
32110
  await this.deps.onStep(report2);
32111
+ return true;
32086
32112
  }
32087
32113
  /** pi compacted (session-log item 6): the results pending since the last
32088
32114
  * assistant turn go as the user turn they would have been, the entry as the
32089
32115
  * row after them — its own step with nothing in flight, so the log holds
32090
- * the summary where pi wrote it and the index moves past it. */
32116
+ * the summary where pi wrote it and the index moves past it. Answers as
32117
+ * `onMessage` does: the ledger holds everything up to the entry. */
32091
32118
  async onCompaction(entry, turn) {
32092
- if (!this.deps.onStep) return;
32119
+ if (!this.deps.onStep) return false;
32120
+ if (this.alreadyHeld({ compaction: entry })) return true;
32093
32121
  const turns = [];
32094
32122
  if (this.pendingUser.length > 0) turns.push({ role: "user", content: this.pendingUser });
32095
32123
  this.pendingUser = [];
@@ -32105,6 +32133,7 @@ var init_mirror = __esm({
32105
32133
  };
32106
32134
  this.idx += turns.length + 1;
32107
32135
  await this.deps.onStep(report2);
32136
+ return true;
32108
32137
  }
32109
32138
  };
32110
32139
  }
@@ -32121,11 +32150,16 @@ var init_transport = __esm({
32121
32150
  constructor(deps) {
32122
32151
  this.deps = deps;
32123
32152
  this.offset = deps.offset ?? 0;
32153
+ this.consumedOffset = this.offset;
32124
32154
  this.lines = this.read();
32125
32155
  }
32126
32156
  deps;
32127
- /** The byte the next read starts at — the re-attach fact. */
32157
+ /** The byte the next read starts at: the read position, moved by whole chunks. */
32128
32158
  offset;
32159
+ /** The boundary after the record last handed out, moved by records: where a
32160
+ * reader that has seen everything so far continues from — the harness's
32161
+ * re-attach fact, taken when nothing handed out is left unwritten. */
32162
+ consumedOffset;
32129
32163
  /** pi was found dead while the harness still listened. */
32130
32164
  exited = false;
32131
32165
  closed = false;
@@ -32178,11 +32212,15 @@ var init_transport = __esm({
32178
32212
  /** Complete lines out of the buffered bytes; the partial tail waits for more. */
32179
32213
  *split(chunk) {
32180
32214
  this.buffer = Buffer.concat([this.buffer, Buffer.from(chunk)]);
32215
+ const base = this.offset - this.buffer.length;
32181
32216
  let start3 = 0;
32182
32217
  for (let i = this.buffer.indexOf(NEWLINE, start3); i >= 0; i = this.buffer.indexOf(NEWLINE, start3)) {
32183
32218
  let line3 = this.buffer.subarray(start3, i).toString("utf8");
32184
32219
  if (line3.endsWith("\r")) line3 = line3.slice(0, -1);
32185
- if (line3.length > 0) yield line3;
32220
+ if (line3.length > 0) {
32221
+ this.consumedOffset = base + i + 1;
32222
+ yield line3;
32223
+ }
32186
32224
  start3 = i + 1;
32187
32225
  }
32188
32226
  this.buffer = this.buffer.subarray(start3);
@@ -32192,6 +32230,7 @@ var init_transport = __esm({
32192
32230
  });
32193
32231
 
32194
32232
  // ../../src/core/harness/pi/harness.ts
32233
+ import { randomUUID as randomUUID2 } from "node:crypto";
32195
32234
  function piHarnessFactsOf(value) {
32196
32235
  if (typeof value !== "object" || value === null) return void 0;
32197
32236
  const v = value;
@@ -32252,6 +32291,13 @@ function settlementResults(settlements) {
32252
32291
  }))
32253
32292
  };
32254
32293
  }
32294
+ function ledgerTailOf(resume) {
32295
+ if (!resume) return void 0;
32296
+ const closing = resume.compactions?.filter((c) => c.before === resume.messages.length).at(-1);
32297
+ if (closing) return { compaction: closing.entry };
32298
+ const last = resume.messages.at(-1);
32299
+ return last?.role === "assistant" ? { turn: last } : void 0;
32300
+ }
32255
32301
  function settlementAnswer(s) {
32256
32302
  return { content: [{ type: "text", text: settlementText(s) }], isError: true };
32257
32303
  }
@@ -32296,10 +32342,12 @@ async function runPiHarness(deps, run2) {
32296
32342
  };
32297
32343
  }
32298
32344
  bridge.turns = run2.resume?.turn ?? 0;
32345
+ const tail = ledgerTailOf(run2.resume);
32299
32346
  const mirror = new PiMirror({
32300
32347
  onStep: run2.onStep,
32301
- seedLength: run2.resume?.messages.length ?? run2.messages.length,
32302
- remainingMs: () => deadline - now()
32348
+ seedLength: run2.resume ? run2.resume.messages.length + (run2.resume.compactions?.length ?? 0) : run2.messages.length,
32349
+ remainingMs: () => deadline - now(),
32350
+ ...tail ? { mirroredTail: tail } : {}
32303
32351
  });
32304
32352
  mirror.inboxConsumedSeq = run2.resume?.inboxConsumedSeq ?? 0;
32305
32353
  let writeUp;
@@ -32336,12 +32384,17 @@ async function runPiHarness(deps, run2) {
32336
32384
  let pid;
32337
32385
  let transport;
32338
32386
  let facts2;
32387
+ let mirrored = 0;
32339
32388
  const save = () => {
32340
32389
  if (facts2) {
32341
- if (transport) facts2 = { ...facts2, logOffset: transport.offset };
32390
+ facts2 = { ...facts2, logOffset: mirrored };
32342
32391
  run2.saveFacts?.(facts2);
32343
32392
  }
32344
32393
  };
32394
+ const held = () => {
32395
+ if (transport) mirrored = transport.consumedOffset;
32396
+ save();
32397
+ };
32345
32398
  let catchingUp = false;
32346
32399
  try {
32347
32400
  const recorded = run2.resume?.facts;
@@ -32368,13 +32421,14 @@ async function runPiHarness(deps, run2) {
32368
32421
  if (reattached && recorded && paths !== void 0) {
32369
32422
  pid = recorded.pid;
32370
32423
  facts2 = { ...recorded, ...recorded.container === void 0 && here !== void 0 ? { container: here } : {} };
32424
+ mirrored = recorded.logOffset;
32371
32425
  transport = new PiRpcTransport({
32372
32426
  container,
32373
32427
  paths,
32374
32428
  pid,
32375
32429
  pollMs: deps.pollMs ?? 750,
32376
32430
  sleep: deps.sleep,
32377
- offset: recorded.logOffset
32431
+ offset: mirrored
32378
32432
  });
32379
32433
  catchingUp = true;
32380
32434
  const inFlight = run2.resume?.settlements.length ?? 0;
@@ -32452,12 +32506,15 @@ async function runPiHarness(deps, run2) {
32452
32506
  save();
32453
32507
  transport = new PiRpcTransport({ container, paths, pid, pollMs: deps.pollMs ?? 750, sleep: deps.sleep });
32454
32508
  }
32455
- transport.send({ id: "retry", type: "set_auto_retry", enabled: false });
32456
- transport.send({ id: "state", type: "get_state" });
32509
+ const stamp = `${now()}-${randomUUID2().slice(0, 8)}`;
32510
+ const ids = { retry: `retry:${stamp}`, state: `state:${stamp}`, prompt: `prompt:${stamp}` };
32511
+ const continuing = reattached || run2.resume !== void 0;
32512
+ transport.send({ id: ids.retry, type: "set_auto_retry", enabled: false });
32513
+ transport.send({ id: ids.state, type: "get_state" });
32457
32514
  if (reattached)
32458
- transport.send({ id: "prompt", type: "prompt", message: CONTINUE_PROMPT, streamingBehavior: "steer" });
32459
- else if (run2.resume) transport.send({ id: "prompt", type: "prompt", message: CONTINUE_PROMPT });
32460
- else transport.send({ id: "prompt", type: "prompt", ...promptOf(run2.messages) });
32515
+ transport.send({ id: ids.prompt, type: "prompt", message: CONTINUE_PROMPT, streamingBehavior: "steer" });
32516
+ else if (run2.resume) transport.send({ id: ids.prompt, type: "prompt", message: CONTINUE_PROMPT });
32517
+ else transport.send({ id: ids.prompt, type: "prompt", ...promptOf(run2.messages) });
32461
32518
  let warned = false;
32462
32519
  let settled = false;
32463
32520
  let stopMode;
@@ -32575,19 +32632,20 @@ ${stagedLine}` : prompt,
32575
32632
  }
32576
32633
  if (obs.response) {
32577
32634
  const r = obs.response;
32578
- if (r.id === "state" && r.success === true && typeof r.data?.sessionFile === "string") {
32635
+ if (r.id === ids.state && r.success === true && typeof r.data?.sessionFile === "string") {
32579
32636
  const data = r.data;
32580
32637
  facts2 = { ...facts2 ?? { pid, logOffset: 0, root: paths.dir }, sessionFile: String(data.sessionFile) };
32581
32638
  save();
32582
32639
  }
32583
- if (r.id === "prompt") {
32640
+ if (r.id === ids.prompt) {
32584
32641
  catchingUp = false;
32585
32642
  if (r.success === false) throw new PromptRefused(String(r.error ?? "no reason"));
32643
+ if (!continuing) mirror.expectSeedEcho();
32586
32644
  }
32587
32645
  }
32588
- if (obs.message) await mirror.onMessage(obs.message, bridge.turns);
32646
+ if (obs.message && await mirror.onMessage(obs.message, bridge.turns)) held();
32589
32647
  if (obs.compaction) {
32590
- await mirror.onCompaction(obs.compaction, bridge.turns);
32648
+ if (await mirror.onCompaction(obs.compaction, bridge.turns)) held();
32591
32649
  if (!writeUp && !hardStopped && !catchingUp) {
32592
32650
  let notepad;
32593
32651
  let unavailable = false;
@@ -32608,7 +32666,6 @@ ${stagedLine}` : prompt,
32608
32666
  note("harness_error", `a model call failed while the bot was away (${obs.providerError}); continuing`);
32609
32667
  else providerError = obs.providerError;
32610
32668
  }
32611
- if (obs.turnEnded) save();
32612
32669
  if (obs.settled && !catchingUp) {
32613
32670
  settled = true;
32614
32671
  break;
@@ -32622,8 +32679,8 @@ ${stagedLine}` : prompt,
32622
32679
  }
32623
32680
  if (bypass) throw bypass;
32624
32681
  if (!settled) {
32625
- const tail = await container.tail(paths.errLog, 2e3);
32626
- throw new Error(`pi exited before the run settled${tail.trim() ? `: ${redactAndCap(tail.trim(), 400)}` : ""}`);
32682
+ const tail2 = await container.tail(paths.errLog, 2e3);
32683
+ throw new Error(`pi exited before the run settled${tail2.trim() ? `: ${redactAndCap(tail2.trim(), 400)}` : ""}`);
32627
32684
  }
32628
32685
  if (providerError !== void 0) throw new Error(`the model call failed: ${providerError}`);
32629
32686
  const text = bridge.answer() ?? "";
@@ -36787,8 +36844,8 @@ var init_types5 = __esm({
36787
36844
  });
36788
36845
 
36789
36846
  // ../../src/core/runLedger/writeThrough.ts
36790
- import { randomUUID as randomUUID2 } from "node:crypto";
36791
- function mintGeneration(now = Date.now, random = randomUUID2) {
36847
+ import { randomUUID as randomUUID3 } from "node:crypto";
36848
+ function mintGeneration(now = Date.now, random = randomUUID3) {
36792
36849
  const stamp = new Date(now()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
36793
36850
  const gen = `${stamp}-${random().replace(/-/g, "").slice(0, 8)}`;
36794
36851
  if (!GEN_PATTERN.test(gen)) throw new Error(`minted generation ${gen} does not match GEN_PATTERN`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coreplane/switchboard",
3
- "version": "1.223.0",
3
+ "version": "1.224.0",
4
4
  "description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://openswitchboard.dev",