pi-smart-compact 10.0.0 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +71 -29
- package/CHANGELOG.md +90 -4
- package/README.md +12 -9
- 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 +4 -3
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1342 -418
- 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 +12 -4
- package/dist/phases/verify.d.ts.map +1 -1
- package/dist/rtk.js +6 -5
- 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 +21 -0
- package/dist/utils/extraction.d.ts.map +1 -1
- package/dist/utils/state.d.ts.map +1 -1
- package/docs/RELEASE.md +1 -2
- package/docs/configuration.md +95 -32
- package/docs/evaluation.md +15 -3
- package/docs/guide.md +65 -58
- package/package.json +1 -1
- package/dist/app/extension-conflicts.d.ts +0 -15
- package/dist/app/extension-conflicts.d.ts.map +0 -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.0";
|
|
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" },
|
|
@@ -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: "
|
|
102
|
+
autoTriggerStrategy: "settled",
|
|
103
103
|
autoTriggerTimeoutMs: 120000,
|
|
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,45 @@ 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
|
+
}
|
|
3378
3406
|
function extractDecisions(msgs, _tcIdx) {
|
|
3379
3407
|
const tcIdx = _tcIdx ?? buildToolCallIndex(msgs);
|
|
3380
3408
|
const decisions = [];
|
|
@@ -3385,7 +3413,7 @@ function extractDecisions(msgs, _tcIdx) {
|
|
|
3385
3413
|
const question = typeof args === "string" ? args : args?.question ?? args?.prompt ?? "";
|
|
3386
3414
|
if (!question)
|
|
3387
3415
|
continue;
|
|
3388
|
-
for (let i = tc.msgIndex + 1;i <
|
|
3416
|
+
for (let i = tc.msgIndex + 1;i < msgs.length; i++) {
|
|
3389
3417
|
if (msgs[i]?.role === "toolResult" && msgs[i]?.toolCallId === id) {
|
|
3390
3418
|
decisions.push({
|
|
3391
3419
|
index: tc.msgIndex,
|
|
@@ -3447,11 +3475,12 @@ function isDiagnosticConstraintText(text) {
|
|
|
3447
3475
|
const candidate = text.replace(/^\s*[-*]\s+/, "").trim();
|
|
3448
3476
|
return /^(?:\[[^\]]+\]\s*)?(?:npm\s+(?:error|warn|notice|audit|verbose|info)\b|(?:rg|grep):|command exited\b)/i.test(candidate);
|
|
3449
3477
|
}
|
|
3450
|
-
var
|
|
3478
|
+
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;
|
|
3479
|
+
var OWN_OUTPUT_PHRASE_RE = /do not bypass verification/i;
|
|
3451
3480
|
var COMPLETED_CHECKLIST_RE = /^\[[xX\u2713\u2714]\]\s*\S/;
|
|
3452
3481
|
function isNonLiveConstraintText(text) {
|
|
3453
3482
|
const candidate = text.replace(/^\s*[-*]\s+/, "").trim();
|
|
3454
|
-
return isDiagnosticConstraintText(candidate) || isCompactionStatusText(candidate) ||
|
|
3483
|
+
return isDiagnosticConstraintText(candidate) || isCompactionStatusText(candidate) || OWN_OUTPUT_CONTEXT_RE.test(candidate) || COMPLETED_CHECKLIST_RE.test(candidate);
|
|
3455
3484
|
}
|
|
3456
3485
|
function mineConstraints(msgs) {
|
|
3457
3486
|
const constraints = [];
|
|
@@ -3462,10 +3491,13 @@ function mineConstraints(msgs) {
|
|
|
3462
3491
|
const text = extractText(msgs[i].content);
|
|
3463
3492
|
if (text.length < 10 || text.startsWith("/"))
|
|
3464
3493
|
continue;
|
|
3494
|
+
const ownOutputMessage = OWN_OUTPUT_CONTEXT_RE.test(text);
|
|
3465
3495
|
for (const raw of text.split(/\n+/)) {
|
|
3466
3496
|
const candidate = raw.replace(/^\s*[-*]\s+/, "").trim();
|
|
3467
3497
|
if (candidate.length < 10 || isNonLiveConstraintText(candidate))
|
|
3468
3498
|
continue;
|
|
3499
|
+
if (ownOutputMessage && OWN_OUTPUT_PHRASE_RE.test(candidate))
|
|
3500
|
+
continue;
|
|
3469
3501
|
for (const { re, cat, conf } of CONSTRAINT_PATTERNS) {
|
|
3470
3502
|
if (!re.test(candidate))
|
|
3471
3503
|
continue;
|
|
@@ -4726,22 +4758,22 @@ function reconcileCachedErrors(errors, deltaMessages, deltaToolCalls, baseMsgCou
|
|
|
4726
4758
|
const message = deltaMessages[j];
|
|
4727
4759
|
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
4728
4760
|
continue;
|
|
4729
|
-
const
|
|
4730
|
-
if (!
|
|
4761
|
+
const retries = message.content.flatMap(flattenToolCallBlock).filter((call) => toolOperationSignature(call.name, call.arguments) === error.operationSignature);
|
|
4762
|
+
if (!retries.length)
|
|
4731
4763
|
continue;
|
|
4732
4764
|
retryAttempted = true;
|
|
4733
|
-
for (
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
if (matches) {
|
|
4740
|
-
resolved = true;
|
|
4741
|
-
break;
|
|
4765
|
+
for (const retry of retries) {
|
|
4766
|
+
for (let k = j + 1;k < Math.min(deltaMessages.length, j + ERROR_RESOLVE_WINDOW); k++) {
|
|
4767
|
+
if (resolvesErrorResult(deltaMessages[k], retry, deltaToolCalls)) {
|
|
4768
|
+
resolved = true;
|
|
4769
|
+
break;
|
|
4770
|
+
}
|
|
4742
4771
|
}
|
|
4772
|
+
if (resolved)
|
|
4773
|
+
break;
|
|
4743
4774
|
}
|
|
4744
|
-
|
|
4775
|
+
if (resolved)
|
|
4776
|
+
break;
|
|
4745
4777
|
}
|
|
4746
4778
|
return { ...error, retryAttempted, resolved };
|
|
4747
4779
|
});
|
|
@@ -6579,7 +6611,7 @@ function formatVerificationGap(gap) {
|
|
|
6579
6611
|
case "missing-constraint":
|
|
6580
6612
|
return "Missing constraint: " + gap.text.slice(0, TRUNC.TOPIC_LABEL);
|
|
6581
6613
|
case "missing-decision":
|
|
6582
|
-
return "Missing decision: " + gap.summary.slice(0, TRUNC.TOPIC_LABEL);
|
|
6614
|
+
return "Missing decision: " + gap.summary.slice(0, TRUNC.TOPIC_LABEL) + (gap.answer ? " \u2192 " + gap.answer.slice(0, TRUNC.TOPIC_LABEL) : "");
|
|
6583
6615
|
case "missing-goal":
|
|
6584
6616
|
return "Main goal may be missing from summary";
|
|
6585
6617
|
case "fabricated-file":
|
|
@@ -6757,7 +6789,8 @@ function stemToken(token) {
|
|
|
6757
6789
|
return lower;
|
|
6758
6790
|
}
|
|
6759
6791
|
function semanticTokens(text) {
|
|
6760
|
-
|
|
6792
|
+
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, "");
|
|
6793
|
+
return (normalized.match(/[\p{L}\p{N}_-]+/gu) ?? []).map(stemToken).filter((token) => token.length > 2 || NEGATION_MARKERS.has(token));
|
|
6761
6794
|
}
|
|
6762
6795
|
var semanticShapeCache = new Map;
|
|
6763
6796
|
var semanticFragmentCache = new Map;
|
|
@@ -6808,7 +6841,10 @@ function hasSemanticEvidence(source, target) {
|
|
|
6808
6841
|
if (!concepts.length)
|
|
6809
6842
|
return true;
|
|
6810
6843
|
const required = Math.min(concepts.length, Math.max(1, Math.ceil(concepts.length * 0.6)));
|
|
6844
|
+
const verbatim = sourceTokens.join(" ");
|
|
6811
6845
|
return semanticFragments(target).some((tokens) => {
|
|
6846
|
+
if (tokens.join(" ") === verbatim)
|
|
6847
|
+
return true;
|
|
6812
6848
|
const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
|
|
6813
6849
|
if (overlap < required)
|
|
6814
6850
|
return false;
|
|
@@ -6848,15 +6884,114 @@ function hasSemanticContradiction(source, target) {
|
|
|
6848
6884
|
});
|
|
6849
6885
|
}
|
|
6850
6886
|
var TEMPORAL_CONSTRAINT_RE = /\b(?:yet|until|before|first|for now|hold off|wait|hen\u00FCz|\u015Fimdilik|\u00F6nce)\b/iu;
|
|
6887
|
+
var WH_QUESTION_LEAD_RE = /^(?:why|what|how|when|where|who|whom|which|whose|neden|ni\u00e7in|niye|nas\u0131l|hangi|kim)\b/iu;
|
|
6888
|
+
var AUX_QUESTION_LEAD_RE = /^(?:do|does|did|can|could|should|would|will|has|have|had|is|are|was|were|am)\b/i;
|
|
6889
|
+
var QUESTION_SUBJECT_RE = /^(?:you|u|i|we|it|they|he|she|this|that|these|those|there)\b/i;
|
|
6890
|
+
function looksLikeQuestion(text) {
|
|
6891
|
+
return text.split(/\r?\n/).some((line) => {
|
|
6892
|
+
const trimmed = line.trim();
|
|
6893
|
+
if (trimmed.endsWith("?"))
|
|
6894
|
+
return true;
|
|
6895
|
+
if (WH_QUESTION_LEAD_RE.test(trimmed))
|
|
6896
|
+
return true;
|
|
6897
|
+
const rest = trimmed.replace(AUX_QUESTION_LEAD_RE, "").trim();
|
|
6898
|
+
return AUX_QUESTION_LEAD_RE.test(trimmed) && QUESTION_SUBJECT_RE.test(rest);
|
|
6899
|
+
});
|
|
6900
|
+
}
|
|
6901
|
+
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;
|
|
6902
|
+
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;
|
|
6903
|
+
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;
|
|
6904
|
+
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;
|
|
6905
|
+
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;
|
|
6906
|
+
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;
|
|
6907
|
+
function addresseeModalGrant(anchor) {
|
|
6908
|
+
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");
|
|
6909
|
+
}
|
|
6910
|
+
var IMPERATIVE_LEAD_TOKENS = new Set([
|
|
6911
|
+
"do",
|
|
6912
|
+
"does",
|
|
6913
|
+
"did",
|
|
6914
|
+
"please",
|
|
6915
|
+
"now",
|
|
6916
|
+
"just",
|
|
6917
|
+
"go"
|
|
6918
|
+
]);
|
|
6919
|
+
var DIRECTIVE_COMPLEMENTS = new Set([
|
|
6920
|
+
"it",
|
|
6921
|
+
"them",
|
|
6922
|
+
"this",
|
|
6923
|
+
"that",
|
|
6924
|
+
"these",
|
|
6925
|
+
"those",
|
|
6926
|
+
"everything",
|
|
6927
|
+
"now",
|
|
6928
|
+
"again",
|
|
6929
|
+
"please",
|
|
6930
|
+
"immediately",
|
|
6931
|
+
"today",
|
|
6932
|
+
"tonight",
|
|
6933
|
+
"away",
|
|
6934
|
+
"ahead",
|
|
6935
|
+
"the",
|
|
6936
|
+
"a",
|
|
6937
|
+
"an",
|
|
6938
|
+
"our",
|
|
6939
|
+
"your",
|
|
6940
|
+
"my",
|
|
6941
|
+
"his",
|
|
6942
|
+
"her",
|
|
6943
|
+
"their"
|
|
6944
|
+
]);
|
|
6945
|
+
function anchorFollowedByComplement(clause, anchor) {
|
|
6946
|
+
const words = clause.split(/\s+/).map((word) => word.replace(/(^\W+|\W+$)/g, "").toLowerCase()).filter(Boolean);
|
|
6947
|
+
for (let i = 0;i < words.length; i++) {
|
|
6948
|
+
if (stemToken(words[i]) !== anchor)
|
|
6949
|
+
continue;
|
|
6950
|
+
const next = words[i + 1];
|
|
6951
|
+
return next !== undefined && DIRECTIVE_COMPLEMENTS.has(stemToken(next));
|
|
6952
|
+
}
|
|
6953
|
+
return false;
|
|
6954
|
+
}
|
|
6955
|
+
function positiveImperativeEvidence(release, anchor, allowImperative) {
|
|
6956
|
+
let rest = release.trim();
|
|
6957
|
+
for (let round = 0;round < 3 && ACK_STRIP_RE.test(rest); round++)
|
|
6958
|
+
rest = rest.replace(ACK_STRIP_RE, "").trim();
|
|
6959
|
+
const modalGrant = addresseeModalGrant(anchor);
|
|
6960
|
+
const clauses = rest.split(/\r?\n|[.;]/).flatMap((fragment) => fragment.split(/\s(?:[\u2014\u2013]|-)\s|,\s|;\s|:\s/)).map((clause) => clause.trim()).filter(Boolean);
|
|
6961
|
+
return clauses.some((clause) => {
|
|
6962
|
+
const tokens = semanticTokens(clause);
|
|
6963
|
+
if (!tokens.includes(anchor) || hasEffectiveTargetNegation(tokens, anchor))
|
|
6964
|
+
return false;
|
|
6965
|
+
if (allowImperative && IMPERATIVE_LEAD_TOKENS.has(tokens[0] ?? "") && tokens[1] === anchor)
|
|
6966
|
+
return true;
|
|
6967
|
+
if (POLICY_GRANT_PREDICATE_RE.test(clause))
|
|
6968
|
+
return true;
|
|
6969
|
+
if (modalGrant.test(clause))
|
|
6970
|
+
return true;
|
|
6971
|
+
if (allowImperative && tokens[0] === anchor && anchorFollowedByComplement(clause, anchor))
|
|
6972
|
+
return true;
|
|
6973
|
+
return false;
|
|
6974
|
+
});
|
|
6975
|
+
}
|
|
6851
6976
|
function releasesConstraint(constraint, release) {
|
|
6852
|
-
if (
|
|
6853
|
-
return
|
|
6854
|
-
if (
|
|
6977
|
+
if (looksLikeQuestion(release))
|
|
6978
|
+
return false;
|
|
6979
|
+
if (CRITICISM_RE.test(release))
|
|
6980
|
+
return false;
|
|
6981
|
+
if (QUOTATION_RE.test(release))
|
|
6982
|
+
return false;
|
|
6983
|
+
if (DENIAL_RE.test(release))
|
|
6855
6984
|
return false;
|
|
6856
6985
|
const { anchor, negative } = semanticShape(constraint);
|
|
6986
|
+
const deferral = TEMPORAL_CONSTRAINT_RE.test(constraint);
|
|
6987
|
+
if (hasSemanticContradiction(constraint, release)) {
|
|
6988
|
+
return FIRST_PERSON_GRANT_RE.test(release) || Boolean(anchor) && positiveImperativeEvidence(release, anchor, deferral);
|
|
6989
|
+
}
|
|
6990
|
+
if (!deferral)
|
|
6991
|
+
return false;
|
|
6857
6992
|
if (!anchor || !negative)
|
|
6858
6993
|
return false;
|
|
6859
|
-
return
|
|
6994
|
+
return positiveImperativeEvidence(release, anchor, true);
|
|
6860
6995
|
}
|
|
6861
6996
|
function isDeterministicallyPatchable(gap) {
|
|
6862
6997
|
if (gap.kind === "fabricated-file")
|
|
@@ -6887,6 +7022,105 @@ function repairSummaryDeterministically(summary, result, extraction, continuity
|
|
|
6887
7022
|
}
|
|
6888
7023
|
return { summary, result, patched };
|
|
6889
7024
|
}
|
|
7025
|
+
function questionOverlapOnLine(question, line) {
|
|
7026
|
+
const { concepts } = semanticShape(question);
|
|
7027
|
+
if (!concepts.length)
|
|
7028
|
+
return 0;
|
|
7029
|
+
const required = Math.min(concepts.length, Math.max(1, Math.ceil(concepts.length * 0.6)));
|
|
7030
|
+
const tokens = semanticTokens(line);
|
|
7031
|
+
const overlap = concepts.filter((concept) => tokens.includes(concept)).length;
|
|
7032
|
+
return overlap >= required ? overlap : 0;
|
|
7033
|
+
}
|
|
7034
|
+
function exactQuestionOwner(head, questions) {
|
|
7035
|
+
const headKey = normalizeFactKey(head);
|
|
7036
|
+
if (!headKey)
|
|
7037
|
+
return null;
|
|
7038
|
+
let owner = null;
|
|
7039
|
+
let ownerLength = -1;
|
|
7040
|
+
for (const question of questions) {
|
|
7041
|
+
const key = normalizeFactKey(question);
|
|
7042
|
+
if (key && headKey.includes(key) && key.length > ownerLength) {
|
|
7043
|
+
owner = question;
|
|
7044
|
+
ownerLength = key.length;
|
|
7045
|
+
}
|
|
7046
|
+
}
|
|
7047
|
+
return owner;
|
|
7048
|
+
}
|
|
7049
|
+
function lineBelongsToQuestion(question, head, questions) {
|
|
7050
|
+
if (exactQuestionOwner(head, questions) === question)
|
|
7051
|
+
return true;
|
|
7052
|
+
const own = questionOverlapOnLine(question, head);
|
|
7053
|
+
if (!own)
|
|
7054
|
+
return false;
|
|
7055
|
+
return questions.every((other) => other === question || questionOverlapOnLine(other, head) < own);
|
|
7056
|
+
}
|
|
7057
|
+
function containsAnswerText(haystack, answer) {
|
|
7058
|
+
const needle = answer.trim().toLowerCase();
|
|
7059
|
+
if (!needle)
|
|
7060
|
+
return false;
|
|
7061
|
+
const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7062
|
+
return new RegExp("(?<![\\p{L}\\p{N}])" + escaped + "(?![\\p{L}\\p{N}])", "iu").test(haystack);
|
|
7063
|
+
}
|
|
7064
|
+
function lineCarriesAnswer(haystack, answer) {
|
|
7065
|
+
if (containsAnswerText(haystack, answer))
|
|
7066
|
+
return true;
|
|
7067
|
+
const { concepts } = semanticShape(answer);
|
|
7068
|
+
return concepts.length > 0 && hasSemanticEvidence(answer, haystack);
|
|
7069
|
+
}
|
|
7070
|
+
function decisionLineSlots(line, question, questions) {
|
|
7071
|
+
let fuzzy = null;
|
|
7072
|
+
for (let index = line.indexOf("\u2192");index >= 0; index = line.indexOf("\u2192", index + 1)) {
|
|
7073
|
+
const candidateHead = line.slice(0, index).replace(/\*+/g, "").trim();
|
|
7074
|
+
const tail = line.slice(index + 1).replace(/\*+/g, "").trim();
|
|
7075
|
+
if (!candidateHead || !tail)
|
|
7076
|
+
continue;
|
|
7077
|
+
if (exactQuestionOwner(candidateHead, questions) === question) {
|
|
7078
|
+
return { head: candidateHead, tail };
|
|
7079
|
+
}
|
|
7080
|
+
if (!fuzzy && lineBelongsToQuestion(question, candidateHead, questions)) {
|
|
7081
|
+
fuzzy = { head: candidateHead, tail };
|
|
7082
|
+
}
|
|
7083
|
+
}
|
|
7084
|
+
return fuzzy;
|
|
7085
|
+
}
|
|
7086
|
+
function stripDecisionAnswerTails(decisionBody, questions) {
|
|
7087
|
+
return decisionBody.split(/\r?\n/).map((line) => {
|
|
7088
|
+
let fuzzyCut = -1;
|
|
7089
|
+
for (let index = line.indexOf("\u2192");index >= 0; index = line.indexOf("\u2192", index + 1)) {
|
|
7090
|
+
const candidateHead = line.slice(0, index).replace(/\*+/g, "").trim();
|
|
7091
|
+
if (!candidateHead)
|
|
7092
|
+
continue;
|
|
7093
|
+
if (exactQuestionOwner(candidateHead, questions) !== null) {
|
|
7094
|
+
return line.slice(0, index);
|
|
7095
|
+
}
|
|
7096
|
+
if (fuzzyCut < 0 && questions.some((question) => lineBelongsToQuestion(question, candidateHead, questions))) {
|
|
7097
|
+
fuzzyCut = index;
|
|
7098
|
+
}
|
|
7099
|
+
}
|
|
7100
|
+
return fuzzyCut >= 0 ? line.slice(0, fuzzyCut) : line;
|
|
7101
|
+
}).join(`
|
|
7102
|
+
`);
|
|
7103
|
+
}
|
|
7104
|
+
function decisionAnswerPresent(question, answer, decisionBody, questions) {
|
|
7105
|
+
return decisionBody.split(/\r?\n/).some((rawLine) => {
|
|
7106
|
+
const line = rawLine.replace(/^\s*(?:[-*+]\s+)?/, "").trim();
|
|
7107
|
+
return decisionLineSlots(line, question, questions) !== null && lineCarriesAnswer(line, answer);
|
|
7108
|
+
});
|
|
7109
|
+
}
|
|
7110
|
+
function decisionAnswerConflict(question, answer, decisionBody, questions) {
|
|
7111
|
+
for (const rawLine of decisionBody.split(/\r?\n/)) {
|
|
7112
|
+
const line = rawLine.replace(/^\s*(?:[-*+]\s+)?/, "").trim();
|
|
7113
|
+
const slots = decisionLineSlots(line, question, questions);
|
|
7114
|
+
if (!slots || !slots.tail)
|
|
7115
|
+
continue;
|
|
7116
|
+
if (hasSemanticEvidence(question, slots.tail))
|
|
7117
|
+
continue;
|
|
7118
|
+
if (lineCarriesAnswer(slots.tail, answer))
|
|
7119
|
+
continue;
|
|
7120
|
+
return true;
|
|
7121
|
+
}
|
|
7122
|
+
return false;
|
|
7123
|
+
}
|
|
6890
7124
|
function addGap(accumulator, gap, penalty) {
|
|
6891
7125
|
accumulator.gaps.push(gap);
|
|
6892
7126
|
accumulator.score -= penalty;
|
|
@@ -6929,10 +7163,14 @@ function collectVerificationEvidence(extraction, continuity, evidence) {
|
|
|
6929
7163
|
...(continuity?.constraints ?? []).flatMap((item) => item.confidence >= 0.8 ? [{ text: item.text }] : []),
|
|
6930
7164
|
...steeringConstraints
|
|
6931
7165
|
], (item) => item.text).filter((item) => !isNonLiveConstraintText(item.text) && !retired.has(normalizeFactKey(item.text)));
|
|
6932
|
-
const
|
|
6933
|
-
...
|
|
6934
|
-
...
|
|
6935
|
-
]
|
|
7166
|
+
const decisionEntries = [
|
|
7167
|
+
...continuity?.decisions ?? [],
|
|
7168
|
+
...extraction.decisions
|
|
7169
|
+
].flatMap((item) => item.type === "explicit" ? [{ summary: item.summary, userResponse: item.userResponse }] : []);
|
|
7170
|
+
const decisions = collapseDecisionsByQuestion(decisionEntries).map((item) => ({
|
|
7171
|
+
summary: item.summary,
|
|
7172
|
+
answer: item.userResponse?.trim() || null
|
|
7173
|
+
}));
|
|
6936
7174
|
return {
|
|
6937
7175
|
unresolved,
|
|
6938
7176
|
resolved,
|
|
@@ -7042,11 +7280,25 @@ function verifySemanticCoverage(parsed, collected, accumulator) {
|
|
|
7042
7280
|
}
|
|
7043
7281
|
}
|
|
7044
7282
|
const decisionBody = findSection(parsed, "decisions")?.body ?? "";
|
|
7283
|
+
const questions = collected.decisions.map((decision) => decision.summary);
|
|
7284
|
+
const decisionQuestionBody = stripDecisionAnswerTails(decisionBody, questions);
|
|
7045
7285
|
for (const decision of collected.decisions) {
|
|
7046
|
-
|
|
7047
|
-
|
|
7286
|
+
const questionVerified = hasSemanticEvidence(decision.summary, decisionQuestionBody);
|
|
7287
|
+
const answerVerified = !decision.answer || decisionAnswerPresent(decision.summary, decision.answer, decisionBody, questions);
|
|
7288
|
+
if (!questionVerified || !answerVerified) {
|
|
7289
|
+
addGap(accumulator, {
|
|
7290
|
+
kind: "missing-decision",
|
|
7291
|
+
summary: decision.summary,
|
|
7292
|
+
...decision.answer ? { answer: decision.answer } : {}
|
|
7293
|
+
}, 8);
|
|
7048
7294
|
}
|
|
7049
|
-
if (
|
|
7295
|
+
if (decision.answer && decisionAnswerConflict(decision.summary, decision.answer, decisionBody, questions)) {
|
|
7296
|
+
addGap(accumulator, {
|
|
7297
|
+
kind: "inconsistency",
|
|
7298
|
+
detail: "decision-answer-conflict: summary carries a different active answer for " + decision.summary.slice(0, TRUNC.SNIPPET)
|
|
7299
|
+
}, 20);
|
|
7300
|
+
}
|
|
7301
|
+
if (hasSemanticContradiction(decision.summary, decisionQuestionBody)) {
|
|
7050
7302
|
addGap(accumulator, {
|
|
7051
7303
|
kind: "inconsistency",
|
|
7052
7304
|
detail: "semantic-contradiction: decision contradicts " + decision.summary.slice(0, TRUNC.SNIPPET)
|
|
@@ -7059,7 +7311,7 @@ function verifyFileReferences(summary, extraction, continuity, evidence, collect
|
|
|
7059
7311
|
...collected.unresolved.map((item) => item.message),
|
|
7060
7312
|
...collected.resolved.map((item) => item.message),
|
|
7061
7313
|
...collected.constraints.map((item) => item.text),
|
|
7062
|
-
...collected.decisions.map((item) => item.summary),
|
|
7314
|
+
...collected.decisions.map((item) => item.answer ? item.summary + " \u2192 " + item.answer : item.summary),
|
|
7063
7315
|
...collected.goal ? [collected.goal] : [],
|
|
7064
7316
|
...extraction.lastUserMessages,
|
|
7065
7317
|
...extraction.timeline.map((item) => item.summary),
|
|
@@ -7248,9 +7500,12 @@ function patchDeterministic(summary, gaps, extraction, continuity = null, eviden
|
|
|
7248
7500
|
case "missing-constraint":
|
|
7249
7501
|
canonical = appendToSection(canonical, "constraints", "- " + safe(gap.text, TRUNC.CONSTRAINT_TEXT));
|
|
7250
7502
|
break;
|
|
7251
|
-
case "missing-decision":
|
|
7252
|
-
|
|
7503
|
+
case "missing-decision": {
|
|
7504
|
+
const question = safe(gap.summary, TRUNC.DECISION_SUMMARY);
|
|
7505
|
+
const answer = gap.answer ? safe(gap.answer, TRUNC.USER_RESPONSE) : "";
|
|
7506
|
+
canonical = appendToSection(canonical, "decisions", "- **" + question + "**" + (answer ? " \u2192 " + answer : ""));
|
|
7253
7507
|
break;
|
|
7508
|
+
}
|
|
7254
7509
|
case "missing-goal":
|
|
7255
7510
|
canonical = upsertSection(canonical, "goal", safe(gap.goal, TRUNC.DETAIL) || "Continue the current task.");
|
|
7256
7511
|
break;
|
|
@@ -7309,9 +7564,12 @@ function hasUnclosedMarkdownFence(markdown) {
|
|
|
7309
7564
|
}
|
|
7310
7565
|
return open !== null;
|
|
7311
7566
|
}
|
|
7567
|
+
var TERMINAL_STOP_REASON_RE = /^(?:stop|endturn)$/i;
|
|
7312
7568
|
function patchResponseIsTruncated(patched, stopReason) {
|
|
7313
|
-
const reason = String(stopReason ?? "");
|
|
7314
|
-
|
|
7569
|
+
const reason = String(stopReason ?? "").trim();
|
|
7570
|
+
if (reason && !TERMINAL_STOP_REASON_RE.test(reason))
|
|
7571
|
+
return true;
|
|
7572
|
+
return /\u2026\u2702\d+\s*$/.test(patched) || hasUnclosedMarkdownFence(patched);
|
|
7315
7573
|
}
|
|
7316
7574
|
function sectionIdentity(section) {
|
|
7317
7575
|
return section.kind === "unknown" ? "unknown:" + section.heading.trim().toLowerCase() : section.kind;
|
|
@@ -7651,7 +7909,11 @@ function mergeOpenLoops(current, previous) {
|
|
|
7651
7909
|
function mergeCompactionStates(previous, current) {
|
|
7652
7910
|
if (!previous) {
|
|
7653
7911
|
const active = applyContinuityOverrides(current, current.factOverrides ?? []);
|
|
7654
|
-
return {
|
|
7912
|
+
return {
|
|
7913
|
+
...active,
|
|
7914
|
+
decisions: collapseDecisionsByQuestion(active.decisions).map((item, index) => ({ ...item, id: ID_PREFIX.DECISION + (index + 1) })),
|
|
7915
|
+
openLoops: mergeOpenLoops(active.openLoops, [])
|
|
7916
|
+
};
|
|
7655
7917
|
}
|
|
7656
7918
|
const factOverrides = mergeBy(current.factOverrides ?? [], previous.factOverrides ?? [], (item) => item.kind + ":" + item.summaryKey, 50);
|
|
7657
7919
|
const activeCurrent = applyContinuityOverrides(current, factOverrides);
|
|
@@ -7671,7 +7933,17 @@ function mergeCompactionStates(previous, current) {
|
|
|
7671
7933
|
}
|
|
7672
7934
|
return false;
|
|
7673
7935
|
};
|
|
7674
|
-
const
|
|
7936
|
+
const collapsedCurrent = collapseDecisionsByQuestion(activeCurrent.decisions);
|
|
7937
|
+
const collapsedPrevious = collapseDecisionsByQuestion(activePrevious.decisions);
|
|
7938
|
+
const authoritative = collapseDecisionsByQuestion([
|
|
7939
|
+
...collapsedPrevious,
|
|
7940
|
+
...collapsedCurrent
|
|
7941
|
+
]);
|
|
7942
|
+
const currentKeys = new Set(collapsedCurrent.map((item) => normalizeFactKey(item.summary)));
|
|
7943
|
+
const decisions = [
|
|
7944
|
+
...authoritative.filter((item) => currentKeys.has(normalizeFactKey(item.summary))),
|
|
7945
|
+
...authoritative.filter((item) => !currentKeys.has(normalizeFactKey(item.summary)))
|
|
7946
|
+
].slice(0, 30).map((item, index) => ({ ...item, id: ID_PREFIX.DECISION + (index + 1) }));
|
|
7675
7947
|
const constraints = mergeBy(activeCurrent.constraints, activePrevious.constraints, (item) => normalizeFactKey(item.text), 30).map((item, index) => ({ ...item, id: "constraint-" + (index + 1) }));
|
|
7676
7948
|
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
7949
|
const openLoops = mergeOpenLoops(activeCurrent.openLoops, activePrevious.openLoops.map((loop) => isResolvedLoop(loop) ? { ...loop, status: "resolved" } : loop));
|
|
@@ -7703,8 +7975,8 @@ function mergeCompactionStates(previous, current) {
|
|
|
7703
7975
|
function renderContinuityCapsule(state, maxChars = TRUNC.CONTINUITY_CAPSULE, existing = "") {
|
|
7704
7976
|
const haystack = normalizeFactKey(existing);
|
|
7705
7977
|
const lines = ["## Continuity Ledger"];
|
|
7706
|
-
const add = (label, value) => {
|
|
7707
|
-
const text = summaryEvidenceLine(value,
|
|
7978
|
+
const add = (label, value, maxLength = TRUNC.MESSAGE) => {
|
|
7979
|
+
const text = summaryEvidenceLine(value, maxLength);
|
|
7708
7980
|
if (!text || haystack.includes(normalizeFactKey(text)))
|
|
7709
7981
|
return;
|
|
7710
7982
|
const line = "- " + label + ": " + text;
|
|
@@ -7716,8 +7988,13 @@ function renderContinuityCapsule(state, maxChars = TRUNC.CONTINUITY_CAPSULE, exi
|
|
|
7716
7988
|
add("Goal", state.goal);
|
|
7717
7989
|
for (const item of state.constraints)
|
|
7718
7990
|
add("Constraint", item.text);
|
|
7719
|
-
for (const item of state.decisions)
|
|
7720
|
-
|
|
7991
|
+
for (const item of state.decisions) {
|
|
7992
|
+
const question = summaryEvidenceLine(item.summary, TRUNC.DECISION_SUMMARY);
|
|
7993
|
+
const answer = item.userResponse ? summaryEvidenceLine(item.userResponse, TRUNC.USER_RESPONSE) : "";
|
|
7994
|
+
const decisionLine = question + (answer ? " \u2192 " + answer : "");
|
|
7995
|
+
if (decisionLine)
|
|
7996
|
+
add("Decision", decisionLine, decisionLine.length);
|
|
7997
|
+
}
|
|
7721
7998
|
for (const item of state.unresolvedErrors)
|
|
7722
7999
|
add("Unresolved error", item.message);
|
|
7723
8000
|
for (const item of state.resolvedErrors.slice(-5))
|
|
@@ -8258,7 +8535,16 @@ function pendingMatchesBranch(pending, branch) {
|
|
|
8258
8535
|
}
|
|
8259
8536
|
function readerSignature(ctx) {
|
|
8260
8537
|
const model = ctx.model;
|
|
8261
|
-
|
|
8538
|
+
const prompt = ctx.getSystemPrompt?.();
|
|
8539
|
+
return model && JSON.stringify([
|
|
8540
|
+
model.provider,
|
|
8541
|
+
model.id,
|
|
8542
|
+
model.api,
|
|
8543
|
+
model.baseUrl,
|
|
8544
|
+
model.contextWindow,
|
|
8545
|
+
model.maxTokens,
|
|
8546
|
+
prompt === undefined ? null : createHash4("sha256").update(prompt).digest("hex")
|
|
8547
|
+
]);
|
|
8262
8548
|
}
|
|
8263
8549
|
function revalidatePending(pending, ctx, reserveTokens = 0) {
|
|
8264
8550
|
const model = ctx.model;
|
|
@@ -8298,7 +8584,7 @@ function revalidatePending(pending, ctx, reserveTokens = 0) {
|
|
|
8298
8584
|
};
|
|
8299
8585
|
}
|
|
8300
8586
|
function createPendingSlot(opts) {
|
|
8301
|
-
const ttlMs = opts.ttlMs;
|
|
8587
|
+
const ttlMs = () => typeof opts.ttlMs === "function" ? opts.ttlMs() : opts.ttlMs;
|
|
8302
8588
|
const now = opts.now ?? Date.now;
|
|
8303
8589
|
const maxEntries = Math.max(1, opts.maxEntries ?? 64);
|
|
8304
8590
|
const entries = new Map;
|
|
@@ -8318,7 +8604,7 @@ function createPendingSlot(opts) {
|
|
|
8318
8604
|
const current = now();
|
|
8319
8605
|
let removedNewest = false;
|
|
8320
8606
|
for (const [sessionId, entry] of entries) {
|
|
8321
|
-
if (current - entry.createdAt <= ttlMs)
|
|
8607
|
+
if (current - entry.createdAt <= ttlMs())
|
|
8322
8608
|
continue;
|
|
8323
8609
|
entries.delete(sessionId);
|
|
8324
8610
|
if (newestSessionId === sessionId)
|
|
@@ -8345,7 +8631,7 @@ function createPendingSlot(opts) {
|
|
|
8345
8631
|
const entry = entries.get(currentSessionId);
|
|
8346
8632
|
if (entry) {
|
|
8347
8633
|
const ageMs = now() - entry.createdAt;
|
|
8348
|
-
if (ageMs > ttlMs) {
|
|
8634
|
+
if (ageMs > ttlMs()) {
|
|
8349
8635
|
deleteEntry(currentSessionId);
|
|
8350
8636
|
prune();
|
|
8351
8637
|
return { kind: "expired", ageMs };
|
|
@@ -10758,6 +11044,7 @@ function buildExplorerRequest(extraction, model, prevSummary, userNote) {
|
|
|
10758
11044
|
function explorationToolSupportKey(model) {
|
|
10759
11045
|
return [model.provider, model.api, model.baseUrl ?? "", model.id].join("\x00");
|
|
10760
11046
|
}
|
|
11047
|
+
var SYNTHESIS_CALL_RESERVE = 2;
|
|
10761
11048
|
async function exploreConversation(llmMessages, extraction, model, auth, prevSummary, userNote, signal, maxRounds = MAX_EXPLORATION_ROUNDS, notify, services) {
|
|
10762
11049
|
const svc = services ?? getDefaultServices();
|
|
10763
11050
|
const request = buildExplorerRequest(extraction, model, prevSummary, userNote);
|
|
@@ -10768,6 +11055,10 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
10768
11055
|
const now = svc.clock.now();
|
|
10769
11056
|
const cachedSupport = toolSupport.get(cacheKey, now);
|
|
10770
11057
|
let supportsTools = cachedSupport === true;
|
|
11058
|
+
if (svc.budget.remainingCalls() <= SYNTHESIS_CALL_RESERVE) {
|
|
11059
|
+
notify?.("Explore skipped \xB7 remaining calls reserved for synthesis", "info");
|
|
11060
|
+
return { report: fallbackExplorationReport(llmMessages), rounds: 0, toolSupported: supportsTools };
|
|
11061
|
+
}
|
|
10771
11062
|
try {
|
|
10772
11063
|
if (cachedSupport === false) {
|
|
10773
11064
|
if (notify)
|
|
@@ -10810,7 +11101,7 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
10810
11101
|
});
|
|
10811
11102
|
}
|
|
10812
11103
|
let rounds = 1;
|
|
10813
|
-
while (rounds < maxRounds) {
|
|
11104
|
+
while (rounds < maxRounds && svc.budget.remainingCalls() > SYNTHESIS_CALL_RESERVE) {
|
|
10814
11105
|
rounds++;
|
|
10815
11106
|
let response;
|
|
10816
11107
|
try {
|
|
@@ -10897,6 +11188,9 @@ async function exploreConversation(llmMessages, extraction, model, auth, prevSum
|
|
|
10897
11188
|
return { report, rounds: 1, toolSupported: supportsTools };
|
|
10898
11189
|
}
|
|
10899
11190
|
async function explorationRetry(model, auth, llmMessages, extraction, userNote, signal, services) {
|
|
11191
|
+
const svc = services ?? getDefaultServices();
|
|
11192
|
+
if (svc.budget.remainingCalls() <= SYNTHESIS_CALL_RESERVE)
|
|
11193
|
+
return fallbackExplorationReport(llmMessages);
|
|
10900
11194
|
const last5 = llmMessages.slice(-5).map((m) => "[" + m?.role + "] " + extractText(m?.content).slice(0, TRUNC.PREVIEW)).join(`
|
|
10901
11195
|
`);
|
|
10902
11196
|
const retryPrompt = `IMPORTANT: Output ONLY valid raw JSON. No markdown. No explanation. No code fences. Just the JSON object.
|
|
@@ -10925,7 +11219,7 @@ User steering: ` + userNote : "");
|
|
|
10925
11219
|
headers: auth.headers,
|
|
10926
11220
|
maxTokens: Math.min(4096, getProviderCaps(model.provider).maxOutputTokens),
|
|
10927
11221
|
signal
|
|
10928
|
-
},
|
|
11222
|
+
}, svc);
|
|
10929
11223
|
const text = resp.content.filter((c) => c.type === "text").map((c) => c.text).join("").trim();
|
|
10930
11224
|
return parseExplorationReport(text, llmMessages);
|
|
10931
11225
|
} catch (e) {
|
|
@@ -10965,6 +11259,9 @@ Output ONLY JSON: {"mainGoal":"...","sessionType":"implementation|review|debuggi
|
|
|
10965
11259
|
};
|
|
10966
11260
|
}
|
|
10967
11261
|
async function directExploration(llmMessages, extraction, model, auth, prevSummary, userNote, signal, services) {
|
|
11262
|
+
const svc = services ?? getDefaultServices();
|
|
11263
|
+
if (svc.budget.remainingCalls() <= SYNTHESIS_CALL_RESERVE)
|
|
11264
|
+
return fallbackExplorationReport(llmMessages);
|
|
10968
11265
|
const request = buildDirectExplorationRequest(llmMessages, extraction, model, prevSummary, userNote);
|
|
10969
11266
|
try {
|
|
10970
11267
|
const resp = await trackedComplete("explore-direct", model, request.context, {
|
|
@@ -10972,7 +11269,7 @@ async function directExploration(llmMessages, extraction, model, auth, prevSumma
|
|
|
10972
11269
|
headers: auth.headers,
|
|
10973
11270
|
maxTokens: request.maxTokens,
|
|
10974
11271
|
signal
|
|
10975
|
-
},
|
|
11272
|
+
}, svc);
|
|
10976
11273
|
const text = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
10977
11274
|
`).trim();
|
|
10978
11275
|
return parseExplorationReport(text, llmMessages);
|
|
@@ -11452,6 +11749,8 @@ async function singlePassCompact(request, model, auth, signal, services) {
|
|
|
11452
11749
|
`).trim();
|
|
11453
11750
|
if (!summary.startsWith("##"))
|
|
11454
11751
|
throw new Error("Single-pass malformed output");
|
|
11752
|
+
if (patchResponseIsTruncated(summary, resp.stopReason))
|
|
11753
|
+
throw new Error("Single-pass truncated output");
|
|
11455
11754
|
return { summary, llmCalls: 1 };
|
|
11456
11755
|
}
|
|
11457
11756
|
function buildBatchRequest(batch, extraction, model, maxOutputTokens) {
|
|
@@ -11525,7 +11824,7 @@ async function summarizeBatch(batch, extraction, model, auth, signal, services,
|
|
|
11525
11824
|
try {
|
|
11526
11825
|
sectionMap = await attempt();
|
|
11527
11826
|
} catch (err) {
|
|
11528
|
-
if (err instanceof BatchSummaryFormatError && /non-terminal stop reason length/.test(err.message) && services?.thinkingLevels.summaryThinkingLevel !== "minimal") {
|
|
11827
|
+
if (err instanceof BatchSummaryFormatError && /non-terminal stop reason length/.test(err.message) && services?.thinkingLevels.summaryThinkingLevel !== "minimal" && (services ?? getDefaultServices()).budget.remainingCalls() > 1) {
|
|
11529
11828
|
debug("Batch synthesis exhausted the output budget (reasoning shared it); retrying once with minimal reasoning");
|
|
11530
11829
|
sectionMap = await attempt("minimal");
|
|
11531
11830
|
} else {
|
|
@@ -11604,8 +11903,11 @@ async function assembleLLM(summaries, extraction, report, model, auth, budget, p
|
|
|
11604
11903
|
maxTokens: Math.min(budget, getProviderCaps(model.provider).maxOutputTokens),
|
|
11605
11904
|
signal
|
|
11606
11905
|
}, services);
|
|
11607
|
-
|
|
11906
|
+
const assembled = resp.content.filter((c) => c.type === "text").map((c) => c.text).join(`
|
|
11608
11907
|
`).trim();
|
|
11908
|
+
if (patchResponseIsTruncated(assembled, resp.stopReason))
|
|
11909
|
+
throw new Error("Assembly truncated output");
|
|
11910
|
+
return assembled;
|
|
11609
11911
|
}
|
|
11610
11912
|
function assembleFallback(summaries, extraction, steering = {}, budgetTokens = 6000, continuity = null) {
|
|
11611
11913
|
const safe = (value, max = TRUNC.PREVIEW_MID) => summaryEvidenceLine(value, max);
|
|
@@ -11620,7 +11922,7 @@ function assembleFallback(summaries, extraction, steering = {}, budgetTokens = 6
|
|
|
11620
11922
|
constraints.push("- [focus] Preserve detail about: " + safe(steering.focus, TRUNC.CONSTRAINT_TEXT));
|
|
11621
11923
|
if (steering.note?.trim())
|
|
11622
11924
|
constraints.push("- [note] " + safe(steering.note, TRUNC.CONSTRAINT_TEXT));
|
|
11623
|
-
const decisions = extraction.decisions.map((item) => {
|
|
11925
|
+
const decisions = collapseDecisionsByQuestion(extraction.decisions).map((item) => {
|
|
11624
11926
|
const summary = safe(item.summary, TRUNC.DECISION_SUMMARY);
|
|
11625
11927
|
const response = item.userResponse ? safe(item.userResponse, TRUNC.USER_RESPONSE) : "";
|
|
11626
11928
|
return summary ? "- **" + summary + "**" + (response ? " \u2192 " + response : "") : "";
|
|
@@ -11695,6 +11997,20 @@ function failedChunkSummary(ch) {
|
|
|
11695
11997
|
}
|
|
11696
11998
|
|
|
11697
11999
|
// src/app/stage-auth.ts
|
|
12000
|
+
function abortableAuth(invoke, signal) {
|
|
12001
|
+
return new Promise((resolve, reject) => {
|
|
12002
|
+
const auth = invoke();
|
|
12003
|
+
const onAbort = () => reject(signal.reason ?? new Error("Stage auth aborted"));
|
|
12004
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
12005
|
+
auth.then((value) => {
|
|
12006
|
+
signal.removeEventListener("abort", onAbort);
|
|
12007
|
+
resolve(value);
|
|
12008
|
+
}, (error) => {
|
|
12009
|
+
signal.removeEventListener("abort", onAbort);
|
|
12010
|
+
reject(error);
|
|
12011
|
+
});
|
|
12012
|
+
});
|
|
12013
|
+
}
|
|
11698
12014
|
async function resolveStageAuth(rc, stage) {
|
|
11699
12015
|
const model = stage === "summary" ? rc.summaryModel : stage === "explore" ? rc.segModel : rc.verifyModel;
|
|
11700
12016
|
const existing = stage === "summary" ? rc.summaryAuth : stage === "explore" ? rc.segAuth : rc.verifyAuth;
|
|
@@ -11715,11 +12031,13 @@ async function resolveStageAuth(rc, stage) {
|
|
|
11715
12031
|
rc.verifyAuth = shared;
|
|
11716
12032
|
return shared;
|
|
11717
12033
|
}
|
|
11718
|
-
|
|
12034
|
+
rc.cancellation.signal.throwIfAborted();
|
|
12035
|
+
const auth = await abortableAuth(() => rc.ctx.modelRegistry.getApiKeyAndHeaders(model), rc.cancellation.signal);
|
|
11719
12036
|
if (!auth.ok || !auth.apiKey) {
|
|
11720
12037
|
throw new Error("Authentication unavailable for " + stage + " route " + model.provider + "/" + model.id);
|
|
11721
12038
|
}
|
|
11722
12039
|
const resolved = { apiKey: auth.apiKey, headers: auth.headers };
|
|
12040
|
+
rc.cancellation.signal.throwIfAborted();
|
|
11723
12041
|
if (stage === "summary")
|
|
11724
12042
|
rc.summaryAuth = resolved;
|
|
11725
12043
|
else if (stage === "explore")
|
|
@@ -11862,7 +12180,7 @@ async function summarizeConversation(rc) {
|
|
|
11862
12180
|
cacheable = false;
|
|
11863
12181
|
generationFallbacks.push("single-pass generation failed");
|
|
11864
12182
|
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) + "]", "
|
|
12183
|
+
rc.notify("Single-pass generation stopped \xB7 using deterministic fallback [" + formatGenerationFailureForUi(err) + "]", "info");
|
|
11866
12184
|
finalSummary = assembleFallback([], extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
|
|
11867
12185
|
method = "heuristic";
|
|
11868
12186
|
}
|
|
@@ -11960,7 +12278,7 @@ async function summarizeConversation(rc) {
|
|
|
11960
12278
|
cacheable = false;
|
|
11961
12279
|
generationFallbacks.push("1 synthesis batch fallback");
|
|
11962
12280
|
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) + "]", "
|
|
12281
|
+
rc.notify("Synthesis batch stopped \xB7 deterministic evidence fallback preserved coverage [" + formatGenerationFailureForUi(err) + "]", "info");
|
|
11964
12282
|
showProgressOverlay(rc.ctx, {
|
|
11965
12283
|
phase: 3,
|
|
11966
12284
|
phaseName: "Synthesize",
|
|
@@ -11993,7 +12311,7 @@ async function summarizeConversation(rc) {
|
|
|
11993
12311
|
const idx = nextBatch++;
|
|
11994
12312
|
if (idx >= batchCallLimit)
|
|
11995
12313
|
return;
|
|
11996
|
-
if (budgetStopped || rc.services.budget.reason()) {
|
|
12314
|
+
if (budgetStopped || rc.services.budget.reason() || rc.services.budget.remainingCalls() <= 1) {
|
|
11997
12315
|
budgetStopped = true;
|
|
11998
12316
|
results[idx] = batches[idx].map((chunk) => failedChunkSummary(chunk));
|
|
11999
12317
|
} else {
|
|
@@ -12036,7 +12354,7 @@ async function summarizeConversation(rc) {
|
|
|
12036
12354
|
if (failedBatches.length) {
|
|
12037
12355
|
cacheable = false;
|
|
12038
12356
|
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]) + "]", "
|
|
12357
|
+
rc.notify(failedBatches.length + " synthesis batch(es) stopped \xB7 deterministic evidence fallback preserved coverage [" + formatGenerationFailureForUi(failedBatches[0]) + "]", "info");
|
|
12040
12358
|
showProgressOverlay(rc.ctx, {
|
|
12041
12359
|
phase: 3,
|
|
12042
12360
|
phaseName: "Synthesize",
|
|
@@ -12068,7 +12386,7 @@ async function summarizeConversation(rc) {
|
|
|
12068
12386
|
cacheable = false;
|
|
12069
12387
|
generationFallbacks.push("assembly generation failed");
|
|
12070
12388
|
recordIssue({ key: "synth.assembly", message: "Assembly used deterministic fallback (" + errorDetail(err) + ").", error: err });
|
|
12071
|
-
rc.notify("Assembly stopped \xB7 using deterministic fallback [" + formatGenerationFailureForUi(err) + "]", "
|
|
12389
|
+
rc.notify("Assembly stopped \xB7 using deterministic fallback [" + formatGenerationFailureForUi(err) + "]", "info");
|
|
12072
12390
|
method = "heuristic";
|
|
12073
12391
|
finalSummary = assembleFallback(summaries, extraction, { focus: rc.focus, note: rc.userNote }, pc.summaryBudgetTokens, rc.previousState);
|
|
12074
12392
|
}
|
|
@@ -13113,11 +13431,30 @@ function supersededResults(projection, candidates) {
|
|
|
13113
13431
|
});
|
|
13114
13432
|
return result;
|
|
13115
13433
|
}
|
|
13116
|
-
function
|
|
13434
|
+
function thinkingEditReason(branch, entries, readerApi) {
|
|
13435
|
+
if (readerApi && readerApi !== "anthropic-messages")
|
|
13436
|
+
return;
|
|
13437
|
+
const edits = new Map(entries.flatMap((entry) => entry.type === "context_edit" ? [[entry.targetId, entry.replacement]] : []));
|
|
13438
|
+
let changed = false;
|
|
13439
|
+
for (const { sourceEntry, messages } of buildSessionProjection2(branch).entries) {
|
|
13440
|
+
if (edits.has(sourceEntry.id)) {
|
|
13441
|
+
changed = true;
|
|
13442
|
+
if (edits.get(sourceEntry.id) === null)
|
|
13443
|
+
continue;
|
|
13444
|
+
}
|
|
13445
|
+
if (changed && messages.some((message) => message.role === "assistant" && message.api === "anthropic-messages" && message.content.some((block) => block.type === "thinking" && Boolean(block.thinkingSignature)))) {
|
|
13446
|
+
return "Cleanup would invalidate retained signed Anthropic thinking. Use provider-native compaction instead.";
|
|
13447
|
+
}
|
|
13448
|
+
}
|
|
13449
|
+
}
|
|
13450
|
+
function planContextTrim(branch, afterId, options = {}) {
|
|
13117
13451
|
if (afterId && !branch.some((entry) => entry.id === afterId))
|
|
13118
13452
|
throw new Error("Trim boundary is not on the active branch.");
|
|
13119
13453
|
const candidates = archivableResults(branch);
|
|
13120
|
-
const
|
|
13454
|
+
const anchorBoundary = options.anchorBoundary === undefined ? lastAnchorBoundary(branch) : options.anchorBoundary;
|
|
13455
|
+
if (anchorBoundary && !branch.some((entry) => entry.id === anchorBoundary))
|
|
13456
|
+
throw new Error("Anchor boundary is not on the active branch.");
|
|
13457
|
+
const boundaries = [afterId, anchorBoundary].filter((id) => Boolean(id));
|
|
13121
13458
|
const protectedIds = new Set(boundaries.flatMap((id) => branch.slice(0, branch.findIndex((entry) => entry.id === id) + 1).map((entry) => entry.id)));
|
|
13122
13459
|
const edited = new Set(branch.flatMap((entry) => entry.type === "context_edit" ? [entry.targetId] : []));
|
|
13123
13460
|
const projection = buildSessionProjection2(branch).entries;
|
|
@@ -13155,6 +13492,18 @@ function planContextTrim(branch, afterId) {
|
|
|
13155
13492
|
archives.push({ id, sha256: digest(text), chars: text.length });
|
|
13156
13493
|
savedChars += text.length - marker.length;
|
|
13157
13494
|
}
|
|
13495
|
+
const blockedReason = thinkingEditReason(branch, entries, options.readerApi);
|
|
13496
|
+
if (blockedReason)
|
|
13497
|
+
return {
|
|
13498
|
+
entries: [],
|
|
13499
|
+
references: [],
|
|
13500
|
+
archives: [],
|
|
13501
|
+
savedChars: 0,
|
|
13502
|
+
automatic: "signed-thinking",
|
|
13503
|
+
cooldownTurns: 0,
|
|
13504
|
+
superseded: 0,
|
|
13505
|
+
blockedReason
|
|
13506
|
+
};
|
|
13158
13507
|
const supersededCount = eligible.filter((item) => superseded.has(item.id)).length;
|
|
13159
13508
|
if (entries.length)
|
|
13160
13509
|
entries.push(contextControlEntry({ version: 1, action: "trim", references, archives }));
|
|
@@ -13165,7 +13514,7 @@ function planContextTrim(branch, afterId) {
|
|
|
13165
13514
|
const turnsSinceChange = branch.slice(lastChange + 1).filter((entry) => entry.type === "message" && entry.message.role === "assistant").length;
|
|
13166
13515
|
const cooldownTurns = lastChange < 0 ? 0 : Math.max(0, AUTO_TRIM_COOLDOWN_TURNS - turnsSinceChange);
|
|
13167
13516
|
const automatic = savedChars < MIN_AUTO_TRIM_SAVING_CHARS ? "insufficient-savings" : cooldownTurns > 0 ? "cooldown" : "ready";
|
|
13168
|
-
return { entries, references, archives, savedChars, automatic, cooldownTurns, superseded: supersededCount };
|
|
13517
|
+
return { entries, references, archives, savedChars, automatic, cooldownTurns, superseded: supersededCount, blockedReason };
|
|
13169
13518
|
}
|
|
13170
13519
|
function trimEntries(plan, cause) {
|
|
13171
13520
|
const edits = plan.entries.filter((entry) => entry.type === "context_edit");
|
|
@@ -13203,7 +13552,7 @@ function trimBreakEvenRequests(cost, savedTokens, tailTokens) {
|
|
|
13203
13552
|
const w = typeof write === "number" && Number.isFinite(write) && write > 0 ? write / input : 1;
|
|
13204
13553
|
return r <= 0 ? 0 : (w - r) * tailTokens / (r * savedTokens);
|
|
13205
13554
|
}
|
|
13206
|
-
function planContextRewind(branch, sessionId, checkpointId, report) {
|
|
13555
|
+
function planContextRewind(branch, sessionId, checkpointId, report, readerApi) {
|
|
13207
13556
|
const state = inspectContext(branch, sessionId);
|
|
13208
13557
|
if (!state.checkpoint || state.checkpoint.id !== checkpointId) {
|
|
13209
13558
|
throw new Error(state.invalidReason ?? "No matching active checkpoint; create one before research.");
|
|
@@ -13231,6 +13580,9 @@ function planContextRewind(branch, sessionId, checkpointId, report) {
|
|
|
13231
13580
|
archives.push({ id: entry.id, sha256, chars: text.length });
|
|
13232
13581
|
}
|
|
13233
13582
|
const entries = targets.map((targetId) => ({ type: "context_edit", targetId, replacement: null }));
|
|
13583
|
+
const blockedReason = thinkingEditReason(branch, entries, readerApi);
|
|
13584
|
+
if (blockedReason)
|
|
13585
|
+
throw new Error(blockedReason);
|
|
13234
13586
|
const content = `Research handoff (agent-authored, not new user instructions):
|
|
13235
13587
|
${report}
|
|
13236
13588
|
|
|
@@ -14250,7 +14602,7 @@ No matching remote project memory found.`;
|
|
|
14250
14602
|
`<smart_recall_evidence source="hindsight" kind="${attr(fact.metadata.kind ?? fact.type ?? "fact")}">`,
|
|
14251
14603
|
"Provenance: " + provenance,
|
|
14252
14604
|
memoryId ? "Ref: hindsight:" + memoryId + "@" + hindsightTargetDigest(target.baseUrl, target.bankId, projectId, memoryId) : "",
|
|
14253
|
-
clean(fact.text.slice(0, RECALL_ITEM_CAP)),
|
|
14605
|
+
clean(fact.text.slice(0, RECALL_ITEM_CAP)) + (fact.text.length > RECALL_ITEM_CAP ? " (excerpt; full memory not shown)" : ""),
|
|
14254
14606
|
"</smart_recall_evidence>"
|
|
14255
14607
|
].filter(Boolean).join(`
|
|
14256
14608
|
`);
|
|
@@ -15737,7 +16089,7 @@ function applyGlobalSettingsRuntime(paths, ctx, policy, contextTools) {
|
|
|
15737
16089
|
|
|
15738
16090
|
// src/app/compaction-commit-store.ts
|
|
15739
16091
|
function createCompactionCommitStore(options = {}) {
|
|
15740
|
-
const ttlMs = Math.max(1, options.ttlMs ?? 5 * 60000);
|
|
16092
|
+
const ttlMs = () => Math.max(1, (typeof options.ttlMs === "function" ? options.ttlMs() : options.ttlMs) ?? 5 * 60000);
|
|
15741
16093
|
const maxEntries = Math.max(1, options.maxEntries ?? 16);
|
|
15742
16094
|
const entries = new Map;
|
|
15743
16095
|
const remove = (runId, reason, notify) => {
|
|
@@ -15752,7 +16104,7 @@ function createCompactionCommitStore(options = {}) {
|
|
|
15752
16104
|
const sweep = () => {
|
|
15753
16105
|
const now = Date.now();
|
|
15754
16106
|
for (const [runId, entry] of entries) {
|
|
15755
|
-
if (now - entry.createdAt > ttlMs)
|
|
16107
|
+
if (now - entry.createdAt > ttlMs())
|
|
15756
16108
|
remove(runId, "expired", true);
|
|
15757
16109
|
}
|
|
15758
16110
|
};
|
|
@@ -15807,72 +16159,6 @@ function createCompactionCommitStore(options = {}) {
|
|
|
15807
16159
|
};
|
|
15808
16160
|
}
|
|
15809
16161
|
|
|
15810
|
-
// src/app/extension-conflicts.ts
|
|
15811
|
-
var KIND = {
|
|
15812
|
-
"context-fold": "compaction",
|
|
15813
|
-
"pi-context-prune": "context-editing",
|
|
15814
|
-
"pi-dcp": "context-editing",
|
|
15815
|
-
"pi-fold": "context-editing",
|
|
15816
|
-
"pi-openai-toolkit": "compaction",
|
|
15817
|
-
"pi-toolkit": "context-editing"
|
|
15818
|
-
};
|
|
15819
|
-
var SEGMENT_OWNER = {
|
|
15820
|
-
"context-fold": "context-fold",
|
|
15821
|
-
dcp: "pi-dcp",
|
|
15822
|
-
"pi-context-prune": "pi-context-prune",
|
|
15823
|
-
"pi-dcp": "pi-dcp",
|
|
15824
|
-
"pi-fold": "pi-fold",
|
|
15825
|
-
"pi-openai-toolkit": "pi-openai-toolkit"
|
|
15826
|
-
};
|
|
15827
|
-
function segments(info) {
|
|
15828
|
-
const out = new Set;
|
|
15829
|
-
if (!info)
|
|
15830
|
-
return out;
|
|
15831
|
-
for (const text of [info.path ?? "", info.source ?? "", info.baseDir ?? ""]) {
|
|
15832
|
-
for (const raw of text.toLowerCase().split(/[\\/:]+/)) {
|
|
15833
|
-
const segment = raw.replace(/(.)@.*$/, "$1").replace(/\.(?:[cm]?[jt]s|git)$/, "");
|
|
15834
|
-
if (segment)
|
|
15835
|
-
out.add(segment);
|
|
15836
|
-
}
|
|
15837
|
-
}
|
|
15838
|
-
return out;
|
|
15839
|
-
}
|
|
15840
|
-
function detectExtensionConflicts(registry) {
|
|
15841
|
-
const entries = [
|
|
15842
|
-
...registry.commands.filter((command) => command.source === "extension").map((command) => ({ name: command.name.toLowerCase(), label: `/${command.name} command`, segments: segments(command.sourceInfo) })),
|
|
15843
|
-
...registry.tools.map((tool) => ({ name: tool.name.toLowerCase(), label: `${tool.name} tool`, segments: segments(tool.sourceInfo) }))
|
|
15844
|
-
].filter((entry) => !(entry.segments.has("pi-smart-compact") || entry.name.startsWith("smart_") || entry.name === "smart-compact"));
|
|
15845
|
-
const found = new Map;
|
|
15846
|
-
const add = (name, evidence) => {
|
|
15847
|
-
if (!found.has(name))
|
|
15848
|
-
found.set(name, evidence);
|
|
15849
|
-
};
|
|
15850
|
-
const bareFold = [];
|
|
15851
|
-
for (const entry of entries) {
|
|
15852
|
-
const owner = [...entry.segments].map((segment) => SEGMENT_OWNER[segment]).find(Boolean);
|
|
15853
|
-
if (owner)
|
|
15854
|
-
add(owner, `${entry.label} from ${owner}`);
|
|
15855
|
-
else if (entry.segments.has("pi-toolkit") && entry.name === "context")
|
|
15856
|
-
add("pi-toolkit", `${entry.label} from pi-toolkit`);
|
|
15857
|
-
else if (entry.name === "fold-handoff")
|
|
15858
|
-
add("context-fold", entry.label);
|
|
15859
|
-
else if (entry.name === "prune")
|
|
15860
|
-
add("pi-context-prune", entry.label);
|
|
15861
|
-
else if (entry.name === "fold")
|
|
15862
|
-
bareFold.push(entry);
|
|
15863
|
-
}
|
|
15864
|
-
for (const entry of bareFold)
|
|
15865
|
-
add(found.has("context-fold") ? "context-fold" : "pi-fold", entry.label);
|
|
15866
|
-
return [...found].map(([name, evidence]) => ({ name, kind: KIND[name], evidence })).sort((a, b) => a.name.localeCompare(b.name));
|
|
15867
|
-
}
|
|
15868
|
-
function conflictNotice(conflicts) {
|
|
15869
|
-
const names = conflicts.map((conflict) => conflict.name);
|
|
15870
|
-
const list = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
|
|
15871
|
-
const verb = names.length === 1 ? "also compacts or edits" : "also compact or edit";
|
|
15872
|
-
const evidence = conflicts.map((conflict) => `${conflict.name}: ${conflict.evidence}`).join("; ");
|
|
15873
|
-
return `${list} ${verb} this conversation (${evidence}). Two owners of the same history are not safe in any load order: summaries can be discarded and cache prefixes rewritten twice. Keep only one loaded (Pi: /settings \u203A packages, or the extensions list).`;
|
|
15874
|
-
}
|
|
15875
|
-
|
|
15876
16162
|
// src/app/native-continuity-bridge.ts
|
|
15877
16163
|
import crypto6 from "crypto";
|
|
15878
16164
|
import fs13 from "fs";
|
|
@@ -16082,89 +16368,6 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
16082
16368
|
};
|
|
16083
16369
|
}
|
|
16084
16370
|
|
|
16085
|
-
// src/app/settled-auto-trigger.ts
|
|
16086
|
-
function createSettledAutoTrigger(options = {}) {
|
|
16087
|
-
const now = options.now ?? Date.now;
|
|
16088
|
-
const cooldownMs = Math.max(0, options.cooldownMs ?? SETTLED_TRIGGER_COOLDOWN_MS);
|
|
16089
|
-
const active = new Map;
|
|
16090
|
-
const lastCompactionAt = new Map;
|
|
16091
|
-
const noteCompaction = (sessionId) => {
|
|
16092
|
-
if (!isUnresolvedSessionId(sessionId))
|
|
16093
|
-
lastCompactionAt.set(sessionId, now());
|
|
16094
|
-
};
|
|
16095
|
-
const clear = (sessionId) => {
|
|
16096
|
-
active.delete(sessionId);
|
|
16097
|
-
lastCompactionAt.delete(sessionId);
|
|
16098
|
-
};
|
|
16099
|
-
const request = async (ctx, config) => {
|
|
16100
|
-
if (!config.autoTrigger || !["settled", "background"].includes(config.autoTriggerStrategy))
|
|
16101
|
-
return;
|
|
16102
|
-
const sessionId = resolveSessionId(ctx);
|
|
16103
|
-
if (isUnresolvedSessionId(sessionId) || active.has(sessionId))
|
|
16104
|
-
return;
|
|
16105
|
-
const usage = ctx.getContextUsage();
|
|
16106
|
-
const totalTokens = usage?.tokens;
|
|
16107
|
-
if (typeof totalTokens !== "number" || !Number.isFinite(totalTokens) || totalTokens < MIN_TOKEN_THRESHOLD || !ctx.model)
|
|
16108
|
-
return;
|
|
16109
|
-
const contextPercent = safeContextPercent(totalTokens, effectiveContextWindow(ctx.model, config));
|
|
16110
|
-
if (contextPercent < config.minContextPercent)
|
|
16111
|
-
return;
|
|
16112
|
-
const lastCompaction = lastCompactionAt.get(sessionId);
|
|
16113
|
-
if (lastCompaction !== undefined && now() - lastCompaction < cooldownMs)
|
|
16114
|
-
return;
|
|
16115
|
-
if (!ctx.isIdle() || ctx.hasPendingMessages())
|
|
16116
|
-
return;
|
|
16117
|
-
const requestToken = Symbol(sessionId);
|
|
16118
|
-
active.set(sessionId, requestToken);
|
|
16119
|
-
await new Promise((resolve) => {
|
|
16120
|
-
let finished = false;
|
|
16121
|
-
const watchdogMs = options.watchdogMs ?? AUTO_TRIGGER_TIMEOUT_CAP_MS + config.autoTriggerTimeoutMs;
|
|
16122
|
-
const watchdog = setTimeout(() => {
|
|
16123
|
-
reportIssue({
|
|
16124
|
-
key: "auto.settled-no-callback",
|
|
16125
|
-
message: "Pi did not report the automatic compaction result within " + Math.round(watchdogMs / 1000) + "s. The next settled turn may request it again."
|
|
16126
|
-
}, ctx);
|
|
16127
|
-
finish();
|
|
16128
|
-
}, watchdogMs);
|
|
16129
|
-
watchdog.unref?.();
|
|
16130
|
-
const finish = () => {
|
|
16131
|
-
if (finished)
|
|
16132
|
-
return;
|
|
16133
|
-
finished = true;
|
|
16134
|
-
clearTimeout(watchdog);
|
|
16135
|
-
if (active.get(sessionId) === requestToken)
|
|
16136
|
-
active.delete(sessionId);
|
|
16137
|
-
resolve();
|
|
16138
|
-
};
|
|
16139
|
-
try {
|
|
16140
|
-
ctx.compact({
|
|
16141
|
-
onComplete: () => {
|
|
16142
|
-
if (active.get(sessionId) === requestToken)
|
|
16143
|
-
noteCompaction(sessionId);
|
|
16144
|
-
finish();
|
|
16145
|
-
},
|
|
16146
|
-
onError: (error) => {
|
|
16147
|
-
reportIssue({
|
|
16148
|
-
key: "auto.settled-apply",
|
|
16149
|
-
message: "Pi could not run the automatic compaction (" + errorDetail(error) + "). Conversation unchanged. Run /smart-compact manually if context is high.",
|
|
16150
|
-
error
|
|
16151
|
-
}, ctx);
|
|
16152
|
-
finish();
|
|
16153
|
-
}
|
|
16154
|
-
});
|
|
16155
|
-
} catch (error) {
|
|
16156
|
-
reportIssue({
|
|
16157
|
-
key: "auto.settled-apply",
|
|
16158
|
-
message: "Pi could not run the automatic compaction (" + errorDetail(error) + "). Conversation unchanged. Run /smart-compact manually if context is high.",
|
|
16159
|
-
error
|
|
16160
|
-
}, ctx);
|
|
16161
|
-
finish();
|
|
16162
|
-
}
|
|
16163
|
-
});
|
|
16164
|
-
};
|
|
16165
|
-
return { request, noteCompaction, clear };
|
|
16166
|
-
}
|
|
16167
|
-
|
|
16168
16371
|
// src/app/background-preparation.ts
|
|
16169
16372
|
function recordPreparationDiscard(sessionId, snapshot, reason) {
|
|
16170
16373
|
return appendMetricsSnapshot(sessionId, {
|
|
@@ -16186,11 +16389,31 @@ function preparationWindow(config, window) {
|
|
|
16186
16389
|
const startTokens = explicit ? Math.max(MIN_TOKEN_THRESHOLD, window * prepare / 100) : preparationStartTokens(applyTokens);
|
|
16187
16390
|
return { startTokens, applyTokens };
|
|
16188
16391
|
}
|
|
16392
|
+
function contextPressure(ctx, config) {
|
|
16393
|
+
const raw = ctx.getContextUsage?.()?.tokens;
|
|
16394
|
+
const tokens = typeof raw === "number" && Number.isFinite(raw) && raw >= 0 ? raw : null;
|
|
16395
|
+
const rawWindow = effectiveContextWindow(ctx.model, config);
|
|
16396
|
+
const policyWindow = typeof rawWindow === "number" && Number.isFinite(rawWindow) && rawWindow > 0 ? rawWindow : null;
|
|
16397
|
+
const gates = policyWindow === null ? null : preparationWindow(config, policyWindow);
|
|
16398
|
+
const cleanupTokens = gates ? Math.ceil(gates.startTokens) : null;
|
|
16399
|
+
const compactionTokens = gates ? Math.ceil(Math.max(MIN_TOKEN_THRESHOLD, gates.applyTokens)) : null;
|
|
16400
|
+
return {
|
|
16401
|
+
tokens,
|
|
16402
|
+
modelWindow: ctx.model?.contextWindow ?? null,
|
|
16403
|
+
policyWindow,
|
|
16404
|
+
percent: tokens !== null && policyWindow !== null ? tokens / policyWindow * 100 : null,
|
|
16405
|
+
cleanupTokens,
|
|
16406
|
+
compactionTokens,
|
|
16407
|
+
cleanup: tokens !== null && cleanupTokens !== null && tokens >= cleanupTokens,
|
|
16408
|
+
compaction: tokens !== null && compactionTokens !== null && tokens >= compactionTokens
|
|
16409
|
+
};
|
|
16410
|
+
}
|
|
16189
16411
|
function snapshotContext(ctx) {
|
|
16190
16412
|
const branch = structuredClone(ctx.sessionManager.getBranch());
|
|
16191
16413
|
const sessionId = resolveSessionId(ctx);
|
|
16192
16414
|
const sessionFile = ctx.sessionManager.getSessionFile?.();
|
|
16193
16415
|
const usage = structuredClone(ctx.getContextUsage());
|
|
16416
|
+
const systemPrompt = ctx.getSystemPrompt?.() ?? "";
|
|
16194
16417
|
const sessionManager = new Proxy(ctx.sessionManager, {
|
|
16195
16418
|
get(target, key) {
|
|
16196
16419
|
if (key === "getBranch")
|
|
@@ -16209,20 +16432,21 @@ function snapshotContext(ctx) {
|
|
|
16209
16432
|
model: ctx.model && { ...ctx.model },
|
|
16210
16433
|
hasUI: false,
|
|
16211
16434
|
getContextUsage: () => usage,
|
|
16435
|
+
getSystemPrompt: () => systemPrompt,
|
|
16212
16436
|
ui: { ...ctx.ui, notify() {}, setStatus() {}, setWidget() {} }
|
|
16213
16437
|
};
|
|
16214
16438
|
return Object.assign(snapshot, { silentBackground: true });
|
|
16215
16439
|
}
|
|
16216
16440
|
function signature(ctx, config) {
|
|
16217
16441
|
const model = ctx.model;
|
|
16218
|
-
return JSON.stringify([model?.provider, model?.id, model?.api, model?.contextWindow, model?.maxTokens, config]);
|
|
16442
|
+
return JSON.stringify([model?.provider, model?.id, model?.api, model?.baseUrl, model?.contextWindow, model?.maxTokens, ctx.getSystemPrompt?.(), config]);
|
|
16219
16443
|
}
|
|
16220
16444
|
function enabled(config) {
|
|
16221
16445
|
return config.autoTrigger && config.autoTriggerStrategy === "background";
|
|
16222
16446
|
}
|
|
16223
16447
|
function createBackgroundPreparation(options) {
|
|
16224
16448
|
const now = options.now ?? Date.now;
|
|
16225
|
-
const
|
|
16449
|
+
const signatureOf = (ctx, config) => signature(ctx, config) + (options.toolSignature?.() ?? "");
|
|
16226
16450
|
const cooldownMs = options.cooldownMs ?? SETTLED_TRIGGER_COOLDOWN_MS;
|
|
16227
16451
|
let current = null;
|
|
16228
16452
|
let lastAttempt;
|
|
@@ -16269,9 +16493,9 @@ function createBackgroundPreparation(options) {
|
|
|
16269
16493
|
return "config";
|
|
16270
16494
|
if (task.sessionId !== resolveSessionId(ctx))
|
|
16271
16495
|
return "session";
|
|
16272
|
-
if (task.signature !==
|
|
16496
|
+
if (task.signature !== signatureOf(ctx, config))
|
|
16273
16497
|
return "config";
|
|
16274
|
-
if (now() - (task.readyAt ?? task.createdAt) > ttlMs)
|
|
16498
|
+
if (now() - (task.readyAt ?? task.createdAt) > (options.ttlMs ?? config.pendingTtlMs))
|
|
16275
16499
|
return "ttl";
|
|
16276
16500
|
const branch = ctx.sessionManager.getBranch();
|
|
16277
16501
|
const origin = branch.findIndex((entry) => entry.id === task.originId);
|
|
@@ -16303,7 +16527,7 @@ function createBackgroundPreparation(options) {
|
|
|
16303
16527
|
const task = {
|
|
16304
16528
|
sessionId,
|
|
16305
16529
|
originId,
|
|
16306
|
-
signature:
|
|
16530
|
+
signature: signatureOf(ctx, config),
|
|
16307
16531
|
controller: new AbortController,
|
|
16308
16532
|
createdAt: now(),
|
|
16309
16533
|
ctx
|
|
@@ -16311,7 +16535,15 @@ function createBackgroundPreparation(options) {
|
|
|
16311
16535
|
current = task;
|
|
16312
16536
|
lastAttempt = { sessionId, at: now() };
|
|
16313
16537
|
const preparationConfig = { ...config, minContextPercent: startTokens / window * 100 };
|
|
16314
|
-
track(Promise.resolve().then(() =>
|
|
16538
|
+
track(Promise.resolve().then(() => {
|
|
16539
|
+
if (current !== task || task.controller.signal.aborted)
|
|
16540
|
+
return null;
|
|
16541
|
+
if (!valid(task, ctx, config)) {
|
|
16542
|
+
cancel("config");
|
|
16543
|
+
return null;
|
|
16544
|
+
}
|
|
16545
|
+
return options.prepare(snapshot, preparationConfig, task.controller.signal);
|
|
16546
|
+
}).then((pending) => {
|
|
16315
16547
|
if (pending) {
|
|
16316
16548
|
task.pending = pending;
|
|
16317
16549
|
task.readyAt = now();
|
|
@@ -16396,6 +16628,83 @@ function createBackgroundPreparation(options) {
|
|
|
16396
16628
|
};
|
|
16397
16629
|
}
|
|
16398
16630
|
|
|
16631
|
+
// src/app/settled-auto-trigger.ts
|
|
16632
|
+
function createSettledAutoTrigger(options = {}) {
|
|
16633
|
+
const now = options.now ?? Date.now;
|
|
16634
|
+
const cooldownMs = Math.max(0, options.cooldownMs ?? SETTLED_TRIGGER_COOLDOWN_MS);
|
|
16635
|
+
const active = new Map;
|
|
16636
|
+
const cooldownStartedAt = new Map;
|
|
16637
|
+
const noteCompaction = (sessionId) => {
|
|
16638
|
+
if (!isUnresolvedSessionId(sessionId))
|
|
16639
|
+
cooldownStartedAt.set(sessionId, now());
|
|
16640
|
+
};
|
|
16641
|
+
const clear = (sessionId) => {
|
|
16642
|
+
active.delete(sessionId);
|
|
16643
|
+
cooldownStartedAt.delete(sessionId);
|
|
16644
|
+
};
|
|
16645
|
+
const request = async (ctx, config) => {
|
|
16646
|
+
if (!config.autoTrigger || !["settled", "background"].includes(config.autoTriggerStrategy))
|
|
16647
|
+
return;
|
|
16648
|
+
const sessionId = resolveSessionId(ctx);
|
|
16649
|
+
if (isUnresolvedSessionId(sessionId) || active.has(sessionId))
|
|
16650
|
+
return;
|
|
16651
|
+
if (!contextPressure(ctx, config).compaction)
|
|
16652
|
+
return;
|
|
16653
|
+
const cooldownStart = cooldownStartedAt.get(sessionId);
|
|
16654
|
+
if (cooldownStart !== undefined && now() - cooldownStart < cooldownMs)
|
|
16655
|
+
return;
|
|
16656
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages())
|
|
16657
|
+
return;
|
|
16658
|
+
const requestToken = Symbol(sessionId);
|
|
16659
|
+
active.set(sessionId, requestToken);
|
|
16660
|
+
await new Promise((resolve) => {
|
|
16661
|
+
let finished = false;
|
|
16662
|
+
const watchdogMs = options.watchdogMs ?? AUTO_TRIGGER_TIMEOUT_CAP_MS + config.autoTriggerTimeoutMs;
|
|
16663
|
+
const watchdog = setTimeout(() => {
|
|
16664
|
+
const hostBusy = typeof ctx.isIdle === "function" ? !ctx.isIdle() : false;
|
|
16665
|
+
reportIssue({
|
|
16666
|
+
key: "auto.settled-no-callback",
|
|
16667
|
+
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."
|
|
16668
|
+
}, ctx);
|
|
16669
|
+
finish();
|
|
16670
|
+
}, watchdogMs);
|
|
16671
|
+
watchdog.unref?.();
|
|
16672
|
+
const finish = () => {
|
|
16673
|
+
if (finished)
|
|
16674
|
+
return;
|
|
16675
|
+
finished = true;
|
|
16676
|
+
clearTimeout(watchdog);
|
|
16677
|
+
if (active.get(sessionId) === requestToken) {
|
|
16678
|
+
active.delete(sessionId);
|
|
16679
|
+
cooldownStartedAt.set(sessionId, now());
|
|
16680
|
+
}
|
|
16681
|
+
resolve();
|
|
16682
|
+
};
|
|
16683
|
+
try {
|
|
16684
|
+
ctx.compact({
|
|
16685
|
+
onComplete: finish,
|
|
16686
|
+
onError: (error) => {
|
|
16687
|
+
reportIssue({
|
|
16688
|
+
key: "auto.settled-apply",
|
|
16689
|
+
message: "Pi could not run the automatic compaction (" + errorDetail(error) + "). Conversation unchanged. Run /smart-compact manually if context is high.",
|
|
16690
|
+
error
|
|
16691
|
+
}, ctx);
|
|
16692
|
+
finish();
|
|
16693
|
+
}
|
|
16694
|
+
});
|
|
16695
|
+
} catch (error) {
|
|
16696
|
+
reportIssue({
|
|
16697
|
+
key: "auto.settled-apply",
|
|
16698
|
+
message: "Pi could not run the automatic compaction (" + errorDetail(error) + "). Conversation unchanged. Run /smart-compact manually if context is high.",
|
|
16699
|
+
error
|
|
16700
|
+
}, ctx);
|
|
16701
|
+
finish();
|
|
16702
|
+
}
|
|
16703
|
+
});
|
|
16704
|
+
};
|
|
16705
|
+
return { request, noteCompaction, clear };
|
|
16706
|
+
}
|
|
16707
|
+
|
|
16399
16708
|
// src/app/host-cache-ledger.ts
|
|
16400
16709
|
var FOREIGN_REBUILD_WARN_COUNT = 3;
|
|
16401
16710
|
var num = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
@@ -16559,8 +16868,106 @@ function compactionUsage(snapshot, resolveModel) {
|
|
|
16559
16868
|
|
|
16560
16869
|
// src/app/register-context-tools.ts
|
|
16561
16870
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
16871
|
+
import { Text as Text4 } from "@earendil-works/pi-tui";
|
|
16562
16872
|
import { Type as Type3 } from "typebox";
|
|
16563
16873
|
|
|
16874
|
+
// src/ui/tool-rows.ts
|
|
16875
|
+
import { stripVTControlCharacters as stripVTControlCharacters2 } from "util";
|
|
16876
|
+
import { Text as Text3, truncateToWidth as truncateToWidth3 } from "@earendil-works/pi-tui";
|
|
16877
|
+
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
16878
|
+
var STATUS_COLOR = {
|
|
16879
|
+
done: "success",
|
|
16880
|
+
queued: "dim",
|
|
16881
|
+
pending: "warning",
|
|
16882
|
+
failed: "error",
|
|
16883
|
+
cancelled: "warning",
|
|
16884
|
+
skipped: "dim",
|
|
16885
|
+
info: "muted"
|
|
16886
|
+
};
|
|
16887
|
+
var STATUS_WORD = {
|
|
16888
|
+
done: "done",
|
|
16889
|
+
queued: "queued",
|
|
16890
|
+
pending: "pending",
|
|
16891
|
+
failed: "failed",
|
|
16892
|
+
cancelled: "cancelled",
|
|
16893
|
+
skipped: "skipped",
|
|
16894
|
+
info: ""
|
|
16895
|
+
};
|
|
16896
|
+
function sanitizeVisible(text) {
|
|
16897
|
+
return stripVTControlCharacters2(text).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, " ");
|
|
16898
|
+
}
|
|
16899
|
+
function firstTextContent(content) {
|
|
16900
|
+
const text = content?.find((part) => part.type === "text" && part.text)?.text;
|
|
16901
|
+
return text ? sanitizeVisible(text) : "";
|
|
16902
|
+
}
|
|
16903
|
+
function summarizeLine(text, maxLength = 120) {
|
|
16904
|
+
const single = sanitizeVisible(text).split(/\r?\n/).find((line) => line.trim()) ?? "";
|
|
16905
|
+
const clipped = single.trim().slice(0, maxLength);
|
|
16906
|
+
return clipped + (single.trim().length > maxLength ? "\u2026" : "");
|
|
16907
|
+
}
|
|
16908
|
+
function statusLabel(theme, status) {
|
|
16909
|
+
const word = STATUS_WORD[status];
|
|
16910
|
+
return word ? theme.fg(STATUS_COLOR[status], word) : "";
|
|
16911
|
+
}
|
|
16912
|
+
function metaLine(theme, key, value) {
|
|
16913
|
+
return theme.fg("dim", key + ":") + " " + sanitizeVisible(value);
|
|
16914
|
+
}
|
|
16915
|
+
function previewBlock(lines, expanded, maxCollapsed, maxLength = 160) {
|
|
16916
|
+
const clean = lines.map((line) => sanitizeVisible(line));
|
|
16917
|
+
if (expanded)
|
|
16918
|
+
return clean;
|
|
16919
|
+
const shown = clean.slice(0, maxCollapsed).map((line) => line.slice(0, maxLength));
|
|
16920
|
+
if (clean.length > maxCollapsed) {
|
|
16921
|
+
shown.push("\u2026 (" + (clean.length - maxCollapsed) + " more)");
|
|
16922
|
+
}
|
|
16923
|
+
return shown;
|
|
16924
|
+
}
|
|
16925
|
+
function allTextParts(content) {
|
|
16926
|
+
return (content ?? []).filter((part) => part.type === "text" && part.text).map((part) => sanitizeVisible(part.text));
|
|
16927
|
+
}
|
|
16928
|
+
function safeArg(value, maxLength = 120) {
|
|
16929
|
+
if (typeof value !== "string")
|
|
16930
|
+
return "";
|
|
16931
|
+
return sanitizeVisible(value).trim().slice(0, maxLength);
|
|
16932
|
+
}
|
|
16933
|
+
function expandHint(theme) {
|
|
16934
|
+
try {
|
|
16935
|
+
return theme.fg("dim", keyHint("app.tools.expand", "expand"));
|
|
16936
|
+
} catch {
|
|
16937
|
+
return theme.fg("dim", "expand tool output for full details");
|
|
16938
|
+
}
|
|
16939
|
+
}
|
|
16940
|
+
function expandedRow(theme, result, summaryLines) {
|
|
16941
|
+
const content = allTextParts(result.content).join(`
|
|
16942
|
+
`);
|
|
16943
|
+
const head = summaryLines.filter(Boolean).join(`
|
|
16944
|
+
`);
|
|
16945
|
+
return new Text3(head + (content ? `
|
|
16946
|
+
` + theme.fg("borderMuted", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500") + `
|
|
16947
|
+
` + content : ""), 0, 0);
|
|
16948
|
+
}
|
|
16949
|
+
function rawFallbackRow(theme, result, expanded) {
|
|
16950
|
+
const parts = allTextParts(result.content);
|
|
16951
|
+
if (!parts.length) {
|
|
16952
|
+
return new Text3(theme.fg("warning", "No result details available"), 0, 0);
|
|
16953
|
+
}
|
|
16954
|
+
if (expanded)
|
|
16955
|
+
return new Text3(parts.join(`
|
|
16956
|
+
`), 0, 0);
|
|
16957
|
+
const joined = parts.join(`
|
|
16958
|
+
`);
|
|
16959
|
+
const bounded = truncateToWidth3(joined.split(/\r?\n/)[0] ?? "", 200, "\u2026");
|
|
16960
|
+
return new Text3(bounded + (joined.length > bounded.length ? `
|
|
16961
|
+
\u2026 (expanded shows full content)` : ""), 0, 0);
|
|
16962
|
+
}
|
|
16963
|
+
function tryRow(theme, build, result, expanded) {
|
|
16964
|
+
try {
|
|
16965
|
+
return build();
|
|
16966
|
+
} catch {
|
|
16967
|
+
return rawFallbackRow(theme, result, expanded);
|
|
16968
|
+
}
|
|
16969
|
+
}
|
|
16970
|
+
|
|
16564
16971
|
// src/app/mnemopi-memory.ts
|
|
16565
16972
|
import { execFile as execFile2 } from "child_process";
|
|
16566
16973
|
import { existsSync as existsSync5 } from "fs";
|
|
@@ -16848,6 +17255,15 @@ function registerContextTools(pi) {
|
|
|
16848
17255
|
if (loadConfig().toolLoading === "off" || !pi.getActiveTools().includes("smart_recall"))
|
|
16849
17256
|
throw new Error("smart_recall is disabled in Pi settings or /tools.");
|
|
16850
17257
|
return executeRecall(params, signal, ctx);
|
|
17258
|
+
},
|
|
17259
|
+
renderCall(args, theme) {
|
|
17260
|
+
const label = theme.fg("toolTitle", "smart_recall ");
|
|
17261
|
+
const scope = args.scope ? theme.fg("dim", " [" + safeArg(args.scope, 20) + "]") : "";
|
|
17262
|
+
const query = safeArg(args.query, 100);
|
|
17263
|
+
return new Text4(label + theme.fg("muted", query) + scope, 0, 0);
|
|
17264
|
+
},
|
|
17265
|
+
renderResult(result, { expanded }, theme, context) {
|
|
17266
|
+
return tryRow(theme, () => renderRecallRow(result, expanded, theme, context), result, expanded);
|
|
16851
17267
|
}
|
|
16852
17268
|
});
|
|
16853
17269
|
pi.registerTool({
|
|
@@ -16882,9 +17298,197 @@ function registerContextTools(pi) {
|
|
|
16882
17298
|
if (loadConfig().toolLoading === "off" || !pi.getActiveTools().includes("smart_save_memory"))
|
|
16883
17299
|
throw new Error("smart_save_memory is disabled in Pi settings or /tools.");
|
|
16884
17300
|
return executeSaveMemory(params, signal, ctx);
|
|
17301
|
+
},
|
|
17302
|
+
renderCall(args, theme) {
|
|
17303
|
+
const label = theme.fg("toolTitle", "smart_save_memory ");
|
|
17304
|
+
if (args.ref && (args.status ?? "active") === "resolved") {
|
|
17305
|
+
return new Text4(label + theme.fg("muted", "resolve ") + theme.fg("dim", safeArg(args.ref, 80)), 0, 0);
|
|
17306
|
+
}
|
|
17307
|
+
const kind = theme.fg("accent", safeArg(args.kind ?? "memory", 40));
|
|
17308
|
+
const title = safeArg(args.title, 80);
|
|
17309
|
+
return new Text4(label + kind + (title ? " " + theme.fg("muted", title) : ""), 0, 0);
|
|
17310
|
+
},
|
|
17311
|
+
renderResult(result, { expanded }, theme, context) {
|
|
17312
|
+
return tryRow(theme, () => renderSaveRow(result, expanded, theme, context), result, expanded);
|
|
16885
17313
|
}
|
|
16886
17314
|
});
|
|
16887
17315
|
}
|
|
17316
|
+
function receiptBuckets(receipts) {
|
|
17317
|
+
const buckets = { done: 0, pending: 0, failed: 0, unknown: 0 };
|
|
17318
|
+
for (const receipt of receipts) {
|
|
17319
|
+
const state = receipt.state ?? "unknown";
|
|
17320
|
+
if (state === "completed" || state === "deleted")
|
|
17321
|
+
buckets.done++;
|
|
17322
|
+
else if (state === "failed")
|
|
17323
|
+
buckets.failed++;
|
|
17324
|
+
else if (state === "unknown")
|
|
17325
|
+
buckets.unknown++;
|
|
17326
|
+
else
|
|
17327
|
+
buckets.pending++;
|
|
17328
|
+
}
|
|
17329
|
+
return buckets;
|
|
17330
|
+
}
|
|
17331
|
+
function renderRecallRow(result, expanded, theme, context) {
|
|
17332
|
+
if (context.isError) {
|
|
17333
|
+
const head = theme.fg("error", "recall failed:");
|
|
17334
|
+
return expanded ? expandedRow(theme, result, [head]) : new Text4(head + " " + summarizeLine(firstTextContent(result.content), 140), 0, 0);
|
|
17335
|
+
}
|
|
17336
|
+
const details = result.details;
|
|
17337
|
+
if (!details)
|
|
17338
|
+
return rawFallbackRow(theme, result, expanded);
|
|
17339
|
+
const lines = [];
|
|
17340
|
+
if (Array.isArray(details.results)) {
|
|
17341
|
+
const results = details.results;
|
|
17342
|
+
lines.push(statusLabel(theme, results.length ? "done" : "skipped") + " " + theme.fg("muted", results.length + " local match(es)"));
|
|
17343
|
+
if (!expanded) {
|
|
17344
|
+
lines.push(...previewBlock(results.map((item) => "[" + (item.kind ?? "fact") + "] " + summarizeLine(item.title ?? "", 100) + (item.content ? " \u2014 " + summarizeLine(item.content, 80) : "")), false, 3));
|
|
17345
|
+
}
|
|
17346
|
+
return expanded ? expandedRow(theme, result, lines) : new Text4(lines.join(`
|
|
17347
|
+
`), 0, 0);
|
|
17348
|
+
}
|
|
17349
|
+
const mnemopi = details.mnemopi;
|
|
17350
|
+
if (mnemopi) {
|
|
17351
|
+
if (mnemopi.state === "recalled") {
|
|
17352
|
+
lines.push(statusLabel(theme, mnemopi.facts?.length ? "done" : "skipped") + " " + theme.fg("muted", (mnemopi.facts?.length ?? 0) + " mnemopi match(es)"));
|
|
17353
|
+
if (!expanded) {
|
|
17354
|
+
lines.push(...previewBlock((mnemopi.facts ?? []).map((fact) => "[" + (fact.kind ?? "fact") + "] " + summarizeLine(fact.title ?? "", 100) + (fact.content ? " \u2014 " + summarizeLine(fact.content, 80) : "")), false, 3));
|
|
17355
|
+
}
|
|
17356
|
+
} else {
|
|
17357
|
+
const status = mnemopi.state === "failed" ? "failed" : mnemopi.state === "skipped" ? "skipped" : "pending";
|
|
17358
|
+
lines.push(statusLabel(theme, status) + " " + theme.fg("muted", "mnemopi " + (mnemopi.state ?? "")));
|
|
17359
|
+
if (mnemopi.reason)
|
|
17360
|
+
lines.push(theme.fg("dim", summarizeLine(mnemopi.reason, 140)));
|
|
17361
|
+
}
|
|
17362
|
+
return expanded ? expandedRow(theme, result, lines) : new Text4(lines.join(`
|
|
17363
|
+
`), 0, 0);
|
|
17364
|
+
}
|
|
17365
|
+
const remote = details.remote;
|
|
17366
|
+
if (remote) {
|
|
17367
|
+
if (remote.state === "ok") {
|
|
17368
|
+
const buckets = receiptBuckets(remote.receipts ?? []);
|
|
17369
|
+
lines.push(statusLabel(theme, remote.facts?.length ? "done" : "skipped") + " " + theme.fg("muted", (remote.facts?.length ?? 0) + " remote fact(s)"));
|
|
17370
|
+
if (remote.receipts?.length) {
|
|
17371
|
+
lines.push(metaLine(theme, "prior saves", buckets.done + " completed, " + buckets.pending + " pending, " + buckets.failed + " failed, " + buckets.unknown + " unknown"));
|
|
17372
|
+
}
|
|
17373
|
+
if (!expanded) {
|
|
17374
|
+
lines.push(...previewBlock((remote.facts ?? []).map((fact) => (fact.type ? "[" + fact.type + "] " : "") + summarizeLine(fact.preview ?? "", 110)), false, 3));
|
|
17375
|
+
lines.push(expandHint(theme));
|
|
17376
|
+
}
|
|
17377
|
+
} else {
|
|
17378
|
+
const status = remote.state === "failed" ? "failed" : remote.state === "not-configured" || remote.state === "skipped" ? "skipped" : "pending";
|
|
17379
|
+
lines.push(statusLabel(theme, status) + " " + theme.fg("muted", "hindsight " + (remote.state ?? "")));
|
|
17380
|
+
if (remote.reason)
|
|
17381
|
+
lines.push(theme.fg("dim", summarizeLine(remote.reason, 140)));
|
|
17382
|
+
}
|
|
17383
|
+
return expanded ? expandedRow(theme, result, lines) : new Text4(lines.join(`
|
|
17384
|
+
`), 0, 0);
|
|
17385
|
+
}
|
|
17386
|
+
return rawFallbackRow(theme, result, expanded);
|
|
17387
|
+
}
|
|
17388
|
+
function renderSaveRow(result, expanded, theme, context) {
|
|
17389
|
+
if (context.isError) {
|
|
17390
|
+
const head = theme.fg("error", "save failed:");
|
|
17391
|
+
return expanded ? expandedRow(theme, result, [head]) : new Text4(head + " " + summarizeLine(firstTextContent(result.content), 140), 0, 0);
|
|
17392
|
+
}
|
|
17393
|
+
const details = result.details;
|
|
17394
|
+
if (!details)
|
|
17395
|
+
return rawFallbackRow(theme, result, expanded);
|
|
17396
|
+
const lines = [];
|
|
17397
|
+
const finish = () => expanded ? expandedRow(theme, result, lines) : new Text4(lines.join(`
|
|
17398
|
+
`), 0, 0);
|
|
17399
|
+
if (details.approved === false) {
|
|
17400
|
+
lines.push(statusLabel(theme, "cancelled") + " " + theme.fg("muted", "not saved \u2014 user did not approve"));
|
|
17401
|
+
return finish();
|
|
17402
|
+
}
|
|
17403
|
+
if (typeof details.closed === "number") {
|
|
17404
|
+
if (details.closed > 0) {
|
|
17405
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "resolved"));
|
|
17406
|
+
if (expanded && typeof details.ref === "string") {
|
|
17407
|
+
lines.push(metaLine(theme, "ref", details.ref));
|
|
17408
|
+
}
|
|
17409
|
+
} else {
|
|
17410
|
+
lines.push(statusLabel(theme, "skipped") + " " + theme.fg("muted", "no active match \u2014 nothing changed"));
|
|
17411
|
+
if (expanded && typeof details.ref === "string") {
|
|
17412
|
+
lines.push(metaLine(theme, "ref", details.ref));
|
|
17413
|
+
}
|
|
17414
|
+
}
|
|
17415
|
+
return finish();
|
|
17416
|
+
}
|
|
17417
|
+
const local = details.memory;
|
|
17418
|
+
if (local && typeof details.ref === "string") {
|
|
17419
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "saved locally"));
|
|
17420
|
+
const fact = "[" + (local.kind ?? "memory") + "] " + (local.title ?? "");
|
|
17421
|
+
if (fact.trim() !== "[]")
|
|
17422
|
+
lines.push(metaLine(theme, "fact", fact));
|
|
17423
|
+
if (expanded)
|
|
17424
|
+
lines.push(metaLine(theme, "ref", details.ref));
|
|
17425
|
+
else
|
|
17426
|
+
lines.push(expandHint(theme));
|
|
17427
|
+
return finish();
|
|
17428
|
+
}
|
|
17429
|
+
const mnemopi = details.mnemopi;
|
|
17430
|
+
if (mnemopi) {
|
|
17431
|
+
if (mnemopi.state === "saved") {
|
|
17432
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "mnemopi saved"));
|
|
17433
|
+
if (expanded && typeof details.ref === "string")
|
|
17434
|
+
lines.push(metaLine(theme, "ref", details.ref));
|
|
17435
|
+
} else if (mnemopi.state === "resolved") {
|
|
17436
|
+
if (mnemopi.closed) {
|
|
17437
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "mnemopi resolved"));
|
|
17438
|
+
} else {
|
|
17439
|
+
lines.push(statusLabel(theme, "skipped") + " " + theme.fg("muted", "no matching active fact \u2014 nothing changed"));
|
|
17440
|
+
}
|
|
17441
|
+
if (expanded && typeof details.ref === "string")
|
|
17442
|
+
lines.push(metaLine(theme, "ref", details.ref));
|
|
17443
|
+
} else {
|
|
17444
|
+
const status = mnemopi.state === "failed" ? "failed" : "pending";
|
|
17445
|
+
lines.push(statusLabel(theme, status) + " " + theme.fg("muted", "mnemopi " + (mnemopi.state ?? "done")));
|
|
17446
|
+
if (mnemopi.reason)
|
|
17447
|
+
lines.push(theme.fg("dim", summarizeLine(mnemopi.reason, 140)));
|
|
17448
|
+
}
|
|
17449
|
+
return finish();
|
|
17450
|
+
}
|
|
17451
|
+
const remote = details.remote;
|
|
17452
|
+
if (remote) {
|
|
17453
|
+
const state = remote.state ?? "unknown";
|
|
17454
|
+
if (state === "completed") {
|
|
17455
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "saved & searchable"));
|
|
17456
|
+
} else if (state === "accepted") {
|
|
17457
|
+
lines.push(statusLabel(theme, "pending") + " " + theme.fg("muted", "accepted \u2014 not yet searchable"));
|
|
17458
|
+
} else if (state === "failed") {
|
|
17459
|
+
lines.push(statusLabel(theme, "failed") + " " + theme.fg("muted", "not saved"));
|
|
17460
|
+
} else if (state === "not-configured") {
|
|
17461
|
+
lines.push(statusLabel(theme, "skipped") + " " + theme.fg("muted", "hindsight not configured"));
|
|
17462
|
+
} else if (state === "deleted") {
|
|
17463
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "deleted remotely"));
|
|
17464
|
+
} else if (state === "not-found") {
|
|
17465
|
+
lines.push(statusLabel(theme, "skipped") + " " + theme.fg("muted", "already absent \u2014 nothing deleted"));
|
|
17466
|
+
} else if (state === "pending") {
|
|
17467
|
+
lines.push(statusLabel(theme, "pending") + " " + theme.fg("muted", "delete pending remotely"));
|
|
17468
|
+
} else {
|
|
17469
|
+
lines.push(statusLabel(theme, "pending") + " " + theme.fg("muted", "outcome unknown"));
|
|
17470
|
+
}
|
|
17471
|
+
if (remote.title)
|
|
17472
|
+
lines.push(metaLine(theme, "fact", remote.title));
|
|
17473
|
+
if (typeof details.ref === "string" && expanded) {
|
|
17474
|
+
lines.push(metaLine(theme, "ref", details.ref));
|
|
17475
|
+
} else if (typeof details.ref === "string" && (state === "completed" || state === "accepted")) {
|
|
17476
|
+
lines.push(expandHint(theme));
|
|
17477
|
+
}
|
|
17478
|
+
if (expanded || state === "unknown" || state === "failed") {
|
|
17479
|
+
if (remote.operationId)
|
|
17480
|
+
lines.push(metaLine(theme, "operation", remote.operationId));
|
|
17481
|
+
if (remote.documentId)
|
|
17482
|
+
lines.push(metaLine(theme, "document", remote.documentId));
|
|
17483
|
+
if (remote.target)
|
|
17484
|
+
lines.push(metaLine(theme, "server", remote.target));
|
|
17485
|
+
}
|
|
17486
|
+
if (remote.reason)
|
|
17487
|
+
lines.push(theme.fg("dim", summarizeLine(remote.reason, 140)));
|
|
17488
|
+
return finish();
|
|
17489
|
+
}
|
|
17490
|
+
return rawFallbackRow(theme, result, expanded);
|
|
17491
|
+
}
|
|
16888
17492
|
function textResult(text, details = undefined) {
|
|
16889
17493
|
return { content: [{ type: "text", text }], details };
|
|
16890
17494
|
}
|
|
@@ -16958,7 +17562,10 @@ Not yet searchable remotely: ` + pending.map((receipt) => receipt.documentId + "
|
|
|
16958
17562
|
}
|
|
16959
17563
|
const kinds = params.kinds?.length ? new Set(params.kinds) : null;
|
|
16960
17564
|
const facts = outcome.facts.filter((fact) => !kinds || !fact.metadata.kind || kinds.has(fact.metadata.kind)).slice(0, params.limit ?? 5);
|
|
16961
|
-
|
|
17565
|
+
const buckets = receiptBuckets(refreshed);
|
|
17566
|
+
const receiptSummary = refreshed.length ? "Prior saves: " + buckets.done + " completed, " + buckets.pending + " pending, " + buckets.failed + " failed, " + buckets.unknown + " unknown." : "";
|
|
17567
|
+
return textResult(formatHindsightFacts(hindsight.target, projectId, facts) + pendingNote + (receiptSummary ? `
|
|
17568
|
+
` + receiptSummary : ""), {
|
|
16962
17569
|
remote: {
|
|
16963
17570
|
state: "ok",
|
|
16964
17571
|
target: describeHindsightTarget(hindsight.target),
|
|
@@ -16966,7 +17573,8 @@ Not yet searchable remotely: ` + pending.map((receipt) => receipt.documentId + "
|
|
|
16966
17573
|
id: fact.id,
|
|
16967
17574
|
documentId: fact.documentId,
|
|
16968
17575
|
type: fact.type,
|
|
16969
|
-
mentionedAt: fact.mentionedAt
|
|
17576
|
+
mentionedAt: fact.mentionedAt,
|
|
17577
|
+
preview: fact.text.replace(/\s+/g, " ").slice(0, 160)
|
|
16970
17578
|
})),
|
|
16971
17579
|
redactions: scrubber.count(),
|
|
16972
17580
|
receipts: refreshed.map((receipt) => ({
|
|
@@ -17085,13 +17693,20 @@ Destination:
|
|
|
17085
17693
|
ref: "hindsight:" + memoryId + "@" + digest,
|
|
17086
17694
|
remote: "receipt" in outcome && outcome.receipt ? {
|
|
17087
17695
|
state: outcome.state,
|
|
17696
|
+
title,
|
|
17088
17697
|
documentId: outcome.receipt.documentId,
|
|
17089
17698
|
operationId: outcome.receipt.operationId,
|
|
17090
17699
|
target: describeHindsightTarget(hindsight.target),
|
|
17091
17700
|
..."reason" in outcome ? { reason: outcome.reason } : {}
|
|
17092
17701
|
} : outcome
|
|
17093
17702
|
};
|
|
17094
|
-
|
|
17703
|
+
let text = describeRemoteSave(outcome);
|
|
17704
|
+
if ("receipt" in outcome && outcome.receipt && (outcome.state === "completed" || outcome.state === "accepted")) {
|
|
17705
|
+
text += `
|
|
17706
|
+
Title: ` + title + `
|
|
17707
|
+
Ref: hindsight:` + memoryId + "@" + digest + (outcome.state === "accepted" ? " (accepted, NOT yet searchable; resolves only after the server completes indexing)" : "");
|
|
17708
|
+
}
|
|
17709
|
+
return textResult(text, details);
|
|
17095
17710
|
}
|
|
17096
17711
|
async function resolveMemoryByRef(params, signal, ctx, config, projectId) {
|
|
17097
17712
|
const ref = params.ref ? parseMemoryRef(params.ref) : null;
|
|
@@ -17210,6 +17825,9 @@ function resolveModels(ctx, primary, config, explicit = false) {
|
|
|
17210
17825
|
return { segModel, sumModel, verifyModel };
|
|
17211
17826
|
}
|
|
17212
17827
|
|
|
17828
|
+
// src/app/register-smart-compact-tool.ts
|
|
17829
|
+
import { Text as Text5 } from "@earendil-works/pi-tui";
|
|
17830
|
+
|
|
17213
17831
|
// src/ui/dashboard-format.ts
|
|
17214
17832
|
function dashboardPageSize(rows = process.stdout.rows) {
|
|
17215
17833
|
if (!rows)
|
|
@@ -17555,16 +18173,16 @@ function statusClass(status) {
|
|
|
17555
18173
|
return "warn";
|
|
17556
18174
|
return "good";
|
|
17557
18175
|
}
|
|
17558
|
-
function
|
|
18176
|
+
function statusLabel2(status) {
|
|
17559
18177
|
return status ?? "success";
|
|
17560
18178
|
}
|
|
17561
18179
|
function badge(status) {
|
|
17562
|
-
const label =
|
|
18180
|
+
const label = statusLabel2(status);
|
|
17563
18181
|
return `<span class="badge ${statusClass(label)}">${escapeHtml(label)}</span>`;
|
|
17564
18182
|
}
|
|
17565
18183
|
function summarizeDashboard(entries) {
|
|
17566
18184
|
const durations = entries.map(metricDuration).filter(Boolean);
|
|
17567
|
-
const success = entries.filter((e) =>
|
|
18185
|
+
const success = entries.filter((e) => statusLabel2(e.status) === "success").length;
|
|
17568
18186
|
const timeout = entries.filter((e) => e.status === "timeout").length;
|
|
17569
18187
|
const error = entries.filter((e) => e.status === "error" && e.failureKind !== "cancelled").length;
|
|
17570
18188
|
const dryRun = entries.filter((e) => e.status === "dry-run").length;
|
|
@@ -18075,7 +18693,7 @@ function registerSmartCompactTool(pi, dependencies) {
|
|
|
18075
18693
|
pi.registerTool({
|
|
18076
18694
|
name: "smart_compact",
|
|
18077
18695
|
label: "Smart Compact",
|
|
18078
|
-
description: "Prepares and stages a verified summary for the next /compact (
|
|
18696
|
+
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.",
|
|
18079
18697
|
parameters: {
|
|
18080
18698
|
type: "object",
|
|
18081
18699
|
properties: {
|
|
@@ -18121,6 +18739,19 @@ function registerSmartCompactTool(pi, dependencies) {
|
|
|
18121
18739
|
}
|
|
18122
18740
|
}
|
|
18123
18741
|
},
|
|
18742
|
+
renderCall(args, theme) {
|
|
18743
|
+
const label = theme.fg("toolTitle", "smart_compact ");
|
|
18744
|
+
const flags = [
|
|
18745
|
+
safeArg(args.mode, 20),
|
|
18746
|
+
args.dry_run === true ? "dry-run" : "",
|
|
18747
|
+
args.report === true ? "report" : "",
|
|
18748
|
+
args.dashboard === true ? "dashboard" : ""
|
|
18749
|
+
].filter(Boolean).join(" ");
|
|
18750
|
+
return new Text5(label + theme.fg("muted", flags || "auto"), 0, 0);
|
|
18751
|
+
},
|
|
18752
|
+
renderResult(result, { expanded }, theme, context) {
|
|
18753
|
+
return tryRow(theme, () => renderCompactRow(result, expanded, theme, context), result, expanded);
|
|
18754
|
+
},
|
|
18124
18755
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
18125
18756
|
if (!policy.isAgentToolEnabled()) {
|
|
18126
18757
|
return textResult2("smart_compact is hidden from the agent for this session. The user can still run /smart-compact manually.");
|
|
@@ -18144,13 +18775,13 @@ function registerSmartCompactTool(pi, dependencies) {
|
|
|
18144
18775
|
const dashboard = action === "dashboard" ? writeMetricsDashboard() : null;
|
|
18145
18776
|
return textResult2(report + (dashboard ? `
|
|
18146
18777
|
|
|
18147
|
-
Dashboard: ` + dashboard : ""));
|
|
18778
|
+
Dashboard: ` + dashboard : ""), { display: { kind: "metrics", dashboard: dashboard !== null } });
|
|
18148
18779
|
}
|
|
18149
18780
|
const config = loadConfig();
|
|
18150
18781
|
const resolvedMode = mode ?? config.mode;
|
|
18151
18782
|
const sessionId = resolveSessionId(ctx);
|
|
18152
18783
|
if (!dryRun && pendingRef.peek(sessionId)) {
|
|
18153
|
-
return textResult2("A smart summary is already staged; context is unchanged. Run /compact before the
|
|
18784
|
+
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.");
|
|
18154
18785
|
}
|
|
18155
18786
|
const usage = ctx.getContextUsage?.();
|
|
18156
18787
|
const totalTokens = usage?.tokens ?? 0;
|
|
@@ -18194,10 +18825,19 @@ Dashboard: ` + dashboard : ""));
|
|
|
18194
18825
|
content: [
|
|
18195
18826
|
{
|
|
18196
18827
|
type: "text",
|
|
18197
|
-
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 " +
|
|
18828
|
+
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."
|
|
18198
18829
|
}
|
|
18199
18830
|
],
|
|
18200
|
-
details:
|
|
18831
|
+
details: {
|
|
18832
|
+
...staged.details,
|
|
18833
|
+
display: {
|
|
18834
|
+
state: "staged",
|
|
18835
|
+
method: staged.details.method,
|
|
18836
|
+
mode: staged.details.mode ?? staged.details.profile ?? resolvedMode,
|
|
18837
|
+
tokens: staged.tokensBefore ?? 0,
|
|
18838
|
+
ttlMinutes: config.pendingTtlMs / 60000
|
|
18839
|
+
}
|
|
18840
|
+
}
|
|
18201
18841
|
};
|
|
18202
18842
|
}
|
|
18203
18843
|
if (outcome.kind === "dry-run") {
|
|
@@ -18209,13 +18849,16 @@ Dashboard: ` + dashboard : ""));
|
|
|
18209
18849
|
text: "Dry run finished (" + resolvedMode + ", " + seconds + "s). Pipeline ran successfully; no summary was staged."
|
|
18210
18850
|
}
|
|
18211
18851
|
],
|
|
18212
|
-
details:
|
|
18852
|
+
details: {
|
|
18853
|
+
...outcome.details,
|
|
18854
|
+
display: { state: "dry-run", mode: resolvedMode, seconds: Number(seconds) }
|
|
18855
|
+
}
|
|
18213
18856
|
};
|
|
18214
18857
|
}
|
|
18215
18858
|
if (outcome.kind === "cancelled") {
|
|
18216
|
-
return textResult2("Smart compact cancelled by " + outcome.source + "; no summary was staged.");
|
|
18859
|
+
return textResult2("Smart compact cancelled by " + outcome.source + "; no summary was staged.", { display: { state: "cancelled", source: outcome.source } });
|
|
18217
18860
|
}
|
|
18218
|
-
return textResult2("Smart compact skipped: " + outcome.reason.replace(/-/g, " ") + ". No summary was staged.");
|
|
18861
|
+
return textResult2("Smart compact skipped: " + outcome.reason.replace(/-/g, " ") + ". No summary was staged.", { display: { state: "skipped" } });
|
|
18219
18862
|
} catch (error) {
|
|
18220
18863
|
recordIssue({ key: "tool.smart-compact", message: "smart_compact failed: " + errorDetail(error) + ".", error });
|
|
18221
18864
|
throw new Error(formatCompactErrorForUi(error));
|
|
@@ -18223,13 +18866,42 @@ Dashboard: ` + dashboard : ""));
|
|
|
18223
18866
|
}
|
|
18224
18867
|
});
|
|
18225
18868
|
}
|
|
18226
|
-
function textResult2(text) {
|
|
18227
|
-
return { content: [{ type: "text", text }], details
|
|
18869
|
+
function textResult2(text, details = undefined) {
|
|
18870
|
+
return { content: [{ type: "text", text }], details };
|
|
18871
|
+
}
|
|
18872
|
+
function renderCompactRow(result, expanded, theme, context) {
|
|
18873
|
+
if (context.isError) {
|
|
18874
|
+
const head = theme.fg("error", "smart_compact failed:");
|
|
18875
|
+
return expanded ? expandedRow(theme, result, [head]) : new Text5(head + " " + summarizeLine(firstTextContent(result.content), 140), 0, 0);
|
|
18876
|
+
}
|
|
18877
|
+
const display = result.details?.display;
|
|
18878
|
+
if (!display)
|
|
18879
|
+
return rawFallbackRow(theme, result, expanded);
|
|
18880
|
+
const lines = [];
|
|
18881
|
+
if (display.state === "staged") {
|
|
18882
|
+
lines.push(statusLabel(theme, "pending") + " " + theme.fg("muted", "staged \u2014 NOT applied; run /compact within " + (Number(display.ttlMinutes) || 0) + " min"));
|
|
18883
|
+
lines.push(metaLine(theme, "summary", (display.method === "native" ? "native" : String(display.mode ?? "")) + " \xB7 " + (Number(display.tokens) || 0).toLocaleString() + " tokens"));
|
|
18884
|
+
} else if (display.state === "dry-run") {
|
|
18885
|
+
lines.push(statusLabel(theme, "info") + " " + theme.fg("muted", "dry run \u2014 nothing staged"));
|
|
18886
|
+
if (display.seconds)
|
|
18887
|
+
lines.push(metaLine(theme, "pipeline", display.seconds + "s"));
|
|
18888
|
+
} else if (display.state === "cancelled") {
|
|
18889
|
+
lines.push(statusLabel(theme, "cancelled") + " " + theme.fg("muted", "by " + String(display.source ?? "host")));
|
|
18890
|
+
} else if (display.state === "skipped") {
|
|
18891
|
+
lines.push(statusLabel(theme, "skipped") + " " + theme.fg("dim", summarizeLine(firstTextContent(result.content), 140)));
|
|
18892
|
+
} else if (display.kind === "metrics") {
|
|
18893
|
+
lines.push(statusLabel(theme, "info") + " " + theme.fg("muted", "metrics report" + (display.dashboard ? " (dashboard written)" : "")));
|
|
18894
|
+
} else {
|
|
18895
|
+
return rawFallbackRow(theme, result, expanded);
|
|
18896
|
+
}
|
|
18897
|
+
return expanded ? expandedRow(theme, result, lines) : new Text5(lines.join(`
|
|
18898
|
+
`), 0, 0);
|
|
18228
18899
|
}
|
|
18229
18900
|
|
|
18230
18901
|
// src/app/register-smart-context-tool.ts
|
|
18231
18902
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
18232
18903
|
import { StringEnum as StringEnum2 } from "@earendil-works/pi-ai";
|
|
18904
|
+
import { Text as Text6 } from "@earendil-works/pi-tui";
|
|
18233
18905
|
import { Type as Type4 } from "typebox";
|
|
18234
18906
|
|
|
18235
18907
|
// src/app/context-evidence.ts
|
|
@@ -18477,11 +19149,11 @@ function registerSmartContextTool(pi, options = {}) {
|
|
|
18477
19149
|
const current = config();
|
|
18478
19150
|
return new SecretScrubber(current.scrubSecrets, current.scrubPii).scrubText(text).value;
|
|
18479
19151
|
};
|
|
18480
|
-
const reply = (text) => ({ content: [{ type: "text", text }], details
|
|
19152
|
+
const reply = (text, details = undefined) => ({ content: [{ type: "text", text }], details });
|
|
18481
19153
|
pi.registerTool({
|
|
18482
19154
|
name: TOOL_NAME,
|
|
18483
19155
|
label: "Session Context",
|
|
18484
|
-
description: "
|
|
19156
|
+
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.",
|
|
18485
19157
|
parameters: Type4.Object({
|
|
18486
19158
|
action: StringEnum2(["status", "plan", "checkpoint", "rewind", "trim", "read", "search"]),
|
|
18487
19159
|
label: Type4.Optional(Type4.String({ maxLength: 120, description: "Checkpoint label." })),
|
|
@@ -18505,15 +19177,28 @@ function registerSmartContextTool(pi, options = {}) {
|
|
|
18505
19177
|
const branch = ctx.sessionManager.getBranch();
|
|
18506
19178
|
const state = inspectContext(branch, sessionId);
|
|
18507
19179
|
if (params.action === "plan") {
|
|
18508
|
-
const plan = planContextTrim(branch, state.checkpoint?.originId);
|
|
18509
|
-
|
|
19180
|
+
const plan = planContextTrim(branch, state.checkpoint?.originId, { readerApi: ctx.model?.api });
|
|
19181
|
+
const payload = {
|
|
19182
|
+
pressure: contextPressure(ctx, config()),
|
|
19183
|
+
blockedReason: plan.blockedReason,
|
|
18510
19184
|
outputs: plan.references.length,
|
|
18511
19185
|
superseded: plan.superseded,
|
|
18512
19186
|
savedChars: plan.savedChars,
|
|
18513
19187
|
batch: plan.automatic,
|
|
18514
19188
|
cooldownTurns: plan.cooldownTurns,
|
|
18515
|
-
note: "No changes applied. Automatic
|
|
18516
|
-
}
|
|
19189
|
+
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."
|
|
19190
|
+
};
|
|
19191
|
+
return reply(JSON.stringify(payload), {
|
|
19192
|
+
display: {
|
|
19193
|
+
kind: "plan",
|
|
19194
|
+
pressurePercent: payload.pressure?.percent == null ? null : Math.round(payload.pressure.percent),
|
|
19195
|
+
outputs: payload.outputs,
|
|
19196
|
+
superseded: payload.superseded,
|
|
19197
|
+
savedChars: payload.savedChars,
|
|
19198
|
+
batchReady: payload.batch === "ready",
|
|
19199
|
+
batchReason: payload.blockedReason ?? payload.batch ?? null
|
|
19200
|
+
}
|
|
19201
|
+
});
|
|
18517
19202
|
}
|
|
18518
19203
|
const offset = params.offset ?? 0;
|
|
18519
19204
|
if (params.action === "status" || params.action === "read" || params.action === "search") {
|
|
@@ -18530,7 +19215,20 @@ function registerSmartContextTool(pi, options = {}) {
|
|
|
18530
19215
|
const evidence = contextEvidence(branch, sessionId, new SecretScrubber(settings.scrubSecrets, settings.scrubPii), lineage);
|
|
18531
19216
|
if (params.action === "status") {
|
|
18532
19217
|
const sources = evidence.list.slice(offset, offset + limit);
|
|
18533
|
-
|
|
19218
|
+
const plan = planContextTrim(branch, state.checkpoint?.originId, { readerApi: ctx.model?.api });
|
|
19219
|
+
const pressure = contextPressure(ctx, settings);
|
|
19220
|
+
const cleanup = {
|
|
19221
|
+
enabled: settings.contextHygieneEnabled || settings.autoTrigger && settings.autoTriggerStrategy === "background",
|
|
19222
|
+
pressureOnly: settings.contextPressureOnly,
|
|
19223
|
+
maintenanceAvailable: options.canAutoTrim?.(ctx) !== false,
|
|
19224
|
+
batch: plan.automatic,
|
|
19225
|
+
savedChars: plan.savedChars,
|
|
19226
|
+
blockedReason: plan.blockedReason,
|
|
19227
|
+
agentAllowed: options.canAgentMutate?.(ctx) !== false
|
|
19228
|
+
};
|
|
19229
|
+
const payload = {
|
|
19230
|
+
pressure,
|
|
19231
|
+
cleanup,
|
|
18534
19232
|
checkpoint: state.checkpoint ? { id: state.checkpoint.id, label: state.checkpoint.label } : null,
|
|
18535
19233
|
rewind: Boolean(state.checkpoint),
|
|
18536
19234
|
reason: state.invalidReason,
|
|
@@ -18546,7 +19244,21 @@ function registerSmartContextTool(pi, options = {}) {
|
|
|
18546
19244
|
sources: evidence.list.filter((source) => source.session === parent.sessionId).length
|
|
18547
19245
|
})) } : {},
|
|
18548
19246
|
nextOffset: offset + limit < evidence.list.length ? offset + limit : null
|
|
18549
|
-
}
|
|
19247
|
+
};
|
|
19248
|
+
return reply(scrub(JSON.stringify(payload)), {
|
|
19249
|
+
display: {
|
|
19250
|
+
kind: "status",
|
|
19251
|
+
pressurePercent: pressure.percent == null ? null : Math.round(pressure.percent),
|
|
19252
|
+
cleanupEnabled: cleanup.enabled,
|
|
19253
|
+
cleanupBatchReady: plan.automatic === "ready",
|
|
19254
|
+
cleanupBatchReason: plan.automatic === "ready" ? null : plan.automatic,
|
|
19255
|
+
cleanupBlockedReason: cleanup.blockedReason ?? null,
|
|
19256
|
+
checkpoint: payload.checkpoint?.label ?? null,
|
|
19257
|
+
pending: payload.pending ?? null,
|
|
19258
|
+
archivedOutputs: payload.archivedOutputs,
|
|
19259
|
+
nextOffset: payload.nextOffset
|
|
19260
|
+
}
|
|
19261
|
+
});
|
|
18550
19262
|
}
|
|
18551
19263
|
if (params.action === "search") {
|
|
18552
19264
|
if (!params.query)
|
|
@@ -18567,6 +19279,12 @@ ${page.text}`);
|
|
|
18567
19279
|
if (options.canAgentMutate?.(ctx) === false) {
|
|
18568
19280
|
throw new Error("Agent-requested context changes are disabled by policy; status, plan, search and read stay available.");
|
|
18569
19281
|
}
|
|
19282
|
+
if (params.action !== "checkpoint" && config().contextPressureOnly && !contextPressure(ctx, config()).cleanup) {
|
|
19283
|
+
throw new Error("No context pressure (or usage is unavailable); history left unchanged. Check status. Human commands can request early cleanup.");
|
|
19284
|
+
}
|
|
19285
|
+
if (params.action !== "checkpoint" && options.canAutoTrim?.(ctx) === false) {
|
|
19286
|
+
throw new Error("Prepared/running compaction or unavailable recovery tools take priority; history left unchanged.");
|
|
19287
|
+
}
|
|
18570
19288
|
if (options.isPaused?.(ctx))
|
|
18571
19289
|
throw new Error("Context changes are paused while a navigation pivot is pending.");
|
|
18572
19290
|
if (queued)
|
|
@@ -18593,7 +19311,21 @@ ${page.text}`);
|
|
|
18593
19311
|
text: params.action === "rewind" ? text : text.slice(0, 120),
|
|
18594
19312
|
signal
|
|
18595
19313
|
};
|
|
18596
|
-
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."}
|
|
19314
|
+
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."}`, {
|
|
19315
|
+
display: { state: "queued", action: params.action, label: text.slice(0, 120) }
|
|
19316
|
+
});
|
|
19317
|
+
},
|
|
19318
|
+
renderCall(args, theme) {
|
|
19319
|
+
const label = theme.fg("toolTitle", "smart_context ");
|
|
19320
|
+
const action = safeArg(args.action, 20);
|
|
19321
|
+
const id = safeArg(args.id, 80);
|
|
19322
|
+
const query = safeArg(args.query, 80);
|
|
19323
|
+
const labelArg = safeArg(args.label, 80);
|
|
19324
|
+
const detail = id ? " " + theme.fg("dim", id) : query ? " " + theme.fg("dim", query) : labelArg ? " " + theme.fg("muted", labelArg) : "";
|
|
19325
|
+
return new Text6(label + theme.fg("accent", action) + detail, 0, 0);
|
|
19326
|
+
},
|
|
19327
|
+
renderResult(result, { expanded }, theme, context) {
|
|
19328
|
+
return tryRow(theme, () => renderSmartContextRow(result, expanded, theme, context), result, expanded);
|
|
18597
19329
|
}
|
|
18598
19330
|
});
|
|
18599
19331
|
const clear = () => {
|
|
@@ -18640,7 +19372,8 @@ ${page.text}`);
|
|
|
18640
19372
|
if (!current) {
|
|
18641
19373
|
if (mark?.sessionId !== sessionId)
|
|
18642
19374
|
return;
|
|
18643
|
-
|
|
19375
|
+
const settings = config();
|
|
19376
|
+
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)) {
|
|
18644
19377
|
mark = null;
|
|
18645
19378
|
return;
|
|
18646
19379
|
}
|
|
@@ -18665,7 +19398,7 @@ ${page.text}`);
|
|
|
18665
19398
|
return changed ? { messages } : undefined;
|
|
18666
19399
|
};
|
|
18667
19400
|
const vetoWarming = (event, ctx, sessionId) => {
|
|
18668
|
-
if (mark?.sessionId !== sessionId || !unchangedSince(ctx.sessionManager.getBranch(), mark.leafId))
|
|
19401
|
+
if (config().contextPressureOnly || mark?.sessionId !== sessionId || !unchangedSince(ctx.sessionManager.getBranch(), mark.leafId) || thinkingEditReason(ctx.sessionManager.getBranch(), mark.entries, ctx.model?.api))
|
|
18669
19402
|
return false;
|
|
18670
19403
|
const cost = ctx.model?.cost;
|
|
18671
19404
|
const price = cost ? cost.cacheWrite > 0 ? cost.cacheWrite : cost.input : NaN;
|
|
@@ -18683,15 +19416,25 @@ ${page.text}`);
|
|
|
18683
19416
|
if (event.outcome !== "completed" || request?.signal?.aborted) {
|
|
18684
19417
|
return request ? cancelled(event, "The turn did not complete.") : undefined;
|
|
18685
19418
|
}
|
|
18686
|
-
|
|
19419
|
+
const requestTool = request?.anchorBoundary !== undefined ? "smart_navigation" : TOOL_NAME;
|
|
19420
|
+
if (request && !request.manual && (config().toolLoading === "off" || !pi.getActiveTools().includes(requestTool)))
|
|
18687
19421
|
return cancelled(event, "Agent tool access was revoked.");
|
|
19422
|
+
if (request?.anchorBoundary !== undefined && !config().contextNavigationEnabled)
|
|
19423
|
+
return cancelled(event, "Navigation was disabled.");
|
|
18688
19424
|
if (options.isPaused?.(ctx)) {
|
|
18689
19425
|
return request ? cancelled(event, "A pending navigation pivot takes priority.") : undefined;
|
|
18690
19426
|
}
|
|
18691
19427
|
if (request && !request.manual && options.canAgentMutate?.(ctx) === false) {
|
|
18692
19428
|
return cancelled(event, "Agent-requested context changes are disabled by policy.");
|
|
18693
19429
|
}
|
|
18694
|
-
|
|
19430
|
+
if (request && !request.manual && request.action !== "checkpoint" && options.canAutoTrim?.(ctx) === false) {
|
|
19431
|
+
return cancelled(event, "Prepared/running compaction or unavailable recovery tools take priority.");
|
|
19432
|
+
}
|
|
19433
|
+
const settings = config();
|
|
19434
|
+
const pressure = contextPressure(ctx, settings).cleanup;
|
|
19435
|
+
if (request && !request.manual && request.action !== "checkpoint" && settings.contextPressureOnly && !pressure) {
|
|
19436
|
+
return cancelled(event, "Context pressure cleared; history left unchanged.");
|
|
19437
|
+
}
|
|
18695
19438
|
if (!request) {
|
|
18696
19439
|
const configNow = config();
|
|
18697
19440
|
const hygiene = configNow.contextHygieneEnabled;
|
|
@@ -18700,11 +19443,11 @@ ${page.text}`);
|
|
|
18700
19443
|
const enabled = hygiene || configNow.autoTrigger && configNow.autoTriggerStrategy === "background";
|
|
18701
19444
|
if (!enabled || options.canAutoTrim?.(ctx) === false)
|
|
18702
19445
|
return;
|
|
18703
|
-
|
|
18704
|
-
|
|
18705
|
-
|
|
18706
|
-
|
|
18707
|
-
|
|
19446
|
+
if ((configNow.contextPressureOnly || !hygiene) && !pressure) {
|
|
19447
|
+
mark = null;
|
|
19448
|
+
if (!applied)
|
|
19449
|
+
return;
|
|
19450
|
+
}
|
|
18708
19451
|
}
|
|
18709
19452
|
const branch = ctx.sessionManager.getBranch();
|
|
18710
19453
|
const sessionId = resolveSessionId(ctx);
|
|
@@ -18735,13 +19478,15 @@ ${page.text}`);
|
|
|
18735
19478
|
}
|
|
18736
19479
|
})];
|
|
18737
19480
|
} else if (request?.action === "rewind") {
|
|
18738
|
-
entries = planContextRewind(branch, sessionId, request.checkpointId, request.text).entries;
|
|
19481
|
+
entries = planContextRewind(branch, sessionId, request.checkpointId, request.text, ctx.model?.api).entries;
|
|
18739
19482
|
} else if (!request && applied?.sessionId === sessionId && unchangedSince(branch, applied.leafId)) {
|
|
18740
19483
|
mark = null;
|
|
18741
19484
|
entries = applied.entries;
|
|
18742
19485
|
applied = null;
|
|
18743
19486
|
} else {
|
|
18744
|
-
const plan = planContextTrim(branch, state.checkpoint?.originId);
|
|
19487
|
+
const plan = planContextTrim(branch, state.checkpoint?.originId, { anchorBoundary: request?.anchorBoundary, readerApi: ctx.model?.api });
|
|
19488
|
+
if (plan.blockedReason)
|
|
19489
|
+
return request ? cancelled(event, plan.blockedReason) : undefined;
|
|
18745
19490
|
if (request) {
|
|
18746
19491
|
entries = trimEntries(plan, request.manual ? "manual" : "agent");
|
|
18747
19492
|
} else {
|
|
@@ -18782,41 +19527,54 @@ ${page.text}`);
|
|
|
18782
19527
|
}
|
|
18783
19528
|
return { entries: [...event.entries, ...entries] };
|
|
18784
19529
|
});
|
|
18785
|
-
const
|
|
18786
|
-
|
|
18787
|
-
|
|
18788
|
-
|
|
18789
|
-
|
|
18790
|
-
|
|
18791
|
-
|
|
18792
|
-
|
|
18793
|
-
|
|
18794
|
-
|
|
18795
|
-
|
|
18796
|
-
|
|
18797
|
-
|
|
18798
|
-
|
|
18799
|
-
|
|
18800
|
-
|
|
18801
|
-
|
|
18802
|
-
|
|
18803
|
-
|
|
18804
|
-
|
|
18805
|
-
|
|
18806
|
-
|
|
18807
|
-
|
|
18808
|
-
|
|
18809
|
-
|
|
18810
|
-
|
|
18811
|
-
|
|
18812
|
-
|
|
18813
|
-
|
|
18814
|
-
|
|
18815
|
-
|
|
18816
|
-
|
|
19530
|
+
const requestTrim = (ctx, anchor) => {
|
|
19531
|
+
const sessionId = resolveSessionId(ctx);
|
|
19532
|
+
if (isUnresolvedSessionId(sessionId)) {
|
|
19533
|
+
return { state: "unavailable", notice: "Context control needs an identifiable session." };
|
|
19534
|
+
}
|
|
19535
|
+
if (options.isPaused?.(ctx)) {
|
|
19536
|
+
return { state: "paused", notice: "A navigation pivot is pending; try again after it finishes." };
|
|
19537
|
+
}
|
|
19538
|
+
if (queued) {
|
|
19539
|
+
return { state: "busy", notice: "Another context change is already queued; it applies at the next turn boundary." };
|
|
19540
|
+
}
|
|
19541
|
+
if (anchor && options.canAutoTrim?.(ctx) === false) {
|
|
19542
|
+
return { state: "busy", notice: "Cleanup deferred: prepared/running compaction or unavailable recovery tools take priority." };
|
|
19543
|
+
}
|
|
19544
|
+
if (anchor?.callId && (options.canAgentMutate?.(ctx) === false || config().contextPressureOnly && !contextPressure(ctx, config()).cleanup)) {
|
|
19545
|
+
return { state: "unavailable", notice: "Cleanup not requested: no context pressure or agent changes are disabled." };
|
|
19546
|
+
}
|
|
19547
|
+
const branch = ctx.sessionManager.getBranch();
|
|
19548
|
+
const origin = anchor ? branch.findIndex((entry) => entry.id === anchor.originId) : -1;
|
|
19549
|
+
if (anchor && origin < 0)
|
|
19550
|
+
return { state: "unavailable", notice: "Anchor origin is no longer on this branch." };
|
|
19551
|
+
const anchorBoundary = anchor ? lastAnchorBoundary(branch.slice(0, origin + 1)) ?? null : undefined;
|
|
19552
|
+
const plan = planContextTrim(branch, inspectContext(branch, sessionId).checkpoint?.originId, { anchorBoundary, readerApi: ctx.model?.api });
|
|
19553
|
+
if (!plan.entries.length || anchor?.callId && plan.automatic !== "ready") {
|
|
19554
|
+
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.") };
|
|
19555
|
+
}
|
|
19556
|
+
mark = null;
|
|
19557
|
+
queued = {
|
|
19558
|
+
action: "trim",
|
|
19559
|
+
callId: anchor?.callId ?? "",
|
|
19560
|
+
sessionId,
|
|
19561
|
+
originId: anchor?.originId ?? branch.at(-1)?.id ?? "",
|
|
19562
|
+
checkpointId: "",
|
|
19563
|
+
text: "",
|
|
19564
|
+
manual: !anchor?.callId,
|
|
19565
|
+
anchorBoundary,
|
|
19566
|
+
signal: anchor?.signal
|
|
19567
|
+
};
|
|
19568
|
+
return {
|
|
19569
|
+
state: "queued",
|
|
19570
|
+
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."
|
|
19571
|
+
};
|
|
19572
|
+
};
|
|
19573
|
+
return {
|
|
19574
|
+
requestManualTrim: (ctx) => requestTrim(ctx),
|
|
19575
|
+
requestAnchorTrim: (ctx, originId, callId, signal) => requestTrim(ctx, { originId, callId, signal }),
|
|
18817
19576
|
deferredTrim: deferred
|
|
18818
19577
|
};
|
|
18819
|
-
return controller;
|
|
18820
19578
|
}
|
|
18821
19579
|
function cancelled(event, reason) {
|
|
18822
19580
|
return {
|
|
@@ -18828,6 +19586,48 @@ function cancelled(event, reason) {
|
|
|
18828
19586
|
}]
|
|
18829
19587
|
};
|
|
18830
19588
|
}
|
|
19589
|
+
function renderSmartContextRow(result, expanded, theme, context) {
|
|
19590
|
+
if (context.isError) {
|
|
19591
|
+
const head = theme.fg("error", "smart_context failed:");
|
|
19592
|
+
return expanded ? expandedRow(theme, result, [head]) : new Text6(head + " " + summarizeLine(firstTextContent(result.content), 140), 0, 0);
|
|
19593
|
+
}
|
|
19594
|
+
const display = result.details?.display;
|
|
19595
|
+
if (!display)
|
|
19596
|
+
return rawFallbackRow(theme, result, expanded);
|
|
19597
|
+
const lines = [];
|
|
19598
|
+
if (display.state === "queued") {
|
|
19599
|
+
lines.push(statusLabel(theme, "queued") + " " + theme.fg("muted", String(display.action ?? "change") + " queued \u2014 applies at the turn boundary, not yet applied"));
|
|
19600
|
+
if (typeof display.label === "string" && display.label) {
|
|
19601
|
+
lines.push(theme.fg("dim", summarizeLine(display.label, 120)));
|
|
19602
|
+
}
|
|
19603
|
+
return new Text6(lines.join(`
|
|
19604
|
+
`), 0, 0);
|
|
19605
|
+
}
|
|
19606
|
+
if (display.kind === "status") {
|
|
19607
|
+
const percent = display.pressurePercent;
|
|
19608
|
+
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"));
|
|
19609
|
+
lines.push(metaLine(theme, "archived outputs", String(display.archivedOutputs ?? 0)));
|
|
19610
|
+
if (display.checkpoint)
|
|
19611
|
+
lines.push(metaLine(theme, "checkpoint", String(display.checkpoint)));
|
|
19612
|
+
if (display.pending)
|
|
19613
|
+
lines.push(metaLine(theme, "pending", String(display.pending)));
|
|
19614
|
+
if (display.cleanupBlockedReason)
|
|
19615
|
+
lines.push(theme.fg("dim", summarizeLine(String(display.cleanupBlockedReason), 120)));
|
|
19616
|
+
return expanded ? expandedRow(theme, result, lines) : new Text6(lines.join(`
|
|
19617
|
+
`), 0, 0);
|
|
19618
|
+
}
|
|
19619
|
+
if (display.kind === "plan") {
|
|
19620
|
+
const percent = display.pressurePercent;
|
|
19621
|
+
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"));
|
|
19622
|
+
if (display.batchReady)
|
|
19623
|
+
lines.push(metaLine(theme, "batch", "ready"));
|
|
19624
|
+
else if (display.batchReason)
|
|
19625
|
+
lines.push(metaLine(theme, "batch", String(display.batchReason)));
|
|
19626
|
+
return expanded ? expandedRow(theme, result, lines) : new Text6(lines.join(`
|
|
19627
|
+
`), 0, 0);
|
|
19628
|
+
}
|
|
19629
|
+
return rawFallbackRow(theme, result, expanded);
|
|
19630
|
+
}
|
|
18831
19631
|
|
|
18832
19632
|
// src/app/model-feasibility.ts
|
|
18833
19633
|
async function createModelFeasibilityResolver(ctx, config, requestedMode = config.mode, steering = {}) {
|
|
@@ -19241,7 +20041,7 @@ import {
|
|
|
19241
20041
|
getKeybindings as getKeybindings2,
|
|
19242
20042
|
Key as Key4,
|
|
19243
20043
|
matchesKey as matchesKey4,
|
|
19244
|
-
Text as
|
|
20044
|
+
Text as Text9,
|
|
19245
20045
|
wrapTextWithAnsi as wrapTextWithAnsi3
|
|
19246
20046
|
} from "@earendil-works/pi-tui";
|
|
19247
20047
|
|
|
@@ -19250,7 +20050,7 @@ import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
|
19250
20050
|
import {
|
|
19251
20051
|
getKeybindings,
|
|
19252
20052
|
SettingsList,
|
|
19253
|
-
truncateToWidth as
|
|
20053
|
+
truncateToWidth as truncateToWidth4,
|
|
19254
20054
|
visibleWidth as visibleWidth2,
|
|
19255
20055
|
wrapTextWithAnsi as wrapTextWithAnsi2
|
|
19256
20056
|
} from "@earendil-works/pi-tui";
|
|
@@ -19296,8 +20096,8 @@ class SmartSettingsList extends SettingsList {
|
|
|
19296
20096
|
internals.items.forEach((item, index) => {
|
|
19297
20097
|
const original = saved[index];
|
|
19298
20098
|
const shown = (this.inactive.has(item.id) ? INACTIVE_PREFIX : "") + original.currentValue;
|
|
19299
|
-
item.label =
|
|
19300
|
-
item.currentValue =
|
|
20099
|
+
item.label = truncateToWidth4(original.label, labelWidth, "\u2026");
|
|
20100
|
+
item.currentValue = truncateToWidth4(shown, valueWidth, "\u2026");
|
|
19301
20101
|
if (index === internals.selectedIndex) {
|
|
19302
20102
|
item.description = [
|
|
19303
20103
|
item.label !== original.label ? original.label : "",
|
|
@@ -19338,7 +20138,7 @@ class SmartSettingsList extends SettingsList {
|
|
|
19338
20138
|
// src/ui/settings-overlay.ts
|
|
19339
20139
|
import {
|
|
19340
20140
|
Container as Container4,
|
|
19341
|
-
Text as
|
|
20141
|
+
Text as Text8
|
|
19342
20142
|
} from "@earendil-works/pi-tui";
|
|
19343
20143
|
|
|
19344
20144
|
// src/ui/settings-complex.ts
|
|
@@ -19349,7 +20149,7 @@ import {
|
|
|
19349
20149
|
Key as Key3,
|
|
19350
20150
|
matchesKey as matchesKey3,
|
|
19351
20151
|
SelectList as SelectList3,
|
|
19352
|
-
Text as
|
|
20152
|
+
Text as Text7
|
|
19353
20153
|
} from "@earendil-works/pi-tui";
|
|
19354
20154
|
var MODEL_SETTINGS = [
|
|
19355
20155
|
{
|
|
@@ -19397,7 +20197,7 @@ var MIN_CONTEXT_SETTING = {
|
|
|
19397
20197
|
var MAX_CONTEXT_SETTING = {
|
|
19398
20198
|
id: "maxContextTokens",
|
|
19399
20199
|
label: "Context cap for start % (tokens)",
|
|
19400
|
-
description: "
|
|
20200
|
+
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.",
|
|
19401
20201
|
placeholder: "0 or 16384\u20132000000; blank = off",
|
|
19402
20202
|
parse: numberParser(CONFIG_NUMERIC_LIMITS.maxContextTokens),
|
|
19403
20203
|
format: (value) => value === undefined || value === 0 ? "off" : String(value)
|
|
@@ -19405,8 +20205,8 @@ var MAX_CONTEXT_SETTING = {
|
|
|
19405
20205
|
var parsePreparePercent = numberParser(CONFIG_NUMERIC_LIMITS.prepareContextPercent);
|
|
19406
20206
|
var PREPARE_CONTEXT_SETTING = {
|
|
19407
20207
|
id: "prepareContextPercent",
|
|
19408
|
-
label: "
|
|
19409
|
-
description: "
|
|
20208
|
+
label: "Cleanup / prepare at context %",
|
|
20209
|
+
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.",
|
|
19410
20210
|
placeholder: "0\u2013100 below Start at; auto or blank = Auto",
|
|
19411
20211
|
parse: (input) => input.trim().toLowerCase() === "auto" ? undefined : parsePreparePercent(input),
|
|
19412
20212
|
format: (value) => value === undefined || value === null ? "Auto" : String(value)
|
|
@@ -19615,7 +20415,7 @@ class InputSettingEditor extends Container3 {
|
|
|
19615
20415
|
save;
|
|
19616
20416
|
done;
|
|
19617
20417
|
input = new Input;
|
|
19618
|
-
status = new
|
|
20418
|
+
status = new Text7("", 0, 0);
|
|
19619
20419
|
saving = false;
|
|
19620
20420
|
pending = Promise.resolve();
|
|
19621
20421
|
get focused() {
|
|
@@ -19629,10 +20429,10 @@ class InputSettingEditor extends Container3 {
|
|
|
19629
20429
|
this.requestRender = requestRender;
|
|
19630
20430
|
this.save = save;
|
|
19631
20431
|
this.done = done;
|
|
19632
|
-
this.addChild(new
|
|
19633
|
-
this.addChild(new
|
|
19634
|
-
this.addChild(new
|
|
19635
|
-
this.addChild(new
|
|
20432
|
+
this.addChild(new Text7(setting.label, 0, 0));
|
|
20433
|
+
this.addChild(new Text7(setting.description, 0, 0));
|
|
20434
|
+
this.addChild(new Text7(`Allowed: ${setting.placeholder}`, 0, 0));
|
|
20435
|
+
this.addChild(new Text7("", 0, 0));
|
|
19636
20436
|
this.input.setValue(initial);
|
|
19637
20437
|
this.input.handleInput("\x1B[F");
|
|
19638
20438
|
this.input.onSubmit = (value) => {
|
|
@@ -19660,9 +20460,9 @@ class InputSettingEditor extends Container3 {
|
|
|
19660
20460
|
this.done();
|
|
19661
20461
|
};
|
|
19662
20462
|
this.addChild(this.input);
|
|
19663
|
-
this.addChild(new
|
|
20463
|
+
this.addChild(new Text7("", 0, 0));
|
|
19664
20464
|
this.addChild(this.status);
|
|
19665
|
-
this.addChild(new
|
|
20465
|
+
this.addChild(new Text7("Enter save \xB7 Esc cancel", 0, 0));
|
|
19666
20466
|
}
|
|
19667
20467
|
handleInput(data) {
|
|
19668
20468
|
this.input.handleInput(data);
|
|
@@ -19677,7 +20477,7 @@ class ModelSettingEditor extends Container3 {
|
|
|
19677
20477
|
requestRender;
|
|
19678
20478
|
search = new Input;
|
|
19679
20479
|
list;
|
|
19680
|
-
status = new
|
|
20480
|
+
status = new Text7("", 0, 0);
|
|
19681
20481
|
saving = false;
|
|
19682
20482
|
pending = Promise.resolve();
|
|
19683
20483
|
get focused() {
|
|
@@ -19689,9 +20489,9 @@ class ModelSettingEditor extends Container3 {
|
|
|
19689
20489
|
constructor(models, selectedValue, requestRender, save, done) {
|
|
19690
20490
|
super();
|
|
19691
20491
|
this.requestRender = requestRender;
|
|
19692
|
-
this.addChild(new
|
|
20492
|
+
this.addChild(new Text7("Type to search models", 0, 0));
|
|
19693
20493
|
this.addChild(this.search);
|
|
19694
|
-
this.addChild(new
|
|
20494
|
+
this.addChild(new Text7("", 0, 0));
|
|
19695
20495
|
this.list = new SelectList3(models, 10, {
|
|
19696
20496
|
selectedPrefix: (text) => text,
|
|
19697
20497
|
selectedText: (text) => text,
|
|
@@ -19726,7 +20526,7 @@ class ModelSettingEditor extends Container3 {
|
|
|
19726
20526
|
};
|
|
19727
20527
|
this.addChild(this.list);
|
|
19728
20528
|
this.addChild(this.status);
|
|
19729
|
-
this.addChild(new
|
|
20529
|
+
this.addChild(new Text7("Type to filter \xB7 Enter select \xB7 Esc cancel", 0, 0));
|
|
19730
20530
|
}
|
|
19731
20531
|
handleInput(data) {
|
|
19732
20532
|
if (matchesKey3(data, Key3.up) || matchesKey3(data, Key3.down) || matchesKey3(data, Key3.enter) || matchesKey3(data, Key3.escape)) {
|
|
@@ -19965,7 +20765,6 @@ var THINKING_VALUES = [
|
|
|
19965
20765
|
"max"
|
|
19966
20766
|
];
|
|
19967
20767
|
var needsAutoTrigger = (config) => config.autoTrigger ? undefined : "turn on Automatic compaction first";
|
|
19968
|
-
var needsBackgroundTiming = (config) => config.autoTrigger && config.autoTriggerStrategy === "background" ? undefined : "used only when Start when = Prepare in background";
|
|
19969
20768
|
var needsMnemopi = (config) => config.memoryBackend === "mnemopi" ? undefined : "used only when Memory store = Mnemopi";
|
|
19970
20769
|
var needsHindsight = (config) => config.memoryBackend === "hindsight" ? undefined : "used only when Memory store = Hindsight server";
|
|
19971
20770
|
var needsBackups = (config) => config.backupEnabled ? undefined : "turn on Backups first";
|
|
@@ -19996,9 +20795,9 @@ var CATEGORIES = [
|
|
|
19996
20795
|
labels: { "native-hook": "Before Pi's compaction", settled: "When idle", background: "Prepare in background" }
|
|
19997
20796
|
}
|
|
19998
20797
|
},
|
|
19999
|
-
{ kind: "input", setting: MIN_CONTEXT_SETTING
|
|
20000
|
-
{ kind: "input", setting: MAX_CONTEXT_SETTING
|
|
20001
|
-
{ kind: "input", setting: PREPARE_CONTEXT_SETTING
|
|
20798
|
+
{ kind: "input", setting: MIN_CONTEXT_SETTING },
|
|
20799
|
+
{ kind: "input", setting: MAX_CONTEXT_SETTING },
|
|
20800
|
+
{ kind: "input", setting: PREPARE_CONTEXT_SETTING },
|
|
20002
20801
|
{
|
|
20003
20802
|
kind: "choice",
|
|
20004
20803
|
setting: {
|
|
@@ -20113,7 +20912,7 @@ var CATEGORIES = [
|
|
|
20113
20912
|
setting: {
|
|
20114
20913
|
id: "toolLoading",
|
|
20115
20914
|
label: "Agent tools",
|
|
20116
|
-
description: "
|
|
20915
|
+
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.",
|
|
20117
20916
|
values: ["lazy", "eager", "off"],
|
|
20118
20917
|
labels: { lazy: "On demand", eager: "Always available", off: "Off" }
|
|
20119
20918
|
}
|
|
@@ -20182,17 +20981,27 @@ var CATEGORIES = [
|
|
|
20182
20981
|
{
|
|
20183
20982
|
id: "hygiene",
|
|
20184
20983
|
label: "Tool output cleanup",
|
|
20185
|
-
description: "
|
|
20984
|
+
description: "Recoverable, batched cleanup; no model call, but history edits can rebuild the prompt cache",
|
|
20186
20985
|
rows: [
|
|
20187
20986
|
{
|
|
20188
20987
|
kind: "choice",
|
|
20189
20988
|
setting: {
|
|
20190
20989
|
id: "contextHygieneEnabled",
|
|
20191
20990
|
label: "Automatic cleanup",
|
|
20192
|
-
description: "
|
|
20991
|
+
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.",
|
|
20193
20992
|
values: BOOLEAN_VALUES
|
|
20194
20993
|
}
|
|
20195
20994
|
},
|
|
20995
|
+
{
|
|
20996
|
+
kind: "choice",
|
|
20997
|
+
setting: {
|
|
20998
|
+
id: "contextPressureOnly",
|
|
20999
|
+
label: "Cleanup timing",
|
|
21000
|
+
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.",
|
|
21001
|
+
values: BOOLEAN_VALUES,
|
|
21002
|
+
labels: { true: "Pressure only", false: "Economic (opt-in)" }
|
|
21003
|
+
}
|
|
21004
|
+
},
|
|
20196
21005
|
{
|
|
20197
21006
|
kind: "choice",
|
|
20198
21007
|
setting: {
|
|
@@ -20722,16 +21531,16 @@ async function showSmartCompactSettings(ctx, policy, coordinator = new GlobalSet
|
|
|
20722
21531
|
await ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
20723
21532
|
const root = createSettingsRoot(policy, ctx, done, () => tui.requestRender(), coordinator, onApplied, writeConfig);
|
|
20724
21533
|
const container = new Container4;
|
|
20725
|
-
container.addChild(new
|
|
20726
|
-
container.addChild(new
|
|
20727
|
-
container.addChild(new
|
|
21534
|
+
container.addChild(new Text8(theme.fg("accent", theme.bold("Smart Compact Settings")), 0, 0));
|
|
21535
|
+
container.addChild(new Text8(theme.fg("dim", "Changes are saved for all projects; This branch only changes just this branch."), 0, 0));
|
|
21536
|
+
container.addChild(new Text8(theme.fg("dim", "/smart-compact always works. \u2022 = changed from default."), 0, 0));
|
|
20728
21537
|
const invalid = invalidSettingsLine();
|
|
20729
21538
|
if (invalid)
|
|
20730
|
-
container.addChild(new
|
|
20731
|
-
container.addChild(new
|
|
21539
|
+
container.addChild(new Text8(theme.fg("warning", invalid), 0, 0));
|
|
21540
|
+
container.addChild(new Text8("", 0, 0));
|
|
20732
21541
|
container.addChild(root);
|
|
20733
|
-
container.addChild(new
|
|
20734
|
-
container.addChild(new
|
|
21542
|
+
container.addChild(new Text8("", 0, 0));
|
|
21543
|
+
container.addChild(new Text8(theme.fg("dim", "\u2191\u2193 move \xB7 Enter change/open \xB7 r reset \xB7 Esc back"), 0, 0));
|
|
20735
21544
|
return createSettingsController(root, container, () => tui.requestRender());
|
|
20736
21545
|
});
|
|
20737
21546
|
}
|
|
@@ -20757,7 +21566,7 @@ var BEHAVIOR_PROFILES = [
|
|
|
20757
21566
|
{
|
|
20758
21567
|
id: "cleanup",
|
|
20759
21568
|
label: "Cleanup only",
|
|
20760
|
-
summary: "
|
|
21569
|
+
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.",
|
|
20761
21570
|
patch: { autoTrigger: false, contextHygieneEnabled: true, agentToolAccess: "disabled" }
|
|
20762
21571
|
},
|
|
20763
21572
|
{
|
|
@@ -20778,11 +21587,11 @@ var AGENT_WORDS = {
|
|
|
20778
21587
|
disabled: "not allowed"
|
|
20779
21588
|
};
|
|
20780
21589
|
function deriveBehaviorProfile(config) {
|
|
20781
|
-
if (config.autoTrigger === DEFAULT_CONFIG.autoTrigger && config.autoTriggerStrategy === DEFAULT_CONFIG.autoTriggerStrategy && config.contextHygieneEnabled === DEFAULT_CONFIG.contextHygieneEnabled && config.agentToolAccess === DEFAULT_CONFIG.agentToolAccess) {
|
|
21590
|
+
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) {
|
|
20782
21591
|
return {
|
|
20783
21592
|
id: "custom",
|
|
20784
|
-
label: "
|
|
20785
|
-
detail: "
|
|
21593
|
+
label: "Pressure-first (default)",
|
|
21594
|
+
detail: "Batched cleanup under early pressure, then idle compaction at the apply gate; agent access follows Pi."
|
|
20786
21595
|
};
|
|
20787
21596
|
}
|
|
20788
21597
|
const found = derive(BEHAVIOR_PROFILES, ({ patch }) => Object.entries(patch).every(([key, value]) => config[key] === value), "Custom");
|
|
@@ -20987,7 +21796,7 @@ function createHomeList(options, finish, requestRender) {
|
|
|
20987
21796
|
id: "trim",
|
|
20988
21797
|
label: "Clean up tool output",
|
|
20989
21798
|
currentValue: "no model call",
|
|
20990
|
-
description: "Queue cleanup for the next completed turn. Older output stays retrievable; the next model request is still sent untrimmed."
|
|
21799
|
+
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."
|
|
20991
21800
|
};
|
|
20992
21801
|
const setup = {
|
|
20993
21802
|
id: "setup",
|
|
@@ -21228,7 +22037,7 @@ async function showSmartCompactHome(options) {
|
|
|
21228
22037
|
return options.ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
21229
22038
|
const list = createHomeList(options, done, () => tui.requestRender());
|
|
21230
22039
|
const container = new Container5;
|
|
21231
|
-
container.addChild(new
|
|
22040
|
+
container.addChild(new Text9(theme.fg("accent", theme.bold("Smart Compact")), 0, 0));
|
|
21232
22041
|
container.addChild({
|
|
21233
22042
|
render(width) {
|
|
21234
22043
|
const config = loadConfig();
|
|
@@ -21244,15 +22053,15 @@ async function showSmartCompactHome(options) {
|
|
|
21244
22053
|
});
|
|
21245
22054
|
const invalid = invalidSettingsLine();
|
|
21246
22055
|
if (invalid)
|
|
21247
|
-
container.addChild(new
|
|
21248
|
-
container.addChild(new
|
|
22056
|
+
container.addChild(new Text9(theme.fg("warning", invalid), 0, 0));
|
|
22057
|
+
container.addChild(new Text9("", 0, 0));
|
|
21249
22058
|
container.addChild(list);
|
|
21250
22059
|
return createSettingsController(list, container, () => tui.requestRender());
|
|
21251
22060
|
});
|
|
21252
22061
|
}
|
|
21253
22062
|
|
|
21254
22063
|
// src/ui/metrics-dashboard-overlay.ts
|
|
21255
|
-
import { Key as Key5, matchesKey as matchesKey5, truncateToWidth as
|
|
22064
|
+
import { Key as Key5, matchesKey as matchesKey5, truncateToWidth as truncateToWidth5 } from "@earendil-works/pi-tui";
|
|
21256
22065
|
async function showMetricsDashboardUI(ctx, opts) {
|
|
21257
22066
|
const entries = opts.entries;
|
|
21258
22067
|
const latest = entries.at(-1);
|
|
@@ -21347,23 +22156,23 @@ async function showMetricsDashboardUI(ctx, opts) {
|
|
|
21347
22156
|
return theme.fg(insights.quality.targetMet ? "success" : "warning", " \u2022 Quality " + insights.quality.healthScore + "/100");
|
|
21348
22157
|
};
|
|
21349
22158
|
const renderHeader = (width) => [
|
|
21350
|
-
|
|
21351
|
-
|
|
21352
|
-
|
|
22159
|
+
truncateToWidth5(theme.fg("accent", theme.bold(" \uD83D\uDCCA Smart Compact Dashboard")) + theme.fg("dim", " " + entries.length + " recorded run(s)"), width),
|
|
22160
|
+
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),
|
|
22161
|
+
truncateToWidth5(theme.fg("borderMuted", "\u2500".repeat(Math.max(0, width))), width)
|
|
21353
22162
|
];
|
|
21354
22163
|
return {
|
|
21355
22164
|
render(width) {
|
|
21356
22165
|
const lines = renderHeader(width);
|
|
21357
22166
|
if (view === "menu") {
|
|
21358
|
-
lines.push(
|
|
22167
|
+
lines.push(truncateToWidth5(theme.fg("text", " Choose what to inspect:"), width), "");
|
|
21359
22168
|
for (let index = 0;index < menuItems.length; index++) {
|
|
21360
22169
|
const item = menuItems[index];
|
|
21361
22170
|
const active = index === selected;
|
|
21362
22171
|
const label = active ? theme.fg("accent", theme.bold(item.label)) : theme.fg("text", item.label);
|
|
21363
|
-
lines.push(
|
|
21364
|
-
lines.push(
|
|
22172
|
+
lines.push(truncateToWidth5((active ? " \u203A " : " ") + label, width));
|
|
22173
|
+
lines.push(truncateToWidth5(" " + theme.fg(active ? "muted" : "dim", item.desc), width));
|
|
21365
22174
|
}
|
|
21366
|
-
lines.push("",
|
|
22175
|
+
lines.push("", truncateToWidth5(theme.fg("dim", " \u2191\u2193 navigate \u2022 enter open \u2022 esc/q close"), width));
|
|
21367
22176
|
return lines;
|
|
21368
22177
|
}
|
|
21369
22178
|
const content = pageLines();
|
|
@@ -21376,12 +22185,12 @@ async function showMetricsDashboardUI(ctx, opts) {
|
|
|
21376
22185
|
styled = theme.fg("accent", theme.bold(line));
|
|
21377
22186
|
else if (line.startsWith("-"))
|
|
21378
22187
|
styled = theme.fg("dim", line);
|
|
21379
|
-
lines.push(
|
|
22188
|
+
lines.push(truncateToWidth5(" " + styled, width));
|
|
21380
22189
|
}
|
|
21381
22190
|
if (content.length > pageSize) {
|
|
21382
|
-
lines.push(
|
|
22191
|
+
lines.push(truncateToWidth5(theme.fg("dim", " showing " + (scroll + 1) + "-" + Math.min(content.length, scroll + pageSize) + " of " + content.length), width));
|
|
21383
22192
|
}
|
|
21384
|
-
lines.push("",
|
|
22193
|
+
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));
|
|
21385
22194
|
return lines;
|
|
21386
22195
|
},
|
|
21387
22196
|
invalidate() {},
|
|
@@ -21443,7 +22252,7 @@ import {
|
|
|
21443
22252
|
import {
|
|
21444
22253
|
Container as Container6,
|
|
21445
22254
|
Input as Input2,
|
|
21446
|
-
Text as
|
|
22255
|
+
Text as Text10,
|
|
21447
22256
|
wrapTextWithAnsi as wrapTextWithAnsi4
|
|
21448
22257
|
} from "@earendil-works/pi-tui";
|
|
21449
22258
|
var PAGE_SIZE = 8;
|
|
@@ -21552,7 +22361,7 @@ class Framed {
|
|
|
21552
22361
|
|
|
21553
22362
|
class Prompt extends Container6 {
|
|
21554
22363
|
input;
|
|
21555
|
-
status = new
|
|
22364
|
+
status = new Text10("", 0, 0);
|
|
21556
22365
|
busy = false;
|
|
21557
22366
|
get focused() {
|
|
21558
22367
|
return this.input.focused;
|
|
@@ -21564,8 +22373,8 @@ class Prompt extends Container6 {
|
|
|
21564
22373
|
super();
|
|
21565
22374
|
this.input = new Input2;
|
|
21566
22375
|
for (const line of lines)
|
|
21567
|
-
this.addChild(new
|
|
21568
|
-
this.addChild(new
|
|
22376
|
+
this.addChild(new Text10(line, 0, 0));
|
|
22377
|
+
this.addChild(new Text10("", 0, 0));
|
|
21569
22378
|
this.input.setValue(initial);
|
|
21570
22379
|
this.input.handleInput("\x1B[F");
|
|
21571
22380
|
this.input.onSubmit = (value) => {
|
|
@@ -21577,9 +22386,9 @@ class Prompt extends Container6 {
|
|
|
21577
22386
|
cancel();
|
|
21578
22387
|
};
|
|
21579
22388
|
this.addChild(this.input);
|
|
21580
|
-
this.addChild(new
|
|
22389
|
+
this.addChild(new Text10("", 0, 0));
|
|
21581
22390
|
this.addChild(this.status);
|
|
21582
|
-
this.addChild(new
|
|
22391
|
+
this.addChild(new Text10(hint, 0, 0));
|
|
21583
22392
|
}
|
|
21584
22393
|
setStatus(text, busy = false) {
|
|
21585
22394
|
this.status.setText(text);
|
|
@@ -21594,7 +22403,7 @@ class TextArea {
|
|
|
21594
22403
|
open;
|
|
21595
22404
|
submit;
|
|
21596
22405
|
editor;
|
|
21597
|
-
status = new
|
|
22406
|
+
status = new Text10("", 0, 0);
|
|
21598
22407
|
busy = false;
|
|
21599
22408
|
active = false;
|
|
21600
22409
|
constructor(open, submit) {
|
|
@@ -22336,11 +23145,13 @@ async function describeEffectiveState(ctx, config, policy, runtime) {
|
|
|
22336
23145
|
lines.push("Agent tool: " + (policy.agentToolEnabled ? "exposed" : "not exposed") + " (policy " + policy.agentToolAccess + ")");
|
|
22337
23146
|
if (ctx.model && !ctx.model.promptCache)
|
|
22338
23147
|
lines.push("Reader prompt-cache lifetime: not declared; host cache warming cannot schedule from this metadata. No cache savings are assumed.");
|
|
22339
|
-
const
|
|
22340
|
-
const
|
|
22341
|
-
const
|
|
22342
|
-
|
|
22343
|
-
|
|
23148
|
+
const pressure = contextPressure(ctx, config);
|
|
23149
|
+
const window = pressure.modelWindow;
|
|
23150
|
+
const gateWindow = pressure.policyWindow;
|
|
23151
|
+
const tokens = pressure.tokens;
|
|
23152
|
+
if (typeof window === "number" && Number.isFinite(window) && window > 0 && gateWindow !== null) {
|
|
23153
|
+
const gates = { startTokens: pressure.cleanupTokens, applyTokens: pressure.compactionTokens };
|
|
23154
|
+
lines.push("Cleanup gate: \u2265" + gates.startTokens.toLocaleString("en-US") + " tokens; policy usage " + (pressure.percent === null ? "unknown" : pressure.percent.toFixed(1) + "%"));
|
|
22344
23155
|
const apply = Math.ceil(Math.max(MIN_TOKEN_THRESHOLD, gates.applyTokens)).toLocaleString("en-US");
|
|
22345
23156
|
const gate = " (" + config.minContextPercent + "% of " + (gateWindow < window ? "maxContextTokens " : "window ") + gateWindow.toLocaleString("en-US") + ", floor " + MIN_TOKEN_THRESHOLD.toLocaleString("en-US") + ")";
|
|
22346
23157
|
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));
|
|
@@ -22352,9 +23163,9 @@ async function describeEffectiveState(ctx, config, policy, runtime) {
|
|
|
22352
23163
|
lines.push("Runtime: " + (runtime ? (runtime.running ? "running" : "idle") + "; preparation " + runtime.preparation : "not attached") + "; candidate reuse is revalidated at apply");
|
|
22353
23164
|
if (runtime?.cacheLedger?.length)
|
|
22354
23165
|
lines.push(...runtime.cacheLedger);
|
|
22355
|
-
lines.push("Auto limits: " + Math.min(config.autoTriggerTimeoutMs, AUTO_TRIGGER_TIMEOUT_CAP_MS) / 1000 + " s timeout; " + SETTLED_TRIGGER_COOLDOWN_MS / 60000 + " min cooldown; " +
|
|
23166
|
+
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");
|
|
22356
23167
|
const backgroundHygiene = policy.autoTrigger && config.autoTriggerStrategy === "background";
|
|
22357
|
-
lines.push("Context hygiene: " + (config.contextHygieneEnabled ? "on (
|
|
23168
|
+
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" : ""));
|
|
22358
23169
|
const memory = await describeMemoryBackendReadiness(config);
|
|
22359
23170
|
lines.push("Memory selected: " + config.memoryBackend + (projectId ? " (project-bound)" : " \u2014 unavailable: no project scope"));
|
|
22360
23171
|
lines.push("Memory readiness: " + memory.reason);
|
|
@@ -22596,7 +23407,8 @@ async function showStorage(ctx) {
|
|
|
22596
23407
|
function compactNowState(ctx, dependencies) {
|
|
22597
23408
|
const config = loadConfig();
|
|
22598
23409
|
const tokens = ctx.getContextUsage()?.tokens;
|
|
22599
|
-
const
|
|
23410
|
+
const window = effectiveContextWindow(ctx.model, config);
|
|
23411
|
+
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" : "");
|
|
22600
23412
|
const runtime = dependencies.getRuntimeState?.(ctx);
|
|
22601
23413
|
if (runtime?.running)
|
|
22602
23414
|
return { value, blocked: "Compaction is already running. Wait for it to finish." };
|
|
@@ -23008,6 +23820,7 @@ function createSmartCompactPolicy(pi, syncTools) {
|
|
|
23008
23820
|
|
|
23009
23821
|
// src/app/lazy-tools.ts
|
|
23010
23822
|
import { StringEnum as StringEnum3 } from "@earendil-works/pi-ai";
|
|
23823
|
+
import { Text as Text11 } from "@earendil-works/pi-tui";
|
|
23011
23824
|
import { Type as Type5 } from "typebox";
|
|
23012
23825
|
|
|
23013
23826
|
// src/app/context-guide.ts
|
|
@@ -23176,33 +23989,127 @@ function registerContextToolLoader(pi, exposure) {
|
|
|
23176
23989
|
if (loadConfig().toolLoading === "off" || !pi.getActiveTools().includes(LOADER))
|
|
23177
23990
|
throw new Error("Context tools are disabled in Pi settings or /tools.");
|
|
23178
23991
|
let text;
|
|
23992
|
+
let details = undefined;
|
|
23179
23993
|
if (params.action === "guide")
|
|
23180
23994
|
text = await readContextGuide();
|
|
23181
|
-
else if (params.action === "status")
|
|
23182
|
-
|
|
23183
|
-
|
|
23995
|
+
else if (params.action === "status") {
|
|
23996
|
+
const status = exposure.status();
|
|
23997
|
+
text = JSON.stringify(status);
|
|
23998
|
+
details = { display: { kind: "tool-groups" }, status };
|
|
23999
|
+
} else {
|
|
23184
24000
|
if (!params.group)
|
|
23185
24001
|
throw new Error(`${params.action} requires a group.`);
|
|
23186
|
-
if (params.action === "load")
|
|
23187
|
-
|
|
23188
|
-
|
|
24002
|
+
if (params.action === "load") {
|
|
24003
|
+
const names = exposure.load(params.group);
|
|
24004
|
+
text = `Loaded: ${names.join(", ")}. Available on the next model turn; no feature instructions were injected.`;
|
|
24005
|
+
details = { display: { state: "loaded", group: params.group, tools: names } };
|
|
24006
|
+
} else {
|
|
23189
24007
|
if (loadConfig().toolLoading === "eager")
|
|
23190
24008
|
throw new Error("Eager mode keeps tools loaded. Select On demand or Off in /smart-compact settings before unloading a group.");
|
|
23191
24009
|
exposure.unload(params.group);
|
|
23192
24010
|
text = `Unloaded ${params.group}. No files or session history changed.`;
|
|
24011
|
+
details = { display: { state: "unloaded", group: params.group } };
|
|
23193
24012
|
}
|
|
23194
24013
|
}
|
|
23195
|
-
return { content: [{ type: "text", text }], details
|
|
24014
|
+
return { content: [{ type: "text", text }], details };
|
|
24015
|
+
},
|
|
24016
|
+
renderCall(args, theme) {
|
|
24017
|
+
const label = theme.fg("toolTitle", "smart_tools ");
|
|
24018
|
+
const action = safeArg(args.action, 20);
|
|
24019
|
+
const group = safeArg(args.group, 40);
|
|
24020
|
+
return new Text11(label + theme.fg("accent", action) + (group ? " " + theme.fg("dim", group) : ""), 0, 0);
|
|
24021
|
+
},
|
|
24022
|
+
renderResult(result, options, theme, context) {
|
|
24023
|
+
return tryRow(theme, () => renderToolsRow(result, theme, context, options.expanded), result, options.expanded);
|
|
23196
24024
|
}
|
|
23197
24025
|
});
|
|
23198
24026
|
}
|
|
24027
|
+
function renderToolsRow(result, theme, context, expanded) {
|
|
24028
|
+
if (context.isError) {
|
|
24029
|
+
const head = theme.fg("error", "smart_tools failed:");
|
|
24030
|
+
return expanded ? expandedRow(theme, result, [head]) : new Text11(head + " " + summarizeLine(firstTextContent(result.content), 140), 0, 0);
|
|
24031
|
+
}
|
|
24032
|
+
const details = result.details;
|
|
24033
|
+
if (!details)
|
|
24034
|
+
return rawFallbackRow(theme, result, expanded);
|
|
24035
|
+
const lines = [];
|
|
24036
|
+
if (details.display?.state === "loaded") {
|
|
24037
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "loaded " + String(details.display.group)));
|
|
24038
|
+
if (details.display.tools?.length) {
|
|
24039
|
+
lines.push(theme.fg("dim", details.display.tools.join(", ")));
|
|
24040
|
+
}
|
|
24041
|
+
return new Text11(lines.join(`
|
|
24042
|
+
`), 0, 0);
|
|
24043
|
+
}
|
|
24044
|
+
if (details.display?.state === "unloaded") {
|
|
24045
|
+
lines.push(statusLabel(theme, "info") + " " + theme.fg("muted", "unloaded " + String(details.display.group)));
|
|
24046
|
+
return new Text11(lines.join(`
|
|
24047
|
+
`), 0, 0);
|
|
24048
|
+
}
|
|
24049
|
+
if (details.display?.kind === "tool-groups" && details.status) {
|
|
24050
|
+
lines.push(theme.fg("muted", "mode: ") + theme.fg("accent", String(details.status.mode ?? "")));
|
|
24051
|
+
for (const group of details.status.groups ?? []) {
|
|
24052
|
+
const marker = group.active.length ? theme.fg("success", "\u25CF") : group.available ? theme.fg("dim", "\u25CB") : theme.fg("error", "\u2715");
|
|
24053
|
+
const state = group.active.length ? group.active.length + " active" : group.available ? "available" : "disabled";
|
|
24054
|
+
lines.push(marker + " " + group.group + " " + theme.fg("dim", state));
|
|
24055
|
+
}
|
|
24056
|
+
return expanded ? expandedRow(theme, result, lines) : new Text11(lines.join(`
|
|
24057
|
+
`), 0, 0);
|
|
24058
|
+
}
|
|
24059
|
+
return rawFallbackRow(theme, result, expanded);
|
|
24060
|
+
}
|
|
23199
24061
|
|
|
23200
24062
|
// src/app/register-navigation.ts
|
|
23201
24063
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
23202
24064
|
import path26 from "path";
|
|
23203
24065
|
import { StringEnum as StringEnum4 } from "@earendil-works/pi-ai";
|
|
23204
24066
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
24067
|
+
import { Text as Text12 } from "@earendil-works/pi-tui";
|
|
23205
24068
|
import { Type as Type6 } from "typebox";
|
|
24069
|
+
function renderNavigationRow(result, expanded, theme, context) {
|
|
24070
|
+
if (context.isError) {
|
|
24071
|
+
const head = theme.fg("error", "navigation failed:");
|
|
24072
|
+
return expanded ? expandedRow(theme, result, [head]) : new Text12(head + " " + summarizeLine(firstTextContent(result.content), 140), 0, 0);
|
|
24073
|
+
}
|
|
24074
|
+
const details = result.details;
|
|
24075
|
+
if (!details)
|
|
24076
|
+
return rawFallbackRow(theme, result, expanded);
|
|
24077
|
+
const display = details.display;
|
|
24078
|
+
const lines = [];
|
|
24079
|
+
if (display?.state === "queued" && display.action === "pivot") {
|
|
24080
|
+
lines.push(statusLabel(theme, "queued") + " " + theme.fg("muted", "pivot queued \u2014 not applied; takes effect after the turn settles"));
|
|
24081
|
+
if (display.target)
|
|
24082
|
+
lines.push(metaLine(theme, "target", String(display.target)));
|
|
24083
|
+
return new Text12(lines.join(`
|
|
24084
|
+
`), 0, 0);
|
|
24085
|
+
}
|
|
24086
|
+
if (details.anchor) {
|
|
24087
|
+
const anchor = details.anchor;
|
|
24088
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "anchor recorded"));
|
|
24089
|
+
lines.push(metaLine(theme, "name", anchor.name ?? ""));
|
|
24090
|
+
if (anchor.targetId)
|
|
24091
|
+
lines.push(metaLine(theme, "target", anchor.targetId));
|
|
24092
|
+
return expanded ? expandedRow(theme, result, lines) : new Text12(lines.join(`
|
|
24093
|
+
`), 0, 0);
|
|
24094
|
+
}
|
|
24095
|
+
if (display?.kind === "anchors" || display?.kind === "anchor-recall") {
|
|
24096
|
+
const count = Number(display.count) || 0;
|
|
24097
|
+
lines.push(statusLabel(theme, count ? "done" : "skipped") + " " + theme.fg("muted", count + " anchor(s) " + (display.kind === "anchor-recall" ? "recalled" : "listed")));
|
|
24098
|
+
if (expanded)
|
|
24099
|
+
return expandedRow(theme, result, lines);
|
|
24100
|
+
lines.push(expandHint(theme));
|
|
24101
|
+
return new Text12(lines.join(`
|
|
24102
|
+
`), 0, 0);
|
|
24103
|
+
}
|
|
24104
|
+
if (display?.kind === "anchor-detail") {
|
|
24105
|
+
lines.push(statusLabel(theme, "done") + " " + theme.fg("muted", "anchor detail"));
|
|
24106
|
+
if (display.name)
|
|
24107
|
+
lines.push(metaLine(theme, "name", String(display.name)));
|
|
24108
|
+
return expanded ? expandedRow(theme, result, lines) : new Text12(lines.join(`
|
|
24109
|
+
`), 0, 0);
|
|
24110
|
+
}
|
|
24111
|
+
return rawFallbackRow(theme, result, expanded);
|
|
24112
|
+
}
|
|
23206
24113
|
var STATUS = "smart-context-navigation";
|
|
23207
24114
|
var APPLY_PREFIX = "context --apply=";
|
|
23208
24115
|
function registerNavigation(pi, options = {}) {
|
|
@@ -23214,7 +24121,7 @@ function registerNavigation(pi, options = {}) {
|
|
|
23214
24121
|
const settings = config();
|
|
23215
24122
|
return new SecretScrubber(settings.scrubSecrets, settings.scrubPii).scrubText(text).value;
|
|
23216
24123
|
};
|
|
23217
|
-
const reply = (text) => ({ content: [{ type: "text", text: scrub(text) }], details
|
|
24124
|
+
const reply = (text, details = undefined) => ({ content: [{ type: "text", text: scrub(text) }], details });
|
|
23218
24125
|
const enabled = () => {
|
|
23219
24126
|
if (!config().contextNavigationEnabled)
|
|
23220
24127
|
throw new Error("Context navigation is disabled in Pi Continuity settings.");
|
|
@@ -23237,10 +24144,10 @@ function registerNavigation(pi, options = {}) {
|
|
|
23237
24144
|
const branch = ctx.sessionManager.getBranch();
|
|
23238
24145
|
const index = branch.findLastIndex((entry) => anchorFromEntry(entry) !== null);
|
|
23239
24146
|
const anchor = index < 0 ? null : anchorFromEntry(branch[index]);
|
|
23240
|
-
const
|
|
24147
|
+
const pressure = contextPressure(ctx, config());
|
|
23241
24148
|
const parts = [];
|
|
23242
|
-
if (
|
|
23243
|
-
parts.push(`ctx ${Math.
|
|
24149
|
+
if (pressure.percent !== null)
|
|
24150
|
+
parts.push(`ctx ${Math.round(pressure.percent)}%${pressure.policyWindow !== pressure.modelWindow ? ` of ${pressure.policyWindow?.toLocaleString("en-US")} policy window` : ""}`);
|
|
23244
24151
|
if (anchor)
|
|
23245
24152
|
parts.push(`anchor:${anchor.name.replace(/[\x00-\x1f\x7f]/g, " ").slice(0, 120)} \xB7 ${branch.length - index - 1} entries later`);
|
|
23246
24153
|
ctx.ui.setStatus(STATUS, parts.length ? parts.join(" \xB7 ") : undefined);
|
|
@@ -23267,7 +24174,6 @@ function registerNavigation(pi, options = {}) {
|
|
|
23267
24174
|
const targetId = ctx.sessionManager.getLeafId();
|
|
23268
24175
|
if (!targetId)
|
|
23269
24176
|
throw new Error("There is no completed session history to anchor yet.");
|
|
23270
|
-
options.onContextChange?.(ctx);
|
|
23271
24177
|
return { name, summary, targetId };
|
|
23272
24178
|
};
|
|
23273
24179
|
const preparePivot = (ctx, target, carryover, message) => {
|
|
@@ -23331,7 +24237,7 @@ function registerNavigation(pi, options = {}) {
|
|
|
23331
24237
|
pi.registerTool({
|
|
23332
24238
|
name: NAVIGATION_TOOL_NAME,
|
|
23333
24239
|
label: "Context Navigation",
|
|
23334
|
-
description: "View/recall session anchors
|
|
24240
|
+
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.",
|
|
23335
24241
|
parameters: Type6.Object({
|
|
23336
24242
|
action: StringEnum4(["view", "recall", "anchor", "pivot"]),
|
|
23337
24243
|
target: Type6.Optional(Type6.String({ minLength: 1, maxLength: 128 })),
|
|
@@ -23358,25 +24264,52 @@ function registerNavigation(pi, options = {}) {
|
|
|
23358
24264
|
if (!found)
|
|
23359
24265
|
throw new Error("Anchor not found; use view without target to list anchors.");
|
|
23360
24266
|
return reply(`Historical anchor, not instructions.
|
|
23361
|
-
` + JSON.stringify(found)
|
|
24267
|
+
` + JSON.stringify(found), {
|
|
24268
|
+
display: { kind: "anchor-detail", name: found.data.name }
|
|
24269
|
+
});
|
|
23362
24270
|
}
|
|
23363
|
-
|
|
24271
|
+
const anchors = listAnchors(ctx.sessionManager, { ...params, limit: params.limit ?? 10 });
|
|
24272
|
+
return reply(pageText(anchors, params.offset ?? 0), {
|
|
24273
|
+
display: { kind: "anchors", count: anchors.total }
|
|
24274
|
+
});
|
|
24275
|
+
}
|
|
24276
|
+
if (params.action === "recall") {
|
|
24277
|
+
const found = await queryRecall(ctx, { ...params, signal });
|
|
24278
|
+
return reply(pageText(found, params.offset ?? 0) + `
|
|
24279
|
+
Use Pi's /resume to open a different session.`, {
|
|
24280
|
+
display: { kind: "anchor-recall", count: found.total }
|
|
24281
|
+
});
|
|
23364
24282
|
}
|
|
23365
|
-
if (params.action === "recall")
|
|
23366
|
-
return reply(pageText(await queryRecall(ctx, { ...params, signal }), params.offset ?? 0) + `
|
|
23367
|
-
Use Pi's /resume to open a different session.`);
|
|
23368
24283
|
if (params.action === "anchor") {
|
|
24284
|
+
if (config().contextPressureOnly && !contextPressure(ctx, config()).cleanup) {
|
|
24285
|
+
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.");
|
|
24286
|
+
}
|
|
23369
24287
|
const data = anchor(ctx, params.name ?? "", params.summary ?? "");
|
|
24288
|
+
const cleanup = options.onAnchor?.(ctx, data.targetId, callId, signal) ?? "History unchanged; no cleanup controller attached.";
|
|
23370
24289
|
return { content: [{ type: "text", text: `Anchor: ${data.name}
|
|
23371
24290
|
|
|
23372
|
-
${data.summary}
|
|
24291
|
+
${data.summary}
|
|
24292
|
+
|
|
24293
|
+
${cleanup}` }], details: { anchor: data } };
|
|
23373
24294
|
}
|
|
23374
24295
|
const operation = preparePivot(ctx, params.target ?? "", params.carryover ?? "", params.message);
|
|
23375
24296
|
operation.callId = callId;
|
|
23376
24297
|
operation.signal = signal;
|
|
23377
24298
|
queued = operation;
|
|
23378
24299
|
options.onContextChange?.(ctx);
|
|
23379
|
-
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 };
|
|
24300
|
+
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 };
|
|
24301
|
+
},
|
|
24302
|
+
renderCall(args, theme) {
|
|
24303
|
+
const label = theme.fg("toolTitle", "smart_navigation ");
|
|
24304
|
+
const action = safeArg(args.action, 20);
|
|
24305
|
+
const target = safeArg(args.target, 80);
|
|
24306
|
+
const name = safeArg(args.name, 80);
|
|
24307
|
+
const keyword = safeArg(args.keyword, 60);
|
|
24308
|
+
const detail = target ? " " + theme.fg("dim", target) : name ? " " + theme.fg("muted", name) : keyword ? " " + theme.fg("dim", '"' + keyword + '"') : "";
|
|
24309
|
+
return new Text12(label + theme.fg("accent", action) + detail, 0, 0);
|
|
24310
|
+
},
|
|
24311
|
+
renderResult(result, { expanded }, theme, context) {
|
|
24312
|
+
return tryRow(theme, () => renderNavigationRow(result, expanded, theme, context), result, expanded);
|
|
23380
24313
|
}
|
|
23381
24314
|
});
|
|
23382
24315
|
pi.on("turn_end", (event, ctx) => {
|
|
@@ -23484,7 +24417,8 @@ ${operation.carryover}` } };
|
|
|
23484
24417
|
${data.summary}`, display: true, details: { anchor: data } }, { triggerTurn: false });
|
|
23485
24418
|
pi.setLabel(ctx.sessionManager.getLeafId(), data.name);
|
|
23486
24419
|
footer(ctx);
|
|
23487
|
-
|
|
24420
|
+
const cleanup = options.onAnchor?.(ctx, data.targetId) ?? "History unchanged; no cleanup controller attached.";
|
|
24421
|
+
return { ok: true, message: `Anchor ${data.name} saved; history was not compacted. ${cleanup}` };
|
|
23488
24422
|
},
|
|
23489
24423
|
async pivot(target, carryover, message) {
|
|
23490
24424
|
return applyPivot(preparePivot(ctx, target, carryover, message), ctx);
|
|
@@ -23780,8 +24714,7 @@ function unwrapConsumed(result, ctx) {
|
|
|
23780
24714
|
}
|
|
23781
24715
|
}
|
|
23782
24716
|
function smartCompactExtension(pi) {
|
|
23783
|
-
const
|
|
23784
|
-
const pendingRef = createPendingSlot({ ttlMs: PENDING_TTL_MS });
|
|
24717
|
+
const pendingRef = createPendingSlot({ ttlMs: () => loadConfig().pendingTtlMs });
|
|
23785
24718
|
const isRunning = createSessionRunLock();
|
|
23786
24719
|
let navigation;
|
|
23787
24720
|
const pivotQueued = (ctx) => navigation?.isPending(ctx) ?? false;
|
|
@@ -23799,14 +24732,19 @@ function smartCompactExtension(pi) {
|
|
|
23799
24732
|
showStatus: policy.branchOverrides().showStatus ?? config.showStatus
|
|
23800
24733
|
};
|
|
23801
24734
|
};
|
|
24735
|
+
const activeToolDefinitions = () => {
|
|
24736
|
+
const active = new Set(pi.getActiveTools());
|
|
24737
|
+
return pi.getAllTools().filter((tool) => active.has(tool.name)).map((tool) => ({ name: tool.name, description: tool.description, parameters: tool.parameters }));
|
|
24738
|
+
};
|
|
23802
24739
|
const background = createBackgroundPreparation({
|
|
24740
|
+
toolSignature: () => JSON.stringify(activeToolDefinitions()),
|
|
23803
24741
|
async prepare(ctx, config, signal) {
|
|
23804
24742
|
if (signal.aborted || isRunning.isSessionActive(resolveSessionId(ctx)))
|
|
23805
24743
|
return null;
|
|
23806
24744
|
const { sumModel, segModel, verifyModel } = resolveModels(ctx, undefined, config);
|
|
23807
24745
|
if (!sumModel || !segModel || !verifyModel)
|
|
23808
24746
|
return null;
|
|
23809
|
-
const localPending = createPendingSlot({ ttlMs:
|
|
24747
|
+
const localPending = createPendingSlot({ ttlMs: config.pendingTtlMs });
|
|
23810
24748
|
const outcome = await runSmartCompact({
|
|
23811
24749
|
ctx,
|
|
23812
24750
|
config,
|
|
@@ -23846,10 +24784,7 @@ function smartCompactExtension(pi) {
|
|
|
23846
24784
|
}
|
|
23847
24785
|
};
|
|
23848
24786
|
const nativeContinuity = createNativeContinuityBridge();
|
|
23849
|
-
setNativeToolSource(
|
|
23850
|
-
const active = new Set(pi.getActiveTools());
|
|
23851
|
-
return pi.getAllTools().filter((tool) => active.has(tool.name)).map((tool) => ({ name: tool.name, description: tool.description, parameters: tool.parameters }));
|
|
23852
|
-
});
|
|
24787
|
+
setNativeToolSource(activeToolDefinitions);
|
|
23853
24788
|
registerAnchorCache(pi, { config: loadConfig });
|
|
23854
24789
|
const nativeReplay = createNativeReplayHook();
|
|
23855
24790
|
pi.on("before_provider_request", (event, ctx) => nativeReplay.handle(event.payload, ctx));
|
|
@@ -23873,7 +24808,7 @@ function smartCompactExtension(pi) {
|
|
|
23873
24808
|
return write;
|
|
23874
24809
|
};
|
|
23875
24810
|
const commitCandidates = createCompactionCommitStore({
|
|
23876
|
-
ttlMs: loadConfig().pendingTtlMs,
|
|
24811
|
+
ttlMs: () => loadConfig().pendingTtlMs,
|
|
23877
24812
|
onDiscard: (pending, reason) => {
|
|
23878
24813
|
recordApplyFailure(pending, reason);
|
|
23879
24814
|
}
|
|
@@ -23888,7 +24823,7 @@ function smartCompactExtension(pi) {
|
|
|
23888
24823
|
const calls = displaced.reduce((sum, pending) => sum + (pending.metricsSnapshot?.totalCalls ?? 0), 0);
|
|
23889
24824
|
reportIssue({
|
|
23890
24825
|
key: "compact.foreign-displaced",
|
|
23891
|
-
message: actor + " was applied instead of Continuity's prepared summary; that summary (" + calls + " model call" + (calls === 1 ? "" : "s") + ") was discarded and its continuity state was not saved.
|
|
24826
|
+
message: actor + " was applied instead of Continuity's prepared summary; that summary (" + calls + " model call" + (calls === 1 ? "" : "s") + ") was discarded and its continuity state was not saved."
|
|
23892
24827
|
}, ctx);
|
|
23893
24828
|
await Promise.all(displaced.flatMap((pending) => applyFailureWrites.get(pending.runId) ?? []));
|
|
23894
24829
|
return;
|
|
@@ -23896,7 +24831,7 @@ function smartCompactExtension(pi) {
|
|
|
23896
24831
|
if (source === "extension") {
|
|
23897
24832
|
reportIssue({
|
|
23898
24833
|
key: "compact.foreign-extension",
|
|
23899
|
-
message: actor + " was applied; Continuity did not summarize it and recorded no continuity state or metrics for it.
|
|
24834
|
+
message: actor + " was applied; Continuity did not summarize it and recorded no continuity state or metrics for it."
|
|
23900
24835
|
}, ctx);
|
|
23901
24836
|
} else if (automaticConfig().autoTrigger) {
|
|
23902
24837
|
reportIssue({
|
|
@@ -23959,6 +24894,7 @@ function smartCompactExtension(pi) {
|
|
|
23959
24894
|
navigation = registerNavigation(pi, {
|
|
23960
24895
|
config: automaticConfig,
|
|
23961
24896
|
mutationBlocked: (ctx) => isRunning.isSessionActive(resolveSessionId(ctx)) ? "Compaction is running; wait for it to finish." : undefined,
|
|
24897
|
+
onAnchor: (ctx, originId, callId, signal) => smartContext.requestAnchorTrim(ctx, originId, callId, signal).notice,
|
|
23962
24898
|
onContextChange: (ctx) => invalidatePreparation(ctx, "branch"),
|
|
23963
24899
|
onContextEdit: (_ctx, kind) => hostCache.noteContextEdit(kind)
|
|
23964
24900
|
});
|
|
@@ -24007,17 +24943,6 @@ function smartCompactExtension(pi) {
|
|
|
24007
24943
|
pi.on("session_start", (_event, ctx) => {
|
|
24008
24944
|
nativeReplay.refresh(ctx);
|
|
24009
24945
|
flushIssues(ctx);
|
|
24010
|
-
try {
|
|
24011
|
-
const conflicts = detectExtensionConflicts({ commands: typeof pi.getCommands === "function" ? pi.getCommands() : [], tools: pi.getAllTools() });
|
|
24012
|
-
if (conflicts.length > 0)
|
|
24013
|
-
reportIssue({ key: "interop.conflicts", message: conflictNotice(conflicts) }, ctx);
|
|
24014
|
-
} catch (error) {
|
|
24015
|
-
reportIssue({
|
|
24016
|
-
key: "interop.detect",
|
|
24017
|
-
message: "Could not check loaded extensions for conflicts (" + errorDetail(error) + "). Other compaction extensions are not detected at startup. Please report this if it repeats.",
|
|
24018
|
-
error
|
|
24019
|
-
}, ctx);
|
|
24020
|
-
}
|
|
24021
24946
|
invalidatePreparation(ctx);
|
|
24022
24947
|
hostCache.reset(resolveSessionId(ctx));
|
|
24023
24948
|
toolExposure.atBoundary();
|
|
@@ -24283,10 +25208,10 @@ function smartCompactExtension(pi) {
|
|
|
24283
25208
|
if (!hostCache.observe(event.message)?.warn)
|
|
24284
25209
|
return;
|
|
24285
25210
|
const { foreign } = hostCache.summary().rebuilds;
|
|
24286
|
-
|
|
25211
|
+
recordIssue({
|
|
24287
25212
|
key: "cache.foreign-rebuilds",
|
|
24288
25213
|
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."
|
|
24289
|
-
}
|
|
25214
|
+
});
|
|
24290
25215
|
});
|
|
24291
25216
|
pi.on("message_end", async (event, ctx) => {
|
|
24292
25217
|
try {
|
|
@@ -24328,7 +25253,6 @@ function smartCompactExtension(pi) {
|
|
|
24328
25253
|
});
|
|
24329
25254
|
pi.on("session_compact", (_event, ctx) => {
|
|
24330
25255
|
nativeReplay.refresh(ctx);
|
|
24331
|
-
toolExposure.atBoundary();
|
|
24332
25256
|
policy.restore(ctx);
|
|
24333
25257
|
navigation.refresh(ctx);
|
|
24334
25258
|
});
|