Skip to content

fix(reminder): 地点提醒常驻守护在进程被杀后无法恢复投递 - #414

Merged
znnnnnnn-wil merged 4 commits into
1024XEngineer:mainfrom
LUPENGHAN:fix/location-guard-stale-registration
Aug 29, 2026
Merged

fix(reminder): 地点提醒常驻守护在进程被杀后无法恢复投递#414
znnnnnnn-wil merged 4 commits into
1024XEngineer:mainfrom
LUPENGHAN:fix/location-guard-stale-registration

Conversation

@LUPENGHAN

@LUPENGHAN LUPENGHAN commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • 真正的根因expo-task-manager(及其依赖 unimodules-app-loader)默认从 Maven 拉官方预编译二进制,不读 node_modules 本地源码。这意味着此前打的所有补丁(下面这些)从未被真正编译进任何 APK,之前几轮"打了 patch 还是不行"的观测因此都不算数。在 package.jsonexpo.autolinking.buildFromSource 逼这两个模块走本地源码编译后,补丁才第一次真正生效。
  • 打上上游 expo/expo#47958 的修复(patches/expo-task-manager+57.0.9.patchpatch-package 自动生效)
  • ReminderGuardCoordinator 新增陈旧检测:不再拿原生注册 options(foreground/degraded)当"还在投递"的证据,改用"本进程是否自己成功建过注册 + 最近是否收到过心跳"判断;检测到继承自上一个(已死)进程的注册时主动重建(含真正的 TaskManager.unregisterTaskAsync()
  • 保留诊断日志([guard] state=... stale=... wantInterval=... / [guard] registered interval=... / [guard] refreshed interval=... withService=...),后续复测和排查用
  • 顺带把地点提醒触发半径从 400m 调到 200m

背景

详见 #413。地点提醒依赖的常驻定位守护,在 App 进程被系统杀掉后,原生位置订阅仍在正常投递(adb dumpsys location 证实),但 expo-task-managerdefineTask 回调收不到,此前只有卸载重装能恢复。

已通过真机端到端验证 ✅

补上 buildFromSource 配置、确认 patch 真正编译进 APK 之后:强杀重开 App,走出围栏再走回来,日志出现:

[reminder] TRIGGERED, delivering 193b46bc72014283aac0fced5b2c6ab5 拿快递

原生响铃/通知正常展示。多轮心跳([guard] dispatching sample to the live listener)持续稳定,未再出现"冷启动余波后彻底沉默"的旧现象。

Test plan

  • npx jest(830+ 测试全过)
  • npx tsc --noEmit
  • npx eslint(无新增问题)
  • 真机:强杀重开后,走出围栏再走回来,[reminder] TRIGGERED 正常触发且原生弹窗/通知正常展示
  • 真机:确认 TimeflowDiag 诊断字符串真的编进了安装的 APK(unzip + grep dex 直接验证),排除"改了但没编译进去"的可能

🤖 Generated with Claude Code

LUPENGHAN and others added 2 commits August 28, 2026 23:10
打上上游 expo/expo#47958 的修复(patch-package),并给
ReminderGuardCoordinator 加陈旧检测:不再拿注册 options 当"还在投递"
的证据,改用"本进程是否自己成功建过注册 + 最近是否收到过心跳"判断,
检测到继承自上一个(已死)进程的注册时主动重建。

详见 issue 1024XEngineer#413,未获完整真机验证,作为已知修复方向保留。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
expo-task-manager(及其依赖 unimodules-app-loader)默认从 Maven 拉官方
预编译二进制,不读 node_modules 本地源码——导致之前 patch-package 打的
expo/expo#47958 backport 从未被真正编译进任何 APK,之前几轮"打了 patch
还是不行"的观测因此都不算数。

在 package.json 加 expo.autolinking.buildFromSource,逼这两个模块走本地
源码编译,backport 才第一次真正生效。真机验证:强杀重开后出圈再回圈,
[reminder] TRIGGERED 正常触发,原生响铃正常展示。

顺带把地点提醒触发半径从 400m 调到 200m。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Flag Coverage Δ
backend 96.57% <ø> (ø)
frontend 91.10% <100.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...s/reminder/application/ReminderGuardCoordinator.ts 100.00% <100.00%> (ø)
frontend/src/features/reminder/domain/geofence.ts 82.97% <100.00%> (ø)
...d/src/infrastructure/location/reminderGuardTask.ts 64.70% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审阅了构建配置、TaskManager backport、守护注册协调器、headless 刷新路径及新增单测。当前实现能在冷启动时重建继承注册,但运行中的静默失活和清理失败路径仍可能让地点/时间提醒永久停摆,建议先修复以下两处。

Additional findings

  • frontend/src/features/reminder/application/ReminderGuardCoordinator.ts:?: [P1] Do not restart after failed task unregistration: 如果 unregisterTaskAsync() 抛错,这里仍继续执行 startLocationUpdatesAsync() 并把 ownsRegistration 设为 true。但本次改动的根因和注释都说明持久化的 TaskManager 注册必须被真正移除;清理失败时继续复用同名注册可能再次命中同一个失活的原生记录,随后协调器又会把它当成健康注册而提前返回,导致恢复失败。请在注销失败时中止本次重建并保留可重试状态(或确认注销成功后再启动)。

- package.json:补 prettier 格式,修 CI 的 format:check 失败
- 注销失败(unregisterTaskAsync 抛错)时不再继续 startLocationUpdatesAsync():
  持久化记录没被真正删掉的话,重注册走的还是原生"已存在就 setOptions"分支,
  等于又绑上同一条失活记录,下次 reconcile 会把它当健康注册直接早退,恢复
  彻底失败且无声无息。现在中止本次重建,把重试留给下一次 reconcile。
- 新增独立于心跳事件的 watchdog 定时器:isRegistrationStale() 要判的恰恰是
  "心跳已经不再来了",此前只在 start()/日程订阅/handleSample() 触发的
  reconcile 里查——一旦心跳静默失活,这三个触发源全指望不上,会一直卡到
  用户手动改日程或重启 App。定时器本身在 Node 测试环境下 unref(),不影响
  真机运行也不会拖 Jest 进程退出。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@LUPENGHAN

Copy link
Copy Markdown
Contributor Author

两条 P1 都确认是真问题,已在 ff2d5bc 修复:

  1. 注销失败后仍继续重建unregisterTaskAsync() 抛错时改为直接 return,中止本次重建,不再往下调 startLocationUpdatesAsync()——避免持久化记录没清干净、又绑上同一条失活记录,还把 ownsRegistration/lastProgressAt 乐观地标成"刚建好",把失活状态盖住。
  2. 无独立重试(内联评论已回复):加了 watchdog 定时器。

CI 那次 Frontend 失败是 prettier --check 没过(新加的 package.json 字段格式不对),同一个 commit 里一起修了。

半径改成 200 之后漏了这处 vitest 断言,CI 里 npm run test 分两段跑
(vitest + jest),本地只跑过 jest 没发现。改成引用常量,以后半径再变不会
再断。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@znnnnnnn-wil
znnnnnnn-wil merged commit aebad6c into 1024XEngineer:main Aug 29, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants