pi-smart-compact 9.4.0 → 9.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +33 -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 +6 -1
- package/dist/app/smart-compact-policy.d.ts.map +1 -1
- package/dist/constants.d.ts +44 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1449 -368
- package/dist/infra/fs.d.ts +2 -1
- package/dist/infra/fs.d.ts.map +1 -1
- package/dist/provider-eval.js +34 -33
- package/dist/provider-scenario-eval.js +34 -33
- package/dist/telemetry-report.js +2 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +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 +83 -83
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",
|
|
@@ -59,6 +85,7 @@ var DEFAULT_CONFIG = {
|
|
|
59
85
|
segmentationThinkingLevel: "minimal",
|
|
60
86
|
agentToolAccess: "inherit",
|
|
61
87
|
autoTrigger: true,
|
|
88
|
+
showStatus: true,
|
|
62
89
|
autoTriggerStrategy: "native-hook",
|
|
63
90
|
autoTriggerTimeoutMs: 120000,
|
|
64
91
|
backupEnabled: true,
|
|
@@ -314,102 +341,283 @@ var EXPLORER_SYSTEM_PROMPT = `You are a conversation analyst. You have determini
|
|
|
314
341
|
` + '{"boundaries":[{"afterIndex":N,"topic":"...","priority":"critical|high|normal|low","confidence":0.0-1.0}],"mainGoal":"...","sessionType":"implementation|review|debugging|discussion","enrichedConstraints":[...],"crossReferences":[...],"statusAssessment":{"done":[...],"inProgress":[...],"blocked":[...]},"criticalContext":[...],"keyDecisions":[...]}';
|
|
315
342
|
|
|
316
343
|
// src/utils/config.ts
|
|
344
|
+
import fs2 from "fs";
|
|
345
|
+
|
|
346
|
+
// src/infra/fs.ts
|
|
317
347
|
import fs from "fs";
|
|
348
|
+
import fsp from "fs/promises";
|
|
349
|
+
import path from "path";
|
|
350
|
+
import crypto from "crypto";
|
|
351
|
+
|
|
352
|
+
// src/utils/logger.ts
|
|
353
|
+
var DEBUG = process.env.DEBUG?.includes("smart-compact") ?? false;
|
|
354
|
+
function warn(msg, err) {
|
|
355
|
+
const detail = err instanceof Error ? err.message : err ?? "";
|
|
356
|
+
console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
|
|
357
|
+
}
|
|
358
|
+
function error(msg, err) {
|
|
359
|
+
const detail = err instanceof Error ? err.message + `
|
|
360
|
+
` + err.stack : err ?? "";
|
|
361
|
+
console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
|
|
362
|
+
}
|
|
363
|
+
function info(msg, ...args) {
|
|
364
|
+
console.error(LOG_PREFIX + " [info] " + msg, ...args);
|
|
365
|
+
}
|
|
366
|
+
function debug(msg, ...args) {
|
|
367
|
+
if (DEBUG)
|
|
368
|
+
console.error(LOG_PREFIX + " [debug] " + msg, ...args);
|
|
369
|
+
}
|
|
370
|
+
function debugError(msg, err) {
|
|
371
|
+
if (DEBUG)
|
|
372
|
+
error(msg, err);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/infra/fs.ts
|
|
376
|
+
var LOCK_RETRY_MS = 25;
|
|
377
|
+
var LOCK_MAX_RETRIES = 80;
|
|
378
|
+
function ensureDir(dir) {
|
|
379
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
380
|
+
fs.chmodSync(dir, 448);
|
|
381
|
+
}
|
|
382
|
+
async function ensureDirAsync(dir) {
|
|
383
|
+
await fsp.mkdir(dir, { recursive: true, mode: 448 });
|
|
384
|
+
await fsp.chmod(dir, 448);
|
|
385
|
+
}
|
|
386
|
+
function tempPath(target) {
|
|
387
|
+
return target + ".tmp." + process.pid + "." + crypto.randomBytes(4).toString("hex");
|
|
388
|
+
}
|
|
389
|
+
function atomicWriteFileSync(target, data) {
|
|
390
|
+
ensureDir(path.dirname(target));
|
|
391
|
+
const tmp = tempPath(target);
|
|
392
|
+
try {
|
|
393
|
+
fs.writeFileSync(tmp, data, { mode: 384 });
|
|
394
|
+
fs.chmodSync(tmp, 384);
|
|
395
|
+
fs.renameSync(tmp, target);
|
|
396
|
+
} catch (e) {
|
|
397
|
+
try {
|
|
398
|
+
fs.unlinkSync(tmp);
|
|
399
|
+
} catch {}
|
|
400
|
+
throw e;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function atomicWriteFile(target, data) {
|
|
404
|
+
await ensureDirAsync(path.dirname(target));
|
|
405
|
+
const tmp = tempPath(target);
|
|
406
|
+
try {
|
|
407
|
+
await fsp.writeFile(tmp, data, { mode: 384 });
|
|
408
|
+
await fsp.chmod(tmp, 384);
|
|
409
|
+
await fsp.rename(tmp, target);
|
|
410
|
+
} catch (e) {
|
|
411
|
+
try {
|
|
412
|
+
await fsp.unlink(tmp);
|
|
413
|
+
} catch {}
|
|
414
|
+
throw e;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function tryAcquireLock(target) {
|
|
418
|
+
const lockDir = target + ".lock";
|
|
419
|
+
const ownerFile = path.join(lockDir, "owner");
|
|
420
|
+
const token = process.pid + ":" + crypto.randomBytes(8).toString("hex");
|
|
421
|
+
try {
|
|
422
|
+
fs.mkdirSync(lockDir, { mode: 448 });
|
|
423
|
+
} catch (error2) {
|
|
424
|
+
if (error2?.code === "EEXIST")
|
|
425
|
+
return null;
|
|
426
|
+
throw new Error("Failed to acquire lock for " + target, { cause: error2 });
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
fs.writeFileSync(ownerFile, token, { mode: 384, flag: "wx" });
|
|
430
|
+
} catch (error2) {
|
|
431
|
+
try {
|
|
432
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
433
|
+
} catch {}
|
|
434
|
+
throw new Error("Failed to acquire lock for " + target, { cause: error2 });
|
|
435
|
+
}
|
|
436
|
+
return () => {
|
|
437
|
+
try {
|
|
438
|
+
if (fs.readFileSync(ownerFile, "utf8") === token) {
|
|
439
|
+
fs.rmSync(lockDir, { recursive: true });
|
|
440
|
+
}
|
|
441
|
+
} catch {}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function acquireLockSync(target) {
|
|
445
|
+
const release = tryAcquireLock(target);
|
|
446
|
+
if (!release)
|
|
447
|
+
throw new Error("Lock busy for " + target);
|
|
448
|
+
return release;
|
|
449
|
+
}
|
|
450
|
+
async function acquireLock(target) {
|
|
451
|
+
for (let attempt = 0;attempt < LOCK_MAX_RETRIES; attempt++) {
|
|
452
|
+
const release = tryAcquireLock(target);
|
|
453
|
+
if (release)
|
|
454
|
+
return release;
|
|
455
|
+
const delay = Promise.withResolvers();
|
|
456
|
+
setTimeout(delay.resolve, LOCK_RETRY_MS);
|
|
457
|
+
await delay.promise;
|
|
458
|
+
}
|
|
459
|
+
throw new Error("Timed out acquiring lock for " + target);
|
|
460
|
+
}
|
|
461
|
+
async function appendLineLockedAsync(target, line, maxBytes) {
|
|
462
|
+
await ensureDirAsync(path.dirname(target));
|
|
463
|
+
const payload = Buffer.from(line.endsWith(`
|
|
464
|
+
`) ? line : line + `
|
|
465
|
+
`);
|
|
466
|
+
if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)) {
|
|
467
|
+
throw new Error("maxBytes must be a positive safe integer");
|
|
468
|
+
}
|
|
469
|
+
if (maxBytes !== undefined && payload.length > maxBytes) {
|
|
470
|
+
throw new Error("Log entry exceeds retention cap for " + target);
|
|
471
|
+
}
|
|
472
|
+
const release = await acquireLock(target);
|
|
473
|
+
try {
|
|
474
|
+
let stat = null;
|
|
475
|
+
try {
|
|
476
|
+
stat = await fsp.stat(target);
|
|
477
|
+
} catch (error2) {
|
|
478
|
+
if (!error2 || typeof error2 !== "object" || !("code" in error2) || error2.code !== "ENOENT")
|
|
479
|
+
throw error2;
|
|
480
|
+
}
|
|
481
|
+
if (maxBytes !== undefined && stat && stat.size + payload.length > maxBytes) {
|
|
482
|
+
const retainedLength = Math.min(stat.size, Math.max(0, maxBytes - payload.length));
|
|
483
|
+
const buffer = Buffer.allocUnsafe(retainedLength);
|
|
484
|
+
if (retainedLength > 0) {
|
|
485
|
+
const handle = await fsp.open(target, "r");
|
|
486
|
+
try {
|
|
487
|
+
await handle.read(buffer, 0, retainedLength, stat.size - retainedLength);
|
|
488
|
+
} finally {
|
|
489
|
+
await handle.close();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
let tail = buffer.toString("utf8");
|
|
493
|
+
if (retainedLength < stat.size) {
|
|
494
|
+
const firstNewline = tail.indexOf(`
|
|
495
|
+
`);
|
|
496
|
+
tail = firstNewline >= 0 ? tail.slice(firstNewline + 1) : "";
|
|
497
|
+
}
|
|
498
|
+
await atomicWriteFile(target, tail);
|
|
499
|
+
}
|
|
500
|
+
await fsp.appendFile(target, payload, { mode: 384 });
|
|
501
|
+
await fsp.chmod(target, 384);
|
|
502
|
+
} finally {
|
|
503
|
+
release();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function readJsonlTail(target, limit, maxBytes = 512 * 1024) {
|
|
507
|
+
if (limit <= 0 || !fs.existsSync(target))
|
|
508
|
+
return [];
|
|
509
|
+
const stat = fs.statSync(target);
|
|
510
|
+
const length = Math.min(stat.size, maxBytes);
|
|
511
|
+
const buffer = Buffer.alloc(length);
|
|
512
|
+
const fd = fs.openSync(target, "r");
|
|
513
|
+
try {
|
|
514
|
+
fs.readSync(fd, buffer, 0, length, stat.size - length);
|
|
515
|
+
} finally {
|
|
516
|
+
fs.closeSync(fd);
|
|
517
|
+
}
|
|
518
|
+
let text = buffer.toString("utf8");
|
|
519
|
+
if (stat.size > length) {
|
|
520
|
+
const newline = text.indexOf(`
|
|
521
|
+
`);
|
|
522
|
+
text = newline >= 0 ? text.slice(newline + 1) : "";
|
|
523
|
+
}
|
|
524
|
+
const values = [];
|
|
525
|
+
for (const line of text.split(`
|
|
526
|
+
`)) {
|
|
527
|
+
if (!line)
|
|
528
|
+
continue;
|
|
529
|
+
try {
|
|
530
|
+
values.push(JSON.parse(line));
|
|
531
|
+
} catch {}
|
|
532
|
+
}
|
|
533
|
+
return values.slice(-limit);
|
|
534
|
+
}
|
|
535
|
+
function readJsonSync(target) {
|
|
536
|
+
try {
|
|
537
|
+
if (!fs.existsSync(target))
|
|
538
|
+
return null;
|
|
539
|
+
const raw = fs.readFileSync(target, "utf8");
|
|
540
|
+
return JSON.parse(raw);
|
|
541
|
+
} catch (e) {
|
|
542
|
+
warn("readJsonSync failed for " + target, e);
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function writeJsonSync(target, value, pretty = false) {
|
|
547
|
+
atomicWriteFileSync(target, pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value));
|
|
548
|
+
}
|
|
318
549
|
|
|
319
550
|
// src/infra/paths.ts
|
|
320
|
-
import
|
|
551
|
+
import path2 from "path";
|
|
321
552
|
import os from "os";
|
|
322
553
|
function home() {
|
|
323
554
|
const configured = process.env.HOME?.trim() || process.env.USERPROFILE?.trim();
|
|
324
555
|
return configured || os.homedir();
|
|
325
556
|
}
|
|
326
557
|
function piAgentDir() {
|
|
327
|
-
return
|
|
558
|
+
return path2.join(home(), ".pi", "agent");
|
|
328
559
|
}
|
|
329
560
|
function cacheDir() {
|
|
330
|
-
return
|
|
561
|
+
return path2.join(piAgentDir(), ".cache");
|
|
331
562
|
}
|
|
332
563
|
function smartCompactCacheDir() {
|
|
333
|
-
return
|
|
564
|
+
return path2.join(cacheDir(), "smart-compact");
|
|
334
565
|
}
|
|
335
566
|
function projectFingerprintDir() {
|
|
336
|
-
return
|
|
567
|
+
return path2.join(smartCompactCacheDir(), "projects");
|
|
337
568
|
}
|
|
338
569
|
function compactionStateDir() {
|
|
339
|
-
return
|
|
570
|
+
return path2.join(smartCompactCacheDir(), "states");
|
|
340
571
|
}
|
|
341
572
|
function sessionsDir() {
|
|
342
|
-
return
|
|
573
|
+
return path2.join(piAgentDir(), "sessions");
|
|
343
574
|
}
|
|
344
575
|
function settingsFile() {
|
|
345
|
-
return
|
|
576
|
+
return path2.join(piAgentDir(), "settings.json");
|
|
346
577
|
}
|
|
347
578
|
function defaultBackupDir() {
|
|
348
|
-
return
|
|
579
|
+
return path2.join(piAgentDir(), "compact-backups");
|
|
349
580
|
}
|
|
350
581
|
function metricsLogFile() {
|
|
351
|
-
return
|
|
582
|
+
return path2.join(cacheDir(), "compact-metrics.jsonl");
|
|
352
583
|
}
|
|
353
584
|
function runLocksDir() {
|
|
354
|
-
return
|
|
585
|
+
return path2.join(smartCompactCacheDir(), "run-locks");
|
|
355
586
|
}
|
|
356
587
|
function nativeContinuityDir() {
|
|
357
|
-
return
|
|
588
|
+
return path2.join(smartCompactCacheDir(), "native-continuity");
|
|
358
589
|
}
|
|
359
590
|
function contextGraphFile() {
|
|
360
|
-
return
|
|
591
|
+
return path2.join(smartCompactCacheDir(), "context-graph.sqlite");
|
|
361
592
|
}
|
|
362
593
|
function damageReportsFile() {
|
|
363
|
-
return
|
|
594
|
+
return path2.join(smartCompactCacheDir(), "damage-reports.jsonl");
|
|
364
595
|
}
|
|
365
596
|
function extractionCacheFile(sessionId) {
|
|
366
|
-
return
|
|
597
|
+
return path2.join(cacheDir(), EXTRACTION_CACHE_PREFIX + sessionId.replace(/[^a-zA-Z0-9-]/g, "_") + ".json");
|
|
367
598
|
}
|
|
368
599
|
function projectFingerprintFile(projectId) {
|
|
369
|
-
return
|
|
600
|
+
return path2.join(projectFingerprintDir(), projectId + ".json");
|
|
370
601
|
}
|
|
371
602
|
function compactionStateFile(projectId) {
|
|
372
|
-
return
|
|
603
|
+
return path2.join(compactionStateDir(), projectId.replace(/[^a-zA-Z0-9_-]/g, "_") + ".json");
|
|
373
604
|
}
|
|
374
605
|
function legacyScopedCompactionStateFile(projectId, sessionId) {
|
|
375
606
|
const project = projectId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
376
607
|
const session = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
377
|
-
return
|
|
608
|
+
return path2.join(compactionStateDir(), project, session + ".json");
|
|
378
609
|
}
|
|
379
610
|
function scopedCompactionStateFile(projectId, sessionId, branchHeadId) {
|
|
380
611
|
const project = projectId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
381
612
|
const session = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
382
613
|
const branch = branchHeadId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
383
|
-
return
|
|
614
|
+
return path2.join(compactionStateDir(), project, session, branch + ".json");
|
|
384
615
|
}
|
|
385
616
|
function remediationHintsFile(projectId) {
|
|
386
|
-
return
|
|
617
|
+
return path2.join(smartCompactCacheDir(), "remediation-" + projectId + ".json");
|
|
387
618
|
}
|
|
388
619
|
function metricsDashboardFile() {
|
|
389
|
-
return
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// src/utils/logger.ts
|
|
393
|
-
var DEBUG = process.env.DEBUG?.includes("smart-compact") ?? false;
|
|
394
|
-
function warn(msg, err) {
|
|
395
|
-
const detail = err instanceof Error ? err.message : err ?? "";
|
|
396
|
-
console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
|
|
397
|
-
}
|
|
398
|
-
function error(msg, err) {
|
|
399
|
-
const detail = err instanceof Error ? err.message + `
|
|
400
|
-
` + err.stack : err ?? "";
|
|
401
|
-
console.error(LOG_PREFIX + " " + msg + (detail ? ": " + detail : ""));
|
|
402
|
-
}
|
|
403
|
-
function info(msg, ...args) {
|
|
404
|
-
console.error(LOG_PREFIX + " [info] " + msg, ...args);
|
|
405
|
-
}
|
|
406
|
-
function debug(msg, ...args) {
|
|
407
|
-
if (DEBUG)
|
|
408
|
-
console.error(LOG_PREFIX + " [debug] " + msg, ...args);
|
|
409
|
-
}
|
|
410
|
-
function debugError(msg, err) {
|
|
411
|
-
if (DEBUG)
|
|
412
|
-
error(msg, err);
|
|
620
|
+
return path2.join(cacheDir(), "smart-compact-report.html");
|
|
413
621
|
}
|
|
414
622
|
|
|
415
623
|
// src/utils/config.ts
|
|
@@ -427,6 +635,7 @@ var VALID_THINKING_LEVELS = [
|
|
|
427
635
|
];
|
|
428
636
|
var BOOLEAN_KEYS = [
|
|
429
637
|
"autoTrigger",
|
|
638
|
+
"showStatus",
|
|
430
639
|
"backupEnabled",
|
|
431
640
|
"requireApproval",
|
|
432
641
|
"scrubSecrets",
|
|
@@ -454,14 +663,116 @@ var PROFILE_NUMERIC_KEYS = [
|
|
|
454
663
|
"singlePassMaxTokens",
|
|
455
664
|
"batchMaxTokens"
|
|
456
665
|
];
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
666
|
+
function readGlobalConfigValue(configPath) {
|
|
667
|
+
assertGlobalConfigPath(configPath);
|
|
668
|
+
try {
|
|
669
|
+
const section = configuredSection(readSettingsRoot(settingsFile()));
|
|
670
|
+
validateSmartCompactConfig(section);
|
|
671
|
+
return cloneGlobalConfigValue(configPathValue(section, configPath));
|
|
672
|
+
} catch {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function isRecord(value) {
|
|
677
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
678
|
+
}
|
|
679
|
+
function cloneProfiles(profiles) {
|
|
680
|
+
return Object.fromEntries(VALID_PROFILES.map((name) => [name, { ...profiles[name] }]));
|
|
681
|
+
}
|
|
682
|
+
function cloneConfig(config) {
|
|
683
|
+
return {
|
|
684
|
+
...config,
|
|
685
|
+
profiles: cloneProfiles(config.profiles),
|
|
686
|
+
pinPaths: [...config.pinPaths]
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function defaultConfig() {
|
|
690
|
+
return cloneConfig({
|
|
691
|
+
...DEFAULT_CONFIG,
|
|
692
|
+
backupDir: defaultBackupDir()
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
function cloneGlobalConfigValue(value) {
|
|
696
|
+
return Array.isArray(value) ? [...value] : value;
|
|
697
|
+
}
|
|
698
|
+
function readSettingsRoot(file) {
|
|
699
|
+
if (!fs2.existsSync(file))
|
|
700
|
+
return {};
|
|
701
|
+
let parsed;
|
|
702
|
+
try {
|
|
703
|
+
parsed = JSON.parse(fs2.readFileSync(file, "utf8"));
|
|
704
|
+
} catch {
|
|
705
|
+
throw new Error("settings.json must contain valid JSON");
|
|
706
|
+
}
|
|
707
|
+
if (!isRecord(parsed)) {
|
|
708
|
+
throw new Error("settings.json root must be an object");
|
|
709
|
+
}
|
|
710
|
+
return parsed;
|
|
711
|
+
}
|
|
712
|
+
function configuredSection(root) {
|
|
713
|
+
const selected = Object.hasOwn(root, CONFIG_KEY) ? root[CONFIG_KEY] : root[CONFIG_KEY_ALT] ?? {};
|
|
714
|
+
if (!isRecord(selected)) {
|
|
715
|
+
throw new Error("smartCompact must be an object");
|
|
716
|
+
}
|
|
717
|
+
return structuredClone(selected);
|
|
718
|
+
}
|
|
719
|
+
function deleteEmptyProfileContainers(section, profile) {
|
|
720
|
+
if (!isRecord(section.profiles))
|
|
721
|
+
return;
|
|
722
|
+
if (isRecord(section.profiles[profile])) {
|
|
723
|
+
const values = section.profiles[profile];
|
|
724
|
+
if (Object.keys(values).length === 0)
|
|
725
|
+
delete section.profiles[profile];
|
|
726
|
+
}
|
|
727
|
+
if (Object.keys(section.profiles).length === 0)
|
|
728
|
+
delete section.profiles;
|
|
729
|
+
}
|
|
730
|
+
function setConfigPath(section, configPath, value) {
|
|
731
|
+
const parts = configPath.split(".");
|
|
732
|
+
if (parts[0] !== "profiles") {
|
|
733
|
+
if (value === undefined)
|
|
734
|
+
delete section[configPath];
|
|
735
|
+
else
|
|
736
|
+
section[configPath] = value;
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const [, profile, key] = parts;
|
|
740
|
+
if (!isRecord(section.profiles))
|
|
741
|
+
section.profiles = {};
|
|
742
|
+
const profiles = section.profiles;
|
|
743
|
+
if (!isRecord(profiles[profile]))
|
|
744
|
+
profiles[profile] = {};
|
|
745
|
+
const values = profiles[profile];
|
|
746
|
+
if (value === undefined)
|
|
747
|
+
delete values[key];
|
|
748
|
+
else
|
|
749
|
+
values[key] = value;
|
|
750
|
+
deleteEmptyProfileContainers(section, profile);
|
|
751
|
+
}
|
|
752
|
+
function assertGlobalConfigPath(configPath) {
|
|
753
|
+
if (configPath !== "profiles" && Object.hasOwn(DEFAULT_CONFIG, configPath)) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const parts = configPath.split(".");
|
|
757
|
+
if (parts.length === 3 && parts[0] === "profiles" && VALID_PROFILES.includes(parts[1]) && PROFILE_NUMERIC_KEYS.includes(parts[2])) {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
throw new Error(`Unknown smartCompact setting path: ${configPath}`);
|
|
761
|
+
}
|
|
762
|
+
function configPathValue(section, configPath) {
|
|
763
|
+
const parts = configPath.split(".");
|
|
764
|
+
if (parts[0] !== "profiles") {
|
|
765
|
+
return section[configPath];
|
|
766
|
+
}
|
|
767
|
+
const [, profile, key] = parts;
|
|
768
|
+
if (!isRecord(section.profiles))
|
|
769
|
+
return;
|
|
770
|
+
const values = section.profiles[profile];
|
|
771
|
+
return isRecord(values) ? values[key] : undefined;
|
|
772
|
+
}
|
|
773
|
+
function sameJsonValue(left, right) {
|
|
774
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
775
|
+
}
|
|
465
776
|
function discard(sc, key, message) {
|
|
466
777
|
warn(message);
|
|
467
778
|
delete sc[key];
|
|
@@ -511,7 +822,7 @@ function validateBasicFields(sc) {
|
|
|
511
822
|
function validateProfiles(sc) {
|
|
512
823
|
if (!("profiles" in sc))
|
|
513
824
|
return;
|
|
514
|
-
if (
|
|
825
|
+
if (!isRecord(sc.profiles)) {
|
|
515
826
|
discard(sc, "profiles", "smart-compact config: profiles must be an object, got " + typeof sc.profiles);
|
|
516
827
|
return;
|
|
517
828
|
}
|
|
@@ -521,7 +832,7 @@ function validateProfiles(sc) {
|
|
|
521
832
|
discard(profiles, profileName, "smart-compact config: ignoring unknown profile override '" + profileName + "'.");
|
|
522
833
|
continue;
|
|
523
834
|
}
|
|
524
|
-
if (
|
|
835
|
+
if (!isRecord(value)) {
|
|
525
836
|
discard(profiles, profileName, "smart-compact config: profile '" + profileName + "' must be an object.");
|
|
526
837
|
continue;
|
|
527
838
|
}
|
|
@@ -545,35 +856,39 @@ function validateProfiles(sc) {
|
|
|
545
856
|
}
|
|
546
857
|
}
|
|
547
858
|
}
|
|
859
|
+
function validNumericLimit(key, value) {
|
|
860
|
+
const limit = CONFIG_NUMERIC_LIMITS[key];
|
|
861
|
+
return Number.isFinite(value) && (!limit.integer || Number.isSafeInteger(value)) && (value >= limit.min && value <= limit.max || ("zeroOrRange" in limit) && limit.zeroOrRange && value === 0);
|
|
862
|
+
}
|
|
548
863
|
var NUMERIC_RULES = [
|
|
549
864
|
{
|
|
550
865
|
key: "autoTriggerTimeoutMs",
|
|
551
|
-
valid: (value) =>
|
|
866
|
+
valid: (value) => validNumericLimit("autoTriggerTimeoutMs", value),
|
|
552
867
|
message: (value) => "smart-compact config: autoTriggerTimeoutMs must be 1000\u2013300000, got " + value + ". Using default " + DEFAULT_CONFIG.autoTriggerTimeoutMs + "ms."
|
|
553
868
|
},
|
|
554
869
|
{
|
|
555
870
|
key: "maxLlmCalls",
|
|
556
|
-
valid: (value) =>
|
|
871
|
+
valid: (value) => validNumericLimit("maxLlmCalls", value),
|
|
557
872
|
message: () => "smart-compact config: maxLlmCalls must be 0\u2013100; 0 uses the selected mode cap."
|
|
558
873
|
},
|
|
559
874
|
{
|
|
560
875
|
key: "maxLlmInputTokens",
|
|
561
|
-
valid: (value) =>
|
|
876
|
+
valid: (value) => validNumericLimit("maxLlmInputTokens", value),
|
|
562
877
|
message: () => "smart-compact config: maxLlmInputTokens must be 0\u20131000000; 0 uses the mode cap."
|
|
563
878
|
},
|
|
564
879
|
{
|
|
565
880
|
key: "codexMaxCallMs",
|
|
566
|
-
valid: (value) =>
|
|
881
|
+
valid: (value) => validNumericLimit("codexMaxCallMs", value),
|
|
567
882
|
message: () => "smart-compact config: codexMaxCallMs must be 0 or 5000\u2013300000; 0 derives a cap from maxTokens."
|
|
568
883
|
},
|
|
569
884
|
{
|
|
570
885
|
key: "maxLatencyMs",
|
|
571
|
-
valid: (value) =>
|
|
886
|
+
valid: (value) => validNumericLimit("maxLatencyMs", value),
|
|
572
887
|
message: () => "smart-compact config: maxLatencyMs must be 0 or 5000\u2013600000; 0 means unlimited."
|
|
573
888
|
},
|
|
574
889
|
{
|
|
575
890
|
key: "minContextPercent",
|
|
576
|
-
valid: (value) =>
|
|
891
|
+
valid: (value) => validNumericLimit("minContextPercent", value),
|
|
577
892
|
message: (value) => "smart-compact config: minContextPercent must be 0\u2013100, got " + value + ". Using default " + DEFAULT_CONFIG.minContextPercent + "."
|
|
578
893
|
}
|
|
579
894
|
];
|
|
@@ -598,39 +913,78 @@ function validateSmartCompactConfig(sc) {
|
|
|
598
913
|
validateProfiles(sc);
|
|
599
914
|
validateLimits(sc);
|
|
600
915
|
}
|
|
916
|
+
async function writeGlobalConfigValue(configPath, value) {
|
|
917
|
+
assertGlobalConfigPath(configPath);
|
|
918
|
+
const file = settingsFile();
|
|
919
|
+
const release = await acquireLock(file);
|
|
920
|
+
try {
|
|
921
|
+
const root = readSettingsRoot(file);
|
|
922
|
+
const section = configuredSection(root);
|
|
923
|
+
setConfigPath(section, configPath, cloneGlobalConfigValue(value));
|
|
924
|
+
if (value === undefined && configPath === "agentToolAccess") {
|
|
925
|
+
delete section.agentToolEnabled;
|
|
926
|
+
}
|
|
927
|
+
const validated = structuredClone(section);
|
|
928
|
+
validateSmartCompactConfig(validated);
|
|
929
|
+
if (!sameJsonValue(configPathValue(validated, configPath), value)) {
|
|
930
|
+
throw new Error(`Invalid smartCompact setting: ${configPath}`);
|
|
931
|
+
}
|
|
932
|
+
root[CONFIG_KEY] = section;
|
|
933
|
+
await atomicWriteFile(file, JSON.stringify(root, null, 2) + `
|
|
934
|
+
`);
|
|
935
|
+
resetConfigCache();
|
|
936
|
+
return loadConfig();
|
|
937
|
+
} finally {
|
|
938
|
+
release();
|
|
939
|
+
}
|
|
940
|
+
}
|
|
601
941
|
var cachedConfig = null;
|
|
602
942
|
var cachedMtime = 0;
|
|
603
943
|
var cachedPath = null;
|
|
944
|
+
function resetConfigCache() {
|
|
945
|
+
cachedConfig = null;
|
|
946
|
+
cachedMtime = 0;
|
|
947
|
+
cachedPath = null;
|
|
948
|
+
}
|
|
604
949
|
function loadConfig() {
|
|
605
950
|
try {
|
|
606
951
|
const file = settingsFile();
|
|
607
|
-
const stat =
|
|
952
|
+
const stat = fs2.statSync(file);
|
|
608
953
|
if (cachedConfig && cachedPath === file && stat.mtimeMs === cachedMtime)
|
|
609
|
-
return cachedConfig;
|
|
610
|
-
const
|
|
611
|
-
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
|
+
}
|
|
612
965
|
validateSmartCompactConfig(sc);
|
|
613
|
-
const merged = { ...
|
|
966
|
+
const merged = { ...defaultConfig(), ...sc };
|
|
614
967
|
if (!("mode" in sc) && "profile" in sc) {
|
|
615
968
|
merged.mode = sc.profile === "light" ? "thorough" : sc.profile;
|
|
616
969
|
}
|
|
617
970
|
if (sc.profiles) {
|
|
618
|
-
|
|
971
|
+
const overrides = sc.profiles;
|
|
972
|
+
merged.profiles = Object.fromEntries(VALID_PROFILES.map((name) => [
|
|
973
|
+
name,
|
|
974
|
+
{ ...PROFILES[name], ...overrides[name] }
|
|
975
|
+
]));
|
|
619
976
|
}
|
|
620
977
|
if (!merged.backupDir)
|
|
621
978
|
merged.backupDir = defaultBackupDir();
|
|
622
979
|
cachedConfig = merged;
|
|
623
980
|
cachedMtime = stat.mtimeMs;
|
|
624
981
|
cachedPath = file;
|
|
625
|
-
return cachedConfig;
|
|
982
|
+
return cloneConfig(cachedConfig);
|
|
626
983
|
} catch (error2) {
|
|
627
984
|
debug("loadConfig: settings.json not found or unreadable, using defaults", error2);
|
|
628
|
-
cachedConfig =
|
|
629
|
-
...DEFAULT_CONFIG,
|
|
630
|
-
backupDir: defaultBackupDir()
|
|
631
|
-
};
|
|
985
|
+
cachedConfig = defaultConfig();
|
|
632
986
|
cachedPath = null;
|
|
633
|
-
return cachedConfig;
|
|
987
|
+
return cloneConfig(cachedConfig);
|
|
634
988
|
}
|
|
635
989
|
}
|
|
636
990
|
|
|
@@ -933,17 +1287,17 @@ import fs3 from "fs";
|
|
|
933
1287
|
import path4 from "path";
|
|
934
1288
|
|
|
935
1289
|
// src/utils/extraction.ts
|
|
936
|
-
import
|
|
1290
|
+
import path3 from "path";
|
|
937
1291
|
|
|
938
1292
|
// src/utils/type-guards.ts
|
|
939
|
-
function
|
|
1293
|
+
function isRecord2(value) {
|
|
940
1294
|
return typeof value === "object" && value !== null;
|
|
941
1295
|
}
|
|
942
1296
|
function isTextBlock(c) {
|
|
943
|
-
return
|
|
1297
|
+
return isRecord2(c) && c.type === "text" && typeof c.text === "string";
|
|
944
1298
|
}
|
|
945
1299
|
function isToolCallBlock(c) {
|
|
946
|
-
return
|
|
1300
|
+
return isRecord2(c) && c.type === "toolCall" && typeof c.name === "string" && isRecord2(c.arguments);
|
|
947
1301
|
}
|
|
948
1302
|
function getToolCallNames(content) {
|
|
949
1303
|
if (!Array.isArray(content))
|
|
@@ -1119,8 +1473,8 @@ function isKnownPathReference(ref, knownPaths) {
|
|
|
1119
1473
|
if (!normalizedRef)
|
|
1120
1474
|
return false;
|
|
1121
1475
|
const pathShaped = normalizedRef.includes("/");
|
|
1122
|
-
return knownPaths.some((
|
|
1123
|
-
const normalizedPath = normalizePath(
|
|
1476
|
+
return knownPaths.some((path3) => {
|
|
1477
|
+
const normalizedPath = normalizePath(path3).replace(/^\/+/, "");
|
|
1124
1478
|
if (normalizedPath === normalizedRef || normalizedPath.endsWith("/" + normalizedRef))
|
|
1125
1479
|
return true;
|
|
1126
1480
|
if (normalizedPath.endsWith(normalizedRef)) {
|
|
@@ -1471,15 +1825,15 @@ function smartKeepBoundaryCandidates(msgs, keepFromIndex, branchEntries) {
|
|
|
1471
1825
|
}
|
|
1472
1826
|
function collectToolCallIds(blocks, msgIndex, out) {
|
|
1473
1827
|
for (const block of blocks) {
|
|
1474
|
-
if (!
|
|
1828
|
+
if (!isRecord2(block) || block.type !== "toolCall")
|
|
1475
1829
|
continue;
|
|
1476
1830
|
if (typeof block.id === "string")
|
|
1477
1831
|
out.set(block.id, msgIndex);
|
|
1478
1832
|
const args = block.arguments;
|
|
1479
|
-
if (block.name !== "multi_tool_use.parallel" || !
|
|
1833
|
+
if (block.name !== "multi_tool_use.parallel" || !isRecord2(args) || !Array.isArray(args.tool_uses))
|
|
1480
1834
|
continue;
|
|
1481
1835
|
for (const nested of args.tool_uses) {
|
|
1482
|
-
if (
|
|
1836
|
+
if (isRecord2(nested) && typeof nested.id === "string")
|
|
1483
1837
|
out.set(nested.id, msgIndex);
|
|
1484
1838
|
}
|
|
1485
1839
|
}
|
|
@@ -1488,7 +1842,7 @@ function buildToolCallBoundaryIndex(msgs) {
|
|
|
1488
1842
|
const map = new Map;
|
|
1489
1843
|
for (let i = 0;i < msgs.length; i++) {
|
|
1490
1844
|
const message = msgs[i].message;
|
|
1491
|
-
if (!
|
|
1845
|
+
if (!isRecord2(message) || message.role !== "assistant")
|
|
1492
1846
|
continue;
|
|
1493
1847
|
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
1494
1848
|
collectToolCallIds(blocks, i, map);
|
|
@@ -1510,7 +1864,7 @@ function guardToolCallBoundary(msgs, keepFrom, tcMap = buildToolCallBoundaryInde
|
|
|
1510
1864
|
changed = false;
|
|
1511
1865
|
for (let i = adjusted;i < msgs.length; i++) {
|
|
1512
1866
|
const message = msgs[i].message;
|
|
1513
|
-
if (!
|
|
1867
|
+
if (!isRecord2(message) || message.role !== "toolResult")
|
|
1514
1868
|
continue;
|
|
1515
1869
|
const tcId = typeof message.toolCallId === "string" ? message.toolCallId : undefined;
|
|
1516
1870
|
if (!tcId)
|
|
@@ -1533,7 +1887,7 @@ function advancePastToolCallBoundary(msgs, keepFrom, tcMap = buildToolCallBounda
|
|
|
1533
1887
|
let next = adjusted;
|
|
1534
1888
|
for (let i = adjusted;i < msgs.length; i++) {
|
|
1535
1889
|
const message = msgs[i].message;
|
|
1536
|
-
if (!
|
|
1890
|
+
if (!isRecord2(message) || message.role !== "toolResult")
|
|
1537
1891
|
continue;
|
|
1538
1892
|
const tcIdx = typeof message.toolCallId === "string" ? tcMap.get(message.toolCallId) : undefined;
|
|
1539
1893
|
if (i === adjusted && tcIdx === undefined || tcIdx !== undefined && tcIdx < adjusted) {
|
|
@@ -1861,7 +2215,7 @@ function buildSummaryPathEvidence(paths, budgetTokens = PROFILES.balanced.summar
|
|
|
1861
2215
|
const unique = Array.from(new Set(paths.filter(Boolean)));
|
|
1862
2216
|
if (!unique.length)
|
|
1863
2217
|
return new Map;
|
|
1864
|
-
const full = unique.map((
|
|
2218
|
+
const full = unique.map((path3) => [path3, summaryPathLine(path3)]);
|
|
1865
2219
|
const minimumPerLine = JSON.stringify("#" + "x".repeat(12)).length + 3;
|
|
1866
2220
|
const budgetChars = Math.max(unique.length * minimumPerLine, Math.min(20000, Math.max(4000, Math.floor(budgetTokens * 2))));
|
|
1867
2221
|
if (full.reduce((total, [, line]) => total + line.length + 3, 0) <= budgetChars) {
|
|
@@ -1869,21 +2223,21 @@ function buildSummaryPathEvidence(paths, budgetTokens = PROFILES.balanced.summar
|
|
|
1869
2223
|
}
|
|
1870
2224
|
const digests = new Map;
|
|
1871
2225
|
const owners = new Map;
|
|
1872
|
-
for (const
|
|
1873
|
-
const fullDigest = createHash("sha256").update(
|
|
2226
|
+
for (const path3 of unique) {
|
|
2227
|
+
const fullDigest = createHash("sha256").update(path3).digest("base64url");
|
|
1874
2228
|
let digest = fullDigest.slice(0, 12);
|
|
1875
2229
|
const owner = owners.get(digest);
|
|
1876
|
-
if (owner && owner !==
|
|
2230
|
+
if (owner && owner !== path3) {
|
|
1877
2231
|
digest = fullDigest;
|
|
1878
2232
|
digests.set(owner, createHash("sha256").update(owner).digest("base64url"));
|
|
1879
2233
|
}
|
|
1880
|
-
owners.set(digest,
|
|
1881
|
-
digests.set(
|
|
2234
|
+
owners.set(digest, path3);
|
|
2235
|
+
digests.set(path3, digest);
|
|
1882
2236
|
}
|
|
1883
2237
|
const perPath = Math.max(JSON.stringify("#" + "x".repeat(12)).length, Math.floor((budgetChars - unique.length * 3) / unique.length));
|
|
1884
|
-
return new Map(unique.map((
|
|
1885
|
-
|
|
1886
|
-
compactPathLine(
|
|
2238
|
+
return new Map(unique.map((path3) => [
|
|
2239
|
+
path3,
|
|
2240
|
+
compactPathLine(path3, perPath, digests.get(path3) ?? "")
|
|
1887
2241
|
]));
|
|
1888
2242
|
}
|
|
1889
2243
|
function mergeBodies(first, second) {
|
|
@@ -2401,7 +2755,7 @@ function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
|
|
|
2401
2755
|
const shiftBasename = (filePath) => {
|
|
2402
2756
|
if (!filePath)
|
|
2403
2757
|
return null;
|
|
2404
|
-
const base =
|
|
2758
|
+
const base = path3.basename(filePath);
|
|
2405
2759
|
return GENERIC_BASENAMES.has(base.toLowerCase()) ? null : base;
|
|
2406
2760
|
};
|
|
2407
2761
|
const topics = [];
|
|
@@ -2555,7 +2909,7 @@ function extractOpenLoops(msgs, extraction) {
|
|
|
2555
2909
|
}));
|
|
2556
2910
|
for (const err of extraction.errors.filter((e) => !e.resolved)) {
|
|
2557
2911
|
const errLower = err.message.toLowerCase();
|
|
2558
|
-
const errFiles = fileNeedles.filter(({ needles }) => needles.some((n) => errLower.includes(n))).map(({ path:
|
|
2912
|
+
const errFiles = fileNeedles.filter(({ needles }) => needles.some((n) => errLower.includes(n))).map(({ path: path4 }) => path4);
|
|
2559
2913
|
loops.push({
|
|
2560
2914
|
id: ID_PREFIX.OPEN_LOOP + ++loopId,
|
|
2561
2915
|
type: "bugfix",
|
|
@@ -2704,191 +3058,6 @@ function extractStructured(msgs, pc, precomputedTcIdx) {
|
|
|
2704
3058
|
};
|
|
2705
3059
|
}
|
|
2706
3060
|
|
|
2707
|
-
// src/infra/fs.ts
|
|
2708
|
-
import fs2 from "fs";
|
|
2709
|
-
import fsp from "fs/promises";
|
|
2710
|
-
import path3 from "path";
|
|
2711
|
-
import crypto from "crypto";
|
|
2712
|
-
var LOCK_STALE_MS = 5000;
|
|
2713
|
-
var LOCK_RETRY_MS = 25;
|
|
2714
|
-
var LOCK_MAX_RETRIES = 80;
|
|
2715
|
-
function ensureDir(dir) {
|
|
2716
|
-
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2717
|
-
fs2.chmodSync(dir, 448);
|
|
2718
|
-
}
|
|
2719
|
-
async function ensureDirAsync(dir) {
|
|
2720
|
-
await fsp.mkdir(dir, { recursive: true, mode: 448 });
|
|
2721
|
-
await fsp.chmod(dir, 448);
|
|
2722
|
-
}
|
|
2723
|
-
function tempPath(target) {
|
|
2724
|
-
return target + ".tmp." + process.pid + "." + crypto.randomBytes(4).toString("hex");
|
|
2725
|
-
}
|
|
2726
|
-
function atomicWriteFileSync(target, data) {
|
|
2727
|
-
ensureDir(path3.dirname(target));
|
|
2728
|
-
const tmp = tempPath(target);
|
|
2729
|
-
try {
|
|
2730
|
-
fs2.writeFileSync(tmp, data, { mode: 384 });
|
|
2731
|
-
fs2.renameSync(tmp, target);
|
|
2732
|
-
fs2.chmodSync(target, 384);
|
|
2733
|
-
} catch (e) {
|
|
2734
|
-
try {
|
|
2735
|
-
fs2.unlinkSync(tmp);
|
|
2736
|
-
} catch {}
|
|
2737
|
-
throw e;
|
|
2738
|
-
}
|
|
2739
|
-
}
|
|
2740
|
-
async function atomicWriteFile(target, data) {
|
|
2741
|
-
await ensureDirAsync(path3.dirname(target));
|
|
2742
|
-
const tmp = tempPath(target);
|
|
2743
|
-
try {
|
|
2744
|
-
await fsp.writeFile(tmp, data, { mode: 384 });
|
|
2745
|
-
await fsp.rename(tmp, target);
|
|
2746
|
-
await fsp.chmod(target, 384);
|
|
2747
|
-
} catch (e) {
|
|
2748
|
-
try {
|
|
2749
|
-
await fsp.unlink(tmp);
|
|
2750
|
-
} catch {}
|
|
2751
|
-
throw e;
|
|
2752
|
-
}
|
|
2753
|
-
}
|
|
2754
|
-
function tryAcquireLock(target) {
|
|
2755
|
-
const lockDir = target + ".lock";
|
|
2756
|
-
for (let reclaimAttempt = 0;reclaimAttempt < 2; reclaimAttempt++) {
|
|
2757
|
-
try {
|
|
2758
|
-
fs2.mkdirSync(lockDir, { mode: 448 });
|
|
2759
|
-
return () => {
|
|
2760
|
-
try {
|
|
2761
|
-
fs2.rmdirSync(lockDir);
|
|
2762
|
-
} catch {}
|
|
2763
|
-
};
|
|
2764
|
-
} catch (error2) {
|
|
2765
|
-
if (error2?.code !== "EEXIST") {
|
|
2766
|
-
throw new Error("Failed to acquire lock for " + target, { cause: error2 });
|
|
2767
|
-
}
|
|
2768
|
-
try {
|
|
2769
|
-
const stat = fs2.statSync(lockDir);
|
|
2770
|
-
if (Date.now() - stat.mtimeMs <= LOCK_STALE_MS)
|
|
2771
|
-
return null;
|
|
2772
|
-
const stolen = lockDir + ".stale." + process.pid + "." + crypto.randomBytes(4).toString("hex");
|
|
2773
|
-
fs2.renameSync(lockDir, stolen);
|
|
2774
|
-
const stolenStat = fs2.statSync(stolen);
|
|
2775
|
-
if (Date.now() - stolenStat.mtimeMs > LOCK_STALE_MS)
|
|
2776
|
-
fs2.rmdirSync(stolen);
|
|
2777
|
-
else
|
|
2778
|
-
try {
|
|
2779
|
-
fs2.renameSync(stolen, lockDir);
|
|
2780
|
-
} catch {}
|
|
2781
|
-
} catch {}
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
return null;
|
|
2785
|
-
}
|
|
2786
|
-
function acquireLockSync(target) {
|
|
2787
|
-
const release = tryAcquireLock(target);
|
|
2788
|
-
if (!release)
|
|
2789
|
-
throw new Error("Lock busy for " + target);
|
|
2790
|
-
return release;
|
|
2791
|
-
}
|
|
2792
|
-
async function acquireLock(target) {
|
|
2793
|
-
for (let attempt = 0;attempt < LOCK_MAX_RETRIES; attempt++) {
|
|
2794
|
-
const release = tryAcquireLock(target);
|
|
2795
|
-
if (release)
|
|
2796
|
-
return release;
|
|
2797
|
-
const delay = Promise.withResolvers();
|
|
2798
|
-
setTimeout(delay.resolve, LOCK_RETRY_MS);
|
|
2799
|
-
await delay.promise;
|
|
2800
|
-
}
|
|
2801
|
-
throw new Error("Timed out acquiring lock for " + target);
|
|
2802
|
-
}
|
|
2803
|
-
async function appendLineLockedAsync(target, line, maxBytes) {
|
|
2804
|
-
await ensureDirAsync(path3.dirname(target));
|
|
2805
|
-
const payload = Buffer.from(line.endsWith(`
|
|
2806
|
-
`) ? line : line + `
|
|
2807
|
-
`);
|
|
2808
|
-
if (maxBytes !== undefined && (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)) {
|
|
2809
|
-
throw new Error("maxBytes must be a positive safe integer");
|
|
2810
|
-
}
|
|
2811
|
-
if (maxBytes !== undefined && payload.length > maxBytes) {
|
|
2812
|
-
throw new Error("Log entry exceeds retention cap for " + target);
|
|
2813
|
-
}
|
|
2814
|
-
const release = await acquireLock(target);
|
|
2815
|
-
try {
|
|
2816
|
-
let stat = null;
|
|
2817
|
-
try {
|
|
2818
|
-
stat = await fsp.stat(target);
|
|
2819
|
-
} catch (error2) {
|
|
2820
|
-
if (!error2 || typeof error2 !== "object" || !("code" in error2) || error2.code !== "ENOENT")
|
|
2821
|
-
throw error2;
|
|
2822
|
-
}
|
|
2823
|
-
if (maxBytes !== undefined && stat && stat.size + payload.length > maxBytes) {
|
|
2824
|
-
const retainedLength = Math.min(stat.size, Math.max(0, maxBytes - payload.length));
|
|
2825
|
-
const buffer = Buffer.allocUnsafe(retainedLength);
|
|
2826
|
-
if (retainedLength > 0) {
|
|
2827
|
-
const handle = await fsp.open(target, "r");
|
|
2828
|
-
try {
|
|
2829
|
-
await handle.read(buffer, 0, retainedLength, stat.size - retainedLength);
|
|
2830
|
-
} finally {
|
|
2831
|
-
await handle.close();
|
|
2832
|
-
}
|
|
2833
|
-
}
|
|
2834
|
-
let tail = buffer.toString("utf8");
|
|
2835
|
-
if (retainedLength < stat.size) {
|
|
2836
|
-
const firstNewline = tail.indexOf(`
|
|
2837
|
-
`);
|
|
2838
|
-
tail = firstNewline >= 0 ? tail.slice(firstNewline + 1) : "";
|
|
2839
|
-
}
|
|
2840
|
-
await atomicWriteFile(target, tail);
|
|
2841
|
-
}
|
|
2842
|
-
await fsp.appendFile(target, payload, { mode: 384 });
|
|
2843
|
-
await fsp.chmod(target, 384);
|
|
2844
|
-
} finally {
|
|
2845
|
-
release();
|
|
2846
|
-
}
|
|
2847
|
-
}
|
|
2848
|
-
function readJsonlTail(target, limit, maxBytes = 512 * 1024) {
|
|
2849
|
-
if (limit <= 0 || !fs2.existsSync(target))
|
|
2850
|
-
return [];
|
|
2851
|
-
const stat = fs2.statSync(target);
|
|
2852
|
-
const length = Math.min(stat.size, maxBytes);
|
|
2853
|
-
const buffer = Buffer.alloc(length);
|
|
2854
|
-
const fd = fs2.openSync(target, "r");
|
|
2855
|
-
try {
|
|
2856
|
-
fs2.readSync(fd, buffer, 0, length, stat.size - length);
|
|
2857
|
-
} finally {
|
|
2858
|
-
fs2.closeSync(fd);
|
|
2859
|
-
}
|
|
2860
|
-
let text = buffer.toString("utf8");
|
|
2861
|
-
if (stat.size > length) {
|
|
2862
|
-
const newline = text.indexOf(`
|
|
2863
|
-
`);
|
|
2864
|
-
text = newline >= 0 ? text.slice(newline + 1) : "";
|
|
2865
|
-
}
|
|
2866
|
-
const values = [];
|
|
2867
|
-
for (const line of text.split(`
|
|
2868
|
-
`)) {
|
|
2869
|
-
if (!line)
|
|
2870
|
-
continue;
|
|
2871
|
-
try {
|
|
2872
|
-
values.push(JSON.parse(line));
|
|
2873
|
-
} catch {}
|
|
2874
|
-
}
|
|
2875
|
-
return values.slice(-limit);
|
|
2876
|
-
}
|
|
2877
|
-
function readJsonSync(target) {
|
|
2878
|
-
try {
|
|
2879
|
-
if (!fs2.existsSync(target))
|
|
2880
|
-
return null;
|
|
2881
|
-
const raw = fs2.readFileSync(target, "utf8");
|
|
2882
|
-
return JSON.parse(raw);
|
|
2883
|
-
} catch (e) {
|
|
2884
|
-
warn("readJsonSync failed for " + target, e);
|
|
2885
|
-
return null;
|
|
2886
|
-
}
|
|
2887
|
-
}
|
|
2888
|
-
function writeJsonSync(target, value, pretty = false) {
|
|
2889
|
-
atomicWriteFileSync(target, pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value));
|
|
2890
|
-
}
|
|
2891
|
-
|
|
2892
3061
|
// src/utils/id-fingerprint.ts
|
|
2893
3062
|
import crypto2 from "crypto";
|
|
2894
3063
|
var FINGERPRINT_TAIL_LEN = 16;
|
|
@@ -4826,7 +4995,7 @@ function advancePastNonPortableToolExchanges(msgs, keepFrom, toolCallIndex) {
|
|
|
4826
4995
|
const exchangeEnds = new Map;
|
|
4827
4996
|
for (let index = keepFrom;index < msgs.length; index++) {
|
|
4828
4997
|
const message = msgs[index].message;
|
|
4829
|
-
if (!
|
|
4998
|
+
if (!isRecord2(message) || message.role !== "toolResult" || typeof message.toolCallId !== "string")
|
|
4830
4999
|
continue;
|
|
4831
5000
|
const callIndex = toolCallIndex.get(message.toolCallId);
|
|
4832
5001
|
if (callIndex === undefined || callIndex < keepFrom)
|
|
@@ -4836,10 +5005,10 @@ function advancePastNonPortableToolExchanges(msgs, keepFrom, toolCallIndex) {
|
|
|
4836
5005
|
let adjusted = keepFrom;
|
|
4837
5006
|
for (let index = keepFrom;index < msgs.length; index++) {
|
|
4838
5007
|
const message = msgs[index].message;
|
|
4839
|
-
if (!
|
|
5008
|
+
if (!isRecord2(message))
|
|
4840
5009
|
continue;
|
|
4841
5010
|
if (message.role === "assistant" && Array.isArray(message.content)) {
|
|
4842
|
-
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)));
|
|
4843
5012
|
if (nonPortable)
|
|
4844
5013
|
adjusted = Math.max(adjusted, exchangeEnds.get(index) ?? index + 1);
|
|
4845
5014
|
} else if (message.role === "toolResult" && typeof message.toolName === "string" && !PORTABLE_TOOL_NAME_RE.test(message.toolName)) {
|
|
@@ -4906,7 +5075,7 @@ function planCompactionWindow(input) {
|
|
|
4906
5075
|
let protectedUserIndex;
|
|
4907
5076
|
for (let index = msgs.length - 1;index >= 0; index--) {
|
|
4908
5077
|
const message = msgs[index].message;
|
|
4909
|
-
if (!
|
|
5078
|
+
if (!isRecord2(message) || message.role !== "user")
|
|
4910
5079
|
continue;
|
|
4911
5080
|
userOrdinal++;
|
|
4912
5081
|
protectedUserIndex = index;
|
|
@@ -4937,7 +5106,7 @@ function planCompactionWindow(input) {
|
|
|
4937
5106
|
const targetAfterTokens = !force && modelContextWindow ? modelContextWindow * targetPercent / 100 : fixedContextTokens + effectiveRetentionCeiling + finalSummaryAllowance;
|
|
4938
5107
|
let reason = "viable";
|
|
4939
5108
|
const firstKeptMessage = msgs[keepFrom]?.message;
|
|
4940
|
-
if (nonPortableTailBlocked ||
|
|
5109
|
+
if (nonPortableTailBlocked || isRecord2(firstKeptMessage) && firstKeptMessage.role === "toolResult")
|
|
4941
5110
|
reason = "unsafe-tool-boundary";
|
|
4942
5111
|
else if (keepFrom <= 0)
|
|
4943
5112
|
reason = "no-eligible-prefix";
|
|
@@ -11418,6 +11587,19 @@ async function runSmartCompact(opts) {
|
|
|
11418
11587
|
}
|
|
11419
11588
|
}
|
|
11420
11589
|
|
|
11590
|
+
// src/app/global-settings-runtime.ts
|
|
11591
|
+
var POLICY_PATHS = new Set([
|
|
11592
|
+
"agentToolAccess",
|
|
11593
|
+
"autoTrigger",
|
|
11594
|
+
"showStatus"
|
|
11595
|
+
]);
|
|
11596
|
+
function applyGlobalSettingRuntime(path14, ctx, policy, contextTools) {
|
|
11597
|
+
if (POLICY_PATHS.has(path14))
|
|
11598
|
+
policy.restore(ctx);
|
|
11599
|
+
if (path14 === "contextGraphEnabled")
|
|
11600
|
+
contextTools.apply();
|
|
11601
|
+
}
|
|
11602
|
+
|
|
11421
11603
|
// src/app/pending-slot.ts
|
|
11422
11604
|
function createPendingSlot(opts) {
|
|
11423
11605
|
const ttlMs = opts.ttlMs;
|
|
@@ -11833,6 +12015,7 @@ function resolveGraphScope(ctx) {
|
|
|
11833
12015
|
};
|
|
11834
12016
|
}
|
|
11835
12017
|
function registerContextTools(pi) {
|
|
12018
|
+
const availability = createContextToolAvailability(pi);
|
|
11836
12019
|
pi.registerTool({
|
|
11837
12020
|
name: "smart_recall",
|
|
11838
12021
|
label: "Smart Recall",
|
|
@@ -12037,12 +12220,48 @@ Paths: ` + relatedPaths.join(", ") : ""));
|
|
|
12037
12220
|
details: { memory, redactions: scrubber.count() }
|
|
12038
12221
|
};
|
|
12039
12222
|
} catch (error2) {
|
|
12040
|
-
debugError("Project memory persistence failed", error2);
|
|
12041
|
-
const message = scrubber.scrubText(error2 instanceof Error ? error2.message : String(error2)).value;
|
|
12042
|
-
throw new Error("Project memory could not be saved: " + message);
|
|
12223
|
+
debugError("Project memory persistence failed", error2);
|
|
12224
|
+
const message = scrubber.scrubText(error2 instanceof Error ? error2.message : String(error2)).value;
|
|
12225
|
+
throw new Error("Project memory could not be saved: " + message);
|
|
12226
|
+
}
|
|
12227
|
+
}
|
|
12228
|
+
});
|
|
12229
|
+
availability.apply();
|
|
12230
|
+
pi.on("session_start", availability.apply);
|
|
12231
|
+
return availability;
|
|
12232
|
+
}
|
|
12233
|
+
var CONTEXT_TOOL_NAMES = ["smart_recall", "smart_save_memory"];
|
|
12234
|
+
function createContextToolAvailability(pi) {
|
|
12235
|
+
const hiddenByConfig = new Set;
|
|
12236
|
+
let disabledByConfig = false;
|
|
12237
|
+
return {
|
|
12238
|
+
apply() {
|
|
12239
|
+
try {
|
|
12240
|
+
const enabled = loadConfig().contextGraphEnabled;
|
|
12241
|
+
const active = pi.getActiveTools();
|
|
12242
|
+
if (!enabled) {
|
|
12243
|
+
const visibleContextTools = CONTEXT_TOOL_NAMES.filter((name) => active.includes(name));
|
|
12244
|
+
for (const name of visibleContextTools)
|
|
12245
|
+
hiddenByConfig.add(name);
|
|
12246
|
+
disabledByConfig = true;
|
|
12247
|
+
if (visibleContextTools.length > 0) {
|
|
12248
|
+
pi.setActiveTools(active.filter((name) => !CONTEXT_TOOL_NAMES.includes(name)));
|
|
12249
|
+
}
|
|
12250
|
+
return;
|
|
12251
|
+
}
|
|
12252
|
+
if (disabledByConfig) {
|
|
12253
|
+
const restored = [...hiddenByConfig].filter((name) => !active.includes(name));
|
|
12254
|
+
if (restored.length > 0) {
|
|
12255
|
+
pi.setActiveTools([...new Set([...active, ...restored])]);
|
|
12256
|
+
}
|
|
12257
|
+
hiddenByConfig.clear();
|
|
12258
|
+
disabledByConfig = false;
|
|
12259
|
+
}
|
|
12260
|
+
} catch (error2) {
|
|
12261
|
+
debugError("Context tool availability update failed", error2);
|
|
12043
12262
|
}
|
|
12044
12263
|
}
|
|
12045
|
-
}
|
|
12264
|
+
};
|
|
12046
12265
|
}
|
|
12047
12266
|
|
|
12048
12267
|
// src/app/model-routing.ts
|
|
@@ -13226,71 +13445,874 @@ async function showMetricsDashboardUI(ctx, opts) {
|
|
|
13226
13445
|
|
|
13227
13446
|
// src/ui/settings-overlay.ts
|
|
13228
13447
|
import {
|
|
13229
|
-
getSettingsListTheme
|
|
13448
|
+
getSettingsListTheme as getSettingsListTheme2
|
|
13230
13449
|
} from "@earendil-works/pi-coding-agent";
|
|
13450
|
+
import {
|
|
13451
|
+
Container as Container4,
|
|
13452
|
+
SettingsList as SettingsList2,
|
|
13453
|
+
Text as Text4
|
|
13454
|
+
} from "@earendil-works/pi-tui";
|
|
13455
|
+
|
|
13456
|
+
// src/ui/settings-complex.ts
|
|
13457
|
+
import path15 from "path";
|
|
13231
13458
|
import {
|
|
13232
13459
|
Container as Container3,
|
|
13460
|
+
Input,
|
|
13461
|
+
Key as Key4,
|
|
13462
|
+
matchesKey as matchesKey4,
|
|
13463
|
+
SelectList as SelectList3,
|
|
13233
13464
|
SettingsList,
|
|
13234
13465
|
Text as Text3
|
|
13235
13466
|
} from "@earendil-works/pi-tui";
|
|
13236
|
-
|
|
13237
|
-
var
|
|
13238
|
-
|
|
13239
|
-
|
|
13240
|
-
|
|
13467
|
+
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
13468
|
+
var MODEL_SETTINGS = [
|
|
13469
|
+
{
|
|
13470
|
+
id: "summaryModel",
|
|
13471
|
+
label: "Summary model",
|
|
13472
|
+
description: "Model used for synthesis and verification fallback."
|
|
13473
|
+
},
|
|
13474
|
+
{
|
|
13475
|
+
id: "segmentationModel",
|
|
13476
|
+
label: "Segmentation model",
|
|
13477
|
+
description: "Optional model used for transcript exploration."
|
|
13478
|
+
},
|
|
13479
|
+
{
|
|
13480
|
+
id: "verificationModel",
|
|
13481
|
+
label: "Verification model",
|
|
13482
|
+
description: "Optional model used for repair after verification."
|
|
13483
|
+
}
|
|
13484
|
+
];
|
|
13485
|
+
var PROFILE_NAMES = Object.keys(PROFILES);
|
|
13486
|
+
function numberParser(options) {
|
|
13487
|
+
return (input) => {
|
|
13488
|
+
const trimmed = input.trim();
|
|
13489
|
+
if (!trimmed)
|
|
13490
|
+
return;
|
|
13491
|
+
const value = Number(trimmed);
|
|
13492
|
+
const inRange = value >= options.min && value <= options.max;
|
|
13493
|
+
if (!Number.isFinite(value) || options.integer !== false && !Number.isSafeInteger(value) || !inRange && !(options.zeroOrRange && value === 0)) {
|
|
13494
|
+
const allowed = options.zeroOrRange ? `0 or ${options.min}\u2013${options.max}` : `${options.min}\u2013${options.max}`;
|
|
13495
|
+
throw new Error(`Enter ${options.integer === false ? "a number" : "an integer"} in ${allowed}.`);
|
|
13496
|
+
}
|
|
13497
|
+
return value;
|
|
13498
|
+
};
|
|
13499
|
+
}
|
|
13500
|
+
function scalarFormat(value) {
|
|
13501
|
+
return value === undefined ? "default" : String(value);
|
|
13502
|
+
}
|
|
13503
|
+
var LIMIT_SETTINGS = [
|
|
13504
|
+
{
|
|
13505
|
+
id: "minContextPercent",
|
|
13506
|
+
label: "Minimum context percent",
|
|
13507
|
+
description: "Auto compaction starts at or above this context usage.",
|
|
13508
|
+
placeholder: "0\u2013100; blank uses default",
|
|
13509
|
+
parse: numberParser(CONFIG_NUMERIC_LIMITS.minContextPercent),
|
|
13510
|
+
format: scalarFormat
|
|
13511
|
+
},
|
|
13512
|
+
{
|
|
13513
|
+
id: "autoTriggerTimeoutMs",
|
|
13514
|
+
label: "Auto-trigger timeout",
|
|
13515
|
+
description: "Maximum host auto-compaction time in milliseconds.",
|
|
13516
|
+
placeholder: "1000\u2013300000; blank uses default",
|
|
13517
|
+
parse: numberParser(CONFIG_NUMERIC_LIMITS.autoTriggerTimeoutMs),
|
|
13518
|
+
format: scalarFormat
|
|
13519
|
+
},
|
|
13520
|
+
{
|
|
13521
|
+
id: "maxLlmCalls",
|
|
13522
|
+
label: "Maximum LLM calls",
|
|
13523
|
+
description: "Zero uses the selected mode's call cap.",
|
|
13524
|
+
placeholder: "0\u2013100; blank uses default",
|
|
13525
|
+
parse: numberParser(CONFIG_NUMERIC_LIMITS.maxLlmCalls),
|
|
13526
|
+
format: scalarFormat
|
|
13527
|
+
},
|
|
13528
|
+
{
|
|
13529
|
+
id: "maxLlmInputTokens",
|
|
13530
|
+
label: "Maximum LLM input tokens",
|
|
13531
|
+
description: "Zero uses the selected mode's token cap.",
|
|
13532
|
+
placeholder: "0\u20131000000; blank uses default",
|
|
13533
|
+
parse: numberParser(CONFIG_NUMERIC_LIMITS.maxLlmInputTokens),
|
|
13534
|
+
format: scalarFormat
|
|
13535
|
+
},
|
|
13536
|
+
{
|
|
13537
|
+
id: "codexMaxCallMs",
|
|
13538
|
+
label: "Codex call watchdog",
|
|
13539
|
+
description: "Zero derives the per-call watchdog automatically.",
|
|
13540
|
+
placeholder: "0 or 5000\u2013300000; blank uses default",
|
|
13541
|
+
parse: numberParser(CONFIG_NUMERIC_LIMITS.codexMaxCallMs),
|
|
13542
|
+
format: scalarFormat
|
|
13543
|
+
},
|
|
13544
|
+
{
|
|
13545
|
+
id: "maxLatencyMs",
|
|
13546
|
+
label: "Pipeline latency limit",
|
|
13547
|
+
description: "Zero disables the overall pipeline deadline.",
|
|
13548
|
+
placeholder: "0 or 5000\u2013600000; blank uses default",
|
|
13549
|
+
parse: numberParser(CONFIG_NUMERIC_LIMITS.maxLatencyMs),
|
|
13550
|
+
format: scalarFormat
|
|
13551
|
+
}
|
|
13552
|
+
];
|
|
13553
|
+
var PATH_SETTINGS = [
|
|
13554
|
+
{
|
|
13555
|
+
id: "backupDir",
|
|
13556
|
+
label: "Backup directory",
|
|
13557
|
+
description: "Directory for recovery Markdown files.",
|
|
13558
|
+
placeholder: "Absolute path; blank uses default",
|
|
13559
|
+
parse(input) {
|
|
13560
|
+
const value = input.trim();
|
|
13561
|
+
if (!value)
|
|
13562
|
+
return;
|
|
13563
|
+
if (value.includes("\x00") || /[\r\n]/.test(value)) {
|
|
13564
|
+
throw new Error("Backup directory must be a single valid path.");
|
|
13565
|
+
}
|
|
13566
|
+
if (!path15.isAbsolute(value)) {
|
|
13567
|
+
throw new Error("Backup directory must be an absolute path.");
|
|
13568
|
+
}
|
|
13569
|
+
return value;
|
|
13570
|
+
},
|
|
13571
|
+
format: scalarFormat
|
|
13572
|
+
},
|
|
13573
|
+
{
|
|
13574
|
+
id: "pinPaths",
|
|
13575
|
+
label: "Pinned paths",
|
|
13576
|
+
description: "Comma-separated file paths that summaries must preserve.",
|
|
13577
|
+
placeholder: "src/api.ts, docs/design.md; blank clears",
|
|
13578
|
+
parse(input) {
|
|
13579
|
+
const trimmed = input.trim();
|
|
13580
|
+
if (!trimmed)
|
|
13581
|
+
return;
|
|
13582
|
+
const paths = trimmed.split(/[,\n]/).map((value) => value.trim()).filter(Boolean);
|
|
13583
|
+
if (paths.some((value) => value.includes("\x00"))) {
|
|
13584
|
+
throw new Error("Pinned paths cannot contain NUL characters.");
|
|
13585
|
+
}
|
|
13586
|
+
return [...new Set(paths)];
|
|
13587
|
+
},
|
|
13588
|
+
format(value) {
|
|
13589
|
+
return value === undefined ? "default" : Array.isArray(value) ? value.join(", ") || "none" : String(value);
|
|
13590
|
+
}
|
|
13591
|
+
}
|
|
13592
|
+
];
|
|
13593
|
+
var PROFILE_FIELDS = [
|
|
13594
|
+
["summaryBudgetTokens", "Summary budget"],
|
|
13595
|
+
["keepRecentTokens", "Recent raw tail"],
|
|
13596
|
+
["minChunkTokens", "Minimum chunk"],
|
|
13597
|
+
["maxChunkTokens", "Maximum chunk"],
|
|
13598
|
+
["singlePassMaxTokens", "Single-pass limit"],
|
|
13599
|
+
["batchMaxTokens", "Batch limit"]
|
|
13600
|
+
];
|
|
13601
|
+
function profileSettings(profile) {
|
|
13602
|
+
return PROFILE_FIELDS.map(([key, label]) => {
|
|
13603
|
+
const [min, max] = PROFILE_NUMERIC_BOUNDS[key];
|
|
13604
|
+
return {
|
|
13605
|
+
id: `profiles.${profile}.${key}`,
|
|
13606
|
+
label,
|
|
13607
|
+
description: `${profile} profile token budget; related chunk bounds must remain consistent.`,
|
|
13608
|
+
placeholder: `${min}\u2013${max}; blank uses built-in`,
|
|
13609
|
+
parse: numberParser({ min, max }),
|
|
13610
|
+
format: scalarFormat
|
|
13611
|
+
};
|
|
13612
|
+
});
|
|
13613
|
+
}
|
|
13614
|
+
function effectiveValue(config, id) {
|
|
13615
|
+
const parts = id.split(".");
|
|
13616
|
+
if (parts[0] !== "profiles") {
|
|
13617
|
+
return config[id];
|
|
13618
|
+
}
|
|
13619
|
+
const [, profile, key] = parts;
|
|
13620
|
+
return config.profiles[profile][key];
|
|
13621
|
+
}
|
|
13622
|
+
|
|
13623
|
+
class InputSettingEditor extends Container3 {
|
|
13624
|
+
requestRender;
|
|
13625
|
+
save;
|
|
13626
|
+
done;
|
|
13627
|
+
input = new Input;
|
|
13628
|
+
status = new Text3("", 0, 0);
|
|
13629
|
+
saving = false;
|
|
13630
|
+
pending = Promise.resolve();
|
|
13631
|
+
get focused() {
|
|
13632
|
+
return this.input.focused;
|
|
13633
|
+
}
|
|
13634
|
+
set focused(value) {
|
|
13635
|
+
this.input.focused = value;
|
|
13636
|
+
}
|
|
13637
|
+
constructor(setting, initial, requestRender, save, done) {
|
|
13638
|
+
super();
|
|
13639
|
+
this.requestRender = requestRender;
|
|
13640
|
+
this.save = save;
|
|
13641
|
+
this.done = done;
|
|
13642
|
+
this.addChild(new Text3(setting.label, 0, 0));
|
|
13643
|
+
this.addChild(new Text3(setting.description, 0, 0));
|
|
13644
|
+
this.addChild(new Text3(`Hint: ${setting.placeholder}`, 0, 0));
|
|
13645
|
+
this.addChild(new Text3("", 0, 0));
|
|
13646
|
+
this.input.setValue(initial);
|
|
13647
|
+
this.input.handleInput("\x1B[F");
|
|
13648
|
+
this.input.onSubmit = (value) => {
|
|
13649
|
+
if (this.saving)
|
|
13650
|
+
return;
|
|
13651
|
+
let parsed;
|
|
13652
|
+
try {
|
|
13653
|
+
parsed = setting.parse(value);
|
|
13654
|
+
} catch (error2) {
|
|
13655
|
+
this.status.setText(error2 instanceof Error ? error2.message : String(error2));
|
|
13656
|
+
this.requestRender();
|
|
13657
|
+
return;
|
|
13658
|
+
}
|
|
13659
|
+
this.saving = true;
|
|
13660
|
+
this.status.setText("Saving\u2026");
|
|
13661
|
+
this.requestRender();
|
|
13662
|
+
this.pending = this.save(parsed).then(() => this.done()).catch((error2) => {
|
|
13663
|
+
this.saving = false;
|
|
13664
|
+
this.status.setText(error2 instanceof Error ? error2.message : String(error2));
|
|
13665
|
+
this.requestRender();
|
|
13666
|
+
});
|
|
13667
|
+
};
|
|
13668
|
+
this.input.onEscape = () => {
|
|
13669
|
+
if (!this.saving)
|
|
13670
|
+
this.done();
|
|
13671
|
+
};
|
|
13672
|
+
this.addChild(this.input);
|
|
13673
|
+
this.addChild(new Text3("", 0, 0));
|
|
13674
|
+
this.addChild(this.status);
|
|
13675
|
+
this.addChild(new Text3("Enter save \xB7 Esc cancel", 0, 0));
|
|
13676
|
+
}
|
|
13677
|
+
handleInput(data) {
|
|
13678
|
+
this.input.handleInput(data);
|
|
13679
|
+
this.requestRender();
|
|
13680
|
+
}
|
|
13681
|
+
settled() {
|
|
13682
|
+
return this.pending;
|
|
13683
|
+
}
|
|
13684
|
+
}
|
|
13685
|
+
|
|
13686
|
+
class ModelSettingEditor extends Container3 {
|
|
13687
|
+
requestRender;
|
|
13688
|
+
search = new Input;
|
|
13689
|
+
list;
|
|
13690
|
+
status = new Text3("", 0, 0);
|
|
13691
|
+
saving = false;
|
|
13692
|
+
pending = Promise.resolve();
|
|
13693
|
+
get focused() {
|
|
13694
|
+
return this.search.focused;
|
|
13695
|
+
}
|
|
13696
|
+
set focused(value) {
|
|
13697
|
+
this.search.focused = value;
|
|
13698
|
+
}
|
|
13699
|
+
constructor(models, selectedValue, requestRender, save, done) {
|
|
13700
|
+
super();
|
|
13701
|
+
this.requestRender = requestRender;
|
|
13702
|
+
this.addChild(new Text3("Search provider/model", 0, 0));
|
|
13703
|
+
this.addChild(this.search);
|
|
13704
|
+
this.addChild(new Text3("", 0, 0));
|
|
13705
|
+
this.list = new SelectList3(models, 10, {
|
|
13706
|
+
selectedPrefix: (text) => text,
|
|
13707
|
+
selectedText: (text) => text,
|
|
13708
|
+
description: (text) => text,
|
|
13709
|
+
scrollInfo: (text) => text,
|
|
13710
|
+
noMatch: (text) => text
|
|
13711
|
+
});
|
|
13712
|
+
const selectedIndex = models.findIndex((item) => item.settingValue === selectedValue);
|
|
13713
|
+
this.list.setSelectedIndex(Math.max(0, selectedIndex));
|
|
13714
|
+
this.list.onSelect = (item) => {
|
|
13715
|
+
if (this.saving)
|
|
13716
|
+
return;
|
|
13717
|
+
this.saving = true;
|
|
13718
|
+
this.status.setText("Saving\u2026");
|
|
13719
|
+
this.requestRender();
|
|
13720
|
+
const selected = models.find((candidate) => candidate.value === item.value);
|
|
13721
|
+
if (!selected)
|
|
13722
|
+
return;
|
|
13723
|
+
this.pending = save(selected.settingValue).then(done).catch((error2) => {
|
|
13724
|
+
this.saving = false;
|
|
13725
|
+
this.status.setText(error2 instanceof Error ? error2.message : String(error2));
|
|
13726
|
+
this.requestRender();
|
|
13727
|
+
});
|
|
13728
|
+
};
|
|
13729
|
+
this.list.onCancel = () => {
|
|
13730
|
+
if (!this.saving)
|
|
13731
|
+
done();
|
|
13732
|
+
};
|
|
13733
|
+
this.addChild(this.list);
|
|
13734
|
+
this.addChild(this.status);
|
|
13735
|
+
this.addChild(new Text3("Type to filter \xB7 Enter select \xB7 Esc cancel", 0, 0));
|
|
13736
|
+
}
|
|
13737
|
+
handleInput(data) {
|
|
13738
|
+
if (matchesKey4(data, Key4.up) || matchesKey4(data, Key4.down) || matchesKey4(data, Key4.enter) || matchesKey4(data, Key4.escape)) {
|
|
13739
|
+
this.list.handleInput(data);
|
|
13740
|
+
} else {
|
|
13741
|
+
this.search.handleInput(data);
|
|
13742
|
+
this.list.setFilter(this.search.getValue());
|
|
13743
|
+
}
|
|
13744
|
+
this.requestRender();
|
|
13745
|
+
}
|
|
13746
|
+
settled() {
|
|
13747
|
+
return this.pending;
|
|
13748
|
+
}
|
|
13749
|
+
}
|
|
13750
|
+
function inputSettingsList(settings, requestRender, done, writeConfig) {
|
|
13751
|
+
const config = loadConfig();
|
|
13752
|
+
const items = settings.map((setting) => {
|
|
13753
|
+
const override = readGlobalConfigValue(setting.id);
|
|
13754
|
+
const item = {
|
|
13755
|
+
id: setting.id,
|
|
13756
|
+
label: setting.label,
|
|
13757
|
+
description: `${setting.description} Effective global value: ${setting.format(effectiveValue(config, setting.id))}.`,
|
|
13758
|
+
currentValue: setting.format(override)
|
|
13759
|
+
};
|
|
13760
|
+
item.submenu = (_current, close) => {
|
|
13761
|
+
const persisted = readGlobalConfigValue(setting.id);
|
|
13762
|
+
return new InputSettingEditor(setting, persisted === undefined ? "" : Array.isArray(persisted) ? persisted.join(", ") : String(persisted), requestRender, async (value) => {
|
|
13763
|
+
const effective = await writeConfig(setting.id, value);
|
|
13764
|
+
item.currentValue = setting.format(value);
|
|
13765
|
+
item.description = `${setting.description} Effective global value: ${setting.format(effectiveValue(effective, setting.id))}.`;
|
|
13766
|
+
}, close);
|
|
13767
|
+
};
|
|
13768
|
+
return item;
|
|
13769
|
+
});
|
|
13770
|
+
return new SettingsList(items, 9, getSettingsListTheme(), () => {}, done);
|
|
13771
|
+
}
|
|
13772
|
+
function modelSettingsItems(ctx, requestRender, writeConfig = writeGlobalConfigValue) {
|
|
13773
|
+
const config = loadConfig();
|
|
13774
|
+
return MODEL_SETTINGS.map((setting) => {
|
|
13775
|
+
const override = readGlobalConfigValue(setting.id);
|
|
13776
|
+
const current = typeof override === "string" ? override : "default";
|
|
13777
|
+
const item = {
|
|
13778
|
+
id: setting.id,
|
|
13779
|
+
label: setting.label,
|
|
13780
|
+
description: `${setting.description} Effective global value: ${String(config[setting.id])}.`,
|
|
13781
|
+
currentValue: current
|
|
13782
|
+
};
|
|
13783
|
+
item.submenu = (_value, close) => {
|
|
13784
|
+
const persisted = readGlobalConfigValue(setting.id);
|
|
13785
|
+
const selectedValue = typeof persisted === "string" ? persisted : "default";
|
|
13786
|
+
const effective = loadConfig();
|
|
13787
|
+
item.currentValue = selectedValue;
|
|
13788
|
+
item.description = `${setting.description} Effective global value: ${String(effective[setting.id])}.`;
|
|
13789
|
+
const available = ctx.modelRegistry.getAvailable().map((model) => ({
|
|
13790
|
+
value: `${model.provider}/${model.id} \u2014 ${model.name}`,
|
|
13791
|
+
settingValue: `${model.provider}/${model.id}`,
|
|
13792
|
+
label: `${model.provider}/${model.id}`,
|
|
13793
|
+
description: model.name
|
|
13794
|
+
})).sort((left, right) => left.value.localeCompare(right.value));
|
|
13795
|
+
const choices = [
|
|
13796
|
+
{
|
|
13797
|
+
value: "default \u2014 use active session model",
|
|
13798
|
+
settingValue: "default",
|
|
13799
|
+
label: "default",
|
|
13800
|
+
description: "Remove the global model override"
|
|
13801
|
+
},
|
|
13802
|
+
...available
|
|
13803
|
+
];
|
|
13804
|
+
if (selectedValue !== "default" && !choices.some((candidate) => candidate.settingValue === selectedValue)) {
|
|
13805
|
+
choices.splice(1, 0, {
|
|
13806
|
+
value: selectedValue,
|
|
13807
|
+
settingValue: selectedValue,
|
|
13808
|
+
label: selectedValue,
|
|
13809
|
+
description: "Configured model is currently unavailable"
|
|
13810
|
+
});
|
|
13811
|
+
}
|
|
13812
|
+
return new ModelSettingEditor(choices, selectedValue, requestRender, async (selected) => {
|
|
13813
|
+
const effective2 = await writeConfig(setting.id, selected === "default" ? undefined : selected);
|
|
13814
|
+
item.currentValue = selected;
|
|
13815
|
+
item.description = `${setting.description} Effective global value: ${String(effective2[setting.id])}.`;
|
|
13816
|
+
}, close);
|
|
13817
|
+
};
|
|
13818
|
+
return item;
|
|
13819
|
+
});
|
|
13820
|
+
}
|
|
13821
|
+
function complexSettingsCategories(ctx, requestRender, writeConfig = writeGlobalConfigValue) {
|
|
13822
|
+
return [
|
|
13823
|
+
{
|
|
13824
|
+
id: "models",
|
|
13825
|
+
label: "Global models",
|
|
13826
|
+
description: "Stage-specific model routing",
|
|
13827
|
+
currentValue: "3 settings",
|
|
13828
|
+
submenu: (_value, done) => new SettingsList(modelSettingsItems(ctx, requestRender, writeConfig), 7, getSettingsListTheme(), () => {}, done)
|
|
13829
|
+
},
|
|
13830
|
+
{
|
|
13831
|
+
id: "limits",
|
|
13832
|
+
label: "Global limits & performance",
|
|
13833
|
+
description: "Context threshold, call budgets, and timeouts",
|
|
13834
|
+
currentValue: "6 settings",
|
|
13835
|
+
submenu: (_value, done) => inputSettingsList(LIMIT_SETTINGS, requestRender, done, writeConfig)
|
|
13836
|
+
},
|
|
13837
|
+
{
|
|
13838
|
+
id: "paths",
|
|
13839
|
+
label: "Global paths",
|
|
13840
|
+
description: "Backup directory and pinned summary paths",
|
|
13841
|
+
currentValue: "2 settings",
|
|
13842
|
+
submenu: (_value, done) => inputSettingsList(PATH_SETTINGS, requestRender, done, writeConfig)
|
|
13843
|
+
},
|
|
13844
|
+
{
|
|
13845
|
+
id: "profiles",
|
|
13846
|
+
label: "Global profile budgets",
|
|
13847
|
+
description: "Advanced token-budget tuning for each profile",
|
|
13848
|
+
currentValue: "3 profiles",
|
|
13849
|
+
submenu: (_value, done) => {
|
|
13850
|
+
const profiles = PROFILE_NAMES.map((profile) => ({
|
|
13851
|
+
id: profile,
|
|
13852
|
+
label: profile,
|
|
13853
|
+
description: `Six token-budget settings for the ${profile} profile.`,
|
|
13854
|
+
currentValue: "6 settings",
|
|
13855
|
+
submenu: (_current, close) => inputSettingsList(profileSettings(profile), requestRender, close, writeConfig)
|
|
13856
|
+
}));
|
|
13857
|
+
return new SettingsList(profiles, 7, getSettingsListTheme(), () => {}, done);
|
|
13858
|
+
}
|
|
13859
|
+
}
|
|
13860
|
+
];
|
|
13861
|
+
}
|
|
13862
|
+
|
|
13863
|
+
// src/ui/settings-overlay.ts
|
|
13864
|
+
function enabled(value) {
|
|
13865
|
+
return value ? "enabled" : "disabled";
|
|
13866
|
+
}
|
|
13867
|
+
|
|
13868
|
+
class GlobalSettingsCoordinator {
|
|
13869
|
+
writer;
|
|
13870
|
+
queue = Promise.resolve();
|
|
13871
|
+
confirmed = new Map;
|
|
13872
|
+
confirmedConfig;
|
|
13873
|
+
pending = new Map;
|
|
13874
|
+
listeners = new Map;
|
|
13875
|
+
constructor(writer = updateGlobalChoiceSetting) {
|
|
13876
|
+
this.writer = writer;
|
|
13877
|
+
}
|
|
13878
|
+
display(id) {
|
|
13879
|
+
return this.pending.get(id)?.display ?? choiceDisplay(readGlobalConfigValue(id));
|
|
13880
|
+
}
|
|
13881
|
+
subscribe(ids, listener) {
|
|
13882
|
+
const registrations = [];
|
|
13883
|
+
for (const id of ids) {
|
|
13884
|
+
const callbacks = this.listeners.get(id) ?? new Set;
|
|
13885
|
+
const callback = (state) => listener(id, state);
|
|
13886
|
+
callbacks.add(callback);
|
|
13887
|
+
this.listeners.set(id, callbacks);
|
|
13888
|
+
registrations.push([id, callback]);
|
|
13889
|
+
}
|
|
13890
|
+
return () => {
|
|
13891
|
+
for (const [id, callback] of registrations) {
|
|
13892
|
+
const callbacks = this.listeners.get(id);
|
|
13893
|
+
callbacks?.delete(callback);
|
|
13894
|
+
if (callbacks?.size === 0)
|
|
13895
|
+
this.listeners.delete(id);
|
|
13896
|
+
}
|
|
13897
|
+
};
|
|
13898
|
+
}
|
|
13899
|
+
submit(group, id, display, onError, onApplied = () => {}) {
|
|
13900
|
+
if (this.pending.size === 0)
|
|
13901
|
+
this.confirmedConfig = loadConfig();
|
|
13902
|
+
if (!this.pending.has(id)) {
|
|
13903
|
+
this.confirmed.set(id, choiceDisplay(readGlobalConfigValue(id)));
|
|
13904
|
+
}
|
|
13905
|
+
const revision = (this.pending.get(id)?.revision ?? 0) + 1;
|
|
13906
|
+
this.pending.set(id, { revision, display });
|
|
13907
|
+
this.queue = this.queue.then(async () => {
|
|
13908
|
+
try {
|
|
13909
|
+
const config = await this.writer(group, id, display);
|
|
13910
|
+
await onApplied(id, config);
|
|
13911
|
+
this.confirmed.set(id, display);
|
|
13912
|
+
this.confirmedConfig = config;
|
|
13913
|
+
if (this.pending.get(id)?.revision === revision) {
|
|
13914
|
+
this.pending.delete(id);
|
|
13915
|
+
this.emit(id, { display, config });
|
|
13916
|
+
}
|
|
13917
|
+
} catch (error2) {
|
|
13918
|
+
onError(error2 instanceof Error ? error2.message : String(error2));
|
|
13919
|
+
if (this.pending.get(id)?.revision === revision) {
|
|
13920
|
+
this.pending.delete(id);
|
|
13921
|
+
this.emit(id, {
|
|
13922
|
+
display: this.confirmed.get(id) ?? "default",
|
|
13923
|
+
config: this.confirmedConfig
|
|
13924
|
+
});
|
|
13925
|
+
}
|
|
13926
|
+
}
|
|
13927
|
+
});
|
|
13928
|
+
}
|
|
13929
|
+
settled() {
|
|
13930
|
+
return this.queue;
|
|
13931
|
+
}
|
|
13932
|
+
emit(id, state) {
|
|
13933
|
+
for (const listener of this.listeners.get(id) ?? [])
|
|
13934
|
+
listener(state);
|
|
13935
|
+
}
|
|
13936
|
+
}
|
|
13937
|
+
var BOOLEAN_VALUES = [true, false];
|
|
13938
|
+
var THINKING_VALUES = [
|
|
13939
|
+
null,
|
|
13940
|
+
"minimal",
|
|
13941
|
+
"low",
|
|
13942
|
+
"medium",
|
|
13943
|
+
"high",
|
|
13944
|
+
"xhigh",
|
|
13945
|
+
"max"
|
|
13946
|
+
];
|
|
13947
|
+
var GLOBAL_CHOICE_SETTINGS = {
|
|
13948
|
+
behavior: [
|
|
13949
|
+
{
|
|
13950
|
+
id: "mode",
|
|
13951
|
+
label: "Compaction mode",
|
|
13952
|
+
description: "Default compaction strategy.",
|
|
13953
|
+
values: ["auto", "fast", "balanced", "thorough"]
|
|
13954
|
+
},
|
|
13955
|
+
{
|
|
13956
|
+
id: "profile",
|
|
13957
|
+
label: "Compression profile",
|
|
13958
|
+
description: "Legacy detail-budget profile.",
|
|
13959
|
+
values: ["light", "balanced", "aggressive"]
|
|
13960
|
+
},
|
|
13961
|
+
{
|
|
13962
|
+
id: "agentToolAccess",
|
|
13963
|
+
label: "Agent tool access",
|
|
13964
|
+
description: "Global default for agent-visible smart_compact access.",
|
|
13965
|
+
values: ["inherit", "enabled", "disabled"]
|
|
13966
|
+
},
|
|
13967
|
+
{
|
|
13968
|
+
id: "autoTrigger",
|
|
13969
|
+
label: "Automatic compaction",
|
|
13970
|
+
description: "Global default for pressure-triggered compaction.",
|
|
13971
|
+
values: BOOLEAN_VALUES
|
|
13972
|
+
},
|
|
13973
|
+
{
|
|
13974
|
+
id: "showStatus",
|
|
13975
|
+
label: "Footer status",
|
|
13976
|
+
description: "Global default for the Smart Compact footer indicator.",
|
|
13977
|
+
values: BOOLEAN_VALUES
|
|
13978
|
+
},
|
|
13979
|
+
{
|
|
13980
|
+
id: "autoTriggerStrategy",
|
|
13981
|
+
label: "Trigger strategy",
|
|
13982
|
+
description: "Host-native hook or settled-turn triggering.",
|
|
13983
|
+
values: ["native-hook", "settled"]
|
|
13984
|
+
}
|
|
13985
|
+
],
|
|
13986
|
+
reasoning: [
|
|
13987
|
+
{
|
|
13988
|
+
id: "summaryThinkingLevel",
|
|
13989
|
+
label: "Summary thinking",
|
|
13990
|
+
description: "Thinking level for synthesis and repair.",
|
|
13991
|
+
values: THINKING_VALUES
|
|
13992
|
+
},
|
|
13993
|
+
{
|
|
13994
|
+
id: "segmentationThinkingLevel",
|
|
13995
|
+
label: "Segmentation thinking",
|
|
13996
|
+
description: "Thinking level for transcript segmentation.",
|
|
13997
|
+
values: THINKING_VALUES
|
|
13998
|
+
}
|
|
13999
|
+
],
|
|
14000
|
+
safety: [
|
|
14001
|
+
{
|
|
14002
|
+
id: "backupEnabled",
|
|
14003
|
+
label: "Backups",
|
|
14004
|
+
description: "Write a recovery backup before applying a compacted summary.",
|
|
14005
|
+
values: BOOLEAN_VALUES
|
|
14006
|
+
},
|
|
14007
|
+
{
|
|
14008
|
+
id: "requireApproval",
|
|
14009
|
+
label: "Require approval",
|
|
14010
|
+
description: "Ask before applying manual compaction output.",
|
|
14011
|
+
values: BOOLEAN_VALUES
|
|
14012
|
+
},
|
|
14013
|
+
{
|
|
14014
|
+
id: "scrubSecrets",
|
|
14015
|
+
label: "Scrub secrets",
|
|
14016
|
+
description: "Redact likely credentials before model calls and memory writes.",
|
|
14017
|
+
values: BOOLEAN_VALUES
|
|
14018
|
+
},
|
|
14019
|
+
{
|
|
14020
|
+
id: "scrubPii",
|
|
14021
|
+
label: "Scrub PII",
|
|
14022
|
+
description: "Redact email, phone, and payment-card shaped data.",
|
|
14023
|
+
values: BOOLEAN_VALUES
|
|
14024
|
+
},
|
|
14025
|
+
{
|
|
14026
|
+
id: "contextGraphEnabled",
|
|
14027
|
+
label: "Project memory",
|
|
14028
|
+
description: "Index project context and expose recall/save tools.",
|
|
14029
|
+
values: BOOLEAN_VALUES
|
|
14030
|
+
}
|
|
14031
|
+
],
|
|
14032
|
+
advanced: [
|
|
14033
|
+
{
|
|
14034
|
+
id: "focusWeighting",
|
|
14035
|
+
label: "Focus weighting",
|
|
14036
|
+
description: "Steer synthesis toward the current task focus.",
|
|
14037
|
+
values: BOOLEAN_VALUES
|
|
14038
|
+
},
|
|
14039
|
+
{
|
|
14040
|
+
id: "zeroCallEnabled",
|
|
14041
|
+
label: "Zero-call fast path",
|
|
14042
|
+
description: "Allow deterministic compaction without an LLM call.",
|
|
14043
|
+
values: BOOLEAN_VALUES
|
|
14044
|
+
},
|
|
14045
|
+
{
|
|
14046
|
+
id: "telemetryChannel",
|
|
14047
|
+
label: "Telemetry channel",
|
|
14048
|
+
description: "Tag local metrics as stable or canary.",
|
|
14049
|
+
values: ["stable", "canary"]
|
|
14050
|
+
},
|
|
14051
|
+
{
|
|
14052
|
+
id: "adaptiveDamageFeedback",
|
|
14053
|
+
label: "Adaptive damage feedback",
|
|
14054
|
+
description: "Increase preservation budgets using prior damage signals.",
|
|
14055
|
+
values: BOOLEAN_VALUES
|
|
14056
|
+
},
|
|
14057
|
+
{
|
|
14058
|
+
id: "onlineDamageMonitor",
|
|
14059
|
+
label: "Online damage monitor",
|
|
14060
|
+
description: "Monitor confirmed compactions for preservation damage.",
|
|
14061
|
+
values: BOOLEAN_VALUES
|
|
14062
|
+
}
|
|
14063
|
+
]
|
|
14064
|
+
};
|
|
14065
|
+
var GLOBAL_CHOICE_CATEGORIES = [
|
|
14066
|
+
{
|
|
14067
|
+
group: "behavior",
|
|
14068
|
+
label: "Global behavior",
|
|
14069
|
+
description: "Mode, profile, automation, and agent defaults"
|
|
14070
|
+
},
|
|
14071
|
+
{
|
|
14072
|
+
group: "reasoning",
|
|
14073
|
+
label: "Global reasoning",
|
|
14074
|
+
description: "Thinking-level defaults"
|
|
14075
|
+
},
|
|
14076
|
+
{
|
|
14077
|
+
group: "safety",
|
|
14078
|
+
label: "Global safety & storage",
|
|
14079
|
+
description: "Backups, approval, scrubbing, and project memory"
|
|
14080
|
+
},
|
|
14081
|
+
{
|
|
14082
|
+
group: "advanced",
|
|
14083
|
+
label: "Global advanced",
|
|
14084
|
+
description: "Focus, fast path, telemetry, and damage monitoring"
|
|
14085
|
+
}
|
|
14086
|
+
];
|
|
14087
|
+
function choiceDisplay(value) {
|
|
14088
|
+
if (value === undefined)
|
|
14089
|
+
return "default";
|
|
14090
|
+
if (value === null)
|
|
14091
|
+
return "provider default";
|
|
14092
|
+
if (typeof value === "boolean")
|
|
14093
|
+
return enabled(value);
|
|
14094
|
+
return String(value);
|
|
14095
|
+
}
|
|
14096
|
+
function choiceValue(setting, display) {
|
|
14097
|
+
if (display === "default")
|
|
14098
|
+
return;
|
|
14099
|
+
const value = setting.values.find((candidate) => choiceDisplay(candidate) === display);
|
|
14100
|
+
if (value === undefined) {
|
|
14101
|
+
throw new Error(`Invalid value for ${setting.id}: ${display}`);
|
|
14102
|
+
}
|
|
14103
|
+
return value;
|
|
14104
|
+
}
|
|
14105
|
+
function effectiveChoiceDescription(setting, config) {
|
|
14106
|
+
const effective = config[setting.id];
|
|
14107
|
+
return `${setting.description} Effective global value: ${choiceDisplay(effective)}.`;
|
|
14108
|
+
}
|
|
14109
|
+
function globalChoiceSettingsItems(group, config, coordinator) {
|
|
14110
|
+
return GLOBAL_CHOICE_SETTINGS[group].map((setting) => ({
|
|
14111
|
+
id: setting.id,
|
|
14112
|
+
label: setting.label,
|
|
14113
|
+
description: effectiveChoiceDescription(setting, config),
|
|
14114
|
+
currentValue: coordinator?.display(setting.id) ?? choiceDisplay(readGlobalConfigValue(setting.id)),
|
|
14115
|
+
values: ["default", ...setting.values.map(choiceDisplay)]
|
|
14116
|
+
}));
|
|
13241
14117
|
}
|
|
13242
|
-
function
|
|
13243
|
-
|
|
14118
|
+
async function updateGlobalChoiceSetting(group, id, display) {
|
|
14119
|
+
const setting = GLOBAL_CHOICE_SETTINGS[group].find((candidate) => candidate.id === id);
|
|
14120
|
+
if (!setting)
|
|
14121
|
+
throw new Error(`Unknown global choice setting: ${id}`);
|
|
14122
|
+
return writeGlobalConfigValue(setting.id, choiceValue(setting, display));
|
|
13244
14123
|
}
|
|
13245
|
-
function
|
|
13246
|
-
|
|
13247
|
-
return accessValue(policy);
|
|
13248
|
-
return policy.autoTrigger ? ENABLED : DISABLED;
|
|
14124
|
+
function effectiveDescription(field, policy) {
|
|
14125
|
+
return field === "agentToolAccess" ? `Effective tool state: ${enabled(policy.agentToolEnabled)}` : `Effective value: ${enabled(policy[field])}`;
|
|
13249
14126
|
}
|
|
13250
|
-
function
|
|
14127
|
+
function sessionSettingsItems(policy) {
|
|
14128
|
+
const current = policy.snapshot();
|
|
14129
|
+
const overrides = policy.branchOverrides();
|
|
13251
14130
|
return [
|
|
13252
14131
|
{
|
|
13253
14132
|
id: "agentToolAccess",
|
|
13254
14133
|
label: "Agent access",
|
|
13255
|
-
description: "
|
|
13256
|
-
currentValue:
|
|
13257
|
-
values: [
|
|
14134
|
+
description: effectiveDescription("agentToolAccess", current),
|
|
14135
|
+
currentValue: overrides.agentToolAccess ?? "global",
|
|
14136
|
+
values: ["global", "inherit", "enabled", "disabled"]
|
|
13258
14137
|
},
|
|
13259
14138
|
{
|
|
13260
14139
|
id: "autoTrigger",
|
|
13261
14140
|
label: "Automatic compaction",
|
|
13262
|
-
description: "
|
|
13263
|
-
currentValue:
|
|
13264
|
-
values: [
|
|
14141
|
+
description: effectiveDescription("autoTrigger", current),
|
|
14142
|
+
currentValue: overrides.autoTrigger === undefined ? "global" : enabled(overrides.autoTrigger),
|
|
14143
|
+
values: ["global", "enabled", "disabled"]
|
|
14144
|
+
},
|
|
14145
|
+
{
|
|
14146
|
+
id: "showStatus",
|
|
14147
|
+
label: "Footer status",
|
|
14148
|
+
description: effectiveDescription("showStatus", current),
|
|
14149
|
+
currentValue: overrides.showStatus === undefined ? "global" : enabled(overrides.showStatus),
|
|
14150
|
+
values: ["global", "enabled", "disabled"]
|
|
13265
14151
|
}
|
|
13266
14152
|
];
|
|
13267
14153
|
}
|
|
13268
|
-
|
|
13269
|
-
|
|
13270
|
-
|
|
13271
|
-
|
|
13272
|
-
|
|
13273
|
-
|
|
13274
|
-
|
|
13275
|
-
|
|
13276
|
-
|
|
13277
|
-
|
|
14154
|
+
function policyField(id) {
|
|
14155
|
+
switch (id) {
|
|
14156
|
+
case "agentToolAccess":
|
|
14157
|
+
case "autoTrigger":
|
|
14158
|
+
case "showStatus":
|
|
14159
|
+
return id;
|
|
14160
|
+
default:
|
|
14161
|
+
throw new Error(`Unknown session setting: ${id}`);
|
|
14162
|
+
}
|
|
14163
|
+
}
|
|
14164
|
+
function updateSessionSetting(policy, ctx, id, value) {
|
|
14165
|
+
const field = policyField(id);
|
|
14166
|
+
if (value === "global")
|
|
14167
|
+
return policy.reset(field, ctx);
|
|
14168
|
+
switch (field) {
|
|
14169
|
+
case "agentToolAccess":
|
|
14170
|
+
if (value !== "inherit" && value !== "enabled" && value !== "disabled") {
|
|
14171
|
+
throw new Error(`Invalid agent access value: ${value}`);
|
|
14172
|
+
}
|
|
14173
|
+
return policy.update({ agentToolAccess: value }, ctx);
|
|
14174
|
+
case "autoTrigger":
|
|
14175
|
+
return policy.update({ autoTrigger: value === "enabled" }, ctx);
|
|
14176
|
+
case "showStatus":
|
|
14177
|
+
return policy.update({ showStatus: value === "enabled" }, ctx);
|
|
14178
|
+
}
|
|
14179
|
+
}
|
|
14180
|
+
function displayValue(field, policy) {
|
|
14181
|
+
const override = policy.branchOverrides()[field];
|
|
14182
|
+
if (override === undefined)
|
|
14183
|
+
return "global";
|
|
14184
|
+
if (typeof override === "boolean")
|
|
14185
|
+
return enabled(override);
|
|
14186
|
+
return override;
|
|
14187
|
+
}
|
|
14188
|
+
function sessionSettingsList(policy, ctx, done) {
|
|
14189
|
+
const items = sessionSettingsItems(policy);
|
|
14190
|
+
let list;
|
|
14191
|
+
list = new SettingsList2(items, 7, getSettingsListTheme2(), (id, value) => {
|
|
14192
|
+
try {
|
|
14193
|
+
const result = updateSessionSetting(policy, ctx, id, value);
|
|
14194
|
+
const field = policyField(id);
|
|
14195
|
+
const item = items.find((candidate) => candidate.id === id);
|
|
14196
|
+
if (item)
|
|
14197
|
+
item.description = effectiveDescription(field, result.policy);
|
|
13278
14198
|
if (!result.ok) {
|
|
13279
|
-
list.updateValue(id, previousValue(id, previous));
|
|
13280
14199
|
ctx.ui.notify(result.error, "error");
|
|
14200
|
+
list.updateValue(id, displayValue(field, policy));
|
|
13281
14201
|
}
|
|
13282
|
-
}
|
|
13283
|
-
|
|
13284
|
-
|
|
13285
|
-
|
|
13286
|
-
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
|
|
13290
|
-
|
|
13291
|
-
|
|
14202
|
+
} catch (error2) {
|
|
14203
|
+
ctx.ui.notify(error2 instanceof Error ? error2.message : String(error2), "error");
|
|
14204
|
+
list.updateValue(id, displayValue(policyField(id), policy));
|
|
14205
|
+
}
|
|
14206
|
+
}, done);
|
|
14207
|
+
return list;
|
|
14208
|
+
}
|
|
14209
|
+
function globalChoiceSettingsList(group, ctx, done, requestRender, coordinator, onApplied) {
|
|
14210
|
+
const settings = GLOBAL_CHOICE_SETTINGS[group];
|
|
14211
|
+
const items = globalChoiceSettingsItems(group, loadConfig(), coordinator);
|
|
14212
|
+
let list;
|
|
14213
|
+
list = new SettingsList2(items, 9, getSettingsListTheme2(), (id, display) => {
|
|
14214
|
+
coordinator.submit(group, id, display, (message) => ctx.ui.notify(message, "error"), onApplied);
|
|
14215
|
+
}, () => {
|
|
14216
|
+
unsubscribe();
|
|
14217
|
+
done();
|
|
14218
|
+
});
|
|
14219
|
+
const unsubscribe = coordinator.subscribe(settings.map((setting) => setting.id), (id, state) => {
|
|
14220
|
+
list.updateValue(id, state.display);
|
|
14221
|
+
if (state.config) {
|
|
14222
|
+
for (const setting of settings) {
|
|
14223
|
+
const item = items.find((candidate) => candidate.id === setting.id);
|
|
14224
|
+
if (item) {
|
|
14225
|
+
item.description = effectiveChoiceDescription(setting, state.config);
|
|
14226
|
+
}
|
|
13292
14227
|
}
|
|
13293
|
-
}
|
|
14228
|
+
}
|
|
14229
|
+
requestRender();
|
|
14230
|
+
});
|
|
14231
|
+
return list;
|
|
14232
|
+
}
|
|
14233
|
+
function settingsCategoryItems(policy, ctx, requestRender = () => {}, coordinator = new GlobalSettingsCoordinator, onApplied = () => {}, writeConfig = writeGlobalConfigValue) {
|
|
14234
|
+
return [
|
|
14235
|
+
{
|
|
14236
|
+
id: "session",
|
|
14237
|
+
label: "Current branch",
|
|
14238
|
+
description: "Agent access, automatic compaction, and footer status",
|
|
14239
|
+
currentValue: "3 settings",
|
|
14240
|
+
submenu: (_current, done) => sessionSettingsList(policy, ctx, done)
|
|
14241
|
+
},
|
|
14242
|
+
...GLOBAL_CHOICE_CATEGORIES.map(({ group, label, description }) => ({
|
|
14243
|
+
id: group,
|
|
14244
|
+
label,
|
|
14245
|
+
description,
|
|
14246
|
+
currentValue: `${GLOBAL_CHOICE_SETTINGS[group].length} settings`,
|
|
14247
|
+
submenu: (_current, done) => globalChoiceSettingsList(group, ctx, done, requestRender, coordinator, onApplied)
|
|
14248
|
+
})),
|
|
14249
|
+
...complexSettingsCategories(ctx, requestRender, writeConfig)
|
|
14250
|
+
];
|
|
14251
|
+
}
|
|
14252
|
+
function createSettingsRoot(policy, ctx, onCancel, requestRender = () => {}, coordinator = new GlobalSettingsCoordinator, onApplied = () => {}, writeConfig = writeGlobalConfigValue) {
|
|
14253
|
+
return new SettingsList2(settingsCategoryItems(policy, ctx, requestRender, coordinator, onApplied, writeConfig), 8, getSettingsListTheme2(), () => {}, onCancel);
|
|
14254
|
+
}
|
|
14255
|
+
function deepestFocusable(component) {
|
|
14256
|
+
let current = component;
|
|
14257
|
+
let focusable;
|
|
14258
|
+
while (current) {
|
|
14259
|
+
if (typeof current.focused === "boolean") {
|
|
14260
|
+
focusable = current;
|
|
14261
|
+
}
|
|
14262
|
+
current = current.submenuComponent;
|
|
14263
|
+
}
|
|
14264
|
+
return focusable;
|
|
14265
|
+
}
|
|
14266
|
+
function createSettingsController(root, display, requestRender) {
|
|
14267
|
+
let focused = false;
|
|
14268
|
+
let target;
|
|
14269
|
+
const syncFocus = () => {
|
|
14270
|
+
const next = focused ? deepestFocusable(root) : undefined;
|
|
14271
|
+
if (target !== next) {
|
|
14272
|
+
if (target)
|
|
14273
|
+
target.focused = false;
|
|
14274
|
+
target = next;
|
|
14275
|
+
}
|
|
14276
|
+
if (target)
|
|
14277
|
+
target.focused = focused;
|
|
14278
|
+
};
|
|
14279
|
+
return {
|
|
14280
|
+
get focused() {
|
|
14281
|
+
return focused;
|
|
14282
|
+
},
|
|
14283
|
+
set focused(value) {
|
|
14284
|
+
focused = value;
|
|
14285
|
+
syncFocus();
|
|
14286
|
+
},
|
|
14287
|
+
render(width) {
|
|
14288
|
+
syncFocus();
|
|
14289
|
+
return display.render(width);
|
|
14290
|
+
},
|
|
14291
|
+
handleInput(data) {
|
|
14292
|
+
root.handleInput(data);
|
|
14293
|
+
syncFocus();
|
|
14294
|
+
requestRender();
|
|
14295
|
+
},
|
|
14296
|
+
invalidate: () => display.invalidate()
|
|
14297
|
+
};
|
|
14298
|
+
}
|
|
14299
|
+
async function showSmartCompactSettings(ctx, policy, coordinator = new GlobalSettingsCoordinator, onApplied = () => {}) {
|
|
14300
|
+
const writeConfig = async (path16, value) => {
|
|
14301
|
+
const config = await writeGlobalConfigValue(path16, value);
|
|
14302
|
+
await onApplied(path16, config);
|
|
14303
|
+
return config;
|
|
14304
|
+
};
|
|
14305
|
+
await ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
14306
|
+
const root = createSettingsRoot(policy, ctx, done, () => tui.requestRender(), coordinator, onApplied, writeConfig);
|
|
14307
|
+
const container = new Container4;
|
|
14308
|
+
container.addChild(new Text4(theme.fg("accent", theme.bold("Smart Compact Settings")), 0, 0));
|
|
14309
|
+
container.addChild(new Text4(theme.fg("dim", "Global defaults and branch-specific overrides."), 0, 0));
|
|
14310
|
+
container.addChild(new Text4(theme.fg("dim", "Manual /smart-compact stays available in every mode."), 0, 0));
|
|
14311
|
+
container.addChild(new Text4("", 0, 0));
|
|
14312
|
+
container.addChild(root);
|
|
14313
|
+
container.addChild(new Text4("", 0, 0));
|
|
14314
|
+
container.addChild(new Text4(theme.fg("dim", "\u2191\u2193 navigate \xB7 enter open/change \xB7 esc back/close"), 0, 0));
|
|
14315
|
+
return createSettingsController(root, container, () => tui.requestRender());
|
|
13294
14316
|
});
|
|
13295
14317
|
}
|
|
13296
14318
|
|
|
@@ -13463,6 +14485,7 @@ async function runInteractiveCompaction(ctx, config, dependencies) {
|
|
|
13463
14485
|
});
|
|
13464
14486
|
}
|
|
13465
14487
|
function registerSmartCompactCommand(pi, dependencies) {
|
|
14488
|
+
const settingsCoordinator = new GlobalSettingsCoordinator;
|
|
13466
14489
|
pi.registerCommand("smart-compact", {
|
|
13467
14490
|
description: "EESV smart compaction v" + VERSION + ". Usage: /smart-compact [model|settings] [mode] [flags] [--focus=topic] [--max-calls=N] [--max-input-tokens=N] [--note=text | -- text]",
|
|
13468
14491
|
getArgumentCompletions(prefix) {
|
|
@@ -13515,7 +14538,7 @@ function registerSmartCompactCommand(pi, dependencies) {
|
|
|
13515
14538
|
ctx.ui.notify("Smart Compact settings require TUI mode. Use settings.json for permanent defaults.", "warning");
|
|
13516
14539
|
return;
|
|
13517
14540
|
}
|
|
13518
|
-
await showSmartCompactSettings(ctx, dependencies.policy);
|
|
14541
|
+
await showSmartCompactSettings(ctx, dependencies.policy, settingsCoordinator, (path16) => dependencies.onGlobalSettingApplied?.(path16, ctx));
|
|
13519
14542
|
return;
|
|
13520
14543
|
}
|
|
13521
14544
|
const config = loadConfig();
|
|
@@ -13562,17 +14585,43 @@ function registerSmartCompactCommand(pi, dependencies) {
|
|
|
13562
14585
|
// src/app/smart-compact-policy.ts
|
|
13563
14586
|
var SMART_COMPACT_TOOL_NAME = "smart_compact";
|
|
13564
14587
|
var SMART_COMPACT_POLICY_ENTRY = "smart-compact-policy";
|
|
13565
|
-
var POLICY_VERSION =
|
|
14588
|
+
var POLICY_VERSION = 3;
|
|
13566
14589
|
var STATUS_KEY = "smart-compact-policy";
|
|
13567
14590
|
function persistedPolicy(value) {
|
|
13568
14591
|
if (typeof value !== "object" || value === null)
|
|
13569
14592
|
return null;
|
|
13570
14593
|
const candidate = value;
|
|
13571
|
-
if (candidate.version === POLICY_VERSION &&
|
|
13572
|
-
|
|
14594
|
+
if (candidate.version === POLICY_VERSION && typeof candidate.overrides === "object" && candidate.overrides !== null && !Array.isArray(candidate.overrides)) {
|
|
14595
|
+
const values = candidate.overrides;
|
|
14596
|
+
const overrides = {};
|
|
14597
|
+
if (values.agentToolAccess !== undefined) {
|
|
14598
|
+
if (values.agentToolAccess !== "inherit" && values.agentToolAccess !== "enabled" && values.agentToolAccess !== "disabled") {
|
|
14599
|
+
return null;
|
|
14600
|
+
}
|
|
14601
|
+
overrides.agentToolAccess = values.agentToolAccess;
|
|
14602
|
+
}
|
|
14603
|
+
if (values.autoTrigger !== undefined) {
|
|
14604
|
+
if (typeof values.autoTrigger !== "boolean")
|
|
14605
|
+
return null;
|
|
14606
|
+
overrides.autoTrigger = values.autoTrigger;
|
|
14607
|
+
}
|
|
14608
|
+
if (values.showStatus !== undefined) {
|
|
14609
|
+
if (typeof values.showStatus !== "boolean")
|
|
14610
|
+
return null;
|
|
14611
|
+
overrides.showStatus = values.showStatus;
|
|
14612
|
+
}
|
|
14613
|
+
return overrides;
|
|
14614
|
+
}
|
|
14615
|
+
if (candidate.version === 2 && (candidate.agentToolAccess === "inherit" || candidate.agentToolAccess === "enabled" || candidate.agentToolAccess === "disabled") && typeof candidate.autoTrigger === "boolean") {
|
|
14616
|
+
const desired = {
|
|
13573
14617
|
agentToolAccess: candidate.agentToolAccess,
|
|
13574
|
-
autoTrigger: candidate.autoTrigger
|
|
14618
|
+
autoTrigger: candidate.autoTrigger,
|
|
14619
|
+
showStatus: true
|
|
13575
14620
|
};
|
|
14621
|
+
if (typeof candidate.showStatus === "boolean") {
|
|
14622
|
+
desired.showStatus = candidate.showStatus;
|
|
14623
|
+
}
|
|
14624
|
+
return desired;
|
|
13576
14625
|
}
|
|
13577
14626
|
if (candidate.version === 1 && typeof candidate.agentToolEnabled === "boolean" && typeof candidate.autoTrigger === "boolean") {
|
|
13578
14627
|
return {
|
|
@@ -13586,7 +14635,8 @@ function configDefaults() {
|
|
|
13586
14635
|
const config = loadConfig();
|
|
13587
14636
|
return {
|
|
13588
14637
|
agentToolAccess: config.agentToolAccess,
|
|
13589
|
-
autoTrigger: config.autoTrigger
|
|
14638
|
+
autoTrigger: config.autoTrigger,
|
|
14639
|
+
showStatus: config.showStatus !== false
|
|
13590
14640
|
};
|
|
13591
14641
|
}
|
|
13592
14642
|
function statusText(policy) {
|
|
@@ -13601,13 +14651,18 @@ function statusText(policy) {
|
|
|
13601
14651
|
return "smart-compact: auto off";
|
|
13602
14652
|
}
|
|
13603
14653
|
function createSmartCompactPolicy(pi) {
|
|
13604
|
-
let
|
|
14654
|
+
let overrides = {};
|
|
14655
|
+
const desired = () => ({
|
|
14656
|
+
...configDefaults(),
|
|
14657
|
+
...overrides
|
|
14658
|
+
});
|
|
13605
14659
|
const effectiveToolState = () => pi.getActiveTools().includes(SMART_COMPACT_TOOL_NAME);
|
|
13606
14660
|
const snapshot = () => ({
|
|
13607
|
-
...
|
|
14661
|
+
...desired(),
|
|
13608
14662
|
agentToolEnabled: effectiveToolState()
|
|
13609
14663
|
});
|
|
13610
14664
|
const apply = (ctx) => {
|
|
14665
|
+
const current = desired();
|
|
13611
14666
|
const active = pi.getActiveTools();
|
|
13612
14667
|
const hasTool = active.includes(SMART_COMPACT_TOOL_NAME);
|
|
13613
14668
|
if (current.agentToolAccess === "enabled" && !hasTool) {
|
|
@@ -13616,48 +14671,67 @@ function createSmartCompactPolicy(pi) {
|
|
|
13616
14671
|
pi.setActiveTools(active.filter((name) => name !== SMART_COMPACT_TOOL_NAME));
|
|
13617
14672
|
}
|
|
13618
14673
|
const effective = snapshot();
|
|
13619
|
-
ctx.ui.setStatus(STATUS_KEY, statusText(effective));
|
|
14674
|
+
ctx.ui.setStatus(STATUS_KEY, current.showStatus ? statusText(effective) : undefined);
|
|
13620
14675
|
return effective;
|
|
13621
14676
|
};
|
|
14677
|
+
const restoreToolMembership = (enabled2) => {
|
|
14678
|
+
const active = pi.getActiveTools();
|
|
14679
|
+
const hasTool = active.includes(SMART_COMPACT_TOOL_NAME);
|
|
14680
|
+
if (enabled2 && !hasTool) {
|
|
14681
|
+
pi.setActiveTools([...new Set([...active, SMART_COMPACT_TOOL_NAME])]);
|
|
14682
|
+
} else if (!enabled2 && hasTool) {
|
|
14683
|
+
pi.setActiveTools(active.filter((name) => name !== SMART_COMPACT_TOOL_NAME));
|
|
14684
|
+
}
|
|
14685
|
+
};
|
|
14686
|
+
const persist = (next, ctx) => {
|
|
14687
|
+
const previous = overrides;
|
|
14688
|
+
const previousToolEnabled = effectiveToolState();
|
|
14689
|
+
overrides = next;
|
|
14690
|
+
try {
|
|
14691
|
+
const effective = apply(ctx);
|
|
14692
|
+
pi.appendEntry(SMART_COMPACT_POLICY_ENTRY, { version: POLICY_VERSION, overrides: { ...overrides } });
|
|
14693
|
+
return { ok: true, policy: effective };
|
|
14694
|
+
} catch (error2) {
|
|
14695
|
+
debugError("Smart Compact policy update failed", error2);
|
|
14696
|
+
overrides = previous;
|
|
14697
|
+
try {
|
|
14698
|
+
restoreToolMembership(previousToolEnabled);
|
|
14699
|
+
} catch (rollbackError) {
|
|
14700
|
+
debugError("Smart Compact policy rollback failed", rollbackError);
|
|
14701
|
+
}
|
|
14702
|
+
const rolledBack = snapshot();
|
|
14703
|
+
const previousDesired = desired();
|
|
14704
|
+
ctx.ui.setStatus(STATUS_KEY, previousDesired.showStatus ? statusText(rolledBack) : undefined);
|
|
14705
|
+
return {
|
|
14706
|
+
ok: false,
|
|
14707
|
+
policy: rolledBack,
|
|
14708
|
+
error: "Smart Compact settings could not be saved; the previous policy was restored."
|
|
14709
|
+
};
|
|
14710
|
+
}
|
|
14711
|
+
};
|
|
13622
14712
|
return {
|
|
13623
14713
|
snapshot,
|
|
14714
|
+
branchOverrides: () => ({ ...overrides }),
|
|
13624
14715
|
isAgentToolEnabled: effectiveToolState,
|
|
13625
|
-
isAutoTriggerEnabled: () =>
|
|
14716
|
+
isAutoTriggerEnabled: () => desired().autoTrigger,
|
|
13626
14717
|
restore(ctx) {
|
|
13627
|
-
|
|
14718
|
+
overrides = {};
|
|
13628
14719
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
13629
14720
|
if (entry.type === "custom" && entry.customType === SMART_COMPACT_POLICY_ENTRY) {
|
|
13630
14721
|
const restored = persistedPolicy(entry.data);
|
|
13631
14722
|
if (restored)
|
|
13632
|
-
|
|
14723
|
+
overrides = restored;
|
|
13633
14724
|
}
|
|
13634
14725
|
}
|
|
13635
14726
|
apply(ctx);
|
|
13636
14727
|
},
|
|
13637
14728
|
update(patch, ctx) {
|
|
13638
|
-
|
|
13639
|
-
|
|
13640
|
-
|
|
13641
|
-
|
|
13642
|
-
|
|
13643
|
-
|
|
13644
|
-
return { ok: true, policy: effective };
|
|
13645
|
-
} catch (error2) {
|
|
13646
|
-
debugError("Smart Compact policy update failed", error2);
|
|
13647
|
-
current = previous;
|
|
13648
|
-
try {
|
|
13649
|
-
pi.setActiveTools(previousActiveTools);
|
|
13650
|
-
} catch (rollbackError) {
|
|
13651
|
-
debugError("Smart Compact policy rollback failed", rollbackError);
|
|
13652
|
-
}
|
|
13653
|
-
const rolledBack = snapshot();
|
|
13654
|
-
ctx.ui.setStatus(STATUS_KEY, statusText(rolledBack));
|
|
13655
|
-
return {
|
|
13656
|
-
ok: false,
|
|
13657
|
-
policy: rolledBack,
|
|
13658
|
-
error: "Smart Compact settings could not be saved; the previous policy was restored."
|
|
13659
|
-
};
|
|
13660
|
-
}
|
|
14729
|
+
return persist({ ...overrides, ...patch }, ctx);
|
|
14730
|
+
},
|
|
14731
|
+
reset(field, ctx) {
|
|
14732
|
+
const next = { ...overrides };
|
|
14733
|
+
delete next[field];
|
|
14734
|
+
return persist(next, ctx);
|
|
13661
14735
|
}
|
|
13662
14736
|
};
|
|
13663
14737
|
}
|
|
@@ -13730,12 +14804,19 @@ function smartCompactExtension(pi) {
|
|
|
13730
14804
|
return false;
|
|
13731
14805
|
}
|
|
13732
14806
|
};
|
|
13733
|
-
registerContextTools(pi);
|
|
14807
|
+
const contextToolAvailability = registerContextTools(pi);
|
|
13734
14808
|
registerSmartCompactCommand(pi, {
|
|
13735
14809
|
pendingRef,
|
|
13736
14810
|
runLock: isRunning,
|
|
13737
14811
|
onNativeApplyError,
|
|
13738
|
-
policy
|
|
14812
|
+
policy,
|
|
14813
|
+
onGlobalSettingApplied(path16, ctx) {
|
|
14814
|
+
try {
|
|
14815
|
+
applyGlobalSettingRuntime(path16, ctx, policy, contextToolAvailability);
|
|
14816
|
+
} catch (error2) {
|
|
14817
|
+
debugError("Smart Compact runtime settings refresh failed", error2);
|
|
14818
|
+
}
|
|
14819
|
+
}
|
|
13739
14820
|
});
|
|
13740
14821
|
pi.on("session_start", (_event, ctx) => {
|
|
13741
14822
|
policy.restore(ctx);
|