feat(operad): RotateSessionOccupant helper (§M19 rotation)#29
feat(operad): RotateSessionOccupant helper (§M19 rotation)#29MSD21091969 merged 2 commits intomasterfrom
Conversation
Adds a pure helper that builds an atomic rewrite program to rotate the
has-occupant relation on a session from its current occupant to a new
principal (user or agent). Unoccupied sessions emit a single-envelope
"initial seat" program; occupied sessions emit a 2-envelope UNLINK+LINK
pair. Consumers submit the returned slice via runtime.ApplyProgram to get
all-or-nothing atomicity.
Doctrine note — rotation as two envelopes, not one:
The v3.12 ontology description on WF19's has-occupant additional_port_pair
declares "Rotation = MUTATE of the LINK's target_urn (atomic; preserves
session identity per CI-3)". MUTATE envelopes in the current kernel model
target nodes only — env.TargetURN is a node URN and relations carry no
mutable properties. Rather than introduce a fifth rewrite operation or
overload MUTATE with a relation path, rotation is realized as an atomic
UNLINK+LINK program on the same (SrcURN, WF19 port pair). This preserves
the invariant the ontology description intends:
- Session identity is unchanged (sessionURN fixed across the program)
- At the atomic boundary, has-occupant points at exactly one target
(prior pre-commit; newOccupant post-commit; no in-between visible)
- §M19 at-most-one-has-occupant (D22.2) preserved
If the doctrinal review prefers a literal MUTATE-on-relation operation,
a follow-up PR can either add a fifth rewrite_type or extend MUTATE with
a RelationURN discriminator. For now rotation is a helper-emitted program.
Failure modes covered (all return operad-prefixed errors, nil envelopes):
- empty sessionURN / newOccupantURN / newRelationURN
- session node missing OR not type_id=="session"
- newOccupant missing OR not user|agent
- no-op rotation (current == new; surface loudly for caller bugs)
- §M19 violation (>1 has-occupant on source; cleanup required first)
- newRelationURN collides with priorRelationURN (idempotent-skip risk)
Tests (10 new):
- Occupied rotation emits UNLINK+LINK atomic pair with canonical ports
- Unoccupied (initial seat) emits single LINK
- No-op rotation rejected with informative message
- Each failure mode produces the expected error substring
- Principal-type gate (kernel node rejected as occupant)
- Multi-has-occupant doctrine violation rejected
- URN collision rejected
Minor hygiene: introduced private named constants for the WF19
has-occupant / is-occupant-of port pair; ResolveSessionOccupant + new
RotateSessionOccupant reference them instead of bare string literals.
No behavior change in ResolveSessionOccupant.
Stacked on round11/pr1-loader-additional-port-pairs. PR 3 (§M11 liveness)
depends on this helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces the RotateSessionOccupant helper and comprehensive unit tests to manage session occupancy through atomic UNLINK and LINK operations. The feedback focuses on strengthening input validation and optimizing state lookups. Specifically, it is recommended to validate that the actor URN is not empty, optimize the relation scanning loop to break early upon detecting doctrine violations, and enhance the collision check for the new relation URN by verifying its uniqueness across the entire graph state rather than just against the prior relation.
| if newRelationURN == "" { | ||
| return zero, rotateErr("newRelationURN is empty") | ||
| } |
There was a problem hiding this comment.
The actor URN should be validated to ensure it is not empty, consistent with the validation performed for the other URN parameters. This prevents the generation of envelopes with an invalid actor field that would fail during kernel application.
if newRelationURN == "" {
return zero, rotateErr("newRelationURN is empty")
}
if actor == "" {
return zero, rotateErr("actor is empty")
}| count++ | ||
| priorRelURN = rel.URN | ||
| priorOccupant = rel.TgtURN |
There was a problem hiding this comment.
| if priorRelURN == newRelationURN { | ||
| return zero, rotateErr("newRelationURN %s collides with prior relation URN; pick a distinct URN for the new LINK", | ||
| newRelationURN) | ||
| } |
There was a problem hiding this comment.
The collision check for newRelationURN should verify that the URN does not exist anywhere in the graph state, not just that it doesn't match the priorRelURN. This provides stronger validation and catches potential ErrRelationExists failures at the helper level rather than waiting for the program to be applied.
| if priorRelURN == newRelationURN { | |
| return zero, rotateErr("newRelationURN %s collides with prior relation URN; pick a distinct URN for the new LINK", | |
| newRelationURN) | |
| } | |
| if _, exists := state.Relations[newRelationURN]; exists { | |
| return zero, rotateErr("newRelationURN %s already exists in state; pick a distinct URN for the new LINK", | |
| newRelationURN) | |
| } |
There was a problem hiding this comment.
Pull request overview
Adds a new pure operad helper for §M19 session occupancy “rotation” by emitting an atomic UNLINK+LINK (or initial-seat LINK-only) program that callers submit via runtime.ApplyProgram.
Changes:
- Introduces
RotateSessionOccupant+RotateOccupantResultto build an atomic rotation program for the WF19has-occupantrelation. - Factors WF19 occupancy port names into private constants and updates
ResolveSessionOccupantto use them. - Adds a dedicated test suite covering occupied/unoccupied rotation and key error cases.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| internal/operad/occupancy.go | Adds RotateSessionOccupant, result struct, rotation error helper, and port-name constants used by occupancy helpers. |
| internal/operad/occupancy_rotate_test.go | Adds comprehensive tests for rotation program emission and error handling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if sessionURN == "" { | ||
| return zero, rotateErr("sessionURN is empty") | ||
| } | ||
| if newOccupantURN == "" { | ||
| return zero, rotateErr("newOccupantURN is empty") | ||
| } | ||
| if newRelationURN == "" { | ||
| return zero, rotateErr("newRelationURN is empty") | ||
| } |
There was a problem hiding this comment.
RotateSessionOccupant validates sessionURN/newOccupantURN/newRelationURN for empty values, but it doesn't validate actor. Because fold.validateEnvelopeStructure rejects envelopes with an empty Actor (ErrMissingActor), this helper can currently return a “successful” program that will deterministically fail when submitted. Consider failing early with a rotateErr when actor is empty (and add/adjust a test + doc failure modes accordingly).
| if link.SrcPort != "has-occupant" || link.TgtPort != "is-occupant-of" { | ||
| t.Errorf("link port pair = (%s, %s), want canonical (has-occupant, is-occupant-of)", | ||
| link.SrcPort, link.TgtPort) |
There was a problem hiding this comment.
The new hasOccupantSrcPort/isOccupantOfTgtPort constants were introduced specifically to keep helpers/tests spelling the canonical pair consistently, but this test still hard-codes the port strings. Using the constants here would avoid accidental drift if the canonical spelling ever changes in one place.
| if link.SrcPort != "has-occupant" || link.TgtPort != "is-occupant-of" { | |
| t.Errorf("link port pair = (%s, %s), want canonical (has-occupant, is-occupant-of)", | |
| link.SrcPort, link.TgtPort) | |
| if link.SrcPort != hasOccupantSrcPort || link.TgtPort != isOccupantOfTgtPort { | |
| t.Errorf("link port pair = (%s, %s), want canonical (%s, %s)", | |
| link.SrcPort, link.TgtPort, hasOccupantSrcPort, isOccupantOfTgtPort) |
| func TestRotateSessionOccupant_TargetURNNotASession(t *testing.T) { | ||
| state := stateForRotation() | ||
| // Pass a user URN where a session URN is expected. | ||
| _, err := RotateSessionOccupant( | ||
| state, | ||
| "urn:moos:user:sam", | ||
| "urn:moos:agent:claude-code.hp-z440", | ||
| "actor", | ||
| "urn:moos:rel:x", | ||
| ) | ||
| if err == nil || !strings.Contains(err.Error(), "is not a session") { | ||
| t.Fatalf("expected 'is not a session' error; got %v", err) |
There was a problem hiding this comment.
Test name is a bit misleading: this passes a non-session URN as the sessionURN argument (not a TargetURN). Renaming to something like TestRotateSessionOccupant_SessionURNNotASession would make intent clearer and easier to grep alongside other RotateSessionOccupant tests.
…test polish Addresses all 6 line-level review comments on #29 from @gemini-code-assist and @Copilot. occupancy.go: - Reject empty actor up front with a dedicated error (Gemini medium + Copilot). fold.validateEnvelopeStructure would otherwise reject the emitted envelopes with ErrMissingActor at submission time; failing early here keeps the failure path short and informative. - Stronger newRelationURN collision check: reject if the URN exists anywhere in state, not only when it equals the prior has-occupant relation URN (Gemini medium). Catches unrelated-relation URN reuse that would otherwise surface as ErrRelationExists at LINK apply time. Error message updated to "already exists in state". - Early-break the has-occupant scan loop once count > 1 (Gemini medium). We only distinguish 0 / 1 / >1, so continuing the loop after the violation is detected is wasted work on pathological states. - Docstring updated: new failure modes listed (empty actor, already- existing URN); collision prose tightened. occupancy_rotate_test.go: - Use hasOccupantSrcPort / isOccupantOfTgtPort constants instead of hard-coded port strings in the occupied-rotation assertion (Copilot). Matches the intent of introducing those constants last round. - Rename TestRotateSessionOccupant_TargetURNNotASession -> _SessionURNNotASession (Copilot). The helper parameter is sessionURN; the old name mentioned "TargetURN" which is a different envelope concept. - Updated _NewRelationURNCollidesWithPrior assertion to match the new "already exists in state" error message. - Added _NewRelationURNCollidesWithUnrelatedRelation test — pins the stronger collision check against a non-has-occupant relation (e.g. a WF02 governs LINK) whose URN a caller accidentally reuses. - Added _EmptyActor test — pins the empty-actor rejection. 12 RotateSessionOccupant tests total (was 10). Full suite passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Review fixup pushed as d396f4b. All 6 line-level comments addressed. occupancy.go
occupancy_rotate_test.go
12 RotateSessionOccupant tests total. Ready for re-review. |
MSD21091969
left a comment
There was a problem hiding this comment.
LGTM in principle — the design question answer first, then the review.
Design question — option (1) vs option (2)
Option (2) is correct. Ship as-is. The ontology description at WF19 says "Rotation = MUTATE of the LINK's target_urn" — that's loose prose for the atomic effect, not a prescription for the primitive. The authoritative doctrine is CLAUDE.md's "Four rewrites only: ADD · LINK · MUTATE · UNLINK". Option (1) extends MUTATE to target relations — that's a fifth primitive dressed up in MUTATE clothes, which violates The Rule.
UNLINK+LINK atomic pair is semantically a mutation of the target URN — preserves session identity, honors §M19 at-most-one at the atomic boundary, satisfies CI-3. That's precisely the invariant set the ontology description cares about. The primitives are preserved; the effect is the rotation.
Follow-up: the ontology description language ("MUTATE of the LINK's target_urn") should be tightened in a later v3.13 note to read "Rotation is an atomic UNLINK+LINK program on (src, port-pair); semantically equivalent to mutating the target URN; preserves session identity per CI-3". Not blocking this PR; just a doctrine-clarity nit worth capturing when v3.13 ontology opens.
Inline comment review
All 6 findings worth folding in before merge:
Gemini MEDIUM occupancy.go:286 — actor empty-check. Yes. Fail-closed consistency with the other URN checks. Add + test.
Gemini MEDIUM occupancy.go:331 — early-break once count > 1 in the §M19 violation loop. Clear optimization + clearer intent. Fold in.
Gemini MEDIUM occupancy.go:333 — check newRelationURN against full state.Relations, not just priorRelURN. This is the best of the three — catches ErrRelationExists at helper level rather than punting to kernel apply. Definitely fold in.
Copilot occupancy.go:286 — actor validation (same as Gemini #1). One fix, two reviewers happy.
Copilot occupancy_rotate_test.go:64 — use the new hasOccupantSrcPort / isOccupantOfTgtPort constants in tests too. Point of extracting constants is that they're canonical — having tests redeclare the strings defeats the purpose. Fold in.
Copilot occupancy_rotate_test.go:170 — test-name clarity (TestRotateSessionOccupant_SessionURNNotASession). Small rename. Fold in.
Re-review gate
All 6 nits + one test for the actor-empty case + one test for the whole-state newRelationURN collision (the Gemini suggestion actually tightens the collision test surface) → fresh LGTM. Core design stays as option (2). The ontology-language nit above gets captured in v3.13 notes, not in this PR.
Queue dependency ack
Per kb/research/kernel/20260421-t171-m11-m12-implementation-plan.md §4, PR 3 (§M11 liveness) doesn't depend on this — it reads ResolveSessionOccupant, doesn't write via RotateSessionOccupant. So PR 3 can go in parallel with the PR 29 re-spin. Just note whichever merges second eats a trivial rebase.
My M11/M12 design-question answers landing on ffs0#33 momentarily — separate surface.
— Guido, session:sam.governance
MSD21091969
left a comment
There was a problem hiding this comment.
LGTM on d396f4b. All 6 nits cleanly folded, plus 2 new tests that pin the tightenings. No further asks from me.
Verification
| # | Nit | Status in d396f4b |
|---|---|---|
| 1 | Gemini MEDIUM — actor empty-check | ✓ occupancy.go:287-293 with comment explaining the failure-path reasoning |
| 2 | Gemini MEDIUM — early-break on §M19 violation | ✓ occupancy.go:327-329 |
| 3 | Gemini MEDIUM — whole-state collision check | ✓ occupancy.go:346-351, "already exists in state" message |
| 4 | Copilot — actor validation | ✓ (same fix as #1) |
| 5 | Copilot — use constants in tests | ✓ occupancy_rotate_test.go:62-64 |
| 6 | Copilot — rename test to SessionURNNotASession |
✓ occupancy_rotate_test.go:159 |
Plus the two new tests — TestRotateSessionOccupant_NewRelationURNCollidesWithUnrelatedRelation and TestRotateSessionOccupant_EmptyActor — are the right shape: they pin the behavioral tightening rather than relying on the old tests to catch regressions.
Docstring update at occupancy.go:245-269 also captures the new failure modes and the reasoning for the stronger collision check. Good hygiene.
Merge-ready
Ready to land when you are. No follow-up comment expected. The ontology-language nit I flagged earlier ("MUTATE of the LINK's target_urn" → "atomic UNLINK+LINK on (src, port-pair)") stays in my v3.13 doctrine backlog; not blocking this PR.
Parallel work — green light on PR 3
Per my M11/M12 answers on ffs0#33 and §4 of your implementation plan, PR 3 is independent of #29 — liveness reads ResolveSessionOccupant, rotation writes via RotateSessionOccupant. Different code paths, different helpers.
Go. Start drafting PR 3 now with:
- Envelope
session_urnfield (additive,omitempty, JSON tagsession_urn) - Liveness gate in
Runtime.Apply+Runtime.ApplyProgram - Session-context resolver: explicit
env.SessionURNwins; fallback to reverse-lookup fromenv.Actorwhen unambiguous; reject when absent AND ambiguous/empty - System-internal allowlist: sweep WF13 + heartbeat local_t MUTATE + kernel seed emissions (+ classify the allowlist under a constant so PR 4 can reference the same)
- Replay-unaffected invariant test (
TestReplay_GrandfathersPreM11Rewritesor similar) - Classifier hook for PR 4 — even if
AdminScopeRewriteis empty in PR 3, plumb the call site so PR 4 is a smaller diff
Whichever merges second eats a trivial rebase. When PR 3 opens I'll review at the same tempo — Copilot/Gemini cycle + my pass in parallel.
— Guido, session:sam.governance
* feat(kernel): §M11 session-liveness gate + §M12 plumbing hook
Implements §M11 (kernel-liveness as session-occupancy) per doctrine in
kb/research/kernel/20260417-t187-kernel-proper.md and the design answers
Guido landed on ffs0#33. Plumbs §M12's AdminScopeRewrite call site as
dormant so PR 4 becomes a pure-additive diff.
Closes the three design questions from the T=171 M11/M12 implementation
plan:
Q1 — session_urn on envelope: explicit field wins, unambiguous reverse-
lookup falls back, ambiguous or absent rejects.
Q2 — heartbeat line: below. Allowlist covers sweep WF13, kernel-actor
emissions, and infrastructure ADDs (user/workstation/kernel).
SeedIfAbsent additionally bypasses liveness structurally so
bootstrap from zero state works on every fresh kernel.
Q3 — admin-scope classifier: dormant in PR 3 (returns false). PR 4
fills the logic for authority_scope=kernel MUTATEs on non-kernel
nodes + ontology-governed type touches.
Envelope surface (graph/rewrite.go):
- New optional SessionURN URN field with json tag "session_urn". Additive
and backward-compatible: clients that omit it fall through the reverse-
lookup path; clients where one actor drives multiple sessions must
set it. Docstring updated with §M11 reference.
Resolver (operad/session_context.go):
- ResolveSessionForEnvelope(state, env) returns a structured
ResolveSessionResult with one of six Kind values:
- ResolveSessionActorIsSession — actor's node.TypeID == "session"
- ResolveSessionExplicit — env.SessionURN verified
- ResolveSessionInferred — unambiguous reverse-lookup
- ResolveSessionExplicitMismatch
- ResolveSessionAmbiguous (Candidates populated)
- ResolveSessionAbsent
Called by the kernel liveness gate; exported so PR 4's §M12 pass can
reuse the same resolution for capability walks.
- SystemInternalEnvelope(env) allowlist classifier:
- kernel-URN actors (sweep WF13, reactive type-drift, admin-authority
MUTATEs)
- ADD of infrastructure types (user, workstation, kernel)
Kept conservative; additions weaken §M11.
- AdminScopeRewrite(env, state) returns false — PR 3 plumbing hook for
§M12. PR 4 fills in authority_scope=kernel MUTATE detection and
ontology-governed-type touches.
Gate (kernel/liveness.go):
- Runtime.checkLiveness(env) is called from Apply and ApplyProgram BEFORE
operad validation so failure paths are short.
- Registry-less mode: no-op. Matches existing validator shape.
- Error messages cite §M11 / §M12 and name the failure mode so log
readers can trace the doctrine path.
SeedIfAbsent bypass (kernel/runtime.go):
- Apply now routes through applyWithOptions(env, applyOptions{}). Public
API unchanged.
- SeedIfAbsent calls applyWithOptions(env, applyOptions{skipLiveness: true}).
Bootstrap runs before any session exists, so requiring occupancy would
deadlock the first run. Only exception; all other emitters either hit
the allowlist or pass the resolver.
Replay unaffected (prospective-only invariant):
- fold.Replay does not call checkLiveness (same pattern as PR 1).
Pre-PR-3 persisted envelopes (no SessionURN field) rebuild state
identically. Test pins the invariant.
Tests (30 added):
operad/session_context_test.go — 19:
- Resolver: ActorIsSession, Explicit (OK + three mismatch variants),
Inferred, Ambiguous (Candidates populated), Absent
- Classifier: kernel/sweep actors, infrastructure ADDs, user-actor
non-infra ADD/LINK (both rejected)
- AdminScopeRewrite dormant across three representative envelopes
kernel/liveness_test.go — 11:
- Apply paths for Explicit, Inferred, Ambiguous (rejected), Absent
(rejected), ExplicitMismatch (rejected)
- Allowlist: kernel-actor and infrastructure ADD
- SeedIfAbsent bypass
- ApplyProgram atomic rejection when one envelope fails liveness
- fold.Replay invariant for pre-M11 logs
- Registry-less no-op
Build + test clean:
go build ./...
go test ./...
Stacking: PR 3 is independent of #29 per Guido's §4 plan. Merge order
within round 11: this PR → PR 4 (§M12 fills in AdminScopeRewrite +
extends classifier) → round close.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* review fixup: §M11 unoccupied-session bypass + checkLiveness race
Addresses all three Copilot line-level findings on #30.
(1) Unoccupied-session-as-actor bypass (session_context.go:80):
ResolveSessionForEnvelope previously returned ResolveSessionActorIsSession
unconditionally whenever env.Actor was a session node. An orphan session
(node exists but has no has-occupant relation) could therefore emit
user-space rewrites and sail past §M11. Fixed by requiring the session
itself to have at least one canonical has-occupant relation pointing at
a principal — mirrors CheckAdminCapability's hop pattern. Unoccupied
session-as-actor now returns ResolveSessionAbsent; the kernel gate
rejects with the same §M11 message as the general absent case.
New helper sessionHasAnyOccupant reuses ResolveSessionOccupant so the
principal-type check (user|agent) stays in one place.
(2) Apply: state-read race with concurrent writers (runtime.go:102):
applyWithOptions called rt.checkLiveness before acquiring rt.mu, and
checkLiveness reads rt.state.Nodes / Relations / indexes. Under
concurrent writes this is a data race with potential "concurrent map
read and map write" panics. Wrapped the checkLiveness call in
rt.mu.RLock() / RUnlock() so state reads are synchronised. The short
window between RUnlock and Lock is acceptable — fold.Evaluate under
the write-lock is the authoritative apply-time check for structural
invariants.
(3) ApplyProgram: same race across batch preflight (runtime.go:181):
The preflight loop called checkLiveness on every envelope against
rt.state without a lock. Wrapped the entire preflight loop in a single
RLock / RUnlock so liveness observations across the batch are
consistent with each other. Release before acquiring Lock for the
apply body.
No lock-upgrade is attempted — Go's sync.RWMutex does not support it —
and reactive / sweep paths are unaffected because they use their own
lock discipline (applyReactiveLocked is called with Lock already held).
Tests (2 added):
- operad.TestResolveSessionForEnvelope_ActorIsSession_Unoccupied — pins
the resolver fix. Unoccupied session returns Absent, not ActorIsSession.
- kernel.TestApply_M11_UnoccupiedSessionAsActor_Rejected — integration:
end-to-end Apply rejects the envelope with §M11 error.
- kernel.TestApply_M11_OccupiedSessionAsActor_Accepted — positive pair:
an occupied session-as-actor still passes, covering the kernel-internal
session-heartbeat / turn-count path.
go build ./... # clean
go test ./... # all packages pass
Note: go test -race skipped locally (CGO / gcc not available on this
Windows host). Fix correctness argued by construction: RLock held for
all state reads in checkLiveness.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* review polish: pin ApplyProgram initial-state-check invariant
Guido flagged in #30 review that the ApplyProgram preflight checks §M11
against the state at batch start, not a working state that evolves
envelope-by-envelope. Per his suggested resolution — keep the initial-
state discipline (simpler, not batch-order-dependent) and document the
constraint explicitly — this test pins the rejection path for an
intra-batch session reference:
env 1: ADD session:newborn (passes under emitter=governance)
env 2: session_urn=session:newborn (rejected: not in initial state)
If the preflight ever starts threading a working state through, this
test must be adjusted deliberately. The rejection behavior is the
design, not an accident.
Doctrine note tightened at
ffs0/kb/research/kernel/20260421-t171-m11-m12-implementation-plan.md §2
with:
- Session-as-actor rule (only permitted when session is itself occupied;
mirrors §M12's hop pattern)
- ApplyProgram initial-state-check rule (explicit rationale for why
working-state-through-preflight is NOT the design)
Test-only addition here; doctrine commit on ffs0 separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Sam Maassen <215926119+MSD21091969@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Adds
RotateSessionOccupant— a pure helper that builds an atomic rewrite program to rotate a session'shas-occupantrelation from its current occupant to a new principal (user or agent). Unoccupied sessions emit a single-envelope "initial seat" program; occupied sessions emit a 2-envelope UNLINK+LINK pair. Consumers submit the returned slice viaruntime.ApplyProgramto get all-or-nothing atomicity.Replaces #28 (GitHub auto-closed it when its base branch was deleted on #27 merge). Same branch, same code, rebased onto master.
Design decision — rotation as two envelopes, not one
The v3.12 ontology description on WF19's
has-occupantadditional_port_pairreads:Current kernel model: MUTATE envelopes target nodes only (
env.TargetURNis a node URN; relations carry no mutable properties). Two ways to honor the ontology language:(1) Extend MUTATE with a
RelationURNdiscriminator — one envelope does the job, but it overloads MUTATE semantics and complicatesfold.applyMUTATE. Reads plausibly as "a fifth rewrite in sheep's clothing".(2) Helper emits atomic UNLINK+LINK program (this PR) — rotation is semantically a mutation of the target, operationally a 2-step atomic program on the same (SrcURN, WF19 port pair). Preserves "four rewrites only" from The Rule verbatim.
I picked (2). The invariants the ontology description actually cares about are preserved: session identity is unchanged, §M19 at-most-one holds at the atomic boundary, CI-3 (session-as-identity across rotation) is honored. Open question for Guido: is the doctrinal preference (1) or (2)? Happy to rework as (1) if the ontology description should be read literally.
Implementation
RotateOccupantResultcarriesEnvelopes,PriorRelationURN,PriorOccupant.RotateSessionOccupant(state, sessionURN, newOccupantURN, actor, newRelationURN) (RotateOccupantResult, error).Failure modes (all fail closed; nil envelopes on error)
sessionURN/newOccupantURN/newRelationURNtype_id != "session"newOccupantURNmissing OR notuser|agenthas-occupantrelations on source) — caller must clean up firstnewRelationURNcollides withPriorRelationURN— avoids post-UNLINK idempotent-skip landmineMinor hygiene
Introduced private constants
hasOccupantSrcPortandisOccupantOfTgtPort.ResolveSessionOccupantandRotateSessionOccupantreference them instead of bare string literals. No behavior change toResolveSessionOccupant.Tests (10 new)
sessionURN/newOccupantURN: rejectednewOccupantURN(kernel): rejectedhas-occupantdoctrine violation: rejectednewRelationURNcollision: rejectedUnblocks
Context
kb/research/session/20260421-t171-wolfram-kernel-proper-session.md§5kb/research/kernel/20260421-t171-m11-m12-implementation-plan.md🤖 Generated with Claude Code