Skip to content

Pad v0.17.0

Choose a tag to compare

@github-actions github-actions released this 02 Oct 01:53
· 87 commits to main since this release
5d420ef

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.
  • Dependency advisories cleared: undici 7.30.0 (test-only), markdown-it 14.3.2 and devalue 5.9.4.
  • 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.

Changelog

Features

  • 07c00e0: feat(agent): add compact item view (@mattfaltyn)
  • 328e0a4: feat(agent): add on-demand guide (@mattfaltyn)
  • cacf492: feat(agent): install compact shared skill (@mattfaltyn)
  • 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)
  • 828c9ba: feat(items): execution lease — atomic claim/checkout with expiry (#1238) (@b4rk13)
  • fa2755e: feat(materialize): headless op-log materializer bundle, goja runner and worker (TASK-2198 U2) (#1678) (@xarmian)
  • cb99879: feat(materialize): recover unsaved editor sessions into items.content (TASK-2198 U4) (#1683) (@xarmian)
  • d78ff9b: feat(materialize): worker supervisor with per-OS memory cap (TASK-2198 U3) (#1682) (@xarmian)
  • 6ed1118: feat(mcp): DR-9 security floor, per-route limits and pre-auth counting (TASK-2320, PLAN-2310 U4) (#1667) (@xarmian)
  • eb167be: feat(mcp): MCP addressing from config; mcp_enabled setting with an env lock (TASK-3302, PLAN-2310 U1) (#1662) (@xarmian)
  • 6b9eafd: feat(mcp): add structured-only results (@mattfaltyn)
  • 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)
  • e43bd7b: feat: the console MCP section; Connected Apps follows OAuth (TASK-3303, PLAN-2310 U5) (#1666) (@xarmian)

Bug fixes

  • 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)
  • 8e5b43d: fix(agent): align compact dispatcher guidance (@mattfaltyn)
  • 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)
  • 6a3ac45: fix(auth): exact invitation email match, no Unicode folding (BUG-3282) (#1644) (@xarmian)
  • cbbe68a: fix(charts): migrate chart layers to layercake 11's rune context (TASK-3093) (@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)
  • 5faf915: fix(limits): item restore enforces items_per_workspace (BUG-3101) (@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)
  • a2e75ec: fix(mcp): preserve compact results in Cursor (@mattfaltyn)
  • 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)
  • 7f216a0: fix(members): invitation accept enforces members_per_workspace (BUG-3098) (@xarmian)
  • 5124a5f: fix(menu): a scroll pending when a portal menu opens no longer closes it (BUG-3278) (#1687) (@xarmian)
  • 033615a: fix(metrics): bound the MCP tool label and the HTTP method label, which callers could fill with invented values (BUG-2817) (#1456) (@xarmian)
  • b510d67: fix(oauth): sweep expired token, code and PKCE rows (BUG-3301) (#1671) (@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)
  • 8405070: fix(store): serialise concurrent account deletions on Postgres (BUG-3286) (#1653) (@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)
  • d7df8a4: fix(web): fence child components' post-await sends against identity change (BUG-3095) (#1400) (@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)

Performance

Refactors

  • 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)

Other changes

  • e78e06b: BUG-3278 diagnostics: self-describing move-menu steps + flaky-run evidence upload (#1648) (@xarmian)
  • dba74e8: Fix macOS session test portability (@mattfaltyn)
  • 4abc8ad: mcp+cli: ToolSurfaceVersion 0.38 for claim/release; refuse sub-second lease ttl on both transports (#1405) (@xarmian)
  • 6282cb9: mcp: correct the claim/release catalog comment — the actions own the 0.38 bump (#1406) (@xarmian)