A refused round settles — it is never re-claimed
Measured 2026-10-05 on memex.systemorph.com, filed by the thread supervisor as Plugins #2907 and 26
duplicates (every one an owner-* thread under Hosting/Triage/pull-request/<item>/_Thread/).
What it looked like
The supervisor's reports read "StartingExecution on cell (none) with no cross-hub write to the node … and no terminal state", "the thread carries no error text — nothing ran far enough to write one", and two relaunches returning to the same state. The node said nothing more.
The node was not quiet. Two reads of owner-systemorph-meshweaver-plugins-2879 4.8 s apart moved its
version by 1,208 — about 250 writes a second — while status read StartingExecution both times.
A Logs action on the control instance (Ops/Actions/logs-memex-20261005-thread-claim-loop) named
the loop in one window:
AccessControlPipeline: Access denied: user 'pr-steward' lacks Thread permission on
'Hosting/Triage/pull-request/…/owner-systemorph-meshweaver-plugins-2879'
(triggered by message=CreateNodeRequest …)
[ThreadSubmission] Response cell creation failed for 7df9521a on …
[ExecRoundWatcher] DispatchAfterClaim failed for … — rolling Status back to Idle
The supervisor could not see it: its clock counts only CROSS-hub writes (LastModified), and a claim,
a rollback and a re-claim are all own-stream writes (see the settle loop,
defect D).
Two defects, composed
1 — a creator LABEL became the round's identity
PullRequestIntake opened each owning thread with StartPreparedThread(…, createdBy: "pr-steward")
under ImpersonateAsSystem. createdBy is not decoration: it is written to MeshThread.CreatedBy,
and DispatchRound resolves the round's principal from it whenever the live context is not a real
user and the first message carries no submitter rider — which is always the case for a thread started
as system (system is never captured as a submitter). So every round of every owning thread posted its
response cell's CreateNodeRequest as pr-steward, an identity that holds no Thread permission
anywhere. Not one round of an owning thread ever ran; the only cells they hold are the supervisor's
Error cells, written as system.
Every other control-plane starter (TriageIntake, PrBabysitter, PlatformBuildInboxWatcher, the
steward's own review threads) passes no createdBy, so MeshThread.CreatedBy stays empty and the
round falls back to the NODE's creator — system-security, the identity the start already runs under.
That is what the owning thread does now. The author is still shown: authorName says "PR steward".
Rule: a thread's CreatedBy is the identity its rounds run as. Never put a label there. A label
belongs in authorName.
2 — the refusal took the transient arm
The response cell is the round's first write under its own identity. When the delivery gate refused it
(DeliveryFailure with ErrorType.Unauthorized), DispatchRound invoked onFailure, which rolls
the claim back to Idle — the arm built for faults of the moment. The input was still pending, so the
submission watcher re-claimed it on the very next emission, the same identity asked the same pipeline,
and the thread oscillated Idle ⇄ StartingExecution for as long as the hub lived.
A refusal is a verdict about the identity, not a fault of the moment: no retry can change it. So
DispatchRound now classifies it (ThreadSubmissionServer.IsRefusal — Unauthorized or Forbidden;
an UNDETERMINED check is Unavailable and stays transient; a create the handler refused in its
response with RejectionReason: ValidationFailed is the same verdict) and settles the round instead:
- the planned input's user cells and ONE Error response cell at the round's deterministic id, written as system, explicitly — the round's own identity is precisely the one that may not write here, and recording that a round could not run is infrastructure, exactly as the supervisor's settle writes its Error cell;
- the same thread write as a round found already over (
SettleAsAnsweredBy): the input ingested as answered, execution reset, the verdict — naming the refused identity — asSummary.
The thread then rests Idle with nothing pending, and the next message opens a new round. If even system cannot write the Error cell the store itself is failing, which IS transient, and the old rollback stands.
Pinned by
RefusedRoundSettlesTest (src/MeshWeaver.AI.Test), on a mesh WITH row-level security and WITHOUT
the blanket public-admin grant, and with the fixture's process-wide DevLogin host identity cleared —
it is what AccessService.CircuitContext falls back to, so with it present the round runs as an
all-granted user and the refusal cannot occur:
- the refused round — a system-created thread whose
CreatedByispr-stewardsettles Idle, nothing pending, an Error cell namingpr-steward, and the node then goes quiet for 5 s. Without theDispatchRoundchange it fails with 12,065 emissions in 30 s and 18,096Access deniedlines. - the control arm — the same thread with no label runs its round as system and the agent answers, so the refusal is the label deciding, not a fixture in which nothing can run.
Not covered
- A create the HANDLER judged and refused (
CreateNodeResponse.RejectionReason: ValidationFailed, which core maps toUnauthorizedAccessException) settles the same way, but no test reaches that shape: it needs a refusal that passes the delivery gate and is then refused by a validator. Every otherSuccess: falseanswer keeps the rollback — it may be transient. The incident's refusal was the typed delivery failure, which the test pins. - Threads opened before this change keep
CreatedBy: "pr-steward"on their node; each new message to one now settles as a named Error instead of looping, but runs nothing until that field is cleared.