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.
- package/README.md +22 -11
- package/extensions/session-ask/agent.ts +400 -0
- package/extensions/session-ask/navigate.ts +880 -0
- package/extensions/session-ask.ts +79 -131
- package/extensions/session-auto-title/model.ts +3 -11
- package/extensions/session-handoff/picker.ts +50 -4
- package/extensions/session-handoff/query.ts +74 -56
- package/extensions/session-handoff/refs.ts +12 -18
- package/extensions/session-handoff.ts +36 -27
- package/extensions/session-messaging/broker/process.ts +23 -26
- package/extensions/session-messaging/broker/spawn.ts +7 -2
- package/extensions/session-messaging/pi/incoming-runtime.ts +2 -18
- package/extensions/session-messaging/pi/message-contracts.ts +9 -10
- package/extensions/session-messaging/pi/message-view.ts +12 -1
- package/extensions/session-messaging/pi/service.ts +117 -62
- package/extensions/session-messaging/pi/tools.ts +4 -56
- package/extensions/session-search/extract.ts +86 -436
- package/extensions/session-search/hooks.ts +17 -25
- package/extensions/session-search/normalize.ts +1 -1
- package/extensions/session-search/reindex.ts +1 -0
- package/extensions/session-search.ts +157 -128
- package/extensions/shared/model.ts +18 -0
- package/extensions/shared/session-broker/active.ts +26 -0
- package/extensions/{session-messaging/pi → shared/session-broker}/client.ts +16 -19
- package/extensions/{session-messaging/shared → shared/session-broker}/framing.ts +1 -1
- package/extensions/{session-messaging/shared → shared/session-broker}/protocol.ts +5 -12
- package/extensions/shared/session-index/access.ts +128 -0
- package/extensions/shared/session-index/common.ts +43 -48
- package/extensions/shared/session-index/index.ts +1 -0
- package/extensions/shared/session-index/lineage.ts +10 -0
- package/extensions/shared/session-index/query/ast.ts +40 -0
- package/extensions/shared/session-index/query/compiler.ts +146 -0
- package/extensions/shared/session-index/query/lexer.ts +140 -0
- package/extensions/shared/session-index/query/parser.ts +178 -0
- package/extensions/shared/session-index/schema.ts +22 -6
- package/extensions/shared/session-index/scoring.ts +80 -0
- package/extensions/shared/session-index/search.ts +552 -278
- package/extensions/shared/session-index/sqlite.ts +16 -9
- package/extensions/shared/session-index/store.ts +12 -21
- package/extensions/shared/settings.ts +61 -4
- package/extensions/shared/text.ts +50 -0
- package/package.json +1 -1
- /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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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 "
|
|
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
|
-
|
|
55
|
-
|
|
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
|
|
107
|
-
const result = this.incomingRuntime.deliver(
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
167
|
-
return this.
|
|
222
|
+
get currentSessionId(): string | undefined {
|
|
223
|
+
return this.sessionId;
|
|
168
224
|
}
|
|
169
225
|
|
|
170
|
-
async start(
|
|
226
|
+
async start(sessionId: string): Promise<void> {
|
|
171
227
|
this.active = true;
|
|
172
|
-
this.
|
|
228
|
+
this.sessionId = sessionId;
|
|
173
229
|
await this.ensureConnectedNow();
|
|
174
230
|
}
|
|
175
231
|
|
|
176
232
|
stop(): void {
|
|
177
233
|
this.active = false;
|
|
178
|
-
this.
|
|
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
|
|
241
|
+
async listSessionIds(): Promise<string[]> {
|
|
186
242
|
await this.ensureConnectedNow();
|
|
187
|
-
return this.requireClient().
|
|
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.
|
|
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
|
|
237
|
-
if (!
|
|
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(
|
|
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
|
|
292
|
-
|
|
347
|
+
function buildReceivedMessageEndpoint(
|
|
348
|
+
sessionId: string,
|
|
349
|
+
session?: SessionLineageRow | undefined,
|
|
350
|
+
): ReceivedMessageEndpoint {
|
|
351
|
+
const sessionName = session?.sessionName.trim();
|
|
293
352
|
return {
|
|
294
|
-
sessionId
|
|
353
|
+
sessionId,
|
|
295
354
|
...(sessionName ? { sessionName } : {}),
|
|
296
|
-
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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 {
|
|
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: [
|
|
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
|
}
|