| title | Session Model Unification |
|---|
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-04-10 |
| Author(s) | Codex |
| Issue | N/A |
| Supersedes | named-configured-sessions (partially); clarifies the post-pool session model layered over agent-pools |
Gas City should expose one runtime model:
[[agent]]is a session-producing config and policy object- sessions are the only runtime identities
[[named_session]]reserves canonical session identities backed by an agent config- generic config demand creates ephemeral sessions
- manual and provider-compatibility entry points create ordinary config-backed sessions with their own session identities
The old "pool vs no-pool" split must disappear as a semantic category. Capacity config answers "how many sessions may run for this config," not "what kind of thing is this" or "what does this bare name mean."
This design keeps user-facing product language mostly unchanged in Phase 1, but it removes the internal footgun by unifying identity, routing, demand, and lifecycle semantics behind a single session model.
The named-session refactor moved Gas City in the right direction, but the codebase still carries a surviving semantic split that is bigger than a naming problem:
- helpers like
isMultiSessionCfgAgentstill let capacity settings control routing and identity behavior - bare names sometimes mean a concrete session and sometimes mean "materialize from config"
gc.routed_to,assignee,work_query, and status all partially encode the old pool/non-pool distinction- controller-owned retirement still leaks through
closedand reopen behavior - prompts and hooks still infer semantics from overloaded variables like
GC_AGENT
The result is a major footgun: users cannot tell whether a config name
is a session, a factory, a singleton, an elastic worker class, or all of
the above depending on max_active_sessions, wake paths, and legacy
fallbacks.
- Make
[[agent]]a pure config/factory concept. - Make all runtime identity live on sessions.
- Allow multiple configured named sessions to share one backing agent config.
- Separate config routing from concrete session ownership.
- Make
scale_checkthe only controller-side generic demand signal. - Keep direct session continuity exact and inspectable.
- Preserve compatibility through read-side shims and phased rollout.
- Start with exhaustive red tests and a gap analysis before production code changes.
- Immediate user-facing terminology cleanup in CLI, API, or docs.
- A mandatory one-shot metadata migration of historical beads.
- Static policing of arbitrary custom
work_queryorscale_checkscripts. - A full Phase 1 rollout of new user-facing
pin/unpincommands. - Preserving every legacy pool-specific identity primitive such as
pool_slot.
[[agent]] defines reusable behavior and policy:
- prompt
- provider/start command
- workdir and environment
- dependencies
- wake mode
- idle policy
scale_checkmin_active_sessions/max_active_sessions- naming policy like
namepool
An agent config does not itself own runtime identity.
Fully qualified config identity uses the same scope form:
<rig>/<name>for rig-scoped configs<name>for city-scoped configs
A session bead is the only runtime identity. Every session has:
- a bead ID
- a backing config identity (
template) - a runtime
session_name - optional presentation alias
- mutable lifecycle state
Phase 1 writes exactly three session_origin values:
namedephemeralmanual
Origin is immutable and means "how this session came into existence." External bindings, provider kind, attachments, pinning, and assigned work are separate runtime facts, not origin values.
[[named_session]] declares canonical configured session identities.
New shape:
[[agent]]
name = "reviewer"
[[named_session]]
name = "mayor"
template = "reviewer"
mode = "on_demand"
[[named_session]]
name = "triage"
template = "reviewer"
mode = "always"Rules:
nameis the public identitytemplateis the backing config identity- multiple named sessions may point at the same template
[[named_session]].namemust be unique across configured named sessions after qualification- if
nameis omitted, compatibility meansname = template - identical
nameandtemplateis a supported steady-state pattern; omittingnameis compatibility syntax, not a separate semantic mode - fully qualified identity is
<rig>/<name>for rig-scoped identities and<name>for city-scoped identities
Gas City now has two separate namespaces:
Configured named identities reserve the session namespace immediately, even before bead materialization.
Normal session-targeting surfaces resolve in strict order:
- bead ID
- configured named-session alias
- current alias
- current
session_name - historical alias only on explicitly allowed compatibility surfaces
Rules:
- if session-namespace resolution finds zero matches, the operation fails with an explicit session-targeting error
- if more than one concrete or reserved session exposes the same bare token, bare resolution fails and qualification is required
- no rig-local implicit precedence exists for bare tokens; cross-rig or city-vs-rig ambiguity always fails closed
- session-targeting resolution never falls through to config namespace
- session-targeting resolution must not fall back to
templateoragent_name - a configured named-session alias in conflict remains authoritative for failure: bare targeting must fail with the conflict rather than hit the blocking session
| Token class | Accepted on | Lookup scope | Success rule | Notes |
|---|---|---|---|---|
| bead ID | session-targeting and compatibility surfaces | global exact bead lookup | exactly one open bead | never falls through |
fully qualified named identity <rig>/<name> |
session-targeting and compatibility surfaces | exact configured_named_identity lookup |
exactly one reserved or open canonical match | config-managed alias is only the mirrored presentation of this same identity, not a second lookup branch |
unqualified named token <name> |
session-targeting and compatibility surfaces | city-scoped identities plus identities in the current rig only | exactly one reserved/open session-side match across configured named alias, current alias, and current session_name |
cross-rig bare lookup is never permitted |
| current alias | session-targeting and compatibility surfaces | same as unqualified named token | exactly one open bead | reserved configured named alias still wins conflicts by failing closed |
current session_name |
session-targeting and compatibility surfaces | same as unqualified named token | exactly one open bead | compatibility only if the surface explicitly allows historical forms |
| historical alias | compatibility-only surfaces | explicit compatibility lookup only | exactly one compatibility candidate | never used by normal target resolution |
Unqualified session-targeting lookup never searches other rigs. If a token is not uniquely resolvable from city-scoped identities plus the current rig, the operation fails and requires qualification.
For a bare session-targeting token within the visible lookup scope:
- exact open bead ID wins if present
- if the token matches any reserved configured named identity:
- exactly one reserved named match and no competing open
alias/
session_namematch: target that named identity - exactly one reserved named match and any competing open
alias/
session_namematch: fail with configured-named conflict - more than one reserved named match: fail and require qualification
- exactly one reserved named match and no competing open
alias/
- otherwise resolve against current alias
- otherwise resolve against current
session_name - consult historical aliases only on explicitly compatibility-only surfaces
No session-targeting surface may reinterpret a bare token as a config token after a session-side miss or conflict.
Multiple matching identifiers on the same concrete bead count as one candidate, not an ambiguity. Ambiguity exists only when the token maps to different reserved identities or different beads.
Fully qualified session-side tokens bypass the bare-token algorithm and resolve only by exact bead ID or exact qualified configured named identity.
Phase 1 keeps historical aliases as compatibility input only.
Rules:
- normal session-targeting CLI/API commands do not consult
alias_history - historical alias lookup is reserved for compatibility translation of persisted legacy ownership/session references and any explicit debug surfaces
- new surfaces default to "no historical alias resolution" unless they opt in deliberately
- no current user-facing targeting command relies on historical alias lookup
Factory targeting resolves only by agent-config identity in strict order:
- explicit qualified config identity
- explicit
template:<name>syntax - bare
<name>token resolved only against city scope plus current-rig scope, and only when exactly one visible config matches
Named-session aliases do not reverse-map into config namespace.
If config-namespace resolution is ambiguous, qualification is required. City-scoped versus rig-scoped bare-name collisions are therefore fail-closed; neither scope silently wins on a bare token.
| Token class | Accepted on | Lookup scope | Success rule | Notes |
|---|---|---|---|---|
fully qualified config identity <rig>/<name> |
factory-targeting surfaces | exact config lookup | exactly one configured agent | canonical stored form for rig-scoped config references |
city-scoped config identity <name> |
factory-targeting surfaces | exact city-scoped config lookup | exactly one city-scoped configured agent | canonical stored form for city-scoped config references |
template:<qualified> |
factory-targeting surfaces | exact config lookup after removing template: |
exactly one configured agent | explicit family marker only |
template:<name> |
factory-targeting surfaces | city plus current-rig config namespace | exactly one visible config | no reverse mapping from named-session aliases |
bare config name <name> |
factory-targeting surfaces | city plus current-rig config namespace only | exactly one visible config | other rigs are never searched by bare lookup |
Cross-rig factory targeting always requires explicit qualification. There is no "search every rig and pick the only one" fallback.
All target-bearing surfaces share the same pipeline:
- classify the surface as
session-targeting,factory-targeting, orcompatibility-only - classify the token form as bead ID, fully qualified identity,
template:token, or bare token - choose the namespace permitted by the surface class
- apply the namespace-specific exact/bare resolution rules
- canonicalize stored identity if the surface writes metadata, or fail closed if resolution is ambiguous
No implementation may merge parsing and resolution in a way that allows post-miss reinterpretation across namespaces.
Top-level invariant: no bare token may ever denote both a session-family target and a factory-family target on the same surface, including under compatibility translation.
Bare-token lookup that refers to "current rig" is only valid when the surface has an unambiguous ambient rig:
- CLI commands use the caller's current rig/session context
- workflow/automation/API surfaces must provide explicit rig context if they want rig-scoped bare-token lookup
- if no ambient rig exists, bare rig-scoped lookup is forbidden and the caller must use a fully qualified token or a city-scoped identity
When ambient rig is absent, the canonical failure is an explicit qualification-required error. The resolver must not attempt partial lookup before failing.
Non-CLI entry points must not invent a current rig heuristically.
Ambient-rig source by surface family:
| Surface family | Ambient rig source |
|---|---|
| CLI commands | caller's current rig/session context |
| workflow/automation actions | explicit rig carried on the workflow object or dispatch context |
| API endpoints with explicit rig field/path | that explicit request scope |
| API endpoints without explicit rig scope | none; bare rig-scoped lookup is qualification-required |
template:<name> is valid only on factory-targeting surfaces:
- CLI arguments whose command family is factory-targeting
- metadata fields whose contract is config-targeted, such as
gc.routed_toandgc.execution_routed_to
It is not a session-targeting token and it is not a separate identity syntax inside config-managed session metadata.
Configured named-session aliases intentionally shadow config names in the session namespace. That means:
- bare
mayorin a session-targeting command means the named session template:mayor,gc session new mayor,gc.routed_to=mayor, andgc.execution_routed_to=mayormean the config
Manual session aliases may not collide with config names. Config-name shadowing is reserved for configured named sessions.
Manual aliases also may not collide with configured named-session identities, because both live in the session namespace.
Uniqueness rules:
- configured named identities are unique after qualification
- current aliases must be unique across open sessions after qualification
session_namemust be unique across open sessions within the city- collisions are rejected at write time and surfaced diagnostically if discovered in historical data
Global collision policy:
- configured named identities are the only session-side identifiers allowed to intentionally shadow config names
- manual aliases may not collide with any current config identity,
configured named identity, current alias, or current
session_name - generated or renamed
session_namevalues may not collide with any configured named identity, current alias, currentsession_name, or current config identity after qualification; generation must retry or fail closed rather than create ambiguity - historical aliases do not reserve namespace and never block new config or session creation on their own
If a newly added config name collides with an existing manual alias, the config remains authoritative in config namespace and bare targeting of the colliding manual alias must fail with an explicit conflict until the alias is renamed or removed.
Manual alias collision checks happen both at alias-creation time and again at config-load/reconciliation time so newly added configs cannot silently inherit a squatted factory name.
Commands are classified by target family rather than by token alone.
Every resolver entrypoint must declare its surface class at registration-time or compile-time. Helper functions may not widen, infer, or retry surface class dynamically.
Examples:
gc session attachgc session wakegc session suspendgc session closegc mailgc session nudge- bare
gc sling <target>
These resolve through the session namespace. If they target a configured named session, they may materialize its canonical bead.
Examples:
gc session new <config>template:<name>gc sling template:<config> <work>gc.routed_to=<config>gc.execution_routed_to=<config>
These resolve through the config namespace only.
Any new surface that accepts a target token must explicitly declare one of:
- session-targeting
- factory-targeting
- compatibility-only
Unclassified bare-token resolution is not allowed.
| Surface class | Examples | Resolution family | Historical alias | Notes |
|---|---|---|---|---|
| session-targeting CLI | gc session attach, gc session wake, gc session suspend, gc session close, gc mail, gc session nudge, bare gc sling |
session namespace | no | materialize named session if needed |
| factory-targeting CLI | gc session new, gc sling template:<config>, explicit template: args |
config namespace | no | generic dispatch/config creation |
| session-targeting API/workflow | direct session-targeted workflow assignee, session action APIs mirroring attach/wake/close/suspend/mail/nudge |
session namespace | no | concrete session delivery only |
| factory-targeting API/workflow | provider/agent create surfaces normalized to config identity, workflow gc.routed_to, gc.execution_routed_to |
config namespace | no | config-backed execution only |
| stored metadata | assignee, gc.routed_to, gc.execution_routed_to |
field-defined | restricted | assignee = session; routed fields = config |
| compatibility readers | legacy assignee, older stored references |
compatibility-only | restricted | may resolve only through explicit compatibility rules |
| session-context execution | gc hook |
non-target-bearing | n/a | may query gc.routed_to=$GC_TEMPLATE explicitly; this is not namespace fallback |
| inspection surfaces | status, doctor |
non-target-bearing unless separately declared | n/a | render/diagnose, not target resolution |
Phase 0 tests must pin every currently shipped target-bearing surface to one row in this matrix so the classification cannot drift silently in implementation.
Phase 1 treats the following as the complete target-bearing surface set that must be classified and tested:
| Surface | Class | Notes |
|---|---|---|
gc session attach |
session-targeting | may materialize named |
gc session wake |
session-targeting | may materialize named |
gc session suspend |
session-targeting | may materialize named into held state |
gc session close |
session-targeting | concrete session lifecycle only |
gc mail |
session-targeting | delivery-only; may materialize named |
gc session nudge |
session-targeting | delivery-only; may materialize named |
bare gc sling <target> |
session-targeting | concrete session delivery |
gc session new <config> |
factory-targeting | explicit config factory |
gc sling template:<config> <work> |
factory-targeting | explicit config routing |
workflow/API direct assignee target |
session-targeting | concrete session ownership |
workflow/API gc.routed_to |
factory-targeting | generic config execution |
workflow/API gc.execution_routed_to |
factory-targeting | control-dispatch preserved config lane |
provider-create boundary (kind=provider, provider name) |
factory-targeting compatibility shim | boundary-only sugar before factory resolution |
| stored legacy ownership/session-reference readers | compatibility-only | translation only |
| doctor/debug/migration identity readers | compatibility-only | inspection/repair only |
If any other public entrypoint accepts a free-form target token, it is a bug against this design until it is added here with an explicit class.
Phase 1 keeps compatibility-only behavior on a narrow explicit list:
- stored metadata readers that normalize legacy
assigneeor older session-reference fields - provider-entry request normalization at the boundary from
kind=provider/provider-name input to canonical config targeting - explicit debug/doctor/migration tooling that intentionally inspects historical aliases or ambiguous legacy references
No normal user-facing session-targeting or factory-targeting CLI/API command remains compatibility-only in Phase 1.
Compatibility terminology is exact:
session-targetingandfactory-targetingare normal runtime surfacescompatibility-onlymeans legacy translation/inspection surfaces that never reinterpret themselves as normal runtime targeters- phrases like "compatibility readers" or "compatibility translation"
refer only to that
compatibility-onlyclass
There are no surviving public CLI shorthands that may first try session resolution and then reinterpret the same bare token as config targeting. If any such surface still exists in implementation, it is a bug against this design.
New canonical rule:
- all new
assigneewrites use the concrete session bead ID
Compatibility:
- readers continue to accept legacy
session_name, legacy alias, exact configured named-session identity tokens, and only those template-era tokens that exactly equal a current configured named identity under present qualification rules - touched records should be opportunistically normalized to bead ID
Legacy ownership normalization is intentionally narrower than normal session-targeting resolution:
- it may resolve through existing open-bead IDs, current alias, and
current
session_name - it may also resolve through exact configured named identity for a config-managed named session
- template-era tokens are never looked up in factory namespace; they may normalize only by exact equality to a configured named identity under current qualification rules
- it must not resolve through historical aliases or generic config/template fallback
- if more than one candidate remains, normalization fails closed and diagnostics report the ambiguity
If a legacy token could match both a session-side candidate and a config name, compatibility resolution must still stay in the session family or fail; it must never reinterpret that token as a factory target.
If legacy ownership normalization resolves by exact configured named identity and no canonical bead currently exists, the ownership reader may materialize that canonical named bead solely to obtain a concrete bead ID for canonical rewrite. This is the only Phase 1 compatibility path that may materialize a reserved named identity.
Direct session-targeted writes set only:
assignee=<session-bead-id>
They do not also stamp gc.routed_to.
gc.routed_to is only for generic config-targeted execution and stores
the resolved qualified config identity.
It is not:
- a session identity
- a named-session alias
- a direct-session delivery hint
gc.execution_routed_to remains only as a narrow internal escape hatch
for control-dispatch flows that temporarily repoint gc.routed_to at
the control dispatcher while preserving the real config execution lane.
It must not become a second general-purpose routing channel.
Generic config-routed work may keep gc.routed_to as provenance after a
session claims it, but once it is claimed:
assigneebecomes the concrete session bead ID- it no longer counts as generic demand for
scale_check - continuity follows the owning session, not the generic route
All new writes of config-backed identity fields persist canonical scope forms, not raw user input:
- rig-scoped config identities are stored as
<rig>/<name> - city-scoped config identities are stored as
<name> - configured named identities are stored in the same qualified form
template,gc.routed_to, andgc.execution_routed_toalways carry backing config identity, never a session alias or user-entered token
Legacy unqualified rig-scoped tokens may be normalized only when the current city snapshot makes the mapping unique. Otherwise they remain legacy data and diagnostics surface the ambiguity instead of guessing.
Provider-era names, legacy agent_name references, and other historical
factory/session shims follow the same rule: they are compatibility input
only at explicitly listed boundary readers and never participate in
normal session-targeting or factory-targeting lookup.
Low-level raw assign surfaces may remain permissive in Phase 1 for
compatibility. gc doctor reports invalid or stale ownership instead of
making the controller guess.
scale_check answers only:
How many generic config-backed sessions should exist for this config?
It does not encode:
- named-session wake semantics
- direct concrete session ownership
- prompt-side work pickup behavior
work_query remains useful for:
gc hook- prompt-side work pickup
- running-session introspection
It no longer drives controller-side materialization or desired-count decisions.
The synthesized default remains, but becomes origin-aware at runtime:
- all sessions check assigned
in_progress - all sessions check assigned ready work
- only
origin=ephemeralchecks unassignedgc.routed_to=$GC_TEMPLATE
Named and manual sessions stop at explicit ownership.
Custom work_query and scale_check remain escape hatches.
Every config-backed session start should receive explicit env that matches the unified model:
GC_SESSION_ID= concrete session bead IDGC_SESSION_NAME= current runtime session handleGC_ALIAS= current public alias, if anyGC_TEMPLATE= qualified backing agent-config identityGC_SESSION_ORIGIN=named,ephemeral, ormanualGC_AGENT= temporary compatibility alias for the public handle only
New prompt and hook logic should key config semantics off GC_TEMPLATE
and lifecycle semantics off GC_SESSION_ORIGIN, not off GC_AGENT.
| Origin | configured_named_identity |
alias |
session_name |
GC_ALIAS |
GC_AGENT |
|---|---|---|---|---|---|
named |
present; immutable fully qualified named identity | always equals configured_named_identity while config-managed |
deterministic runtime handle derived from the named identity and workspace naming policy | same as alias |
same as alias |
ephemeral |
absent | optional, mutable if non-conflicting | opaque runtime handle | alias if present | alias if present, otherwise session_name |
manual |
absent | optional, mutable if non-conflicting | opaque runtime handle | alias if present | alias if present, otherwise session_name |
Configured named sessions do not carry a second mutable runtime alias separate from their configured identity.
Configured named-session alias vocabulary:
[[named_session]].nameis the config-declared public identityconfigured_named_identityis that same identity after qualification and is stored on the bead- bead
aliasmirrorsconfigured_named_identityfor config-managed named sessions
GC_AGENT remains compatibility-only. It must not be read by new logic
for routing, ownership, demand, or namespace resolution.
Phase 1 GC_AGENT contract is exact:
named: identical toGC_ALIAS, which is the configured named identityephemeralandmanual:GC_ALIASif present, otherwiseGC_SESSION_NAME
No Phase 1 path may interpret GC_AGENT as backing config identity,
factory target, or durable ownership token.
mode = "always":
- canonical bead is always desired
- the controller rematerializes a fresh bead if needed
mode = "on_demand":
- identity is reserved immediately
- canonical bead is created only when needed
Materialization causes for on_demand named sessions:
- direct session targeting
- direct concrete ownership writes that first materialize the canonical
bead and then persist
assignee=<session-bead-id> - dependency wake
- active external binding continuity for that exact session
- explicit pinning
Reserved-but-unmaterialized named identities must be visible in config-aware status surfaces, but bead-only listings remain bead-only.
Commands that need concrete ownership never persist an abstract named identity token. They first materialize the canonical bead, then persist its bead ID.
Delivery/ownership commands that materialize a named session, such as mail or direct work assignment, continue their delivery against that materialized bead. They do not require a pre-existing live runtime to resolve the target identity.
Non-named direct session-targeting must hit an existing concrete session. Ordinary sessions are never implicitly created from a bare config name. Creation remains explicit through factory-targeting actions.
Per-origin continuity rules:
named: exact identity continuity is keyed byconfigured_named_identity; non-terminal beads for that identity may be resumed or re-adopted according to the named-session rules in this documentephemeral: generic config demand always creates fresh session identity; it never revives a priordrainedorarchivedbead just because the config still has workephemeral: only explicit concrete continuity, such as exactassigneeownership or active external binding continuity, may revive a non-closed ephemeral beadmanual: manual sessions never satisfy generic config demand and are resumed only by direct targeting or exact continuity handles such as bindings
Concrete sessions and configured named identities also project a controller desired state separate from their current bead state:
undesired: no bead needs to exist or continue existing for this identity right nowdesired-asleep: the identity should exist as a bead, but no runtime start is currently requireddesired-running: the identity should have a live runtime nowdesired-blocked: the identity would otherwise bedesired-running, but a hard blocker currently suppresses start
For configured named identities, reserved-unmaterialized and
conflict describe whether a bead exists. The desired-state values
describe whether that identity should be absent, materialized asleep,
materialized/running, or currently blocked.
Desired-state projection rules:
| Inputs | Projected desired state | Notes |
|---|---|---|
| no wake/materialization cause and no requirement to preserve a concrete bead | undesired |
configured named identity may still project reserved-unmaterialized |
| concrete bead required for ownership/identity continuity, but no current wake cause | desired-asleep |
typical for on-demand named materialized for ownership only |
| durable or one-shot wake cause present and no hard blocker applies | desired-running |
runtime should become creating/active |
| durable or one-shot wake cause present, but a hard blocker applies | desired-blocked |
health is degraded, not silently healthy |
mode=always plus per-session suspend is therefore a supported
desired-blocked steady state: the named identity remains desired by
policy, the suspend override blocks start, and status should show the
identity as degraded/blocked rather than healthy-running or undesired.
Two important projected states are not bead states:
reserved-unmaterialized: a configured named identity exists but no bead currently exists for itconflict: config reserves a named identity, but the canonical bead cannot currently be materialized because of a namespace conflict or similar blocker
They are status projections only. They do not require placeholder bead records.
conflict enters when config owns the identity but canonical
materialization is blocked by a namespace collision or similar
reservation failure. It clears only when the blocking condition is
removed, and bare targeting must fail while the conflict exists.
Projection rules:
reserved-unmaterialized= configured named identity exists, no open canonical bead exists, and no conflict blocks materializationconflict= configured named identity exists, but canonical alias reservation or materialization is currently blocked- pending wake or delivery intents affect desiredness, not the projected identity class itself
Canonical occupancy rule:
- for a configured named identity, any unique open bead whose
configured_named_identityexactly matches that fully qualified identity and iscontinuity_eligible=trueremains the canonical occupant regardless of whether its base state isactive,asleep,suspended,drained,archived, ororphaned - the identity is
reserved-unmaterializedonly when no such open bead exists - an open bead with matching
configured_named_identitybutcontinuity_eligible=falseis historical only and must not block fresh canonical rematerialization once reconciliation has removed it from the canonical uniqueness set drainedtherefore still occupies canonical identity until it is terminally closed or loses continuity eligibility by explicit controller action
Crash quarantine is also a blocker overlay, not a separate base identity/state model.
Overlay rules:
conflictapplies only to configured named identities, not to generic ephemeral/manual sessions- crash quarantine applies only to materialized non-terminal beads and
normally leaves them in
archivedwhile blocked from restart reserved-unmaterializedidentities do not carry runtime-only overlays like quarantine until a bead exists
| State | Counts toward max_active_sessions |
Meaning | Typical exits |
|---|---|---|---|
creating |
yes | bead exists and runtime start/rematerialization is in progress | active, suspended, archived, closed |
active |
yes | runtime is live | creating, asleep, drained, suspended, archived, closed |
asleep |
no | bead exists but runtime is not live | creating, suspended, drained, archived, closed |
suspended |
no | per-session hold suppresses wake | asleep, creating, closed, orphaned, archived |
drained |
no | non-terminal completed/retired bead that may later resume the same identity | creating, archived, closed |
archived |
no | controller-retired historical bead preserved for inspection, quarantine recovery, duplicate repair, or explicit continuity re-adoption | creating, closed |
orphaned |
no | backing config is missing, so the bead is not startable | archived, creating, closed |
closed |
no | terminal bead; never reopened or re-adopted | none |
Retired/open non-terminal beads also carry a controller-owned continuity eligibility bit for exact-identity reuse:
continuity_eligible=true: exact identity may later resume this same bead if other rules permit itcontinuity_eligible=false: the bead is historical only and must never become the continuity target again
Normative defaults:
drainedcanonical beads are continuity-eligiblesuspendedcanonical beads are continuity-eligibleorphanedcanonical beads are continuity-eligible while the missing config condition is the only blockerarchivedbeads are continuity-eligible only when the controller archived them from the canonical exact identity due to temporary blocker/recovery conditions such as crash quarantine or interrupted restart- duplicate-bead losers, abandoned stray beads, and other historical
non-canonical records must be
continuity_eligible=false
continuity_eligible=false and canonical occupancy are mutually
exclusive after reconcile repair. If a bead still carries matching
configured_named_identity but is non-eligible, repair must first
remove it from the canonical uniqueness set before a replacement bead is
created.
reserved-unmaterialized->creatingwhen materialization also requires immediate liveness, such asmode=always, explicit wake, attach, active bound inbound event, or an unblockedpin_awakereserved-unmaterialized->asleepwhen materialization is needed only to create concrete ownership or identity continuityreserved-unmaterialized->suspendedwhen suspend materializes the canonical bead directly into held statecreating->activewhen runtime becomes livecreating->suspended,archived, orclosedif start intent is cancelled, controller retirement wins, or explicit close terminates the bead before activationactive->creatingon live restart paths such as non-deferrable config drift while preserving the cap slot for that same sessionactive->asleepwhen no durable wake reason remains and normal idle policy allows sleep- any non-terminal bead state ->
suspendedon per-session suspend asleep,drained,archived, andorphaned->creatingwhen the same exact identity becomes startable and desired again- any non-terminal bead state ->
closedon explicit close - any non-terminal bead state ->
orphanedwhen the backing config is removed or cannot be resolved - controller retirement uses
drained,suspended,archived, ororphaned; it never usesclosed
drained versus archived:
drainedis normal non-terminal retirement of a previously healthy session whose exact identity may still matterarchivedis controller history retention for failed starts, quarantine, duplicate-bead losers, or other retired sessions that should not look like active job completion
Fresh rematerialization versus resume is exact:
- if a configured named identity already has one open canonical bead and
that bead is
continuity_eligible=true, wake/resume/start targets that same bead - reconciliation mints a fresh canonical bead only when no open continuity-eligible canonical bead exists for the identity, or after the prior bead has crossed the terminal close barrier
- duplicate-bead losers and any bead with
continuity_eligible=falseare never resume/re-adoption candidates
| Origin | direct session target | generic scale_check demand |
exact assigned continuity | generic dependency satisfaction | bound inbound continuity | suspend | close |
|---|---|---|---|---|---|---|---|
named/on_demand |
materialize or resume exact canonical bead | never | resume exact canonical bead | may use implicit/explicit named satisfier rules | resume exact bound bead | may materialize exact bead into held state | terminal for current bead; config rematerializes only on the next real demand/explicit target |
named/always |
materialize or resume exact canonical bead | never | resume exact canonical bead | may use implicit/explicit named satisfier rules | resume exact bound bead | may materialize exact bead into held state | terminal for current bead; config immediately desires a fresh canonical bead once the close barrier completes |
ephemeral |
existing bead only | create fresh generic bead | resume exact bead only if non-closed and continuity-eligible | generic dependency may create fresh ephemeral bead | resume exact bound bead | existing bead only | terminal for current bead |
manual |
existing bead only | never | resume exact bead only if non-closed and continuity-eligible | never satisfies generic dependency by default | resume exact bound bead | existing bead only | terminal for current bead |
| Identity/bead condition | Qualifying cause | Result |
|---|---|---|
named identity, one open canonical bead, continuity_eligible=true |
direct target, assigned continuity, dependency, binding, pin_awake, mode=always policy |
resume/start that same bead |
named identity, one open matching bead, continuity_eligible=false |
any named-session cause | repair removes it from canonical uniqueness; then mint a fresh canonical bead if the identity remains desired |
| named identity, no open canonical bead | any named-session materialization cause | mint a fresh canonical bead |
ephemeral/manual bead, exact bead exists and continuity_eligible=true |
direct exact-bead continuity (assignee, bound inbound event, direct session target) |
resume/start that same bead |
ephemeral/manual bead, exact bead exists and continuity_eligible=false |
direct exact-bead continuity | fail; do not invent successor identity |
| ephemeral, no exact continuity target | generic scale_check or generic dependency demand |
mint a fresh ephemeral bead |
| manual, no exact continuity target | generic demand | fail; manual sessions are never generic capacity |
No policy or controller path may choose between "resume same bead" and "mint fresh bead" heuristically once the row above is known.
creating is not an indefinite limbo state. A failed or abandoned start
attempt must leave creating for a non-counting state, normally
archived with quarantine/blocker metadata preserved on the bead, and
it must stop counting toward cap once the attempt has terminated.
creating carries a controller-owned start epoch/lease. If the runtime
does not become active before that lease expires, or the start owner
is lost, reconciliation terminates the attempt, records quarantine or
failure metadata, and moves the bead out of creating.
The controller computes two disjoint desired-state outputs:
- per-session desired materialization/liveness for concrete identities
- per-config generic ephemeral desired count
It never infers concrete session identity from generic config demand.
Normative reconciliation invariants:
- at most one open bead may exist per fully qualified
configured_named_identity closedbeads are never wake targets, never adoption targets, and never continuity targets- repeated reconcile passes with unchanged inputs must be idempotent: they must not create extra beads or rewrite ownership differently
- direct commands and APIs may assert intent or materialize identity, but canonical bead creation/adoption must still flow through the same reconciliation/materialization guard path
- if multiple explicit continuity starts become newly eligible in the same pass, reconciliation emits start attempts in deterministic order: configured named identities sort by fully qualified identity, all other sessions sort by bead ID; this ordering affects attempt sequencing only, not desiredness
Canonical materialization for a configured named identity is compare-and-swap on that fully qualified identity key. Command-side materialization may create intent, but it must acquire the same canonical guard before creating or adopting a bead.
If duplicate open canonical beads are ever observed for one configured named identity, reconciliation must deterministically keep exactly one winner and retire all losers non-terminally. The winner is selected by the canonical materialization ordering metadata already on the beads (generation, then creation order as a tiebreaker). Ownership and bindings remain attached to exact bead IDs; the controller restores the uniqueness invariant, and diagnostics report the anomaly.
generation is the monotonic canonical-materialization generation for a
configured named identity. It increments whenever that identity creates
a fresh canonical bead after terminal close or other fresh
rematerialization event.
Normative reconciliation order:
- Validate config invariants and normalize compatibility inputs.
- Repair metadata drift that does not require runtime liveness.
- Project per-named-identity desired state from config mode, direct
targeting, concrete ownership, dependency wake, active binding
continuity, and
pin_awake. - Project generic ephemeral demand from
scale_checkandmin_active_sessions. - Apply hard blockers and wake-cause precedence.
- Count
active+creatingsessions toward caps. - Materialize, start, sleep, retire, or refuse starts accordingly.
When a direct session-targeted operation needs a named session that does not yet have a bead, the command materializes the canonical bead record or start intent, then reconciliation performs any actual runtime start. Commands do not bypass canonical duplicate-prevention by spawning runtime directly.
If ownership repair, duplicate-canonical repair, and startability projection all occur in one pass, the ordering is strict:
- normalize and repair metadata that does not depend on runtime
- restore canonical uniqueness for configured named identities
- compute desired state and start/stop decisions against that repaired identity set
Later phases in the same pass must not target duplicate losers or stale pre-repair ownership candidates.
When canonical fields are present, core lifecycle and identity logic
must not consult legacy pool-era markers such as pool_slot,
pool_managed, or manual_session. Those fields are compatibility-read
inputs only and may participate only in translation/diagnostic paths.
gc session wake becomes a real liveness trigger:
- materializes the canonical bead for configured named sessions if needed
- sets the existing one-shot start request path
- starts the session immediately via reconciliation
- does not implicitly pin it
Implementation should reuse pending_create_claim=true rather than
introducing a separate transient wake flag.
attach is a first-class liveness transition:
- it clears per-session suspend on the target bead or named identity before reconciliation evaluates startability
- it sets the same one-shot start intent as explicit wake
- for
reserved-unmaterializednamed identities it materializes the canonical bead and requests immediate start - against a
closingbead ID it fails - against a
closingnamed identity it is retained as successor demand on that identity and re-evaluated after the close barrier
attach is therefore stronger than passive inspection: it is explicit
operator intent to make the target live and attachable now.
Per-session suspend is a runtime override, not a config edit:
- may apply even to config-managed
mode=alwaysnamed sessions - suppresses wake until explicit wake or attach clears it
- may materialize an unmaterialized named session directly into suspended/held state without starting runtime
Per-session suspend is a hard blocker. pin_awake may remain set while
the session is suspended, but it has no effect until the suspend hold is
cleared.
pin_awake is a first-class per-session override:
- durable explicit wake reason
- suppresses idle sleep and no-wake-reason sleep
- may materialize and wake a reserved named session
- visible in normal status as a wake reason
pin_awake does not override hard blockers:
- backing config suspended
- backing config missing/orphaned
- per-session suspend
- explicit terminal close
- crash quarantine
While a temporary blocker exists, the pin remains set. If the blocker is later cleared, the session becomes wake-eligible again without needing to be re-pinned.
unpin removes only the durable pin wake reason. It does not force an
immediate stop.
close clears pin_awake because the bead itself is being terminated.
Non-terminal restarts preserve it.
The reconciler should combine causes with one invariant order:
- terminal close wins completely and clears bead-scoped overrides
- hard blockers suppress start even if wake causes exist
- durable wake causes remain recorded while temporarily blocked:
mode=always,pin_awake, assigned-work continuity, and live dependency demand - one-shot wake causes request immediate start but do not pin: explicit wake, attach, bound inbound event, and newly assigned concrete work
- lack of durable or one-shot causes allows normal sleep/drain policy
One-shot wake intents are retained only until one of three outcomes:
- they are consumed by a successful transition into
creating - they are invalidated by terminal close of the target bead/identity
- they are explicitly replaced by a stronger operator action
Hard blockers may defer a one-shot wake, but they do not create a second independent wake queue.
Close/wake races follow one invariant:
- wake never reopens a closing or closed bead
- bead-ID-targeted wake against a closed/closing bead fails
- named-identity-targeted wake or continuity after close resolves against the post-close desired state and, if still valid, targets the fresh canonical bead rather than the terminating one
For configured named identities, once close is accepted for bead B,
B is permanently excluded from future wake, adoption, and continuity
targeting. Any replacement bead may only appear after B has crossed
the terminal close barrier in the canonical materialization guard path.
closing-window intents are handled exactly as follows:
- bead-ID-targeted wake/attach/delivery against a
closingbead fails for every origin - named-identity-targeted demand received while bead
Bisclosingis retained against the named identity, not againstB - once
Bcrosses the close barrier, retained named-identity demand is re-evaluated against the successor identity state and may materialize or wake a fresh canonical bead - ephemeral/manual sessions have no successor-identity rewrite; exact bead-targeted callers must retry explicitly after close if they want a new session
For policy-desired mode=always named sessions, fresh rematerialization
is same-pass desired once the old bead has crossed that close barrier.
The controller must not leave the identity undesired waiting for a later
independent demand edge.
max_active_sessions is a config-wide concurrency bound.
It counts all active or creating sessions from that config, regardless of origin:
- named
- ephemeral
- manual
Asleep, drained, archived, orphaned, and closed sessions do not count.
A config with a finite max_active_sessions may not declare more
mode=always named sessions than that cap at config-load time. That is
an invalid config, not a runtime guess.
Manual sessions are explicit user actions. They may soft exceed the cap the same way other explicit continuity actions do; generic automation is the only thing denied under sustained over-cap pressure.
min_active_sessions applies only to generic ephemeral sessions. Named
and manual sessions have their own lifecycle contracts.
For generic config demand:
- only active + creating ephemerals satisfy desired count
- asleep, drained, and archived do not
- assigned-work continuity may revive a specific non-closed session, but that is not generic demand reuse
Explicit continuity-targeted actions may soft exceed the cap:
- explicit user-targeted wake/attach
- explicit
gc session new - inbound external event on an active binding for a concrete session
- durable
pin_awake - rematerialization of a policy-required
mode=alwaysnamed session if temporary runtime conditions have already consumed all headroom
While over cap:
- automation may not start more generic sessions
- the controller does not forcibly kill sessions just to get back under
- the system returns to normal only as sessions naturally sleep, drain, close, or unpin
- policy-required named sessions still outrank generic automation; they must not be left undesired just because generic load already filled headroom
Phase 1 treats simultaneous explicit continuity causes as intentionally soft-unbounded. The controller does not invent a second refusal rule for explicit user or continuity-directed actions; infrastructure/resource limits outside this model remain the backstop.
The starvation rule is explicit: policy-required named sessions and exact continuity-targeted sessions remain desired even under sustained soft over-cap pressure, while generic automation stays blocked.
For named mode=on_demand continuity retention:
- a materialized non-running bead remains
desired-asleepwhile it still carries concrete continuity anchors such as owned work, active binding continuity,pin_awake, or explicit per-session suspend - once no such anchor remains, ordinary idle reconciliation may retire
the bead non-terminally to
drained archivedis not the normal idle outcome for a healthy namedon_demandbead; it remains reserved for failure/quarantine/history
Dependencies remain config-to-config relationships.
Generic dependency satisfaction rules:
- if a config has exactly one named session, it is the implicit default named satisfier
- if multiple named sessions exist and exactly one is explicitly marked as the default satisfier, that named session is used
- otherwise generic dependency satisfaction uses ephemeral sessions only and must not pick an arbitrary named session
Named sessions may satisfy generic dependencies. Dependency wake is its
own wake cause and does not require synthetic assignee or
gc.routed_to.
Phase 1 may defer the explicit "default dependency satisfier" config field unless a real city requires it immediately. Until that field exists, any config with multiple named sessions behaves as "no named default available" and therefore uses ephemeral dependency satisfaction only.
gc session close is terminal for the bead being closed.
It must:
- fail if the session still owns open or
in_progresswork - require explicit requeue, migrate, or unassign before close succeeds
- avoid any implicit transfer of ownership to a fresh bead
- avoid any Phase 1
--forceescape hatch
Closing a config-managed named session closes the current bead only.
If the named identity remains desired by policy:
mode=alwaysrematerializes a fresh canonical bead immediatelymode=on_demandrematerializes only on the next real demand or explicit targeting event
This keeps close terminal for the old conversation while keeping
desired-state semantics coherent for the configured identity.
close also terminates bead-scoped overrides such as suspend and
pin_awake. A fresh rematerialized bead starts from config defaults and
new runtime causes, not from the old bead's per-instance overrides.
This is deliberate for suspend + close as well: closing a suspended
named bead clears the hold with that bead. A replacement canonical bead,
if policy later rematerializes one, starts unsuspended unless a new
per-session suspend is applied to that replacement bead.
For mode=always named identities, the post-close projection is exact:
- if no remaining hard blocker applies after the close barrier, the
identity projects
desired-runningand reconciliation may mint the fresh canonical bead in the same pass - if some other hard blocker still applies, the identity projects
reserved-unmaterialized + desired-blockeduntil that blocker is cleared or some other materialization rule requires a bead
closing is a transient controller barrier, not a stable bead state. It
means close has been accepted for a bead and the canonical-materialization
guard path is preventing any new continuity from targeting that bead
before terminal close completes.
closing inherits cap accounting from the bead's last base state until
terminal close commits:
- if the bead was
activeorcreating, it continues to occupy that cap slot until the close barrier completes - otherwise it does not count toward cap
closing is observable only as controller metadata/status, not as a new
base lifecycle state.
Controller-owned retirement must stop using closed as a generic
inactive state. Use non-terminal states such as:
drainedsuspendedorphanedarchived
This removes the need for "reopen closed bead" continuity semantics.
Removing a configured named session:
- releases the alias immediately
- tears down the session immediately
- retires it non-terminally
Re-adding the same identity may re-adopt that retired bead by exact identity match only. Never use heuristics like template or historical runtime name for re-adoption.
For configured named sessions, exact identity match means the fully
qualified configured_named_identity string only. Template, alias
history, session_name, and prior routing metadata must not participate
in re-adoption.
Re-adoption is allowed only from non-terminal retired states such as
drained, archived, or orphaned. closed beads and duplicate-bead
losers are never re-adoption sources.
Re-add adopts only the unique retained retired bead for that exact
identity, meaning a non-terminal retired bead with matching
configured_named_identity and continuity_eligible=true. If no such
retained retired bead exists, re-add mints a fresh canonical bead.
If more than one eligible retired bead exists for the same fully qualified configured named identity, re-adoption uses deterministic selection:
- highest
generation - latest canonical materialization ordering metadata within that generation
- latest retirement timestamp as final tiebreaker
All non-winning candidates remain retired and diagnostics surface the anomaly.
When config removal retires a named-session bead, the bead keeps
configured_named_identity only as exact-match historical metadata for
possible re-add adoption. That metadata no longer reserves namespace or
claims canonical occupancy once the config entry has been removed.
Config-removed named beads are outside the active canonical uniqueness set until the config identity is reintroduced. Duplicate-canonical repair and ordinary named-identity occupancy accounting ignore them until re-add makes that qualified identity live again.
They remain only as historical retained bead records. They are not ordinary active/open named-session occupants for wake, attach, or generic status accounting while the config entry is absent.
For a configured named identity, the "current canonical bead" is the
unique open bead whose configured_named_identity exactly matches that
fully qualified identity. If no such open bead exists, the identity is
currently unmaterialized.
Any config fingerprint mismatch is non-deferrable. The old "attached-session deferral" should be removed.
Rules:
- active sessions hold their cap slot and transition through the unified
live-restart path back into
creating; they do not expose a temporary generic-cap vacancy mid-restart - creating sessions that must restart stay within the same guarded start/restart path rather than creating a second bead
- non-live non-terminal sessions repair drift in place, mark continuation reset as needed, and use the fresh provider conversation on their next start
- reserved-but-unmaterialized identities update their projected config state without forcing materialization
- restart uses the unified continuation-reset path
- the next wake creates a fresh provider conversation
For named sessions:
- changing
namemeans a new identity and is semantically remove-old plus add-new - changing
templatepreserves the named identity but resets provider conversation continuity
active -> drained is controller retirement, not ordinary idle sleep.
Idle/no-cause behavior is active -> asleep; controller scale-down or
job-complete retirement is active -> drained.
Configured named-session aliases are config-owned, not rename history:
- alias and
configured_named_identitymust match - drift is repairable metadata drift
- configured aliases do not accumulate
alias_history - old configured aliases are released immediately on rename or removal
External bindings are orthogonal to origin.
A session may be:
origin=ephemeralplus a Discord bindingorigin=namedplus a mailbox bindingorigin=manualwith no binding
Rules:
- active binding continuity routes to the exact bound session
- non-terminal inactive bound sessions may revive on inbound events
- explicit
closeends the binding path as well - bound inbound continuity may soft exceed
max_active_sessions - suspend still blocks wake even for bound sessions
"Provider session" should not remain a separate ontology.
Provider-oriented entry points normalize to the equivalent explicit or implicit agent config at the boundary. The created session is then an ordinary config-backed session with whatever origin applies.
Phase 1 may preserve current provider-entry compatibility behavior:
- synchronous creation
- immediate option-default application
- immediate initial-message delivery
201 Created
But those are compatibility details at the boundary, not a second internal lifecycle model.
Provider names are resolution sugar, not automatic persisted aliases.
User-facing status should move toward two top-level concepts:
- agent configs and their capacity/policy
- sessions and their state/origin
Phase 1 does not require terminology cleanup everywhere, but new logic and new docs should stop relying on "pool vs non-pool" as ontology.
Config-aware status may synthesize:
- reserved but unmaterialized named sessions
- materialized inactive named sessions
- active named sessions
Bead-oriented listings remain bead-only.
gc doctor should report, without auto-fixing:
- work beads whose
assigneepoints at a missing or closed session bead - stale or unknown
gc.routed_toidentities - alias/config conflicts that leave a reserved identity or manual alias unresolved
- other migration-visible identity drift introduced by permissive low-level compatibility paths
Config load and reconciliation should surface alias/config conflicts proactively, not only when a later targeting operation fails.
Phase 1 surfaces should converge on this public error set:
| Error | Meaning | Typical trigger | Notes |
|---|---|---|---|
session_not_found |
no session-family target exists | session-targeting miss | no materialization attempted unless the surface allows named materialization |
factory_not_found |
no config target exists | factory-targeting miss | never consults session namespace |
ambiguous_session_target |
multiple session-family candidates remain | bare session token conflict | qualification required |
ambiguous_factory_target |
multiple config candidates remain | bare factory token conflict | qualification required |
configured_named_conflict |
configured named identity is reserved but blocked by another session-side claimant | named alias collision | must fail closed |
qualification_required |
bare token cannot be resolved safely under current scope rules | city/rig ambiguity or no ambient rig | caller must qualify |
target_closing |
concrete target bead is closing and cannot accept new bead-ID-targeted work | close race on exact bead | named-identity successor demand may still exist separately |
invalid_surface_class |
surface attempted an illegal resolver family or fallback | implementation bug / invalid API path | should never be silently coerced |
| Surface | Requires existing concrete session? | May materialize configured named session? | Must synchronously ensure liveness? |
|---|---|---|---|
gc session attach |
no for named; yes otherwise | yes | yes |
gc session wake |
no for named; yes otherwise | yes | yes |
gc session suspend |
no for named; yes otherwise | yes, into held state | no |
gc mail |
no for named; yes otherwise | yes | no |
gc session nudge |
no for named; yes otherwise | yes | no |
bare gc sling <target> / direct session-targeted workflow ownership |
no for named; yes otherwise | yes | no |
gc session close |
yes | no | n/a |
gc session new <config> / factory create |
no | n/a | follows create surface contract |
Compatibility-only readers are not normal operator surfaces. Their only allowed materialization side effect is the exact configured named identity rewrite path described in the compatibility appendix.
Config-aware status for each configured named identity should expose at least:
- qualified named identity
- backing template/config identity
- mode
- projected desired state
- projected identity class (
reserved-unmaterialized,conflict, or materialized) - canonical bead ID if materialized
- base bead state
- continuity eligibility
- wake causes
- hard blockers
- degraded yes/no
Every gc doctor identity/routing finding should include at least:
- finding kind
- affected object ID (
workbead orsessionbead) - offending field/value
- relevant named identity or config identity
- rig/city scope context
- collision peer IDs or candidate IDs if any
- one concrete operator action suggestion
Required finding classes include at least:
missing-bead-ownerclosed-bead-ownerambiguous-legacy-session-tokenlegacy-token-matches-config-onlycanonical-legacy-divergencestale-routed-configconfigured-named-conflict
City status is degraded whenever any mode=always named identity is not
currently satisfiable as running because it is:
desired-blockedconflictorphaned- repeatedly failing/quarantined in start paths
- under unresolved duplicate-canonical repair
Canonical behavior must follow field-specific rules:
| Field | Accepted legacy input | Canonical stored form | Normalization trigger | Must fail closed when |
|---|---|---|---|---|
assignee |
open bead ID, current alias, current session_name, exact configured named identity, limited template-era exact-to-named token |
concrete session bead ID | any ownership mutation or compatibility rewrite | token is ambiguous or resolves only through config/factory namespace |
template |
legacy qualified/unqualified config identity | canonical qualified config identity | any session-bead rewrite or create | rig-scoped legacy token is not uniquely mappable |
configured_named_identity |
implicit name=template compatibility shape |
canonical qualified named identity | named-session create/reconcile | multiple configured identities would result |
gc.routed_to |
older config tokens | canonical qualified config identity | any workflow/config-routing mutation | no unique config target exists |
gc.execution_routed_to |
older config tokens | canonical qualified config identity | control-dispatch mutation | no unique config target exists |
session_origin |
legacy marker combinations | named, ephemeral, or manual |
session create or touched-bead normalization | canonical fields are absent and legacy inputs conflict |
Canonical fields always govern runtime behavior. When canonical and legacy hints disagree on the same record:
- canonical fields drive behavior
- legacy fields survive only for diagnostics or compatibility rewrite
gc doctormust report the divergence explicitly
Compatibility-triggered named-session materialization used for ownership rewrite must be retry-safe. One normalized legacy owner token must produce at most one canonical materialization attempt and one canonical rewrite outcome under concurrent readers.
Allowed ownership/routing states:
| Work state | assignee |
gc.routed_to |
Meaning |
|---|---|---|---|
| generic unclaimed | absent | config identity present | generic config demand |
| generic claimed | bead ID present | config identity may remain as provenance | continuity belongs to assignee; route is non-operative provenance |
| direct session-targeted | bead ID present | absent | direct concrete ownership |
| explicitly unassigned/requeued | absent | config identity present or re-added intentionally | generic demand again |
Atomic claim invariant:
- once a valid concrete
assigneeis present, generic-demand accounting must exclude that work item regardless of preserved provenance fields - retry/continuity follows
assigneeonly - retained
gc.routed_tomust not re-activate generic routing unless an explicit unassign/requeue action clears ownership
Low-level permissive assignment APIs may still store malformed data in
Phase 1, but the controller must never derive ownership by reconciling
assignee and gc.routed_to together. Invalid combinations are
diagnostic problems, not alternate routing instructions.
Phase 1 provider-create compatibility normalizes to ordinary config-backed session creation with these exact rules:
- resulting
session_originismanual - provider-create is fresh-session factory creation only; it does not silently resume prior sessions by provider identity
- provider-create and
gc session new <config>must converge on the same persisted session schema (template,session_origin, configured named metadata if applicable, lifecycle intent metadata, and option/override persistence) - the compatibility difference is boundary behavior only: sync response, immediate option-default application, and optional immediate first message delivery
For synchronous provider-create plus initial message:
- bead/session creation must be durable before returning success
- if first-message delivery fails after session creation succeeds, the caller still receives the concrete session identity and may inspect or retry against that session explicitly
- partial success must never fabricate a second session on retry when idempotency keys or equivalent request identity is available
- dependency-driven starts stay in the automation lane unless they are resuming an already exact continuity-anchored bead; they do not gain soft-cap exception merely because the satisfier is named
- repeated one-shot explicit wake/attach/inbound events must deduplicate by target identity per reconcile pass; they do not create multiple over-cap entitlements for the same target
min_active_sessionsis a generic floor input, not a stronger claim than named/manual continuity under over-cap pressure- the implicit single-named-session dependency satisfier rule is a temporary compatibility shortcut; adding a second named session disables that implicit named satisfier until an explicit default is configured
Phase 1 is incremental, not a bulk rewrite.
Rules:
- read legacy data
- write canonical data for new or mutated records
- opportunistically normalize touched records
- avoid one-shot mandatory store migration
- legacy ownership tokens may normalize to bead ID only through session-namespace rules, never through config/template fallback
Legacy fields such as pool_slot, pool_managed, and manual_session
may still be read during migration, but new writes should converge on:
session_origin- canonical
template - canonical
assignee=<bead-id> configured_named_identitywhere applicable
Phase 1 permits legacy-token interpretation only on the following surfaces:
| Surface | Examples | Class | Historical alias consults | May materialize reserved named session? |
|---|---|---|---|---|
| stored ownership/reference readers | legacy assignee, legacy stored session refs |
compatibility-only | yes, only as explicit compatibility translation | yes, but only when exact configured named identity wins ownership normalization |
| provider-entry normalization boundary | kind=provider, provider-name create inputs |
factory-targeting compatibility shim | no | no; normalize to config target first |
| debug/doctor/migration tools | doctor checks, explicit migration repair tools | compatibility-only | yes | no unless the tool explicitly invokes a session-targeting surface afterward |
No other Phase 1 surface may ingest historical alias, provider-era name, template-era session token, or other legacy identifier form.
The accepted legacy token taxonomy is closed:
- open bead ID
- current alias
- current
session_name - exact configured named identity
- historical alias where the matrix permits it
- template-era token that exactly matches a configured named identity
Any other legacy string fails closed.
Stored ownership/reference readers are stricter still. They accept only:
- open bead ID
- current alias
- current
session_name - exact configured named identity
- historical alias only if no current reserved named identity or other current session-side candidate matches that token
- template-era token only if it exactly maps to configured named identity under current qualification rules
Compatibility precedence matrix:
| Surface | Open bead ID | Current alias | Current session_name |
Exact configured named identity | Historical alias | Template-era exact-to-named translation | Config/factory lookup |
|---|---|---|---|---|---|---|---|
| stored ownership/reference readers | yes | yes | yes | yes | yes, only if still unambiguous after current session-side checks | yes | never |
| provider-entry normalization boundary | no | no | no | no | no | no | factory-only after sugar expansion |
| debug/doctor/migration tools | yes when inspecting current state | yes | yes | yes for reporting | yes | yes for reporting only | never unless the tool explicitly invokes a factory-targeting action |
The surviving public boundary sugar set is intentionally tiny:
template:for explicit factory targeting- provider-entry normalization on provider-create boundaries only
There are no other sanctioned public legacy shorthands that may resolve config identity from a bare token in Phase 1.
The only Phase 1 provider-name sugar surfaces are provider-create compatibility entrypoints, including the direct provider session-create API/CLI boundary and equivalent request wrappers that normalize to the same factory-targeting path. No other public endpoint may accept provider-name sugar.
Provider compatibility normalization is boundary-contained and ordered:
- classify the incoming surface
- if and only if the surface is factory-targeting, expand provider-name sugar into explicit-or-implicit config identity
- run ordinary factory-targeting resolution
Provider/name sugar must never run on session-targeting surfaces or on compatibility-only session-reference readers.
Any provider-era name that maps to more than one factory candidate is ordinary factory ambiguity and must fail with the same qualification-required/error shape as any other factory-targeting bare token.
No ambiguous or partially normalized provider result may be persisted.
Provider-create compatibility endpoints must register as
factory-targeting in the same resolver-class registry as other
target-bearing surfaces. They may not implement separate ad hoc fallback
logic.
Worked ambiguity cases:
gc sling mayor ...where named sessionmayorshadows configmayor: session-targeting, resolves to the named sessiongc sling template:mayor ...in the same city: factory-targeting, resolves to configmayor- stored legacy
assignee=mayorwhile both named sessionmayorand configmayorexist: compatibility reader stays in the session family or fails closed; it never falls through to config - stored legacy
assignee=mayorwheremayoris a configured named session but currently unmaterialized: ownership compatibility may materialize the canonical named bead, then rewriteassigneeto that bead ID - compatibility-only read of historical alias
mayoraftermayorbecomes a reserved configured named identity: fail closed unless the token is the exact configured named identity form being translated - provider-create input
name=mayorwhile named sessionmayorexists: factory-targeting provider normalization runs only inside the provider/create boundary and resolves only to config identity; if config resolution is ambiguous or absent, it fails instead of stealing session-targeted traffic
Compatibility-only resolution algorithm:
- the caller must already be a registered
compatibility-onlysurface - accept only stored legacy session-reference tokens, not live user-command targets
- if the token is an unqualified rig-scoped form and no ambient rig is defined, reject it; do not search the whole city heuristically
- consult open bead ID, current alias, current
session_name, exact configured named identity, and historical alias only if this appendix says that surface may consult that class - never consult generic
template, config identity, or factory namespace; template-era compatibility is allowed only through exact configured named identity translation - on miss or ambiguity, fail closed and surface diagnostics rather than retrying another namespace
If a compatibility-only unqualified token could refer to both a city-scoped identity and a current-rig identity, it is ambiguous and must fail closed exactly as normal runtime resolution would.
Inspection helpers such as status/doctor must use the same declared resolver class as command/API paths for the identifier they are reporting. They must not silently widen lookup rules just because the surface is read-only.
Compatibility-only translation helpers are a separate resolver entry family. Normal session-targeting and factory-targeting commands must not call them as fallback after ordinary lookup fails.
Compatibility-triggered materialization of a named session is allowed only after the token has already resolved as an exact session-family match under this compatibility matrix. It is never part of exploratory lookup.
For ownership/reference readers specifically:
- exact configured named identity may trigger canonical-bead materialization for rewrite
- template-era exact-to-named translation may trigger that same materialization only after the exact configured named identity match is established
- current alias, current
session_name, and historical alias matches do not create or reserve sessions; they only bind to existing concrete beads
Compatibility collision example:
- compatibility-only read of bare
mayorwhen city-scoped named identitymayorand current-rig named identitydemo/mayorboth exist: fail closed and require qualification, even if the historical data predates qualification support
Historical aliases must never outrank or capture a token that is currently reserved as a configured named identity. On a compatibility-only surface, that collision fails closed unless the surface is explicitly translating exact configured named identity.
If exact configured named identity and a different open bead alias both match due to corruption, compatibility resolution fails closed and diagnostics surface the corruption. Exact configured named identity does not silently steal the write in that case.
Before production code changes:
- write the full desired-behavior test matrix
- land it as the first deliberately red commit on the branch
- use the failing set as the formal gap analysis
The spec suite should be primarily deterministic and exercise semantic boundaries:
- resolution and namespace behavior
- lifecycle and wake logic
- demand accounting and cap behavior
- metadata writes and compatibility reads
- workflow routing and retry behavior
- config evolution and re-adoption paths
Minimum Phase 0 red matrix dimensions:
- token class: bead ID, configured named identity, current alias,
current
session_name, historical alias, template-era exact-to-named token, provider-name sugar - surface class: session-targeting, factory-targeting, compatibility-only
- scope condition: city-scoped, current-rig, cross-rig ambiguity, no ambient rig
- materialization state: open named bead, reserved-unmaterialized named identity, closed bead, duplicate/corrupt candidates
Mixed-era red cases must include at least:
- legacy
assigneecolliding with a current config name - historical alias that is now a reserved configured named identity
- unqualified rig-scoped legacy token with no ambient rig
- provider-create sugar colliding with a same-named session identity
- touched-record rewrite that requires canonical bead materialization
- no deliberate user-facing CLI/API/docs rename
- canonicalize new behavior and data model
- preserve compatibility on read paths and raw low-level surfaces where needed
Phase 1 sequencing is normative. These must land together or behind one gate:
- resolver surface-class registry
- canonical write-path changes
- compatibility-reader narrowing
- doctor/status visibility for ambiguity and stale references
- the red spec suite covering the public surface inventory
Possible later work:
- terminology cleanup in CLI, API, and docs
- explicit
pin/unpincommand surface - stricter validation on raw assignment APIs
- further removal of legacy pool language from user-visible flows
The old pool/no-pool ontology is behaviorally dead only when all of the following are true:
- every public target-bearing surface is classified in the registry and covered by the Phase 0 matrix
- no bare session-facing token can create from config implicitly
- no compatibility reader can fall through into config/factory lookup
- new writes use only canonical ownership/routing/origin forms
- diagnostics surface every accepted mixed-era drift case without guessing intent
- there is no remaining runtime decision that depends on
pool_slot,pool_managed,manual_session, or similar pool-era semantic flags
Rejected because capacity settings would continue to change routing and resolution semantics.
Rejected because ownership should be one exact concrete session token. Human readability belongs in formatters and UI, not persistence.
Rejected because it double-counts demand and conflates session-local introspection with controller desired-state logic.
Rejected because provider-vs-agent is a config concern, not a lifecycle identity concern.
The existing code already contains partial seams this design can reuse:
pending_create_claimfor one-shot start requests- continuation-reset metadata for fresh provider conversations after restart
held_untilandsleep_intentfor suspend-style negative overrides- config injection of implicit provider agents
Implementation should prefer consolidating on those seams rather than adding parallel mechanisms unless the behavior truly differs.