DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path standing-notes/loomworks-scoping-attribution-is-a-property-of-the-use-v0_2.md

Loomworks — Scoping Note: Attribution Is a Property of the Use, Not of the Actor — v0.2

Version: v0.2 Date: 2026-08-21 Status: Scoping note. Not a change request. Names a design question and its two live instances; rules nothing. Markdown primary — the consumer is Claude Code and future change-request drafting. Changes from v0.1: §5.4 is answered and its answer narrows the problem rather than widening it — ActorRef has no general per-use defect, it has two specific paths with two different root causes (§2b). The second path was not known to v0.1. The delegation dispatchers are recorded at §3a as the correct pattern already present in the codebase. §4's ranking is revised — 4c gets smaller, not larger, and v0.1's reasoning for expecting the opposite is preserved at §7. v0.1 stands as sibling. Occasion: CC's post-merge trace of agent-ActorRef uses and the §5.4 findings, 2026-08-21, following CR-2026-232's merge at fbe5ce2. Reads with: change-requests/cr-2026-232-unsessioned-agent-actions-sentinel-and-baseline-v0_2 §2a and §10; change-requests/cr-2026-231-w6-writing-the-grant-into-the-record-v0_3 §2, §3, §11; agents/unsessioned_baseline.py, the "THE LIMIT OF THIS BASELINE" block. Does not touch: CR-2026-231 §6, unruled.


Plain-language summary

CR-2026-232 established that an agent reference is used for one of two reasons — to say this agent did this thing, or to say this is the agent. Only the first can be missing a grant. The sentinel marks the first; the second carries nothing.

That distinction is correct. What the change request assumed, without saying so, is that it is a property of the object — that a given agent reference is an attributing one or a naming one. It is not. It is a property of the use. And there is now one place in the engine where a single object is used both ways in the same function call, which means it carries a marker that is true of one use and false of the other.

Nothing is broken today. The sentinel says this action had no grant, and it is riding into a field that describes no action, so the record contains a statement about an act where no act occurred. It is a false statement in a field that means something — small, contained, and the same kind of falsehood this arc has spent a week eliminating elsewhere.

The follow-up check narrowed the problem instead of widening it. The worry was that this might be a general defect in the actor type — two other fields on it are per-use facts too. They are not affected, and the reason is that the code already handles them correctly: the delegation paths build their actors fresh each time and deliberately drop those fields when passing them on. Somebody thought about this and got it right.

So there is no general rot. There are two specific paths, and they fail for two different reasons. One hands an already-attributing object to a naming field in the same call, where a reader could see both. The other has a field that is correctly naming on one branch and silently receives an attributing object on the other — and nobody reading either end can see it. The second is the harder one and it is currently dormant, which is a fact about the data rather than about the code.

This note names the shape, records both instances, and lists what closing them would involve. It does not choose.


1. The shape

CR-2026-232 §2a states the category and its test:

> An agent-kind ActorRef is built for one of two reasons. Attributingthis agent did this thing. Namingthis is the agent. Only an attributing construction can be missing a grant. The discriminating question: if this reference were wrong, would the record say the wrong agent did something, or would the system use the wrong agent?

The category is right. Its subject is wrong.

The question §2a asks — would the record say the wrong agent did something — is a question about a field, not about an object. wasAttributedTo attributes. specialist_ref names. The same ActorRef handed to both is attributing in one and naming in the other, simultaneously.

So attribution is a property of the use site. The sentinel, however, lives on the object. A per-use property is being stored on a shared object, and it travels wherever the object travels.

This is the third instance of one error in this arc.

| Where | Per-X property | Stored on | Caught by | |---|---|---|---| | CR-2026-231 v0.1 §3.2 | Per-action session id | Per-process ActorRef | Gate 1 pre-flight | | CR-2026-231 v0.2 §5 | Per-identity grant event | Per-engagement Memory log | Gate 2 pre-flight | | Here | Per-use attribution marker | Shared ActorRef object | Post-merge trace |

The two questions this arc has now earned, and they are siblings:

  1. What is the lifetime of the thing I am attaching this to, and is it the same as the lifetime of the value? — CR-2026-231 §11.
  2. What is the scope of the property I am storing — the object, or one particular use of it?

