Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
**Files:**
- Modify: `tests/commands-context.test.ts`
- Modify: `tests/commands-stats.test.ts`
- Modify: `tests/index.test.ts`
- Modify: `src/commands/context.ts`
- Modify: `src/commands/stats.ts`

Expand Down Expand Up @@ -87,6 +88,10 @@ pnpm vitest run tests/commands-context.test.ts tests/commands-stats.test.ts

Expected: PASS.

The extension-level compaction regression must also verify that `dcp:stats`
preserves cumulative values while `dcp:context` reports zero active pruned
tool calls after `session_compact`.

### Task 2: Prove stale-anchor reconciliation and remove stale commentary

**Files:**
Expand Down Expand Up @@ -146,7 +151,7 @@ Do not alter `addAnchorIfAllowed()` or add cleanup to `session_compact`.
Run:

```bash
pnpm vitest run tests/commands-context.test.ts tests/commands-stats.test.ts tests/pipeline.test.ts
pnpm vitest run tests/commands-context.test.ts tests/commands-stats.test.ts tests/index.test.ts tests/pipeline.test.ts
pnpm typecheck
git diff --check
```
Expand All @@ -156,6 +161,6 @@ Expected: all PASS.
- [ ] **Step 5: Commit Phase 2**

```bash
git add src/commands/context.ts src/commands/stats.ts src/messages/inject.ts tests/commands-context.test.ts tests/commands-stats.test.ts tests/pipeline.test.ts
git add docs/superpowers/plans/2026-08-22-dcp-reliability-troubleshooting/phase-02-stats-and-anchor-contracts.md src/commands/context.ts src/commands/stats.ts src/messages/inject.ts tests/commands-context.test.ts tests/commands-stats.test.ts tests/index.test.ts tests/pipeline.test.ts
git commit -m "fix: clarify dcp statistics and anchor cleanup"
```
2 changes: 1 addition & 1 deletion src/commands/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function contextCommand(
lines.push(" Tokens: unavailable");
}

lines.push(` Pruned tool calls: ${state.prune.tools.size}`);
lines.push(` Currently pruned tool calls: ${state.prune.tools.size}`);
lines.push(` Active compression blocks: ${state.prune.messages.activeBlockIds.size}`);
lines.push(` Total blocks: ${state.prune.messages.blocksById.size}`);
lines.push(` Tool cache entries: ${state.toolParameters.size}`);
Expand Down
6 changes: 3 additions & 3 deletions src/commands/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import type { SessionState } from "../state/types.ts";
export function statsCommand(state: SessionState): string {
return [
"DCP Session Statistics:",
` Tools pruned: ${state.stats.toolsPruned}`,
` Total tokens saved (pruning): ${state.stats.totalPruneTokens}`,
` Messages compressed: ${state.stats.messagesCompressed}`,
` Tools pruned this session: ${state.stats.toolsPruned}`,
` Cumulative tokens saved by pruning: ${state.stats.totalPruneTokens}`,
` Messages compressed this session: ${state.stats.messagesCompressed}`,
` Prune token counter: ${state.stats.pruneTokenCounter}`,
].join("\n");
}
4 changes: 0 additions & 4 deletions src/messages/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,6 @@ function addAnchorIfAllowed(
// Anchors not in current messages (stale) are ignored for distance calculation
}

// TODO: Stale anchors (keys not present in current messages) are never pruned from the Sets.
// In sessions with heavy compaction, Sets may grow over time. A future task should clean them
// up — e.g., after compaction by intersecting anchor sets with keys of surviving messages.

if (closestDistance >= frequency) {
anchorSet.add(targetKey);
}
Expand Down
2 changes: 2 additions & 0 deletions tests/commands-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ describe("context command", () => {
expect(result).toContain("200000");
expect(result).toContain("2.5");
expect(result).toContain("Current user turn: 0");
expect(result).toContain("Currently pruned tool calls: 1");
expect(result).not.toContain("\n Pruned tool calls:");
});

it("handles null token values in context usage (E5)", () => {
Expand Down
6 changes: 3 additions & 3 deletions tests/commands-stats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ describe("stats command", () => {
state.stats.messagesCompressed = 3;

const result = statsCommand(state);
expect(result).toContain("5");
expect(result).toContain("1234");
expect(result).toContain("3");
expect(result).toContain("Tools pruned this session: 5");
expect(result).toContain("Cumulative tokens saved by pruning: 1234");
expect(result).toContain("Messages compressed this session: 3");
});
});
96 changes: 91 additions & 5 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -845,14 +845,100 @@ describe("sub-agent support", () => {
}
});

