pi-posthorse 0.10.16 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-posthorse",
3
- "version": "0.10.16",
3
+ "version": "0.10.17",
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": [
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}. Copy a scope selector from notes list/search.`,
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, params, signal, , ctx]) {
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
- const scopedPath =
266
- typeof args.scope === "string" && path !== undefined ? `${args.scope}/${path}` : path;
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);
@@ -77,22 +77,39 @@ function applyTouch(
77
77
  order,
78
78
  };
79
79
  }
80
- function formatNoteLabel(note: TrackedNote): string {
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 `${note.notePath} (another notes scope: ${note.absPath ?? `${note.referenceScope}/${note.notePath}`})`;
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(note: WrittenNote, span: RetainedSpan | undefined): string {
89
- const label = formatNoteLabel(note);
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(note: ReadNote, span: RetainedSpan | undefined): string {
110
- const label = formatNoteLabel(note);
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
- // notes read resolves a bare name in this session's own scope, so a note elsewhere is named by its path.
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" : "notes read or 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
  },