Skip to content

Commit dfd776c

Browse files
AIVentureAIVenture
authored andcommitted
Fix PR 75776 rebase regressions
1 parent 1d21818 commit dfd776c

7 files changed

Lines changed: 112 additions & 41 deletions

File tree

src/auto-reply/reply/dispatch-from-config.acp-abort.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ let dispatchReplyFromConfig: typeof import("./dispatch-from-config.js").dispatch
3030
let tryDispatchAcpReplyHook: typeof import("../../plugin-sdk/acp-runtime.js").tryDispatchAcpReplyHook;
3131
let resetInboundDedupe: typeof import("./inbound-dedupe.js").resetInboundDedupe;
3232
let replyRunRegistry: typeof import("./reply-run-registry.js").replyRunRegistry;
33+
let replyRunRegistryTesting: typeof import("./reply-run-registry.js").__testing;
3334
let getActiveReplyRunCount: typeof import("./reply-run-registry.js").getActiveReplyRunCount;
3435
let createReplyOperation: typeof import("./reply-run-registry.js").createReplyOperation;
3536

@@ -155,11 +156,16 @@ describe("dispatchReplyFromConfig ACP abort", () => {
155156
({ dispatchReplyFromConfig } = await import("./dispatch-from-config.js"));
156157
({ tryDispatchAcpReplyHook } = await import("../../plugin-sdk/acp-runtime.js"));
157158
({ resetInboundDedupe } = await import("./inbound-dedupe.js"));
158-
({ replyRunRegistry, getActiveReplyRunCount, createReplyOperation } =
159-
await import("./reply-run-registry.js"));
159+
({
160+
replyRunRegistry,
161+
__testing: replyRunRegistryTesting,
162+
getActiveReplyRunCount,
163+
createReplyOperation,
164+
} = await import("./reply-run-registry.js"));
160165
});
161166

