Skip to content

Move app behaviour to the TaskNotes settings record (tasknotes.task rc.6) - #2432

Merged
callumalpass merged 7 commits into
agent/tasknotes-v5-documentationfrom
agent/v5-app-settings-record
Oct 27, 2026
Merged

callumalpass merged 7 commits into
agent/tasknotes-v5-documentationfrom
agent/v5-app-settings-record

Conversation

@callumalpass

@callumalpass callumalpass commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

Moves TaskNotes app behaviour out of the tasknotes.task binding into the TaskNotes settings record, _settings/dev.tasknotes.md. The record uses app dev.tasknotes, scope collection and settings_version 1. Its type is tasknotes-settings, which implements mdbase.app-settings 1.0.0 and tasknotes.settings 1.0.0.

Merge and release order: mdbase-contracts#18 → publish tasknotes-model#25 (0.3.0-rc.20) → TaskNotes App callumalpass/tasknotes-app#311 ships → this plugin upgrades collections. Spec: callumalpass/tasknotes-spec#11.

This branch uses the unpublished model tarball vendor/tasknotes-model-0.3.0-rc.20.tgz (file: dependency) and the rc.19 pack JSON from mdbase-contracts#18. Swap the tarball for the registry version once the model is published.

Moved to the record

  • nlp.triggers
  • all of templating
  • archive.move_on_archive and archive.folder
  • time_tracking
  • recurrence.reset_body_checkboxes
  • links.write_format
  • per-status next_status, exclude_from_cycle, auto_archive and auto_archive_delay_minutes, keyed by status value

Everything else stays in the binding. x-tasknotes-generator is unchanged.

Behaviour

  • Upgrade: rc.3 and rc.5 types are lifted to rc.6, and the record is written in the same journalled metadata transaction, which can be rolled back. v4 (v0.2) migrations write the record from data.json inside the v0.2 journal. Creation only happens if the record is absent, and it is idempotent. If the record already exists, only its absent fields are added.
  • Read precedence: the record, then the earlier (rc.3–rc.5) binding, then local plugin settings. A missing or late record never loses a value.
  • Sync:
    • A local settings change patches only the changed record fields; other keys and the body are kept.
    • An external record edit is applied to the plugin.
    • When both changed, the conflict modal offers "Use settings file".
    • A record with a newer settings_version is read-only.
  • Shared resources: the settings contracts, schemas (schemas/… beside the contracts folder, as the pack lays them out) and the _types/tasknotes-settings.md type are seeded only when absent.
  • UI and migration helpers:
    • portableSettingsFingerprint no longer covers the moved fields.
    • The mdbase settings description mentions the record, in all locales.
    • applyCanonicalTaskTypeToSettings takes {record, legacyBinding}.

Fixes found on the way

  • The v0.2 migration derived contracts_folder from the contract path. With the versioned path _contracts/tasknotes.task/<ver>.md this would have produced _contracts/tasknotes.task.
  • Beta duplicate set-aside now treats rc.5 types as current, because they are being upgraded in this load. Without this, beta3 app-then-plugin collections kept two task types.
  • The v0.2 retry tolerates the canonical settings type in the types folder, which an earlier failed attempt or TaskNotes App may have left there.

Decisions (brief left open)

  • Contract location: the rc.6 contract lives at _contracts/tasknotes.task/0.3.0-rc.6.md. The rc.5 contract at _contracts/tasknotes.task.md is retained as is, matching the pack.
  • Multiple task types: the plugin already refuses to run with more than one current task type. Set-aside beta duplicates get a notice that their app settings were not used.
  • Rollback (changed in fix round 2): rollback and startup recovery now remove files the failed update created (contracts, schemas, _types/tasknotes-settings.md and the settings record), but only while each still holds exactly the bytes written. A file edited since is kept. Recovery also removes abandoned *.tasknotes-stage-* files beside journalled paths, unless a pending swap journal owns them. This reverses the earlier rule that rollback never deletes created files. Re-running is idempotent.
  • Existing record: on the first sync of a session, an existing record's absent fields are filled from the effective settings, so the plugin and the App agree.
  • Status cycle default: exclude_from_cycle defaults to the status's is_skipped.

Risks

  • The model jump rc.16 → rc.20 brings rc.17–rc.19 model changes into v5. Typecheck needed FieldMapping.recurrenceOriginalDate and a displayValue fix.
  • Pack upgrades through mdbase CLI/App conflict on binding/status/definitions for collections with customised statuses, because arrays merge atomically. The plugin's own upgrade path is unaffected.

