pi-tuck 1.0.21 → 1.0.23
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/CHANGELOG.md +83 -0
- package/README.md +9 -10
- package/dist/src/build-id.d.ts +1 -1
- package/dist/src/build-id.js +1 -1
- package/dist/src/compaction.d.ts +14 -3
- package/dist/src/compaction.js +127 -30
- package/dist/src/diagnostics.d.ts +1 -1
- package/dist/src/eligibility-cache.d.ts +1 -1
- package/dist/src/eligibility-cache.js +3 -3
- package/dist/src/request-capacity.d.ts +14 -0
- package/dist/src/request-capacity.js +57 -0
- package/dist/src/semantic-recall.d.ts +6 -1
- package/dist/src/semantic-recall.js +51 -24
- package/dist/src/tuck/core/search-entries.js +2 -2
- package/dist/src/tuck.d.ts +2 -1
- package/dist/src/tuck.js +8 -3
- package/package.json +1 -1
- package/src/build-id.ts +1 -1
- package/src/compaction.ts +114 -34
- package/src/eligibility-cache.ts +3 -3
- package/src/request-capacity.ts +56 -0
- package/src/semantic-recall.ts +19 -8
- package/src/tuck/core/search-entries.ts +2 -2
- package/src/tuck.ts +8 -4
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SessionEntry } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import type { Messages } from './budget.js';
|
|
3
|
+
/** 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.
|
|
5
|
+
*/
|
|
6
|
+
export declare function piContextSafetyMargin(): Promise<number>;
|
|
7
|
+
/** 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). */
|
|
8
|
+
export declare const REPLY_FLOOR_MIN = 3584;
|
|
9
|
+
/** Read the whole current branch, including replies before cuts. No session-local fallback flag. */
|
|
10
|
+
export declare function branchReplyFloor(branch: readonly SessionEntry[], reserve: number): number;
|
|
11
|
+
/** Admission never promises more output than a model with a positive reply cap can produce. */
|
|
12
|
+
export declare function requestReplyFloor(branch: readonly SessionEntry[], reserve: number, maxTokens?: number): number;
|
|
13
|
+
/** Check decoded text, including a whole verbatim user quote in our summary; never JSON-escaped substring matching. */
|
|
14
|
+
export declare function carriesPrompt(messages: Messages, prompt: string): boolean;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isContextOverflow, normalizeContext } from '@earendil-works/pi-ai';
|
|
2
|
+
import { parseRecords, splitSummary } from './tuck/core/transcript.js';
|
|
3
|
+
/** 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.
|
|
5
|
+
*/
|
|
6
|
+
export async function piContextSafetyMargin() {
|
|
7
|
+
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
|
+
]);
|
|
11
|
+
const window = 1_000_000, context = normalizeContext({ messages: [] });
|
|
12
|
+
const margin = window - estimateContextTokens(context).tokens - clampMaxTokensToContext({ contextWindow: window, maxTokens: window }, context, window);
|
|
13
|
+
if (!Number.isSafeInteger(margin) || margin < 0 || margin >= window)
|
|
14
|
+
throw new Error('Invalid Pi context margin');
|
|
15
|
+
return margin;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return 4_096;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** 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
|
+
export const REPLY_FLOOR_MIN = 3_584;
|
|
23
|
+
/** Read the whole current branch, including replies before cuts. No session-local fallback flag. */
|
|
24
|
+
export function branchReplyFloor(branch, reserve) {
|
|
25
|
+
let largest = REPLY_FLOOR_MIN;
|
|
26
|
+
for (const entry of branch) {
|
|
27
|
+
if (entry.type !== 'message' || entry.message.role !== 'assistant')
|
|
28
|
+
continue;
|
|
29
|
+
const message = entry.message;
|
|
30
|
+
if (message.stopReason === 'length' || message.stopReason === 'error' && isContextOverflow(message))
|
|
31
|
+
return reserve;
|
|
32
|
+
const output = message.usage?.output;
|
|
33
|
+
if (typeof output === 'number' && Number.isFinite(output) && output > largest)
|
|
34
|
+
largest = output;
|
|
35
|
+
}
|
|
36
|
+
return Math.min(reserve, Math.ceil(largest));
|
|
37
|
+
}
|
|
38
|
+
/** Admission never promises more output than a model with a positive reply cap can produce. */
|
|
39
|
+
export function requestReplyFloor(branch, reserve, maxTokens) {
|
|
40
|
+
const floor = branchReplyFloor(branch, reserve);
|
|
41
|
+
return maxTokens !== undefined && maxTokens > 0 ? Math.min(floor, maxTokens) : floor;
|
|
42
|
+
}
|
|
43
|
+
/** Check decoded text, including a whole verbatim user quote in our summary; never JSON-escaped substring matching. */
|
|
44
|
+
export function carriesPrompt(messages, prompt) {
|
|
45
|
+
if (!prompt.length)
|
|
46
|
+
return true;
|
|
47
|
+
return messages.some(message => {
|
|
48
|
+
if (message.role === 'user') {
|
|
49
|
+
const text = typeof message.content === 'string' ? message.content : message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n');
|
|
50
|
+
return text.includes(prompt);
|
|
51
|
+
}
|
|
52
|
+
if (message.role !== 'compactionSummary')
|
|
53
|
+
return false;
|
|
54
|
+
const split = splitSummary(message.summary);
|
|
55
|
+
return split !== null && split !== undefined && parseRecords(split.body, split.version).some(record => record.kind === 'excerpt' && record.role === 'user' && record.text.includes(prompt));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -129,7 +129,9 @@ export declare function healDelayMs(attempts: number, integrity: boolean, delays
|
|
|
129
129
|
export type SemanticState = 'disabled' | 'unavailable' | 'preparing' | 'ready' | 'waiting';
|
|
130
130
|
/** Where the current view starts, as a branch position: the latest compaction entry's first kept entry. Earlier entries were compacted away. With no compaction, nothing is out of view. */
|
|
131
131
|
export declare function viewStart(branch: SessionEntry[]): number;
|
|
132
|
-
|
|
132
|
+
/** `pendingKeptId`: the first kept entry of a cut pi-tuck has applied to the requests but Pi has not yet saved (a prompt's cut is saved at the end of
|
|
133
|
+
* the first turn). Tools in that turn see the history as the model does: everything before that entry is out of view. */
|
|
134
|
+
export declare function semanticEligibleSources(ctx: Pick<ExtensionContext, 'sessionManager' | 'cwd'>, policy: RecallPolicy, access: () => void, pendingKeptId?: string): Snapshot;
|
|
133
135
|
export declare class SemanticRecallController {
|
|
134
136
|
private readonly timing;
|
|
135
137
|
private readonly ownerId;
|
|
@@ -294,6 +296,9 @@ export declare class SemanticRecallController {
|
|
|
294
296
|
/** A plan for this snapshot that is fully indexed or, for a search, has all its out-of-view conversation indexed. The tool output behind it and the in-view tail are not worth waiting for: they keep indexing in the background and the coverage statement says how far they got.
|
|
295
297
|
* With no out-of-view conversation (no cut) nothing waits: the search returns at once. Only a window whose out-of-view part is tool output alone still waits for the whole window. */
|
|
296
298
|
private readyFor;
|
|
299
|
+
/** A search may rank a plan its own snapshot has grown from (every source in the plan still eligible, unchanged), as long as the session has not
|
|
300
|
+
* moved since the search took that snapshot; a move during the search still fails it. Indexing keeps the exact `recheck`. */
|
|
301
|
+
private recheckGrown;
|
|
297
302
|
private recheck;
|
|
298
303
|
private joinPreparation;
|
|
299
304
|
search(query: string, provider: () => Snapshot, signal?: AbortSignal, prior?: PriorResult[], words?: any): Promise<SemanticPage>;
|
|
@@ -427,6 +427,15 @@ export function viewStart(branch) {
|
|
|
427
427
|
}
|
|
428
428
|
const TOOL_OUTPUT = new Set(['toolResult', 'bashExecution']);
|
|
429
429
|
const isToolOutput = (source) => TOOL_OUTPUT.has(source.row.role);
|
|
430
|
+
/** Whether `now` only grew from `plan`: the same session and project, and every source the plan ranks still eligible with the same hash. */
|
|
431
|
+
function grownFrom(now, plan) {
|
|
432
|
+
if (now.session !== plan.session || now.project !== plan.project)
|
|
433
|
+
return false;
|
|
434
|
+
if (now.id === plan.id)
|
|
435
|
+
return true;
|
|
436
|
+
const shas = new Map(now.sources.map(source => [source.entry.id, source.sha]));
|
|
437
|
+
return plan.sources.every(source => shas.get(source.entry.id) === source.sha);
|
|
438
|
+
}
|
|
430
439
|
/** Sources in the order they are covered: out-of-view conversation (user messages, assistant text, tool-call names and arguments), then out-of-view tool output, then what is in view.
|
|
431
440
|
* Each part is newest first, extending backward. */
|
|
432
441
|
function windowOrder(snapshot) {
|
|
@@ -464,7 +473,9 @@ function windowCoverage(snapshot, prefix = indexedPrefix(snapshot)) {
|
|
|
464
473
|
return { indexedCharacters, windowCharacters: window.windowCharacters, outOfViewCharacters: window.conversationOutOfView + window.toolOutOfView, eligibleCharacters: window.eligibleCharacters, oldestCoveredEntryId: oldest?.entry.id ?? null, oldestCoveredTime: time(oldest),
|
|
465
474
|
conversation: { indexedCharacters: conversation, outOfViewCharacters: window.conversationOutOfView }, toolResults: { indexedCharacters: tools, outOfViewCharacters: window.toolOutOfView, oldestCoveredTime: time(oldestTool) } };
|
|
466
475
|
}
|
|
467
|
-
|
|
476
|
+
/** `pendingKeptId`: the first kept entry of a cut pi-tuck has applied to the requests but Pi has not yet saved (a prompt's cut is saved at the end of
|
|
477
|
+
* the first turn). Tools in that turn see the history as the model does: everything before that entry is out of view. */
|
|
478
|
+
export function semanticEligibleSources(ctx, policy, access, pendingKeptId) {
|
|
468
479
|
try {
|
|
469
480
|
access();
|
|
470
481
|
}
|
|
@@ -486,8 +497,9 @@ export function semanticEligibleSources(ctx, policy, access) {
|
|
|
486
497
|
const orders = new Map(branch.map((entry, order) => [entry.id, order]));
|
|
487
498
|
const sources = eligible.flatMap(entry => { const order = orders.get(entry.id); const row = recall.discoveryEntry(entry, attempts, links); if (!row)
|
|
488
499
|
return []; const { sourceText: unusedText, ranges: unusedRanges, ...readable } = row; return [{ entry, row: readable, sourceText: row.sourceText, ranges: row.ranges, order, sha: hash({ entry, order, row: readable, ranges: row.ranges, index: indices.get(entry.id) }) }]; });
|
|
489
|
-
const
|
|
490
|
-
|
|
500
|
+
const pending = pendingKeptId === undefined ? -1 : branch.findIndex(e => e.id === pendingKeptId), start = Math.max(viewStart(branch), pending);
|
|
501
|
+
const session = ctx.sessionManager.getSessionId(), id = hash({ session, project: resolve(ctx.cwd), branch: branch.map(e => [e.id, e.parentId]), sources: sources.map(s => [s.entry.id, s.sha]), ...(pending >= 0 ? { pendingKeptId } : {}) });
|
|
502
|
+
return { id, session, project: ctx.cwd, sources, units: [], complete: false, throughEntryId: sources.at(-1)?.entry.id ?? null, viewStart: start, indexed: new Set() };
|
|
491
503
|
}
|
|
492
504
|
export class SemanticRecallController {
|
|
493
505
|
timing;
|
|
@@ -647,11 +659,19 @@ export class SemanticRecallController {
|
|
|
647
659
|
return this.worker;
|
|
648
660
|
}
|
|
649
661
|
path(snapshot) { const directory = this.config.indexDirectory; outsideProject(snapshot.project, directory, 'index'); return join(directory, hash({ owner: this.ownerId, session: snapshot.session, project: resolve(snapshot.project) }) + '.sqlite'); }
|
|
650
|
-
async index(action, snapshot, extra = {}) { const path = this.path(snapshot), generation = this.generation, worker = await this.runtime(snapshot.project); if (!this.enabled || generation !== this.generation)
|
|
651
|
-
throw abort(); if (this.provider)
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
662
|
+
async index(action, snapshot, extra = {}, boundId) { const path = this.path(snapshot), generation = this.generation, worker = await this.runtime(snapshot.project); if (!this.enabled || generation !== this.generation)
|
|
663
|
+
throw abort(); if (this.provider) {
|
|
664
|
+
if (boundId !== undefined)
|
|
665
|
+
this.recheckGrown(this.provider, snapshot, boundId);
|
|
666
|
+
else
|
|
667
|
+
this.recheck(this.provider, snapshot);
|
|
668
|
+
} /* a search's scoring may read a plan its snapshot grew from */ /* a search's scoring may read a plan its snapshot grew from */ this.indexFiles.add(path); const result = await worker.call('index', { path, identity: this.identity, action, projectDirectory: snapshot.project, ...extra }); if (!this.enabled || generation !== this.generation)
|
|
669
|
+
throw abort(); if (this.provider) {
|
|
670
|
+
if (boundId !== undefined)
|
|
671
|
+
this.recheckGrown(this.provider, snapshot, boundId);
|
|
672
|
+
else
|
|
673
|
+
this.recheck(this.provider, snapshot);
|
|
674
|
+
} return result; }
|
|
655
675
|
async prepare(provider, full = true) {
|
|
656
676
|
if (!this.enabled)
|
|
657
677
|
return;
|
|
@@ -801,6 +821,10 @@ export class SemanticRecallController {
|
|
|
801
821
|
/** A plan for this snapshot that is fully indexed or, for a search, has all its out-of-view conversation indexed. The tool output behind it and the in-view tail are not worth waiting for: they keep indexing in the background and the coverage statement says how far they got.
|
|
802
822
|
* With no out-of-view conversation (no cut) nothing waits: the search returns at once. Only a window whose out-of-view part is tool output alone still waits for the whole window. */
|
|
803
823
|
readyFor(id, forSearch) { const plan = this.current; return Boolean(plan && plan.id === id && (plan.complete || (forSearch && plan.window !== undefined && plan.window.searchUnits > 0 && plan.units.slice(0, plan.window.searchUnits).every(unit => plan.indexed.has(unit.key))))); }
|
|
824
|
+
/** A search may rank a plan its own snapshot has grown from (every source in the plan still eligible, unchanged), as long as the session has not
|
|
825
|
+
* moved since the search took that snapshot; a move during the search still fails it. Indexing keeps the exact `recheck`. */
|
|
826
|
+
recheckGrown(provider, snapshot, boundId) { const now = provider(); if (now.id !== boundId || !grownFrom(now, snapshot))
|
|
827
|
+
throw new Error('Eligible branch or source changed before semantic admission'); }
|
|
804
828
|
recheck(provider, snapshot) { const now = provider(); if (now.id !== snapshot.id || now.session !== snapshot.session || now.project !== snapshot.project)
|
|
805
829
|
throw new Error('Eligible branch or source changed before semantic admission'); }
|
|
806
830
|
async joinPreparation(deadline, signal) {
|
|
@@ -868,22 +892,25 @@ export class SemanticRecallController {
|
|
|
868
892
|
const prepared = !this.task || await this.joinPreparation(deadline, signal);
|
|
869
893
|
if (signal?.aborted || queryGeneration !== this.generation)
|
|
870
894
|
throw abort();
|
|
871
|
-
if (
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
895
|
+
if (prepared && performance.now() < deadline) {
|
|
896
|
+
this.error = null;
|
|
897
|
+
this.schedule(provider);
|
|
898
|
+
await this.waitForReady(Math.max(0, deadline - performance.now()), signal, bound.id, true);
|
|
899
|
+
}
|
|
875
900
|
}
|
|
876
|
-
else
|
|
877
|
-
this.
|
|
878
|
-
|
|
901
|
+
else {
|
|
902
|
+
if (!this.current.complete) {
|
|
903
|
+
this.error = null;
|
|
904
|
+
this.schedule(provider);
|
|
905
|
+
}
|
|
906
|
+
await this.waitForReady(Math.max(0, deadline - performance.now()), signal, bound.id, true);
|
|
879
907
|
}
|
|
880
|
-
await this.waitForReady(Math.max(0, deadline - performance.now()), signal, bound.id, true);
|
|
881
908
|
if (signal?.aborted || queryGeneration !== this.generation)
|
|
882
909
|
throw abort();
|
|
883
910
|
}
|
|
884
911
|
// Ranking needs a plan for the bound snapshot and an indexed prefix; the coverage statement describes exactly that prefix and nothing else is ranked, so a search that reaches its wait limit with part of the window indexed ranks that part. Only an empty prefix is `indexing_incomplete`.
|
|
885
|
-
const snapshot = this.current;
|
|
886
|
-
if (!snapshot || snapshot.id !== bound.id || (snapshot.units.length ? indexedPrefix(snapshot) === 0 : !snapshot.complete)) {
|
|
912
|
+
const snapshot = this.current, older = snapshot !== undefined && snapshot.id !== bound.id && grownFrom(bound, snapshot);
|
|
913
|
+
if (!snapshot || (snapshot.id !== bound.id && !older) || (snapshot.units.length ? indexedPrefix(snapshot) === 0 : !snapshot.complete)) {
|
|
887
914
|
const failure = this.error ?? this.healReason;
|
|
888
915
|
return failure && !CHURN.test(failure) ? unavailable() : incomplete();
|
|
889
916
|
}
|
|
@@ -891,15 +918,15 @@ export class SemanticRecallController {
|
|
|
891
918
|
return { status: 'no_matches', notice: 'No eligible original source units in this snapshot.', coverage: this.status(), method: 'embedding', groups: [] };
|
|
892
919
|
try {
|
|
893
920
|
const worker = await this.runtime(snapshot.project);
|
|
894
|
-
this.
|
|
921
|
+
this.recheckGrown(provider, snapshot, bound.id);
|
|
895
922
|
const encoded = await worker.call('embed', { texts: [query], kind: 'query' }, signal);
|
|
896
923
|
this.operations.queryEmbeddings++;
|
|
897
|
-
this.
|
|
924
|
+
this.recheckGrown(provider, snapshot, bound.id);
|
|
898
925
|
const prefix = indexedPrefix(snapshot), stored = snapshot.units.slice(0, prefix), byKey = new Map(stored.map(u => [u.key, u])), covered = windowCoverage(snapshot, prefix);
|
|
899
926
|
let ranked = [], observed = 0;
|
|
900
927
|
for (let offset = 0; offset < stored.length; offset += SCORE_CHUNK) {
|
|
901
|
-
this.
|
|
902
|
-
const scored = await this.index('score', snapshot, { keys: stored.slice(offset, offset + SCORE_CHUNK).map(u => u.key), query: encoded.vectors[0] });
|
|
928
|
+
this.recheckGrown(provider, snapshot, bound.id);
|
|
929
|
+
const scored = await this.index('score', snapshot, { keys: stored.slice(offset, offset + SCORE_CHUNK).map(u => u.key), query: encoded.vectors[0] }, bound.id);
|
|
903
930
|
observed += scored.observed;
|
|
904
931
|
ranked.push(...scored.ranked.map(row => ({ unit: byKey.get(row.key), score: row.score })));
|
|
905
932
|
ranked.sort((a, b) => b.score - a.score);
|
|
@@ -912,7 +939,7 @@ export class SemanticRecallController {
|
|
|
912
939
|
throw new Error('Snapshot index coverage changed');
|
|
913
940
|
if (signal?.aborted || queryGeneration !== this.generation || !this.enabled)
|
|
914
941
|
throw abort();
|
|
915
|
-
this.
|
|
942
|
+
this.recheckGrown(provider, snapshot, bound.id);
|
|
916
943
|
const final = provider();
|
|
917
944
|
const finalSources = new Map(final.sources.map(source => [source.entry.id, source.sha]));
|
|
918
945
|
if (snapshot.sources.some(source => finalSources.get(source.entry.id) !== source.sha))
|
|
@@ -920,7 +947,7 @@ export class SemanticRecallController {
|
|
|
920
947
|
// What the caller already read only spares it a repeat. A reference that does not match this history spares nothing, and it must not fail the search.
|
|
921
948
|
const usable = prior.filter(reference => { const source = snapshot.sources.find(s => s.entry.id === reference.entryId); return Number.isSafeInteger(reference.start) && Number.isSafeInteger(reference.end) && reference.start <= reference.end && source !== undefined && reference.sourceSha === source.sha && reference.end <= source.sourceText.length; }), ignored = prior.length - usable.length;
|
|
922
949
|
const previous = [...usable, ...(words?.hits ?? []).map((hit) => ({ entryId: hit.entryId, start: hit.read.offset, end: hit.read.offset + hit.read.limit, sourceSha: snapshot.sources.find(s => s.entry.id === hit.entryId)?.sha }))];
|
|
923
|
-
return { status: 'ok', notice: 'Related original evidence, not current truth or proof that every qualification was found. Scores do not establish authority, event time or supersession. Sources preserve recorded time and original order.' + (ignored ? ` ${ignored} of the priorResults did not match this history and were ignored, so their text may appear again.` : '') + ` Semantic ranking covers ${covered.conversation.indexedCharacters} of ${covered.conversation.outOfViewCharacters} characters of earlier conversation and ${covered.toolResults.indexedCharacters} of ${covered.toolResults.outOfViewCharacters} characters of earlier tool output (newest first, back to ${covered.toolResults.oldestCoveredTime ?? covered.oldestCoveredTime ?? 'an unknown time'}); use tuck_recall for the rest
|
|
950
|
+
return { status: 'ok', notice: 'Related original evidence, not current truth or proof that every qualification was found. Scores do not establish authority, event time or supersession. Sources preserve recorded time and original order.' + (ignored ? ` ${ignored} of the priorResults did not match this history and were ignored, so their text may appear again.` : '') + ` Semantic ranking covers ${covered.conversation.indexedCharacters} of ${covered.conversation.outOfViewCharacters} characters of earlier conversation and ${covered.toolResults.indexedCharacters} of ${covered.toolResults.outOfViewCharacters} characters of earlier tool output (newest first, back to ${covered.toolResults.oldestCoveredTime ?? covered.oldestCoveredTime ?? 'an unknown time'}); use tuck_recall for the rest.` + (older ? ` This ranking covers the history through entry ${snapshot.throughEntryId}: later entries are still being indexed, so search them by keyword.` : ''), coverage: { ...this.status(), window: covered }, method: 'embedding', groups: this.assemble(ranked.map(hit => hit.unit), snapshot, previous) };
|
|
924
951
|
}
|
|
925
952
|
catch (error) {
|
|
926
953
|
if (error instanceof Error && error.name === 'SemanticAccessError') {
|
|
@@ -16,8 +16,8 @@ const startBudget = () => {
|
|
|
16
16
|
const deadline = Date.now() + SEARCH_BUDGET_MS;
|
|
17
17
|
return () => {
|
|
18
18
|
if (Date.now() > deadline) {
|
|
19
|
-
throw new Error(`Search aborted: query exceeded ${SEARCH_BUDGET_MS}ms
|
|
20
|
-
"
|
|
19
|
+
throw new Error(`Search aborted: query exceeded ${SEARCH_BUDGET_MS}ms on this history. ` +
|
|
20
|
+
"Use more specific words, or read an entry by index.");
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
23
|
};
|
package/dist/src/tuck.d.ts
CHANGED
|
@@ -74,7 +74,7 @@ export interface Tuck {
|
|
|
74
74
|
import { compileDialogue } from "./tuck/core/summarize.js";
|
|
75
75
|
export declare const tuck: Tuck;
|
|
76
76
|
/** Uses pi-tuck's own cut, calibrated maximum tail and dialogue compiler; Pi owns persistence and continuation. */
|
|
77
|
-
export declare function tuckDraft(tuck: Tuck, event: SessionBeforeCompactEvent, ctx: ExtensionContext, reserveTokens: number, measure: Measure, policy?: RecallPolicy, retainCurrentInput?: boolean, operatorManual?: boolean, newPrompt?: boolean): Draft;
|
|
77
|
+
export declare function tuckDraft(tuck: Tuck, event: SessionBeforeCompactEvent, ctx: ExtensionContext, reserveTokens: number, measure: Measure, policy?: RecallPolicy, retainCurrentInput?: boolean, operatorManual?: boolean, newPrompt?: boolean, protectedPromptEntryId?: string): Draft;
|
|
78
78
|
/** Shared compiler observes canonical branch and current token estimate; no native preparation utility. */
|
|
79
79
|
/** `newPrompt`: the cut is planned for a run that has not begun (a prompt Pi has not appended yet, an operator's manual cut, Pi's own cut before
|
|
80
80
|
* a run a custom message starts); the session has moved on from an ended run.
|
|
@@ -86,6 +86,7 @@ export declare function tuckObservedDraft(tuck: Tuck, input: {
|
|
|
86
86
|
operatorManual?: boolean;
|
|
87
87
|
newPrompt?: boolean;
|
|
88
88
|
reRenderSummary?: boolean;
|
|
89
|
+
protectedPromptEntryId?: string;
|
|
89
90
|
}, ctx: ExtensionContext, reserveTokens: number, measure: Measure, policy?: RecallPolicy, retainCurrentInput?: boolean): Draft;
|
|
90
91
|
/** Numeric references use the original session index, then intersect with readable current-branch entries. */
|
|
91
92
|
export declare function tuckRecall(tuck: Tuck, entries: readonly SessionEntry[], branch: readonly SessionEntry[], input: RecallOptions & {
|
package/dist/src/tuck.js
CHANGED
|
@@ -19,8 +19,8 @@ import { prepareKeywordQuery } from "./tuck/core/keyword-query.js";
|
|
|
19
19
|
import { searchEntriesDetailed } from "./tuck/core/search-entries.js";
|
|
20
20
|
export const tuck = { buildOwnCut, applyTailBudget, calibrateCharsPerToken, buildGlobalIndexById, compileDialogue, prepareKeywordQuery, searchEntriesDetailed };
|
|
21
21
|
/** Uses pi-tuck's own cut, calibrated maximum tail and dialogue compiler; Pi owns persistence and continuation. */
|
|
22
|
-
export function tuckDraft(tuck, event, ctx, reserveTokens, measure, policy = {}, retainCurrentInput = true, operatorManual = false, newPrompt = false) {
|
|
23
|
-
return tuckObservedDraft(tuck, { branchEntries: event.branchEntries, tokensBefore: event.preparation.tokensBefore, previousSummary: event.preparation.previousSummary, operatorManual, newPrompt }, ctx, reserveTokens, measure, policy, retainCurrentInput);
|
|
22
|
+
export function tuckDraft(tuck, event, ctx, reserveTokens, measure, policy = {}, retainCurrentInput = true, operatorManual = false, newPrompt = false, protectedPromptEntryId) {
|
|
23
|
+
return tuckObservedDraft(tuck, { branchEntries: event.branchEntries, tokensBefore: event.preparation.tokensBefore, previousSummary: event.preparation.previousSummary, operatorManual, newPrompt, protectedPromptEntryId }, ctx, reserveTokens, measure, policy, retainCurrentInput);
|
|
24
24
|
}
|
|
25
25
|
/** Shared compiler observes canonical branch and current token estimate; no native preparation utility. */
|
|
26
26
|
/** `newPrompt`: the cut is planned for a run that has not begun (a prompt Pi has not appended yet, an operator's manual cut, Pi's own cut before
|
|
@@ -115,6 +115,9 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
|
|
|
115
115
|
const current = entries[userPosition];
|
|
116
116
|
const userContent = current?.type === "message" && current.message.role === "user" ? current.message.content : undefined;
|
|
117
117
|
const textOnly = isTextOnly(userContent);
|
|
118
|
+
// Steering can become the latest user source while the run still owns its original prompt.
|
|
119
|
+
const startingPosition = entries.findIndex(entry => entry.id === input.protectedPromptEntryId && entry.type === "message" && entry.message.role === "user");
|
|
120
|
+
const starting = entries[startingPosition];
|
|
118
121
|
// Anchor the actual first source only; a native omission never promotes a later prompt.
|
|
119
122
|
const firstUser = branch.find(entry => entry.type === "message" && entry.message.role === "user");
|
|
120
123
|
const firstUserPosition = entries.findIndex(entry => entry.id === firstUser?.id && entry.type === "message");
|
|
@@ -277,8 +280,10 @@ export function tuckObservedDraft(tuck, input, ctx, reserveTokens, measure, poli
|
|
|
277
280
|
};
|
|
278
281
|
if (userPosition >= 0 && userPosition < boundary && textOnly)
|
|
279
282
|
add(current, Infinity);
|
|
283
|
+
if (startingPosition >= 0 && startingPosition < boundary && starting?.id !== current?.id)
|
|
284
|
+
add(starting, Infinity);
|
|
280
285
|
for (const entry of anchors)
|
|
281
|
-
if (entry.id !== current?.id && positions.get(entry.id) < boundary)
|
|
286
|
+
if (entry.id !== current?.id && entry.id !== starting?.id && positions.get(entry.id) < boundary)
|
|
282
287
|
add(entry, length);
|
|
283
288
|
const omissions = [];
|
|
284
289
|
for (const role of ["user", "assistant", "toolResult"]) {
|
package/package.json
CHANGED
package/src/build-id.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const BUILD_ID = "
|
|
1
|
+
export const BUILD_ID = "37e6c0132f0638bca430bc165bef86ac4814e136c47e4672e06b086310a2de70";
|