Reply: defer direct secret resolution

This commit is contained in:
Mariano Belinky
2026-04-08 17:43:41 +02:00
parent ae073f9820
commit b02a5e5f0e
2 changed files with 120 additions and 37 deletions

View File

@@ -22,6 +22,7 @@ const createReplyToModeFilterForChannelMock = vi.fn();
const createReplyMediaPathNormalizerMock = vi.fn();
const runPreflightCompactionIfNeededMock = vi.fn();
const runMemoryFlushIfNeededMock = vi.fn();
const enqueueFollowupRunMock = vi.fn();
vi.mock("./agent-runner-utils.js", () => ({
resolveQueuedReplyExecutionConfig: (...args: unknown[]) =>
@@ -45,6 +46,14 @@ vi.mock("./agent-runner-memory.js", () => ({
runMemoryFlushIfNeeded: (...args: unknown[]) => runMemoryFlushIfNeededMock(...args),
}));
vi.mock("./queue.js", async () => {
const actual = await vi.importActual<typeof import("./queue.js")>("./queue.js");
return {
...actual,
enqueueFollowupRun: (...args: unknown[]) => enqueueFollowupRunMock(...args),
};
});
const { runReplyAgent } = await import("./agent-runner.js");
describe("runReplyAgent runtime config", () => {
@@ -55,6 +64,7 @@ describe("runReplyAgent runtime config", () => {
createReplyMediaPathNormalizerMock.mockReset();
runPreflightCompactionIfNeededMock.mockReset();
runMemoryFlushIfNeededMock.mockReset();
enqueueFollowupRunMock.mockReset();
resolveQueuedReplyExecutionConfigMock.mockResolvedValue(freshCfg);
resolveReplyToModeMock.mockReturnValue("default");
@@ -140,4 +150,76 @@ describe("runReplyAgent runtime config", () => {
}),
);
});
it("does not resolve secrets before the enqueue-followup queue path", async () => {
const followupRun = {
prompt: "hello",
summaryLine: "hello",
enqueuedAt: Date.now(),
run: {
sessionId: "session-1",
sessionKey: "agent:main:telegram:default:direct:test",
messageProvider: "telegram",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
config: staleCfg,
skillsSnapshot: {},
provider: "openai",
model: "gpt-5.4",
thinkLevel: "low",
verboseLevel: "off",
elevatedLevel: "off",
bashElevated: {
enabled: false,
allowed: false,
defaultLevel: "off",
},
timeoutMs: 1_000,
blockReplyBreak: "message_end",
},
} as unknown as FollowupRun;
const resolvedQueue = { mode: "interrupt" } as QueueSettings;
const typing = createMockTypingController();
const sessionCtx = {
Provider: "telegram",
OriginatingChannel: "telegram",
OriginatingTo: "12345",
AccountId: "default",
ChatType: "dm",
MessageSid: "msg-1",
} as unknown as TemplateContext;
await expect(
runReplyAgent({
commandBody: "hello",
followupRun,
queueKey: "main",
resolvedQueue,
shouldSteer: false,
shouldFollowup: true,
isActive: true,
isStreaming: false,
typing,
sessionCtx,
defaultModel: "openai/gpt-5.4",
resolvedVerboseLevel: "off",
isNewSession: false,
blockStreamingEnabled: false,
resolvedBlockStreamingBreak: "message_end",
shouldInjectGroupIntro: false,
typingMode: "instant",
}),
).resolves.toBeUndefined();
expect(resolveQueuedReplyExecutionConfigMock).not.toHaveBeenCalled();
expect(enqueueFollowupRunMock).toHaveBeenCalledWith(
"main",
followupRun,
resolvedQueue,
"message-id",
expect.any(Function),
false,
);
});
});

View File

@@ -163,43 +163,6 @@ export async function runReplyAgent(params: {
const pendingToolTasks = new Set<Promise<void>>();
const blockReplyTimeoutMs = opts?.blockReplyTimeoutMs ?? BLOCK_REPLY_SEND_TIMEOUT_MS;
followupRun.run.config = await resolveQueuedReplyExecutionConfig(followupRun.run.config);
const replyToChannel = resolveOriginMessageProvider({
originatingChannel: sessionCtx.OriginatingChannel,
provider: sessionCtx.Surface ?? sessionCtx.Provider,
}) as OriginatingChannelType | undefined;
const replyToMode = resolveReplyToMode(
followupRun.run.config,
replyToChannel,
sessionCtx.AccountId,
sessionCtx.ChatType,
);
const applyReplyToMode = createReplyToModeFilterForChannel(replyToMode, replyToChannel);
const cfg = followupRun.run.config;
const normalizeReplyMediaPaths = createReplyMediaPathNormalizer({
cfg,
sessionKey,
workspaceDir: followupRun.run.workspaceDir,
});
const blockReplyCoalescing =
blockStreamingEnabled && opts?.onBlockReply
? resolveEffectiveBlockStreamingConfig({
cfg,
provider: sessionCtx.Provider,
accountId: sessionCtx.AccountId,
chunking: blockReplyChunking,
}).coalescing
: undefined;
const blockReplyPipeline =
blockStreamingEnabled && opts?.onBlockReply
? createBlockReplyPipeline({
onBlockReply: opts.onBlockReply,
timeoutMs: blockReplyTimeoutMs,
coalescing: blockReplyCoalescing,
buffer: createAudioAsVoiceBuffer({ isAudioPayload }),
})
: null;
const touchActiveSessionEntry = async () => {
if (!activeSessionEntry || !activeSessionStore || !sessionKey) {
return;
@@ -271,6 +234,44 @@ export async function runReplyAgent(params: {
return undefined;
}
followupRun.run.config = await resolveQueuedReplyExecutionConfig(followupRun.run.config);
const replyToChannel = resolveOriginMessageProvider({
originatingChannel: sessionCtx.OriginatingChannel,
provider: sessionCtx.Surface ?? sessionCtx.Provider,
}) as OriginatingChannelType | undefined;
const replyToMode = resolveReplyToMode(
followupRun.run.config,
replyToChannel,
sessionCtx.AccountId,
sessionCtx.ChatType,
);
const applyReplyToMode = createReplyToModeFilterForChannel(replyToMode, replyToChannel);
const cfg = followupRun.run.config;
const normalizeReplyMediaPaths = createReplyMediaPathNormalizer({
cfg,
sessionKey,
workspaceDir: followupRun.run.workspaceDir,
});
const blockReplyCoalescing =
blockStreamingEnabled && opts?.onBlockReply
? resolveEffectiveBlockStreamingConfig({
cfg,
provider: sessionCtx.Provider,
accountId: sessionCtx.AccountId,
chunking: blockReplyChunking,
}).coalescing
: undefined;
const blockReplyPipeline =
blockStreamingEnabled && opts?.onBlockReply
? createBlockReplyPipeline({
onBlockReply: opts.onBlockReply,
timeoutMs: blockReplyTimeoutMs,
coalescing: blockReplyCoalescing,
buffer: createAudioAsVoiceBuffer({ isAudioPayload }),
})
: null;
const replySessionKey = sessionKey ?? followupRun.run.sessionKey;
let replyOperation: ReplyOperation;
try {