pi-posthorse 0.10.15 → 0.10.17
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 +2 -0
- package/package.json +1 -1
- package/src/budget.ts +1 -1
- package/src/note-store.ts +1 -1
- package/src/notes.ts +7 -1
- package/src/recovery-note-touches.ts +3 -2
- package/src/recovery-notes.ts +39 -24
- package/src/rollover.ts +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,8 @@ See the [changelog](https://github.com/fitchmultz/pi-posthorse/blob/main/CHANGEL
|
|
|
35
35
|
|
|
36
36
|
Recoverable history is not guaranteed model recall. See the [practical guarantees and limits](https://github.com/fitchmultz/pi-posthorse/blob/main/docs/context-recovery.md) for the stock-Pi comparison, search scope, checkpoints, privacy, concurrency and cost, or the [rollover reference](https://github.com/fitchmultz/pi-posthorse/blob/main/docs/reference.md#how-it-works) for implementation details.
|
|
37
37
|
|
|
38
|
+
The [field study](https://github.com/fitchmultz/pi-posthorse/blob/main/docs/field-study.md) measures what agents did around 24,940 context boundaries in daily use: note and history use, repeated work, stops, wait time, and cost. It is observational, not a controlled comparison with stock compaction.
|
|
39
|
+
|
|
38
40
|
## Tools
|
|
39
41
|
|
|
40
42
|
These tools are available to the model:
|
package/package.json
CHANGED
package/src/budget.ts
CHANGED
|
@@ -79,7 +79,7 @@ function automaticGuidance(budget: Budget | undefined, enabled: boolean): string
|
|
|
79
79
|
? ""
|
|
80
80
|
: `The rollover line is ${budget.rolloverAt.toLocaleString("en-US")} tokens used. `;
|
|
81
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.`;
|
|
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, or write one if the task has none, then continue with the next step in the same run. new_context is an optional clean reset.`;
|
|
83
83
|
}
|
|
84
84
|
export function buildGuidance(ctx: PolicyContext): string {
|
|
85
85
|
const budget = budgetFor(ctx);
|
package/src/note-store.ts
CHANGED
|
@@ -129,7 +129,7 @@ export class SessionNotes {
|
|
|
129
129
|
return manifest;
|
|
130
130
|
}
|
|
131
131
|
throw new Error(
|
|
132
|
-
`Unknown notes scope ${selector}.
|
|
132
|
+
`Unknown notes scope ${JSON.stringify(selector)}. Omit scope for this session's notes, or copy a scope selector from notes list/search.`,
|
|
133
133
|
);
|
|
134
134
|
}
|
|
135
135
|
async #reference(path: string, signal?: AbortSignal): Promise<NoteAddress | undefined> {
|
package/src/notes.ts
CHANGED
|
@@ -278,6 +278,11 @@ async function searchNotes(
|
|
|
278
278
|
header: hits.skipped > 0 ? `[Not searched: ${skipped} over 1 MiB.]\n` : "",
|
|
279
279
|
});
|
|
280
280
|
}
|
|
281
|
+
/** Some models send every optional argument; an empty scope means no scope. */
|
|
282
|
+
function withoutEmptyScope(params: NotesParams): NotesParams {
|
|
283
|
+
const { scope, ...unscoped } = params;
|
|
284
|
+
return scope === "" ? unscoped : params;
|
|
285
|
+
}
|
|
281
286
|
export function registerNotes(
|
|
282
287
|
pi: ExtensionAPI,
|
|
283
288
|
policy: (ctx: ExtensionContext) => PolicyContext,
|
|
@@ -325,7 +330,8 @@ export function registerNotes(
|
|
|
325
330
|
}),
|
|
326
331
|
),
|
|
327
332
|
}),
|
|
328
|
-
async execute(...[_id,
|
|
333
|
+
async execute(...[_id, received, signal, , ctx]) {
|
|
334
|
+
const params = withoutEmptyScope(received);
|
|
329
335
|
// Migrate newly returned legacy package notes without changing this session’s binding.
|
|
330
336
|
notesDir(ctx.cwd);
|
|
331
337
|
const session = await store.get(ctx);
|
|
@@ -262,8 +262,9 @@ function notesToolTouches(
|
|
|
262
262
|
const parsed = resultText === undefined ? undefined : extractNotesResultTarget(resultText);
|
|
263
263
|
const kind = notesCallKind(args) ?? parsed?.kind;
|
|
264
264
|
const path = optionalString(args.path);
|
|
265
|
-
|
|
266
|
-
|
|
265
|
+
// An empty scope counts as omitted, as it does when the tool runs.
|
|
266
|
+
const scope = args.scope === "" ? undefined : optionalString(args.scope);
|
|
267
|
+
const scopedPath = scope !== undefined && path !== undefined ? `${scope}/${path}` : path;
|
|
267
268
|
const callTarget = resolveNoteTarget(scopedPath, context.knownDirs, true);
|
|
268
269
|
const resTarget = resolveNoteTarget(parsed?.rawPath, context.knownDirs, true);
|
|
269
270
|
const merged = mergeNoteTargets(callTarget, resTarget);
|
package/src/recovery-notes.ts
CHANGED
|
@@ -77,22 +77,39 @@ function applyTouch(
|
|
|
77
77
|
order,
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
/** `notes read` resolves a bare name in this session's own notes, so a note elsewhere is named by its path. */
|
|
81
|
+
function pathElsewhere(note: TrackedNote, notesDirectory: string | undefined): string | undefined {
|
|
81
82
|
if (note.referenceScope !== undefined) {
|
|
82
|
-
return
|
|
83
|
+
return note.absPath ?? `${note.referenceScope}/${note.notePath}`;
|
|
84
|
+
}
|
|
85
|
+
return note.absPath !== undefined &&
|
|
86
|
+
notesDirectory !== undefined &&
|
|
87
|
+
note.absPath !== join(notesDirectory, note.notePath)
|
|
88
|
+
? note.absPath
|
|
89
|
+
: undefined;
|
|
90
|
+
}
|
|
91
|
+
function formatNoteLabel(note: TrackedNote, elsewhere: string | undefined): string {
|
|
92
|
+
if (elsewhere !== undefined) {
|
|
93
|
+
return note.referenceScope === undefined ? elsewhere : `${elsewhere} (another notes scope)`;
|
|
83
94
|
}
|
|
84
95
|
return note.absPath !== undefined && note.absPath !== note.notePath
|
|
85
96
|
? `${note.notePath} (${note.absPath})`
|
|
86
97
|
: note.notePath;
|
|
87
98
|
}
|
|
88
|
-
function formatWrittenNoteLine(
|
|
89
|
-
|
|
99
|
+
function formatWrittenNoteLine(
|
|
100
|
+
note: WrittenNote,
|
|
101
|
+
span: RetainedSpan | undefined,
|
|
102
|
+
notesDirectory: string | undefined,
|
|
103
|
+
): string {
|
|
104
|
+
const elsewhere = pathElsewhere(note, notesDirectory);
|
|
105
|
+
const label = formatNoteLabel(note, elsewhere);
|
|
106
|
+
const how = elsewhere === undefined ? "" : " on its path";
|
|
90
107
|
const { proof } = note;
|
|
91
108
|
if (proof.kind === "effect-only") {
|
|
92
|
-
return `- Active note (written): ${label} [verified effect at entry ${note.entryId}; full content is not retained — read this note now]`;
|
|
109
|
+
return `- Active note (written): ${label} [verified effect at entry ${note.entryId}; full content is not retained — read this note${how} now]`;
|
|
93
110
|
}
|
|
94
111
|
if (span === undefined) {
|
|
95
|
-
return `- Active note (written): ${label} [compacted at entry ${note.entryId} — read this note now]`;
|
|
112
|
+
return `- Active note (written): ${label} [compacted at entry ${note.entryId} — read this note${how} now]`;
|
|
96
113
|
}
|
|
97
114
|
switch (proof.kind) {
|
|
98
115
|
case "full": {
|
|
@@ -101,13 +118,17 @@ function formatWrittenNoteLine(note: WrittenNote, span: RetainedSpan | undefined
|
|
|
101
118
|
return `- Active note (written): ${label} [latest full write is in the retained tail above at entry ${proof.entryId}${updated}]`;
|
|
102
119
|
}
|
|
103
120
|
case "partial":
|
|
104
|
-
return `- Active note (written): ${label} [only partial edits are in the retained tail above at entry ${proof.entryId} — read full note now]`;
|
|
121
|
+
return `- Active note (written): ${label} [only partial edits are in the retained tail above at entry ${proof.entryId} — read full note${how} now]`;
|
|
105
122
|
case "compacted":
|
|
106
|
-
return `- Active note (written): ${label} [compacted before entry ${span.firstId} — read this note now]`;
|
|
123
|
+
return `- Active note (written): ${label} [compacted before entry ${span.firstId} — read this note${how} now]`;
|
|
107
124
|
}
|
|
108
125
|
}
|
|
109
|
-
function formatReadNoteLine(
|
|
110
|
-
|
|
126
|
+
function formatReadNoteLine(
|
|
127
|
+
note: ReadNote,
|
|
128
|
+
span: RetainedSpan | undefined,
|
|
129
|
+
notesDirectory: string | undefined,
|
|
130
|
+
): string {
|
|
131
|
+
const label = formatNoteLabel(note, pathElsewhere(note, notesDirectory));
|
|
111
132
|
if (span !== undefined && note.inRetained) {
|
|
112
133
|
return `- Referenced note (read): ${label} [read in the retained tail above at entry ${note.entryId}]`;
|
|
113
134
|
}
|
|
@@ -123,16 +144,12 @@ function formatNotesAction(
|
|
|
123
144
|
if (primaryWritten === undefined) {
|
|
124
145
|
return "Read the referenced note again if its context is still needed, and keep one current-state note for this task.";
|
|
125
146
|
}
|
|
126
|
-
|
|
127
|
-
const elsewhere
|
|
128
|
-
primaryWritten.absPath !== undefined &&
|
|
129
|
-
notesDirectory !== undefined &&
|
|
130
|
-
primaryWritten.absPath !== join(notesDirectory, primaryWritten.notePath);
|
|
131
|
-
const target = elsewhere ? primaryWritten.absPath : primaryWritten.notePath;
|
|
147
|
+
const elsewhere = pathElsewhere(primaryWritten, notesDirectory);
|
|
148
|
+
const target = elsewhere ?? primaryWritten.notePath;
|
|
132
149
|
if (span !== undefined && primaryWritten.proof.kind === "full") {
|
|
133
150
|
return `Your latest full write to ${target} is already visible above at entry ${primaryWritten.proof.entryId}; do not rewrite it unless new findings after entry ${span.lastId} require it.`;
|
|
134
151
|
}
|
|
135
|
-
const how = elsewhere ? "read on its path" : "
|
|
152
|
+
const how = elsewhere === undefined ? "notes read or read on its path" : "read on its path";
|
|
136
153
|
return `Read ${target} (${how}) before continuing to restore the saved goal, progress, decisions, and next steps, then keep that note current as work progresses.`;
|
|
137
154
|
}
|
|
138
155
|
function trackedNotes(
|
|
@@ -181,16 +198,14 @@ export function renderSessionNotes(
|
|
|
181
198
|
}
|
|
182
199
|
const written = all.filter((note) => note.kind === "written").slice(0, MAX_WRITTEN_NOTES);
|
|
183
200
|
const readOnly = all.filter((note) => note.kind === "read").slice(0, MAX_READ_NOTES);
|
|
201
|
+
const notesDirectory =
|
|
202
|
+
options.notes === undefined ? defaultDir : scopeDirectory(options.notes.binding);
|
|
184
203
|
const lines = [
|
|
185
|
-
...written.map((note) => formatWrittenNoteLine(note, span)),
|
|
186
|
-
...readOnly.map((note) => formatReadNoteLine(note, span)),
|
|
204
|
+
...written.map((note) => formatWrittenNoteLine(note, span, notesDirectory)),
|
|
205
|
+
...readOnly.map((note) => formatReadNoteLine(note, span, notesDirectory)),
|
|
187
206
|
];
|
|
188
207
|
const header = "Task notes in this session:";
|
|
189
|
-
const action = formatNotesAction(
|
|
190
|
-
written[0],
|
|
191
|
-
span,
|
|
192
|
-
options.notes === undefined ? defaultDir : scopeDirectory(options.notes.binding),
|
|
193
|
-
);
|
|
208
|
+
const action = formatNotesAction(written[0], span, notesDirectory);
|
|
194
209
|
const selected: string[] = [];
|
|
195
210
|
for (const line of lines) {
|
|
196
211
|
if ([header, ...selected, line, action].join("\n").length <= maxChars) {
|
package/src/rollover.ts
CHANGED
|
@@ -445,7 +445,7 @@ export function registerRollover(
|
|
|
445
445
|
{
|
|
446
446
|
customType: REMINDER_TYPE,
|
|
447
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.",
|
|
448
|
+
"[posthorse] Routine task-note update. Update the task's current-state note if it is stale, or write one if the task has none, then continue with the next step in this run.",
|
|
449
449
|
display: true,
|
|
450
450
|
details: reminder.fingerprint,
|
|
451
451
|
},
|