@openclaw/acpx 2026.7.2-beta.3 → 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
@@ -1,9 +1,12 @@
1
- import { t as createAcpxRuntimeService } from "./register.runtime-BrVpU-xc.js";
1
+ import { t as createAcpxRuntimeService } from "./register.runtime-Dajn8myc.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();
@@ -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);
@@ -391,7 +434,7 @@ const LOCAL_HOST_ID$1 = "gateway";
391
434
  const DEFAULT_PAGE_LIMIT = 20;
392
435
  const MAX_PAGE_LIMIT$1 = 100;
393
436
  const MAX_SEARCH_LENGTH = 500;
394
- const MAX_CURSOR_LENGTH$1 = 128;
437
+ const MAX_CURSOR_LENGTH = 128;
395
438
  const MAX_TRANSCRIPT_ITEM_BYTES = 512 * 1024;
396
439
  const MAX_TRANSCRIPT_PAGE_BYTES = 20 * 1024 * 1024;
397
440
  const SESSION_ID_PATTERN$1 = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
@@ -408,18 +451,35 @@ function boundedLimit(value, fallback = DEFAULT_PAGE_LIMIT) {
408
451
  function encodeCursor(offset) {
409
452
  return Buffer.from(JSON.stringify({ offset }), "utf8").toString("base64url");
410
453
  }
454
+ function optionalRawCursor(value) {
455
+ if (value === void 0) return;
456
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_CURSOR_LENGTH) throw new Error("cursor is invalid");
457
+ return value;
458
+ }
411
459
  function decodeCursor(value) {
412
- if (value === void 0) return 0;
413
- const cursor = optionalPiString(value, MAX_CURSOR_LENGTH$1);
414
- if (!cursor) throw new Error("cursor is invalid");
460
+ const cursor = optionalRawCursor(value);
461
+ if (cursor === void 0) return 0;
415
462
  try {
416
- const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
417
- if (!isRecord(parsed) || !Number.isInteger(parsed.offset) || Number(parsed.offset) < 0) throw new Error("invalid offset");
418
- return Number(parsed.offset);
463
+ const bytes = Buffer.from(cursor, "base64url");
464
+ if (bytes.toString("base64url") !== cursor) throw new Error("non-canonical base64url");
465
+ const parsed = JSON.parse(bytes.toString("utf8"));
466
+ if (!isRecord(parsed) || !Number.isSafeInteger(parsed.offset) || Number(parsed.offset) < 0) throw new Error("invalid offset");
467
+ const offset = Number(parsed.offset);
468
+ if (encodeCursor(offset) !== cursor) throw new Error("non-canonical cursor payload");
469
+ return offset;
419
470
  } catch (error) {
420
471
  throw new Error("cursor is invalid", { cause: error });
421
472
  }
422
473
  }
474
+ function isExactPiSessionCursor(value) {
475
+ if (typeof value !== "string") return false;
476
+ try {
477
+ decodeCursor(value);
478
+ return true;
479
+ } catch {
480
+ return false;
481
+ }
482
+ }
423
483
  function truncateUtf8(text, maxBytes) {
424
484
  if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
425
485
  let low = 0;
@@ -455,7 +515,7 @@ function transcriptPage(items, limit, offset) {
455
515
  ...consumed < items.length ? { nextCursor: encodeCursor(consumed) } : {}
456
516
  };
457
517
  }
