makecindy / makecindy/cindy

bug: 手机端 unknown presence 被当作离线,远程历史读取被静默拒绝(加载更早消息无效且无任何提示)

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

Description

## 问题描述 / What happened

**实际行为**:手机端打开一个远程任务时,「加载更早消息」无效、历史只显示最近一小段;屏幕上**既没有错误提示、也没有连接横幅**,看起来像"这个任务本来就没有更早历史"。

**期望行为**:只有在被控端**明确不可用**(presence 显式 `false`)时才停止远程读取;状态**未知**时应照常发起读取(失败则走既有错误提示)。未知不等于离线。

**根因**:presence 是三态(`true` / `false` / unknown),但手机端会话页把 unknown 当成了不可用,且拒绝路径全程静默。

同一文件内已是两套相反口径:

```ts
// apps/mobile/app/sessions/[sessionId].tsx:1033
const remoteHistoryAvailable = status === 'online' && getPresenceAvailability(deviceId) === true;

// apps/mobile/app/sessions/[sessionId].tsx:1034-1036
// Unknown presence while connecting is loading, not evidence of a lost computer.
const showCachedHistoryNotice = status !== 'connecting'
&& (status !== 'online' || getPresenceAvailability(deviceId) === false);
```

`:1036` 明确注释「未知不是离线证据」(所以不弹横幅),`:1033` 却要求显式 `true`,把未知当作不可用——于是既不读取、也不提示。

拒绝链路(全部静默返回,不置 loading / error):

- `apps/mobile/src/session/remoteHistoryView.ts:12` — `view.setNetworkAvailable(networkAvailable)`,`networkAvailable` 即上面的 `remoteHistoryAvailable`
- `packages/maker-shared/src/historyViewController.ts:60` — `isActive = () => this.active && this.networkAvailable`
- `:127` — `readPage` 首行 `if (!this.isActive() || ...) return;` → 不发请求、不置 `loading`、不置 `error`
- `:117`(`invalidate`)、`:213`(`loadDetails`)同样静默返回
- `apps/mobile/app/sessions/[sessionId].tsx:3258` — `if (!remoteHistoryAvailable) { setLoading(false); return; }`
- `:4610` — `if (!remoteHistoryAvailable) return;`(点「加载更早消息」直接吞掉)

**unknown 会长期存在**,不是瞬时窗口:`apps/mobile/src/device-link/presenceRecovery.ts:300` 的 `resetPresenceAvailabilityForConnection` 在每次重连都清空 availability(全部退回 unknown);presence 是增量广播、服务端不重放全量;唯一补全手段是 `apps/mobile/src/device-link/DeviceLinkContext.tsx:900` 的 `void readDeviceList().catch(() => undefined)` —— 失败被吞、无重试、无退避。

**与全仓其他位置的约定不一致**(其余都是「默认可用,只有显式 false 才拦」):

- `packages/maker-shared/src/deviceResponsiveness.ts:65-67` — `isPresenceValueEligible` → `return available !== false;`(其注释明确「missing value means this generation has not received a presence edge yet」「must not send into an explicitly unavailable peer」,即未知允许探测)
- `apps/mobile/src/device-link/DeviceLinkContext.tsx:452 / 1323 / 1372` — transport 自家门都用 `=== false`
- `apps/desktop/src/renderer/lib/makerChatStore.ts:3917 / 8763 / 10872` — `setNetworkAvailable(!isRemoteDeviceMarkedDisconnected(deviceId))`,只有 `connectionStatus === 'disconnected'` 才拦

即:全仓只有 `[sessionId].tsx:1033` 使用 `=== true`。

## 环境 / Environment

- Cindy 版本或 commit: 被控端为 Desktop 已安装构建(sourceCommit `db19a86b`);源码核对基于 origin/main `5adbdd80d363ce907ee08c06e04c0f5c24c27ce1`(2026-09-14)
- 平台与版本: 被控端 Windows Desktop;控制端 Android 手机端(手机端具体构建号未记录)
- 安装方式: Desktop 安装包;手机端经 device-link 连接同一账号

