@ai-agent-forge/agent-forge 0.86.2 → 0.86.3

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.
Files changed (132) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/bundle/.build-manifest.json +151 -145
  3. package/dist/bundle/chunks/{anthropic-messages-RJYU4TCY.js → anthropic-messages-EVFQ6MLR.js} +3 -3
  4. package/dist/bundle/chunks/{azure-openai-responses-LESTCUL7.js → azure-openai-responses-WQCZWFAH.js} +1 -1
  5. package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
  6. package/dist/bundle/chunks/{chunk-MDOTREMR.js → chunk-3WE3W52L.js} +2 -2
  7. package/dist/bundle/chunks/chunk-F6SBZUJG.js +141 -0
  8. package/dist/bundle/chunks/{chunk-SER2HCLE.js → chunk-N6BQJSGH.js} +1 -1
  9. package/dist/bundle/chunks/chunk-RK2X2KU2.js +1442 -0
  10. package/dist/bundle/chunks/chunk-TBPTHASB.js +2 -0
  11. package/dist/bundle/chunks/github-copilot.js +1 -1
  12. package/dist/bundle/chunks/{google-generative-ai-NZ66X6K7.js → google-generative-ai-ISTTUZMW.js} +1 -1
  13. package/dist/bundle/chunks/{google-vertex-2ZZOFBNQ.js → google-vertex-NS7ZF7DK.js} +1 -1
  14. package/dist/bundle/chunks/jiti-static-loader-CDSTNEDU.js +2 -0
  15. package/dist/bundle/chunks/{mistral-conversations-B6DZRMEK.js → mistral-conversations-GINO6Z4I.js} +1 -1
  16. package/dist/bundle/chunks/{openai-codex-responses-UFH6O5K6.js → openai-codex-responses-YDJSZOIH.js} +1 -1
  17. package/dist/bundle/chunks/{openai-completions-A55GHA4O.js → openai-completions-UPRWTQPT.js} +3 -3
  18. package/dist/bundle/chunks/openai-responses-O63W6HQ3.js +2 -0
  19. package/dist/bundle/chunks/virtual-modules-O72ICTMQ.js +2 -0
  20. package/dist/bundle/cli.js +1 -1
  21. package/dist/bundle/index.js +1 -1
  22. package/dist/bundle/rpc-entry.js +1 -1
  23. package/dist/cli/args.d.ts.map +1 -1
  24. package/dist/cli/args.js +14 -4
  25. package/dist/cli/args.js.map +1 -1
  26. package/dist/cli/session-picker.d.ts +1 -1
  27. package/dist/cli/session-picker.d.ts.map +1 -1
  28. package/dist/cli/session-picker.js +8 -2
  29. package/dist/cli/session-picker.js.map +1 -1
  30. package/dist/core/agent-session-compaction.d.ts +2 -2
  31. package/dist/core/agent-session-compaction.d.ts.map +1 -1
  32. package/dist/core/agent-session-compaction.js +120 -44
  33. package/dist/core/agent-session-compaction.js.map +1 -1
  34. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  35. package/dist/core/agent-session-runtime.js +1 -1
  36. package/dist/core/agent-session-runtime.js.map +1 -1
  37. package/dist/core/agent-session.d.ts +13 -4
  38. package/dist/core/agent-session.d.ts.map +1 -1
  39. package/dist/core/agent-session.js +91 -93
  40. package/dist/core/agent-session.js.map +1 -1
  41. package/dist/core/compaction/compaction.d.ts +15 -0
  42. package/dist/core/compaction/compaction.d.ts.map +1 -1
  43. package/dist/core/compaction/compaction.js +20 -1
  44. package/dist/core/compaction/compaction.js.map +1 -1
  45. package/dist/core/export-html/template.css +10 -0
  46. package/dist/core/export-html/template.js +50 -17
  47. package/dist/core/extensions/capability-plugin.d.ts.map +1 -1
  48. package/dist/core/extensions/capability-plugin.js +25 -30
  49. package/dist/core/extensions/capability-plugin.js.map +1 -1
  50. package/dist/core/extensions/jiti-static-loader.d.ts +2 -0
  51. package/dist/core/extensions/jiti-static-loader.d.ts.map +1 -0
  52. package/dist/core/extensions/jiti-static-loader.js +5 -0
  53. package/dist/core/extensions/jiti-static-loader.js.map +1 -0
  54. package/dist/core/extensions/virtual-modules.d.ts +3 -0
  55. package/dist/core/extensions/virtual-modules.d.ts.map +1 -0
  56. package/dist/core/extensions/virtual-modules.js +32 -0
  57. package/dist/core/extensions/virtual-modules.js.map +1 -0
  58. package/dist/core/facade/coding-agent-facade.d.ts +3 -3
  59. package/dist/core/facade/coding-agent-facade.d.ts.map +1 -1
  60. package/dist/core/facade/coding-agent-facade.js.map +1 -1
  61. package/dist/core/facade/coding-agent-runtime-facade.d.ts +2 -2
  62. package/dist/core/facade/coding-agent-runtime-facade.d.ts.map +1 -1
  63. package/dist/core/facade/coding-agent-runtime-facade.js +4 -4
  64. package/dist/core/facade/coding-agent-runtime-facade.js.map +1 -1
  65. package/dist/core/model-resolver.d.ts.map +1 -1
  66. package/dist/core/model-resolver.js +2 -2
  67. package/dist/core/model-resolver.js.map +1 -1
  68. package/dist/core/package-install-host.d.ts +1 -1
  69. package/dist/core/package-install-host.d.ts.map +1 -1
  70. package/dist/core/package-install-host.js +4 -3
  71. package/dist/core/package-install-host.js.map +1 -1
  72. package/dist/core/prompt-templates.d.ts +6 -1
  73. package/dist/core/prompt-templates.d.ts.map +1 -1
  74. package/dist/core/prompt-templates.js +61 -35
  75. package/dist/core/prompt-templates.js.map +1 -1
  76. package/dist/core/resource-loader.d.ts.map +1 -1
  77. package/dist/core/resource-loader.js +6 -2
  78. package/dist/core/resource-loader.js.map +1 -1
  79. package/dist/core/session-manager.d.ts +23 -4
  80. package/dist/core/session-manager.d.ts.map +1 -1
  81. package/dist/core/session-manager.js +146 -108
  82. package/dist/core/session-manager.js.map +1 -1
  83. package/dist/core/tools/bash.d.ts +2 -1
  84. package/dist/core/tools/bash.d.ts.map +1 -1
  85. package/dist/core/tools/bash.js +18 -3
  86. package/dist/core/tools/bash.js.map +1 -1
  87. package/dist/main.d.ts.map +1 -1
  88. package/dist/main.js +6 -7
  89. package/dist/main.js.map +1 -1
  90. package/dist/modes/interactive/components/session-selector.d.ts +5 -5
  91. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  92. package/dist/modes/interactive/components/session-selector.js +74 -50
  93. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  94. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  95. package/dist/modes/interactive/components/tool-execution.js +18 -9
  96. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  97. package/dist/modes/interactive/interactive-mode.d.ts +2 -2
  98. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  99. package/dist/modes/interactive/interactive-mode.js +35 -32
  100. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  101. package/dist/modes/rpc/rpc-client.d.ts +14 -6
  102. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  103. package/dist/modes/rpc/rpc-client.js +47 -10
  104. package/dist/modes/rpc/rpc-client.js.map +1 -1
  105. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  106. package/dist/modes/rpc/rpc-mode.js +9 -10
  107. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  108. package/dist/modes/rpc/rpc-types.d.ts +10 -1
  109. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  110. package/dist/modes/rpc/rpc-types.js.map +1 -1
  111. package/dist/remote/server-assembly.d.ts.map +1 -1
  112. package/dist/remote/server-assembly.js +1 -1
  113. package/dist/remote/server-assembly.js.map +1 -1
  114. package/dist/utils/clipboard-image.d.ts.map +1 -1
  115. package/dist/utils/clipboard-image.js +1 -12
  116. package/dist/utils/clipboard-image.js.map +1 -1
  117. package/dist/utils/clipboard.d.ts.map +1 -1
  118. package/dist/utils/clipboard.js +88 -13
  119. package/dist/utils/clipboard.js.map +1 -1
  120. package/dist/utils/mime.d.ts.map +1 -1
  121. package/dist/utils/mime.js +1 -1
  122. package/dist/utils/mime.js.map +1 -1
  123. package/dist/utils/wsl.d.ts +3 -0
  124. package/dist/utils/wsl.d.ts.map +1 -0
  125. package/dist/utils/wsl.js +15 -0
  126. package/dist/utils/wsl.js.map +1 -0
  127. package/docs/rpc.md +7 -5
  128. package/docs/sdk.md +9 -8
  129. package/package.json +1 -1
  130. package/dist/bundle/chunks/chunk-JR3Q27QP.js +0 -1581
  131. package/dist/bundle/chunks/chunk-KQEGUM34.js +0 -2
  132. package/dist/bundle/chunks/openai-responses-TJLXRAXR.js +0 -2
