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/CHANGELOG.md +20 -0
- package/README.md +32 -17
- package/dist/app/global-settings-runtime.d.ts +7 -0
- package/dist/app/global-settings-runtime.d.ts.map +1 -0
- package/dist/app/register-context-tools.d.ts +4 -1
- package/dist/app/register-context-tools.d.ts.map +1 -1
- package/dist/app/register-smart-compact-command.d.ts +3 -1
- package/dist/app/register-smart-compact-command.d.ts.map +1 -1
- package/dist/app/smart-compact-policy.d.ts +4 -1
- package/dist/app/smart-compact-policy.d.ts.map +1 -1
- package/dist/constants.d.ts +43 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1436 -372
- package/dist/infra/fs.d.ts +2 -1
- package/dist/infra/fs.d.ts.map +1 -1
- package/dist/provider-eval.js +33 -33
- package/dist/provider-scenario-eval.js +33 -33
- package/dist/telemetry-report.js +1 -1
- package/dist/ui/settings-complex.d.ts +9 -0
- package/dist/ui/settings-complex.d.ts.map +1 -0
- package/dist/ui/settings-overlay.d.ts +43 -3
- package/dist/ui/settings-overlay.d.ts.map +1 -1
- package/dist/utils/config.d.ts +14 -1
- package/dist/utils/config.d.ts.map +1 -1
- package/package.json +2 -2
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.
|
|
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
|
|
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
|
|
558
|
+
return path2.join(home(), ".pi", "agent");
|
|
329
559
|
}
|
|
330
560
|
function cacheDir() {
|
|
331
|
-
return
|
|
561
|
+
return path2.join(piAgentDir(), ".cache");
|
|
332
562
|
}
|
|
333
563
|
function smartCompactCacheDir() {
|
|
334
|
-
return
|
|
564
|
+
return path2.join(cacheDir(), "smart-compact");
|
|
335
565
|
}
|
|
336
566
|
function projectFingerprintDir() {
|
|
337
|
-
return
|
|
567
|
+
return path2.join(smartCompactCacheDir(), "projects");
|
|
338
568
|
}
|
|
339
569
|
function compactionStateDir() {
|
|
340
|
-
return
|
|
570
|
+
return path2.join(smartCompactCacheDir(), "states");
|
|
341
571
|
}
|
|
342
572
|
function sessionsDir() {
|
|
343
|
-
return
|
|
573
|
+
return path2.join(piAgentDir(), "sessions");
|
|
344
574
|
}
|
|
345
575
|
function settingsFile() {
|
|
346
|
-
return
|
|
576
|
+
return path2.join(piAgentDir(), "settings.json");
|
|
347
577
|
}
|
|
348
578
|
function defaultBackupDir() {
|
|
349
|
-
return
|
|
579
|
+
return path2.join(piAgentDir(), "compact-backups");
|
|
350
580
|
}
|
|
351
581
|
function metricsLogFile() {
|
|
352
|
-
return
|
|
582
|
+
return path2.join(cacheDir(), "compact-metrics.jsonl");
|
|
353
583
|
}
|
|
354
584
|
function runLocksDir() {
|
|
355
|
-
return
|
|
585
|
+
return path2.join(smartCompactCacheDir(), "run-locks");
|
|
356
586
|
}
|
|
357
587
|
function nativeContinuityDir() {
|
|
358
|
-
return
|
|
588
|
+
return path2.join(smartCompactCacheDir(), "native-continuity");
|
|
359
589
|
}
|
|
360
590
|
function contextGraphFile() {
|
|
361
|
-
return
|
|
591
|
+
return path2.join(smartCompactCacheDir(), "context-graph.sqlite");
|
|
362
592
|
}
|
|
363
593
|
function damageReportsFile() {
|
|
364
|
-
return
|
|
594
|
+
return path2.join(smartCompactCacheDir(), "damage-reports.jsonl");
|
|
365
595
|
}
|
|
366
596
|
function extractionCacheFile(sessionId) {
|
|
367
|
-
return
|
|
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
|
|
600
|
+
return path2.join(projectFingerprintDir(), projectId + ".json");
|
|
371
601
|
}
|
|
372
602
|
function compactionStateFile(projectId) {
|
|
373
|
-
return
|
|
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
|
|
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
|
|
614
|
+
return path2.join(compactionStateDir(), project, session, branch + ".json");
|
|
385
615
|
}
|
|
386
616
|
function remediationHintsFile(projectId) {
|
|
387
|
-
return
|
|
617
|
+
return path2.join(smartCompactCacheDir(), "remediation-" + projectId + ".json");
|
|
388
618
|
}
|
|
389
619
|
function metricsDashboardFile() {
|
|
390
|
-
return
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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 (
|
|
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 (
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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 =
|
|
952
|
+
const stat = fs2.statSync(file);
|
|
610
953
|
if (cachedConfig && cachedPath === file && stat.mtimeMs === cachedMtime)
|
|
611
|
-
return cachedConfig;
|
|
612
|
-
const
|
|
613
|
-
const
|
|
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 = { ...
|
|
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
|
-
|
|
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
|
|
1290
|
+
import path3 from "path";
|
|
939
1291
|
|
|
940
1292
|
// src/utils/type-guards.ts
|
|
941
|
-
function
|
|
1293
|
+
function isRecord2(value) {
|
|
942
1294
|
return typeof value === "object" && value !== null;
|
|
943
1295
|
}
|
|
944
1296
|
function isTextBlock(c) {
|
|
945
|
-
return
|
|
1297
|
+
return isRecord2(c) && c.type === "text" && typeof c.text === "string";
|
|
946
1298
|
}
|
|
947
1299
|
function isToolCallBlock(c) {
|
|
948
|
-
return
|
|
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((
|
|
1125
|
-
const normalizedPath = normalizePath(
|
|
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 (!
|
|
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" || !
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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((
|
|
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
|
|
1875
|
-
const fullDigest = createHash("sha256").update(
|
|
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 !==
|
|
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,
|
|
1883
|
-
digests.set(
|
|
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((
|
|
1887
|
-
|
|
1888
|
-
compactPathLine(
|
|
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 =
|
|
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:
|
|
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 (!
|
|
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 (!
|
|
5008
|
+
if (!isRecord2(message))
|
|
4842
5009
|
continue;
|
|
4843
5010
|
if (message.role === "assistant" && Array.isArray(message.content)) {
|
|
4844
|
-
const nonPortable = message.content.some((block) =>
|
|
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 (!
|
|
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 ||
|
|
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
|
-
|
|
13239
|
-
var
|
|
13240
|
-
|
|
13241
|
-
|
|
13242
|
-
|
|
13243
|
-
|
|
13244
|
-
|
|
13245
|
-
|
|
13246
|
-
|
|
13247
|
-
|
|
13248
|
-
|
|
13249
|
-
|
|
13250
|
-
|
|
13251
|
-
|
|
13252
|
-
|
|
13253
|
-
|
|
13254
|
-
|
|
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: "
|
|
13260
|
-
currentValue:
|
|
13261
|
-
values: [
|
|
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: "
|
|
13267
|
-
currentValue:
|
|
13268
|
-
values: [
|
|
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: "
|
|
13274
|
-
currentValue:
|
|
13275
|
-
values: [
|
|
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
|
-
|
|
13280
|
-
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13284
|
-
|
|
13285
|
-
|
|
13286
|
-
|
|
13287
|
-
|
|
13288
|
-
|
|
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
|
-
}
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13299
|
-
|
|
13300
|
-
|
|
13301
|
-
|
|
13302
|
-
|
|
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 =
|
|
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 &&
|
|
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
|
|
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
|
-
...
|
|
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: () =>
|
|
14716
|
+
isAutoTriggerEnabled: () => desired().autoTrigger,
|
|
13643
14717
|
restore(ctx) {
|
|
13644
|
-
|
|
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
|
-
|
|
14723
|
+
overrides = restored;
|
|
13650
14724
|
}
|
|
13651
14725
|
}
|
|
13652
14726
|
apply(ctx);
|
|
13653
14727
|
},
|
|
13654
14728
|
update(patch, ctx) {
|
|
13655
|
-
|
|
13656
|
-
|
|
13657
|
-
|
|
13658
|
-
|
|
13659
|
-
|
|
13660
|
-
|
|
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);
|