Skip to content

Commit a3747b1

Browse files
committed
fix: add compaction headroom for memory writes
1 parent 96e4fdb commit a3747b1

6 files changed

Lines changed: 90 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
- Docker: allow optional home volume + extra bind mounts in `docker-setup.sh`. (#679) — thanks @gabriel-trigo.
3535

3636
### Fixes
37+
- Agents/Pi: raise compaction `reserveTokens` floor to 20k to leave enough headroom for pre-compaction “memory write” turns.
38+
- Auto-reply: suppress draft/typing streaming for `NO_REPLY` (silent system ops) so it doesn’t leak partial output.
3739
- CLI/Status: expand tables to full terminal width; clarify provider setup vs runtime warnings; richer per-provider detail; token previews in `status` while keeping `status --all` redacted; add troubleshooting link footer; keep log tails pasteable; show gateway auth used when reachable; surface provider runtime errors (Signal/iMessage/Slack); harden `tailscale status --json` parsing; make `status --all` scan progress determinate; and replace the footer with a 3-line “Next steps” recommendation (share/debug/probe).
3840
- CLI/Gateway: clarify that `clawdbot gateway status` reports RPC health (connect + RPC) and shows RPC failures separately from connect failures.
3941
- CLI/Update: gate progress spinner on stdout TTY and align clean-check step label. (#701) — thanks @bjesuiter.

src/agents/pi-embedded-runner.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {
6969
import { normalizeModelCompat } from "./model-compat.js";
7070
import { ensureClawdbotModelsJson } from "./models-config.js";
7171
import type { MessagingToolSend } from "./pi-embedded-messaging.js";
72+
import { ensurePiCompactionReserveTokens } from "./pi-settings.js";
7273
import { acquireSessionWriteLock } from "./session-write-lock.js";
7374

7475
export type { MessagingToolSend } from "./pi-embedded-messaging.js";
@@ -981,6 +982,7 @@ export async function compactEmbeddedPiSession(params: {
981982
effectiveWorkspace,
982983
agentDir,
983984
);
985+
ensurePiCompactionReserveTokens({ settingsManager });
984986
const additionalExtensionPaths = buildEmbeddedExtensionPaths({
985987
cfg: params.config,
986988
sessionManager,
@@ -1369,6 +1371,7 @@ export async function runEmbeddedPiAgent(params: {
13691371
effectiveWorkspace,
13701372
agentDir,
13711373
);
1374+
ensurePiCompactionReserveTokens({ settingsManager });
13721375
const additionalExtensionPaths = buildEmbeddedExtensionPaths({
13731376
cfg: params.config,
13741377
sessionManager,

src/agents/pi-settings.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
import {
4+
DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR,
5+
ensurePiCompactionReserveTokens,
6+
} from "./pi-settings.js";
7+
8+
describe("ensurePiCompactionReserveTokens", () => {
9+
it("bumps reserveTokens when below floor", () => {
10+
const settingsManager = {
11+
getCompactionReserveTokens: () => 16_384,
12+
applyOverrides: vi.fn(),
13+
};
14+
15+
const result = ensurePiCompactionReserveTokens({ settingsManager });
16+
17+
expect(result).toEqual({
18+
didOverride: true,
19+
reserveTokens: DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR,
20+
});
21+
expect(settingsManager.applyOverrides).toHaveBeenCalledWith({
22+
compaction: { reserveTokens: DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR },
23+
});
24+
});
25+
26+
it("does not override when already above floor", () => {
27+
const settingsManager = {
28+
getCompactionReserveTokens: () => 32_000,
29+
applyOverrides: vi.fn(),
30+
};
31+
32+
const result = ensurePiCompactionReserveTokens({ settingsManager });
33+
34+
expect(result).toEqual({ didOverride: false, reserveTokens: 32_000 });
35+
expect(settingsManager.applyOverrides).not.toHaveBeenCalled();
36+
});
37+
});

src/agents/pi-settings.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export const DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR = 20_000;
2+
3+
type PiSettingsManagerLike = {
4+
getCompactionReserveTokens: () => number;
5+
applyOverrides: (overrides: {
6+
compaction: { reserveTokens: number };
7+
}) => void;
8+
};
9+
10+
export function ensurePiCompactionReserveTokens(params: {
11+
settingsManager: PiSettingsManagerLike;
12+
minReserveTokens?: number;
13+
}): { didOverride: boolean; reserveTokens: number } {
14+
const minReserveTokens =
15+
params.minReserveTokens ?? DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR;
16+
const current = params.settingsManager.getCompactionReserveTokens();
17+
18+
if (current >= minReserveTokens) {
19+
return { didOverride: false, reserveTokens: current };
20+
}
21+
22+
params.settingsManager.applyOverrides({
23+
compaction: { reserveTokens: minReserveTokens },
24+
});
25+
26+
return { didOverride: true, reserveTokens: minReserveTokens };
27+
}

src/auto-reply/reply/agent-runner.heartbeat-typing.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,25 @@ describe("runReplyAgent typing (heartbeat)", () => {
164164
expect(typing.startTypingLoop).not.toHaveBeenCalled();
165165
});
166166

167+
it("suppresses partial streaming for NO_REPLY", async () => {
168+
const onPartialReply = vi.fn();
169+
runEmbeddedPiAgentMock.mockImplementationOnce(
170+
async (params: EmbeddedPiAgentParams) => {
171+
await params.onPartialReply?.({ text: "NO_REPLY" });
172+
return { payloads: [{ text: "NO_REPLY" }], meta: {} };
173+
},
174+
);
175+
176+
const { run, typing } = createMinimalRun({
177+
opts: { isHeartbeat: false, onPartialReply },
178+
});
179+
await run();
180+
181+
expect(onPartialReply).not.toHaveBeenCalled();
182+
expect(typing.startTypingOnText).not.toHaveBeenCalled();
183+
expect(typing.startTypingLoop).not.toHaveBeenCalled();
184+
});
185+
167186
it("starts typing only on deltas in message mode", async () => {
168187
runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({
169188
payloads: [{ text: "final" }],

src/auto-reply/reply/agent-runner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636
import { stripHeartbeatToken } from "../heartbeat.js";
3737
import type { OriginatingChannelType, TemplateContext } from "../templating.js";
3838
import { normalizeVerboseLevel, type VerboseLevel } from "../thinking.js";
39-
import { SILENT_REPLY_TOKEN } from "../tokens.js";
39+
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
4040
import type { GetReplyOptions, ReplyPayload } from "../types.js";
4141
import {
4242
createAudioAsVoiceBuffer,
@@ -485,6 +485,7 @@ export async function runReplyAgent(params: {
485485
}
486486
text = stripped.text;
487487
}
488+
if (isSilentReplyText(text, SILENT_REPLY_TOKEN)) return;
488489
await typingSignals.signalTextDelta(text);
489490
await opts.onPartialReply?.({
490491
text,

0 commit comments

Comments
 (0)