Skip to content

fix(intake): make Notion claims workspace-durable - #217

Open
khaliqgant wants to merge 3 commits into
mainfrom
codex/notion-durable-claim-20260806
Open

fix(intake): make Notion claims workspace-durable#217
khaliqgant wants to merge 3 commits into
mainfrom
codex/notion-durable-claim-20260806

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Aug 6, 2026

Copy link
Copy Markdown
Member

Summary

  • make the stable Notion source key authoritative in the shared Agent Relay workspace before any GitHub issue creation or fleet spawn
  • keep the local intake state file as a reconciliation cache, including rollout migration for prior receipts
  • preserve digest/contract mismatch fail-closed behavior and add cross-machine issue/spawn regressions
  • document the product decisions: Notion remains read-only intake, and issueSource remains the lifecycle adapter selector (linear | github)
  • document the exact single-config resolution rule

Assessment

  1. Local claim durability is a defect and is fixed here. A deterministic claim channel uses workspace-global channel uniqueness as the cross-dispatcher gate. The acknowledged claim record is immutable and digest-bound. Message idempotency is intentionally not the authority because it is sender-scoped and time-bounded.
  2. Notion writeback remains a deliberate boundary. The mounted page is a read-only execution contract; there is no guarded Notion property/comment adapter or operator-defined lifecycle mapping. Repository lifecycle stays on the generated GitHub issue.
  3. Notion does not become issueSource: notion. issueSource selects discovery plus lifecycle writeback. The separate intake command normalizes Notion tasks into GitHub lifecycle or exact-path work.

Full assessment: docs/notion-ticket-feeder-assessment.md.

Fail-closed behavior

  • claim write failure aborts before issue creation or agent spawn
  • an incomplete/ambiguous claim blocks later dispatch
  • a second machine with a separate local statePath cannot create a second issue or spawn a second agent
  • claim/source digest mismatch blocks
  • mounted content is re-hashed again after claiming and before the external action
  • missing active Agent Relay workspace blocks every non-dry-run Notion intake

Verification

  • npm run build
  • focused: 140/140 tests passed across intake claim, Notion intake, CLI, config schema, and built entrypoints
  • full suite under bounded workers: 1,440/1,441 passed; one unrelated 5-second published-root import timeout
  • isolated rerun of that file: 13/13 passed, including the timed-out test in 393 ms
  • package dry-run contains dist/intake/notion-relay-claim.js and its declarations
  • staged diff check and token-shaped secret scan passed

Live proof / current limitation

