@openclaw/acpx 2026.7.2-beta.4 → 2026.7.2-beta.5

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
@@ -3,7 +3,10 @@ import "./config-schema-lrk5nlcV.js";
3
3
  import { tryDispatchAcpReplyHook } from "openclaw/plugin-sdk/acp-runtime-backend";
4
4
  import { finiteSecondsToTimerSafeMilliseconds } from "openclaw/plugin-sdk/number-runtime";
5
5
  import process$1 from "node:process";
6
+ import { resolveAcpSessionAvailability } from "openclaw/plugin-sdk/acp-runtime";
7
+ import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
6
8
  import { decodeNodePtyResumeParams, resolveNodeHostExecutable, runNodePtyCommand } from "openclaw/plugin-sdk/node-host";
9
+ import { createSessionCatalogAdoptionCoordinator, importSessionCatalogHistory, isExternalUserText, listAdoptedSessionCatalogSessions, sessionCatalogAdoptedSessionKey, sessionCatalogAdoptedSourceKey } from "openclaw/plugin-sdk/session-catalog";
7
10
  import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
8
11
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
9
12
  import { createReadStream, readFileSync, statSync } from "node:fs";
@@ -67,6 +70,13 @@ function piSessionStore(env, cwd = process.cwd()) {
67
70
  flat: false
68
71
  };
69
72
  }
