Skip to content

feat(scheduling): TASK-299.2 bidirectional reminder sync#714

Merged
rmusser01 merged 44 commits into
devfrom
feature/task-299.2-bidirectional-reminder-sync
Jul 20, 2026
Merged

feat(scheduling): TASK-299.2 bidirectional reminder sync#714
rmusser01 merged 44 commits into
devfrom
feature/task-299.2-bidirectional-reminder-sync

Conversation

@rmusser01

Copy link
Copy Markdown
Owner

Implements bidirectional reminder sync with tldw_server.

Summary

  • Hardened SchedulingServerClient with retry, typed errors, and idempotency-key stripping.
  • Added SyncCompleted/SyncFailed events.
  • Refactored SchedulingService and SyncEngine for explicit owner_id and network-then-transaction sync.
  • Added connection-aware bulk DB helpers for sync.
  • Built SyncStatusWidget and ConflictsTab; wired them into SchedulesWorkbench with sync worker, owner switcher, and conflict refresh.
  • Always instantiate SchedulingServerClient in app.py.
  • Updated ADR-018 with TASK-299.2 decisions.

Verification

  • pytest Tests/Scheduling Tests/UI/test_schedules_workbench.py: 222 passed
  • ruff check tldw_chatbook/Scheduling tldw_chatbook/UI/Screens/scheduling tldw_chatbook/app.py: clean
  • mypy tldw_chatbook/Scheduling tldw_chatbook/UI/Screens/scheduling: clean

Closes TASK-299.2

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4d182e81-af54-4ec8-80a8-49db9f2629e9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/task-299.2-bidirectional-reminder-sync

Comment @coderabbitai help to get the list of available commands.

rmusser01 added 29 commits July 19, 2026 20:41
@rmusser01
rmusser01 force-pushed the feature/task-299.2-bidirectional-reminder-sync branch from 8e0f740 to 72fe84b Compare July 20, 2026 03:42
@rmusser01
rmusser01 merged commit 3ac0b38 into dev Jul 20, 2026
11 of 13 checks passed

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces comprehensive documentation for new features, including the enhanced file picker, the scheduling module, and the watchlists TUI screen. Additionally, it applies extensive formatting and import cleanup across numerous test files. As there are no review comments provided, I have no feedback to offer.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Bidirectional reminder sync with tldw_server (TASK-299.2)

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add bidirectional reminder sync with network-first then atomic DB commit.
• Expose sync status/errors and conflicts resolution in the Scheduling workbench UI.
• Document TASK-299.2 sync decisions and add focused server-client/sync-engine tests.
Diagram

graph TD
  UI["SchedulesWorkbench"] -->|calls| SVC["SchedulingService"] -->|reads/writes| DB[("ScheduledTasksDB")]
  SVC -->|drives| SYNC["SyncEngine"] -->|pull/push| SC["SchedulingServerClient"] -->|delegates| NS{{"ServerNotificationsService"}}
  SYNC -->|transaction helpers| DB
  UI -->|shows| STATUS["SyncStatusWidget"]
  UI -->|resolves| CONFLICTS["ConflictsTab"] -->|uses| SYNC

  subgraph Legend
    direction LR
    _ui["UI"] ~~~ _svc["Service"] ~~~ _db[("DB")] ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Hold DB transaction across network I/O
  • ➕ Would eliminate the crash window where a server create succeeds but local mapping isn’t committed yet
  • ➖ SQLite connection/transaction held across async I/O increases lock contention and deadlock risk
  • ➖ Harder error handling; long-running sync blocks local scheduling operations
2. Persistent push-staging/outbox with acknowledgements
  • ➕ Eliminates duplicate-create crash window without server-side idempotency
  • ➕ Provides resumable, auditable sync steps
  • ➖ More schema + state-machine complexity (staging rows, replay rules)
  • ➖ Higher implementation and maintenance cost vs current TASK-299.2 scope
3. Server-side idempotency support (accept idempotency_key)
  • ➕ Allows safe retries for create operations
  • ➕ Simplifies client logic and reduces duplicate risk
  • ➖ Requires coordinated tldw_server API/schema change and rollout
  • ➖ Still needs migration/compat behavior for older servers

