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 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.0...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
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 do not adopt earlier trees. At startup a fresh root inspects only
327
- this agent directory's newest **64** registry files, and only entries using its
328
- current session-storage directory. It reports at most **10** direct owners with
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-squire",
3
- "version": "0.19.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",
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()),
@@ -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
- }