Skip to content

fix(relay): let one owner hold pty.ackData instead of relying on construction order - #15079

Merged
brennanb2025 merged 1 commit into
mainfrom
brennanb2025/sta-4571-ackdata-registration
Aug 18, 2026
Merged

brennanb2025 merged 1 commit into
mainfrom
brennanb2025/sta-4571-ackdata-registration

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Aug 17, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​22 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​21
Prod 2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​7 $\color{#cf222e}{\Huge{\mathbf{−}}}$​3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​4

ELI5

Two different places both told the relay "call me when a pty.ackData arrives." Only one of them can win, because that registration is a single slot — and the winner is simply whichever registered last. Today the real handler wins by luck of construction order. The loser is a do-nothing stub. Swap two lines in relay.ts and terminal output over SSH stops forever after 256 KB, with no error and no log.

This deletes the stub and makes registering twice an immediate, loud error instead of a silent coin flip.

What Changed

  • Deleted the no-op pty.ackData registration in PtyHandler (src/relay/pty-handler.ts). The real handler lives in SshPtyConsumerSessionAdapter and calls sourceCredit.acknowledge(...).
  • RelayDispatcher.onNotification now throws on a duplicate registration, matching the single-owner guard registerPtyDataPublicationAdmission already has (dispatcher.ts:229-241).
  • Moved the new guard test into its own dispatcher-notification-ownership.test.ts — adding it to dispatcher.test.ts pushed that file over the 800-line cap, and a max-lines disable is not acceptable.

Why

onNotification is notificationHandlers.set(method, handler) — one slot, last write wins. PtyHandler registers a no-op; SshPtyConsumerSessionAdapter registers the real credit acknowledgement. It works only because relay.ts:699-705 constructs the adapter second.

If that order ever flips, creditedEndSu never advances, reserveNextSend returns null once the 256 KB window is exhausted, and every credit-mode delivery wedges permanently. Silent, and hard to trace back.

I verified pty.ackData is the only double-registered method in the tree, so making the guard strict changes no other behavior.

Linked Issue

Fixes STA-4571

Visual Proof

N/A — no UI surface. This is relay handler registration; the observable difference is an error at construction time instead of a silently shadowed handler.

Testing

  • Full relay suite: 133 files / 1363 tests pass.
  • pnpm tc exit 0.
  • oxfmt + oxlint clean.

Both guards mutation-verified:

mutation caught by
strict duplicate guard removed dispatcher-notification-ownership.test.ts
no-op pty.ackData re-added to PtyHandler pty-handler-spawn-admission.test.ts — "registers all expected handlers"

The existing test could not have caught this. pty-handler-spawn-admission.test.ts:82 asserted only expect(notifMethods).toContain('pty.ackData') — true whether the handler is real or a shadowed stub. It now asserts PtyHandler does not own the method, which is the actual contract.

One transient failure during development (subprocess.test.ts, "uses configured grace after a detached relay has accepted a socket client") turned out to be a load-induced flake, not this change: it spawns a real subprocess against a 10s timeout while a mobile QA rig was running on the same machine. Confirmed by reverting to a pristine tree (passed), restoring the change (passed), and a clean full-suite run.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Review

Agent skill upstream boundary

  • Not applicable.

Notes

  • Backwards compatibility: no wire change, no protocol change. pty.ackData behaves exactly as it does today; only the dead registration is gone.
  • Blast radius of the strict guard: verified pty.ackData was the sole duplicate across non-test source. Any future duplicate now fails loudly at construction rather than shipping a coin flip.
  • Cross-platform / SSH: this is the SSH relay path specifically. Behavior is unchanged on the winning path; the fix removes the way it could silently lose.
  • Found while investigating STA-4510 (fix(runtime): tear down terminal subscriptions only through their owning registration #14992). Not that bug — mobile never traverses the relay pty path — and deliberately kept out of that PR.

Checklist

  • This PR is small and focused
  • I explained what changed and why (including ELI5)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

Author

  • X / Twitter: @BrennanKB5

@coderabbitai

coderabbitai Bot commented Aug 17, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 15a35e4c-f1e5-478f-9f70-a573ac2fa74d

📥 Commits

Reviewing files that changed from the base of the PR and between 8cd3383 and 0b9436a.

📒 Files selected for processing (4)
  • src/relay/dispatcher-notification-ownership.test.ts
  • src/relay/dispatcher.ts
  • src/relay/pty-handler-spawn-admission.test.ts
  • src/relay/pty-handler.ts
💤 Files with no reviewable changes (1)
  • src/relay/pty-handler.ts

Included review availability: Your plan includes up to 10 reviews per rolling hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

RelayDispatcher.onNotification now rejects duplicate registrations for the same method. A test verifies the rejection and disposal behavior. PtyHandler no longer registers the unused pty.ackData notification handler, and its registration test now expects that handler to be absent.

Merge Risk: ⚪ Minimal · up to 0b943

The change makes pty.ackData ownership explicit without changing wire behavior; no actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: enforcing single ownership of the pty.ackData notification handler.
Description check ✅ Passed The description completes the required sections, links issue STA-4571, explains the change, and documents testing and compatibility considerations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@brennanb2025
brennanb2025 force-pushed the brennanb2025/sta-4571-ackdata-registration branch from 14652af to b71ba9a Compare August 17, 2026 09:19
…truction order

PtyHandler registered a no-op pty.ackData handler and SshPtyConsumerSessionAdapter
registered the real sourceCredit.acknowledge for the same method. onNotification is a
single slot, so only the adapter's survived — and only because relay.ts constructs it
second. Reversing those two lines would have made every credit-mode delivery wedge
permanently once the 256KB window emptied, with no error and no log.

Delete the dead no-op, and make onNotification throw on a duplicate registration the way
registerPtyDataPublicationAdmission already does for the admission slot. pty.ackData was
the only double-registered method in the tree, so nothing else changes behavior.

The existing test asserted only that pty.ackData appeared in the handler map, which stays
true when the real handler is shadowed. It now asserts PtyHandler does not own the method.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/sta-4571-ackdata-registration branch from b71ba9a to 0b9436a Compare August 17, 2026 23:18
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review-loop completion (PR #15079)

Verdict: clean stop. The loop changed nothing beyond rebasing this branch onto current origin/main; the implementation and tests remain unchanged, so there was no justified code follow-up.

Verification after the rebase:

  • Full src/relay with pnpm exec vitest run --config config/vitest.config.ts src/relay: 133 test files, 1,364 tests passed.
  • pnpm tc: exit 0 (all three typecheck projects completed).
  • Mutation check 1: removing the strict duplicate-notification guard made dispatcher-notification-ownership.test.ts fail as expected (1 failed); the guard was restored.
  • Mutation check 2: re-adding the no-op pty.ackData registration in PtyHandler made pty-handler-spawn-admission.test.ts fail as expected (1 failed, 25 passed); the no-op was removed and the tree restored exactly.
  • Readiness checklist was clean at START and END: no P0/P1/P2 findings, and the duplicate non-test notification-registration scan found no duplicates.

Precedent leg: mature registries use sets/first-match behavior when multiple handlers are intentional, but APIs that define a single slot or unique provider identity reject duplicate registration loudly and return explicit ownership/disposal. For Orca's single-owner notification slot, rejecting a second registration and deleting the dead no-op in favor of the explicit owner matches that shape; silent last-write-wins would make behavior construction-order dependent.

QA: N/A. This is relay-only with no UI surface, and a screenshot cannot demonstrate notification ownership or duplicate-registration rejection; no screenshots were manufactured.

Known flake: src/relay/subprocess.test.ts — “uses configured grace after a detached relay has accepted a socket client” — spawns a real subprocess against a 10s timeout and can fail under machine load. It was confirmed as a flake by pristine-revert / restore / clean-run, not by this implementation.

@brennanb2025
brennanb2025 merged commit fe95698 into main Aug 18, 2026
45 checks passed
paidaxingyo666 pushed a commit to paidaxingyo666/Manta that referenced this pull request Aug 21, 2026
…truction order (stablyai#15079)

PtyHandler registered a no-op pty.ackData handler and SshPtyConsumerSessionAdapter
registered the real sourceCredit.acknowledge for the same method. onNotification is a
single slot, so only the adapter's survived — and only because relay.ts constructs it
second. Reversing those two lines would have made every credit-mode delivery wedge
permanently once the 256KB window emptied, with no error and no log.

Delete the dead no-op, and make onNotification throw on a duplicate registration the way
registerPtyDataPublicationAdmission already does for the admission slot. pty.ackData was
the only double-registered method in the tree, so nothing else changes behavior.

The existing test asserted only that pty.ackData appeared in the handler map, which stays
true when the real handler is shadowed. It now asserts PtyHandler does not own the method.
dallascrilley pushed a commit to dallascrilley/orca that referenced this pull request Aug 27, 2026
…truction order (stablyai#15079)

PtyHandler registered a no-op pty.ackData handler and SshPtyConsumerSessionAdapter
registered the real sourceCredit.acknowledge for the same method. onNotification is a
single slot, so only the adapter's survived — and only because relay.ts constructs it
second. Reversing those two lines would have made every credit-mode delivery wedge
permanently once the 256KB window emptied, with no error and no log.

Delete the dead no-op, and make onNotification throw on a duplicate registration the way
registerPtyDataPublicationAdmission already does for the admission slot. pty.ackData was
the only double-registered method in the tree, so nothing else changes behavior.

The existing test asserted only that pty.ackData appeared in the handler map, which stays
true when the real handler is shadowed. It now asserts PtyHandler does not own the method.
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…truction order (stablyai#15079)

PtyHandler registered a no-op pty.ackData handler and SshPtyConsumerSessionAdapter
registered the real sourceCredit.acknowledge for the same method. onNotification is a
single slot, so only the adapter's survived — and only because relay.ts constructs it
second. Reversing those two lines would have made every credit-mode delivery wedge
permanently once the 256KB window emptied, with no error and no log.

Delete the dead no-op, and make onNotification throw on a duplicate registration the way
registerPtyDataPublicationAdmission already does for the admission slot. pty.ackData was
the only double-registered method in the tree, so nothing else changes behavior.

The existing test asserted only that pty.ackData appeared in the handler map, which stays
true when the real handler is shadowed. It now asserts PtyHandler does not own the method.
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