@keyou007/agent-bot 0.1.24-alpha.8 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +8 -4
- package/README.zh.md +8 -4
- package/dist/codex/CodexRuntime.js +141 -21
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ProviderSwitch.js +1 -1
- package/dist/codex/ProviderSwitch.js.map +1 -1
- package/dist/feishu/CardRenderer.js +10 -6
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +7 -3
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +26 -10
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/visibleTurnActivities.js +13 -0
- package/dist/presentation/visibleTurnActivities.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +84 -17
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/runtime/responseErrors.js +60 -0
- package/dist/runtime/responseErrors.js.map +1 -0
- package/docs/technical-reference.md +9 -1
- package/docs/technical-reference.zh.md +9 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +3 -1
- package/src/codex/CodexRuntime.ts +139 -20
- package/src/codex/ProviderSwitch.ts +1 -1
- package/src/feishu/CardRenderer.ts +10 -5
- package/src/feishu/FeishuConnector.ts +7 -3
- package/src/local-files/TurnPreviewPage.ts +25 -10
- package/src/presentation/visibleTurnActivities.ts +12 -0
- package/src/proxy/ProxySessionController.ts +90 -20
- package/src/runtime/responseErrors.ts +62 -0
- package/src/runtime/types.ts +7 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const RUNTIME_WARNING_PREFIXES = ["运行请求失败,等待 Agent 返回最终状态:", "运行请求出错,Agent 正在重试:"];
|
|
3
|
+
const responseErrorSchema = z.object({
|
|
4
|
+
type: z.literal("error"),
|
|
5
|
+
status: z.number().optional(),
|
|
6
|
+
error: z.object({
|
|
7
|
+
message: z.string().min(1),
|
|
8
|
+
code: z.string().nullish(),
|
|
9
|
+
param: z.string().nullish(),
|
|
10
|
+
type: z.string().nullish(),
|
|
11
|
+
}).passthrough(),
|
|
12
|
+
}).passthrough();
|
|
13
|
+
function parseResponseError(message) {
|
|
14
|
+
// Agent Bot may append a recovery notice after the upstream JSON error.
|
|
15
|
+
const boundary = message.indexOf("\n\n");
|
|
16
|
+
for (const end of [message.length, ...(boundary >= 0 ? [boundary] : [])]) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = responseErrorSchema.safeParse(JSON.parse(message.slice(0, end)));
|
|
19
|
+
if (parsed.success)
|
|
20
|
+
return { value: parsed.data, notice: message.slice(end) };
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Non-JSON runtime errors remain unchanged.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
export function isResponseContinuationError(message) {
|
|
29
|
+
const parsed = parseResponseError(message);
|
|
30
|
+
return parsed?.value.error.code === "unsupported_persisted_item_context"
|
|
31
|
+
&& parsed.value.error.param === "previous_response_id"
|
|
32
|
+
&& parsed.value.error.type === "invalid_request_error"
|
|
33
|
+
&& parsed.value.status === 400
|
|
34
|
+
&& !parsed.notice;
|
|
35
|
+
}
|
|
36
|
+
export function displayRuntimeError(message) {
|
|
37
|
+
const prefix = RUNTIME_WARNING_PREFIXES.find((value) => message.startsWith(value));
|
|
38
|
+
const parsed = parseResponseError(prefix ? message.slice(prefix.length) : message);
|
|
39
|
+
if (!parsed)
|
|
40
|
+
return message;
|
|
41
|
+
const { error, status } = parsed.value;
|
|
42
|
+
const details = [status === undefined ? undefined : `HTTP ${status}`, error.code, error.param];
|
|
43
|
+
return (prefix ?? "") + `${error.message}\n${details.filter(Boolean).join(" · ")}${parsed.notice}`.trim();
|
|
44
|
+
}
|
|
45
|
+
export function sameRuntimeError(warning, terminal) {
|
|
46
|
+
if (warning.trim() === terminal.trim())
|
|
47
|
+
return true;
|
|
48
|
+
const parsed = parseResponseError(terminal);
|
|
49
|
+
if (parsed) {
|
|
50
|
+
const other = parseResponseError(warning);
|
|
51
|
+
return Boolean(other && !other.notice && (!parsed.notice || isRecoveryNotice(parsed.notice))
|
|
52
|
+
&& JSON.stringify(other.value) === JSON.stringify(parsed.value));
|
|
53
|
+
}
|
|
54
|
+
const suffix = terminal.slice(warning.trim().length);
|
|
55
|
+
return terminal.startsWith(warning.trim()) && isRecoveryNotice(suffix);
|
|
56
|
+
}
|
|
57
|
+
function isRecoveryNotice(text) {
|
|
58
|
+
return /^\n\n(?:检测到临时模型服务错误,正在自动重试|模型续接状态失效,正在恢复连接并继续|已自动重试)/u.test(text);
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=responseErrors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"responseErrors.js","sourceRoot":"","sources":["../../src/runtime/responseErrors.ts"],"names":[],"mappings":"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"}
|
|
@@ -379,7 +379,7 @@ Forks use the latest available completed turn and do not interrupt an active sou
|
|
|
379
379
|
|
|
380
380
|
### History-preserving Provider switches
|
|
381
381
|
|
|
382
|
-
Settings switching uses only the existing thread ID: no implicit thread/start, thread/fork, history rewrite, synthetic Turn, or completed-Turn boundary. The controller independently
|
|
382
|
+
Settings switching for tasks with history uses only the existing thread ID: no implicit thread/start, thread/fork, history rewrite, synthetic Turn, or completed-Turn boundary. A verified empty task with no inherited Turns or Goal may use thread/start with the selected Provider/model. Metadata and a one-item summary page are checked again before persistence. The runtime passes the replaced empty ID to the persistence callback; the controller independently checks the saved ID, absence of any Turn attempt/last Turn/Fork source, and matching local task identity before atomically saving the new remote ID and settings. Creation or persistence failure releases only the candidate subscription and retains the original task. Successful replacement releases the old empty subscription. Settings-only hydration bypasses the old Provider resume, but requires a verified resume before execution. thread/unsubscribe is a subscription operation, not proof of unloading or external ownership: notSubscribed is idempotent, and a timeout requires recovery rather than an unverified success. Failed switches restore the same-thread Provider/model; missing or partial Agent defaults are restored by replacing the known YAML keys, not merging the rejected target back in. Disconnects during asynchronous persistence preserve the need to revalidate on the next request. No model prompt is sent merely to validate settings, so configuration success does not prove endpoint authentication/availability.
|
|
383
383
|
|
|
384
384
|
Codex requires version 0.162.1 or later. The process manager appends `-c thread_unload_delay_secs=0` only to Codex startup arguments; it does not edit global Codex configuration or pass the option to TraeX. After checking that the selected task is inactive, settings changes unsubscribe that thread and poll `thread/read` metadata every 100 ms, for at most 10 seconds, until the same ID reports `notLoaded`. An unsubscribe response or unrelated close notification is insufficient. Only then does `thread/resume` apply and verify the new Provider/model. Failed `systemError` threads use this same path. No shared-process restart, all-task scan, or sibling-task idle check is required. If unloading fails before target resume is requested, recovery directly reattaches and verifies the original settings. If resume was already requested or persistence fails, rollback first unloads the selected thread again. Existing same-ID verification and execution blocking after unverified rollback remain in effect.
|
|
385
385
|
|
|
@@ -387,6 +387,14 @@ Codex requires version 0.162.1 or later. The process manager appends `-c thread_
|
|
|
387
387
|
|
|
388
388
|
`pending_provider_settings` stores a task-scoped target Provider/model/reasoning/permission bundle and the notification route independently of active session fields. Settings mutations and Turn starts are serialized per task, including multiple bound conversations. Cards use the pending Provider’s model catalog and mark the selection as pending; model/thinking callbacks carry the Provider ID to reject stale cards. Terminal events and startup recovery apply pending settings, and every Turn start checks them before submitting a Prompt. Failed switches restore the original Provider/model; a typed runtime error reports whether rollback succeeded. Only after verifying rollback and matching both runtime and persisted settings does the controller clear the draft, restore the card with a warning, and allow queued/new Prompts to continue. Failed or unverified rollback retains the draft and queued Prompts for explicit recovery. Archive clears the draft. Agent defaults are only persisted after the runtime confirms the actual Provider/model.
|
|
389
389
|
|
|
390
|
+
### Responses continuation recovery
|
|
391
|
+
|
|
392
|
+
After a Codex terminal failure containing a structured Responses error with HTTP 400, `invalid_request_error`, `code=unsupported_persisted_item_context`, and `param=previous_response_id`, the controller allows one automatic recovery on the existing accepted attempt. Native warning/retry notifications alone never trigger it. The runtime verifies that the failed Turn is still the latest using metadata and one summary page, then calls `turn/start` with `input: []` on the same thread and unchanged execution settings. Codex opens a fresh model request with its existing history and completed tool outputs; Agent Bot does not replay the original Prompt or images. This is a new physical Turn and progress card, linked to the original attempt and pending reactions, not an in-place retry of the failed model step. It does not restart the shared App Server, modify the Provider, fork, or rewrite history.
|
|
393
|
+
|
|
394
|
+
The attempt's persisted retry count limits this recovery to once; other HTTP 400/authentication errors are not eligible. A pending Provider change, cancellation, Reset, newer Turn, changed settings, or lost runtime attachment prevents recovery. Stop/Reset abort preflight and interrupt a continuation that starts while cancellation is in flight. An ambiguous `turn/start` failure is not resubmitted and never falls back to replaying the Prompt. Recovery cannot guarantee that the upstream service will accept the new request; a second rejection remains terminal. This path was verified against Codex 0.162.1 with an isolated local Responses WebSocket service injecting the exact rejection after a successful command, then checking a new connection, complete tool result, unchanged user-message count, and no repeated command execution.
|
|
395
|
+
|
|
396
|
+
Presentation filters only the same Turn's matching `commentary:runtime-error:<turnId>` when the final failure already displays it, including Agent Bot's appended recovery notice. Distinct errors and warnings on running/successful Turns stay visible. Card pagination and Preview snapshots use the same filter; SSE patches explicitly remove a previously rendered duplicate warning. Structured Responses errors display their message, HTTP status, code, and parameter as literal text instead of a JSON envelope. Original runtime events, preview journals, and saved snapshots are not rewritten.
|
|
397
|
+
|
|
390
398
|
## Persistence And Recovery
|
|
391
399
|
|
|
392
400
|
SQLite stores:
|
|
@@ -379,7 +379,7 @@ Agent Bot 内部保留本地路由键以关联飞书卡片和投递状态,但
|
|
|
379
379
|
|
|
380
380
|
### 保留历史的 Provider 切换
|
|
381
381
|
|
|
382
|
-
|
|
382
|
+
已有历史的任务切换只使用现有线程 ID,不隐式调用 thread/start、thread/fork,不改写历史、不制造轮次。确认没有继承历史或 Goal 的空任务可使用 thread/start 创建指定 Provider/模型的底层线程,保存前再次读取元信息和一条 Turn 摘要验证。运行时向持久化回调传递被替换的空线程 ID;控制器独立核对已保存 ID、本地任务标识,以及不存在提交尝试、最后轮次和 Fork 来源后,原子保存新远端 ID 和运行设置。创建或持久化失败只释放候选线程订阅并保留原会话,成功后再释放原空线程订阅。设置专用加载不依赖旧 Provider 的恢复成功,但执行前仍必须验证恢复结果。thread/unsubscribe 是订阅操作,不代表已卸载,也不能据此断言其他客户端占用;notSubscribed 按幂等结果处理,超时必须恢复验证而非报告成功。失败回退在同一线程验证原 Provider/模型,原本缺省或不完整的 Agent 默认值通过替换 YAML 已知字段恢复,不把失败目标重新合并进去。异步持久化期间断线会保留下一次请求前重新验证的标记。切设置不会偷偷发送模型 Prompt 测连,配置成功不等于端点认证或服务可用。
|
|
383
383
|
|
|
384
384
|
Codex 要求 0.162.1 或更新版本。进程管理器仅在 Codex 启动参数末尾追加 `-c thread_unload_delay_secs=0`,不修改全局 Codex 配置,也不向 TraeX 传递此参数。设置切换先确认目标任务没有活动轮次,再取消该线程订阅,每 100 毫秒轮询 `thread/read` 元数据,最多等待 10 秒,直到同一 ID 确认 `notLoaded`,才调用 `thread/resume` 应用并验证新 Provider 和模型。取消订阅的响应或其他线程的关闭通知均不能代替实际卸载确认。失败的 `systemError` 线程也使用相同流程;不再重启共享进程、扫描全部任务或要求其他任务空闲。若卸载失败且尚未请求目标恢复,直接重新订阅并验证原设置;若已请求目标恢复或持久化失败,回退前仍先卸载目标线程。继续保留同 ID 校验及回退无法确认时阻止执行的保护。
|
|
385
385
|
|
|
@@ -387,6 +387,14 @@ Codex 要求 0.162.1 或更新版本。进程管理器仅在 Codex 启动参数
|
|
|
387
387
|
|
|
388
388
|
`pending_provider_settings` 按任务保存目标 Provider、模型、思考强度、权限和通知路由,与实际 session 字段分离。设置修改和轮次启动按任务串行,覆盖多个会话绑定同一任务的情况。卡片使用目标 Provider 的模型列表并标注待生效,模型和思考强度回调携带 Provider ID,避免旧卡误操作。终态事件和启动恢复应用待生效设置,每次启动新轮次前也会检查。切换失败回退原 Provider/模型,运行时通过结构化错误报告回退结果。只有确认回退成功且运行时、持久化设置都与原设置一致时,才清除待生效选择、恢复卡片并显示警告,排队及新 Prompt 继续使用原设置。回退失败或无法确认时保留选择和排队 Prompt 等待手动恢复;归档清理待生效设置。仅在运行时确认实际 Provider/模型后才保存 Agent 默认值。
|
|
389
389
|
|
|
390
|
+
### Responses 续接恢复
|
|
391
|
+
|
|
392
|
+
Codex 以结构化 Responses 错误结束轮次,且 HTTP 400、`invalid_request_error`、`code=unsupported_persisted_item_context`、`param=previous_response_id` 全部匹配时,控制器允许原已接受请求自动恢复一次。运行警告或原生重试通知本身不触发恢复。Runtime 仅通过元数据和一页轮次摘要确认失败轮次仍为最新,再以原线程及运行设置发送 `turn/start`、`input: []`。Codex 使用已有历史和已完成的工具结果发起新的模型请求;Agent Bot 不重发原 Prompt 或图片。这是新增的真实轮次及思考卡片,沿用原请求记录及待完成的 Reaction,不是原模型步骤的原地重试;不重启共享 App Server、不改变 Provider、不 Fork,也不改写历史。
|
|
393
|
+
|
|
394
|
+
请求记录中持久化的重试计数将此恢复限制为一次,其他 HTTP 400 或认证错误不适用。存在待生效 Provider 切换、取消、Reset、新轮次、设置变化或 Runtime 连接丢失时不自动继续。Stop/Reset 取消恢复检查,并中断取消期间才启动的续接轮次。`turn/start` 结果不明确时不重复提交,也不回退为重发 Prompt。无法保证上游一定接受新请求,第二次拒绝仍按失败结束。已使用真实 Codex 0.162.1 和隔离的本地 Responses WebSocket 服务验证:命令成功后注入相同错误,恢复使用新连接、完整工具结果及相同用户消息数,命令不重复执行。
|
|
395
|
+
|
|
396
|
+
显示层只在最终错误已经包含同一轮次的 `commentary:runtime-error:<turnId>` 时隐藏重复警告,允许附带 Agent Bot 的恢复提示;不同错误、运行中及成功轮次的警告仍保留。卡片分页和 Preview 快照共用过滤规则,SSE 更新还会明确移除之前显示的重复警告。结构化 Responses 错误以纯文本显示消息、HTTP 状态、错误码及参数,不再直接显示 JSON 包装。原始 Runtime 事件、Preview 日志和快照不作改写。
|
|
397
|
+
|
|
390
398
|
## 持久化与恢复
|
|
391
399
|
|
|
392
400
|
SQLite 保存:
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keyou007/agent-bot",
|
|
3
|
-
"version": "0.1.24
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@keyou007/agent-bot",
|
|
9
|
-
"version": "0.1.24
|
|
9
|
+
"version": "0.1.24",
|
|
10
10
|
"license": "GPL-3.0-only",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@agentclientprotocol/sdk": "1.2.1",
|
package/package.json
CHANGED
|
@@ -139,7 +139,9 @@ Providers such as a Responses bridge may omit assistant message phases. Unphased
|
|
|
139
139
|
|
|
140
140
|
Provider changes during an Agent Bot turn are staged until it ends; do not stop the turn. The settings card and CLI show the pending Provider/model, and model/thinking/permission selections edit that pending configuration. The latest selection is applied before the next turn, including queued/retried turns. Pending settings survive restart, remain separate from actual task settings, and are verified before saving Agent defaults. Deferred failures roll back to the original Provider/model. After the runtime confirms rollback, the card returns to the original settings, the pending selection is cleared, and queued prompts continue with the original settings. Failed or unverified rollback still pauses queued prompts and requires explicit recovery; never claim rollback success based only on unchanged local fields. Custom Providers discover models through their OpenAI-compatible `/models` endpoint when available. Switching keeps the previous model when supported, otherwise uses the Provider default or its first model. A missing model-list endpoint falls back to the current or configured model and does not by itself block switching. Wait for active work to finish; do not stop it without the user's request. Switching detaches only the selected thread and waits for its actual unload before resuming. On failure, previous settings are retained and recovery is attempted. If recovery fails, resolve the reported configuration or ownership issue and retry the Provider switch before sending another prompt. Never replace a forked task with an empty task to work around a switch failure.
|
|
141
141
|
|
|
142
|
-
Provider changes must keep the same remote task ID and complete history. Never use Fork,
|
|
142
|
+
Provider changes for tasks with history must keep the same remote task ID and complete history. Never use Fork, synthetic Turns, or history repair to change settings. A verified empty task with no Turns, inherited history, or Goal may receive a new remote ID; Agent Bot keeps its local task and conversation bindings and commits only after verifying the target settings. Failure retains the original empty task. A rejected switch must report the reason and verify rollback; Codex is started with zero thread-unload delay, and switching waits for the selected thread to report notLoaded before resuming it. Other tasks may remain active, and empty sibling tasks stay untouched. An unload timeout does not prove external ownership; inspect the selected task and retry after releasing any remaining subscription. Do not restart or force-release the shared process to switch Provider. Settings inspection and selection must remain available when the old Provider cannot resume.
|
|
143
|
+
|
|
144
|
+
After the specific Codex Responses failure `unsupported_persisted_item_context` for `previous_response_id`, Agent Bot can automatically start one empty-input continuation in the same thread. It retains history and completed tool results without resending the original Prompt/images; it creates a new Turn/card, not a new task or Fork. Do not also send a retry Prompt while recovery is pending. Stop/Reset cancel recovery; changed settings, newer work, or a lost attachment block it. A second rejection remains a failure, not an unlimited retry. Matching progress warnings and final errors appear once in cards and Preview, with raw diagnostic events retained. This execution recovery is separate from Provider switching; it never changes settings or restarts the shared process.
|
|
143
145
|
|
|
144
146
|
## Goals And Turns
|
|
145
147
|
|
|
@@ -28,9 +28,11 @@ import type {
|
|
|
28
28
|
RuntimeReleaseResult,
|
|
29
29
|
RuntimeSession,
|
|
30
30
|
RuntimeSessionMetadata,
|
|
31
|
+
RuntimeSettingsReplacement,
|
|
31
32
|
} from "../runtime/types.js";
|
|
32
33
|
import { appendGeneratedImageMarkdown } from "../utils/generatedImageMarkdown.js";
|
|
33
34
|
import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
35
|
+
import { isResponseContinuationError } from "../runtime/responseErrors.js";
|
|
34
36
|
import { AppServerRequestError } from "./AppServerConnection.js";
|
|
35
37
|
import { workspaceSandboxFromConfig, workspaceSandboxFromResponse, type TurnSandboxPolicy, type WorkspaceSandboxPolicy } from "./AppServerPermissions.js";
|
|
36
38
|
import { formatCodexError, mapCodexNotification } from "./CodexEventMapper.js";
|
|
@@ -88,6 +90,7 @@ interface CodexSession extends RuntimeSession {
|
|
|
88
90
|
terminalTurnIds: Set<string>;
|
|
89
91
|
modeChangeRequestIds: Set<string>;
|
|
90
92
|
planContinuationTurnId?: string;
|
|
93
|
+
responseFailureTurnId?: string;
|
|
91
94
|
finalText: string;
|
|
92
95
|
generatedImagePaths: string[];
|
|
93
96
|
messagePhases: Map<string, "commentary" | "final_answer">;
|
|
@@ -266,16 +269,51 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
266
269
|
}
|
|
267
270
|
|
|
268
271
|
async startTurn(sessionId: string, prompt: RuntimePrompt): Promise<string> {
|
|
269
|
-
return this.runSessionOperation(sessionId, () =>
|
|
272
|
+
return this.runSessionOperation(sessionId, () => {
|
|
273
|
+
this.requireSession(sessionId).responseFailureTurnId = undefined;
|
|
274
|
+
return this.startTurnNow(sessionId, prompt);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
get recoverResponseContinuation(): AgentRuntime["recoverResponseContinuation"] {
|
|
279
|
+
return this.provider.getAgentFamily?.() === "codex"
|
|
280
|
+
? (sessionId, failedTurnId, signal) => this.recoverResponseContinuationNow(sessionId, failedTurnId, signal)
|
|
281
|
+
: undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private async recoverResponseContinuationNow(sessionId: string, failedTurnId: string, signal?: AbortSignal): Promise<string> {
|
|
285
|
+
return this.runSessionOperation(sessionId, async () => {
|
|
286
|
+
signal?.throwIfAborted();
|
|
287
|
+
const session = this.requireSession(sessionId);
|
|
288
|
+
if (session.responseFailureTurnId !== failedTurnId || session.activeTurnId || session.needsResume) {
|
|
289
|
+
throw new Error("模型续接恢复已失效,请发送消息继续。");
|
|
290
|
+
}
|
|
291
|
+
// Consume before starting: a timeout must not create a second continuation.
|
|
292
|
+
session.responseFailureTurnId = undefined;
|
|
293
|
+
const client = await this.client();
|
|
294
|
+
const metadata = await client.request<ThreadReadResponse>("thread/read", {
|
|
295
|
+
threadId: session.remoteSessionId, includeTurns: false,
|
|
296
|
+
}, SYNC_REQUEST_TIMEOUT_MS);
|
|
297
|
+
const latest = await this.readLatestThreadTurns(client, session.remoteSessionId, "summary");
|
|
298
|
+
signal?.throwIfAborted();
|
|
299
|
+
if (session.activeTurnId || session.needsResume || metadata.thread.id !== session.remoteSessionId || metadata.thread.status?.type === "active"
|
|
300
|
+
|| latest.at(-1)?.id !== failedTurnId || latest.at(-1)?.status !== "failed") {
|
|
301
|
+
throw new Error("任务已发生变化,未自动恢复模型续接。请检查当前任务后继续。");
|
|
302
|
+
}
|
|
303
|
+
const turnId = await this.startTurnNow(sessionId, "", true);
|
|
304
|
+
if (signal?.aborted && session.activeTurnId === turnId) await this.cancelTurn(sessionId, turnId);
|
|
305
|
+
signal?.throwIfAborted();
|
|
306
|
+
return turnId;
|
|
307
|
+
});
|
|
270
308
|
}
|
|
271
309
|
|
|
272
|
-
private async startTurnNow(sessionId: string, prompt: RuntimePrompt): Promise<string> {
|
|
310
|
+
private async startTurnNow(sessionId: string, prompt: RuntimePrompt, responseContinuation = false): Promise<string> {
|
|
273
311
|
const session = this.requireSession(sessionId);
|
|
274
312
|
const client = await this.client();
|
|
275
313
|
await this.ensureSessionResumed(session, client);
|
|
276
314
|
const start = async () => client.request<{ turn: { id: string } }>("turn/start", {
|
|
277
315
|
threadId: session.remoteSessionId,
|
|
278
|
-
input: codexUserInput(prompt),
|
|
316
|
+
input: responseContinuation ? [] : codexUserInput(prompt),
|
|
279
317
|
cwd: session.cwd,
|
|
280
318
|
model: session.model,
|
|
281
319
|
effort: session.reasoningEffort,
|
|
@@ -293,7 +331,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
293
331
|
try {
|
|
294
332
|
response = await start();
|
|
295
333
|
} catch (error) {
|
|
296
|
-
if (!isInvalidWorkingDirectoryError(error)) throw error;
|
|
334
|
+
if (responseContinuation || !isInvalidWorkingDirectoryError(error)) throw error;
|
|
297
335
|
this.logger.warn(
|
|
298
336
|
{ error, sessionId: session.localSessionId, cwd: session.cwd },
|
|
299
337
|
"App Server rejected the task working directory; refreshing the current thread before retrying.",
|
|
@@ -797,24 +835,37 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
797
835
|
}
|
|
798
836
|
|
|
799
837
|
async setModel(sessionId: string, model: string): Promise<void> {
|
|
800
|
-
await this.runSessionOperation(sessionId, async () => {
|
|
838
|
+
await this.runSessionOperation(sessionId, async () => {
|
|
839
|
+
const session = this.requireSession(sessionId);
|
|
840
|
+
session.model = model;
|
|
841
|
+
session.responseFailureTurnId = undefined;
|
|
842
|
+
});
|
|
801
843
|
}
|
|
802
844
|
|
|
803
845
|
async setReasoningEffort(sessionId: string, effort: string): Promise<void> {
|
|
804
|
-
await this.runSessionOperation(sessionId, async () => {
|
|
846
|
+
await this.runSessionOperation(sessionId, async () => {
|
|
847
|
+
const session = this.requireSession(sessionId);
|
|
848
|
+
session.reasoningEffort = effort;
|
|
849
|
+
session.responseFailureTurnId = undefined;
|
|
850
|
+
});
|
|
805
851
|
}
|
|
806
852
|
|
|
807
853
|
async setPermissionMode(sessionId: string, mode: PermissionMode): Promise<void> {
|
|
808
|
-
await this.runSessionOperation(sessionId, async () => {
|
|
854
|
+
await this.runSessionOperation(sessionId, async () => {
|
|
855
|
+
const session = this.requireSession(sessionId);
|
|
856
|
+
session.permissionMode = mode;
|
|
857
|
+
session.responseFailureTurnId = undefined;
|
|
858
|
+
});
|
|
809
859
|
}
|
|
810
860
|
|
|
811
861
|
async setExecutionSettings(
|
|
812
862
|
sessionId: string,
|
|
813
863
|
settings: RuntimeExecutionSettings,
|
|
814
|
-
persist?: (session: RuntimeSession) => Promise<void>,
|
|
864
|
+
persist?: (session: RuntimeSession, replacement?: RuntimeSettingsReplacement) => Promise<void>,
|
|
815
865
|
): Promise<RuntimeSession> {
|
|
816
866
|
return this.runSessionOperation(sessionId, async () => {
|
|
817
867
|
try {
|
|
868
|
+
this.requireSession(sessionId).responseFailureTurnId = undefined;
|
|
818
869
|
return await this.setExecutionSettingsNow(sessionId, settings, persist);
|
|
819
870
|
} catch (error) {
|
|
820
871
|
if (error instanceof ProviderSwitchError) throw error;
|
|
@@ -828,7 +879,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
828
879
|
private async setExecutionSettingsNow(
|
|
829
880
|
sessionId: string,
|
|
830
881
|
settings: RuntimeExecutionSettings,
|
|
831
|
-
persist?: (session: RuntimeSession) => Promise<void>,
|
|
882
|
+
persist?: (session: RuntimeSession, replacement?: RuntimeSettingsReplacement) => Promise<void>,
|
|
832
883
|
): Promise<RuntimeSession> {
|
|
833
884
|
const session = this.requireSession(sessionId);
|
|
834
885
|
if (session.activeTurnId) throw new Error(PROVIDER_SWITCH_BUSY);
|
|
@@ -836,19 +887,13 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
836
887
|
const metadata = await client.request<ThreadReadResponse>("thread/read", {
|
|
837
888
|
threadId: session.remoteSessionId, includeTurns: false,
|
|
838
889
|
}, SYNC_REQUEST_TIMEOUT_MS);
|
|
890
|
+
if (metadata.thread.id !== session.remoteSessionId) throw new Error("App Server 返回了不同的任务 ID,未切换 Provider。");
|
|
839
891
|
const localActive = await this.localActivityDetector?.activeThreads([session.remoteSessionId]);
|
|
840
892
|
if (metadata.thread.status?.type === "active" || localActive?.has(session.remoteSessionId) || session.activeTurnId) {
|
|
841
893
|
throw new Error(PROVIDER_SWITCH_BUSY);
|
|
842
894
|
}
|
|
843
|
-
if (
|
|
844
|
-
|
|
845
|
-
const turns = await this.readLatestThreadTurns(client, session.remoteSessionId, "summary");
|
|
846
|
-
if (turns.length === 0) throw new Error(PROVIDER_SWITCH_EMPTY);
|
|
847
|
-
} catch (error) {
|
|
848
|
-
if (!(error instanceof AppServerRequestError) || error.method !== "thread/turns/list"
|
|
849
|
-
|| !/missing source rollout|no rollout found/iu.test(error.serverMessage)) throw error;
|
|
850
|
-
throw new Error(PROVIDER_SWITCH_EMPTY, { cause: error });
|
|
851
|
-
}
|
|
895
|
+
if (await this.isEmptySettingsSession(client, session, metadata.thread)) {
|
|
896
|
+
return this.replaceEmptySettingsSession(client, session, settings, persist);
|
|
852
897
|
}
|
|
853
898
|
const previous = { ...session };
|
|
854
899
|
const restoreProvider = previous.modelProvider?.trim()
|
|
@@ -911,6 +956,72 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
911
956
|
return session;
|
|
912
957
|
}
|
|
913
958
|
|
|
959
|
+
private async isEmptySettingsSession(client: AppServerClient, session: CodexSession, thread: CodexThreadSnapshot): Promise<boolean> {
|
|
960
|
+
if (thread.id !== session.remoteSessionId || thread.preview !== "" || thread.forkedFromId
|
|
961
|
+
|| (session.turnCount === undefined && thread.forkedFromId !== null)
|
|
962
|
+
|| session.activeTurnId || (session.turnCount ?? 0) > 0 || session.knownTurnIds.size > 0
|
|
963
|
+
|| session.terminalTurnIds.size > 0 || (thread.turns?.length ?? 0) > 0) return false;
|
|
964
|
+
try {
|
|
965
|
+
if ((await this.readLatestThreadTurns(client, session.remoteSessionId, "summary")).length > 0) return false;
|
|
966
|
+
} catch (error) {
|
|
967
|
+
if (!(error instanceof AppServerRequestError) || error.method !== "thread/turns/list"
|
|
968
|
+
|| !/missing source rollout|no rollout found|not materialized yet/iu.test(error.serverMessage)) throw error;
|
|
969
|
+
}
|
|
970
|
+
if (!["idle", "notLoaded"].includes(thread.status?.type ?? "")) throw new Error(PROVIDER_SWITCH_EMPTY);
|
|
971
|
+
const { goal } = await client.request<{ goal: RuntimeGoal | null }>("thread/goal/get", {
|
|
972
|
+
threadId: session.remoteSessionId,
|
|
973
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
974
|
+
if (goal !== null) throw new Error(PROVIDER_SWITCH_EMPTY);
|
|
975
|
+
return true;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
private async replaceEmptySettingsSession(
|
|
979
|
+
client: AppServerClient, session: CodexSession, settings: RuntimeExecutionSettings,
|
|
980
|
+
persist?: (session: RuntimeSession, replacement?: RuntimeSettingsReplacement) => Promise<void>,
|
|
981
|
+
): Promise<RuntimeSession> {
|
|
982
|
+
const oldId = session.remoteSessionId;
|
|
983
|
+
let replacementId: string | undefined;
|
|
984
|
+
try {
|
|
985
|
+
// Keep the original attachment until target verification and persistence both succeed.
|
|
986
|
+
const response = await client.request<ThreadResponse>("thread/start", {
|
|
987
|
+
cwd: session.cwd, modelProvider: settings.modelProvider, model: settings.model, threadSource: "user",
|
|
988
|
+
...threadLifecycleParams(session.cwd), ...permissionParams(settings.permissionMode),
|
|
989
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
990
|
+
if (!response.thread.id || response.thread.id === oldId) throw new Error("App Server 未创建独立的空任务,未保存设置。");
|
|
991
|
+
replacementId = response.thread.id;
|
|
992
|
+
assertProviderSettingsApplied(response, settings);
|
|
993
|
+
if (session.title) await client.request("thread/name/set", { threadId: replacementId, name: session.title }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
994
|
+
const { thread } = await client.request<ThreadReadResponse>("thread/read", { threadId: oldId, includeTurns: false }, SYNC_REQUEST_TIMEOUT_MS);
|
|
995
|
+
const active = await this.localActivityDetector?.activeThreads([oldId]);
|
|
996
|
+
if (active?.has(oldId) || !await this.isEmptySettingsSession(client, session, thread)) throw new Error(PROVIDER_SWITCH_BUSY);
|
|
997
|
+
if (this.attachedClient !== client) throw new Error("App Server 连接已变化,请重试 Provider 切换。");
|
|
998
|
+
const candidate = this.makeSession({
|
|
999
|
+
localSessionId: session.localSessionId, agentName: session.agentName, cwd: session.cwd, title: session.title, ...settings,
|
|
1000
|
+
}, response, settings.reasoningEffort);
|
|
1001
|
+
candidate.title = session.title;
|
|
1002
|
+
await persist?.(candidate, { emptyRemoteSessionId: oldId });
|
|
1003
|
+
Object.assign(session, candidate, {
|
|
1004
|
+
needsResume: this.attachedClient !== client, settingsRecoveryError: undefined, remoteSummary: undefined,
|
|
1005
|
+
});
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
if (replacementId) await this.releaseUnusedSettingsThread(client, replacementId);
|
|
1008
|
+
throw new ProviderSwitchError(`Provider 切换失败:${providerSwitchFailure(error)} 原会话和设置已保留。`,
|
|
1009
|
+
!session.needsResume && !session.settingsRecoveryError, { cause: error });
|
|
1010
|
+
}
|
|
1011
|
+
if (![...this.sessions.values()].some(other => other.remoteSessionId === oldId)) {
|
|
1012
|
+
await this.releaseUnusedSettingsThread(client, oldId);
|
|
1013
|
+
}
|
|
1014
|
+
return session;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
private async releaseUnusedSettingsThread(client: AppServerClient, threadId: string): Promise<void> {
|
|
1018
|
+
try {
|
|
1019
|
+
await client.request("thread/unsubscribe", { threadId }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
this.logger.warn({ error, threadId }, "Could not release an unused empty settings thread.");
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
914
1025
|
private async detachThreadForSettings(client: AppServerClient, session: CodexSession, onDetach?: () => void): Promise<void> {
|
|
915
1026
|
const current = await client.request<ThreadReadResponse>("thread/read", {
|
|
916
1027
|
threadId: session.remoteSessionId, includeTurns: false,
|
|
@@ -1426,6 +1537,9 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1426
1537
|
session.activeTurnStartedAt = undefined;
|
|
1427
1538
|
session.terminalTurnIds.add(mapped.turnId);
|
|
1428
1539
|
session.messagePhases.clear();
|
|
1540
|
+
session.responseFailureTurnId = mapped.status === "failed" && mapped.error
|
|
1541
|
+
&& this.provider.getAgentFamily?.() === "codex" && isResponseContinuationError(mapped.error)
|
|
1542
|
+
? mapped.turnId : undefined;
|
|
1429
1543
|
if (mapped.status === "cancelled") {
|
|
1430
1544
|
this.emit({ type: "turn_cancelled", sessionId, turnId: mapped.turnId });
|
|
1431
1545
|
} else if (mapped.status === "failed") {
|
|
@@ -1604,6 +1718,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1604
1718
|
if (session.turnCount !== undefined) session.turnCount += 1;
|
|
1605
1719
|
}
|
|
1606
1720
|
session.activeTurnId = turnId;
|
|
1721
|
+
session.responseFailureTurnId = undefined;
|
|
1607
1722
|
session.activeTurnStartedAt = startedAt;
|
|
1608
1723
|
session.planContinuationTurnId = undefined;
|
|
1609
1724
|
session.modeChangeRequestIds.clear();
|
|
@@ -1638,6 +1753,9 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1638
1753
|
session.activeTurnStartedAt = undefined;
|
|
1639
1754
|
session.terminalTurnIds.add(turn.id);
|
|
1640
1755
|
session.messagePhases.clear();
|
|
1756
|
+
const failure = formatCodexError(turn.error);
|
|
1757
|
+
session.responseFailureTurnId = turn.status === "failed" && failure
|
|
1758
|
+
&& this.provider.getAgentFamily?.() === "codex" && isResponseContinuationError(failure) ? turn.id : undefined;
|
|
1641
1759
|
if (turn.status === "interrupted") {
|
|
1642
1760
|
this.emit({ type: "turn_cancelled", sessionId: session.localSessionId, turnId: turn.id });
|
|
1643
1761
|
return;
|
|
@@ -1647,7 +1765,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1647
1765
|
type: "turn_failed",
|
|
1648
1766
|
sessionId: session.localSessionId,
|
|
1649
1767
|
turnId: turn.id,
|
|
1650
|
-
message:
|
|
1768
|
+
message: failure ?? "App Server turn failed.",
|
|
1651
1769
|
});
|
|
1652
1770
|
return;
|
|
1653
1771
|
}
|
|
@@ -1710,7 +1828,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1710
1828
|
workspaceSandboxPolicy: workspaceSandboxFromResponse(response.sandbox),
|
|
1711
1829
|
rolloutPath: stringValue(response.thread.path)?.trim() || undefined,
|
|
1712
1830
|
turnCount: "remoteSessionId" in input ? undefined : 0,
|
|
1713
|
-
knownTurnIds: new Set(),
|
|
1831
|
+
knownTurnIds: new Set("lastTurnId" in input && input.lastTurnId ? [input.lastTurnId] : []),
|
|
1714
1832
|
};
|
|
1715
1833
|
}
|
|
1716
1834
|
|
|
@@ -1782,6 +1900,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1782
1900
|
for (const session of this.sessions.values()) {
|
|
1783
1901
|
session.needsResume = true;
|
|
1784
1902
|
session.planContinuationTurnId = undefined;
|
|
1903
|
+
session.responseFailureTurnId = undefined;
|
|
1785
1904
|
const turnId = session.activeTurnId;
|
|
1786
1905
|
if (!turnId) continue;
|
|
1787
1906
|
session.activeTurnId = undefined;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RuntimeExecutionSettings } from "../runtime/types.js";
|
|
2
2
|
|
|
3
3
|
export const PROVIDER_SWITCH_BUSY = "当前任务正在执行,暂时不能切换 Provider。请等待任务完成,或停止任务后重试。";
|
|
4
|
-
export const PROVIDER_SWITCH_EMPTY = "
|
|
4
|
+
export const PROVIDER_SWITCH_EMPTY = "无法确认此会话不包含历史、分支或 Goal,未替换任务。请检查原会话状态后重试。";
|
|
5
5
|
|
|
6
6
|
export class ProviderSwitchError extends Error {
|
|
7
7
|
constructor(message: string, readonly previousSettingsRestored: boolean, options?: ErrorOptions) {
|
|
@@ -13,6 +13,8 @@ import type {
|
|
|
13
13
|
ToolState,
|
|
14
14
|
} from "../runtime/types.js";
|
|
15
15
|
import type { TurnActivity, TurnViewState, TurnViewStatus } from "../presentation/turnViewTypes.js";
|
|
16
|
+
import { visibleTurnActivities } from "../presentation/visibleTurnActivities.js";
|
|
17
|
+
import { displayRuntimeError } from "../runtime/responseErrors.js";
|
|
16
18
|
import { truncateMiddle, truncateText } from "../utils/markdown.js";
|
|
17
19
|
import type { UpdateNotice } from "../updates/types.js";
|
|
18
20
|
import type { ActiveTerminal, TerminalCardView } from "../terminal/TerminalServer.js";
|
|
@@ -1778,7 +1780,7 @@ function renderTurnElements(
|
|
|
1778
1780
|
if (state.fileSummary.length > 0) elements.push(fileSummaryPanel(state));
|
|
1779
1781
|
|
|
1780
1782
|
if (state.approval) elements.push(...renderApprovalElements(state, state.approval));
|
|
1781
|
-
if (state.error) elements.push(markdown(codeBlock(state.error, 2_000)));
|
|
1783
|
+
if (state.error) elements.push(markdown(codeBlock(displayRuntimeError(state.error), 2_000)));
|
|
1782
1784
|
const showAssistantText = state.assistantText && assistantTextMode === "always";
|
|
1783
1785
|
if (showAssistantText) {
|
|
1784
1786
|
if (elements.length > 0) elements.push({ tag: "hr" });
|
|
@@ -1818,7 +1820,7 @@ function renderGroupedTurnElements(
|
|
|
1818
1820
|
if (state.fileSummary.length > 0) elements.push(fileSummaryPanel(state));
|
|
1819
1821
|
|
|
1820
1822
|
if (state.approval) elements.push(...renderApprovalElements(state, state.approval));
|
|
1821
|
-
if (state.error) elements.push(markdown(codeBlock(state.error, 2_000)));
|
|
1823
|
+
if (state.error) elements.push(markdown(codeBlock(displayRuntimeError(state.error), 2_000)));
|
|
1822
1824
|
const showAssistantText = state.assistantText && assistantTextMode === "always";
|
|
1823
1825
|
if (showAssistantText) {
|
|
1824
1826
|
if (elements.length > 0) elements.push({ tag: "hr" });
|
|
@@ -1954,7 +1956,8 @@ function renderActivity(
|
|
|
1954
1956
|
const text = activity.text.trim();
|
|
1955
1957
|
if (!text) return [];
|
|
1956
1958
|
if (activity.id.startsWith("commentary:runtime-error:")) {
|
|
1957
|
-
const
|
|
1959
|
+
const displayed = displayRuntimeError(text);
|
|
1960
|
+
const chunks = fullAssistantText ? splitText(displayed, ACTIVITY_TEXT_CHUNK) : [displayed];
|
|
1958
1961
|
return chunks.map((chunk) => markdown(codeBlock(chunk, fullAssistantText ? ACTIVITY_TEXT_CHUNK : MAX_LIVE_ASSISTANT_TEXT)));
|
|
1959
1962
|
}
|
|
1960
1963
|
return fullAssistantText
|
|
@@ -2327,8 +2330,10 @@ function removeMarkdownBold(value: string): string {
|
|
|
2327
2330
|
}
|
|
2328
2331
|
|
|
2329
2332
|
function turnActivities(state: TurnViewState): TurnActivity[] {
|
|
2330
|
-
if (state.activities?.length)
|
|
2331
|
-
|
|
2333
|
+
if (state.activities?.length) {
|
|
2334
|
+
const activities = visibleTurnActivities(state);
|
|
2335
|
+
return state.approval ? activities.filter((a) => a.id !== approvalActivityId(state.turnId, state.approval!.id)) : activities;
|
|
2336
|
+
}
|
|
2332
2337
|
|
|
2333
2338
|
const activities: TurnActivity[] = [];
|
|
2334
2339
|
if (state.progressText) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import type { Logger } from "pino";
|
|
2
3
|
import type { WSClient as LarkWsClient, WSConnectionStatus } from "@larksuiteoapi/node-sdk";
|
|
3
4
|
import type { AppConfig } from "../config/schema.js";
|
|
@@ -395,7 +396,7 @@ function toCardAction(data: unknown) {
|
|
|
395
396
|
const openId = event.operator?.open_id ?? event.operator?.user_id;
|
|
396
397
|
const chatId = event.context?.open_chat_id ?? event.chat_id;
|
|
397
398
|
return {
|
|
398
|
-
actionId: getEventId(data) ?? event.action.action_id ??
|
|
399
|
+
actionId: getEventId(data) ?? event.action.action_id ?? randomUUID(),
|
|
399
400
|
contextKey: chatId ? `chat_id:${chatId}` : `open_id:${openId}`,
|
|
400
401
|
userId: openId,
|
|
401
402
|
...(typeof event.context?.open_message_id === "string" ? { messageId: event.context.open_message_id } : {}),
|
|
@@ -415,8 +416,11 @@ function getCardActionValue(action: Record<string, unknown>): Record<string, unk
|
|
|
415
416
|
}
|
|
416
417
|
|
|
417
418
|
function getEventId(data: unknown): string | undefined {
|
|
418
|
-
|
|
419
|
-
|
|
419
|
+
// The SDK flattens header fields into the event passed to handlers.
|
|
420
|
+
const value = getNested<unknown>(data, ["header", "event_id"])
|
|
421
|
+
?? getNested(data, ["data", "header", "event_id"])
|
|
422
|
+
?? getNested(data, ["event_id"]);
|
|
423
|
+
return typeof value === "string" && value ? value : undefined;
|
|
420
424
|
}
|
|
421
425
|
|
|
422
426
|
function getFeishuEvent(data: unknown): Record<string, any> | undefined {
|