pi-smart-compact 9.4.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.4.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",
@@ -59,6 +85,7 @@ var DEFAULT_CONFIG = {
59
85
  segmentationThinkingLevel: "minimal",
60
86
  agentToolAccess: "inherit",
61
87
  autoTrigger: true,
88
+ showStatus: true,
62
89
  autoTriggerStrategy: "native-hook",
63
90
  autoTriggerTimeoutMs: 120000,
64
91
  backupEnabled: true,
@@ -314,102 +341,283 @@ var EXPLORER_SYSTEM_PROMPT = `You are a conversation analyst. You have determini
314
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":[...]}';
315
342
 
316
343
  // src/utils/config.ts
344
+ import fs2 from "fs";
345
+
346
+ // src/infra/fs.ts
317
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
+ }
318
549
 
319
550
  // src/infra/paths.ts
320
- import path from "path";
551
+ import path2 from "path";
321
552
  import os from "os";
322
553
  function home() {
323
554
  const configured = process.env.HOME?.trim() || process.env.USERPROFILE?.trim();
324
555
  return configured || os.homedir();
325
556
  }
326
557
  function piAgentDir() {
327
- return path.join(home(), ".pi", "agent");
558
+ return path2.join(home(), ".pi", "agent");
328
559
  }
329
560
  function cacheDir() {
330
- return path.join(piAgentDir(), ".cache");
561
+ return path2.join(piAgentDir(), ".cache");
331
562
  }
332
563
  function smartCompactCacheDir() {
333
- return path.join(cacheDir(), "smart-compact");
564
+ return path2.join(cacheDir(), "smart-compact");
334
565
  }
335
566
  function projectFingerprintDir() {
336
- return path.join(smartCompactCacheDir(), "projects");
567
+ return path2.join(smartCompactCacheDir(), "projects");
337
568
  }
338
569
  function compactionStateDir() {
339
- return path.join(smartCompactCacheDir(), "states");
570
+ return path2.join(smartCompactCacheDir(), "states");
340
571
  }
341
572
  function sessionsDir() {
342
- return path.join(piAgentDir(), "sessions");
573
+ return path2.join(piAgentDir(), "sessions");
343
574
  }
344
575
  function settingsFile() {
345
- return path.join(piAgentDir(), "settings.json");
576
+ return path2.join(piAgentDir(), "settings.json");
346
577
  }
347
578
  function defaultBackupDir() {
348
- return path.join(piAgentDir(), "compact-backups");
579
+ return path2.join(piAgentDir(), "compact-backups");
349
580
  }
350
581
  function metricsLogFile() {
351
- return path.join(cacheDir(), "compact-metrics.jsonl");
582
+ return path2.join(cacheDir(), "compact-metrics.jsonl");
352
583
  }
353
584
  function runLocksDir() {
354
- return path.join(smartCompactCacheDir(), "run-locks");
585
+ return path2.join(smartCompactCacheDir(), "run-locks");
355
586
  }
356
587
  function nativeContinuityDir() {
357
- return path.join(smartCompactCacheDir(), "native-continuity");
588
+ return path2.join(smartCompactCacheDir(), "native-continuity");
358
589
  }
359
590
  function contextGraphFile() {
360
- return path.join(smartCompactCacheDir(), "context-graph.sqlite");
591
+ return path2.join(smartCompactCacheDir(), "context-graph.sqlite");
361
592
  }
362
593
  function damageReportsFile() {
363
- return path.join(smartCompactCacheDir(), "damage-reports.jsonl");
594
+ return path2.join(smartCompactCacheDir(), "damage-reports.jsonl");
364
595
  }
365
596
  function extractionCacheFile(sessionId) {
366
- 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");
367
598
  }
368
599
  function projectFingerprintFile(projectId) {
369
- return path.join(projectFingerprintDir(), projectId + ".json");
600
+ return path2.join(projectFingerprintDir(), projectId + ".json");
370
601
  }
371
602
  function compactionStateFile(projectId) {
372
- 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");
373
604
  }
374
605
  function legacyScopedCompactionStateFile(projectId, sessionId) {
375
606
  const project = projectId.replace(/[^a-zA-Z0-9_-]/g, "_");
376
607
  const session = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
377
- return path.join(compactionStateDir(), project, session + ".json");
608
+ return path2.join(compactionStateDir(), project, session + ".json");
378
609
  }
379
610
  function scopedCompactionStateFile(projectId, sessionId, branchHeadId) {
380
611
  const project = projectId.replace(/[^a-zA-Z0-9_-]/g, "_");
381
612
  const session = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
382
613
  const branch = branchHeadId.replace(/[^a-zA-Z0-9_-]/g, "_");
383
- return path.join(compactionStateDir(), project, session, branch + ".json");
614
+ return path2.join(compactionStateDir(), project, session, branch + ".json");
384
615
  }
385
616
  function remediationHintsFile(projectId) {
386
- return path.join(smartCompactCacheDir(), "remediation-" + projectId + ".json");
617
+ return path2.join(smartCompactCacheDir(), "remediation-" + projectId + ".json");
387
618
  }
388
619
  function metricsDashboardFile() {
389
- return path.join(cacheDir(), "smart-compact-report.html");
390
- }
391
-
392
- // src/utils/logger.ts
393
- var DEBUG = process.env.DEBUG?.includes("smart-compact") ?? false;
394
- function warn(msg, err) {
395
- const detail = err instanceof Error ? err.message : err ?? "";
396
- console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
397
- }
398
- function error(msg, err) {
399
- const detail = err instanceof Error ? err.message + `
400
- ` + err.stack : err ?? "";
401
- console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
402
- }
403
- function info(msg, ...args) {
404
- console.error(LOG_PREFIX + " [info] " + msg, ...args);
405
- }
406
- function debug(msg, ...args) {
407
- if (DEBUG)
408
- console.error(LOG_PREFIX + " [debug] " + msg, ...args);
409
- }
410
- function debugError(msg, err) {
411
- if (DEBUG)
412
- error(msg, err);
620
+ return path2.join(cacheDir(), "smart-compact-report.html");
413
621
  }
414
622
 
415
623
  // src/utils/config.ts
@@ -427,6 +635,7 @@ var VALID_THINKING_LEVELS = [
427
635
  ];
428
636
  var BOOLEAN_KEYS = [
429
637
  "autoTrigger",
638
+ "showStatus",
430
639
  "backupEnabled",
431
640
  "requireApproval",
432
641
  "scrubSecrets",
@@ -454,14 +663,116 @@ var PROFILE_NUMERIC_KEYS = [
454
663
  "singlePassMaxTokens",
455
664
  "batchMaxTokens"
456
665
  ];
457
- var PROFILE_NUMERIC_BOUNDS = {
458
- summaryBudgetTokens: [256, 1e5],
459
- keepRecentTokens: [1000, 500000],
460
- minChunkTokens: [100, 1e5],
461
- maxChunkTokens: [500, 200000],
462
- singlePassMaxTokens: [1000, 500000],
463
- batchMaxTokens: [1000, 500000]
464
- };
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
+ }
465
776
  function discard(sc, key, message) {
466
777
  warn(message);
467
778
  delete sc[key];
@@ -511,7 +822,7 @@ function validateBasicFields(sc) {
511
822
  function validateProfiles(sc) {
512
823
  if (!("profiles" in sc))
513
824
  return;
514
- if (typeof sc.profiles !== "object" || sc.profiles === null || Array.isArray(sc.profiles)) {
825
+ if (!isRecord(sc.profiles)) {
515
826
  discard(sc, "profiles", "smart-compact config: profiles must be an object, got " + typeof sc.profiles);
516
827
  return;
517
828
  }
@@ -521,7 +832,7 @@ function validateProfiles(sc) {
521
832
  discard(profiles, profileName, "smart-compact config: ignoring unknown profile override '" + profileName + "'.");
522
833
  continue;
523
834
  }
524
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
835
+ if (!isRecord(value)) {
525
836
  discard(profiles, profileName, "smart-compact config: profile '" + profileName + "' must be an object.");
526
837
  continue;
527
838
  }
@@ -545,35 +856,39 @@ function validateProfiles(sc) {
545
856
  }
546
857
  }
547
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
+ }
548
863
  var NUMERIC_RULES = [
549
864
  {
550
865
  key: "autoTriggerTimeoutMs",
551
- valid: (value) => Number.isFinite(value) && value >= 1000 && value <= 300000,
866
+ valid: (value) => validNumericLimit("autoTriggerTimeoutMs", value),
552
867
  message: (value) => "smart-compact config: autoTriggerTimeoutMs must be 1000\u2013300000, got " + value + ". Using default " + DEFAULT_CONFIG.autoTriggerTimeoutMs + "ms."
553
868
  },
554
869
  {
555
870
  key: "maxLlmCalls",
556
- valid: (value) => Number.isInteger(value) && value >= 0 && value <= 100,
871
+ valid: (value) => validNumericLimit("maxLlmCalls", value),
557
872
  message: () => "smart-compact config: maxLlmCalls must be 0\u2013100; 0 uses the selected mode cap."
558
873
  },
559
874
  {
560
875
  key: "maxLlmInputTokens",
561
- valid: (value) => Number.isInteger(value) && value >= 0 && value <= 1e6,
876
+ valid: (value) => validNumericLimit("maxLlmInputTokens", value),
562
877
  message: () => "smart-compact config: maxLlmInputTokens must be 0\u20131000000; 0 uses the mode cap."
563
878
  },
564
879
  {
565
880
  key: "codexMaxCallMs",
566
- valid: (value) => Number.isInteger(value) && (value === 0 || value >= 5000 && value <= 300000),
881
+ valid: (value) => validNumericLimit("codexMaxCallMs", value),
567
882
  message: () => "smart-compact config: codexMaxCallMs must be 0 or 5000\u2013300000; 0 derives a cap from maxTokens."
568
883
  },
569
884
  {
570
885
  key: "maxLatencyMs",
571
- valid: (value) => Number.isFinite(value) && (value === 0 || value >= 5000 && value <= 600000),
886
+ valid: (value) => validNumericLimit("maxLatencyMs", value),
572
887
  message: () => "smart-compact config: maxLatencyMs must be 0 or 5000\u2013600000; 0 means unlimited."
573
888
  },
574
889
  {
575
890
  key: "minContextPercent",
576
- valid: (value) => Number.isFinite(value) && value >= 0 && value <= 100,
891
+ valid: (value) => validNumericLimit("minContextPercent", value),
577
892
  message: (value) => "smart-compact config: minContextPercent must be 0\u2013100, got " + value + ". Using default " + DEFAULT_CONFIG.minContextPercent + "."
578
893
  }
579
894
  ];
@@ -598,39 +913,78 @@ function validateSmartCompactConfig(sc) {
598
913
  validateProfiles(sc);
599
914
  validateLimits(sc);
600
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
+ }
601
941
  var cachedConfig = null;
602
942
  var cachedMtime = 0;
603
943
  var cachedPath = null;
944
+ function resetConfigCache() {
945
+ cachedConfig = null;
946
+ cachedMtime = 0;
947
+ cachedPath = null;
948
+ }
604
949
  function loadConfig() {
605
950
  try {
606
951
  const file = settingsFile();
607
- const stat = fs.statSync(file);
952
+ const stat = fs2.statSync(file);
608
953
  if (cachedConfig && cachedPath === file && stat.mtimeMs === cachedMtime)
609
- return cachedConfig;
610
- const raw = JSON.parse(fs.readFileSync(file, "utf-8"));
611
- 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
+ }
612
965
  validateSmartCompactConfig(sc);
613
- const merged = { ...DEFAULT_CONFIG, ...sc };
966
+ const merged = { ...defaultConfig(), ...sc };
614
967
  if (!("mode" in sc) && "profile" in sc) {
615
968
  merged.mode = sc.profile === "light" ? "thorough" : sc.profile;
616
969
  }
617
970
  if (sc.profiles) {
618
- 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
+ ]));
619
976
  }
620
977
  if (!merged.backupDir)
621
978
  merged.backupDir = defaultBackupDir();
622
979
  cachedConfig = merged;
623
980
  cachedMtime = stat.mtimeMs;
624
981
  cachedPath = file;
625
- return cachedConfig;
982
+ return cloneConfig(cachedConfig);
626
983
  } catch (error2) {
627
984
  debug("loadConfig: settings.json not found or unreadable, using defaults", error2);
628
- cachedConfig = {
629
- ...DEFAULT_CONFIG,
630
- backupDir: defaultBackupDir()
631
- };
985
+ cachedConfig = defaultConfig();
632
986
  cachedPath = null;
633
- return cachedConfig;
987
+ return cloneConfig(cachedConfig);
634
988
  }
635
989
  }
636
990
 
@@ -933,17 +1287,17 @@ import fs3 from "fs";
933
1287
  import path4 from "path";
934
1288
 
935
1289
  // src/utils/extraction.ts
936
- import path2 from "path";
1290
+ import path3 from "path";
937
1291
 
938
1292
  // src/utils/type-guards.ts
939
- function isRecord(value) {
1293
+ function isRecord2(value) {
940
1294
  return typeof value === "object" && value !== null;
941
1295
  }
942
1296
  function isTextBlock(c) {
943
- return isRecord(c) && c.type === "text" && typeof c.text === "string";
1297
+ return isRecord2(c) && c.type === "text" && typeof c.text === "string";
944
1298
  }
945
1299
  function isToolCallBlock(c) {
946
- 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);
947
1301
  }
948
1302
  function getToolCallNames(content) {
949
1303
  if (!Array.isArray(content))
@@ -1119,8 +1473,8 @@ function isKnownPathReference(ref, knownPaths) {
1119
1473
  if (!normalizedRef)
1120
1474
  return false;
1121
1475
  const pathShaped = normalizedRef.includes("/");
1122
- return knownPaths.some((path2) => {
1123
- const normalizedPath = normalizePath(path2).replace(/^\/+/, "");
1476
+ return knownPaths.some((path3) => {
1477
+ const normalizedPath = normalizePath(path3).replace(/^\/+/, "");
1124
1478
  if (normalizedPath === normalizedRef || normalizedPath.endsWith("/" + normalizedRef))
1125
1479
  return true;
1126
1480
  if (normalizedPath.endsWith(normalizedRef)) {
@@ -1471,15 +1825,15 @@ function smartKeepBoundaryCandidates(msgs, keepFromIndex, branchEntries) {
1471
1825
  }
1472
1826
  function collectToolCallIds(blocks, msgIndex, out) {
1473
1827
  for (const block of blocks) {
1474
- if (!isRecord(block) || block.type !== "toolCall")
1828
+ if (!isRecord2(block) || block.type !== "toolCall")
1475
1829
  continue;
1476
1830
  if (typeof block.id === "string")
1477
1831
  out.set(block.id, msgIndex);
1478
1832
  const args = block.arguments;
1479
- 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))
1480
1834
  continue;
1481
1835
  for (const nested of args.tool_uses) {
1482
- if (isRecord(nested) && typeof nested.id === "string")
1836
+ if (isRecord2(nested) && typeof nested.id === "string")
1483
1837
  out.set(nested.id, msgIndex);
1484
1838
  }
1485
1839
  }
@@ -1488,7 +1842,7 @@ function buildToolCallBoundaryIndex(msgs) {
1488
1842
  const map = new Map;
1489
1843
  for (let i = 0;i < msgs.length; i++) {
1490
1844
  const message = msgs[i].message;
1491
- if (!isRecord(message) || message.role !== "assistant")
1845
+ if (!isRecord2(message) || message.role !== "assistant")
1492
1846
  continue;
1493
1847
  const blocks = Array.isArray(message.content) ? message.content : [];
1494
1848
  collectToolCallIds(blocks, i, map);
@@ -1510,7 +1864,7 @@ function guardToolCallBoundary(msgs, keepFrom, tcMap = buildToolCallBoundaryInde
1510
1864
  changed = false;
1511
1865
  for (let i = adjusted;i < msgs.length; i++) {
1512
1866
  const message = msgs[i].message;
1513
- if (!isRecord(message) || message.role !== "toolResult")
1867
+ if (!isRecord2(message) || message.role !== "toolResult")
1514
1868
  continue;
1515
1869
  const tcId = typeof message.toolCallId === "string" ? message.toolCallId : undefined;
1516
1870
  if (!tcId)
@@ -1533,7 +1887,7 @@ function advancePastToolCallBoundary(msgs, keepFrom, tcMap = buildToolCallBounda
1533
1887
  let next = adjusted;
1534
1888
  for (let i = adjusted;i < msgs.length; i++) {
1535
1889
  const message = msgs[i].message;
1536
- if (!isRecord(message) || message.role !== "toolResult")
1890
+ if (!isRecord2(message) || message.role !== "toolResult")
1537
1891
  continue;
1538
1892
  const tcIdx = typeof message.toolCallId === "string" ? tcMap.get(message.toolCallId) : undefined;
1539
1893
  if (i === adjusted && tcIdx === undefined || tcIdx !== undefined && tcIdx < adjusted) {
@@ -1861,7 +2215,7 @@ function buildSummaryPathEvidence(paths, budgetTokens = PROFILES.balanced.summar
1861
2215
  const unique = Array.from(new Set(paths.filter(Boolean)));
1862
2216
  if (!unique.length)
1863
2217
  return new Map;
1864
- const full = unique.map((path2) => [path2, summaryPathLine(path2)]);
2218
+ const full = unique.map((path3) => [path3, summaryPathLine(path3)]);
1865
2219
  const minimumPerLine = JSON.stringify("#" + "x".repeat(12)).length + 3;
1866
2220
  const budgetChars = Math.max(unique.length * minimumPerLine, Math.min(20000, Math.max(4000, Math.floor(budgetTokens * 2))));
1867
2221
  if (full.reduce((total, [, line]) => total + line.length + 3, 0) <= budgetChars) {
@@ -1869,21 +2223,21 @@ function buildSummaryPathEvidence(paths, budgetTokens = PROFILES.balanced.summar
1869
2223
  }
1870
2224
  const digests = new Map;
1871
2225
  const owners = new Map;
1872
- for (const path2 of unique) {
1873
- const fullDigest = createHash("sha256").update(path2).digest("base64url");
2226
+ for (const path3 of unique) {
2227
+ const fullDigest = createHash("sha256").update(path3).digest("base64url");
1874
2228
  let digest = fullDigest.slice(0, 12);
1875
2229
  const owner = owners.get(digest);
1876
- if (owner && owner !== path2) {
2230
+ if (owner && owner !== path3) {
1877
2231
  digest = fullDigest;
1878
2232
  digests.set(owner, createHash("sha256").update(owner).digest("base64url"));
1879
2233
  }
1880
- owners.set(digest, path2);
1881
- digests.set(path2, digest);
2234
+ owners.set(digest, path3);
2235
+ digests.set(path3, digest);
1882
2236
  }
1883
2237
  const perPath = Math.max(JSON.stringify("#" + "x".repeat(12)).length, Math.floor((budgetChars - unique.length * 3) / unique.length));
1884
- return new Map(unique.map((path2) => [
1885
- path2,
1886
- compactPathLine(path2, perPath, digests.get(path2) ?? "")
2238
+ return new Map(unique.map((path3) => [
2239
+ path3,
2240
+ compactPathLine(path3, perPath, digests.get(path3) ?? "")
1887
2241
  ]));
1888
2242
  }
1889
2243
  function mergeBodies(first, second) {
@@ -2401,7 +2755,7 @@ function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
2401
2755
  const shiftBasename = (filePath) => {
2402
2756
  if (!filePath)
2403
2757
  return null;
2404
- const base = path2.basename(filePath);
2758
+ const base = path3.basename(filePath);
2405
2759
  return GENERIC_BASENAMES.has(base.toLowerCase()) ? null : base;
2406
2760
  };
2407
2761
  const topics = [];
@@ -2555,7 +2909,7 @@ function extractOpenLoops(msgs, extraction) {
2555
2909
  }));
2556
2910
  for (const err of extraction.errors.filter((e) => !e.resolved)) {
2557
2911
  const errLower = err.message.toLowerCase();
2558
- 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);
2559
2913
  loops.push({
2560
2914
  id: ID_PREFIX.OPEN_LOOP + ++loopId,
2561
2915
  type: "bugfix",
@@ -2704,191 +3058,6 @@ function extractStructured(msgs, pc, precomputedTcIdx) {
2704
3058
  };
2705
3059
  }
2706
3060
 
2707
- // src/infra/fs.ts
2708
- import fs2 from "fs";
2709
- import fsp from "fs/promises";
2710
- import path3 from "path";
2711
- import crypto from "crypto";
2712
- var LOCK_STALE_MS = 5000;
2713
- var LOCK_RETRY_MS = 25;
2714
- var LOCK_MAX_RETRIES = 80;
2715
- function ensureDir(dir) {
2716
- fs2.mkdirSync(dir, { recursive: true, mode: 448 });
2717
- fs2.chmodSync(dir, 448);
2718
- }
2719
- async function ensureDirAsync(dir) {
2720
- await fsp.mkdir(dir, { recursive: true, mode: 448 });
2721
- await fsp.chmod(dir, 448);
2722
- }
2723
- function tempPath(target) {
2724
- return target + ".tmp." + process.pid + "." + crypto.randomBytes(4).toString("hex");
2725
- }
2726
- function atomicWriteFileSync(target, data) {
2727
- ensureDir(path3.dirname(target));
2728
- const tmp = tempPath(target);
2729
- try {
2730
- fs2.writeFileSync(tmp, data, { mode: 384 });
2731
- fs2.renameSync(tmp, target);
2732
- fs2.chmodSync(target, 384);
2733
- } catch (e) {
2734
- try {
2735
- fs2.unlinkSync(tmp);
2736
- } catch {}
2737
- throw e;
2738
- }
2739
- }
2740
- async function atomicWriteFile(target, data) {
2741
- await ensureDirAsync(path3.dirname(target));
2742
- const tmp = tempPath(target);
2743
- try {
2744
- await fsp.writeFile(tmp, data, { mode: 384 });
2745
- await fsp.rename(tmp, target);
2746
- await fsp.chmod(target, 384);
2747
- } catch (e) {
2748
- try {
2749
- await fsp.unlink(tmp);
2750
- } catch {}
2751
- throw e;
2752
- }
2753
- }
2754
- function tryAcquireLock(target) {
2755
- const lockDir = target + ".lock";
2756
- for (let reclaimAttempt = 0;reclaimAttempt < 2; reclaimAttempt++) {
2757
- try {
2758
- fs2.mkdirSync(lockDir, { mode: 448 });
2759
- return () => {
2760
- try {
2761
- fs2.rmdirSync(lockDir);
2762
- } catch {}
2763
- };
2764
- } catch (error2) {
2765
- if (error2?.code !== "EEXIST") {
2766
- throw new Error("Failed to acquire lock for " + target, { cause: error2 });
2767
- }
2768
- try {
2769
- const stat = fs2.statSync(lockDir);
2770
- if (Date.now() - stat.mtimeMs <= LOCK_STALE_MS)
2771
- return null;
2772
- const stolen = lockDir + ".stale." + process.pid + "." + crypto.randomBytes(4).toString("hex");
2773
- fs2.renameSync(lockDir, stolen);
2774
- const stolenStat = fs2.statSync(stolen);
2775
- if (Date.now() - stolenStat.mtimeMs > LOCK_STALE_MS)
2776
- fs2.rmdirSync(stolen);
2777
- else
2778
- try {
2779
- fs2.renameSync(stolen, lockDir);
2780
- } catch {}
2781
- } catch {}
2782
- }
2783
- }
2784
- return null;
2785
- }
2786
- function acquireLockSync(target) {
2787
- const release = tryAcquireLock(target);
2788
- if (!release)
2789
- throw new Error("Lock busy for " + target);
2790
- return release;
2791
- }
2792
- async function acquireLock(target) {
2793
- for (let attempt = 0;attempt < LOCK_MAX_RETRIES; attempt++) {
2794
- const release = tryAcquireLock(target);
2795
- if (release)
2796
- return release;
2797
- const delay = Promise.withResolvers();
2798
- setTimeout(delay.resolve, LOCK_RETRY_MS);
2799
- await delay.promise;
2800
- }
2801
- throw new Error("Timed out acquiring lock for " + target);
2802
- }
2803
- async function appendLineLockedAsync(target, line, maxBytes) {
2804
- await ensureDirAsync(path3.dirname(target));
2805
- const payload = Buffer.from(line.endsWith(`
2806
- `) ? line : line + `
2807
- `);
2808
- if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)) {
2809
- throw new Error("maxBytes must be a positive safe integer");
2810
- }
2811
- if (maxBytes !== undefined && payload.length > maxBytes) {
2812
- throw new Error("Log entry exceeds retention cap for " + target);
2813
- }
2814
- const release = await acquireLock(target);
2815
- try {
2816
- let stat = null;
2817
- try {
2818
- stat = await fsp.stat(target);
2819
- } catch (error2) {
2820
- if (!error2 || typeof error2 !== "object" || !("code" in error2) || error2.code !== "ENOENT")
2821
- throw error2;
2822
- }
2823
- if (maxBytes !== undefined && stat && stat.size + payload.length > maxBytes) {
2824
- const retainedLength = Math.min(stat.size, Math.max(0, maxBytes - payload.length));
2825
- const buffer = Buffer.allocUnsafe(retainedLength);
2826
- if (retainedLength > 0) {
2827
- const handle = await fsp.open(target, "r");
2828
- try {
2829
- await handle.read(buffer, 0, retainedLength, stat.size - retainedLength);
2830
- } finally {
2831
- await handle.close();
2832
- }
2833
- }
2834
- let tail = buffer.toString("utf8");
2835
- if (retainedLength < stat.size) {
2836
- const firstNewline = tail.indexOf(`
2837
- `);
2838
- tail = firstNewline >= 0 ? tail.slice(firstNewline + 1) : "";
2839
- }
2840
- await atomicWriteFile(target, tail);
2841
- }
2842
- await fsp.appendFile(target, payload, { mode: 384 });
2843
- await fsp.chmod(target, 384);
2844
- } finally {
2845
- release();
2846
- }
2847
- }
2848
- function readJsonlTail(target, limit, maxBytes = 512 * 1024) {
2849
- if (limit <= 0 || !fs2.existsSync(target))
2850
- return [];
2851
- const stat = fs2.statSync(target);
2852
- const length = Math.min(stat.size, maxBytes);
2853
- const buffer = Buffer.alloc(length);
2854
- const fd = fs2.openSync(target, "r");
2855
- try {
2856
- fs2.readSync(fd, buffer, 0, length, stat.size - length);
2857
- } finally {
2858
- fs2.closeSync(fd);
2859
- }
2860
- let text = buffer.toString("utf8");
2861
- if (stat.size > length) {
2862
- const newline = text.indexOf(`
2863
- `);
2864
- text = newline >= 0 ? text.slice(newline + 1) : "";
2865
- }
2866
- const values = [];
2867
- for (const line of text.split(`
2868
- `)) {
2869
- if (!line)
2870
- continue;
2871
- try {
2872
- values.push(JSON.parse(line));
2873
- } catch {}
2874
- }
2875
- return values.slice(-limit);
2876
- }
2877
- function readJsonSync(target) {
2878
- try {
2879
- if (!fs2.existsSync(target))
2880
- return null;
2881
- const raw = fs2.readFileSync(target, "utf8");
2882
- return JSON.parse(raw);
2883
- } catch (e) {
2884
- warn("readJsonSync failed for " + target, e);
2885
- return null;
2886
- }
2887
- }
2888
- function writeJsonSync(target, value, pretty = false) {
2889
- atomicWriteFileSync(target, pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value));
2890
- }
2891
-
2892
3061
  // src/utils/id-fingerprint.ts
2893
3062
  import crypto2 from "crypto";
2894
3063
  var FINGERPRINT_TAIL_LEN = 16;
@@ -4826,7 +4995,7 @@ function advancePastNonPortableToolExchanges(msgs, keepFrom, toolCallIndex) {
4826
4995
  const exchangeEnds = new Map;
4827
4996
  for (let index = keepFrom;index < msgs.length; index++) {
4828
4997
  const message = msgs[index].message;
4829
- if (!isRecord(message) || message.role !== "toolResult" || typeof message.toolCallId !== "string")
4998
+ if (!isRecord2(message) || message.role !== "toolResult" || typeof message.toolCallId !== "string")
4830
4999
  continue;
4831
5000
  const callIndex = toolCallIndex.get(message.toolCallId);
4832
5001
  if (callIndex === undefined || callIndex < keepFrom)
@@ -4836,10 +5005,10 @@ function advancePastNonPortableToolExchanges(msgs, keepFrom, toolCallIndex) {
4836
5005
  let adjusted = keepFrom;
4837
5006
  for (let index = keepFrom;index < msgs.length; index++) {
4838
5007
  const message = msgs[index].message;
4839
- if (!isRecord(message))
5008
+ if (!isRecord2(message))
4840
5009
  continue;
4841
5010
  if (message.role === "assistant" && Array.isArray(message.content)) {
4842
- 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)));
4843
5012
  if (nonPortable)
4844
5013
  adjusted = Math.max(adjusted, exchangeEnds.get(index) ?? index + 1);
4845
5014
  } else if (message.role === "toolResult" && typeof message.toolName === "string" && !PORTABLE_TOOL_NAME_RE.test(message.toolName)) {
@@ -4906,7 +5075,7 @@ function planCompactionWindow(input) {
4906
5075
  let protectedUserIndex;
4907
5076
  for (let index = msgs.length - 1;index >= 0; index--) {
4908
5077
  const message = msgs[index].message;
4909
- if (!isRecord(message) || message.role !== "user")
5078
+ if (!isRecord2(message) || message.role !== "user")
4910
5079
  continue;
4911
5080
  userOrdinal++;
4912
5081
  protectedUserIndex = index;
@@ -4937,7 +5106,7 @@ function planCompactionWindow(input) {
4937
5106
  const targetAfterTokens = !force && modelContextWindow ? modelContextWindow * targetPercent / 100 : fixedContextTokens + effectiveRetentionCeiling + finalSummaryAllowance;
4938
5107
  let reason = "viable";
4939
5108
  const firstKeptMessage = msgs[keepFrom]?.message;
4940
- if (nonPortableTailBlocked || isRecord(firstKeptMessage) && firstKeptMessage.role === "toolResult")
5109
+ if (nonPortableTailBlocked || isRecord2(firstKeptMessage) && firstKeptMessage.role === "toolResult")
4941
5110
  reason = "unsafe-tool-boundary";
4942
5111
  else if (keepFrom <= 0)
4943
5112
  reason = "no-eligible-prefix";
@@ -11418,6 +11587,19 @@ async function runSmartCompact(opts) {
11418
11587
  }
11419
11588
  }
11420
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
+
11421
11603
  // src/app/pending-slot.ts
11422
11604
  function createPendingSlot(opts) {
11423
11605
  const ttlMs = opts.ttlMs;
@@ -11833,6 +12015,7 @@ function resolveGraphScope(ctx) {
11833
12015
  };
11834
12016
  }
11835
12017
  function registerContextTools(pi) {
12018
+ const availability = createContextToolAvailability(pi);
11836
12019
  pi.registerTool({
11837
12020
  name: "smart_recall",
11838
12021
  label: "Smart Recall",
@@ -12037,12 +12220,48 @@ Paths: ` + relatedPaths.join(", ") : ""));
12037
12220
  details: { memory, redactions: scrubber.count() }
