@openclaw/acpx 2026.7.2-beta.5 → 2026.7.33

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