@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
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ The project follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.24] - 2026-10-24
|
|
10
|
+
|
|
11
|
+
### 中文
|
|
12
|
+
|
|
13
|
+
- 修复 Turn Preview 只有思考标题、没有正文时仍显示空折叠箭头的问题;后续收到正文才启用展开,保留实时局部更新及飞书思考卡片行为。
|
|
14
|
+
- 修复飞书 SDK 展平事件 ID 后,新建目录按钮因固定名称被误去重而失效的问题;创建成功后原文件浏览卡片进入新目录,不改变任务工作目录。
|
|
15
|
+
- 允许无轮次、分支历史或 Goal 的空会话切换 Provider:验证目标设置后更换底层线程,保留本地任务和群绑定,失败保留原会话。
|
|
16
|
+
- Codex 遇到特定的 Responses 续接状态失效时,在原任务中自动尝试一次空输入续接,保留历史和已完成的工具结果,不重发 Prompt/图片;尊重取消、Reset、设置变化和任务接管。
|
|
17
|
+
- 修复思考卡片及 Preview 重复显示同一运行警告与最终错误,实时更新移除重复项;提取结构化错误的可读信息并保留原始诊断记录。
|
|
18
|
+
|
|
19
|
+
### English
|
|
20
|
+
|
|
21
|
+
- Render title-only reasoning in Turn Preview without an empty disclosure or detail request; enable expansion when a body arrives, preserving live incremental updates and Feishu thinking cards.
|
|
22
|
+
- Fix folder creation being silently deduplicated by the button's fixed name after the Feishu SDK flattens event IDs; open the created directory in the same browser card without changing the task's working directory.
|
|
23
|
+
- Allow verified empty tasks without Turns, inherited history, or a Goal to switch Providers through a new remote thread, preserving local task/chat bindings and retaining the original on failure.
|
|
24
|
+
- Recover a specific Codex Responses continuation-state rejection once with empty input on the same task, preserving history and completed tool outputs without replaying the Prompt/images; honor cancellation, Reset, settings changes, and newer work.
|
|
25
|
+
- Deduplicate matching runtime warnings and final errors in thinking cards and Preview, including live updates; display readable structured error details while retaining original diagnostic records.
|
|
26
|
+
|
|
9
27
|
## [0.1.24-alpha.8] - 2026-10-19
|
|
10
28
|
|
|
11
29
|
### 中文
|
package/README.md
CHANGED
|
@@ -292,6 +292,8 @@ Only groups explicitly enabled with `/mute` or `/mute on` supplement ordinary @-
|
|
|
292
292
|
|
|
293
293
|
`/new` and `/newgroup` use the conversation's default Agent unless `--agent <name>` selects another configured Agent for this task only; the source conversation's default is unchanged. For example, `/new Fix tests --agent codex` or `/newgroup Review --agent traex --dir ~/dev/project`. They inherit the current project; execution settings are inherited only when the selected Agent matches the current task's Agent, otherwise Provider, model, reasoning, and permission mode use the target Agent's saved defaults. Use `--dir` to choose another directory or `--nodir` to start without a project directory; `--nodir` requires the selected Agent to be App Server, and `~` represents your home directory.
|
|
294
294
|
|
|
295
|
+
`/dir` opens the file browser. `NewFolder` creates a child directory and opens it in the same card at page one, without changing the task's working directory. `Back` cancels creation and restores the previous directory and page.
|
|
296
|
+
|
|
295
297
|
`/file` accepts relative paths, absolute paths, and paths beginning with `~`; relative paths resolve from the current task directory.
|
|
296
298
|
|
|
297
299
|
`/fork` and `/forkgroup` branch from completed work without interrupting a running turn. `/sessions` manages tasks across projects and Providers in pages of up to 10 tasks; switching Provider does not hide a task from the list or search. Use each project menu for `New` and `NewGroup`. Each task’s `SwitchGroup` action creates a new group bound to that same task, preserving its history, title, and execution settings without creating or forking an Agent task. The original conversation stays unchanged. An Agent Bot Turn already in progress keeps replying to its original conversation; new messages from the new group queue until it finishes. Tasks running in an external client cannot be switched until they finish. Expanding a task directly shows the first 50 characters of its latest user Prompt, its update time, and task-specific actions. Running tasks also show a direct `Stop` button, including the current task and tasks running in other conversations or external clients. It sends the normal Agent interrupt without switching tasks or killing processes. The list refresh preserves search and pagination and keeps Stop available until the Agent reports the task idle; Archive is available only when idle. Use `Turns` to view that task’s history without switching tasks, including tasks not yet bound to Agent Bot; non-current tasks are read-only until you switch to them. `/turns` restores conversation context without reverting local files.
|
|
@@ -400,17 +402,19 @@ For App Server tasks, permission changes apply both approval and sandbox policie
|
|
|
400
402
|
|
|
401
403
|
The execution settings card ends with a one-sentence Markdown blockquote for the selected tab's scope and timing. Agent selection changes only the default for new tasks in that conversation, without migrating the current task. Model, reasoning, and permission changes apply from the next turn. Agent and Provider options show their identifiers without duplicate display names, while retaining current/default indicators. `/provider` opens this card even when only one Provider is available, keeping the Model, Thinking, and Permission tabs accessible.
|
|
402
404
|
|
|
403
|
-
Provider changes requested during an Agent Bot turn are saved as pending without interrupting it. The settings card immediately shows the target Provider and the model that will be used; the Model tab lists that Provider’s models, and later selections replace the pending choice. Thinking and permission changes made while a Provider switch is pending are staged with it. The switch is applied after the current turn ends, before a queued, retried, or new turn starts; idle tasks switch immediately. Pending settings survive restarts and do not overwrite active settings or Agent defaults until applied. If a deferred switch fails, Agent Bot restores the previous Provider and model. After verified rollback, the card returns to the original settings with an error notice, clears the pending choice, and queued/new turns continue using the original settings. Only failed or unverified rollback pauses further turns and keeps queued prompts for manual recovery. For a custom Provider, Agent Bot uses its OpenAI-compatible `/models` endpoint when available and shows only that Provider's models. A switch keeps the previous model when the target supports it, otherwise selects the target's default model or its first returned model. Providers without a model-list endpoint remain usable: Agent Bot keeps the current or configured model as the single fallback candidate and lets Codex apply it. Agent Bot verifies the task ID, Provider, and model before saving or reporting success.
|
|
405
|
+
Provider changes requested during an Agent Bot turn are saved as pending without interrupting it. The settings card immediately shows the target Provider and the model that will be used; the Model tab lists that Provider’s models, and later selections replace the pending choice. Thinking and permission changes made while a Provider switch is pending are staged with it. The switch is applied after the current turn ends, before a queued, retried, or new turn starts; idle tasks switch immediately. Pending settings survive restarts and do not overwrite active settings or Agent defaults until applied. If a deferred switch fails, Agent Bot restores the previous Provider and model. After verified rollback, the card returns to the original settings with an error notice, clears the pending choice, and queued/new turns continue using the original settings. Only failed or unverified rollback pauses further turns and keeps queued prompts for manual recovery. For a custom Provider, Agent Bot uses its OpenAI-compatible `/models` endpoint when available and shows only that Provider's models. A switch keeps the previous model when the target supports it, otherwise selects the target's default model or its first returned model. Providers without a model-list endpoint remain usable: Agent Bot keeps the current or configured model as the single fallback candidate and lets Codex apply it. Agent Bot verifies the task ID, Provider, and model before saving or reporting success. Tasks with history always preserve their remote thread ID and all history, including failed and interrupted Turns. Before the first message, a verified empty task without inherited history or a Goal may switch through a new remote thread, keeping the same local task, conversation, directory, and title. The old thread remains attached until the new Provider/model and persistence succeed; failure keeps the original task. No synthetic Prompt or history rewrite is used. If a switch fails, the previous settings are retained and remote recovery is attempted. If recovery also fails, further turns are blocked until a Provider switch succeeds.
|
|
404
406
|
|
|
405
407
|
For TraeX's built-in `trae` Provider, Agent Bot uses the complete native `model/list` catalog, including model names and reasoning options, without requiring a custom Provider `base_url`. Other custom Providers keep their own model discovery and fallback behavior.
|
|
406
408
|
|
|
407
|
-
If Provider model discovery fails, settings cards retain the fallback model but show the failure and warn that service availability is unconfirmed. A successful settings change verifies configuration, not a live inference request. App Server runtime errors and native retry notices show the message and any `additionalDetails` in the existing progress card and Preview, including upstream error codes and retry delays when supplied. Error text is displayed literally, duplicate details are omitted, and long reasons remain available in Preview and activity history. Missing details are not guessed
|
|
409
|
+
If Provider model discovery fails, settings cards retain the fallback model but show the failure and warn that service availability is unconfirmed. A successful settings change verifies configuration, not a live inference request. App Server runtime errors and native retry notices show the message and any `additionalDetails` in the existing progress card and Preview, including upstream error codes and retry delays when supplied. Error text is displayed literally, duplicate details are omitted, and long reasons remain available in Preview and activity history. Missing details are not guessed. Native retries do not create another card or end the turn; the final turn notification still determines success, failure, or cancellation. If the final failure repeats the preceding runtime warning, cards and Preview display it only once; the original execution record is retained.
|
|
410
|
+
|
|
411
|
+
For Codex's specific `unsupported_persisted_item_context` continuation failure, Agent Bot attempts one empty-input continuation in the same task, preserving history and completed tool results without resending your Prompt or images. This creates a new Turn and progress card, not a new task or Fork. If recovery fails, the error remains visible and you can send a message to continue. See the [technical reference](docs/technical-reference.md#responses-continuation-recovery) for recovery limits.
|
|
408
412
|
|
|
409
413
|
Some Responses-compatible Providers omit assistant message phases. Their text stays in the progress timeline instead of being concatenated into a premature final answer. On successful completion, only the last unphased message not followed by a tool start becomes the final answer; explicit `commentary` and `final_answer` phases take precedence. Cards and Preview share this classification. Previously saved snapshots are not automatically rewritten.
|
|
410
414
|
|
|
411
415
|
`feishu.groupNameFormat` defines separate name templates for new Project and Projectless groups, with variables for the operating system, Agent, project, task name, and date. See the [technical reference](docs/technical-reference.md#configuration-model) for the complete format.
|
|
412
416
|
|
|
413
|
-
If App Server ignores the requested Provider/model, Agent Bot reports the requested and actual settings and verifies restoration on the same thread. It never forks to bypass an upstream limitation. Provider switching releases and resumes only the selected task, preserving
|
|
417
|
+
If App Server ignores the requested Provider/model, Agent Bot reports the requested and actual settings and verifies restoration on the same thread. It never forks to bypass an upstream limitation. Provider switching releases and resumes only the selected task, preserving existing history while other tasks keep running; only verified empty tasks may receive a new remote thread ID. It does not restart the shared App Server. If the selected task cannot unload, Agent Bot reports the failure and attempts to restore its previous settings. Settings pages can load saved task metadata without first resuming a broken old Provider; ordinary resume/reconnect verifies thread identity and known Provider before allowing a new Turn. Permanent authentication failures are not automatically retried by Agent Bot.
|
|
414
418
|
|
|
415
419
|
Agent processes inherit ordinary parent-process variables and their explicit `agents.<name>.env` settings. Before starting an Agent, Agent Bot removes inherited `FEISHU_*` credentials and internal `AGENT_BOT_*` state, then provides only namespaced, non-secret Profile and Lark identity context. `FEISHU_APP_SECRET` is never forwarded to an Agent process.
|
|
416
420
|
|
|
@@ -424,7 +428,7 @@ Thinking cards use the grouped layout by default: auxiliary Commentary and user
|
|
|
424
428
|
|
|
425
429
|
Whitespace-only Commentary does not use a pinned message slot. Adjacent omission markers generated by the card are shown once, without removing literal message text or changing saved history.
|
|
426
430
|
|
|
427
|
-
Turn Preview shows
|
|
431
|
+
Turn Preview shows saved reasoning with a body in a collapsed panel. A leading Markdown heading or standalone bold title appears directly in the header; expanding shows the remaining summary and content without extra Summary/Content labels or a repeated title. Title-only items remain visible as non-interactive rows without a disclosure arrow or detail request; live updates add a disclosure if a body arrives later. Items without an explicit title keep the Reasoning fallback and all original text. Collapsed panels load their full Markdown on expansion and retain expansion during live updates. Reasoning content and authoritative completed items are stored separately from card activities; this does not change thinking-card titles, grouping, or final replies. Existing saved summaries remain viewable, but previously unrecorded content is not backfilled from Agent history.
|
|
428
432
|
|
|
429
433
|
Quoted cards and cards inside merged forwards include their available original text and images in the Agent's context.
|
|
430
434
|
|
package/README.zh.md
CHANGED
|
@@ -293,6 +293,8 @@ Fork 只记录来源任务和分支轮次,不同步完整历史。普通任务
|
|
|
293
293
|
|
|
294
294
|
`/new` 和 `/newgroup` 默认使用当前会话设置的默认 Agent,也可用 `--agent <名称>` 为本次任务指定已配置的 Agent,不改变源会话的默认值。例如:`/new 修复测试 --agent codex`、`/newgroup 代码审查 --agent traex --dir ~/dev/project`。项目目录仍继承当前任务;选中的 Agent 与当前任务相同时继承运行设置,不同时使用目标 Agent 已保存的 Provider、模型、思考强度和权限默认值。使用 `--dir` 指定其他目录,或使用 `--nodir` 创建无项目目录的任务;`--nodir` 要求选中的 Agent 为 App Server,`~` 表示用户主目录。
|
|
295
295
|
|
|
296
|
+
`/dir` 打开文件浏览卡片。`NewFolder` 创建子目录后,原卡片直接进入新目录的第一页,不改变任务工作目录;`Back` 取消创建并回到原目录和页码。
|
|
297
|
+
|
|
296
298
|
`/file` 支持相对路径、绝对路径和以 `~` 开头的用户目录路径;相对路径以当前任务目录为基准。
|
|
297
299
|
|
|
298
300
|
`/fork` 和 `/forkgroup` 会从已完成的工作创建分支,不会中断正在执行的轮次。`/sessions` 用于跨项目、跨 Provider 管理任务,每页最多显示 10 个任务;切换 Provider 不会让任务从列表或搜索结果中消失。项目菜单提供 `New` 和 `NewGroup`。每个任务的 `SwitchGroup` 会创建新群并绑定同一个任务,保留历史、标题和运行设置,不创建新 Agent 任务,也不 fork,原会话绑定不变。若任务正在 Agent Bot 中执行,本轮回复仍留在原会话,新群发来的消息先排队;正在外部客户端执行的任务需要等待完成后再切换。展开任务后会直接显示最后一个用户 Prompt 的前 50 个字符、更新时间以及任务级操作。运行中任务还会显示独立的 `Stop` 按钮,包括当前任务、其他会话和外部客户端中的任务;点击发送正常的 Agent 中断信号,不切换任务、不结束进程。列表刷新保留搜索条件和页码,并根据 Agent 的实际状态保留或移除 Stop;空闲后才提供 Archive。点击 `Turns` 可查看对应任务的轮次,包括尚未绑定到 Agent Bot 的任务,不会切换当前任务;非当前任务仅供查看,需要先切换任务才能 Reset。`/turns` 用于恢复对话上下文,不会回退本地文件。
|
|
@@ -345,7 +347,7 @@ Agent Bot 将用户相关文件保存在仓库之外:
|
|
|
345
347
|
|
|
346
348
|
`/status` 状态卡片底部也提供 **Preview**,与 Refresh、Stop/Switch 放在同一行。点击可查看正在执行的轮次,空闲时则查看最近一轮;刷新卡片后会更新目标。对于尚未被 Agent Bot 记录的外部任务,打开 Preview 时会从 Agent 按需读取所选轮次并缓存完整执行详情;读取失败可刷新页面重试。没有轮次、无法读取历史且无本地记录,或文件预览服务不可用时不显示按钮。查看其他任务的状态不会切换或接管任务。
|
|
347
349
|
|
|
348
|
-
思考卡片底部操作区提供 `Preview`
|
|
350
|
+
思考卡片底部操作区提供 `Preview` 按钮,可在浏览器中查看本轮已保存的执行时间线,采用紧凑布局并适配手机,思考块直接以内容标题作为折叠标题,展开后显示内容。只有标题而没有正文时保留普通标题行,不显示折叠箭头、不请求详情;实时更新收到正文后才变为可展开项。工具块只有一级折叠,默认只显示有意义的单行命令摘要、简短耗时(如 `58s` 或 `2:32`)和完成状态。展开后先显示以 `$` 开头的完整命令,随后直接显示结果、图片和修改文件。相同的输出和错误日志只显示一次。执行中的 Turn 会通过局部更新实时显示变化,保留手动展开状态、滚动位置和未变化的图片节点,避免图片反复加载;Token 等元数据更新不会替换时间线。折叠工具块和文件变更汇总首屏只加载摘要,展开时才请求命令、日志、图片和文件列表;再次展开复用已加载内容,运行中只刷新已展开且内容变化的详情,加载失败可点击重试。已结束的 Turn 不再建立 SSE 实时连接。“正在等待 Agent 返回进度”只在尚无可显示进度时出现,已有回答、其他可见进展或 Turn 已结束时不再显示。点击文件变更列表或工具文件列表中的文件,可在新标签页预览其当前内容;已删除或不可用的文件保留为路径文字。Turn Preview 与本地文件链接共用相同的签名、访问地址和网络范围。“最终回答”旁提供下载、复制两个图标,导出原始 Markdown(保留格式、代码块和原始链接),而非渲染后的网页内容。下载为 UTF-8 `.md` 文件;复制支持 HTTP/局域网页面的回退方式,并显示成功或失败提示。已结束页面及实时到达的最终回答均可使用。
|
|
349
351
|
|
|
350
352
|
Turn Preview 完整保留已记录的执行内容,不再反复把整轮内容写入数据库。页面按增量实时更新,折叠工具展开时加载完整详情。备份时请保留整个 Profile 数据目录,以同时保存执行记录和任务信息;已有历史仍可查看。
|
|
351
353
|
|
|
@@ -391,17 +393,19 @@ App Server 任务切换权限时,下一轮会同时应用审批与沙箱策略
|
|
|
391
393
|
|
|
392
394
|
运行设置卡片末尾以 Markdown 引用块显示当前选项页的一句重点提示,标明作用范围和生效时机。Agent 只改变本聊天或话题中新任务的默认 Agent,不迁移当前任务;模型、思考强度和权限从下一轮生效。Agent 和 Provider 选项只显示标识,去掉重复的展示名称,保留当前与默认状态标记。只有一个 Provider 时,`/provider` 仍会打开运行设置卡片,可以继续切换到 Model、Thinking 和 Permission 页。
|
|
393
395
|
|
|
394
|
-
任务正在 Agent Bot 中执行时,切换 Provider 会保存为待生效设置,不中断本轮。运行设置卡片立即显示目标 Provider 和将使用的模型,Model 页切换为该 Provider 的模型列表,可继续选择,以最后选择为准;此时修改思考强度和权限也一起暂存。本轮结束后、排队消息、重试或新轮次开始前实际应用,空闲任务则立即切换。待生效设置重启后保留,应用前不改动实际运行设置或 Agent 默认值。如果延迟切换失败,自动回退之前的 Provider 和模型;确认回退成功后,卡片恢复显示原设置并提示失败原因,清除待生效选择,排队消息和后续轮次继续使用原设置。只有回退也失败或无法确认时才暂停后续轮次、保留排队消息,等待重新选择恢复。对于自定义 Provider,Agent Bot 会在接口可用时读取其 OpenAI 兼容 `/models` 接口,并只展示该 Provider 返回的模型。切换时优先保留目标 Provider 也支持的原模型;否则选择目标 Provider 的默认模型,没有默认模型时选择返回列表中的第一个模型。未提供模型列表接口的 Provider 仍可使用:Agent Bot 会把当前或已配置模型作为唯一候选交给 Codex 应用。Agent Bot 校验任务 ID、Provider
|
|
396
|
+
任务正在 Agent Bot 中执行时,切换 Provider 会保存为待生效设置,不中断本轮。运行设置卡片立即显示目标 Provider 和将使用的模型,Model 页切换为该 Provider 的模型列表,可继续选择,以最后选择为准;此时修改思考强度和权限也一起暂存。本轮结束后、排队消息、重试或新轮次开始前实际应用,空闲任务则立即切换。待生效设置重启后保留,应用前不改动实际运行设置或 Agent 默认值。如果延迟切换失败,自动回退之前的 Provider 和模型;确认回退成功后,卡片恢复显示原设置并提示失败原因,清除待生效选择,排队消息和后续轮次继续使用原设置。只有回退也失败或无法确认时才暂停后续轮次、保留排队消息,等待重新选择恢复。对于自定义 Provider,Agent Bot 会在接口可用时读取其 OpenAI 兼容 `/models` 接口,并只展示该 Provider 返回的模型。切换时优先保留目标 Provider 也支持的原模型;否则选择目标 Provider 的默认模型,没有默认模型时选择返回列表中的第一个模型。未提供模型列表接口的 Provider 仍可使用:Agent Bot 会把当前或已配置模型作为唯一候选交给 Codex 应用。Agent Bot 校验任务 ID、Provider 和模型后才保存并提示成功。已有历史的任务切换时保留远端线程 ID 和全部历史,包括失败、中断的轮次。首次发消息前,确认没有历史、分支来源或 Goal 的空会话可通过新建底层线程切换 Provider,本地任务、群聊/话题绑定、目录和标题保持不变;新 Provider/模型及持久化成功前保留原线程,失败仍使用原会话,不发送虚构 Prompt 或改写历史。切换失败时保留原设置并尝试恢复远端;如果恢复也失败,将阻止后续执行,直到重新切换 Provider 成功。
|
|
395
397
|
|
|
396
398
|
`feishu.groupNameFormat` 可分别设置 Project 与 Projectless 新群的名称模板,支持系统、Agent、项目、任务名和日期变量。完整格式说明见[技术参考](docs/technical-reference.zh.md#配置模型)。
|
|
397
399
|
|
|
398
400
|
TraeX 内置的 `trae` Provider 直接使用原生 `model/list` 返回的完整模型列表,保留模型名称和思考强度选项,不要求配置自定义 Provider 的 `base_url`。其他自定义 Provider 的模型发现和降级行为保持不变。
|
|
399
401
|
|
|
400
|
-
Provider 模型列表读取失败时,设置卡片保留降级候选模型,同时显示失败原因并提示服务可用性未确认。切换成功只代表配置已通过校验,不代表实际模型请求可用。App Server 执行错误和原生重试通知会在同一张思考卡片和 Preview 中显示消息及 `additionalDetails`,包括 Agent 提供的上游错误码和重试等待时间。错误按纯文本展示,重复详情不重复显示,长原因可在 Preview
|
|
402
|
+
Provider 模型列表读取失败时,设置卡片保留降级候选模型,同时显示失败原因并提示服务可用性未确认。切换成功只代表配置已通过校验,不代表实际模型请求可用。App Server 执行错误和原生重试通知会在同一张思考卡片和 Preview 中显示消息及 `additionalDetails`,包括 Agent 提供的上游错误码和重试等待时间。错误按纯文本展示,重复详情不重复显示,长原因可在 Preview 和思考活动历史中查看。缺少详情时不猜测原因。原生重试不会新建卡片或提前结束任务,最终完成通知仍决定成功、失败或取消状态。最终错误与此前的运行警告相同时,卡片和 Preview 只显示一次,原始执行记录仍然保留。
|
|
403
|
+
|
|
404
|
+
Codex 出现特定的 `unsupported_persisted_item_context` 续接失败时,Agent Bot 会在同一任务中自动尝试一次空输入续接,保留历史和已完成的工具结果,不重发用户的 Prompt 或图片。它会新增一个轮次及思考卡片,不会新建任务或 Fork。恢复失败时仍显示错误,可以发送消息继续。恢复边界见[技术参考](docs/technical-reference.zh.md#responses-续接恢复)。
|
|
401
405
|
|
|
402
406
|
部分 Responses 兼容 Provider 不返回助手消息阶段。这类文本先按顺序显示在进度时间线,不再提前拼接成最终回答;轮次成功结束时,只有最后一条且之后未开始工具调用的未标记消息会成为最终回答。明确的 `commentary` 和 `final_answer` 标记优先。思考卡片和 Preview 使用同一分类结果,不自动改写此前保存的快照。
|
|
403
407
|
|
|
404
|
-
如果 App Server 忽略指定的 Provider 或模型,Agent Bot 会报告请求值和实际值,并在同一线程上验证恢复原设置,绝不通过 Fork 绕过上游限制。切换 Provider
|
|
408
|
+
如果 App Server 忽略指定的 Provider 或模型,Agent Bot 会报告请求值和实际值,并在同一线程上验证恢复原设置,绝不通过 Fork 绕过上游限制。切换 Provider 只处理当前任务并保留已有历史,其他任务可以继续执行;只有确认无历史的空会话允许更换底层线程 ID;不会因此重启共享 App Server。若目标任务无法卸载,会提示失败并尝试恢复原设置。设置入口可直接加载保存的任务元数据,无需先用故障的旧 Provider 恢复任务;普通恢复和断线重连会验证线程 ID 与已知 Provider,再允许发起新轮次。明确的认证失败不再触发 Agent Bot 自动重试。
|
|
405
409
|
|
|
406
410
|
Agent 进程会继承普通父进程变量及其显式配置的 `agents.<name>.env`。启动 Agent 前,Agent Bot 会移除继承的 `FEISHU_*` 凭据和内部 `AGENT_BOT_*` 状态,再仅提供带命名空间的非敏感 Profile 与 Lark 身份上下文;`FEISHU_APP_SECRET` 永远不会传入 Agent 进程。
|
|
407
411
|
|
|
@@ -2,6 +2,7 @@ import { statSync } from "node:fs";
|
|
|
2
2
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3
3
|
import { appendGeneratedImageMarkdown } from "../utils/generatedImageMarkdown.js";
|
|
4
4
|
import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
5
|
+
import { isResponseContinuationError } from "../runtime/responseErrors.js";
|
|
5
6
|
import { AppServerRequestError } from "./AppServerConnection.js";
|
|
6
7
|
import { workspaceSandboxFromConfig, workspaceSandboxFromResponse } from "./AppServerPermissions.js";
|
|
7
8
|
import { formatCodexError, mapCodexNotification } from "./CodexEventMapper.js";
|
|
@@ -170,15 +171,49 @@ export class CodexRuntime {
|
|
|
170
171
|
return session;
|
|
171
172
|
}
|
|
172
173
|
async startTurn(sessionId, prompt) {
|
|
173
|
-
return this.runSessionOperation(sessionId, () =>
|
|
174
|
+
return this.runSessionOperation(sessionId, () => {
|
|
175
|
+
this.requireSession(sessionId).responseFailureTurnId = undefined;
|
|
176
|
+
return this.startTurnNow(sessionId, prompt);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
get recoverResponseContinuation() {
|
|
180
|
+
return this.provider.getAgentFamily?.() === "codex"
|
|
181
|
+
? (sessionId, failedTurnId, signal) => this.recoverResponseContinuationNow(sessionId, failedTurnId, signal)
|
|
182
|
+
: undefined;
|
|
174
183
|
}
|
|
175
|
-
async
|
|
184
|
+
async recoverResponseContinuationNow(sessionId, failedTurnId, signal) {
|
|
185
|
+
return this.runSessionOperation(sessionId, async () => {
|
|
186
|
+
signal?.throwIfAborted();
|
|
187
|
+
const session = this.requireSession(sessionId);
|
|
188
|
+
if (session.responseFailureTurnId !== failedTurnId || session.activeTurnId || session.needsResume) {
|
|
189
|
+
throw new Error("模型续接恢复已失效,请发送消息继续。");
|
|
190
|
+
}
|
|
191
|
+
// Consume before starting: a timeout must not create a second continuation.
|
|
192
|
+
session.responseFailureTurnId = undefined;
|
|
193
|
+
const client = await this.client();
|
|
194
|
+
const metadata = await client.request("thread/read", {
|
|
195
|
+
threadId: session.remoteSessionId, includeTurns: false,
|
|
196
|
+
}, SYNC_REQUEST_TIMEOUT_MS);
|
|
197
|
+
const latest = await this.readLatestThreadTurns(client, session.remoteSessionId, "summary");
|
|
198
|
+
signal?.throwIfAborted();
|
|
199
|
+
if (session.activeTurnId || session.needsResume || metadata.thread.id !== session.remoteSessionId || metadata.thread.status?.type === "active"
|
|
200
|
+
|| latest.at(-1)?.id !== failedTurnId || latest.at(-1)?.status !== "failed") {
|
|
201
|
+
throw new Error("任务已发生变化,未自动恢复模型续接。请检查当前任务后继续。");
|
|
202
|
+
}
|
|
203
|
+
const turnId = await this.startTurnNow(sessionId, "", true);
|
|
204
|
+
if (signal?.aborted && session.activeTurnId === turnId)
|
|
205
|
+
await this.cancelTurn(sessionId, turnId);
|
|
206
|
+
signal?.throwIfAborted();
|
|
207
|
+
return turnId;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async startTurnNow(sessionId, prompt, responseContinuation = false) {
|
|
176
211
|
const session = this.requireSession(sessionId);
|
|
177
212
|
const client = await this.client();
|
|
178
213
|
await this.ensureSessionResumed(session, client);
|
|
179
214
|
const start = async () => client.request("turn/start", {
|
|
180
215
|
threadId: session.remoteSessionId,
|
|
181
|
-
input: codexUserInput(prompt),
|
|
216
|
+
input: responseContinuation ? [] : codexUserInput(prompt),
|
|
182
217
|
cwd: session.cwd,
|
|
183
218
|
model: session.model,
|
|
184
219
|
effort: session.reasoningEffort,
|
|
@@ -197,7 +232,7 @@ export class CodexRuntime {
|
|
|
197
232
|
response = await start();
|
|
198
233
|
}
|
|
199
234
|
catch (error) {
|
|
200
|
-
if (!isInvalidWorkingDirectoryError(error))
|
|
235
|
+
if (responseContinuation || !isInvalidWorkingDirectoryError(error))
|
|
201
236
|
throw error;
|
|
202
237
|
this.logger.warn({ error, sessionId: session.localSessionId, cwd: session.cwd }, "App Server rejected the task working directory; refreshing the current thread before retrying.");
|
|
203
238
|
await this.resumeAppServerSession(session, client);
|
|
@@ -617,17 +652,30 @@ export class CodexRuntime {
|
|
|
617
652
|
return response.cleared;
|
|
618
653
|
}
|
|
619
654
|
async setModel(sessionId, model) {
|
|
620
|
-
await this.runSessionOperation(sessionId, async () => {
|
|
655
|
+
await this.runSessionOperation(sessionId, async () => {
|
|
656
|
+
const session = this.requireSession(sessionId);
|
|
657
|
+
session.model = model;
|
|
658
|
+
session.responseFailureTurnId = undefined;
|
|
659
|
+
});
|
|
621
660
|
}
|
|
622
661
|
async setReasoningEffort(sessionId, effort) {
|
|
623
|
-
await this.runSessionOperation(sessionId, async () => {
|
|
662
|
+
await this.runSessionOperation(sessionId, async () => {
|
|
663
|
+
const session = this.requireSession(sessionId);
|
|
664
|
+
session.reasoningEffort = effort;
|
|
665
|
+
session.responseFailureTurnId = undefined;
|
|
666
|
+
});
|
|
624
667
|
}
|
|
625
668
|
async setPermissionMode(sessionId, mode) {
|
|
626
|
-
await this.runSessionOperation(sessionId, async () => {
|
|
669
|
+
await this.runSessionOperation(sessionId, async () => {
|
|
670
|
+
const session = this.requireSession(sessionId);
|
|
671
|
+
session.permissionMode = mode;
|
|
672
|
+
session.responseFailureTurnId = undefined;
|
|
673
|
+
});
|
|
627
674
|
}
|
|
628
675
|
async setExecutionSettings(sessionId, settings, persist) {
|
|
629
676
|
return this.runSessionOperation(sessionId, async () => {
|
|
630
677
|
try {
|
|
678
|
+
this.requireSession(sessionId).responseFailureTurnId = undefined;
|
|
631
679
|
return await this.setExecutionSettingsNow(sessionId, settings, persist);
|
|
632
680
|
}
|
|
633
681
|
catch (error) {
|
|
@@ -647,22 +695,14 @@ export class CodexRuntime {
|
|
|
647
695
|
const metadata = await client.request("thread/read", {
|
|
648
696
|
threadId: session.remoteSessionId, includeTurns: false,
|
|
649
697
|
}, SYNC_REQUEST_TIMEOUT_MS);
|
|
698
|
+
if (metadata.thread.id !== session.remoteSessionId)
|
|
699
|
+
throw new Error("App Server 返回了不同的任务 ID,未切换 Provider。");
|
|
650
700
|
const localActive = await this.localActivityDetector?.activeThreads([session.remoteSessionId]);
|
|
651
701
|
if (metadata.thread.status?.type === "active" || localActive?.has(session.remoteSessionId) || session.activeTurnId) {
|
|
652
702
|
throw new Error(PROVIDER_SWITCH_BUSY);
|
|
653
703
|
}
|
|
654
|
-
if (
|
|
655
|
-
|
|
656
|
-
const turns = await this.readLatestThreadTurns(client, session.remoteSessionId, "summary");
|
|
657
|
-
if (turns.length === 0)
|
|
658
|
-
throw new Error(PROVIDER_SWITCH_EMPTY);
|
|
659
|
-
}
|
|
660
|
-
catch (error) {
|
|
661
|
-
if (!(error instanceof AppServerRequestError) || error.method !== "thread/turns/list"
|
|
662
|
-
|| !/missing source rollout|no rollout found/iu.test(error.serverMessage))
|
|
663
|
-
throw error;
|
|
664
|
-
throw new Error(PROVIDER_SWITCH_EMPTY, { cause: error });
|
|
665
|
-
}
|
|
704
|
+
if (await this.isEmptySettingsSession(client, session, metadata.thread)) {
|
|
705
|
+
return this.replaceEmptySettingsSession(client, session, settings, persist);
|
|
666
706
|
}
|
|
667
707
|
const previous = { ...session };
|
|
668
708
|
const restoreProvider = previous.modelProvider?.trim()
|
|
@@ -726,6 +766,78 @@ export class CodexRuntime {
|
|
|
726
766
|
}
|
|
727
767
|
return session;
|
|
728
768
|
}
|
|
769
|
+
async isEmptySettingsSession(client, session, thread) {
|
|
770
|
+
if (thread.id !== session.remoteSessionId || thread.preview !== "" || thread.forkedFromId
|
|
771
|
+
|| (session.turnCount === undefined && thread.forkedFromId !== null)
|
|
772
|
+
|| session.activeTurnId || (session.turnCount ?? 0) > 0 || session.knownTurnIds.size > 0
|
|
773
|
+
|| session.terminalTurnIds.size > 0 || (thread.turns?.length ?? 0) > 0)
|
|
774
|
+
return false;
|
|
775
|
+
try {
|
|
776
|
+
if ((await this.readLatestThreadTurns(client, session.remoteSessionId, "summary")).length > 0)
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
catch (error) {
|
|
780
|
+
if (!(error instanceof AppServerRequestError) || error.method !== "thread/turns/list"
|
|
781
|
+
|| !/missing source rollout|no rollout found|not materialized yet/iu.test(error.serverMessage))
|
|
782
|
+
throw error;
|
|
783
|
+
}
|
|
784
|
+
if (!["idle", "notLoaded"].includes(thread.status?.type ?? ""))
|
|
785
|
+
throw new Error(PROVIDER_SWITCH_EMPTY);
|
|
786
|
+
const { goal } = await client.request("thread/goal/get", {
|
|
787
|
+
threadId: session.remoteSessionId,
|
|
788
|
+
}, CONTROL_REQUEST_TIMEOUT_MS);
|
|
789
|
+
if (goal !== null)
|
|
790
|
+
throw new Error(PROVIDER_SWITCH_EMPTY);
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
async replaceEmptySettingsSession(client, session, settings, persist) {
|
|
794
|
+
const oldId = session.remoteSessionId;
|
|
795
|
+
let replacementId;
|
|
796
|
+
try {
|
|
797
|
+
// Keep the original attachment until target verification and persistence both succeed.
|
|
798
|
+
const response = await client.request("thread/start", {
|
|
799
|
+
cwd: session.cwd, modelProvider: settings.modelProvider, model: settings.model, threadSource: "user",
|
|
800
|
+
...threadLifecycleParams(session.cwd), ...permissionParams(settings.permissionMode),
|
|
801
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
802
|
+
if (!response.thread.id || response.thread.id === oldId)
|
|
803
|
+
throw new Error("App Server 未创建独立的空任务,未保存设置。");
|
|
804
|
+
replacementId = response.thread.id;
|
|
805
|
+
assertProviderSettingsApplied(response, settings);
|
|
806
|
+
if (session.title)
|
|
807
|
+
await client.request("thread/name/set", { threadId: replacementId, name: session.title }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
808
|
+
const { thread } = await client.request("thread/read", { threadId: oldId, includeTurns: false }, SYNC_REQUEST_TIMEOUT_MS);
|
|
809
|
+
const active = await this.localActivityDetector?.activeThreads([oldId]);
|
|
810
|
+
if (active?.has(oldId) || !await this.isEmptySettingsSession(client, session, thread))
|
|
811
|
+
throw new Error(PROVIDER_SWITCH_BUSY);
|
|
812
|
+
if (this.attachedClient !== client)
|
|
813
|
+
throw new Error("App Server 连接已变化,请重试 Provider 切换。");
|
|
814
|
+
const candidate = this.makeSession({
|
|
815
|
+
localSessionId: session.localSessionId, agentName: session.agentName, cwd: session.cwd, title: session.title, ...settings,
|
|
816
|
+
}, response, settings.reasoningEffort);
|
|
817
|
+
candidate.title = session.title;
|
|
818
|
+
await persist?.(candidate, { emptyRemoteSessionId: oldId });
|
|
819
|
+
Object.assign(session, candidate, {
|
|
820
|
+
needsResume: this.attachedClient !== client, settingsRecoveryError: undefined, remoteSummary: undefined,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
if (replacementId)
|
|
825
|
+
await this.releaseUnusedSettingsThread(client, replacementId);
|
|
826
|
+
throw new ProviderSwitchError(`Provider 切换失败:${providerSwitchFailure(error)} 原会话和设置已保留。`, !session.needsResume && !session.settingsRecoveryError, { cause: error });
|
|
827
|
+
}
|
|
828
|
+
if (![...this.sessions.values()].some(other => other.remoteSessionId === oldId)) {
|
|
829
|
+
await this.releaseUnusedSettingsThread(client, oldId);
|
|
830
|
+
}
|
|
831
|
+
return session;
|
|
832
|
+
}
|
|
833
|
+
async releaseUnusedSettingsThread(client, threadId) {
|
|
834
|
+
try {
|
|
835
|
+
await client.request("thread/unsubscribe", { threadId }, CONTROL_REQUEST_TIMEOUT_MS);
|
|
836
|
+
}
|
|
837
|
+
catch (error) {
|
|
838
|
+
this.logger.warn({ error, threadId }, "Could not release an unused empty settings thread.");
|
|
839
|
+
}
|
|
840
|
+
}
|
|
729
841
|
async detachThreadForSettings(client, session, onDetach) {
|
|
730
842
|
const current = await client.request("thread/read", {
|
|
731
843
|
threadId: session.remoteSessionId, includeTurns: false,
|
|
@@ -1236,6 +1348,9 @@ export class CodexRuntime {
|
|
|
1236
1348
|
session.activeTurnStartedAt = undefined;
|
|
1237
1349
|
session.terminalTurnIds.add(mapped.turnId);
|
|
1238
1350
|
session.messagePhases.clear();
|
|
1351
|
+
session.responseFailureTurnId = mapped.status === "failed" && mapped.error
|
|
1352
|
+
&& this.provider.getAgentFamily?.() === "codex" && isResponseContinuationError(mapped.error)
|
|
1353
|
+
? mapped.turnId : undefined;
|
|
1239
1354
|
if (mapped.status === "cancelled") {
|
|
1240
1355
|
this.emit({ type: "turn_cancelled", sessionId, turnId: mapped.turnId });
|
|
1241
1356
|
}
|
|
@@ -1406,6 +1521,7 @@ export class CodexRuntime {
|
|
|
1406
1521
|
session.turnCount += 1;
|
|
1407
1522
|
}
|
|
1408
1523
|
session.activeTurnId = turnId;
|
|
1524
|
+
session.responseFailureTurnId = undefined;
|
|
1409
1525
|
session.activeTurnStartedAt = startedAt;
|
|
1410
1526
|
session.planContinuationTurnId = undefined;
|
|
1411
1527
|
session.modeChangeRequestIds.clear();
|
|
@@ -1440,6 +1556,9 @@ export class CodexRuntime {
|
|
|
1440
1556
|
session.activeTurnStartedAt = undefined;
|
|
1441
1557
|
session.terminalTurnIds.add(turn.id);
|
|
1442
1558
|
session.messagePhases.clear();
|
|
1559
|
+
const failure = formatCodexError(turn.error);
|
|
1560
|
+
session.responseFailureTurnId = turn.status === "failed" && failure
|
|
1561
|
+
&& this.provider.getAgentFamily?.() === "codex" && isResponseContinuationError(failure) ? turn.id : undefined;
|
|
1443
1562
|
if (turn.status === "interrupted") {
|
|
1444
1563
|
this.emit({ type: "turn_cancelled", sessionId: session.localSessionId, turnId: turn.id });
|
|
1445
1564
|
return;
|
|
@@ -1449,7 +1568,7 @@ export class CodexRuntime {
|
|
|
1449
1568
|
type: "turn_failed",
|
|
1450
1569
|
sessionId: session.localSessionId,
|
|
1451
1570
|
turnId: turn.id,
|
|
1452
|
-
message:
|
|
1571
|
+
message: failure ?? "App Server turn failed.",
|
|
1453
1572
|
});
|
|
1454
1573
|
return;
|
|
1455
1574
|
}
|
|
@@ -1499,7 +1618,7 @@ export class CodexRuntime {
|
|
|
1499
1618
|
workspaceSandboxPolicy: workspaceSandboxFromResponse(response.sandbox),
|
|
1500
1619
|
rolloutPath: stringValue(response.thread.path)?.trim() || undefined,
|
|
1501
1620
|
turnCount: "remoteSessionId" in input ? undefined : 0,
|
|
1502
|
-
knownTurnIds: new Set(),
|
|
1621
|
+
knownTurnIds: new Set("lastTurnId" in input && input.lastTurnId ? [input.lastTurnId] : []),
|
|
1503
1622
|
};
|
|
1504
1623
|
}
|
|
1505
1624
|
requireSession(sessionId) {
|
|
@@ -1572,6 +1691,7 @@ export class CodexRuntime {
|
|
|
1572
1691
|
for (const session of this.sessions.values()) {
|
|
1573
1692
|
session.needsResume = true;
|
|
1574
1693
|
session.planContinuationTurnId = undefined;
|
|
1694
|
+
session.responseFailureTurnId = undefined;
|
|
1575
1695
|
const turnId = session.activeTurnId;
|
|
1576
1696
|
if (!turnId)
|
|
1577
1697
|
continue;
|