Chief already has a live Notion-to-agent receipt for notion-9a84f582-8fc3bc47 on kjg-laptop, with a durable fleet invocation record and portable Relay delivery. Mounted repository pages also produced labeled lifecycle issues (Cloud #2935 and Relay #1433).

I did not create another production task: every page in Chief's active manifest already has a receipt, so that would duplicate real work. Current Relayfile health reports Notion as lagging with no sync cursor/watermark, and the Chief snapshots are dated August 5. The remote page is readable and the checked page digest matches the local snapshot, but a fresh-page ingress proof remains blocked until a new Notion sync watermark is observable.

No merge or deployment performed.


Summary by cubic

Makes Notion intake claims workspace-durable to stop duplicate issue creation and agent spawns across machines. Adds strict portable-mount migration safeguards that block uncertain reconciliation and require an acknowledged shared claim before any external action.

  • Bug Fixes

    • Added RelayChannelNotionClaimStore (via @agent-relay/sdk) and require a claim ACK before any issue, body edit, worker spawn, or portable-mount redispatch.
    • Enforced cross-machine exclusion: observe existing claims, verify digest, and reconcile to running agents via workspace lookup; block if a claim exists but no agent is found, lookup is unavailable, or a portable-migration claim exists without a local completion receipt.
    • Added a durable migration claim (<sourceKey>:portable-mount) to prevent double redispatch during portable mount upgrades.
    • CLI wires the claim store by default and requires an active Agent Relay workspace key for all non-dry-run Notion dispatches; it only creates RelayChannelNotionContractPublisher when workerMountTransport.kind is relay-channel.
    • Treat the local receipt as a cache: preserve re-hash-after-claim, block local/portable mismatches, and refuse edits that change delivery/body semantics.
    • Kept Notion as read-only intake and limited issueSource to linear | github (schema rejects 'notion').
  • Migration

    • Non-dry-run Notion intake now requires an active Agent Relay workspace key for its durable shared claim; relay-channel worker delivery still also needs the key.
    • Blocked runs include enough info to derive the claim channel (factory-notion-claim-<sha256(sourceKey)>). Verify no matching issue/agent exists before deleting the channel to unblock; for portable upgrades use <sourceKey>:portable-mount.

Written for commit 5c13be5. Summary will update on new commits.

Review in cubic

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: eacb9bca-0f7d-424c-9f35-83cc9cd16d72

📥 Commits

Reviewing files that changed from the base of the PR and between 94bcd4e and 5c13be5.

📒 Files selected for processing (2)
  • src/intake/notion.test.ts
  • src/intake/notion.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/intake/notion.test.ts
  • src/intake/notion.ts

📝 Walkthrough

Walkthrough

Notion intake now requires immutable, workspace-global Agent Relay claims before GitHub issue creation or workspace-agent dispatch. The fleet CLI manages claim-store lifecycle. Documentation and schema validation separate Notion intake from lifecycle issue sources.

Changes

Notion claim durability

Layer / File(s) Summary
Relay-backed claim store
src/intake/notion-relay-claim.ts, src/intake/index.ts, src/intake/notion-relay-claim.test.ts, src/__tests__/dist-entrypoints.test.ts
Adds validated Relay-backed claims with deterministic channels, atomic acquisition, bounded pagination, disposal, public exports, and tests for concurrency and failed writes.
Claim-aware intake dispatch
src/intake/notion.ts, src/intake/notion.test.ts
Requires durable claims before GitHub issue creation or workspace-agent spawning. Reconciles claims with receipts, delivery metadata, migrations, and running agents.
Fleet claim-store lifecycle
src/cli/fleet.ts, src/cli/fleet.test.ts
Initializes or injects claim stores, resolves active Relay workspaces, passes claims to intake, and disposes resources independently.
Intake and lifecycle configuration contract
README.md, docs/notion-ticket-feeder-assessment.md, src/config/schema.ts, src/config/schema.test.ts
Documents Notion as read-only intake, rejects issueSource: 'notion', and restricts configuration resolution to the selected path or current-directory config.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant NotionIntake
  participant ClaimStore
  participant AgentRelay
  participant GitHub
  participant WorkspaceAgent
  NotionIntake->>ClaimStore: Acquire digest-bound claim
  ClaimStore->>AgentRelay: Create or join workspace claim channel
  AgentRelay-->>ClaimStore: Return claim status
  ClaimStore-->>NotionIntake: New or existing claim
  NotionIntake->>GitHub: Create or reconcile issue
  NotionIntake->>WorkspaceAgent: Locate or spawn exact-path agent
Loading

Possibly related PRs

Poem

I claim each page before I leap,
Relay records guard the sleep.
No issue twice, no agent spun,
A durable race is safely won.
— A careful rabbit 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: making Notion intake claims durable across the workspace.
Description check ✅ Passed The description directly explains workspace-durable Notion claims, fail-closed behavior, migration, documentation, and verification.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/notion-durable-claim-20260806

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/intake/notion.test.ts (2)

23-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering the claim/source digest mismatch branch.

The shared fake returns the stored claim on conflict and never returns a different digest. claimNotionTask in src/intake/notion.ts throws durable Notion claim digest does not match the mounted spec when the returned claim carries another digest, and observeNotionClaim throws the same message from get. The PR objectives list claim/source digest mismatch as fail-closed behavior. No test in the provided ranges drives a store that returns a mismatched digest.

Add one test with a store whose get or claim returns a claim for the same sourceKey with a different digest, and assert the blocked result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/intake/notion.test.ts` around lines 23 - 37, Add a test in the Notion
intake test suite using a claim store whose get or claim returns the same
sourceKey with a different digest, then assert the operation produces the
blocked result and the expected durable Notion claim digest mismatch error. Keep
the existing shared claims fixture unchanged and target the claimNotionTask or
observeNotionClaim path as appropriate.

657-688: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering the recovered-agent branch.

find returns undefined here, so the test exercises only the blocked path. src/intake/notion.ts lines 621-629 build a reconstructed workspace receipt from running.agent, running.node, and claim.claim.claimedAt when find returns an agent. That branch writes local state and reports already-dispatched. No test in the provided ranges drives it.

Add a case where the second run's find resolves to a running agent, then assert the already-dispatched status, the reported agent, and that dispatch is still called only once.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/intake/notion.test.ts` around lines 657 - 688, The existing concurrency
test only covers the blocked path because WorkspaceTaskDispatcher.find returns
undefined. Add a test case in the same suite where the second run’s find
resolves to a running agent, exercising runNotionIntake’s reconstructed
workspace receipt path; assert the second result is already-dispatched with the
expected agent and workspace.dispatch is called exactly once.
src/cli/fleet.ts (1)

466-505: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider flattening the nested shutdown steps.

The finally block now nests five levels of try/finally to guarantee that each disposal runs after the previous one fails. The behavior is correct. The nesting makes the ordering hard to verify by reading.

A sequential loop over guarded steps preserves the order and the independent-failure property with less nesting.

♻️ Proposed refactor
   } finally {
-    try {
-      try {
-        await notionClaims?.dispose?.()
-      } catch {
-        err.write('[factory] warning: Notion claim store failed during shutdown\n')
-      }
-    } finally {
-      try {
-        try {
-          await notionContracts?.dispose?.()
-        } catch {
-          err.write('[factory] warning: Notion contract publisher failed during shutdown\n')
-        }
-      } finally {
-        try {
-          await mount?.dispose?.()
-        } finally {
-          try {
-            await fleet?.dispose()
-          } finally {
-            if (reporter) {
-              try {
-                await reporter.report(createFactoryCloudEventV1({
-                  type: 'instance.stopping',
-                  attributes: { component: 'cli', operation: 'stop' },
-                }))
-                await reporter.report(createFactoryCloudEventV1({
-                  type: 'instance.stopped',
-                  attributes: { component: 'cli', operation: 'stop' },
-                }))
-                await reporter.close?.({ deadlineMs: 2_000 })
-              } catch {
-                err.write('[factory] warning: Cloud progress reporter failed during shutdown\n')
-              }
-            }
-          }
-        }
-      }
-    }
+    const shutdownSteps: readonly { run: () => Promise<unknown>; warning: string }[] = [
+      {
+        run: async () => await notionClaims?.dispose?.(),
+        warning: '[factory] warning: Notion claim store failed during shutdown\n',
+      },
+      {
+        run: async () => await notionContracts?.dispose?.(),
+        warning: '[factory] warning: Notion contract publisher failed during shutdown\n',
+      },
+      {
+        run: async () => await mount?.dispose?.(),
+        warning: '[factory] warning: Relayfile mount failed during shutdown\n',
+      },
+      {
+        run: async () => await fleet?.dispose(),
+        warning: '[factory] warning: fleet client failed during shutdown\n',
+      },
+      {
+        run: async () => {
+          if (!reporter) return
+          await reporter.report(createFactoryCloudEventV1({
+            type: 'instance.stopping',
+            attributes: { component: 'cli', operation: 'stop' },
+          }))
+          await reporter.report(createFactoryCloudEventV1({
+            type: 'instance.stopped',
+            attributes: { component: 'cli', operation: 'stop' },
+          }))
+          await reporter.close?.({ deadlineMs: 2_000 })
+        },
+        warning: '[factory] warning: Cloud progress reporter failed during shutdown\n',
+      },
+    ]
+    for (const step of shutdownSteps) {
+      try {
+        await step.run()
+      } catch {
+        err.write(step.warning)
+      }
+    }
   }

