pi-tuck 1.0.24 → 1.0.26

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.
@@ -1,13 +1,51 @@
1
1
  import { isContextOverflow, normalizeContext } from '@earendil-works/pi-ai';
2
2
  import { parseRecords, splitSummary } from './tuck/core/transcript.js';
3
+ /** Pi's own request arithmetic: its reply clamp and the estimator behind it, as the running Pi has them.
4
+ * An installed extension cannot import these subpaths by name: Pi's extension loader aliases only pi-ai's root, compat, models, oauth and
5
+ * providers/all, and an installed package has no node_modules of its own. So resolve them first by name (tests, the eval harness, a dev
6
+ * checkout), then from the running Pi's own install, beside its executable. If neither works (an SDK host), there is none, and callers
7
+ * keep their fallbacks. */
8
+ export async function loadPiRequestMath() {
9
+ try {
10
+ const [simple, estimate] = await Promise.all([import('@earendil-works/pi-ai/api/simple-options'), import('@earendil-works/pi-ai/utils/estimate')]);
11
+ return { clampMaxTokensToContext: simple.clampMaxTokensToContext, estimateContextTokens: estimate.estimateContextTokens };
12
+ }
13
+ catch { /* not resolvable by name: an installed extension */ }
14
+ try {
15
+ // pi-ai's exports map these subpaths for `import` only (./api/* -> ./dist/api/*.js), so find the package beside Pi's executable and
16
+ // import the files that map names.
17
+ const [{ existsSync, realpathSync }, { dirname, join }, { pathToFileURL }] = await Promise.all([import('node:fs'), import('node:path'), import('node:url')]);
18
+ const entry = process.argv[1];
19
+ if (!entry)
20
+ return undefined;
21
+ let dir = dirname(realpathSync(entry)), root;
22
+ for (let up = 0; up < 12 && !root; up++, dir = dirname(dir)) {
23
+ const candidate = join(dir, 'node_modules', '@earendil-works', 'pi-ai');
24
+ if (existsSync(join(candidate, 'dist', 'api', 'simple-options.js')))
25
+ root = candidate;
26
+ if (dirname(dir) === dir)
27
+ break;
28
+ }
29
+ if (!root)
30
+ return undefined;
31
+ const [simple, estimate] = await Promise.all([
32
+ import(pathToFileURL(join(root, 'dist', 'api', 'simple-options.js')).href),
33
+ import(pathToFileURL(join(root, 'dist', 'utils', 'estimate.js')).href)
34
+ ]);
35
+ if (typeof simple.clampMaxTokensToContext !== 'function' || typeof estimate.estimateContextTokens !== 'function')
36
+ return undefined;
37
+ return { clampMaxTokensToContext: simple.clampMaxTokensToContext, estimateContextTokens: estimate.estimateContextTokens };
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
3
43
  /** Read Pi's private safety allowance through its public clamp, rather than copying it.
4
- * Older/unavailable subpaths fall back to Pi 1.2–1.5's documented 4,096-token allowance.
44
+ * Without Pi's request arithmetic, fall back to Pi 1.2–1.5's documented 4,096-token allowance.
5
45
  */
6
- export async function piContextSafetyMargin() {
46
+ export async function piContextSafetyMargin(math) {
7
47
  try {
8
- const [{ clampMaxTokensToContext }, { estimateContextTokens }] = await Promise.all([
9
- import('@earendil-works/pi-ai/api/simple-options'), import('@earendil-works/pi-ai/utils/estimate'),
10
- ]);
48
+ const { clampMaxTokensToContext, estimateContextTokens } = math ?? await loadPiRequestMath() ?? (() => { throw new Error('No Pi request math'); })();
11
49
  const window = 1_000_000, context = normalizeContext({ messages: [] });
12
50
  const margin = window - estimateContextTokens(context).tokens - clampMaxTokensToContext({ contextWindow: window, maxTokens: window }, context, window);
13
51
  if (!Number.isSafeInteger(margin) || margin < 0 || margin >= window)
@@ -20,20 +58,29 @@ export async function piContextSafetyMargin() {
20
58
  }
21
59
  /** The smallest reply room a request above the room must leave after Pi's own clamp: above the user's p99 reply (3,270 tokens). */
22
60
  export const REPLY_FLOOR_MIN = 3_584;
61
+ /** A spent thinking budget that produced neither a visible answer nor work. Empty/text/tool replies keep the existing length recovery. */
62
+ export const thinkingOnlyLength = (message) => message.role === 'assistant' && message.stopReason === 'length' &&
63
+ message.content.length > 0 && message.content.every(block => block.type === 'thinking');
64
+ /** Thinking-only exhaustion uses the observed largest reply, not the full reserve. Every other failed length/overflow keeps its override. */
65
+ export const needsFullReplyReserve = (message) => message.role === 'assistant' &&
66
+ (message.stopReason === 'length' && !thinkingOnlyLength(message) || message.stopReason === 'error' && isContextOverflow(message));
23
67
  /** Read the whole current branch, including replies before cuts. No session-local fallback flag. */
24
68
  export function branchReplyFloor(branch, reserve) {
25
- let largest = REPLY_FLOOR_MIN;
69
+ let largest = REPLY_FLOOR_MIN, thinkingStop = false;
26
70
  for (const entry of branch) {
27
71
  if (entry.type !== 'message' || entry.message.role !== 'assistant')
28
72
  continue;
29
73
  const message = entry.message;
30
- if (message.stopReason === 'length' || message.stopReason === 'error' && isContextOverflow(message))
74
+ if (needsFullReplyReserve(message))
31
75
  return reserve;
76
+ thinkingStop ||= thinkingOnlyLength(message);
32
77
  const output = message.usage?.output;
33
78
  if (typeof output === 'number' && Number.isFinite(output) && output > largest)
34
79
  largest = output;
35
80
  }
36
- return Math.min(reserve, Math.ceil(largest));
81
+ // Include the exhausted thinking reply itself, even across later cuts/restarts. Its observed output must fit again;
82
+ // requestReplyFloor applies the model cap. Ordinary branches retain their existing reserve cap.
83
+ return thinkingStop ? Math.ceil(largest) : Math.min(reserve, Math.ceil(largest));
37
84
  }
38
85
  /** Admission never promises more output than a model with a positive reply cap can produce. */
39
86
  export function requestReplyFloor(branch, reserve, maxTokens) {
@@ -9,6 +9,8 @@ export declare function statusText(input: {
9
9
  type: string;
10
10
  tokensBefore?: unknown;
11
11
  details?: unknown;
12
+ customType?: unknown;
13
+ data?: unknown;
12
14
  }[];
13
15
  cutAt: number | undefined;
14
16
  search: boolean;
@@ -21,6 +21,8 @@ export function statusText(input) {
21
21
  const cutLine = ours.length === 0 ? "Cuts: none yet in this session. The recall tools appear after the first cut."
22
22
  : `Cuts: ${ours.length} by pi-tuck in this session` + (typeof before === "number" && typeof after === "number" ? `, the last from ${count(before)} to ${count(after)} tokens.` : ".");
23
23
  const native = cuts.length - ours.length;
24
+ const retries = input.branch.filter(entry => entry.type === "custom" && entry.customType === "pi-tuck-capacity" &&
25
+ entry.data?.kind === "retry").length;
24
26
  const searchLine = !input.search ? "Search: by keyword (search by meaning needs macOS on Apple silicon)."
25
27
  : input.semantic.state === "ready" || input.semantic.state === "waiting" ? "Search: by meaning and keyword."
26
28
  : input.semantic.state === "preparing" ? "Search: by meaning and keyword; indexing history in the background."
@@ -28,6 +30,7 @@ export function statusText(input) {
28
30
  return [
29
31
  `pi-tuck ${input.version} (the version this session loaded; restart Pi after an update).`,
30
32
  cutLine,
33
+ ...(retries ? [`Retries: ${retries} after thinking-only length stops in this session.`] : []),
31
34
  ...(native > 0 ? [`Pi's own compaction cut ${native} time${native === 1 ? "" : "s"} in this session.`] : []),
32
35
  input.cutAt === undefined ? "Cut point: adapts by itself." : `Cut point: fixed at cutAt ${input.cutAt} of the input room. Remove cutAt from pi-tuck.json to let it adapt.`,
33
36
  searchLine,
@@ -87,6 +87,7 @@ export declare function tuckObservedDraft(tuck: Tuck, input: {
87
87
  newPrompt?: boolean;
88
88
  reRenderSummary?: boolean;
89
89
  protectedPromptEntryId?: string;
90
+ deepest?: boolean;
90
91
  }, ctx: ExtensionContext, reserveTokens: number, measure: Measure, policy?: RecallPolicy, retainCurrentInput?: boolean): Draft;
91
92
  /** Numeric references use the original session index, then intersect with readable current-branch entries. */
92
93
  export declare function tuckRecall(tuck: Tuck, entries: readonly SessionEntry[], branch: readonly SessionEntry[], input: RecallOptions & {
package/dist/src/tuck.js CHANGED
@@ -311,6 +311,14 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
311
311
  };
312
312
  const calibrationHigh = highAt(floorBoundary, minimumOpeningLength);
313
313
  const floorSummary = render(calibrationHigh.rows), floorProjection = preview(floorSummary, floorBoundary);
314
+ // The last admission attempt and thinking-only retry share this exact floor: whole protected prompts, the smallest
315
+ // existing summary representation, and the required fresh/native suffix. No historical fill or room target spends reply space.
316
+ if (input.deepest) {
317
+ assertNativeToolRounds(floorProjection);
318
+ const after = measure(floorProjection);
319
+ return { compaction: { summary: floorSummary, firstKeptEntryId: entries[floorBoundary]?.id ?? "", tokensBefore: input.tokensBefore,
320
+ details: { ...detailsAt(floorBoundary), tokensAfter: after, carriedTarget: after, view: "shrunk-dialogue", mode: "shrunk", deepest: true } }, after, mode: "shrunk" };
321
+ }
314
322
  const offset = measure(floorProjection) - nativeAt(floorBoundary) - summaryTokens(floorSummary.length);
315
323
  const price = (characters, boundary) => offset + nativeAt(boundary) + summaryTokens(characters);
316
324
  const listCache = new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-tuck",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "Deterministic compaction for Pi with no model call, plus exact recall of earlier history. Search by meaning on Apple silicon.",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/build-id.ts CHANGED
@@ -1 +1 @@
1
- export const BUILD_ID = "c3eefa3bb76e52635b1e8394a025d3ffd6a056fc5636ed9b35f89dfddc1a322a";
1
+ export const BUILD_ID = "e95edde95c614d01e1cbf6489134676fac0c1bbf6aa57ef639ffac1e7e105041";