pi-tuck 1.0.26 → 1.0.28
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 +47 -0
- package/README.md +9 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +37 -24
- package/dist/src/build-id.d.ts +1 -1
- package/dist/src/build-id.js +1 -1
- package/dist/src/compaction.d.ts +4 -0
- package/dist/src/compaction.js +156 -172
- package/dist/src/diagnostics.d.ts +1 -1
- package/dist/src/request-capacity.d.ts +8 -3
- package/dist/src/request-capacity.js +19 -14
- package/dist/src/run.d.ts +52 -0
- package/dist/src/run.js +89 -0
- package/dist/src/semantic-recall.d.ts +72 -2
- package/dist/src/semantic-recall.js +58 -12
- package/dist/src/status.d.ts +1 -0
- package/dist/src/status.js +2 -1
- package/dist/src/tuck.js +33 -27
- package/index.ts +31 -22
- package/package.json +1 -1
- package/src/build-id.ts +1 -1
- package/src/compaction.ts +137 -155
- package/src/request-capacity.ts +18 -12
- package/src/run.ts +91 -0
- package/src/semantic-recall.ts +46 -14
- package/src/status.ts +3 -1
- package/src/tuck.ts +32 -25
package/src/compaction.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { textOf } from "./tuck/core/content.js";
|
|
2
|
-
import {
|
|
2
|
+
import { PendingCut, StartingPrompt, createRun, type CutEntry, type Run } from "./run.js";
|
|
3
|
+
import { requestReplyFloor, replyFit, carriesPrompt, thinkingOnlyLength, piContextSafetyMargin, loadPiRequestMath } from "./request-capacity.js";
|
|
3
4
|
import { BUILD_ID } from "./build-id.js";
|
|
4
5
|
import { createSemanticRecall, budgetSemantic, type SemanticRecallController } from "./semantic-recall";
|
|
5
6
|
import { DIAGNOSTIC_BUILD, IncidentRecorder, attachReceipt, permittedEntries, receiptId, findReceiptEntry, type Receipt, type RecorderOptions } from "./diagnostics.js";
|
|
@@ -12,7 +13,7 @@ import { applyClips, clipBudgets, clipFor, type Clip } from "./clip.js";
|
|
|
12
13
|
import { DEFAULT_CUT_AT, bandOf, chooseCut, conversationTokens, gateCut, incomingEventTokens, incomingTokens, measurableConversationTokens, planningReserve, projectedConversationTokens, systemPart, validCutAt,
|
|
13
14
|
type AgentMessage, type Decision, type Messages, type Plan, type Target } from "./budget.js";
|
|
14
15
|
import { getCurrentSystemMessage, isContextOverflow, normalizeContext, type ImageContent, type ToolResultMessage } from "@earendil-works/pi-ai";
|
|
15
|
-
import { calculateContextTokens, convertToLlm, estimateTokens, shouldCompact,
|
|
16
|
+
import { calculateContextTokens, convertToLlm, estimateTokens, shouldCompact, SettingsManager, type ExtensionAPI, type ExtensionContext, type SessionEntry, type SessionBoundaryDraft, type TurnEndEvent } from "@earendil-works/pi-coding-agent";
|
|
16
17
|
import { Type } from "typebox";
|
|
17
18
|
import { Check } from "typebox/value";
|
|
18
19
|
import type { RecallPolicy } from "./recall.js";
|
|
@@ -22,7 +23,7 @@ import { tuck, tuckDraft, tuckObservedDraft, tuckRecall } from "./tuck.js";
|
|
|
22
23
|
import { LOADED_VERSION, statusText } from "./status.js";
|
|
23
24
|
import { ImageRecallInput, historyImage, historyAround } from "./history-image.js";
|
|
24
25
|
import { eligibilityKey, eligibilityKeyMoved, memoizedEligibleSources } from "./eligibility-cache";
|
|
25
|
-
import { applyHistoryTools,
|
|
26
|
+
import { applyHistoryTools, historyToolDeclarations, historyToolNames, stageHistoryTools } from "./history-tools.js";
|
|
26
27
|
|
|
27
28
|
const query = Type.String({ minLength: 1, maxLength: MAX_RECALL_QUERY_CHARACTERS });
|
|
28
29
|
const entryId = Type.String({ minLength: 1, maxLength: MAX_RECALL_ENTRY_ID_CHARACTERS });
|
|
@@ -47,7 +48,7 @@ const RecallInput = Type.Union([
|
|
|
47
48
|
const SearchInput = Type.Object({query,priorResults:Type.Optional(Type.Array(Type.Object({sourceSha:Type.String({minLength:64,maxLength:64}),read:Type.Object({entryId,offset:Type.Integer({minimum:0}),limit:Type.Integer({minimum:0})},{additionalProperties:false})},{additionalProperties:false}),{maxItems:20}))},{additionalProperties:false});
|
|
48
49
|
const reply = (value: unknown) => ({ content: [{ type: "text" as const, text: JSON.stringify(value) }], details: {} });
|
|
49
50
|
/** The compactor's verdict that this attempt cannot be made: the cut it needs cannot be made, or the request cannot fit. Like every failure but
|
|
50
|
-
* a `CommitFault`, it ends with its attempt (`
|
|
51
|
+
* a `CommitFault`, it ends with its attempt (`resetRun`), and the next attempt plans afresh from the session as it is. */
|
|
51
52
|
class Refusal extends Error {}
|
|
52
53
|
/** A cut this compactor supplied that Pi then failed to commit. Pi appends a cut to the session in memory before it writes the file, so the
|
|
53
54
|
* session may hold a cut its file lacks; what is written after would build on it. The one failure that stays until the session restarts. */
|
|
@@ -150,11 +151,16 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
150
151
|
// Semantic indexing outlives any one attempt, so it checks only that the host still grants access: a compactor failure, which ends with its
|
|
151
152
|
// attempt, must not read as a revocation (that disables semantic recall and deletes its index for the rest of the session).
|
|
152
153
|
// A prompt's cut reaches the requests at once but the session only at the end of the first turn: until then search sees the history as the model does.
|
|
153
|
-
const
|
|
154
|
+
const view = (ctx: ExtensionContext, threshold = false) => (threshold && run?.expired ? liveCut : run?.cut ?? liveCut).view(ctx);
|
|
155
|
+
const liveCut = new PendingCut();
|
|
156
|
+
const semanticProvider = (ctx: ExtensionContext) => () => {
|
|
157
|
+
const current = view(ctx);
|
|
158
|
+
return memoizedEligibleSources(ctx, options.recallPolicy, hostAccess, current.pending ? current.keptFromId : undefined);
|
|
159
|
+
};
|
|
154
160
|
const recorder = new IncidentRecorder(options.diagnostics);
|
|
155
161
|
let proposalId: string | undefined;
|
|
156
162
|
const observe = (ctx: ExtensionContext) => recorder.measureInline(() => {
|
|
157
|
-
if (!active() || failure) return;
|
|
163
|
+
if (!active() || failure()) return;
|
|
158
164
|
if (!proposalId) return;
|
|
159
165
|
const found = findReceiptEntry(ctx.sessionManager.getBranch(), proposalId);
|
|
160
166
|
if (found) { recorder.capture("observed-entry", "current-native-branch-memory-not-durable", { entryId: found.id, diagnosticReceiptId: proposalId, details: found.type === "compaction" ? found.details : undefined }); recorder.cut(); proposalId = undefined; }
|
|
@@ -214,15 +220,18 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
214
220
|
ctx.ui.notify(JSON.stringify({...semanticStatus(),...(errors.length?{errors}:{})}),errors.length?"warning":"info");
|
|
215
221
|
}});
|
|
216
222
|
pi.registerCommand("tuck", { description: "pi-tuck's state in this session: version, cuts, cut point, search", handler: async (_args, ctx) => {
|
|
217
|
-
const
|
|
218
|
-
ctx.ui.notify(statusText({ version: LOADED_VERSION, active: active(), branch: ctx.sessionManager.getBranch(), cutAt: options.cutAt, search, semantic:
|
|
223
|
+
const state = semantic.status();
|
|
224
|
+
ctx.ui.notify(statusText({ version: LOADED_VERSION, active: active(), branch: ctx.sessionManager.getBranch(), cutAt: options.cutAt, search, semantic: state, pending: view(ctx).pending }), "info");
|
|
219
225
|
} });
|
|
220
226
|
// Pi's native start/settled events bracket the run and its post-turn cuts.
|
|
221
|
-
let
|
|
222
|
-
|
|
223
|
-
let
|
|
224
|
-
const
|
|
225
|
-
|
|
227
|
+
let run: Run | undefined;
|
|
228
|
+
// A native append may have mutated memory before its disk write failed. This session fault outlives the run.
|
|
229
|
+
let commitFault: Error | undefined;
|
|
230
|
+
const failure = () => commitFault ?? run?.failure;
|
|
231
|
+
const protectedPrompt = (ctx: ExtensionContext) => run?.prompt?.id(ctx.sessionManager.getBranch());
|
|
232
|
+
const clips = () => run?.clips ?? emptyClips;
|
|
233
|
+
const emptyClips = new Map<string, Clip>();
|
|
234
|
+
const resetRun = () => { run = undefined; pending = undefined; };
|
|
226
235
|
const promptRefusal = () => new Refusal("Prompt check failed: the whole starting prompt cannot fit after cutting to the band and room, shrinking the summary and clipping fresh results. Shorten the prompt or choose a model with a larger context window, then retry");
|
|
227
236
|
let estimatedTokens = 0;
|
|
228
237
|
// One serial request, one accounting identity; observations never certify new content. A new identity (the first request after a session
|
|
@@ -257,7 +266,7 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
257
266
|
const paired = pending; pending = undefined;
|
|
258
267
|
const key = identityFor(ctx), message = event.message;
|
|
259
268
|
const overflow = message.stopReason === "error" && isContextOverflow(message);
|
|
260
|
-
if (!paired || !key || paired.identity !== key || !active() || failure || ctx.signal?.aborted || (message.errorMessage && !overflow) ||
|
|
269
|
+
if (!paired || !key || paired.identity !== key || !active() || failure() || ctx.signal?.aborted || (message.errorMessage && !overflow) ||
|
|
261
270
|
message.provider !== ctx.model?.provider || message.model !== ctx.model.id || message.api !== ctx.model.api ||
|
|
262
271
|
(!overflow && !["stop", "toolUse", "length"].includes(message.stopReason)) ||
|
|
263
272
|
!isDeepStrictEqual(convertToLlm(paired.messages), paired.snapshot)) return;
|
|
@@ -282,8 +291,19 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
282
291
|
// 6,000-token prompt refuse. The overflow branch above learns from a refusal, where the count is a lower bound, and the ceiling is unchanged.
|
|
283
292
|
if (input > 0 && input <= ctx.model.contextWindow && Number.isSafeInteger(input) && Number.isFinite(ratio)) learn(ratio);
|
|
284
293
|
});
|
|
285
|
-
|
|
286
|
-
|
|
294
|
+
// The operator's length-stop notice is decided at settle and is not attempt state: a run reset before then (a tree move inside another
|
|
295
|
+
// extension's agent_settled, a failure) must not erase it. Only the final assistant reply decides it; a successful retry clears it.
|
|
296
|
+
let lengthStop = false;
|
|
297
|
+
pi.on("agent_start", () => { run ??= createRun("admitted"); run.phase = "admitted"; run.thinkingRetryPending = false; lengthStop = false; pending = undefined; });
|
|
298
|
+
pi.on("message_end", event => { if (event.message.role === "assistant") lengthStop = event.message.stopReason === "length"; });
|
|
299
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
300
|
+
try {
|
|
301
|
+
if (!lengthStop || !active()) return;
|
|
302
|
+
lengthStop = false;
|
|
303
|
+
const message = "pi-tuck: the last reply stopped at the model's output limit and may be cut off. Ask the agent to continue.";
|
|
304
|
+
try { if (ctx.hasUI === false) process.stderr.write(message + "\n"); else ctx.ui.notify(message, "warning"); } catch { /* ended */ }
|
|
305
|
+
} finally { resetRun(); }
|
|
306
|
+
});
|
|
287
307
|
const active = () => options.active?.() ?? true;
|
|
288
308
|
const settingsFor = () => options.settings ?? SettingsManager.inMemory(pi.getSettings());
|
|
289
309
|
// The one reserve decision; every consumer takes its room from limits. Standalone starts at Pi's native cut point. Once a reply did
|
|
@@ -296,8 +316,7 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
296
316
|
const budget = host ?? { response: settingsFor().getCompactionSettings(ctx.model).reserveTokens };
|
|
297
317
|
if (!Number.isSafeInteger(budget.response) || budget.response < 0)
|
|
298
318
|
throw new Error("Invalid host request reserve");
|
|
299
|
-
const floor = host ? host.responseExact !== true : ctx.sessionManager.getBranch().
|
|
300
|
-
entry.type === "message" && needsFullReplyReserve(entry.message));
|
|
319
|
+
const floor = host ? host.responseExact !== true : replyFit(ctx.sessionManager.getBranch()).failed;
|
|
301
320
|
return { response: floor ? Math.max(budget.response, ctx.model?.maxTokens ?? 0) : budget.response };
|
|
302
321
|
};
|
|
303
322
|
const inputRoom = (response: number, ctx: ExtensionContext) => {
|
|
@@ -315,20 +334,20 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
315
334
|
const hardLine = Math.min(Math.max(room, (ctx.model?.contextWindow ?? 0) - piMargin - floor), lastCeiling ?? Infinity);
|
|
316
335
|
return { response, room, hardLine, replyFloor: floor };
|
|
317
336
|
};
|
|
318
|
-
const decisionBand = (ctx: ExtensionContext, room: number, system: number) => {
|
|
319
|
-
const last =
|
|
337
|
+
const decisionBand = (ctx: ExtensionContext, room: number, system: number, current = view(ctx)) => {
|
|
338
|
+
const last = current.session.getBranch().findLast(entry => entry.type === "compaction");
|
|
320
339
|
const after = last?.type === "compaction" ? (last.details as { tokensAfter?: unknown } | undefined)?.tokensAfter : undefined;
|
|
321
340
|
return bandOf(room, cutAt, options.cutAt === undefined && typeof after === "number" && Number.isFinite(after) && after >= 0
|
|
322
341
|
? { system, tokensAfter: after, factor } : undefined);
|
|
323
342
|
};
|
|
324
343
|
const fits = (tokens: number, limit: number, ctx: ExtensionContext) => corrected(tokens, ctx) < limit;
|
|
325
344
|
// The saved conversation and a cut's quantities are counted by the one function the guard counts with (strict, as the context hook is).
|
|
326
|
-
const measureFor = (ctx: ExtensionContext) => (messages: Messages) => projectedConversationTokens(applyClips(messages, clips), ctx, options.recallPolicy);
|
|
345
|
+
const measureFor = (ctx: ExtensionContext) => (messages: Messages) => projectedConversationTokens(applyClips(messages, clips()), ctx, options.recallPolicy);
|
|
327
346
|
// The planner counts with the same function, except that it does not fail on a summary in another format: a cut can replace one
|
|
328
347
|
// (an operator's /compact repairs a session whose saved summary is foreign). Identical to the strict one for this compactor's summaries.
|
|
329
348
|
const plannerMeasureFor = (ctx: ExtensionContext) => Object.assign(
|
|
330
|
-
(messages: Messages) => measurableConversationTokens(applyClips(messages, clips), ctx, options.recallPolicy),
|
|
331
|
-
{ messageTokens: (message: Messages[number]) => conversationTokens(applyClips([message], clips)) });
|
|
349
|
+
(messages: Messages) => measurableConversationTokens(applyClips(messages, clips()), ctx, options.recallPolicy),
|
|
350
|
+
{ messageTokens: (message: Messages[number]) => conversationTokens(applyClips([message], clips())) });
|
|
332
351
|
const hostMessages = (ctx: ExtensionContext) => options.hostMessages?.(ctx) ?? [];
|
|
333
352
|
const toolShapes = (names: Iterable<string>) => {
|
|
334
353
|
const selected = new Set(names);
|
|
@@ -356,17 +375,17 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
356
375
|
const saved = messages.some(message => message.role === "system") ? messages :
|
|
357
376
|
[{ role: "system" as const, content: prompt, toolsAdded: shapes, timestamp: 0 }, ...messages];
|
|
358
377
|
const declared = enabled ? historyToolDeclarations(saved, shapes, true) : saved;
|
|
359
|
-
return applyClips([...host, ...(enabled ? projectedMessages(declared, ctx, options.recallPolicy) : declared), ...incoming], clips);
|
|
378
|
+
return applyClips([...host, ...(enabled ? projectedMessages(declared, ctx, options.recallPolicy) : declared), ...incoming], clips());
|
|
360
379
|
};
|
|
361
380
|
const clampAboveRoom = <D extends object>(decision: Decision<D>, ctx: ExtensionContext, room: number, target: number,
|
|
362
381
|
total: (decision: Decision<D>) => number, project: (decision: Decision<D>) => Messages, replan: (target: number) => Decision<D>, deepest: () => Decision<D>): Decision<D> => {
|
|
363
382
|
if (options.requestReserve || !piClamp) return decision;
|
|
364
383
|
const lastAttempt = (): Decision<D> => {
|
|
365
|
-
const previousClips = new Map(clips);
|
|
384
|
+
const previousClips = new Map(clips());
|
|
366
385
|
const candidate = deepest();
|
|
367
386
|
if (candidate.kind === "cut" && fits(total(candidate), limits(ctx).hardLine, ctx) && requestClamp(project(candidate), ctx).leavesFloor)
|
|
368
387
|
return candidate;
|
|
369
|
-
clips.clear(); for (const [id, clip] of previousClips) clips.set(id, clip);
|
|
388
|
+
clips().clear(); for (const [id, clip] of previousClips) clips().set(id, clip);
|
|
370
389
|
return { kind: "fault", capacity: true, message: clampMessage };
|
|
371
390
|
};
|
|
372
391
|
for (let retry = 0; ; retry++) {
|
|
@@ -386,22 +405,7 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
386
405
|
const isNoWork = (message: string | undefined) => ["Already compacted", "Nothing to compact (session too small)"].includes(message?.replace(/^Compaction failed: /, "") ?? "");
|
|
387
406
|
// A missing model is configuration, not capacity.
|
|
388
407
|
const missingModel = () => new Error("No context model for the request budget");
|
|
389
|
-
// A prompt's cut, until its run saves it at its first boundary: every request of that run carries it (the context hook). A run that ends
|
|
390
|
-
// before a boundary drops it, and the next prompt decides afresh.
|
|
391
|
-
type CutEntry = { type: "compaction"; summary: string; firstKeptEntryId: string; tokensBefore: number; details?: unknown };
|
|
392
408
|
type CutPlan = { compaction: NonNullable<ReturnType<typeof tuckDraft>["compaction"]>; reserve: number; messages: Messages };
|
|
393
|
-
let promptCut: CutEntry | undefined;
|
|
394
|
-
// Where the branch ended when the prompt's cut was planned: the prompt is the entry Pi appends there. A cut that keeps nothing of the conversation
|
|
395
|
-
// before it names no entry to keep from, and read as it stands that drops the prompt too, so the model never sees it.
|
|
396
|
-
let promptCutAt = 0;
|
|
397
|
-
const keptFrom = (cut: CutEntry, ctx: ExtensionContext) => cut.firstKeptEntryId ||
|
|
398
|
-
ctx.sessionManager.getBranch().slice(promptCutAt).find(entry => entry.type !== "custom" || entry.customType !== "pi-tuck-capacity")?.id || "";
|
|
399
|
-
let failure: Error | undefined;
|
|
400
|
-
let refusalReceipted = false;
|
|
401
|
-
let pendingBoundaryCommit = false;
|
|
402
|
-
// Fresh tool results the room cannot hold, by call id: each request of the run carries the clip in their place; the session keeps them
|
|
403
|
-
// whole. A clip ends with its run like any failure would: the next run plans from the session as it is, where the result is no longer fresh.
|
|
404
|
-
const clips = new Map<string, Clip>();
|
|
405
409
|
const receipt = (kind: "refusal" | "above-room", reason: string, ctx: ExtensionContext) => {
|
|
406
410
|
let room: number | null = null, hardLine: number | null = null;
|
|
407
411
|
try { ({ room, hardLine } = limits(ctx)); } catch { /* Invalid configuration has no usable capacity. Preserve its original failure. */ }
|
|
@@ -411,42 +415,40 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
411
415
|
recorder.stop("native-attempt-failed");
|
|
412
416
|
// Every refusal says why, once per attempt: a capacity error with what the user can change, a cut Pi failed to commit with what that
|
|
413
417
|
// means for the session, anything else as it is (an attempt that ended with no word looked like a hang).
|
|
414
|
-
const notify = !
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
418
|
+
const previous = failure(), notify = !previous;
|
|
419
|
+
const fault = previous instanceof CommitFault ? previous : error instanceof Error ? error : new Error(String(error));
|
|
420
|
+
const receipted = run?.refusalReceipted;
|
|
421
|
+
resetRun();
|
|
422
|
+
// Keep only a terminal attempt record until Pi settles: its fresh abort controller must still see the refusal.
|
|
423
|
+
run = createRun("failed"); run.failure = fault; run.refusalReceipted = true;
|
|
424
|
+
if (fault instanceof CommitFault) commitFault = fault;
|
|
425
|
+
try {
|
|
426
|
+
if (!receipted) receipt("refusal", fault.message, ctx);
|
|
422
427
|
options.fail?.(error);
|
|
423
428
|
} finally {
|
|
424
|
-
try { if (notify) ctx.ui.notify(
|
|
425
|
-
? `pi-tuck: ${
|
|
426
|
-
: `pi-tuck could not plan this request (${
|
|
429
|
+
try { if (notify) ctx.ui.notify(fault instanceof CapacityError ? refusalNotice(fault.message, factor) : fault instanceof CommitFault
|
|
430
|
+
? `pi-tuck: ${fault.message}; the session holds a cut its file may lack, so start a new session.`
|
|
431
|
+
: `pi-tuck could not plan this request (${fault.message}); the next prompt is planned afresh.`, "error"); }
|
|
427
432
|
finally { ctx.abort(); }
|
|
428
433
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
// input started (a custom message) has only the first of these. No failure but a CommitFault outlives its attempt.
|
|
432
|
-
const forgetFailure = () => { refusalReceipted = false; if (!(failure instanceof CommitFault)) failure = undefined; };
|
|
434
|
+
};
|
|
435
|
+
|
|
433
436
|
const access = () => {
|
|
434
|
-
if (failure) throw failure;
|
|
437
|
+
if (failure()) throw failure();
|
|
435
438
|
hostAccess();
|
|
436
439
|
};
|
|
437
|
-
// Abort alone is insufficient: Pi starts a fresh abort controller after
|
|
438
|
-
// before_agent_start. Keep this attempt blocked, then recheck on new idle input.
|
|
439
|
-
// "decided": `input` has decided for the run it starts, so that run's before_agent_start does not decide again. "starting": a run is
|
|
440
|
-
// between before_agent_start and agent_start, where Pi 1.1 still reports idle but a message joins that run.
|
|
441
|
-
let phase: "idle" | "decided" | "starting" = "idle";
|
|
442
440
|
pi.on("input", async (event, ctx) => {
|
|
443
|
-
if (!ctx.isIdle() || phase === "starting") return; //
|
|
444
|
-
|
|
445
|
-
|
|
441
|
+
if (!ctx.isIdle() || run?.phase === "starting") return; // steering joins the existing run
|
|
442
|
+
run = createRun("decided");
|
|
443
|
+
const decision = run;
|
|
446
444
|
try { syncHistoryTools(ctx); await preflight(ctx, incomingTokens(event.text, event.images),
|
|
447
445
|
[{ role: "user", content: [{ type: "text", text: event.text }, ...(event.images ?? [])], timestamp: 0 }], ctx.getSystemPrompt(), pi.getActiveTools()); syncHistoryTools(ctx); }
|
|
448
446
|
catch (error) { fail(error, ctx); }
|
|
447
|
+
// Pi normally reaches its pre-run threshold check within microtasks. Auth I/O or a later extension's input I/O can make a
|
|
448
|
+
// real prompt expire too; safely fall back to carrying its cut in the request and saving it at the first boundary.
|
|
449
|
+
setImmediate(() => { if (run === decision && !decision.claimed) decision.expired = true; });
|
|
449
450
|
});
|
|
451
|
+
|
|
450
452
|
pi.registerTool({
|
|
451
453
|
name: "tuck_recall", label: "pi-tuck history", parameters: RecallInput,
|
|
452
454
|
description: "Keyword lookup locates particular text in your current branch. {query} returns ranked passages with read coordinates; punctuation separates words. Pass the returned next as {continuation} unchanged. " + (search ? "Use tuck_search for historical information relevant to a question, including different wording and qualifications. " : "") + "Knowing an identifier does not itself justify narrowing discovery. {entryId} or {index} (a summary sourceIndex or #N) reads one source, optionally with query, offset and limit in UTF-16 characters (limit defaults to 8000). {} browses history; {around} expands neighbors; {entryId,image} reads an image; outputPath writes an original to a file with the native write tool. Hidden reasoning and system records are excluded.",
|
|
@@ -500,26 +502,28 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
500
502
|
if (event.signal.aborted || ctx.signal?.aborted) { recorder.stop("cut-cancelled"); return { cancel: true }; }
|
|
501
503
|
// Pi checks its own threshold after a prompt's input and before the run starts: the prompt's cut, planned with the prompt counted,
|
|
502
504
|
// is the answer, and it is saved before the first request.
|
|
503
|
-
if (
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
505
|
+
if (run?.phase === "decided" && !run.expired && event.reason === "threshold") {
|
|
506
|
+
run.claimed = true;
|
|
507
|
+
if (view(ctx).pending) {
|
|
508
|
+
access();
|
|
509
|
+
const current = view(ctx), { type: _, ...compaction } = run.cut.take()!;
|
|
510
|
+
return { compaction: { ...compaction, firstKeptEntryId: current.keptFromId! } };
|
|
511
|
+
}
|
|
508
512
|
}
|
|
509
513
|
const operatorManual = event.reason === "manual";
|
|
510
|
-
if (operatorManual &&
|
|
514
|
+
if (operatorManual && run?.phase !== "admitted") resetRun();
|
|
511
515
|
access();
|
|
512
516
|
// pi-tuck's summary is built from the session itself, so text after /compact cannot steer it. The operator asked for a cut: make the one
|
|
513
517
|
// plain /compact makes and say the text was not used, rather than cancel (which Pi shows as an error, with no cut).
|
|
514
518
|
if (event.customInstructions?.trim()) ctx.ui.notify("pi-tuck: /compact ignores the text after it, since pi-tuck builds its summary from the session itself. Cutting as plain /compact does.", "info");
|
|
515
519
|
const { response, room } = limits(ctx);
|
|
516
|
-
const retain = operatorManual ||
|
|
520
|
+
const retain = operatorManual || run?.phase === "admitted";
|
|
517
521
|
// A cut Pi asks for (an operator's /compact, Pi's threshold or overflow recovery) charges what the request must carry whatever is
|
|
518
522
|
// cut: the system part and the host input. A prompt's cut is pi-tuck's own and never comes here.
|
|
519
523
|
const saved = ctx.sessionManager.buildSessionProjection().messages;
|
|
520
524
|
const hostInput = hostMessages(ctx), host = conversationTokens(hostInput);
|
|
521
525
|
const system = requestSystemPart(ctx.getSystemPrompt(), pi.getActiveTools(), saved, ctx.getSystemPrompt());
|
|
522
|
-
const mandatory = system + host, band = decisionBand(ctx, room, system);
|
|
526
|
+
const mandatory = system + host, band = decisionBand(ctx, room, system, view(ctx, event.reason === "threshold"));
|
|
523
527
|
const before = plannerMeasureFor(ctx)(saved);
|
|
524
528
|
observeBudget(before + mandatory, ctx);
|
|
525
529
|
// Pi's own usage threshold (reason "threshold") counts the last reply's output tokens, reasoning included, plus the messages after
|
|
@@ -541,9 +545,9 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
541
545
|
const reserve = planReserve(target, targetRoom, targetBand, mandatory, ctx);
|
|
542
546
|
// Every cut asked while no run is admitted (an operator's cut) is planned for a run that has not begun: the session has moved on
|
|
543
547
|
// from any run that ended. A retry is inside its run.
|
|
544
|
-
const draft = tuckDraft(tuck, event, ctx, reserve, plannerMeasureFor(ctx), options.recallPolicy, retain, operatorManual, operatorManual ||
|
|
548
|
+
const draft = tuckDraft(tuck, event, ctx, reserve, plannerMeasureFor(ctx), options.recallPolicy, retain, operatorManual, operatorManual || run?.phase !== "admitted", run?.phase === "admitted" ? protectedPrompt(ctx) : undefined);
|
|
545
549
|
return draft.compaction ? { draft: true, after: draft.after, mode: draft.mode, compaction: draft.compaction, reserve,
|
|
546
|
-
messages:
|
|
550
|
+
messages: plannedView(ctx, { type: "compaction", ...draft.compaction, tokensBefore: before }).session.buildSessionProjection().messages }
|
|
547
551
|
: { draft: false, capacityRejected: Boolean(draft.capacityRejected), reason: draft.unavailable };
|
|
548
552
|
},
|
|
549
553
|
});
|
|
@@ -556,7 +560,7 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
556
560
|
candidate => (candidate.kind === "cut" ? candidate.plan.after : before) + mandatory,
|
|
557
561
|
candidate => projectedRequest(candidate.kind === "cut" ? candidate.plan.messages : saved, ctx, hostInput, [], ctx.getSystemPrompt(), pi.getActiveTools(), candidate.kind === "cut"),
|
|
558
562
|
target => target <= room ? decide() : decide(target, target),
|
|
559
|
-
() => deepestDecision(ctx, operatorManual ||
|
|
563
|
+
() => deepestDecision(ctx, operatorManual || run?.phase !== "admitted", retain));
|
|
560
564
|
}
|
|
561
565
|
if (decision.kind === "cut") { return { compaction: captureDraft(decision.plan.compaction, ctx, decision.plan.reserve, event.reason, event.preparation.tokensBefore,
|
|
562
566
|
retain, operatorManual, event.preparation.previousSummary, { response, host, mandatory, target: decision.target, margin: room - band }) }; }
|
|
@@ -572,38 +576,21 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
572
576
|
}
|
|
573
577
|
});
|
|
574
578
|
pi.on("session_before_tree", (event) => event.preparation.userWantsSummary ? { cancel: true } : undefined);
|
|
575
|
-
pi.on("session_start", () => {
|
|
576
|
-
const historyToolsEnabled = (ctx: ExtensionContext) =>
|
|
579
|
+
pi.on("session_start", () => { resetRun(); recorder.stop("session-transition"); proposalId = undefined; commitFault = undefined; estimatedTokens = 0; identity = ""; factor = 1; });
|
|
580
|
+
const historyToolsEnabled = (ctx: ExtensionContext) => view(ctx).hasTuckCut;
|
|
577
581
|
const syncHistoryTools = (ctx: ExtensionContext) => { if (active()) applyHistoryTools(pi, historyToolsEnabled(ctx)); };
|
|
578
582
|
pi.on("session_start", (_event, ctx) => syncHistoryTools(ctx));
|
|
579
583
|
// A plan decided on `input` is for the branch it was decided on: moving in the tree leaves it planned for another branch, whose kept entry
|
|
580
584
|
// may not be on this one.
|
|
581
|
-
pi.on("session_tree", (_event, ctx) => {
|
|
582
|
-
pi.on("session_compact", (_event, ctx) => { if (
|
|
585
|
+
pi.on("session_tree", (_event, ctx) => { resetRun(); syncHistoryTools(ctx); });
|
|
586
|
+
pi.on("session_compact", (_event, ctx) => { if (run?.phase !== "admitted") syncHistoryTools(ctx); });
|
|
583
587
|
pi.on("turn_start", (_event, ctx) => syncHistoryTools(ctx));
|
|
584
588
|
pi.on("agent_settled", (_event, ctx) => syncHistoryTools(ctx));
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
pi.on("agent_start", () => { lengthStop = false; });
|
|
590
|
-
pi.on("message_end", event => { if (event.message.role === "assistant") lengthStop = event.message.stopReason === "length"; });
|
|
591
|
-
pi.on("agent_settled", (_event, ctx) => {
|
|
592
|
-
if (!lengthStop || !active()) return;
|
|
593
|
-
lengthStop = false;
|
|
594
|
-
const message = "pi-tuck: the last reply stopped at the model's output limit and may be cut off. Ask the agent to continue.";
|
|
595
|
-
try { if (ctx.hasUI === false) process.stderr.write(message + "\n"); else ctx.ui.notify(message, "warning"); } catch { /* ended */ } // with no UI (pi -p), stderr is the operator's
|
|
596
|
-
});
|
|
597
|
-
// The session as it will be once a cut that is not saved yet is saved, and its conversation as the next request carries it.
|
|
598
|
-
const previewCut = (ctx: ExtensionContext, cut: CutEntry) => {
|
|
599
|
-
const header = ctx.sessionManager.getHeader();
|
|
600
|
-
if (!header) throw new Error("Missing native session header for the prompt's cut");
|
|
601
|
-
const preview = SessionManager.inMemory(ctx.cwd, undefined, [header, ...ctx.sessionManager.getBranch()]);
|
|
602
|
-
preview.appendCompaction(cut.summary, cut.firstKeptEntryId, 0, cut.details, true);
|
|
603
|
-
return preview;
|
|
589
|
+
const plannedView = (ctx: ExtensionContext, cut: CutEntry) => {
|
|
590
|
+
const pending = new PendingCut();
|
|
591
|
+
pending.plan(cut, ctx.sessionManager.getBranch().length);
|
|
592
|
+
return pending.view(ctx);
|
|
604
593
|
};
|
|
605
|
-
const cutSession = (ctx: ExtensionContext, cut: CutEntry) => previewCut(ctx, { ...cut, firstKeptEntryId: keptFrom(cut, ctx) });
|
|
606
|
-
const withCut = (ctx: ExtensionContext, cut: CutEntry) => cutSession(ctx, cut).buildSessionProjection().messages;
|
|
607
594
|
// The shared final planner: preserve the compiler's prompt/protocol floor and spend nothing on optional history. Fresh text results
|
|
608
595
|
// get the smallest existing head/tail + recall-locator representation; their originals remain in the session, as for ordinary clips.
|
|
609
596
|
const deepestDecision = (ctx: ExtensionContext, newPrompt: boolean, retain: boolean): Decision<CutPlan> => {
|
|
@@ -621,7 +608,7 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
621
608
|
const middle = Math.floor((low + high) / 2);
|
|
622
609
|
if (clipFor([result], entryIdOf, middle).length) high = middle; else low = middle + 1;
|
|
623
610
|
}
|
|
624
|
-
for (const clip of clipFor([result], entryIdOf, low)) clips.set(clip.toolCallId, clip);
|
|
611
|
+
for (const clip of clipFor([result], entryIdOf, low)) clips().set(clip.toolCallId, clip);
|
|
625
612
|
}
|
|
626
613
|
}
|
|
627
614
|
const before = measureFor(ctx)(saved);
|
|
@@ -629,7 +616,7 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
629
616
|
previousSummary: saved.find(message => message.role === "compactionSummary")?.summary, newPrompt, reRenderSummary: true,
|
|
630
617
|
protectedPromptEntryId: newPrompt ? undefined : protectedPrompt(ctx), deepest: true }, ctx, 0, plannerMeasureFor(ctx), options.recallPolicy, retain);
|
|
631
618
|
if (!draft.compaction) return { kind: "fault", capacity: Boolean(draft.capacityRejected), message: draft.unavailable };
|
|
632
|
-
const messages =
|
|
619
|
+
const messages = plannedView(ctx, { type: "compaction", ...draft.compaction }).session.buildSessionProjection().messages;
|
|
633
620
|
assertNativeToolRounds(messages);
|
|
634
621
|
return { kind: "cut", target: "room", plan: { draft: true, after: measureFor(ctx)(messages), mode: draft.mode,
|
|
635
622
|
compaction: draft.compaction, reserve: 0, messages } };
|
|
@@ -639,8 +626,8 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
639
626
|
// keepRecentTokens, and where those are the whole conversation it has nothing to cut, so every later prompt was refused for good (t06 at
|
|
640
627
|
// Pi's defaults on a 32k window). Each request of the run carries the cut, and the run's first boundary saves it.
|
|
641
628
|
const preflight = (ctx: ExtensionContext, incomingPart: number, incomingMessages: Messages, observedPrompt: string, tools: readonly string[], hostSnapshot?: Messages) => {
|
|
642
|
-
|
|
643
|
-
|
|
629
|
+
run!.cut.clear();
|
|
630
|
+
const branchLength = ctx.sessionManager.getBranch().length;
|
|
644
631
|
if (!active()) return;
|
|
645
632
|
access();
|
|
646
633
|
if (!settingsFor().getCompactionSettings(ctx.model).enabled) return;
|
|
@@ -670,7 +657,9 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
670
657
|
// there buys nothing the request needed and costs history the next recall would have found.
|
|
671
658
|
const draft = tuckObservedDraft(tuck, { branchEntries: ctx.sessionManager.getBranch(), tokensBefore: before, previousSummary, newPrompt: true, reRenderSummary: !opportunistic }, ctx, reserve, plannerMeasureFor(ctx), options.recallPolicy, false);
|
|
672
659
|
if (!draft.compaction) return { draft: false, capacityRejected: Boolean(draft.capacityRejected), reason: draft.unavailable };
|
|
673
|
-
const
|
|
660
|
+
const candidate = new PendingCut();
|
|
661
|
+
candidate.plan({ type: "compaction", ...draft.compaction, tokensBefore: before }, branchLength);
|
|
662
|
+
const messages = candidate.view(ctx).session.buildSessionProjection().messages;
|
|
674
663
|
assertNativeToolRounds(messages);
|
|
675
664
|
return { draft: true, after: measure(messages), mode: draft.mode, compaction: draft.compaction, reserve, messages };
|
|
676
665
|
},
|
|
@@ -692,14 +681,18 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
692
681
|
if (decision.kind === "fault") throw new (decision.capacity ? CapacityError : Refusal)(decision.message);
|
|
693
682
|
const compaction = captureDraft(decision.plan.compaction, ctx, decision.plan.reserve, "prompt", before, false, false, previousSummary,
|
|
694
683
|
{ response, host, mandatory, target: decision.target, margin: room - band });
|
|
695
|
-
|
|
684
|
+
run!.cut.plan({ type: "compaction", summary: compaction.summary, firstKeptEntryId: compaction.firstKeptEntryId, tokensBefore: before, details: compaction.details }, branchLength);
|
|
696
685
|
};
|
|
697
686
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
698
687
|
// A decided plan is for the prompt `input` saw. A run that a custom message starts is not that run (another extension handled the
|
|
699
688
|
// input, so no run started for it): plan afresh.
|
|
700
|
-
const prompted = phase === "decided" && event.messageType !== "custom";
|
|
701
|
-
phase = "starting";
|
|
702
|
-
|
|
689
|
+
const prompted = run?.phase === "decided" && event.messageType !== "custom";
|
|
690
|
+
if (!prompted && run?.phase !== "failed") run = createRun("starting");
|
|
691
|
+
run ??= createRun("starting");
|
|
692
|
+
run.claimed = true;
|
|
693
|
+
run.phase = "starting";
|
|
694
|
+
const prompt = event.messageType === "custom" ? textOf(event.message.content) : event.prompt;
|
|
695
|
+
run.prompt = prompt ? new StartingPrompt(prompt, ctx.sessionManager.getBranch().length, event.messageType) : undefined;
|
|
703
696
|
try {
|
|
704
697
|
syncHistoryTools(ctx);
|
|
705
698
|
if (prompted) access(); // decided on input; a failure latched there still stops the run
|
|
@@ -719,7 +712,7 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
719
712
|
// handlers have run. Pi's prepareRequest has not yet snapshotted executable tools. A first cut discovered in context_with_system
|
|
720
713
|
// would be too late for that snapshot, even if its declarations were repaired there, so make the late prompt decision here.
|
|
721
714
|
pi.on("message_end", (event, ctx) => {
|
|
722
|
-
if (event.message.role !== "user" || !active() ||
|
|
715
|
+
if (event.message.role !== "user" || !active() || run?.phase !== "admitted" || view(ctx).pending) return;
|
|
723
716
|
try {
|
|
724
717
|
access();
|
|
725
718
|
if (!ctx.model) throw missingModel();
|
|
@@ -804,11 +797,7 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
804
797
|
}
|
|
805
798
|
return { draft: false, capacityRejected: Boolean(draft.capacityRejected), reason: draft.unavailable };
|
|
806
799
|
}
|
|
807
|
-
const
|
|
808
|
-
if (!header) throw new Error("Missing native session header at tool-loop boundary");
|
|
809
|
-
const preview = SessionManager.inMemory(ctx.cwd, undefined, [header, ...ctx.sessionManager.getBranch()]);
|
|
810
|
-
preview.appendCompaction(draft.compaction.summary, draft.compaction.firstKeptEntryId, before, draft.compaction.details, true);
|
|
811
|
-
const messages = preview.buildSessionProjection().messages;
|
|
800
|
+
const messages = plannedView(ctx, { type: "compaction", ...draft.compaction, tokensBefore: before }).session.buildSessionProjection().messages;
|
|
812
801
|
assertNativeToolRounds(messages);
|
|
813
802
|
previews.push(messages);
|
|
814
803
|
return { draft: true, after: measure(messages), mode: draft.mode, compaction: draft.compaction, reserve, messages };
|
|
@@ -831,9 +820,9 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
831
820
|
for (const budget of clipBudgets(clipRoom, factor, mandatory, kept)) {
|
|
832
821
|
const round = clipFor(event.toolResults, entryIdOf, budget);
|
|
833
822
|
if (!round.length) continue;
|
|
834
|
-
for (const clip of round) clips.set(clip.toolCallId, clip);
|
|
823
|
+
for (const clip of round) run!.clips.set(clip.toolCallId, clip);
|
|
835
824
|
const clippedDecision = decide(clipRoom, targetBand, required);
|
|
836
|
-
if (clippedDecision.kind === "fault") for (const clip of round) clips.delete(clip.toolCallId);
|
|
825
|
+
if (clippedDecision.kind === "fault") for (const clip of round) run!.clips.delete(clip.toolCallId);
|
|
837
826
|
else { decision = clippedDecision; break; }
|
|
838
827
|
}
|
|
839
828
|
};
|
|
@@ -869,14 +858,14 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
869
858
|
// the session as it is with that cut: a fresh result can still need a cut of its own there.
|
|
870
859
|
type Boundary = Parameters<typeof boundaryCut>[0];
|
|
871
860
|
const atBoundary = (event: Boundary, ctx: ExtensionContext, reason: "tool-loop" | "run-end", decide: boolean) => {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
const preview =
|
|
861
|
+
const current = view(ctx);
|
|
862
|
+
if (!current.pending) return decide ? boundaryCut(event, ctx, reason) : undefined;
|
|
863
|
+
const entry = run!.cut.take()!;
|
|
864
|
+
const preview = current.session, context: ExtensionContext = Object.create(ctx, { sessionManager: { value: preview } });
|
|
876
865
|
const more = decide && !event.entries.length
|
|
877
|
-
? boundaryCut({ ...event, context: { ...event.context, contextMessages: preview.buildSessionProjection().messages } },
|
|
866
|
+
? boundaryCut({ ...event, context: { ...event.context, contextMessages: preview.buildSessionProjection().messages } }, context, reason) : undefined;
|
|
878
867
|
const { tokensBefore: _, ...cut } = entry;
|
|
879
|
-
const saved = { ...cut, firstKeptEntryId:
|
|
868
|
+
const saved = { ...cut, firstKeptEntryId: current.keptFromId! };
|
|
880
869
|
// As for a cut Pi makes (session_compact): the history the cut leaves behind is indexed, once Pi has committed the entries.
|
|
881
870
|
setImmediate(() => { semantic.indexAtCut(semanticProvider(ctx)); });
|
|
882
871
|
return { entries: [...event.entries as CutEntry[], saved, ...(more?.entries ?? [])] };
|
|
@@ -886,44 +875,36 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
886
875
|
// Pi's later native length recovery from retrying it independently. The stored message and its output usage stay on the branch.
|
|
887
876
|
const entries: SessionBoundaryDraft[] = [...(atBoundary(event, ctx, "run-end", false)?.entries ?? event.entries),
|
|
888
877
|
{ type: "context_edit", targetId: event.messageEntryId, replacement: null }];
|
|
889
|
-
if (thinkingRetryPending) return { entries, continue: false };
|
|
890
|
-
const
|
|
891
|
-
if (!header) throw new Error("Missing native session header for the thinking-only retry");
|
|
892
|
-
const preview = SessionManager.inMemory(ctx.cwd, undefined, [header, ...ctx.sessionManager.getBranch()]);
|
|
893
|
-
for (const entry of entries) {
|
|
894
|
-
if (entry.type === "context_edit") preview.appendContextEdit(entry.targetId, entry.replacement);
|
|
895
|
-
else if (entry.type === "compaction") preview.appendCompaction(entry.summary, entry.firstKeptEntryId, 0, entry.details, true, entry.usage);
|
|
896
|
-
else if (entry.type === "custom") preview.appendCustomEntry(entry.customType, entry.data);
|
|
897
|
-
else preview.appendCustomMessageEntry(entry.customType, entry.content, entry.display, entry.details);
|
|
898
|
-
}
|
|
878
|
+
if (run!.thinkingRetryPending) return { entries, continue: false };
|
|
879
|
+
const preview = liveCut.view(ctx, entries).session;
|
|
899
880
|
const view: ExtensionContext = Object.create(ctx, { sessionManager: { value: preview } });
|
|
900
881
|
const hostInput = hostMessages(ctx), host = conversationTokens(hostInput);
|
|
901
882
|
// The deepest cut only when it buys reply room: where Pi's cap on the request as it stands (the stopped reply left out) is already
|
|
902
883
|
// as large as the deepest cut's, the turn is retried uncut, and no history is given up for nothing.
|
|
903
884
|
const asIs = projectedRequest(preview.buildSessionProjection().messages, view, hostInput, event.context.pendingMessages, ctx.getSystemPrompt(), pi.getActiveTools(), false);
|
|
904
885
|
const asIsCheck = requestClamp(asIs, view);
|
|
905
|
-
const previousClips = new Map(clips);
|
|
886
|
+
const previousClips = new Map(clips());
|
|
906
887
|
const decision = deepestDecision(view, false, true);
|
|
907
888
|
if (decision.kind === "cut" && asIsCheck.cap !== undefined && asIsCheck.leavesFloor &&
|
|
908
889
|
requestClamp(projectedRequest(decision.plan.messages, view, hostInput, event.context.pendingMessages, ctx.getSystemPrompt(), pi.getActiveTools(), true), view).cap! <= asIsCheck.cap) {
|
|
909
|
-
clips.clear(); for (const [id, clip] of previousClips) clips.set(id, clip);
|
|
890
|
+
clips().clear(); for (const [id, clip] of previousClips) clips().set(id, clip);
|
|
910
891
|
const { room, hardLine } = limits(view);
|
|
911
|
-
thinkingRetryPending = true;
|
|
892
|
+
run!.thinkingRetryPending = true;
|
|
912
893
|
return { entries: [...entries, { type: "custom" as const, customType: "pi-tuck-capacity", data: { kind: "retry", stoppedReplyEntryId: event.messageEntryId,
|
|
913
894
|
stoppedOutputTokens: event.message.role === "assistant" ? event.message.usage?.output ?? 0 : 0, cut: false,
|
|
914
895
|
piEstimateTokens: asIsCheck.estimate, piCap: asIsCheck.cap, room, hardLine, buildId: BUILD_ID } }], continue: true };
|
|
915
896
|
}
|
|
916
897
|
if (decision.kind !== "cut") throw new CapacityError(clampMessage);
|
|
917
898
|
const messages = projectedRequest(decision.plan.messages, view, hostInput, event.context.pendingMessages, ctx.getSystemPrompt(), pi.getActiveTools(), true);
|
|
918
|
-
if (
|
|
899
|
+
if (run?.prompt && !carriesPrompt(messages, run.prompt.text(ctx.sessionManager.getBranch()), run.prompt.role)) throw promptRefusal();
|
|
919
900
|
const system = requestSystemPart(ctx.getSystemPrompt(), pi.getActiveTools(), messages, ctx.getSystemPrompt());
|
|
920
901
|
const tokens = system + conversationTokens(messages), check = requestClamp(messages, view);
|
|
921
902
|
const { response, room, hardLine } = limits(view);
|
|
922
903
|
if (!fits(tokens, hardLine, view) || check.cap === undefined || !check.leavesFloor) throw new CapacityError(clampMessage);
|
|
923
904
|
const compaction = captureDraft(decision.plan.compaction, view, decision.plan.reserve, "length-retry", tokens, true, false, undefined,
|
|
924
905
|
{ response, host, mandatory: system + host + conversationTokens(event.context.pendingMessages), target: decision.target, margin: room - decisionBand(view, room, system) });
|
|
925
|
-
thinkingRetryPending = true;
|
|
926
|
-
pendingBoundaryCommit = true;
|
|
906
|
+
run!.thinkingRetryPending = true;
|
|
907
|
+
run!.pendingBoundaryCommit = true;
|
|
927
908
|
return { entries: [...entries, { type: "compaction" as const, summary: compaction.summary, firstKeptEntryId: compaction.firstKeptEntryId || null, details: compaction.details },
|
|
928
909
|
{ type: "custom" as const, customType: "pi-tuck-capacity", data: { kind: "retry", stoppedReplyEntryId: event.messageEntryId,
|
|
929
910
|
stoppedOutputTokens: event.message.role === "assistant" ? event.message.usage?.output ?? 0 : 0, cut: true,
|
|
@@ -931,16 +912,16 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
931
912
|
};
|
|
932
913
|
pi.on("turn_end", (event, ctx) => {
|
|
933
914
|
try {
|
|
934
|
-
if (!active() ||
|
|
935
|
-
if (thinkingOnlyLength(event.message) && !failure && !ctx.signal?.aborted && settingsFor().getCompactionSettings(ctx.model).enabled)
|
|
915
|
+
if (!active() || run?.phase !== "admitted") return;
|
|
916
|
+
if (thinkingOnlyLength(event.message) && !failure() && !ctx.signal?.aborted && settingsFor().getCompactionSettings(ctx.model).enabled)
|
|
936
917
|
return retryThinking(event, ctx);
|
|
937
|
-
thinkingRetryPending = false;
|
|
918
|
+
run.thinkingRetryPending = false;
|
|
938
919
|
const decide = event.outcome === "completed" && !ctx.signal?.aborted && event.context.canContinue && event.toolResults.length > 0 &&
|
|
939
920
|
settingsFor().getCompactionSettings(ctx.model).enabled;
|
|
940
|
-
if (!
|
|
921
|
+
if (!view(ctx).pending && !decide) return;
|
|
941
922
|
const result = atBoundary(event, ctx, "tool-loop", decide);
|
|
942
923
|
if (result) {
|
|
943
|
-
pendingBoundaryCommit = true;
|
|
924
|
+
run!.pendingBoundaryCommit = true;
|
|
944
925
|
}
|
|
945
926
|
return result;
|
|
946
927
|
} catch (error) { fail(error, ctx); }
|
|
@@ -956,21 +937,22 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
956
937
|
// A request of a run whose prompt's cut is not saved yet: the saved conversation in it, which other handlers may surround, is replaced by
|
|
957
938
|
// the conversation with the cut applied. Pi sends the saved system message apart from the request's messages, so neither side carries it
|
|
958
939
|
// here. A request that does not carry the saved conversation as it stands cannot take the cut.
|
|
959
|
-
const cutRequest = <M,>(messages: M[], ctx: ExtensionContext
|
|
940
|
+
const cutRequest = <M,>(messages: M[], ctx: ExtensionContext): M[] => {
|
|
960
941
|
const conversation = (all: Messages) => all.filter(message => message.role !== "system") as unknown[];
|
|
961
942
|
const saved = conversation(ctx.sessionManager.buildSessionProjection().messages);
|
|
962
943
|
for (let at = 0; at + saved.length <= messages.length; at++)
|
|
963
944
|
if (saved.every((message, i) => isDeepStrictEqual(messages[at + i], message)))
|
|
964
|
-
return [...messages.slice(0, at), ...conversation(
|
|
945
|
+
return [...messages.slice(0, at), ...conversation(view(ctx).session.buildSessionProjection().messages) as M[], ...messages.slice(at + saved.length)];
|
|
965
946
|
throw new Error("The prompt's cut cannot be applied: the request does not carry the saved conversation");
|
|
966
947
|
};
|
|
967
948
|
pi.on("context", (event, ctx) => {
|
|
968
949
|
try {
|
|
969
950
|
if (!active()) return;
|
|
951
|
+
run?.prompt?.id(ctx.sessionManager.getBranch());
|
|
970
952
|
// Before a cut, Pi owns the whole request, including its object identities and system-message serialization.
|
|
971
|
-
if (!historyToolsEnabled(ctx)) return clips.size ? { messages: applyClips(event.messages, clips) } : undefined;
|
|
953
|
+
if (!historyToolsEnabled(ctx)) return clips().size ? { messages: applyClips(event.messages, clips()) } : undefined;
|
|
972
954
|
access();
|
|
973
|
-
return { messages: applyClips(projectedMessages(
|
|
955
|
+
return { messages: applyClips(projectedMessages(view(ctx).pending ? cutRequest(event.messages, ctx) : event.messages, ctx, options.recallPolicy), clips()) };
|
|
974
956
|
} catch (error) { try { fail(error, ctx); } finally { return historyToolsEnabled(ctx) ? { messages: [] } : undefined; } }
|
|
975
957
|
});
|
|
976
958
|
pi.on("session_compact_failed", (event, ctx) => {
|
|
@@ -988,21 +970,21 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
988
970
|
if (active() && !event.aborted) fail(new (event.fromExtension ? CommitFault : Error)(event.errorMessage ?? "Compaction failed"), ctx);
|
|
989
971
|
});
|
|
990
972
|
pi.on("agent_before_settle", (event, ctx) => {
|
|
991
|
-
if (pendingBoundaryCommit) {
|
|
973
|
+
if (run?.pendingBoundaryCommit) {
|
|
992
974
|
if (event.outcome === "error") return void fail(new CommitFault("Native tool-loop boundary draft remained unconfirmed at error settlement"), ctx);
|
|
993
|
-
pendingBoundaryCommit = false;
|
|
975
|
+
if (run) run.pendingBoundaryCommit = false;
|
|
994
976
|
}
|
|
995
977
|
// Pi commits these entries before the run settles.
|
|
996
978
|
try {
|
|
997
|
-
if (!active() ||
|
|
979
|
+
if (!active() || run?.phase !== "admitted" || failure() || !settingsFor().getCompactionSettings(ctx.model).enabled) return;
|
|
998
980
|
const decide = event.outcome === "completed" && !ctx.signal?.aborted;
|
|
999
|
-
if (
|
|
981
|
+
if (view(ctx).pending ? event.outcome === "error" : !decide) return;
|
|
1000
982
|
return atBoundary(event, ctx, "run-end", decide);
|
|
1001
983
|
} catch (error) { fail(error, ctx); }
|
|
1002
984
|
});
|
|
1003
985
|
pi.on("context_with_system", (event, ctx) => {
|
|
1004
986
|
// Reaching the next request boundary proves native commit returned, not durable storage.
|
|
1005
|
-
pendingBoundaryCommit = false;
|
|
987
|
+
if (run) run.pendingBoundaryCommit = false;
|
|
1006
988
|
pending = undefined;
|
|
1007
989
|
try {
|
|
1008
990
|
if (!active()) return;
|
|
@@ -1019,7 +1001,7 @@ export async function installCompaction(pi: ExtensionAPI, options: CompactionOpt
|
|
|
1019
1001
|
observeBudget(tokens, ctx);
|
|
1020
1002
|
estimatedTokens = corrected(tokens, ctx);
|
|
1021
1003
|
const { room, hardLine } = limits(ctx);
|
|
1022
|
-
if (
|
|
1004
|
+
if (run?.prompt && !carriesPrompt(messages, run.prompt.text(ctx.sessionManager.getBranch()), run.prompt.role))
|
|
1023
1005
|
throw new Refusal("Prompt check failed: the request is missing the whole text of the prompt that started this run. Shorten the prompt or choose a model with a larger context window, then retry");
|
|
1024
1006
|
if (!fits(tokens, hardLine, ctx))
|
|
1025
1007
|
throw new CapacityError("Complete Pi context and response reserve exceed the estimated request budget");
|