Note one behavior change in the proposal: mount and fleet disposal failures are currently propagated, and the proposal converts them to warnings. Keep them propagating if the current behavior is intentional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/fleet.ts` around lines 466 - 505, Flatten the shutdown sequence
currently anchored by the outer try/finally chain into sequential guarded
disposal steps, preserving the order: notionClaims, notionContracts, mount,
fleet, then reporter shutdown. Keep independent cleanup running after earlier
failures; retain the existing warning handling for Notion and reporter failures,
while allowing mount and fleet disposal failures to propagate as they do now.
src/intake/notion-relay-claim.ts (1)

61-86: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add or document how operators clear a stranded claim channel.

If channels.create succeeds but relay.messages.send fails, the channel exists with no claim record. Later claim/get calls throw has 0 immutable claim records; refusing dispatch and block the source key. RelayChannelNotionClaimStore only exposes claim, get, and dispose; add a release/cleanup method or document the operator agent-relay channel-deletion step.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/intake/notion-relay-claim.ts` around lines 61 - 86, The claim flow around
RelayChannelNotionClaimStore must provide a way to clear a channel created
before relay.messages.send fails. Add a release/cleanup operation that deletes
the stranded channel, or document the exact agent-relay channel-deletion
procedure for operators; ensure it is accessible alongside claim, get, and
dispose and targets the source key’s durable Notion claim channel.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 226-234: Update the README paragraph around workerMountRoot to
explicitly name workerMountTransport as the field that defaults to `{ "kind":
"local" }`, while preserving the existing behavior and explanation of omitted
manifests.

