pi-fold 4.2.7 → 4.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -110,7 +110,7 @@ Four commands ship for the human: `/fold-status`, `/fold` (commit every staged m
110
110
 
111
111
  <picture>
112
112
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/shaneconner/fold/main/media/marks-and-commits-dark.svg">
113
- <img alt="A completed unit becomes a pending mark that leaves the window and its cached prefix untouched. Marks repeat for free below maxTarget; at maxTarget one commit applies every standing mark in a single rewrite. From the brief, peek appends the exact bytes and expand restores them in place, both after verifying the SHA-256." src="https://raw.githubusercontent.com/shaneconner/fold/main/media/marks-and-commits-light.svg">
113
+ <img alt="A completed unit becomes a pending mark that leaves the window and its cached prefix untouched. Marks repeat for free below maxTarget; at maxTarget one commit applies every standing mark in a single rewrite. From the brief, peek appends verified history without thinking blocks and expand restores the original messages in place." src="https://raw.githubusercontent.com/shaneconner/fold/main/media/marks-and-commits-light.svg">
114
114
  </picture>
115
115
 
116
116
  A mark moves nothing. The moment a tool batch or chapter closes, the runtime cuts it into a pending mark, up to eight cuts per pass, stalest first. The mark lives in durable state outside the window, so the projection stays byte-identical and the cached prefix survives. Marks accumulate for free until one commit applies all of them in a single rewrite.
@@ -136,10 +136,12 @@ Every commit re-reads roughly `minTarget` of the budget uncached, so a shallower
136
136
 
137
137
  ### Peek and expand
138
138
 
139
- `peek` returns a fold's exact SHA-256-verified source as a tool result at any depth, with the ancestors still collapsed. It appends and never edits the prefix, so checking what a fold holds costs nothing a commit has to pay for. It takes `offset` and `bytes` for a bounded slice and reads any child fold by id, so a large fold has a narrow read.
139
+ `peek` verifies a fold's original source against its SHA-256 references, then returns a history view as a tool result at any depth, with the ancestors still collapsed. Assistant thinking blocks and their opaque signatures are omitted before slicing; visible text, tool calls and tool results stay verbatim. It appends and never edits the prefix, so checking what a fold holds costs nothing a commit has to pay for. It takes `offset` and `bytes` within that view for a bounded slice and reads any child fold by id, so a large fold has a narrow read. The result reports `sourceFormat: "messages-without-thinking"` and `omittedThinkingBlocks`. Pi's stored transcript and exact `expand` restoration remain unchanged.
140
140
 
141
141
  A peek is ephemeral by default: the bytes ride the window until the model's next message, then their place holds a one-line placeholder and the reply that used them is the surviving trace. Peeking again is lossless and costs one append. The default follows what the agent chose: on the run measured for it, six of seven peeks asked for ephemeral at first exposure. `expand` is the commitment, restoring the source in place until the fold is refolded. Looking is not the same as taking.
142
142
 
143
+ Anthropic documents [reasoning-extraction refusals](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#keep-reasoning-in-thinking-blocks). Omitting native thinking from history reads avoids replaying it as ordinary text, but does not guarantee that a provider will accept a request. Existing tool results can still contain previously printed transcript excerpts. A durable peek stays in later requests, and reloading makes an ephemeral peek durable. Fold does not automatically retry content-filter refusals or remove arbitrary transcript text.
144
+
143
145
  ### Clipping, pins and the compaction path
144
146
 
145
147
  Tool results get a second, lighter treatment at the same commit. Results in the oldest `toolFoldThreshold` share of the projected window, half of it by default, are clipped in view: the identifying head stays, the rest becomes a marker naming the entry id that peeks it back, and the stored bytes are untouched, so a later fold of that span still encodes the full result.
@@ -1427,13 +1427,31 @@ export function peekFoldSource(input: {
1427
1427
  }): Record<string, unknown> {
1428
1428
  const fold = input.state.folds.find((item) => item.id === input.foldId);
1429
1429
  if (!fold) throw new Error(`Unknown active-context fold ${input.foldId}`);
1430
- const messages = foldStoredSpan({
1430
+ const storedMessages = foldStoredSpan({
1431
1431
  foldId: input.foldId,
1432
1432
  state: input.state,
1433
1433
  entries: input.entries,
1434
1434
  sessionId: input.sessionId,
1435
1435
  projectEntry: input.projectEntry,
1436
1436
  });
1437
+ // Verify the ORIGINAL messages first, then build the history read. Native
1438
+ // reasoning and opaque signatures belong to the provider's assistant protocol,
1439
+ // not to a JSON transcript replayed as ordinary tool-result text (issue #10).
1440
+ // Only structural assistant blocks are removed: prose and tool output containing
1441
+ // words such as "thinking" stay verbatim. Stored refs, hashes and expand are intact.
1442
+ let omittedThinkingBlocks = 0;
1443
+ const messages = storedMessages.map((message) => {
1444
+ const content = ownValue(message, "content");
1445
+ if (!isPlainRecord(message) || ownValue(message, "role") !== "assistant" ||
1446
+ !Array.isArray(content)) return message;
1447
+ const visible = content.filter((block) => {
1448
+ const type = ownValue(block, "type");
1449
+ if (type !== "thinking" && type !== "redacted_thinking") return true;
1450
+ omittedThinkingBlocks += 1;
1451
+ return false;
1452
+ });
1453
+ return visible.length === content.length ? message : { ...message, content: visible };
1454
+ });
1437
1455
  const source = stableStringify(messages);
1438
1456
  const sourceBytes = bytes(source);
1439
1457
  const offset = Math.min(Math.max(0, input.offset ?? 0), sourceBytes);
@@ -1475,6 +1493,8 @@ export function peekFoldSource(input: {
1475
1493
  brief: foldBrief(fold, input.state),
1476
1494
  sourceCount: messages.length,
1477
1495
  sourceSha256: fold.sourceSha256,
1496
+ sourceFormat: "messages-without-thinking",
1497
+ omittedThinkingBlocks,
1478
1498
  sourceBytes,
1479
1499
  offset,
1480
1500
  returnedBytes,
@@ -1493,16 +1513,20 @@ export function peekFoldSource(input: {
1493
1513
  ? { wider: { action: "peek", id: fold.id, bytes: Math.min(sourceBytes, ACTIVE_CONTEXT_POLICY.maxSourceChars) } }
1494
1514
  : {}),
1495
1515
  lifetime: "these bytes stay in the window exactly as returned, like any other tool result, and " +
1496
- `nothing rewrites this result in place. It is a COPY of fold ${fold.id}'s stored source, so the ` +
1516
+ `nothing rewrites this result in place. It is a COPY of fold ${fold.id}'s source view, so the ` +
1497
1517
  `next commit reclaims it behind a placeholder naming ${fold.id}; peek ${fold.id} again for the ` +
1498
1518
  "same verbatim bytes, or pin this result to keep the copy raw.",
1499
1519
  note: (truncated
1500
- ? `Bounded read: ${returnedBytes} of ${sourceBytes} exact source bytes, ${view.omittedBytes} omitted ` +
1520
+ ? `Bounded read: ${returnedBytes} of ${sourceBytes} ${omittedThinkingBlocks ? "source view" : "exact source"} bytes, ${view.omittedBytes} omitted ` +
1501
1521
  `from the middle. Widen with bytes, page with offset, or expand ${fold.id} to restore it in place.`
1502
1522
  : children.length
1503
- ? `Complete stored span, one level: raw entries exactly, and ${children.length} child fold(s) still ` +
1523
+ ? `Complete stored span, one level: raw entries ${omittedThinkingBlocks ? "with thinking blocks omitted" : "exactly"}, and ${children.length} child fold(s) still ` +
1504
1524
  "placeheld. Peek a child id to read its own span; the fold stayed collapsed and no projection changed."
1505
- : "Complete exact source; the fold stayed collapsed and no projection changed.") +
1525
+ : `Complete ${omittedThinkingBlocks ? "source view" : "exact source"}; the fold stayed collapsed and no projection changed.`) +
1526
+ (omittedThinkingBlocks
1527
+ ? ` ${omittedThinkingBlocks} thinking blocks omitted from this read, including their opaque ` +
1528
+ "signatures. The original messages remain unchanged for exact in-place restoration."
1529
+ : "") +
1506
1530
  (catalog.omitted
1507
1531
  ? ` The nested index seats the shallowest ${index.length} of ${index.length + catalog.omitted} ` +
1508
1532
  "descendant briefs inside the byte budget; the deeper rows are one hop away by peeking a child id, " +
@@ -29,7 +29,7 @@ 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 -- nested folds' briefs seated shallowest first inside the byte budget with the omitted rows counted, 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.`
32
+ ? ` Peek is the point read: it returns one fold's index view -- nested folds' briefs seated shallowest first inside the byte budget with the omitted rows counted, 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 with thinking blocks and their signatures omitted, all other content unchanged, 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
34
  const correctionGuidance = input.allowedActions.includes("reboundary")
35
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."
@@ -80,7 +80,7 @@ export function buildActiveContextTool(input: {
80
80
  bytes: {
81
81
  type: "integer",
82
82
  minimum: input.minPeekSliceBytes,
83
- description: `Peek only: bytes of exact source to return, widening or narrowing the ${input.defaultPeekBytes}-byte default.`,
83
+ description: `Peek only: bytes of the source view to return, widening or narrowing the ${input.defaultPeekBytes}-byte default. Thinking blocks and their signatures are omitted before slicing.`,
84
84
  },
85
85
  limit: { type: "integer", minimum: 1, maximum: 100 },
86
86
  detail: { type: "string", enum: [...input.statusDetails] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fold",
3
- "version": "4.2.7",
3
+ "version": "4.2.8",
4
4
  "description": "Lossless context folding for the Pi coding agent: only the oldest context changes; the fresh window stays untouched.",
5
5
  "keywords": [
6
6
  "pi-package",