pi-smart-compact 10.0.1 → 10.1.1

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