## 复现步骤 / Steps to reproduce

1. 桌面端与被控端同账号登录,被控端开启远程控制(`remoteControlEnabled`)。
2. 手机端连接该被控端,打开一个消息较多的远程任务。
3. 向上滚动 / 点「加载更早消息」。
4. 观察:更早历史不加载;同时屏幕没有错误提示、也没有「仅显示缓存」类横幅。
5. 对照:在桌面端打开同一任务,向上滚动可正常逐页加载更早历史(同一被控端、同一会话)。

更易命中的触发条件:弱网/链路抖动下让手机端重连一次,使该设备 presence 停在 unknown(本机同一被控端当天出现 34 次 link-open / 26 次 presence-offline、4 次响应性熔断开启、32 次 DEVICE_UNRESPONSIVE、38 次 relay-error)。

## 日志与截图 / Logs & screenshots

**手机端日志(当前未取得)**:本机没有手机端日志包,因此上述结论来自代码路径复现 + 被控端链路证据,不是手机日志原文。需要手机端日志确认两点:① 该设备 presence 长期为 unknown(表现为 `remoteHistoryAvailable=false` 而 `showCachedHistoryNotice=false`,即无横幅但加载无效);② 该时刻被控端未收到任何 `local-db:messages:view`。

**被控端(Windows host)日志**:对应时刻**没有**任何 `blocked non-allowlisted channel`,也**没有** history 扫描预算报错 —— 说明不是通道/allowlist 或大消息问题。

**代码路径复现**(真实 `HistoryViewController` + `isPresenceValueEligible`):

```text
presence=true strictGate=true sharedGate=true -> ready=true items=1 error=null pageCalls=1
presence=false strictGate=false sharedGate=false -> ready=false items=0 error=null pageCalls=0
presence=null strictGate=false sharedGate=true -> ready=false items=0 error=null pageCalls=0
```

第三行为本缺陷:`error=null`、`loading=false`、`items=0`、**未发出任何请求**,调用方无法与「本就没有更早历史」区分。

**被控端读取健康性对照**(用于排除数据侧):用被控端真实读取实现 `createHistoryViewReader` 连真实库跑该会话,13 轮上翻、246 个 item 全部返回、单页 0.008–0.142 MiB(帧上限 2 MiB)、无 `UNSUPPORTED_CAPABILITY`。

## 修复建议

1. `[sessionId].tsx:1033` 改用共享三态判据(`isPresenceValueEligible`),与 `:1036` 横幅口径、transport、桌面端一致;真离线时 transport 仍会以 `DEVICE_OFFLINE` 快速失败,不会变成盲发。
2. `:3258` 区分「显式不可用」与「未知」,未知照常尝试读取。
3. 静默路径补可见状态,避免「加载更早消息」无声失效。
4. `DeviceLinkContext.tsx:900` 的 roster 补读失败后加退避重试,或在重连后对该设备做一次具名探测以定论。
5. 测试:`packages/maker-shared/src/__tests__/historyViewOffline.test.ts` 目前只覆盖 `false`/`true`,补一条 unknown ≠ false 的断言;`apps/mobile/src/__tests__/sessionMainLayerDesktopFirst.test.ts:108` 只断言「用到了 `getPresenceAvailability(deviceId)`」、未断言比较语义,因此锁不住本缺陷。

Contributor guide

Open the contributing guide

Research direction

Start with apps/mobile/app/sessions/[sessionId].tsx and trace remoteHistoryAvailable into apps/mobile/src/session/remoteHistoryView.ts and packages/maker-shared/src/historyViewController.ts. Run packages/maker-shared/src/__tests__/historyViewOffline.test.ts and apps/mobile/src/__tests__/sessionMainLayerDesktopFirst.test.ts, then verify unknown presence is not treated as explicit offline and the relevant loading path no longer fails silently.

Written by the indexing model from the issue text.

Assessment

Tech stack
react-native, typescript
Domain
mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.