12038
12221
  };
12039
12222
  } catch (error2) {
12040
- debugError("Project memory persistence failed", error2);
12041
- const message = scrubber.scrubText(error2 instanceof Error ? error2.message : String(error2)).value;
12042
- 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);
12043
12262
  }
12044
12263
  }
12045
- });
12264
+ };
12046
12265
  }
12047
12266
 
12048
12267
  // src/app/model-routing.ts
@@ -13226,71 +13445,874 @@ async function showMetricsDashboardUI(ctx, opts) {
13226
13445
 
13227
13446
  // src/ui/settings-overlay.ts
13228
13447
  import {
13229
- getSettingsListTheme
13448
+ getSettingsListTheme as getSettingsListTheme2
13230
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";
13231
13458
  import {
13232
13459
  Container as Container3,
13460
+ Input,
13461
+ Key as Key4,
13462
+ matchesKey as matchesKey4,
13463
+ SelectList as SelectList3,
13233
13464
  SettingsList,
13234
13465
  Text as Text3
13235
13466
  } from "@earendil-works/pi-tui";
13236
- var INHERIT = "host default";
13237
- var ENABLED = "enabled";
13238
- var DISABLED = "disabled";
13239
- function accessValue(policy) {
13240
- return policy.agentToolAccess === "inherit" ? INHERIT : policy.agentToolAccess;
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
+ }));
13241
14117
  }
