Skip to content

DX-1209: inline fix-it hints on SDK ErrorInfo throw sites#2233

Open
umair-ably wants to merge 17 commits into
mainfrom
DX-1209/error-hints
Open

DX-1209: inline fix-it hints on SDK ErrorInfo throw sites#2233
umair-ably wants to merge 17 commits into
mainfrom
DX-1209/error-hints

Conversation

@umair-ably
Copy link
Copy Markdown
Contributor

@umair-ably umair-ably commented May 26, 2026

Intent

This PR extends the message-vs-hint split DX-1204 introduced (err.message says what failed; err.hint says how to fix it) to ~80 client-side ErrorInfo throw sites in the SDK. Where applicable, hints also forecast the next likely failure — typically a server-side rejection driven by channel-namespace / capability config the agent can't see from inside the SDK alone.

Why

LLMs (and humans) recover faster from SDK errors when the actionable next step is named in the error itself, rather than left to be inferred or searched. We saw two recurring failure modes when agents hit SDK errors:

  1. Spiralling — agent keeps mutating code to chase an error whose root cause is outside the code (dashboard config, capability gap, wrong execution context). Without a hint that surfaces the next wall, agents fix the client-side check, then hit the server-side rejection, then loop, often burning upwards of 40 tool calls before quitting. Quite often, an agent would also choose a non-optimal solution just so the code "works".
  2. Quitting — agent recognises the wall but has no path forward, gives up, or papers over the failure with empty catches.

