pi-smart-compact 10.0.1 → 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 +67 -19
- package/CHANGELOG.md +75 -0
- package/README.md +12 -7
- package/assets/skills/context-management/SKILL.md +5 -5
- package/dist/app/background-preparation.d.ts +13 -0
- package/dist/app/background-preparation.d.ts.map +1 -1
- package/dist/app/compaction-commit-store.d.ts +1 -1
- package/dist/app/compaction-commit-store.d.ts.map +1 -1
- package/dist/app/context-operations.d.ts +8 -2
- package/dist/app/context-operations.d.ts.map +1 -1
- package/dist/app/effective-state.d.ts.map +1 -1
- package/dist/app/hindsight-memory.d.ts.map +1 -1
- package/dist/app/lazy-tools.d.ts.map +1 -1
- package/dist/app/pending-slot.d.ts +2 -2
- package/dist/app/pending-slot.d.ts.map +1 -1
- package/dist/app/register-context-tools.d.ts.map +1 -1
- package/dist/app/register-navigation.d.ts +3 -1
- package/dist/app/register-navigation.d.ts.map +1 -1
- package/dist/app/register-smart-compact-command.d.ts.map +1 -1
- package/dist/app/register-smart-compact-tool.d.ts.map +1 -1
- package/dist/app/register-smart-context-tool.d.ts +1 -0
- package/dist/app/register-smart-context-tool.d.ts.map +1 -1
- package/dist/app/settled-auto-trigger.d.ts.map +1 -1
- package/dist/app/stage-auth.d.ts.map +1 -1
- package/dist/app/steps/synthesize.d.ts.map +1 -1
- package/dist/constants.d.ts +4 -3
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1340 -339
- 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/configuration.md +95 -32
- package/docs/evaluation.md +15 -2
- package/docs/guide.md +57 -31
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
6
|
|
|
7
7
|
// src/constants.ts
|
|
8
|
-
var VERSION = "10.0
|
|
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
|
};
|
|
@@ -16016,89 +16368,6 @@ function createNativeContinuityBridge(opts = {}) {
|
|
|
16016
16368
|
};
|
|
16017
16369
|
}
|
|
16018
16370
|
|
|
16019
|
-
// src/app/settled-auto-trigger.ts
|
|
16020
|
-
function createSettledAutoTrigger(options = {}) {
|
|
16021
|
-
const now = options.now ?? Date.now;
|
|
16022
|
-
const cooldownMs = Math.max(0, options.cooldownMs ?? SETTLED_TRIGGER_COOLDOWN_MS);
|
|
16023
|
-
const active = new Map;
|
|
16024
|
-
const lastCompactionAt = new Map;
|
|
16025
|
-
const noteCompaction = (sessionId) => {
|
|
16026
|
-
if (!isUnresolvedSessionId(sessionId))
|
|
16027
|
-
lastCompactionAt.set(sessionId, now());
|
|
16028
|
-
};
|
|
16029
|
-
const clear = (sessionId) => {
|
|
16030
|
-
active.delete(sessionId);
|
|
16031
|
-
lastCompactionAt.delete(sessionId);
|
|
16032
|
-
};
|
|
16033
|
-
const request = async (ctx, config) => {
|
|
16034
|
-
if (!config.autoTrigger || !["settled", "background"].includes(config.autoTriggerStrategy))
|
|
16035
|
-
return;
|
|
16036
|
-
const sessionId = resolveSessionId(ctx);
|
|
16037
|
-
if (isUnresolvedSessionId(sessionId) || active.has(sessionId))
|
|
16038
|
-
return;
|
|
16039
|
-
const usage = ctx.getContextUsage();
|
|
16040
|
-
const totalTokens = usage?.tokens;
|
|
16041
|
-
if (typeof totalTokens !== "number" || !Number.isFinite(totalTokens) || totalTokens < MIN_TOKEN_THRESHOLD || !ctx.model)
|
|
16042
|
-
return;
|
|
16043
|
-
const contextPercent = safeContextPercent(totalTokens, effectiveContextWindow(ctx.model, config));
|
|
16044
|
-
if (contextPercent < config.minContextPercent)
|
|
16045
|
-
return;
|
|
16046
|
-
const lastCompaction = lastCompactionAt.get(sessionId);
|
|
16047
|
-
if (lastCompaction !== undefined && now() - lastCompaction < cooldownMs)
|
|
16048
|
-
return;
|
|
16049
|
-
if (!ctx.isIdle() || ctx.hasPendingMessages())
|
|
16050
|
-
return;
|
|
16051
|
-
const requestToken = Symbol(sessionId);
|
|
16052
|
-
active.set(sessionId, requestToken);
|
|
16053
|
-
await new Promise((resolve) => {
|
|
16054
|
-
let finished = false;
|
|
16055
|
-
const watchdogMs = options.watchdogMs ?? AUTO_TRIGGER_TIMEOUT_CAP_MS + config.autoTriggerTimeoutMs;
|
|
16056
|
-
const watchdog = setTimeout(() => {
|
|
16057
|
-
reportIssue({
|
|
16058
|
-
key: "auto.settled-no-callback",
|
|
16059
|
-
message: "Pi did not report the automatic compaction result within " + Math.round(watchdogMs / 1000) + "s. The next settled turn may request it again."
|
|
16060
|
-
}, ctx);
|
|
16061
|
-
finish();
|
|
16062
|
-
}, watchdogMs);
|
|
16063
|
-
watchdog.unref?.();
|
|
16064
|
-
const finish = () => {
|
|
16065
|
-
if (finished)
|
|
16066
|
-
return;
|
|
16067
|
-
finished = true;
|
|
16068
|
-
clearTimeout(watchdog);
|
|
16069
|
-
if (active.get(sessionId) === requestToken)
|
|
16070
|
-
active.delete(sessionId);
|
|
16071
|
-
resolve();
|
|
16072
|
-
};
|
|
16073
|
-
try {
|
|
16074
|
-
ctx.compact({
|
|
16075
|
-
onComplete: () => {
|
|
16076
|
-
if (active.get(sessionId) === requestToken)
|
|
16077
|
-
noteCompaction(sessionId);
|
|
16078
|
-
finish();
|
|
16079
|
-
},
|
|
16080
|
-
onError: (error) => {
|
|
16081
|
-
reportIssue({
|
|
16082
|
-
key: "auto.settled-apply",
|
|
16083
|
-
message: "Pi could not run the automatic compaction (" + errorDetail(error) + "). Conversation unchanged. Run /smart-compact manually if context is high.",
|
|
16084
|
-
error
|
|
16085
|
-
}, ctx);
|
|
16086
|
-
finish();
|
|
16087
|
-
}
|
|
16088
|
-
});
|
|
16089
|
-
} catch (error) {
|
|
16090
|
-
reportIssue({
|
|
16091
|
-
key: "auto.settled-apply",
|
|
16092
|
-
message: "Pi could not run the automatic compaction (" + errorDetail(error) + "). Conversation unchanged. Run /smart-compact manually if context is high.",
|
|
16093
|
-
error
|
|
16094
|
-
}, ctx);
|
|
16095
|
-
finish();
|
|
16096
|
-
}
|
|
16097
|
-
});
|
|
16098
|
-
};
|
|
16099
|
-
return { request, noteCompaction, clear };
|
|
16100
|
-
}
|
|
16101
|
-
|
|
16102
16371
|
// src/app/background-preparation.ts
|
|
16103
16372
|
function recordPreparationDiscard(sessionId, snapshot, reason) {
|
|
16104
16373
|
return appendMetricsSnapshot(sessionId, {
|
|
@@ -16120,11 +16389,31 @@ function preparationWindow(config, window) {
|
|
|
16120
16389
|
const startTokens = explicit ? Math.max(MIN_TOKEN_THRESHOLD, window * prepare / 100) : preparationStartTokens(applyTokens);
|
|
16121
16390
|
return { startTokens, applyTokens };
|
|
16122
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
|
+
}
|
|
16123
16411
|
function snapshotContext(ctx) {
|
|
16124
16412
|
const branch = structuredClone(ctx.sessionManager.getBranch());
|
|
16125
16413
|
const sessionId = resolveSessionId(ctx);
|
|
16126
16414
|
const sessionFile = ctx.sessionManager.getSessionFile?.();
|
|
16127
16415
|
const usage = structuredClone(ctx.getContextUsage());
|
|
16416
|
+
const systemPrompt = ctx.getSystemPrompt?.() ?? "";
|
|
16128
16417
|
const sessionManager = new Proxy(ctx.sessionManager, {
|
|
16129
16418
|
get(target, key) {
|
|
16130
16419
|
if (key === "getBranch")
|
|
@@ -16143,20 +16432,21 @@ function snapshotContext(ctx) {
|
|
|
16143
16432
|
model: ctx.model && { ...ctx.model },
|
|
16144
16433
|
hasUI: false,
|
|
16145
16434
|
getContextUsage: () => usage,
|
|
16435
|
+
getSystemPrompt: () => systemPrompt,
|
|
16146
16436
|
ui: { ...ctx.ui, notify() {}, setStatus() {}, setWidget() {} }
|
|
16147
16437
|
};
|
|
16148
16438
|
return Object.assign(snapshot, { silentBackground: true });
|
|
16149
16439
|
}
|
|
16150
16440
|
function signature(ctx, config) {
|
|
16151
16441
|
const model = ctx.model;
|
|
16152
|
-
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]);
|
|
16153
16443
|
}
|
|
16154
16444
|
function enabled(config) {
|
|
16155
16445
|
return config.autoTrigger && config.autoTriggerStrategy === "background";
|
|
16156
16446
|
}
|
|
16157
16447
|
function createBackgroundPreparation(options) {
|
|
16158
16448
|
const now = options.now ?? Date.now;
|
|
16159
|
-
const
|
|
16449
|
+
const signatureOf = (ctx, config) => signature(ctx, config) + (options.toolSignature?.() ?? "");
|
|
16160
16450
|
const cooldownMs = options.cooldownMs ?? SETTLED_TRIGGER_COOLDOWN_MS;
|
|
16161
16451
|
let current = null;
|
|
16162
16452
|
let lastAttempt;
|
|
@@ -16203,9 +16493,9 @@ function createBackgroundPreparation(options) {
|
|
|
16203
16493
|
return "config";
|
|
16204
16494
|
if (task.sessionId !== resolveSessionId(ctx))
|
|
16205
16495
|
return "session";
|
|
16206
|
-
if (task.signature !==
|
|
16496
|
+
if (task.signature !== signatureOf(ctx, config))
|
|
16207
16497
|
return "config";
|
|
16208
|
-
if (now() - (task.readyAt ?? task.createdAt) > ttlMs)
|
|
16498
|
+
if (now() - (task.readyAt ?? task.createdAt) > (options.ttlMs ?? config.pendingTtlMs))
|
|
16209
16499
|
return "ttl";
|
|
16210
16500
|
const branch = ctx.sessionManager.getBranch();
|
|
16211
16501
|
const origin = branch.findIndex((entry) => entry.id === task.originId);
|
|
@@ -16237,7 +16527,7 @@ function createBackgroundPreparation(options) {
|
|
|
16237
16527
|
const task = {
|
|
16238
16528
|
sessionId,
|
|
16239
16529
|
originId,
|
|
16240
|
-
signature:
|
|
16530
|
+
signature: signatureOf(ctx, config),
|
|
16241
16531
|
controller: new AbortController,
|
|
16242
16532
|
createdAt: now(),
|
|
16243
16533
|
ctx
|
|
@@ -16245,7 +16535,15 @@ function createBackgroundPreparation(options) {
|
|
|
16245
16535
|
current = task;
|
|
16246
16536
|
lastAttempt = { sessionId, at: now() };
|
|
16247
16537
|
const preparationConfig = { ...config, minContextPercent: startTokens / window * 100 };
|
|
16248
|
-
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) => {
|
|
16249
16547
|
if (pending) {
|
|
16250
16548
|
task.pending = pending;
|
|
16251
16549
|
task.readyAt = now();
|
|
@@ -16330,6 +16628,83 @@ function createBackgroundPreparation(options) {
|
|
|
16330
16628
|
};
|
|
16331
16629
|
}
|
|
16332
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
|
+
|
|
16333
16708
|
// src/app/host-cache-ledger.ts
|
|
16334
16709
|
var FOREIGN_REBUILD_WARN_COUNT = 3;
|
|
16335
16710
|
var num = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
@@ -16493,8 +16868,106 @@ function compactionUsage(snapshot, resolveModel) {
|
|
|
16493
16868
|
|
|
16494
16869
|
// src/app/register-context-tools.ts
|
|
16495
16870
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
16871
|
+
import { Text as Text4 } from "@earendil-works/pi-tui";
|
|
16496
16872
|
import { Type as Type3 } from "typebox";
|
|
16497
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
|
+
|
|
16498
16971
|
// src/app/mnemopi-memory.ts
|
|
16499
16972
|
import { execFile as execFile2 } from "child_process";
|
|
16500
16973
|
import { existsSync as existsSync5 } from "fs";
|
|
@@ -16782,6 +17255,15 @@ function registerContextTools(pi) {
|
|
|
16782
17255
|
if (loadConfig().toolLoading === "off" || !pi.getActiveTools().includes("smart_recall"))
|
|
16783
17256
|
throw new Error("smart_recall is disabled in Pi settings or /tools.");
|
|
16784
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);
|
|
16785
17267
|
}
|
|
16786
17268
|
});
|
|
16787
17269
|
pi.registerTool({
|
|
@@ -16816,9 +17298,197 @@ function registerContextTools(pi) {
|
|
|
16816
17298
|
if (loadConfig().toolLoading === "off" || !pi.getActiveTools().includes("smart_save_memory"))
|
|
16817
17299
|
throw new Error("smart_save_memory is disabled in Pi settings or /tools.");
|
|
16818
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);
|
|
16819
17313
|
}
|
|
16820
17314
|
});
|
|
16821
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
|
+
}
|
|
16822
17492
|
function textResult(text, details = undefined) {
|
|
16823
17493
|
return { content: [{ type: "text", text }], details };
|
|
16824
17494
|
}
|
|
@@ -16892,7 +17562,10 @@ Not yet searchable remotely: ` + pending.map((receipt) => receipt.documentId + "
|
|
|
16892
17562
|
}
|
|
16893
17563
|
const kinds = params.kinds?.length ? new Set(params.kinds) : null;
|
|
16894
17564
|
const facts = outcome.facts.filter((fact) => !kinds || !fact.metadata.kind || kinds.has(fact.metadata.kind)).slice(0, params.limit ?? 5);
|
|
16895
|
-
|
|
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 : ""), {
|
|
16896
17569
|
remote: {
|
|
16897
17570
|
state: "ok",
|
|
16898
17571
|
target: describeHindsightTarget(hindsight.target),
|
|
@@ -16900,7 +17573,8 @@ Not yet searchable remotely: ` + pending.map((receipt) => receipt.documentId + "
|
|
|
16900
17573
|
id: fact.id,
|
|
16901
17574
|
documentId: fact.documentId,
|
|
16902
17575
|
type: fact.type,
|
|
16903
|
-
mentionedAt: fact.mentionedAt
|
|
17576
|
+
mentionedAt: fact.mentionedAt,
|
|
17577
|
+
preview: fact.text.replace(/\s+/g, " ").slice(0, 160)
|
|
16904
17578
|
})),
|
|
16905
17579
|
redactions: scrubber.count(),
|
|
16906
17580
|
receipts: refreshed.map((receipt) => ({
|
|
@@ -17019,13 +17693,20 @@ Destination:
|
|
|
17019
17693
|
ref: "hindsight:" + memoryId + "@" + digest,
|
|
17020
17694
|
remote: "receipt" in outcome && outcome.receipt ? {
|
|
17021
17695
|
state: outcome.state,
|
|
17696
|
+
title,
|
|
17022
17697
|
documentId: outcome.receipt.documentId,
|
|
17023
17698
|
operationId: outcome.receipt.operationId,
|
|
17024
17699
|
target: describeHindsightTarget(hindsight.target),
|
|
17025
17700
|
..."reason" in outcome ? { reason: outcome.reason } : {}
|
|
17026
17701
|
} : outcome
|
|
17027
17702
|
};
|
|
17028
|
-
|
|
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);
|
|
17029
17710
|
}
|
|
17030
17711
|
async function resolveMemoryByRef(params, signal, ctx, config, projectId) {
|
|
17031
17712
|
const ref = params.ref ? parseMemoryRef(params.ref) : null;
|
|
@@ -17144,6 +17825,9 @@ function resolveModels(ctx, primary, config, explicit = false) {
|
|
|
17144
17825
|
return { segModel, sumModel, verifyModel };
|
|
17145
17826
|
}
|
|
17146
17827
|
|
|
17828
|
+
// src/app/register-smart-compact-tool.ts
|
|
17829
|
+
import { Text as Text5 } from "@earendil-works/pi-tui";
|
|
17830
|
+
|
|
17147
17831
|
// src/ui/dashboard-format.ts
|
|
17148
17832
|
function dashboardPageSize(rows = process.stdout.rows) {
|
|
17149
17833
|
if (!rows)
|
|
@@ -17489,16 +18173,16 @@ function statusClass(status) {
|
|
|
17489
18173
|
return "warn";
|
|
17490
18174
|
return "good";
|
|
17491
18175
|
}
|
|
17492
|
-
function
|
|
18176
|
+
function statusLabel2(status) {
|
|
17493
18177
|
return status ?? "success";
|
|
17494
18178
|
}
|
|
17495
18179
|
function badge(status) {
|
|
17496
|
-
const label =
|
|
18180
|
+
const label = statusLabel2(status);
|
|
17497
18181
|
return `<span class="badge ${statusClass(label)}">${escapeHtml(label)}</span>`;
|
|
17498
18182
|
}
|
|
17499
18183
|
function summarizeDashboard(entries) {
|
|
17500
18184
|
const durations = entries.map(metricDuration).filter(Boolean);
|
|
17501
|
-
const success = entries.filter((e) =>
|
|
18185
|
+
const success = entries.filter((e) => statusLabel2(e.status) === "success").length;
|
|
17502
18186
|
const timeout = entries.filter((e) => e.status === "timeout").length;
|
|
17503
18187
|
const error = entries.filter((e) => e.status === "error" && e.failureKind !== "cancelled").length;
|
|
17504
18188
|
const dryRun = entries.filter((e) => e.status === "dry-run").length;
|
|
@@ -18009,7 +18693,7 @@ function registerSmartCompactTool(pi, dependencies) {
|
|
|
18009
18693
|
pi.registerTool({
|
|
18010
18694
|
name: "smart_compact",
|
|
18011
18695
|
label: "Smart Compact",
|
|
18012
|
-
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.",
|
|
18013
18697
|
parameters: {
|
|
18014
18698
|
type: "object",
|
|
18015
18699
|
properties: {
|
|
@@ -18055,6 +18739,19 @@ function registerSmartCompactTool(pi, dependencies) {
|
|
|
18055
18739
|
}
|
|
18056
18740
|
}
|
|
18057
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
|
+
},
|
|
18058
18755
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
18059
18756
|
if (!policy.isAgentToolEnabled()) {
|
|
18060
18757
|
return textResult2("smart_compact is hidden from the agent for this session. The user can still run /smart-compact manually.");
|
|
@@ -18078,13 +18775,13 @@ function registerSmartCompactTool(pi, dependencies) {
|
|
|
18078
18775
|
const dashboard = action === "dashboard" ? writeMetricsDashboard() : null;
|
|
18079
18776
|
return textResult2(report + (dashboard ? `
|
|
18080
18777
|
|
|
18081
|
-
Dashboard: ` + dashboard : ""));
|
|
18778
|
+
Dashboard: ` + dashboard : ""), { display: { kind: "metrics", dashboard: dashboard !== null } });
|
|
18082
18779
|
}
|
|
18083
18780
|
const config = loadConfig();
|
|
18084
18781
|
const resolvedMode = mode ?? config.mode;
|
|
18085
18782
|
const sessionId = resolveSessionId(ctx);
|
|
18086
18783
|
if (!dryRun && pendingRef.peek(sessionId)) {
|
|
18087
|
-
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.");
|
|
18088
18785
|
}
|
|
18089
18786
|
const usage = ctx.getContextUsage?.();
|
|
18090
18787
|
const totalTokens = usage?.tokens ?? 0;
|
|
@@ -18128,10 +18825,19 @@ Dashboard: ` + dashboard : ""));
|
|
|
18128
18825
|
content: [
|
|
18129
18826
|
{
|
|
18130
18827
|
type: "text",
|
|
18131
|
-
text: (staged.details.method === "native" ? "Native compaction prepared (" + staged.details.model + "; provider state, not EESV-verified). Tokens: " : "Smart summary prepared (" + resolvedMode + " \u2192 " + (staged.details.mode ?? staged.details.profile) + "). Tokens: ") + (staged.tokensBefore ?? 0).toLocaleString() + " \u2014 staged, not applied, for " +
|
|
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."
|
|
18132
18829
|
}
|
|
18133
18830
|
],
|
|
18134
|
-
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
|
+
}
|
|
18135
18841
|
};
|
|
18136
18842
|
}
|
|
18137
18843
|
if (outcome.kind === "dry-run") {
|
|
@@ -18143,13 +18849,16 @@ Dashboard: ` + dashboard : ""));
|
|
|
18143
18849
|
text: "Dry run finished (" + resolvedMode + ", " + seconds + "s). Pipeline ran successfully; no summary was staged."
|
|
18144
18850
|
}
|
|
18145
18851
|
],
|
|
18146
|
-
details:
|
|
18852
|
+
details: {
|
|
18853
|
+
...outcome.details,
|
|
18854
|
+
display: { state: "dry-run", mode: resolvedMode, seconds: Number(seconds) }
|
|
18855
|
+
}
|
|
18147
18856
|
};
|
|
18148
18857
|
}
|
|
18149
18858
|
if (outcome.kind === "cancelled") {
|
|
18150
|
-
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 } });
|
|
18151
18860
|
}
|
|
18152
|
-
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" } });
|
|
18153
18862
|
} catch (error) {
|
|
18154
18863
|
recordIssue({ key: "tool.smart-compact", message: "smart_compact failed: " + errorDetail(error) + ".", error });
|
|
18155
18864
|
throw new Error(formatCompactErrorForUi(error));
|
|
@@ -18157,13 +18866,42 @@ Dashboard: ` + dashboard : ""));
|
|
|
18157
18866
|
}
|
|
18158
18867
|
});
|
|
18159
18868
|
}
|
|
18160
|
-
function textResult2(text) {
|
|
18161
|
-
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);
|
|
18162
18899
|
}
|
|
18163
18900
|
|
|
18164
18901
|
// src/app/register-smart-context-tool.ts
|
|
18165
18902
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
18166
18903
|
import { StringEnum as StringEnum2 } from "@earendil-works/pi-ai";
|
|
18904
|
+
import { Text as Text6 } from "@earendil-works/pi-tui";
|
|
18167
18905
|
import { Type as Type4 } from "typebox";
|
|
18168
18906
|
|
|
18169
18907
|
// src/app/context-evidence.ts
|
|
@@ -18411,11 +19149,11 @@ function registerSmartContextTool(pi, options = {}) {
|
|
|
18411
19149
|
const current = config();
|
|
18412
19150
|
return new SecretScrubber(current.scrubSecrets, current.scrubPii).scrubText(text).value;
|
|
18413
19151
|
};
|
|
18414
|
-
const reply = (text) => ({ content: [{ type: "text", text }], details
|
|
19152
|
+
const reply = (text, details = undefined) => ({ content: [{ type: "text", text }], details });
|
|
18415
19153
|
pi.registerTool({
|
|
18416
19154
|
name: TOOL_NAME,
|
|
18417
19155
|
label: "Session Context",
|
|
18418
|
-
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.",
|
|
18419
19157
|
parameters: Type4.Object({
|
|
18420
19158
|
action: StringEnum2(["status", "plan", "checkpoint", "rewind", "trim", "read", "search"]),
|
|
18421
19159
|
label: Type4.Optional(Type4.String({ maxLength: 120, description: "Checkpoint label." })),
|
|
@@ -18439,15 +19177,28 @@ function registerSmartContextTool(pi, options = {}) {
|
|
|
18439
19177
|
const branch = ctx.sessionManager.getBranch();
|
|
18440
19178
|
const state = inspectContext(branch, sessionId);
|
|
18441
19179
|
if (params.action === "plan") {
|
|
18442
|
-
const plan = planContextTrim(branch, state.checkpoint?.originId);
|
|
18443
|
-
|
|
19180
|
+
const plan = planContextTrim(branch, state.checkpoint?.originId, { readerApi: ctx.model?.api });
|
|
19181
|
+
const payload = {
|
|
19182
|
+
pressure: contextPressure(ctx, config()),
|
|
19183
|
+
blockedReason: plan.blockedReason,
|
|
18444
19184
|
outputs: plan.references.length,
|
|
18445
19185
|
superseded: plan.superseded,
|
|
18446
19186
|
savedChars: plan.savedChars,
|
|
18447
19187
|
batch: plan.automatic,
|
|
18448
19188
|
cooldownTurns: plan.cooldownTurns,
|
|
18449
|
-
note: "No changes applied. Automatic
|
|
18450
|
-
}
|
|
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
|
+
});
|
|
18451
19202
|
}
|
|
18452
19203
|
const offset = params.offset ?? 0;
|
|
18453
19204
|
if (params.action === "status" || params.action === "read" || params.action === "search") {
|
|
@@ -18464,7 +19215,20 @@ function registerSmartContextTool(pi, options = {}) {
|
|
|
18464
19215
|
const evidence = contextEvidence(branch, sessionId, new SecretScrubber(settings.scrubSecrets, settings.scrubPii), lineage);
|
|
18465
19216
|
if (params.action === "status") {
|
|
18466
19217
|
const sources = evidence.list.slice(offset, offset + limit);
|
|
18467
|
-
|
|
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,
|
|
18468
19232
|
checkpoint: state.checkpoint ? { id: state.checkpoint.id, label: state.checkpoint.label } : null,
|
|
18469
19233
|
rewind: Boolean(state.checkpoint),
|
|
18470
19234
|
reason: state.invalidReason,
|
|
@@ -18480,7 +19244,21 @@ function registerSmartContextTool(pi, options = {}) {
|
|
|
18480
19244
|
sources: evidence.list.filter((source) => source.session === parent.sessionId).length
|
|
18481
19245
|
})) } : {},
|
|
18482
19246
|
nextOffset: offset + limit < evidence.list.length ? offset + limit : null
|
|
18483
|
-
}
|
|
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
|
+
});
|
|
18484
19262
|
}
|
|
18485
19263
|
if (params.action === "search") {
|
|
18486
19264
|
if (!params.query)
|
|
@@ -18501,6 +19279,12 @@ ${page.text}`);
|
|
|
18501
19279
|
if (options.canAgentMutate?.(ctx) === false) {
|
|
18502
19280
|
throw new Error("Agent-requested context changes are disabled by policy; status, plan, search and read stay available.");
|
|
18503
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
|
+
}
|
|
18504
19288
|
if (options.isPaused?.(ctx))
|
|
18505
19289
|
throw new Error("Context changes are paused while a navigation pivot is pending.");
|
|
18506
19290
|
if (queued)
|
|
@@ -18527,7 +19311,21 @@ ${page.text}`);
|
|
|
18527
19311
|
text: params.action === "rewind" ? text : text.slice(0, 120),
|
|
18528
19312
|
signal
|
|
18529
19313
|
};
|
|
18530
|
-
return reply(`${params.action} queued for the completed tool batch. ${params.action === "rewind" ? "Stop research here; files and processes stay unchanged." : "Use status to inspect the committed state."}
|
|
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);
|
|
18531
19329
|
}
|
|
18532
19330
|
});
|
|
18533
19331
|
const clear = () => {
|
|
@@ -18574,7 +19372,8 @@ ${page.text}`);
|
|
|
18574
19372
|
if (!current) {
|
|
18575
19373
|
if (mark?.sessionId !== sessionId)
|
|
18576
19374
|
return;
|
|
18577
|
-
|
|
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)) {
|
|
18578
19377
|
mark = null;
|
|
18579
19378
|
return;
|
|
18580
19379
|
}
|
|
@@ -18599,7 +19398,7 @@ ${page.text}`);
|
|
|
18599
19398
|
return changed ? { messages } : undefined;
|
|
18600
19399
|
};
|
|
18601
19400
|
const vetoWarming = (event, ctx, sessionId) => {
|
|
18602
|
-
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))
|
|
18603
19402
|
return false;
|
|
18604
19403
|
const cost = ctx.model?.cost;
|
|
18605
19404
|
const price = cost ? cost.cacheWrite > 0 ? cost.cacheWrite : cost.input : NaN;
|
|
@@ -18617,15 +19416,25 @@ ${page.text}`);
|
|
|
18617
19416
|
if (event.outcome !== "completed" || request?.signal?.aborted) {
|
|
18618
19417
|
return request ? cancelled(event, "The turn did not complete.") : undefined;
|
|
18619
19418
|
}
|
|
18620
|
-
|
|
19419
|
+
const requestTool = request?.anchorBoundary !== undefined ? "smart_navigation" : TOOL_NAME;
|
|
19420
|
+
if (request && !request.manual && (config().toolLoading === "off" || !pi.getActiveTools().includes(requestTool)))
|
|
18621
19421
|
return cancelled(event, "Agent tool access was revoked.");
|
|
19422
|
+
if (request?.anchorBoundary !== undefined && !config().contextNavigationEnabled)
|
|
19423
|
+
return cancelled(event, "Navigation was disabled.");
|
|
18622
19424
|
if (options.isPaused?.(ctx)) {
|
|
18623
19425
|
return request ? cancelled(event, "A pending navigation pivot takes priority.") : undefined;
|
|
18624
19426
|
}
|
|
18625
19427
|
if (request && !request.manual && options.canAgentMutate?.(ctx) === false) {
|
|
18626
19428
|
return cancelled(event, "Agent-requested context changes are disabled by policy.");
|
|
18627
19429
|
}
|
|
18628
|
-
|
|
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
|
+
}
|
|
18629
19438
|
if (!request) {
|
|
18630
19439
|
const configNow = config();
|
|
18631
19440
|
const hygiene = configNow.contextHygieneEnabled;
|
|
@@ -18634,11 +19443,11 @@ ${page.text}`);
|
|
|
18634
19443
|
const enabled = hygiene || configNow.autoTrigger && configNow.autoTriggerStrategy === "background";
|
|
18635
19444
|
if (!enabled || options.canAutoTrim?.(ctx) === false)
|
|
18636
19445
|
return;
|
|
18637
|
-
|
|
18638
|
-
|
|
18639
|
-
|
|
18640
|
-
|
|
18641
|
-
|
|
19446
|
+
if ((configNow.contextPressureOnly || !hygiene) && !pressure) {
|
|
19447
|
+
mark = null;
|
|
19448
|
+
if (!applied)
|
|
19449
|
+
return;
|
|
19450
|
+
}
|
|
18642
19451
|
}
|
|
18643
19452
|
const branch = ctx.sessionManager.getBranch();
|
|
18644
19453
|
const sessionId = resolveSessionId(ctx);
|
|
@@ -18669,13 +19478,15 @@ ${page.text}`);
|
|
|
18669
19478
|
}
|
|
18670
19479
|
})];
|
|
18671
19480
|
} else if (request?.action === "rewind") {
|
|
18672
|
-
entries = planContextRewind(branch, sessionId, request.checkpointId, request.text).entries;
|
|
19481
|
+
entries = planContextRewind(branch, sessionId, request.checkpointId, request.text, ctx.model?.api).entries;
|
|
18673
19482
|
} else if (!request && applied?.sessionId === sessionId && unchangedSince(branch, applied.leafId)) {
|
|
18674
19483
|
mark = null;
|
|
18675
19484
|
entries = applied.entries;
|
|
18676
19485
|
applied = null;
|
|
18677
19486
|
} else {
|
|
18678
|
-
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;
|
|
18679
19490
|
if (request) {
|
|
18680
19491
|
entries = trimEntries(plan, request.manual ? "manual" : "agent");
|
|
18681
19492
|
} else {
|
|
@@ -18716,41 +19527,54 @@ ${page.text}`);
|
|
|
18716
19527
|
}
|
|
18717
19528
|
return { entries: [...event.entries, ...entries] };
|
|
18718
19529
|
});
|
|
18719
|
-
const
|
|
18720
|
-
|
|
18721
|
-
|
|
18722
|
-
|
|
18723
|
-
|
|
18724
|
-
|
|
18725
|
-
|
|
18726
|
-
|
|
18727
|
-
|
|
18728
|
-
|
|
18729
|
-
|
|
18730
|
-
|
|
18731
|
-
|
|
18732
|
-
|
|
18733
|
-
|
|
18734
|
-
|
|
18735
|
-
|
|
18736
|
-
|
|
18737
|
-
|
|
18738
|
-
|
|
18739
|
-
|
|
18740
|
-
|
|
18741
|
-
|
|
18742
|
-
|
|
18743
|
-
|
|
18744
|
-
|
|
18745
|
-
|
|
18746
|
-
|
|
18747
|
-
|
|
18748
|
-
|
|
18749
|
-
|
|
18750
|
-
|
|
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 }),
|
|
18751
19576
|
deferredTrim: deferred
|
|
18752
19577
|
};
|
|
18753
|
-
return controller;
|
|
18754
19578
|
}
|
|
18755
19579
|
function cancelled(event, reason) {
|
|
18756
19580
|
return {
|
|
@@ -18762,6 +19586,48 @@ function cancelled(event, reason) {
|
|
|
18762
19586
|
}]
|
|
18763
19587
|
};
|
|
18764
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
|
+
}
|
|
18765
19631
|
|
|
18766
19632
|
// src/app/model-feasibility.ts
|
|
18767
19633
|
async function createModelFeasibilityResolver(ctx, config, requestedMode = config.mode, steering = {}) {
|
|
@@ -19175,7 +20041,7 @@ import {
|
|
|
19175
20041
|
getKeybindings as getKeybindings2,
|
|
19176
20042
|
Key as Key4,
|
|
19177
20043
|
matchesKey as matchesKey4,
|
|
19178
|
-
Text as
|
|
20044
|
+
Text as Text9,
|
|
19179
20045
|
wrapTextWithAnsi as wrapTextWithAnsi3
|
|
19180
20046
|
} from "@earendil-works/pi-tui";
|
|
19181
20047
|
|
|
@@ -19184,7 +20050,7 @@ import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
|
19184
20050
|
import {
|
|
19185
20051
|
getKeybindings,
|
|
19186
20052
|
SettingsList,
|
|
19187
|
-
truncateToWidth as
|
|
20053
|
+
truncateToWidth as truncateToWidth4,
|
|
19188
20054
|
visibleWidth as visibleWidth2,
|
|
19189
20055
|
wrapTextWithAnsi as wrapTextWithAnsi2
|
|
19190
20056
|
} from "@earendil-works/pi-tui";
|
|
@@ -19230,8 +20096,8 @@ class SmartSettingsList extends SettingsList {
|
|
|
19230
20096
|
internals.items.forEach((item, index) => {
|
|
19231
20097
|
const original = saved[index];
|
|
19232
20098
|
const shown = (this.inactive.has(item.id) ? INACTIVE_PREFIX : "") + original.currentValue;
|
|
19233
|
-
item.label =
|
|
19234
|
-
item.currentValue =
|
|
20099
|
+
item.label = truncateToWidth4(original.label, labelWidth, "\u2026");
|
|
20100
|
+
item.currentValue = truncateToWidth4(shown, valueWidth, "\u2026");
|
|
19235
20101
|
if (index === internals.selectedIndex) {
|
|
19236
20102
|
item.description = [
|
|
19237
20103
|
item.label !== original.label ? original.label : "",
|
|
@@ -19272,7 +20138,7 @@ class SmartSettingsList extends SettingsList {
|
|
|
19272
20138
|
// src/ui/settings-overlay.ts
|
|
19273
20139
|
import {
|
|
19274
20140
|
Container as Container4,
|
|
19275
|
-
Text as
|
|
20141
|
+
Text as Text8
|
|
19276
20142
|
} from "@earendil-works/pi-tui";
|
|
19277
20143
|
|
|
19278
20144
|
// src/ui/settings-complex.ts
|
|
@@ -19283,7 +20149,7 @@ import {
|
|
|
19283
20149
|
Key as Key3,
|
|
19284
20150
|
matchesKey as matchesKey3,
|
|
19285
20151
|
SelectList as SelectList3,
|
|
19286
|
-
Text as
|
|
20152
|
+
Text as Text7
|
|
19287
20153
|
} from "@earendil-works/pi-tui";
|
|
19288
20154
|
var MODEL_SETTINGS = [
|
|
19289
20155
|
{
|
|
@@ -19331,7 +20197,7 @@ var MIN_CONTEXT_SETTING = {
|
|
|
19331
20197
|
var MAX_CONTEXT_SETTING = {
|
|
19332
20198
|
id: "maxContextTokens",
|
|
19333
20199
|
label: "Context cap for start % (tokens)",
|
|
19334
|
-
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.",
|
|
19335
20201
|
placeholder: "0 or 16384\u20132000000; blank = off",
|
|
19336
20202
|
parse: numberParser(CONFIG_NUMERIC_LIMITS.maxContextTokens),
|
|
19337
20203
|
format: (value) => value === undefined || value === 0 ? "off" : String(value)
|
|
@@ -19339,8 +20205,8 @@ var MAX_CONTEXT_SETTING = {
|
|
|
19339
20205
|
var parsePreparePercent = numberParser(CONFIG_NUMERIC_LIMITS.prepareContextPercent);
|
|
19340
20206
|
var PREPARE_CONTEXT_SETTING = {
|
|
19341
20207
|
id: "prepareContextPercent",
|
|
19342
|
-
label: "
|
|
19343
|
-
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.",
|
|
19344
20210
|
placeholder: "0\u2013100 below Start at; auto or blank = Auto",
|
|
19345
20211
|
parse: (input) => input.trim().toLowerCase() === "auto" ? undefined : parsePreparePercent(input),
|
|
19346
20212
|
format: (value) => value === undefined || value === null ? "Auto" : String(value)
|
|
@@ -19549,7 +20415,7 @@ class InputSettingEditor extends Container3 {
|
|
|
19549
20415
|
save;
|
|
19550
20416
|
done;
|
|
19551
20417
|
input = new Input;
|
|
19552
|
-
status = new
|
|
20418
|
+
status = new Text7("", 0, 0);
|
|
19553
20419
|
saving = false;
|
|
19554
20420
|
pending = Promise.resolve();
|
|
19555
20421
|
get focused() {
|
|
@@ -19563,10 +20429,10 @@ class InputSettingEditor extends Container3 {
|
|
|
19563
20429
|
this.requestRender = requestRender;
|
|
19564
20430
|
this.save = save;
|
|
19565
20431
|
this.done = done;
|
|
19566
|
-
this.addChild(new
|
|
19567
|
-
this.addChild(new
|
|
19568
|
-
this.addChild(new
|
|
19569
|
-
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));
|
|
19570
20436
|
this.input.setValue(initial);
|
|
19571
20437
|
this.input.handleInput("\x1B[F");
|
|
19572
20438
|
this.input.onSubmit = (value) => {
|
|
@@ -19594,9 +20460,9 @@ class InputSettingEditor extends Container3 {
|
|
|
19594
20460
|
this.done();
|
|
19595
20461
|
};
|
|
19596
20462
|
this.addChild(this.input);
|
|
19597
|
-
this.addChild(new
|
|
20463
|
+
this.addChild(new Text7("", 0, 0));
|
|
19598
20464
|
this.addChild(this.status);
|
|
19599
|
-
this.addChild(new
|
|
20465
|
+
this.addChild(new Text7("Enter save \xB7 Esc cancel", 0, 0));
|
|
19600
20466
|
}
|
|
19601
20467
|
handleInput(data) {
|
|
19602
20468
|
this.input.handleInput(data);
|
|
@@ -19611,7 +20477,7 @@ class ModelSettingEditor extends Container3 {
|
|
|
19611
20477
|
requestRender;
|
|
19612
20478
|
search = new Input;
|
|
19613
20479
|
list;
|
|
19614
|
-
status = new
|
|
20480
|
+
status = new Text7("", 0, 0);
|
|
19615
20481
|
saving = false;
|
|
19616
20482
|
pending = Promise.resolve();
|
|
19617
20483
|
get focused() {
|
|
@@ -19623,9 +20489,9 @@ class ModelSettingEditor extends Container3 {
|
|
|
19623
20489
|
constructor(models, selectedValue, requestRender, save, done) {
|
|
19624
20490
|
super();
|
|
19625
20491
|
this.requestRender = requestRender;
|
|
19626
|
-
this.addChild(new
|
|
20492
|
+
this.addChild(new Text7("Type to search models", 0, 0));
|
|
19627
20493
|
this.addChild(this.search);
|
|
19628
|
-
this.addChild(new
|
|
20494
|
+
this.addChild(new Text7("", 0, 0));
|
|
19629
20495
|
this.list = new SelectList3(models, 10, {
|
|
19630
20496
|
selectedPrefix: (text) => text,
|
|
19631
20497
|
selectedText: (text) => text,
|
|
@@ -19660,7 +20526,7 @@ class ModelSettingEditor extends Container3 {
|
|
|
19660
20526
|
};
|
|
19661
20527
|
this.addChild(this.list);
|
|
19662
20528
|
this.addChild(this.status);
|
|
19663
|
-
this.addChild(new
|
|
20529
|
+
this.addChild(new Text7("Type to filter \xB7 Enter select \xB7 Esc cancel", 0, 0));
|
|
19664
20530
|
}
|
|
19665
20531
|
handleInput(data) {
|
|
19666
20532
|
if (matchesKey3(data, Key3.up) || matchesKey3(data, Key3.down) || matchesKey3(data, Key3.enter) || matchesKey3(data, Key3.escape)) {
|
|
@@ -19899,7 +20765,6 @@ var THINKING_VALUES = [
|
|
|
19899
20765
|
"max"
|
|
19900
20766
|
];
|
|
19901
20767
|
var needsAutoTrigger = (config) => config.autoTrigger ? undefined : "turn on Automatic compaction first";
|
|
19902
|
-
var needsBackgroundTiming = (config) => config.autoTrigger && config.autoTriggerStrategy === "background" ? undefined : "used only when Start when = Prepare in background";
|
|
19903
20768
|
var needsMnemopi = (config) => config.memoryBackend === "mnemopi" ? undefined : "used only when Memory store = Mnemopi";
|
|
19904
20769
|
var needsHindsight = (config) => config.memoryBackend === "hindsight" ? undefined : "used only when Memory store = Hindsight server";
|
|
19905
20770
|
var needsBackups = (config) => config.backupEnabled ? undefined : "turn on Backups first";
|
|
@@ -19930,9 +20795,9 @@ var CATEGORIES = [
|
|
|
19930
20795
|
labels: { "native-hook": "Before Pi's compaction", settled: "When idle", background: "Prepare in background" }
|
|
19931
20796
|
}
|
|
19932
20797
|
},
|
|
19933
|
-
{ kind: "input", setting: MIN_CONTEXT_SETTING
|
|
19934
|
-
{ kind: "input", setting: MAX_CONTEXT_SETTING
|
|
19935
|
-
{ 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 },
|
|
19936
20801
|
{
|
|
19937
20802
|
kind: "choice",
|
|
19938
20803
|
setting: {
|
|
@@ -20047,7 +20912,7 @@ var CATEGORIES = [
|
|
|
20047
20912
|
setting: {
|
|
20048
20913
|
id: "toolLoading",
|
|
20049
20914
|
label: "Agent tools",
|
|
20050
|
-
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.",
|
|
20051
20916
|
values: ["lazy", "eager", "off"],
|
|
20052
20917
|
labels: { lazy: "On demand", eager: "Always available", off: "Off" }
|
|
20053
20918
|
}
|
|
@@ -20116,17 +20981,27 @@ var CATEGORIES = [
|
|
|
20116
20981
|
{
|
|
20117
20982
|
id: "hygiene",
|
|
20118
20983
|
label: "Tool output cleanup",
|
|
20119
|
-
description: "
|
|
20984
|
+
description: "Recoverable, batched cleanup; no model call, but history edits can rebuild the prompt cache",
|
|
20120
20985
|
rows: [
|
|
20121
20986
|
{
|
|
20122
20987
|
kind: "choice",
|
|
20123
20988
|
setting: {
|
|
20124
20989
|
id: "contextHygieneEnabled",
|
|
20125
20990
|
label: "Automatic cleanup",
|
|
20126
|
-
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.",
|
|
20127
20992
|
values: BOOLEAN_VALUES
|
|
20128
20993
|
}
|
|
20129
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
|
+
},
|
|
20130
21005
|
{
|
|
20131
21006
|
kind: "choice",
|
|
20132
21007
|
setting: {
|
|
@@ -20656,16 +21531,16 @@ async function showSmartCompactSettings(ctx, policy, coordinator = new GlobalSet
|
|
|
20656
21531
|
await ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
20657
21532
|
const root = createSettingsRoot(policy, ctx, done, () => tui.requestRender(), coordinator, onApplied, writeConfig);
|
|
20658
21533
|
const container = new Container4;
|
|
20659
|
-
container.addChild(new
|
|
20660
|
-
container.addChild(new
|
|
20661
|
-
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));
|
|
20662
21537
|
const invalid = invalidSettingsLine();
|
|
20663
21538
|
if (invalid)
|
|
20664
|
-
container.addChild(new
|
|
20665
|
-
container.addChild(new
|
|
21539
|
+
container.addChild(new Text8(theme.fg("warning", invalid), 0, 0));
|
|
21540
|
+
container.addChild(new Text8("", 0, 0));
|
|
20666
21541
|
container.addChild(root);
|
|
20667
|
-
container.addChild(new
|
|
20668
|
-
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));
|
|
20669
21544
|
return createSettingsController(root, container, () => tui.requestRender());
|
|
20670
21545
|
});
|
|
20671
21546
|
}
|
|
@@ -20691,7 +21566,7 @@ var BEHAVIOR_PROFILES = [
|
|
|
20691
21566
|
{
|
|
20692
21567
|
id: "cleanup",
|
|
20693
21568
|
label: "Cleanup only",
|
|
20694
|
-
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.",
|
|
20695
21570
|
patch: { autoTrigger: false, contextHygieneEnabled: true, agentToolAccess: "disabled" }
|
|
20696
21571
|
},
|
|
20697
21572
|
{
|
|
@@ -20712,11 +21587,11 @@ var AGENT_WORDS = {
|
|
|
20712
21587
|
disabled: "not allowed"
|
|
20713
21588
|
};
|
|
20714
21589
|
function deriveBehaviorProfile(config) {
|
|
20715
|
-
if (config.autoTrigger === DEFAULT_CONFIG.autoTrigger && config.autoTriggerStrategy === DEFAULT_CONFIG.autoTriggerStrategy && config.contextHygieneEnabled === DEFAULT_CONFIG.contextHygieneEnabled && config.agentToolAccess === DEFAULT_CONFIG.agentToolAccess) {
|
|
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) {
|
|
20716
21591
|
return {
|
|
20717
21592
|
id: "custom",
|
|
20718
|
-
label: "
|
|
20719
|
-
detail: "
|
|
21593
|
+
label: "Pressure-first (default)",
|
|
21594
|
+
detail: "Batched cleanup under early pressure, then idle compaction at the apply gate; agent access follows Pi."
|
|
20720
21595
|
};
|
|
20721
21596
|
}
|
|
20722
21597
|
const found = derive(BEHAVIOR_PROFILES, ({ patch }) => Object.entries(patch).every(([key, value]) => config[key] === value), "Custom");
|
|
@@ -20921,7 +21796,7 @@ function createHomeList(options, finish, requestRender) {
|
|
|
20921
21796
|
id: "trim",
|
|
20922
21797
|
label: "Clean up tool output",
|
|
20923
21798
|
currentValue: "no model call",
|
|
20924
|
-
description: "Queue cleanup for the next completed turn. Older output stays retrievable; the next model request is still sent untrimmed."
|
|
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."
|
|
20925
21800
|
};
|
|
20926
21801
|
const setup = {
|
|
20927
21802
|
id: "setup",
|
|
@@ -21162,7 +22037,7 @@ async function showSmartCompactHome(options) {
|
|
|
21162
22037
|
return options.ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
21163
22038
|
const list = createHomeList(options, done, () => tui.requestRender());
|
|
21164
22039
|
const container = new Container5;
|
|
21165
|
-
container.addChild(new
|
|
22040
|
+
container.addChild(new Text9(theme.fg("accent", theme.bold("Smart Compact")), 0, 0));
|
|
21166
22041
|
container.addChild({
|
|
21167
22042
|
render(width) {
|
|
21168
22043
|
const config = loadConfig();
|
|
@@ -21178,15 +22053,15 @@ async function showSmartCompactHome(options) {
|
|
|
21178
22053
|
});
|
|
21179
22054
|
const invalid = invalidSettingsLine();
|
|
21180
22055
|
if (invalid)
|
|
21181
|
-
container.addChild(new
|
|
21182
|
-
container.addChild(new
|
|
22056
|
+
container.addChild(new Text9(theme.fg("warning", invalid), 0, 0));
|
|
22057
|
+
container.addChild(new Text9("", 0, 0));
|
|
21183
22058
|
container.addChild(list);
|
|
21184
22059
|
return createSettingsController(list, container, () => tui.requestRender());
|
|
21185
22060
|
});
|
|
21186
22061
|
}
|
|
21187
22062
|
|
|
21188
22063
|
// src/ui/metrics-dashboard-overlay.ts
|
|
21189
|
-
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";
|
|
21190
22065
|
async function showMetricsDashboardUI(ctx, opts) {
|
|
21191
22066
|
const entries = opts.entries;
|
|
21192
22067
|
const latest = entries.at(-1);
|
|
@@ -21281,23 +22156,23 @@ async function showMetricsDashboardUI(ctx, opts) {
|
|
|
21281
22156
|
return theme.fg(insights.quality.targetMet ? "success" : "warning", " \u2022 Quality " + insights.quality.healthScore + "/100");
|
|
21282
22157
|
};
|
|
21283
22158
|
const renderHeader = (width) => [
|
|
21284
|
-
|
|
21285
|
-
|
|
21286
|
-
|
|
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)
|
|
21287
22162
|
];
|
|
21288
22163
|
return {
|
|
21289
22164
|
render(width) {
|
|
21290
22165
|
const lines = renderHeader(width);
|
|
21291
22166
|
if (view === "menu") {
|
|
21292
|
-
lines.push(
|
|
22167
|
+
lines.push(truncateToWidth5(theme.fg("text", " Choose what to inspect:"), width), "");
|
|
21293
22168
|
for (let index = 0;index < menuItems.length; index++) {
|
|
21294
22169
|
const item = menuItems[index];
|
|
21295
22170
|
const active = index === selected;
|
|
21296
22171
|
const label = active ? theme.fg("accent", theme.bold(item.label)) : theme.fg("text", item.label);
|
|
21297
|
-
lines.push(
|
|
21298
|
-
lines.push(
|
|
22172
|
+
lines.push(truncateToWidth5((active ? " \u203A " : " ") + label, width));
|
|
22173
|
+
lines.push(truncateToWidth5(" " + theme.fg(active ? "muted" : "dim", item.desc), width));
|
|
21299
22174
|
}
|
|
21300
|
-
lines.push("",
|
|
22175
|
+
lines.push("", truncateToWidth5(theme.fg("dim", " \u2191\u2193 navigate \u2022 enter open \u2022 esc/q close"), width));
|
|
21301
22176
|
return lines;
|
|
21302
22177
|
}
|
|
21303
22178
|
const content = pageLines();
|
|
@@ -21310,12 +22185,12 @@ async function showMetricsDashboardUI(ctx, opts) {
|
|
|
21310
22185
|
styled = theme.fg("accent", theme.bold(line));
|
|
21311
22186
|
else if (line.startsWith("-"))
|
|
21312
22187
|
styled = theme.fg("dim", line);
|
|
21313
|
-
lines.push(
|
|
22188
|
+
lines.push(truncateToWidth5(" " + styled, width));
|
|
21314
22189
|
}
|
|
21315
22190
|
if (content.length > pageSize) {
|
|
21316
|
-
lines.push(
|
|
22191
|
+
lines.push(truncateToWidth5(theme.fg("dim", " showing " + (scroll + 1) + "-" + Math.min(content.length, scroll + pageSize) + " of " + content.length), width));
|
|
21317
22192
|
}
|
|
21318
|
-
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));
|
|
21319
22194
|
return lines;
|
|
21320
22195
|
},
|
|
21321
22196
|
invalidate() {},
|
|
@@ -21377,7 +22252,7 @@ import {
|
|
|
21377
22252
|
import {
|
|
21378
22253
|
Container as Container6,
|
|
21379
22254
|
Input as Input2,
|
|
21380
|
-
Text as
|
|
22255
|
+
Text as Text10,
|
|
21381
22256
|
wrapTextWithAnsi as wrapTextWithAnsi4
|
|
21382
22257
|
} from "@earendil-works/pi-tui";
|
|
21383
22258
|
var PAGE_SIZE = 8;
|
|
@@ -21486,7 +22361,7 @@ class Framed {
|
|
|
21486
22361
|
|
|
21487
22362
|
class Prompt extends Container6 {
|
|
21488
22363
|
input;
|
|
21489
|
-
status = new
|
|
22364
|
+
status = new Text10("", 0, 0);
|
|
21490
22365
|
busy = false;
|
|
21491
22366
|
get focused() {
|
|
21492
22367
|
return this.input.focused;
|
|
@@ -21498,8 +22373,8 @@ class Prompt extends Container6 {
|
|
|
21498
22373
|
super();
|
|
21499
22374
|
this.input = new Input2;
|
|
21500
22375
|
for (const line of lines)
|
|
21501
|
-
this.addChild(new
|
|
21502
|
-
this.addChild(new
|
|
22376
|
+
this.addChild(new Text10(line, 0, 0));
|
|
22377
|
+
this.addChild(new Text10("", 0, 0));
|
|
21503
22378
|
this.input.setValue(initial);
|
|
21504
22379
|
this.input.handleInput("\x1B[F");
|
|
21505
22380
|
this.input.onSubmit = (value) => {
|
|
@@ -21511,9 +22386,9 @@ class Prompt extends Container6 {
|
|
|
21511
22386
|
cancel();
|
|
21512
22387
|
};
|
|
21513
22388
|
this.addChild(this.input);
|
|
21514
|
-
this.addChild(new
|
|
22389
|
+
this.addChild(new Text10("", 0, 0));
|
|
21515
22390
|
this.addChild(this.status);
|
|
21516
|
-
this.addChild(new
|
|
22391
|
+
this.addChild(new Text10(hint, 0, 0));
|
|
21517
22392
|
}
|
|
21518
22393
|
setStatus(text, busy = false) {
|
|
21519
22394
|
this.status.setText(text);
|
|
@@ -21528,7 +22403,7 @@ class TextArea {
|
|
|
21528
22403
|
open;
|
|
21529
22404
|
submit;
|
|
21530
22405
|
editor;
|
|
21531
|
-
status = new
|
|
22406
|
+
status = new Text10("", 0, 0);
|
|
21532
22407
|
busy = false;
|
|
21533
22408
|
active = false;
|
|
21534
22409
|
constructor(open, submit) {
|
|
@@ -22270,11 +23145,13 @@ async function describeEffectiveState(ctx, config, policy, runtime) {
|
|
|
22270
23145
|
lines.push("Agent tool: " + (policy.agentToolEnabled ? "exposed" : "not exposed") + " (policy " + policy.agentToolAccess + ")");
|
|
22271
23146
|
if (ctx.model && !ctx.model.promptCache)
|
|
22272
23147
|
lines.push("Reader prompt-cache lifetime: not declared; host cache warming cannot schedule from this metadata. No cache savings are assumed.");
|
|
22273
|
-
const
|
|
22274
|
-
const
|
|
22275
|
-
const
|
|
22276
|
-
|
|
22277
|
-
|
|
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) + "%"));
|
|
22278
23155
|
const apply = Math.ceil(Math.max(MIN_TOKEN_THRESHOLD, gates.applyTokens)).toLocaleString("en-US");
|
|
22279
23156
|
const gate = " (" + config.minContextPercent + "% of " + (gateWindow < window ? "maxContextTokens " : "window ") + gateWindow.toLocaleString("en-US") + ", floor " + MIN_TOKEN_THRESHOLD.toLocaleString("en-US") + ")";
|
|
22280
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));
|
|
@@ -22286,9 +23163,9 @@ async function describeEffectiveState(ctx, config, policy, runtime) {
|
|
|
22286
23163
|
lines.push("Runtime: " + (runtime ? (runtime.running ? "running" : "idle") + "; preparation " + runtime.preparation : "not attached") + "; candidate reuse is revalidated at apply");
|
|
22287
23164
|
if (runtime?.cacheLedger?.length)
|
|
22288
23165
|
lines.push(...runtime.cacheLedger);
|
|
22289
|
-
lines.push("Auto limits: " + Math.min(config.autoTriggerTimeoutMs, AUTO_TRIGGER_TIMEOUT_CAP_MS) / 1000 + " s timeout; " + SETTLED_TRIGGER_COOLDOWN_MS / 60000 + " min cooldown; " +
|
|
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");
|
|
22290
23167
|
const backgroundHygiene = policy.autoTrigger && config.autoTriggerStrategy === "background";
|
|
22291
|
-
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" : ""));
|
|
22292
23169
|
const memory = await describeMemoryBackendReadiness(config);
|
|
22293
23170
|
lines.push("Memory selected: " + config.memoryBackend + (projectId ? " (project-bound)" : " \u2014 unavailable: no project scope"));
|
|
22294
23171
|
lines.push("Memory readiness: " + memory.reason);
|
|
@@ -22530,7 +23407,8 @@ async function showStorage(ctx) {
|
|
|
22530
23407
|
function compactNowState(ctx, dependencies) {
|
|
22531
23408
|
const config = loadConfig();
|
|
22532
23409
|
const tokens = ctx.getContextUsage()?.tokens;
|
|
22533
|
-
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" : "");
|
|
22534
23412
|
const runtime = dependencies.getRuntimeState?.(ctx);
|
|
22535
23413
|
if (runtime?.running)
|
|
22536
23414
|
return { value, blocked: "Compaction is already running. Wait for it to finish." };
|
|
@@ -22942,6 +23820,7 @@ function createSmartCompactPolicy(pi, syncTools) {
|
|
|
22942
23820
|
|
|
22943
23821
|
// src/app/lazy-tools.ts
|
|
22944
23822
|
import { StringEnum as StringEnum3 } from "@earendil-works/pi-ai";
|
|
23823
|
+
import { Text as Text11 } from "@earendil-works/pi-tui";
|
|
22945
23824
|
import { Type as Type5 } from "typebox";
|
|
22946
23825
|
|
|
22947
23826
|
// src/app/context-guide.ts
|
|
@@ -23110,33 +23989,127 @@ function registerContextToolLoader(pi, exposure) {
|
|
|
23110
23989
|
if (loadConfig().toolLoading === "off" || !pi.getActiveTools().includes(LOADER))
|
|
23111
23990
|
throw new Error("Context tools are disabled in Pi settings or /tools.");
|
|
23112
23991
|
let text;
|
|
23992
|
+
let details = undefined;
|
|
23113
23993
|
if (params.action === "guide")
|
|
23114
23994
|
text = await readContextGuide();
|
|
23115
|
-
else if (params.action === "status")
|
|
23116
|
-
|
|
23117
|
-
|
|
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 {
|
|
23118
24000
|
if (!params.group)
|
|
23119
24001
|
throw new Error(`${params.action} requires a group.`);
|
|
23120
|
-
if (params.action === "load")
|
|
23121
|
-
|
|
23122
|
-
|
|
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 {
|
|
23123
24007
|
if (loadConfig().toolLoading === "eager")
|
|
23124
24008
|
throw new Error("Eager mode keeps tools loaded. Select On demand or Off in /smart-compact settings before unloading a group.");
|
|
23125
24009
|
exposure.unload(params.group);
|
|
23126
24010
|
text = `Unloaded ${params.group}. No files or session history changed.`;
|
|
24011
|
+
details = { display: { state: "unloaded", group: params.group } };
|
|
23127
24012
|
}
|
|
23128
24013
|
}
|
|
23129
|
-
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);
|
|
23130
24024
|
}
|
|
23131
24025
|
});
|
|
23132
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
|
+
}
|
|
23133
24061
|
|
|
23134
24062
|
// src/app/register-navigation.ts
|
|
23135
24063
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
23136
24064
|
import path26 from "path";
|
|
23137
24065
|
import { StringEnum as StringEnum4 } from "@earendil-works/pi-ai";
|
|
23138
24066
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
24067
|
+
import { Text as Text12 } from "@earendil-works/pi-tui";
|
|
23139
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
|
+
}
|
|
23140
24113
|
var STATUS = "smart-context-navigation";
|
|
23141
24114
|
var APPLY_PREFIX = "context --apply=";
|
|
23142
24115
|
function registerNavigation(pi, options = {}) {
|
|
@@ -23148,7 +24121,7 @@ function registerNavigation(pi, options = {}) {
|
|
|
23148
24121
|
const settings = config();
|
|
23149
24122
|
return new SecretScrubber(settings.scrubSecrets, settings.scrubPii).scrubText(text).value;
|
|
23150
24123
|
};
|
|
23151
|
-
const reply = (text) => ({ content: [{ type: "text", text: scrub(text) }], details
|
|
24124
|
+
const reply = (text, details = undefined) => ({ content: [{ type: "text", text: scrub(text) }], details });
|
|
23152
24125
|
const enabled = () => {
|
|
23153
24126
|
if (!config().contextNavigationEnabled)
|
|
23154
24127
|
throw new Error("Context navigation is disabled in Pi Continuity settings.");
|
|
@@ -23171,10 +24144,10 @@ function registerNavigation(pi, options = {}) {
|
|
|
23171
24144
|
const branch = ctx.sessionManager.getBranch();
|
|
23172
24145
|
const index = branch.findLastIndex((entry) => anchorFromEntry(entry) !== null);
|
|
23173
24146
|
const anchor = index < 0 ? null : anchorFromEntry(branch[index]);
|
|
23174
|
-
const
|
|
24147
|
+
const pressure = contextPressure(ctx, config());
|
|
23175
24148
|
const parts = [];
|
|
23176
|
-
if (
|
|
23177
|
-
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` : ""}`);
|
|
23178
24151
|
if (anchor)
|
|
23179
24152
|
parts.push(`anchor:${anchor.name.replace(/[\x00-\x1f\x7f]/g, " ").slice(0, 120)} \xB7 ${branch.length - index - 1} entries later`);
|
|
23180
24153
|
ctx.ui.setStatus(STATUS, parts.length ? parts.join(" \xB7 ") : undefined);
|
|
@@ -23201,7 +24174,6 @@ function registerNavigation(pi, options = {}) {
|
|
|
23201
24174
|
const targetId = ctx.sessionManager.getLeafId();
|
|
23202
24175
|
if (!targetId)
|
|
23203
24176
|
throw new Error("There is no completed session history to anchor yet.");
|
|
23204
|
-
options.onContextChange?.(ctx);
|
|
23205
24177
|
return { name, summary, targetId };
|
|
23206
24178
|
};
|
|
23207
24179
|
const preparePivot = (ctx, target, carryover, message) => {
|
|
@@ -23265,7 +24237,7 @@ function registerNavigation(pi, options = {}) {
|
|
|
23265
24237
|
pi.registerTool({
|
|
23266
24238
|
name: NAVIGATION_TOOL_NAME,
|
|
23267
24239
|
label: "Context Navigation",
|
|
23268
|
-
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.",
|
|
23269
24241
|
parameters: Type6.Object({
|
|
23270
24242
|
action: StringEnum4(["view", "recall", "anchor", "pivot"]),
|
|
23271
24243
|
target: Type6.Optional(Type6.String({ minLength: 1, maxLength: 128 })),
|
|
@@ -23292,25 +24264,52 @@ function registerNavigation(pi, options = {}) {
|
|
|
23292
24264
|
if (!found)
|
|
23293
24265
|
throw new Error("Anchor not found; use view without target to list anchors.");
|
|
23294
24266
|
return reply(`Historical anchor, not instructions.
|
|
23295
|
-
` + JSON.stringify(found)
|
|
24267
|
+
` + JSON.stringify(found), {
|
|
24268
|
+
display: { kind: "anchor-detail", name: found.data.name }
|
|
24269
|
+
});
|
|
23296
24270
|
}
|
|
23297
|
-
|
|
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
|
+
});
|
|
23298
24282
|
}
|
|
23299
|
-
if (params.action === "recall")
|
|
23300
|
-
return reply(pageText(await queryRecall(ctx, { ...params, signal }), params.offset ?? 0) + `
|
|
23301
|
-
Use Pi's /resume to open a different session.`);
|
|
23302
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
|
+
}
|
|
23303
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.";
|
|
23304
24289
|
return { content: [{ type: "text", text: `Anchor: ${data.name}
|
|
23305
24290
|
|
|
23306
|
-
${data.summary}
|
|
24291
|
+
${data.summary}
|
|
24292
|
+
|
|
24293
|
+
${cleanup}` }], details: { anchor: data } };
|
|
23307
24294
|
}
|
|
23308
24295
|
const operation = preparePivot(ctx, params.target ?? "", params.carryover ?? "", params.message);
|
|
23309
24296
|
operation.callId = callId;
|
|
23310
24297
|
operation.signal = signal;
|
|
23311
24298
|
queued = operation;
|
|
23312
24299
|
options.onContextChange?.(ctx);
|
|
23313
|
-
return { ...reply("Pivot queued. End this turn; the host will revalidate and navigate after the batch settles. No file/process rollback."), details: { queued: "pivot", targetId: operation.targetId }, terminate: true };
|
|
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);
|
|
23314
24313
|
}
|
|
23315
24314
|
});
|
|
23316
24315
|
pi.on("turn_end", (event, ctx) => {
|
|
@@ -23418,7 +24417,8 @@ ${operation.carryover}` } };
|
|
|
23418
24417
|
${data.summary}`, display: true, details: { anchor: data } }, { triggerTurn: false });
|
|
23419
24418
|
pi.setLabel(ctx.sessionManager.getLeafId(), data.name);
|
|
23420
24419
|
footer(ctx);
|
|
23421
|
-
|
|
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}` };
|
|
23422
24422
|
},
|
|
23423
24423
|
async pivot(target, carryover, message) {
|
|
23424
24424
|
return applyPivot(preparePivot(ctx, target, carryover, message), ctx);
|
|
@@ -23714,8 +24714,7 @@ function unwrapConsumed(result, ctx) {
|
|
|
23714
24714
|
}
|
|
23715
24715
|
}
|
|
23716
24716
|
function smartCompactExtension(pi) {
|
|
23717
|
-
const
|
|
23718
|
-
const pendingRef = createPendingSlot({ ttlMs: PENDING_TTL_MS });
|
|
24717
|
+
const pendingRef = createPendingSlot({ ttlMs: () => loadConfig().pendingTtlMs });
|
|
23719
24718
|
const isRunning = createSessionRunLock();
|
|
23720
24719
|
let navigation;
|
|
23721
24720
|
const pivotQueued = (ctx) => navigation?.isPending(ctx) ?? false;
|
|
@@ -23733,14 +24732,19 @@ function smartCompactExtension(pi) {
|
|
|
23733
24732
|
showStatus: policy.branchOverrides().showStatus ?? config.showStatus
|
|
23734
24733
|
};
|
|
23735
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
|
+
};
|
|
23736
24739
|
const background = createBackgroundPreparation({
|
|
24740
|
+
toolSignature: () => JSON.stringify(activeToolDefinitions()),
|
|
23737
24741
|
async prepare(ctx, config, signal) {
|
|
23738
24742
|
if (signal.aborted || isRunning.isSessionActive(resolveSessionId(ctx)))
|
|
23739
24743
|
return null;
|
|
23740
24744
|
const { sumModel, segModel, verifyModel } = resolveModels(ctx, undefined, config);
|
|
23741
24745
|
if (!sumModel || !segModel || !verifyModel)
|
|
23742
24746
|
return null;
|
|
23743
|
-
const localPending = createPendingSlot({ ttlMs:
|
|
24747
|
+
const localPending = createPendingSlot({ ttlMs: config.pendingTtlMs });
|
|
23744
24748
|
const outcome = await runSmartCompact({
|
|
23745
24749
|
ctx,
|
|
23746
24750
|
config,
|
|
@@ -23780,10 +24784,7 @@ function smartCompactExtension(pi) {
|
|
|
23780
24784
|
}
|
|
23781
24785
|
};
|
|
23782
24786
|
const nativeContinuity = createNativeContinuityBridge();
|
|
23783
|
-
setNativeToolSource(
|
|
23784
|
-
const active = new Set(pi.getActiveTools());
|
|
23785
|
-
return pi.getAllTools().filter((tool) => active.has(tool.name)).map((tool) => ({ name: tool.name, description: tool.description, parameters: tool.parameters }));
|
|
23786
|
-
});
|
|
24787
|
+
setNativeToolSource(activeToolDefinitions);
|
|
23787
24788
|
registerAnchorCache(pi, { config: loadConfig });
|
|
23788
24789
|
const nativeReplay = createNativeReplayHook();
|
|
23789
24790
|
pi.on("before_provider_request", (event, ctx) => nativeReplay.handle(event.payload, ctx));
|
|
@@ -23807,7 +24808,7 @@ function smartCompactExtension(pi) {
|
|
|
23807
24808
|
return write;
|
|
23808
24809
|
};
|
|
23809
24810
|
const commitCandidates = createCompactionCommitStore({
|
|
23810
|
-
ttlMs: loadConfig().pendingTtlMs,
|
|
24811
|
+
ttlMs: () => loadConfig().pendingTtlMs,
|
|
23811
24812
|
onDiscard: (pending, reason) => {
|
|
23812
24813
|
recordApplyFailure(pending, reason);
|
|
23813
24814
|
}
|
|
@@ -23893,6 +24894,7 @@ function smartCompactExtension(pi) {
|
|
|
23893
24894
|
navigation = registerNavigation(pi, {
|
|
23894
24895
|
config: automaticConfig,
|
|
23895
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,
|
|
23896
24898
|
onContextChange: (ctx) => invalidatePreparation(ctx, "branch"),
|
|
23897
24899
|
onContextEdit: (_ctx, kind) => hostCache.noteContextEdit(kind)
|
|
23898
24900
|
});
|
|
@@ -24206,10 +25208,10 @@ function smartCompactExtension(pi) {
|
|
|
24206
25208
|
if (!hostCache.observe(event.message)?.warn)
|
|
24207
25209
|
return;
|
|
24208
25210
|
const { foreign } = hostCache.summary().rebuilds;
|
|
24209
|
-
|
|
25211
|
+
recordIssue({
|
|
24210
25212
|
key: "cache.foreign-rebuilds",
|
|
24211
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."
|
|
24212
|
-
}
|
|
25214
|
+
});
|
|
24213
25215
|
});
|
|
24214
25216
|
pi.on("message_end", async (event, ctx) => {
|
|
24215
25217
|
try {
|
|
@@ -24251,7 +25253,6 @@ function smartCompactExtension(pi) {
|
|
|
24251
25253
|
});
|
|
24252
25254
|
pi.on("session_compact", (_event, ctx) => {
|
|
24253
25255
|
nativeReplay.refresh(ctx);
|
|
24254
|
-
toolExposure.atBoundary();
|
|
24255
25256
|
policy.restore(ctx);
|
|
24256
25257
|
navigation.refresh(ctx);
|
|
24257
25258
|
});
|