pi-posthorse 0.4.5 → 0.4.6
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 +6 -4
- package/index.ts +120 -56
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ Native, no-summary context windows for the [`fitchmultz/pi`](https://github.com/
|
|
|
8
8
|
|
|
9
9
|
Pi owns the persisted boundary. Posthorse owns the policy: stable window guidance, one best-effort checkpoint reminder, `new_context`, `get_context_remaining`, durable `notes`, and window-aware `history`. A rollover removes the old window from active model context while the JSONL transcript stays append-only and complete.
|
|
10
10
|
|
|
11
|
+
An isolated [Codex prototype](adapters/codex-posthorse/README.md) tests local notes and recovery with Codex's native context reset. It is not ready for everyday use and is not included in the Pi package.
|
|
12
|
+
|
|
11
13
|
## Requirements
|
|
12
14
|
|
|
13
15
|
- Node `>=22.19.0`.
|
|
@@ -52,7 +54,7 @@ Keep exactly one copy loaded. `pi list` shows every package source; if an older
|
|
|
52
54
|
4. **`new_context`.** Requests an atomic rollover after the complete tool batch succeeds. An optional handoff is persisted and becomes the first state of the fresh window. If a sibling tool in the same batch fails, Pi does not commit the boundary; the checkpoint reminder still applies.
|
|
53
55
|
5. **Automatic rollover without summaries.** With a supported context budget and room for a recovery record, Posthorse claims Pi's automatic threshold and overflow trigger through `session_before_auto_compact`, before Pi resolves summarization credentials or prepares a summary. Oversized first turns and tool results can then roll over even without summarization credentials. Otherwise Pi's own compaction remains in control. Manual `/compact` is unchanged.
|
|
54
56
|
6. **Bounded recovery record.** The automatic handoff keeps direct user inputs, `ask_question` outcomes, visible coordination messages, and the trailing tool batch that no model has consumed yet (call arguments, bounded result text, and the entry ids to recover the rest). A clearly labeled, possibly stale older checkpoint comes last, after the current inputs and unseen results. Older assistant prose and consumed tool results are not treated as state. Newly submitted input stays separate and is saved after the boundary, not copied into the handoff.
|
|
55
|
-
7. **`notes` and `history`.** Notes
|
|
57
|
+
7. **`notes` and `history`.** Notes are shared across linked worktrees, at the main checkout for conventional Git layouts or inside the common Git directory when metadata is stored separately. History searches normalized transcript text and returns stored images for a requested entry.
|
|
56
58
|
|
|
57
59
|
At turn end, Posthorse checks whether usage is in the reminder band before explicitly looking up the full branch. Context filtering skips its branch lookup when model input contains neither `posthorse-reminder` nor legacy `headroom-reminder` messages. History searches, reads, and recovery remain available with no new limits.
|
|
58
60
|
|
|
@@ -79,20 +81,20 @@ In the TUI, tools use Pi's native expandable cards. Collapsed cards show the ope
|
|
|
79
81
|
|
|
80
82
|
Committed context-window messages and checkpoint reminders are compact, expandable cards too. The `new_context` tool card describes a request; only the committed context-window message says a fresh window has started.
|
|
81
83
|
|
|
82
|
-
Read pages, including returned images, shrink to the context that is actually left. Before usage is known, they reserve prompt/tool overhead and leave half the rest free. Unsafe pages are refused with the offset preserved; call `new_context` and retry.
|
|
84
|
+
Read pages, including returned images, shrink to the context that is actually left. Parallel note and history reads share that budget; pages already counted by Pi are not counted twice. Before usage is known, they reserve prompt/tool overhead and leave half the rest free. Unsafe pages are refused with the offset preserved in the call; call `new_context` and retry. Refusal text uses the same budget, so later failures omit repeated guidance once no more fits.
|
|
83
85
|
|
|
84
86
|
`history search` puts matching original content before recovery material: handoffs, compaction and branch summaries, checkpoint reminders, and `notes`, `new_context`, and `history` calls/results. Ordinary prose or another tool call in the same assistant entry keeps its priority when that content matches. Every entry remains searchable; `history read` returns the complete normalized entry, including any recovery content omitted from a search excerpt.
|
|
85
87
|
|
|
86
88
|
Within each group, current-branch matches are newest first. With `all: true`, Posthorse searches every session file in the active Pi session directory, newest-modified sessions first and newest entries within each session; this is not a global timestamp sort. The result limit applies after priority, so newer echoes cannot displace older original matches. Entries copied by a fork are reported once.
|
|
87
89
|
|
|
88
|
-
Notes live in `.pi/notes/` at the
|
|
90
|
+
Notes live in `.pi/notes/` at the main checkout for conventional Git layouts, or the current directory outside Git. When Git metadata is stored separately, including in submodules, all checkouts use `.pi/notes/` inside Git's common directory instead; no configuration is needed. If that metadata is unavailable, notes stay local to the checkout. Old checkout-local notes are imported when that checkout is accessed, without overwriting shared files or deleting the originals. Writes report the actual storage path. Add `.pi/notes/` to `.gitignore` when the project should not track it.
|
|
89
91
|
|
|
90
92
|
## Data and privacy
|
|
91
93
|
|
|
92
94
|
- Posthorse makes no network requests.
|
|
93
95
|
- Notes are plaintext files under `.pi/notes`. They survive package removal and may be committed unless ignored.
|
|
94
96
|
- `history` with `all: true` scans nested JSONL files in the active Pi session directory, including subagent sessions.
|
|
95
|
-
- History can return user text, assistant text and thinking, tool arguments and results, handoffs, custom messages, and images. Direct shell entries Pi marked `excludeFromContext` come back as a placeholder only.
|
|
97
|
+
- History can return user text, assistant text and thinking, assistant failure status and error messages, tool arguments and results, handoffs, custom messages, and images. Direct shell entries Pi marked `excludeFromContext` come back as a placeholder only.
|
|
96
98
|
- Returned history content enters the currently selected model and provider context.
|
|
97
99
|
- Removing Posthorse does not remove notes or Pi session history.
|
|
98
100
|
|
package/index.ts
CHANGED
|
@@ -8,16 +8,18 @@
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
appendFileSync,
|
|
11
|
+
constants,
|
|
12
|
+
copyFileSync,
|
|
11
13
|
createReadStream,
|
|
12
14
|
existsSync,
|
|
13
15
|
mkdirSync,
|
|
14
16
|
readdirSync,
|
|
15
17
|
readFileSync,
|
|
18
|
+
realpathSync,
|
|
16
19
|
statSync,
|
|
17
20
|
writeFileSync,
|
|
18
21
|
} from "node:fs";
|
|
19
|
-
import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
20
|
-
import { createInterface } from "node:readline";
|
|
22
|
+
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
21
23
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
22
24
|
import { Type } from "typebox";
|
|
23
25
|
import { registerPosthorseMessages, toolCards, type PosthorseDisplay } from "./ui.ts";
|
|
@@ -64,6 +66,7 @@ type ImageLike = { type: "image"; data: string; mimeType: string };
|
|
|
64
66
|
type MessageLike = {
|
|
65
67
|
role?: string;
|
|
66
68
|
stopReason?: string;
|
|
69
|
+
errorMessage?: string;
|
|
67
70
|
content?: unknown;
|
|
68
71
|
toolName?: string;
|
|
69
72
|
toolCallId?: string;
|
|
@@ -164,21 +167,49 @@ function textResult(text: string, images: ImageLike[] = [], display?: PosthorseD
|
|
|
164
167
|
return { content: [{ type: "text" as const, text }, ...images], details: display };
|
|
165
168
|
}
|
|
166
169
|
|
|
167
|
-
/**
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
/** Import old checkout-local notes without overwriting shared notes, including concurrent writes. */
|
|
171
|
+
function importLegacyNotes(source: string, target: string): void {
|
|
172
|
+
if (!existsSync(source)) return;
|
|
173
|
+
if (existsSync(target) && (!statSync(target).isDirectory() || realpathSync(source) === realpathSync(target))) return;
|
|
174
|
+
mkdirSync(target, { recursive: true });
|
|
175
|
+
for (const name of readdirSync(source)) {
|
|
176
|
+
const from = join(source, name);
|
|
177
|
+
const to = join(target, name);
|
|
178
|
+
if (statSync(from).isDirectory()) {
|
|
179
|
+
importLegacyNotes(from, to);
|
|
180
|
+
} else {
|
|
181
|
+
try {
|
|
182
|
+
copyFileSync(from, to, constants.COPYFILE_EXCL);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Conventional repos use the main checkout; separate Git directories are their own shared root. */
|
|
172
191
|
function notesRoot(cwd: string): string {
|
|
173
192
|
for (let dir = cwd; ; dir = dirname(dir)) {
|
|
174
193
|
const marker = join(dir, ".git");
|
|
175
194
|
if (existsSync(marker)) {
|
|
195
|
+
let root: string;
|
|
176
196
|
try {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
let target = marker;
|
|
198
|
+
if (!statSync(marker).isDirectory()) {
|
|
199
|
+
const gitdir = readFileSync(marker, "utf8").match(/^gitdir:\s*(.+?)\s*$/m)?.[1];
|
|
200
|
+
if (!gitdir) return dir;
|
|
201
|
+
target = resolve(dir, gitdir);
|
|
202
|
+
}
|
|
203
|
+
target = realpathSync(target);
|
|
204
|
+
const commonFile = join(target, "commondir");
|
|
205
|
+
const common = realpathSync(existsSync(commonFile) ? resolve(target, readFileSync(commonFile, "utf8").trim()) : target);
|
|
206
|
+
root = basename(common) === ".git" ? dirname(common) : common;
|
|
207
|
+
} catch {
|
|
208
|
+
// Orphaned/copied worktrees can outlive their Git metadata; local notes still work.
|
|
209
|
+
return dir;
|
|
210
|
+
}
|
|
211
|
+
if (root !== dir) importLegacyNotes(join(dir, ".pi", "notes"), join(root, ".pi", "notes"));
|
|
212
|
+
return root;
|
|
182
213
|
}
|
|
183
214
|
if (dirname(dir) === dir) return cwd;
|
|
184
215
|
}
|
|
@@ -195,6 +226,12 @@ function excerptAround(text: string, index: number, before: number, length: numb
|
|
|
195
226
|
return `${start ? "…" : ""}${text.slice(start, end)}${end < text.length ? "…" : ""}`;
|
|
196
227
|
}
|
|
197
228
|
|
|
229
|
+
function assistantFailure(message: MessageLike): string {
|
|
230
|
+
if (message.role !== "assistant") return "";
|
|
231
|
+
if (message.stopReason !== "error" && message.stopReason !== "aborted" && !message.errorMessage) return "";
|
|
232
|
+
return `[${message.stopReason ?? "error"}]${message.errorMessage ? ` ${message.errorMessage}` : ""}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
198
235
|
function flattenEntry(entry: EntryLike): string | undefined {
|
|
199
236
|
if (entry.type === "message") {
|
|
200
237
|
const message = entry.message ?? {};
|
|
@@ -203,7 +240,7 @@ function flattenEntry(entry: EntryLike): string | undefined {
|
|
|
203
240
|
if (message.excludeFromContext === true) return "[bashExecution] (excluded from model context by Pi)";
|
|
204
241
|
return `[bashExecution] $ ${message.command ?? ""}\n${message.output ?? ""}`;
|
|
205
242
|
}
|
|
206
|
-
return `[${message.role ?? "message"}] ${[textOf(message), imageSummary(imagesOf(message.content))].filter(Boolean).join("\n")}`;
|
|
243
|
+
return `[${message.role ?? "message"}] ${[textOf(message), assistantFailure(message), imageSummary(imagesOf(message.content))].filter(Boolean).join("\n")}`;
|
|
207
244
|
}
|
|
208
245
|
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
209
246
|
return `[${entry.type}] ${entry.summary ?? ""}`;
|
|
@@ -254,8 +291,8 @@ function historyHit(item: WindowedEntry, query: string, source = ""): HistoryHit
|
|
|
254
291
|
if (partText) originals[originals.length - 1] += `${originals.at(-1) ? "\n" : ""}${partText}`;
|
|
255
292
|
}
|
|
256
293
|
}
|
|
257
|
-
const
|
|
258
|
-
if (
|
|
294
|
+
const metadata = [assistantFailure(entry.message), imageSummary(item.images)].filter(Boolean).join("\n");
|
|
295
|
+
if (metadata) originals[originals.length - 1] += `${originals.at(-1) ? "\n" : ""}${metadata}`;
|
|
259
296
|
if (originals[0]) originals[0] = `[assistant] ${originals[0]}`;
|
|
260
297
|
const original = originals.find((part) => part.toLowerCase().includes(query));
|
|
261
298
|
priority = original ? 0 : 1;
|
|
@@ -291,25 +328,30 @@ function* windowEntries(entries: Iterable<EntryLike>): Generator<WindowedEntry>
|
|
|
291
328
|
}
|
|
292
329
|
}
|
|
293
330
|
|
|
331
|
+
/** JSONL splits on LF, not the Unicode separators that Node's readline also recognizes. */
|
|
332
|
+
async function* jsonlLines(file: string, signal?: AbortSignal): AsyncGenerator<string> {
|
|
333
|
+
const stream = createReadStream(file, { encoding: "utf8", signal });
|
|
334
|
+
let pending = "";
|
|
335
|
+
for await (const chunk of stream) {
|
|
336
|
+
const lines = (pending + chunk).split("\n");
|
|
337
|
+
pending = lines.pop()!;
|
|
338
|
+
yield* lines;
|
|
339
|
+
}
|
|
340
|
+
if (pending) yield pending;
|
|
341
|
+
}
|
|
342
|
+
|
|
294
343
|
async function* sessionWindowEntries(file: string, signal?: AbortSignal): AsyncGenerator<WindowedEntry> {
|
|
295
344
|
if (!existsSync(file)) return;
|
|
296
|
-
const stream = createReadStream(file, { encoding: "utf8", signal });
|
|
297
|
-
const lines = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
|
|
298
345
|
const windows = new Map<string, string>();
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
const item = toWindowedEntry(entry, windows);
|
|
308
|
-
if (item) yield item;
|
|
346
|
+
for await (const line of jsonlLines(file, signal)) {
|
|
347
|
+
let entry: EntryLike;
|
|
348
|
+
try {
|
|
349
|
+
entry = JSON.parse(line) as EntryLike;
|
|
350
|
+
} catch {
|
|
351
|
+
continue;
|
|
309
352
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
stream.destroy();
|
|
353
|
+
const item = toWindowedEntry(entry, windows);
|
|
354
|
+
if (item) yield item;
|
|
313
355
|
}
|
|
314
356
|
}
|
|
315
357
|
|
|
@@ -636,26 +678,13 @@ function unsupportedMessage(budget: Budget): string {
|
|
|
636
678
|
return `Posthorse: unsupported configuration. The model's context window (${n(budget.contextWindow)} tokens) minus Pi's compaction.reserveTokens (${n(budget.reserveTokens)}) leaves ${n(budget.usable)} usable tokens; Posthorse needs at least ${n(MIN_USABLE_TOKENS)}. Automatic rollover and checkpoint reminders are off for this model. Lower compaction.reserveTokens in Pi settings or use a larger-context model. new_context remains available with a model-aware handoff limit.`;
|
|
637
679
|
}
|
|
638
680
|
|
|
639
|
-
/**
|
|
640
|
-
function
|
|
681
|
+
/** Capacity before Pi's automatic line (or hard limit), including the margin for page metadata and refusals. */
|
|
682
|
+
function pageCapacity(ctx: NativeContext, toolTokens: number): number {
|
|
641
683
|
const usage = ctx.getContextUsage();
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
const budget = budgetFor(ctx, usage.contextWindow);
|
|
647
|
-
const line = budget?.enabled && budget.supported ? budget.rolloverAt : usage.contextWindow;
|
|
648
|
-
chars = Math.min(
|
|
649
|
-
MAX_HANDOFF_CHARS,
|
|
650
|
-
Math.max(0, line - usage.tokens - PAGE_MARGIN_TOKENS) * 4 - imageCount * ESTIMATED_IMAGE_CHARS,
|
|
651
|
-
);
|
|
652
|
-
}
|
|
653
|
-
if (chars < MIN_PAGE_CHARS) {
|
|
654
|
-
throw new Error(
|
|
655
|
-
`Too little context remains to read a page safely. Call new_context first, then retry with offset ${offset}.`,
|
|
656
|
-
);
|
|
657
|
-
}
|
|
658
|
-
return chars;
|
|
684
|
+
if (!usage || usage.tokens == null) return freshPayloadChars(ctx, toolTokens) / 4 + PAGE_MARGIN_TOKENS;
|
|
685
|
+
const budget = budgetFor(ctx, usage.contextWindow);
|
|
686
|
+
const line = budget?.enabled && budget.supported ? budget.rolloverAt : usage.contextWindow;
|
|
687
|
+
return line - usage.tokens;
|
|
659
688
|
}
|
|
660
689
|
|
|
661
690
|
function buildGuidance(ctx: NativeContext): string {
|
|
@@ -698,7 +727,42 @@ export default function (pi: ExtensionAPI) {
|
|
|
698
727
|
);
|
|
699
728
|
};
|
|
700
729
|
|
|
730
|
+
let pendingPageTokens = 0;
|
|
731
|
+
let previousPageUsage: number | null | undefined;
|
|
732
|
+
const pageSize = (ctx: NativeContext, offset: number, imageCount: number) => {
|
|
733
|
+
const usage = ctx.getContextUsage()?.tokens;
|
|
734
|
+
// Parallel siblings are not in native usage yet; sequential results must not be counted twice.
|
|
735
|
+
if (usage != null && previousPageUsage != null) {
|
|
736
|
+
pendingPageTokens = Math.max(0, pendingPageTokens - Math.max(0, usage - previousPageUsage));
|
|
737
|
+
}
|
|
738
|
+
previousPageUsage = usage;
|
|
739
|
+
const remaining = pageCapacity(ctx, activeToolTokens()) - pendingPageTokens;
|
|
740
|
+
const chars = Math.min(
|
|
741
|
+
MAX_HANDOFF_CHARS,
|
|
742
|
+
Math.max(0, remaining - PAGE_MARGIN_TOKENS) * 4 - imageCount * ESTIMATED_IMAGE_CHARS,
|
|
743
|
+
);
|
|
744
|
+
if (chars < MIN_PAGE_CHARS) {
|
|
745
|
+
const message = `Too little context remains to read a page safely. Call new_context first, then retry with offset ${offset}.`;
|
|
746
|
+
// Refusals consume context too. Once even the guidance no longer fits, omit repeated text;
|
|
747
|
+
// the failed call still carries its original offset and earlier refusals explain the retry.
|
|
748
|
+
const text = Math.ceil(message.length / 4) <= remaining ? message : "";
|
|
749
|
+
pendingPageTokens += Math.ceil(text.length / 4);
|
|
750
|
+
throw new Error(text);
|
|
751
|
+
}
|
|
752
|
+
return chars;
|
|
753
|
+
};
|
|
754
|
+
const pageResult = (text: string, images: ImageLike[], display: PosthorseDisplay) => {
|
|
755
|
+
pendingPageTokens += Math.ceil(text.length / 4) + images.length * (ESTIMATED_IMAGE_CHARS / 4);
|
|
756
|
+
return textResult(text, images, display);
|
|
757
|
+
};
|
|
758
|
+
pi.on("turn_start", () => {
|
|
759
|
+
pendingPageTokens = 0;
|
|
760
|
+
previousPageUsage = undefined;
|
|
761
|
+
});
|
|
762
|
+
|
|
701
763
|
pi.on("session_start", (_event, ctx) => {
|
|
764
|
+
pendingPageTokens = 0;
|
|
765
|
+
previousPageUsage = undefined;
|
|
702
766
|
nativeContext(ctx);
|
|
703
767
|
});
|
|
704
768
|
|
|
@@ -854,7 +918,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
854
918
|
label: "Notes",
|
|
855
919
|
...toolCards("notes"),
|
|
856
920
|
description:
|
|
857
|
-
"Persistent notes in .pi/notes/ that survive context resets. Ops: list, read (paged; pass offset to continue), write (create/replace; empty content clears), append, search (case-insensitive substring over note lines).
|
|
921
|
+
"Persistent notes in .pi/notes/ that survive context resets. Ops: list, read (paged; pass offset to continue), write (create/replace; empty content clears), append, search (case-insensitive substring over note lines). Git worktrees share notes: at the main checkout for conventional .git layouts, or in the common Git directory when metadata is stored separately. Old checkout-local notes are imported without overwriting shared notes.",
|
|
858
922
|
promptSnippet: "save and recall durable state that survives context resets",
|
|
859
923
|
promptGuidelines: [
|
|
860
924
|
"Use notes for durable state too large for a new_context handoff",
|
|
@@ -903,10 +967,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
903
967
|
if (offset && offset >= text.length) {
|
|
904
968
|
throw new Error(`Offset ${offset} is past the end of ${relative} (${text.length} chars).`);
|
|
905
969
|
}
|
|
906
|
-
const end = Math.min(text.length, offset +
|
|
970
|
+
const end = Math.min(text.length, offset + pageSize(nativeContext(ctx), offset, 0));
|
|
907
971
|
const more =
|
|
908
972
|
end < text.length ? `\n[chars ${offset}-${end} of ${text.length}; continue with offset ${end}]` : "";
|
|
909
|
-
return
|
|
973
|
+
return pageResult(`${text.slice(offset, end)}${more}`, [], { kind: "note-read", offset, end, total: text.length });
|
|
910
974
|
}
|
|
911
975
|
case "write": {
|
|
912
976
|
const relative = requireValue(params.path, "path", params.op);
|
|
@@ -914,7 +978,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
914
978
|
const path = safeJoin(relative);
|
|
915
979
|
mkdirSync(dirname(path), { recursive: true });
|
|
916
980
|
writeFileSync(path, params.content);
|
|
917
|
-
return textResult(`Wrote
|
|
981
|
+
return textResult(`Wrote ${path}`, [], { kind: "note-write" });
|
|
918
982
|
}
|
|
919
983
|
case "append": {
|
|
920
984
|
const relative = requireValue(params.path, "path", params.op);
|
|
@@ -927,7 +991,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
927
991
|
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
928
992
|
const separator = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
929
993
|
appendFileSync(path, `${separator}${content.replace(/\n?$/, "\n")}`);
|
|
930
|
-
return textResult(`Appended to
|
|
994
|
+
return textResult(`Appended to ${path}`, [], { kind: "note-append" });
|
|
931
995
|
}
|
|
932
996
|
case "search": {
|
|
933
997
|
const query = requireValue(params.query, "query", params.op).toLowerCase();
|
|
@@ -1019,12 +1083,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1019
1083
|
}
|
|
1020
1084
|
const end = Math.min(
|
|
1021
1085
|
item.text.length,
|
|
1022
|
-
offset +
|
|
1086
|
+
offset + pageSize(nativeContext(ctx), offset, offset === 0 ? item.images.length : 0),
|
|
1023
1087
|
);
|
|
1024
1088
|
const more = end < item.text.length ? `\nMore remains; call history read with id "${id}" and offset ${end}.` : "";
|
|
1025
1089
|
const header = `${source ? `${source} ` : ""}${item.entry.timestamp ?? ""} [window ${item.windowId}] [${id}] [chars ${offset}-${end} of ${item.text.length}] `;
|
|
1026
1090
|
// Stored images ride along with the first page only.
|
|
1027
|
-
return
|
|
1091
|
+
return pageResult(
|
|
1028
1092
|
`${header}${item.text.slice(offset, end)}${more}`,
|
|
1029
1093
|
offset === 0 ? item.images : [],
|
|
1030
1094
|
{ kind: "history-read", headerLength: header.length, offset, end, total: item.text.length },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-posthorse",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Posthorse: fresh context, same journey. Native no-summary context windows for the fitchmultz/pi fork, with sparse budget reminders, handoffs, notes, and history.",
|
|
6
6
|
"keywords": ["pi-package", "pi-extension", "context", "posthorse", "rollover"],
|