You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Highlights
Security
Two-factor can no longer be bypassed (BUG-3322). Signing in with Google or GitHub asks for the 2FA code when TOTP is on, and a password reset no longer signs you in past 2FA. Native apps get a "sign in on the web" message until they have their own 2FA step.
Client IPs can't be spoofed behind append-only proxies (BUG-3323). Pad resolves the client from the right-most untrusted X-Forwarded-For hop, skipping trusted proxies, and parses ip:port entries (Azure App Gateway). This affects rate limits, 2FA binding and audit IPs. Every proxy listed in PAD_TRUSTED_PROXIES must append to X-Forwarded-For.
Activity no longer exposes IP address and user agent (BUG-3314). Activity reads that a workspace member can reach used to return each entry's ip_address and user_agent. They no longer do.
Sign-in and session hygiene:
A web sign-in now destroys the session its cookie replaces, so signing in as someone else no longer leaves the previous user's session alive (BUG-3011).
An admin creating an account no longer gets a live session for the new user (BUG-3232).
Invitations match the email exactly, with no Unicode folding (BUG-3282).
Rate limits:
One general API bucket per caller across both middleware arms (BUG-3310).
The per-key limiter maps are bounded, and IPv6 is keyed on the /64 (BUG-3308).
Synchronous decision-provider calls have their own limit (TASK-3141).
OAuth:
Expired token, code and PKCE rows are swept (BUG-3301).
Workspace reorder honours the consent allow-list (BUG-3255).
A workspace import joins the importing connection, as create does (BUG-2794).
Guests arriving by URL get the guest sidebar, not member chrome (BUG-3254). Several pages now fence their async loads on the signed-in identity, so switching accounts can't leak the previous user's data into the page (BUG-3084, BUG-3006, BUG-3130).
Plan limits are decided inside the insert's own transaction, and a create charges the authenticated owner's plan, not an owner named in the request body (BUG-2808, BUG-3101).
Data safety
Unsaved editor sessions are recovered (TASK-2198). When a browser tab closed or crashed before saving, its typing sat in the collaborative op-log where REST, the CLI and agents could not see it, and the next direct write could destroy it. Pad now replays such op-logs in the background and writes the result into the item.
Each recovery writes a version (system / recovery, "recovered from an unsaved editor session"), bumps seq, and never deletes the op-log.
It runs in a separate, memory-capped worker process. It never runs on a request path.
It will not write an empty body over a stored one (BUG-3316).
Comment deletes keep threads intact (BUG-3252). Deleting a comment that has replies leaves a "deleted" placeholder; its replies stay under it. Before, it failed with a server error.
Links keep their text:
A wiki link whose target's title contains >, <, *, & and similar characters no longer turns into a mangled display override on the first save (BUG-3315 U1).
A renamed item's links in collab-edited documents follow the new title instead of pinning the old one (BUG-3315 U2).
The rename cascade uses the wiki grammar, so legacy titles containing |, \ or ] keep their links (BUG-2806).
Concurrent edits:
A version-token write is refused rather than replacing a tab's unsaved edits (BUG-3133).
A tab's older write can no longer land over its newer one (BUG-3080).
An editor schema upgrade sets unflushed edits aside instead of deleting them (BUG-3244).
New
MCP for self-hosted installs (PLAN-2310). The remote MCP endpoint (/mcp) can now be turned on by an instance admin. It has a Host allowlist, per-route limits, a console section and a connect modal. It is off by default on self-hosted.
Workspace tabs (PLAN-3002). The top bar is a bar of real tabs over your open workspaces, kept server-side so it follows you across devices.
"+" finds, creates or restores a workspace, and lists your pending invitations.
A workspace you lose access to closes its tab live.
History (PLAN-2348 U1/U2). Version rows record their writer and line counts, and there is a per-version diff endpoint. Activity runs are split by source, and field changes are no longer dropped.
Content-aware attention (PLAN-3114). With a decision provider configured, the dashboard flags items that need a human decision or read as blocked, and playbooks can be matched from free text. It is off unless an admin configures a provider.
Agents:
Comments can be edited and deleted from the CLI and every MCP transport (TASK-2695).
Items support an execution lease: an atomic claim with expiry.
The pad item update success line reports what happened to the body (TASK-3132).
Unattached uploads can be attached to an item, so an item's share renders them (TASK-2247).
The item pane edits multi-select values (IDEA-3223).
The schema editor can mark a terminal option as "abandoned" (BUG-2347).
Fixes and UI
A workspace can be created with the name of one deleted in the last 30 days. It gets name-2 instead of a server error (BUG-3307).
A move refused because it needs a value only you can supply now says so, and offers a way forward, on web, CLI and MCP (BUG-3200).
Protected share pages render their images on every install (BUG-3305).
A portal menu is no longer closed by a pending scroll (BUG-3278).
The recovery sweep no longer re-queues an item it has given up on every minute (BUG-3325).
221 fix commits in all, listed below by GoReleaser. By scope, the largest groups are web (77) and store/server (37).
Touch targets at phone width are 44px: the collection strip, one ⋯ per card for star, copy and reorder, the status chip, group archive, board lane controls and card tags (TASK-2244, TASK-3311).
Workspace tabs feel like browser tabs (TASK-3312): open and close motion, widths freeze while you close tabs in a row, middle-click closes, the active tab flows into the page, and favicon-style icons.
Several open pad tabs no longer stall new pages over plain HTTP (BUG-3318). One tab per browser now holds the access stream for all of them.
The image and file viewer shows when, and by whom, an attachment was uploaded (TASK-3319).
One History tab replaces Activity and Versions on an item (PLAN-2348 U3, #1692).
Operator notes (upgrade)
Recovery is ON by default. Before upgrading, take a backup: pad db backup (docs/backup.md). Every recovered write is versioned and reversible per item, but a backup lets you roll the whole first pass back.
First start after upgrading: one recovery burst. On its first sweeps (every minute, for items idle for 2 minutes), recovery processes every item whose op-log holds edits newer than its stored body.
On an instance with a long collab history this can be a few hundred items in the first minutes. Most are no-ops that only move a watermark; the rest write one version each.
Measured on one real instance (read-only dry run): 175 candidates. 156 were watermark-only, 17 wrote a recovered body, 1 was refused (an empty replay), and 1 hit the time limit and is retried.
To stage it (review first, recover later), start with PAD_MATERIALIZE=off, then remove the setting when ready.
A new child process:pad __materialize-worker.
It is started on the first recovery job, not at server start.
It holds about 200 MB resident while it runs, and stops after 5 minutes with no job.
It dies with the server.
On Linux, macOS and Windows it runs under a memory cap. On any OS where no cap can be established, recovery refuses to run rather than run uncapped.
New settings (docs/deployment.md, "Op-log materializer worker"):
PAD_MATERIALIZE=off disables recovery entirely: no worker, no triggers.
PAD_MATERIALIZE_TIMEOUT: per-job time limit, default 2s, clamped 250ms–60s.
PAD_MATERIALIZE_MEM_LIMIT: memory one job may add, default 2GiB, clamped 256MiB–16GiB.
PAD_MATERIALIZE_IDLE_TIMEOUT: stop the idle worker after this long, default 5m, clamped 30s–24h; 0 = never.
Binary size: about +7 MB. The embedded editor bundle and JS engine measured +7,217,152 B stripped (#1678).
Workspace names after a soft delete: a deleted workspace keeps its slug for its 30-day restore window. A new workspace with the same name gets name-2, and the deleted one stays restorable under its own slug.
MCP stays off on self-hosted until an admin enables it (GET/PUT /api/v1/admin/mcp, or PAD_MCP_ENABLED). It needs a usable public origin; see docs/deployment.md, "MCP for agents (self-hosted)".
Serve Pad over HTTPS (HTTP/2) where you can. Over plain HTTP, browsers allow only 6 connections per host; see docs/deployment.md.
The MCP tool surface is now v0.60. Clients that pin the version should re-check pad://_meta/version.
2373336: feat(attachments): attach binds an unattached upload to an item, so its share renders it (TASK-2247) (#1652) (@xarmian)
c6df08f: feat(attachments): the viewer says when a file was uploaded, and by whom (TASK-3319) (#1697) (@xarmian)
249e44d: feat(billing): record plan_source; a lowering applies only from the source that set the plan (TASK-3295) (#1657) (@xarmian)
b7e0d6e: feat(cli): item update reports the body outcome on its success line; show flags a stale body (TASK-3132) (#1416) (@xarmian)
0f1069e: feat(comments): agents can edit and delete comments from the CLI and every MCP transport (TASK-2695) (#1607) (@xarmian)
1681be7: feat(decision): content-aware attention on the dashboard and item page (TASK-3118) (#1421) (@xarmian)
442dcbd: feat(decision): instance-admin settings for the decision provider (TASK-3121) (#1426) (@xarmian)
096584c: feat(decision): pluggable typed-decision provider with a typesafe.ai backend (TASK-3116) (#1411) (@xarmian)
304673f: feat(decision): typed-decision storage and async runner (TASK-3117) (#1417) (@xarmian)
4f367ad: feat(history): one History tab replaces Activity and Versions (PLAN-2348 U3) (#1692) (@xarmian)
7ba8230: feat(history): version rows carry their writer, line counts and create marker, plus a per-version diff endpoint (PLAN-2348 U2) (#1691) (@xarmian)
ae7f77d: feat(mcp): construct MCP off cloud, mount always, gate per request (TASK-2317, PLAN-2310 U2) (#1663) (@xarmian)
22bfe8c: feat(mcp): off-cloud Host allowlist on the non-API MCP paths (TASK-2319, PLAN-2310 U3) (#1665) (@xarmian)
bf0631d: feat(ops): health reports which web bundle is served, and install-refresh warns when it is not a clean build of the commit (TASK-3233) (#1583) (@xarmian)
6fdd881: feat(playbook): match free text to an active playbook via the decision provider (TASK-3120) (#1429) (@xarmian)
5da904b: feat(server): a dedicated rate limit for synchronous decision-provider calls (TASK-3141) (#1436) (@xarmian)
18eb37a: feat(server): workspace_access_changed on the user stream when an existing account gains or loses a workspace (TASK-3272) (#1636) (@xarmian)
2174993: feat(web): "+" finds a workspace outside your tabs, creates one, or restores one (TASK-3276) (#1639) (@xarmian)
0e69eb3: feat(web): a lost workspace closes its tab live, through a per-tab access stream (TASK-3275) (#1640) (@xarmian)
ba75b6f: feat(web): landings open an ephemeral workspace tab, a write keeps it, and last_route lives only on the tab row (TASK-3279, PLAN-3002 U5) (#1646) (@xarmian)
918a016: feat(web): mobile switcher lists the open set first (TASK-3283, workspace tabs U6) (#1643) (@xarmian)
71d89d1: feat(web): the TopBar is a tab bar over the workspace open set (TASK-3274) (#1637) (@xarmian)
754d85f: feat(web): the connect modal serves the token path (TASK-2321, PLAN-2310 U6) (#1668) (@xarmian)
c573b37: feat(web): the item pane edits a multi_select value, where it fell to a text box whose string the server refused (IDEA-3223) (#1558) (@xarmian)
db16b0c: feat(web): the schema editor marks a terminal option as abandoned, and saving no longer strips abandoned_options (BUG-2347) (#1468) (@xarmian)
4c57d94: feat(web): the workspace tabs are real tabs (TASK-3306) (#1664) (@xarmian)
552dee8: feat(web): workspace tabs feel like Chrome's (TASK-3312) (#1676) (@xarmian)
1e7cd9a: feat(web): workspace tabs store with last route on the tab row (TASK-3271) (#1633) (@xarmian)
cbc2cce: feat(workspaces): a per-user open set of workspace tabs, stored server-side and filtered on read to the workspaces the caller can see (TASK-3256) (#1619) (@xarmian)
2e45d75: feat: a caught-up tab stamps the flush watermark, so viewing an item no longer leaves it pending (BUG-3124 unit B) (#1420) (@xarmian)
7c51b36: feat: pending invitations in '+', accepted by id through one accept core (TASK-3277) (#1641) (@xarmian)
a231d0b: fix(BUG-2367): a move or copy that would reopen, close or un-abandon an item must name the status instead of inheriting whatever survived migration (#1516) (@xarmian)
fd95b79: fix(BUG-2367): moves and copies never carry into a computed field, and a carried unique-field collision is dropped and named instead of 500ing or storing a duplicate (#1514) (@xarmian)
175a81c: fix(BUG-2837): table header labels wrap to two lines and clip to their column, so a long label no longer paints over its neighbour (#1472) (@xarmian)
0b2a8d9: fix(BUG-2858): the date picker opens from a focused input, so Safari can close it on macOS and show it at all on iOS (#1480) (@xarmian)
d06e9f2: fix(BUG-2872): an activity change on a relation field shows the target (REF · title), never the item ID it stores (#1479) (@xarmian)
a39d83b: fix(BUG-2992): a failed links refresh schedules its own retry, because the sync cursor cannot carry it (#1488) (@xarmian)
001d243: fix(BUG-3013): a relation id that does not resolve is "Unavailable item", never a claim that its target is gone (#1487) (@xarmian)
c49ee76: fix(BUG-3112): a mermaid diagram rendered in dark mode prints light (#1489) (@xarmian)
512572b: fix(BUG-3161): a role with no icon shows the robot character, not a literal HTML entity (#1467) (@xarmian)
e346b85: fix(BUG-3165): the collection list keeps its position when the split pane opens and closes, and on Back from Expand to full page (#1475) (@xarmian)
536e90c: fix(BUG-3181): the activity page brings the workspace index up through the collection page's own sequence, now shared; nothing else calls localIndex.bootstrap directly (#1486) (@xarmian)
80ec51c: fix(BUG-3185): an item export's file name whose stem is a Windows device gets a "_" prefix, on the server and in the CLI (#1495) (@xarmian)
03ff8fc: fix(BUG-3186): the CLI turns an attachment name into a path Windows can store, on Windows only; stored names stay as given (#1498) (@xarmian)
019f8c9: fix(BUG-3192): a 429 on a GET is retried up to 3 times, each no sooner than Retry-After and jittered (#1503) (@xarmian)
6862c6f: fix(BUG-3192): a parent's progress is the server's, and a tab loaded hidden defers its non-essential panels until shown (#1505) (@xarmian)
25a6d17: fix(BUG-3192): one item-page load is one loadData, and a reconcile ask is answered by one read issued after it (#1502) (@xarmian)
8c7141a: fix(activity): feeds page by a (created_at, id) keyset and break timestamp ties by id (BUG-2781) (#1459) (@xarmian)
a814a96: fix(api): a workspace that does not resolve answers 404 not_found marked details.scope=workspace, so a client can tell it from a missing item (BUG-3069) (#1534) (@xarmian)
a280980: fix(attachments): a stored name Windows would open as a device gets a "_" prefix (BUG-2822) (#1493) (@xarmian)
a92af3e: fix(attachments): drop Bidi_Control characters from attachment names and rewrite legacy rows (BUG-3153) (#1452) (@xarmian)
d048631: fix(attachments): the blocklist judges the same filename the download header serves (BUG-2818) (#1445) (@xarmian)
c1fa878: fix(attachments,mcp-audit): record where a stored filename or tool name came from, instead of sentinel strings a caller can choose (BUG-2819) (#1537) (@xarmian)
1c6348e: fix(auth): OAuth login and password reset no longer mint a session past TOTP (BUG-3322) (#1699) (@xarmian)
9b911de: fix(auth): a web sign-in destroys the session row its cookie replaces, so a sign-in as someone else no longer leaves the previous user's session alive (BUG-3011) (#1579) (@xarmian)
a537ba8: fix(auth): an admin-created registration mints no session, so creating an account no longer hands the admin a live credential for the new user (BUG-3232) (#1581) (@xarmian)
d4ec587: fix(cli): a CLI call during an install-refresh bounce waits for the refresh instead of auto-starting a server over it (BUG-3215) (#1544) (@xarmian)
1144fe8: fix(cli): item edit refuses a stale body, guards its save with expected_seq, and keeps a recovery file on failure (BUG-3035) (#1418) (@xarmian)
df5531b: fix(cli): pad server stop keeps a live legacy PID file instead of deleting it, and clears a dead one (BUG-2970) (#1485) (@xarmian)
98213b0: fix(cli): refuse a blank --stdin body on item create/update; add --clear-content (BUG-3100) (#1415) (@xarmian)
e422d6b: fix(cli,web): the CLI activity surfaces apply the change-pill display rule through a Go twin sharing one fixture with the web, and the admin audit log expands a row to its full record (BUG-2789) (#1570) (@xarmian)
9aa6c7b: fix(cloud): auto-create removes a workspace it cannot seed or administer, and leaves no restorable husk (BUG-3087) (#1383) (@xarmian)
c50f058: fix(collab): WebSocket dials get their own per-user bucket and a per-user socket cap, and the editor jitters its reconnects (BUG-1308) (#1499) (@xarmian)
2d51308: fix(collab): a schema-version rebuild sets unflushed edits aside instead of deleting them (BUG-3244 A) (#1598) (@xarmian)
381420a: fix(collab): read-only peers are evicted and force-refreshed after a no-applier direct write, so a viewer cannot keep showing or push back the pre-write document (BUG-2103) (#1575) (@xarmian)
6be2f81: fix(collab): the relay does not store a byte-identical re-sent frame (BUG-3135) (#1425) (@xarmian)
0f6e246: fix(collab): the sync completes on the relay's post-replay cursor frame and the relay elects one seeder, so the lazy seed can no longer store a body twice (BUG-3240) (#1604) (@xarmian)
a2e7768: fix(comments): a deleted comment with replies leaves a tombstone (BUG-3252) (#1675) (@xarmian)
9b7e083: fix(comments): deleting a comment that has replies answers 409 comment_has_replies instead of 500 (BUG-3252) (#1608) (@xarmian)
c1e57ee: fix(comments): the "commented" activity and the comment commit in one transaction (BUG-2716) (#1388) (@xarmian)
71fa167: fix(documents): the rename cascade decides a link by the wiki grammar, so a legacy title containing | \ or ] keeps its links and no longer rewrites a link to another target (BUG-2806) (#1572) (@xarmian)
f7d4b61: fix(editor): a leading YAML frontmatter block survives the markdown round trip instead of flattening into a heading (BUG-2692) (#1508) (@xarmian)
cdbc69d: fix(editor): an empty mermaid block no longer shows "Rendering..." forever (BUG-3113) (#1414) (@xarmian)
a1b4c4c: fix(editor): an upload finishing while the master is frozen is inserted on thaw, and interrupted image edits and source refreshes say so (BUG-2177) (#1511) (@xarmian)
4e81480: fix(editor): block-drag global listeners defer to a frontmost viewer (BUG-2453) (#1373) (@xarmian)
216e608: fix(editor): code block Copy copies the model text, not the contentDOM (BUG-3111) (#1412) (@xarmian)
6c5ead1: fix(editor): follow the app's light/dark mode in mermaid diagrams (BUG-3106) (#1404) (@xarmian)
1169a05: fix(editor): opening an item without typing no longer rewrites a body the editor does not reproduce byte for byte (BUG-3197) (#1515) (@xarmian)
568b113: fix(editor): pin mermaid's layout engine to dagre across the 12 bump (TASK-3090) (@xarmian)
7574c4b: fix(events): Publish reports its outcome, and a failed activity publish is counted and logged (BUG-2732) (#1457) (@xarmian)
0b3ca0d: fix(events): a closed MemoryBus refuses a late subscribe instead of handing out a channel nothing will close (BUG-2737) (#1483) (@xarmian)
372f91f: fix(events): a resume from exactly the discarded peak is refused after a counter-backwards reset, instead of being handed the new sequence as though it followed (BUG-3206) (#1530) (@xarmian)
bd44f5d: fix(events, watchevents): a SUBSCRIBE that Redis rejects with an error reply refuses its callers instead of admitting them (BUG-2799) (#1449) (@xarmian)
25d76af: fix(fields): field writes keep every number's literal, so an integer above 2^53 is no longer rounded by a write that never named it (BUG-3202) (#1545) (@xarmian)
c49a8ef: fix(history): split activity runs by source, stamp version source, stop dropping field changes (PLAN-2348 U1) (#1685) (@xarmian)
f1512de: fix(import): a failed bundle import always reports its workspace, and the body is read under a per-Read deadline (BUG-3184) (#1497) (@xarmian)
c348083: fix(import): a kept partial bundle import is attached to its importer instead of left ownerless (BUG-2709) (#1386) (@xarmian)
8b5cad8: fix(import): a panic after a bundle import creates its workspace takes the keep door, then re-panics (BUG-3191) (#1501) (@xarmian)
8ca7e55: fix(import): a rejected bundle import leaves no restorable husk, and the shared removal reclaims blobs before it purges (BUG-3094) (#1385) (@xarmian)
9b8f72d: fix(insights): a default report leaves out system collections (Conventions, Playbooks), so they stop counting in Insights (BUG-2410, ToolSurfaceVersion 0.49) (#1471) (@xarmian)
c95e8ba: fix(invitations): accepting as an existing member is idempotent (BUG-3281) (#1645) (@xarmian)
6cc2c7e: fix(items): create refuses reserved metadata keys; convention metadata goes through a typed create member; a full fields update may only carry them (BUG-3163, ToolSurfaceVersion 0.48) (#1470) (@xarmian)
bee399b: fix(items): github_pr is written only through a typed, validated update member; the field-setter door refuses it (BUG-2696, ToolSurfaceVersion 0.47) (#1469) (@xarmian)
919c297: fix(items): note and decide append server-side under the write lock, so they no longer revert a concurrent field write (BUG-3056) (#1458) (@xarmian)
9d00bf3: fix(limits): user-scoped plan limits are decided under the owner lock in the insert's transaction (BUG-2808 PR A) (@xarmian)
59d0eec: fix(limits): workspace-scoped plan limits are decided in the insert's transaction (BUG-2808 PR B) (@xarmian)
15c872b: fix(materialize): recovery never writes a blank body over a stored one (BUG-3316) (#1693) (@xarmian)
5d420ef: fix(materialize): the recovery sweep stops re-queuing an exhausted item every tick (BUG-3325) (#1704) (@xarmian)
969955c: fix(mcp): a 413 is too_large on both transports, not server_error (BUG-2829) (#1455) (@xarmian)
db02156: fix(mcp): a fields number sent through MCP keeps its literal, so an integer above 2^53 is no longer rounded by mcp-go's argument decode (BUG-3217) (#1565) (@xarmian)
c95168f: fix(mcp): bulk-update refuses an undeclared status/priority per item on stdio and remote too (BUG-3156, ToolSurfaceVersion 0.46) (#1463) (@xarmian)
e5e5d4f: fix(mcp): remote bulk-update patches only status/priority, so it no longer reverts a concurrent field write (BUG-3156) (#1462) (@xarmian)
88a5af1: fix(mcp): stdio passes "-"-leading free text as text, and reports argv refusals and 429s with the right code (BUG-3142, BUG-3147) (#1444) (@xarmian)
50636b3: fix(oauth): the creator-connection auto-add decides on the grant in force when it writes (BUG-2792) (@xarmian)
6f8cb6c: fix(pane): pane Back restores as near as the body reaches while late text is still arriving, instead of leaving the reader at the top (BUG-3251) (#1609) (@xarmian)
e4224da: fix(progress): an abandoned child leaves both the done count and the total, and 0/0 renders as no progress (BUG-3195) (#1510) (@xarmian)
a4990d7: fix(project): a collection's own abandoned_options decide what the changelog, standup and reports count as shipped (BUG-2347) (#1464) (@xarmian)
a3ebf69: fix(project): a guest's item grants count as completed work only under their own collection's terminal values, and each item is listed once (BUG-2639) (#1474) (@xarmian)
1db30da: fix(project): standup and changelog rows show the value of the item's own done field, not a blank read from a literal status (BUG-2640) (#1477) (@xarmian)
71de56f: fix(relations): a relation value stored as text reads as text, not as an unavailable target (BUG-3014, ToolSurfaceVersion 0.50) (#1484) (@xarmian)
9a8bff1: fix(relations): a wrong_collection reason is judged on the row resolved, never by a second lookup (BUG-3012) (#1490) (@xarmian)
2d8aedc: fix(release): the changelog starts at the previous stable release by version, and its exclude filters match GitHub's hash-prefixed entries (TASK-3268) (#1626) (@xarmian)
44ba666: fix(schema): a grandfathered reserved-key FieldDef never reaches an item-field path (BUG-2685) (#1377) (@xarmian)
45dddb8: fix(scripts): install-refresh replaces the server listening on its port, by pid, and leaves other pad servers running (BUG-3194) (#1504) (@xarmian)
b2bee32: fix(scripts): install-refresh restores the server's own cwd and matches the running server by identity (BUG-3196) (#1512) (@xarmian)
b459892: fix(search): the collection filter resolves exact-first per workspace in scope, so a collection named task is no longer shadowed by tasks (BUG-2659, BUG-2839) (#1576) (@xarmian)
547527d: fix(server): Content-Disposition carries a non-ASCII or unquotable filename in filename* with an ASCII fallback, so NBSP and zero-width names no longer reach clients as Go escape text (BUG-3190) (#1569) (@xarmian)
4cd168d: fix(server): OAuth form-encoded bodies take the transport text rule, so a NUL or invalid UTF-8 in connection_name, code or refresh_token answers 400 instead of 500 (BUG-2811) (#1564) (@xarmian)
2ef5a5e: fix(server): a bulk status or priority change refuses an item whose collection has no such field (BUG-3154) (#1450) (@xarmian)
98a0465: fix(server): a move refuses a field override the destination does not declare, as the copy does (BUG-2379) (#1448) (@xarmian)
bdfb8a9: fix(server): a request that repeats fields_patch or field_overrides is refused, where it silently dropped the first copy (BUG-3219) (#1548) (@xarmian)
40066cc: fix(server): a role's item_count on the roles board counts only items the caller may see (BUG-3257) (#1625) (@xarmian)
aed1ebb: fix(server): a workspace create answers with a usable workspace or with nothing (BUG-2715) (#1376) (@xarmian)
ab4cd69: fix(server): an open workspace event stream closes when its workspace is soft-deleted (BUG-3273) (#1638) (@xarmian)
0302977: fix(server): bound the per-key rate limiter maps and key IPv6 on the /64 (BUG-3308) (#1670) (@xarmian)
fd88275: fix(server): one general API bucket per caller across both middleware arms (BUG-3310) (#1672) (@xarmian)
fc1f4e6: fix(server): resolve the client from the right-most untrusted X-Forwarded-For hop (BUG-3323) (#1700) (@xarmian)
a90f75f: fix(server): sign protected share-page images on every install (BUG-3305) (#1673) (@xarmian)
19cd591: fix(server): the create door charges the authenticated owner's plan, not a body-supplied owner_id (BUG-2808 PR A review r1 N1) (@xarmian)
7376023: fix(server): the request-body gate reads tokens with jsontext, so a NUL behind a repeated key or beside 1e999 is refused, and a repeated request member is refused on every body (BUG-2812) (#1550) (@xarmian)
9059862: fix(session): a headless disarm holds for as long as its session lives: an unverifiable owner fails closed and is kept, and the file names the session, not the exiting command (BUG-2771, BUG-3227) (#1574) (@xarmian)
f36436f: fix(settings): the workspace name and context fill in after a cold load, instead of loading blank (BUG-3260) (#1621) (@xarmian)
f5b0062: fix(store): NUL Layer B covers columns added after 084 through later generated trigger files (BUG-3108) (#1506) (@xarmian)
228abe9: fix(store): a JSON blob that repeats a member is stored collapsed, so SQLite's json_extract and Go read the same value (BUG-2896) (#1547) (@xarmian)
8f297f7: fix(store): a collection gated by ID is queried by ID, so a slug freed and re-taken in between cannot answer for another collection (BUG-2631) (#1535) (@xarmian)
93693dc: fix(store): a relation ref falls back to matching by number only when its prefix names no live collection (BUG-3082) (#1369) (@xarmian)
6d6ac71: fix(store): a step2 answer carrying an update's exact payload is not content (BUG-3136) (#1422) (@xarmian)
7c2c514: fix(store): a workspace import no longer re-inserts every item into items_fts after its commit, and migration 095 rebuilds the index (BUG-2758) (#1507) (@xarmian)
f6c007a: fix(store): account deletion reads the owned workspaces inside its transaction under the owner lock, so a workspace minted or unjoined in the window no longer survives its deleted owner (BUG-3099) (#1567) (@xarmian)
9ed8cc6: fix(store): activity reads a member can reach no longer return ip_address and user_agent (BUG-3314) (#1679) (@xarmian)
a3de1bd: fix(store): both workspace mints take slug-then-owner locks in one order (BUG-2808 PR A review r2) (@xarmian)
3e09942: fix(store): content_state counts only op-log rows that can change the document (BUG-3124 unit A) (#1419) (@xarmian)
cf32474: fix(store): delete issued grants before tabs in account deletion (BUG-3288) (#1654) (@xarmian)
7f823c7: fix(store): enforce items_per_workspace at workspace import (BUG-3103) (@xarmian)
3043c8c: fix(store): field values are compared, counted, sorted and shown by JSON type, so the same query answers the same on SQLite and Postgres (BUG-3221, BUG-3218) (#1555) (@xarmian)
5e63d27: fix(store): item field filters match exactly; the comma OR is explicit and lives only in the list endpoint's query parse (BUG-3167) (#1478) (@xarmian)
5aacb33: fix(store): renaming a multi_select option rewrites the option inside every array that holds it, and a rename map applies simultaneously (BUG-3224) (#1557) (@xarmian)
f896af9: fix(store): restore and move return their own in-tx snapshot, and bulk member snapshots are read in batches (BUG-2717, BUG-2718) (#1539) (@xarmian)
2f9e8cf: fix(store): retry account deletion when a late reference fails its final delete (BUG-3289) (#1655) (@xarmian)
941cf1b: fix(store): reusing a recently deleted workspace's name no longer 500s, and a create that loses a slug race takes the next one (BUG-3307) (#1689) (@xarmian)
c31fb06: fix(store): the migrate-to-pg preflight refuses a stored value that is not valid UTF-8, and scan-nul / repair-nul report and repair it (BUG-3222) (#1559) (@xarmian)
f4595ff: fix(store): two concurrent updates by one writer start one activity row, not two (BUG-2777) (#1482) (@xarmian)
a69656e: fix(store,server): an invitation to a soft-deleted workspace reads as unknown (BUG-3104) (#1413) (@xarmian)
bd3e79e: fix(tabs): order workspace-tab lists by a per-user server revision (BUG-3285) (#1649) (@xarmian)
ef5f7c3: fix(test): call SetUserPlan with a PlanWrite; main broken since 1ed52bd (TASK-3293) (#1659) (@xarmian)
8d81f3a: fix(timeline): structured entries get a persisted id instead of a positional one that renumbers when a sibling moves (BUG-2788) (#1540) (@xarmian)
20ff89d: fix(versions): a restore over unsaved collaborative edits is refused unless the caller consents to discarding them (BUG-3031) (#1573) (@xarmian)
70f7c0c: fix(watchevents): a resume from a cursor inside an abandoned id space is refused on every arm, instead of being served the new space's ids or told it is caught up (BUG-2743, BUG-2728) (#1518) (@xarmian)
ef6f57e: fix(watchevents, server): the watch stream refuses a subscriber when the instance has no watch subscription, as the activity stream does (BUG-2800) (#1451) (@xarmian)
02cbfd5: fix(web): 44px touch targets at phone width: strip, card ⋯, status chip (TASK-2244) (#1674) (@xarmian)
a4f16bf: fix(web): 44px touch targets round 2: group archive button, board lane +/⋯, card tags (TASK-3311) (#1698) (@xarmian)
6c4b7bf: fix(web): Conventions and Playbooks offer status, edit and delete only to accounts that may edit the item (BUG-3266) (#1631) (@xarmian)
293de6a: fix(web): Conventions, Playbooks and the library offer create, import and Activate only to accounts that may create there (BUG-3264) (#1624) (@xarmian)
6fba216: fix(web): New collection and saved-view controls render only for accounts the server lets write them (BUG-3261, BUG-3262) (#1620) (@xarmian)
4e2a7cd: fix(web): a board lane named proto or constructor keeps its own bucket and sort, so one option can no longer un-proxy the whole board (BUG-3208) (#1549) (@xarmian)
0b601b1: fix(web): a body saved outside the item pane into an open tab's live document says so (BUG-3230 U3) (#1597) (@xarmian)
bda3015: fix(web): a body the server marks as behind its live document says so wherever it is rendered — share pages, the read-only pane, the diff, and a dot on marked rows in lists, backlinks and the palette (BUG-3050 U3) (#1577) (@xarmian)
a5000fa: fix(web): a card the caller may only view cannot be moved from any door: board, list, table and child menus, drag by pointer or keyboard, the roles board, and the lane renumber (BUG-3259) (#1630) (@xarmian)
a2917bf: fix(web): a collection named "Constructor" no longer crashes the command palette, and no other value naming an Object.prototype member crashes a view or loses its lane (BUG-3054) (#1527) (@xarmian)
3c1d126: fix(web): a copy or move whose source body was behind its live document says so instead of a green success (BUG-3230 U1) (#1594) (@xarmian)
09f9bfb: fix(web): a date field holding a valid RFC3339 timestamp shows its day instead of "Invalid Date", and the picker opens on it (BUG-3225) (#1560) (@xarmian)
81e58a7: fix(web): a field filter keeps exactly the items its lane holds, so a number under a retyped select or a multi_select value is found (BUG-3052 unit 3) (#1554) (@xarmian)
9e4ffe5: fix(web): a field value whose shape does not match its declared type shows its raw text with a note and is replaced explicitly, never coerced (BUG-3052 unit 2) (#1556) (@xarmian)
38034bb: fix(web): a lane draft is keyed by the field it was typed under, so a regroup cannot drop it into another field's same-named lane (BUG-3214) (#1542) (@xarmian)
46c91b9: fix(web): a lane draft whose lane no longer exists moves to Uncategorized, marked, instead of blocking Save all invisibly (BUG-3043) (#1538) (@xarmian)
1f7402c: fix(web): a list-field retry re-applies the gesture to the fresh row (BUG-3038) (#1431) (@xarmian)
5742393: fix(web): a pending lightbox delete confirmation owns the pointer gestures, as it already owned the keyboard (BUG-2522) (#1522) (@xarmian)
c9aae56: fix(web): a queued open-children confirmation that is no longer live is never shown (BUG-3046) (#1528) (@xarmian)
a884660: fix(web): a quick action asks before pushing an out-of-date body excerpt to an agent, and says so when it copies one (BUG-3050 U2) (#1568) (@xarmian)
5e278ac: fix(web): a reactive epoch read re-armed the collection page's search under the new identity (BUG-3084) (#1379) (@xarmian)
3f56796: fix(web): a refused title keeps the typed text, and a too-long one is refused before sending (BUG-3115) (#1437) (@xarmian)
85e0c98: fix(web): a regroup during a drop's cooldown shows the cards in the new lanes at once, instead of a notice over an empty board (BUG-3042) (#1563) (@xarmian)
bbf09ee: fix(web): a relation hold belongs to one item and one field type, and a retarget discards it instead of reviving it on return (TASK-3048) (#1582) (@xarmian)
c7cd363: fix(web): a renamed item's links follow the new title in collab-edited docs (BUG-3315 U2) (#1688) (@xarmian)
2c8960b: fix(web): a request the server never answers times out, so a gate held across it is released instead of held for ever (BUG-3211) (#1541) (@xarmian)
271174f: fix(web): a server sync_required is answered after a per-tab random delay, so a fleet-wide coverage drop does not land every tab's resync on the database at once (BUG-2761) (#1517) (@xarmian)
6e83faa: fix(web): a share page draws mermaid diagrams through the app's one mermaid loader, falling back to the code (TASK-2248 U2) (#1586) (@xarmian)
e6d29c9: fix(web): a share page renders wiki-links as text and internal links as plain text, so an anonymous viewer is never sent to a login (TASK-2248 U1) (#1585) (@xarmian)
275efc1: fix(web): a starting copy preview cancels the debounce it already answers (BUG-3151) (#1440) (@xarmian)
96a75e7: fix(web): a stored field value String() cannot convert no longer crashes the list, board, table, child items or shared views (BUG-3052 unit 1) (#1553) (@xarmian)
07e73fe: fix(web): a text selection dragged out of a modal no longer closes it — backdrops dismiss only when the press and the release both land on them (BUG-3229) (#1578) (@xarmian)
c0819dd: fix(web): a wheel over a lightbox nav arrow or the close button no longer zooms the image (BUG-2507) (#1521) (@xarmian)
621720d: fix(web): an identity change is a load on ItemDetail, and the commit points no generation reaches are fenced (BUG-3084) (#1387) (@xarmian)
0362057: fix(web): an item change delivered while the item page is still loading is recorded and re-read, not dropped (BUG-3198) (#1513) (@xarmian)
0aa777c: fix(web): an open item timeline and the activity page show a merged activity row without a reload, throttled to one re-read per 10s (BUG-3160) (#1466) (@xarmian)
f4e12db: fix(web): board and list drag are off on a touch device, not only on a narrow screen, so a held finger no longer moves an item (BUG-3158) (#1461) (@xarmian)
9aa85c4: fix(web): create chrome offers only collections the caller may create in: sidebar quick-add and + New, Cmd-N and its picker, dashboard buttons, quick capture and Extract (BUG-3258) (#1622) (@xarmian)
0d0c0cf: fix(web): duplicating a long-titled playbook cuts the source so " (copy)" fits (BUG-3149) (#1442) (@xarmian)
edf0314: fix(web): editors send an item's body only when it changed and always with its version token, so a stale body cannot replace a tab's unsaved edits (BUG-3050 U1) (#1566) (@xarmian)
0f1477f: fix(web): every job on the one mermaid render queue is bounded, so a diagram that never settles no longer stalls every diagram after it (BUG-3239) (#1591) (@xarmian)
7fef073: fix(web): fence every async commit point on the collection page on the signed-in identity (BUG-3084) (#1372) (@xarmian)
d2ceab6: fix(web): fence every async commit point on the library page on the signed-in identity (BUG-3084) (#1378) (@xarmian)
cee0267: fix(web): fence every async commit point on the roles board on the signed-in identity (BUG-3084) (#1375) (@xarmian)
17acbe4: fix(web): fence every settings-page commit point on the signed-in identity (BUG-3006) (#1370) (@xarmian)
364c94f: fix(web): fence the dashboard's load on the signed-in identity (BUG-3084 surface 4) (#1382) (@xarmian)
3cc3ba5: fix(web): flush the collaborative teardown on the events a suspended tab actually delivers (BUG-3030) (#1363) (@xarmian)
5272c7a: fix(web): identity-fence ItemDetail's children, part 2 (BUG-3105) (#1410) (@xarmian)
1e16d22: fix(web): link text with markdown/HTML-significant characters survives a save (BUG-3315 U1) (#1686) (@xarmian)
212645c: fix(web): model post-await COMMITS in the child identity guard; fence ShareDialog and ItemPicker's hand-off (BUG-3105 PR A) (#1402) (@xarmian)
d44c4f6: fix(web): one workspace-access stream per browser, so open tabs cannot starve a new page over HTTP/1.1 (BUG-3318) (#1696) (@xarmian)
3ce7c5c: fix(web): pane Back holds the restored scroll position until the pane settles (BUG-3250) (#1606) (@xarmian)
25c6681: fix(web): reconcile a list response that predates a workspace create (BUG-2981) (#1371) (@xarmian)
b64ee56: fix(web): surface the server's reason for a refused bulk restore (BUG-3102) (@xarmian)
e190809: fix(web): tag suggestions can be picked from the keyboard (BUG-3150) (#1441) (@xarmian)
b6f4e85: fix(web): the activity stream owns its reconnect, so a refused tab recovers and a restart does not herd (BUG-2733) (#1454) (@xarmian)
fb7e39e: fix(web): the collection page's identity fence must be able to recover (BUG-3084) (#1374) (@xarmian)
4d4ab1d: fix(web): the collection table owns its scroll in table view, so its header row sticks while the rows scroll (BUG-3164) (#1551) (@xarmian)
3c6a080: fix(web): the item pane never puts an older row over a newer one (BUG-3036) (#1430) (@xarmian)
5ae8eeb: fix(web): the item pane's raw-markdown saves are refused, not applied, over another tab's unstored edits; an unload save keeps its text for recovery (BUG-3230 U0) (#1590) (@xarmian)
326acd2: fix(web): the item pane's save indicator counts the writes outstanding, so "Saved" never shows over a write still in flight (BUG-3044) (#1532) (@xarmian)
eb3c490: fix(web): the item title shows its length while you type and marks it invalid past the server's limit (BUG-2836) (#1571) (@xarmian)
ac99f66: fix(web): the mobile apps link no marketing page (TASK-3299) (#1660) (@xarmian)
69c2f5d: fix(web): the mobile pane trap keeps focus when the focused control is removed (BUG-3148) (#1439) (@xarmian)
3f75c2f: fix(web): the pane Back scroll restore waits past the collab sync grace, so it no longer races it and loses under load (BUG-3228) (#1592) (@xarmian)
77c5915: fix(web): the playbook editor opens read-only for accounts that cannot edit the playbook (BUG-3270) (#1632) (@xarmian)
5099fb4: fix(web): the playbook editor saves only the fields you changed, so an untouched status it cannot show is no longer overwritten with a default (BUG-3075) (#1562) (@xarmian)
209e18c: fix(web): the remaining direct fetches run under the request deadline (BUG-3216) (#1543) (@xarmian)
4333c0b: fix(web): the roles board and an item's child list withhold drag on a touch device, so a held finger no longer changes a role or reorders children (BUG-3159) (#1465) (@xarmian)
db37dc2: fix(web): the split pane's Back returns to the previous item at the position it was read at (BUG-2182) (#1473) (@xarmian)
0862efd: fix(web): the status chip opens a picker instead of cycling on a tap, so a tap no longer moves a card (BUG-3157) (#1460) (@xarmian)
53c567d: fix(web): the sync cursor is only ever a server time taken before the reads it vouches for (BUG-3207) (#1525) (@xarmian)
d67a02e: fix(web): the tag saver no longer overwrites a concurrent tag change (BUG-3143) (#1432) (@xarmian)
e2aeb51: fix(web): the tags page, the playbook editor and the collection page's remaining commit points check the identity that asked (BUG-3236, BUG-3238) (#1589) (@xarmian)
be37c14: fix(web): the workspace sync seed delivers the changes it returns instead of dropping them (BUG-3201) (#1520) (@xarmian)
4b5daad: fix(web): three identity leaks found by sweeping the item pane's unmodelled post-suspension calls, and the guard now enumerates them (BUG-3130) (#1438) (@xarmian)
7e2545a: fix(web): two quick clicks on a checkbox field are two toggles, not one lost (BUG-3047) (#1526) (@xarmian)
c9c299f: fix(web,server): a direct item share labels and colours its field chips like a collection share, from defs that disclose only what the item shows (TASK-2248 U3) (#1587) (@xarmian)
47fc780: fix(workspaces): GET /workspaces/{ws} marks a guest, so a guest who arrives by URL gets the guest sidebar, not member chrome (BUG-3254) (#1616) (@xarmian)
55eefcb: fix(workspaces): reorder skips workspaces outside the OAuth consent allow-list, as the list endpoint does (BUG-3255) (#1614) (@xarmian)
58f3a68: fix(workspaces): workspace import adds the new workspace to the importing OAuth connection, as create does (BUG-2794) (#1491) (@xarmian)
a61f504: fix: a move refused for a value only the caller can supply gets a way forward on web, CLI and MCP (BUG-3200) (#1690) (@xarmian)
c1076ce: fix: a relation's "no target" has one stored form, and required means it names one (BUG-3028) (#1427) (@xarmian)
371ea3a: fix: a tab's older content write can no longer land over its newer one (BUG-3080) (#1435) (@xarmian)
7ff8b58: fix: an overwrite, including a confirmed version restore, that deleted another tab's unsaved edits says how many (BUG-3230 U2) (#1595) (@xarmian)
1ed52bd: fix: no commerce in the mobile apps (TASK-3293, PLAN-3291 U1, BUG-3290) (#1658) (@xarmian)
5fe478b: fix: refuse a token-guarded content write over unflushed tab edits (BUG-3133) (#1423) (@xarmian)
e7811c5: fix: registering from an invitation link lands in the joined workspace on an ephemeral tab (BUG-3284) (#1647) (@xarmian)
79ed724: refactor(editor): shared modules for codeBlock, link and table (TASK-2198 U1) (#1677) (@xarmian)
6b85f68: refactor(mcp): route tool results through a Pad-owned seam (TASK-2306 unit 2) (#1611) (@xarmian)
3017e82: refactor(store): CreateItem passes its options through createItemTx (BUG-2808 PR B review r1) (@xarmian)
bd21b97: refactor(store): delete the NUL SUSPECT path, now that the predicate sees a NUL behind a repeated key (BUG-3220) (#1561) (@xarmian)
115abb9: refactor(web): TopBar closeTab lands through tabLanding, the one copy of the Q3 rule (TASK-3280) (#1642) (@xarmian)
ccd5feb: refactor(web): the six click-outside menus dismiss through the shared clickOutside primitive, so no menu carries a hand-rolled window click closer (BUG-3231) (#1580) (@xarmian)