pi-posthorse 0.10.2 → 0.10.4
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 +7 -2
- package/index.ts +13 -3
- package/package.json +24 -21
- package/src/context-tools.ts +2 -2
- package/src/contracts.ts +39 -32
- package/src/files.ts +0 -5
- package/src/guidance.ts +41 -0
- package/src/history-entries.ts +16 -16
- package/src/history-pages.ts +1 -1
- package/src/history-search.ts +6 -6
- package/src/history.ts +5 -2
- package/src/note-discovery.ts +291 -0
- package/src/note-evidence.ts +179 -0
- package/src/note-guards.ts +203 -0
- package/src/note-inventory.ts +111 -0
- package/src/note-provenance.ts +299 -0
- package/src/note-scopes.ts +293 -0
- package/src/note-snapshots.ts +185 -0
- package/src/note-store.ts +278 -0
- package/src/note-tool-effects.ts +190 -0
- package/src/notes.ts +118 -137
- package/src/paging.ts +2 -2
- package/src/recovery-note-touches.ts +389 -0
- package/src/recovery-notes.ts +54 -503
- package/src/recovery-records.ts +90 -0
- package/src/recovery-results.ts +7 -6
- package/src/recovery.ts +24 -108
- package/src/reminders.ts +3 -3
- package/src/retention.ts +23 -17
- package/src/rollover.ts +66 -88
- package/src/session-files.ts +29 -20
- package/src/session-lines.ts +1 -2
- package/src/ui/cards.ts +3 -2
- package/src/ui/display.ts +2 -2
- package/src/ui/text.ts +0 -1
package/README.md
CHANGED
|
@@ -48,7 +48,12 @@ These tools are available to the model:
|
|
|
48
48
|
|
|
49
49
|
Expand a tool card with Ctrl+O. In fullscreen mode, click a tool card to expand it. A `new_context` card shows a request. Pi's `Compacted from … tokens` card confirms the rollover.
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
Forks get private copies of the notes used on their selected branch. The parent and fork can
|
|
52
|
+
update the same note name without overwriting each other. Project-wide lists and searches still
|
|
53
|
+
find both, with scope selectors for read-only comparisons.
|
|
54
|
+
|
|
55
|
+
See [tool examples and paging details](https://github.com/fitchmultz/pi-posthorse/blob/main/docs/reference.md#tools)
|
|
56
|
+
and [fork notes](https://github.com/fitchmultz/pi-posthorse/blob/main/docs/reference.md#forks-references-and-discovery).
|
|
52
57
|
|
|
53
58
|
## Settings and limits
|
|
54
59
|
|
|
@@ -80,7 +85,7 @@ Removal leaves notes and Pi session history in place. See the [installation refe
|
|
|
80
85
|
|
|
81
86
|
## Data and privacy
|
|
82
87
|
|
|
83
|
-
Posthorse makes no network requests. Notes are plaintext files under `.pi/notes
|
|
88
|
+
Posthorse makes no network requests. Notes are plaintext files under `.pi/notes/`. Linked Git worktrees share that store; forks keep private note files inside its `.forks/` directory. Notes for Pi-managed Git packages live outside their checkouts because `pi update` deletes untracked files there: global packages (including their descendants) use Pi's agent-directory `notes/`, and other project-local packages use the containing project's `.pi/package-notes/`, with project-local stores keyed by a full SHA-256 digest of the host and package path. Access existing checkout-local notes with this version before updating the package so they can migrate. Add `.pi/notes/` and `.pi/package-notes/` to the containing project's `.gitignore` if the notes must stay private.
|
|
84
89
|
|
|
85
90
|
History can include user messages, assistant thinking, tool arguments, errors, and images. Content you retrieve enters the context of your selected model and provider.
|
|
86
91
|
|
package/index.ts
CHANGED
|
@@ -15,6 +15,8 @@ import type { CompactionPolicy, PolicyReader } from "./src/contracts.ts";
|
|
|
15
15
|
import { registerHistory } from "./src/history.ts";
|
|
16
16
|
import { safeJsonStringify } from "./src/message-text.ts";
|
|
17
17
|
import { registerNotes } from "./src/notes.ts";
|
|
18
|
+
import { createNotesStore } from "./src/note-store.ts";
|
|
19
|
+
import { registerNoteGuards } from "./src/note-guards.ts";
|
|
18
20
|
import { PageReservations } from "./src/paging.ts";
|
|
19
21
|
import { registerRollover } from "./src/rollover.ts";
|
|
20
22
|
|
|
@@ -47,10 +49,18 @@ export function createPosthorse(getPolicy?: PolicyReader): (pi: ExtensionAPI) =>
|
|
|
47
49
|
const toolTokens = () => activeToolTokens(pi);
|
|
48
50
|
const pages = new PageReservations(toolTokens);
|
|
49
51
|
registerPosthorseMessages(pi);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
const notes = createNotesStore(pi);
|
|
53
|
+
registerRollover(pi, policy, toolTokens, {
|
|
54
|
+
resetPages: () => {
|
|
55
|
+
pages.reset();
|
|
56
|
+
},
|
|
57
|
+
notes: (ctx) => notes.recovery(ctx),
|
|
52
58
|
});
|
|
53
|
-
|
|
59
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
60
|
+
await notes.get(ctx);
|
|
61
|
+
});
|
|
62
|
+
registerNoteGuards(pi, notes);
|
|
63
|
+
registerNotes(pi, policy, pages, notes);
|
|
54
64
|
registerHistory(pi, policy, pages);
|
|
55
65
|
};
|
|
56
66
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-posthorse",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
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": [
|
|
@@ -31,25 +31,28 @@
|
|
|
31
31
|
"image": "https://raw.githubusercontent.com/fitchmultz/pi-posthorse/main/diagram.png"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
|
-
"check": "tsc",
|
|
35
|
-
"quality:
|
|
36
|
-
"quality:
|
|
37
|
-
"
|
|
38
|
-
"lint": "
|
|
39
|
-
"lint:
|
|
40
|
-
"lint:fix": "npm run quality:prepare && npm run quality:policy && oxlint --fix .",
|
|
34
|
+
"check": "tsc --noEmit",
|
|
35
|
+
"quality:policy": "node scripts/quality-policy.ts",
|
|
36
|
+
"quality:probes": "node --test test/quality*.test.ts",
|
|
37
|
+
"lint": "node scripts/verify.ts quality:policy quality:config lint:oxlint",
|
|
38
|
+
"lint:agent": "node scripts/verify.ts quality:policy quality:config lint:agent:oxlint",
|
|
39
|
+
"lint:fix": "node scripts/verify.ts quality:policy quality:config lint:fix:oxlint",
|
|
41
40
|
"format": "oxfmt --write .",
|
|
42
41
|
"format:check": "oxfmt --check .",
|
|
43
42
|
"build": "npm pack --dry-run",
|
|
44
|
-
"verify": "node scripts/toolchain-policy.ts &&
|
|
45
|
-
"check:compat": "
|
|
43
|
+
"verify": "node scripts/toolchain-policy.ts && node scripts/verify.ts quality:policy quality:config lint:oxlint format:check check test build",
|
|
44
|
+
"check:compat": "node scripts/verify.ts check test",
|
|
46
45
|
"test:native": "node --import ./test/pi-loader.ts --test test/host.test.mjs",
|
|
47
|
-
"test": "
|
|
48
|
-
"prepublishOnly": "npm run verify"
|
|
46
|
+
"test": "node --import ./test/pi-loader.ts --test test/*.test.ts test/host.test.mjs",
|
|
47
|
+
"prepublishOnly": "npm run verify",
|
|
48
|
+
"quality:config": "node scripts/config-policy.ts",
|
|
49
|
+
"lint:oxlint": "oxlint --deny-warnings .",
|
|
50
|
+
"lint:agent:oxlint": "oxlint --deny-warnings --format=agent .",
|
|
51
|
+
"lint:fix:oxlint": "oxlint --fix ."
|
|
49
52
|
},
|
|
50
53
|
"peerDependencies": {
|
|
51
|
-
"@earendil-works/pi-coding-agent": ">=1.
|
|
52
|
-
"@earendil-works/pi-tui": ">=1.
|
|
54
|
+
"@earendil-works/pi-coding-agent": ">=1.4.0",
|
|
55
|
+
"@earendil-works/pi-tui": ">=1.4.0",
|
|
53
56
|
"typebox": "*"
|
|
54
57
|
},
|
|
55
58
|
"peerDependenciesMeta": {
|
|
@@ -64,14 +67,14 @@
|
|
|
64
67
|
}
|
|
65
68
|
},
|
|
66
69
|
"devDependencies": {
|
|
67
|
-
"@earendil-works/pi-ai": "1.
|
|
68
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
69
|
-
"@earendil-works/pi-tui": "1.
|
|
70
|
+
"@earendil-works/pi-ai": "1.4.0",
|
|
71
|
+
"@earendil-works/pi-coding-agent": "1.4.0",
|
|
72
|
+
"@earendil-works/pi-tui": "1.4.0",
|
|
70
73
|
"@types/node": "24.19.1",
|
|
71
|
-
"oxc-parser": "0.
|
|
72
|
-
"oxfmt": "0.
|
|
73
|
-
"oxlint": "1.
|
|
74
|
-
"oxlint-tsgolint": "7.0.
|
|
74
|
+
"oxc-parser": "0.156.0",
|
|
75
|
+
"oxfmt": "0.75.0",
|
|
76
|
+
"oxlint": "1.90.0",
|
|
77
|
+
"oxlint-tsgolint": "7.0.2005",
|
|
75
78
|
"typebox": "1.3.27",
|
|
76
79
|
"typescript": "7.0.2",
|
|
77
80
|
"yaml": "2.9.1"
|
package/src/context-tools.ts
CHANGED
|
@@ -71,7 +71,7 @@ export function registerContextTools(
|
|
|
71
71
|
),
|
|
72
72
|
}),
|
|
73
73
|
// Pi's fixed tool contract supplies id, params, signal, updates, and context.
|
|
74
|
-
// oxlint-disable-next-line max-params
|
|
74
|
+
// oxlint-disable-next-line max-params -- QX-002: Pi supplies five fixed positional tool arguments.
|
|
75
75
|
async execute(id, { handoff }, _signal, _onUpdate, ctx) {
|
|
76
76
|
const trimmed = handoff?.trim();
|
|
77
77
|
const limit = freshPayloadChars(policy(ctx), activeToolTokens());
|
|
@@ -99,7 +99,7 @@ export function registerContextTools(
|
|
|
99
99
|
"verify remaining context before reporting a token count or changing plans because of context limits",
|
|
100
100
|
parameters: Type.Object({}),
|
|
101
101
|
// Pi's fixed tool contract supplies id, params, signal, updates, and context.
|
|
102
|
-
// oxlint-disable-next-line max-params
|
|
102
|
+
// oxlint-disable-next-line max-params -- QX-002: Pi supplies five fixed positional tool arguments.
|
|
103
103
|
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
104
104
|
return contextReport(policy(ctx));
|
|
105
105
|
},
|
package/src/contracts.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { PosthorseDisplay } from "../ui.ts";
|
|
|
4
4
|
export type CompactionPolicy = {
|
|
5
5
|
readonly enabled: boolean;
|
|
6
6
|
readonly reserveTokens: number;
|
|
7
|
-
readonly keepRecentTokens?: number;
|
|
7
|
+
readonly keepRecentTokens?: number | undefined;
|
|
8
8
|
};
|
|
9
9
|
export type ContextUsage = {
|
|
10
10
|
readonly tokens: number | null;
|
|
@@ -17,52 +17,59 @@ export type PolicyContext = {
|
|
|
17
17
|
readonly getSystemPrompt: () => string;
|
|
18
18
|
};
|
|
19
19
|
export type PolicyReader = (ctx: ExtensionContext) => CompactionPolicy;
|
|
20
|
+
export type SnapshotReader = (
|
|
21
|
+
ctx: ExtensionContext,
|
|
22
|
+
livePolicy?: CompactionPolicy,
|
|
23
|
+
) => PolicyContext;
|
|
20
24
|
export type ImageLike = {
|
|
21
25
|
readonly type: "image";
|
|
22
26
|
readonly data: string;
|
|
23
27
|
readonly mimeType: string;
|
|
24
28
|
};
|
|
25
29
|
export type ToolCallLike = {
|
|
26
|
-
readonly type?: string;
|
|
27
|
-
readonly id?: string;
|
|
28
|
-
readonly name?: string;
|
|
29
|
-
readonly namespace?: string;
|
|
30
|
+
readonly type?: string | undefined;
|
|
31
|
+
readonly id?: string | undefined;
|
|
32
|
+
readonly name?: string | undefined;
|
|
33
|
+
readonly namespace?: string | undefined;
|
|
30
34
|
readonly arguments?: unknown;
|
|
31
35
|
readonly executionArguments?: unknown;
|
|
32
|
-
readonly async?: boolean;
|
|
36
|
+
readonly async?: boolean | undefined;
|
|
33
37
|
};
|
|
34
38
|
export type MessageLike = {
|
|
35
|
-
readonly role?: string;
|
|
36
|
-
readonly stopReason?: string;
|
|
37
|
-
readonly errorMessage?: string;
|
|
39
|
+
readonly role?: string | undefined;
|
|
40
|
+
readonly stopReason?: string | undefined;
|
|
41
|
+
readonly errorMessage?: string | undefined;
|
|
38
42
|
readonly content?: unknown;
|
|
39
|
-
readonly summary?: string;
|
|
40
|
-
readonly toolName?: string;
|
|
41
|
-
readonly customType?: string;
|
|
42
|
-
readonly namespace?: string;
|
|
43
|
-
readonly toolCallId?: string;
|
|
44
|
-
readonly isError?: boolean;
|
|
43
|
+
readonly summary?: string | undefined;
|
|
44
|
+
readonly toolName?: string | undefined;
|
|
45
|
+
readonly customType?: string | undefined;
|
|
46
|
+
readonly namespace?: string | undefined;
|
|
47
|
+
readonly toolCallId?: string | undefined;
|
|
48
|
+
readonly isError?: boolean | undefined;
|
|
45
49
|
readonly details?: unknown;
|
|
46
50
|
readonly nestedCalls?: unknown;
|
|
47
|
-
readonly command?: string;
|
|
48
|
-
readonly output?: string;
|
|
49
|
-
readonly excludeFromContext?: boolean;
|
|
51
|
+
readonly command?: string | undefined;
|
|
52
|
+
readonly output?: string | undefined;
|
|
53
|
+
readonly excludeFromContext?: boolean | undefined;
|
|
50
54
|
};
|
|
51
55
|
export type EntryLike = {
|
|
52
|
-
readonly type?: string;
|
|
53
|
-
readonly id?: string;
|
|
54
|
-
readonly parentId?: string | null;
|
|
55
|
-
readonly timestamp?: string;
|
|
56
|
-
readonly message?: MessageLike;
|
|
57
|
-
readonly summary?: string;
|
|
58
|
-
readonly customType?: string;
|
|
56
|
+
readonly type?: string | undefined;
|
|
57
|
+
readonly id?: string | undefined;
|
|
58
|
+
readonly parentId?: string | null | undefined;
|
|
59
|
+
readonly timestamp?: string | undefined;
|
|
60
|
+
readonly message?: MessageLike | undefined;
|
|
61
|
+
readonly summary?: string | undefined;
|
|
62
|
+
readonly customType?: string | undefined;
|
|
59
63
|
readonly content?: unknown;
|
|
60
64
|
readonly details?: unknown;
|
|
61
|
-
readonly display?: boolean;
|
|
62
|
-
readonly
|
|
63
|
-
readonly
|
|
64
|
-
readonly
|
|
65
|
-
readonly
|
|
65
|
+
readonly display?: boolean | undefined;
|
|
66
|
+
readonly data?: unknown;
|
|
67
|
+
readonly cwd?: string | undefined;
|
|
68
|
+
readonly parentSession?: string | undefined;
|
|
69
|
+
readonly handoff?: string | undefined;
|
|
70
|
+
readonly firstKeptEntryId?: string | null | undefined;
|
|
71
|
+
readonly targetId?: string | undefined;
|
|
72
|
+
readonly replacement?: { readonly content: unknown } | null | undefined;
|
|
66
73
|
};
|
|
67
74
|
export type WindowedEntry = {
|
|
68
75
|
readonly entry: EntryLike;
|
|
@@ -82,8 +89,8 @@ export type HistoryHit = {
|
|
|
82
89
|
};
|
|
83
90
|
export type ReminderFingerprint = {
|
|
84
91
|
readonly windowId: string;
|
|
85
|
-
readonly contextWindow?: number;
|
|
86
|
-
readonly reserveTokens?: number;
|
|
92
|
+
readonly contextWindow?: number | undefined;
|
|
93
|
+
readonly reserveTokens?: number | undefined;
|
|
87
94
|
};
|
|
88
95
|
export type TextResult = {
|
|
89
96
|
content: Array<
|
package/src/files.ts
CHANGED
|
@@ -45,22 +45,18 @@ export async function resolveFileTarget(absolutePath: string): Promise<string> {
|
|
|
45
45
|
if (target.endsWith(sep) || target.endsWith("/")) {
|
|
46
46
|
// Trailing separators require an existing directory, never a new regular file.
|
|
47
47
|
// Resolution follows dependent links in filesystem order.
|
|
48
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
49
48
|
await stat(target);
|
|
50
49
|
return realpath(target);
|
|
51
50
|
}
|
|
52
51
|
const parentInput = dirname(target);
|
|
53
52
|
// stat enforces regular-file/.. traversal that macOS realpath alone accepts.
|
|
54
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
55
53
|
if (!(await stat(parentInput)).isDirectory()) {
|
|
56
54
|
throw Object.assign(new Error(`Not a directory: ${parentInput}`), { code: "ENOTDIR" });
|
|
57
55
|
}
|
|
58
56
|
// Each parent must resolve before its child link can be examined.
|
|
59
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
60
57
|
const parent = await realpath(parentInput);
|
|
61
58
|
target = join(parent, basename(target));
|
|
62
59
|
// Link targets cannot be known before examining the current link.
|
|
63
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
64
60
|
const info = await lstatOrMissing(target);
|
|
65
61
|
if (info === undefined) {
|
|
66
62
|
return target;
|
|
@@ -73,7 +69,6 @@ export async function resolveFileTarget(absolutePath: string): Promise<string> {
|
|
|
73
69
|
}
|
|
74
70
|
links.add(target);
|
|
75
71
|
// Preserve .. until the preceding directory symlink has been followed.
|
|
76
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
77
72
|
const link = await readlink(target);
|
|
78
73
|
target = isAbsolute(link) ? link : `${parent}${sep}${link}`;
|
|
79
74
|
}
|
package/src/guidance.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { buildGuidance } from "./budget.ts";
|
|
3
|
+
import type { SnapshotReader } from "./contracts.ts";
|
|
4
|
+
|
|
5
|
+
export function registerGuidance(pi: ExtensionAPI, policy: SnapshotReader): void {
|
|
6
|
+
let promptOptions: { readonly forceSystemPrompt?: string } | undefined;
|
|
7
|
+
pi.on("before_agent_start", (event) => {
|
|
8
|
+
promptOptions = event.systemPromptOptions;
|
|
9
|
+
});
|
|
10
|
+
pi.on("agent_settled", () => {
|
|
11
|
+
// Prompt options belong to one run, never to later idle context evaluation.
|
|
12
|
+
promptOptions = undefined;
|
|
13
|
+
});
|
|
14
|
+
pi.on("context_with_system", (event, ctx) => {
|
|
15
|
+
const guidance = buildGuidance(policy(ctx));
|
|
16
|
+
const system =
|
|
17
|
+
event.messages.findLast(
|
|
18
|
+
(message) =>
|
|
19
|
+
message.role === "system" && typeof message.sections?.posthorse === "string",
|
|
20
|
+
) ?? event.messages.find((message) => message.role === "system");
|
|
21
|
+
if (system === undefined || system.role !== "system") {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (promptOptions?.forceSystemPrompt !== undefined) {
|
|
25
|
+
// ponytail: Pi applies forced prompts after this hook; keep request-local guidance
|
|
26
|
+
// until the host projects forced text before context_with_system.
|
|
27
|
+
event.messages.splice(1, 0, {
|
|
28
|
+
role: "custom",
|
|
29
|
+
customType: "posthorse-guidance",
|
|
30
|
+
content: guidance,
|
|
31
|
+
display: false,
|
|
32
|
+
timestamp: 0,
|
|
33
|
+
});
|
|
34
|
+
} else {
|
|
35
|
+
system.sections = {
|
|
36
|
+
...system.sections,
|
|
37
|
+
posthorse: `<posthorse>\n${guidance}\n</posthorse>`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
package/src/history-entries.ts
CHANGED
|
@@ -36,10 +36,10 @@ export function contextStart(entries: readonly EntryLike[]): {
|
|
|
36
36
|
} {
|
|
37
37
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
38
38
|
const entry = entries[i];
|
|
39
|
-
if (entry
|
|
39
|
+
if (entry?.type === "context_window") {
|
|
40
40
|
return { start: i + 1, boundary: entry };
|
|
41
41
|
}
|
|
42
|
-
if (entry
|
|
42
|
+
if (entry?.type === "compaction") {
|
|
43
43
|
const kept = entries.findIndex((candidate) => candidate.id === entry.firstKeptEntryId);
|
|
44
44
|
return { start: kept >= 0 ? kept : i + 1, boundary: entry };
|
|
45
45
|
}
|
|
@@ -101,27 +101,26 @@ function contentText(content: unknown): string {
|
|
|
101
101
|
.join("\n");
|
|
102
102
|
}
|
|
103
103
|
function ownAssistantText(message: MessageLike, images: WindowedEntry["images"]): string[] {
|
|
104
|
-
const originals = [
|
|
104
|
+
const originals: string[] = [];
|
|
105
|
+
let current = "";
|
|
105
106
|
for (const part of unknownArray(message.content) ?? []) {
|
|
106
107
|
if (isRecoveryCall(part)) {
|
|
107
|
-
originals.push(
|
|
108
|
+
originals.push(current);
|
|
109
|
+
current = "";
|
|
108
110
|
} else {
|
|
109
111
|
const partText = textOf({ content: [part] });
|
|
110
|
-
const index = originals.length - 1;
|
|
111
112
|
if (partText !== "") {
|
|
112
|
-
|
|
113
|
+
current = current === "" ? partText : `${current}\n${partText}`;
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
|
-
const
|
|
117
|
+
const last = [current, assistantFailure(message), imageSummary(images)]
|
|
117
118
|
.filter((part) => part !== "")
|
|
118
119
|
.join("\n");
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (originals[0] !== "") {
|
|
124
|
-
originals[0] = `[assistant] ${originals[0]}`;
|
|
120
|
+
originals.push(last);
|
|
121
|
+
const first = originals[0] ?? "";
|
|
122
|
+
if (first !== "") {
|
|
123
|
+
originals[0] = `[assistant] ${first}`;
|
|
125
124
|
}
|
|
126
125
|
return originals;
|
|
127
126
|
}
|
|
@@ -228,9 +227,10 @@ export function windowProjection(): (entry: EntryLike) => string {
|
|
|
228
227
|
export function* reverseWindowEntries(entries: readonly EntryLike[]): Generator<WindowedEntry> {
|
|
229
228
|
// Preserve parent-relative window labels without normalizing the whole branch.
|
|
230
229
|
// ponytail: ancestry still scans metadata; a native window-aware iterator could remove that cost.
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
230
|
+
const project = windowProjection();
|
|
231
|
+
const windows = entries.map((entry) => ({ entry, windowId: project(entry) }));
|
|
232
|
+
for (const { entry, windowId } of windows.reverse()) {
|
|
233
|
+
const item = toWindowedEntry(entry, windowId);
|
|
234
234
|
if (item !== undefined) {
|
|
235
235
|
yield item;
|
|
236
236
|
}
|
package/src/history-pages.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { PageAccess } from "./paging.ts";
|
|
|
13
13
|
type SearchPage = {
|
|
14
14
|
readonly parts: readonly string[];
|
|
15
15
|
readonly spans: readonly { readonly headerLength: number; readonly length: number }[];
|
|
16
|
-
readonly next?: string;
|
|
16
|
+
readonly next?: string | undefined;
|
|
17
17
|
};
|
|
18
18
|
function cursorFor(matches: HistoryMatches, hit: HistoryHit, offset: number): string {
|
|
19
19
|
return Buffer.from(JSON.stringify([hit.id, hit.priority, offset, matches.key])).toString(
|
package/src/history-search.ts
CHANGED
|
@@ -30,7 +30,7 @@ export type HistoryParams = {
|
|
|
30
30
|
export type HistorySource = {
|
|
31
31
|
readonly dir: string;
|
|
32
32
|
readonly cwd: string;
|
|
33
|
-
readonly currentFile?: string;
|
|
33
|
+
readonly currentFile?: string | undefined;
|
|
34
34
|
readonly branch: () => readonly EntryLike[];
|
|
35
35
|
readonly projection: () => readonly ProjectedEntry[];
|
|
36
36
|
};
|
|
@@ -38,7 +38,7 @@ export type HistoryCursor = readonly [id: string, priority: 0 | 1, offset: numbe
|
|
|
38
38
|
export type HistoryMatches = {
|
|
39
39
|
readonly hits: readonly HistoryHit[];
|
|
40
40
|
readonly skipped: number;
|
|
41
|
-
readonly cursor?: HistoryCursor;
|
|
41
|
+
readonly cursor?: HistoryCursor | undefined;
|
|
42
42
|
readonly key: string;
|
|
43
43
|
readonly limit: number;
|
|
44
44
|
};
|
|
@@ -109,7 +109,7 @@ function contextVisibility(project: HistorySource["projection"]): (entry: EntryL
|
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
111
|
type SearchProgress = {
|
|
112
|
-
readonly cursor?: HistoryCursor;
|
|
112
|
+
readonly cursor?: HistoryCursor | undefined;
|
|
113
113
|
readonly limit: number;
|
|
114
114
|
readonly add: (hit: HistoryHit) => void;
|
|
115
115
|
readonly seeking: (priority: 0 | 1) => boolean;
|
|
@@ -201,8 +201,8 @@ async function scanArchive(
|
|
|
201
201
|
options: {
|
|
202
202
|
readonly source: string;
|
|
203
203
|
readonly query: string;
|
|
204
|
-
readonly signal?: AbortSignal;
|
|
205
|
-
readonly wanted?: LineMatcher;
|
|
204
|
+
readonly signal?: AbortSignal | undefined;
|
|
205
|
+
readonly wanted?: LineMatcher | undefined;
|
|
206
206
|
readonly inContext: (entry: EntryLike) => boolean;
|
|
207
207
|
},
|
|
208
208
|
accumulator: SearchProgress,
|
|
@@ -231,7 +231,7 @@ async function archivedMatches(
|
|
|
231
231
|
source: HistorySource,
|
|
232
232
|
search: {
|
|
233
233
|
readonly query: string;
|
|
234
|
-
readonly signal?: AbortSignal;
|
|
234
|
+
readonly signal?: AbortSignal | undefined;
|
|
235
235
|
readonly inContext: (entry: EntryLike) => boolean;
|
|
236
236
|
},
|
|
237
237
|
accumulator: SearchProgress,
|
package/src/history.ts
CHANGED
|
@@ -10,7 +10,10 @@ import type { PageAccess } from "./paging.ts";
|
|
|
10
10
|
import { scopedSessionFiles, sessionWindowEntries } from "./session-files.ts";
|
|
11
11
|
|
|
12
12
|
type LocatedEntry = { readonly item: WindowedEntry; readonly source: string };
|
|
13
|
-
function entryReference(id: string): {
|
|
13
|
+
function entryReference(id: string): {
|
|
14
|
+
readonly entryId: string;
|
|
15
|
+
readonly fileKey?: string | undefined;
|
|
16
|
+
} {
|
|
14
17
|
const separator = id.indexOf("@");
|
|
15
18
|
return {
|
|
16
19
|
entryId: separator < 0 ? id : id.slice(0, separator),
|
|
@@ -125,7 +128,7 @@ export function registerHistory(
|
|
|
125
128
|
),
|
|
126
129
|
}),
|
|
127
130
|
// Pi's fixed tool contract supplies id, params, signal, updates, and context.
|
|
128
|
-
// oxlint-disable-next-line max-params
|
|
131
|
+
// oxlint-disable-next-line max-params -- QX-002: Pi supplies five fixed positional tool arguments.
|
|
129
132
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
130
133
|
const source = historySource(ctx);
|
|
131
134
|
if (params.op === "search") {
|