pi-smart-compact 10.0.1 → 10.1.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 +71 -28
- package/CHANGELOG.md +124 -0
- package/README.md +12 -7
- package/assets/skills/context-management/SKILL.md +5 -5
- package/dist/app/background-preparation.d.ts +13 -0
- package/dist/app/background-preparation.d.ts.map +1 -1
- package/dist/app/compaction-commit-store.d.ts +1 -1
- package/dist/app/compaction-commit-store.d.ts.map +1 -1
- package/dist/app/context-operations.d.ts +8 -2
- package/dist/app/context-operations.d.ts.map +1 -1
- package/dist/app/effective-state.d.ts.map +1 -1
- package/dist/app/hindsight-memory.d.ts.map +1 -1
- package/dist/app/lazy-tools.d.ts.map +1 -1
- package/dist/app/pending-slot.d.ts +2 -2
- package/dist/app/pending-slot.d.ts.map +1 -1
- package/dist/app/register-context-tools.d.ts.map +1 -1
- package/dist/app/register-navigation.d.ts +3 -1
- package/dist/app/register-navigation.d.ts.map +1 -1
- package/dist/app/register-smart-compact-command.d.ts.map +1 -1
- package/dist/app/register-smart-compact-tool.d.ts.map +1 -1
- package/dist/app/register-smart-context-tool.d.ts +1 -0
- package/dist/app/register-smart-context-tool.d.ts.map +1 -1
- package/dist/app/settled-auto-trigger.d.ts.map +1 -1
- package/dist/app/stage-auth.d.ts.map +1 -1
- package/dist/app/steps/synthesize.d.ts.map +1 -1
- package/dist/constants.d.ts +5 -4
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1424 -357
- package/dist/phases/explore.d.ts.map +1 -1
- package/dist/phases/synthesize.d.ts +1 -1
- package/dist/phases/synthesize.d.ts.map +1 -1
- package/dist/phases/verify.d.ts +13 -5
- package/dist/phases/verify.d.ts.map +1 -1
- package/dist/rtk.js +8 -7
- package/dist/types.d.ts +5 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/profiles.d.ts +1 -1
- package/dist/ui/profiles.d.ts.map +1 -1
- package/dist/ui/settings-overlay.d.ts.map +1 -1
- package/dist/ui/tool-rows.d.ts +76 -0
- package/dist/ui/tool-rows.d.ts.map +1 -0
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/extraction.d.ts +22 -0
- package/dist/utils/extraction.d.ts.map +1 -1
- package/dist/utils/state.d.ts.map +1 -1
- package/docs/README.md +8 -13
- package/docs/RELEASE.md +4 -3
- package/docs/configuration.md +101 -38
- package/docs/evaluation.md +29 -24
- package/docs/guide.md +58 -32
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
6
|
|
|
7
7
|
// src/constants.ts
|
|
8
|
-
var VERSION = "10.
|
|
8
|
+
var VERSION = "10.1.1";
|
|
9
9
|
var OPTIONAL_COMPONENTS = {
|
|
10
10
|
mnemopi: { name: "@oh-my-pi/pi-mnemopi", version: "18.3.1", enables: "Mnemopi memory store" },
|
|
11
11
|
bun: { name: "bun", version: "1.4.2", enables: "Mnemopi worker runtime when no Bun 1.3.14+ is on PATH" },
|
|
@@ -16,7 +16,7 @@ var MIN_COMPACTION_SAVING_RATIO = 0.1;
|
|
|
16
16
|
var ESTIMATOR_ROUNDING_TOLERANCE_TOKENS = 1;
|
|
17
17
|
var POST_SUMMARY_RESERVE_RATIO = 0.25;
|
|
18
18
|
var MAX_STATE_OPEN_LOOPS = 25;
|
|
19
|
-
var AUTO_TRIGGER_TIMEOUT_CAP_MS =
|
|
19
|
+
var AUTO_TRIGGER_TIMEOUT_CAP_MS = 300000;
|
|
20
20
|
var AUTO_TRIGGER_MAX_LLM_CALLS = 4;
|
|
21
21
|
var SETTLED_TRIGGER_COOLDOWN_MS = 10 * 60000;
|
|
22
22
|
var REBUILD_MIN_TOKENS = 16384;
|
|
@@ -96,14 +96,14 @@ var DEFAULT_CONFIG = {
|
|
|
96
96
|
summaryThinkingLevel: "minimal",
|
|
97
97
|
segmentationThinkingLevel: "minimal",
|
|
98
98
|
agentToolAccess: "inherit",
|
|
99
|
-
toolLoading: "
|
|
99
|
+
toolLoading: "eager",
|
|
100
100
|
autoTrigger: true,
|
|
101
101
|
showStatus: true,
|
|
102
|
-
autoTriggerStrategy: "
|
|
103
|
-
autoTriggerTimeoutMs:
|
|
102
|
+
autoTriggerStrategy: "settled",
|
|
103
|
+
autoTriggerTimeoutMs: 300000,
|
|
104
104
|
backupEnabled: true,
|
|
105
105
|
backupDir: "",
|
|
106
|
-
minContextPercent:
|
|
106
|
+
minContextPercent: 80,
|
|
107
107
|
prepareContextPercent: null,
|
|
108
108
|
maxContextTokens: 0,
|
|
109
109
|
requireApproval: true,
|
|
@@ -127,7 +127,8 @@ var DEFAULT_CONFIG = {
|
|
|
127
127
|
mnemopiDataDir: null,
|
|
128
128
|
visualArchiveEnabled: false,
|
|
129
129
|
artifactOffloadEnabled: false,
|
|
130
|
-
contextHygieneEnabled:
|
|
130
|
+
contextHygieneEnabled: true,
|
|
131
|
+
contextPressureOnly: true,
|
|
131
132
|
contextNavigationEnabled: true,
|
|
132
133
|
contextRecallEnabled: true,
|
|
133
134
|
contextPivotEnabled: true,
|
|
@@ -1011,6 +1012,7 @@ var BOOLEAN_KEYS = [
|
|
|
1011
1012
|
"visualArchiveEnabled",
|
|
1012
1013
|
"artifactOffloadEnabled",
|
|
1013
1014
|
"contextHygieneEnabled",
|
|
1015
|
+
"contextPressureOnly",
|
|
1014
1016
|
"contextNavigationEnabled",
|
|
1015
1017
|
"contextRecallEnabled",
|
|
1016
1018
|
"contextPivotEnabled",
|
|
@@ -3310,6 +3312,15 @@ function isTransientToolDiagnostic(text) {
|
|
|
3310
3312
|
const candidate = text.trim();
|
|
3311
3313
|
return /\bBrave Search API error\s*\(429\)/i.test(candidate) || /\bnpm error code ENOLOCK\b/i.test(candidate) && /(?:audit|existing lockfile|loadVirtual)/i.test(candidate) || /^Found \d+ occurrences? of edits\[\d+\](?!\w)/i.test(candidate) || /^Unknown JSON field:/i.test(candidate) && /Available fields:/i.test(candidate);
|
|
3312
3314
|
}
|
|
3315
|
+
function resolvesErrorResult(result, retry, tcIdx) {
|
|
3316
|
+
if (!result || result.role !== "toolResult" || result.isError)
|
|
3317
|
+
return false;
|
|
3318
|
+
const resultCall = tcIdx.get(result.toolCallId ?? "");
|
|
3319
|
+
const matches = retry.id == null ? Boolean(resultCall && sameToolOperation(retry, resultCall)) : result.toolCallId === retry.id;
|
|
3320
|
+
if (!matches)
|
|
3321
|
+
return false;
|
|
3322
|
+
return !(classifyToolOperation(retry.arguments, retry.name) === "execute" && hasCommandFailureSignal(extractText(result.content)));
|
|
3323
|
+
}
|
|
3313
3324
|
function catalogErrors(msgs, _tcIdx) {
|
|
3314
3325
|
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
3315
3326
|
const errors = [];
|
|
@@ -3353,28 +3364,49 @@ function catalogErrors(msgs, _tcIdx) {
|
|
|
3353
3364
|
if (msgs[j]?.role !== "assistant")
|
|
3354
3365
|
continue;
|
|
3355
3366
|
const blocks = Array.isArray(msgs[j]?.content) ? msgs[j].content : [];
|
|
3356
|
-
const
|
|
3357
|
-
if (!
|
|
3367
|
+
const retryTools = blocks.flatMap((block) => flattenToolCallBlock(block)).filter((candidate) => sameToolOperation(failedCall, candidate));
|
|
3368
|
+
if (!retryTools.length)
|
|
3358
3369
|
continue;
|
|
3359
3370
|
err.retryAttempted = true;
|
|
3360
|
-
for (
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
return Boolean(resultCall && sameToolOperation(retryTool, resultCall));
|
|
3367
|
-
})() : result.toolCallId === retryTool.id;
|
|
3368
|
-
if (resolved) {
|
|
3369
|
-
err.resolved = true;
|
|
3370
|
-
break;
|
|
3371
|
+
for (const retryTool of retryTools) {
|
|
3372
|
+
for (let k = j + 1;k < Math.min(msgs.length, j + ERROR_RESOLVE_WINDOW); k++) {
|
|
3373
|
+
if (resolvesErrorResult(msgs[k], retryTool, tcIdx)) {
|
|
3374
|
+
err.resolved = true;
|
|
3375
|
+
break;
|
|
3376
|
+
}
|
|
3371
3377
|
}
|
|
3378
|
+
if (err.resolved)
|
|
3379
|
+
break;
|
|
3372
3380
|
}
|
|
3373
|
-
|
|
3381
|
+
if (err.resolved)
|
|
3382
|
+
break;
|
|
3374
3383
|
}
|
|
3375
3384
|
}
|
|
3376
3385
|
return errors;
|
|
3377
3386
|
}
|
|
3387
|
+
function collapseDecisionsByQuestion(items) {
|
|
3388
|
+
const order = [];
|
|
3389
|
+
const byQuestion = new Map;
|
|
3390
|
+
for (const item of items) {
|
|
3391
|
+
const key = normalizeFactKey(item.summary);
|
|
3392
|
+
if (!key)
|
|
3393
|
+
continue;
|
|
3394
|
+
const existing = byQuestion.get(key);
|
|
3395
|
+
if (!existing) {
|
|
3396
|
+
order.push(key);
|
|
3397
|
+
byQuestion.set(key, item);
|
|
3398
|
+
continue;
|
|
3399
|
+
}
|
|
3400
|
+
if (item.userResponse && item.userResponse.trim()) {
|
|
3401
|
+
byQuestion.set(key, item);
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
return order.map((key) => byQuestion.get(key));
|
|
3405
|
+
}
|
|
3406
|
+
var HARNESS_INJECTED_RE = /^(?:<process_event\b|Native compaction continuity bridge\b|## Continuity Ledger\b|\[agent-mesh\]|(?:\*\*)?agent-mesh (?:message|reply)\b)/;
|
|
3407
|
+
function isHarnessInjectedText(text) {
|
|
3408
|
+
return HARNESS_INJECTED_RE.test(text.trim());
|
|
3409
|
+
}
|
|
3378
3410
|
function extractDecisions(msgs, _tcIdx) {
|
|
3379
3411
|
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
3380
3412
|
const decisions = [];
|
|
@@ -3385,7 +3417,7 @@ function extractDecisions(msgs, _tcIdx) {
|
|
|
3385
3417
|
const question = typeof args === "string" ? args : args?.question ?? args?.prompt ?? "";
|
|
3386
3418
|
if (!question)
|
|
3387
3419
|
continue;
|
|
3388
|
-
for (let i = tc.msgIndex + 1;i <
|
|
3420
|
+
for (let i = tc.msgIndex + 1;i < msgs.length; i++) {
|
|
3389
3421
|
if (msgs[i]?.role === "toolResult" && msgs[i]?.toolCallId === id) {
|
|
3390
3422
|
decisions.push({
|
|
3391
3423
|
index: tc.msgIndex,
|
|
@@ -3401,6 +3433,8 @@ function extractDecisions(msgs, _tcIdx) {
|
|
|
3401
3433
|
if (msgs[i]?.role !== "user")
|
|
3402
3434
|
continue;
|
|
3403
3435
|
const txt = extractText(msgs[i].content);
|
|
3436
|
+
if (isHarnessInjectedText(txt))
|
|
3437
|
+
continue;
|
|
3404
3438
|
if (CHOICE_RE.test(txt)) {
|
|
3405
3439
|
decisions.push({
|
|
3406
3440
|
index: i,
|
|
@@ -3447,12 +3481,14 @@ function isDiagnosticConstraintText(text) {
|
|
|
3447
3481
|
const candidate = text.replace(/^\s*[-*]\s+/, "").trim();
|
|
3448
3482
|
return /^(?:\[[^\]]+\]\s*)?(?:npm\s+(?:error|warn|notice|audit|verbose|info)\b|(?:rg|grep):|command exited\b)/i.test(candidate);
|
|
3449
3483
|
}
|
|
3450
|
-
var
|
|
3484
|
+
var OWN_OUTPUT_CONTEXT_RE = /(?:verification stopped apply|yield check stopped apply|smart compact failed|conversation unchanged|review \/smart-compact metrics|restart pi with debug=smart-compact|smart compact: )/i;
|
|
3485
|
+
var OWN_OUTPUT_PHRASE_RE = /do not bypass verification/i;
|
|
3451
3486
|
var COMPLETED_CHECKLIST_RE = /^\[[xX\u2713\u2714]\]\s*\S/;
|
|
3452
3487
|
function isNonLiveConstraintText(text) {
|
|
3453
3488
|
const candidate = text.replace(/^\s*[-*]\s+/, "").trim();
|
|
3454
|
-
return isDiagnosticConstraintText(candidate) || isCompactionStatusText(candidate) ||
|
|
3489
|
+
return isDiagnosticConstraintText(candidate) || isCompactionStatusText(candidate) || OWN_OUTPUT_CONTEXT_RE.test(candidate) || COMPLETED_CHECKLIST_RE.test(candidate);
|
|
3455
3490
|
}
|
|
3491
|
+
var CONTINUITY_LABEL_RE = /^(?:(?:Constraint|Goal|Decision|Critical|Open loop|Unresolved error|Resolved error):\s*)+/i;
|
|
3456
3492
|
function mineConstraints(msgs) {
|
|
3457
3493
|
const constraints = [];
|
|
3458
3494
|
const seen = new Set;
|
|
@@ -3462,10 +3498,15 @@ function mineConstraints(msgs) {
|
|
|
3462
3498
|
const text = extractText(msgs[i].content);
|
|
3463
3499
|
if (text.length < 10 || text.startsWith("/"))
|
|
3464
3500
|
continue;
|
|
3501
|
+
if (isHarnessInjectedText(text))
|
|
3502
|
+
continue;
|
|
3503
|
+
const ownOutputMessage = OWN_OUTPUT_CONTEXT_RE.test(text);
|
|
3465
3504
|
for (const raw of text.split(/\n+/)) {
|
|
3466
|
-
const candidate = raw.replace(/^\s*[-*]\s+/, "").trim();
|
|
3505
|
+
const candidate = raw.replace(/^\s*[-*]\s+/, "").trim().replace(CONTINUITY_LABEL_RE, "");
|
|
3467
3506
|
if (candidate.length < 10 || isNonLiveConstraintText(candidate))
|
|
3468
3507
|
continue;
|
|
3508
|
+
if (ownOutputMessage && OWN_OUTPUT_PHRASE_RE.test(candidate))
|
|
3509
|
+
continue;
|
|
3469
3510
|
for (const { re, cat, conf } of CONSTRAINT_PATTERNS) {
|
|
3470
3511
|
if (!re.test(candidate))
|
|
3471
3512
|
continue;
|
|
@@ -3506,7 +3547,7 @@ function segmentTopicsHeuristic(msgs, pc, maxSegs = 20, _tcIdx) {
|
|
|
3506
3547
|
const nextBasename = shiftBasename(nextFile);
|
|
3507
3548
|
const closesActiveTool = message.role === "toolResult" && (tcIdx.get(message.toolCallId ?? "")?.msgIndex ?? -1) >= startIdx;
|
|
3508
3549
|
const fileShift = Boolean(lastFile && nextBasename && nextBasename !== lastFile);
|
|
3509
|
-
const userShift = message.role === "user" && SHIFT_RE.test(text);
|
|
3550
|
+
const userShift = message.role === "user" && !isHarnessInjectedText(text) && SHIFT_RE.test(text);
|
|
3510
3551
|
const sizeShift = tokenAcc > 0 && tokenAcc + messageTokens > pc.maxChunkTokens;
|
|
3511
3552
|
const breakBefore = !closesActiveTool && i > startIdx && tokenAcc >= pc.minChunkTokens && (fileShift || userShift || sizeShift) && topics.length < maxSegs - 1;
|
|
3512
3553
|
if (breakBefore) {
|
|
@@ -3568,7 +3609,7 @@ function buildTimeline(msgs, errors) {
|
|
|
3568
3609
|
const m = msgs[i];
|
|
3569
3610
|
if (m.role === "user") {
|
|
3570
3611
|
const txt = extractText(m.content);
|
|
3571
|
-
if (!txt.startsWith("/"))
|
|
3612
|
+
if (!txt.startsWith("/") && !isHarnessInjectedText(txt))
|
|
3572
3613
|
timeline.push({
|
|
3573
3614
|
index: i,
|
|
3574
3615
|
event: "user_request",
|
|
@@ -3599,7 +3640,7 @@ function extractMainGoal(msgs) {
|
|
|
3599
3640
|
if (m?.role !== "user")
|
|
3600
3641
|
continue;
|
|
3601
3642
|
const text = extractText(m.content).trim();
|
|
3602
|
-
if (!text)
|
|
3643
|
+
if (!text || isHarnessInjectedText(text))
|
|
3603
3644
|
continue;
|
|
3604
3645
|
if (HISTORY_SUMMARY_RE.test(text)) {
|
|
3605
3646
|
const carried = summaryEvidenceLine(findSection(text, "goal")?.body ?? "", TRUNC.MESSAGE);
|
|
@@ -3667,7 +3708,7 @@ function extractOpenLoops(msgs, extraction) {
|
|
|
3667
3708
|
if (msg.role !== "user")
|
|
3668
3709
|
continue;
|
|
3669
3710
|
const txt = extractText(msg.content);
|
|
3670
|
-
if (txt.length < 10 || txt.startsWith("/"))
|
|
3711
|
+
if (txt.length < 10 || txt.startsWith("/") || isHarnessInjectedText(txt))
|
|
3671
3712
|
continue;
|
|
3672
3713
|
if (FOLLOWUP_RE.test(txt)) {
|
|
3673
3714
|
const isDup = loops.some((l) => isNearbyDuplicate(l, txt, idx));
|
|
@@ -3690,7 +3731,7 @@ function extractOpenLoops(msgs, extraction) {
|
|
|
3690
3731
|
if (msg.role !== "user")
|
|
3691
3732
|
continue;
|
|
3692
3733
|
const txt = extractText(msg.content);
|
|
3693
|
-
if (txt.length < 10 || txt.startsWith("/"))
|
|
3734
|
+
if (txt.length < 10 || txt.startsWith("/") || isHarnessInjectedText(txt))
|
|
3694
3735
|
continue;
|
|
3695
3736
|
if (BLOCKED_RE.test(txt)) {
|
|
3696
3737
|
const isDup = loops.some((l) => isNearbyDuplicate(l, txt, idx));
|
|
@@ -3771,7 +3812,7 @@ function extractStructured(msgs, pc, precomputedTcIdx) {
|
|
|
3771
3812
|
};
|
|
3772
3813
|
const evidenceOverflow = Object.fromEntries(Object.entries(overflow).filter(([, count]) => count > 0));
|
|
3773
3814
|
const mainGoal = extractMainGoal(msgs);
|
|
3774
|
-
const lastUserMessages = msgs.filter((m) => m.role === "user").slice(-5).map((m) => extractText(m.content));
|
|
3815
|
+
const lastUserMessages = msgs.filter((m) => m.role === "user" && !isHarnessInjectedText(extractText(m.content))).slice(-5).map((m) => extractText(m.content));
|
|
3775
3816
|
const lastErrors = errors.slice(-3).map((e) => e.message);
|
|
3776
3817
|
return {
|
|
3777
3818
|
modifiedFiles,
|
|
@@ -4726,22 +4767,22 @@ function reconcileCachedErrors(errors, deltaMessages, deltaToolCalls, baseMsgCou
|
|
|
4726
4767
|
const message = deltaMessages[j];
|
|
4727
4768
|
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
4728
4769
|
continue;
|
|
4729
|
-
const
|
|
4730
|
-
if (!
|
|
4770
|
+
const retries = message.content.flatMap(flattenToolCallBlock).filter((call) => toolOperationSignature(call.name, call.arguments) === error.operationSignature);
|
|
4771
|
+
if (!retries.length)
|
|
4731
4772
|
continue;
|
|
4732
4773
|
retryAttempted = true;
|
|
4733
|
-
for (
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
if (matches) {
|
|
4740
|
-
resolved = true;
|
|
4741
|
-
break;
|
|
4774
|
+
for (const retry of retries) {
|
|
4775
|
+
for (let k = j + 1;k < Math.min(deltaMessages.length, j + ERROR_RESOLVE_WINDOW); k++) {
|
|
4776
|
+
if (resolvesErrorResult(deltaMessages[k], retry, deltaToolCalls)) {
|
|
4777
|
+
resolved = true;
|
|
4778
|
+
break;
|
|
4779
|
+
}
|
|
4742
4780
|
}
|
|
4781
|
+
if (resolved)
|
|
4782
|
+
break;
|
|
4743
4783
|
}
|
|
4744
|
-
|
|
4784
|
+
if (resolved)
|
|
4785
|
+
break;
|
|
4745
4786
|
}
|
|
4746
4787
|
return { ...error, retryAttempted, resolved };
|
|
4747
4788
|
});
|
|
@@ -6456,9 +6497,32 @@ function hasListedPath(listed, file, display, normalizedOwners) {
|
|
|
6456
6497
|
}
|
|
6457
6498
|
return false;
|
|
6458
6499
|
}
|
|
6459
|
-
|
|
6500
|
+
var EVIDENCE_LINE_LABEL_RE = /^(?:(?:constraint|goal|decision|critical|open loop|unresolved error|resolved error|\[focus\]|\[note\]):?\s*)+/i;
|
|
6501
|
+
var EVIDENCE_QUOTE_MIN_CHARS = 80;
|
|
6502
|
+
function isRequiredEvidenceLine(line, corpus) {
|
|
6503
|
+
const key = normalizeFactKey(line.replace(/\*\*/g, "").replace(EVIDENCE_LINE_LABEL_RE, ""));
|
|
6504
|
+
if (!key)
|
|
6505
|
+
return false;
|
|
6506
|
+
return corpus.some((text) => text === key || key.length >= EVIDENCE_QUOTE_MIN_CHARS && text.startsWith(key));
|
|
6507
|
+
}
|
|
6508
|
+
function requiredEvidenceCorpus(collected, continuity) {
|
|
6509
|
+
const texts = [
|
|
6510
|
+
...collected.constraints.map((item) => item.text),
|
|
6511
|
+
...collected.unresolved.map((item) => item.message),
|
|
6512
|
+
...collected.resolved.map((item) => item.message),
|
|
6513
|
+
...collected.goal ? [collected.goal] : [],
|
|
6514
|
+
...continuity?.criticalContext ?? [],
|
|
6515
|
+
...collected.decisions.flatMap((item) => {
|
|
6516
|
+
const question = summaryEvidenceLine(item.summary, TRUNC.DECISION_SUMMARY);
|
|
6517
|
+
const answer = item.answer ? summaryEvidenceLine(item.answer, TRUNC.USER_RESPONSE) : "";
|
|
6518
|
+
return answer ? [item.summary, item.answer, question + " \u2192 " + answer] : [item.summary];
|
|
6519
|
+
})
|
|
6520
|
+
];
|
|
6521
|
+
return Array.from(new Set(texts.map((text) => normalizeFactKey(text.replace(/\*\*/g, ""))).filter(Boolean)));
|
|
6522
|
+
}
|
|
6523
|
+
function outcomeClaims(summary, pathEvidence, evidenceCorpus = []) {
|
|
6460
6524
|
const pathLines = new Set(Array.from(pathEvidence, ([path, display]) => [path, display, "`" + path + "`"]).flat());
|
|
6461
|
-
return Array.from(new Set(summary.split(/\r?\n/).map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").replace(/^\[[ x]\]\s+/i, "").trim()).filter((line) => line.length > 0 && !line.startsWith("#") && !pathLines.has(line)).filter((line) => HIGH_RISK_OUTCOME_RE.test(line)).filter((line) => /\bno\s+(?:errors?|failures?)\b/i.test(line) || !NEGATED_OUTCOME_RE.test(line)))).slice(0, 12);
|
|
6525
|
+
return Array.from(new Set(summary.split(/\r?\n/).map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "").replace(/^\[[ x]\]\s+/i, "").trim()).filter((line) => line.length > 0 && !line.startsWith("#") && !pathLines.has(line)).filter((line) => HIGH_RISK_OUTCOME_RE.test(line)).filter((line) => !isRequiredEvidenceLine(line, evidenceCorpus)).filter((line) => /\bno\s+(?:errors?|failures?)\b/i.test(line) || !NEGATED_OUTCOME_RE.test(line)))).slice(0, 12);
|
|
6462
6526
|
}
|
|
6463
6527
|
function classifyOutcomeClaim(claim) {
|
|
6464
6528
|
const lower = claim.toLowerCase();
|
|
@@ -6579,7 +6643,7 @@ function formatVerificationGap(gap) {
|
|
|
6579
6643
|
case "missing-constraint":
|
|
6580
6644
|
return "Missing constraint: " + gap.text.slice(0, TRUNC.TOPIC_LABEL);
|
|
6581
6645
|
case "missing-decision":
|
|
6582
|
-
return "Missing decision: " + gap.summary.slice(0, TRUNC.TOPIC_LABEL);
|
|
6646
|
+
return "Missing decision: " + gap.summary.slice(0, TRUNC.TOPIC_LABEL) + (gap.answer ? " \u2192 " + gap.answer.slice(0, TRUNC.TOPIC_LABEL) : "");
|
|
6583
6647
|
case "missing-goal":
|
|
6584
6648
|
return "Main goal may be missing from summary";
|
|
6585
6649
|
case "fabricated-file":
|
|
@@ -6757,7 +6821,8 @@ function stemToken(token) {
|
|
|
6757
6821
|
return lower;
|
|
6758
6822
|
}
|
|
6759
6823
|
function semanticTokens(text) {
|
|
6760
|
-
|
|
6824
|
+
const normalized = text.normalize("NFKC").replace(/[\u{2018}\u{2019}\u{02BC}`\u{00B4}]/gu, "'").replace(/\bcannot\b/gi, "can not").replace(/\b(\w+)n't\b/gi, "$1 not").replace(/^\s*(?:Constraint|Goal|Decision):\s*/i, "");
|
|
6825
|
+
return (normalized.match(/[\p{L}\p{N}_-]+/gu) ?? []).map(stemToken).filter((token) => token.length > 2 || NEGATION_MARKERS.has(token));
|
|
6761
6826
|
}
|
|
6762
6827
|
var semanticShapeCache = new Map;
|
|
6763
6828
|
var semanticFragmentCache = new Map;
|
|
@@ -6808,7 +6873,10 @@ function hasSemanticEvidence(source, target) {
|
|
|
6808
6873
|
if (!concepts.length)
|
|
6809
6874
|
return true;
|
|
6810
6875
|
const required = Math.min(concepts.length, Math.max(1, Math.ceil(concepts.length * 0.6)));
|
|
6876
|
+
const verbatim = sourceTokens.join(" ");
|
|
6811
6877
|
return semanticFragments(target).some((tokens) => {
|
|
6878
|
+
if (tokens.join(" ") === verbatim)
|
|
6879
|
+
return true;
|
|
6812
6880
|
const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
|
|
6813
6881
|
if (overlap < required)
|
|
6814
6882
|
return false;
|
|
@@ -6825,14 +6893,15 @@ function hasSemanticEvidence(source, target) {
|
|
|
6825
6893
|
return true;
|
|
6826
6894
|
});
|
|
6827
6895
|
}
|
|
6828
|
-
function hasSemanticContradiction(source, target) {
|
|
6896
|
+
function hasSemanticContradiction(source, target, exempt) {
|
|
6829
6897
|
const sourceFragments = new Set(semanticFragments(source).map((tokens) => tokens.join(" ")));
|
|
6830
6898
|
const { sourceTokens, concepts, anchor, negative, conditional } = semanticShape(source);
|
|
6831
6899
|
if (!anchor)
|
|
6832
6900
|
return false;
|
|
6833
6901
|
const required = Math.min(concepts.length, Math.max(1, Math.ceil(concepts.length * 0.6)));
|
|
6834
6902
|
return semanticFragments(target).some((tokens) => {
|
|
6835
|
-
|
|
6903
|
+
const key = tokens.join(" ");
|
|
6904
|
+
if (!tokens.includes(anchor) || sourceFragments.has(key) || exempt?.has(key))
|
|
6836
6905
|
return false;
|
|
6837
6906
|
const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
|
|
6838
6907
|
if (overlap < required)
|
|
@@ -6848,15 +6917,114 @@ function hasSemanticContradiction(source, target) {
|
|
|
6848
6917
|
});
|
|
6849
6918
|
}
|
|
6850
6919
|
var TEMPORAL_CONSTRAINT_RE = /\b(?:yet|until|before|first|for now|hold off|wait|hen\u00FCz|\u015Fimdilik|\u00F6nce)\b/iu;
|
|
6920
|
+
var WH_QUESTION_LEAD_RE = /^(?:why|what|how|when|where|who|whom|which|whose|neden|ni\u00e7in|niye|nas\u0131l|hangi|kim)\b/iu;
|
|
6921
|
+
var AUX_QUESTION_LEAD_RE = /^(?:do|does|did|can|could|should|would|will|has|have|had|is|are|was|were|am)\b/i;
|
|
6922
|
+
var QUESTION_SUBJECT_RE = /^(?:you|u|i|we|it|they|he|she|this|that|these|those|there)\b/i;
|
|
6923
|
+
function looksLikeQuestion(text) {
|
|
6924
|
+
return text.split(/\r?\n/).some((line) => {
|
|
6925
|
+
const trimmed = line.trim();
|
|
6926
|
+
if (trimmed.endsWith("?"))
|
|
6927
|
+
return true;
|
|
6928
|
+
if (WH_QUESTION_LEAD_RE.test(trimmed))
|
|
6929
|
+
return true;
|
|
6930
|
+
const rest = trimmed.replace(AUX_QUESTION_LEAD_RE, "").trim();
|
|
6931
|
+
return AUX_QUESTION_LEAD_RE.test(trimmed) && QUESTION_SUBJECT_RE.test(rest);
|
|
6932
|
+
});
|
|
6933
|
+
}
|
|
6934
|
+
var FIRST_PERSON_GRANT_RE = /\b(?:i|we)(?:'ve|\s+have)?\s+(?:approv\w+|allow\w+|permitt?\w*|ok(?:ay(?:ed)?)?|confirm\w+|green[- ]?light\w*|sanction\w*)\b/iu;
|
|
6935
|
+
var QUOTATION_RE = /\b(?:the log|logs?\b|the (?:output|console|error|diff|dashboard)|according to|it says|it said|they said|reported that|quoted?)\b/iu;
|
|
6936
|
+
var DENIAL_RE = /\b(?:denied|deny|refus\w*|forbid\w*|retract\w+|did not ask|didn't ask|never asked|never said|not allowed|not permitted|no permission|without permission|wasn't asked|weren't asked)\b/iu;
|
|
6937
|
+
var CRITICISM_RE = /\b(?:why|how come|explain(?:ing|ed)?|tell me|show me|describe|justify|what were you|supposed to|weren't you|shouldn't you|neden|ni\u00e7in|niye|a\u00e7\u0131kla|anlat|nas\u0131l)\b/iu;
|
|
6938
|
+
var ACK_STRIP_RE = /^(?:ok(?:ay)?|okey|yes|yep|sure|alright|right|now|actually|please|just|go ahead|go for it|proceed|do it|ship it|approved?|alright then|tamam|olur|onay(?:l\u0131yorum|lanm\u0131\u015ft\u0131r| ver)?|devam(?:\s+et)?|ba\u015fla)[,;:.!\s]+/i;
|
|
6939
|
+
var POLICY_GRANT_PREDICATE_RE = /\b(?:are|is|was|were|be|been|remain(?:s|ed)?)\s+(?:now\s+|hereby\s+|officially\s+)?(?:allowed|permitted|approved|authorized|acceptable|optional|fine|okay|ok|enabled|lifted)|\bno longer\s+(?:required|needed|banned|prohibited|forbidden|necessary)|\bcan\s+be\b|\bmay\s+be\b/i;
|
|
6940
|
+
function addresseeModalGrant(anchor) {
|
|
6941
|
+
return new RegExp("\\b(?:you|y'?all|we|i)\\s+(?:can|could|may|might)\\s+(?:now\\s+|also\\s+|please\\s+|just\\s+)*(?:[\\p{L}]+\\s+){0,2}" + anchor.replace(/[^\p{L}\p{N}_-]/gu, "\\$&") + "\\b", "iu");
|
|
6942
|
+
}
|
|
6943
|
+
var IMPERATIVE_LEAD_TOKENS = new Set([
|
|
6944
|
+
"do",
|
|
6945
|
+
"does",
|
|
6946
|
+
"did",
|
|
6947
|
+
"please",
|
|
6948
|
+
"now",
|
|
6949
|
+
"just",
|
|
6950
|
+
"go"
|
|
6951
|
+
]);
|
|
6952
|
+
var DIRECTIVE_COMPLEMENTS = new Set([
|
|
6953
|
+
"it",
|
|
6954
|
+
"them",
|
|
6955
|
+
"this",
|
|
6956
|
+
"that",
|
|
6957
|
+
"these",
|
|
6958
|
+
"those",
|
|
6959
|
+
"everything",
|
|
6960
|
+
"now",
|
|
6961
|
+
"again",
|
|
6962
|
+
"please",
|
|
6963
|
+
"immediately",
|
|
6964
|
+
"today",
|
|
6965
|
+
"tonight",
|
|
6966
|
+
"away",
|
|
6967
|
+
"ahead",
|
|
6968
|
+
"the",
|
|
6969
|
+
"a",
|
|
6970
|
+
"an",
|
|
6971
|
+
"our",
|
|
6972
|
+
"your",
|
|
6973
|
+
"my",
|
|
6974
|
+
"his",
|
|
6975
|
+
"her",
|
|
6976
|
+
"their"
|
|
6977
|
+
]);
|
|
6978
|
+
function anchorFollowedByComplement(clause, anchor) {
|
|
6979
|
+
const words = clause.split(/\s+/).map((word) => word.replace(/(^\W+|\W+$)/g, "").toLowerCase()).filter(Boolean);
|
|
6980
|
+
for (let i = 0;i < words.length; i++) {
|
|
6981
|
+
if (stemToken(words[i]) !== anchor)
|
|
6982
|
+
continue;
|
|
6983
|
+
const next = words[i + 1];
|
|
6984
|
+
return next !== undefined && DIRECTIVE_COMPLEMENTS.has(stemToken(next));
|
|
6985
|
+
}
|
|
6986
|
+
return false;
|
|
6987
|
+
}
|
|
6988
|
+
function positiveImperativeEvidence(release, anchor, allowImperative) {
|
|
6989
|
+
let rest = release.trim();
|
|
6990
|
+
for (let round = 0;round < 3 && ACK_STRIP_RE.test(rest); round++)
|
|
6991
|
+
rest = rest.replace(ACK_STRIP_RE, "").trim();
|
|
6992
|
+
const modalGrant = addresseeModalGrant(anchor);
|
|
6993
|
+
const clauses = rest.split(/\r?\n|[.;]/).flatMap((fragment) => fragment.split(/\s(?:[\u2014\u2013]|-)\s|,\s|;\s|:\s/)).map((clause) => clause.trim()).filter(Boolean);
|
|
6994
|
+
return clauses.some((clause) => {
|
|
6995
|
+
const tokens = semanticTokens(clause);
|
|
6996
|
+
if (!tokens.includes(anchor) || hasEffectiveTargetNegation(tokens, anchor))
|
|
6997
|
+
return false;
|
|
6998
|
+
if (allowImperative && IMPERATIVE_LEAD_TOKENS.has(tokens[0] ?? "") && tokens[1] === anchor)
|
|
6999
|
+
return true;
|
|
7000
|
+
if (POLICY_GRANT_PREDICATE_RE.test(clause))
|
|
7001
|
+
return true;
|
|
7002
|
+
if (modalGrant.test(clause))
|
|
7003
|
+
return true;
|
|
7004
|
+
if (allowImperative && tokens[0] === anchor && anchorFollowedByComplement(clause, anchor))
|
|
7005
|
+
return true;
|
|
7006
|
+
return false;
|
|
7007
|
+
});
|
|
7008
|
+
}
|
|
6851
7009
|
function releasesConstraint(constraint, release) {
|
|
6852
|
-
if (
|
|
6853
|
-
return
|
|
6854
|
-
if (
|
|
7010
|
+
if (looksLikeQuestion(release))
|
|
7011
|
+
return false;
|
|
7012
|
+
if (CRITICISM_RE.test(release))
|
|
7013
|
+
return false;
|
|
7014
|
+
if (QUOTATION_RE.test(release))
|
|
7015
|
+
return false;
|
|
7016
|
+
if (DENIAL_RE.test(release))
|
|
6855
7017
|
return false;
|
|
6856
7018
|
const { anchor, negative } = semanticShape(constraint);
|
|
7019
|
+
const deferral = TEMPORAL_CONSTRAINT_RE.test(constraint);
|
|
7020
|
+
if (hasSemanticContradiction(constraint, release)) {
|
|
7021
|
+
return FIRST_PERSON_GRANT_RE.test(release) || Boolean(anchor) && positiveImperativeEvidence(release, anchor, deferral);
|
|
7022
|
+
}
|
|
7023
|
+
if (!deferral)
|
|
7024
|
+
return false;
|
|
6857
7025
|
if (!anchor || !negative)
|
|
6858
7026
|
return false;
|
|
6859
|
-
return
|
|
7027
|
+
return positiveImperativeEvidence(release, anchor, true);
|
|
6860
7028
|
}
|
|
6861
7029
|
function isDeterministicallyPatchable(gap) {
|
|
6862
7030
|
if (gap.kind === "fabricated-file")
|
|
@@ -6887,6 +7055,105 @@ function repairSummaryDeterministically(summary, result, extraction, continuity
|
|
|
6887
7055
|
}
|
|
6888
7056
|
return { summary, result, patched };
|
|
6889
7057
|
}
|
|
7058
|
+
function questionOverlapOnLine(question, line) {
|
|
7059
|
+
const { concepts } = semanticShape(question);
|
|
7060
|
+
if (!concepts.length)
|
|
7061
|
+
return 0;
|
|
7062
|
+
const required = Math.min(concepts.length, Math.max(1, Math.ceil(concepts.length * 0.6)));
|
|
7063
|
+
const tokens = semanticTokens(line);
|
|
7064
|
+
const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
|
|
7065
|
+
return overlap >= required ? overlap : 0;
|
|
7066
|
+
}
|
|
7067
|
+
function exactQuestionOwner(head, questions) {
|
|
7068
|
+
const headKey = normalizeFactKey(head);
|
|
7069
|
+
if (!headKey)
|
|
7070
|
+
return null;
|
|
7071
|
+
let owner = null;
|
|
7072
|
+
let ownerLength = -1;
|
|
7073
|
+
for (const question of questions) {
|
|
7074
|
+
const key = normalizeFactKey(question);
|
|
7075
|
+
if (key && headKey.includes(key) && key.length > ownerLength) {
|
|
7076
|
+
owner = question;
|
|
7077
|
+
ownerLength = key.length;
|
|
7078
|
+
}
|
|
7079
|
+
}
|
|
7080
|
+
return owner;
|
|
7081
|
+
}
|
|
7082
|
+
function lineBelongsToQuestion(question, head, questions) {
|
|
7083
|
+
if (exactQuestionOwner(head, questions) === question)
|
|
7084
|
+
return true;
|
|
7085
|
+
const own = questionOverlapOnLine(question, head);
|
|
7086
|
+
if (!own)
|
|
7087
|
+
return false;
|
|
7088
|
+
return questions.every((other) => other === question || questionOverlapOnLine(other, head) < own);
|
|
7089
|
+
}
|
|
7090
|
+
function containsAnswerText(haystack, answer) {
|
|
7091
|
+
const needle = answer.trim().toLowerCase();
|
|
7092
|
+
if (!needle)
|
|
7093
|
+
return false;
|
|
7094
|
+
const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7095
|
+
return new RegExp("(?<![\\p{L}\\p{N}])" + escaped + "(?![\\p{L}\\p{N}])", "iu").test(haystack);
|
|
7096
|
+
}
|
|
7097
|
+
function lineCarriesAnswer(haystack, answer) {
|
|
7098
|
+
if (containsAnswerText(haystack, answer))
|
|
7099
|
+
return true;
|
|
7100
|
+
const { concepts } = semanticShape(answer);
|
|
7101
|
+
return concepts.length > 0 && hasSemanticEvidence(answer, haystack);
|
|
7102
|
+
}
|
|
7103
|
+
function decisionLineSlots(line, question, questions) {
|
|
7104
|
+
let fuzzy = null;
|
|
7105
|
+
for (let index = line.indexOf("\u2192");index >= 0; index = line.indexOf("\u2192", index + 1)) {
|
|
7106
|
+
const candidateHead = line.slice(0, index).replace(/\*+/g, "").trim();
|
|
7107
|
+
const tail = line.slice(index + 1).replace(/\*+/g, "").trim();
|
|
7108
|
+
if (!candidateHead || !tail)
|
|
7109
|
+
continue;
|
|
7110
|
+
if (exactQuestionOwner(candidateHead, questions) === question) {
|
|
7111
|
+
return { head: candidateHead, tail };
|
|
7112
|
+
}
|
|
7113
|
+
if (!fuzzy && lineBelongsToQuestion(question, candidateHead, questions)) {
|
|
7114
|
+
fuzzy = { head: candidateHead, tail };
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7117
|
+
return fuzzy;
|
|
7118
|
+
}
|
|
7119
|
+
function stripDecisionAnswerTails(decisionBody, questions) {
|
|
7120
|
+
return decisionBody.split(/\r?\n/).map((line) => {
|
|
7121
|
+
let fuzzyCut = -1;
|
|
7122
|
+
for (let index = line.indexOf("\u2192");index >= 0; index = line.indexOf("\u2192", index + 1)) {
|
|
7123
|
+
const candidateHead = line.slice(0, index).replace(/\*+/g, "").trim();
|
|
7124
|
+
if (!candidateHead)
|
|
7125
|
+
continue;
|
|
7126
|
+
if (exactQuestionOwner(candidateHead, questions) !== null) {
|
|
7127
|
+
return line.slice(0, index);
|
|
7128
|
+
}
|
|
7129
|
+
if (fuzzyCut < 0 && questions.some((question) => lineBelongsToQuestion(question, candidateHead, questions))) {
|
|
7130
|
+
fuzzyCut = index;
|
|
7131
|
+
}
|
|
7132
|
+
}
|
|
7133
|
+
return fuzzyCut >= 0 ? line.slice(0, fuzzyCut) : line;
|
|
7134
|
+
}).join(`
|
|
7135
|
+
`);
|
|
7136
|
+
}
|
|
7137
|
+
function decisionAnswerPresent(question, answer, decisionBody, questions) {
|
|
7138
|
+
return decisionBody.split(/\r?\n/).some((rawLine) => {
|
|
7139
|
+
const line = rawLine.replace(/^\s*(?:[-*+]\s+)?/, "").trim();
|
|
7140
|
+
return decisionLineSlots(line, question, questions) !== null && lineCarriesAnswer(line, answer);
|
|
7141
|
+
});
|
|
7142
|
+
}
|
|
7143
|
+
function decisionAnswerConflict(question, answer, decisionBody, questions) {
|
|
7144
|
+
for (const rawLine of decisionBody.split(/\r?\n/)) {
|
|
7145
|
+
const line = rawLine.replace(/^\s*(?:[-*+]\s+)?/, "").trim();
|
|
7146
|
+
const slots = decisionLineSlots(line, question, questions);
|
|
7147
|
+
if (!slots || !slots.tail)
|
|
7148
|
+
continue;
|
|
7149
|
+
if (hasSemanticEvidence(question, slots.tail))
|
|
7150
|
+
continue;
|
|
7151
|
+
if (lineCarriesAnswer(slots.tail, answer))
|
|
7152
|
+
continue;
|
|
7153
|
+
return true;
|
|
7154
|
+
}
|
|
7155
|
+
return false;
|
|
7156
|
+
}
|
|
6890
7157
|
function addGap(accumulator, gap, penalty) {
|
|
6891
7158
|
accumulator.gaps.push(gap);
|
|
6892
7159
|
accumulator.score -= penalty;
|
|
@@ -6929,10 +7196,14 @@ function collectVerificationEvidence(extraction, continuity, evidence) {
|
|
|
6929
7196
|
...(continuity?.constraints ?? []).flatMap((item) => item.confidence >= 0.8 ? [{ text: item.text }] : []),
|
|
6930
7197
|
...steeringConstraints
|
|
6931
7198
|
], (item) => item.text).filter((item) => !isNonLiveConstraintText(item.text) && !retired.has(normalizeFactKey(item.text)));
|
|
6932
|
-
const
|
|
6933
|
-
...
|
|
6934
|
-
...
|
|
6935
|
-
]
|
|
7199
|
+
const decisionEntries = [
|
|
7200
|
+
...continuity?.decisions ?? [],
|
|
7201
|
+
...extraction.decisions
|
|
7202
|
+
].flatMap((item) => item.type === "explicit" ? [{ summary: item.summary, userResponse: item.userResponse }] : []);
|
|
7203
|
+
const decisions = collapseDecisionsByQuestion(decisionEntries).map((item) => ({
|
|
7204
|
+
summary: item.summary,
|
|
7205
|
+
answer: item.userResponse?.trim() || null
|
|
7206
|
+
}));
|
|
6936
7207
|
return {
|
|
6937
7208
|
unresolved,
|
|
6938
7209
|
resolved,
|
|
@@ -7012,7 +7283,39 @@ function verifyErrorEvidence(normalizedSummary, collected, accumulator) {
|
|
|
7012
7283
|
}
|
|
7013
7284
|
}
|
|
7014
7285
|
}
|
|
7015
|
-
|
|
7286
|
+
var EVIDENCE_RENDER_LIMITS = [
|
|
7287
|
+
TRUNC.CONSTRAINT_TEXT,
|
|
7288
|
+
TRUNC.MESSAGE,
|
|
7289
|
+
TRUNC.PREVIEW_MID,
|
|
7290
|
+
TRUNC.PREVIEW,
|
|
7291
|
+
TRUNC.DECISION_SUMMARY,
|
|
7292
|
+
TRUNC.USER_RESPONSE,
|
|
7293
|
+
TRUNC.DETAIL,
|
|
7294
|
+
TRUNC.TOPIC_LABEL
|
|
7295
|
+
];
|
|
7296
|
+
function requiredEvidenceKeysByPolarity(collected, continuity) {
|
|
7297
|
+
const texts = [
|
|
7298
|
+
...collected.constraints.map((item) => item.text),
|
|
7299
|
+
...collected.decisions.flatMap((item) => item.answer ? [item.summary, item.answer] : [item.summary]),
|
|
7300
|
+
...collected.unresolved.map((item) => item.message),
|
|
7301
|
+
...collected.resolved.map((item) => item.message),
|
|
7302
|
+
...collected.goal ? [collected.goal] : [],
|
|
7303
|
+
...continuity?.criticalContext ?? []
|
|
7304
|
+
];
|
|
7305
|
+
const keys = { negative: new Set, positive: new Set };
|
|
7306
|
+
for (const text of texts) {
|
|
7307
|
+
const bucket = semanticShape(text).negative ? keys.negative : keys.positive;
|
|
7308
|
+
const variants = [text, ...EVIDENCE_RENDER_LIMITS.map((max) => summaryEvidenceLine(text, max))];
|
|
7309
|
+
for (const variant of variants) {
|
|
7310
|
+
for (const tokens of semanticFragments(variant))
|
|
7311
|
+
bucket.add(tokens.join(" "));
|
|
7312
|
+
}
|
|
7313
|
+
}
|
|
7314
|
+
return keys;
|
|
7315
|
+
}
|
|
7316
|
+
function verifySemanticCoverage(parsed, collected, continuity, accumulator) {
|
|
7317
|
+
const evidenceKeys = requiredEvidenceKeysByPolarity(collected, continuity);
|
|
7318
|
+
const exemptFor = (source) => semanticShape(source).negative ? evidenceKeys.negative : evidenceKeys.positive;
|
|
7016
7319
|
const constraintTarget = [
|
|
7017
7320
|
findSection(parsed, "constraints")?.body ?? "",
|
|
7018
7321
|
findSection(parsed, "critical-context")?.body ?? ""
|
|
@@ -7022,7 +7325,7 @@ function verifySemanticCoverage(parsed, collected, accumulator) {
|
|
|
7022
7325
|
if (!hasSemanticEvidence(constraint.text, constraintTarget)) {
|
|
7023
7326
|
addGap(accumulator, { kind: "missing-constraint", text: constraint.text }, 8);
|
|
7024
7327
|
}
|
|
7025
|
-
if (hasSemanticContradiction(constraint.text, constraintTarget)) {
|
|
7328
|
+
if (hasSemanticContradiction(constraint.text, constraintTarget, exemptFor(constraint.text))) {
|
|
7026
7329
|
addGap(accumulator, {
|
|
7027
7330
|
kind: "inconsistency",
|
|
7028
7331
|
detail: "semantic-contradiction: constraint contradicts " + constraint.text.slice(0, TRUNC.SNIPPET)
|
|
@@ -7034,7 +7337,7 @@ function verifySemanticCoverage(parsed, collected, accumulator) {
|
|
|
7034
7337
|
if (!hasSemanticEvidence(collected.goal, goalTarget)) {
|
|
7035
7338
|
addGap(accumulator, { kind: "missing-goal", goal: collected.goal }, 12);
|
|
7036
7339
|
}
|
|
7037
|
-
if (hasSemanticContradiction(collected.goal, goalTarget)) {
|
|
7340
|
+
if (hasSemanticContradiction(collected.goal, goalTarget, exemptFor(collected.goal))) {
|
|
7038
7341
|
addGap(accumulator, {
|
|
7039
7342
|
kind: "inconsistency",
|
|
7040
7343
|
detail: "semantic-contradiction: goal polarity or condition changed"
|
|
@@ -7042,11 +7345,25 @@ function verifySemanticCoverage(parsed, collected, accumulator) {
|
|
|
7042
7345
|
}
|
|
7043
7346
|
}
|
|
7044
7347
|
const decisionBody = findSection(parsed, "decisions")?.body ?? "";
|
|
7348
|
+
const questions = collected.decisions.map((decision) => decision.summary);
|
|
7349
|
+
const decisionQuestionBody = stripDecisionAnswerTails(decisionBody, questions);
|
|
7045
7350
|
for (const decision of collected.decisions) {
|
|
7046
|
-
|
|
7047
|
-
|
|
7351
|
+
const questionVerified = hasSemanticEvidence(decision.summary, decisionQuestionBody);
|
|
7352
|
+
const answerVerified = !decision.answer || decisionAnswerPresent(decision.summary, decision.answer, decisionBody, questions);
|
|
7353
|
+
if (!questionVerified || !answerVerified) {
|
|
7354
|
+
addGap(accumulator, {
|
|
7355
|
+
kind: "missing-decision",
|
|
7356
|
+
summary: decision.summary,
|
|
7357
|
+
...decision.answer ? { answer: decision.answer } : {}
|
|
7358
|
+
}, 8);
|
|
7359
|
+
}
|
|
7360
|
+
if (decision.answer && decisionAnswerConflict(decision.summary, decision.answer, decisionBody, questions)) {
|
|
7361
|
+
addGap(accumulator, {
|
|
7362
|
+
kind: "inconsistency",
|
|
7363
|
+
detail: "decision-answer-conflict: summary carries a different active answer for " + decision.summary.slice(0, TRUNC.SNIPPET)
|
|
7364
|
+
}, 20);
|
|
7048
7365
|
}
|
|
7049
|
-
if (hasSemanticContradiction(decision.summary,
|
|
7366
|
+
if (hasSemanticContradiction(decision.summary, decisionQuestionBody, exemptFor(decision.summary))) {
|
|
7050
7367
|
addGap(accumulator, {
|
|
7051
7368
|
kind: "inconsistency",
|
|
7052
7369
|
detail: "semantic-contradiction: decision contradicts " + decision.summary.slice(0, TRUNC.SNIPPET)
|
|
@@ -7059,7 +7376,7 @@ function verifyFileReferences(summary, extraction, continuity, evidence, collect
|
|
|
7059
7376
|
...collected.unresolved.map((item) => item.message),
|
|
7060
7377
|
...collected.resolved.map((item) => item.message),
|
|
7061
7378
|
...collected.constraints.map((item) => item.text),
|
|
7062
|
-
...collected.decisions.map((item) => item.summary),
|
|
7379
|
+
...collected.decisions.map((item) => item.answer ? item.summary + " \u2192 " + item.answer : item.summary),
|
|
7063
7380
|
...collected.goal ? [collected.goal] : [],
|
|
7064
7381
|
...extraction.lastUserMessages,
|
|
7065
7382
|
...extraction.timeline.map((item) => item.summary),
|
|
@@ -7137,7 +7454,8 @@ function verifyOpenLoopsAndClaims(summary, parsed, paths, extraction, continuity
|
|
|
7137
7454
|
if (!evidence.sourceMessages)
|
|
7138
7455
|
return;
|
|
7139
7456
|
const tools = successfulToolEvidence(evidence.sourceMessages);
|
|
7140
|
-
|
|
7457
|
+
const evidenceCorpus = requiredEvidenceCorpus(collected, continuity);
|
|
7458
|
+
for (const claim of outcomeClaims(summary, paths.rendered, evidenceCorpus)) {
|
|
7141
7459
|
if (!successfulToolSupportsClaim(claim, tools, extraction)) {
|
|
7142
7460
|
addGap(accumulator, { kind: "unsupported-claim", claim }, 20);
|
|
7143
7461
|
}
|
|
@@ -7150,7 +7468,7 @@ function verifySummary(summary, extraction, continuity = null, evidence = {}) {
|
|
|
7150
7468
|
verifyRequiredSections(parsed, accumulator);
|
|
7151
7469
|
const paths = verifyPathCoverage(parsed, extraction, continuity, evidence, accumulator);
|
|
7152
7470
|
verifyErrorEvidence(summary.toLowerCase().replace(/\\/g, "/").replace(/\s+/g, " "), collected, accumulator);
|
|
7153
|
-
verifySemanticCoverage(parsed, collected, accumulator);
|
|
7471
|
+
verifySemanticCoverage(parsed, collected, continuity, accumulator);
|
|
7154
7472
|
verifyFileReferences(summary, extraction, continuity, evidence, collected, paths, accumulator);
|
|
7155
7473
|
verifyProgressConsistency(parsed, extraction, collected, paths, accumulator);
|
|
7156
7474
|
verifyOpenLoopsAndClaims(summary, parsed, paths, extraction, continuity, evidence, collected, accumulator);
|
|
@@ -7248,9 +7566,12 @@ function patchDeterministic(summary, gaps, extraction, continuity = null, eviden
|
|
|
7248
7566
|
case "missing-constraint":
|
|
7249
7567
|
canonical = appendToSection(canonical, "constraints", "- " + safe(gap.text, TRUNC.CONSTRAINT_TEXT));
|
|
7250
7568
|
break;
|
|
7251
|
-
case "missing-decision":
|
|
7252
|
-
|
|
7569
|
+
case "missing-decision": {
|
|
7570
|
+
const question = safe(gap.summary, TRUNC.DECISION_SUMMARY);
|
|
7571
|
+
const answer = gap.answer ? safe(gap.answer, TRUNC.USER_RESPONSE) : "";
|
|
7572
|
+
canonical = appendToSection(canonical, "decisions", "- **" + question + "**" + (answer ? " \u2192 " + answer : ""));
|
|
7253
7573
|
break;
|
|
7574
|
+
}
|
|
7254
7575
|
case "missing-goal":
|
|
7255
7576
|
canonical = upsertSection(canonical, "goal", safe(gap.goal, TRUNC.DETAIL) || "Continue the current task.");
|
|
7256
7577
|
break;
|
|
@@ -7309,9 +7630,12 @@ function hasUnclosedMarkdownFence(markdown) {
|
|
|
7309
7630
|
}
|
|
7310
7631
|
return open !== null;
|
|
7311
7632
|
}
|
|
7633
|
+
var TERMINAL_STOP_REASON_RE = /^(?:stop|endturn)$/i;
|
|
7312
7634
|
function patchResponseIsTruncated(patched, stopReason) {
|
|
7313
|
-
const reason = String(stopReason ?? "");
|
|
7314
|
-
|
|
7635
|
+
const reason = String(stopReason ?? "").trim();
|
|
7636
|
+
if (reason && !TERMINAL_STOP_REASON_RE.test(reason))
|
|
7637
|
+
return true;
|
|
7638
|
+
return /\u2026\u2702\d+\s*$/.test(patched) || hasUnclosedMarkdownFence(patched);
|
|
7315
7639
|
}
|
|
7316
7640
|
function sectionIdentity(section) {
|
|
7317
7641
|
return section.kind === "unknown" ? "unknown:" + section.heading.trim().toLowerCase() : section.kind;
|
|
@@ -7651,7 +7975,11 @@ function mergeOpenLoops(current, previous) {
|
|
|
7651
7975
|
function mergeCompactionStates(previous, current) {
|
|
7652
7976
|
if (!previous) {
|
|
7653
7977
|
const active = applyContinuityOverrides(current, current.factOverrides ?? []);
|
|
7654
|
-
return {
|
|
7978
|
+
return {
|
|
7979
|
+
...active,
|
|
7980
|
+
decisions: collapseDecisionsByQuestion(active.decisions).map((item, index) => ({ ...item, id: ID_PREFIX.DECISION + (index + 1) })),
|
|
7981
|
+
openLoops: mergeOpenLoops(active.openLoops, [])
|
|
7982
|
+
};
|
|
7655
7983
|
}
|
|
7656
7984
|
const factOverrides = mergeBy(current.factOverrides ?? [], previous.factOverrides ?? [], (item) => item.kind + ":" + item.summaryKey, 50);
|
|
7657
7985
|
const activeCurrent = applyContinuityOverrides(current, factOverrides);
|
|
@@ -7671,7 +7999,17 @@ function mergeCompactionStates(previous, current) {
|
|
|
7671
7999
|
}
|
|
7672
8000
|
return false;
|
|
7673
8001
|
};
|
|
7674
|
-
const
|
|
8002
|
+
const collapsedCurrent = collapseDecisionsByQuestion(activeCurrent.decisions);
|
|
8003
|
+
const collapsedPrevious = collapseDecisionsByQuestion(activePrevious.decisions);
|
|
8004
|
+
const authoritative = collapseDecisionsByQuestion([
|
|
8005
|
+
...collapsedPrevious,
|
|
8006
|
+
...collapsedCurrent
|
|
8007
|
+
]);
|
|
8008
|
+
const currentKeys = new Set(collapsedCurrent.map((item) => normalizeFactKey(item.summary)));
|
|
8009
|
+
const decisions = [
|
|
8010
|
+
...authoritative.filter((item) => currentKeys.has(normalizeFactKey(item.summary))),
|
|
8011
|
+
...authoritative.filter((item) => !currentKeys.has(normalizeFactKey(item.summary)))
|
|
8012
|
+
].slice(0, 30).map((item, index) => ({ ...item, id: ID_PREFIX.DECISION + (index + 1) }));
|
|
7675
8013
|
const constraints = mergeBy(activeCurrent.constraints, activePrevious.constraints, (item) => normalizeFactKey(item.text), 30).map((item, index) => ({ ...item, id: "constraint-" + (index + 1) }));
|
|
7676
8014
|
const unresolvedErrors = mergeBy(activeCurrent.unresolvedErrors, activePrevious.unresolvedErrors.filter((error) => !resolvedKeys.has(normalizeFactKey(error.message))), (item) => normalizeFactKey(item.message), 15).map((item, index) => ({ ...item, id: ID_PREFIX.ERROR + (index + 1) }));
|
|
7677
8015
|
const openLoops = mergeOpenLoops(activeCurrent.openLoops, activePrevious.openLoops.map((loop) => isResolvedLoop(loop) ? { ...loop, status: "resolved" } : loop));
|
|
@@ -7703,8 +8041,8 @@ function mergeCompactionStates(previous, current) {
|
|
|
7703
8041
|
function renderContinuityCapsule(state, maxChars = TRUNC.CONTINUITY_CAPSULE, existing = "") {
|
|
7704
8042
|
const haystack = normalizeFactKey(existing);
|
|
7705
8043
|
const lines = ["## Continuity Ledger"];
|
|
7706
|
-
const add = (label, value) => {
|
|
7707
|
-
const text = summaryEvidenceLine(value,
|
|
8044
|
+
const add = (label, value, maxLength = TRUNC.MESSAGE) => {
|
|
8045
|
+
const text = summaryEvidenceLine(value, maxLength);
|
|
7708
8046
|
if (!text || haystack.includes(normalizeFactKey(text)))
|
|
7709
8047
|
return;
|
|
7710
8048
|
const line = "- " + label + ": " + text;
|
|
@@ -7716,8 +8054,13 @@ function renderContinuityCapsule(state, maxChars = TRUNC.CONTINUITY_CAPSULE, exi
|
|
|
7716
8054
|
add("Goal", state.goal);
|
|
7717
8055
|
for (const item of state.constraints)
|
|
7718
8056
|
add("Constraint", item.text);
|
|
7719
|
-
for (const item of state.decisions)
|
|
7720
|
-
|
|
8057
|
+
for (const item of state.decisions) {
|
|
8058
|
+
const question = summaryEvidenceLine(item.summary, TRUNC.DECISION_SUMMARY);
|
|
8059
|
+
const answer = item.userResponse ? summaryEvidenceLine(item.userResponse, TRUNC.USER_RESPONSE) : "";
|
|
8060
|
+
const decisionLine = question + (answer ? " \u2192 " + answer : "");
|
|
8061
|
+
if (decisionLine)
|
|
8062
|
+
add("Decision", decisionLine, decisionLine.length);
|
|
8063
|
+
}
|
|
7721
8064
|
for (const item of state.unresolvedErrors)
|
|
7722
8065
|
add("Unresolved error", item.message);
|
|
7723
8066
|
for (const item of state.resolvedErrors.slice(-5))
|
|
@@ -8258,7 +8601,16 @@ function pendingMatchesBranch(pending, branch) {
|
|
|
8258
8601
|
}
|
|
8259
8602
|
function readerSignature(ctx) {
|
|
8260
8603
|
const model = ctx.model;
|
|
8261
|
-
|
|
8604
|
+
const prompt = ctx.getSystemPrompt?.();
|
|
8605
|
+
return model && JSON.stringify([
|
|
8606
|
+
model.provider,
|
|
8607
|
+
model.id,
|
|
8608
|
+
model.api,
|
|
8609
|
+
model.baseUrl,
|
|
8610
|
+
model.contextWindow,
|
|
8611
|
+
model.maxTokens,
|
|
8612
|
+
prompt === undefined ? null : createHash4("sha256").update(prompt).digest("hex")
|
|
8613
|
+
]);
|
|
8262
8614
|
}
|
|
8263
8615
|
function revalidatePending(pending, ctx, reserveTokens = 0) {
|
|
8264
8616
|
const model = ctx.model;
|
|
@@ -8298,7 +8650,7 @@ function revalidatePending(pending, ctx, reserveTokens = 0) {
|
|
|
8298
8650
|
};
|
|
8299
8651
|
}
|
|
8300
8652
|
function createPendingSlot(opts) {
|
|
8301
|
-
const ttlMs = opts.ttlMs;
|
|
8653
|
+
const ttlMs = () => typeof opts.ttlMs === "function" ? opts.ttlMs() : opts.ttlMs;
|
|
8302
8654
|
const now = opts.now ?? Date.now;
|
|
8303
8655
|
const maxEntries = Math.max(1, opts.maxEntries ?? 64);
|
|
8304
8656
|
const entries = new Map;
|
|
@@ -8318,7 +8670,7 @@ function createPendingSlot(opts) {
|
|
|
8318
8670
|
const current = now();
|
|
8319
8671
|
let removedNewest = false;
|
|
8320
8672
|
for (const [sessionId, entry] of entries) {
|
|
8321
|
-
if (current - entry.createdAt <= ttlMs)
|
|
8673
|
+
if (current - entry.createdAt <= ttlMs())
|
|
8322
8674
|
continue;
|
|
8323
8675
|
entries.delete(sessionId);
|
|
8324
8676
|
if (newestSessionId === sessionId)
|
|
@@ -8345,7 +8697,7 @@ function createPendingSlot(opts) {
|
|
|
8345
8697
|
const entry = entries.get(currentSessionId);
|
|
8346
8698
|
if (entry) {
|
|
8347
8699
|
const ageMs = now() - entry.createdAt;
|
|
8348
|
-
if (ageMs > ttlMs) {
|
|
8700
|
+
if (ageMs > ttlMs()) {
|
|
8349
8701
|
deleteEntry(currentSessionId);
|
|
8350
8702
|
prune();
|
|
8351
8703
|
return { kind: "expired", ageMs };
|
|
@@ -10758,6 +11110,7 @@ function buildExplorerRequest(extraction, model, prevSummary, userNote) {
|
|
|
10758
11110
|
function explorationToolSupportKey(model) {
|
|
10759
11111
|
return [model.provider, model.api, model.baseUrl ?? "", model.id].join("\x00");
|
|
10760
11112
|
}
|
|
11113
|
+
var SYNTHESIS_CALL_RESERVE = 2;
|
|
10761
11114
|
async function exploreConversation(llmMessages, extraction, model, auth, prevSummary, userNote, signal, maxRounds = MAX_EXPLORATION_ROUNDS, notify, services) {
|
|
10762
11115
|
const svc = services ?? getDefaultServices();
|
|
10763
11116
|
const request = buildExplorerRequest(extraction, model, prevSummary, userNote);
|
|
@@ -10768,6 +11121,10 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
10768
11121
|
const now = svc.clock.now();
|
|
10769
11122
|
const cachedSupport = toolSupport.get(cacheKey, now);
|
|
10770
11123
|
let supportsTools = cachedSupport === true;
|
|
11124
|
+
if (svc.budget.remainingCalls() <= SYNTHESIS_CALL_RESERVE) {
|
|
11125
|
+
notify?.("Explore skipped \xB7 remaining calls reserved for synthesis", "info");
|
|
11126
|
+
return { report: fallbackExplorationReport(llmMessages), rounds: 0, toolSupported: supportsTools };
|
|
11127
|
+
}
|
|
10771
11128
|
try {
|
|
10772
11129
|
if (cachedSupport === false) {
|
|
10773
11130
|
if (notify)
|
|
@@ -10810,7 +11167,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
10810
11167
|
});
|
|
10811
11168
|
}
|
|
10812
11169
|
let rounds = 1;
|
|
10813
|
-
while (rounds < maxRounds) {
|
|
11170
|
+
while (rounds < maxRounds && svc.budget.remainingCalls() > SYNTHESIS_CALL_RESERVE) {
|
|
10814
11171
|
rounds++;
|
|
10815
11172
|
let response;
|
|
10816
11173
|
try {
|
|
@@ -10897,6 +11254,9 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
10897
11254
|
return { report, rounds: 1, toolSupported: supportsTools };
|
|
10898
11255
|
}
|
|
10899
11256
|
async function explorationRetry(model, auth, llmMessages, extraction, userNote, signal, services) {
|
|
11257
|
+
const svc = services ?? getDefaultServices();
|
|
11258
|
+
if (svc.budget.remainingCalls() <= SYNTHESIS_CALL_RESERVE)
|
|
11259
|
+
return fallbackExplorationReport(llmMessages);
|
|
10900
11260
|
const last5 = llmMessages.slice(-5).map((m) => "[" + m?.role + "] " + extractText(m?.content).slice(0, TRUNC.PREVIEW)).join(`
|
|
10901
11261
|
`);
|
|
10902
11262
|
const retryPrompt = `IMPORTANT: Output ONLY valid raw JSON. No markdown. No explanation. No code fences. Just the JSON object.
|
|
@@ -10925,7 +11285,7 @@ User steering: ` + userNote : "");
|
|
|
10925
11285
|
headers: auth.headers,
|
|
10926
11286
|
maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens),
|
|
10927
11287
|
signal
|
|
10928
|
-
},
|
|
11288
|
+
}, svc);
|
|
10929
11289
|
const text = resp.content.filter((c) => c.type === "text").map((c) => c.text).join("").trim();
|
|
10930
11290
|
return parseExplorationReport(text, llmMessages);
|
|
10931
11291
|
} catch (e) {
|
|
@@ -10965,6 +11325,9 @@ Output ONLY JSON: {"mainGoal":"...","sessionType":"implementation|review|debuggi
|
|
|
10965
11325
|
};
|
|
10966
11326
|
}
|
|
10967
11327
|
async function directExploration(llmMessages, extraction, model, auth, prevSummary, userNote, signal, services) {
|
|
11328
|
+
const svc = services ?? getDefaultServices();
|
|
11329
|
+
if (svc.budget.remainingCalls() <= SYNTHESIS_CALL_RESERVE)
|
|
11330
|
+
return fallbackExplorationReport(llmMessages);
|
|
10968
11331
|
const request = buildDirectExplorationRequest(llmMessages, extraction, model, prevSummary, userNote);
|
|
10969
11332
|
try {
|
|
10970
11333
|
const resp = await trackedComplete("explore-direct", model, request.context, {
|
|
@@ -10972,7 +11335,7 @@ async function directExploration(llmMessages, extraction, model, auth, prevSumma
|
|
|
10972
11335
|
headers: auth.headers,
|
|
10973
11336
|
maxTokens: request.maxTokens,
|
|
10974
11337
|
signal
|
|
10975
|
-
},
|
|
11338
|
+
}, svc);
|
|
10976
11339
|
const text = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
10977
11340
|
`).trim();
|
|
10978
11341
|
return parseExplorationReport(text, llmMessages);
|
|
@@ -11452,6 +11815,8 @@ async function singlePassCompact(request, model, auth, signal, services) {
|
|
|
11452
11815
|
`).trim();
|
|
11453
11816
|
if (!summary.startsWith("##"))
|
|
11454
11817
|
throw new Error("Single-pass malformed output");
|
|
11818
|
+
if (patchResponseIsTruncated(summary, resp.stopReason))
|
|
11819
|
+
throw new Error("Single-pass truncated output");
|
|
11455
11820
|
return { summary, llmCalls: 1 };
|
|
11456
11821
|
}
|
|
11457
11822
|
function buildBatchRequest(batch, extraction, model, maxOutputTokens) {
|
|
@@ -11525,7 +11890,7 @@ async function summarizeBatch(batch, extraction, model, auth, signal, services,
|
|
|
11525
11890
|
try {
|
|
11526
11891
|
sectionMap = await attempt();
|
|
11527
11892
|
} catch (err) {
|
|
11528
|
-
if (err instanceof BatchSummaryFormatError && /non-terminal stop reason length/.test(err.message) && services?.thinkingLevels.summaryThinkingLevel !== "minimal") {
|
|
11893
|
+
if (err instanceof BatchSummaryFormatError && /non-terminal stop reason length/.test(err.message) && services?.thinkingLevels.summaryThinkingLevel !== "minimal" && (services ?? getDefaultServices()).budget.remainingCalls() > 1) {
|
|
11529
11894
|
debug("Batch synthesis exhausted the output budget (reasoning shared it); retrying once with minimal reasoning");
|
|
11530
11895
|
sectionMap = await attempt("minimal");
|
|
11531
11896
|
} else {
|
|
@@ -11604,8 +11969,11 @@ async function assembleLLM(summaries, extraction, report, model, auth, budget, p
|
|
|
11604
11969
|
maxTokens: Math.min(budget, getProviderCaps(model.provider).maxOutputTokens),
|
|
11605
11970
|
signal
|
|
11606
11971
|
}, services);
|
|
11607
|
-
|
|
11972
|
+
const assembled = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
11608
11973
|
`).trim();
|
|
11974
|
+
if (patchResponseIsTruncated(assembled, resp.stopReason))
|
|
11975
|
+
throw new Error("Assembly truncated output");
|
|
11976
|
+
return assembled;
|
|
11609
11977
|
}
|
|
11610
11978
|
function assembleFallback(summaries, extraction, steering = {}, budgetTokens = 6000, continuity = null) {
|
|
11611
11979
|
const safe = (value, max = TRUNC.PREVIEW_MID) => summaryEvidenceLine(value, max);
|
|
@@ -11620,7 +11988,7 @@ function assembleFallback(summaries, extraction, steering = {}, budgetTokens = 6
|
|
|
11620
11988
|
constraints.push("- [focus] Preserve detail about: " + safe(steering.focus, TRUNC.CONSTRAINT_TEXT));
|
|
11621
11989
|
if (steering.note?.trim())
|
|
11622
11990
|
constraints.push("- [note] " + safe(steering.note, TRUNC.CONSTRAINT_TEXT));
|
|
11623
|
-
const decisions = extraction.decisions.map((item) => {
|
|
11991
|
+
const decisions = collapseDecisionsByQuestion(extraction.decisions).map((item) => {
|
|
11624
11992
|
const summary = safe(item.summary, TRUNC.DECISION_SUMMARY);
|
|
11625
11993
|
const response = item.userResponse ? safe(item.userResponse, TRUNC.USER_RESPONSE) : "";
|
|
11626
11994
|
return summary ? "- **" + summary + "**" + (response ? " \u2192 " + response : "") : "";
|
|
@@ -11695,6 +12063,20 @@ function failedChunkSummary(ch) {
|
|
|
11695
12063
|
}
|
|
11696
12064
|
|
|
11697
12065
|
// src/app/stage-auth.ts
|
|
12066
|
+
function abortableAuth(invoke, signal) {
|
|
12067
|
+
return new Promise((resolve, reject) => {
|
|
12068
|
+
const auth = invoke();
|
|
12069
|
+
const onAbort = () => reject(signal.reason ?? new Error("Stage auth aborted"));
|
|
12070
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
12071
|
+
auth.then((value) => {
|
|
12072
|
+
signal.removeEventListener("abort", onAbort);
|
|
12073
|
+
resolve(value);
|
|
12074
|
+
}, (error) => {
|
|
12075
|
+
signal.removeEventListener("abort", onAbort);
|
|
12076
|
+
reject(error);
|
|
12077
|
+
});
|
|
12078
|
+
});
|
|
12079
|
+
}
|
|
11698
12080
|
async function resolveStageAuth(rc, stage) {
|
|
11699
12081
|
const model = stage === "summary" ? rc.summaryModel : stage === "explore" ? rc.segModel : rc.verifyModel;
|
|
11700
12082
|
const existing = stage === "summary" ? rc.summaryAuth : stage === "explore" ? rc.segAuth : rc.verifyAuth;
|
|
@@ -11715,11 +12097,13 @@ async function resolveStageAuth(rc, stage) {
|
|
|
11715
12097
|
rc.verifyAuth = shared;
|
|
11716
12098
|
return shared;
|
|
11717
12099
|
}
|
|
11718
|
-
|
|
12100
|
+
rc.cancellation.signal.throwIfAborted();
|
|
12101
|
+
const auth = await abortableAuth(() => rc.ctx.modelRegistry.getApiKeyAndHeaders(model), rc.cancellation.signal);
|
|
11719
12102
|
if (!auth.ok || !auth.apiKey) {
|
|
11720
12103
|
throw new Error("Authentication unavailable for " + stage + " route " + model.provider + "/" + model.id);
|
|
11721
12104
|
}
|
|
11722
12105
|
const resolved = { apiKey: auth.apiKey, headers: auth.headers };
|
|
12106
|
+
rc.cancellation.signal.throwIfAborted();
|
|
11723
12107
|
if (stage === "summary")
|
|
11724
12108
|
rc.summaryAuth = resolved;
|
|
11725
12109
|
else if (stage === "explore")
|
|
@@ -11862,7 +12246,7 @@ async function summarizeConversation(rc) {
|
|
|
11862
12246
|
cacheable = false;
|
|
11863
12247
|
generationFallbacks.push("single-pass generation failed");
|
|
11864
12248
|
recordIssue({ key: "synth.single-pass", message: "Single-pass synthesis used deterministic fallback (" + errorDetail(err) + ").", error: err });
|
|
11865
|
-
rc.notify("Single-pass generation stopped \xB7 using deterministic fallback [" + formatGenerationFailureForUi(err) + "]", "
|
|
12249
|
+
rc.notify("Single-pass generation stopped \xB7 using deterministic fallback [" + formatGenerationFailureForUi(err) + "]", "info");
|
|
11866
12250
|
finalSummary = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
|
|
11867
12251
|
method = "heuristic";
|
|
11868
12252
|
}
|
|
@@ -11960,7 +12344,7 @@ async function summarizeConversation(rc) {
|
|
|
11960
12344
|
cacheable = false;
|
|
11961
12345
|
generationFallbacks.push("1 synthesis batch fallback");
|
|
11962
12346
|
recordIssue({ key: "synth.batch", message: "Synthesis batch used deterministic fallback (" + errorDetail(err) + ").", error: err });
|
|
11963
|
-
rc.notify("Synthesis batch stopped \xB7 deterministic evidence fallback preserved coverage [" + formatGenerationFailureForUi(err) + "]", "
|
|
12347
|
+
rc.notify("Synthesis batch stopped \xB7 deterministic evidence fallback preserved coverage [" + formatGenerationFailureForUi(err) + "]", "info");
|
|
11964
12348
|
showProgressOverlay(rc.ctx, {
|
|
11965
12349
|
phase: 3,
|
|
11966
12350
|
phaseName: "Synthesize",
|
|
@@ -11993,7 +12377,7 @@ async function summarizeConversation(rc) {
|
|
|
11993
12377
|
const idx = nextBatch++;
|
|
11994
12378
|
if (idx >= batchCallLimit)
|
|
11995
12379
|
return;
|
|
11996
|
-
if (budgetStopped || rc.services.budget.reason()) {
|
|
12380
|
+
if (budgetStopped || rc.services.budget.reason() || rc.services.budget.remainingCalls() <= 1) {
|
|
11997
12381
|
budgetStopped = true;
|
|
11998
12382
|
results[idx] = batches[idx].map((chunk) => failedChunkSummary(chunk));
|
|
11999
12383
|
} else {
|
|
@@ -12036,7 +12420,7 @@ async function summarizeConversation(rc) {
|
|
|
12036
12420
|
if (failedBatches.length) {
|
|
12037
12421
|
cacheable = false;
|
|
12038
12422
|
generationFallbacks.push(failedBatches.length + " synthesis batch fallback(s)");
|
|
12039
|
-
rc.notify(failedBatches.length + " synthesis batch(es) stopped \xB7 deterministic evidence fallback preserved coverage [" + formatGenerationFailureForUi(failedBatches[0]) + "]", "
|
|
12423
|
+
rc.notify(failedBatches.length + " synthesis batch(es) stopped \xB7 deterministic evidence fallback preserved coverage [" + formatGenerationFailureForUi(failedBatches[0]) + "]", "info");
|
|
12040
12424
|
showProgressOverlay(rc.ctx, {
|
|
12041
12425
|
phase: 3,
|
|
12042
12426
|
phaseName: "Synthesize",
|
|
@@ -12068,7 +12452,7 @@ async function summarizeConversation(rc) {
|
|
|
12068
12452
|
cacheable = false;
|
|
12069
12453
|
generationFallbacks.push("assembly generation failed");
|
|
12070
12454
|
recordIssue({ key: "synth.assembly", message: "Assembly used deterministic fallback (" + errorDetail(err) + ").", error: err });
|
|
12071
|
-
rc.notify("Assembly stopped \xB7 using deterministic fallback [" + formatGenerationFailureForUi(err) + "]", "
|
|
12455
|
+
rc.notify("Assembly stopped \xB7 using deterministic fallback [" + formatGenerationFailureForUi(err) + "]", "info");
|
|
12072
12456
|
method = "heuristic";
|
|
12073
12457
|
finalSummary = assembleFallback(summaries, extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
|
|
12074
12458
|
}
|
|
@@ -13113,11 +13497,30 @@ function supersededResults(projection, candidates) {
|
|
|
13113
13497
|
});
|
|
13114
13498
|
return result;
|
|
13115
13499
|
}
|
|
13116
|
-
function
|
|
13500
|
+
function thinkingEditReason(branch, entries, readerApi) {
|
|
13501
|
+
if (readerApi && readerApi !== "anthropic-messages")
|
|
13502
|
+
return;
|
|
13503
|
+
const edits = new Map(entries.flatMap((entry) => entry.type === "context_edit" ? [[entry.targetId, entry.replacement]] : []));
|
|
13504
|
+
let changed = false;
|
|
13505
|
+
for (const { sourceEntry, messages } of buildSessionProjection2(branch).entries) {
|
|
13506
|
+
if (edits.has(sourceEntry.id)) {
|
|
13507
|
+
changed = true;
|
|
13508
|
+
if (edits.get(sourceEntry.id) === null)
|
|
13509
|
+
continue;
|
|
13510
|
+
}
|
|
13511
|
+
if (changed && messages.some((message) => message.role === "assistant" && message.api === "anthropic-messages" && message.content.some((block) => block.type === "thinking" && Boolean(block.thinkingSignature)))) {
|
|
13512
|
+
return "Cleanup would invalidate retained signed Anthropic thinking. Use provider-native compaction instead.";
|
|
13513
|
+
}
|
|
13514
|
+
}
|
|
13515
|
+
}
|
|
13516
|
+
function planContextTrim(branch, afterId, options = {}) {
|
|
13117
13517
|
if (afterId && !branch.some((entry) => entry.id === afterId))
|
|
13118
13518
|
throw new Error("Trim boundary is not on the active branch.");
|
|
13119
13519
|
const candidates = archivableResults(branch);
|
|
13120
|
-
const
|
|
13520
|
+
const anchorBoundary = options.anchorBoundary === undefined ? lastAnchorBoundary(branch) : options.anchorBoundary;
|
|
13521
|
+
if (anchorBoundary && !branch.some((entry) => entry.id === anchorBoundary))
|
|
13522
|
+
throw new Error("Anchor boundary is not on the active branch.");
|
|
13523
|
+
const boundaries = [afterId, anchorBoundary].filter((id) => Boolean(id));
|
|
13121
13524
|
const protectedIds = new Set(boundaries.flatMap((id) => branch.slice(0, branch.findIndex((entry) => entry.id === id) + 1).map((entry) => entry.id)));
|
|
13122
13525
|
const edited = new Set(branch.flatMap((entry) => entry.type === "context_edit" ? [entry.targetId] : []));
|
|
13123
13526
|
const projection = buildSessionProjection2(branch).entries;
|
|
@@ -13155,6 +13558,18 @@ function planContextTrim(branch, afterId) {
|
|
|
13155
13558
|
archives.push({ id, sha256: digest(text), chars: text.length });
|
|
13156
13559
|
savedChars += text.length - marker.length;
|
|
13157
13560
|
}
|
|
13561
|
+
const blockedReason = thinkingEditReason(branch, entries, options.readerApi);
|
|
13562
|
+
if (blockedReason)
|
|
13563
|
+
return {
|
|
13564
|
+
entries: [],
|
|
13565
|
+
references: [],
|
|
13566
|
+
archives: [],
|
|
13567
|
+
savedChars: 0,
|
|
13568
|
+
automatic: "signed-thinking",
|
|
13569
|
+
cooldownTurns: 0,
|
|
13570
|
+
superseded: 0,
|
|
13571
|
+
blockedReason
|
|
13572
|
+
};
|
|
13158
13573
|
const supersededCount = eligible.filter((item) => superseded.has(item.id)).length;
|
|
13159
13574
|
if (entries.length)
|
|
13160
13575
|
entries.push(contextControlEntry({ version: 1, action: "trim", references, archives }));
|
|
@@ -13165,7 +13580,7 @@ function planContextTrim(branch, afterId) {
|
|
|
13165
13580
|
const turnsSinceChange = branch.slice(lastChange + 1).filter((entry) => entry.type === "message" && entry.message.role === "assistant").length;
|
|
13166
13581
|
const cooldownTurns = lastChange < 0 ? 0 : Math.max(0, AUTO_TRIM_COOLDOWN_TURNS - turnsSinceChange);
|
|
13167
13582
|
const automatic = savedChars < MIN_AUTO_TRIM_SAVING_CHARS ? "insufficient-savings" : cooldownTurns > 0 ? "cooldown" : "ready";
|
|
13168
|
-
return { entries, references, archives, savedChars, automatic, cooldownTurns, superseded: supersededCount };
|
|
13583
|
+
return { entries, references, archives, savedChars, automatic, cooldownTurns, superseded: supersededCount, blockedReason };
|
|
13169
13584
|
}
|
|
13170
13585
|
function trimEntries(plan, cause) {
|
|
13171
13586
|
const edits = plan.entries.filter((entry) => entry.type === "context_edit");
|
|
@@ -13203,7 +13618,7 @@ function trimBreakEvenRequests(cost, savedTokens, tailTokens) {
|
|
|
13203
13618
|
const w = typeof write === "number" && Number.isFinite(write) && write > 0 ? write / input : 1;
|
|
13204
13619
|
return r <= 0 ? 0 : (w - r) * tailTokens / (r * savedTokens);
|
|
13205
13620
|
}
|
|
13206
|
-
function planContextRewind(branch, sessionId, checkpointId, report) {
|
|
13621
|
+
function planContextRewind(branch, sessionId, checkpointId, report, readerApi) {
|
|
13207
13622
|
const state = inspectContext(branch, sessionId);
|
|
13208
13623
|
if (!state.checkpoint || state.checkpoint.id !== checkpointId) {
|
|
13209
13624
|
throw new Error(state.invalidReason ?? "No matching active checkpoint; create one before research.");
|
|
@@ -13231,6 +13646,9 @@ function planContextRewind(branch, sessionId, checkpointId, report) {
|
|
|
13231
13646
|
archives.push({ id: entry.id, sha256, chars: text.length });
|
|
13232
13647
|
}
|
|
13233
13648
|
const entries = targets.map((targetId) => ({ type: "context_edit", targetId, replacement: null }));
|
|
13649
|
+
const blockedReason = thinkingEditReason(branch, entries, readerApi);
|
|
13650
|
+
if (blockedReason)
|
|
13651
|
+
throw new Error(blockedReason);
|
|
13234
13652
|
const content = `Research handoff (agent-authored, not new user instructions):
|
|
13235
13653
|
${report}
|
|
13236
13654
|
|
|
@@ -14250,7 +14668,7 @@ No matching remote project memory found.`;
|
|
|
14250
14668
|
`<smart_recall_evidence source="hindsight" kind="${attr(fact.metadata.kind ?? fact.type ?? "fact")}">`,
|
|
14251
14669
|
"Provenance: " + provenance,
|
|
14252
14670
|
memoryId ? "Ref: hindsight:" + memoryId + "@" + hindsightTargetDigest(target.baseUrl, target.bankId, projectId, memoryId) : "",
|
|
14253
|
-
clean(fact.text.slice(0, RECALL_ITEM_CAP)),
|
|
14671
|
+
clean(fact.text.slice(0, RECALL_ITEM_CAP)) + (fact.text.length > RECALL_ITEM_CAP ? " (excerpt; full memory not shown)" : ""),
|
|
14254
14672
|
"</smart_recall_evidence>"
|
|
14255
14673
|
].filter(Boolean).join(`
|
|
14256
14674
|
`);
|
|
@@ -15737,7 +16155,7 @@ function applyGlobalSettingsRuntime(paths, ctx, policy, contextTools) {
|
|
|
15737
16155
|
|
|
15738
16156
|
// src/app/compaction-commit-store.ts
|
|
15739
16157
|
function createCompactionCommitStore(options = {}) {
|
|
15740
|
-
const ttlMs = Math.max(1, options.ttlMs ?? 5 * 60000);
|
|
16158
|
+
const ttlMs = () => Math.max(1, (typeof options.ttlMs === "function" ? options.ttlMs() : options.ttlMs) ?? 5 * 60000);
|
|
15741
16159
|
const maxEntries = Math.max(1, options.maxEntries ?? 16);
|
|
15742
16160
|
const entries = new Map;
|
|
15743
16161
|
const remove = (runId, reason, notify) => {
|
|
@@ -15752,7 +16170,7 @@ function createCompactionCommitStore(options = {}) {
|
|
|
15752
16170
|
const sweep = () => {
|
|
15753
16171
|
const now = Date.now();
|
|
15754
16172
|
for (const [runId, entry] of entries) {
|
|
15755
|
-
if (now - entry.createdAt > ttlMs)
|
|
16173
|
+
if (now - entry.createdAt > ttlMs())
|
|
15756
16174
|
remove(runId, "expired", true);
|
|
15757
16175
|
}
|
|
15758
16176
|
};
|
|
@@ -16016,89 +16434,6 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
16016
16434
|
};
|
|
16017
16435
|
}
|
|
16018
16436
|
|
|
16019
|
-
// src/app/settled-auto-trigger.ts
|
|
16020
|
-
function createSettledAutoTrigger(options = {}) {
|
|
16021
|
-
const now = options.now ?? Date.now;
|
|
16022
|
-
const cooldownMs = Math.max(0, options.cooldownMs ?? SETTLED_TRIGGER_COOLDOWN_MS);
|
|
16023
|
-
const active = new Map;
|
|
16024
|
-
const lastCompactionAt = new Map;
|
|
16025
|
-
const noteCompaction = (sessionId) => {
|
|
16026
|
-
if (!isUnresolvedSessionId(sessionId))
|
|
16027
|
-
lastCompactionAt.set(sessionId, now());
|
|
16028
|
-
};
|
|
16029
|
-
const clear = (sessionId) => {
|
|
16030
|
-
active.delete(sessionId);
|
|
16031
|
-
lastCompactionAt.delete(sessionId);
|
|
16032
|
-
};
|
|
16033
|
-
const request = async (ctx, config) => {
|
|
16034
|
-
if (!config.autoTrigger || !["settled", "background"].includes(config.autoTriggerStrategy))
|
|
16035
|
-
return;
|
|
16036
|
-
const sessionId = resolveSessionId(ctx);
|
|
16037
|
-
if (isUnresolvedSessionId(sessionId) || active.has(sessionId))
|
|
16038
|
-
return;
|
|
16039
|
-
const usage = ctx.getContextUsage();
|
|
16040
|
-
const totalTokens = usage?.tokens;
|
|
16041
|
-
if (typeof totalTokens !== "number" || !Number.isFinite(totalTokens) || totalTokens < MIN_TOKEN_THRESHOLD || !ctx.model)
|
|
16042
|
-
return;
|
|
16043
|
-
const contextPercent = safeContextPercent(totalTokens, effectiveContextWindow(ctx.model, config));
|
|
16044
|
-
if (contextPercent < config.minContextPercent)
|
|
16045
|
-
return;
|
|
16046
|
-
const lastCompaction = lastCompactionAt.get(sessionId);
|
|
16047
|
-
if (lastCompaction !== undefined && now() - lastCompaction < cooldownMs)
|
|
16048
|
-
return;
|
|
16049
|
-
if (!ctx.isIdle() || ctx.hasPendingMessages())
|
|
16050
|
-
return;
|
|
16051
|
-
const requestToken = Symbol(sessionId);
|
|
16052
|
-
active.set(sessionId, requestToken);
|
|
16053
|
-
await new Promise((resolve) => {
|
|
16054
|
-
let finished = false;
|
|
16055
|
-
const watchdogMs = options.watchdogMs ?? AUTO_TRIGGER_TIMEOUT_CAP_MS + config.autoTriggerTimeoutMs;
|
|
16056
|
-
const watchdog = setTimeout(() => {
|
|
16057
|
-
reportIssue({
|
|
16058
|
-
key: "auto.settled-no-callback",
|
|
16059
|
-
message: "Pi did not report the automatic compaction result within " + Math.round(watchdogMs / 1000) + "s. The next settled turn may request it again."
|
|
16060
|
-
}, ctx);
|
|
16061
|
-
finish();
|
|
16062
|
-
}, watchdogMs);
|
|
16063
|
-
watchdog.unref?.();
|
|
16064
|
-
const finish = () => {
|
|
16065
|
-
if (finished)
|
|
16066
|
-
return;
|
|
16067
|
-
finished = true;
|
|
16068
|
-
clearTimeout(watchdog);
|
|
16069
|
-
if (active.get(sessionId) === requestToken)
|
|
16070
|
-
active.delete(sessionId);
|
|
16071
|
-
resolve();
|
|
16072
|
-
};
|
|
16073
|
-
try {
|
|
16074
|
-
ctx.compact({
|
|
16075
|
-
onComplete: () => {
|
|
16076
|
-
if (active.get(sessionId) === requestToken)
|
|
16077
|
-
noteCompaction(sessionId);
|
|
16078
|
-
finish();
|
|
16079
|
-
},
|
|
16080
|
-
onError: (error) => {
|
|
16081
|
-
reportIssue({
|
|
16082
|
-
key: "auto.settled-apply",
|
|
16083
|
-
message: "Pi could not run the automatic compaction (" + errorDetail(error) + "). Conversation unchanged. Run /smart-compact manually if context is high.",
|
|
16084
|
-
error
|
|
16085
|
-
}, ctx);
|
|
16086
|
-
finish();
|
|
16087
|
-
}
|
|
16088
|
-
});
|
|
16089
|
-
} catch (error) {
|
|
16090
|
-
reportIssue({
|
|
16091
|
-
key: "auto.settled-apply",
|
|
16092
|
-
message: "Pi could not run the automatic compaction (" + errorDetail(error) + "). Conversation unchanged. Run /smart-compact manually if context is high.",
|
|
16093
|
-
error
|
|
16094
|
-
}, ctx);
|
|
16095
|
-
finish();
|
|
16096
|
-
}
|
|
16097
|
-
});
|
|
16098
|
-
};
|
|
16099
|
-
return { request, noteCompaction, clear };
|
|
16100
|
-
}
|
|
16101
|
-
|
|
16102
16437
|
// src/app/background-preparation.ts
|
|
16103
16438
|
function recordPreparationDiscard(sessionId, snapshot, reason) {
|
|
16104
16439
|
return appendMetricsSnapshot(sessionId, {
|
|
@@ -16120,11 +16455,31 @@ function preparationWindow(config, window) {
|
|
|
16120
16455
|
const startTokens = explicit ? Math.max(MIN_TOKEN_THRESHOLD, window * prepare / 100) : preparationStartTokens(applyTokens);
|
|
16121
16456
|
return { startTokens, applyTokens };
|
|
16122
16457
|
}
|
|
16458
|
+
function contextPressure(ctx, config) {
|
|
16459
|
+
const raw = ctx.getContextUsage?.()?.tokens;
|
|
16460
|
+
const tokens = typeof raw === "number" && Number.isFinite(raw) && raw >= 0 ? raw : null;
|
|
16461
|
+
const rawWindow = effectiveContextWindow(ctx.model, config);
|
|
16462
|
+
const policyWindow = typeof rawWindow === "number" && Number.isFinite(rawWindow) && rawWindow > 0 ? rawWindow : null;
|
|
16463
|
+
const gates = policyWindow === null ? null : preparationWindow(config, policyWindow);
|
|
16464
|
+
const cleanupTokens = gates ? Math.ceil(gates.startTokens) : null;
|
|
16465
|
+
const compactionTokens = gates ? Math.ceil(Math.max(MIN_TOKEN_THRESHOLD, gates.applyTokens)) : null;
|
|
16466
|
+
return {
|
|
16467
|
+
tokens,
|
|
16468
|
+
modelWindow: ctx.model?.contextWindow ?? null,
|
|
16469
|
+
policyWindow,
|
|
16470
|
+
percent: tokens !== null && policyWindow !== null ? tokens / policyWindow * 100 : null,
|
|
16471
|
+
cleanupTokens,
|
|
16472
|
+
compactionTokens,
|
|
16473
|
+
cleanup: tokens !== null && cleanupTokens !== null && tokens >= cleanupTokens,
|
|
16474
|
+
compaction: tokens !== null && compactionTokens !== null && tokens >= compactionTokens
|
|
16475
|
+
};
|
|
16476
|
+
}
|
|
16123
16477
|
function snapshotContext(ctx) {
|
|
16124
16478
|
const branch = structuredClone(ctx.sessionManager.getBranch());
|
|
16125
16479
|
const sessionId = resolveSessionId(ctx);
|
|
16126
16480
|
const sessionFile = ctx.sessionManager.getSessionFile?.();
|
|
16127
16481
|
const usage = structuredClone(ctx.getContextUsage());
|
|
16482
|
+
const systemPrompt = ctx.getSystemPrompt?.() ?? "";
|
|
16128
16483
|
const sessionManager = new Proxy(ctx.sessionManager, {
|
|
16129
16484
|
get(target, key) {
|
|
16130
16485
|
if (key === "getBranch")
|
|
@@ -16143,20 +16498,21 @@ function snapshotContext(ctx) {
|
|
|
16143
16498
|
model: ctx.model && { ...ctx.model },
|
|
16144
16499
|
hasUI: false,
|
|
16145
16500
|
getContextUsage: () => usage,
|
|
16501
|
+
getSystemPrompt: () => systemPrompt,
|
|
16146
16502
|
ui: { ...ctx.ui, notify() {}, setStatus() {}, setWidget() {} }
|
|
16147
16503
|
};
|
|
16148
16504
|
return Object.assign(snapshot, { silentBackground: true });
|
|
16149
16505
|
}
|
|
16150
16506
|
function signature(ctx, config) {
|
|
16151
16507
|
const model = ctx.model;
|
|
16152
|
-
return JSON.stringify([model?.provider, model?.id, model?.api, model?.contextWindow, model?.maxTokens, config]);
|
|
16508
|
+
return JSON.stringify([model?.provider, model?.id, model?.api, model?.baseUrl, model?.contextWindow, model?.maxTokens, ctx.getSystemPrompt?.(), config]);
|
|
16153
16509
|
}
|
|
16154
16510
|
function enabled(config) {
|
|
16155
16511
|
return config.autoTrigger && config.autoTriggerStrategy === "background";
|
|
16156
16512
|
}
|
|
16157
16513
|
function createBackgroundPreparation(options) {
|
|
16158
16514
|
const now = options.now ?? Date.now;
|
|
16159
|
-
const
|
|
16515
|
+
const signatureOf = (ctx, config) => signature(ctx, config) + (options.toolSignature?.() ?? "");
|
|
16160
16516
|
const cooldownMs = options.cooldownMs ?? SETTLED_TRIGGER_COOLDOWN_MS;
|
|
16161
16517
|
let current = null;
|
|
16162
16518
|
let lastAttempt;
|
|
@@ -16203,9 +16559,9 @@ function createBackgroundPreparation(options) {
|
|
|
16203
16559
|
return "config";
|
|
16204
16560
|
if (task.sessionId !== resolveSessionId(ctx))
|
|
16205
16561
|
return "session";
|
|
16206
|
-
if (task.signature !==
|
|
16562
|
+
if (task.signature !== signatureOf(ctx, config))
|
|
16207
16563
|
return "config";
|
|
16208
|
-
if (now() - (task.readyAt ?? task.createdAt) > ttlMs)
|
|
16564
|
+
if (now() - (task.readyAt ?? task.createdAt) > (options.ttlMs ?? config.pendingTtlMs))
|
|
16209
16565
|
return "ttl";
|
|
16210
16566
|
const branch = ctx.sessionManager.getBranch();
|
|
16211
16567
|
const origin = branch.findIndex((entry) => entry.id === task.originId);
|
|
@@ -16237,7 +16593,7 @@ function createBackgroundPreparation(options) {
|
|
|
16237
16593
|
const task = {
|
|
16238
16594
|
sessionId,
|
|
16239
16595
|
originId,
|
|
16240
|
-
signature:
|
|
16596
|
+
signature: signatureOf(ctx, config),
|
|
16241
16597
|
controller: new AbortController,
|
|
16242
16598
|
createdAt: now(),
|
|
16243
16599
|
ctx
|
|
@@ -16245,7 +16601,15 @@ function createBackgroundPreparation(options) {
|
|
|
16245
16601
|
current = task;
|
|
16246
16602
|
lastAttempt = { sessionId, at: now() };
|
|
16247
16603
|
const preparationConfig = { ...config, minContextPercent: startTokens / window * 100 };
|
|
16248
|
-
track(Promise.resolve().then(() =>
|
|
16604
|
+
track(Promise.resolve().then(() => {
|
|
16605
|
+
if (current !== task || task.controller.signal.aborted)
|
|
16606
|
+
return null;
|
|
16607
|
+
if (!valid(task, ctx, config)) {
|
|
16608
|
+
cancel("config");
|
|
16609
|
+
return null;
|
|
16610
|
+
}
|
|
16611
|
+
return options.prepare(snapshot, preparationConfig, task.controller.signal);
|
|
16612
|
+
}).then((pending) => {
|
|
16249
16613
|
if (pending) {
|
|
16250
16614
|
task.pending = pending;
|
|
16251
16615
|
task.readyAt = now();
|
|
@@ -16330,6 +16694,83 @@ function createBackgroundPreparation(options) {
|
|
|
16330
16694
|
};
|
|
16331
16695
|
}
|
|
16332
16696
|
|
|
16697
|
+
// src/app/settled-auto-trigger.ts
|
|
16698
|
+
function createSettledAutoTrigger(options = {}) {
|
|
16699
|
+
const now = options.now ?? Date.now;
|
|
16700
|
+
const cooldownMs = Math.max(0, options.cooldownMs ?? SETTLED_TRIGGER_COOLDOWN_MS);
|
|
16701
|
+
const active = new Map;
|
|
16702
|
+
const cooldownStartedAt = new Map;
|
|
16703
|
+
const noteCompaction = (sessionId) => {
|
|
16704
|
+
if (!isUnresolvedSessionId(sessionId))
|
|
16705
|
+
cooldownStartedAt.set(sessionId, now());
|
|
16706
|
+
};
|
|
16707
|
+
const clear = (sessionId) => {
|
|
16708
|
+
active.delete(sessionId);
|
|
16709
|
+
cooldownStartedAt.delete(sessionId);
|
|
16710
|
+
};
|
|
16711
|
+
const request = async (ctx, config) => {
|
|
16712
|
+
if (!config.autoTrigger || !["settled", "background"].includes(config.autoTriggerStrategy))
|
|
16713
|
+
return;
|
|
16714
|
+
const sessionId = resolveSessionId(ctx);
|
|
16715
|
+
if (isUnresolvedSessionId(sessionId) || active.has(sessionId))
|
|
16716
|
+
return;
|
|
16717
|
+
if (!contextPressure(ctx, config).compaction)
|
|
16718
|
+
return;
|
|
16719
|
+
const cooldownStart = cooldownStartedAt.get(sessionId);
|
|
16720
|
+
if (cooldownStart !== undefined && now() - cooldownStart < cooldownMs)
|
|
16721
|
+
return;
|
|
16722
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages())
|
|
16723
|
+
return;
|
|
16724
|
+
const requestToken = Symbol(sessionId);
|
|
16725
|
+
active.set(sessionId, requestToken);
|
|
16726
|
+
await new Promise((resolve) => {
|
|
16727
|
+
let finished = false;
|
|
16728
|
+
const watchdogMs = options.watchdogMs ?? AUTO_TRIGGER_TIMEOUT_CAP_MS + config.autoTriggerTimeoutMs;
|
|
16729
|
+
const watchdog = setTimeout(() => {
|
|
16730
|
+
const hostBusy = typeof ctx.isIdle === "function" ? !ctx.isIdle() : false;
|
|
16731
|
+
reportIssue({
|
|
16732
|
+
key: "auto.settled-no-callback",
|
|
16733
|
+
message: hostBusy ? "Pi is still busy after " + Math.round(watchdogMs / 1000) + "s; the automatic compaction result may still arrive and be applied late. No action needed; automatic retries wait for the cooldown." : "Pi did not report the automatic compaction result within " + Math.round(watchdogMs / 1000) + "s. Automatic retries wait for the cooldown. Run /smart-compact manually if needed."
|
|
16734
|
+
}, ctx);
|
|
16735
|
+
finish();
|
|
16736
|
+
}, watchdogMs);
|
|
16737
|
+
watchdog.unref?.();
|
|
16738
|
+
const finish = () => {
|
|
16739
|
+
if (finished)
|
|
16740
|
+
return;
|
|
16741
|
+
finished = true;
|
|
16742
|
+
clearTimeout(watchdog);
|
|
16743
|
+
if (active.get(sessionId) === requestToken) {
|
|
16744
|
+
active.delete(sessionId);
|
|
16745
|
+
cooldownStartedAt.set(sessionId, now());
|
|
16746
|
+
}
|
|
16747
|
+
resolve();
|
|
16748
|
+
};
|
|
16749
|
+
try {
|
|
16750
|
+
ctx.compact({
|
|
16751
|
+
onComplete: finish,
|
|
16752
|
+
onError: (error) => {
|
|
16753
|
+
reportIssue({
|
|
16754
|
+
key: "auto.settled-apply",
|
|
16755
|
+
message: "Pi could not run the automatic compaction (" + errorDetail(error) + "). Conversation unchanged. Run /smart-compact manually if context is high.",
|
|
16756
|
+
error
|
|
16757
|
+
}, ctx);
|
|
16758
|
+
finish();
|
|
16759
|
+
}
|
|
16760
|
+
});
|
|
16761
|
+
} catch (error) {
|
|
16762
|
+
reportIssue({
|
|
16763
|
+
key: "auto.settled-apply",
|
|
16764
|
+
message: "Pi could not run the automatic compaction (" + errorDetail(error) + "). Conversation unchanged. Run /smart-compact manually if context is high.",
|
|
16765
|
+
error
|
|
16766
|
+
}, ctx);
|
|
16767
|
+
finish();
|
|
16768
|
+
}
|
|
16769
|
+
});
|
|
16770
|
+
};
|
|
16771
|
+
return { request, noteCompaction, clear };
|
|
16772
|
+
}
|
|
16773
|
+
|
|
16333
16774
|
// src/app/host-cache-ledger.ts
|
|
16334
16775
|
var FOREIGN_REBUILD_WARN_COUNT = 3;
|
|
16335
16776
|
var num = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
@@ -16493,8 +16934,106 @@ function compactionUsage(snapshot, resolveModel) {
|
|
|
16493
16934
|
|
|
16494
16935
|
// src/app/register-context-tools.ts
|
|
16495
16936
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
16937
|
+
import { Text as Text4 } from "@earendil-works/pi-tui";
|
|
16496
16938
|
import { Type as Type3 } from "typebox";
|
|
16497
16939
|
|
|
16940
|
+
// src/ui/tool-rows.ts
|
|
16941
|
+
import { stripVTControlCharacters as stripVTControlCharacters2 } from "util";
|
|
16942
|
+
import { Text as Text3, truncateToWidth as truncateToWidth3 } from "@earendil-works/pi-tui";
|
|
16943
|
+
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
16944
|
+
var STATUS_COLOR = {
|
|
16945
|
+
done: "success",
|
|
16946
|
+
queued: "dim",
|
|
16947
|
+
pending: "warning",
|
|
16948
|
+
failed: "error",
|
|
16949
|
+
cancelled: "warning",
|
|
16950
|
+
skipped: "dim",
|
|
16951
|
+
info: "muted"
|
|
16952
|
+
};
|
|
16953
|
+
var STATUS_WORD = {
|
|
16954
|
+
done: "done",
|
|
16955
|
+
queued: "queued",
|
|
16956
|
+
pending: "pending",
|
|
16957
|
+
failed: "failed",
|
|
16958
|
+
cancelled: "cancelled",
|
|
16959
|
+
skipped: "skipped",
|
|
16960
|
+
info: ""
|
|
16961
|
+
};
|
|
16962
|
+
function sanitizeVisible(text) {
|
|
16963
|
+
return stripVTControlCharacters2(text).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, " ");
|
|
16964
|
+
}
|
|
16965
|
+
function firstTextContent(content) {
|
|
16966
|
+
const text = content?.find((part) => part.type === "text" && part.text)?.text;
|
|
16967
|
+
return text ? sanitizeVisible(text) : "";
|
|
16968
|
+
}
|
|
16969
|
+
function summarizeLine(text, maxLength = 120) {
|
|
16970
|
+
const single = sanitizeVisible(text).split(/\r?\n/).find((line) => line.trim()) ?? "";
|
|
16971
|
+
const clipped = single.trim().slice(0, maxLength);
|
|
16972
|
+
return clipped + (single.trim().length > maxLength ? "\u2026" : "");
|
|
16973
|
+
}
|
|
16974
|
+
function statusLabel(theme, status) {
|
|
16975
|
+
const word = STATUS_WORD[status];
|
|
16976
|
+
return word ? theme.fg(STATUS_COLOR[status], word) : "";
|
|
16977
|
+
}
|
|
16978
|
+
function metaLine(theme, key, value) {
|
|
16979
|
+
return theme.fg("dim", key + ":") + " " + sanitizeVisible(value);
|
|
16980
|
+
}
|
|
16981
|
+
function previewBlock(lines, expanded, maxCollapsed, maxLength = 160) {
|
|
16982
|
+
const clean = lines.map((line) => sanitizeVisible(line));
|
|
16983
|
+
if (expanded)
|
|
16984
|
+
return clean;
|
|
16985
|
+
const shown = clean.slice(0, maxCollapsed).map((line) => line.slice(0, maxLength));
|
|
16986
|
+
if (clean.length > maxCollapsed) {
|
|
16987
|
+
shown.push("\u2026 (" + (clean.length - maxCollapsed) + " more)");
|
|
16988
|
+
}
|
|
16989
|
+
return shown;
|
|
16990
|
+
}
|
|
16991
|
+
function allTextParts(content) {
|
|
16992
|
+
return (content ?? []).filter((part) => part.type === "text" && part.text).map((part) => sanitizeVisible(part.text));
|
|
16993
|
+
}
|
|
16994
|
+
function safeArg(value, maxLength = 120) {
|
|
16995
|
+
if (typeof value !== "string")
|
|
16996
|
+
return "";
|
|
16997
|
+
return sanitizeVisible(value).trim().slice(0, maxLength);
|
|
16998
|
+
}
|
|
16999
|
+
function expandHint(theme) {
|
|
17000
|
+
try {
|
|
17001
|
+
return theme.fg("dim", keyHint("app.tools.expand", "expand"));
|
|
17002
|
+
} catch {
|
|
17003
|
+
return theme.fg("dim", "expand tool output for full details");
|
|
17004
|
+
}
|
|
17005
|
+
}
|
|
17006
|
+
function expandedRow(theme, result, summaryLines) {
|
|
17007
|
+
const content = allTextParts(result.content).join(`
|
|
17008
|
+
`);
|
|
17009
|
+
const head = summaryLines.filter(Boolean).join(`
|
|
17010
|
+
`);
|
|
17011
|
+
return new Text3(head + (content ? `
|
|
17012
|
+
` + theme.fg("borderMuted", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500") + `
|
|
17013
|
+
` + content : ""), 0, 0);
|
|
17014
|
+
}
|
|
17015
|
+
function rawFallbackRow(theme, result, expanded) {
|
|
17016
|
+
const parts = allTextParts(result.content);
|
|
17017
|
+
if (!parts.length) {
|
|
17018
|
+
return new Text3(theme.fg("warning", "No result details available"), 0, 0);
|
|
17019
|
+
}
|
|
17020
|
+
if (expanded)
|
|
17021
|
+
return new Text3(parts.join(`
|
|
17022
|
+
`), 0, 0);
|
|
17023
|
+
const joined = parts.join(`
|
|
17024
|
+
`);
|
|
17025
|
+
const bounded = truncateToWidth3(joined.split(/\r?\n/)[0] ?? "", 200, "\u2026");
|
|
17026
|
+
return new Text3(bounded + (joined.length > bounded.length ? `
|
|
17027
|
+
\u2026 (expanded shows full content)` : ""), 0, 0);
|
|
17028
|
+
}
|
|
17029
|
+
function tryRow(theme, build, result, expanded) {
|
|
17030
|
+
try {
|
|
17031
|
+
return build();
|
|
17032
|
+
} catch {
|
|
17033
|
+
return rawFallbackRow(theme, result, expanded);
|
|
17034
|
+
}
|
|
17035
|
+
}
|
|
17036
|
+
|
|
16498
17037
|
// src/app/mnemopi-memory.ts
|
|
16499
17038
|
import { execFile as execFile2 } from "child_process";
|
|
16500
17039
|
import { existsSync as existsSync5 } from "fs";
|
|
@@ -16782,6 +17321,15 @@ function registerContextTools(pi) {
|
|
|
16782
17321
|
if (loadConfig().toolLoading === "off" || !pi.getActiveTools().includes("smart_recall"))
|
|
16783
17322
|
throw new Error("smart_recall is disabled in Pi settings or /tools.");
|
|
16784
17323
|
return executeRecall(params, signal, ctx);
|
|
17324
|
+
},
|
|
17325
|
+
renderCall(args, theme) {
|
|
17326
|
+
const label = theme.fg("toolTitle", "smart_recall ");
|
|
17327
|
+
const scope = args.scope ? theme.fg("dim", " [" + safeArg(args.scope, 20) + "]") : "";
|
|
17328
|
+
const query = safeArg(args.query, 100);
|
|
17329
|
+
return new Text4(label + theme.fg("muted", query) + scope, 0, 0);
|
|
17330
|
+
},
|
|
17331
|
+
renderResult(result, { expanded }, theme, context) {
|
|
17332
|
+
return tryRow(theme, () => renderRecallRow(result, expanded, theme, context), result, expanded);
|
|
16785
17333
|
}
|
|
16786
17334
|
});
|
|
16787
17335
|
pi.registerTool({
|
|
@@ -16816,9 +17364,197 @@ function registerContextTools(pi) {
|
|
|
16816
17364
|
if (loadConfig().toolLoading === "off" || !pi.getActiveTools().includes("smart_save_memory"))
|
|
16817
17365
|
throw new Error("smart_save_memory is disabled in Pi settings or /tools.");
|
|
16818
17366
|
return executeSaveMemory(params, signal, ctx);
|
|
17367
|
+
},
|
|
17368
|
+
renderCall(args, theme) {
|
|
17369
|
+
const label = theme.fg("toolTitle", "smart_save_memory ");
|
|
17370
|
+
if (args.ref && (args.status ?? "active") === "resolved") {
|
|
17371
|
+
return new Text4(label + theme.fg("muted", "resolve ") + theme.fg("dim", safeArg(args.ref, 80)), 0, 0);
|
|
17372
|
+
}
|
|
17373
|
+
const kind = theme.fg("accent", safeArg(args.kind ?? "memory", 40));
|
|
17374
|
+
const title = safeArg(args.title, 80);
|
|
17375
|
+
return new Text4(label + kind + (title ? " " + theme.fg("muted", title) : ""), 0, 0);
|
|
17376
|
+
},
|
|
17377
|
+
renderResult(result, { expanded }, theme, context) {
|
|
17378
|
+
return tryRow(theme, () => renderSaveRow(result, expanded, theme, context), result, expanded);
|
|
16819
17379
|
}
|
|
16820
17380
|
});
|
|
16821
17381
|
}
|
|
17382
|
+
function receiptBuckets(receipts) {
|
|
17383
|
+
const buckets = { done: 0, pending: 0, failed: 0, unknown: 0 };
|
|
17384
|
+
for (const receipt of receipts) {
|
|
17385
|
+
const state = receipt.state ?? "unknown";
|
|
17386
|
+
if (state === "completed" || state === "deleted")
|
|
17387
|
+
buckets.done++;
|
|
17388
|
+
else if (state === "failed")
|
|
17389
|
+
buckets.failed++;
|
|
17390
|
+
else if (state === "unknown")
|
|
17391
|
+
buckets.unknown++;
|
|
17392
|
+
else
|
|
17393
|
+
buckets.pending++;
|
|
17394
|
+
}
|
|
17395
|
+
return buckets;
|
|
17396
|
+
}
|
|
17397
|
+
function renderRecallRow(result, expanded, theme, context) {
|
|
17398
|
+
if (context.isError) {
|
|
17399
|
+
const head = theme.fg("error", "recall failed:");
|
|
17400
|
+
return expanded ? expandedRow(theme, result, [head]) : new Text4(head + " " + summarizeLine(firstTextContent(result.content), 140), 0, 0);
|
|
17401
|
+
}
|
|
17402
|
+
const details = result.details;
|
|
17403
|
+
if (!details)
|
|
17404
|
+
return rawFallbackRow(theme, result, expanded);
|
|
17405
|
+
const lines = [];
|
|
17406
|
+
if (Array.isArray(details.results)) {
|
|
17407
|
+
const results = details.results;
|
|
17408
|
+
lines.push(statusLabel(theme, results.length ? "done" : "skipped") + " " + theme.fg("muted", results.length + " local match(es)"));
|
|
17409
|
+
if (!expanded) {
|
|
17410
|
+
lines.push(...previewBlock(results.map((item) => "[" + (item.kind ?? "fact") + "] " + summarizeLine(item.title ?? "", 100) + (item.content ? " \u2014 " + summarizeLine(item.content, 80) : "")), false, 3));
|
|
17411
|
+
}
|
|
17412
|
+
return expanded ? expandedRow(theme, result, lines) : new Text4(lines.join(`
|
|
17413
|
+
`), 0, 0);
|
|
17414
|
+
}
|
|
17415
|
+
const mnemopi = details.mnemopi;
|
|
17416
|
+
if (mnemopi) {
|
|
17417
|
+
if (mnemopi.state === "recalled") {
|
|
17418
|
+
lines.push(statusLabel(theme, mnemopi.facts?.length ? "done" : "skipped") + " " + theme.fg("muted", (mnemopi.facts?.length ?? 0) + " mnemopi match(es)"));
|
|
17419
|
+
if (!expanded) {
|
|
17420
|
+
lines.push(...previewBlock((mnemopi.facts ?? []).map((fact) => "[" + (fact.kind ?? "fact") + "] " + summarizeLine(fact.title ?? "", 100) + (fact.content ? " \u2014 " + summarizeLine(fact.content, 80) : "")), false, 3));
|
|
17421
|
+
}
|
|
17422
|
+
} else {
|
|
17423
|
+
const status = mnemopi.state === "failed" ? "failed" : mnemopi.state === "skipped" ? "skipped" : "pending";
|
|
17424
|
+
lines.push(statusLabel(theme, status) + " " + theme.fg("muted", "mnemopi " + (mnemopi.state ?? "")));
|
|
17425
|
+
if (mnemopi.reason)
|
|
17426
|
+
lines.push(theme.fg("dim", summarizeLine(mnemopi.reason, 140)));
|
|
17427
|
+
}
|
|
17428
|
+
return expanded ? expandedRow(theme, result, lines) : new Text4(lines.join(`
|
|
17429
|
+
`), 0, 0);
|
|
17430
|
+
}
|
|
17431
|
+
const remote = details.remote;
|
|
17432
|
+
if (remote) {
|
|
17433
|
+
if (remote.state === "ok") {
|
|
17434
|
+
const buckets = receiptBuckets(remote.receipts ?? []);
|
|
17435
|
+
lines.push(statusLabel(theme, remote.facts?.length ? "done" : "skipped") + " " + theme.fg("muted", (remote.facts?.length ?? 0) + " remote fact(s)"));
|
|
17436
|
+
if (remote.receipts?.length) {
|
|
17437
|
+
lines.push(metaLine(theme, "prior saves", buckets.done + " completed, " + buckets.pending + " pending, " + buckets.failed + " failed, " + buckets.unknown + " unknown"));
|
|
17438
|
+
}
|
|
17439
|
+
if (!expanded) {
|
|
17440
|
+
lines.push(...previewBlock((remote.facts ?? []).map((fact) => (fact.type ? "[" + fact.type + "] " : "") + summarizeLine(fact.preview ?? "", 110)), false, 3));
|
|
17441
|
+
lines.push(expandHint(theme));
|
|
17442
|
+
}
|
|
17443
|
+
} else {
|
|
17444
|
+
const status = remote.state === "failed" ? "failed" : remote.state === "not-configured" || remote.state === "skipped" ? "skipped" : "pending";
|
|
17445
|
+
lines.push(statusLabel(theme, status) + " " + theme.fg("muted", "hindsight " + (remote.state ?? "")));
|
|
17446
|
+
if (remote.reason)
|
|
17447
|
+
lines.push(theme.fg("dim", summarizeLine(remote.reason, 140)));
|
|
17448
|
+
}
|
|
17449
|
+
return expanded ? expandedRow(theme, result, lines) : new Text4(lines.join(`
|
|
17450
|
+
`), 0, 0);
|
|
17451
|
+
}
|
|
17452
|
+
return rawFallbackRow(theme, result, expanded);
|
|
17453
|
+
}
|
|
17454
|
+
function renderSaveRow(result, expanded, theme, context) {
|
|
17455
|
+
if (context.isError) {
|
|
17456
|
+
const head = theme.fg("error", "save failed:");
|
|
17457
|
+
return expanded ? expandedRow(theme, result, [head]) : new Text4(head + " " + summarizeLine(firstTextContent(result.content), 140), 0, 0);
|
|
17458
|
+
}
|
|
17459
|
+
const details = result.details;
|
|
17460
|
+
if (!details)
|
|
17461
|
+
return rawFallbackRow(theme, result, expanded);
|
|
17462
|
+
const lines = [];
|
|
17463
|
+
const finish = () => expanded ? expandedRow(theme, result, lines) : new Text4(lines.join(`
|
|
17464
|
+
`), 0, 0);
|
|
17465
|
+
if (details.approved === false) {
|
|
17466
|
+
lines.push(statusLabel(theme, "cancelled") + " " + theme.fg("muted", "not saved \u2014 user did not approve"));
|
|
17467
|
+
return finish();
|
|
17468
|
+
}
|
|
17469
|
+
if (typeof details.closed === "number") {
|
|
17470
|
+
if (details.closed > 0) {
|
|
17471
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "resolved"));
|
|
17472
|
+
if (expanded && typeof details.ref === "string") {
|
|
17473
|
+
lines.push(metaLine(theme, "ref", details.ref));
|
|
17474
|
+
}
|
|
17475
|
+
} else {
|
|
17476
|
+
lines.push(statusLabel(theme, "skipped") + " " + theme.fg("muted", "no active match \u2014 nothing changed"));
|
|
17477
|
+
if (expanded && typeof details.ref === "string") {
|
|
17478
|
+
lines.push(metaLine(theme, "ref", details.ref));
|
|
17479
|
+
}
|
|
17480
|
+
}
|
|
17481
|
+
return finish();
|
|
17482
|
+
}
|
|
17483
|
+
const local = details.memory;
|
|
17484
|
+
if (local && typeof details.ref === "string") {
|
|
17485
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "saved locally"));
|
|
17486
|
+
const fact = "[" + (local.kind ?? "memory") + "] " + (local.title ?? "");
|
|
17487
|
+
if (fact.trim() !== "[]")
|
|
17488
|
+
lines.push(metaLine(theme, "fact", fact));
|
|
17489
|
+
if (expanded)
|
|
17490
|
+
lines.push(metaLine(theme, "ref", details.ref));
|
|
17491
|
+
else
|
|
17492
|
+
lines.push(expandHint(theme));
|
|
17493
|
+
return finish();
|
|
17494
|
+
}
|
|
17495
|
+
const mnemopi = details.mnemopi;
|
|
17496
|
+
if (mnemopi) {
|
|
17497
|
+
if (mnemopi.state === "saved") {
|
|
17498
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "mnemopi saved"));
|
|
17499
|
+
if (expanded && typeof details.ref === "string")
|
|
17500
|
+
lines.push(metaLine(theme, "ref", details.ref));
|
|
17501
|
+
} else if (mnemopi.state === "resolved") {
|
|
17502
|
+
if (mnemopi.closed) {
|
|
17503
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "mnemopi resolved"));
|
|
17504
|
+
} else {
|
|
17505
|
+
lines.push(statusLabel(theme, "skipped") + " " + theme.fg("muted", "no matching active fact \u2014 nothing changed"));
|
|
17506
|
+
}
|
|
17507
|
+
if (expanded && typeof details.ref === "string")
|
|
17508
|
+
lines.push(metaLine(theme, "ref", details.ref));
|
|
17509
|
+
} else {
|
|
17510
|
+
const status = mnemopi.state === "failed" ? "failed" : "pending";
|
|
17511
|
+
lines.push(statusLabel(theme, status) + " " + theme.fg("muted", "mnemopi " + (mnemopi.state ?? "done")));
|
|
17512
|
+
if (mnemopi.reason)
|
|
17513
|
+
lines.push(theme.fg("dim", summarizeLine(mnemopi.reason, 140)));
|
|
17514
|
+
}
|
|
17515
|
+
return finish();
|
|
17516
|
+
}
|
|
17517
|
+
const remote = details.remote;
|
|
17518
|
+
if (remote) {
|
|
17519
|
+
const state = remote.state ?? "unknown";
|
|
17520
|
+
if (state === "completed") {
|
|
17521
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "saved & searchable"));
|
|
17522
|
+
} else if (state === "accepted") {
|
|
17523
|
+
lines.push(statusLabel(theme, "pending") + " " + theme.fg("muted", "accepted \u2014 not yet searchable"));
|
|
17524
|
+
} else if (state === "failed") {
|
|
17525
|
+
lines.push(statusLabel(theme, "failed") + " " + theme.fg("muted", "not saved"));
|
|
17526
|
+
} else if (state === "not-configured") {
|
|
17527
|
+
lines.push(statusLabel(theme, "skipped") + " " + theme.fg("muted", "hindsight not configured"));
|
|
17528
|
+
} else if (state === "deleted") {
|
|
17529
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "deleted remotely"));
|
|
17530
|
+
} else if (state === "not-found") {
|
|
17531
|
+
lines.push(statusLabel(theme, "skipped") + " " + theme.fg("muted", "already absent \u2014 nothing deleted"));
|
|
17532
|
+
} else if (state === "pending") {
|
|
17533
|
+
lines.push(statusLabel(theme, "pending") + " " + theme.fg("muted", "delete pending remotely"));
|
|
17534
|
+
} else {
|
|
17535
|
+
lines.push(statusLabel(theme, "pending") + " " + theme.fg("muted", "outcome unknown"));
|
|
17536
|
+
}
|
|
17537
|
+
if (remote.title)
|
|
17538
|
+
lines.push(metaLine(theme, "fact", remote.title));
|
|
17539
|
+
if (typeof details.ref === "string" && expanded) {
|
|
17540
|
+
lines.push(metaLine(theme, "ref", details.ref));
|
|
17541
|
+
} else if (typeof details.ref === "string" && (state === "completed" || state === "accepted")) {
|
|
17542
|
+
lines.push(expandHint(theme));
|
|
17543
|
+
}
|
|
17544
|
+
if (expanded || state === "unknown" || state === "failed") {
|
|
17545
|
+
if (remote.operationId)
|
|
17546
|
+
lines.push(metaLine(theme, "operation", remote.operationId));
|
|
17547
|
+
if (remote.documentId)
|
|
17548
|
+
lines.push(metaLine(theme, "document", remote.documentId));
|
|
17549
|
+
if (remote.target)
|
|
17550
|
+
lines.push(metaLine(theme, "server", remote.target));
|
|
17551
|
+
}
|
|
17552
|
+
if (remote.reason)
|
|
17553
|
+
lines.push(theme.fg("dim", summarizeLine(remote.reason, 140)));
|
|
17554
|
+
return finish();
|
|
17555
|
+
}
|
|
17556
|
+
return rawFallbackRow(theme, result, expanded);
|
|
17557
|
+
}
|
|
16822
17558
|
function textResult(text, details = undefined) {
|
|
16823
17559
|
return { content: [{ type: "text", text }], details };
|
|
16824
17560
|
}
|
|
@@ -16892,7 +17628,10 @@ Not yet searchable remotely: ` + pending.map((receipt) => receipt.documentId + "
|
|
|
16892
17628
|
}
|
|
16893
17629
|
const kinds = params.kinds?.length ? new Set(params.kinds) : null;
|
|
16894
17630
|
const facts = outcome.facts.filter((fact) => !kinds || !fact.metadata.kind || kinds.has(fact.metadata.kind)).slice(0, params.limit ?? 5);
|
|
16895
|
-
|
|
17631
|
+
const buckets = receiptBuckets(refreshed);
|
|
17632
|
+
const receiptSummary = refreshed.length ? "Prior saves: " + buckets.done + " completed, " + buckets.pending + " pending, " + buckets.failed + " failed, " + buckets.unknown + " unknown." : "";
|
|
17633
|
+
return textResult(formatHindsightFacts(hindsight.target, projectId, facts) + pendingNote + (receiptSummary ? `
|
|
17634
|
+
` + receiptSummary : ""), {
|
|
16896
17635
|
remote: {
|
|
16897
17636
|
state: "ok",
|
|
16898
17637
|
target: describeHindsightTarget(hindsight.target),
|
|
@@ -16900,7 +17639,8 @@ Not yet searchable remotely: ` + pending.map((receipt) => receipt.documentId + "
|
|
|
16900
17639
|
id: fact.id,
|
|
16901
17640
|
documentId: fact.documentId,
|
|
16902
17641
|
type: fact.type,
|
|
16903
|
-
mentionedAt: fact.mentionedAt
|
|
17642
|
+
mentionedAt: fact.mentionedAt,
|
|
17643
|
+
preview: fact.text.replace(/\s+/g, " ").slice(0, 160)
|
|
16904
17644
|
})),
|
|
16905
17645
|
redactions: scrubber.count(),
|
|
16906
17646
|
receipts: refreshed.map((receipt) => ({
|
|
@@ -17019,13 +17759,20 @@ Destination:
|
|
|
17019
17759
|
ref: "hindsight:" + memoryId + "@" + digest,
|
|
17020
17760
|
remote: "receipt" in outcome && outcome.receipt ? {
|
|
17021
17761
|
state: outcome.state,
|
|
17762
|
+
title,
|
|
17022
17763
|
documentId: outcome.receipt.documentId,
|
|
17023
17764
|
operationId: outcome.receipt.operationId,
|
|
17024
17765
|
target: describeHindsightTarget(hindsight.target),
|
|
17025
17766
|
..."reason" in outcome ? { reason: outcome.reason } : {}
|
|
17026
17767
|
} : outcome
|
|
17027
17768
|
};
|
|
17028
|
-
|
|
17769
|
+
let text = describeRemoteSave(outcome);
|
|
17770
|
+
if ("receipt" in outcome && outcome.receipt && (outcome.state === "completed" || outcome.state === "accepted")) {
|
|
17771
|
+
text += `
|
|
17772
|
+
Title: ` + title + `
|
|
17773
|
+
Ref: hindsight:` + memoryId + "@" + digest + (outcome.state === "accepted" ? " (accepted, NOT yet searchable; resolves only after the server completes indexing)" : "");
|
|
17774
|
+
}
|
|
17775
|
+
return textResult(text, details);
|
|
17029
17776
|
}
|
|
17030
17777
|
async function resolveMemoryByRef(params, signal, ctx, config, projectId) {
|
|
17031
17778
|
const ref = params.ref ? parseMemoryRef(params.ref) : null;
|
|
@@ -17144,6 +17891,9 @@ function resolveModels(ctx, primary, config, explicit = false) {
|
|
|
17144
17891
|
return { segModel, sumModel, verifyModel };
|
|
17145
17892
|
}
|
|
17146
17893
|
|
|
17894
|
+
// src/app/register-smart-compact-tool.ts
|
|
17895
|
+
import { Text as Text5 } from "@earendil-works/pi-tui";
|
|
17896
|
+
|
|
17147
17897
|
// src/ui/dashboard-format.ts
|
|
17148
17898
|
function dashboardPageSize(rows = process.stdout.rows) {
|
|
17149
17899
|
if (!rows)
|
|
@@ -17489,16 +18239,16 @@ function statusClass(status) {
|
|
|
17489
18239
|
return "warn";
|
|
17490
18240
|
return "good";
|
|
17491
18241
|
}
|
|
17492
|
-
function
|
|
18242
|
+
function statusLabel2(status) {
|
|
17493
18243
|
return status ?? "success";
|
|
17494
18244
|
}
|
|
17495
18245
|
function badge(status) {
|
|
17496
|
-
const label =
|
|
18246
|
+
const label = statusLabel2(status);
|
|
17497
18247
|
return `<span class="badge ${statusClass(label)}">${escapeHtml(label)}</span>`;
|
|
17498
18248
|
}
|
|
17499
18249
|
function summarizeDashboard(entries) {
|
|
17500
18250
|
const durations = entries.map(metricDuration).filter(Boolean);
|
|
17501
|
-
const success = entries.filter((e) =>
|
|
18251
|
+
const success = entries.filter((e) => statusLabel2(e.status) === "success").length;
|
|
17502
18252
|
const timeout = entries.filter((e) => e.status === "timeout").length;
|
|
17503
18253
|
const error = entries.filter((e) => e.status === "error" && e.failureKind !== "cancelled").length;
|
|
17504
18254
|
const dryRun = entries.filter((e) => e.status === "dry-run").length;
|
|
@@ -18009,7 +18759,7 @@ function registerSmartCompactTool(pi, dependencies) {
|
|
|
18009
18759
|
pi.registerTool({
|
|
18010
18760
|
name: "smart_compact",
|
|
18011
18761
|
label: "Smart Compact",
|
|
18012
|
-
description: "Prepares and stages a verified summary for the next /compact (
|
|
18762
|
+
description: "Prepares and stages a verified summary for the next /compact (configured staging TTL; never applies mid-turn). Call only when actual context usage is high; tool=XX% is tool-output share, not fullness.",
|
|
18013
18763
|
parameters: {
|
|
18014
18764
|
type: "object",
|
|
18015
18765
|
properties: {
|
|
@@ -18055,6 +18805,19 @@ function registerSmartCompactTool(pi, dependencies) {
|
|
|
18055
18805
|
}
|
|
18056
18806
|
}
|
|
18057
18807
|
},
|
|
18808
|
+
renderCall(args, theme) {
|
|
18809
|
+
const label = theme.fg("toolTitle", "smart_compact ");
|
|
18810
|
+
const flags = [
|
|
18811
|
+
safeArg(args.mode, 20),
|
|
18812
|
+
args.dry_run === true ? "dry-run" : "",
|
|
18813
|
+
args.report === true ? "report" : "",
|
|
18814
|
+
args.dashboard === true ? "dashboard" : ""
|
|
18815
|
+
].filter(Boolean).join(" ");
|
|
18816
|
+
return new Text5(label + theme.fg("muted", flags || "auto"), 0, 0);
|
|
18817
|
+
},
|
|
18818
|
+
renderResult(result, { expanded }, theme, context) {
|
|
18819
|
+
return tryRow(theme, () => renderCompactRow(result, expanded, theme, context), result, expanded);
|
|
18820
|
+
},
|
|
18058
18821
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
18059
18822
|
if (!policy.isAgentToolEnabled()) {
|
|
18060
18823
|
return textResult2("smart_compact is hidden from the agent for this session. The user can still run /smart-compact manually.");
|
|
@@ -18078,13 +18841,13 @@ function registerSmartCompactTool(pi, dependencies) {
|
|
|
18078
18841
|
const dashboard = action === "dashboard" ? writeMetricsDashboard() : null;
|
|
18079
18842
|
return textResult2(report + (dashboard ? `
|
|
18080
18843
|
|
|
18081
|
-
Dashboard: ` + dashboard : ""));
|
|
18844
|
+
Dashboard: ` + dashboard : ""), { display: { kind: "metrics", dashboard: dashboard !== null } });
|
|
18082
18845
|
}
|
|
18083
18846
|
const config = loadConfig();
|
|
18084
18847
|
const resolvedMode = mode ?? config.mode;
|
|
18085
18848
|
const sessionId = resolveSessionId(ctx);
|
|
18086
18849
|
if (!dryRun && pendingRef.peek(sessionId)) {
|
|
18087
|
-
return textResult2("A smart summary is already staged; context is unchanged. Run /compact before the
|
|
18850
|
+
return textResult2("A smart summary is already staged; context is unchanged. Run /compact before the configured staging TTL expires to apply it. No LLM calls were made.");
|
|
18088
18851
|
}
|
|
18089
18852
|
const usage = ctx.getContextUsage?.();
|
|
18090
18853
|
const totalTokens = usage?.tokens ?? 0;
|
|
@@ -18128,10 +18891,19 @@ Dashboard: ` + dashboard : ""));
|
|
|
18128
18891
|
content: [
|
|
18129
18892
|
{
|
|
18130
18893
|
type: "text",
|
|
18131
|
-
text: (staged.details.method === "native" ? "Native compaction prepared (" + staged.details.model + "; provider state, not EESV-verified). Tokens: " : "Smart summary prepared (" + resolvedMode + " \u2192 " + (staged.details.mode ?? staged.details.profile) + "). Tokens: ") + (staged.tokensBefore ?? 0).toLocaleString() + " \u2014 staged, not applied, for " +
|
|
18894
|
+
text: (staged.details.method === "native" ? "Native compaction prepared (" + staged.details.model + "; provider state, not EESV-verified). Tokens: " : "Smart summary prepared (" + resolvedMode + " \u2192 " + (staged.details.mode ?? staged.details.profile) + "). Tokens: ") + (staged.tokensBefore ?? 0).toLocaleString() + " \u2014 staged, not applied, for " + config.pendingTtlMs / 60000 + " min. Context is unchanged. Run /compact within that time to apply it; expiry discards the candidate."
|
|
18132
18895
|
}
|
|
18133
18896
|
],
|
|
18134
|
-
details:
|
|
18897
|
+
details: {
|
|
18898
|
+
...staged.details,
|
|
18899
|
+
display: {
|
|
18900
|
+
state: "staged",
|
|
18901
|
+
method: staged.details.method,
|
|
18902
|
+
mode: staged.details.mode ?? staged.details.profile ?? resolvedMode,
|
|
18903
|
+
tokens: staged.tokensBefore ?? 0,
|
|
18904
|
+
ttlMinutes: config.pendingTtlMs / 60000
|
|
18905
|
+
}
|
|
18906
|
+
}
|
|
18135
18907
|
};
|
|
18136
18908
|
}
|
|
18137
18909
|
if (outcome.kind === "dry-run") {
|
|
@@ -18143,13 +18915,16 @@ Dashboard: ` + dashboard : ""));
|
|
|
18143
18915
|
text: "Dry run finished (" + resolvedMode + ", " + seconds + "s). Pipeline ran successfully; no summary was staged."
|
|
18144
18916
|
}
|
|
18145
18917
|
],
|
|
18146
|
-
details:
|
|
18918
|
+
details: {
|
|
18919
|
+
...outcome.details,
|
|
18920
|
+
display: { state: "dry-run", mode: resolvedMode, seconds: Number(seconds) }
|
|
18921
|
+
}
|
|
18147
18922
|
};
|
|
18148
18923
|
}
|
|
18149
18924
|
if (outcome.kind === "cancelled") {
|
|
18150
|
-
return textResult2("Smart compact cancelled by " + outcome.source + "; no summary was staged.");
|
|
18925
|
+
return textResult2("Smart compact cancelled by " + outcome.source + "; no summary was staged.", { display: { state: "cancelled", source: outcome.source } });
|
|
18151
18926
|
}
|
|
18152
|
-
return textResult2("Smart compact skipped: " + outcome.reason.replace(/-/g, " ") + ". No summary was staged.");
|
|
18927
|
+
return textResult2("Smart compact skipped: " + outcome.reason.replace(/-/g, " ") + ". No summary was staged.", { display: { state: "skipped" } });
|
|
18153
18928
|
} catch (error) {
|
|
18154
18929
|
recordIssue({ key: "tool.smart-compact", message: "smart_compact failed: " + errorDetail(error) + ".", error });
|
|
18155
18930
|
throw new Error(formatCompactErrorForUi(error));
|
|
@@ -18157,13 +18932,42 @@ Dashboard: ` + dashboard : ""));
|
|
|
18157
18932
|
}
|
|
18158
18933
|
});
|
|
18159
18934
|
}
|
|
18160
|
-
function textResult2(text) {
|
|
18161
|
-
return { content: [{ type: "text", text }], details
|
|
18935
|
+
function textResult2(text, details = undefined) {
|
|
18936
|
+
return { content: [{ type: "text", text }], details };
|
|
18937
|
+
}
|
|
18938
|
+
function renderCompactRow(result, expanded, theme, context) {
|
|
18939
|
+
if (context.isError) {
|
|
18940
|
+
const head = theme.fg("error", "smart_compact failed:");
|
|
18941
|
+
return expanded ? expandedRow(theme, result, [head]) : new Text5(head + " " + summarizeLine(firstTextContent(result.content), 140), 0, 0);
|
|
18942
|
+
}
|
|
18943
|
+
const display = result.details?.display;
|
|
18944
|
+
if (!display)
|
|
18945
|
+
return rawFallbackRow(theme, result, expanded);
|
|
18946
|
+
const lines = [];
|
|
18947
|
+
if (display.state === "staged") {
|
|
18948
|
+
lines.push(statusLabel(theme, "pending") + " " + theme.fg("muted", "staged \u2014 NOT applied; run /compact within " + (Number(display.ttlMinutes) || 0) + " min"));
|
|
18949
|
+
lines.push(metaLine(theme, "summary", (display.method === "native" ? "native" : String(display.mode ?? "")) + " \xB7 " + (Number(display.tokens) || 0).toLocaleString() + " tokens"));
|
|
18950
|
+
} else if (display.state === "dry-run") {
|
|
18951
|
+
lines.push(statusLabel(theme, "info") + " " + theme.fg("muted", "dry run \u2014 nothing staged"));
|
|
18952
|
+
if (display.seconds)
|
|
18953
|
+
lines.push(metaLine(theme, "pipeline", display.seconds + "s"));
|
|
18954
|
+
} else if (display.state === "cancelled") {
|
|
18955
|
+
lines.push(statusLabel(theme, "cancelled") + " " + theme.fg("muted", "by " + String(display.source ?? "host")));
|
|
18956
|
+
} else if (display.state === "skipped") {
|
|
18957
|
+
lines.push(statusLabel(theme, "skipped") + " " + theme.fg("dim", summarizeLine(firstTextContent(result.content), 140)));
|
|
18958
|
+
} else if (display.kind === "metrics") {
|
|
18959
|
+
lines.push(statusLabel(theme, "info") + " " + theme.fg("muted", "metrics report" + (display.dashboard ? " (dashboard written)" : "")));
|
|
18960
|
+
} else {
|
|
18961
|
+
return rawFallbackRow(theme, result, expanded);
|
|
18962
|
+
}
|
|
18963
|
+
return expanded ? expandedRow(theme, result, lines) : new Text5(lines.join(`
|
|
18964
|
+
`), 0, 0);
|
|
18162
18965
|
}
|
|
18163
18966
|
|
|
18164
18967
|
// src/app/register-smart-context-tool.ts
|
|
18165
18968
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
18166
18969
|
import { StringEnum as StringEnum2 } from "@earendil-works/pi-ai";
|
|
18970
|
+
import { Text as Text6 } from "@earendil-works/pi-tui";
|
|
18167
18971
|
import { Type as Type4 } from "typebox";
|
|
18168
18972
|
|
|
18169
18973
|
// src/app/context-evidence.ts
|
|
@@ -18411,11 +19215,11 @@ function registerSmartContextTool(pi, options = {}) {
|
|
|
18411
19215
|
const current = config();
|
|
18412
19216
|
return new SecretScrubber(current.scrubSecrets, current.scrubPii).scrubText(text).value;
|
|
18413
19217
|
};
|
|
18414
|
-
const reply = (text) => ({ content: [{ type: "text", text }], details
|
|
19218
|
+
const reply = (text, details = undefined) => ({ content: [{ type: "text", text }], details });
|
|
18415
19219
|
pi.registerTool({
|
|
18416
19220
|
name: TOOL_NAME,
|
|
18417
19221
|
label: "Session Context",
|
|
18418
|
-
description: "
|
|
19222
|
+
description: "Check status for context pressure/gates. Before a large read-only detour, checkpoint; when finished, rewind(report) keeps findings and drops safe research under pressure. plan/trim old output; search/read archived evidence (scope=lineage includes parents). Changes apply after the batch, never roll back files/processes.",
|
|
18419
19223
|
parameters: Type4.Object({
|
|
18420
19224
|
action: StringEnum2(["status", "plan", "checkpoint", "rewind", "trim", "read", "search"]),
|
|
18421
19225
|
label: Type4.Optional(Type4.String({ maxLength: 120, description: "Checkpoint label." })),
|
|
@@ -18439,15 +19243,28 @@ function registerSmartContextTool(pi, options = {}) {
|
|
|
18439
19243
|
const branch = ctx.sessionManager.getBranch();
|
|
18440
19244
|
const state = inspectContext(branch, sessionId);
|
|
18441
19245
|
if (params.action === "plan") {
|
|
18442
|
-
const plan = planContextTrim(branch, state.checkpoint?.originId);
|
|
18443
|
-
|
|
19246
|
+
const plan = planContextTrim(branch, state.checkpoint?.originId, { readerApi: ctx.model?.api });
|
|
19247
|
+
const payload = {
|
|
19248
|
+
pressure: contextPressure(ctx, config()),
|
|
19249
|
+
blockedReason: plan.blockedReason,
|
|
18444
19250
|
outputs: plan.references.length,
|
|
18445
19251
|
superseded: plan.superseded,
|
|
18446
19252
|
savedChars: plan.savedChars,
|
|
18447
19253
|
batch: plan.automatic,
|
|
18448
19254
|
cooldownTurns: plan.cooldownTurns,
|
|
18449
|
-
note: "No changes applied. Automatic
|
|
18450
|
-
}
|
|
19255
|
+
note: "No changes applied. Automatic cleanup requires enablement, a safe batch and an uncontested boundary. Pressure-only is the default; cache economics is opt-in. Estimates are not billed-token savings."
|
|
19256
|
+
};
|
|
19257
|
+
return reply(JSON.stringify(payload), {
|
|
19258
|
+
display: {
|
|
19259
|
+
kind: "plan",
|
|
19260
|
+
pressurePercent: payload.pressure?.percent == null ? null : Math.round(payload.pressure.percent),
|
|
19261
|
+
outputs: payload.outputs,
|
|
19262
|
+
superseded: payload.superseded,
|
|
19263
|
+
savedChars: payload.savedChars,
|
|
19264
|
+
batchReady: payload.batch === "ready",
|
|
19265
|
+
batchReason: payload.blockedReason ?? payload.batch ?? null
|
|
19266
|
+
}
|
|
19267
|
+
});
|
|
18451
19268
|
}
|
|
18452
19269
|
const offset = params.offset ?? 0;
|
|
18453
19270
|
if (params.action === "status" || params.action === "read" || params.action === "search") {
|
|
@@ -18464,7 +19281,20 @@ function registerSmartContextTool(pi, options = {}) {
|
|
|
18464
19281
|
const evidence = contextEvidence(branch, sessionId, new SecretScrubber(settings.scrubSecrets, settings.scrubPii), lineage);
|
|
18465
19282
|
if (params.action === "status") {
|
|
18466
19283
|
const sources = evidence.list.slice(offset, offset + limit);
|
|
18467
|
-
|
|
19284
|
+
const plan = planContextTrim(branch, state.checkpoint?.originId, { readerApi: ctx.model?.api });
|
|
19285
|
+
const pressure = contextPressure(ctx, settings);
|
|
19286
|
+
const cleanup = {
|
|
19287
|
+
enabled: settings.contextHygieneEnabled || settings.autoTrigger && settings.autoTriggerStrategy === "background",
|
|
19288
|
+
pressureOnly: settings.contextPressureOnly,
|
|
19289
|
+
maintenanceAvailable: options.canAutoTrim?.(ctx) !== false,
|
|
19290
|
+
batch: plan.automatic,
|
|
19291
|
+
savedChars: plan.savedChars,
|
|
19292
|
+
blockedReason: plan.blockedReason,
|
|
19293
|
+
agentAllowed: options.canAgentMutate?.(ctx) !== false
|
|
19294
|
+
};
|
|
19295
|
+
const payload = {
|
|
19296
|
+
pressure,
|
|
19297
|
+
cleanup,
|
|
18468
19298
|
checkpoint: state.checkpoint ? { id: state.checkpoint.id, label: state.checkpoint.label } : null,
|
|
18469
19299
|
rewind: Boolean(state.checkpoint),
|
|
18470
19300
|
reason: state.invalidReason,
|
|
@@ -18480,7 +19310,21 @@ function registerSmartContextTool(pi, options = {}) {
|
|
|
18480
19310
|
sources: evidence.list.filter((source) => source.session === parent.sessionId).length
|
|
18481
19311
|
})) } : {},
|
|
18482
19312
|
nextOffset: offset + limit < evidence.list.length ? offset + limit : null
|
|
18483
|
-
}
|
|
19313
|
+
};
|
|
19314
|
+
return reply(scrub(JSON.stringify(payload)), {
|
|
19315
|
+
display: {
|
|
19316
|
+
kind: "status",
|
|
19317
|
+
pressurePercent: pressure.percent == null ? null : Math.round(pressure.percent),
|
|
19318
|
+
cleanupEnabled: cleanup.enabled,
|
|
19319
|
+
cleanupBatchReady: plan.automatic === "ready",
|
|
19320
|
+
cleanupBatchReason: plan.automatic === "ready" ? null : plan.automatic,
|
|
19321
|
+
cleanupBlockedReason: cleanup.blockedReason ?? null,
|
|
19322
|
+
checkpoint: payload.checkpoint?.label ?? null,
|
|
19323
|
+
pending: payload.pending ?? null,
|
|
19324
|
+
archivedOutputs: payload.archivedOutputs,
|
|
19325
|
+
nextOffset: payload.nextOffset
|
|
19326
|
+
}
|
|
19327
|
+
});
|
|
18484
19328
|
}
|
|
18485
19329
|
if (params.action === "search") {
|
|
18486
19330
|
if (!params.query)
|
|
@@ -18501,6 +19345,12 @@ ${page.text}`);
|
|
|
18501
19345
|
if (options.canAgentMutate?.(ctx) === false) {
|
|
18502
19346
|
throw new Error("Agent-requested context changes are disabled by policy; status, plan, search and read stay available.");
|
|
18503
19347
|
}
|
|
19348
|
+
if (params.action !== "checkpoint" && config().contextPressureOnly && !contextPressure(ctx, config()).cleanup) {
|
|
19349
|
+
throw new Error("No context pressure (or usage is unavailable); history left unchanged. Check status. Human commands can request early cleanup.");
|
|
19350
|
+
}
|
|
19351
|
+
if (params.action !== "checkpoint" && options.canAutoTrim?.(ctx) === false) {
|
|
19352
|
+
throw new Error("Prepared/running compaction or unavailable recovery tools take priority; history left unchanged.");
|
|
19353
|
+
}
|
|
18504
19354
|
if (options.isPaused?.(ctx))
|
|
18505
19355
|
throw new Error("Context changes are paused while a navigation pivot is pending.");
|
|
18506
19356
|
if (queued)
|
|
@@ -18527,7 +19377,21 @@ ${page.text}`);
|
|
|
18527
19377
|
text: params.action === "rewind" ? text : text.slice(0, 120),
|
|
18528
19378
|
signal
|
|
18529
19379
|
};
|
|
18530
|
-
return reply(`${params.action} queued for the completed tool batch. ${params.action === "rewind" ? "Stop research here; files and processes stay unchanged." : "Use status to inspect the committed state."}
|
|
19380
|
+
return reply(`${params.action} queued for the completed tool batch. ${params.action === "rewind" ? "Stop research here; files and processes stay unchanged." : "Use status to inspect the committed state."}`, {
|
|
19381
|
+
display: { state: "queued", action: params.action, label: text.slice(0, 120) }
|
|
19382
|
+
});
|
|
19383
|
+
},
|
|
19384
|
+
renderCall(args, theme) {
|
|
19385
|
+
const label = theme.fg("toolTitle", "smart_context ");
|
|
19386
|
+
const action = safeArg(args.action, 20);
|
|
19387
|
+
const id = safeArg(args.id, 80);
|
|
19388
|
+
const query = safeArg(args.query, 80);
|
|
19389
|
+
const labelArg = safeArg(args.label, 80);
|
|
19390
|
+
const detail = id ? " " + theme.fg("dim", id) : query ? " " + theme.fg("dim", query) : labelArg ? " " + theme.fg("muted", labelArg) : "";
|
|
19391
|
+
return new Text6(label + theme.fg("accent", action) + detail, 0, 0);
|
|
19392
|
+
},
|
|
19393
|
+
renderResult(result, { expanded }, theme, context) {
|
|
19394
|
+
return tryRow(theme, () => renderSmartContextRow(result, expanded, theme, context), result, expanded);
|
|
18531
19395
|
}
|
|
18532
19396
|
});
|
|
18533
19397
|
const clear = () => {
|
|
@@ -18574,7 +19438,8 @@ ${page.text}`);
|
|
|
18574
19438
|
if (!current) {
|
|
18575
19439
|
if (mark?.sessionId !== sessionId)
|
|
18576
19440
|
return;
|
|
18577
|
-
|
|
19441
|
+
const settings = config();
|
|
19442
|
+
if (!settings.contextHygieneEnabled || settings.contextPressureOnly || options.canAutoTrim?.(ctx) === false || !unchangedSince(branch, mark.leafId) || lastAnchorBoundary(branch) !== lastAnchorBoundary(branch.slice(0, branch.findIndex((entry) => entry.id === mark.leafId) + 1)) || thinkingEditReason(branch, mark.entries, ctx.model?.api)) {
|
|
18578
19443
|
mark = null;
|
|
18579
19444
|
return;
|
|
18580
19445
|
}
|
|
@@ -18599,7 +19464,7 @@ ${page.text}`);
|
|
|
18599
19464
|
return changed ? { messages } : undefined;
|
|
18600
19465
|
};
|
|
18601
19466
|
const vetoWarming = (event, ctx, sessionId) => {
|
|
18602
|
-
if (mark?.sessionId !== sessionId || !unchangedSince(ctx.sessionManager.getBranch(), mark.leafId))
|
|
19467
|
+
if (config().contextPressureOnly || mark?.sessionId !== sessionId || !unchangedSince(ctx.sessionManager.getBranch(), mark.leafId) || thinkingEditReason(ctx.sessionManager.getBranch(), mark.entries, ctx.model?.api))
|
|
18603
19468
|
return false;
|
|
18604
19469
|
const cost = ctx.model?.cost;
|
|
18605
19470
|
const price = cost ? cost.cacheWrite > 0 ? cost.cacheWrite : cost.input : NaN;
|
|
@@ -18617,15 +19482,25 @@ ${page.text}`);
|
|
|
18617
19482
|
if (event.outcome !== "completed" || request?.signal?.aborted) {
|
|
18618
19483
|
return request ? cancelled(event, "The turn did not complete.") : undefined;
|
|
18619
19484
|
}
|
|
18620
|
-
|
|
19485
|
+
const requestTool = request?.anchorBoundary !== undefined ? "smart_navigation" : TOOL_NAME;
|
|
19486
|
+
if (request && !request.manual && (config().toolLoading === "off" || !pi.getActiveTools().includes(requestTool)))
|
|
18621
19487
|
return cancelled(event, "Agent tool access was revoked.");
|
|
19488
|
+
if (request?.anchorBoundary !== undefined && !config().contextNavigationEnabled)
|
|
19489
|
+
return cancelled(event, "Navigation was disabled.");
|
|
18622
19490
|
if (options.isPaused?.(ctx)) {
|
|
18623
19491
|
return request ? cancelled(event, "A pending navigation pivot takes priority.") : undefined;
|
|
18624
19492
|
}
|
|
18625
19493
|
if (request && !request.manual && options.canAgentMutate?.(ctx) === false) {
|
|
18626
19494
|
return cancelled(event, "Agent-requested context changes are disabled by policy.");
|
|
18627
19495
|
}
|
|
18628
|
-
|
|
19496
|
+
if (request && !request.manual && request.action !== "checkpoint" && options.canAutoTrim?.(ctx) === false) {
|
|
19497
|
+
return cancelled(event, "Prepared/running compaction or unavailable recovery tools take priority.");
|
|
19498
|
+
}
|
|
19499
|
+
const settings = config();
|
|
19500
|
+
const pressure = contextPressure(ctx, settings).cleanup;
|
|
19501
|
+
if (request && !request.manual && request.action !== "checkpoint" && settings.contextPressureOnly && !pressure) {
|
|
19502
|
+
return cancelled(event, "Context pressure cleared; history left unchanged.");
|
|
19503
|
+
}
|
|
18629
19504
|
if (!request) {
|
|
18630
19505
|
const configNow = config();
|
|
18631
19506
|
const hygiene = configNow.contextHygieneEnabled;
|
|
@@ -18634,11 +19509,11 @@ ${page.text}`);
|
|
|
18634
19509
|
const enabled = hygiene || configNow.autoTrigger && configNow.autoTriggerStrategy === "background";
|
|
18635
19510
|
if (!enabled || options.canAutoTrim?.(ctx) === false)
|
|
18636
19511
|
return;
|
|
18637
|
-
|
|
18638
|
-
|
|
18639
|
-
|
|
18640
|
-
|
|
18641
|
-
|
|
19512
|
+
if ((configNow.contextPressureOnly || !hygiene) && !pressure) {
|
|
19513
|
+
mark = null;
|
|
19514
|
+
if (!applied)
|
|
19515
|
+
return;
|
|
19516
|
+
}
|
|
18642
19517
|
}
|
|
18643
19518
|
const branch = ctx.sessionManager.getBranch();
|
|
18644
19519
|
const sessionId = resolveSessionId(ctx);
|
|
@@ -18669,13 +19544,15 @@ ${page.text}`);
|
|
|
18669
19544
|
}
|
|
18670
19545
|
})];
|
|
18671
19546
|
} else if (request?.action === "rewind") {
|
|
18672
|
-
entries = planContextRewind(branch, sessionId, request.checkpointId, request.text).entries;
|
|
19547
|
+
entries = planContextRewind(branch, sessionId, request.checkpointId, request.text, ctx.model?.api).entries;
|
|
18673
19548
|
} else if (!request && applied?.sessionId === sessionId && unchangedSince(branch, applied.leafId)) {
|
|
18674
19549
|
mark = null;
|
|
18675
19550
|
entries = applied.entries;
|
|
18676
19551
|
applied = null;
|
|
18677
19552
|
} else {
|
|
18678
|
-
const plan = planContextTrim(branch, state.checkpoint?.originId);
|
|
19553
|
+
const plan = planContextTrim(branch, state.checkpoint?.originId, { anchorBoundary: request?.anchorBoundary, readerApi: ctx.model?.api });
|
|
19554
|
+
if (plan.blockedReason)
|
|
19555
|
+
return request ? cancelled(event, plan.blockedReason) : undefined;
|
|
18679
19556
|
if (request) {
|
|
18680
19557
|
entries = trimEntries(plan, request.manual ? "manual" : "agent");
|
|
18681
19558
|
} else {
|
|
@@ -18716,41 +19593,54 @@ ${page.text}`);
|
|
|
18716
19593
|
}
|
|
18717
19594
|
return { entries: [...event.entries, ...entries] };
|
|
18718
19595
|
});
|
|
18719
|
-
const
|
|
18720
|
-
|
|
18721
|
-
|
|
18722
|
-
|
|
18723
|
-
|
|
18724
|
-
|
|
18725
|
-
|
|
18726
|
-
|
|
18727
|
-
|
|
18728
|
-
|
|
18729
|
-
|
|
18730
|
-
|
|
18731
|
-
|
|
18732
|
-
|
|
18733
|
-
|
|
18734
|
-
|
|
18735
|
-
|
|
18736
|
-
|
|
18737
|
-
|
|
18738
|
-
|
|
18739
|
-
|
|
18740
|
-
|
|
18741
|
-
|
|
18742
|
-
|
|
18743
|
-
|
|
18744
|
-
|
|
18745
|
-
|
|
18746
|
-
|
|
18747
|
-
|
|
18748
|
-
|
|
18749
|
-
|
|
18750
|
-
|
|
19596
|
+
const requestTrim = (ctx, anchor) => {
|
|
19597
|
+
const sessionId = resolveSessionId(ctx);
|
|
19598
|
+
if (isUnresolvedSessionId(sessionId)) {
|
|
19599
|
+
return { state: "unavailable", notice: "Context control needs an identifiable session." };
|
|
19600
|
+
}
|
|
19601
|
+
if (options.isPaused?.(ctx)) {
|
|
19602
|
+
return { state: "paused", notice: "A navigation pivot is pending; try again after it finishes." };
|
|
19603
|
+
}
|
|
19604
|
+
if (queued) {
|
|
19605
|
+
return { state: "busy", notice: "Another context change is already queued; it applies at the next turn boundary." };
|
|
19606
|
+
}
|
|
19607
|
+
if (anchor && options.canAutoTrim?.(ctx) === false) {
|
|
19608
|
+
return { state: "busy", notice: "Cleanup deferred: prepared/running compaction or unavailable recovery tools take priority." };
|
|
19609
|
+
}
|
|
19610
|
+
if (anchor?.callId && (options.canAgentMutate?.(ctx) === false || config().contextPressureOnly && !contextPressure(ctx, config()).cleanup)) {
|
|
19611
|
+
return { state: "unavailable", notice: "Cleanup not requested: no context pressure or agent changes are disabled." };
|
|
19612
|
+
}
|
|
19613
|
+
const branch = ctx.sessionManager.getBranch();
|
|
19614
|
+
const origin = anchor ? branch.findIndex((entry) => entry.id === anchor.originId) : -1;
|
|
19615
|
+
if (anchor && origin < 0)
|
|
19616
|
+
return { state: "unavailable", notice: "Anchor origin is no longer on this branch." };
|
|
19617
|
+
const anchorBoundary = anchor ? lastAnchorBoundary(branch.slice(0, origin + 1)) ?? null : undefined;
|
|
19618
|
+
const plan = planContextTrim(branch, inspectContext(branch, sessionId).checkpoint?.originId, { anchorBoundary, readerApi: ctx.model?.api });
|
|
19619
|
+
if (!plan.entries.length || anchor?.callId && plan.automatic !== "ready") {
|
|
19620
|
+
return { state: "no-eligible", notice: plan.blockedReason ?? (anchor ? "No safe cleanup batch ready; recent turns and protected prefixes stay unchanged." : "No eligible archived output to trim.") };
|
|
19621
|
+
}
|
|
19622
|
+
mark = null;
|
|
19623
|
+
queued = {
|
|
19624
|
+
action: "trim",
|
|
19625
|
+
callId: anchor?.callId ?? "",
|
|
19626
|
+
sessionId,
|
|
19627
|
+
originId: anchor?.originId ?? branch.at(-1)?.id ?? "",
|
|
19628
|
+
checkpointId: "",
|
|
19629
|
+
text: "",
|
|
19630
|
+
manual: !anchor?.callId,
|
|
19631
|
+
anchorBoundary,
|
|
19632
|
+
signal: anchor?.signal
|
|
19633
|
+
};
|
|
19634
|
+
return {
|
|
19635
|
+
state: "queued",
|
|
19636
|
+
notice: anchor?.callId ? "Anchor cleanup queued for the completed tool batch; prior anchor prefixes and recent turns stay protected. Use status to confirm the committed state." : "Manual trim queued. The first next provider request is not yet trimmed; the change applies at the next completed turn boundary. A pending navigation pivot or newer boundary change cancels it."
|
|
19637
|
+
};
|
|
19638
|
+
};
|
|
19639
|
+
return {
|
|
19640
|
+
requestManualTrim: (ctx) => requestTrim(ctx),
|
|
19641
|
+
requestAnchorTrim: (ctx, originId, callId, signal) => requestTrim(ctx, { originId, callId, signal }),
|
|
18751
19642
|
deferredTrim: deferred
|
|
18752
19643
|
};
|
|
18753
|
-
return controller;
|
|
18754
19644
|
}
|
|
18755
19645
|
function cancelled(event, reason) {
|
|
18756
19646
|
return {
|
|
@@ -18762,6 +19652,48 @@ function cancelled(event, reason) {
|
|
|
18762
19652
|
}]
|
|
18763
19653
|
};
|
|
18764
19654
|
}
|
|
19655
|
+
function renderSmartContextRow(result, expanded, theme, context) {
|
|
19656
|
+
if (context.isError) {
|
|
19657
|
+
const head = theme.fg("error", "smart_context failed:");
|
|
19658
|
+
return expanded ? expandedRow(theme, result, [head]) : new Text6(head + " " + summarizeLine(firstTextContent(result.content), 140), 0, 0);
|
|
19659
|
+
}
|
|
19660
|
+
const display = result.details?.display;
|
|
19661
|
+
if (!display)
|
|
19662
|
+
return rawFallbackRow(theme, result, expanded);
|
|
19663
|
+
const lines = [];
|
|
19664
|
+
if (display.state === "queued") {
|
|
19665
|
+
lines.push(statusLabel(theme, "queued") + " " + theme.fg("muted", String(display.action ?? "change") + " queued \u2014 applies at the turn boundary, not yet applied"));
|
|
19666
|
+
if (typeof display.label === "string" && display.label) {
|
|
19667
|
+
lines.push(theme.fg("dim", summarizeLine(display.label, 120)));
|
|
19668
|
+
}
|
|
19669
|
+
return new Text6(lines.join(`
|
|
19670
|
+
`), 0, 0);
|
|
19671
|
+
}
|
|
19672
|
+
if (display.kind === "status") {
|
|
19673
|
+
const percent = display.pressurePercent;
|
|
19674
|
+
lines.push(theme.fg("muted", "context " + (percent == null ? "unknown" : percent + "%")) + " " + statusLabel(theme, display.cleanupBatchReady ? "pending" : "info") + " " + theme.fg("dim", display.cleanupBatchReady ? "cleanup batch ready" : display.cleanupBatchReason ? "cleanup: " + String(display.cleanupBatchReason) : "no cleanup batch"));
|
|
19675
|
+
lines.push(metaLine(theme, "archived outputs", String(display.archivedOutputs ?? 0)));
|
|
19676
|
+
if (display.checkpoint)
|
|
19677
|
+
lines.push(metaLine(theme, "checkpoint", String(display.checkpoint)));
|
|
19678
|
+
if (display.pending)
|
|
19679
|
+
lines.push(metaLine(theme, "pending", String(display.pending)));
|
|
19680
|
+
if (display.cleanupBlockedReason)
|
|
19681
|
+
lines.push(theme.fg("dim", summarizeLine(String(display.cleanupBlockedReason), 120)));
|
|
19682
|
+
return expanded ? expandedRow(theme, result, lines) : new Text6(lines.join(`
|
|
19683
|
+
`), 0, 0);
|
|
19684
|
+
}
|
|
19685
|
+
if (display.kind === "plan") {
|
|
19686
|
+
const percent = display.pressurePercent;
|
|
19687
|
+
lines.push(theme.fg("muted", "context " + (percent == null ? "unknown" : percent + "%")) + " " + theme.fg("dim", "trim plan: " + (Number(display.outputs) || 0) + " output(s) \u2248" + (Number(display.savedChars) || 0).toLocaleString() + " chars"));
|
|
19688
|
+
if (display.batchReady)
|
|
19689
|
+
lines.push(metaLine(theme, "batch", "ready"));
|
|
19690
|
+
else if (display.batchReason)
|
|
19691
|
+
lines.push(metaLine(theme, "batch", String(display.batchReason)));
|
|
19692
|
+
return expanded ? expandedRow(theme, result, lines) : new Text6(lines.join(`
|
|
19693
|
+
`), 0, 0);
|
|
19694
|
+
}
|
|
19695
|
+
return rawFallbackRow(theme, result, expanded);
|
|
19696
|
+
}
|
|
18765
19697
|
|
|
18766
19698
|
// src/app/model-feasibility.ts
|
|
18767
19699
|
async function createModelFeasibilityResolver(ctx, config, requestedMode = config.mode, steering = {}) {
|
|
@@ -19175,7 +20107,7 @@ import {
|
|
|
19175
20107
|
getKeybindings as getKeybindings2,
|
|
19176
20108
|
Key as Key4,
|
|
19177
20109
|
matchesKey as matchesKey4,
|
|
19178
|
-
Text as
|
|
20110
|
+
Text as Text9,
|
|
19179
20111
|
wrapTextWithAnsi as wrapTextWithAnsi3
|
|
19180
20112
|
} from "@earendil-works/pi-tui";
|
|
19181
20113
|
|
|
@@ -19184,7 +20116,7 @@ import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
|
19184
20116
|
import {
|
|
19185
20117
|
getKeybindings,
|
|
19186
20118
|
SettingsList,
|
|
19187
|
-
truncateToWidth as
|
|
20119
|
+
truncateToWidth as truncateToWidth4,
|
|
19188
20120
|
visibleWidth as visibleWidth2,
|
|
19189
20121
|
wrapTextWithAnsi as wrapTextWithAnsi2
|
|
19190
20122
|
} from "@earendil-works/pi-tui";
|
|
@@ -19230,8 +20162,8 @@ class SmartSettingsList extends SettingsList {
|
|
|
19230
20162
|
internals.items.forEach((item, index) => {
|
|
19231
20163
|
const original = saved[index];
|
|
19232
20164
|
const shown = (this.inactive.has(item.id) ? INACTIVE_PREFIX : "") + original.currentValue;
|
|
19233
|
-
item.label =
|
|
19234
|
-
item.currentValue =
|
|
20165
|
+
item.label = truncateToWidth4(original.label, labelWidth, "\u2026");
|
|
20166
|
+
item.currentValue = truncateToWidth4(shown, valueWidth, "\u2026");
|
|
19235
20167
|
if (index === internals.selectedIndex) {
|
|
19236
20168
|
item.description = [
|
|
19237
20169
|
item.label !== original.label ? original.label : "",
|
|
@@ -19272,7 +20204,7 @@ class SmartSettingsList extends SettingsList {
|
|
|
19272
20204
|
// src/ui/settings-overlay.ts
|
|
19273
20205
|
import {
|
|
19274
20206
|
Container as Container4,
|
|
19275
|
-
Text as
|
|
20207
|
+
Text as Text8
|
|
19276
20208
|
} from "@earendil-works/pi-tui";
|
|
19277
20209
|
|
|
19278
20210
|
// src/ui/settings-complex.ts
|
|
@@ -19283,7 +20215,7 @@ import {
|
|
|
19283
20215
|
Key as Key3,
|
|
19284
20216
|
matchesKey as matchesKey3,
|
|
19285
20217
|
SelectList as SelectList3,
|
|
19286
|
-
Text as
|
|
20218
|
+
Text as Text7
|
|
19287
20219
|
} from "@earendil-works/pi-tui";
|
|
19288
20220
|
var MODEL_SETTINGS = [
|
|
19289
20221
|
{
|
|
@@ -19331,7 +20263,7 @@ var MIN_CONTEXT_SETTING = {
|
|
|
19331
20263
|
var MAX_CONTEXT_SETTING = {
|
|
19332
20264
|
id: "maxContextTokens",
|
|
19333
20265
|
label: "Context cap for start % (tokens)",
|
|
19334
|
-
description: "
|
|
20266
|
+
description: "Cleanup, agent tools and automatic compaction measure pressure against this cap when smaller than the model window. Model requests and safety headroom still use the real window. 0 = off.",
|
|
19335
20267
|
placeholder: "0 or 16384\u20132000000; blank = off",
|
|
19336
20268
|
parse: numberParser(CONFIG_NUMERIC_LIMITS.maxContextTokens),
|
|
19337
20269
|
format: (value) => value === undefined || value === 0 ? "off" : String(value)
|
|
@@ -19339,8 +20271,8 @@ var MAX_CONTEXT_SETTING = {
|
|
|
19339
20271
|
var parsePreparePercent = numberParser(CONFIG_NUMERIC_LIMITS.prepareContextPercent);
|
|
19340
20272
|
var PREPARE_CONTEXT_SETTING = {
|
|
19341
20273
|
id: "prepareContextPercent",
|
|
19342
|
-
label: "
|
|
19343
|
-
description: "
|
|
20274
|
+
label: "Cleanup / prepare at context %",
|
|
20275
|
+
description: "Early pressure gate for cleanup and optional background preparation. Compaction starts at Start at context %, so this must be lower. Auto starts 8k\u201332k tokens earlier.",
|
|
19344
20276
|
placeholder: "0\u2013100 below Start at; auto or blank = Auto",
|
|
19345
20277
|
parse: (input) => input.trim().toLowerCase() === "auto" ? undefined : parsePreparePercent(input),
|
|
19346
20278
|
format: (value) => value === undefined || value === null ? "Auto" : String(value)
|
|
@@ -19549,7 +20481,7 @@ class InputSettingEditor extends Container3 {
|
|
|
19549
20481
|
save;
|
|
19550
20482
|
done;
|
|
19551
20483
|
input = new Input;
|
|
19552
|
-
status = new
|
|
20484
|
+
status = new Text7("", 0, 0);
|
|
19553
20485
|
saving = false;
|
|
19554
20486
|
pending = Promise.resolve();
|
|
19555
20487
|
get focused() {
|
|
@@ -19563,10 +20495,10 @@ class InputSettingEditor extends Container3 {
|
|
|
19563
20495
|
this.requestRender = requestRender;
|
|
19564
20496
|
this.save = save;
|
|
19565
20497
|
this.done = done;
|
|
19566
|
-
this.addChild(new
|
|
19567
|
-
this.addChild(new
|
|
19568
|
-
this.addChild(new
|
|
19569
|
-
this.addChild(new
|
|
20498
|
+
this.addChild(new Text7(setting.label, 0, 0));
|
|
20499
|
+
this.addChild(new Text7(setting.description, 0, 0));
|
|
20500
|
+
this.addChild(new Text7(`Allowed: ${setting.placeholder}`, 0, 0));
|
|
20501
|
+
this.addChild(new Text7("", 0, 0));
|
|
19570
20502
|
this.input.setValue(initial);
|
|
19571
20503
|
this.input.handleInput("\x1B[F");
|
|
19572
20504
|
this.input.onSubmit = (value) => {
|
|
@@ -19594,9 +20526,9 @@ class InputSettingEditor extends Container3 {
|
|
|
19594
20526
|
this.done();
|
|
19595
20527
|
};
|
|
19596
20528
|
this.addChild(this.input);
|
|
19597
|
-
this.addChild(new
|
|
20529
|
+
this.addChild(new Text7("", 0, 0));
|
|
19598
20530
|
this.addChild(this.status);
|
|
19599
|
-
this.addChild(new
|
|
20531
|
+
this.addChild(new Text7("Enter save \xB7 Esc cancel", 0, 0));
|
|
19600
20532
|
}
|
|
19601
20533
|
handleInput(data) {
|
|
19602
20534
|
this.input.handleInput(data);
|
|
@@ -19611,7 +20543,7 @@ class ModelSettingEditor extends Container3 {
|
|
|
19611
20543
|
requestRender;
|
|
19612
20544
|
search = new Input;
|
|
19613
20545
|
list;
|
|
19614
|
-
status = new
|
|
20546
|
+
status = new Text7("", 0, 0);
|
|
19615
20547
|
saving = false;
|
|
19616
20548
|
pending = Promise.resolve();
|
|
19617
20549
|
get focused() {
|
|
@@ -19623,9 +20555,9 @@ class ModelSettingEditor extends Container3 {
|
|
|
19623
20555
|
constructor(models, selectedValue, requestRender, save, done) {
|
|
19624
20556
|
super();
|
|
19625
20557
|
this.requestRender = requestRender;
|
|
19626
|
-
this.addChild(new
|
|
20558
|
+
this.addChild(new Text7("Type to search models", 0, 0));
|
|
19627
20559
|
this.addChild(this.search);
|
|
19628
|
-
this.addChild(new
|
|
20560
|
+
this.addChild(new Text7("", 0, 0));
|
|
19629
20561
|
this.list = new SelectList3(models, 10, {
|
|
19630
20562
|
selectedPrefix: (text) => text,
|
|
19631
20563
|
selectedText: (text) => text,
|
|
@@ -19660,7 +20592,7 @@ class ModelSettingEditor extends Container3 {
|
|
|
19660
20592
|
};
|
|
19661
20593
|
this.addChild(this.list);
|
|
19662
20594
|
this.addChild(this.status);
|
|
19663
|
-
this.addChild(new
|
|
20595
|
+
this.addChild(new Text7("Type to filter \xB7 Enter select \xB7 Esc cancel", 0, 0));
|
|
19664
20596
|
}
|
|
19665
20597
|
handleInput(data) {
|
|
19666
20598
|
if (matchesKey3(data, Key3.up) || matchesKey3(data, Key3.down) || matchesKey3(data, Key3.enter) || matchesKey3(data, Key3.escape)) {
|
|
@@ -19899,7 +20831,6 @@ var THINKING_VALUES = [
|
|
|
19899
20831
|
"max"
|
|
19900
20832
|
];
|
|
19901
20833
|
var needsAutoTrigger = (config) => config.autoTrigger ? undefined : "turn on Automatic compaction first";
|
|
19902
|
-
var needsBackgroundTiming = (config) => config.autoTrigger && config.autoTriggerStrategy === "background" ? undefined : "used only when Start when = Prepare in background";
|
|
19903
20834
|
var needsMnemopi = (config) => config.memoryBackend === "mnemopi" ? undefined : "used only when Memory store = Mnemopi";
|
|
19904
20835
|
var needsHindsight = (config) => config.memoryBackend === "hindsight" ? undefined : "used only when Memory store = Hindsight server";
|
|
19905
20836
|
var needsBackups = (config) => config.backupEnabled ? undefined : "turn on Backups first";
|
|
@@ -19930,9 +20861,9 @@ var CATEGORIES = [
|
|
|
19930
20861
|
labels: { "native-hook": "Before Pi's compaction", settled: "When idle", background: "Prepare in background" }
|
|
19931
20862
|
}
|
|
19932
20863
|
},
|
|
19933
|
-
{ kind: "input", setting: MIN_CONTEXT_SETTING
|
|
19934
|
-
{ kind: "input", setting: MAX_CONTEXT_SETTING
|
|
19935
|
-
{ kind: "input", setting: PREPARE_CONTEXT_SETTING
|
|
20864
|
+
{ kind: "input", setting: MIN_CONTEXT_SETTING },
|
|
20865
|
+
{ kind: "input", setting: MAX_CONTEXT_SETTING },
|
|
20866
|
+
{ kind: "input", setting: PREPARE_CONTEXT_SETTING },
|
|
19936
20867
|
{
|
|
19937
20868
|
kind: "choice",
|
|
19938
20869
|
setting: {
|
|
@@ -20047,7 +20978,7 @@ var CATEGORIES = [
|
|
|
20047
20978
|
setting: {
|
|
20048
20979
|
id: "toolLoading",
|
|
20049
20980
|
label: "Agent tools",
|
|
20050
|
-
description: "
|
|
20981
|
+
description: "Always available (default): permitted tools stay visible with a stable prompt prefix. On demand saves initial schema tokens but later loading can rebuild the cache. Off disables agent context tools, not human commands. Host permissions still apply.",
|
|
20051
20982
|
values: ["lazy", "eager", "off"],
|
|
20052
20983
|
labels: { lazy: "On demand", eager: "Always available", off: "Off" }
|
|
20053
20984
|
}
|
|
@@ -20116,17 +21047,27 @@ var CATEGORIES = [
|
|
|
20116
21047
|
{
|
|
20117
21048
|
id: "hygiene",
|
|
20118
21049
|
label: "Tool output cleanup",
|
|
20119
|
-
description: "
|
|
21050
|
+
description: "Recoverable, batched cleanup; no model call, but history edits can rebuild the prompt cache",
|
|
20120
21051
|
rows: [
|
|
20121
21052
|
{
|
|
20122
21053
|
kind: "choice",
|
|
20123
21054
|
setting: {
|
|
20124
21055
|
id: "contextHygieneEnabled",
|
|
20125
21056
|
label: "Automatic cleanup",
|
|
20126
|
-
description: "
|
|
21057
|
+
description: "Archives safe old output in batches under context pressure; preserves recent turns, instructions and signed thinking dependencies. Cleanup and compaction use the same pressure window.",
|
|
20127
21058
|
values: BOOLEAN_VALUES
|
|
20128
21059
|
}
|
|
20129
21060
|
},
|
|
21061
|
+
{
|
|
21062
|
+
kind: "choice",
|
|
21063
|
+
setting: {
|
|
21064
|
+
id: "contextPressureOnly",
|
|
21065
|
+
label: "Cleanup timing",
|
|
21066
|
+
description: "Pressure only (default) leaves roomy cached history alone, including agent trim/rewind/anchor requests. Economic also allows break-even/cold-cache cleanup. Human commands can request early cleanup; safety checks always apply.",
|
|
21067
|
+
values: BOOLEAN_VALUES,
|
|
21068
|
+
labels: { true: "Pressure only", false: "Economic (opt-in)" }
|
|
21069
|
+
}
|
|
21070
|
+
},
|
|
20130
21071
|
{
|
|
20131
21072
|
kind: "choice",
|
|
20132
21073
|
setting: {
|
|
@@ -20656,16 +21597,16 @@ async function showSmartCompactSettings(ctx, policy, coordinator = new GlobalSet
|
|
|
20656
21597
|
await ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
20657
21598
|
const root = createSettingsRoot(policy, ctx, done, () => tui.requestRender(), coordinator, onApplied, writeConfig);
|
|
20658
21599
|
const container = new Container4;
|
|
20659
|
-
container.addChild(new
|
|
20660
|
-
container.addChild(new
|
|
20661
|
-
container.addChild(new
|
|
21600
|
+
container.addChild(new Text8(theme.fg("accent", theme.bold("Smart Compact Settings")), 0, 0));
|
|
21601
|
+
container.addChild(new Text8(theme.fg("dim", "Changes are saved for all projects; This branch only changes just this branch."), 0, 0));
|
|
21602
|
+
container.addChild(new Text8(theme.fg("dim", "/smart-compact always works. \u2022 = changed from default."), 0, 0));
|
|
20662
21603
|
const invalid = invalidSettingsLine();
|
|
20663
21604
|
if (invalid)
|
|
20664
|
-
container.addChild(new
|
|
20665
|
-
container.addChild(new
|
|
21605
|
+
container.addChild(new Text8(theme.fg("warning", invalid), 0, 0));
|
|
21606
|
+
container.addChild(new Text8("", 0, 0));
|
|
20666
21607
|
container.addChild(root);
|
|
20667
|
-
container.addChild(new
|
|
20668
|
-
container.addChild(new
|
|
21608
|
+
container.addChild(new Text8("", 0, 0));
|
|
21609
|
+
container.addChild(new Text8(theme.fg("dim", "\u2191\u2193 move \xB7 Enter change/open \xB7 r reset \xB7 Esc back"), 0, 0));
|
|
20669
21610
|
return createSettingsController(root, container, () => tui.requestRender());
|
|
20670
21611
|
});
|
|
20671
21612
|
}
|
|
@@ -20691,7 +21632,7 @@ var BEHAVIOR_PROFILES = [
|
|
|
20691
21632
|
{
|
|
20692
21633
|
id: "cleanup",
|
|
20693
21634
|
label: "Cleanup only",
|
|
20694
|
-
summary: "
|
|
21635
|
+
summary: "Archives safe old output using Cleanup timing (pressure-only by default). No model call, but history edits can rebuild the cache; never compacts by itself.",
|
|
20695
21636
|
patch: { autoTrigger: false, contextHygieneEnabled: true, agentToolAccess: "disabled" }
|
|
20696
21637
|
},
|
|
20697
21638
|
{
|
|
@@ -20712,11 +21653,11 @@ var AGENT_WORDS = {
|
|
|
20712
21653
|
disabled: "not allowed"
|
|
20713
21654
|
};
|
|
20714
21655
|
function deriveBehaviorProfile(config) {
|
|
20715
|
-
if (config.autoTrigger === DEFAULT_CONFIG.autoTrigger && config.autoTriggerStrategy === DEFAULT_CONFIG.autoTriggerStrategy && config.contextHygieneEnabled === DEFAULT_CONFIG.contextHygieneEnabled && config.agentToolAccess === DEFAULT_CONFIG.agentToolAccess) {
|
|
21656
|
+
if (config.autoTrigger === DEFAULT_CONFIG.autoTrigger && config.autoTriggerStrategy === DEFAULT_CONFIG.autoTriggerStrategy && config.contextHygieneEnabled === DEFAULT_CONFIG.contextHygieneEnabled && config.contextPressureOnly === DEFAULT_CONFIG.contextPressureOnly && config.agentToolAccess === DEFAULT_CONFIG.agentToolAccess) {
|
|
20716
21657
|
return {
|
|
20717
21658
|
id: "custom",
|
|
20718
|
-
label: "
|
|
20719
|
-
detail: "
|
|
21659
|
+
label: "Pressure-first (default)",
|
|
21660
|
+
detail: "Batched cleanup under early pressure, then idle compaction at the apply gate; agent access follows Pi."
|
|
20720
21661
|
};
|
|
20721
21662
|
}
|
|
20722
21663
|
const found = derive(BEHAVIOR_PROFILES, ({ patch }) => Object.entries(patch).every(([key, value]) => config[key] === value), "Custom");
|
|
@@ -20921,7 +21862,7 @@ function createHomeList(options, finish, requestRender) {
|
|
|
20921
21862
|
id: "trim",
|
|
20922
21863
|
label: "Clean up tool output",
|
|
20923
21864
|
currentValue: "no model call",
|
|
20924
|
-
description: "Queue cleanup for the next completed turn. Older output stays retrievable; the next model request is still sent untrimmed."
|
|
21865
|
+
description: "Queue cleanup for the next completed turn. Older output stays retrievable; the next model request is still sent untrimmed. No summarizer call, but a history rewrite can incur cache cost."
|
|
20925
21866
|
};
|
|
20926
21867
|
const setup = {
|
|
20927
21868
|
id: "setup",
|
|
@@ -21162,7 +22103,7 @@ async function showSmartCompactHome(options) {
|
|
|
21162
22103
|
return options.ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
21163
22104
|
const list = createHomeList(options, done, () => tui.requestRender());
|
|
21164
22105
|
const container = new Container5;
|
|
21165
|
-
container.addChild(new
|
|
22106
|
+
container.addChild(new Text9(theme.fg("accent", theme.bold("Smart Compact")), 0, 0));
|
|
21166
22107
|
container.addChild({
|
|
21167
22108
|
render(width) {
|
|
21168
22109
|
const config = loadConfig();
|
|
@@ -21178,15 +22119,15 @@ async function showSmartCompactHome(options) {
|
|
|
21178
22119
|
});
|
|
21179
22120
|
const invalid = invalidSettingsLine();
|
|
21180
22121
|
if (invalid)
|
|
21181
|
-
container.addChild(new
|
|
21182
|
-
container.addChild(new
|
|
22122
|
+
container.addChild(new Text9(theme.fg("warning", invalid), 0, 0));
|
|
22123
|
+
container.addChild(new Text9("", 0, 0));
|
|
21183
22124
|
container.addChild(list);
|
|
21184
22125
|
return createSettingsController(list, container, () => tui.requestRender());
|
|
21185
22126
|
});
|
|
21186
22127
|
}
|
|
21187
22128
|
|
|
21188
22129
|
// src/ui/metrics-dashboard-overlay.ts
|
|
21189
|
-
import { Key as Key5, matchesKey as matchesKey5, truncateToWidth as
|
|
22130
|
+
import { Key as Key5, matchesKey as matchesKey5, truncateToWidth as truncateToWidth5 } from "@earendil-works/pi-tui";
|
|
21190
22131
|
async function showMetricsDashboardUI(ctx, opts) {
|
|
21191
22132
|
const entries = opts.entries;
|
|
21192
22133
|
const latest = entries.at(-1);
|
|
@@ -21281,23 +22222,23 @@ async function showMetricsDashboardUI(ctx, opts) {
|
|
|
21281
22222
|
return theme.fg(insights.quality.targetMet ? "success" : "warning", " \u2022 Quality " + insights.quality.healthScore + "/100");
|
|
21282
22223
|
};
|
|
21283
22224
|
const renderHeader = (width) => [
|
|
21284
|
-
|
|
21285
|
-
|
|
21286
|
-
|
|
22225
|
+
truncateToWidth5(theme.fg("accent", theme.bold(" \uD83D\uDCCA Smart Compact Dashboard")) + theme.fg("dim", " " + entries.length + " recorded run(s)"), width),
|
|
22226
|
+
truncateToWidth5(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),
|
|
22227
|
+
truncateToWidth5(theme.fg("borderMuted", "\u2500".repeat(Math.max(0, width))), width)
|
|
21287
22228
|
];
|
|
21288
22229
|
return {
|
|
21289
22230
|
render(width) {
|
|
21290
22231
|
const lines = renderHeader(width);
|
|
21291
22232
|
if (view === "menu") {
|
|
21292
|
-
lines.push(
|
|
22233
|
+
lines.push(truncateToWidth5(theme.fg("text", " Choose what to inspect:"), width), "");
|
|
21293
22234
|
for (let index = 0;index < menuItems.length; index++) {
|
|
21294
22235
|
const item = menuItems[index];
|
|
21295
22236
|
const active = index === selected;
|
|
21296
22237
|
const label = active ? theme.fg("accent", theme.bold(item.label)) : theme.fg("text", item.label);
|
|
21297
|
-
lines.push(
|
|
21298
|
-
lines.push(
|
|
22238
|
+
lines.push(truncateToWidth5((active ? " \u203A " : " ") + label, width));
|
|
22239
|
+
lines.push(truncateToWidth5(" " + theme.fg(active ? "muted" : "dim", item.desc), width));
|
|
21299
22240
|
}
|
|
21300
|
-
lines.push("",
|
|
22241
|
+
lines.push("", truncateToWidth5(theme.fg("dim", " \u2191\u2193 navigate \u2022 enter open \u2022 esc/q close"), width));
|
|
21301
22242
|
return lines;
|
|
21302
22243
|
}
|
|
21303
22244
|
const content = pageLines();
|
|
@@ -21310,12 +22251,12 @@ async function showMetricsDashboardUI(ctx, opts) {
|
|
|
21310
22251
|
styled = theme.fg("accent", theme.bold(line));
|
|
21311
22252
|
else if (line.startsWith("-"))
|
|
21312
22253
|
styled = theme.fg("dim", line);
|
|
21313
|
-
lines.push(
|
|
22254
|
+
lines.push(truncateToWidth5(" " + styled, width));
|
|
21314
22255
|
}
|
|
21315
22256
|
if (content.length > pageSize) {
|
|
21316
|
-
lines.push(
|
|
22257
|
+
lines.push(truncateToWidth5(theme.fg("dim", " showing " + (scroll + 1) + "-" + Math.min(content.length, scroll + pageSize) + " of " + content.length), width));
|
|
21317
22258
|
}
|
|
21318
|
-
lines.push("",
|
|
22259
|
+
lines.push("", truncateToWidth5(theme.fg("dim", " \u2191\u2193 scroll \u2022 pgup/pgdn page \u2022 home/end jump \u2022 b back \u2022 esc/q close"), width));
|
|
21319
22260
|
return lines;
|
|
21320
22261
|
},
|
|
21321
22262
|
invalidate() {},
|
|
@@ -21377,7 +22318,7 @@ import {
|
|
|
21377
22318
|
import {
|
|
21378
22319
|
Container as Container6,
|
|
21379
22320
|
Input as Input2,
|
|
21380
|
-
Text as
|
|
22321
|
+
Text as Text10,
|
|
21381
22322
|
wrapTextWithAnsi as wrapTextWithAnsi4
|
|
21382
22323
|
} from "@earendil-works/pi-tui";
|
|
21383
22324
|
var PAGE_SIZE = 8;
|
|
@@ -21486,7 +22427,7 @@ class Framed {
|
|
|
21486
22427
|
|
|
21487
22428
|
class Prompt extends Container6 {
|
|
21488
22429
|
input;
|
|
21489
|
-
status = new
|
|
22430
|
+
status = new Text10("", 0, 0);
|
|
21490
22431
|
busy = false;
|
|
21491
22432
|
get focused() {
|
|
21492
22433
|
return this.input.focused;
|
|
@@ -21498,8 +22439,8 @@ class Prompt extends Container6 {
|
|
|
21498
22439
|
super();
|
|
21499
22440
|
this.input = new Input2;
|
|
21500
22441
|
for (const line of lines)
|
|
21501
|
-
this.addChild(new
|
|
21502
|
-
this.addChild(new
|
|
22442
|
+
this.addChild(new Text10(line, 0, 0));
|
|
22443
|
+
this.addChild(new Text10("", 0, 0));
|
|
21503
22444
|
this.input.setValue(initial);
|
|
21504
22445
|
this.input.handleInput("\x1B[F");
|
|
21505
22446
|
this.input.onSubmit = (value) => {
|
|
@@ -21511,9 +22452,9 @@ class Prompt extends Container6 {
|
|
|
21511
22452
|
cancel();
|
|
21512
22453
|
};
|
|
21513
22454
|
this.addChild(this.input);
|
|
21514
|
-
this.addChild(new
|
|
22455
|
+
this.addChild(new Text10("", 0, 0));
|
|
21515
22456
|
this.addChild(this.status);
|
|
21516
|
-
this.addChild(new
|
|
22457
|
+
this.addChild(new Text10(hint, 0, 0));
|
|
21517
22458
|
}
|
|
21518
22459
|
setStatus(text, busy = false) {
|
|
21519
22460
|
this.status.setText(text);
|
|
@@ -21528,7 +22469,7 @@ class TextArea {
|
|
|
21528
22469
|
open;
|
|
21529
22470
|
submit;
|
|
21530
22471
|
editor;
|
|
21531
|
-
status = new
|
|
22472
|
+
status = new Text10("", 0, 0);
|
|
21532
22473
|
busy = false;
|
|
21533
22474
|
active = false;
|
|
21534
22475
|
constructor(open, submit) {
|
|
@@ -22270,11 +23211,13 @@ async function describeEffectiveState(ctx, config, policy, runtime) {
|
|
|
22270
23211
|
lines.push("Agent tool: " + (policy.agentToolEnabled ? "exposed" : "not exposed") + " (policy " + policy.agentToolAccess + ")");
|
|
22271
23212
|
if (ctx.model && !ctx.model.promptCache)
|
|
22272
23213
|
lines.push("Reader prompt-cache lifetime: not declared; host cache warming cannot schedule from this metadata. No cache savings are assumed.");
|
|
22273
|
-
const
|
|
22274
|
-
const
|
|
22275
|
-
const
|
|
22276
|
-
|
|
22277
|
-
|
|
23214
|
+
const pressure = contextPressure(ctx, config);
|
|
23215
|
+
const window = pressure.modelWindow;
|
|
23216
|
+
const gateWindow = pressure.policyWindow;
|
|
23217
|
+
const tokens = pressure.tokens;
|
|
23218
|
+
if (typeof window === "number" && Number.isFinite(window) && window > 0 && gateWindow !== null) {
|
|
23219
|
+
const gates = { startTokens: pressure.cleanupTokens, applyTokens: pressure.compactionTokens };
|
|
23220
|
+
lines.push("Cleanup gate: \u2265" + gates.startTokens.toLocaleString("en-US") + " tokens; policy usage " + (pressure.percent === null ? "unknown" : pressure.percent.toFixed(1) + "%"));
|
|
22278
23221
|
const apply = Math.ceil(Math.max(MIN_TOKEN_THRESHOLD, gates.applyTokens)).toLocaleString("en-US");
|
|
22279
23222
|
const gate = " (" + config.minContextPercent + "% of " + (gateWindow < window ? "maxContextTokens " : "window ") + gateWindow.toLocaleString("en-US") + ", floor " + MIN_TOKEN_THRESHOLD.toLocaleString("en-US") + ")";
|
|
22280
23223
|
lines.push("Context: " + (typeof tokens === "number" && Number.isFinite(tokens) ? Math.ceil(tokens).toLocaleString("en-US") : "unknown") + "/" + window.toLocaleString("en-US") + " tokens; " + (!policy.autoTrigger ? "automatic compaction off" : hostDriven ? "Pi decides when; PSC replaces Pi's threshold summary only \u2265" + apply + gate + ", overflow skips the % gate" : "PSC starts \u2265" + apply + gate));
|
|
@@ -22286,9 +23229,9 @@ async function describeEffectiveState(ctx, config, policy, runtime) {
|
|
|
22286
23229
|
lines.push("Runtime: " + (runtime ? (runtime.running ? "running" : "idle") + "; preparation " + runtime.preparation : "not attached") + "; candidate reuse is revalidated at apply");
|
|
22287
23230
|
if (runtime?.cacheLedger?.length)
|
|
22288
23231
|
lines.push(...runtime.cacheLedger);
|
|
22289
|
-
lines.push("Auto limits: " + Math.min(config.autoTriggerTimeoutMs, AUTO_TRIGGER_TIMEOUT_CAP_MS) / 1000 + " s timeout; " + SETTLED_TRIGGER_COOLDOWN_MS / 60000 + " min cooldown; " +
|
|
23232
|
+
lines.push("Auto limits: " + Math.min(config.autoTriggerTimeoutMs, AUTO_TRIGGER_TIMEOUT_CAP_MS) / 1000 + " s timeout; " + SETTLED_TRIGGER_COOLDOWN_MS / 60000 + " min cooldown; " + config.pendingTtlMs / 60000 + " min preparation TTL");
|
|
22290
23233
|
const backgroundHygiene = policy.autoTrigger && config.autoTriggerStrategy === "background";
|
|
22291
|
-
lines.push("Context hygiene: " + (config.contextHygieneEnabled ? "on (
|
|
23234
|
+
lines.push("Context hygiene: " + (config.contextHygieneEnabled ? config.contextPressureOnly ? "on (pressure only; roomy cached history stays unchanged)" : "on (economic opt-in: pressure, cache break-even, or cold cache)" : backgroundHygiene ? "on (enabled by effective background strategy, pressure-gated)" : "off") + ((config.contextHygieneEnabled || backgroundHygiene) && config.toolLoading === "off" ? " \u2014 automatic trims inactive: agent tools are off, so archived output could not be read back" : ""));
|
|
22292
23235
|
const memory = await describeMemoryBackendReadiness(config);
|
|
22293
23236
|
lines.push("Memory selected: " + config.memoryBackend + (projectId ? " (project-bound)" : " \u2014 unavailable: no project scope"));
|
|
22294
23237
|
lines.push("Memory readiness: " + memory.reason);
|
|
@@ -22530,7 +23473,8 @@ async function showStorage(ctx) {
|
|
|
22530
23473
|
function compactNowState(ctx, dependencies) {
|
|
22531
23474
|
const config = loadConfig();
|
|
22532
23475
|
const tokens = ctx.getContextUsage()?.tokens;
|
|
22533
|
-
const
|
|
23476
|
+
const window = effectiveContextWindow(ctx.model, config);
|
|
23477
|
+
const value = tokens == null ? "not measured yet" : Math.round(tokens / 1000) + "k tokens \xB7 " + Math.round(safeContextPercent(tokens, window)) + "%" + (window !== ctx.model?.contextWindow ? " of " + window?.toLocaleString("en-US") + " policy window" : "");
|
|
22534
23478
|
const runtime = dependencies.getRuntimeState?.(ctx);
|
|
22535
23479
|
if (runtime?.running)
|
|
22536
23480
|
return { value, blocked: "Compaction is already running. Wait for it to finish." };
|
|
@@ -22942,6 +23886,7 @@ function createSmartCompactPolicy(pi, syncTools) {
|
|
|
22942
23886
|
|
|
22943
23887
|
// src/app/lazy-tools.ts
|
|
22944
23888
|
import { StringEnum as StringEnum3 } from "@earendil-works/pi-ai";
|
|
23889
|
+
import { Text as Text11 } from "@earendil-works/pi-tui";
|
|
22945
23890
|
import { Type as Type5 } from "typebox";
|
|
22946
23891
|
|
|
22947
23892
|
// src/app/context-guide.ts
|
|
@@ -23110,33 +24055,127 @@ function registerContextToolLoader(pi, exposure) {
|
|
|
23110
24055
|
if (loadConfig().toolLoading === "off" || !pi.getActiveTools().includes(LOADER))
|
|
23111
24056
|
throw new Error("Context tools are disabled in Pi settings or /tools.");
|
|
23112
24057
|
let text;
|
|
24058
|
+
let details = undefined;
|
|
23113
24059
|
if (params.action === "guide")
|
|
23114
24060
|
text = await readContextGuide();
|
|
23115
|
-
else if (params.action === "status")
|
|
23116
|
-
|
|
23117
|
-
|
|
24061
|
+
else if (params.action === "status") {
|
|
24062
|
+
const status = exposure.status();
|
|
24063
|
+
text = JSON.stringify(status);
|
|
24064
|
+
details = { display: { kind: "tool-groups" }, status };
|
|
24065
|
+
} else {
|
|
23118
24066
|
if (!params.group)
|
|
23119
24067
|
throw new Error(`${params.action} requires a group.`);
|
|
23120
|
-
if (params.action === "load")
|
|
23121
|
-
|
|
23122
|
-
|
|
24068
|
+
if (params.action === "load") {
|
|
24069
|
+
const names = exposure.load(params.group);
|
|
24070
|
+
text = `Loaded: ${names.join(", ")}. Available on the next model turn; no feature instructions were injected.`;
|
|
24071
|
+
details = { display: { state: "loaded", group: params.group, tools: names } };
|
|
24072
|
+
} else {
|
|
23123
24073
|
if (loadConfig().toolLoading === "eager")
|
|
23124
24074
|
throw new Error("Eager mode keeps tools loaded. Select On demand or Off in /smart-compact settings before unloading a group.");
|
|
23125
24075
|
exposure.unload(params.group);
|
|
23126
24076
|
text = `Unloaded ${params.group}. No files or session history changed.`;
|
|
24077
|
+
details = { display: { state: "unloaded", group: params.group } };
|
|
23127
24078
|
}
|
|
23128
24079
|
}
|
|
23129
|
-
return { content: [{ type: "text", text }], details
|
|
24080
|
+
return { content: [{ type: "text", text }], details };
|
|
24081
|
+
},
|
|
24082
|
+
renderCall(args, theme) {
|
|
24083
|
+
const label = theme.fg("toolTitle", "smart_tools ");
|
|
24084
|
+
const action = safeArg(args.action, 20);
|
|
24085
|
+
const group = safeArg(args.group, 40);
|
|
24086
|
+
return new Text11(label + theme.fg("accent", action) + (group ? " " + theme.fg("dim", group) : ""), 0, 0);
|
|
24087
|
+
},
|
|
24088
|
+
renderResult(result, options, theme, context) {
|
|
24089
|
+
return tryRow(theme, () => renderToolsRow(result, theme, context, options.expanded), result, options.expanded);
|
|
23130
24090
|
}
|
|
23131
24091
|
});
|
|
23132
24092
|
}
|
|
24093
|
+
function renderToolsRow(result, theme, context, expanded) {
|
|
24094
|
+
if (context.isError) {
|
|
24095
|
+
const head = theme.fg("error", "smart_tools failed:");
|
|
24096
|
+
return expanded ? expandedRow(theme, result, [head]) : new Text11(head + " " + summarizeLine(firstTextContent(result.content), 140), 0, 0);
|
|
24097
|
+
}
|
|
24098
|
+
const details = result.details;
|
|
24099
|
+
if (!details)
|
|
24100
|
+
return rawFallbackRow(theme, result, expanded);
|
|
24101
|
+
const lines = [];
|
|
24102
|
+
if (details.display?.state === "loaded") {
|
|
24103
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "loaded " + String(details.display.group)));
|
|
24104
|
+
if (details.display.tools?.length) {
|
|
24105
|
+
lines.push(theme.fg("dim", details.display.tools.join(", ")));
|
|
24106
|
+
}
|
|
24107
|
+
return new Text11(lines.join(`
|
|
24108
|
+
`), 0, 0);
|
|
24109
|
+
}
|
|
24110
|
+
if (details.display?.state === "unloaded") {
|
|
24111
|
+
lines.push(statusLabel(theme, "info") + " " + theme.fg("muted", "unloaded " + String(details.display.group)));
|
|
24112
|
+
return new Text11(lines.join(`
|
|
24113
|
+
`), 0, 0);
|
|
24114
|
+
}
|
|
24115
|
+
if (details.display?.kind === "tool-groups" && details.status) {
|
|
24116
|
+
lines.push(theme.fg("muted", "mode: ") + theme.fg("accent", String(details.status.mode ?? "")));
|
|
24117
|
+
for (const group of details.status.groups ?? []) {
|
|
24118
|
+
const marker = group.active.length ? theme.fg("success", "\u25CF") : group.available ? theme.fg("dim", "\u25CB") : theme.fg("error", "\u2715");
|
|
24119
|
+
const state = group.active.length ? group.active.length + " active" : group.available ? "available" : "disabled";
|
|
24120
|
+
lines.push(marker + " " + group.group + " " + theme.fg("dim", state));
|
|
24121
|
+
}
|
|
24122
|
+
return expanded ? expandedRow(theme, result, lines) : new Text11(lines.join(`
|
|
24123
|
+
`), 0, 0);
|
|
24124
|
+
}
|
|
24125
|
+
return rawFallbackRow(theme, result, expanded);
|
|
24126
|
+
}
|
|
23133
24127
|
|
|
23134
24128
|
// src/app/register-navigation.ts
|
|
23135
24129
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
23136
24130
|
import path26 from "path";
|
|
23137
24131
|
import { StringEnum as StringEnum4 } from "@earendil-works/pi-ai";
|
|
23138
24132
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
24133
|
+
import { Text as Text12 } from "@earendil-works/pi-tui";
|
|
23139
24134
|
import { Type as Type6 } from "typebox";
|
|
24135
|
+
function renderNavigationRow(result, expanded, theme, context) {
|
|
24136
|
+
if (context.isError) {
|
|
24137
|
+
const head = theme.fg("error", "navigation failed:");
|
|
24138
|
+
return expanded ? expandedRow(theme, result, [head]) : new Text12(head + " " + summarizeLine(firstTextContent(result.content), 140), 0, 0);
|
|
24139
|
+
}
|
|
24140
|
+
const details = result.details;
|
|
24141
|
+
if (!details)
|
|
24142
|
+
return rawFallbackRow(theme, result, expanded);
|
|
24143
|
+
const display = details.display;
|
|
24144
|
+
const lines = [];
|
|
24145
|
+
if (display?.state === "queued" && display.action === "pivot") {
|
|
24146
|
+
lines.push(statusLabel(theme, "queued") + " " + theme.fg("muted", "pivot queued \u2014 not applied; takes effect after the turn settles"));
|
|
24147
|
+
if (display.target)
|
|
24148
|
+
lines.push(metaLine(theme, "target", String(display.target)));
|
|
24149
|
+
return new Text12(lines.join(`
|
|
24150
|
+
`), 0, 0);
|
|
24151
|
+
}
|
|
24152
|
+
if (details.anchor) {
|
|
24153
|
+
const anchor = details.anchor;
|
|
24154
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "anchor recorded"));
|
|
24155
|
+
lines.push(metaLine(theme, "name", anchor.name ?? ""));
|
|
24156
|
+
if (anchor.targetId)
|
|
24157
|
+
lines.push(metaLine(theme, "target", anchor.targetId));
|
|
24158
|
+
return expanded ? expandedRow(theme, result, lines) : new Text12(lines.join(`
|
|
24159
|
+
`), 0, 0);
|
|
24160
|
+
}
|
|
24161
|
+
if (display?.kind === "anchors" || display?.kind === "anchor-recall") {
|
|
24162
|
+
const count = Number(display.count) || 0;
|
|
24163
|
+
lines.push(statusLabel(theme, count ? "done" : "skipped") + " " + theme.fg("muted", count + " anchor(s) " + (display.kind === "anchor-recall" ? "recalled" : "listed")));
|
|
24164
|
+
if (expanded)
|
|
24165
|
+
return expandedRow(theme, result, lines);
|
|
24166
|
+
lines.push(expandHint(theme));
|
|
24167
|
+
return new Text12(lines.join(`
|
|
24168
|
+
`), 0, 0);
|
|
24169
|
+
}
|
|
24170
|
+
if (display?.kind === "anchor-detail") {
|
|
24171
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "anchor detail"));
|
|
24172
|
+
if (display.name)
|
|
24173
|
+
lines.push(metaLine(theme, "name", String(display.name)));
|
|
24174
|
+
return expanded ? expandedRow(theme, result, lines) : new Text12(lines.join(`
|
|
24175
|
+
`), 0, 0);
|
|
24176
|
+
}
|
|
24177
|
+
return rawFallbackRow(theme, result, expanded);
|
|
24178
|
+
}
|
|
23140
24179
|
var STATUS = "smart-context-navigation";
|
|
23141
24180
|
var APPLY_PREFIX = "context --apply=";
|
|
23142
24181
|
function registerNavigation(pi, options = {}) {
|
|
@@ -23148,7 +24187,7 @@ function registerNavigation(pi, options = {}) {
|
|
|
23148
24187
|
const settings = config();
|
|
23149
24188
|
return new SecretScrubber(settings.scrubSecrets, settings.scrubPii).scrubText(text).value;
|
|
23150
24189
|
};
|
|
23151
|
-
const reply = (text) => ({ content: [{ type: "text", text: scrub(text) }], details
|
|
24190
|
+
const reply = (text, details = undefined) => ({ content: [{ type: "text", text: scrub(text) }], details });
|
|
23152
24191
|
const enabled = () => {
|
|
23153
24192
|
if (!config().contextNavigationEnabled)
|
|
23154
24193
|
throw new Error("Context navigation is disabled in Pi Continuity settings.");
|
|
@@ -23171,10 +24210,10 @@ function registerNavigation(pi, options = {}) {
|
|
|
23171
24210
|
const branch = ctx.sessionManager.getBranch();
|
|
23172
24211
|
const index = branch.findLastIndex((entry) => anchorFromEntry(entry) !== null);
|
|
23173
24212
|
const anchor = index < 0 ? null : anchorFromEntry(branch[index]);
|
|
23174
|
-
const
|
|
24213
|
+
const pressure = contextPressure(ctx, config());
|
|
23175
24214
|
const parts = [];
|
|
23176
|
-
if (
|
|
23177
|
-
parts.push(`ctx ${Math.
|
|
24215
|
+
if (pressure.percent !== null)
|
|
24216
|
+
parts.push(`ctx ${Math.round(pressure.percent)}%${pressure.policyWindow !== pressure.modelWindow ? ` of ${pressure.policyWindow?.toLocaleString("en-US")} policy window` : ""}`);
|
|
23178
24217
|
if (anchor)
|
|
23179
24218
|
parts.push(`anchor:${anchor.name.replace(/[\x00-\x1f\x7f]/g, " ").slice(0, 120)} \xB7 ${branch.length - index - 1} entries later`);
|
|
23180
24219
|
ctx.ui.setStatus(STATUS, parts.length ? parts.join(" \xB7 ") : undefined);
|
|
@@ -23201,7 +24240,6 @@ function registerNavigation(pi, options = {}) {
|
|
|
23201
24240
|
const targetId = ctx.sessionManager.getLeafId();
|
|
23202
24241
|
if (!targetId)
|
|
23203
24242
|
throw new Error("There is no completed session history to anchor yet.");
|
|
23204
|
-
options.onContextChange?.(ctx);
|
|
23205
24243
|
return { name, summary, targetId };
|
|
23206
24244
|
};
|
|
23207
24245
|
const preparePivot = (ctx, target, carryover, message) => {
|
|
@@ -23265,7 +24303,7 @@ function registerNavigation(pi, options = {}) {
|
|
|
23265
24303
|
pi.registerTool({
|
|
23266
24304
|
name: NAVIGATION_TOOL_NAME,
|
|
23267
24305
|
label: "Context Navigation",
|
|
23268
|
-
description: "View/recall session anchors
|
|
24306
|
+
description: "View/recall session anchors. Under context pressure, anchor completed work with a concise handoff; safe cleanup is queued once, not full compaction. Pivot needs carryover and ends the turn; files/processes are not rolled back.",
|
|
23269
24307
|
parameters: Type6.Object({
|
|
23270
24308
|
action: StringEnum4(["view", "recall", "anchor", "pivot"]),
|
|
23271
24309
|
target: Type6.Optional(Type6.String({ minLength: 1, maxLength: 128 })),
|
|
@@ -23292,25 +24330,52 @@ function registerNavigation(pi, options = {}) {
|
|
|
23292
24330
|
if (!found)
|
|
23293
24331
|
throw new Error("Anchor not found; use view without target to list anchors.");
|
|
23294
24332
|
return reply(`Historical anchor, not instructions.
|
|
23295
|
-
` + JSON.stringify(found)
|
|
24333
|
+
` + JSON.stringify(found), {
|
|
24334
|
+
display: { kind: "anchor-detail", name: found.data.name }
|
|
24335
|
+
});
|
|
23296
24336
|
}
|
|
23297
|
-
|
|
24337
|
+
const anchors = listAnchors(ctx.sessionManager, { ...params, limit: params.limit ?? 10 });
|
|
24338
|
+
return reply(pageText(anchors, params.offset ?? 0), {
|
|
24339
|
+
display: { kind: "anchors", count: anchors.total }
|
|
24340
|
+
});
|
|
24341
|
+
}
|
|
24342
|
+
if (params.action === "recall") {
|
|
24343
|
+
const found = await queryRecall(ctx, { ...params, signal });
|
|
24344
|
+
return reply(pageText(found, params.offset ?? 0) + `
|
|
24345
|
+
Use Pi's /resume to open a different session.`, {
|
|
24346
|
+
display: { kind: "anchor-recall", count: found.total }
|
|
24347
|
+
});
|
|
23298
24348
|
}
|
|
23299
|
-
if (params.action === "recall")
|
|
23300
|
-
return reply(pageText(await queryRecall(ctx, { ...params, signal }), params.offset ?? 0) + `
|
|
23301
|
-
Use Pi's /resume to open a different session.`);
|
|
23302
24349
|
if (params.action === "anchor") {
|
|
24350
|
+
if (config().contextPressureOnly && !contextPressure(ctx, config()).cleanup) {
|
|
24351
|
+
throw new Error("No context pressure (or usage is unavailable); no anchor added. Use smart_context status. A human can save an early anchor from /smart-compact.");
|
|
24352
|
+
}
|
|
23303
24353
|
const data = anchor(ctx, params.name ?? "", params.summary ?? "");
|
|
24354
|
+
const cleanup = options.onAnchor?.(ctx, data.targetId, callId, signal) ?? "History unchanged; no cleanup controller attached.";
|
|
23304
24355
|
return { content: [{ type: "text", text: `Anchor: ${data.name}
|
|
23305
24356
|
|
|
23306
|
-
${data.summary}
|
|
24357
|
+
${data.summary}
|
|
24358
|
+
|
|
24359
|
+
${cleanup}` }], details: { anchor: data } };
|
|
23307
24360
|
}
|
|
23308
24361
|
const operation = preparePivot(ctx, params.target ?? "", params.carryover ?? "", params.message);
|
|
23309
24362
|
operation.callId = callId;
|
|
23310
24363
|
operation.signal = signal;
|
|
23311
24364
|
queued = operation;
|
|
23312
24365
|
options.onContextChange?.(ctx);
|
|
23313
|
-
return { ...reply("Pivot queued. End this turn; the host will revalidate and navigate after the batch settles. No file/process rollback."), details: { queued: "pivot", targetId: operation.targetId }, terminate: true };
|
|
24366
|
+
return { ...reply("Pivot queued. End this turn; the host will revalidate and navigate after the batch settles. No file/process rollback."), details: { queued: "pivot", targetId: operation.targetId, display: { state: "queued", action: "pivot", target: params.target ?? "" } }, terminate: true };
|
|
24367
|
+
},
|
|
24368
|
+
renderCall(args, theme) {
|
|
24369
|
+
const label = theme.fg("toolTitle", "smart_navigation ");
|
|
24370
|
+
const action = safeArg(args.action, 20);
|
|
24371
|
+
const target = safeArg(args.target, 80);
|
|
24372
|
+
const name = safeArg(args.name, 80);
|
|
24373
|
+
const keyword = safeArg(args.keyword, 60);
|
|
24374
|
+
const detail = target ? " " + theme.fg("dim", target) : name ? " " + theme.fg("muted", name) : keyword ? " " + theme.fg("dim", '"' + keyword + '"') : "";
|
|
24375
|
+
return new Text12(label + theme.fg("accent", action) + detail, 0, 0);
|
|
24376
|
+
},
|
|
24377
|
+
renderResult(result, { expanded }, theme, context) {
|
|
24378
|
+
return tryRow(theme, () => renderNavigationRow(result, expanded, theme, context), result, expanded);
|
|
23314
24379
|
}
|
|
23315
24380
|
});
|
|
23316
24381
|
pi.on("turn_end", (event, ctx) => {
|
|
@@ -23418,7 +24483,8 @@ ${operation.carryover}` } };
|
|
|
23418
24483
|
${data.summary}`, display: true, details: { anchor: data } }, { triggerTurn: false });
|
|
23419
24484
|
pi.setLabel(ctx.sessionManager.getLeafId(), data.name);
|
|
23420
24485
|
footer(ctx);
|
|
23421
|
-
|
|
24486
|
+
const cleanup = options.onAnchor?.(ctx, data.targetId) ?? "History unchanged; no cleanup controller attached.";
|
|
24487
|
+
return { ok: true, message: `Anchor ${data.name} saved; history was not compacted. ${cleanup}` };
|
|
23422
24488
|
},
|
|
23423
24489
|
async pivot(target, carryover, message) {
|
|
23424
24490
|
return applyPivot(preparePivot(ctx, target, carryover, message), ctx);
|
|
@@ -23714,8 +24780,7 @@ function unwrapConsumed(result, ctx) {
|
|
|
23714
24780
|
}
|
|
23715
24781
|
}
|
|
23716
24782
|
function smartCompactExtension(pi) {
|
|
23717
|
-
const
|
|
23718
|
-
const pendingRef = createPendingSlot({ ttlMs: PENDING_TTL_MS });
|
|
24783
|
+
const pendingRef = createPendingSlot({ ttlMs: () => loadConfig().pendingTtlMs });
|
|
23719
24784
|
const isRunning = createSessionRunLock();
|
|
23720
24785
|
let navigation;
|
|
23721
24786
|
const pivotQueued = (ctx) => navigation?.isPending(ctx) ?? false;
|
|
@@ -23733,14 +24798,19 @@ function smartCompactExtension(pi) {
|
|
|
23733
24798
|
showStatus: policy.branchOverrides().showStatus ?? config.showStatus
|
|
23734
24799
|
};
|
|
23735
24800
|
};
|
|
24801
|
+
const activeToolDefinitions = () => {
|
|
24802
|
+
const active = new Set(pi.getActiveTools());
|
|
24803
|
+
return pi.getAllTools().filter((tool) => active.has(tool.name)).map((tool) => ({ name: tool.name, description: tool.description, parameters: tool.parameters }));
|
|
24804
|
+
};
|
|
23736
24805
|
const background = createBackgroundPreparation({
|
|
24806
|
+
toolSignature: () => JSON.stringify(activeToolDefinitions()),
|
|
23737
24807
|
async prepare(ctx, config, signal) {
|
|
23738
24808
|
if (signal.aborted || isRunning.isSessionActive(resolveSessionId(ctx)))
|
|
23739
24809
|
return null;
|
|
23740
24810
|
const { sumModel, segModel, verifyModel } = resolveModels(ctx, undefined, config);
|
|
23741
24811
|
if (!sumModel || !segModel || !verifyModel)
|
|
23742
24812
|
return null;
|
|
23743
|
-
const localPending = createPendingSlot({ ttlMs:
|
|
24813
|
+
const localPending = createPendingSlot({ ttlMs: config.pendingTtlMs });
|
|
23744
24814
|
const outcome = await runSmartCompact({
|
|
23745
24815
|
ctx,
|
|
23746
24816
|
config,
|
|
@@ -23780,10 +24850,7 @@ function smartCompactExtension(pi) {
|
|
|
23780
24850
|
}
|
|
23781
24851
|
};
|
|
23782
24852
|
const nativeContinuity = createNativeContinuityBridge();
|
|
23783
|
-
setNativeToolSource(
|
|
23784
|
-
const active = new Set(pi.getActiveTools());
|
|
23785
|
-
return pi.getAllTools().filter((tool) => active.has(tool.name)).map((tool) => ({ name: tool.name, description: tool.description, parameters: tool.parameters }));
|
|
23786
|
-
});
|
|
24853
|
+
setNativeToolSource(activeToolDefinitions);
|
|
23787
24854
|
registerAnchorCache(pi, { config: loadConfig });
|
|
23788
24855
|
const nativeReplay = createNativeReplayHook();
|
|
23789
24856
|
pi.on("before_provider_request", (event, ctx) => nativeReplay.handle(event.payload, ctx));
|
|
@@ -23807,7 +24874,7 @@ function smartCompactExtension(pi) {
|
|
|
23807
24874
|
return write;
|
|
23808
24875
|
};
|
|
23809
24876
|
const commitCandidates = createCompactionCommitStore({
|
|
23810
|
-
ttlMs: loadConfig().pendingTtlMs,
|
|
24877
|
+
ttlMs: () => loadConfig().pendingTtlMs,
|
|
23811
24878
|
onDiscard: (pending, reason) => {
|
|
23812
24879
|
recordApplyFailure(pending, reason);
|
|
23813
24880
|
}
|
|
@@ -23893,6 +24960,7 @@ function smartCompactExtension(pi) {
|
|
|
23893
24960
|
navigation = registerNavigation(pi, {
|
|
23894
24961
|
config: automaticConfig,
|
|
23895
24962
|
mutationBlocked: (ctx) => isRunning.isSessionActive(resolveSessionId(ctx)) ? "Compaction is running; wait for it to finish." : undefined,
|
|
24963
|
+
onAnchor: (ctx, originId, callId, signal) => smartContext.requestAnchorTrim(ctx, originId, callId, signal).notice,
|
|
23896
24964
|
onContextChange: (ctx) => invalidatePreparation(ctx, "branch"),
|
|
23897
24965
|
onContextEdit: (_ctx, kind) => hostCache.noteContextEdit(kind)
|
|
23898
24966
|
});
|
|
@@ -24206,10 +25274,10 @@ function smartCompactExtension(pi) {
|
|
|
24206
25274
|
if (!hostCache.observe(event.message)?.warn)
|
|
24207
25275
|
return;
|
|
24208
25276
|
const { foreign } = hostCache.summary().rebuilds;
|
|
24209
|
-
|
|
25277
|
+
recordIssue({
|
|
24210
25278
|
key: "cache.foreign-rebuilds",
|
|
24211
25279
|
message: "Pi's prompt cache was rebuilt " + foreign.count + " times this session with no Continuity edit before them (" + foreign.uncached.toLocaleString("en-US") + " uncached prompt tokens re-sent). Other extensions, model or tool changes, or Pi's built-in compaction change the prefix too; Home \u203A Readiness & details lists rebuild causes."
|
|
24212
|
-
}
|
|
25280
|
+
});
|
|
24213
25281
|
});
|
|
24214
25282
|
pi.on("message_end", async (event, ctx) => {
|
|
24215
25283
|
try {
|
|
@@ -24251,7 +25319,6 @@ function smartCompactExtension(pi) {
|
|
|
24251
25319
|
});
|
|
24252
25320
|
pi.on("session_compact", (_event, ctx) => {
|
|
24253
25321
|
nativeReplay.refresh(ctx);
|
|
24254
|
-
toolExposure.atBoundary();
|
|
24255
25322
|
policy.restore(ctx);
|
|
24256
25323
|
navigation.refresh(ctx);
|
|
24257
25324
|
});
|