The first catches a value that outlives its truth. The second catches a value that is true here and false one argument over.

2. The first instance — object reuse

agents/render_dispatch.py, in _record_external_production_dispatched.

The same specialist_actor object is passed three times in one call:

Its construction is in render_specialist_bootstrap.py, an attributing site in the render_specialists group, so the object carries session_id=UNSESSIONED_ACTION_ID.

The consequence: the sentinel — whose documented meaning is an agent attributed an act on a path where no session exists — is written into a field that describes no act. The record makes a claim about an action in a place where there is no action.

Why CR-2026-232's tests cannot see it. Both directions of CR-2026-232 §5.2 are asserted and both pass. The scan finds Call nodes; nothing is constructed at the naming use, so there is no node to classify. The test guarantees every construction is classified. This is not a construction. The baseline module says exactly this in its own limit block — it was written knowing the gap, which is why the gap was found and not stumbled into.

2b. The second instance — conditional forwarding

engagement/shaping.py::_job_attribution_ref returns triggered_by verbatim on its SkillExecutor branch, and that result is written to shaping_agent_refa naming field. A delegation-authorized Companion actor reaches the function as triggered_by via produce_specification_dispatchrequest_shape_production.

This is not the same mechanism as §2, and the difference is what matters.

| | render_dispatch.py | shaping.py::_job_attribution_ref | |---|---|---| | Mechanism | Object reuse — one attributing object handed to a naming field | Conditional forwarding — a field correctly naming on one branch, silently receiving an attributing object on the other | | Visible where? | Both uses in one call. A reader of that call could see it | Nowhere. Gated by data neither call site sees together | | Reachable by a construction scan? | No | No | | Reachable by reading either end? | Yes | No |

The second is the harder failure. The first is a mistake someone could catch by reading carefully. The second cannot be caught by reading either site, because the fault is in the relationship between them and neither end holds both halves.

Dormant, and dormancy is a property of the data. register_shaping_skill has zero call sites outside tests, so no Shaping is bound to a SkillExecutor in production today. But it is live code on the primary shape-production path, exercised by the suite, and it activates the moment anyone registers a skill — with no code change. Nothing marks that branch as load-bearing when it wakes. CC checked this rather than assuming it, which is why it is stated as dormant rather than as safe.

shaping_skill.py's triggered_by — flagged in v0.1 as unconfirmed — belongs to this same path and this same mechanism.

3. Four other uses, safe today, same blind spot

From the same trace, recorded so a future change request has the set rather than re-deriving it:

| Site | Forwards | Status | |---|---|---| | credit/proposal_applier.py | RECONCILIATION_SPECIALIST_ACTOR into shaping_agent_ref | Safe — the constant was never attributing, carries None throughout | | engagement/composition_orchestrator.py | step.specialist_ref onward | Safe — naming to naming | | engagement/shape_confirmation.py | prior.shaping_agent_ref into the next version | Safe — naming to naming | | engagement/shaping_skill.py | triggered_by into shaping_agent_ref | Unconfirmedtriggered_by may be a person or an agent depending on caller |

All four are safe by construction, not by design. They forward a value that happens never to have been attributing. The one that broke is the one where an attributing object reached a naming field, and nothing structural prevents another.

shaping_skill.py is the one to look at first in any closure work: it is not confirmed agent-kind at all, so its behaviour depends on the caller.

3a. The pattern the codebase already got right

The §5.4 check asked whether capability_ref and approval_mode — both per-use delegation facts on the same shared object — have the same shape. They do not, and the reason is structural rather than lucky.

That third point is option 4b already implemented, by someone who thought about it. It is the exact opposite shape from render_dispatch.py's specialist actor, which is built once at bootstrap and reused for every job the specialist runs — the same distinction that made W1 need a per-act model_copy derivation for session_id and makes these two sites need nothing.

The engine contains the correct pattern and the incorrect one side by side. Any closure should cite the delegation dispatchers as precedent rather than inventing a convention.

4. Options, none chosen

