@keyou007/agent-bot 0.1.24-alpha.3 → 0.1.24-alpha.5

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +22 -4
  3. package/README.zh.md +22 -4
  4. package/dist/acp/AcpSessionManager.js +0 -14
  5. package/dist/acp/AcpSessionManager.js.map +1 -1
  6. package/dist/acp/acpTypes.js.map +1 -1
  7. package/dist/codex/AppServerPermissions.js +34 -0
  8. package/dist/codex/AppServerPermissions.js.map +1 -0
  9. package/dist/codex/CodexLocalActivityDetector.js +37 -5
  10. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  11. package/dist/codex/CodexRuntime.js +37 -13
  12. package/dist/codex/CodexRuntime.js.map +1 -1
  13. package/dist/codex/ThreadWriterProcess.js +19 -5
  14. package/dist/codex/ThreadWriterProcess.js.map +1 -1
  15. package/dist/commands/CommandRouter.js +0 -3
  16. package/dist/commands/CommandRouter.js.map +1 -1
  17. package/dist/feishu/CardRenderer.js +80 -64
  18. package/dist/feishu/CardRenderer.js.map +1 -1
  19. package/dist/local-files/CsvPreview.js +148 -0
  20. package/dist/local-files/CsvPreview.js.map +1 -0
  21. package/dist/local-files/LocalFileViewerServer.js +53 -10
  22. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  23. package/dist/local-files/TurnPreviewPage.js +125 -14
  24. package/dist/local-files/TurnPreviewPage.js.map +1 -1
  25. package/dist/presentation/TurnStateReducer.js +39 -0
  26. package/dist/presentation/TurnStateReducer.js.map +1 -1
  27. package/dist/proxy/ProxySessionController.js +158 -50
  28. package/dist/proxy/ProxySessionController.js.map +1 -1
  29. package/docs/technical-reference.md +5 -3
  30. package/docs/technical-reference.zh.md +4 -2
  31. package/npm-shrinkwrap.json +2 -2
  32. package/package.json +1 -1
  33. package/skills/agent-bot/SKILL.md +10 -0
  34. package/src/acp/AcpSessionManager.ts +0 -17
  35. package/src/acp/acpTypes.ts +0 -1
  36. package/src/codex/AppServerPermissions.ts +39 -0
  37. package/src/codex/CodexLocalActivityDetector.ts +34 -4
  38. package/src/codex/CodexRuntime.ts +44 -17
  39. package/src/codex/ThreadWriterProcess.ts +17 -9
  40. package/src/commands/CommandRouter.ts +0 -3
  41. package/src/feishu/CardRenderer.ts +78 -64
  42. package/src/local-files/CsvPreview.ts +132 -0
  43. package/src/local-files/LocalFileViewerServer.ts +54 -11
  44. package/src/local-files/TurnPreviewPage.ts +128 -15
  45. package/src/presentation/TurnStateReducer.ts +41 -0
  46. package/src/presentation/turnViewTypes.ts +4 -0
  47. package/src/proxy/ProxySessionController.ts +142 -51
  48. package/src/runtime/types.ts +2 -0
@@ -211,7 +211,7 @@ Turn Preview uses the existing highlight.js dependency to highlight explicitly l
211
211
 
212
212
  Input attachments are stored as optional `promptImagePaths` on `TurnViewState` and `localImagePaths` on user activities, separate from bounded display text. Pending-Turn promotion, steering/reconciliation, queued starts, and snapshot recovery retain these paths. Preview resolves them through the existing signed file service with `raw=1`; missing files get a placeholder. The additive JSON fields require no database migration, and historical user activities without paths are not backfilled.
213
213
 
214
- Turn Preview recognizes `mermaid` and `flowchart` fences in Prompt, Commentary, and answer Markdown. The browser lazy-loads the pinned Mermaid distribution from the same-origin `/assets/mermaid.js` endpoint (respecting `fileViewer.publicBaseUrl` prefixes), not a CDN. Strict security mode disables HTML labels and interactive callbacks; frontmatter/config directives and sources over 50,000 characters are rejected, and graphs are capped at 500 edges. Rendered SVG has active elements and external links removed; the existing viewer CSP remains unchanged. Each block defaults to Preview with width fitting, keeps its selected mode, fit/actual-size choice, and unchanged SVG across SSE updates, and shows escaped source when loading or rendering fails. SVG width is capped by both its viewBox width and its container width, preserving aspect ratio and avoiding enlargement. Narrow/touch screens cap the preview at 480 px or 55 svh (55 vh fallback); long diagrams scroll vertically. Actual size restores the viewBox width with block-local scrolling, while Fit resets the block scroll position. CSS sizing adapts to orientation changes without re-rendering Mermaid. Ordinary code blocks and tool output remain unchanged.
214
+ Turn Preview recognizes `mermaid` and `flowchart` fences in Prompt, Commentary, and answer Markdown. Markdown file previews reuse the same renderer, controls, sizing rules, and security policy; full-file Code view and raw/download content remain unchanged. File updates retain matching diagram nodes, modes, sizes, and scroll positions, re-rendering only changed sources. The browser lazy-loads the pinned Mermaid distribution from the same-origin `/assets/mermaid.js` endpoint (respecting `fileViewer.publicBaseUrl` prefixes), not a CDN. Strict security mode disables HTML labels and interactive callbacks; frontmatter/config directives and sources over 50,000 characters are rejected, and graphs are capped at 500 edges. Rendered SVG has active elements and external links removed; the existing viewer CSP remains unchanged. Each block defaults to Preview with width fitting, keeps its selected mode, fit/actual-size choice, and unchanged SVG across SSE updates, and shows escaped source when loading or rendering fails. SVG width is capped by both its viewBox width and its container width, preserving aspect ratio and avoiding enlargement. Narrow/touch screens cap the preview at 480 px or 55 svh (55 vh fallback); long diagrams scroll vertically. Actual size restores the viewBox width with block-local scrolling, while Fit resets the block scroll position. CSS sizing adapts to orientation changes without re-rendering Mermaid. Ordinary code blocks and tool output remain unchanged.
215
215
 
216
216
  HTML text files (`.html` and `.htm`, case-insensitive) have preview/code modes. The preview iframe loads the signed file URL with `render=html`, streams the complete original document in its detected encoding, and carries a file-version query for live updates. Both the iframe and the response CSP use `sandbox allow-scripts` without `allow-same-origin`; the response sandbox also applies when the rendered URL is opened directly. Inline scripts/styles and embedded data/blob media are allowed, but external/local resource loading, network requests, forms, popups, and top-level navigation are not enabled. The outer viewer retains its own restrictive script policy, and raw/download responses are unchanged. Source remains escaped, syntax-highlighted where possible, limited to 2 MiB, and addressable with `#L<n>`.
