Skip to content

fix(acp): serialize wakeup prompts to prevent turn misalignment#1202

Merged
carlosflorencio merged 3 commits into
mainfrom
feature/chat-messages-after-eq9
May 31, 2026
Merged

fix(acp): serialize wakeup prompts to prevent turn misalignment#1202
carlosflorencio merged 3 commits into
mainfrom
feature/chat-messages-after-eq9

Conversation

@carlosflorencio
Copy link
Copy Markdown
Member

@carlosflorencio carlosflorencio commented May 31, 2026

When a ScheduleWakeup synthetic prompt fired while a user prompt was still in flight, both calls reached the ACP bridge concurrently and stop_reason was paired with the wrong turn — shifting chat messages one prompt behind. This serializes prompts through a context-aware gate and drops stale wakeups if the session changed while queued.

Important Changes

  • Added a 1-slot promptGate channel (not a mutex) so queued wakeups honour context cancellation
  • Refactored Prompt into sendPrompt with optional session pinning; wakeups re-validate the session after acquiring the gate
  • Added regression tests with a blocking fake agent to prove max one in-flight prompt

Validation

  • make -C apps/backend fmt
  • make -C apps/backend test (all packages pass)
  • make -C apps/backend lint
  • go test -race ./internal/agentctl/server/adapter/transport/acp/... -run TestWakeup

Possible Improvements

Low risk — wakeups may be dropped if a session switch happens while one is queued, which is the intended safety behaviour.

Checklist

  • I have performed a self-review of my code.
  • I have manually tested my changes and they work as expected.
  • My changes have tests that cover the new functionality and edge cases.
  • If my change touches UI files (apps/web/), I have added or updated Playwright e2e tests in apps/web/e2e/ and verified them with make test-e2e.

Made with Cursor

ScheduleWakeup synthetic prompts could race user prompts, causing the
bridge to return stop_reason against the wrong turn and shift chat
messages one prompt behind. Add a context-aware prompt gate and pin
wakeups to their scheduled session after re-validation.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 31, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a promptGate 1-slot semaphore to the ACP Adapter to serialize prompt/session operations. Adapter.Prompt now delegates to sendPrompt, which acquires the gate and validates session pinning before sending. fireWakeup sends synthetic wakeups through sendPrompt with the scheduled session pinned, dropping them if the session changes while waiting. Regression tests verify no concurrent prompts, that session-mismatched wakeups are dropped, and that wakeups don't consume pendingContext.

Changes

Prompt gate serialization for wakeup/user-prompt race prevention

Layer / File(s) Summary
Prompt gate field initialization
apps/backend/internal/agentctl/server/adapter/transport/acp/adapter.go
Added promptGate field to Adapter struct with documentation. Initialized as a buffered 1-capacity channel in NewAdapter to serialize in-flight session/prompt activity.
Serialized prompt dispatch with session validation
apps/backend/internal/agentctl/server/adapter/transport/acp/adapter_prompt.go
Refactored Adapter.Prompt to delegate to new sendPrompt helper that acquires the gate (respecting context cancellation), validates expected session matching, conditionally drops queued wakeups, and consumes pendingContext only when the prompt is actually sent.
Wakeup gating and session-pinned delivery
apps/backend/internal/agentctl/server/adapter/transport/acp/adapter_prompt.go
Updated fireWakeup to send synthetic prompts via sendPrompt with the scheduled session ID pinned as expectSession, dropping wakeups if the active session changes while waiting. Rewrote comments to describe gated, session-pinned behavior.
Concurrency regression tests
apps/backend/internal/agentctl/server/adapter/transport/acp/wakeup_concurrency_test.go
Introduced concurrencyFakeAgent test double with atomic counters for concurrent Prompt tracking. Added three tests: verifying no overlapping prompts when wakeup fires during in-flight user prompt, verifying queued wakeups are dropped when the session changes before prompt release, and verifying wakeups do not consume/clear pendingContext.

Sequence Diagram

sequenceDiagram
  participant Adapter
  participant promptGate
  participant Agent
  Adapter->>promptGate: request gate (sendPrompt)
  promptGate->>Adapter: grant gate
  Adapter->>Agent: Prompt(message)  -- pinned to expectSession if provided
  Agent-->>Adapter: Prompt returns
  Adapter->>promptGate: release gate
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • kdlbs/kandev#706: Modifies ACP wakeup injection and scheduler behavior that this PR further adjusts via gated, session-pinned prompt delivery.
  • kdlbs/kandev#998: Also touches the ACP adapter wakeup/synthetic-prompt flow and session matching logic overlapping with this PR.

