@dreb/coding-agent 2.66.3 → 2.67.0

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 (45) hide show
  1. package/README.md +3 -1
  2. package/dist/cli/args.d.ts.map +1 -1
  3. package/dist/cli/args.js +1 -1
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/config.d.ts +12 -2
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +27 -4
  8. package/dist/config.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +4 -0
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +16 -1
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/sdk.d.ts.map +1 -1
  14. package/dist/core/sdk.js +2 -1
  15. package/dist/core/sdk.js.map +1 -1
  16. package/dist/core/session-manager.d.ts +18 -4
  17. package/dist/core/session-manager.d.ts.map +1 -1
  18. package/dist/core/session-manager.js +54 -22
  19. package/dist/core/session-manager.js.map +1 -1
  20. package/dist/core/settings-manager.d.ts +4 -0
  21. package/dist/core/settings-manager.d.ts.map +1 -1
  22. package/dist/core/settings-manager.js +4 -0
  23. package/dist/core/settings-manager.js.map +1 -1
  24. package/dist/core/tools/subagent.d.ts +14 -8
  25. package/dist/core/tools/subagent.d.ts.map +1 -1
  26. package/dist/core/tools/subagent.js +145 -71
  27. package/dist/core/tools/subagent.js.map +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/main.d.ts.map +1 -1
  33. package/dist/main.js +16 -7
  34. package/dist/main.js.map +1 -1
  35. package/dist/modes/rpc/rpc-mode.d.ts +2 -2
  36. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  37. package/dist/modes/rpc/rpc-mode.js +7 -5
  38. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  39. package/docs/dashboard.md +6 -0
  40. package/docs/rpc.md +3 -3
  41. package/docs/session.md +16 -7
  42. package/docs/settings.md +17 -5
  43. package/docs/skills.md +1 -1
  44. package/package.json +1 -1
  45. package/skills/model-routing-guide/SKILL.md +10 -5
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomBytes } from "node:crypto";
3
- import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, statSync } from "node:fs";
3
+ import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, realpathSync, statSync, } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { basename, join, resolve } from "node:path";
6
6
  import { completeSimple } from "@dreb/ai";
@@ -559,7 +559,7 @@ async function spawnSubagent(agentConfig, task, cwd, signal, onProgress, parentP
559
559
  });
560
560
  }
561
561
  const STEP_SESSION_DIR_RE = /^step-(\d+)$/;
562
- function findNewestJsonlFileInDir(dir) {
562
+ function findNewestJsonlFileInDir(dir, throwOnOperationalError = false) {
563
563
  let best;
564
564
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
565
565
  if (!entry.name.endsWith(".jsonl"))
@@ -573,14 +573,14 @@ function findNewestJsonlFileInDir(dir) {
573
573
  best = { path: fullPath, mtime };
574
574
  }
575
575
  catch (err) {
576
- if (isExpectedFilesystemError(err))
576
+ if (isExpectedFilesystemError(err) && (!throwOnOperationalError || isMissingFilesystemError(err)))
577
577
  continue;
578
578
  throw err;
579
579
  }
580
580
  }
581
581
  return best;
582
582
  }