DX-1209's hints are designed to convert both behaviours into "investigate, then commit to the idiomatic fix" — naming the second wall up front (so the agent doesn't spiral into it), and pointing at concrete diagnostics or replacement APIs (so the agent doesn't quit).

Outcome and Evidence

Same emulated-constraint LLM testing as DX-1204: subagents fix a failing Node test, no WebFetch / WebSearch, SDK as a black box visible only through runtime errors. Four experiments below summarise ~120 piloted runs across Opus 4.7, Sonnet 4.6, Haiku 4.5 × pre/post × n=5.

How we scored fixes

The interesting question wasn't just "did the test go green?" but "how did the agent make it green?" Every passing run was bucketed by reading the final test.js:

  • IDIOMATIC — the agent fixed the problem the way the SDK was designed to be used. For the annotations test that means channels.get(name, { modes: ['subscribe', 'annotation_subscribe'] }) plus a channel name in a namespace where Mutable Messages is enabled. The code looks like documentation.
  • HALF — the agent got the API call right (e.g. correct modes) but wrapped the result in a try/catch they don't appear to understand, or paired the fix with an unrelated workaround. Test passes; intent is partial.
  • HACKY — the agent papered over the symptom rather than fixing the cause: empty .catch(() => {}) on a real failure, channel.modes.push('annotation_subscribe') after attach to satisfy a snapshot assertion, swapping push.activate() for a private monkey-patch of Platform.Config.push. Test passes; the code that lands in the repo doesn't reflect how the SDK is meant to be used.

Experiment 1: annotation_subscribe hint with second-wall forecast

channel.annotations.subscribe() without annotation_subscribe in ChannelOptions.modes throws 93001. Pre-PR the message named the symptom only; post-PR the hint names the modes API call shape and forecasts that the channel namespace must have Mutable Messages enabled in the Ably dashboard.

The idiomatic fix is two steps: add annotation_subscribe to ChannelOptions.modes, and use a channel name in a namespace that has Mutable Messages enabled. Pre-PR agents tended to get the first step and miss the second, then either swallow the server-side rejection (HACKY) or fence-sit between the right modes call and a defensive try/catch (HALF). Post-PR, the hint primes them to expect the second wall, so they go looking for the right namespace immediately.

  • With a snapshot-only assertion (test checks that channel.modes after attach contains annotation_subscribe), IDIOMATIC rate among passing runs: pre 67% → post 83%. The HALF bucket collapses (3 → 0): no more "I added the modes and threw a catch around it just in case." Sonnet specifically shifts from HACKY (no-await + empty .catch) to IDIOMATIC (probes namespaces, finds test:, commits to the fix).
  • With end-to-end verification (test publishes a real annotation via REST and asserts the realtime listener receives it), every passing run is IDIOMATIC on both sides — the harder test rejects every hacky path, so by definition only idiomatic fixes pass. The metric that moves instead is quit rate: Haiku abandons 4/5 pre vs 1/5 post.

The annotation hint also includes a short anti-hack line — "Note: appending to channel.modes after attach() does not enable the mode server-side." That sentence exists because one Haiku run literally did channel.modes.push('annotation_subscribe') to satisfy a snapshot assertion. The hint now names that gaming pattern explicitly so future agents (and humans) don't repeat it. Same line was added to the LiveObjects hint for the same reason. This provides some evidence going forwards that in a similar vain to prompting LLMs, our hints can and should also include advice on what not to do.

Experiment 2: pointing at the Ably CLI from inside the hint

The dashboard line in the annotations hint surfaces a real wall ("this is a config issue, not a code issue") but doesn't tell an agent how to see the config. From inside a Node process the agent has no view of which channel namespaces actually have Mutable Messages enabled; without that visibility, weaker models quit, capable models burn tool calls probing namespace prefixes by trial and error.

Adding a final sentence — "If you have the Ably CLI installed, ably apps rules list will show which channel namespaces have Mutable Messages enabled" — bridges that gap. The CLI is the natural diagnostic surface for this class of question (it speaks to the same backend the SDK does, with the same credentials), and agents are willing to invoke it when the hint tells them it exists. The conditional phrasing matters:

  • CLI installed, conditional hint: Haiku abandons 7/10 → 0/10; mean tool_uses on passes 27.5 → 11.6 (-58%). Every Haiku ran ably apps rules list, found a working namespace, and moved on. No more namespace-probing spirals.
  • CLI absent, conditional hint: 5/5 Opus pass. Agents see "if you have the CLI installed", register that they don't, and fall back to namespace probing without burning time trying to invoke a missing binary. 1 Sonnet run actually installed the CLI using npx and then inspected the namespaces.
  • CLI absent, imperative hint (an earlier experiment with "Use ably apps rules list"): 3/5 Opus runs timed out trying to invoke the missing CLI. The imperative phrasing turned "the CLI helps if you have it" into "the CLI is required" — and capable agents committed hardest to that wrong path.

The conditional form is therefore shipped: it's strictly better when the CLI is absent, equivalent when present, and tells production users that the CLI is the right diagnostic for any error rooted in dashboard or capability config.

NOTE: Just this experiment was a strong signal that the CLI paired with these hints was a powerful combination in reducing the number of tool calls whilst reaching an idiomatic solution quicker. I will investigate how we can surface installing the CLI at a sooner, and more appropriate stage e.g. perhaps we log CLI install instructions when the SDK is first initialised.

Experiment 3: LiveObjects portability

I originally didn't apply a hint to object_subscribe mode missing (40024) because the error message alone seemed descriptive enough, however, there's evidence that shows we can still help weaker models here.

Idiomatic fix mirrors annotations: channels.get(name, { modes: [..., 'object_subscribe'] }). The test channel name happens to work in the default namespace, so there's no second-wall to navigate here; the hint's job is just to surface the modes API.

  • Opus / Sonnet: no measurable delta (5/5 pass either side, ~5 tool calls — the existing error message names the missing mode plainly enough that capable agents fix it from training data alone).
  • Haiku: pre 3/5 pass + 2 abandons → post 5/5 pass. Same idiomatic fix every time; the hint just makes it discoverable at the weakest tier. The second-wall forecast pattern transfers cleanly across SDK surfaces and eliminates weakest-tier quit rate without regressing capable models.

Experiment 4: telling agents to stop fighting the SDK when the API is structurally wrong for the context

Some errors signal "you're using the wrong API entirely, not a fixable mistake" — e.g. push.activate() on a Node server, where there is no device to register. The idiomatic fix is to recognise that push.activate() is the wrong API in this context and pivot to client.push.admin.publish(...) / deviceRegistrations.save(...), which are designed for server-side push management. The HACKY alternative the eval kept seeing was monkey-patching Platform.Config.push with a hand-rolled storage object so the SDK's platform check would pass — an entirely different code path that simulates being a browser inside Node.

The original hint named the alternative ("Use Push admin") but left the door open to retry; Opus runs spent 22–30 tool calls building those monkey-patches. The hint was true but too soft, so agents read it as advice instead of an instruction to stop.

Rephrasing to lead with the impossibility — "push.activate() registers this process as a push target — it cannot succeed in Node.js/server contexts. Use client.push.admin.publish(...) / deviceRegistrations.save(...) instead." — dropped Opus tool counts to mean 7.3 (range 6–9) across 3 confirmation runs, with zero monkey-patches. The transcripts shift from "puzzle to solve" to "cite the hint, accept the SDK is right, wrap the call narrowly and move on." Same pattern applied to all 6 push platform-not-supported throw sites (2 in push.ts, 4 in pushactivation.ts).

Where the hints do not help

Two scenarios where the eval showed no measurable delta — included for honest scoping:

  • Server-relayed 40160 / "Channel access denied" (constrained-key presence test): the server's own error already names the failure clearly enough that capable agents decode it without needing the client-side hint. Adding the capability forecast didn't move tool counts.
  • Token revocation under token auth (40162): the SDK message itself names the fix verbatim ("Cannot revoke tokens when using token auth" + the hint already in place names "construct a separate Ably.Rest client with ClientOptions.key"). All 12 piloted runs converged in 4–6 tool calls regardless of pre/post.

These are useful as the negative space around DX-1209's value: hints help most where the message is symptomatic-only, the fix is non-obvious, or the next failure is hidden behind a wall the agent can't see from inside the process. Where the message already encodes the fix, the hint is a no-op.


The cross-cutting story across all four experiments: DX-1209's hints shorten the loop between hitting an SDK error and committing to the idiomatic fix. Where agents previously spiralled (mutating code against a wall that needs a dashboard change) or quit (no path forward visible from inside the process), the hints now name the next wall up front, point at the diagnostic tool that can see it, and — when the call is structurally wrong for the context — tell the agent to pivot rather than keep fighting. Tool-count averages move modestly; abandon rates at the weakest tier collapse; and the code that lands in your repo is materially more likely to follow the SDK's intended design.

Summary

  • Adopts the message (what failed) / hint (how to fix) split that DX-1204 introduced for detectV1Callback and applies it to every ErrorInfo throw in the SDK where a fix-it hint adds value. The split lets agents (and humans) read the what in err.message and act on the how in err.hint without parsing prose.
  • Covers ~80 inline throw sites across 19 files — the high-traffic codes LLM agents and migrating users will trip on first: channel-state / publish-shape / presence-clientId / auth-options / push-state / message-serial / capability / pagination / plugin-missing / derived-channel / vcdiff / endpoint-vs-host.
  • Server-relayed codes are deliberately out of scope here (40140 token expired, 40160 capability, 40300 forbidden, 42910 rate-limit, etc.). Propagating hints from the server is being tracked separately.

Themes / where this sits

This is the foundation slice of LLMEvalUpdatedPlan.md A1 (TKT-6) and aligns with the team's "Theme 3 — errors guide self-healing" target for the quarter. It's defence-in-depth alongside DX-1204 (v1-callback runtime throws with a hint) and DX-1205 (legacy-import shims with a hint).

What's in scope vs deliberately not

Hint added (~80 sites):

  • realtimechannel.ts — channel options validation, publish shape (40013), max size (40009), detach-while-failed, invalidStateError (90001), release-state, attach/detach timeout (90007), untilAttach, sendUpdate missing serial (40003), sync() not-attached.
  • realtimepresence.ts — clientId missing for enter/update/leave (40012), leave-in-incompatible-state (90001), get() on suspended (91005), untilAttach, auto-re-enter failure (91004).
  • auth.ts — no auth options (40160), incompatible key (40102 ×2), authUrl/authCallback errors (40170 ×9), no-renewable-token (40171), no/invalid key (40101 ×2), empty/wildcard/non-string clientId (40012 ×3), token clientId mismatch (RSA15c), token-shape rejection.
  • baseclient.ts, defaults.ts, rest.ts, baserealtime.ts, paginatedresource.ts, utils.ts, connectionmanager.ts, basemessage.ts — see commit message for the per-file rundown.
  • push.ts, pushactivation.ts, pushchannel.ts, getW3CDeviceDetails.ts — push platform/state, registration callback, device-identity preconditions.
  • restchannel.ts, restchannelmixin.ts, restannotations.ts, realtimeannotations.ts — message-serial requirements, annotation shape / mode.

Skipped (per A1 scope):

  • Transport 80xxx connection-lifecycle codes — auto-recovered by the SDK; a hint would encourage user retry code that fights the SDK.
  • 50000 internal errors — only actionable advice is file an issue, not worth a per-site hint.
  • Server-relayed codes (40140 family, 40160, 40300, 42910, etc.) — the SDK does not construct these, the server does. Propagating server-supplied hints is separate ongoing work.

What's next

As mentioned in the above section; server-supplied hints is ongoing work. Once this is ready, we can revisit these current hints to see if we still need to forecast the next error, or if the combination of client error + server error works just as well.

I'd also like to surface the CLI install steps in the SDK initialisation journey.

Base branch

⚠️ This PR targets DX-1205/legacy-import-shims, not main. We're stacking — merge DX-1205 (#2227) first, then this branch auto-retargets to main or can be rebased.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced error messages across the SDK with actionable hints and remediation guidance for authentication, channels, presence, push notifications, and other operations.
  • Chores

    • Increased minimal bundle size thresholds and introduced automated validation to ensure error hints remain consistent and helpful across updates.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR introduces a comprehensive error-hint infrastructure: a static TypeScript AST scanner validates that error sites include hints matching a hardcoded rubric, integrates into CI via npm script, extends ErrorInfo/PartialErrorInfo constructors to accept object-form initialization with optional hint/cause/href fields, and systematically adds actionable err.hint guidance across 20+ files covering authentication, channels, presence, REST, push, plugins, and utilities.

Changes

Error Hint Coverage Infrastructure and Rollout

Layer / File(s) Summary
Hint coverage validator script and CI integration
scripts/hint-coverage.ts, .github/workflows/check.yml, package.json
Static TypeScript AST analyzer that detects ErrorInfo/PartialErrorInfo construction sites, extracts and validates hint text against a hardcoded rubric, and runs in CI after format checks to enforce hint coverage.
ErrorInfo and PartialErrorInfo constructor overloads
src/common/lib/types/errorinfo.ts
Both error classes now support overloaded constructors accepting either positional arguments (legacy) or a values object with optional hint, cause, href fields; runtime validation and proper Error subclassing via Object.setPrototypeOf.
Authentication and client-config error hints
src/common/lib/client/auth.ts, src/common/lib/client/baseclient.ts
Token-auth initialization, authorize() key mismatches, token renewal, callback timeouts, key format validation, and clientId type/value errors now include actionable err.hint about configuration options and remediation.
Realtime channel, presence, and annotation error hints
src/common/lib/client/realtimechannel.ts, src/common/lib/client/baserealtime.ts, src/common/lib/client/realtimepresence.ts, src/common/lib/client/realtimeannotations.ts
Channel option validation, publish/attach/detach/sync/history/release operations, message serial requirements, presence enter/update/leave/get operations, and annotation-subscribe checks enriched with contextual err.hint about state transitions and required configurations.
REST, message encoding, and resource error hints
src/common/lib/client/rest.ts, src/common/lib/client/restchannel.ts, src/common/lib/client/restchannelmixin.ts, src/common/lib/client/restannotations.ts, src/common/lib/types/basemessage.ts, src/common/lib/client/paginatedresource.ts
REST method validation, publish argument/batch-size validation, message payload encoding/vcdiff-decoding errors, message serial requirements, annotation validation, and paginated link errors all include err.hint describing acceptable inputs and recovery steps.
Connection manager error hints
src/common/lib/transport/connectionmanager.ts
ping() non-connected errors and authentication-provider failures now wrapped with structured ErrorInfo containing cause and branch-specific hints for 403 vs. network/endpoint errors.
Push, W3C device, and plugin error hints
src/common/lib/client/push.ts, src/plugins/push/pushactivation.ts, src/plugins/push/pushchannel.ts, src/plugins/push/getW3CDeviceDetails.ts, src/plugins/liveobjects/realtimeobject.ts
Push platform availability, device activation/registration, push-channel clientId/token validation, W3C notification permissions, service-worker configuration, missing modular plugin imports, and channel-mode validation enriched with err.hint guiding users toward required setup steps.
Utility and client-options validation error hints
src/common/lib/util/utils.ts, src/common/lib/util/defaults.ts
Host/endpoint validation, mutually exclusive option detection (endpoint/environment/restHost), derived-channel matching, missing plugin import guidance, v1-callback deprecation, and concurrent async-iterator usage errors all include contextual err.hint.
Error hint unit tests and bundle size thresholds
test/unit/error-hints.test.js, scripts/moduleReport.ts
Unit tests pin err.hint contents for specific error codes; minimal-useful Realtime bundle size thresholds increased to accommodate hint-coverage infrastructure.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • ably/ably-js#2226: Both PRs update ErrorInfo constructor behavior and error-hint annotation patterns; retrieved PR also adds v1-callback detection with err.hint guidance.

Suggested reviewers

  • AndyTWF

Poem

🐰 Hints now bloom throughout the SDK,
Where errors once were cryptic whispers small—
A validator sweeps each throw-site with care,
Guiding lost developers to solutions fair.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding actionable hint messages to SDK ErrorInfo throw sites across ~80 locations to guide developers on remediation.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch DX-1209/error-hints

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.

@github-actions github-actions Bot temporarily deployed to staging/pull/2233/features May 26, 2026 17:05 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/bundle-report May 26, 2026 17:05 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/typedoc May 26, 2026 17:05 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/features May 27, 2026 10:15 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/bundle-report May 27, 2026 10:15 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/typedoc May 27, 2026 10:15 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/bundle-report May 27, 2026 10:25 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/typedoc May 27, 2026 10:25 Inactive
Comment thread test/unit/error-hints.test.js Outdated
'use strict';

/*
* DX-1209 — hint pinning tests for inline SDK throw sites.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not bedded to the hint-coverage script and/or this test file but we need something to test the presence of hints... I'm leaning more towards the script as opposed to exhaustively testing all the hints here (although these few as a smokescreen don't hurt). Happy to hear your thoughts here @AndyTWF

@umair-ably umair-ably marked this pull request as ready for review May 27, 2026 10:32
@umair-ably umair-ably requested a review from AndyTWF May 27, 2026 10:32
umair-ably and others added 2 commits May 27, 2026 11:35
Add subpath exports for the two v1 entry points (callbacks.js and
promises.js at the package root, both removed in mid-2023 during v2
prep) so that code written against v1 — or generated by an LLM trained
on v1 docs — fails on import with an actionable migration message
instead of Node's generic ERR_PACKAGE_PATH_NOT_EXPORTED.

The .js shims throw at module load; the .d.ts siblings keep
`@arethetypeswrong/cli` happy across node10/node16/bundler resolution
modes, carry an `@deprecated` tag for IDE hovers, and resolve to `never`
so any property access errors at compile time. URL points at the
in-repo migration guide, matching the convention from DX-1204
(src/common/lib/util/utils.ts:293).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns with the DX-1204 / DX-1209 convention that ErrorInfo throws separate
*what failed* (message) from *how to fix it* (hint). The shims still throw
plain Error rather than ErrorInfo — pulling in ErrorInfo would mean
require'ing the built bundle just to throw, which defeats the point of a
top-level alias shim.

- promises.js / callbacks.js: stamp .hint on the thrown Error.
- Tests assert on err.message + err.hint separately (drift-pin against
  silent rephrasing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@umair-ably umair-ably force-pushed the DX-1205/legacy-import-shims branch from 0e65931 to 1ff768b Compare May 27, 2026 10:35
umair-ably and others added 4 commits May 27, 2026 11:36
Adopts the message-vs-hint split DX-1204 introduced (`err.message` says
*what* failed; `err.hint` says *how* to fix it) and applies it at every
ErrorInfo throw in the SDK where a fix-it hint adds value. Server-relayed
codes (40140, 40142, 40160, 40300, 42910, …) are deliberately out of
scope here — that propagation is being tracked separately.

Covered (~80 sites across 19 files):

- realtimechannel.ts — channel options validation, publish shape (40013),
  max size (40009), detach-while-failed, invalidStateError, release-state,
  attach/detach timeout, untilAttach, sendUpdate missing serial, sync
  not-attached.
- realtimepresence.ts — clientId missing for enter/update/leave,
  leave-in-incompatible-state, get() on suspended, untilAttach, auto-
  re-enter failure.
- auth.ts — no auth options, incompatible key (×2), authUrl/authCallback
  errors, no-renewable-token, no/invalid key, empty/wildcard/non-string
  clientId, token clientId mismatch, token-shape rejection.
- baseclient.ts — invalid key format, clientId type/wildcard.
- defaults.ts — endpoint/environment/host conflicts, host validation.
- rest.ts — unsupported HTTP method, revoke-tokens-under-token-auth.
- baserealtime.ts — channels.get with mode change after first call.
- paginatedresource.ts — no link to first/current page.
- utils.ts — derived-channel regex, missing-plugin, concurrent iterator.
- connectionmanager.ts — ping when not connected, authUrl/authCallback
  403/error.
- basemessage.ts — unsupported data type, vcdiff plugin missing/decode/
  unsupported.
- push.ts / pushactivation.ts / pushchannel.ts / getW3CDeviceDetails.ts —
  push platform/state, registration callback, deviceId/device-identity
  preconditions.
- restchannel.ts / restchannelmixin.ts / restannotations.ts /
  realtimeannotations.ts — message-serial requirements, annotation
  shape/mode.

Skipped per `LLMEvalUpdatedPlan.md` A1 scope:
- LiveObjects 92xxx (messages already specific).
- Transport 80xxx lifecycle codes (auto-recovered; a hint would encourage
  retry code that fights the SDK).
- 50000 internal errors (only actionable advice is file-an-issue, not
  worth a per-site hint).

Tests pin the hint strings so silent rephrasing flags as drift, modelled
on the DX-1204 v1-callback assertions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sharpens ~15 of the inline error hints DX-1209 introduced based on
~120 LLM-pilot runs (see PR description for the full evaluation).

Language: remove soft hedges (`probably`, `likely`, `potentially`,
`may trigger`) in favour of active, definitive guidance.

Second-wall forecasts: where a client-side fix opens up a server-side
rejection, the hint now names the downstream cause inline. Applied to
annotation_subscribe (realtimeannotations.ts), object_subscribe
(liveobjects/realtimeobject.ts), wildcard clientId (auth.ts,
baseclient.ts), presence.enter/update/leave (realtimepresence.ts),
invalid channel modes (realtimechannel.ts), publish wrong-shape
(realtimechannel.ts, restchannel.ts), and message annotations dashboard
config (restchannelmixin.ts, realtimechannel.ts). The pattern brings
LiveObjects 92xxx into scope — pilots showed weaker-model abandon rates
of 40% on this surface without the forecast, 0% with it.

Anti-hack notes: annotation_subscribe and object_subscribe hints now
say "appending to channel.modes after attach() does not enable the
mode server-side" so agents stop mutating the local modes array.

Ably CLI pointers: where a CLI command would close the diagnosis loop,
the hint adds a conditional `If you have the Ably CLI installed, ...`
sentence (e.g. `ably auth keys list` for capability errors,
`ably apps rules list` for channel-namespace settings). The conditional
phrasing avoids regressing agents on machines without the CLI — an
imperative phrasing caused 3/5 Opus runs to time out trying to invoke
a missing binary in a confirmation pilot.

Push platform-not-supported hints (push.ts ×2, pushactivation.ts ×4)
now lead with the structural impossibility — "push.activate() cannot
succeed in Node.js/server contexts (there is no device to register)" —
and name the specific admin APIs to use instead. This turned
22-30-tool-call monkey-patch attempts into clean 7-tool-call
acknowledgements that the SDK is right and the call is misused.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Static drift-guard for the ~90 inline err.hint sites DX-1209 added.
For every err.hint = assignment under src/, asserts the hint string is
non-empty and — if a per-code rubric exists — contains required tokens
(e.g. 93001 must contain "annotation_subscribe" and "modes"; 40024 must
contain the dynamic "expectedMode" identifier and "modes"; 40162 must
contain "revoke" and "key"; etc).

The rubric is deliberately scoped: codes shared across semantically
unrelated throw sites (40000/40012/40013/40400/40500) are left out
since a code-level rule would over-constrain. New entries are a 4-line
block per error code.

Wired into the existing Lint workflow (.github/workflows/check.yml)
alongside npm run lint / format:check so drift fails PRs cheaply.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prettier --write across the 9 hint-affected files (8 SDK source + the
new scripts/hint-coverage.ts) and bump the minimal-Realtime bundle
threshold to keep the size guard green.

- Lint: prettier formatting on the trailing CLI-line additions and
  the multi-line hint strings. No semantic change.
- Bundle: minimal-Realtime raw 107 → 116 KiB and gzip 33 → 36 KiB in
  scripts/moduleReport.ts. The hint extensions across the audit
  (capability forecasts, CLI mentions, anti-hack notes, Push admin
  pivot) added ~9 KiB raw / ~2 KiB gzipped to the minimal bundle.
  Same threshold-bump pattern fff1c1b used for the initial DX-1209
  hint additions (106→107 / 32→33).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@umair-ably umair-ably force-pushed the DX-1209/error-hints branch from e326daa to 89ec75a Compare May 27, 2026 10:37
@umair-ably umair-ably requested a review from lmars May 27, 2026 10:37
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/bundle-report May 27, 2026 10:37 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/typedoc May 27, 2026 10:38 Inactive
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: 3

🧹 Nitpick comments (9)
scripts/hint-coverage.ts (3)

111-113: 💤 Low value

Block comment stripping may mishandle strings containing /* or */.

The regex-based block comment removal can incorrectly strip content from string literals that contain /* or */ sequences (e.g., const url = "https://example.com/*path*/"). This could cause false negatives if hint assignments are within such strings or false positives if code is incorrectly stripped.

For a static lint tool, this edge case is unlikely to affect actual hint validation in this codebase, but worth noting.

🤖 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 `@scripts/hint-coverage.ts` around lines 111 - 113, The stripBlockComments
function uses a regex that will remove /*...*/ sequences even when they occur
inside string or template literals; replace this with a safe scanner or
tokenizer that walks the source and only removes block comments when not inside
single-quoted, double-quoted, or template literal strings (and should also
respect escaped quotes and backticks), or use an existing JS/TS tokenizer (e.g.,
Acorn/TypeScript scanner) to detect comment tokens; update stripBlockComments to
implement this stateful scan/token-based approach so it ignores /*...*/ inside
strings and only strips real block comments.

233-236: 💤 Low value

Consider using flatMap for cleaner collection.

Minor style suggestion: the repeated concat in a loop can be replaced with flatMap for a more idiomatic approach.

♻️ Optional refactor
-  let allEntries: HintEntry[] = [];
-  for (const f of files) {
-    allEntries = allEntries.concat(extractHints(f));
-  }
+  const allEntries: HintEntry[] = files.flatMap(extractHints);
🤖 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 `@scripts/hint-coverage.ts` around lines 233 - 236, Replace the manual
accumulation loop that builds allEntries by repeatedly concat-ing extractHints
results with a single idiomatic flatMap: call files.flatMap(f =>
extractHints(f)) and assign that to allEntries, keeping the variable name
allEntries and using the existing extractHints function so behavior remains
identical but the code is clearer and more concise.

148-155: 💤 Low value

Multiline hint assembly may fail if semicolon appears inside the string literal.

The loop termination condition checks if the line ends with ;, but if a hint string contains a semicolon near a line break (e.g., "Fix; see docs" split across lines), the loop could terminate prematurely. Additionally, the trailing semicolon removal on line 155 happens after join(' ') but before trim(), which works correctly.

Given hint strings are developer-controlled and this is an internal lint tool, the impact is low—but malformed hints would silently pass validation.

🤖 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 `@scripts/hint-coverage.ts` around lines 148 - 155, The current multiline
assembly for the RHS (rhsParts, rhs, hintMatch, lines) stops when a line ends
with ';' which can be inside a string literal; change the loop to scan until you
find a statement-terminating semicolon that is not inside a string/template
literal or escaped sequence: maintain a small state machine (inSingleQuote,
inDoubleQuote, inBacktick, escaped) while walking characters across lines (or
join progressively) and only break when you see an unescaped semicolon outside
any quote state; then strip the trailing semicolon only if it was that
terminating semicolon (i.e., not part of a string) before trim().
src/plugins/push/pushactivation.ts (1)

65-70: ⚡ Quick win

Remove unnecessary block braces.

The braces wrapping the error construction and hint assignment serve no functional purpose. This pattern appears throughout the file.

♻️ Proposed fix
-      {
-        const err = new this.rest.ErrorInfo('Push activation is not available on this platform', 40000, 400);
-        err.hint =
-          'push.activate() registers this process as a push target — it cannot succeed in Node.js/server contexts (there is no device to register). Use client.push.admin to manage other devices from a server: client.push.admin.publish(recipient, payload) to send to a device or clientId, client.push.admin.deviceRegistrations.save(device) to register a device record.';
-        throw err;
-      }
+      const err = new this.rest.ErrorInfo('Push activation is not available on this platform', 40000, 400);
+      err.hint =
+        'push.activate() registers this process as a push target — it cannot succeed in Node.js/server contexts (there is no device to register). Use client.push.admin to manage other devices from a server: client.push.admin.publish(recipient, payload) to send to a device or clientId, client.push.admin.deviceRegistrations.save(device) to register a device record.';
+      throw err;
🤖 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/plugins/push/pushactivation.ts` around lines 65 - 70, Remove the
unnecessary standalone block braces around the error construction and hint
assignment in the push activation path; locate the push.activate implementation
in pushactivation.ts where new this.rest.ErrorInfo(...) is created and the
err.hint is set, and replace the braced block with the same statements directly
(create err, set err.hint, then throw err) — apply the same removal for other
occurrences in this file where a braced-only block wraps an ErrorInfo
creation/throw.
src/common/lib/client/push.ts (2)

40-45: ⚡ Quick win

Remove unnecessary block braces.

The braces wrapping the error construction and hint assignment serve no functional purpose and add unnecessary nesting.

♻️ Proposed fix
-      {
-        const err = new ErrorInfo('This platform is not supported as a target of push notifications', 40000, 400);
-        err.hint =
-          'push.activate() registers this process as a push target — it cannot succeed in Node.js/server contexts (there is no device to register). Use client.push.admin to manage other devices from a server: client.push.admin.publish(recipient, payload) to send to a device or clientId, client.push.admin.deviceRegistrations.save(device) to register a device record.';
-        reject(err);
-      }
+      const err = new ErrorInfo('This platform is not supported as a target of push notifications', 40000, 400);
+      err.hint =
+        'push.activate() registers this process as a push target — it cannot succeed in Node.js/server contexts (there is no device to register). Use client.push.admin to manage other devices from a server: client.push.admin.publish(recipient, payload) to send to a device or clientId, client.push.admin.deviceRegistrations.save(device) to register a device record.';
+      reject(err);
🤖 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/common/lib/client/push.ts` around lines 40 - 45, The extra block braces
around the ErrorInfo construction and reject call are unnecessary; remove the
surrounding { } so the code directly constructs the ErrorInfo, sets err.hint,
and calls reject(err). Locate the snippet that creates new ErrorInfo('This
platform is not supported as a target of push notifications', 40000, 400) and
the subsequent err.hint assignment inside the push.activate() / push
registration failure branch and simplify it by eliminating the enclosing braces
so the flow is not nested needlessly.

76-81: ⚡ Quick win

Remove unnecessary block braces.

The braces wrapping the error construction and hint assignment serve no functional purpose. Same issue as lines 40-45.

♻️ Proposed fix
-      {
-        const err = new ErrorInfo('This platform is not supported as a target of push notifications', 40000, 400);
-        err.hint =
-          'push.activate() registers this process as a push target — it cannot succeed in Node.js/server contexts (there is no device to register). Use client.push.admin to manage other devices from a server: client.push.admin.publish(recipient, payload) to send to a device or clientId, client.push.admin.deviceRegistrations.save(device) to register a device record.';
-        reject(err);
-      }
+      const err = new ErrorInfo('This platform is not supported as a target of push notifications', 40000, 400);
+      err.hint =
+        'push.activate() registers this process as a push target — it cannot succeed in Node.js/server contexts (there is no device to register). Use client.push.admin to manage other devices from a server: client.push.admin.publish(recipient, payload) to send to a device or clientId, client.push.admin.deviceRegistrations.save(device) to register a device record.';
+      reject(err);
🤖 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/common/lib/client/push.ts` around lines 76 - 81, Remove the unnecessary
block braces around the error creation in the push activation failure path:
inline the ErrorInfo construction, set its hint, and call reject(err) directly
instead of wrapping these statements in an extra { ... } block; locate the code
in src/common/lib/client/push.ts inside the push activation flow (the branch
that constructs new ErrorInfo(...), assigns err.hint and calls reject(err)) and
mirror the same fix already applied for the earlier occurrence around the other
error block (lines ~40-45).
test/unit/error-hints.test.js (2)

19-28: ⚡ Quick win

Consider asserting err.hint existence before substring checks.

All five test cases check err.hint.to.contain(...) without first asserting that err.hint is defined. If err.hint is unexpectedly undefined, the test will fail with a TypeError rather than a clear assertion failure.

🛡️ Proposed fix to add hint existence checks

For each test, add an assertion before the substring checks:

        } catch (err) {
          expect(err.code).to.equal(40400);
+         expect(err.hint).to.be.a('string');
          expect(err.hint).to.contain('appId');
          expect(err.hint).to.contain('keyId');
        }

Apply the same pattern to all five test cases (lines 24-27, 35-37, 45-48, 56-59, 70-73).

Also applies to: 30-38, 40-49, 51-60, 64-75

🤖 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 `@test/unit/error-hints.test.js` around lines 19 - 28, The tests call
expect(err.hint).to.contain(...) without ensuring err.hint exists; update each
test case (e.g., the "invalid key format (40400) carries a key-format hint"
block where err is caught after new Ably.Rest(...)) to first assert that
err.hint is defined (e.g., expect(err).to.have.property('hint') or
expect(err.hint).to.exist) before the subsequent substring checks; apply the
same change to the other four test blocks that catch err so the substring
assertions never run when hint is undefined.

67-67: 💤 Low value

Private member access may be fragile across refactors.

The test accesses rest._FilteredSubscriptions, which is a private implementation detail. While acceptable for validating the missing-plugin error path, this coupling may break if the SDK renames or restructures the getter.

Consider adding a comment documenting that _FilteredSubscriptions is the stable trigger for createMissingPluginError('MessageInteractions'), or verify whether there's a public API path that triggers the same error.

🤖 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 `@test/unit/error-hints.test.js` at line 67, The test is fragile because it
accesses the private member rest._FilteredSubscriptions to trigger
createMissingPluginError('MessageInteractions'); either replace this with a
public API call that provokes the same missing-plugin error (if one exists) or,
if no public trigger is available, add a short inline comment immediately above
the rest._FilteredSubscriptions access documenting that this specific private
getter is the intentional, stable trigger for
createMissingPluginError('MessageInteractions') and why the test relies on it
(to prevent future refactors from removing the usage unintentionally).
scripts/moduleReport.ts (1)

9-9: ⚡ Quick win

Document the rationale for threshold increase.

The threshold increased by ~9 KiB raw and ~3 KiB gzipped to accommodate the hint string additions, but there's no inline comment explaining this. Future maintainers may not understand why these specific values were chosen.

📝 Proposed addition
-// The maximum size we allow for a minimal useful Realtime bundle (i.e. one that can subscribe to a channel)
+// The maximum size we allow for a minimal useful Realtime bundle (i.e. one that can subscribe to a channel).
+// Thresholds updated in DX-1209 to accommodate ~80 inline err.hint strings across SDK throw sites.
 const minimalUsefulRealtimeBundleSizeThresholdsKiB = { raw: 116, gzip: 36 };
🤖 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 `@scripts/moduleReport.ts` at line 9, Add an inline comment above the
minimalUsefulRealtimeBundleSizeThresholdsKiB constant documenting why the
thresholds were raised: note that raw increased by ~9 KiB and gzip by ~3 KiB to
accommodate recent hint string additions, reference the related PR/commit or
feature that introduced the hint strings, and include guidance on how to
recalibrate these numbers if the hint size changes (e.g., measuring bundle sizes
before/after removing hints). This comment should live next to
minimalUsefulRealtimeBundleSizeThresholdsKiB so future maintainers understand
the specific rationale and how to update the values.
🤖 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 `@src/common/lib/client/realtimeannotations.ts`:
- Line 72: Fix the typo in the inline comment referencing the attach state:
change "caes" to "case" in the comment near the attachOnSubscribe handling (the
comment that starts "// explicit check for attach state in caes
attachOnSubscribe=false") in realtimeannotations.ts so it reads "// explicit
check for attach state in case attachOnSubscribe=false".

In `@src/plugins/push/getW3CDeviceDetails.ts`:
- Around line 31-34: The ErrorInfo constructor arguments are swapped in the push
device flow: when creating the ErrorInfo for denied Notification permission in
getW3CDeviceDetails.ts you passed (message, 400, 40000) but ErrorInfo expects
(message, code, statusCode); update the call to pass the error code first and
the HTTP status second (i.e., use (message, 40000, 400)) so the
GettingPushDeviceDetailsFailed event (created and sent via machine.handleEvent)
contains the correct code and statusCode.
- Around line 40-43: The ErrorInfo constructor arguments are swapped in the new
ErrorInfo call inside getW3CDeviceDetails; replace the current arguments so they
match the expected (message, code, statusCode) order—i.e., construct ErrorInfo
with the numeric error code 40000 as the second argument and the HTTP status
code 400 as the third—then continue to set err.hint and dispatch via
machine.handleEvent(new GettingPushDeviceDetailsFailed(err)).

---

Nitpick comments:
In `@scripts/hint-coverage.ts`:
- Around line 111-113: The stripBlockComments function uses a regex that will
remove /*...*/ sequences even when they occur inside string or template
literals; replace this with a safe scanner or tokenizer that walks the source
and only removes block comments when not inside single-quoted, double-quoted, or
template literal strings (and should also respect escaped quotes and backticks),
or use an existing JS/TS tokenizer (e.g., Acorn/TypeScript scanner) to detect
comment tokens; update stripBlockComments to implement this stateful
scan/token-based approach so it ignores /*...*/ inside strings and only strips
real block comments.
- Around line 233-236: Replace the manual accumulation loop that builds
allEntries by repeatedly concat-ing extractHints results with a single idiomatic
flatMap: call files.flatMap(f => extractHints(f)) and assign that to allEntries,
keeping the variable name allEntries and using the existing extractHints
function so behavior remains identical but the code is clearer and more concise.
- Around line 148-155: The current multiline assembly for the RHS (rhsParts,
rhs, hintMatch, lines) stops when a line ends with ';' which can be inside a
string literal; change the loop to scan until you find a statement-terminating
semicolon that is not inside a string/template literal or escaped sequence:
maintain a small state machine (inSingleQuote, inDoubleQuote, inBacktick,
escaped) while walking characters across lines (or join progressively) and only
break when you see an unescaped semicolon outside any quote state; then strip
the trailing semicolon only if it was that terminating semicolon (i.e., not part
of a string) before trim().

In `@scripts/moduleReport.ts`:
- Line 9: Add an inline comment above the
minimalUsefulRealtimeBundleSizeThresholdsKiB constant documenting why the
thresholds were raised: note that raw increased by ~9 KiB and gzip by ~3 KiB to
accommodate recent hint string additions, reference the related PR/commit or
feature that introduced the hint strings, and include guidance on how to
recalibrate these numbers if the hint size changes (e.g., measuring bundle sizes
before/after removing hints). This comment should live next to
minimalUsefulRealtimeBundleSizeThresholdsKiB so future maintainers understand
the specific rationale and how to update the values.

In `@src/common/lib/client/push.ts`:
- Around line 40-45: The extra block braces around the ErrorInfo construction
and reject call are unnecessary; remove the surrounding { } so the code directly
constructs the ErrorInfo, sets err.hint, and calls reject(err). Locate the
snippet that creates new ErrorInfo('This platform is not supported as a target
of push notifications', 40000, 400) and the subsequent err.hint assignment
inside the push.activate() / push registration failure branch and simplify it by
eliminating the enclosing braces so the flow is not nested needlessly.
- Around line 76-81: Remove the unnecessary block braces around the error
creation in the push activation failure path: inline the ErrorInfo construction,
set its hint, and call reject(err) directly instead of wrapping these statements
in an extra { ... } block; locate the code in src/common/lib/client/push.ts
inside the push activation flow (the branch that constructs new ErrorInfo(...),
assigns err.hint and calls reject(err)) and mirror the same fix already applied
for the earlier occurrence around the other error block (lines ~40-45).

In `@src/plugins/push/pushactivation.ts`:
- Around line 65-70: Remove the unnecessary standalone block braces around the
error construction and hint assignment in the push activation path; locate the
push.activate implementation in pushactivation.ts where new
this.rest.ErrorInfo(...) is created and the err.hint is set, and replace the
braced block with the same statements directly (create err, set err.hint, then
throw err) — apply the same removal for other occurrences in this file where a
braced-only block wraps an ErrorInfo creation/throw.

In `@test/unit/error-hints.test.js`:
- Around line 19-28: The tests call expect(err.hint).to.contain(...) without
ensuring err.hint exists; update each test case (e.g., the "invalid key format
(40400) carries a key-format hint" block where err is caught after new
Ably.Rest(...)) to first assert that err.hint is defined (e.g.,
expect(err).to.have.property('hint') or expect(err.hint).to.exist) before the
subsequent substring checks; apply the same change to the other four test blocks
that catch err so the substring assertions never run when hint is undefined.
- Line 67: The test is fragile because it accesses the private member
rest._FilteredSubscriptions to trigger
createMissingPluginError('MessageInteractions'); either replace this with a
public API call that provokes the same missing-plugin error (if one exists) or,
if no public trigger is available, add a short inline comment immediately above
the rest._FilteredSubscriptions access documenting that this specific private
getter is the intentional, stable trigger for
createMissingPluginError('MessageInteractions') and why the test relies on it
(to prevent future refactors from removing the usage unintentionally).
🪄 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: b1d70444-e652-4027-8bca-25c66c751fa5

📥 Commits

Reviewing files that changed from the base of the PR and between 0e65931 and e326daa.

📒 Files selected for processing (25)
  • .github/workflows/check.yml
  • package.json
  • scripts/hint-coverage.ts
  • scripts/moduleReport.ts
  • src/common/lib/client/auth.ts
  • src/common/lib/client/baseclient.ts
  • src/common/lib/client/baserealtime.ts
  • src/common/lib/client/paginatedresource.ts
  • src/common/lib/client/push.ts
  • src/common/lib/client/realtimeannotations.ts
  • src/common/lib/client/realtimechannel.ts
  • src/common/lib/client/realtimepresence.ts
  • src/common/lib/client/rest.ts
  • src/common/lib/client/restannotations.ts
  • src/common/lib/client/restchannel.ts
  • src/common/lib/client/restchannelmixin.ts
  • src/common/lib/transport/connectionmanager.ts
  • src/common/lib/types/basemessage.ts
  • src/common/lib/util/defaults.ts
  • src/common/lib/util/utils.ts
  • src/plugins/liveobjects/realtimeobject.ts
  • src/plugins/push/getW3CDeviceDetails.ts
  • src/plugins/push/pushactivation.ts
  • src/plugins/push/pushchannel.ts
  • test/unit/error-hints.test.js

Comment thread src/common/lib/client/realtimeannotations.ts Outdated
Comment thread src/plugins/push/getW3CDeviceDetails.ts Outdated
Comment thread src/plugins/push/getW3CDeviceDetails.ts Outdated
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/bundle-report May 27, 2026 14:52 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/typedoc May 27, 2026 14:53 Inactive
@umair-ably umair-ably force-pushed the DX-1209/error-hints branch from 4886d8d to 89ec75a Compare May 27, 2026 14:55
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/bundle-report May 27, 2026 14:56 Inactive
umair-ably added a commit that referenced this pull request May 28, 2026
Address AndyTWF's substantive feedback on hint wording, factual
accuracy, and over-prescriptive server-rejection forecasts.

Wording (declarative tone, drop docs-style imperative):
  - auth.ts authUrl Content-Type missing/unacceptable hints: switch
    from "Have your auth endpoint return..." to "Auth endpoints may
    return..."
  - auth.ts unacceptable Content-Type message: "should be either" -> "Expected one of"
  - "mint" word replaced with "construct" / removed in auth.ts hints

Factual corrections:
  - auth.ts authUrl/token-too-large hints: acknowledge that a TokenDetails
    can legitimately be large (big capability block); soften the accusation
    that the endpoint is wrapping the token in extra JSON.
  - auth.ts authCallback timeout hint: authUrl does not invoke a callback
    and authCallback cannot return a promise; reword to differentiate the
    two paths.
  - auth.ts double-encoded token: trim the docs-style suffix off the
    message, expand the hint with the JSON-vs-JWT diagnosis. (D1)
  - connectionmanager.ts authorize 403 hint: per RSA4d the Ably server
    itself can return 403 refusing to mint a TokenDetails from a
    TokenRequest; broaden the hint beyond just authUrl/authCallback.
  - baserealtime.ts channels.get options hint: per RTS3c, channels.get
    with options is deprecated; rewrite to point at channel.setOptions()
    only.
  - realtimechannel.ts invalidStateError suspended branch: "suspended"
    recovers automatically once the connection is re-established; only
    "failed" needs an explicit attach() call.
  - realtimechannel.ts/restchannel.ts publish: drop the
    capability-forecast tail ("capability must include 'publish'");
    there are many reasons a publish can be rejected, not just caps.
  - realtimechannel.ts sync(): drop the hint entirely — sync() is an
    internal SDK method, no user/LLM action is applicable.
  - realtimepresence.ts enter/update/leave: drop the generic
    presence-capability forecast; surface the wildcard-clientId
    requirement when using enterClient/updateClient/leaveClient
    on behalf of a different identity.
  - realtimeannotations.ts and realtimeobject.ts (×2): "namespace must
    have X enabled" reads as both an obligation and a cause; reword to
    "check that the namespace has X enabled" so it's unambiguously a
    hypothesis the user verifies.
  - defaults.ts host-not-string hint: change example from
    "main.realtime.ably.net" (a host) to "main" (an endpoint name).
  - push.ts / pushactivation.ts PUSH_NOT_AVAILABLE_HINT: lead with the
    supported platforms (browsers with service-worker support) before
    naming the unsupported context (Node.js/server).
  - utils.ts derived-channel regex: replace the placeholder "regex match
    failed" message with a useful description, and swap the
    transposed code/statusCode args (40010, 400 not 400, 40010) so
    href auto-populates correctly under the new ErrorInfo overload.

No new throw sites added; no semantic behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/bundle-report May 28, 2026 11:16 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/typedoc May 28, 2026 11:16 Inactive
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: 2

🧹 Nitpick comments (1)
scripts/hint-coverage.ts (1)

244-249: 💤 Low value

Shorthand property assignments are silently skipped.

ts.isPropertyAssignment returns false for shorthand properties like { hint } (which would be ShorthandPropertyAssignment). If a hint site uses { hint } instead of { hint: hintValue }, it won't be detected.

This is likely fine since hint values are typically string literals or expressions, but worth noting for completeness.

🤖 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 `@scripts/hint-coverage.ts` around lines 244 - 249, The loop over
objExpr.properties only handles ts.isPropertyAssignment and thus silently skips
shorthand properties (e.g., `{ hint }`) which are
ts.ShorthandPropertyAssignment; update the logic in the property scan to also
detect and handle ts.isShorthandPropertyAssignment (and/or
ts.isPropertyAssignment with NamedBindings) so that shorthand names "hint" and
"code" set hintExpr and codeExpr respectively (extract the identifier
initializer or treat the shorthand identifier as the initializer), keeping
existing behavior for normal PropertyAssignment.
🤖 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 `@src/common/lib/types/basemessage.ts`:
- Around line 220-225: The ErrorInfo instances thrown for missing Vcdiff decoder
(the new throws that set message/code/statusCode/hint) are being rewrapped by
the outer error handler (the decode-failure wrapper that creates a new
ErrorInfo) which currently discards the inner ErrorInfo.hint (and cause); update
the wrapper where decode failures are caught to forward the inner error's hint
and cause when constructing the outer ErrorInfo (e.g., pass hint:
innerError.hint and cause: innerError or use Object.assign/merge) so the
original hint is preserved for callers; apply the same change to the other
similar throw sites that are rewrapped (the other ErrorInfo throws in the same
module).

In `@src/common/lib/types/errorinfo.ts`:
- Around line 64-91: The constructor for ErrorInfo should mirror
ErrorInfo.fromValues by deriving href from code when href is not provided: in
the ErrorInfo constructor (both the object branch handling messageOrValues and
the primitive-args branch), after assigning code/statusCode/detail (and after
Object.assign(values) in the object branch so we don't overwrite), if this.code
is truthy and this.href is falsy set this.href =
`https://help.ably.io/error/${this.code}`; update both branches around symbols
ErrorInfo constructor, messageOrValues handling, Object.assign(this, values),
and the primitive-args assignment to ensure consistent href behavior with
ErrorInfo.fromValues.

---

Nitpick comments:
In `@scripts/hint-coverage.ts`:
- Around line 244-249: The loop over objExpr.properties only handles
ts.isPropertyAssignment and thus silently skips shorthand properties (e.g., `{
hint }`) which are ts.ShorthandPropertyAssignment; update the logic in the
property scan to also detect and handle ts.isShorthandPropertyAssignment (and/or
ts.isPropertyAssignment with NamedBindings) so that shorthand names "hint" and
"code" set hintExpr and codeExpr respectively (extract the identifier
initializer or treat the shorthand identifier as the initializer), keeping
existing behavior for normal PropertyAssignment.
🪄 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: f2eb2a17-34d1-43b3-96ae-ad401d48f9b8

📥 Commits

Reviewing files that changed from the base of the PR and between 4886d8d and 60f4b84.

📒 Files selected for processing (24)
  • scripts/hint-coverage.ts
  • scripts/moduleReport.ts
  • src/common/lib/client/auth.ts
  • src/common/lib/client/baseclient.ts
  • src/common/lib/client/baserealtime.ts
  • src/common/lib/client/paginatedresource.ts
  • src/common/lib/client/push.ts
  • src/common/lib/client/realtimeannotations.ts
  • src/common/lib/client/realtimechannel.ts
  • src/common/lib/client/realtimepresence.ts
  • src/common/lib/client/rest.ts
  • src/common/lib/client/restannotations.ts
  • src/common/lib/client/restchannel.ts
  • src/common/lib/client/restchannelmixin.ts
  • src/common/lib/transport/connectionmanager.ts
  • src/common/lib/types/basemessage.ts
  • src/common/lib/types/errorinfo.ts
  • src/common/lib/util/defaults.ts
  • src/common/lib/util/utils.ts
  • src/plugins/liveobjects/realtimeobject.ts
  • src/plugins/push/getW3CDeviceDetails.ts
  • src/plugins/push/pushactivation.ts
  • src/plugins/push/pushchannel.ts
  • test/unit/error-hints.test.js
🚧 Files skipped from review as they are similar to previous changes (17)
  • src/plugins/push/getW3CDeviceDetails.ts
  • src/common/lib/client/restchannel.ts
  • src/common/lib/client/baserealtime.ts
  • src/common/lib/client/paginatedresource.ts
  • src/common/lib/client/rest.ts
  • src/common/lib/client/push.ts
  • src/plugins/liveobjects/realtimeobject.ts
  • src/common/lib/client/baseclient.ts
  • test/unit/error-hints.test.js
  • src/plugins/push/pushactivation.ts
  • src/common/lib/transport/connectionmanager.ts
  • src/common/lib/client/realtimechannel.ts
  • src/common/lib/client/realtimeannotations.ts
  • src/common/lib/client/realtimepresence.ts
  • src/common/lib/client/auth.ts
  • src/plugins/push/pushchannel.ts
  • src/common/lib/client/restchannelmixin.ts

Comment thread src/common/lib/types/basemessage.ts
Comment thread src/common/lib/types/errorinfo.ts
umair-ably and others added 5 commits May 28, 2026 13:01
Hint-bearing throw sites in the SDK currently follow a 3-step pattern:

  const err = new ErrorInfo('msg', 40000, 400);
  err.hint = 'how to fix...';
  throw err;

Add an options-object overload to ErrorInfo and PartialErrorInfo so
the same throw collapses to one call:

  throw new ErrorInfo({ message, code, statusCode, hint });

The overload mirrors `fromValues` semantics: validate the shape, set
the fields, Object.assign for hint/cause/href/extras, auto-populate
href from the error code if not already set. Server-decoded errors
still flow through ErrorInfo.fromValues unchanged and continue to
carry their server-provided href and extra fields (requestId,
serverId) via the same Object.assign mechanism.

Also extend IConvertibleToErrorInfo and IConvertibleToPartialErrorInfo
to declare the hint, cause, and href fields that Object.assign has
been carrying through at runtime since the interface was introduced.
This is a type-only widening — no runtime behaviour change for any
existing caller.

Addresses AndyTWF's review-feedback question on whether fromValues
should accept hint directly so the SDK throw pattern is a single call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address AndyTWF's review-feedback question on whether the SDK should
expose hint as a constructor parameter so the throw pattern is one call
instead of three. Now that ErrorInfo (and PartialErrorInfo) accept a
values-object overload, all ~85 inline hint sites move from

  const err = new ErrorInfo('msg', code, statusCode);
  err.hint = 'how to fix...';
  throw err;

to

  throw new ErrorInfo({ message, code, statusCode, hint: '...' });

(or `const err = new ErrorInfo({...}); reject(err);` where the variable
is used by a callback). One call site that already used
ErrorInfo.fromValues({...}) for its construction (realtimepresence.ts
get-when-suspended) folds the hint into the same object literal.

Also address CodeRabbit nits in the push plugin:
  - Drop the unnecessary block braces wrapping single throws/rejects in
    src/common/lib/client/push.ts (2 sites) and
    src/plugins/push/pushactivation.ts (3 sites) and
    src/common/lib/client/auth.ts (1 site) and
    src/common/lib/types/basemessage.ts (1 site).
  - Extract the four-times-repeated "push.activate() registers this
    process..." hint into module-level PUSH_NOT_AVAILABLE_HINT constants
    in pushactivation.ts and push.ts.

No wording changes in this commit; subsequent commits will refine hint
content per Andy's substantive feedback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address AndyTWF's substantive feedback on hint wording, factual
accuracy, and over-prescriptive server-rejection forecasts.

Wording (declarative tone, drop docs-style imperative):
  - auth.ts authUrl Content-Type missing/unacceptable hints: switch
    from "Have your auth endpoint return..." to "Auth endpoints may
    return..."
  - auth.ts unacceptable Content-Type message: "should be either" -> "Expected one of"
  - "mint" word replaced with "construct" / removed in auth.ts hints

Factual corrections:
  - auth.ts authUrl/token-too-large hints: acknowledge that a TokenDetails
    can legitimately be large (big capability block); soften the accusation
    that the endpoint is wrapping the token in extra JSON.
  - auth.ts authCallback timeout hint: authUrl does not invoke a callback
    and authCallback cannot return a promise; reword to differentiate the
    two paths.
  - auth.ts double-encoded token: trim the docs-style suffix off the
    message, expand the hint with the JSON-vs-JWT diagnosis. (D1)
  - connectionmanager.ts authorize 403 hint: per RSA4d the Ably server
    itself can return 403 refusing to mint a TokenDetails from a
    TokenRequest; broaden the hint beyond just authUrl/authCallback.
  - baserealtime.ts channels.get options hint: per RTS3c, channels.get
    with options is deprecated; rewrite to point at channel.setOptions()
    only.
  - realtimechannel.ts invalidStateError suspended branch: "suspended"
    recovers automatically once the connection is re-established; only
    "failed" needs an explicit attach() call.
  - realtimechannel.ts/restchannel.ts publish: drop the
    capability-forecast tail ("capability must include 'publish'");
    there are many reasons a publish can be rejected, not just caps.
  - realtimechannel.ts sync(): drop the hint entirely — sync() is an
    internal SDK method, no user/LLM action is applicable.
  - realtimepresence.ts enter/update/leave: drop the generic
    presence-capability forecast; surface the wildcard-clientId
    requirement when using enterClient/updateClient/leaveClient
    on behalf of a different identity.
  - realtimeannotations.ts and realtimeobject.ts (×2): "namespace must
    have X enabled" reads as both an obligation and a cause; reword to
    "check that the namespace has X enabled" so it's unambiguously a
    hypothesis the user verifies.
  - defaults.ts host-not-string hint: change example from
    "main.realtime.ably.net" (a host) to "main" (an endpoint name).
  - push.ts / pushactivation.ts PUSH_NOT_AVAILABLE_HINT: lead with the
    supported platforms (browsers with service-worker support) before
    naming the unsupported context (Node.js/server).
  - utils.ts derived-channel regex: replace the placeholder "regex match
    failed" message with a useful description, and swap the
    transposed code/statusCode args (40010, 400 not 400, 40010) so
    href auto-populates correctly under the new ErrorInfo overload.

No new throw sites added; no semantic behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace em-dashes (—) with hyphens (-) inside every err.hint string
  per AndyTWF's convention preference. Code-comment em-dashes elsewhere
  in the codebase are left alone.
- Fix typo: "caes" → "case" in realtimeannotations.ts attach-state
  comment (CodeRabbit).

No semantic change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the regex-based scanner with a ts.createSourceFile + AST walk.
Addresses AndyTWF's concern that the script is brittle, and CodeRabbit's
edge-case findings (block-comment stripping breaks inside string
literals; semicolon termination breaks inside string literals).

The new implementation walks `NewExpression` and `CallExpression` nodes
to recognise:
  - new ErrorInfo({ ..., hint, code, ... })
  - new PartialErrorInfo({ ... })
  - <chain>.ErrorInfo({ ... })   (e.g. this.client.ErrorInfo)
  - ErrorInfo.fromValues({ ... }) / PartialErrorInfo.fromValues({ ... })

For each match it extracts the `code:` numeric value and the `hint:`
string. Hint values can be plain string literals, template literals
(interpolations collapsed to their identifier text for substring
checks), string concatenations, or identifier references resolved
against same-file top-level const declarations (e.g.
PUSH_NOT_AVAILABLE_HINT).

The RUBRIC and per-code rules are unchanged from the regex version;
this commit only swaps the extractor.

After C3 (which migrated hint sites to the options-object form) the
old regex extractor was finding zero hints. The new walker finds 92
sites; all pass the rubric.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@umair-ably umair-ably force-pushed the DX-1209/error-hints branch from 60f4b84 to fe03b54 Compare May 28, 2026 12:07
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/bundle-report May 28, 2026 12:07 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/typedoc May 28, 2026 12:08 Inactive
@umair-ably umair-ably requested a review from AndyTWF May 28, 2026 12:08
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

🤖 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 `@src/plugins/push/pushchannel.ts`:
- Around line 118-123: The thrown ErrorInfo in pushchannel (the new
this.client.ErrorInfo call with message 'Cannot subscribe from client without
deviceIdentityToken') is used by both subscribe and unsubscribe flows (e.g.,
unsubscribeDevice), so update the hint to be action-neutral: locate the
ErrorInfo instantiation in pushchannel.ts and replace the hint text that
currently says 'before subscribing...' with wording like 'activate this device
first by awaiting client.push.activate(registerCallback) — the device must hold
an identity token before managing push subscriptions' (or similar) so it applies
to subscribe and unsubscribe paths; keep the existing message or consider
changing the message to not explicitly say "subscribe" if you want full parity.
🪄 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: aa74b92c-3195-49ce-aed2-d2f9427b6c2a

📥 Commits

Reviewing files that changed from the base of the PR and between 60f4b84 and fe03b54.

📒 Files selected for processing (24)
  • scripts/hint-coverage.ts
  • scripts/moduleReport.ts
  • src/common/lib/client/auth.ts
  • src/common/lib/client/baseclient.ts
  • src/common/lib/client/baserealtime.ts
  • src/common/lib/client/paginatedresource.ts
  • src/common/lib/client/push.ts
  • src/common/lib/client/realtimeannotations.ts
  • src/common/lib/client/realtimechannel.ts
  • src/common/lib/client/realtimepresence.ts
  • src/common/lib/client/rest.ts
  • src/common/lib/client/restannotations.ts
  • src/common/lib/client/restchannel.ts
  • src/common/lib/client/restchannelmixin.ts
  • src/common/lib/transport/connectionmanager.ts
  • src/common/lib/types/basemessage.ts
  • src/common/lib/types/errorinfo.ts
  • src/common/lib/util/defaults.ts
  • src/common/lib/util/utils.ts
  • src/plugins/liveobjects/realtimeobject.ts
  • src/plugins/push/getW3CDeviceDetails.ts
  • src/plugins/push/pushactivation.ts
  • src/plugins/push/pushchannel.ts
  • test/unit/error-hints.test.js
✅ Files skipped from review due to trivial changes (1)
  • src/plugins/push/getW3CDeviceDetails.ts
🚧 Files skipped from review as they are similar to previous changes (22)
  • src/common/lib/client/restchannel.ts
  • src/common/lib/transport/connectionmanager.ts
  • src/common/lib/client/rest.ts
  • test/unit/error-hints.test.js
  • scripts/moduleReport.ts
  • src/common/lib/client/baserealtime.ts
  • src/plugins/liveobjects/realtimeobject.ts
  • src/common/lib/client/baseclient.ts
  • src/common/lib/util/defaults.ts
  • src/common/lib/client/realtimeannotations.ts
  • src/common/lib/client/restannotations.ts
  • src/common/lib/types/basemessage.ts
  • src/common/lib/client/realtimepresence.ts
  • src/common/lib/client/push.ts
  • src/plugins/push/pushactivation.ts
  • src/common/lib/client/restchannelmixin.ts
  • src/common/lib/util/utils.ts
  • src/common/lib/client/paginatedresource.ts
  • src/common/lib/types/errorinfo.ts
  • src/common/lib/client/realtimechannel.ts
  • src/common/lib/client/auth.ts
  • scripts/hint-coverage.ts

Comment thread src/plugins/push/pushchannel.ts
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/bundle-report May 28, 2026 13:08 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/typedoc May 28, 2026 13:08 Inactive
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

Actionable comments posted: 0

@lmars
Copy link
Copy Markdown
Member

lmars commented May 28, 2026

@umair-ably I've not reviewed the diff yet, but here's a review just based on the PR description:

This PR extends the message-vs-hint split DX-1204 introduced (err.message says what failed; err.hint says how to fix it)

It would be useful to explain why we're doing a message-vs-hint split rather than expanding the error messages themselves, since DX-1204 says nothing about it (it seems to be about re-introducing deprecated callbacks, is it definitely the correct ticket?). I'm not necessarily saying we shouldn't, but we should be deliberate in why this is a new field.

For example, I haven't looked at the diff yet, but let's say this introduces ErrorInfo{message: "invalid key", hint: "The provided key must have format x.y:z"}, then why not just update the message to invalid key: the provided key must have format x.y:z?

Experiment 2: pointing at the Ably CLI from inside the hint ... "If you have the Ably CLI installed, ably apps rules list will show which channel namespaces have Mutable Messages enabled"

If they have the Ably CLI installed, I would suggest we instead consider a command like ably diagnose 93001 which internally knows to lookup the namespace and to return an appropriate response, even suggesting the command to run to create the relevant rule? This way the error message in the SDKs stays lightweight (i.e. "if you have the Ably CLI, check ably diagnose <error code>") and we centralise the diagnostic checks in the CLI. WDYT?

Experiment 3: LiveObjects portability

OOI, what exactly was this experiment? As in, what was the prompt?

Comment thread scripts/hint-coverage.ts Outdated
Comment thread scripts/hint-coverage.ts Outdated
Comment thread scripts/moduleReport.ts Outdated
message: msg,
code: 40160,
statusCode: 401,
hint: 'Pass one of ClientOptions.{ key, authUrl, authCallback, token, tokenDetails }. For production, prefer authUrl or authCallback so the API key stays on your server.',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The error message says:

"No authentication options provided; need one of: key, authUrl, or authCallback (or for testing only, token or tokenDetails)"

and then this hint says:

"Pass one of ClientOptions.{ key, authUrl, authCallback, token, tokenDetails }. For production, prefer authUrl or authCallback so the API key stays on your server."

which just seems like a re-wording of the existing message, so perhaps just update the message?

message: 'Unable to update auth options with incompatible key',
code: 40102,
statusCode: 401,
hint: 'auth.authorize() cannot change the API key - the new authOptions.key differs from the one the client was constructed with. To use a different key, construct a new Ably client.',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, this just seems like an alternative error message? I won't keep saying this, I think we should consider whether this initiative should be "improve the error message" rather than adding a new field.

@github-actions github-actions Bot temporarily deployed to staging/pull/2233/bundle-report May 29, 2026 09:55 Inactive
@github-actions github-actions Bot temporarily deployed to staging/pull/2233/typedoc May 29, 2026 09:55 Inactive
umair-ably and others added 5 commits May 29, 2026 10:58
test/unit/error-hints.test.js:
  - Add `expect(err.hint).to.be.a('string')` before each substring
    assertion so a missing/undefined hint fails with a clear "expected
    a string" rather than a TypeError on `.contain()` (CodeRabbit nit).
  - Document why `rest._FilteredSubscriptions` is the trigger for the
    missing-plugin throw: no public API exposes the throw without a
    Realtime connection, so we rely on the stable internal getter
    (CodeRabbit fragility flag).

scripts/moduleReport.ts:
  - Annotate the minimalUsefulRealtimeBundleSizeThresholdsKiB
    constant: previous {raw: 107, gzip: 33}; bumped in DX-1209 to
    absorb ~80 inline err.hint strings (~9 KiB raw, ~3 KiB gzip), with
    recalibration guidance for future hint changes (CodeRabbit nit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweep every (message, hint) pair in the SDK; six sites had a hint that
just rephrased the message (or vice versa). For each, keep the
"what failed" in the message and lean on the hint for the "how to fix".

utils.ts:
  - derived-channel regex hint dropped the rephrased "Channel names with
    derived options must look like..." (now in the message); keeps only
    the docs link.
  - concurrent next() hint dropped its trailing "Calling next() twice
    concurrently is not supported" sentence, which mirrored the message.

restchannelmixin.ts (×3 sites):
  - message trimmed to "This message lacks a serial" /
    "...and cannot be updated"; the "Make sure you have enabled X in
    channel settings on your dashboard" guidance already lives in the
    hint and is dropped from the message.

realtimechannel.ts:
  - message-lacks-serial site: same trim as restchannelmixin.
  - detach-from-failed hint dropped the "A failed channel cannot be
    detached" sentence (a rephrasing of "Unable to detach; channel
    state = failed").
  - attach/detach-timeout hints dropped the "server did not acknowledge
    within realtimeRequestTimeout" sentence (a rephrasing of "Channel
    attach/detach timed out").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The decoding loop in basemessage.ts catches per-encoding errors and
rewraps them with an outer ErrorInfo so the caller gets one error per
message rather than one per encoding stage. The wrapper used the
positional ErrorInfo constructor, which has no hint parameter, so the
three hint-bearing throws in this loop - Vcdiff plugin missing (40019),
no typed-array support (40020), Vcdiff decode failure (40018) - had
their hints silently swallowed before reaching the caller. Pre-DX-1209
this was harmless because no inner hints existed; flagged by CodeRabbit
review.

Switch the wrapper to the options-object constructor and forward
err.hint. Cause is intentionally not forwarded: three of the inner
throws are plain Error (cipher and 'Unknown encoding' branches) which
do not fit cause: ErrorInfo | PartialErrorInfo without an instanceof
branch, and CodeRabbit flagged cause as optional.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The _getDeviceIdentityToken() throw site in pushchannel.ts is reached
from both subscribeDevice() and unsubscribeDevice() (via the shared
_getPushAuthHeaders() helper), but its message and hint only mentioned
subscribing. CodeRabbit flagged that this misdirects users hitting the
error from an unsubscribe call. Rewrite both message and hint to be
action-neutral.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pinning exact hint substrings (and the static AST coverage check that
guarded token presence) treats hints like a tested API surface. We don't
test exact error-message wording elsewhere, and the same reasoning
applies to hints — they're guidance, not a contract. Remove the apparatus
rather than maintain drift assertions for it.

Removes:
  - scripts/hint-coverage.ts (+ npm run hintcoverage, CI step in check.yml)
  - test/unit/error-hints.test.js

Guidance on writing/using hints will live in a best-practices doc instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@umair-ably
Copy link
Copy Markdown
Contributor Author

umair-ably commented May 29, 2026

@umair-ably I've not reviewed the diff yet, but here's a review just based on the PR description:

This PR extends the message-vs-hint split DX-1204 introduced (err.message says what failed; err.hint says how to fix it)

It would be useful to explain why we're doing a message-vs-hint split rather than expanding the error messages themselves, since DX-1204 says nothing about it (it seems to be about re-introducing deprecated callbacks, is it definitely the correct ticket?). I'm not necessarily saying we shouldn't, but we should be deliberate in why this is a new field.

For example, I haven't looked at the diff yet, but let's say this introduces ErrorInfo{message: "invalid key", hint: "The provided key must have format x.y:z"}, then why not just update the message to invalid key: the provided key must have format x.y:z?

That PR is correct in that it first added the hint field because we needed it for that work too.

As for why we have a hint vs longer error messages... my stance is the error message is for what went wrong, the hint is for how to fix it. They serve very different purposes. In the example you picked, granted, it does seem like we can just extend the error message, but for more complex examples e.g. telling the user they need to enable mutable messages in the dashboard and/or need a specific mode enabled, I think the separation is a clear win.

We're also not the only ones going down this route. There's a similar (if not, more detailed) experiment here... fairly certain supabase also have hints in their error envelope.

Experiment 2: pointing at the Ably CLI from inside the hint ... "If you have the Ably CLI installed, ably apps rules list will show which channel namespaces have Mutable Messages enabled"

If they have the Ably CLI installed, I would suggest we instead consider a command like ably diagnose 93001 which internally knows to lookup the namespace and to return an appropriate response, even suggesting the command to run to create the relevant rule? This way the error message in the SDKs stays lightweight (i.e. "if you have the Ably CLI, check ably diagnose <error code>") and we centralise the diagnostic checks in the CLI. WDYT?

Yeah I like this! Not going to commit to this now as it's beyond the scope of this work but we should iterate towards this.

Experiment 3: LiveObjects portability

OOI, what exactly was this experiment? As in, what was the prompt?

I created a failing test which tries to subscribe to a liveobjects backed channel without the objects_subsribe mode. All the agent has to do is extend channels.get with the correct modes and the test would pass.

The prompt was to make the test pass without doing any web searches, and when it's done to write a few sentences on what it did to fix it and what it thinks helped it to fix it.

I ran this before and after adding a hint. I also thought the error message was enough but there was still an improvement for haiku with the hint instead of relying on the error message alone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants