pi-squire 0.19.0 → 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 +13 -1
- package/README.md +3 -15
- package/package.json +1 -1
- package/src/index.ts +0 -10
- package/src/tree-registry.ts +0 -15
- package/src/tree-notice.ts +0 -132
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,17 @@ 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
|
+
|
|
9
20
|
## [0.19.0] - 2026-11-02
|
|
10
21
|
|
|
11
22
|
### Changed
|
|
@@ -1083,7 +1094,8 @@ author's dotfiles; the changes below are against that copy.
|
|
|
1083
1094
|
|
|
1084
1095
|
- The published file list includes `child.ts`, which `index.ts` imports.
|
|
1085
1096
|
|
|
1086
|
-
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.19.
|
|
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
|
|
1087
1099
|
[0.19.0]: https://github.com/gvanderclay/pi-squire/compare/v0.18.4...v0.19.0
|
|
1088
1100
|
[0.18.4]: https://github.com/gvanderclay/pi-squire/compare/v0.18.3...v0.18.4
|
|
1089
1101
|
[0.18.3]: https://github.com/gvanderclay/pi-squire/compare/v0.18.2...v0.18.3
|
package/README.md
CHANGED
|
@@ -323,21 +323,9 @@ one Pi process per session. Ownership refusals name the PID and owner file; if
|
|
|
323
323
|
stale/corrupt, verify the original owner has ended, stop affected Squire runtimes
|
|
324
324
|
and repair only that record. Never remove live lock tickets or force adoption.
|
|
325
325
|
|
|
326
|
-
Fresh sessions
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
positively matched live windows and explains reopening the owning session (or
|
|
330
|
-
returning to its existing owner). Registry-content reads are limited to 1 MiB
|
|
331
|
-
per tree and one **200 ms total cooperative notice budget**, which starts once
|
|
332
|
-
tmux has listed its windows, with registry lock waits capped at the remaining
|
|
333
|
-
budget or 20 ms. Discovery runs after `session_start` returns, so it never
|
|
334
|
-
delays startup or a session switch; the tmux listing is bounded by the tmux
|
|
335
|
-
client's own timeout, and a session that ends first reports nothing. The scan
|
|
336
|
-
yields between trees and reports, once, a tmux failure with its message,
|
|
337
|
-
unreadable owner records or truncated scope. Individual filesystem operations cannot be preempted.
|
|
338
|
-
Directory names/mtimes are enumerated, but no recursive
|
|
339
|
-
session search, mailbox search or other agent-directory search occurs. Exact-id
|
|
340
|
-
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.
|
|
341
329
|
|
|
342
330
|
Both `delegation_status` and `/delegates` show descendants: a root sees its tree;
|
|
343
331
|
a coordinator sees only its own subtree, not siblings or unrelated roots.
|
package/package.json
CHANGED
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/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
|
-
}
|