583
- function discoverStepSessionFileCandidates(sessionDir) {
583
+ function discoverStepSessionFileCandidates(sessionDir, throwOnOperationalError = false) {
584
584
  const steps = [];
585
585
  for (const entry of readdirSync(sessionDir, { withFileTypes: true })) {
586
586
  if (!entry.isDirectory())
@@ -594,12 +594,12 @@ function discoverStepSessionFileCandidates(sessionDir) {
594
594
  const files = [];
595
595
  for (const step of steps) {
596
596
  try {
597
- const candidate = findNewestJsonlFileInDir(join(sessionDir, step.name));
597
+ const candidate = findNewestJsonlFileInDir(join(sessionDir, step.name), throwOnOperationalError);
598
598
  if (candidate)
599
599
  files.push(candidate);
600
600
  }
601
601
  catch (err) {
602
- if (isExpectedFilesystemError(err))
602
+ if (isExpectedFilesystemError(err) && (!throwOnOperationalError || isMissingFilesystemError(err)))
603
603
  continue;
604
604
  throw err;
605
605
  }
@@ -615,22 +615,28 @@ function discoverStepSessionFileCandidates(sessionDir) {
615
615
  * present this recurses one level into step-* directories and returns one file
616
616
  * per step in numeric step order.
617
617
  */
618
- export function discoverSessionFiles(sessionDir, agentName) {
618
+ export function discoverSessionFiles(sessionDir, agentName, options = {}) {
619
+ const throwOnOperationalError = options.throwOnOperationalError ?? false;
619
620
  try {
620
- if (!existsSync(sessionDir))
621
+ if (!throwOnOperationalError && !existsSync(sessionDir))
621
622
  return [];
622
- const flatFile = findNewestJsonlFileInDir(sessionDir);
623
+ const flatFile = findNewestJsonlFileInDir(sessionDir, throwOnOperationalError);
623
624
  if (flatFile) {
624
625
  log.debug(`[subagent] session file: ${flatFile.path} (agent=${agentName})`);
625
626
  return [flatFile.path];
626
627
  }
627
- const stepFiles = discoverStepSessionFileCandidates(sessionDir);
628
+ const stepFiles = discoverStepSessionFileCandidates(sessionDir, throwOnOperationalError);
628
629
  if (stepFiles.length > 0) {
629
630
  log.debug(`[subagent] chain session files: ${stepFiles.map((file) => file.path).join(", ")} (agent=${agentName})`);
630
631
  return stepFiles.map((file) => file.path);
631
632
  }
632
633
  }
633
634
  catch (err) {
635
+ if (throwOnOperationalError) {
636
+ if (isMissingFilesystemError(err))
637
+ return [];
638
+ throw err;
639
+ }
634
640
  if (!isExpectedFilesystemError(err))
635
641
  throw err;
636
642
  log.warn(`[subagent] failed to discover session file (agent=${agentName}): ${err instanceof Error ? err.message : String(err)}`);
@@ -1337,6 +1343,18 @@ function isRecord(value) {
1337
1343
  function isExpectedFilesystemError(err) {
1338
1344
  return typeof err?.code === "string";
1339
1345
  }
1346
+ /** Only absence is optional during primary-root restart recovery. */
1347
+ function isMissingFilesystemError(err) {
1348
+ return err?.code === "ENOENT";
1349
+ }
1350
+ function shouldSkipRehydrationFilesystemError(err, isPrimaryRoot, context) {
1351
+ if (isMissingFilesystemError(err))
1352
+ return true;
1353
+ if (isPrimaryRoot || !isExpectedFilesystemError(err))
1354
+ return false;
1355
+ log.warn(`[subagent] skipped compatibility-root recovery (${context}): ${err instanceof Error ? err.message : String(err)}`);
1356
+ return true;
1357
+ }
1340
1358
  function parseJsonlLine(line) {
1341
1359
  try {
1342
1360
  const parsed = JSON.parse(line);
@@ -1372,7 +1390,7 @@ function readFirstNonEmptyLine(filePath) {
1372
1390
  return buffered.trim() ? buffered : undefined;
1373
1391
  }
1374
1392
  catch (err) {
1375
- if (isExpectedFilesystemError(err))
1393
+ if (isMissingFilesystemError(err))
1376
1394
  return undefined;
1377
1395
  throw err;
1378
1396
  }
@@ -1396,7 +1414,7 @@ function readFileSlice(filePath, start, length) {
1396
1414
  return buffer.toString("utf8", 0, bytesRead);
1397
1415
  }
1398
1416
  catch (err) {
1399
- if (isExpectedFilesystemError(err))
1417
+ if (isMissingFilesystemError(err))
1400
1418
  return undefined;
1401
1419
  throw err;
1402
1420
  }
@@ -1417,7 +1435,7 @@ function readFileStart(filePath, maxBytes) {
1417
1435
  size = Math.min(statSync(filePath).size, maxBytes);
1418
1436
  }
1419
1437
  catch (err) {
1420
- if (isExpectedFilesystemError(err))
1438
+ if (isMissingFilesystemError(err))
1421
1439
  return undefined;
1422
1440
  throw err;
1423
1441
  }
@@ -1432,7 +1450,7 @@ function readFileTail(filePath, maxBytes) {
1432
1450
  length = size - start;
1433
1451
  }
1434
1452
  catch (err) {
1435
- if (isExpectedFilesystemError(err))
1453
+ if (isMissingFilesystemError(err))
1436
1454
  return undefined;
1437
1455
  throw err;
1438
1456
  }
@@ -1448,6 +1466,19 @@ function comparablePath(pathValue) {
1448
1466
  const resolved = resolve(pathValue);
1449
1467
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
1450
1468
  }
1469
+ /** Canonical identity for an existing path, with a lexical fallback for races/missing paths. */
1470
+ function canonicalExistingPath(pathValue) {
1471
+ let canonical;
1472
+ try {
1473
+ canonical = realpathSync.native(pathValue);
1474
+ }
1475
+ catch (err) {
1476
+ if (!isMissingFilesystemError(err))
1477
+ throw err;
1478
+ canonical = resolve(pathValue);
1479
+ }
1480
+ return process.platform === "win32" ? canonical.toLowerCase() : canonical;
1481
+ }
1451
1482
  function parentSessionMatches(recordedParentSession, parentSessionFile) {
1452
1483
  if (recordedParentSession === parentSessionFile)
1453
1484
  return true;
@@ -1524,82 +1555,125 @@ function parseStartedAt(header, sessionFile) {
1524
1555
  return statSync(sessionFile).mtime.getTime();
1525
1556
  }
1526
1557
  catch (err) {
1527
- if (isExpectedFilesystemError(err))
1558
+ if (isMissingFilesystemError(err))
1528
1559
  return Date.now();
1529
1560
  throw err;
1530
1561
  }
1531
1562
  }
1532
1563
  function hasRegisteredSession(sessionDir, sessionFile) {
1533
- const comparableSessionDir = comparablePath(sessionDir);
1534
- const comparableSessionFile = comparablePath(sessionFile);
1564
+ const comparableSessionDir = canonicalExistingPath(sessionDir);
1565
+ const comparableSessionFile = canonicalExistingPath(sessionFile);
1535
1566
  for (const agent of backgroundAgentRegistry.values()) {
1536
- if (agent.sessionDir && comparablePath(agent.sessionDir) === comparableSessionDir)
1567
+ if (agent.sessionDir && canonicalExistingPath(agent.sessionDir) === comparableSessionDir)
1537
1568
  return true;
1538
- if (agent.sessionFile && comparablePath(agent.sessionFile) === comparableSessionFile)
1569
+ if (agent.sessionFile && canonicalExistingPath(agent.sessionFile) === comparableSessionFile)
1539
1570
  return true;
1540
1571
  }
1541
1572
  return false;
1542
1573
  }
1543
1574
  /**
1544
- * Best-effort recovery for completed background subagents after a dashboard/RPC
1545
- * process resumes an existing parent session. Live background-agent state is an
1546
- * in-memory registry, while child sessions are durable JSONL files under the
1547
- * subagent sessions directory.
1575
+ * Recover completed background subagents after a dashboard/RPC process resumes
1576
+ * an existing parent session. Live background-agent state is an in-memory
1577
+ * registry, while child sessions are durable JSONL files under the subagent
1578
+ * sessions directory.
1548
1579
  *
1549
- * Returns the number of newly registered agents. Expected filesystem and JSONL
1550
- * parse failures are skipped; unexpected programming errors are allowed to throw.
1580
+ * The first root is authoritative: absence races and malformed JSON are skipped,
1581
+ * while operational filesystem failures throw. Later roots are compatibility
1582
+ * stores, so their operational filesystem failures are warned and skipped.
1583
+ * Returns the number of newly registered agents.
1551
1584
  */
1552
- export function rehydrateBackgroundAgentsFromDisk(parentSessionFile, subagentSessionsBase = getSubagentSessionsDir()) {
1585
+ export function rehydrateBackgroundAgentsFromDisk(parentSessionFile, subagentSessionRoots = getSubagentSessionsDir()) {
1553
1586
  if (!parentSessionFile)
1554
1587
  return 0;
1555
- let entries;
1556
- try {
1557
- entries = readdirSync(subagentSessionsBase, { withFileTypes: true });
1558
- }
1559
- catch (err) {
1560
- if (isExpectedFilesystemError(err))
1561
- return 0;
1562
- throw err;
1563
- }
1588
+ const roots = typeof subagentSessionRoots === "string" ? [subagentSessionRoots] : subagentSessionRoots;
1589
+ const scannedRoots = new Set();
1590
+ const seenSessionDirs = new Set();
1591
+ const seenSessionFiles = new Set();
1564
1592
  let registered = 0;
1565
- for (const entry of entries) {
1566
- if (!entry.isDirectory())
1567
- continue;
1568
- const sessionDir = join(subagentSessionsBase, entry.name);
1569
- const sessionFiles = discoverSessionFiles(sessionDir, entry.name);
1570
- if (sessionFiles.length === 0)
1571
- continue;
1572
- let sessionFile;
1573
- let header;
1574
- for (const candidateFile of sessionFiles) {
1575
- const candidateHeader = parseSessionHeader(candidateFile);
1576
- if (candidateHeader?.type !== "session" || typeof candidateHeader.parentSession !== "string")
1577
- continue;
1578
- if (!parentSessionMatches(candidateHeader.parentSession, parentSessionFile))
1593
+ for (const [rootIndex, subagentSessionsBase] of roots.entries()) {
1594
+ const isPrimaryRoot = rootIndex === 0;
1595
+ let canonicalRoot;
1596
+ try {
1597
+ canonicalRoot = canonicalExistingPath(subagentSessionsBase);
1598
+ }
1599
+ catch (err) {
1600
+ if (shouldSkipRehydrationFilesystemError(err, isPrimaryRoot, `root ${subagentSessionsBase}`))
1579
1601
  continue;
1580
- sessionFile = candidateFile;
1581
- header = candidateHeader;
1582
- break;
1602
+ throw err;
1583
1603
  }
1584
- if (!sessionFile || !header)
1585
- continue;
1586
- const agentId = `${REHYDRATED_AGENT_ID_PREFIX}${entry.name}`;
1587
- if (backgroundAgentRegistry.has(agentId) || hasRegisteredSession(sessionDir, sessionFile))
1604
+ if (scannedRoots.has(canonicalRoot))
1588
1605
  continue;
1589
- const statusFile = sessionFiles[sessionFiles.length - 1] ?? sessionFile;
1590
- const agentType = typeof header.agentType === "string" && header.agentType.trim() ? header.agentType : "agent";
1591
- const taskSummary = findFirstUserMessageSummary(sessionFile) ?? `${agentType} (${entry.name})`;
1592
- backgroundAgentRegistry.set(agentId, {
1593
- agentId,
1594
- agentType,
1595
- taskSummary,
1596
- startedAt: parseStartedAt(header, sessionFile),
1597
- status: inferCompletedSessionStatus(statusFile),
1598
- sessionDir,
1599
- sessionFile,
1600
- cwd: typeof header.cwd === "string" ? header.cwd : undefined,
1601
- });
1602
- registered++;
1606
+ scannedRoots.add(canonicalRoot);
1607
+ let entries;
1608
+ try {
1609
+ entries = readdirSync(subagentSessionsBase, { withFileTypes: true });
1610
+ }
1611
+ catch (err) {
1612
+ if (shouldSkipRehydrationFilesystemError(err, isPrimaryRoot, `root ${subagentSessionsBase}`))
1613
+ continue;
1614
+ throw err;
1615
+ }
1616
+ for (const entry of entries) {
1617
+ const sessionDir = join(subagentSessionsBase, entry.name);
1618
+ try {
1619
+ let isDirectory = entry.isDirectory();
1620
+ if (!isDirectory && entry.isSymbolicLink()) {
1621
+ isDirectory = statSync(sessionDir).isDirectory();
1622
+ }
1623
+ if (!isDirectory)
1624
+ continue;
1625
+ const canonicalSessionDir = canonicalExistingPath(sessionDir);
1626
+ if (seenSessionDirs.has(canonicalSessionDir))
1627
+ continue;
1628
+ seenSessionDirs.add(canonicalSessionDir);
1629
+ const sessionFiles = discoverSessionFiles(sessionDir, entry.name, { throwOnOperationalError: true });
1630
+ if (sessionFiles.length === 0)
1631
+ continue;
1632
+ let sessionFile;
1633
+ let header;
1634
+ for (const candidateFile of sessionFiles) {
1635
+ const candidateHeader = parseSessionHeader(candidateFile);
1636
+ if (candidateHeader?.type !== "session" || typeof candidateHeader.parentSession !== "string")
1637
+ continue;
1638
+ if (!parentSessionMatches(candidateHeader.parentSession, parentSessionFile))
1639
+ continue;
1640
+ const canonicalSessionFile = canonicalExistingPath(candidateFile);
1641
+ if (seenSessionFiles.has(canonicalSessionFile))
1642
+ break;
1643
+ seenSessionFiles.add(canonicalSessionFile);
1644
+ sessionFile = candidateFile;
1645
+ header = candidateHeader;
1646
+ break;
1647
+ }
1648
+ if (!sessionFile || !header || hasRegisteredSession(sessionDir, sessionFile))
1649
+ continue;
1650
+ const baseAgentId = `${REHYDRATED_AGENT_ID_PREFIX}${entry.name}`;
1651
+ let agentId = baseAgentId;
1652
+ let suffix = 2;
1653
+ while (backgroundAgentRegistry.has(agentId)) {
1654
+ agentId = `${baseAgentId}-${suffix++}`;
1655
+ }
1656
+ const statusFile = sessionFiles[sessionFiles.length - 1] ?? sessionFile;
1657
+ const agentType = typeof header.agentType === "string" && header.agentType.trim() ? header.agentType : "agent";
1658
+ const taskSummary = findFirstUserMessageSummary(sessionFile) ?? `${agentType} (${entry.name})`;
1659
+ backgroundAgentRegistry.set(agentId, {
1660
+ agentId,
1661
+ agentType,
1662
+ taskSummary,
1663
+ startedAt: parseStartedAt(header, sessionFile),
1664
+ status: inferCompletedSessionStatus(statusFile),
1665
+ sessionDir,
1666
+ sessionFile,
1667
+ cwd: typeof header.cwd === "string" ? header.cwd : undefined,
1668
+ });
1669
+ registered++;
1670
+ }
1671
+ catch (err) {
1672
+ if (shouldSkipRehydrationFilesystemError(err, isPrimaryRoot, `entry ${sessionDir}`))
1673
+ continue;
1674
+ throw err;
1675
+ }
1676
+ }
1603
1677
  }
1604
1678
  return registered;
1605
1679
  }
@@ -1803,6 +1877,7 @@ export function formatSingleResult(result) {
1803
1877
  return text;
1804
1878
  }
1805
1879
  export function createSubagentToolDefinition(cwd, options) {
1880
+ const subagentSessionsBase = options?.subagentSessionsDir ?? getSubagentSessionsDir();
1806
1881
  const onBackgroundStart = options?.onBackgroundStart;
1807
1882
  const onBackgroundComplete = options?.onBackgroundComplete;
1808
1883
  const onBackgroundEvent = options?.onBackgroundEvent;
@@ -2016,7 +2091,6 @@ export function createSubagentToolDefinition(cwd, options) {
2016
2091
  return agentId;
2017
2092
  };
2018
2093
  // Helper to launch a single background task
2019
- const subagentSessionsBase = getSubagentSessionsDir();
2020
2094
  const launchBackgroundTask = (agentName, task, taskLabel, taskCwd, modelOverride, thinkingOverride) => {
2021
2095
  const resolvedCwd = taskCwd ?? cwd;
2022
2096
  // Each background agent gets its own session subdirectory