4a. Rule the instance benign and document it. The sentinel in specialist_ref is noise a reader can be told to ignore. Cheapest. Costs the thing this arc has been buying all week — the record saying only true things. It also leaves the general shape open, so the next instance is discovered the same way.

4b. Strip the marker at naming use sites. Pass specialist_actor.model_copy(update={"session_id": None}) into naming fields. Correct at each site, invisible in the type, and requires every future author to know the rule. It is discipline where structure is wanted.

4c. Give naming its own type. A NamedAgent — kind, id, display name — with no session, no capability_ref, no approval_mode. Attribution keeps ActorRef. Makes the category structural: a naming field cannot hold an attributing reference because it will not typecheck. Anticipated at CR-2026-232 §2a as an observation. The largest option — it touches persisted payload shapes and their read paths.

4d. Make the marker per-use at the boundary. The session travels as an argument to the write rather than on the actor; the writer composes what is recorded. Considered at CR-2026-231 §3 and rejected there because widening append_event touches 55 files. That reasoning still holds for append_event. It may not hold for a narrower boundary, and this note does not re-litigate it.

Revised by §5.4, and revised downward. v0.1 expected 4c might have to grow to contain a general defect in ActorRef. There is no general defect (§3a), so a type change is not needed to stop rot — only to make two specific paths structurally impossible.

The ranking, offered not ruled: 4b is now the proportionate answer, because §3a shows it is already the codebase's own convention at the delegation dispatchers and would be applied, not invented. 4c remains the structurally correct answer and is no longer justified by scale — only by whether the category deserves to exist in the type. 4a is what happens by default if nothing is decided, and it should be chosen deliberately if it is chosen at all.

One thing separates the two instances on cost. §2's object reuse is closed by 4b at a single call. §2b's conditional forwarding is not — 4b applied there means stripping the marker on one branch of a function whose two branches disagree about what the value is, which is a fix that has to be understood to be maintained. That is the instance that most argues for 4c.

5. What a closure would need to settle

5.1 — answered. Does the same shape exist for capability_ref and approval_mode? No — §3a. This was v0.1's §5.4 and the one most likely to change the size of the work. It did: it made it smaller.

5.2 — open. Is specialist_ref naming in every use, or attributing in some? §2a's question has been asked of it once, at one site.

5.3 — open. Under 4c, what does a persisted payload do with an existing session_id? The field is already serialized into memory_events.provenance on live rows; a type change has a read-side.

5.4 — open, and now the sharpest. Should _job_attribution_ref's two branches return the same type at all? One returns a naming reference and the other forwards an attributing one. A function whose branches disagree about what kind of thing they return is the defect, and stripping a marker at the call site treats the symptom.

5.5 — open. What marks a dormant branch as load-bearing? register_shaping_skill has no production caller today and needs none to activate §2b. Nothing in the code says when this wakes, check the attribution.

6. What this note does not do

It does not rule which option. It does not open a change request. It does not touch CR-2026-232's baseline, which is correct as merged and already documents this limit. It does not claim either instance is a defect — each is a false statement in a field, which may be judged acceptable. It does not fix the dormant path or mark it. And it does not treat shaping_skill.py's triggered_by as confirmed agent-kind; its type permits a person.

7. Correction from v0.1

Preserved rather than smoothed.

v0.1 expected §5.4 to enlarge the problem. Its words: if they have the same shape, this is not one instance with four safe neighbours — it is a property of ActorRef itself, and option 4c gets considerably larger. The check found the opposite. capability_ref and approval_mode are safe structurally, and the delegation path already implements the mitigation.

The reasoning was not wrong to worry. Three fields on one shared object, all per-use, one demonstrably leaking — expecting the other two to leak was the right thing to check. What v0.1 could not know is that a different author had already solved it in a different subsystem, which is why the note said plainly that it had not checked rather than implying it had.

And the check found something v0.1 did not know existed — §2b, a second instance with a different root cause, on the primary shape-production path. The question that was asked to bound the problem is what found the larger half of it.


DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — Scoping Note: Attribution Is a Property of the Use, Not of the Actor — v0.2 — 2026-08-21