pi-squire 0.18.4 → 0.19.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/CHANGELOG.md CHANGED
@@ -6,6 +6,28 @@ follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.19.1] - 2026-11-02
10
+
11
+ ### Removed
12
+
13
+ - Fresh sessions no longer report other sessions' running delegates at
14
+ startup, and the "owner discovery unavailable" and "discovery is limited"
15
+ warnings are gone with that check. A session only ever reports its own tree;
16
+ to collect the delegates of a session you closed, reopen that session. The
17
+ tests that pinned the startup notices now pin that a fresh session reports
18
+ nothing, and the notice-budget tests are removed.
19
+
20
+ ## [0.19.0] - 2026-11-02
21
+
22
+ ### Changed
23
+
24
+ - **Breaking:** Pi 1.5.0 or later is now required. Delegate result expand hints use Pi's
25
+ configured keybinding, and the inline delegate dashboard and new-delegate
26
+ settings form report that Pi is waiting for a choice. The render tests now
27
+ require the configured key in the hint instead of accepting a missing key;
28
+ dashboard and form tests pin their blocked-input status. The active-tools
29
+ test fake now preserves retained tools' order, matching Pi 1.5.
30
+
9
31
  ## [0.18.4] - 2026-11-02
10
32
 
11
33
  ### Fixed
@@ -1072,7 +1094,9 @@ author's dotfiles; the changes below are against that copy.
1072
1094
 
1073
1095
  - The published file list includes `child.ts`, which `index.ts` imports.
1074
1096
 