458
- function textFromContent(content) {
518
+ function textFromContent$1(content) {
459
519
  if (typeof content === "string") return content;
460
520
  if (!Array.isArray(content)) return "";
461
521
  return content.flatMap((part) => {
@@ -468,7 +528,7 @@ function textFromContent(content) {
468
528
  return [];
469
529
  }).join("\n");
470
530
  }
471
- function timestampMs(value) {
531
+ function timestampMs$1(value) {
472
532
  if (typeof value === "number" && Number.isFinite(value)) return value;
473
533
  if (typeof value === "string") {
474
534
  const parsed = Date.parse(value);
@@ -486,8 +546,7 @@ function parseListParams(value) {
486
546
  if (unknown) throw new Error(`unknown Pi session list parameter: ${unknown}`);
487
547
  const searchTerm = optionalPiString(value.searchTerm, MAX_SEARCH_LENGTH);
488
548
  if (value.searchTerm !== void 0 && !searchTerm) throw new Error("searchTerm is invalid");
489
- const cursor = optionalPiString(value.cursor, MAX_CURSOR_LENGTH$1);
490
- if (value.cursor !== void 0 && !cursor) throw new Error("cursor is invalid");
549
+ const cursor = optionalRawCursor(value.cursor);
491
550
  return {
492
551
  limit: boundedLimit(value.limit),
493
552
  ...searchTerm ? { searchTerm } : {},
@@ -504,8 +563,7 @@ function parseReadParams(value) {
504
563
  if (unknown) throw new Error(`unknown Pi session read parameter: ${unknown}`);
505
564
  const threadId = optionalPiString(value.threadId, 256);
506
565
  if (!threadId || !SESSION_ID_PATTERN$1.test(threadId)) throw new Error("threadId is invalid");
507
- const cursor = optionalPiString(value.cursor, MAX_CURSOR_LENGTH$1);
508
- if (value.cursor !== void 0 && !cursor) throw new Error("cursor is invalid");
566
+ const cursor = optionalRawCursor(value.cursor);
509
567
  return {
510
568
  threadId,
511
569
  limit: boundedLimit(value.limit),
@@ -520,14 +578,14 @@ async function listLocalPiSessionPage(value) {
520
578
  limit: params.limit,
521
579
  ...params.searchTerm ? { searchTerm: params.searchTerm } : {}
522
580
  });
523
- const page = summaries.map(({ file: _file, ...session }) => session);
581
+ const page = summaries.map(({ file: _file, version: _version, ...session }) => session);
524
582
  return {
525
583
  sessions: page,
526
584
  ...hasMore ? { nextCursor: encodeCursor(offset + page.length) } : {}
527
585
  };
528
586
  }
529
587
  function isoTimestamp(message, entry) {
530
- const value = timestampMs(message.timestamp) ?? timestampMs(entry.timestamp);
588
+ const value = timestampMs$1(message.timestamp) ?? timestampMs$1(entry.timestamp);
531
589
  if (value === void 0) return;
532
590
  const date = new Date(value);
533
591
  return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
@@ -573,7 +631,7 @@ function piMessageItems(entry) {
573
631
  ...modelRef ? { model: modelRef } : {}
574
632
  };
575
633
  if (role === "user") {
576
- const text = textFromContent(message.content);
634
+ const text = textFromContent$1(message.content);
577
635
  return text ? [{
578
636
  ...common,
579
637
  type: "userMessage",
@@ -582,7 +640,7 @@ function piMessageItems(entry) {
582
640
  }
583
641
  if (role === "toolResult") {
584
642
  const toolName = optionalPiString(message.toolName, 256);
585
- const text = textFromContent(message.content);
643
+ const text = textFromContent$1(message.content);
586
644
  return [{
587
645
  ...common,
588
646
  type: "toolResult",
@@ -607,7 +665,7 @@ function piMessageItems(entry) {
607
665
  if (role === "custom" || role === "hookMessage") {
608
666
  if (message.display !== true) return [];
609
667
  const customType = optionalPiString(message.customType, 256);
610
- const text = textFromContent(message.content);
668
+ const text = textFromContent$1(message.content);
611
669
  return text ? [{
612
670
  ...common,
613
671
  type: "other",
@@ -663,7 +721,7 @@ function piTranscriptItems(entries) {
663
721
  text: entry.summary
664
722
  }];
665
723
  if (entry.type === "custom_message" && entry.display === true) {
666
- const text = textFromContent(entry.content);
724
+ const text = textFromContent$1(entry.content);
667
725
  return text ? [{
668
726
  ...common,
669
727
  type: "other",
@@ -685,6 +743,121 @@ async function readLocalPiTranscriptPage(value) {
685
743
  };
686
744
  }
687
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
688
861
  //#region extensions/acpx/src/pi-session-catalog-plugin.ts
689
862
  const PI_SESSIONS_LIST_COMMAND = "acpx.pi.sessions.list.v1";
690
863
  const PI_SESSION_READ_COMMAND = "acpx.pi.sessions.read.v1";
@@ -693,7 +866,6 @@ const CAPABILITY = "pi-sessions";
693
866
  const LOCAL_HOST_ID = "gateway";
694
867
  const MAX_PAGE_LIMIT = 100;
695
868
  const MAX_HOSTS = 100;
696
- const MAX_CURSOR_LENGTH = 128;
697
869
  const NODE_TIMEOUT_MS = 2e4;
698
870
  const SESSION_ID_PATTERN = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
699
871
  const TRANSCRIPT_ITEM_TYPES = /* @__PURE__ */ new Set([
@@ -704,6 +876,11 @@ const TRANSCRIPT_ITEM_TYPES = /* @__PURE__ */ new Set([
704
876
  "toolResult",
705
877
  "other"
706
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();
707
884
  function validatePiThreadId(value) {
708
885
  if (typeof value !== "string" || !SESSION_ID_PATTERN.test(value)) throw new Error("INVALID_REQUEST: threadId is invalid");
709
886
  return value;
@@ -715,7 +892,7 @@ function isOptionalNumber(value) {
715
892
  return value === void 0 || typeof value === "number";
716
893
  }
717
894
  function isNodeSession(value) {
718
- 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.openClawSessionKey) && isOptionalNumber(value.createdAt) && isOptionalNumber(value.updatedAt) && isOptionalNumber(value.recencyAt);
895
+ 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);
719
896
  }
720
897
  function isNodeTranscriptItem(value) {
721
898
  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");
@@ -809,8 +986,11 @@ function nodeLabel(node) {
809
986
  function unwrapNodePayload(value) {
810
987
  return isRecord(value) && typeof value.payloadJSON === "string" ? JSON.parse(value.payloadJSON) : value;
811
988
  }
812
- function setTerminalCapability(page, canOpenTerminal) {
813
- 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
+ }
814
994
  return page;
815
995
  }
816
996
  async function listPiNodeHost(runtime, query, node) {
@@ -831,13 +1011,15 @@ async function listPiNodeHost(runtime, query, node) {
831
1011
  }
832
1012
  };
833
1013
  try {
1014
+ const cursor = query.cursors?.[hostId];
1015
+ if (cursor !== void 0 && !isExactPiSessionCursor(cursor)) throw new Error("cursor is invalid");
834
1016
  const page = parseNodeSessionPage(unwrapNodePayload(await runtime.nodes.invoke({
835
1017
  nodeId: node.nodeId,
836
1018
  command: PI_SESSIONS_LIST_COMMAND,
837
1019
  params: {
838
1020
  ...query.limitPerHost ? { limit: query.limitPerHost } : {},
839
1021
  ...query.search ? { searchTerm: query.search } : {},
840
- ...query.cursors?.[hostId] ? { cursor: query.cursors[hostId] } : {}
1022
+ ...cursor !== void 0 ? { cursor } : {}
841
1023
  },
842
1024
  timeoutMs: NODE_TIMEOUT_MS,
843
1025
  scopes: ["operator.write"]
@@ -845,7 +1027,10 @@ async function listPiNodeHost(runtime, query, node) {
845
1027
  const canOpenTerminal = (node.invocableCommands ?? node.commands)?.includes(PI_TERMINAL_RESUME_COMMAND) === true;
846
1028
  return {
847
1029
  ...common,
848
- ...setTerminalCapability(page, canOpenTerminal)
1030
+ ...setCatalogCapabilities(page, {
1031
+ canContinue: false,
1032
+ canOpenTerminal
1033
+ })
849
1034
  };
850
1035
  } catch {
851
1036
  return {
@@ -862,25 +1047,27 @@ function parseNodeSessionPage(value) {
862
1047
  if (!isRecord(value) || !Array.isArray(value.sessions) || value.sessions.length > MAX_PAGE_LIMIT) throw new Error("Pi node returned an invalid session page");
863
1048
  if (!value.sessions.every(isNodeSession)) throw new Error("Pi node returned an invalid session page");
864
1049
  const sessions = value.sessions;
865
- const nextCursor = optionalPiString(value.nextCursor, MAX_CURSOR_LENGTH);
866
- if (value.nextCursor !== void 0 && !nextCursor) throw new Error("Pi node returned an invalid cursor");
1050
+ const nextCursor = value.nextCursor;
1051
+ if (nextCursor !== void 0 && !isExactPiSessionCursor(nextCursor)) throw new Error("Pi node returned an invalid cursor");
867
1052
  return {
868
1053
  sessions,
869
- ...nextCursor ? { nextCursor } : {}
1054
+ ...nextCursor !== void 0 ? { nextCursor } : {}
870
1055
  };
871
1056
  }
872
1057
  function parseNodeTranscriptPage(value, threadId) {
873
1058
  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");
874
- const nextCursor = optionalPiString(value.nextCursor, MAX_CURSOR_LENGTH);
875
- if (value.nextCursor !== void 0 && !nextCursor) throw new Error("Pi node returned an invalid cursor");
1059
+ const nextCursor = value.nextCursor;
1060
+ if (nextCursor !== void 0 && !isExactPiSessionCursor(nextCursor)) throw new Error("Pi node returned an invalid cursor");
876
1061
  return {
877
1062
  hostId: LOCAL_HOST_ID,
878
1063
  threadId,
879
1064
  items: value.items,
880
- ...nextCursor ? { nextCursor } : {}
1065
+ ...nextCursor !== void 0 ? { nextCursor } : {}
881
1066
  };
882
1067
  }
883
- async function listPiHosts(runtime, query) {
1068
+ async function listPiHosts(api, query) {
1069
+ const runtime = api.runtime;
1070
+ const canContinue = resolvePiContinuationAvailability(api).available;
884
1071
  const requested = query.hostIds ? new Set(query.hostIds) : void 0;
885
1072
  const hosts = [];
886
1073
  if ((!requested || requested.has(LOCAL_HOST_ID)) && piSessionStoreAvailable(process$1.env)) try {
@@ -893,11 +1080,14 @@ async function listPiHosts(runtime, query) {
893
1080
  limit: query.limitPerHost,
894
1081
  ...query.search ? { searchTerm: query.search } : {},
895
1082
  cursor: query.cursors?.[LOCAL_HOST_ID]
896
- }).then((page) => setTerminalCapability(page, resolveNodeHostExecutable("pi", {
897
- env: process$1.env,
898
- pathEnv: process$1.env.PATH ?? "",
899
- strategy: "fallback"
900
- }) !== 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
+ }))
901
1091
  });
902
1092
  } catch {
903
1093
  hosts.push({
@@ -914,7 +1104,7 @@ async function listPiHosts(runtime, query) {
914
1104
  }
915
1105
  let nodes;
916
1106
  try {
917
- nodes = (await runtime.nodes.list()).nodes;
1107
+ nodes = (await (query.listNodes?.() ?? runtime.nodes.list())).nodes;
918
1108
  } catch {
919
1109
  return hosts;
920
1110
  }
@@ -930,6 +1120,95 @@ async function requireLocalPiSession(threadId) {
930
1120
  if (!record) throw new Error("Pi session is unavailable");
931
1121
  return record;
932
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
+ }
933
1212
  async function resolveNodePiSession(params) {
934
1213
  const record = parseNodeSessionPage(unwrapNodePayload(await params.runtime.nodes.invoke({
935
1214
  nodeId: params.nodeId,
@@ -987,10 +1266,12 @@ async function openPiTerminal(params) {
987
1266
  };
988
1267
  }
989
1268
  async function readPiTranscript(runtime, request) {
1269
+ const cursor = request.cursor;
1270
+ if (cursor !== void 0 && !isExactPiSessionCursor(cursor)) throw new Error("cursor is invalid");
990
1271
  if (request.hostId === LOCAL_HOST_ID) return await readLocalPiTranscriptPage({
991
1272
  threadId: request.threadId,
992
1273
  ...request.limit ? { limit: request.limit } : {},
993
- ...request.cursor ? { cursor: request.cursor } : {}
1274
+ ...cursor !== void 0 ? { cursor } : {}
994
1275
  });
995
1276
  if (!request.hostId.startsWith("node:")) throw new Error("hostId is invalid");
996
1277
  const nodeId = request.hostId.slice(5);
@@ -1003,7 +1284,7 @@ async function readPiTranscript(runtime, request) {
1003
1284
  params: {
1004
1285
  threadId: request.threadId,
1005
1286
  ...request.limit ? { limit: request.limit } : {},
1006
- ...request.cursor ? { cursor: request.cursor } : {}
1287
+ ...cursor !== void 0 ? { cursor } : {}
1007
1288
  },
1008
1289
  timeoutMs: NODE_TIMEOUT_MS,
1009
1290
  scopes: ["operator.write"]
@@ -1017,8 +1298,10 @@ function registerPiSessionCatalog(api) {
1017
1298
  api.registerSessionCatalog({
1018
1299
  id: "pi",
1019
1300
  label: "Pi",
1020
- list: async (query) => await listPiHosts(api.runtime, query),
1301
+ list: async (query) => await listPiHosts(api, query),
1021
1302
  read: async (request) => await readPiTranscript(api.runtime, request),
1303
+ continueSession: async (request) => await continuePiSession(api, request.hostId, request.threadId),
1304
+ checkUpstreamActivity: checkPiUpstreamActivity,
1022
1305
  openTerminal: async (request) => await openPiTerminal({
1023
1306
  runtime: api.runtime,
1024
1307
  ...request