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

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,4 +1,4 @@
1
- import { c as openAcpxProcessLeaseStateStore, d as ACPX_GATEWAY_INSTANCE_NAMESPACE, f as ACPX_LEGACY_GATEWAY_INSTANCE_FILE, m as normalizeAcpxGatewayInstanceRecord, o as normalizeAcpxProcessLease, p as ACPX_LEGACY_PROCESS_LEASE_FILE, s as normalizeAcpxProcessLeaseFile, u as ACPX_GATEWAY_INSTANCE_KEY } from "./process-lease-CU1cFI4I.js";
1
+ import { c as openAcpxProcessLeaseStateStore, d as ACPX_GATEWAY_INSTANCE_KEY, f as ACPX_GATEWAY_INSTANCE_NAMESPACE, h as normalizeAcpxGatewayInstanceRecord, m as ACPX_LEGACY_PROCESS_LEASE_FILE, o as normalizeAcpxProcessLease, p as ACPX_LEGACY_GATEWAY_INSTANCE_FILE, s as normalizeAcpxProcessLeaseFile } from "./process-lease-DSLDgiNl.js";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { archiveLegacyStateSource } from "openclaw/plugin-sdk/runtime-doctor";
package/dist/index.js CHANGED
@@ -1,9 +1,12 @@
1
- import { t as createAcpxRuntimeService } from "./register.runtime-Dajn8myc.js";
1
+ import { t as createAcpxRuntimeService } from "./register.runtime-BbS2JTTv.js";
2
2
  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();
@@ -83,9 +93,12 @@ const MAX_SESSION_BYTES = 32 * 1024 * 1024;
83
93
  const MAX_SUMMARY_LINE_BYTES = 1024 * 1024;
84
94
  const APPEND_PROOF_EDGE_BYTES = 64 * 1024;
85
95
  const IO_CONCURRENCY = 8;
96
+ const PI_FILE_CANDIDATE_CACHE_TTL_MS = 32e3;
97
+ const PI_FILE_CANDIDATE_CACHE_MAX_ENTRIES = 8;
86
98
  const SESSION_ID_PATTERN$2 = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
87
99
  const summaryCache = /* @__PURE__ */ new Map();
88
100
  const threadFileCache = /* @__PURE__ */ new Map();
101
+ const piFileCandidateCache = /* @__PURE__ */ new Map();
89
102
  function threadCacheKey(storeRoot, threadId) {
90
103
  return `${storeRoot}\0${threadId}`;
91
104
  }
@@ -114,9 +127,10 @@ function optionalString(value, maxLength) {
114
127
  }
115
128
  async function discoverPiSessionFiles(env) {
116
129
  const store = piSessionStore(env);
130
+ const resolvedRoot = await realpathOrResolve(store.root);
117
131
  let entries;
118
132
  try {
119
- entries = await fs$1.readdir(store.root, { withFileTypes: true });
133
+ entries = await fs$1.readdir(resolvedRoot, { withFileTypes: true });
120
134
  } catch {
121
135
  return {
122
136
  root: store.root,
@@ -125,12 +139,12 @@ async function discoverPiSessionFiles(env) {
125
139
  }
126
140
  if (store.flat) return {
127
141
  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))
142
+ files: entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).slice(0, MAX_DISCOVERY_FILES).map((entry) => path.join(resolvedRoot, entry.name))
129
143
  };
130
144
  const files = [];
