pi-sessions 0.9.0 → 0.10.0

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.
@@ -0,0 +1,182 @@
1
+ import type { ExtensionAPI, SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ import { SUBAGENT_LAUNCH } from "../session-handoff/launch-target.ts";
3
+ import { findCurrentHandoffBootstrap, type HandoffSubagent } from "../session-handoff/metadata.ts";
4
+ import { hasAttachedTmuxClients, tmuxSessionName } from "../shared/tmux.ts";
5
+ import { isRunningSubagentState } from "./classify.ts";
6
+ import {
7
+ getChildSubagentLifecycle,
8
+ SUBAGENT_CLOSED_CUSTOM_TYPE,
9
+ SUBAGENT_REPORT_REMINDER_MESSAGE_CUSTOM_TYPE,
10
+ } from "./ledger.ts";
11
+ import type { ReconcileResult, SubagentReconciler } from "./reconcile.ts";
12
+
13
+ const LINGER_POLL_MS = 1_000;
14
+ const OWNED_SUBAGENT_POLL_MS = 10_000;
15
+
16
+ export interface SubagentChildSessionState {
17
+ identity: HandoffSubagent;
18
+ requestResponse: boolean;
19
+ reportsAtTurnStart: number;
20
+ }
21
+
22
+ export interface SettledChildParentSession {
23
+ epoch: number;
24
+ getBranch(): readonly SessionEntry[];
25
+ hasPendingMessages(): boolean;
26
+ isIdle(): boolean;
27
+ shutdown(): void;
28
+ }
29
+
30
+ export interface SettledChildLifecycle {
31
+ cancel(): void;
32
+ settle(
33
+ parent: SettledChildParentSession,
34
+ child: SubagentChildSessionState,
35
+ reconciliation: ReconcileResult | undefined,
36
+ ): Promise<void>;
37
+ }
38
+
39
+ export function createSettledChildLifecycle(
40
+ pi: ExtensionAPI,
41
+ reconciler: SubagentReconciler,
42
+ isCurrentSession: (epoch: number) => boolean,
43
+ ): SettledChildLifecycle {
44
+ let generation = 0;
45
+ let timer: NodeJS.Timeout | undefined;
46
+
47
+ const cancel = (): void => {
48
+ generation += 1;
49
+ if (timer) {
50
+ clearTimeout(timer);
51
+ timer = undefined;
52
+ }
53
+ };
54
+
55
+ return {
56
+ cancel,
57
+ async settle(parent, child, reconciliation) {
58
+ cancel();
59
+ const currentGeneration = generation;
60
+ let phase: "owned-subagents" | "settle" | "observer" =
61
+ reconciliation && hasRunningOwnedSubagents(reconciliation) ? "owned-subagents" : "settle";
62
+ const canAdvance = (): boolean =>
63
+ generation === currentGeneration &&
64
+ isCurrentSession(parent.epoch) &&
65
+ !parent.hasPendingMessages() &&
66
+ parent.isIdle();
67
+ const schedule = (delayMs: number): void => {
68
+ timer = setTimeout(() => {
69
+ timer = undefined;
70
+ void advance();
71
+ }, delayMs);
72
+ };
73
+ const advance = async (): Promise<void> => {
74
+ if (!canAdvance()) {
75
+ return;
76
+ }
77
+
78
+ if (phase === "owned-subagents") {
79
+ const result = await reconciler.reconcile();
80
+ if (!canAdvance()) {
81
+ return;
82
+ }
83
+ if (hasRunningOwnedSubagents(result)) {
84
+ schedule(OWNED_SUBAGENT_POLL_MS);
85
+ return;
86
+ }
87
+ phase = "settle";
88
+ }
89
+
90
+ if (phase === "settle") {
91
+ if (!settleChild(pi, child, parent.getBranch())) {
92
+ return;
93
+ }
94
+ phase = "observer";
95
+ }
96
+
97
+ let attached = false;
98
+ try {
99
+ attached = await hasAttachedTmuxClients(
100
+ pi,
101
+ tmuxSessionName(child.identity.ownerSessionId),
102
+ );
103
+ } catch {
104
+ attached = false;
105
+ }
106
+ if (!canAdvance()) {
107
+ return;
108
+ }
109
+ if (!attached) {
110
+ parent.shutdown();
111
+ return;
112
+ }
113
+ schedule(LINGER_POLL_MS);
114
+ };
115
+
116
+ if (phase === "owned-subagents") {
117
+ schedule(OWNED_SUBAGENT_POLL_MS);
118
+ return;
119
+ }
120
+ await advance();
121
+ },
122
+ };
123
+ }
124
+
125
+ export function countSubagentReports(branch: readonly SessionEntry[]): number {
126
+ return getChildSubagentLifecycle(branch).reports.length;
127
+ }
128
+
129
+ export function findSelfSubagentIdentity(
130
+ sessionId: string,
131
+ branch: readonly SessionEntry[],
132
+ ): HandoffSubagent | undefined {
133
+ const bootstrap = findCurrentHandoffBootstrap(branch);
134
+ if (
135
+ bootstrap?.launch !== SUBAGENT_LAUNCH ||
136
+ bootstrap.sessionId !== sessionId ||
137
+ bootstrap.subagent.childSessionId !== sessionId
138
+ ) {
139
+ return undefined;
140
+ }
141
+ return bootstrap.subagent;
142
+ }
143
+
144
+ function hasRunningOwnedSubagents(result: ReconcileResult): boolean {
145
+ return [...result.states.values()].some(isRunningSubagentState);
146
+ }
147
+
148
+ function settleChild(
149
+ pi: ExtensionAPI,
150
+ child: SubagentChildSessionState,
151
+ branch: readonly SessionEntry[],
152
+ ): boolean {
153
+ const lifecycle = getChildSubagentLifecycle(branch);
154
+ const reports = lifecycle.reports.length;
155
+ if (reports > child.reportsAtTurnStart) {
156
+ return true;
157
+ }
158
+ if (!child.requestResponse) {
159
+ pi.appendEntry(SUBAGENT_CLOSED_CUSTOM_TYPE, {
160
+ reason: "no_response_expected",
161
+ });
162
+ return true;
163
+ }
164
+
165
+ if (lifecycle.hasReminder) {
166
+ pi.appendEntry(SUBAGENT_CLOSED_CUSTOM_TYPE, {
167
+ reason: "no_report_after_reminder",
168
+ });
169
+ return true;
170
+ }
171
+
172
+ pi.sendMessage(
173
+ {
174
+ customType: SUBAGENT_REPORT_REMINDER_MESSAGE_CUSTOM_TYPE,
175
+ content:
176
+ "[system] Your delegated turn settled without a task report. Call submit_task_report now with done, blocked, or incomplete status.",
177
+ display: true,
178
+ },
179
+ { triggerTurn: true },
180
+ );
181
+ return false;
182
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sessions",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "Pi session search, ask, handoff, messaging, auto-titling, and indexing tools",
6
6
  "license": "MIT",
@@ -19,6 +19,7 @@
19
19
  "node": ">=24 <26"
20
20
  },
21
21
  "files": [
22
+ "dist",
22
23
  "extensions",
23
24
  "images"
24
25
  ],
@@ -28,12 +29,15 @@
28
29
  ]
29
30
  },
30
31
  "scripts": {
32
+ "build:broker": "tsc --project tsconfig.broker.json",
31
33
  "lint": "biome check .",
32
34
  "format": "biome format --write .",
33
35
  "typecheck": "tsc --noEmit",
36
+ "pretest": "npm run build:broker",
34
37
  "test": "vitest run",
35
- "check": "biome check . && tsc --noEmit && vitest run",
36
- "prepare": "husky || true"
38
+ "check": "biome check . && tsc --noEmit && npm test",
39
+ "prepare": "npm run build:broker && npm run prepare:husky",
40
+ "prepare:husky": "husky || true"
37
41
  },
38
42
  "dependencies": {
39
43
  "typebox": "^1.1.38"