@astrosheep/pi-context 0.24.0 → 0.25.0
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 +52 -5
- package/dist/build-info.json +4 -0
- package/dist/extension.js +1861 -0
- package/dist/src/context/budget.js +150 -0
- package/dist/src/context/context-window.js +97 -0
- package/dist/src/context/prompts.js +94 -0
- package/dist/src/context/reset-lifecycle.js +134 -0
- package/dist/src/context/runtime.js +236 -0
- package/dist/src/context/thresholds.js +62 -0
- package/dist/src/dream/cli.js +1 -1
- package/dist/src/dream/doctor.js +34 -6
- package/dist/src/dream/runner.js +1 -1
- package/dist/src/dream/settings.js +30 -0
- package/dist/src/{history-tools.js → history/history-tools.js} +3 -3
- package/dist/src/{history.js → history/history.js} +8 -46
- package/dist/src/index.js +27 -94
- package/dist/src/notes/address.js +97 -16
- package/dist/src/notes/frontmatter.js +18 -3
- package/dist/src/notes/notes-snapshot.js +30 -0
- package/dist/src/notes/paths.js +64 -7
- package/dist/src/notes/session-replay.js +41 -0
- package/dist/src/notes/store.js +76 -22
- package/dist/src/notes/tools.js +7 -7
- package/dist/src/protocol.js +7 -5
- package/dist/src/settings.js +16 -0
- package/dist/src/tool-schema.js +1 -1
- package/dist/test/agent-loop.test.js +813 -221
- package/dist/test/boot.integration.test.js +167 -0
- package/dist/test/budget-settings.integration.test.js +126 -0
- package/dist/test/doctor.test.js +14 -36
- package/dist/test/dream.test.js +37 -380
- package/dist/test/helpers/extension.js +393 -0
- package/dist/test/history.integration.test.js +316 -0
- package/dist/test/notes.integration.test.js +273 -0
- package/dist/test/notes.test.js +40 -359
- package/dist/test/reset-lifecycle.test.js +248 -180
- package/docs/architecture.md +35 -18
- package/docs/reset-lifecycle.md +16 -14
- package/package.json +11 -10
- package/src/context/budget.ts +148 -0
- package/src/context/context-window.ts +103 -0
- package/src/context/prompts.ts +111 -0
- package/src/context/reset-lifecycle.ts +145 -0
- package/src/context/runtime.ts +246 -0
- package/src/context/thresholds.ts +78 -0
- package/src/dream/cli.ts +1 -1
- package/src/dream/doctor.ts +27 -6
- package/src/dream/runner.ts +1 -1
- package/src/dream/settings.ts +32 -0
- package/src/{history-tools.ts → history/history-tools.ts} +3 -3
- package/src/{history.ts → history/history.ts} +9 -48
- package/src/index.ts +27 -89
- package/src/notes/address.ts +82 -16
- package/src/notes/frontmatter.ts +20 -3
- package/src/notes/notes-snapshot.ts +40 -0
- package/src/notes/paths.ts +64 -7
- package/src/notes/session-replay.ts +53 -0
- package/src/notes/store.ts +78 -25
- package/src/notes/tools.ts +7 -7
- package/src/protocol.ts +7 -5
- package/src/settings.ts +20 -0
- package/src/tool-schema.ts +1 -2
- package/dist/src/budget.js +0 -65
- package/dist/src/notes/model.js +0 -101
- package/dist/src/prompts.js +0 -88
- package/dist/src/reset-lifecycle.js +0 -155
- package/dist/src/thresholds.js +0 -102
- package/dist/src/warning.js +0 -44
- package/dist/test/coherence.test.js +0 -371
- package/dist/test/history.test.js +0 -26
- package/dist/test/integration.test.js +0 -1759
- package/dist/test/pagination.property.test.js +0 -471
- package/src/budget.ts +0 -67
- package/src/notes/model.ts +0 -109
- package/src/prompts.ts +0 -91
- package/src/reset-lifecycle.ts +0 -173
- package/src/thresholds.ts +0 -110
- package/src/warning.ts +0 -46
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { historyFromSession, internal } from "../src/index.js";
|
|
4
|
+
import { listNotes } from "../src/notes/store.js";
|
|
5
|
+
import { CONTINUATION_TYPE } from "../src/protocol.js";
|
|
6
|
+
import { appendText, call, commitTurnEndBoundary, context, installExtensionTestEnvironment, makeExtension, manager, noticesOf, resultJson, resultRead, runContextHook, runContextWithSystemHook, runHandlers, runManualCompact, runCommand, sentOf, } from "./helpers/extension.js";
|
|
7
|
+
const testEnvironment = installExtensionTestEnvironment("pi-context-integration");
|
|
8
|
+
test.beforeEach(() => testEnvironment.beforeEach());
|
|
9
|
+
test.afterEach(() => testEnvironment.afterEach());
|
|
10
|
+
test.after(() => testEnvironment.dispose());
|
|
11
|
+
test("custom reset marker removes old provider context while history remains searchable", async () => {
|
|
12
|
+
const sessionManager = manager();
|
|
13
|
+
const captured = makeExtension(sessionManager);
|
|
14
|
+
const ctx = context(sessionManager);
|
|
15
|
+
const oldUserId = appendText(sessionManager, "user", "OLD-UNIQUE-TRANSCRIPT needle");
|
|
16
|
+
appendText(sessionManager, "assistant", "I will use a tool");
|
|
17
|
+
const toolResultId = appendText(sessionManager, "toolResult", "tool result safely recorded");
|
|
18
|
+
await call(captured, "wipe_memory", {}, ctx);
|
|
19
|
+
const boundary = await commitTurnEndBoundary(captured, sessionManager, ctx);
|
|
20
|
+
assert.equal(boundary.continue, true);
|
|
21
|
+
const branch = sessionManager.getBranch();
|
|
22
|
+
const marker = branch.find((entry) => entry.type === "custom" && entry.customType === internal.RESET_MARKER_TYPE);
|
|
23
|
+
assert.ok(marker && marker.type === "custom");
|
|
24
|
+
assert.equal(marker.parentId, toolResultId, "marker follows the completed tool result");
|
|
25
|
+
assert.deepEqual(Object.keys(marker.data), ["windowId"]);
|
|
26
|
+
const projected = await runContextWithSystemHook(captured, ctx, sessionManager.buildSessionContext().messages);
|
|
27
|
+
const providerText = JSON.stringify(projected?.messages ?? []);
|
|
28
|
+
assert.equal(providerText.includes("OLD-UNIQUE-TRANSCRIPT"), false);
|
|
29
|
+
assert.equal(providerText.includes(internal.CONTEXT_WINDOW_OPEN_TAG), true);
|
|
30
|
+
assert.equal(providerText.includes(internal.RESET_MARKER_TYPE), false, "plain reset state never reaches the provider");
|
|
31
|
+
const windows = historyFromSession(ctx);
|
|
32
|
+
assert.equal(windows.length, 2);
|
|
33
|
+
const oldWindow = windows[0]?.windowId;
|
|
34
|
+
assert.ok(oldWindow);
|
|
35
|
+
const read = resultRead(await call(captured, "history_read", { window_id: oldWindow, item_id: oldUserId }, ctx));
|
|
36
|
+
assert.match(read.content, /OLD-UNIQUE-TRANSCRIPT/);
|
|
37
|
+
const found = resultJson(await call(captured, "history_search", { query: "needle" }, ctx));
|
|
38
|
+
assert.equal(found.items.length, 1);
|
|
39
|
+
assert.equal(found.items[0]?.item_id, oldUserId);
|
|
40
|
+
});
|
|
41
|
+
test("the root boot and reset boot carry durable window identity", async () => {
|
|
42
|
+
const sessionManager = manager();
|
|
43
|
+
const captured = makeExtension(sessionManager);
|
|
44
|
+
const ctx = context(sessionManager);
|
|
45
|
+
appendText(sessionManager, "user", "task before reset");
|
|
46
|
+
appendText(sessionManager, "assistant", "working");
|
|
47
|
+
await call(captured, "notes_write", { path: "decisions.md", content: "use terra" }, ctx);
|
|
48
|
+
// Root window: session_start persists the boot block without triggering a turn.
|
|
49
|
+
runHandlers(captured, "session_start", { reason: "startup" }, ctx);
|
|
50
|
+
assert.equal(captured.sent.length, 1);
|
|
51
|
+
const rootBoot = captured.sent[0];
|
|
52
|
+
assert.equal(rootBoot?.message.customType, internal.BOOT_TYPE);
|
|
53
|
+
assert.equal(rootBoot?.message.display, false, "boot block stays out of the TUI");
|
|
54
|
+
assert.equal(rootBoot?.options?.triggerTurn, false);
|
|
55
|
+
assert.equal(noticesOf(ctx).length, 0, "initial boot is silent");
|
|
56
|
+
runHandlers(captured, "session_start", { reason: "reload" }, ctx);
|
|
57
|
+
assert.equal(noticesOf(ctx).length, 0, "reload is silent");
|
|
58
|
+
assert.deepEqual(rootBoot?.message.details, { windowId: `pcw:${sessionManager.getSessionId().slice(0, 8)}:root` });
|
|
59
|
+
const rootText = typeof rootBoot?.message.content === "string" ? rootBoot.message.content : "";
|
|
60
|
+
assert.ok(rootText.startsWith(internal.CONTEXT_WINDOW_OPEN_TAG), "root block omits the reset line");
|
|
61
|
+
assert.equal(rootText.includes("Previous context window id:"), false, "root block omits the previous-id line");
|
|
62
|
+
assert.match(rootText, new RegExp(`First context window id: pcw:${sessionManager.getSessionId().slice(0, 8)}:root`));
|
|
63
|
+
assert.match(rootText, new RegExp(`Current context window id: pcw:${sessionManager.getSessionId().slice(0, 8)}:root`));
|
|
64
|
+
assert.ok(rootText.includes("decisions.md"));
|
|
65
|
+
const decisionsMeta = listNotes(ctx, { scope: "session" }).find((row) => row.path === "decisions.md")?.meta;
|
|
66
|
+
assert.ok(decisionsMeta);
|
|
67
|
+
assert.match(rootText, /updated \d+s ago\)/, "boot note metadata carries a relative update time");
|
|
68
|
+
assert.ok(rootText.includes(internal.CONTEXT_WINDOW_PROTOCOL_OPEN_TAG));
|
|
69
|
+
// Reset: the marker and boot are committed together at the turn boundary.
|
|
70
|
+
await call(captured, "wipe_memory", {}, ctx);
|
|
71
|
+
assert.equal(noticesOf(ctx).length, 0, "requesting a reset does not announce success");
|
|
72
|
+
const boundary = await commitTurnEndBoundary(captured, sessionManager, ctx);
|
|
73
|
+
assert.equal(boundary.continue, true);
|
|
74
|
+
runHandlers(captured, "turn_start", {}, ctx);
|
|
75
|
+
runHandlers(captured, "agent_settled", {}, ctx);
|
|
76
|
+
assert.equal(noticesOf(ctx).filter((notice) => notice.message.includes("memory cleared")).length, 1, "the committed reset is announced once");
|
|
77
|
+
const entries = sessionManager.getBranch();
|
|
78
|
+
const marker = entries.find((entry) => entry.type === "custom" && entry.customType === internal.RESET_MARKER_TYPE);
|
|
79
|
+
assert.ok(marker && marker.type === "custom");
|
|
80
|
+
const resetWindowId = marker.data.windowId;
|
|
81
|
+
const resetBoot = entries.find((entry) => entry.type === "custom_message" && entry.customType === internal.BOOT_TYPE && entry.details && typeof entry.details === "object" && entry.details.windowId === resetWindowId);
|
|
82
|
+
assert.ok(resetBoot && resetBoot.type === "custom_message");
|
|
83
|
+
assert.equal(resetBoot.details.windowId, marker.data.windowId);
|
|
84
|
+
const continuation = entries.find((entry) => entry.type === "custom_message" && entry.customType === CONTINUATION_TYPE);
|
|
85
|
+
assert.ok(continuation && continuation.type === "custom_message" && continuation.display === false, "the resumed run is represented by one hidden continuation");
|
|
86
|
+
});
|
|
87
|
+
test("a marker tail with only metadata repairs its missing boot without moving the boundary", () => {
|
|
88
|
+
const sessionManager = manager(true);
|
|
89
|
+
const windowId = "pcw:metadata-tail";
|
|
90
|
+
const markerId = sessionManager.appendCustomEntry(internal.RESET_MARKER_TYPE, { windowId });
|
|
91
|
+
const modelChangeId = sessionManager.appendModelChange("openai", "scripted-model");
|
|
92
|
+
sessionManager.appendThinkingLevelChange("low");
|
|
93
|
+
const captured = makeExtension(sessionManager);
|
|
94
|
+
const ctx = context(sessionManager);
|
|
95
|
+
runHandlers(captured, "session_start", { reason: "startup" }, ctx);
|
|
96
|
+
const branch = sessionManager.getBranch();
|
|
97
|
+
const markerIndex = branch.findIndex((entry) => entry.id === markerId);
|
|
98
|
+
const bootEntries = branch.filter((entry) => entry.type === "custom_message" && entry.customType === internal.BOOT_TYPE && entry.details && typeof entry.details === "object" && entry.details.windowId === windowId);
|
|
99
|
+
assert.equal(bootEntries.length, 1, "an incomplete marker tail gets one repaired boot");
|
|
100
|
+
assert.ok(branch.findIndex((entry) => entry.id === modelChangeId) > markerIndex, "metadata remains after the marker");
|
|
101
|
+
assert.ok(branch.findIndex((entry) => entry.id === bootEntries[0]?.id) > markerIndex, "the repaired boot remains in the marked window");
|
|
102
|
+
assert.equal(captured.sent.length, 1, "repair emits one hidden boot without a model turn");
|
|
103
|
+
assert.equal(noticesOf(ctx).length, 0, "repairing a boot is not a new reset");
|
|
104
|
+
});
|
|
105
|
+
test("off preserves an existing marker window and still cancels native compaction", async () => {
|
|
106
|
+
const sessionManager = manager();
|
|
107
|
+
const captured = makeExtension(sessionManager);
|
|
108
|
+
const ctx = context(sessionManager);
|
|
109
|
+
appendText(sessionManager, "user", "old context must stay hidden");
|
|
110
|
+
const windowId = "pcw:test:existing";
|
|
111
|
+
sessionManager.appendCustomEntry(internal.RESET_MARKER_TYPE, { windowId });
|
|
112
|
+
sessionManager.appendCustomMessageEntry(internal.BOOT_TYPE, "fresh boot", false, { windowId });
|
|
113
|
+
await runCommand(captured, "pi-context", "off", ctx);
|
|
114
|
+
const before = await runManualCompact(captured, ctx);
|
|
115
|
+
assert.deepEqual(before, { cancel: true });
|
|
116
|
+
const projected = await runContextWithSystemHook(captured, ctx, sessionManager.buildSessionContext().messages);
|
|
117
|
+
assert.equal(JSON.stringify(projected?.messages ?? []).includes("old context must stay hidden"), false);
|
|
118
|
+
const windows = resultJson(await call(captured, "history_windows", {}, ctx));
|
|
119
|
+
assert.equal(windows.windows[0]?.window_id, windowId);
|
|
120
|
+
});
|
|
121
|
+
test("wipe_memory uses one turn boundary and never calls ctx.compact", async () => {
|
|
122
|
+
const sessionManager = manager();
|
|
123
|
+
const captured = makeExtension(sessionManager);
|
|
124
|
+
const ctx = context(sessionManager, () => assert.fail("ctx.compact must not be used"));
|
|
125
|
+
appendText(sessionManager, "user", "enough history for the boundary test");
|
|
126
|
+
const result = resultJson(await call(captured, "wipe_memory", {}, ctx));
|
|
127
|
+
assert.ok(result.status);
|
|
128
|
+
const boundary = await commitTurnEndBoundary(captured, sessionManager, ctx);
|
|
129
|
+
assert.equal(boundary.continue, true);
|
|
130
|
+
assert.equal(sessionManager.getBranch().filter((entry) => entry.type === "custom" && entry.customType === internal.RESET_MARKER_TYPE).length, 1);
|
|
131
|
+
assert.equal(sessionManager.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === internal.BOOT_TYPE && entry.details && typeof entry.details === "object" && "windowId" in entry.details).length, 1);
|
|
132
|
+
});
|
|
133
|
+
test("pi-context command toggles future work, /wipe-memory is the manual path, and active markers cancel /compact", async () => {
|
|
134
|
+
const sessionManager = manager();
|
|
135
|
+
appendText(sessionManager, "user", "hello");
|
|
136
|
+
const captured = makeExtension(sessionManager);
|
|
137
|
+
const low = context(sessionManager, undefined, { tokens: 170_000, contextWindow: 200_000, percent: 85 });
|
|
138
|
+
// On by default: session_start persists the root boot block.
|
|
139
|
+
runHandlers(captured, "session_start", { reason: "startup" }, low);
|
|
140
|
+
assert.equal(captured.sent.length, 1);
|
|
141
|
+
assert.equal(captured.sent[0]?.message.customType, internal.BOOT_TYPE);
|
|
142
|
+
let notices = await runCommand(captured, "pi-context", "off", low);
|
|
143
|
+
assert.match(notices[0]?.message ?? "", /off/);
|
|
144
|
+
assert.equal(await runContextHook(captured, low), undefined, "no guidance while off");
|
|
145
|
+
assert.equal(sentOf(captured, internal.GUIDANCE_TYPE).length, 0, "no guidance persisted while off");
|
|
146
|
+
runHandlers(captured, "session_start", { reason: "startup" }, low);
|
|
147
|
+
assert.equal(captured.sent.length, 1, "no new boot block is persisted while off");
|
|
148
|
+
const offResult = resultJson(await call(captured, "wipe_memory", {}, low));
|
|
149
|
+
assert.match(offResult.error ?? "", /off/, "wipe_memory refuses while off");
|
|
150
|
+
notices = await runCommand(captured, "pi-context", "on", low);
|
|
151
|
+
assert.match(notices[0]?.message ?? "", /on/);
|
|
152
|
+
notices = await runCommand(captured, "wipe-memory", "", low);
|
|
153
|
+
assert.equal(notices.length, 1, "manual clear emits exactly one notification");
|
|
154
|
+
assert.match(notices[0]?.message ?? "", /memory cleared/);
|
|
155
|
+
assert.equal(captured.sent.length, 2, "/wipe-memory writes one hidden boot without triggering a model turn");
|
|
156
|
+
assert.equal(captured.sent[1]?.options?.triggerTurn, false);
|
|
157
|
+
assert.equal(sessionManager.getBranch().filter((entry) => entry.type === "custom" && entry.customType === internal.RESET_MARKER_TYPE).length, 1);
|
|
158
|
+
const markerContext = await runManualCompact(captured, low);
|
|
159
|
+
assert.deepEqual(markerContext, { cancel: true }, "/compact is canceled while a marker is active");
|
|
160
|
+
const projected = await runContextWithSystemHook(captured, low, sessionManager.buildSessionContext().messages);
|
|
161
|
+
assert.equal(JSON.stringify(projected?.messages ?? []).includes("hello"), false, "off keeps the existing wipe in force");
|
|
162
|
+
notices = await runCommand(captured, "pi-context", "maybe", low);
|
|
163
|
+
assert.equal(notices[0]?.type, "error", "unknown argument rejected");
|
|
164
|
+
// Bare command reports current state without changing it.
|
|
165
|
+
notices = await runCommand(captured, "pi-context", "", low);
|
|
166
|
+
assert.match(notices[0]?.message ?? "", /on/);
|
|
167
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { internal } from "../src/index.js";
|
|
4
|
+
import { appendText, call, commitTurnEndBoundary, context, installExtensionTestEnvironment, makeExtension, manager, noticesOf, resultJson, runContextHook, runHandlers, sentOf, settingsFixture, } from "./helpers/extension.js";
|
|
5
|
+
const testEnvironment = installExtensionTestEnvironment("pi-context-integration");
|
|
6
|
+
test.beforeEach(() => testEnvironment.beforeEach());
|
|
7
|
+
test.afterEach(() => testEnvironment.afterEach());
|
|
8
|
+
test.after(() => testEnvironment.dispose());
|
|
9
|
+
test("low-budget guidance and warning persist at turn_end, once per active window", async () => {
|
|
10
|
+
const sessionManager = manager();
|
|
11
|
+
const captured = makeExtension(sessionManager);
|
|
12
|
+
const low = context(sessionManager, undefined, { tokens: 170_000, percent: 85, contextWindow: 200_000 });
|
|
13
|
+
assert.equal(await runContextHook(captured, low), undefined, "guidance is staged, not injected into this request");
|
|
14
|
+
assert.equal(captured.sent.length, 0, "guidance does not trigger a detached turn");
|
|
15
|
+
const guidanceBoundary = await commitTurnEndBoundary(captured, sessionManager, low);
|
|
16
|
+
assert.equal(guidanceBoundary.entries.filter((entry) => entry.type === "custom_message" && entry.customType === internal.GUIDANCE_TYPE).length, 1);
|
|
17
|
+
assert.equal(sessionManager.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === internal.GUIDANCE_TYPE).length, 1);
|
|
18
|
+
assert.equal(await runContextHook(captured, low), undefined, "the same window does not repeat guidance");
|
|
19
|
+
const warningContext = context(sessionManager, undefined, { tokens: 199_000, percent: 99.5, contextWindow: 200_000 });
|
|
20
|
+
const warning = await runContextHook(captured, warningContext);
|
|
21
|
+
assert.equal(warning?.messages.length, 1, "the warning is visible in the current provider request");
|
|
22
|
+
assert.equal((warning?.messages[0]).customType, internal.WARNING_TYPE);
|
|
23
|
+
const warningBoundary = await commitTurnEndBoundary(captured, sessionManager, warningContext);
|
|
24
|
+
assert.equal(warningBoundary.entries.filter((entry) => entry.type === "custom_message" && entry.customType === internal.WARNING_TYPE).length, 1);
|
|
25
|
+
assert.equal(sessionManager.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === internal.WARNING_TYPE).length, 1);
|
|
26
|
+
});
|
|
27
|
+
test("the visible countdown ends at the warning line, clamps at zero, and preserves unknown usage", async () => {
|
|
28
|
+
const fixture = settingsFixture({
|
|
29
|
+
reserveTokens: 16_384,
|
|
30
|
+
project: { compaction: { reserveTokens: 32_768 } },
|
|
31
|
+
});
|
|
32
|
+
const sm = manager();
|
|
33
|
+
const captured = makeExtension(sm);
|
|
34
|
+
runHandlers(captured, "session_tree", {}, context(sm, undefined, undefined, true, fixture.cwd, true));
|
|
35
|
+
const readBudget = async (tokens, trusted = true) => {
|
|
36
|
+
const ctx = context(sm, undefined, { tokens, contextWindow: 200_000, percent: tokens === null ? null : tokens / 2000 }, true, fixture.cwd, trusted);
|
|
37
|
+
return resultJson(await call(captured, "get_context_remaining", {}, ctx)).remaining_tokens;
|
|
38
|
+
};
|
|
39
|
+
assert.equal(await readBudget(72_563), 82_381, "the reported 127437 physical tokens exclude reserve plus runway (45056)");
|
|
40
|
+
assert.equal(await readBudget(167_232), 0, "inside the runway the countdown reads zero");
|
|
41
|
+
assert.equal(await readBudget(190_000), 0, "below the reserve, still zero");
|
|
42
|
+
assert.equal(await readBudget(210_000), 0, "over the physical window");
|
|
43
|
+
assert.equal(await readBudget(null), null, "unknown usage remains unknown");
|
|
44
|
+
const absent = context(sm, undefined, undefined, true, fixture.cwd);
|
|
45
|
+
assert.equal(resultJson(await call(captured, "get_context_remaining", {}, absent)).remaining_tokens, null);
|
|
46
|
+
const untrusted = context(sm, undefined, { tokens: 72_563, contextWindow: 200_000, percent: 36.2815 }, true, fixture.cwd, false);
|
|
47
|
+
runHandlers(captured, "session_start", {}, untrusted);
|
|
48
|
+
assert.equal(await readBudget(72_563, false), 98_765, "session start reloads the global reserve when the project is untrusted");
|
|
49
|
+
});
|
|
50
|
+
test("absent pi-context key or margins reproduce the default reminder threshold at Pi's default reserve", async () => {
|
|
51
|
+
assert.equal(internal.DEFAULT_RESERVE_TOKENS, 16_384);
|
|
52
|
+
assert.equal(internal.DEFAULT_RESERVE_TOKENS + internal.DEFAULT_REMINDER_MARGIN_TOKENS, 40_960);
|
|
53
|
+
for (const [label, options] of [
|
|
54
|
+
["absent key", { global: {} }],
|
|
55
|
+
["absent margins", { global: { [internal.PI_CONTEXT_SETTINGS_KEY]: {} } }],
|
|
56
|
+
]) {
|
|
57
|
+
const fixture = settingsFixture(options);
|
|
58
|
+
const sm = manager();
|
|
59
|
+
const captured = makeExtension(sm);
|
|
60
|
+
// Thresholds are resolved once per session and cached; branch navigation clears the
|
|
61
|
+
// cache without emitting a boot block, so the next read uses this fixture.
|
|
62
|
+
runHandlers(captured, "session_tree", {}, context(sm, undefined, undefined, true, fixture.cwd));
|
|
63
|
+
const window = 200_000;
|
|
64
|
+
const at = (remaining) => context(sm, undefined, { tokens: window - remaining, percent: 0, contextWindow: window }, true, fixture.cwd);
|
|
65
|
+
const first = at(40_961);
|
|
66
|
+
assert.equal(await runContextHook(captured, first), undefined, `${label}: nothing injected above the default reminder`);
|
|
67
|
+
assert.equal(sentOf(captured, internal.GUIDANCE_TYPE).length, 0, `${label}: no guidance above the default reminder`);
|
|
68
|
+
const crossing = at(40_960);
|
|
69
|
+
assert.equal(await runContextHook(captured, crossing), undefined, `${label}: default reminder crossing persists only`);
|
|
70
|
+
await commitTurnEndBoundary(captured, sm, crossing);
|
|
71
|
+
assert.equal(sm.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === internal.GUIDANCE_TYPE).length, 1, `${label}: default reminder fires`);
|
|
72
|
+
assert.equal(noticesOf(first).length, 0, `${label}: valid defaults warn nobody`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
test("project pi-context reminder margin and reserve override global per key", async () => {
|
|
76
|
+
const fixture = settingsFixture({
|
|
77
|
+
reserveTokens: 20_000,
|
|
78
|
+
global: { [internal.PI_CONTEXT_SETTINGS_KEY]: { reminderMarginTokens: 30_000 } },
|
|
79
|
+
project: { compaction: { reserveTokens: 50_000 }, [internal.PI_CONTEXT_SETTINGS_KEY]: { reminderMarginTokens: 40_000 } },
|
|
80
|
+
});
|
|
81
|
+
// Project reserve wins: reminder = 50000 + 40000 (project margin).
|
|
82
|
+
const sm = manager();
|
|
83
|
+
const captured = makeExtension(sm);
|
|
84
|
+
runHandlers(captured, "session_tree", {}, context(sm, undefined, undefined, true, fixture.cwd));
|
|
85
|
+
const window = 300_000;
|
|
86
|
+
const at = (remaining) => context(sm, undefined, { tokens: window - remaining, percent: 0, contextWindow: window }, true, fixture.cwd);
|
|
87
|
+
assert.equal(await runContextHook(captured, at(90_001)), undefined, "nothing injected above the project-derived reminder");
|
|
88
|
+
assert.equal(sentOf(captured, internal.GUIDANCE_TYPE).length, 0);
|
|
89
|
+
const crossing = at(90_000);
|
|
90
|
+
assert.equal(await runContextHook(captured, crossing), undefined, "project-derived reminder crossing persists only");
|
|
91
|
+
await commitTurnEndBoundary(captured, sm, crossing);
|
|
92
|
+
assert.equal(sm.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === internal.GUIDANCE_TYPE).length, 1, "project reminder margin wins");
|
|
93
|
+
});
|
|
94
|
+
test("an invalid reminder margin degrades to its default with one warning and never throws", async () => {
|
|
95
|
+
const fixture = settingsFixture({ global: { [internal.PI_CONTEXT_SETTINGS_KEY]: { reminderMarginTokens: 0 } } });
|
|
96
|
+
const sm = manager();
|
|
97
|
+
const captured = makeExtension(sm);
|
|
98
|
+
const ctx = context(sm, undefined, undefined, true, fixture.cwd);
|
|
99
|
+
assert.doesNotThrow(() => runHandlers(captured, "session_start", { reason: "startup" }, ctx));
|
|
100
|
+
const notices = noticesOf(ctx).filter((notice) => notice.type === "warning");
|
|
101
|
+
assert.equal(notices.length, 1, "one warning for the offending key");
|
|
102
|
+
assert.equal(notices[0]?.type, "warning");
|
|
103
|
+
assert.match(notices[0]?.message ?? "", /reminderMarginTokens/);
|
|
104
|
+
assert.match(notices[0]?.message ?? "", /24576/);
|
|
105
|
+
const window = 200_000;
|
|
106
|
+
const at = (remaining) => context(sm, undefined, { tokens: window - remaining, percent: 0, contextWindow: window }, true, fixture.cwd);
|
|
107
|
+
// The degraded reminder is Pi's default reserve + default margin = 40960.
|
|
108
|
+
assert.equal(await runContextHook(captured, at(40_961)), undefined, "nothing injected above the degraded reminder");
|
|
109
|
+
const crossing = at(40_960);
|
|
110
|
+
assert.equal(await runContextHook(captured, crossing), undefined, "degraded reminder uses its default");
|
|
111
|
+
await commitTurnEndBoundary(captured, sm, crossing);
|
|
112
|
+
assert.equal(sm.getBranch().filter((entry) => entry.type === "custom_message" && entry.customType === internal.GUIDANCE_TYPE).length, 1, "the degraded reminder is persisted once");
|
|
113
|
+
assert.equal(noticesOf(ctx).filter((notice) => notice.type === "warning").length, 1, "warning stays one-time across handler calls");
|
|
114
|
+
});
|
|
115
|
+
test("the warning supersedes the early reminder when usage jumps across both thresholds", async () => {
|
|
116
|
+
const sm = manager();
|
|
117
|
+
appendText(sm, "user", "ongoing work");
|
|
118
|
+
const captured = makeExtension(sm);
|
|
119
|
+
const ctx = context(sm, undefined, { tokens: 199_000, percent: 99.5, contextWindow: 200_000 }, false);
|
|
120
|
+
const warningResult = await runContextHook(captured, ctx);
|
|
121
|
+
assert.deepEqual(warningResult?.messages.map((message) => message.customType), [internal.WARNING_TYPE]);
|
|
122
|
+
await commitTurnEndBoundary(captured, sm, ctx);
|
|
123
|
+
const reloaded = makeExtension(sm);
|
|
124
|
+
runHandlers(reloaded, "context", {}, ctx);
|
|
125
|
+
assert.equal(reloaded.sent.length, 0, "persisted warning also suppresses a late reminder after reload");
|
|
126
|
+
});
|
package/dist/test/doctor.test.js
CHANGED
|
@@ -1,44 +1,22 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
1
|
import assert from "node:assert/strict";
|
|
3
|
-
import {
|
|
2
|
+
import { main } from "../src/dream/cli.js";
|
|
3
|
+
import { doctor } from "../src/dream/doctor.js";
|
|
4
|
+
import { existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import test from "node:test";
|
|
4
6
|
import { tmpdir } from "node:os";
|
|
5
7
|
import { join } from "node:path";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
writeFileSync(join(root, "personal/a.md"), note());
|
|
14
|
-
writeFileSync(join(root, "personal/MAP.md"), note("- `a.md`\n- `@personal/a.md`"));
|
|
15
|
-
const before = readFileSync(join(root, "personal/a.md"));
|
|
8
|
+
const note = "---\norigin: self\nstatus: active\nstale: false\ncreated_at: 2026-01-01T00:00:00Z\nupdated_at: 2026-01-01T00:00:00Z\nlast_accessed: 2026-01-01T00:00:00Z\naccess_count: 0\n---\n\n";
|
|
9
|
+
test("doctor validates without repairing files or running the dreamer", async () => {
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), "dream-doctor-test-"));
|
|
11
|
+
mkdirSync(join(root, "human"));
|
|
12
|
+
writeFileSync(join(root, "human/a.md"), note);
|
|
13
|
+
writeFileSync(join(root, "human/MAP.md"), `${note}- \`a.md\``);
|
|
14
|
+
const before = readFileSync(join(root, "human/a.md"));
|
|
16
15
|
assert.deepEqual(doctor(root), []);
|
|
17
16
|
assert.equal(await main(["doctor", "--notes-home", root], { runDreamer: async () => { throw new Error("must not run"); } }), 0);
|
|
18
|
-
assert.deepEqual(readFileSync(join(root, "
|
|
19
|
-
assert.deepEqual(readdirSync(root), ["
|
|
20
|
-
|
|
21
|
-
test("doctor reports layout, metadata, links and locks; never repairs", () => {
|
|
22
|
-
const root = home();
|
|
23
|
-
mkdirSync(join(root, "global"));
|
|
24
|
-
mkdirSync(join(root, "project/bad"), { recursive: true });
|
|
25
|
-
writeFileSync(join(root, "project/bad/MAP.md"), note("`missing.md` `@global/old.md`"));
|
|
26
|
-
writeFileSync(join(root, "project/bad/broken.md"), "---\norigin: nope\n---\n");
|
|
27
|
-
writeFileSync(join(root, ".dream.lock"), "garbage");
|
|
28
|
-
symlinkSync(join(root, "project"), join(root, "project/bad/link"));
|
|
29
|
-
const output = doctor(root).join("\n");
|
|
30
|
-
for (const expected of ["legacy home", "invalid project key", "target missing", "invalid address", "invalid origin", "invalid created_at", "malformed lock", "symlink"])
|
|
31
|
-
assert.ok(output.includes(expected), expected);
|
|
32
|
-
assert.equal(readFileSync(join(root, ".dream.lock"), "utf8"), "garbage");
|
|
33
|
-
assert.ok(existsSync(join(root, "global")));
|
|
34
|
-
});
|
|
35
|
-
test("doctor reports missing homes without creating them, including CLI", async () => {
|
|
36
|
-
const missing = join(home(), "absent");
|
|
17
|
+
assert.deepEqual(readFileSync(join(root, "human/a.md")), before);
|
|
18
|
+
assert.deepEqual(readdirSync(root), ["human"]);
|
|
19
|
+
const missing = join(root, "absent");
|
|
37
20
|
assert.equal(await main(["doctor", "--notes-home", missing]), 1);
|
|
38
21
|
assert.equal(existsSync(missing), false);
|
|
39
22
|
});
|
|
40
|
-
test("doctor reports valid lock presence without claiming it is stale", () => {
|
|
41
|
-
const root = home();
|
|
42
|
-
writeFileSync(join(root, ".dream.lock"), "123 12345678-1234-1234-1234-123456789abc");
|
|
43
|
-
assert.match(doctor(root).join("\n"), /lock present.*liveness not inferred/);
|
|
44
|
-
});
|