@@ -2,7 +2,7 @@ import { uuidv7 } from "@agent-forge/ai";
2
2
  import { randomUUID } from "crypto";
3
3
  import { createReadStream, existsSync, mkdirSync } from "fs";
4
4
  import { readdir, stat } from "fs/promises";
5
- import { join, resolve } from "path";
5
+ import { basename, join, resolve } from "path";
6
6
  import { createInterface } from "readline";
7
7
  import { isDeepStrictEqual } from "util";
8
8
  import { APP_NAME, getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
@@ -435,12 +435,16 @@ function findMostRecentSessionWithStorage(storage, sessionDir, cwd) {
435
435
  try {
436
436
  const files = storage
437
437
  .listSessionFiles(sessionDir)
438
- .map((path) => ({ path, header: readSessionHeaderForDiscovery(storage, path) }))
439
- .filter((file) => file.header !== null &&
440
- (!resolvedCwd || sessionCwdMatches(getSessionHeaderCwd(file.header), resolvedCwd)))
441
- .map(({ path }) => ({ path, mtime: storage.modifiedTime(path) }))
442
- .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
443
- return files[0]?.path || null;
438
+ .map((path) => ({ path, mtime: storage.modifiedTime(path).getTime() }))
439
+ .sort((a, b) => b.mtime - a.mtime);
440
+ // mtime-ordered short-circuit: only the newest files need a header read
441
+ // before one matches, instead of reading every header up front.
442
+ for (const { path } of files) {
443
+ const header = readSessionHeaderForDiscovery(storage, path);
444
+ if (header && (!resolvedCwd || sessionCwdMatches(getSessionHeaderCwd(header), resolvedCwd)))
445
+ return path;
446
+ }
447
+ return null;
444
448
  }
445
449
  catch {
446
450
  // Directory access and stat races make recent-session discovery unavailable.
@@ -477,9 +481,9 @@ function getMessageActivityTime(entry) {
477
481
  const t = new Date(entry.timestamp).getTime();
478
482
  return Number.isNaN(t) ? undefined : t;
479
483
  }
480
- async function buildSessionInfo(filePath) {
484
+ async function buildSessionInfo(filePath, signal, fileStats) {
481
485
  try {
482
- const stats = await stat(filePath);
486
+ const stats = fileStats ?? (await stat(filePath));
483
487
  let header = null;
484
488
  let messageCount = 0;
485
489
  let firstMessage = "";
@@ -487,7 +491,7 @@ async function buildSessionInfo(filePath) {
487
491
  let name;
488
492
  let lastActivityTime;
489
493
  const rl = createInterface({
490
- input: createReadStream(filePath, { encoding: "utf8" }),
494
+ input: createReadStream(filePath, { encoding: "utf8", signal }),
491
495
  crlfDelay: Infinity,
492
496
  });
493
497
  for await (const line of rl) {
@@ -554,67 +558,63 @@ async function buildSessionInfo(filePath) {
554
558
  };
555
559
  }
556
560
  catch {
561
+ signal?.throwIfAborted();
557
562
  return null;
558
563
  }
559
564
  }
560
565
  const MAX_CONCURRENT_SESSION_INFO_LOADS = 10;
561
- async function buildSessionInfosWithConcurrency(files, onLoaded) {
562
- const results = new Array(files.length).fill(null);
563
- const inFlight = new Set();
566
+ const MAX_CONCURRENT_SESSION_DISCOVERY_LOADS = 64;
567
+ const CURRENT_SESSION_LIST_PUBLISH_INTERVAL = 10;
568
+ const ALL_SESSION_LIST_PUBLISH_INTERVAL = 100;
569
+ async function mapWithConcurrency(items, limit, map, signal) {
570
+ const results = new Array(items.length);
564
571
  let nextIndex = 0;
565
- const startNext = () => {
566
- const index = nextIndex++;
567
- const file = files[index];
568
- if (!file)
569
- return;
570
- let task;
571
- task = buildSessionInfo(file)
572
- .then((info) => {
573
- results[index] = info;
574
- })
575
- .catch(() => {
576
- results[index] = null;
577
- })
578
- .finally(() => {
579
- inFlight.delete(task);
580
- onLoaded();
581
- });
582
- inFlight.add(task);
583
- };
584
- while (nextIndex < files.length || inFlight.size > 0) {
585
- while (nextIndex < files.length && inFlight.size < MAX_CONCURRENT_SESSION_INFO_LOADS) {
586
- startNext();
587
- }
588
- if (inFlight.size > 0) {
589
- await Promise.race(inFlight);
572
+ const worker = async () => {
573
+ while (nextIndex < items.length) {
574
+ signal?.throwIfAborted();
575
+ const index = nextIndex++;
576
+ results[index] = await map(items[index], index);
590
577
  }
591
- }
578
+ };
579
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
592
580
  return results;
593
581
  }
594
- async function listSessionsFromDir(dir, onProgress, progressOffset = 0, progressTotal) {
595
- const sessions = [];
596
- if (!existsSync(dir)) {
597
- return sessions;
598
- }
582
+ function sortSessionInfos(sessions) {
583
+ return sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
584
+ }
585
+ function buildSessionInfosWithConcurrency(files, onLoaded, signal) {
586
+ return mapWithConcurrency(files, MAX_CONCURRENT_SESSION_INFO_LOADS, async (file, index) => {
587
+ const info = await buildSessionInfo(file.path, signal, file.stats);
588
+ onLoaded(info, index);
589
+ return info;
590
+ }, signal);
591
+ }
592
+ async function listSessionsFromDir(dir, onProgress, signal) {
593
+ signal?.throwIfAborted();
594
+ if (!existsSync(dir))
595
+ return [];
599
596
  try {
600
597
  const dirEntries = await readdir(dir);
601
- const files = dirEntries.filter((f) => f.endsWith(".jsonl")).map((f) => join(dir, f));
602
- const total = progressTotal ?? files.length;
598
+ const files = dirEntries
599
+ .filter((file) => file.endsWith(".jsonl"))
600
+ .sort((a, b) => b.localeCompare(a))
601
+ .map((file) => ({ path: join(dir, file) }));
602
+ const total = files.length;
603
+ const partialSessions = [];
603
604
  let loaded = 0;
604
- const results = await buildSessionInfosWithConcurrency(files, () => {
605
+ const results = await buildSessionInfosWithConcurrency(files, (info) => {
605
606
  loaded++;
606
- onProgress?.(progressOffset + loaded, total);
607
- });
608
- for (const info of results) {
609
- if (info) {
610
- sessions.push(info);
611
- }
612
- }
607
+ if (info)
608
+ partialSessions.push(info);
609
+ const publishPartial = loaded === 1 || loaded % CURRENT_SESSION_LIST_PUBLISH_INTERVAL === 0 || loaded === total;
610
+ onProgress?.(loaded, total, publishPartial ? sortSessionInfos([...partialSessions]) : undefined);
611
+ }, signal);
612
+ return results.filter((info) => info !== null);
613
613
  }
614
614
  catch {
615
- // Return empty list on error
615
+ signal?.throwIfAborted();
616
+ return [];
616
617
  }
617
- return sessions;
618
618
  }
619
619
  /**
620
620
  * Manages conversation sessions as append-only trees stored in JSONL files.
@@ -809,22 +809,22 @@ export class SessionManager {
809
809
  leafId: this.leafId,
810
810
  };
811
811
  }
812
+ /**
813
+ * A new session file is created only once the session contains a user or assistant message.
814
+ * Setup entries alone (model, thinking level) stay in memory so opening and closing without
815
+ * chatting leaves no file behind. Starting at the user message (not the first assistant
816
+ * reply) keeps the prompt on disk if the first turn never completes.
817
+ */
818
+ _hasConversation() {
819
+ return this.fileEntries.some((e) => e.type === "message" && (e.message.role === "user" || e.message.role === "assistant"));
820
+ }
812
821
  _persist(entry) {
813
822
  if (!this.persist || !this.sessionFile)
814
823
  return;
815
824
  this.assertWritable();
816
- const hasAssistant = this.fileEntries.some((e) => e.type === "message" && e.message.role === "assistant");
817
- if (!hasAssistant) {
818
- if (this.flushed) {
819
- this.storage.append(this.sessionFile, entry);
820
- }
821
- else {
822
- // Mark as not flushed so when assistant arrives, all entries get written
823
- this.flushed = false;
824
- }
825
- return;
826
- }
827
825
  if (!this.flushed) {
826
+ if (!this._hasConversation())
827
+ return;
828
828
  this.storage.createExclusive(this.sessionFile, this.fileEntries);
829
829
  this.flushed = true;
830
830
  }
@@ -1374,13 +1374,9 @@ export class SessionManager {
1374
1374
  this.sessionId = newSessionId;
1375
1375
  this.sessionFile = newSessionFile;
1376
1376
  this._buildIndex();
1377
- // Only write the file now if it contains an assistant message.
1378
- // Otherwise defer to _persist(), which creates the file on the
1379
- // first assistant response, matching the newSession() contract
1380
- // and avoiding the duplicate-header bug when _persist()'s
1381
- // no-assistant guard later resets flushed to false.
1382
- const hasAssistant = this.fileEntries.some((e) => e.type === "message" && e.message.role === "assistant");
1383
- if (hasAssistant) {
1377
+ // Use the same rule as _persist(): write now if the branched path already
1378
+ // has a conversation, otherwise let _persist() create the file later.
1379
+ if (this._hasConversation()) {
1384
1380
  this._rewriteFile();
1385
1381
  this.flushed = true;
1386
1382
  }
@@ -1515,67 +1511,109 @@ export class SessionManager {
1515
1511
  ]);
1516
1512
  return new SessionManager(resolvedTargetCwd, dir, newSessionFile, true, undefined, undefined, storage);
1517
1513
  }
1514
+ /**
1515
+ * Find an exact session ID without loading transcript bodies.
1516
+ * @param cwd Working directory (used to compute default session directory)
1517
+ * @param id Exact session ID
1518
+ * @param sessionDir Optional session directory. If omitted, uses default (~/.agent-forge/agent/sessions/<encoded-cwd>/).
1519
+ * @param storage Optional storage provider; defaults to the local file protocol.
1520
+ */
1521
+ static findById(cwd, id, sessionDir, storage = FileSessionStorageProvider.instance) {
1522
+ const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
1523
+ const filterCwd = sessionDir !== undefined && dir !== getDefaultSessionDirPath(cwd);
1524
+ const resolvedCwd = resolvePath(cwd);
1525
+ try {
1526
+ for (const path of storage.listSessionFiles(dir)) {
1527
+ const header = readSessionHeaderForDiscovery(storage, path);
1528
+ if (header?.id !== id)
1529
+ continue;
1530
+ if (filterCwd && !sessionCwdMatches(getSessionHeaderCwd(header), resolvedCwd))
1531
+ continue;
1532
+ return path;
1533
+ }
1534
+ }
1535
+ catch {
1536
+ // Exact session discovery is best-effort, matching list().
1537
+ }
1538
+ return undefined;
1539
+ }
1518
1540
  /**
1519
1541
  * List all sessions for a directory.
1520
1542
  * @param cwd Working directory (used to compute default session directory)
1521
1543
  * @param sessionDir Optional session directory. If omitted, uses default (~/.agent-forge/agent/sessions/<encoded-cwd>/).
1522
1544
  * @param onProgress Optional callback for progress updates (loaded, total)
1545
+ * @param signal Optional abort signal; an aborted listing rejects with AbortError
1523
1546
  */
1524
- static async list(cwd, sessionDir, onProgress) {
1547
+ static async list(cwd, sessionDir, onProgress, signal) {
1525
1548
  const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
1526
1549
  const filterCwd = sessionDir !== undefined && dir !== getDefaultSessionDirPath(cwd);
1527
1550
  const resolvedCwd = resolvePath(cwd);
1528
- const sessions = (await listSessionsFromDir(dir, onProgress)).filter((session) => !filterCwd || sessionCwdMatches(session.cwd, resolvedCwd));
1529
- sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
1530
- return sessions;
1531
- }
1532
- static async listAll(sessionDirOrOnProgress, onProgress) {
1551
+ const includeSession = (session) => !filterCwd || sessionCwdMatches(session.cwd, resolvedCwd);
1552
+ const progress = onProgress
1553
+ ? (loaded, total, partialSessions) => onProgress(loaded, total, partialSessions?.filter(includeSession))
1554
+ : undefined;
1555
+ const sessions = (await listSessionsFromDir(dir, progress, signal)).filter(includeSession);
1556
+ return sortSessionInfos(sessions);
1557
+ }
1558
+ static async listAll(sessionDirOrOnProgress, onProgressOrSignal, signal) {
1533
1559
  const customSessionDir = typeof sessionDirOrOnProgress === "string" ? normalizePath(sessionDirOrOnProgress) : undefined;
1534
- const progress = typeof sessionDirOrOnProgress === "function" ? sessionDirOrOnProgress : onProgress;
1560
+ const progress = typeof sessionDirOrOnProgress === "function"
1561
+ ? sessionDirOrOnProgress
1562
+ : typeof onProgressOrSignal === "function"
1563
+ ? onProgressOrSignal
1564
+ : undefined;
1565
+ const abortSignal = typeof sessionDirOrOnProgress === "string" || typeof onProgressOrSignal === "function"
1566
+ ? signal
1567
+ : (onProgressOrSignal ?? signal);
1568
+ abortSignal?.throwIfAborted();
1535
1569
  if (customSessionDir) {
1536
- const sessions = await listSessionsFromDir(customSessionDir, progress);
1537
- sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
1538
- return sessions;
1570
+ return sortSessionInfos(await listSessionsFromDir(customSessionDir, progress, abortSignal));
1539
1571
  }
1540
1572
  const sessionsDir = getSessionsDir();
1541
1573
  try {
1542
- if (!existsSync(sessionsDir)) {
1574
+ if (!existsSync(sessionsDir))
1543
1575
  return [];
1544
- }
1545
1576
  const entries = await readdir(sessionsDir, { withFileTypes: true });
1546
1577
  const dirs = entries
1547
1578
  .filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
1548
1579
  .map((entry) => join(sessionsDir, entry.name));
1549
- // Count total files first for accurate progress
1550
- let totalFiles = 0;
1551
- const dirFiles = [];
1552
- for (const dir of dirs) {
1580
+ const dirFiles = await mapWithConcurrency(dirs, MAX_CONCURRENT_SESSION_DISCOVERY_LOADS, async (dir) => {
1553
1581
  try {
1554
- const files = (await readdir(dir)).filter((f) => f.endsWith(".jsonl"));
1555
- dirFiles.push(files.map((f) => join(dir, f)));
1556
- totalFiles += files.length;
1582
+ return (await readdir(dir)).filter((file) => file.endsWith(".jsonl")).map((file) => join(dir, file));
1557
1583
  }
1558
1584
  catch {
1559
- dirFiles.push([]);
1585
+ return [];
1560
1586
  }
1561
- }
1562
- // Process all files with progress tracking
1563
- let loaded = 0;
1564
- const sessions = [];
1587
+ }, abortSignal);
1565
1588
  const allFiles = dirFiles.flat();
1566
- const results = await buildSessionInfosWithConcurrency(allFiles, () => {
1567
- loaded++;
1568
- progress?.(loaded, totalFiles);
1569
- });
1570
- for (const info of results) {
1571
- if (info) {
1572
- sessions.push(info);
1589
+ const candidates = await mapWithConcurrency(allFiles, MAX_CONCURRENT_SESSION_DISCOVERY_LOADS, async (path) => {
1590
+ try {
1591
+ return { path, stats: await stat(path) };
1573
1592
  }
1574
- }
1575
- sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
1576
- return sessions;
1593
+ catch {
1594
+ return { path };
1595
+ }
1596
+ }, abortSignal);
1597
+ candidates.sort((a, b) => (b.stats?.mtimeMs ?? Number.NEGATIVE_INFINITY) - (a.stats?.mtimeMs ?? Number.NEGATIVE_INFINITY) ||
1598
+ basename(b.path).localeCompare(basename(a.path)));
1599
+ const totalFiles = candidates.length;
1600
+ let loaded = 0;
1601
+ let firstCandidateLoaded = false;
1602
+ const partialSessions = [];
1603
+ const results = await buildSessionInfosWithConcurrency(candidates, (info, index) => {
1604
+ loaded++;
1605
+ if (index === 0)
1606
+ firstCandidateLoaded = true;
1607
+ if (info)
1608
+ partialSessions.push(info);
1609
+ const publishPartial = firstCandidateLoaded &&
1610
+ (index === 0 || loaded % ALL_SESSION_LIST_PUBLISH_INTERVAL === 0 || loaded === totalFiles);
1611
+ progress?.(loaded, totalFiles, publishPartial ? sortSessionInfos([...partialSessions]) : undefined);
1612
+ }, abortSignal);
1613
+ return sortSessionInfos(results.filter((info) => info !== null));
1577
1614
  }
1578
1615
  catch {
1616
+ abortSignal?.throwIfAborted();
1579
1617
  return [];
1580
1618
  }
1581
1619
  }