13242
- function parseAccessValue(value) {
13243
- return value === INHERIT ? "inherit" : value;
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));
13244
14123
  }
13245
- function previousValue(id, policy) {
13246
- if (id === "agentToolAccess")
13247
- return accessValue(policy);
13248
- return policy.autoTrigger ? ENABLED : DISABLED;
14124
+ function effectiveDescription(field, policy) {
14125
+ return field === "agentToolAccess" ? `Effective tool state: ${enabled(policy.agentToolEnabled)}` : `Effective value: ${enabled(policy[field])}`;
13249
14126
  }
13250
- function settingsItems(policy) {
14127
+ function sessionSettingsItems(policy) {
14128
+ const current = policy.snapshot();
14129
+ const overrides = policy.branchOverrides();
13251
14130
  return [
13252
14131
  {
13253
14132
  id: "agentToolAccess",
13254
14133
  label: "Agent access",
13255
- description: "Inherit Pi's tool selection, or explicitly enable/disable smart_compact",
13256
- currentValue: accessValue(policy),
13257
- values: [INHERIT, ENABLED, DISABLED]
14134
+ description: effectiveDescription("agentToolAccess", current),
14135
+ currentValue: overrides.agentToolAccess ?? "global",
14136
+ values: ["global", "inherit", "enabled", "disabled"]
13258
14137
  },
13259
14138
  {
13260
14139
  id: "autoTrigger",
13261
14140
  label: "Automatic compaction",
13262
- description: "Run Smart Compact from Pi's automatic compaction lifecycle",
13263
- currentValue: policy.autoTrigger ? ENABLED : DISABLED,
13264
- values: [ENABLED, DISABLED]
14141
+ description: effectiveDescription("autoTrigger", current),
14142
+ currentValue: overrides.autoTrigger === undefined ? "global" : enabled(overrides.autoTrigger),
14143
+ values: ["global", "enabled", "disabled"]
14144
+ },
14145
+ {
14146
+ id: "showStatus",
14147
+ label: "Footer status",
14148
+ description: effectiveDescription("showStatus", current),
14149
+ currentValue: overrides.showStatus === undefined ? "global" : enabled(overrides.showStatus),
14150
+ values: ["global", "enabled", "disabled"]
13265
14151
  }
13266
14152
  ];
13267
14153
  }
13268
- async function showSmartCompactSettings(ctx, policy) {
13269
- await ctx.ui.custom((tui, theme, _keybindings, done) => {
13270
- const container = new Container3;
13271
- container.addChild(new Text3(theme.fg("accent", theme.bold("Smart Compact Settings")), 0, 0));
13272
- container.addChild(new Text3(theme.fg("dim", "Changes apply now and follow this session branch."), 0, 0));
13273
- container.addChild(new Text3(theme.fg("dim", "Manual /smart-compact stays available in every mode."), 0, 0));
13274
- container.addChild(new Text3("", 0, 0));
13275
- const list = new SettingsList(settingsItems(policy.snapshot()), 6, getSettingsListTheme(), (id, value) => {
13276
- const previous = policy.snapshot();
13277
- const result = id === "agentToolAccess" ? policy.update({ agentToolAccess: parseAccessValue(value) }, ctx) : policy.update({ autoTrigger: 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);
13278
14198
  if (!result.ok) {
13279
- list.updateValue(id, previousValue(id, previous));
13280
14199
  ctx.ui.notify(result.error, "error");
14200
+ list.updateValue(id, displayValue(field, policy));
13281
14201
  }
13282
- }, () => done(undefined));
13283
- container.addChild(list);
13284
- container.addChild(new Text3("", 0, 0));
13285
- container.addChild(new Text3(theme.fg("dim", "\u2191\u2193 navigate \xB7 enter change \xB7 esc close"), 0, 0));
13286
- return {
13287
- render: (width) => container.render(width),
13288
- invalidate: () => container.invalidate(),
13289
- handleInput(data) {
13290
- list.handleInput?.(data);
13291
- 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
+ }
13292
14227
  }
13293
- };
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());
13294
14316
  });
13295
14317
  }
13296
14318
 
@@ -13463,6 +14485,7 @@ async function runInteractiveCompaction(ctx, config, dependencies) {
13463
14485
  });
