pi-smart-compact 9.5.0 → 9.6.1

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 (44) hide show
  1. package/ARCHITECTURE.md +7 -5
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +35 -19
  4. package/dist/app/global-settings-runtime.d.ts +7 -0
  5. package/dist/app/global-settings-runtime.d.ts.map +1 -0
  6. package/dist/app/register-context-tools.d.ts +4 -1
  7. package/dist/app/register-context-tools.d.ts.map +1 -1
  8. package/dist/app/register-smart-compact-command.d.ts +3 -1
  9. package/dist/app/register-smart-compact-command.d.ts.map +1 -1
  10. package/dist/app/run-context.d.ts +3 -3
  11. package/dist/app/run-context.d.ts.map +1 -1
  12. package/dist/app/smart-compact-policy.d.ts +4 -1
  13. package/dist/app/smart-compact-policy.d.ts.map +1 -1
  14. package/dist/app/steps/tier.d.ts +5 -4
  15. package/dist/app/steps/tier.d.ts.map +1 -1
  16. package/dist/constants.d.ts +43 -1
  17. package/dist/constants.d.ts.map +1 -1
  18. package/dist/domain/tool-semantics.d.ts.map +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1830 -560
  21. package/dist/infra/ai-messages.d.ts.map +1 -1
  22. package/dist/infra/fs.d.ts +2 -1
  23. package/dist/infra/fs.d.ts.map +1 -1
  24. package/dist/phases/explore.d.ts.map +1 -1
  25. package/dist/phases/synthesize.d.ts +5 -0
  26. package/dist/phases/synthesize.d.ts.map +1 -1
  27. package/dist/phases/verify.d.ts.map +1 -1
  28. package/dist/provider-eval.js +157 -54
  29. package/dist/provider-scenario-eval.js +162 -57
  30. package/dist/telemetry-report.js +125 -22
  31. package/dist/ui/overlays.d.ts.map +1 -1
  32. package/dist/ui/settings-complex.d.ts +9 -0
  33. package/dist/ui/settings-complex.d.ts.map +1 -0
  34. package/dist/ui/settings-overlay.d.ts +43 -3
  35. package/dist/ui/settings-overlay.d.ts.map +1 -1
  36. package/dist/utils/config.d.ts +14 -1
  37. package/dist/utils/config.d.ts.map +1 -1
  38. package/dist/utils/file-needles.d.ts +24 -0
  39. package/dist/utils/file-needles.d.ts.map +1 -1
  40. package/dist/utils/file-ref-detect.d.ts +6 -1
  41. package/dist/utils/file-ref-detect.d.ts.map +1 -1
  42. package/dist/utils/helpers.d.ts +1 -0
  43. package/dist/utils/helpers.d.ts.map +1 -1
  44. package/package.json +5 -3
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  } from "@earendil-works/pi-coding-agent";
8
8
 
9
9
  // src/constants.ts
10
- var VERSION = "9.5.0";
10
+ var VERSION = "9.6.1";
11
11
  var CHARS_PER_TOKEN = 3.8;
12
12
  var MIN_COMPACTION_SAVING_RATIO = 0.1;
13
13
  var ESTIMATOR_ROUNDING_TOLERANCE_TOKENS = 1;
@@ -48,6 +48,32 @@ var PROFILES = {
48
48
  batchMaxTokens: 18000
49
49
  }
50
50
  };
51
+ var PROFILE_NUMERIC_BOUNDS = {
52
+ summaryBudgetTokens: [256, 1e5],
53
+ keepRecentTokens: [1000, 500000],
54
+ minChunkTokens: [100, 1e5],
55
+ maxChunkTokens: [500, 200000],
56
+ singlePassMaxTokens: [1000, 500000],
57
+ batchMaxTokens: [1000, 500000]
58
+ };
59
+ var CONFIG_NUMERIC_LIMITS = {
60
+ minContextPercent: { min: 0, max: 100, integer: false },
61
+ autoTriggerTimeoutMs: { min: 1000, max: 300000, integer: true },
62
+ maxLlmCalls: { min: 0, max: 100, integer: true },
63
+ maxLlmInputTokens: { min: 0, max: 1e6, integer: true },
64
+ codexMaxCallMs: {
65
+ min: 5000,
66
+ max: 300000,
67
+ integer: true,
68
+ zeroOrRange: true
69
+ },
70
+ maxLatencyMs: {
71
+ min: 5000,
72
+ max: 600000,
73
+ integer: true,
74
+ zeroOrRange: true
75
+ }
76
+ };
51
77
  var DEFAULT_CONFIG = {
52
78
  mode: "auto",
53
79
  profile: "balanced",
@@ -315,102 +341,283 @@ var EXPLORER_SYSTEM_PROMPT = `You are a conversation analyst. You have determini
315
341
  ` + '{"boundaries":[{"afterIndex":N,"topic":"...","priority":"critical|high|normal|low","confidence":0.0-1.0}],"mainGoal":"...","sessionType":"implementation|review|debugging|discussion","enrichedConstraints":[...],"crossReferences":[...],"statusAssessment":{"done":[...],"inProgress":[...],"blocked":[...]},"criticalContext":[...],"keyDecisions":[...]}';
316
342
 
317
343
  // src/utils/config.ts
344
+ import fs2 from "fs";
345
+
346
+ // src/infra/fs.ts
318
347
  import fs from "fs";
348
+ import fsp from "fs/promises";
349
+ import path from "path";
350
+ import crypto from "crypto";
351
+
352
+ // src/utils/logger.ts
353
+ var DEBUG = process.env.DEBUG?.includes("smart-compact") ?? false;
354
+ function warn(msg, err) {
355
+ const detail = err instanceof Error ? err.message : err ?? "";
356
+ console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
357
+ }
358
+ function error(msg, err) {
359
+ const detail = err instanceof Error ? err.message + `
360
+ ` + err.stack : err ?? "";
361
+ console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
362
+ }
363
+ function info(msg, ...args) {
364
+ console.error(LOG_PREFIX + " [info] " + msg, ...args);
365
+ }
366
+ function debug(msg, ...args) {
367
+ if (DEBUG)
368
+ console.error(LOG_PREFIX + " [debug] " + msg, ...args);
369
+ }
370
+ function debugError(msg, err) {
371
+ if (DEBUG)
372
+ error(msg, err);
373
+ }
374
+
375
+ // src/infra/fs.ts
376
+ var LOCK_RETRY_MS = 25;
377
+ var LOCK_MAX_RETRIES = 80;
378
+ function ensureDir(dir) {
379
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
380
+ fs.chmodSync(dir, 448);
381
+ }
382
+ async function ensureDirAsync(dir) {
383
+ await fsp.mkdir(dir, { recursive: true, mode: 448 });
384
+ await fsp.chmod(dir, 448);
385
+ }
386
+ function tempPath(target) {
387
+ return target + ".tmp." + process.pid + "." + crypto.randomBytes(4).toString("hex");
388
+ }
389
+ function atomicWriteFileSync(target, data) {
390
+ ensureDir(path.dirname(target));
391
+ const tmp = tempPath(target);
392
+ try {
393
+ fs.writeFileSync(tmp, data, { mode: 384 });
394
+ fs.chmodSync(tmp, 384);
395
+ fs.renameSync(tmp, target);
396
+ } catch (e) {
397
+ try {
398
+ fs.unlinkSync(tmp);
399
+ } catch {}
400
+ throw e;
401
+ }
402
+ }
403
+ async function atomicWriteFile(target, data) {
404
+ await ensureDirAsync(path.dirname(target));
405
+ const tmp = tempPath(target);
406
+ try {
407
+ await fsp.writeFile(tmp, data, { mode: 384 });
408
+ await fsp.chmod(tmp, 384);
409
+ await fsp.rename(tmp, target);
410
+ } catch (e) {
411
+ try {
412
+ await fsp.unlink(tmp);
413
+ } catch {}
414
+ throw e;
415
+ }
416
+ }
417
+ function tryAcquireLock(target) {
418
+ const lockDir = target + ".lock";
419
+ const ownerFile = path.join(lockDir, "owner");
420
+ const token = process.pid + ":" + crypto.randomBytes(8).toString("hex");
421
+ try {
422
+ fs.mkdirSync(lockDir, { mode: 448 });
423
+ } catch (error2) {
424
+ if (error2?.code === "EEXIST")
425
+ return null;
426
+ throw new Error("Failed to acquire lock for " + target, { cause: error2 });
427
+ }
428
+ try {
429
+ fs.writeFileSync(ownerFile, token, { mode: 384, flag: "wx" });
430
+ } catch (error2) {
431
+ try {
432
+ fs.rmSync(lockDir, { recursive: true, force: true });
433
+ } catch {}
434
+ throw new Error("Failed to acquire lock for " + target, { cause: error2 });
435
+ }
436
+ return () => {
437
+ try {
438
+ if (fs.readFileSync(ownerFile, "utf8") === token) {
439
+ fs.rmSync(lockDir, { recursive: true });
440
+ }
441
+ } catch {}
442
+ };
443
+ }
444
+ function acquireLockSync(target) {
445
+ const release = tryAcquireLock(target);
446
+ if (!release)
447
+ throw new Error("Lock busy for " + target);
448
+ return release;
449
+ }
450
+ async function acquireLock(target) {
451
+ for (let attempt = 0;attempt < LOCK_MAX_RETRIES; attempt++) {
452
+ const release = tryAcquireLock(target);
453
+ if (release)
454
+ return release;
455
+ const delay = Promise.withResolvers();
456
+ setTimeout(delay.resolve, LOCK_RETRY_MS);
457
+ await delay.promise;
458
+ }
459
+ throw new Error("Timed out acquiring lock for " + target);
460
+ }
461
+ async function appendLineLockedAsync(target, line, maxBytes) {
462
+ await ensureDirAsync(path.dirname(target));
463
+ const payload = Buffer.from(line.endsWith(`
464
+ `) ? line : line + `
465
+ `);
466
+ if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)) {
467
+ throw new Error("maxBytes must be a positive safe integer");
468
+ }
469
+ if (maxBytes !== undefined && payload.length > maxBytes) {
470
+ throw new Error("Log entry exceeds retention cap for " + target);
471
+ }
472
+ const release = await acquireLock(target);
473
+ try {
474
+ let stat = null;
475
+ try {
476
+ stat = await fsp.stat(target);
477
+ } catch (error2) {
478
+ if (!error2 || typeof error2 !== "object" || !("code" in error2) || error2.code !== "ENOENT")
479
+ throw error2;
480
+ }
481
+ if (maxBytes !== undefined && stat && stat.size + payload.length > maxBytes) {
482
+ const retainedLength = Math.min(stat.size, Math.max(0, maxBytes - payload.length));
483
+ const buffer = Buffer.allocUnsafe(retainedLength);
484
+ if (retainedLength > 0) {
485
+ const handle = await fsp.open(target, "r");
486
+ try {
487
+ await handle.read(buffer, 0, retainedLength, stat.size - retainedLength);
488
+ } finally {
489
+ await handle.close();
490
+ }
491
+ }
492
+ let tail = buffer.toString("utf8");
493
+ if (retainedLength < stat.size) {
494
+ const firstNewline = tail.indexOf(`
495
+ `);
496
+ tail = firstNewline >= 0 ? tail.slice(firstNewline + 1) : "";
497
+ }
498
+ await atomicWriteFile(target, tail);
499
+ }
500
+ await fsp.appendFile(target, payload, { mode: 384 });
501
+ await fsp.chmod(target, 384);
502
+ } finally {
503
+ release();
504
+ }
505
+ }
506
+ function readJsonlTail(target, limit, maxBytes = 512 * 1024) {
507
+ if (limit <= 0 || !fs.existsSync(target))
508
+ return [];
509
+ const stat = fs.statSync(target);
510
+ const length = Math.min(stat.size, maxBytes);
511
+ const buffer = Buffer.alloc(length);
512
+ const fd = fs.openSync(target, "r");
513
+ try {
514
+ fs.readSync(fd, buffer, 0, length, stat.size - length);
515
+ } finally {
516
+ fs.closeSync(fd);
517
+ }
518
+ let text = buffer.toString("utf8");
519
+ if (stat.size > length) {
520
+ const newline = text.indexOf(`
521
+ `);
522
+ text = newline >= 0 ? text.slice(newline + 1) : "";
523
+ }
524
+ const values = [];
525
+ for (const line of text.split(`
526
+ `)) {
527
+ if (!line)
528
+ continue;
529
+ try {
530
+ values.push(JSON.parse(line));
531
+ } catch {}
532
+ }
533
+ return values.slice(-limit);
534
+ }
535
+ function readJsonSync(target) {
536
+ try {
537
+ if (!fs.existsSync(target))
538
+ return null;
539
+ const raw = fs.readFileSync(target, "utf8");
540
+ return JSON.parse(raw);
541
+ } catch (e) {
542
+ warn("readJsonSync failed for " + target, e);
543
+ return null;
544
+ }
545
+ }
546
+ function writeJsonSync(target, value, pretty = false) {
547
+ atomicWriteFileSync(target, pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value));
548
+ }
319
549
 
320
550
  // src/infra/paths.ts
321
- import path from "path";
551
+ import path2 from "path";
322
552
  import os from "os";
323
553
  function home() {
324
554
  const configured = process.env.HOME?.trim() || process.env.USERPROFILE?.trim();
325
555
  return configured || os.homedir();
326
556
  }
327
557
  function piAgentDir() {
328
- return path.join(home(), ".pi", "agent");
558
+ return path2.join(home(), ".pi", "agent");
329
559
  }
330
560
  function cacheDir() {
331
- return path.join(piAgentDir(), ".cache");
561
+ return path2.join(piAgentDir(), ".cache");
332
562
  }
333
563
  function smartCompactCacheDir() {
334
- return path.join(cacheDir(), "smart-compact");
564
+ return path2.join(cacheDir(), "smart-compact");
335
565
  }
336
566
  function projectFingerprintDir() {
337
- return path.join(smartCompactCacheDir(), "projects");
567
+ return path2.join(smartCompactCacheDir(), "projects");
338
568
  }
339
569
  function compactionStateDir() {
340
- return path.join(smartCompactCacheDir(), "states");
570
+ return path2.join(smartCompactCacheDir(), "states");
341
571
  }
342
572
  function sessionsDir() {
343
- return path.join(piAgentDir(), "sessions");
573
+ return path2.join(piAgentDir(), "sessions");
344
574
  }
345
575
  function settingsFile() {
346
- return path.join(piAgentDir(), "settings.json");
576
+ return path2.join(piAgentDir(), "settings.json");
347
577
  }
348
578
  function defaultBackupDir() {
349
- return path.join(piAgentDir(), "compact-backups");
579
+ return path2.join(piAgentDir(), "compact-backups");
350
580
  }
351
581
  function metricsLogFile() {
352
- return path.join(cacheDir(), "compact-metrics.jsonl");
582
+ return path2.join(cacheDir(), "compact-metrics.jsonl");
353
583
  }
354
584
  function runLocksDir() {
355
- return path.join(smartCompactCacheDir(), "run-locks");
585
+ return path2.join(smartCompactCacheDir(), "run-locks");
356
586
  }
357
587
  function nativeContinuityDir() {
358
- return path.join(smartCompactCacheDir(), "native-continuity");
588
+ return path2.join(smartCompactCacheDir(), "native-continuity");
359
589
  }
360
590
  function contextGraphFile() {
361
- return path.join(smartCompactCacheDir(), "context-graph.sqlite");
591
+ return path2.join(smartCompactCacheDir(), "context-graph.sqlite");
362
592
  }
363
593
  function damageReportsFile() {
364
- return path.join(smartCompactCacheDir(), "damage-reports.jsonl");
594
+ return path2.join(smartCompactCacheDir(), "damage-reports.jsonl");
365
595
  }
366
596
  function extractionCacheFile(sessionId) {
367
- return path.join(cacheDir(), EXTRACTION_CACHE_PREFIX + sessionId.replace(/[^a-zA-Z0-9-]/g, "_") + ".json");
597
+ return path2.join(cacheDir(), EXTRACTION_CACHE_PREFIX + sessionId.replace(/[^a-zA-Z0-9-]/g, "_") + ".json");
368
598
  }
369
599
  function projectFingerprintFile(projectId) {
370
- return path.join(projectFingerprintDir(), projectId + ".json");
600
+ return path2.join(projectFingerprintDir(), projectId + ".json");
371
601
  }
372
602
  function compactionStateFile(projectId) {
373
- return path.join(compactionStateDir(), projectId.replace(/[^a-zA-Z0-9_-]/g, "_") + ".json");
603
+ return path2.join(compactionStateDir(), projectId.replace(/[^a-zA-Z0-9_-]/g, "_") + ".json");
374
604
  }
375
605
  function legacyScopedCompactionStateFile(projectId, sessionId) {
376
606
  const project = projectId.replace(/[^a-zA-Z0-9_-]/g, "_");
377
607
  const session = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
378
- return path.join(compactionStateDir(), project, session + ".json");
608
+ return path2.join(compactionStateDir(), project, session + ".json");
379
609
  }
380
610
  function scopedCompactionStateFile(projectId, sessionId, branchHeadId) {
381
611
  const project = projectId.replace(/[^a-zA-Z0-9_-]/g, "_");
382
612
  const session = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
383
613
  const branch = branchHeadId.replace(/[^a-zA-Z0-9_-]/g, "_");
384
- return path.join(compactionStateDir(), project, session, branch + ".json");
614
+ return path2.join(compactionStateDir(), project, session, branch + ".json");
385
615
  }
386
616
  function remediationHintsFile(projectId) {
387
- return path.join(smartCompactCacheDir(), "remediation-" + projectId + ".json");
617
+ return path2.join(smartCompactCacheDir(), "remediation-" + projectId + ".json");
388
618
  }
389
619
  function metricsDashboardFile() {
390
- return path.join(cacheDir(), "smart-compact-report.html");
391
- }
392
-
393
- // src/utils/logger.ts
394
- var DEBUG = process.env.DEBUG?.includes("smart-compact") ?? false;
395
- function warn(msg, err) {
396
- const detail = err instanceof Error ? err.message : err ?? "";
397
- console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
398
- }
399
- function error(msg, err) {
400
- const detail = err instanceof Error ? err.message + `
401
- ` + err.stack : err ?? "";
402
- console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
403
- }
404
- function info(msg, ...args) {
405
- console.error(LOG_PREFIX + " [info] " + msg, ...args);
406
- }
407
- function debug(msg, ...args) {
408
- if (DEBUG)
409
- console.error(LOG_PREFIX + " [debug] " + msg, ...args);
410
- }
411
- function debugError(msg, err) {
412
- if (DEBUG)
413
- error(msg, err);
620
+ return path2.join(cacheDir(), "smart-compact-report.html");
414
621
  }
415
622
 
416
623
  // src/utils/config.ts
@@ -456,14 +663,116 @@ var PROFILE_NUMERIC_KEYS = [
456
663
  "singlePassMaxTokens",
457
664
  "batchMaxTokens"
458
665
  ];
459
- var PROFILE_NUMERIC_BOUNDS = {
460
- summaryBudgetTokens: [256, 1e5],
461
- keepRecentTokens: [1000, 500000],
462
- minChunkTokens: [100, 1e5],
463
- maxChunkTokens: [500, 200000],
464
- singlePassMaxTokens: [1000, 500000],
465
- batchMaxTokens: [1000, 500000]
466
- };
666
+ function readGlobalConfigValue(configPath) {
667
+ assertGlobalConfigPath(configPath);
668
+ try {
669
+ const section = configuredSection(readSettingsRoot(settingsFile()));
670
+ validateSmartCompactConfig(section);
671
+ return cloneGlobalConfigValue(configPathValue(section, configPath));
672
+ } catch {
673
+ return;
674
+ }
675
+ }
676
+ function isRecord(value) {
677
+ return typeof value === "object" && value !== null && !Array.isArray(value);
678
+ }
679
+ function cloneProfiles(profiles) {
680
+ return Object.fromEntries(VALID_PROFILES.map((name) => [name, { ...profiles[name] }]));
681
+ }
682
+ function cloneConfig(config) {
683
+ return {
684
+ ...config,
685
+ profiles: cloneProfiles(config.profiles),
686
+ pinPaths: [...config.pinPaths]
687
+ };
688
+ }
689
+ function defaultConfig() {
690
+ return cloneConfig({
691
+ ...DEFAULT_CONFIG,
692
+ backupDir: defaultBackupDir()
693
+ });
694
+ }
695
+ function cloneGlobalConfigValue(value) {
696
+ return Array.isArray(value) ? [...value] : value;
697
+ }
698
+ function readSettingsRoot(file) {
699
+ if (!fs2.existsSync(file))
700
+ return {};
701
+ let parsed;
702
+ try {
703
+ parsed = JSON.parse(fs2.readFileSync(file, "utf8"));
704
+ } catch {
705
+ throw new Error("settings.json must contain valid JSON");
706
+ }
707
+ if (!isRecord(parsed)) {
708
+ throw new Error("settings.json root must be an object");
709
+ }
710
+ return parsed;
711
+ }
712
+ function configuredSection(root) {
713
+ const selected = Object.hasOwn(root, CONFIG_KEY) ? root[CONFIG_KEY] : root[CONFIG_KEY_ALT] ?? {};
714
+ if (!isRecord(selected)) {
715
+ throw new Error("smartCompact must be an object");
716
+ }
717
+ return structuredClone(selected);
718
+ }
719
+ function deleteEmptyProfileContainers(section, profile) {
720
+ if (!isRecord(section.profiles))
721
+ return;
722
+ if (isRecord(section.profiles[profile])) {
723
+ const values = section.profiles[profile];
724
+ if (Object.keys(values).length === 0)
725
+ delete section.profiles[profile];
726
+ }
727
+ if (Object.keys(section.profiles).length === 0)
728
+ delete section.profiles;
729
+ }
730
+ function setConfigPath(section, configPath, value) {
731
+ const parts = configPath.split(".");
732
+ if (parts[0] !== "profiles") {
733
+ if (value === undefined)
734
+ delete section[configPath];
735
+ else
736
+ section[configPath] = value;
737
+ return;
738
+ }
739
+ const [, profile, key] = parts;
740
+ if (!isRecord(section.profiles))
741
+ section.profiles = {};
742
+ const profiles = section.profiles;
743
+ if (!isRecord(profiles[profile]))
744
+ profiles[profile] = {};
745
+ const values = profiles[profile];
746
+ if (value === undefined)
747
+ delete values[key];
748
+ else
749
+ values[key] = value;
750
+ deleteEmptyProfileContainers(section, profile);
751
+ }
752
+ function assertGlobalConfigPath(configPath) {
753
+ if (configPath !== "profiles" && Object.hasOwn(DEFAULT_CONFIG, configPath)) {
754
+ return;
755
+ }
756
+ const parts = configPath.split(".");
757
+ if (parts.length === 3 && parts[0] === "profiles" && VALID_PROFILES.includes(parts[1]) && PROFILE_NUMERIC_KEYS.includes(parts[2])) {
758
+ return;
759
+ }
760
+ throw new Error(`Unknown smartCompact setting path: ${configPath}`);
761
+ }
762
+ function configPathValue(section, configPath) {
763
+ const parts = configPath.split(".");
764
+ if (parts[0] !== "profiles") {
765
+ return section[configPath];
766
+ }
767
+ const [, profile, key] = parts;
768
+ if (!isRecord(section.profiles))
769
+ return;
770
+ const values = section.profiles[profile];
771
+ return isRecord(values) ? values[key] : undefined;
772
+ }
773
+ function sameJsonValue(left, right) {
774
+ return JSON.stringify(left) === JSON.stringify(right);
775
+ }
467
776
  function discard(sc, key, message) {
468
777
  warn(message);
469
778
  delete sc[key];
@@ -513,7 +822,7 @@ function validateBasicFields(sc) {
513
822
  function validateProfiles(sc) {
514
823
  if (!("profiles" in sc))
515
824
  return;
516
- if (typeof sc.profiles !== "object" || sc.profiles === null || Array.isArray(sc.profiles)) {
825
+ if (!isRecord(sc.profiles)) {
517
826
  discard(sc, "profiles", "smart-compact config: profiles must be an object, got " + typeof sc.profiles);
518
827
  return;
519
828
  }
@@ -523,7 +832,7 @@ function validateProfiles(sc) {
523
832
  discard(profiles, profileName, "smart-compact config: ignoring unknown profile override '" + profileName + "'.");
524
833
  continue;
525
834
  }
526
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
835
+ if (!isRecord(value)) {
527
836
  discard(profiles, profileName, "smart-compact config: profile '" + profileName + "' must be an object.");
528
837
  continue;
529
838
  }
@@ -547,35 +856,39 @@ function validateProfiles(sc) {
547
856
  }
548
857
  }
549
858
  }
859
+ function validNumericLimit(key, value) {
860
+ const limit = CONFIG_NUMERIC_LIMITS[key];
861
+ return Number.isFinite(value) && (!limit.integer || Number.isSafeInteger(value)) && (value >= limit.min && value <= limit.max || ("zeroOrRange" in limit) && limit.zeroOrRange && value === 0);
862
+ }
550
863
  var NUMERIC_RULES = [
551
864
  {
552
865
  key: "autoTriggerTimeoutMs",
553
- valid: (value) => Number.isFinite(value) && value >= 1000 && value <= 300000,
866
+ valid: (value) => validNumericLimit("autoTriggerTimeoutMs", value),
554
867
  message: (value) => "smart-compact config: autoTriggerTimeoutMs must be 1000\u2013300000, got " + value + ". Using default " + DEFAULT_CONFIG.autoTriggerTimeoutMs + "ms."
555
868
  },
556
869
  {
557
870
  key: "maxLlmCalls",
558
- valid: (value) => Number.isInteger(value) && value >= 0 && value <= 100,
871
+ valid: (value) => validNumericLimit("maxLlmCalls", value),
559
872
  message: () => "smart-compact config: maxLlmCalls must be 0\u2013100; 0 uses the selected mode cap."
560
873
  },
561
874
  {
562
875
  key: "maxLlmInputTokens",
563
- valid: (value) => Number.isInteger(value) && value >= 0 && value <= 1e6,
876
+ valid: (value) => validNumericLimit("maxLlmInputTokens", value),
564
877
  message: () => "smart-compact config: maxLlmInputTokens must be 0\u20131000000; 0 uses the mode cap."
565
878
  },
566
879
  {
567
880
  key: "codexMaxCallMs",
568
- valid: (value) => Number.isInteger(value) && (value === 0 || value >= 5000 && value <= 300000),
881
+ valid: (value) => validNumericLimit("codexMaxCallMs", value),
569
882
  message: () => "smart-compact config: codexMaxCallMs must be 0 or 5000\u2013300000; 0 derives a cap from maxTokens."
570
883
  },
571
884
  {
572
885
  key: "maxLatencyMs",
573
- valid: (value) => Number.isFinite(value) && (value === 0 || value >= 5000 && value <= 600000),
886
+ valid: (value) => validNumericLimit("maxLatencyMs", value),
574
887
  message: () => "smart-compact config: maxLatencyMs must be 0 or 5000\u2013600000; 0 means unlimited."
575
888
  },
576
889
  {
577
890
  key: "minContextPercent",
578
- valid: (value) => Number.isFinite(value) && value >= 0 && value <= 100,
891
+ valid: (value) => validNumericLimit("minContextPercent", value),
579
892
  message: (value) => "smart-compact config: minContextPercent must be 0\u2013100, got " + value + ". Using default " + DEFAULT_CONFIG.minContextPercent + "."
580
893
  }
581
894
  ];
@@ -600,39 +913,78 @@ function validateSmartCompactConfig(sc) {
600
913
  validateProfiles(sc);
601
914
  validateLimits(sc);
602
915
  }
916
+ async function writeGlobalConfigValue(configPath, value) {
917
+ assertGlobalConfigPath(configPath);
918
+ const file = settingsFile();
919
+ const release = await acquireLock(file);
920
+ try {
921
+ const root = readSettingsRoot(file);
922
+ const section = configuredSection(root);
923
+ setConfigPath(section, configPath, cloneGlobalConfigValue(value));
924
+ if (value === undefined && configPath === "agentToolAccess") {
925
+ delete section.agentToolEnabled;
926
+ }
927
+ const validated = structuredClone(section);
928
+ validateSmartCompactConfig(validated);
929
+ if (!sameJsonValue(configPathValue(validated, configPath), value)) {
930
+ throw new Error(`Invalid smartCompact setting: ${configPath}`);
931
+ }
932
+ root[CONFIG_KEY] = section;
933
+ await atomicWriteFile(file, JSON.stringify(root, null, 2) + `
934
+ `);
935
+ resetConfigCache();
936
+ return loadConfig();
937
+ } finally {
938
+ release();
939
+ }
940
+ }
603
941
  var cachedConfig = null;
604
942
  var cachedMtime = 0;
605
943
  var cachedPath = null;
944
+ function resetConfigCache() {
945
+ cachedConfig = null;
946
+ cachedMtime = 0;
947
+ cachedPath = null;
948
+ }
606
949
  function loadConfig() {
607
950
  try {
608
951
  const file = settingsFile();
609
- const stat = fs.statSync(file);
952
+ const stat = fs2.statSync(file);
610
953
  if (cachedConfig && cachedPath === file && stat.mtimeMs === cachedMtime)
611
- return cachedConfig;
612
- const raw = JSON.parse(fs.readFileSync(file, "utf-8"));
613
- const sc = raw[CONFIG_KEY] ?? raw[CONFIG_KEY_ALT] ?? {};
954
+ return cloneConfig(cachedConfig);
955
+ const parsed = JSON.parse(fs2.readFileSync(file, "utf-8"));
956
+ const raw = isRecord(parsed) ? parsed : {};
957
+ if (raw !== parsed) {
958
+ warn("smart-compact config: settings.json root must be an object.");
959
+ }
960
+ const configured = Object.hasOwn(raw, CONFIG_KEY) ? raw[CONFIG_KEY] : raw[CONFIG_KEY_ALT] ?? {};
961
+ const sc = isRecord(configured) ? configured : {};
962
+ if (sc !== configured) {
963
+ warn("smart-compact config: smartCompact must be an object.");
964
+ }
614
965
  validateSmartCompactConfig(sc);
615
- const merged = { ...DEFAULT_CONFIG, ...sc };
966
+ const merged = { ...defaultConfig(), ...sc };
616
967
  if (!("mode" in sc) && "profile" in sc) {
617
968
  merged.mode = sc.profile === "light" ? "thorough" : sc.profile;
618
969
  }
619
970
  if (sc.profiles) {
620
- merged.profiles = { ...PROFILES, ...sc.profiles };
971
+ const overrides = sc.profiles;
972
+ merged.profiles = Object.fromEntries(VALID_PROFILES.map((name) => [
973
+ name,
974
+ { ...PROFILES[name], ...overrides[name] }
975
+ ]));
621
976
  }
622
977
  if (!merged.backupDir)
623
978
  merged.backupDir = defaultBackupDir();
624
979
  cachedConfig = merged;
625
980
  cachedMtime = stat.mtimeMs;
626
981
  cachedPath = file;
627
- return cachedConfig;
982
+ return cloneConfig(cachedConfig);
628
983
  } catch (error2) {
629
984
  debug("loadConfig: settings.json not found or unreadable, using defaults", error2);
630
- cachedConfig = {
631
- ...DEFAULT_CONFIG,
632
- backupDir: defaultBackupDir()
633
- };
985
+ cachedConfig = defaultConfig();
634
986
  cachedPath = null;
635
- return cachedConfig;
987
+ return cloneConfig(cachedConfig);
636
988
  }
637
989
  }
638
990
 
@@ -935,17 +1287,17 @@ import fs3 from "fs";
935
1287
  import path4 from "path";
936
1288
 
937
1289
  // src/utils/extraction.ts
938
- import path2 from "path";
1290
+ import path3 from "path";
939
1291
 
940
1292
  // src/utils/type-guards.ts
941
- function isRecord(value) {
1293
+ function isRecord2(value) {
942
1294
  return typeof value === "object" && value !== null;
943
1295
  }
944
1296
  function isTextBlock(c) {
945
- return isRecord(c) && c.type === "text" && typeof c.text === "string";
1297
+ return isRecord2(c) && c.type === "text" && typeof c.text === "string";
946
1298
  }
947
1299
  function isToolCallBlock(c) {
948
- return isRecord(c) && c.type === "toolCall" && typeof c.name === "string" && isRecord(c.arguments);
1300
+ return isRecord2(c) && c.type === "toolCall" && typeof c.name === "string" && isRecord2(c.arguments);
949
1301
  }
950
1302
  function getToolCallNames(content) {
951
1303
  if (!Array.isArray(content))
@@ -1103,38 +1455,117 @@ function buildPathNeedles(filePath) {
1103
1455
  }
1104
1456
  return needles;
1105
1457
  }
1106
- function buildUniquePathNeedles(filePath, allPaths) {
1107
- const normalized = allPaths.map(normalizePath);
1458
+ var MAX_INDEXED_SUFFIX_CHARS = 1024;
1459
+ function buildPathNeedleOwnershipIndex(allPaths) {
1460
+ const owners = new Map;
1461
+ const normalizedPaths = allPaths.map(normalizePath);
1462
+ let hasUnindexedSuffixes = false;
1463
+ for (const normalized of normalizedPaths) {
1464
+ const suffixes = new Set([normalized]);
1465
+ for (let index = 0;index < normalized.length; index++) {
1466
+ if (normalized[index] !== "/")
1467
+ continue;
1468
+ if (normalized.length - index - 1 <= MAX_INDEXED_SUFFIX_CHARS) {
1469
+ suffixes.add(normalized.slice(index + 1));
1470
+ } else {
1471
+ hasUnindexedSuffixes = true;
1472
+ }
1473
+ }
1474
+ for (const suffix of suffixes) {
1475
+ owners.set(suffix, (owners.get(suffix) ?? 0) + 1);
1476
+ }
1477
+ }
1478
+ return { counts: owners, normalizedPaths, hasUnindexedSuffixes };
1479
+ }
1480
+ function buildUniquePathNeedlesFromIndex(filePath, owners) {
1108
1481
  return buildPathNeedles(filePath).filter((needle) => {
1109
- let owners = 0;
1110
- for (const candidate of normalized) {
1482
+ if (!owners.hasUnindexedSuffixes)
1483
+ return owners.counts.get(needle) === 1;
1484
+ let count = 0;
1485
+ for (const candidate of owners.normalizedPaths) {
1111
1486
  if (candidate === needle || candidate.endsWith("/" + needle))
1112
- owners++;
1113
- if (owners > 1)
1487
+ count++;
1488
+ if (count > 1)
1114
1489
  return false;
1115
1490
  }
1116
- return owners === 1;
1491
+ return count === 1;
1117
1492
  });
1118
1493
  }
1119
- function isKnownPathReference(ref, knownPaths) {
1120
- const normalizedRef = normalizePath(ref).replace(/^\/+/, "");
1121
- if (!normalizedRef)
1122
- return false;
1494
+ var PATH_CANDIDATE_CHAR_RE = /[\w./-]/;
1495
+ function buildKnownPathReferenceIndex(knownPaths) {
1496
+ const segmentSuffixes = new Set;
1497
+ const boundarySuffixes = new Set;
1498
+ const normalizedPaths = [];
1499
+ let hasUnindexedSuffixes = false;
1500
+ for (const path3 of knownPaths) {
1501
+ const normalizedPath = normalizePath(path3).replace(/^\/+/, "");
1502
+ if (!normalizedPath)
1503
+ continue;
1504
+ normalizedPaths.push(normalizedPath);
1505
+ segmentSuffixes.add(normalizedPath);
1506
+ for (let index = 0;index < normalizedPath.length; index++) {
1507
+ if (normalizedPath[index] === "/") {
1508
+ if (normalizedPath.length - index - 1 <= MAX_INDEXED_SUFFIX_CHARS) {
1509
+ segmentSuffixes.add(normalizedPath.slice(index + 1));
1510
+ } else {
1511
+ hasUnindexedSuffixes = true;
1512
+ }
1513
+ }
1514
+ if (index > 0 && !PATH_CANDIDATE_CHAR_RE.test(normalizedPath[index - 1])) {
1515
+ if (normalizedPath.length - index <= MAX_INDEXED_SUFFIX_CHARS) {
1516
+ boundarySuffixes.add(normalizedPath.slice(index));
1517
+ } else {
1518
+ hasUnindexedSuffixes = true;
1519
+ }
1520
+ }
1521
+ }
1522
+ }
1523
+ return {
1524
+ segmentSuffixes,
1525
+ sortedSegmentSuffixes: [...segmentSuffixes].sort(),
1526
+ boundarySuffixes,
1527
+ normalizedPaths,
1528
+ hasUnindexedSuffixes
1529
+ };
1530
+ }
1531
+ function matchesKnownPathReference(normalizedRef, normalizedPaths) {
1123
1532
  const pathShaped = normalizedRef.includes("/");
1124
- return knownPaths.some((path2) => {
1125
- const normalizedPath = normalizePath(path2).replace(/^\/+/, "");
1533
+ return normalizedPaths.some((normalizedPath) => {
1126
1534
  if (normalizedPath === normalizedRef || normalizedPath.endsWith("/" + normalizedRef))
1127
1535
  return true;
1128
1536
  if (normalizedPath.endsWith(normalizedRef)) {
1129
1537
  const boundary = normalizedPath[normalizedPath.length - normalizedRef.length - 1];
1130
- if (boundary && !/[\w./-]/.test(boundary))
1538
+ if (boundary && !PATH_CANDIDATE_CHAR_RE.test(boundary))
1131
1539
  return true;
1132
1540
  }
1133
1541
  if (!pathShaped)
1134
1542
  return false;
1135
- return normalizedPath.split("/").some((_, index, parts) => parts.slice(index).join("/").startsWith(normalizedRef + "/"));
1543
+ return normalizedPath.startsWith(normalizedRef + "/") || normalizedPath.includes("/" + normalizedRef + "/");
1136
1544
  });
1137
1545
  }
1546
+ function sortedHasPrefix(values, prefix) {
1547
+ let low = 0;
1548
+ let high = values.length;
1549
+ while (low < high) {
1550
+ const middle = low + high >>> 1;
1551
+ if (values[middle] < prefix)
1552
+ low = middle + 1;
1553
+ else
1554
+ high = middle;
1555
+ }
1556
+ return values[low]?.startsWith(prefix) ?? false;
1557
+ }
1558
+ function isKnownPathReferenceInIndex(ref, index) {
1559
+ const normalizedRef = normalizePath(ref).replace(/^\/+/, "");
1560
+ if (!normalizedRef)
1561
+ return false;
1562
+ if (index.segmentSuffixes.has(normalizedRef) || index.boundarySuffixes.has(normalizedRef)) {
1563
+ return true;
1564
+ }
1565
+ if (normalizedRef.includes("/") && sortedHasPrefix(index.sortedSegmentSuffixes, normalizedRef + "/"))
1566
+ return true;
1567
+ return index.hasUnindexedSuffixes ? matchesKnownPathReference(normalizedRef, index.normalizedPaths) : false;
1568
+ }
1138
1569
 
1139
1570
  // src/domain/tool-semantics.ts
1140
1571
  var PATH_KEYS = [
@@ -1352,8 +1783,12 @@ function classifyToolOperation(args, toolName) {
1352
1783
  function stableValue(value) {
1353
1784
  if (Array.isArray(value))
1354
1785
  return value.map(stableValue);
1355
- if (!value || typeof value !== "object")
1786
+ if (value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean")
1356
1787
  return value;
1788
+ if (typeof value === "bigint")
1789
+ return value.toString();
1790
+ if (typeof value !== "object")
1791
+ return;
1357
1792
  return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stableValue(item)]));
1358
1793
  }
1359
1794
  function commandIdentity(args) {
@@ -1473,15 +1908,15 @@ function smartKeepBoundaryCandidates(msgs, keepFromIndex, branchEntries) {
1473
1908
  }
1474
1909
  function collectToolCallIds(blocks, msgIndex, out) {
1475
1910
  for (const block of blocks) {
1476
- if (!isRecord(block) || block.type !== "toolCall")
1911
+ if (!isRecord2(block) || block.type !== "toolCall")
1477
1912
  continue;
1478
1913
  if (typeof block.id === "string")
1479
1914
  out.set(block.id, msgIndex);
1480
1915
  const args = block.arguments;
1481
- if (block.name !== "multi_tool_use.parallel" || !isRecord(args) || !Array.isArray(args.tool_uses))
1916
+ if (block.name !== "multi_tool_use.parallel" || !isRecord2(args) || !Array.isArray(args.tool_uses))
1482
1917
  continue;
1483
1918
  for (const nested of args.tool_uses) {
1484
- if (isRecord(nested) && typeof nested.id === "string")
1919
+ if (isRecord2(nested) && typeof nested.id === "string")
1485
1920
  out.set(nested.id, msgIndex);
1486
1921
  }
1487
1922
  }
@@ -1490,7 +1925,7 @@ function buildToolCallBoundaryIndex(msgs) {
1490
1925
  const map = new Map;
1491
1926
  for (let i = 0;i < msgs.length; i++) {
1492
1927
  const message = msgs[i].message;
1493
- if (!isRecord(message) || message.role !== "assistant")
1928
+ if (!isRecord2(message) || message.role !== "assistant")
1494
1929
  continue;
1495
1930
  const blocks = Array.isArray(message.content) ? message.content : [];
1496
1931
  collectToolCallIds(blocks, i, map);
@@ -1512,7 +1947,7 @@ function guardToolCallBoundary(msgs, keepFrom, tcMap = buildToolCallBoundaryInde
1512
1947
  changed = false;
1513
1948
  for (let i = adjusted;i < msgs.length; i++) {
1514
1949
  const message = msgs[i].message;
1515
- if (!isRecord(message) || message.role !== "toolResult")
1950
+ if (!isRecord2(message) || message.role !== "toolResult")
1516
1951
  continue;
1517
1952
  const tcId = typeof message.toolCallId === "string" ? message.toolCallId : undefined;
1518
1953
  if (!tcId)
@@ -1535,7 +1970,7 @@ function advancePastToolCallBoundary(msgs, keepFrom, tcMap = buildToolCallBounda
1535
1970
  let next = adjusted;
1536
1971
  for (let i = adjusted;i < msgs.length; i++) {
1537
1972
  const message = msgs[i].message;
1538
- if (!isRecord(message) || message.role !== "toolResult")
1973
+ if (!isRecord2(message) || message.role !== "toolResult")
1539
1974
  continue;
1540
1975
  const tcIdx = typeof message.toolCallId === "string" ? tcMap.get(message.toolCallId) : undefined;
1541
1976
  if (i === adjusted && tcIdx === undefined || tcIdx !== undefined && tcIdx < adjusted) {
@@ -1739,7 +2174,12 @@ function buildExplorationContext(report) {
1739
2174
  // src/utils/file-ref-detect.ts
1740
2175
  var CODE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs|rs|py|go|java|rb|cs|cpp|c|h|hpp|swift|kt|scala|php|css|scss|html|json|yaml|yml|toml|md|mdx|sh|sql|tf|ini|env|lock|gradle|xml)$/i;
1741
2176
  var VERSION_RE = /^v?\d+(?:\.\d+)+(?:[-+][\w.-]+)?$/i;
1742
- var FILE_REF_CANDIDATE_RE = /[\w.\/-]+\.[\w]+/g;
2177
+ function isAsciiWordCode(code) {
2178
+ return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code === 95 || code >= 97 && code <= 122;
2179
+ }
2180
+ function isCandidateCode(code) {
2181
+ return isAsciiWordCode(code) || code === 45 || code === 46 || code === 47;
2182
+ }
1743
2183
  function isLikelyFileRef(candidate) {
1744
2184
  if (candidate.startsWith("//") || VERSION_RE.test(candidate))
1745
2185
  return false;
@@ -1750,13 +2190,32 @@ function isLikelyFileRef(candidate) {
1750
2190
  return CODE_EXT_RE.test(candidate);
1751
2191
  }
1752
2192
  function extractFileRefs(summary) {
1753
- const matcher = new RegExp(FILE_REF_CANDIDATE_RE.source, FILE_REF_CANDIDATE_RE.flags);
1754
2193
  const refs = [];
1755
- for (const match of summary.matchAll(matcher)) {
1756
- if (/[\\/]/.test(summary[(match.index ?? 0) + match[0].length] ?? ""))
2194
+ let cursor = 0;
2195
+ while (cursor < summary.length) {
2196
+ while (cursor < summary.length && !isCandidateCode(summary.charCodeAt(cursor)))
2197
+ cursor++;
2198
+ const runStart = cursor;
2199
+ while (cursor < summary.length && isCandidateCode(summary.charCodeAt(cursor)))
2200
+ cursor++;
2201
+ const runEnd = cursor;
2202
+ let extensionDot = -1;
2203
+ for (let index = runStart + 1;index + 1 < runEnd; index++) {
2204
+ if (summary.charCodeAt(index) === 46 && isAsciiWordCode(summary.charCodeAt(index + 1))) {
2205
+ extensionDot = index;
2206
+ }
2207
+ }
2208
+ if (extensionDot < 0)
2209
+ continue;
2210
+ let matchEnd = extensionDot + 2;
2211
+ while (matchEnd < runEnd && isAsciiWordCode(summary.charCodeAt(matchEnd))) {
2212
+ matchEnd++;
2213
+ }
2214
+ const candidate = summary.slice(runStart, matchEnd);
2215
+ if (/[\\/]/.test(summary[matchEnd] ?? ""))
1757
2216
  continue;
1758
- if (isLikelyFileRef(match[0]))
1759
- refs.push(match[0]);
2217
+ if (isLikelyFileRef(candidate))
2218
+ refs.push(candidate);
1760
2219
  }
1761
2220
  return refs;
1762
2221
  }
@@ -1863,7 +2322,7 @@ function buildSummaryPathEvidence(paths, budgetTokens = PROFILES.balanced.summar
1863
2322
  const unique = Array.from(new Set(paths.filter(Boolean)));
1864
2323
  if (!unique.length)
1865
2324
  return new Map;
1866
- const full = unique.map((path2) => [path2, summaryPathLine(path2)]);
2325
+ const full = unique.map((path3) => [path3, summaryPathLine(path3)]);
1867
2326
  const minimumPerLine = JSON.stringify("#" + "x".repeat(12)).length + 3;
1868
2327
  const budgetChars = Math.max(unique.length * minimumPerLine, Math.min(20000, Math.max(4000, Math.floor(budgetTokens * 2))));
1869
2328
  if (full.reduce((total, [, line]) => total + line.length + 3, 0) <= budgetChars) {
@@ -1871,21 +2330,21 @@ function buildSummaryPathEvidence(paths, budgetTokens = PROFILES.balanced.summar
1871
2330
  }
1872
2331
  const digests = new Map;
1873
2332
  const owners = new Map;
1874
- for (const path2 of unique) {
1875
- const fullDigest = createHash("sha256").update(path2).digest("base64url");
2333
+ for (const path3 of unique) {
2334
+ const fullDigest = createHash("sha256").update(path3).digest("base64url");
1876
2335
  let digest = fullDigest.slice(0, 12);
1877
2336
  const owner = owners.get(digest);
1878
- if (owner && owner !== path2) {
2337
+ if (owner && owner !== path3) {
1879
2338
  digest = fullDigest;
1880
2339
  digests.set(owner, createHash("sha256").update(owner).digest("base64url"));
1881
2340
  }
1882
- owners.set(digest, path2);
1883
- digests.set(path2, digest);
2341
+ owners.set(digest, path3);
2342
+ digests.set(path3, digest);
1884
2343
  }
1885
2344
  const perPath = Math.max(JSON.stringify("#" + "x".repeat(12)).length, Math.floor((budgetChars - unique.length * 3) / unique.length));
1886
- return new Map(unique.map((path2) => [
1887
- path2,
1888
- compactPathLine(path2, perPath, digests.get(path2) ?? "")
2345
+ return new Map(unique.map((path3) => [
2346
+ path3,
2347
+ compactPathLine(path3, perPath, digests.get(path3) ?? "")
1889
2348
  ]));
1890
2349
  }
1891
2350
  function mergeBodies(first, second) {
@@ -2403,7 +2862,7 @@ function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
2403
2862
  const shiftBasename = (filePath) => {
2404
2863
  if (!filePath)
2405
2864
  return null;
2406
- const base = path2.basename(filePath);
2865
+ const base = path3.basename(filePath);
2407
2866
  return GENERIC_BASENAMES.has(base.toLowerCase()) ? null : base;
2408
2867
  };
2409
2868
  const topics = [];
@@ -2557,7 +3016,7 @@ function extractOpenLoops(msgs, extraction) {
2557
3016
  }));
2558
3017
  for (const err of extraction.errors.filter((e) => !e.resolved)) {
2559
3018
  const errLower = err.message.toLowerCase();
2560
- const errFiles = fileNeedles.filter(({ needles }) => needles.some((n) => errLower.includes(n))).map(({ path: path3 }) => path3);
3019
+ const errFiles = fileNeedles.filter(({ needles }) => needles.some((n) => errLower.includes(n))).map(({ path: path4 }) => path4);
2561
3020
  loops.push({
2562
3021
  id: ID_PREFIX.OPEN_LOOP + ++loopId,
2563
3022
  type: "bugfix",
@@ -2706,191 +3165,6 @@ function extractStructured(msgs, pc, precomputedTcIdx) {
2706
3165
  };
2707
3166
  }
2708
3167
 
2709
- // src/infra/fs.ts
2710
- import fs2 from "fs";
2711
- import fsp from "fs/promises";
2712
- import path3 from "path";
2713
- import crypto from "crypto";
2714
- var LOCK_STALE_MS = 5000;
2715
- var LOCK_RETRY_MS = 25;
2716
- var LOCK_MAX_RETRIES = 80;
2717
- function ensureDir(dir) {
2718
- fs2.mkdirSync(dir, { recursive: true, mode: 448 });
2719
- fs2.chmodSync(dir, 448);
2720
- }
2721
- async function ensureDirAsync(dir) {
2722
- await fsp.mkdir(dir, { recursive: true, mode: 448 });
2723
- await fsp.chmod(dir, 448);
2724
- }
2725
- function tempPath(target) {
2726
- return target + ".tmp." + process.pid + "." + crypto.randomBytes(4).toString("hex");
2727
- }
2728
- function atomicWriteFileSync(target, data) {
2729
- ensureDir(path3.dirname(target));
2730
- const tmp = tempPath(target);
2731
- try {
2732
- fs2.writeFileSync(tmp, data, { mode: 384 });
2733
- fs2.renameSync(tmp, target);
2734
- fs2.chmodSync(target, 384);
2735
- } catch (e) {
2736
- try {
2737
- fs2.unlinkSync(tmp);
2738
- } catch {}
2739
- throw e;
2740
- }
2741
- }
2742
- async function atomicWriteFile(target, data) {
2743
- await ensureDirAsync(path3.dirname(target));
2744
- const tmp = tempPath(target);
2745
- try {
2746
- await fsp.writeFile(tmp, data, { mode: 384 });
2747
- await fsp.rename(tmp, target);
2748
- await fsp.chmod(target, 384);
2749
- } catch (e) {
2750
- try {
2751
- await fsp.unlink(tmp);
2752
- } catch {}
2753
- throw e;
2754
- }
2755
- }
2756
- function tryAcquireLock(target) {
2757
- const lockDir = target + ".lock";
2758
- for (let reclaimAttempt = 0;reclaimAttempt < 2; reclaimAttempt++) {
2759
- try {
2760
- fs2.mkdirSync(lockDir, { mode: 448 });
2761
- return () => {
2762
- try {
2763
- fs2.rmdirSync(lockDir);
2764
- } catch {}
2765
- };
2766
- } catch (error2) {
2767
- if (error2?.code !== "EEXIST") {
2768
- throw new Error("Failed to acquire lock for " + target, { cause: error2 });
2769
- }
2770
- try {
2771
- const stat = fs2.statSync(lockDir);
2772
- if (Date.now() - stat.mtimeMs <= LOCK_STALE_MS)
2773
- return null;
2774
- const stolen = lockDir + ".stale." + process.pid + "." + crypto.randomBytes(4).toString("hex");
2775
- fs2.renameSync(lockDir, stolen);
2776
- const stolenStat = fs2.statSync(stolen);
2777
- if (Date.now() - stolenStat.mtimeMs > LOCK_STALE_MS)
2778
- fs2.rmdirSync(stolen);
2779
- else
2780
- try {
2781
- fs2.renameSync(stolen, lockDir);
2782
- } catch {}
2783
- } catch {}
2784
- }
2785
- }
2786
- return null;
2787
- }
2788
- function acquireLockSync(target) {
2789
- const release = tryAcquireLock(target);
2790
- if (!release)
2791
- throw new Error("Lock busy for " + target);
2792
- return release;
2793
- }
2794
- async function acquireLock(target) {
2795
- for (let attempt = 0;attempt < LOCK_MAX_RETRIES; attempt++) {
2796
- const release = tryAcquireLock(target);
2797
- if (release)
2798
- return release;
2799
- const delay = Promise.withResolvers();
2800
- setTimeout(delay.resolve, LOCK_RETRY_MS);
2801
- await delay.promise;
2802
- }
2803
- throw new Error("Timed out acquiring lock for " + target);
2804
- }
2805
- async function appendLineLockedAsync(target, line, maxBytes) {
2806
- await ensureDirAsync(path3.dirname(target));
2807
- const payload = Buffer.from(line.endsWith(`
2808
- `) ? line : line + `
2809
- `);
2810
- if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)) {
2811
- throw new Error("maxBytes must be a positive safe integer");
2812
- }
2813
- if (maxBytes !== undefined && payload.length > maxBytes) {
2814
- throw new Error("Log entry exceeds retention cap for " + target);
2815
- }
2816
- const release = await acquireLock(target);
2817
- try {
2818
- let stat = null;
2819
- try {
2820
- stat = await fsp.stat(target);
2821
- } catch (error2) {
2822
- if (!error2 || typeof error2 !== "object" || !("code" in error2) || error2.code !== "ENOENT")
2823
- throw error2;
2824
- }
2825
- if (maxBytes !== undefined && stat && stat.size + payload.length > maxBytes) {
2826
- const retainedLength = Math.min(stat.size, Math.max(0, maxBytes - payload.length));
2827
- const buffer = Buffer.allocUnsafe(retainedLength);
2828
- if (retainedLength > 0) {
2829
- const handle = await fsp.open(target, "r");
2830
- try {
2831
- await handle.read(buffer, 0, retainedLength, stat.size - retainedLength);
2832
- } finally {
2833
- await handle.close();
2834
- }
2835
- }
2836
- let tail = buffer.toString("utf8");
2837
- if (retainedLength < stat.size) {
2838
- const firstNewline = tail.indexOf(`
2839
- `);
2840
- tail = firstNewline >= 0 ? tail.slice(firstNewline + 1) : "";
2841
- }
2842
- await atomicWriteFile(target, tail);
2843
- }
2844
- await fsp.appendFile(target, payload, { mode: 384 });
2845
- await fsp.chmod(target, 384);
2846
- } finally {
2847
- release();
2848
- }
2849
- }
2850
- function readJsonlTail(target, limit, maxBytes = 512 * 1024) {
2851
- if (limit <= 0 || !fs2.existsSync(target))
2852
- return [];
2853
- const stat = fs2.statSync(target);
2854
- const length = Math.min(stat.size, maxBytes);
2855
- const buffer = Buffer.alloc(length);
2856
- const fd = fs2.openSync(target, "r");
2857
- try {
2858
- fs2.readSync(fd, buffer, 0, length, stat.size - length);
2859
- } finally {
2860
- fs2.closeSync(fd);
2861
- }
2862
- let text = buffer.toString("utf8");
2863
- if (stat.size > length) {
2864
- const newline = text.indexOf(`
2865
- `);
2866
- text = newline >= 0 ? text.slice(newline + 1) : "";
2867
- }
2868
- const values = [];
2869
- for (const line of text.split(`
2870
- `)) {
2871
- if (!line)
2872
- continue;
2873
- try {
2874
- values.push(JSON.parse(line));
2875
- } catch {}
2876
- }
2877
- return values.slice(-limit);
2878
- }
2879
- function readJsonSync(target) {
2880
- try {
2881
- if (!fs2.existsSync(target))
2882
- return null;
2883
- const raw = fs2.readFileSync(target, "utf8");
2884
- return JSON.parse(raw);
2885
- } catch (e) {
2886
- warn("readJsonSync failed for " + target, e);
2887
- return null;
2888
- }
2889
- }
2890
- function writeJsonSync(target, value, pretty = false) {
2891
- atomicWriteFileSync(target, pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value));
2892
- }
2893
-
2894
3168
  // src/utils/id-fingerprint.ts
2895
3169
  import crypto2 from "crypto";
2896
3170
  var FINGERPRINT_TAIL_LEN = 16;
@@ -4780,12 +5054,9 @@ function advance(rc, stage) {
4780
5054
  const marker = stage;
4781
5055
  const index = STAGE_ORDER.indexOf(marker);
4782
5056
  const record = rc;
4783
- const hasStageHistory = STAGE_ORDER.some((candidate) => record[candidate] === true);
4784
- if (hasStageHistory) {
4785
- for (let prior = 0;prior < index; prior++) {
4786
- if (record[STAGE_ORDER[prior]] !== true) {
4787
- throw new Error("Pipeline stage out of order: " + marker + " requires " + STAGE_ORDER[prior]);
4788
- }
5057
+ for (let prior = 0;prior < index; prior++) {
5058
+ if (record[STAGE_ORDER[prior]] !== true) {
5059
+ throw new Error("Pipeline stage out of order: " + marker + " requires " + STAGE_ORDER[prior]);
4789
5060
  }
4790
5061
  }
4791
5062
  for (const field of STAGE_REQUIRED_FIELDS[marker]) {
@@ -4828,7 +5099,7 @@ function advancePastNonPortableToolExchanges(msgs, keepFrom, toolCallIndex) {
4828
5099
  const exchangeEnds = new Map;
4829
5100
  for (let index = keepFrom;index < msgs.length; index++) {
4830
5101
  const message = msgs[index].message;
4831
- if (!isRecord(message) || message.role !== "toolResult" || typeof message.toolCallId !== "string")
5102
+ if (!isRecord2(message) || message.role !== "toolResult" || typeof message.toolCallId !== "string")
4832
5103
  continue;
4833
5104
  const callIndex = toolCallIndex.get(message.toolCallId);
4834
5105
  if (callIndex === undefined || callIndex < keepFrom)
@@ -4838,10 +5109,10 @@ function advancePastNonPortableToolExchanges(msgs, keepFrom, toolCallIndex) {
4838
5109
  let adjusted = keepFrom;
4839
5110
  for (let index = keepFrom;index < msgs.length; index++) {
4840
5111
  const message = msgs[index].message;
4841
- if (!isRecord(message))
5112
+ if (!isRecord2(message))
4842
5113
  continue;
4843
5114
  if (message.role === "assistant" && Array.isArray(message.content)) {
4844
- const nonPortable = message.content.some((block) => isRecord(block) && block.type === "toolCall" && (typeof block.name !== "string" || !PORTABLE_TOOL_NAME_RE.test(block.name)));
5115
+ const nonPortable = message.content.some((block) => isRecord2(block) && block.type === "toolCall" && (typeof block.name !== "string" || !PORTABLE_TOOL_NAME_RE.test(block.name)));
4845
5116
  if (nonPortable)
4846
5117
  adjusted = Math.max(adjusted, exchangeEnds.get(index) ?? index + 1);
4847
5118
  } else if (message.role === "toolResult" && typeof message.toolName === "string" && !PORTABLE_TOOL_NAME_RE.test(message.toolName)) {
@@ -4908,7 +5179,7 @@ function planCompactionWindow(input) {
4908
5179
  let protectedUserIndex;
4909
5180
  for (let index = msgs.length - 1;index >= 0; index--) {
4910
5181
  const message = msgs[index].message;
4911
- if (!isRecord(message) || message.role !== "user")
5182
+ if (!isRecord2(message) || message.role !== "user")
4912
5183
  continue;
4913
5184
  userOrdinal++;
4914
5185
  protectedUserIndex = index;
@@ -4939,7 +5210,7 @@ function planCompactionWindow(input) {
4939
5210
  const targetAfterTokens = !force && modelContextWindow ? modelContextWindow * targetPercent / 100 : fixedContextTokens + effectiveRetentionCeiling + finalSummaryAllowance;
4940
5211
  let reason = "viable";
4941
5212
  const firstKeptMessage = msgs[keepFrom]?.message;
4942
- if (nonPortableTailBlocked || isRecord(firstKeptMessage) && firstKeptMessage.role === "toolResult")
5213
+ if (nonPortableTailBlocked || isRecord2(firstKeptMessage) && firstKeptMessage.role === "toolResult")
4943
5214
  reason = "unsafe-tool-boundary";
4944
5215
  else if (keepFrom <= 0)
4945
5216
  reason = "no-eligible-prefix";
@@ -6131,12 +6402,15 @@ async function showResultScreen(ctx, details, extraction, services, opts = {}) {
6131
6402
  c.addChild(new Text2(theme.fg("text", opts.summary), 2, 0));
6132
6403
  c.addChild(new Text2("", 0, 0));
6133
6404
  }
6405
+ const scrollbarStyle = (text) => theme.fg("borderMuted", text);
6134
6406
  const scroll = new ScrollView(c, {
6135
6407
  follow: "none",
6136
6408
  primary: true,
6137
6409
  overscroll: "contain",
6138
6410
  scrollbar: "auto",
6139
- scrollbarStyle: (text) => theme.fg("borderMuted", text)
6411
+ scrollbarStyle,
6412
+ scrollbarTrackStyle: scrollbarStyle,
6413
+ scrollbarThumbStyle: scrollbarStyle
6140
6414
  });
6141
6415
  const footer = new Container2;
6142
6416
  footer.addChild(new DynamicBorder2((s) => theme.fg("accent", s)));
@@ -7334,8 +7608,12 @@ function boundedExplorationValue(value, depth = 0) {
7334
7608
  if (typeof value === "string") {
7335
7609
  return value.length > TRUNC.PREVIEW_XL ? value.slice(0, TRUNC.PREVIEW_XL) + "\u2026" : value;
7336
7610
  }
7337
- if (value == null || typeof value !== "object")
7611
+ if (value == null || typeof value === "number" || typeof value === "boolean")
7338
7612
  return value;
7613
+ if (typeof value === "bigint")
7614
+ return value.toString();
7615
+ if (typeof value !== "object")
7616
+ return;
7339
7617
  if (depth >= 3)
7340
7618
  return "[bounded]";
7341
7619
  if (Array.isArray(value))
@@ -7344,7 +7622,7 @@ function boundedExplorationValue(value, depth = 0) {
7344
7622
  }
7345
7623
  function serializeExplorationResult(value, scrubber) {
7346
7624
  const safe = boundedExplorationValue(scrubber.scrubValue(value).value);
7347
- const serialized = JSON.stringify(safe);
7625
+ const serialized = JSON.stringify(safe) ?? "null";
7348
7626
  if (serialized.length <= MAX_EXPLORER_OUTPUT_CHARS)
7349
7627
  return serialized;
7350
7628
  let excerptChars = Math.max(0, Math.floor((MAX_EXPLORER_OUTPUT_CHARS - 160) / 2));
@@ -7993,16 +8271,75 @@ function batchFieldPattern(name) {
7993
8271
  let pattern = batchFieldPatterns.get(name);
7994
8272
  if (!pattern) {
7995
8273
  const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7996
- pattern = new RegExp("\\*\\*" + escaped + "\\*\\*:\\s*(.+?)(?:\\n|$)", "i");
8274
+ pattern = new RegExp("(?:^|\\n)\\*\\*" + escaped + "\\*\\*:\\s*(.+?)(?:\\n|$)", "i");
7997
8275
  batchFieldPatterns.set(name, pattern);
7998
8276
  }
7999
8277
  return pattern;
8000
8278
  }
8279
+
8280
+ class BatchSummaryFormatError extends Error {
8281
+ name = "BatchSummaryFormatError";
8282
+ constructor(reason) {
8283
+ super("Malformed batch summary response: " + reason);
8284
+ }
8285
+ }
8286
+ var BATCH_REQUIRED_FIELDS = [
8287
+ "Priority",
8288
+ "Summary",
8289
+ "Decisions",
8290
+ "Modified",
8291
+ "Deleted",
8292
+ "Read"
8293
+ ];
8294
+ function assertCompleteBatchResponse(stopReason, sectionMap, duplicateIds, batchSize) {
8295
+ const reason = String(stopReason ?? "");
8296
+ if (reason !== "stop" && reason !== "endTurn") {
8297
+ throw new BatchSummaryFormatError("non-terminal stop reason " + (reason || "unknown"));
8298
+ }
8299
+ if (duplicateIds.size > 0) {
8300
+ throw new BatchSummaryFormatError("duplicate chunk id(s): " + [...duplicateIds].sort((a, b) => a - b).join(", "));
8301
+ }
8302
+ const unexpected = [...sectionMap.keys()].filter((id) => id < 1 || id > batchSize);
8303
+ if (unexpected.length > 0) {
8304
+ throw new BatchSummaryFormatError("unexpected chunk id(s): " + unexpected.sort((a, b) => a - b).join(", "));
8305
+ }
8306
+ const missingSections = [];
8307
+ for (let id = 1;id <= batchSize; id++) {
8308
+ if (!sectionMap.has(id))
8309
+ missingSections.push(id);
8310
+ }
8311
+ if (missingSections.length > 0) {
8312
+ throw new BatchSummaryFormatError("missing chunk section(s): " + missingSections.join(", "));
8313
+ }
8314
+ for (let id = 1;id <= batchSize; id++) {
8315
+ const section = sectionMap.get(id) ?? "";
8316
+ const missingFields = BATCH_REQUIRED_FIELDS.filter((field) => !batchFieldPattern(field).test(section));
8317
+ if (missingFields.length > 0) {
8318
+ throw new BatchSummaryFormatError("chunk " + id + " missing field(s): " + missingFields.join(", "));
8319
+ }
8320
+ }
8321
+ const missing = [];
8322
+ for (let id = 1;id <= batchSize; id++) {
8323
+ const section = sectionMap.get(id);
8324
+ const summary = section?.match(batchFieldPattern("Summary"))?.[1].trim();
8325
+ if (!summary || summary.toLowerCase() === "none")
8326
+ missing.push(id);
8327
+ }
8328
+ if (missing.length > 0) {
8329
+ throw new BatchSummaryFormatError("missing usable Summary for chunk(s): " + missing.join(", "));
8330
+ }
8331
+ }
8001
8332
  function boundedToolArgs(value, depth = 0) {
8002
8333
  if (typeof value === "string")
8003
8334
  return value.length > TRUNC.DETAIL ? value.slice(0, TRUNC.DETAIL) + "\u2026" : value;
8004
- if (value == null || typeof value !== "object" || depth >= 2)
8335
+ if (value == null || typeof value === "number" || typeof value === "boolean")
8005
8336
  return value;
8337
+ if (typeof value === "bigint")
8338
+ return value.toString();
8339
+ if (typeof value !== "object")
8340
+ return;
8341
+ if (depth >= 2)
8342
+ return "[bounded]";
8006
8343
  if (Array.isArray(value))
8007
8344
  return value.slice(0, 8).map((item) => boundedToolArgs(item, depth + 1));
8008
8345
  return Object.fromEntries(Object.entries(value).slice(0, 12).map(([key, item]) => [key, boundedToolArgs(item, depth + 1)]));
@@ -8316,13 +8653,19 @@ async function summarizeBatch(batch, extraction, model, auth, signal, services,
8316
8653
  const output = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
8317
8654
  `);
8318
8655
  const sectionMap = new Map;
8656
+ const duplicateIds = new Set;
8319
8657
  const sections = output.split(/^### /m).filter((s) => s.trim());
8320
8658
  for (const sec of sections) {
8321
8659
  const m = sec.match(/^CHUNK\s+(\d+):\s*(.*?)\n/i);
8322
8660
  if (m) {
8323
- sectionMap.set(parseInt(m[1], 10), sec);
8661
+ const id = parseInt(m[1], 10);
8662
+ if (sectionMap.has(id))
8663
+ duplicateIds.add(id);
8664
+ else
8665
+ sectionMap.set(id, sec);
8324
8666
  }
8325
8667
  }
8668
+ assertCompleteBatchResponse(resp.stopReason, sectionMap, duplicateIds, batch.length);
8326
8669
  const result = batch.map((ch, i) => {
8327
8670
  const id = i + 1;
8328
8671
  const sec = sectionMap.get(id) ?? "";
@@ -9570,8 +9913,9 @@ function verifyFileReferences(summary, extraction, continuity, evidence, collect
9570
9913
  ...(continuity?.unresolvedErrors ?? []).flatMap((error2) => error2.files),
9571
9914
  ...(continuity?.openLoops ?? []).flatMap((loop) => loop.files)
9572
9915
  ]));
9916
+ const knownFileIndex = buildKnownPathReferenceIndex(knownFiles);
9573
9917
  for (const ref of new Set(extractFileRefs(summary))) {
9574
- const grounded = isKnownPathReference(ref, knownFiles) || Boolean(evidence.sourceMessages && sourceSupportsFileReference(ref, evidence.sourceMessages));
9918
+ const grounded = isKnownPathReferenceInIndex(ref, knownFileIndex) || Boolean(evidence.sourceMessages && sourceSupportsFileReference(ref, evidence.sourceMessages));
9575
9919
  if (!grounded)
9576
9920
  addGap(accumulator, { kind: "fabricated-file", ref }, 4);
9577
9921
  }
@@ -9589,9 +9933,10 @@ function verifyProgressConsistency(parsed, extraction, collected, paths, accumul
9589
9933
  }, 12);
9590
9934
  }
9591
9935
  const doneRefs = new Set(extractFileRefs(done).map(normalizePath));
9936
+ const modifiedPathOwners = buildPathNeedleOwnershipIndex(paths.modified);
9592
9937
  for (const file of extraction.modifiedFiles) {
9593
- const needles = buildUniquePathNeedles(file.path, paths.modified);
9594
- if (!needles.some((needle) => doneRefs.has(normalizePath(needle))))
9938
+ const needles = buildUniquePathNeedlesFromIndex(file.path, modifiedPathOwners);
9939
+ if (!needles.some((needle) => doneRefs.has(needle)))
9595
9940
  continue;
9596
9941
  const unresolved = collected.unresolved.find((error2) => {
9597
9942
  const firstLine = error2.message.split(/\r?\n/, 1)[0] ?? "";
@@ -11420,6 +11765,19 @@ async function runSmartCompact(opts) {
11420
11765
  }
11421
11766
  }
11422
11767
 
11768
+ // src/app/global-settings-runtime.ts
11769
+ var POLICY_PATHS = new Set([
11770
+ "agentToolAccess",
11771
+ "autoTrigger",
11772
+ "showStatus"
11773
+ ]);
11774
+ function applyGlobalSettingRuntime(path14, ctx, policy, contextTools) {
11775
+ if (POLICY_PATHS.has(path14))
11776
+ policy.restore(ctx);
11777
+ if (path14 === "contextGraphEnabled")
11778
+ contextTools.apply();
11779
+ }
11780
+
11423
11781
  // src/app/pending-slot.ts
11424
11782
  function createPendingSlot(opts) {
11425
11783
  const ttlMs = opts.ttlMs;
@@ -11835,6 +12193,7 @@ function resolveGraphScope(ctx) {
11835
12193
  };
11836
12194
  }
11837
12195
  function registerContextTools(pi) {
12196
+ const availability = createContextToolAvailability(pi);
11838
12197
  pi.registerTool({
11839
12198
  name: "smart_recall",
11840
12199
  label: "Smart Recall",
@@ -12045,6 +12404,42 @@ Paths: ` + relatedPaths.join(", ") : ""));
12045
12404
  }
12046
12405
  }
12047
12406
  });
12407
+ availability.apply();
12408
+ pi.on("session_start", availability.apply);
12409
+ return availability;
12410
+ }
12411
+ var CONTEXT_TOOL_NAMES = ["smart_recall", "smart_save_memory"];
12412
+ function createContextToolAvailability(pi) {
12413
+ const hiddenByConfig = new Set;
12414
+ let disabledByConfig = false;
12415
+ return {
12416
+ apply() {
12417
+ try {
12418
+ const enabled = loadConfig().contextGraphEnabled;
12419
+ const active = pi.getActiveTools();
12420
+ if (!enabled) {
12421
+ const visibleContextTools = CONTEXT_TOOL_NAMES.filter((name) => active.includes(name));
12422
+ for (const name of visibleContextTools)
12423
+ hiddenByConfig.add(name);
12424
+ disabledByConfig = true;
12425
+ if (visibleContextTools.length > 0) {
12426
+ pi.setActiveTools(active.filter((name) => !CONTEXT_TOOL_NAMES.includes(name)));
12427
+ }
12428
+ return;
12429
+ }
12430
+ if (disabledByConfig) {
12431
+ const restored = [...hiddenByConfig].filter((name) => !active.includes(name));
12432
+ if (restored.length > 0) {
12433
+ pi.setActiveTools([...new Set([...active, ...restored])]);
12434
+ }
12435
+ hiddenByConfig.clear();
12436
+ disabledByConfig = false;
12437
+ }
12438
+ } catch (error2) {
12439
+ debugError("Context tool availability update failed", error2);
12440
+ }
12441
+ }
12442
+ };
12048
12443
  }
12049
12444
 
12050
12445
  // src/app/model-routing.ts
@@ -13069,239 +13464,1033 @@ async function showMetricsDashboardUI(ctx, opts) {
13069
13464
  label: "Overview report",
13070
13465
  desc: entries.length + " run(s) \xB7 Data Confidence " + insights.confidence.score + "/100"
13071
13466
  },
13072
- ...hasQualityData ? [
13073
- {
13074
- view: "quality",
13075
- label: "Quality & confidence",
13076
- desc: "Verifier evidence, repair gain, and \u226585 trust target"
13077
- }
13078
- ] : [],
13467
+ ...hasQualityData ? [
13468
+ {
13469
+ view: "quality",
13470
+ label: "Quality & confidence",
13471
+ desc: "Verifier evidence, repair gain, and \u226585 trust target"
13472
+ }
13473
+ ] : [],
13474
+ {
13475
+ view: "providers",
13476
+ label: "Provider routes",
13477
+ desc: insights.providers.length + " stage/provider/model comparison row(s)"
13478
+ },
13479
+ {
13480
+ view: "canary",
13481
+ label: "Canary vs stable",
13482
+ desc: insights.canary.decision.toUpperCase() + " \xB7 " + insights.canary.dataConfidence + "% canary confidence"
13483
+ },
13484
+ {
13485
+ view: "latest",
13486
+ label: "Latest run details",
13487
+ desc: latest ? formatMetricRunCompact(latest) : "No run recorded yet"
13488
+ },
13489
+ {
13490
+ view: "session",
13491
+ label: "Current session",
13492
+ desc: (opts.currentSessionId ?? "unknown") + " \u2014 " + currentRuns.length + " run(s)"
13493
+ },
13494
+ {
13495
+ view: "recent",
13496
+ label: "Recent runs",
13497
+ desc: "Last " + Math.min(entries.length, 30) + " run(s)"
13498
+ },
13499
+ {
13500
+ action: "html",
13501
+ label: "Write HTML dashboard",
13502
+ desc: "Generate ~/.pi/agent/.cache/smart-compact-report.html"
13503
+ }
13504
+ ];
13505
+ return ctx.ui.custom((tui, theme, keybindings, done) => {
13506
+ let view = "menu";
13507
+ let selected = 0;
13508
+ let scroll = 0;
13509
+ const pageLines = () => {
13510
+ switch (view) {
13511
+ case "overview":
13512
+ return opts.report.split(`
13513
+ `);
13514
+ case "quality":
13515
+ return formatDashboardQuality(insights);
13516
+ case "providers":
13517
+ return formatDashboardProviders(insights);
13518
+ case "canary":
13519
+ return formatDashboardCanary(insights);
13520
+ case "latest":
13521
+ return formatRunDetails(latest, "Latest run details");
13522
+ case "session":
13523
+ return formatCurrentSession(entries, opts.currentSessionId);
13524
+ case "recent":
13525
+ return formatRecentRuns(entries);
13526
+ case "menu":
13527
+ return [];
13528
+ default:
13529
+ return [];
13530
+ }
13531
+ };
13532
+ const resetPage = (nextView) => {
13533
+ view = nextView;
13534
+ scroll = 0;
13535
+ };
13536
+ const qualityStatus = () => {
13537
+ if (!hasQualityData)
13538
+ return theme.fg("dim", " \u2022 Quality unavailable");
13539
+ return theme.fg(insights.quality.targetMet ? "success" : "warning", " \u2022 Quality " + insights.quality.healthScore + "/100");
13540
+ };
13541
+ const renderHeader = (width) => [
13542
+ truncateToWidth3(theme.fg("accent", theme.bold(" \uD83D\uDCCA Smart Compact Dashboard")) + theme.fg("dim", " " + entries.length + " recorded run(s)"), width),
13543
+ truncateToWidth3(theme.fg("dim", " session: " + (opts.currentSessionId ?? "unknown")) + theme.fg("dim", latest && Number.isFinite(latest.verificationScore) ? " \u2022 latest score " + metricScore(latest) : "") + theme.fg(insights.confidence.targetMet ? "success" : "warning", " \u2022 Data Confidence " + insights.confidence.score + "/100") + qualityStatus(), width),
13544
+ truncateToWidth3(theme.fg("borderMuted", "\u2500".repeat(Math.max(0, width))), width)
13545
+ ];
13546
+ return {
13547
+ render(width) {
13548
+ const lines = renderHeader(width);
13549
+ if (view === "menu") {
13550
+ lines.push(truncateToWidth3(theme.fg("text", " Choose what to inspect:"), width), "");
13551
+ for (let index = 0;index < menuItems.length; index++) {
13552
+ const item = menuItems[index];
13553
+ const active = index === selected;
13554
+ const label = active ? theme.fg("accent", theme.bold(item.label)) : theme.fg("text", item.label);
13555
+ lines.push(truncateToWidth3((active ? " \u203A " : " ") + label, width));
13556
+ lines.push(truncateToWidth3(" " + theme.fg(active ? "muted" : "dim", item.desc), width));
13557
+ }
13558
+ lines.push("", truncateToWidth3(theme.fg("dim", " \u2191\u2193 navigate \u2022 enter open \u2022 esc/q close"), width));
13559
+ return lines;
13560
+ }
13561
+ const content = pageLines();
13562
+ const maxScroll = Math.max(0, content.length - DASHBOARD_PAGE_SIZE);
13563
+ scroll = Math.min(scroll, maxScroll);
13564
+ for (const line of content.slice(scroll, scroll + DASHBOARD_PAGE_SIZE)) {
13565
+ let styled = theme.fg("text", line);
13566
+ if (isDashboardTitleLine(line))
13567
+ styled = theme.fg("accent", theme.bold(line));
13568
+ else if (line.startsWith("-"))
13569
+ styled = theme.fg("dim", line);
13570
+ lines.push(truncateToWidth3(" " + styled, width));
13571
+ }
13572
+ if (content.length > DASHBOARD_PAGE_SIZE) {
13573
+ lines.push(truncateToWidth3(theme.fg("dim", " showing " + (scroll + 1) + "-" + Math.min(content.length, scroll + DASHBOARD_PAGE_SIZE) + " of " + content.length), width));
13574
+ }
13575
+ lines.push("", truncateToWidth3(theme.fg("dim", " \u2191\u2193 scroll \u2022 pgup/pgdn page \u2022 home/end jump \u2022 b back \u2022 esc/q close"), width));
13576
+ return lines;
13577
+ },
13578
+ invalidate() {},
13579
+ handleInput(data) {
13580
+ if (keybindings.matches(data, "tui.select.cancel") || data === "q") {
13581
+ done(null);
13582
+ return;
13583
+ }
13584
+ if (view === "menu") {
13585
+ if (keybindings.matches(data, "tui.select.up"))
13586
+ selected = Math.max(0, selected - 1);
13587
+ else if (keybindings.matches(data, "tui.select.down"))
13588
+ selected = Math.min(menuItems.length - 1, selected + 1);
13589
+ else if (keybindings.matches(data, "tui.select.confirm")) {
13590
+ const item = menuItems[selected];
13591
+ if (item.action) {
13592
+ done(item.action);
13593
+ return;
13594
+ }
13595
+ if (item.view)
13596
+ resetPage(item.view);
13597
+ }
13598
+ } else {
13599
+ const maxScroll = Math.max(0, pageLines().length - DASHBOARD_PAGE_SIZE);
13600
+ if (data === "b" || matchesKey3(data, Key3.left))
13601
+ resetPage("menu");
13602
+ else if (matchesKey3(data, Key3.home))
13603
+ scroll = 0;
13604
+ else if (matchesKey3(data, Key3.end))
13605
+ scroll = maxScroll;
13606
+ else if (keybindings.matches(data, "tui.select.pageUp"))
13607
+ scroll = Math.max(0, scroll - DASHBOARD_PAGE_SIZE);
13608
+ else if (keybindings.matches(data, "tui.select.pageDown"))
13609
+ scroll = Math.min(maxScroll, scroll + DASHBOARD_PAGE_SIZE);
13610
+ else if (keybindings.matches(data, "tui.select.up"))
13611
+ scroll = Math.max(0, scroll - 1);
13612
+ else if (keybindings.matches(data, "tui.select.down"))
13613
+ scroll = Math.min(maxScroll, scroll + 1);
13614
+ }
13615
+ tui.requestRender();
13616
+ }
13617
+ };
13618
+ }, {
13619
+ overlay: true,
13620
+ overlayOptions: { width: "80%", anchor: "center", maxHeight: "85%" }
13621
+ });
13622
+ }
13623
+
13624
+ // src/ui/settings-overlay.ts
13625
+ import {
13626
+ getSettingsListTheme as getSettingsListTheme2
13627
+ } from "@earendil-works/pi-coding-agent";
13628
+ import {
13629
+ Container as Container4,
13630
+ SettingsList as SettingsList2,
13631
+ Text as Text4
13632
+ } from "@earendil-works/pi-tui";
13633
+
13634
+ // src/ui/settings-complex.ts
13635
+ import path15 from "path";
13636
+ import {
13637
+ Container as Container3,
13638
+ Input,
13639
+ Key as Key4,
13640
+ matchesKey as matchesKey4,
13641
+ SelectList as SelectList3,
13642
+ SettingsList,
13643
+ Text as Text3
13644
+ } from "@earendil-works/pi-tui";
13645
+ import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
13646
+ var MODEL_SETTINGS = [
13647
+ {
13648
+ id: "summaryModel",
13649
+ label: "Summary model",
13650
+ description: "Model used for synthesis and verification fallback."
13651
+ },
13652
+ {
13653
+ id: "segmentationModel",
13654
+ label: "Segmentation model",
13655
+ description: "Optional model used for transcript exploration."
13656
+ },
13657
+ {
13658
+ id: "verificationModel",
13659
+ label: "Verification model",
13660
+ description: "Optional model used for repair after verification."
13661
+ }
13662
+ ];
13663
+ var PROFILE_NAMES = Object.keys(PROFILES);
13664
+ function numberParser(options) {
13665
+ return (input) => {
13666
+ const trimmed = input.trim();
13667
+ if (!trimmed)
13668
+ return;
13669
+ const value = Number(trimmed);
13670
+ const inRange = value >= options.min && value <= options.max;
13671
+ if (!Number.isFinite(value) || options.integer !== false && !Number.isSafeInteger(value) || !inRange && !(options.zeroOrRange && value === 0)) {
13672
+ const allowed = options.zeroOrRange ? `0 or ${options.min}\u2013${options.max}` : `${options.min}\u2013${options.max}`;
13673
+ throw new Error(`Enter ${options.integer === false ? "a number" : "an integer"} in ${allowed}.`);
13674
+ }
13675
+ return value;
13676
+ };
13677
+ }
13678
+ function scalarFormat(value) {
13679
+ return value === undefined ? "default" : String(value);
13680
+ }
13681
+ var LIMIT_SETTINGS = [
13682
+ {
13683
+ id: "minContextPercent",
13684
+ label: "Minimum context percent",
13685
+ description: "Auto compaction starts at or above this context usage.",
13686
+ placeholder: "0\u2013100; blank uses default",
13687
+ parse: numberParser(CONFIG_NUMERIC_LIMITS.minContextPercent),
13688
+ format: scalarFormat
13689
+ },
13690
+ {
13691
+ id: "autoTriggerTimeoutMs",
13692
+ label: "Auto-trigger timeout",
13693
+ description: "Maximum host auto-compaction time in milliseconds.",
13694
+ placeholder: "1000\u2013300000; blank uses default",
13695
+ parse: numberParser(CONFIG_NUMERIC_LIMITS.autoTriggerTimeoutMs),
13696
+ format: scalarFormat
13697
+ },
13698
+ {
13699
+ id: "maxLlmCalls",
13700
+ label: "Maximum LLM calls",
13701
+ description: "Zero uses the selected mode's call cap.",
13702
+ placeholder: "0\u2013100; blank uses default",
13703
+ parse: numberParser(CONFIG_NUMERIC_LIMITS.maxLlmCalls),
13704
+ format: scalarFormat
13705
+ },
13706
+ {
13707
+ id: "maxLlmInputTokens",
13708
+ label: "Maximum LLM input tokens",
13709
+ description: "Zero uses the selected mode's token cap.",
13710
+ placeholder: "0\u20131000000; blank uses default",
13711
+ parse: numberParser(CONFIG_NUMERIC_LIMITS.maxLlmInputTokens),
13712
+ format: scalarFormat
13713
+ },
13714
+ {
13715
+ id: "codexMaxCallMs",
13716
+ label: "Codex call watchdog",
13717
+ description: "Zero derives the per-call watchdog automatically.",
13718
+ placeholder: "0 or 5000\u2013300000; blank uses default",
13719
+ parse: numberParser(CONFIG_NUMERIC_LIMITS.codexMaxCallMs),
13720
+ format: scalarFormat
13721
+ },
13722
+ {
13723
+ id: "maxLatencyMs",
13724
+ label: "Pipeline latency limit",
13725
+ description: "Zero disables the overall pipeline deadline.",
13726
+ placeholder: "0 or 5000\u2013600000; blank uses default",
13727
+ parse: numberParser(CONFIG_NUMERIC_LIMITS.maxLatencyMs),
13728
+ format: scalarFormat
13729
+ }
13730
+ ];
13731
+ var PATH_SETTINGS = [
13732
+ {
13733
+ id: "backupDir",
13734
+ label: "Backup directory",
13735
+ description: "Directory for recovery Markdown files.",
13736
+ placeholder: "Absolute path; blank uses default",
13737
+ parse(input) {
13738
+ const value = input.trim();
13739
+ if (!value)
13740
+ return;
13741
+ if (value.includes("\x00") || /[\r\n]/.test(value)) {
13742
+ throw new Error("Backup directory must be a single valid path.");
13743
+ }
13744
+ if (!path15.isAbsolute(value)) {
13745
+ throw new Error("Backup directory must be an absolute path.");
13746
+ }
13747
+ return value;
13748
+ },
13749
+ format: scalarFormat
13750
+ },
13751
+ {
13752
+ id: "pinPaths",
13753
+ label: "Pinned paths",
13754
+ description: "Comma-separated file paths that summaries must preserve.",
13755
+ placeholder: "src/api.ts, docs/design.md; blank clears",
13756
+ parse(input) {
13757
+ const trimmed = input.trim();
13758
+ if (!trimmed)
13759
+ return;
13760
+ const paths = trimmed.split(/[,\n]/).map((value) => value.trim()).filter(Boolean);
13761
+ if (paths.some((value) => value.includes("\x00"))) {
13762
+ throw new Error("Pinned paths cannot contain NUL characters.");
13763
+ }
13764
+ return [...new Set(paths)];
13765
+ },
13766
+ format(value) {
13767
+ return value === undefined ? "default" : Array.isArray(value) ? value.join(", ") || "none" : String(value);
13768
+ }
13769
+ }
13770
+ ];
13771
+ var PROFILE_FIELDS = [
13772
+ ["summaryBudgetTokens", "Summary budget"],
13773
+ ["keepRecentTokens", "Recent raw tail"],
13774
+ ["minChunkTokens", "Minimum chunk"],
13775
+ ["maxChunkTokens", "Maximum chunk"],
13776
+ ["singlePassMaxTokens", "Single-pass limit"],
13777
+ ["batchMaxTokens", "Batch limit"]
13778
+ ];
13779
+ function profileSettings(profile) {
13780
+ return PROFILE_FIELDS.map(([key, label]) => {
13781
+ const [min, max] = PROFILE_NUMERIC_BOUNDS[key];
13782
+ return {
13783
+ id: `profiles.${profile}.${key}`,
13784
+ label,
13785
+ description: `${profile} profile token budget; related chunk bounds must remain consistent.`,
13786
+ placeholder: `${min}\u2013${max}; blank uses built-in`,
13787
+ parse: numberParser({ min, max }),
13788
+ format: scalarFormat
13789
+ };
13790
+ });
13791
+ }
13792
+ function effectiveValue(config, id) {
13793
+ const parts = id.split(".");
13794
+ if (parts[0] !== "profiles") {
13795
+ return config[id];
13796
+ }
13797
+ const [, profile, key] = parts;
13798
+ return config.profiles[profile][key];
13799
+ }
13800
+
13801
+ class InputSettingEditor extends Container3 {
13802
+ requestRender;
13803
+ save;
13804
+ done;
13805
+ input = new Input;
13806
+ status = new Text3("", 0, 0);
13807
+ saving = false;
13808
+ pending = Promise.resolve();
13809
+ get focused() {
13810
+ return this.input.focused;
13811
+ }
13812
+ set focused(value) {
13813
+ this.input.focused = value;
13814
+ }
13815
+ constructor(setting, initial, requestRender, save, done) {
13816
+ super();
13817
+ this.requestRender = requestRender;
13818
+ this.save = save;
13819
+ this.done = done;
13820
+ this.addChild(new Text3(setting.label, 0, 0));
13821
+ this.addChild(new Text3(setting.description, 0, 0));
13822
+ this.addChild(new Text3(`Hint: ${setting.placeholder}`, 0, 0));
13823
+ this.addChild(new Text3("", 0, 0));
13824
+ this.input.setValue(initial);
13825
+ this.input.handleInput("\x1B[F");
13826
+ this.input.onSubmit = (value) => {
13827
+ if (this.saving)
13828
+ return;
13829
+ let parsed;
13830
+ try {
13831
+ parsed = setting.parse(value);
13832
+ } catch (error2) {
13833
+ this.status.setText(error2 instanceof Error ? error2.message : String(error2));
13834
+ this.requestRender();
13835
+ return;
13836
+ }
13837
+ this.saving = true;
13838
+ this.status.setText("Saving\u2026");
13839
+ this.requestRender();
13840
+ this.pending = this.save(parsed).then(() => this.done()).catch((error2) => {
13841
+ this.saving = false;
13842
+ this.status.setText(error2 instanceof Error ? error2.message : String(error2));
13843
+ this.requestRender();
13844
+ });
13845
+ };
13846
+ this.input.onEscape = () => {
13847
+ if (!this.saving)
13848
+ this.done();
13849
+ };
13850
+ this.addChild(this.input);
13851
+ this.addChild(new Text3("", 0, 0));
13852
+ this.addChild(this.status);
13853
+ this.addChild(new Text3("Enter save \xB7 Esc cancel", 0, 0));
13854
+ }
13855
+ handleInput(data) {
13856
+ this.input.handleInput(data);
13857
+ this.requestRender();
13858
+ }
13859
+ settled() {
13860
+ return this.pending;
13861
+ }
13862
+ }
13863
+
13864
+ class ModelSettingEditor extends Container3 {
13865
+ requestRender;
13866
+ search = new Input;
13867
+ list;
13868
+ status = new Text3("", 0, 0);
13869
+ saving = false;
13870
+ pending = Promise.resolve();
13871
+ get focused() {
13872
+ return this.search.focused;
13873
+ }
13874
+ set focused(value) {
13875
+ this.search.focused = value;
13876
+ }
13877
+ constructor(models, selectedValue, requestRender, save, done) {
13878
+ super();
13879
+ this.requestRender = requestRender;
13880
+ this.addChild(new Text3("Search provider/model", 0, 0));
13881
+ this.addChild(this.search);
13882
+ this.addChild(new Text3("", 0, 0));
13883
+ this.list = new SelectList3(models, 10, {
13884
+ selectedPrefix: (text) => text,
13885
+ selectedText: (text) => text,
13886
+ description: (text) => text,
13887
+ scrollInfo: (text) => text,
13888
+ noMatch: (text) => text
13889
+ });
13890
+ const selectedIndex = models.findIndex((item) => item.settingValue === selectedValue);
13891
+ this.list.setSelectedIndex(Math.max(0, selectedIndex));
13892
+ this.list.onSelect = (item) => {
13893
+ if (this.saving)
13894
+ return;
13895
+ this.saving = true;
13896
+ this.status.setText("Saving\u2026");
13897
+ this.requestRender();
13898
+ const selected = models.find((candidate) => candidate.value === item.value);
13899
+ if (!selected)
13900
+ return;
13901
+ this.pending = save(selected.settingValue).then(done).catch((error2) => {
13902
+ this.saving = false;
13903
+ this.status.setText(error2 instanceof Error ? error2.message : String(error2));
13904
+ this.requestRender();
13905
+ });
13906
+ };
13907
+ this.list.onCancel = () => {
13908
+ if (!this.saving)
13909
+ done();
13910
+ };
13911
+ this.addChild(this.list);
13912
+ this.addChild(this.status);
13913
+ this.addChild(new Text3("Type to filter \xB7 Enter select \xB7 Esc cancel", 0, 0));
13914
+ }
13915
+ handleInput(data) {
13916
+ if (matchesKey4(data, Key4.up) || matchesKey4(data, Key4.down) || matchesKey4(data, Key4.enter) || matchesKey4(data, Key4.escape)) {
13917
+ this.list.handleInput(data);
13918
+ } else {
13919
+ this.search.handleInput(data);
13920
+ this.list.setFilter(this.search.getValue());
13921
+ }
13922
+ this.requestRender();
13923
+ }
13924
+ settled() {
13925
+ return this.pending;
13926
+ }
13927
+ }
13928
+ function inputSettingsList(settings, requestRender, done, writeConfig) {
13929
+ const config = loadConfig();
13930
+ const items = settings.map((setting) => {
13931
+ const override = readGlobalConfigValue(setting.id);
13932
+ const item = {
13933
+ id: setting.id,
13934
+ label: setting.label,
13935
+ description: `${setting.description} Effective global value: ${setting.format(effectiveValue(config, setting.id))}.`,
13936
+ currentValue: setting.format(override)
13937
+ };
13938
+ item.submenu = (_current, close) => {
13939
+ const persisted = readGlobalConfigValue(setting.id);
13940
+ return new InputSettingEditor(setting, persisted === undefined ? "" : Array.isArray(persisted) ? persisted.join(", ") : String(persisted), requestRender, async (value) => {
13941
+ const effective = await writeConfig(setting.id, value);
13942
+ item.currentValue = setting.format(value);
13943
+ item.description = `${setting.description} Effective global value: ${setting.format(effectiveValue(effective, setting.id))}.`;
13944
+ }, close);
13945
+ };
13946
+ return item;
13947
+ });
13948
+ return new SettingsList(items, 9, getSettingsListTheme(), () => {}, done);
13949
+ }
13950
+ function modelSettingsItems(ctx, requestRender, writeConfig = writeGlobalConfigValue) {
13951
+ const config = loadConfig();
13952
+ return MODEL_SETTINGS.map((setting) => {
13953
+ const override = readGlobalConfigValue(setting.id);
13954
+ const current = typeof override === "string" ? override : "default";
13955
+ const item = {
13956
+ id: setting.id,
13957
+ label: setting.label,
13958
+ description: `${setting.description} Effective global value: ${String(config[setting.id])}.`,
13959
+ currentValue: current
13960
+ };
13961
+ item.submenu = (_value, close) => {
13962
+ const persisted = readGlobalConfigValue(setting.id);
13963
+ const selectedValue = typeof persisted === "string" ? persisted : "default";
13964
+ const effective = loadConfig();
13965
+ item.currentValue = selectedValue;
13966
+ item.description = `${setting.description} Effective global value: ${String(effective[setting.id])}.`;
13967
+ const available = ctx.modelRegistry.getAvailable().map((model) => ({
13968
+ value: `${model.provider}/${model.id} \u2014 ${model.name}`,
13969
+ settingValue: `${model.provider}/${model.id}`,
13970
+ label: `${model.provider}/${model.id}`,
13971
+ description: model.name
13972
+ })).sort((left, right) => left.value.localeCompare(right.value));
13973
+ const choices = [
13974
+ {
13975
+ value: "default \u2014 use active session model",
13976
+ settingValue: "default",
13977
+ label: "default",
13978
+ description: "Remove the global model override"
13979
+ },
13980
+ ...available
13981
+ ];
13982
+ if (selectedValue !== "default" && !choices.some((candidate) => candidate.settingValue === selectedValue)) {
13983
+ choices.splice(1, 0, {
13984
+ value: selectedValue,
13985
+ settingValue: selectedValue,
13986
+ label: selectedValue,
13987
+ description: "Configured model is currently unavailable"
13988
+ });
13989
+ }
13990
+ return new ModelSettingEditor(choices, selectedValue, requestRender, async (selected) => {
13991
+ const effective2 = await writeConfig(setting.id, selected === "default" ? undefined : selected);
13992
+ item.currentValue = selected;
13993
+ item.description = `${setting.description} Effective global value: ${String(effective2[setting.id])}.`;
13994
+ }, close);
13995
+ };
13996
+ return item;
13997
+ });
13998
+ }
13999
+ function complexSettingsCategories(ctx, requestRender, writeConfig = writeGlobalConfigValue) {
14000
+ return [
14001
+ {
14002
+ id: "models",
14003
+ label: "Global models",
14004
+ description: "Stage-specific model routing",
14005
+ currentValue: "3 settings",
14006
+ submenu: (_value, done) => new SettingsList(modelSettingsItems(ctx, requestRender, writeConfig), 7, getSettingsListTheme(), () => {}, done)
14007
+ },
14008
+ {
14009
+ id: "limits",
14010
+ label: "Global limits & performance",
14011
+ description: "Context threshold, call budgets, and timeouts",
14012
+ currentValue: "6 settings",
14013
+ submenu: (_value, done) => inputSettingsList(LIMIT_SETTINGS, requestRender, done, writeConfig)
14014
+ },
14015
+ {
14016
+ id: "paths",
14017
+ label: "Global paths",
14018
+ description: "Backup directory and pinned summary paths",
14019
+ currentValue: "2 settings",
14020
+ submenu: (_value, done) => inputSettingsList(PATH_SETTINGS, requestRender, done, writeConfig)
14021
+ },
14022
+ {
14023
+ id: "profiles",
14024
+ label: "Global profile budgets",
14025
+ description: "Advanced token-budget tuning for each profile",
14026
+ currentValue: "3 profiles",
14027
+ submenu: (_value, done) => {
14028
+ const profiles = PROFILE_NAMES.map((profile) => ({
14029
+ id: profile,
14030
+ label: profile,
14031
+ description: `Six token-budget settings for the ${profile} profile.`,
14032
+ currentValue: "6 settings",
14033
+ submenu: (_current, close) => inputSettingsList(profileSettings(profile), requestRender, close, writeConfig)
14034
+ }));
14035
+ return new SettingsList(profiles, 7, getSettingsListTheme(), () => {}, done);
14036
+ }
14037
+ }
14038
+ ];
14039
+ }
14040
+
14041
+ // src/ui/settings-overlay.ts
14042
+ function enabled(value) {
14043
+ return value ? "enabled" : "disabled";
14044
+ }
14045
+
14046
+ class GlobalSettingsCoordinator {
14047
+ writer;
14048
+ queue = Promise.resolve();
14049
+ confirmed = new Map;
14050
+ confirmedConfig;
14051
+ pending = new Map;
14052
+ listeners = new Map;
14053
+ constructor(writer = updateGlobalChoiceSetting) {
14054
+ this.writer = writer;
14055
+ }
14056
+ display(id) {
14057
+ return this.pending.get(id)?.display ?? choiceDisplay(readGlobalConfigValue(id));
14058
+ }
14059
+ subscribe(ids, listener) {
14060
+ const registrations = [];
14061
+ for (const id of ids) {
14062
+ const callbacks = this.listeners.get(id) ?? new Set;
14063
+ const callback = (state) => listener(id, state);
14064
+ callbacks.add(callback);
14065
+ this.listeners.set(id, callbacks);
14066
+ registrations.push([id, callback]);
14067
+ }
14068
+ return () => {
14069
+ for (const [id, callback] of registrations) {
14070
+ const callbacks = this.listeners.get(id);
14071
+ callbacks?.delete(callback);
14072
+ if (callbacks?.size === 0)
14073
+ this.listeners.delete(id);
14074
+ }
14075
+ };
14076
+ }
14077
+ submit(group, id, display, onError, onApplied = () => {}) {
14078
+ if (this.pending.size === 0)
14079
+ this.confirmedConfig = loadConfig();
14080
+ if (!this.pending.has(id)) {
14081
+ this.confirmed.set(id, choiceDisplay(readGlobalConfigValue(id)));
14082
+ }
14083
+ const revision = (this.pending.get(id)?.revision ?? 0) + 1;
14084
+ this.pending.set(id, { revision, display });
14085
+ this.queue = this.queue.then(async () => {
14086
+ try {
14087
+ const config = await this.writer(group, id, display);
14088
+ await onApplied(id, config);
14089
+ this.confirmed.set(id, display);
14090
+ this.confirmedConfig = config;
14091
+ if (this.pending.get(id)?.revision === revision) {
14092
+ this.pending.delete(id);
14093
+ this.emit(id, { display, config });
14094
+ }
14095
+ } catch (error2) {
14096
+ onError(error2 instanceof Error ? error2.message : String(error2));
14097
+ if (this.pending.get(id)?.revision === revision) {
14098
+ this.pending.delete(id);
14099
+ this.emit(id, {
14100
+ display: this.confirmed.get(id) ?? "default",
14101
+ config: this.confirmedConfig
14102
+ });
14103
+ }
14104
+ }
14105
+ });
14106
+ }
14107
+ settled() {
14108
+ return this.queue;
14109
+ }
14110
+ emit(id, state) {
14111
+ for (const listener of this.listeners.get(id) ?? [])
14112
+ listener(state);
14113
+ }
14114
+ }
14115
+ var BOOLEAN_VALUES = [true, false];
14116
+ var THINKING_VALUES = [
14117
+ null,
14118
+ "minimal",
14119
+ "low",
14120
+ "medium",
14121
+ "high",
14122
+ "xhigh",
14123
+ "max"
14124
+ ];
14125
+ var GLOBAL_CHOICE_SETTINGS = {
14126
+ behavior: [
14127
+ {
14128
+ id: "mode",
14129
+ label: "Compaction mode",
14130
+ description: "Default compaction strategy.",
14131
+ values: ["auto", "fast", "balanced", "thorough"]
14132
+ },
14133
+ {
14134
+ id: "profile",
14135
+ label: "Compression profile",
14136
+ description: "Legacy detail-budget profile.",
14137
+ values: ["light", "balanced", "aggressive"]
14138
+ },
14139
+ {
14140
+ id: "agentToolAccess",
14141
+ label: "Agent tool access",
14142
+ description: "Global default for agent-visible smart_compact access.",
14143
+ values: ["inherit", "enabled", "disabled"]
14144
+ },
14145
+ {
14146
+ id: "autoTrigger",
14147
+ label: "Automatic compaction",
14148
+ description: "Global default for pressure-triggered compaction.",
14149
+ values: BOOLEAN_VALUES
14150
+ },
14151
+ {
14152
+ id: "showStatus",
14153
+ label: "Footer status",
14154
+ description: "Global default for the Smart Compact footer indicator.",
14155
+ values: BOOLEAN_VALUES
14156
+ },
14157
+ {
14158
+ id: "autoTriggerStrategy",
14159
+ label: "Trigger strategy",
14160
+ description: "Host-native hook or settled-turn triggering.",
14161
+ values: ["native-hook", "settled"]
14162
+ }
14163
+ ],
14164
+ reasoning: [
14165
+ {
14166
+ id: "summaryThinkingLevel",
14167
+ label: "Summary thinking",
14168
+ description: "Thinking level for synthesis and repair.",
14169
+ values: THINKING_VALUES
14170
+ },
14171
+ {
14172
+ id: "segmentationThinkingLevel",
14173
+ label: "Segmentation thinking",
14174
+ description: "Thinking level for transcript segmentation.",
14175
+ values: THINKING_VALUES
14176
+ }
14177
+ ],
14178
+ safety: [
14179
+ {
14180
+ id: "backupEnabled",
14181
+ label: "Backups",
14182
+ description: "Write a recovery backup before applying a compacted summary.",
14183
+ values: BOOLEAN_VALUES
14184
+ },
14185
+ {
14186
+ id: "requireApproval",
14187
+ label: "Require approval",
14188
+ description: "Ask before applying manual compaction output.",
14189
+ values: BOOLEAN_VALUES
14190
+ },
13079
14191
  {
13080
- view: "providers",
13081
- label: "Provider routes",
13082
- desc: insights.providers.length + " stage/provider/model comparison row(s)"
14192
+ id: "scrubSecrets",
14193
+ label: "Scrub secrets",
14194
+ description: "Redact likely credentials before model calls and memory writes.",
14195
+ values: BOOLEAN_VALUES
13083
14196
  },
13084
14197
  {
13085
- view: "canary",
13086
- label: "Canary vs stable",
13087
- desc: insights.canary.decision.toUpperCase() + " \xB7 " + insights.canary.dataConfidence + "% canary confidence"
14198
+ id: "scrubPii",
14199
+ label: "Scrub PII",
14200
+ description: "Redact email, phone, and payment-card shaped data.",
14201
+ values: BOOLEAN_VALUES
13088
14202
  },
13089
14203
  {
13090
- view: "latest",
13091
- label: "Latest run details",
13092
- desc: latest ? formatMetricRunCompact(latest) : "No run recorded yet"
14204
+ id: "contextGraphEnabled",
14205
+ label: "Project memory",
14206
+ description: "Index project context and expose recall/save tools.",
14207
+ values: BOOLEAN_VALUES
14208
+ }
14209
+ ],
14210
+ advanced: [
14211
+ {
14212
+ id: "focusWeighting",
14213
+ label: "Focus weighting",
14214
+ description: "Steer synthesis toward the current task focus.",
14215
+ values: BOOLEAN_VALUES
13093
14216
  },
13094
14217
  {
13095
- view: "session",
13096
- label: "Current session",
13097
- desc: (opts.currentSessionId ?? "unknown") + " \u2014 " + currentRuns.length + " run(s)"
14218
+ id: "zeroCallEnabled",
14219
+ label: "Zero-call fast path",
14220
+ description: "Allow deterministic compaction without an LLM call.",
14221
+ values: BOOLEAN_VALUES
13098
14222
  },
13099
14223
  {
13100
- view: "recent",
13101
- label: "Recent runs",
13102
- desc: "Last " + Math.min(entries.length, 30) + " run(s)"
14224
+ id: "telemetryChannel",
14225
+ label: "Telemetry channel",
14226
+ description: "Tag local metrics as stable or canary.",
14227
+ values: ["stable", "canary"]
13103
14228
  },
13104
14229
  {
13105
- action: "html",
13106
- label: "Write HTML dashboard",
13107
- desc: "Generate ~/.pi/agent/.cache/smart-compact-report.html"
14230
+ id: "adaptiveDamageFeedback",
14231
+ label: "Adaptive damage feedback",
14232
+ description: "Increase preservation budgets using prior damage signals.",
14233
+ values: BOOLEAN_VALUES
14234
+ },
14235
+ {
14236
+ id: "onlineDamageMonitor",
14237
+ label: "Online damage monitor",
14238
+ description: "Monitor confirmed compactions for preservation damage.",
14239
+ values: BOOLEAN_VALUES
13108
14240
  }
13109
- ];
13110
- return ctx.ui.custom((tui, theme, keybindings, done) => {
13111
- let view = "menu";
13112
- let selected = 0;
13113
- let scroll = 0;
13114
- const pageLines = () => {
13115
- switch (view) {
13116
- case "overview":
13117
- return opts.report.split(`
13118
- `);
13119
- case "quality":
13120
- return formatDashboardQuality(insights);
13121
- case "providers":
13122
- return formatDashboardProviders(insights);
13123
- case "canary":
13124
- return formatDashboardCanary(insights);
13125
- case "latest":
13126
- return formatRunDetails(latest, "Latest run details");
13127
- case "session":
13128
- return formatCurrentSession(entries, opts.currentSessionId);
13129
- case "recent":
13130
- return formatRecentRuns(entries);
13131
- case "menu":
13132
- return [];
13133
- default:
13134
- return [];
13135
- }
13136
- };
13137
- const resetPage = (nextView) => {
13138
- view = nextView;
13139
- scroll = 0;
13140
- };
13141
- const qualityStatus = () => {
13142
- if (!hasQualityData)
13143
- return theme.fg("dim", " \u2022 Quality unavailable");
13144
- return theme.fg(insights.quality.targetMet ? "success" : "warning", " \u2022 Quality " + insights.quality.healthScore + "/100");
13145
- };
13146
- const renderHeader = (width) => [
13147
- truncateToWidth3(theme.fg("accent", theme.bold(" \uD83D\uDCCA Smart Compact Dashboard")) + theme.fg("dim", " " + entries.length + " recorded run(s)"), width),
13148
- truncateToWidth3(theme.fg("dim", " session: " + (opts.currentSessionId ?? "unknown")) + theme.fg("dim", latest && Number.isFinite(latest.verificationScore) ? " \u2022 latest score " + metricScore(latest) : "") + theme.fg(insights.confidence.targetMet ? "success" : "warning", " \u2022 Data Confidence " + insights.confidence.score + "/100") + qualityStatus(), width),
13149
- truncateToWidth3(theme.fg("borderMuted", "\u2500".repeat(Math.max(0, width))), width)
13150
- ];
13151
- return {
13152
- render(width) {
13153
- const lines = renderHeader(width);
13154
- if (view === "menu") {
13155
- lines.push(truncateToWidth3(theme.fg("text", " Choose what to inspect:"), width), "");
13156
- for (let index = 0;index < menuItems.length; index++) {
13157
- const item = menuItems[index];
13158
- const active = index === selected;
13159
- const label = active ? theme.fg("accent", theme.bold(item.label)) : theme.fg("text", item.label);
13160
- lines.push(truncateToWidth3((active ? " \u203A " : " ") + label, width));
13161
- lines.push(truncateToWidth3(" " + theme.fg(active ? "muted" : "dim", item.desc), width));
13162
- }
13163
- lines.push("", truncateToWidth3(theme.fg("dim", " \u2191\u2193 navigate \u2022 enter open \u2022 esc/q close"), width));
13164
- return lines;
13165
- }
13166
- const content = pageLines();
13167
- const maxScroll = Math.max(0, content.length - DASHBOARD_PAGE_SIZE);
13168
- scroll = Math.min(scroll, maxScroll);
13169
- for (const line of content.slice(scroll, scroll + DASHBOARD_PAGE_SIZE)) {
13170
- let styled = theme.fg("text", line);
13171
- if (isDashboardTitleLine(line))
13172
- styled = theme.fg("accent", theme.bold(line));
13173
- else if (line.startsWith("-"))
13174
- styled = theme.fg("dim", line);
13175
- lines.push(truncateToWidth3(" " + styled, width));
13176
- }
13177
- if (content.length > DASHBOARD_PAGE_SIZE) {
13178
- lines.push(truncateToWidth3(theme.fg("dim", " showing " + (scroll + 1) + "-" + Math.min(content.length, scroll + DASHBOARD_PAGE_SIZE) + " of " + content.length), width));
13179
- }
13180
- lines.push("", truncateToWidth3(theme.fg("dim", " \u2191\u2193 scroll \u2022 pgup/pgdn page \u2022 home/end jump \u2022 b back \u2022 esc/q close"), width));
13181
- return lines;
13182
- },
13183
- invalidate() {},
13184
- handleInput(data) {
13185
- if (keybindings.matches(data, "tui.select.cancel") || data === "q") {
13186
- done(null);
13187
- return;
13188
- }
13189
- if (view === "menu") {
13190
- if (keybindings.matches(data, "tui.select.up"))
13191
- selected = Math.max(0, selected - 1);
13192
- else if (keybindings.matches(data, "tui.select.down"))
13193
- selected = Math.min(menuItems.length - 1, selected + 1);
13194
- else if (keybindings.matches(data, "tui.select.confirm")) {
13195
- const item = menuItems[selected];
13196
- if (item.action) {
13197
- done(item.action);
13198
- return;
13199
- }
13200
- if (item.view)
13201
- resetPage(item.view);
13202
- }
13203
- } else {
13204
- const maxScroll = Math.max(0, pageLines().length - DASHBOARD_PAGE_SIZE);
13205
- if (data === "b" || matchesKey3(data, Key3.left))
13206
- resetPage("menu");
13207
- else if (matchesKey3(data, Key3.home))
13208
- scroll = 0;
13209
- else if (matchesKey3(data, Key3.end))
13210
- scroll = maxScroll;
13211
- else if (keybindings.matches(data, "tui.select.pageUp"))
13212
- scroll = Math.max(0, scroll - DASHBOARD_PAGE_SIZE);
13213
- else if (keybindings.matches(data, "tui.select.pageDown"))
13214
- scroll = Math.min(maxScroll, scroll + DASHBOARD_PAGE_SIZE);
13215
- else if (keybindings.matches(data, "tui.select.up"))
13216
- scroll = Math.max(0, scroll - 1);
13217
- else if (keybindings.matches(data, "tui.select.down"))
13218
- scroll = Math.min(maxScroll, scroll + 1);
13219
- }
13220
- tui.requestRender();
13221
- }
13222
- };
13223
- }, {
13224
- overlay: true,
13225
- overlayOptions: { width: "80%", anchor: "center", maxHeight: "85%" }
13226
- });
14241
+ ]
14242
+ };
14243
+ var GLOBAL_CHOICE_CATEGORIES = [
14244
+ {
14245
+ group: "behavior",
14246
+ label: "Global behavior",
14247
+ description: "Mode, profile, automation, and agent defaults"
14248
+ },
14249
+ {
14250
+ group: "reasoning",
14251
+ label: "Global reasoning",
14252
+ description: "Thinking-level defaults"
14253
+ },
14254
+ {
14255
+ group: "safety",
14256
+ label: "Global safety & storage",
14257
+ description: "Backups, approval, scrubbing, and project memory"
14258
+ },
14259
+ {
14260
+ group: "advanced",
14261
+ label: "Global advanced",
14262
+ description: "Focus, fast path, telemetry, and damage monitoring"
14263
+ }
14264
+ ];
14265
+ function choiceDisplay(value) {
14266
+ if (value === undefined)
14267
+ return "default";
14268
+ if (value === null)
14269
+ return "provider default";
14270
+ if (typeof value === "boolean")
14271
+ return enabled(value);
14272
+ return String(value);
14273
+ }
14274
+ function choiceValue(setting, display) {
14275
+ if (display === "default")
14276
+ return;
14277
+ const value = setting.values.find((candidate) => choiceDisplay(candidate) === display);
14278
+ if (value === undefined) {
14279
+ throw new Error(`Invalid value for ${setting.id}: ${display}`);
14280
+ }
14281
+ return value;
14282
+ }
14283
+ function effectiveChoiceDescription(setting, config) {
14284
+ const effective = config[setting.id];
14285
+ return `${setting.description} Effective global value: ${choiceDisplay(effective)}.`;
14286
+ }
14287
+ function globalChoiceSettingsItems(group, config, coordinator) {
14288
+ return GLOBAL_CHOICE_SETTINGS[group].map((setting) => ({
14289
+ id: setting.id,
14290
+ label: setting.label,
14291
+ description: effectiveChoiceDescription(setting, config),
14292
+ currentValue: coordinator?.display(setting.id) ?? choiceDisplay(readGlobalConfigValue(setting.id)),
14293
+ values: ["default", ...setting.values.map(choiceDisplay)]
14294
+ }));
13227
14295
  }
13228
-
13229
- // src/ui/settings-overlay.ts
13230
- import {
13231
- getSettingsListTheme
13232
- } from "@earendil-works/pi-coding-agent";
13233
- import {
13234
- Container as Container3,
13235
- SettingsList,
13236
- Text as Text3
13237
- } from "@earendil-works/pi-tui";
13238
- var INHERIT = "host default";
13239
- var ENABLED = "enabled";
13240
- var DISABLED = "disabled";
13241
- function accessValue(policy) {
13242
- return policy.agentToolAccess === "inherit" ? INHERIT : policy.agentToolAccess;
13243
- }
13244
- function parseAccessValue(value) {
13245
- return value === INHERIT ? "inherit" : value;
13246
- }
13247
- function previousValue(id, policy) {
13248
- if (id === "agentToolAccess")
13249
- return accessValue(policy);
13250
- if (id === "showStatus")
13251
- return policy.showStatus ? ENABLED : DISABLED;
13252
- return policy.autoTrigger ? ENABLED : DISABLED;
13253
- }
13254
- function settingsItems(policy) {
14296
+ async function updateGlobalChoiceSetting(group, id, display) {
14297
+ const setting = GLOBAL_CHOICE_SETTINGS[group].find((candidate) => candidate.id === id);
14298
+ if (!setting)
14299
+ throw new Error(`Unknown global choice setting: ${id}`);
14300
+ return writeGlobalConfigValue(setting.id, choiceValue(setting, display));
14301
+ }
14302
+ function effectiveDescription(field, policy) {
14303
+ return field === "agentToolAccess" ? `Effective tool state: ${enabled(policy.agentToolEnabled)}` : `Effective value: ${enabled(policy[field])}`;
14304
+ }
14305
+ function sessionSettingsItems(policy) {
14306
+ const current = policy.snapshot();
14307
+ const overrides = policy.branchOverrides();
13255
14308
  return [
13256
14309
  {
13257
14310
  id: "agentToolAccess",
13258
14311
  label: "Agent access",
13259
- description: "Inherit Pi's tool selection, or explicitly enable/disable smart_compact",
13260
- currentValue: accessValue(policy),
13261
- values: [INHERIT, ENABLED, DISABLED]
14312
+ description: effectiveDescription("agentToolAccess", current),
14313
+ currentValue: overrides.agentToolAccess ?? "global",
14314
+ values: ["global", "inherit", "enabled", "disabled"]
13262
14315
  },
13263
14316
  {
13264
14317
  id: "autoTrigger",
13265
14318
  label: "Automatic compaction",
13266
- description: "Run Smart Compact from Pi's automatic compaction lifecycle",
13267
- currentValue: policy.autoTrigger ? ENABLED : DISABLED,
13268
- values: [ENABLED, DISABLED]
14319
+ description: effectiveDescription("autoTrigger", current),
14320
+ currentValue: overrides.autoTrigger === undefined ? "global" : enabled(overrides.autoTrigger),
14321
+ values: ["global", "enabled", "disabled"]
13269
14322
  },
13270
14323
  {
13271
14324
  id: "showStatus",
13272
14325
  label: "Footer status",
13273
- description: "Show the smart-compact policy status line in Pi's footer",
13274
- currentValue: policy.showStatus ? ENABLED : DISABLED,
13275
- values: [ENABLED, DISABLED]
14326
+ description: effectiveDescription("showStatus", current),
14327
+ currentValue: overrides.showStatus === undefined ? "global" : enabled(overrides.showStatus),
14328
+ values: ["global", "enabled", "disabled"]
13276
14329
  }
13277
14330
  ];
13278
14331
  }
13279
- async function showSmartCompactSettings(ctx, policy) {
13280
- await ctx.ui.custom((tui, theme, _keybindings, done) => {
13281
- const container = new Container3;
13282
- container.addChild(new Text3(theme.fg("accent", theme.bold("Smart Compact Settings")), 0, 0));
13283
- container.addChild(new Text3(theme.fg("dim", "Changes apply now and follow this session branch."), 0, 0));
13284
- container.addChild(new Text3(theme.fg("dim", "Manual /smart-compact stays available in every mode."), 0, 0));
13285
- container.addChild(new Text3("", 0, 0));
13286
- const list = new SettingsList(settingsItems(policy.snapshot()), 6, getSettingsListTheme(), (id, value) => {
13287
- const previous = policy.snapshot();
13288
- const result = id === "agentToolAccess" ? policy.update({ agentToolAccess: parseAccessValue(value) }, ctx) : id === "autoTrigger" ? policy.update({ autoTrigger: value === ENABLED }, ctx) : policy.update({ showStatus: value === ENABLED }, ctx);
14332
+ function policyField(id) {
14333
+ switch (id) {
14334
+ case "agentToolAccess":
14335
+ case "autoTrigger":
14336
+ case "showStatus":
14337
+ return id;
14338
+ default:
14339
+ throw new Error(`Unknown session setting: ${id}`);
14340
+ }
14341
+ }
14342
+ function updateSessionSetting(policy, ctx, id, value) {
14343
+ const field = policyField(id);
14344
+ if (value === "global")
14345
+ return policy.reset(field, ctx);
14346
+ switch (field) {
14347
+ case "agentToolAccess":
14348
+ if (value !== "inherit" && value !== "enabled" && value !== "disabled") {
14349
+ throw new Error(`Invalid agent access value: ${value}`);
14350
+ }
14351
+ return policy.update({ agentToolAccess: value }, ctx);
14352
+ case "autoTrigger":
14353
+ return policy.update({ autoTrigger: value === "enabled" }, ctx);
14354
+ case "showStatus":
14355
+ return policy.update({ showStatus: value === "enabled" }, ctx);
14356
+ }
14357
+ }
14358
+ function displayValue(field, policy) {
14359
+ const override = policy.branchOverrides()[field];
14360
+ if (override === undefined)
14361
+ return "global";
14362
+ if (typeof override === "boolean")
14363
+ return enabled(override);
14364
+ return override;
14365
+ }
14366
+ function sessionSettingsList(policy, ctx, done) {
14367
+ const items = sessionSettingsItems(policy);
14368
+ let list;
14369
+ list = new SettingsList2(items, 7, getSettingsListTheme2(), (id, value) => {
14370
+ try {
14371
+ const result = updateSessionSetting(policy, ctx, id, value);
14372
+ const field = policyField(id);
14373
+ const item = items.find((candidate) => candidate.id === id);
14374
+ if (item)
14375
+ item.description = effectiveDescription(field, result.policy);
13289
14376
  if (!result.ok) {
13290
- list.updateValue(id, previousValue(id, previous));
13291
14377
  ctx.ui.notify(result.error, "error");
14378
+ list.updateValue(id, displayValue(field, policy));
13292
14379
  }
13293
- }, () => done(undefined));
13294
- container.addChild(list);
13295
- container.addChild(new Text3("", 0, 0));
13296
- container.addChild(new Text3(theme.fg("dim", "\u2191\u2193 navigate \xB7 enter change \xB7 esc close"), 0, 0));
13297
- return {
13298
- render: (width) => container.render(width),
13299
- invalidate: () => container.invalidate(),
13300
- handleInput(data) {
13301
- list.handleInput?.(data);
13302
- tui.requestRender();
14380
+ } catch (error2) {
14381
+ ctx.ui.notify(error2 instanceof Error ? error2.message : String(error2), "error");
14382
+ list.updateValue(id, displayValue(policyField(id), policy));
14383
+ }
14384
+ }, done);
14385
+ return list;
14386
+ }
14387
+ function globalChoiceSettingsList(group, ctx, done, requestRender, coordinator, onApplied) {
14388
+ const settings = GLOBAL_CHOICE_SETTINGS[group];
14389
+ const items = globalChoiceSettingsItems(group, loadConfig(), coordinator);
14390
+ let list;
14391
+ list = new SettingsList2(items, 9, getSettingsListTheme2(), (id, display) => {
14392
+ coordinator.submit(group, id, display, (message) => ctx.ui.notify(message, "error"), onApplied);
14393
+ }, () => {
14394
+ unsubscribe();
14395
+ done();
14396
+ });
14397
+ const unsubscribe = coordinator.subscribe(settings.map((setting) => setting.id), (id, state) => {
14398
+ list.updateValue(id, state.display);
14399
+ if (state.config) {
14400
+ for (const setting of settings) {
14401
+ const item = items.find((candidate) => candidate.id === setting.id);
14402
+ if (item) {
14403
+ item.description = effectiveChoiceDescription(setting, state.config);
14404
+ }
13303
14405
  }
13304
- };
14406
+ }
14407
+ requestRender();
14408
+ });
14409
+ return list;
14410
+ }
14411
+ function settingsCategoryItems(policy, ctx, requestRender = () => {}, coordinator = new GlobalSettingsCoordinator, onApplied = () => {}, writeConfig = writeGlobalConfigValue) {
14412
+ return [
14413
+ {
14414
+ id: "session",
14415
+ label: "Current branch",
14416
+ description: "Agent access, automatic compaction, and footer status",
14417
+ currentValue: "3 settings",
14418
+ submenu: (_current, done) => sessionSettingsList(policy, ctx, done)
14419
+ },
14420
+ ...GLOBAL_CHOICE_CATEGORIES.map(({ group, label, description }) => ({
14421
+ id: group,
14422
+ label,
14423
+ description,
14424
+ currentValue: `${GLOBAL_CHOICE_SETTINGS[group].length} settings`,
14425
+ submenu: (_current, done) => globalChoiceSettingsList(group, ctx, done, requestRender, coordinator, onApplied)
14426
+ })),
14427
+ ...complexSettingsCategories(ctx, requestRender, writeConfig)
14428
+ ];
14429
+ }
14430
+ function createSettingsRoot(policy, ctx, onCancel, requestRender = () => {}, coordinator = new GlobalSettingsCoordinator, onApplied = () => {}, writeConfig = writeGlobalConfigValue) {
14431
+ return new SettingsList2(settingsCategoryItems(policy, ctx, requestRender, coordinator, onApplied, writeConfig), 8, getSettingsListTheme2(), () => {}, onCancel);
14432
+ }
14433
+ function deepestFocusable(component) {
14434
+ let current = component;
14435
+ let focusable;
14436
+ while (current) {
14437
+ if (typeof current.focused === "boolean") {
14438
+ focusable = current;
14439
+ }
14440
+ current = current.submenuComponent;
14441
+ }
14442
+ return focusable;
14443
+ }
14444
+ function createSettingsController(root, display, requestRender) {
14445
+ let focused = false;
14446
+ let target;
14447
+ const syncFocus = () => {
14448
+ const next = focused ? deepestFocusable(root) : undefined;
14449
+ if (target !== next) {
14450
+ if (target)
14451
+ target.focused = false;
14452
+ target = next;
14453
+ }
14454
+ if (target)
14455
+ target.focused = focused;
14456
+ };
14457
+ return {
14458
+ get focused() {
14459
+ return focused;
14460
+ },
14461
+ set focused(value) {
14462
+ focused = value;
14463
+ syncFocus();
14464
+ },
14465
+ render(width) {
14466
+ syncFocus();
14467
+ return display.render(width);
14468
+ },
14469
+ handleInput(data) {
14470
+ root.handleInput(data);
14471
+ syncFocus();
14472
+ requestRender();
14473
+ },
14474
+ invalidate: () => display.invalidate()
14475
+ };
14476
+ }
14477
+ async function showSmartCompactSettings(ctx, policy, coordinator = new GlobalSettingsCoordinator, onApplied = () => {}) {
14478
+ const writeConfig = async (path16, value) => {
14479
+ const config = await writeGlobalConfigValue(path16, value);
14480
+ await onApplied(path16, config);
14481
+ return config;
14482
+ };
14483
+ await ctx.ui.custom((tui, theme, _keybindings, done) => {
14484
+ const root = createSettingsRoot(policy, ctx, done, () => tui.requestRender(), coordinator, onApplied, writeConfig);
14485
+ const container = new Container4;
14486
+ container.addChild(new Text4(theme.fg("accent", theme.bold("Smart Compact Settings")), 0, 0));
14487
+ container.addChild(new Text4(theme.fg("dim", "Global defaults and branch-specific overrides."), 0, 0));
14488
+ container.addChild(new Text4(theme.fg("dim", "Manual /smart-compact stays available in every mode."), 0, 0));
14489
+ container.addChild(new Text4("", 0, 0));
14490
+ container.addChild(root);
14491
+ container.addChild(new Text4("", 0, 0));
14492
+ container.addChild(new Text4(theme.fg("dim", "\u2191\u2193 navigate \xB7 enter open/change \xB7 esc back/close"), 0, 0));
14493
+ return createSettingsController(root, container, () => tui.requestRender());
13305
14494
  });
13306
14495
  }
13307
14496
 
@@ -13474,6 +14663,7 @@ async function runInteractiveCompaction(ctx, config, dependencies) {
13474
14663
  });
13475
14664
  }
13476
14665
  function registerSmartCompactCommand(pi, dependencies) {
14666
+ const settingsCoordinator = new GlobalSettingsCoordinator;
13477
14667
  pi.registerCommand("smart-compact", {
13478
14668
  description: "EESV smart compaction v" + VERSION + ". Usage: /smart-compact [model|settings] [mode] [flags] [--focus=topic] [--max-calls=N] [--max-input-tokens=N] [--note=text | -- text]",
13479
14669
  getArgumentCompletions(prefix) {
@@ -13526,7 +14716,7 @@ function registerSmartCompactCommand(pi, dependencies) {
13526
14716
  ctx.ui.notify("Smart Compact settings require TUI mode. Use settings.json for permanent defaults.", "warning");
13527
14717
  return;
13528
14718
  }
13529
- await showSmartCompactSettings(ctx, dependencies.policy);
14719
+ await showSmartCompactSettings(ctx, dependencies.policy, settingsCoordinator, (path16) => dependencies.onGlobalSettingApplied?.(path16, ctx));
13530
14720
  return;
13531
14721
  }
13532
14722
  const config = loadConfig();
@@ -13573,13 +14763,34 @@ function registerSmartCompactCommand(pi, dependencies) {
13573
14763
  // src/app/smart-compact-policy.ts
13574
14764
  var SMART_COMPACT_TOOL_NAME = "smart_compact";
13575
14765
  var SMART_COMPACT_POLICY_ENTRY = "smart-compact-policy";
13576
- var POLICY_VERSION = 2;
14766
+ var POLICY_VERSION = 3;
13577
14767
  var STATUS_KEY = "smart-compact-policy";
13578
14768
  function persistedPolicy(value) {
13579
14769
  if (typeof value !== "object" || value === null)
13580
14770
  return null;
13581
14771
  const candidate = value;
13582
- if (candidate.version === POLICY_VERSION && (candidate.agentToolAccess === "inherit" || candidate.agentToolAccess === "enabled" || candidate.agentToolAccess === "disabled") && typeof candidate.autoTrigger === "boolean") {
14772
+ if (candidate.version === POLICY_VERSION && typeof candidate.overrides === "object" && candidate.overrides !== null && !Array.isArray(candidate.overrides)) {
14773
+ const values = candidate.overrides;
14774
+ const overrides = {};
14775
+ if (values.agentToolAccess !== undefined) {
14776
+ if (values.agentToolAccess !== "inherit" && values.agentToolAccess !== "enabled" && values.agentToolAccess !== "disabled") {
14777
+ return null;
14778
+ }
14779
+ overrides.agentToolAccess = values.agentToolAccess;
14780
+ }
14781
+ if (values.autoTrigger !== undefined) {
14782
+ if (typeof values.autoTrigger !== "boolean")
14783
+ return null;
14784
+ overrides.autoTrigger = values.autoTrigger;
14785
+ }
14786
+ if (values.showStatus !== undefined) {
14787
+ if (typeof values.showStatus !== "boolean")
14788
+ return null;
14789
+ overrides.showStatus = values.showStatus;
14790
+ }
14791
+ return overrides;
14792
+ }
14793
+ if (candidate.version === 2 && (candidate.agentToolAccess === "inherit" || candidate.agentToolAccess === "enabled" || candidate.agentToolAccess === "disabled") && typeof candidate.autoTrigger === "boolean") {
13583
14794
  const desired = {
13584
14795
  agentToolAccess: candidate.agentToolAccess,
13585
14796
  autoTrigger: candidate.autoTrigger,
@@ -13618,13 +14829,18 @@ function statusText(policy) {
13618
14829
  return "smart-compact: auto off";
13619
14830
  }
13620
14831
  function createSmartCompactPolicy(pi) {
13621
- let current = configDefaults();
14832
+ let overrides = {};
14833
+ const desired = () => ({
14834
+ ...configDefaults(),
14835
+ ...overrides
14836
+ });
13622
14837
  const effectiveToolState = () => pi.getActiveTools().includes(SMART_COMPACT_TOOL_NAME);
13623
14838
  const snapshot = () => ({
13624
- ...current,
14839
+ ...desired(),
13625
14840
  agentToolEnabled: effectiveToolState()
13626
14841
  });
13627
14842
  const apply = (ctx) => {
14843
+ const current = desired();
13628
14844
  const active = pi.getActiveTools();
13629
14845
  const hasTool = active.includes(SMART_COMPACT_TOOL_NAME);
13630
14846
  if (current.agentToolAccess === "enabled" && !hasTool) {
@@ -13636,45 +14852,64 @@ function createSmartCompactPolicy(pi) {
13636
14852
  ctx.ui.setStatus(STATUS_KEY, current.showStatus ? statusText(effective) : undefined);
13637
14853
  return effective;
13638
14854
  };
14855
+ const restoreToolMembership = (enabled2) => {
14856
+ const active = pi.getActiveTools();
14857
+ const hasTool = active.includes(SMART_COMPACT_TOOL_NAME);
14858
+ if (enabled2 && !hasTool) {
14859
+ pi.setActiveTools([...new Set([...active, SMART_COMPACT_TOOL_NAME])]);
14860
+ } else if (!enabled2 && hasTool) {
14861
+ pi.setActiveTools(active.filter((name) => name !== SMART_COMPACT_TOOL_NAME));
14862
+ }
14863
+ };
14864
+ const persist = (next, ctx) => {
14865
+ const previous = overrides;
14866
+ const previousToolEnabled = effectiveToolState();
14867
+ overrides = next;
14868
+ try {
14869
+ const effective = apply(ctx);
14870
+ pi.appendEntry(SMART_COMPACT_POLICY_ENTRY, { version: POLICY_VERSION, overrides: { ...overrides } });
14871
+ return { ok: true, policy: effective };
14872
+ } catch (error2) {
14873
+ debugError("Smart Compact policy update failed", error2);
14874
+ overrides = previous;
14875
+ try {
14876
+ restoreToolMembership(previousToolEnabled);
14877
+ } catch (rollbackError) {
14878
+ debugError("Smart Compact policy rollback failed", rollbackError);
14879
+ }
14880
+ const rolledBack = snapshot();
14881
+ const previousDesired = desired();
14882
+ ctx.ui.setStatus(STATUS_KEY, previousDesired.showStatus ? statusText(rolledBack) : undefined);
14883
+ return {
14884
+ ok: false,
14885
+ policy: rolledBack,
14886
+ error: "Smart Compact settings could not be saved; the previous policy was restored."
14887
+ };
14888
+ }
14889
+ };
13639
14890
  return {
13640
14891
  snapshot,
14892
+ branchOverrides: () => ({ ...overrides }),
13641
14893
  isAgentToolEnabled: effectiveToolState,
13642
- isAutoTriggerEnabled: () => current.autoTrigger,
14894
+ isAutoTriggerEnabled: () => desired().autoTrigger,
13643
14895
  restore(ctx) {
13644
- current = configDefaults();
14896
+ overrides = {};
13645
14897
  for (const entry of ctx.sessionManager.getBranch()) {
13646
14898
  if (entry.type === "custom" && entry.customType === SMART_COMPACT_POLICY_ENTRY) {
13647
14899
  const restored = persistedPolicy(entry.data);
13648
14900
  if (restored)
13649
- current = { ...current, ...restored };
14901
+ overrides = restored;
13650
14902
  }
13651
14903
  }
13652
14904
  apply(ctx);
13653
14905
  },
13654
14906
  update(patch, ctx) {
13655
- const previous = current;
13656
- const previousActiveTools = pi.getActiveTools();
13657
- current = { ...current, ...patch };
13658
- try {
13659
- const effective = apply(ctx);
13660
- pi.appendEntry(SMART_COMPACT_POLICY_ENTRY, { version: POLICY_VERSION, ...current });
13661
- return { ok: true, policy: effective };
13662
- } catch (error2) {
13663
- debugError("Smart Compact policy update failed", error2);
13664
- current = previous;
13665
- try {
13666
- pi.setActiveTools(previousActiveTools);
13667
- } catch (rollbackError) {
13668
- debugError("Smart Compact policy rollback failed", rollbackError);
13669
- }
13670
- const rolledBack = snapshot();
13671
- ctx.ui.setStatus(STATUS_KEY, current.showStatus ? statusText(rolledBack) : undefined);
13672
- return {
13673
- ok: false,
13674
- policy: rolledBack,
13675
- error: "Smart Compact settings could not be saved; the previous policy was restored."
13676
- };
13677
- }
14907
+ return persist({ ...overrides, ...patch }, ctx);
14908
+ },
14909
+ reset(field, ctx) {
14910
+ const next = { ...overrides };
14911
+ delete next[field];
14912
+ return persist(next, ctx);
13678
14913
  }
13679
14914
  };
13680
14915
  }
@@ -13710,19 +14945,29 @@ function smartCompactExtension(pi) {
13710
14945
  const settledAutoTrigger = createSettledAutoTrigger();
13711
14946
  const policy = createSmartCompactPolicy(pi);
13712
14947
  const nativeContinuity = createNativeContinuityBridge();
14948
+ const applyFailureWrites = new Map;
13713
14949
  const recordApplyFailure = (pending, reason) => {
13714
14950
  if (!pending.metricsSnapshot)
13715
- return;
14951
+ return null;
13716
14952
  const cancelled = reason === "aborted" || reason === "shutdown";
13717
- appendMetricsSnapshot(pending.sessionId, {
14953
+ const write = appendMetricsSnapshot(pending.sessionId, {
13718
14954
  ...pending.metricsSnapshot,
13719
14955
  status: cancelled ? "cancelled" : "error",
13720
14956
  failureKind: cancelled ? "cancelled" : reason === "evicted" ? "internal" : "persistence",
13721
14957
  fallbackReason: "native-apply:" + reason
13722
14958
  });
14959
+ applyFailureWrites.set(pending.runId, write);
14960
+ write.finally(() => {
14961
+ if (applyFailureWrites.get(pending.runId) === write) {
14962
+ applyFailureWrites.delete(pending.runId);
14963
+ }
14964
+ });
14965
+ return write;
13723
14966
  };
13724
14967
  const commitCandidates = createCompactionCommitStore({
13725
- onDiscard: recordApplyFailure
14968
+ onDiscard: (pending, reason) => {
14969
+ recordApplyFailure(pending, reason);
14970
+ }
13726
14971
  });
13727
14972
  const onNativeApplyError = (runId) => Boolean(commitCandidates.discard(runId, "apply-error"));
13728
14973
  const activateOnlineDamage = (pending) => {
@@ -13747,12 +14992,19 @@ function smartCompactExtension(pi) {
13747
14992
  return false;
13748
14993
  }
13749
14994
  };
13750
- registerContextTools(pi);
14995
+ const contextToolAvailability = registerContextTools(pi);
13751
14996
  registerSmartCompactCommand(pi, {
13752
14997
  pendingRef,
13753
14998
  runLock: isRunning,
13754
14999
  onNativeApplyError,
13755
- policy
15000
+ policy,
15001
+ onGlobalSettingApplied(path16, ctx) {
15002
+ try {
15003
+ applyGlobalSettingRuntime(path16, ctx, policy, contextToolAvailability);
15004
+ } catch (error2) {
15005
+ debugError("Smart Compact runtime settings refresh failed", error2);
15006
+ }
15007
+ }
13756
15008
  });
13757
15009
  pi.on("session_start", (_event, ctx) => {
13758
15010
  policy.restore(ctx);
@@ -13890,6 +15142,24 @@ function smartCompactExtension(pi) {
13890
15142
  if (state)
13891
15143
  nativeContinuity.stage({ projectId, sessionId, branchHeadId }, renderContinuityCapsule(state));
13892
15144
  });
15145
+ const compactFailedEvents = pi;
15146
+ compactFailedEvents.on("session_compact_failed", async (event, ctx) => {
15147
+ if (!event.fromExtension)
15148
+ return;
15149
+ const sessionId = resolveSessionId(ctx);
15150
+ clearCompactProgress(ctx);
15151
+ const reason = event.aborted ? "aborted" : "apply-error";
15152
+ const discarded = commitCandidates.clearSession(sessionId, reason);
15153
+ const metricWrites = discarded.flatMap((pending) => {
15154
+ const write = applyFailureWrites.get(pending.runId);
15155
+ return write ? [write] : [];
15156
+ });
15157
+ if (metricWrites.length > 0)
15158
+ await Promise.all(metricWrites);
15159
+ if (discarded.length > 0) {
15160
+ warn("Discarded " + discarded.length + " staged smart compaction after native apply " + (event.aborted ? "abort" : "failure") + (event.errorMessage ? ": " + event.errorMessage : ""));
15161
+ }
15162
+ });
13893
15163
  pi.on("before_agent_start", async (_event, ctx) => {
13894
15164
  const scope = resolveGraphScope(ctx);
13895
15165
  if (!scope?.branchHeadId)
@@ -13950,7 +15220,7 @@ function smartCompactExtension(pi) {
13950
15220
  });
13951
15221
  }
13952
15222
  export {
13953
- resolveModels,
15223
+ smartCompactExtension as default,
13954
15224
  findModelById,
13955
- smartCompactExtension as default
15225
+ resolveModels
13956
15226
  };