162167
beforeEach(() => {
168+
replyRunRegistryTesting.resetReplyRunRegistry();
163169
setDiscordTestRegistry();
164170
resetInboundDedupe();
165171
acpManagerRuntimeMocks.getAcpSessionManager.mockReset();

src/auto-reply/reply/get-reply-run.media-only.test.ts

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ vi.mock("./session-updates.runtime.js", () => ({
120120
}));
121121

122122
vi.mock("./session-system-events.js", () => ({
123+
drainFormattedSystemEventBlock: vi.fn().mockResolvedValue(undefined),
123124
drainFormattedSystemEvents: vi.fn().mockResolvedValue(undefined),
124125
}));
125126

@@ -130,7 +131,7 @@ vi.mock("./typing-mode.js", () => ({
130131
let runPreparedReply: typeof import("./get-reply-run.js").runPreparedReply;
131132
let runReplyAgent: typeof import("./agent-runner.runtime.js").runReplyAgent;
132133
let routeReply: typeof import("./route-reply.runtime.js").routeReply;
133-
let drainFormattedSystemEvents: typeof import("./session-system-events.js").drainFormattedSystemEvents;
134+
let drainFormattedSystemEventBlock: typeof import("./session-system-events.js").drainFormattedSystemEventBlock;
134135
let resolveTypingMode: typeof import("./typing-mode.js").resolveTypingMode;
135136
let buildDirectChatContext: typeof import("./groups.js").buildDirectChatContext;
136137
let buildGroupChatContext: typeof import("./groups.js").buildGroupChatContext;
@@ -277,7 +278,7 @@ describe("runPreparedReply media-only handling", () => {
277278
({ runPreparedReply } = await import("./get-reply-run.js"));
278279
({ runReplyAgent } = await import("./agent-runner.runtime.js"));
279280
({ routeReply } = await import("./route-reply.runtime.js"));
280-
({ drainFormattedSystemEvents } = await import("./session-system-events.js"));
281+
({ drainFormattedSystemEventBlock } = await import("./session-system-events.js"));
281282
({ resolveTypingMode } = await import("./typing-mode.js"));
282283
({ buildDirectChatContext, buildGroupChatContext } = await import("./groups.js"));
283284
({ buildInboundUserContextPrefix, resolveInboundUserContextPromptJoiner } =
@@ -1517,9 +1518,15 @@ describe("runPreparedReply media-only handling", () => {
15171518
it("re-drains system events after waiting behind an active run", async () => {
15181519
const queueSettings = await import("./queue/settings-runtime.js");
15191520
vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt" });
1520-
vi.mocked(drainFormattedSystemEvents)
1521-
.mockResolvedValueOnce("System: [t] Initial event.")
1522-
.mockResolvedValueOnce("System: [t] Post-compaction context.");
1521+
vi.mocked(drainFormattedSystemEventBlock)
1522+
.mockResolvedValueOnce({
1523+
text: "System: [t] Initial event.",
1524+
forceSenderIsOwnerFalse: false,
1525+
})
1526+
.mockResolvedValueOnce({
1527+
text: "System: [t] Post-compaction context.",
1528+
forceSenderIsOwnerFalse: false,
1529+
});
15231530

15241531
const previousRun = createReplyOperation({
15251532
sessionId: "session-events-after-wait",
@@ -2224,7 +2231,10 @@ describe("runPreparedReply media-only handling", () => {
22242231
});
22252232

22262233
it("routes queued system events into user prompt text, not system prompt context", async () => {
2227-
vi.mocked(drainFormattedSystemEvents).mockResolvedValueOnce("System: [t] Model switched.");
2234+
vi.mocked(drainFormattedSystemEventBlock).mockResolvedValueOnce({
2235+
text: "System: [t] Model switched.",
2236+
forceSenderIsOwnerFalse: false,
2237+
});
22282238

22292239
await runPreparedReply(baseParams());
22302240

@@ -2304,7 +2314,10 @@ describe("runPreparedReply media-only handling", () => {
23042314
});
23052315

23062316
it("keeps sender ownership when drained system events are present", async () => {
2307-
vi.mocked(drainFormattedSystemEvents).mockResolvedValueOnce("System: [t] Trusted event.");
2317+
vi.mocked(drainFormattedSystemEventBlock).mockResolvedValueOnce({
2318+
text: "System: [t] Trusted event.",
2319+
forceSenderIsOwnerFalse: false,
2320+
});
23082321
const params = ownerParams();
23092322

23102323
await runPreparedReply(params);
@@ -2314,9 +2327,10 @@ describe("runPreparedReply media-only handling", () => {
23142327
});
23152328

23162329
it("does not downgrade sender ownership when event text contains the untrusted marker", async () => {
2317-
vi.mocked(drainFormattedSystemEvents).mockResolvedValueOnce(
2318-
"System: [t] Relay text mentions System (untrusted): but event is trusted.",
2319-
);
2330+
vi.mocked(drainFormattedSystemEventBlock).mockResolvedValueOnce({
2331+
text: "System: [t] Relay text mentions System (untrusted): but event is trusted.",
2332+
forceSenderIsOwnerFalse: false,
2333+
});
23202334
const params = ownerParams();
23212335

23222336
await runPreparedReply(params);
@@ -2329,7 +2343,10 @@ describe("runPreparedReply media-only handling", () => {
23292343
// drainFormattedSystemEvents returns the events block; the caller prepends it.
23302344
// The hint must be extracted from the user body BEFORE prepending, so "System:"
23312345
// does not shadow the low|medium|high shorthand.
2332-
vi.mocked(drainFormattedSystemEvents).mockResolvedValueOnce("System: [t] Node connected.");
2346+
vi.mocked(drainFormattedSystemEventBlock).mockResolvedValueOnce({
2347+
text: "System: [t] Node connected.",
2348+
forceSenderIsOwnerFalse: false,
2349+
});
23332350

23342351
await runPreparedReply(
23352352
baseParams({
@@ -2352,7 +2369,10 @@ describe("runPreparedReply media-only handling", () => {
23522369
it("carries system events into followupRun.prompt for deferred turns", async () => {
23532370
// drainFormattedSystemEvents returns the events block; the caller prepends it to
23542371
// effectiveBaseBody for the queue path so deferred turns see events.
2355-
vi.mocked(drainFormattedSystemEvents).mockResolvedValueOnce("System: [t] Node connected.");
2372+
vi.mocked(drainFormattedSystemEventBlock).mockResolvedValueOnce({
2373+
text: "System: [t] Node connected.",
2374+
forceSenderIsOwnerFalse: false,
2375+
});
23562376

23572377
await runPreparedReply(baseParams());
23582378

@@ -2363,7 +2383,7 @@ describe("runPreparedReply media-only handling", () => {
23632383
it("does not strip think-hint token from deferred queue body", async () => {
23642384
// In steer mode the inferred thinkLevel is never consumed, so the first token
23652385
// must not be stripped from the queue/steer body (followupRun.prompt).
2366-
vi.mocked(drainFormattedSystemEvents).mockResolvedValueOnce(undefined);
2386+
vi.mocked(drainFormattedSystemEventBlock).mockResolvedValueOnce(undefined);
23672387

23682388
await runPreparedReply(
23692389
baseParams({

src/auto-reply/reply/get-reply-run.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ import { resolveRoutedDeliveryThreadId } from "./routed-delivery-thread.js";
8181
import { resolveRuntimePolicySessionKey } from "./runtime-policy-session-key.js";
8282
import { resolveBareSessionResetPromptState } from "./session-reset-prompt.js";
8383
import { resolveBareResetBootstrapFileAccess } from "./session-reset-prompt.js";
84-
import { drainFormattedSystemEvents } from "./session-system-events.js";
84+
import { drainFormattedSystemEventBlock } from "./session-system-events.js";
8585
import { buildSessionStartupContextPrelude, shouldApplyStartupContext } from "./startup-context.js";
8686
import { resolveStoredModelOverride } from "./stored-model-override.js";
8787
import { resolveTypingMode } from "./typing-mode.js";
@@ -724,21 +724,25 @@ export async function runPreparedReply(
724724
? `[Thread starter - for context]\n${threadStarterBody}`
725725
: undefined;
726726
const drainedSystemEventBlocks: string[] = [];
727+
let forceSenderIsOwnerFalseFromSystemEvents = false;
727728
const rebuildPromptBodies = async (): Promise<{
728729
prefixedCommandBody: string;
729730
queuedBody: string;
730731
transcriptCommandBody: string;
731732
currentInboundContext?: typeof promptEnvelopeBase.currentInboundContext;
732733
}> => {
733734
if (!useFastReplyRuntime) {
734-
const eventsBlock = await drainFormattedSystemEvents({
735+
const eventsBlock = await drainFormattedSystemEventBlock({
735736
cfg,
736737
sessionKey,
737738
isMainSession,
738739
isNewSession,
739740
});
740741
if (eventsBlock) {
741-
drainedSystemEventBlocks.push(eventsBlock);
742+
drainedSystemEventBlocks.push(eventsBlock.text);
743+
if (eventsBlock.forceSenderIsOwnerFalse) {
744+
forceSenderIsOwnerFalseFromSystemEvents = true;
745+
}
742746
}
743747
}
744748
return buildReplyPromptEnvelope({
@@ -1123,11 +1127,10 @@ export async function runPreparedReply(
11231127
senderName: normalizeOptionalString(sessionCtx.SenderName),
11241128
senderUsername: normalizeOptionalString(sessionCtx.SenderUsername),
11251129
senderE164: normalizeOptionalString(sessionCtx.SenderE164),
1126-
// Queued system events are prompt content in the same trusted session;
1127-
// they do not rewrite the sender identity used by command/action auth.
1128-
senderIsOwner: command.senderIsOwner,
1130+
senderIsOwner: forceSenderIsOwnerFalseFromSystemEvents ? false : command.senderIsOwner,
11291131
traceAuthorized:
1130-
command.senderIsOwner || (ctx.GatewayClientScopes ?? []).includes("operator.admin"),
1132+
(forceSenderIsOwnerFalseFromSystemEvents ? false : command.senderIsOwner) ||
1133+
(ctx.GatewayClientScopes ?? []).includes("operator.admin"),
11311134
sessionFile: preparedSessionState.sessionFile,
11321135
workspaceDir,
11331136
config: cfg,

src/auto-reply/reply/memory-flush.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { parseNonNegativeByteSize } from "../../config/byte-size.js";
44
import { resolveFreshSessionTotalTokens, type SessionEntry } from "../../config/sessions.js";
55
import type { OpenClawConfig } from "../../config/types.openclaw.js";
66

7-
const PREFLIGHT_COMPACTION_MAX_THRESHOLD_TOKENS = 65_000;
7+
const PREFLIGHT_COMPACTION_PROACTIVE_THRESHOLD_TOKENS = 65_000;
8+
const PREFLIGHT_COMPACTION_PROACTIVE_CONTEXT_WINDOW_MAX_TOKENS = 400_000;
89

910
export function resolveMemoryFlushContextWindowTokens(params: {
1011
modelId?: string;
@@ -57,10 +58,13 @@ export function resolvePreflightCompactionThresholdTokens(params: {
5758
}): number {
5859
const contextWindow = Math.max(1, Math.floor(params.contextWindowTokens));
5960
const configuredThreshold = resolveConfiguredCompactionThresholdTokens(params);
60-
const proactiveThreshold = Math.max(
61-
1,
62-
Math.min(PREFLIGHT_COMPACTION_MAX_THRESHOLD_TOKENS, Math.floor(contextWindow * 0.25)),
63-
);
61+
if (
62+
typeof configuredThreshold === "number" &&
63+
contextWindow > PREFLIGHT_COMPACTION_PROACTIVE_CONTEXT_WINDOW_MAX_TOKENS
64+
) {
65+
return configuredThreshold;
66+
}
67+
const proactiveThreshold = Math.max(1, PREFLIGHT_COMPACTION_PROACTIVE_THRESHOLD_TOKENS);
6468
return Math.min(configuredThreshold ?? proactiveThreshold, proactiveThreshold);
6569
}
6670

src/auto-reply/reply/session-system-events.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ function selectGenericSystemEvents(events: readonly SystemEvent[]): SystemEvent[
2121
return events.filter((event) => !isExecCompletionEvent(event.text));
2222
}
2323

24+
export type FormattedSystemEventBlock = {
25+
text: string;
26+
forceSenderIsOwnerFalse: boolean;
27+
};
28+
2429
function compactSystemEvent(line: string): string | null {
2530
const trimmed = line.trim();
2631
if (!trimmed) {
@@ -83,15 +88,16 @@ function formatSystemEventTimestamp(ts: number, cfg: OpenClawConfig) {
8388
);
8489
}
8590

86-
/** Drain queued system events, format as `System:` lines, return the block text (or undefined). */
87-
export async function drainFormattedSystemEvents(params: {
91+
/** Drain queued system events, format as `System:` lines, return the block with authority metadata. */
92+
export async function drainFormattedSystemEventBlock(params: {
8893
cfg: OpenClawConfig;
8994
sessionKey: string;
9095
isMainSession: boolean;
9196
isNewSession: boolean;
92-
}): Promise<string | undefined> {
97+
}): Promise<FormattedSystemEventBlock | undefined> {
9398
const summaryLines: string[] = [];
9499
const systemLines: string[] = [];
100+
let forceSenderIsOwnerFalse = false;
95101
// Exec completions have a dedicated heartbeat prompt; leave those entries queued
96102
// so the heartbeat path can consume and deliver them.
97103
const queued = consumeSelectedSystemEventEntries(
@@ -103,6 +109,9 @@ export async function drainFormattedSystemEvents(params: {
103109
if (!compacted) {
104110
continue;
105111
}
112+
if (event.forceSenderIsOwnerFalse === true) {
113+
forceSenderIsOwnerFalse = true;
114+
}
106115
const timestamp = `[${formatSystemEventTimestamp(event.ts, params.cfg)}]`;
107116
let index = 0;
108117
for (const subline of compacted.split("\n")) {
@@ -126,7 +135,21 @@ export async function drainFormattedSystemEvents(params: {
126135

127136
// Each sub-line gets its own prefix so continuation lines can't be mistaken
128137
// for regular user content.
129-
return summaryLines.length > 0
130-
? [...summaryLines, ...systemLines].join("\n")
131-
: systemLines.join("\n");
138+
return {
139+
text:
140+
summaryLines.length > 0
141+
? [...summaryLines, ...systemLines].join("\n")
142+
: systemLines.join("\n"),
143+
forceSenderIsOwnerFalse,
144+
};
145+
}
146+
147+
/** Drain queued system events, format as `System:` lines, return the block text (or undefined). */
148+
export async function drainFormattedSystemEvents(params: {
149+
cfg: OpenClawConfig;
150+
sessionKey: string;
151+
isMainSession: boolean;
152+
isNewSession: boolean;
153+
}): Promise<string | undefined> {
154+
return (await drainFormattedSystemEventBlock(params))?.text;
132155
}

src/gateway/server-methods/chat.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2140,7 +2140,7 @@ export const chatHandlers: GatewayRequestHandlers = {
21402140
runIds: res.aborted ? [runId] : [],
21412141
});
21422142
},
2143-
"chat.send": async ({ params, respond, context, client }) => {
2143+
"chat.send": async ({ params, respond, context, client, isWebchatConnect }) => {
21442144
if (!validateChatSendParams(params)) {
21452145
respond(
21462146
false,
@@ -2514,12 +2514,14 @@ export const chatHandlers: GatewayRequestHandlers = {
25142514
status: "started" as const,
25152515
};
25162516
respond(true, ackPayload, undefined, { runId: clientRunId });
2517-
broadcastWebchatPreflightAcknowledgement({
2518-
context,
2519-
runId: clientRunId,
2520-
sessionKey,
2521-
message: parsedMessage,
2522-
});
2517+
if (client?.connect && isWebchatConnect(client.connect)) {
2518+
broadcastWebchatPreflightAcknowledgement({
2519+
context,
2520+
runId: clientRunId,
2521+
sessionKey,
2522+
message: parsedMessage,
2523+
});
2524+
}
25232525
const persistedImagesPromise = persistChatSendImages({
25242526
images: parsedImages,
25252527
imageOrder,

test/vitest/vitest.auto-reply-reply.config.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,27 @@ import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
22
import { autoReplyReplySubtreeTestInclude } from "./vitest.test-shards.mjs";
33

44
export function createAutoReplyReplyVitestConfig(env?: Record<string, string | undefined>) {
5-
return createScopedVitestConfig([...autoReplyReplySubtreeTestInclude], {
5+
const config = createScopedVitestConfig([...autoReplyReplySubtreeTestInclude], {
66
dir: "src/auto-reply",
77
env,
88
name: "auto-reply-reply",
99
sequence: {
1010
groupOrder: 1,
1111
},
1212
});
13+
// This shard uses the non-isolated runner and shared reply dispatch/abort
14+
// registries. Keep files serialized so ACP abort tests cannot race other
15+
// dispatch tests that intentionally mutate the same singleton state.
16+
config.test = {
17+
...config.test,
18+
maxWorkers: 1,
19+
fileParallelism: false,
20+
sequence: {
21+
...config.test?.sequence,
22+
groupOrder: 1,
23+
},
24+
};
25+
return config;
1326
}
1427

1528
export default createAutoReplyReplyVitestConfig();

0 commit comments

Comments
 (0)