@openclaw/acpx 2026.8.1-beta.2 → 2026.9.1-beta.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.
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
- import { t as createAcpxRuntimeService } from "./register.runtime-C29AY8LI.js";
1
+ import { t as createAcpxRuntimeService } from "./register.runtime-Dj29TKFy.js";
2
2
  import "./config-schema-DN_uAi4R.js";
3
- import { a as PI_SESSION_READ_COMMAND, i as PI_SESSIONS_LIST_COMMAND, o as PI_TERMINAL_RESUME_COMMAND, r as piSessionStoreAvailable } from "./pi-session-paths-DgYt8LUg.js";
3
+ import { a as PI_SESSIONS_CAPABILITY, c as PI_SESSION_READ_COMMAND, l as PI_TERMINAL_RESUME_COMMAND, o as PI_SESSIONS_LIST_COMMAND, r as piSessionStoreAvailable, s as PI_SESSION_ID_PATTERN } from "./pi-session-paths-EMbd4Hkz.js";
4
4
  import { tryDispatchAcpReplyHook } from "openclaw/plugin-sdk/acp-runtime-backend";
5
5
  import { finiteSecondsToTimerSafeMilliseconds } from "openclaw/plugin-sdk/number-runtime";
6
6
  import { createLazyRuntimeModule, createLazyRuntimeSurface } from "openclaw/plugin-sdk/lazy-runtime";
7
7
  import { resolveNodeHostExecutable } from "openclaw/plugin-sdk/node-host";
8
+ import { createSessionCatalogNodeHostBindings } from "openclaw/plugin-sdk/session-catalog";
8
9
  import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
9
10
  //#region extensions/acpx/src/pi-session-catalog-plugin.ts
