feat(reminder): add alarm ring UI toolkit and build scaffolding - #264
Merged
Wintercom merged 3 commits intoAug 18, 2026
Merged
Conversation
Part of 1024XEngineer#263. The zero-dependency slice of the timeflow-alarm native module: AlarmContract (shared constants), DayRulerView and AlarmRingUi (the hand-drawn ring-screen UI -- colors, typefaces, entrance animation, pill buttons, self-ticking clock). None of these three import anything else in the module, so this compiles standalone; the scheduling/RN bridge/ring Activity+Service classes that DO reference each other are a separate PR on top of this one. Also the app-level scaffolding: app.config.js (replaces static app.json so plugins can read env vars), the withTimeflowAlarm config plugin, and the new package dependency. Not included: Baidu location module -- dropped entirely per 1024XEngineer#263's decision to use system geofencing instead.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
There was a problem hiding this comment.
本次检查覆盖了 Expo 动态配置与权限插件、本地 Android 模块脚手架、手写响铃 UI,以及依赖和 CI 调整。前端 lint、格式、类型检查、47 个 Vitest 与 282 个 Jest 用例均通过;在临时目录执行 expo prebuild --platform android --no-install 也成功。当前需要修正的是百度定位模块删除不完整,项目配置仍向后续维护者和工具表达一个已经废弃的集成。Android Gradle 编译未完成,因为 Gradle wrapper 下载遇到本机证书信任错误。
react-native.config.js still registered the deleted modules/timeflow-baidu-location for autolinking, package-lock.json still had an extraneous entry for it, and CI still exported a TIMEFLOW_BAIDU_LOCATION_API_KEY placeholder for a plugin that's no longer registered in app.config.js. None of these broke anything (Expo silently skips the missing directory) but all three kept describing an integration 1024XEngineer#263 said to drop entirely. Verified: tsc/eslint clean, `expo prebuild --platform android --no-install` still succeeds.
Contributor
Author
|
三处残留都删了:
|
Code review (PR 1024XEngineer#264): formatDate() constructed a new SimpleDateFormat on every call, including every 20-second RingRoot tick for as long as the ring screen is visible -- unnecessary allocation on a repeating timer. Cached as a static final field; its locale is hardcoded to Locale.CHINA so there's no correctness risk. formatClock() is left as a fresh SimpleDateFormat per call since it uses Locale.getDefault(), which can change at runtime -- caching it would risk showing a stale locale, not worth it for this level of savings.
Closed
LUPENGHAN
added a commit
to LUPENGHAN/timeflow
that referenced
this pull request
Aug 17, 2026
Closes 1024XEngineer#272 voice.command.result gains an occurrence_overrides field alongside schedule/schedules. Backend: CommandOutcome/CommandResult carry it through from ScheduleMutationResult.occurrence_overrides (already populated by _delete_recurring_range for scope=this_occurrence, but previously dropped before reaching the wire); schedule_tools.py's _mutation_result serializes each ScheduleOccurrenceOverrideSnapshot the same way _snapshot_for_client already does for schedules. Frontend: AppliedCommand carries the new field through from the WS message; LocalScheduleWriter.applyCommandResult now handles schedule and occurrence_overrides independently (a this_occurrence delete produces only an override, no new schedule snapshot) and writes each override via the already-existing, already-tested ScheduleLocalRepository.upsertOccurrenceOverride. Independent of the reminder-integration branch stack (1024XEngineer#264-1024XEngineer#271) -- the occurrence-override table and upsertOccurrenceOverride already exist on main; this only needed LocalScheduleWriter's plain-main shape, not the reminder stack's SqliteLocalScheduleReader wiring.
MeteorsLiu
approved these changes
Aug 17, 2026
LUPENGHAN
added a commit
to LUPENGHAN/timeflow
that referenced
this pull request
Aug 18, 2026
Two changes on top of the rebase onto upstream/main's native alarm module (1024XEngineer#264/1024XEngineer#265): - AlarmSchedulerPort.consumeNativeDispositions (single-phase, clears the native buffer on read) is now peekNativeDispositions + ackNativeDispositions (two-phase), matching the split 1024XEngineer#265 already made on the native side. hydrateNativeDispositions() now acks only after the whole batch persists successfully, so a mid-batch failure leaves the un-acked rows in the native buffer for retry instead of losing them. - This PR's own interface changes (DeviceCapabilityPort.onAppActive, LocationMonitorPort.rebuild's new LocationRebuildTarget param, dropping MockTimeListener from shared/time) were never propagated to their implementers, so `npm run check` failed standalone -- expected for a mid-stack PR per its own description, but not acceptable for merging. Added the minimal implementations needed to close the gap: onAppActive on Native/MockDeviceCapability, MockLocationMonitor.rebuild's signature, and a new infrastructure/time/MockTimeListener placeholder. 1024XEngineer#269 replaces all of these with real adapters; this just keeps 1024XEngineer#266 green on its own. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wintercom
pushed a commit
that referenced
this pull request
Aug 18, 2026
* feat(schedule): sync occurrence overrides to the client Closes #272 voice.command.result gains an occurrence_overrides field alongside schedule/schedules. Backend: CommandOutcome/CommandResult carry it through from ScheduleMutationResult.occurrence_overrides (already populated by _delete_recurring_range for scope=this_occurrence, but previously dropped before reaching the wire); schedule_tools.py's _mutation_result serializes each ScheduleOccurrenceOverrideSnapshot the same way _snapshot_for_client already does for schedules. Frontend: AppliedCommand carries the new field through from the WS message; LocalScheduleWriter.applyCommandResult now handles schedule and occurrence_overrides independently (a this_occurrence delete produces only an override, no new schedule snapshot) and writes each override via the already-existing, already-tested ScheduleLocalRepository.upsertOccurrenceOverride. Independent of the reminder-integration branch stack (#264-#271) -- the occurrence-override table and upsertOccurrenceOverride already exist on main; this only needed LocalScheduleWriter's plain-main shape, not the reminder stack's SqliteLocalScheduleReader wiring. * test(schedule): cover the this_occurrence override serialization path Prettier fix for LocalScheduleWriter.ts (npm run check caught it). Adds a realtime-toolbox test exercising a delete with scope=this_occurrence that produces an occurrence_override -- the only path that actually calls _override_for_client. Closes the 2-line patch-coverage gap Codecov flagged on the previous commit. * test(schedule): cover LocalScheduleWriter's missing-field rejection Closes the 1-line patch-coverage gap Codecov flagged: requireString's throw path had no test on this branch (the file existed on main with no dedicated test at all before this PR added one). * fix(schedule): surface every schedule a mutation produced, not just the first _delete_recurring_range's this_occurrence path, when it hits an existing replace override, soft-deletes the replacement schedule and returns it alongside the untouched parent in ScheduleMutationResult.schedules. _mutation_result only ever forwarded schedules[0] to the client, so that soft-delete never reached voice.command.result and the client kept showing the stale replacement as active. CommandOutcome already had a schedules (plural) field - only list_schedules populated it. _mutation_result now fills it with every schedule the mutation touched; schedule (singular) stays as schedules[0] for backward compat with clients not yet updated. * fix(schedule): check write results and transact LocalScheduleWriter applyCommandResult ignored the boolean applyCloudSchedule()/ upsertOccurrenceOverride() return, so a failed write (missing parent schedule, account mismatch) still let the caller send message.ack status=applied - the server believed the command was persisted when it wasn't. Now every write's return value is checked and a false throws, and all writes for one command result run inside a single transaction via the repository's new withTransaction(). A command can produce multiple schedule and override writes that together represent one voice command landing; without the transaction a partial failure would leave state the server never actually had, and since the caller already skips the ack on any throw, that state would never get retried either. Also switches to consuming the schedules (plural) field the backend now populates for mutations, explicitly excluding list_schedules by operation instead of relying on the coincidence that query results never set schedule/occurrence_overrides.
Wintercom
pushed a commit
that referenced
this pull request
Aug 19, 2026
* feat(reminder): add reminder engine core (domain + application) LocalReminderApplication and its ports (AlarmSchedulerPort, LocationMonitorPort, DeviceCapabilityPort, NotificationChannels, ReminderDeliveryPort, ReminderApplicationPort), plus the domain layer (reminder.ts, strengthDelivery.ts) that drives arm/fire/confirm state transitions and recurring-schedule advancement. Pure logic layer: no device-specific adapters yet, nothing wired into the app. shared/time gains format.ts (used by the delivery strength calc); MockClock/MockTimeListener are removed since nothing in this stack still needs a fake clock once the real engine lands. * fix(reminder): close native/JS double-fire race in acknowledgeNativeFire Code review (PR #266): acknowledgeNativeFire() only checked the persisted disposition_state (confirmed/pending) before proceeding, not the in-memory activeDeliveries/deliverLocks sets. Those sets are exactly what canDeliver() checks on the JS-driven handleTime path to skip a schedule the native alarm already owns -- but the guard only worked in that one direction. If handleTime was already mid-flight for a schedule (added to activeDeliveries, not yet persisted any disposition change) at the moment the native alarm for the same schedule fired, acknowledgeNativeFire would fall through and the two channels could both run -- the exact double-fire the surrounding comment says this mechanism eliminates. Added the activeDeliveries check as an additional early-return, matching canDeliver()'s guard. * fix(reminder): peek/ack native dispositions, close CI gap after rebase Two changes on top of the rebase onto upstream/main's native alarm module (#264/#265): - AlarmSchedulerPort.consumeNativeDispositions (single-phase, clears the native buffer on read) is now peekNativeDispositions + ackNativeDispositions (two-phase), matching the split #265 already made on the native side. hydrateNativeDispositions() now acks only after the whole batch persists successfully, so a mid-batch failure leaves the un-acked rows in the native buffer for retry instead of losing them. - This PR's own interface changes (DeviceCapabilityPort.onAppActive, LocationMonitorPort.rebuild's new LocationRebuildTarget param, dropping MockTimeListener from shared/time) were never propagated to their implementers, so `npm run check` failed standalone -- expected for a mid-stack PR per its own description, but not acceptable for merging. Added the minimal implementations needed to close the gap: onAppActive on Native/MockDeviceCapability, MockLocationMonitor.rebuild's signature, and a new infrastructure/time/MockTimeListener placeholder. #269 replaces all of these with real adapters; this just keeps #266 green on its own. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(reminder): break cold-start deadlock in hydrateNativeDispositions, add adapter tests hydrateNativeDispositions() runs inside startInternal(), which is itself the task currently occupying opChain. It was calling the public confirm()/snooze() wrappers, which re-enqueue onto that same opChain -- but that chain can't advance until startInternal() (the caller) returns, and startInternal() won't return until those re-enqueued calls resolve. Any cold start with a confirmed/snoozed row sitting in the native disposition buffer deadlocked permanently. Fixed by calling confirmInternal()/snoozeInternal() directly, since hydrateNativeDispositions() is already running as the sole active opChain task and doesn't need to re-enqueue. Also closes the Codecov patch-coverage gap on the adapter changes from the previous commit: onAppActive on Native/MockDeviceCapability, and the MockLocationMonitor/MockTimeListener mocks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test(reminder): add application-layer coverage for LocalReminderApplication Covers what review flagged as missing: cold-start peek/ack (batch-level ack only after every row persists, none on partial failure), hydration for each native disposition state (confirmed/snoozed/pending), the native/JS dual-channel race guard, alarm rescheduling after confirm/snooze, stop/ restart cleanup, and recurring-reminder re-arming across two occurrences (demonstrating the next_trigger_at signal / state-layer-computes-the-next- occurrence split with actual test evidence, not just an explanation). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test(reminder): cover location triggering and strength delivery plans Codecov flagged patch coverage at 66% after the application-layer test commit -- LocalReminderApplication.ts's location-schedule path (arm on leave, deliver on re-entry, no re-fire while still armed) and strengthDelivery.ts's low/high branches had zero coverage. Both were only exercised indirectly (or not at all) by the existing tests, which focused on time-triggered schedules. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test(reminder): cover delivery channels by strength and native alarm event routing Codecov target for this PR's patch is 91.16%, was at 76.78%. Adds: - low strength delivery: system notification only, no popup/vibration/audio - high strength delivery: popup + vibration + tts, falls back to local audio when tts fails - native "snoozed"/"dismissed" events routed through alarms.subscribe() to snooze()/confirm(), including unsubscribe on stop() --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
LUPENGHAN
added a commit
to LUPENGHAN/timeflow
that referenced
this pull request
Aug 20, 2026
AssistantConversationService.handleClose() nulled unsubscribeConnection without calling it. Switching from push-to-talk to continuous mode makes the shared AuthenticatedWebSocketClient drop and reopen the connection (the two modes negotiate different voiceMode), so the old service stayed subscribed to the new connection's TTS/PCM and pushed the same reply into the player alongside the continuous service -- audible as one sentence played twice, overlapping. dispose() already unsubscribed correctly; only the close path was missing it. Carried over from 1024XEngineer#245, which this stacked series replaces -- the fix is not reminder-scoped so none of 1024XEngineer#264-1024XEngineer#270 picked it up.
LUPENGHAN
added a commit
to LUPENGHAN/timeflow
that referenced
this pull request
Aug 20, 2026
A barge-in that lands after the model already finished delivering a reply cancelled whatever was playing *next*, not the reply the phone was still sounding out. Two halves: Backend: _Turn reset _audio_id before that late interrupted() ran, so it sent AudioCanceled(audio_id=""). Added _last_audio_id, which survives the reset, and send that instead -- the cancellation now names the audio it actually refers to. Frontend: voice.tts.canceled routed stop() through playbackChain, so every PCM chunk already queued was still fed to the native player before the stop landed. It now bypasses the chain via stopPlaybackImmediately(), and chainPlayback() tags each queued operation with a playbackGeneration that stop bumps, so the stale queue is dropped rather than replayed. tts.end and tts.canceled are both matched against currentAudioId/canceledAudioId, so the server's follow-up tts.end for a cancelled reply no longer ends a newer stream or flips interrupted back to listening. The empty-audio_id case is still handled on the client so a not-yet-updated backend cannot stop a newer reply. 1024XEngineer#297 removed only the user-facing interrupt button; backend barge-in still drives voice.tts.canceled, so this path is live. Carried over from 1024XEngineer#245, which this stacked series replaces -- not reminder-scoped, so none of 1024XEngineer#264-1024XEngineer#270 picked it up.
gac0812
pushed a commit
that referenced
this pull request
Aug 20, 2026
…271) * feat(reminder): add SQLite-backed reminder data layer Part of #263. SqliteLocalScheduleReader / SqliteReminderStateStore read and persist against the real local database (ScheduleLocalRepository) instead of in-memory fixtures; geofence_radius_meters is hardcoded to 200m for now (known simplification, see Issue #263 Out of Scope). InMemoryLocalScheduleReader is kept as a non-persisted alternative implementation of the same port. LocalScheduleWriter's post-write hook refreshes the new reader after a voice-driven schedule mutation lands. Only depends on application interfaces already on main and the existing ScheduleLocalRepository -- independent of the audio/location/ notifications adapter PRs in this stack. Removes MockLocalScheduleReader, MockReminderApplication, MockReminderDispositionSync, MockReminderStateStore, mockReminderSchedules. * feat(reminder): wire real engine into the app, drop remaining mocks Swaps the composition root over to the real implementations added in the previous three commits: LocalReminderApplication replaces MockReminderApplication, SqliteLocalScheduleReader/SqliteReminderStateStore replace their Mock counterparts, and every device port (audio/notification/vibration/alarm/location) now points at its real adapter. ExpoLocationMonitor (system geofencing) is used for location monitoring; NativeLocationMonitor (Baidu SDK) stays in the repo but unwired -- see the comment in createAppServices.ts for how to switch. AppProviders/AppRoot gain the reminder permission-request flow (useReminderPermissionsOnLaunch, now driven by an injected AlertDialogPort instead of calling Alert.alert directly, with a settings-page fallback for denied background-location permission) and rebuild() the engine once permissions change. Removes MockReminderPresenter, the last remaining Mock* adapter. * fix(reminder): permission flow stalled after the first prompt Every branch in promptNext() except the overlay/full_screen/battery confirm-and-continue path returns from inside the try block, so the setTimeout(runPrompt, 250) that was meant to advance to the next missing permission -- placed after the try/finally -- was dead code for those branches. In practice: grant notifications, and exact_alarm (the next permission in line) would just never get prompted; same for any declined dialog, or a granted/denied location permission. Moved the continuation check into the finally block so it always runs regardless of which branch returned. Also replaces the old mock-based useReminderPermissionsOnLaunch test (deleted upstream when this hook's signature changed to take an injected AlertDialogPort + onPermissionsUpdated callback, with no replacement written) and drops the now-redundant .gitkeep placeholders left over from directories that have had real files in them since earlier commits in this stack. * fix(voice): remove stale push-to-talk listeners AssistantConversationService.handleClose() nulled unsubscribeConnection without calling it. Switching from push-to-talk to continuous mode makes the shared AuthenticatedWebSocketClient drop and reopen the connection (the two modes negotiate different voiceMode), so the old service stayed subscribed to the new connection's TTS/PCM and pushed the same reply into the player alongside the continuous service -- audible as one sentence played twice, overlapping. dispose() already unsubscribed correctly; only the close path was missing it. Carried over from #245, which this stacked series replaces -- the fix is not reminder-scoped so none of #264-#270 picked it up. * fix(voice): preserve new TTS after interruption A barge-in that lands after the model already finished delivering a reply cancelled whatever was playing *next*, not the reply the phone was still sounding out. Two halves: Backend: _Turn reset _audio_id before that late interrupted() ran, so it sent AudioCanceled(audio_id=""). Added _last_audio_id, which survives the reset, and send that instead -- the cancellation now names the audio it actually refers to. Frontend: voice.tts.canceled routed stop() through playbackChain, so every PCM chunk already queued was still fed to the native player before the stop landed. It now bypasses the chain via stopPlaybackImmediately(), and chainPlayback() tags each queued operation with a playbackGeneration that stop bumps, so the stale queue is dropped rather than replayed. tts.end and tts.canceled are both matched against currentAudioId/canceledAudioId, so the server's follow-up tts.end for a cancelled reply no longer ends a newer stream or flips interrupted back to listening. The empty-audio_id case is still handled on the client so a not-yet-updated backend cannot stop a newer reply. #297 removed only the user-facing interrupt button; backend barge-in still drives voice.tts.canceled, so this path is live. Carried over from #245, which this stacked series replaces -- not reminder-scoped, so none of #264-#270 picked it up. * fix(reminder): reset permission-prompt state when the effect unmounts Code review (PR #271, fennoai): the cleanup only cleared the timer and unsubscribed onAppActive, not awaitingReturnRef/skippedRef. If a user opened settings for exact_alarm (or a denied location permission) and logged out before returning, AppProviders reruns this effect with device=null, but the stale awaitingReturnRef stayed true. On the next login the new effect's promptNext() reads that same ref (it's a component-level useRef, not reset by the effect re-running) and returns immediately every time, and since the app is already active there's no new onAppActive event left to clear it -- every permission prompt stays disabled until the process restarts. Reset both refs in the cleanup so a fresh login starts a clean prompt round. * test(reminder,voice): close Codecov patch-coverage gaps Codecov flagged 60.79% patch coverage across six files from this branch's recent commits. Closed each: - InMemoryLocalScheduleReader.ts: 0% because nothing in the app or tests actually uses it -- only re-exported from two barrels, never imported or instantiated anywhere in feature/reminder-wiring's own history. Deleted the file and its two re-exports instead of testing dead code. - AlertReminderPresenter.ts: new AlertReminderPresenter.test.ts covers every reason-specific message, the title fallback, confirm/snooze dispatch, unsubscribe, and hide()'s suppression window. The `?? '...'` message fallback is unreachable (MESSAGE_BY_REASON already covers every ReminderTriggerReason), so it's istanbul-ignored with a stated reason instead of faked with an invalid reason value. - useReminderPermissionsOnLaunch.ts: added 7 tests for branches the existing suite didn't reach -- denied notifications, failed openSettings on both the direct-settings and location paths, granted location, the bottom settings-redirect branch, a rejected getStatus(), and a dismissed (vs declined) dialog. Its own similarly-unreachable `prompt == null` branch (all 7 DevicePermission values already have a prompt) got the same istanbul-ignore treatment. - AppProviders.tsx: new AppProviders.test.tsx isolates the onPermissionsUpdated -> reminder.rebuild() wiring with a mocked useReminderPermissionsOnLaunch, instead of relying on AppRoot.test.tsx's much heavier integration setup for one line. - AssistantContinuousConversationService.ts: dismissReply() had no coverage at all before this branch touched one line of it (routing through stopPlaybackImmediately()); added a test that drives a reply through voice.tts.start/voice.dialogue.reply and asserts dismissReply() clears it and stops playback. - backend agent.py: the interrupted()-with-nothing-ever-spoken branch (_last_audio_id is None) wasn't exercised; added test_a_barge_in_before_any_reply_started_sends_no_cancellation. Verified: frontend tsc/eslint/prettier clean, Jest 520/520, Vitest 87/87; backend ruff/mypy clean, pytest 97.53% coverage.
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.
关联 Issue
Part of #263
改动
验证
npx tsc --noEmit、npx eslint .全绿本轮不含(见 #263 Out of Scope)