pi-smart-compact 9.5.0 → 9.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.0";
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))
@@ -1121,8 +1473,8 @@ function isKnownPathReference(ref, knownPaths) {
1121
1473
  if (!normalizedRef)
1122
1474
  return false;
1123
1475
  const pathShaped = normalizedRef.includes("/");
1124
- return knownPaths.some((path2) => {
1125
- const normalizedPath = normalizePath(path2).replace(/^\/+/, "");
1476
+ return knownPaths.some((path3) => {
1477
+ const normalizedPath = normalizePath(path3).replace(/^\/+/, "");
1126
1478
  if (normalizedPath === normalizedRef || normalizedPath.endsWith("/" + normalizedRef))
1127
1479
  return true;
1128
1480
  if (normalizedPath.endsWith(normalizedRef)) {
@@ -1473,15 +1825,15 @@ function smartKeepBoundaryCandidates(msgs, keepFromIndex, branchEntries) {
1473
1825
  }
1474
1826
  function collectToolCallIds(blocks, msgIndex, out) {
1475
1827
  for (const block of blocks) {
1476
- if (!isRecord(block) || block.type !== "toolCall")
1828
+ if (!isRecord2(block) || block.type !== "toolCall")
1477
1829
  continue;
1478
1830
  if (typeof block.id === "string")
1479
1831
  out.set(block.id, msgIndex);
1480
1832
  const args = block.arguments;
1481
- if (block.name !== "multi_tool_use.parallel" || !isRecord(args) || !Array.isArray(args.tool_uses))
1833
+ if (block.name !== "multi_tool_use.parallel" || !isRecord2(args) || !Array.isArray(args.tool_uses))
1482
1834
  continue;
1483
1835
  for (const nested of args.tool_uses) {
1484
- if (isRecord(nested) && typeof nested.id === "string")
1836
+ if (isRecord2(nested) && typeof nested.id === "string")
1485
1837
  out.set(nested.id, msgIndex);
1486
1838
  }
1487
1839
  }
@@ -1490,7 +1842,7 @@ function buildToolCallBoundaryIndex(msgs) {
1490
1842
  const map = new Map;
1491
1843
  for (let i = 0;i < msgs.length; i++) {
1492
1844
  const message = msgs[i].message;
1493
- if (!isRecord(message) || message.role !== "assistant")
1845
+ if (!isRecord2(message) || message.role !== "assistant")
1494
1846
  continue;
1495
1847
  const blocks = Array.isArray(message.content) ? message.content : [];
1496
1848
  collectToolCallIds(blocks, i, map);
@@ -1512,7 +1864,7 @@ function guardToolCallBoundary(msgs, keepFrom, tcMap = buildToolCallBoundaryInde
1512
1864
  changed = false;
1513
1865
  for (let i = adjusted;i < msgs.length; i++) {
1514
1866
  const message = msgs[i].message;
1515
- if (!isRecord(message) || message.role !== "toolResult")
1867
+ if (!isRecord2(message) || message.role !== "toolResult")
1516
1868
  continue;
1517
1869
  const tcId = typeof message.toolCallId === "string" ? message.toolCallId : undefined;
1518
1870
  if (!tcId)
@@ -1535,7 +1887,7 @@ function advancePastToolCallBoundary(msgs, keepFrom, tcMap = buildToolCallBounda
1535
1887
  let next = adjusted;
1536
1888
  for (let i = adjusted;i < msgs.length; i++) {
1537
1889
  const message = msgs[i].message;
1538
- if (!isRecord(message) || message.role !== "toolResult")
1890
+ if (!isRecord2(message) || message.role !== "toolResult")
1539
1891
  continue;
1540
1892
  const tcIdx = typeof message.toolCallId === "string" ? tcMap.get(message.toolCallId) : undefined;
1541
1893
  if (i === adjusted && tcIdx === undefined || tcIdx !== undefined && tcIdx < adjusted) {
@@ -1863,7 +2215,7 @@ function buildSummaryPathEvidence(paths, budgetTokens = PROFILES.balanced.summar
1863
2215
  const unique = Array.from(new Set(paths.filter(Boolean)));
1864
2216
  if (!unique.length)
1865
2217
  return new Map;
1866
- const full = unique.map((path2) => [path2, summaryPathLine(path2)]);
2218
+ const full = unique.map((path3) => [path3, summaryPathLine(path3)]);
1867
2219
  const minimumPerLine = JSON.stringify("#" + "x".repeat(12)).length + 3;
1868
2220
  const budgetChars = Math.max(unique.length * minimumPerLine, Math.min(20000, Math.max(4000, Math.floor(budgetTokens * 2))));
1869
2221
  if (full.reduce((total, [, line]) => total + line.length + 3, 0) <= budgetChars) {
@@ -1871,21 +2223,21 @@ function buildSummaryPathEvidence(paths, budgetTokens = PROFILES.balanced.summar
1871
2223
  }
1872
2224
  const digests = new Map;
1873
2225
  const owners = new Map;
1874
- for (const path2 of unique) {
1875
- const fullDigest = createHash("sha256").update(path2).digest("base64url");
2226
+ for (const path3 of unique) {
2227
+ const fullDigest = createHash("sha256").update(path3).digest("base64url");
1876
2228
  let digest = fullDigest.slice(0, 12);
1877
2229
  const owner = owners.get(digest);
1878
- if (owner && owner !== path2) {
2230
+ if (owner && owner !== path3) {
1879
2231
  digest = fullDigest;
1880
2232
  digests.set(owner, createHash("sha256").update(owner).digest("base64url"));
1881
2233
  }
1882
- owners.set(digest, path2);
1883
- digests.set(path2, digest);
2234
+ owners.set(digest, path3);
2235
+ digests.set(path3, digest);
1884
2236
  }
1885
2237
  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) ?? "")
2238
+ return new Map(unique.map((path3) => [
2239
+ path3,
2240
+ compactPathLine(path3, perPath, digests.get(path3) ?? "")
1889
2241
  ]));
1890
2242
  }
1891
2243
  function mergeBodies(first, second) {
@@ -2403,7 +2755,7 @@ function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
2403
2755
  const shiftBasename = (filePath) => {
2404
2756
  if (!filePath)
2405
2757
  return null;
2406
- const base = path2.basename(filePath);
2758
+ const base = path3.basename(filePath);
2407
2759
  return GENERIC_BASENAMES.has(base.toLowerCase()) ? null : base;
2408
2760
  };
2409
2761
  const topics = [];
@@ -2557,7 +2909,7 @@ function extractOpenLoops(msgs, extraction) {
2557
2909
  }));
2558
2910
  for (const err of extraction.errors.filter((e) => !e.resolved)) {
2559
2911
  const errLower = err.message.toLowerCase();
2560
- const errFiles = fileNeedles.filter(({ needles }) => needles.some((n) => errLower.includes(n))).map(({ path: path3 }) => path3);
2912
+ const errFiles = fileNeedles.filter(({ needles }) => needles.some((n) => errLower.includes(n))).map(({ path: path4 }) => path4);
2561
2913
  loops.push({
2562
2914
  id: ID_PREFIX.OPEN_LOOP + ++loopId,
2563
2915
  type: "bugfix",
@@ -2706,191 +3058,6 @@ function extractStructured(msgs, pc, precomputedTcIdx) {
2706
3058
  };
2707
3059
  }
2708
3060
 
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
3061
  // src/utils/id-fingerprint.ts
2895
3062
  import crypto2 from "crypto";
2896
3063
  var FINGERPRINT_TAIL_LEN = 16;
@@ -4828,7 +4995,7 @@ function advancePastNonPortableToolExchanges(msgs, keepFrom, toolCallIndex) {
4828
4995
  const exchangeEnds = new Map;
4829
4996
  for (let index = keepFrom;index < msgs.length; index++) {
4830
4997
  const message = msgs[index].message;
4831
- if (!isRecord(message) || message.role !== "toolResult" || typeof message.toolCallId !== "string")
4998
+ if (!isRecord2(message) || message.role !== "toolResult" || typeof message.toolCallId !== "string")
4832
4999
  continue;
4833
5000
  const callIndex = toolCallIndex.get(message.toolCallId);
4834
5001
  if (callIndex === undefined || callIndex < keepFrom)
@@ -4838,10 +5005,10 @@ function advancePastNonPortableToolExchanges(msgs, keepFrom, toolCallIndex) {
4838
5005
  let adjusted = keepFrom;
4839
5006
  for (let index = keepFrom;index < msgs.length; index++) {
4840
5007
  const message = msgs[index].message;
4841
- if (!isRecord(message))
5008
+ if (!isRecord2(message))
4842
5009
  continue;
4843
5010
  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)));
5011
+ const nonPortable = message.content.some((block) => isRecord2(block) && block.type === "toolCall" && (typeof block.name !== "string" || !PORTABLE_TOOL_NAME_RE.test(block.name)));
4845
5012
  if (nonPortable)
4846
5013
  adjusted = Math.max(adjusted, exchangeEnds.get(index) ?? index + 1);
4847
5014
  } else if (message.role === "toolResult" && typeof message.toolName === "string" && !PORTABLE_TOOL_NAME_RE.test(message.toolName)) {
@@ -4908,7 +5075,7 @@ function planCompactionWindow(input) {
4908
5075
  let protectedUserIndex;
4909
5076
  for (let index = msgs.length - 1;index >= 0; index--) {
4910
5077
  const message = msgs[index].message;
4911
- if (!isRecord(message) || message.role !== "user")
5078
+ if (!isRecord2(message) || message.role !== "user")
4912
5079
  continue;
4913
5080
  userOrdinal++;
4914
5081
  protectedUserIndex = index;
@@ -4939,7 +5106,7 @@ function planCompactionWindow(input) {
4939
5106
  const targetAfterTokens = !force && modelContextWindow ? modelContextWindow * targetPercent / 100 : fixedContextTokens + effectiveRetentionCeiling + finalSummaryAllowance;
4940
5107
  let reason = "viable";
4941
5108
  const firstKeptMessage = msgs[keepFrom]?.message;
4942
- if (nonPortableTailBlocked || isRecord(firstKeptMessage) && firstKeptMessage.role === "toolResult")
5109
+ if (nonPortableTailBlocked || isRecord2(firstKeptMessage) && firstKeptMessage.role === "toolResult")
4943
5110
  reason = "unsafe-tool-boundary";
4944
5111
  else if (keepFrom <= 0)
4945
5112
  reason = "no-eligible-prefix";
@@ -11420,6 +11587,19 @@ async function runSmartCompact(opts) {
11420
11587
  }
11421
11588
  }
11422
11589
 
11590
+ // src/app/global-settings-runtime.ts
11591
+ var POLICY_PATHS = new Set([
11592
+ "agentToolAccess",
11593
+ "autoTrigger",
11594
+ "showStatus"
11595
+ ]);
11596
+ function applyGlobalSettingRuntime(path14, ctx, policy, contextTools) {
11597
+ if (POLICY_PATHS.has(path14))
11598
+ policy.restore(ctx);
11599
+ if (path14 === "contextGraphEnabled")
11600
+ contextTools.apply();
11601
+ }
11602
+
11423
11603
  // src/app/pending-slot.ts
11424
11604
  function createPendingSlot(opts) {
11425
11605
  const ttlMs = opts.ttlMs;
@@ -11835,6 +12015,7 @@ function resolveGraphScope(ctx) {
11835
12015
  };
11836
12016
  }
11837
12017
  function registerContextTools(pi) {
12018
+ const availability = createContextToolAvailability(pi);
11838
12019
  pi.registerTool({
11839
12020
  name: "smart_recall",
11840
12021
  label: "Smart Recall",
@@ -12039,12 +12220,48 @@ Paths: ` + relatedPaths.join(", ") : ""));
12039
12220
  details: { memory, redactions: scrubber.count() }
12040
12221
  };
12041
12222
  } catch (error2) {
12042
- debugError("Project memory persistence failed", error2);
12043
- const message = scrubber.scrubText(error2 instanceof Error ? error2.message : String(error2)).value;
12044
- throw new Error("Project memory could not be saved: " + message);
12223
+ debugError("Project memory persistence failed", error2);
12224
+ const message = scrubber.scrubText(error2 instanceof Error ? error2.message : String(error2)).value;
12225
+ throw new Error("Project memory could not be saved: " + message);
12226
+ }
12227
+ }
12228
+ });
12229
+ availability.apply();
12230
+ pi.on("session_start", availability.apply);
12231
+ return availability;
12232
+ }
12233
+ var CONTEXT_TOOL_NAMES = ["smart_recall", "smart_save_memory"];
12234
+ function createContextToolAvailability(pi) {
12235
+ const hiddenByConfig = new Set;
12236
+ let disabledByConfig = false;
12237
+ return {
12238
+ apply() {
12239
+ try {
12240
+ const enabled = loadConfig().contextGraphEnabled;
12241
+ const active = pi.getActiveTools();
12242
+ if (!enabled) {
12243
+ const visibleContextTools = CONTEXT_TOOL_NAMES.filter((name) => active.includes(name));
12244
+ for (const name of visibleContextTools)
12245
+ hiddenByConfig.add(name);
12246
+ disabledByConfig = true;
12247
+ if (visibleContextTools.length > 0) {
12248
+ pi.setActiveTools(active.filter((name) => !CONTEXT_TOOL_NAMES.includes(name)));
12249
+ }
12250
+ return;
12251
+ }
12252
+ if (disabledByConfig) {
12253
+ const restored = [...hiddenByConfig].filter((name) => !active.includes(name));
12254
+ if (restored.length > 0) {
12255
+ pi.setActiveTools([...new Set([...active, ...restored])]);
12256
+ }
12257
+ hiddenByConfig.clear();
12258
+ disabledByConfig = false;
12259
+ }
12260
+ } catch (error2) {
12261
+ debugError("Context tool availability update failed", error2);
12045
12262
  }
12046
12263
  }
12047
- });
12264
+ };
12048
12265
  }
12049
12266
 
12050
12267
  // src/app/model-routing.ts
@@ -13228,80 +13445,874 @@ async function showMetricsDashboardUI(ctx, opts) {
13228
13445
 
13229
13446
  // src/ui/settings-overlay.ts
13230
13447
  import {
13231
- getSettingsListTheme
13448
+ getSettingsListTheme as getSettingsListTheme2
13232
13449
  } from "@earendil-works/pi-coding-agent";
13450
+ import {
13451
+ Container as Container4,
13452
+ SettingsList as SettingsList2,
13453
+ Text as Text4
13454
+ } from "@earendil-works/pi-tui";
13455
+
13456
+ // src/ui/settings-complex.ts
13457
+ import path15 from "path";
13233
13458
  import {
13234
13459
  Container as Container3,
13460
+ Input,
13461
+ Key as Key4,
13462
+ matchesKey as matchesKey4,
13463
+ SelectList as SelectList3,
13235
13464
  SettingsList,
13236
13465
  Text as Text3
13237
13466
  } 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) {
13467
+ import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
13468
+ var MODEL_SETTINGS = [
13469
+ {
13470
+ id: "summaryModel",
13471
+ label: "Summary model",
13472
+ description: "Model used for synthesis and verification fallback."
13473
+ },
13474
+ {
13475
+ id: "segmentationModel",
13476
+ label: "Segmentation model",
13477
+ description: "Optional model used for transcript exploration."
13478
+ },
13479
+ {
13480
+ id: "verificationModel",
13481
+ label: "Verification model",
13482
+ description: "Optional model used for repair after verification."
13483
+ }
13484
+ ];
13485
+ var PROFILE_NAMES = Object.keys(PROFILES);
13486
+ function numberParser(options) {
13487
+ return (input) => {
13488
+ const trimmed = input.trim();
13489
+ if (!trimmed)
13490
+ return;
13491
+ const value = Number(trimmed);
13492
+ const inRange = value >= options.min && value <= options.max;
13493
+ if (!Number.isFinite(value) || options.integer !== false && !Number.isSafeInteger(value) || !inRange && !(options.zeroOrRange && value === 0)) {
13494
+ const allowed = options.zeroOrRange ? `0 or ${options.min}\u2013${options.max}` : `${options.min}\u2013${options.max}`;
13495
+ throw new Error(`Enter ${options.integer === false ? "a number" : "an integer"} in ${allowed}.`);
13496
+ }
13497
+ return value;
13498
+ };
13499
+ }
13500
+ function scalarFormat(value) {
13501
+ return value === undefined ? "default" : String(value);
13502
+ }
13503
+ var LIMIT_SETTINGS = [
13504
+ {
13505
+ id: "minContextPercent",
13506
+ label: "Minimum context percent",
13507
+ description: "Auto compaction starts at or above this context usage.",
13508
+ placeholder: "0\u2013100; blank uses default",
13509
+ parse: numberParser(CONFIG_NUMERIC_LIMITS.minContextPercent),
13510
+ format: scalarFormat
13511
+ },
13512
+ {
13513
+ id: "autoTriggerTimeoutMs",
13514
+ label: "Auto-trigger timeout",
13515
+ description: "Maximum host auto-compaction time in milliseconds.",
13516
+ placeholder: "1000\u2013300000; blank uses default",
13517
+ parse: numberParser(CONFIG_NUMERIC_LIMITS.autoTriggerTimeoutMs),
13518
+ format: scalarFormat
13519
+ },
13520
+ {
13521
+ id: "maxLlmCalls",
13522
+ label: "Maximum LLM calls",
13523
+ description: "Zero uses the selected mode's call cap.",
13524
+ placeholder: "0\u2013100; blank uses default",
13525
+ parse: numberParser(CONFIG_NUMERIC_LIMITS.maxLlmCalls),
13526
+ format: scalarFormat
13527
+ },
13528
+ {
13529
+ id: "maxLlmInputTokens",
13530
+ label: "Maximum LLM input tokens",
13531
+ description: "Zero uses the selected mode's token cap.",
13532
+ placeholder: "0\u20131000000; blank uses default",
13533
+ parse: numberParser(CONFIG_NUMERIC_LIMITS.maxLlmInputTokens),
13534
+ format: scalarFormat
13535
+ },
13536
+ {
13537
+ id: "codexMaxCallMs",
13538
+ label: "Codex call watchdog",
13539
+ description: "Zero derives the per-call watchdog automatically.",
13540
+ placeholder: "0 or 5000\u2013300000; blank uses default",
13541
+ parse: numberParser(CONFIG_NUMERIC_LIMITS.codexMaxCallMs),
13542
+ format: scalarFormat
13543
+ },
13544
+ {
13545
+ id: "maxLatencyMs",
13546
+ label: "Pipeline latency limit",
13547
+ description: "Zero disables the overall pipeline deadline.",
13548
+ placeholder: "0 or 5000\u2013600000; blank uses default",
13549
+ parse: numberParser(CONFIG_NUMERIC_LIMITS.maxLatencyMs),
13550
+ format: scalarFormat
13551
+ }
13552
+ ];
13553
+ var PATH_SETTINGS = [
13554
+ {
13555
+ id: "backupDir",
13556
+ label: "Backup directory",
13557
+ description: "Directory for recovery Markdown files.",
13558
+ placeholder: "Absolute path; blank uses default",
13559
+ parse(input) {
13560
+ const value = input.trim();
13561
+ if (!value)
13562
+ return;
13563
+ if (value.includes("\x00") || /[\r\n]/.test(value)) {
13564
+ throw new Error("Backup directory must be a single valid path.");
13565
+ }
13566
+ if (!path15.isAbsolute(value)) {
13567
+ throw new Error("Backup directory must be an absolute path.");
13568
+ }
13569
+ return value;
13570
+ },
13571
+ format: scalarFormat
13572
+ },
13573
+ {
13574
+ id: "pinPaths",
13575
+ label: "Pinned paths",
13576
+ description: "Comma-separated file paths that summaries must preserve.",
13577
+ placeholder: "src/api.ts, docs/design.md; blank clears",
13578
+ parse(input) {
13579
+ const trimmed = input.trim();
13580
+ if (!trimmed)
13581
+ return;
13582
+ const paths = trimmed.split(/[,\n]/).map((value) => value.trim()).filter(Boolean);
13583
+ if (paths.some((value) => value.includes("\x00"))) {
13584
+ throw new Error("Pinned paths cannot contain NUL characters.");
13585
+ }
13586
+ return [...new Set(paths)];
13587
+ },
13588
+ format(value) {
13589
+ return value === undefined ? "default" : Array.isArray(value) ? value.join(", ") || "none" : String(value);
13590
+ }
13591
+ }
13592
+ ];
13593
+ var PROFILE_FIELDS = [
13594
+ ["summaryBudgetTokens", "Summary budget"],
13595
+ ["keepRecentTokens", "Recent raw tail"],
13596
+ ["minChunkTokens", "Minimum chunk"],
13597
+ ["maxChunkTokens", "Maximum chunk"],
13598
+ ["singlePassMaxTokens", "Single-pass limit"],
13599
+ ["batchMaxTokens", "Batch limit"]
13600
+ ];
13601
+ function profileSettings(profile) {
13602
+ return PROFILE_FIELDS.map(([key, label]) => {
13603
+ const [min, max] = PROFILE_NUMERIC_BOUNDS[key];
13604
+ return {
13605
+ id: `profiles.${profile}.${key}`,
13606
+ label,
13607
+ description: `${profile} profile token budget; related chunk bounds must remain consistent.`,
13608
+ placeholder: `${min}\u2013${max}; blank uses built-in`,
13609
+ parse: numberParser({ min, max }),
13610
+ format: scalarFormat
13611
+ };
13612
+ });
13613
+ }
13614
+ function effectiveValue(config, id) {
13615
+ const parts = id.split(".");
13616
+ if (parts[0] !== "profiles") {
13617
+ return config[id];
13618
+ }
13619
+ const [, profile, key] = parts;
13620
+ return config.profiles[profile][key];
13621
+ }
13622
+
13623
+ class InputSettingEditor extends Container3 {
13624
+ requestRender;
13625
+ save;
13626
+ done;
13627
+ input = new Input;
13628
+ status = new Text3("", 0, 0);
13629
+ saving = false;
13630
+ pending = Promise.resolve();
13631
+ get focused() {
13632
+ return this.input.focused;
13633
+ }
13634
+ set focused(value) {
13635
+ this.input.focused = value;
13636
+ }
13637
+ constructor(setting, initial, requestRender, save, done) {
13638
+ super();
13639
+ this.requestRender = requestRender;
13640
+ this.save = save;
13641
+ this.done = done;
13642
+ this.addChild(new Text3(setting.label, 0, 0));
13643
+ this.addChild(new Text3(setting.description, 0, 0));
13644
+ this.addChild(new Text3(`Hint: ${setting.placeholder}`, 0, 0));
13645
+ this.addChild(new Text3("", 0, 0));
13646
+ this.input.setValue(initial);
13647
+ this.input.handleInput("\x1B[F");
13648
+ this.input.onSubmit = (value) => {
13649
+ if (this.saving)
13650
+ return;
13651
+ let parsed;
13652
+ try {
13653
+ parsed = setting.parse(value);
13654
+ } catch (error2) {
13655
+ this.status.setText(error2 instanceof Error ? error2.message : String(error2));
13656
+ this.requestRender();
13657
+ return;
13658
+ }
13659
+ this.saving = true;
13660
+ this.status.setText("Saving\u2026");
13661
+ this.requestRender();
13662
+ this.pending = this.save(parsed).then(() => this.done()).catch((error2) => {
13663
+ this.saving = false;
13664
+ this.status.setText(error2 instanceof Error ? error2.message : String(error2));
13665
+ this.requestRender();
13666
+ });
13667
+ };
13668
+ this.input.onEscape = () => {
13669
+ if (!this.saving)
13670
+ this.done();
13671
+ };
13672
+ this.addChild(this.input);
13673
+ this.addChild(new Text3("", 0, 0));
13674
+ this.addChild(this.status);
13675
+ this.addChild(new Text3("Enter save \xB7 Esc cancel", 0, 0));
13676
+ }
13677
+ handleInput(data) {
13678
+ this.input.handleInput(data);
13679
+ this.requestRender();
13680
+ }
13681
+ settled() {
13682
+ return this.pending;
13683
+ }
13684
+ }
13685
+
13686
+ class ModelSettingEditor extends Container3 {
13687
+ requestRender;
13688
+ search = new Input;
13689
+ list;
13690
+ status = new Text3("", 0, 0);
13691
+ saving = false;
13692
+ pending = Promise.resolve();
13693
+ get focused() {
13694
+ return this.search.focused;
13695
+ }
13696
+ set focused(value) {
13697
+ this.search.focused = value;
13698
+ }
13699
+ constructor(models, selectedValue, requestRender, save, done) {
13700
+ super();
13701
+ this.requestRender = requestRender;
13702
+ this.addChild(new Text3("Search provider/model", 0, 0));
13703
+ this.addChild(this.search);
13704
+ this.addChild(new Text3("", 0, 0));
13705
+ this.list = new SelectList3(models, 10, {
13706
+ selectedPrefix: (text) => text,
13707
+ selectedText: (text) => text,
13708
+ description: (text) => text,
13709
+ scrollInfo: (text) => text,
13710
+ noMatch: (text) => text
13711
+ });
13712
+ const selectedIndex = models.findIndex((item) => item.settingValue === selectedValue);
13713
+ this.list.setSelectedIndex(Math.max(0, selectedIndex));
13714
+ this.list.onSelect = (item) => {
13715
+ if (this.saving)
13716
+ return;
13717
+ this.saving = true;
13718
+ this.status.setText("Saving\u2026");
13719
+ this.requestRender();
13720
+ const selected = models.find((candidate) => candidate.value === item.value);
13721
+ if (!selected)
13722
+ return;
13723
+ this.pending = save(selected.settingValue).then(done).catch((error2) => {
13724
+ this.saving = false;
13725
+ this.status.setText(error2 instanceof Error ? error2.message : String(error2));
13726
+ this.requestRender();
13727
+ });
13728
+ };
13729
+ this.list.onCancel = () => {
13730
+ if (!this.saving)
13731
+ done();
13732
+ };
13733
+ this.addChild(this.list);
13734
+ this.addChild(this.status);
13735
+ this.addChild(new Text3("Type to filter \xB7 Enter select \xB7 Esc cancel", 0, 0));
13736
+ }
13737
+ handleInput(data) {
13738
+ if (matchesKey4(data, Key4.up) || matchesKey4(data, Key4.down) || matchesKey4(data, Key4.enter) || matchesKey4(data, Key4.escape)) {
13739
+ this.list.handleInput(data);
13740
+ } else {
13741
+ this.search.handleInput(data);
13742
+ this.list.setFilter(this.search.getValue());
13743
+ }
13744
+ this.requestRender();
13745
+ }
13746
+ settled() {
13747
+ return this.pending;
13748
+ }
13749
+ }
13750
+ function inputSettingsList(settings, requestRender, done, writeConfig) {
13751
+ const config = loadConfig();
13752
+ const items = settings.map((setting) => {
13753
+ const override = readGlobalConfigValue(setting.id);
13754
+ const item = {
13755
+ id: setting.id,
13756
+ label: setting.label,
13757
+ description: `${setting.description} Effective global value: ${setting.format(effectiveValue(config, setting.id))}.`,
13758
+ currentValue: setting.format(override)
13759
+ };
13760
+ item.submenu = (_current, close) => {
13761
+ const persisted = readGlobalConfigValue(setting.id);
13762
+ return new InputSettingEditor(setting, persisted === undefined ? "" : Array.isArray(persisted) ? persisted.join(", ") : String(persisted), requestRender, async (value) => {
13763
+ const effective = await writeConfig(setting.id, value);
13764
+ item.currentValue = setting.format(value);
13765
+ item.description = `${setting.description} Effective global value: ${setting.format(effectiveValue(effective, setting.id))}.`;
13766
+ }, close);
13767
+ };
13768
+ return item;
13769
+ });
13770
+ return new SettingsList(items, 9, getSettingsListTheme(), () => {}, done);
13771
+ }
13772
+ function modelSettingsItems(ctx, requestRender, writeConfig = writeGlobalConfigValue) {
13773
+ const config = loadConfig();
13774
+ return MODEL_SETTINGS.map((setting) => {
13775
+ const override = readGlobalConfigValue(setting.id);
13776
+ const current = typeof override === "string" ? override : "default";
13777
+ const item = {
13778
+ id: setting.id,
13779
+ label: setting.label,
13780
+ description: `${setting.description} Effective global value: ${String(config[setting.id])}.`,
13781
+ currentValue: current
13782
+ };
13783
+ item.submenu = (_value, close) => {
13784
+ const persisted = readGlobalConfigValue(setting.id);
13785
+ const selectedValue = typeof persisted === "string" ? persisted : "default";
13786
+ const effective = loadConfig();
13787
+ item.currentValue = selectedValue;
13788
+ item.description = `${setting.description} Effective global value: ${String(effective[setting.id])}.`;
13789
+ const available = ctx.modelRegistry.getAvailable().map((model) => ({
13790
+ value: `${model.provider}/${model.id} \u2014 ${model.name}`,
13791
+ settingValue: `${model.provider}/${model.id}`,
13792
+ label: `${model.provider}/${model.id}`,
13793
+ description: model.name
13794
+ })).sort((left, right) => left.value.localeCompare(right.value));
13795
+ const choices = [
13796
+ {
13797
+ value: "default \u2014 use active session model",
13798
+ settingValue: "default",
13799
+ label: "default",
13800
+ description: "Remove the global model override"
13801
+ },
13802
+ ...available
13803
+ ];
13804
+ if (selectedValue !== "default" && !choices.some((candidate) => candidate.settingValue === selectedValue)) {
13805
+ choices.splice(1, 0, {
13806
+ value: selectedValue,
13807
+ settingValue: selectedValue,
13808
+ label: selectedValue,
13809
+ description: "Configured model is currently unavailable"
13810
+ });
13811
+ }
13812
+ return new ModelSettingEditor(choices, selectedValue, requestRender, async (selected) => {
13813
+ const effective2 = await writeConfig(setting.id, selected === "default" ? undefined : selected);
13814
+ item.currentValue = selected;
13815
+ item.description = `${setting.description} Effective global value: ${String(effective2[setting.id])}.`;
13816
+ }, close);
13817
+ };
13818
+ return item;
13819
+ });
13820
+ }
13821
+ function complexSettingsCategories(ctx, requestRender, writeConfig = writeGlobalConfigValue) {
13822
+ return [
13823
+ {
13824
+ id: "models",
13825
+ label: "Global models",
13826
+ description: "Stage-specific model routing",
13827
+ currentValue: "3 settings",
13828
+ submenu: (_value, done) => new SettingsList(modelSettingsItems(ctx, requestRender, writeConfig), 7, getSettingsListTheme(), () => {}, done)
13829
+ },
13830
+ {
13831
+ id: "limits",
13832
+ label: "Global limits & performance",
13833
+ description: "Context threshold, call budgets, and timeouts",
13834
+ currentValue: "6 settings",
13835
+ submenu: (_value, done) => inputSettingsList(LIMIT_SETTINGS, requestRender, done, writeConfig)
13836
+ },
13837
+ {
13838
+ id: "paths",
13839
+ label: "Global paths",
13840
+ description: "Backup directory and pinned summary paths",
13841
+ currentValue: "2 settings",
13842
+ submenu: (_value, done) => inputSettingsList(PATH_SETTINGS, requestRender, done, writeConfig)
13843
+ },
13844
+ {
13845
+ id: "profiles",
13846
+ label: "Global profile budgets",
13847
+ description: "Advanced token-budget tuning for each profile",
13848
+ currentValue: "3 profiles",
13849
+ submenu: (_value, done) => {
13850
+ const profiles = PROFILE_NAMES.map((profile) => ({
13851
+ id: profile,
13852
+ label: profile,
13853
+ description: `Six token-budget settings for the ${profile} profile.`,
13854
+ currentValue: "6 settings",
13855
+ submenu: (_current, close) => inputSettingsList(profileSettings(profile), requestRender, close, writeConfig)
13856
+ }));
13857
+ return new SettingsList(profiles, 7, getSettingsListTheme(), () => {}, done);
13858
+ }
13859
+ }
13860
+ ];
13861
+ }
13862
+
13863
+ // src/ui/settings-overlay.ts
13864
+ function enabled(value) {
13865
+ return value ? "enabled" : "disabled";
13866
+ }
13867
+
13868
+ class GlobalSettingsCoordinator {
13869
+ writer;
13870
+ queue = Promise.resolve();
13871
+ confirmed = new Map;
13872
+ confirmedConfig;
13873
+ pending = new Map;
13874
+ listeners = new Map;
13875
+ constructor(writer = updateGlobalChoiceSetting) {
13876
+ this.writer = writer;
13877
+ }
13878
+ display(id) {
13879
+ return this.pending.get(id)?.display ?? choiceDisplay(readGlobalConfigValue(id));
13880
+ }
13881
+ subscribe(ids, listener) {
13882
+ const registrations = [];
13883
+ for (const id of ids) {
13884
+ const callbacks = this.listeners.get(id) ?? new Set;
13885
+ const callback = (state) => listener(id, state);
13886
+ callbacks.add(callback);
13887
+ this.listeners.set(id, callbacks);
13888
+ registrations.push([id, callback]);
13889
+ }
13890
+ return () => {
13891
+ for (const [id, callback] of registrations) {
13892
+ const callbacks = this.listeners.get(id);
13893
+ callbacks?.delete(callback);
13894
+ if (callbacks?.size === 0)
13895
+ this.listeners.delete(id);
13896
+ }
13897
+ };
13898
+ }
13899
+ submit(group, id, display, onError, onApplied = () => {}) {
13900
+ if (this.pending.size === 0)
13901
+ this.confirmedConfig = loadConfig();
13902
+ if (!this.pending.has(id)) {
13903
+ this.confirmed.set(id, choiceDisplay(readGlobalConfigValue(id)));
13904
+ }
13905
+ const revision = (this.pending.get(id)?.revision ?? 0) + 1;
13906
+ this.pending.set(id, { revision, display });
13907
+ this.queue = this.queue.then(async () => {
13908
+ try {
13909
+ const config = await this.writer(group, id, display);
13910
+ await onApplied(id, config);
13911
+ this.confirmed.set(id, display);
13912
+ this.confirmedConfig = config;
13913
+ if (this.pending.get(id)?.revision === revision) {
13914
+ this.pending.delete(id);
13915
+ this.emit(id, { display, config });
13916
+ }
13917
+ } catch (error2) {
13918
+ onError(error2 instanceof Error ? error2.message : String(error2));
13919
+ if (this.pending.get(id)?.revision === revision) {
13920
+ this.pending.delete(id);
13921
+ this.emit(id, {
13922
+ display: this.confirmed.get(id) ?? "default",
13923
+ config: this.confirmedConfig
13924
+ });
13925
+ }
13926
+ }
13927
+ });
13928
+ }
13929
+ settled() {
13930
+ return this.queue;
13931
+ }
13932
+ emit(id, state) {
13933
+ for (const listener of this.listeners.get(id) ?? [])
13934
+ listener(state);
13935
+ }
13936
+ }
13937
+ var BOOLEAN_VALUES = [true, false];
13938
+ var THINKING_VALUES = [
13939
+ null,
13940
+ "minimal",
13941
+ "low",
13942
+ "medium",
13943
+ "high",
13944
+ "xhigh",
13945
+ "max"
13946
+ ];
13947
+ var GLOBAL_CHOICE_SETTINGS = {
13948
+ behavior: [
13949
+ {
13950
+ id: "mode",
13951
+ label: "Compaction mode",
13952
+ description: "Default compaction strategy.",
13953
+ values: ["auto", "fast", "balanced", "thorough"]
13954
+ },
13955
+ {
13956
+ id: "profile",
13957
+ label: "Compression profile",
13958
+ description: "Legacy detail-budget profile.",
13959
+ values: ["light", "balanced", "aggressive"]
13960
+ },
13961
+ {
13962
+ id: "agentToolAccess",
13963
+ label: "Agent tool access",
13964
+ description: "Global default for agent-visible smart_compact access.",
13965
+ values: ["inherit", "enabled", "disabled"]
13966
+ },
13967
+ {
13968
+ id: "autoTrigger",
13969
+ label: "Automatic compaction",
13970
+ description: "Global default for pressure-triggered compaction.",
13971
+ values: BOOLEAN_VALUES
13972
+ },
13973
+ {
13974
+ id: "showStatus",
13975
+ label: "Footer status",
13976
+ description: "Global default for the Smart Compact footer indicator.",
13977
+ values: BOOLEAN_VALUES
13978
+ },
13979
+ {
13980
+ id: "autoTriggerStrategy",
13981
+ label: "Trigger strategy",
13982
+ description: "Host-native hook or settled-turn triggering.",
13983
+ values: ["native-hook", "settled"]
13984
+ }
13985
+ ],
13986
+ reasoning: [
13987
+ {
13988
+ id: "summaryThinkingLevel",
13989
+ label: "Summary thinking",
13990
+ description: "Thinking level for synthesis and repair.",
13991
+ values: THINKING_VALUES
13992
+ },
13993
+ {
13994
+ id: "segmentationThinkingLevel",
13995
+ label: "Segmentation thinking",
13996
+ description: "Thinking level for transcript segmentation.",
13997
+ values: THINKING_VALUES
13998
+ }
13999
+ ],
14000
+ safety: [
14001
+ {
14002
+ id: "backupEnabled",
14003
+ label: "Backups",
14004
+ description: "Write a recovery backup before applying a compacted summary.",
14005
+ values: BOOLEAN_VALUES
14006
+ },
14007
+ {
14008
+ id: "requireApproval",
14009
+ label: "Require approval",
14010
+ description: "Ask before applying manual compaction output.",
14011
+ values: BOOLEAN_VALUES
14012
+ },
14013
+ {
14014
+ id: "scrubSecrets",
14015
+ label: "Scrub secrets",
14016
+ description: "Redact likely credentials before model calls and memory writes.",
14017
+ values: BOOLEAN_VALUES
14018
+ },
14019
+ {
14020
+ id: "scrubPii",
14021
+ label: "Scrub PII",
14022
+ description: "Redact email, phone, and payment-card shaped data.",
14023
+ values: BOOLEAN_VALUES
14024
+ },
14025
+ {
14026
+ id: "contextGraphEnabled",
14027
+ label: "Project memory",
14028
+ description: "Index project context and expose recall/save tools.",
14029
+ values: BOOLEAN_VALUES
14030
+ }
14031
+ ],
14032
+ advanced: [
14033
+ {
14034
+ id: "focusWeighting",
14035
+ label: "Focus weighting",
14036
+ description: "Steer synthesis toward the current task focus.",
14037
+ values: BOOLEAN_VALUES
14038
+ },
14039
+ {
14040
+ id: "zeroCallEnabled",
14041
+ label: "Zero-call fast path",
14042
+ description: "Allow deterministic compaction without an LLM call.",
14043
+ values: BOOLEAN_VALUES
14044
+ },
14045
+ {
14046
+ id: "telemetryChannel",
14047
+ label: "Telemetry channel",
14048
+ description: "Tag local metrics as stable or canary.",
14049
+ values: ["stable", "canary"]
14050
+ },
14051
+ {
14052
+ id: "adaptiveDamageFeedback",
14053
+ label: "Adaptive damage feedback",
14054
+ description: "Increase preservation budgets using prior damage signals.",
14055
+ values: BOOLEAN_VALUES
14056
+ },
14057
+ {
14058
+ id: "onlineDamageMonitor",
14059
+ label: "Online damage monitor",
14060
+ description: "Monitor confirmed compactions for preservation damage.",
14061
+ values: BOOLEAN_VALUES
14062
+ }
14063
+ ]
14064
+ };
14065
+ var GLOBAL_CHOICE_CATEGORIES = [
14066
+ {
14067
+ group: "behavior",
14068
+ label: "Global behavior",
14069
+ description: "Mode, profile, automation, and agent defaults"
14070
+ },
14071
+ {
14072
+ group: "reasoning",
14073
+ label: "Global reasoning",
14074
+ description: "Thinking-level defaults"
14075
+ },
14076
+ {
14077
+ group: "safety",
14078
+ label: "Global safety & storage",
14079
+ description: "Backups, approval, scrubbing, and project memory"
14080
+ },
14081
+ {
14082
+ group: "advanced",
14083
+ label: "Global advanced",
14084
+ description: "Focus, fast path, telemetry, and damage monitoring"
14085
+ }
14086
+ ];
14087
+ function choiceDisplay(value) {
14088
+ if (value === undefined)
14089
+ return "default";
14090
+ if (value === null)
14091
+ return "provider default";
14092
+ if (typeof value === "boolean")
14093
+ return enabled(value);
14094
+ return String(value);
14095
+ }
14096
+ function choiceValue(setting, display) {
14097
+ if (display === "default")
14098
+ return;
14099
+ const value = setting.values.find((candidate) => choiceDisplay(candidate) === display);
14100
+ if (value === undefined) {
14101
+ throw new Error(`Invalid value for ${setting.id}: ${display}`);
14102
+ }
14103
+ return value;
14104
+ }
14105
+ function effectiveChoiceDescription(setting, config) {
14106
+ const effective = config[setting.id];
14107
+ return `${setting.description} Effective global value: ${choiceDisplay(effective)}.`;
14108
+ }
14109
+ function globalChoiceSettingsItems(group, config, coordinator) {
14110
+ return GLOBAL_CHOICE_SETTINGS[group].map((setting) => ({
14111
+ id: setting.id,
14112
+ label: setting.label,
14113
+ description: effectiveChoiceDescription(setting, config),
14114
+ currentValue: coordinator?.display(setting.id) ?? choiceDisplay(readGlobalConfigValue(setting.id)),
14115
+ values: ["default", ...setting.values.map(choiceDisplay)]
14116
+ }));
14117
+ }
14118
+ async function updateGlobalChoiceSetting(group, id, display) {
14119
+ const setting = GLOBAL_CHOICE_SETTINGS[group].find((candidate) => candidate.id === id);
14120
+ if (!setting)
14121
+ throw new Error(`Unknown global choice setting: ${id}`);
14122
+ return writeGlobalConfigValue(setting.id, choiceValue(setting, display));
14123
+ }
14124
+ function effectiveDescription(field, policy) {
14125
+ return field === "agentToolAccess" ? `Effective tool state: ${enabled(policy.agentToolEnabled)}` : `Effective value: ${enabled(policy[field])}`;
14126
+ }
14127
+ function sessionSettingsItems(policy) {
14128
+ const current = policy.snapshot();
14129
+ const overrides = policy.branchOverrides();
13255
14130
  return [
13256
14131
  {
13257
14132
  id: "agentToolAccess",
13258
14133
  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]
14134
+ description: effectiveDescription("agentToolAccess", current),
14135
+ currentValue: overrides.agentToolAccess ?? "global",
14136
+ values: ["global", "inherit", "enabled", "disabled"]
13262
14137
  },
13263
14138
  {
13264
14139
  id: "autoTrigger",
13265
14140
  label: "Automatic compaction",
13266
- description: "Run Smart Compact from Pi's automatic compaction lifecycle",
13267
- currentValue: policy.autoTrigger ? ENABLED : DISABLED,
13268
- values: [ENABLED, DISABLED]
14141
+ description: effectiveDescription("autoTrigger", current),
14142
+ currentValue: overrides.autoTrigger === undefined ? "global" : enabled(overrides.autoTrigger),
14143
+ values: ["global", "enabled", "disabled"]
13269
14144
  },
13270
14145
  {
13271
14146
  id: "showStatus",
13272
14147
  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]
14148
+ description: effectiveDescription("showStatus", current),
14149
+ currentValue: overrides.showStatus === undefined ? "global" : enabled(overrides.showStatus),
14150
+ values: ["global", "enabled", "disabled"]
13276
14151
  }
13277
14152
  ];
13278
14153
  }
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);
14154
+ function policyField(id) {
14155
+ switch (id) {
14156
+ case "agentToolAccess":
14157
+ case "autoTrigger":
14158
+ case "showStatus":
14159
+ return id;
14160
+ default:
14161
+ throw new Error(`Unknown session setting: ${id}`);
14162
+ }
14163
+ }
14164
+ function updateSessionSetting(policy, ctx, id, value) {
14165
+ const field = policyField(id);
14166
+ if (value === "global")
14167
+ return policy.reset(field, ctx);
14168
+ switch (field) {
14169
+ case "agentToolAccess":
14170
+ if (value !== "inherit" && value !== "enabled" && value !== "disabled") {
14171
+ throw new Error(`Invalid agent access value: ${value}`);
14172
+ }
14173
+ return policy.update({ agentToolAccess: value }, ctx);
14174
+ case "autoTrigger":
14175
+ return policy.update({ autoTrigger: value === "enabled" }, ctx);
14176
+ case "showStatus":
14177
+ return policy.update({ showStatus: value === "enabled" }, ctx);
14178
+ }
14179
+ }
14180
+ function displayValue(field, policy) {
14181
+ const override = policy.branchOverrides()[field];
14182
+ if (override === undefined)
14183
+ return "global";
14184
+ if (typeof override === "boolean")
14185
+ return enabled(override);
14186
+ return override;
14187
+ }
14188
+ function sessionSettingsList(policy, ctx, done) {
14189
+ const items = sessionSettingsItems(policy);
14190
+ let list;
14191
+ list = new SettingsList2(items, 7, getSettingsListTheme2(), (id, value) => {
14192
+ try {
14193
+ const result = updateSessionSetting(policy, ctx, id, value);
14194
+ const field = policyField(id);
14195
+ const item = items.find((candidate) => candidate.id === id);
14196
+ if (item)
14197
+ item.description = effectiveDescription(field, result.policy);
13289
14198
  if (!result.ok) {
13290
- list.updateValue(id, previousValue(id, previous));
13291
14199
  ctx.ui.notify(result.error, "error");
14200
+ list.updateValue(id, displayValue(field, policy));
13292
14201
  }
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();
14202
+ } catch (error2) {
14203
+ ctx.ui.notify(error2 instanceof Error ? error2.message : String(error2), "error");
14204
+ list.updateValue(id, displayValue(policyField(id), policy));
14205
+ }
14206
+ }, done);
14207
+ return list;
14208
+ }
14209
+ function globalChoiceSettingsList(group, ctx, done, requestRender, coordinator, onApplied) {
14210
+ const settings = GLOBAL_CHOICE_SETTINGS[group];
14211
+ const items = globalChoiceSettingsItems(group, loadConfig(), coordinator);
14212
+ let list;
14213
+ list = new SettingsList2(items, 9, getSettingsListTheme2(), (id, display) => {
14214
+ coordinator.submit(group, id, display, (message) => ctx.ui.notify(message, "error"), onApplied);
14215
+ }, () => {
14216
+ unsubscribe();
14217
+ done();
14218
+ });
14219
+ const unsubscribe = coordinator.subscribe(settings.map((setting) => setting.id), (id, state) => {
14220
+ list.updateValue(id, state.display);
14221
+ if (state.config) {
14222
+ for (const setting of settings) {
14223
+ const item = items.find((candidate) => candidate.id === setting.id);
14224
+ if (item) {
14225
+ item.description = effectiveChoiceDescription(setting, state.config);
14226
+ }
13303
14227
  }
13304
- };
14228
+ }
14229
+ requestRender();
14230
+ });
14231
+ return list;
14232
+ }
14233
+ function settingsCategoryItems(policy, ctx, requestRender = () => {}, coordinator = new GlobalSettingsCoordinator, onApplied = () => {}, writeConfig = writeGlobalConfigValue) {
14234
+ return [
14235
+ {
14236
+ id: "session",
14237
+ label: "Current branch",
14238
+ description: "Agent access, automatic compaction, and footer status",
14239
+ currentValue: "3 settings",
14240
+ submenu: (_current, done) => sessionSettingsList(policy, ctx, done)
14241
+ },
14242
+ ...GLOBAL_CHOICE_CATEGORIES.map(({ group, label, description }) => ({
14243
+ id: group,
14244
+ label,
14245
+ description,
14246
+ currentValue: `${GLOBAL_CHOICE_SETTINGS[group].length} settings`,
14247
+ submenu: (_current, done) => globalChoiceSettingsList(group, ctx, done, requestRender, coordinator, onApplied)
14248
+ })),
14249
+ ...complexSettingsCategories(ctx, requestRender, writeConfig)
14250
+ ];
14251
+ }
14252
+ function createSettingsRoot(policy, ctx, onCancel, requestRender = () => {}, coordinator = new GlobalSettingsCoordinator, onApplied = () => {}, writeConfig = writeGlobalConfigValue) {
14253
+ return new SettingsList2(settingsCategoryItems(policy, ctx, requestRender, coordinator, onApplied, writeConfig), 8, getSettingsListTheme2(), () => {}, onCancel);
14254
+ }
14255
+ function deepestFocusable(component) {
14256
+ let current = component;
14257
+ let focusable;
14258
+ while (current) {
14259
+ if (typeof current.focused === "boolean") {
14260
+ focusable = current;
14261
+ }
14262
+ current = current.submenuComponent;
14263
+ }
14264
+ return focusable;
14265
+ }
14266
+ function createSettingsController(root, display, requestRender) {
14267
+ let focused = false;
14268
+ let target;
14269
+ const syncFocus = () => {
14270
+ const next = focused ? deepestFocusable(root) : undefined;
14271
+ if (target !== next) {
14272
+ if (target)
14273
+ target.focused = false;
14274
+ target = next;
14275
+ }
14276
+ if (target)
14277
+ target.focused = focused;
14278
+ };
14279
+ return {
14280
+ get focused() {
14281
+ return focused;
14282
+ },
14283
+ set focused(value) {
14284
+ focused = value;
14285
+ syncFocus();
14286
+ },
14287
+ render(width) {
14288
+ syncFocus();
14289
+ return display.render(width);
14290
+ },
14291
+ handleInput(data) {
14292
+ root.handleInput(data);
14293
+ syncFocus();
14294
+ requestRender();
14295
+ },
14296
+ invalidate: () => display.invalidate()
14297
+ };
14298
+ }
14299
+ async function showSmartCompactSettings(ctx, policy, coordinator = new GlobalSettingsCoordinator, onApplied = () => {}) {
14300
+ const writeConfig = async (path16, value) => {
14301
+ const config = await writeGlobalConfigValue(path16, value);
14302
+ await onApplied(path16, config);
14303
+ return config;
14304
+ };
14305
+ await ctx.ui.custom((tui, theme, _keybindings, done) => {
14306
+ const root = createSettingsRoot(policy, ctx, done, () => tui.requestRender(), coordinator, onApplied, writeConfig);
14307
+ const container = new Container4;
14308
+ container.addChild(new Text4(theme.fg("accent", theme.bold("Smart Compact Settings")), 0, 0));
14309
+ container.addChild(new Text4(theme.fg("dim", "Global defaults and branch-specific overrides."), 0, 0));
14310
+ container.addChild(new Text4(theme.fg("dim", "Manual /smart-compact stays available in every mode."), 0, 0));
14311
+ container.addChild(new Text4("", 0, 0));
14312
+ container.addChild(root);
14313
+ container.addChild(new Text4("", 0, 0));
14314
+ container.addChild(new Text4(theme.fg("dim", "\u2191\u2193 navigate \xB7 enter open/change \xB7 esc back/close"), 0, 0));
14315
+ return createSettingsController(root, container, () => tui.requestRender());
13305
14316
  });
13306
14317
  }
13307
14318
 
@@ -13474,6 +14485,7 @@ async function runInteractiveCompaction(ctx, config, dependencies) {
13474
14485
  });
13475
14486
  }
13476
14487
  function registerSmartCompactCommand(pi, dependencies) {
14488
+ const settingsCoordinator = new GlobalSettingsCoordinator;
13477
14489
  pi.registerCommand("smart-compact", {
13478
14490
  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
14491
  getArgumentCompletions(prefix) {
@@ -13526,7 +14538,7 @@ function registerSmartCompactCommand(pi, dependencies) {
13526
14538
  ctx.ui.notify("Smart Compact settings require TUI mode. Use settings.json for permanent defaults.", "warning");
13527
14539
  return;
13528
14540
  }
13529
- await showSmartCompactSettings(ctx, dependencies.policy);
14541
+ await showSmartCompactSettings(ctx, dependencies.policy, settingsCoordinator, (path16) => dependencies.onGlobalSettingApplied?.(path16, ctx));
13530
14542
  return;
13531
14543
  }
13532
14544
  const config = loadConfig();
@@ -13573,13 +14585,34 @@ function registerSmartCompactCommand(pi, dependencies) {
13573
14585
  // src/app/smart-compact-policy.ts
13574
14586
  var SMART_COMPACT_TOOL_NAME = "smart_compact";
13575
14587
  var SMART_COMPACT_POLICY_ENTRY = "smart-compact-policy";
13576
- var POLICY_VERSION = 2;
14588
+ var POLICY_VERSION = 3;
13577
14589
  var STATUS_KEY = "smart-compact-policy";
13578
14590
  function persistedPolicy(value) {
13579
14591
  if (typeof value !== "object" || value === null)
13580
14592
  return null;
13581
14593
  const candidate = value;
13582
- if (candidate.version === POLICY_VERSION && (candidate.agentToolAccess === "inherit" || candidate.agentToolAccess === "enabled" || candidate.agentToolAccess === "disabled") && typeof candidate.autoTrigger === "boolean") {
14594
+ if (candidate.version === POLICY_VERSION && typeof candidate.overrides === "object" && candidate.overrides !== null && !Array.isArray(candidate.overrides)) {
14595
+ const values = candidate.overrides;
14596
+ const overrides = {};
14597
+ if (values.agentToolAccess !== undefined) {
14598
+ if (values.agentToolAccess !== "inherit" && values.agentToolAccess !== "enabled" && values.agentToolAccess !== "disabled") {
14599
+ return null;
14600
+ }
14601
+ overrides.agentToolAccess = values.agentToolAccess;
14602
+ }
14603
+ if (values.autoTrigger !== undefined) {
14604
+ if (typeof values.autoTrigger !== "boolean")
14605
+ return null;
14606
+ overrides.autoTrigger = values.autoTrigger;
14607
+ }
14608
+ if (values.showStatus !== undefined) {
14609
+ if (typeof values.showStatus !== "boolean")
14610
+ return null;
14611
+ overrides.showStatus = values.showStatus;
14612
+ }
14613
+ return overrides;
14614
+ }
14615
+ if (candidate.version === 2 && (candidate.agentToolAccess === "inherit" || candidate.agentToolAccess === "enabled" || candidate.agentToolAccess === "disabled") && typeof candidate.autoTrigger === "boolean") {
13583
14616
  const desired = {
13584
14617
  agentToolAccess: candidate.agentToolAccess,
13585
14618
  autoTrigger: candidate.autoTrigger,
@@ -13618,13 +14651,18 @@ function statusText(policy) {
13618
14651
  return "smart-compact: auto off";
13619
14652
  }
13620
14653
  function createSmartCompactPolicy(pi) {
13621
- let current = configDefaults();
14654
+ let overrides = {};
14655
+ const desired = () => ({
14656
+ ...configDefaults(),
14657
+ ...overrides
14658
+ });
13622
14659
  const effectiveToolState = () => pi.getActiveTools().includes(SMART_COMPACT_TOOL_NAME);
13623
14660
  const snapshot = () => ({
13624
- ...current,
14661
+ ...desired(),
13625
14662
  agentToolEnabled: effectiveToolState()
13626
14663
  });
13627
14664
  const apply = (ctx) => {
14665
+ const current = desired();
13628
14666
  const active = pi.getActiveTools();
13629
14667
  const hasTool = active.includes(SMART_COMPACT_TOOL_NAME);
13630
14668
  if (current.agentToolAccess === "enabled" && !hasTool) {
@@ -13636,45 +14674,64 @@ function createSmartCompactPolicy(pi) {
13636
14674
  ctx.ui.setStatus(STATUS_KEY, current.showStatus ? statusText(effective) : undefined);
13637
14675
  return effective;
13638
14676
  };
14677
+ const restoreToolMembership = (enabled2) => {
14678
+ const active = pi.getActiveTools();
14679
+ const hasTool = active.includes(SMART_COMPACT_TOOL_NAME);
14680
+ if (enabled2 && !hasTool) {
14681
+ pi.setActiveTools([...new Set([...active, SMART_COMPACT_TOOL_NAME])]);
14682
+ } else if (!enabled2 && hasTool) {
14683
+ pi.setActiveTools(active.filter((name) => name !== SMART_COMPACT_TOOL_NAME));
14684
+ }
14685
+ };
14686
+ const persist = (next, ctx) => {
14687
+ const previous = overrides;
14688
+ const previousToolEnabled = effectiveToolState();
14689
+ overrides = next;
14690
+ try {
14691
+ const effective = apply(ctx);
14692
+ pi.appendEntry(SMART_COMPACT_POLICY_ENTRY, { version: POLICY_VERSION, overrides: { ...overrides } });
14693
+ return { ok: true, policy: effective };
14694
+ } catch (error2) {
14695
+ debugError("Smart Compact policy update failed", error2);
14696
+ overrides = previous;
14697
+ try {
14698
+ restoreToolMembership(previousToolEnabled);
14699
+ } catch (rollbackError) {
14700
+ debugError("Smart Compact policy rollback failed", rollbackError);
14701
+ }
14702
+ const rolledBack = snapshot();
14703
+ const previousDesired = desired();
14704
+ ctx.ui.setStatus(STATUS_KEY, previousDesired.showStatus ? statusText(rolledBack) : undefined);
14705
+ return {
14706
+ ok: false,
14707
+ policy: rolledBack,
14708
+ error: "Smart Compact settings could not be saved; the previous policy was restored."
14709
+ };
14710
+ }
14711
+ };
13639
14712
  return {
13640
14713
  snapshot,
14714
+ branchOverrides: () => ({ ...overrides }),
13641
14715
  isAgentToolEnabled: effectiveToolState,
13642
- isAutoTriggerEnabled: () => current.autoTrigger,
14716
+ isAutoTriggerEnabled: () => desired().autoTrigger,
13643
14717
  restore(ctx) {
13644
- current = configDefaults();
14718
+ overrides = {};
13645
14719
  for (const entry of ctx.sessionManager.getBranch()) {
13646
14720
  if (entry.type === "custom" && entry.customType === SMART_COMPACT_POLICY_ENTRY) {
13647
14721
  const restored = persistedPolicy(entry.data);
13648
14722
  if (restored)
13649
- current = { ...current, ...restored };
14723
+ overrides = restored;
13650
14724
  }
13651
14725
  }
13652
14726
  apply(ctx);
13653
14727
  },
13654
14728
  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
- }
14729
+ return persist({ ...overrides, ...patch }, ctx);
14730
+ },
14731
+ reset(field, ctx) {
14732
+ const next = { ...overrides };
14733
+ delete next[field];
14734
+ return persist(next, ctx);
13678
14735
  }
13679
14736
  };
13680
14737
  }
@@ -13747,12 +14804,19 @@ function smartCompactExtension(pi) {
13747
14804
  return false;
13748
14805
  }
13749
14806
  };
13750
- registerContextTools(pi);
14807
+ const contextToolAvailability = registerContextTools(pi);
13751
14808
  registerSmartCompactCommand(pi, {
13752
14809
  pendingRef,
13753
14810
  runLock: isRunning,
13754
14811
  onNativeApplyError,
13755
- policy
14812
+ policy,
14813
+ onGlobalSettingApplied(path16, ctx) {
14814
+ try {
14815
+ applyGlobalSettingRuntime(path16, ctx, policy, contextToolAvailability);
14816
+ } catch (error2) {
14817
+ debugError("Smart Compact runtime settings refresh failed", error2);
14818
+ }
14819
+ }
13756
14820
  });
13757
14821
  pi.on("session_start", (_event, ctx) => {
13758
14822
  policy.restore(ctx);