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 +25 -1
- package/README.md +8 -21
- package/package.json +5 -5
- package/src/index.ts +0 -10
- package/src/navigator.ts +1 -0
- package/src/new-delegate.ts +1 -0
- package/src/render.ts +4 -4
- package/src/tree-registry.ts +0 -15
- package/src/tree-notice.ts +0 -132
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.
|
|
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.
|
|
14
|
-
pi-squire is tested with. It provides
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
328
|
-
|
|
329
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
55
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
56
|
-
"@earendil-works/pi-tui": "1.
|
|
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
package/src/new-delegate.ts
CHANGED
|
@@ -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))
|
|
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);
|
package/src/tree-registry.ts
CHANGED
|
@@ -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;
|
package/src/tree-notice.ts
DELETED
|
@@ -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
|
-
}
|