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.
@@ -24,8 +24,9 @@ import {
24
24
  } from "@earendil-works/pi-coding-agent";
25
25
  import { type Static, Type } from "typebox";
26
26
  import { freshenModel } from "../shared/model-runtime.ts";
27
- import { getDefaultHandoffRunsDir } from "../shared/settings.ts";
27
+ import { getDefaultHandoffRunsDir, type HandoffSettings } from "../shared/settings.ts";
28
28
  import { parseTypeBoxValue } from "../shared/typebox.ts";
29
+ import { resolveModelOverride } from "./model.ts";
29
30
 
30
31
  const MAX_HANDOFF_EXTRACTION_ATTEMPTS = 3;
31
32
  const HANDOFF_EXTRACTION_RETRY_PROMPT =
@@ -88,22 +89,39 @@ export function resolveHandoffSource(
88
89
  return sessionContext;
89
90
  }
90
91
 
91
- export async function generateHandoffDraftFromSessionManager(
92
- ctx: ExtensionContext,
93
- modelRuntime: ModelRuntime,
94
- sourceSessionManager: ExtensionContext["sessionManager"],
95
- sourceLeafId: string,
96
- goal: string,
97
- thinkingLevel: ThinkingLevel | undefined,
98
- persistRuns: boolean,
99
- signal?: AbortSignal,
92
+ export async function generateHandoffDraftFromSessionManager({
93
+ ctx,
94
+ modelRuntime,
95
+ sourceSessionManager,
96
+ sourceLeafId,
97
+ goal,
98
+ settings,
99
+ destinationThinkingLevel,
100
+ signal,
100
101
  requestResponse = false,
101
- ): Promise<HandoffDraftResult | undefined> {
102
- if (!ctx.model) {
103
- throw new Error("No model is available for handoff.");
102
+ }: {
103
+ ctx: ExtensionContext;
104
+ modelRuntime: ModelRuntime;
105
+ sourceSessionManager: ExtensionContext["sessionManager"];
106
+ sourceLeafId: string;
107
+ goal: string;
108
+ settings: HandoffSettings;
109
+ destinationThinkingLevel: ThinkingLevel | undefined;
110
+ signal?: AbortSignal | undefined;
111
+ requestResponse?: boolean | undefined;
112
+ }): Promise<HandoffDraftResult | undefined> {
113
+ const modelOverride = settings.model
114
+ ? resolveModelOverride(modelRuntime, settings.model, settings.thinkingLevel)
115
+ : undefined;
116
+ let model = modelOverride?.model;
117
+ if (!model) {
118
+ if (!ctx.model) {
119
+ throw new Error("No model is available for handoff.");
120
+ }
121
+ model = freshenModel(modelRuntime, ctx.model);
104
122
  }
105
-
106
- const model = freshenModel(modelRuntime, ctx.model);
123
+ const thinkingLevel =
124
+ modelOverride?.thinkingLevel ?? settings.thinkingLevel ?? destinationThinkingLevel;
107
125
  const sessionContext = resolveHandoffSource(sourceSessionManager, sourceLeafId);
108
126
 
109
127
  const conversationText = serializeConversation(convertToLlm(sessionContext.messages));
@@ -114,7 +132,7 @@ export async function generateHandoffDraftFromSessionManager(
114
132
  conversationText,
115
133
  goal,
116
134
  thinkingLevel,
117
- persistRuns,
135
+ settings.persistRuns,
118
136
  signal,
119
137
  );
120
138
  if (!handoffContext) {
@@ -214,7 +214,7 @@ export function installHandoff(
214
214
  ctx,
215
215
  deps.getModelRuntime,
216
216
  pi.getThinkingLevel(),
217
- settings.handoff.persistRuns,
217
+ settings.handoff,
218
218
  );
219
219
  },
220
220
  };
@@ -59,7 +59,7 @@ export interface HandoffLaunchTarget {
59
59
  description?: string | undefined;
60
60
  requestResponseDefault: boolean;
61
61
  bootstrapMode: "review" | "automatic";
62
- /** Subagent launches stamp their child's identity into the durable handoff record. */
62
+ /** Subagent launches stamp their child's identity into the child bootstrap. */
63
63
  describeSubagentChild?(input: {
64
64
  childSessionId: string;
65
65
  ownerSessionId: string;
@@ -3,6 +3,7 @@ import { type Static, Type } from "typebox";
3
3
  import { safeParseTypeBoxValue } from "../shared/typebox.ts";
4
4
  import { HANDOFF_KICKOFF_CUSTOM_TYPE, HANDOFF_KICKOFF_DETAILS_SCHEMA } from "./kickoff.ts";
5
5
  import {
6
+ HANDOFF_LAUNCH_VALUE_SCHEMA,
6
7
  HANDOFF_NON_SUBAGENT_LAUNCH_SCHEMA,
7
8
  type HandoffLaunchValue,
8
9
  SUBAGENT_LAUNCH,
@@ -16,7 +17,7 @@ export const HANDOFF_STALE_SESSION_MESSAGE =
16
17
  export const SESSION_STARTING_MESSAGE =
17
18
  "Target session is still starting. wait for it to show up in session_search, then resend.";
18
19
 
19
- // A subagent's self-knowledge, stamped into the durable handoff record so the
20
+ // A subagent's self-knowledge, stamped into its child-local bootstrap so the
20
21
  // child never opens the parent transcript to learn who it is.
21
22
  export const HANDOFF_SUBAGENT_SCHEMA = Type.Object({
22
23
  childSessionId: Type.String(),
@@ -32,18 +33,10 @@ const HANDOFF_METADATA_BASE = {
32
33
  initial_prompt: Type.String(),
33
34
  };
34
35
 
35
- export const HANDOFF_SESSION_METADATA_SCHEMA = Type.Union([
36
- Type.Object({
37
- ...HANDOFF_METADATA_BASE,
38
- launch: Type.Literal(SUBAGENT_LAUNCH),
39
- subagent: HANDOFF_SUBAGENT_SCHEMA,
40
- }),
41
- Type.Object({
42
- ...HANDOFF_METADATA_BASE,
43
- launch: HANDOFF_NON_SUBAGENT_LAUNCH_SCHEMA,
44
- subagent: Type.Optional(Type.Never()),
45
- }),
46
- ]);
36
+ export const HANDOFF_SESSION_METADATA_SCHEMA = Type.Object({
37
+ ...HANDOFF_METADATA_BASE,
38
+ launch: HANDOFF_LAUNCH_VALUE_SCHEMA,
39
+ });
47
40
 
48
41
  const HANDOFF_BOOTSTRAP_BASE = {
49
42
  mode: Type.Literal("generate"),
@@ -91,21 +84,14 @@ export function createHandoffSessionMetadata(
91
84
  initialPrompt: string,
92
85
  title: string,
93
86
  launch: HandoffLaunchValue,
94
- subagent: HandoffSubagent | undefined,
95
87
  ): HandoffSessionMetadata {
96
- const base = {
97
- origin: "handoff" as const,
88
+ return {
89
+ origin: "handoff",
98
90
  goal: goal.trim(),
99
91
  title,
100
92
  initial_prompt: initialPrompt.trim(),
93
+ launch,
101
94
  };
102
- if (launch === SUBAGENT_LAUNCH) {
103
- if (!subagent) {
104
- throw new Error("A subagent handoff requires a subagent identity block.");
105
- }
106
- return { ...base, launch, subagent };
107
- }
108
- return { ...base, launch };
109
95
  }
110
96
 
111
97
  export function createChildGeneratedHandoffBootstrap(options: {
@@ -195,6 +181,69 @@ export function isSessionStarting(branch: readonly SessionEntry[]): boolean {
195
181
  return findPendingHandoffBootstrap(branch)?.kind === "pending";
196
182
  }
197
183
 
184
+ /**
185
+ * Folds the bootstrap lifecycle in one transcript pass. The bootstrap owns its
186
+ * data; kickoff and consumed entries only establish whether it started or ended.
187
+ */
188
+ export function findCurrentHandoffBootstrap(
189
+ branch: readonly SessionEntry[],
190
+ ): HandoffBootstrap | undefined {
191
+ const bootstrapsById = new Map<string, HandoffBootstrap>();
192
+ const bootstrapIds: string[] = [];
193
+ const startedBootstrapIds: string[] = [];
194
+ const endedBootstrapIds = new Set<string>();
195
+ let conversationStarted = false;
196
+
197
+ for (const entry of branch) {
198
+ if (entry.type === "message" && entry.message.role === "user") {
199
+ conversationStarted = true;
200
+ continue;
201
+ }
202
+ if (entry.type === "custom_message" && entry.customType === HANDOFF_KICKOFF_CUSTOM_TYPE) {
203
+ conversationStarted = true;
204
+ const details = safeParseTypeBoxValue(HANDOFF_KICKOFF_DETAILS_SCHEMA, entry.details);
205
+ if (details?.bootstrapEntryId) {
206
+ startedBootstrapIds.push(details.bootstrapEntryId);
207
+ }
208
+ continue;
209
+ }
210
+ if (entry.type !== "custom") {
211
+ continue;
212
+ }
213
+ if (entry.customType === HANDOFF_BOOTSTRAP_PENDING_CUSTOM_TYPE) {
214
+ const bootstrap = safeParseTypeBoxValue(HANDOFF_BOOTSTRAP_SCHEMA, entry.data);
215
+ if (bootstrap) {
216
+ bootstrapsById.set(entry.id, bootstrap);
217
+ bootstrapIds.push(entry.id);
218
+ }
219
+ continue;
220
+ }
221
+ if (entry.customType === HANDOFF_BOOTSTRAP_CONSUMED_CUSTOM_TYPE) {
222
+ const consumed = safeParseTypeBoxValue(HANDOFF_BOOTSTRAP_CONSUMED_SCHEMA, entry.data);
223
+ if (consumed) {
224
+ endedBootstrapIds.add(consumed.bootstrapEntryId);
225
+ }
226
+ }
227
+ }
228
+
229
+ for (let index = startedBootstrapIds.length - 1; index >= 0; index -= 1) {
230
+ const bootstrap = bootstrapsById.get(startedBootstrapIds[index] ?? "");
231
+ if (bootstrap) {
232
+ return bootstrap;
233
+ }
234
+ }
235
+ if (conversationStarted) {
236
+ return undefined;
237
+ }
238
+ for (let index = bootstrapIds.length - 1; index >= 0; index -= 1) {
239
+ const bootstrapId = bootstrapIds[index];
240
+ if (bootstrapId && !endedBootstrapIds.has(bootstrapId)) {
241
+ return bootstrapsById.get(bootstrapId);
242
+ }
243
+ }
244
+ return undefined;
245
+ }
246
+
198
247
  export function parseHandoffSessionMetadata(value: unknown): HandoffSessionMetadata | undefined {
199
248
  return safeParseTypeBoxValue(HANDOFF_SESSION_METADATA_SCHEMA, value);
200
249
  }
@@ -34,7 +34,15 @@ export async function spawnSessionMessagingBrokerIfNeeded(): Promise<void> {
34
34
  return;
35
35
  }
36
36
 
37
- const brokerPath = join(dirname(fileURLToPath(import.meta.url)), "process.ts");
37
+ const brokerPath = join(
38
+ dirname(fileURLToPath(import.meta.url)),
39
+ "../../../dist/session-messaging/broker/process.js",
40
+ );
41
+ if (!existsSync(brokerPath)) {
42
+ throw new Error(
43
+ `Session messaging broker build not found: ${brokerPath}. Run npm install in the pi-sessions package directory.`,
44
+ );
45
+ }
38
46
  const child = spawn(process.execPath, [brokerPath], {
39
47
  detached: true,
40
48
  stdio: "ignore",
@@ -164,15 +172,12 @@ function releaseSpawnLock(): void {
164
172
  }
165
173
 
166
174
  function brokerSpawnEnv(): NodeJS.ProcessEnv {
167
- // NODE_NO_WARNINGS silences Node's experimental-TypeScript warning when the
168
- // broker is launched as `node process.ts`. Redundant while stdio is ignored,
169
- // but cheap insurance if that changes or an older Node revives the warning.
170
- const env: NodeJS.ProcessEnv = { ...process.env, NODE_NO_WARNINGS: "1" };
175
+ const env: NodeJS.ProcessEnv = { ...process.env };
171
176
  // process.execPath is the Pi executable. As a Bun-compiled binary it treats a
172
177
  // positional path as a file argument to the agent, not a script to run, which
173
178
  // would relaunch a full agent session and recurse. BUN_BE_BUN makes it behave
174
- // as the Bun CLI and execute process.ts directly. It is set whenever Bun is the
175
- // runtime (compiled binary or `bun run`) and ignored by a plain Node launch.
179
+ // as the Bun CLI and execute the broker. It is set whenever Bun is the runtime
180
+ // (compiled binary or `bun run`) and ignored by a plain Node launch.
176
181
  if (process.versions.bun) {
177
182
  env.BUN_BE_BUN = "1";
178
183
  }
@@ -27,6 +27,8 @@ const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
27
27
  enable: Type.Optional(Type.Boolean()),
28
28
  pickerShortcut: Type.Optional(Type.String()),
29
29
  persistRuns: Type.Optional(Type.Boolean()),
30
+ model: Type.Optional(Type.String()),
31
+ thinkingLevel: Type.Optional(Type.String()),
30
32
  deferred: Type.Optional(
31
33
  Type.Object({
32
34
  copyToClipboard: Type.Optional(Type.Boolean()),
@@ -87,6 +89,14 @@ export interface AskSettings extends AgentModelSettings {
87
89
  persistRuns: boolean;
88
90
  }
89
91
 
92
+ export interface HandoffSettings extends AgentModelSettings {
93
+ pickerShortcut: KeyId;
94
+ persistRuns: boolean;
95
+ deferred: {
96
+ copyToClipboard: boolean;
97
+ };
98
+ }
99
+
90
100
  export interface FeatureToggles {
91
101
  messaging: boolean;
92
102
  subagents: boolean;
@@ -102,13 +112,7 @@ export interface SessionSettings {
102
112
  subagents: {
103
113
  maxDepth: number;
104
114
  };
105
- handoff: {
106
- pickerShortcut: KeyId;
107
- persistRuns: boolean;
108
- deferred: {
109
- copyToClipboard: boolean;
110
- };
111
- };
115
+ handoff: HandoffSettings;
112
116
  index: {
113
117
  path: string;
114
118
  };
@@ -222,6 +226,7 @@ function resolveSessionSettings(fileSettings: SessionFileSettings): SessionSetti
222
226
  maxDepth: fileSettings.subagents?.maxDepth ?? 2,
223
227
  },
224
228
  handoff: {
229
+ ...resolveAgentModelSettings(fileSettings.handoff),
225
230
  pickerShortcut: normalizePickerShortcut(fileSettings.handoff?.pickerShortcut),
226
231
  persistRuns: fileSettings.handoff?.persistRuns ?? false,
227
232
  deferred: {
@@ -9,6 +9,8 @@ export type SubagentState =
9
9
  | "interrupted"
10
10
  | "unknown";
11
11
 
12
+ const RUNNING_SUBAGENT_STATES: ReadonlySet<SubagentState> = new Set(["starting", "busy", "active"]);
13
+
12
14
  export interface SubagentEvidence {
13
15
  hasWindow: boolean;
14
16
  brokerLive: boolean;
@@ -20,6 +22,10 @@ export interface SubagentEvidence {
20
22
  childReadable: boolean;
21
23
  }
22
24
 
25
+ export function isRunningSubagentState(state: SubagentState): boolean {
26
+ return RUNNING_SUBAGENT_STATES.has(state);
27
+ }
28
+
23
29
  export function classifySubagent(evidence: SubagentEvidence): SubagentState {
24
30
  if (!evidence.childReadable) {
25
31
  return "unknown";
@@ -1,9 +1,5 @@
1
- import type { ExtensionAPI, SessionEntry, SessionTreeNode } from "@earendil-works/pi-coding-agent";
2
- import { type HandoffLaunchTarget, SUBAGENT_LAUNCH } from "../session-handoff/launch-target.ts";
3
- import {
4
- getHandoffMetadataFromEntries,
5
- type HandoffSubagent,
6
- } from "../session-handoff/metadata.ts";
1
+ import type { ExtensionAPI, SessionTreeNode } from "@earendil-works/pi-coding-agent";
2
+ import type { HandoffLaunchTarget } from "../session-handoff/launch-target.ts";
7
3
  import type {
8
4
  MessagingHandle,
9
5
  SendMessageRequest,
@@ -11,17 +7,14 @@ import type {
11
7
  } from "../session-messaging/install.ts";
12
8
  import type { SessionLifecycle } from "../shared/composition.ts";
13
9
  import type { SessionSettings } from "../shared/settings.ts";
14
- import { hasAttachedTmuxClients, isTmuxInstalled, tmuxSessionName } from "../shared/tmux.ts";
10
+ import { isTmuxInstalled } from "../shared/tmux.ts";
15
11
  import { SubagentCancellationRouter, type SubagentCancelResult } from "./cancel.ts";
16
12
  import { createSubagentLaunchTarget, type SubagentLaunchState } from "./launch-target.ts";
17
13
  import {
18
- getChildSubagentLifecycle,
19
14
  hasSubagentLaunchEntries,
20
- SUBAGENT_CLOSED_CUSTOM_TYPE,
21
15
  SUBAGENT_LAUNCHED_CUSTOM_TYPE,
22
16
  SUBAGENT_REPORT_MESSAGE_CUSTOM_TYPE,
23
17
  SUBAGENT_REPORT_RECEIVED_CUSTOM_TYPE,
24
- SUBAGENT_REPORT_REMINDER_MESSAGE_CUSTOM_TYPE,
25
18
  } from "./ledger.ts";
26
19
  import { openReconcileSession, SubagentReconciler } from "./reconcile.ts";
27
20
  import {
@@ -31,10 +24,14 @@ import {
31
24
  } from "./report.ts";
32
25
  import { renderSubagentReportMessage } from "./report-message-renderer.ts";
33
26
  import { openRosterSession, type SubagentRoster, TranscriptSubagentRoster } from "./roster.ts";
27
+ import {
28
+ countSubagentReports,
29
+ createSettledChildLifecycle,
30
+ findSelfSubagentIdentity,
31
+ type SubagentChildSessionState,
32
+ } from "./settle.ts";
34
33
  import { SubagentMessageRouter } from "./wake.ts";
35
34
 
36
- const LINGER_POLL_MS = 1_000;
37
-
38
35
  interface ParentSessionState extends SubagentParentSession {
39
36
  getSessionName(): string | undefined;
40
37
  launchState: SubagentLaunchState;
@@ -44,15 +41,9 @@ interface ParentSessionState extends SubagentParentSession {
44
41
  shutdown(): void;
45
42
  }
46
43
 
47
- interface ChildSessionState {
48
- identity: HandoffSubagent;
49
- requestResponse: boolean;
50
- reportsAtTurnStart: number;
51
- }
52
-
53
44
  interface CurrentSubagentSession {
54
45
  parent: ParentSessionState;
55
- child?: ChildSessionState | undefined;
46
+ child?: SubagentChildSessionState | undefined;
56
47
  }
57
48
 
58
49
  export interface SubagentsHandle extends SessionLifecycle {
@@ -70,16 +61,9 @@ export function installSubagents(
70
61
 
71
62
  let epoch = 0;
72
63
  let current: CurrentSubagentSession | undefined;
73
- let lingerTimer: NodeJS.Timeout | undefined;
74
64
 
75
65
  const isCurrentSession = (candidateEpoch: number): boolean =>
76
66
  current?.parent.epoch === candidateEpoch;
77
- const clearLinger = (): void => {
78
- if (lingerTimer) {
79
- clearTimeout(lingerTimer);
80
- lingerTimer = undefined;
81
- }
82
- };
83
67
  const reconciler = new SubagentReconciler({
84
68
  executor: pi,
85
69
  messaging: deps.messaging,
@@ -87,6 +71,7 @@ export function installSubagents(
87
71
  isCurrent: isCurrentSession,
88
72
  openSession: openReconcileSession,
89
73
  });
74
+ const settledChildLifecycle = createSettledChildLifecycle(pi, reconciler, isCurrentSession);
90
75
  const roster = new TranscriptSubagentRoster({
91
76
  executor: pi,
92
77
  messaging: deps.messaging,
@@ -150,9 +135,9 @@ You are working as a subagent on one task delegated by a parent session. The han
150
135
  });
151
136
 
152
137
  pi.on("agent_start", (_event, ctx) => {
153
- clearLinger();
138
+ settledChildLifecycle.cancel();
154
139
  if (current?.child) {
155
- current.child.reportsAtTurnStart = countReports(ctx.sessionManager.getBranch());
140
+ current.child.reportsAtTurnStart = countSubagentReports(ctx.sessionManager.getBranch());
156
141
  }
157
142
  });
158
143
 
@@ -161,28 +146,11 @@ You are working as a subagent on one task delegated by a parent session. The han
161
146
  if (!session || session.parent.sessionId !== ctx.sessionManager.getSessionId()) {
162
147
  return;
163
148
  }
164
- if (hasSubagentLaunchEntries(session.parent.getBranch())) {
165
- await reconciler.reconcile();
166
- }
149
+ const reconciliation = hasSubagentLaunchEntries(session.parent.getBranch())
150
+ ? await reconciler.reconcile()
151
+ : undefined;
167
152
  if (session.child) {
168
- if (ctx.hasPendingMessages()) {
169
- return;
170
- }
171
- const shouldExit = settleChild(pi, session.child, ctx.sessionManager.getBranch());
172
- if (!shouldExit) {
173
- return;
174
- }
175
- await exitWhenUnobserved(
176
- pi,
177
- session.parent,
178
- session.child.identity,
179
- isCurrentSession,
180
- (timer) => {
181
- clearLinger();
182
- lingerTimer = timer;
183
- },
184
- );
185
- return;
153
+ await settledChildLifecycle.settle(session.parent, session.child, reconciliation);
186
154
  }
187
155
  });
188
156
 
@@ -202,11 +170,12 @@ You are working as a subagent on one task delegated by a parent session. The han
202
170
  return [createSubagentLaunchTarget(pi, parent.launchState, isCurrentSession)];
203
171
  },
204
172
  async onSessionStart(_event, ctx) {
205
- clearLinger();
173
+ settledChildLifecycle.cancel();
206
174
  epoch += 1;
207
175
  const sessionId = ctx.sessionManager.getSessionId();
208
- const identity = findSelfSubagentIdentity(ctx.sessionManager);
209
176
  const sessionManager = ctx.sessionManager;
177
+ const sessionStartBranch = sessionManager.getBranch();
178
+ const identity = findSelfSubagentIdentity(sessionId, sessionStartBranch);
210
179
  const parent: ParentSessionState = {
211
180
  sessionId,
212
181
  getSessionName: () => sessionManager.getSessionName(),
@@ -227,7 +196,7 @@ You are working as a subagent on one task delegated by a parent session. The han
227
196
  child: {
228
197
  identity,
229
198
  requestResponse: identity.requestResponse,
230
- reportsAtTurnStart: countReports(sessionManager.getBranch()),
199
+ reportsAtTurnStart: countSubagentReports(sessionStartBranch),
231
200
  },
232
201
  }
233
202
  : {}),
@@ -249,7 +218,7 @@ You are working as a subagent on one task delegated by a parent session. The han
249
218
  }
250
219
  },
251
220
  async onSessionShutdown(event) {
252
- clearLinger();
221
+ settledChildLifecycle.cancel();
253
222
  if (current && event.reason !== "reload") {
254
223
  await reconciler.suspendForShutdown();
255
224
  }
@@ -258,87 +227,3 @@ You are working as a subagent on one task delegated by a parent session. The han
258
227
  },
259
228
  };
260
229
  }
261
-
262
- function settleChild(
263
- pi: ExtensionAPI,
264
- child: ChildSessionState,
265
- branch: readonly SessionEntry[],
266
- ): boolean {
267
- const lifecycle = getChildSubagentLifecycle(branch);
268
- const reports = lifecycle.reports.length;
269
- if (reports > child.reportsAtTurnStart) {
270
- return true;
271
- }
272
- if (!child.requestResponse) {
273
- pi.appendEntry(SUBAGENT_CLOSED_CUSTOM_TYPE, {
274
- reason: "no_response_expected",
275
- });
276
- return true;
277
- }
278
-
279
- if (lifecycle.hasReminder) {
280
- pi.appendEntry(SUBAGENT_CLOSED_CUSTOM_TYPE, {
281
- reason: "no_report_after_reminder",
282
- });
283
- return true;
284
- }
285
-
286
- pi.sendMessage(
287
- {
288
- customType: SUBAGENT_REPORT_REMINDER_MESSAGE_CUSTOM_TYPE,
289
- content:
290
- "[system] Your delegated turn settled without a task report. Call submit_task_report now with done, blocked, or incomplete status.",
291
- display: true,
292
- },
293
- { triggerTurn: true },
294
- );
295
- return false;
296
- }
297
-
298
- function countReports(branch: readonly SessionEntry[]): number {
299
- return getChildSubagentLifecycle(branch).reports.length;
300
- }
301
-
302
- function findSelfSubagentIdentity(session: {
303
- getSessionId(): string;
304
- getBranch(): readonly SessionEntry[];
305
- }): HandoffSubagent | undefined {
306
- const metadata = getHandoffMetadataFromEntries(session.getBranch());
307
- if (metadata?.launch !== SUBAGENT_LAUNCH) {
308
- return undefined;
309
- }
310
- return metadata.subagent.childSessionId === session.getSessionId()
311
- ? metadata.subagent
312
- : undefined;
313
- }
314
-
315
- async function exitWhenUnobserved(
316
- executor: ExtensionAPI,
317
- parent: ParentSessionState,
318
- identity: HandoffSubagent,
319
- isCurrentSession: (epoch: number) => boolean,
320
- setTimer: (timer: NodeJS.Timeout) => void,
321
- ): Promise<void> {
322
- const check = async (): Promise<void> => {
323
- if (!isCurrentSession(parent.epoch) || parent.hasPendingMessages() || !parent.isIdle()) {
324
- return;
325
- }
326
-
327
- let attached = false;
328
- try {
329
- attached = await hasAttachedTmuxClients(executor, tmuxSessionName(identity.ownerSessionId));
330
- } catch {
331
- attached = false;
332
- }
333
- if (!isCurrentSession(parent.epoch)) {
334
- return;
335
- }
336
- if (!attached) {
337
- parent.shutdown();
338
- return;
339
- }
340
- setTimer(setTimeout(() => void check(), LINGER_POLL_MS));
341
- };
342
-
343
- await check();
344
- }