@openclaw/acpx 2026.8.1-beta.2 → 2026.8.1

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.
@@ -1,15 +1,17 @@
1
- import { a as PI_SESSION_READ_COMMAND, i as PI_SESSIONS_LIST_COMMAND, n as piSessionStore, o as PI_TERMINAL_RESUME_COMMAND, r as piSessionStoreAvailable, t as piAcpSessionStoreRoot } from "./pi-session-paths-DgYt8LUg.js";
1
+ import { c as PI_SESSION_READ_COMMAND, i as PI_LOCAL_SESSION_HOST_ID, l as PI_TERMINAL_RESUME_COMMAND, n as piSessionStore, o as PI_SESSIONS_LIST_COMMAND, r as piSessionStoreAvailable, s as PI_SESSION_ID_PATTERN, t as piAcpSessionStoreRoot } from "./pi-session-paths-EMbd4Hkz.js";
2
2
  import { parseDateFirstTimestampMs } from "openclaw/plugin-sdk/number-runtime";
3
- import { decodeNodePtyResumeParams, resolveNodeHostExecutable, runNodePtyCommand } from "openclaw/plugin-sdk/node-host";
3
+ import { resolveNodeHostExecutable } from "openclaw/plugin-sdk/node-host";
4
+ import { createSessionCatalogFamily, importSessionCatalogHistory, isExternalUserText, listAdoptedSessionCatalogSessions, sessionCatalogAdoptedSessionKey, sessionCatalogPaging } from "openclaw/plugin-sdk/session-catalog";
4
5
  import { isRecord, normalizeBoundedOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
5
6
  import { createReadStream } from "node:fs";
6
7
  import path from "node:path";
7
8
  import fs$1 from "node:fs/promises";
8
9
  import process from "node:process";
9
10
  import { resolveAcpSessionAvailability } from "openclaw/plugin-sdk/acp-runtime";
10
- import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
11
- import { createSessionCatalogAdoptionCoordinator, importSessionCatalogHistory, isExternalUserText, listAdoptedSessionCatalogSessions, sessionCatalogAdoptedSessionKey, sessionCatalogAdoptedSourceKey } from "openclaw/plugin-sdk/session-catalog";
11
+ import { resolveSessionAgentIdsStrict } from "openclaw/plugin-sdk/agent-scope-runtime";
12
12
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
13
+ import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
14
+ import { isPathStrictlyInside } from "openclaw/plugin-sdk/file-access-runtime";
13
15
  //#region extensions/acpx/src/pi-session-timestamp.ts
14
16
  /** Preserve Pi JSONL's date-first string contract while accepting numeric millisecond values. */
15
17
  function parsePiSessionTimestampMs(value) {
@@ -26,7 +28,7 @@ const APPEND_PROOF_EDGE_BYTES = 64 * 1024;
26
28
  const IO_CONCURRENCY = 8;
27
29
  const PI_FILE_CANDIDATE_CACHE_TTL_MS = 32e3;
28
30
  const PI_FILE_CANDIDATE_CACHE_MAX_ENTRIES = 8;
29
- const SESSION_ID_PATTERN$2 = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
31
+ const SESSION_ID_PATTERN = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
30
32
  const summaryCache = /* @__PURE__ */ new Map();
31
33
  const threadFileCache = /* @__PURE__ */ new Map();
32
34
  const piFileCandidateCache = /* @__PURE__ */ new Map();
@@ -94,38 +96,30 @@ async function realpathOrResolve(value) {
94
96
  return path.resolve(value);
95
97
  }
96
98
  }
97
- async function mapConcurrent(values, limit, mapper) {
98
- const results = [];
99
- results.length = values.length;
100
- let nextIndex = 0;
101
- const workers = Array.from({ length: Math.min(limit, values.length) }, async () => {
102
- while (nextIndex < values.length) {
103
- const index = nextIndex++;
104
- results[index] = await mapper(values[index]);
105
- }
106
- });
107
- await Promise.all(workers);
108
- return results;
109
- }
110
99
  async function scanPiFileCandidates(env) {
111
100
  const { root, files } = await discoverPiSessionFiles(env);
112
101
  const configuredAcpRoot = piAcpSessionStoreRoot(env);
113
102
  const acpRoot = configuredAcpRoot ? await realpathOrResolve(configuredAcpRoot) : void 0;
114
- return (await mapConcurrent(files, IO_CONCURRENCY, async (file) => {
115
- try {
116
- const stats = await fs$1.stat(file);
117
- return stats.isFile() ? {
118
- file,
119
- storeRoot: root,
120
- identity: `${String(stats.dev)}:${String(stats.ino)}:${String(stats.birthtimeMs)}`,
121
- mtimeMs: stats.mtimeMs,
122
- size: stats.size,
123
- resumable: acpRoot ? pathIsWithin(acpRoot, file) : false
124
- } : void 0;
125
- } catch {
126
- return;
127
- }
128
- })).filter((candidate) => candidate !== void 0).toSorted((left, right) => right.mtimeMs - left.mtimeMs);
103
+ const { results: candidates } = await runTasksWithConcurrency({
104
+ tasks: files.map((file) => async () => {
105
+ try {
106
+ const stats = await fs$1.stat(file);
107
+ return stats.isFile() ? {
108
+ file,
109
+ storeRoot: root,
110
+ identity: `${String(stats.dev)}:${String(stats.ino)}:${String(stats.birthtimeMs)}`,
111
+ mtimeMs: stats.mtimeMs,
112
+ size: stats.size,
113
+ resumable: acpRoot ? isPathStrictlyInside(acpRoot, file) : false
114
+ } : void 0;
115
+ } catch {
116
+ return;
117
+ }
118
+ }),
119
+ limit: IO_CONCURRENCY,
120
+ throwOnError: true
121
+ });
122
+ return candidates.filter((candidate) => candidate !== void 0).toSorted((left, right) => right.mtimeMs - left.mtimeMs);
129
123
  }
130
124
  async function piFileCandidates(env) {
131
125
  const store = piSessionStore(env);
@@ -155,10 +149,6 @@ async function piFileCandidates(env) {
155
149
  throw error;
156
150
  }
157
151
  }
158
- function pathIsWithin(root, candidate) {
159
- const relative = path.relative(root, candidate);
160
- return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
161
- }
162
152
  function parsePiJsonLines(content) {
163
153
  return content.split(/\r?\n/u).flatMap((line) => {
164
154
  if (!line.trim()) return [];
@@ -277,7 +267,7 @@ async function readPiSessionSummary(candidate) {
277
267
  const { header, name, firstMessage } = projectedState;
278
268
  const version = header?.type === "session" && typeof header.version === "number" ? header.version : 1;
279
269
  const threadId = header?.type === "session" ? normalizeBoundedOptionalString(header.id, 256) : void 0;
280
- if (header && threadId && SESSION_ID_PATTERN$2.test(threadId)) {
270
+ if (header && threadId && SESSION_ID_PATTERN.test(threadId)) {
281
271
  const cwd = normalizeBoundedOptionalString(header.cwd, 4096);
282
272
  const createdAt = parsePiSessionTimestampMs(header.timestamp);
283
273
  summary = {
@@ -326,7 +316,11 @@ async function listPiSummaryPage(env, params) {
326
316
  const matches = [];
327
317
  const needle = params.searchTerm?.toLocaleLowerCase();
328
318
  for (let index = 0; index < candidates.length && matches.length < target; index += SUMMARY_SCAN_BATCH_SIZE) {
329
- const summaries = await mapConcurrent(candidates.slice(index, index + SUMMARY_SCAN_BATCH_SIZE), IO_CONCURRENCY, readPiSessionSummary);
319
+ const { results: summaries } = await runTasksWithConcurrency({
320
+ tasks: candidates.slice(index, index + SUMMARY_SCAN_BATCH_SIZE).map((candidate) => () => readPiSessionSummary(candidate)),
321
+ limit: IO_CONCURRENCY,
322
+ throwOnError: true
323
+ });
330
324
  for (const summary of summaries) if (summary && summaryMatches(summary, needle)) {
331
325
  matches.push(summary);
332
326
  if (matches.length >= target) break;
@@ -340,7 +334,12 @@ async function listPiSummaryPage(env, params) {
340
334
  async function findPiSummary(threadId, env) {
341
335
  const candidates = await piFileCandidates(env);
342
336
  for (let index = 0; index < candidates.length; index += SUMMARY_SCAN_BATCH_SIZE) {
343
- const match = (await mapConcurrent(candidates.slice(index, index + SUMMARY_SCAN_BATCH_SIZE), IO_CONCURRENCY, readPiSessionSummary)).find((summary) => summary?.threadId === threadId);
337
+ const { results: summaries } = await runTasksWithConcurrency({
338
+ tasks: candidates.slice(index, index + SUMMARY_SCAN_BATCH_SIZE).map((candidate) => () => readPiSessionSummary(candidate)),
339
+ limit: IO_CONCURRENCY,
340
+ throwOnError: true
341
+ });
342
+ const match = summaries.find((summary) => summary?.threadId === threadId);
344
343
  if (match) return match;
345
344
  }
346
345
  }
@@ -380,86 +379,8 @@ async function readPiSessionById(threadId, env) {
380
379
  }
381
380
  //#endregion
382
381
  //#region extensions/acpx/src/pi-session-catalog.ts
383
- const LOCAL_HOST_ID$1 = "gateway";
384
- const DEFAULT_PAGE_LIMIT = 20;
385
- const MAX_PAGE_LIMIT$1 = 100;
386
382
  const MAX_SEARCH_LENGTH = 500;
387
- const MAX_CURSOR_LENGTH = 128;
388
- const MAX_TRANSCRIPT_ITEM_BYTES = 512 * 1024;
389
- const MAX_TRANSCRIPT_PAGE_BYTES = 20 * 1024 * 1024;
390
- const SESSION_ID_PATTERN$1 = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
391
- function boundedLimit(value, fallback = DEFAULT_PAGE_LIMIT) {
392
- if (value === void 0) return fallback;
393
- if (!Number.isInteger(value) || Number(value) < 1 || Number(value) > MAX_PAGE_LIMIT$1) throw new Error(`limit must be an integer between 1 and ${String(MAX_PAGE_LIMIT$1)}`);
394
- return Number(value);
395
- }
396
- function encodeCursor(offset) {
397
- return Buffer.from(JSON.stringify({ offset }), "utf8").toString("base64url");
398
- }
399
- function optionalRawCursor(value) {
400
- if (value === void 0) return;
401
- if (typeof value !== "string" || value.length === 0 || value.length > MAX_CURSOR_LENGTH) throw new Error("cursor is invalid");
402
- return value;
403
- }
404
- function decodeCursor(value) {
405
- const cursor = optionalRawCursor(value);
406
- if (cursor === void 0) return 0;
407
- try {
408
- const bytes = Buffer.from(cursor, "base64url");
409
- if (bytes.toString("base64url") !== cursor) throw new Error("non-canonical base64url");
410
- const parsed = JSON.parse(bytes.toString("utf8"));
411
- if (!isRecord(parsed) || !Number.isSafeInteger(parsed.offset) || Number(parsed.offset) < 0) throw new Error("invalid offset");
412
- const offset = Number(parsed.offset);
413
- if (encodeCursor(offset) !== cursor) throw new Error("non-canonical cursor payload");
414
- return offset;
415
- } catch (error) {
416
- throw new Error("cursor is invalid", { cause: error });
417
- }
418
- }
419
- function isExactPiSessionCursor(value) {
420
- if (typeof value !== "string") return false;
421
- try {
422
- decodeCursor(value);
423
- return true;
424
- } catch {
425
- return false;
426
- }
427
- }
428
- function truncateUtf8(text, maxBytes) {
429
- if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
430
- let low = 0;
431
- let high = text.length;
432
- while (low < high) {
433
- const middle = Math.ceil((low + high) / 2);
434
- if (Buffer.byteLength(text.slice(0, middle), "utf8") <= maxBytes - 3) low = middle;
435
- else high = middle - 1;
436
- }
437
- const end = low > 0 && /[\uD800-\uDBFF]/u.test(text.charAt(low - 1)) ? low - 1 : low;
438
- return `${text.slice(0, end)}…`;
439
- }
440
- function transcriptPage(items, limit, offset) {
441
- const end = Math.max(0, items.length - offset);
442
- const start = Math.max(0, end - limit);
443
- const page = [];
444
- let pageBytes = 2;
445
- for (let index = end - 1; index >= start; index -= 1) {
446
- const item = items[index];
447
- if (!item) continue;
448
- const bounded = {
449
- ...item,
450
- text: truncateUtf8(item.text ?? "", MAX_TRANSCRIPT_ITEM_BYTES)
451
- };
452
- const itemBytes = Buffer.byteLength(JSON.stringify(bounded), "utf8") + 1;
453
- if (page.length > 0 && pageBytes + itemBytes > MAX_TRANSCRIPT_PAGE_BYTES) break;
454
- page.unshift(bounded);
455
- pageBytes += itemBytes;
456
- }
457
- const consumed = offset + page.length;
458
- return {
459
- items: page,
460
- ...consumed < items.length ? { nextCursor: encodeCursor(consumed) } : {}
461
- };
462
- }
383
+ const isExactPiSessionCursor = sessionCatalogPaging.isExactCursor;
463
384
  function textFromContent$1(content) {
464
385
  if (typeof content === "string") return content;
465
386
  if (!Array.isArray(content)) return "";
@@ -473,44 +394,20 @@ function textFromContent$1(content) {
473
394
  return [];
474
395
  }).join("\n");
475
396
  }
476
- function parseListParams(value) {
477
- if (value === void 0 || value === null) return { limit: DEFAULT_PAGE_LIMIT };
478
- if (!isRecord(value)) throw new Error("Pi session list parameters must be an object");
479
- const unknown = Object.keys(value).find((key) => ![
480
- "searchTerm",
481
- "limit",
482
- "cursor"
483
- ].includes(key));
484
- if (unknown) throw new Error(`unknown Pi session list parameter: ${unknown}`);
485
- const searchTerm = normalizeBoundedOptionalString(value.searchTerm, MAX_SEARCH_LENGTH);
486
- if (value.searchTerm !== void 0 && !searchTerm) throw new Error("searchTerm is invalid");
487
- const cursor = optionalRawCursor(value.cursor);
488
- return {
489
- limit: boundedLimit(value.limit),
490
- ...searchTerm ? { searchTerm } : {},
491
- ...cursor ? { cursor } : {}
492
- };
493
- }
494
- function parseReadParams(value) {
495
- if (!isRecord(value)) throw new Error("Pi session read parameters must be an object");
496
- const unknown = Object.keys(value).find((key) => ![
497
- "threadId",
498
- "limit",
499
- "cursor"
500
- ].includes(key));
501
- if (unknown) throw new Error(`unknown Pi session read parameter: ${unknown}`);
502
- const threadId = normalizeBoundedOptionalString(value.threadId, 256);
503
- if (!threadId || !SESSION_ID_PATTERN$1.test(threadId)) throw new Error("threadId is invalid");
504
- const cursor = optionalRawCursor(value.cursor);
505
- return {
506
- threadId,
507
- limit: boundedLimit(value.limit),
508
- ...cursor ? { cursor } : {}
509
- };
510
- }
397
+ const PI_PARAMETER_MESSAGES = {
398
+ listNotObject: "Pi session list parameters must be an object",
399
+ unknownListParameter: (key) => `unknown Pi session list parameter: ${key}`,
400
+ invalidSearchTerm: "searchTerm is invalid",
401
+ readNotObject: "Pi session read parameters must be an object",
402
+ unknownReadParameter: (key) => `unknown Pi session read parameter: ${key}`,
403
+ invalidThreadId: "threadId is invalid"
404
+ };
511
405
  async function listLocalPiSessionPage(value) {
512
- const params = parseListParams(value);
513
- const offset = decodeCursor(params.cursor);
406
+ const params = sessionCatalogPaging.parseListParams(value, {
407
+ searchMaxLength: MAX_SEARCH_LENGTH,
408
+ messages: PI_PARAMETER_MESSAGES
409
+ });
410
+ const offset = sessionCatalogPaging.decodeCursor(params.cursor);
514
411
  const { summaries, hasMore } = await listPiSummaryPage(process.env, {
515
412
  offset,
516
413
  limit: params.limit,
@@ -519,7 +416,7 @@ async function listLocalPiSessionPage(value) {
519
416
  const page = summaries.map(({ file: _file, version: _version, ...session }) => session);
520
417
  return {
521
418
  sessions: page,
522
- ...hasMore ? { nextCursor: encodeCursor(offset + page.length) } : {}
419
+ ...hasMore ? { nextCursor: sessionCatalogPaging.encodeCursor(offset + page.length) } : {}
523
420
  };
524
421
  }
525
422
  function isoTimestamp(message, entry) {
@@ -670,11 +567,16 @@ function piTranscriptItems(entries) {
670
567
  });
671
568
  }
672
569
  async function readLocalPiTranscriptPage(value) {
673
- const params = parseReadParams(value);
674
- const offset = decodeCursor(params.cursor);
675
- const page = transcriptPage(piTranscriptItems(await readPiSessionById(params.threadId, process.env)), params.limit, offset);
570
+ const params = sessionCatalogPaging.parseReadParams(value, {
571
+ threadIdMaxLength: 256,
572
+ threadIdPattern: PI_SESSION_ID_PATTERN,
573
+ messages: PI_PARAMETER_MESSAGES
574
+ });
575
+ const offset = sessionCatalogPaging.decodeCursor(params.cursor);
576
+ const items = piTranscriptItems(await readPiSessionById(params.threadId, process.env));
577
+ const page = sessionCatalogPaging.boundTranscriptPage(items, params.limit, offset);
676
578
  return {
677
- hostId: LOCAL_HOST_ID$1,
579
+ hostId: PI_LOCAL_SESSION_HOST_ID,
678
580
  label: "Local Pi",
679
581
  threadId: params.threadId,
680
582
  ...page
@@ -798,201 +700,17 @@ async function checkPiUpstreamActivity(probes) {
798
700
  }
799
701
  //#endregion
800
702
  //#region extensions/acpx/src/pi-session-catalog-runtime.ts
801
- const LOCAL_HOST_ID = "gateway";
802
- const MAX_PAGE_LIMIT = 100;
803
- const MAX_HOSTS = 100;
804
703
  const NODE_TIMEOUT_MS = 2e4;
805
- const SESSION_ID_PATTERN = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
806
- const TRANSCRIPT_ITEM_TYPES = /* @__PURE__ */ new Set([
807
- "userMessage",
808
- "agentMessage",
809
- "reasoning",
810
- "toolCall",
811
- "toolResult",
812
- "other"
813
- ]);
814
704
  const ACPX_BACKEND_ID = "acpx";
815
705
  const PI_ACP_AGENT_ID = "pi";
816
706
  const PI_ADOPTED_SESSION_KEY_PREFIX = "plugin:acpx:catalog-adopt:pi:";
817
- var PiCatalogParamsError = class extends Error {};
818
- const continueAdoption = createSessionCatalogAdoptionCoordinator();
819
- function validatePiThreadId(value) {
820
- if (typeof value !== "string" || !SESSION_ID_PATTERN.test(value)) throw new Error("INVALID_REQUEST: threadId is invalid");
821
- return value;
822
- }
823
- function isOptionalString(value) {
824
- return value === void 0 || typeof value === "string";
825
- }
826
- function isOptionalNumber(value) {
827
- return value === void 0 || typeof value === "number";
828
- }
829
- function isNodeSession(value) {
830
- return isRecord(value) && typeof value.threadId === "string" && SESSION_ID_PATTERN.test(value.threadId) && typeof value.status === "string" && value.status.length > 0 && typeof value.archived === "boolean" && typeof value.canContinue === "boolean" && typeof value.canArchive === "boolean" && isOptionalString(value.name) && isOptionalString(value.cwd) && isOptionalString(value.source) && isOptionalString(value.modelProvider) && isOptionalString(value.cliVersion) && isOptionalString(value.gitBranch) && isOptionalString(value.sessionKey) && isOptionalNumber(value.createdAt) && isOptionalNumber(value.updatedAt) && isOptionalNumber(value.recencyAt);
831
- }
832
- function isNodeTranscriptItem(value) {
833
- return isRecord(value) && typeof value.type === "string" && TRANSCRIPT_ITEM_TYPES.has(value.type) && isOptionalString(value.id) && isOptionalString(value.text) && isOptionalString(value.timestamp) && isOptionalString(value.model) && (value.truncated === void 0 || typeof value.truncated === "boolean");
834
- }
835
- function parseNodeParams(paramsJSON) {
836
- if (!paramsJSON) return;
837
- try {
838
- return JSON.parse(paramsJSON);
839
- } catch (error) {
840
- throw new Error("Pi session parameters must be valid JSON", { cause: error });
841
- }
842
- }
843
- function nodeLabel(node) {
844
- return node.displayName?.trim() || node.remoteIp?.trim() || node.nodeId;
845
- }
846
- function unwrapNodePayload(value) {
847
- return isRecord(value) && typeof value.payloadJSON === "string" ? JSON.parse(value.payloadJSON) : value;
848
- }
849
- function setCatalogCapabilities(page, capabilities) {
850
- for (const session of page.sessions) {
851
- session.canContinue = capabilities.canContinue && session.canContinue;
852
- session.canOpenTerminal = capabilities.canOpenTerminal;
853
- }
854
- return page;
855
- }
856
- function projectPiAdoptedSessions(page, adopted) {
857
- return {
858
- ...page,
859
- sessions: page.sessions.map((session) => {
860
- const sessionKey = adopted.get(sessionCatalogAdoptedSourceKey(LOCAL_HOST_ID, session.threadId));
861
- return sessionKey ? {
862
- ...session,
863
- sessionKey
864
- } : session;
865
- })
866
- };
867
- }
868
- async function listPiNodeHost(runtime, query, node) {
869
- const hostId = `node:${node.nodeId}`;
870
- const common = {
871
- hostId,
872
- label: nodeLabel(node),
873
- kind: "node",
874
- connected: node.connected === true,
875
- nodeId: node.nodeId
876
- };
877
- if (node.connected !== true) return {
878
- ...common,
879
- sessions: [],
880
- error: {
881
- code: "NODE_OFFLINE",
882
- message: "Paired node is offline"
883
- }
884
- };
885
- try {
886
- const cursor = query.cursors?.[hostId];
887
- if (cursor !== void 0 && !isExactPiSessionCursor(cursor)) throw new Error("cursor is invalid");
888
- const page = parseNodeSessionPage(unwrapNodePayload(await runtime.nodes.invoke({
889
- nodeId: node.nodeId,
890
- command: PI_SESSIONS_LIST_COMMAND,
891
- params: {
892
- ...query.limitPerHost ? { limit: query.limitPerHost } : {},
893
- ...query.search ? { searchTerm: query.search } : {},
894
- ...cursor !== void 0 ? { cursor } : {}
895
- },
896
- timeoutMs: NODE_TIMEOUT_MS,
897
- scopes: ["operator.write"]
898
- })));
899
- const canOpenTerminal = (node.invocableCommands ?? node.commands)?.includes(PI_TERMINAL_RESUME_COMMAND) === true;
900
- return {
901
- ...common,
902
- ...setCatalogCapabilities(page, {
903
- canContinue: false,
904
- canOpenTerminal
905
- })
906
- };
907
- } catch {
908
- return {
909
- ...common,
910
- sessions: [],
911
- error: {
912
- code: "NODE_INVOKE_FAILED",
913
- message: "Paired node Pi sessions are unavailable"
914
- }
915
- };
916
- }
917
- }
918
- function parseNodeSessionPage(value) {
919
- if (!isRecord(value) || !Array.isArray(value.sessions) || value.sessions.length > MAX_PAGE_LIMIT) throw new Error("Pi node returned an invalid session page");
920
- if (!value.sessions.every(isNodeSession)) throw new Error("Pi node returned an invalid session page");
921
- const sessions = value.sessions;
922
- const nextCursor = value.nextCursor;
923
- if (nextCursor !== void 0 && !isExactPiSessionCursor(nextCursor)) throw new Error("Pi node returned an invalid cursor");
924
- return {
925
- sessions,
926
- ...nextCursor !== void 0 ? { nextCursor } : {}
927
- };
928
- }
929
- function parseNodeTranscriptPage(value, threadId) {
930
- if (!isRecord(value) || value.threadId !== threadId || !Array.isArray(value.items) || value.items.length > MAX_PAGE_LIMIT || !value.items.every(isNodeTranscriptItem)) throw new Error("Pi node returned an invalid transcript page");
931
- const nextCursor = value.nextCursor;
932
- if (nextCursor !== void 0 && !isExactPiSessionCursor(nextCursor)) throw new Error("Pi node returned an invalid cursor");
933
- return {
934
- hostId: LOCAL_HOST_ID,
935
- threadId,
936
- items: value.items,
937
- ...nextCursor !== void 0 ? { nextCursor } : {}
938
- };
939
- }
940
- async function listPiHosts(api, query) {
941
- const runtime = api.runtime;
942
- const canContinue = resolvePiContinuationAvailability(api).available;
943
- const adopted = query.sessionEntries ? listAdoptedPiSessions(api, query.sessionEntries) : /* @__PURE__ */ new Map();
944
- const requested = query.hostIds ? new Set(query.hostIds) : void 0;
945
- const hosts = [];
946
- const localStore = !requested || requested.has(LOCAL_HOST_ID) ? piSessionStore(process.env) : void 0;
947
- if (localStore && (query.allowProcessHomeFallback !== false || !localStore.usesProcessHomeFallback) && piSessionStoreAvailable(process.env, localStore)) try {
948
- hosts.push({
949
- hostId: LOCAL_HOST_ID,
950
- label: "Local Pi",
951
- kind: "gateway",
952
- connected: true,
953
- ...await listLocalPiSessionPage({
954
- limit: query.limitPerHost,
955
- ...query.search ? { searchTerm: query.search } : {},
956
- cursor: query.cursors?.[LOCAL_HOST_ID]
957
- }).then((page) => projectPiAdoptedSessions(setCatalogCapabilities(page, {
958
- canContinue,
959
- canOpenTerminal: resolveNodeHostExecutable("pi", {
960
- env: process.env,
961
- pathEnv: process.env.PATH ?? "",
962
- strategy: "fallback"
963
- }) !== void 0
964
- }), adopted))
965
- });
966
- } catch {
967
- hosts.push({
968
- hostId: LOCAL_HOST_ID,
969
- label: "Local Pi",
970
- kind: "gateway",
971
- connected: true,
972
- sessions: [],
973
- error: {
974
- code: "LOCAL_READ_FAILED",
975
- message: "Local Pi sessions are unavailable"
976
- }
977
- });
978
- }
979
- let nodes;
980
- try {
981
- nodes = (await (query.listNodes?.() ?? runtime.nodes.list())).nodes;
982
- } catch {
983
- return hosts;
984
- }
985
- const eligible = nodes.filter((node) => node.commands?.includes("acpx.pi.sessions.list.v1") && (!requested || requested.has(`node:${node.nodeId}`))).toSorted((left, right) => nodeLabel(left).localeCompare(nodeLabel(right))).slice(0, MAX_HOSTS - hosts.length);
986
- const nodeHosts = await Promise.all(eligible.map((node) => listPiNodeHost(runtime, query, node)));
987
- return [...hosts, ...nodeHosts];
988
- }
989
707
  async function requireLocalPiSession(threadId) {
990
- const record = (await listLocalPiSessionPage({
708
+ const session = (await listLocalPiSessionPage({
991
709
  searchTerm: threadId,
992
- limit: MAX_PAGE_LIMIT
993
- })).sessions.find((session) => session.threadId === threadId);
994
- if (!record) throw new Error("Pi session is unavailable");
995
- return record;
710
+ limit: 100
711
+ })).sessions.find((candidate) => candidate.threadId === threadId);
712
+ if (!session) throw new Error("Pi session is unavailable");
713
+ return session;
996
714
  }
997
715
  function currentPiCatalogConfig(api) {
998
716
  return api.runtime.config?.current?.() ?? api.config ?? {};
@@ -1013,8 +731,9 @@ function resolvePiContinuationAvailability(api) {
1013
731
  message: "Pi CLI is unavailable"
1014
732
  };
1015
733
  }
1016
- function listAdoptedPiSessions(api, sessionEntries) {
734
+ function listAdoptedPiSessions(api, agentId, sessionEntries) {
1017
735
  return listAdoptedSessionCatalogSessions({
736
+ ...agentId ? { agentId } : {},
1018
737
  config: currentPiCatalogConfig(api),
1019
738
  pluginId: api.id,
1020
739
  runtime: api.runtime,
@@ -1023,198 +742,148 @@ function listAdoptedPiSessions(api, sessionEntries) {
1023
742
  const acpx = isRecord(entry.pluginExtensions?.acpx) ? entry.pluginExtensions.acpx : void 0;
1024
743
  const marker = acpx && isRecord(acpx.piSessionCatalog) ? acpx.piSessionCatalog : void 0;
1025
744
  return marker && typeof marker.sourceThreadId === "string" ? {
1026
- hostId: LOCAL_HOST_ID,
745
+ hostId: PI_LOCAL_SESSION_HOST_ID,
1027
746
  threadId: marker.sourceThreadId
1028
747
  } : void 0;
1029
748
  }
1030
749
  });
1031
750
  }
1032
- async function continuePiSession(api, hostId, threadId) {
1033
- if (hostId.startsWith("node:")) throw new PiCatalogParamsError("paired-node Pi session rows are view-only");
1034
- if (hostId !== LOCAL_HOST_ID) throw new PiCatalogParamsError("Pi session catalog hostId is invalid");
1035
- const availability = resolvePiContinuationAvailability(api);
1036
- if (!availability.available) throw new PiCatalogParamsError(availability.message);
1037
- const sourceKey = sessionCatalogAdoptedSourceKey(hostId, threadId);
1038
- return await continueAdoption({
1039
- sourceKey,
1040
- findExisting: () => listAdoptedPiSessions(api).get(sourceKey),
1041
- create: async () => {
1042
- const record = await requireLocalPiSession(threadId).catch(() => void 0);
1043
- if (!record) throw new PiCatalogParamsError("Pi session is unavailable");
1044
- if (!record.canContinue) throw new PiCatalogParamsError("Pi session is outside the session store supported by pi-acp");
1045
- const currentAvailability = resolvePiContinuationAvailability(api);
1046
- if (!currentAvailability.available) throw new PiCatalogParamsError(currentAvailability.message);
1047
- const config = currentPiCatalogConfig(api);
1048
- const marker = { sourceThreadId: threadId };
1049
- return { sessionKey: (await api.runtime.agent.session.createSessionEntry({
1050
- cfg: config,
1051
- key: sessionCatalogAdoptedSessionKey(PI_ADOPTED_SESSION_KEY_PREFIX, threadId),
1052
- agentId: resolveDefaultAgentId(config),
1053
- recoverMatchingInitialEntry: true,
1054
- ...record.name ? { label: record.name } : {},
1055
- ...record.cwd ? { spawnedCwd: record.cwd } : {},
1056
- initialEntry: {
1057
- acpBackendId: ACPX_BACKEND_ID,
1058
- acpSessionBinding: {
1059
- acpAgentId: PI_ACP_AGENT_ID,
1060
- agentSessionId: threadId
1061
- },
1062
- pluginExtensions: { acpx: { piSessionCatalog: marker } }
1063
- },
1064
- afterCreate: async (entry) => {
1065
- await importSessionCatalogHistory({
1066
- catalogId: "pi",
1067
- threadId,
1068
- read: async ({ cursor, limit }) => await readPiTranscript(api.runtime, {
1069
- hostId,
1070
- threadId,
1071
- limit,
1072
- ...cursor ? { cursor } : {}
1073
- }),
1074
- sessionId: entry.sessionId,
1075
- sessionKey: entry.key,
1076
- agentId: entry.agentId,
1077
- ...record.cwd ? { cwd: record.cwd } : {},
1078
- config
1079
- });
1080
- return { pluginExtensions: { acpx: { piSessionCatalog: marker } } };
1081
- }
1082
- })).key };
1083
- },
1084
- complete: async (continued) => await linkContinuedPiSession(continued.sessionKey, threadId)
1085
- });
1086
- }
1087
- async function resolveNodePiSession(params) {
1088
- const record = parseNodeSessionPage(unwrapNodePayload(await params.runtime.nodes.invoke({
1089
- nodeId: params.nodeId,
1090
- command: PI_SESSIONS_LIST_COMMAND,
1091
- params: {
1092
- searchTerm: params.threadId,
1093
- limit: MAX_PAGE_LIMIT
1094
- },
1095
- timeoutMs: NODE_TIMEOUT_MS,
1096
- scopes: ["operator.write"]
1097
- }))).sessions.find((session) => session.threadId === params.threadId);
1098
- if (!record) throw new Error("Pi session is unavailable");
1099
- return record;
1100
- }
1101
- async function openPiTerminal(params) {
1102
- const title = `pi --session ${params.threadId.slice(0, 12)}…`;
1103
- if (params.hostId === LOCAL_HOST_ID) {
1104
- const record = await requireLocalPiSession(params.threadId);
1105
- const resolution = resolveNodeHostExecutable("pi", {
1106
- env: process.env,
1107
- pathEnv: process.env.PATH ?? "",
1108
- strategy: "fallback"
1109
- });
1110
- if (!resolution) throw new Error("Pi CLI is unavailable");
1111
- return {
1112
- kind: "local",
1113
- argv: [
1114
- resolution.executable,
1115
- "--session",
1116
- params.threadId
1117
- ],
1118
- ...record.cwd ? { cwd: record.cwd } : {},
1119
- ...resolution.pathEnv ? { pathEnv: resolution.pathEnv } : {},
1120
- title
1121
- };
1122
- }
1123
- if (!params.hostId.startsWith("node:")) throw new Error("hostId is invalid");
1124
- const nodeId = params.hostId.slice(5);
1125
- if (!(await params.runtime.nodes.list()).nodes.find((candidate) => {
1126
- const commands = candidate.invocableCommands ?? candidate.commands;
1127
- return candidate.nodeId === nodeId && candidate.connected === true && commands?.includes("acpx.pi.sessions.list.v1") === true && commands.includes("acpx.pi.terminal.resume.v1");
1128
- })) throw new Error("paired-node Pi terminal is unavailable");
1129
- const record = await resolveNodePiSession({
1130
- runtime: params.runtime,
1131
- nodeId,
1132
- threadId: params.threadId
1133
- });
1134
- return {
1135
- kind: "node",
1136
- nodeId,
1137
- command: PI_TERMINAL_RESUME_COMMAND,
1138
- paramsJSON: JSON.stringify({ threadId: params.threadId }),
1139
- ...record.cwd ? { cwd: record.cwd } : {},
1140
- title
1141
- };
1142
- }
1143
- async function readPiTranscript(runtime, request) {
1144
- const cursor = request.cursor;
1145
- if (cursor !== void 0 && !isExactPiSessionCursor(cursor)) throw new Error("cursor is invalid");
1146
- if (request.hostId === LOCAL_HOST_ID) {
1147
- assertPiLocalAccess(request.hostId, request.allowProcessHomeFallback);
1148
- return await readLocalPiTranscriptPage({
1149
- threadId: request.threadId,
1150
- ...request.limit ? { limit: request.limit } : {},
1151
- ...cursor !== void 0 ? { cursor } : {}
1152
- });
1153
- }
1154
- if (!request.hostId.startsWith("node:")) throw new Error("hostId is invalid");
1155
- const nodeId = request.hostId.slice(5);
1156
- const node = (await runtime.nodes.list()).nodes.find((candidate) => candidate.nodeId === nodeId && candidate.connected === true && candidate.commands?.includes("acpx.pi.sessions.read.v1"));
1157
- if (!node) throw new Error("paired-node Pi session host is unavailable");
1158
- return {
1159
- ...parseNodeTranscriptPage(unwrapNodePayload(await runtime.nodes.invoke({
1160
- nodeId,
1161
- command: PI_SESSION_READ_COMMAND,
1162
- params: {
1163
- threadId: request.threadId,
1164
- ...request.limit ? { limit: request.limit } : {},
1165
- ...cursor !== void 0 ? { cursor } : {}
751
+ async function createAdoptedPiSession(params) {
752
+ const config = currentPiCatalogConfig(params.api);
753
+ const marker = { sourceThreadId: params.threadId };
754
+ return { sessionKey: (await params.api.runtime.agent.session.createSessionEntry({
755
+ cfg: config,
756
+ key: sessionCatalogAdoptedSessionKey(PI_ADOPTED_SESSION_KEY_PREFIX, params.threadId),
757
+ agentId: params.agentId,
758
+ recoverMatchingInitialEntry: true,
759
+ ...params.session.name ? { displayName: params.session.name } : {},
760
+ ...params.session.cwd ? { spawnedCwd: params.session.cwd } : {},
761
+ initialEntry: {
762
+ acpBackendId: ACPX_BACKEND_ID,
763
+ acpSessionBinding: {
764
+ acpAgentId: PI_ACP_AGENT_ID,
765
+ agentSessionId: params.threadId
1166
766
  },
1167
- timeoutMs: NODE_TIMEOUT_MS,
1168
- scopes: ["operator.write"]
1169
- })), request.threadId),
1170
- hostId: request.hostId,
1171
- label: nodeLabel(node)
1172
- };
767
+ pluginExtensions: { acpx: { piSessionCatalog: marker } }
768
+ },
769
+ afterCreate: async (entry) => {
770
+ await importSessionCatalogHistory({
771
+ catalogId: "pi",
772
+ threadId: params.threadId,
773
+ read: async ({ cursor, limit }) => await readLocalPiTranscriptPage({
774
+ threadId: params.threadId,
775
+ limit,
776
+ ...cursor ? { cursor } : {}
777
+ }),
778
+ sessionId: entry.sessionId,
779
+ sessionKey: entry.key,
780
+ agentId: entry.agentId,
781
+ ...params.session.cwd ? { cwd: params.session.cwd } : {},
782
+ config
783
+ });
784
+ return { pluginExtensions: { acpx: { piSessionCatalog: marker } } };
785
+ }
786
+ })).key };
1173
787
  }
1174
788
  function assertPiLocalAccess(hostId, allowProcessHomeFallback) {
1175
- if (hostId === LOCAL_HOST_ID && allowProcessHomeFallback === false && piSessionStore(process.env).usesProcessHomeFallback) throw new PiCatalogParamsError("local Pi sessions are unavailable in isolated state");
789
+ if (hostId === "gateway" && allowProcessHomeFallback === false && piSessionStore(process.env).usesProcessHomeFallback) throw new Error("local Pi sessions are unavailable in isolated state");
1176
790
  }
1177
- async function listPiSessions(paramsJSON) {
1178
- return JSON.stringify(await listLocalPiSessionPage(parseNodeParams(paramsJSON)));
791
+ async function listPiSessions(params) {
792
+ return await listLocalPiSessionPage(params);
1179
793
  }
1180
- async function readPiSession(paramsJSON) {
1181
- return JSON.stringify(await readLocalPiTranscriptPage(parseNodeParams(paramsJSON)));
1182
- }
1183
- async function resumePiSession(paramsJSON, io) {
1184
- if (!io) throw new Error("Pi terminal command requires duplex transport");
1185
- const params = decodeNodePtyResumeParams(paramsJSON, validatePiThreadId);
1186
- const record = await requireLocalPiSession(params.threadId);
1187
- const resolution = resolveNodeHostExecutable("pi", {
1188
- env: process.env,
1189
- pathEnv: process.env.PATH ?? process.env.Path ?? "",
1190
- strategy: "direct"
1191
- });
1192
- if (!resolution) throw new Error("Pi CLI is unavailable");
1193
- return JSON.stringify(await runNodePtyCommand({
1194
- file: resolution.executable,
1195
- args: ["--session", params.threadId],
1196
- cwd: record.cwd,
1197
- cols: params.cols,
1198
- rows: params.rows
1199
- }, io));
794
+ async function readPiSession(params) {
795
+ return await readLocalPiTranscriptPage(params);
1200
796
  }
1201
797
  function createPiSessionCatalogRuntime(api) {
1202
- return {
1203
- list: async (query) => await listPiHosts(api, query),
1204
- read: async (request) => await readPiTranscript(api.runtime, request),
1205
- continueSession: async (request) => {
1206
- assertPiLocalAccess(request.hostId, request.allowProcessHomeFallback);
1207
- return await continuePiSession(api, request.hostId, request.threadId);
798
+ return createSessionCatalogFamily({
799
+ runtime: api.runtime,
800
+ local: {
801
+ hostId: PI_LOCAL_SESSION_HOST_ID,
802
+ label: "Local Pi",
803
+ available: (query) => {
804
+ const store = piSessionStore(process.env);
805
+ return (query.allowProcessHomeFallback !== false || !store.usesProcessHomeFallback) && piSessionStoreAvailable(process.env, store);
806
+ },
807
+ list: async (query) => await listLocalPiSessionPage({
808
+ limit: query.limitPerHost,
809
+ ...query.search ? { searchTerm: query.search } : {},
810
+ cursor: query.cursors?.[PI_LOCAL_SESSION_HOST_ID]
811
+ }),
812
+ read: async (request) => await readLocalPiTranscriptPage({
813
+ threadId: request.threadId,
814
+ ...request.limit ? { limit: request.limit } : {},
815
+ ...request.cursor !== void 0 ? { cursor: request.cursor } : {}
816
+ }),
817
+ assertAccess: assertPiLocalAccess
1208
818
  },
1209
- checkUpstreamActivity: (probes, policy) => checkPiUpstreamActivity(probes.filter((probe) => probe.hostId !== LOCAL_HOST_ID || policy?.allowProcessHomeFallback !== false || !piSessionStore(process.env).usesProcessHomeFallback)),
1210
- openTerminal: async (request) => {
1211
- assertPiLocalAccess(request.hostId, request.allowProcessHomeFallback);
1212
- return await openPiTerminal({
1213
- runtime: api.runtime,
1214
- ...request
1215
- });
1216
- }
1217
- };
819
+ node: {
820
+ listCommand: PI_SESSIONS_LIST_COMMAND,
821
+ readCommand: PI_SESSION_READ_COMMAND,
822
+ terminalCommand: PI_TERMINAL_RESUME_COMMAND,
823
+ timeoutMs: NODE_TIMEOUT_MS,
824
+ maxHosts: 100,
825
+ maxPageLimit: 100,
826
+ sessionIdPattern: PI_SESSION_ID_PATTERN
827
+ },
828
+ capabilities: {
829
+ local: () => ({
830
+ canContinue: resolvePiContinuationAvailability(api).available,
831
+ canOpenTerminal: resolveNodeHostExecutable("pi", {
832
+ env: process.env,
833
+ pathEnv: process.env.PATH ?? "",
834
+ strategy: "fallback"
835
+ }) !== void 0
836
+ }),
837
+ node: (node) => {
838
+ return {
839
+ canContinue: false,
840
+ canOpenTerminal: (node.invocableCommands ?? node.commands)?.includes(PI_TERMINAL_RESUME_COMMAND) === true
841
+ };
842
+ },
843
+ project: (session, capabilities) => ({
844
+ ...session,
845
+ canContinue: capabilities.canContinue && session.canContinue,
846
+ canOpenTerminal: capabilities.canOpenTerminal
847
+ })
848
+ },
849
+ messages: {
850
+ invalidNodeCursor: "Pi node returned an invalid cursor",
851
+ invalidNodeSessionPage: "Pi node returned an invalid session page",
852
+ invalidNodeTranscriptPage: "Pi node returned an invalid transcript page",
853
+ invalidHostId: "Pi session catalog hostId is invalid",
854
+ localReadFailed: "Local Pi sessions are unavailable",
855
+ nodeInvokeFailed: "Paired node Pi sessions are unavailable",
856
+ nodeReadUnavailable: "paired-node Pi session host is unavailable",
857
+ nodeTerminalUnavailable: "paired-node Pi terminal is unavailable",
858
+ sessionUnavailable: "Pi session is unavailable"
859
+ },
860
+ continuation: {
861
+ resolveAgentId: (agentId) => resolveSessionAgentIdsStrict({
862
+ config: api.config,
863
+ agentId
864
+ }).sessionAgentId,
865
+ availability: () => resolvePiContinuationAvailability(api),
866
+ listAdopted: (agentId, sessionEntries) => listAdoptedPiSessions(api, agentId, sessionEntries),
867
+ loadSession: requireLocalPiSession,
868
+ validateSession: (session) => {
869
+ if (!session.canContinue) throw new Error("Pi session is outside the session store supported by pi-acp");
870
+ },
871
+ create: async (params) => await createAdoptedPiSession({
872
+ api,
873
+ ...params
874
+ }),
875
+ complete: async (continued, threadId) => await linkContinuedPiSession(continued.sessionKey, threadId),
876
+ nodeReadOnlyMessage: "paired-node Pi session rows are view-only"
877
+ },
878
+ terminal: {
879
+ executable: "pi",
880
+ args: (threadId) => ["--session", threadId],
881
+ title: (threadId) => `pi --session ${threadId.slice(0, 12)}…`,
882
+ requireLocalSession: requireLocalPiSession,
883
+ unavailableMessage: "Pi CLI is unavailable"
884
+ },
885
+ checkUpstreamActivity: (probes, policy) => checkPiUpstreamActivity(probes.filter((probe) => probe.hostId !== "gateway" || policy?.allowProcessHomeFallback !== false || !piSessionStore(process.env).usesProcessHomeFallback))
886
+ }, isExactPiSessionCursor);
1218
887
  }
1219
888
  //#endregion
1220
- export { createPiSessionCatalogRuntime, listPiSessions, readPiSession, resumePiSession };
889
+ export { createPiSessionCatalogRuntime, listPiSessions, readPiSession, requireLocalPiSession };