Recommendation: The chosen approach (network phase then single atomic DB transaction) is the right default for a local-first Textual app: it avoids holding SQLite locks during async HTTP calls while still making each sync attempt an all-or-nothing local commit. The remaining known risk (duplicate server creates after a crash) is explicitly documented in ADR-018 and is acceptable for TASK-299.2; the best long-term fix is adding server-side idempotency support or a lightweight persistent outbox/ack model.

Files changed (12) +5244 / -1540 · 5 not counted

Enhancement (9) +4079 / -1540
server_client.pyAdd retrying scheduling server client with typed errors not counted

Add retrying scheduling server client with typed errors

• Introduces SchedulingServerClient with a configurable retry/backoff wrapper around an injected notifications service. Maps failures into typed exceptions, enforces ServerUnavailableError when disconnected, and strips local-only idempotency_key before delegating calls.

tldw_chatbook/Scheduling/services/server_client.py

sync_engine.pyImplement bidirectional reminder sync with network-then-transaction boundary not counted

Implement bidirectional reminder sync with network-then-transaction boundary

• Adds SyncEngine that pulls server reminders, pushes pending local mutations/tombstones, detects deletions, records conflicts, and updates sync_state. All server calls run outside SQLite transactions; results are applied atomically via connection-aware DB helpers.

tldw_chatbook/Scheduling/services/sync_engine.py

scheduling_service.pyRefactor SchedulingService to be owner-aware and sync-capable not counted

Refactor SchedulingService to be owner-aware and sync-capable

• Defines a local-first SchedulingService facade used by the UI, including explicit owner_id switching and integration with SyncEngine. CRUD operations prefer server calls when available, otherwise persist locally and queue pending mutations for later sync.

tldw_chatbook/Scheduling/services/scheduling_service.py

scheduled_tasks_db.pyExpand ScheduledTasksDB with transaction API and sync helpers not counted

Expand ScheduledTasksDB with transaction API and sync helpers

• Implements ScheduledTasksDB schema + CRUD and adds a transaction() context manager plus connection-aware bulk helpers used by SyncEngine. Adds/organizes sync-state, mapping, pending-mutation, tombstone, and conflict persistence utilities to support atomic sync commits.

tldw_chatbook/Scheduling/db/scheduled_tasks_db.py

events.pyAdd Scheduling UI events for sync success/failure not counted

Add Scheduling UI events for sync success/failure

• Defines SyncCompleted and SyncFailed Textual messages alongside existing reminder CRUD messages. These events enable the workbench to react to sync outcomes and refresh status/conflicts.

tldw_chatbook/Scheduling/events.py

schedules_workbench.pyAdd schedules workbench with sync worker, owner switcher, and conflicts tab +603/-0

Add schedules workbench with sync worker, owner switcher, and conflicts tab

• Introduces a TabbedContent-based workbench showing the scheduling queue and a conflicts view. Wires Ctrl+S sync action, owner switching between local and server owners, and refresh flows for task list, sync status, and conflict resolution.

tldw_chatbook/UI/Screens/scheduling/schedules_workbench.py

sync_status_widget.pyAdd sync status bar widget for owner/sync timestamps/errors +92/-0

Add sync status bar widget for owner/sync timestamps/errors

• Adds a horizontal status widget displaying current owner selection, last pull/push timestamps, and latest sync error with a clear action. Provides imperative methods to update owner state and sync status from the screen.

tldw_chatbook/UI/Screens/scheduling/sync_status_widget.py

conflicts_tab.pyAdd conflicts tab for resolving reminder sync conflicts +130/-0

Add conflicts tab for resolving reminder sync conflicts

• Adds a ConflictsTab widget that lists unresolved conflicts and supports resolving them with 'Use server' or 'Use local'. Posts a ConflictResolved message so the workbench can refresh downstream state.

tldw_chatbook/UI/Screens/scheduling/conflicts_tab.py

app.pyAlways instantiate SchedulingServerClient for later service injection +3254/-1540

Always instantiate SchedulingServerClient for later service injection

• Adjusts app wiring so SchedulingServerClient is created even when server notifications aren’t available at startup. This supports post-login injection via set_notifications_service without rebuilding SchedulingService.