217
217
 
@@ -321,6 +321,8 @@ Every `agentbot task` operation accepts `--task <task>` and retains the legacy l
321
321
 
322
322
  Provider is stored on each task alongside model, reasoning effort, and permission mode. Each Agent can also keep these values under `agents.<name>.defaults`. New tasks start from the selected Agent's saved defaults, then override them with explicit or same-Agent inherited task settings. Agent Bot passes the resolved settings through `thread/start`, `thread/resume`, and `thread/fork` and persists the effective values returned by the runtime on the task.
323
323
 
324
+ Every `turn/start` also carries both `approvalPolicy` and `sandboxPolicy`: `auto` maps to `never` + `dangerFullAccess`, and `confirm` to `on-request` + `workspaceWrite`. Switching modes takes effect on the next Turn without restarting the shared App Server or resuming/forking the thread; `turn/steer` does not carry these overrides. Workspace policy options come from the thread lifecycle response; if unavailable, one cwd-scoped `config/read` resolves them lazily and the runtime caches them per session. This preserves configured writable roots, network access, and temporary-directory exclusions without reading history. Invalid/unavailable configuration blocks the new Turn; policy rejection is not retried without sandbox restrictions.
325
+
324
326
  `/provider`, `/model`, `/thinking`, and `/permissions` open one Card 2.0 execution-settings surface with the matching tab active. All four commands reject arguments; tab navigation and setting changes use card callbacks only. Provider choices come from App Server `config/read`; for Codex, Agent Bot also keeps the implicit built-in `openai` Provider available when it is absent from the explicit `model_providers` map. The built-in OpenAI Provider uses App Server `model/list`. A configured custom Provider uses `GET <base_url>/models` when available, including its `query_params`, `http_headers`, `env_http_headers`, and configured Bearer credential; credentials remain internal and are never included in cards or model records. A Provider change keeps the previous model when it appears in the target catalog, otherwise selects the marked default, then the first returned model. Matching App Server catalog metadata supplies reasoning options when a custom endpoint returns only model IDs. If model discovery is unavailable, the current or configured model becomes a single fallback candidate so discovery does not block switching; Codex still verifies the effective Provider and model before Agent Bot persists the result. Every successful Provider, model, reasoning, or permission change updates the current task, atomically writes the complete effective setting group to that Agent's defaults in `config.yaml`, refreshes the same card in place, and applies from the next request. The equivalent CLI task-setting commands use the same persistence path.
325
327
 
326
328
  Agent Bot keeps a local routing key for Feishu cards and delivery state, but presents the owning App Server's task ID to users. It does not resume, steer, stop, or fork externally running Agent work without an explicit user action.
@@ -347,11 +349,11 @@ SQLite stores:
347
349
  - Progress snapshots and message bindings
348
350
  - Final-message delivery records
349
351
 
350
- On every Worker startup, Agent Bot scans nonterminal turn attempts before releasing persisted Prompt queues. Only attempts with activity during the preceding five minutes are eligible; runtime events and a one-minute heartbeat refresh the durable activity timestamp while a turn remains active. Older unfinished attempts are marked interrupted without a recovery notification or continuation. Eligible recovery first notifies the original private chat, group, or topic. A remotely completed turn is synchronized and only its pending final delivery is resumed. A remotely active turn is reattached to a new progress card and polled until terminal. A stale App Server turn or an interrupted ACP process is continued in the same task and workspace with a new turn and a new progress card; the continuation Prompt tells the Agent to inspect existing effects before repeating work. Recovery retries remain durable across repeated restarts.
352
+ On every Worker startup, Agent Bot scans nonterminal turn attempts before releasing persisted Prompt queues. Only attempts with activity during the preceding ten minutes are eligible; runtime events and a one-minute heartbeat refresh the durable activity timestamp while a turn remains active. Older unfinished attempts are marked interrupted without a recovery notification or continuation. Eligible recovery first notifies the original private chat, group, or topic. A remotely completed turn is synchronized and only its pending final delivery is resumed. A remotely active turn is reattached to a new progress card and polled until terminal. A stale App Server turn or an interrupted ACP process is continued in the same task and workspace with a new turn and a new progress card; the continuation Prompt tells the Agent to inspect existing effects before repeating work. Writer conflicts end automatic recovery and display the existing occupancy controls without closing any process. Other unconfirmed recovery failures retry every five seconds within a one-minute window, then close the recovery card and mark the attempt interrupted. Failed activity checks do not authorize replaying the Prompt. A confirmed live turn continues to be monitored through temporary connection outages; the retry window is not a turn execution timeout.
351
353
 
352
354
  When a turn ends with a transient LLM-service failure, Agent Bot automatically starts up to three additional turns in the same task. Rate limits, temporary overload, upstream 5xx responses, timeouts, and interrupted provider streams are retryable. Authentication, quota exhaustion, context limits, invalid requests, unsupported models, permissions, policy failures, and tool errors are terminal immediately. Every retry gets a fresh progress card and a Prompt that warns the Agent to inspect existing effects before repeating work. The retry count and pending message bindings are persisted; original and steering-message reactions remain pending and follow the newest retry turn until it succeeds or exhausts the retry budget.
353
355
 
354
- Agent Bot also reconciles when turn identifiers change, terminal notifications arrive, or control requests fail. Stale persisted `inProgress` turns are not considered active unless the App Server thread itself reports an active status.
356
+ Agent Bot also reconciles when turn identifiers change, terminal notifications arrive, or control requests fail. Stale persisted `inProgress` turns are not considered active unless the App Server reports active, or an unloaded thread has both an unfinished rollout and an OS-reported live writer. Process-inspection errors remain errors rather than being treated as idle. Writer checks are cached for five seconds independently of the incremental rollout cache; loaded idle/completed state takes precedence over old rollout starts.
355
357
 
356
358
  The final-delivery ledger prevents duplicate successful replies. App Server requests use finite timeouts so a stalled request cannot permanently occupy a message route. Session lifecycle requests allow 60 seconds because compatible third-party Agents may need more than 30 seconds for `thread/start`; control requests remain shorter.
357
359
 
@@ -211,7 +211,7 @@ Turn Preview 复用 highlight.js,在服务端高亮显式标注语言的 Markd
211
211
 
