惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Attack and Defense Labs
Attack and Defense Labs
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园_首页
博客园 - 司徒正美
月光博客
月光博客
Apple Machine Learning Research
Apple Machine Learning Research
T
Tailwind CSS Blog
Jina AI
Jina AI
GbyAI
GbyAI
Y
Y Combinator Blog
罗磊的独立博客
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
P
Proofpoint News Feed
Last Week in AI
Last Week in AI
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
量子位
雷峰网
雷峰网
博客园 - 【当耐特】
H
Hackread – Cybersecurity News, Data Breaches, AI and More
The GitHub Blog
The GitHub Blog
S
Secure Thoughts
博客园 - 三生石上(FineUI控件)
Cyberwarzone
Cyberwarzone
NISL@THU
NISL@THU
J
Java Code Geeks
C
Cisco Blogs
人人都是产品经理
人人都是产品经理
Webroot Blog
Webroot Blog
腾讯CDC
博客园 - 叶小钗
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Troy Hunt's Blog
AI
AI
L
LangChain Blog
Know Your Adversary
Know Your Adversary
T
Tenable Blog
M
MIT News - Artificial intelligence
P
Privacy & Cybersecurity Law Blog
L
LINUX DO - 最新话题
Hugging Face - Blog
Hugging Face - Blog
F
Full Disclosure
P
Proofpoint News Feed
AWS News Blog
AWS News Blog
有赞技术团队
有赞技术团队
A
Arctic Wolf
Security Archives - TechRepublic
Security Archives - TechRepublic
S
Schneier on Security
Recent Commits to openclaw:main
Recent Commits to openclaw:main
W
WeLiveSecurity
The Cloudflare Blog

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4 test: trim doctor command hotspots · openclaw/openclaw@c66f16a test: isolate agent auth and spawn hotspots · openclaw/openclaw@9285935 test: stabilize MCP startup disposal race · openclaw/openclaw@dd9d2eb test: merge browser contract server suites · openclaw/openclaw@5817a76 test: narrow ollama provider discovery setup · openclaw/openclaw@a0d9598 build: declare qa-lab aimock runtime dependency · openclaw/openclaw@24431e5 test: speed up safe-bins exec harness · openclaw/openclaw@ee856ab test: preserve tool helpers in embedded runner mocks · openclaw/openclaw@acd86a0 refactor: move memory embeddings into provider plugins · openclaw/openclaw@77e6e4c test: reuse system-run temp fixtures · openclaw/openclaw@7e9ff0f test: trim hotspot wait overhead · openclaw/openclaw@12a59b0 Check: avoid duplicate boundary prep · openclaw/openclaw@baf11b8 test: reduce hotspot fixture overhead · openclaw/openclaw@3a59edd feat(ui): overhaul settings and slash command UX (#67819) thanks @Bun… · openclaw/openclaw@2cfb660 QA Matrix: exit cleanly on failure · openclaw/openclaw@42805d2 QA Matrix: isolate scenario coverage · openclaw/openclaw@7e659e1 Matrix: refresh crypto bootstrap state · openclaw/openclaw@94081d8 QA Lab: add provider registry · openclaw/openclaw@bb7e982 Matrix: add plugin changelog · openclaw/openclaw@4acab55 test: trim more hotspot overhead · openclaw/openclaw@f485311 test: trim remaining hotspot tests · openclaw/openclaw@6ba8626 test: narrow hotspot mocks · openclaw/openclaw@dbc8179 test: isolate gemini embedding request helpers · openclaw/openclaw@cd330f5 test: trim memory and mcp hotspots · openclaw/openclaw@fd48dfa test: slim provider registry mocks · openclaw/openclaw@2e08c77 test: harden Parallels update smoke · openclaw/openclaw@1a98090 feat: default Anthropic to Opus 4.7 · openclaw/openclaw@628b454 fix: harden node-host shell payload mutability checks · openclaw/openclaw@75c551e fix: land node-host approval binding for native binaries (#66731) (th… · openclaw/openclaw@29919bb CI: add daily schedule to CodeQL workflow (#67645) · openclaw/openclaw@69d25f5 fix(gateway): capture config hash after plugin auto-enable to prevent… · openclaw/openclaw@8c11210 fix: repair sanitized replay tool results before send (#67620) (thank… · openclaw/openclaw@c3c7a99 fix: restrict HTML timeout short-circuit to transient statuses · openclaw/openclaw@de129a6 fix: keep TUI watchdog bound to active run (#67401) (thanks @xantorres) · openclaw/openclaw@3525273 Gateway/skills: dedupe skills prefix-match + drop dead fallback on log · openclaw/openclaw@d7f489f Extensions/lmstudio: back off inference preload after consecutive fai… · openclaw/openclaw@b555214 TUI/streaming: add watchdog that resets the activity indicator after … · openclaw/openclaw@f44ab20 Agents/tool-loop: enable unknown-tool stream guard by default · openclaw/openclaw@36ed367 Gateway/skills: invalidate session skills snapshot on config write · openclaw/openclaw@b23d59a fix: classify HTML provider error pages correctly (#67642) (thanks @s… · openclaw/openclaw@e588e90 fix(skills): remove unused model-usage import (#67641) · openclaw/openclaw@55f05df docs(changelog): credit codex fix superseded PRs · openclaw/openclaw@e485f24 fix(openai-codex): normalize stale transport metadata in resolution a… · openclaw/openclaw@90801ba CI: pin Docker-related GitHub Actions (#67632) · openclaw/openclaw@f697b01 Android: modernize WebView and discovery API usage (#67627) · openclaw/openclaw@44a6e50 fix(deps): bump hono to 4.12.14 and @hono/node-server to 1.19.14 (GHS… · openclaw/openclaw@fbccc18 fix(deps): bump dompurify to 3.4.0 (#67614) · openclaw/openclaw@2c2dc00 CI: add explicit permissions to all workflow jobs (fixes code-scannin… · openclaw/openclaw@01b7516 fix: register bundled TTS providers and route overrides correctly (#6… · openclaw/openclaw@6ea3cdd fix: align host tilde paths with OS home (#62804) (thanks @stainlu) · openclaw/openclaw@ecfaf64 fix: flush creds queue before reconnect socket open (#67464) (thanks … · openclaw/openclaw@405c63f fix: strip standalone <function> tool call tags from visible text (#6… · openclaw/openclaw@78df859 fix(agents): preserve cli session metadata before transcript persist … · openclaw/openclaw@898fd04 docs(changelog): move cli transcript entry · openclaw/openclaw@c1817c6 fix(agents): normalize cli transcript api field · openclaw/openclaw@3a3fae0 docs(changelog): note cli transcript persistence · openclaw/openclaw@6c343f1 fix(agents): persist cli transcript turns · openclaw/openclaw@b8ef507 fix(msteams): harden security-sensitive flows (#65841) · openclaw/openclaw@c56b56e [Dashboard] Fix exec approval modal overflow for long command content… · openclaw/openclaw@053c5b0 Docs: remove QA changelog entry · openclaw/openclaw@7fd5771 QA: fix private runtime source loading (#67428) · openclaw/openclaw@d5933af docs(gateway): correct protocol.md schema path, hello-ok example, aut… · openclaw/openclaw@489404d CI: pin Node 22 runners to 22.18.0 · openclaw/openclaw@4ffa621 models.authStatus: normalize provider ids + tighten env-backed escape… · openclaw/openclaw@f2fdb9d Update CHANGELOG.md · openclaw/openclaw@7694a92 test(parallels): clean up npm update guard jobs · openclaw/openclaw@045ea7b Plugins: prefer scanDir override paths · openclaw/openclaw@b2974da fix(dreaming): default storage.mode to "separate" so phase blocks sto… · openclaw/openclaw@8c392f0 fix(memory-core): skip dreaming transcript ingestion via session stor… · openclaw/openclaw@a1b01f0 fix: dedupe replayed exec.finished node events (#67281) · openclaw/openclaw@5dcf526
fix: deliver compaction hook messages (#76651) (thanks @simplyclever914) · openclaw/openclaw@825ad57
steipete · 2026-05-03 · via Recent Commits to openclaw:main
Original file line numberDiff line numberDiff line change

@@ -71,7 +71,7 @@ Docs: https://docs.openclaw.ai

7171

- Channels/secrets: resolve SecretRef-backed channel credentials through external plugin secret contracts after the plugin split, covering runtime startup, target discovery, webhook auth, disabled-account enumeration, and late-bound web_search config. Fixes #76371. (#76449) Thanks @joshavant and @neeravmakwana.

7272

- Docker/Gateway: pass Docker setup `.env` values into gateway and CLI containers and preserve exec SecretRef `passEnv` keys in managed service plans, so 1Password Connect-backed Discord tokens keep resolving after doctor or plugin repair. Thanks @vincentkoc.

7373

- Control UI/WebChat: explain compaction boundaries in chat history and link directly to session checkpoint controls so pre-compaction turns no longer look silently lost after refresh. Fixes #76415. Thanks @BunsDev.

74-

- Agents/compaction: add an optional bundled compaction notifier hook and retry once from the compacted transcript when automatic compaction leaves a turn without a final visible reply. Thanks @simplyclever914.

74+

- Agents/compaction: add an optional bundled compaction notifier hook and retry once from the compacted transcript when automatic compaction leaves a turn without a final visible reply. (#76651) Thanks @simplyclever914.

7575

- Agents/incomplete-turn: detect and surface a warning when the agent's final text after a tool-call chain is silently dropped because the post-tool assistant response was never produced, instead of completing the turn with only the pre-tool analysis text. Fixes #76477. Thanks @amknight.

7676

- Channels/WhatsApp: attach native outbound mention metadata for group text and media captions by resolving `@+<digits>` and `@<digits>` tokens against WhatsApp participant data, including LID groups. Fixes #39879; carries forward #56863. Thanks @kengi1437, @joe2643, and @fridayck.

7777

- Channels/WhatsApp: require outbound mention tokens to end at a word boundary so phone-number prefixes inside longer strings no longer trigger hidden native mentions.

@@ -80,6 +80,7 @@ Docs: https://docs.openclaw.ai

8080

- Plugins/install: require OpenClaw-owned install provenance before granting official npm plugin scanner trust, so direct npm package names no longer bypass launch-code scanning while catalog, onboarding, and doctor installs stay trusted. Thanks @fede-kamel and @vincentkoc.

8181

- Network proxy: preserve target TLS hostname validation for Node HTTPS requests routed through the managed HTTP proxy, so Discord-style CONNECT traffic no longer validates certificates against the local proxy host. Fixes #74809. (#76442) Thanks @jesse-merhi and @abnershang.

8282

- Gateway/sessions: keep async `sessions.list` title and preview hydration bounded to transcript head/tail reads so Control UI polling cannot full-scan large session transcripts every refresh. Thanks @vincentkoc.

83+

- Gateway/performance: cache per-run verbose-level session reads, skip a redundant `lsof` scan in `gateway --force` when no listener was killed, and make the Gateway startup benchmark print usage for `--help`.

8384

- Gateway/sessions: keep agent runtime metadata on lightweight `sessions.list` rows so model-only session patches do not make Control UI lose runtime identity. Thanks @vincentkoc.

8485

- Gateway/sessions: keep bulk `sessions.list` rows lightweight by skipping per-row transcript usage fallback, display model inference, and plugin projection, avoiding event-loop stalls in large session stores. Thanks @Marvinthebored and @vincentkoc.

8586

- Gateway/models: keep read-only `models.list` fallbacks on persisted/current metadata and configured rows while using static auth checks, so missing `models.json` files no longer runtime-load provider discovery or stall gateway after restart. Fixes #76382; refs #76360 and #75707. Thanks @trojy13, @RayWoo, @AnathemaOfficial, and @vincentkoc.

Original file line numberDiff line numberDiff line change

@@ -647,6 +647,55 @@ describe("compactEmbeddedPiSessionDirect hooks", () => {

647647

tokenCount: 0,

648648

});

649649

});

650+
651+

it("forwards internal compaction hook messages to the caller", async () => {

652+

const onHookMessages = vi.fn();

653+

triggerInternalHook.mockImplementation(async (event: unknown) => {

654+

const hookEvent = event as { action?: string; messages?: string[] };

655+

hookEvent.messages?.push(`${hookEvent.action} notice`);

656+

});

657+

const beforeMetrics = compactTesting.buildBeforeCompactionHookMetrics({

658+

originalMessages: sessionMessages.slice(1) as AgentMessage[],

659+

currentMessages: sessionMessages.slice(1) as AgentMessage[],

660+

estimateTokensFn: estimateTokensMock as (message: AgentMessage) => number,

661+

});

662+
663+

const hookState = await compactTesting.runBeforeCompactionHooks({

664+

hookRunner,

665+

sessionId: "session-1",

666+

sessionKey: "agent:main:session-1",

667+

sessionAgentId: "main",

668+

workspaceDir: "/tmp",

669+

metrics: beforeMetrics,

670+

onHookMessages,

671+

});

672+

await compactTesting.runAfterCompactionHooks({

673+

hookRunner,

674+

sessionId: "session-1",

675+

sessionAgentId: "main",

676+

hookSessionKey: hookState.hookSessionKey,

677+

missingSessionKey: hookState.missingSessionKey,

678+

workspaceDir: "/tmp",

679+

messageCountAfter: 1,

680+

tokensAfter: 10,

681+

compactedCount: 1,

682+

sessionFile: "/tmp/session.jsonl",

683+

onHookMessages,

684+

});

685+
686+

expect(onHookMessages).toHaveBeenNthCalledWith(1, {

687+

phase: "before",

688+

messages: ["compact:before notice"],

689+

sessionId: "session-1",

690+

sessionKey: "agent:main:session-1",

691+

});

692+

expect(onHookMessages).toHaveBeenNthCalledWith(2, {

693+

phase: "after",

694+

messages: ["compact:after notice"],

695+

sessionId: "session-1",

696+

sessionKey: "agent:main:session-1",

697+

});

698+

});

650699

it("emits a transcript update after successful compaction", async () => {

651700

const listener = vi.fn();

652701

const cleanup = onSessionTranscriptUpdate(listener);

Original file line numberDiff line numberDiff line change

@@ -1130,6 +1130,7 @@ async function compactEmbeddedPiSessionDirectOnce(

11301130

workspaceDir: effectiveWorkspace,

11311131

messageProvider: resolvedMessageProvider,

11321132

metrics: beforeHookMetrics,

1133+

onHookMessages: params.onCompactionHookMessages,

11331134

});

11341135

const { messageCountOriginal } = beforeHookMetrics;

11351136

const diagEnabled = log.isEnabled("debug");

@@ -1314,6 +1315,7 @@ async function compactEmbeddedPiSessionDirectOnce(

13141315

summaryLength: typeof result.summary === "string" ? result.summary.length : undefined,

13151316

tokensBefore: result.tokensBefore,

13161317

firstKeptEntryId: effectiveFirstKeptEntryId,

1318+

onHookMessages: params.onCompactionHookMessages,

13171319

});

13181320

return {

13191321

ok: true,

Original file line numberDiff line numberDiff line change

@@ -72,6 +72,12 @@ export type CompactEmbeddedPiSessionParams = {

7272

sourceReplyDeliveryMode?: SourceReplyDeliveryMode;

7373

ownerNumbers?: string[];

7474

abortSignal?: AbortSignal;

75+

onCompactionHookMessages?: (payload: {

76+

phase: "before" | "after";

77+

messages: string[];

78+

sessionId: string;

79+

sessionKey: string;

80+

}) => void | Promise<void>;

7581

/** Allow runtime plugins for this compaction to late-bind the gateway subagent. */

7682

allowGatewaySubagentBinding?: boolean;

7783

};

Original file line numberDiff line numberDiff line change

@@ -178,6 +178,12 @@ export async function runBeforeCompactionHooks(params: {

178178

workspaceDir: string;

179179

messageProvider?: string;

180180

metrics: ReturnType<typeof buildBeforeCompactionHookMetrics>;

181+

onHookMessages?: (payload: {

182+

phase: "before";

183+

messages: string[];

184+

sessionId: string;

185+

sessionKey: string;

186+

}) => void | Promise<void>;

181187

}) {

182188

const missingSessionKey = !params.sessionKey || !params.sessionKey.trim();

183189

const hookSessionKey = params.sessionKey?.trim() || params.sessionId;

@@ -191,6 +197,14 @@ export async function runBeforeCompactionHooks(params: {

191197

tokenCountOriginal: params.metrics.tokenCountOriginal,

192198

});

193199

await triggerInternalHook(hookEvent);

200+

if (hookEvent.messages.length > 0) {

201+

await params.onHookMessages?.({

202+

phase: "before",

203+

messages: hookEvent.messages.slice(),

204+

sessionId: params.sessionId,

205+

sessionKey: hookSessionKey,

206+

});

207+

}

194208

} catch (err) {

195209

log.warn("session:compact:before hook failed", {

196210

errorMessage: formatErrorMessage(err),

@@ -261,6 +275,12 @@ export async function runAfterCompactionHooks(params: {

261275

summaryLength?: number;

262276

tokensBefore?: number;

263277

firstKeptEntryId?: string;

278+

onHookMessages?: (payload: {

279+

phase: "after";

280+

messages: string[];

281+

sessionId: string;

282+

sessionKey: string;

283+

}) => void | Promise<void>;

264284

}) {

265285

try {

266286

const hookEvent = createInternalHookEvent("session", "compact:after", params.hookSessionKey, {

@@ -275,6 +295,14 @@ export async function runAfterCompactionHooks(params: {

275295

firstKeptEntryId: params.firstKeptEntryId,

276296

});

277297

await triggerInternalHook(hookEvent);

298+

if (hookEvent.messages.length > 0) {

299+

await params.onHookMessages?.({

300+

phase: "after",

301+

messages: hookEvent.messages.slice(),

302+

sessionId: params.sessionId,

303+

sessionKey: params.hookSessionKey,

304+

});

305+

}

278306

} catch (err) {

279307

log.warn("session:compact:after hook failed", {

280308

errorMessage: formatErrorMessage(err),

Original file line numberDiff line numberDiff line change

@@ -901,6 +901,24 @@ export async function runEmbeddedPiAgent(

901901

activeSessionFile = nextSessionFile;

902902

}

903903

};

904+

const onCompactionHookMessages = async (payload: {

905+

phase: "before" | "after";

906+

messages: string[];

907+

}) => {

908+

const messages = payload.messages.filter((message) => message.trim().length > 0);

909+

if (messages.length === 0) {

910+

return;

911+

}

912+

await params.onAgentEvent?.({

913+

stream: "compaction",

914+

data: {

915+

phase: payload.phase === "before" ? "start" : "end",

916+

...(payload.phase === "after" ? { completed: true } : {}),

917+

messages,

918+

},

919+

...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),

920+

});

921+

};

904922

// When the engine owns compaction, compactEmbeddedPiSessionDirect is

905923

// bypassed. Fire lifecycle hooks here so recovery paths still notify

906924

// subscribers like memory extensions and usage trackers.

@@ -1369,6 +1387,7 @@ export async function runEmbeddedPiAgent(

13691387

sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,

13701388

ownerNumbers: params.ownerNumbers,

13711389

}),

1390+

onCompactionHookMessages,

13721391

...(attempt.promptCache ? { promptCache: attempt.promptCache } : {}),

13731392

runId: params.runId,

13741393

trigger: "timeout_recovery",

@@ -1525,6 +1544,7 @@ export async function runEmbeddedPiAgent(

15251544

sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,

15261545

ownerNumbers: params.ownerNumbers,

15271546

}),

1547+

onCompactionHookMessages,

15281548

...(attempt.promptCache ? { promptCache: attempt.promptCache } : {}),

15291549

runId: params.runId,

15301550

trigger: "overflow",

Original file line numberDiff line numberDiff line change

@@ -1745,6 +1745,78 @@ describe("runAgentTurnWithFallback", () => {

17451745

);

17461746

});

17471747
1748+

it("delivers compaction hook messages without duplicating notifyUser notices", async () => {

1749+

const onBlockReply = vi.fn();

1750+

state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => {

1751+

await params.onAgentEvent?.({

1752+

stream: "compaction",

1753+

data: { phase: "start", messages: ["Hook before"] },

1754+

});

1755+

await params.onAgentEvent?.({

1756+

stream: "compaction",

1757+

data: { phase: "end", completed: true, messages: ["Hook after"] },

1758+

});

1759+

return { payloads: [{ text: "final" }], meta: {} };

1760+

});

1761+
1762+

const followupRun = createFollowupRun();

1763+

followupRun.run.config = {

1764+

agents: {

1765+

defaults: {

1766+

compaction: {

1767+

notifyUser: true,

1768+

},

1769+

},

1770+

},

1771+

};

1772+
1773+

const runAgentTurnWithFallback = await getRunAgentTurnWithFallback();

1774+

const result = await runAgentTurnWithFallback({

1775+

commandBody: "hello",

1776+

followupRun,

1777+

sessionCtx: {

1778+

Provider: "whatsapp",

1779+

MessageSid: "msg",

1780+

} as unknown as TemplateContext,

1781+

opts: { onBlockReply },

1782+

typingSignals: createMockTypingSignaler(),

1783+

blockReplyPipeline: null,

1784+

blockStreamingEnabled: false,

1785+

resolvedBlockStreamingBreak: "message_end",

1786+

applyReplyToMode: (payload) => payload,

1787+

shouldEmitToolResult: () => true,

1788+

shouldEmitToolOutput: () => false,

1789+

pendingToolTasks: new Set(),

1790+

resetSessionAfterCompactionFailure: async () => false,

1791+

resetSessionAfterRoleOrderingConflict: async () => false,

1792+

isHeartbeat: false,

1793+

sessionKey: "main",

1794+

getActiveSessionEntry: () => undefined,

1795+

resolvedVerboseLevel: "off",

1796+

});

1797+
1798+

expect(result.kind).toBe("success");

1799+

expect(onBlockReply).toHaveBeenCalledTimes(2);

1800+

expect(onBlockReply).toHaveBeenNthCalledWith(

1801+

1,

1802+

expect.objectContaining({

1803+

text: "Hook before",

1804+

replyToId: "msg",

1805+

replyToCurrent: true,

1806+

isCompactionNotice: true,

1807+

}),

1808+

);

1809+

expect(onBlockReply).toHaveBeenNthCalledWith(

1810+

2,

1811+

expect.objectContaining({

1812+

text: "Hook after",

1813+

replyToId: "msg",

1814+

replyToCurrent: true,

1815+

isCompactionNotice: true,

1816+

}),

1817+

);

1818+

});

1819+
17481820

it("prefers onCompactionEnd callback over default notice when notifyUser is enabled", async () => {

17491821

const onBlockReply = vi.fn();

17501822

const onCompactionEnd = vi.fn();

Original file line numberDiff line numberDiff line change

@@ -967,6 +967,31 @@ export async function runAgentTurnWithFallback(params: {

967967

logVerbose(`compaction ${phase} notice delivery failed (non-fatal): ${String(err)}`);

968968

}

969969

};

970+

const readCompactionHookMessages = (value: unknown): string[] => {

971+

if (!Array.isArray(value)) {

972+

return [];

973+

}

974+

return value

975+

.filter((entry): entry is string => typeof entry === "string")

976+

.map((entry) => entry.trim())

977+

.filter((entry) => entry.length > 0);

978+

};

979+

const sendCompactionHookMessages = async (messages: string[]) => {

980+

if (!params.opts?.onBlockReply || messages.length === 0) {

981+

return;

982+

}

983+

const noticePayload = params.applyReplyToMode({

984+

text: messages.join("\n\n"),

985+

replyToId: currentMessageId,

986+

replyToCurrent: true,

987+

isCompactionNotice: true,

988+

});

989+

try {

990+

await params.opts.onBlockReply(noticePayload);

991+

} catch (err) {

992+

logVerbose(`compaction hook notice delivery failed (non-fatal): ${String(err)}`);

993+

}

994+

};

970995

const shouldSurfaceToControlUi = isInternalMessageChannel(

971996

params.followupRun.run.messageProvider ??

972997

params.sessionCtx.Surface ??

@@ -1591,12 +1616,19 @@ export async function runAgentTurnWithFallback(params: {

15911616

// Track auto-compaction and notify higher layers.

15921617

if (evt.stream === "compaction") {

15931618

const phase = readStringValue(evt.data.phase) ?? "";

1619+

const hookMessages = readCompactionHookMessages(evt.data.messages);

15941620

if (phase === "start") {

15951621

// Keep custom compaction callbacks active, but gate the

15961622

// fallback user-facing notice behind explicit opt-in.

15971623

if (params.opts?.onCompactionStart) {

15981624

await params.opts.onCompactionStart();

1599-

} else if (shouldNotifyUserAboutCompaction) {

1625+

}

1626+

if (hookMessages.length > 0) {

1627+

await sendCompactionHookMessages(hookMessages);

1628+

} else if (

1629+

!params.opts?.onCompactionStart &&

1630+

shouldNotifyUserAboutCompaction

1631+

) {

16001632

// Send directly via opts.onBlockReply (bypassing the

16011633

// pipeline) so the notice does not cause final payloads

16021634

// to be discarded on non-streaming model paths.

@@ -1609,9 +1641,17 @@ export async function runAgentTurnWithFallback(params: {

16091641

attemptCompactionCount += 1;

16101642

if (params.opts?.onCompactionEnd) {

16111643

await params.opts.onCompactionEnd();

1612-

} else if (shouldNotifyUserAboutCompaction) {

1644+

}

1645+

if (hookMessages.length > 0) {

1646+

await sendCompactionHookMessages(hookMessages);

1647+

} else if (

1648+

!params.opts?.onCompactionEnd &&

1649+

shouldNotifyUserAboutCompaction

1650+

) {

16131651

await sendCompactionNotice("end");

16141652

}

1653+

} else if (hookMessages.length > 0) {

1654+

await sendCompactionHookMessages(hookMessages);

16151655

} else if (shouldNotifyUserAboutCompaction) {

16161656

await sendCompactionNotice("incomplete");

16171657

}