pi-sessions 0.9.0 → 0.11.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.
Files changed (42) hide show
  1. package/README.md +29 -20
  2. package/dist/session-messaging/broker/client-frame.js +62 -0
  3. package/dist/session-messaging/broker/process.js +234 -0
  4. package/dist/shared/session-broker/framing.js +59 -0
  5. package/dist/shared/session-broker/socket-path.js +19 -0
  6. package/extensions/pi-sessions.ts +14 -1
  7. package/extensions/session-auto-title/generate.ts +56 -66
  8. package/extensions/session-auto-title/install.ts +2 -0
  9. package/extensions/session-auto-title/runs.ts +50 -0
  10. package/extensions/session-handoff/board-view-model.ts +2 -4
  11. package/extensions/session-handoff/bootstrap.ts +14 -16
  12. package/extensions/session-handoff/extract.ts +34 -16
  13. package/extensions/session-handoff/install.ts +1 -1
  14. package/extensions/session-handoff/kickoff.ts +4 -3
  15. package/extensions/session-handoff/launch-target.ts +1 -1
  16. package/extensions/session-handoff/metadata.ts +72 -23
  17. package/extensions/session-messaging/broker/client-frame.ts +113 -0
  18. package/extensions/session-messaging/broker/process.ts +12 -21
  19. package/extensions/session-messaging/broker/spawn.ts +12 -7
  20. package/extensions/session-messaging/pi/reachable-contract.ts +59 -0
  21. package/extensions/session-messaging/pi/reachable-presenter.ts +49 -0
  22. package/extensions/session-messaging/pi/reachable-tool.ts +190 -0
  23. package/extensions/session-messaging/pi/reachable-view-model.ts +59 -0
  24. package/extensions/session-messaging/pi/renderer.ts +1 -1
  25. package/extensions/session-messaging/pi/tools.ts +8 -8
  26. package/extensions/session-search/hooks.ts +45 -52
  27. package/extensions/session-search/install.ts +3 -158
  28. package/extensions/session-search/reindex.ts +3 -3
  29. package/extensions/session-search/renderer.ts +1 -32
  30. package/extensions/session-search/tool-contract.ts +1 -21
  31. package/extensions/shared/model-runtime.ts +8 -1
  32. package/extensions/shared/session-broker/client.ts +3 -4
  33. package/extensions/shared/session-broker/framing.ts +6 -7
  34. package/extensions/shared/session-index/lineage.ts +6 -2
  35. package/extensions/shared/session-index/sqlite.ts +44 -17
  36. package/extensions/shared/settings.ts +25 -9
  37. package/extensions/shared/tmux.ts +22 -0
  38. package/extensions/subagents/classify.ts +6 -0
  39. package/extensions/subagents/install.ts +22 -137
  40. package/extensions/subagents/report-message-renderer.ts +2 -2
  41. package/extensions/subagents/settle.ts +182 -0
  42. package/package.json +18 -26
@@ -1,3 +1,4 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { createRequire } from "node:module";
2
3
 
3
4
  const requireModule = createRequire(import.meta.url);
@@ -18,32 +19,30 @@ export interface SqliteStatement {
18
19
  run(...params: SqliteBindValue[]): SqliteRunResult;
19
20
  }
20
21
 
