The pre-replace import backup and the USE_REMOTE rebuild recovery
marker were matched by timestamp only, so a concurrent capture or a
reload between rebuild completion and snack dismissal could clear or
restore the wrong backup.
- Import backups carry an opaque backupId (uuidv7); clearImportBackup,
replaceAllForRawRebuild and applyRemoteReplaceWithSnapshot verify the
expected identity inside their transactions before acting.
- Completing a raw rebuild atomically replaces the incomplete marker
with a durable raw_rebuild_recovery entry, so the "restore previous
data" Undo survives a reload; StartupService re-offers it at boot and
dismissal retires exactly the offered backup.
- SnackService treats a sticky recovery/update action as a single
persistent slot: unrelated transient snacks no longer destroy a
visible Undo, and dismissal awaits the snack's dismissFn.
A committed reducer must never be durable without its vector clock, and
a merged clock must never be durable without its reducer checkpoint —
either mismatch lets the next local operation be causally older than
state already visible in NgRx after a crash.
- markArchivePending + separate mergeRemoteOpClocks are replaced by one
markReducersCommittedAndMergeClocks transaction (ops + vector_clock);
the clock math is extracted into a pure calculateRemoteClockMerge so
the standalone merge path keeps identical full-state-reset semantics.
- The sync-core RemoteOperationApplyStorePort gains an
onRemoteClocksDurable hook so deferred local actions drain exactly
when clocks are durable, not merely when ops were applied; a
checkpoint rejection can no longer mask the primary apply error.
- Conflict resolution's local-wins path writes remote losers and rebased
local compensations in one appendMixedSourceBatchSkipDuplicates
transaction, so synthetic ops cannot reuse or regress the client
counter.
- DB_VERSION 7 -> 8 as a deliberate downgrade barrier: released v7
readers only understand 'failed' and would silently overlook
outstanding 'archive_pending' work.
op-log suite and sync-core suite cover the checkpoint rollback, atomic
clock merge, mixed-source ordering and drain failure matrix.
Remediate the findings of the 5-agent necessity review:
- USE_REMOTE: a local capture racing the locked rebuild now throws a
typed CaptureRacedRebuildError, and forceDownloadRemoteState retries
phase 2 in-call (bounded, 3 attempts) through the existing
crash-resume branch — raced ops fold into preservedLocalOps and the
already-downloaded history is reused (WS downloads and immediate
uploads stay gated by the marker, so no re-download is needed).
Previously every attempt aborted while e.g. time tracking dispatched
continuously, re-downloading the full history per sync trigger and
churning the Undo snack (now shown on final failure only).
- Hydrator archive retry: pass skipDeferredLocalActions and drain
explicitly with a caught error. A drain throw from the coordinator's
finally used to mask the archive result and escalate out of
hydrateStore() into attemptRecovery(), which can import stale legacy
data over a correctly hydrated store.
- Incomplete-remote gate: run one in-session archive-only retry when
the only blockers are quarantined failed/archive_pending ops, so a
transient archive failure self-heals on the next sync instead of
wedging until app restart. Never attempted while the rebuild marker
is set or reducer-uncommitted pending rows exist.
- Boot recovery: StartupService surfaces the pre-replace backup's
persistent restore snack when a stranded raw_rebuild_incomplete
marker is found, covering users who boot offline or disable sync
after finding the app "emptied" by a mid-rebuild crash.
- Snack correctness: latch the USE_REMOTE newer-schema warning once per
session; guard _notifyBlockedOp and the LWW apply-failure snack with
hasPendingPersistentAction() so they cannot destroy a visible Undo;
drop the useless "Update app" action for below-minimum data.
- Strings: MIGRATION_FAILED / VERSION_UNSUPPORTED now describe the
blocking semantics instead of the removed skip behavior.
- Store: getPendingRemoteOps excludes rejected rows (parity with
getFailedRemoteOps) so a rejected-but-pending row cannot trip the
sync gate for a session.
- Server: compute the upload request fingerprint eagerly after the
rate-limit gate — identical cost to the lazy closure in every path,
minus the memoization machinery; laziness remains in the snapshot
handler where it skips hashing multi-MB states.
op-log suite 3004/3004, super-sync-server 831/831, checkFile clean on
all touched files. Nine regression tests pin the new behavior.
Fresh clients generate startup config and genesis operations before their first sync. Treat those operations as setup state until a prior sync exists, while continuing to protect later config changes and all ordinary user work from authoritative snapshot replacement.
Multi-review remediation for the USE_REMOTE rebuild path.
- Crash-resume recovery gap (confirmed by 2 independent reviewers): when an
interrupted-rebuild resume aborts in its download/validate phase (empty or
newer-schema remote, or a persistent download failure), forceDownloadRemoteState
threw before it could offer Undo. The prior attempt had already committed the
destructive baseline, so the pre-replace backup was stranded with no restore
affordance — reading as total data loss. downloadRemoteOps now surfaces the
recovery Undo on a resume abort (deduped via hasPendingPersistentAction so
repeated auto/WS syncs don't respawn it). Covered by two new unit tests.
- SnackService: collapse the redundant dual write of the persistent-action flag
(set in open() AND recomputed in _openSnack) to one authoritative write in
open(); _openSnack keeps only the on-dismiss clear (3-reviewer consensus).
- Document the archive-retry idempotency invariant on ARCHIVE_AFFECTING_ACTION_TYPES:
the hydrator retry re-runs archive side effects with skipReducerDispatch even
after a fully-successful run, so they must stay idempotent (overwrite, never
additive) to avoid double-counting time-tracking/counter deltas.
After a "Use Server Data" replace shows the persistent Undo recovery
snack (#8107), a follow-up routine sync-success snack must not silently
replace it. SnackService now tracks a pending persistent action
(actionStr + duration 0); the header sync() skips its success feedback
while one is showing. Unblocks the committed USE_REMOTE crash-resume e2e
(supersync-use-remote-crash-resume.spec.ts), whose Undo assertion
depends on the recovery snack surviving the sync that resumed the rebuild.
Reloads client B at the exact atomic-baseline-commit cutoff, then
verifies the next sync detects the interrupted rebuild, redoes the raw
download, keeps the first attempt's pre-replace backup, finishes on the
remote state, and offers an Undo that restores B's original import.
USE_REMOTE's atomic rebuild synthesizes a globalConfig shell from
DEFAULT_GLOBAL_CONFIG (getDefaultMainModelData omits globalConfig), then
re-applies the device's local-only sync settings (provider, isEnabled,
isEncryptionEnabled, syncInterval, isManualSyncOnly) onto the baseline.
Without this, an interrupted rebuild committed a baseline whose sync
config was pure defaults, so the resumed client could lose the provider
and schedule it needs to sync again. Adds a unit test asserting the
device-local settings survive into runRemoteStateReplacement's baseline.
The integration spec still pinned the pre-branch semantics where a
pending GLOBAL_CONFIG op counted as non-meaningful. The widened gate
(79f91e36fe) deliberately treats config changes as meaningful user work
(entity-wide exemptions are unsafe — GLOBAL_CONFIG carries synced
preferences), pinned by the gate unit spec but missed here, so the full
suite failed since that commit. Split the case: example-only pending
stays silent + discardable; config pending now expects the dialog.
- USE_REMOTE: fix guaranteed self-deadlock — flushPendingWrites() was
called while already holding the non-reentrant sp_op_log lock its own
Phase 2 re-acquires, so every "Use Server Data" hung 30s and aborted.
New OperationWriteFlushService.flushThenRunExclusive owns the bounded
flush->lock->recheck barrier; ServerMigrationService reuses it, which
also bounds its previously unbounded snapshot-cutoff retry loop.
- USE_REMOTE: make the rebuild crash-resumable. A raw_rebuild_incomplete
META marker is set atomically with the baseline replacement and cleared
after the replay commits; the next sync redoes the raw rebuild instead
of the normal download (which excludes own ops server-side and would
silently lose the un-replayed suffix). The resume keeps the first
attempt's pre-replace backup instead of overwriting the single slot
with the partial baseline.
- Deferred actions: serialize overlapping drains (two concurrent drains
could mint two ops for one user intent), stop the drain at the first
transient failure instead of persisting successors out of order (the
older edit would win LWW everywhere via clock inversion), abandon
permanently invalid actions after one attempt (typed
PermanentDeferredWriteError) instead of retrying + snacking on every
sync forever, and latch the buffer-size warnings to once per stuck
window (previously a blocking dev dialog per action past 100); drop
the no-op 5000 "hard cap" tier.
- writeOperation: post-append bookkeeping failures no longer propagate
into the deferred retry loop, which re-appended the same action under
a fresh op id (double-apply of additive payloads).
- remote-apply: full-state cleanup retains every full-state op of the
current batch, so an archive_pending import can no longer be deleted
before its archive retry (startup replay would lose a change already
visible at runtime).
- Hydration: sanitize a malformed stored schemaVersion per-op instead of
failing the whole boot into recovery; strict parsing stays on the
receive/upload paths (0 still parses so a below-minimum remote op
surfaces as VERSION_UNSUPPORTED, not a generic migration failure).
- Server: request fingerprints are computed lazily — only when a dedup
entry exists, and otherwise after the rate-limit/pre-quota gates but
before uploadOps mutates the parsed ops. Fixes the pre-gate hashing of
full payloads (authenticated DoS-cost regression) and repairs three
sync-fixes retry tests to model true identical-body retries.
- Delete dead code: failed-op-ids util (+spec) and MAX_VERSION_SKIP (the
removed forward-compat band); fix the stale clock-merge parity comment.
sync-core 210/210, shared-schema 50/50, super-sync-server 825/825, and
all touched Angular specs pass.
getNamedClickCounter hovered the counter button and waited for its
matTooltip title, but matTooltip does not open on Playwright's synthetic
hover in headless CI — the assertion timed out at PHASE 1 before the gate
logic ran (run 29098937743, SuperSync 1/6). Each client has exactly one
counter at every interaction point here, so assert the button's rendered
initial instead of hovering for a tooltip.
- USE_REMOTE: fix guaranteed self-deadlock — flushPendingWrites() was
called while already holding the non-reentrant sp_op_log lock its own
Phase 2 re-acquires, so every "Use Server Data" hung 30s and aborted.
New OperationWriteFlushService.flushThenRunExclusive owns the bounded
flush->lock->recheck barrier; ServerMigrationService reuses it, which
also bounds its previously unbounded snapshot-cutoff retry loop.
- USE_REMOTE: make the rebuild crash-resumable. A raw_rebuild_incomplete
META marker is set atomically with the baseline replacement and cleared
after the replay commits; the next sync redoes the raw rebuild instead
of the normal download (which excludes own ops server-side and would
silently lose the un-replayed suffix). The resume keeps the first
attempt's pre-replace backup instead of overwriting the single slot
with the partial baseline.
- Deferred actions: serialize overlapping drains (two concurrent drains
could mint two ops for one user intent), stop the drain at the first
transient failure instead of persisting successors out of order (the
older edit would win LWW everywhere via clock inversion), abandon
permanently invalid actions after one attempt (typed
PermanentDeferredWriteError) instead of retrying + snacking on every
sync forever, and latch the buffer-size warnings to once per stuck
window (previously a blocking dev dialog per action past 100); drop
the no-op 5000 "hard cap" tier.
- writeOperation: post-append bookkeeping failures no longer propagate
into the deferred retry loop, which re-appended the same action under
a fresh op id (double-apply of additive payloads).
- remote-apply: full-state cleanup retains every full-state op of the
current batch, so an archive_pending import can no longer be deleted
before its archive retry (startup replay would lose a change already
visible at runtime).
- Hydration: sanitize a malformed stored schemaVersion per-op instead of
failing the whole boot into recovery; strict parsing (floor now 1,
matching the server contract) stays on the receive/upload paths.
- Server: request fingerprints are computed lazily — only when a dedup
entry exists, and otherwise after the rate-limit/pre-quota gates but
before uploadOps mutates the parsed ops. Fixes the pre-gate hashing of
full payloads (authenticated DoS-cost regression) and repairs three
sync-fixes retry tests to model true identical-body retries.
- Delete dead code: failed-op-ids util (+spec) and MAX_VERSION_SKIP (the
removed forward-compat band); fix the stale clock-merge parity comment.
sync-core 210/210, shared-schema 50/50, super-sync-server 825/825, and
all touched Angular specs pass.
Client A already synced a populated state, so importing a backup diverges
from the server and A's own sync raises the sync-import conflict gate.
syncAndWait() defaulted to USE_REMOTE, which discarded A's import before it
reached the server, leaving Client B with nothing to conflict against, so
the PHASE 5 conflict dialog never appeared (run 29090279243, SuperSync 1/6).
Pass { useLocal: true } so A force-uploads the import as a new SYNC_IMPORT
the server keeps, letting B's pending simple-counter change trigger the
conflict dialog as intended.
Make remote replay and authoritative state replacement crash-resumable and atomic. Preserve pending changes until piggyback processing commits, and stop cleanly on incompatible operations. Validate sync identities and schemas while strengthening request deduplication and privacy-safe diagnostics.
Regression test for the widened incoming-full-state conflict gate and the
piggyback pre-upload pending-snapshot fix: a pending SIMPLE_COUNTER change
(non-TASK/PROJECT/TAG/NOTE) must trigger the conflict dialog instead of
being silently discarded by an incoming SYNC_IMPORT from another client.
Not yet run (SuperSync docker stack is unavailable in this sandbox); run via
npm run e2e:supersync:file or the E2E Tests (Scheduled) workflow.
Address findings from a full sync-system review (blockers + high/medium):
- USE_REMOTE ("Use Server Data") is now a true download-first rebuild:
fetches the complete server history (incl. own/already-known ops via a
raw-download mode) and validates it before any local mutation; aborts
untouched on download failure, empty remote, or newer-schema ops.
- Widen the incoming full-state conflict gate to treat all user work as
meaningful (not just TASK/PROJECT/TAG/NOTE CUD); fix the piggyback race
by judging against a pre-upload pending snapshot.
- Stop advancing the server cursor past ops blocked by schema/migration
failures so they are retried after an app update; block any op from a
newer schema version (no forward-compat band).
- Retry of failed remote ops runs archive side effects only, never
re-dispatching reducers whose effects already committed (fixes additive
double-apply of time-tracking/counter deltas across hydration+retry).
- Use local monotonic seq (not lexical UUIDv7) to decide which ops are
covered by an uploaded snapshot, preventing dropped unsynced ops under
clock rollback.
- Server: include entityIds in duplicate-operation equality.
- Capture buffer no longer silently drops accepted actions at 100 (warns
to reload); drop only past a 5000 pathological cap.
Adds regression coverage for each fix. sync-core 209/209,
super-sync-server 817/817, and all touched Angular specs pass.
The Claude Code runtime scaffolds .claude/ each session and materializes
.claude/skills as a real directory, clobbering the committed symlink
(-> ../.agents/skills). Git then reported a perpetual 'D .claude/skills' in
every worktree/session. Skill sources remain tracked under .agents/skills/;
let the runtime own .claude/skills and ignore it via the existing /.claude/*
rule.
Contributors on npm 10 (Node 22's bundled default) vs npm 11 reconcile the
app-builder-lib.minimatch override differently, rewriting package-lock.json on
npm install. Pin npm 11.7.0 via the standard packageManager field (Corepack)
and complete the existing volta block so volta users pick it up automatically.
Ref: discussion #8869
* feat(eml): add EML file parsing and task creation on drop
Currently a simple skeleton, needs more work, but should be a good POC before refining.
Fixes#518
* fix(main-header): correct email message formatting and handle EML upload errors
while this commit looks bad, its a first commit before refining this solution and sending the final PR.
Fixes#518
* refactor(eml): move eml-parser to util and tighten isFileEml check
* feat(eml): on eml hover error parse error to log and show a snack message
* bugfix(eml-typecheck): tighten type for 'sender' to not have bugs
* refactor(eml-drop): updated file locations for each file drop
* bugfix(eml-drop): use stable npm package for parsing eml files
* bugfix(eml-drop): remove relevant "dev" flag
* refactor(eml-drop): drop empty emails with a snack instead of throwing
* test(eml-drop): add isFileEml and parseEml unit tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* rafactor(eml-service): refactor code to be consistent as written in issue number 3
* bugfix(eml-drop): remove file name when geting an error
as written in 1. inside the pr
* docs: update task creation instructions to include .eml file support
* refactor(eml-parser): update import statement for PostalMime to dynamic import
* refactor(eml-drop): enhance file drop handling and improve error logging
* test(eml-drop): add unit tests for EmlDropDirective and EmlDropService
* feat(tasks): save eml body to task notes and harden eml parsing
- capture the parsed plain-text email body into task notes (text/plain
only, never HTML, to avoid injecting untrusted markup into markdown notes)
- pass the File/Blob straight to postal-mime so it applies the message's
own charset/transfer-encoding instead of forcing UTF-8 via file.text()
- use relative import paths to match repo convention
- document why the drop handler relies on the global dragover preventDefault
- update wiki wording + tests for the new add() signature and notes behavior
* fix(tasks): harden eml-to-task against untrusted subject/size
Follow-up to the eml-drop feature from a multi-agent review:
- suppress short-syntax on imported email subjects: email content is
untrusted external input, but TaskService.add() dispatches addTask which
ShortSyntaxEffects parses for #tag/@date/+project/URL tokens — stripping
them from the intended literal "sender: subject" title and mutating
tags/scheduling/attachments. Thread isIgnoreShortSyntax through add()
(matching the existing plugin-bridge pattern) and set it here.
- guard against oversized .eml files (postal-mime parses synchronously on
the main thread and the body syncs to every device) with a 10MB cap +
EML_TOO_LARGE snack.
- trim sender/subject so whitespace-only headers don't bypass the empty
check or block the address fallback.
- drop the stale "add attachment" TODO.
* fix(tasks): log bounded eml parse reason, not the raw error
The source is untrusted email content and the log history is exportable
(rule #9). Log only e.message (postal-mime's throw messages are structural,
so no email content leaks) instead of the raw error object + stack.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
Resolve the terminology collision where "Due" labeled both the deadline
and the planned-day concept. Standardize UI state labels on "Deadline"
(deadlineDay/deadlineWithTime) and "Planned" (dueDay/dueWithTime), and
move the deadline-passed label off "Overdue!" to "Past deadline!" so it
no longer clashes with the planned-past Overdue list.
en.json values only; keys unchanged (t.const.ts regenerates identically).
The "Schedule" action verb/keybinding and the issue-provider "Due Date"
field are intentionally left. The behavioral question of what "Overdue"
should mean is split out to #8877.
* feat(tasks): create "Check <url>" task from links dropped on empty app chrome
Dropping a web link anywhere on the app except onto a task, note, or panel
now creates a "Check <url>" task with a success snack, mirroring the Android
share-a-link flow. Task/note/panel drop handlers already stopPropagation, so
the document-level handler only sees links dropped on empty chrome.
- add pure getDroppedUrl() helper (http(s) only; rejects files, plain text,
non-web schemes, and whitespace/multiline selections)
- wire the existing global onDrop in AppComponent to create the task
- add APP.DROP_LINK translations
* fix(tasks): robustly extract dropped link URL; stop notes double-fire
The dropped-link → task feature could no-op for real link drags: getDroppedUrl
only read text/plain and rejected anything with inner whitespace, but browsers
often deliver the URL only in text/uri-list, or as "<url>\n<title>" in
text/plain (and Electron cross-app drops fill only one). Now scan both types
line by line and take the first exact http(s) URL.
Also fix a duplicate-task bug found in review: dropping a non-image link on the
Notes panel created BOTH a note and a "Check" task, because NoteService only
calls stopPropagation after an await (too late). Claim the event synchronously
in the notes drop handler.
* feat(tasks): give dropped-link tasks a readable title + clickable attachment
Mirror the Android share flow more closely: the dropped-link task now gets a
human-readable title ("Check example.com: some article") plus the raw URL as a
clickable LINK attachment, instead of a raw URL crammed into the title.
Extract the shared readableUrl() helper out of android.effects into
util/readable-url.ts (now used by both the Android share effect and the desktop
drop handler) with its own spec.
SHA-pinning the actions in wiki-sync.yml (#8868) pushed the
setup-python `uses:` line to 84 chars, tripping yamllint's default
80-char line-length rule and failing the Lint job on every wiki push
and every PR touching wiki paths.
Relax the yamllint step to accept the 40-char SHA pins and their
single-space `# vX.Y.Z` version comments. Tightening the comments to
two spaces instead would push the already-80-char pin lines over the
limit, so raising the ceiling is the correct fix.
task.component's `isTodayListActive` computed read the plain mutable
`WorkContextService.isTodayList` boolean, which is not a signal producer,
so the computed never invalidated and returned its first value for the
component's lifetime.
Expose `isTodayListSignal` (a toSignal mirror of isTodayList$) on the
service and read that from the computed. The plain boolean stays for the
synchronous reader in history.component.
#8843
* perf(tasks): content-stable scheduling snapshot for collection selectors
The collection selectors (overdue, due-time-sorted, deadline-sorted,
due-day, later-today, all-with-subtasks, and today-task-ids) all derive
from selectAllTasks, which returns a new array every second while a task
tracks time. None of them read timeSpent, yet each re-ran its O(n)
filter/sort and reallocated every tick.
Introduce selectTaskSchedulingSnapshot: a per-entity ref-cached, array-
ref-stable projection of the scheduling fields only (id, isDone,
dueDay/dueWithTime, deadlineDay/deadlineWithTime, parentId, subTaskIds),
so a timeSpent-only tick yields the identical snapshot ref. Each
collection selector now derives its filter/sort DECISION (an ordered id
list) from the snapshot — skipped by NgRx memoization when the snapshot
is stable — and a per-selector memoized re-map turns the stable id list
back into live task entities, keeping the existing output types and
returning the previous array ref unless a genuine member changed. No
consumer/API changes; task refs stay live (no staleness).
selectTimeConflictTaskIds is intentionally left on the live sorted
selector because getTimeLeftForTask reads timeSpent.
SPAP-20
* test(tasks): clear leaked selectAllTasksWithDueTimeSorted override between tests
dialog-schedule-task.* and time-block-sync.effects specs call
store.overrideSelector(selectAllTasksWithDueTimeSorted, ...). overrideSelector
uses setResult(), which sticks on the singleton selector across spec files and
is only reset by clearResult() (not release()). When one of those specs ran
before task.selectors.spec in the full karma bundle, the new
selectAllTasksWithDueTimeSorted(mockState) test got the leaked value instead of
computing from mockState — "Expected [ ] to contain 'task5'". Single-file runs
passed, so it only surfaced in CI's full run.
Add the selector to the existing beforeEach clearResult()/release() defensive
block (mirrors selectOverdueTasks etc.). Repro: dialog-schedule-task +
task.selectors together went 1 FAILED -> 90 SUCCESS.
SPAP-20
Make AGENTS.md the single source of guidance read natively by Claude
Code, OpenAI Codex, and GitHub Copilot. CLAUDE.md becomes a symlink to
it. Extract commit-message guidance into a reusable Agent Skill under
.agents/skills (read by Codex and Copilot); .claude/skills symlinks to
it for Claude Code, and .gitignore is narrowed to /.claude/* so only
that skills pointer is tracked. Drop the now-redundant
.github/copilot-instructions.md symlink since Copilot reads AGENTS.md.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ci(actions): SHA-pin remaining workflow actions
Replace the mutable derekprior/add-autoresponse@master ref in
issue-open-auto-reply.yml with an inline actions/github-script snippet
to remove the third-party code-injection risk in CI. SHA-pin the 4
remaining tag-pinned actions in wiki-sync.yml (checkout, setup-python)
to match the rest of the workflows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* wrong sha for the github-script v9
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(task-repeat): default skip-overdue for new everyday recurring tasks
New recurring configs now default skipOverdue ("Don't let overdue instances
pile up") to ON only for a plain everyday schedule — the "Daily" preset or a
CUSTOM every-single-day cycle. That is the one case where the option is both
useful and provably safe: everyday tasks are the only schedule that actually
piles up (one empty overdue copy per missed day), and today is always
scheduled so a missed instance regenerates the same day and can never
silently vanish (it cannot even drop to zero).
Every other schedule stays OFF — workday/weekly/monthly/yearly and every-N-day
custom cycles keep their one missed occurrence visible, so a real obligation
(pay rent on the 1st, renew the domain) never disappears until its next
occurrence. Deriving the default purely from the effective schedule means the
"Daily" preset and a CUSTOM every-day cycle (the same schedule entered two
ways) get the same default — no "same schedule, different default" surprise.
The default is seeded from the chosen schedule in both config-creation paths:
the repeat dialog (re-derived from the final schedule on save, and skipped
when the user explicitly toggled the Advanced checkbox) and the inline
add-task-bar recurrence. Existing configs and DEFAULT_TASK_REPEAT_CFG
(skipOverdue: false) are unchanged; only newly created configs are affected.
Supersedes the broader daily+Mon-Fri variant, dropping the schedule-change
checkbox re-sync machinery: an Advanced checkbox opened on a Daily config and
then switched may briefly show a stale ON, but save always persists the safe
re-derived value.
* docs(task-repeat): clarify custom every-day default; signpost baseline
Multi-review follow-ups (no behavior change):
- Wiki 2.06 + 4.13: a CUSTOM every-single-day cycle also defaults skipOverdue
ON (it is the same schedule as the Daily preset); "off" now reads "custom
cycles longer than a day" so the docs match getDefaultSkipOverdue.
- Comment on DEFAULT_TASK_REPEAT_CFG.skipOverdue pointing to the schedule-aware
creation default, so the model's `false` baseline is not mistaken for the
effective default.
- Tighten the save() display-gap comment: the cosmetic seeded-ON/persist-OFF
gap applies to any new non-Daily config whose Advanced panel is opened
untouched, not only a Daily→switch.
* refactor(task-repeat): use type-only import in skip-overdue predicate
TaskRepeatCfgCopy is used only in type position; match the codebase's import type convention (58 other files). No behavior change.
compressArchive() persisted archiveYoung and archiveOld via two
independent writes (Promise.all). A crash between them left a
half-compressed archive, and because compression is op-replayed on
other clients, a torn local result diverged from replicas.
Route both writes through the existing saveArchivesAtomic API (one
IndexedDB transaction over both stores), matching the flush-young-to-old
path in archive-operation-handler.service.ts. Adds a regression spec.
#8843
Android/mobile soft keyboards hide the Enter key or deliver it as a composing
keydown that onKeydown filters out (to protect CJK candidate confirmation), so
the inline sub-task draft could not be committed at all: neither Enter, tapping
away, nor any on-screen control saved the text.
- Add an always-visible submit button beside the input as an accessible,
discoverable commit target (mouse-clickable, screen-reader labelled). Its
mousedown is preventDefaulted so a desktop click keeps input focus, commits,
and keeps the field open for rapid entry — mirroring the main add-task bar.
On touch the tap commits via the blur path below.
- Commit the draft on blur, but only on touch (isTouchActive): the natural
mobile "done" gesture is tapping away, and the soft-keyboard Enter is
unreliable there (#8791). Desktop keeps click-away-to-cancel — Enter and the
button are the reliable commit paths — so blur no longer silently creates a
task, and a user can move to the button without the draft being committed out
from under them. Escape still discards on all devices.
Both commit paths read the live input value, so IME/predictive-text buffering
no longer strands the text.
Closes#8856. Supersedes the standalone #8791 commit-on-blur branch.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Restore outlook: (reported in #8859) plus the same-class note/task apps
notion:, things:, omnifocus:, bear:, joplin: that the GHSA-hr87 fix also
silently broke. All low-risk registered-app deep-links, same class as the
already-allowed obsidian:/logseq:/zotero:.