it("session_compact handler clears subagent cache without error", async () => {
const { api, handlers } = createMockApi();
it("preserves cumulative stats while compaction clears active pruning", async () => {
const { api, handlers, commands } = createMockApi();
createExtension(api);

const sessionStartHandler = handlers.get("session_start")?.[0];
await (sessionStartHandler as (...args: unknown[]) => Promise<void>)(
{ reason: "new" },
{
sessionManager: {
getSessionDir: () => "/tmp/test-session-dir",
getSessionId: () => "session",
getBranch: () => [],
},
getContextUsage: () => undefined,
},
);

const contextHandler = handlers.get("context")?.[0];
await (contextHandler as (...args: unknown[]) => Promise<unknown>)(
{
messages: [
{ role: "user", content: [{ type: "text", text: "find the file" }], timestamp: 1001 },
{
role: "assistant",
content: [
{
type: "toolCall",
id: "call-1",
name: "search_files",
arguments: { query: "foo" },
},
],
stopReason: "toolUse",
usage: {
inputTokens: 0,
outputTokens: 0,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
totalTokens: 0,
},
timestamp: 1002,
},
{
role: "toolResult",
toolCallId: "call-1",
toolName: "search_files",
content: [{ type: "text", text: "result" }],
isError: false,
timestamp: 1003,
},
],
},
{
getContextUsage: () => ({ tokens: 1000, contextWindow: 200000, percent: 0.5 }),
},
);

const sweepHandler = commands.get("dcp:sweep") as {
handler: (...args: unknown[]) => Promise<void>;
};
await sweepHandler.handler("", { ui: { notify: vi.fn() } });

const statsHandler = commands.get("dcp:stats") as {
handler: (...args: unknown[]) => Promise<void>;
};
const statsBeforeNotify = vi.fn();
await statsHandler.handler("", { ui: { notify: statsBeforeNotify } });
const statsBefore = statsBeforeNotify.mock.calls[0]?.[0] as string;
expect(statsBefore).toContain("Tools pruned this session: 1");
expect(statsBefore).toMatch(/Cumulative tokens saved by pruning: \d+/);

const contextCommandHandler = commands.get("dcp:context") as {
handler: (...args: unknown[]) => Promise<void>;
};
const contextBeforeNotify = vi.fn();
await contextCommandHandler.handler("", {
getContextUsage: () => undefined,
ui: { notify: contextBeforeNotify },
});
expect(contextBeforeNotify.mock.calls[0]?.[0]).toContain("Currently pruned tool calls: 1");

const sessionCompactHandler = handlers.get("session_compact")?.[0];
await expect(
(sessionCompactHandler as (...args: unknown[]) => Promise<void>)({}, {}),
).resolves.not.toThrow();
await (sessionCompactHandler as (...args: unknown[]) => Promise<void>)({}, {});

const statsAfterNotify = vi.fn();
await statsHandler.handler("", { ui: { notify: statsAfterNotify } });
expect(statsAfterNotify.mock.calls[0]?.[0]).toBe(statsBefore);

const contextAfterNotify = vi.fn();
await contextCommandHandler.handler("", {
getContextUsage: () => undefined,
ui: { notify: contextAfterNotify },
});
expect(contextAfterNotify.mock.calls[0]?.[0]).toContain("Currently pruned tool calls: 0");
});

it("before_agent_start returns early when PI_SUBAGENT_CHILD=1", async () => {
Expand Down
16 changes: 16 additions & 0 deletions tests/pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ describe("runPipeline", () => {
expect(result.messages.length).toBe(2);
});

it("removes stale nudge anchors and preserves surviving anchors", () => {
const state = createSessionState();
const config = makeDefaultConfig();
state.nudges.turnAnchors.add("user:1:0");
state.nudges.turnAnchors.add("user:999:0");
state.nudges.contextLimitAnchors.add("assistant:2:0");
state.nudges.iterationAnchors.add("assistant:998:0");
const messages = [makeUserMessage("kept user", 1), makeAssistantMessage("kept assistant", 2)];

runPipeline(state, config, messages, undefined);

expect(state.nudges.turnAnchors).toEqual(new Set(["user:1:0"]));
expect(state.nudges.contextLimitAnchors).toEqual(new Set(["assistant:2:0"]));
expect(state.nudges.iterationAnchors).toEqual(new Set());
});

it("strips hallucinated DCP tags from assistant messages", () => {
const state = createSessionState();
const config = makeDefaultConfig();
Expand Down