tldw_chatbook/app.py

Tests (2) +1084 / -0
test_server_client.pyAdd unit tests for SchedulingServerClient error mapping and retries +257/-0

Add unit tests for SchedulingServerClient error mapping and retries

• Covers delegation behavior, unavailable-client errors, retry/no-retry semantics (notably create_reminder), idempotency_key stripping, and PolicyDeniedError mapping to validation errors.

Tests/Scheduling/test_server_client.py

test_sync_engine.pyAdd tests for SyncEngine pull/push, mapping, deletions, and error recording +827/-0

Add tests for SyncEngine pull/push, mapping, deletions, and error recording

• Introduces async tests validating server pulls, mapping creation, title defaults, sync_state updates, and error recording on server failures. Exercises update behavior both with and without pre-existing mappings.

Tests/Scheduling/test_sync_engine.py

Documentation (1) +81 / -0
018-local-server-hybrid-scheduled-tasks.mdRecord ADR-018 decisions and TASK-299.2 sync addendum +81/-0

Record ADR-018 decisions and TASK-299.2 sync addendum

• Adds an architecture decision record documenting local/server hybrid storage, owner_id semantics, default server-wins conflicts, and TASK-299.2 specifics (idempotency_key handling, network/transaction boundary, crash-window trade-off).

backlog/decisions/018-local-server-hybrid-scheduled-tasks.md

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (5) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 39 rules

Grey Divider


Action required

1. entity_table used unvalidated in SQL 📘 Rule violation ≡ Correctness
Description
The test helper builds SQL using entity_table directly in an f-string, allowing an arbitrary table
name to be injected into the query. This violates the requirement to validate SQL identifiers via
the central sql_validation module before interpolation.
Code

Tests/Media_DB/test_media_db_v2.py[R55-57]

+    cursor = db.execute_query(
+        f"SELECT version FROM {entity_table} WHERE uuid = ?", (uuid,)
+    )
Relevance

⭐⭐ Medium

No evidence of sql_validation identifier enforcement; cited test path not found in this repo
history.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1011840 requires SQL identifiers (like table names) be validated via the
centralized sql_validation module before being interpolated. The changed code interpolates the
entity_table variable directly into the SQL string.