21
- export interface SqliteTransaction<Args extends unknown[], Result> {
22
- (...args: Args): Result;
23
- immediate(...args: Args): Result;
24
- }
25
-
26
- export interface SqliteDatabase {
22
+ // The surface this module consumes from a native driver. Transactions are
23
+ // implemented inline
24
+ interface SqliteDriver {
27
25
  prepare(sql: string): SqliteStatement;
28
26
  exec(sql: string): void;
29
- transaction<Args extends unknown[], Result>(
30
- fn: (...args: Args) => Result,
31
- ): SqliteTransaction<Args, Result>;
32
27
  close(): void;
33
28
  }
34
29
 
30
+ export interface SqliteDatabase extends SqliteDriver {
31
+ transaction<Result>(fn: () => Result): Result;
32
+ }
33
+
35
34
  interface SqliteConstructor {
36
- new (path: string, options?: Record<string, unknown>): SqliteDatabase;
35
+ new (path: string, options?: Record<string, unknown>): SqliteDriver;
37
36
  }
38
37
 
39
- // The driver is resolved at runtime so Bun never loads the native better-sqlite3
40
- // addon (unsupported) and Node never loads the bun:sqlite builtin. The require
41
- // result is untyped, so the cast to our structural contract is unavoidable here.
38
+ // Each runtime gets its own built-in SQLite, resolved at runtime because neither
39
+ // module is importable from the other. The require result is untyped, so the
40
+ // cast to our structural contract is unavoidable here.
42
41
  function loadDatabaseConstructor(): SqliteConstructor {
43
42
  if (isBun) {
44
43
  return (requireModule("bun:sqlite") as { Database: SqliteConstructor }).Database;
45
44
  }
46
- return requireModule("better-sqlite3") as SqliteConstructor;
45
+ return (requireModule("node:sqlite") as { DatabaseSync: SqliteConstructor }).DatabaseSync;
47
46
  }
48
47
 
49
48
  export function openSqlite(
@@ -52,13 +51,20 @@ export function openSqlite(
52
51
  ): SqliteDatabase {
53
52
  const Database = loadDatabaseConstructor();
54
53
  const readonly = options.readonly ?? false;
54
+
55
+ // node:sqlite has no fileMustExist equivalent and would otherwise create an
56
+ // empty database where the caller demanded an existing one.
57
+ if (!options.create && !existsSync(dbPath)) {
58
+ throw new Error(`SQLite database does not exist: ${dbPath}`);
59
+ }
60
+
55
61
  const db = isBun
56
62
  ? new Database(dbPath, {
57
63
  create: readonly ? false : options.create,
58
64
  readonly,
59
65
  readwrite: !readonly,
60
66
  })
61
- : new Database(dbPath, { fileMustExist: readonly || !options.create, readonly });
67
+ : new Database(dbPath, { readOnly: readonly });
62
68
 
63
69
  db.exec(`PRAGMA busy_timeout = ${options.timeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS}`);
64
70
  db.exec("PRAGMA foreign_keys = ON");
@@ -77,8 +83,9 @@ export function openSqlite(
77
83
  // Neither driver memoizes prepare(), and the write paths prepare the same
78
84
  // statements once per row. SQLite re-prepares cached statements transparently
79
85
  // when the schema changes, so reusing them across DDL is safe.
80
- function withStatementCache(db: SqliteDatabase): SqliteDatabase {
86
+ function withStatementCache(db: SqliteDriver): SqliteDatabase {
81
87
  const statements = new Map<string, SqliteStatement>();
88
+ let inTransaction = false;
82
89
 
83
90
  return {
84
91
  prepare(sql) {
@@ -94,8 +101,28 @@ function withStatementCache(db: SqliteDatabase): SqliteDatabase {
94
101
  exec(sql) {
95
102
  db.exec(sql);
96
103
  },
104
+ // Immediate rather than deferred: these callbacks read before they write, and
105
+ // a deferred transaction's read-to-write upgrade fails SQLITE_BUSY outright
106
+ // instead of waiting on busy_timeout.
97
107
  transaction(fn) {
98
- return db.transaction(fn);
108
+ if (inTransaction) {
109
+ throw new Error(
110
+ "Cannot nest transactions: the outer BEGIN IMMEDIATE already holds the write lock",
111
+ );
112
+ }
113
+
114
+ inTransaction = true;
115
+ db.exec("BEGIN IMMEDIATE");
116
+ try {
117
+ const result = fn();
118
+ db.exec("COMMIT");
119
+ return result;
120
+ } catch (error) {
121
+ db.exec("ROLLBACK");
122
+ throw error;
123
+ } finally {
124
+ inTransaction = false;
125
+ }
99
126
  },
100
127
  close() {
101
128
  statements.clear();
@@ -1,7 +1,7 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
3
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
4
- import { SettingsManager } from "@earendil-works/pi-coding-agent";
4
+ import { getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
5
5
  import type { KeyId } from "@earendil-works/pi-tui";
6
6
  import { type Static, Type } from "typebox";
7
7
  import { isThinkingLevel } from "./thinking-levels.ts";
@@ -9,6 +9,7 @@ import { parseTypeBoxValue } from "./typebox.ts";
9
9
 
10
10
  export const DEFAULT_AUTO_TITLE_REFRESH_TURNS = 4;
11
11
  export const DEFAULT_AUTO_TITLE_TIMEOUT_SECONDS = 15;
12
+ export const DEFAULT_AUTO_TITLE_TOKEN_BUDGET = 64;
12
13
  export const DEFAULT_AUTO_TITLE_PROMPT = `Name this coding session (under 80 chars). Be specific to what is being discussed. Your exact output will be displayed to the user, so make sure that it contains ONLY the title itself and nothing else.`;
13
14
  const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
14
15
  messaging: Type.Optional(
@@ -27,6 +28,8 @@ const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
27
28
  enable: Type.Optional(Type.Boolean()),
28
29
  pickerShortcut: Type.Optional(Type.String()),
29
30
  persistRuns: Type.Optional(Type.Boolean()),
31
+ model: Type.Optional(Type.String()),
32
+ thinkingLevel: Type.Optional(Type.String()),
30
33
  deferred: Type.Optional(
31
34
  Type.Object({
32
35
  copyToClipboard: Type.Optional(Type.Boolean()),
@@ -49,9 +52,11 @@ const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
49
52
  enable: Type.Optional(Type.Boolean()),
50
53
  refreshTurns: Type.Optional(Type.Integer({ minimum: 1 })),
51
54
  timeoutSecs: Type.Optional(Type.Integer({ minimum: 1 })),
55
+ tokenBudget: Type.Optional(Type.Integer({ minimum: 1 })),
52
56
  model: Type.Optional(Type.String()),
53
57
  thinkingLevel: Type.Optional(Type.String()),
54
58
  prompt: Type.Optional(Type.String()),
59
+ persistRuns: Type.Optional(Type.Boolean()),
55
60
  }),
56
61
  ),
57
62
  ask: Type.Optional(
@@ -80,13 +85,23 @@ export interface AgentModelSettings {
80
85
  export interface AutoTitleSettings extends AgentModelSettings {
81
86
  refreshTurns: number;
82
87
  timeoutMs: number;
88
+ tokenBudget: number;
83
89
  prompt: string;
90
+ persistRuns: boolean;
84
91
  }
85
92
 
86
93
  export interface AskSettings extends AgentModelSettings {
87
94
  persistRuns: boolean;
88
95
  }
89
96
 
97
+ export interface HandoffSettings extends AgentModelSettings {
98
+ pickerShortcut: KeyId;
99
+ persistRuns: boolean;
100
+ deferred: {
101
+ copyToClipboard: boolean;
102
+ };
103
+ }
104
+
90
105
  export interface FeatureToggles {
91
106
  messaging: boolean;
92
107
  subagents: boolean;
@@ -102,13 +117,7 @@ export interface SessionSettings {
102
117
  subagents: {
103
118
  maxDepth: number;
104
119
  };
105
- handoff: {
106
- pickerShortcut: KeyId;
107
- persistRuns: boolean;
108
- deferred: {
109
- copyToClipboard: boolean;
110
- };
111
- };
120
+ handoff: HandoffSettings;
112
121
  index: {
113
122
  path: string;
114
123
  };
@@ -119,7 +128,7 @@ export interface SessionSettings {
119
128
  type SessionFileSettings = Static<typeof SESSION_FILE_SETTINGS_SCHEMA>;
120
129
 
121
130
  export function getDefaultIndexDir(): string {
122
- return path.join(os.homedir(), ".pi", "agent", "pi-sessions");
131
+ return path.join(getAgentDir(), "pi-sessions");
123
132
  }
124
133
 
125
134
  export function getDefaultIndexPath(): string {
@@ -134,6 +143,10 @@ export function getDefaultHandoffRunsDir(): string {
134
143
  return path.join(getDefaultIndexDir(), "session-handoff");
135
144
  }
136
145
 
146
+ export function getDefaultAutoTitleRunsDir(): string {
147
+ return path.join(getDefaultIndexDir(), "session-auto-title");
148
+ }
149
+
137
150
  function expandHome(rawPath: string): string {
138
151
  if (rawPath === "~") {
139
152
  return os.homedir();
@@ -222,6 +235,7 @@ function resolveSessionSettings(fileSettings: SessionFileSettings): SessionSetti
222
235
  maxDepth: fileSettings.subagents?.maxDepth ?? 2,
223
236
  },
224
237
  handoff: {
238
+ ...resolveAgentModelSettings(fileSettings.handoff),
225
239
  pickerShortcut: normalizePickerShortcut(fileSettings.handoff?.pickerShortcut),
226
240
  persistRuns: fileSettings.handoff?.persistRuns ?? false,
227
241
  deferred: {
@@ -235,7 +249,9 @@ function resolveSessionSettings(fileSettings: SessionFileSettings): SessionSetti
235
249
  ...resolveAgentModelSettings(fileSettings.autoTitle),
236
250
  refreshTurns: fileSettings.autoTitle?.refreshTurns ?? DEFAULT_AUTO_TITLE_REFRESH_TURNS,
237
251
  timeoutMs: (fileSettings.autoTitle?.timeoutSecs ?? DEFAULT_AUTO_TITLE_TIMEOUT_SECONDS) * 1000,
252
+ tokenBudget: fileSettings.autoTitle?.tokenBudget ?? DEFAULT_AUTO_TITLE_TOKEN_BUDGET,
238
253
  prompt: normalizeAutoTitlePrompt(fileSettings.autoTitle?.prompt),
254
+ persistRuns: fileSettings.autoTitle?.persistRuns ?? false,
239
255
  },
240
256
  ask: {
241
257
  ...resolveAgentModelSettings(fileSettings.ask),
@@ -81,9 +81,31 @@ export async function hasAttachedTmuxClients(
81
81
  return result.stdout.split("\n").some((line) => line.trim().length > 0);
82
82
  }
83
83
 
84
+ const pendingCreations = new Map<string, Promise<unknown>>();
85
+
86
+ // Creating a window is check-then-act: it asks whether the tmux session exists and then either
87
+ // creates it or adds to it. Sibling subagents launched in one turn share a session name and race,
88
+ // so all but the winner get `duplicate session`. Only the parent's process ever creates its own
89
+ // session name, so serializing here is enough to close the window.
84
90
  export async function createTmuxWindow(
85
91
  executor: TmuxExecutor,
86
92
  options: CreateTmuxWindowOptions,
93
+ ): Promise<TmuxWindow> {
94
+ const preceding = pendingCreations.get(options.tmuxSession) ?? Promise.resolve();
95
+ const creation = preceding.catch(() => {}).then(() => createWindow(executor, options));
96
+ pendingCreations.set(options.tmuxSession, creation);
97
+ try {
98
+ return await creation;
99
+ } finally {
100
+ if (pendingCreations.get(options.tmuxSession) === creation) {
101
+ pendingCreations.delete(options.tmuxSession);
102
+ }
103
+ }
104
+ }
105
+
106
+ async function createWindow(
107
+ executor: TmuxExecutor,
108
+ options: CreateTmuxWindowOptions,
87
109
  ): Promise<TmuxWindow> {
88
110
  const existing = (await listTmuxWindows(executor, options.tmuxSession)).find(
89
111
  (window) => window.piSessionId === options.piSessionId,
@@ -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
- }
@@ -5,7 +5,7 @@ import { SUBAGENT_REPORT_MESSAGE_SCHEMA } from "./ledger.ts";
5
5
  import { presentSubagentReportMessage } from "./report-message-presenter.ts";
6
6
  import { buildSubagentReportMessageView } from "./report-message-view-model.ts";
7
7
 
8
- export const renderSubagentReportMessage: MessageRenderer = (message, _options, theme) => {
8
+ export const renderSubagentReportMessage: MessageRenderer = (message, options, theme) => {
9
9
  const details = safeParseTypeBoxValue(SUBAGENT_REPORT_MESSAGE_SCHEMA, message.details);
10
10
  if (!details) {
11
11
  return undefined;
@@ -15,7 +15,7 @@ export const renderSubagentReportMessage: MessageRenderer = (message, _options,
15
15
  return undefined;
16
16
  }
17
17
 
18
- const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
18
+ const box = new Box(options.outputPad, 1, (text) => theme.bg("customMessageBg", text));
19
19
  box.addChild(new Text(presentSubagentReportMessage(report, theme), 0, 0));
20
20
  return box;
21
21
  };