pi-sessions 0.5.0 → 0.6.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 (44) hide show
  1. package/README.md +19 -9
  2. package/extensions/session-ask.ts +6 -5
  3. package/extensions/session-auto-title/command.ts +1 -1
  4. package/extensions/session-auto-title/controller.ts +3 -3
  5. package/extensions/session-auto-title/generate.ts +4 -9
  6. package/extensions/session-auto-title/model.ts +1 -1
  7. package/extensions/session-auto-title/retitle.ts +5 -5
  8. package/extensions/session-auto-title/state.ts +1 -1
  9. package/extensions/session-auto-title/wizard.ts +4 -4
  10. package/extensions/session-auto-title.ts +8 -8
  11. package/extensions/session-handoff/extract.ts +18 -5
  12. package/extensions/session-handoff/metadata.ts +4 -1
  13. package/extensions/session-handoff/picker.ts +2 -2
  14. package/extensions/session-handoff/query.ts +6 -8
  15. package/extensions/session-handoff/refs.ts +2 -2
  16. package/extensions/session-handoff/spawn.ts +1 -1
  17. package/extensions/session-handoff.ts +18 -8
  18. package/extensions/session-hooks.ts +3 -3
  19. package/extensions/session-index.ts +4 -4
  20. package/extensions/session-messaging/broker/process.ts +305 -0
  21. package/extensions/session-messaging/broker/spawn.ts +159 -0
  22. package/extensions/session-messaging/pi/client.ts +256 -0
  23. package/extensions/session-messaging/pi/incoming-runtime.ts +130 -0
  24. package/extensions/session-messaging/pi/message-contracts.ts +37 -0
  25. package/extensions/session-messaging/pi/message-view.ts +120 -0
  26. package/extensions/session-messaging/pi/renderer.ts +16 -0
  27. package/extensions/session-messaging/pi/service.ts +315 -0
  28. package/extensions/session-messaging/pi/tools.ts +144 -0
  29. package/extensions/session-messaging/shared/framing.ts +72 -0
  30. package/extensions/session-messaging/shared/protocol.ts +102 -0
  31. package/extensions/session-messaging/shared/socket-path.ts +30 -0
  32. package/extensions/session-messaging.ts +30 -0
  33. package/extensions/session-search/extract.ts +4 -4
  34. package/extensions/session-search/hooks.ts +3 -3
  35. package/extensions/session-search/reindex.ts +2 -2
  36. package/extensions/session-search.ts +4 -4
  37. package/extensions/shared/session-index/common.ts +3 -3
  38. package/extensions/shared/session-index/index.ts +5 -5
  39. package/extensions/shared/session-index/lineage.ts +9 -2
  40. package/extensions/shared/session-index/schema.ts +3 -3
  41. package/extensions/shared/session-index/search.ts +4 -4
  42. package/extensions/shared/session-index/store.ts +2 -2
  43. package/extensions/shared/settings.ts +1 -1
  44. package/package.json +12 -11
