fix: show accurate request status in message footers
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s

Requests could appear to keep working while OpenCode waited to retry, and
retry backoff was described as an active attempt. Report native OpenCode
retry timing, distinguish direct-model backoff from dispatch, and show
preparation, tool activity, pending input, and terminal states accurately.

Keep only the message-footer indicator static. Conversation-list activity
indicators, send/stop controls, retry limits, and command replay behavior
remain unchanged. Align the feature documentation with verified behavior.

Validation: 3618 tests passed, 61 skipped; typecheck and lint passed.
Isolated desktop validation used five real text-model calls across three
profiles, with controlled local fault injection for retry transitions.

Release note: 消息底部现在准确区分请求准备、等待重试、开始重试和需要回答的状态,不再用闪烁暗示仍有进展;会话列表闪点及发送、停止行为保持不变。
This commit is contained in:
mesalogo
2026-09-08 22:47:06 +08:00
parent 9b7cf730d0
commit c82d0161a9
18 changed files with 289 additions and 37 deletions
+4
View File
@@ -133,6 +133,10 @@ describe('ContinueAgentRuntime', () => {
const events = await collectEvents(runtime)
expect(events[0]).toMatchObject({
type: 'status',
message: 'Continue 正在处理请求'
})
expect(mocks.detectRuntimeBinary).toHaveBeenCalledWith({
binaryPath: '',
bundledPath: undefined,
+1 -1
View File
@@ -504,7 +504,7 @@ export class ContinueAgentRuntime implements AgentRuntime {
yield {
requestId: request.requestId,
type: 'status',
message: 'Continue 正在生成回复'
message: 'Continue 正在处理请求'
}
const execute = request.workMode === 'execute'
+13 -6
View File
@@ -1483,9 +1483,16 @@ describe('ModelAgentRuntime', () => {
expect(events).toContainEqual(
expect.objectContaining({
type: 'status',
message: '模型网络请求失败,正在重试(1/3)'
message: '模型网络请求失败,等待重试(1/3,0.5 秒后)'
})
)
const retryWaiting = events.findIndex((event) =>
event.type === 'status' && event.message.includes('等待重试'))
const retryStarted = events.findIndex((event) =>
event.type === 'status' && event.message.includes('正在重试请求'))
const responseStarted = events.findIndex((event) => event.type === 'text')
expect(retryStarted).toBeGreaterThan(retryWaiting)
expect(responseStarted).toBeGreaterThan(retryStarted)
expect(events).toContainEqual(
expect.objectContaining({
type: 'text',
@@ -1561,7 +1568,7 @@ describe('ModelAgentRuntime', () => {
)
expect(events).not.toContainEqual(
expect.objectContaining({
message: expect.stringContaining('正在重试')
message: expect.stringContaining('等待重试')
})
)
})
@@ -1598,7 +1605,7 @@ describe('ModelAgentRuntime', () => {
expect(fetcher).toHaveBeenCalledOnce()
expect(events).not.toContainEqual(
expect.objectContaining({
message: expect.stringContaining('正在重试')
message: expect.stringContaining('等待重试')
})
)
})
@@ -1628,12 +1635,12 @@ describe('ModelAgentRuntime', () => {
)
await expect(stream.next()).resolves.toMatchObject({
value: { type: 'status', message: 'sonnet-5 正在思考' }
value: { type: 'status', message: 'sonnet-5 请求处理中' }
})
await expect(stream.next()).resolves.toMatchObject({
value: {
type: 'status',
message: '模型网络请求失败,正在重试(1/3)'
message: '模型网络请求失败,等待重试(1/3,0.5 秒后)'
}
})
const pendingRetry = stream.next()
@@ -2051,7 +2058,7 @@ describe('ModelAgentRuntime', () => {
expect(events).toContainEqual(
expect.objectContaining({
type: 'status',
message: '模型网络请求失败,正在重试(1/3)'
message: '模型网络请求失败,等待重试(1/3,0.5 秒后)'
})
)
expect(events).toContainEqual(
+12 -3
View File
@@ -1898,13 +1898,22 @@ export class ModelAgentRuntime implements AgentRuntime {
requestId,
type: 'status',
message:
`模型网络请求失败,正在重试(${retryCount + 1}/` +
`${modelRequestRetryDelaysMs.length})`
`模型网络请求失败,等待重试(${retryCount + 1}/` +
`${modelRequestRetryDelaysMs.length},` +
`${modelRequestRetryDelaysMs[retryCount]! / 1_000} 秒后)`
}
await waitForModelRetry(
modelRequestRetryDelaysMs[retryCount]!,
signal
)
signal.throwIfAborted()
yield {
requestId,
type: 'status',
message:
`${this.options.model} 正在重试请求(${retryCount + 1}/` +
`${modelRequestRetryDelaysMs.length}),等待响应`
}
}
throw new Error('模型请求重试状态无效')
}
@@ -3957,7 +3966,7 @@ export class ModelAgentRuntime implements AgentRuntime {
yield {
requestId: request.requestId,
type: 'status',
message: `${this.options.model} 正在思考`
message: `${this.options.model} 请求处理中`
}
const system = [
+46
View File
@@ -288,6 +288,52 @@ async function collectRun(
}
describe("OpenCodeRuntime embedded launcher", () => {
it("forwards retry scheduling and resumes only on a matching native busy event", async () => {
const next = Date.now() + 120_000;
const setup = runClient([
{
type: "session.status",
properties: { sessionID: "another-session", status: {
type: "retry", attempt: 9, message: "Unrelated retry", next,
} },
},
{
type: "session.status",
properties: { sessionID: "session-1", status: {
type: "retry", attempt: 2, message: "HTTP 503", next,
} },
},
{
type: "session.status",
properties: { sessionID: "another-session", status: { type: "busy" } },
},
{
type: "session.status",
properties: { sessionID: "session-1", status: { type: "busy" } },
},
{
type: "session.status",
properties: { sessionID: "session-1", status: { type: "busy" } },
},
{ type: "session.idle", properties: { sessionID: "session-1" } },
]);
const runtime = embeddedRuntime(setup.client);
try {
const events = await collectRun(runtime);
const statuses = events.filter((event) => event.type === "status");
expect(statuses.map((event) => event.message)).toEqual([
"OpenCode 正在处理请求",
`OpenCode 等待第 2 次重试(计划 ${
new Date(next).toLocaleTimeString("zh-CN", { hour12: false })
}):HTTP 503`,
"OpenCode 已开始重试,等待后续进展",
]);
expect(events.at(-1)).toMatchObject({ type: "done" });
} finally {
await runtime.dispose();
}
});
it("checks embedded availability without starting OpenCode", async () => {
const child = fakeChild();
const {
+27
View File
@@ -2466,6 +2466,7 @@ export class OpenCodeRuntime implements AgentRuntime {
return false;
};
const reportedMessageIds = new Set<string>();
let waitingForRetry = false;
for await (const event of subscription.stream) {
const childProgress = subagentProgress.update(event);
if (
@@ -2479,6 +2480,32 @@ export class OpenCodeRuntime implements AgentRuntime {
if (task) task.subagent = childProgress;
yield childProgress;
}
if (
event.type === "session.status" &&
event.properties.sessionID === sessionId
) {
const status = event.properties.status;
if (status.type === "retry") {
waitingForRetry = true;
const scheduled = Number.isSafeInteger(status.next) && status.next > 0
? `(计划 ${new Date(status.next).toLocaleTimeString("zh-CN", { hour12: false })})`
: "";
yield {
requestId: request.requestId,
type: "status",
message: `OpenCode 等待第 ${status.attempt} 次重试${scheduled}:${
safeToolErrorDetail(status.message, 1_000) || "模型请求失败"
}`,
};
} else if (status.type === "busy" && waitingForRetry) {
waitingForRetry = false;
yield {
requestId: request.requestId,
type: "status",
message: "OpenCode 已开始重试,等待后续进展",
};
}
}
if (
event.type === "message.updated" &&
event.properties.sessionID === sessionId &&