@hachej/boring-agent 0.1.103 → 0.1.105

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.
@@ -174,12 +174,17 @@ var QueuedUserMessageSchema = z.object({
174
174
  createdAt: z.string().optional()
175
175
  });
176
176
  var PiChatStatusSchema = z.enum(["idle", "hydrating", "submitted", "streaming", "aborting", "error"]);
177
+ var ChatModelSelectionSchema = z.object({
178
+ provider: nonEmptyString,
179
+ id: nonEmptyString
180
+ });
177
181
  var PiChatSnapshotSchema = z.object({
178
182
  protocolVersion: z.literal(1),
179
183
  sessionId: nonEmptyString,
180
184
  seq: seqNumber,
181
185
  status: PiChatStatusSchema,
182
186
  activeTurnId: z.string().optional(),
187
+ currentModel: ChatModelSelectionSchema.optional(),
183
188
  messages: z.array(BoringChatMessageSchema),
184
189
  queue: z.object({ followUps: z.array(QueuedUserMessageSchema) }),
185
190
  followUpMode: z.literal("one-at-a-time"),
@@ -188,6 +193,7 @@ var PiChatSnapshotSchema = z.object({
188
193
  var baseEvent = z.object({ seq: seqNumber });
189
194
  var PiChatEventSchema = z.discriminatedUnion("type", [
190
195
  baseEvent.extend({ type: z.literal("agent-start"), turnId: nonEmptyString }),
196
+ baseEvent.extend({ type: z.literal("model-changed"), currentModel: ChatModelSelectionSchema }),
191
197
  baseEvent.extend({ type: z.literal("agent-end"), turnId: nonEmptyString, status: z.enum(["ok", "aborted", "error"]), willRetry: z.boolean().optional() }),
192
198
  baseEvent.extend({
193
199
  type: z.literal("message-start"),
@@ -258,10 +264,6 @@ var PiChatEventSchema = z.discriminatedUnion("type", [
258
264
  ]);
259
265
  var PiChatHeartbeatFrameSchema = z.object({ type: z.literal("heartbeat"), now: z.string() });
260
266
  var PiChatStreamFrameSchema = z.union([PiChatEventSchema, PiChatHeartbeatFrameSchema]);
261
- var ChatModelSelectionSchema = z.object({
262
- provider: nonEmptyString,
263
- id: nonEmptyString
264
- });
265
267
  var ThinkingLevelSchema = z.enum(["off", "low", "medium", "high"]);
266
268
  var ChatAttachmentPayloadSchema = z.object({
267
269
  filename: z.string().optional(),
@@ -330,11 +332,11 @@ export {
330
332
  BoringChatMessageSchema,
331
333
  QueuedUserMessageSchema,
332
334
  PiChatStatusSchema,
335
+ ChatModelSelectionSchema,
333
336
  PiChatSnapshotSchema,
334
337
  PiChatEventSchema,
335
338
  PiChatHeartbeatFrameSchema,
336
339
  PiChatStreamFrameSchema,
337
- ChatModelSelectionSchema,
338
340
  ThinkingLevelSchema,
339
341
  ChatAttachmentPayloadSchema,
340
342
  PromptPayloadSchema,
@@ -567,6 +567,28 @@ async function summarizeNativeTranscript(filepath) {
567
567
  }
568
568
 
569
569
  // src/server/harness/pi-coding-agent/sessions.ts
570
+ function legacyAssistantModel(entry) {
571
+ const message = entry.message;
572
+ if (message.role !== "assistant") return void 0;
573
+ if (typeof message.provider === "string" && typeof message.model === "string") {
574
+ return { provider: message.provider, id: message.model };
575
+ }
576
+ if (typeof message.model !== "object" || message.model === null) return void 0;
577
+ const model = message.model;
578
+ return typeof model.provider === "string" && typeof model.id === "string" ? { provider: model.provider, id: model.id } : void 0;
579
+ }
580
+ function currentModelFromTranscript(entries) {
581
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
582
+ const entry = entries[index];
583
+ if (!entry) continue;
584
+ if (entry.type === "model_change") return { provider: entry.provider, id: entry.modelId };
585
+ if (entry.type === "message") {
586
+ const model = legacyAssistantModel(entry);
587
+ if (model) return model;
588
+ }
589
+ }
590
+ return void 0;
591
+ }
570
592
  function sessionBaseDir(explicitRoot) {
571
593
  const explicit = explicitRoot?.trim();
572
594
  if (explicit) return resolve(explicit);
@@ -666,11 +688,8 @@ var PiSessionStore = class {
666
688
  }));
667
689
  const existingFiles = fileStats.filter((item) => item !== null);
668
690
  const referencedPiFiles = await this.referencedPiFiles(existingFiles);
669
- const visibleFiles = await Promise.all(existingFiles.filter(({ filepath }) => !referencedPiFiles.has(resolve(filepath))).map(async (file) => ({
670
- ...file,
671
- sortMtimeMs: await this.sessionSortMtimeMs(file)
672
- })));
673
- visibleFiles.sort((a, b) => b.sortMtimeMs - a.sortMtimeMs);
691
+ 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) => b.sortMtimeMs - a.sortMtimeMs || a.sortId.localeCompare(b.sortId));
674
693
  const { offset, limit } = options;
675
694
  const includeId = options.includeId;
676
695
  const pageSummaries = await this.summarizeVisiblePage(visibleFiles, {
@@ -745,7 +764,11 @@ var PiSessionStore = class {
745
764
  async loadEntries(ctx, sessionId) {
746
765
  const resolved = await this.resolveSessionTranscript(ctx, sessionId);
747
766
  const messages = resolved.transcriptEntries.filter((entry) => entry.type === "message").map((entry) => withStableMessageId(entry.message, entry.id));
748
- return { id: resolved.resolvedSessionId, messages };
767
+ return {
768
+ id: resolved.resolvedSessionId,
769
+ messages,
770
+ currentModel: currentModelFromTranscript(resolved.transcriptEntries)
771
+ };
749
772
  }
750
773
  async loadAttachment(ctx, sessionId, messageId, index) {
751
774
  const resolved = await this.resolveSessionTranscript(ctx, sessionId);
@@ -941,8 +964,8 @@ var PiSessionStore = class {
941
964
  if (readHeaderSessionCtx(header) !== null) return null;
942
965
  const existingWrapper = await this.findWrapperReferencingNativeSession(filepath);
943
966
  if (existingWrapper) {
944
- const wrapperSessionId = await this.readSessionFileId(existingWrapper);
945
- if (wrapperSessionId !== sessionId) return null;
967
+ const wrapperSessionId2 = await this.readSessionFileId(existingWrapper);
968
+ if (wrapperSessionId2 !== sessionId) return null;
946
969
  const wrapperEntries = parseJsonlPrefixEntries2(await readJsonlPrefix2(existingWrapper));
947
970
  const wrapperHeader = wrapperEntries.find((entry) => entry.type === "session");
948
971
  if (!this.headerBelongsToCtx(wrapperHeader, ctx)) return null;
@@ -1043,21 +1066,37 @@ var PiSessionStore = class {
1043
1066
  }));
1044
1067
  return referenced;
1045
1068
  }
1046
- async sessionSortMtimeMs({ filepath, stat }) {
1069
+ async sessionSortKey({ filepath, stat }) {
1047
1070
  let sortMtimeMs = stat.mtime.getTime();
1071
+ let headerId = void 0;
1072
+ const stemFallback = () => ({
1073
+ sortMtimeMs,
1074
+ sortId: wrapperSessionId(filepath)
1075
+ });
1048
1076
  try {
1049
- const linkedPiFile = (await this.readPrefixCache(filepath, stat)).referencedPiFile;
1077
+ const cached = await this.readPrefixCache(filepath, stat);
1078
+ headerId = cached.headerId ?? void 0;
1079
+ const linkedPiFile = cached.referencedPiFile;
1050
1080
  if (linkedPiFile && resolve(linkedPiFile) !== resolve(filepath)) {
1051
1081
  const linkedStat = await fsStat2(linkedPiFile);
1052
1082
  sortMtimeMs = Math.max(sortMtimeMs, linkedStat.mtime.getTime());
1053
1083
  }
1054
- const header = parseJsonlPrefixEntries2(await readJsonlPrefix2(filepath)).find((entry) => entry.type === "session");
1055
- if (header && isTimestampNamedPiSessionFile(filepath, header.id)) {
1056
- sortMtimeMs = await latestNativeMessageTimestamp(filepath, Number(stat.size)) ?? sortMtimeMs;
1084
+ if (!headerId) {
1085
+ return stemFallback();
1086
+ }
1087
+ if (!isTimestampNamedPiSessionFile(filepath, headerId)) {
1088
+ return { sortMtimeMs, sortId: headerId };
1057
1089
  }
1090
+ if (cached.nativeSortMtimeMs !== void 0) {
1091
+ return { sortMtimeMs: cached.nativeSortMtimeMs, sortId: headerId };
1092
+ }
1093
+ const latest = await latestNativeMessageTimestamp(filepath, Number(stat.size));
1094
+ if (latest === void 0) return { sortMtimeMs, sortId: headerId };
1095
+ this.prefixCache.set(filepath, { ...cached, nativeSortMtimeMs: latest });
1096
+ return { sortMtimeMs: latest, sortId: headerId };
1058
1097
  } catch {
1098
+ return headerId ? { sortMtimeMs, sortId: headerId } : stemFallback();
1059
1099
  }
1060
- return sortMtimeMs;
1061
1100
  }
1062
1101
  async summarizeFile(ctx, filepath, existingStat) {
1063
1102
  try {
@@ -1106,6 +1145,10 @@ var PiSessionStore = class {
1106
1145
  mtimeMs: fileStat.mtime.getTime(),
1107
1146
  size: Number(fileStat.size),
1108
1147
  referencedPiFile: linkedPiFile,
1148
+ // Carried so a later `sessionSortMtimeMs` on this same entry still
1149
+ // recognises a native transcript instead of falling back to mtime.
1150
+ headerId: header.id,
1151
+ ...cached?.nativeSortMtimeMs !== void 0 ? { nativeSortMtimeMs: cached.nativeSortMtimeMs } : {},
1109
1152
  sessionCtx,
1110
1153
  ...linked ? { linkedMtimeMs: linked.mtime.getTime(), linkedSize: linked.size } : {},
1111
1154
  ...!nativeSummary ? { summary } : {}
@@ -1136,11 +1179,13 @@ var PiSessionStore = class {
1136
1179
  if (cached) return cached;
1137
1180
  const content = await readJsonlPrefix2(filepath);
1138
1181
  const entries = parseJsonlPrefixEntries2(content);
1182
+ const header = entries.find((item) => item.type === "session");
1139
1183
  const entry = {
1140
1184
  mtimeMs: fileStat.mtime.getTime(),
1141
1185
  size: Number(fileStat.size),
1142
1186
  referencedPiFile: extractPiSessionFilePath(entries),
1143
- sessionCtx: readHeaderSessionCtx(entries.find((item) => item.type === "session"))
1187
+ headerId: header?.id ?? null,
1188
+ sessionCtx: readHeaderSessionCtx(header)
1144
1189
  };
1145
1190
  this.prefixCache.set(filepath, entry);
1146
1191
  return entry;
@@ -1405,6 +1450,10 @@ function extractSessionHeaderId(entries) {
1405
1450
  function nativeSessionFilename(sessionId, isoTimestamp) {
1406
1451
  return `${isoTimestamp.replace(/[:.]/g, "-")}_${sessionId}.jsonl`;
1407
1452
  }
1453
+ function wrapperSessionId(filepath) {
1454
+ const base = basename(filepath);
1455
+ return base.endsWith(".jsonl") ? base.slice(0, -".jsonl".length) : base;
1456
+ }
1408
1457
  function isTimestampNamedPiSessionFile(filepath, sessionId) {
1409
1458
  const filename = basename(filepath);
1410
1459
  return /^\d{4}-\d{2}-\d{2}/.test(filename) && filename.endsWith(`_${sessionId}.jsonl`);