73
+ /** Store root scanned by pi-acp@0.0.26 when resolving a native session id. */
74
+ function piAcpSessionStoreRoot(env) {
75
+ const configuredAgentDir = env.PI_CODING_AGENT_DIR?.trim();
76
+ if (configuredAgentDir && !isPiSessionCatalogPathAbsolute(configuredAgentDir)) return;
77
+ const agentDir = configuredAgentDir ? path.resolve(configuredAgentDir) : path.join(piHome(env), ".pi", "agent");
78
+ return path.join(agentDir, "sessions");
79
+ }
70
80
  function piSessionStoreAvailable(env) {
71
81
  try {
72
82
  return statSync(piSessionStore(env).root).isDirectory();
@@ -114,9 +124,10 @@ function optionalString(value, maxLength) {
114
124
  }
115
125
  async function discoverPiSessionFiles(env) {
116
126
  const store = piSessionStore(env);
127
+ const resolvedRoot = await realpathOrResolve(store.root);
117
128
  let entries;
118
129
  try {
119
- entries = await fs$1.readdir(store.root, { withFileTypes: true });
130
+ entries = await fs$1.readdir(resolvedRoot, { withFileTypes: true });
120
131
  } catch {
121
132
  return {
122
133
  root: store.root,
@@ -125,12 +136,12 @@ async function discoverPiSessionFiles(env) {
125
136
  }
126
137
  if (store.flat) return {
127
138
  root: store.root,
128
- files: entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).slice(0, MAX_DISCOVERY_FILES).map((entry) => path.join(store.root, entry.name))
139
+ files: entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).slice(0, MAX_DISCOVERY_FILES).map((entry) => path.join(resolvedRoot, entry.name))
129
140
  };
130
141
  const files = [];
131
142
  for (const entry of entries) {
132
143
  if (!entry.isDirectory() || files.length >= MAX_DISCOVERY_FILES) continue;
133
- const directory = path.join(store.root, entry.name);
144
+ const directory = path.join(resolvedRoot, entry.name);
134
145
  let children;
135
146
  try {
136
147
  children = await fs$1.readdir(directory, { withFileTypes: true });
@@ -147,6 +158,13 @@ async function discoverPiSessionFiles(env) {
147
158
  files
148
159
  };
149
160
  }
161
+ async function realpathOrResolve(value) {
162
+ try {
163
+ return await fs$1.realpath(value);
164
+ } catch {
165
+ return path.resolve(value);
166
+ }
167
+ }
150
168
  async function mapConcurrent(values, limit, mapper) {
151
169
  const results = [];
152
170
  results.length = values.length;
@@ -162,6 +180,8 @@ async function mapConcurrent(values, limit, mapper) {
162
180
  }
163
181
  async function piFileCandidates(env) {
164
182
  const { root, files } = await discoverPiSessionFiles(env);
183
+ const configuredAcpRoot = piAcpSessionStoreRoot(env);
184
+ const acpRoot = configuredAcpRoot ? await realpathOrResolve(configuredAcpRoot) : void 0;
165
185
  return (await mapConcurrent(files, IO_CONCURRENCY, async (file) => {
166
186
  try {
167
187
  const stats = await fs$1.stat(file);
@@ -170,13 +190,18 @@ async function piFileCandidates(env) {
170
190
  storeRoot: root,
171
191
  identity: `${String(stats.dev)}:${String(stats.ino)}:${String(stats.birthtimeMs)}`,
172
192
  mtimeMs: stats.mtimeMs,
173
- size: stats.size
193
+ size: stats.size,
194
+ resumable: acpRoot ? pathIsWithin(acpRoot, file) : false
174
195
  } : void 0;
175
196
  } catch {
176
197
  return;
177
198
  }
178
199
  })).filter((candidate) => candidate !== void 0).toSorted((left, right) => right.mtimeMs - left.mtimeMs);
179
200
  }
201
+ function pathIsWithin(root, candidate) {
202
+ const relative = path.relative(root, candidate);
203
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
204
+ }
180
205
  function parsePiJsonLines(content) {
181
206
  return content.split(/\r?\n/u).flatMap((line) => {
182
207
  if (!line.trim()) return [];
@@ -188,12 +213,12 @@ function parsePiJsonLines(content) {
188
213
  }
189
214
  });
190
215
  }
191
- function textFromContent$1(content) {
216
+ function textFromContent$2(content) {
192
217
  if (typeof content === "string") return content;
193
218
  if (!Array.isArray(content)) return "";
194
219
  return content.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n");
195
220
  }
196
- function timestampMs$1(value) {
221
+ function timestampMs$2(value) {
197
222
  if (typeof value === "number" && Number.isFinite(value)) return value;
198
223
  if (typeof value === "string") {
199
224
  const parsed = Date.parse(value);
@@ -212,7 +237,7 @@ function processSummaryLine(state, line) {
212
237
  return;
213
238
  }
214
239
  if (entry.type === "session_info") state.name = optionalString(entry.name, 1e3);
215
- else if (!state.firstMessage && entry.type === "message" && isRecord(entry.message) && entry.message.role === "user") state.firstMessage = optionalString(textFromContent$1(entry.message.content), 1e3);
240
+ else if (!state.firstMessage && entry.type === "message" && isRecord(entry.message) && entry.message.role === "user") state.firstMessage = optionalString(textFromContent$2(entry.message.content), 1e3);
216
241
  }
217
242
  function appendSummaryBytes(state, bytes) {
218
243
  if (state.discarding || bytes.length === 0) return;
@@ -274,7 +299,10 @@ async function readPiSessionSummary(candidate) {
274
299
  if (cached?.mtimeMs === candidate.mtimeMs && cached.size === candidate.size) {
275
300
  summaryCache.delete(candidate.file);
276
301
  summaryCache.set(candidate.file, cached);
277
- return cached.summary;
302
+ return cached.summary ? {
303
+ ...cached.summary,
304
+ canContinue: candidate.resumable
305
+ } : cached.summary;
278
306
  }
279
307
  let summary;
280
308
  let scanState;
@@ -297,12 +325,14 @@ async function readPiSessionSummary(candidate) {
297
325
  };
298
326
  if (!projectedState.discarding && projectedState.pending.length > 0) processSummaryLine(projectedState, projectedState.pending);
299
327
  const { header, name, firstMessage } = projectedState;
328
+ const version = header?.type === "session" && typeof header.version === "number" ? header.version : 1;
300
329
  const threadId = header?.type === "session" ? optionalString(header.id, 256) : void 0;
301
330
  if (header && threadId && SESSION_ID_PATTERN$2.test(threadId)) {
302
331
  const cwd = optionalString(header.cwd, 4096);
303
- const createdAt = timestampMs$1(header.timestamp);
332
+ const createdAt = timestampMs$2(header.timestamp);
304
333
  summary = {
305
334
  file: candidate.file,
335
+ version,
306
336
  threadId,
307
337
  ...name || firstMessage ? { name: name ?? firstMessage } : {},
308
338
  ...cwd ? { cwd } : {},
@@ -313,7 +343,7 @@ async function readPiSessionSummary(candidate) {
313
343
  source: "pi-cli",
314
344
  modelProvider: "pi",
315
345
  archived: false,
316
- canContinue: false,
346
+ canContinue: candidate.resumable,
317
347
  canArchive: false
318
348
  };
319
349
  }
@@ -364,6 +394,19 @@ async function findPiSummary(threadId, env) {
364
394
  if (match) return match;
365
395
  }
366
396
  }
397
+ async function readPiSessionFileBaseline(threadId, env) {
398
+ const summary = await findPiSummary(threadId, env);
399
+ if (!summary?.canContinue || summary.version < 3) return;
400
+ try {
401
+ const stats = await fs$1.stat(summary.file);
402
+ return stats.isFile() ? {
403
+ filePath: summary.file,
404
+ offset: stats.size
405
+ } : void 0;
406
+ } catch {
407
+ return;
408
+ }
409
+ }
367
410
  async function readPiSessionById(threadId, env) {
368
411
  const cacheKey = threadCacheKey(piSessionStore(env).root, threadId);
369
412
  let file = threadFileCache.get(cacheKey);
@@ -472,7 +515,7 @@ function transcriptPage(items, limit, offset) {
472
515
  ...consumed < items.length ? { nextCursor: encodeCursor(consumed) } : {}
473
516
  };
474
517
  }
475
- function textFromContent(content) {
518
+ function textFromContent$1(content) {
476
519
  if (typeof content === "string") return content;
477
520
  if (!Array.isArray(content)) return "";
478
521
  return content.flatMap((part) => {
@@ -485,7 +528,7 @@ function textFromContent(content) {
485
528
  return [];
486
529
  }).join("\n");
487
530
  }
488
- function timestampMs(value) {
531
+ function timestampMs$1(value) {
489
532
  if (typeof value === "number" && Number.isFinite(value)) return value;
490
533
  if (typeof value === "string") {
491
534
  const parsed = Date.parse(value);
@@ -535,14 +578,14 @@ async function listLocalPiSessionPage(value) {
535
578
  limit: params.limit,
536
579
  ...params.searchTerm ? { searchTerm: params.searchTerm } : {}
537
580
  });
538
- const page = summaries.map(({ file: _file, ...session }) => session);
581
+ const page = summaries.map(({ file: _file, version: _version, ...session }) => session);
539
582
  return {
540
583
  sessions: page,
541
584
  ...hasMore ? { nextCursor: encodeCursor(offset + page.length) } : {}
542
585
  };
543
586
  }
544
587
  function isoTimestamp(message, entry) {
545
- const value = timestampMs(message.timestamp) ?? timestampMs(entry.timestamp);
588
+ const value = timestampMs$1(message.timestamp) ?? timestampMs$1(entry.timestamp);
546
589
  if (value === void 0) return;
547
590
  const date = new Date(value);
548
591
  return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
@@ -588,7 +631,7 @@ function piMessageItems(entry) {
588
631
  ...modelRef ? { model: modelRef } : {}
589
632
  };
590
633
  if (role === "user") {
591
- const text = textFromContent(message.content);
634
+ const text = textFromContent$1(message.content);
592
635
  return text ? [{
593
636
  ...common,
594
637
  type: "userMessage",
@@ -597,7 +640,7 @@ function piMessageItems(entry) {
597
640
  }
598
641
  if (role === "toolResult") {
599
642
  const toolName = optionalPiString(message.toolName, 256);
600
- const text = textFromContent(message.content);
643
+ const text = textFromContent$1(message.content);
601
644
  return [{
602
645
  ...common,
603
646
  type: "toolResult",
@@ -622,7 +665,7 @@ function piMessageItems(entry) {
622
665
  if (role === "custom" || role === "hookMessage") {
623
666
  if (message.display !== true) return [];
624
667
  const customType = optionalPiString(message.customType, 256);
625
- const text = textFromContent(message.content);
668
+ const text = textFromContent$1(message.content);
626
669
  return text ? [{
627
670
  ...common,
628
671
  type: "other",
@@ -678,7 +721,7 @@ function piTranscriptItems(entries) {
678
721
  text: entry.summary
679
722
  }];
680
723
  if (entry.type === "custom_message" && entry.display === true) {
681
- const text = textFromContent(entry.content);
724
+ const text = textFromContent$1(entry.content);
682
725
  return text ? [{
683
726
  ...common,
684
727
  type: "other",
@@ -700,6 +743,121 @@ async function readLocalPiTranscriptPage(value) {
700
743
  };
701
744
  }
702
745
  //#endregion
746
+ //#region extensions/acpx/src/pi-session-upstream-activity.ts
747
+ const MAX_PI_UPSTREAM_SCAN_BYTES = 1024 * 1024;
748
+ function parseCompletePiRows(tail) {
749
+ const entries = [];
750
+ let lineStart = 0;
751
+ let classifiedBytes = 0;
752
+ for (let index = 0; index < tail.length; index += 1) {
753
+ if (tail[index] !== 10) continue;
754
+ const line = tail.subarray(lineStart, index).toString("utf8").trim();
755
+ if (line) try {
756
+ const value = JSON.parse(line);
757
+ if (!isRecord(value)) break;
758
+ entries.push(value);
759
+ } catch {
760
+ break;
761
+ }
762
+ classifiedBytes = index + 1;
763
+ lineStart = index + 1;
764
+ }
765
+ return {
766
+ entries,
767
+ classifiedBytes
768
+ };
769
+ }
770
+ function textFromContent(content) {
771
+ if (typeof content === "string") return content;
772
+ if (!Array.isArray(content)) return;
773
+ return content.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n") || void 0;
774
+ }
775
+ function timestampMs(value) {
776
+ if (typeof value === "number" && Number.isFinite(value)) return value;
777
+ if (typeof value === "string") {
778
+ const parsed = Date.parse(value);
779
+ return Number.isNaN(parsed) ? void 0 : parsed;
780
+ }
781
+ }
782
+ function readFilePath(probe) {
783
+ return isRecord(probe.upstreamRef) && typeof probe.upstreamRef.filePath === "string" ? probe.upstreamRef.filePath : void 0;
784
+ }
785
+ function readMarkerOffset(probe) {
786
+ return isRecord(probe.marker) && Number.isSafeInteger(probe.marker.offset) && Number(probe.marker.offset) >= 0 ? Number(probe.marker.offset) : void 0;
787
+ }
788
+ async function linkContinuedPiSession(sessionKey, threadId) {
789
+ try {
790
+ const baseline = await readPiSessionFileBaseline(threadId, process$1.env);
791
+ return baseline ? {
792
+ sessionKey,
793
+ upstream: {
794
+ kind: "pi-cli",
795
+ ref: { filePath: baseline.filePath },
796
+ marker: { offset: baseline.offset }
797
+ }
798
+ } : { sessionKey };
799
+ } catch {
800
+ return { sessionKey };
801
+ }
802
+ }
803
+ async function checkPiSessionUpstreamActivity(probe) {
804
+ if (probe.hostId !== "gateway" || probe.upstreamKind !== "pi-cli") return;
805
+ const filePath = readFilePath(probe);
806
+ const markerOffset = readMarkerOffset(probe);
807
+ if (!filePath || markerOffset === void 0) return;
808
+ let handle;
809
+ try {
810
+ handle = await fs$1.open(filePath, "r");
811
+ } catch (error) {
812
+ return isRecord(error) && error.code === "ENOENT" ? {
813
+ kind: "missing",
814
+ sessionKey: probe.sessionKey
815
+ } : void 0;
816
+ }
817
+ try {
818
+ const stat = await handle.stat();
819
+ if (!stat.isFile()) return {
820
+ kind: "missing",
821
+ sessionKey: probe.sessionKey
822
+ };
823
+ if (stat.size <= markerOffset) return;
824
+ const readLength = Math.min(stat.size - markerOffset, MAX_PI_UPSTREAM_SCAN_BYTES);
825
+ const buffer = Buffer.allocUnsafe(readLength);
826
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, markerOffset);
827
+ const { entries, classifiedBytes } = parseCompletePiRows(buffer.subarray(0, bytesRead));
828
+ if (classifiedBytes === 0) return;
829
+ let humanTurns = 0;
830
+ let occurredAt;
831
+ for (const entry of entries) {
832
+ if (entry.type !== "message" || !isRecord(entry.message) || entry.message.role !== "user") continue;
833
+ if (!isExternalUserText(probe, textFromContent(entry.message.content))) continue;
834
+ humanTurns += 1;
835
+ occurredAt = Math.max(occurredAt ?? 0, timestampMs(entry.message.timestamp) ?? timestampMs(entry.timestamp) ?? stat.mtimeMs);
836
+ }
837
+ const nextOffset = markerOffset + classifiedBytes;
838
+ return {
839
+ kind: "activity",
840
+ sessionKey: probe.sessionKey,
841
+ humanTurns,
842
+ nextMarker: { offset: nextOffset },
843
+ ...humanTurns > 0 ? {
844
+ occurredAt: occurredAt ?? stat.mtimeMs,
845
+ dedupeId: String(nextOffset)
846
+ } : {}
847
+ };
848
+ } finally {
849
+ await handle.close();
850
+ }
851
+ }
852
+ async function checkPiUpstreamActivity(probes) {
853
+ const outcomes = [];
854
+ for (const probe of probes) try {
855
+ const outcome = await checkPiSessionUpstreamActivity(probe);
856
+ if (outcome) outcomes.push(outcome);
857
+ } catch {}
858
+ return outcomes;
859
+ }
860
+ //#endregion
703
861
  //#region extensions/acpx/src/pi-session-catalog-plugin.ts
704
862
  const PI_SESSIONS_LIST_COMMAND = "acpx.pi.sessions.list.v1";
705
863
  const PI_SESSION_READ_COMMAND = "acpx.pi.sessions.read.v1";
@@ -718,6 +876,11 @@ const TRANSCRIPT_ITEM_TYPES = /* @__PURE__ */ new Set([
718
876
  "toolResult",
719
877
  "other"
720
878
  ]);
879
+ const ACPX_BACKEND_ID = "acpx";
880
+ const PI_ACP_AGENT_ID = "pi";
881
+ const PI_ADOPTED_SESSION_KEY_PREFIX = "plugin:acpx:catalog-adopt:pi:";
882
+ var PiCatalogParamsError = class extends Error {};
883
+ const continueAdoption = createSessionCatalogAdoptionCoordinator();
721
884
  function validatePiThreadId(value) {
722
885
  if (typeof value !== "string" || !SESSION_ID_PATTERN.test(value)) throw new Error("INVALID_REQUEST: threadId is invalid");
723
886
  return value;
@@ -823,8 +986,11 @@ function nodeLabel(node) {
823
986
  function unwrapNodePayload(value) {
824
987
  return isRecord(value) && typeof value.payloadJSON === "string" ? JSON.parse(value.payloadJSON) : value;
825
988
  }
826
- function setTerminalCapability(page, canOpenTerminal) {
827
- for (const session of page.sessions) session.canOpenTerminal = canOpenTerminal;
989
+ function setCatalogCapabilities(page, capabilities) {
990
+ for (const session of page.sessions) {
991
+ session.canContinue = capabilities.canContinue && session.canContinue;
992
+ session.canOpenTerminal = capabilities.canOpenTerminal;
993
+ }
828
994
  return page;
829
995
  }
830
996
  async function listPiNodeHost(runtime, query, node) {
@@ -861,7 +1027,10 @@ async function listPiNodeHost(runtime, query, node) {
861
1027
  const canOpenTerminal = (node.invocableCommands ?? node.commands)?.includes(PI_TERMINAL_RESUME_COMMAND) === true;
862
1028
  return {
863
1029
  ...common,
864
- ...setTerminalCapability(page, canOpenTerminal)
1030
+ ...setCatalogCapabilities(page, {
1031
+ canContinue: false,
1032
+ canOpenTerminal
1033
+ })
865
1034
  };
866
1035
  } catch {
867
1036
  return {
@@ -896,7 +1065,9 @@ function parseNodeTranscriptPage(value, threadId) {
896
1065
  ...nextCursor !== void 0 ? { nextCursor } : {}
897
1066
  };
898
1067
  }
899
- async function listPiHosts(runtime, query) {
1068
+ async function listPiHosts(api, query) {
1069
+ const runtime = api.runtime;
1070
+ const canContinue = resolvePiContinuationAvailability(api).available;
900
1071
  const requested = query.hostIds ? new Set(query.hostIds) : void 0;
901
1072
  const hosts = [];
902
1073
  if ((!requested || requested.has(LOCAL_HOST_ID)) && piSessionStoreAvailable(process$1.env)) try {
@@ -909,11 +1080,14 @@ async function listPiHosts(runtime, query) {
909
1080
  limit: query.limitPerHost,
910
1081
  ...query.search ? { searchTerm: query.search } : {},
911
1082
  cursor: query.cursors?.[LOCAL_HOST_ID]
912
- }).then((page) => setTerminalCapability(page, resolveNodeHostExecutable("pi", {
913
- env: process$1.env,
914
- pathEnv: process$1.env.PATH ?? "",
915
- strategy: "fallback"
916
- }) !== void 0))
1083
+ }).then((page) => setCatalogCapabilities(page, {
1084
+ canContinue,
1085
+ canOpenTerminal: resolveNodeHostExecutable("pi", {
1086
+ env: process$1.env,
1087
+ pathEnv: process$1.env.PATH ?? "",
1088
+ strategy: "fallback"
1089
+ }) !== void 0
1090
+ }))
917
1091
  });
918
1092
  } catch {
919
1093
  hosts.push({
@@ -930,7 +1104,7 @@ async function listPiHosts(runtime, query) {
930
1104
  }
931
1105
  let nodes;
932
1106
  try {
933
- nodes = (await runtime.nodes.list()).nodes;
1107
+ nodes = (await (query.listNodes?.() ?? runtime.nodes.list())).nodes;
934
1108
  } catch {
935
1109
  return hosts;
936
1110
  }
@@ -946,6 +1120,95 @@ async function requireLocalPiSession(threadId) {
946
1120
  if (!record) throw new Error("Pi session is unavailable");
947
1121
  return record;
948
1122
  }
1123
+ function currentPiCatalogConfig(api) {
1124
+ return api.runtime.config?.current?.() ?? api.config ?? {};
1125
+ }
1126
+ function resolvePiContinuationAvailability(api) {
1127
+ const availability = resolveAcpSessionAvailability({
1128
+ config: currentPiCatalogConfig(api),
1129
+ backendId: ACPX_BACKEND_ID,
1130
+ agentId: PI_ACP_AGENT_ID
1131
+ });
1132
+ if (!availability.available) return availability;
1133
+ return resolveNodeHostExecutable("pi", {
1134
+ env: process$1.env,
1135
+ pathEnv: process$1.env.PATH ?? "",
1136
+ strategy: "fallback"
1137
+ }) ? { available: true } : {
1138
+ available: false,
1139
+ message: "Pi CLI is unavailable"
1140
+ };
1141
+ }
1142
+ function listAdoptedPiSessions(api) {
1143
+ return listAdoptedSessionCatalogSessions({
1144
+ config: currentPiCatalogConfig(api),
1145
+ pluginId: api.id,
1146
+ runtime: api.runtime,
1147
+ sourceFromEntry: (entry) => {
1148
+ const acpx = isRecord(entry.pluginExtensions?.acpx) ? entry.pluginExtensions.acpx : void 0;
1149
+ const marker = acpx && isRecord(acpx.piSessionCatalog) ? acpx.piSessionCatalog : void 0;
1150
+ return marker && typeof marker.sourceThreadId === "string" ? {
1151
+ hostId: LOCAL_HOST_ID,
1152
+ threadId: marker.sourceThreadId
1153
+ } : void 0;
1154
+ }
1155
+ });
1156
+ }
1157
+ async function continuePiSession(api, hostId, threadId) {
1158
+ if (hostId.startsWith("node:")) throw new PiCatalogParamsError("paired-node Pi session rows are view-only");
1159
+ if (hostId !== LOCAL_HOST_ID) throw new PiCatalogParamsError("Pi session catalog hostId is invalid");
1160
+ const availability = resolvePiContinuationAvailability(api);
1161
+ if (!availability.available) throw new PiCatalogParamsError(availability.message);
1162
+ const sourceKey = sessionCatalogAdoptedSourceKey(hostId, threadId);
1163
+ return await continueAdoption({
1164
+ sourceKey,
1165
+ findExisting: () => listAdoptedPiSessions(api).get(sourceKey),
1166
+ create: async () => {
1167
+ const record = await requireLocalPiSession(threadId).catch(() => void 0);
1168
+ if (!record) throw new PiCatalogParamsError("Pi session is unavailable");
1169
+ if (!record.canContinue) throw new PiCatalogParamsError("Pi session is outside the session store supported by pi-acp");
1170
+ const currentAvailability = resolvePiContinuationAvailability(api);
1171
+ if (!currentAvailability.available) throw new PiCatalogParamsError(currentAvailability.message);
1172
+ const config = currentPiCatalogConfig(api);
1173
+ const marker = { sourceThreadId: threadId };
1174
+ return { sessionKey: (await api.runtime.agent.session.createSessionEntry({
1175
+ cfg: config,
1176
+ key: sessionCatalogAdoptedSessionKey(PI_ADOPTED_SESSION_KEY_PREFIX, threadId),
1177
+ agentId: resolveDefaultAgentId(config),
1178
+ recoverMatchingInitialEntry: true,
1179
+ ...record.name ? { label: record.name } : {},
1180
+ ...record.cwd ? { spawnedCwd: record.cwd } : {},
1181
+ initialEntry: {
1182
+ acpBackendId: ACPX_BACKEND_ID,
1183
+ acpSessionBinding: {
1184
+ acpAgentId: PI_ACP_AGENT_ID,
1185
+ agentSessionId: threadId
1186
+ },
1187
+ pluginExtensions: { acpx: { piSessionCatalog: marker } }
1188
+ },
1189
+ afterCreate: async (entry) => {
1190
+ await importSessionCatalogHistory({
1191
+ catalogId: "pi",
1192
+ threadId,
1193
+ read: async ({ cursor, limit }) => await readPiTranscript(api.runtime, {
1194
+ hostId,
1195
+ threadId,
1196
+ limit,
1197
+ ...cursor ? { cursor } : {}
1198
+ }),
1199
+ sessionId: entry.sessionId,
1200
+ sessionKey: entry.key,
1201
+ agentId: entry.agentId,
1202
+ ...record.cwd ? { cwd: record.cwd } : {},
1203
+ config
1204
+ });
1205
+ return { pluginExtensions: { acpx: { piSessionCatalog: marker } } };
1206
+ }
1207
+ })).key };
1208
+ },
1209
+ complete: async (continued) => await linkContinuedPiSession(continued.sessionKey, threadId)
1210
+ });
1211
+ }
949
1212
  async function resolveNodePiSession(params) {
950
1213
  const record = parseNodeSessionPage(unwrapNodePayload(await params.runtime.nodes.invoke({
951
1214
  nodeId: params.nodeId,
@@ -1035,8 +1298,10 @@ function registerPiSessionCatalog(api) {
1035
1298
  api.registerSessionCatalog({
1036
1299
  id: "pi",
1037
1300
  label: "Pi",
1038
- list: async (query) => await listPiHosts(api.runtime, query),
1301
+ list: async (query) => await listPiHosts(api, query),
1039
1302
  read: async (request) => await readPiTranscript(api.runtime, request),
1303
+ continueSession: async (request) => await continuePiSession(api, request.hostId, request.threadId),
1304
+ checkUpstreamActivity: checkPiUpstreamActivity,
1040
1305
  openTerminal: async (request) => await openPiTerminal({
1041
1306
  runtime: api.runtime,
1042
1307
  ...request
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/acpx",
3
- "version": "2026.7.2-beta.4",
3
+ "version": "2026.7.2-beta.5",
4
4
  "description": "OpenClaw ACP runtime backend with plugin-owned session and transport management.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,7 +9,7 @@
9
9
  "type": "module",
10
10
  "dependencies": {
11
11
  "@agentclientprotocol/claude-agent-acp": "0.59.0",
12
- "@agentclientprotocol/codex-acp": "1.1.4",
12
+ "@agentclientprotocol/codex-acp": "1.1.7",
13
13
  "acpx": "0.12.0",
14
14
  "smol-toml": "1.7.0",
15
15
  "zod": "4.4.3"
@@ -43,10 +43,10 @@
43
43
  ]
44
44
  },
45
45
  "compat": {
46
- "pluginApi": ">=2026.7.2-beta.4"
46
+ "pluginApi": ">=2026.7.2-beta.5"
47
47
  },
48
48
  "build": {
49
- "openclawVersion": "2026.7.2-beta.4",
49
+ "openclawVersion": "2026.7.2-beta.5",
50
50
  "staticAssets": [
51
51
  {
52
52
  "source": "./src/runtime-internals/mcp-proxy.mjs",
@@ -70,12 +70,11 @@
70
70
  "files": [
71
71
  "dist/**",
72
72
  "openclaw.plugin.json",
73
- "npm-shrinkwrap.json",
74
73
  "README.md",
75
74
  "skills/**"
76
75
  ],
77
76
  "peerDependencies": {
78
- "openclaw": ">=2026.7.2-beta.4"
77
+ "openclaw": ">=2026.7.2-beta.5"
79
78
  },
80
79
  "peerDependenciesMeta": {
81
80
  "openclaw": {