@openclaw/acpx 2026.7.1-beta.6 → 2026.7.2-beta.1
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/config-schema-lrk5nlcV.js +39 -0
- package/dist/index.js +1034 -2
- package/dist/{process-reaper-cncoDfwV.js → process-reaper-B29BzBk4.js} +15 -47
- package/dist/{register.runtime-Yz704dK5.js → register.runtime-DBqbPx5P.js} +1 -1
- package/dist/register.runtime.js +1 -1
- package/dist/{runtime-WjNm2DKO.js → runtime-D_fW1HnP.js} +20 -26
- package/dist/{service-CzJr7Koj.js → service-Dm8hpy-C.js} +51 -31
- package/dist/setup-api.js +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/openclaw.plugin.json +13 -3
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,20 +1,1052 @@
|
|
|
1
|
-
import { t as createAcpxRuntimeService } from "./register.runtime-
|
|
1
|
+
import { t as createAcpxRuntimeService } from "./register.runtime-DBqbPx5P.js";
|
|
2
|
+
import "./config-schema-lrk5nlcV.js";
|
|
2
3
|
import { tryDispatchAcpReplyHook } from "openclaw/plugin-sdk/acp-runtime-backend";
|
|
4
|
+
import { finiteSecondsToTimerSafeMilliseconds } from "openclaw/plugin-sdk/number-runtime";
|
|
5
|
+
import process$1 from "node:process";
|
|
6
|
+
import { decodeNodePtyResumeParams, resolveExecutableFromPathEnv, runNodePtyCommand } from "openclaw/plugin-sdk/node-host";
|
|
7
|
+
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
8
|
+
import { createReadStream, readFileSync, statSync } from "node:fs";
|
|
9
|
+
import fs$1 from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
//#region extensions/acpx/src/pi-session-paths.ts
|
|
13
|
+
function optionalString$1(value, maxLength) {
|
|
14
|
+
if (typeof value !== "string") return;
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
return trimmed && trimmed.length <= maxLength ? trimmed : void 0;
|
|
17
|
+
}
|
|
18
|
+
function piHome(env) {
|
|
19
|
+
return (process.platform === "win32" ? env.USERPROFILE?.trim() : env.HOME?.trim()) || os.homedir();
|
|
20
|
+
}
|
|
21
|
+
function isPiSessionCatalogPathAbsolute(value, platform = process.platform) {
|
|
22
|
+
if (platform !== "win32") return path.posix.isAbsolute(value);
|
|
23
|
+
const root = path.win32.parse(value).root;
|
|
24
|
+
return path.win32.isAbsolute(value) && root !== "\\" && root !== "/";
|
|
25
|
+
}
|
|
26
|
+
function resolveConfiguredPath(value, env, relativeBase) {
|
|
27
|
+
const home = piHome(env);
|
|
28
|
+
let resolved = value;
|
|
29
|
+
if (value === "~") resolved = home;
|
|
30
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) resolved = path.join(home, value.slice(2));
|
|
31
|
+
if (!isPiSessionCatalogPathAbsolute(resolved)) {
|
|
32
|
+
if (relativeBase) return path.resolve(relativeBase, resolved);
|
|
33
|
+
throw new Error("Pi session catalog requires absolute or home-relative storage paths");
|
|
34
|
+
}
|
|
35
|
+
return path.resolve(resolved);
|
|
36
|
+
}
|
|
37
|
+
function settingsSessionDir(file) {
|
|
38
|
+
try {
|
|
39
|
+
const value = JSON.parse(readFileSync(file, "utf8"));
|
|
40
|
+
return isRecord(value) ? optionalString$1(value.sessionDir, 4096) : void 0;
|
|
41
|
+
} catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function piSessionStore(env, cwd = process.cwd()) {
|
|
46
|
+
const customSessionDir = env.PI_CODING_AGENT_SESSION_DIR?.trim();
|
|
47
|
+
if (customSessionDir) return {
|
|
48
|
+
root: resolveConfiguredPath(customSessionDir, env),
|
|
49
|
+
flat: true
|
|
50
|
+
};
|
|
51
|
+
const home = piHome(env);
|
|
52
|
+
const customAgentDir = env.PI_CODING_AGENT_DIR?.trim();
|
|
53
|
+
const agentDir = customAgentDir ? resolveConfiguredPath(customAgentDir, env) : path.join(home, ".pi", "agent");
|
|
54
|
+
const projectSessionDir = settingsSessionDir(path.join(cwd, ".pi", "settings.json"));
|
|
55
|
+
if (projectSessionDir) return {
|
|
56
|
+
root: resolveConfiguredPath(projectSessionDir, env, path.join(cwd, ".pi")),
|
|
57
|
+
flat: true
|
|
58
|
+
};
|
|
59
|
+
const globalSessionDir = settingsSessionDir(path.join(agentDir, "settings.json"));
|
|
60
|
+
if (globalSessionDir) return {
|
|
61
|
+
root: resolveConfiguredPath(globalSessionDir, env, agentDir),
|
|
62
|
+
flat: true
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
root: path.join(agentDir, "sessions"),
|
|
66
|
+
flat: false
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function piSessionStoreAvailable(env) {
|
|
70
|
+
try {
|
|
71
|
+
return statSync(piSessionStore(env).root).isDirectory();
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region extensions/acpx/src/pi-session-store.ts
|
|
78
|
+
const MAX_DISCOVERY_FILES = 1e4;
|
|
79
|
+
const SUMMARY_SCAN_BATCH_SIZE = 100;
|
|
80
|
+
const MAX_SUMMARY_CACHE_ENTRIES = 256;
|
|
81
|
+
const MAX_SESSION_BYTES = 32 * 1024 * 1024;
|
|
82
|
+
const MAX_SUMMARY_LINE_BYTES = 1024 * 1024;
|
|
83
|
+
const APPEND_PROOF_EDGE_BYTES = 64 * 1024;
|
|
84
|
+
const IO_CONCURRENCY = 8;
|
|
85
|
+
const SESSION_ID_PATTERN$2 = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
|
|
86
|
+
const summaryCache = /* @__PURE__ */ new Map();
|
|
87
|
+
const threadFileCache = /* @__PURE__ */ new Map();
|
|
88
|
+
function threadCacheKey(storeRoot, threadId) {
|
|
89
|
+
return `${storeRoot}\0${threadId}`;
|
|
90
|
+
}
|
|
91
|
+
function forgetCachedSummary(file) {
|
|
92
|
+
const cached = summaryCache.get(file);
|
|
93
|
+
const threadId = cached?.summary?.threadId;
|
|
94
|
+
if (cached && threadId) {
|
|
95
|
+
const key = threadCacheKey(cached.storeRoot, threadId);
|
|
96
|
+
if (threadFileCache.get(key) === file) threadFileCache.delete(key);
|
|
97
|
+
}
|
|
98
|
+
summaryCache.delete(file);
|
|
99
|
+
}
|
|
100
|
+
function cacheSummary(file, value) {
|
|
101
|
+
forgetCachedSummary(file);
|
|
102
|
+
summaryCache.set(file, value);
|
|
103
|
+
while (summaryCache.size > MAX_SUMMARY_CACHE_ENTRIES) {
|
|
104
|
+
const oldest = summaryCache.keys().next().value;
|
|
105
|
+
if (typeof oldest !== "string") break;
|
|
106
|
+
forgetCachedSummary(oldest);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function optionalString(value, maxLength) {
|
|
110
|
+
if (typeof value !== "string") return;
|
|
111
|
+
const trimmed = value.trim();
|
|
112
|
+
return trimmed && trimmed.length <= maxLength ? trimmed : void 0;
|
|
113
|
+
}
|
|
114
|
+
async function discoverPiSessionFiles(env) {
|
|
115
|
+
const store = piSessionStore(env);
|
|
116
|
+
let entries;
|
|
117
|
+
try {
|
|
118
|
+
entries = await fs$1.readdir(store.root, { withFileTypes: true });
|
|
119
|
+
} catch {
|
|
120
|
+
return {
|
|
121
|
+
root: store.root,
|
|
122
|
+
files: []
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (store.flat) return {
|
|
126
|
+
root: store.root,
|
|
127
|
+
files: entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).slice(0, MAX_DISCOVERY_FILES).map((entry) => path.join(store.root, entry.name))
|
|
128
|
+
};
|
|
129
|
+
const files = [];
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
if (!entry.isDirectory() || files.length >= MAX_DISCOVERY_FILES) continue;
|
|
132
|
+
const directory = path.join(store.root, entry.name);
|
|
133
|
+
let children;
|
|
134
|
+
try {
|
|
135
|
+
children = await fs$1.readdir(directory, { withFileTypes: true });
|
|
136
|
+
} catch {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
for (const child of children) if (child.isFile() && child.name.endsWith(".jsonl")) {
|
|
140
|
+
files.push(path.join(directory, child.name));
|
|
141
|
+
if (files.length >= MAX_DISCOVERY_FILES) break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
root: store.root,
|
|
146
|
+
files
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function mapConcurrent(values, limit, mapper) {
|
|
150
|
+
const results = [];
|
|
151
|
+
results.length = values.length;
|
|
152
|
+
let nextIndex = 0;
|
|
153
|
+
const workers = Array.from({ length: Math.min(limit, values.length) }, async () => {
|
|
154
|
+
while (nextIndex < values.length) {
|
|
155
|
+
const index = nextIndex++;
|
|
156
|
+
results[index] = await mapper(values[index]);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
await Promise.all(workers);
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
async function piFileCandidates(env) {
|
|
163
|
+
const { root, files } = await discoverPiSessionFiles(env);
|
|
164
|
+
return (await mapConcurrent(files, IO_CONCURRENCY, async (file) => {
|
|
165
|
+
try {
|
|
166
|
+
const stats = await fs$1.stat(file);
|
|
167
|
+
return stats.isFile() ? {
|
|
168
|
+
file,
|
|
169
|
+
storeRoot: root,
|
|
170
|
+
identity: `${String(stats.dev)}:${String(stats.ino)}:${String(stats.birthtimeMs)}`,
|
|
171
|
+
mtimeMs: stats.mtimeMs,
|
|
172
|
+
size: stats.size
|
|
173
|
+
} : void 0;
|
|
174
|
+
} catch {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
})).filter((candidate) => candidate !== void 0).toSorted((left, right) => right.mtimeMs - left.mtimeMs);
|
|
178
|
+
}
|
|
179
|
+
function parsePiJsonLines(content) {
|
|
180
|
+
return content.split(/\r?\n/u).flatMap((line) => {
|
|
181
|
+
if (!line.trim()) return [];
|
|
182
|
+
try {
|
|
183
|
+
const value = JSON.parse(line);
|
|
184
|
+
return isRecord(value) ? [value] : [];
|
|
185
|
+
} catch {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
function textFromContent$1(content) {
|
|
191
|
+
if (typeof content === "string") return content;
|
|
192
|
+
if (!Array.isArray(content)) return "";
|
|
193
|
+
return content.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n");
|
|
194
|
+
}
|
|
195
|
+
function timestampMs$1(value) {
|
|
196
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
197
|
+
if (typeof value === "string") {
|
|
198
|
+
const parsed = Date.parse(value);
|
|
199
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function processSummaryLine(state, line) {
|
|
203
|
+
const entry = parsePiJsonLines((line.at(-1) === 13 ? line.subarray(0, -1) : line).toString("utf8"))[0];
|
|
204
|
+
if (!entry) return;
|
|
205
|
+
if (!state.header) {
|
|
206
|
+
if (entry.type !== "session") {
|
|
207
|
+
state.invalid = true;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
state.header = entry;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (entry.type === "session_info") state.name = optionalString(entry.name, 1e3);
|
|
214
|
+
else if (!state.firstMessage && entry.type === "message" && isRecord(entry.message) && entry.message.role === "user") state.firstMessage = optionalString(textFromContent$1(entry.message.content), 1e3);
|
|
215
|
+
}
|
|
216
|
+
function appendSummaryBytes(state, bytes) {
|
|
217
|
+
if (state.discarding || bytes.length === 0) return;
|
|
218
|
+
if (state.pending.length + bytes.length > MAX_SUMMARY_LINE_BYTES) {
|
|
219
|
+
state.pending = Buffer.alloc(0);
|
|
220
|
+
state.discarding = true;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
state.pending = state.pending.length === 0 ? Buffer.from(bytes) : Buffer.concat([state.pending, bytes]);
|
|
224
|
+
}
|
|
225
|
+
async function scanSummaryAppend(candidate, start, state) {
|
|
226
|
+
if (start >= candidate.size || state.invalid) return;
|
|
227
|
+
const stream = createReadStream(candidate.file, {
|
|
228
|
+
start,
|
|
229
|
+
end: candidate.size - 1
|
|
230
|
+
});
|
|
231
|
+
for await (const value of stream) {
|
|
232
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
233
|
+
let offset = 0;
|
|
234
|
+
while (offset < chunk.length) {
|
|
235
|
+
const newline = chunk.indexOf(10, offset);
|
|
236
|
+
const end = newline < 0 ? chunk.length : newline;
|
|
237
|
+
appendSummaryBytes(state, chunk.subarray(offset, end));
|
|
238
|
+
if (newline < 0) break;
|
|
239
|
+
if (!state.discarding) processSummaryLine(state, state.pending);
|
|
240
|
+
state.pending = Buffer.alloc(0);
|
|
241
|
+
state.discarding = false;
|
|
242
|
+
if (state.invalid) return;
|
|
243
|
+
offset = newline + 1;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function readAppendProof(file, size) {
|
|
248
|
+
const length = Math.min(size, APPEND_PROOF_EDGE_BYTES);
|
|
249
|
+
if (length === 0) return {
|
|
250
|
+
head: Buffer.alloc(0),
|
|
251
|
+
tail: Buffer.alloc(0)
|
|
252
|
+
};
|
|
253
|
+
const handle = await fs$1.open(file, "r");
|
|
254
|
+
try {
|
|
255
|
+
const head = Buffer.alloc(length);
|
|
256
|
+
const tail = Buffer.alloc(length);
|
|
257
|
+
const [headRead, tailRead] = await Promise.all([handle.read(head, 0, length, 0), handle.read(tail, 0, length, size - length)]);
|
|
258
|
+
return {
|
|
259
|
+
head: head.subarray(0, headRead.bytesRead),
|
|
260
|
+
tail: tail.subarray(0, tailRead.bytesRead)
|
|
261
|
+
};
|
|
262
|
+
} finally {
|
|
263
|
+
await handle.close();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async function cachedPrefixIsUnchanged(candidate, cached) {
|
|
267
|
+
if (cached.identity !== candidate.identity || cached.size >= candidate.size) return false;
|
|
268
|
+
const current = await readAppendProof(candidate.file, cached.size);
|
|
269
|
+
return current.head.equals(cached.appendProof.head) && current.tail.equals(cached.appendProof.tail);
|
|
270
|
+
}
|
|
271
|
+
async function readPiSessionSummary(candidate) {
|
|
272
|
+
const cached = summaryCache.get(candidate.file);
|
|
273
|
+
if (cached?.mtimeMs === candidate.mtimeMs && cached.size === candidate.size) {
|
|
274
|
+
summaryCache.delete(candidate.file);
|
|
275
|
+
summaryCache.set(candidate.file, cached);
|
|
276
|
+
return cached.summary;
|
|
277
|
+
}
|
|
278
|
+
let summary;
|
|
279
|
+
let scanState;
|
|
280
|
+
let appendProof;
|
|
281
|
+
try {
|
|
282
|
+
const resumable = cached && await cachedPrefixIsUnchanged(candidate, cached) ? cached : void 0;
|
|
283
|
+
scanState = resumable ? {
|
|
284
|
+
...resumable.scanState,
|
|
285
|
+
pending: Buffer.from(resumable.scanState.pending)
|
|
286
|
+
} : {
|
|
287
|
+
pending: Buffer.alloc(0),
|
|
288
|
+
discarding: false,
|
|
289
|
+
invalid: false
|
|
290
|
+
};
|
|
291
|
+
await scanSummaryAppend(candidate, resumable?.size ?? 0, scanState);
|
|
292
|
+
appendProof = await readAppendProof(candidate.file, candidate.size);
|
|
293
|
+
const projectedState = {
|
|
294
|
+
...scanState,
|
|
295
|
+
pending: Buffer.from(scanState.pending)
|
|
296
|
+
};
|
|
297
|
+
if (!projectedState.discarding && projectedState.pending.length > 0) processSummaryLine(projectedState, projectedState.pending);
|
|
298
|
+
const { header, name, firstMessage } = projectedState;
|
|
299
|
+
const threadId = header?.type === "session" ? optionalString(header.id, 256) : void 0;
|
|
300
|
+
if (header && threadId && SESSION_ID_PATTERN$2.test(threadId)) {
|
|
301
|
+
const cwd = optionalString(header.cwd, 4096);
|
|
302
|
+
const createdAt = timestampMs$1(header.timestamp);
|
|
303
|
+
summary = {
|
|
304
|
+
file: candidate.file,
|
|
305
|
+
threadId,
|
|
306
|
+
...name || firstMessage ? { name: name ?? firstMessage } : {},
|
|
307
|
+
...cwd ? { cwd } : {},
|
|
308
|
+
status: "stored",
|
|
309
|
+
...createdAt !== void 0 ? { createdAt } : {},
|
|
310
|
+
updatedAt: candidate.mtimeMs,
|
|
311
|
+
recencyAt: candidate.mtimeMs,
|
|
312
|
+
source: "pi-cli",
|
|
313
|
+
modelProvider: "pi",
|
|
314
|
+
archived: false,
|
|
315
|
+
canContinue: false,
|
|
316
|
+
canArchive: false
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
return cached?.summary;
|
|
321
|
+
}
|
|
322
|
+
if (cached?.summary?.threadId && cached.summary.threadId !== summary?.threadId) threadFileCache.delete(threadCacheKey(cached.storeRoot, cached.summary.threadId));
|
|
323
|
+
cacheSummary(candidate.file, {
|
|
324
|
+
...candidate,
|
|
325
|
+
summary,
|
|
326
|
+
scanState,
|
|
327
|
+
appendProof
|
|
328
|
+
});
|
|
329
|
+
if (summary) threadFileCache.set(threadCacheKey(candidate.storeRoot, summary.threadId), candidate.file);
|
|
330
|
+
return summary;
|
|
331
|
+
}
|
|
332
|
+
function summaryMatches(summary, needle) {
|
|
333
|
+
if (!needle) return true;
|
|
334
|
+
return [
|
|
335
|
+
summary.threadId,
|
|
336
|
+
summary.name,
|
|
337
|
+
summary.cwd
|
|
338
|
+
].some((field) => field?.toLocaleLowerCase().includes(needle));
|
|
339
|
+
}
|
|
340
|
+
async function listPiSummaryPage(env, params) {
|
|
341
|
+
const candidates = await piFileCandidates(env);
|
|
342
|
+
const activeFiles = new Set(candidates.map((candidate) => candidate.file));
|
|
343
|
+
for (const file of summaryCache.keys()) if (!activeFiles.has(file)) forgetCachedSummary(file);
|
|
344
|
+
const target = params.offset + params.limit + 1;
|
|
345
|
+
const matches = [];
|
|
346
|
+
const needle = params.searchTerm?.toLocaleLowerCase();
|
|
347
|
+
for (let index = 0; index < candidates.length && matches.length < target; index += SUMMARY_SCAN_BATCH_SIZE) {
|
|
348
|
+
const summaries = await mapConcurrent(candidates.slice(index, index + SUMMARY_SCAN_BATCH_SIZE), IO_CONCURRENCY, readPiSessionSummary);
|
|
349
|
+
for (const summary of summaries) if (summary && summaryMatches(summary, needle)) {
|
|
350
|
+
matches.push(summary);
|
|
351
|
+
if (matches.length >= target) break;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
summaries: matches.slice(params.offset, params.offset + params.limit),
|
|
356
|
+
hasMore: matches.length > params.offset + params.limit
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
async function findPiSummary(threadId, env) {
|
|
360
|
+
const candidates = await piFileCandidates(env);
|
|
361
|
+
for (let index = 0; index < candidates.length; index += SUMMARY_SCAN_BATCH_SIZE) {
|
|
362
|
+
const match = (await mapConcurrent(candidates.slice(index, index + SUMMARY_SCAN_BATCH_SIZE), IO_CONCURRENCY, readPiSessionSummary)).find((summary) => summary?.threadId === threadId);
|
|
363
|
+
if (match) return match;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async function readPiSessionById(threadId, env) {
|
|
367
|
+
const cacheKey = threadCacheKey(piSessionStore(env).root, threadId);
|
|
368
|
+
let file = threadFileCache.get(cacheKey);
|
|
369
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
370
|
+
if (!file) file = (await findPiSummary(threadId, env))?.file;
|
|
371
|
+
if (!file) throw new Error("Pi session was not found");
|
|
372
|
+
try {
|
|
373
|
+
const stats = await fs$1.stat(file);
|
|
374
|
+
if (!stats.isFile()) throw new Error("Pi session is not a file");
|
|
375
|
+
if (stats.size > MAX_SESSION_BYTES) throw new RangeError("Pi session exceeds the 32 MiB read safety limit");
|
|
376
|
+
const entries = parsePiJsonLines(await fs$1.readFile(file, "utf8"));
|
|
377
|
+
if (entries[0]?.type === "session" && entries[0].id === threadId) return entries;
|
|
378
|
+
} catch (error) {
|
|
379
|
+
if (error instanceof RangeError) throw error;
|
|
380
|
+
if (attempt > 0) throw new Error("Pi session is unavailable", { cause: error });
|
|
381
|
+
}
|
|
382
|
+
threadFileCache.delete(cacheKey);
|
|
383
|
+
file = void 0;
|
|
384
|
+
}
|
|
385
|
+
throw new Error("Pi session changed during read");
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
//#region extensions/acpx/src/pi-session-catalog.ts
|
|
389
|
+
const LOCAL_HOST_ID$1 = "gateway";
|
|
390
|
+
const DEFAULT_PAGE_LIMIT = 20;
|
|
391
|
+
const MAX_PAGE_LIMIT$1 = 100;
|
|
392
|
+
const MAX_SEARCH_LENGTH$1 = 500;
|
|
393
|
+
const MAX_CURSOR_LENGTH$1 = 128;
|
|
394
|
+
const MAX_TRANSCRIPT_ITEM_BYTES = 512 * 1024;
|
|
395
|
+
const MAX_TRANSCRIPT_PAGE_BYTES = 20 * 1024 * 1024;
|
|
396
|
+
const SESSION_ID_PATTERN$1 = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
|
|
397
|
+
function optionalPiString(value, maxLength) {
|
|
398
|
+
if (typeof value !== "string") return;
|
|
399
|
+
const trimmed = value.trim();
|
|
400
|
+
return trimmed && trimmed.length <= maxLength ? trimmed : void 0;
|
|
401
|
+
}
|
|
402
|
+
function boundedLimit(value, fallback = DEFAULT_PAGE_LIMIT) {
|
|
403
|
+
if (value === void 0) return fallback;
|
|
404
|
+
if (!Number.isInteger(value) || Number(value) < 1 || Number(value) > MAX_PAGE_LIMIT$1) throw new Error(`limit must be an integer between 1 and ${String(MAX_PAGE_LIMIT$1)}`);
|
|
405
|
+
return Number(value);
|
|
406
|
+
}
|
|
407
|
+
function encodeCursor(offset) {
|
|
408
|
+
return Buffer.from(JSON.stringify({ offset }), "utf8").toString("base64url");
|
|
409
|
+
}
|
|
410
|
+
function decodeCursor(value) {
|
|
411
|
+
if (value === void 0) return 0;
|
|
412
|
+
const cursor = optionalPiString(value, MAX_CURSOR_LENGTH$1);
|
|
413
|
+
if (!cursor) throw new Error("cursor is invalid");
|
|
414
|
+
try {
|
|
415
|
+
const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
416
|
+
if (!isRecord(parsed) || !Number.isInteger(parsed.offset) || Number(parsed.offset) < 0) throw new Error("invalid offset");
|
|
417
|
+
return Number(parsed.offset);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
throw new Error("cursor is invalid", { cause: error });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function truncateUtf8(text, maxBytes) {
|
|
423
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
424
|
+
let low = 0;
|
|
425
|
+
let high = text.length;
|
|
426
|
+
while (low < high) {
|
|
427
|
+
const middle = Math.ceil((low + high) / 2);
|
|
428
|
+
if (Buffer.byteLength(text.slice(0, middle), "utf8") <= maxBytes - 3) low = middle;
|
|
429
|
+
else high = middle - 1;
|
|
430
|
+
}
|
|
431
|
+
const end = low > 0 && /[\uD800-\uDBFF]/u.test(text.charAt(low - 1)) ? low - 1 : low;
|
|
432
|
+
return `${text.slice(0, end)}…`;
|
|
433
|
+
}
|
|
434
|
+
function transcriptPage(items, limit, offset) {
|
|
435
|
+
const end = Math.max(0, items.length - offset);
|
|
436
|
+
const start = Math.max(0, end - limit);
|
|
437
|
+
const page = [];
|
|
438
|
+
let pageBytes = 2;
|
|
439
|
+
for (let index = end - 1; index >= start; index -= 1) {
|
|
440
|
+
const item = items[index];
|
|
441
|
+
if (!item) continue;
|
|
442
|
+
const bounded = {
|
|
443
|
+
...item,
|
|
444
|
+
text: truncateUtf8(item.text ?? "", MAX_TRANSCRIPT_ITEM_BYTES)
|
|
445
|
+
};
|
|
446
|
+
const itemBytes = Buffer.byteLength(JSON.stringify(bounded), "utf8") + 1;
|
|
447
|
+
if (page.length > 0 && pageBytes + itemBytes > MAX_TRANSCRIPT_PAGE_BYTES) break;
|
|
448
|
+
page.unshift(bounded);
|
|
449
|
+
pageBytes += itemBytes;
|
|
450
|
+
}
|
|
451
|
+
const consumed = offset + page.length;
|
|
452
|
+
return {
|
|
453
|
+
items: page,
|
|
454
|
+
...consumed < items.length ? { nextCursor: encodeCursor(consumed) } : {}
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function textFromContent(content) {
|
|
458
|
+
if (typeof content === "string") return content;
|
|
459
|
+
if (!Array.isArray(content)) return "";
|
|
460
|
+
return content.flatMap((part) => {
|
|
461
|
+
if (!isRecord(part)) return [];
|
|
462
|
+
if (part.type === "text" && typeof part.text === "string") return [part.text];
|
|
463
|
+
if (part.type === "image") {
|
|
464
|
+
const mimeType = optionalPiString(part.mimeType, 128);
|
|
465
|
+
return [mimeType ? `[image: ${mimeType}]` : "[image]"];
|
|
466
|
+
}
|
|
467
|
+
return [];
|
|
468
|
+
}).join("\n");
|
|
469
|
+
}
|
|
470
|
+
function timestampMs(value) {
|
|
471
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
472
|
+
if (typeof value === "string") {
|
|
473
|
+
const parsed = Date.parse(value);
|
|
474
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function parseListParams(value) {
|
|
478
|
+
if (value === void 0 || value === null) return { limit: DEFAULT_PAGE_LIMIT };
|
|
479
|
+
if (!isRecord(value)) throw new Error("Pi session list parameters must be an object");
|
|
480
|
+
const unknown = Object.keys(value).find((key) => ![
|
|
481
|
+
"searchTerm",
|
|
482
|
+
"limit",
|
|
483
|
+
"cursor"
|
|
484
|
+
].includes(key));
|
|
485
|
+
if (unknown) throw new Error(`unknown Pi session list parameter: ${unknown}`);
|
|
486
|
+
const searchTerm = optionalPiString(value.searchTerm, MAX_SEARCH_LENGTH$1);
|
|
487
|
+
if (value.searchTerm !== void 0 && !searchTerm) throw new Error("searchTerm is invalid");
|
|
488
|
+
const cursor = optionalPiString(value.cursor, MAX_CURSOR_LENGTH$1);
|
|
489
|
+
if (value.cursor !== void 0 && !cursor) throw new Error("cursor is invalid");
|
|
490
|
+
return {
|
|
491
|
+
limit: boundedLimit(value.limit),
|
|
492
|
+
...searchTerm ? { searchTerm } : {},
|
|
493
|
+
...cursor ? { cursor } : {}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function parseReadParams(value) {
|
|
497
|
+
if (!isRecord(value)) throw new Error("Pi session read parameters must be an object");
|
|
498
|
+
const unknown = Object.keys(value).find((key) => ![
|
|
499
|
+
"threadId",
|
|
500
|
+
"limit",
|
|
501
|
+
"cursor"
|
|
502
|
+
].includes(key));
|
|
503
|
+
if (unknown) throw new Error(`unknown Pi session read parameter: ${unknown}`);
|
|
504
|
+
const threadId = optionalPiString(value.threadId, 256);
|
|
505
|
+
if (!threadId || !SESSION_ID_PATTERN$1.test(threadId)) throw new Error("threadId is invalid");
|
|
506
|
+
const cursor = optionalPiString(value.cursor, MAX_CURSOR_LENGTH$1);
|
|
507
|
+
if (value.cursor !== void 0 && !cursor) throw new Error("cursor is invalid");
|
|
508
|
+
return {
|
|
509
|
+
threadId,
|
|
510
|
+
limit: boundedLimit(value.limit),
|
|
511
|
+
...cursor ? { cursor } : {}
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
async function listLocalPiSessionPage(value) {
|
|
515
|
+
const params = parseListParams(value);
|
|
516
|
+
const offset = decodeCursor(params.cursor);
|
|
517
|
+
const { summaries, hasMore } = await listPiSummaryPage(process$1.env, {
|
|
518
|
+
offset,
|
|
519
|
+
limit: params.limit,
|
|
520
|
+
...params.searchTerm ? { searchTerm: params.searchTerm } : {}
|
|
521
|
+
});
|
|
522
|
+
const page = summaries.map(({ file: _file, ...session }) => session);
|
|
523
|
+
return {
|
|
524
|
+
sessions: page,
|
|
525
|
+
...hasMore ? { nextCursor: encodeCursor(offset + page.length) } : {}
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
function isoTimestamp(message, entry) {
|
|
529
|
+
const value = timestampMs(message.timestamp) ?? timestampMs(entry.timestamp);
|
|
530
|
+
if (value === void 0) return;
|
|
531
|
+
const date = new Date(value);
|
|
532
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
533
|
+
}
|
|
534
|
+
function jsonText(value, maxLength = 2e4) {
|
|
535
|
+
try {
|
|
536
|
+
const text = JSON.stringify(value);
|
|
537
|
+
return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;
|
|
538
|
+
} catch {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function activePiEntries(entries) {
|
|
543
|
+
const header = entries[0];
|
|
544
|
+
if ((header?.type === "session" && typeof header.version === "number" ? header.version : 1) < 2) return entries.slice(1);
|
|
545
|
+
const body = entries.filter((entry) => entry.type !== "session" && optionalPiString(entry.id, 256));
|
|
546
|
+
const byId = new Map(body.map((entry) => [String(entry.id), entry]));
|
|
547
|
+
const active = [];
|
|
548
|
+
let current = body.at(-1);
|
|
549
|
+
const visited = /* @__PURE__ */ new Set();
|
|
550
|
+
while (current) {
|
|
551
|
+
const id = String(current.id);
|
|
552
|
+
if (visited.has(id)) break;
|
|
553
|
+
visited.add(id);
|
|
554
|
+
active.push(current);
|
|
555
|
+
const parentId = optionalPiString(current.parentId, 256);
|
|
556
|
+
current = parentId ? byId.get(parentId) : void 0;
|
|
557
|
+
}
|
|
558
|
+
return active.toReversed();
|
|
559
|
+
}
|
|
560
|
+
function piMessageItems(entry) {
|
|
561
|
+
if (!isRecord(entry.message)) return [];
|
|
562
|
+
const message = entry.message;
|
|
563
|
+
const role = message.role;
|
|
564
|
+
const id = optionalPiString(entry.id, 256);
|
|
565
|
+
const timestamp = isoTimestamp(message, entry);
|
|
566
|
+
const model = optionalPiString(message.model, 256);
|
|
567
|
+
const provider = optionalPiString(message.provider, 256);
|
|
568
|
+
const modelRef = provider && model ? `${provider}/${model}` : model;
|
|
569
|
+
const common = {
|
|
570
|
+
...id ? { id } : {},
|
|
571
|
+
...timestamp ? { timestamp } : {},
|
|
572
|
+
...modelRef ? { model: modelRef } : {}
|
|
573
|
+
};
|
|
574
|
+
if (role === "user") {
|
|
575
|
+
const text = textFromContent(message.content);
|
|
576
|
+
return text ? [{
|
|
577
|
+
...common,
|
|
578
|
+
type: "userMessage",
|
|
579
|
+
text
|
|
580
|
+
}] : [];
|
|
581
|
+
}
|
|
582
|
+
if (role === "toolResult") {
|
|
583
|
+
const toolName = optionalPiString(message.toolName, 256);
|
|
584
|
+
const text = textFromContent(message.content);
|
|
585
|
+
return [{
|
|
586
|
+
...common,
|
|
587
|
+
type: "toolResult",
|
|
588
|
+
text: toolName ? `${toolName}\n${text}` : text
|
|
589
|
+
}];
|
|
590
|
+
}
|
|
591
|
+
if (role === "bashExecution") {
|
|
592
|
+
const command = optionalPiString(message.command, 4096) ?? "bash";
|
|
593
|
+
const output = typeof message.output === "string" ? message.output : "";
|
|
594
|
+
const status = message.cancelled === true ? "command cancelled" : typeof message.exitCode === "number" && message.exitCode !== 0 ? `command exited with code ${String(message.exitCode)}` : "";
|
|
595
|
+
return [{
|
|
596
|
+
...common,
|
|
597
|
+
type: "toolCall",
|
|
598
|
+
text: `bash\n${command}`
|
|
599
|
+
}, {
|
|
600
|
+
...common,
|
|
601
|
+
...id ? { id: `${id}:result` } : {},
|
|
602
|
+
type: "toolResult",
|
|
603
|
+
text: [output, status].filter(Boolean).join("\n\n")
|
|
604
|
+
}];
|
|
605
|
+
}
|
|
606
|
+
if (role === "custom" || role === "hookMessage") {
|
|
607
|
+
if (message.display !== true) return [];
|
|
608
|
+
const customType = optionalPiString(message.customType, 256);
|
|
609
|
+
const text = textFromContent(message.content);
|
|
610
|
+
return text ? [{
|
|
611
|
+
...common,
|
|
612
|
+
type: "other",
|
|
613
|
+
text: customType ? `${customType}\n${text}` : text
|
|
614
|
+
}] : [];
|
|
615
|
+
}
|
|
616
|
+
if (role !== "assistant" || !Array.isArray(message.content)) return [];
|
|
617
|
+
return message.content.flatMap((part, index) => {
|
|
618
|
+
if (!isRecord(part)) return [];
|
|
619
|
+
const partCommon = {
|
|
620
|
+
...common,
|
|
621
|
+
...id ? { id: `${id}:${String(index)}` } : {}
|
|
622
|
+
};
|
|
623
|
+
if (part.type === "text" && typeof part.text === "string") return [{
|
|
624
|
+
...partCommon,
|
|
625
|
+
type: "agentMessage",
|
|
626
|
+
text: part.text
|
|
627
|
+
}];
|
|
628
|
+
if (part.type === "thinking" && typeof part.thinking === "string") return [{
|
|
629
|
+
...partCommon,
|
|
630
|
+
type: "reasoning",
|
|
631
|
+
text: part.thinking
|
|
632
|
+
}];
|
|
633
|
+
if (part.type === "toolCall") {
|
|
634
|
+
const name = optionalPiString(part.name, 256) ?? "tool";
|
|
635
|
+
const args = jsonText(part.arguments);
|
|
636
|
+
return [{
|
|
637
|
+
...partCommon,
|
|
638
|
+
type: "toolCall",
|
|
639
|
+
text: args ? `${name}\n${args}` : name
|
|
640
|
+
}];
|
|
641
|
+
}
|
|
642
|
+
return [];
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
function piTranscriptItems(entries) {
|
|
646
|
+
return activePiEntries(entries).flatMap((entry) => {
|
|
647
|
+
if (entry.type === "message") return piMessageItems(entry);
|
|
648
|
+
const id = optionalPiString(entry.id, 256);
|
|
649
|
+
const timestamp = optionalPiString(entry.timestamp, 128);
|
|
650
|
+
const common = {
|
|
651
|
+
...id ? { id } : {},
|
|
652
|
+
...timestamp ? { timestamp } : {}
|
|
653
|
+
};
|
|
654
|
+
if (entry.type === "compaction" && typeof entry.summary === "string") return [{
|
|
655
|
+
...common,
|
|
656
|
+
type: "other",
|
|
657
|
+
text: entry.summary
|
|
658
|
+
}];
|
|
659
|
+
if (entry.type === "branch_summary" && typeof entry.summary === "string") return [{
|
|
660
|
+
...common,
|
|
661
|
+
type: "other",
|
|
662
|
+
text: entry.summary
|
|
663
|
+
}];
|
|
664
|
+
if (entry.type === "custom_message" && entry.display === true) {
|
|
665
|
+
const text = textFromContent(entry.content);
|
|
666
|
+
return text ? [{
|
|
667
|
+
...common,
|
|
668
|
+
type: "other",
|
|
669
|
+
text
|
|
670
|
+
}] : [];
|
|
671
|
+
}
|
|
672
|
+
return [];
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
async function readLocalPiTranscriptPage(value) {
|
|
676
|
+
const params = parseReadParams(value);
|
|
677
|
+
const offset = decodeCursor(params.cursor);
|
|
678
|
+
const page = transcriptPage(piTranscriptItems(await readPiSessionById(params.threadId, process$1.env)), params.limit, offset);
|
|
679
|
+
return {
|
|
680
|
+
hostId: LOCAL_HOST_ID$1,
|
|
681
|
+
label: "Local Pi",
|
|
682
|
+
threadId: params.threadId,
|
|
683
|
+
...page
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
//#endregion
|
|
687
|
+
//#region extensions/acpx/src/pi-session-catalog-plugin.ts
|
|
688
|
+
const PI_SESSIONS_LIST_COMMAND = "acpx.pi.sessions.list.v1";
|
|
689
|
+
const PI_SESSION_READ_COMMAND = "acpx.pi.sessions.read.v1";
|
|
690
|
+
const PI_TERMINAL_RESUME_COMMAND = "acpx.pi.terminal.resume.v1";
|
|
691
|
+
const CAPABILITY = "pi-sessions";
|
|
692
|
+
const LOCAL_HOST_ID = "gateway";
|
|
693
|
+
const MAX_PAGE_LIMIT = 100;
|
|
694
|
+
const MAX_HOSTS = 100;
|
|
695
|
+
const MAX_CURSOR_LENGTH = 128;
|
|
696
|
+
const MAX_SEARCH_LENGTH = 500;
|
|
697
|
+
const NODE_TIMEOUT_MS = 2e4;
|
|
698
|
+
const SESSION_ID_PATTERN = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
|
|
699
|
+
const TRANSCRIPT_ITEM_TYPES = /* @__PURE__ */ new Set([
|
|
700
|
+
"userMessage",
|
|
701
|
+
"agentMessage",
|
|
702
|
+
"reasoning",
|
|
703
|
+
"toolCall",
|
|
704
|
+
"toolResult",
|
|
705
|
+
"other"
|
|
706
|
+
]);
|
|
707
|
+
function validatePiThreadId(value) {
|
|
708
|
+
if (typeof value !== "string" || !SESSION_ID_PATTERN.test(value)) throw new Error("INVALID_REQUEST: threadId is invalid");
|
|
709
|
+
return value;
|
|
710
|
+
}
|
|
711
|
+
function isOptionalString(value) {
|
|
712
|
+
return value === void 0 || typeof value === "string";
|
|
713
|
+
}
|
|
714
|
+
function isOptionalNumber(value) {
|
|
715
|
+
return value === void 0 || typeof value === "number";
|
|
716
|
+
}
|
|
717
|
+
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);
|
|
719
|
+
}
|
|
720
|
+
function isNodeTranscriptItem(value) {
|
|
721
|
+
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");
|
|
722
|
+
}
|
|
723
|
+
function parseNodeParams(paramsJSON) {
|
|
724
|
+
if (!paramsJSON) return;
|
|
725
|
+
try {
|
|
726
|
+
return JSON.parse(paramsJSON);
|
|
727
|
+
} catch (error) {
|
|
728
|
+
throw new Error("Pi session parameters must be valid JSON", { cause: error });
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function fullConfigCatalogEnabled(config) {
|
|
732
|
+
if (!isRecord(config) || !isRecord(config.plugins) || !isRecord(config.plugins.entries)) return true;
|
|
733
|
+
const entry = config.plugins.entries.acpx;
|
|
734
|
+
if (!isRecord(entry) || !isRecord(entry.config) || !isRecord(entry.config.piSessionCatalog)) return true;
|
|
735
|
+
return entry.config.piSessionCatalog.enabled !== false;
|
|
736
|
+
}
|
|
737
|
+
function isPiSessionCatalogEnabled(pluginConfig) {
|
|
738
|
+
return !isRecord(pluginConfig) || !isRecord(pluginConfig.piSessionCatalog) || pluginConfig.piSessionCatalog.enabled !== false;
|
|
739
|
+
}
|
|
740
|
+
function createPiSessionNodeHostCommands() {
|
|
741
|
+
const storeAvailable = ({ config, env }) => fullConfigCatalogEnabled(config) && piSessionStoreAvailable(env);
|
|
742
|
+
return [
|
|
743
|
+
{
|
|
744
|
+
command: PI_SESSIONS_LIST_COMMAND,
|
|
745
|
+
cap: CAPABILITY,
|
|
746
|
+
dangerous: false,
|
|
747
|
+
isAvailable: storeAvailable,
|
|
748
|
+
handle: async (paramsJSON) => JSON.stringify(await listLocalPiSessionPage(parseNodeParams(paramsJSON)))
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
command: PI_SESSION_READ_COMMAND,
|
|
752
|
+
cap: CAPABILITY,
|
|
753
|
+
dangerous: false,
|
|
754
|
+
isAvailable: storeAvailable,
|
|
755
|
+
handle: async (paramsJSON) => JSON.stringify(await readLocalPiTranscriptPage(parseNodeParams(paramsJSON)))
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
command: PI_TERMINAL_RESUME_COMMAND,
|
|
759
|
+
cap: CAPABILITY,
|
|
760
|
+
dangerous: false,
|
|
761
|
+
duplex: true,
|
|
762
|
+
isAvailable: ({ config, env }) => storeAvailable({
|
|
763
|
+
config,
|
|
764
|
+
env
|
|
765
|
+
}) && Boolean(resolveExecutableFromPathEnv("pi", env.PATH ?? "")),
|
|
766
|
+
handle: async (paramsJSON, io) => {
|
|
767
|
+
if (!io) throw new Error("Pi terminal command requires duplex transport");
|
|
768
|
+
const params = decodeNodePtyResumeParams(paramsJSON, validatePiThreadId);
|
|
769
|
+
const record = await requireLocalPiSession(params.threadId);
|
|
770
|
+
const file = resolveExecutableFromPathEnv("pi", process$1.env.PATH ?? "");
|
|
771
|
+
if (!file) throw new Error("Pi CLI is unavailable");
|
|
772
|
+
return JSON.stringify(await runNodePtyCommand({
|
|
773
|
+
file,
|
|
774
|
+
args: ["--session", params.threadId],
|
|
775
|
+
cwd: record.cwd,
|
|
776
|
+
cols: params.cols,
|
|
777
|
+
rows: params.rows
|
|
778
|
+
}, io));
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
];
|
|
782
|
+
}
|
|
783
|
+
function createPiSessionNodeInvokePolicies() {
|
|
784
|
+
return [{
|
|
785
|
+
commands: [
|
|
786
|
+
PI_SESSIONS_LIST_COMMAND,
|
|
787
|
+
PI_SESSION_READ_COMMAND,
|
|
788
|
+
PI_TERMINAL_RESUME_COMMAND
|
|
789
|
+
],
|
|
790
|
+
defaultPlatforms: [
|
|
791
|
+
"macos",
|
|
792
|
+
"linux",
|
|
793
|
+
"windows"
|
|
794
|
+
],
|
|
795
|
+
handle: (context) => context.command === PI_TERMINAL_RESUME_COMMAND ? { ok: true } : context.invokeNode()
|
|
796
|
+
}];
|
|
797
|
+
}
|
|
798
|
+
function nodeLabel(node) {
|
|
799
|
+
return node.displayName?.trim() || node.remoteIp?.trim() || node.nodeId;
|
|
800
|
+
}
|
|
801
|
+
function unwrapNodePayload(value) {
|
|
802
|
+
return isRecord(value) && typeof value.payloadJSON === "string" ? JSON.parse(value.payloadJSON) : value;
|
|
803
|
+
}
|
|
804
|
+
function setTerminalCapability(page, canOpenTerminal) {
|
|
805
|
+
for (const session of page.sessions) session.canOpenTerminal = canOpenTerminal;
|
|
806
|
+
return page;
|
|
807
|
+
}
|
|
808
|
+
async function listPiNodeHost(runtime, query, node) {
|
|
809
|
+
const hostId = `node:${node.nodeId}`;
|
|
810
|
+
const common = {
|
|
811
|
+
hostId,
|
|
812
|
+
label: nodeLabel(node),
|
|
813
|
+
kind: "node",
|
|
814
|
+
connected: node.connected === true,
|
|
815
|
+
nodeId: node.nodeId
|
|
816
|
+
};
|
|
817
|
+
if (node.connected !== true) return {
|
|
818
|
+
...common,
|
|
819
|
+
sessions: [],
|
|
820
|
+
error: {
|
|
821
|
+
code: "NODE_OFFLINE",
|
|
822
|
+
message: "Paired node is offline"
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
try {
|
|
826
|
+
const page = parseNodeSessionPage(unwrapNodePayload(await runtime.nodes.invoke({
|
|
827
|
+
nodeId: node.nodeId,
|
|
828
|
+
command: PI_SESSIONS_LIST_COMMAND,
|
|
829
|
+
params: {
|
|
830
|
+
...query.limitPerHost ? { limit: query.limitPerHost } : {},
|
|
831
|
+
...query.search?.trim() ? { searchTerm: query.search.trim().slice(0, MAX_SEARCH_LENGTH) } : {},
|
|
832
|
+
...query.cursors?.[hostId] ? { cursor: query.cursors[hostId] } : {}
|
|
833
|
+
},
|
|
834
|
+
timeoutMs: NODE_TIMEOUT_MS,
|
|
835
|
+
scopes: ["operator.write"]
|
|
836
|
+
})));
|
|
837
|
+
const canOpenTerminal = (node.invocableCommands ?? node.commands)?.includes(PI_TERMINAL_RESUME_COMMAND) === true;
|
|
838
|
+
return {
|
|
839
|
+
...common,
|
|
840
|
+
...setTerminalCapability(page, canOpenTerminal)
|
|
841
|
+
};
|
|
842
|
+
} catch {
|
|
843
|
+
return {
|
|
844
|
+
...common,
|
|
845
|
+
sessions: [],
|
|
846
|
+
error: {
|
|
847
|
+
code: "NODE_INVOKE_FAILED",
|
|
848
|
+
message: "Paired node Pi sessions are unavailable"
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
function parseNodeSessionPage(value) {
|
|
854
|
+
if (!isRecord(value) || !Array.isArray(value.sessions) || value.sessions.length > MAX_PAGE_LIMIT) throw new Error("Pi node returned an invalid session page");
|
|
855
|
+
if (!value.sessions.every(isNodeSession)) throw new Error("Pi node returned an invalid session page");
|
|
856
|
+
const sessions = value.sessions;
|
|
857
|
+
const nextCursor = optionalPiString(value.nextCursor, MAX_CURSOR_LENGTH);
|
|
858
|
+
if (value.nextCursor !== void 0 && !nextCursor) throw new Error("Pi node returned an invalid cursor");
|
|
859
|
+
return {
|
|
860
|
+
sessions,
|
|
861
|
+
...nextCursor ? { nextCursor } : {}
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
function parseNodeTranscriptPage(value, threadId) {
|
|
865
|
+
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");
|
|
866
|
+
const nextCursor = optionalPiString(value.nextCursor, MAX_CURSOR_LENGTH);
|
|
867
|
+
if (value.nextCursor !== void 0 && !nextCursor) throw new Error("Pi node returned an invalid cursor");
|
|
868
|
+
return {
|
|
869
|
+
hostId: LOCAL_HOST_ID,
|
|
870
|
+
threadId,
|
|
871
|
+
items: value.items,
|
|
872
|
+
...nextCursor ? { nextCursor } : {}
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
async function listPiHosts(runtime, query) {
|
|
876
|
+
const requested = query.hostIds ? new Set(query.hostIds) : void 0;
|
|
877
|
+
const searchTerm = query.search?.trim().slice(0, MAX_SEARCH_LENGTH) || void 0;
|
|
878
|
+
const hosts = [];
|
|
879
|
+
if ((!requested || requested.has(LOCAL_HOST_ID)) && piSessionStoreAvailable(process$1.env)) try {
|
|
880
|
+
hosts.push({
|
|
881
|
+
hostId: LOCAL_HOST_ID,
|
|
882
|
+
label: "Local Pi",
|
|
883
|
+
kind: "gateway",
|
|
884
|
+
connected: true,
|
|
885
|
+
...await listLocalPiSessionPage({
|
|
886
|
+
limit: query.limitPerHost,
|
|
887
|
+
...searchTerm ? { searchTerm } : {},
|
|
888
|
+
cursor: query.cursors?.[LOCAL_HOST_ID]
|
|
889
|
+
}).then((page) => setTerminalCapability(page, resolveExecutableFromPathEnv("pi", process$1.env.PATH ?? "") !== void 0))
|
|
890
|
+
});
|
|
891
|
+
} catch {
|
|
892
|
+
hosts.push({
|
|
893
|
+
hostId: LOCAL_HOST_ID,
|
|
894
|
+
label: "Local Pi",
|
|
895
|
+
kind: "gateway",
|
|
896
|
+
connected: true,
|
|
897
|
+
sessions: [],
|
|
898
|
+
error: {
|
|
899
|
+
code: "LOCAL_READ_FAILED",
|
|
900
|
+
message: "Local Pi sessions are unavailable"
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
let nodes;
|
|
905
|
+
try {
|
|
906
|
+
nodes = (await runtime.nodes.list()).nodes;
|
|
907
|
+
} catch {
|
|
908
|
+
return hosts;
|
|
909
|
+
}
|
|
910
|
+
const eligible = nodes.filter((node) => node.commands?.includes(PI_SESSIONS_LIST_COMMAND) && (!requested || requested.has(`node:${node.nodeId}`))).toSorted((left, right) => nodeLabel(left).localeCompare(nodeLabel(right))).slice(0, MAX_HOSTS - hosts.length);
|
|
911
|
+
const nodeHosts = await Promise.all(eligible.map((node) => listPiNodeHost(runtime, query, node)));
|
|
912
|
+
return [...hosts, ...nodeHosts];
|
|
913
|
+
}
|
|
914
|
+
async function requireLocalPiSession(threadId) {
|
|
915
|
+
const record = (await listLocalPiSessionPage({
|
|
916
|
+
searchTerm: threadId,
|
|
917
|
+
limit: MAX_PAGE_LIMIT
|
|
918
|
+
})).sessions.find((session) => session.threadId === threadId);
|
|
919
|
+
if (!record) throw new Error("Pi session is unavailable");
|
|
920
|
+
return record;
|
|
921
|
+
}
|
|
922
|
+
async function resolveNodePiSession(params) {
|
|
923
|
+
const record = parseNodeSessionPage(unwrapNodePayload(await params.runtime.nodes.invoke({
|
|
924
|
+
nodeId: params.nodeId,
|
|
925
|
+
command: PI_SESSIONS_LIST_COMMAND,
|
|
926
|
+
params: {
|
|
927
|
+
searchTerm: params.threadId,
|
|
928
|
+
limit: MAX_PAGE_LIMIT
|
|
929
|
+
},
|
|
930
|
+
timeoutMs: NODE_TIMEOUT_MS,
|
|
931
|
+
scopes: ["operator.write"]
|
|
932
|
+
}))).sessions.find((session) => session.threadId === params.threadId);
|
|
933
|
+
if (!record) throw new Error("Pi session is unavailable");
|
|
934
|
+
return record;
|
|
935
|
+
}
|
|
936
|
+
async function openPiTerminal(params) {
|
|
937
|
+
const title = `pi --session ${params.threadId.slice(0, 12)}…`;
|
|
938
|
+
if (params.hostId === LOCAL_HOST_ID) {
|
|
939
|
+
const record = await requireLocalPiSession(params.threadId);
|
|
940
|
+
const executable = resolveExecutableFromPathEnv("pi", process$1.env.PATH ?? "");
|
|
941
|
+
if (!executable) throw new Error("Pi CLI is unavailable");
|
|
942
|
+
return {
|
|
943
|
+
kind: "local",
|
|
944
|
+
argv: [
|
|
945
|
+
executable,
|
|
946
|
+
"--session",
|
|
947
|
+
params.threadId
|
|
948
|
+
],
|
|
949
|
+
...record.cwd ? { cwd: record.cwd } : {},
|
|
950
|
+
title
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
if (!params.hostId.startsWith("node:")) throw new Error("hostId is invalid");
|
|
954
|
+
const nodeId = params.hostId.slice(5);
|
|
955
|
+
if (!(await params.runtime.nodes.list()).nodes.find((candidate) => {
|
|
956
|
+
const commands = candidate.invocableCommands ?? candidate.commands;
|
|
957
|
+
return candidate.nodeId === nodeId && candidate.connected === true && commands?.includes(PI_SESSIONS_LIST_COMMAND) === true && commands.includes(PI_TERMINAL_RESUME_COMMAND);
|
|
958
|
+
})) throw new Error("paired-node Pi terminal is unavailable");
|
|
959
|
+
const record = await resolveNodePiSession({
|
|
960
|
+
runtime: params.runtime,
|
|
961
|
+
nodeId,
|
|
962
|
+
threadId: params.threadId
|
|
963
|
+
});
|
|
964
|
+
return {
|
|
965
|
+
kind: "node",
|
|
966
|
+
nodeId,
|
|
967
|
+
command: PI_TERMINAL_RESUME_COMMAND,
|
|
968
|
+
paramsJSON: JSON.stringify({ threadId: params.threadId }),
|
|
969
|
+
...record.cwd ? { cwd: record.cwd } : {},
|
|
970
|
+
title
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
async function readPiTranscript(runtime, request) {
|
|
974
|
+
if (request.hostId === LOCAL_HOST_ID) return await readLocalPiTranscriptPage({
|
|
975
|
+
threadId: request.threadId,
|
|
976
|
+
...request.limit ? { limit: request.limit } : {},
|
|
977
|
+
...request.cursor ? { cursor: request.cursor } : {}
|
|
978
|
+
});
|
|
979
|
+
if (!request.hostId.startsWith("node:")) throw new Error("hostId is invalid");
|
|
980
|
+
const nodeId = request.hostId.slice(5);
|
|
981
|
+
const node = (await runtime.nodes.list()).nodes.find((candidate) => candidate.nodeId === nodeId && candidate.connected === true && candidate.commands?.includes(PI_SESSION_READ_COMMAND));
|
|
982
|
+
if (!node) throw new Error("paired-node Pi session host is unavailable");
|
|
983
|
+
return {
|
|
984
|
+
...parseNodeTranscriptPage(unwrapNodePayload(await runtime.nodes.invoke({
|
|
985
|
+
nodeId,
|
|
986
|
+
command: PI_SESSION_READ_COMMAND,
|
|
987
|
+
params: {
|
|
988
|
+
threadId: request.threadId,
|
|
989
|
+
...request.limit ? { limit: request.limit } : {},
|
|
990
|
+
...request.cursor ? { cursor: request.cursor } : {}
|
|
991
|
+
},
|
|
992
|
+
timeoutMs: NODE_TIMEOUT_MS,
|
|
993
|
+
scopes: ["operator.write"]
|
|
994
|
+
})), request.threadId),
|
|
995
|
+
hostId: request.hostId,
|
|
996
|
+
label: nodeLabel(node)
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function registerPiSessionCatalog(api) {
|
|
1000
|
+
if (!isPiSessionCatalogEnabled(api.pluginConfig)) return;
|
|
1001
|
+
api.registerSessionCatalog({
|
|
1002
|
+
id: "pi",
|
|
1003
|
+
label: "Pi",
|
|
1004
|
+
list: async (query) => await listPiHosts(api.runtime, query),
|
|
1005
|
+
read: async (request) => await readPiTranscript(api.runtime, request),
|
|
1006
|
+
openTerminal: async (request) => await openPiTerminal({
|
|
1007
|
+
runtime: api.runtime,
|
|
1008
|
+
...request
|
|
1009
|
+
})
|
|
1010
|
+
});
|
|
1011
|
+
for (const command of createPiSessionNodeHostCommands()) api.registerNodeHostCommand(command);
|
|
1012
|
+
for (const policy of createPiSessionNodeInvokePolicies()) api.registerNodeInvokePolicy(policy);
|
|
1013
|
+
}
|
|
1014
|
+
//#endregion
|
|
3
1015
|
//#region extensions/acpx/index.ts
|
|
4
1016
|
/**
|
|
5
1017
|
* ACPX runtime plugin entry. It registers the embedded ACP backend service and
|
|
6
1018
|
* wires reply-dispatch hooks into the plugin SDK runtime.
|
|
7
1019
|
*/
|
|
1020
|
+
function resolveReplyDispatchTimeoutMs(pluginConfig) {
|
|
1021
|
+
const timeoutSeconds = pluginConfig?.timeoutSeconds;
|
|
1022
|
+
return finiteSecondsToTimerSafeMilliseconds(typeof timeoutSeconds === "number" && Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? timeoutSeconds : 120) ?? 1;
|
|
1023
|
+
}
|
|
1024
|
+
async function tryDispatchAcpReplyHookWithTimeout(event, ctx, timeoutMs) {
|
|
1025
|
+
const timeoutController = new AbortController();
|
|
1026
|
+
const timeout = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
1027
|
+
timeout.unref?.();
|
|
1028
|
+
const abortSignal = ctx.abortSignal ? AbortSignal.any([ctx.abortSignal, timeoutController.signal]) : timeoutController.signal;
|
|
1029
|
+
try {
|
|
1030
|
+
return await tryDispatchAcpReplyHook(event, {
|
|
1031
|
+
...ctx,
|
|
1032
|
+
abortSignal
|
|
1033
|
+
});
|
|
1034
|
+
} finally {
|
|
1035
|
+
clearTimeout(timeout);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
8
1038
|
const plugin = {
|
|
9
1039
|
id: "acpx",
|
|
10
1040
|
name: "ACPX Runtime",
|
|
11
1041
|
description: "Embedded ACP runtime backend with plugin-owned session and transport management.",
|
|
12
1042
|
register(api) {
|
|
1043
|
+
const replyDispatchTimeoutMs = resolveReplyDispatchTimeoutMs(api.pluginConfig);
|
|
1044
|
+
registerPiSessionCatalog(api);
|
|
13
1045
|
api.registerService(createAcpxRuntimeService({
|
|
14
1046
|
pluginConfig: api.pluginConfig,
|
|
15
1047
|
openKeyedStore: (options) => api.runtime.state.openKeyedStore(options)
|
|
16
1048
|
}));
|
|
17
|
-
api.on("reply_dispatch",
|
|
1049
|
+
api.on("reply_dispatch", (event, ctx) => tryDispatchAcpReplyHookWithTimeout(event, ctx, replyDispatchTimeoutMs), { timeoutMs: replyDispatchTimeoutMs });
|
|
18
1050
|
}
|
|
19
1051
|
};
|
|
20
1052
|
//#endregion
|