makecindy / makecindy/cindy

缺一条会话级 notice 的持久呈现通道,非终止提示只能活到下一条事件

Open
#1,603 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
2.7k
Forks
395
Avg merge
21h 48m
Merged PRs (30d)
776

Description

## 现象

Agent runtime 想告诉用户一件「会话级状态」时(不是 turn 失败、也不是自动重试进度),desktop 侧没有能持久呈现的通道。当前唯一可用的是非终止 `error` 事件,但它**只活到下一条事件**:

`makerChatStore.ts:2321` —— `handleStreamEvent` 对**任何非 error 事件**都清掉 `recoverableError`:

```ts
const state = event.type === 'error' || inputState.recoverableError == null
? inputState
: { ...inputState, recoverableError: null };
```

所以一条提示在下一个 `text` / `tool_result` / `status` 到达时就从 ErrorBanner 消失了。而 Auto 档被拒的工具后面通常紧跟 agent 的解释文本,等于提示一闪而过。

## 为什么不能靠落库补上

`role='error'` 行(由 `ErrorMessageCard` 渲染)是持久的,但它的落库入口 `onTurnErrorEvent` 不适合承载这类提示,#1597 尝试过并撤回了,两个原因:

1. **per-turn dedup 会吃掉真正的失败记录**(`messagePersistBroadcaster.ts:1128-1131`):

```ts
const hasTurnIdentity = turnId !== null || turnDedupId !== null;
if (lastT !== undefined && (hasTurnIdentity || capturedAt - lastT < DEDUP_WINDOW_MS_MESSAGE)) {
return undefined;
}
```

`turnId = agentMeta.requestId ?? agentMeta.uuid`。提示先用掉这个身份后,**同一 turn 后续真正的终止 error 会被静默丢弃** —— 用户失去这一轮的失败记录。这是数据丢失,不是体验问题。

2. **不广播**(`messagePersistBroadcaster.ts:1207` 的 `{ shouldBroadcast: () => false }`):`local-db:session:error-persisted` 对当前打开的会话刻意不 invalidate,所以正在看这个会话的用户仍然看不到,要等下次重开。

## 期望

一条真正的「会话级 notice」通道,语义上区别于既有三者:

| | 语义 | 生命周期 |
|---|---|---|
| `role='error'` 行 | turn 失败终态 | 持久,每 turn 至多一条 |
| `recoverableError` / ErrorBanner | live 过程态 | 到下一条事件 |
| **缺的这个** | 会话级状态说明 | 持久,可多条,不占用 turn 的 error 身份 |

它至少要满足:落库且能被 live 流看到(广播或 invalidate)、不与 `onTurnErrorEvent` 的 per-turn dedup 共用身份、renderer 有对应的呈现形态(可复用 `ErrorMessageCard` 的视觉,但不该复用 `role='error'`)。

现成的消费方已经有两个:

- Auto 档「审阅器不可用」(#1574 / #1597);
- `maker:auto-permission:fallback` 这条**已经没有发送端**的死代码通道(`channels.ts:752` + preload fanOut + `autoPermissionFallbackToast.ts` + 四语 `newChat.permissionSelector.autoFallback`),是 #596 改造后的残留。它想做的正是这件事,只是 payload 语义(`to: 'ask'`)已经不成立,且它经 device-link 转发、属于跨端 wire,复活要走 `docs/dev-rules/protocol-and-submodules.md` 的专项审查。这条 issue 一并决定它是复活成新语义还是整套删掉。

## 修好之前用户会遇到什么

Auto 档下审阅器故障(provider 被停用、目录解析失败、reviewer 超时)时:

- **desktop**:提示出现在 ErrorBanner,但下一条 agent 事件就会清掉它。#1597 已把「整个会话一次」改成「每轮一次」,所以每轮都有一次机会看到,但如果那一轮的 block 发生在早期、后面还有大量输出,仍然可能错过。错过后用户看到的就是工具接连被拒、原因只在模型的转述里。
- **IM 渠道**(Slack / Telegram / 飞书)**不受影响**:#1597 已在 `turnRetryNotice.ts` 补了独立映射,提示会进渠道的进度区。

## 验收标准

- 一条会话级 notice 发出后,**在当前打开的会话里立即可见**,且后续 text / tool_result / status 事件不会清掉它;
- 同一 turn 内发过 notice 之后,该 turn 真正的终止 error 仍然正常落库(回归测试覆盖这一点);
- 决定 `maker:auto-permission:fallback` 的去向:复活(含 payload 语义与 device-link 兼容方案)或删除(含四语文案与 preload/channel 清理);
- Auto 档「审阅器不可用」改接这条通道后,#1597 里那条「非终止 error + 每轮重置」的临时办法可以撤掉。

拆自 #1574 / #1597(PR #1597 的 review 里 codex 连续两轮指出了这一层,两条 P1 均已在此收口)。

Contributor guide

Open the contributing guide

Research direction

Start by tracing makerChatStore.ts:2321, messagePersistBroadcaster.ts:1128-1131 and 1207, then inspect channels.ts:752, preload fanOut, autoPermissionFallbackToast.ts, and docs/dev-rules/protocol-and-submodules.md. Done means a separate persisted, live-visible session notice survives later events, does not suppress a turn error, and the fallback channel is either compatibly restored or fully removed with its translations and preload/channel cleanup.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
desktop
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.