1075
- [Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.18.4...HEAD
1097
+ [Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.19.1...HEAD
1098
+ [0.19.1]: https://github.com/gvanderclay/pi-squire/compare/v0.19.0...v0.19.1
1099
+ [0.19.0]: https://github.com/gvanderclay/pi-squire/compare/v0.18.4...v0.19.0
1076
1100
  [0.18.4]: https://github.com/gvanderclay/pi-squire/compare/v0.18.3...v0.18.4
1077
1101
  [0.18.3]: https://github.com/gvanderclay/pi-squire/compare/v0.18.2...v0.18.3
1078
1102
  [0.18.2]: https://github.com/gvanderclay/pi-squire/compare/v0.18.1...v0.18.2
package/README.md CHANGED
@@ -10,12 +10,11 @@ answers with one message, which the parent session shows as a result.
10
10
 
11
11
  You need:
12
12
 
13
- - Pi 1.3.0 or later, on macOS or Linux (not Windows). Pi 1.3.0 is the version
14
- pi-squire is tested with. It provides the per-run `before_agent_start`
15
- preparation for idle custom-message results, the structured system-prompt
16
- options the parent guidance uses, and the extension APIs the package relies
17
- on. Pi supplies the peer dependencies `@earendil-works/pi-coding-agent` and
18
- `typebox`.
13
+ - Pi 1.5.0 or later, on macOS or Linux (not Windows). Pi 1.5.0 is the version
14
+ pi-squire is tested with. It provides configured keybinding hints for delegate
15
+ results, blocked-input status for inline choice screens, and the extension
16
+ APIs the package relies on. Pi supplies the peer dependencies
17
+ `@earendil-works/pi-coding-agent` and `typebox`.
19
18
  - tmux 3.2 or later, with the Pi session running in a tmux pane. That is the
20
19
  first version with `new-session -e`. Windows are placed
21
20
  relative to the pane's session (see [Where delegate windows
@@ -324,21 +323,9 @@ one Pi process per session. Ownership refusals name the PID and owner file; if
324
323
  stale/corrupt, verify the original owner has ended, stop affected Squire runtimes
325
324
  and repair only that record. Never remove live lock tickets or force adoption.
326
325
 
327
- Fresh sessions do not adopt earlier trees. At startup a fresh root inspects only
328
- this agent directory's newest **64** registry files, and only entries using its
329
- current session-storage directory. It reports at most **10** direct owners with
330
- positively matched live windows and explains reopening the owning session (or
331
- returning to its existing owner). Registry-content reads are limited to 1 MiB
332
- per tree and one **200 ms total cooperative notice budget**, which starts once
333
- tmux has listed its windows, with registry lock waits capped at the remaining
334
- budget or 20 ms. Discovery runs after `session_start` returns, so it never
335
- delays startup or a session switch; the tmux listing is bounded by the tmux
336
- client's own timeout, and a session that ends first reports nothing. The scan
337
- yields between trees and reports, once, a tmux failure with its message,
338
- unreadable owner records or truncated scope. Individual filesystem operations cannot be preempted.
339
- Directory names/mtimes are enumerated, but no recursive
340
- session search, mailbox search or other agent-directory search occurs. Exact-id
341
- restoration is not restricted by this notice budget.
326
+ Fresh sessions neither adopt nor report earlier trees. Each session sees only
327
+ its own tree; to collect the delegates of a session you closed, reopen that
328
+ exact session, which restores its full tree.
342
329
 
343
330
  Both `delegation_status` and `/delegates` show descendants: a root sees its tree;
344
331
  a coordinator sees only its own subtree, not siblings or unrelated roots.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-squire",
3
- "version": "0.18.4",
3
+ "version": "0.19.1",
4
4
  "description": "Starts a delegate Pi session in a background tmux window, sends it the task over message:*, and shows the parent its result",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -45,15 +45,15 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@earendil-works/pi-ai": "*",
48
- "@earendil-works/pi-coding-agent": ">=1.3.0",
48
+ "@earendil-works/pi-coding-agent": ">=1.5.0",
49
49
  "@earendil-works/pi-tui": "*",
50
50
  "typebox": "*"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@biomejs/biome": "2.5.14",
54
- "@earendil-works/pi-ai": "1.3.0",
55
- "@earendil-works/pi-coding-agent": "1.3.0",
56
- "@earendil-works/pi-tui": "1.3.0",
54
+ "@earendil-works/pi-ai": "1.5.0",
55
+ "@earendil-works/pi-coding-agent": "1.5.0",
56
+ "@earendil-works/pi-tui": "1.5.0",
57
57
  "@types/node": "26.6.5",
58
58
  "pi-session-mail": "0.6.0",
59
59
  "typebox": "1.4.3",
package/src/index.ts CHANGED
@@ -80,7 +80,6 @@ import { registerTaskReplies } from "./task-replies.ts";
80
80
  import { createTmuxClient, type OpenedWindow, requireTmuxVersion, type TmuxClient, WindowOpenError } from "./tmux.ts";
81
81
  import { availableTools } from "./tool-names.ts";
82
82
  import { createTracking, toolResult } from "./tracking.ts";
83
- import { treeNotice } from "./tree-notice.ts";
84
83
  import {
85
84
  assertParentPlacement,
86
85
  bindTree,
@@ -741,15 +740,6 @@ export default function delegate(
741
740
  pi.on("session_start", (_event, ctx) => {
742
741
  viewContext = ctx;
743
742
  });
744
- // Discovery is advisory: it runs after session_start returns, so tmux never delays startup or a session switch.
745
- pi.on("session_start", (_event, ctx) => {
746
- if (!identity.current() && results.list().length === 0) {
747
- const active = () => owner.active();
748
- treeNotice(ctx, tmux, active).catch((error: unknown) => {
749
- if (active()) ctx.ui.notify(`delegate: owner discovery unavailable: ${String(error)}`, "warning");
750
- });
751
- }
752
- });
753
743
  const viewResults = {
754
744
  ...results,
755
745
  list: () => (viewContext ? visibleRecords(results, viewContext) : results.list()),
package/src/navigator.ts CHANGED
@@ -263,6 +263,7 @@ function show(
263
263
  redraw: () => tui.requestRender(),
264
264
  rows: () => tui.terminal.rows,
265
265
  }),
266
+ { blocked: { kind: "question", message: "Choose a delegate action" } },
266
267
  );
267
268
  }
268
269
 
@@ -441,6 +441,7 @@ export async function startNewDelegate(ctx: ExtensionContext, deps: NewDelegateD
441
441
  cwdProblem: (value: string): string | undefined => cwdProblem(value, ctx.cwd),
442
442
  done,
443
443
  }),
444
+ { blocked: { kind: "question", message: "Choose delegate settings" } },
444
445
  );
445
446
  if (chosen === undefined) return;
446
447
  await deps.start({ agent: agent.name, task, ...(label === undefined ? {} : { label }), ...chosen }, ctx);
package/src/render.ts CHANGED
@@ -9,7 +9,6 @@ import { homedir } from "node:os";
9
9
 