13464
14486
  }
13465
14487
  function registerSmartCompactCommand(pi, dependencies) {
14488
+ const settingsCoordinator = new GlobalSettingsCoordinator;
13466
14489
  pi.registerCommand("smart-compact", {
13467
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]",
13468
14491
  getArgumentCompletions(prefix) {
@@ -13515,7 +14538,7 @@ function registerSmartCompactCommand(pi, dependencies) {
13515
14538
  ctx.ui.notify("Smart Compact settings require TUI mode. Use settings.json for permanent defaults.", "warning");
13516
14539
  return;
13517
14540
  }
13518
- await showSmartCompactSettings(ctx, dependencies.policy);
14541
+ await showSmartCompactSettings(ctx, dependencies.policy, settingsCoordinator, (path16) => dependencies.onGlobalSettingApplied?.(path16, ctx));
13519
14542
  return;
13520
14543
  }
13521
14544
  const config = loadConfig();
@@ -13562,17 +14585,43 @@ function registerSmartCompactCommand(pi, dependencies) {
13562
14585
  // src/app/smart-compact-policy.ts
13563
14586
  var SMART_COMPACT_TOOL_NAME = "smart_compact";
13564
14587
  var SMART_COMPACT_POLICY_ENTRY = "smart-compact-policy";
13565
- var POLICY_VERSION = 2;
14588
+ var POLICY_VERSION = 3;
13566
14589
  var STATUS_KEY = "smart-compact-policy";
13567
14590
  function persistedPolicy(value) {
13568
14591
  if (typeof value !== "object" || value === null)
13569
14592
  return null;
13570
14593
  const candidate = value;
13571
- if (candidate.version === POLICY_VERSION && (candidate.agentToolAccess === "inherit" || candidate.agentToolAccess === "enabled" || candidate.agentToolAccess === "disabled") && typeof candidate.autoTrigger === "boolean") {
13572
- return {
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") {
14616
+ const desired = {
13573
14617
  agentToolAccess: candidate.agentToolAccess,
13574
- autoTrigger: candidate.autoTrigger
14618
+ autoTrigger: candidate.autoTrigger,
14619
+ showStatus: true
13575
14620
  };
14621
+ if (typeof candidate.showStatus === "boolean") {
14622
+ desired.showStatus = candidate.showStatus;
14623
+ }
14624
+ return desired;
13576
14625
  }
13577
14626
  if (candidate.version === 1 && typeof candidate.agentToolEnabled === "boolean" && typeof candidate.autoTrigger === "boolean") {
13578
14627
  return {
@@ -13586,7 +14635,8 @@ function configDefaults() {
13586
14635
  const config = loadConfig();
13587
14636
  return {
13588
14637
  agentToolAccess: config.agentToolAccess,
13589
- autoTrigger: config.autoTrigger
14638
+ autoTrigger: config.autoTrigger,
14639
+ showStatus: config.showStatus !== false
13590
14640
  };
13591
14641
  }
13592
14642
  function statusText(policy) {
@@ -13601,13 +14651,18 @@ function statusText(policy) {
13601
14651
  return "smart-compact: auto off";
13602
14652
  }
13603
14653
  function createSmartCompactPolicy(pi) {
13604
- let current = configDefaults();
14654
+ let overrides = {};
14655
+ const desired = () => ({
14656
+ ...configDefaults(),
14657
+ ...overrides
14658
+ });
13605
14659
  const effectiveToolState = () => pi.getActiveTools().includes(SMART_COMPACT_TOOL_NAME);
13606
14660
  const snapshot = () => ({
13607
- ...current,
14661
+ ...desired(),
13608
14662
  agentToolEnabled: effectiveToolState()
13609
14663
  });
13610
14664
  const apply = (ctx) => {
14665
+ const current = desired();
13611
14666
  const active = pi.getActiveTools();
13612
14667
  const hasTool = active.includes(SMART_COMPACT_TOOL_NAME);
13613
14668
  if (current.agentToolAccess === "enabled" && !hasTool) {
@@ -13616,48 +14671,67 @@ function createSmartCompactPolicy(pi) {
13616
14671
  pi.setActiveTools(active.filter((name) => name !== SMART_COMPACT_TOOL_NAME));
13617
14672
  }
13618
14673
  const effective = snapshot();
13619
- ctx.ui.setStatus(STATUS_KEY, statusText(effective));
14674
+ ctx.ui.setStatus(STATUS_KEY, current.showStatus ? statusText(effective) : undefined);
13620
14675
  return effective;
13621
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
+ };
13622
14712
  return {
13623
14713
  snapshot,
14714
+ branchOverrides: () => ({ ...overrides }),
13624
14715
  isAgentToolEnabled: effectiveToolState,
13625
- isAutoTriggerEnabled: () => current.autoTrigger,
14716
+ isAutoTriggerEnabled: () => desired().autoTrigger,
13626
14717
  restore(ctx) {
13627
- current = configDefaults();
14718
+ overrides = {};
13628
14719
  for (const entry of ctx.sessionManager.getBranch()) {
13629
14720
  if (entry.type === "custom" && entry.customType === SMART_COMPACT_POLICY_ENTRY) {
13630
14721
  const restored = persistedPolicy(entry.data);
13631
14722
  if (restored)
13632
- current = restored;
14723
+ overrides = restored;
13633
14724
  }
13634
14725
  }
13635
14726
  apply(ctx);
13636
14727
  },
13637
14728
  update(patch, ctx) {
13638
- const previous = current;
13639
- const previousActiveTools = pi.getActiveTools();
13640
- current = { ...current, ...patch };
13641
- try {
13642
- const effective = apply(ctx);
13643
- pi.appendEntry(SMART_COMPACT_POLICY_ENTRY, { version: POLICY_VERSION, ...current });
13644
- return { ok: true, policy: effective };
13645
- } catch (error2) {
13646
- debugError("Smart Compact policy update failed", error2);
13647
- current = previous;
13648
- try {
13649
- pi.setActiveTools(previousActiveTools);
13650
- } catch (rollbackError) {
13651
- debugError("Smart Compact policy rollback failed", rollbackError);
13652
- }
13653
- const rolledBack = snapshot();
13654
- ctx.ui.setStatus(STATUS_KEY, statusText(rolledBack));
13655
- return {
13656
- ok: false,
13657
- policy: rolledBack,
13658
- error: "Smart Compact settings could not be saved; the previous policy was restored."
13659
- };
13660
- }
14729
+ return persist({ ...overrides, ...patch }, ctx);
14730
+ },
14731
+ reset(field, ctx) {
14732
+ const next = { ...overrides };
14733
+ delete next[field];
14734
+ return persist(next, ctx);
13661
14735
  }
13662
14736
  };
13663
14737
  }
@@ -13730,12 +14804,19 @@ function smartCompactExtension(pi) {
13730
14804
  return false;
13731
14805
  }
13732
14806
  };
13733
- registerContextTools(pi);
14807
+ const contextToolAvailability = registerContextTools(pi);
13734
14808
  registerSmartCompactCommand(pi, {
13735
14809
  pendingRef,
13736
14810
  runLock: isRunning,
13737
14811
  onNativeApplyError,
13738
- 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
+ }
13739
14820
  });
13740
14821
  pi.on("session_start", (_event, ctx) => {
13741
14822
  policy.restore(ctx);