@openclaw/acpx 2026.7.2-beta.7 → 2026.8.1-beta.3
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/dist/{config-schema-lrk5nlcV.js → config-schema-DN_uAi4R.js} +0 -2
- package/dist/doctor-contract-api.js +36 -4
- package/dist/index.js +47 -1318
- package/dist/pi-session-catalog-runtime-BIu-8uRZ.js +887 -0
- package/dist/pi-session-paths-EMbd4Hkz.js +84 -0
- package/dist/{process-lease-DSLDgiNl.js → process-lease-Cwvj7WGe.js} +5 -2
- package/dist/{process-reaper-DFwbcdPa.js → process-reaper-DduWm_7N.js} +66 -24
- package/dist/register.runtime-Dj29TKFy.js +180 -0
- package/dist/register.runtime.js +1 -1
- package/dist/{runtime-By_lR8uk.js → runtime-BmOxa15I.js} +89 -67
- package/dist/{service-CBZSAymH.js → service-PRlUtXMf.js} +39 -45
- package/openclaw.plugin.json +5 -17
- package/package.json +5 -5
- package/dist/register.runtime-BbS2JTTv.js +0 -263
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
import { c as PI_SESSION_READ_COMMAND, i as PI_LOCAL_SESSION_HOST_ID, l as PI_TERMINAL_RESUME_COMMAND, n as piSessionStore, o as PI_SESSIONS_LIST_COMMAND, r as piSessionStoreAvailable, s as PI_SESSION_ID_PATTERN, t as piAcpSessionStoreRoot } from "./pi-session-paths-EMbd4Hkz.js";
|
|
2
|
+
import { parseDateFirstTimestampMs } from "openclaw/plugin-sdk/number-runtime";
|
|
3
|
+
import { resolveNodeHostExecutable } from "openclaw/plugin-sdk/node-host";
|
|
4
|
+
import { createSessionCatalogFamily, importSessionCatalogHistory, isExternalUserText, listAdoptedSessionCatalogSessions, sessionCatalogAdoptedSessionKey, sessionCatalogPaging } from "openclaw/plugin-sdk/session-catalog";
|
|
5
|
+
import { isRecord, normalizeBoundedOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
6
|
+
import { createReadStream } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import fs$1 from "node:fs/promises";
|
|
9
|
+
import process from "node:process";
|
|
10
|
+
import { resolveAcpSessionAvailability } from "openclaw/plugin-sdk/acp-runtime";
|
|
11
|
+
import { resolveSessionAgentIds } from "openclaw/plugin-sdk/agent-runtime";
|
|
12
|
+
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
13
|
+
import { isPathStrictlyInside } from "openclaw/plugin-sdk/file-access-runtime";
|
|
14
|
+
//#region extensions/acpx/src/pi-session-timestamp.ts
|
|
15
|
+
/** Preserve Pi JSONL's date-first string contract while accepting numeric millisecond values. */
|
|
16
|
+
function parsePiSessionTimestampMs(value) {
|
|
17
|
+
return parseDateFirstTimestampMs(value);
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region extensions/acpx/src/pi-session-store.ts
|
|
21
|
+
const MAX_DISCOVERY_FILES = 1e4;
|
|
22
|
+
const SUMMARY_SCAN_BATCH_SIZE = 100;
|
|
23
|
+
const MAX_SUMMARY_CACHE_ENTRIES = 256;
|
|
24
|
+
const MAX_SESSION_BYTES = 32 * 1024 * 1024;
|
|
25
|
+
const MAX_SUMMARY_LINE_BYTES = 1024 * 1024;
|
|
26
|
+
const APPEND_PROOF_EDGE_BYTES = 64 * 1024;
|
|
27
|
+
const IO_CONCURRENCY = 8;
|
|
28
|
+
const PI_FILE_CANDIDATE_CACHE_TTL_MS = 32e3;
|
|
29
|
+
const PI_FILE_CANDIDATE_CACHE_MAX_ENTRIES = 8;
|
|
30
|
+
const SESSION_ID_PATTERN = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
|
|
31
|
+
const summaryCache = /* @__PURE__ */ new Map();
|
|
32
|
+
const threadFileCache = /* @__PURE__ */ new Map();
|
|
33
|
+
const piFileCandidateCache = /* @__PURE__ */ new Map();
|
|
34
|
+
function threadCacheKey(storeRoot, threadId) {
|
|
35
|
+
return `${storeRoot}\0${threadId}`;
|
|
36
|
+
}
|
|
37
|
+
function forgetCachedSummary(file) {
|
|
38
|
+
const cached = summaryCache.get(file);
|
|
39
|
+
const threadId = cached?.summary?.threadId;
|
|
40
|
+
if (cached && threadId) {
|
|
41
|
+
const key = threadCacheKey(cached.storeRoot, threadId);
|
|
42
|
+
if (threadFileCache.get(key) === file) threadFileCache.delete(key);
|
|
43
|
+
}
|
|
44
|
+
summaryCache.delete(file);
|
|
45
|
+
}
|
|
46
|
+
function cacheSummary(file, value) {
|
|
47
|
+
forgetCachedSummary(file);
|
|
48
|
+
summaryCache.set(file, value);
|
|
49
|
+
while (summaryCache.size > MAX_SUMMARY_CACHE_ENTRIES) {
|
|
50
|
+
const oldest = summaryCache.keys().next().value;
|
|
51
|
+
if (typeof oldest !== "string") break;
|
|
52
|
+
forgetCachedSummary(oldest);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function discoverPiSessionFiles(env) {
|
|
56
|
+
const store = piSessionStore(env);
|
|
57
|
+
const resolvedRoot = await realpathOrResolve(store.root);
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = await fs$1.readdir(resolvedRoot, { withFileTypes: true });
|
|
61
|
+
} catch {
|
|
62
|
+
return {
|
|
63
|
+
root: store.root,
|
|
64
|
+
files: []
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (store.flat) return {
|
|
68
|
+
root: store.root,
|
|
69
|
+
files: entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).slice(0, MAX_DISCOVERY_FILES).map((entry) => path.join(resolvedRoot, entry.name))
|
|
70
|
+
};
|
|
71
|
+
const files = [];
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (!entry.isDirectory() || files.length >= MAX_DISCOVERY_FILES) continue;
|
|
74
|
+
const directory = path.join(resolvedRoot, entry.name);
|
|
75
|
+
let children;
|
|
76
|
+
try {
|
|
77
|
+
children = await fs$1.readdir(directory, { withFileTypes: true });
|
|
78
|
+
} catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (const child of children) if (child.isFile() && child.name.endsWith(".jsonl")) {
|
|
82
|
+
files.push(path.join(directory, child.name));
|
|
83
|
+
if (files.length >= MAX_DISCOVERY_FILES) break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
root: store.root,
|
|
88
|
+
files
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async function realpathOrResolve(value) {
|
|
92
|
+
try {
|
|
93
|
+
return await fs$1.realpath(value);
|
|
94
|
+
} catch {
|
|
95
|
+
return path.resolve(value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function mapConcurrent(values, limit, mapper) {
|
|
99
|
+
const results = [];
|
|
100
|
+
results.length = values.length;
|
|
101
|
+
let nextIndex = 0;
|
|
102
|
+
const workers = Array.from({ length: Math.min(limit, values.length) }, async () => {
|
|
103
|
+
while (nextIndex < values.length) {
|
|
104
|
+
const index = nextIndex++;
|
|
105
|
+
results[index] = await mapper(values[index]);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
await Promise.all(workers);
|
|
109
|
+
return results;
|
|
110
|
+
}
|
|
111
|
+
async function scanPiFileCandidates(env) {
|
|
112
|
+
const { root, files } = await discoverPiSessionFiles(env);
|
|
113
|
+
const configuredAcpRoot = piAcpSessionStoreRoot(env);
|
|
114
|
+
const acpRoot = configuredAcpRoot ? await realpathOrResolve(configuredAcpRoot) : void 0;
|
|
115
|
+
return (await mapConcurrent(files, IO_CONCURRENCY, async (file) => {
|
|
116
|
+
try {
|
|
117
|
+
const stats = await fs$1.stat(file);
|
|
118
|
+
return stats.isFile() ? {
|
|
119
|
+
file,
|
|
120
|
+
storeRoot: root,
|
|
121
|
+
identity: `${String(stats.dev)}:${String(stats.ino)}:${String(stats.birthtimeMs)}`,
|
|
122
|
+
mtimeMs: stats.mtimeMs,
|
|
123
|
+
size: stats.size,
|
|
124
|
+
resumable: acpRoot ? isPathStrictlyInside(acpRoot, file) : false
|
|
125
|
+
} : void 0;
|
|
126
|
+
} catch {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
})).filter((candidate) => candidate !== void 0).toSorted((left, right) => right.mtimeMs - left.mtimeMs);
|
|
130
|
+
}
|
|
131
|
+
async function piFileCandidates(env) {
|
|
132
|
+
const store = piSessionStore(env);
|
|
133
|
+
const key = `${store.root}\0${store.flat}\0${piAcpSessionStoreRoot(env) ?? ""}`;
|
|
134
|
+
const cached = piFileCandidateCache.get(key);
|
|
135
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
136
|
+
piFileCandidateCache.delete(key);
|
|
137
|
+
piFileCandidateCache.set(key, cached);
|
|
138
|
+
return await cached.candidates;
|
|
139
|
+
}
|
|
140
|
+
if (cached) piFileCandidateCache.delete(key);
|
|
141
|
+
const candidates = scanPiFileCandidates(env);
|
|
142
|
+
const entry = {
|
|
143
|
+
expiresAt: Date.now() + PI_FILE_CANDIDATE_CACHE_TTL_MS,
|
|
144
|
+
candidates
|
|
145
|
+
};
|
|
146
|
+
piFileCandidateCache.set(key, entry);
|
|
147
|
+
while (piFileCandidateCache.size > PI_FILE_CANDIDATE_CACHE_MAX_ENTRIES) {
|
|
148
|
+
const oldest = piFileCandidateCache.keys().next();
|
|
149
|
+
if (oldest.done) break;
|
|
150
|
+
piFileCandidateCache.delete(oldest.value);
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
return await candidates;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (piFileCandidateCache.get(key) === entry) piFileCandidateCache.delete(key);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function parsePiJsonLines(content) {
|
|
160
|
+
return content.split(/\r?\n/u).flatMap((line) => {
|
|
161
|
+
if (!line.trim()) return [];
|
|
162
|
+
try {
|
|
163
|
+
const value = JSON.parse(line);
|
|
164
|
+
return isRecord(value) ? [value] : [];
|
|
165
|
+
} catch {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function textFromContent$2(content) {
|
|
171
|
+
if (typeof content === "string") return content;
|
|
172
|
+
if (!Array.isArray(content)) return "";
|
|
173
|
+
return content.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n");
|
|
174
|
+
}
|
|
175
|
+
function processSummaryLine(state, line) {
|
|
176
|
+
const entry = parsePiJsonLines((line.at(-1) === 13 ? line.subarray(0, -1) : line).toString("utf8"))[0];
|
|
177
|
+
if (!entry) return;
|
|
178
|
+
if (!state.header) {
|
|
179
|
+
if (entry.type !== "session") {
|
|
180
|
+
state.invalid = true;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
state.header = entry;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (entry.type === "session_info") state.name = normalizeBoundedOptionalString(entry.name, 1e3);
|
|
187
|
+
else if (!state.firstMessage && entry.type === "message" && isRecord(entry.message) && entry.message.role === "user") state.firstMessage = normalizeBoundedOptionalString(textFromContent$2(entry.message.content), 1e3);
|
|
188
|
+
}
|
|
189
|
+
function appendSummaryBytes(state, bytes) {
|
|
190
|
+
if (state.discarding || bytes.length === 0) return;
|
|
191
|
+
if (state.pending.length + bytes.length > MAX_SUMMARY_LINE_BYTES) {
|
|
192
|
+
state.pending = Buffer.alloc(0);
|
|
193
|
+
state.discarding = true;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
state.pending = state.pending.length === 0 ? Buffer.from(bytes) : Buffer.concat([state.pending, bytes]);
|
|
197
|
+
}
|
|
198
|
+
async function scanSummaryAppend(candidate, start, state) {
|
|
199
|
+
if (start >= candidate.size || state.invalid) return;
|
|
200
|
+
const stream = createReadStream(candidate.file, {
|
|
201
|
+
start,
|
|
202
|
+
end: candidate.size - 1
|
|
203
|
+
});
|
|
204
|
+
for await (const value of stream) {
|
|
205
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
206
|
+
let offset = 0;
|
|
207
|
+
while (offset < chunk.length) {
|
|
208
|
+
const newline = chunk.indexOf(10, offset);
|
|
209
|
+
const end = newline < 0 ? chunk.length : newline;
|
|
210
|
+
appendSummaryBytes(state, chunk.subarray(offset, end));
|
|
211
|
+
if (newline < 0) break;
|
|
212
|
+
if (!state.discarding) processSummaryLine(state, state.pending);
|
|
213
|
+
state.pending = Buffer.alloc(0);
|
|
214
|
+
state.discarding = false;
|
|
215
|
+
if (state.invalid) return;
|
|
216
|
+
offset = newline + 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function readAppendProof(file, size) {
|
|
221
|
+
const length = Math.min(size, APPEND_PROOF_EDGE_BYTES);
|
|
222
|
+
if (length === 0) return {
|
|
223
|
+
head: Buffer.alloc(0),
|
|
224
|
+
tail: Buffer.alloc(0)
|
|
225
|
+
};
|
|
226
|
+
const handle = await fs$1.open(file, "r");
|
|
227
|
+
try {
|
|
228
|
+
const head = Buffer.alloc(length);
|
|
229
|
+
const tail = Buffer.alloc(length);
|
|
230
|
+
const [headRead, tailRead] = await Promise.all([handle.read(head, 0, length, 0), handle.read(tail, 0, length, size - length)]);
|
|
231
|
+
return {
|
|
232
|
+
head: head.subarray(0, headRead.bytesRead),
|
|
233
|
+
tail: tail.subarray(0, tailRead.bytesRead)
|
|
234
|
+
};
|
|
235
|
+
} finally {
|
|
236
|
+
await handle.close();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function cachedPrefixIsUnchanged(candidate, cached) {
|
|
240
|
+
if (cached.identity !== candidate.identity || cached.size >= candidate.size) return false;
|
|
241
|
+
const current = await readAppendProof(candidate.file, cached.size);
|
|
242
|
+
return current.head.equals(cached.appendProof.head) && current.tail.equals(cached.appendProof.tail);
|
|
243
|
+
}
|
|
244
|
+
async function readPiSessionSummary(candidate) {
|
|
245
|
+
const cached = summaryCache.get(candidate.file);
|
|
246
|
+
if (cached?.mtimeMs === candidate.mtimeMs && cached.size === candidate.size) {
|
|
247
|
+
summaryCache.delete(candidate.file);
|
|
248
|
+
summaryCache.set(candidate.file, cached);
|
|
249
|
+
return cached.summary ? {
|
|
250
|
+
...cached.summary,
|
|
251
|
+
canContinue: candidate.resumable
|
|
252
|
+
} : cached.summary;
|
|
253
|
+
}
|
|
254
|
+
let summary;
|
|
255
|
+
let scanState;
|
|
256
|
+
let appendProof;
|
|
257
|
+
try {
|
|
258
|
+
const resumable = cached && await cachedPrefixIsUnchanged(candidate, cached) ? cached : void 0;
|
|
259
|
+
scanState = resumable ? {
|
|
260
|
+
...resumable.scanState,
|
|
261
|
+
pending: Buffer.from(resumable.scanState.pending)
|
|
262
|
+
} : {
|
|
263
|
+
pending: Buffer.alloc(0),
|
|
264
|
+
discarding: false,
|
|
265
|
+
invalid: false
|
|
266
|
+
};
|
|
267
|
+
await scanSummaryAppend(candidate, resumable?.size ?? 0, scanState);
|
|
268
|
+
appendProof = await readAppendProof(candidate.file, candidate.size);
|
|
269
|
+
const projectedState = {
|
|
270
|
+
...scanState,
|
|
271
|
+
pending: Buffer.from(scanState.pending)
|
|
272
|
+
};
|
|
273
|
+
if (!projectedState.discarding && projectedState.pending.length > 0) processSummaryLine(projectedState, projectedState.pending);
|
|
274
|
+
const { header, name, firstMessage } = projectedState;
|
|
275
|
+
const version = header?.type === "session" && typeof header.version === "number" ? header.version : 1;
|
|
276
|
+
const threadId = header?.type === "session" ? normalizeBoundedOptionalString(header.id, 256) : void 0;
|
|
277
|
+
if (header && threadId && SESSION_ID_PATTERN.test(threadId)) {
|
|
278
|
+
const cwd = normalizeBoundedOptionalString(header.cwd, 4096);
|
|
279
|
+
const createdAt = parsePiSessionTimestampMs(header.timestamp);
|
|
280
|
+
summary = {
|
|
281
|
+
file: candidate.file,
|
|
282
|
+
version,
|
|
283
|
+
threadId,
|
|
284
|
+
...name || firstMessage ? { name: name ?? firstMessage } : {},
|
|
285
|
+
...cwd ? { cwd } : {},
|
|
286
|
+
status: "stored",
|
|
287
|
+
...createdAt !== void 0 ? { createdAt } : {},
|
|
288
|
+
updatedAt: candidate.mtimeMs,
|
|
289
|
+
recencyAt: candidate.mtimeMs,
|
|
290
|
+
source: "pi-cli",
|
|
291
|
+
modelProvider: "pi",
|
|
292
|
+
archived: false,
|
|
293
|
+
canContinue: candidate.resumable,
|
|
294
|
+
canArchive: false
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
return cached?.summary;
|
|
299
|
+
}
|
|
300
|
+
if (cached?.summary?.threadId && cached.summary.threadId !== summary?.threadId) threadFileCache.delete(threadCacheKey(cached.storeRoot, cached.summary.threadId));
|
|
301
|
+
cacheSummary(candidate.file, {
|
|
302
|
+
...candidate,
|
|
303
|
+
summary,
|
|
304
|
+
scanState,
|
|
305
|
+
appendProof
|
|
306
|
+
});
|
|
307
|
+
if (summary) threadFileCache.set(threadCacheKey(candidate.storeRoot, summary.threadId), candidate.file);
|
|
308
|
+
return summary;
|
|
309
|
+
}
|
|
310
|
+
function summaryMatches(summary, needle) {
|
|
311
|
+
if (!needle) return true;
|
|
312
|
+
return [
|
|
313
|
+
summary.threadId,
|
|
314
|
+
summary.name,
|
|
315
|
+
summary.cwd
|
|
316
|
+
].some((field) => field?.toLocaleLowerCase().includes(needle));
|
|
317
|
+
}
|
|
318
|
+
async function listPiSummaryPage(env, params) {
|
|
319
|
+
const candidates = await piFileCandidates(env);
|
|
320
|
+
const activeFiles = new Set(candidates.map((candidate) => candidate.file));
|
|
321
|
+
for (const file of summaryCache.keys()) if (!activeFiles.has(file)) forgetCachedSummary(file);
|
|
322
|
+
const target = params.offset + params.limit + 1;
|
|
323
|
+
const matches = [];
|
|
324
|
+
const needle = params.searchTerm?.toLocaleLowerCase();
|
|
325
|
+
for (let index = 0; index < candidates.length && matches.length < target; index += SUMMARY_SCAN_BATCH_SIZE) {
|
|
326
|
+
const summaries = await mapConcurrent(candidates.slice(index, index + SUMMARY_SCAN_BATCH_SIZE), IO_CONCURRENCY, readPiSessionSummary);
|
|
327
|
+
for (const summary of summaries) if (summary && summaryMatches(summary, needle)) {
|
|
328
|
+
matches.push(summary);
|
|
329
|
+
if (matches.length >= target) break;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
summaries: matches.slice(params.offset, params.offset + params.limit),
|
|
334
|
+
hasMore: matches.length > params.offset + params.limit
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
async function findPiSummary(threadId, env) {
|
|
338
|
+
const candidates = await piFileCandidates(env);
|
|
339
|
+
for (let index = 0; index < candidates.length; index += SUMMARY_SCAN_BATCH_SIZE) {
|
|
340
|
+
const match = (await mapConcurrent(candidates.slice(index, index + SUMMARY_SCAN_BATCH_SIZE), IO_CONCURRENCY, readPiSessionSummary)).find((summary) => summary?.threadId === threadId);
|
|
341
|
+
if (match) return match;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async function readPiSessionFileBaseline(threadId, env) {
|
|
345
|
+
const summary = await findPiSummary(threadId, env);
|
|
346
|
+
if (!summary?.canContinue || summary.version < 3) return;
|
|
347
|
+
try {
|
|
348
|
+
const stats = await fs$1.stat(summary.file);
|
|
349
|
+
return stats.isFile() ? {
|
|
350
|
+
filePath: summary.file,
|
|
351
|
+
offset: stats.size
|
|
352
|
+
} : void 0;
|
|
353
|
+
} catch {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async function readPiSessionById(threadId, env) {
|
|
358
|
+
const cacheKey = threadCacheKey(piSessionStore(env).root, threadId);
|
|
359
|
+
let file = threadFileCache.get(cacheKey);
|
|
360
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
361
|
+
if (!file) file = (await findPiSummary(threadId, env))?.file;
|
|
362
|
+
if (!file) throw new Error("Pi session was not found");
|
|
363
|
+
try {
|
|
364
|
+
const stats = await fs$1.stat(file);
|
|
365
|
+
if (!stats.isFile()) throw new Error("Pi session is not a file");
|
|
366
|
+
if (stats.size > MAX_SESSION_BYTES) throw new RangeError("Pi session exceeds the 32 MiB read safety limit");
|
|
367
|
+
const entries = parsePiJsonLines(await fs$1.readFile(file, "utf8"));
|
|
368
|
+
if (entries[0]?.type === "session" && entries[0].id === threadId) return entries;
|
|
369
|
+
} catch (error) {
|
|
370
|
+
if (error instanceof RangeError) throw error;
|
|
371
|
+
if (attempt > 0) throw new Error("Pi session is unavailable", { cause: error });
|
|
372
|
+
}
|
|
373
|
+
threadFileCache.delete(cacheKey);
|
|
374
|
+
file = void 0;
|
|
375
|
+
}
|
|
376
|
+
throw new Error("Pi session changed during read");
|
|
377
|
+
}
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region extensions/acpx/src/pi-session-catalog.ts
|
|
380
|
+
const MAX_SEARCH_LENGTH = 500;
|
|
381
|
+
const isExactPiSessionCursor = sessionCatalogPaging.isExactCursor;
|
|
382
|
+
function textFromContent$1(content) {
|
|
383
|
+
if (typeof content === "string") return content;
|
|
384
|
+
if (!Array.isArray(content)) return "";
|
|
385
|
+
return content.flatMap((part) => {
|
|
386
|
+
if (!isRecord(part)) return [];
|
|
387
|
+
if (part.type === "text" && typeof part.text === "string") return [part.text];
|
|
388
|
+
if (part.type === "image") {
|
|
389
|
+
const mimeType = normalizeBoundedOptionalString(part.mimeType, 128);
|
|
390
|
+
return [mimeType ? `[image: ${mimeType}]` : "[image]"];
|
|
391
|
+
}
|
|
392
|
+
return [];
|
|
393
|
+
}).join("\n");
|
|
394
|
+
}
|
|
395
|
+
const PI_PARAMETER_MESSAGES = {
|
|
396
|
+
listNotObject: "Pi session list parameters must be an object",
|
|
397
|
+
unknownListParameter: (key) => `unknown Pi session list parameter: ${key}`,
|
|
398
|
+
invalidSearchTerm: "searchTerm is invalid",
|
|
399
|
+
readNotObject: "Pi session read parameters must be an object",
|
|
400
|
+
unknownReadParameter: (key) => `unknown Pi session read parameter: ${key}`,
|
|
401
|
+
invalidThreadId: "threadId is invalid"
|
|
402
|
+
};
|
|
403
|
+
async function listLocalPiSessionPage(value) {
|
|
404
|
+
const params = sessionCatalogPaging.parseListParams(value, {
|
|
405
|
+
searchMaxLength: MAX_SEARCH_LENGTH,
|
|
406
|
+
messages: PI_PARAMETER_MESSAGES
|
|
407
|
+
});
|
|
408
|
+
const offset = sessionCatalogPaging.decodeCursor(params.cursor);
|
|
409
|
+
const { summaries, hasMore } = await listPiSummaryPage(process.env, {
|
|
410
|
+
offset,
|
|
411
|
+
limit: params.limit,
|
|
412
|
+
...params.searchTerm ? { searchTerm: params.searchTerm } : {}
|
|
413
|
+
});
|
|
414
|
+
const page = summaries.map(({ file: _file, version: _version, ...session }) => session);
|
|
415
|
+
return {
|
|
416
|
+
sessions: page,
|
|
417
|
+
...hasMore ? { nextCursor: sessionCatalogPaging.encodeCursor(offset + page.length) } : {}
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function isoTimestamp(message, entry) {
|
|
421
|
+
const value = parsePiSessionTimestampMs(message.timestamp) ?? parsePiSessionTimestampMs(entry.timestamp);
|
|
422
|
+
if (value === void 0) return;
|
|
423
|
+
const date = new Date(value);
|
|
424
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
425
|
+
}
|
|
426
|
+
function jsonText(value, maxLength = 2e4) {
|
|
427
|
+
try {
|
|
428
|
+
const text = JSON.stringify(value);
|
|
429
|
+
return text.length > maxLength ? `${truncateUtf16Safe(text, maxLength)}…` : text;
|
|
430
|
+
} catch {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function activePiEntries(entries) {
|
|
435
|
+
const header = entries[0];
|
|
436
|
+
if ((header?.type === "session" && typeof header.version === "number" ? header.version : 1) < 2) return entries.slice(1);
|
|
437
|
+
const body = entries.filter((entry) => entry.type !== "session" && normalizeBoundedOptionalString(entry.id, 256));
|
|
438
|
+
const byId = new Map(body.map((entry) => [String(entry.id), entry]));
|
|
439
|
+
const active = [];
|
|
440
|
+
let current = body.at(-1);
|
|
441
|
+
const visited = /* @__PURE__ */ new Set();
|
|
442
|
+
while (current) {
|
|
443
|
+
const id = String(current.id);
|
|
444
|
+
if (visited.has(id)) break;
|
|
445
|
+
visited.add(id);
|
|
446
|
+
active.push(current);
|
|
447
|
+
const parentId = normalizeBoundedOptionalString(current.parentId, 256);
|
|
448
|
+
current = parentId ? byId.get(parentId) : void 0;
|
|
449
|
+
}
|
|
450
|
+
return active.toReversed();
|
|
451
|
+
}
|
|
452
|
+
function piMessageItems(entry) {
|
|
453
|
+
if (!isRecord(entry.message)) return [];
|
|
454
|
+
const message = entry.message;
|
|
455
|
+
const role = message.role;
|
|
456
|
+
const id = normalizeBoundedOptionalString(entry.id, 256);
|
|
457
|
+
const timestamp = isoTimestamp(message, entry);
|
|
458
|
+
const model = normalizeBoundedOptionalString(message.model, 256);
|
|
459
|
+
const provider = normalizeBoundedOptionalString(message.provider, 256);
|
|
460
|
+
const modelRef = provider && model ? `${provider}/${model}` : model;
|
|
461
|
+
const common = {
|
|
462
|
+
...id ? { id } : {},
|
|
463
|
+
...timestamp ? { timestamp } : {},
|
|
464
|
+
...modelRef ? { model: modelRef } : {}
|
|
465
|
+
};
|
|
466
|
+
if (role === "user") {
|
|
467
|
+
const text = textFromContent$1(message.content);
|
|
468
|
+
return text ? [{
|
|
469
|
+
...common,
|
|
470
|
+
type: "userMessage",
|
|
471
|
+
text
|
|
472
|
+
}] : [];
|
|
473
|
+
}
|
|
474
|
+
if (role === "toolResult") {
|
|
475
|
+
const toolName = normalizeBoundedOptionalString(message.toolName, 256);
|
|
476
|
+
const text = textFromContent$1(message.content);
|
|
477
|
+
return [{
|
|
478
|
+
...common,
|
|
479
|
+
type: "toolResult",
|
|
480
|
+
text: toolName ? `${toolName}\n${text}` : text
|
|
481
|
+
}];
|
|
482
|
+
}
|
|
483
|
+
if (role === "bashExecution") {
|
|
484
|
+
const command = normalizeBoundedOptionalString(message.command, 4096) ?? "bash";
|
|
485
|
+
const output = typeof message.output === "string" ? message.output : "";
|
|
486
|
+
const status = message.cancelled === true ? "command cancelled" : typeof message.exitCode === "number" && message.exitCode !== 0 ? `command exited with code ${String(message.exitCode)}` : "";
|
|
487
|
+
return [{
|
|
488
|
+
...common,
|
|
489
|
+
type: "toolCall",
|
|
490
|
+
text: `bash\n${command}`
|
|
491
|
+
}, {
|
|
492
|
+
...common,
|
|
493
|
+
...id ? { id: `${id}:result` } : {},
|
|
494
|
+
type: "toolResult",
|
|
495
|
+
text: [output, status].filter(Boolean).join("\n\n")
|
|
496
|
+
}];
|
|
497
|
+
}
|
|
498
|
+
if (role === "custom" || role === "hookMessage") {
|
|
499
|
+
if (message.display !== true) return [];
|
|
500
|
+
const customType = normalizeBoundedOptionalString(message.customType, 256);
|
|
501
|
+
const text = textFromContent$1(message.content);
|
|
502
|
+
return text ? [{
|
|
503
|
+
...common,
|
|
504
|
+
type: "other",
|
|
505
|
+
text: customType ? `${customType}\n${text}` : text
|
|
506
|
+
}] : [];
|
|
507
|
+
}
|
|
508
|
+
if (role !== "assistant" || !Array.isArray(message.content)) return [];
|
|
509
|
+
return message.content.flatMap((part, index) => {
|
|
510
|
+
if (!isRecord(part)) return [];
|
|
511
|
+
const partCommon = {
|
|
512
|
+
...common,
|
|
513
|
+
...id ? { id: `${id}:${String(index)}` } : {}
|
|
514
|
+
};
|
|
515
|
+
if (part.type === "text" && typeof part.text === "string") return [{
|
|
516
|
+
...partCommon,
|
|
517
|
+
type: "agentMessage",
|
|
518
|
+
text: part.text
|
|
519
|
+
}];
|
|
520
|
+
if (part.type === "thinking" && typeof part.thinking === "string") return [{
|
|
521
|
+
...partCommon,
|
|
522
|
+
type: "reasoning",
|
|
523
|
+
text: part.thinking
|
|
524
|
+
}];
|
|
525
|
+
if (part.type === "toolCall") {
|
|
526
|
+
const name = normalizeBoundedOptionalString(part.name, 256) ?? "tool";
|
|
527
|
+
const args = jsonText(part.arguments);
|
|
528
|
+
return [{
|
|
529
|
+
...partCommon,
|
|
530
|
+
type: "toolCall",
|
|
531
|
+
text: args ? `${name}\n${args}` : name
|
|
532
|
+
}];
|
|
533
|
+
}
|
|
534
|
+
return [];
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
function piTranscriptItems(entries) {
|
|
538
|
+
return activePiEntries(entries).flatMap((entry) => {
|
|
539
|
+
if (entry.type === "message") return piMessageItems(entry);
|
|
540
|
+
const id = normalizeBoundedOptionalString(entry.id, 256);
|
|
541
|
+
const timestamp = normalizeBoundedOptionalString(entry.timestamp, 128);
|
|
542
|
+
const common = {
|
|
543
|
+
...id ? { id } : {},
|
|
544
|
+
...timestamp ? { timestamp } : {}
|
|
545
|
+
};
|
|
546
|
+
if (entry.type === "compaction" && typeof entry.summary === "string") return [{
|
|
547
|
+
...common,
|
|
548
|
+
type: "other",
|
|
549
|
+
text: entry.summary
|
|
550
|
+
}];
|
|
551
|
+
if (entry.type === "branch_summary" && typeof entry.summary === "string") return [{
|
|
552
|
+
...common,
|
|
553
|
+
type: "other",
|
|
554
|
+
text: entry.summary
|
|
555
|
+
}];
|
|
556
|
+
if (entry.type === "custom_message" && entry.display === true) {
|
|
557
|
+
const text = textFromContent$1(entry.content);
|
|
558
|
+
return text ? [{
|
|
559
|
+
...common,
|
|
560
|
+
type: "other",
|
|
561
|
+
text
|
|
562
|
+
}] : [];
|
|
563
|
+
}
|
|
564
|
+
return [];
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
async function readLocalPiTranscriptPage(value) {
|
|
568
|
+
const params = sessionCatalogPaging.parseReadParams(value, {
|
|
569
|
+
threadIdMaxLength: 256,
|
|
570
|
+
threadIdPattern: PI_SESSION_ID_PATTERN,
|
|
571
|
+
messages: PI_PARAMETER_MESSAGES
|
|
572
|
+
});
|
|
573
|
+
const offset = sessionCatalogPaging.decodeCursor(params.cursor);
|
|
574
|
+
const items = piTranscriptItems(await readPiSessionById(params.threadId, process.env));
|
|
575
|
+
const page = sessionCatalogPaging.boundTranscriptPage(items, params.limit, offset);
|
|
576
|
+
return {
|
|
577
|
+
hostId: PI_LOCAL_SESSION_HOST_ID,
|
|
578
|
+
label: "Local Pi",
|
|
579
|
+
threadId: params.threadId,
|
|
580
|
+
...page
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
//#endregion
|
|
584
|
+
//#region extensions/acpx/src/pi-session-upstream-activity.ts
|
|
585
|
+
const MAX_PI_UPSTREAM_SCAN_BYTES = 1024 * 1024;
|
|
586
|
+
async function readFileRange(handle, position, length) {
|
|
587
|
+
const buffer = Buffer.alloc(length);
|
|
588
|
+
let offset = 0;
|
|
589
|
+
while (offset < length) {
|
|
590
|
+
const { bytesRead } = await handle.read(buffer, offset, length - offset, position + offset);
|
|
591
|
+
if (bytesRead <= 0) break;
|
|
592
|
+
offset += bytesRead;
|
|
593
|
+
}
|
|
594
|
+
return offset === length ? buffer : buffer.subarray(0, offset);
|
|
595
|
+
}
|
|
596
|
+
function parseCompletePiRows(tail) {
|
|
597
|
+
const entries = [];
|
|
598
|
+
let lineStart = 0;
|
|
599
|
+
let classifiedBytes = 0;
|
|
600
|
+
for (let index = 0; index < tail.length; index += 1) {
|
|
601
|
+
if (tail[index] !== 10) continue;
|
|
602
|
+
const line = tail.subarray(lineStart, index).toString("utf8").trim();
|
|
603
|
+
if (line) try {
|
|
604
|
+
const value = JSON.parse(line);
|
|
605
|
+
if (!isRecord(value)) break;
|
|
606
|
+
entries.push(value);
|
|
607
|
+
} catch {
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
classifiedBytes = index + 1;
|
|
611
|
+
lineStart = index + 1;
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
entries,
|
|
615
|
+
classifiedBytes
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function textFromContent(content) {
|
|
619
|
+
if (typeof content === "string") return content;
|
|
620
|
+
if (!Array.isArray(content)) return;
|
|
621
|
+
return content.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n") || void 0;
|
|
622
|
+
}
|
|
623
|
+
function readFilePath(probe) {
|
|
624
|
+
return isRecord(probe.upstreamRef) && typeof probe.upstreamRef.filePath === "string" ? probe.upstreamRef.filePath : void 0;
|
|
625
|
+
}
|
|
626
|
+
function readMarkerOffset(probe) {
|
|
627
|
+
return isRecord(probe.marker) && Number.isSafeInteger(probe.marker.offset) && Number(probe.marker.offset) >= 0 ? Number(probe.marker.offset) : void 0;
|
|
628
|
+
}
|
|
629
|
+
async function linkContinuedPiSession(sessionKey, threadId) {
|
|
630
|
+
try {
|
|
631
|
+
const baseline = await readPiSessionFileBaseline(threadId, process.env);
|
|
632
|
+
return baseline ? {
|
|
633
|
+
sessionKey,
|
|
634
|
+
upstream: {
|
|
635
|
+
kind: "pi-cli",
|
|
636
|
+
ref: { filePath: baseline.filePath },
|
|
637
|
+
marker: { offset: baseline.offset }
|
|
638
|
+
}
|
|
639
|
+
} : { sessionKey };
|
|
640
|
+
} catch {
|
|
641
|
+
return { sessionKey };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
async function checkPiSessionUpstreamActivity(probe) {
|
|
645
|
+
if (probe.hostId !== "gateway" || probe.upstreamKind !== "pi-cli") return;
|
|
646
|
+
const filePath = readFilePath(probe);
|
|
647
|
+
const markerOffset = readMarkerOffset(probe);
|
|
648
|
+
if (!filePath || markerOffset === void 0) return;
|
|
649
|
+
let handle;
|
|
650
|
+
try {
|
|
651
|
+
handle = await fs$1.open(filePath, "r");
|
|
652
|
+
} catch (error) {
|
|
653
|
+
return isRecord(error) && error.code === "ENOENT" ? {
|
|
654
|
+
kind: "missing",
|
|
655
|
+
sessionKey: probe.sessionKey
|
|
656
|
+
} : void 0;
|
|
657
|
+
}
|
|
658
|
+
try {
|
|
659
|
+
const stat = await handle.stat();
|
|
660
|
+
if (!stat.isFile()) return {
|
|
661
|
+
kind: "missing",
|
|
662
|
+
sessionKey: probe.sessionKey
|
|
663
|
+
};
|
|
664
|
+
if (stat.size <= markerOffset) return;
|
|
665
|
+
const readLength = Math.min(stat.size - markerOffset, MAX_PI_UPSTREAM_SCAN_BYTES);
|
|
666
|
+
const { entries, classifiedBytes } = parseCompletePiRows(await readFileRange(handle, markerOffset, readLength));
|
|
667
|
+
if (classifiedBytes === 0) return;
|
|
668
|
+
let humanTurns = 0;
|
|
669
|
+
let occurredAt;
|
|
670
|
+
for (const entry of entries) {
|
|
671
|
+
if (entry.type !== "message" || !isRecord(entry.message) || entry.message.role !== "user") continue;
|
|
672
|
+
if (!isExternalUserText(probe, textFromContent(entry.message.content))) continue;
|
|
673
|
+
humanTurns += 1;
|
|
674
|
+
occurredAt = Math.max(occurredAt ?? 0, parsePiSessionTimestampMs(entry.message.timestamp) ?? parsePiSessionTimestampMs(entry.timestamp) ?? stat.mtimeMs);
|
|
675
|
+
}
|
|
676
|
+
const nextOffset = markerOffset + classifiedBytes;
|
|
677
|
+
return {
|
|
678
|
+
kind: "activity",
|
|
679
|
+
sessionKey: probe.sessionKey,
|
|
680
|
+
humanTurns,
|
|
681
|
+
nextMarker: { offset: nextOffset },
|
|
682
|
+
...humanTurns > 0 ? {
|
|
683
|
+
occurredAt: occurredAt ?? stat.mtimeMs,
|
|
684
|
+
dedupeId: String(nextOffset)
|
|
685
|
+
} : {}
|
|
686
|
+
};
|
|
687
|
+
} finally {
|
|
688
|
+
await handle.close();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
async function checkPiUpstreamActivity(probes) {
|
|
692
|
+
const outcomes = [];
|
|
693
|
+
for (const probe of probes) try {
|
|
694
|
+
const outcome = await checkPiSessionUpstreamActivity(probe);
|
|
695
|
+
if (outcome) outcomes.push(outcome);
|
|
696
|
+
} catch {}
|
|
697
|
+
return outcomes;
|
|
698
|
+
}
|
|
699
|
+
//#endregion
|
|
700
|
+
//#region extensions/acpx/src/pi-session-catalog-runtime.ts
|
|
701
|
+
const NODE_TIMEOUT_MS = 2e4;
|
|
702
|
+
const ACPX_BACKEND_ID = "acpx";
|
|
703
|
+
const PI_ACP_AGENT_ID = "pi";
|
|
704
|
+
const PI_ADOPTED_SESSION_KEY_PREFIX = "plugin:acpx:catalog-adopt:pi:";
|
|
705
|
+
async function requireLocalPiSession(threadId) {
|
|
706
|
+
const session = (await listLocalPiSessionPage({
|
|
707
|
+
searchTerm: threadId,
|
|
708
|
+
limit: 100
|
|
709
|
+
})).sessions.find((candidate) => candidate.threadId === threadId);
|
|
710
|
+
if (!session) throw new Error("Pi session is unavailable");
|
|
711
|
+
return session;
|
|
712
|
+
}
|
|
713
|
+
function currentPiCatalogConfig(api) {
|
|
714
|
+
return api.runtime.config?.current?.() ?? api.config ?? {};
|
|
715
|
+
}
|
|
716
|
+
function resolvePiContinuationAvailability(api) {
|
|
717
|
+
const availability = resolveAcpSessionAvailability({
|
|
718
|
+
config: currentPiCatalogConfig(api),
|
|
719
|
+
backendId: ACPX_BACKEND_ID,
|
|
720
|
+
agentId: PI_ACP_AGENT_ID
|
|
721
|
+
});
|
|
722
|
+
if (!availability.available) return availability;
|
|
723
|
+
return resolveNodeHostExecutable("pi", {
|
|
724
|
+
env: process.env,
|
|
725
|
+
pathEnv: process.env.PATH ?? "",
|
|
726
|
+
strategy: "fallback"
|
|
727
|
+
}) ? { available: true } : {
|
|
728
|
+
available: false,
|
|
729
|
+
message: "Pi CLI is unavailable"
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
function listAdoptedPiSessions(api, agentId, sessionEntries) {
|
|
733
|
+
return listAdoptedSessionCatalogSessions({
|
|
734
|
+
...agentId ? { agentId } : {},
|
|
735
|
+
config: currentPiCatalogConfig(api),
|
|
736
|
+
pluginId: api.id,
|
|
737
|
+
runtime: api.runtime,
|
|
738
|
+
sessionEntries,
|
|
739
|
+
sourceFromEntry: (entry) => {
|
|
740
|
+
const acpx = isRecord(entry.pluginExtensions?.acpx) ? entry.pluginExtensions.acpx : void 0;
|
|
741
|
+
const marker = acpx && isRecord(acpx.piSessionCatalog) ? acpx.piSessionCatalog : void 0;
|
|
742
|
+
return marker && typeof marker.sourceThreadId === "string" ? {
|
|
743
|
+
hostId: PI_LOCAL_SESSION_HOST_ID,
|
|
744
|
+
threadId: marker.sourceThreadId
|
|
745
|
+
} : void 0;
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
async function createAdoptedPiSession(params) {
|
|
750
|
+
const config = currentPiCatalogConfig(params.api);
|
|
751
|
+
const marker = { sourceThreadId: params.threadId };
|
|
752
|
+
return { sessionKey: (await params.api.runtime.agent.session.createSessionEntry({
|
|
753
|
+
cfg: config,
|
|
754
|
+
key: sessionCatalogAdoptedSessionKey(PI_ADOPTED_SESSION_KEY_PREFIX, params.threadId),
|
|
755
|
+
agentId: params.agentId,
|
|
756
|
+
recoverMatchingInitialEntry: true,
|
|
757
|
+
...params.session.name ? { label: params.session.name } : {},
|
|
758
|
+
...params.session.cwd ? { spawnedCwd: params.session.cwd } : {},
|
|
759
|
+
initialEntry: {
|
|
760
|
+
acpBackendId: ACPX_BACKEND_ID,
|
|
761
|
+
acpSessionBinding: {
|
|
762
|
+
acpAgentId: PI_ACP_AGENT_ID,
|
|
763
|
+
agentSessionId: params.threadId
|
|
764
|
+
},
|
|
765
|
+
pluginExtensions: { acpx: { piSessionCatalog: marker } }
|
|
766
|
+
},
|
|
767
|
+
afterCreate: async (entry) => {
|
|
768
|
+
await importSessionCatalogHistory({
|
|
769
|
+
catalogId: "pi",
|
|
770
|
+
threadId: params.threadId,
|
|
771
|
+
read: async ({ cursor, limit }) => await readLocalPiTranscriptPage({
|
|
772
|
+
threadId: params.threadId,
|
|
773
|
+
limit,
|
|
774
|
+
...cursor ? { cursor } : {}
|
|
775
|
+
}),
|
|
776
|
+
sessionId: entry.sessionId,
|
|
777
|
+
sessionKey: entry.key,
|
|
778
|
+
agentId: entry.agentId,
|
|
779
|
+
...params.session.cwd ? { cwd: params.session.cwd } : {},
|
|
780
|
+
config
|
|
781
|
+
});
|
|
782
|
+
return { pluginExtensions: { acpx: { piSessionCatalog: marker } } };
|
|
783
|
+
}
|
|
784
|
+
})).key };
|
|
785
|
+
}
|
|
786
|
+
function assertPiLocalAccess(hostId, allowProcessHomeFallback) {
|
|
787
|
+
if (hostId === "gateway" && allowProcessHomeFallback === false && piSessionStore(process.env).usesProcessHomeFallback) throw new Error("local Pi sessions are unavailable in isolated state");
|
|
788
|
+
}
|
|
789
|
+
async function listPiSessions(params) {
|
|
790
|
+
return await listLocalPiSessionPage(params);
|
|
791
|
+
}
|
|
792
|
+
async function readPiSession(params) {
|
|
793
|
+
return await readLocalPiTranscriptPage(params);
|
|
794
|
+
}
|
|
795
|
+
function createPiSessionCatalogRuntime(api) {
|
|
796
|
+
return createSessionCatalogFamily({
|
|
797
|
+
runtime: api.runtime,
|
|
798
|
+
local: {
|
|
799
|
+
hostId: PI_LOCAL_SESSION_HOST_ID,
|
|
800
|
+
label: "Local Pi",
|
|
801
|
+
available: (query) => {
|
|
802
|
+
const store = piSessionStore(process.env);
|
|
803
|
+
return (query.allowProcessHomeFallback !== false || !store.usesProcessHomeFallback) && piSessionStoreAvailable(process.env, store);
|
|
804
|
+
},
|
|
805
|
+
list: async (query) => await listLocalPiSessionPage({
|
|
806
|
+
limit: query.limitPerHost,
|
|
807
|
+
...query.search ? { searchTerm: query.search } : {},
|
|
808
|
+
cursor: query.cursors?.[PI_LOCAL_SESSION_HOST_ID]
|
|
809
|
+
}),
|
|
810
|
+
read: async (request) => await readLocalPiTranscriptPage({
|
|
811
|
+
threadId: request.threadId,
|
|
812
|
+
...request.limit ? { limit: request.limit } : {},
|
|
813
|
+
...request.cursor !== void 0 ? { cursor: request.cursor } : {}
|
|
814
|
+
}),
|
|
815
|
+
assertAccess: assertPiLocalAccess
|
|
816
|
+
},
|
|
817
|
+
node: {
|
|
818
|
+
listCommand: PI_SESSIONS_LIST_COMMAND,
|
|
819
|
+
readCommand: PI_SESSION_READ_COMMAND,
|
|
820
|
+
terminalCommand: PI_TERMINAL_RESUME_COMMAND,
|
|
821
|
+
timeoutMs: NODE_TIMEOUT_MS,
|
|
822
|
+
maxHosts: 100,
|
|
823
|
+
maxPageLimit: 100,
|
|
824
|
+
sessionIdPattern: PI_SESSION_ID_PATTERN
|
|
825
|
+
},
|
|
826
|
+
capabilities: {
|
|
827
|
+
local: () => ({
|
|
828
|
+
canContinue: resolvePiContinuationAvailability(api).available,
|
|
829
|
+
canOpenTerminal: resolveNodeHostExecutable("pi", {
|
|
830
|
+
env: process.env,
|
|
831
|
+
pathEnv: process.env.PATH ?? "",
|
|
832
|
+
strategy: "fallback"
|
|
833
|
+
}) !== void 0
|
|
834
|
+
}),
|
|
835
|
+
node: (node) => {
|
|
836
|
+
return {
|
|
837
|
+
canContinue: false,
|
|
838
|
+
canOpenTerminal: (node.invocableCommands ?? node.commands)?.includes(PI_TERMINAL_RESUME_COMMAND) === true
|
|
839
|
+
};
|
|
840
|
+
},
|
|
841
|
+
project: (session, capabilities) => ({
|
|
842
|
+
...session,
|
|
843
|
+
canContinue: capabilities.canContinue && session.canContinue,
|
|
844
|
+
canOpenTerminal: capabilities.canOpenTerminal
|
|
845
|
+
})
|
|
846
|
+
},
|
|
847
|
+
messages: {
|
|
848
|
+
invalidNodeCursor: "Pi node returned an invalid cursor",
|
|
849
|
+
invalidNodeSessionPage: "Pi node returned an invalid session page",
|
|
850
|
+
invalidNodeTranscriptPage: "Pi node returned an invalid transcript page",
|
|
851
|
+
invalidHostId: "Pi session catalog hostId is invalid",
|
|
852
|
+
localReadFailed: "Local Pi sessions are unavailable",
|
|
853
|
+
nodeInvokeFailed: "Paired node Pi sessions are unavailable",
|
|
854
|
+
nodeReadUnavailable: "paired-node Pi session host is unavailable",
|
|
855
|
+
nodeTerminalUnavailable: "paired-node Pi terminal is unavailable",
|
|
856
|
+
sessionUnavailable: "Pi session is unavailable"
|
|
857
|
+
},
|
|
858
|
+
continuation: {
|
|
859
|
+
resolveAgentId: (agentId) => resolveSessionAgentIds({
|
|
860
|
+
config: api.config,
|
|
861
|
+
agentId
|
|
862
|
+
}).sessionAgentId,
|
|
863
|
+
availability: () => resolvePiContinuationAvailability(api),
|
|
864
|
+
listAdopted: (agentId, sessionEntries) => listAdoptedPiSessions(api, agentId, sessionEntries),
|
|
865
|
+
loadSession: requireLocalPiSession,
|
|
866
|
+
validateSession: (session) => {
|
|
867
|
+
if (!session.canContinue) throw new Error("Pi session is outside the session store supported by pi-acp");
|
|
868
|
+
},
|
|
869
|
+
create: async (params) => await createAdoptedPiSession({
|
|
870
|
+
api,
|
|
871
|
+
...params
|
|
872
|
+
}),
|
|
873
|
+
complete: async (continued, threadId) => await linkContinuedPiSession(continued.sessionKey, threadId),
|
|
874
|
+
nodeReadOnlyMessage: "paired-node Pi session rows are view-only"
|
|
875
|
+
},
|
|
876
|
+
terminal: {
|
|
877
|
+
executable: "pi",
|
|
878
|
+
args: (threadId) => ["--session", threadId],
|
|
879
|
+
title: (threadId) => `pi --session ${threadId.slice(0, 12)}…`,
|
|
880
|
+
requireLocalSession: requireLocalPiSession,
|
|
881
|
+
unavailableMessage: "Pi CLI is unavailable"
|
|
882
|
+
},
|
|
883
|
+
checkUpstreamActivity: (probes, policy) => checkPiUpstreamActivity(probes.filter((probe) => probe.hostId !== "gateway" || policy?.allowProcessHomeFallback !== false || !piSessionStore(process.env).usesProcessHomeFallback))
|
|
884
|
+
}, isExactPiSessionCursor);
|
|
885
|
+
}
|
|
886
|
+
//#endregion
|
|
887
|
+
export { createPiSessionCatalogRuntime, listPiSessions, readPiSession, requireLocalPiSession };
|