pi-sessions 0.6.0 → 0.7.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 (43) hide show
  1. package/README.md +22 -11
  2. package/extensions/session-ask/agent.ts +400 -0
  3. package/extensions/session-ask/navigate.ts +880 -0
  4. package/extensions/session-ask.ts +79 -131
  5. package/extensions/session-auto-title/model.ts +3 -11
  6. package/extensions/session-handoff/picker.ts +50 -4
  7. package/extensions/session-handoff/query.ts +74 -56
  8. package/extensions/session-handoff/refs.ts +12 -18
  9. package/extensions/session-handoff.ts +36 -27
  10. package/extensions/session-messaging/broker/process.ts +23 -26
  11. package/extensions/session-messaging/broker/spawn.ts +7 -2
  12. package/extensions/session-messaging/pi/incoming-runtime.ts +2 -18
  13. package/extensions/session-messaging/pi/message-contracts.ts +9 -10
  14. package/extensions/session-messaging/pi/message-view.ts +12 -1
  15. package/extensions/session-messaging/pi/service.ts +117 -62
  16. package/extensions/session-messaging/pi/tools.ts +4 -56
  17. package/extensions/session-search/extract.ts +86 -436
  18. package/extensions/session-search/hooks.ts +17 -25
  19. package/extensions/session-search/normalize.ts +1 -1
  20. package/extensions/session-search/reindex.ts +1 -0
  21. package/extensions/session-search.ts +157 -128
  22. package/extensions/shared/model.ts +18 -0
  23. package/extensions/shared/session-broker/active.ts +26 -0
  24. package/extensions/{session-messaging/pi → shared/session-broker}/client.ts +16 -19
  25. package/extensions/{session-messaging/shared → shared/session-broker}/framing.ts +1 -1
  26. package/extensions/{session-messaging/shared → shared/session-broker}/protocol.ts +5 -12
  27. package/extensions/shared/session-index/access.ts +128 -0
  28. package/extensions/shared/session-index/common.ts +43 -48
  29. package/extensions/shared/session-index/index.ts +1 -0
  30. package/extensions/shared/session-index/lineage.ts +10 -0
  31. package/extensions/shared/session-index/query/ast.ts +40 -0
  32. package/extensions/shared/session-index/query/compiler.ts +146 -0
  33. package/extensions/shared/session-index/query/lexer.ts +140 -0
  34. package/extensions/shared/session-index/query/parser.ts +178 -0
  35. package/extensions/shared/session-index/schema.ts +22 -6
  36. package/extensions/shared/session-index/scoring.ts +80 -0
  37. package/extensions/shared/session-index/search.ts +552 -278
  38. package/extensions/shared/session-index/sqlite.ts +16 -9
  39. package/extensions/shared/session-index/store.ts +12 -21
  40. package/extensions/shared/settings.ts +61 -4
  41. package/extensions/shared/text.ts +50 -0
  42. package/package.json +1 -1
  43. /package/extensions/{session-messaging/shared → shared/session-broker}/socket-path.ts +0 -0
@@ -1,29 +1,32 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import type { SessionLineageRelation } from "../../shared/session-index/index.ts";
4
3
  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";
4
+ type ActiveSessionBrokerConnection,
5
+ setActiveSessionBrokerConnection,
6
+ } from "../../shared/session-broker/active.ts";
12
7
  import {
13
8
  INCOMING_SESSION_MESSAGE_EVENT,
14
9
  type SessionMessageSendResult,
15
10
  SessionMessagingClient,
16
- } from "./client.ts";
11
+ } from "../../shared/session-broker/client.ts";
12
+ import type { SessionMessagePayload } from "../../shared/session-broker/protocol.ts";
13
+ import type {
14
+ SessionLineageRelation,
15
+ SessionLineageRow,
16
+ } from "../../shared/session-index/index.ts";
17
+ import {
18
+ getLineageRelationMap,
19
+ getSessionById,
20
+ withSessionIndex,
21
+ } from "../../shared/session-index/index.ts";
22
+ import { spawnSessionMessagingBrokerIfNeeded } from "../broker/spawn.ts";
17
23
  import type { IncomingSessionMessageRuntime } from "./incoming-runtime.ts";
24
+ import type { ReceivedMessageEndpoint, ReceivedMessageEntry } from "./message-contracts.ts";
18
25
 
19
26
  export const MESSAGE_SENT_CUSTOM_TYPE = "pi-sessions.message_sent";
20
27
 
21
28
  const RECONNECT_DELAYS_MS = [250, 500, 1_000, 2_000, 5_000, 10_000, 30_000] as const;
22
29
 
