pi-posthorse 0.10.1 → 0.10.3
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 +19 -16
- package/src/context-tools.ts +2 -2
- package/src/contracts.ts +38 -31
- 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/notes.ts +1 -4
- package/src/paging.ts +2 -2
- package/src/recovery-note-touches.ts +436 -0
- package/src/recovery-notes.ts +200 -0
- package/src/recovery-records.ts +90 -0
- package/src/recovery-results.ts +7 -6
- package/src/recovery.ts +95 -105
- package/src/reminders.ts +102 -4
- package/src/retention.ts +20 -17
- package/src/rollover.ts +91 -88
- package/src/session-files.ts +22 -18
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-posthorse",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
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,21 +31,24 @@
|
|
|
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
54
|
"@earendil-works/pi-coding-agent": ">=1.3.0",
|
|
@@ -68,10 +71,10 @@
|
|
|
68
71
|
"@earendil-works/pi-coding-agent": "1.3.0",
|
|
69
72
|
"@earendil-works/pi-tui": "1.3.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,49 +17,56 @@ 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
|
|
42
|
-
readonly
|
|
43
|
-
readonly
|
|
44
|
-
readonly
|
|
45
|
-
readonly
|
|
46
|
-
readonly
|
|
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;
|
|
49
|
+
readonly details?: unknown;
|
|
50
|
+
readonly nestedCalls?: unknown;
|
|
51
|
+
readonly command?: string | undefined;
|
|
52
|
+
readonly output?: string | undefined;
|
|
53
|
+
readonly excludeFromContext?: boolean | undefined;
|
|
47
54
|
};
|
|
48
55
|
export type EntryLike = {
|
|
49
|
-
readonly type?: string;
|
|
50
|
-
readonly id?: string;
|
|
51
|
-
readonly parentId?: string | null;
|
|
52
|
-
readonly timestamp?: string;
|
|
53
|
-
readonly message?: MessageLike;
|
|
54
|
-
readonly summary?: string;
|
|
55
|
-
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;
|
|
56
63
|
readonly content?: unknown;
|
|
57
64
|
readonly details?: unknown;
|
|
58
|
-
readonly display?: boolean;
|
|
59
|
-
readonly handoff?: string;
|
|
60
|
-
readonly firstKeptEntryId?: string | null;
|
|
61
|
-
readonly targetId?: string;
|
|
62
|
-
readonly replacement?: { readonly content: unknown } | null;
|
|
65
|
+
readonly display?: boolean | undefined;
|
|
66
|
+
readonly handoff?: string | undefined;
|
|
67
|
+
readonly firstKeptEntryId?: string | null | undefined;
|
|
68
|
+
readonly targetId?: string | undefined;
|
|
69
|
+
readonly replacement?: { readonly content: unknown } | null | undefined;
|
|
63
70
|
};
|
|
64
71
|
export type WindowedEntry = {
|
|
65
72
|
readonly entry: EntryLike;
|
|
@@ -79,8 +86,8 @@ export type HistoryHit = {
|
|
|
79
86
|
};
|
|
80
87
|
export type ReminderFingerprint = {
|
|
81
88
|
readonly windowId: string;
|
|
82
|
-
readonly contextWindow?: number;
|
|
83
|
-
readonly reserveTokens?: number;
|
|
89
|
+
readonly contextWindow?: number | undefined;
|
|
90
|
+
readonly reserveTokens?: number | undefined;
|
|
84
91
|
};
|
|
85
92
|
export type TextResult = {
|
|
86
93
|
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") {
|
package/src/notes.ts
CHANGED
|
@@ -84,7 +84,6 @@ async function* folderEntries(directory: string, signal: AbortSignal | undefined
|
|
|
84
84
|
.slice(0, firstFolder === -1 ? batch.length : firstFolder + 1)
|
|
85
85
|
.map((entry) => join(directory, entry.name));
|
|
86
86
|
// Stats overlap only up to the next entry that may be a folder.
|
|
87
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
88
87
|
const infos = await Promise.all(paths.map(statIfPresent));
|
|
89
88
|
for (const [index, path] of paths.entries()) {
|
|
90
89
|
start += 1;
|
|
@@ -148,7 +147,6 @@ async function listNotes(
|
|
|
148
147
|
const rows: string[] = [];
|
|
149
148
|
for (let start = 0; start < files.length; start += FORMAT_BATCH) {
|
|
150
149
|
// Pi gets an event-loop turn before each batch of rows.
|
|
151
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
152
150
|
await setImmediate();
|
|
153
151
|
signal?.throwIfAborted();
|
|
154
152
|
for (const file of files.slice(start, start + FORMAT_BATCH)) {
|
|
@@ -274,7 +272,6 @@ async function noteHits(
|
|
|
274
272
|
// A batch of notes over 1 MiB reads nothing that would notice cancellation.
|
|
275
273
|
signal?.throwIfAborted();
|
|
276
274
|
// Batches keep note order, so a search stops within one batch of the page limit.
|
|
277
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
278
275
|
const batch = await Promise.all(
|
|
279
276
|
notes
|
|
280
277
|
.slice(start, start + SEARCH_BATCH)
|
|
@@ -364,7 +361,7 @@ export function registerNotes(
|
|
|
364
361
|
),
|
|
365
362
|
}),
|
|
366
363
|
// Pi's fixed tool contract supplies id, params, signal, updates, and context.
|
|
367
|
-
// oxlint-disable-next-line max-params
|
|
364
|
+
// oxlint-disable-next-line max-params -- QX-002: Pi supplies five fixed positional tool arguments.
|
|
368
365
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
369
366
|
const dir = notesDir(ctx.cwd);
|
|
370
367
|
const access: NotesAccess = { host: () => policy(ctx), pages, signal };
|
package/src/paging.ts
CHANGED
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
export type PageRequest = {
|
|
15
15
|
readonly offset: number;
|
|
16
16
|
readonly imageCount?: number;
|
|
17
|
-
readonly cursor?: string;
|
|
18
|
-
readonly imageOffset?: number;
|
|
17
|
+
readonly cursor?: string | undefined;
|
|
18
|
+
readonly imageOffset?: number | undefined;
|
|
19
19
|
};
|
|
20
20
|
export type NotesPage = {
|
|
21
21
|
readonly rows: readonly string[];
|