Poem

🐰 I hop a gentle, careful path,
One prompt at once to spare the wrath.
Wakeups wait, then check the scene,
If sessions shift, they stay unseen.
Quiet queues—no tangled chat—one-at-a-time, just like that.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: serializing wakeup prompts to prevent turn misalignment, which is the core problem being fixed.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
Description check ✅ Passed The PR description includes all required sections: summary (problem and solution), validation steps with specific commands, and a checklist. The description is well-structured and directly addresses the root cause.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/chat-messages-after-eq9

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 and usage tips.

@claude
Copy link
Copy Markdown

claude Bot commented May 31, 2026

Claude finished @carlosflorencio's task in 7m 37s —— View job


Code Review

  • Gather context and read changed files
  • Analyze adapter.go changes
  • Analyze adapter_prompt.go changes
  • Analyze wakeup_concurrency_test.go
  • Post inline findings
  • Post summary review

Findings

Suggestion (recommended, doesn't block)

  1. Misleading present-tense comment describes the old bug, not the fixwakeup_concurrency_test.go:73

    • Issue: The doc comment says "With the current code the synthetic wakeup prompt overlaps the user prompt (maxInFlight == 2)". This describes the pre-fix behavior in present tense — a new reader landing here will think the test is documenting a known failure.
    • Fix: Rewrite to past tense or as a "before/after" regression note, e.g. "Before this fix, the synthetic wakeup overlapped the user prompt (maxInFlight == 2). This test verifies the fix serializes them (maxInFlight == 1)."
  2. Negative-assertion waits add 4 s to every test runwakeup_concurrency_test.go:121,195

    • Issue: Both tests use time.After(2 * time.Second) to assert that a second prompt does not arrive. The backend AGENTS.md guidance is to prefer synctest or channel-based synchronisation over wall-clock waits.
    • The 2-second window is real wait time that runs on every go test invocation. For pure in-process pipes (no external subprocess), it can be tighter.
    • Fix: In TestWakeupDoesNotRaceConcurrentPromptWithUserPrompt, there is a cleaner approach: after close(fa.release) + userWG.Wait() (user prompt fully complete, gate released), wait a short but generous time (e.g. 100 ms) for the wakeup goroutine to proceed, then assert. The wakeup goroutine is unblocked and deterministically completes quickly once the gate is free. In TestWakeupDroppedWhenSessionChangesWhileQueued, same idea — the wakeup goroutine checks the session synchronously after gate acquisition; 100 ms is ample. This reduces the combined floor from 4 s to ~200 ms while remaining reliable.
  3. Wakeup goroutine not joined before test assertion in first testwakeup_concurrency_test.go:113

    • Issue: After close(fa.release) and userWG.Wait(), fa.maxInFlight.Load() is checked immediately. The wakeup goroutine spawned inside fireWakeup may or may not have run yet. If it runs after the assertion, its peak concurrency (1) is never captured in the test's verdict — the test always sees maxInFlight == 1 regardless of whether the fix is working, as long as the race doesn't happen to collide in the assertion window.
    • In practice the fix is correct, so the sequential run does happen and maxInFlight stays 1. But the test's integrity comes from the 2-second window (suggestion 2), not from joining the goroutine. A second sync.WaitGroup tracking the wakeup goroutine would make causality explicit.
    • Fix (low priority): expose a channel or WaitGroup from fireWakeup in test mode, or make fireWakeup return the goroutine's done-channel in tests.

What's correct ✓

  • Gate design is sound. A 1-slot make(chan struct{}, 1) semaphore is the right tool here — it's a channel, so it naturally honours context cancellation (via the select on ctx.Done()), which a sync.Mutex cannot. The acquire-on-send / release-on-receive pattern is idiomatic and leak-free (defer func() { <-a.promptGate }() fires even on error paths).
  • No deadlock risk. Only sendPrompt holds both promptGate and a.mu, always in that order. Close() holds only a.mu. No cycle.
  • Session pinning logic is correct. drop := expectSession != "" && (closed || sessionID != expectSession) — user prompts always pass "" so they're never dropped; wakeups are dropped if either the adapter closed or the session rotated while queued. pendingContext is correctly not consumed on a dropped call.
  • fireWakeup's pre-gate early-exit is a nice optimisation. It avoids spawning a goroutine when the condition is trivially stale, while correctness still comes from the re-validation inside sendPrompt.
  • Test scenarios cover both race conditions — the overlap race (two concurrent conn.Prompt()) and the session-change-while-queued drop. The fake agent using io.Pipe + acp.NewAgentSideConnection is idiomatic and avoids flaky real-subprocess timing.

Summary

Severity Count
Blocker 0
Suggestion 3

Verdict: Ready to merge. The gate logic is correct, the tests cover both key races, and the lifetimeCtx wiring means Close() cleanly aborts a queued wakeup. The suggestions above are quality-of-life improvements — none block the fix.

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

No issues found across 3 files

You’re at about 92% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Re-trigger cubic

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 31, 2026

Greptile Summary

This PR serializes session/prompt calls in the ACP adapter through a 1-slot channel semaphore (promptGate), preventing a wakeup timer from issuing a concurrent conn.Prompt() while a user prompt is in flight — the root cause of turn-misalignment where stop_reason was paired with the wrong chat turn. Wakeup prompts are session-pinned so they are silently dropped if the active session changed while they were queued, and they no longer consume pendingContext reserved for the next user prompt.

  • adapter.go: Adds promptGate chan struct{} (capacity 1) to Adapter, initialized in NewAdapter.
  • adapter_prompt.go: Splits Prompt into a public wrapper and sendPrompt, which acquires the gate via a select that respects context cancellation; a non-empty expectSession pins the call to the session it was scheduled for and skips pendingContext consumption.
  • wakeup_concurrency_test.go: Three new regression tests with a blocking fake agent covering: max-concurrency enforcement, session-change drop, and pendingContext isolation.

Confidence Score: 5/5

Safe to merge — the gate is a standard Go 1-slot channel semaphore, session-pinning logic is correct, and three focused regression tests confirm the fix holds under race conditions.

The serialization mechanism is idiomatic and the gate acquire/release lifecycle (deferred receive) is leak-free. The drop condition correctly handles all combinations of session-match and closed-adapter state for wakeup-pinned calls, while user prompts are unaffected. Previously flagged issues were already addressed in commit 95b02d9. No incorrect state transitions or data-loss paths are introduced.

No files require special attention.

Important Files Changed

Filename Overview
apps/backend/internal/agentctl/server/adapter/transport/acp/adapter.go Adds promptGate (1-slot buffered channel) to the Adapter struct as a semaphore; initialized in NewAdapter. No functional change beyond introducing the field.
apps/backend/internal/agentctl/server/adapter/transport/acp/adapter_prompt.go Refactors Prompt into a thin wrapper over sendPrompt, which acquires promptGate before proceeding. Wakeup-pinned calls pass expectSession and are dropped if the session has changed or the adapter is closed. pendingContext is intentionally not consumed for pinned wakeup calls.
apps/backend/internal/agentctl/server/adapter/transport/acp/wakeup_concurrency_test.go Three regression tests using a blocking fake agent: serialization prevents concurrent prompts, dropped wakeup when session changes while queued, and wakeup doesn't consume pendingContext. Gate release and event ordering are handled correctly.

Sequence Diagram

sequenceDiagram
    participant U as User Prompt
    participant W as Wakeup Goroutine
    participant G as promptGate (1-slot)
    participant B as ACP Bridge

    U->>G: "send struct{} (acquire)"
    activate G
    U->>B: conn.Prompt(sessionID, ...)
    Note over W: ScheduleWakeup timer fires
    W->>G: "try send struct{} (blocks — gate full)"
    Note over W: waits on gate or ctx.Done()
    B-->>U: PromptResponse + stop_reason
    U->>G: receive (release)
    deactivate G
    W->>G: "send struct{} (acquire)"
    activate G
    Note over W: re-validate: sessionID == expectSession?
    alt session unchanged
        W->>B: conn.Prompt(sessionID, ...)
        B-->>W: PromptResponse + stop_reason
    else session changed / adapter closed
        W-->>W: drop silently (return nil)
    end
    W->>G: receive (release)
    deactivate G
Loading

Reviews (3): Last reviewed commit: "test(acp): register cleanup early and jo..." | Re-trigger Greptile

Skip pendingContext consumption for wakeup-pinned prompts, tighten
concurrency test synchronization, shorten negative-assertion timeouts,
and add a regression test for pendingContext preservation.

Co-authored-by: Cursor <cursoragent@cursor.com>
@claude
Copy link
Copy Markdown

claude Bot commented May 31, 2026

Claude finished @carlosflorencio's task in 4m 30s —— View job


Code Review

  • Gather context and understand the PR
  • Read changed files
  • Review adapter.go changes
  • Review adapter_prompt.go changes
  • Review wakeup_concurrency_test.go
  • Post inline findings
  • Post summary

Summary

All findings from the prior review round have been correctly addressed in commit 95b02d98. The implementation is sound.

What's correct ✓

  • Gate design: 1-slot chan struct{} semaphore is the right primitive — it's context-aware (the select on ctx.Done() lets queued wakeups abort cleanly), whereas sync.Mutex cannot unblock on cancellation. Acquire-send / release-deferred-receive is idiomatic and leak-free.
  • pendingContext fix (greptile P1): Correctly gated on !drop && expectSession == "" — wakeup-pinned calls never consume the fork_session resume context.
  • Session pinning drop logic: drop := expectSession != "" && (closed || sessionID != expectSession) — user prompts always pass "" so they're never dropped; wakeups are dropped if the session rotated or the adapter closed while queued.
  • No lock cycle: sendPrompt always acquires promptGate then a.mu; Close() only acquires a.mu. No deadlock possible.
  • lifetimeCtx wiring: Close() cancelling the lifetime context correctly aborts queued wakeup goroutines that are blocked on gate acquisition.
  • maxInFlight assertion placement (greptile P2): Checked at line 129, before close(fa.release) — the assertion window is correctly while both the user prompt is parked and the wakeup is blocked on the gate.
  • TestWakeupDoesNotConsumePendingContext: New test directly exercises the P1 fix. The assertion on pendingContext is safe — it was read-and-preserved (or not) under the mutex before conn.Prompt() was ever called, so the value is stable by the time fa.entered fires.
  • Test comment phrasing and 100 ms timeouts: Both addressed.

Two minor observations (neither blocks the merge)

  1. TestWakeupDoesNotConsumePendingContext goroutine not joined — see inline on line 253. A drainEvents(a) call would future-proof the test against goleak, which is used in related packages.
  2. pendingContext consumed on closed-adapter user prompt — see inline on line 51. Harmless in practice; documented for future readers.
Severity Count
Blocker 0
Suggestion 2

Verdict: Ready to merge. The race fix is correct, both key concurrency scenarios are tested, and all prior feedback is resolved.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/backend/internal/agentctl/server/adapter/transport/acp/wakeup_concurrency_test.go (1)

113-127: ⚡ Quick win

Replace these real-clock timeout assertions with synctest.

The 100ms/5s time.After windows make these concurrency checks scheduler-dependent and slower than they need to be. Wrapping the tests in synctest.Test would keep the ordering assertions deterministic without relying on wall-clock delays. As per coding guidelines, "Prefer testing/synctest (Go 1.24+) over time.Sleep for time-dependent tests; use synctest.Test to wrap tests with tickers or timeouts to advance fake time instantly when all goroutines are idle."

Also applies to: 184-206, 248-252

🤖 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
`@apps/backend/internal/agentctl/server/adapter/transport/acp/wakeup_concurrency_test.go`
around lines 113 - 127, Wrap the test body with testing/synctest by replacing
real-clock waits with synctest.Test so assertions are deterministic: change the
outer test to synctest.Test(t, func(st *synctest.T) { ... }), replace time.After
usages (the selects waiting on <-time.After(5*time.Second) and
<-time.After(100*time.Millisecond) and the other occurrences at the noted
ranges) with st.After(5*time.Second) and st.After(100*time.Millisecond) or
st.Tick equivalents, and keep the same channel waits on fa.entered and the
fireWakeup call (a.fireWakeup(sid, "synthetic wakeup prompt")) unchanged; do
this for the blocks referencing fa.entered so the wakeup vs user-prompt ordering
is asserted using the synctest fake clock instead of real time.
🤖 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
`@apps/backend/internal/agentctl/server/adapter/transport/acp/wakeup_concurrency_test.go`:
- Around line 143-147: Move the t.Cleanup registration immediately after
creating the pipes and adapter to ensure resources are always released even if
early t.Fatal occurs; specifically, after you construct a, c2aW, and a2cW (the
adapter and the two pipe writers) call t.Cleanup to close them and return/stop
background goroutines (use _ = a.Close(); _ = c2aW.Close(); _ = a2cW.Close()).
Repeat the same change in the other test setup blocks that create those same
symbols (the other places where a, c2aW, a2cW are allocated) so cleanup is
registered before any potential failure paths.

---

Nitpick comments:
In
`@apps/backend/internal/agentctl/server/adapter/transport/acp/wakeup_concurrency_test.go`:
- Around line 113-127: Wrap the test body with testing/synctest by replacing
real-clock waits with synctest.Test so assertions are deterministic: change the
outer test to synctest.Test(t, func(st *synctest.T) { ... }), replace time.After
usages (the selects waiting on <-time.After(5*time.Second) and
<-time.After(100*time.Millisecond) and the other occurrences at the noted
ranges) with st.After(5*time.Second) and st.After(100*time.Millisecond) or
st.Tick equivalents, and keep the same channel waits on fa.entered and the
fireWakeup call (a.fireWakeup(sid, "synthetic wakeup prompt")) unchanged; do
this for the blocks referencing fa.entered so the wakeup vs user-prompt ordering
is asserted using the synctest fake clock instead of real time.
🪄 Autofix (Beta)

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

Run ID: d92f3207-0ca5-422b-a9de-755a169b33df

📥 Commits

Reviewing files that changed from the base of the PR and between 4dd1633 and 95b02d9.

📒 Files selected for processing (2)
  • apps/backend/internal/agentctl/server/adapter/transport/acp/adapter_prompt.go
  • apps/backend/internal/agentctl/server/adapter/transport/acp/wakeup_concurrency_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/backend/internal/agentctl/server/adapter/transport/acp/adapter_prompt.go

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

You’re at about 93% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

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

Re-trigger cubic

Extract setupConcurrencyFakeAgent so t.Cleanup runs before failure
paths, and waitForPromptComplete so fireWakeup goroutines drain
before assertions.

Co-authored-by: Cursor <cursoragent@cursor.com>
@claude
Copy link
Copy Markdown

claude Bot commented May 31, 2026

Claude finished @carlosflorencio's task in 4m 31s —— View job


Code Review

  • Gather context and understand the PR
  • Read skill file and changed files
  • Review latest commit (27d5d18)
  • Post inline findings
  • Post summary

Findings

All feedback from the two prior review rounds has been correctly addressed. The latest commit (27d5d18) resolved the remaining structural gaps:

  • setupConcurrencyFakeAgent extracts the adapter/pipe setup and registers t.Cleanup before any t.Fatalf paths — so early failures can no longer leak pipes or background goroutines (CodeRabbit/cubic finding).
  • waitForPromptComplete provides a proper join point: after close(fa.release), the test blocks on the EventTypeComplete event from sendPrompt rather than racing the wakeup goroutine's teardown (prior Claude finding on line 250).

What's correct ✓

  • Gate design: make(chan struct{}, 1) is the right primitive. Acquire-on-send, release via deferred receive — the deferred <-a.promptGate fires regardless of error path, so no gate leak is possible.
  • Context-aware acquisition: the select on ctx.Done() lets fireWakeup's goroutine abort cleanly when Close() cancels lifetimeCtx — impossible with sync.Mutex.
  • Session-pin drop logic: drop := expectSession != "" && (closed || sessionID != expectSession) is evaluated after acquiring the gate, so the session read happens-after the in-flight user prompt releases the gate. The session change in TestWakeupDroppedWhenSessionChangesWhileQueued is performed before close(fa.release), ensuring the drop is deterministically visible to the wakeup goroutine.
  • pendingContext isolation: if !drop && expectSession == "" — wakeup-pinned calls never consume the resume context. Verified by TestWakeupDoesNotConsumePendingContext.
  • isLoadingSession cleared only for non-dropped prompts: the flag-clear block is reached only after the early if drop { return nil }, so a dropped wakeup cannot prematurely clear the loading flag.
  • No lock cycle: sendPrompt always acquires promptGatea.mu; Close() acquires only a.mu. No deadlock possible.
  • Test structure: setupConcurrencyFakeAgent eliminates boilerplate duplication and guarantees cleanup ordering. The 100 ms time.After for negative assertions is consistent with other tests in this package and is appropriate for in-process io.Pipe communication.

Suggestion (doesn't block)

One minor observation on TestWakeupDoesNotConsumePendingContext: the waitForPromptComplete helper drains events from a.updatesCh until it sees EventTypeComplete. With the current simple fake agent this is always one event, but the function silently discards anything else it sees. This is fine for these tests; just worth knowing if this helper is reused in more complex scenarios with intermediate events (tool calls, message chunks). No action needed now.


Summary

Severity Count
Blocker 0
Suggestion 0

Verdict: Ready to merge. The gate logic is correct, all three concurrency scenarios are tested, cleanup is robust, and all prior feedback from three review rounds is resolved.

@carlosflorencio carlosflorencio merged commit 13e6e2d into main May 31, 2026
55 checks passed
@carlosflorencio carlosflorencio deleted the feature/chat-messages-after-eq9 branch May 31, 2026 20:12
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