pi-posthorse 0.10.13 → 0.10.15
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/package.json +1 -1
- package/src/budget.ts +6 -14
- package/src/history-pages.ts +3 -7
- package/src/history.ts +1 -2
- package/src/notes.ts +4 -9
- package/src/paging.ts +18 -32
- package/src/reminders.ts +1 -3
- package/src/rollover.ts +8 -20
package/package.json
CHANGED
package/src/budget.ts
CHANGED
|
@@ -13,13 +13,6 @@ import {
|
|
|
13
13
|
import { imagesOf, textOf } from "./message-text.ts";
|
|
14
14
|
|
|
15
15
|
const MIN_USABLE_TOKENS = Math.ceil(MAX_HANDOFF_CHARS / 4) * 2;
|
|
16
|
-
export const CHECKPOINT_GUIDANCE = `Keep working on the assigned task; Pi handles context rollover automatically.
|
|
17
|
-
|
|
18
|
-
The current context window is not a deadline for finishing your task. Do not reduce scope, skip prescribed steps or verification, or issue a final progress summary because rollover is approaching.
|
|
19
|
-
|
|
20
|
-
Update the task's current-state note if needed: goal, progress, decisions, and next steps. Use available file-editing tools at its absolute path; use notes write only for creation, substantial restructuring, or when editing tools are unavailable. Leave an already-current note unchanged. Then immediately execute the next prescribed task step in this run. No new_context call, waiting for rollover, or user "continue" is needed.
|
|
21
|
-
|
|
22
|
-
Finish when the requested work and required verification are complete, or report a genuine blocker. Approaching context rollover is not a blocker.`;
|
|
23
16
|
export type Budget = {
|
|
24
17
|
readonly contextWindow: number;
|
|
25
18
|
readonly reserveTokens: number;
|
|
@@ -84,8 +77,9 @@ function automaticGuidance(budget: Budget | undefined, enabled: boolean): string
|
|
|
84
77
|
const line =
|
|
85
78
|
budget === undefined
|
|
86
79
|
? ""
|
|
87
|
-
: `
|
|
88
|
-
return
|
|
80
|
+
: `The rollover line is ${budget.rolloverAt.toLocaleString("en-US")} tokens used. `;
|
|
81
|
+
return `Pi rolls context over automatically, so the context window is not a deadline. Do not reduce scope, skip steps or verification, or write a final summary because rollover is near, and do not wait for the user to say "continue". Finish when the requested work and its verification are complete, or report a genuine blocker.
|
|
82
|
+
${line}Rollover keeps a recent-message tail without summarizing. One task-note reminder may arrive at the end of a working turn near the line: update the note only if it is stale, then continue with the next step in the same run. new_context is an optional clean reset.`;
|
|
89
83
|
}
|
|
90
84
|
export function buildGuidance(ctx: PolicyContext): string {
|
|
91
85
|
const budget = budgetFor(ctx);
|
|
@@ -93,14 +87,12 @@ export function buildGuidance(ctx: PolicyContext): string {
|
|
|
93
87
|
const capacity =
|
|
94
88
|
budget === undefined
|
|
95
89
|
? "Configured context capacity is unknown."
|
|
96
|
-
: `Configured context capacity: ${budget.contextWindow.toLocaleString("en-US")} tokens
|
|
90
|
+
: `Configured context capacity: ${budget.contextWindow.toLocaleString("en-US")} tokens (not remaining space).`;
|
|
97
91
|
return `## Automatic context management (Posthorse)
|
|
98
92
|
${automaticGuidance(budget, enabled)}
|
|
99
|
-
${capacity}
|
|
100
|
-
Do not guess context capacity or remaining space from response-token limits or reasoning budgets. Before reporting a remaining-token count or changing your work plan because of context limits, call get_context_remaining. If usage is unknown, report it as unknown. No routine budget checks are needed.
|
|
93
|
+
${capacity} Do not guess capacity or remaining space from response-token or reasoning limits. Before reporting a remaining-token count or changing your work plan because of context limits, call get_context_remaining. If usage is unknown, report it as unknown. No routine budget checks are needed.
|
|
101
94
|
After a rollover, earlier conversation stays in history. Restore notes and todos, then verify live state before stateful or external work; automatic handoffs record inputs, not progress.
|
|
102
|
-
Keep one concise current-state note per task. Preserve decisions and safety constraints
|
|
103
|
-
Write handoffs and notes as normal readable prose; notes and history search match literal text.`;
|
|
95
|
+
Keep one concise current-state note per task: goal, progress, decisions, and next steps. Preserve decisions and safety constraints, link fuller evidence instead of copying it, and edit only changed sections.`;
|
|
104
96
|
}
|
|
105
97
|
/** ponytail: native estimates omit request-local guidance; reserve it until post-transform usage is exposed. */
|
|
106
98
|
export function guidanceTokens(ctx: PolicyContext): number {
|
package/src/history-pages.ts
CHANGED
|
@@ -84,13 +84,13 @@ export function searchPage(
|
|
|
84
84
|
host: PolicyContext,
|
|
85
85
|
pages: PageAccess,
|
|
86
86
|
): TextResult {
|
|
87
|
-
const chars = pages.size(host
|
|
87
|
+
const chars = pages.size(host);
|
|
88
88
|
const reserve = Math.max(
|
|
89
89
|
0,
|
|
90
90
|
...matches.hits.map((hit) => searchFooter(cursorFor(matches, hit, hit.text.length)).length),
|
|
91
91
|
);
|
|
92
92
|
if (reserve >= chars) {
|
|
93
|
-
pages.error(host, "History entry id is too large for pagination."
|
|
93
|
+
pages.error(host, "History entry id is too large for pagination.");
|
|
94
94
|
}
|
|
95
95
|
const skippedNote =
|
|
96
96
|
matches.skipped > 0 && (params.cursor === undefined || params.cursor === "")
|
|
@@ -146,11 +146,7 @@ export function historyReadPage(
|
|
|
146
146
|
const shownId = shownEntryId(item, params.id, source);
|
|
147
147
|
const { offset, imageOffset } = readOffsets(item, params);
|
|
148
148
|
const firstImage = imageOffset < item.images.length ? 1 : 0;
|
|
149
|
-
const chars = access.pages.size(access.host, {
|
|
150
|
-
offset,
|
|
151
|
-
imageCount: firstImage,
|
|
152
|
-
imageOffset: item.images.length > 0 ? imageOffset : undefined,
|
|
153
|
-
});
|
|
149
|
+
const chars = access.pages.size(access.host, { imageCount: firstImage });
|
|
154
150
|
const imageEnd = Math.min(
|
|
155
151
|
item.images.length,
|
|
156
152
|
imageOffset + firstImage + Math.floor((chars - MIN_PAGE_CHARS) / ESTIMATED_IMAGE_CHARS),
|
package/src/history.ts
CHANGED
|
@@ -130,8 +130,7 @@ export function registerHistory(
|
|
|
130
130
|
async execute(...[, params, signal, , ctx]) {
|
|
131
131
|
const source = historySource(ctx);
|
|
132
132
|
if (params.op === "search") {
|
|
133
|
-
const fail = (message: string): never =>
|
|
134
|
-
pages.error(policy(ctx), message, params.cursor);
|
|
133
|
+
const fail = (message: string): never => pages.error(policy(ctx), message);
|
|
135
134
|
const matches = await historyMatches(source, params, signal, fail);
|
|
136
135
|
return searchPage(matches, params, policy(ctx), pages);
|
|
137
136
|
}
|
package/src/notes.ts
CHANGED
|
@@ -115,15 +115,10 @@ async function readNote(
|
|
|
115
115
|
const header = target.writable
|
|
116
116
|
? `File: ${path}\n`
|
|
117
117
|
: `Read-only live reference: notes read ${JSON.stringify({ scope: scopeDirectory(target.address), path: target.address.path })}\n`;
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
host,
|
|
123
|
-
`Too little context remains to include the note path. Call new_context first, then retry with offset ${offset}.`,
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
const end = Math.min(text.length, offset + chars);
|
|
118
|
+
const end = Math.min(
|
|
119
|
+
text.length,
|
|
120
|
+
offset + pages.size(access.host(), { headerChars: header.length }),
|
|
121
|
+
);
|
|
127
122
|
const more =
|
|
128
123
|
end < text.length
|
|
129
124
|
? `\n[chars ${offset}-${end} of ${text.length}; continue with offset ${end}]`
|
package/src/paging.ts
CHANGED
|
@@ -11,11 +11,10 @@ import {
|
|
|
11
11
|
type TextResult,
|
|
12
12
|
} from "./contracts.ts";
|
|
13
13
|
|
|
14
|
+
/** What else the page carries: images share the budget, and the header comes out of the body. */
|
|
14
15
|
export type PageRequest = {
|
|
15
|
-
readonly offset: number;
|
|
16
16
|
readonly imageCount?: number;
|
|
17
|
-
readonly
|
|
18
|
-
readonly imageOffset?: number | undefined;
|
|
17
|
+
readonly headerChars?: number;
|
|
19
18
|
};
|
|
20
19
|
export type NotesPage = {
|
|
21
20
|
readonly rows: readonly string[];
|
|
@@ -27,13 +26,13 @@ export type NotesPage = {
|
|
|
27
26
|
readonly more?: boolean;
|
|
28
27
|
};
|
|
29
28
|
export type PageAccess = {
|
|
30
|
-
readonly size: (ctx: PolicyContext, request
|
|
29
|
+
readonly size: (ctx: PolicyContext, request?: PageRequest) => number;
|
|
31
30
|
readonly result: (
|
|
32
31
|
text: string,
|
|
33
32
|
images: readonly ImageLike[],
|
|
34
33
|
display: PosthorseDisplay,
|
|
35
34
|
) => TextResult;
|
|
36
|
-
readonly error: (ctx: PolicyContext, message: string
|
|
35
|
+
readonly error: (ctx: PolicyContext, message: string) => never;
|
|
37
36
|
readonly notes: (ctx: PolicyContext, page: NotesPage) => TextResult;
|
|
38
37
|
};
|
|
39
38
|
type PageAccounting = {
|
|
@@ -58,18 +57,22 @@ export class PageReservations {
|
|
|
58
57
|
reset(): void {
|
|
59
58
|
this.accounting = { pendingTokens: 0, usage: undefined };
|
|
60
59
|
}
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Body characters for the next page. Near the rollover line pages shrink to MIN_PAGE_CHARS
|
|
62
|
+
* instead of refusing, so reading keeps making progress; Pi's rollover handles the overshoot.
|
|
63
|
+
*/
|
|
64
|
+
size(ctx: PolicyContext, request: PageRequest = {}): number {
|
|
65
|
+
this.settle(ctx);
|
|
63
66
|
const remaining = pageCapacity(ctx, this.toolTokens) - this.accounting.pendingTokens;
|
|
64
|
-
const
|
|
67
|
+
const budget = Math.min(
|
|
65
68
|
MAX_PAGE_CHARS,
|
|
66
69
|
Math.max(0, remaining - PAGE_MARGIN_TOKENS) * 4 -
|
|
67
70
|
(request.imageCount ?? 0) * ESTIMATED_IMAGE_CHARS,
|
|
68
71
|
);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
return Math.max(MIN_PAGE_CHARS, budget - (request.headerChars ?? 0));
|
|
73
|
+
}
|
|
74
|
+
private settle(ctx: PolicyContext): void {
|
|
75
|
+
this.accounting = reconcile(this.accounting, ctx.getContextUsage()?.tokens);
|
|
73
76
|
}
|
|
74
77
|
private reserve(text: string, imageCount = 0): void {
|
|
75
78
|
this.accounting = {
|
|
@@ -80,38 +83,21 @@ export class PageReservations {
|
|
|
80
83
|
imageCount * (ESTIMATED_IMAGE_CHARS / 4),
|
|
81
84
|
};
|
|
82
85
|
}
|
|
83
|
-
private refuse(remaining: number, request: PageRequest): never {
|
|
84
|
-
const position =
|
|
85
|
-
request.cursor !== undefined && request.cursor !== ""
|
|
86
|
-
? "with the same cursor"
|
|
87
|
-
: `with offset ${request.offset}${request.imageOffset === undefined ? "" : ` and imageOffset ${request.imageOffset}`}`;
|
|
88
|
-
const message = `Too little context remains to read a page safely. Call new_context first, then retry ${position}.`;
|
|
89
|
-
// Even refusals consume context; omit repeated guidance when it no longer fits.
|
|
90
|
-
const text = Math.ceil(message.length / 4) <= remaining ? message : "";
|
|
91
|
-
this.reserve(text);
|
|
92
|
-
throw new Error(text);
|
|
93
|
-
}
|
|
94
86
|
result(text: string, images: readonly ImageLike[], display: PosthorseDisplay): TextResult {
|
|
95
87
|
this.reserve(text, images.length);
|
|
96
88
|
return textResult(text, images, display);
|
|
97
89
|
}
|
|
98
|
-
error(ctx: PolicyContext, message: string
|
|
99
|
-
this.
|
|
90
|
+
error(ctx: PolicyContext, message: string): never {
|
|
91
|
+
this.settle(ctx);
|
|
100
92
|
this.reserve(message);
|
|
101
93
|
throw new Error(message);
|
|
102
94
|
}
|
|
103
95
|
notes(ctx: PolicyContext, page: NotesPage): TextResult {
|
|
104
96
|
const { rows, offset, kind, empty, header = "", more } = page;
|
|
105
97
|
const text = rows.length > 0 ? rows.join("\n") : empty;
|
|
106
|
-
const chars = this.size(ctx, {
|
|
98
|
+
const chars = this.size(ctx, { headerChars: header.length });
|
|
107
99
|
const footer = (end: number) =>
|
|
108
100
|
`\n[chars ${offset}-${end}${more === true ? "; more matches follow" : ` of ${text.length}`}; continue with offset ${end}]`;
|
|
109
|
-
if (chars <= footer(text.length).length) {
|
|
110
|
-
this.error(
|
|
111
|
-
ctx,
|
|
112
|
-
`Too little context remains to include the notes path. Call new_context first, then retry with offset ${offset}.`,
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
101
|
if (offset > 0 && offset >= text.length) {
|
|
116
102
|
this.error(ctx, "Offset is past the end; restart with offset 0.");
|
|
117
103
|
}
|
package/src/reminders.ts
CHANGED
|
@@ -231,10 +231,8 @@ function countRetainedFromProjected(
|
|
|
231
231
|
continue;
|
|
232
232
|
}
|
|
233
233
|
for (const msg of item.messages) {
|
|
234
|
-
const role = msg.role ?? "";
|
|
235
234
|
if (
|
|
236
|
-
role
|
|
237
|
-
!stale({ role, customType: msg.customType, details: msg.details })
|
|
235
|
+
!stale({ role: msg.role ?? "", customType: msg.customType, details: msg.details })
|
|
238
236
|
) {
|
|
239
237
|
count += 1;
|
|
240
238
|
}
|
package/src/rollover.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
SessionBeforeCompactEvent,
|
|
7
7
|
TurnEndEvent,
|
|
8
8
|
} from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { budgetFor,
|
|
9
|
+
import { budgetFor, freshPayloadChars } from "./budget.ts";
|
|
10
10
|
import {
|
|
11
11
|
isRecord,
|
|
12
12
|
isReminderType,
|
|
@@ -113,22 +113,6 @@ function staleReminderPredicate(
|
|
|
113
113
|
isReminderType(message.customType) &&
|
|
114
114
|
(!enabled || !reminderMatches(message.details, fingerprint));
|
|
115
115
|
}
|
|
116
|
-
function retainedTailEnd(
|
|
117
|
-
messages: ContextWithSystemEvent["messages"],
|
|
118
|
-
start: number,
|
|
119
|
-
count: number,
|
|
120
|
-
): number | undefined {
|
|
121
|
-
let remaining = count;
|
|
122
|
-
for (let index = start; index < messages.length; index += 1) {
|
|
123
|
-
if (messages[index]?.role !== "system") {
|
|
124
|
-
remaining -= 1;
|
|
125
|
-
}
|
|
126
|
-
if (remaining === 0) {
|
|
127
|
-
return index + 1;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
116
|
function chronologicalRetainedTail(
|
|
133
117
|
messages: ContextWithSystemEvent["messages"],
|
|
134
118
|
ctx: ExtensionContext,
|
|
@@ -137,9 +121,12 @@ function chronologicalRetainedTail(
|
|
|
137
121
|
const summaryIndex = messages.findIndex((message) => message.role !== "system");
|
|
138
122
|
const first = messages[summaryIndex];
|
|
139
123
|
if (first?.role === "compactionSummary" && hasRetainedRolloverMarker(first.summary)) {
|
|
124
|
+
// Pi projects no system messages into the retained tail, but an earlier handler can
|
|
125
|
+
// present a retained message as one (pi-change-working-dir's directory snapshots).
|
|
126
|
+
// Count positions, not roles, so the record never lands inside the next tool batch.
|
|
140
127
|
const count = countRetainedMessages(ctx, stale);
|
|
141
|
-
const end =
|
|
142
|
-
if (end
|
|
128
|
+
const end = summaryIndex + 1 + count;
|
|
129
|
+
if (count > 0 && end <= messages.length) {
|
|
143
130
|
const prefix = messages.slice(0, summaryIndex);
|
|
144
131
|
const retainedSlice = messages.slice(summaryIndex + 1, end);
|
|
145
132
|
const afterSlice = messages.slice(end);
|
|
@@ -457,7 +444,8 @@ export function registerRollover(
|
|
|
457
444
|
pi.sendMessage(
|
|
458
445
|
{
|
|
459
446
|
customType: REMINDER_TYPE,
|
|
460
|
-
content:
|
|
447
|
+
content:
|
|
448
|
+
"[posthorse] Routine task-note update. Update the task's current-state note if it is stale, then continue with the next step in this run.",
|
|
461
449
|
display: true,
|
|
462
450
|
details: reminder.fingerprint,
|
|
463
451
|
},
|