Validation

  • npm test: 784 suites passed and 70 skipped; 5202 tests passed, 1012 skipped and 4 todo.
  • npm run lint (strict) and npm run typecheck: pass.
  • npm run i18n:verify: all up to date.
  • npm run docs:check: pass.
  • npm run build: pass. check:bundle-size: main.js is 4,796,163 bytes, below the 5,000,000 limit.
  • Isolated Obsidian 1.12.7 (separate profile, vault and port), rc.5 beta3-custom collection, all passing:
    • The type is lifted to rc.6.
    • The record is created from the binding (Legacy/Archive).
    • A settings change patches the record, not the type.
    • An external record edit is applied to the plugin.

Regression tests:

  • v4 → record and rc.5 → record + rc.6 (release fixture matrix)
  • idempotent re-run
  • record already present, with absent fields only and record over binding over local
  • rollback when the record write fails (mdbaseSafeWrites)

Fix round 2 (manual upgrade testing)

  1. High: first moved-setting save after a v4 (v0.2) migration was lost. The migration wrote the record but left recordAbsentFieldsMerged=false. The first save then took the first-sight branch, added only absent fields, remembered the new values as applied and never patched the existing move_on_archive / archive_folder. The first-sight branch now also patches fields changed locally, and the migration marks the record as merged. Regression test: v4 migration → save → save → restart.
  2. Medium: competing settings records were ignored silently. Any other _settings/*.md with app: dev.tasknotes and scope: collection (for example dev.tasknotes (conflicted copy Device-B).md) is now named in a non-blocking notice (mdbaseSafety.competingSettings, translated in all locales). This follows the beta.6 "Multiple TaskNotes types" notice pattern. The canonical record stays in use; other files are not changed, merged or deleted. _settings/*.md changes trigger reconciliation.
  3. Medium: link format not honoured consistently. generateLink delegated Markdown links to fileManager.generateMarkdownLink, which follows Obsidian's global wikilink preference. When that returns a wikilink, it now builds a Markdown link. NLP projects (task modal, inline convert, API/MCP/CLI via buildTaskCreationDataFromParsed), the parent-note project default and the Bases current-file default now use the TaskNotes setting. The {{parentNote}} template variable keeps Obsidian's format, because it is also used in bodies and filenames. The edit modal keeps existing project links as written.
    • Not fully fixed (found by desk testing, deferred): dependency (Blocked by) links and inline conversion still ignore the setting. Blocked-by writes [[…]], and inline conversion writes the project as plain text. Both also reproduce on beta.6, so they are not regressions from this PR, and they are tracked for a follow-up. The HTTP API keeps caller-supplied link syntax, which is intended.
  4. Low–P2: rollback and recovery left created files. See Decisions → Rollback. This applies to both the v0.2 journal and MetadataTransaction.

Round 2 validation (head 311534e):

  • npm test: 784 suites passed and 70 skipped; 5208 passed, 1012 skipped and 4 todo. TMPDIR is needed for the symlink and native-publication tests.
  • npm run lint, lint:ts, lint:review-types, lint:css and typecheck: pass.
  • i18n:verify and i18n:check-usage: pass.
  • docs:check: pass.
  • build: pass. check:bundle-size: main.js is 4,801,603 bytes, below 5,000,000.
  • Isolated Obsidian (separate profile, vault and port) on the slice-D large4-final-spec v4.14.0 fixture:
    • The migration wrote move_on_archive: true.
    • The first save wrote false.
    • After a restart, the live setting is false.
  • New and updated tests:
    • v4 migration then save (mdbaseReleaseUpgrades)
    • competing records (mdbaseReleaseUpgrades)
    • generateLink with an Obsidian wikilink preference, and formatResolvedProjectReference (linkUtils)
    • NLP project formatting (taskModalProjects)
    • the Bases current-file default
    • rollback removes created files and staging files, for both v0.2 and transaction paths (MdbaseSpecService, mdbaseSafeWrites)
    • a user-edited created file is kept

Manual upgrade testing (desk, Obsidian on gcp)

Four tester slices ran against 5.0.0-beta.6, candidate r1 (58ba266) and R2 (311534e). Every source was restored from the same pre-upgrade snapshot for each target.

Slice Sources R2 result
A: v4, mdbase off, then opt-in 4.10.0, 4.12.5, 4.13.10, 4.14.0, empty vault Pass; records and types are byte-identical to r1
B: v4 power users, mdbase on 4.13.10, 4.14.0 × untouched, hand-edited, malformed, custom types folder, second type Untouched and custom folder migrate. Edited, malformed and second type are safely refused, the same as beta.6
C: beta chains 4.12.5→beta.0→beta.3, 4.13.10→beta.3/4/5, hand-edited binding, beta duplicate types Pass; duplicates cleaned up as in beta.6
D: edge cases 2,001-task vaults from 4.14.0 and beta.5; SIGKILL; read-only _types/ and _settings/; sync with last-writer-wins and with conflict copies; downgrade and re-upgrade; integrations; phone layout Pass. Startup including migration is about 4.2 s

Fixes 1, 2 and 4 were verified through the real settings UI. Fix 3 is partial, as noted above. No task, frontmatter, template or Base rewrites were found, restarts were idempotent, and every downgrade to the source version still works.

Issues found but out of scope here; they are general v5 and also in beta.6:

  • An edited, malformed or multi-type v4 type is refused and never migrated, and the notice repeats on every start.
  • Clicking the centre of the status dot opens "Move task" (a drag handle overlaps it). 4.14.0 has this too.

Release dependencies

  • @tasknotes/model 0.3.0-rc.20 is published to npm (tag next, with provenance) and replaces the vendored tarball. Its integrity hash matches the tarball. Tests, lint, typecheck, build and bundle size were rerun on 10b5222.
  • mdbase-contracts#18 is merged, and the tasknotes.task pack 0.3.0-rc.19 is live on mdbase.dev (mdbase.dev#87) with the pinned digest.
  • tasknotes-app#311 is merged and deployed to production at c51c086, so the app reads rc.6 collections and the settings record before this plugin upgrades anyone.

tasknotes.task 0.3.0-rc.6 removes app behaviour from the type binding.
TaskNotes now keeps it in _settings/dev.tasknotes.md (app dev.tasknotes,
scope collection, settings_version 1), a tasknotes-settings record that
implements mdbase.app-settings 1.0.0 and tasknotes.settings 1.0.0:
NLP triggers, templating, archive moves, time-tracking auto-stop,
recurrence checkbox reset, link write format, and per-status next
status, cycle exclusion and auto-archive.

- Consume @tasknotes/model 0.3.0-rc.20 from vendor/ (unpublished) and
  the mdbase-contracts tasknotes.task 0.3.0-rc.19 pack.
- Upgrade rc.3 and rc.5 types to rc.6 and write the record in the same
  journalled, rollback-capable metadata transaction; v4 migrations write
  it from data.json. Creation is create-if-absent; an existing record
  only gains absent fields. Seed the settings contracts, schemas and
  type when absent.
- Read precedence: record, then the earlier binding, then local
  settings. Sync later changes both ways with a conflict prompt; respect
  read-only (newer settings_version) records.
- Recovery allowlists accept the versioned contract and settings files.
  Fix the v0.2 contracts_folder derivation for versioned contract paths,
  and treat rc.5 types as current when setting aside beta duplicates.
The v0.2 migration wrote the settings record but left the session's
first-sight flag unset, so the first save took the add-absent-fields
branch, remembered the new values as applied and never patched the
fields the record already held. The first-sight branch now also writes
local changes, and the migration marks the record as merged.
A sync conflict copy, or any other _settings/ file with app: dev.tasknotes
and scope: collection, was ignored without a word. TaskNotes now names
those files in a notice and keeps using _settings/dev.tasknotes.md; the
other files are not changed.
generateLink delegated Markdown links to Obsidian's generateMarkdownLink,
which follows the global wikilink preference, so the project picker,
dependencies and other relationship writes could produce wikilinks with
the TaskNotes setting on Markdown. It now builds a Markdown link when
Obsidian returns a wikilink. Projects from natural-language input (task
modal, inline convert, API/MCP/CLI), the parent-note project default and
the Bases current-file default now use the same setting.
Rollback and startup recovery restored only files that existed before,
leaving new contracts, schemas, the settings type, the settings record
and abandoned *.tasknotes-stage-* files behind. Both the v0.2 migration
journal and the metadata transaction now remove a file they created
while it still holds exactly the bytes written, keep any file edited
since, and clean staging files beside journaled paths that no pending
swap journal owns. This reverses the earlier choice to never delete
created files during rollback.

Also documents competing settings records and the new rollback
behaviour, and adds release notes for this round.
Inline conversion only needs the TaskNotes link format for the parent
note project; the template variable is also used in note bodies and
filenames, so it keeps Obsidian's generated link.
…d tarball

The npm package (published with provenance from tasknotes-model v0.3.0-rc.20)
has the same integrity hash as the vendored tarball it replaces.
@callumalpass
callumalpass marked this pull request as ready for review October 27, 2026 20:08
@callumalpass
callumalpass merged commit 4061997 into agent/tasknotes-v5-documentation Oct 27, 2026
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