# dsh-keep-going [English](README.md) | 中文 **不应该因为网关重启,就让所有未完成的对话都等着你再说一遍“继续”。** ## 痛点 几个对话本来都在正常工作。其中一个装了插件,要求重启 DSH。DSH 回来了,其它任务却没回来:历史记录还在,工作已经停了。你只能逐个找到它们,再说一次“继续”。 `dsh-keep-going` 根据 DSH 保存的对话记录恢复未完成的工作。不需要你用浏览器逐个打开对话,也不局限于通过本插件发起的重启。 重启仍会短暂断开进程和连接。这里保证的目标是**自动恢复任务**,不是网络连接永不断开,也不是把每个任务从头再做一遍。 ## 预期行为 | 重启前的状态 | DSH 再次启动后 | | --- | --- | | 原有目标是 `active` | 恢复它的对话,继续目标;不按最近活跃时间筛除。 | | 目标已暂停、受阻或完成 | 保持停止;也不能另外发送“继续”绕过这个状态。 | | 对话已被**归档** | 绝不恢复。归档就是有意搁置;恢复会整体跳过它。 | | 普通对话的任务被打断 | 恢复原历史,继续未完成部分。 | | 正在等用户回答问题 | 保留原问题,继续等;用户在原对话回答后才继续。 | | 任务已完成,或用户取消了任务 | 不重新执行。 | | 是你自己停下来的对话 | 保持停止;手动停止绝不会被当成重启。 | | 提供方报额度、余额或硬性请求错误 | 立刻停止并如实报告;等待不会增加额度,所以不重试。 | | 已经发出的继续任务失败,或继续过程中又发生一次重启 | 报告并停止;只有“又发生一次重启”才允许接着做,且同一任务最多自动尝试 3 次,之后交回给你。 | | 是另一个对话要求重启的 | 不接收那个对话的专属继续指令。 | 复制对话时,复制过来的历史不自动授权再执行一份父对话任务;新对话自己开始的新任务仍可恢复。 ## 安装 只通过 GitHub 分发,不发布到 npm。 ```sh dsh plugin --profile web add git+https://github.com/Carrick-K7/dsh-keep-going.git#0.2.6 ``` ## 何时生效,何时绝不打扰 **每次进程重启后只做一次恢复**,随后插件进入休眠: - 启动时扫描一次已保存的会话,恢复符合条件的原有工作,然后停下。 - 只有**被重启打断**的工作才属于恢复范围。DSH 正常运行时会留下轻量检查点,因此运行中崩溃仍能被发现;但在正常运行期间失败的任务——提供方报错、任务被判定 blocked、你自己停下的任务——都不属于重启遗留的工作:检查点会被收回,不会向你背后发送、重试或恢复任何东西。 - 正常运行期间**不做周期扫描,也不会自动重启对话**。你手动打开一个旧对话是正常操作,不是重启——插件不会因此唤醒或重新武装任何东西。 - **DSH 正常运行期间,插件对你的对话什么都不做。** 不写恢复记录、不恢复会话、不发消息、不加维护锁、不改目标状态;唯一的动作是每 500ms 检查一次是否有待执行的重启请求。上一次重启遗留的工作只作用于它所属的那个对话:提问中的对话继续等你回答,与那一次重启无关的对话绝不被触碰。 - 被重启打断的 `active` 目标只被**重新武装一次**,随后完全交给 DSH 自己的目标驱动器。驱动器之后因错误或上限把它解除,那是 DSH 正常的生命周期——插件不会在后台反复重新武装,只有下一次进程重启才会再次恢复。 - 唯一的例外:发起的重启正在等待运行中的任务(排水)时,插件会跟踪现场会话以保证退出安全。另外,属于 active 目标的旧工作一律移交目标驱动器续轮,不会收到一条通用的“继续”——它永远不会替别人的任务跑。 安装后重启 DSH。**必须有服务管理器在进程退出后重新启动 DSH。** 插件自己不启动替代进程。 测试版本为 DSH `0.1.5-rc.2`。当前 profile 必须提供原生会话、持久化会话查询、会话控制器、目标、工具和命令服务。缺少必要服务是部署错误,不能悄悄关闭恢复功能当作成功。 ## 重启与状态 - **`restart_harness`**:等待正在运行的任务,保存新收到的消息,再请求 DSH 正常退出。参数为只交给发起对话的 `continuePrompt`、`waitMs` 和 `force`。 - **`/restart`**:同样等待后重启,并正确记录输入命令的对话。 - **`cancel_harness_action` / `/cancel-restart`**:由发起对话撤销待执行的重启。斜杠命令不需要先让模型开始回答。第二个请求不能覆盖第一个请求的期限或指令。 - **`keep_going_status` / `/keep-going`**:查看重启进度和当前对话的恢复问题,包括尚未回答的问题。 - **`keep_going_clear_goal`**:移除指定对话里已停止的目标(paused/blocked/complete),历史保留为 tombstone,同时清掉它的恢复记录;仍在执行中的目标必须先暂停;active 但未在执行的目标会先记录一次暂停、再移除,两步都可审计。 默认情况下,**等到期限不代表有权中断一个正常任务**。没有 `force` 时,请求会继续等待,而不是被悄悄取消。明确传入 `force: true` 后,才允许到期退出,并先保存未完成工作。长时间运行的工具或模型请求不会仅因 60 秒没有输出就被判定为“卡死”。 **已安排的重启绝不冻结 DSH。** 等待期间所有对话照常工作:新消息立刻开轮,目标保持 armed,不加任何锁。真正退出时仍在跑的工作由同步快照保存,重启后继续。维护锁只在交接的那一刻持有(先保存持久状态,且确认没有任务在跑),因此一个等待别的长任务的请求不可能拖住整个部署。 如果正在等用户回答,可以保存问题后重启,不必无限等用户在线。重启不会替用户回答;恢复后保持 `waiting-user`,直到用户答复。 **关闭与重启不同。** 停止 DSH 请使用服务管理器,例如 `systemctl stop `。兼容保留的 `shutdown_harness` 会说明这一点,不执行退出:在 `Restart=always` 下直接退出其实会重新启动,不能声称已经关闭服务。 ## 提问与批准 插件识别 DSH 的 `ask_user_question` 调用和批准请求记录。系统补写的“工具被中断”不是用户答案;目标续轮和插件生成的“继续”也不算用户答复。 重启后,原问题仍保留在对话历史和恢复状态中。在原对话回复即可继续。插件**不会**自动重复发送问题、重建已经失效的浏览器弹窗,或猜测用户选择。 安全批准不是普通答案。以前的一次性 `allowed-once` 不能被重放,用来执行结果不确定的操作。未完成的批准请求会明确保留,需要通过 DSH 正常的批准机制重新作出决定。 ## 恢复与重复执行 插件主动读取已保存的对话,通过 DSH 的会话控制器恢复符合条件的原对话,并在继续前再次检查当前任务状态。恢复记录会保留到工作结果可靠保存;读取文件不等于删除它。因此,再次重启或暂时发送失败不会抹掉待恢复的任务。 恢复使用固定的消息标识,保留等待处理的用户消息顺序,并把发起者指令与其它对话严格分开。恢复过程中发生关闭或取消,也会停止尚未执行的恢复动作。 **不能对所有外部操作保证只执行一次。** 如果工具刚发了消息或部署了代码,进程就崩溃,结果还没保存,操作是否成功可能无法确认。继续提示会要求助手先核实已完成操作,不要盲目重做。更强的保证需要工具或聊天适配器自己支持幂等操作。 ## 目标与停止原因 `active` 目标是仍要执行的任务,不受浏览器是否打开、网关是否手动重启影响,也不以最近有没有活动来猜测用户是否放弃。但 active 不等于现在就可以调用模型:正在等用户回答时必须继续等。 已暂停、完成或受阻的目标保持原样。同一个任务最多自动尝试 3 次;超过后标记为受阻,并保留提供方给出的原始报错,等你处理。恢复不按固定间隔盲目重试:它读的是那一轮实际发生了什么,而不是“安静了多久”。不绕过 DSH 现有安全限制。额度、凭据、输出上限、目标轮数上限以及不可重试的问题,会显示在恢复状态里,不会被说成“任务已经完成”。处理原因后,通过原对话或目标控制恢复任务。 ## 设置 在 `settings.yaml` 的 `dsh-keep-going` 段配置: ```yaml dsh-keep-going: drainTimeoutMs: 600000 # 首次等待 10 分钟;到期中断必须显式 force:true。 retryMinMs: 1000 retryMaxMs: 60000 restartExitCode: 75 # 服务管理器应配置为重启这个退出码。 restartBurstLimit: 5 restartWindowMs: 60000 ``` 短时间反复重启时,会等这个时间窗过去再恢复;不会删除任务或永久禁止继续。`stateDirectory` 可为独立 profile 指定单独的恢复目录,修改它后需要重启 DSH。 旧配置 `stuckAgentMs`、`stormLimit`、`stormWindowMs` 和 `scanIntervalMs` 不再使用。尤其是,沉默不再成为丢弃任务的理由;恢复也不是周期性的,因此不再有扫描间隔。 ## 部署与保存的文件 兼容的 systemd 最小配置: ```ini [Service] ExecStart=/path/to/dsh web --host 127.0.0.1 --port 3080 Restart=always RestartSec=3 ``` 也可以显式配置服务管理器重启退出码 75。若使用 `on-failure`,不要只加 `SuccessExitStatus=75`,却没有同时配置该退出码的强制重启行为。 默认恢复文件:`$DSH_HOME/dsh-keep-going/recovery.json`。其中保存待恢复任务、重试状态,以及必要的待处理输入或发起者专属指令。新建目录和文件使用私有权限;写入采用文件同步、原子替换,以及系统支持时的目录同步。损坏的记录会报错并保留,不会直接清空。 一个恢复目录只供一个 DSH 进程使用;它不协调多个独立进程同时写入。旧 `restart.json` 和 `state.json` 不再用于投递消息;升级后依据 DSH 已保存的历史识别未完成工作。 ## 聊天平台,包括飞书 这是 DSH 恢复插件,不是飞书连接器。聊天连接器需要保存 DSH 会话与原群聊、私聊或线程的对应关系,并把后续回复送回原处。恢复保持会话标识,不猜测新的发送对象。 自动化测试通过适配器替身和真实 DSH 消息事件,验证不同对话回复到各自原 chat/thread。**测试没有调用飞书 API,不能证明某个具体连接器已经通过飞书收发验收,也不能保证网关离线期间发送的消息一定被接收。** 这些需要在对应连接器中单独验证。 ## 开发与验证 ```sh pnpm install --frozen-lockfile --ignore-scripts pnpm check pnpm test ``` 测试使用真实的 Cordis、DSH 会话、目标、提问工具与本地持久化;只有模型和聊天适配器是可控制的测试替身。测试会终止并重启独立子进程,验证不连接浏览器也能恢复、再次中断不丢记录、固定恢复标识、指令隔离、等待用户和失败重试。测试不操作线上会话,也不发送外部聊天消息。 也可以使用已有 DSH 安装的依赖: ```sh DSH_NATIVE_TEST_RESOLVE_FROM=/path/to/install/package.json pnpm test ``` 该路径用于选择安装目录的模块树。显式指定路径后,缺少依赖会使测试失败,不会悄悄跳过。 ## 许可证 MIT © 2026 Carrick