Repository navigation
Pad v0.19.0-rc.3
Pre-releaseSince rc.2
Account settings keep to the signed-in account (TASK-4129). If the signed-in account changed without a page reload while a settings action was still running (a password change, 2FA setup, a new API token, a data export, linking or unlinking a sign-in provider, or an account deletion), its result could appear under the new account, including a 2FA setup secret, recovery codes or a new token's secret. The page now drops a result that belongs to the previous account, and an account change clears its per-account form state.
Highlights
Behaviour change: a workspace-pinned API token now works only in its workspace (BUG-3982). A token created for one workspace used to be accepted on other workspaces its owner could reach, and on account-wide routes. It is now refused outside its pinned workspace and on global routes, such as creating a workspace, the event stream, export and import status, and workspace tabs. It can list and revoke only tokens pinned to the same workspace. If a script or integration used a pinned token across workspaces or for account-wide calls, give it an unpinned token.
Security updates. Go toolchain 1.26.10 (GO-2026-6809) and golang.org/x/net v0.62.0 (GO-2026-6866, GO-2026-6860). The user read path no longer decrypts the TOTP secret; only the second-factor check does (BUG-4019).
Sign-in and session fixes. A browser-approved CLI token gets its own lifetime (TASK-3975). Changing your password or 2FA keeps the session's sliding window (BUG-3978). pad auth 2fa disable exits non-zero when it can't save the rotated session (BUG-3882).
CLI and MCP correctness. pad item list --format json pages to completion when --limit isn't set (BUG-4024). MCP numeric parameters are checked strictly on both transports: a non-number or non-integer is refused instead of being dropped or changed (BUG-4056, BUG-4061, BUG-4065, BUG-4070). pad session list reads "unknown" instead of "dead" across a PID namespace (BUG-4090). A relative PAD_DATA_DIR or PAD_DB_PATH now warns (BUG-4066).
Retrying a write safely (BUG-3999, reported by @thomasthinks in #2250). The /pad skill no longer says a retry is safe on its own. Each run sends a new idempotency key, so a retry is deduplicated only when both runs pass the same --op-key. When a write that sent a key fails or times out, the CLI now prints that key on stderr, so you can re-run with --op-key <k>. --format json output on stdout is unchanged.
New items land on top (BUG-4005). A new item with no parent now goes to the top of its collection's manual order. A child still goes to the end of its parent's list. Children created in the same second keep their creation order, and items seeded by a template or an app install keep their manifest order.
2FA after an app's social sign-in (BUG-4136, server half TASK-4137). The server can bind a 2FA challenge to the app's PKCE verifier instead of its IP address, so an app that hands social sign-in to the browser can finish the 2FA step from the app. A request without a PKCE challenge is IP-bound exactly as before, so existing clients are unchanged. The Pad mobile apps use it from their next release.
Smaller fixes. Inviting with an unknown role answers 400, not 500 (BUG-3861). A workspace whose owner no longer resolves never produces a // link (BUG-4050). Toasts with an action or link stay up about 10 seconds (TASK-3973). The new-item dialogs label their dismiss button Close, not Cancel (BUG-4004).
Changelog
Features
- 096bde6: feat(apps): admin Apps toggle, paused webhook status, app-name attribution, developer docs (TASK-3980) (#2258) (@xarmian)
- e09b11c: feat(attachments): in-app previews for text, Markdown and CSV/TSV (TASK-4074) (#2310) (@xarmian)
- 875bbaa: feat(attachments): sandboxed in-pane PDF viewer with page navigation (TASK-4075) (#2333) (@xarmian)
- 30fa047: feat(auth): bind an app's 2FA challenge to its PKCE verifier on oauth-login (BUG-4136 2a, TASK-4137) (#2350) (@xarmian)
- fdbba1a: feat(cli,mcp): pad project catch-up and pad_project action=catch-up, read-only (TASK-4035) (#2290) (@xarmian)
- 16aaaf5: feat(collections): awaits_human collection setting (TASK-3919) (#2244) (@xarmian)
- e432d0f: feat(decision): debounce decision re-asks with decision_jobs.not_before (TASK-3916) (#2230) (@xarmian)
- e598bc4: feat(decision): effort, effort_medium and high_stakes on the attention set, staged; drain-paced backfill (TASK-3921) (#2236) (@xarmian)
- 6f8d43e: feat(metrics): signup and first-write funnel metrics; TouchUserWrite reports the first write (TASK-3926) (#2227) (@xarmian)
- dd412a0: feat(needs): GET /me/needs, the cross-workspace needs-you inbox and its router (TASK-3920) (#2253) (@xarmian)
- 5aa9e49: feat(needs): importance and simplicity scorers on /me/needs (TASK-3959, part 1b) (#2273) (@xarmian)
- eb7f8a2: feat(needs): per-user dismiss/defer storage and the needs/snooze routes (TASK-3918) (#2231) (@xarmian)
- ca184cf: feat(needs): sort=importance|simplicity with a score-keyed cursor (TASK-3959, part 2) (#2274) (@xarmian)
- 94191b2: feat(needs): store reads for importance and simplicity scoring (TASK-3959, part 1a) (#2271) (@xarmian)
- 6cfe322: feat(push): GET /me/notifications/{id} enrichment and the real-enqueue mention leg (TASK-3939 part B2b) (#2284) (@xarmian)
- 20a1500: feat(push): gateway client and notification claim lifecycle (TASK-3939, part A of 3) (#2278) (@xarmian)
- eaa1b0d: feat(push): start the push dispatcher at server startup when a gateway is set (TASK-3939 B2c) (#2286) (@xarmian)
- 2a98237: feat(push): summary push, 30/h cap, retry matrix and expire/prune sweeps (TASK-3939 part B2a) (#2283) (@xarmian)
- dbbbefd: feat(push): the push dispatcher core and its privacy gates, not yet wired (TASK-3939, part B1) (#2280) (@xarmian)
- b1dce0e: feat(server): log every unguarded content write the PLAN-3555 counter counts (TASK-4062) (#2301) (@xarmian)
- 9dc679e: feat(server): per-user catch-up digest and read cursor (TASK-3615, PR A) (#2219) (@xarmian)
- d76f06d: feat(server): push device registration and notification settings API (TASK-3938) (#2269) (@xarmian)
- 8204f03: feat(server): push dispatch timings are configurable, and a bad value refuses startup (TASK-3939) (#2288) (@xarmian)
- 1e9b877: feat(store): @mention index on comments (TASK-3917) (#2243) (@xarmian)
- 423d312: feat(store): @mentions enqueue a push notification (TASK-3946) (#2272) (@xarmian)
- 104f809: feat(store): enqueue push notifications at four store sites (TASK-3937 part B) (#2268) (@xarmian)
- 9af942a: feat(store): push notification schema and settings store (TASK-3937 part A) (#2265) (@xarmian)
- ffc1138: feat(verify): verification evidence tied to a revision, slice 1 (TASK-3617) (#2228) (@xarmian)
- dd93b7c: feat(web): Notifications section in account settings (TASK-3940) (#2276) (@xarmian)
- ed559de: feat(web): catch-up card, Mark caught up, and See everything into Activity (TASK-3615, PR B) (#2226) (@xarmian)
- ce2ca4f: feat(web): quick add gains Cancel and 'Add and open' buttons (TASK-3972) (#2247) (@xarmian)
- 2bacd54: feat(web): the Needs you page at /console/needs (TASK-3923) (#2287) (@xarmian)
- 5eeca6f: feat(web): the side pane can widen to 90% of the usable width (TASK-3971) (#2246) (@xarmian)
- bae3601: feat(web): the workspace import toasts the unique values it dropped (TASK-3842) (#2259) (@xarmian)
Bug fixes
- 393d986: fix(auth): a browser-approved CLI token gets its own lifetime (TASK-3975) (#2252) (@xarmian)
- 2655ad5: fix(auth): a credential-change rotation keeps the session's own sliding window (BUG-3978) (#2254) (@xarmian)
- 94c6d3c: fix(brand): the favicon's gaps tighten to match the mark (TASK-3974) (#2248) (@xarmian)
- 7c9f29b: fix(ci): Go toolchain 1.26.10 (GO-2026-6809) and markdown-it 14.3.3 (TASK-4007) (#2267) (@xarmian)
- 0e05111: fix(cli): 2fa disable exits non-zero when the rotated session cannot be saved (BUG-3882) (#2264) (@xarmian)
- aad3c35: fix(cli): pad item list --format json pages to completion when --limit is not set (BUG-4024) (#2279) (@xarmian)
- 786406e: fix(cli): print the op_key of a failed write; stop claiming a re-run is safe (BUG-3999) (#2346) (@xarmian)
- a8af168: fix(cli): session liveness reads unknown, not dead, across a PID namespace (BUG-4090) (#2328) (@xarmian)
- cba880c: fix(config): warn on a relative PAD_DATA_DIR / PAD_DB_PATH; anchor install-refresh's marker and config reads (BUG-4066 phase 1) (#2308) (@xarmian)
- 25153bc: fix(decision): a failure the store refuses is still counted, and an empty message is never a supersede (BUG-4022) (#2317) (@xarmian)
- b1785f8: fix(decision): retire text-derived blocked, waiting_on_external and conventions_comments; keep a pending answer on the dashboard (TASK-3915, TASK-3924) (#2229) (@xarmian)
- 6dfc431: fix(deps): bump golang.org/x/net to v0.62.0 for GO-2026-6866 and GO-2026-6860 (TASK-4115) (#2331) (@xarmian)
- adbbb89: fix(links): wiki-link code-span check walks the spans once per call (BUG-3968) (#2261) (@xarmian)
- b13e842: fix(mcp): a refused tool call is audited as denied, not ok (BUG-3879) (#2222) (@xarmian)
- 0ed34ea: fix(mcp): integral numbers keep decimal notation on both transports (BUG-4056) (#2300) (@xarmian)
- 82f81ee: fix(mcp): refuse a non-number for a numeric parameter instead of reading it as absent (BUG-4065) (#2306) (@xarmian)
- ba981cb: fix(mcp): refuse a numeric parameter that is not a whole int64 instead of changing it (BUG-4061) (#2303) (@xarmian)
- 729ae5f: fix(mcp): stdio applies remote's number rule before a value becomes CLI text (BUG-4070) (#2313) (@xarmian)
- 6402eb3: fix(mcp): stdio's item patch gives line_hint remote's rule through one shared helper (BUG-4082) (#2315) (@xarmian)
- 07215d4: fix(plugin): session start runs nothing; presence and the arm check wait for the first use of Pad (TASK-3970) (#2249) (@xarmian)
- b039f11: fix(push): bound re-sends when the sent record fails after a gateway 'sent' (TASK-4032) (#2285) (@xarmian)
- 0a32eff: fix(scripts): install-refresh removes pad.pid only when it is provably not a live sibling's (BUG-4068) (#2311) (@xarmian)
- 3e7f9e1: fix(scripts): install-refresh restarts the server logging to cfg.LogFile() (BUG-4064) (#2304) (@xarmian)
- 5035168: fix(security)!: a workspace-pinned API token is confined to its workspace (BUG-3982) (#2266) (@xarmian)
- a16d76f: fix(server): a typed settings member over non-object stored settings answers 400 (BUG-3976) (#2256) (@xarmian)
- d36f11c: fix(server): a workspace whose owner no longer resolves never yields a '//' path (BUG-4050) (#2334) (@xarmian)
- 164768d: fix(server): an invite with an unknown role answers 400, not 500 (BUG-3861) (#2255) (@xarmian)
- ba3a0ab: fix(store): a same-user push re-register keeps the device's created_at and row (TASK-4027) (#2281) (@xarmian)
- 527fa47: fix(store): a user read no longer decrypts the TOTP secret; only the second-factor check does (BUG-4019) (#2275) (@xarmian)
- 9d563aa: fix(store): collection settings must be a JSON object at every write door (BUG-3985) (#2260) (@xarmian)
- d6dcc2d: fix(store): decision last_error is sanitised and cut at a rune boundary (BUG-4018) (#2277) (@xarmian)
- 2b3b7f9: fix(store): new board items land at the top of their lane; children keep appending in creation order (BUG-4005) (#2348) (@xarmian)
- d97c528: fix(store): push token ownership follows a registration sequence, not updated_at (TASK-4031) (#2282) (@xarmian)
- 27bc097: fix(store): refuse writes to read cursors and app installs after migrate-to-pg, and classify every table (BUG-3958) (#2257) (@xarmian)
- 019dc3f: fix(web): identity-fence the settings page's auth settles and reset per-identity state (TASK-4129) (#2356) (@xarmian)
- 0821f97: fix(web): label the new-item dialogs' dismiss button Close, not Cancel (BUG-4004) (#2345) (@xarmian)
- 1bbca07: fix(web): toasts with an action or link stay ~10s (TASK-3973) (#2245) (@xarmian)