212
212
  输入附件路径分别保存在 `TurnViewState.promptImagePaths` 和用户活动的 `localImagePaths` 可选字段中,不混入受长度限制的文字。待启动轮次转为正式轮次、追加及重试、排队启动和快照恢复都会保留路径。Preview 通过现有签名文件服务加 `raw=1` 加载图片,文件缺失则显示占位提示。新增 JSON 字段不需要数据库迁移,也不会自动回填缺少路径的历史用户活动。
213
213
 
214
- Turn Preview 支持在 Prompt、Commentary 和回答的 Markdown 中使用 `mermaid`、`flowchart` 代码块。浏览器按需从同源 `/assets/mermaid.js` 加载锁定版本的 Mermaid(支持 `fileViewer.publicBaseUrl` 路径前缀),不依赖 CDN。严格模式禁用 HTML 标签和交互回调;拒绝 frontmatter、配置指令和超过 50,000 字符的源码,最多 500 条边。生成的 SVG 还会移除可执行元素和外部链接,保留现有 CSP。图表块默认预览并按宽度适配,SSE 更新保留手动模式、「适应 / 原大」选择和未变化的 SVG;加载或渲染失败时显示转义后的源码。SVG 宽度不超过 viewBox 原宽度和容器宽度,保持比例且不放大小图。窄屏或触摸设备的预览高度最多为 480 px 或 55 svh(回退为 55 vh),长图纵向滚动;原大恢复 viewBox 宽度并在块内滚动,切回适应时重置块内滚动位置。CSS 自动适应横竖屏切换,无需重新渲染 Mermaid。普通代码块和工具输出保持不变。
214
+ Turn Preview 支持在 Prompt、Commentary 和回答的 Markdown 中使用 `mermaid`、`flowchart` 代码块。Markdown 文件预览复用相同的渲染器、切换控件、尺寸规则和安全策略,整份文件的代码视图及原始内容/下载保持不变。文件更新保留匹配图表的节点、模式、尺寸和滚动位置,仅重新渲染变化的源码。浏览器按需从同源 `/assets/mermaid.js` 加载锁定版本的 Mermaid(支持 `fileViewer.publicBaseUrl` 路径前缀),不依赖 CDN。严格模式禁用 HTML 标签和交互回调;拒绝 frontmatter、配置指令和超过 50,000 字符的源码,最多 500 条边。生成的 SVG 还会移除可执行元素和外部链接,保留现有 CSP。图表块默认预览并按宽度适配,SSE 更新保留手动模式、「适应 / 原大」选择和未变化的 SVG;加载或渲染失败时显示转义后的源码。SVG 宽度不超过 viewBox 原宽度和容器宽度,保持比例且不放大小图。窄屏或触摸设备的预览高度最多为 480 px 或 55 svh(回退为 55 vh),长图纵向滚动;原大恢复 viewBox 宽度并在块内滚动,切回适应时重置块内滚动位置。CSS 自动适应横竖屏切换,无需重新渲染 Mermaid。普通代码块和工具输出保持不变。
215
215
 
216
216
  HTML 文本文件(`.html`、`.htm`,不区分大小写)支持预览与代码模式。预览 iframe 使用带 `render=html` 的签名文件 URL,按检测到的编码流式加载完整原文,并携带文件版本参数用于实时刷新。iframe 与响应 CSP 都使用 `sandbox allow-scripts`,不授予 `allow-same-origin`;直接打开渲染 URL 时,响应级沙箱仍生效。允许内联脚本、样式及 data/blob 嵌入媒体,不开放外部或本地资源加载、网络请求、表单、弹窗和顶层跳转。外层页面保持原有脚本限制,原始文件与下载响应不变。源码仍经过转义、按需高亮,限制为开头 2 MiB,并支持 `#L<n>` 行号定位。
217
217
 
@@ -321,6 +321,8 @@ Fork 从未绑定过 Agent Bot 的任务前,Agent Bot 会先读取不含 Turns
321
321
 
322
322
  Provider 会与模型、思考强度和权限模式一起保存在任务中;每个 Agent 还可以在 `agents.<name>.defaults` 下分别保存这些默认值。新任务先读取所选 Agent 的默认值,再由显式设置或同 Agent 任务继承值覆盖。Agent Bot 会通过 `thread/start`、`thread/resume` 和 `thread/fork` 传递最终设置,并把运行时返回的实际值保存到任务。
323
323
 
324
+ 每次 `turn/start` 还会同时传递 `approvalPolicy` 和 `sandboxPolicy`:`auto` 对应 `never` + `dangerFullAccess`,`confirm` 对应 `on-request` + `workspaceWrite`。权限切换从下一轮生效,无需重启共享 App Server、恢复或分支任务;`turn/steer` 不携带这些覆盖参数。工作区策略优先取会话生命周期响应;缺失时按当前 cwd 懒加载一次 `config/read` 并按会话缓存,保留额外可写目录、网络访问和临时目录排除配置,不读取历史。配置无效或不可用时不启动新轮次;沙箱策略被拒绝时不会去掉限制后重试。
325
+
324
326
  `/provider`、`/model`、`/thinking` 和 `/permissions` 打开同一张 Card 2.0 运行设置卡片,并激活对应的 tab。四个命令都拒绝参数,tab 切换和设置修改只通过卡片回调完成。Provider 选项来自 App Server 的 `config/read`;对于 Codex,即使隐式内置的 `openai` 没有出现在显式 `model_providers` 配置中,Agent Bot 也会保留该选项。内置 OpenAI Provider 使用 App Server 的 `model/list`;显式配置的自定义 Provider 会在接口可用时使用 `GET <base_url>/models`,并携带其 `query_params`、`http_headers`、`env_http_headers` 和已配置的 Bearer 凭据,凭据只在 Agent Bot 内部使用,不会进入卡片或模型记录。切换 Provider 时,如果目标模型目录包含原模型则继续使用;否则依次选择标记的默认模型和返回列表中的第一个模型。自定义接口只返回模型 ID 时,会按同名模型合并 App Server 目录中的思考强度元数据。模型发现不可用时,Agent Bot 会把当前或已配置模型作为唯一候选,因此不会因缺少 `/models` 而阻止切换;保存前仍由 Codex 返回的实际 Provider 和模型完成校验。每次成功修改 Provider、模型、思考强度或权限后,Agent Bot 都会更新当前任务,把完整生效设置原子写入该 Agent 在 `config.yaml` 中的默认值,就地刷新卡片,并从下一次请求生效;对应 CLI 任务设置命令使用相同的持久化路径。
