@hachej/boring-agent 0.1.104 → 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-I7KFNGB4.js → chunk-GS4ADE34.js} +877 -349
- package/dist/{chunk-BTYOMR7W.js → chunk-KDXIXVF6.js} +5179 -3668
- 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-POZ4AXCZ.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 +43 -7
- package/dist/front/index.js +1505 -729
- package/dist/front/styles.css +109 -15
- 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 +207 -243
- package/dist/server/index.js +15 -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-B1xDlBSo.d.ts → types-B7-PN5jV.d.ts} +75 -23
- package/docs/AGENT_GATEWAY_V0.md +40 -2
- package/docs/ERROR_CODES.md +2 -1
- 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) };
|
|
@@ -688,27 +1127,36 @@ var PiSessionStore = class {
|
|
|
688
1127
|
}));
|
|
689
1128
|
const existingFiles = fileStats.filter((item) => item !== null);
|
|
690
1129
|
const referencedPiFiles = await this.referencedPiFiles(existingFiles);
|
|
691
|
-
const visibleFiles = await Promise.all(existingFiles.filter(({ filepath }) => !referencedPiFiles.has(resolve(filepath))).map(async (file) => ({
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1130
|
+
const visibleFiles = await Promise.all(existingFiles.filter(({ filepath }) => !referencedPiFiles.has(resolve(filepath))).map(async (file) => ({ ...file, ...await this.sessionSortKey(file) })));
|
|
1131
|
+
visibleFiles.sort((a, b) => compareSessionOrder(
|
|
1132
|
+
[a.orderingUpdatedAtMs, "", a.sortId],
|
|
1133
|
+
[b.orderingUpdatedAtMs, "", b.sortId]
|
|
1134
|
+
));
|
|
696
1135
|
const { offset, limit } = options;
|
|
697
1136
|
const includeId = options.includeId;
|
|
698
|
-
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, {
|
|
699
1145
|
ctx,
|
|
700
1146
|
offset,
|
|
701
1147
|
limit,
|
|
702
1148
|
includeId,
|
|
703
|
-
includeEmpty: options.includeEmpty
|
|
1149
|
+
includeEmpty: options.includeEmpty,
|
|
1150
|
+
archivedIds
|
|
704
1151
|
});
|
|
705
1152
|
if (!includeId || pageSummaries.some((summary) => summary.id === includeId)) return pageSummaries;
|
|
706
1153
|
const includeSummary = await this.summarizeIncludedSession(ctx, includeId, referencedPiFiles);
|
|
707
|
-
return includeSummary ? [...pageSummaries, includeSummary] : pageSummaries;
|
|
1154
|
+
return includeSummary ? [...pageSummaries, withArchivedFlag(includeSummary, archivedIds)] : pageSummaries;
|
|
708
1155
|
}
|
|
709
1156
|
async create(ctx, init) {
|
|
710
|
-
|
|
711
|
-
|
|
1157
|
+
const manualTitle = init?.title !== void 0 ? normalizeUserSessionTitle(init.title) : void 0;
|
|
1158
|
+
await mkdir2(this.sessionDir, { recursive: true });
|
|
1159
|
+
const id = randomUUID3();
|
|
712
1160
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
713
1161
|
const header = {
|
|
714
1162
|
type: "session",
|
|
@@ -719,21 +1167,21 @@ var PiSessionStore = class {
|
|
|
719
1167
|
boringSessionCtx: normalizeSessionCtx(ctx) ?? {}
|
|
720
1168
|
};
|
|
721
1169
|
const lines = [JSON.stringify(header)];
|
|
722
|
-
if (
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
id: randomUUID(),
|
|
1170
|
+
if (manualTitle) {
|
|
1171
|
+
const pair = createUserSessionTitleEntries({
|
|
1172
|
+
title: manualTitle,
|
|
726
1173
|
parentId: null,
|
|
727
1174
|
timestamp: now,
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1175
|
+
authorityId: randomUUID3(),
|
|
1176
|
+
titleId: randomUUID3()
|
|
1177
|
+
});
|
|
1178
|
+
lines.push(JSON.stringify(pair.authority), JSON.stringify(pair.title));
|
|
731
1179
|
}
|
|
732
|
-
const filepath =
|
|
733
|
-
await
|
|
1180
|
+
const filepath = join2(this.sessionDir, `${nativeSessionFilename(id, now)}`);
|
|
1181
|
+
await writeFile2(filepath, lines.join("\n") + "\n", "utf-8");
|
|
734
1182
|
return {
|
|
735
1183
|
id,
|
|
736
|
-
title:
|
|
1184
|
+
title: manualTitle ?? "New session",
|
|
737
1185
|
createdAt: now,
|
|
738
1186
|
updatedAt: now,
|
|
739
1187
|
turnCount: 0,
|
|
@@ -741,23 +1189,39 @@ var PiSessionStore = class {
|
|
|
741
1189
|
hasAssistantReply: false
|
|
742
1190
|
};
|
|
743
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
|
+
}
|
|
744
1210
|
async load(ctx, sessionId) {
|
|
745
1211
|
const resolved = await this.resolveSessionTranscript(ctx, sessionId);
|
|
746
|
-
const
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
const updatedAtMs = nativeSummary?.latestMessageAtMs ?? Math.max(resolved.fileStat.mtime.getTime(), resolved.linkedMtimeMs ?? 0);
|
|
750
|
-
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({
|
|
751
1215
|
id: resolved.resolvedSessionId,
|
|
752
1216
|
title,
|
|
753
1217
|
createdAt: resolved.header?.timestamp ?? resolved.fileStat.birthtime.toISOString(),
|
|
754
1218
|
updatedAt: new Date(updatedAtMs).toISOString(),
|
|
755
|
-
turnCount,
|
|
1219
|
+
turnCount: resolved.transcriptSummary.turnCount,
|
|
756
1220
|
...resolved.directNative ? {
|
|
757
1221
|
nativeSessionId: resolved.resolvedSessionId,
|
|
758
|
-
hasAssistantReply:
|
|
1222
|
+
hasAssistantReply: resolved.transcriptSummary.hasAssistantReply
|
|
759
1223
|
} : {}
|
|
760
|
-
};
|
|
1224
|
+
}, await readArchivedSessionIds(this.sessionDir));
|
|
761
1225
|
}
|
|
762
1226
|
/**
|
|
763
1227
|
* Returns the persisted pi message objects in file order so callers can run
|
|
@@ -790,11 +1254,12 @@ var PiSessionStore = class {
|
|
|
790
1254
|
const filepath = await this.resolveSessionFile(sessionId, ctx);
|
|
791
1255
|
let content;
|
|
792
1256
|
try {
|
|
793
|
-
content = await
|
|
1257
|
+
content = await readFile3(filepath, "utf-8");
|
|
794
1258
|
} catch {
|
|
795
1259
|
throw new Error(`Session not found: ${sessionId}`);
|
|
796
1260
|
}
|
|
797
|
-
const
|
|
1261
|
+
const parsedFile = parseSessionTranscript(content);
|
|
1262
|
+
const fileEntries = parsedFile.entries;
|
|
798
1263
|
const header = fileEntries.find(
|
|
799
1264
|
(e) => e.type === "session"
|
|
800
1265
|
);
|
|
@@ -802,17 +1267,6 @@ var PiSessionStore = class {
|
|
|
802
1267
|
filepath,
|
|
803
1268
|
header?.id ?? sessionId
|
|
804
1269
|
);
|
|
805
|
-
if (!timestampNamedNative && fileEntries.some((e) => e.type === "ui_snapshot")) {
|
|
806
|
-
const compacted = fileEntries.filter((e) => e.type !== "ui_snapshot").map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
807
|
-
const tmp = `${filepath}.compact-${randomUUID()}`;
|
|
808
|
-
try {
|
|
809
|
-
await writeFile(tmp, compacted, "utf-8");
|
|
810
|
-
await rename(tmp, filepath);
|
|
811
|
-
} catch {
|
|
812
|
-
await rm(tmp, { force: true }).catch(() => {
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
1270
|
if (!this.headerBelongsToCtx(header, ctx, timestampNamedNative)) {
|
|
817
1271
|
throw new Error(`Session not found: ${sessionId}`);
|
|
818
1272
|
}
|
|
@@ -826,17 +1280,18 @@ var PiSessionStore = class {
|
|
|
826
1280
|
const linkedEntries = linked?.entries.filter(
|
|
827
1281
|
(e) => e.type !== "session"
|
|
828
1282
|
) ?? [];
|
|
829
|
-
const
|
|
1283
|
+
const transcriptSummary = selectTranscriptSummary(parsedFile.summary, linked?.summary);
|
|
1284
|
+
const transcriptEntries = transcriptSummary === linked?.summary ? linkedEntries : sessionEntries;
|
|
830
1285
|
return {
|
|
831
1286
|
resolvedSessionId: header?.id ?? sessionId,
|
|
832
1287
|
header,
|
|
833
|
-
sessionEntries,
|
|
834
|
-
linkedEntries,
|
|
835
1288
|
transcriptEntries,
|
|
1289
|
+
sessionSummary: parsedFile.summary,
|
|
1290
|
+
...linked ? { linkedSummary: linked.summary } : {},
|
|
1291
|
+
transcriptSummary,
|
|
836
1292
|
fileStat,
|
|
837
|
-
linkedMtimeMs: linked?.
|
|
1293
|
+
linkedMtimeMs: linked?.mtimeMs,
|
|
838
1294
|
filepath,
|
|
839
|
-
...linkedPiFile && linked ? { linkedFilepath: linkedPiFile } : {},
|
|
840
1295
|
directNative
|
|
841
1296
|
};
|
|
842
1297
|
}
|
|
@@ -845,8 +1300,7 @@ var PiSessionStore = class {
|
|
|
845
1300
|
* appended through the same JSONL path under one process-local writer lock.
|
|
846
1301
|
*/
|
|
847
1302
|
async rename(ctx, sessionId, title) {
|
|
848
|
-
const trimmed = title
|
|
849
|
-
if (!trimmed) throw new Error("Session title must not be empty");
|
|
1303
|
+
const trimmed = normalizeUserSessionTitle(title);
|
|
850
1304
|
return await this.withWriter(sessionId, async () => {
|
|
851
1305
|
const resolved = await this.resolveSessionTranscript(ctx, sessionId);
|
|
852
1306
|
if (resolved.directNative) {
|
|
@@ -862,20 +1316,8 @@ var PiSessionStore = class {
|
|
|
862
1316
|
}
|
|
863
1317
|
return await this.load(ctx, sessionId);
|
|
864
1318
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
id: randomUUID(),
|
|
868
|
-
parentId: null,
|
|
869
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
870
|
-
name: trimmed
|
|
871
|
-
};
|
|
872
|
-
const line = `${JSON.stringify(entry)}
|
|
873
|
-
`;
|
|
874
|
-
const targets = [resolved.filepath, resolved.linkedFilepath].filter((path) => Boolean(path));
|
|
875
|
-
for (const filepath of targets) {
|
|
876
|
-
await appendFile(filepath, line);
|
|
877
|
-
this.prefixCache.delete(filepath);
|
|
878
|
-
}
|
|
1319
|
+
await appendVerifiedWrapperRename(resolved.filepath, trimmed);
|
|
1320
|
+
this.prefixCache.delete(resolved.filepath);
|
|
879
1321
|
return await this.load(ctx, sessionId);
|
|
880
1322
|
});
|
|
881
1323
|
}
|
|
@@ -901,7 +1343,7 @@ var PiSessionStore = class {
|
|
|
901
1343
|
loadPiSessionFileSync(ctx, sessionId) {
|
|
902
1344
|
if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) return null;
|
|
903
1345
|
try {
|
|
904
|
-
const direct =
|
|
1346
|
+
const direct = join2(this.sessionDir, `${sessionId}.jsonl`);
|
|
905
1347
|
let filepath = direct;
|
|
906
1348
|
let content;
|
|
907
1349
|
try {
|
|
@@ -911,7 +1353,7 @@ var PiSessionStore = class {
|
|
|
911
1353
|
(f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
|
|
912
1354
|
);
|
|
913
1355
|
if (files.length === 0) return null;
|
|
914
|
-
filepath =
|
|
1356
|
+
filepath = join2(this.sessionDir, files[0]);
|
|
915
1357
|
content = readFileSync(filepath, "utf-8");
|
|
916
1358
|
}
|
|
917
1359
|
const entries = safeParseEntries(content);
|
|
@@ -941,19 +1383,19 @@ var PiSessionStore = class {
|
|
|
941
1383
|
async loadPiSessionFile(ctx, sessionId) {
|
|
942
1384
|
if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) return null;
|
|
943
1385
|
try {
|
|
944
|
-
const direct =
|
|
1386
|
+
const direct = join2(this.sessionDir, `${sessionId}.jsonl`);
|
|
945
1387
|
let filepath = direct;
|
|
946
1388
|
let content;
|
|
947
1389
|
try {
|
|
948
|
-
content = await
|
|
1390
|
+
content = await readFile3(direct, "utf-8");
|
|
949
1391
|
} catch {
|
|
950
|
-
const files = await
|
|
1392
|
+
const files = await readdir2(this.sessionDir).catch(() => []);
|
|
951
1393
|
const match = files.find(
|
|
952
1394
|
(f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
|
|
953
1395
|
);
|
|
954
1396
|
if (!match) return null;
|
|
955
|
-
filepath =
|
|
956
|
-
content = await
|
|
1397
|
+
filepath = join2(this.sessionDir, match);
|
|
1398
|
+
content = await readFile3(filepath, "utf-8");
|
|
957
1399
|
}
|
|
958
1400
|
const entries = safeParseEntries(content);
|
|
959
1401
|
const header = entries.find((entry) => entry.type === "session");
|
|
@@ -967,8 +1409,8 @@ var PiSessionStore = class {
|
|
|
967
1409
|
if (readHeaderSessionCtx(header) !== null) return null;
|
|
968
1410
|
const existingWrapper = await this.findWrapperReferencingNativeSession(filepath);
|
|
969
1411
|
if (existingWrapper) {
|
|
970
|
-
const
|
|
971
|
-
if (
|
|
1412
|
+
const wrapperSessionId2 = await this.readSessionFileId(existingWrapper);
|
|
1413
|
+
if (wrapperSessionId2 !== sessionId) return null;
|
|
972
1414
|
const wrapperEntries = parseJsonlPrefixEntries2(await readJsonlPrefix2(existingWrapper));
|
|
973
1415
|
const wrapperHeader = wrapperEntries.find((entry) => entry.type === "session");
|
|
974
1416
|
if (!this.headerBelongsToCtx(wrapperHeader, ctx)) return null;
|
|
@@ -987,7 +1429,7 @@ var PiSessionStore = class {
|
|
|
987
1429
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
988
1430
|
path: piFilePath
|
|
989
1431
|
});
|
|
990
|
-
await
|
|
1432
|
+
await appendFile2(filepath, entry + "\n");
|
|
991
1433
|
}
|
|
992
1434
|
async delete(ctx, sessionId) {
|
|
993
1435
|
const filepath = await this.resolveSessionFile(sessionId, ctx).catch(
|
|
@@ -997,30 +1439,31 @@ var PiSessionStore = class {
|
|
|
997
1439
|
const fileSessionId = await this.readSessionFileId(filepath);
|
|
998
1440
|
if (fileSessionId && fileSessionId !== sessionId) return;
|
|
999
1441
|
const linkedPiFile = await this.linkedPiFileFor(filepath);
|
|
1000
|
-
await
|
|
1442
|
+
await rm2(filepath, { force: true });
|
|
1001
1443
|
this.prefixCache.delete(filepath);
|
|
1002
1444
|
if (linkedPiFile && resolve(linkedPiFile) !== resolve(filepath)) {
|
|
1003
|
-
await
|
|
1445
|
+
await rm2(linkedPiFile, { force: true });
|
|
1004
1446
|
this.prefixCache.delete(linkedPiFile);
|
|
1005
1447
|
}
|
|
1448
|
+
await forgetArchivedSession(this.sessionDir, fileSessionId ?? sessionId);
|
|
1006
1449
|
}
|
|
1007
1450
|
async resolveSessionFile(sessionId, ctx) {
|
|
1008
1451
|
if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) {
|
|
1009
1452
|
throw new Error(`Session not found: ${sessionId}`);
|
|
1010
1453
|
}
|
|
1011
|
-
const direct =
|
|
1454
|
+
const direct = join2(this.sessionDir, `${sessionId}.jsonl`);
|
|
1012
1455
|
try {
|
|
1013
1456
|
await fsStat2(direct);
|
|
1014
1457
|
if (ctx) await this.assertFileBelongsToCtx(direct, ctx, sessionId);
|
|
1015
1458
|
return direct;
|
|
1016
1459
|
} catch {
|
|
1017
1460
|
}
|
|
1018
|
-
const files = await
|
|
1461
|
+
const files = await readdir2(this.sessionDir).catch(() => []);
|
|
1019
1462
|
const match = files.find(
|
|
1020
1463
|
(f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
|
|
1021
1464
|
);
|
|
1022
1465
|
if (!match) throw new Error(`Session not found: ${sessionId}`);
|
|
1023
|
-
const matchedPath =
|
|
1466
|
+
const matchedPath = join2(this.sessionDir, match);
|
|
1024
1467
|
if (!isTimestampNamedPiSessionFile(matchedPath, sessionId)) {
|
|
1025
1468
|
if (ctx) await this.assertFileBelongsToCtx(matchedPath, ctx, sessionId);
|
|
1026
1469
|
return matchedPath;
|
|
@@ -1050,7 +1493,7 @@ var PiSessionStore = class {
|
|
|
1050
1493
|
}
|
|
1051
1494
|
async linkedPiFileFor(filepath) {
|
|
1052
1495
|
try {
|
|
1053
|
-
const content = await
|
|
1496
|
+
const content = await readFile3(filepath, "utf-8");
|
|
1054
1497
|
return extractPiSessionFilePath(safeParseEntries(content));
|
|
1055
1498
|
} catch {
|
|
1056
1499
|
return null;
|
|
@@ -1069,21 +1512,53 @@ var PiSessionStore = class {
|
|
|
1069
1512
|
}));
|
|
1070
1513
|
return referenced;
|
|
1071
1514
|
}
|
|
1072
|
-
async
|
|
1073
|
-
let
|
|
1515
|
+
async sessionSortKey({ filepath, stat }) {
|
|
1516
|
+
let fileMtimeMs = Number(stat.mtimeMs);
|
|
1517
|
+
let headerId;
|
|
1518
|
+
const stemFallback = () => ({
|
|
1519
|
+
orderingUpdatedAtMs: fileMtimeMs,
|
|
1520
|
+
sortId: wrapperSessionId(filepath)
|
|
1521
|
+
});
|
|
1074
1522
|
try {
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1523
|
+
const cached = await this.readPrefixCache(filepath, stat);
|
|
1524
|
+
fileMtimeMs = cached.mtimeMs;
|
|
1525
|
+
headerId = cached.headerId ?? void 0;
|
|
1526
|
+
if (!headerId) return stemFallback();
|
|
1527
|
+
const linkedPiFile = cached.referencedPiFile;
|
|
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 };
|
|
1079
1535
|
}
|
|
1080
|
-
|
|
1081
|
-
if (
|
|
1082
|
-
|
|
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;
|
|
1083
1543
|
}
|
|
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 };
|
|
1084
1559
|
} catch {
|
|
1560
|
+
return headerId ? { orderingUpdatedAtMs: fileMtimeMs, sortId: headerId } : stemFallback();
|
|
1085
1561
|
}
|
|
1086
|
-
return sortMtimeMs;
|
|
1087
1562
|
}
|
|
1088
1563
|
async summarizeFile(ctx, filepath, existingStat) {
|
|
1089
1564
|
try {
|
|
@@ -1103,39 +1578,51 @@ var PiSessionStore = class {
|
|
|
1103
1578
|
const directNative = isTimestampNamedPiSessionFile(filepath, header.id);
|
|
1104
1579
|
if (directNative ? !this.nativeFileBelongsToCtx(header, ctx) : !this.storedCtxBelongsToCtx(sessionCtx, ctx)) return null;
|
|
1105
1580
|
const entries = parseJsonlPrefixEntries2(content);
|
|
1106
|
-
const sessionEntries = entries.filter(
|
|
1107
|
-
(e) => e.type !== "session"
|
|
1108
|
-
);
|
|
1109
1581
|
const linkedPiFile = extractPiSessionFilePath(entries);
|
|
1110
|
-
const linked =
|
|
1111
|
-
|
|
1112
|
-
(
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
const
|
|
1117
|
-
|
|
1118
|
-
)
|
|
1119
|
-
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";
|
|
1120
1591
|
const summary = {
|
|
1121
1592
|
id: header.id,
|
|
1122
1593
|
title,
|
|
1123
1594
|
createdAt: header.timestamp,
|
|
1124
|
-
updatedAt: new Date(
|
|
1125
|
-
turnCount,
|
|
1126
|
-
...
|
|
1595
|
+
updatedAt: new Date(order.orderingUpdatedAtMs).toISOString(),
|
|
1596
|
+
turnCount: transcript.turnCount,
|
|
1597
|
+
...directNative ? {
|
|
1127
1598
|
nativeSessionId: header.id,
|
|
1128
|
-
hasAssistantReply:
|
|
1599
|
+
hasAssistantReply: transcript.hasAssistantReply
|
|
1129
1600
|
} : {}
|
|
1130
1601
|
};
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
+
}
|
|
1139
1626
|
return summary;
|
|
1140
1627
|
} catch {
|
|
1141
1628
|
return null;
|
|
@@ -1144,7 +1631,7 @@ var PiSessionStore = class {
|
|
|
1144
1631
|
cachedPrefix(filepath, fileStat) {
|
|
1145
1632
|
const cached = this.prefixCache.get(filepath);
|
|
1146
1633
|
if (!cached) return void 0;
|
|
1147
|
-
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;
|
|
1148
1635
|
return cached;
|
|
1149
1636
|
}
|
|
1150
1637
|
async cachedSummaryIsFresh(filepath, cached) {
|
|
@@ -1152,7 +1639,7 @@ var PiSessionStore = class {
|
|
|
1152
1639
|
if (!linkedPiFile || resolve(linkedPiFile) === resolve(filepath)) return true;
|
|
1153
1640
|
try {
|
|
1154
1641
|
const linkedStat = await fsStat2(linkedPiFile);
|
|
1155
|
-
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);
|
|
1156
1643
|
} catch {
|
|
1157
1644
|
return cached.linkedMtimeMs === void 0 && cached.linkedSize === void 0;
|
|
1158
1645
|
}
|
|
@@ -1160,15 +1647,29 @@ var PiSessionStore = class {
|
|
|
1160
1647
|
async readPrefixCache(filepath, fileStat) {
|
|
1161
1648
|
const cached = this.cachedPrefix(filepath, fileStat);
|
|
1162
1649
|
if (cached) return cached;
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
+
}
|
|
1172
1673
|
return entry;
|
|
1173
1674
|
}
|
|
1174
1675
|
async summarizeVisiblePage(visibleFiles, options) {
|
|
@@ -1176,15 +1677,19 @@ var PiSessionStore = class {
|
|
|
1176
1677
|
const page = [];
|
|
1177
1678
|
let validSeen = 0;
|
|
1178
1679
|
let index = 0;
|
|
1179
|
-
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
|
+
);
|
|
1180
1684
|
while (index < visibleFiles.length && (options.limit === void 0 || page.length < options.limit)) {
|
|
1181
1685
|
const batch = visibleFiles.slice(index, index + batchSize);
|
|
1182
1686
|
index += batch.length;
|
|
1183
1687
|
const summaries = await Promise.all(
|
|
1184
1688
|
batch.map(({ filepath, stat }) => this.summarizeFile(options.ctx, filepath, stat))
|
|
1185
1689
|
);
|
|
1186
|
-
for (const
|
|
1187
|
-
if (!
|
|
1690
|
+
for (const rawSummary of summaries) {
|
|
1691
|
+
if (!rawSummary) continue;
|
|
1692
|
+
const summary = withArchivedFlag(rawSummary, options.archivedIds);
|
|
1188
1693
|
if (summary.turnCount === 0 && !options.includeEmpty && summary.id !== options.includeId) {
|
|
1189
1694
|
continue;
|
|
1190
1695
|
}
|
|
@@ -1213,7 +1718,7 @@ var PiSessionStore = class {
|
|
|
1213
1718
|
try {
|
|
1214
1719
|
const files = readdirSync(this.sessionDir).filter((file) => file.endsWith(".jsonl"));
|
|
1215
1720
|
for (const file of files) {
|
|
1216
|
-
const filepath =
|
|
1721
|
+
const filepath = join2(this.sessionDir, file);
|
|
1217
1722
|
if (resolve(filepath) === resolvedNativePath) continue;
|
|
1218
1723
|
try {
|
|
1219
1724
|
const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries2(readJsonlPrefixSync(filepath)));
|
|
@@ -1228,10 +1733,10 @@ var PiSessionStore = class {
|
|
|
1228
1733
|
}
|
|
1229
1734
|
async findWrapperReferencingNativeSession(nativePath) {
|
|
1230
1735
|
const resolvedNativePath = resolve(nativePath);
|
|
1231
|
-
const files = await
|
|
1736
|
+
const files = await readdir2(this.sessionDir).catch(() => []);
|
|
1232
1737
|
for (const file of files) {
|
|
1233
1738
|
if (!file.endsWith(".jsonl")) continue;
|
|
1234
|
-
const filepath =
|
|
1739
|
+
const filepath = join2(this.sessionDir, file);
|
|
1235
1740
|
if (resolve(filepath) === resolvedNativePath) continue;
|
|
1236
1741
|
try {
|
|
1237
1742
|
const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries2(await readJsonlPrefix2(filepath)));
|
|
@@ -1242,7 +1747,7 @@ var PiSessionStore = class {
|
|
|
1242
1747
|
return null;
|
|
1243
1748
|
}
|
|
1244
1749
|
ensureWrapperForNativeSessionSync(sessionId, nativePath, entries, ctx) {
|
|
1245
|
-
const wrapperPath =
|
|
1750
|
+
const wrapperPath = join2(this.sessionDir, `${sessionId}.jsonl`);
|
|
1246
1751
|
if (resolve(wrapperPath) === resolve(nativePath)) return wrapperPath;
|
|
1247
1752
|
try {
|
|
1248
1753
|
readFileSync(wrapperPath, "utf-8");
|
|
@@ -1262,7 +1767,7 @@ var PiSessionStore = class {
|
|
|
1262
1767
|
return wrapperPath;
|
|
1263
1768
|
}
|
|
1264
1769
|
async ensureWrapperForNativeSession(sessionId, nativePath, ctx) {
|
|
1265
|
-
const wrapperPath =
|
|
1770
|
+
const wrapperPath = join2(this.sessionDir, `${sessionId}.jsonl`);
|
|
1266
1771
|
if (resolve(wrapperPath) === resolve(nativePath)) return wrapperPath;
|
|
1267
1772
|
try {
|
|
1268
1773
|
await fsStat2(wrapperPath);
|
|
@@ -1271,7 +1776,7 @@ var PiSessionStore = class {
|
|
|
1271
1776
|
}
|
|
1272
1777
|
const entries = parseJsonlPrefixEntries2(await readJsonlPrefix2(nativePath));
|
|
1273
1778
|
try {
|
|
1274
|
-
await
|
|
1779
|
+
await writeFile2(
|
|
1275
1780
|
wrapperPath,
|
|
1276
1781
|
buildNativePiSessionWrapper(sessionId, this.cwd, nativePath, entries, ctx),
|
|
1277
1782
|
{ encoding: "utf-8", flag: "wx" }
|
|
@@ -1283,27 +1788,67 @@ var PiSessionStore = class {
|
|
|
1283
1788
|
return wrapperPath;
|
|
1284
1789
|
}
|
|
1285
1790
|
async readLinkedPiSession(filepath) {
|
|
1791
|
+
let last;
|
|
1792
|
+
let parsed;
|
|
1286
1793
|
try {
|
|
1287
|
-
|
|
1288
|
-
fsStat2(filepath)
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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;
|
|
1292
1820
|
} catch {
|
|
1293
1821
|
return null;
|
|
1294
1822
|
}
|
|
1295
1823
|
}
|
|
1296
|
-
async
|
|
1824
|
+
async readTranscriptSummary(filepath) {
|
|
1825
|
+
let last;
|
|
1826
|
+
let summary;
|
|
1297
1827
|
try {
|
|
1298
|
-
|
|
1299
|
-
fsStat2(filepath)
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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;
|
|
1303
1836
|
} catch {
|
|
1304
1837
|
return null;
|
|
1305
1838
|
}
|
|
1306
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
|
+
}
|
|
1307
1852
|
headerBelongsToCtx(header, ctx, directNative = false) {
|
|
1308
1853
|
if (directNative) return this.nativeFileBelongsToCtx(header, ctx);
|
|
1309
1854
|
return header ? this.storedCtxBelongsToCtx(readHeaderSessionCtx(header), ctx) : isEmptySessionCtx(ctx);
|
|
@@ -1343,7 +1888,7 @@ var PiSessionStore = class {
|
|
|
1343
1888
|
}
|
|
1344
1889
|
};
|
|
1345
1890
|
async function readJsonlPrefix2(filepath, maxBytes = SUMMARY_PREFIX_BYTES2) {
|
|
1346
|
-
const handle = await
|
|
1891
|
+
const handle = await open3(filepath, "r");
|
|
1347
1892
|
try {
|
|
1348
1893
|
const buffer = Buffer.alloc(maxBytes);
|
|
1349
1894
|
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
@@ -1431,39 +1976,23 @@ function extractSessionHeaderId(entries) {
|
|
|
1431
1976
|
function nativeSessionFilename(sessionId, isoTimestamp) {
|
|
1432
1977
|
return `${isoTimestamp.replace(/[:.]/g, "-")}_${sessionId}.jsonl`;
|
|
1433
1978
|
}
|
|
1979
|
+
function wrapperSessionId(filepath) {
|
|
1980
|
+
const base = basename(filepath);
|
|
1981
|
+
return base.endsWith(".jsonl") ? base.slice(0, -".jsonl".length) : base;
|
|
1982
|
+
}
|
|
1434
1983
|
function isTimestampNamedPiSessionFile(filepath, sessionId) {
|
|
1435
1984
|
const filename = basename(filepath);
|
|
1436
1985
|
return /^\d{4}-\d{2}-\d{2}/.test(filename) && filename.endsWith(`_${sessionId}.jsonl`);
|
|
1437
1986
|
}
|
|
1438
|
-
function
|
|
1439
|
-
return
|
|
1440
|
-
(e) => e.type === "message" && e.message?.role === "user"
|
|
1441
|
-
).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;
|
|
1442
1989
|
}
|
|
1443
|
-
function
|
|
1444
|
-
return
|
|
1445
|
-
(entry) => entry.type === "message" && entry.message.role === "assistant"
|
|
1446
|
-
);
|
|
1990
|
+
function selectTranscriptSummary(wrapper, native) {
|
|
1991
|
+
return native && native.entryCount > 0 ? native : wrapper;
|
|
1447
1992
|
}
|
|
1448
|
-
function
|
|
1449
|
-
const
|
|
1450
|
-
|
|
1451
|
-
...nativeEntries.flatMap((entry, offset) => entry.type === "session_info" ? [{ entry, native: 1, offset }] : [])
|
|
1452
|
-
];
|
|
1453
|
-
candidates.sort((a, b) => {
|
|
1454
|
-
const timestamp = Date.parse(b.entry.timestamp) - Date.parse(a.entry.timestamp);
|
|
1455
|
-
return timestamp || b.native - a.native || b.offset - a.offset;
|
|
1456
|
-
});
|
|
1457
|
-
return candidates[0]?.entry.name;
|
|
1458
|
-
}
|
|
1459
|
-
function firstUserMessage(entries) {
|
|
1460
|
-
for (const e of entries) {
|
|
1461
|
-
if (e.type !== "message") continue;
|
|
1462
|
-
const msg = e.message;
|
|
1463
|
-
if (msg?.role !== "user") continue;
|
|
1464
|
-
const text = textFromPiContent2(msg.content);
|
|
1465
|
-
if (text) return text.slice(0, 80);
|
|
1466
|
-
}
|
|
1993
|
+
function projectedTranscriptTitle(wrapper, native) {
|
|
1994
|
+
const selected = selectTranscriptSummary(wrapper, native);
|
|
1995
|
+
return wrapper.userTitle ?? selected.title ?? wrapper.title ?? native?.title ?? selected.firstUserTitle;
|
|
1467
1996
|
}
|
|
1468
1997
|
function safeParseEntries(content) {
|
|
1469
1998
|
try {
|
|
@@ -1491,14 +2020,6 @@ function parseJsonlPrefixEntries2(content) {
|
|
|
1491
2020
|
}
|
|
1492
2021
|
return entries;
|
|
1493
2022
|
}
|
|
1494
|
-
function textFromPiContent2(content) {
|
|
1495
|
-
if (typeof content === "string") return content;
|
|
1496
|
-
if (!Array.isArray(content)) return "";
|
|
1497
|
-
return content.map((part) => {
|
|
1498
|
-
const item = part;
|
|
1499
|
-
return item?.type === "text" && typeof item.text === "string" ? item.text : "";
|
|
1500
|
-
}).join("");
|
|
1501
|
-
}
|
|
1502
2023
|
function withStableMessageId(message, entryId) {
|
|
1503
2024
|
if (!entryId || !message || typeof message !== "object" || Array.isArray(message)) return message;
|
|
1504
2025
|
if (typeof message.id === "string") return message;
|
|
@@ -1599,7 +2120,7 @@ function readApiKeyEnv(candidates) {
|
|
|
1599
2120
|
function buildOpenAICompatibleProviderConfig(opts) {
|
|
1600
2121
|
return {
|
|
1601
2122
|
baseUrl: opts.baseUrl,
|
|
1602
|
-
//
|
|
2123
|
+
// Pi resolves `$ENV_VAR` config references through
|
|
1603
2124
|
// the provider environment. A bare name is a literal API key, which would
|
|
1604
2125
|
// send "INFOMANIAK_API_TOKEN" as the bearer token and cause provider 401s.
|
|
1605
2126
|
apiKey: `$${opts.apiKeyEnv}`,
|
|
@@ -1730,6 +2251,17 @@ function readConfiguredDefaultModel() {
|
|
|
1730
2251
|
return readPiSettingsDefaultModel() ?? readInfomaniakProvider()?.defaultModel ?? readCustomProvider()?.defaultModel;
|
|
1731
2252
|
}
|
|
1732
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
|
+
|
|
1733
2265
|
// src/server/piPackages.ts
|
|
1734
2266
|
var PI_PACKAGE_RESOURCE_FILTERS = [
|
|
1735
2267
|
"extensions",
|
|
@@ -1774,7 +2306,7 @@ function mergePiPackageSources(base = [], additional = []) {
|
|
|
1774
2306
|
|
|
1775
2307
|
// src/server/harness/pi-coding-agent/resourceSettingsManager.ts
|
|
1776
2308
|
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
1777
|
-
import { join as
|
|
2309
|
+
import { join as join3 } from "node:path";
|
|
1778
2310
|
import { SettingsManager as SettingsManager2 } from "@mariozechner/pi-coding-agent";
|
|
1779
2311
|
function readSettingsFileIfPresent(path) {
|
|
1780
2312
|
return existsSync(path) ? readFileSync2(path, "utf-8") : void 0;
|
|
@@ -1789,8 +2321,8 @@ function mergeInjectedProjectPackages(settingsJson, piPackages, includePackage)
|
|
|
1789
2321
|
}
|
|
1790
2322
|
function createResourceSettingsManager(cwd, agentDir, piPackages, options = {}) {
|
|
1791
2323
|
if (piPackages.length === 0 && !options.includePackage) return SettingsManager2.create(cwd, agentDir);
|
|
1792
|
-
const globalSettingsPath =
|
|
1793
|
-
const projectSettingsPath =
|
|
2324
|
+
const globalSettingsPath = join3(agentDir, "settings.json");
|
|
2325
|
+
const projectSettingsPath = join3(cwd, ".pi", "settings.json");
|
|
1794
2326
|
let globalSettingsOverrideJson;
|
|
1795
2327
|
let projectSettingsOverrideJson;
|
|
1796
2328
|
const storage = {
|
|
@@ -1908,11 +2440,11 @@ function meteredExtensionCommandContext(ctx, command) {
|
|
|
1908
2440
|
guarded.switchSession = async () => block();
|
|
1909
2441
|
return guarded;
|
|
1910
2442
|
}
|
|
1911
|
-
function resolveRequestedModel(
|
|
2443
|
+
function resolveRequestedModel(modelRuntime, input, options = {}) {
|
|
1912
2444
|
const requestedId = input.model?.id;
|
|
1913
2445
|
if (!input.model || !requestedId) return void 0;
|
|
1914
|
-
const model =
|
|
1915
|
-
const available =
|
|
2446
|
+
const model = modelRuntime.getModel(input.model.provider, requestedId);
|
|
2447
|
+
const available = modelRuntime.getAvailableSnapshot();
|
|
1916
2448
|
const hasAuth = Boolean(model) && available.some(
|
|
1917
2449
|
(m) => m.provider === model.provider && m.id === model.id
|
|
1918
2450
|
);
|
|
@@ -1922,13 +2454,13 @@ function resolveRequestedModel(modelRegistry, input, options = {}) {
|
|
|
1922
2454
|
}
|
|
1923
2455
|
return model;
|
|
1924
2456
|
}
|
|
1925
|
-
function resolveDefaultModel(
|
|
2457
|
+
function resolveDefaultModel(modelRuntime, override, strict) {
|
|
1926
2458
|
if (override) {
|
|
1927
|
-
return resolveRequestedModel(
|
|
2459
|
+
return resolveRequestedModel(modelRuntime, { model: override }, { strict });
|
|
1928
2460
|
}
|
|
1929
2461
|
const configured = readConfiguredDefaultModel();
|
|
1930
2462
|
if (configured) {
|
|
1931
|
-
const model =
|
|
2463
|
+
const model = modelRuntime.getModel(configured.provider, configured.id);
|
|
1932
2464
|
if (model) return model;
|
|
1933
2465
|
}
|
|
1934
2466
|
return void 0;
|
|
@@ -1947,7 +2479,7 @@ function sessionCacheKey(sessionId, ctx) {
|
|
|
1947
2479
|
return JSON.stringify([sessionId, ctx.workspaceId ?? ""]);
|
|
1948
2480
|
}
|
|
1949
2481
|
async function applyRequestedSessionOptions(handle, input, options = {}) {
|
|
1950
|
-
const requestedModel = resolveRequestedModel(handle.
|
|
2482
|
+
const requestedModel = resolveRequestedModel(handle.modelRuntime, input, { strict: options.strictModelResolution });
|
|
1951
2483
|
if (requestedModel) {
|
|
1952
2484
|
const current = handle.piSession.model;
|
|
1953
2485
|
if (!current || current.provider !== requestedModel.provider || current.id !== requestedModel.id) {
|
|
@@ -2079,10 +2611,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
2079
2611
|
return await runContextStorage.run(ctx, run);
|
|
2080
2612
|
}
|
|
2081
2613
|
function createRunBoundAdapter(handle, sessionId, ctx) {
|
|
2082
|
-
const adapter = createPiAgentSessionAdapter(handle.piSession, {
|
|
2083
|
-
sessionId,
|
|
2084
|
-
...handle.piSession.agent && typeof handle.piSession.agent.continue === "function" ? { continueQueuedFollowUp: () => handle.piSession.agent.continue() } : {}
|
|
2085
|
-
});
|
|
2614
|
+
const adapter = createPiAgentSessionAdapter(handle.piSession, { sessionId });
|
|
2086
2615
|
return {
|
|
2087
2616
|
...adapter,
|
|
2088
2617
|
prompt: (promptInput) => bindRunContext(ctx, () => adapter.prompt(promptInput)),
|
|
@@ -2092,11 +2621,9 @@ function createPiCodingAgentHarness(opts) {
|
|
|
2092
2621
|
};
|
|
2093
2622
|
}
|
|
2094
2623
|
async function createPiSession(sessionId, sessionCtx, input, ctx) {
|
|
2095
|
-
const
|
|
2096
|
-
const
|
|
2097
|
-
|
|
2098
|
-
const resolvedModel = resolveRequestedModel(modelRegistry, input, { strict: pi.strictModelResolution });
|
|
2099
|
-
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);
|
|
2100
2627
|
const savedPiFile = await sessionStore.loadPiSessionFile(sessionCtx, sessionId);
|
|
2101
2628
|
const runtimeCwd = opts.runtimeCwd ?? ctx.workdir;
|
|
2102
2629
|
if (!savedPiFile) {
|
|
@@ -2170,8 +2697,8 @@ function createPiCodingAgentHarness(opts) {
|
|
|
2170
2697
|
model,
|
|
2171
2698
|
thinkingLevel: input.thinkingLevel ?? "off",
|
|
2172
2699
|
sessionManager,
|
|
2173
|
-
|
|
2174
|
-
|
|
2700
|
+
modelRuntime,
|
|
2701
|
+
settingsManager,
|
|
2175
2702
|
...resourceLoader ? { resourceLoader } : {}
|
|
2176
2703
|
});
|
|
2177
2704
|
const restoreFollowUpContextWrapper = rememberQueuedFollowUpRunContexts(piSession, runContextState, () => runContextStorage.getStore());
|
|
@@ -2182,7 +2709,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
2182
2709
|
};
|
|
2183
2710
|
const handle = {
|
|
2184
2711
|
piSession,
|
|
2185
|
-
|
|
2712
|
+
modelRuntime,
|
|
2186
2713
|
sessionManager,
|
|
2187
2714
|
resourceLoader,
|
|
2188
2715
|
sessionId,
|
|
@@ -2244,7 +2771,8 @@ function createPiCodingAgentHarness(opts) {
|
|
|
2244
2771
|
* the expected pre-first-turn state, not an error.
|
|
2245
2772
|
*/
|
|
2246
2773
|
getSystemPrompt(sessionId) {
|
|
2247
|
-
|
|
2774
|
+
const prompt = piSessionHandlesFor(sessionId)[0]?.piSession.systemPrompt;
|
|
2775
|
+
return prompt && pi.locateSkillResource ? projectSkillResourceLocations(prompt, pi.locateSkillResource) : prompt;
|
|
2248
2776
|
},
|
|
2249
2777
|
hasPiSession(sessionId, ctx) {
|
|
2250
2778
|
return ctx ? piSessions.has(sessionCacheKey(sessionId, ctx)) : piSessionHandlesFor(sessionId).length > 0;
|
|
@@ -2316,8 +2844,8 @@ export {
|
|
|
2316
2844
|
getEnv,
|
|
2317
2845
|
PiSessionStore,
|
|
2318
2846
|
parseEncodedModelSelection,
|
|
2319
|
-
registerConfiguredModelProviders,
|
|
2320
2847
|
readConfiguredDefaultModel,
|
|
2848
|
+
createConfiguredModelRuntime,
|
|
2321
2849
|
PI_PACKAGE_RESOURCE_FILTERS,
|
|
2322
2850
|
piPackageSourceKey,
|
|
2323
2851
|
compactPiPackages,
|