Skip to content

Commit 20d7bf7

Browse files
committed
refactor: remove duplicate user turn handoff
1 parent fe44ecd commit 20d7bf7

15 files changed

Lines changed: 114 additions & 95 deletions

src/agents/cli-runner.reliability.test.ts

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,23 @@ function createSessionFile(params?: { history?: Array<{ role: "user"; content: s
103103
return { dir, sessionFile, storePath };
104104
}
105105

106+
function createCliUserTurnRecorder(params: {
107+
text: string;
108+
sessionFile: string;
109+
sessionKey?: string;
110+
workspaceDir: string;
111+
}) {
112+
return createUserTurnTranscriptRecorder({
113+
input: { text: params.text },
114+
target: {
115+
transcriptPath: params.sessionFile,
116+
sessionId: "s1",
117+
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
118+
cwd: params.workspaceDir,
119+
},
120+
});
121+
}
122+
106123
function buildPreparedContext(params?: {
107124
sessionKey?: string;
108125
cliSessionId?: string;
@@ -838,7 +855,12 @@ describe("runCliAgent reliability", () => {
838855
sessionFile,
839856
workspaceDir: dir,
840857
prompt: "runtime prompt",
841-
userTurnTranscript: { text: "display prompt" },
858+
userTurnTranscriptRecorder: createCliUserTurnRecorder({
859+
text: "display prompt",
860+
sessionFile,
861+
sessionKey: "agent:main:main",
862+
workspaceDir: dir,
863+
}),
842864
onUserMessagePersisted,
843865
},
844866
});
@@ -914,7 +936,6 @@ describe("runCliAgent reliability", () => {
914936
sessionFile,
915937
workspaceDir: dir,
916938
prompt: "runtime prompt",
917-
userTurnTranscript: { text: "legacy display prompt" },
918939
userTurnTranscriptRecorder: recorder,
919940
},
920941
});
@@ -974,7 +995,12 @@ describe("runCliAgent reliability", () => {
974995
sessionFile,
975996
workspaceDir: dir,
976997
prompt: "runtime prompt",
977-
userTurnTranscript: { text: "display prompt" },
998+
userTurnTranscriptRecorder: createCliUserTurnRecorder({
999+
text: "display prompt",
1000+
sessionFile,
1001+
sessionKey: "agent:main:main",
1002+
workspaceDir: dir,
1003+
}),
9781004
onUserMessagePersisted: () => {
9791005
throw new Error("notification failed");
9801006
},
@@ -1010,7 +1036,12 @@ describe("runCliAgent reliability", () => {
10101036
sessionFile: path.join(blockedParent, "s1.jsonl"),
10111037
workspaceDir: dir,
10121038
prompt: "runtime prompt",
1013-
userTurnTranscript: { text: "display prompt" },
1039+
userTurnTranscriptRecorder: createCliUserTurnRecorder({
1040+
text: "display prompt",
1041+
sessionFile: path.join(blockedParent, "s1.jsonl"),
1042+
sessionKey: "agent:main:main",
1043+
workspaceDir: dir,
1044+
}),
10141045
onUserMessagePersisted,
10151046
},
10161047
}),
@@ -1064,7 +1095,12 @@ describe("runCliAgent reliability", () => {
10641095
sessionFile,
10651096
workspaceDir: dir,
10661097
prompt: "secret prompt",
1067-
userTurnTranscript: { text: "secret prompt" },
1098+
userTurnTranscriptRecorder: createCliUserTurnRecorder({
1099+
text: "secret prompt",
1100+
sessionFile,
1101+
sessionKey: "agent:main:main",
1102+
workspaceDir: dir,
1103+
}),
10681104
onUserMessagePersisted,
10691105
},
10701106
}).then((result) => {

src/agents/cli-runner.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { createSubsystemLogger } from "../logging/subsystem.js";
77
import { buildAgentHookContextChannelFields } from "../plugins/hook-agent-context.js";
88
import { resolveBlockMessage } from "../plugins/hook-decision-types.js";
99
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
10-
import { createUserTurnTranscriptRecorder } from "../sessions/user-turn-transcript.js";
1110
import {
1211
loadCliSessionContextEngineMessages,
1312
loadCliSessionHistoryMessages,
@@ -128,7 +127,7 @@ async function runCliAgentEndHook(
128127
}
129128

130129
async function persistApprovedCliUserTurnTranscript(params: RunCliAgentParams): Promise<void> {
131-
if (params.suppressNextUserMessagePersistence === true || !params.userTurnTranscript) {
130+
if (params.suppressNextUserMessagePersistence === true || !params.userTurnTranscriptRecorder) {
132131
return;
133132
}
134133

@@ -140,20 +139,7 @@ async function persistApprovedCliUserTurnTranscript(params: RunCliAgentParams):
140139
cwd: params.workspaceDir,
141140
...(params.config ? { config: params.config } : {}),
142141
};
143-
const recorder =
144-
params.userTurnTranscriptRecorder ??
145-
createUserTurnTranscriptRecorder({
146-
...(params.userTurnTranscript.message
147-
? { message: params.userTurnTranscript.message }
148-
: {
149-
input: {
150-
text: params.userTurnTranscript.text,
151-
timestamp: Date.now(),
152-
},
153-
}),
154-
target,
155-
});
156-
const persisted = await recorder.persistApproved({ target });
142+
const persisted = await params.userTurnTranscriptRecorder.persistApproved({ target });
157143
if (persisted) {
158144
try {
159145
const notification = params.onUserMessagePersisted?.(persisted.message);

src/agents/cli-runner/types.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,6 @@ import type {
2525
import type { SkillSnapshot } from "../skills.js";
2626
import type { SilentReplyPromptMode } from "../system-prompt.types.js";
2727

28-
export type CliUserTurnTranscriptInput =
29-
| {
30-
message: PersistedUserTurnMessage;
31-
text?: never;
32-
}
33-
| {
34-
message?: never;
35-
text: string;
36-
};
37-
3828
export type RunCliAgentParams = {
3929
sessionId: string;
4030
sessionKey?: string;
@@ -46,11 +36,6 @@ export type RunCliAgentParams = {
4636
config?: OpenClawConfig;
4737
prompt: string;
4838
transcriptPrompt?: string;
49-
/**
50-
* Canonical user turn to persist after before_agent_run allows the prompt.
51-
* This is transcript projection only; model input still comes from prompt/images/context.
52-
*/
53-
userTurnTranscript?: CliUserTurnTranscriptInput;
5439
suppressNextUserMessagePersistence?: boolean;
5540
userTurnTranscriptRecorder?: UserTurnTranscriptRecorder;
5641
onUserMessagePersisted?: (message: PersistedUserTurnMessage) => void | Promise<void>;

src/auto-reply/dispatch.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { isOutboundDeliveryError } from "../infra/outbound/deliver-types.js";
1313
import { logMessageReceived } from "../logging/diagnostic.js";
1414
import { hasOutboundReplyContent } from "../plugin-sdk/reply-payload.js";
1515
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
16-
import type { UserTurnInput } from "../sessions/user-turn-transcript.js";
1716
import type { SilentReplyConversationType } from "../shared/silent-reply-policy.js";
1817
import {
1918
resolveCommandTurnContext,
@@ -382,7 +381,6 @@ export async function dispatchInboundMessage(params: {
382381
cfg: OpenClawConfig;
383382
dispatcher: ReplyDispatcher;
384383
replyOptions?: Omit<GetReplyOptions, "onBlockReply">;
385-
userTurnInput?: UserTurnInput;
386384
replyResolver?: GetReplyFromConfig;
387385
}): Promise<DispatchInboundResult> {
388386
const finalized = measureDiagnosticsTimelineSpanSync(
@@ -414,7 +412,6 @@ export async function dispatchInboundMessage(params: {
414412
cfg: params.cfg,
415413
dispatcher: params.dispatcher,
416414
replyOptions: params.replyOptions,
417-
userTurnInput: params.userTurnInput,
418415
replyResolver: params.replyResolver,
419416
}),
420417
{
@@ -432,7 +429,6 @@ export async function dispatchInboundMessageWithBufferedDispatcher(params: {
432429
cfg: OpenClawConfig;
433430
dispatcherOptions: ReplyDispatcherWithTypingOptions;
434431
replyOptions?: Omit<GetReplyOptions, "onBlockReply">;
435-
userTurnInput?: UserTurnInput;
436432
replyResolver?: GetReplyFromConfig;
437433
}): Promise<DispatchInboundResult> {
438434
const finalized = finalizeInboundContext(params.ctx);
@@ -489,7 +485,6 @@ export async function dispatchInboundMessageWithBufferedDispatcher(params: {
489485
...params.replyOptions,
490486
...replyOptions,
491487
},
492-
userTurnInput: params.userTurnInput,
493488
});
494489
} finally {
495490
try {
@@ -512,7 +507,6 @@ export async function dispatchInboundMessageWithDispatcher(params: {
512507
cfg: OpenClawConfig;
513508
dispatcherOptions: ReplyDispatcherOptions;
514509
replyOptions?: Omit<GetReplyOptions, "onBlockReply">;
515-
userTurnInput?: UserTurnInput;
516510
replyResolver?: GetReplyFromConfig;
517511
}): Promise<DispatchInboundResult> {
518512
const silentReplyContext = resolveDispatcherSilentReplyContext(params.ctx, params.cfg);
@@ -528,6 +522,5 @@ export async function dispatchInboundMessageWithDispatcher(params: {
528522
dispatcher,
529523
replyResolver: params.replyResolver,
530524
replyOptions: params.replyOptions,
531-
userTurnInput: params.userTurnInput,
532525
});
533526
}

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1826,13 +1826,15 @@ describe("runAgentTurnWithFallback", () => {
18261826
sessionKey: "main",
18271827
agentId: "agent",
18281828
sessionId: "session",
1829-
userTurnTranscript: { message: preparedUserTurnMessage },
18301829
suppressNextUserMessagePersistence: false,
18311830
});
18321831
const call = requireMockCall(state.runCliAgentMock, 0, "CLI runtime");
1833-
expect(requireRecord(call[0], "CLI runtime").onUserMessagePersisted).toEqual(
1834-
expect.any(Function),
1832+
const callParams = requireRecord(call[0], "CLI runtime");
1833+
expect(callParams.userTurnTranscriptRecorder).toEqual(expect.any(Object));
1834+
expect(requireRecord(callParams.userTurnTranscriptRecorder, "user turn recorder").message).toBe(
1835+
preparedUserTurnMessage,
18351836
);
1837+
expect(callParams.onUserMessagePersisted).toEqual(expect.any(Function));
18361838
});
18371839

18381840
it("passes clean transcript text for text-only CLI user persistence", async () => {
@@ -1852,6 +1854,10 @@ describe("runAgentTurnWithFallback", () => {
18521854
const followupRun = createFollowupRun();
18531855
followupRun.run.provider = "codex-cli";
18541856
followupRun.run.model = "gpt-5.4";
1857+
followupRun.userTurnTranscriptRecorder = createTestUserTurnRecorder({
1858+
role: "user",
1859+
content: "display prompt",
1860+
} as never);
18551861

18561862
await runAgentTurnWithFallback({
18571863
...createMinimalRunAgentTurnParams({ followupRun }),
@@ -1866,9 +1872,17 @@ describe("runAgentTurnWithFallback", () => {
18661872
agentId: "agent",
18671873
prompt: "runtime prompt with metadata",
18681874
transcriptPrompt: "display prompt",
1869-
userTurnTranscript: { text: "display prompt" },
18701875
suppressNextUserMessagePersistence: false,
18711876
});
1877+
const call = requireMockCall(state.runCliAgentMock, 0, "CLI runtime");
1878+
const callParams = requireRecord(call[0], "CLI runtime");
1879+
expect(callParams.userTurnTranscriptRecorder).toEqual(expect.any(Object));
1880+
expect(
1881+
requireRecord(callParams.userTurnTranscriptRecorder, "user turn recorder").message,
1882+
).toMatchObject({
1883+
role: "user",
1884+
content: "display prompt",
1885+
});
18721886
});
18731887

18741888
it("does not reuse or persist CLI sessions for room-event turns", async () => {

src/auto-reply/reply/dispatch-from-config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2512,7 +2512,6 @@ export async function dispatchReplyFromConfig(
25122512
},
25132513
},
25142514
replyConfig,
2515-
params.userTurnInput,
25162515
),
25172516
),
25182517
);

src/auto-reply/reply/dispatch-from-config.types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { OpenClawConfig } from "../../config/types.openclaw.js";
2-
import type { UserTurnInput } from "../../sessions/user-turn-transcript.js";
32
import type { GetReplyOptions, SourceReplyDeliveryMode } from "../get-reply-options.types.js";
43
import type { FinalizedMsgContext } from "../templating.js";
54
import type { FormatAbortReplyText, TryFastAbortFromMessage } from "./abort.runtime-types.js";
@@ -19,7 +18,6 @@ export type DispatchFromConfigParams = {
1918
cfg: OpenClawConfig;
2019
dispatcher: ReplyDispatcher;
2120
replyOptions?: Omit<GetReplyOptions, "onBlockReply">;
22-
userTurnInput?: UserTurnInput;
2321
replyResolver?: GetReplyFromConfig;
2422
fastAbortResolver?: TryFastAbortFromMessage;
2523
formatAbortReplyTextResolver?: FormatAbortReplyText;

src/auto-reply/reply/followup-runner.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,10 @@ describe("createFollowupRunner runtime config", () => {
843843
await runner(
844844
createQueuedRun({
845845
originatingChannel: "telegram",
846+
userTurnTranscriptRecorder: createTestUserTurnRecorder({
847+
role: "user",
848+
content: "hello",
849+
} as never),
846850
run: {
847851
config: runtimeConfig,
848852
sessionId: "session-cli-followup",
@@ -867,9 +871,12 @@ describe("createFollowupRunner runtime config", () => {
867871
agentId: "agent",
868872
workspaceDir: "/tmp",
869873
config: runtimeConfig,
870-
userTurnTranscript: { text: "hello" },
871874
suppressNextUserMessagePersistence: false,
872875
});
876+
expect(call.userTurnTranscriptRecorder?.message).toMatchObject({
877+
role: "user",
878+
content: "hello",
879+
});
873880
expect(call.onUserMessagePersisted).toEqual(expect.any(Function));
874881
});
875882

@@ -917,7 +924,7 @@ describe("createFollowupRunner runtime config", () => {
917924

918925
expect(runCliAgentMock).toHaveBeenCalledOnce();
919926
const mediaCall = requireLastMockCallArg(runCliAgentMock, "run cli agent");
920-
expect(mediaCall.userTurnTranscript).toEqual({ message: preparedUserTurnMessage });
927+
expect(mediaCall.userTurnTranscriptRecorder?.message).toBe(preparedUserTurnMessage);
921928
});
922929

923930
it("defers queued CLI attempt terminal lifecycle events until fallback settles", async () => {

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -728,9 +728,6 @@ export function createFollowupRunner(params: {
728728
config: runtimeConfig,
729729
prompt: queued.prompt,
730730
transcriptPrompt: queued.transcriptPrompt,
731-
userTurnTranscript: userTurnTranscriptRecorder?.message
732-
? { message: userTurnTranscriptRecorder.message }
733-
: { text: effectiveQueued.transcriptPrompt ?? effectiveQueued.prompt },
734731
suppressNextUserMessagePersistence: suppressQueuedUserPersistenceForCandidate,
735732
userTurnTranscriptRecorder,
736733
onUserMessagePersisted: notifyUserMessagePersisted,

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -997,7 +997,12 @@ describe("runPreparedReply media-only handling", () => {
997997
);
998998

999999
const call = requireRunReplyAgentCall();
1000-
expect(call.followupRun.userTurnTranscriptRecorder?.message).toBeUndefined();
1000+
expect(call.followupRun.userTurnTranscriptRecorder?.message).toMatchObject({
1001+
role: "user",
1002+
content: "follow up without media",
1003+
});
1004+
expect(call.followupRun.userTurnTranscriptRecorder?.message).not.toHaveProperty("MediaPath");
1005+
expect(call.followupRun.userTurnTranscriptRecorder?.message).not.toHaveProperty("MediaPaths");
10011006
});
10021007

10031008
it("does not rehydrate current MediaPaths after image understanding enriched the prompt", async () => {

0 commit comments

Comments
 (0)