mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
fix(messaging): recover from empty router prompt replies (#645)
This commit is contained in:
@@ -1701,38 +1701,70 @@ export async function startBridge(config: Config, logger: Logger, reporter?: Bri
|
||||
},
|
||||
"prompt start",
|
||||
);
|
||||
const response = await getClient(boundDirectory).session.prompt({
|
||||
sessionID,
|
||||
parts: [{ type: "text", text: promptText }],
|
||||
...(effectiveModel ? { model: effectiveModel } : {}),
|
||||
...(messagingAgent.selectedAgent ? { agent: messagingAgent.selectedAgent } : {}),
|
||||
});
|
||||
const parts = (response as { parts?: Array<{ type?: string; text?: string; ignored?: boolean }> }).parts ?? [];
|
||||
const textParts = parts.filter((part) => part.type === "text" && !part.ignored);
|
||||
logger.debug(
|
||||
{
|
||||
|
||||
type PromptPart = { type?: string; text?: string; ignored?: boolean };
|
||||
|
||||
const extractReply = (parts: PromptPart[]) =>
|
||||
parts
|
||||
.filter((part) => part.type === "text" && !part.ignored)
|
||||
.map((part) => part.text ?? "")
|
||||
.join("\n")
|
||||
.trim();
|
||||
|
||||
const logPromptResponse = (attempt: "initial" | "retry", parts: PromptPart[]) => {
|
||||
const textParts = parts.filter((part) => part.type === "text" && !part.ignored);
|
||||
logger.debug(
|
||||
{
|
||||
sessionID,
|
||||
attempt,
|
||||
partCount: parts.length,
|
||||
textCount: textParts.length,
|
||||
partTypes: parts.map((p) => p.type),
|
||||
ignoredCount: parts.filter((p) => p.ignored).length,
|
||||
},
|
||||
"prompt response",
|
||||
);
|
||||
};
|
||||
|
||||
const runPrompt = async (): Promise<PromptPart[]> => {
|
||||
const response = await getClient(boundDirectory).session.prompt({
|
||||
sessionID,
|
||||
partCount: parts.length,
|
||||
textCount: textParts.length,
|
||||
partTypes: parts.map((p) => p.type),
|
||||
ignoredCount: parts.filter((p) => p.ignored).length,
|
||||
},
|
||||
"prompt response",
|
||||
);
|
||||
const reply = parts
|
||||
.filter((part) => part.type === "text" && !part.ignored)
|
||||
.map((part) => part.text ?? "")
|
||||
.join("\n")
|
||||
.trim();
|
||||
parts: [{ type: "text", text: promptText }],
|
||||
...(effectiveModel ? { model: effectiveModel } : {}),
|
||||
...(messagingAgent.selectedAgent ? { agent: messagingAgent.selectedAgent } : {}),
|
||||
});
|
||||
return (response as { parts?: PromptPart[] }).parts ?? [];
|
||||
};
|
||||
|
||||
let parts = await runPrompt();
|
||||
logPromptResponse("initial", parts);
|
||||
let reply = extractReply(parts);
|
||||
|
||||
if (!reply && !parts.some((part) => part.type === "tool")) {
|
||||
logger.warn({ sessionID }, "prompt returned no visible text; retrying once");
|
||||
parts = await runPrompt();
|
||||
logPromptResponse("retry", parts);
|
||||
reply = extractReply(parts);
|
||||
}
|
||||
|
||||
if (reply) {
|
||||
logger.debug({ sessionID, replyLength: reply.length }, "reply built");
|
||||
await sendText(inbound.channel, inbound.identityId, inbound.peerId, reply, { kind: "reply" });
|
||||
} else {
|
||||
logger.debug({ sessionID }, "reply empty");
|
||||
await sendText(inbound.channel, inbound.identityId, inbound.peerId, "No response generated. Try again.", {
|
||||
kind: "system",
|
||||
});
|
||||
logger.warn(
|
||||
{ sessionID, partTypes: parts.map((part) => part.type), ignoredCount: parts.filter((part) => part.ignored).length },
|
||||
"prompt returned no visible text; clearing session",
|
||||
);
|
||||
store.deleteSession(inbound.channel, inbound.identityId, peerKey);
|
||||
await sendText(
|
||||
inbound.channel,
|
||||
inbound.identityId,
|
||||
inbound.peerId,
|
||||
"No visible response was generated. I reset this chat session in case stale state was blocking replies. Send your message again.",
|
||||
{
|
||||
kind: "system",
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// Log full error details for debugging
|
||||
|
||||
@@ -84,3 +84,81 @@ test("bridge end-to-end: inbound -> prompt -> outbound", async () => {
|
||||
|
||||
await bridge.stop();
|
||||
});
|
||||
|
||||
test("bridge recovers from empty prompt replies by clearing stale session", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "opencodeRouter-e2e-empty-"));
|
||||
const dbPath = path.join(dir, "opencode-router.db");
|
||||
|
||||
const sent = [];
|
||||
let sessionCounter = 0;
|
||||
const slackAdapter = {
|
||||
key: "slack:default",
|
||||
name: "slack",
|
||||
identityId: "default",
|
||||
maxTextLength: 39_000,
|
||||
async start() {},
|
||||
async stop() {},
|
||||
async sendText(peerId, text) {
|
||||
sent.push({ peerId, text });
|
||||
},
|
||||
};
|
||||
|
||||
const fakeClient = {
|
||||
global: {
|
||||
health: async () => ({ healthy: true, version: "test" }),
|
||||
},
|
||||
session: {
|
||||
create: async () => {
|
||||
sessionCounter += 1;
|
||||
return { id: `session-${sessionCounter}` };
|
||||
},
|
||||
prompt: async ({ sessionID }) => {
|
||||
if (sessionID === "session-1") {
|
||||
return { parts: [] };
|
||||
}
|
||||
return { parts: [{ type: "text", text: "fresh reply" }] };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bridge = await startBridge(
|
||||
{
|
||||
configPath: path.join(dir, "opencode-router.json"),
|
||||
configFile: { version: 1 },
|
||||
opencodeUrl: "http://127.0.0.1:4096",
|
||||
opencodeDirectory: dir,
|
||||
telegramBots: [],
|
||||
slackApps: [],
|
||||
dataDir: dir,
|
||||
dbPath,
|
||||
logFile: path.join(dir, "opencode-router.log"),
|
||||
toolUpdatesEnabled: false,
|
||||
groupsEnabled: false,
|
||||
permissionMode: "allow",
|
||||
toolOutputLimit: 1200,
|
||||
healthPort: undefined,
|
||||
logLevel: "silent",
|
||||
},
|
||||
createLoggerStub(),
|
||||
undefined,
|
||||
{
|
||||
client: fakeClient,
|
||||
adapters: new Map([["slack:default", slackAdapter]]),
|
||||
disableEventStream: true,
|
||||
disableHealthServer: true,
|
||||
},
|
||||
);
|
||||
|
||||
await bridge.dispatchInbound({ channel: "slack", identityId: "default", peerId: "D123", text: "first", raw: {} });
|
||||
await bridge.dispatchInbound({ channel: "slack", identityId: "default", peerId: "D123", text: "second", raw: {} });
|
||||
|
||||
assert.equal(sessionCounter, 2);
|
||||
assert.ok(
|
||||
sent.some((message) =>
|
||||
message.text.includes("No visible response was generated. I reset this chat session in case stale state was blocking replies."),
|
||||
),
|
||||
);
|
||||
assert.ok(sent.some((message) => message.text === "fresh reply"));
|
||||
|
||||
await bridge.stop();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user