pi-sessions 0.5.1 → 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 -1
- package/extensions/session-ask/agent.ts +400 -0
- package/extensions/session-ask/navigate.ts +880 -0
- package/extensions/session-ask.ts +82 -134
- package/extensions/session-auto-title/command.ts +1 -1
- package/extensions/session-auto-title/controller.ts +3 -3
- package/extensions/session-auto-title/generate.ts +2 -2
- package/extensions/session-auto-title/model.ts +4 -12
- package/extensions/session-auto-title/retitle.ts +5 -5
- package/extensions/session-auto-title/state.ts +1 -1
- package/extensions/session-auto-title/wizard.ts +4 -4
- package/extensions/session-auto-title.ts +8 -8
- package/extensions/session-handoff/extract.ts +18 -5
- package/extensions/session-handoff/metadata.ts +4 -1
- package/extensions/session-handoff/picker.ts +52 -6
- package/extensions/session-handoff/query.ts +78 -62
- package/extensions/session-handoff/refs.ts +14 -20
- package/extensions/session-handoff/spawn.ts +1 -1
- package/extensions/session-handoff.ts +54 -35
- package/extensions/session-hooks.ts +3 -3
- package/extensions/session-index.ts +4 -4
- package/extensions/session-messaging/broker/process.ts +302 -0
- package/extensions/session-messaging/broker/spawn.ts +164 -0
- package/extensions/session-messaging/pi/incoming-runtime.ts +114 -0
- package/extensions/session-messaging/pi/message-contracts.ts +36 -0
- package/extensions/session-messaging/pi/message-view.ts +131 -0
- package/extensions/session-messaging/pi/renderer.ts +16 -0
- package/extensions/session-messaging/pi/service.ts +370 -0
- package/extensions/session-messaging/pi/tools.ts +92 -0
- package/extensions/session-messaging.ts +30 -0
- package/extensions/session-search/extract.ts +90 -440
- package/extensions/session-search/hooks.ts +20 -28
- package/extensions/session-search/normalize.ts +1 -1
- package/extensions/session-search/reindex.ts +3 -2
- package/extensions/session-search.ts +161 -132
- package/extensions/shared/model.ts +18 -0
- package/extensions/shared/session-broker/active.ts +26 -0
- package/extensions/shared/session-broker/client.ts +253 -0
- package/extensions/shared/session-broker/framing.ts +72 -0
- package/extensions/shared/session-broker/protocol.ts +95 -0
- package/extensions/shared/session-broker/socket-path.ts +30 -0
- package/extensions/shared/session-index/access.ts +128 -0
- package/extensions/shared/session-index/common.ts +46 -51
- package/extensions/shared/session-index/index.ts +6 -5
- package/extensions/shared/session-index/lineage.ts +19 -2
- 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 +25 -9
- package/extensions/shared/session-index/scoring.ts +80 -0
- package/extensions/shared/session-index/search.ts +555 -281
- package/extensions/shared/session-index/sqlite.ts +16 -9
- package/extensions/shared/session-index/store.ts +14 -23
- package/extensions/shared/settings.ts +62 -5
- package/extensions/shared/text.ts +50 -0
- package/package.json +4 -3
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
sourceSessionName?: string | undefined;
|
|
18
|
+
message: string;
|
|
19
|
+
requestResponse?: boolean | undefined;
|
|
20
|
+
relation?: string | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderSessionMessageView(
|
|
24
|
+
options: MessageViewOptions,
|
|
25
|
+
theme: MessageViewTheme,
|
|
26
|
+
): string {
|
|
27
|
+
const header = formatHeader(options, theme);
|
|
28
|
+
const body = formatMessageBody(options.message, options.expanded, theme);
|
|
29
|
+
return body ? `${header}\n\n${body}` : header;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createReceivedSessionMessageComponent(
|
|
33
|
+
received: ReceivedMessageEntry,
|
|
34
|
+
expanded: boolean,
|
|
35
|
+
theme: MessageViewTheme,
|
|
36
|
+
): Component {
|
|
37
|
+
const box = new Box(1, 1, (text: string) => theme.bg("toolSuccessBg", text));
|
|
38
|
+
box.addChild(
|
|
39
|
+
new Text(
|
|
40
|
+
renderSessionMessageView(
|
|
41
|
+
{
|
|
42
|
+
expanded,
|
|
43
|
+
status: "received",
|
|
44
|
+
sourceSessionId: received.source.sessionId,
|
|
45
|
+
sourceSessionName: received.source.sessionName,
|
|
46
|
+
message: received.body,
|
|
47
|
+
requestResponse: received.requestResponse,
|
|
48
|
+
relation: received.relation,
|
|
49
|
+
},
|
|
50
|
+
theme,
|
|
51
|
+
),
|
|
52
|
+
0,
|
|
53
|
+
0,
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
return box;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatSendMessageCall(
|
|
60
|
+
args: { session?: string; message?: string; requestResponse?: boolean } | undefined,
|
|
61
|
+
expanded: boolean,
|
|
62
|
+
status: "sending",
|
|
63
|
+
theme: MessageViewTheme,
|
|
64
|
+
relation?: string | undefined,
|
|
65
|
+
): string {
|
|
66
|
+
return renderSessionMessageView(
|
|
67
|
+
{
|
|
68
|
+
expanded,
|
|
69
|
+
status,
|
|
70
|
+
targetSessionId: args?.session,
|
|
71
|
+
message: args?.message ?? "",
|
|
72
|
+
requestResponse: args?.requestResponse,
|
|
73
|
+
relation,
|
|
74
|
+
},
|
|
75
|
+
theme,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatHeader(options: MessageViewOptions, theme: MessageViewTheme): string {
|
|
80
|
+
const toolName = options.status === "received" ? "incoming_message" : "session_send_message";
|
|
81
|
+
const name = theme.fg("toolTitle", theme.bold(toolName));
|
|
82
|
+
const metadata = [
|
|
83
|
+
options.relation,
|
|
84
|
+
options.requestResponse ? "response requested" : undefined,
|
|
85
|
+
].filter((item): item is string => Boolean(item));
|
|
86
|
+
const metadataHint = metadata.length > 0 ? theme.fg("muted", ` (${metadata.join(", ")})`) : "";
|
|
87
|
+
switch (options.status) {
|
|
88
|
+
case "received":
|
|
89
|
+
return `${name} ${theme.fg("muted", `from ${formatSourceLabel(options)}`)}${metadataHint}`;
|
|
90
|
+
case "sending":
|
|
91
|
+
return `${name} ${theme.fg("muted", `to ${options.targetSessionId ?? "[target pending]"}`)}${metadataHint}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatSourceLabel(options: MessageViewOptions): string {
|
|
96
|
+
if (!options.sourceSessionId) {
|
|
97
|
+
return "[source unknown]";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const title = options.sourceSessionName?.trim();
|
|
101
|
+
return title ? `${title} (${options.sourceSessionId})` : options.sourceSessionId;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function formatKeyHint(
|
|
105
|
+
keybinding: Keybinding,
|
|
106
|
+
description: string,
|
|
107
|
+
theme: MessageViewTheme,
|
|
108
|
+
): string {
|
|
109
|
+
const keyText = getKeybindings().getKeys(keybinding).join("/");
|
|
110
|
+
return `${theme.fg("dim", keyText)}${theme.fg("muted", ` ${description}`)}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function formatMessageBody(message: string, expanded: boolean, theme: MessageViewTheme): string {
|
|
114
|
+
if (!message) {
|
|
115
|
+
return "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const lines = message.replaceAll("\t", " ").split("\n");
|
|
119
|
+
const maxLines = expanded ? lines.length : COLLAPSED_MESSAGE_LINES;
|
|
120
|
+
const visibleLines = lines.slice(0, maxLines);
|
|
121
|
+
const remaining = lines.length - visibleLines.length;
|
|
122
|
+
const body = visibleLines.map((line) => theme.fg("toolOutput", line)).join("\n");
|
|
123
|
+
if (remaining <= 0) {
|
|
124
|
+
return body;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return `${body}${theme.fg(
|
|
128
|
+
"muted",
|
|
129
|
+
`\n... (${remaining} more lines, ${lines.length} total,`,
|
|
130
|
+
)} ${formatKeyHint("app.tools.expand", "to expand", theme)}${theme.fg("muted", ")")}`;
|
|
131
|
+
}
|
|
@@ -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,370 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import {
|
|
4
|
+
type ActiveSessionBrokerConnection,
|
|
5
|
+
setActiveSessionBrokerConnection,
|
|
6
|
+
} from "../../shared/session-broker/active.ts";
|
|
7
|
+
import {
|
|
8
|
+
INCOMING_SESSION_MESSAGE_EVENT,
|
|
9
|
+
type SessionMessageSendResult,
|
|
10
|
+
SessionMessagingClient,
|
|
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";
|
|
23
|
+
import type { IncomingSessionMessageRuntime } from "./incoming-runtime.ts";
|
|
24
|
+
import type { ReceivedMessageEndpoint, ReceivedMessageEntry } from "./message-contracts.ts";
|
|
25
|
+
|
|
26
|
+
export const MESSAGE_SENT_CUSTOM_TYPE = "pi-sessions.message_sent";
|
|
27
|
+
|
|
28
|
+
const RECONNECT_DELAYS_MS = [250, 500, 1_000, 2_000, 5_000, 10_000, 30_000] as const;
|
|
29
|
+
|
|
30
|
+
export interface SendMessageRequest {
|
|
31
|
+
target: string;
|
|
32
|
+
body: string;
|
|
33
|
+
requestResponse?: boolean | undefined;
|
|
34
|
+
sourceToolCallId?: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface IncomingMessageIndexContext {
|
|
38
|
+
source: ReceivedMessageEndpoint;
|
|
39
|
+
target: ReceivedMessageEndpoint;
|
|
40
|
+
relation?: SessionLineageRelation | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class SessionMessagingService {
|
|
44
|
+
private readonly indexPath: string;
|
|
45
|
+
private readonly incomingRuntime: IncomingSessionMessageRuntime;
|
|
46
|
+
private readonly appendEntry: (customType: string, data: unknown) => void;
|
|
47
|
+
private relationBySessionId = new Map<string, SessionLineageRelation | undefined>();
|
|
48
|
+
private readonly connection = new BrokerConnection((requestId, message) =>
|
|
49
|
+
this.handleIncoming(requestId, message),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
indexPath: string,
|
|
54
|
+
incomingRuntime: IncomingSessionMessageRuntime,
|
|
55
|
+
appendEntry: (customType: string, data: unknown) => void,
|
|
56
|
+
) {
|
|
57
|
+
this.indexPath = indexPath;
|
|
58
|
+
this.incomingRuntime = incomingRuntime;
|
|
59
|
+
this.appendEntry = appendEntry;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async start(ctx: ExtensionContext): Promise<void> {
|
|
63
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
64
|
+
this.refreshCachedRelations(sessionId);
|
|
65
|
+
setActiveSessionBrokerConnection(this.connection);
|
|
66
|
+
await this.connection.start(sessionId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
stop(): void {
|
|
70
|
+
setActiveSessionBrokerConnection(undefined);
|
|
71
|
+
this.relationBySessionId.clear();
|
|
72
|
+
this.connection.stop();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async sendMessage(request: SendMessageRequest): Promise<SessionMessageSendResult> {
|
|
76
|
+
const relation = this.getCachedRelationTo(request.target, true);
|
|
77
|
+
const sentAt = new Date().toISOString();
|
|
78
|
+
const messageId = randomUUID();
|
|
79
|
+
const result = await this.connection.sendMessage({
|
|
80
|
+
messageId,
|
|
81
|
+
target: request.target,
|
|
82
|
+
body: request.body,
|
|
83
|
+
sentAt,
|
|
84
|
+
requestResponse: request.requestResponse,
|
|
85
|
+
sourceToolCallId: request.sourceToolCallId,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (result.delivered) {
|
|
89
|
+
this.appendEntry(MESSAGE_SENT_CUSTOM_TYPE, {
|
|
90
|
+
messageId: result.messageId,
|
|
91
|
+
target: request.target,
|
|
92
|
+
body: request.body,
|
|
93
|
+
requestResponse: request.requestResponse,
|
|
94
|
+
sourceToolCallId: request.sourceToolCallId,
|
|
95
|
+
sentAt,
|
|
96
|
+
...(relation === undefined ? {} : { relation }),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private handleIncoming(requestId: string, message: SessionMessagePayload): void {
|
|
104
|
+
const received = this.buildReceivedMessageEntry(message);
|
|
105
|
+
const result = this.incomingRuntime.deliver(received);
|
|
106
|
+
this.connection.acknowledgeIncoming(requestId, message.messageId, result);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getCachedRelationTo(
|
|
110
|
+
sessionId: string | undefined,
|
|
111
|
+
refresh = false,
|
|
112
|
+
): SessionLineageRelation | undefined {
|
|
113
|
+
if (!sessionId) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (this.relationBySessionId.has(sessionId)) {
|
|
118
|
+
return this.relationBySessionId.get(sessionId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!refresh) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const identity = this.connection.currentSessionId;
|
|
126
|
+
if (!identity) {
|
|
127
|
+
throw new Error("Session messaging is not active.");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.refreshCachedRelations(identity);
|
|
131
|
+
if (!this.relationBySessionId.has(sessionId)) {
|
|
132
|
+
this.relationBySessionId.set(sessionId, undefined);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return this.relationBySessionId.get(sessionId);
|
|
136
|
+
}
|
|
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
|
+
|
|
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 {
|
|
198
|
+
const previousRelations = this.relationBySessionId;
|
|
199
|
+
|
|
200
|
+
for (const [sessionId, relation] of previousRelations) {
|
|
201
|
+
if (relation === undefined && !nextRelations.has(sessionId)) {
|
|
202
|
+
nextRelations.set(sessionId, undefined);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
this.relationBySessionId = nextRelations;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
class BrokerConnection implements ActiveSessionBrokerConnection {
|
|
211
|
+
private client: SessionMessagingClient | undefined;
|
|
212
|
+
private sessionId: string | undefined;
|
|
213
|
+
private active = false;
|
|
214
|
+
private reconnectTimer: NodeJS.Timeout | undefined;
|
|
215
|
+
private reconnectDelayIndex = 0;
|
|
216
|
+
private connectPromise: Promise<void> | undefined;
|
|
217
|
+
|
|
218
|
+
constructor(
|
|
219
|
+
private readonly onIncoming: (requestId: string, message: SessionMessagePayload) => void,
|
|
220
|
+
) {}
|
|
221
|
+
|
|
222
|
+
get currentSessionId(): string | undefined {
|
|
223
|
+
return this.sessionId;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async start(sessionId: string): Promise<void> {
|
|
227
|
+
this.active = true;
|
|
228
|
+
this.sessionId = sessionId;
|
|
229
|
+
await this.ensureConnectedNow();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
stop(): void {
|
|
233
|
+
this.active = false;
|
|
234
|
+
this.sessionId = undefined;
|
|
235
|
+
this.clearReconnectTimer();
|
|
236
|
+
this.client?.disconnect();
|
|
237
|
+
this.client = undefined;
|
|
238
|
+
this.connectPromise = undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async listSessionIds(): Promise<string[]> {
|
|
242
|
+
await this.ensureConnectedNow();
|
|
243
|
+
return this.requireClient().listSessionIds();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async sendMessage(options: {
|
|
247
|
+
messageId: string;
|
|
248
|
+
target: string;
|
|
249
|
+
body: string;
|
|
250
|
+
sentAt: string;
|
|
251
|
+
requestResponse?: boolean | undefined;
|
|
252
|
+
sourceToolCallId?: string | undefined;
|
|
253
|
+
}): Promise<SessionMessageSendResult> {
|
|
254
|
+
await this.ensureConnectedNow();
|
|
255
|
+
return this.requireClient().sendMessage(options);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
acknowledgeIncoming(
|
|
259
|
+
requestId: string,
|
|
260
|
+
messageId: string,
|
|
261
|
+
result: { delivered: true } | { delivered: false; error: string },
|
|
262
|
+
): void {
|
|
263
|
+
try {
|
|
264
|
+
this.requireClient().acknowledgeIncoming(requestId, messageId, result);
|
|
265
|
+
} catch {}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private async ensureConnectedNow(): Promise<void> {
|
|
269
|
+
if (this.client?.isConnected) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (!this.active || !this.sessionId) {
|
|
273
|
+
throw new Error("Session messaging is not active.");
|
|
274
|
+
}
|
|
275
|
+
this.clearReconnectTimer();
|
|
276
|
+
|
|
277
|
+
if (!this.connectPromise) {
|
|
278
|
+
this.connectPromise = this.connect().finally(() => {
|
|
279
|
+
this.connectPromise = undefined;
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
await this.connectPromise;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
this.scheduleReconnect();
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private async connect(): Promise<void> {
|
|
292
|
+
const sessionId = this.sessionId;
|
|
293
|
+
if (!sessionId) {
|
|
294
|
+
throw new Error("Session messaging is not active.");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
this.client?.disconnect();
|
|
298
|
+
this.client = undefined;
|
|
299
|
+
|
|
300
|
+
await spawnSessionMessagingBrokerIfNeeded();
|
|
301
|
+
const client = new SessionMessagingClient();
|
|
302
|
+
client.on(INCOMING_SESSION_MESSAGE_EVENT, this.onIncoming);
|
|
303
|
+
client.on("disconnect", () => {
|
|
304
|
+
if (this.client === client) {
|
|
305
|
+
this.client = undefined;
|
|
306
|
+
this.scheduleReconnect();
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
await client.connect(sessionId);
|
|
311
|
+
this.client = client;
|
|
312
|
+
this.reconnectDelayIndex = 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private scheduleReconnect(): void {
|
|
316
|
+
if (!this.active || this.reconnectTimer) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const delay = RECONNECT_DELAYS_MS[this.reconnectDelayIndex] ?? 30_000;
|
|
321
|
+
this.reconnectDelayIndex = Math.min(
|
|
322
|
+
this.reconnectDelayIndex + 1,
|
|
323
|
+
RECONNECT_DELAYS_MS.length - 1,
|
|
324
|
+
);
|
|
325
|
+
this.reconnectTimer = setTimeout(() => {
|
|
326
|
+
this.reconnectTimer = undefined;
|
|
327
|
+
this.ensureConnectedNow().catch(() => {});
|
|
328
|
+
}, delay);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private clearReconnectTimer(): void {
|
|
332
|
+
if (!this.reconnectTimer) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
clearTimeout(this.reconnectTimer);
|
|
336
|
+
this.reconnectTimer = undefined;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private requireClient(): SessionMessagingClient {
|
|
340
|
+
if (!this.client?.isConnected) {
|
|
341
|
+
throw new Error("Session messaging is not connected.");
|
|
342
|
+
}
|
|
343
|
+
return this.client;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function buildReceivedMessageEndpoint(
|
|
348
|
+
sessionId: string,
|
|
349
|
+
session?: SessionLineageRow | undefined,
|
|
350
|
+
): ReceivedMessageEndpoint {
|
|
351
|
+
const sessionName = session?.sessionName.trim();
|
|
352
|
+
return {
|
|
353
|
+
sessionId,
|
|
354
|
+
...(sessionName ? { sessionName } : {}),
|
|
355
|
+
...(session?.cwd ? { cwd: session.cwd } : {}),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function getRelationMapForSession(
|
|
360
|
+
indexPath: string,
|
|
361
|
+
sessionId: string,
|
|
362
|
+
): Map<string, SessionLineageRelation | undefined> {
|
|
363
|
+
return (
|
|
364
|
+
withSessionIndex(
|
|
365
|
+
indexPath,
|
|
366
|
+
{ mode: "read", required: false },
|
|
367
|
+
({ db }) => new Map(getLineageRelationMap(db, sessionId)),
|
|
368
|
+
) ?? new Map()
|
|
369
|
+
);
|
|
370
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { SEND_MESSAGE_PARAMS, type SendMessageParams } from "./message-contracts.ts";
|
|
4
|
+
import { formatSendMessageCall } from "./message-view.ts";
|
|
5
|
+
import type { SessionMessagingService } from "./service.ts";
|
|
6
|
+
|
|
7
|
+
export function registerSessionMessagingTools(
|
|
8
|
+
pi: ExtensionAPI,
|
|
9
|
+
service: SessionMessagingService,
|
|
10
|
+
): void {
|
|
11
|
+
pi.registerTool({
|
|
12
|
+
name: "session_send_message",
|
|
13
|
+
label: "Send Message to Session",
|
|
14
|
+
description: "Send a message to another live pi session",
|
|
15
|
+
promptSnippet: "Send a message to another live pi session",
|
|
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
|
+
],
|
|
19
|
+
parameters: SEND_MESSAGE_PARAMS,
|
|
20
|
+
renderCall(args, theme, context) {
|
|
21
|
+
const component = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
22
|
+
component.setText(
|
|
23
|
+
formatSendMessageCall(
|
|
24
|
+
args,
|
|
25
|
+
context.expanded,
|
|
26
|
+
"sending",
|
|
27
|
+
theme,
|
|
28
|
+
service.getCachedRelationTo(args?.session),
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
return component;
|
|
32
|
+
},
|
|
33
|
+
renderResult(result, _options, theme, context) {
|
|
34
|
+
if (!context.isError) {
|
|
35
|
+
return new Text("", 0, 0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const output = getFirstText(result);
|
|
39
|
+
return new Text(output ? `\n${theme.fg("error", output)}` : "", 0, 0);
|
|
40
|
+
},
|
|
41
|
+
async execute(toolCallId, params: SendMessageParams, _signal, _onUpdate, _ctx) {
|
|
42
|
+
const target = params.session.trim();
|
|
43
|
+
if (!target) {
|
|
44
|
+
throw new Error("session_send_message requires a target session id.");
|
|
45
|
+
}
|
|
46
|
+
if (target.startsWith("@session:")) {
|
|
47
|
+
throw new Error("Use the bare session UUID, not an @session token.");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const body = params.message.trim();
|
|
51
|
+
if (!body) {
|
|
52
|
+
throw new Error("session_send_message requires a non-empty message.");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const result = await service.sendMessage({
|
|
57
|
+
target,
|
|
58
|
+
body,
|
|
59
|
+
requestResponse: params.requestResponse,
|
|
60
|
+
sourceToolCallId: toolCallId,
|
|
61
|
+
});
|
|
62
|
+
if (!result.delivered) {
|
|
63
|
+
throw new Error(result.error ?? "Message was not delivered.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: "text" as const,
|
|
70
|
+
text: `Message delivered to ${target}.`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
details: {
|
|
74
|
+
delivered: true,
|
|
75
|
+
messageId: result.messageId,
|
|
76
|
+
targetSessionId: target,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error(formatError(error));
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getFirstText(result: { content: Array<{ type: string; text?: string }> }): string {
|
|
87
|
+
return result.content.find((item) => item.type === "text")?.text ?? "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatError(error: unknown): string {
|
|
91
|
+
return error instanceof Error && error.message.trim() ? error.message : String(error);
|
|
92
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { IncomingSessionMessageRuntime } from "./session-messaging/pi/incoming-runtime.ts";
|
|
3
|
+
import { registerSessionMessagingRenderer } from "./session-messaging/pi/renderer.ts";
|
|
4
|
+
import { SessionMessagingService } from "./session-messaging/pi/service.ts";
|
|
5
|
+
import { registerSessionMessagingTools } from "./session-messaging/pi/tools.ts";
|
|
6
|
+
import { loadSettings } from "./shared/settings.ts";
|
|
7
|
+
|
|
8
|
+
export default function sessionMessagingExtension(pi: ExtensionAPI): void {
|
|
9
|
+
const settings = loadSettings();
|
|
10
|
+
const incomingRuntime = new IncomingSessionMessageRuntime(pi);
|
|
11
|
+
const service = new SessionMessagingService(
|
|
12
|
+
settings.index.path,
|
|
13
|
+
incomingRuntime,
|
|
14
|
+
pi.appendEntry.bind(pi),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
18
|
+
incomingRuntime.replayPending(ctx);
|
|
19
|
+
try {
|
|
20
|
+
await service.start(ctx);
|
|
21
|
+
} catch {}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
pi.on("session_shutdown", () => {
|
|
25
|
+
service.stop();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
registerSessionMessagingTools(pi, service);
|
|
29
|
+
registerSessionMessagingRenderer(pi);
|
|
30
|
+
}
|