pi-posthorse 0.10.16 → 0.10.18
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 +1 -1
- package/src/note-provenance.ts +3 -2
- package/src/note-scopes.ts +8 -1
- package/src/note-snapshots.ts +14 -1
- package/src/note-store.ts +4 -7
- 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/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-provenance.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { parseSessionLine } from "./session-files.ts";
|
|
|
7
7
|
import { sessionNoteTouches } from "./note-evidence.ts";
|
|
8
8
|
import { mapBatch, readManifest, snapshot, type SnapshotNote } from "./note-snapshots.ts";
|
|
9
9
|
import {
|
|
10
|
+
addressIn,
|
|
10
11
|
addressOf,
|
|
11
12
|
bindingsOf,
|
|
12
13
|
canonicalPath,
|
|
@@ -130,7 +131,7 @@ export async function resolveReference(
|
|
|
130
131
|
}
|
|
131
132
|
visited.add(directory);
|
|
132
133
|
if (existsSync(notePath(current, path))) {
|
|
133
|
-
return
|
|
134
|
+
return addressIn(current, path);
|
|
134
135
|
}
|
|
135
136
|
// A manifest includes later adoptions; the journal includes later writes and deletions.
|
|
136
137
|
const binding: NoteBinding | undefined =
|
|
@@ -204,7 +205,7 @@ function seedCandidates(
|
|
|
204
205
|
) {
|
|
205
206
|
selected.set(seed.path, {
|
|
206
207
|
...seed,
|
|
207
|
-
source:
|
|
208
|
+
source: addressIn(binding, seed.path),
|
|
208
209
|
cleared: false,
|
|
209
210
|
});
|
|
210
211
|
}
|
package/src/note-scopes.ts
CHANGED
|
@@ -83,6 +83,13 @@ export function forkScope(owner: NoteOwner): string {
|
|
|
83
83
|
export function scopeDirectory(scope: NoteScope): string {
|
|
84
84
|
return join(scope.root, scope.scope);
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* An address is stored in the journal, and a scope value is often a whole binding. Build an
|
|
88
|
+
* address here, never by spreading a scope, so it holds the two location fields and nothing else.
|
|
89
|
+
*/
|
|
90
|
+
export function addressIn(scope: NoteScope, path: string): NoteAddress {
|
|
91
|
+
return { root: scope.root, scope: scope.scope, path };
|
|
92
|
+
}
|
|
86
93
|
export function sameScope(left: NoteScope, right: NoteScope): boolean {
|
|
87
94
|
return left.root === right.root && left.scope === right.scope;
|
|
88
95
|
}
|
|
@@ -278,7 +285,7 @@ export function addressOf(path: string, scopes: readonly NoteScope[]): NoteAddre
|
|
|
278
285
|
? undefined
|
|
279
286
|
: candidates.find((item) => item.directory === match.directory);
|
|
280
287
|
if (match !== undefined && candidate !== undefined) {
|
|
281
|
-
return
|
|
288
|
+
return addressIn(candidate.scope, match.path);
|
|
282
289
|
}
|
|
283
290
|
if (
|
|
284
291
|
scopes.some(
|
package/src/note-snapshots.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
|
18
18
|
import { isRecord } from "./contracts.ts";
|
|
19
19
|
import { publishFile } from "./files.ts";
|
|
20
20
|
import {
|
|
21
|
+
addressIn,
|
|
21
22
|
notePath,
|
|
22
23
|
parseBinding,
|
|
23
24
|
sameOwner,
|
|
@@ -127,7 +128,19 @@ async function copyNote(note: SnapshotNote, destination: string): Promise<CopyRe
|
|
|
127
128
|
}
|
|
128
129
|
await utimes(destination, before.atimeMs / 1000, before.mtimeMs / 1000);
|
|
129
130
|
const [digest, destInfo] = await Promise.all([fileDigest(destination), stat(destination)]);
|
|
130
|
-
return {
|
|
131
|
+
return {
|
|
132
|
+
kind: "verified",
|
|
133
|
+
// Journals written by 0.10.4 through 0.10.17 hold whole bindings in seed addresses.
|
|
134
|
+
// A new seed keeps each location only, so that data is not copied into this fork.
|
|
135
|
+
seed: {
|
|
136
|
+
path: note.path,
|
|
137
|
+
source: addressIn(note.source, note.source.path),
|
|
138
|
+
lineage: addressIn(note.lineage, note.lineage.path),
|
|
139
|
+
digest,
|
|
140
|
+
identity: fileIdentity(destInfo),
|
|
141
|
+
anchors: note.anchors,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
131
144
|
} catch (error) {
|
|
132
145
|
return {
|
|
133
146
|
kind: "unverified",
|
package/src/note-store.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
NOTES_BINDING,
|
|
15
15
|
NOTES_TOUCH,
|
|
16
|
+
addressIn,
|
|
16
17
|
bindingsOf,
|
|
17
18
|
canonicalPath,
|
|
18
19
|
inside,
|
|
@@ -129,7 +130,7 @@ export class SessionNotes {
|
|
|
129
130
|
return manifest;
|
|
130
131
|
}
|
|
131
132
|
throw new Error(
|
|
132
|
-
`Unknown notes scope ${selector}.
|
|
133
|
+
`Unknown notes scope ${JSON.stringify(selector)}. Omit scope for this session's notes, or copy a scope selector from notes list/search.`,
|
|
133
134
|
);
|
|
134
135
|
}
|
|
135
136
|
async #reference(path: string, signal?: AbortSignal): Promise<NoteAddress | undefined> {
|
|
@@ -154,7 +155,7 @@ export class SessionNotes {
|
|
|
154
155
|
if (options.scope !== undefined) {
|
|
155
156
|
const scope = await this.selectScope(options.scope);
|
|
156
157
|
return {
|
|
157
|
-
address:
|
|
158
|
+
address: addressIn(scope, relative),
|
|
158
159
|
path: notePath(scope, relative),
|
|
159
160
|
writable: sameScope(scope, this.binding),
|
|
160
161
|
};
|
|
@@ -162,11 +163,7 @@ export class SessionNotes {
|
|
|
162
163
|
const reference = this.owns(relative)
|
|
163
164
|
? undefined
|
|
164
165
|
: await this.#reference(relative, options.signal);
|
|
165
|
-
const address = reference ??
|
|
166
|
-
root: this.binding.root,
|
|
167
|
-
scope: this.binding.scope,
|
|
168
|
-
path: relative,
|
|
169
|
-
};
|
|
166
|
+
const address = reference ?? addressIn(this.binding, relative);
|
|
170
167
|
return { address, path: notePath(address, relative), writable: reference === undefined };
|
|
171
168
|
}
|
|
172
169
|
async adopt(path: string, call: NoteCall): Promise<Adoption> {
|
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
|
},
|