Repository navigation
fix(desktop): refuse a slash submission when the command source fails, and give compaction its own deadline (#795) - #848
Merged
Conversation
#795) `resolveComposerCommand` answered `null` both for "no such command" and for a failed `composer/commands` read, so the submit path could not tell them apart and sent `/compact` to the model as literal prompt text whenever the source was down. The model read that as an instruction and acted on it. Resolution now answers with three outcomes: resolved (builtin/plugin/extension dispatch), unknown (templates, aliases, and id-less entries keep the prompt path), and unavailable. An unavailable source refuses the submission, keeps the draft, and reports `chat.slashCommandSourceUnavailable`; the failed read is not cached, so the next submit retries it, while a warm cache still resolves through a source blip. The decision lives in the React-free `resolveSlashDispatch` so it is testable on its own. Locked by `apps/desktop/test/slash-command-source.test.mjs` (fails on the previous code) and by a new refusal case in the composer-submission E2E fixture, which reaches the real Composer and asserts nothing was sent.
…dict (#795) `agent.compact` is a blocking RPC that spends a whole model summary request inside the sidecar, but it ran under the flat 130s transport default. An 888KB context needed ~158s, so Electron reported `sidecar RPC timeout` while the sidecar kept summarizing and persisted the checkpoint: the user was told the compaction failed, and the next turn proved it had succeeded. The deadline now follows the ceilings the sidecar does enforce — one 180s stream watchdog per attempt, `1 + 3` attempts, the 2s/4s/8s retry backoff, and transport slack (`AGENT_COMPACT_RPC_TIMEOUT_MS`, per-method rather than a wider global default, mirrored by `plugin-timeout-budgets.test.mjs`). Both host paths (Electron `agentCompact` IPC and `RuntimeService.compact`) also stop treating a lost reply as the sidecar's verdict: on a transport timeout they re-read the durable `session.compaction` record through `compactionRecordId`, report success when a new checkpoint landed, log the mismatch, and rethrow the timeout otherwise. A verdict the sidecar reported itself is never reconciled. Pinned by `packages/host-runtime/src/runtime-service.test.ts`, `packages/shared/src/protocol.test.ts`, and `apps/desktop/test/plugin-timeout-budgets.test.mjs`.
Every durable `session.get` window was taken as the truth and cached. The host answers such a read from a transcript file it may be rewriting, so a window that came back empty for a session with 2700 messages was stored as an empty snapshot; the sidebar's hover prefetch then re-served that emptiness on every later open and the pane stayed blank until the app restarted. Nothing retried a read and nothing told a failed or stale read apart from an empty conversation. A read is now judged against the session's own count (`sessionReadLooksEmpty`): zero messages for a session the sidebar counts as having history triggers one more read, then keeps the snapshot the user already has, and otherwise reports `chat.sessionTranscriptEmpty` instead of committing an empty transcript. The suspicious page is never written to the transcript cache, so a hover prefetch cannot poison every later open. This is a defense, not the host-side root cause: the transcript rewrite and the read side that answers "session exists, no messages" are unchanged. Pinned by `apps/desktop/test/session-transcript-empty-read.test.mjs`.
- `chat.slashCommandSourceUnavailable` and `chat.sessionTranscriptEmpty` in all eight locales (locale typing requires every catalog to carry the key). - `03-runtime/01-ipc-protocol.md` §5.4 records the compaction deadline and the durable-verdict rule; §13c records the three resolutions of a typed `/name`. - `03-runtime/07-process-model.md` states the per-method deadline rule. - `04-ux/09-interaction-patterns.md` records that an empty read for a session with history is read as unreadable, not empty. - Decisions log D613/D614/D615, and the E2E plan gains the four scenarios with the exact commands that automate them. zh-CN mirrors all of it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
摘要
修
#795里两条已确认的缺陷,并给巨型会话空白视图补读侧防御。Refs #795(不 closes:issue 里还有宿主读侧可区分错误、搜狗 IME 下
/被吞两条未处理,见文末)缺陷与根因
1. slash 指令被静默降级成自然语言发给模型(最高危)
resolveComposerCommand在api.composerCommands()抛错时catch { return null; },而useComposerSubmit拿不到 command 就把startsWith("/")的文本当普通 prompt 提交。RPC 一失败,/compact就被当字面文本发给模型,issue 报告者实测模型据此改了代码。resolveComposerCommand改为三态resolved / unknown / unavailable(不再返回 null,且失败不写 TTL 缓存 → 下次回车可重试)。新增与 React、store 无关的纯逻辑模块slash-dispatch.ts承载决策:unknown(别名/模板/无 id)继续走提示词路径;unavailablefail-closed —— 保留草稿、提示chat.slashCommandSourceUnavailable、绝不调用sendPrompt。2.
agent.compact走默认 130s RPC 超时,真实耗时约 158srpc-timeouts.ts只有tools.execute有专属超时。大上下文下宿主先报错、sidecar 实际跑完并落盘,产生虚警。新增AGENT_COMPACT_RPC_TIMEOUT_MS(3×180s流看门狗 + 重试退避 + 余量)并按方法路由,未改动全局 130s。宿主不再把传输超时当成 sidecar 的判定:超时后重读持久化的session.compaction,新检查点已落盘则返回accepted:true并记 warn,未落盘原样抛错,sidecar 自己报的错(如CONTEXT_COMPACTION_FAILED)绝不改写。3. 巨型会话视图永久空白(读侧防御)
宿主侧根因(Windows 上
remove_file+rename的非原子替换窗口)已由 #671 修成单次原子rename,且不在 v0.15.1、已在 v0.15.2+;报告者当时用的 0.15.1 确实命中。仍存在的缺陷在读侧与渲染层:瞬时空窗口被当成真实的空会话并缓存(sidebar hover prefetch 复用has(id)早退),且不区分"读失败"与"真的没消息",于是一旦命中就永久空白、只能重启。本 PR 补防御(不是宿主根因修复):
sessionReadLooksEmpty判定"sidebar 计为有历史却读回零消息",此时再读一次、随后保留用户已有快照,否则显示chat.sessionTranscriptEmpty且不提交空会话;持久化读取缓存拒绝写入这类可疑空页,悬停预取无法污染。测试
node --test apps/desktop/test/slash-command-source.test.mjs→ 7/7(回退源码后 5 项失败,已验证)apps/desktop/test/session-transcript-empty-read.test.mjs→ 4/4scripts/e2e/composer-submission.tsx增加拒绝用例;node scripts/e2e-composer-paste.mjs→ok:true(回退源码后该用例报a refused slash submission must not reach the send path: [... "/compact"])pnpm --filter @pi-desktop/shared test940/940、@pi-desktop/host-runtime46/46、@pi-desktop/agent-runtime917/917、@pi-desktop/i18n25/25node --test apps/desktop/test/*.test.mjs→ 2590/2590pnpm -r --if-present typecheck、pnpm lint通过文档
03-runtime/01-ipc-protocol.md§5.4 / §13c、03-runtime/07-process-model.md、04-ux/09-interaction-patterns.md、06-delivery/04-e2e-test-plan.md(+4 场景),decisions-log D613/D614/D615,均有docs/zh-CN/镜像;两个新 i18n key 覆盖全部 8 个语言。已知边界
session.get失败加可重试路径(当前HOST_OVERLOADED不重试)——建议单独开 issue。session-slice.selectSession在可疑空读时提前 return,会跳过 model pin / compaction 记录等收尾逻辑,属异常路径,评审请重点看这里。/变/、后唤不出指令的问题不在本 PR 范围。