pi-sessions 0.10.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 +25 -20
- package/dist/session-messaging/broker/client-frame.js +62 -0
- package/dist/session-messaging/broker/process.js +2 -2
- package/dist/shared/session-broker/framing.js +4 -3
- 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/kickoff.ts +4 -3
- package/extensions/session-messaging/broker/client-frame.ts +113 -0
- package/extensions/session-messaging/broker/process.ts +12 -21
- 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 +13 -2
- package/extensions/shared/tmux.ts +22 -0
- package/extensions/subagents/report-message-renderer.ts +2 -2
- package/package.json +17 -29
- package/dist/shared/session-broker/protocol.js +0 -163
- package/dist/shared/typebox.js +0 -36
|
@@ -2,18 +2,17 @@ import { mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { createServer, type Socket } from "node:net";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { readFrames, writeFrame } from "../../shared/session-broker/framing.ts";
|
|
5
|
-
import {
|
|
6
|
-
CLIENT_FRAME_SCHEMA,
|
|
7
|
-
type OutboundSessionEnvelope,
|
|
8
|
-
type SessionEnvelope,
|
|
9
|
-
type SessionMessagingClientFrame,
|
|
10
|
-
type SessionMessagingIncomingAckClientFrame,
|
|
11
|
-
type SessionMessagingSendClientFrame,
|
|
12
|
-
} from "../../shared/session-broker/protocol.ts";
|
|
13
5
|
import {
|
|
14
6
|
getSessionMessagingDir,
|
|
15
7
|
getSessionMessagingSocketPath,
|
|
16
8
|
} from "../../shared/session-broker/socket-path.ts";
|
|
9
|
+
import {
|
|
10
|
+
type BrokerClientFrame,
|
|
11
|
+
type IncomingAckClientFrame,
|
|
12
|
+
parseClientFrame,
|
|
13
|
+
type RoutedEnvelope,
|
|
14
|
+
type SendClientFrame,
|
|
15
|
+
} from "./client-frame.ts";
|
|
17
16
|
|
|
18
17
|
const SEND_TIMEOUT_MS = 10_000;
|
|
19
18
|
const EXIT_IDLE_MS = 5_000;
|
|
@@ -71,7 +70,7 @@ function resolveTarget(target: string): { sessionId: string; socket: Socket } |
|
|
|
71
70
|
|
|
72
71
|
function handleFrame(
|
|
73
72
|
socket: Socket,
|
|
74
|
-
message:
|
|
73
|
+
message: BrokerClientFrame,
|
|
75
74
|
getSessionId: () => string | undefined,
|
|
76
75
|
setSessionId: (sessionId: string | undefined) => void,
|
|
77
76
|
): void {
|
|
@@ -129,7 +128,7 @@ function handleFrame(
|
|
|
129
128
|
function handleSend(
|
|
130
129
|
socket: Socket,
|
|
131
130
|
currentSessionId: string | undefined,
|
|
132
|
-
message:
|
|
131
|
+
message: SendClientFrame,
|
|
133
132
|
): void {
|
|
134
133
|
const source = currentSessionId ? sessions.get(currentSessionId) : undefined;
|
|
135
134
|
|
|
@@ -206,7 +205,7 @@ function failPendingSendsTo(targetSessionId: string): void {
|
|
|
206
205
|
}
|
|
207
206
|
}
|
|
208
207
|
|
|
209
|
-
function handleIncomingAck(socket: Socket, message:
|
|
208
|
+
function handleIncomingAck(socket: Socket, message: IncomingAckClientFrame): void {
|
|
210
209
|
const pending = pendingSends.get(message.requestId);
|
|
211
210
|
if (!pending || pending.target !== socket) return;
|
|
212
211
|
|
|
@@ -220,11 +219,7 @@ function handleIncomingAck(socket: Socket, message: SessionMessagingIncomingAckC
|
|
|
220
219
|
});
|
|
221
220
|
}
|
|
222
221
|
|
|
223
|
-
function stampEnvelope(
|
|
224
|
-
envelope: OutboundSessionEnvelope,
|
|
225
|
-
source: string,
|
|
226
|
-
target: string,
|
|
227
|
-
): SessionEnvelope {
|
|
222
|
+
function stampEnvelope(envelope: RoutedEnvelope, source: string, target: string): RoutedEnvelope {
|
|
228
223
|
return { ...envelope, source, target };
|
|
229
224
|
}
|
|
230
225
|
|
|
@@ -251,11 +246,7 @@ async function readConnection(
|
|
|
251
246
|
setSessionId: (sessionId: string | undefined) => void,
|
|
252
247
|
): Promise<void> {
|
|
253
248
|
try {
|
|
254
|
-
for await (const frame of readFrames(
|
|
255
|
-
socket,
|
|
256
|
-
CLIENT_FRAME_SCHEMA,
|
|
257
|
-
"Invalid session messaging client frame",
|
|
258
|
-
)) {
|
|
249
|
+
for await (const frame of readFrames(socket, parseClientFrame)) {
|
|
259
250
|
handleFrame(socket, frame, getSessionId, setSessionId);
|
|
260
251
|
}
|
|
261
252
|
} catch (error) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Static, Type } from "typebox";
|
|
2
|
+
|
|
3
|
+
const SCOPE_SCHEMA = Type.Union([
|
|
4
|
+
Type.Literal("user"),
|
|
5
|
+
Type.Literal("branch"),
|
|
6
|
+
Type.Literal("tree"),
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
/** Registered when subagents are active; the roster is what makes branch/tree answerable. */
|
|
10
|
+
export const SESSION_REACHABLE_PARAMS = Type.Object({
|
|
11
|
+
scope: Type.Optional(
|
|
12
|
+
Type.Union([Type.Literal("user"), Type.Literal("branch"), Type.Literal("tree")], {
|
|
13
|
+
description:
|
|
14
|
+
'Which sessions to list: "user" for live user-facing sessions, "branch" for subagents launched from the active conversation branch, "tree" for subagents launched anywhere in this session\'s conversation tree. Defaults to "user".',
|
|
15
|
+
}),
|
|
16
|
+
),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/** Registered when subagents are inactive; only live user sessions can ever be returned. */
|
|
20
|
+
export const SESSION_REACHABLE_USER_PARAMS = Type.Object({});
|
|
21
|
+
|
|
22
|
+
const REACHABLE_USER_SESSION_SCHEMA = Type.Object({
|
|
23
|
+
kind: Type.Literal("user"),
|
|
24
|
+
sessionId: Type.String(),
|
|
25
|
+
title: Type.Optional(Type.String()),
|
|
26
|
+
cwd: Type.Optional(Type.String()),
|
|
27
|
+
modifiedAt: Type.Optional(Type.String()),
|
|
28
|
+
relation: Type.Optional(Type.String()),
|
|
29
|
+
state: Type.Union([Type.Literal("live"), Type.Literal("starting")]),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const REACHABLE_SUBAGENT_SCHEMA = Type.Object({
|
|
33
|
+
kind: Type.Literal("subagent"),
|
|
34
|
+
sessionId: Type.String(),
|
|
35
|
+
title: Type.String(),
|
|
36
|
+
goal: Type.String(),
|
|
37
|
+
cwd: Type.String(),
|
|
38
|
+
state: Type.String(),
|
|
39
|
+
depth: Type.Number(),
|
|
40
|
+
onActiveBranch: Type.Boolean(),
|
|
41
|
+
launchedAt: Type.String(),
|
|
42
|
+
model: Type.Optional(Type.String()),
|
|
43
|
+
ownerSessionId: Type.String(),
|
|
44
|
+
ownerTitle: Type.String(),
|
|
45
|
+
ownerIsCurrentSession: Type.Boolean(),
|
|
46
|
+
resumeCommand: Type.String(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const SESSION_REACHABLE_TOOL_DETAILS_SCHEMA = Type.Object({
|
|
50
|
+
scope: SCOPE_SCHEMA,
|
|
51
|
+
sessions: Type.Array(Type.Union([REACHABLE_USER_SESSION_SCHEMA, REACHABLE_SUBAGENT_SCHEMA])),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export type SessionReachableScope = Static<typeof SCOPE_SCHEMA>;
|
|
55
|
+
export type SessionReachableParams = Static<typeof SESSION_REACHABLE_PARAMS>;
|
|
56
|
+
export type ReachableUserSession = Static<typeof REACHABLE_USER_SESSION_SCHEMA>;
|
|
57
|
+
export type ReachableSubagent = Static<typeof REACHABLE_SUBAGENT_SCHEMA>;
|
|
58
|
+
export type ReachableSession = ReachableUserSession | ReachableSubagent;
|
|
59
|
+
export type SessionReachableToolDetails = Static<typeof SESSION_REACHABLE_TOOL_DETAILS_SCHEMA>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ExpandableContentPresentation } from "../../shared/rendering/expandable-content-layout.ts";
|
|
2
|
+
import type { RenderTheme } from "../../shared/rendering/theme.ts";
|
|
3
|
+
import type {
|
|
4
|
+
ReachableSessionRowViewModel,
|
|
5
|
+
SessionReachableViewModel,
|
|
6
|
+
} from "./reachable-view-model.ts";
|
|
7
|
+
|
|
8
|
+
const COLLAPSED_ROW_COUNT = 6;
|
|
9
|
+
|
|
10
|
+
export function buildSessionReachablePresentation(
|
|
11
|
+
model: SessionReachableViewModel,
|
|
12
|
+
theme: RenderTheme,
|
|
13
|
+
): ExpandableContentPresentation {
|
|
14
|
+
const header = theme.fg(
|
|
15
|
+
"muted",
|
|
16
|
+
`scope: ${model.scope} • ${formatSessionCount(model.rows.length)}`,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
if (model.rows.length === 0) {
|
|
20
|
+
return {
|
|
21
|
+
header,
|
|
22
|
+
metadata: [theme.fg("warning", formatEmptyText(model.scope))],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
header,
|
|
28
|
+
body: {
|
|
29
|
+
text: model.rows.map((row, index) => formatRow(row, index, theme)).join("\n"),
|
|
30
|
+
collapsedRows: COLLAPSED_ROW_COUNT,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatRow(row: ReachableSessionRowViewModel, index: number, theme: RenderTheme): string {
|
|
36
|
+
const annotations =
|
|
37
|
+
row.annotations.length > 0 ? ` ${theme.fg("dim", `[${row.annotations.join(" • ")}]`)}` : "";
|
|
38
|
+
const location = row.location ? ` ${theme.fg("dim", `(${row.location})`)}` : "";
|
|
39
|
+
const heading = `${index + 1}. ${theme.bold(row.label)}${annotations}${location}`;
|
|
40
|
+
return row.detail ? `${heading}\n${theme.fg("dim", ` ${row.detail}`)}` : heading;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatSessionCount(count: number): string {
|
|
44
|
+
return count === 1 ? "1 session" : `${count} sessions`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatEmptyText(scope: SessionReachableViewModel["scope"]): string {
|
|
48
|
+
return scope === "user" ? "No other live sessions." : "No subagents launched from this session.";
|
|
49
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineTool,
|
|
3
|
+
type ExtensionContext,
|
|
4
|
+
SessionManager,
|
|
5
|
+
type ToolDefinition,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
8
|
+
import { isSessionStarting } from "../../session-handoff/metadata.ts";
|
|
9
|
+
import { formatError } from "../../shared/errors.ts";
|
|
10
|
+
import { ExpandableContentLayout } from "../../shared/rendering/expandable-content-layout.ts";
|
|
11
|
+
import {
|
|
12
|
+
getSessionById,
|
|
13
|
+
type SessionLineageRow,
|
|
14
|
+
withSessionIndex,
|
|
15
|
+
} from "../../shared/session-index/index.ts";
|
|
16
|
+
import { safeParseTypeBoxValue } from "../../shared/typebox.ts";
|
|
17
|
+
import {
|
|
18
|
+
type ReachableSubagent,
|
|
19
|
+
type ReachableUserSession,
|
|
20
|
+
SESSION_REACHABLE_PARAMS,
|
|
21
|
+
SESSION_REACHABLE_TOOL_DETAILS_SCHEMA,
|
|
22
|
+
SESSION_REACHABLE_USER_PARAMS,
|
|
23
|
+
type SessionReachableParams,
|
|
24
|
+
type SessionReachableScope,
|
|
25
|
+
type SessionReachableToolDetails,
|
|
26
|
+
} from "./reachable-contract.ts";
|
|
27
|
+
import { buildSessionReachablePresentation } from "./reachable-presenter.ts";
|
|
28
|
+
import { buildSessionReachableView } from "./reachable-view-model.ts";
|
|
29
|
+
|
|
30
|
+
/** The subagent facts this tool needs; the roster satisfies it structurally. */
|
|
31
|
+
export interface ReachableSubagentEntry {
|
|
32
|
+
sessionId: string;
|
|
33
|
+
title: string;
|
|
34
|
+
goal: string;
|
|
35
|
+
cwd: string;
|
|
36
|
+
state: string;
|
|
37
|
+
depth: number;
|
|
38
|
+
onActiveBranch: boolean;
|
|
39
|
+
launchedAt: string;
|
|
40
|
+
model?: string | undefined;
|
|
41
|
+
ownerSessionId: string;
|
|
42
|
+
ownerTitle: string;
|
|
43
|
+
ownerIsCurrentSession: boolean;
|
|
44
|
+
resumeCommand: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SessionReachableDeps {
|
|
48
|
+
indexPath: string;
|
|
49
|
+
listSessions(): Promise<string[]>;
|
|
50
|
+
getRelationTo(sessionId: string): string | undefined;
|
|
51
|
+
listSubagents?:
|
|
52
|
+
| ((
|
|
53
|
+
scope: Exclude<SessionReachableScope, "user">,
|
|
54
|
+
) => Promise<readonly ReachableSubagentEntry[]>)
|
|
55
|
+
| undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface SessionReachableRendererState {
|
|
59
|
+
layout?: ExpandableContentLayout | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createSessionReachableTool(deps: SessionReachableDeps): ToolDefinition {
|
|
63
|
+
const subagentsAvailable = Boolean(deps.listSubagents);
|
|
64
|
+
|
|
65
|
+
return defineTool({
|
|
66
|
+
name: "session_reachable",
|
|
67
|
+
label: "Reachable Sessions",
|
|
68
|
+
description: subagentsAvailable
|
|
69
|
+
? "List the sessions this session can address: live user sessions, or its own subagents."
|
|
70
|
+
: "List the live user sessions this session can address.",
|
|
71
|
+
promptSnippet: subagentsAvailable
|
|
72
|
+
? "List addressable live sessions and owned subagents"
|
|
73
|
+
: "List addressable live sessions",
|
|
74
|
+
promptGuidelines: [
|
|
75
|
+
"Before session_send_message, use session_reachable to find the target session id.",
|
|
76
|
+
],
|
|
77
|
+
parameters: subagentsAvailable ? SESSION_REACHABLE_PARAMS : SESSION_REACHABLE_USER_PARAMS,
|
|
78
|
+
renderResult(result, options, theme, context) {
|
|
79
|
+
const output = result.content.find((item) => item.type === "text")?.text ?? "";
|
|
80
|
+
if (context.isError) {
|
|
81
|
+
return new Text(output ? theme.fg("error", output) : "", 0, 0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const details = safeParseTypeBoxValue(SESSION_REACHABLE_TOOL_DETAILS_SCHEMA, result.details);
|
|
85
|
+
if (!details) {
|
|
86
|
+
return new Text(output, 0, 0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const state = context.state as SessionReachableRendererState;
|
|
90
|
+
const layout = state.layout ?? new ExpandableContentLayout(theme);
|
|
91
|
+
state.layout = layout;
|
|
92
|
+
layout.update(
|
|
93
|
+
buildSessionReachablePresentation(buildSessionReachableView(details), theme),
|
|
94
|
+
options.expanded,
|
|
95
|
+
);
|
|
96
|
+
return layout;
|
|
97
|
+
},
|
|
98
|
+
async execute(_toolCallId, params: SessionReachableParams, _signal, _onUpdate, ctx) {
|
|
99
|
+
const scope = params.scope ?? "user";
|
|
100
|
+
const sessions =
|
|
101
|
+
scope === "user"
|
|
102
|
+
? await listReachableUserSessions(deps, ctx)
|
|
103
|
+
: await listReachableSubagents(deps, scope);
|
|
104
|
+
|
|
105
|
+
const details: SessionReachableToolDetails = { scope, sessions };
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: "text" as const, text: JSON.stringify(details, null, 2) }],
|
|
108
|
+
details,
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function listReachableUserSessions(
|
|
115
|
+
deps: SessionReachableDeps,
|
|
116
|
+
ctx: ExtensionContext,
|
|
117
|
+
): Promise<ReachableUserSession[]> {
|
|
118
|
+
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
119
|
+
let liveSessionIds: string[];
|
|
120
|
+
try {
|
|
121
|
+
liveSessionIds = await deps.listSessions();
|
|
122
|
+
} catch (error) {
|
|
123
|
+
throw new Error(`Session messaging is unavailable: ${formatError(error)}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const targetIds = liveSessionIds.filter((sessionId) => sessionId !== currentSessionId);
|
|
127
|
+
return withSessionIndex(deps.indexPath, { mode: "read", required: true }, ({ db }) => {
|
|
128
|
+
const sessions: ReachableUserSession[] = [];
|
|
129
|
+
for (const sessionId of targetIds) {
|
|
130
|
+
const row = getSessionById(db, sessionId);
|
|
131
|
+
if (row?.sessionOrigin === "subagent") {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const relation = deps.getRelationTo(sessionId);
|
|
135
|
+
sessions.push({
|
|
136
|
+
kind: "user",
|
|
137
|
+
sessionId,
|
|
138
|
+
state: row && isStartingSession(row) ? "starting" : "live",
|
|
139
|
+
...(row?.sessionName ? { title: row.sessionName } : {}),
|
|
140
|
+
...(row?.cwd ? { cwd: row.cwd } : {}),
|
|
141
|
+
...(row?.modifiedAt ? { modifiedAt: row.modifiedAt } : {}),
|
|
142
|
+
...(relation === undefined ? {} : { relation }),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return sessions.sort(compareByModifiedDesc);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function listReachableSubagents(
|
|
150
|
+
deps: SessionReachableDeps,
|
|
151
|
+
scope: Exclude<SessionReachableScope, "user">,
|
|
152
|
+
): Promise<ReachableSubagent[]> {
|
|
153
|
+
if (!deps.listSubagents) {
|
|
154
|
+
throw new Error(`session_reachable scope "${scope}" requires subagents to be active.`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const entries = await deps.listSubagents(scope);
|
|
159
|
+
return entries.map((entry) => ({
|
|
160
|
+
kind: "subagent",
|
|
161
|
+
sessionId: entry.sessionId,
|
|
162
|
+
title: entry.title,
|
|
163
|
+
goal: entry.goal,
|
|
164
|
+
cwd: entry.cwd,
|
|
165
|
+
state: entry.state,
|
|
166
|
+
depth: entry.depth,
|
|
167
|
+
onActiveBranch: entry.onActiveBranch,
|
|
168
|
+
launchedAt: entry.launchedAt,
|
|
169
|
+
...(entry.model === undefined ? {} : { model: entry.model }),
|
|
170
|
+
ownerSessionId: entry.ownerSessionId,
|
|
171
|
+
ownerTitle: entry.ownerTitle,
|
|
172
|
+
ownerIsCurrentSession: entry.ownerIsCurrentSession,
|
|
173
|
+
resumeCommand: entry.resumeCommand,
|
|
174
|
+
}));
|
|
175
|
+
} catch (error) {
|
|
176
|
+
throw new Error(`Subagent roster is unavailable: ${formatError(error)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isStartingSession(row: SessionLineageRow): boolean {
|
|
181
|
+
try {
|
|
182
|
+
return isSessionStarting(SessionManager.open(row.sessionPath).getBranch());
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function compareByModifiedDesc(left: ReachableUserSession, right: ReachableUserSession): number {
|
|
189
|
+
return (right.modifiedAt ?? "").localeCompare(left.modifiedAt ?? "");
|
|
190
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { formatSessionTitleOrShortId } from "../../shared/session-ui.ts";
|
|
3
|
+
import { normalizeOptionalText } from "../../shared/text.ts";
|
|
4
|
+
import type {
|
|
5
|
+
ReachableSession,
|
|
6
|
+
SessionReachableScope,
|
|
7
|
+
SessionReachableToolDetails,
|
|
8
|
+
} from "./reachable-contract.ts";
|
|
9
|
+
|
|
10
|
+
export interface ReachableSessionRowViewModel {
|
|
11
|
+
label: string;
|
|
12
|
+
sessionId: string;
|
|
13
|
+
annotations: string[];
|
|
14
|
+
location?: string | undefined;
|
|
15
|
+
detail?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SessionReachableViewModel {
|
|
19
|
+
scope: SessionReachableScope;
|
|
20
|
+
rows: ReachableSessionRowViewModel[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildSessionReachableView(
|
|
24
|
+
details: SessionReachableToolDetails,
|
|
25
|
+
): SessionReachableViewModel {
|
|
26
|
+
return {
|
|
27
|
+
scope: details.scope,
|
|
28
|
+
rows: details.sessions.map(buildReachableSessionRow),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildReachableSessionRow(session: ReachableSession): ReachableSessionRowViewModel {
|
|
33
|
+
if (session.kind === "user") {
|
|
34
|
+
return {
|
|
35
|
+
label: formatSessionTitleOrShortId(session.title, session.sessionId),
|
|
36
|
+
sessionId: session.sessionId,
|
|
37
|
+
annotations: [session.state, ...(session.relation ? [session.relation] : [])],
|
|
38
|
+
location: formatLocation(session.cwd),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
label: formatSessionTitleOrShortId(session.title, session.sessionId),
|
|
44
|
+
sessionId: session.sessionId,
|
|
45
|
+
annotations: [
|
|
46
|
+
session.state,
|
|
47
|
+
`depth ${session.depth}`,
|
|
48
|
+
...(session.onActiveBranch ? [] : ["history"]),
|
|
49
|
+
...(session.ownerIsCurrentSession ? [] : [`owner ${session.ownerTitle}`]),
|
|
50
|
+
],
|
|
51
|
+
location: formatLocation(session.cwd),
|
|
52
|
+
detail: normalizeOptionalText(session.goal),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatLocation(cwd: string | undefined): string | undefined {
|
|
57
|
+
if (!cwd) return undefined;
|
|
58
|
+
return path.basename(cwd) || cwd;
|
|
59
|
+
}
|
|
@@ -17,7 +17,7 @@ export const renderIncomingSessionMessage: MessageRenderer = (message, options,
|
|
|
17
17
|
buildIncomingMessagePresentation(buildIncomingMessageView(details), theme),
|
|
18
18
|
options.expanded,
|
|
19
19
|
);
|
|
20
|
-
const box = new Box(
|
|
20
|
+
const box = new Box(options.outputPad, 1, (text) => theme.bg("toolSuccessBg", text));
|
|
21
21
|
box.addChild(layout);
|
|
22
22
|
return box;
|
|
23
23
|
};
|
|
@@ -66,13 +66,8 @@ export function createSessionSendMessageTool(
|
|
|
66
66
|
? "Send a message to another pi session or a subagent"
|
|
67
67
|
: "Send a message to another live pi session";
|
|
68
68
|
const promptGuidelines = options.wakeCapable
|
|
69
|
-
? [
|
|
70
|
-
|
|
71
|
-
"It is always possible to session_send_message to an owned subagent",
|
|
72
|
-
]
|
|
73
|
-
: [
|
|
74
|
-
"Before session_send_message, use session_search with live: true to list all live sessions and find the target.",
|
|
75
|
-
];
|
|
69
|
+
? ["Use session_reachable to discover live sessions and owned subagents."]
|
|
70
|
+
: ["Use session_reachable to list live sessions and find the target."];
|
|
76
71
|
|
|
77
72
|
return defineTool({
|
|
78
73
|
name: "session_send_message",
|
|
@@ -194,7 +189,12 @@ export function createSessionCancelTool(service: CancelSessionService): ToolDefi
|
|
|
194
189
|
throw new Error(buildUnknownCancellationError(result.target));
|
|
195
190
|
}
|
|
196
191
|
return {
|
|
197
|
-
content: [
|
|
192
|
+
content: [
|
|
193
|
+
{
|
|
194
|
+
type: "text" as const,
|
|
195
|
+
text: buildCancelSessionModelText(details),
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
198
|
details,
|
|
199
199
|
};
|
|
200
200
|
}
|
|
@@ -228,7 +228,7 @@ function syncSessionFileWithDb(
|
|
|
228
228
|
const stat = statSync(sessionFile);
|
|
229
229
|
|
|
230
230
|
if (baseline && isIndexCurrent(baseline, stat, sessionOrigin)) {
|
|
231
|
-
db.transaction(() => writeHookSyncMetadata(db, eventType))
|
|
231
|
+
db.transaction(() => writeHookSyncMetadata(db, eventType));
|
|
232
232
|
return true;
|
|
233
233
|
}
|
|
234
234
|
|
|
@@ -279,11 +279,6 @@ function isIndexCurrent(
|
|
|
279
279
|
);
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
// All write transactions are immediate: they read before they write, and a
|
|
283
|
-
// deferred transaction whose snapshot goes stale fails with SQLITE_BUSY on the
|
|
284
|
-
// read-to-write upgrade without ever invoking the busy handler. Immediate mode
|
|
285
|
-
// takes the write lock at BEGIN, where busy_timeout queues us behind concurrent
|
|
286
|
-
// writers from other pi processes.
|
|
287
282
|
function applyFullSync(
|
|
288
283
|
db: SessionIndexDatabase,
|
|
289
284
|
extracted: ExtractedSessionRecord,
|
|
@@ -312,7 +307,7 @@ function applyFullSync(
|
|
|
312
307
|
}
|
|
313
308
|
|
|
314
309
|
writeHookSyncMetadata(db, eventType);
|
|
315
|
-
})
|
|
310
|
+
});
|
|
316
311
|
}
|
|
317
312
|
|
|
318
313
|
function applyTailSync(
|
|
@@ -322,60 +317,58 @@ function applyTailSync(
|
|
|
322
317
|
eventType: string,
|
|
323
318
|
sessionOrigin?: SessionOrigin,
|
|
324
319
|
): boolean {
|
|
325
|
-
return db
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
320
|
+
return db.transaction((): boolean => {
|
|
321
|
+
// Another process may have advanced the index between our baseline read
|
|
322
|
+
// and this transaction; the tail deltas would then double-count.
|
|
323
|
+
const current = getSessionRowByPath(db, baseline.sessionPath);
|
|
324
|
+
if (
|
|
325
|
+
!current ||
|
|
326
|
+
current.sessionId !== baseline.sessionId ||
|
|
327
|
+
current.indexedFileSize !== baseline.indexedFileSize
|
|
328
|
+
) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
337
331
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (scan.sessionName !== undefined && scan.sessionName !== baseline.sessionName) {
|
|
342
|
-
clearSessionChunksBySourceKind(db, baseline.sessionId, "session_name");
|
|
343
|
-
if (scan.sessionName && scan.sessionNameEntryId) {
|
|
344
|
-
insertTextChunk(db, {
|
|
345
|
-
sessionId: baseline.sessionId,
|
|
346
|
-
...createSessionNameChunk(
|
|
347
|
-
scan.sessionName,
|
|
348
|
-
scan.sessionNameTs ?? baseline.startedAt,
|
|
349
|
-
scan.sessionNameEntryId,
|
|
350
|
-
),
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
}
|
|
332
|
+
const scan = tail.scan;
|
|
333
|
+
upsertSession(db, buildTailSessionRow(baseline, tail, sessionOrigin), "hook");
|
|
354
334
|
|
|
355
|
-
|
|
356
|
-
|
|
335
|
+
if (scan.sessionName !== undefined && scan.sessionName !== baseline.sessionName) {
|
|
336
|
+
clearSessionChunksBySourceKind(db, baseline.sessionId, "session_name");
|
|
337
|
+
if (scan.sessionName && scan.sessionNameEntryId) {
|
|
357
338
|
insertTextChunk(db, {
|
|
358
339
|
sessionId: baseline.sessionId,
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
340
|
+
...createSessionNameChunk(
|
|
341
|
+
scan.sessionName,
|
|
342
|
+
scan.sessionNameTs ?? baseline.startedAt,
|
|
343
|
+
scan.sessionNameEntryId,
|
|
344
|
+
),
|
|
364
345
|
});
|
|
365
346
|
}
|
|
347
|
+
}
|
|
366
348
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
349
|
+
if (!baseline.handoffGoal && scan.handoffMetadata) {
|
|
350
|
+
const { entryId, ts, metadata } = scan.handoffMetadata;
|
|
351
|
+
insertTextChunk(db, {
|
|
352
|
+
sessionId: baseline.sessionId,
|
|
353
|
+
entryId,
|
|
354
|
+
entryType: "custom",
|
|
355
|
+
ts,
|
|
356
|
+
sourceKind: "handoff_goal",
|
|
357
|
+
text: metadata.goal,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
370
360
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
361
|
+
for (const chunk of scan.chunks) {
|
|
362
|
+
insertTextChunk(db, { sessionId: baseline.sessionId, ...chunk });
|
|
363
|
+
}
|
|
374
364
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
365
|
+
for (const fileTouch of scan.fileTouches) {
|
|
366
|
+
insertSessionFileTouch(db, { sessionId: baseline.sessionId, ...fileTouch });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
writeHookSyncMetadata(db, eventType);
|
|
370
|
+
return true;
|
|
371
|
+
});
|
|
379
372
|
}
|
|
380
373
|
|
|
381
374
|
function buildTailSessionRow(
|