325
327
 
326
328
  Agent Bot 内部保留本地路由键以关联飞书卡片和投递状态,但对用户展示所属 App Server 的任务 ID。没有明确用户操作时,不会续写、steer、停止或分支其他客户端正在运行的 Agent 工作。
@@ -347,7 +349,7 @@ SQLite 保存:
347
349
  - 进度快照和消息绑定
348
350
  - 最终消息投递记录
349
351
 
350
- 每次 Worker 启动时,Agent Bot 都会先扫描未终结的 turn 执行记录,再放行持久化的 Prompt 队列。只有前 5 分钟内仍有活动的执行才具备恢复资格;turn 运行期间的运行时事件和每分钟心跳会刷新持久化的活动时间。更早的未完成执行会被标记为已中断,不发送恢复通知,也不创建续跑 turn。符合条件的恢复会先通知原私聊、群聊或话题;远端已完成的 turn 只同步状态并继续未完成的最终消息投递;远端仍活动的 turn 会换用新的进度卡并持续轮询到终态;已失效的 App Server turn 或被中断的 ACP 进程会在同一任务和工作目录中创建新的续跑 turn,续跑 Prompt 会要求 Agent 先检查已有副作用,避免重复操作。恢复状态在连续多次重启之间保持持久化。
352
+ 每次 Worker 启动时,Agent Bot 都会先扫描未终结的 turn 执行记录,再放行持久化的 Prompt 队列。只有前 10 分钟内仍有活动的执行才具备恢复资格;turn 运行期间的运行时事件和每分钟心跳会刷新持久化的活动时间。更早的未完成执行会被标记为已中断,不发送恢复通知,也不创建续跑 turn。符合条件的恢复会先通知原私聊、群聊或话题;远端已完成的 turn 只同步状态并继续未完成的最终消息投递;远端仍活动的 turn 会换用新的进度卡并持续轮询到终态;已失效的 App Server turn 或被中断的 ACP 进程会在同一任务和工作目录中创建新的续跑 turn,续跑 Prompt 会要求 Agent 先检查已有副作用,避免重复操作。写锁冲突会终止自动恢复并显示占用卡片,不自动关闭任何进程。其他尚未确认恢复成功的故障每 5 秒重试,1 分钟重试窗口耗尽后关闭恢复卡片、标记中断并通知用户;活动检查失败不代表可以重放原 Prompt。已经确认活跃的轮次在临时断连期间继续监控,不受该恢复超时限制。未加载的 Codex 任务必须同时存在未结束的 rollout 和操作系统确认的存活写入进程,才补充为活跃状态;进程检查失败不会当成空闲。写入进程检查独立缓存 5 秒,不影响 rollout 增量读取;已加载任务的空闲或完成状态优先于旧日志中的开始事件。
351
353
 
352
354
  当 turn 因临时 LLM 服务错误失败时,Agent Bot 会在同一任务中自动再创建最多 3 个 turn。限流、临时过载、上游 5xx、超时和模型服务流中断可以重试;认证失败、额度耗尽、上下文超限、无效请求、不支持的模型、权限或策略错误以及工具错误会立即终止。每次重试使用新的思考卡片,重试 Prompt 会要求 Agent 先检查已有副作用再继续,避免重复操作。重试次数和待处理消息绑定会持久化;原始消息及追加消息的 Reaction 会保持处理中,并跟随最新的重试 turn,直到成功或耗尽重试次数。
353
355
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@keyou007/agent-bot",
3
- "version": "0.1.24-alpha.3",
3
+ "version": "0.1.24-alpha.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@keyou007/agent-bot",
9
- "version": "0.1.24-alpha.3",
9
+ "version": "0.1.24-alpha.5",
10
10
  "license": "GPL-3.0-only",