23
- export interface LiveSessionRow extends SessionMessagingSessionInfo {
24
- relation?: SessionLineageRelation | undefined;
25
- }
26
-
27
30
  export interface SendMessageRequest {
28
31
  target: string;
29
32
  body: string;
@@ -31,6 +34,12 @@ export interface SendMessageRequest {
31
34
  sourceToolCallId?: string | undefined;
32
35
  }
33
36
 
37
+ interface IncomingMessageIndexContext {
38
+ source: ReceivedMessageEndpoint;
39
+ target: ReceivedMessageEndpoint;
40
+ relation?: SessionLineageRelation | undefined;
41
+ }
42
+
34
43
  export class SessionMessagingService {
35
44
  private readonly indexPath: string;
36
45
  private readonly incomingRuntime: IncomingSessionMessageRuntime;
@@ -51,29 +60,18 @@ export class SessionMessagingService {
51
60
  }
52
61
 
53
62
  async start(ctx: ExtensionContext): Promise<void> {
54
- this.refreshCachedRelations(ctx.sessionManager.getSessionId());
55
- await this.connection.start(buildSessionInfo(ctx));
63
+ const sessionId = ctx.sessionManager.getSessionId();
64
+ this.refreshCachedRelations(sessionId);
65
+ setActiveSessionBrokerConnection(this.connection);
66
+ await this.connection.start(sessionId);
56
67
  }
57
68
 
58
69
  stop(): void {
70
+ setActiveSessionBrokerConnection(undefined);
59
71
  this.relationBySessionId.clear();
60
72
  this.connection.stop();
61
73
  }
62
74
 
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
75
  async sendMessage(request: SendMessageRequest): Promise<SessionMessageSendResult> {
78
76
  const relation = this.getCachedRelationTo(request.target, true);
79
77
  const sentAt = new Date().toISOString();
@@ -103,8 +101,8 @@ export class SessionMessagingService {
103
101
  }
104
102
 
105
103
  private handleIncoming(requestId: string, message: SessionMessagePayload): void {
106
- const relation = this.getCachedRelationTo(message.source.sessionId, true);
107
- const result = this.incomingRuntime.deliver(message, relation);
104
+ const received = this.buildReceivedMessageEntry(message);
105
+ const result = this.incomingRuntime.deliver(received);
108
106
  this.connection.acknowledgeIncoming(requestId, message.messageId, result);
109
107
  }
110
108
 
@@ -124,12 +122,12 @@ export class SessionMessagingService {
124
122
  return undefined;
125
123
  }
126
124
 
127
- const identity = this.connection.currentIdentity;
125
+ const identity = this.connection.currentSessionId;
128
126
  if (!identity) {
129
127
  throw new Error("Session messaging is not active.");
130
128
  }
131
129
 
132
- this.refreshCachedRelations(identity.sessionId);
130
+ this.refreshCachedRelations(identity);
133
131
  if (!this.relationBySessionId.has(sessionId)) {
134
132
  this.relationBySessionId.set(sessionId, undefined);
135
133
  }
@@ -137,9 +135,67 @@ export class SessionMessagingService {
137
135
  return this.relationBySessionId.get(sessionId);
138
136
  }
139
137
 
138
+ private buildReceivedMessageEntry(message: SessionMessagePayload): ReceivedMessageEntry {
139
+ const context = this.getIncomingMessageIndexContext(message.source, message.target);
140
+ return {
141
+ messageId: message.messageId,
142
+ source: context.source,
143
+ target: context.target,
144
+ body: message.body,
145
+ sentAt: message.sentAt,
146
+ receivedAt: new Date().toISOString(),
147
+ ...(message.requestResponse === undefined
148
+ ? {}
149
+ : { requestResponse: message.requestResponse }),
150
+ ...(message.sourceToolCallId === undefined
151
+ ? {}
152
+ : { sourceToolCallId: message.sourceToolCallId }),
153
+ ...(context.relation === undefined ? {} : { relation: context.relation }),
154
+ };
155
+ }
156
+
157
+ private getIncomingMessageIndexContext(
158
+ sourceSessionId: string,
159
+ targetSessionId: string,
160
+ ): IncomingMessageIndexContext {
161
+ const fallback = {
162
+ source: buildReceivedMessageEndpoint(sourceSessionId),
163
+ target: buildReceivedMessageEndpoint(targetSessionId),
164
+ };
165
+ return (
166
+ withSessionIndex(this.indexPath, { mode: "read", required: false }, ({ db }) => {
167
+ const source = getSessionById(db, sourceSessionId);
168
+ const target = getSessionById(db, targetSessionId);
169
+ const currentSessionId = this.connection.currentSessionId;
170
+ const relationBySessionId = currentSessionId
171
+ ? new Map(getLineageRelationMap(db, currentSessionId))
172
+ : new Map<string, SessionLineageRelation>();
173
+ const relation = relationBySessionId.get(sourceSessionId);
174
+
175
+ if (currentSessionId) {
176
+ this.replaceCachedRelations(relationBySessionId);
177
+ if (!this.relationBySessionId.has(sourceSessionId)) {
178
+ this.relationBySessionId.set(sourceSessionId, undefined);
179
+ }
180
+ }
181
+
182
+ return {
183
+ source: buildReceivedMessageEndpoint(sourceSessionId, source),
184
+ target: buildReceivedMessageEndpoint(targetSessionId, target),
185
+ ...(relation === undefined ? {} : { relation }),
186
+ };
187
+ }) ?? fallback
188
+ );
189
+ }
190
+
140
191
  private refreshCachedRelations(currentSessionId: string): void {
192
+ this.replaceCachedRelations(getRelationMapForSession(this.indexPath, currentSessionId));
193
+ }
194
+
195
+ private replaceCachedRelations(
196
+ nextRelations: Map<string, SessionLineageRelation | undefined>,
197
+ ): void {
141
198
  const previousRelations = this.relationBySessionId;
142
- const nextRelations = getRelationMapForSession(this.indexPath, currentSessionId);
143
199
 
144
200
  for (const [sessionId, relation] of previousRelations) {
145
201
  if (relation === undefined && !nextRelations.has(sessionId)) {
@@ -151,9 +207,9 @@ export class SessionMessagingService {
151
207
  }
152
208
  }
153
209
 
154
- class BrokerConnection {
210
+ class BrokerConnection implements ActiveSessionBrokerConnection {
155
211
  private client: SessionMessagingClient | undefined;
156
- private identity: SessionMessagingSessionInfo | undefined;
212
+ private sessionId: string | undefined;
157
213
  private active = false;
158
214
  private reconnectTimer: NodeJS.Timeout | undefined;
159
215
  private reconnectDelayIndex = 0;
@@ -163,28 +219,28 @@ class BrokerConnection {
163
219
  private readonly onIncoming: (requestId: string, message: SessionMessagePayload) => void,
164
220
  ) {}
165
221
 
166
- get currentIdentity(): SessionMessagingSessionInfo | undefined {
167
- return this.identity;
222
+ get currentSessionId(): string | undefined {
223
+ return this.sessionId;
168
224
  }
169
225
 
170
- async start(identity: SessionMessagingSessionInfo): Promise<void> {
226
+ async start(sessionId: string): Promise<void> {
171
227
  this.active = true;
172
- this.identity = identity;
228
+ this.sessionId = sessionId;
173
229
  await this.ensureConnectedNow();
174
230
  }
175
231
 
176
232
  stop(): void {
177
233
  this.active = false;
178
- this.identity = undefined;
234
+ this.sessionId = undefined;
179
235
  this.clearReconnectTimer();
180
236
  this.client?.disconnect();
181
237
  this.client = undefined;
182
238
  this.connectPromise = undefined;
183
239
  }
184
240
 
185
- async listSessions(): Promise<SessionMessagingSessionInfo[]> {
241
+ async listSessionIds(): Promise<string[]> {
186
242
  await this.ensureConnectedNow();
187
- return this.requireClient().listSessions();
243
+ return this.requireClient().listSessionIds();
188
244
  }
189
245
 
190
246
  async sendMessage(options: {
@@ -213,7 +269,7 @@ class BrokerConnection {
213
269
  if (this.client?.isConnected) {
214
270
  return;
215
271
  }
216
- if (!this.active || !this.identity) {
272
+ if (!this.active || !this.sessionId) {
217
273
  throw new Error("Session messaging is not active.");
218
274
  }
219
275
  this.clearReconnectTimer();
@@ -233,8 +289,8 @@ class BrokerConnection {
233
289
  }
234
290
 
235
291
  private async connect(): Promise<void> {
236
- const identity = this.identity;
237
- if (!identity) {
292
+ const sessionId = this.sessionId;
293
+ if (!sessionId) {
238
294
  throw new Error("Session messaging is not active.");
239
295
  }
240
296
 
@@ -251,7 +307,7 @@ class BrokerConnection {
251
307
  }
252
308
  });
253
309
 
254
- await client.connect(identity);
310
+ await client.connect(sessionId);
255
311
  this.client = client;
256
312
  this.reconnectDelayIndex = 0;
257
313
  }
@@ -288,12 +344,15 @@ class BrokerConnection {
288
344
  }
289
345
  }
290
346
 
291
- function buildSessionInfo(ctx: ExtensionContext): SessionMessagingSessionInfo {
292
- const sessionName = ctx.sessionManager.getSessionName()?.trim();
347
+ function buildReceivedMessageEndpoint(
348
+ sessionId: string,
349
+ session?: SessionLineageRow | undefined,
350
+ ): ReceivedMessageEndpoint {
351
+ const sessionName = session?.sessionName.trim();
293
352
  return {
294
- sessionId: ctx.sessionManager.getSessionId(),
353
+ sessionId,
295
354
  ...(sessionName ? { sessionName } : {}),
296
- cwd: ctx.cwd,
355
+ ...(session?.cwd ? { cwd: session.cwd } : {}),
297
356
  };
298
357
  }
299
358
 
@@ -301,15 +360,11 @@ function getRelationMapForSession(
301
360
  indexPath: string,
302
361
  sessionId: string,
303
362
  ): 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
- }
363
+ return (
364
+ withSessionIndex(
365
+ indexPath,
366
+ { mode: "read", required: false },
367
+ ({ db }) => new Map(getLineageRelationMap(db, sessionId)),
368
+ ) ?? new Map()
369
+ );
315
370
  }
@@ -1,52 +1,21 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Text } from "@earendil-works/pi-tui";
3
- import { Type } from "typebox";
4
3
  import { SEND_MESSAGE_PARAMS, type SendMessageParams } from "./message-contracts.ts";
5
4
  import { formatSendMessageCall } from "./message-view.ts";
6
- import type { LiveSessionRow, SessionMessagingService } from "./service.ts";
7
-
8
- interface ListLiveDetails {
9
- error?: boolean | undefined;
10
- sessions: LiveSessionRow[];
11
- }
5
+ import type { SessionMessagingService } from "./service.ts";
12
6
 
13
7
  export function registerSessionMessagingTools(
14
8
  pi: ExtensionAPI,
15
9
  service: SessionMessagingService,
16
10
  ): void {
17
- pi.registerTool({
18
- name: "session_list_live",
19
- label: "List Live Pi Sessions",
20
- description: "List other Pi sessions that are currently active.",
21
- promptSnippet: "List other Pi sessions that are currently active.",
22
- parameters: Type.Object({}),
23
- renderResult(result, _options, theme, context) {
24
- if (context.isError) {
25
- return new Text(`\n${theme.fg("error", getFirstText(result))}`, 0, 0);
26
- }
27
-
28
- const details = result.details as ListLiveDetails | undefined;
29
- return new Text(`\n${formatLiveSessionsForDisplay(details?.sessions ?? [], theme)}`, 0, 0);
30
- },
31
- async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
32
- try {
33
- const sessions = await service.listLiveSessions(ctx);
34
- return {
35
- content: [{ type: "text" as const, text: formatLiveSessions(sessions) }],
36
- details: { sessions } satisfies ListLiveDetails,
37
- };
38
- } catch (error) {
39
- throw new Error(formatError(error));
40
- }
41
- },
42
- });
43
-
44
11
  pi.registerTool({
45
12
  name: "session_send_message",
46
13
  label: "Send Message to Session",
47
14
  description: "Send a message to another live pi session",
48
15
  promptSnippet: "Send a message to another live pi session",
49
- promptGuidelines: ["Use session_list_live to discover targetable sessions."],
16
+ promptGuidelines: [
17
+ "Use session_search with live: true to discover targetable sessions. Usually it is best to exclude all other filters and simply get a list of all live sessions",
18
+ ],
50
19
  parameters: SEND_MESSAGE_PARAMS,
51
20
  renderCall(args, theme, context) {
52
21
  const component = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
@@ -114,27 +83,6 @@ export function registerSessionMessagingTools(
114
83
  });
115
84
  }
116
85
 
117
- function formatLiveSessions(sessions: LiveSessionRow[]): string {
118
- return JSON.stringify({ sessions }, null, 2);
119
- }
120
-
121
- function formatLiveSessionsForDisplay(
122
- sessions: LiveSessionRow[],
123
- theme: { fg(token: string, text: string): string },
124
- ): string {
125
- if (sessions.length === 0) {
126
- return "No other live sessions found.";
127
- }
128
-
129
- return sessions
130
- .map((session) => {
131
- const title = session.sessionName?.trim() || "[untitled]";
132
- const relation = session.relation ? ` (${session.relation})` : "";
133
- return `${session.sessionId}${relation} - ${title}\n${theme.fg("dim", session.cwd)}`;
134
- })
135
- .join("\n\n");
136
- }
137
-
138
86
  function getFirstText(result: { content: Array<{ type: string; text?: string }> }): string {
139
87
  return result.content.find((item) => item.type === "text")?.text ?? "";
140
88
  }