@@ -0,0 +1,130 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { SessionLineageRelation } from "../../shared/session-index/index.ts";
3
+ import { safeParseTypeBoxValue } from "../../shared/typebox.ts";
4
+ import type { SessionMessagePayload } from "../shared/protocol.ts";
5
+ import { RECEIVED_MESSAGE_ENTRY_SCHEMA, type ReceivedMessageEntry } from "./message-contracts.ts";
6
+ export const MESSAGE_RECEIVED_CUSTOM_TYPE = "pi-sessions.message_received";
7
+ export const SESSION_MESSAGE_CUSTOM_TYPE = "pi-sessions.session_message";
8
+
9
+ type DeliveryResult = { delivered: true } | { delivered: false; error: string };
10
+
11
+ interface IncomingMessageActions {
12
+ appendEntry(customType: string, data: unknown): void;
13
+ sendMessage(
14
+ message: {
15
+ customType: string;
16
+ content: string;
17
+ display: boolean;
18
+ details: unknown;
19
+ },
20
+ options: { triggerTurn: true } | { deliverAs: "steer" },
21
+ ): void;
22
+ }
23
+
24
+ export class IncomingSessionMessageRuntime {
25
+ private context: ExtensionContext | undefined;
26
+ private readonly actions: IncomingMessageActions;
27
+
28
+ constructor(pi: ExtensionAPI) {
29
+ this.actions = {
30
+ appendEntry: pi.appendEntry.bind(pi),
31
+ sendMessage: pi.sendMessage.bind(pi),
32
+ };
33
+
34
+ pi.on("session_start", (_event, ctx) => {
35
+ this.context = ctx;
36
+ });
37
+ pi.on("session_shutdown", () => {
38
+ this.context = undefined;
39
+ });
40
+ }
41
+
42
+ deliver(
43
+ message: SessionMessagePayload,
44
+ relation: SessionLineageRelation | undefined,
45
+ ): DeliveryResult {
46
+ const ctx = this.context;
47
+ if (!ctx) {
48
+ return { delivered: false, error: "Target session is not ready." };
49
+ }
50
+
51
+ try {
52
+ const received = buildReceivedMessageEntry(message, relation);
53
+ this.actions.appendEntry(MESSAGE_RECEIVED_CUSTOM_TYPE, received);
54
+ this.inject(ctx, received);
55
+ return { delivered: true };
56
+ } catch (error) {
57
+ return { delivered: false, error: formatError(error) };
58
+ }
59
+ }
60
+
61
+ replayPending(ctx: ExtensionContext): void {
62
+ const entries = ctx.sessionManager.getEntries();
63
+ const deliveredMessageIds = new Set<string>();
64
+ const pendingReceipts: ReceivedMessageEntry[] = [];
65
+
66
+ for (const entry of entries) {
67
+ if (entry.type === "custom_message" && entry.customType === SESSION_MESSAGE_CUSTOM_TYPE) {
68
+ const delivered = safeParseTypeBoxValue(RECEIVED_MESSAGE_ENTRY_SCHEMA, entry.details);
69
+ if (delivered) {
70
+ deliveredMessageIds.add(delivered.messageId);
71
+ }
72
+ continue;
73
+ }
74
+
75
+ if (entry.type !== "custom" || entry.customType !== MESSAGE_RECEIVED_CUSTOM_TYPE) {
76
+ continue;
77
+ }
78
+
79
+ const received = safeParseTypeBoxValue(RECEIVED_MESSAGE_ENTRY_SCHEMA, entry.data);
80
+ if (received) {
81
+ pendingReceipts.push(received);
82
+ }
83
+ }
84
+
85
+ for (const received of pendingReceipts) {
86
+ if (!deliveredMessageIds.has(received.messageId)) {
87
+ this.inject(ctx, received);
88
+ }
89
+ }
90
+ }
91
+
92
+ private inject(ctx: ExtensionContext, received: ReceivedMessageEntry): void {
93
+ const deliveryOptions = ctx.isIdle()
94
+ ? { triggerTurn: true as const }
95
+ : { deliverAs: "steer" as const };
96
+
97
+ this.actions.sendMessage(
98
+ {
99
+ customType: SESSION_MESSAGE_CUSTOM_TYPE,
100
+ content: formatIncomingMessageForModel(received),
101
+ display: true,
102
+ details: received,
103
+ },
104
+ deliveryOptions,
105
+ );
106
+ }
107
+ }
108
+
109
+ function buildReceivedMessageEntry(
110
+ message: SessionMessagePayload,
111
+ relation: SessionLineageRelation | undefined,
112
+ ): ReceivedMessageEntry {
113
+ return {
114
+ ...message,
115
+ receivedAt: new Date().toISOString(),
116
+ ...(relation === undefined ? {} : { relation }),
117
+ };
118
+ }
119
+
120
+ function formatIncomingMessageForModel(received: ReceivedMessageEntry): string {
121
+ const title = received.source.sessionName?.trim();
122
+ const titlePart = title ? ` "${title}"` : "";
123
+ const relationPart = received.relation ? `, relation: ${received.relation}` : "";
124
+ const responseLine = received.requestResponse ? "\n\nResponse requested." : "";
125
+ return `Incoming message from session${titlePart} (session: ${received.source.sessionId}${relationPart}):\n\n${received.body}${responseLine}`;
126
+ }
127
+
128
+ function formatError(error: unknown): string {
129
+ return error instanceof Error && error.message.trim() ? error.message : String(error);
130
+ }
@@ -0,0 +1,37 @@
1
+ import { type Static, Type } from "typebox";
2
+
3
+ export const SEND_MESSAGE_PARAMS = Type.Object({
4
+ session: Type.String({ description: "Pi session UUID." }),
5
+ message: Type.String({
6
+ description: "Message to send to the target session.",
7
+ }),
8
+ requestResponse: Type.Optional(
9
+ Type.Boolean({
10
+ description:
11
+ "Whether the target session should respond with completion/results back to this session.",
12
+ }),
13
+ ),
14
+ });
15
+
16
+ export const RECEIVED_MESSAGE_ENTRY_SCHEMA = Type.Object({
17
+ messageId: Type.String(),
18
+ source: Type.Object({
19
+ sessionId: Type.String(),
20
+ sessionName: Type.Optional(Type.String()),
21
+ cwd: Type.String(),
22
+ }),
23
+ target: Type.Object({
24
+ sessionId: Type.String(),
25
+ sessionName: Type.Optional(Type.String()),
26
+ cwd: Type.String(),
27
+ }),
28
+ body: Type.String(),
29
+ sentAt: Type.String(),
30
+ receivedAt: Type.String(),
31
+ requestResponse: Type.Optional(Type.Boolean()),
32
+ sourceToolCallId: Type.Optional(Type.String()),
33
+ relation: Type.Optional(Type.String()),
34
+ });
35
+
36
+ export type SendMessageParams = Static<typeof SEND_MESSAGE_PARAMS>;
37
+ export type ReceivedMessageEntry = Static<typeof RECEIVED_MESSAGE_ENTRY_SCHEMA>;
@@ -0,0 +1,120 @@
1
+ import { Box, type Component, getKeybindings, type Keybinding, Text } from "@earendil-works/pi-tui";
2
+ import type { ReceivedMessageEntry } from "./message-contracts.ts";
3
+
4
+ const COLLAPSED_MESSAGE_LINES = 10;
5
+
6
+ interface MessageViewTheme {
7
+ bold(text: string): string;
8
+ fg(token: string, text: string): string;
9
+ bg(token: string, text: string): string;
10
+ }
11
+
12
+ interface MessageViewOptions {
13
+ expanded: boolean;
14
+ status: "sending" | "received";
15
+ targetSessionId?: string | undefined;
16
+ sourceSessionId?: string | undefined;
17
+ message: string;
18
+ requestResponse?: boolean | undefined;
19
+ relation?: string | undefined;
20
+ }
21
+
22
+ export function renderSessionMessageView(
23
+ options: MessageViewOptions,
24
+ theme: MessageViewTheme,
25
+ ): string {
26
+ const header = formatHeader(options, theme);
27
+ const body = formatMessageBody(options.message, options.expanded, theme);
28
+ return body ? `${header}\n\n${body}` : header;
29
+ }
30
+
31
+ export function createReceivedSessionMessageComponent(
32
+ received: ReceivedMessageEntry,
33
+ expanded: boolean,
34
+ theme: MessageViewTheme,
35
+ ): Component {
36
+ const box = new Box(1, 1, (text: string) => theme.bg("toolSuccessBg", text));
37
+ box.addChild(
38
+ new Text(
39
+ renderSessionMessageView(
40
+ {
41
+ expanded,
42
+ status: "received",
43
+ sourceSessionId: received.source.sessionId,
44
+ message: received.body,
45
+ requestResponse: received.requestResponse,
46
+ relation: received.relation,
47
+ },
48
+ theme,
49
+ ),
50
+ 0,
51
+ 0,
52
+ ),
53
+ );
54
+ return box;
55
+ }
56
+
57
+ export function formatSendMessageCall(
58
+ args: { session?: string; message?: string; requestResponse?: boolean } | undefined,
59
+ expanded: boolean,
60
+ status: "sending",
61
+ theme: MessageViewTheme,
62
+ relation?: string | undefined,
63
+ ): string {
64
+ return renderSessionMessageView(
65
+ {
66
+ expanded,
67
+ status,
68
+ targetSessionId: args?.session,
69
+ message: args?.message ?? "",
70
+ requestResponse: args?.requestResponse,
71
+ relation,
72
+ },
73
+ theme,
74
+ );
75
+ }
76
+
77
+ function formatHeader(options: MessageViewOptions, theme: MessageViewTheme): string {
78
+ const toolName = options.status === "received" ? "incoming_message" : "session_send_message";
79
+ const name = theme.fg("toolTitle", theme.bold(toolName));
80
+ const metadata = [
81
+ options.relation,
82
+ options.requestResponse ? "response requested" : undefined,
83
+ ].filter((item): item is string => Boolean(item));
84
+ const metadataHint = metadata.length > 0 ? theme.fg("muted", ` (${metadata.join(", ")})`) : "";
85
+ switch (options.status) {
86
+ case "received":
87
+ return `${name} ${theme.fg("muted", `from ${options.sourceSessionId ?? "[source unknown]"}`)}${metadataHint}`;
88
+ case "sending":
89
+ return `${name} ${theme.fg("muted", `to ${options.targetSessionId ?? "[target pending]"}`)}${metadataHint}`;
90
+ }
91
+ }
92
+
93
+ function formatKeyHint(
94
+ keybinding: Keybinding,
95
+ description: string,
96
+ theme: MessageViewTheme,
97
+ ): string {
98
+ const keyText = getKeybindings().getKeys(keybinding).join("/");
99
+ return `${theme.fg("dim", keyText)}${theme.fg("muted", ` ${description}`)}`;
100
+ }
101
+
102
+ function formatMessageBody(message: string, expanded: boolean, theme: MessageViewTheme): string {
103
+ if (!message) {
104
+ return "";
105
+ }
106
+
107
+ const lines = message.replaceAll("\t", " ").split("\n");
108
+ const maxLines = expanded ? lines.length : COLLAPSED_MESSAGE_LINES;
109
+ const visibleLines = lines.slice(0, maxLines);
110
+ const remaining = lines.length - visibleLines.length;
111
+ const body = visibleLines.map((line) => theme.fg("toolOutput", line)).join("\n");
112
+ if (remaining <= 0) {
113
+ return body;
114
+ }
115
+
116
+ return `${body}${theme.fg(
117
+ "muted",
118
+ `\n... (${remaining} more lines, ${lines.length} total,`,
119
+ )} ${formatKeyHint("app.tools.expand", "to expand", theme)}${theme.fg("muted", ")")}`;
120
+ }
@@ -0,0 +1,16 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { safeParseTypeBoxValue } from "../../shared/typebox.ts";
3
+ import { SESSION_MESSAGE_CUSTOM_TYPE } from "./incoming-runtime.ts";
4
+ import { RECEIVED_MESSAGE_ENTRY_SCHEMA } from "./message-contracts.ts";
5
+ import { createReceivedSessionMessageComponent } from "./message-view.ts";
6
+
7
+ export function registerSessionMessagingRenderer(pi: ExtensionAPI): void {
8
+ pi.registerMessageRenderer(SESSION_MESSAGE_CUSTOM_TYPE, (message, options, theme) => {
9
+ const details = safeParseTypeBoxValue(RECEIVED_MESSAGE_ENTRY_SCHEMA, message.details);
10
+ if (!details) {
11
+ return undefined;
12
+ }
13
+
14
+ return createReceivedSessionMessageComponent(details, options.expanded, theme);
15
+ });
16
+ }
@@ -0,0 +1,315 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import type { SessionLineageRelation } from "../../shared/session-index/index.ts";
4
+ import {
5
+ getIndexStatus,
6
+ getLineageRelationMap,
7
+ INDEX_SCHEMA_VERSION,
8
+ openIndexDatabase,
9
+ } from "../../shared/session-index/index.ts";
10
+ import { spawnSessionMessagingBrokerIfNeeded } from "../broker/spawn.ts";
11
+ import type { SessionMessagePayload, SessionMessagingSessionInfo } from "../shared/protocol.ts";
12
+ import {
13
+ INCOMING_SESSION_MESSAGE_EVENT,
14
+ type SessionMessageSendResult,
15
+ SessionMessagingClient,
16
+ } from "./client.ts";
17
+ import type { IncomingSessionMessageRuntime } from "./incoming-runtime.ts";
18
+
19
+ export const MESSAGE_SENT_CUSTOM_TYPE = "pi-sessions.message_sent";
20
+
21
+ const RECONNECT_DELAYS_MS = [250, 500, 1_000, 2_000, 5_000, 10_000, 30_000] as const;
22
+
23
+ export interface LiveSessionRow extends SessionMessagingSessionInfo {
24
+ relation?: SessionLineageRelation | undefined;
25
+ }
26
+
27
+ export interface SendMessageRequest {
28
+ target: string;
29
+ body: string;
30
+ requestResponse?: boolean | undefined;
31
+ sourceToolCallId?: string | undefined;
32
+ }
33
+
34
+ export class SessionMessagingService {
35
+ private readonly indexPath: string;
36
+ private readonly incomingRuntime: IncomingSessionMessageRuntime;
37
+ private readonly appendEntry: (customType: string, data: unknown) => void;
38
+ private relationBySessionId = new Map<string, SessionLineageRelation | undefined>();
39
+ private readonly connection = new BrokerConnection((requestId, message) =>
40
+ this.handleIncoming(requestId, message),
41
+ );
42
+
43
+ constructor(
44
+ indexPath: string,
45
+ incomingRuntime: IncomingSessionMessageRuntime,
46
+ appendEntry: (customType: string, data: unknown) => void,
47
+ ) {
48
+ this.indexPath = indexPath;
49
+ this.incomingRuntime = incomingRuntime;
50
+ this.appendEntry = appendEntry;
51
+ }
52
+
53
+ async start(ctx: ExtensionContext): Promise<void> {
54
+ this.refreshCachedRelations(ctx.sessionManager.getSessionId());
55
+ await this.connection.start(buildSessionInfo(ctx));
56
+ }
57
+
58
+ stop(): void {
59
+ this.relationBySessionId.clear();
60
+ this.connection.stop();
61
+ }
62
+
63
+ async listLiveSessions(ctx: ExtensionContext): Promise<LiveSessionRow[]> {
64
+ const sessions = await this.connection.listSessions();
65
+ this.refreshCachedRelations(ctx.sessionManager.getSessionId());
66
+ const currentSessionId = ctx.sessionManager.getSessionId();
67
+ return sessions
68
+ .filter((session) => session.sessionId !== currentSessionId)
69
+ .map(
70
+ (session): LiveSessionRow => ({
71
+ ...session,
72
+ relation: this.getCachedRelationTo(session.sessionId),
73
+ }),
74
+ );
75
+ }
76
+
77
+ async sendMessage(request: SendMessageRequest): Promise<SessionMessageSendResult> {
78
+ const relation = this.getCachedRelationTo(request.target, true);
79
+ const sentAt = new Date().toISOString();
80
+ const messageId = randomUUID();
81
+ const result = await this.connection.sendMessage({
82
+ messageId,
83
+ target: request.target,
84
+ body: request.body,
85
+ sentAt,
86
+ requestResponse: request.requestResponse,
87
+ sourceToolCallId: request.sourceToolCallId,
88
+ });
89
+
90
+ if (result.delivered) {
91
+ this.appendEntry(MESSAGE_SENT_CUSTOM_TYPE, {
92
+ messageId: result.messageId,
93
+ target: request.target,
94
+ body: request.body,
95
+ requestResponse: request.requestResponse,
96
+ sourceToolCallId: request.sourceToolCallId,
97
+ sentAt,
98
+ ...(relation === undefined ? {} : { relation }),
99
+ });
100
+ }
101
+
102
+ return result;
103
+ }
104
+
105
+ private handleIncoming(requestId: string, message: SessionMessagePayload): void {
106
+ const relation = this.getCachedRelationTo(message.source.sessionId, true);
107
+ const result = this.incomingRuntime.deliver(message, relation);
108
+ this.connection.acknowledgeIncoming(requestId, message.messageId, result);
109
+ }
110
+
111
+ getCachedRelationTo(
112
+ sessionId: string | undefined,
113
+ refresh = false,
114
+ ): SessionLineageRelation | undefined {
115
+ if (!sessionId) {
116
+ return undefined;
117
+ }
118
+
119
+ if (this.relationBySessionId.has(sessionId)) {
120
+ return this.relationBySessionId.get(sessionId);
121
+ }
122
+
123
+ if (!refresh) {
124
+ return undefined;
125
+ }
126
+
127
+ const identity = this.connection.currentIdentity;
128
+ if (!identity) {
129
+ throw new Error("Session messaging is not active.");
130
+ }
131
+
132
+ this.refreshCachedRelations(identity.sessionId);
133
+ if (!this.relationBySessionId.has(sessionId)) {
134
+ this.relationBySessionId.set(sessionId, undefined);
135
+ }
136
+
137
+ return this.relationBySessionId.get(sessionId);
138
+ }
139
+
140
+ private refreshCachedRelations(currentSessionId: string): void {
141
+ const previousRelations = this.relationBySessionId;
142
+ const nextRelations = getRelationMapForSession(this.indexPath, currentSessionId);
143
+
144
+ for (const [sessionId, relation] of previousRelations) {
145
+ if (relation === undefined && !nextRelations.has(sessionId)) {
146
+ nextRelations.set(sessionId, undefined);
147
+ }
148
+ }
149
+
150
+ this.relationBySessionId = nextRelations;
151
+ }
152
+ }
153
+
154
+ class BrokerConnection {
155
+ private client: SessionMessagingClient | undefined;
156
+ private identity: SessionMessagingSessionInfo | undefined;
157
+ private active = false;
158
+ private reconnectTimer: NodeJS.Timeout | undefined;
159
+ private reconnectDelayIndex = 0;
160
+ private connectPromise: Promise<void> | undefined;
161
+
162
+ constructor(
163
+ private readonly onIncoming: (requestId: string, message: SessionMessagePayload) => void,
164
+ ) {}
165
+
166
+ get currentIdentity(): SessionMessagingSessionInfo | undefined {
167
+ return this.identity;
168
+ }
169
+
170
+ async start(identity: SessionMessagingSessionInfo): Promise<void> {
171
+ this.active = true;
172
+ this.identity = identity;
173
+ await this.ensureConnectedNow();
174
+ }
175
+
176
+ stop(): void {
177
+ this.active = false;
178
+ this.identity = undefined;
179
+ this.clearReconnectTimer();
180
+ this.client?.disconnect();
181
+ this.client = undefined;
182
+ this.connectPromise = undefined;
183
+ }
184
+
185
+ async listSessions(): Promise<SessionMessagingSessionInfo[]> {
186
+ await this.ensureConnectedNow();
187
+ return this.requireClient().listSessions();
188
+ }
189
+
190
+ async sendMessage(options: {
191
+ messageId: string;
192
+ target: string;
193
+ body: string;
194
+ sentAt: string;
195
+ requestResponse?: boolean | undefined;
196
+ sourceToolCallId?: string | undefined;
197
+ }): Promise<SessionMessageSendResult> {
198
+ await this.ensureConnectedNow();
199
+ return this.requireClient().sendMessage(options);
200
+ }
201
+
202
+ acknowledgeIncoming(
203
+ requestId: string,
204
+ messageId: string,
205
+ result: { delivered: true } | { delivered: false; error: string },
206
+ ): void {
207
+ try {
208
+ this.requireClient().acknowledgeIncoming(requestId, messageId, result);
209
+ } catch {}
210
+ }
211
+
212
+ private async ensureConnectedNow(): Promise<void> {
213
+ if (this.client?.isConnected) {
214
+ return;
215
+ }
216
+ if (!this.active || !this.identity) {
217
+ throw new Error("Session messaging is not active.");
218
+ }
219
+ this.clearReconnectTimer();
220
+
221
+ if (!this.connectPromise) {
222
+ this.connectPromise = this.connect().finally(() => {
223
+ this.connectPromise = undefined;
224
+ });
225
+ }
226
+
227
+ try {
228
+ await this.connectPromise;
229
+ } catch (error) {
230
+ this.scheduleReconnect();
231
+ throw error;
232
+ }
233
+ }
234
+
235
+ private async connect(): Promise<void> {
236
+ const identity = this.identity;
237
+ if (!identity) {
238
+ throw new Error("Session messaging is not active.");
239
+ }
240
+
241
+ this.client?.disconnect();
242
+ this.client = undefined;
243
+
244
+ await spawnSessionMessagingBrokerIfNeeded();
245
+ const client = new SessionMessagingClient();
246
+ client.on(INCOMING_SESSION_MESSAGE_EVENT, this.onIncoming);
247
+ client.on("disconnect", () => {
248
+ if (this.client === client) {
249
+ this.client = undefined;
250
+ this.scheduleReconnect();
251
+ }
252
+ });
253
+
254
+ await client.connect(identity);
255
+ this.client = client;
256
+ this.reconnectDelayIndex = 0;
257
+ }
258
+
259
+ private scheduleReconnect(): void {
260
+ if (!this.active || this.reconnectTimer) {
261
+ return;
262
+ }
263
+
264
+ const delay = RECONNECT_DELAYS_MS[this.reconnectDelayIndex] ?? 30_000;
265
+ this.reconnectDelayIndex = Math.min(
266
+ this.reconnectDelayIndex + 1,
267
+ RECONNECT_DELAYS_MS.length - 1,
268
+ );
269
+ this.reconnectTimer = setTimeout(() => {
270
+ this.reconnectTimer = undefined;
271
+ this.ensureConnectedNow().catch(() => {});
272
+ }, delay);
273
+ }
274
+
275
+ private clearReconnectTimer(): void {
276
+ if (!this.reconnectTimer) {
277
+ return;
278
+ }
279
+ clearTimeout(this.reconnectTimer);
280
+ this.reconnectTimer = undefined;
281
+ }
282
+
283
+ private requireClient(): SessionMessagingClient {
284
+ if (!this.client?.isConnected) {
285
+ throw new Error("Session messaging is not connected.");
286
+ }
287
+ return this.client;
288
+ }
289
+ }
290
+
291
+ function buildSessionInfo(ctx: ExtensionContext): SessionMessagingSessionInfo {
292
+ const sessionName = ctx.sessionManager.getSessionName()?.trim();
293
+ return {
294
+ sessionId: ctx.sessionManager.getSessionId(),
295
+ ...(sessionName ? { sessionName } : {}),
296
+ cwd: ctx.cwd,
297
+ };
298
+ }
299
+
300
+ function getRelationMapForSession(
301
+ indexPath: string,
302
+ sessionId: string,
303
+ ): Map<string, SessionLineageRelation | undefined> {
304
+ const status = getIndexStatus(indexPath);
305
+ if (!status.exists || status.schemaVersion !== INDEX_SCHEMA_VERSION) {
306
+ return new Map();
307
+ }
308
+
309
+ const db = openIndexDatabase(status.dbPath, { create: false });
310
+ try {
311
+ return new Map(getLineageRelationMap(db, sessionId));
312
+ } finally {
313
+ db.close();
314
+ }
315
+ }