pi-sessions 0.14.1 → 0.14.2
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.
|
@@ -2,6 +2,7 @@ import type { ExecOptions, ExecResult } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
|
|
3
3
|
const TMUX_COMMAND = "tmux";
|
|
4
4
|
const TMUX_TIMEOUT_MS = 15_000;
|
|
5
|
+
const CREATE_WINDOW_ATTEMPTS = 3;
|
|
5
6
|
const SESSION_ID_OPTION = "@pi_session_id";
|
|
6
7
|
const WINDOW_FORMAT = "#{window_id}\t#{window_name}\t#{@pi_session_id}";
|
|
7
8
|
|
|
@@ -103,15 +104,31 @@ export async function createTmuxWindow(
|
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
// The server can exit between any two commands below; the next attempt starts a fresh one.
|
|
106
108
|
async function createWindow(
|
|
107
109
|
executor: TmuxExecutor,
|
|
108
110
|
options: CreateTmuxWindowOptions,
|
|
109
111
|
): Promise<TmuxWindow> {
|
|
112
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
113
|
+
const outcome = await attemptCreateWindow(executor, options);
|
|
114
|
+
if ("window" in outcome) {
|
|
115
|
+
return outcome.window;
|
|
116
|
+
}
|
|
117
|
+
if (attempt === CREATE_WINDOW_ATTEMPTS || !isMissingSession(outcome.result)) {
|
|
118
|
+
throw tmuxError(outcome.action, outcome.result);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function attemptCreateWindow(
|
|
124
|
+
executor: TmuxExecutor,
|
|
125
|
+
options: CreateTmuxWindowOptions,
|
|
126
|
+
): Promise<{ window: TmuxWindow } | { action: string; result: ExecResult }> {
|
|
110
127
|
const existing = (await listTmuxWindows(executor, options.tmuxSession)).find(
|
|
111
128
|
(window) => window.piSessionId === options.piSessionId,
|
|
112
129
|
);
|
|
113
130
|
if (existing) {
|
|
114
|
-
return existing;
|
|
131
|
+
return { window: existing };
|
|
115
132
|
}
|
|
116
133
|
|
|
117
134
|
const sessionExists = await hasTmuxSession(executor, options.tmuxSession);
|
|
@@ -149,7 +166,7 @@ async function createWindow(
|
|
|
149
166
|
timeout: TMUX_TIMEOUT_MS,
|
|
150
167
|
});
|
|
151
168
|
if (created.code !== 0) {
|
|
152
|
-
|
|
169
|
+
return { action: "create window", result: created };
|
|
153
170
|
}
|
|
154
171
|
|
|
155
172
|
const windowId = created.stdout.trim();
|
|
@@ -165,10 +182,10 @@ async function createWindow(
|
|
|
165
182
|
await executor.exec(TMUX_COMMAND, ["kill-window", "-t", windowId], {
|
|
166
183
|
timeout: TMUX_TIMEOUT_MS,
|
|
167
184
|
});
|
|
168
|
-
|
|
185
|
+
return { action: "stamp window", result: stamped };
|
|
169
186
|
}
|
|
170
187
|
|
|
171
|
-
return { windowId, name: options.name, piSessionId: options.piSessionId };
|
|
188
|
+
return { window: { windowId, name: options.name, piSessionId: options.piSessionId } };
|
|
172
189
|
}
|
|
173
190
|
|
|
174
191
|
export async function killTmuxWindow(
|
|
@@ -215,8 +232,10 @@ function parseWindow(line: string): TmuxWindow | undefined {
|
|
|
215
232
|
return { windowId, name, piSessionId };
|
|
216
233
|
}
|
|
217
234
|
|
|
235
|
+
// A server that is shutting down answers in-flight commands with `no current target` (its last
|
|
236
|
+
// session is gone but it has not exited yet) or drops them with `server exited unexpectedly`.
|
|
218
237
|
function isMissingSession(result: ExecResult): boolean {
|
|
219
|
-
return /can't find session|no server running|failed to connect to server|error connecting to .*no such file/i.test(
|
|
238
|
+
return /can't find session|no current target|no server running|server exited unexpectedly|failed to connect to server|error connecting to .*no such file/i.test(
|
|
220
239
|
`${result.stderr}\n${result.stdout}`,
|
|
221
240
|
);
|
|
222
241
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionUIContext,
|
|
4
|
+
SessionTreeNode,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
6
|
import type { HandoffLaunchTarget } from "../session-handoff/launch-target.ts";
|
|
3
7
|
import type {
|
|
4
8
|
MessagingHandle,
|
|
@@ -23,7 +27,7 @@ import {
|
|
|
23
27
|
SUBAGENT_REPORT_MESSAGE_SCHEMA,
|
|
24
28
|
SUBAGENT_REPORT_RECEIVED_CUSTOM_TYPE,
|
|
25
29
|
} from "./ledger.ts";
|
|
26
|
-
import { openReconcileSession, SubagentReconciler } from "./reconcile.ts";
|
|
30
|
+
import { openReconcileSession, type ReconcileResult, SubagentReconciler } from "./reconcile.ts";
|
|
27
31
|
import {
|
|
28
32
|
buildIncomingSubagentReport,
|
|
29
33
|
createSubmitTaskReportTool,
|
|
@@ -51,6 +55,7 @@ interface ParentSessionState extends SubagentParentSession {
|
|
|
51
55
|
getTree(): SessionTreeNode[];
|
|
52
56
|
hasPendingMessages(): boolean;
|
|
53
57
|
shutdown(): void;
|
|
58
|
+
reportError(error: unknown): void;
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
interface CurrentSubagentSession {
|
|
@@ -178,9 +183,15 @@ export function installSubagents(
|
|
|
178
183
|
if (!session || session.parent.sessionId !== ctx.sessionManager.getSessionId()) {
|
|
179
184
|
return;
|
|
180
185
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
186
|
+
let reconciliation: ReconcileResult | undefined;
|
|
187
|
+
try {
|
|
188
|
+
reconciliation = hasSubagentLaunchEntries(session.parent.getBranch())
|
|
189
|
+
? await reconciler.reconcile()
|
|
190
|
+
: undefined;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
notifySubagentFailure(ctx.ui, error);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
184
195
|
if (session.child) {
|
|
185
196
|
await contextLimit.compactIfOverLimit(ctx);
|
|
186
197
|
await settledChildLifecycle.settle(session.parent, session.child, reconciliation);
|
|
@@ -233,11 +244,15 @@ export function installSubagents(
|
|
|
233
244
|
);
|
|
234
245
|
};
|
|
235
246
|
|
|
236
|
-
pi.on("session_tree", async () => {
|
|
247
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
237
248
|
settledChildLifecycle.cancel();
|
|
238
249
|
refreshChildSession();
|
|
239
250
|
registerMessagingTools();
|
|
240
|
-
|
|
251
|
+
try {
|
|
252
|
+
await reconciler.reconcileAndRestoreSuspended();
|
|
253
|
+
} catch (error) {
|
|
254
|
+
notifySubagentFailure(ctx.ui, error);
|
|
255
|
+
}
|
|
241
256
|
});
|
|
242
257
|
|
|
243
258
|
const handle: SubagentsHandle = {
|
|
@@ -283,6 +298,7 @@ export function installSubagents(
|
|
|
283
298
|
epoch,
|
|
284
299
|
launchState: { sessionId, depth: identity?.depth ?? 0, epoch },
|
|
285
300
|
tmuxInstalled: await isTmuxInstalled(pi, ctx.cwd),
|
|
301
|
+
reportError: (error) => notifySubagentFailure(ctx.ui, error),
|
|
286
302
|
};
|
|
287
303
|
reconciler.beginSession();
|
|
288
304
|
current = { parent };
|
|
@@ -290,16 +306,24 @@ export function installSubagents(
|
|
|
290
306
|
if (identity) {
|
|
291
307
|
registerMessagingTools();
|
|
292
308
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
309
|
+
try {
|
|
310
|
+
if (_event.reason === "reload") {
|
|
311
|
+
await reconciler.reconcile();
|
|
312
|
+
} else {
|
|
313
|
+
await reconciler.reconcileAndRestoreSuspended();
|
|
314
|
+
}
|
|
315
|
+
} catch (error) {
|
|
316
|
+
notifySubagentFailure(ctx.ui, error);
|
|
297
317
|
}
|
|
298
318
|
},
|
|
299
|
-
async onSessionShutdown(event) {
|
|
319
|
+
async onSessionShutdown(event, ctx) {
|
|
300
320
|
settledChildLifecycle.cancel();
|
|
301
321
|
if (current && event.reason !== "reload") {
|
|
302
|
-
|
|
322
|
+
try {
|
|
323
|
+
await reconciler.suspendForShutdown();
|
|
324
|
+
} catch (error) {
|
|
325
|
+
notifySubagentFailure(ctx.ui, error);
|
|
326
|
+
}
|
|
303
327
|
}
|
|
304
328
|
epoch += 1;
|
|
305
329
|
current = undefined;
|
|
@@ -307,3 +331,12 @@ export function installSubagents(
|
|
|
307
331
|
};
|
|
308
332
|
return handle;
|
|
309
333
|
}
|
|
334
|
+
|
|
335
|
+
// Lifecycle hooks and timers run outside any tool call. Pi renders a thrown error's full stack
|
|
336
|
+
// over the conversation, so report only the message.
|
|
337
|
+
function notifySubagentFailure(ui: ExtensionUIContext, error: unknown): void {
|
|
338
|
+
ui.notify(
|
|
339
|
+
`Subagent lifecycle failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
340
|
+
"error",
|
|
341
|
+
);
|
|
342
|
+
}
|
|
@@ -25,6 +25,7 @@ export interface SettledChildParentSession {
|
|
|
25
25
|
hasPendingMessages(): boolean;
|
|
26
26
|
isIdle(): boolean;
|
|
27
27
|
shutdown(): void;
|
|
28
|
+
reportError(error: unknown): void;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export interface SettledChildLifecycle {
|
|
@@ -72,7 +73,11 @@ export function createSettledChildLifecycle(
|
|
|
72
73
|
const schedule = (delayMs: number): void => {
|
|
73
74
|
timer = setTimeout(() => {
|
|
74
75
|
timer = undefined;
|
|
75
|
-
|
|
76
|
+
advance().catch((error: unknown) => {
|
|
77
|
+
if (isCurrentSession(parent.epoch)) {
|
|
78
|
+
parent.reportError(error);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
76
81
|
}, delayMs);
|
|
77
82
|
};
|
|
78
83
|
const advance = async (): Promise<void> => {
|