一句话摘要
Qoder 已经实现了续跑所需的全部零件——CLI 的 --resume、中断恢复队列表 pending_turn_projections、轮次状态字段(含 interrupted 取值)、启动时的 recoveredInterruptedTurnCount 打点——但本机可查的全部历史显示:被明确标记为 interrupted 的 37 条轮次,没有一条进入过恢复队列,重启后也没有恢复过任何一轮。其中 2 条自 2026-06-19 起一直停在 interrupted,跨越此后所有次启动。会话历史本身是完整落库的(单会话最多 393 条消息),所以这不是"数据丢了",而是恢复链路没接上。
环境
| 项 |
值 |
| Qoder 桌面版 |
0.4.1(同机曾有 0.3.4 / 0.3.3,见 I:\qoder\.qoder-versions\) |
| Agent CLI |
cli_version: 1.1.57 |
| Node |
v24.18.0 |
| OS |
Windows 11 25H2(build 10.0.26200),x64 |
| 数据目录 |
%APPDATA%\com.qoder.app.stable |
| Run 日志 |
~/.qoder/logs/runs/,本机留存 1116 个 run,跨度 2026-09-18 → 2026-09-23 |
| 采集时刻 |
2026-09-23 21:50 (+08:00),以下命令均为只读 |
期望 vs 实际
期望:turn 执行中被杀死(worker 崩溃、App 重启、自动更新重启)后,该轮次进入恢复队列,重启时自动续跑,或至少向用户提供"继续这一轮"的入口。
实际:轮次被写成 interrupted 终态就停在那里。恢复队列 0 行,重启恢复计数 0,用户侧只能从头重跑。
取证 A:状态机知道"这一轮被打断了",但恢复队列为空
import { DatabaseSync } from 'node:sqlite';
const buf = new DatabaseSync('%APPDATA%/com.qoder.app.stable/chat-session-turn-payload-buffer.sqlite', { readOnly: true });
const main = new DatabaseSync('%APPDATA%/com.qoder.app.stable/main.sqlite', { readOnly: true });
buf.prepare('select count(*) n from pending_turn_projections').get(); // { n: 0 } ← 恢复队列
buf.prepare('select count(*) n from active_turn_projection_items').get(); // { n: 6 } ← 正在流的会话确有写入
main.prepare('select count(*) n from chat_session_turn_states').get(); // { n: 0 }
main.prepare('select count(*) n from chat_sessions').get(); // { n: 93 }
main.prepare('select count(*) n from chat_session_messages').get(); // { n: 3887 }
main.prepare('select status, count(*) n from chat_session_messages group by status order by n desc').all();
// [ { status: 'completed', n: 3804 }, { status: 'failed', n: 46 }, { status: 'interrupted', n: 37 } ]
要点:
pending_turn_projections 的 schema 本身就是为续跑准备的:
session_id, turn_id, message_id, source, message_json, message_status, state, attempt_count, created_at, updated_at —— 现在 0 行。
- 同一时刻
chat_session_messages 里有 37 条 status='interrupted':App 自己判定并记录了"这一轮是被打断的",但没有任何一行投影进恢复队列。
- 同库的
active_turn_projection_items 有 6 行,可排除"连错库 / 写入路径不通"这类误判。
取证 B:17 个会话的最后一轮至今停在非终态
select s.session_id, m.status, s.model, s.updated_at
from chat_sessions s
join (select session_id, status,
row_number() over (partition by session_id order by sequence desc) rn
from chat_session_messages) m
on m.session_id = s.session_id and m.rn = 1
where m.status in ('interrupted','failed')
order by s.updated_at desc;
返回 17 行(会话标题已去除,避免暴露本地项目名):
| session_id 前缀 |
末轮状态 |
最后更新 (UTC) |
模型通道 |
27fc0bfb |
interrupted |
2026-09-23 13:06 |
BYOK |
cb28e5eb |
failed |
2026-09-23 13:03 |
BYOK |
4726c108 |
failed |
2026-09-23 12:45 |
BYOK |
8b6a786b |
failed |
2026-09-23 12:06 |
BYOK |
a7725915 |
interrupted |
2026-09-22 10:17 |
BYOK |
2fa8e715 |
interrupted |
2026-09-22 08:44 |
官方 qfmodel |
eeb6aeec 8361ed71 88cd72ac 6ac4c466 f01266b0 21c36be6 9ba76c02 |
failed |
2026-09-22 08:43 |
官方 qfmodel(7 个会话同一时刻) |
691d1ef1 |
interrupted |
2026-09-22 00:58 |
BYOK |
5ff997c2 |
interrupted |
2026-09-20 13:32 |
官方 qfmodel |
57b64f75 |
interrupted |
2026-06-19 16:07 |
auto |
9072e5a1 |
interrupted |
2026-06-19 16:06 |
auto |
最后两行是本 issue 最硬的证据:这两条 interrupted 距今约 3 个月,期间本机至少启动 17 次桌面实例,它们从未被恢复、也从未被清理。
取证 C:17 次启动,恢复计数全为 0
grep -ho 'Execution queue initialization completed {"recoveredInterruptedTurnCount":[0-9]*}' \
"$APPDATA/com.qoder.app.stable/logs/"*/main.log | sort | uniq -c
# 17 Execution queue initialization completed {"recoveredInterruptedTurnCount":0}
覆盖实例(目录名前缀,UTC):
20260918-054551 20260918-054603 20260918-113857 20260918-131843 20260918-143028
20260921-032052 20260921-032139 20260921-111428 20260922-020345 20260922-090645
20260922-153631 20260923-032124 20260923-081713 20260923-082127 20260923-101045
20260923-130542 20260923-131410
同一窗口内有 103 个会话以 process.exiting exit_code=1 结束,其中 16 个是运行 ≥20 分钟的长会话(见取证 D)。也就是说"该入队的时机"至少出现过 16 次,队列却始终 0 行、恢复数始终 0。
取证 D:≥20 分钟被异常终止的长会话(run_id 清单)
~/.qoder/logs/runs/<run_id>/manifest.json 的 argv 带 --model,据此区分通道;qodercli.log 末行均为 exit_code=1。
| run_id |
模型通道 |
存活 |
终止时刻 (+08:00) |
退出原因 |
2026-09-20T21-32-24-286+08-00-fsi4wa-p18116 |
官方 qfmodel |
20min |
09-20 21:52:56 |
fatal_error |
2026-09-21T11-23-13-567+08-00-bkdmv1-p24280 |
官方 qfmodel |
275min |
09-21 15:58:33 |
fatal_error |
2026-09-21T19-20-22-155+08-00-gsn1ai-p8408 |
官方 qfmodel |
41min |
09-21 20:02:06 |
fatal_error |
2026-09-21T19-28-31-979+08-00-qzyit4-p8408 |
BYOK |
34min |
09-21 20:02:25 |
fatal_error |
2026-09-21T22-26-47-192+08-00-0fswgb-p8408 |
官方 qfmodel |
636min |
09-22 09:02:49 |
fatal_error |
2026-09-21T22-34-48-859+08-00-0xakfu-p8408 |
官方 qfmodel |
628min |
09-22 09:02:49 |
uncaught_exception(worker runtime main()) |
2026-09-22T07-52-27-633+08-00-tnuuek-p8408 |
官方 qfmodel |
24min |
09-22 08:16:32 |
fatal_error |
2026-09-22T10-05-26-971+08-00-8i59u4-p25008 |
官方 qfmodel |
399min |
09-22 16:44:18 |
fatal_error |
2026-09-22T10-13-34-321+08-00-z1b88n-p25008 |
BYOK |
77min |
09-22 11:30:55 |
fatal_error |
2026-09-22T13-23-48-448+08-00-p0r7fr-p25008 |
BYOK |
31min |
09-22 13:54:27 |
fatal_error |
2026-09-22T17-07-34-598+08-00-59z4rv-p9504 |
BYOK |
24min |
09-22 17:32:01 |
fatal_error |
2026-09-23T18-16-28-168+08-00-laibcn-p21636 |
BYOK |
37min |
09-23 18:53:14 |
fatal_error |
2026-09-23T19-06-01-268+08-00-t1zyse-p21636 |
BYOK |
20min |
09-23 19:26:17 |
fatal_error |
2026-09-23T20-06-44-652+08-00-crptze-p21636 |
BYOK |
35min |
09-23 20:42:27 |
fatal_error |
2026-09-23T20-22-40-696+08-00-d7gfy5-p21636 |
BYOK |
43min |
09-23 21:05:39 |
fatal_error(紧随 0.4.1 自动更新重启) |
2026-09-23T20-38-31-583+08-00-s211aq-p21636 |
BYOK |
27min |
09-23 21:05:39 |
fatal_error(同一次重启,同一秒) |
典型收尾(...d7gfy5-p21636/qodercli.log):
2026-09-23T21:05:38.466+08:00 INFO debug.message [HeadlessSession] stdio permission channel closed; failing closed in-flight permission asks
2026-09-23T21:05:39.573+08:00 WARN process.exiting exit_code=1 reason="fatal_error" message="Headless session exited with error" source="gemini.exitHeadlessMode" uptime_ms=2578877
把 16 个终止时刻与取证 C 的实例启动时刻做 ±3 分钟对齐:只有 09-23 21:05 的 2 例(同一次自动更新重启)能对上,其余 14 例终止时桌面进程一直存活。所以"只有自动更新会杀会话"不成立,worker 自身失败路径同样会丢弃轮次。
取证 E:能力齐备,只是未接通
对 1116 个 run 的 manifest.json 统计 argv:
--resume:80 次 —— CLI 层的续跑入口确实实现了;
--continue:0 次;
--no-session-persistence:255 次(23%) —— 这类 run 从设计上不可续跑。想确认:后台/长任务是否也会落到这个开关上?如果是,它本身是一条独立成因。
取证 F:用 Qoder 自己的会话 API 读,历史和工具状态都在,但活动轮次是空的
调用 mcp__builtin__read_chat_session(即产品自带的会话读取接口,非我手工解析文件)读取取证 B 中的 5ff997c2:
{
"sessionId": "5ff997c2-...",
"runtimeState": "cold",
"activeTurnId": null,
"queuedTurnCount": 0,
"pendingInteractionCount": 0,
"revision": 0,
"messages": [
{ "role": "user", "status": "completed", "text": "(原文已省略:含本地项目名)" },
{ "role": "assistant", "status": "interrupted",
"tools": [ { "name": "Glob", "status": "completed" },
{ "name": "Bash", "status": "failed" } ] }
]
}
对照结论:
- 会话本身没有丢:
runtimeState: cold(可再唤醒)、消息与每个工具调用的终态都还在。
- 但那一轮没有任何续跑凭据:
activeTurnId: null、queuedTurnCount: 0、revision: 0(注意 chat_session_turn_states 表里正好有个 revision 列,而全表 0 行)。
- 也就是说"恢复"所需的一切材料都齐了(历史 + 已完成到哪一步 + 哪个工具失败),只差把这轮重新挂回执行队列。用户此时只能重发一遍请求,而模型对"我刚才已经 Glob 到哪、哪个 Bash 失败了"没有任何凭据可依。
历史数据是在的(所以修复成本应该不高)
| session |
存活 |
DB 内消息数 |
status 分布 |
recap |
c21a6f37 |
636min |
73 |
completed 70 / interrupted 3 |
有 |
47ed4c11 |
628min |
34 |
completed 29 / failed 5 |
有 |
01c060d6 |
275min |
393 |
completed 382 / failed 1 / … |
有 |
4726c108 |
43min |
44 |
completed 37 / failed 2 / … |
无 |
消息、序列号(max(sequence) 与条数一致)、recap 都在,16 个会话的 run 级转录文件也全部在盘。缺的就是"最后那一轮的可续跑投影"。
最小复现
- 新建会话,安排一个持续 ≥20 分钟的任务(例如让工具循环 sleep)。
- 任务进行中二选一:A. 强杀桌面进程(或等一次自动更新重启);B. 不动,等 worker 自身
exit_code=1。
- 重启 Qoder。
- 查三处:
recoveredInterruptedTurnCount 是否为 0;pending_turn_projections 是否出现该 turn_id;chat_session_messages 里那一轮是否只是被写成 interrupted 就结束。
本机结果:第 4 步三项依次为 0 / 无 / 是。
尚未证实(不想把没验证的写成结论)
pending_turn_projections = 0 行 分不清"从未写入"与"正常收尾时被清空"(run 日志里找不到任何 projection 写入/清理痕迹,相关关键字 0 条命中)。但取证 B 里 2026-06-19 那两条 interrupted 至今 3 个月未被恢复、队列仍空,倾向于"入队这一步根本没发生"。请从代码侧确认。
- 我没有实测"重新发一条消息给这些会话,能否带回原有上下文继续",也没测 CLI 层手工
--resume <session_id> 的效果(前者会写入你的真实会话、后者需要真实模型调用与工具执行,代价与副作用都较高,计划作为补充实验更新在本 issue 下)。取证 F 只证明了"续跑所需的凭据没有被挂上",不证明"任何形式的手动续跑都不可能"。
- UI 是否有恢复入口:未验证。
- 取证 D 那 16 例各自的直接死因,日志未记 reason 字段(仅见死前 1.4–3.8 秒一次
submitMessage() done reason=error)。本 issue 不对死因归因,只主张"死亡之后不可恢复"这一件事。
影响
长任务是 Qoder 主打能力之一(后台会话、/goal、定时唤醒会话)。当前状态下中断代价是整轮作废,且用户不会收到"这一轮没有被保存"的提示。本机 6 天窗口内 16 个长会话累计 runHeadlessSession 存活 2351 分钟(约 39.2 小时) 作废(注:墙钟时长,含轮次间等待,非纯计算时间)。
建议的最小修复面
- turn 被判定中断时向
pending_turn_projections 落一行(schema 里的 state / attempt_count 看起来正为此准备);
- 启动时把
status='interrupted' 且无投影的行纳入恢复或提示路径,而不是静默停在终态;
- 对走
--no-session-persistence 的长任务补一条日志,明示该 run 不可续跑。
附件
- 上述 16 个
run_id 的 qodercli.log + manifest.json
- 17 个实例的
main.log
- 采集脚本(本报告全部数字由脚本产出,只读、无副作用)
这些日志仅通过官方私密渠道提供(应用内"问题反馈"或官方邮箱),不会上传到公开评论区。 原因是日志原文包含进程命令行参数(可能含第三方服务 token)、本机绝对路径与完整会话内容。任何人都可以用本文列出的只读命令在本地复算出同样的数字,无需依赖我的附件。
隐私处理
已去除:账号 ID、API key、会话标题(含本地项目路径)。BYOK 仅保留 byok(sensenova-6.8-flash-lite) 形式标识,profile_id 在正文中省略。
一句话摘要
Qoder 已经实现了续跑所需的全部零件——CLI 的
--resume、中断恢复队列表pending_turn_projections、轮次状态字段(含interrupted取值)、启动时的recoveredInterruptedTurnCount打点——但本机可查的全部历史显示:被明确标记为interrupted的 37 条轮次,没有一条进入过恢复队列,重启后也没有恢复过任何一轮。其中 2 条自 2026-06-19 起一直停在interrupted,跨越此后所有次启动。会话历史本身是完整落库的(单会话最多 393 条消息),所以这不是"数据丢了",而是恢复链路没接上。环境
I:\qoder\.qoder-versions\)cli_version: 1.1.57%APPDATA%\com.qoder.app.stable~/.qoder/logs/runs/,本机留存 1116 个 run,跨度 2026-09-18 → 2026-09-23期望 vs 实际
期望:turn 执行中被杀死(worker 崩溃、App 重启、自动更新重启)后,该轮次进入恢复队列,重启时自动续跑,或至少向用户提供"继续这一轮"的入口。
实际:轮次被写成
interrupted终态就停在那里。恢复队列 0 行,重启恢复计数 0,用户侧只能从头重跑。取证 A:状态机知道"这一轮被打断了",但恢复队列为空
要点:
pending_turn_projections的 schema 本身就是为续跑准备的:session_id, turn_id, message_id, source, message_json, message_status, state, attempt_count, created_at, updated_at—— 现在 0 行。chat_session_messages里有 37 条status='interrupted':App 自己判定并记录了"这一轮是被打断的",但没有任何一行投影进恢复队列。active_turn_projection_items有 6 行,可排除"连错库 / 写入路径不通"这类误判。取证 B:17 个会话的最后一轮至今停在非终态
返回 17 行(会话标题已去除,避免暴露本地项目名):
27fc0bfbcb28e5eb4726c1088b6a786ba77259152fa8e715eeb6aeec8361ed7188cd72ac6ac4c466f01266b021c36be69ba76c02691d1ef15ff997c257b64f759072e5a1最后两行是本 issue 最硬的证据:这两条
interrupted距今约 3 个月,期间本机至少启动 17 次桌面实例,它们从未被恢复、也从未被清理。取证 C:17 次启动,恢复计数全为 0
覆盖实例(目录名前缀,UTC):
同一窗口内有 103 个会话以
process.exiting exit_code=1结束,其中 16 个是运行 ≥20 分钟的长会话(见取证 D)。也就是说"该入队的时机"至少出现过 16 次,队列却始终 0 行、恢复数始终 0。取证 D:≥20 分钟被异常终止的长会话(run_id 清单)
~/.qoder/logs/runs/<run_id>/manifest.json的 argv 带--model,据此区分通道;qodercli.log末行均为exit_code=1。2026-09-20T21-32-24-286+08-00-fsi4wa-p181162026-09-21T11-23-13-567+08-00-bkdmv1-p242802026-09-21T19-20-22-155+08-00-gsn1ai-p84082026-09-21T19-28-31-979+08-00-qzyit4-p84082026-09-21T22-26-47-192+08-00-0fswgb-p84082026-09-21T22-34-48-859+08-00-0xakfu-p8408main())2026-09-22T07-52-27-633+08-00-tnuuek-p84082026-09-22T10-05-26-971+08-00-8i59u4-p250082026-09-22T10-13-34-321+08-00-z1b88n-p250082026-09-22T13-23-48-448+08-00-p0r7fr-p250082026-09-22T17-07-34-598+08-00-59z4rv-p95042026-09-23T18-16-28-168+08-00-laibcn-p216362026-09-23T19-06-01-268+08-00-t1zyse-p216362026-09-23T20-06-44-652+08-00-crptze-p216362026-09-23T20-22-40-696+08-00-d7gfy5-p216362026-09-23T20-38-31-583+08-00-s211aq-p21636典型收尾(
...d7gfy5-p21636/qodercli.log):把 16 个终止时刻与取证 C 的实例启动时刻做 ±3 分钟对齐:只有 09-23 21:05 的 2 例(同一次自动更新重启)能对上,其余 14 例终止时桌面进程一直存活。所以"只有自动更新会杀会话"不成立,worker 自身失败路径同样会丢弃轮次。
取证 E:能力齐备,只是未接通
对 1116 个 run 的
manifest.json统计 argv:--resume:80 次 —— CLI 层的续跑入口确实实现了;--continue:0 次;--no-session-persistence:255 次(23%) —— 这类 run 从设计上不可续跑。想确认:后台/长任务是否也会落到这个开关上?如果是,它本身是一条独立成因。取证 F:用 Qoder 自己的会话 API 读,历史和工具状态都在,但活动轮次是空的
调用
mcp__builtin__read_chat_session(即产品自带的会话读取接口,非我手工解析文件)读取取证 B 中的5ff997c2:{ "sessionId": "5ff997c2-...", "runtimeState": "cold", "activeTurnId": null, "queuedTurnCount": 0, "pendingInteractionCount": 0, "revision": 0, "messages": [ { "role": "user", "status": "completed", "text": "(原文已省略:含本地项目名)" }, { "role": "assistant", "status": "interrupted", "tools": [ { "name": "Glob", "status": "completed" }, { "name": "Bash", "status": "failed" } ] } ] }对照结论:
runtimeState: cold(可再唤醒)、消息与每个工具调用的终态都还在。activeTurnId: null、queuedTurnCount: 0、revision: 0(注意chat_session_turn_states表里正好有个revision列,而全表 0 行)。历史数据是在的(所以修复成本应该不高)
c21a6f3747ed4c1101c060d64726c108消息、序列号(
max(sequence)与条数一致)、recap 都在,16 个会话的 run 级转录文件也全部在盘。缺的就是"最后那一轮的可续跑投影"。最小复现
exit_code=1。recoveredInterruptedTurnCount是否为 0;pending_turn_projections是否出现该turn_id;chat_session_messages里那一轮是否只是被写成interrupted就结束。本机结果:第 4 步三项依次为 0 / 无 / 是。
尚未证实(不想把没验证的写成结论)
pending_turn_projections = 0 行分不清"从未写入"与"正常收尾时被清空"(run 日志里找不到任何 projection 写入/清理痕迹,相关关键字 0 条命中)。但取证 B 里 2026-06-19 那两条interrupted至今 3 个月未被恢复、队列仍空,倾向于"入队这一步根本没发生"。请从代码侧确认。--resume <session_id>的效果(前者会写入你的真实会话、后者需要真实模型调用与工具执行,代价与副作用都较高,计划作为补充实验更新在本 issue 下)。取证 F 只证明了"续跑所需的凭据没有被挂上",不证明"任何形式的手动续跑都不可能"。submitMessage() done reason=error)。本 issue 不对死因归因,只主张"死亡之后不可恢复"这一件事。影响
长任务是 Qoder 主打能力之一(后台会话、
/goal、定时唤醒会话)。当前状态下中断代价是整轮作废,且用户不会收到"这一轮没有被保存"的提示。本机 6 天窗口内 16 个长会话累计runHeadlessSession存活 2351 分钟(约 39.2 小时) 作废(注:墙钟时长,含轮次间等待,非纯计算时间)。建议的最小修复面
pending_turn_projections落一行(schema 里的state/attempt_count看起来正为此准备);status='interrupted'且无投影的行纳入恢复或提示路径,而不是静默停在终态;--no-session-persistence的长任务补一条日志,明示该 run 不可续跑。附件
run_id的qodercli.log+manifest.jsonmain.log这些日志仅通过官方私密渠道提供(应用内"问题反馈"或官方邮箱),不会上传到公开评论区。 原因是日志原文包含进程命令行参数(可能含第三方服务 token)、本机绝对路径与完整会话内容。任何人都可以用本文列出的只读命令在本地复算出同样的数字,无需依赖我的附件。
隐私处理
已去除:账号 ID、API key、会话标题(含本地项目路径)。BYOK 仅保留
byok(sensenova-6.8-flash-lite)形式标识,profile_id在正文中省略。