DX-1209: inline fix-it hints on SDK ErrorInfo throw sites#2233
DX-1209: inline fix-it hints on SDK ErrorInfo throw sites#2233umair-ably wants to merge 17 commits into
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis 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 ChangesError Hint Coverage Infrastructure and Rollout
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
| 'use strict'; | ||
|
|
||
| /* | ||
| * DX-1209 — hint pinning tests for inline SDK throw sites. |
There was a problem hiding this comment.
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
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>
0e65931 to
1ff768b
Compare
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>
e326daa to
89ec75a
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (9)
scripts/hint-coverage.ts (3)
111-113: 💤 Low valueBlock 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 valueConsider using
flatMapfor cleaner collection.Minor style suggestion: the repeated
concatin a loop can be replaced withflatMapfor 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 valueMultiline 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 afterjoin(' ')but beforetrim(), 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 winRemove 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 winRemove 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 winRemove 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 winConsider asserting
err.hintexistence before substring checks.All five test cases check
err.hint.to.contain(...)without first asserting thaterr.hintis defined. Iferr.hintis unexpectedlyundefined, 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 valuePrivate 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
_FilteredSubscriptionsis the stable trigger forcreateMissingPluginError('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 winDocument 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
📒 Files selected for processing (25)
.github/workflows/check.ymlpackage.jsonscripts/hint-coverage.tsscripts/moduleReport.tssrc/common/lib/client/auth.tssrc/common/lib/client/baseclient.tssrc/common/lib/client/baserealtime.tssrc/common/lib/client/paginatedresource.tssrc/common/lib/client/push.tssrc/common/lib/client/realtimeannotations.tssrc/common/lib/client/realtimechannel.tssrc/common/lib/client/realtimepresence.tssrc/common/lib/client/rest.tssrc/common/lib/client/restannotations.tssrc/common/lib/client/restchannel.tssrc/common/lib/client/restchannelmixin.tssrc/common/lib/transport/connectionmanager.tssrc/common/lib/types/basemessage.tssrc/common/lib/util/defaults.tssrc/common/lib/util/utils.tssrc/plugins/liveobjects/realtimeobject.tssrc/plugins/push/getW3CDeviceDetails.tssrc/plugins/push/pushactivation.tssrc/plugins/push/pushchannel.tstest/unit/error-hints.test.js
4886d8d to
89ec75a
Compare
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>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
scripts/hint-coverage.ts (1)
244-249: 💤 Low valueShorthand property assignments are silently skipped.
ts.isPropertyAssignmentreturnsfalsefor shorthand properties like{ hint }(which would beShorthandPropertyAssignment). 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
📒 Files selected for processing (24)
scripts/hint-coverage.tsscripts/moduleReport.tssrc/common/lib/client/auth.tssrc/common/lib/client/baseclient.tssrc/common/lib/client/baserealtime.tssrc/common/lib/client/paginatedresource.tssrc/common/lib/client/push.tssrc/common/lib/client/realtimeannotations.tssrc/common/lib/client/realtimechannel.tssrc/common/lib/client/realtimepresence.tssrc/common/lib/client/rest.tssrc/common/lib/client/restannotations.tssrc/common/lib/client/restchannel.tssrc/common/lib/client/restchannelmixin.tssrc/common/lib/transport/connectionmanager.tssrc/common/lib/types/basemessage.tssrc/common/lib/types/errorinfo.tssrc/common/lib/util/defaults.tssrc/common/lib/util/utils.tssrc/plugins/liveobjects/realtimeobject.tssrc/plugins/push/getW3CDeviceDetails.tssrc/plugins/push/pushactivation.tssrc/plugins/push/pushchannel.tstest/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
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>
60f4b84 to
fe03b54
Compare
There was a problem hiding this comment.
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
📒 Files selected for processing (24)
scripts/hint-coverage.tsscripts/moduleReport.tssrc/common/lib/client/auth.tssrc/common/lib/client/baseclient.tssrc/common/lib/client/baserealtime.tssrc/common/lib/client/paginatedresource.tssrc/common/lib/client/push.tssrc/common/lib/client/realtimeannotations.tssrc/common/lib/client/realtimechannel.tssrc/common/lib/client/realtimepresence.tssrc/common/lib/client/rest.tssrc/common/lib/client/restannotations.tssrc/common/lib/client/restchannel.tssrc/common/lib/client/restchannelmixin.tssrc/common/lib/transport/connectionmanager.tssrc/common/lib/types/basemessage.tssrc/common/lib/types/errorinfo.tssrc/common/lib/util/defaults.tssrc/common/lib/util/utils.tssrc/plugins/liveobjects/realtimeobject.tssrc/plugins/push/getW3CDeviceDetails.tssrc/plugins/push/pushactivation.tssrc/plugins/push/pushchannel.tstest/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
|
Actionable comments posted: 0 |
|
@umair-ably I've not reviewed the diff yet, but here's a review just based on the PR description:
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
If they have the Ably CLI installed, I would suggest we instead consider a command like
OOI, what exactly was this experiment? As in, what was the prompt? |
| 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.', |
There was a problem hiding this comment.
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.', |
There was a problem hiding this comment.
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.
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>
c0be1dc to
22170a7
Compare
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.
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.
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. |
Intent
This PR extends the message-vs-hint split DX-1204 introduced (
err.messagesays what failed;err.hintsays how to fix it) to ~80 client-sideErrorInfothrow 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:
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:channels.get(name, { modes: ['subscribe', 'annotation_subscribe'] })plus a channel name in a namespace where Mutable Messages is enabled. The code looks like documentation.modes) but wrapped the result in atry/catchthey don't appear to understand, or paired the fix with an unrelated workaround. Test passes; intent is partial..catch(() => {})on a real failure,channel.modes.push('annotation_subscribe')after attach to satisfy a snapshot assertion, swappingpush.activate()for a private monkey-patch ofPlatform.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()withoutannotation_subscribeinChannelOptions.modesthrows 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_subscribetoChannelOptions.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.channel.modesafter attach containsannotation_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, findstest:, commits to the fix).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 listwill 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:ably apps rules list, found a working namespace, and moved on. No more namespace-probing spirals.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_subscribemode 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.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 thatpush.activate()is the wrong API in this context and pivot toclient.push.admin.publish(...)/deviceRegistrations.save(...), which are designed for server-side push management. The HACKY alternative the eval kept seeing was monkey-patchingPlatform.Config.pushwith 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 inpushactivation.ts).Where the hints do not help
Two scenarios where the eval showed no measurable delta — included for honest scoping:
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.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
message(what failed) /hint(how to fix) split that DX-1204 introduced fordetectV1Callbackand applies it to everyErrorInfothrow in the SDK where a fix-it hint adds value. The split lets agents (and humans) read the what inerr.messageand act on the how inerr.hintwithout parsing prose.Themes / where this sits
This is the foundation slice of
LLMEvalUpdatedPlan.mdA1 (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,sendUpdatemissing 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):
80xxxconnection-lifecycle codes — auto-recovered by the SDK; a hint would encourage user retry code that fights the SDK.50000internal errors — only actionable advice is file an issue, not worth a per-site hint.40140family,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
DX-1205/legacy-import-shims, notmain. We're stacking — merge DX-1205 (#2227) first, then this branch auto-retargets tomainor can be rebased.🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Chores