pi-bro 0.19.2 → 0.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to pi-bro are documented here.
4
4
 
5
+ ## [0.19.3] - 2026-09-30
6
+
7
+ ### Fixed
8
+
9
+ - Send large Agy explain, Show, and BTW prompts over stdin instead of exceeding argument-size limits. Older Agy versions report an upgrade hint rather than silently falling back (#73).
10
+ - Retain newest turns and message tails when bounding Show and BTW context, preserving JSON message framing (#73).
11
+
5
12
  ## [0.19.2] - 2026-09-30
6
13
 
7
14
  ### Fixed
package/backend.ts CHANGED
@@ -211,8 +211,8 @@ export function parseBtwAgyLine(line: string): { delta?: string; result?: string
211
211
  return { conversationId };
212
212
  }
213
213
 
214
- // explain/show and btw all print the prompt as a single --print argv value and read a stream-json
215
- // result off stdout; only sandbox flag, cwd, timeout, and (btw only) --conversation differ.
214
+ // Explain/show and BTW use argv for small prompts and stdin NDJSON for large
215
+ // prompts; sandbox, cwd, timeout, and continuation semantics stay unchanged.
216
216
  async function executeArgvPrint(
217
217
  request: BackendRequest,
218
218
  selection: AgySelection,
@@ -221,6 +221,9 @@ async function executeArgvPrint(
221
221
  killEscalationMs: number,
222
222
  deadlineMsOverride: number | undefined,
223
223
  ): Promise<BackendOutcome> {
224
+ // Keep legacy CLI compatibility for small prompts; large inputs use the advisor's
225
+ // stdin protocol rather than risking Linux's per-argument byte limit.
226
+ const stdinPrompt = Buffer.byteLength(request.prompt, "utf8") >= 120_000;
224
227
  const isBtw = request.feature === "btw";
225
228
  const full = isBtw && request.access === "workspace-full";
226
229
  const deadlineMs = deadlineMsOverride ?? (isBtw ? (full ? 610_000 : 130_000) : 125_000);
@@ -251,12 +254,11 @@ async function executeArgvPrint(
251
254
  "--print-timeout",
252
255
  printTimeout,
253
256
  ...(request.continuation ? ["--conversation", request.continuation.id] : []),
254
- "--print",
255
- request.prompt,
257
+ ...(stdinPrompt ? ["--input-format", "stream-json"] : ["--print", request.prompt]),
256
258
  ],
257
259
  {
258
260
  cwd: full ? request.cwd : runDirectory,
259
- stdio: ["ignore", "pipe", "pipe"],
261
+ stdio: [stdinPrompt ? "pipe" : "ignore", "pipe", "pipe"],
260
262
  windowsHide: true,
261
263
  detached: process.platform !== "win32",
262
264
  },
@@ -278,6 +280,11 @@ async function executeArgvPrint(
278
280
  processError = error;
279
281
  });
280
282
 
283
+ if (stdinPrompt) {
284
+ child.stdin?.on("error", () => { /* Report early exits through the close/error path. */ });
285
+ child.stdin?.end(`${JSON.stringify({ event: "user", message: { content: request.prompt } })}\n`);
286
+ }
287
+
281
288
  const lines = createInterface({ input: child.stdout!, crlfDelay: Infinity });
282
289
  try {
283
290
  for await (const line of lines) {
@@ -327,7 +334,9 @@ async function executeArgvPrint(
327
334
  if (exitSignal || code === null) {
328
335
  return { status: "failure", message: unexpectedSignalMessage(exitSignal), partialText: partial || undefined };
329
336
  }
330
- if (code !== 0) return { status: "failure", message: agyFailureMessage(action, { code, killed: false, stderr }) };
337
+ if (code !== 0) return { status: "failure", message: stdinPrompt && advisorFlagErrorHint(stderr)
338
+ ? withDoctor("Large prompts require Agy 1.1.15+ with stdin support. Run `agy update`. " + stderr.trim())
339
+ : agyFailureMessage(action, { code, killed: false, stderr }) };
331
340
 
332
341
  const text = final.trim();
333
342
  if (!text) return { status: "failure", message: withDoctor(stderr.trim() || emptyTextMessage) };
package/bro.ts CHANGED
@@ -1475,7 +1475,8 @@ function serializeShowTurns(turns: readonly ShowTurn[]): string {
1475
1475
  return turns.flatMap((turn) => turn.entries).join("\n\n");
1476
1476
  }
1477
1477
 
1478
- export function captureShowTranscript(ctx: ExtensionCommandContext, turnsRequested: number): BroSource | undefined {
1478
+ export function captureShowTranscript(ctx: ExtensionCommandContext, turnsRequested: number, maxLength = MAX_TEXT_LENGTH): BroSource | undefined {
1479
+ if (!Number.isSafeInteger(maxLength) || maxLength < 128) throw new Error("Transcript limit must be an integer of at least 128 characters.");
1479
1480
  const turns: ShowTurn[] = [];
1480
1481
  for (const entry of ctx.sessionManager.getBranch()) {
1481
1482
  if (entry.type !== "message") continue;
@@ -1503,7 +1504,7 @@ export function captureShowTranscript(ctx: ExtensionCommandContext, turnsRequest
1503
1504
  if (seen === 0) return undefined;
1504
1505
 
1505
1506
  let text = serializeShowTurns(turns.slice(start));
1506
- while (text.length > MAX_TEXT_LENGTH && start < turns.length - 1) {
1507
+ while (text.length > maxLength && start < turns.length - 1) {
1507
1508
  let next = turns.length;
1508
1509
  for (let index = start + 1; index < turns.length; index += 1) {
1509
1510
  if (turns[index]!.startsTurn) {
@@ -1515,7 +1516,22 @@ export function captureShowTranscript(ctx: ExtensionCommandContext, turnsRequest
1515
1516
  start = next;
1516
1517
  text = serializeShowTurns(turns.slice(start));
1517
1518
  }
1518
- if (text.length > MAX_TEXT_LENGTH) text = `${text.slice(0, MAX_TEXT_LENGTH)}\n[… transcript truncated …]`;
1519
+ if (text.length > maxLength) {
1520
+ // Prefer the newest complete messages even within an oversized single turn.
1521
+ const entries = turns.slice(start).flatMap(turn => turn.entries);
1522
+ const notice = "[… earlier messages truncated …]\n";
1523
+ while (entries.length > 1 && entries.join("\n\n").length + notice.length > maxLength) entries.shift();
1524
+ text = entries.join("\n\n");
1525
+ if (text.length + notice.length > maxLength) {
1526
+ const newline = text.indexOf("\n");
1527
+ const header = text.slice(0, newline);
1528
+ const content = JSON.parse(text.slice(newline + 1)) as string;
1529
+ const marker = "[… earlier text truncated …] ";
1530
+ let tail = content.slice(-(maxLength - header.length - marker.length - 8));
1531
+ while (`${header}\n${JSON.stringify(marker + tail)}`.length > maxLength) tail = tail.slice(Math.max(1, Math.floor(tail.length / 10)));
1532
+ text = `${header}\n${JSON.stringify(marker + tail)}`;
1533
+ } else text = notice + text;
1534
+ }
1519
1535
  const kept = turns.slice(start).filter((turn) => turn.startsTurn).length;
1520
1536
  return { text: text.trim(), label: `last ${Math.max(1, kept)} turn${kept > 1 ? "s" : ""} · conversation only` };
1521
1537
  }
@@ -3322,10 +3338,7 @@ async function openBtwModal(
3322
3338
  }
3323
3339
  if (thread.turns.length === 0) {
3324
3340
  thread.conversationId = undefined;
3325
- let context = captureShowTranscript(ctx, BTW_CONTEXT_TURNS)?.text;
3326
- if (context && context.length > BTW_CONTEXT_MAX) {
3327
- context = `${context.slice(0, BTW_CONTEXT_MAX)}\n[… context truncated …]`;
3328
- }
3341
+ const context = captureShowTranscript(ctx, BTW_CONTEXT_TURNS, BTW_CONTEXT_MAX)?.text;
3329
3342
  thread.context = context;
3330
3343
  }
3331
3344
  // Resume natively when possible; otherwise seed the fresh native session with the main-session
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bro",
3
- "version": "0.19.2",
3
+ "version": "0.19.3",
4
4
  "description": "An Earendil Pi extension that explains pasted text, assistant responses, local documents, public webpages, and recent session turns (as shapes) in a context-isolated window, opens a sandboxed side conversation with /bro btw, and provides a second-opinion advisor tool for executor agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",