Skip to content

Commit e1eac7a

Browse files
openperfvincentkocsteipete
authored
fix(agents): strip system-event prefix from modelPrompt when before_prompt_build hooks add context (#95349)
* fix(agents): strip system-event prefix from modelPrompt when before_prompt_build hooks add context * fix(agents): preserve prompt boundaries when stripping runtime context * test(agents): assert nested prompt text in boundary regression * fix(agents): anchor runtime context stripping to prompt boundaries * fix(agents): prefer the active prompt boundary * fix(agents): avoid rewriting prompts without hidden context * fix(agents): make prompt stripping helper total * test(agents): drop synthetic prompt boundary proof * fix(agents): anchor prompt stripping to hook boundaries * fix(agents): preserve normalized prompt boundaries * fix(agents): carry prompt build boundaries --------- Co-authored-by: Vincent Koc <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
1 parent 7e0d530 commit e1eac7a

3 files changed

Lines changed: 420 additions & 17 deletions

File tree

src/agents/embedded-agent-runner/run/attempt.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4185,6 +4185,7 @@ export async function runEmbeddedAttempt(
41854185
}
41864186
}
41874187
const promptForModelBeforeRuntimeContextSplit = effectivePrompt;
4188+
const promptForRuntimeContextBeforeAnnotation = promptForRuntimeContextSplit;
41884189
if (!isRawModelRun) {
41894190
promptForRuntimeContextSplit = annotateInterSessionPromptText(
41904191
promptForRuntimeContextSplit,
@@ -4265,6 +4266,16 @@ export async function runEmbeddedAttempt(
42654266
modelPrompt: hasPromptBuildContext
42664267
? promptForModelBeforeRuntimeContextSplit
42674268
: undefined,
4269+
modelPromptBuildContext:
4270+
hasPromptBuildContext && effectiveTranscriptPrompt !== undefined
4271+
? {
4272+
promptBeforeHooks: promptBeforePromptBuildHooks,
4273+
transcriptPromptBeforeTransforms: effectiveTranscriptPrompt,
4274+
promptBeforeAnnotation: promptForRuntimeContextBeforeAnnotation,
4275+
prependContext: promptBuildPrependContext ?? "",
4276+
appendContext: promptBuildAppendContext ?? "",
4277+
}
4278+
: undefined,
42684279
emptyTranscriptMode: params.suppressNextUserMessagePersistence
42694280
? "model-prompt"
42704281
: "runtime-event",

src/agents/embedded-agent-runner/run/runtime-context-prompt.test.ts

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ import {
77
resolveRuntimeContextPromptParts,
88
} from "./runtime-context-prompt.js";
99

10+
function withModelPromptBuildContext(params: {
11+
promptBeforeHooks: string;
12+
transcriptPrompt: string;
13+
promptBeforeAnnotation?: string;
14+
prependContext?: string;
15+
appendContext?: string;
16+
}) {
17+
return {
18+
modelPromptBuildContext: {
19+
promptBeforeHooks: params.promptBeforeHooks,
20+
transcriptPromptBeforeTransforms: params.transcriptPrompt,
21+
promptBeforeAnnotation: params.promptBeforeAnnotation ?? params.promptBeforeHooks,
22+
prependContext: params.prependContext ?? "",
23+
appendContext: params.appendContext ?? "",
24+
},
25+
};
26+
}
27+
1028
describe("runtime context prompt submission", () => {
1129
it("keeps unchanged prompts as a normal user prompt", () => {
1230
expect(
@@ -99,6 +117,260 @@ describe("runtime context prompt submission", () => {
99117
});
100118
});
101119

120+
it("strips system-event prefix from modelPrompt when hooks add prepend context", () => {
121+
// Regression: before_prompt_build hooks that add prependContext set hasPromptBuildContext=true,
122+
// causing modelPrompt=effectivePrompt (with system-event prefix). Without this fix the event
123+
// appeared in both runtimeContext (Message A) and modelPrompt (Message B). #95323
124+
const systemEvent = "System: [2026-06-20 13:59:51] Slack DM from Alice";
125+
const userText = "Hello, what can you do?";
126+
const prependContext = "Hook injected context";
127+
const queuedBody = [systemEvent, "", userText].join("\n");
128+
129+
expect(
130+
resolveRuntimeContextPromptParts({
131+
effectivePrompt: queuedBody,
132+
transcriptPrompt: userText,
133+
modelPrompt: [prependContext, "", queuedBody].join("\n"),
134+
...withModelPromptBuildContext({
135+
promptBeforeHooks: queuedBody,
136+
transcriptPrompt: userText,
137+
prependContext,
138+
}),
139+
}),
140+
).toEqual({
141+
prompt: userText,
142+
modelPrompt: [prependContext, "", userText].join("\n"),
143+
runtimeContext: systemEvent,
144+
});
145+
});
146+
147+
it("strips system-event prefix from modelPrompt when hooks add append context", () => {
148+
const systemEvent = "System: [2026-06-20 13:59:51] Slack DM from Alice";
149+
const userText = "Hello";
150+
const appendContext = "Hook tail context";
151+
const queuedBody = [systemEvent, "", userText].join("\n");
152+
153+
expect(
154+
resolveRuntimeContextPromptParts({
155+
effectivePrompt: queuedBody,
156+
transcriptPrompt: userText,
157+
modelPrompt: [queuedBody, "", appendContext].join("\n"),
158+
...withModelPromptBuildContext({
159+
promptBeforeHooks: queuedBody,
160+
transcriptPrompt: userText,
161+
appendContext,
162+
}),
163+
}),
164+
).toEqual({
165+
prompt: userText,
166+
modelPrompt: [userText, "", appendContext].join("\n"),
167+
runtimeContext: systemEvent,
168+
});
169+
});
170+
171+
it("strips hidden prompt context on both sides without removing repeated hook text", () => {
172+
const systemEvent = "System: [2026-06-20 13:59:51] Slack DM from Alice";
173+
const userText = "Hello";
174+
const untrustedContext = "Untrusted channel metadata";
175+
const hookContext = systemEvent;
176+
const effectivePrompt = [systemEvent, userText, untrustedContext].join("\n\n");
177+
const modelPrompt = [hookContext, effectivePrompt, hookContext].join("\n\n");
178+
179+
expect(
180+
resolveRuntimeContextPromptParts({
181+
effectivePrompt,
182+
transcriptPrompt: userText,
183+
modelPrompt,
184+
...withModelPromptBuildContext({
185+
promptBeforeHooks: effectivePrompt,
186+
transcriptPrompt: userText,
187+
prependContext: hookContext,
188+
appendContext: hookContext,
189+
}),
190+
}),
191+
).toEqual({
192+
prompt: userText,
193+
modelPrompt: [hookContext, userText, hookContext].join("\n\n"),
194+
runtimeContext: [systemEvent, untrustedContext].join("\n\n"),
195+
});
196+
});
197+
198+
it("anchors hidden-context removal before append hooks that repeat the prompt", () => {
199+
const systemEvent = "System: [2026-06-20 13:59:51] Slack DM from Alice";
200+
const userText = "Hello";
201+
const appendContext = "Hook summary: Hello";
202+
203+
expect(
204+
resolveRuntimeContextPromptParts({
205+
effectivePrompt: [systemEvent, userText].join("\n\n"),
206+
transcriptPrompt: userText,
207+
modelPrompt: [systemEvent, userText, appendContext].join("\n\n"),
208+
...withModelPromptBuildContext({
209+
promptBeforeHooks: [systemEvent, userText].join("\n\n"),
210+
transcriptPrompt: userText,
211+
appendContext,
212+
}),
213+
}),
214+
).toEqual({
215+
prompt: userText,
216+
modelPrompt: [userText, appendContext].join("\n\n"),
217+
runtimeContext: systemEvent,
218+
});
219+
});
220+
221+
it("strips the last matching prompt occurrence when prepend hooks quote the body", () => {
222+
const systemEvent = "System: [2026-06-20 13:59:51] Slack DM from Alice";
223+
const userText = "Hello";
224+
const untrustedContext = "Untrusted channel metadata";
225+
const effectivePrompt = [systemEvent, userText, untrustedContext].join("\n\n");
226+
227+
expect(
228+
resolveRuntimeContextPromptParts({
229+
effectivePrompt,
230+
transcriptPrompt: userText,
231+
modelPrompt: [effectivePrompt, effectivePrompt].join("\n\n"),
232+
...withModelPromptBuildContext({
233+
promptBeforeHooks: effectivePrompt,
234+
transcriptPrompt: userText,
235+
prependContext: effectivePrompt,
236+
}),
237+
}),
238+
).toEqual({
239+
prompt: userText,
240+
modelPrompt: [effectivePrompt, userText].join("\n\n"),
241+
runtimeContext: [systemEvent, untrustedContext].join("\n\n"),
242+
});
243+
});
244+
245+
it("strips the active prompt before append hooks that quote the body", () => {
246+
const systemEvent = "System: [2026-06-20 13:59:51] Slack DM from Alice";
247+
const userText = "Hello";
248+
const effectivePrompt = [systemEvent, userText].join("\n\n");
249+
250+
expect(
251+
resolveRuntimeContextPromptParts({
252+
effectivePrompt,
253+
transcriptPrompt: userText,
254+
modelPrompt: [effectivePrompt, effectivePrompt].join("\n\n"),
255+
...withModelPromptBuildContext({
256+
promptBeforeHooks: effectivePrompt,
257+
transcriptPrompt: userText,
258+
appendContext: effectivePrompt,
259+
}),
260+
}),
261+
).toEqual({
262+
prompt: userText,
263+
modelPrompt: [userText, effectivePrompt].join("\n\n"),
264+
runtimeContext: systemEvent,
265+
});
266+
});
267+
268+
it("normalizes quoted hook prompts before locating the active prompt", () => {
269+
const systemEvent = "System: [2026-06-20 13:59:51] Slack DM from Alice";
270+
const userText = "Hello";
271+
const internalContext = [
272+
"<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>",
273+
"private runtime note",
274+
"<<<END_OPENCLAW_INTERNAL_CONTEXT>>>",
275+
].join("\n");
276+
const effectivePrompt = [systemEvent, userText, internalContext].join("\n\n");
277+
278+
expect(
279+
resolveRuntimeContextPromptParts({
280+
effectivePrompt,
281+
transcriptPrompt: userText,
282+
modelPrompt: [effectivePrompt, effectivePrompt].join("\n\n"),
283+
...withModelPromptBuildContext({
284+
promptBeforeHooks: effectivePrompt,
285+
transcriptPrompt: userText,
286+
appendContext: effectivePrompt,
287+
}),
288+
}),
289+
).toEqual({
290+
prompt: userText,
291+
modelPrompt: [userText, systemEvent, userText].join("\n\n"),
292+
runtimeContext: [systemEvent, internalContext].join("\n\n"),
293+
});
294+
});
295+
296+
it("preserves user prompt edge whitespace while removing hidden context", () => {
297+
const systemEvent = "System: [2026-06-20 13:59:51] Slack DM from Alice";
298+
const userText = " leading text ";
299+
const appendContext = "Hook tail";
300+
const effectivePrompt = [systemEvent, userText].join("\n\n");
301+
302+
expect(
303+
resolveRuntimeContextPromptParts({
304+
effectivePrompt,
305+
transcriptPrompt: userText,
306+
modelPrompt: [effectivePrompt, appendContext].join("\n\n"),
307+
...withModelPromptBuildContext({
308+
promptBeforeHooks: effectivePrompt,
309+
transcriptPrompt: userText,
310+
appendContext,
311+
}),
312+
}),
313+
).toEqual({
314+
prompt: userText,
315+
modelPrompt: `${userText}\n\n${appendContext}`,
316+
runtimeContext: systemEvent,
317+
});
318+
});
319+
320+
it("strips hidden context after prompt transforms decorate the active hook body", () => {
321+
const systemEvent = "System: [2026-06-20 13:59:51] Slack DM from Alice";
322+
const userText = "Hello";
323+
const prependContext = "Hook injected context";
324+
const queuedContext = "[Queued messages while agent was busy]";
325+
const promptBeforeHooks = [systemEvent, userText].join("\n\n");
326+
const promptBeforeAnnotation = [queuedContext, promptBeforeHooks].join("\n\n");
327+
const transcriptPrompt = [queuedContext, userText].join("\n\n");
328+
const modelPrompt = [queuedContext, prependContext, promptBeforeHooks].join("\n\n");
329+
330+
expect(
331+
resolveRuntimeContextPromptParts({
332+
effectivePrompt: promptBeforeAnnotation,
333+
transcriptPrompt,
334+
modelPrompt,
335+
...withModelPromptBuildContext({
336+
promptBeforeHooks,
337+
transcriptPrompt: userText,
338+
promptBeforeAnnotation,
339+
prependContext,
340+
}),
341+
}),
342+
).toEqual({
343+
prompt: transcriptPrompt,
344+
modelPrompt: [queuedContext, prependContext, userText].join("\n\n"),
345+
runtimeContext: systemEvent,
346+
});
347+
});
348+
349+
it("keeps outer provenance context ahead of source runtime context", () => {
350+
const provenance = "[Inter-session message] sourceTool=sessions_send isUser=false";
351+
const systemEvent = "System: [2026-06-20 13:59:51] Slack DM from Alice";
352+
const userText = "Hello";
353+
const promptBeforeHooks = [systemEvent, userText].join("\n\n");
354+
const prependContext = "Hook injected context";
355+
356+
expect(
357+
resolveRuntimeContextPromptParts({
358+
effectivePrompt: [provenance, promptBeforeHooks].join("\n"),
359+
transcriptPrompt: userText,
360+
modelPrompt: [prependContext, promptBeforeHooks].join("\n\n"),
361+
...withModelPromptBuildContext({
362+
promptBeforeHooks,
363+
transcriptPrompt: userText,
364+
prependContext,
365+
}),
366+
}),
367+
).toEqual({
368+
prompt: userText,
369+
modelPrompt: [prependContext, userText].join("\n\n"),
370+
runtimeContext: [provenance, systemEvent].join("\n\n"),
371+
});
372+
});
373+
102374
it("does not extract no-transcript delimiter text", () => {
103375
const effectivePrompt = [
104376
"visible ask",
@@ -180,6 +452,25 @@ describe("runtime context prompt submission", () => {
180452
);
181453
});
182454

455+
it("preserves repeated hook text when there is no hidden runtime context", () => {
456+
const modelPrompt = "Hello\n\nHook summary: Hello";
457+
expect(
458+
resolveRuntimeContextPromptParts({
459+
effectivePrompt: "Hello",
460+
transcriptPrompt: "Hello",
461+
modelPrompt,
462+
...withModelPromptBuildContext({
463+
promptBeforeHooks: "Hello",
464+
transcriptPrompt: "Hello",
465+
appendContext: "Hook summary: Hello",
466+
}),
467+
}),
468+
).toEqual({
469+
prompt: "Hello",
470+
modelPrompt,
471+
});
472+
});
473+
183474
it("fails closed for unterminated hidden runtime context blocks", () => {
184475
// Unterminated internal context is ambiguous; keep only the known transcript
185476
// prompt rather than leaking partial hidden content.

0 commit comments

Comments
 (0)