11
11
  "dependencies": {
12
12
  "@agentclientprotocol/sdk": "1.2.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keyou007/agent-bot",
3
- "version": "0.1.24-alpha.3",
3
+ "version": "0.1.24-alpha.5",
4
4
  "type": "module",
5
5
  "description": "Use Codex, TraeX, and compatible coding agents from Lark/Feishu.",
6
6
  "license": "GPL-3.0-only",
@@ -110,12 +110,16 @@ These commands export only user Prompts and final answers through the latest com
110
110
 
111
111
  Feishu `/sessions [keyword]` lists and searches tasks across projects and all Providers, not just the App Server's current Provider. Switching Provider does not hide existing tasks; listing still uses paged metadata and the latest Turn summary only.
112
112
 
113
+ Feishu group renames update only the group's currently bound task title, not topic or historical tasks. Users may remove generated prefixes or use another format: recognized configured names or matching Agent prefixes yield the task title, otherwise the normalized complete group name is used. A renamed prefix does not change the task's Agent. New-group naming rules are unchanged.
114
+
113
115
  Fork creation stores source-task and branch-Turn references without synchronizing the full local Turn list. The Feishu Turn card reuses local records and loads only the summary pages needed for the requested page, not the entire history. `task turns` loads the first page if necessary and returns available local records. This does not change the Agent's inherited context. Retry listing Turns after a temporary history-read failure; unsupported summary pagination never falls back to downloading full history.
114
116
 
115
117
  If a TraeX task stops producing output, check its progress card for a pending plan-mode confirmation. `Approve Plan` / `Enter Plan Mode`, `Reject`, and `Cancel Request` require the user's explicit choice, even with automatic permissions. Preview displays the plan but cannot approve it. Cancelling a request does not itself stop the task; waiting turns still block safe restart. Never approve, stop, or force-restart merely to clear that wait. Notifications dropped by an older running version are not automatically replayed after upgrading.
116
118
 
117
119
  ## Change Settings
118
120
 
121
+ Feishu slash commands accept unique prefixes: `/mo`, `/mod`, and `/mode` open the `/model` settings card and do not accept arguments. This is model selection, not an Agent mode switch. CLI `task model [model]` keeps its documented argument behavior.
122
+
119
123
  ```powershell
120
124
  agentbot task agent [name]
121
125
  agentbot task provider [provider]
@@ -126,6 +130,8 @@ agentbot task permissions [auto|confirm]
126
130
 
127
131
  Omit the value to inspect the current setting and available choices. `agent` changes the default Agent for future tasks in that conversation. The other settings affect the specified task from its next request and become the saved defaults for that Agent.
128
132
 
133
+ For App Server tasks, `permissions auto` applies `never` approval and `dangerFullAccess` sandbox policies on the next Turn; `permissions confirm` applies `on-request` and `workspaceWrite`, preserving configured workspace restrictions. No Agent restart, fork, or new task is needed. Steering or changing the setting does not re-sandbox tools already executing. Feishu `/permissions` takes no arguments: choose the mode on its card.
134
+
129
135
  A successful Provider switch confirms settings, not inference connectivity. Model-list fallback warnings appear on settings cards; inspect the Provider service when availability is unconfirmed. App Server errors and native retry notices show the message and supplied `additionalDetails` as literal text on the current progress card and Preview without terminating the turn. Use Preview for long reasons; `Reconnecting` alone does not identify the cause when the Agent supplies no details, and old snapshots are not backfilled. Do not stop or restart a retrying task unless the user requests it.
130
136
 
131
137
  Providers such as a Responses bridge may omit assistant message phases. Unphased text stays in the progress timeline; successful completion promotes only the last message not followed by a tool start. Do not diagnose progress text in the answer area as a CSS issue without checking message phases. Old snapshots are not automatically backfilled.
@@ -149,6 +155,8 @@ The Feishu `/sessions` card also has `SwitchGroup`: create a new group bound to
149
155
 
150
156
  The Feishu `/sessions` card provides `Turns` for each task, including unbound external tasks. Browsing and pagination never switch, resume, or bind a task; Reset is available only for the current task.
151
157
 
158
+ Expand a running task in `/sessions` to use its `Stop` button, including the current task and tasks in other conversations or external clients. It requests the normal Agent interrupt without switching tasks or killing processes; only click it when the user requests stopping that task. The refreshed list preserves search/page and uses actual Agent status, not interrupt acknowledgement, to decide whether Stop remains available. Archive is offered only when idle.
159
+
152
160
  Use `turns` to obtain a real Turn ID before `reset`. Reset changes conversation context only; it does not revert local files. Agent Bot announces an interactive Reset when it starts and queues every new message after its Reaction until the replacement thread is ready.
153
161
 
154
162
  ## Files And Local Commands
@@ -212,6 +220,8 @@ Use `init --reset` only for an explicitly requested full reset. Without `--profi
212
220
 
213
221
  ## After Code Changes
214
222
 
223
+ For tasks stuck after a Worker crash, inspect the recovery notice and any writer-conflict card before acting. Unconfirmed recovery retries stop after a one-minute window; a live writer conflict stops them immediately. An old unfinished rollout or a leftover lock file alone is not proof of a running task. Do not delete locks, edit task state, or close another Agent process automatically. Already-confirmed live turns remain monitored; ask the user before stopping them.
224
+
215
225
  Run the relevant checks, then build:
216
226
 
217
227
  ```powershell
@@ -18,7 +18,6 @@ export interface RuntimeSession {
18
18
  acpSessionId: string;
19
19
  managed: ManagedAcpProcess;
20
20
  running: boolean;
21
- modes?: JsonValue;
22
21
  configOptions?: JsonValue;
23
22
  availableCommands?: JsonValue;
24
23
  onUpdate: (session: RuntimeSession, update: Record<string, JsonValue>) => void;
@@ -81,7 +80,6 @@ export class AcpSessionManager {
81
80
  acpSessionId: newSessionResult.sessionId,
82
81
  managed,
83
82
  running: false,
84
- modes: newSessionResult.modes,
85
83
  configOptions: newSessionResult.configOptions,
86
84
  onUpdate: input.onUpdate,
87
85
  onPermissionRequest: input.onPermissionRequest,
@@ -147,14 +145,6 @@ export class AcpSessionManager {
147
145
  this.processManager.stopAll();
148
146
  }
149
147
 
150
- async setMode(localSessionId: string, modeId: string): Promise<void> {
151
- const session = this.requireSession(localSessionId);
152
- await session.managed.connection.request("session/set_mode", {
153
- sessionId: session.acpSessionId,
154
- modeId,
155
- });
156
- }
157
-
158
148
  async setConfigOption(localSessionId: string, configId: string, value: string): Promise<JsonValue> {
159
149
  const session = this.requireSession(localSessionId);
160
150
  const result = await session.managed.connection.request<JsonValue>("session/set_config_option", {
@@ -242,13 +232,6 @@ export class AcpSessionManager {
242
232
  session.configOptions = update.configOptions;
243
233
  }
244
234
 
245
- if (updateType === "current_mode_update" && session.modes && isObject(session.modes)) {
246
- session.modes = {
247
- ...session.modes,
248
- currentModeId: update.modeId,
249
- };
250
- }
251
-
252
235
  if (updateType === "available_commands_update" && "availableCommands" in update) {
253
236
  session.availableCommands = update.availableCommands;
254
237
  }
@@ -63,7 +63,6 @@ export interface AcpInitializeResult {
63
63
 
64
64
  export interface AcpSessionNewResult {
65
65
  sessionId: string;
66
- modes?: JsonValue;
67
66
  configOptions?: JsonValue;
68
67
  }
69
68
 
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+
3
+ const workspacePolicySchema = z.object({
4
+ type: z.literal("workspaceWrite"),
5
+ writableRoots: z.array(z.string().min(1)),
6
+ networkAccess: z.boolean(),
7
+ excludeTmpdirEnvVar: z.boolean(),
8
+ excludeSlashTmp: z.boolean(),
9
+ });
10
+
11
+ const workspaceConfigSchema = z.object({
12
+ sandbox_workspace_write: z.object({
13
+ writable_roots: z.array(z.string().min(1)).default([]),
14
+ network_access: z.boolean().default(false),
15
+ exclude_tmpdir_env_var: z.boolean().default(false),
16
+ exclude_slash_tmp: z.boolean().default(false),
17
+ }).nullish(),
18
+ });
19
+
20
+ export type WorkspaceSandboxPolicy = z.infer<typeof workspacePolicySchema>;
21
+ export type TurnSandboxPolicy = WorkspaceSandboxPolicy | { type: "dangerFullAccess" };
22
+
23
+ export function workspaceSandboxFromResponse(sandbox: unknown): WorkspaceSandboxPolicy | undefined {
24
+ const parsed = workspacePolicySchema.safeParse(sandbox);
25
+ return parsed.success ? parsed.data : undefined;
26
+ }
27
+
28
+ export function workspaceSandboxFromConfig(config: unknown): WorkspaceSandboxPolicy {
29
+ const parsed = workspaceConfigSchema.safeParse(config);
30
+ if (!parsed.success) throw new Error("App Server 返回了无效的工作区沙箱配置,未启动新轮次。");
31
+ const settings = parsed.data.sandbox_workspace_write;
32
+ return {
33
+ type: "workspaceWrite",
34
+ writableRoots: settings?.writable_roots ?? [],
35
+ networkAccess: settings?.network_access ?? false,
36
+ excludeTmpdirEnvVar: settings?.exclude_tmpdir_env_var ?? false,
37
+ excludeSlashTmp: settings?.exclude_slash_tmp ?? false,
38
+ };
39
+ }
@@ -2,8 +2,10 @@ import { open } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import Database from "better-sqlite3";
4
4
  import type { PermissionMode } from "../runtime/types.js";
5
+ import { SystemThreadWriterProcessController, threadWriterLockPath } from "./ThreadWriterProcess.js";
5
6
 
6
7
  const READ_CHUNK_SIZE = 64 * 1024;
8
+ const WRITER_CHECK_CACHE_MS = 5_000;
7
9
  const TASK_STARTED = Buffer.from('"type":"task_started"');
8
10
  const TASK_COMPLETE = Buffer.from('"type":"task_complete"');
9
11
  const TURN_ABORTED = Buffer.from('"type":"turn_aborted"');
@@ -46,7 +48,14 @@ export interface CodexLocalThreadSettings {
46
48
  */
47
49
  export class CodexLocalActivityDetector {
48
50
  private readonly rollouts = new Map<string, RolloutState>();
49
- constructor(private readonly codexHome: string) {}
51
+ private readonly writerChecks = new Map<string, { expiresAt: number; result: Promise<boolean> }>();
52
+ constructor(
53
+ private readonly codexHome: string,
54
+ private readonly hasWriter: (threadId: string) => Promise<boolean> = async (threadId) => {
55
+ const lockPath = threadWriterLockPath(codexHome, threadId);
56
+ return Boolean(lockPath && (await new SystemThreadWriterProcessController().inspect(lockPath)).length > 0);
57
+ },
58
+ ) {}
50
59
 
51
60
  async activeThreadIds(threadIds: string[]): Promise<Set<string>> {
52
61
  return new Set((await this.activeThreads(threadIds)).keys());
@@ -74,13 +83,34 @@ export class CodexLocalActivityDetector {
74
83
  }
75
84
 
76
85
  const active = new Map<string, string | undefined>();
77
- for (const row of rows) {
78
- const task = await latestTask(row.rollout_path, this.rollouts);
79
- if (task.active) active.set(row.id, task.turnId);
86
+ for (let index = 0; index < rows.length; index += 4) {
87
+ await Promise.all(rows.slice(index, index + 4).map(async (row) => {
88
+ const task = await latestTask(row.rollout_path, this.rollouts);
89
+ // An unpaired start survives a crash. It is not evidence of a live process.
90
+ if (task.active && await this.hasLiveWriter(row.id)) active.set(row.id, task.turnId);
91
+ }));
80
92
  }
81
93
  return active;
82
94
  }
83
95
 
96
+ private async hasLiveWriter(threadId: string): Promise<boolean> {
97
+ const cached = this.writerChecks.get(threadId);
98
+ if (cached && cached.expiresAt > Date.now()) return cached.result;
99
+ const result = this.hasWriter(threadId).catch((error: unknown) => {
100
+ throw new Error("无法确认任务的写入进程是否仍在运行,请稍后重试。", { cause: error });
101
+ });
102
+ const entry = { expiresAt: Date.now() + WRITER_CHECK_CACHE_MS, result };
103
+ this.writerChecks.delete(threadId);
104
+ this.writerChecks.set(threadId, entry);
105
+ if (this.writerChecks.size > 512) this.writerChecks.delete(this.writerChecks.keys().next().value!);
106
+ try {
107
+ return await result;
108
+ } catch (error) {
109
+ if (this.writerChecks.get(threadId) === entry) this.writerChecks.delete(threadId);
110
+ throw error;
111
+ }
112
+ }
113
+
84
114
  async threadSettings(threadIds: string[]): Promise<Map<string, CodexLocalThreadSettings>> {
85
115
  const ids = [...new Set(threadIds.filter(Boolean))];
86
116
  if (ids.length === 0) return new Map();
@@ -30,6 +30,7 @@ import type {
30
30
  import { appendGeneratedImageMarkdown } from "../utils/generatedImageMarkdown.js";
31
31
  import { normalizeTaskTitle } from "../utils/taskTitle.js";
32
32
  import { AppServerRequestError } from "./AppServerConnection.js";
33
+ import { workspaceSandboxFromConfig, workspaceSandboxFromResponse, type TurnSandboxPolicy, type WorkspaceSandboxPolicy } from "./AppServerPermissions.js";
33
34
  import { formatCodexError, mapCodexNotification } from "./CodexEventMapper.js";
34
35
  import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
35
36
  import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
@@ -89,6 +90,7 @@ interface CodexSession extends RuntimeSession {
89
90
  unphasedMessages: Map<string, string>;
90
91
  unphasedFinalMessageId?: string;
91
92
  needsResume: boolean;
93
+ workspaceSandboxPolicy?: WorkspaceSandboxPolicy;
92
94
  canReplaceEmptyThread: boolean;
93
95
  settingsRecoveryError?: string;
94
96
  rolloutPath?: string;
@@ -138,6 +140,10 @@ export class CodexRuntime implements AgentRuntime {
138
140
  return this.sessions.get(localSessionId);
139
141
  }
140
142
 
143
+ forgetSession(localSessionId: string): void {
144
+ this.sessions.delete(localSessionId);
145
+ }
146
+
141
147
  getProcessInfo(): AgentProcessInfo {
142
148
  return this.provider.getProcessInfo?.() ?? {};
143
149
  }
@@ -238,7 +244,7 @@ export class CodexRuntime implements AgentRuntime {
238
244
  const client = await this.client();
239
245
  await this.ensureSessionResumed(session, client);
240
246
  session.canReplaceEmptyThread = false;
241
- const start = () => client.request<{ turn: { id: string } }>("turn/start", {
247
+ const start = async () => client.request<{ turn: { id: string } }>("turn/start", {
242
248
  threadId: session.remoteSessionId,
243
249
  input: codexUserInput(prompt),
244
250
  cwd: session.cwd,
@@ -246,6 +252,7 @@ export class CodexRuntime implements AgentRuntime {
246
252
  effort: session.reasoningEffort,
247
253
  summary: "auto",
248
254
  approvalPolicy: session.permissionMode === "auto" ? "never" : "on-request",
255
+ sandboxPolicy: await this.turnSandboxPolicy(session, client),
249
256
  }, SESSION_REQUEST_TIMEOUT_MS);
250
257
  let response: { turn: { id: string } };
251
258
  try {
@@ -294,7 +301,9 @@ export class CodexRuntime implements AgentRuntime {
294
301
  return listed;
295
302
  }
296
303
  }));
297
- const activeThreads = await this.localActivityDetector?.activeThreads(sessions.map((session) => session.id));
304
+ const activeThreads = await this.localActivityDetector?.activeThreads(
305
+ sessions.filter((session) => session.status === "not_loaded").map((session) => session.id),
306
+ );
298
307
  return {
299
308
  sessions: activeThreads
300
309
  ? sessions.map((session) => markLocallyDetectedActive(session, activeThreads))
@@ -499,7 +508,8 @@ export class CodexRuntime implements AgentRuntime {
499
508
  remoteSessionId: string,
500
509
  summary: RemoteSessionSummary,
501
510
  ): Promise<RemoteSessionSummary> {
502
- const activeThreads = await this.localActivityDetector?.activeThreads([remoteSessionId]);
511
+ const activeThreads = summary.status === "not_loaded"
512
+ ? await this.localActivityDetector?.activeThreads([remoteSessionId]) : undefined;
503
513
  const activeSummary = activeThreads ? markLocallyDetectedActive(summary, activeThreads) : summary;
504
514
  const settings = await this.localActivityDetector?.threadSettings([remoteSessionId]);
505
515
  return {
@@ -571,24 +581,22 @@ export class CodexRuntime implements AgentRuntime {
571
581
  }
572
582
 
573
583
  async inspectRemoteSessionActivity(remoteSessionId: string): Promise<RemoteSessionActivity> {
574
- const [response, activeThreads] = await Promise.all([
575
- (await this.client()).request<ThreadReadResponse>(
576
- "thread/read",
577
- { threadId: remoteSessionId, includeTurns: false },
578
- SYNC_REQUEST_TIMEOUT_MS,
579
- ),
580
- this.localActivityDetector?.activeThreads([remoteSessionId]),
581
- ]);
584
+ const response = await (await this.client()).request<ThreadReadResponse>(
585
+ "thread/read", { threadId: remoteSessionId, includeTurns: false }, SYNC_REQUEST_TIMEOUT_MS,
586
+ );
587
+ const status = remoteThreadStatus(response.thread.status?.type);
588
+ const activeThreads = status === "not_loaded"
589
+ ? await this.localActivityDetector?.activeThreads([remoteSessionId]) : undefined;
582
590
  const runtimeSession = [...this.sessions.values()]
583
591
  .find((session) => session.remoteSessionId === remoteSessionId);
584
592
  const detectedTurnId = activeThreads?.get(remoteSessionId);
585
- const active = remoteThreadStatus(response.thread.status?.type) === "active"
586
- || Boolean(runtimeSession?.activeTurnId)
587
- || Boolean(activeThreads?.has(remoteSessionId));
593
+ // A turn started in this process is evidence; a restored activeTurnId alone is not.
594
+ const active = status === "active" || Boolean(activeThreads?.has(remoteSessionId))
595
+ || Boolean(runtimeSession?.activeTurnId && runtimeSession.activeTurnStartedAt !== undefined);
588
596
  const activeTurnId = detectedTurnId ?? runtimeSession?.activeTurnId;
589
597
  return {
590
598
  active,
591
- ...(activeTurnId ? { activeTurnId } : {}),
599
+ ...(active && activeTurnId ? { activeTurnId } : {}),
592
600
  };
593
601
  }
594
602
 
@@ -807,6 +815,7 @@ export class CodexRuntime implements AgentRuntime {
807
815
  threadId: response.thread.id, name: session.title,
808
816
  }, SESSION_REQUEST_TIMEOUT_MS);
809
817
  const candidate = { ...session, ...settings, remoteSessionId: response.thread.id,
818
+ workspaceSandboxPolicy: workspaceSandboxFromResponse(response.sandbox),
810
819
  needsResume: false, settingsRecoveryError: undefined };
811
820
  await persist?.(candidate);
812
821
  Object.assign(session, candidate);
@@ -826,6 +835,7 @@ export class CodexRuntime implements AgentRuntime {
826
835
  });
827
836
  if (restored.thread.id !== previous.remoteSessionId) throw new Error("恢复时返回了不同的任务 ID。");
828
837
  session.needsResume = false;
838
+ session.workspaceSandboxPolicy = workspaceSandboxFromResponse(restored.sandbox);
829
839
  session.settingsRecoveryError = undefined;
830
840
  } catch (restoreError) {
831
841
  this.logger.warn({ error: restoreError, sessionId }, "Failed to restore Provider after a rejected settings change.");
@@ -1390,7 +1400,7 @@ export class CodexRuntime implements AgentRuntime {
1390
1400
  }
1391
1401
 
1392
1402
  private async resumeAppServerSession(session: CodexSession, client: AppServerClient): Promise<void> {
1393
- await client.request("thread/resume", {
1403
+ const response = await client.request<ThreadResponse>("thread/resume", {
1394
1404
  threadId: session.remoteSessionId,
1395
1405
  excludeTurns: true,
1396
1406
  cwd: session.cwd,
@@ -1399,6 +1409,19 @@ export class CodexRuntime implements AgentRuntime {
1399
1409
  ...threadLifecycleParams(session.cwd),
1400
1410
  ...permissionParams(session.permissionMode),
1401
1411
  }, SESSION_REQUEST_TIMEOUT_MS);
1412
+ session.workspaceSandboxPolicy = workspaceSandboxFromResponse(response.sandbox);
1413
+ }
1414
+
1415
+ private async turnSandboxPolicy(session: CodexSession, client: AppServerClient): Promise<TurnSandboxPolicy> {
1416
+ if (session.permissionMode === "auto") return { type: "dangerFullAccess" };
1417
+ if (!session.workspaceSandboxPolicy) {
1418
+ // Resolve project-specific options once, without resuming or scanning history.
1419
+ const response = await client.request<{ config?: unknown }>("config/read", {
1420
+ cwd: session.cwd, includeLayers: false,
1421
+ }, CONTROL_REQUEST_TIMEOUT_MS);
1422
+ session.workspaceSandboxPolicy = workspaceSandboxFromConfig(response.config);
1423
+ }
1424
+ return session.workspaceSandboxPolicy;
1402
1425
  }
1403
1426
 
1404
1427
  private async synchronizeSessionNow(sessionId: string): Promise<RuntimeSession> {
@@ -1435,7 +1458,9 @@ export class CodexRuntime implements AgentRuntime {
1435
1458
  }
1436
1459
  if (latestInProgress) {
1437
1460
  this.supersedeTurn(session, activeTurnId);
1438
- this.adoptTurn(session, latestInProgress.id, turnStartedAt(latestInProgress));
1461
+ if (thread.status?.type === "active") {
1462
+ this.adoptTurn(session, latestInProgress.id, turnStartedAt(latestInProgress));
1463
+ }
1439
1464
  return;
1440
1465
  }
1441
1466
 
@@ -1570,6 +1595,7 @@ export class CodexRuntime implements AgentRuntime {
1570
1595
  messagePhases: new Map(),
1571
1596
  unphasedMessages: new Map(),
1572
1597
  needsResume: false,
1598
+ workspaceSandboxPolicy: workspaceSandboxFromResponse(response.sandbox),
1573
1599
  canReplaceEmptyThread: !("remoteSessionId" in input),
1574
1600
  rolloutPath: stringValue(response.thread.path)?.trim() || undefined,
1575
1601
  turnCount: "remoteSessionId" in input ? undefined : 0,
@@ -1642,6 +1668,7 @@ interface ThreadResponse {
1642
1668
  modelProvider?: string;
1643
1669
  model?: string;
1644
1670
  reasoningEffort?: string | null;
1671
+ sandbox?: unknown;
1645
1672
  }
1646
1673
 
1647
1674
  interface ThreadReadResponse {
@@ -29,8 +29,9 @@ export class SystemThreadWriterProcessController implements ThreadWriterProcessC
29
29
  async inspect(lockPath: string): Promise<ThreadWriterProcess[]> {
30
30
  try {
31
31
  await access(lockPath);
32
- } catch {
33
- return [];
32
+ } catch (error) {
33
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
34
+ throw error;
34
35
  }
35
36
  return process.platform === "win32"
36
37
  ? inspectWindowsLockOwners(lockPath)
@@ -134,12 +135,15 @@ async function inspectWindowsLockOwners(lockPath: string): Promise<ThreadWriterP
134
135
  async function inspectPosixLockOwners(lockPath: string): Promise<ThreadWriterProcess[]> {
135
136
  const pids = await posixLockOwnerPids(lockPath);
136
137
  const owners = await Promise.all(pids.map((pid) => inspectPosixProcess(pid)));
138
+ if (owners.some((owner) => !owner)) throw new Error("Cannot identify a reported thread writer; retry the inspection.");
137
139
  return owners.filter((owner): owner is ThreadWriterProcess => owner !== undefined);
138
140
  }
139
141
 
140
142
  async function posixLockOwnerPids(lockPath: string): Promise<number[]> {
141
143
  const lsof = await tryExecuteFile("lsof", ["-t", "--", lockPath]);
142
- const raw = lsof.ok ? lsof.stdout : (await tryExecuteFile("fuser", [lockPath])).stdout;
144
+ const result = lsof.ok || lsof.noMatches ? lsof : await tryExecuteFile("fuser", [lockPath]);
145
+ if (!result.ok && !result.noMatches) throw new Error("Cannot inspect thread writers: lsof and fuser failed.");
146
+ const raw = result.stdout;
143
147
  return [...new Set(raw.match(/\d+/gu)?.map(Number).filter((pid) => Number.isSafeInteger(pid) && pid > 0) ?? [])];
144
148
  }
145
149
 
@@ -233,11 +237,15 @@ function executeFile(command: string, args: string[], extraEnv: Record<string, s
233
237
  });
234
238
  }
235
239
 
236
- function tryExecuteFile(command: string, args: string[]): Promise<{ ok: boolean; stdout: string }> {
237
- return executeFile(command, args).then(
238
- (stdout) => ({ ok: true, stdout }),
239
- () => ({ ok: false, stdout: "" }),
240
- );
240
+ function tryExecuteFile(command: string, args: string[]): Promise<{ ok: boolean; stdout: string; noMatches: boolean }> {
241
+ return new Promise((resolve) => {
242
+ execFile(command, args, {
243
+ timeout: PROCESS_INSPECTION_TIMEOUT_MS, maxBuffer: MAX_PROCESS_OUTPUT_BYTES, windowsHide: true, encoding: "utf8",
244
+ }, (error, stdout, stderr) => resolve({
245
+ ok: !error, stdout,
246
+ noMatches: error?.code === 1 && !error.killed && !stdout.trim() && !stderr.trim(),
247
+ }));
248
+ });
241
249
  }
242
250
 
243
251
  function positiveInteger(value: unknown): number | undefined {
@@ -352,7 +360,7 @@ try {
352
360
  for ($index = 0; $index -lt $count; $index++) {
353
361
  $writerPid = $items[$index].Process.dwProcessId
354
362
  $writer = Get-AgentBotProcess $writerPid
355
- if ($null -eq $writer) { continue }
363
+ if ($null -eq $writer) { throw "Cannot identify reported thread writer PID $writerPid; retry the inspection." }
356
364
  $application = $writer
357
365
  $cursor = $writer
358
366
  for ($depth = 0; $depth -lt 8 -and $cursor.ParentProcessId -gt 1; $depth++) {
@@ -42,8 +42,6 @@ const COMMAND_INITIALISMS: Partial<Record<CommandName, string>> = {
42
42
  nosteer: "ns",
43
43
  };
44
44
 
45
- const DISABLED_COMMAND_NAMES = new Set(["mode", "modes"]);
46
-
47
45
  export class CommandRouter {
48
46
  parse(text: string): Command {
49
47
  const trimmed = text.trim();
@@ -162,7 +160,6 @@ export class CommandRouter {
162
160
  function resolveCommandName(rawCommand: string): CommandName {
163
161
  const input = rawCommand.slice(1).toLowerCase();
164
162
  if (!input) throw unknownCommand(rawCommand);
165
- if (DISABLED_COMMAND_NAMES.has(input)) throw unknownCommand(rawCommand);
166
163
 
167
164
  const exact = COMMAND_NAMES.find((command) => command === input);
168
165
  if (exact) return exact;