Skip to content

refactor(native-integration): make stores async, drop actors - #4143

Merged
ScriptedAlchemy merged 2 commits into
masterfrom
refactor/storage-native-integration-async
Oct 16, 2026
Merged

ScriptedAlchemy merged 2 commits into
masterfrom
refactor/storage-native-integration-async

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Summary

Step 5 of the storage redesign (docs/plans/storage/README.md), fifth slice. It deletes the native-integration storage actor thread, and the GitHub stack-delivery actor that had the same shape. Same pattern as #4140. Two commits:

  • Native integration. NativeIntegrationStore is now async. DaemonNativeIntegrationStore is a cloneable handle that holds the lease and awaits the global-db store.
    • Deleted: the tracedecay-native-integration-store thread, its 21-variant StoreCommand, its block_on settle and the SharedDaemonNativeIntegrationStore wrapper.
    • The coordinator's per-repository lock is now an async mutex held across the native commit.
    • NativeIntegrationPort and its service are async. NativeWorktreePort is async for remove and reconcile, the two calls that use the store. OptionalStackPreflightPort and GitHubStackSignalExpandPort are async.
  • Stack delivery. The tracedecay-github-stack-delivery-store thread and its 10-variant protocol are deleted. DaemonStackDeliveryStoreV1 holds the lease and the project id.
    • StackCoordinatorStore and its authorize and deliver steps are async.
    • The drain loop no longer hops to spawn_blocking, and keeps its 250 ms cadence.
    • github_stack_hook_available is async, which reaches MCP hook admission.

How each invariant is kept:

  • Compare-and-swap, one-use approval, terminal receipts, quarantine. Each is still one IMMEDIATE transaction in the global-db store. The existing global-db tests cover them.
  • Ordering per integration. The coordinator awaits each step in turn while it holds the per-repository lock. Cancel still runs concurrently and is settled by the status-revision compare-and-swap.
  • Debounced-signal dedupe. The dedupe lock is held across the append await, so check, append and insert stay one critical section.
  • Shutdown. shutdown takes the shared lease once for all clones, and later calls fail as unavailable.
  • Dropped backpressure. The actors' 64-slot queue and 5 s reply timeout are gone. Waits are bounded by the RegisteredGlobalDb writer wait, as in refactor(git-index): make transaction store async, drop actor #4140.
  • Blocking work. Operations that mix store awaits with synchronous native Git use on_blocking_pool: preflight, apply, worktree remove and reconcile, startup recovery, and cleanup recovery. Store-only operations await in place.

Test plan

Each test below was mutation-checked. The mutation was applied, the test failed with the stated assertion, and the mutation was reverted.

  • store_awaited_on_a_native_worker_never_parks_it: no block_on, and the wait counters do not move. A block_on settle in save_approval fails it.
  • delivery_store_awaited_on_a_native_worker_never_parks_it: a block_on in pending fails it.
  • shutdown_closes_every_clone_once_and_fails_later_calls_closed: keeping the lease fails it.
  • concurrent_applies_to_one_repository_never_overlap: dropping the repository guard early fails it (left 2, right 1).
  • concurrent_duplicate_debounced_signals_append_once: dropping the dedupe guard before the append fails it (left 2, right 1).
  • joined_preflight_returns_the_single_owner_result: covers the rewritten single-flight join.
  • bazel test --config=clippy:
    • agent-hosts: unit_test (540 passed)
    • application: unit_test and application_suite
    • daemon-service: unit_test
    • contracts: unit_test and contracts_suite
    • store: all targets
  • Filtered runs: //crates/tracedecay-mcp:unit_test --test_arg=hook, //crates/tracedecay:unit_test --test_arg=native_integration and --test_arg=stack, and //crates/tracedecay:product_surface_suite --test_arg=native_integration
  • bazel build --config=clippy for //crates/tracedecay:unit_test, :daemon_suite, :mcp_suite and //crates/tracedecay-mcp:unit_test
  • rustfmt_check_2024 for every touched crate
  • Full //crates/tracedecay:daemon_suite, :mcp_suite, :session_suite, :unit_test in CI

Known gap: optional_preflight now awaits between admission and counter release. If its owner future were dropped mid-preflight, the in-flight entry would leak. The daemon runs it on the blocking pool, where it always completes.

Refs #4052


Devin Review

@changeset-bot

changeset-bot Bot commented Oct 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 87bcf02

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment on lines +793 to 796
.await?;
let outcome = self
.ports
.preflight_outcomes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Concurrent stack preflights lose previews

When identical declared-stack preflights overlap, preflight_outcomes.remove gives the saved preview to only one caller. The other returns Unavailable despite a successful preflight.

(Refers to this code)

Learn more

The stack coordinator shares one disposition among identical in-flight preflights in optional_preflight. Its native preflight port caches the concrete preview once in preflight. Each caller then independently consumes that single cache entry. Even though the underlying preflight succeeds, one caller receives Unavailable; a later caller can also consume a new run's entry if it arrives before the earlier caller removes it.

Example: Two callers submit the same declared-stack request. One native preflight saves Preview P. Both receive Complete, but the first removal takes P and the second finds no outcome, so only one caller receives P.

Recommended fix: Carry the complete NativeIntegrationPreflightOutcomeV1 in the shared in-flight result, or otherwise publish an immutable shared result to every joiner. Avoid a destructive lookup keyed solely by request digest after the single-flight future returns.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@ScriptedAlchemy
ScriptedAlchemy merged commit 25ee387 into master Oct 16, 2026
14 of 20 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