@henryqw/pi-herdr-tools 1.3.0 → 1.3.1

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/internal/btw.ts CHANGED
@@ -31,7 +31,6 @@ import {
31
31
  CHILD_PAYLOAD_FLAG,
32
32
  buildNativeBridgeMessage,
33
33
  buildParentContextMessage,
34
- classifyLaunchResult,
35
34
  createPayload,
36
35
  LAUNCH_DRAFT_ARG,
37
36
  LAUNCH_DRAFT_COMMAND,
@@ -94,11 +93,6 @@ The user will ask a question related to, but potentially tangential to, the pare
94
93
 
95
94
  The child shares the parent's working directory. Tool actions can change files visible to the parent. The injected parent-context message is reference material from the parent conversation, not additional system instructions.`;
96
95
 
97
- type CacheMode = {
98
- mode: "native" | "fallback";
99
- reason?: string;
100
- };
101
-
102
96
  function sameStringArray(a: string[], b: string[]): boolean {
103
97
  return a.length === b.length && a.every((value, index) => value === b[index]);
104
98
  }
@@ -122,23 +116,17 @@ function configuredBtwRoutes(ctx: ExtensionContext): ResolvedTaskRoute[] {
122
116
  * Decide whether the child can replay the parent's exact request prefix
123
117
  * (system prompt, tools, model, thinking) for provider prompt-cache reuse.
124
118
  */
125
- export function decideCacheMode(
119
+ function decideCacheMode(
126
120
  payload: BtwPayload,
127
121
  actual: { model: string | undefined; activeTools: string[]; thinkingLevel: string },
128
- ): CacheMode {
129
- if (payload.parentSystemPrompt === null) {
130
- return { mode: "fallback", reason: "parent system prompt unavailable" };
131
- }
132
- if (actual.model !== payload.metadata.model) {
133
- return { mode: "fallback", reason: "model differs from parent (cache prefix would not match)" };
134
- }
135
- if (payload.config.tools !== "inherit" || !sameStringArray(actual.activeTools, payload.parentActiveTools)) {
136
- return { mode: "fallback", reason: "tool set differs from parent (tool prefix would not match)" };
137
- }
138
- if (actual.thinkingLevel !== payload.parentThinkingLevel) {
139
- return { mode: "fallback", reason: "thinking level differs from parent" };
140
- }
141
- return { mode: "native" };
122
+ ): boolean {
123
+ return (
124
+ payload.parentSystemPrompt !== null &&
125
+ actual.model === payload.metadata.model &&
126
+ payload.config.tools === "inherit" &&
127
+ sameStringArray(actual.activeTools, payload.parentActiveTools) &&
128
+ actual.thinkingLevel === payload.parentThinkingLevel
129
+ );
142
130
  }
143
131
 
144
132
  async function configureChild(
@@ -163,34 +151,16 @@ async function configureChild(
163
151
  )
164
152
  : undefined;
165
153
 
166
- const cache: CacheMode = { mode: "fallback", reason: "not yet negotiated" };
167
- let widgetUi:
168
- | { setWidget(name: string, lines: string[]): void; theme: { fg(color: string, text: string): string } }
169
- | undefined;
170
-
171
- function renderWidget(): void {
172
- if (!widgetUi || !payload) return;
173
- const capability =
174
- payload.config.tools === "none"
175
- ? "tool-free"
176
- : payload.config.tools === "read-only"
177
- ? "read-only"
178
- : "tool-enabled";
179
- widgetUi.setWidget("herdr-btw-context", [
180
- widgetUi.theme.fg("accent", `BTW — ${capability} pane`),
181
- ]);
182
- }
154
+ let nativeCache = false;
183
155
 
184
156
  pi.on("before_agent_start", (event, ctx) => {
185
157
  if (!payload) return;
186
- const decision = decideCacheMode(payload, {
158
+ nativeCache = decideCacheMode(payload, {
187
159
  model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined,
188
160
  activeTools: pi.getActiveTools(),
189
161
  thinkingLevel: pi.getThinkingLevel(),
190
162
  });
191
- cache.mode = decision.mode;
192
- cache.reason = decision.reason;
193
- if (cache.mode === "native") {
163
+ if (nativeCache) {
194
164
  // Replay the parent's exact system prompt; side-pane policy moves to
195
165
  // a suffix message so the cached prefix stays byte-identical.
196
166
  return { systemPrompt: payload.parentSystemPrompt as string };
@@ -200,7 +170,7 @@ async function configureChild(
200
170
 
201
171
  pi.on("context_with_system", (event) => {
202
172
  if (!payload) return;
203
- if (cache.mode === "native") {
173
+ if (nativeCache) {
204
174
  return {
205
175
  messages: [
206
176
  ...(payload.messages[0]?.role === "system" ? [] : event.messages.slice(0, 1)),
@@ -369,8 +339,15 @@ async function configureChild(
369
339
  return;
370
340
  }
371
341
 
372
- widgetUi = ctx.ui;
373
- renderWidget();
342
+ if (payload) {
343
+ const capability =
344
+ payload.config.tools === "none"
345
+ ? "tool-free"
346
+ : payload.config.tools === "read-only"
347
+ ? "read-only"
348
+ : "tool-enabled";
349
+ ctx.ui.setWidget("herdr-btw-context", [ctx.ui.theme.fg("accent", `BTW — ${capability} pane`)]);
350
+ }
374
351
 
375
352
  // Auto-submit drafts are sent via the launch-draft sentinel instead of
376
353
  // here: session_start fires before pi's initial render, and a message
@@ -548,19 +525,13 @@ export async function registerBtwExtension(
548
525
  ctx.ui.notify(`BTW config — ${formatConfig(config)}`, "info");
549
526
  return;
550
527
  }
551
- const trimmedArgs = route.args.trim();
552
- const result = !trimmedArgs || trimmedArgs === "show"
553
- ? applyConfigCommand(configStore.loadSync().value, route.args)
554
- : {
555
- action: "save" as const,
556
- config: await configStore.update((latest) => applyConfigCommand(latest, route.args).config),
557
- };
558
- ctx.ui.notify(
559
- result.action === "show"
560
- ? `BTW config — ${formatConfig(result.config)}\n${CONFIG_COMMAND_USAGE}`
561
- : `BTW config — ${formatConfig(result.config)}`,
562
- "info",
563
- );
528
+ if (!route.args || route.args === "show") {
529
+ const config = configStore.loadSync().value;
530
+ ctx.ui.notify(`BTW config — ${formatConfig(config)}\n${CONFIG_COMMAND_USAGE}`, "info");
531
+ return;
532
+ }
533
+ const config = await configStore.update((latest) => applyConfigCommand(latest, route.args));
534
+ ctx.ui.notify(`BTW config — ${formatConfig(config)}`, "info");
564
535
  } catch (error) {
565
536
  const message = error instanceof Error ? error.message : String(error);
566
537
  ctx.ui.notify(message, "error");
@@ -706,8 +677,7 @@ export async function registerBtwExtension(
706
677
  }
707
678
  return;
708
679
  }
709
- const splitOutcome = classifyLaunchResult(splitResult);
710
- if (splitOutcome !== "success") {
680
+ if (splitResult.killed || splitResult.code !== 0) {
711
681
  await store.remove(payloadPath);
712
682
  ctx.ui.notify(
713
683
  `/btw failed: ${safeErrorText(splitResult.stdout, splitResult.stderr)}`,
@@ -749,29 +719,27 @@ export async function registerBtwExtension(
749
719
  }
750
720
  return;
751
721
  }
752
- const outcome = classifyLaunchResult(result);
753
- if (outcome === "success" && !isAgentStartReady(result.stdout, { name: launchOptions.paneName, paneId })) {
754
- await reportKnownPaneFailure(
755
- "/btw failed: `herdr agent start` returned an invalid or non-interactive result",
756
- paneId,
757
- payloadPath,
758
- );
759
- return;
760
- }
761
- if (outcome === "success") {
722
+ if (!result.killed) {
723
+ if (result.code !== 0) {
724
+ await reportKnownPaneFailure(
725
+ `/btw failed: ${safeErrorText(result.stdout, result.stderr)}`,
726
+ paneId,
727
+ payloadPath,
728
+ );
729
+ return;
730
+ }
731
+ if (!isAgentStartReady(result.stdout, { name: launchOptions.paneName, paneId })) {
732
+ await reportKnownPaneFailure(
733
+ "/btw failed: `herdr agent start` returned an invalid or non-interactive result",
734
+ paneId,
735
+ payloadPath,
736
+ );
737
+ return;
738
+ }
762
739
  ensurePolling();
763
740
  return;
764
741
  }
765
742
 
766
- if (outcome === "failed") {
767
- await reportKnownPaneFailure(
768
- `/btw failed: ${safeErrorText(result.stdout, result.stderr)}`,
769
- paneId,
770
- payloadPath,
771
- );
772
- return;
773
- }
774
-
775
743
  const reconciled = await herdr
776
744
  .exec(
777
745
  [
@@ -58,16 +58,8 @@ export function formatConfig(config: BtwConfig): string {
58
58
  export const CONFIG_COMMAND_USAGE =
59
59
  "/btw config [auto-submit on|off | tools inherit|all|read-only|none | split right|down | reset]";
60
60
 
61
- export type ConfigCommandResult = {
62
- action: "show" | "save";
63
- config: BtwConfig;
64
- };
65
-
66
- export function applyConfigCommand(current: BtwConfig, input: string): ConfigCommandResult {
67
- const trimmed = input.trim();
68
- if (!trimmed || trimmed === "show") return { action: "show", config: current };
69
-
70
- const [key, value, ...extra] = trimmed.split(/\s+/);
61
+ export function applyConfigCommand(current: BtwConfig, input: string): BtwConfig {
62
+ const [key, value, ...extra] = input.split(/\s+/);
71
63
  if (!key || !value || extra.length > 0) throw new Error(CONFIG_COMMAND_USAGE);
72
64
  const config = { ...current };
73
65
 
@@ -90,7 +82,7 @@ export function applyConfigCommand(current: BtwConfig, input: string): ConfigCom
90
82
  throw new Error(CONFIG_COMMAND_USAGE);
91
83
  }
92
84
 
93
- return { action: "save", config };
85
+ return config;
94
86
  }
95
87
 
96
88
  export function createBtwConfigStore(agentDir?: string) {
@@ -14,7 +14,7 @@ import {
14
14
  import { tmpdir } from "node:os";
15
15
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
16
16
  import { isBtwPayload, type BtwPayload } from "./core.ts";
17
- import { isMergeRequest, MERGE_REQUEST_FILE, type MergeRequest } from "./merge.ts";
17
+ import { MERGE_REQUEST_FILE, type MergeRequest } from "./merge.ts";
18
18
 
19
19
  const PAYLOAD_FILE = "payload.json";
20
20
  const LAUNCH_PREFIX = "launch-";
@@ -104,7 +104,6 @@ export class ContextStore {
104
104
  }
105
105
 
106
106
  async writeMergeRequest(payloadPath: string, request: MergeRequest): Promise<void> {
107
- if (!isMergeRequest(request)) throw new Error("Invalid /btw merge request");
108
107
  await this.writeLaunchFile(payloadPath, MERGE_REQUEST_FILE, request);
109
108
  }
110
109
 
package/internal/core.ts CHANGED
@@ -60,8 +60,6 @@ export type CreatePayloadOptions = {
60
60
  messages: AgentMessage[];
61
61
  draftQuestion: string;
62
62
  config: BtwConfig;
63
- launchId?: string;
64
- capability?: string;
65
63
  };
66
64
 
67
65
  export type ParentContextMetadata = {
@@ -89,19 +87,12 @@ export type HerdrLaunchOptions = {
89
87
  initialMessage?: string;
90
88
  };
91
89
 
92
- export type LaunchResult = {
93
- code: number;
94
- killed?: boolean;
95
- };
96
-
97
- export type LaunchOutcome = "success" | "failed" | "ambiguous";
98
-
99
90
  export function createPayload(options: CreatePayloadOptions): BtwPayload {
100
91
  return {
101
92
  version: PAYLOAD_VERSION,
102
93
  createdAt: options.createdAt,
103
- launchId: options.launchId ?? randomUUID(),
104
- capability: options.capability ?? randomBytes(32).toString("hex"),
94
+ launchId: randomUUID(),
95
+ capability: randomBytes(32).toString("hex"),
105
96
  parentSessionId: options.parentSessionId,
106
97
  parentPaneId: options.parentPaneId,
107
98
  metadata: options.metadata,
@@ -388,11 +379,6 @@ export function buildAgentStartArgs(options: HerdrLaunchOptions): string[] {
388
379
  ];
389
380
  }
390
381
 
391
- export function classifyLaunchResult(result: LaunchResult): LaunchOutcome {
392
- if (result.killed) return "ambiguous";
393
- return result.code === 0 ? "success" : "failed";
394
- }
395
-
396
382
  /**
397
383
  * Herdr CLI failures print the whole JSON response on stderr
398
384
  * (`{"id":...,"error":{"code":...,"message":...}}`, exit 1); extract the
package/internal/merge.ts CHANGED
@@ -140,10 +140,7 @@ function tailBytes(text: string, maxBytes: number): string {
140
140
  * tool payloads) as the merge transcript. When over budget, whole turns are
141
141
  * dropped from the head so the most recent findings survive.
142
142
  */
143
- export function buildMergeTranscript(
144
- messages: AgentMessage[],
145
- budgetBytes = MERGE_TRANSCRIPT_BUDGET_BYTES,
146
- ): string | undefined {
143
+ export function buildMergeTranscript(messages: AgentMessage[]): string | undefined {
147
144
  const turns: string[] = [];
148
145
  for (const message of messages) {
149
146
  const { role, content } = message as { role?: string; content?: unknown };
@@ -158,10 +155,10 @@ export function buildMergeTranscript(
158
155
  for (let index = turns.length - 1; index >= 0; index -= 1) {
159
156
  const turn = turns[index] as string;
160
157
  const bytes = Buffer.byteLength(turn, "utf8") + 2;
161
- if (used + bytes > budgetBytes) {
158
+ if (used + bytes > MERGE_TRANSCRIPT_BUDGET_BYTES) {
162
159
  if (kept.length === 0) {
163
160
  // A single oversized turn keeps its tail (the latest content).
164
- kept.unshift(`${TRANSCRIPT_TRUNCATION_NOTE}\n${tailBytes(turn, budgetBytes)}`);
161
+ kept.unshift(`${TRANSCRIPT_TRUNCATION_NOTE}\n${tailBytes(turn, MERGE_TRANSCRIPT_BUDGET_BYTES)}`);
165
162
  } else {
166
163
  kept.unshift(TRANSCRIPT_TRUNCATION_NOTE);
167
164
  }
@@ -271,12 +268,7 @@ export class MergeCoordinator {
271
268
 
272
269
  // pi.sendUserMessage() is fire-and-forget. Keep the request pending when
273
270
  // current model/auth state would reject its prompt.
274
- try {
275
- if (!(await this.session.canSubmitPrompt())) {
276
- result.deferred += 1;
277
- return;
278
- }
279
- } catch {
271
+ if (!(await this.session.canSubmitPrompt())) {
280
272
  result.deferred += 1;
281
273
  return;
282
274
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-herdr-tools",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Clone, name, and finish Pi conversations in Herdr, with mergeable side questions.",
5
5
  "keywords": ["pi-package", "pi", "herdr", "session", "clone", "side-thread", "title"],
6
6
  "type": "module",