@openclaw/acpx 2026.7.2-beta.7 → 2026.8.1-beta.2
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 +21 -1279
- package/dist/pi-session-catalog-runtime-BiTQtT7s.js +1220 -0
- package/dist/pi-session-paths-DgYt8LUg.js +81 -0
- package/dist/{process-lease-DSLDgiNl.js → process-lease-Cwvj7WGe.js} +5 -2
- package/dist/{process-reaper-DFwbcdPa.js → process-reaper-DzVuCxl3.js} +63 -13
- package/dist/{register.runtime-BbS2JTTv.js → register.runtime-C29AY8LI.js} +75 -30
- package/dist/register.runtime.js +1 -1
- package/dist/{runtime-By_lR8uk.js → runtime-BmzHUTK8.js} +86 -67
- package/dist/{service-CBZSAymH.js → service-LTeZyc7q.js} +38 -45
- package/openclaw.plugin.json +5 -17
- package/package.json +4 -4
|
@@ -0,0 +1,1220 @@
|
|
|
1
|
+
import { a as PI_SESSION_READ_COMMAND, i as PI_SESSIONS_LIST_COMMAND, n as piSessionStore, o as PI_TERMINAL_RESUME_COMMAND, r as piSessionStoreAvailable, t as piAcpSessionStoreRoot } from "./pi-session-paths-DgYt8LUg.js";
|
|
2
|
+
import { parseDateFirstTimestampMs } from "openclaw/plugin-sdk/number-runtime";
|
|
3
|
+
import { decodeNodePtyResumeParams, resolveNodeHostExecutable, runNodePtyCommand } from "openclaw/plugin-sdk/node-host";
|
|
4
|
+
import { isRecord, normalizeBoundedOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
5
|
+
import { createReadStream } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import fs$1 from "node:fs/promises";
|
|
8
|
+
import process from "node:process";
|
|
9
|
+
import { resolveAcpSessionAvailability } from "openclaw/plugin-sdk/acp-runtime";
|
|
10
|
+
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
|
|
11
|
+
import { createSessionCatalogAdoptionCoordinator, importSessionCatalogHistory, isExternalUserText, listAdoptedSessionCatalogSessions, sessionCatalogAdoptedSessionKey, sessionCatalogAdoptedSourceKey } from "openclaw/plugin-sdk/session-catalog";
|
|
12
|
+
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
13
|
+
//#region extensions/acpx/src/pi-session-timestamp.ts
|
|
14
|
+
/** Preserve Pi JSONL's date-first string contract while accepting numeric millisecond values. */
|
|
15
|
+
function parsePiSessionTimestampMs(value) {
|
|
16
|
+
return parseDateFirstTimestampMs(value);
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region extensions/acpx/src/pi-session-store.ts
|
|
20
|
+
const MAX_DISCOVERY_FILES = 1e4;
|
|
21
|
+
const SUMMARY_SCAN_BATCH_SIZE = 100;
|
|
22
|
+
const MAX_SUMMARY_CACHE_ENTRIES = 256;
|
|
23
|
+
const MAX_SESSION_BYTES = 32 * 1024 * 1024;
|
|
24
|
+
const MAX_SUMMARY_LINE_BYTES = 1024 * 1024;
|
|
25
|
+
const APPEND_PROOF_EDGE_BYTES = 64 * 1024;
|
|
26
|
+
const IO_CONCURRENCY = 8;
|
|
27
|
+
const PI_FILE_CANDIDATE_CACHE_TTL_MS = 32e3;
|
|
28
|
+
const PI_FILE_CANDIDATE_CACHE_MAX_ENTRIES = 8;
|
|
29
|
+
const SESSION_ID_PATTERN$2 = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
|
|
30
|
+
const summaryCache = /* @__PURE__ */ new Map();
|
|
31
|
+
const threadFileCache = /* @__PURE__ */ new Map();
|
|
32
|
+
const piFileCandidateCache = /* @__PURE__ */ new Map();
|
|
33
|
+
function threadCacheKey(storeRoot, threadId) {
|
|
34
|
+
return `${storeRoot}\0${threadId}`;
|
|
35
|
+
}
|
|
36
|
+
function forgetCachedSummary(file) {
|
|
37
|
+
const cached = summaryCache.get(file);
|
|
38
|
+
const threadId = cached?.summary?.threadId;
|
|
39
|
+
if (cached && threadId) {
|
|
40
|
+
const key = threadCacheKey(cached.storeRoot, threadId);
|
|
41
|
+
if (threadFileCache.get(key) === file) threadFileCache.delete(key);
|
|
42
|
+
}
|
|
43
|
+
summaryCache.delete(file);
|
|
44
|
+
}
|
|
45
|
+
function cacheSummary(file, value) {
|
|
46
|
+
forgetCachedSummary(file);
|
|
47
|
+
summaryCache.set(file, value);
|
|
48
|
+
while (summaryCache.size > MAX_SUMMARY_CACHE_ENTRIES) {
|
|
49
|
+
const oldest = summaryCache.keys().next().value;
|
|
50
|
+
if (typeof oldest !== "string") break;
|
|
51
|
+
forgetCachedSummary(oldest);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function discoverPiSessionFiles(env) {
|
|
55
|
+
const store = piSessionStore(env);
|
|
56
|
+
const resolvedRoot = await realpathOrResolve(store.root);
|
|
57
|
+
let entries;
|
|
58
|
+
try {
|
|
59
|
+
entries = await fs$1.readdir(resolvedRoot, { withFileTypes: true });
|
|
60
|
+
} catch {
|
|
61
|
+
return {
|
|
62
|
+
root: store.root,
|
|
63
|
+
files: []
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (store.flat) return {
|
|
67
|
+
root: store.root,
|
|
68
|
+
files: entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).slice(0, MAX_DISCOVERY_FILES).map((entry) => path.join(resolvedRoot, entry.name))
|
|
69
|
+
};
|
|
70
|
+
const files = [];
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (!entry.isDirectory() || files.length >= MAX_DISCOVERY_FILES) continue;
|
|
73
|
+
const directory = path.join(resolvedRoot, entry.name);
|
|
74
|
+
let children;
|
|
75
|
+
try {
|
|
76
|
+
children = await fs$1.readdir(directory, { withFileTypes: true });
|
|
77
|
+
} catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
for (const child of children) if (child.isFile() && child.name.endsWith(".jsonl")) {
|
|
81
|
+
files.push(path.join(directory, child.name));
|
|
82
|
+
if (files.length >= MAX_DISCOVERY_FILES) break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
root: store.root,
|
|
87
|
+
files
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function realpathOrResolve(value) {
|
|
91
|
+
try {
|
|
92
|
+
return await fs$1.realpath(value);
|
|
93
|
+
} catch {
|
|
94
|
+
return path.resolve(value);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function mapConcurrent(values, limit, mapper) {
|
|
98
|
+
const results = [];
|
|
99
|
+
results.length = values.length;
|
|
100
|
+
let nextIndex = 0;
|
|
101
|
+
const workers = Array.from({ length: Math.min(limit, values.length) }, async () => {
|
|
102
|
+
while (nextIndex < values.length) {
|
|
103
|
+
const index = nextIndex++;
|
|
104
|
+
results[index] = await mapper(values[index]);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
await Promise.all(workers);
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
async function scanPiFileCandidates(env) {
|
|
111
|
+
const { root, files } = await discoverPiSessionFiles(env);
|
|
112
|
+
const configuredAcpRoot = piAcpSessionStoreRoot(env);
|
|
113
|
+
const acpRoot = configuredAcpRoot ? await realpathOrResolve(configuredAcpRoot) : void 0;
|
|
114
|
+
return (await mapConcurrent(files, IO_CONCURRENCY, async (file) => {
|
|
115
|
+
try {
|
|
116
|
+
const stats = await fs$1.stat(file);
|
|
117
|
+
return stats.isFile() ? {
|
|
118
|
+
file,
|
|
119
|
+
storeRoot: root,
|
|
120
|
+
identity: `${String(stats.dev)}:${String(stats.ino)}:${String(stats.birthtimeMs)}`,
|
|
121
|
+
mtimeMs: stats.mtimeMs,
|
|
122
|
+
size: stats.size,
|
|
123
|
+
resumable: acpRoot ? pathIsWithin(acpRoot, file) : false
|
|
124
|
+
} : void 0;
|
|
125
|
+
} catch {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
})).filter((candidate) => candidate !== void 0).toSorted((left, right) => right.mtimeMs - left.mtimeMs);
|
|
129
|
+
}
|
|
130
|
+
async function piFileCandidates(env) {
|
|
131
|
+
const store = piSessionStore(env);
|
|
132
|
+
const key = `${store.root}\0${store.flat}\0${piAcpSessionStoreRoot(env) ?? ""}`;
|
|
133
|
+
const cached = piFileCandidateCache.get(key);
|
|
134
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
135
|
+
piFileCandidateCache.delete(key);
|
|
136
|
+
piFileCandidateCache.set(key, cached);
|
|
137
|
+
return await cached.candidates;
|
|
138
|
+
}
|
|
139
|
+
if (cached) piFileCandidateCache.delete(key);
|
|
140
|
+
const candidates = scanPiFileCandidates(env);
|
|
141
|
+
const entry = {
|
|
142
|
+
expiresAt: Date.now() + PI_FILE_CANDIDATE_CACHE_TTL_MS,
|
|
143
|
+
candidates
|
|
144
|
+
};
|
|
145
|
+
piFileCandidateCache.set(key, entry);
|
|
146
|
+
while (piFileCandidateCache.size > PI_FILE_CANDIDATE_CACHE_MAX_ENTRIES) {
|
|
147
|
+
const oldest = piFileCandidateCache.keys().next();
|
|
148
|
+
if (oldest.done) break;
|
|
149
|
+
piFileCandidateCache.delete(oldest.value);
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
return await candidates;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (piFileCandidateCache.get(key) === entry) piFileCandidateCache.delete(key);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function pathIsWithin(root, candidate) {
|
|
159
|
+
const relative = path.relative(root, candidate);
|
|
160
|
+
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
161
|
+
}
|
|
162
|
+
function parsePiJsonLines(content) {
|
|
163
|
+
return content.split(/\r?\n/u).flatMap((line) => {
|
|
164
|
+
if (!line.trim()) return [];
|
|
165
|
+
try {
|
|
166
|
+
const value = JSON.parse(line);
|
|
167
|
+
return isRecord(value) ? [value] : [];
|
|
168
|
+
} catch {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function textFromContent$2(content) {
|
|
174
|
+
if (typeof content === "string") return content;
|
|
175
|
+
if (!Array.isArray(content)) return "";
|
|
176
|
+
return content.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n");
|
|
177
|
+
}
|
|
178
|
+
function processSummaryLine(state, line) {
|
|
179
|
+
const entry = parsePiJsonLines((line.at(-1) === 13 ? line.subarray(0, -1) : line).toString("utf8"))[0];
|
|
180
|
+
if (!entry) return;
|
|
181
|
+
if (!state.header) {
|
|
182
|
+
if (entry.type !== "session") {
|
|
183
|
+
state.invalid = true;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
state.header = entry;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (entry.type === "session_info") state.name = normalizeBoundedOptionalString(entry.name, 1e3);
|
|
190
|
+
else if (!state.firstMessage && entry.type === "message" && isRecord(entry.message) && entry.message.role === "user") state.firstMessage = normalizeBoundedOptionalString(textFromContent$2(entry.message.content), 1e3);
|
|
191
|
+
}
|
|
192
|
+
function appendSummaryBytes(state, bytes) {
|
|
193
|
+
if (state.discarding || bytes.length === 0) return;
|
|
194
|
+
if (state.pending.length + bytes.length > MAX_SUMMARY_LINE_BYTES) {
|
|
195
|
+
state.pending = Buffer.alloc(0);
|
|
196
|
+
state.discarding = true;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
state.pending = state.pending.length === 0 ? Buffer.from(bytes) : Buffer.concat([state.pending, bytes]);
|
|
200
|
+
}
|
|
201
|
+
async function scanSummaryAppend(candidate, start, state) {
|
|
202
|
+
if (start >= candidate.size || state.invalid) return;
|
|
203
|
+
const stream = createReadStream(candidate.file, {
|
|
204
|
+
start,
|
|
205
|
+
end: candidate.size - 1
|
|
206
|
+
});
|
|
207
|
+
for await (const value of stream) {
|
|
208
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
209
|
+
let offset = 0;
|
|
210
|
+
while (offset < chunk.length) {
|
|
211
|
+
const newline = chunk.indexOf(10, offset);
|
|
212
|
+
const end = newline < 0 ? chunk.length : newline;
|
|
213
|
+
appendSummaryBytes(state, chunk.subarray(offset, end));
|
|
214
|
+
if (newline < 0) break;
|
|
215
|
+
if (!state.discarding) processSummaryLine(state, state.pending);
|
|
216
|
+
state.pending = Buffer.alloc(0);
|
|
217
|
+
state.discarding = false;
|
|
218
|
+
if (state.invalid) return;
|
|
219
|
+
offset = newline + 1;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function readAppendProof(file, size) {
|
|
224
|
+
const length = Math.min(size, APPEND_PROOF_EDGE_BYTES);
|
|
225
|
+
if (length === 0) return {
|
|
226
|
+
head: Buffer.alloc(0),
|
|
227
|
+
tail: Buffer.alloc(0)
|
|
228
|
+
};
|
|
229
|
+
const handle = await fs$1.open(file, "r");
|
|
230
|
+
try {
|
|
231
|
+
const head = Buffer.alloc(length);
|
|
232
|
+
const tail = Buffer.alloc(length);
|
|
233
|
+
const [headRead, tailRead] = await Promise.all([handle.read(head, 0, length, 0), handle.read(tail, 0, length, size - length)]);
|
|
234
|
+
return {
|
|
235
|
+
head: head.subarray(0, headRead.bytesRead),
|
|
236
|
+
tail: tail.subarray(0, tailRead.bytesRead)
|
|
237
|
+
};
|
|
238
|
+
} finally {
|
|
239
|
+
await handle.close();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function cachedPrefixIsUnchanged(candidate, cached) {
|
|
243
|
+
if (cached.identity !== candidate.identity || cached.size >= candidate.size) return false;
|
|
244
|
+
const current = await readAppendProof(candidate.file, cached.size);
|
|
245
|
+
return current.head.equals(cached.appendProof.head) && current.tail.equals(cached.appendProof.tail);
|
|
246
|
+
}
|
|
247
|
+
async function readPiSessionSummary(candidate) {
|
|
248
|
+
const cached = summaryCache.get(candidate.file);
|
|
249
|
+
if (cached?.mtimeMs === candidate.mtimeMs && cached.size === candidate.size) {
|
|
250
|
+
summaryCache.delete(candidate.file);
|
|
251
|
+
summaryCache.set(candidate.file, cached);
|
|
252
|
+
return cached.summary ? {
|
|
253
|
+
...cached.summary,
|
|
254
|
+
canContinue: candidate.resumable
|
|
255
|
+
} : cached.summary;
|
|
256
|
+
}
|
|
257
|
+
let summary;
|
|
258
|
+
let scanState;
|
|
259
|
+
let appendProof;
|
|
260
|
+
try {
|
|
261
|
+
const resumable = cached && await cachedPrefixIsUnchanged(candidate, cached) ? cached : void 0;
|
|
262
|
+
scanState = resumable ? {
|
|
263
|
+
...resumable.scanState,
|
|
264
|
+
pending: Buffer.from(resumable.scanState.pending)
|
|
265
|
+
} : {
|
|
266
|
+
pending: Buffer.alloc(0),
|
|
267
|
+
discarding: false,
|
|
268
|
+
invalid: false
|
|
269
|
+
};
|
|
270
|
+
await scanSummaryAppend(candidate, resumable?.size ?? 0, scanState);
|
|
271
|
+
appendProof = await readAppendProof(candidate.file, candidate.size);
|
|
272
|
+
const projectedState = {
|
|
273
|
+
...scanState,
|
|
274
|
+
pending: Buffer.from(scanState.pending)
|
|
275
|
+
};
|
|
276
|
+
if (!projectedState.discarding && projectedState.pending.length > 0) processSummaryLine(projectedState, projectedState.pending);
|
|
277
|
+
const { header, name, firstMessage } = projectedState;
|
|
278
|
+
const version = header?.type === "session" && typeof header.version === "number" ? header.version : 1;
|
|
279
|
+
const threadId = header?.type === "session" ? normalizeBoundedOptionalString(header.id, 256) : void 0;
|
|
280
|
+
if (header && threadId && SESSION_ID_PATTERN$2.test(threadId)) {
|
|
281
|
+
const cwd = normalizeBoundedOptionalString(header.cwd, 4096);
|
|
282
|
+
const createdAt = parsePiSessionTimestampMs(header.timestamp);
|
|
283
|
+
summary = {
|
|
284
|
+
file: candidate.file,
|
|
285
|
+
version,
|
|
286
|
+
threadId,
|
|
287
|
+
...name || firstMessage ? { name: name ?? firstMessage } : {},
|
|
288
|
+
...cwd ? { cwd } : {},
|
|
289
|
+
status: "stored",
|
|
290
|
+
...createdAt !== void 0 ? { createdAt } : {},
|
|
291
|
+
updatedAt: candidate.mtimeMs,
|
|
292
|
+
recencyAt: candidate.mtimeMs,
|
|
293
|
+
source: "pi-cli",
|
|
294
|
+
modelProvider: "pi",
|
|
295
|
+
archived: false,
|
|
296
|
+
canContinue: candidate.resumable,
|
|
297
|
+
canArchive: false
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
return cached?.summary;
|
|
302
|
+
}
|
|
303
|
+
if (cached?.summary?.threadId && cached.summary.threadId !== summary?.threadId) threadFileCache.delete(threadCacheKey(cached.storeRoot, cached.summary.threadId));
|
|
304
|
+
cacheSummary(candidate.file, {
|
|
305
|
+
...candidate,
|
|
306
|
+
summary,
|
|
307
|
+
scanState,
|
|
308
|
+
appendProof
|
|
309
|
+
});
|
|
310
|
+
if (summary) threadFileCache.set(threadCacheKey(candidate.storeRoot, summary.threadId), candidate.file);
|
|
311
|
+
return summary;
|
|
312
|
+
}
|
|
313
|
+
function summaryMatches(summary, needle) {
|
|
314
|
+
if (!needle) return true;
|
|
315
|
+
return [
|
|
316
|
+
summary.threadId,
|
|
317
|
+
summary.name,
|
|
318
|
+
summary.cwd
|
|
319
|
+
].some((field) => field?.toLocaleLowerCase().includes(needle));
|
|
320
|
+
}
|
|
321
|
+
async function listPiSummaryPage(env, params) {
|
|
322
|
+
const candidates = await piFileCandidates(env);
|
|
323
|
+
const activeFiles = new Set(candidates.map((candidate) => candidate.file));
|
|
324
|
+
for (const file of summaryCache.keys()) if (!activeFiles.has(file)) forgetCachedSummary(file);
|
|
325
|
+
const target = params.offset + params.limit + 1;
|
|
326
|
+
const matches = [];
|
|
327
|
+
const needle = params.searchTerm?.toLocaleLowerCase();
|
|
328
|
+
for (let index = 0; index < candidates.length && matches.length < target; index += SUMMARY_SCAN_BATCH_SIZE) {
|
|
329
|
+
const summaries = await mapConcurrent(candidates.slice(index, index + SUMMARY_SCAN_BATCH_SIZE), IO_CONCURRENCY, readPiSessionSummary);
|
|
330
|
+
for (const summary of summaries) if (summary && summaryMatches(summary, needle)) {
|
|
331
|
+
matches.push(summary);
|
|
332
|
+
if (matches.length >= target) break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
summaries: matches.slice(params.offset, params.offset + params.limit),
|
|
337
|
+
hasMore: matches.length > params.offset + params.limit
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
async function findPiSummary(threadId, env) {
|
|
341
|
+
const candidates = await piFileCandidates(env);
|
|
342
|
+
for (let index = 0; index < candidates.length; index += SUMMARY_SCAN_BATCH_SIZE) {
|
|
343
|
+
const match = (await mapConcurrent(candidates.slice(index, index + SUMMARY_SCAN_BATCH_SIZE), IO_CONCURRENCY, readPiSessionSummary)).find((summary) => summary?.threadId === threadId);
|
|
344
|
+
if (match) return match;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async function readPiSessionFileBaseline(threadId, env) {
|
|
348
|
+
const summary = await findPiSummary(threadId, env);
|
|
349
|
+
if (!summary?.canContinue || summary.version < 3) return;
|
|
350
|
+
try {
|
|
351
|
+
const stats = await fs$1.stat(summary.file);
|
|
352
|
+
return stats.isFile() ? {
|
|
353
|
+
filePath: summary.file,
|
|
354
|
+
offset: stats.size
|
|
355
|
+
} : void 0;
|
|
356
|
+
} catch {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async function readPiSessionById(threadId, env) {
|
|
361
|
+
const cacheKey = threadCacheKey(piSessionStore(env).root, threadId);
|
|
362
|
+
let file = threadFileCache.get(cacheKey);
|
|
363
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
364
|
+
if (!file) file = (await findPiSummary(threadId, env))?.file;
|
|
365
|
+
if (!file) throw new Error("Pi session was not found");
|
|
366
|
+
try {
|
|
367
|
+
const stats = await fs$1.stat(file);
|
|
368
|
+
if (!stats.isFile()) throw new Error("Pi session is not a file");
|
|
369
|
+
if (stats.size > MAX_SESSION_BYTES) throw new RangeError("Pi session exceeds the 32 MiB read safety limit");
|
|
370
|
+
const entries = parsePiJsonLines(await fs$1.readFile(file, "utf8"));
|
|
371
|
+
if (entries[0]?.type === "session" && entries[0].id === threadId) return entries;
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (error instanceof RangeError) throw error;
|
|
374
|
+
if (attempt > 0) throw new Error("Pi session is unavailable", { cause: error });
|
|
375
|
+
}
|
|
376
|
+
threadFileCache.delete(cacheKey);
|
|
377
|
+
file = void 0;
|
|
378
|
+
}
|
|
379
|
+
throw new Error("Pi session changed during read");
|
|
380
|
+
}
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region extensions/acpx/src/pi-session-catalog.ts
|
|
383
|
+
const LOCAL_HOST_ID$1 = "gateway";
|
|
384
|
+
const DEFAULT_PAGE_LIMIT = 20;
|
|
385
|
+
const MAX_PAGE_LIMIT$1 = 100;
|
|
386
|
+
const MAX_SEARCH_LENGTH = 500;
|
|
387
|
+
const MAX_CURSOR_LENGTH = 128;
|
|
388
|
+
const MAX_TRANSCRIPT_ITEM_BYTES = 512 * 1024;
|
|
389
|
+
const MAX_TRANSCRIPT_PAGE_BYTES = 20 * 1024 * 1024;
|
|
390
|
+
const SESSION_ID_PATTERN$1 = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
|
|
391
|
+
function boundedLimit(value, fallback = DEFAULT_PAGE_LIMIT) {
|
|
392
|
+
if (value === void 0) return fallback;
|
|
393
|
+
if (!Number.isInteger(value) || Number(value) < 1 || Number(value) > MAX_PAGE_LIMIT$1) throw new Error(`limit must be an integer between 1 and ${String(MAX_PAGE_LIMIT$1)}`);
|
|
394
|
+
return Number(value);
|
|
395
|
+
}
|
|
396
|
+
function encodeCursor(offset) {
|
|
397
|
+
return Buffer.from(JSON.stringify({ offset }), "utf8").toString("base64url");
|
|
398
|
+
}
|
|
399
|
+
function optionalRawCursor(value) {
|
|
400
|
+
if (value === void 0) return;
|
|
401
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_CURSOR_LENGTH) throw new Error("cursor is invalid");
|
|
402
|
+
return value;
|
|
403
|
+
}
|
|
404
|
+
function decodeCursor(value) {
|
|
405
|
+
const cursor = optionalRawCursor(value);
|
|
406
|
+
if (cursor === void 0) return 0;
|
|
407
|
+
try {
|
|
408
|
+
const bytes = Buffer.from(cursor, "base64url");
|
|
409
|
+
if (bytes.toString("base64url") !== cursor) throw new Error("non-canonical base64url");
|
|
410
|
+
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
411
|
+
if (!isRecord(parsed) || !Number.isSafeInteger(parsed.offset) || Number(parsed.offset) < 0) throw new Error("invalid offset");
|
|
412
|
+
const offset = Number(parsed.offset);
|
|
413
|
+
if (encodeCursor(offset) !== cursor) throw new Error("non-canonical cursor payload");
|
|
414
|
+
return offset;
|
|
415
|
+
} catch (error) {
|
|
416
|
+
throw new Error("cursor is invalid", { cause: error });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function isExactPiSessionCursor(value) {
|
|
420
|
+
if (typeof value !== "string") return false;
|
|
421
|
+
try {
|
|
422
|
+
decodeCursor(value);
|
|
423
|
+
return true;
|
|
424
|
+
} catch {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function truncateUtf8(text, maxBytes) {
|
|
429
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
430
|
+
let low = 0;
|
|
431
|
+
let high = text.length;
|
|
432
|
+
while (low < high) {
|
|
433
|
+
const middle = Math.ceil((low + high) / 2);
|
|
434
|
+
if (Buffer.byteLength(text.slice(0, middle), "utf8") <= maxBytes - 3) low = middle;
|
|
435
|
+
else high = middle - 1;
|
|
436
|
+
}
|
|
437
|
+
const end = low > 0 && /[\uD800-\uDBFF]/u.test(text.charAt(low - 1)) ? low - 1 : low;
|
|
438
|
+
return `${text.slice(0, end)}…`;
|
|
439
|
+
}
|
|
440
|
+
function transcriptPage(items, limit, offset) {
|
|
441
|
+
const end = Math.max(0, items.length - offset);
|
|
442
|
+
const start = Math.max(0, end - limit);
|
|
443
|
+
const page = [];
|
|
444
|
+
let pageBytes = 2;
|
|
445
|
+
for (let index = end - 1; index >= start; index -= 1) {
|
|
446
|
+
const item = items[index];
|
|
447
|
+
if (!item) continue;
|
|
448
|
+
const bounded = {
|
|
449
|
+
...item,
|
|
450
|
+
text: truncateUtf8(item.text ?? "", MAX_TRANSCRIPT_ITEM_BYTES)
|
|
451
|
+
};
|
|
452
|
+
const itemBytes = Buffer.byteLength(JSON.stringify(bounded), "utf8") + 1;
|
|
453
|
+
if (page.length > 0 && pageBytes + itemBytes > MAX_TRANSCRIPT_PAGE_BYTES) break;
|
|
454
|
+
page.unshift(bounded);
|
|
455
|
+
pageBytes += itemBytes;
|
|
456
|
+
}
|
|
457
|
+
const consumed = offset + page.length;
|
|
458
|
+
return {
|
|
459
|
+
items: page,
|
|
460
|
+
...consumed < items.length ? { nextCursor: encodeCursor(consumed) } : {}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function textFromContent$1(content) {
|
|
464
|
+
if (typeof content === "string") return content;
|
|
465
|
+
if (!Array.isArray(content)) return "";
|
|
466
|
+
return content.flatMap((part) => {
|
|
467
|
+
if (!isRecord(part)) return [];
|
|
468
|
+
if (part.type === "text" && typeof part.text === "string") return [part.text];
|
|
469
|
+
if (part.type === "image") {
|
|
470
|
+
const mimeType = normalizeBoundedOptionalString(part.mimeType, 128);
|
|
471
|
+
return [mimeType ? `[image: ${mimeType}]` : "[image]"];
|
|
472
|
+
}
|
|
473
|
+
return [];
|
|
474
|
+
}).join("\n");
|
|
475
|
+
}
|
|
476
|
+
function parseListParams(value) {
|
|
477
|
+
if (value === void 0 || value === null) return { limit: DEFAULT_PAGE_LIMIT };
|
|
478
|
+
if (!isRecord(value)) throw new Error("Pi session list parameters must be an object");
|
|
479
|
+
const unknown = Object.keys(value).find((key) => ![
|
|
480
|
+
"searchTerm",
|
|
481
|
+
"limit",
|
|
482
|
+
"cursor"
|
|
483
|
+
].includes(key));
|
|
484
|
+
if (unknown) throw new Error(`unknown Pi session list parameter: ${unknown}`);
|
|
485
|
+
const searchTerm = normalizeBoundedOptionalString(value.searchTerm, MAX_SEARCH_LENGTH);
|
|
486
|
+
if (value.searchTerm !== void 0 && !searchTerm) throw new Error("searchTerm is invalid");
|
|
487
|
+
const cursor = optionalRawCursor(value.cursor);
|
|
488
|
+
return {
|
|
489
|
+
limit: boundedLimit(value.limit),
|
|
490
|
+
...searchTerm ? { searchTerm } : {},
|
|
491
|
+
...cursor ? { cursor } : {}
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function parseReadParams(value) {
|
|
495
|
+
if (!isRecord(value)) throw new Error("Pi session read parameters must be an object");
|
|
496
|
+
const unknown = Object.keys(value).find((key) => ![
|
|
497
|
+
"threadId",
|
|
498
|
+
"limit",
|
|
499
|
+
"cursor"
|
|
500
|
+
].includes(key));
|
|
501
|
+
if (unknown) throw new Error(`unknown Pi session read parameter: ${unknown}`);
|
|
502
|
+
const threadId = normalizeBoundedOptionalString(value.threadId, 256);
|
|
503
|
+
if (!threadId || !SESSION_ID_PATTERN$1.test(threadId)) throw new Error("threadId is invalid");
|
|
504
|
+
const cursor = optionalRawCursor(value.cursor);
|
|
505
|
+
return {
|
|
506
|
+
threadId,
|
|
507
|
+
limit: boundedLimit(value.limit),
|
|
508
|
+
...cursor ? { cursor } : {}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
async function listLocalPiSessionPage(value) {
|
|
512
|
+
const params = parseListParams(value);
|
|
513
|
+
const offset = decodeCursor(params.cursor);
|
|
514
|
+
const { summaries, hasMore } = await listPiSummaryPage(process.env, {
|
|
515
|
+
offset,
|
|
516
|
+
limit: params.limit,
|
|
517
|
+
...params.searchTerm ? { searchTerm: params.searchTerm } : {}
|
|
518
|
+
});
|
|
519
|
+
const page = summaries.map(({ file: _file, version: _version, ...session }) => session);
|
|
520
|
+
return {
|
|
521
|
+
sessions: page,
|
|
522
|
+
...hasMore ? { nextCursor: encodeCursor(offset + page.length) } : {}
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function isoTimestamp(message, entry) {
|
|
526
|
+
const value = parsePiSessionTimestampMs(message.timestamp) ?? parsePiSessionTimestampMs(entry.timestamp);
|
|
527
|
+
if (value === void 0) return;
|
|
528
|
+
const date = new Date(value);
|
|
529
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
530
|
+
}
|
|
531
|
+
function jsonText(value, maxLength = 2e4) {
|
|
532
|
+
try {
|
|
533
|
+
const text = JSON.stringify(value);
|
|
534
|
+
return text.length > maxLength ? `${truncateUtf16Safe(text, maxLength)}…` : text;
|
|
535
|
+
} catch {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function activePiEntries(entries) {
|
|
540
|
+
const header = entries[0];
|
|
541
|
+
if ((header?.type === "session" && typeof header.version === "number" ? header.version : 1) < 2) return entries.slice(1);
|
|
542
|
+
const body = entries.filter((entry) => entry.type !== "session" && normalizeBoundedOptionalString(entry.id, 256));
|
|
543
|
+
const byId = new Map(body.map((entry) => [String(entry.id), entry]));
|
|
544
|
+
const active = [];
|
|
545
|
+
let current = body.at(-1);
|
|
546
|
+
const visited = /* @__PURE__ */ new Set();
|
|
547
|
+
while (current) {
|
|
548
|
+
const id = String(current.id);
|
|
549
|
+
if (visited.has(id)) break;
|
|
550
|
+
visited.add(id);
|
|
551
|
+
active.push(current);
|
|
552
|
+
const parentId = normalizeBoundedOptionalString(current.parentId, 256);
|
|
553
|
+
current = parentId ? byId.get(parentId) : void 0;
|
|
554
|
+
}
|
|
555
|
+
return active.toReversed();
|
|
556
|
+
}
|
|
557
|
+
function piMessageItems(entry) {
|
|
558
|
+
if (!isRecord(entry.message)) return [];
|
|
559
|
+
const message = entry.message;
|
|
560
|
+
const role = message.role;
|
|
561
|
+
const id = normalizeBoundedOptionalString(entry.id, 256);
|
|
562
|
+
const timestamp = isoTimestamp(message, entry);
|
|
563
|
+
const model = normalizeBoundedOptionalString(message.model, 256);
|
|
564
|
+
const provider = normalizeBoundedOptionalString(message.provider, 256);
|
|
565
|
+
const modelRef = provider && model ? `${provider}/${model}` : model;
|
|
566
|
+
const common = {
|
|
567
|
+
...id ? { id } : {},
|
|
568
|
+
...timestamp ? { timestamp } : {},
|
|
569
|
+
...modelRef ? { model: modelRef } : {}
|
|
570
|
+
};
|
|
571
|
+
if (role === "user") {
|
|
572
|
+
const text = textFromContent$1(message.content);
|
|
573
|
+
return text ? [{
|
|
574
|
+
...common,
|
|
575
|
+
type: "userMessage",
|
|
576
|
+
text
|
|
577
|
+
}] : [];
|
|
578
|
+
}
|
|
579
|
+
if (role === "toolResult") {
|
|
580
|
+
const toolName = normalizeBoundedOptionalString(message.toolName, 256);
|
|
581
|
+
const text = textFromContent$1(message.content);
|
|
582
|
+
return [{
|
|
583
|
+
...common,
|
|
584
|
+
type: "toolResult",
|
|
585
|
+
text: toolName ? `${toolName}\n${text}` : text
|
|
586
|
+
}];
|
|
587
|
+
}
|
|
588
|
+
if (role === "bashExecution") {
|
|
589
|
+
const command = normalizeBoundedOptionalString(message.command, 4096) ?? "bash";
|
|
590
|
+
const output = typeof message.output === "string" ? message.output : "";
|
|
591
|
+
const status = message.cancelled === true ? "command cancelled" : typeof message.exitCode === "number" && message.exitCode !== 0 ? `command exited with code ${String(message.exitCode)}` : "";
|
|
592
|
+
return [{
|
|
593
|
+
...common,
|
|
594
|
+
type: "toolCall",
|
|
595
|
+
text: `bash\n${command}`
|
|
596
|
+
}, {
|
|
597
|
+
...common,
|
|
598
|
+
...id ? { id: `${id}:result` } : {},
|
|
599
|
+
type: "toolResult",
|
|
600
|
+
text: [output, status].filter(Boolean).join("\n\n")
|
|
601
|
+
}];
|
|
602
|
+
}
|
|
603
|
+
if (role === "custom" || role === "hookMessage") {
|
|
604
|
+
if (message.display !== true) return [];
|
|
605
|
+
const customType = normalizeBoundedOptionalString(message.customType, 256);
|
|
606
|
+
const text = textFromContent$1(message.content);
|
|
607
|
+
return text ? [{
|
|
608
|
+
...common,
|
|
609
|
+
type: "other",
|
|
610
|
+
text: customType ? `${customType}\n${text}` : text
|
|
611
|
+
}] : [];
|
|
612
|
+
}
|
|
613
|
+
if (role !== "assistant" || !Array.isArray(message.content)) return [];
|
|
614
|
+
return message.content.flatMap((part, index) => {
|
|
615
|
+
if (!isRecord(part)) return [];
|
|
616
|
+
const partCommon = {
|
|
617
|
+
...common,
|
|
618
|
+
...id ? { id: `${id}:${String(index)}` } : {}
|
|
619
|
+
};
|
|
620
|
+
if (part.type === "text" && typeof part.text === "string") return [{
|
|
621
|
+
...partCommon,
|
|
622
|
+
type: "agentMessage",
|
|
623
|
+
text: part.text
|
|
624
|
+
}];
|
|
625
|
+
if (part.type === "thinking" && typeof part.thinking === "string") return [{
|
|
626
|
+
...partCommon,
|
|
627
|
+
type: "reasoning",
|
|
628
|
+
text: part.thinking
|
|
629
|
+
}];
|
|
630
|
+
if (part.type === "toolCall") {
|
|
631
|
+
const name = normalizeBoundedOptionalString(part.name, 256) ?? "tool";
|
|
632
|
+
const args = jsonText(part.arguments);
|
|
633
|
+
return [{
|
|
634
|
+
...partCommon,
|
|
635
|
+
type: "toolCall",
|
|
636
|
+
text: args ? `${name}\n${args}` : name
|
|
637
|
+
}];
|
|
638
|
+
}
|
|
639
|
+
return [];
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
function piTranscriptItems(entries) {
|
|
643
|
+
return activePiEntries(entries).flatMap((entry) => {
|
|
644
|
+
if (entry.type === "message") return piMessageItems(entry);
|
|
645
|
+
const id = normalizeBoundedOptionalString(entry.id, 256);
|
|
646
|
+
const timestamp = normalizeBoundedOptionalString(entry.timestamp, 128);
|
|
647
|
+
const common = {
|
|
648
|
+
...id ? { id } : {},
|
|
649
|
+
...timestamp ? { timestamp } : {}
|
|
650
|
+
};
|
|
651
|
+
if (entry.type === "compaction" && typeof entry.summary === "string") return [{
|
|
652
|
+
...common,
|
|
653
|
+
type: "other",
|
|
654
|
+
text: entry.summary
|
|
655
|
+
}];
|
|
656
|
+
if (entry.type === "branch_summary" && typeof entry.summary === "string") return [{
|
|
657
|
+
...common,
|
|
658
|
+
type: "other",
|
|
659
|
+
text: entry.summary
|
|
660
|
+
}];
|
|
661
|
+
if (entry.type === "custom_message" && entry.display === true) {
|
|
662
|
+
const text = textFromContent$1(entry.content);
|
|
663
|
+
return text ? [{
|
|
664
|
+
...common,
|
|
665
|
+
type: "other",
|
|
666
|
+
text
|
|
667
|
+
}] : [];
|
|
668
|
+
}
|
|
669
|
+
return [];
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
async function readLocalPiTranscriptPage(value) {
|
|
673
|
+
const params = parseReadParams(value);
|
|
674
|
+
const offset = decodeCursor(params.cursor);
|
|
675
|
+
const page = transcriptPage(piTranscriptItems(await readPiSessionById(params.threadId, process.env)), params.limit, offset);
|
|
676
|
+
return {
|
|
677
|
+
hostId: LOCAL_HOST_ID$1,
|
|
678
|
+
label: "Local Pi",
|
|
679
|
+
threadId: params.threadId,
|
|
680
|
+
...page
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
//#endregion
|
|
684
|
+
//#region extensions/acpx/src/pi-session-upstream-activity.ts
|
|
685
|
+
const MAX_PI_UPSTREAM_SCAN_BYTES = 1024 * 1024;
|
|
686
|
+
async function readFileRange(handle, position, length) {
|
|
687
|
+
const buffer = Buffer.alloc(length);
|
|
688
|
+
let offset = 0;
|
|
689
|
+
while (offset < length) {
|
|
690
|
+
const { bytesRead } = await handle.read(buffer, offset, length - offset, position + offset);
|
|
691
|
+
if (bytesRead <= 0) break;
|
|
692
|
+
offset += bytesRead;
|
|
693
|
+
}
|
|
694
|
+
return offset === length ? buffer : buffer.subarray(0, offset);
|
|
695
|
+
}
|
|
696
|
+
function parseCompletePiRows(tail) {
|
|
697
|
+
const entries = [];
|
|
698
|
+
let lineStart = 0;
|
|
699
|
+
let classifiedBytes = 0;
|
|
700
|
+
for (let index = 0; index < tail.length; index += 1) {
|
|
701
|
+
if (tail[index] !== 10) continue;
|
|
702
|
+
const line = tail.subarray(lineStart, index).toString("utf8").trim();
|
|
703
|
+
if (line) try {
|
|
704
|
+
const value = JSON.parse(line);
|
|
705
|
+
if (!isRecord(value)) break;
|
|
706
|
+
entries.push(value);
|
|
707
|
+
} catch {
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
classifiedBytes = index + 1;
|
|
711
|
+
lineStart = index + 1;
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
entries,
|
|
715
|
+
classifiedBytes
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function textFromContent(content) {
|
|
719
|
+
if (typeof content === "string") return content;
|
|
720
|
+
if (!Array.isArray(content)) return;
|
|
721
|
+
return content.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n") || void 0;
|
|
722
|
+
}
|
|
723
|
+
function readFilePath(probe) {
|
|
724
|
+
return isRecord(probe.upstreamRef) && typeof probe.upstreamRef.filePath === "string" ? probe.upstreamRef.filePath : void 0;
|
|
725
|
+
}
|
|
726
|
+
function readMarkerOffset(probe) {
|
|
727
|
+
return isRecord(probe.marker) && Number.isSafeInteger(probe.marker.offset) && Number(probe.marker.offset) >= 0 ? Number(probe.marker.offset) : void 0;
|
|
728
|
+
}
|
|
729
|
+
async function linkContinuedPiSession(sessionKey, threadId) {
|
|
730
|
+
try {
|
|
731
|
+
const baseline = await readPiSessionFileBaseline(threadId, process.env);
|
|
732
|
+
return baseline ? {
|
|
733
|
+
sessionKey,
|
|
734
|
+
upstream: {
|
|
735
|
+
kind: "pi-cli",
|
|
736
|
+
ref: { filePath: baseline.filePath },
|
|
737
|
+
marker: { offset: baseline.offset }
|
|
738
|
+
}
|
|
739
|
+
} : { sessionKey };
|
|
740
|
+
} catch {
|
|
741
|
+
return { sessionKey };
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async function checkPiSessionUpstreamActivity(probe) {
|
|
745
|
+
if (probe.hostId !== "gateway" || probe.upstreamKind !== "pi-cli") return;
|
|
746
|
+
const filePath = readFilePath(probe);
|
|
747
|
+
const markerOffset = readMarkerOffset(probe);
|
|
748
|
+
if (!filePath || markerOffset === void 0) return;
|
|
749
|
+
let handle;
|
|
750
|
+
try {
|
|
751
|
+
handle = await fs$1.open(filePath, "r");
|
|
752
|
+
} catch (error) {
|
|
753
|
+
return isRecord(error) && error.code === "ENOENT" ? {
|
|
754
|
+
kind: "missing",
|
|
755
|
+
sessionKey: probe.sessionKey
|
|
756
|
+
} : void 0;
|
|
757
|
+
}
|
|
758
|
+
try {
|
|
759
|
+
const stat = await handle.stat();
|
|
760
|
+
if (!stat.isFile()) return {
|
|
761
|
+
kind: "missing",
|
|
762
|
+
sessionKey: probe.sessionKey
|
|
763
|
+
};
|
|
764
|
+
if (stat.size <= markerOffset) return;
|
|
765
|
+
const readLength = Math.min(stat.size - markerOffset, MAX_PI_UPSTREAM_SCAN_BYTES);
|
|
766
|
+
const { entries, classifiedBytes } = parseCompletePiRows(await readFileRange(handle, markerOffset, readLength));
|
|
767
|
+
if (classifiedBytes === 0) return;
|
|
768
|
+
let humanTurns = 0;
|
|
769
|
+
let occurredAt;
|
|
770
|
+
for (const entry of entries) {
|
|
771
|
+
if (entry.type !== "message" || !isRecord(entry.message) || entry.message.role !== "user") continue;
|
|
772
|
+
if (!isExternalUserText(probe, textFromContent(entry.message.content))) continue;
|
|
773
|
+
humanTurns += 1;
|
|
774
|
+
occurredAt = Math.max(occurredAt ?? 0, parsePiSessionTimestampMs(entry.message.timestamp) ?? parsePiSessionTimestampMs(entry.timestamp) ?? stat.mtimeMs);
|
|
775
|
+
}
|
|
776
|
+
const nextOffset = markerOffset + classifiedBytes;
|
|
777
|
+
return {
|
|
778
|
+
kind: "activity",
|
|
779
|
+
sessionKey: probe.sessionKey,
|
|
780
|
+
humanTurns,
|
|
781
|
+
nextMarker: { offset: nextOffset },
|
|
782
|
+
...humanTurns > 0 ? {
|
|
783
|
+
occurredAt: occurredAt ?? stat.mtimeMs,
|
|
784
|
+
dedupeId: String(nextOffset)
|
|
785
|
+
} : {}
|
|
786
|
+
};
|
|
787
|
+
} finally {
|
|
788
|
+
await handle.close();
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
async function checkPiUpstreamActivity(probes) {
|
|
792
|
+
const outcomes = [];
|
|
793
|
+
for (const probe of probes) try {
|
|
794
|
+
const outcome = await checkPiSessionUpstreamActivity(probe);
|
|
795
|
+
if (outcome) outcomes.push(outcome);
|
|
796
|
+
} catch {}
|
|
797
|
+
return outcomes;
|
|
798
|
+
}
|
|
799
|
+
//#endregion
|
|
800
|
+
//#region extensions/acpx/src/pi-session-catalog-runtime.ts
|
|
801
|
+
const LOCAL_HOST_ID = "gateway";
|
|
802
|
+
const MAX_PAGE_LIMIT = 100;
|
|
803
|
+
const MAX_HOSTS = 100;
|
|
804
|
+
const NODE_TIMEOUT_MS = 2e4;
|
|
805
|
+
const SESSION_ID_PATTERN = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
|
|
806
|
+
const TRANSCRIPT_ITEM_TYPES = /* @__PURE__ */ new Set([
|
|
807
|
+
"userMessage",
|
|
808
|
+
"agentMessage",
|
|
809
|
+
"reasoning",
|
|
810
|
+
"toolCall",
|
|
811
|
+
"toolResult",
|
|
812
|
+
"other"
|
|
813
|
+
]);
|
|
814
|
+
const ACPX_BACKEND_ID = "acpx";
|
|
815
|
+
const PI_ACP_AGENT_ID = "pi";
|
|
816
|
+
const PI_ADOPTED_SESSION_KEY_PREFIX = "plugin:acpx:catalog-adopt:pi:";
|
|
817
|
+
var PiCatalogParamsError = class extends Error {};
|
|
818
|
+
const continueAdoption = createSessionCatalogAdoptionCoordinator();
|
|
819
|
+
function validatePiThreadId(value) {
|
|
820
|
+
if (typeof value !== "string" || !SESSION_ID_PATTERN.test(value)) throw new Error("INVALID_REQUEST: threadId is invalid");
|
|
821
|
+
return value;
|
|
822
|
+
}
|
|
823
|
+
function isOptionalString(value) {
|
|
824
|
+
return value === void 0 || typeof value === "string";
|
|
825
|
+
}
|
|
826
|
+
function isOptionalNumber(value) {
|
|
827
|
+
return value === void 0 || typeof value === "number";
|
|
828
|
+
}
|
|
829
|
+
function isNodeSession(value) {
|
|
830
|
+
return isRecord(value) && typeof value.threadId === "string" && SESSION_ID_PATTERN.test(value.threadId) && typeof value.status === "string" && value.status.length > 0 && typeof value.archived === "boolean" && typeof value.canContinue === "boolean" && typeof value.canArchive === "boolean" && isOptionalString(value.name) && isOptionalString(value.cwd) && isOptionalString(value.source) && isOptionalString(value.modelProvider) && isOptionalString(value.cliVersion) && isOptionalString(value.gitBranch) && isOptionalString(value.sessionKey) && isOptionalNumber(value.createdAt) && isOptionalNumber(value.updatedAt) && isOptionalNumber(value.recencyAt);
|
|
831
|
+
}
|
|
832
|
+
function isNodeTranscriptItem(value) {
|
|
833
|
+
return isRecord(value) && typeof value.type === "string" && TRANSCRIPT_ITEM_TYPES.has(value.type) && isOptionalString(value.id) && isOptionalString(value.text) && isOptionalString(value.timestamp) && isOptionalString(value.model) && (value.truncated === void 0 || typeof value.truncated === "boolean");
|
|
834
|
+
}
|
|
835
|
+
function parseNodeParams(paramsJSON) {
|
|
836
|
+
if (!paramsJSON) return;
|
|
837
|
+
try {
|
|
838
|
+
return JSON.parse(paramsJSON);
|
|
839
|
+
} catch (error) {
|
|
840
|
+
throw new Error("Pi session parameters must be valid JSON", { cause: error });
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function nodeLabel(node) {
|
|
844
|
+
return node.displayName?.trim() || node.remoteIp?.trim() || node.nodeId;
|
|
845
|
+
}
|
|
846
|
+
function unwrapNodePayload(value) {
|
|
847
|
+
return isRecord(value) && typeof value.payloadJSON === "string" ? JSON.parse(value.payloadJSON) : value;
|
|
848
|
+
}
|
|
849
|
+
function setCatalogCapabilities(page, capabilities) {
|
|
850
|
+
for (const session of page.sessions) {
|
|
851
|
+
session.canContinue = capabilities.canContinue && session.canContinue;
|
|
852
|
+
session.canOpenTerminal = capabilities.canOpenTerminal;
|
|
853
|
+
}
|
|
854
|
+
return page;
|
|
855
|
+
}
|
|
856
|
+
function projectPiAdoptedSessions(page, adopted) {
|
|
857
|
+
return {
|
|
858
|
+
...page,
|
|
859
|
+
sessions: page.sessions.map((session) => {
|
|
860
|
+
const sessionKey = adopted.get(sessionCatalogAdoptedSourceKey(LOCAL_HOST_ID, session.threadId));
|
|
861
|
+
return sessionKey ? {
|
|
862
|
+
...session,
|
|
863
|
+
sessionKey
|
|
864
|
+
} : session;
|
|
865
|
+
})
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
async function listPiNodeHost(runtime, query, node) {
|
|
869
|
+
const hostId = `node:${node.nodeId}`;
|
|
870
|
+
const common = {
|
|
871
|
+
hostId,
|
|
872
|
+
label: nodeLabel(node),
|
|
873
|
+
kind: "node",
|
|
874
|
+
connected: node.connected === true,
|
|
875
|
+
nodeId: node.nodeId
|
|
876
|
+
};
|
|
877
|
+
if (node.connected !== true) return {
|
|
878
|
+
...common,
|
|
879
|
+
sessions: [],
|
|
880
|
+
error: {
|
|
881
|
+
code: "NODE_OFFLINE",
|
|
882
|
+
message: "Paired node is offline"
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
try {
|
|
886
|
+
const cursor = query.cursors?.[hostId];
|
|
887
|
+
if (cursor !== void 0 && !isExactPiSessionCursor(cursor)) throw new Error("cursor is invalid");
|
|
888
|
+
const page = parseNodeSessionPage(unwrapNodePayload(await runtime.nodes.invoke({
|
|
889
|
+
nodeId: node.nodeId,
|
|
890
|
+
command: PI_SESSIONS_LIST_COMMAND,
|
|
891
|
+
params: {
|
|
892
|
+
...query.limitPerHost ? { limit: query.limitPerHost } : {},
|
|
893
|
+
...query.search ? { searchTerm: query.search } : {},
|
|
894
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
895
|
+
},
|
|
896
|
+
timeoutMs: NODE_TIMEOUT_MS,
|
|
897
|
+
scopes: ["operator.write"]
|
|
898
|
+
})));
|
|
899
|
+
const canOpenTerminal = (node.invocableCommands ?? node.commands)?.includes(PI_TERMINAL_RESUME_COMMAND) === true;
|
|
900
|
+
return {
|
|
901
|
+
...common,
|
|
902
|
+
...setCatalogCapabilities(page, {
|
|
903
|
+
canContinue: false,
|
|
904
|
+
canOpenTerminal
|
|
905
|
+
})
|
|
906
|
+
};
|
|
907
|
+
} catch {
|
|
908
|
+
return {
|
|
909
|
+
...common,
|
|
910
|
+
sessions: [],
|
|
911
|
+
error: {
|
|
912
|
+
code: "NODE_INVOKE_FAILED",
|
|
913
|
+
message: "Paired node Pi sessions are unavailable"
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
function parseNodeSessionPage(value) {
|
|
919
|
+
if (!isRecord(value) || !Array.isArray(value.sessions) || value.sessions.length > MAX_PAGE_LIMIT) throw new Error("Pi node returned an invalid session page");
|
|
920
|
+
if (!value.sessions.every(isNodeSession)) throw new Error("Pi node returned an invalid session page");
|
|
921
|
+
const sessions = value.sessions;
|
|
922
|
+
const nextCursor = value.nextCursor;
|
|
923
|
+
if (nextCursor !== void 0 && !isExactPiSessionCursor(nextCursor)) throw new Error("Pi node returned an invalid cursor");
|
|
924
|
+
return {
|
|
925
|
+
sessions,
|
|
926
|
+
...nextCursor !== void 0 ? { nextCursor } : {}
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function parseNodeTranscriptPage(value, threadId) {
|
|
930
|
+
if (!isRecord(value) || value.threadId !== threadId || !Array.isArray(value.items) || value.items.length > MAX_PAGE_LIMIT || !value.items.every(isNodeTranscriptItem)) throw new Error("Pi node returned an invalid transcript page");
|
|
931
|
+
const nextCursor = value.nextCursor;
|
|
932
|
+
if (nextCursor !== void 0 && !isExactPiSessionCursor(nextCursor)) throw new Error("Pi node returned an invalid cursor");
|
|
933
|
+
return {
|
|
934
|
+
hostId: LOCAL_HOST_ID,
|
|
935
|
+
threadId,
|
|
936
|
+
items: value.items,
|
|
937
|
+
...nextCursor !== void 0 ? { nextCursor } : {}
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
async function listPiHosts(api, query) {
|
|
941
|
+
const runtime = api.runtime;
|
|
942
|
+
const canContinue = resolvePiContinuationAvailability(api).available;
|
|
943
|
+
const adopted = query.sessionEntries ? listAdoptedPiSessions(api, query.sessionEntries) : /* @__PURE__ */ new Map();
|
|
944
|
+
const requested = query.hostIds ? new Set(query.hostIds) : void 0;
|
|
945
|
+
const hosts = [];
|
|
946
|
+
const localStore = !requested || requested.has(LOCAL_HOST_ID) ? piSessionStore(process.env) : void 0;
|
|
947
|
+
if (localStore && (query.allowProcessHomeFallback !== false || !localStore.usesProcessHomeFallback) && piSessionStoreAvailable(process.env, localStore)) try {
|
|
948
|
+
hosts.push({
|
|
949
|
+
hostId: LOCAL_HOST_ID,
|
|
950
|
+
label: "Local Pi",
|
|
951
|
+
kind: "gateway",
|
|
952
|
+
connected: true,
|
|
953
|
+
...await listLocalPiSessionPage({
|
|
954
|
+
limit: query.limitPerHost,
|
|
955
|
+
...query.search ? { searchTerm: query.search } : {},
|
|
956
|
+
cursor: query.cursors?.[LOCAL_HOST_ID]
|
|
957
|
+
}).then((page) => projectPiAdoptedSessions(setCatalogCapabilities(page, {
|
|
958
|
+
canContinue,
|
|
959
|
+
canOpenTerminal: resolveNodeHostExecutable("pi", {
|
|
960
|
+
env: process.env,
|
|
961
|
+
pathEnv: process.env.PATH ?? "",
|
|
962
|
+
strategy: "fallback"
|
|
963
|
+
}) !== void 0
|
|
964
|
+
}), adopted))
|
|
965
|
+
});
|
|
966
|
+
} catch {
|
|
967
|
+
hosts.push({
|
|
968
|
+
hostId: LOCAL_HOST_ID,
|
|
969
|
+
label: "Local Pi",
|
|
970
|
+
kind: "gateway",
|
|
971
|
+
connected: true,
|
|
972
|
+
sessions: [],
|
|
973
|
+
error: {
|
|
974
|
+
code: "LOCAL_READ_FAILED",
|
|
975
|
+
message: "Local Pi sessions are unavailable"
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
let nodes;
|
|
980
|
+
try {
|
|
981
|
+
nodes = (await (query.listNodes?.() ?? runtime.nodes.list())).nodes;
|
|
982
|
+
} catch {
|
|
983
|
+
return hosts;
|
|
984
|
+
}
|
|
985
|
+
const eligible = nodes.filter((node) => node.commands?.includes("acpx.pi.sessions.list.v1") && (!requested || requested.has(`node:${node.nodeId}`))).toSorted((left, right) => nodeLabel(left).localeCompare(nodeLabel(right))).slice(0, MAX_HOSTS - hosts.length);
|
|
986
|
+
const nodeHosts = await Promise.all(eligible.map((node) => listPiNodeHost(runtime, query, node)));
|
|
987
|
+
return [...hosts, ...nodeHosts];
|
|
988
|
+
}
|
|
989
|
+
async function requireLocalPiSession(threadId) {
|
|
990
|
+
const record = (await listLocalPiSessionPage({
|
|
991
|
+
searchTerm: threadId,
|
|
992
|
+
limit: MAX_PAGE_LIMIT
|
|
993
|
+
})).sessions.find((session) => session.threadId === threadId);
|
|
994
|
+
if (!record) throw new Error("Pi session is unavailable");
|
|
995
|
+
return record;
|
|
996
|
+
}
|
|
997
|
+
function currentPiCatalogConfig(api) {
|
|
998
|
+
return api.runtime.config?.current?.() ?? api.config ?? {};
|
|
999
|
+
}
|
|
1000
|
+
function resolvePiContinuationAvailability(api) {
|
|
1001
|
+
const availability = resolveAcpSessionAvailability({
|
|
1002
|
+
config: currentPiCatalogConfig(api),
|
|
1003
|
+
backendId: ACPX_BACKEND_ID,
|
|
1004
|
+
agentId: PI_ACP_AGENT_ID
|
|
1005
|
+
});
|
|
1006
|
+
if (!availability.available) return availability;
|
|
1007
|
+
return resolveNodeHostExecutable("pi", {
|
|
1008
|
+
env: process.env,
|
|
1009
|
+
pathEnv: process.env.PATH ?? "",
|
|
1010
|
+
strategy: "fallback"
|
|
1011
|
+
}) ? { available: true } : {
|
|
1012
|
+
available: false,
|
|
1013
|
+
message: "Pi CLI is unavailable"
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function listAdoptedPiSessions(api, sessionEntries) {
|
|
1017
|
+
return listAdoptedSessionCatalogSessions({
|
|
1018
|
+
config: currentPiCatalogConfig(api),
|
|
1019
|
+
pluginId: api.id,
|
|
1020
|
+
runtime: api.runtime,
|
|
1021
|
+
sessionEntries,
|
|
1022
|
+
sourceFromEntry: (entry) => {
|
|
1023
|
+
const acpx = isRecord(entry.pluginExtensions?.acpx) ? entry.pluginExtensions.acpx : void 0;
|
|
1024
|
+
const marker = acpx && isRecord(acpx.piSessionCatalog) ? acpx.piSessionCatalog : void 0;
|
|
1025
|
+
return marker && typeof marker.sourceThreadId === "string" ? {
|
|
1026
|
+
hostId: LOCAL_HOST_ID,
|
|
1027
|
+
threadId: marker.sourceThreadId
|
|
1028
|
+
} : void 0;
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
async function continuePiSession(api, hostId, threadId) {
|
|
1033
|
+
if (hostId.startsWith("node:")) throw new PiCatalogParamsError("paired-node Pi session rows are view-only");
|
|
1034
|
+
if (hostId !== LOCAL_HOST_ID) throw new PiCatalogParamsError("Pi session catalog hostId is invalid");
|
|
1035
|
+
const availability = resolvePiContinuationAvailability(api);
|
|
1036
|
+
if (!availability.available) throw new PiCatalogParamsError(availability.message);
|
|
1037
|
+
const sourceKey = sessionCatalogAdoptedSourceKey(hostId, threadId);
|
|
1038
|
+
return await continueAdoption({
|
|
1039
|
+
sourceKey,
|
|
1040
|
+
findExisting: () => listAdoptedPiSessions(api).get(sourceKey),
|
|
1041
|
+
create: async () => {
|
|
1042
|
+
const record = await requireLocalPiSession(threadId).catch(() => void 0);
|
|
1043
|
+
if (!record) throw new PiCatalogParamsError("Pi session is unavailable");
|
|
1044
|
+
if (!record.canContinue) throw new PiCatalogParamsError("Pi session is outside the session store supported by pi-acp");
|
|
1045
|
+
const currentAvailability = resolvePiContinuationAvailability(api);
|
|
1046
|
+
if (!currentAvailability.available) throw new PiCatalogParamsError(currentAvailability.message);
|
|
1047
|
+
const config = currentPiCatalogConfig(api);
|
|
1048
|
+
const marker = { sourceThreadId: threadId };
|
|
1049
|
+
return { sessionKey: (await api.runtime.agent.session.createSessionEntry({
|
|
1050
|
+
cfg: config,
|
|
1051
|
+
key: sessionCatalogAdoptedSessionKey(PI_ADOPTED_SESSION_KEY_PREFIX, threadId),
|
|
1052
|
+
agentId: resolveDefaultAgentId(config),
|
|
1053
|
+
recoverMatchingInitialEntry: true,
|
|
1054
|
+
...record.name ? { label: record.name } : {},
|
|
1055
|
+
...record.cwd ? { spawnedCwd: record.cwd } : {},
|
|
1056
|
+
initialEntry: {
|
|
1057
|
+
acpBackendId: ACPX_BACKEND_ID,
|
|
1058
|
+
acpSessionBinding: {
|
|
1059
|
+
acpAgentId: PI_ACP_AGENT_ID,
|
|
1060
|
+
agentSessionId: threadId
|
|
1061
|
+
},
|
|
1062
|
+
pluginExtensions: { acpx: { piSessionCatalog: marker } }
|
|
1063
|
+
},
|
|
1064
|
+
afterCreate: async (entry) => {
|
|
1065
|
+
await importSessionCatalogHistory({
|
|
1066
|
+
catalogId: "pi",
|
|
1067
|
+
threadId,
|
|
1068
|
+
read: async ({ cursor, limit }) => await readPiTranscript(api.runtime, {
|
|
1069
|
+
hostId,
|
|
1070
|
+
threadId,
|
|
1071
|
+
limit,
|
|
1072
|
+
...cursor ? { cursor } : {}
|
|
1073
|
+
}),
|
|
1074
|
+
sessionId: entry.sessionId,
|
|
1075
|
+
sessionKey: entry.key,
|
|
1076
|
+
agentId: entry.agentId,
|
|
1077
|
+
...record.cwd ? { cwd: record.cwd } : {},
|
|
1078
|
+
config
|
|
1079
|
+
});
|
|
1080
|
+
return { pluginExtensions: { acpx: { piSessionCatalog: marker } } };
|
|
1081
|
+
}
|
|
1082
|
+
})).key };
|
|
1083
|
+
},
|
|
1084
|
+
complete: async (continued) => await linkContinuedPiSession(continued.sessionKey, threadId)
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
async function resolveNodePiSession(params) {
|
|
1088
|
+
const record = parseNodeSessionPage(unwrapNodePayload(await params.runtime.nodes.invoke({
|
|
1089
|
+
nodeId: params.nodeId,
|
|
1090
|
+
command: PI_SESSIONS_LIST_COMMAND,
|
|
1091
|
+
params: {
|
|
1092
|
+
searchTerm: params.threadId,
|
|
1093
|
+
limit: MAX_PAGE_LIMIT
|
|
1094
|
+
},
|
|
1095
|
+
timeoutMs: NODE_TIMEOUT_MS,
|
|
1096
|
+
scopes: ["operator.write"]
|
|
1097
|
+
}))).sessions.find((session) => session.threadId === params.threadId);
|
|
1098
|
+
if (!record) throw new Error("Pi session is unavailable");
|
|
1099
|
+
return record;
|
|
1100
|
+
}
|
|
1101
|
+
async function openPiTerminal(params) {
|
|
1102
|
+
const title = `pi --session ${params.threadId.slice(0, 12)}…`;
|
|
1103
|
+
if (params.hostId === LOCAL_HOST_ID) {
|
|
1104
|
+
const record = await requireLocalPiSession(params.threadId);
|
|
1105
|
+
const resolution = resolveNodeHostExecutable("pi", {
|
|
1106
|
+
env: process.env,
|
|
1107
|
+
pathEnv: process.env.PATH ?? "",
|
|
1108
|
+
strategy: "fallback"
|
|
1109
|
+
});
|
|
1110
|
+
if (!resolution) throw new Error("Pi CLI is unavailable");
|
|
1111
|
+
return {
|
|
1112
|
+
kind: "local",
|
|
1113
|
+
argv: [
|
|
1114
|
+
resolution.executable,
|
|
1115
|
+
"--session",
|
|
1116
|
+
params.threadId
|
|
1117
|
+
],
|
|
1118
|
+
...record.cwd ? { cwd: record.cwd } : {},
|
|
1119
|
+
...resolution.pathEnv ? { pathEnv: resolution.pathEnv } : {},
|
|
1120
|
+
title
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
if (!params.hostId.startsWith("node:")) throw new Error("hostId is invalid");
|
|
1124
|
+
const nodeId = params.hostId.slice(5);
|
|
1125
|
+
if (!(await params.runtime.nodes.list()).nodes.find((candidate) => {
|
|
1126
|
+
const commands = candidate.invocableCommands ?? candidate.commands;
|
|
1127
|
+
return candidate.nodeId === nodeId && candidate.connected === true && commands?.includes("acpx.pi.sessions.list.v1") === true && commands.includes("acpx.pi.terminal.resume.v1");
|
|
1128
|
+
})) throw new Error("paired-node Pi terminal is unavailable");
|
|
1129
|
+
const record = await resolveNodePiSession({
|
|
1130
|
+
runtime: params.runtime,
|
|
1131
|
+
nodeId,
|
|
1132
|
+
threadId: params.threadId
|
|
1133
|
+
});
|
|
1134
|
+
return {
|
|
1135
|
+
kind: "node",
|
|
1136
|
+
nodeId,
|
|
1137
|
+
command: PI_TERMINAL_RESUME_COMMAND,
|
|
1138
|
+
paramsJSON: JSON.stringify({ threadId: params.threadId }),
|
|
1139
|
+
...record.cwd ? { cwd: record.cwd } : {},
|
|
1140
|
+
title
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
async function readPiTranscript(runtime, request) {
|
|
1144
|
+
const cursor = request.cursor;
|
|
1145
|
+
if (cursor !== void 0 && !isExactPiSessionCursor(cursor)) throw new Error("cursor is invalid");
|
|
1146
|
+
if (request.hostId === LOCAL_HOST_ID) {
|
|
1147
|
+
assertPiLocalAccess(request.hostId, request.allowProcessHomeFallback);
|
|
1148
|
+
return await readLocalPiTranscriptPage({
|
|
1149
|
+
threadId: request.threadId,
|
|
1150
|
+
...request.limit ? { limit: request.limit } : {},
|
|
1151
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
if (!request.hostId.startsWith("node:")) throw new Error("hostId is invalid");
|
|
1155
|
+
const nodeId = request.hostId.slice(5);
|
|
1156
|
+
const node = (await runtime.nodes.list()).nodes.find((candidate) => candidate.nodeId === nodeId && candidate.connected === true && candidate.commands?.includes("acpx.pi.sessions.read.v1"));
|
|
1157
|
+
if (!node) throw new Error("paired-node Pi session host is unavailable");
|
|
1158
|
+
return {
|
|
1159
|
+
...parseNodeTranscriptPage(unwrapNodePayload(await runtime.nodes.invoke({
|
|
1160
|
+
nodeId,
|
|
1161
|
+
command: PI_SESSION_READ_COMMAND,
|
|
1162
|
+
params: {
|
|
1163
|
+
threadId: request.threadId,
|
|
1164
|
+
...request.limit ? { limit: request.limit } : {},
|
|
1165
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
1166
|
+
},
|
|
1167
|
+
timeoutMs: NODE_TIMEOUT_MS,
|
|
1168
|
+
scopes: ["operator.write"]
|
|
1169
|
+
})), request.threadId),
|
|
1170
|
+
hostId: request.hostId,
|
|
1171
|
+
label: nodeLabel(node)
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
function assertPiLocalAccess(hostId, allowProcessHomeFallback) {
|
|
1175
|
+
if (hostId === LOCAL_HOST_ID && allowProcessHomeFallback === false && piSessionStore(process.env).usesProcessHomeFallback) throw new PiCatalogParamsError("local Pi sessions are unavailable in isolated state");
|
|
1176
|
+
}
|
|
1177
|
+
async function listPiSessions(paramsJSON) {
|
|
1178
|
+
return JSON.stringify(await listLocalPiSessionPage(parseNodeParams(paramsJSON)));
|
|
1179
|
+
}
|
|
1180
|
+
async function readPiSession(paramsJSON) {
|
|
1181
|
+
return JSON.stringify(await readLocalPiTranscriptPage(parseNodeParams(paramsJSON)));
|
|
1182
|
+
}
|
|
1183
|
+
async function resumePiSession(paramsJSON, io) {
|
|
1184
|
+
if (!io) throw new Error("Pi terminal command requires duplex transport");
|
|
1185
|
+
const params = decodeNodePtyResumeParams(paramsJSON, validatePiThreadId);
|
|
1186
|
+
const record = await requireLocalPiSession(params.threadId);
|
|
1187
|
+
const resolution = resolveNodeHostExecutable("pi", {
|
|
1188
|
+
env: process.env,
|
|
1189
|
+
pathEnv: process.env.PATH ?? process.env.Path ?? "",
|
|
1190
|
+
strategy: "direct"
|
|
1191
|
+
});
|
|
1192
|
+
if (!resolution) throw new Error("Pi CLI is unavailable");
|
|
1193
|
+
return JSON.stringify(await runNodePtyCommand({
|
|
1194
|
+
file: resolution.executable,
|
|
1195
|
+
args: ["--session", params.threadId],
|
|
1196
|
+
cwd: record.cwd,
|
|
1197
|
+
cols: params.cols,
|
|
1198
|
+
rows: params.rows
|
|
1199
|
+
}, io));
|
|
1200
|
+
}
|
|
1201
|
+
function createPiSessionCatalogRuntime(api) {
|
|
1202
|
+
return {
|
|
1203
|
+
list: async (query) => await listPiHosts(api, query),
|
|
1204
|
+
read: async (request) => await readPiTranscript(api.runtime, request),
|
|
1205
|
+
continueSession: async (request) => {
|
|
1206
|
+
assertPiLocalAccess(request.hostId, request.allowProcessHomeFallback);
|
|
1207
|
+
return await continuePiSession(api, request.hostId, request.threadId);
|
|
1208
|
+
},
|
|
1209
|
+
checkUpstreamActivity: (probes, policy) => checkPiUpstreamActivity(probes.filter((probe) => probe.hostId !== LOCAL_HOST_ID || policy?.allowProcessHomeFallback !== false || !piSessionStore(process.env).usesProcessHomeFallback)),
|
|
1210
|
+
openTerminal: async (request) => {
|
|
1211
|
+
assertPiLocalAccess(request.hostId, request.allowProcessHomeFallback);
|
|
1212
|
+
return await openPiTerminal({
|
|
1213
|
+
runtime: api.runtime,
|
|
1214
|
+
...request
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
//#endregion
|
|
1220
|
+
export { createPiSessionCatalogRuntime, listPiSessions, readPiSession, resumePiSession };
|