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