@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.
- package/dist/doctor-contract-api.js +1 -1
- package/dist/index.js +327 -31
- package/dist/{process-lease-CU1cFI4I.js → process-lease-DSLDgiNl.js} +62 -1
- package/dist/{process-reaper-BIOtp_Ek.js → process-reaper-DFwbcdPa.js} +7 -51
- package/dist/{register.runtime-Dajn8myc.js → register.runtime-BbS2JTTv.js} +1 -1
- package/dist/register.runtime.js +1 -1
- package/dist/{runtime-CN4JQkdV.js → runtime-By_lR8uk.js} +274 -71
- package/dist/{service-DOsKw_R1.js → service-CBZSAymH.js} +5 -5
- package/package.json +8 -9
- package/npm-shrinkwrap.json +0 -2489
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as openAcpxProcessLeaseStateStore, d as
|
|
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-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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:
|
|
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
|
|
827
|
-
for (const session of page.sessions)
|
|
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
|
-
...
|
|
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(
|
|
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) =>
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
|
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,
|
|
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 };
|