Rule 1011840: Validate SQL identifiers via centralized sql_validation module
Tests/Media_DB/test_media_db_v2.py[53-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`get_entity_version()` interpolates `entity_table` directly into SQL (`f"... FROM {entity_table} ..."`) without validating the identifier via `sql_validation.py`.

## Issue Context
Even in tests/helpers, this pattern can be copied into production code and defeats centralized identifier validation.

## Fix Focus Areas
- Tests/Media_DB/test_media_db_v2.py[53-59]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. list_reminder_tasks() missing pagination 📘 Rule violation ➹ Performance
Description
list_reminder_tasks() executes SELECT * without any LIMIT/pagination, which can return an
unbounded collection and degrade performance as the table grows. Add a bounded page size (fixed cap
or validated parameter) and optionally expose pagination metadata.
Code

tldw_chatbook/Scheduling/db/scheduled_tasks_db.py[R679-683]

+        with closing(self._get_connection()) as conn:
+            cursor = conn.execute(
+                f"SELECT * FROM reminder_tasks {where_clause} ORDER BY created_at",
+                params,
+            )
Relevance

⭐⭐ Medium

Repo sometimes accepts perf caps (PR #99), but no evidence of mandatory pagination/LIMIT
enforcement.

PR-#99

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 497167 requires pagination (LIMIT/OFFSET or equivalent) for DB queries that return
collections. The new list_reminder_tasks() query returns all matching rows without any limit.

Rule 497167: Enforce pagination for database queries returning collections
tldw_chatbook/Scheduling/db/scheduled_tasks_db.py[657-686]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`list_reminder_tasks()` runs an unbounded query (`SELECT * ... ORDER BY created_at`) with no LIMIT/OFFSET.

## Issue Context
Compliance requires pagination for collection-returning DB queries to prevent large unbounded reads.

## Fix Focus Areas
- tldw_chatbook/Scheduling/db/scheduled_tasks_db.py[657-686]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. DB read bypasses transaction() 📘 Rule violation ≡ Correctness
Description
get_reminder_task() performs a database query using closing(self._get_connection()) instead of
the shared transaction() context manager required for all DB operations. This can bypass the
project’s standardized commit/rollback handling and consistency guarantees.
Code

tldw_chatbook/Scheduling/db/scheduled_tasks_db.py[R636-641]

+    def get_reminder_task(self, task_id: str) -> Optional[dict[str, Any]]:
+        """Fetch a reminder task by local id."""
+        with closing(self._get_connection()) as conn:
+            cursor = conn.execute(
+                "SELECT * FROM reminder_tasks WHERE id = ?", (task_id,)
+            )
Relevance

⭐⭐ Medium

No historical standard found requiring transaction() for all DB ops; referenced Scheduling/db path
not found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1011851 requires using the shared transaction() context manager for all DB
operations. The changed method get_reminder_task() executes SELECT using a raw connection
context instead.

Rule 1011851: Use transaction() context manager for all database operations
tldw_chatbook/Scheduling/db/scheduled_tasks_db.py[636-641]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A DB query is executed outside the shared `transaction()` context manager.

## Issue Context
Project compliance requires all DB operations (including reads) to run inside `transaction()` for consistent handling.

## Fix Focus Areas
- tldw_chatbook/Scheduling/db/scheduled_tasks_db.py[636-644]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. Local owner sync pollution 🐞 Bug ≡ Correctness
Description
SchedulingService.sync_now() always runs a server-backed sync for the provided owner_id, so
triggering sync while owner_id is "local" will still pull server reminders and write them into the
local owner scope. This can mix server reminders into the local queue and corrupt isolation between
local and server owners.
Code

tldw_chatbook/Scheduling/services/scheduling_service.py[R269-272]

+    async def sync_now(self, owner_id: str | None = None) -> None:
+        """Trigger a full sync for the given owner (defaults to current owner)."""
+        target_owner = owner_id if owner_id is not None else self.owner_id
+        await self.sync_engine.sync_now(target_owner)
Relevance

⭐⭐ Medium

No historical evidence on local-vs-server owner isolation; referenced Scheduling paths not present
on default branch.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
SchedulingService.sync_now() always calls into the sync engine; the sync engine always pulls
server reminders and applies them using the passed owner_id; and the UI triggers sync for whatever
the current owner_id is (including "local").

tldw_chatbook/Scheduling/services/scheduling_service.py[269-276]
tldw_chatbook/Scheduling/services/sync_engine.py[197-217]
tldw_chatbook/UI/Screens/scheduling/schedules_workbench.py[569-595]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`SchedulingService.sync_now()` always delegates to `SyncEngine.sync_now()` regardless of whether the current owner is local or server-scoped. Because `SyncEngine._network_phase()` unconditionally pulls server reminders and applies them under the passed `owner_id`, calling sync while `owner_id == "local"` can import server reminders into the local owner scope.

### Issue Context
- `_use_server()` already defines when server operations should run (`owner_id` starts with `server:`), but `sync_now()` does not use it.
- The UI calls `service.sync_now(service.owner_id)` without checking owner/source.

### Fix Focus Areas
- tldw_chatbook/Scheduling/services/scheduling_service.py[269-276]
- tldw_chatbook/Scheduling/services/sync_engine.py[197-217]
- tldw_chatbook/UI/Screens/scheduling/schedules_workbench.py[569-595]

### Expected fix shape
- In `SchedulingService.sync_now()`: no-op (or raise a user-facing error) unless `target_owner.startswith("server:")`.
- Defense-in-depth: in `SyncEngine.sync_now()` / `_network_phase()`, reject non-`server:` owners (so callers cannot accidentally sync into local scope).
- In UI: disable/guard the Sync action when owner is local (optional but helps prevent misuse).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Invalid sync mutation fields 🐞 Bug ≡ Correctness
Description
SchedulingService.update_reminder() records pending mutations using the full local update payload,
which can include local-only fields like next_run_at. SyncEngine then replays these fields into
ServerNotificationsService.update_reminder(), which does not accept next_run_at, causing replay
failures and leaving the mutation queue stuck.
Code

tldw_chatbook/Scheduling/services/scheduling_service.py[R195-215]

+        if any(key in payload for key in ("schedule_kind", "run_at", "cron", "timezone")):
+            row_task = self._row_to_reminder(row)
+            merged_data = row_task.model_dump()
+            merged_data.update(payload)
+            merged_task = ReminderTask(**merged_data)
+            payload = dict(payload)
+            if merged_task.schedule_kind == ScheduleKind.ONE_TIME:
+                payload["cron"] = None
+                payload["timezone"] = None
+            elif merged_task.schedule_kind == ScheduleKind.RECURRING:
+                payload["run_at"] = None
+            payload["next_run_at"] = self._compute_next_run_at(merged_task)
+
+        self.db.update_reminder_task(task_id, **payload)
+        if use_server:
+            self.db.record_pending_mutation(
+                task_id,
+                _REMINDER_PRIMITIVE,
+                self.owner_id,
+                {"action": "update", "fields": dict(payload)},
+            )
Relevance

⭐⭐ Medium

Correctness bugs sometimes accepted (PR #128), but no similar sync mutation-field precedent found.

PR-#128

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The update fallback path injects next_run_at into the local payload and records the entire payload
as the mutation fields; the sync engine replays **fields into the server update call; and the
server update method signature does not accept next_run_at.

tldw_chatbook/Scheduling/services/scheduling_service.py[193-215]
tldw_chatbook/Scheduling/services/sync_engine.py[249-286]
tldw_chatbook/Notifications/server_notifications_service.py[220-256]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
When the server is unavailable, `SchedulingService.update_reminder()` falls back to a local update and records a pending mutation using `{"action": "update", "fields": dict(payload)}`. The local `payload` may include fields like `next_run_at` that are not accepted by the server `update_reminder()` API. During sync, `SyncEngine._push_mutation()` expands `**fields` into the server call, which can raise `TypeError` for unexpected kwargs and prevents successful push/purge.

### Issue Context
- Server update API only accepts a limited set of kwargs (title/body/schedule_kind/run_at/cron/timezone/link_*/enabled).
- Local update path adds `next_run_at` automatically when schedule-related fields change.

### Fix Focus Areas
- tldw_chatbook/Scheduling/services/scheduling_service.py[193-215]
- tldw_chatbook/Scheduling/services/sync_engine.py[249-286]
- tldw_chatbook/Notifications/server_notifications_service.py[220-256]

### Expected fix shape
- When recording pending mutations for "update": build a server-safe payload by filtering keys to the server-accepted set (and serializing datetimes to ISO strings).
- Consider a shared helper (similar to `_server_create_payload`) for update-mutation payloads.
- Optionally, add a defensive filter in `SyncEngine._push_mutation()` before calling the server client (so older bad rows don’t brick sync forever).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Hardcoded sk-secret123 API key 📘 Rule violation ⛨ Security
Description
The test includes a hardcoded API-key-shaped literal (sk-secret123) in source, which can be
mistaken for a real secret and violates the no-hardcoded-keys policy. Replace it with a clearly fake
placeholder (e.g., sk_test_example_key_do_not_use_in_prod) while keeping the redaction test
intent.
Code

Tests/MCP/test_control_plane_tool_execute.py[R230-233]

+    assert result == {
+        "api_key": "sk-secret123",
+        "data": "ok",
+    }  # returned raw, unredacted
Relevance

⭐⭐ Medium

No repo evidence on hardcoded test keys; likely policy, but not historically enforced here.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 497144 disallows hardcoded API keys/tokens in source and requires clearly fake
placeholders instead. The modified assertion embeds sk-secret123 directly in code.

Rule 497144: Disallow hardcoded API keys in source code
Tests/MCP/test_control_plane_tool_execute.py[230-233]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A hardcoded API-key-looking value (`sk-secret123`) appears in a test assertion.

## Issue Context
This test is validating redaction behavior, but compliance requires avoiding real-looking secret literals in committed source.

## Fix Focus Areas
- Tests/MCP/test_control_plane_tool_execute.py[226-233]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Server conflict leaves mutation 🐞 Bug ☼ Reliability
Description
SyncEngine.resolve_conflict(..., resolution="server") updates/deletes the local record but does not
clear any existing pending_mutations for that local_id, so stale mutations may remain after a
server-authoritative resolution. If such a mutation still exists, the next sync can replay discarded
local changes or repeatedly fail on the same stale payload.
Code

tldw_chatbook/Scheduling/services/sync_engine.py[R356-364]

+        if resolution == "server":
+            if not server_state:
+                self.db.delete_reminder_task(local_id)
+                self.db.delete_sync_mapping(local_id, _REMINDER_PRIMITIVE, owner_id)
+                self.db.delete_tombstone(local_id, _REMINDER_PRIMITIVE, owner_id)
+            else:
+                self.db.update_reminder_task(
+                    local_id, **self._whitelist_reminder_fields(server_state)
+                )
Relevance

⭐⭐ Medium

No historical evidence about clearing pending mutations on server conflict resolution.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The server-resolution branch modifies/deletes local records and mappings but never calls into DB
pending-mutation deletion, even though such a helper exists, leaving a clear path for stale mutation
rows to persist post-resolution.

tldw_chatbook/Scheduling/services/sync_engine.py[341-364]
tldw_chatbook/Scheduling/db/scheduled_tasks_db.py[1125-1139]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
In `SyncEngine.resolve_conflict()`, choosing `resolution == "server"` makes the server state authoritative but does not delete pending mutations for the affected record. If pending mutations still exist, they can be replayed after the conflict is resolved, undermining the selected resolution or creating repeated sync errors.

### Issue Context
- `ScheduledTasksDB` provides `delete_pending_mutation_for_record()`.
- `resolve_conflict()` already computes `pending_mutation`, but only uses it on the "local" branch.

### Fix Focus Areas
- tldw_chatbook/Scheduling/services/sync_engine.py[341-412]
- tldw_chatbook/Scheduling/db/scheduled_tasks_db.py[1125-1139]

### Expected fix shape
- In the `resolution == "server"` branch, delete pending mutations for `(local_id, primitive, owner_id)`.
- Consider also clearing/adjusting tombstones depending on server_state (e.g., if server says deleted, ensure no leftover delete/create mutations remain).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Sync failures reported success 🐞 Bug ◔ Observability
Description
SyncEngine.sync_now() catches ServerClientError, records it, and returns normally instead of
surfacing failure to the caller. SchedulesWorkbench treats normal return as success and posts
SyncCompleted ("Sync completed.") even when sync actually failed.
Code

tldw_chatbook/Scheduling/services/sync_engine.py[R120-132]

+        except ServerClientError as exc:
+            self._record_sync_error(str(exc), target_owner)
+            return
+        except Exception as exc:  # noqa: BLE001
+            logger.exception(f"Sync network phase failed for {target_owner}: {exc}")
+            self._record_sync_error(str(exc), target_owner)
+            return
+
+        try:
+            with self.db.transaction() as conn:
+                # Ensure pulled items have safe defaults before the DB helper
+                # copies fields verbatim.
+                for item in pulled_items:
Relevance

⭐⭐ Medium

No prior evidence about surfacing sync failures; no similar accepted/rejected patterns located.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The sync engine explicitly returns on ServerClientError after recording the error, and the UI code
posts SyncCompleted solely based on the absence of an exception.

tldw_chatbook/Scheduling/services/sync_engine.py[106-126]
tldw_chatbook/UI/Screens/scheduling/schedules_workbench.py[584-599]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`SyncEngine.sync_now()` swallows `ServerClientError` by recording a sync error and returning, which makes the operation appear successful to callers. The UI (`SchedulesWorkbench._run_sync`) posts `SyncCompleted` after `await service.sync_now(owner_id)` completes without raising, producing a misleading success notification.

### Issue Context
- Errors are persisted in `sync_errors`, but callers currently have no explicit success/failure signal.

### Fix Focus Areas
- tldw_chatbook/Scheduling/services/sync_engine.py[106-126]
- tldw_chatbook/UI/Screens/scheduling/schedules_workbench.py[584-599]

### Expected fix shape
- Change the sync API to return an explicit result (e.g., `bool success` or a small result object with `success/error/conflict_count`).
 - Alternatively: re-raise after recording, and let UI show SyncFailed.
- Update `SchedulesWorkbench._run_sync` to post `SyncFailed` when sync fails (by exception or by returned status).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

9. list_reminder_tasks() docstring non-Google 📘 Rule violation ⚙ Maintainability
Description
The new public method list_reminder_tasks() has a minimal docstring but lacks required
Google-style Args: and Returns: sections. This reduces API clarity and violates the docstring
standard for new/modified public callables.
Code

tldw_chatbook/Scheduling/db/scheduled_tasks_db.py[R657-663]

+    def list_reminder_tasks(
+        self,
+        owner_id: Optional[str] = None,
+        enabled: Optional[bool] = None,
+        status: Optional[str] = None,
+    ) -> list[dict[str, Any]]:
+        """List reminder tasks with optional filters."""
Relevance

⭐ Low

Maintainability/style nits often rejected (e.g., similar refactor/cleanup suggestions rejected in
PRs #99, #128).

PR-#99
PR-#128

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 497152 requires Google-style docstrings (including Args:/Returns: when
applicable) for new/modified public methods. list_reminder_tasks() is new/modified and documents
neither its parameters nor its return value in the required format.

Rule 497152: Enforce Google-style docstrings with Args/Returns/Raises sections
tldw_chatbook/Scheduling/db/scheduled_tasks_db.py[657-663]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`list_reminder_tasks()` is missing Google-style docstring sections (`Args:`, `Returns:`).

## Issue Context
The compliance standard requires Google-style docstrings for new/modified public functions/methods.

## Fix Focus Areas
- tldw_chatbook/Scheduling/db/scheduled_tasks_db.py[657-663]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ  8 issues published inline · 9 in summary

Qodo Logo

async def sync_now(self) -> None:
"""Trigger a full sync for the current owner."""
await self.sync_engine.sync_now()
async def sync_now(self, owner_id: str | None = None) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

6. Local owner sync pollution 🐞 Bug ≡ Correctness

SchedulingService.sync_now() always runs a server-backed sync for the provided owner_id, so
triggering sync while owner_id is "local" will still pull server reminders and write them into the
local owner scope. This can mix server reminders into the local queue and corrupt isolation between
local and server owners.
Agent Prompt
### Issue description
`SchedulingService.sync_now()` always delegates to `SyncEngine.sync_now()` regardless of whether the current owner is local or server-scoped. Because `SyncEngine._network_phase()` unconditionally pulls server reminders and applies them under the passed `owner_id`, calling sync while `owner_id == "local"` can import server reminders into the local owner scope.

### Issue Context
- `_use_server()` already defines when server operations should run (`owner_id` starts with `server:`), but `sync_now()` does not use it.
- The UI calls `service.sync_now(service.owner_id)` without checking owner/source.

### Fix Focus Areas
- tldw_chatbook/Scheduling/services/scheduling_service.py[269-276]
- tldw_chatbook/Scheduling/services/sync_engine.py[197-217]
- tldw_chatbook/UI/Screens/scheduling/schedules_workbench.py[569-595]

### Expected fix shape
- In `SchedulingService.sync_now()`: no-op (or raise a user-facing error) unless `target_owner.startswith("server:")`.
- Defense-in-depth: in `SyncEngine.sync_now()` / `_network_phase()`, reject non-`server:` owners (so callers cannot accidentally sync into local scope).
- In UI: disable/guard the Sync action when owner is local (optional but helps prevent misuse).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

else None
)
if pending_mutation and pending_mutation.get("fields"):
if resolution == "server":

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

8. Server conflict leaves mutation 🐞 Bug ☼ Reliability

SyncEngine.resolve_conflict(..., resolution="server") updates/deletes the local record but does not
clear any existing pending_mutations for that local_id, so stale mutations may remain after a
server-authoritative resolution. If such a mutation still exists, the next sync can replay discarded
local changes or repeatedly fail on the same stale payload.
Agent Prompt
### Issue description
In `SyncEngine.resolve_conflict()`, choosing `resolution == "server"` makes the server state authoritative but does not delete pending mutations for the affected record. If pending mutations still exist, they can be replayed after the conflict is resolved, undermining the selected resolution or creating repeated sync errors.

### Issue Context
- `ScheduledTasksDB` provides `delete_pending_mutation_for_record()`.
- `resolve_conflict()` already computes `pending_mutation`, but only uses it on the "local" branch.

### Fix Focus Areas
- tldw_chatbook/Scheduling/services/sync_engine.py[341-412]
- tldw_chatbook/Scheduling/db/scheduled_tasks_db.py[1125-1139]

### Expected fix shape
- In the `resolution == "server"` branch, delete pending mutations for `(local_id, primitive, owner_id)`.
- Consider also clearing/adjusting tombstones depending on server_state (e.g., if server says deleted, ensure no leftover delete/create mutations remain).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

pending_local_ids,
mutations,
) = await self._network_phase(target_owner)
except ServerClientError as exc:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

9. Sync failures reported success 🐞 Bug ◔ Observability

SyncEngine.sync_now() catches ServerClientError, records it, and returns normally instead of
surfacing failure to the caller. SchedulesWorkbench treats normal return as success and posts
SyncCompleted ("Sync completed.") even when sync actually failed.
Agent Prompt
### Issue description
`SyncEngine.sync_now()` swallows `ServerClientError` by recording a sync error and returning, which makes the operation appear successful to callers. The UI (`SchedulesWorkbench._run_sync`) posts `SyncCompleted` after `await service.sync_now(owner_id)` completes without raising, producing a misleading success notification.

### Issue Context
- Errors are persisted in `sync_errors`, but callers currently have no explicit success/failure signal.

### Fix Focus Areas
- tldw_chatbook/Scheduling/services/sync_engine.py[106-126]
- tldw_chatbook/UI/Screens/scheduling/schedules_workbench.py[584-599]

### Expected fix shape
- Change the sync API to return an explicit result (e.g., `bool success` or a small result object with `success/error/conflict_count`).
  - Alternatively: re-raise after recording, and let UI show SyncFailed.
- Update `SchedulesWorkbench._run_sync` to post `SyncFailed` when sync fails (by exception or by returned status).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@github-actions

Copy link
Copy Markdown

Test Results Summary

Overall Summary

  • Total Tests: 4468
  • Passed: 4170 (93.3%)
  • Failed: 0
  • Skipped: 298
  • Total Duration: 6398.07s

Status: ✅ All tests passed!

✅ Unit Tests macos - latest - 3.11

  • Total: 459
  • Passed: 459 (100.0%)
  • Failed: 0
  • Skipped: 0
  • Duration: 527.53s

✅ Unit Tests ubuntu - latest - 3.12

  • Total: 459
  • Passed: 459 (100.0%)
  • Failed: 0
  • Skipped: 0
  • Duration: 530.92s

✅ Integration Tests 3.12

  • Total: 398
  • Passed: 250 (62.8%)
  • Failed: 0
  • Skipped: 148
  • Duration: 563.64s

✅ Unit Tests macos - latest - 3.13

  • Total: 459
  • Passed: 459 (100.0%)
  • Failed: 0
  • Skipped: 0
  • Duration: 666.40s

✅ Unit Tests macos - latest - 3.12

  • Total: 459
  • Passed: 459 (100.0%)
  • Failed: 0
  • Skipped: 0
  • Duration: 544.23s

✅ Integration Tests 3.11

  • Total: 398
  • Passed: 250 (62.8%)
  • Failed: 0
  • Skipped: 148
  • Duration: 485.35s

✅ Unit Tests windows - latest - 3.12

  • Total: 459
  • Passed: 458 (99.8%)
  • Failed: 0
  • Skipped: 1
  • Duration: 858.33s

✅ Unit Tests ubuntu - latest - 3.13

  • Total: 459
  • Passed: 459 (100.0%)
  • Failed: 0
  • Skipped: 0
  • Duration: 784.91s

✅ Unit Tests ubuntu - latest - 3.11

  • Total: 459
  • Passed: 459 (100.0%)
  • Failed: 0
  • Skipped: 0
  • Duration: 698.52s

✅ Unit Tests windows - latest - 3.11

  • Total: 459
  • Passed: 458 (99.8%)
  • Failed: 0
  • Skipped: 1
  • Duration: 738.23s

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.

1 participant