pi-posthorse 0.10.8 → 0.10.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +4 -4
- package/src/budget.ts +10 -9
- package/src/context-tools.ts +2 -6
- package/src/contracts.ts +2 -3
- package/src/history.ts +1 -3
- package/src/notes.ts +1 -3
- package/src/records.ts +3 -0
- package/src/rollover.ts +6 -9
- package/src/ui/text.ts +3 -4
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Give Pi your task. For example:
|
|
|
23
23
|
|
|
24
24
|
## How rollover works
|
|
25
25
|
|
|
26
|
-
Posthorse can
|
|
26
|
+
Posthorse can send a routine task-note reminder before rollover. The reminder tells the model to save current task state if needed, then immediately continue the prescribed work. The context window is not a task deadline. The reminder does not require `new_context` or wake an agent after its final answer. Large turns can reach rollover before the reminder. See [Reminder timing](docs/reminder-timing.md) for the trigger and its relationship to Pi's reserve and retention settings.
|
|
27
27
|
|
|
28
28
|
Automatic rollover retains a safe recent-message tail using Pi's `compaction.keepRecentTokens`, alongside a bounded recovery record for discarded inputs and tool evidence. It never calls a summarization model. The model must restore notes and check live state before it continues. Optional `new_context` deliberately retains no prior messages; manual `/compact` uses Pi's normal summarization.
|
|
29
29
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-posthorse",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Posthorse: fresh context, same journey. No-summary context rollover for Pi, with sparse budget reminders, handoffs, notes, and history.",
|
|
6
6
|
"keywords": [
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
"@earendil-works/pi-coding-agent": "1.5.0",
|
|
72
72
|
"@earendil-works/pi-tui": "1.5.0",
|
|
73
73
|
"@types/node": "24.19.1",
|
|
74
|
-
"oxc-parser": "0.
|
|
75
|
-
"oxfmt": "0.
|
|
76
|
-
"oxlint": "1.
|
|
74
|
+
"oxc-parser": "0.157.0",
|
|
75
|
+
"oxfmt": "0.76.0",
|
|
76
|
+
"oxlint": "1.91.0",
|
|
77
77
|
"oxlint-tsgolint": "7.0.2005",
|
|
78
78
|
"typebox": "1.3.27",
|
|
79
79
|
"typescript": "7.0.2",
|
package/src/budget.ts
CHANGED
|
@@ -13,8 +13,13 @@ 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
|
|
17
|
-
|
|
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.`;
|
|
18
23
|
export type Budget = {
|
|
19
24
|
readonly contextWindow: number;
|
|
20
25
|
readonly reserveTokens: number;
|
|
@@ -76,15 +81,11 @@ function automaticGuidance(budget: Budget | undefined, enabled: boolean): string
|
|
|
76
81
|
if (budget !== undefined && !budget.supported) {
|
|
77
82
|
return unsupportedMessage(budget);
|
|
78
83
|
}
|
|
79
|
-
const deadline =
|
|
80
|
-
budget === undefined
|
|
81
|
-
? "the configured Pi context limit"
|
|
82
|
-
: `${Math.max(1, Math.round((budget.rolloverAt / budget.contextWindow) * 100))}% used`;
|
|
83
84
|
const line =
|
|
84
85
|
budget === undefined
|
|
85
86
|
? ""
|
|
86
87
|
: ` The rollover line is ${budget.rolloverAt.toLocaleString("en-US")} tokens used under the available Pi settings.`;
|
|
87
|
-
return
|
|
88
|
+
return `${CHECKPOINT_GUIDANCE}\nAutomatic rollover follows Pi's compaction setting and retains a safe recent-message tail using compaction.keepRecentTokens, without model summarization.${line} At most one routine task-note reminder may arrive at the end of a working turn, never on a turn that ends the run. new_context is an optional deliberate clean reset, not a checkpoint requirement.`;
|
|
88
89
|
}
|
|
89
90
|
export function buildGuidance(ctx: PolicyContext): string {
|
|
90
91
|
const budget = budgetFor(ctx);
|
|
@@ -93,10 +94,10 @@ export function buildGuidance(ctx: PolicyContext): string {
|
|
|
93
94
|
budget === undefined
|
|
94
95
|
? "Configured context capacity is unknown."
|
|
95
96
|
: `Configured context capacity: ${budget.contextWindow.toLocaleString("en-US")} tokens. This is Pi's best available native limit, not remaining space or a measured provider boundary. Fresh windows use the active configuration; system instructions, tools, the handoff, retained recent messages, and new messages consume part of it.`;
|
|
96
|
-
return `##
|
|
97
|
+
return `## Automatic context management (Posthorse)
|
|
98
|
+
${automaticGuidance(budget, enabled)}
|
|
97
99
|
${capacity}
|
|
98
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.
|
|
99
|
-
${automaticGuidance(budget, enabled)}
|
|
100
101
|
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.
|
|
101
102
|
Keep one concise current-state note per task. Preserve decisions and safety constraints; link fuller evidence and history instead of copying them. Edit changed sections rather than resending unchanged content.
|
|
102
103
|
Write handoffs and notes as normal readable prose; notes and history search match literal text.`;
|
package/src/context-tools.ts
CHANGED
|
@@ -70,9 +70,7 @@ export function registerContextTools(
|
|
|
70
70
|
}),
|
|
71
71
|
),
|
|
72
72
|
}),
|
|
73
|
-
|
|
74
|
-
// oxlint-disable-next-line max-params -- QX-002: Pi supplies five fixed positional tool arguments.
|
|
75
|
-
async execute(id, { handoff }, _signal, _onUpdate, ctx) {
|
|
73
|
+
async execute(...[id, { handoff }, , , ctx]) {
|
|
76
74
|
const trimmed = handoff?.trim();
|
|
77
75
|
const limit = freshPayloadChars(policy(ctx), activeToolTokens());
|
|
78
76
|
if (trimmed !== undefined && trimmed !== "" && trimmed.length > limit) {
|
|
@@ -98,9 +96,7 @@ export function registerContextTools(
|
|
|
98
96
|
promptSnippet:
|
|
99
97
|
"verify remaining context before reporting a token count or changing plans because of context limits",
|
|
100
98
|
parameters: Type.Object({}),
|
|
101
|
-
|
|
102
|
-
// oxlint-disable-next-line max-params -- QX-002: Pi supplies five fixed positional tool arguments.
|
|
103
|
-
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
99
|
+
async execute(...[, , , , ctx]) {
|
|
104
100
|
return contextReport(policy(ctx));
|
|
105
101
|
},
|
|
106
102
|
});
|
package/src/contracts.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { PosthorseDisplay } from "../ui.ts";
|
|
3
3
|
|
|
4
|
+
export { isRecord } from "./records.ts";
|
|
5
|
+
|
|
4
6
|
export type CompactionPolicy = {
|
|
5
7
|
readonly enabled: boolean;
|
|
6
8
|
readonly reserveTokens: number;
|
|
@@ -108,9 +110,6 @@ export const REMINDER_TYPE = "posthorse-reminder";
|
|
|
108
110
|
export const AUTO_HANDOFF_PREFIX = "Automatic context rollover recovery record.";
|
|
109
111
|
export const EMPTY_HANDOFF = "Fresh context. Restore relevant notes and history before continuing.";
|
|
110
112
|
|
|
111
|
-
export function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
112
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
113
|
-
}
|
|
114
113
|
export function unknownArray(value: unknown): readonly unknown[] | undefined {
|
|
115
114
|
return Array.isArray(value) ? value : undefined;
|
|
116
115
|
}
|
package/src/history.ts
CHANGED
|
@@ -127,9 +127,7 @@ export function registerHistory(
|
|
|
127
127
|
}),
|
|
128
128
|
),
|
|
129
129
|
}),
|
|
130
|
-
|
|
131
|
-
// oxlint-disable-next-line max-params -- QX-002: Pi supplies five fixed positional tool arguments.
|
|
132
|
-
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
130
|
+
async execute(...[, params, signal, , ctx]) {
|
|
133
131
|
const source = historySource(ctx);
|
|
134
132
|
if (params.op === "search") {
|
|
135
133
|
const fail = (message: string): never =>
|
package/src/notes.ts
CHANGED
|
@@ -330,9 +330,7 @@ export function registerNotes(
|
|
|
330
330
|
}),
|
|
331
331
|
),
|
|
332
332
|
}),
|
|
333
|
-
|
|
334
|
-
// oxlint-disable-next-line max-params -- QX-002: Pi supplies five fixed positional tool arguments.
|
|
335
|
-
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
333
|
+
async execute(...[_id, params, signal, , ctx]) {
|
|
336
334
|
// Migrate newly returned legacy package notes without changing this session’s binding.
|
|
337
335
|
notesDir(ctx.cwd);
|
|
338
336
|
const session = await store.get(ctx);
|
package/src/records.ts
ADDED
package/src/rollover.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
ExtensionContext,
|
|
6
6
|
TurnEndEvent,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import { budgetFor,
|
|
8
|
+
import { budgetFor, CHECKPOINT_GUIDANCE, freshPayloadChars, type Budget } from "./budget.ts";
|
|
9
9
|
import {
|
|
10
10
|
isRecord,
|
|
11
11
|
isReminderType,
|
|
@@ -83,9 +83,7 @@ function endsRun(
|
|
|
83
83
|
ctx.hasPendingMessages?.() !== true
|
|
84
84
|
);
|
|
85
85
|
}
|
|
86
|
-
function reminderBudget(
|
|
87
|
-
host: PolicyContext,
|
|
88
|
-
): { readonly budget: Budget; readonly tokens: number } | undefined {
|
|
86
|
+
function reminderBudget(host: PolicyContext): Budget | undefined {
|
|
89
87
|
const usage = host.getContextUsage();
|
|
90
88
|
if (usage === undefined || usage.tokens === null || usage.contextWindow <= 0) {
|
|
91
89
|
return undefined;
|
|
@@ -100,14 +98,13 @@ function reminderBudget(
|
|
|
100
98
|
return undefined;
|
|
101
99
|
}
|
|
102
100
|
const buffer = Math.min(REMINDER_BUFFER_TOKENS, Math.floor(budget.usable * 0.1));
|
|
103
|
-
return usage.tokens < budget.rolloverAt - buffer ? undefined :
|
|
101
|
+
return usage.tokens < budget.rolloverAt - buffer ? undefined : budget;
|
|
104
102
|
}
|
|
105
103
|
function sendReminder(
|
|
106
104
|
pi: ExtensionAPI,
|
|
107
105
|
ctx: ExtensionContext,
|
|
108
106
|
available: {
|
|
109
107
|
readonly budget: Budget;
|
|
110
|
-
readonly tokens: number;
|
|
111
108
|
readonly pendingMessages: readonly {
|
|
112
109
|
readonly role: string;
|
|
113
110
|
readonly customType?: string | undefined;
|
|
@@ -116,7 +113,7 @@ function sendReminder(
|
|
|
116
113
|
},
|
|
117
114
|
reminders: ReminderLookup,
|
|
118
115
|
): void {
|
|
119
|
-
const { budget
|
|
116
|
+
const { budget } = available;
|
|
120
117
|
const state = reminders.read(ctx);
|
|
121
118
|
const fingerprint: ReminderFingerprint = {
|
|
122
119
|
windowId: state.windowId,
|
|
@@ -137,7 +134,7 @@ function sendReminder(
|
|
|
137
134
|
pi.sendMessage(
|
|
138
135
|
{
|
|
139
136
|
customType: REMINDER_TYPE,
|
|
140
|
-
content: `[posthorse]
|
|
137
|
+
content: `[posthorse] Routine task-note update.\n${CHECKPOINT_GUIDANCE}`,
|
|
141
138
|
display: true,
|
|
142
139
|
details: fingerprint,
|
|
143
140
|
},
|
|
@@ -481,7 +478,7 @@ export function registerRollover(
|
|
|
481
478
|
sendReminder(
|
|
482
479
|
pi,
|
|
483
480
|
ctx,
|
|
484
|
-
{
|
|
481
|
+
{ budget: available, pendingMessages: event.context.pendingMessages },
|
|
485
482
|
reminders,
|
|
486
483
|
);
|
|
487
484
|
}
|
package/src/ui/text.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Text, stripTerminalSequences, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { isRecord } from "../records.ts";
|
|
4
|
+
|
|
5
|
+
export { isRecord } from "../records.ts";
|
|
3
6
|
|
|
4
7
|
export type OutputColor = "toolOutput" | "muted" | "dim" | "error" | "warning";
|
|
5
8
|
|
|
@@ -8,10 +11,6 @@ export function clean(value: string): string {
|
|
|
8
11
|
return stripTerminalSequences(value).replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f\ufff9-\ufffb]/g, "");
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
export function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
12
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
14
|
export function argsOf(value: unknown): Readonly<Record<string, unknown>> {
|
|
16
15
|
return isRecord(value) ? value : {};
|
|
17
16
|
}
|