fix(inbound): a throw while handling an inbound message is no longer silent - #108
Merged
Merged
Conversation
…silent handleInbound wrapped dispatchInbound in try/finally with no catch, and the call site is `void handleInbound(...)`. So anything dispatch threw -- rather than returned as a refusal -- became an unhandled rejection: no log from this file, no prompt, no decision sent to the executor, and the message already acked to the mediator so its queued copy was gone. That is indistinguishable from a request that never arrived, and it is exactly what it looked like: `[pnm inbound] received` followed by silence. Dispatching the identical message by hand from the offscreen console raised the consent window correctly, which is what localised it -- the channel, the background listener, window creation and bounds are all fine. Every deliberate refusal in dispatchInbound already logs and returns; only a thrown error could vanish, and nothing was watching for one. The candidates on the task-consent path are enrolledExecutorDids() and parseTaskConsentRequest(), both awaited before any logging. Log it. This does not stop the throw -- the logged error is what will name the actual cause -- but a consent request can no longer disappear without trace after its mediator copy has been deleted. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78
added a commit
that referenced
this pull request
Aug 7, 2026
…109) chrome.runtime.sendMessage from an offscreen document does not dependably START a terminated MV3 service worker. The send resolves nowhere, nothing is thrown, and the caller's await hangs forever. That is the remaining failure. A task-consent request arrived on the approver inbox, verified, passed both dedup gates, and was acked to the mediator -- deleting its queued copy -- and then nothing: no prompt, no error, no decision. #108 proved it was not throwing; the same message sent by hand from the offscreen console, with the worker already awake, raised the window correctly. Inspect views showed "service worker (Inactive)". chrome.runtime.connect does start the worker, and an open port keeps it alive for the connection's lifetime. That also covers the second half: requestTaskConsent awaits a human decision that can run minutes past the ~30s idle teardown, with the resolver held in the worker's memory -- so even a delivered ask could be discarded mid-decision. Both offscreen consent sites open the port before asking and disconnect in finally, so an answered, denied or failed prompt never leaves the worker pinned awake. The background accepts the port and does nothing else; accepting it is the entire purpose. The name lives in bridge-protocol.ts with the message types it belongs beside. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78
added a commit
that referenced
this pull request
Aug 7, 2026
…ndow (#110) A prompt window that must appear at an arbitrary moment is the least reliable thing this extension can attempt. It needs a live service worker, a live offscreen document, and a promise chain spanning both, on a runtime free to kill either at any time. Every failure chased in this series -- a create() that hung (#106), a swallowed throw (#108), a send that could not start a terminated worker (#109) -- was a different edge of that one design problem. A badge has none of those dependencies. It is derived from the durable record the inbound path already writes before it acks the mediator (pending.ts), so it is correct after any teardown, and it gives the user a way IN rather than depending on a window finding its way OUT. refreshPendingBadge() counts approver-bound pending records and sets the action badge. It runs on service-worker startup and when a consent port opens or closes -- every wake is a chance to be correct. Idempotent and cheap, so no single missed call can leave it lying, and a failure is swallowed with a warn: a cosmetic surface must never break a wake path. The window remains the fast path. This is the floor beneath it: the first thing in this flow that does not depend on two ephemeral contexts being alive simultaneously. First step of the larger inversion -- making the durable record the spine of the flow rather than crash recovery for it. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
handleInboundwrappeddispatchInboundintry/finallywith nocatch, and the call site isvoid handleInbound(...).So anything dispatch threw — rather than returned as a refusal — became an unhandled rejection: no log from this file, no prompt, no decision sent to the executor, and the message already acked to the mediator, so its queued copy was gone too.
How it presented
[pnm inbound] received (approver inbox) type=…/task-consent/request/0.1followed by silence. No refusal, no dedup skip, no decision sent, no error. Indistinguishable from a request that was never sent — which is how it was read for several rounds of cross-service debugging.What localised it: dispatching the identical message by hand from the offscreen console raised the consent window correctly. That proves the offscreen→background channel, the background listener,
chrome.windows.createandconsentWindowBoundsare all fine, and narrows the fault to the path between arrival andsendMessage.Every deliberate refusal in
dispatchInboundlogs and returns. Only a thrown error could vanish, and nothing was watching for one. On the task-consent path the candidates areenrolledExecutorDids()andparseTaskConsentRequest()— both awaited before any logging happens.What this does and does not do
It does not stop the throw. The logged error is what will name the actual cause, and that is the point: a consent request can no longer disappear without trace after its mediator copy has been deleted.
The
finallythat releases the pending record is unchanged, so recovery semantics are untouched.Lint clean, 223 tests pass.