10
10
  import {
11
11
  getMarkdownTheme,
12
- keyHint,
13
12
  type MessageRenderer,
14
13
  type MessageRenderOptions,
15
14
  type Theme,
@@ -178,7 +177,7 @@ function idLines(details: MessageDetails): string[] {
178
177
  */
179
178
  export function renderMessage(
180
179
  message: Parameters<MessageRenderer>[0],
181
- { expanded, outputPad }: MessageRenderOptions,
180
+ { expanded, outputPad, keyHint }: MessageRenderOptions,
182
181
  theme: Theme,
183
182
  ): Component | undefined {
184
183
  const details = message.details as MessageDetails | undefined;
@@ -192,7 +191,8 @@ export function renderMessage(
192
191
  box.addChild(expanded ? new Text(header, 0, 0) : new TruncatedText(header));
193
192
  if (details.kind === "result" && details.limit !== undefined)
194
193
  box.addChild(new Text(theme.fg("warning", details.limit), 0, 0));
195
- for (const child of expanded ? expandedLines(details, theme) : collapsedLines(details, theme)) box.addChild(child);
194
+ for (const child of expanded ? expandedLines(details, theme) : collapsedLines(details, theme, keyHint))
195
+ box.addChild(child);
196
196
  return box;
197
197
  }
198
198
 
@@ -209,7 +209,7 @@ function headerLine(details: MessageDetails, status: string, theme: Theme): stri
209
209
  }
210
210
 
211
211
  /** A result's first lines, a count of the rest, and the expand hint. */
212
- function collapsedLines(details: MessageDetails, theme: Theme): Component[] {
212
+ function collapsedLines(details: MessageDetails, theme: Theme, keyHint: MessageRenderOptions["keyHint"]): Component[] {
213
213
  const children: Component[] = [];
214
214
  if (details.kind === "result") {
215
215
  const lines = plainLines(details.body);
@@ -1,7 +1,3 @@
1
- // One registry per root owns structural/admission accounting. Parent-local
2
- // child facts and frozen worker replies remain semantic authorities. No bodies,
3
- // mailboxes, processing inboxes, or ancestor-liveness policy live here.
4
- import { readdirSync } from "node:fs";
5
1
  import { join } from "node:path";
6
2
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
7
3
  import { readChildRecords } from "./child-tasks.ts";
@@ -24,17 +20,6 @@ export type { Reservation, TreeCounts, TreeEntry, TreeIntent, TreeState } from "
24
20
  export function treePath(root: string): string {
25
21
  return join(getAgentDir(), "squire", "trees", `${encodeURIComponent(root)}.json`);
26
22
  }
27
- /** Discovery only; callers choose notice/view scope, never adopt another root. */
28
- export function listTreeRoots(): string[] {
29
- try {
30
- return readdirSync(join(getAgentDir(), "squire", "trees"))
31
- .filter((name) => name.endsWith(".json"))
32
- .map((name) => decodeURIComponent(name.slice(0, -5)));
33
- } catch (error) {
34
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
35
- throw error;
36
- }
37
- }
38
23
  function load(root: string): TreeState {
39
24
  const saved: TreeState | undefined = readState<TreeState>(treePath(root));
40
25
  const state: TreeState = saved === undefined ? { version: 1, root, entries: [] } : saved;
@@ -1,132 +0,0 @@
1
- // Bounded discovery is a notice only. Exact-session restore never uses this budget.
2
- // Callers start it without awaiting, so a slow tmux never holds session_start; the
3
- // tmux client's own timeout bounds the listing, and the budget bounds only the scan.
4
- import { statSync } from "node:fs";
5
- import { performance } from "node:perf_hooks";
6
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
- import { findSession } from "./result-sessions.ts";
8
- import { ownerDetails, sessionIsOwned } from "./session-owner.ts";
9
- import type { TmuxClient, TmuxWindow } from "./tmux.ts";
10
- import { listTreeRoots, readTree, type TreeEntry, treePath } from "./tree-registry.ts";
11
-
12
- export const NOTICE_TREES = 64;
13
- export const NOTICE_OWNERS = 10;
14
- export const NOTICE_MS = 200;
15
- export async function treeNotice(
16
- ctx: ExtensionContext,
17
- tmux: TmuxClient,
18
- active: () => boolean = () => true,
19
- ): Promise<void> {
20
- const session: string = ctx.sessionManager.getSessionId();
21
- const dir: string = ctx.sessionManager.getSessionDir();
22
- const roots: string[] = listTreeRoots().filter((root) => root !== session);
23
- const newest: string[] = newestRoots(roots);
24
- if (!newest.length) return;
25
- const windows: TmuxWindow[] | undefined = await noticeWindows(ctx, tmux, active);
26
- if (!windows || !active()) return;
27
- // The scan budget starts once tmux has answered.
28
- const deadline: number = performance.now() + NOTICE_MS;
29
- const owners: Map<string, TreeEntry> = new Map();
30
- let unavailable = 0,
31
- scanned = 0,
32
- ownerErrors = 0;
33
- for (const root of newest) {
34
- if (!active() || remaining(deadline) <= 0 || owners.size >= NOTICE_OWNERS) break;
35
- try {
36
- collectOwners(root, session, dir, windows, owners, deadline);
37
- } catch {
38
- unavailable++;
39
- }
40
- scanned++;
41
- await new Promise<void>((resolve) => setImmediate(resolve));
42
- }
43
- if (!active()) return;
44
- ownerErrors = reportOwners(ctx, dir, owners, deadline);
45
- if (roots.length > NOTICE_TREES || scanned < newest.length || unavailable || ownerErrors)
46
- ctx.ui.notify(
47
- `delegate: discovery is limited to newest ${NOTICE_TREES} trees and ${NOTICE_OWNERS} owners in this agent/session directory, with a ${NOTICE_MS} ms total budget and 1 MiB per tree; ${unavailable} registries unavailable and ${ownerErrors} owner records unreadable. Reopen an exact owner to restore its full tree.`,
48
- "warning",
49
- );
50
- }
51
- function reportOwners(ctx: ExtensionContext, dir: string, owners: Map<string, TreeEntry>, deadline: number): number {
52
- let ownerErrors = 0;
53
- for (const [parent, entry] of [...owners].slice(0, NOTICE_OWNERS)) {
54
- const path: string | undefined = remaining(deadline) > 0 ? findSession(dir, parent) : undefined;
55
- let action: string;
56
- try {
57
- action = sessionIsOwned(parent)
58
- ? "Its Squire owner is already open; return to that process."
59
- : `Reopen the owning Pi session with pi --session ${shellArg(path ?? parent)} --session-dir ${shellArg(dir)}.`;
60
- } catch {
61
- ownerErrors++;
62
- action = `Owner status is unavailable: ${ownerDetails(parent)}. Inspect it before reopening.`;
63
- }
64
- ctx.ui.notify(
65
- `delegate: earlier session ${parent} has running delegates (tree ${entry.identity.root}). ${action} This session does not adopt them.`,
66
- "info",
67
- );
68
- }
69
- return ownerErrors;
70
- }
71
- /** The newest roots first; each tree is stat'ed once, not on every sort comparison. */
72
- function newestRoots(roots: string[]): string[] {
73
- return roots
74
- .map((root) => ({ root, mtime: modified(root) }))
75
- .sort((a, b) => b.mtime - a.mtime)
76
- .slice(0, NOTICE_TREES)
77
- .map(({ root }) => root);
78
- }
79
- /** tmux's windows, or undefined after warning (while still active) that tmux failed. */
80
- async function noticeWindows(
81
- ctx: ExtensionContext,
82
- tmux: TmuxClient,
83
- active: () => boolean,
84
- ): Promise<TmuxWindow[] | undefined> {
85
- try {
86
- return await tmux.listWindows();
87
- } catch (error) {
88
- if (!active()) return undefined;
89
- const reason: string = error instanceof Error ? error.message : String(error);
90
- ctx.ui.notify(
91
- `delegate: owner discovery unavailable: tmux could not be asked: ${reason}; no absence or ownership was inferred.`,
92
- "warning",
93
- );
94
- return undefined;
95
- }
96
- }
97
- function remaining(deadline: number): number {
98
- return Math.max(0, deadline - performance.now());
99
- }
100
- function collectOwners(
101
- root: string,
102
- session: string,
103
- dir: string,
104
- windows: TmuxWindow[],
105
- owners: Map<string, TreeEntry>,
106
- deadline: number,
107
- ): void {
108
- if (statSync(treePath(root)).size > 1024 * 1024) throw new Error("tree exceeds notice byte budget");
109
- for (const entry of readTree(root, Math.min(20, remaining(deadline))).entries) {
110
- if (owners.size >= NOTICE_OWNERS || remaining(deadline) <= 0) return;
111
- if (entry.identity.parent !== session && entry.sessionDir === dir && live(entry, windows))
112
- owners.set(entry.identity.parent, entry);
113
- }
114
- }
115
- function shellArg(value: string): string {
116
- return `'${value.replaceAll("'", "'\\''")}'`;
117
- }
118
- function modified(root: string): number {
119
- try {
120
- return statSync(treePath(root)).mtimeMs;
121
- } catch {
122
- return 0;
123
- }
124
- }
125
- function live(entry: TreeEntry, windows: TmuxWindow[]): boolean {
126
- return (
127
- entry.phase === "started" &&
128
- !entry.finished &&
129
- !entry.closed &&
130
- windows.some((window) => window.id === entry.windowId && window.name === entry.windowName && !window.exited)
131
- );
132
- }