fix(chat): guard abort and early idle races
This commit is contained in:
+7
-1
@@ -49,6 +49,8 @@ const envSchema = z
|
|||||||
OPENCODE_SKILLS_ROOT_DIR: z.string().default("./.opencode/skills"),
|
OPENCODE_SKILLS_ROOT_DIR: z.string().default("./.opencode/skills"),
|
||||||
// client 模式下,目标 opencode server 的基础地址。
|
// client 模式下,目标 opencode server 的基础地址。
|
||||||
OPENCODE_CLIENT_BASE_URL: z.string().url().optional(),
|
OPENCODE_CLIENT_BASE_URL: z.string().url().optional(),
|
||||||
|
// 旧版 client 模式环境变量名,保留兼容,解析时会映射到 OPENCODE_CLIENT_BASE_URL。
|
||||||
|
OPENCODE_BASE_URL: z.string().url().optional(),
|
||||||
// tjwater-cli 可执行文件路径。
|
// tjwater-cli 可执行文件路径。
|
||||||
TJWATER_CLI_PATH: z.string().default("./cli/tjwater-cli"),
|
TJWATER_CLI_PATH: z.string().default("./cli/tjwater-cli"),
|
||||||
// TJWater 后端 API 的基础地址。
|
// TJWater 后端 API 的基础地址。
|
||||||
@@ -122,7 +124,11 @@ const normalizedEnv = {
|
|||||||
...process.env,
|
...process.env,
|
||||||
OPENCODE_MODE:
|
OPENCODE_MODE:
|
||||||
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);
|
export const config: AppConfig = envSchema.parse(normalizedEnv);
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ export const streamPromptResponse = async ({
|
|||||||
let firstToolEventLogged = false;
|
let firstToolEventLogged = false;
|
||||||
let lastSessionStatus: string | null = null;
|
let lastSessionStatus: string | null = null;
|
||||||
let lastSessionStatusMessage: string | null = null;
|
let lastSessionStatusMessage: string | null = null;
|
||||||
|
let sawResponseActivity = false;
|
||||||
let emittedText = false;
|
let emittedText = false;
|
||||||
let toolCallCount = 0;
|
let toolCallCount = 0;
|
||||||
let done = false;
|
let done = false;
|
||||||
@@ -529,6 +530,7 @@ export const streamPromptResponse = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isSkillEvent(event)) {
|
if (isSkillEvent(event)) {
|
||||||
|
sawResponseActivity = true;
|
||||||
const { name, reason, payload } = extractSkillAuditInfo(event);
|
const { name, reason, payload } = extractSkillAuditInfo(event);
|
||||||
logDevelopmentDebug("skill event received", {
|
logDevelopmentDebug("skill event received", {
|
||||||
...debugContext,
|
...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") {
|
if (event.type === "message.part.delta" && event.properties.field === "text") {
|
||||||
|
sawResponseActivity = true;
|
||||||
const partType = partTypes.get(event.properties.partID);
|
const partType = partTypes.get(event.properties.partID);
|
||||||
if (partType === "text") {
|
if (partType === "text") {
|
||||||
if (!firstTokenLogged) {
|
if (!firstTokenLogged) {
|
||||||
@@ -591,6 +601,7 @@ export const streamPromptResponse = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "message.part.updated") {
|
if (event.type === "message.part.updated") {
|
||||||
|
sawResponseActivity = true;
|
||||||
const part = event.properties.part;
|
const part = event.properties.part;
|
||||||
partTypes.set(part.id, part.type);
|
partTypes.set(part.id, part.type);
|
||||||
if (part.type === "text") {
|
if (part.type === "text") {
|
||||||
@@ -720,6 +731,7 @@ export const streamPromptResponse = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "todo.updated") {
|
if (event.type === "todo.updated") {
|
||||||
|
sawResponseActivity = true;
|
||||||
const completed = event.properties.todos.filter(
|
const completed = event.properties.todos.filter(
|
||||||
(todo) => todo.status === "completed",
|
(todo) => todo.status === "completed",
|
||||||
).length;
|
).length;
|
||||||
@@ -736,6 +748,7 @@ export const streamPromptResponse = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "session.error") {
|
if (event.type === "session.error") {
|
||||||
|
sawResponseActivity = true;
|
||||||
logDevelopmentDebug("session error received", {
|
logDevelopmentDebug("session error received", {
|
||||||
...debugContext,
|
...debugContext,
|
||||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||||
@@ -757,6 +770,13 @@ export const streamPromptResponse = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "session.idle") {
|
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", {
|
logDevelopmentDebug("session idle received", {
|
||||||
...debugContext,
|
...debugContext,
|
||||||
emittedText,
|
emittedText,
|
||||||
@@ -832,8 +852,10 @@ export const streamPromptResponse = async ({
|
|||||||
return { aborted: false, failed: false, toolCallCount };
|
return { aborted: false, failed: false, toolCallCount };
|
||||||
} finally {
|
} finally {
|
||||||
await iterator.return?.(undefined);
|
await iterator.return?.(undefined);
|
||||||
if (!promptSettled) {
|
if (!promptSettled && !aborted) {
|
||||||
await promptPromise.catch(() => undefined);
|
await promptPromise.catch(() => undefined);
|
||||||
|
} else if (!promptSettled) {
|
||||||
|
void promptPromise.catch(() => undefined);
|
||||||
}
|
}
|
||||||
logDevelopmentDebug("chat stream cleanup finished", {
|
logDevelopmentDebug("chat stream cleanup finished", {
|
||||||
...debugContext,
|
...debugContext,
|
||||||
|
|||||||
Reference in New Issue
Block a user