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

推荐订阅源

WordPress大学
WordPress大学
G
Google Developers Blog
M
MIT News - Artificial intelligence
Vercel News
Vercel News
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
GbyAI
GbyAI
B
Blog RSS Feed
Blog — PlanetScale
Blog — PlanetScale
Microsoft Security Blog
Microsoft Security Blog
V
Visual Studio Blog
Stack Overflow Blog
Stack Overflow Blog
雷峰网
雷峰网
The Cloudflare Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
F
Fortinet All Blogs
L
LangChain Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
A
About on SuperTechFans
T
The Blog of Author Tim Ferriss
B
Blog
J
Java Code Geeks
Hugging Face - Blog
Hugging Face - Blog
I
InfoQ
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com

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
fix: deliver compaction hook messages (#76651) (thanks @s...
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

}