Skip to content

Notion adapter v1: read, verified append, formatting-preserving block edit - #48

Merged
DaveHomeAssist merged 17 commits into
mainfrom
claude/notion-adapter-v1
Oct 2, 2026
Merged

DaveHomeAssist merged 17 commits into
mainfrom
claude/notion-adapter-v1

Conversation

@DaveHomeAssist

@DaveHomeAssist DaveHomeAssist commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Summary

DaveLLM's own Notion adapter, behind DAVE_ENABLE_NOTION_TOOLS (honored only with DAVE_ENABLE_TOOLS):

  • notion.page.read (read): title plus blocks with run-scoped refs (b1, b2, …); marks blocks whose children were not read.
  • notion.page.append (write, approval every call): plain-text blocks at the end of a configured page, verified by readback.
  • notion.block.update (write, approval every call): replace text inside one rich text run (every other run, link, mention and equation sent back unchanged) and/or check a to-do (named by block_text).

Guarantees:

  • Scope: pages only from DAVE_NOTION_PAGES; blocks only by run-local refs held server-side; before every write the page and block are re-read, parents walked to the configured page, content compared. Not a lock (Notion has none).
  • Outcomes: every write is verified, failed (retrying is safe), or unknown (never repeated in the run). 429/529 count as not carried out and 5xx writes are never retried, matching Notion's own JS client. A cancelled run sends no new write; a dispatched one is still checked.
  • Approval card: exact arguments plus the run's own record of the whole block before and after (GET /tools/agent/runs/{id}/pending/notion-context), and a warning when the call will be refused. Text nodes only.
  • Transport and secrets: fixed api.notion.com, Notion-Version: 2026-03-11, no redirects or proxies, absolute deadlines, response cap; the secret (environment only) is validated and appears in no result, log, route, event, transcript or manifest.
  • Limits: lazy reads (300 blocks, 30 requests, 48 KiB, linear trimming), 8 s bounded page-lock wait, 2,000 blocks / 8 MiB per run ledger.

Iterated through 10 test → fix runs (scope, cancellation, rich-text fuzzing with Hypothesis, transport and secrets, read limits, lifecycle, approval UI in a real browser, concurrency, docs and contracts, the proposal's acceptance sequence and Notion's SDK types). Includes Codex's independent cancellation fixes (#49, #50). Complete error catalog in docs/DAVELLM_TOOLS.md.

Separate follow-ups (task chips, not in this PR): file.edit overlapping-match ambiguity; a DaveHarness pre-approval check hook; Cache-Control for static assets.

