pi-sessions 0.14.3 → 0.16.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 +63 -9
- package/extensions/hosts/contract.ts +51 -0
- package/extensions/hosts/registry.ts +129 -0
- package/extensions/pi-sessions.ts +36 -10
- package/extensions/session-handoff/install.ts +4 -1
- package/extensions/session-handoff/launch/backend.ts +3 -10
- package/extensions/session-handoff/launch-options.ts +17 -2
- package/extensions/session-handoff/launch-target.ts +28 -16
- package/extensions/session-handoff/metadata.ts +2 -0
- package/extensions/session-handoff/query.ts +5 -4
- package/extensions/session-handoff/spawn.ts +2 -1
- package/extensions/session-handoff/tool-schema.ts +10 -2
- package/extensions/session-handoff/tool.ts +1 -0
- package/extensions/session-hooks/install.ts +14 -5
- package/extensions/session-index/install.ts +25 -4
- package/extensions/session-index/panel.ts +9 -0
- package/extensions/session-messaging/message-router.ts +91 -0
- package/extensions/session-messaging/pi/reachable-contract.ts +2 -1
- package/extensions/session-messaging/pi/reachable-tool.ts +24 -4
- package/extensions/session-messaging/pi/tools.ts +9 -1
- package/extensions/shared/session-index/access.ts +42 -5
- package/extensions/shared/session-index/common.ts +1 -0
- package/extensions/shared/session-index/recovery.ts +270 -0
- package/extensions/shared/session-index/schema.ts +9 -1
- package/extensions/shared/session-index/sqlite.ts +20 -10
- package/extensions/{session-search/hooks.ts → shared/session-index/sync.ts} +40 -16
- package/extensions/subagents/install.ts +13 -13
- package/extensions/subagents/settle.ts +1 -1
- package/extensions/subagents/wake.ts +32 -116
- package/package.json +5 -5
- package/extensions/session-search/reindex.ts +0 -97
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SessionWaker, WakeMessaging } from "../session-messaging/message-router.ts";
|
|
3
3
|
import {
|
|
4
4
|
createTmuxWindow,
|
|
5
5
|
killTmuxWindow,
|
|
@@ -9,125 +9,60 @@ import {
|
|
|
9
9
|
} from "../shared/tmux.ts";
|
|
10
10
|
import { findOwnedSubagentLaunch, type SubagentLaunched } from "./ledger.ts";
|
|
11
11
|
|
|
12
|
-
const SESSION_READY_TIMEOUT_MS = 30_000;
|
|
13
|
-
|
|
14
12
|
export interface WakeParentSession {
|
|
15
13
|
sessionId: string;
|
|
16
14
|
epoch: number;
|
|
17
15
|
getBranch(): readonly SessionEntry[];
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
sendMessage(request: SendMessageRequest): Promise<SendMessageResult>;
|
|
22
|
-
listSessions(): Promise<string[]>;
|
|
23
|
-
waitForSession(sessionId: string, timeoutMs: number): Promise<boolean>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface SubagentMessageRouterOptions {
|
|
27
|
-
readyTimeoutMs?: number;
|
|
28
|
-
onMaterialize?(launch: SubagentLaunched): void;
|
|
29
|
-
afterOwnedSend?(): Promise<void> | void;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class SubagentMessageRouter {
|
|
33
|
-
private readonly wakeBySessionId = new Map<string, Promise<void>>();
|
|
34
|
-
|
|
18
|
+
export class SubagentWaker implements SessionWaker {
|
|
35
19
|
constructor(
|
|
36
20
|
private readonly executor: TmuxExecutor,
|
|
37
21
|
private readonly messaging: WakeMessaging,
|
|
38
22
|
private readonly getParent: () => WakeParentSession | undefined,
|
|
39
23
|
private readonly isCurrent: (epoch: number) => boolean,
|
|
40
|
-
private readonly options:
|
|
24
|
+
private readonly options: {
|
|
25
|
+
onMaterialize?(launch: SubagentLaunched): void;
|
|
26
|
+
afterSend?(): Promise<void> | void;
|
|
27
|
+
} = {},
|
|
41
28
|
) {}
|
|
42
29
|
|
|
43
|
-
|
|
44
|
-
let failedLiveSend: SendMessageResult | undefined;
|
|
45
|
-
if (await this.isLive(request.target)) {
|
|
46
|
-
const result = await this.messaging.sendMessage(request);
|
|
47
|
-
if (result.delivered || !isTargetDeparture(result)) {
|
|
48
|
-
return result;
|
|
49
|
-
}
|
|
50
|
-
failedLiveSend = result;
|
|
51
|
-
}
|
|
52
|
-
|
|
30
|
+
owns(target: string): boolean {
|
|
53
31
|
const parent = this.getParent();
|
|
54
|
-
|
|
55
|
-
? findOwnedSubagentLaunch(parent.getBranch(), parent.sessionId, request.target)
|
|
56
|
-
: undefined;
|
|
57
|
-
if (!parent || !launch) {
|
|
58
|
-
return failedLiveSend ?? this.messaging.sendMessage(request);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
await this.wake(parent, launch);
|
|
63
|
-
const result = await this.sendOwned(parent, request);
|
|
64
|
-
if (result.delivered || !isTargetDeparture(result)) {
|
|
65
|
-
return result;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
await this.wake(parent, launch);
|
|
69
|
-
return this.sendOwned(parent, request);
|
|
70
|
-
} finally {
|
|
71
|
-
await this.options.afterOwnedSend?.();
|
|
72
|
-
}
|
|
32
|
+
return Boolean(parent && findOwnedSubagentLaunch(parent.getBranch(), parent.sessionId, target));
|
|
73
33
|
}
|
|
74
34
|
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
if (existing) {
|
|
78
|
-
return existing;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const wake = this.ensureReady(parent, launch).finally(() => {
|
|
82
|
-
if (this.wakeBySessionId.get(launch.childSessionId) === wake) {
|
|
83
|
-
this.wakeBySessionId.delete(launch.childSessionId);
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
this.wakeBySessionId.set(launch.childSessionId, wake);
|
|
87
|
-
return wake;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
private async ensureReady(parent: WakeParentSession, launch: SubagentLaunched): Promise<void> {
|
|
91
|
-
if (await this.isLive(launch.childSessionId)) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
35
|
+
async wake(target: string): Promise<void> {
|
|
36
|
+
const { parent, launch } = this.resolve(target);
|
|
95
37
|
const tmuxSession = tmuxSessionName(parent.sessionId);
|
|
96
38
|
const hasWindow = (await listTmuxWindows(this.executor, tmuxSession)).some(
|
|
97
|
-
(window) => window.piSessionId ===
|
|
39
|
+
(window) => window.piSessionId === target,
|
|
98
40
|
);
|
|
41
|
+
if (!hasWindow) await this.createWindow(parent, launch, tmuxSession);
|
|
42
|
+
}
|
|
99
43
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
await this.messaging.waitForSession(
|
|
105
|
-
launch.childSessionId,
|
|
106
|
-
this.options.readyTimeoutMs ?? SESSION_READY_TIMEOUT_MS,
|
|
107
|
-
)
|
|
108
|
-
) {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
this.requireCurrent(parent);
|
|
113
|
-
const killed = await killTmuxWindow(this.executor, tmuxSession, launch.childSessionId);
|
|
44
|
+
async restart(target: string): Promise<void> {
|
|
45
|
+
const { parent, launch } = this.resolve(target);
|
|
46
|
+
const tmuxSession = tmuxSessionName(parent.sessionId);
|
|
47
|
+
const killed = await killTmuxWindow(this.executor, tmuxSession, target);
|
|
114
48
|
if (!killed) {
|
|
115
49
|
throw new Error(
|
|
116
|
-
`Subagent ${
|
|
50
|
+
`Subagent ${target} did not register and its stale tmux window could not be stopped.`,
|
|
117
51
|
);
|
|
118
52
|
}
|
|
119
|
-
|
|
120
53
|
await this.createWindow(parent, launch, tmuxSession);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
afterSend(): Promise<void> | void {
|
|
57
|
+
return this.options.afterSend?.();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private resolve(target: string): { parent: WakeParentSession; launch: SubagentLaunched } {
|
|
61
|
+
const parent = this.getParent();
|
|
62
|
+
const launch = parent && findOwnedSubagentLaunch(parent.getBranch(), parent.sessionId, target);
|
|
63
|
+
if (!parent || !launch) throw new Error(`No owned subagent ${target}.`);
|
|
64
|
+
this.requireCurrent(parent);
|
|
65
|
+
return { parent, launch };
|
|
131
66
|
}
|
|
132
67
|
|
|
133
68
|
private async createWindow(
|
|
@@ -135,9 +70,7 @@ export class SubagentMessageRouter {
|
|
|
135
70
|
launch: SubagentLaunched,
|
|
136
71
|
tmuxSession: string,
|
|
137
72
|
): Promise<void> {
|
|
138
|
-
if (await this.
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
73
|
+
if ((await this.messaging.listSessions()).includes(launch.childSessionId)) return;
|
|
141
74
|
this.requireCurrent(parent);
|
|
142
75
|
this.options.onMaterialize?.(launch);
|
|
143
76
|
await createTmuxWindow(this.executor, {
|
|
@@ -149,25 +82,8 @@ export class SubagentMessageRouter {
|
|
|
149
82
|
});
|
|
150
83
|
}
|
|
151
84
|
|
|
152
|
-
private sendOwned(
|
|
153
|
-
parent: WakeParentSession,
|
|
154
|
-
request: SendMessageRequest,
|
|
155
|
-
): Promise<SendMessageResult> {
|
|
156
|
-
this.requireCurrent(parent);
|
|
157
|
-
return this.messaging.sendMessage(request);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
private async isLive(sessionId: string): Promise<boolean> {
|
|
161
|
-
return (await this.messaging.listSessions()).includes(sessionId);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
85
|
private requireCurrent(parent: WakeParentSession): void {
|
|
165
|
-
if (!this.isCurrent(parent.epoch))
|
|
86
|
+
if (!this.isCurrent(parent.epoch))
|
|
166
87
|
throw new Error("The parent session changed while waking its subagent.");
|
|
167
|
-
}
|
|
168
88
|
}
|
|
169
89
|
}
|
|
170
|
-
|
|
171
|
-
function isTargetDeparture(result: SendMessageResult): boolean {
|
|
172
|
-
return !result.delivered && (result.reason === "no_session" || result.reason === "disconnected");
|
|
173
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-sessions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi session search, ask, handoff, messaging, auto-titling, and indexing tools",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@biomejs/biome": "^2.5.10",
|
|
41
|
-
"@earendil-works/pi-agent-core": "^1.0.
|
|
42
|
-
"@earendil-works/pi-ai": "^1.0.
|
|
43
|
-
"@earendil-works/pi-coding-agent": "^1.0.
|
|
44
|
-
"@earendil-works/pi-tui": "^1.0.
|
|
41
|
+
"@earendil-works/pi-agent-core": "^1.0.4",
|
|
42
|
+
"@earendil-works/pi-ai": "^1.0.4",
|
|
43
|
+
"@earendil-works/pi-coding-agent": "^1.0.4",
|
|
44
|
+
"@earendil-works/pi-tui": "^1.0.4",
|
|
45
45
|
"@types/node": "^24.13.3",
|
|
46
46
|
"strip-ansi": "^7.2.0",
|
|
47
47
|
"typebox": "1.3.27",
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import {
|
|
4
|
-
clearSessionIndexedData,
|
|
5
|
-
ensureIndexDir,
|
|
6
|
-
initializeSchema,
|
|
7
|
-
insertSessionFileTouch,
|
|
8
|
-
insertTextChunk,
|
|
9
|
-
openIndexDatabase,
|
|
10
|
-
rebuildSessionLineageRelations,
|
|
11
|
-
type SessionIndexDatabase,
|
|
12
|
-
setMetadata,
|
|
13
|
-
upsertSession,
|
|
14
|
-
} from "../shared/session-index/index.ts";
|
|
15
|
-
import { type ExtractedSessionRecord, extractSessionRecord, listSessionFiles } from "./extract.ts";
|
|
16
|
-
|
|
17
|
-
export interface ReindexOptions {
|
|
18
|
-
indexPath: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface ReindexResult {
|
|
22
|
-
sessionCount: number;
|
|
23
|
-
chunkCount: number;
|
|
24
|
-
indexPath: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function rebuildSessionIndex(options: ReindexOptions): Promise<ReindexResult> {
|
|
28
|
-
const indexPath = options.indexPath;
|
|
29
|
-
ensureIndexDir(path.dirname(indexPath));
|
|
30
|
-
const sessionFiles = listSessionFiles(path.join(getAgentDir(), "sessions"));
|
|
31
|
-
|
|
32
|
-
const db = openIndexDatabase(indexPath, { create: true });
|
|
33
|
-
try {
|
|
34
|
-
db.transaction(() => {
|
|
35
|
-
dropIndexTables(db);
|
|
36
|
-
initializeSchema(db);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const indexedSessions = new Map<string, { modifiedAt: string; chunks: number }>();
|
|
40
|
-
let chunkCount = 0;
|
|
41
|
-
for (const sessionFile of sessionFiles) {
|
|
42
|
-
const extracted = extractSessionRecord(sessionFile);
|
|
43
|
-
if (!extracted) {
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const previous = indexedSessions.get(extracted.sessionId);
|
|
48
|
-
if (previous && previous.modifiedAt > extracted.modifiedAt) continue;
|
|
49
|
-
db.transaction(() => indexSession(db, extracted));
|
|
50
|
-
chunkCount += extracted.chunks.length - (previous?.chunks ?? 0);
|
|
51
|
-
indexedSessions.set(extracted.sessionId, {
|
|
52
|
-
modifiedAt: extracted.modifiedAt,
|
|
53
|
-
chunks: extracted.chunks.length,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
db.transaction(() => {
|
|
58
|
-
rebuildSessionLineageRelations(db);
|
|
59
|
-
setMetadata(db, "indexed_at", new Date().toISOString());
|
|
60
|
-
setMetadata(db, "session_source", "session files");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
return { sessionCount: indexedSessions.size, chunkCount, indexPath };
|
|
64
|
-
} finally {
|
|
65
|
-
db.close();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function dropIndexTables(db: SessionIndexDatabase): void {
|
|
70
|
-
// Children before parents: foreign_keys is ON and DROP TABLE runs an
|
|
71
|
-
// implicit DELETE that parent-side constraints would reject.
|
|
72
|
-
db.exec(`
|
|
73
|
-
DROP TABLE IF EXISTS session_lineage_relations;
|
|
74
|
-
DROP TABLE IF EXISTS session_repo_roots;
|
|
75
|
-
DROP TABLE IF EXISTS session_text_chunks;
|
|
76
|
-
DROP TABLE IF EXISTS session_file_touches;
|
|
77
|
-
DROP TABLE IF EXISTS session_text_chunks_fts;
|
|
78
|
-
DROP TABLE IF EXISTS sessions;
|
|
79
|
-
DROP TABLE IF EXISTS metadata;
|
|
80
|
-
`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function indexSession(db: SessionIndexDatabase, extracted: ExtractedSessionRecord): void {
|
|
84
|
-
clearSessionIndexedData(db, extracted.sessionId);
|
|
85
|
-
upsertSession(db, extracted, "full_reindex");
|
|
86
|
-
|
|
87
|
-
for (const chunk of extracted.chunks) {
|
|
88
|
-
insertTextChunk(db, { sessionId: extracted.sessionId, ...chunk });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
for (const fileTouch of extracted.fileTouches) {
|
|
92
|
-
insertSessionFileTouch(db, {
|
|
93
|
-
sessionId: extracted.sessionId,
|
|
94
|
-
...fileTouch,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|