@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 +294 -29
- package/package.json +5 -6
- package/npm-shrinkwrap.json +0 -2489
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(
|
|
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(
|
|
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(
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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:
|
|
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
|
|
827
|
-
for (const session of page.sessions)
|
|
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
|
-
...
|
|
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(
|
|
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) =>
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
46
|
+
"pluginApi": ">=2026.7.2-beta.5"
|
|
47
47
|
},
|
|
48
48
|
"build": {
|
|
49
|
-
"openclawVersion": "2026.7.2-beta.
|
|
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.
|
|
77
|
+
"openclaw": ">=2026.7.2-beta.5"
|
|
79
78
|
},
|
|
80
79
|
"peerDependenciesMeta": {
|
|
81
80
|
"openclaw": {
|