10
- const PI_SESSIONS_CAPABILITY = "pi-sessions";
11
- const loadPiSessionCatalogModule = createLazyRuntimeModule(() => import("./pi-session-catalog-runtime-BiTQtT7s.js"));
11
+ const loadPiSessionCatalogModule = createLazyRuntimeModule(() => import("./pi-session-catalog-runtime-BIu-8uRZ.js"));
12
12
  function fullConfigCatalogEnabled(config) {
13
13
  if (!isRecord(config) || !isRecord(config.plugins) || !isRecord(config.plugins.entries)) return true;
14
14
  const entry = config.plugins.entries.acpx;
@@ -18,54 +18,40 @@ function fullConfigCatalogEnabled(config) {
18
18
  function isPiSessionCatalogEnabled(pluginConfig) {
19
19
  return !isRecord(pluginConfig) || !isRecord(pluginConfig.piSessionCatalog) || pluginConfig.piSessionCatalog.enabled !== false;
20
20
  }
21
- function createPiSessionNodeHostCommands() {
21
+ function createPiSessionNodeHostBindings() {
22
22
  const storeAvailable = ({ config, env }) => fullConfigCatalogEnabled(config) && piSessionStoreAvailable(env);
23
- return [
24
- {
25
- command: PI_SESSIONS_LIST_COMMAND,
26
- cap: PI_SESSIONS_CAPABILITY,
27
- dangerous: false,
28
- isAvailable: storeAvailable,
29
- handle: async (paramsJSON) => await (await loadPiSessionCatalogModule()).listPiSessions(paramsJSON)
23
+ return createSessionCatalogNodeHostBindings({
24
+ capability: PI_SESSIONS_CAPABILITY,
25
+ listCommand: PI_SESSIONS_LIST_COMMAND,
26
+ readCommand: PI_SESSION_READ_COMMAND,
27
+ terminalCommand: PI_TERMINAL_RESUME_COMMAND,
28
+ sessionIdPattern: PI_SESSION_ID_PATTERN,
29
+ executable: "pi",
30
+ args: (threadId) => ["--session", threadId],
31
+ listAvailable: storeAvailable,
32
+ terminalAvailable: ({ config, env }) => storeAvailable({
33
+ config,
34
+ env
35
+ }) && Boolean(resolveNodeHostExecutable("pi", {
36
+ env,
37
+ pathEnv: env.PATH ?? env.Path ?? "",
38
+ strategy: "direct"
39
+ })),
40
+ parseParams: (paramsJSON) => {
41
+ if (!paramsJSON) return;
42
+ try {
43
+ return JSON.parse(paramsJSON);
44
+ } catch (error) {
45
+ throw new Error("Pi session parameters must be valid JSON", { cause: error });
46
+ }
30
47
  },
31
- {
32
- command: PI_SESSION_READ_COMMAND,
33
- cap: PI_SESSIONS_CAPABILITY,
34
- dangerous: false,
35
- isAvailable: storeAvailable,
36
- handle: async (paramsJSON) => await (await loadPiSessionCatalogModule()).readPiSession(paramsJSON)
37
- },
38
- {
39
- command: PI_TERMINAL_RESUME_COMMAND,
40
- cap: PI_SESSIONS_CAPABILITY,
41
- dangerous: false,
42
- duplex: true,
43
- isAvailable: ({ config, env }) => storeAvailable({
44
- config,
45
- env
46
- }) && Boolean(resolveNodeHostExecutable("pi", {
47
- env,
48
- pathEnv: env.PATH ?? env.Path ?? "",
49
- strategy: "direct"
50
- })),
51
- handle: async (paramsJSON, io) => await (await loadPiSessionCatalogModule()).resumePiSession(paramsJSON, io)
52
- }
53
- ];
54
- }
55
- function createPiSessionNodeInvokePolicies() {
56
- return [{
57
- commands: [
58
- PI_SESSIONS_LIST_COMMAND,
59
- PI_SESSION_READ_COMMAND,
60
- PI_TERMINAL_RESUME_COMMAND
61
- ],
62
- defaultPlatforms: [
63
- "macos",
64
- "linux",
65
- "windows"
66
- ],
67
- handle: (context) => context.command === "acpx.pi.terminal.resume.v1" ? { ok: true } : context.invokeNode()
68
- }];
48
+ list: async (params) => await (await loadPiSessionCatalogModule()).listPiSessions(params),
49
+ read: async (params) => await (await loadPiSessionCatalogModule()).readPiSession(params),
50
+ requireSession: async (threadId) => await (await loadPiSessionCatalogModule()).requireLocalPiSession(threadId),
51
+ terminalIoRequiredMessage: "Pi terminal command requires duplex transport",
52
+ terminalUnavailableMessage: "Pi CLI is unavailable",
53
+ invalidThreadIdMessage: "INVALID_REQUEST: threadId is invalid"
54
+ });
69
55
  }
70
56
  function registerPiSessionCatalog(api) {
71
57
  if (!isPiSessionCatalogEnabled(api.pluginConfig)) return;
@@ -80,8 +66,9 @@ function registerPiSessionCatalog(api) {
80
66
  checkUpstreamActivity: async (probes, policy) => await (await loadCatalogRuntime()).checkUpstreamActivity(probes, policy),
81
67
  openTerminal: async (request) => await (await loadCatalogRuntime()).openTerminal(request)
82
68
  });
83
- for (const command of createPiSessionNodeHostCommands()) api.registerNodeHostCommand(command);
84
- for (const policy of createPiSessionNodeInvokePolicies()) api.registerNodeInvokePolicy(policy);
69
+ const nodeHost = createPiSessionNodeHostBindings();
70
+ for (const command of nodeHost.commands) api.registerNodeHostCommand(command);
71
+ for (const policy of nodeHost.policies) api.registerNodeInvokePolicy(policy);
85
72
  }
86
73
  //#endregion
87
74
  //#region extensions/acpx/index.ts
@@ -1,15 +1,16 @@
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 { resolveSessionAgentIds } from "openclaw/plugin-sdk/agent-runtime";
12
12
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
13
+ import { isPathStrictlyInside } from "openclaw/plugin-sdk/file-access-runtime";
13
14
  //#region extensions/acpx/src/pi-session-timestamp.ts
14
15
  /** Preserve Pi JSONL's date-first string contract while accepting numeric millisecond values. */
15
16
  function parsePiSessionTimestampMs(value) {
@@ -26,7 +27,7 @@ const APPEND_PROOF_EDGE_BYTES = 64 * 1024;
26
27
  const IO_CONCURRENCY = 8;
27
28
  const PI_FILE_CANDIDATE_CACHE_TTL_MS = 32e3;
28
29
  const PI_FILE_CANDIDATE_CACHE_MAX_ENTRIES = 8;
29
- const SESSION_ID_PATTERN$2 = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
30
+ const SESSION_ID_PATTERN = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
30
31
  const summaryCache = /* @__PURE__ */ new Map();
31
32
  const threadFileCache = /* @__PURE__ */ new Map();
32
33
  const piFileCandidateCache = /* @__PURE__ */ new Map();
@@ -120,7 +121,7 @@ async function scanPiFileCandidates(env) {
120
121
  identity: `${String(stats.dev)}:${String(stats.ino)}:${String(stats.birthtimeMs)}`,
121
122
  mtimeMs: stats.mtimeMs,
122
123
  size: stats.size,
123
- resumable: acpRoot ? pathIsWithin(acpRoot, file) : false
124
+ resumable: acpRoot ? isPathStrictlyInside(acpRoot, file) : false
124
125
  } : void 0;
125
126
  } catch {
126
127
  return;
@@ -155,10 +156,6 @@ async function piFileCandidates(env) {
155
156
  throw error;
156
157
  }
157
158
  }
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
159
  function parsePiJsonLines(content) {
163
160
  return content.split(/\r?\n/u).flatMap((line) => {
164
161
  if (!line.trim()) return [];
@@ -277,7 +274,7 @@ async function readPiSessionSummary(candidate) {
277
274
  const { header, name, firstMessage } = projectedState;
278
275
  const version = header?.type === "session" && typeof header.version === "number" ? header.version : 1;
279
276
  const threadId = header?.type === "session" ? normalizeBoundedOptionalString(header.id, 256) : void 0;
280
- if (header && threadId && SESSION_ID_PATTERN$2.test(threadId)) {
277
+ if (header && threadId && SESSION_ID_PATTERN.test(threadId)) {
281
278
  const cwd = normalizeBoundedOptionalString(header.cwd, 4096);
282
279
  const createdAt = parsePiSessionTimestampMs(header.timestamp);
283
280
  summary = {
@@ -380,86 +377,8 @@ async function readPiSessionById(threadId, env) {
380
377
  }
381
378
  //#endregion
382
379
  //#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
380
  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
- }
381
+ const isExactPiSessionCursor = sessionCatalogPaging.isExactCursor;
463
382
  function textFromContent$1(content) {
464
383
  if (typeof content === "string") return content;
465
384
  if (!Array.isArray(content)) return "";
@@ -473,44 +392,20 @@ function textFromContent$1(content) {
473
392
  return [];
474
393
  }).join("\n");
475
394
  }
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
- }
395
+ const PI_PARAMETER_MESSAGES = {
396
+ listNotObject: "Pi session list parameters must be an object",
397
+ unknownListParameter: (key) => `unknown Pi session list parameter: ${key}`,
398
+ invalidSearchTerm: "searchTerm is invalid",
399
+ readNotObject: "Pi session read parameters must be an object",
400
+ unknownReadParameter: (key) => `unknown Pi session read parameter: ${key}`,
401
+ invalidThreadId: "threadId is invalid"
402
+ };
511
403
  async function listLocalPiSessionPage(value) {
512
- const params = parseListParams(value);
513
- const offset = decodeCursor(params.cursor);
404
+ const params = sessionCatalogPaging.parseListParams(value, {
405
+ searchMaxLength: MAX_SEARCH_LENGTH,
406
+ messages: PI_PARAMETER_MESSAGES
407
+ });
408
+ const offset = sessionCatalogPaging.decodeCursor(params.cursor);
514
409
  const { summaries, hasMore } = await listPiSummaryPage(process.env, {
515
410
  offset,
516
411
  limit: params.limit,
@@ -519,7 +414,7 @@ async function listLocalPiSessionPage(value) {
519
414
  const page = summaries.map(({ file: _file, version: _version, ...session }) => session);
520
415
  return {
521
416
  sessions: page,
522
- ...hasMore ? { nextCursor: encodeCursor(offset + page.length) } : {}
417
+ ...hasMore ? { nextCursor: sessionCatalogPaging.encodeCursor(offset + page.length) } : {}
523
418
  };
524
419
  }
525
420
  function isoTimestamp(message, entry) {
@@ -670,11 +565,16 @@ function piTranscriptItems(entries) {
670
565
  });
671
566
  }
672
567
  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);
568
+ const params = sessionCatalogPaging.parseReadParams(value, {
569
+ threadIdMaxLength: 256,
570
+ threadIdPattern: PI_SESSION_ID_PATTERN,
571
+ messages: PI_PARAMETER_MESSAGES
572
+ });
573
+ const offset = sessionCatalogPaging.decodeCursor(params.cursor);
574
+ const items = piTranscriptItems(await readPiSessionById(params.threadId, process.env));
575
+ const page = sessionCatalogPaging.boundTranscriptPage(items, params.limit, offset);
676
576
  return {
677
- hostId: LOCAL_HOST_ID$1,
577
+ hostId: PI_LOCAL_SESSION_HOST_ID,
678
578
  label: "Local Pi",
679
579
  threadId: params.threadId,
680
580
  ...page
@@ -798,201 +698,17 @@ async function checkPiUpstreamActivity(probes) {
798
698
  }
799
699
  //#endregion
800
700
  //#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
701
  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
702
  const ACPX_BACKEND_ID = "acpx";
815
703
  const PI_ACP_AGENT_ID = "pi";
816
704
  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
705
  async function requireLocalPiSession(threadId) {
990
- const record = (await listLocalPiSessionPage({
706
+ const session = (await listLocalPiSessionPage({
991
707
  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;
708
+ limit: 100
709
+ })).sessions.find((candidate) => candidate.threadId === threadId);
710
+ if (!session) throw new Error("Pi session is unavailable");
711
+ return session;
996
712
  }
997
713
  function currentPiCatalogConfig(api) {
998
714
  return api.runtime.config?.current?.() ?? api.config ?? {};
@@ -1013,8 +729,9 @@ function resolvePiContinuationAvailability(api) {
1013
729
  message: "Pi CLI is unavailable"
1014
730
  };
1015
731
  }
1016
- function listAdoptedPiSessions(api, sessionEntries) {
732
+ function listAdoptedPiSessions(api, agentId, sessionEntries) {
1017
733
  return listAdoptedSessionCatalogSessions({
734
+ ...agentId ? { agentId } : {},
1018
735
  config: currentPiCatalogConfig(api),
1019
736
  pluginId: api.id,
1020
737
  runtime: api.runtime,
@@ -1023,198 +740,148 @@ function listAdoptedPiSessions(api, sessionEntries) {
1023
740
  const acpx = isRecord(entry.pluginExtensions?.acpx) ? entry.pluginExtensions.acpx : void 0;
1024
741
  const marker = acpx && isRecord(acpx.piSessionCatalog) ? acpx.piSessionCatalog : void 0;
1025
742
  return marker && typeof marker.sourceThreadId === "string" ? {
1026
- hostId: LOCAL_HOST_ID,
743
+ hostId: PI_LOCAL_SESSION_HOST_ID,
1027
744
  threadId: marker.sourceThreadId
1028
745
  } : void 0;
1029
746
  }
1030
747
  });
1031
748
  }
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 } : {}
749
+ async function createAdoptedPiSession(params) {
750
+ const config = currentPiCatalogConfig(params.api);
751
+ const marker = { sourceThreadId: params.threadId };
752
+ return { sessionKey: (await params.api.runtime.agent.session.createSessionEntry({
753
+ cfg: config,
754
+ key: sessionCatalogAdoptedSessionKey(PI_ADOPTED_SESSION_KEY_PREFIX, params.threadId),
755
+ agentId: params.agentId,
756
+ recoverMatchingInitialEntry: true,
757
+ ...params.session.name ? { label: params.session.name } : {},
758
+ ...params.session.cwd ? { spawnedCwd: params.session.cwd } : {},
759
+ initialEntry: {
760
+ acpBackendId: ACPX_BACKEND_ID,
761
+ acpSessionBinding: {
762
+ acpAgentId: PI_ACP_AGENT_ID,
763
+ agentSessionId: params.threadId
1166
764
  },
1167
- timeoutMs: NODE_TIMEOUT_MS,
1168
- scopes: ["operator.write"]
1169
- })), request.threadId),
1170
- hostId: request.hostId,
1171
- label: nodeLabel(node)
1172
- };
765
+ pluginExtensions: { acpx: { piSessionCatalog: marker } }
766
+ },
767
+ afterCreate: async (entry) => {
768
+ await importSessionCatalogHistory({
769
+ catalogId: "pi",
770
+ threadId: params.threadId,
771
+ read: async ({ cursor, limit }) => await readLocalPiTranscriptPage({
772
+ threadId: params.threadId,
773
+ limit,
774
+ ...cursor ? { cursor } : {}
775
+ }),
776
+ sessionId: entry.sessionId,
777
+ sessionKey: entry.key,
778
+ agentId: entry.agentId,
779
+ ...params.session.cwd ? { cwd: params.session.cwd } : {},
780
+ config
781
+ });
782
+ return { pluginExtensions: { acpx: { piSessionCatalog: marker } } };
783
+ }
784
+ })).key };
1173
785
  }
1174
786
  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");
787
+ if (hostId === "gateway" && allowProcessHomeFallback === false && piSessionStore(process.env).usesProcessHomeFallback) throw new Error("local Pi sessions are unavailable in isolated state");
1176
788
  }
1177
- async function listPiSessions(paramsJSON) {
1178
- return JSON.stringify(await listLocalPiSessionPage(parseNodeParams(paramsJSON)));
789
+ async function listPiSessions(params) {
790
+ return await listLocalPiSessionPage(params);
1179
791
  }
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));
792
+ async function readPiSession(params) {
793
+ return await readLocalPiTranscriptPage(params);
1200
794
  }
1201
795
  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);
796
+ return createSessionCatalogFamily({
797
+ runtime: api.runtime,
798
+ local: {
799
+ hostId: PI_LOCAL_SESSION_HOST_ID,
800
+ label: "Local Pi",
801
+ available: (query) => {
802
+ const store = piSessionStore(process.env);
803
+ return (query.allowProcessHomeFallback !== false || !store.usesProcessHomeFallback) && piSessionStoreAvailable(process.env, store);
804
+ },
805
+ list: async (query) => await listLocalPiSessionPage({
806
+ limit: query.limitPerHost,
807
+ ...query.search ? { searchTerm: query.search } : {},
808
+ cursor: query.cursors?.[PI_LOCAL_SESSION_HOST_ID]
809
+ }),
810
+ read: async (request) => await readLocalPiTranscriptPage({
811
+ threadId: request.threadId,
812
+ ...request.limit ? { limit: request.limit } : {},
813
+ ...request.cursor !== void 0 ? { cursor: request.cursor } : {}
814
+ }),
815
+ assertAccess: assertPiLocalAccess
1208
816
  },
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
- };
817
+ node: {
818
+ listCommand: PI_SESSIONS_LIST_COMMAND,
819
+ readCommand: PI_SESSION_READ_COMMAND,
820
+ terminalCommand: PI_TERMINAL_RESUME_COMMAND,
821
+ timeoutMs: NODE_TIMEOUT_MS,
822
+ maxHosts: 100,
823
+ maxPageLimit: 100,
824
+ sessionIdPattern: PI_SESSION_ID_PATTERN
825
+ },
826
+ capabilities: {
827
+ local: () => ({
828
+ canContinue: resolvePiContinuationAvailability(api).available,
829
+ canOpenTerminal: resolveNodeHostExecutable("pi", {
830
+ env: process.env,
831
+ pathEnv: process.env.PATH ?? "",
832
+ strategy: "fallback"
833
+ }) !== void 0
834
+ }),
835
+ node: (node) => {
836
+ return {
837
+ canContinue: false,
838
+ canOpenTerminal: (node.invocableCommands ?? node.commands)?.includes(PI_TERMINAL_RESUME_COMMAND) === true
839
+ };
840
+ },
841
+ project: (session, capabilities) => ({
842
+ ...session,
843
+ canContinue: capabilities.canContinue && session.canContinue,
844
+ canOpenTerminal: capabilities.canOpenTerminal
845
+ })
846
+ },
847
+ messages: {
848
+ invalidNodeCursor: "Pi node returned an invalid cursor",
849
+ invalidNodeSessionPage: "Pi node returned an invalid session page",
850
+ invalidNodeTranscriptPage: "Pi node returned an invalid transcript page",
851
+ invalidHostId: "Pi session catalog hostId is invalid",
852
+ localReadFailed: "Local Pi sessions are unavailable",
853
+ nodeInvokeFailed: "Paired node Pi sessions are unavailable",
854
+ nodeReadUnavailable: "paired-node Pi session host is unavailable",
855
+ nodeTerminalUnavailable: "paired-node Pi terminal is unavailable",
856
+ sessionUnavailable: "Pi session is unavailable"
857
+ },
858
+ continuation: {
859
+ resolveAgentId: (agentId) => resolveSessionAgentIds({
860
+ config: api.config,
861
+ agentId
862
+ }).sessionAgentId,
863
+ availability: () => resolvePiContinuationAvailability(api),
864
+ listAdopted: (agentId, sessionEntries) => listAdoptedPiSessions(api, agentId, sessionEntries),
865
+ loadSession: requireLocalPiSession,
866
+ validateSession: (session) => {
867
+ if (!session.canContinue) throw new Error("Pi session is outside the session store supported by pi-acp");
868
+ },
869
+ create: async (params) => await createAdoptedPiSession({
870
+ api,
871
+ ...params
872
+ }),
873
+ complete: async (continued, threadId) => await linkContinuedPiSession(continued.sessionKey, threadId),
874
+ nodeReadOnlyMessage: "paired-node Pi session rows are view-only"
875
+ },
876
+ terminal: {
877
+ executable: "pi",
878
+ args: (threadId) => ["--session", threadId],
879
+ title: (threadId) => `pi --session ${threadId.slice(0, 12)}…`,
880
+ requireLocalSession: requireLocalPiSession,
881
+ unavailableMessage: "Pi CLI is unavailable"
882
+ },
883
+ checkUpstreamActivity: (probes, policy) => checkPiUpstreamActivity(probes.filter((probe) => probe.hostId !== "gateway" || policy?.allowProcessHomeFallback !== false || !piSessionStore(process.env).usesProcessHomeFallback))
884
+ }, isExactPiSessionCursor);
1218
885
  }
1219
886
  //#endregion
1220
- export { createPiSessionCatalogRuntime, listPiSessions, readPiSession, resumePiSession };
887
+ export { createPiSessionCatalogRuntime, listPiSessions, readPiSession, requireLocalPiSession };
@@ -6,6 +6,9 @@ import path from "node:path";
6
6
  const PI_SESSIONS_LIST_COMMAND = "acpx.pi.sessions.list.v1";
7
7
  const PI_SESSION_READ_COMMAND = "acpx.pi.sessions.read.v1";
8
8
  const PI_TERMINAL_RESUME_COMMAND = "acpx.pi.terminal.resume.v1";
9
+ const PI_SESSIONS_CAPABILITY = "pi-sessions";
10
+ const PI_LOCAL_SESSION_HOST_ID = "gateway";
11
+ const PI_SESSION_ID_PATTERN = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
9
12
  //#endregion
10
13
  //#region extensions/acpx/src/pi-session-paths.ts
11
14
  function piHome(env) {
@@ -78,4 +81,4 @@ function piSessionStoreAvailable(env, store) {
78
81
  }
79
82
  }
80
83
  //#endregion
81
- export { PI_SESSION_READ_COMMAND as a, PI_SESSIONS_LIST_COMMAND as i, piSessionStore as n, PI_TERMINAL_RESUME_COMMAND as o, piSessionStoreAvailable as r, piAcpSessionStoreRoot as t };
84
+ export { PI_SESSIONS_CAPABILITY as a, PI_SESSION_READ_COMMAND as c, PI_LOCAL_SESSION_HOST_ID as i, PI_TERMINAL_RESUME_COMMAND as l, piSessionStore as n, PI_SESSIONS_LIST_COMMAND as o, piSessionStoreAvailable as r, PI_SESSION_ID_PATTERN as s, piAcpSessionStoreRoot as t };
@@ -1,12 +1,12 @@
1
1
  import { t as AcpxPluginConfigSchema } from "./config-schema-DN_uAi4R.js";
2
2
  import { u as readAcpxProcessLeaseIdentity, v as splitCommandParts } from "./process-lease-Cwvj7WGe.js";
3
3
  import { createRequire } from "node:module";
4
- import { formatPluginConfigIssue } from "openclaw/plugin-sdk/extension-shared";
5
4
  import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
6
5
  import fs from "node:fs";
7
6
  import path from "node:path";
8
- import { runExec } from "openclaw/plugin-sdk/process-runtime";
7
+ import { isPidAlive, runExec } from "openclaw/plugin-sdk/process-runtime";
9
8
  import { fileURLToPath } from "node:url";
9
+ import { formatPluginConfigIssue } from "openclaw/plugin-sdk/extension-shared";
10
10
  //#region extensions/acpx/src/codex-adapter.ts
11
11
  const CODEX_ACP_PACKAGE = "@agentclientprotocol/codex-acp";
12
12
  const CODEX_ACP_BIN = "codex-acp";
@@ -355,14 +355,6 @@ function collectProcessTree(processes, rootPid) {
355
355
  function uniquePids(processes) {
356
356
  return Array.from(new Set(processes.map((processInfo) => processInfo.pid).filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid)));
357
357
  }
358
- function isProcessAlive(pid) {
359
- try {
360
- process.kill(pid, 0);
361
- return true;
362
- } catch {
363
- return false;
364
- }
365
- }
366
358
  async function terminatePids(pids, deps) {
367
359
  const killProcess = deps?.killProcess ?? ((pid, signal) => process.kill(pid, signal));
368
360
  const sleep = deps?.sleep ?? ((ms) => new Promise((resolve) => {
@@ -375,7 +367,7 @@ async function terminatePids(pids, deps) {
375
367
  } catch {}
376
368
  if (terminated.length === 0) return terminated;
377
369
  await sleep(750);
378
- for (const pid of terminated) if (deps?.killProcess || isProcessAlive(pid)) try {
370
+ for (const pid of terminated) if (deps?.killProcess || isPidAlive(pid)) try {
379
371
  killProcess(pid, "SIGKILL");
380
372
  } catch {}
381
373
  return terminated;
@@ -1,137 +1,14 @@
1
1
  import { getAcpRuntimeBackend, registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "openclaw/plugin-sdk/acp-runtime-backend";
2
2
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
3
- import { toErrorObject } from "openclaw/plugin-sdk/error-runtime";
4
- import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
5
- //#region extensions/acpx/src/runtime-turn.ts
6
- /**
7
- * ACPX turn adapters. Modern runtimes can expose startTurn directly; legacy
8
- * runtimes that only stream runTurn events are adapted to the newer contract.
9
- */
10
- function isCancellationStopReason(stopReason) {
11
- return stopReason === "cancel" || stopReason === "cancelled" || stopReason === "manual-cancel";
12
- }
13
- var LegacyRunTurnEventQueue = class {
14
- constructor() {
15
- this.items = [];
16
- this.waits = [];
17
- this.closed = false;
18
- }
19
- push(item) {
20
- if (this.closed) return;
21
- const waiter = this.waits.shift();
22
- if (waiter) {
23
- waiter.resolve(item);
24
- return;
25
- }
26
- this.items.push(item);
27
- }
28
- clear() {
29
- this.items.length = 0;
30
- }
31
- close() {
32
- if (this.closed) return;
33
- this.closed = true;
34
- for (const waiter of this.waits.splice(0)) waiter.resolve(null);
35
- }
36
- fail(error) {
37
- if (this.closed) return;
38
- this.error = error;
39
- this.closed = true;
40
- for (const waiter of this.waits.splice(0)) waiter.reject(error);
41
- }
42
- async next() {
43
- const item = this.items.shift();
44
- if (item) return item;
45
- if (this.error) throw toErrorObject(this.error, "Non-Error thrown");
46
- if (this.closed) return null;
47
- return await new Promise((resolve, reject) => {
48
- this.waits.push({
49
- resolve,
50
- reject
51
- });
52
- });
53
- }
54
- async *iterate() {
55
- for (;;) {
56
- const item = await this.next();
57
- if (!item) return;
58
- yield item;
59
- }
60
- }
61
- };
62
- function legacyRunTurnAsStartTurn(runtime, input) {
63
- const result = createDeferred();
64
- result.promise.catch(() => {});
65
- const queue = new LegacyRunTurnEventQueue();
66
- let resultSettled = false;
67
- const settleResult = (next) => {
68
- if (resultSettled) return;
69
- resultSettled = true;
70
- result.resolve(next);
71
- };
72
- (async () => {
73
- try {
74
- for await (const event of runtime.runTurn(input)) {
75
- if (event.type === "done") {
76
- settleResult({
77
- status: event.status ?? (isCancellationStopReason(event.stopReason) ? "cancelled" : "completed"),
78
- ...event.stopReason ? { stopReason: event.stopReason } : {}
79
- });
80
- continue;
81
- }
82
- if (event.type === "error") {
83
- settleResult({
84
- status: "failed",
85
- error: {
86
- message: event.message,
87
- ...event.code ? { code: event.code } : {},
88
- ...event.detailCode ? { detailCode: event.detailCode } : {},
89
- ...event.retryable === void 0 ? {} : { retryable: event.retryable }
90
- }
91
- });
92
- continue;
93
- }
94
- queue.push(event);
95
- }
96
- settleResult({
97
- status: "failed",
98
- error: {
99
- code: "ACP_TURN_FAILED",
100
- message: "ACP turn ended without a terminal done event."
101
- }
102
- });
103
- } catch (error) {
104
- result.reject(error);
105
- queue.fail(error);
106
- return;
107
- }
108
- queue.close();
109
- })();
110
- return {
111
- requestId: input.requestId,
112
- events: queue.iterate(),
113
- result: result.promise,
114
- async cancel(inputArgs) {
115
- await runtime.cancel({
116
- handle: input.handle,
117
- reason: inputArgs?.reason
118
- });
119
- },
120
- async closeStream() {
121
- queue.clear();
122
- queue.close();
123
- }
124
- };
125
- }
126
- /** Start an ACP turn, adapting legacy runTurn-only runtimes when needed. */
127
- function startRuntimeTurn(runtime, input) {
128
- return runtime.startTurn?.(input) ?? legacyRunTurnAsStartTurn(runtime, input);
129
- }
130
- /** Start an ACP turn through a lazy runtime resolver. */
3
+ //#region extensions/acpx/src/runtime-proxy.ts
4
+ /** Start an ACP turn through a lazy runtime resolver without awaiting resolution up front. */
131
5
  function lazyStartRuntimeTurn(resolveRuntime, input) {
132
- const turnPromise = resolveRuntime().then((runtime) => startRuntimeTurn(runtime, input));
6
+ const turnPromise = resolveRuntime().then((runtime) => runtime.startTurn(input));
133
7
  return {
134
8
  requestId: input.requestId,
9
+ get promptStarted() {
10
+ return turnPromise.then((turn) => turn.promptStarted);
11
+ },
135
12
  events: { async *[Symbol.asyncIterator]() {
136
13
  yield* (await turnPromise).events;
137
14
  } },
@@ -144,8 +21,6 @@ function lazyStartRuntimeTurn(resolveRuntime, input) {
144
21
  }
145
22
  };
146
23
  }
147
- //#endregion
148
- //#region extensions/acpx/src/runtime-proxy.ts
149
24
  /** Create an ACP runtime facade backed by an async runtime resolver. */
150
25
  function createLazyAcpRuntimeProxy(resolveRuntime) {
151
26
  return {
@@ -159,25 +34,22 @@ function createLazyAcpRuntimeProxy(resolveRuntime) {
159
34
  yield* (await resolveRuntime()).runTurn(input);
160
35
  },
161
36
  async getCapabilities(input) {
162
- return await (await resolveRuntime()).getCapabilities?.(input) ?? { controls: [] };
37
+ return await (await resolveRuntime()).getCapabilities(input);
163
38
  },
164
39
  async getStatus(input) {
165
- return await (await resolveRuntime()).getStatus?.(input) ?? {};
40
+ return await (await resolveRuntime()).getStatus(input);
166
41
  },
167
42
  async setMode(input) {
168
- await (await resolveRuntime()).setMode?.(input);
43
+ await (await resolveRuntime()).setMode(input);
169
44
  },
170
45
  async setConfigOption(input) {
171
- await (await resolveRuntime()).setConfigOption?.(input);
46
+ await (await resolveRuntime()).setConfigOption(input);
172
47
  },
173
48
  async doctor() {
174
- return await (await resolveRuntime()).doctor?.() ?? {
175
- ok: true,
176
- message: "ok"
177
- };
49
+ return await (await resolveRuntime()).doctor();
178
50
  },
179
51
  async prepareFreshSession(input) {
180
- await (await resolveRuntime()).prepareFreshSession?.(input);
52
+ await (await resolveRuntime()).prepareFreshSession(input);
181
53
  },
182
54
  async cancel(input) {
183
55
  await (await resolveRuntime()).cancel(input);
@@ -194,7 +66,7 @@ function createLazyAcpRuntimeProxy(resolveRuntime) {
194
66
  * immediately, then imports the heavier service only when a session needs it.
195
67
  */
196
68
  const ACPX_BACKEND_ID = "acpx";
197
- const loadServiceModule = createLazyRuntimeModule(() => import("./service-LTeZyc7q.js"));
69
+ const loadServiceModule = createLazyRuntimeModule(() => import("./service-PRlUtXMf.js"));
198
70
  function unregisterOwnedRuntime(runtime) {
199
71
  if (runtime && getAcpRuntimeBackend(ACPX_BACKEND_ID)?.runtime === runtime) unregisterAcpRuntimeBackend(ACPX_BACKEND_ID);
200
72
  }
@@ -1,2 +1,2 @@
1
- import { t as createAcpxRuntimeService } from "./register.runtime-C29AY8LI.js";
1
+ import { t as createAcpxRuntimeService } from "./register.runtime-Dj29TKFy.js";
2
2
  export { createAcpxRuntimeService };
@@ -1,6 +1,6 @@
1
1
  import { d as withAcpxLeaseEnvironment, i as createAcpxProcessLeaseId, o as hashAcpxProcessCommand, t as ACPX_PROBE_LEASE_SESSION_KEY, u as readAcpxProcessLeaseIdentity, v as splitCommandParts } from "./process-lease-Cwvj7WGe.js";
2
2
  import { AcpRuntimeError } from "./runtime-api.js";
3
- import { d as OPENCLAW_CODEX_CONFIG_ARG, l as CODEX_ACP_PACKAGE, n as cleanupOpenClawOwnedAcpxProcessTree, r as isOpenClawLeaseAwareAcpxProcessCommand, t as cleanupOpenClawOwnedAcpxPendingLease } from "./process-reaper-DzVuCxl3.js";
3
+ import { d as OPENCLAW_CODEX_CONFIG_ARG, l as CODEX_ACP_PACKAGE, n as cleanupOpenClawOwnedAcpxProcessTree, r as isOpenClawLeaseAwareAcpxProcessCommand, t as cleanupOpenClawOwnedAcpxPendingLease } from "./process-reaper-DduWm_7N.js";
4
4
  import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
5
5
  import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
6
6
  import path, { resolve } from "node:path";
@@ -940,6 +940,9 @@ var AcpxRuntime = class {
940
940
  });
941
941
  return {
942
942
  requestId: input.requestId,
943
+ get promptStarted() {
944
+ return turnPromise.then(({ turn }) => turn.promptStarted);
945
+ },
943
946
  events: { async *[Symbol.asyncIterator]() {
944
947
  const { command, turn } = await turnPromise;
945
948
  try {
@@ -1,11 +1,10 @@
1
- import { n as createLazyAcpRuntimeProxy } from "./register.runtime-C29AY8LI.js";
1
+ import { n as createLazyAcpRuntimeProxy } from "./register.runtime-Dj29TKFy.js";
2
2
  import "./config-schema-DN_uAi4R.js";
3
3
  import { _ as quoteCommandPart, a as createAcpxProcessLeaseStore, f as ACPX_GATEWAY_INSTANCE_KEY, g as normalizeAcpxGatewayInstanceRecord, l as openAcpxProcessLeaseStateStore, n as OPENCLAW_ACPX_LEASE_ID_ARG, p as ACPX_GATEWAY_INSTANCE_NAMESPACE, r as OPENCLAW_GATEWAY_INSTANCE_ID_ARG, v as splitCommandParts } from "./process-lease-Cwvj7WGe.js";
4
- import { a as resolveAcpxPluginConfig, c as CODEX_ACP_BIN, d as OPENCLAW_CODEX_CONFIG_ARG, i as reapStaleOpenClawOwnedAcpxOrphans, l as CODEX_ACP_PACKAGE, n as cleanupOpenClawOwnedAcpxProcessTree, o as resolveAcpxPluginRoot, s as toAcpMcpServers, t as cleanupOpenClawOwnedAcpxPendingLease, u as LEGACY_CODEX_ACP_PACKAGE } from "./process-reaper-DzVuCxl3.js";
4
+ import { a as resolveAcpxPluginConfig, c as CODEX_ACP_BIN, d as OPENCLAW_CODEX_CONFIG_ARG, i as reapStaleOpenClawOwnedAcpxOrphans, l as CODEX_ACP_PACKAGE, n as cleanupOpenClawOwnedAcpxProcessTree, o as resolveAcpxPluginRoot, s as toAcpMcpServers, t as cleanupOpenClawOwnedAcpxPendingLease, u as LEGACY_CODEX_ACP_PACKAGE } from "./process-reaper-DduWm_7N.js";
5
5
  import { createRequire } from "node:module";
6
6
  import { finiteSecondsToTimerSafeMilliseconds } from "openclaw/plugin-sdk/number-runtime";
7
7
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
8
- import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
9
8
  import { isRecord, normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
10
9
  import fs from "node:fs";
11
10
  import os from "node:os";
@@ -13,6 +12,7 @@ import path from "node:path";
13
12
  import fs$1 from "node:fs/promises";
14
13
  import { randomUUID } from "node:crypto";
15
14
  import { inspect } from "node:util";
15
+ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
16
16
  import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
17
17
  import { parse, stringify } from "smol-toml";
18
18
  //#region extensions/acpx/src/codex-trust-config.ts
@@ -1021,7 +1021,7 @@ async function prepareAcpxCodexAuthConfig(params) {
1021
1021
  */
1022
1022
  const ENABLE_STARTUP_PROBE_ENV = "OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE";
1023
1023
  const SKIP_RUNTIME_PROBE_ENV = "OPENCLAW_SKIP_ACPX_RUNTIME_PROBE";
1024
- const loadRuntimeModule = createLazyRuntimeModule(() => import("./runtime-BmzHUTK8.js"));
1024
+ const loadRuntimeModule = createLazyRuntimeModule(() => import("./runtime-BmOxa15I.js"));
1025
1025
  /** Convert ACPX timeout seconds into timer-safe milliseconds. */
1026
1026
  function resolveAcpxTimerTimeoutMs(timeoutSeconds) {
1027
1027
  if (timeoutSeconds === void 0) return;
@@ -1046,6 +1046,7 @@ function createLazyDefaultRuntime(params) {
1046
1046
  openclawToolsMcpBridgeEnabled: params.pluginConfig.openClawToolsMcpBridge,
1047
1047
  permissionMode: params.pluginConfig.permissionMode,
1048
1048
  nonInteractivePermissions: params.pluginConfig.nonInteractivePermissions,
1049
+ elicitationModes: ["form", "url"],
1049
1050
  timeoutMs: resolveAcpxTimerTimeoutMs(params.pluginConfig.timeoutSeconds)
1050
1051
  });
1051
1052
  return runtime;
@@ -1260,7 +1261,7 @@ function createAcpxRuntimeService(params) {
1260
1261
  ctx.logger.info("embedded acpx runtime backend ready");
1261
1262
  return;
1262
1263
  }
1263
- const doctorReport = await measureAcpxStartup(ctx, "probe.doctor", () => startedRuntime.doctor?.());
1264
+ const doctorReport = await measureAcpxStartup(ctx, "probe.doctor", () => startedRuntime.doctor());
1264
1265
  if (currentRevision !== lifecycleRevision) return;
1265
1266
  detailAcpxStartup(ctx, "probe.result", [["healthyCount", 0]]);
1266
1267
  ctx.logger.warn(`embedded acpx runtime backend probe failed: ${doctorReport ? formatDoctorFailureMessage(doctorReport) : "backend remained unhealthy after probe"}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/acpx",
3
- "version": "2026.8.1-beta.2",
3
+ "version": "2026.9.1-beta.1",
4
4
  "description": "OpenClaw ACP runtime backend with plugin-owned session and transport management.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,10 +8,10 @@
8
8
  },
9
9
  "type": "module",
10
10
  "dependencies": {
11
- "@agentclientprotocol/claude-agent-acp": "0.62.0",
12
- "@agentclientprotocol/codex-acp": "1.1.7",
13
- "acpx": "0.13.0",
14
- "smol-toml": "1.7.1",
11
+ "@agentclientprotocol/claude-agent-acp": "0.70.0",
12
+ "@agentclientprotocol/codex-acp": "1.6.0",
13
+ "acpx": "0.13.1",
14
+ "smol-toml": "1.8.0",
15
15
  "zod": "4.4.3"
16
16
  },
17
17
  "devDependencies": {
@@ -43,10 +43,10 @@
43
43
  ]
44
44
  },
45
45
  "compat": {
46
- "pluginApi": ">=2026.8.1-beta.2"
46
+ "pluginApi": ">=2026.9.1-beta.1"
47
47
  },
48
48
  "build": {
49
- "openclawVersion": "2026.8.1-beta.2",
49
+ "openclawVersion": "2026.9.1-beta.1",
50
50
  "staticAssets": [
51
51
  {
52
52
  "source": "./src/runtime-internals/mcp-proxy.mjs",
@@ -74,7 +74,7 @@
74
74
  "skills/**"
75
75
  ],
76
76
  "peerDependencies": {
77
- "openclaw": ">=2026.8.1-beta.2"
77
+ "openclaw": ">=2026.9.1-beta.1"
78
78
  },
79
79
  "peerDependenciesMeta": {
80
80
  "openclaw": {
@@ -209,8 +209,8 @@ ${ACPX_CMD} codex sessions close oc-codex-<conversationId>
209
209
  Defaults are:
210
210
 
211
211
  - `openclaw -> openclaw acp`
212
- - `claude -> bundled @agentclientprotocol/claude-agent-acp@0.55.0`
213
- - `codex -> bundled @agentclientprotocol/codex-acp@1.1.2 through OpenClaw's isolated CODEX_HOME wrapper`
212
+ - `claude -> bundled @agentclientprotocol/claude-agent-acp@0.70.0`
213
+ - `codex -> bundled @agentclientprotocol/codex-acp@1.6.0 through OpenClaw's isolated CODEX_HOME wrapper`
214
214
  - `copilot -> copilot --acp --stdio`
215
215
  - `cursor -> cursor-agent acp`
216
216
  - `droid -> droid exec --output-format acp`