Test plan

  • Full suite locally (Python 3.14): 944 passed, 1 skipped (baseline 757 passed, 1 skipped)
  • 138 adversarial probes + 41 adapter tests + 5 independent-review tests; adapter branch coverage 97% (CI gate 90%)
  • Strict mypy on the adapter, mypy daveharness, manifest --check, node checks, 9 approval-preview tests, git diff --check
  • Real app in a browser with simulated Notion/Ollama: edit, to-do, reject, hostile append, phone width, dark mode, refusal warning
  • CI 3.12/3.13/3.14 on the final commit
  • Live acceptance on a designated page (needs Dave's internal connection + shared page; nothing has touched real Notion yet)

Dave Robertson added 2 commits October 1, 2026 08:53
…ing block edit

notion.page.read, notion.page.append and notion.block.update behind
DAVE_ENABLE_NOTION_TOOLS (needs DAVE_ENABLE_TOOLS). Pages are named in
DAVE_NOTION_PAGES; the secret comes only from DAVE_NOTION_TOKEN.

- Refs (b1, b2, ...) live in a per-run server-side ledger keyed by run_id;
  the model never supplies a Notion ID, and the tools refuse outside
  lifecycle runs.
- Before every write: re-read the block, walk its parents to the
  configured page, compare the content the run saw.
- Text edits replace inside one rich text run and send every other run,
  link, mention and equation back unchanged.
- Writes end verified, failed, or unknown; an unknown append is never
  repeated in the run. Write plus check run shielded from cancellation
  under a per-page guard.
- Fixed api.notion.com transport, pinned Notion-Version 2026-03-11, no
  redirects, no env proxies, response cap.
- Approval previews for both write tools (text nodes only), catalog
  fixture, manifest, docs.
DaveHomeAssist and others added 2 commits October 1, 2026 09:02
…deadlines

Stop new Notion writes after cancellation and bound response reads
DaveHomeAssist and others added 8 commits October 1, 2026 09:18
…conciliation

Keep Notion cancellation tests on the ledger outcome contract
… failed vs unknown before send

- The run ledger records whether a verified append was delivered to the
  caller. When a run deadline stopped waiting, the shielded append still
  lands and verifies; an identical retry in that run is now refused
  instead of duplicating the blocks. An in-flight identical append gets
  its own refusal message.
- The page guard is checked before the ledger, so a busy refusal no
  longer overwrites an earlier record for the same content.
- An internal error before the write request is sent is reported as
  failed (nothing written), not unknown.
- Probes: tests/test_notion_adversarial.py (uncertain writes and
  cancellation, fault at every request position); fake Notion gains
  delays and skip counts.
…accurate depth reason

- notion.block.update needs block_text (the to-do's text, checked against
  what the run read) when only checked changes, and the approval card
  shows it, so a confused ref cannot tick the wrong to-do unseen.
- Before any write, the configured page is fetched; a trashed page is
  refused with a clear reason instead of Notion's opaque refusal.
- A block nested past the eight-level parent walk is reported as too
  deep to confirm, not as moved off the page.
- Probes: exact-match refs, moves into child pages and between configured
  pages, type changes, child-page refs, trashed pages and blocks,
  per-run ref isolation, deep nesting, a database configured as a page.
- old_text uniqueness counts overlapping matches: "aa" in "aaa" occurs
  twice, so the edit is refused instead of silently taking the first.
  Found by a Hypothesis property ("bb" + bold "b", old_text "bb").
- Text runs are split at 2000 UTF-16 units on code-point boundaries, so
  emoji-heavy text is accepted for appends and edits that grow a run.
- Probes: Hypothesis properties for single-run edits (every other run,
  link, mention and equation kept), cross-run refusal, split-invariant
  signatures, round trips through the fake; overlap and UTF-16 cases;
  the fake enforces the UTF-16 limit.
…t; actionable refusals

- DAVE_NOTION_TOKEN must be 20-256 printable ASCII characters without
  whitespace; otherwise every call answers a configuration error and the
  value is never sent.
- httpx.LocalProtocolError / UnsupportedProtocol mean nothing left the
  process: a write reports failed (retry allowed), not unknown.
- unauthorized, object_not_found and restricted_resource refusals name
  the operator's fix (check the token / share the page).
- A response over the 2 MB cap is reported as such for reads.
- Probes: proxies and SSL env ignored, redirects not followed, hostile
  error codes, cursor and ID injection, token absent from DEBUG logs,
  routes, events, transcript and the manifest.
- Listing stops once 300 blocks are held (3 requests for a 1,000-block
  page instead of 10).
- Blocks whose children were not read (depth limit, child pages, synced
  blocks, meeting notes, request budget, a refused subtree listing) are
  marked children_not_read; a refused subtree no longer fails the read.
- Out of request budget, blocks already fetched are still returned.
- Duplicate block IDs, repeated cursors, and has_more without a cursor
  end the listing as truncated; the append baseline treats them as
  incomplete and refuses rather than anchoring on a wrong last block.
- Probes: long pages, depth and type markers, refused subtrees, cursor
  loops, missing cursors, output budget speed and order, clipped text,
  request budget, tables and columns.
…ncel mid-write

- Probes through the router: tampered decisions (409, nothing written),
  expired approvals, approval after the run deadline, the legacy loop
  cannot pre-approve a Notion write, an edit made while approval waits is
  never overwritten, a model that edits before reading recovers, failed
  and unknown outcomes reach the model and events as errors, and
  cancelling mid-write lands once and frees the page.
- Docs: DaveHarness approves before the adapter's own checks run, so a
  call that will be refused can still ask for approval (it writes
  nothing); cancelling a run during an approved write cannot recall a
  request Notion already has (the run ends cancellation_failed).
- New GET /tools/agent/runs/{run_id}/pending/notion-context (run owner
  only) returns, from the run ledger and never from the model, the whole
  block before and after a pending edit, or the refusal a doomed call will
  meet. It contacts nothing and changes nothing.
- The approval card renders it as text: 'Whole block now / after', a note
  that formatting is kept, and a red 'This call will be refused, so
  approving it writes nothing' above the buttons.
- Probes: router pending calls render the Notion preview in the real JS;
  context content, refusals, and route scoping.
- Verified in the real app in a browser (simulated Notion and Ollama):
  edit, to-do, reject, hostile append, phone width, dark mode.
Dave Robertson added 5 commits October 1, 2026 15:59
- A write that finds another DaveLLM write to the same page waits up to
  8 s (polling, outside the shield, nothing held while waiting) before
  refusing, instead of throwing away the user's approval at once.
- A run's ledger keeps at most 8 MiB of block snapshots as well as 2,000
  blocks.
- Probes: two runs racing on one page take turns; the wait is bounded and
  a cancelled waiter takes nothing; stale cross-run edits are refused; ten
  pages in parallel stay separate; the guard is released on errors and
  cancellation; dropping a ledger mid-write is harmless; ledger memory
  bound.
- docs/DAVELLM_TOOLS.md lists every Notion message verbatim, in four
  tables (refused before sending, reads, Nothing was written, Outcome
  unknown); a probe extracts every message from the source with ast and
  keeps the catalog complete.
- CI measures davellm_notion.py branch coverage separately (97% now,
  gate 90%) without touching the DaveHarness coverage gate.
- CLAUDE.md's required checks compile davellm_node_profiles.py like CI;
  a probe pins the two lists together.
- Remove the unused _ANNOTATION_KEYS.
- Probes for every previously untested outcome path: long pages,
  accepted-but-different appends and edits, vanished anchors, unreadable
  or changed blocks after a lost edit, refused page checks, odd parents,
  the 100-run limit, errors after sending, malformed rich text, argument
  checks without the schema, ledger eviction.
CI failed on Python 3.14 for 580be1a and 6d4cdb3: the Run 5 probe used
a 1.0 s wall-clock limit and the runner took 1.13 s and 1.35 s. The
limit was flaky, but it was measuring real waste: trimming re-encoded
the whole result after every removal (182 MB of JSON for one 300-block
read, on the event loop). Each entry is now measured once and cut from
a running total; the probe bounds bytes encoded instead of time.
…ion's client

- 429 and 529 (service_overload) mean Notion did not carry the request
  out, as in Notion's own JS client: retried once (Retry-After up to 5 s,
  or 1 s without one), otherwise reported as Nothing was written instead
  of an unknown outcome that blocked a safe retry.
- Reads are retried once after a 500 or 503; writes never are.
- The legacy transcription block (renamed meeting_notes) is not opened.
- Probes: the proposal's whole live acceptance sequence through the
  router (read, approved append, verified, formatting-preserving edit,
  rejected edit, out-of-scope page, re-read), and listing shapes from the
  SDK's types (partial blocks, tab, transcription).
- The lifecycle fixture keeps one event loop per test: a TestClient
  outside a with block starts a loop per request and cancels run tasks
  that outlive their POST.
…em today

Developer portal URL, workspace-owner requirement, Configuration tab for
the token, and the page's ••• menu → Add connections; start the router
from a terminal so the exports are seen.
@DaveHomeAssist
DaveHomeAssist marked this pull request as ready for review October 2, 2026 04:24
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@DaveHomeAssist
DaveHomeAssist merged commit 2381c1b into main Oct 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant