fix(chat): guard abort and early idle races

This commit is contained in:
2026-06-07 20:22:05 +08:00
parent 2295bdcb97
commit 6f3b72628f
2 changed files with 30 additions and 2 deletions
+7 -1
View File
@@ -49,6 +49,8 @@ const envSchema = z
OPENCODE_SKILLS_ROOT_DIR: z.string().default("./.opencode/skills"),
// client 模式下,目标 opencode server 的基础地址。
OPENCODE_CLIENT_BASE_URL: z.string().url().optional(),
// 旧版 client 模式环境变量名,保留兼容,解析时会映射到 OPENCODE_CLIENT_BASE_URL。
OPENCODE_BASE_URL: z.string().url().optional(),
// tjwater-cli 可执行文件路径。
TJWATER_CLI_PATH: z.string().default("./cli/tjwater-cli"),
// TJWater 后端 API 的基础地址。
@@ -122,7 +124,11 @@ const normalizedEnv = {
...process.env,
OPENCODE_MODE:
process.env.OPENCODE_MODE ??
(process.env.OPENCODE_CLIENT_BASE_URL ? "client" : "embedded"),
(process.env.OPENCODE_CLIENT_BASE_URL || process.env.OPENCODE_BASE_URL
? "client"
: "embedded"),
OPENCODE_CLIENT_BASE_URL:
process.env.OPENCODE_CLIENT_BASE_URL ?? process.env.OPENCODE_BASE_URL,
};
export const config: AppConfig = envSchema.parse(normalizedEnv);
+23 -1
View File
@@ -323,6 +323,7 @@ export const streamPromptResponse = async ({
let firstToolEventLogged = false;
let lastSessionStatus: string | null = null;
let lastSessionStatusMessage: string | null = null;
let sawResponseActivity = false;
let emittedText = false;
let toolCallCount = 0;
let done = false;
@@ -529,6 +530,7 @@ export const streamPromptResponse = async ({
}
if (isSkillEvent(event)) {
sawResponseActivity = true;
const { name, reason, payload } = extractSkillAuditInfo(event);
logDevelopmentDebug("skill event received", {
...debugContext,
@@ -552,7 +554,15 @@ export const streamPromptResponse = async ({
});
}
if (event.type === "message.updated") {
if (event.properties.info.role === "assistant") {
sawResponseActivity = true;
}
continue;
}
if (event.type === "message.part.delta" && event.properties.field === "text") {
sawResponseActivity = true;
const partType = partTypes.get(event.properties.partID);
if (partType === "text") {
if (!firstTokenLogged) {
@@ -591,6 +601,7 @@ export const streamPromptResponse = async ({
}
if (event.type === "message.part.updated") {
sawResponseActivity = true;
const part = event.properties.part;
partTypes.set(part.id, part.type);
if (part.type === "text") {
@@ -720,6 +731,7 @@ export const streamPromptResponse = async ({
}
if (event.type === "todo.updated") {
sawResponseActivity = true;
const completed = event.properties.todos.filter(
(todo) => todo.status === "completed",
).length;
@@ -736,6 +748,7 @@ export const streamPromptResponse = async ({
}
if (event.type === "session.error") {
sawResponseActivity = true;
logDevelopmentDebug("session error received", {
...debugContext,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
@@ -757,6 +770,13 @@ export const streamPromptResponse = async ({
}
if (event.type === "session.idle") {
if (!sawResponseActivity) {
logDevelopmentDebug("ignoring session idle before response activity", {
...debugContext,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
continue;
}
logDevelopmentDebug("session idle received", {
...debugContext,
emittedText,
@@ -832,8 +852,10 @@ export const streamPromptResponse = async ({
return { aborted: false, failed: false, toolCallCount };
} finally {
await iterator.return?.(undefined);
if (!promptSettled) {
if (!promptSettled && !aborted) {
await promptPromise.catch(() => undefined);
} else if (!promptSettled) {
void promptPromise.catch(() => undefined);
}
logDevelopmentDebug("chat stream cleanup finished", {
...debugContext,