pi-fold 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,6 @@ import {
16
16
  orderedRoots,
17
17
  refsInOrder,
18
18
  refsProtected,
19
- toolRefsProtected,
20
19
  visibleCollapsedRoots,
21
20
  } from "./measurement.ts";
22
21
  import {
@@ -28,7 +27,6 @@ import {
28
27
  import {
29
28
  ACTIVE_CONTEXT_POLICY,
30
29
  DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
31
- MAX_FOLD_SPAN_CHARS,
32
30
  } from "./policy.ts";
33
31
  import type {
34
32
  ActiveContextSnapshot,
@@ -42,7 +40,6 @@ import type {
42
40
  } from "./policy.ts";
43
41
  import {
44
42
  chapterSegments,
45
- currentTurnBoundary,
46
43
  scanTurnToolBatches,
47
44
  structurallyClosedChapterUnits,
48
45
  terminalAssistant,
@@ -503,9 +500,7 @@ export function pinnedChildFold(
503
500
  const child = byId.get(part.foldId);
504
501
  if (!child) continue;
505
502
  const refs = flattenFoldRefs(child, state);
506
- const pinned = child.kind === "tool-result"
507
- ? toolRefsProtected(refs, state, snapshot)
508
- : refsProtected(refs, state, snapshot);
503
+ const pinned = refsProtected(refs, state, snapshot);
509
504
  if (pinned) return child;
510
505
  }
511
506
  return null;
@@ -513,7 +508,7 @@ export function pinnedChildFold(
513
508
 
514
509
  export function pinnedNestingRefusal(pinned: ActiveFold, toolName: string): string {
515
510
  return `fold refused: ${pinned.id} is pinned, and nesting it would hide context you asked to keep. ` +
516
- `Release it with ${toolName} {"action":"unprotect","ids":["${pinned.id}"]} first, ` +
511
+ `Release it with ${toolName} {"action":"unpin","ids":["${pinned.id}"]} first, ` +
517
512
  "or name a span that stops at its boundary.";
518
513
  }
519
514
 
@@ -676,7 +671,6 @@ function consolidationCandidate(
676
671
  if (refsProtected(raw, state, snapshot)) return null;
677
672
  if (raw.some((ref) => claims.refKeys.has(objectRefKey(ref)))) return null;
678
673
  const sourceRefs = candidateSourceRefs(parts, state);
679
- if (sourceRefs.length > snapshot.policy.maxFoldSourceRefs) return null;
680
674
  return { kind: "consolidation", parts, sourceRefs };
681
675
  }
682
676
 
@@ -735,9 +729,8 @@ export function automaticToolBatches(
735
729
  refs.some((ref) => !ref || ref.role !== "toolResult")) continue;
736
730
  const exactRefs = refs as EvidenceRef[];
737
731
  const size = group.reduce((total, { item }) => total + bytes(item.message), 0);
738
- if (exactRefs.length > snapshot.policy.maxFoldSourceRefs ||
739
- toolRefsProtected(exactRefs, state, snapshot) || refsInOrder(snapshot, exactRefs) === null ||
740
- size < snapshot.policy.minToolChars) continue;
732
+ if (refsProtected(exactRefs, state, snapshot) || refsInOrder(snapshot, exactRefs) === null ||
733
+ size < snapshot.thresholds.minFoldChars) continue;
741
734
  batches.push({ refs: exactRefs, indices: group.map(({ item }) => item.index), bytes: size });
742
735
  }
743
736
  return batches;
@@ -770,9 +763,7 @@ export function selectAutomaticRefold(
770
763
  if (claimedFoldIds.has(id)) return [];
771
764
  const fold = state.folds.find((item) => item.id === id);
772
765
  const interval = fold ? foldInterval(fold, state, snapshot) : null;
773
- const protectedSource = fold && (fold.kind === "tool-result"
774
- ? toolRefsProtected(flattenFoldRefs(fold, state), state, snapshot)
775
- : refsProtected(flattenFoldRefs(fold, state), state, snapshot));
766
+ const protectedSource = fold && (refsProtected(flattenFoldRefs(fold, state), state, snapshot));
776
767
  return fold && interval && !protectedSource && !state.leases[id]
777
768
  ? [{ id, ...interval }]
778
769
  : [];
@@ -805,47 +796,9 @@ export function resolveFoldInputIds(
805
796
  export function chapterRangeIsUnitAligned(snapshot: ActiveContextSnapshot, start: number, end: number): boolean {
806
797
  const units = chapterUnits(snapshot).filter((unit) => unit.end > start && unit.start <= end);
807
798
  if (!units.length || units[0].start !== start || units.at(-1)!.end !== end + 1) return false;
808
- if (new Set(units.map((unit) => unit.turnStart)).size > snapshot.policy.maxChapterTurns) return false;
809
799
  return units.every((unit, index) => index === 0 || unit.start === units[index - 1].end);
810
800
  }
811
801
 
812
- export function splitCandidateBySize(
813
- snapshot: ActiveContextSnapshot,
814
- state: ActiveContextState,
815
- candidate: FoldCandidate,
816
- maxChars: number = MAX_FOLD_SPAN_CHARS,
817
- ): FoldCandidate[] {
818
- if (candidate.kind !== "chapter") return [candidate];
819
- const refs = candidate.sourceRefs.length ? candidate.sourceRefs : candidateSourceRefs(candidate.parts, state);
820
- const indices = refs.map((ref) => exactMapped(snapshot, ref)?.index ?? -1);
821
- if (!indices.length || indices.some((index) => index < 0)) return [candidate];
822
- const start = Math.min(...indices);
823
- const end = Math.max(...indices);
824
- if (spanBytes(snapshot, start, end + 1) <= maxChars) return [candidate];
825
- const units = chapterUnits(snapshot).filter((unit) => unit.start >= start && unit.end <= end + 1);
826
- if (units.length < 2) return [candidate];
827
- const split: FoldCandidate[] = [];
828
- const emit = (from: number, to: number): boolean => {
829
- const parts = partsForRange(snapshot, state, from, to, new Set<FoldKind>(["tool-result"]));
830
- if (!parts) return false;
831
- split.push({ kind: "chapter", parts, sourceRefs: candidateSourceRefs(parts, state) });
832
- return true;
833
- };
834
- let runStart = units[0].start;
835
- let runBytes = 0;
836
- for (const unit of units) {
837
- const size = spanBytes(snapshot, unit.start, unit.end);
838
- if (runBytes > 0 && runBytes + size > maxChars) {
839
- if (!emit(runStart, unit.start - 1)) return [candidate];
840
- runStart = unit.start;
841
- runBytes = 0;
842
- }
843
- runBytes += size;
844
- }
845
- if (runBytes > 0 && !emit(runStart, units.at(-1)!.end - 1)) return [candidate];
846
- return split.length ? split : [candidate];
847
- }
848
-
849
802
  export function candidateSpanChars(
850
803
  snapshot: ActiveContextSnapshot,
851
804
  state: ActiveContextState,
@@ -986,11 +939,6 @@ export function snapSpanIds(
986
939
  `${absorb ? "end" : "near"} boundary`);
987
940
  }
988
941
  }
989
- const boundary = currentTurnBoundary(snapshot);
990
- if (boundary >= 0 && end > boundary) {
991
- end = boundary;
992
- note("span reached into the turn still in flight; corrected back to the last closed turn");
993
- }
994
942
  const startUnit = units.find((unit) => start >= unit.start && start < unit.end);
995
943
  if (startUnit && startUnit.start !== start) {
996
944
  start = startUnit.start;
@@ -1001,23 +949,6 @@ export function snapSpanIds(
1001
949
  end = endUnit.end - 1;
1002
950
  note("span ended mid-unit; corrected to the end of its closed user/assistant/tool unit");
1003
951
  }
1004
- const covered = units.filter((unit) => unit.start >= start && unit.end <= end + 1);
1005
- if (covered.length) {
1006
- const turns: number[] = [];
1007
- let clamped = end;
1008
- for (const unit of covered) {
1009
- if (!turns.includes(unit.turnStart)) {
1010
- if (turns.length >= snapshot.policy.maxChapterTurns) break;
1011
- turns.push(unit.turnStart);
1012
- }
1013
- clamped = unit.end - 1;
1014
- }
1015
- if (clamped < end) {
1016
- end = clamped;
1017
- note(`span covered more than the ${snapshot.policy.maxChapterTurns}-turn chapter limit; ` +
1018
- "corrected to the last turn that fits");
1019
- }
1020
- }
1021
952
  if (start === passStart && end === passEnd) break;
1022
953
  }
1023
954
  if (!corrections.length || end < start ||
@@ -1067,14 +998,11 @@ export function manualFoldCandidate(
1067
998
  options: { allowProtected?: boolean } = {},
1068
999
  ): FoldCandidate {
1069
1000
  const blockedTool = (refs: EvidenceRef[]): boolean =>
1070
- !options.allowProtected && toolRefsProtected(refs, state, snapshot);
1001
+ !options.allowProtected && refsProtected(refs, state, snapshot);
1071
1002
  const blocked = (refs: EvidenceRef[]): boolean =>
1072
1003
  !options.allowProtected && refsProtected(refs, state, snapshot);
1073
1004
  const owners = directFoldOwners(state.folds);
1074
1005
  const bounded = (candidate: FoldCandidate): FoldCandidate => {
1075
- if (candidate.sourceRefs.length > snapshot.policy.maxFoldSourceRefs) {
1076
- throw new Error(`Folds may include at most ${snapshot.policy.maxFoldSourceRefs} exact source references`);
1077
- }
1078
1006
  for (const part of candidate.parts) {
1079
1007
  if (part.kind !== "raw") continue;
1080
1008
  const ownerId = owners.get(objectRefKey(part.ref));
@@ -29,16 +29,16 @@ export function buildActiveContextTool(input: {
29
29
  handler: ToolHandler;
30
30
  }) {
31
31
  const peekGuidance = input.allowedActions.includes("peek")
32
- ? ` Peek is the point read: it returns one fold's index view -- every nested fold's brief in full, plus the head and tail of that fold's own span with the omitted middle stated -- for any fold id, ancestors still collapsed, changing nothing. It serves ONE level: raw entries come back exactly, folds nested inside stay placeheld, and peeking a child id you see there returns that child's own bytes, so anything folded is one hop away at any depth. It returns at most ${input.defaultPeekBytes} bytes unless you widen it with bytes, and it stays in the window like any other tool result. Expand is the durable in-place restoration.`
32
+ ? ` Peek is the point read: it returns one fold's index view -- every nested fold's brief in full, plus the head and tail of that fold's own span with the omitted middle stated -- for any fold id, ancestors still collapsed, changing nothing. It serves ONE level: raw entries come back exactly, folds nested inside stay placeheld, and peeking a child id you see there returns that child's own bytes, so anything folded is one hop away at any depth. It returns at most ${input.defaultPeekBytes} bytes unless you widen it with bytes, and it is EPHEMERAL by default: the bytes ride only until your next message, your reply then the surviving trace, so extract what your current task needs from them in that reply. Peeking again is lossless and costs one append, so a read you turn out to need twice is cheap; pass ephemeral false when you need the bytes standing across several turns. Expand is the durable in-place restoration.`
33
33
  : "";
34
- const correctionGuidance = input.allowedActions.includes("rebrief")
35
- ? " Curation is correctable: rebrief replaces a fold's brief, and reboundary with ids re-cuts a span into exactly one fold, which merges adjacent folds when the span covers several and splits one when the span sits inside it. Reboundary with a single id returns that fold's span to raw."
34
+ const correctionGuidance = input.allowedActions.includes("reboundary")
35
+ ? " Curation is correctable: reboundary with ids re-cuts a span into exactly one fold, which merges adjacent folds when the span covers several and splits one when the span sits inside it. Reboundary with a single id returns that fold's span to raw. A brief is written when you mark the span; a standing fold's brief is not rewritten, because rewriting it rewrites the projection at that fold's placeholder and costs the whole prefix cache from there on."
36
36
  : "";
37
- const pinGuidance = input.allowedActions.includes("protect")
38
- ? " Protect is the pin: protect with ids holds folds or entries against automatic folding, consolidation and refolding until unprotect releases them, so an expanded span you still need stays expanded. Protect reveals nothing on its own: a folded span stays folded, just held. Pinning moves no context bytes, and fold receipts report how much mass you have pinned."
37
+ const pinGuidance = input.allowedActions.includes("pin")
38
+ ? " Pin with ids holds folds or entries against automatic folding, consolidation and refolding until unpin releases them, so an expanded span you still need stays expanded. Pin reveals nothing on its own: a folded span stays folded, just held. Pinning moves no context bytes, and fold receipts report how much mass you have pinned."
39
39
  : "";
40
- const epochGuidance = input.allowedActions.includes("unmark")
41
- ? " Fold scheduling is epoch mode: fold records pending marks and moves no context bytes, and the runtime applies every pending mark in one rewrite when the fold event fires. Mark SEVERAL spans in one call: marks carries several {ids, brief} pairs, and one call answers with your whole picture, every span now held plus the unmarked remainder and what share of the non-fresh window it is. Between fold events nothing else in your context changes, so that result is where the picture lives. Mark freely as you work; when to fold is the runtime's to decide, and unmark withdraws a mark you no longer want."
40
+ const epochGuidance = input.allowedActions.includes("brief")
41
+ ? " THE RUNTIME CUTS THE FOLDS AND YOU ANNOTATE THEM. As raw material piles up it is cut into pending folds behind you, stalest first. A pending fold moves NO context bytes: you keep seeing the raw material exactly as it is, and the fold reaches your window only when a commit applies it. You are told when folds are cut and which ones carry no brief yet. Answer with brief, one id and one sentence: you know why the span mattered and what you will want back from it, and the deterministic brief that fills the gap reads the span alone and does not. Writing a brief on a pending fold is FREE, because the fold is not in your window yet; briefing a standing fold is refused, because its placeholder is in your window and rewriting it rewrites your context from that point on. reboundary re-cuts a span the runtime got wrong, unmark withdraws a cut you do not want taken, and when to commit stays the runtime's to decide."
42
42
  : "";
43
43
  const recoveryGuidance = input.allowedActions.includes("expand")
44
44
  ? " A fold brief is an index entry, not the source: when a question or task depends on material that is folded, peek or expand that fold and answer from the exact bytes, never from memory of them. Recalling folded detail without reopening it is how specifics get misremembered."
@@ -48,8 +48,8 @@ export function buildActiveContextTool(input: {
48
48
  name: input.name,
49
49
  label: input.label,
50
50
  description: input.fullSurface
51
- ? `Page, peek, fold, expand, refold, protect, rebrief, or re-cut exact Pi active-context evidence.${peekGuidance}${correctionGuidance}${pinGuidance}${epochGuidance}${recoveryGuidance}${standingGuidance} Mutations persist immediately and affect the next model call inside the same continuing turn; no turn boundary is required. Supplied fold briefs have a hard ${input.maxBriefChars}-character maximum.`
52
- : `Use only the configured active-context actions: ${input.allowedActions.join(", ")}.${peekGuidance}${correctionGuidance}${pinGuidance}${epochGuidance}${recoveryGuidance}${standingGuidance} Call fold only by copying the exact eligibleChapter.action returned by status; if status has no eligibleChapter, continue the task without folding. Supplied fold briefs have a hard ${input.maxBriefChars}-character maximum.`,
51
+ ? `Page, peek, brief, expand, refold, pin, or re-cut exact Pi active-context evidence.${peekGuidance}${correctionGuidance}${pinGuidance}${epochGuidance}${recoveryGuidance}${standingGuidance} Mutations persist immediately and affect the next model call inside the same continuing turn; no turn boundary is required. Supplied fold briefs have a hard ${input.maxBriefChars}-character maximum.`
52
+ : `Use only the configured active-context actions: ${input.allowedActions.join(", ")}.${peekGuidance}${correctionGuidance}${pinGuidance}${epochGuidance}${recoveryGuidance}${standingGuidance} Supplied fold briefs have a hard ${input.maxBriefChars}-character maximum.`,
53
53
  parameters: {
54
54
  type: "object",
55
55
  additionalProperties: false,
@@ -58,35 +58,24 @@ export function buildActiveContextTool(input: {
58
58
  action: { type: "string", enum: input.allowedActions },
59
59
  ids: { type: "array", minItems: 1, maxItems: 64, items: { type: "string", minLength: 1 } },
60
60
  id: { type: "string", minLength: 1 },
61
- marks: {
62
- type: "array",
63
- minItems: 1,
64
- maxItems: 64,
65
- description: "Fold only: several spans with their own briefs in one call, which is the shape to prefer. An invalid or overlapping span is corrected to the nearest valid edge and the correction is reported back.",
66
- items: {
67
- type: "object",
68
- additionalProperties: false,
69
- required: ["ids"],
70
- properties: {
71
- ids: { type: "array", minItems: 1, maxItems: 64, items: { type: "string", minLength: 1 } },
72
- brief: {
73
- type: "string",
74
- minLength: 1,
75
- maxLength: input.maxBriefChars,
76
- description: "Write one for every span you mark. You know why the span mattered and " +
77
- "what you will want back from it; the deterministic brief that fills the gap " +
78
- "reads the span alone and does not.",
79
- },
80
- },
81
- },
82
- },
83
61
  brief: {
84
62
  type: "string",
85
63
  minLength: 1,
86
64
  maxLength: input.maxBriefChars,
87
- description: `Factual fold brief, at most ${input.maxBriefChars} characters.`,
65
+ description: "Brief only: what this pending fold covers and what you will want " +
66
+ `back from it, at most ${input.maxBriefChars} characters. Free to write, ` +
67
+ "because the fold it names is not in your window yet.",
88
68
  },
89
69
  offset: { type: "integer", minimum: 0 },
70
+ ephemeral: {
71
+ type: "boolean",
72
+ description: "Peek only. Default TRUE: the returned bytes ride your context " +
73
+ "exactly until your next message, then their place holds a one-line " +
74
+ "placeholder and your reply is the surviving trace, so extract what you need " +
75
+ "into that reply. Peeking again is lossless. Pass false to keep the result " +
76
+ "standing like any other tool result, which is what you want when the bytes " +
77
+ "have to survive several turns of work.",
78
+ },
90
79
  bytes: {
91
80
  type: "integer",
92
81
  minimum: input.minPeekSliceBytes,
@@ -103,8 +92,10 @@ export function buildActiveContextTool(input: {
103
92
  export function buildActiveContextCommands(input: {
104
93
  statusName: string;
105
94
  foldName: string;
95
+ editorName: string;
106
96
  statusHandler: CommandHandler;
107
97
  foldHandler: CommandHandler;
98
+ editorHandler: CommandHandler;
108
99
  }) {
109
100
  return [
110
101
  {
@@ -114,9 +105,14 @@ export function buildActiveContextCommands(input: {
114
105
  },
115
106
  {
116
107
  name: input.foldName,
117
- description: "Losslessly fold a stale context span, or \"commit\" to apply every eligible mark now; " +
108
+ description: "Commit every staged fold in one epoch; with explicit ids, folds exactly that span now; " +
118
109
  "works without a main-model request",
119
110
  handler: input.foldHandler,
120
111
  },
112
+ {
113
+ name: input.editorName,
114
+ description: "Open the context window editor: occupancy, fold roots and briefs, staged marks, pins",
115
+ handler: input.editorHandler,
116
+ },
121
117
  ];
122
118
  }
@@ -3,12 +3,10 @@ import {
3
3
  evidenceRef,
4
4
  evidenceValue,
5
5
  isPlainRecord,
6
- objectRefKey,
7
6
  sha256Text,
8
7
  stableStringify,
9
8
  } from "../json.ts";
10
9
  import {
11
- bytes,
12
10
  clone,
13
11
  messageRole,
14
12
  ownValue,
@@ -24,7 +22,6 @@ import {
24
22
  DEFAULT_THRESHOLDS,
25
23
  PEEK_READ_ONLY_CONTEXT_ACTIONS,
26
24
  servingBudgetTokens,
27
- zoneBytes,
28
25
  } from "./policy.ts";
29
26
  import type {
30
27
  ActiveContextSnapshot,
@@ -70,7 +67,13 @@ export function leadingCompactionContinuation(messages: unknown[]): CompleteTurn
70
67
 
71
68
  export const READ_ONLY_CONTEXT_ACTION_ARGUMENTS: Readonly<Record<string, readonly string[]>> = Object.freeze({
72
69
  status: Object.freeze(["action", "detail", "offset", "limit"]),
73
- peek: Object.freeze(["action", "id"]),
70
+ // offset and bytes are the tool surface's own narrowing parameters: a paged or
71
+ // narrowed peek is as byte-inert as a bare one, and excluding them made the
72
+ // read built to SAVE window mass linger as raw mass past the reclaimer.
73
+ // ephemeral joins them for the same reason: it changes when the RESULT leaves
74
+ // the projection, never what the call moves, and a call the reclaimer cannot
75
+ // claim would linger as exactly the mass the parameter exists to shed.
76
+ peek: Object.freeze(["action", "id", "offset", "bytes", "ephemeral"]),
74
77
  });
75
78
 
76
79
  export function isAutoFoldableToolCall(
@@ -317,7 +320,14 @@ export function unsafeChapterIndices(messages: unknown[]): Set<number> {
317
320
  return unsafe;
318
321
  }
319
322
 
320
- export function freshBoundary(messages: unknown[], turns: CompleteTurn[], freshBytes: number): number {
323
+ /**
324
+ * The first index of an unfinished user turn, or the end of the transcript when the last
325
+ * turn completed. Folding into an unfinished turn leaves a user message whose tool calls are
326
+ * unanswered, which is malformed for every provider, so this is a validity boundary rather
327
+ * than a curation preference. The fresh-tail byte walk that used to sit here is deleted:
328
+ * nothing decided on it, and stale-first ordering already leaves recent material last.
329
+ */
330
+ export function freshBoundary(messages: unknown[], turns: CompleteTurn[]): number {
321
331
  if (!messages.length) return 0;
322
332
  let unfinishedUser = messages.length;
323
333
  for (let index = messages.length - 1; index >= 0; index -= 1) {
@@ -329,36 +339,7 @@ export function freshBoundary(messages: unknown[], turns: CompleteTurn[], freshB
329
339
  unfinishedUser = complete ? messages.length : index;
330
340
  break;
331
341
  }
332
- const leading = leadingCompactionContinuation(messages);
333
- let boundary = Math.min(messages.length, unfinishedUser);
334
- while (boundary > 0) {
335
- const suffix: unknown[] = [];
336
- for (let index = boundary; index < messages.length; index += 1) suffix.push(messages[index]);
337
- if (bytes(suffix) >= freshBytes) break;
338
- let previous: CompleteTurn | null = null;
339
- for (let turnIndex = 0; turnIndex < turns.length; turnIndex += 1) {
340
- if (turns[turnIndex].start < boundary) previous = turns[turnIndex];
341
- }
342
- if (!previous) { boundary = leading?.end ?? 0; break; }
343
- boundary = previous.start;
344
- }
345
- return boundary;
346
- }
347
-
348
- export function toolFreshIndices(
349
- messages: unknown[],
350
- _turns: CompleteTurn[],
351
- freshBytes: number,
352
- ): Set<number> {
353
- const protectedIndices = new Set<number>();
354
- let tailBytes = 0;
355
- let boundary = messages.length;
356
- while (boundary > 0 && tailBytes < freshBytes) {
357
- boundary -= 1;
358
- tailBytes += bytes(messages[boundary]);
359
- }
360
- for (let index = boundary; index < messages.length; index += 1) protectedIndices.add(index);
361
- return protectedIndices;
342
+ return Math.min(messages.length, unfinishedUser);
362
343
  }
363
344
 
364
345
  type EntryEvidence = { message: unknown; ref: BranchObject["ref"] };
@@ -486,17 +467,13 @@ export function mapActiveContext(input: {
486
467
  const budgetTokens = input.netBudget === true
487
468
  ? (reportedContextWindow ?? DEFAULT_CONTEXT_WINDOW)
488
469
  : servingBudgetTokens(reportedContextWindow ?? DEFAULT_CONTEXT_WINDOW);
489
- const freshBytes = zoneBytes(thresholds.freshTail, budgetTokens);
490
470
  const turns = completeTurns(input.eventMessages);
491
- const boundary = freshBoundary(input.eventMessages, turns, freshBytes);
471
+ const boundary = freshBoundary(input.eventMessages, turns);
492
472
  const protectedIndices = unsafeChapterIndices(input.eventMessages);
493
- const toolProtectedIndices = toolFreshIndices(input.eventMessages, turns, freshBytes);
494
- for (const index of toolProtectedIndices) protectedIndices.add(index);
495
473
  for (let mappedIndex = 0; mappedIndex < mapped.length; mappedIndex += 1) {
496
474
  const item = mapped[mappedIndex];
497
475
  if (item.ref) continue;
498
476
  protectedIndices.add(item.index);
499
- toolProtectedIndices.add(item.index);
500
477
  }
501
478
  const snapshot: ActiveContextSnapshot = {
502
479
  sessionId: input.sessionId,
@@ -508,7 +485,6 @@ export function mapActiveContext(input: {
508
485
  thresholds,
509
486
  budgetTokens,
510
487
  protectedIndices,
511
- toolProtectedIndices,
512
488
  policy,
513
489
  toolName: input.toolName ?? DEFAULT_ACTIVE_CONTEXT_TOOL_NAME,
514
490
  brandNoun: input.brandNoun ?? DEFAULT_ACTIVE_CONTEXT_BRAND_NOUN,
@@ -520,22 +496,3 @@ export function mapActiveContext(input: {
520
496
  };
521
497
  return snapshot;
522
498
  }
523
-
524
- export function currentTurnBoundary(snapshot: Pick<ActiveContextSnapshot, "messages">): number {
525
- let boundary = -1;
526
- for (let index = 0; index < snapshot.messages.length; index += 1) {
527
- if (terminalAssistant(snapshot.messages[index])) boundary = index;
528
- }
529
- return boundary;
530
- }
531
-
532
- export function currentTurnRefKeys(snapshot: ActiveContextSnapshot): Set<string> {
533
- const boundary = currentTurnBoundary(snapshot);
534
- const keys = new Set<string>();
535
- for (const item of snapshot.mapped) {
536
- if (item.index <= boundary || !item.ref) continue;
537
- if (messageRole(item.message) !== "toolResult") continue;
538
- keys.add(objectRefKey(item.ref));
539
- }
540
- return keys;
541
- }