@openclaw/acpx 2026.7.2-beta.3 → 2026.7.2-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/doctor-contract-api.js +1 -1
- package/dist/index.js +335 -52
- package/dist/{process-lease-DiKkFj6F.js → process-lease-CU1cFI4I.js} +4 -14
- package/dist/{process-reaper-CoGRyMJ_.js → process-reaper-BIOtp_Ek.js} +1 -1
- package/dist/{register.runtime-BrVpU-xc.js → register.runtime-Dajn8myc.js} +1 -1
- package/dist/register.runtime.js +1 -1
- package/dist/{runtime-CMARmJuS.js → runtime-CN4JQkdV.js} +2 -2
- package/dist/{service-D6LQXIcJ.js → service-DOsKw_R1.js} +4 -5
- package/package.json +24 -9
- package/npm-shrinkwrap.json +0 -2517
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-Dajn8myc.js";
|
|
2
2
|
import "./config-schema-lrk5nlcV.js";
|
|
3
3
|
import { tryDispatchAcpReplyHook } from "openclaw/plugin-sdk/acp-runtime-backend";
|
|
4
4
|
import { finiteSecondsToTimerSafeMilliseconds } from "openclaw/plugin-sdk/number-runtime";
|
|
5
5
|
import process$1 from "node:process";
|
|
6
|
+
import { resolveAcpSessionAvailability } from "openclaw/plugin-sdk/acp-runtime";
|
|
7
|
+
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
|
|
6
8
|
import { decodeNodePtyResumeParams, resolveNodeHostExecutable, runNodePtyCommand } from "openclaw/plugin-sdk/node-host";
|
|
9
|
+
import { createSessionCatalogAdoptionCoordinator, importSessionCatalogHistory, isExternalUserText, listAdoptedSessionCatalogSessions, sessionCatalogAdoptedSessionKey, sessionCatalogAdoptedSourceKey } from "openclaw/plugin-sdk/session-catalog";
|
|
7
10
|
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
8
11
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
9
12
|
import { createReadStream, readFileSync, statSync } from "node:fs";
|
|
@@ -67,6 +70,13 @@ function piSessionStore(env, cwd = process.cwd()) {
|
|
|
67
70
|
flat: false
|
|
68
71
|
};
|
|
69
72
|
}
|
|
73
|
+
/** Store root scanned by pi-acp@0.0.26 when resolving a native session id. */
|
|
74
|
+
function piAcpSessionStoreRoot(env) {
|
|
75
|
+
const configuredAgentDir = env.PI_CODING_AGENT_DIR?.trim();
|
|
76
|
+
if (configuredAgentDir && !isPiSessionCatalogPathAbsolute(configuredAgentDir)) return;
|
|
77
|
+
const agentDir = configuredAgentDir ? path.resolve(configuredAgentDir) : path.join(piHome(env), ".pi", "agent");
|
|
78
|
+
return path.join(agentDir, "sessions");
|
|
79
|
+
}
|
|
70
80
|
function piSessionStoreAvailable(env) {
|
|
71
81
|
try {
|
|
72
82
|
return statSync(piSessionStore(env).root).isDirectory();
|
|
@@ -114,9 +124,10 @@ function optionalString(value, maxLength) {
|
|
|
114
124
|
}
|
|
115
125
|
async function discoverPiSessionFiles(env) {
|
|
116
126
|
const store = piSessionStore(env);
|
|
127
|
+
const resolvedRoot = await realpathOrResolve(store.root);
|
|
117
128
|
let entries;
|
|
118
129
|
try {
|
|
119
|
-
entries = await fs$1.readdir(
|
|
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);
|
|
@@ -391,7 +434,7 @@ const LOCAL_HOST_ID$1 = "gateway";
|
|
|
391
434
|
const DEFAULT_PAGE_LIMIT = 20;
|
|
392
435
|
const MAX_PAGE_LIMIT$1 = 100;
|
|
393
436
|
const MAX_SEARCH_LENGTH = 500;
|
|
394
|
-
const MAX_CURSOR_LENGTH
|
|
437
|
+
const MAX_CURSOR_LENGTH = 128;
|
|
395
438
|
const MAX_TRANSCRIPT_ITEM_BYTES = 512 * 1024;
|
|
396
439
|
const MAX_TRANSCRIPT_PAGE_BYTES = 20 * 1024 * 1024;
|
|
397
440
|
const SESSION_ID_PATTERN$1 = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
|
|
@@ -408,18 +451,35 @@ function boundedLimit(value, fallback = DEFAULT_PAGE_LIMIT) {
|
|
|
408
451
|
function encodeCursor(offset) {
|
|
409
452
|
return Buffer.from(JSON.stringify({ offset }), "utf8").toString("base64url");
|
|
410
453
|
}
|
|
454
|
+
function optionalRawCursor(value) {
|
|
455
|
+
if (value === void 0) return;
|
|
456
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_CURSOR_LENGTH) throw new Error("cursor is invalid");
|
|
457
|
+
return value;
|
|
458
|
+
}
|
|
411
459
|
function decodeCursor(value) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (!cursor) throw new Error("cursor is invalid");
|
|
460
|
+
const cursor = optionalRawCursor(value);
|
|
461
|
+
if (cursor === void 0) return 0;
|
|
415
462
|
try {
|
|
416
|
-
const
|
|
417
|
-
if (
|
|
418
|
-
|
|
463
|
+
const bytes = Buffer.from(cursor, "base64url");
|
|
464
|
+
if (bytes.toString("base64url") !== cursor) throw new Error("non-canonical base64url");
|
|
465
|
+
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
466
|
+
if (!isRecord(parsed) || !Number.isSafeInteger(parsed.offset) || Number(parsed.offset) < 0) throw new Error("invalid offset");
|
|
467
|
+
const offset = Number(parsed.offset);
|
|
468
|
+
if (encodeCursor(offset) !== cursor) throw new Error("non-canonical cursor payload");
|
|
469
|
+
return offset;
|
|
419
470
|
} catch (error) {
|
|
420
471
|
throw new Error("cursor is invalid", { cause: error });
|
|
421
472
|
}
|
|
422
473
|
}
|
|
474
|
+
function isExactPiSessionCursor(value) {
|
|
475
|
+
if (typeof value !== "string") return false;
|
|
476
|
+
try {
|
|
477
|
+
decodeCursor(value);
|
|
478
|
+
return true;
|
|
479
|
+
} catch {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
423
483
|
function truncateUtf8(text, maxBytes) {
|
|
424
484
|
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
425
485
|
let low = 0;
|
|
@@ -455,7 +515,7 @@ function transcriptPage(items, limit, offset) {
|
|
|
455
515
|
...consumed < items.length ? { nextCursor: encodeCursor(consumed) } : {}
|
|
456
516
|
};
|
|
457
517
|
}
|
|
458
|
-
function textFromContent(content) {
|
|
518
|
+
function textFromContent$1(content) {
|
|
459
519
|
if (typeof content === "string") return content;
|
|
460
520
|
if (!Array.isArray(content)) return "";
|
|
461
521
|
return content.flatMap((part) => {
|
|
@@ -468,7 +528,7 @@ function textFromContent(content) {
|
|
|
468
528
|
return [];
|
|
469
529
|
}).join("\n");
|
|
470
530
|
}
|
|
471
|
-
function timestampMs(value) {
|
|
531
|
+
function timestampMs$1(value) {
|
|
472
532
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
473
533
|
if (typeof value === "string") {
|
|
474
534
|
const parsed = Date.parse(value);
|
|
@@ -486,8 +546,7 @@ function parseListParams(value) {
|
|
|
486
546
|
if (unknown) throw new Error(`unknown Pi session list parameter: ${unknown}`);
|
|
487
547
|
const searchTerm = optionalPiString(value.searchTerm, MAX_SEARCH_LENGTH);
|
|
488
548
|
if (value.searchTerm !== void 0 && !searchTerm) throw new Error("searchTerm is invalid");
|
|
489
|
-
const cursor =
|
|
490
|
-
if (value.cursor !== void 0 && !cursor) throw new Error("cursor is invalid");
|
|
549
|
+
const cursor = optionalRawCursor(value.cursor);
|
|
491
550
|
return {
|
|
492
551
|
limit: boundedLimit(value.limit),
|
|
493
552
|
...searchTerm ? { searchTerm } : {},
|
|
@@ -504,8 +563,7 @@ function parseReadParams(value) {
|
|
|
504
563
|
if (unknown) throw new Error(`unknown Pi session read parameter: ${unknown}`);
|
|
505
564
|
const threadId = optionalPiString(value.threadId, 256);
|
|
506
565
|
if (!threadId || !SESSION_ID_PATTERN$1.test(threadId)) throw new Error("threadId is invalid");
|
|
507
|
-
const cursor =
|
|
508
|
-
if (value.cursor !== void 0 && !cursor) throw new Error("cursor is invalid");
|
|
566
|
+
const cursor = optionalRawCursor(value.cursor);
|
|
509
567
|
return {
|
|
510
568
|
threadId,
|
|
511
569
|
limit: boundedLimit(value.limit),
|
|
@@ -520,14 +578,14 @@ async function listLocalPiSessionPage(value) {
|
|
|
520
578
|
limit: params.limit,
|
|
521
579
|
...params.searchTerm ? { searchTerm: params.searchTerm } : {}
|
|
522
580
|
});
|
|
523
|
-
const page = summaries.map(({ file: _file, ...session }) => session);
|
|
581
|
+
const page = summaries.map(({ file: _file, version: _version, ...session }) => session);
|
|
524
582
|
return {
|
|
525
583
|
sessions: page,
|
|
526
584
|
...hasMore ? { nextCursor: encodeCursor(offset + page.length) } : {}
|
|
527
585
|
};
|
|
528
586
|
}
|
|
529
587
|
function isoTimestamp(message, entry) {
|
|
530
|
-
const value = timestampMs(message.timestamp) ?? timestampMs(entry.timestamp);
|
|
588
|
+
const value = timestampMs$1(message.timestamp) ?? timestampMs$1(entry.timestamp);
|
|
531
589
|
if (value === void 0) return;
|
|
532
590
|
const date = new Date(value);
|
|
533
591
|
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
@@ -573,7 +631,7 @@ function piMessageItems(entry) {
|
|
|
573
631
|
...modelRef ? { model: modelRef } : {}
|
|
574
632
|
};
|
|
575
633
|
if (role === "user") {
|
|
576
|
-
const text = textFromContent(message.content);
|
|
634
|
+
const text = textFromContent$1(message.content);
|
|
577
635
|
return text ? [{
|
|
578
636
|
...common,
|
|
579
637
|
type: "userMessage",
|
|
@@ -582,7 +640,7 @@ function piMessageItems(entry) {
|
|
|
582
640
|
}
|
|
583
641
|
if (role === "toolResult") {
|
|
584
642
|
const toolName = optionalPiString(message.toolName, 256);
|
|
585
|
-
const text = textFromContent(message.content);
|
|
643
|
+
const text = textFromContent$1(message.content);
|
|
586
644
|
return [{
|
|
587
645
|
...common,
|
|
588
646
|
type: "toolResult",
|
|
@@ -607,7 +665,7 @@ function piMessageItems(entry) {
|
|
|
607
665
|
if (role === "custom" || role === "hookMessage") {
|
|
608
666
|
if (message.display !== true) return [];
|
|
609
667
|
const customType = optionalPiString(message.customType, 256);
|
|
610
|
-
const text = textFromContent(message.content);
|
|
668
|
+
const text = textFromContent$1(message.content);
|
|
611
669
|
return text ? [{
|
|
612
670
|
...common,
|
|
613
671
|
type: "other",
|
|
@@ -663,7 +721,7 @@ function piTranscriptItems(entries) {
|
|
|
663
721
|
text: entry.summary
|
|
664
722
|
}];
|
|
665
723
|
if (entry.type === "custom_message" && entry.display === true) {
|
|
666
|
-
const text = textFromContent(entry.content);
|
|
724
|
+
const text = textFromContent$1(entry.content);
|
|
667
725
|
return text ? [{
|
|
668
726
|
...common,
|
|
669
727
|
type: "other",
|
|
@@ -685,6 +743,121 @@ async function readLocalPiTranscriptPage(value) {
|
|
|
685
743
|
};
|
|
686
744
|
}
|
|
687
745
|
//#endregion
|
|
746
|
+
//#region extensions/acpx/src/pi-session-upstream-activity.ts
|
|
747
|
+
const MAX_PI_UPSTREAM_SCAN_BYTES = 1024 * 1024;
|
|
748
|
+
function parseCompletePiRows(tail) {
|
|
749
|
+
const entries = [];
|
|
750
|
+
let lineStart = 0;
|
|
751
|
+
let classifiedBytes = 0;
|
|
752
|
+
for (let index = 0; index < tail.length; index += 1) {
|
|
753
|
+
if (tail[index] !== 10) continue;
|
|
754
|
+
const line = tail.subarray(lineStart, index).toString("utf8").trim();
|
|
755
|
+
if (line) try {
|
|
756
|
+
const value = JSON.parse(line);
|
|
757
|
+
if (!isRecord(value)) break;
|
|
758
|
+
entries.push(value);
|
|
759
|
+
} catch {
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
classifiedBytes = index + 1;
|
|
763
|
+
lineStart = index + 1;
|
|
764
|
+
}
|
|
765
|
+
return {
|
|
766
|
+
entries,
|
|
767
|
+
classifiedBytes
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
function textFromContent(content) {
|
|
771
|
+
if (typeof content === "string") return content;
|
|
772
|
+
if (!Array.isArray(content)) return;
|
|
773
|
+
return content.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n") || void 0;
|
|
774
|
+
}
|
|
775
|
+
function timestampMs(value) {
|
|
776
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
777
|
+
if (typeof value === "string") {
|
|
778
|
+
const parsed = Date.parse(value);
|
|
779
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
function readFilePath(probe) {
|
|
783
|
+
return isRecord(probe.upstreamRef) && typeof probe.upstreamRef.filePath === "string" ? probe.upstreamRef.filePath : void 0;
|
|
784
|
+
}
|
|
785
|
+
function readMarkerOffset(probe) {
|
|
786
|
+
return isRecord(probe.marker) && Number.isSafeInteger(probe.marker.offset) && Number(probe.marker.offset) >= 0 ? Number(probe.marker.offset) : void 0;
|
|
787
|
+
}
|
|
788
|
+
async function linkContinuedPiSession(sessionKey, threadId) {
|
|
789
|
+
try {
|
|
790
|
+
const baseline = await readPiSessionFileBaseline(threadId, process$1.env);
|
|
791
|
+
return baseline ? {
|
|
792
|
+
sessionKey,
|
|
793
|
+
upstream: {
|
|
794
|
+
kind: "pi-cli",
|
|
795
|
+
ref: { filePath: baseline.filePath },
|
|
796
|
+
marker: { offset: baseline.offset }
|
|
797
|
+
}
|
|
798
|
+
} : { sessionKey };
|
|
799
|
+
} catch {
|
|
800
|
+
return { sessionKey };
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
async function checkPiSessionUpstreamActivity(probe) {
|
|
804
|
+
if (probe.hostId !== "gateway" || probe.upstreamKind !== "pi-cli") return;
|
|
805
|
+
const filePath = readFilePath(probe);
|
|
806
|
+
const markerOffset = readMarkerOffset(probe);
|
|
807
|
+
if (!filePath || markerOffset === void 0) return;
|
|
808
|
+
let handle;
|
|
809
|
+
try {
|
|
810
|
+
handle = await fs$1.open(filePath, "r");
|
|
811
|
+
} catch (error) {
|
|
812
|
+
return isRecord(error) && error.code === "ENOENT" ? {
|
|
813
|
+
kind: "missing",
|
|
814
|
+
sessionKey: probe.sessionKey
|
|
815
|
+
} : void 0;
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
const stat = await handle.stat();
|
|
819
|
+
if (!stat.isFile()) return {
|
|
820
|
+
kind: "missing",
|
|
821
|
+
sessionKey: probe.sessionKey
|
|
822
|
+
};
|
|
823
|
+
if (stat.size <= markerOffset) return;
|
|
824
|
+
const readLength = Math.min(stat.size - markerOffset, MAX_PI_UPSTREAM_SCAN_BYTES);
|
|
825
|
+
const buffer = Buffer.allocUnsafe(readLength);
|
|
826
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, markerOffset);
|
|
827
|
+
const { entries, classifiedBytes } = parseCompletePiRows(buffer.subarray(0, bytesRead));
|
|
828
|
+
if (classifiedBytes === 0) return;
|
|
829
|
+
let humanTurns = 0;
|
|
830
|
+
let occurredAt;
|
|
831
|
+
for (const entry of entries) {
|
|
832
|
+
if (entry.type !== "message" || !isRecord(entry.message) || entry.message.role !== "user") continue;
|
|
833
|
+
if (!isExternalUserText(probe, textFromContent(entry.message.content))) continue;
|
|
834
|
+
humanTurns += 1;
|
|
835
|
+
occurredAt = Math.max(occurredAt ?? 0, timestampMs(entry.message.timestamp) ?? timestampMs(entry.timestamp) ?? stat.mtimeMs);
|
|
836
|
+
}
|
|
837
|
+
const nextOffset = markerOffset + classifiedBytes;
|
|
838
|
+
return {
|
|
839
|
+
kind: "activity",
|
|
840
|
+
sessionKey: probe.sessionKey,
|
|
841
|
+
humanTurns,
|
|
842
|
+
nextMarker: { offset: nextOffset },
|
|
843
|
+
...humanTurns > 0 ? {
|
|
844
|
+
occurredAt: occurredAt ?? stat.mtimeMs,
|
|
845
|
+
dedupeId: String(nextOffset)
|
|
846
|
+
} : {}
|
|
847
|
+
};
|
|
848
|
+
} finally {
|
|
849
|
+
await handle.close();
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async function checkPiUpstreamActivity(probes) {
|
|
853
|
+
const outcomes = [];
|
|
854
|
+
for (const probe of probes) try {
|
|
855
|
+
const outcome = await checkPiSessionUpstreamActivity(probe);
|
|
856
|
+
if (outcome) outcomes.push(outcome);
|
|
857
|
+
} catch {}
|
|
858
|
+
return outcomes;
|
|
859
|
+
}
|
|
860
|
+
//#endregion
|
|
688
861
|
//#region extensions/acpx/src/pi-session-catalog-plugin.ts
|
|
689
862
|
const PI_SESSIONS_LIST_COMMAND = "acpx.pi.sessions.list.v1";
|
|
690
863
|
const PI_SESSION_READ_COMMAND = "acpx.pi.sessions.read.v1";
|
|
@@ -693,7 +866,6 @@ const CAPABILITY = "pi-sessions";
|
|
|
693
866
|
const LOCAL_HOST_ID = "gateway";
|
|
694
867
|
const MAX_PAGE_LIMIT = 100;
|
|
695
868
|
const MAX_HOSTS = 100;
|
|
696
|
-
const MAX_CURSOR_LENGTH = 128;
|
|
697
869
|
const NODE_TIMEOUT_MS = 2e4;
|
|
698
870
|
const SESSION_ID_PATTERN = /^(?!-)[A-Za-z0-9._:-]{1,256}$/u;
|
|
699
871
|
const TRANSCRIPT_ITEM_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -704,6 +876,11 @@ const TRANSCRIPT_ITEM_TYPES = /* @__PURE__ */ new Set([
|
|
|
704
876
|
"toolResult",
|
|
705
877
|
"other"
|
|
706
878
|
]);
|
|
879
|
+
const ACPX_BACKEND_ID = "acpx";
|
|
880
|
+
const PI_ACP_AGENT_ID = "pi";
|
|
881
|
+
const PI_ADOPTED_SESSION_KEY_PREFIX = "plugin:acpx:catalog-adopt:pi:";
|
|
882
|
+
var PiCatalogParamsError = class extends Error {};
|
|
883
|
+
const continueAdoption = createSessionCatalogAdoptionCoordinator();
|
|
707
884
|
function validatePiThreadId(value) {
|
|
708
885
|
if (typeof value !== "string" || !SESSION_ID_PATTERN.test(value)) throw new Error("INVALID_REQUEST: threadId is invalid");
|
|
709
886
|
return value;
|
|
@@ -715,7 +892,7 @@ function isOptionalNumber(value) {
|
|
|
715
892
|
return value === void 0 || typeof value === "number";
|
|
716
893
|
}
|
|
717
894
|
function isNodeSession(value) {
|
|
718
|
-
return isRecord(value) && typeof value.threadId === "string" && SESSION_ID_PATTERN.test(value.threadId) && typeof value.status === "string" && value.status.length > 0 && typeof value.archived === "boolean" && typeof value.canContinue === "boolean" && typeof value.canArchive === "boolean" && isOptionalString(value.name) && isOptionalString(value.cwd) && isOptionalString(value.source) && isOptionalString(value.modelProvider) && isOptionalString(value.cliVersion) && isOptionalString(value.gitBranch) && isOptionalString(value.
|
|
895
|
+
return isRecord(value) && typeof value.threadId === "string" && SESSION_ID_PATTERN.test(value.threadId) && typeof value.status === "string" && value.status.length > 0 && typeof value.archived === "boolean" && typeof value.canContinue === "boolean" && typeof value.canArchive === "boolean" && isOptionalString(value.name) && isOptionalString(value.cwd) && isOptionalString(value.source) && isOptionalString(value.modelProvider) && isOptionalString(value.cliVersion) && isOptionalString(value.gitBranch) && isOptionalString(value.sessionKey) && isOptionalNumber(value.createdAt) && isOptionalNumber(value.updatedAt) && isOptionalNumber(value.recencyAt);
|
|
719
896
|
}
|
|
720
897
|
function isNodeTranscriptItem(value) {
|
|
721
898
|
return isRecord(value) && typeof value.type === "string" && TRANSCRIPT_ITEM_TYPES.has(value.type) && isOptionalString(value.id) && isOptionalString(value.text) && isOptionalString(value.timestamp) && isOptionalString(value.model) && (value.truncated === void 0 || typeof value.truncated === "boolean");
|
|
@@ -809,8 +986,11 @@ function nodeLabel(node) {
|
|
|
809
986
|
function unwrapNodePayload(value) {
|
|
810
987
|
return isRecord(value) && typeof value.payloadJSON === "string" ? JSON.parse(value.payloadJSON) : value;
|
|
811
988
|
}
|
|
812
|
-
function
|
|
813
|
-
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
|
+
}
|
|
814
994
|
return page;
|
|
815
995
|
}
|
|
816
996
|
async function listPiNodeHost(runtime, query, node) {
|
|
@@ -831,13 +1011,15 @@ async function listPiNodeHost(runtime, query, node) {
|
|
|
831
1011
|
}
|
|
832
1012
|
};
|
|
833
1013
|
try {
|
|
1014
|
+
const cursor = query.cursors?.[hostId];
|
|
1015
|
+
if (cursor !== void 0 && !isExactPiSessionCursor(cursor)) throw new Error("cursor is invalid");
|
|
834
1016
|
const page = parseNodeSessionPage(unwrapNodePayload(await runtime.nodes.invoke({
|
|
835
1017
|
nodeId: node.nodeId,
|
|
836
1018
|
command: PI_SESSIONS_LIST_COMMAND,
|
|
837
1019
|
params: {
|
|
838
1020
|
...query.limitPerHost ? { limit: query.limitPerHost } : {},
|
|
839
1021
|
...query.search ? { searchTerm: query.search } : {},
|
|
840
|
-
...
|
|
1022
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
841
1023
|
},
|
|
842
1024
|
timeoutMs: NODE_TIMEOUT_MS,
|
|
843
1025
|
scopes: ["operator.write"]
|
|
@@ -845,7 +1027,10 @@ async function listPiNodeHost(runtime, query, node) {
|
|
|
845
1027
|
const canOpenTerminal = (node.invocableCommands ?? node.commands)?.includes(PI_TERMINAL_RESUME_COMMAND) === true;
|
|
846
1028
|
return {
|
|
847
1029
|
...common,
|
|
848
|
-
...
|
|
1030
|
+
...setCatalogCapabilities(page, {
|
|
1031
|
+
canContinue: false,
|
|
1032
|
+
canOpenTerminal
|
|
1033
|
+
})
|
|
849
1034
|
};
|
|
850
1035
|
} catch {
|
|
851
1036
|
return {
|
|
@@ -862,25 +1047,27 @@ function parseNodeSessionPage(value) {
|
|
|
862
1047
|
if (!isRecord(value) || !Array.isArray(value.sessions) || value.sessions.length > MAX_PAGE_LIMIT) throw new Error("Pi node returned an invalid session page");
|
|
863
1048
|
if (!value.sessions.every(isNodeSession)) throw new Error("Pi node returned an invalid session page");
|
|
864
1049
|
const sessions = value.sessions;
|
|
865
|
-
const nextCursor =
|
|
866
|
-
if (
|
|
1050
|
+
const nextCursor = value.nextCursor;
|
|
1051
|
+
if (nextCursor !== void 0 && !isExactPiSessionCursor(nextCursor)) throw new Error("Pi node returned an invalid cursor");
|
|
867
1052
|
return {
|
|
868
1053
|
sessions,
|
|
869
|
-
...nextCursor ? { nextCursor } : {}
|
|
1054
|
+
...nextCursor !== void 0 ? { nextCursor } : {}
|
|
870
1055
|
};
|
|
871
1056
|
}
|
|
872
1057
|
function parseNodeTranscriptPage(value, threadId) {
|
|
873
1058
|
if (!isRecord(value) || value.threadId !== threadId || !Array.isArray(value.items) || value.items.length > MAX_PAGE_LIMIT || !value.items.every(isNodeTranscriptItem)) throw new Error("Pi node returned an invalid transcript page");
|
|
874
|
-
const nextCursor =
|
|
875
|
-
if (
|
|
1059
|
+
const nextCursor = value.nextCursor;
|
|
1060
|
+
if (nextCursor !== void 0 && !isExactPiSessionCursor(nextCursor)) throw new Error("Pi node returned an invalid cursor");
|
|
876
1061
|
return {
|
|
877
1062
|
hostId: LOCAL_HOST_ID,
|
|
878
1063
|
threadId,
|
|
879
1064
|
items: value.items,
|
|
880
|
-
...nextCursor ? { nextCursor } : {}
|
|
1065
|
+
...nextCursor !== void 0 ? { nextCursor } : {}
|
|
881
1066
|
};
|
|
882
1067
|
}
|
|
883
|
-
async function listPiHosts(
|
|
1068
|
+
async function listPiHosts(api, query) {
|
|
1069
|
+
const runtime = api.runtime;
|
|
1070
|
+
const canContinue = resolvePiContinuationAvailability(api).available;
|
|
884
1071
|
const requested = query.hostIds ? new Set(query.hostIds) : void 0;
|
|
885
1072
|
const hosts = [];
|
|
886
1073
|
if ((!requested || requested.has(LOCAL_HOST_ID)) && piSessionStoreAvailable(process$1.env)) try {
|
|
@@ -893,11 +1080,14 @@ async function listPiHosts(runtime, query) {
|
|
|
893
1080
|
limit: query.limitPerHost,
|
|
894
1081
|
...query.search ? { searchTerm: query.search } : {},
|
|
895
1082
|
cursor: query.cursors?.[LOCAL_HOST_ID]
|
|
896
|
-
}).then((page) =>
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1083
|
+
}).then((page) => setCatalogCapabilities(page, {
|
|
1084
|
+
canContinue,
|
|
1085
|
+
canOpenTerminal: resolveNodeHostExecutable("pi", {
|
|
1086
|
+
env: process$1.env,
|
|
1087
|
+
pathEnv: process$1.env.PATH ?? "",
|
|
1088
|
+
strategy: "fallback"
|
|
1089
|
+
}) !== void 0
|
|
1090
|
+
}))
|
|
901
1091
|
});
|
|
902
1092
|
} catch {
|
|
903
1093
|
hosts.push({
|
|
@@ -914,7 +1104,7 @@ async function listPiHosts(runtime, query) {
|
|
|
914
1104
|
}
|
|
915
1105
|
let nodes;
|
|
916
1106
|
try {
|
|
917
|
-
nodes = (await runtime.nodes.list()).nodes;
|
|
1107
|
+
nodes = (await (query.listNodes?.() ?? runtime.nodes.list())).nodes;
|
|
918
1108
|
} catch {
|
|
919
1109
|
return hosts;
|
|
920
1110
|
}
|
|
@@ -930,6 +1120,95 @@ async function requireLocalPiSession(threadId) {
|
|
|
930
1120
|
if (!record) throw new Error("Pi session is unavailable");
|
|
931
1121
|
return record;
|
|
932
1122
|
}
|
|
1123
|
+
function currentPiCatalogConfig(api) {
|
|
1124
|
+
return api.runtime.config?.current?.() ?? api.config ?? {};
|
|
1125
|
+
}
|
|
1126
|
+
function resolvePiContinuationAvailability(api) {
|
|
1127
|
+
const availability = resolveAcpSessionAvailability({
|
|
1128
|
+
config: currentPiCatalogConfig(api),
|
|
1129
|
+
backendId: ACPX_BACKEND_ID,
|
|
1130
|
+
agentId: PI_ACP_AGENT_ID
|
|
1131
|
+
});
|
|
1132
|
+
if (!availability.available) return availability;
|
|
1133
|
+
return resolveNodeHostExecutable("pi", {
|
|
1134
|
+
env: process$1.env,
|
|
1135
|
+
pathEnv: process$1.env.PATH ?? "",
|
|
1136
|
+
strategy: "fallback"
|
|
1137
|
+
}) ? { available: true } : {
|
|
1138
|
+
available: false,
|
|
1139
|
+
message: "Pi CLI is unavailable"
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
function listAdoptedPiSessions(api) {
|
|
1143
|
+
return listAdoptedSessionCatalogSessions({
|
|
1144
|
+
config: currentPiCatalogConfig(api),
|
|
1145
|
+
pluginId: api.id,
|
|
1146
|
+
runtime: api.runtime,
|
|
1147
|
+
sourceFromEntry: (entry) => {
|
|
1148
|
+
const acpx = isRecord(entry.pluginExtensions?.acpx) ? entry.pluginExtensions.acpx : void 0;
|
|
1149
|
+
const marker = acpx && isRecord(acpx.piSessionCatalog) ? acpx.piSessionCatalog : void 0;
|
|
1150
|
+
return marker && typeof marker.sourceThreadId === "string" ? {
|
|
1151
|
+
hostId: LOCAL_HOST_ID,
|
|
1152
|
+
threadId: marker.sourceThreadId
|
|
1153
|
+
} : void 0;
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
async function continuePiSession(api, hostId, threadId) {
|
|
1158
|
+
if (hostId.startsWith("node:")) throw new PiCatalogParamsError("paired-node Pi session rows are view-only");
|
|
1159
|
+
if (hostId !== LOCAL_HOST_ID) throw new PiCatalogParamsError("Pi session catalog hostId is invalid");
|
|
1160
|
+
const availability = resolvePiContinuationAvailability(api);
|
|
1161
|
+
if (!availability.available) throw new PiCatalogParamsError(availability.message);
|
|
1162
|
+
const sourceKey = sessionCatalogAdoptedSourceKey(hostId, threadId);
|
|
1163
|
+
return await continueAdoption({
|
|
1164
|
+
sourceKey,
|
|
1165
|
+
findExisting: () => listAdoptedPiSessions(api).get(sourceKey),
|
|
1166
|
+
create: async () => {
|
|
1167
|
+
const record = await requireLocalPiSession(threadId).catch(() => void 0);
|
|
1168
|
+
if (!record) throw new PiCatalogParamsError("Pi session is unavailable");
|
|
1169
|
+
if (!record.canContinue) throw new PiCatalogParamsError("Pi session is outside the session store supported by pi-acp");
|
|
1170
|
+
const currentAvailability = resolvePiContinuationAvailability(api);
|
|
1171
|
+
if (!currentAvailability.available) throw new PiCatalogParamsError(currentAvailability.message);
|
|
1172
|
+
const config = currentPiCatalogConfig(api);
|
|
1173
|
+
const marker = { sourceThreadId: threadId };
|
|
1174
|
+
return { sessionKey: (await api.runtime.agent.session.createSessionEntry({
|
|
1175
|
+
cfg: config,
|
|
1176
|
+
key: sessionCatalogAdoptedSessionKey(PI_ADOPTED_SESSION_KEY_PREFIX, threadId),
|
|
1177
|
+
agentId: resolveDefaultAgentId(config),
|
|
1178
|
+
recoverMatchingInitialEntry: true,
|
|
1179
|
+
...record.name ? { label: record.name } : {},
|
|
1180
|
+
...record.cwd ? { spawnedCwd: record.cwd } : {},
|
|
1181
|
+
initialEntry: {
|
|
1182
|
+
acpBackendId: ACPX_BACKEND_ID,
|
|
1183
|
+
acpSessionBinding: {
|
|
1184
|
+
acpAgentId: PI_ACP_AGENT_ID,
|
|
1185
|
+
agentSessionId: threadId
|
|
1186
|
+
},
|
|
1187
|
+
pluginExtensions: { acpx: { piSessionCatalog: marker } }
|
|
1188
|
+
},
|
|
1189
|
+
afterCreate: async (entry) => {
|
|
1190
|
+
await importSessionCatalogHistory({
|
|
1191
|
+
catalogId: "pi",
|
|
1192
|
+
threadId,
|
|
1193
|
+
read: async ({ cursor, limit }) => await readPiTranscript(api.runtime, {
|
|
1194
|
+
hostId,
|
|
1195
|
+
threadId,
|
|
1196
|
+
limit,
|
|
1197
|
+
...cursor ? { cursor } : {}
|
|
1198
|
+
}),
|
|
1199
|
+
sessionId: entry.sessionId,
|
|
1200
|
+
sessionKey: entry.key,
|
|
1201
|
+
agentId: entry.agentId,
|
|
1202
|
+
...record.cwd ? { cwd: record.cwd } : {},
|
|
1203
|
+
config
|
|
1204
|
+
});
|
|
1205
|
+
return { pluginExtensions: { acpx: { piSessionCatalog: marker } } };
|
|
1206
|
+
}
|
|
1207
|
+
})).key };
|
|
1208
|
+
},
|
|
1209
|
+
complete: async (continued) => await linkContinuedPiSession(continued.sessionKey, threadId)
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
933
1212
|
async function resolveNodePiSession(params) {
|
|
934
1213
|
const record = parseNodeSessionPage(unwrapNodePayload(await params.runtime.nodes.invoke({
|
|
935
1214
|
nodeId: params.nodeId,
|
|
@@ -987,10 +1266,12 @@ async function openPiTerminal(params) {
|
|
|
987
1266
|
};
|
|
988
1267
|
}
|
|
989
1268
|
async function readPiTranscript(runtime, request) {
|
|
1269
|
+
const cursor = request.cursor;
|
|
1270
|
+
if (cursor !== void 0 && !isExactPiSessionCursor(cursor)) throw new Error("cursor is invalid");
|
|
990
1271
|
if (request.hostId === LOCAL_HOST_ID) return await readLocalPiTranscriptPage({
|
|
991
1272
|
threadId: request.threadId,
|
|
992
1273
|
...request.limit ? { limit: request.limit } : {},
|
|
993
|
-
...
|
|
1274
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
994
1275
|
});
|
|
995
1276
|
if (!request.hostId.startsWith("node:")) throw new Error("hostId is invalid");
|
|
996
1277
|
const nodeId = request.hostId.slice(5);
|
|
@@ -1003,7 +1284,7 @@ async function readPiTranscript(runtime, request) {
|
|
|
1003
1284
|
params: {
|
|
1004
1285
|
threadId: request.threadId,
|
|
1005
1286
|
...request.limit ? { limit: request.limit } : {},
|
|
1006
|
-
...
|
|
1287
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
1007
1288
|
},
|
|
1008
1289
|
timeoutMs: NODE_TIMEOUT_MS,
|
|
1009
1290
|
scopes: ["operator.write"]
|
|
@@ -1017,8 +1298,10 @@ function registerPiSessionCatalog(api) {
|
|
|
1017
1298
|
api.registerSessionCatalog({
|
|
1018
1299
|
id: "pi",
|
|
1019
1300
|
label: "Pi",
|
|
1020
|
-
list: async (query) => await listPiHosts(api
|
|
1301
|
+
list: async (query) => await listPiHosts(api, query),
|
|
1021
1302
|
read: async (request) => await readPiTranscript(api.runtime, request),
|
|
1303
|
+
continueSession: async (request) => await continuePiSession(api, request.hostId, request.threadId),
|
|
1304
|
+
checkUpstreamActivity: checkPiUpstreamActivity,
|
|
1022
1305
|
openTerminal: async (request) => await openPiTerminal({
|
|
1023
1306
|
runtime: api.runtime,
|
|
1024
1307
|
...request
|