@hachej/boring-agent 0.1.105 → 0.1.107
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/{chunk-K25PK5EC.js → chunk-3JFPS2O6.js} +1 -0
- package/dist/{chunk-JNEKOVMJ.js → chunk-6OANSUEI.js} +1 -1
- package/dist/{chunk-4IQ4HHBG.js → chunk-7XAOVFSN.js} +7 -0
- package/dist/{chunk-F2OJI2LX.js → chunk-GS4ADE34.js} +863 -358
- package/dist/{chunk-LFN45QDZ.js → chunk-KDXIXVF6.js} +5076 -3640
- package/dist/{chunk-6DRHERVK.js → chunk-MVKXLMH3.js} +44 -2
- package/dist/chunk-YTUZFKB3.js +78 -0
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +2 -2
- package/dist/{createHarness-GMBZZPTN.js → createHarness-AHWSC36D.js} +3 -3
- package/dist/{errors-DH7C01Pm.d.ts → errors-CEU4iujR.d.ts} +59 -4
- package/dist/{file-search-qcD5zItr.d.ts → file-search-BGUHnW9o.d.ts} +3 -3
- package/dist/front/index.d.ts +36 -7
- package/dist/front/index.js +1491 -727
- package/dist/front/styles.css +57 -0
- package/dist/{harness-Dcd08yKr.d.ts → harness-Ct0e4RWc.d.ts} +6 -3
- package/dist/{lease-DffrPrQn.d.ts → lease-nASWv2tU.d.ts} +1 -1
- package/dist/{piChatEvent-iNSXiFQq.d.ts → piChatEvent-BUWIzw5l.d.ts} +67 -36
- package/dist/{piChatSessionService-DQSQ_kWZ.d.ts → piChatSessionService-nvroRAVk.d.ts} +13 -11
- package/dist/server/agent-host/testing/gatewayConformance.d.ts +6 -5
- package/dist/server/agent-host/testing/gatewayConformance.js +57 -8
- package/dist/server/index.d.ts +167 -212
- package/dist/server/index.js +13 -9
- package/dist/server/worker/index.js +7 -7
- package/dist/shared/index.d.ts +34 -28
- package/dist/shared/index.js +8 -8
- package/dist/{types-BTsdaAQ2.d.ts → types-B7-PN5jV.d.ts} +47 -14
- package/docs/AGENT_GATEWAY_V0.md +40 -2
- package/docs/ERROR_CODES.md +1 -0
- package/docs/README.md +1 -0
- package/docs/SESSION_IDENTITY.md +91 -0
- package/docs/tools.md +4 -0
- package/package.json +11 -10
- package/dist/chunk-WSQ5QNIY.js +0 -18
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SAFE_NATIVE_SESSION_ID,
|
|
3
|
+
compareSessionOrder,
|
|
3
4
|
noopTelemetry,
|
|
4
5
|
safeCapture
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-7XAOVFSN.js";
|
|
6
7
|
import {
|
|
7
8
|
ErrorCode
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-3JFPS2O6.js";
|
|
9
10
|
|
|
10
11
|
// src/server/harness/pi-coding-agent/createHarness.ts
|
|
11
12
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
12
13
|
import {
|
|
13
14
|
createAgentSession,
|
|
14
15
|
SessionManager as SessionManager2,
|
|
15
|
-
AuthStorage,
|
|
16
|
-
ModelRegistry,
|
|
17
16
|
DefaultResourceLoader,
|
|
18
17
|
getAgentDir as getAgentDir2,
|
|
19
18
|
loadSkills
|
|
@@ -122,6 +121,11 @@ function createPiFollowUpQueueCompat() {
|
|
|
122
121
|
if (nonce) seenNonces.add(nonce);
|
|
123
122
|
return true;
|
|
124
123
|
}
|
|
124
|
+
function rollback(text, options) {
|
|
125
|
+
const index = queue.findIndex((item) => item.text === text && item.clientNonce === options?.clientNonce && item.clientSeq === options?.clientSeq);
|
|
126
|
+
if (index >= 0) queue.splice(index, 1);
|
|
127
|
+
if (options?.clientNonce) seenNonces.delete(options.clientNonce);
|
|
128
|
+
}
|
|
125
129
|
function clear(piSession, options) {
|
|
126
130
|
syncWithPi(piSession);
|
|
127
131
|
const removed = removeNativeFollowUp(options);
|
|
@@ -176,7 +180,7 @@ function createPiFollowUpQueueCompat() {
|
|
|
176
180
|
if (queue.length === 0) seenNonces.clear();
|
|
177
181
|
return removed;
|
|
178
182
|
}
|
|
179
|
-
return { record, clear };
|
|
183
|
+
return { record, rollback, clear };
|
|
180
184
|
}
|
|
181
185
|
function removePiQueuedFollowUp(piSession, text, textOrdinal = 0) {
|
|
182
186
|
const queue = piQueueAccess(piSession);
|
|
@@ -241,12 +245,29 @@ function matchesFollowUpSelector(item, options) {
|
|
|
241
245
|
}
|
|
242
246
|
|
|
243
247
|
// src/server/pi-chat/PiAgentSessionAdapter.ts
|
|
248
|
+
var followUpQueuesBySession = /* @__PURE__ */ new WeakMap();
|
|
249
|
+
function followUpQueueFor(session) {
|
|
250
|
+
const existing = followUpQueuesBySession.get(session);
|
|
251
|
+
if (existing) return existing;
|
|
252
|
+
const created = createPiFollowUpQueueCompat();
|
|
253
|
+
followUpQueuesBySession.set(session, created);
|
|
254
|
+
return created;
|
|
255
|
+
}
|
|
244
256
|
function normalizePromptInput(input) {
|
|
245
257
|
if (typeof input === "string") return { text: input };
|
|
246
258
|
return input;
|
|
247
259
|
}
|
|
260
|
+
var cancellationStateBySession = /* @__PURE__ */ new WeakMap();
|
|
261
|
+
function cancellationStateFor(session) {
|
|
262
|
+
const existing = cancellationStateBySession.get(session);
|
|
263
|
+
if (existing) return existing;
|
|
264
|
+
const created = { abortRequested: false };
|
|
265
|
+
cancellationStateBySession.set(session, created);
|
|
266
|
+
return created;
|
|
267
|
+
}
|
|
248
268
|
function createPiAgentSessionAdapter(session, options = {}) {
|
|
249
|
-
const followUpQueue =
|
|
269
|
+
const followUpQueue = followUpQueueFor(session);
|
|
270
|
+
const cancellationState = cancellationStateFor(session);
|
|
250
271
|
const adapter = {
|
|
251
272
|
readSnapshot() {
|
|
252
273
|
return {
|
|
@@ -268,7 +289,25 @@ function createPiAgentSessionAdapter(session, options = {}) {
|
|
|
268
289
|
return typeof model?.provider === "string" && typeof model?.id === "string" ? { provider: model.provider, id: model.id } : void 0;
|
|
269
290
|
},
|
|
270
291
|
subscribe(listener) {
|
|
271
|
-
return session.subscribe(
|
|
292
|
+
return session.subscribe((event) => {
|
|
293
|
+
if (event.type === "agent_start") cancellationState.abortRequested = false;
|
|
294
|
+
if (!cancellationState.abortRequested) {
|
|
295
|
+
listener(event);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
299
|
+
listener({ ...event, message: { ...event.message, stopReason: "aborted" } });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (event.type === "agent_end") {
|
|
303
|
+
const messages = event.messages.map(
|
|
304
|
+
(message, index) => index === event.messages.length - 1 && message.role === "assistant" ? { ...message, stopReason: "aborted" } : message
|
|
305
|
+
);
|
|
306
|
+
listener({ ...event, messages });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
listener(event);
|
|
310
|
+
});
|
|
272
311
|
},
|
|
273
312
|
async prompt(input) {
|
|
274
313
|
const { text, options: options2 } = normalizePromptInput(input);
|
|
@@ -277,12 +316,18 @@ function createPiAgentSessionAdapter(session, options = {}) {
|
|
|
277
316
|
async followUp(text, followUpOptions) {
|
|
278
317
|
const accepted = followUpQueue.record(text, followUpOptions);
|
|
279
318
|
if (!accepted) return;
|
|
280
|
-
|
|
319
|
+
try {
|
|
320
|
+
await session.followUp(text);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
followUpQueue.rollback(text, followUpOptions);
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
281
325
|
},
|
|
282
326
|
clearFollowUp(selector) {
|
|
283
327
|
followUpQueue.clear(session, selector);
|
|
284
328
|
},
|
|
285
329
|
async abort() {
|
|
330
|
+
cancellationState.abortRequested = true;
|
|
286
331
|
await session.abort();
|
|
287
332
|
}
|
|
288
333
|
};
|
|
@@ -296,20 +341,19 @@ function createPiAgentSessionAdapter(session, options = {}) {
|
|
|
296
341
|
}
|
|
297
342
|
|
|
298
343
|
// src/server/harness/pi-coding-agent/sessions.ts
|
|
299
|
-
import { randomUUID } from "node:crypto";
|
|
344
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
300
345
|
import {
|
|
301
|
-
readdir,
|
|
302
|
-
readFile,
|
|
346
|
+
readdir as readdir2,
|
|
347
|
+
readFile as readFile3,
|
|
303
348
|
stat as fsStat2,
|
|
304
|
-
rm,
|
|
305
|
-
mkdir,
|
|
306
|
-
writeFile,
|
|
307
|
-
appendFile,
|
|
308
|
-
|
|
309
|
-
open as open4
|
|
349
|
+
rm as rm2,
|
|
350
|
+
mkdir as mkdir2,
|
|
351
|
+
writeFile as writeFile2,
|
|
352
|
+
appendFile as appendFile2,
|
|
353
|
+
open as open3
|
|
310
354
|
} from "node:fs/promises";
|
|
311
355
|
import { closeSync, openSync, readFileSync, readSync, readdirSync, writeFileSync } from "node:fs";
|
|
312
|
-
import { join, basename, resolve } from "node:path";
|
|
356
|
+
import { join as join2, basename, resolve } from "node:path";
|
|
313
357
|
import { homedir } from "node:os";
|
|
314
358
|
|
|
315
359
|
// src/server/config/env.ts
|
|
@@ -324,7 +368,8 @@ import {
|
|
|
324
368
|
} from "@mariozechner/pi-coding-agent";
|
|
325
369
|
|
|
326
370
|
// src/server/harness/pi-coding-agent/nativeSessionRename.ts
|
|
327
|
-
import {
|
|
371
|
+
import { randomUUID } from "node:crypto";
|
|
372
|
+
import { appendFile, readFile, stat as fsStat, utimes } from "node:fs/promises";
|
|
328
373
|
import {
|
|
329
374
|
CURRENT_SESSION_VERSION,
|
|
330
375
|
SessionManager
|
|
@@ -360,99 +405,9 @@ function completeJsonlPrefix(content, reachedLimit) {
|
|
|
360
405
|
return lastNewline >= 0 ? content.slice(0, lastNewline + 1) : content;
|
|
361
406
|
}
|
|
362
407
|
|
|
363
|
-
// src/server/harness/pi-coding-agent/nativeSessionRename.ts
|
|
364
|
-
var NATIVE_RENAME_MAX_APPEND_BYTES = 64 * 1024;
|
|
365
|
-
var NATIVE_RENAME_MAX_ATTEMPTS = 3;
|
|
366
|
-
async function restoreVerifiedNativeRenameMtime(filepath, before, verifiedSize) {
|
|
367
|
-
try {
|
|
368
|
-
const current = await fsStat(filepath);
|
|
369
|
-
if (current.dev !== before.dev || current.ino !== before.ino || current.size !== verifiedSize) return;
|
|
370
|
-
const atimeSeconds = fileTimeSeconds(before.atimeMs);
|
|
371
|
-
const mtimeSeconds = fileTimeSeconds(before.mtimeMs);
|
|
372
|
-
if (atimeSeconds === void 0 || mtimeSeconds === void 0) return;
|
|
373
|
-
await utimes(filepath, atimeSeconds, mtimeSeconds);
|
|
374
|
-
const restored = await fsStat(filepath);
|
|
375
|
-
if (restored.size !== verifiedSize) {
|
|
376
|
-
const restoredAtimeSeconds = fileTimeSeconds(restored.atimeMs);
|
|
377
|
-
if (restoredAtimeSeconds !== void 0) await utimes(filepath, restoredAtimeSeconds, Date.now() / 1e3);
|
|
378
|
-
}
|
|
379
|
-
} catch {
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
async function verifiedNativeRenameAppend(filepath, before, append, title) {
|
|
383
|
-
const after = await fsStat(filepath);
|
|
384
|
-
if (after.dev !== before.dev || after.ino !== before.ino) return null;
|
|
385
|
-
const appendedBytes = after.size - before.size;
|
|
386
|
-
if (!Number.isSafeInteger(before.size) || !Number.isSafeInteger(after.size) || appendedBytes <= 0 || appendedBytes > NATIVE_RENAME_MAX_APPEND_BYTES) return null;
|
|
387
|
-
const handle = await open2(filepath, "r");
|
|
388
|
-
try {
|
|
389
|
-
const record = Buffer.allocUnsafe(appendedBytes);
|
|
390
|
-
const { bytesRead } = await handle.read(record, 0, record.length, before.size);
|
|
391
|
-
if (bytesRead !== record.length || record.at(-1) !== 10 || record.indexOf(10) !== record.length - 1) return null;
|
|
392
|
-
const parsed = JSON.parse(record.toString("utf-8"));
|
|
393
|
-
if (typeof parsed !== "object" || parsed === null) return null;
|
|
394
|
-
const entry = parsed;
|
|
395
|
-
return entry.type === "session_info" && entry.id === append.id && entry.parentId === append.parentId && entry.name === title ? after.size : null;
|
|
396
|
-
} catch {
|
|
397
|
-
return null;
|
|
398
|
-
} finally {
|
|
399
|
-
await handle.close();
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
function latestConcurrentEntryId(manager, staleRenameIds) {
|
|
403
|
-
for (const entry of manager.getEntries().reverse()) {
|
|
404
|
-
if (!staleRenameIds.has(entry.id)) return entry.id;
|
|
405
|
-
}
|
|
406
|
-
return null;
|
|
407
|
-
}
|
|
408
|
-
function nativeRenameLockedError(message = "native session changed while renaming; retry") {
|
|
409
|
-
return Object.assign(new Error(message), {
|
|
410
|
-
code: ErrorCode.enum.SESSION_LOCKED,
|
|
411
|
-
statusCode: 409,
|
|
412
|
-
retryable: true
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
async function appendVerifiedNativeRename(filepath, sessionDir, cwd, title) {
|
|
416
|
-
const staleRenameIds = /* @__PURE__ */ new Set();
|
|
417
|
-
for (let attempt = 0; attempt < NATIVE_RENAME_MAX_ATTEMPTS; attempt += 1) {
|
|
418
|
-
try {
|
|
419
|
-
const before = await fsStat(filepath);
|
|
420
|
-
const header = parseJsonlPrefixEntries(await readJsonlPrefix(filepath)).find(
|
|
421
|
-
(entry) => entry.type === "session"
|
|
422
|
-
);
|
|
423
|
-
if (header?.version !== CURRENT_SESSION_VERSION) {
|
|
424
|
-
throw nativeRenameLockedError("This native session was created by an unsupported Pi version and cannot be renamed.");
|
|
425
|
-
}
|
|
426
|
-
const manager = SessionManager.open(filepath, sessionDir, cwd);
|
|
427
|
-
if (staleRenameIds.size > 0) {
|
|
428
|
-
const concurrentLeaf = latestConcurrentEntryId(manager, staleRenameIds);
|
|
429
|
-
if (!concurrentLeaf) break;
|
|
430
|
-
manager.branch(concurrentLeaf);
|
|
431
|
-
}
|
|
432
|
-
const append = {
|
|
433
|
-
parentId: manager.getLeafId(),
|
|
434
|
-
id: manager.appendSessionInfo(title)
|
|
435
|
-
};
|
|
436
|
-
staleRenameIds.add(append.id);
|
|
437
|
-
const verifiedSize = await verifiedNativeRenameAppend(filepath, before, append, title);
|
|
438
|
-
if (verifiedSize !== null) {
|
|
439
|
-
await restoreVerifiedNativeRenameMtime(filepath, before, verifiedSize);
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
} catch (error) {
|
|
443
|
-
if (error.code === ErrorCode.enum.SESSION_LOCKED) throw error;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
throw nativeRenameLockedError();
|
|
447
|
-
}
|
|
448
|
-
function fileTimeSeconds(milliseconds) {
|
|
449
|
-
const seconds = milliseconds / 1e3;
|
|
450
|
-
return Number.isFinite(seconds) ? seconds : void 0;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
408
|
// src/server/harness/pi-coding-agent/nativeSessionTranscript.ts
|
|
454
409
|
import { createReadStream } from "node:fs";
|
|
455
|
-
import { open as
|
|
410
|
+
import { open as open2 } from "node:fs/promises";
|
|
456
411
|
import { createInterface } from "node:readline";
|
|
457
412
|
|
|
458
413
|
// src/server/harness/pi-coding-agent/piSessionMessages.ts
|
|
@@ -465,14 +420,108 @@ function textFromPiContent(content) {
|
|
|
465
420
|
}).join("");
|
|
466
421
|
}
|
|
467
422
|
|
|
423
|
+
// src/server/harness/pi-coding-agent/sessionTitleAuthority.ts
|
|
424
|
+
var USER_SESSION_TITLE_CUSTOM_TYPE = "boring.session-title-authority";
|
|
425
|
+
var MAX_USER_SESSION_TITLE_CHARACTERS = 200;
|
|
426
|
+
function normalizeUserSessionTitle(title) {
|
|
427
|
+
const normalized = title.replace(/[\r\n]+/g, " ").trim();
|
|
428
|
+
if (!normalized) throw new Error("Session title must not be empty");
|
|
429
|
+
if (normalized.length > MAX_USER_SESSION_TITLE_CHARACTERS) {
|
|
430
|
+
throw new Error(`Session title must be at most ${MAX_USER_SESSION_TITLE_CHARACTERS} characters`);
|
|
431
|
+
}
|
|
432
|
+
return normalized;
|
|
433
|
+
}
|
|
434
|
+
function userSessionTitleData(title) {
|
|
435
|
+
return { titleSetByUser: true, title };
|
|
436
|
+
}
|
|
437
|
+
function isAttachedUserSessionTitleMarker(parent, marker) {
|
|
438
|
+
if (marker?.type !== "custom" || typeof marker.id !== "string" || !marker.id || marker.customType !== USER_SESSION_TITLE_CUSTOM_TYPE) return false;
|
|
439
|
+
if (parent && (typeof parent.id !== "string" || !parent.id)) return false;
|
|
440
|
+
return marker.parentId === (parent?.id ?? null);
|
|
441
|
+
}
|
|
442
|
+
function userSessionTitleFromSequence(parent, marker, titleEntry) {
|
|
443
|
+
if (!isAttachedUserSessionTitleMarker(parent, marker)) return void 0;
|
|
444
|
+
if (titleEntry?.type !== "session_info" || typeof titleEntry.id !== "string" || !titleEntry.id || titleEntry.parentId !== marker?.id) return void 0;
|
|
445
|
+
if (parent?.id === marker?.id || parent?.id === titleEntry.id || marker?.id === titleEntry.id) return void 0;
|
|
446
|
+
const data = marker?.data;
|
|
447
|
+
if (data?.titleSetByUser !== true || typeof data.title !== "string") return void 0;
|
|
448
|
+
try {
|
|
449
|
+
const title = normalizeUserSessionTitle(data.title);
|
|
450
|
+
return titleEntry.name === title ? title : void 0;
|
|
451
|
+
} catch {
|
|
452
|
+
return void 0;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function createUserSessionTitleProjection() {
|
|
456
|
+
let parent;
|
|
457
|
+
let marker;
|
|
458
|
+
let previousKnown = false;
|
|
459
|
+
let markerPredecessorKnown = false;
|
|
460
|
+
let title;
|
|
461
|
+
let timestamp;
|
|
462
|
+
const reset = (predecessorKnown) => {
|
|
463
|
+
parent = void 0;
|
|
464
|
+
marker = void 0;
|
|
465
|
+
previousKnown = predecessorKnown;
|
|
466
|
+
markerPredecessorKnown = false;
|
|
467
|
+
};
|
|
468
|
+
return {
|
|
469
|
+
get title() {
|
|
470
|
+
return title;
|
|
471
|
+
},
|
|
472
|
+
get timestamp() {
|
|
473
|
+
return timestamp;
|
|
474
|
+
},
|
|
475
|
+
accept(entry) {
|
|
476
|
+
if (entry.type === "session" || entry.type === "pi_session_file") {
|
|
477
|
+
reset(true);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const projected = markerPredecessorKnown ? userSessionTitleFromSequence(parent, marker, entry) : void 0;
|
|
481
|
+
if (projected) {
|
|
482
|
+
title = projected;
|
|
483
|
+
timestamp = typeof entry.timestamp === "string" ? entry.timestamp : void 0;
|
|
484
|
+
}
|
|
485
|
+
parent = marker;
|
|
486
|
+
marker = entry;
|
|
487
|
+
markerPredecessorKnown = previousKnown;
|
|
488
|
+
previousKnown = true;
|
|
489
|
+
},
|
|
490
|
+
breakSequence() {
|
|
491
|
+
reset(false);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
function createUserSessionTitleEntries(input) {
|
|
496
|
+
const authority = {
|
|
497
|
+
type: "custom",
|
|
498
|
+
id: input.authorityId,
|
|
499
|
+
parentId: input.parentId,
|
|
500
|
+
timestamp: input.timestamp,
|
|
501
|
+
customType: USER_SESSION_TITLE_CUSTOM_TYPE,
|
|
502
|
+
data: userSessionTitleData(input.title)
|
|
503
|
+
};
|
|
504
|
+
return {
|
|
505
|
+
authority,
|
|
506
|
+
title: {
|
|
507
|
+
type: "session_info",
|
|
508
|
+
id: input.titleId,
|
|
509
|
+
parentId: input.authorityId,
|
|
510
|
+
timestamp: input.timestamp,
|
|
511
|
+
name: input.title
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
468
516
|
// src/server/harness/pi-coding-agent/nativeSessionTranscript.ts
|
|
469
517
|
var NATIVE_TAIL_CHUNK_BYTES = 64 * 1024;
|
|
470
518
|
var NATIVE_TAIL_MAX_RECORD_BYTES = 256 * 1024;
|
|
471
519
|
var NATIVE_TAIL_MAX_RECORD_FRAGMENTS = 4;
|
|
472
|
-
async function
|
|
473
|
-
const handle = await
|
|
520
|
+
async function nativeTranscriptOrdering(filepath, size) {
|
|
521
|
+
const handle = await open2(filepath, "r");
|
|
474
522
|
let end = size;
|
|
475
523
|
let lineFragments = [];
|
|
524
|
+
let hasEntries = false;
|
|
476
525
|
try {
|
|
477
526
|
while (end > 0) {
|
|
478
527
|
const start = Math.max(0, end - NATIVE_TAIL_CHUNK_BYTES);
|
|
@@ -482,11 +531,14 @@ async function latestNativeMessageTimestamp(filepath, size) {
|
|
|
482
531
|
while (lineEnd > 0) {
|
|
483
532
|
const newline = chunk.lastIndexOf(10, lineEnd - 1);
|
|
484
533
|
if (newline < 0) break;
|
|
485
|
-
const
|
|
534
|
+
const metadata2 = nativeRecordOrderingFromBoundedPrefix(
|
|
486
535
|
nativeTailRecordPrefix(chunk.subarray(newline + 1, lineEnd), lineFragments)
|
|
487
536
|
);
|
|
488
537
|
lineFragments = [];
|
|
489
|
-
|
|
538
|
+
hasEntries ||= metadata2.hasEntry;
|
|
539
|
+
if (metadata2.latestMessageAtMs !== void 0) {
|
|
540
|
+
return { hasEntries: true, latestMessageAtMs: metadata2.latestMessageAtMs };
|
|
541
|
+
}
|
|
490
542
|
lineEnd = newline;
|
|
491
543
|
}
|
|
492
544
|
if (lineEnd > 0) {
|
|
@@ -494,9 +546,13 @@ async function latestNativeMessageTimestamp(filepath, size) {
|
|
|
494
546
|
}
|
|
495
547
|
end = start;
|
|
496
548
|
}
|
|
497
|
-
|
|
549
|
+
const metadata = nativeRecordOrderingFromBoundedPrefix(
|
|
498
550
|
nativeTailRecordPrefix(lineFragments.at(-1) ?? Buffer.alloc(0), lineFragments.slice(0, -1))
|
|
499
551
|
);
|
|
552
|
+
return {
|
|
553
|
+
hasEntries: hasEntries || metadata.hasEntry,
|
|
554
|
+
...metadata.latestMessageAtMs !== void 0 ? { latestMessageAtMs: metadata.latestMessageAtMs } : {}
|
|
555
|
+
};
|
|
500
556
|
} finally {
|
|
501
557
|
await handle.close();
|
|
502
558
|
}
|
|
@@ -526,44 +582,419 @@ function nativeTailRecordPrefix(recordStart, followingFragments) {
|
|
|
526
582
|
}
|
|
527
583
|
return prefix;
|
|
528
584
|
}
|
|
529
|
-
function
|
|
530
|
-
if (prefix.length === 0) return
|
|
585
|
+
function nativeRecordOrderingFromBoundedPrefix(prefix) {
|
|
586
|
+
if (prefix.length === 0) return { hasEntry: false };
|
|
531
587
|
const line = prefix.subarray(0, NATIVE_TAIL_MAX_RECORD_BYTES).toString("utf-8");
|
|
532
|
-
|
|
588
|
+
const typeMatch = /^\s*\{\s*"type"\s*:\s*"([^"\\]+)"(?:\s*,|\s*})/.exec(line);
|
|
589
|
+
if (!typeMatch || typeMatch[1] === "session") return { hasEntry: false };
|
|
590
|
+
if (typeMatch[1] !== "message") return { hasEntry: true };
|
|
533
591
|
const timestampMatch = /"timestamp"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/.exec(line);
|
|
534
|
-
if (!timestampMatch) return
|
|
592
|
+
if (!timestampMatch) return { hasEntry: true };
|
|
535
593
|
const timestamp = Date.parse(timestampMatch[1]);
|
|
536
|
-
return Number.isFinite(timestamp) ? timestamp :
|
|
594
|
+
return Number.isFinite(timestamp) ? { hasEntry: true, latestMessageAtMs: timestamp } : { hasEntry: true };
|
|
537
595
|
}
|
|
538
|
-
|
|
596
|
+
function createSessionTranscriptAccumulator() {
|
|
539
597
|
let title;
|
|
598
|
+
let titleTimestamp;
|
|
540
599
|
let firstUserTitle;
|
|
600
|
+
let entryCount = 0;
|
|
541
601
|
let turnCount = 0;
|
|
542
|
-
let
|
|
602
|
+
let hasAssistantReply = false;
|
|
543
603
|
let latestMessageAtMs;
|
|
604
|
+
const userTitle = createUserSessionTitleProjection();
|
|
605
|
+
const authorityMarkerIds = /* @__PURE__ */ new Set();
|
|
606
|
+
return {
|
|
607
|
+
acceptLine(line, committed = true) {
|
|
608
|
+
if (!line.trim()) return void 0;
|
|
609
|
+
let entry;
|
|
610
|
+
try {
|
|
611
|
+
entry = JSON.parse(line);
|
|
612
|
+
} catch {
|
|
613
|
+
userTitle.breakSequence();
|
|
614
|
+
return void 0;
|
|
615
|
+
}
|
|
616
|
+
if (!entry || typeof entry !== "object" || typeof entry.type !== "string") {
|
|
617
|
+
userTitle.breakSequence();
|
|
618
|
+
return void 0;
|
|
619
|
+
}
|
|
620
|
+
const record = entry;
|
|
621
|
+
if (record.type === "custom" && record.customType === USER_SESSION_TITLE_CUSTOM_TYPE && typeof record.id === "string") authorityMarkerIds.add(record.id);
|
|
622
|
+
if (committed) userTitle.accept(entry);
|
|
623
|
+
else userTitle.breakSequence();
|
|
624
|
+
if (entry.type !== "session") entryCount += 1;
|
|
625
|
+
if (entry.type === "session_info" && typeof entry.name === "string") {
|
|
626
|
+
if (!authorityMarkerIds.has(entry.parentId ?? "")) {
|
|
627
|
+
title = entry.name;
|
|
628
|
+
titleTimestamp = typeof entry.timestamp === "string" ? entry.timestamp : void 0;
|
|
629
|
+
}
|
|
630
|
+
} else if (entry.type === "message") {
|
|
631
|
+
const timestamp = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : Number.NaN;
|
|
632
|
+
if (!Number.isNaN(timestamp)) latestMessageAtMs = timestamp;
|
|
633
|
+
if (entry.message?.role === "user") {
|
|
634
|
+
turnCount += 1;
|
|
635
|
+
firstUserTitle ??= textFromPiContent(entry.message.content).slice(0, 80) || void 0;
|
|
636
|
+
} else if (entry.message?.role === "assistant") {
|
|
637
|
+
hasAssistantReply = true;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return entry;
|
|
641
|
+
},
|
|
642
|
+
summary() {
|
|
643
|
+
return {
|
|
644
|
+
...title ? { title } : {},
|
|
645
|
+
...titleTimestamp ? { titleTimestamp } : {},
|
|
646
|
+
...userTitle.title ? { userTitle: userTitle.title } : {},
|
|
647
|
+
...userTitle.timestamp ? { userTitleTimestamp: userTitle.timestamp } : {},
|
|
648
|
+
...firstUserTitle ? { firstUserTitle } : {},
|
|
649
|
+
entryCount,
|
|
650
|
+
turnCount,
|
|
651
|
+
hasAssistantReply,
|
|
652
|
+
...latestMessageAtMs !== void 0 ? { latestMessageAtMs } : {}
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function parseSessionTranscript(content) {
|
|
658
|
+
const accumulator = createSessionTranscriptAccumulator();
|
|
659
|
+
const entries = [];
|
|
660
|
+
const physicalLines = content.split("\n");
|
|
661
|
+
const lastIndex = physicalLines.length - 1;
|
|
662
|
+
for (const [index, line] of physicalLines.entries()) {
|
|
663
|
+
const entry = accumulator.acceptLine(line, index < lastIndex);
|
|
664
|
+
if (entry) entries.push(entry);
|
|
665
|
+
}
|
|
666
|
+
return { entries, summary: accumulator.summary() };
|
|
667
|
+
}
|
|
668
|
+
async function transcriptEndsWithNewline(filepath) {
|
|
669
|
+
const handle = await open2(filepath, "r");
|
|
670
|
+
try {
|
|
671
|
+
const { size } = await handle.stat();
|
|
672
|
+
if (size === 0) return true;
|
|
673
|
+
const byte = Buffer.allocUnsafe(1);
|
|
674
|
+
const { bytesRead } = await handle.read(byte, 0, 1, size - 1);
|
|
675
|
+
return bytesRead === 1 && byte[0] === 10;
|
|
676
|
+
} finally {
|
|
677
|
+
await handle.close();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async function summarizeSessionTranscript(filepath) {
|
|
681
|
+
const accumulator = createSessionTranscriptAccumulator();
|
|
682
|
+
const endsWithNewline = await transcriptEndsWithNewline(filepath);
|
|
544
683
|
const input = createReadStream(filepath, { encoding: "utf-8" });
|
|
545
684
|
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
685
|
+
let pending;
|
|
546
686
|
for await (const line of lines) {
|
|
687
|
+
if (pending !== void 0) accumulator.acceptLine(pending);
|
|
688
|
+
pending = line;
|
|
689
|
+
}
|
|
690
|
+
if (pending !== void 0) accumulator.acceptLine(pending, endsWithNewline);
|
|
691
|
+
return accumulator.summary();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/server/harness/pi-coding-agent/nativeSessionRename.ts
|
|
695
|
+
var SESSION_TITLE_MAX_APPEND_ATTEMPTS = 3;
|
|
696
|
+
var SESSION_TITLE_RETRY_CUSTOM_TYPE = "boring.session-title-retry";
|
|
697
|
+
function sameSnapshot(a, b) {
|
|
698
|
+
return a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
|
|
699
|
+
}
|
|
700
|
+
async function readStableFile(filepath) {
|
|
701
|
+
const before = await fsStat(filepath);
|
|
702
|
+
const content = await readFile(filepath, "utf-8");
|
|
703
|
+
const after = await fsStat(filepath);
|
|
704
|
+
return sameSnapshot(before, after) ? { before, after, content } : null;
|
|
705
|
+
}
|
|
706
|
+
async function restoreVerifiedRenameMtime(filepath, before, verifiedSize) {
|
|
707
|
+
try {
|
|
708
|
+
const current = await fsStat(filepath);
|
|
709
|
+
if (current.dev !== before.dev || current.ino !== before.ino || current.size !== verifiedSize) return;
|
|
710
|
+
const atimeSeconds = fileTimeSeconds(before.atimeMs);
|
|
711
|
+
const mtimeSeconds = fileTimeSeconds(before.mtimeMs);
|
|
712
|
+
if (atimeSeconds === void 0 || mtimeSeconds === void 0) return;
|
|
713
|
+
await utimes(filepath, atimeSeconds, mtimeSeconds);
|
|
714
|
+
const restored = await fsStat(filepath);
|
|
715
|
+
if (restored.size !== verifiedSize) {
|
|
716
|
+
const restoredAtimeSeconds = fileTimeSeconds(restored.atimeMs);
|
|
717
|
+
if (restoredAtimeSeconds !== void 0) await utimes(filepath, restoredAtimeSeconds, Date.now() / 1e3);
|
|
718
|
+
}
|
|
719
|
+
} catch {
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
function committedPhysicalLines(content) {
|
|
723
|
+
const lines = content.split("\n");
|
|
724
|
+
if (!content.endsWith("\n")) lines.pop();
|
|
725
|
+
return lines;
|
|
726
|
+
}
|
|
727
|
+
function hasVerifiedTitleSequence(content, append, title) {
|
|
728
|
+
let parent;
|
|
729
|
+
let marker;
|
|
730
|
+
let previousKnown = false;
|
|
731
|
+
let markerPredecessorKnown = false;
|
|
732
|
+
for (const line of committedPhysicalLines(content)) {
|
|
547
733
|
if (!line.trim()) continue;
|
|
734
|
+
let entry;
|
|
548
735
|
try {
|
|
549
|
-
|
|
550
|
-
if (entry.type === "session_info" && typeof entry.name === "string") {
|
|
551
|
-
title = entry.name;
|
|
552
|
-
continue;
|
|
553
|
-
}
|
|
554
|
-
if (entry.type !== "message") continue;
|
|
555
|
-
const timestamp = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : Number.NaN;
|
|
556
|
-
if (!Number.isNaN(timestamp)) latestMessageAtMs = timestamp;
|
|
557
|
-
if (entry.message?.role === "user") {
|
|
558
|
-
turnCount += 1;
|
|
559
|
-
firstUserTitle ??= textFromPiContent(entry.message.content).slice(0, 80) || void 0;
|
|
560
|
-
} else if (entry.message?.role === "assistant") {
|
|
561
|
-
hasAssistantReply2 = true;
|
|
562
|
-
}
|
|
736
|
+
entry = JSON.parse(line);
|
|
563
737
|
} catch {
|
|
738
|
+
parent = void 0;
|
|
739
|
+
marker = void 0;
|
|
740
|
+
previousKnown = false;
|
|
741
|
+
markerPredecessorKnown = false;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
if (!entry || typeof entry !== "object" || typeof entry.type !== "string") {
|
|
745
|
+
parent = void 0;
|
|
746
|
+
marker = void 0;
|
|
747
|
+
previousKnown = false;
|
|
748
|
+
markerPredecessorKnown = false;
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
if (entry.type === "session" || entry.type === "pi_session_file") {
|
|
752
|
+
parent = void 0;
|
|
753
|
+
marker = void 0;
|
|
754
|
+
previousKnown = true;
|
|
755
|
+
markerPredecessorKnown = false;
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (entry.id === append.title.id && marker?.id === append.authority.id && markerPredecessorKnown && marker.parentId === append.authority.parentId && entry.parentId === append.title.parentId && userSessionTitleFromSequence(parent, marker, entry) === title) return true;
|
|
759
|
+
parent = marker;
|
|
760
|
+
marker = entry;
|
|
761
|
+
markerPredecessorKnown = previousKnown;
|
|
762
|
+
previousKnown = true;
|
|
763
|
+
}
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
async function verifyTitleSequence(filepath, append, title) {
|
|
767
|
+
const snapshot = await readStableFile(filepath);
|
|
768
|
+
if (!snapshot || !hasVerifiedTitleSequence(snapshot.content, append, title)) return null;
|
|
769
|
+
const parsed = parseSessionTranscript(snapshot.content);
|
|
770
|
+
const tailId = parsed.entries.filter((entry) => entry.type !== "session" && typeof entry.id === "string").at(-1)?.id ?? null;
|
|
771
|
+
return { stat: snapshot.after, content: snapshot.content, tailId };
|
|
772
|
+
}
|
|
773
|
+
function isExactRenameOnlyAppend(origin, verifiedContent, append) {
|
|
774
|
+
const prefix = `${origin.content}${origin.content.endsWith("\n") ? "" : "\n"}`;
|
|
775
|
+
if (!verifiedContent.startsWith(prefix)) return false;
|
|
776
|
+
const suffix = verifiedContent.slice(prefix.length);
|
|
777
|
+
const lines = suffix.split("\n");
|
|
778
|
+
if (lines.length !== 3 || lines[2] !== "" || !lines[0]?.trim() || !lines[1]?.trim()) return false;
|
|
779
|
+
try {
|
|
780
|
+
const entries = lines.slice(0, 2).map((line) => JSON.parse(line));
|
|
781
|
+
return entries[0]?.id === append.authority.id && entries[1]?.id === append.title.id;
|
|
782
|
+
} catch {
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function physicalTail(content) {
|
|
787
|
+
const endsWithNewline = content.length === 0 || content.endsWith("\n");
|
|
788
|
+
const line = content.split("\n").reverse().find((candidate) => candidate.trim());
|
|
789
|
+
if (!line) return { endsWithNewline, valid: true, parentId: null };
|
|
790
|
+
try {
|
|
791
|
+
const entry = JSON.parse(line);
|
|
792
|
+
if (!entry || typeof entry !== "object" || typeof entry.type !== "string") {
|
|
793
|
+
return { endsWithNewline, valid: false, parentId: null };
|
|
794
|
+
}
|
|
795
|
+
if (entry.type === "session" || entry.type === "pi_session_file") {
|
|
796
|
+
return { endsWithNewline, valid: true, parentId: null };
|
|
797
|
+
}
|
|
798
|
+
return typeof entry.id === "string" && entry.id ? { endsWithNewline, valid: true, parentId: entry.id } : { endsWithNewline, valid: false, parentId: null };
|
|
799
|
+
} catch {
|
|
800
|
+
return { endsWithNewline, valid: false, parentId: null };
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function latestConcurrentEntryId(manager, staleRenameIds) {
|
|
804
|
+
for (const entry of manager.getEntries().reverse()) {
|
|
805
|
+
if (!staleRenameIds.has(entry.id)) return entry.id;
|
|
806
|
+
}
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
function sessionTitleLockedError(message = "session changed while renaming; retry") {
|
|
810
|
+
return Object.assign(new Error(message), {
|
|
811
|
+
code: ErrorCode.enum.SESSION_LOCKED,
|
|
812
|
+
statusCode: 409,
|
|
813
|
+
retryable: true
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
function sessionTranscriptUnreadableError(message) {
|
|
817
|
+
return Object.assign(new Error(message), {
|
|
818
|
+
code: ErrorCode.enum.SESSION_TRANSCRIPT_UNREADABLE,
|
|
819
|
+
statusCode: 500,
|
|
820
|
+
retryable: false
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
async function repairNativeTerminalNewline(filepath) {
|
|
824
|
+
const snapshot = await readStableFile(filepath);
|
|
825
|
+
if (!snapshot) return false;
|
|
826
|
+
const tail = physicalTail(snapshot.content);
|
|
827
|
+
if (tail.endsWithNewline) return true;
|
|
828
|
+
if (!tail.valid) throw sessionTitleLockedError("native session has an incomplete transcript record");
|
|
829
|
+
await appendFile(filepath, "\n");
|
|
830
|
+
const repaired = await readStableFile(filepath);
|
|
831
|
+
return Boolean(repaired?.content.endsWith("\n"));
|
|
832
|
+
}
|
|
833
|
+
async function appendVerifiedNativeRename(filepath, sessionDir, cwd, title) {
|
|
834
|
+
const staleRenameIds = /* @__PURE__ */ new Set();
|
|
835
|
+
let renameOnlyOrigin;
|
|
836
|
+
for (let attempt = 0; attempt < SESSION_TITLE_MAX_APPEND_ATTEMPTS; attempt += 1) {
|
|
837
|
+
const attemptOrigin = await readStableFile(filepath);
|
|
838
|
+
if (!attemptOrigin) continue;
|
|
839
|
+
if (attempt === 0) renameOnlyOrigin = attemptOrigin;
|
|
840
|
+
const header = parseJsonlPrefixEntries(await readJsonlPrefix(filepath)).find(
|
|
841
|
+
(entry) => entry.type === "session"
|
|
842
|
+
);
|
|
843
|
+
if (header?.version !== CURRENT_SESSION_VERSION) {
|
|
844
|
+
throw sessionTitleLockedError("This native session was created by an unsupported Pi version and cannot be renamed.");
|
|
845
|
+
}
|
|
846
|
+
if (!await repairNativeTerminalNewline(filepath)) continue;
|
|
847
|
+
const committed = await readStableFile(filepath);
|
|
848
|
+
if (!committed) continue;
|
|
849
|
+
if (!physicalTail(committed.content).valid) {
|
|
850
|
+
throw sessionTranscriptUnreadableError("native session contains a malformed transcript record");
|
|
851
|
+
}
|
|
852
|
+
const manager = SessionManager.open(filepath, sessionDir, cwd);
|
|
853
|
+
if (staleRenameIds.size > 0) {
|
|
854
|
+
const concurrentLeaf = latestConcurrentEntryId(manager, staleRenameIds);
|
|
855
|
+
if (!concurrentLeaf) break;
|
|
856
|
+
manager.branch(concurrentLeaf);
|
|
857
|
+
manager.appendCustomEntry(SESSION_TITLE_RETRY_CUSTOM_TYPE, {});
|
|
858
|
+
}
|
|
859
|
+
const beforeAppend = await readStableFile(filepath);
|
|
860
|
+
if (!beforeAppend || !beforeAppend.content.endsWith("\n")) continue;
|
|
861
|
+
const tail = physicalTail(beforeAppend.content);
|
|
862
|
+
const authorityParentId = manager.getLeafId();
|
|
863
|
+
if (!tail.valid || tail.parentId !== authorityParentId) continue;
|
|
864
|
+
const authorityId = manager.appendCustomEntry(
|
|
865
|
+
USER_SESSION_TITLE_CUSTOM_TYPE,
|
|
866
|
+
userSessionTitleData(title)
|
|
867
|
+
);
|
|
868
|
+
const titleId = manager.appendSessionInfo(title);
|
|
869
|
+
const append = {
|
|
870
|
+
authority: { id: authorityId, parentId: authorityParentId },
|
|
871
|
+
title: { id: titleId, parentId: authorityId }
|
|
872
|
+
};
|
|
873
|
+
staleRenameIds.add(authorityId);
|
|
874
|
+
staleRenameIds.add(titleId);
|
|
875
|
+
const verified = await verifyTitleSequence(filepath, append, title);
|
|
876
|
+
if (!verified) continue;
|
|
877
|
+
if (attempt === 0 && verified.tailId === titleId && renameOnlyOrigin && isExactRenameOnlyAppend(renameOnlyOrigin, verified.content, append)) {
|
|
878
|
+
await restoreVerifiedRenameMtime(filepath, renameOnlyOrigin.before, Number(verified.stat.size));
|
|
564
879
|
}
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
throw sessionTitleLockedError();
|
|
883
|
+
}
|
|
884
|
+
function wrapperPredecessor(content) {
|
|
885
|
+
const tail = physicalTail(content);
|
|
886
|
+
if (!tail.valid) {
|
|
887
|
+
if (!tail.endsWithNewline) throw sessionTitleLockedError("session has an incomplete transcript record");
|
|
888
|
+
throw sessionTranscriptUnreadableError("session contains a malformed transcript record");
|
|
889
|
+
}
|
|
890
|
+
return {
|
|
891
|
+
parentId: tail.parentId,
|
|
892
|
+
separator: tail.endsWithNewline ? "" : "\n"
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
async function appendVerifiedWrapperRename(filepath, title) {
|
|
896
|
+
let renameOnlyStart;
|
|
897
|
+
for (let attempt = 0; attempt < SESSION_TITLE_MAX_APPEND_ATTEMPTS; attempt += 1) {
|
|
898
|
+
const snapshot = await readStableFile(filepath);
|
|
899
|
+
if (!snapshot) continue;
|
|
900
|
+
renameOnlyStart ??= snapshot.before;
|
|
901
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
902
|
+
const predecessor = wrapperPredecessor(snapshot.content);
|
|
903
|
+
const pair = createUserSessionTitleEntries({
|
|
904
|
+
title,
|
|
905
|
+
parentId: predecessor.parentId,
|
|
906
|
+
timestamp,
|
|
907
|
+
authorityId: randomUUID(),
|
|
908
|
+
titleId: randomUUID()
|
|
909
|
+
});
|
|
910
|
+
const append = {
|
|
911
|
+
authority: { id: pair.authority.id, parentId: pair.authority.parentId },
|
|
912
|
+
title: { id: pair.title.id, parentId: pair.title.parentId }
|
|
913
|
+
};
|
|
914
|
+
const records = [pair.authority, pair.title].map((entry) => JSON.stringify(entry)).join("\n");
|
|
915
|
+
const payload = `${predecessor.separator}${records}
|
|
916
|
+
`;
|
|
917
|
+
await appendFile(filepath, payload);
|
|
918
|
+
const verified = await verifyTitleSequence(filepath, append, title);
|
|
919
|
+
if (!verified) continue;
|
|
920
|
+
const exactSize = Number(snapshot.after.size) + Buffer.byteLength(payload);
|
|
921
|
+
if (attempt === 0 && Number(verified.stat.size) === exactSize && renameOnlyStart) {
|
|
922
|
+
await restoreVerifiedRenameMtime(filepath, renameOnlyStart, exactSize);
|
|
923
|
+
}
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
throw sessionTitleLockedError();
|
|
927
|
+
}
|
|
928
|
+
function fileTimeSeconds(milliseconds) {
|
|
929
|
+
const seconds = milliseconds / 1e3;
|
|
930
|
+
return Number.isFinite(seconds) ? seconds : void 0;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/server/harness/pi-coding-agent/sessionArchiveIndex.ts
|
|
934
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
935
|
+
import { mkdir, readFile as readFile2, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
936
|
+
import { join } from "node:path";
|
|
937
|
+
var SESSION_ARCHIVE_DIRECTORY_NAME = ".archive";
|
|
938
|
+
var ARCHIVE_MARKER_VERSION = 1;
|
|
939
|
+
var ARCHIVE_MARKER_SUFFIX = ".json";
|
|
940
|
+
function archiveDirectory(sessionDir) {
|
|
941
|
+
return join(sessionDir, SESSION_ARCHIVE_DIRECTORY_NAME);
|
|
942
|
+
}
|
|
943
|
+
function markerFilename(sessionId) {
|
|
944
|
+
return `${Buffer.from(sessionId, "utf8").toString("base64url")}${ARCHIVE_MARKER_SUFFIX}`;
|
|
945
|
+
}
|
|
946
|
+
function markerPath(sessionDir, sessionId) {
|
|
947
|
+
return join(archiveDirectory(sessionDir), markerFilename(sessionId));
|
|
948
|
+
}
|
|
949
|
+
function parseMarker(raw) {
|
|
950
|
+
try {
|
|
951
|
+
const parsed = JSON.parse(raw);
|
|
952
|
+
return parsed.version === ARCHIVE_MARKER_VERSION && typeof parsed.sessionId === "string" && parsed.sessionId.length > 0 && typeof parsed.archivedAt === "string" ? parsed : null;
|
|
953
|
+
} catch {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
async function readArchivedSessionIds(sessionDir) {
|
|
958
|
+
const directory = archiveDirectory(sessionDir);
|
|
959
|
+
const files = await readdir(directory).catch(() => []);
|
|
960
|
+
const ids = /* @__PURE__ */ new Set();
|
|
961
|
+
await Promise.all(files.filter((file) => file.endsWith(ARCHIVE_MARKER_SUFFIX)).map(async (file) => {
|
|
962
|
+
const marker = parseMarker(await readFile2(join(directory, file), "utf8").catch(() => ""));
|
|
963
|
+
if (marker && file === markerFilename(marker.sessionId)) ids.add(marker.sessionId);
|
|
964
|
+
}));
|
|
965
|
+
return ids;
|
|
966
|
+
}
|
|
967
|
+
async function writeSessionArchived(sessionDir, sessionId, archived, now = /* @__PURE__ */ new Date()) {
|
|
968
|
+
const target = markerPath(sessionDir, sessionId);
|
|
969
|
+
if (!archived) {
|
|
970
|
+
try {
|
|
971
|
+
await rm(target);
|
|
972
|
+
return true;
|
|
973
|
+
} catch (error) {
|
|
974
|
+
if (error.code === "ENOENT") return false;
|
|
975
|
+
throw error;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
await mkdir(archiveDirectory(sessionDir), { recursive: true });
|
|
979
|
+
const marker = {
|
|
980
|
+
version: ARCHIVE_MARKER_VERSION,
|
|
981
|
+
sessionId,
|
|
982
|
+
archivedAt: now.toISOString()
|
|
983
|
+
};
|
|
984
|
+
const tmp = `${target}.${randomUUID2()}.tmp`;
|
|
985
|
+
try {
|
|
986
|
+
await writeFile(tmp, `${JSON.stringify(marker)}
|
|
987
|
+
`, { encoding: "utf8", flag: "wx" });
|
|
988
|
+
await rename(tmp, target);
|
|
989
|
+
return true;
|
|
990
|
+
} catch (error) {
|
|
991
|
+
await rm(tmp, { force: true }).catch(() => {
|
|
992
|
+
});
|
|
993
|
+
throw error;
|
|
565
994
|
}
|
|
566
|
-
|
|
995
|
+
}
|
|
996
|
+
async function forgetArchivedSession(sessionDir, sessionId) {
|
|
997
|
+
await rm(markerPath(sessionDir, sessionId), { force: true });
|
|
567
998
|
}
|
|
568
999
|
|
|
569
1000
|
// src/server/harness/pi-coding-agent/sessions.ts
|
|
@@ -593,31 +1024,37 @@ function sessionBaseDir(explicitRoot) {
|
|
|
593
1024
|
const explicit = explicitRoot?.trim();
|
|
594
1025
|
if (explicit) return resolve(explicit);
|
|
595
1026
|
const configured = getEnv(SESSION_ROOT_ENV)?.trim();
|
|
596
|
-
return configured ? resolve(configured) :
|
|
1027
|
+
return configured ? resolve(configured) : join2(homedir(), ".pi", "agent", "sessions");
|
|
597
1028
|
}
|
|
598
1029
|
function defaultSessionDir(cwd, explicitRoot) {
|
|
599
1030
|
if (explicitRoot && cwd.trim().length === 0) return sessionBaseDir(explicitRoot);
|
|
600
1031
|
const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
601
|
-
return
|
|
1032
|
+
return join2(sessionBaseDir(explicitRoot), safePath);
|
|
602
1033
|
}
|
|
603
1034
|
var SAFE_SESSION_NAMESPACE = /^[a-zA-Z0-9_-]+$/;
|
|
604
1035
|
var SESSION_ROOT_ENV = "BORING_AGENT_SESSION_ROOT";
|
|
605
1036
|
var SUMMARY_PREFIX_BYTES2 = 64 * 1024;
|
|
1037
|
+
var MAX_CONCURRENT_SESSION_SUMMARIES = 4;
|
|
606
1038
|
var DEFAULT_LEGACY_WORKSPACE_ID = "default";
|
|
607
1039
|
var TRUSTED_LOCAL_USER_ID = "local";
|
|
1040
|
+
function withArchivedFlag(summary, archivedIds) {
|
|
1041
|
+
return archivedIds.has(summary.id) ? { ...summary, archived: true } : summary;
|
|
1042
|
+
}
|
|
608
1043
|
function sessionDirForNamespace(namespace, explicitRoot) {
|
|
609
1044
|
const safeNamespace = namespace.trim();
|
|
610
1045
|
if (!SAFE_SESSION_NAMESPACE.test(safeNamespace)) {
|
|
611
1046
|
throw new Error("session namespace must contain only letters, numbers, underscores, and dashes");
|
|
612
1047
|
}
|
|
613
|
-
return
|
|
1048
|
+
return join2(sessionBaseDir(explicitRoot), safeNamespace);
|
|
614
1049
|
}
|
|
615
1050
|
function normalizeListOptions(options) {
|
|
616
1051
|
return {
|
|
617
1052
|
limit: options?.limit === void 0 ? void 0 : Math.max(0, options.limit),
|
|
618
1053
|
offset: Math.max(0, options?.offset ?? 0),
|
|
619
1054
|
includeId: options?.includeId,
|
|
620
|
-
includeEmpty: options?.includeEmpty === true
|
|
1055
|
+
includeEmpty: options?.includeEmpty === true,
|
|
1056
|
+
archived: options?.archived ?? "all",
|
|
1057
|
+
after: options?.after
|
|
621
1058
|
};
|
|
622
1059
|
}
|
|
623
1060
|
var PiSessionStore = class {
|
|
@@ -663,7 +1100,9 @@ var PiSessionStore = class {
|
|
|
663
1100
|
normalizedOptions.limit ?? null,
|
|
664
1101
|
normalizedOptions.offset,
|
|
665
1102
|
normalizedOptions.includeId ?? null,
|
|
666
|
-
normalizedOptions.includeEmpty
|
|
1103
|
+
normalizedOptions.includeEmpty,
|
|
1104
|
+
normalizedOptions.archived,
|
|
1105
|
+
normalizedOptions.after ?? null
|
|
667
1106
|
]);
|
|
668
1107
|
const inFlight = this.listInFlight.get(inFlightKey);
|
|
669
1108
|
if (inFlight) return inFlight;
|
|
@@ -676,9 +1115,9 @@ var PiSessionStore = class {
|
|
|
676
1115
|
}
|
|
677
1116
|
}
|
|
678
1117
|
async listUncached(ctx, options) {
|
|
679
|
-
const files = await
|
|
1118
|
+
const files = await readdir2(this.sessionDir).catch(() => []);
|
|
680
1119
|
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
|
|
681
|
-
const filepaths = jsonlFiles.map((f) =>
|
|
1120
|
+
const filepaths = jsonlFiles.map((f) => join2(this.sessionDir, f));
|
|
682
1121
|
const fileStats = await Promise.all(filepaths.map(async (filepath) => {
|
|
683
1122
|
try {
|
|
684
1123
|
return { filepath, stat: await fsStat2(filepath) };
|
|
@@ -689,23 +1128,35 @@ var PiSessionStore = class {
|
|
|
689
1128
|
const existingFiles = fileStats.filter((item) => item !== null);
|
|
690
1129
|
const referencedPiFiles = await this.referencedPiFiles(existingFiles);
|
|
691
1130
|
const visibleFiles = await Promise.all(existingFiles.filter(({ filepath }) => !referencedPiFiles.has(resolve(filepath))).map(async (file) => ({ ...file, ...await this.sessionSortKey(file) })));
|
|
692
|
-
visibleFiles.sort((a, b) =>
|
|
1131
|
+
visibleFiles.sort((a, b) => compareSessionOrder(
|
|
1132
|
+
[a.orderingUpdatedAtMs, "", a.sortId],
|
|
1133
|
+
[b.orderingUpdatedAtMs, "", b.sortId]
|
|
1134
|
+
));
|
|
693
1135
|
const { offset, limit } = options;
|
|
694
1136
|
const includeId = options.includeId;
|
|
695
|
-
const
|
|
1137
|
+
const archivedIds = await readArchivedSessionIds(this.sessionDir);
|
|
1138
|
+
const after = options.after;
|
|
1139
|
+
const afterFiles = after ? visibleFiles.filter((file) => compareSessionOrder(
|
|
1140
|
+
[file.orderingUpdatedAtMs, after.agentTypeId, file.sortId],
|
|
1141
|
+
after.position
|
|
1142
|
+
) > 0) : visibleFiles;
|
|
1143
|
+
const filteredFiles = options.archived === "all" ? afterFiles : afterFiles.filter((file) => archivedIds.has(file.sortId) === (options.archived === "archived"));
|
|
1144
|
+
const pageSummaries = await this.summarizeVisiblePage(filteredFiles, {
|
|
696
1145
|
ctx,
|
|
697
1146
|
offset,
|
|
698
1147
|
limit,
|
|
699
1148
|
includeId,
|
|
700
|
-
includeEmpty: options.includeEmpty
|
|
1149
|
+
includeEmpty: options.includeEmpty,
|
|
1150
|
+
archivedIds
|
|
701
1151
|
});
|
|
702
1152
|
if (!includeId || pageSummaries.some((summary) => summary.id === includeId)) return pageSummaries;
|
|
703
1153
|
const includeSummary = await this.summarizeIncludedSession(ctx, includeId, referencedPiFiles);
|
|
704
|
-
return includeSummary ? [...pageSummaries, includeSummary] : pageSummaries;
|
|
1154
|
+
return includeSummary ? [...pageSummaries, withArchivedFlag(includeSummary, archivedIds)] : pageSummaries;
|
|
705
1155
|
}
|
|
706
1156
|
async create(ctx, init) {
|
|
707
|
-
|
|
708
|
-
|
|
1157
|
+
const manualTitle = init?.title !== void 0 ? normalizeUserSessionTitle(init.title) : void 0;
|
|
1158
|
+
await mkdir2(this.sessionDir, { recursive: true });
|
|
1159
|
+
const id = randomUUID3();
|
|
709
1160
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
710
1161
|
const header = {
|
|
711
1162
|
type: "session",
|
|
@@ -716,21 +1167,21 @@ var PiSessionStore = class {
|
|
|
716
1167
|
boringSessionCtx: normalizeSessionCtx(ctx) ?? {}
|
|
717
1168
|
};
|
|
718
1169
|
const lines = [JSON.stringify(header)];
|
|
719
|
-
if (
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
id: randomUUID(),
|
|
1170
|
+
if (manualTitle) {
|
|
1171
|
+
const pair = createUserSessionTitleEntries({
|
|
1172
|
+
title: manualTitle,
|
|
723
1173
|
parentId: null,
|
|
724
1174
|
timestamp: now,
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1175
|
+
authorityId: randomUUID3(),
|
|
1176
|
+
titleId: randomUUID3()
|
|
1177
|
+
});
|
|
1178
|
+
lines.push(JSON.stringify(pair.authority), JSON.stringify(pair.title));
|
|
728
1179
|
}
|
|
729
|
-
const filepath =
|
|
730
|
-
await
|
|
1180
|
+
const filepath = join2(this.sessionDir, `${nativeSessionFilename(id, now)}`);
|
|
1181
|
+
await writeFile2(filepath, lines.join("\n") + "\n", "utf-8");
|
|
731
1182
|
return {
|
|
732
1183
|
id,
|
|
733
|
-
title:
|
|
1184
|
+
title: manualTitle ?? "New session",
|
|
734
1185
|
createdAt: now,
|
|
735
1186
|
updatedAt: now,
|
|
736
1187
|
turnCount: 0,
|
|
@@ -738,23 +1189,39 @@ var PiSessionStore = class {
|
|
|
738
1189
|
hasAssistantReply: false
|
|
739
1190
|
};
|
|
740
1191
|
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Visibility only. Resolve tenancy and build the receipt through the bounded
|
|
1194
|
+
* listing projection; never call `load`, whose legacy compatibility path may
|
|
1195
|
+
* compact ui_snapshot entries and therefore mutate transcript bytes/mtime.
|
|
1196
|
+
*/
|
|
1197
|
+
async setArchived(ctx, sessionId, archived) {
|
|
1198
|
+
const filepath = await this.resolveSessionFile(sessionId, ctx);
|
|
1199
|
+
const summary = await this.summarizeFile(ctx, filepath);
|
|
1200
|
+
if (!summary) throw new Error(`Session not found: ${sessionId}`);
|
|
1201
|
+
await writeSessionArchived(this.sessionDir, summary.id, archived);
|
|
1202
|
+
try {
|
|
1203
|
+
await this.resolveSessionFile(summary.id, ctx);
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
await forgetArchivedSession(this.sessionDir, summary.id);
|
|
1206
|
+
throw error;
|
|
1207
|
+
}
|
|
1208
|
+
return archived ? { ...summary, archived: true } : summary;
|
|
1209
|
+
}
|
|
741
1210
|
async load(ctx, sessionId) {
|
|
742
1211
|
const resolved = await this.resolveSessionTranscript(ctx, sessionId);
|
|
743
|
-
const
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
const updatedAtMs = nativeSummary?.latestMessageAtMs ?? Math.max(resolved.fileStat.mtime.getTime(), resolved.linkedMtimeMs ?? 0);
|
|
747
|
-
return {
|
|
1212
|
+
const title = projectedTranscriptTitle(resolved.sessionSummary, resolved.linkedSummary) ?? "New session";
|
|
1213
|
+
const updatedAtMs = resolved.transcriptSummary.latestMessageAtMs ?? Math.max(resolved.fileStat.mtime.getTime(), resolved.linkedMtimeMs ?? 0);
|
|
1214
|
+
return withArchivedFlag({
|
|
748
1215
|
id: resolved.resolvedSessionId,
|
|
749
1216
|
title,
|
|
750
1217
|
createdAt: resolved.header?.timestamp ?? resolved.fileStat.birthtime.toISOString(),
|
|
751
1218
|
updatedAt: new Date(updatedAtMs).toISOString(),
|
|
752
|
-
turnCount,
|
|
1219
|
+
turnCount: resolved.transcriptSummary.turnCount,
|
|
753
1220
|
...resolved.directNative ? {
|
|
754
1221
|
nativeSessionId: resolved.resolvedSessionId,
|
|
755
|
-
hasAssistantReply:
|
|
1222
|
+
hasAssistantReply: resolved.transcriptSummary.hasAssistantReply
|
|
756
1223
|
} : {}
|
|
757
|
-
};
|
|
1224
|
+
}, await readArchivedSessionIds(this.sessionDir));
|
|
758
1225
|
}
|
|
759
1226
|
/**
|
|
760
1227
|
* Returns the persisted pi message objects in file order so callers can run
|
|
@@ -787,11 +1254,12 @@ var PiSessionStore = class {
|
|
|
787
1254
|
const filepath = await this.resolveSessionFile(sessionId, ctx);
|
|
788
1255
|
let content;
|
|
789
1256
|
try {
|
|
790
|
-
content = await
|
|
1257
|
+
content = await readFile3(filepath, "utf-8");
|
|
791
1258
|
} catch {
|
|
792
1259
|
throw new Error(`Session not found: ${sessionId}`);
|
|
793
1260
|
}
|
|
794
|
-
const
|
|
1261
|
+
const parsedFile = parseSessionTranscript(content);
|
|
1262
|
+
const fileEntries = parsedFile.entries;
|
|
795
1263
|
const header = fileEntries.find(
|
|
796
1264
|
(e) => e.type === "session"
|
|
797
1265
|
);
|
|
@@ -799,17 +1267,6 @@ var PiSessionStore = class {
|
|
|
799
1267
|
filepath,
|
|
800
1268
|
header?.id ?? sessionId
|
|
801
1269
|
);
|
|
802
|
-
if (!timestampNamedNative && fileEntries.some((e) => e.type === "ui_snapshot")) {
|
|
803
|
-
const compacted = fileEntries.filter((e) => e.type !== "ui_snapshot").map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
804
|
-
const tmp = `${filepath}.compact-${randomUUID()}`;
|
|
805
|
-
try {
|
|
806
|
-
await writeFile(tmp, compacted, "utf-8");
|
|
807
|
-
await rename(tmp, filepath);
|
|
808
|
-
} catch {
|
|
809
|
-
await rm(tmp, { force: true }).catch(() => {
|
|
810
|
-
});
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
1270
|
if (!this.headerBelongsToCtx(header, ctx, timestampNamedNative)) {
|
|
814
1271
|
throw new Error(`Session not found: ${sessionId}`);
|
|
815
1272
|
}
|
|
@@ -823,17 +1280,18 @@ var PiSessionStore = class {
|
|
|
823
1280
|
const linkedEntries = linked?.entries.filter(
|
|
824
1281
|
(e) => e.type !== "session"
|
|
825
1282
|
) ?? [];
|
|
826
|
-
const
|
|
1283
|
+
const transcriptSummary = selectTranscriptSummary(parsedFile.summary, linked?.summary);
|
|
1284
|
+
const transcriptEntries = transcriptSummary === linked?.summary ? linkedEntries : sessionEntries;
|
|
827
1285
|
return {
|
|
828
1286
|
resolvedSessionId: header?.id ?? sessionId,
|
|
829
1287
|
header,
|
|
830
|
-
sessionEntries,
|
|
831
|
-
linkedEntries,
|
|
832
1288
|
transcriptEntries,
|
|
1289
|
+
sessionSummary: parsedFile.summary,
|
|
1290
|
+
...linked ? { linkedSummary: linked.summary } : {},
|
|
1291
|
+
transcriptSummary,
|
|
833
1292
|
fileStat,
|
|
834
|
-
linkedMtimeMs: linked?.
|
|
1293
|
+
linkedMtimeMs: linked?.mtimeMs,
|
|
835
1294
|
filepath,
|
|
836
|
-
...linkedPiFile && linked ? { linkedFilepath: linkedPiFile } : {},
|
|
837
1295
|
directNative
|
|
838
1296
|
};
|
|
839
1297
|
}
|
|
@@ -842,8 +1300,7 @@ var PiSessionStore = class {
|
|
|
842
1300
|
* appended through the same JSONL path under one process-local writer lock.
|
|
843
1301
|
*/
|
|
844
1302
|
async rename(ctx, sessionId, title) {
|
|
845
|
-
const trimmed = title
|
|
846
|
-
if (!trimmed) throw new Error("Session title must not be empty");
|
|
1303
|
+
const trimmed = normalizeUserSessionTitle(title);
|
|
847
1304
|
return await this.withWriter(sessionId, async () => {
|
|
848
1305
|
const resolved = await this.resolveSessionTranscript(ctx, sessionId);
|
|
849
1306
|
if (resolved.directNative) {
|
|
@@ -859,20 +1316,8 @@ var PiSessionStore = class {
|
|
|
859
1316
|
}
|
|
860
1317
|
return await this.load(ctx, sessionId);
|
|
861
1318
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
id: randomUUID(),
|
|
865
|
-
parentId: null,
|
|
866
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
867
|
-
name: trimmed
|
|
868
|
-
};
|
|
869
|
-
const line = `${JSON.stringify(entry)}
|
|
870
|
-
`;
|
|
871
|
-
const targets = [resolved.filepath, resolved.linkedFilepath].filter((path) => Boolean(path));
|
|
872
|
-
for (const filepath of targets) {
|
|
873
|
-
await appendFile(filepath, line);
|
|
874
|
-
this.prefixCache.delete(filepath);
|
|
875
|
-
}
|
|
1319
|
+
await appendVerifiedWrapperRename(resolved.filepath, trimmed);
|
|
1320
|
+
this.prefixCache.delete(resolved.filepath);
|
|
876
1321
|
return await this.load(ctx, sessionId);
|
|
877
1322
|
});
|
|
878
1323
|
}
|
|
@@ -898,7 +1343,7 @@ var PiSessionStore = class {
|
|
|
898
1343
|
loadPiSessionFileSync(ctx, sessionId) {
|
|
899
1344
|
if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) return null;
|
|
900
1345
|
try {
|
|
901
|
-
const direct =
|
|
1346
|
+
const direct = join2(this.sessionDir, `${sessionId}.jsonl`);
|
|
902
1347
|
let filepath = direct;
|
|
903
1348
|
let content;
|
|
904
1349
|
try {
|
|
@@ -908,7 +1353,7 @@ var PiSessionStore = class {
|
|
|
908
1353
|
(f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
|
|
909
1354
|
);
|
|
910
1355
|
if (files.length === 0) return null;
|
|
911
|
-
filepath =
|
|
1356
|
+
filepath = join2(this.sessionDir, files[0]);
|
|
912
1357
|
content = readFileSync(filepath, "utf-8");
|
|
913
1358
|
}
|
|
914
1359
|
const entries = safeParseEntries(content);
|
|
@@ -938,19 +1383,19 @@ var PiSessionStore = class {
|
|
|
938
1383
|
async loadPiSessionFile(ctx, sessionId) {
|
|
939
1384
|
if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) return null;
|
|
940
1385
|
try {
|
|
941
|
-
const direct =
|
|
1386
|
+
const direct = join2(this.sessionDir, `${sessionId}.jsonl`);
|
|
942
1387
|
let filepath = direct;
|
|
943
1388
|
let content;
|
|
944
1389
|
try {
|
|
945
|
-
content = await
|
|
1390
|
+
content = await readFile3(direct, "utf-8");
|
|
946
1391
|
} catch {
|
|
947
|
-
const files = await
|
|
1392
|
+
const files = await readdir2(this.sessionDir).catch(() => []);
|
|
948
1393
|
const match = files.find(
|
|
949
1394
|
(f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
|
|
950
1395
|
);
|
|
951
1396
|
if (!match) return null;
|
|
952
|
-
filepath =
|
|
953
|
-
content = await
|
|
1397
|
+
filepath = join2(this.sessionDir, match);
|
|
1398
|
+
content = await readFile3(filepath, "utf-8");
|
|
954
1399
|
}
|
|
955
1400
|
const entries = safeParseEntries(content);
|
|
956
1401
|
const header = entries.find((entry) => entry.type === "session");
|
|
@@ -984,7 +1429,7 @@ var PiSessionStore = class {
|
|
|
984
1429
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
985
1430
|
path: piFilePath
|
|
986
1431
|
});
|
|
987
|
-
await
|
|
1432
|
+
await appendFile2(filepath, entry + "\n");
|
|
988
1433
|
}
|
|
989
1434
|
async delete(ctx, sessionId) {
|
|
990
1435
|
const filepath = await this.resolveSessionFile(sessionId, ctx).catch(
|
|
@@ -994,30 +1439,31 @@ var PiSessionStore = class {
|
|
|
994
1439
|
const fileSessionId = await this.readSessionFileId(filepath);
|
|
995
1440
|
if (fileSessionId && fileSessionId !== sessionId) return;
|
|
996
1441
|
const linkedPiFile = await this.linkedPiFileFor(filepath);
|
|
997
|
-
await
|
|
1442
|
+
await rm2(filepath, { force: true });
|
|
998
1443
|
this.prefixCache.delete(filepath);
|
|
999
1444
|
if (linkedPiFile && resolve(linkedPiFile) !== resolve(filepath)) {
|
|
1000
|
-
await
|
|
1445
|
+
await rm2(linkedPiFile, { force: true });
|
|
1001
1446
|
this.prefixCache.delete(linkedPiFile);
|
|
1002
1447
|
}
|
|
1448
|
+
await forgetArchivedSession(this.sessionDir, fileSessionId ?? sessionId);
|
|
1003
1449
|
}
|
|
1004
1450
|
async resolveSessionFile(sessionId, ctx) {
|
|
1005
1451
|
if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) {
|
|
1006
1452
|
throw new Error(`Session not found: ${sessionId}`);
|
|
1007
1453
|
}
|
|
1008
|
-
const direct =
|
|
1454
|
+
const direct = join2(this.sessionDir, `${sessionId}.jsonl`);
|
|
1009
1455
|
try {
|
|
1010
1456
|
await fsStat2(direct);
|
|
1011
1457
|
if (ctx) await this.assertFileBelongsToCtx(direct, ctx, sessionId);
|
|
1012
1458
|
return direct;
|
|
1013
1459
|
} catch {
|
|
1014
1460
|
}
|
|
1015
|
-
const files = await
|
|
1461
|
+
const files = await readdir2(this.sessionDir).catch(() => []);
|
|
1016
1462
|
const match = files.find(
|
|
1017
1463
|
(f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
|
|
1018
1464
|
);
|
|
1019
1465
|
if (!match) throw new Error(`Session not found: ${sessionId}`);
|
|
1020
|
-
const matchedPath =
|
|
1466
|
+
const matchedPath = join2(this.sessionDir, match);
|
|
1021
1467
|
if (!isTimestampNamedPiSessionFile(matchedPath, sessionId)) {
|
|
1022
1468
|
if (ctx) await this.assertFileBelongsToCtx(matchedPath, ctx, sessionId);
|
|
1023
1469
|
return matchedPath;
|
|
@@ -1047,7 +1493,7 @@ var PiSessionStore = class {
|
|
|
1047
1493
|
}
|
|
1048
1494
|
async linkedPiFileFor(filepath) {
|
|
1049
1495
|
try {
|
|
1050
|
-
const content = await
|
|
1496
|
+
const content = await readFile3(filepath, "utf-8");
|
|
1051
1497
|
return extractPiSessionFilePath(safeParseEntries(content));
|
|
1052
1498
|
} catch {
|
|
1053
1499
|
return null;
|
|
@@ -1067,35 +1513,51 @@ var PiSessionStore = class {
|
|
|
1067
1513
|
return referenced;
|
|
1068
1514
|
}
|
|
1069
1515
|
async sessionSortKey({ filepath, stat }) {
|
|
1070
|
-
let
|
|
1071
|
-
let headerId
|
|
1516
|
+
let fileMtimeMs = Number(stat.mtimeMs);
|
|
1517
|
+
let headerId;
|
|
1072
1518
|
const stemFallback = () => ({
|
|
1073
|
-
|
|
1519
|
+
orderingUpdatedAtMs: fileMtimeMs,
|
|
1074
1520
|
sortId: wrapperSessionId(filepath)
|
|
1075
1521
|
});
|
|
1076
1522
|
try {
|
|
1077
1523
|
const cached = await this.readPrefixCache(filepath, stat);
|
|
1524
|
+
fileMtimeMs = cached.mtimeMs;
|
|
1078
1525
|
headerId = cached.headerId ?? void 0;
|
|
1526
|
+
if (!headerId) return stemFallback();
|
|
1079
1527
|
const linkedPiFile = cached.referencedPiFile;
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
if (!isTimestampNamedPiSessionFile(filepath, headerId)) {
|
|
1088
|
-
return { sortMtimeMs, sortId: headerId };
|
|
1528
|
+
const hasLinkedTranscript = Boolean(
|
|
1529
|
+
linkedPiFile && resolve(linkedPiFile) !== resolve(filepath)
|
|
1530
|
+
);
|
|
1531
|
+
const linkedStat = hasLinkedTranscript ? await fsStat2(linkedPiFile).catch(() => void 0) : void 0;
|
|
1532
|
+
const linkedSnapshotMatches = linkedStat ? cached.linkedDev === Number(linkedStat.dev) && cached.linkedIno === Number(linkedStat.ino) && cached.linkedMtimeMs === Number(linkedStat.mtimeMs) && cached.linkedCtimeMs === Number(linkedStat.ctimeMs) && cached.linkedSize === Number(linkedStat.size) : cached.linkedMtimeMs === void 0 && cached.linkedSize === void 0;
|
|
1533
|
+
if (cached.orderingUpdatedAtMs !== void 0 && (!hasLinkedTranscript || linkedSnapshotMatches)) {
|
|
1534
|
+
return { orderingUpdatedAtMs: cached.orderingUpdatedAtMs, sortId: headerId };
|
|
1089
1535
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1536
|
+
let selectedOrdering;
|
|
1537
|
+
if (linkedPiFile && linkedStat) {
|
|
1538
|
+
const linkedOrdering = await nativeTranscriptOrdering(
|
|
1539
|
+
linkedPiFile,
|
|
1540
|
+
Number(linkedStat.size)
|
|
1541
|
+
).catch(() => void 0);
|
|
1542
|
+
if (linkedOrdering?.hasEntries) selectedOrdering = linkedOrdering;
|
|
1092
1543
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1544
|
+
selectedOrdering ??= await nativeTranscriptOrdering(filepath, cached.size).catch(() => ({ hasEntries: false }));
|
|
1545
|
+
const orderingUpdatedAtMs = selectedOrdering.latestMessageAtMs ?? Math.max(fileMtimeMs, linkedStat ? Number(linkedStat.mtimeMs) : 0);
|
|
1546
|
+
const { summary: _staleSummary, ...cacheWithoutSummary } = cached;
|
|
1547
|
+
this.prefixCache.set(filepath, {
|
|
1548
|
+
...cacheWithoutSummary,
|
|
1549
|
+
orderingUpdatedAtMs,
|
|
1550
|
+
...linkedStat ? {
|
|
1551
|
+
linkedDev: Number(linkedStat.dev),
|
|
1552
|
+
linkedIno: Number(linkedStat.ino),
|
|
1553
|
+
linkedMtimeMs: Number(linkedStat.mtimeMs),
|
|
1554
|
+
linkedCtimeMs: Number(linkedStat.ctimeMs),
|
|
1555
|
+
linkedSize: Number(linkedStat.size)
|
|
1556
|
+
} : {}
|
|
1557
|
+
});
|
|
1558
|
+
return { orderingUpdatedAtMs, sortId: headerId };
|
|
1097
1559
|
} catch {
|
|
1098
|
-
return headerId ? {
|
|
1560
|
+
return headerId ? { orderingUpdatedAtMs: fileMtimeMs, sortId: headerId } : stemFallback();
|
|
1099
1561
|
}
|
|
1100
1562
|
}
|
|
1101
1563
|
async summarizeFile(ctx, filepath, existingStat) {
|
|
@@ -1116,43 +1578,51 @@ var PiSessionStore = class {
|
|
|
1116
1578
|
const directNative = isTimestampNamedPiSessionFile(filepath, header.id);
|
|
1117
1579
|
if (directNative ? !this.nativeFileBelongsToCtx(header, ctx) : !this.storedCtxBelongsToCtx(sessionCtx, ctx)) return null;
|
|
1118
1580
|
const entries = parseJsonlPrefixEntries2(content);
|
|
1119
|
-
const sessionEntries = entries.filter(
|
|
1120
|
-
(e) => e.type !== "session"
|
|
1121
|
-
);
|
|
1122
1581
|
const linkedPiFile = extractPiSessionFilePath(entries);
|
|
1123
|
-
const linked =
|
|
1124
|
-
|
|
1125
|
-
(
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1131
|
-
)
|
|
1132
|
-
const updatedAtMs = nativeSummary?.latestMessageAtMs ?? Math.max(fileStat.mtime.getTime(), linked?.mtime.getTime() ?? 0);
|
|
1582
|
+
const [sessionScan, linked, order] = await Promise.all([
|
|
1583
|
+
this.readTranscriptSummary(filepath),
|
|
1584
|
+
linkedPiFile && resolve(linkedPiFile) !== resolve(filepath) ? this.readLinkedPiSessionSummary(linkedPiFile) : null,
|
|
1585
|
+
this.sessionSortKey({ filepath, stat: fileStat })
|
|
1586
|
+
]);
|
|
1587
|
+
if (!sessionScan) return null;
|
|
1588
|
+
const sessionTranscript = sessionScan.summary;
|
|
1589
|
+
const transcript = selectTranscriptSummary(sessionTranscript, linked?.summary);
|
|
1590
|
+
const title = projectedTranscriptTitle(sessionTranscript, linked?.summary) ?? "New session";
|
|
1133
1591
|
const summary = {
|
|
1134
1592
|
id: header.id,
|
|
1135
1593
|
title,
|
|
1136
1594
|
createdAt: header.timestamp,
|
|
1137
|
-
updatedAt: new Date(
|
|
1138
|
-
turnCount,
|
|
1139
|
-
...
|
|
1595
|
+
updatedAt: new Date(order.orderingUpdatedAtMs).toISOString(),
|
|
1596
|
+
turnCount: transcript.turnCount,
|
|
1597
|
+
...directNative ? {
|
|
1140
1598
|
nativeSessionId: header.id,
|
|
1141
|
-
hasAssistantReply:
|
|
1599
|
+
hasAssistantReply: transcript.hasAssistantReply
|
|
1142
1600
|
} : {}
|
|
1143
1601
|
};
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1602
|
+
const stableForCache = sessionScan.stable && sameFileSnapshot(fileStat, sessionScan.stat) && (!linked || linked.stable);
|
|
1603
|
+
if (stableForCache) {
|
|
1604
|
+
this.prefixCache.set(filepath, {
|
|
1605
|
+
dev: Number(sessionScan.stat.dev),
|
|
1606
|
+
ino: Number(sessionScan.stat.ino),
|
|
1607
|
+
mtimeMs: Number(sessionScan.stat.mtimeMs),
|
|
1608
|
+
ctimeMs: Number(sessionScan.stat.ctimeMs),
|
|
1609
|
+
size: Number(sessionScan.stat.size),
|
|
1610
|
+
referencedPiFile: linkedPiFile,
|
|
1611
|
+
// Preserve the exact ordering timestamp emitted above while
|
|
1612
|
+
// retaining snapshot identity required by title-authority caching.
|
|
1613
|
+
headerId: header.id,
|
|
1614
|
+
orderingUpdatedAtMs: order.orderingUpdatedAtMs,
|
|
1615
|
+
sessionCtx,
|
|
1616
|
+
...linked ? {
|
|
1617
|
+
linkedDev: Number(linked.dev),
|
|
1618
|
+
linkedIno: Number(linked.ino),
|
|
1619
|
+
linkedMtimeMs: linked.mtimeMs,
|
|
1620
|
+
linkedCtimeMs: linked.ctimeMs,
|
|
1621
|
+
linkedSize: linked.size
|
|
1622
|
+
} : {},
|
|
1623
|
+
summary
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1156
1626
|
return summary;
|
|
1157
1627
|
} catch {
|
|
1158
1628
|
return null;
|
|
@@ -1161,7 +1631,7 @@ var PiSessionStore = class {
|
|
|
1161
1631
|
cachedPrefix(filepath, fileStat) {
|
|
1162
1632
|
const cached = this.prefixCache.get(filepath);
|
|
1163
1633
|
if (!cached) return void 0;
|
|
1164
|
-
if (cached.mtimeMs !== fileStat.
|
|
1634
|
+
if (cached.dev !== Number(fileStat.dev) || cached.ino !== Number(fileStat.ino) || cached.mtimeMs !== Number(fileStat.mtimeMs) || cached.ctimeMs !== Number(fileStat.ctimeMs) || cached.size !== Number(fileStat.size)) return void 0;
|
|
1165
1635
|
return cached;
|
|
1166
1636
|
}
|
|
1167
1637
|
async cachedSummaryIsFresh(filepath, cached) {
|
|
@@ -1169,7 +1639,7 @@ var PiSessionStore = class {
|
|
|
1169
1639
|
if (!linkedPiFile || resolve(linkedPiFile) === resolve(filepath)) return true;
|
|
1170
1640
|
try {
|
|
1171
1641
|
const linkedStat = await fsStat2(linkedPiFile);
|
|
1172
|
-
return cached.linkedMtimeMs === linkedStat.
|
|
1642
|
+
return cached.linkedDev === Number(linkedStat.dev) && cached.linkedIno === Number(linkedStat.ino) && cached.linkedMtimeMs === Number(linkedStat.mtimeMs) && cached.linkedCtimeMs === Number(linkedStat.ctimeMs) && cached.linkedSize === Number(linkedStat.size);
|
|
1173
1643
|
} catch {
|
|
1174
1644
|
return cached.linkedMtimeMs === void 0 && cached.linkedSize === void 0;
|
|
1175
1645
|
}
|
|
@@ -1177,17 +1647,29 @@ var PiSessionStore = class {
|
|
|
1177
1647
|
async readPrefixCache(filepath, fileStat) {
|
|
1178
1648
|
const cached = this.cachedPrefix(filepath, fileStat);
|
|
1179
1649
|
if (cached) return cached;
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1650
|
+
let before = fileStat;
|
|
1651
|
+
let entry;
|
|
1652
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1653
|
+
const content = await readJsonlPrefix2(filepath);
|
|
1654
|
+
const after = await fsStat2(filepath);
|
|
1655
|
+
const entries = parseJsonlPrefixEntries2(content);
|
|
1656
|
+
const header = entries.find((item) => item.type === "session");
|
|
1657
|
+
entry = {
|
|
1658
|
+
dev: Number(after.dev),
|
|
1659
|
+
ino: Number(after.ino),
|
|
1660
|
+
mtimeMs: Number(after.mtimeMs),
|
|
1661
|
+
ctimeMs: Number(after.ctimeMs),
|
|
1662
|
+
size: Number(after.size),
|
|
1663
|
+
referencedPiFile: extractPiSessionFilePath(entries),
|
|
1664
|
+
headerId: header?.id ?? null,
|
|
1665
|
+
sessionCtx: readHeaderSessionCtx(header)
|
|
1666
|
+
};
|
|
1667
|
+
if (sameFileSnapshot(before, after)) {
|
|
1668
|
+
this.prefixCache.set(filepath, entry);
|
|
1669
|
+
return entry;
|
|
1670
|
+
}
|
|
1671
|
+
before = after;
|
|
1672
|
+
}
|
|
1191
1673
|
return entry;
|
|
1192
1674
|
}
|
|
1193
1675
|
async summarizeVisiblePage(visibleFiles, options) {
|
|
@@ -1195,15 +1677,19 @@ var PiSessionStore = class {
|
|
|
1195
1677
|
const page = [];
|
|
1196
1678
|
let validSeen = 0;
|
|
1197
1679
|
let index = 0;
|
|
1198
|
-
const batchSize =
|
|
1680
|
+
const batchSize = Math.min(
|
|
1681
|
+
MAX_CONCURRENT_SESSION_SUMMARIES,
|
|
1682
|
+
options.limit === void 0 ? Math.max(1, visibleFiles.length) : Math.max(1, options.limit)
|
|
1683
|
+
);
|
|
1199
1684
|
while (index < visibleFiles.length && (options.limit === void 0 || page.length < options.limit)) {
|
|
1200
1685
|
const batch = visibleFiles.slice(index, index + batchSize);
|
|
1201
1686
|
index += batch.length;
|
|
1202
1687
|
const summaries = await Promise.all(
|
|
1203
1688
|
batch.map(({ filepath, stat }) => this.summarizeFile(options.ctx, filepath, stat))
|
|
1204
1689
|
);
|
|
1205
|
-
for (const
|
|
1206
|
-
if (!
|
|
1690
|
+
for (const rawSummary of summaries) {
|
|
1691
|
+
if (!rawSummary) continue;
|
|
1692
|
+
const summary = withArchivedFlag(rawSummary, options.archivedIds);
|
|
1207
1693
|
if (summary.turnCount === 0 && !options.includeEmpty && summary.id !== options.includeId) {
|
|
1208
1694
|
continue;
|
|
1209
1695
|
}
|
|
@@ -1232,7 +1718,7 @@ var PiSessionStore = class {
|
|
|
1232
1718
|
try {
|
|
1233
1719
|
const files = readdirSync(this.sessionDir).filter((file) => file.endsWith(".jsonl"));
|
|
1234
1720
|
for (const file of files) {
|
|
1235
|
-
const filepath =
|
|
1721
|
+
const filepath = join2(this.sessionDir, file);
|
|
1236
1722
|
if (resolve(filepath) === resolvedNativePath) continue;
|
|
1237
1723
|
try {
|
|
1238
1724
|
const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries2(readJsonlPrefixSync(filepath)));
|
|
@@ -1247,10 +1733,10 @@ var PiSessionStore = class {
|
|
|
1247
1733
|
}
|
|
1248
1734
|
async findWrapperReferencingNativeSession(nativePath) {
|
|
1249
1735
|
const resolvedNativePath = resolve(nativePath);
|
|
1250
|
-
const files = await
|
|
1736
|
+
const files = await readdir2(this.sessionDir).catch(() => []);
|
|
1251
1737
|
for (const file of files) {
|
|
1252
1738
|
if (!file.endsWith(".jsonl")) continue;
|
|
1253
|
-
const filepath =
|
|
1739
|
+
const filepath = join2(this.sessionDir, file);
|
|
1254
1740
|
if (resolve(filepath) === resolvedNativePath) continue;
|
|
1255
1741
|
try {
|
|
1256
1742
|
const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries2(await readJsonlPrefix2(filepath)));
|
|
@@ -1261,7 +1747,7 @@ var PiSessionStore = class {
|
|
|
1261
1747
|
return null;
|
|
1262
1748
|
}
|
|
1263
1749
|
ensureWrapperForNativeSessionSync(sessionId, nativePath, entries, ctx) {
|
|
1264
|
-
const wrapperPath =
|
|
1750
|
+
const wrapperPath = join2(this.sessionDir, `${sessionId}.jsonl`);
|
|
1265
1751
|
if (resolve(wrapperPath) === resolve(nativePath)) return wrapperPath;
|
|
1266
1752
|
try {
|
|
1267
1753
|
readFileSync(wrapperPath, "utf-8");
|
|
@@ -1281,7 +1767,7 @@ var PiSessionStore = class {
|
|
|
1281
1767
|
return wrapperPath;
|
|
1282
1768
|
}
|
|
1283
1769
|
async ensureWrapperForNativeSession(sessionId, nativePath, ctx) {
|
|
1284
|
-
const wrapperPath =
|
|
1770
|
+
const wrapperPath = join2(this.sessionDir, `${sessionId}.jsonl`);
|
|
1285
1771
|
if (resolve(wrapperPath) === resolve(nativePath)) return wrapperPath;
|
|
1286
1772
|
try {
|
|
1287
1773
|
await fsStat2(wrapperPath);
|
|
@@ -1290,7 +1776,7 @@ var PiSessionStore = class {
|
|
|
1290
1776
|
}
|
|
1291
1777
|
const entries = parseJsonlPrefixEntries2(await readJsonlPrefix2(nativePath));
|
|
1292
1778
|
try {
|
|
1293
|
-
await
|
|
1779
|
+
await writeFile2(
|
|
1294
1780
|
wrapperPath,
|
|
1295
1781
|
buildNativePiSessionWrapper(sessionId, this.cwd, nativePath, entries, ctx),
|
|
1296
1782
|
{ encoding: "utf-8", flag: "wx" }
|
|
@@ -1302,27 +1788,67 @@ var PiSessionStore = class {
|
|
|
1302
1788
|
return wrapperPath;
|
|
1303
1789
|
}
|
|
1304
1790
|
async readLinkedPiSession(filepath) {
|
|
1791
|
+
let last;
|
|
1792
|
+
let parsed;
|
|
1305
1793
|
try {
|
|
1306
|
-
|
|
1307
|
-
fsStat2(filepath)
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1794
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1795
|
+
const before = await fsStat2(filepath);
|
|
1796
|
+
parsed = parseSessionTranscript(await readFile3(filepath, "utf-8"));
|
|
1797
|
+
const after = await fsStat2(filepath);
|
|
1798
|
+
last = after;
|
|
1799
|
+
if (sameFileSnapshot(before, after)) {
|
|
1800
|
+
return {
|
|
1801
|
+
...parsed,
|
|
1802
|
+
dev: Number(after.dev),
|
|
1803
|
+
ino: Number(after.ino),
|
|
1804
|
+
mtimeMs: Number(after.mtimeMs),
|
|
1805
|
+
ctimeMs: Number(after.ctimeMs),
|
|
1806
|
+
size: Number(after.size),
|
|
1807
|
+
stable: true
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
return parsed && last ? {
|
|
1812
|
+
...parsed,
|
|
1813
|
+
dev: Number(last.dev),
|
|
1814
|
+
ino: Number(last.ino),
|
|
1815
|
+
mtimeMs: Number(last.mtimeMs),
|
|
1816
|
+
ctimeMs: Number(last.ctimeMs),
|
|
1817
|
+
size: Number(last.size),
|
|
1818
|
+
stable: false
|
|
1819
|
+
} : null;
|
|
1311
1820
|
} catch {
|
|
1312
1821
|
return null;
|
|
1313
1822
|
}
|
|
1314
1823
|
}
|
|
1315
|
-
async
|
|
1824
|
+
async readTranscriptSummary(filepath) {
|
|
1825
|
+
let last;
|
|
1826
|
+
let summary;
|
|
1316
1827
|
try {
|
|
1317
|
-
|
|
1318
|
-
fsStat2(filepath)
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1828
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1829
|
+
const before = await fsStat2(filepath);
|
|
1830
|
+
summary = await summarizeSessionTranscript(filepath);
|
|
1831
|
+
const after = await fsStat2(filepath);
|
|
1832
|
+
last = after;
|
|
1833
|
+
if (sameFileSnapshot(before, after)) return { summary, stat: after, stable: true };
|
|
1834
|
+
}
|
|
1835
|
+
return summary && last ? { summary, stat: last, stable: false } : null;
|
|
1322
1836
|
} catch {
|
|
1323
1837
|
return null;
|
|
1324
1838
|
}
|
|
1325
1839
|
}
|
|
1840
|
+
async readLinkedPiSessionSummary(filepath) {
|
|
1841
|
+
const scan = await this.readTranscriptSummary(filepath);
|
|
1842
|
+
return scan ? {
|
|
1843
|
+
summary: scan.summary,
|
|
1844
|
+
dev: Number(scan.stat.dev),
|
|
1845
|
+
ino: Number(scan.stat.ino),
|
|
1846
|
+
mtimeMs: Number(scan.stat.mtimeMs),
|
|
1847
|
+
ctimeMs: Number(scan.stat.ctimeMs),
|
|
1848
|
+
size: Number(scan.stat.size),
|
|
1849
|
+
stable: scan.stable
|
|
1850
|
+
} : null;
|
|
1851
|
+
}
|
|
1326
1852
|
headerBelongsToCtx(header, ctx, directNative = false) {
|
|
1327
1853
|
if (directNative) return this.nativeFileBelongsToCtx(header, ctx);
|
|
1328
1854
|
return header ? this.storedCtxBelongsToCtx(readHeaderSessionCtx(header), ctx) : isEmptySessionCtx(ctx);
|
|
@@ -1362,7 +1888,7 @@ var PiSessionStore = class {
|
|
|
1362
1888
|
}
|
|
1363
1889
|
};
|
|
1364
1890
|
async function readJsonlPrefix2(filepath, maxBytes = SUMMARY_PREFIX_BYTES2) {
|
|
1365
|
-
const handle = await
|
|
1891
|
+
const handle = await open3(filepath, "r");
|
|
1366
1892
|
try {
|
|
1367
1893
|
const buffer = Buffer.alloc(maxBytes);
|
|
1368
1894
|
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
@@ -1458,35 +1984,15 @@ function isTimestampNamedPiSessionFile(filepath, sessionId) {
|
|
|
1458
1984
|
const filename = basename(filepath);
|
|
1459
1985
|
return /^\d{4}-\d{2}-\d{2}/.test(filename) && filename.endsWith(`_${sessionId}.jsonl`);
|
|
1460
1986
|
}
|
|
1461
|
-
function
|
|
1462
|
-
return
|
|
1463
|
-
(e) => e.type === "message" && e.message?.role === "user"
|
|
1464
|
-
).length;
|
|
1987
|
+
function sameFileSnapshot(first, second) {
|
|
1988
|
+
return first.dev === second.dev && first.ino === second.ino && first.size === second.size && first.mtimeMs === second.mtimeMs && first.ctimeMs === second.ctimeMs;
|
|
1465
1989
|
}
|
|
1466
|
-
function
|
|
1467
|
-
return
|
|
1468
|
-
(entry) => entry.type === "message" && entry.message.role === "assistant"
|
|
1469
|
-
);
|
|
1990
|
+
function selectTranscriptSummary(wrapper, native) {
|
|
1991
|
+
return native && native.entryCount > 0 ? native : wrapper;
|
|
1470
1992
|
}
|
|
1471
|
-
function
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
...nativeEntries.flatMap((entry, offset) => entry.type === "session_info" ? [{ entry, native: 1, offset }] : [])
|
|
1475
|
-
];
|
|
1476
|
-
candidates.sort((a, b) => {
|
|
1477
|
-
const timestamp = Date.parse(b.entry.timestamp) - Date.parse(a.entry.timestamp);
|
|
1478
|
-
return timestamp || b.native - a.native || b.offset - a.offset;
|
|
1479
|
-
});
|
|
1480
|
-
return candidates[0]?.entry.name;
|
|
1481
|
-
}
|
|
1482
|
-
function firstUserMessage(entries) {
|
|
1483
|
-
for (const e of entries) {
|
|
1484
|
-
if (e.type !== "message") continue;
|
|
1485
|
-
const msg = e.message;
|
|
1486
|
-
if (msg?.role !== "user") continue;
|
|
1487
|
-
const text = textFromPiContent2(msg.content);
|
|
1488
|
-
if (text) return text.slice(0, 80);
|
|
1489
|
-
}
|
|
1993
|
+
function projectedTranscriptTitle(wrapper, native) {
|
|
1994
|
+
const selected = selectTranscriptSummary(wrapper, native);
|
|
1995
|
+
return wrapper.userTitle ?? selected.title ?? wrapper.title ?? native?.title ?? selected.firstUserTitle;
|
|
1490
1996
|
}
|
|
1491
1997
|
function safeParseEntries(content) {
|
|
1492
1998
|
try {
|
|
@@ -1514,14 +2020,6 @@ function parseJsonlPrefixEntries2(content) {
|
|
|
1514
2020
|
}
|
|
1515
2021
|
return entries;
|
|
1516
2022
|
}
|
|
1517
|
-
function textFromPiContent2(content) {
|
|
1518
|
-
if (typeof content === "string") return content;
|
|
1519
|
-
if (!Array.isArray(content)) return "";
|
|
1520
|
-
return content.map((part) => {
|
|
1521
|
-
const item = part;
|
|
1522
|
-
return item?.type === "text" && typeof item.text === "string" ? item.text : "";
|
|
1523
|
-
}).join("");
|
|
1524
|
-
}
|
|
1525
2023
|
function withStableMessageId(message, entryId) {
|
|
1526
2024
|
if (!entryId || !message || typeof message !== "object" || Array.isArray(message)) return message;
|
|
1527
2025
|
if (typeof message.id === "string") return message;
|
|
@@ -1622,7 +2120,7 @@ function readApiKeyEnv(candidates) {
|
|
|
1622
2120
|
function buildOpenAICompatibleProviderConfig(opts) {
|
|
1623
2121
|
return {
|
|
1624
2122
|
baseUrl: opts.baseUrl,
|
|
1625
|
-
//
|
|
2123
|
+
// Pi resolves `$ENV_VAR` config references through
|
|
1626
2124
|
// the provider environment. A bare name is a literal API key, which would
|
|
1627
2125
|
// send "INFOMANIAK_API_TOKEN" as the bearer token and cause provider 401s.
|
|
1628
2126
|
apiKey: `$${opts.apiKeyEnv}`,
|
|
@@ -1753,6 +2251,17 @@ function readConfiguredDefaultModel() {
|
|
|
1753
2251
|
return readPiSettingsDefaultModel() ?? readInfomaniakProvider()?.defaultModel ?? readCustomProvider()?.defaultModel;
|
|
1754
2252
|
}
|
|
1755
2253
|
|
|
2254
|
+
// src/server/models/modelRuntime.ts
|
|
2255
|
+
import {
|
|
2256
|
+
ModelRuntime
|
|
2257
|
+
} from "@mariozechner/pi-coding-agent";
|
|
2258
|
+
async function createConfiguredModelRuntime(options) {
|
|
2259
|
+
const modelRuntime = await ModelRuntime.create(options);
|
|
2260
|
+
const configuredModels = registerConfiguredModelProviders(modelRuntime);
|
|
2261
|
+
await modelRuntime.refresh({ allowNetwork: false });
|
|
2262
|
+
return { modelRuntime, configuredModels };
|
|
2263
|
+
}
|
|
2264
|
+
|
|
1756
2265
|
// src/server/piPackages.ts
|
|
1757
2266
|
var PI_PACKAGE_RESOURCE_FILTERS = [
|
|
1758
2267
|
"extensions",
|
|
@@ -1797,7 +2306,7 @@ function mergePiPackageSources(base = [], additional = []) {
|
|
|
1797
2306
|
|
|
1798
2307
|
// src/server/harness/pi-coding-agent/resourceSettingsManager.ts
|
|
1799
2308
|
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
1800
|
-
import { join as
|
|
2309
|
+
import { join as join3 } from "node:path";
|
|
1801
2310
|
import { SettingsManager as SettingsManager2 } from "@mariozechner/pi-coding-agent";
|
|
1802
2311
|
function readSettingsFileIfPresent(path) {
|
|
1803
2312
|
return existsSync(path) ? readFileSync2(path, "utf-8") : void 0;
|
|
@@ -1812,8 +2321,8 @@ function mergeInjectedProjectPackages(settingsJson, piPackages, includePackage)
|
|
|
1812
2321
|
}
|
|
1813
2322
|
function createResourceSettingsManager(cwd, agentDir, piPackages, options = {}) {
|
|
1814
2323
|
if (piPackages.length === 0 && !options.includePackage) return SettingsManager2.create(cwd, agentDir);
|
|
1815
|
-
const globalSettingsPath =
|
|
1816
|
-
const projectSettingsPath =
|
|
2324
|
+
const globalSettingsPath = join3(agentDir, "settings.json");
|
|
2325
|
+
const projectSettingsPath = join3(cwd, ".pi", "settings.json");
|
|
1817
2326
|
let globalSettingsOverrideJson;
|
|
1818
2327
|
let projectSettingsOverrideJson;
|
|
1819
2328
|
const storage = {
|
|
@@ -1931,11 +2440,11 @@ function meteredExtensionCommandContext(ctx, command) {
|
|
|
1931
2440
|
guarded.switchSession = async () => block();
|
|
1932
2441
|
return guarded;
|
|
1933
2442
|
}
|
|
1934
|
-
function resolveRequestedModel(
|
|
2443
|
+
function resolveRequestedModel(modelRuntime, input, options = {}) {
|
|
1935
2444
|
const requestedId = input.model?.id;
|
|
1936
2445
|
if (!input.model || !requestedId) return void 0;
|
|
1937
|
-
const model =
|
|
1938
|
-
const available =
|
|
2446
|
+
const model = modelRuntime.getModel(input.model.provider, requestedId);
|
|
2447
|
+
const available = modelRuntime.getAvailableSnapshot();
|
|
1939
2448
|
const hasAuth = Boolean(model) && available.some(
|
|
1940
2449
|
(m) => m.provider === model.provider && m.id === model.id
|
|
1941
2450
|
);
|
|
@@ -1945,13 +2454,13 @@ function resolveRequestedModel(modelRegistry, input, options = {}) {
|
|
|
1945
2454
|
}
|
|
1946
2455
|
return model;
|
|
1947
2456
|
}
|
|
1948
|
-
function resolveDefaultModel(
|
|
2457
|
+
function resolveDefaultModel(modelRuntime, override, strict) {
|
|
1949
2458
|
if (override) {
|
|
1950
|
-
return resolveRequestedModel(
|
|
2459
|
+
return resolveRequestedModel(modelRuntime, { model: override }, { strict });
|
|
1951
2460
|
}
|
|
1952
2461
|
const configured = readConfiguredDefaultModel();
|
|
1953
2462
|
if (configured) {
|
|
1954
|
-
const model =
|
|
2463
|
+
const model = modelRuntime.getModel(configured.provider, configured.id);
|
|
1955
2464
|
if (model) return model;
|
|
1956
2465
|
}
|
|
1957
2466
|
return void 0;
|
|
@@ -1970,7 +2479,7 @@ function sessionCacheKey(sessionId, ctx) {
|
|
|
1970
2479
|
return JSON.stringify([sessionId, ctx.workspaceId ?? ""]);
|
|
1971
2480
|
}
|
|
1972
2481
|
async function applyRequestedSessionOptions(handle, input, options = {}) {
|
|
1973
|
-
const requestedModel = resolveRequestedModel(handle.
|
|
2482
|
+
const requestedModel = resolveRequestedModel(handle.modelRuntime, input, { strict: options.strictModelResolution });
|
|
1974
2483
|
if (requestedModel) {
|
|
1975
2484
|
const current = handle.piSession.model;
|
|
1976
2485
|
if (!current || current.provider !== requestedModel.provider || current.id !== requestedModel.id) {
|
|
@@ -2102,10 +2611,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
2102
2611
|
return await runContextStorage.run(ctx, run);
|
|
2103
2612
|
}
|
|
2104
2613
|
function createRunBoundAdapter(handle, sessionId, ctx) {
|
|
2105
|
-
const adapter = createPiAgentSessionAdapter(handle.piSession, {
|
|
2106
|
-
sessionId,
|
|
2107
|
-
...handle.piSession.agent && typeof handle.piSession.agent.continue === "function" ? { continueQueuedFollowUp: () => handle.piSession.agent.continue() } : {}
|
|
2108
|
-
});
|
|
2614
|
+
const adapter = createPiAgentSessionAdapter(handle.piSession, { sessionId });
|
|
2109
2615
|
return {
|
|
2110
2616
|
...adapter,
|
|
2111
2617
|
prompt: (promptInput) => bindRunContext(ctx, () => adapter.prompt(promptInput)),
|
|
@@ -2115,11 +2621,9 @@ function createPiCodingAgentHarness(opts) {
|
|
|
2115
2621
|
};
|
|
2116
2622
|
}
|
|
2117
2623
|
async function createPiSession(sessionId, sessionCtx, input, ctx) {
|
|
2118
|
-
const
|
|
2119
|
-
const
|
|
2120
|
-
|
|
2121
|
-
const resolvedModel = resolveRequestedModel(modelRegistry, input, { strict: pi.strictModelResolution });
|
|
2122
|
-
const model = resolvedModel ?? resolveDefaultModel(modelRegistry, pi.defaultModel, pi.strictModelResolution);
|
|
2624
|
+
const { modelRuntime } = await createConfiguredModelRuntime();
|
|
2625
|
+
const resolvedModel = resolveRequestedModel(modelRuntime, input, { strict: pi.strictModelResolution });
|
|
2626
|
+
const model = resolvedModel ?? resolveDefaultModel(modelRuntime, pi.defaultModel, pi.strictModelResolution);
|
|
2123
2627
|
const savedPiFile = await sessionStore.loadPiSessionFile(sessionCtx, sessionId);
|
|
2124
2628
|
const runtimeCwd = opts.runtimeCwd ?? ctx.workdir;
|
|
2125
2629
|
if (!savedPiFile) {
|
|
@@ -2193,8 +2697,8 @@ function createPiCodingAgentHarness(opts) {
|
|
|
2193
2697
|
model,
|
|
2194
2698
|
thinkingLevel: input.thinkingLevel ?? "off",
|
|
2195
2699
|
sessionManager,
|
|
2196
|
-
|
|
2197
|
-
|
|
2700
|
+
modelRuntime,
|
|
2701
|
+
settingsManager,
|
|
2198
2702
|
...resourceLoader ? { resourceLoader } : {}
|
|
2199
2703
|
});
|
|
2200
2704
|
const restoreFollowUpContextWrapper = rememberQueuedFollowUpRunContexts(piSession, runContextState, () => runContextStorage.getStore());
|
|
@@ -2205,7 +2709,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
2205
2709
|
};
|
|
2206
2710
|
const handle = {
|
|
2207
2711
|
piSession,
|
|
2208
|
-
|
|
2712
|
+
modelRuntime,
|
|
2209
2713
|
sessionManager,
|
|
2210
2714
|
resourceLoader,
|
|
2211
2715
|
sessionId,
|
|
@@ -2267,7 +2771,8 @@ function createPiCodingAgentHarness(opts) {
|
|
|
2267
2771
|
* the expected pre-first-turn state, not an error.
|
|
2268
2772
|
*/
|
|
2269
2773
|
getSystemPrompt(sessionId) {
|
|
2270
|
-
|
|
2774
|
+
const prompt = piSessionHandlesFor(sessionId)[0]?.piSession.systemPrompt;
|
|
2775
|
+
return prompt && pi.locateSkillResource ? projectSkillResourceLocations(prompt, pi.locateSkillResource) : prompt;
|
|
2271
2776
|
},
|
|
2272
2777
|
hasPiSession(sessionId, ctx) {
|
|
2273
2778
|
return ctx ? piSessions.has(sessionCacheKey(sessionId, ctx)) : piSessionHandlesFor(sessionId).length > 0;
|
|
@@ -2339,8 +2844,8 @@ export {
|
|
|
2339
2844
|
getEnv,
|
|
2340
2845
|
PiSessionStore,
|
|
2341
2846
|
parseEncodedModelSelection,
|
|
2342
|
-
registerConfiguredModelProviders,
|
|
2343
2847
|
readConfiguredDefaultModel,
|
|
2848
|
+
createConfiguredModelRuntime,
|
|
2344
2849
|
PI_PACKAGE_RESOURCE_FILTERS,
|
|
2345
2850
|
piPackageSourceKey,
|
|
2346
2851
|
compactPiPackages,
|