@astrosheep/pi-context 0.25.2 → 0.26.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 +88 -7
- package/dist/build-info.json +2 -2
- package/dist/extension.js +613 -367
- package/dist/src/context/boot.d.ts +24 -0
- package/dist/src/context/boot.js +33 -24
- package/dist/src/context/budget.d.ts +9 -0
- package/dist/src/context/budget.js +16 -12
- package/dist/src/context/context-window.d.ts +41 -0
- package/dist/src/context/context-window.js +16 -1
- package/dist/src/context/prompts.d.ts +20 -0
- package/dist/src/context/prompts.js +1 -1
- package/dist/src/context/reset-artifacts.d.ts +26 -0
- package/dist/src/context/reset-artifacts.js +18 -17
- package/dist/src/context/reset-lifecycle.d.ts +89 -0
- package/dist/src/context/reset-lifecycle.js +103 -75
- package/dist/src/context/runtime.d.ts +3 -0
- package/dist/src/context/runtime.js +53 -21
- package/dist/src/context/thresholds.d.ts +33 -0
- package/dist/src/context/thresholds.js +1 -1
- package/dist/src/dream/cli.d.ts +10 -0
- package/dist/src/dream/cli.js +1 -1
- package/dist/src/dream/doctor.d.ts +2 -0
- package/dist/src/dream/doctor.js +6 -2
- package/dist/src/dream/gates.d.ts +10 -0
- package/dist/src/dream/git.d.ts +21 -0
- package/dist/src/dream/lock.d.ts +31 -0
- package/dist/src/dream/runner.d.ts +30 -0
- package/dist/src/dream/settings.d.ts +16 -0
- package/dist/src/history/history-tools.d.ts +2 -0
- package/dist/src/history/history.d.ts +57 -0
- package/dist/src/index.d.ts +39 -0
- package/dist/src/index.js +4 -4
- package/dist/src/notes/address.d.ts +26 -0
- package/dist/src/notes/address.js +8 -14
- package/dist/src/notes/constants.d.ts +3 -0
- package/dist/src/notes/constants.js +3 -0
- package/dist/src/notes/context.d.ts +10 -0
- package/dist/src/notes/context.js +33 -0
- package/dist/src/notes/frontmatter.d.ts +46 -0
- package/dist/src/notes/frontmatter.js +10 -5
- package/dist/src/notes/index.d.ts +4 -0
- package/dist/src/notes/index.js +2 -0
- package/dist/src/notes/paths.d.ts +21 -0
- package/dist/src/notes/paths.js +72 -76
- package/dist/src/notes/store.d.ts +94 -0
- package/dist/src/notes/store.js +298 -242
- package/dist/src/pi/notes/adapter.d.ts +12 -0
- package/dist/src/pi/notes/adapter.js +39 -0
- package/dist/src/pi/notes/session-replay.d.ts +16 -0
- package/dist/src/{notes → pi/notes}/session-replay.js +2 -2
- package/dist/src/pi/notes/snapshot.d.ts +33 -0
- package/dist/src/{notes/notes-snapshot.js → pi/notes/snapshot.js} +11 -3
- package/dist/src/pi/notes/tools.d.ts +2 -0
- package/dist/src/{notes → pi/notes}/tools.js +24 -21
- package/dist/src/protocol.d.ts +41 -0
- package/dist/src/protocol.js +4 -6
- package/dist/src/session-reader.d.ts +5 -0
- package/dist/src/settings.d.ts +6 -0
- package/dist/src/tool-output.d.ts +101 -0
- package/dist/src/tool-schema.d.ts +17 -0
- package/dist/test/agent-loop.test.d.ts +1 -0
- package/dist/test/agent-loop.test.js +309 -10
- package/dist/test/boot.integration.test.d.ts +1 -0
- package/dist/test/boot.integration.test.js +55 -29
- package/dist/test/budget-settings.integration.test.d.ts +1 -0
- package/dist/test/budget-settings.integration.test.js +8 -7
- package/dist/test/doctor.test.d.ts +1 -0
- package/dist/test/doctor.test.js +10 -2
- package/dist/test/dream-skill.test.d.ts +1 -0
- package/dist/test/dream-skill.test.js +69 -0
- package/dist/test/dream.test.d.ts +1 -0
- package/dist/test/helpers/extension.d.ts +115 -0
- package/dist/test/helpers/extension.js +6 -6
- package/dist/test/helpers/notes.d.ts +6 -0
- package/dist/test/helpers/notes.js +13 -0
- package/dist/test/history.integration.test.d.ts +1 -0
- package/dist/test/notes-library.test.d.ts +1 -0
- package/dist/test/notes-library.test.js +111 -0
- package/dist/test/notes.integration.test.d.ts +1 -0
- package/dist/test/notes.integration.test.js +22 -24
- package/dist/test/notes.test.d.ts +1 -0
- package/dist/test/notes.test.js +137 -7
- package/dist/test/reset-lifecycle.test.d.ts +1 -0
- package/dist/test/reset-lifecycle.test.js +142 -85
- package/docs/architecture.md +8 -8
- package/docs/reset-lifecycle.md +63 -79
- package/package.json +35 -2
- package/playbook.md +33 -32
- package/skills/dream/SKILL.md +12 -0
- package/src/context/boot.ts +44 -25
- package/src/context/budget.ts +19 -11
- package/src/context/context-window.ts +16 -1
- package/src/context/prompts.ts +2 -2
- package/src/context/reset-artifacts.ts +26 -24
- package/src/context/reset-lifecycle.ts +117 -111
- package/src/context/runtime.ts +50 -22
- package/src/context/thresholds.ts +1 -1
- package/src/dream/cli.ts +1 -1
- package/src/dream/doctor.ts +5 -2
- package/src/index.ts +4 -4
- package/src/notes/address.ts +9 -15
- package/src/notes/constants.ts +3 -0
- package/src/notes/context.ts +40 -0
- package/src/notes/frontmatter.ts +18 -12
- package/src/notes/index.ts +22 -0
- package/src/notes/paths.ts +64 -78
- package/src/notes/store.ts +308 -244
- package/src/pi/notes/adapter.ts +44 -0
- package/src/{notes → pi/notes}/session-replay.ts +3 -3
- package/src/{notes/notes-snapshot.ts → pi/notes/snapshot.ts} +13 -4
- package/src/{notes → pi/notes}/tools.ts +25 -23
- package/src/protocol.ts +5 -6
package/dist/test/notes.test.js
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
* HERMETIC: every test points PI_NOTES_HOME at its own temp root; no real ~/.agents is touched.
|
|
8
8
|
*/
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
|
-
import {
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
11
12
|
import { join } from "node:path";
|
|
12
13
|
import test from "node:test";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
14
|
+
import { parseNote } from "../src/notes/frontmatter.js";
|
|
15
|
+
import { projectKey } from "../src/notes/paths.js";
|
|
16
|
+
import { listNotes, physicalPath, scopeDir } from "./helpers/notes.js";
|
|
17
|
+
import { call, context, explicitBoot, installExtensionTestEnvironment, makeExtension, manager, resultJson, resultRead } from "./helpers/extension.js";
|
|
15
18
|
const testEnvironment = installExtensionTestEnvironment("pi-context-notes");
|
|
16
19
|
test.beforeEach(() => testEnvironment.beforeEach());
|
|
17
20
|
test.afterEach(() => testEnvironment.afterEach());
|
|
@@ -22,7 +25,7 @@ function freshRoot() {
|
|
|
22
25
|
function setUpdatedAt(scope, path, ctx, timestamp) {
|
|
23
26
|
const file = physicalPath(scope, path, ctx);
|
|
24
27
|
const raw = readFileSync(file, "utf8");
|
|
25
|
-
writeFileSync(file, raw.replace(/^
|
|
28
|
+
writeFileSync(file, raw.replace(/^updatedAt: .*$/m, `updatedAt: ${new Date(timestamp).toISOString()}`));
|
|
26
29
|
}
|
|
27
30
|
function assertNoPublicScope(value, label) {
|
|
28
31
|
if (Array.isArray(value)) {
|
|
@@ -64,21 +67,126 @@ test("write lands a real markdown file with harness frontmatter and a pure body"
|
|
|
64
67
|
const raw = readFileSync(file, "utf8");
|
|
65
68
|
assert.match(raw, /^---\n/, "the file opens with frontmatter");
|
|
66
69
|
assert.match(raw, /\n---\n\nhello$/, "frontmatter is followed by a blank line and the exact body");
|
|
67
|
-
for (const [key, value] of [["origin", "self"], ["status", "active"], ["stale", "false"], ["
|
|
70
|
+
for (const [key, value] of [["origin", "self"], ["status", "active"], ["stale", "false"], ["accessCount", "0"]]) {
|
|
68
71
|
assert.match(raw, new RegExp(`^${key}: ${value}$`, "m"), `frontmatter carries ${key}=${value}`);
|
|
69
72
|
}
|
|
70
73
|
assert.equal(/^scope:/m.test(raw), false, "scope is derived from the file home, never persisted");
|
|
71
|
-
for (const key of ["
|
|
74
|
+
for (const key of ["createdAt", "updatedAt", "lastAccessed"]) {
|
|
72
75
|
assert.match(raw, new RegExp(`^${key}: \\d{4}-\\d{2}-\\d{2}T`, "m"), `frontmatter renders ${key} via localIso`);
|
|
73
76
|
}
|
|
77
|
+
assert.equal(parseNote(raw).meta.project, projectKey(ctx.cwd), "a newly-created session note records its existing project key");
|
|
74
78
|
assert.equal(result.address, "a/b.md");
|
|
75
79
|
assert.equal(result.written, true);
|
|
80
|
+
assert.equal(existsSync(join(scopeDir("session", ctx), ".session.json")), false, "ownership is stored in note frontmatter, not a sidecar");
|
|
76
81
|
// A leading YAML block in user content is stripped from the body.
|
|
77
82
|
await call(captured, "notes_write", { path: "stripped.md", content: "---\nscope: human\nnonsense: true\n---\nreal body" }, ctx);
|
|
78
83
|
const stripped = readFileSync(physicalPath("session", "stripped.md", ctx), "utf8");
|
|
79
84
|
assert.match(stripped, /\n---\n\nreal body$/, "the injected block is not part of the body");
|
|
80
85
|
assert.equal(stripped.includes("nonsense"), false, "the injected block never reaches the file");
|
|
81
86
|
});
|
|
87
|
+
test("session-note project ownership is per note, persistent across sessions, and not reassigned by cwd", async () => {
|
|
88
|
+
freshRoot();
|
|
89
|
+
const cwdA = join(testEnvironment.cwd, "project-a");
|
|
90
|
+
const cwdB = join(testEnvironment.cwd, "project-b");
|
|
91
|
+
mkdirSync(cwdA, { recursive: true });
|
|
92
|
+
mkdirSync(cwdB, { recursive: true });
|
|
93
|
+
const projectA = projectKey(cwdA);
|
|
94
|
+
const projectB = projectKey(cwdB);
|
|
95
|
+
assert.notEqual(projectA, projectB);
|
|
96
|
+
const firstSession = manager();
|
|
97
|
+
const firstCaptured = makeExtension(firstSession);
|
|
98
|
+
const firstCtx = context(firstSession, undefined, undefined, true, cwdA);
|
|
99
|
+
await call(firstCaptured, "notes_write", { path: "first.md", content: "first project session" }, firstCtx);
|
|
100
|
+
const firstFile = physicalPath("session", "first.md", firstCtx);
|
|
101
|
+
assert.equal(parseNote(readFileSync(firstFile, "utf8")).meta.project, projectA);
|
|
102
|
+
const secondSession = manager();
|
|
103
|
+
const secondCaptured = makeExtension(secondSession);
|
|
104
|
+
const secondCtx = context(secondSession, undefined, undefined, true, cwdA);
|
|
105
|
+
await call(secondCaptured, "notes_write", { path: "second.md", content: "same project, another session" }, secondCtx);
|
|
106
|
+
const secondFile = physicalPath("session", "second.md", secondCtx);
|
|
107
|
+
assert.equal(parseNote(readFileSync(secondFile, "utf8")).meta.project, projectA, "another session in the same project carries the matching key");
|
|
108
|
+
const thirdSession = manager();
|
|
109
|
+
const thirdCaptured = makeExtension(thirdSession);
|
|
110
|
+
const thirdCtx = context(thirdSession, undefined, undefined, true, cwdB);
|
|
111
|
+
await call(thirdCaptured, "notes_write", { path: "third.md", content: "different project" }, thirdCtx);
|
|
112
|
+
const thirdFile = physicalPath("session", "third.md", thirdCtx);
|
|
113
|
+
assert.equal(parseNote(readFileSync(thirdFile, "utf8")).meta.project, projectB);
|
|
114
|
+
const projectASessions = [firstFile, secondFile, thirdFile].filter((file) => parseNote(readFileSync(file, "utf8")).meta.project === projectA);
|
|
115
|
+
assert.deepEqual(projectASessions.sort(), [firstFile, secondFile].sort(), "exact frontmatter project matching recognizes only sessions from the same project");
|
|
116
|
+
const movedContext = context(firstSession, undefined, undefined, true, cwdB);
|
|
117
|
+
await call(firstCaptured, "notes_write", { path: "first.md", content: "overwritten from another cwd" }, movedContext);
|
|
118
|
+
assert.equal(parseNote(readFileSync(firstFile, "utf8")).meta.project, projectA, "overwriting an existing note does not silently reassign it");
|
|
119
|
+
await call(firstCaptured, "notes_edit", { path: "first.md", edits: [{ oldText: "overwritten", newText: "edited" }] }, movedContext);
|
|
120
|
+
assert.equal(parseNote(readFileSync(firstFile, "utf8")).meta.project, projectA, "editing an existing note preserves its original project key");
|
|
121
|
+
await call(firstCaptured, "notes_read", { path: "first.md" }, movedContext);
|
|
122
|
+
assert.equal(parseNote(readFileSync(firstFile, "utf8")).meta.project, projectA, "reading preserves existing project ownership");
|
|
123
|
+
await call(firstCaptured, "notes_write", { path: "new-from-project-b.md", content: "new note", scope: "session" }, movedContext);
|
|
124
|
+
assert.equal(parseNote(readFileSync(physicalPath("session", "new-from-project-b.md", movedContext), "utf8")).meta.project, projectB, "only a newly-created session note uses the current project key");
|
|
125
|
+
await call(firstCaptured, "notes_write", { path: "project-note.md", content: "project home note", scope: "project" }, movedContext);
|
|
126
|
+
assert.equal(parseNote(readFileSync(physicalPath("project", "project-note.md", movedContext), "utf8")).meta.project, undefined, "project-home notes do not receive session ownership metadata");
|
|
127
|
+
assert.equal((await listNotes(movedContext, { scope: "session" })).length, 2, "project ownership remains frontmatter, not a separate note");
|
|
128
|
+
});
|
|
129
|
+
test("linked git worktrees share the main checkout's project key", () => {
|
|
130
|
+
freshRoot();
|
|
131
|
+
// realpath keeps the fixture's gitdir pointer and the asserted paths on one spelling
|
|
132
|
+
// (macOS temp roots live under the /var -> /private/var symlink).
|
|
133
|
+
const root = realpathSync(testEnvironment.cwd);
|
|
134
|
+
const main = join(root, "repo");
|
|
135
|
+
mkdirSync(main, { recursive: true });
|
|
136
|
+
const git = (args) => {
|
|
137
|
+
execFileSync("git", ["-c", "user.email=test@test", "-c", "user.name=test", ...args], { cwd: main, stdio: "ignore" });
|
|
138
|
+
};
|
|
139
|
+
git(["init", "-q"]);
|
|
140
|
+
git(["commit", "-q", "--allow-empty", "-m", "init"]);
|
|
141
|
+
const worktree = join(root, "wt");
|
|
142
|
+
git(["worktree", "add", "-q", "--detach", worktree]);
|
|
143
|
+
assert.equal(projectKey(worktree), projectKey(main), "a linked worktree resolves to the main checkout's key");
|
|
144
|
+
assert.equal(projectKey(join(worktree, "gone", "deeper")), projectKey(main), "a nonexistent subdirectory still resolves through its worktree");
|
|
145
|
+
assert.equal(scopeDir("project", context(manager(), undefined, undefined, true, worktree)), scopeDir("project", context(manager(), undefined, undefined, true, main)), "@project uses the same physical home from both checkouts");
|
|
146
|
+
});
|
|
147
|
+
test("legacy metadata is refused for explicit manual migration; invalid project ownership stays unknown", async () => {
|
|
148
|
+
freshRoot();
|
|
149
|
+
const session = manager();
|
|
150
|
+
const captured = makeExtension(session);
|
|
151
|
+
const ctx = context(session);
|
|
152
|
+
const legacyFile = physicalPath("session", "legacy.md", ctx);
|
|
153
|
+
mkdirSync(scopeDir("session", ctx), { recursive: true });
|
|
154
|
+
writeFileSync(legacyFile, `---
|
|
155
|
+
origin: self
|
|
156
|
+
status: active
|
|
157
|
+
stale: false
|
|
158
|
+
created_at: 2026-01-01T00:00:00.000+00:00
|
|
159
|
+
updated_at: 2026-01-01T00:00:00.000+00:00
|
|
160
|
+
last_accessed: 2026-01-01T00:00:00.000+00:00
|
|
161
|
+
access_count: 0
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
legacy body`);
|
|
165
|
+
assert.throws(() => parseNote(readFileSync(legacyFile, "utf8")), /legacy note metadata .*requires manual migration/);
|
|
166
|
+
const legacyBytes = readFileSync(legacyFile, "utf8");
|
|
167
|
+
await assert.rejects(() => listNotes(ctx, { scope: "session" }), /requires manual migration/);
|
|
168
|
+
await assert.rejects(() => call(captured, "notes_read", { path: "legacy.md" }, ctx), /requires manual migration/);
|
|
169
|
+
await assert.rejects(() => call(captured, "notes_write", { path: "legacy.md", content: "legacy overwritten" }, ctx), /requires manual migration/);
|
|
170
|
+
assert.equal(readFileSync(legacyFile, "utf8"), legacyBytes, "refusal preserves the unmigrated file byte-for-byte");
|
|
171
|
+
const invalidFile = physicalPath("session", "invalid.md", ctx);
|
|
172
|
+
writeFileSync(invalidFile, `---
|
|
173
|
+
origin: self
|
|
174
|
+
status: active
|
|
175
|
+
stale: false
|
|
176
|
+
createdAt: 2026-01-01T00:00:00.000+00:00
|
|
177
|
+
updatedAt: 2026-01-01T00:00:00.000+00:00
|
|
178
|
+
lastAccessed: 2026-01-01T00:00:00.000+00:00
|
|
179
|
+
accessCount: 0
|
|
180
|
+
project: 17
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
invalid owner`);
|
|
184
|
+
assert.equal(parseNote(readFileSync(invalidFile, "utf8")).meta.project, 17);
|
|
185
|
+
assert.notEqual(parseNote(readFileSync(invalidFile, "utf8")).meta.project, projectKey(ctx.cwd), "invalid ownership does not match the current project key");
|
|
186
|
+
await call(captured, "notes_write", { path: "invalid.md", content: "still invalid" }, ctx);
|
|
187
|
+
assert.equal(parseNote(readFileSync(invalidFile, "utf8")).meta.project, 17, "an invalid value remains unknown and is not replaced with cwd-derived ownership");
|
|
188
|
+
assert.equal(existsSync(join(scopeDir("session", ctx), ".session.json")), false, "new notes use no ownership sidecar");
|
|
189
|
+
});
|
|
82
190
|
test("edit is body-scoped with named failures and a replace_all escape hatch", async () => {
|
|
83
191
|
freshRoot();
|
|
84
192
|
const session = manager();
|
|
@@ -209,7 +317,29 @@ test("@ addresses select one home, reject illegal sigils, and never fall back",
|
|
|
209
317
|
await assert.rejects(() => call(captured, "notes_write", { address: "bad@name.md", content: "no" }, ctx), /@project\/.*@human\/.*bare names are this session/);
|
|
210
318
|
assert.equal(existsSync(join(root, "human", "bad@name.md")), false, "a bad sigil creates nothing anywhere");
|
|
211
319
|
});
|
|
212
|
-
|
|
320
|
+
test("Pi adapter resolves agent and switched model identity on each notes call and boot", async () => {
|
|
321
|
+
const root = freshRoot();
|
|
322
|
+
const session = manager();
|
|
323
|
+
const captured = makeExtension(session);
|
|
324
|
+
const ctx = context(session, undefined, undefined, true, testEnvironment.cwd, true, "provider/First.Model");
|
|
325
|
+
await withAgent("Test Agent", async () => {
|
|
326
|
+
await call(captured, "notes_write", { address: "@self/private.md", content: "agent note" }, ctx);
|
|
327
|
+
await call(captured, "notes_write", { address: "@model/private.md", content: "first model note" }, ctx);
|
|
328
|
+
ctx.model = context(session, undefined, undefined, true, testEnvironment.cwd, true, "other/Second.Model").model;
|
|
329
|
+
assert.equal(resultJson(await call(captured, "notes_read", { address: "@model/private.md" }, ctx)).error, "note not found", "switched model does not fall back to previous home");
|
|
330
|
+
await call(captured, "notes_write", { address: "@model/private.md", content: "second model note" }, ctx);
|
|
331
|
+
const listed = resultJson(await call(captured, "notes_list", {}, ctx));
|
|
332
|
+
assert.deepEqual(listed.files.map((row) => row.address).sort(), ["@agents/test-agent/private.md", "@models/second-model/private.md"]);
|
|
333
|
+
const boot = await explicitBoot(ctx, "test-window", undefined);
|
|
334
|
+
assert.ok(boot.includes("@models/second-model/private.md"));
|
|
335
|
+
assert.equal(boot.includes("@models/first-model/private.md"), false);
|
|
336
|
+
assert.match(resultRead(await call(captured, "notes_read", { address: "@models/first-model/private.md" }, ctx)).content, /first model note$/);
|
|
337
|
+
const refused = resultJson(await call(captured, "notes_edit", { address: "@models/first-model/private.md", stale: true }, ctx));
|
|
338
|
+
assert.match(refused.error, /not your home/);
|
|
339
|
+
assert.match(readFileSync(join(root, "models/first-model/private.md"), "utf8"), /first model note$/);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
const FRONTMATTER = (body) => `---\norigin: self\nstatus: active\nstale: false\ncreatedAt: 2026-01-01T00:00:00.000+00:00\nupdatedAt: 2026-01-01T00:00:00.000+00:00\nlastAccessed: 2026-01-01T00:00:00.000+00:00\naccessCount: 0\n---\n\n${body}`;
|
|
213
343
|
async function withAgent(name, run) {
|
|
214
344
|
const previous = process.env.PI_NOTES_AGENT;
|
|
215
345
|
if (name === undefined)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -109,22 +109,25 @@ function appendDrafts(sessionManager, entries) {
|
|
|
109
109
|
}
|
|
110
110
|
function turnEndFacts(overrides = {}) {
|
|
111
111
|
return {
|
|
112
|
+
windowId: "pcw:test:window",
|
|
112
113
|
aborted: false,
|
|
113
114
|
overflow: false,
|
|
114
115
|
failed: false,
|
|
115
116
|
enabled: true,
|
|
116
117
|
queued: false,
|
|
117
118
|
automaticResetEnabled: true,
|
|
118
|
-
|
|
119
|
+
hardReserveDue: false,
|
|
119
120
|
...overrides,
|
|
120
121
|
};
|
|
121
122
|
}
|
|
122
123
|
function beforeSettleFacts(overrides = {}) {
|
|
123
124
|
return {
|
|
125
|
+
windowId: "pcw:test:window",
|
|
124
126
|
queued: false,
|
|
125
127
|
enabled: true,
|
|
126
128
|
automaticResetEnabled: true,
|
|
127
129
|
aborted: false,
|
|
130
|
+
failed: false,
|
|
128
131
|
...overrides,
|
|
129
132
|
};
|
|
130
133
|
}
|
|
@@ -156,14 +159,15 @@ test("public reset boundary drafts one marker, one boot, and one continuation af
|
|
|
156
159
|
assert.equal(markerDrafts.length, 1, "duplicate wipe requests in one turn dedupe");
|
|
157
160
|
assert.equal(bootDrafts.length, 1);
|
|
158
161
|
assert.equal(boundary.entries[0]?.type, "custom_message", "ordinary tool-batch entries precede the reset drafts");
|
|
159
|
-
assert.equal(boundary.entries[1]?.type, "
|
|
160
|
-
assert.equal(boundary.entries[2]?.type, "
|
|
162
|
+
assert.equal(boundary.entries[1]?.type, "compaction", "native retain-none checkpoint precedes the reset marker");
|
|
163
|
+
assert.equal(boundary.entries[2]?.type, "custom");
|
|
164
|
+
assert.equal(boundary.entries[3]?.type, "custom_message");
|
|
161
165
|
const windowId = markerDrafts[0].data.windowId;
|
|
162
166
|
assert.match(windowId, /^pcw:/);
|
|
163
167
|
assert.equal(bootDrafts[0].details.windowId, windowId);
|
|
164
168
|
const continuationDrafts = boundary.entries.filter((entry) => entry.type === "custom_message" && entry.customType === internal.CONTINUATION_TYPE);
|
|
165
169
|
assert.equal(continuationDrafts.length, 1, "the boundary persists exactly one reset message");
|
|
166
|
-
assert.equal(boundary.entries[
|
|
170
|
+
assert.equal(boundary.entries[4]?.type, "custom_message", "the continuation closes the ordered reset shape");
|
|
167
171
|
appendDrafts(h.sessionManager, boundary.entries);
|
|
168
172
|
const branch = h.sessionManager.getBranch();
|
|
169
173
|
assert.deepEqual(branch.filter((entry) => entry.type === "custom" && entry.customType === internal.RESET_MARKER_TYPE).map((entry) => entry.type === "custom" ? entry.data : undefined), [{ windowId }]);
|
|
@@ -189,12 +193,14 @@ test("off stops future automatic/manual reset requests while an existing marker
|
|
|
189
193
|
assert.equal(afterOff.entries.length, 0, "off does not create another reset");
|
|
190
194
|
await h.runCommand("pi-context", "on");
|
|
191
195
|
await h.runCommand("wipe-memory");
|
|
192
|
-
assert.equal(h.sent.length,
|
|
193
|
-
assert.equal(h.sent[0]?.triggerTurn,
|
|
194
|
-
assert.equal(h.sent[0]?.customType, internal.
|
|
195
|
-
|
|
196
|
+
assert.equal(h.sent.length, 1, "/wipe-memory sends one hidden close-out warning and starts a normal turn");
|
|
197
|
+
assert.equal(h.sent[0]?.triggerTurn, true);
|
|
198
|
+
assert.equal(h.sent[0]?.customType, internal.WARNING_TYPE);
|
|
199
|
+
const warning = h.sessionManager.getBranch().find((entry) => entry.type === "custom_message" && entry.customType === internal.WARNING_TYPE);
|
|
200
|
+
assert.ok(warning && warning.type === "custom_message");
|
|
201
|
+
assert.equal(warning.content, internal.WARNING_CONTENT);
|
|
196
202
|
const markers = h.sessionManager.getBranch().filter((entry) => entry.type === "custom" && entry.customType === internal.RESET_MARKER_TYPE);
|
|
197
|
-
assert.equal(markers.length,
|
|
203
|
+
assert.equal(markers.length, 1, "manual command waits for an agent boundary; it does not persist an immediate reset");
|
|
198
204
|
});
|
|
199
205
|
test("reset construction failure preserves incoming and budget drafts without continuation", async () => {
|
|
200
206
|
const sessionManager = Manager.inMemory("/private/tmp/pi-context-reset-failure-test");
|
|
@@ -220,13 +226,13 @@ test("reset construction failure preserves incoming and budget drafts without co
|
|
|
220
226
|
isEnabled: () => true,
|
|
221
227
|
budget: {
|
|
222
228
|
automaticResetEnabled: () => true,
|
|
223
|
-
|
|
229
|
+
hardReserveDue: () => false,
|
|
224
230
|
consumeTurnEnd: () => [budgetDraft],
|
|
225
231
|
clear: () => { },
|
|
226
232
|
},
|
|
227
233
|
buildReset: () => { throw new Error("synthetic reset construction failure"); },
|
|
228
234
|
});
|
|
229
|
-
lifecycle.request();
|
|
235
|
+
lifecycle.request(`pcw:${sessionManager.getSessionId().slice(0, 8)}:root`);
|
|
230
236
|
const incoming = { type: "custom_message", customType: "foreign/boundary", content: "foreign draft", display: false };
|
|
231
237
|
const results = [];
|
|
232
238
|
for (const handler of handlers.get("turn_end") ?? [])
|
|
@@ -237,6 +243,52 @@ test("reset construction failure preserves incoming and budget drafts without co
|
|
|
237
243
|
assert.equal(notices.at(-1)?.type, "warning");
|
|
238
244
|
assert.match(notices.at(-1)?.message ?? "", /could not build reset/);
|
|
239
245
|
});
|
|
246
|
+
test("a stale async reset is discarded after a lifecycle switch without staging a notice", async () => {
|
|
247
|
+
const sessionManager = Manager.inMemory("/private/tmp/pi-context-stale-reset-test");
|
|
248
|
+
const handlers = new Map();
|
|
249
|
+
const api = {
|
|
250
|
+
on(name, handler) {
|
|
251
|
+
const list = handlers.get(name) ?? [];
|
|
252
|
+
list.push(handler);
|
|
253
|
+
handlers.set(name, list);
|
|
254
|
+
return () => { };
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
const ctx = {
|
|
258
|
+
sessionManager,
|
|
259
|
+
model: undefined,
|
|
260
|
+
signal: undefined,
|
|
261
|
+
hasPendingMessages: () => false,
|
|
262
|
+
ui: { notify() { } },
|
|
263
|
+
};
|
|
264
|
+
let resolveBuild;
|
|
265
|
+
const pendingBuild = new Promise((resolve) => { resolveBuild = resolve; });
|
|
266
|
+
let readyCount = 0;
|
|
267
|
+
const lifecycle = registerResetLifecycle(api, {
|
|
268
|
+
isEnabled: () => true,
|
|
269
|
+
budget: {
|
|
270
|
+
automaticResetEnabled: () => true,
|
|
271
|
+
hardReserveDue: () => false,
|
|
272
|
+
consumeTurnEnd: () => [],
|
|
273
|
+
clear: () => { },
|
|
274
|
+
},
|
|
275
|
+
buildReset: () => pendingBuild,
|
|
276
|
+
onResetReady: () => { readyCount++; },
|
|
277
|
+
});
|
|
278
|
+
const windowId = `pcw:${sessionManager.getSessionId().slice(0, 8)}:root`;
|
|
279
|
+
lifecycle.request(windowId);
|
|
280
|
+
const incoming = { type: "custom_message", customType: "foreign/boundary", content: "preserve me", display: false };
|
|
281
|
+
const turnEnd = handlers.get("turn_end")?.[0];
|
|
282
|
+
const sessionTree = handlers.get("session_tree")?.[0];
|
|
283
|
+
assert.ok(turnEnd && sessionTree);
|
|
284
|
+
const pending = turnEnd(fakeBoundaryEvent([incoming]), ctx);
|
|
285
|
+
await sessionTree({}, ctx);
|
|
286
|
+
resolveBuild([{ type: "custom_message", customType: internal.BOOT_TYPE, content: "stale", display: false }]);
|
|
287
|
+
const result = resultEntries([await pending]);
|
|
288
|
+
assert.deepEqual(result.entries, [incoming], "a switched lifecycle keeps incoming drafts but discards stale reset drafts");
|
|
289
|
+
assert.equal(result.continue, false);
|
|
290
|
+
assert.equal(readyCount, 0, "stale reset work cannot stage a success notice");
|
|
291
|
+
});
|
|
240
292
|
test("a queued success clears an overflow failure before settle recovery can reset", async () => {
|
|
241
293
|
const sessionManager = Manager.inMemory("/private/tmp/pi-context-queued-overflow-test");
|
|
242
294
|
const handlers = new Map();
|
|
@@ -260,7 +312,7 @@ test("a queued success clears an overflow failure before settle recovery can res
|
|
|
260
312
|
isEnabled: () => true,
|
|
261
313
|
budget: {
|
|
262
314
|
automaticResetEnabled: () => true,
|
|
263
|
-
|
|
315
|
+
hardReserveDue: () => false,
|
|
264
316
|
consumeTurnEnd: () => [],
|
|
265
317
|
clear: () => { },
|
|
266
318
|
},
|
|
@@ -288,55 +340,63 @@ test("a queued success clears an overflow failure before settle recovery can res
|
|
|
288
340
|
assert.equal(await beforeSettle(settled, ctx), undefined, "the successful queued turn clears the stale recovery");
|
|
289
341
|
assert.equal(resetCount, 0);
|
|
290
342
|
});
|
|
291
|
-
test("reset-control:
|
|
292
|
-
const
|
|
293
|
-
const first = reduceResetControl(
|
|
294
|
-
assert.equal(first.effect, "
|
|
295
|
-
assert.deepEqual(first.state, {
|
|
296
|
-
const
|
|
297
|
-
assert.equal(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
assert.
|
|
301
|
-
|
|
343
|
+
test("reset-control: close-out phases deduplicate, span turns, and upgrade to a tool commit", () => {
|
|
344
|
+
const windowId = "pcw:test:window";
|
|
345
|
+
const first = reduceResetControl(initialResetControl(), { type: "close_out", windowId, source: "manual" });
|
|
346
|
+
assert.equal(first.effect, "close-out-armed");
|
|
347
|
+
assert.deepEqual(first.state.request, { phase: "close-out", windowId, source: "manual" });
|
|
348
|
+
const duplicate = reduceResetControl(first.state, { type: "close_out", windowId, source: "manual" });
|
|
349
|
+
assert.equal(duplicate.effect, "already-pending");
|
|
350
|
+
const noteTurn = reduceResetControl(first.state, { type: "turn_end", facts: turnEndFacts() });
|
|
351
|
+
assert.equal(noteTurn.effect, "none");
|
|
352
|
+
assert.deepEqual(noteTurn.state.request, first.state.request, "note/tool turns do not consume close-out");
|
|
353
|
+
const tool = reduceResetControl(noteTurn.state, { type: "tool_request", windowId });
|
|
354
|
+
assert.deepEqual(tool.state.request, { phase: "tool-requested", windowId });
|
|
355
|
+
const commit = reduceResetControl(tool.state, { type: "turn_end", facts: turnEndFacts() });
|
|
356
|
+
assert.equal(commit.effect, "commit-boundary");
|
|
357
|
+
assert.deepEqual(commit.state, initialResetControl());
|
|
302
358
|
});
|
|
303
|
-
test("reset-control:
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
assert.equal(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
assert.equal(
|
|
312
|
-
assert.deepEqual(
|
|
313
|
-
const
|
|
314
|
-
assert.equal(
|
|
315
|
-
|
|
359
|
+
test("reset-control: fallback is normal-stop only; hard reserve remains safety", () => {
|
|
360
|
+
const windowId = "pcw:test:window";
|
|
361
|
+
const manual = reduceResetControl(initialResetControl(), { type: "close_out", windowId, source: "manual" }).state;
|
|
362
|
+
const fallback = reduceResetControl(manual, { type: "before_settle", facts: beforeSettleFacts() });
|
|
363
|
+
assert.equal(fallback.effect, "commit-boundary", "manual close-out works even when automatic resets are disabled");
|
|
364
|
+
assert.deepEqual(fallback.state, initialResetControl());
|
|
365
|
+
const automatic = reduceResetControl(initialResetControl(), { type: "close_out", windowId, source: "automatic" }).state;
|
|
366
|
+
const disabledAuto = reduceResetControl(automatic, { type: "before_settle", facts: beforeSettleFacts({ automaticResetEnabled: false }) });
|
|
367
|
+
assert.equal(disabledAuto.effect, "none");
|
|
368
|
+
assert.deepEqual(disabledAuto.state, initialResetControl());
|
|
369
|
+
const safety = reduceResetControl(initialResetControl(), { type: "turn_end", facts: turnEndFacts({ hardReserveDue: true }) });
|
|
370
|
+
assert.equal(safety.effect, "commit-boundary");
|
|
371
|
+
for (const outcome of [{ failed: true }, { aborted: true }]) {
|
|
372
|
+
const notNormalStop = reduceResetControl(manual, { type: "before_settle", facts: beforeSettleFacts(outcome) });
|
|
373
|
+
assert.equal(notNormalStop.effect, "none");
|
|
374
|
+
assert.deepEqual(notNormalStop.state, initialResetControl());
|
|
375
|
+
}
|
|
376
|
+
assert.deepEqual(reduceResetControl(manual, { type: "settled" }).state, initialResetControl());
|
|
316
377
|
});
|
|
317
|
-
test("reset-control:
|
|
318
|
-
const
|
|
319
|
-
const
|
|
378
|
+
test("reset-control: abort and lifecycle transitions clear every pending phase", () => {
|
|
379
|
+
const windowId = "pcw:test:window";
|
|
380
|
+
const pending = reduceResetControl(initialResetControl(), { type: "close_out", windowId, source: "manual" }).state;
|
|
381
|
+
const aborted = reduceResetControl(pending, { type: "turn_end", facts: turnEndFacts({ aborted: true }) });
|
|
320
382
|
assert.equal(aborted.effect, "none");
|
|
321
|
-
assert.deepEqual(aborted.state, initialResetControl()
|
|
322
|
-
|
|
323
|
-
assert.deepEqual(
|
|
324
|
-
const cleared = reduceResetControl(both, { type: "clear" });
|
|
325
|
-
assert.deepEqual(cleared.state, initialResetControl());
|
|
383
|
+
assert.deepEqual(aborted.state, initialResetControl());
|
|
384
|
+
assert.deepEqual(reduceResetControl(pending, { type: "clear" }).state, initialResetControl());
|
|
385
|
+
assert.deepEqual(reduceResetControl(pending, { type: "settled" }).state, initialResetControl());
|
|
326
386
|
});
|
|
327
387
|
test("reset-control: overflow recovery is armed at turn_end and spent exactly once at settle", () => {
|
|
328
388
|
const armed = reduceResetControl(initialResetControl(), { type: "turn_end", facts: turnEndFacts({ overflow: true, failed: true }) });
|
|
329
|
-
assert.deepEqual(armed.state, { request: "none", overflow: "pending" });
|
|
389
|
+
assert.deepEqual(armed.state, { request: { phase: "none" }, overflow: "pending" });
|
|
330
390
|
const recovered = reduceResetControl(armed.state, { type: "before_settle", facts: beforeSettleFacts() });
|
|
331
391
|
assert.equal(recovered.effect, "recover-overflow", "the first settle commits the bounded recovery");
|
|
332
|
-
assert.deepEqual(recovered.state, { request: "none", overflow: "spent" });
|
|
392
|
+
assert.deepEqual(recovered.state, { request: { phase: "none" }, overflow: "spent" });
|
|
333
393
|
const repeated = reduceResetControl(recovered.state, { type: "before_settle", facts: beforeSettleFacts() });
|
|
334
394
|
assert.equal(repeated.effect, "none", "a spent recovery is never retried");
|
|
335
395
|
const rearmed = reduceResetControl(recovered.state, { type: "turn_end", facts: turnEndFacts({ overflow: true, failed: true }) });
|
|
336
|
-
assert.deepEqual(rearmed.state, { request: "none", overflow: "pending-spent" });
|
|
396
|
+
assert.deepEqual(rearmed.state, { request: { phase: "none" }, overflow: "pending-spent" });
|
|
337
397
|
const bounded = reduceResetControl(rearmed.state, { type: "before_settle", facts: beforeSettleFacts() });
|
|
338
398
|
assert.equal(bounded.effect, "none", "a second failure chain stays bounded to the spent attempt");
|
|
339
|
-
assert.deepEqual(bounded.state, { request: "none", overflow: "spent" });
|
|
399
|
+
assert.deepEqual(bounded.state, { request: { phase: "none" }, overflow: "spent" });
|
|
340
400
|
});
|
|
341
401
|
test("reset-control: a queued turn defers recovery and its success supersedes the failure", () => {
|
|
342
402
|
const armed = reduceResetControl(initialResetControl(), { type: "turn_end", facts: turnEndFacts({ overflow: true, failed: true }) }).state;
|
|
@@ -350,52 +410,40 @@ test("reset-control: disabled mode and explicit aborts disarm overflow without a
|
|
|
350
410
|
const armed = reduceResetControl(initialResetControl(), { type: "turn_end", facts: turnEndFacts({ overflow: true, failed: true }) }).state;
|
|
351
411
|
const disabled = reduceResetControl(armed, { type: "before_settle", facts: beforeSettleFacts({ enabled: false }) });
|
|
352
412
|
assert.equal(disabled.effect, "none");
|
|
353
|
-
assert.deepEqual(disabled.state, { request: "none", overflow: "idle" });
|
|
413
|
+
assert.deepEqual(disabled.state, { request: { phase: "none" }, overflow: "idle" });
|
|
354
414
|
const automaticOff = reduceResetControl(armed, { type: "before_settle", facts: beforeSettleFacts({ automaticResetEnabled: false }) });
|
|
355
415
|
assert.equal(automaticOff.effect, "none");
|
|
356
|
-
assert.deepEqual(automaticOff.state, { request: "none", overflow: "idle" });
|
|
416
|
+
assert.deepEqual(automaticOff.state, { request: { phase: "none" }, overflow: "idle" });
|
|
357
417
|
const aborted = reduceResetControl(armed, { type: "before_settle", facts: beforeSettleFacts({ aborted: true }) });
|
|
358
418
|
assert.equal(aborted.effect, "none");
|
|
359
|
-
assert.deepEqual(aborted.state, { request: "none", overflow: "idle" });
|
|
419
|
+
assert.deepEqual(aborted.state, { request: { phase: "none" }, overflow: "idle" });
|
|
360
420
|
});
|
|
361
|
-
test("reset-control:
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
};
|
|
373
|
-
return { facts, calls };
|
|
421
|
+
test("reset-control: lazy guards preserve overflow policy ordering", () => {
|
|
422
|
+
const calls = { queued: 0, automatic: 0, hardReserve: 0 };
|
|
423
|
+
const facts = {
|
|
424
|
+
windowId: "pcw:test:window",
|
|
425
|
+
aborted: false,
|
|
426
|
+
overflow: true,
|
|
427
|
+
failed: true,
|
|
428
|
+
enabled: true,
|
|
429
|
+
get queued() { calls.queued += 1; return false; },
|
|
430
|
+
get automaticResetEnabled() { calls.automatic += 1; return true; },
|
|
431
|
+
get hardReserveDue() { calls.hardReserve += 1; return true; },
|
|
374
432
|
};
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
assert.deepEqual(aborted.calls, { queued: 0, automatic: 0, threshold: 0 }, "an abort consults no policy guard");
|
|
378
|
-
const failed = tracked({ failed: true });
|
|
379
|
-
reduceResetControl(initialResetControl(), { type: "turn_end", facts: failed.facts });
|
|
380
|
-
assert.deepEqual(failed.calls, { queued: 0, automatic: 0, threshold: 0 }, "a failed turn consults no threshold guard");
|
|
381
|
-
const disabled = tracked({ enabled: false });
|
|
382
|
-
reduceResetControl(initialResetControl(), { type: "turn_end", facts: disabled.facts });
|
|
383
|
-
assert.deepEqual(disabled.calls, { queued: 0, automatic: 0, threshold: 0 }, "a disabled turn consults no threshold guard");
|
|
384
|
-
const overflow = tracked({ overflow: true, queuedResult: false });
|
|
385
|
-
reduceResetControl(initialResetControl(), { type: "turn_end", facts: overflow.facts });
|
|
386
|
-
assert.deepEqual(overflow.calls, { queued: 1, automatic: 1, threshold: 0 }, "an overflow turn consults only the overflow guards");
|
|
387
|
-
const complete = tracked({ thresholdResult: false });
|
|
388
|
-
reduceResetControl(initialResetControl(), { type: "turn_end", facts: complete.facts });
|
|
389
|
-
assert.deepEqual(complete.calls, { queued: 0, automatic: 0, threshold: 1 }, "a completed turn consults only the threshold guard");
|
|
433
|
+
reduceResetControl(initialResetControl(), { type: "turn_end", facts });
|
|
434
|
+
assert.deepEqual(calls, { queued: 1, automatic: 1, hardReserve: 0 }, "overflow checks never consult hard-reserve policy");
|
|
390
435
|
const settleCalls = { queued: 0, enabled: 0, automatic: 0 };
|
|
391
436
|
const settleFacts = {
|
|
437
|
+
windowId: "pcw:test:window",
|
|
392
438
|
get queued() { settleCalls.queued += 1; return true; },
|
|
393
439
|
get enabled() { settleCalls.enabled += 1; return true; },
|
|
394
440
|
get automaticResetEnabled() { settleCalls.automatic += 1; return true; },
|
|
395
441
|
aborted: false,
|
|
442
|
+
failed: false,
|
|
396
443
|
};
|
|
397
|
-
reduceResetControl({
|
|
398
|
-
|
|
444
|
+
const pendingOverflow = reduceResetControl(initialResetControl(), { type: "turn_end", facts: turnEndFacts({ overflow: true, failed: true }) }).state;
|
|
445
|
+
reduceResetControl(pendingOverflow, { type: "before_settle", facts: settleFacts });
|
|
446
|
+
assert.deepEqual(settleCalls, { queued: 1, enabled: 0, automatic: 0 }, "queued settle checks no further overflow policy");
|
|
399
447
|
});
|
|
400
448
|
test("a committed reset places incoming and budget drafts before marker -> boot -> continuation", async () => {
|
|
401
449
|
const sessionManager = Manager.inMemory("/private/tmp/pi-context-reset-ordering-test");
|
|
@@ -421,17 +469,18 @@ test("a committed reset places incoming and budget drafts before marker -> boot
|
|
|
421
469
|
isEnabled: () => true,
|
|
422
470
|
budget: {
|
|
423
471
|
automaticResetEnabled: () => true,
|
|
424
|
-
|
|
472
|
+
hardReserveDue: () => false,
|
|
425
473
|
consumeTurnEnd: () => [budgetDraft],
|
|
426
474
|
clear: () => { },
|
|
427
475
|
},
|
|
428
476
|
buildReset: () => [
|
|
477
|
+
{ type: "compaction", summary: "", firstKeptEntryId: null },
|
|
429
478
|
{ type: "custom", customType: internal.RESET_MARKER_TYPE, data: { windowId } },
|
|
430
479
|
{ type: "custom_message", customType: internal.BOOT_TYPE, content: "boot", display: false, details: { windowId } },
|
|
431
480
|
{ type: "custom_message", customType: internal.CONTINUATION_TYPE, content: "continuation", display: false },
|
|
432
481
|
],
|
|
433
482
|
});
|
|
434
|
-
lifecycle.request();
|
|
483
|
+
lifecycle.request(`pcw:${sessionManager.getSessionId().slice(0, 8)}:root`);
|
|
435
484
|
const incoming = { type: "custom_message", customType: "foreign/boundary", content: "incoming", display: false };
|
|
436
485
|
const results = [];
|
|
437
486
|
for (const handler of handlers.get("turn_end") ?? [])
|
|
@@ -439,12 +488,15 @@ test("a committed reset places incoming and budget drafts before marker -> boot
|
|
|
439
488
|
const result = resultEntries(results);
|
|
440
489
|
assert.equal(result.continue, true);
|
|
441
490
|
const customTypes = (entries) => entries.map((entry) => {
|
|
442
|
-
|
|
491
|
+
if (entry.type === "compaction")
|
|
492
|
+
return "native-compaction";
|
|
493
|
+
assert.ok(entry.type === "custom" || entry.type === "custom_message", "the boundary only carries expected reset drafts here");
|
|
443
494
|
return entry.customType;
|
|
444
495
|
});
|
|
445
496
|
assert.deepEqual(customTypes(result.entries), [
|
|
446
497
|
"foreign/boundary",
|
|
447
498
|
internal.GUIDANCE_TYPE,
|
|
499
|
+
"native-compaction",
|
|
448
500
|
internal.RESET_MARKER_TYPE,
|
|
449
501
|
internal.BOOT_TYPE,
|
|
450
502
|
internal.CONTINUATION_TYPE,
|
|
@@ -452,7 +504,12 @@ test("a committed reset places incoming and budget drafts before marker -> boot
|
|
|
452
504
|
appendDrafts(sessionManager, result.entries);
|
|
453
505
|
const branch = sessionManager.getBranch();
|
|
454
506
|
const markerIndex = branch.findIndex((entry) => entry.type === "custom" && entry.customType === internal.RESET_MARKER_TYPE);
|
|
455
|
-
assert.ok(markerIndex
|
|
507
|
+
assert.ok(markerIndex > 0);
|
|
508
|
+
assert.equal(branch[markerIndex - 1]?.type, "compaction", "the checkpoint is directly before the marker");
|
|
509
|
+
const checkpoint = branch[markerIndex - 1];
|
|
510
|
+
assert.ok(checkpoint?.type === "compaction");
|
|
511
|
+
assert.equal(checkpoint.summary, "");
|
|
512
|
+
assert.equal(checkpoint.firstKeptEntryId, checkpoint.id, "Pi materializes null as retain-none");
|
|
456
513
|
assert.deepEqual(customTypes(branch.slice(markerIndex)), [
|
|
457
514
|
internal.RESET_MARKER_TYPE,
|
|
458
515
|
internal.BOOT_TYPE,
|