@howaboua/pi-notes-compaction 0.0.4 → 0.0.5
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/CHANGELOG.md +11 -0
- package/package.json +1 -1
- package/src/bridge.ts +3 -0
- package/src/index.ts +34 -4
- package/src/lifecycle.ts +1 -0
- package/src/settings-screen.ts +73 -16
- package/src/settings.ts +17 -1
- package/src/task-boundary.ts +32 -0
- package/src/tools.ts +10 -1
- package/src/windows.ts +59 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @howaboua/pi-notes-compaction
|
|
2
2
|
|
|
3
|
+
## 0.0.5
|
|
4
|
+
|
|
5
|
+
- We've been informed that Astra and Sol currently retain their prompt caches for around an hour. If you use notes-managed idle rollover, consider increasing it from 25 to 55 minutes. Cache retention can vary. YMMV. We'll keep monitoring. Please share your cache-retention and long-context quota observations in [the feedback thread](https://github.com/IgorWarzocha/howaboua-pi-stuff/issues/508).
|
|
6
|
+
|
|
7
|
+
Added configurable task-boundary reminders and a clearer settings layout.
|
|
8
|
+
|
|
9
|
+
- Added optional reminders at preset or custom token counts when context capacity exceeds 272k, showing the remaining percentage.
|
|
10
|
+
- Status text and keyboard hints now wrap to fit narrow terminals.
|
|
11
|
+
- Notes help now lists required arguments. Writes accept `content` when `text` is omitted.
|
|
12
|
+
- Fixed fresh worker startup with Notes enabled.
|
|
13
|
+
|
|
3
14
|
## 0.0.4
|
|
4
15
|
|
|
5
16
|
- Fixed note queries and manual context rollover with Codex Conversion.
|
package/package.json
CHANGED
package/src/bridge.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type {
|
|
3
3
|
AgentToolResult,
|
|
4
|
+
CustomMessageEntryDraft,
|
|
4
5
|
ExtensionAPI,
|
|
5
6
|
ExtensionContext,
|
|
6
7
|
SessionBeforeCompactEvent,
|
|
@@ -86,6 +87,8 @@ export interface NotesOwner {
|
|
|
86
87
|
|
|
87
88
|
export interface NotesBridge {
|
|
88
89
|
protocol: 1;
|
|
90
|
+
/** Preserve the owner's chronological entry while enabling developer-role delivery. */
|
|
91
|
+
prepareReminder?(entry: CustomMessageEntryDraft): CustomMessageEntryDraft;
|
|
89
92
|
/** Synchronous atomic handoff. No legacy registrations or lifecycle owners remain active. */
|
|
90
93
|
claim(owner: NotesOwner):
|
|
91
94
|
| {
|
package/src/index.ts
CHANGED
|
@@ -54,6 +54,15 @@ export default function notesCompaction(pi: ExtensionAPI): void {
|
|
|
54
54
|
entry.customType === "@howaboua/pi-stuff/changelog"
|
|
55
55
|
)
|
|
56
56
|
return true;
|
|
57
|
+
// PCC links the fresh worker before binding its shared notes family.
|
|
58
|
+
if (
|
|
59
|
+
entry.type === "custom" &&
|
|
60
|
+
entry.customType === "codex-agent-relationship"
|
|
61
|
+
)
|
|
62
|
+
return (
|
|
63
|
+
(entry.data as { threadId?: unknown } | undefined)?.threadId ===
|
|
64
|
+
ctx.sessionManager.getSessionId()
|
|
65
|
+
);
|
|
57
66
|
// Herdr activation persists its empty registry before first task delivery.
|
|
58
67
|
if (
|
|
59
68
|
entry.type === "custom" &&
|
|
@@ -168,10 +177,29 @@ export default function notesCompaction(pi: ExtensionAPI): void {
|
|
|
168
177
|
);
|
|
169
178
|
}
|
|
170
179
|
});
|
|
171
|
-
pi.on("turn_end", (
|
|
172
|
-
if (!lifecycle.active
|
|
173
|
-
const reminder = lifecycle.windows.reminder(
|
|
174
|
-
|
|
180
|
+
pi.on("turn_end", (event, ctx) => {
|
|
181
|
+
if (!lifecycle.active) return;
|
|
182
|
+
const reminder = lifecycle.windows.reminder(
|
|
183
|
+
ctx,
|
|
184
|
+
event.message.role === "assistant" &&
|
|
185
|
+
!ctx.signal?.aborted &&
|
|
186
|
+
!["error", "length", "aborted"].includes(event.message.stopReason) &&
|
|
187
|
+
event.toolResults.length > 0 &&
|
|
188
|
+
pi.getActiveTools().includes("new_context")
|
|
189
|
+
? lifecycle.taskBoundaryReminder
|
|
190
|
+
: 0,
|
|
191
|
+
!lifecycle.fresh(ctx, lifecycle.runId),
|
|
192
|
+
);
|
|
193
|
+
return reminder
|
|
194
|
+
? {
|
|
195
|
+
entries: [
|
|
196
|
+
...event.entries,
|
|
197
|
+
windowDetails(reminder)?.kind === "task-boundary"
|
|
198
|
+
? (lifecycle.bridge?.prepareReminder?.(reminder) ?? reminder)
|
|
199
|
+
: reminder,
|
|
200
|
+
],
|
|
201
|
+
}
|
|
202
|
+
: undefined;
|
|
175
203
|
});
|
|
176
204
|
pi.on("context", (event, ctx) => {
|
|
177
205
|
if (!lifecycle.active) return;
|
|
@@ -240,8 +268,10 @@ export default function notesCompaction(pi: ExtensionAPI): void {
|
|
|
240
268
|
settings: () => ({
|
|
241
269
|
normalCompaction: lifecycle.normalCompaction,
|
|
242
270
|
idleMinutes: lifecycle.idleMinutes,
|
|
271
|
+
taskBoundaryReminder: lifecycle.taskBoundaryReminder,
|
|
243
272
|
}),
|
|
244
273
|
save,
|
|
274
|
+
settingsPath,
|
|
245
275
|
prune,
|
|
246
276
|
status: () => {
|
|
247
277
|
const counts = store.status();
|
package/src/lifecycle.ts
CHANGED
package/src/settings-screen.ts
CHANGED
|
@@ -2,13 +2,19 @@ import {
|
|
|
2
2
|
type ExtensionContext,
|
|
3
3
|
getSettingsListTheme,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
SettingsList,
|
|
7
|
+
truncateToWidth,
|
|
8
|
+
wrapTextWithAnsi,
|
|
9
|
+
} from "@earendil-works/pi-tui";
|
|
6
10
|
import { IDLE_MINUTES, type NotesSettings } from "./settings.js";
|
|
11
|
+
import { TASK_BOUNDARY_PRESETS, taskBoundaryLabel } from "./task-boundary.js";
|
|
7
12
|
|
|
8
13
|
export async function openNotesSettings(
|
|
9
14
|
ctx: ExtensionContext,
|
|
10
15
|
options: {
|
|
11
16
|
settings: () => NotesSettings;
|
|
17
|
+
settingsPath: string;
|
|
12
18
|
save: (settings: NotesSettings) => void;
|
|
13
19
|
status: () => string[];
|
|
14
20
|
prune: () => Promise<void>;
|
|
@@ -31,6 +37,17 @@ export async function openNotesSettings(
|
|
|
31
37
|
currentValue: settings.normalCompaction ? "on" : "off",
|
|
32
38
|
values: ["off", "on"],
|
|
33
39
|
},
|
|
40
|
+
{
|
|
41
|
+
id: "taskBoundaryReminder",
|
|
42
|
+
label: "Task-boundary reminder",
|
|
43
|
+
description:
|
|
44
|
+
"Suggest a separate-task rollover after this many used tokens, only with active capacity above 272k. Custom: edit taskBoundaryReminder in the notes settings JSON (whole tokens); 0 is off.",
|
|
45
|
+
currentValue: taskBoundaryLabel(settings.taskBoundaryReminder),
|
|
46
|
+
values: [
|
|
47
|
+
...TASK_BOUNDARY_PRESETS.map(taskBoundaryLabel),
|
|
48
|
+
"custom (edit JSON)",
|
|
49
|
+
],
|
|
50
|
+
},
|
|
34
51
|
{
|
|
35
52
|
id: "idle",
|
|
36
53
|
label: "Idle rollover",
|
|
@@ -56,9 +73,31 @@ export async function openNotesSettings(
|
|
|
56
73
|
return;
|
|
57
74
|
}
|
|
58
75
|
const current = options.settings();
|
|
76
|
+
if (
|
|
77
|
+
id === "taskBoundaryReminder" &&
|
|
78
|
+
value === "custom (edit JSON)"
|
|
79
|
+
) {
|
|
80
|
+
ctx.ui.notify(
|
|
81
|
+
`Set taskBoundaryReminder to a positive whole token count in ${options.settingsPath}, then reload Pi. 0 is off.`,
|
|
82
|
+
"info",
|
|
83
|
+
);
|
|
84
|
+
list.updateValue(
|
|
85
|
+
id,
|
|
86
|
+
taskBoundaryLabel(current.taskBoundaryReminder),
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
59
90
|
try {
|
|
60
91
|
if (id === "compact")
|
|
61
92
|
options.save({ ...current, normalCompaction: value === "on" });
|
|
93
|
+
else if (id === "taskBoundaryReminder")
|
|
94
|
+
options.save({
|
|
95
|
+
...current,
|
|
96
|
+
taskBoundaryReminder:
|
|
97
|
+
TASK_BOUNDARY_PRESETS.find(
|
|
98
|
+
(tokens) => taskBoundaryLabel(tokens) === value,
|
|
99
|
+
) ?? current.taskBoundaryReminder,
|
|
100
|
+
});
|
|
62
101
|
else {
|
|
63
102
|
const idleMinutes = IDLE_MINUTES.find(
|
|
64
103
|
(minutes) => idleValue(minutes) === value,
|
|
@@ -69,11 +108,13 @@ export async function openNotesSettings(
|
|
|
69
108
|
} catch (error) {
|
|
70
109
|
list.updateValue(
|
|
71
110
|
id,
|
|
72
|
-
id === "
|
|
73
|
-
? current.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
111
|
+
id === "taskBoundaryReminder"
|
|
112
|
+
? taskBoundaryLabel(current.taskBoundaryReminder)
|
|
113
|
+
: id === "compact"
|
|
114
|
+
? current.normalCompaction
|
|
115
|
+
? "on"
|
|
116
|
+
: "off"
|
|
117
|
+
: idleValue(current.idleMinutes),
|
|
77
118
|
);
|
|
78
119
|
ctx.ui.notify(
|
|
79
120
|
`Could not save notes settings: ${String(error)}`,
|
|
@@ -86,20 +127,36 @@ export async function openNotesSettings(
|
|
|
86
127
|
{ enableSearch: false },
|
|
87
128
|
);
|
|
88
129
|
return {
|
|
89
|
-
render: (width) =>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
130
|
+
render: (width) => {
|
|
131
|
+
const wrap = (text: string) =>
|
|
132
|
+
wrapTextWithAnsi(text, Math.max(1, width - 2)).map(
|
|
133
|
+
(line) => ` ${line}`,
|
|
134
|
+
);
|
|
135
|
+
const rule = theme.fg("accent", "─".repeat(Math.max(0, width)));
|
|
136
|
+
return [
|
|
137
|
+
rule,
|
|
138
|
+
` ${theme.bold("Notes & context")}`,
|
|
139
|
+
"",
|
|
140
|
+
...status.flatMap((line) =>
|
|
141
|
+
wrap(line).map((part) => theme.fg("dim", part)),
|
|
142
|
+
),
|
|
93
143
|
"",
|
|
94
|
-
...list.render(width),
|
|
144
|
+
...list.render(width).slice(0, -2),
|
|
95
145
|
"",
|
|
96
|
-
|
|
97
|
-
"dim",
|
|
98
|
-
"↑↓ navigate · Enter/Space change · Esc close · saved immediately",
|
|
146
|
+
...wrap("↑/↓ select · Enter/Space change · Esc close").map(
|
|
147
|
+
(line) => theme.fg("dim", line),
|
|
99
148
|
),
|
|
100
|
-
|
|
149
|
+
...wrap("Settings saved immediately").map((line) =>
|
|
150
|
+
theme.fg("dim", line),
|
|
151
|
+
),
|
|
152
|
+
rule,
|
|
153
|
+
].map((line) => truncateToWidth(line, width, ""));
|
|
154
|
+
},
|
|
101
155
|
invalidate: () => list.invalidate(),
|
|
102
|
-
handleInput: (data) =>
|
|
156
|
+
handleInput: (data) => {
|
|
157
|
+
list.handleInput(data);
|
|
158
|
+
tui.requestRender();
|
|
159
|
+
},
|
|
103
160
|
};
|
|
104
161
|
},
|
|
105
162
|
);
|
package/src/settings.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type IdleMinutes = (typeof IDLE_MINUTES)[number];
|
|
|
6
6
|
export interface NotesSettings {
|
|
7
7
|
normalCompaction: boolean;
|
|
8
8
|
idleMinutes: IdleMinutes;
|
|
9
|
+
taskBoundaryReminder: number;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function readSettings(path: string): NotesSettings {
|
|
@@ -14,7 +15,11 @@ export function readSettings(path: string): NotesSettings {
|
|
|
14
15
|
source = readFileSync(path, "utf8");
|
|
15
16
|
} catch (error) {
|
|
16
17
|
if ((error as NodeJS.ErrnoException).code === "ENOENT")
|
|
17
|
-
return {
|
|
18
|
+
return {
|
|
19
|
+
normalCompaction: false,
|
|
20
|
+
idleMinutes: 0,
|
|
21
|
+
taskBoundaryReminder: 0,
|
|
22
|
+
};
|
|
18
23
|
throw error;
|
|
19
24
|
}
|
|
20
25
|
const settings: unknown = JSON.parse(source);
|
|
@@ -28,6 +33,16 @@ export function readSettings(path: string): NotesSettings {
|
|
|
28
33
|
"Invalid notes settings. Save a setting in /notes, then reload Pi.",
|
|
29
34
|
);
|
|
30
35
|
const idleMinutes = "idleMinutes" in settings ? settings.idleMinutes : 0;
|
|
36
|
+
const taskBoundaryReminder =
|
|
37
|
+
"taskBoundaryReminder" in settings ? settings.taskBoundaryReminder : 0;
|
|
38
|
+
if (
|
|
39
|
+
typeof taskBoundaryReminder !== "number" ||
|
|
40
|
+
!Number.isSafeInteger(taskBoundaryReminder) ||
|
|
41
|
+
taskBoundaryReminder < 0
|
|
42
|
+
)
|
|
43
|
+
throw new Error(
|
|
44
|
+
"Invalid task-boundary reminder setting. Save a setting in /notes, then reload Pi.",
|
|
45
|
+
);
|
|
31
46
|
if (!IDLE_MINUTES.some((value) => value === idleMinutes))
|
|
32
47
|
throw new Error(
|
|
33
48
|
"Invalid idle rollover interval. Choose off, 5, 15, 25 or 55 minutes.",
|
|
@@ -35,6 +50,7 @@ export function readSettings(path: string): NotesSettings {
|
|
|
35
50
|
return {
|
|
36
51
|
normalCompaction: settings.normalCompaction,
|
|
37
52
|
idleMinutes: idleMinutes as IdleMinutes,
|
|
53
|
+
taskBoundaryReminder,
|
|
38
54
|
};
|
|
39
55
|
}
|
|
40
56
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export function renderTaskBoundaryReminder(remainingPercent: number): string {
|
|
4
|
+
return `<context_window_reminder>\nThis window is approaching the recommended token budget. You still have ${remainingPercent}% of your context window remaining, so there is no need to start a new window immediately. If you are working through a sequence of tasks toward a larger goal, finish the current task, save a checkpoint, and start a new window before continuing with the next task. Otherwise, continue here.\n</context_window_reminder>`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const TASK_BOUNDARY_PRESETS = [
|
|
8
|
+
0, 250_000, 300_000, 400_000, 450_000,
|
|
9
|
+
] as const;
|
|
10
|
+
export function taskBoundaryLabel(tokens: number): string {
|
|
11
|
+
return tokens === 0
|
|
12
|
+
? "off"
|
|
13
|
+
: TASK_BOUNDARY_PRESETS.some((value) => value === tokens)
|
|
14
|
+
? `${tokens / 1000}k`
|
|
15
|
+
: "custom (edit JSON)";
|
|
16
|
+
}
|
|
17
|
+
export function taskBoundaryEligible(
|
|
18
|
+
ctx: ExtensionContext,
|
|
19
|
+
threshold: number,
|
|
20
|
+
tokens = ctx.getContextUsage()?.tokens,
|
|
21
|
+
): boolean {
|
|
22
|
+
const capacity =
|
|
23
|
+
ctx.getContextUsage()?.contextWindow ?? ctx.model?.contextWindow ?? 0;
|
|
24
|
+
return (
|
|
25
|
+
threshold > 0 &&
|
|
26
|
+
Number.isFinite(capacity) &&
|
|
27
|
+
capacity > 272_000 &&
|
|
28
|
+
typeof tokens === "number" &&
|
|
29
|
+
Number.isFinite(tokens) &&
|
|
30
|
+
tokens >= threshold
|
|
31
|
+
);
|
|
32
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -41,7 +41,8 @@ const HISTORY_ACTIONS = [
|
|
|
41
41
|
"read_item",
|
|
42
42
|
"search_contents",
|
|
43
43
|
] as const;
|
|
44
|
-
export const NOTES_USAGE =
|
|
44
|
+
export const NOTES_USAGE =
|
|
45
|
+
"await tools.notes({ action, ...args }) // no args: help; actions(required args): list_files_by_prefix(); read_file(path); search_contents(query); append_to_file(path,text); write_file(path,text)";
|
|
45
46
|
export const HISTORY_USAGE = "await tools.history() // help";
|
|
46
47
|
const STRING = Type.Optional(Type.String());
|
|
47
48
|
const INTEGER = Type.Optional(Type.Integer({ minimum: 1 }));
|
|
@@ -526,6 +527,14 @@ export function createTools(
|
|
|
526
527
|
'Supply an action object: await tools.notes({ action: "read_file", path: "checkpoint" }); tools.notes() returns help',
|
|
527
528
|
);
|
|
528
529
|
const params = { ...args } as Record<string, unknown>;
|
|
530
|
+
if (
|
|
531
|
+
(params["action"] === "write_file" ||
|
|
532
|
+
params["action"] === "append_to_file") &&
|
|
533
|
+
"content" in params
|
|
534
|
+
) {
|
|
535
|
+
if (!("text" in params)) params["text"] = params["content"];
|
|
536
|
+
delete params["content"];
|
|
537
|
+
}
|
|
529
538
|
if (
|
|
530
539
|
params["action"] === "list_files_by_prefix" &&
|
|
531
540
|
params["max_files"] !== undefined
|
package/src/windows.ts
CHANGED
|
@@ -10,12 +10,16 @@ import type {
|
|
|
10
10
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { Box, Text, TruncatedText } from "@earendil-works/pi-tui";
|
|
12
12
|
import type { WindowIdentity } from "./bridge.js";
|
|
13
|
+
import {
|
|
14
|
+
renderTaskBoundaryReminder,
|
|
15
|
+
taskBoundaryEligible,
|
|
16
|
+
} from "./task-boundary.js";
|
|
13
17
|
|
|
14
18
|
export const WINDOW_MESSAGE = "notes-compaction:window:v1";
|
|
15
19
|
export const SETTLEMENT_ENTRY = "notes-compaction:settled:v1";
|
|
16
20
|
export interface WindowDetails {
|
|
17
21
|
protocol: 1;
|
|
18
|
-
kind: "window" | "identity" | "reminder" | "urgent";
|
|
22
|
+
kind: "window" | "identity" | "reminder" | "urgent" | "task-boundary";
|
|
19
23
|
identity: WindowIdentity;
|
|
20
24
|
trim: boolean;
|
|
21
25
|
}
|
|
@@ -32,11 +36,13 @@ export function registerWindowRenderer(pi: ExtensionAPI): void {
|
|
|
32
36
|
const details = parseWindowDetails(message.details);
|
|
33
37
|
if (!details || typeof message.content !== "string") return undefined;
|
|
34
38
|
const title =
|
|
35
|
-
details.kind === "
|
|
36
|
-
? "
|
|
37
|
-
: details.kind === "
|
|
38
|
-
? "Context
|
|
39
|
-
:
|
|
39
|
+
details.kind === "task-boundary"
|
|
40
|
+
? "Task-boundary reminder"
|
|
41
|
+
: details.kind === "urgent"
|
|
42
|
+
? "Context nearly full · Save notes and start a new window now"
|
|
43
|
+
: details.kind === "reminder"
|
|
44
|
+
? "Context checkpoint reminder · Save notes and start a new window"
|
|
45
|
+
: `Context window ${details.identity.windowNumber + 1} · Notes and history`;
|
|
40
46
|
const box = new Box(outputPad, 1, (text) =>
|
|
41
47
|
theme.bg("customMessageBg", text),
|
|
42
48
|
);
|
|
@@ -85,7 +91,7 @@ function parseWindowDetails(value: unknown): WindowDetails | undefined {
|
|
|
85
91
|
if (
|
|
86
92
|
details.protocol !== 1 ||
|
|
87
93
|
!ids ||
|
|
88
|
-
!["window", "identity", "reminder", "urgent"].includes(
|
|
94
|
+
!["window", "identity", "reminder", "urgent", "task-boundary"].includes(
|
|
89
95
|
details.kind ?? "",
|
|
90
96
|
) ||
|
|
91
97
|
typeof details.trim !== "boolean"
|
|
@@ -100,7 +106,9 @@ function parseWindowDetails(value: unknown): WindowDetails | undefined {
|
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
/** Understand previous PCC window IDs for familiar history lookup, without importing its runtime. */
|
|
103
|
-
export function windowDetails(
|
|
109
|
+
export function windowDetails(
|
|
110
|
+
entry: SessionEntry | CustomMessageEntryDraft,
|
|
111
|
+
): WindowDetails | undefined {
|
|
104
112
|
if (entry.type !== "custom_message") return;
|
|
105
113
|
return messageWindowDetails(entry.customType, entry.details);
|
|
106
114
|
}
|
|
@@ -122,14 +130,14 @@ function messageWindowDetails(
|
|
|
122
130
|
!details ||
|
|
123
131
|
typeof details !== "object" ||
|
|
124
132
|
!("kind" in details) ||
|
|
125
|
-
details.kind !== "window"
|
|
133
|
+
(details.kind !== "window" && details.kind !== "task-boundary")
|
|
126
134
|
)
|
|
127
135
|
return;
|
|
128
136
|
const ids = identity(details);
|
|
129
137
|
return ids
|
|
130
138
|
? {
|
|
131
139
|
protocol: 1,
|
|
132
|
-
kind:
|
|
140
|
+
kind: details.kind,
|
|
133
141
|
identity: ids,
|
|
134
142
|
trim:
|
|
135
143
|
"trimPreviousWindow" in details &&
|
|
@@ -182,13 +190,18 @@ export class Windows {
|
|
|
182
190
|
this.compactedSinceBoundary = false;
|
|
183
191
|
} else if (
|
|
184
192
|
details &&
|
|
185
|
-
(details.kind === "reminder" ||
|
|
193
|
+
(details.kind === "reminder" ||
|
|
194
|
+
details.kind === "urgent" ||
|
|
195
|
+
details.kind === "task-boundary")
|
|
186
196
|
)
|
|
187
197
|
this.reminders.add(
|
|
188
198
|
`${details.identity.currentWindowId}:${details.kind}`,
|
|
189
199
|
);
|
|
190
200
|
if (entry.type === "compaction") {
|
|
191
|
-
|
|
201
|
+
// Compaction frees capacity, but does not create a new window identity.
|
|
202
|
+
this.reminders = new Set(
|
|
203
|
+
[...this.reminders].filter((key) => key.endsWith(":task-boundary")),
|
|
204
|
+
);
|
|
192
205
|
this.compactedSinceBoundary = true;
|
|
193
206
|
}
|
|
194
207
|
}
|
|
@@ -226,21 +239,47 @@ export class Windows {
|
|
|
226
239
|
: Math.round((remainingTokens / contextWindow) * 1000) / 10,
|
|
227
240
|
};
|
|
228
241
|
}
|
|
229
|
-
reminder(
|
|
242
|
+
reminder(
|
|
243
|
+
ctx: ExtensionContext,
|
|
244
|
+
taskBoundaryReminder = 0,
|
|
245
|
+
checkpointReminder = true,
|
|
246
|
+
): CustomMessageEntryDraft | undefined {
|
|
230
247
|
const { remainingTokens, contextWindow, remainingPercent } =
|
|
231
248
|
this.remaining(ctx);
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
249
|
+
if (
|
|
250
|
+
!this.current ||
|
|
251
|
+
remainingTokens === undefined ||
|
|
252
|
+
remainingPercent === undefined
|
|
253
|
+
)
|
|
236
254
|
return;
|
|
237
|
-
|
|
255
|
+
const used = 100 * (1 - remainingTokens / contextWindow);
|
|
256
|
+
let kind: WindowDetails["kind"] | undefined = checkpointReminder
|
|
257
|
+
? used >= 90
|
|
258
|
+
? "urgent"
|
|
259
|
+
: used >= 85
|
|
260
|
+
? "reminder"
|
|
261
|
+
: undefined
|
|
262
|
+
: undefined;
|
|
263
|
+
if (kind && this.reminders.has(`${this.current.currentWindowId}:${kind}`))
|
|
264
|
+
kind = undefined;
|
|
265
|
+
if (
|
|
266
|
+
!kind &&
|
|
267
|
+
taskBoundaryReminder &&
|
|
268
|
+
!this.reminders.has(`${this.current.currentWindowId}:task-boundary`) &&
|
|
269
|
+
taskBoundaryEligible(ctx, taskBoundaryReminder)
|
|
270
|
+
)
|
|
271
|
+
kind = "task-boundary";
|
|
272
|
+
if (!kind) return;
|
|
273
|
+
this.reminders.add(`${this.current.currentWindowId}:${kind}`);
|
|
238
274
|
if (kind === "urgent")
|
|
239
|
-
this.reminders.add(`${this.current.currentWindowId}:
|
|
275
|
+
this.reminders.add(`${this.current.currentWindowId}:reminder`);
|
|
240
276
|
return {
|
|
241
277
|
type: "custom_message",
|
|
242
278
|
customType: WINDOW_MESSAGE,
|
|
243
|
-
content:
|
|
279
|
+
content:
|
|
280
|
+
kind === "task-boundary"
|
|
281
|
+
? renderTaskBoundaryReminder(remainingPercent)
|
|
282
|
+
: `<context_window_reminder>\n${kind === "urgent" ? "Urgent: " : ""}${remainingPercent}% remaining. Checkpoint the active request, state and known history IDs in notes, then call new_context ${kind === "urgent" ? "now, before other work" : "before continuing work"}.\n</context_window_reminder>`,
|
|
244
283
|
display: true,
|
|
245
284
|
details: {
|
|
246
285
|
protocol: 1,
|