131
145
  for (const entry of entries) {
132
146
  if (!entry.isDirectory() || files.length >= MAX_DISCOVERY_FILES) continue;
133
- const directory = path.join(store.root, entry.name);
147
+ const directory = path.join(resolvedRoot, entry.name);
134
148
  let children;
135
149
  try {
136
150
  children = await fs$1.readdir(directory, { withFileTypes: true });
@@ -147,6 +161,13 @@ async function discoverPiSessionFiles(env) {
147
161
  files
148
162
  };
149
163
  }
164
+ async function realpathOrResolve(value) {
165
+ try {
166
+ return await fs$1.realpath(value);
167
+ } catch {
168
+ return path.resolve(value);
169
+ }
170
+ }
150
171
  async function mapConcurrent(values, limit, mapper) {
151
172
  const results = [];
152
173
  results.length = values.length;
@@ -160,8 +181,10 @@ async function mapConcurrent(values, limit, mapper) {
160
181
  await Promise.all(workers);
161
182
  return results;
162
183
  }
163
- async function piFileCandidates(env) {
184
+ async function scanPiFileCandidates(env) {
164
185
  const { root, files } = await discoverPiSessionFiles(env);
186
+ const configuredAcpRoot = piAcpSessionStoreRoot(env);
187
+ const acpRoot = configuredAcpRoot ? await realpathOrResolve(configuredAcpRoot) : void 0;
165
188
  return (await mapConcurrent(files, IO_CONCURRENCY, async (file) => {
166
189
  try {
167
190
  const stats = await fs$1.stat(file);
@@ -170,13 +193,46 @@ async function piFileCandidates(env) {
170
193
  storeRoot: root,
171
194
  identity: `${String(stats.dev)}:${String(stats.ino)}:${String(stats.birthtimeMs)}`,
172
195
  mtimeMs: stats.mtimeMs,
173
- size: stats.size
196
+ size: stats.size,
197
+ resumable: acpRoot ? pathIsWithin(acpRoot, file) : false
174
198
  } : void 0;
175
199
  } catch {
176
200
  return;
177
201
  }
178
202
  })).filter((candidate) => candidate !== void 0).toSorted((left, right) => right.mtimeMs - left.mtimeMs);
179
203
  }
204
+ async function piFileCandidates(env) {
205
+ const store = piSessionStore(env);
206
+ const key = `${store.root}\0${store.flat}\0${piAcpSessionStoreRoot(env) ?? ""}`;
207
+ const cached = piFileCandidateCache.get(key);
208
+ if (cached && cached.expiresAt > Date.now()) {
209
+ piFileCandidateCache.delete(key);
210
+ piFileCandidateCache.set(key, cached);
211
+ return await cached.candidates;
212
+ }
213
+ if (cached) piFileCandidateCache.delete(key);
214
+ const candidates = scanPiFileCandidates(env);
215
+ const entry = {
216
+ expiresAt: Date.now() + PI_FILE_CANDIDATE_CACHE_TTL_MS,
217
+ candidates
218
+ };
219
+ piFileCandidateCache.set(key, entry);
220
+ while (piFileCandidateCache.size > PI_FILE_CANDIDATE_CACHE_MAX_ENTRIES) {
221
+ const oldest = piFileCandidateCache.keys().next();
222
+ if (oldest.done) break;
223
+ piFileCandidateCache.delete(oldest.value);
224
+ }
225
+ try {
226
+ return await candidates;
227
+ } catch (error) {
228
+ if (piFileCandidateCache.get(key) === entry) piFileCandidateCache.delete(key);
229
+ throw error;
230
+ }
231
+ }
232
+ function pathIsWithin(root, candidate) {
233
+ const relative = path.relative(root, candidate);
234
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
235
+ }
180
236
  function parsePiJsonLines(content) {
181
237
  return content.split(/\r?\n/u).flatMap((line) => {
182
238
  if (!line.trim()) return [];
@@ -188,12 +244,12 @@ function parsePiJsonLines(content) {
188
244
  }
189
245
  });
190
246
  }
191
- function textFromContent$1(content) {
247
+ function textFromContent$2(content) {
192
248
  if (typeof content === "string") return content;
193
249
  if (!Array.isArray(content)) return "";
194
250
  return content.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n");
195
251
  }
196
- function timestampMs$1(value) {
252
+ function timestampMs$2(value) {
197
253
  if (typeof value === "number" && Number.isFinite(value)) return value;
198
254
  if (typeof value === "string") {
199
255
  const parsed = Date.parse(value);
@@ -212,7 +268,7 @@ function processSummaryLine(state, line) {
212
268
  return;
213
269
  }
214
270
  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);
271
+ else if (!state.firstMessage && entry.type === "message" && isRecord(entry.message) && entry.message.role === "user") state.firstMessage = optionalString(textFromContent$2(entry.message.content), 1e3);
216
272
  }
217
273
  function appendSummaryBytes(state, bytes) {
218
274
  if (state.discarding || bytes.length === 0) return;
@@ -274,7 +330,10 @@ async function readPiSessionSummary(candidate) {
274
330
  if (cached?.mtimeMs === candidate.mtimeMs && cached.size === candidate.size) {
275
331
  summaryCache.delete(candidate.file);
276
332
  summaryCache.set(candidate.file, cached);
277
- return cached.summary;
333
+ return cached.summary ? {
334
+ ...cached.summary,
335
+ canContinue: candidate.resumable
336
+ } : cached.summary;
278
337
  }
279
338
  let summary;
280
339
  let scanState;
@@ -297,12 +356,14 @@ async function readPiSessionSummary(candidate) {
297
356
  };
298
357
  if (!projectedState.discarding && projectedState.pending.length > 0) processSummaryLine(projectedState, projectedState.pending);
299
358
  const { header, name, firstMessage } = projectedState;
359
+ const version = header?.type === "session" && typeof header.version === "number" ? header.version : 1;
300
360
  const threadId = header?.type === "session" ? optionalString(header.id, 256) : void 0;
301
361
  if (header && threadId && SESSION_ID_PATTERN$2.test(threadId)) {
302
362
  const cwd = optionalString(header.cwd, 4096);
303
- const createdAt = timestampMs$1(header.timestamp);
363
+ const createdAt = timestampMs$2(header.timestamp);
304
364
  summary = {
305
365
  file: candidate.file,
366
+ version,
306
367
  threadId,
307
368
  ...name || firstMessage ? { name: name ?? firstMessage } : {},
308
369
  ...cwd ? { cwd } : {},
@@ -313,7 +374,7 @@ async function readPiSessionSummary(candidate) {
313
374
  source: "pi-cli",
314
375
  modelProvider: "pi",
315
376
  archived: false,
316
- canContinue: false,
377
+ canContinue: candidate.resumable,
317
378
  canArchive: false
318
379
  };
319
380
  }
@@ -364,6 +425,19 @@ async function findPiSummary(threadId, env) {
364
425
  if (match) return match;
365
426
  }
366
427
  }
428
+ async function readPiSessionFileBaseline(threadId, env) {
429
+ const summary = await findPiSummary(threadId, env);
430
+ if (!summary?.canContinue || summary.version < 3) return;
431
+ try {
432
+ const stats = await fs$1.stat(summary.file);
433
+ return stats.isFile() ? {
434
+ filePath: summary.file,
435
+ offset: stats.size
436
+ } : void 0;
437
+ } catch {
438
+ return;
439
+ }
440
+ }
367
441
  async function readPiSessionById(threadId, env) {
368
442
  const cacheKey = threadCacheKey(piSessionStore(env).root, threadId);
369
443
  let file = threadFileCache.get(cacheKey);
@@ -472,7 +546,7 @@ function transcriptPage(items, limit, offset) {
472
546
  ...consumed < items.length ? { nextCursor: encodeCursor(consumed) } : {}
473
547
  };
474
548
  }
475
- function textFromContent(content) {
549
+ function textFromContent$1(content) {
476
550
  if (typeof content === "string") return content;
477
551
  if (!Array.isArray(content)) return "";
478
552
  return content.flatMap((part) => {
@@ -485,7 +559,7 @@ function textFromContent(content) {
485
559
  return [];
486
560
  }).join("\n");
487
561
  }
488
- function timestampMs(value) {
562
+ function timestampMs$1(value) {
489
563
  if (typeof value === "number" && Number.isFinite(value)) return value;
490
564
  if (typeof value === "string") {
491
565
  const parsed = Date.parse(value);
@@ -535,14 +609,14 @@ async function listLocalPiSessionPage(value) {
535
609
  limit: params.limit,
536
610
  ...params.searchTerm ? { searchTerm: params.searchTerm } : {}
537
611
  });
538
- const page = summaries.map(({ file: _file, ...session }) => session);
612
+ const page = summaries.map(({ file: _file, version: _version, ...session }) => session);
539
613
  return {
540
614
  sessions: page,
541
615
  ...hasMore ? { nextCursor: encodeCursor(offset + page.length) } : {}
542
616
  };
543
617
  }
544
618
  function isoTimestamp(message, entry) {
545
- const value = timestampMs(message.timestamp) ?? timestampMs(entry.timestamp);
619
+ const value = timestampMs$1(message.timestamp) ?? timestampMs$1(entry.timestamp);
546
620
  if (value === void 0) return;
547
621
  const date = new Date(value);
548
622
  return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
@@ -588,7 +662,7 @@ function piMessageItems(entry) {
588
662
  ...modelRef ? { model: modelRef } : {}
589
663
  };
590
664
  if (role === "user") {
591
- const text = textFromContent(message.content);
665
+ const text = textFromContent$1(message.content);
592
666
  return text ? [{
593
667
  ...common,
594
668
  type: "userMessage",
@@ -597,7 +671,7 @@ function piMessageItems(entry) {
597
671
  }
598
672
  if (role === "toolResult") {
599
673
  const toolName = optionalPiString(message.toolName, 256);
600
- const text = textFromContent(message.content);
674
+ const text = textFromContent$1(message.content);
601
675
  return [{
602
676
  ...common,
603
677
  type: "toolResult",
@@ -622,7 +696,7 @@ function piMessageItems(entry) {
622
696
  if (role === "custom" || role === "hookMessage") {
623
697
  if (message.display !== true) return [];
624
698
  const customType = optionalPiString(message.customType, 256);
625
- const text = textFromContent(message.content);
699
+ const text = textFromContent$1(message.content);
626
700
  return text ? [{
627
701
  ...common,
628
702
  type: "other",
@@ -678,7 +752,7 @@ function piTranscriptItems(entries) {
678
752
  text: entry.summary
679
753
  }];
680
754
  if (entry.type === "custom_message" && entry.display === true) {
681
- const text = textFromContent(entry.content);
755
+ const text = textFromContent$1(entry.content);
682
756
  return text ? [{
683
757
  ...common,
684
758
  type: "other",
@@ -700,6 +774,121 @@ async function readLocalPiTranscriptPage(value) {
700
774
  };
701
775
  }
702
776
  //#endregion
777
+ //#region extensions/acpx/src/pi-session-upstream-activity.ts
778
+ const MAX_PI_UPSTREAM_SCAN_BYTES = 1024 * 1024;
779
+ function parseCompletePiRows(tail) {
780
+ const entries = [];
781
+ let lineStart = 0;
782
+ let classifiedBytes = 0;
783
+ for (let index = 0; index < tail.length; index += 1) {
784
+ if (tail[index] !== 10) continue;
785
+ const line = tail.subarray(lineStart, index).toString("utf8").trim();
786
+ if (line) try {
787
+ const value = JSON.parse(line);
788
+ if (!isRecord(value)) break;
789
+ entries.push(value);
790
+ } catch {
791
+ break;
792
+ }
793
+ classifiedBytes = index + 1;
794
+ lineStart = index + 1;
795
+ }
796
+ return {
797
+ entries,
798
+ classifiedBytes
799
+ };
800
+ }
801
+ function textFromContent(content) {
802
+ if (typeof content === "string") return content;
803
+ if (!Array.isArray(content)) return;
804
+ return content.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n") || void 0;
805
+ }
806
+ function timestampMs(value) {
807
+ if (typeof value === "number" && Number.isFinite(value)) return value;
808
+ if (typeof value === "string") {
809
+ const parsed = Date.parse(value);
810
+ return Number.isNaN(parsed) ? void 0 : parsed;
811
+ }
812
+ }
813
+ function readFilePath(probe) {
814
+ return isRecord(probe.upstreamRef) && typeof probe.upstreamRef.filePath === "string" ? probe.upstreamRef.filePath : void 0;
815
+ }
816
+ function readMarkerOffset(probe) {
817
+ return isRecord(probe.marker) && Number.isSafeInteger(probe.marker.offset) && Number(probe.marker.offset) >= 0 ? Number(probe.marker.offset) : void 0;
818
+ }
819
+ async function linkContinuedPiSession(sessionKey, threadId) {
820
+ try {
821
+ const baseline = await readPiSessionFileBaseline(threadId, process$1.env);
822
+ return baseline ? {
823
+ sessionKey,
824
+ upstream: {
825
+ kind: "pi-cli",
826
+ ref: { filePath: baseline.filePath },
827
+ marker: { offset: baseline.offset }
828
+ }
829
+ } : { sessionKey };
830
+ } catch {
831
+ return { sessionKey };
832
+ }
833
+ }
834
+ async function checkPiSessionUpstreamActivity(probe) {
835
+ if (probe.hostId !== "gateway" || probe.upstreamKind !== "pi-cli") return;
836
+ const filePath = readFilePath(probe);
837
+ const markerOffset = readMarkerOffset(probe);
838
+ if (!filePath || markerOffset === void 0) return;
839
+ let handle;
840
+ try {
841
+ handle = await fs$1.open(filePath, "r");
842
+ } catch (error) {
843
+ return isRecord(error) && error.code === "ENOENT" ? {
844
+ kind: "missing",
845
+ sessionKey: probe.sessionKey
846
+ } : void 0;
847
+ }
848
+ try {
849
+ const stat = await handle.stat();
850
+ if (!stat.isFile()) return {
851
+ kind: "missing",
852
+ sessionKey: probe.sessionKey
853
+ };
854
+ if (stat.size <= markerOffset) return;
855
+ const readLength = Math.min(stat.size - markerOffset, MAX_PI_UPSTREAM_SCAN_BYTES);
856
+ const buffer = Buffer.allocUnsafe(readLength);
857
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, markerOffset);
858
+ const { entries, classifiedBytes } = parseCompletePiRows(buffer.subarray(0, bytesRead));
859
+ if (classifiedBytes === 0) return;
860
+ let humanTurns = 0;
861
+ let occurredAt;
862
+ for (const entry of entries) {
863
+ if (entry.type !== "message" || !isRecord(entry.message) || entry.message.role !== "user") continue;
864
+ if (!isExternalUserText(probe, textFromContent(entry.message.content))) continue;
865
+ humanTurns += 1;
866
+ occurredAt = Math.max(occurredAt ?? 0, timestampMs(entry.message.timestamp) ?? timestampMs(entry.timestamp) ?? stat.mtimeMs);
867
+ }
868
+ const nextOffset = markerOffset + classifiedBytes;
869
+ return {
870
+ kind: "activity",
871
+ sessionKey: probe.sessionKey,
872
+ humanTurns,
873
+ nextMarker: { offset: nextOffset },
874
+ ...humanTurns > 0 ? {
875
+ occurredAt: occurredAt ?? stat.mtimeMs,
876
+ dedupeId: String(nextOffset)
877
+ } : {}
878
+ };
879
+ } finally {
880
+ await handle.close();
881
+ }
882
+ }
883
+ async function checkPiUpstreamActivity(probes) {
884
+ const outcomes = [];
885
+ for (const probe of probes) try {
886
+ const outcome = await checkPiSessionUpstreamActivity(probe);
887
+ if (outcome) outcomes.push(outcome);
888
+ } catch {}
889
+ return outcomes;
890
+ }
891
+ //#endregion
703
892
  //#region extensions/acpx/src/pi-session-catalog-plugin.ts
704
893
  const PI_SESSIONS_LIST_COMMAND = "acpx.pi.sessions.list.v1";
705
894
  const PI_SESSION_READ_COMMAND = "acpx.pi.sessions.read.v1";
@@ -718,6 +907,11 @@ const TRANSCRIPT_ITEM_TYPES = /* @__PURE__ */ new Set([
718
907
  "toolResult",
719
908
  "other"
720
909
  ]);
910
+ const ACPX_BACKEND_ID = "acpx";
911
+ const PI_ACP_AGENT_ID = "pi";
912
+ const PI_ADOPTED_SESSION_KEY_PREFIX = "plugin:acpx:catalog-adopt:pi:";
913
+ var PiCatalogParamsError = class extends Error {};
914
+ const continueAdoption = createSessionCatalogAdoptionCoordinator();
721
915
  function validatePiThreadId(value) {
722
916
  if (typeof value !== "string" || !SESSION_ID_PATTERN.test(value)) throw new Error("INVALID_REQUEST: threadId is invalid");
723
917
  return value;
@@ -823,8 +1017,11 @@ function nodeLabel(node) {
823
1017
  function unwrapNodePayload(value) {
824
1018
  return isRecord(value) && typeof value.payloadJSON === "string" ? JSON.parse(value.payloadJSON) : value;
825
1019
  }
826
- function setTerminalCapability(page, canOpenTerminal) {
827
- for (const session of page.sessions) session.canOpenTerminal = canOpenTerminal;
1020
+ function setCatalogCapabilities(page, capabilities) {
1021
+ for (const session of page.sessions) {
1022
+ session.canContinue = capabilities.canContinue && session.canContinue;
1023
+ session.canOpenTerminal = capabilities.canOpenTerminal;
1024
+ }
828
1025
  return page;
829
1026
  }
830
1027
  async function listPiNodeHost(runtime, query, node) {
@@ -861,7 +1058,10 @@ async function listPiNodeHost(runtime, query, node) {
861
1058
  const canOpenTerminal = (node.invocableCommands ?? node.commands)?.includes(PI_TERMINAL_RESUME_COMMAND) === true;
862
1059
  return {
863
1060
  ...common,
864
- ...setTerminalCapability(page, canOpenTerminal)
1061
+ ...setCatalogCapabilities(page, {
1062
+ canContinue: false,
1063
+ canOpenTerminal
1064
+ })
865
1065
  };
866
1066
  } catch {
867
1067
  return {
@@ -896,7 +1096,9 @@ function parseNodeTranscriptPage(value, threadId) {
896
1096
  ...nextCursor !== void 0 ? { nextCursor } : {}
897
1097
  };
898
1098
  }
899
- async function listPiHosts(runtime, query) {
1099
+ async function listPiHosts(api, query) {
1100
+ const runtime = api.runtime;
1101
+ const canContinue = resolvePiContinuationAvailability(api).available;
900
1102
  const requested = query.hostIds ? new Set(query.hostIds) : void 0;
901
1103
  const hosts = [];
902
1104
  if ((!requested || requested.has(LOCAL_HOST_ID)) && piSessionStoreAvailable(process$1.env)) try {
@@ -909,11 +1111,14 @@ async function listPiHosts(runtime, query) {
909
1111
  limit: query.limitPerHost,
910
1112
  ...query.search ? { searchTerm: query.search } : {},
911
1113
  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))
1114
+ }).then((page) => setCatalogCapabilities(page, {
1115
+ canContinue,
1116
+ canOpenTerminal: resolveNodeHostExecutable("pi", {
1117
+ env: process$1.env,
1118
+ pathEnv: process$1.env.PATH ?? "",
1119
+ strategy: "fallback"
1120
+ }) !== void 0
1121
+ }))
917
1122
  });
918
1123
  } catch {
919
1124
  hosts.push({
@@ -930,7 +1135,7 @@ async function listPiHosts(runtime, query) {
930
1135
  }
931
1136
  let nodes;
932
1137
  try {
933
- nodes = (await runtime.nodes.list()).nodes;
1138
+ nodes = (await (query.listNodes?.() ?? runtime.nodes.list())).nodes;
934
1139
  } catch {
935
1140
  return hosts;
936
1141
  }
@@ -946,6 +1151,95 @@ async function requireLocalPiSession(threadId) {
946
1151
  if (!record) throw new Error("Pi session is unavailable");
947
1152
  return record;
948
1153
  }
1154
+ function currentPiCatalogConfig(api) {
1155
+ return api.runtime.config?.current?.() ?? api.config ?? {};
1156
+ }
1157
+ function resolvePiContinuationAvailability(api) {
1158
+ const availability = resolveAcpSessionAvailability({
1159
+ config: currentPiCatalogConfig(api),
1160
+ backendId: ACPX_BACKEND_ID,
1161
+ agentId: PI_ACP_AGENT_ID
1162
+ });
1163
+ if (!availability.available) return availability;
1164
+ return resolveNodeHostExecutable("pi", {
1165
+ env: process$1.env,
1166
+ pathEnv: process$1.env.PATH ?? "",
1167
+ strategy: "fallback"
1168
+ }) ? { available: true } : {
1169
+ available: false,
1170
+ message: "Pi CLI is unavailable"
1171
+ };
1172
+ }
1173
+ function listAdoptedPiSessions(api) {
1174
+ return listAdoptedSessionCatalogSessions({
1175
+ config: currentPiCatalogConfig(api),
1176
+ pluginId: api.id,
1177
+ runtime: api.runtime,
1178
+ sourceFromEntry: (entry) => {
1179
+ const acpx = isRecord(entry.pluginExtensions?.acpx) ? entry.pluginExtensions.acpx : void 0;
1180
+ const marker = acpx && isRecord(acpx.piSessionCatalog) ? acpx.piSessionCatalog : void 0;
1181
+ return marker && typeof marker.sourceThreadId === "string" ? {
1182
+ hostId: LOCAL_HOST_ID,
1183
+ threadId: marker.sourceThreadId
1184
+ } : void 0;
1185
+ }
1186
+ });
1187
+ }
1188
+ async function continuePiSession(api, hostId, threadId) {
1189
+ if (hostId.startsWith("node:")) throw new PiCatalogParamsError("paired-node Pi session rows are view-only");
1190
+ if (hostId !== LOCAL_HOST_ID) throw new PiCatalogParamsError("Pi session catalog hostId is invalid");
1191
+ const availability = resolvePiContinuationAvailability(api);
1192
+ if (!availability.available) throw new PiCatalogParamsError(availability.message);
1193
+ const sourceKey = sessionCatalogAdoptedSourceKey(hostId, threadId);
1194
+ return await continueAdoption({
1195
+ sourceKey,
1196
+ findExisting: () => listAdoptedPiSessions(api).get(sourceKey),
1197
+ create: async () => {
1198
+ const record = await requireLocalPiSession(threadId).catch(() => void 0);
1199
+ if (!record) throw new PiCatalogParamsError("Pi session is unavailable");
1200
+ if (!record.canContinue) throw new PiCatalogParamsError("Pi session is outside the session store supported by pi-acp");
1201
+ const currentAvailability = resolvePiContinuationAvailability(api);
1202
+ if (!currentAvailability.available) throw new PiCatalogParamsError(currentAvailability.message);
1203
+ const config = currentPiCatalogConfig(api);
1204
+ const marker = { sourceThreadId: threadId };
1205
+ return { sessionKey: (await api.runtime.agent.session.createSessionEntry({
1206
+ cfg: config,
1207
+ key: sessionCatalogAdoptedSessionKey(PI_ADOPTED_SESSION_KEY_PREFIX, threadId),
1208
+ agentId: resolveDefaultAgentId(config),
1209
+ recoverMatchingInitialEntry: true,
1210
+ ...record.name ? { label: record.name } : {},
1211
+ ...record.cwd ? { spawnedCwd: record.cwd } : {},
1212
+ initialEntry: {
1213
+ acpBackendId: ACPX_BACKEND_ID,
1214
+ acpSessionBinding: {
1215
+ acpAgentId: PI_ACP_AGENT_ID,
1216
+ agentSessionId: threadId
1217
+ },
1218
+ pluginExtensions: { acpx: { piSessionCatalog: marker } }
1219
+ },
1220
+ afterCreate: async (entry) => {
1221
+ await importSessionCatalogHistory({
1222
+ catalogId: "pi",
1223
+ threadId,
1224
+ read: async ({ cursor, limit }) => await readPiTranscript(api.runtime, {
1225
+ hostId,
1226
+ threadId,
1227
+ limit,
1228
+ ...cursor ? { cursor } : {}
1229
+ }),
1230
+ sessionId: entry.sessionId,
1231
+ sessionKey: entry.key,
1232
+ agentId: entry.agentId,
1233
+ ...record.cwd ? { cwd: record.cwd } : {},
1234
+ config
1235
+ });
1236
+ return { pluginExtensions: { acpx: { piSessionCatalog: marker } } };
1237
+ }
1238
+ })).key };
1239
+ },
1240
+ complete: async (continued) => await linkContinuedPiSession(continued.sessionKey, threadId)
1241
+ });
1242
+ }
949
1243
  async function resolveNodePiSession(params) {
950
1244
  const record = parseNodeSessionPage(unwrapNodePayload(await params.runtime.nodes.invoke({
951
1245
  nodeId: params.nodeId,
@@ -1035,8 +1329,10 @@ function registerPiSessionCatalog(api) {
1035
1329
  api.registerSessionCatalog({
1036
1330
  id: "pi",
1037
1331
  label: "Pi",
1038
- list: async (query) => await listPiHosts(api.runtime, query),
1332
+ list: async (query) => await listPiHosts(api, query),
1039
1333
  read: async (request) => await readPiTranscript(api.runtime, request),
1334
+ continueSession: async (request) => await continuePiSession(api, request.hostId, request.threadId),
1335
+ checkUpstreamActivity: checkPiUpstreamActivity,
1040
1336
  openTerminal: async (request) => await openPiTerminal({
1041
1337
  runtime: api.runtime,
1042
1338
  ...request
@@ -1,4 +1,52 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
+ //#region extensions/acpx/src/command-line.ts
3
+ /**
4
+ * Small shell-command helpers for ACPX-launched processes. Splitting supports
5
+ * simple quoted command strings from config without invoking a shell parser.
6
+ */
7
+ /** Quote one command argument for display or config serialization. */
8
+ function quoteCommandPart(value) {
9
+ return JSON.stringify(value);
10
+ }
11
+ /** Split a command string into argv-like parts using simple quote/backslash rules. */
12
+ function splitCommandParts(value) {
13
+ const parts = [];
14
+ let current = "";
15
+ let quote = null;
16
+ let escaping = false;
17
+ for (const ch of value) {
18
+ if (escaping) {
19
+ current += ch;
20
+ escaping = false;
21
+ continue;
22
+ }
23
+ if (ch === "\\" && quote !== "'") {
24
+ escaping = true;
25
+ continue;
26
+ }
27
+ if (quote) {
28
+ if (ch === quote) quote = null;
29
+ else current += ch;
30
+ continue;
31
+ }
32
+ if (ch === "'" || ch === "\"") {
33
+ quote = ch;
34
+ continue;
35
+ }
36
+ if (/\s/.test(ch)) {
37
+ if (current) {
38
+ parts.push(current);
39
+ current = "";
40
+ }
41
+ continue;
42
+ }
43
+ current += ch;
44
+ }
45
+ if (escaping) current += "\\";
46
+ if (current) parts.push(current);
47
+ return parts;
48
+ }
49
+ //#endregion
2
50
  //#region extensions/acpx/src/state.ts
3
51
  const ACPX_PROCESS_LEASE_NAMESPACE = "process-leases";
4
52
  const ACPX_PROCESS_LEASE_MAX_ENTRIES = 4096;
@@ -26,6 +74,19 @@ function normalizeAcpxGatewayInstanceRecord(value) {
26
74
  const OPENCLAW_ACPX_LEASE_ID_ARG = "--openclaw-acpx-lease-id";
27
75
  /** CLI argument carrying the owning gateway instance id. */
28
76
  const OPENCLAW_GATEWAY_INSTANCE_ID_ARG = "--openclaw-gateway-instance-id";
77
+ /** Read OpenClaw lease identity from a generated wrapper command. */
78
+ function readAcpxProcessLeaseIdentity(command) {
79
+ const parts = splitCommandParts(command?.trim() ?? "");
80
+ const leaseIndex = parts.lastIndexOf(OPENCLAW_ACPX_LEASE_ID_ARG);
81
+ const gatewayIndex = parts.lastIndexOf(OPENCLAW_GATEWAY_INSTANCE_ID_ARG);
82
+ const leaseId = leaseIndex >= 0 ? parts[leaseIndex + 1]?.trim() : "";
83
+ const gatewayInstanceId = gatewayIndex >= 0 ? parts[gatewayIndex + 1]?.trim() : "";
84
+ if (!leaseId || !gatewayInstanceId) return;
85
+ return {
86
+ leaseId,
87
+ gatewayInstanceId
88
+ };
89
+ }
29
90
  function normalizeAcpxProcessLease(value) {
30
91
  if (typeof value !== "object" || value === null) return;
31
92
  const record = value;
@@ -128,4 +189,4 @@ function withAcpxLeaseEnvironment(params) {
128
189
  return appendAcpxLeaseArgs(params);
129
190
  }
130
191
  //#endregion
131
- export { hashAcpxProcessCommand as a, openAcpxProcessLeaseStateStore as c, ACPX_GATEWAY_INSTANCE_NAMESPACE as d, ACPX_LEGACY_GATEWAY_INSTANCE_FILE as f, createAcpxProcessLeaseStore as i, withAcpxLeaseEnvironment as l, normalizeAcpxGatewayInstanceRecord as m, OPENCLAW_GATEWAY_INSTANCE_ID_ARG as n, normalizeAcpxProcessLease as o, ACPX_LEGACY_PROCESS_LEASE_FILE as p, createAcpxProcessLeaseId as r, normalizeAcpxProcessLeaseFile as s, OPENCLAW_ACPX_LEASE_ID_ARG as t, ACPX_GATEWAY_INSTANCE_KEY as u };
192
+ export { splitCommandParts as _, hashAcpxProcessCommand as a, openAcpxProcessLeaseStateStore as c, ACPX_GATEWAY_INSTANCE_KEY as d, ACPX_GATEWAY_INSTANCE_NAMESPACE as f, quoteCommandPart as g, normalizeAcpxGatewayInstanceRecord as h, createAcpxProcessLeaseStore as i, readAcpxProcessLeaseIdentity as l, ACPX_LEGACY_PROCESS_LEASE_FILE as m, OPENCLAW_GATEWAY_INSTANCE_ID_ARG as n, normalizeAcpxProcessLease as o, ACPX_LEGACY_GATEWAY_INSTANCE_FILE as p, createAcpxProcessLeaseId as r, normalizeAcpxProcessLeaseFile as s, OPENCLAW_ACPX_LEASE_ID_ARG as t, withAcpxLeaseEnvironment as u };