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