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.
- package/README.md +29 -20
- package/dist/session-messaging/broker/client-frame.js +62 -0
- package/dist/session-messaging/broker/process.js +234 -0
- package/dist/shared/session-broker/framing.js +59 -0
- package/dist/shared/session-broker/socket-path.js +19 -0
- package/extensions/pi-sessions.ts +14 -1
- package/extensions/session-auto-title/generate.ts +56 -66
- package/extensions/session-auto-title/install.ts +2 -0
- package/extensions/session-auto-title/runs.ts +50 -0
- package/extensions/session-handoff/board-view-model.ts +2 -4
- package/extensions/session-handoff/bootstrap.ts +14 -16
- package/extensions/session-handoff/extract.ts +34 -16
- package/extensions/session-handoff/install.ts +1 -1
- package/extensions/session-handoff/kickoff.ts +4 -3
- package/extensions/session-handoff/launch-target.ts +1 -1
- package/extensions/session-handoff/metadata.ts +72 -23
- package/extensions/session-messaging/broker/client-frame.ts +113 -0
- package/extensions/session-messaging/broker/process.ts +12 -21
- package/extensions/session-messaging/broker/spawn.ts +12 -7
- package/extensions/session-messaging/pi/reachable-contract.ts +59 -0
- package/extensions/session-messaging/pi/reachable-presenter.ts +49 -0
- package/extensions/session-messaging/pi/reachable-tool.ts +190 -0
- package/extensions/session-messaging/pi/reachable-view-model.ts +59 -0
- package/extensions/session-messaging/pi/renderer.ts +1 -1
- package/extensions/session-messaging/pi/tools.ts +8 -8
- package/extensions/session-search/hooks.ts +45 -52
- package/extensions/session-search/install.ts +3 -158
- package/extensions/session-search/reindex.ts +3 -3
- package/extensions/session-search/renderer.ts +1 -32
- package/extensions/session-search/tool-contract.ts +1 -21
- package/extensions/shared/model-runtime.ts +8 -1
- package/extensions/shared/session-broker/client.ts +3 -4
- package/extensions/shared/session-broker/framing.ts +6 -7
- package/extensions/shared/session-index/lineage.ts +6 -2
- package/extensions/shared/session-index/sqlite.ts +44 -17
- package/extensions/shared/settings.ts +25 -9
- package/extensions/shared/tmux.ts +22 -0
- package/extensions/subagents/classify.ts +6 -0
- package/extensions/subagents/install.ts +22 -137
- package/extensions/subagents/report-message-renderer.ts +2 -2
- package/extensions/subagents/settle.ts +182 -0
- 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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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>):
|
|
35
|
+
new (path: string, options?: Record<string, unknown>): SqliteDriver;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
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("
|
|
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, {
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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,
|
|
2
|
-
import {
|
|
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 {
|
|
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?:
|
|
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
|
-
|
|
138
|
+
settledChildLifecycle.cancel();
|
|
154
139
|
if (current?.child) {
|
|
155
|
-
current.child.reportsAtTurnStart =
|
|
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
|
-
|
|
165
|
-
await reconciler.reconcile()
|
|
166
|
-
|
|
149
|
+
const reconciliation = hasSubagentLaunchEntries(session.parent.getBranch())
|
|
150
|
+
? await reconciler.reconcile()
|
|
151
|
+
: undefined;
|
|
167
152
|
if (session.child) {
|
|
168
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
};
|