In `@src/intake/notion.ts`:
- Around line 611-620: Update the existing-claim branch around workspace.find so
it distinguishes an unavailable find capability from a completed lookup with no
running agent. Preserve the current fail-closed blocked result, but use a reason
stating that the dispatcher lacks lookup support when find is absent and the
existing no-running-agent reason only when find was invoked and returned no
agent.

---

Nitpick comments:
In `@src/cli/fleet.ts`:
- Around line 466-505: Flatten the shutdown sequence currently anchored by the
outer try/finally chain into sequential guarded disposal steps, preserving the
order: notionClaims, notionContracts, mount, fleet, then reporter shutdown. Keep
independent cleanup running after earlier failures; retain the existing warning
handling for Notion and reporter failures, while allowing mount and fleet
disposal failures to propagate as they do now.

In `@src/intake/notion-relay-claim.ts`:
- Around line 61-86: The claim flow around RelayChannelNotionClaimStore must
provide a way to clear a channel created before relay.messages.send fails. Add a
release/cleanup operation that deletes the stranded channel, or document the
exact agent-relay channel-deletion procedure for operators; ensure it is
accessible alongside claim, get, and dispose and targets the source key’s
durable Notion claim channel.

In `@src/intake/notion.test.ts`:
- Around line 23-37: Add a test in the Notion intake test suite using a claim
store whose get or claim returns the same sourceKey with a different digest,
then assert the operation produces the blocked result and the expected durable
Notion claim digest mismatch error. Keep the existing shared claims fixture
unchanged and target the claimNotionTask or observeNotionClaim path as
appropriate.
- Around line 657-688: The existing concurrency test only covers the blocked
path because WorkspaceTaskDispatcher.find returns undefined. Add a test case in
the same suite where the second run’s find resolves to a running agent,
exercising runNotionIntake’s reconstructed workspace receipt path; assert the
second result is already-dispatched with the expected agent and
workspace.dispatch is called exactly once.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d5869ee-8f4f-4043-a4d9-ed36f8f7ea87

📥 Commits

Reviewing files that changed from the base of the PR and between 16ad97e and 2a2487b.

📒 Files selected for processing (12)
  • README.md
  • docs/notion-ticket-feeder-assessment.md
  • src/__tests__/dist-entrypoints.test.ts
  • src/cli/fleet.test.ts
  • src/cli/fleet.ts
  • src/config/schema.test.ts
  • src/config/schema.ts
  • src/intake/index.ts
  • src/intake/notion-relay-claim.test.ts
  • src/intake/notion-relay-claim.ts
  • src/intake/notion.test.ts
  • src/intake/notion.ts

Comment thread README.md Outdated
Comment thread src/intake/notion.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 12 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/intake/notion.ts">

<violation number="1" location="src/intake/notion.ts:523">
P1: A transient failure after this claim succeeds can permanently dead-letter the task: no receipt is written, and every retry sees the existing claim and refuses to create the issue or spawn the agent. A durable completion/retry reconciliation path or an explicit operator-removable claim is needed for failures after claim acquisition.</violation>
</file>

<file name="src/intake/notion-relay-claim.test.ts">

<violation number="1" location="src/intake/notion-relay-claim.test.ts:53">
P3: This test gives logical-contract coverage, not cross-machine coverage. Both dispatchers share a single in-process Map whose create-then-join and immediately-visible-message semantics are hand-coded to always produce one claim, so it cannot detect a real cross-host regression such as non-atomic channel creation across processes or delayed message visibility (which would yield 2 records or a 0-record race). Consider renaming the intent as 'workspace-uniqueness contract' and adding a separate two-process/e2e test against the real relay if cross-machine durability is a stated goal.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/intake/notion.ts
Comment thread src/intake/notion.ts
Comment thread src/cli/fleet.test.ts
Comment thread src/intake/notion-relay-claim.test.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/intake/notion.ts
Comment thread src/intake/notion.ts
Comment thread src/intake/notion.test.ts
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.

3 participants