gc session wake/nudge: a live named session conflicts with ITSELF, making the alias unreachable (gc-h4s93) - #154
Conversation
Cherry-pick of upstream gastownhall/gascity 08461bb (PR gastownhall#5487, Jim Wordelman, 2026-08-21). Fixes gc-h4s93: `gc session wake`/`gc session nudge` could not address a live named session by its configured name, failing with a self-referential error in which the same alias appeared on both sides of "conflicts with": configured named session conflict: "gascity/gc-toolkit.refinery" conflicts with configured named session "gascity/gc-toolkit.refinery" via live bead lx-fp9dj The resolver found the correct, unique, live session bead for the alias and then counted that match as a competing claimant rather than as the answer. Why a cherry-pick rather than a fork-authored fix: upstream had already shipped this exact repair, and standing operator policy on this subject prefers importing an existing upstream commit over writing our own for the same defect -- it carries near-zero conflict cost at the next rebase. Both `internal/session/named_config.go` and its test file were verified byte-identical to the upstream commit's parent before the pick, so this applied with zero conflicts and no fork-side adaptation. The change adds an alias-based canonical pass to both FindCanonicalNamedSessionBead (bead-backed CLI lookup) and FindCanonicalNamedSessionInfo (Info-shaped API lookup), so the two paths stay symmetric. The pass is deliberately narrow: it still requires the existing liveness and continuity checks plus a matching named-session template/spec, and it promotes a candidate only when exactly one template-matching alias candidate qualifies. That uniqueness requirement is what keeps a genuine collision -- two distinct live beads that both match the backing template -- falling through to conflict detection instead of one silently winning on first-match, and it keeps a reserved-name decoy with no corroborating template metadata from being promoted. Note that this defect was latent rather than cured in our fork: it reproduces only when a session is reachable solely through its configured alias, with no corroborating canonical metadata. A live session not in that state resolves fine today, which is why a spot check on 2026-08-22 could not reproduce it. The code path was unchanged in our fork (zero fork commits on every file emitting the conflict message), so the bug was still present. Validation: the commit's five regression tests were run both with and without the source change. With the fix, all five pass. With `internal/session/named_config.go` reverted to the base revision and the new tests kept, the three single-live-session cases fail -- TestFindCanonicalNamedSessionBead_AliasSoleLiveCandidateIsCanonical, TestFindCanonicalNamedSessionInfo_AliasSoleLiveCandidateIsCanonical and TestLookupConfiguredNamedSession_AliasOnlyLiveBeadResolvesCanonical -- confirming the tests genuinely cover the reported defect and the fix is not inert. The two true-collision guards pass in both states, as they should: they exist to prove the new pass does not over-reach. Co-authored-by: investigator <investigator@gascity.local> (cherry picked from commit 08461bb)
…s own diff check (gc-sx5cq)
Pre-open signoff (review bead gc-adxqr) raised one P1: lines 3-7 of the
ga-rcroz7 release-gate record ended in Markdown hard-break double-spaces, so
`git diff --check origin/main...HEAD` failed on the very document that records
that check as PASS. The gate record was falsifying its own evidence -- criterion
5 ("Final branch is clean") and the diff_lane entry both assert the check
passes, and on this branch it did not.
Stripped the trailing whitespace on those five lines. Nothing else in the file
or the branch changes, and no gate claim is weakened: the fix makes the recorded
evidence TRUE rather than restating it. Re-ran the check on the committed tree
to confirm.
Chose stripping over rewriting the block as a Markdown list because that is the
repository convention -- every sibling document in release-gates/ (e.g.
concrete-identity-claims-gate.md, doctor-backlog-depth-ready-error-gate.md)
writes the same header as plain consecutive metadata lines with no trailing
whitespace and no hard breaks.
Kept as a separate commit rather than amending 836e928 so the delta since the
reviewed OID is visible to the re-gate.
|
Codex signoff (pre-open, comment-only — not an approval): VERDICT: COMMENT Scope checked: full diff for internal/session/named_config.go, internal/session/named_config_test.go, and release-gates/ga-rcroz7-named-session-alias-canonical-gate.md; anchor bead gc-h4s93 and prior review gc-adxqr; resolver call sites in cmd/gc/session_resolve.go, cmd/gc/named_sessions.go, internal/api/session_resolution.go, and internal/dispatch/control.go; session creation metadata in internal/session/manager.go; upstream equivalence against 08461bb for the first cherry-pick commit; rework delta from 836e928 to 5d260bd. Findings: Verification:
Not checked: full repository matrix; this signoff focused on the changed session resolver package, direct CLI/API consumers, the release-gate diff-check regression, and the prior P1 rework. |
Summary
Defect
`gc session wake`/`gc session nudge` cannot address a named session that is
alive, active and running. Resolution fails with a self-referential conflict:
The name on both sides of "conflicts with" is identical. The live bead it names
(lx-fp9dj) IS that session: title "gascity/gc-toolkit.refinery", status open,
created 2026-08-17T17:36:02Z.
gc session list --state allshows the sessionas state=active running=true.
So the resolver finds the correct, unique, live session bead for the alias and
then treats that match as a competing entry rather than as the answer. Nothing
else claims the name -- this is not two sessions racing for one alias.
Impact
Every
mol-polecat-workhandoff ends with step 7 ofsubmit-and-exit:Both fail for the gascity refinery. The formula marks them non-fatal (
|| true)and the
gc bd updatein step 6 still emitsbead.updated, so work is not lost-- the refinery picks it up on event-watch or its next poll. But the immediate
signal is gone for every polecat in this rig, so each handoff now waits up to
event_timeoutinstead of starting at once. It is a silent latency regressionin the merge queue, invisible because the failure is swallowed by design.
It also removes the only lever for waking a stopped on_demand refinery, which
is the case
|| truewas never meant to cover.Reproduce
Observed 2026-08-19T16:5xZ from polecat gc-toolkit.furiosa while handing gc-x1a87
to the refinery. All three rigs (gascity, gc-toolkit, shutupandlisten) show an
active refinery session; the conflict was observed on the gascity one.
Where to look
Named-session resolution -- the path that maps a configured named session to a
live session bead and raises
configured named session conflict. The bug isalmost certainly a missing identity check: the candidate bead is not excluded
from the conflict set when it is the very session being resolved, so a
single live match is counted as two.
Related but distinct, so do not conflate: gc-5fdrr (a CLOSED session bead
permanently reserves its alias, landed as f475b68). Here the bead is OPEN and
the session is running -- the reservation is not stale, it is self-colliding.
Acceptance
gc session wakeandgc session nudgeaddress a live named session by itsconfigured name. A conflict is raised only when two DISTINCT live session beads
claim one alias, with a regression test covering the single-live-session case
that fails today.
Implementation notes
Recurrence evidence (ported from gc-bwcd8, folded here 2026-08-21, visit gc-qcvca)
Confirmed NOT a stale-bead artifact — two observations naming DIFFERENT live
session beads for the same alias:
2026-08-19T16:5xZ — conflict named live bead lx-fp9dj (this bead's own filing)
2026-08-20T09:0xZ — conflict named live bead lx-fp9dj (gc-bwcd8)
2026-08-20T11:0xZ — conflict named live bead lx-npl7f (gc-bwcd8)
The refinery session was RECYCLED between the 09:0x and 11:0x observations, so a
FRESH session bead reproduces it immediately. That rules out "one leftover
duplicate bead needs cleaning up" and points squarely at the resolver treating
the target's OWN live session bead as a competing claimant.
Both times the session was healthy (
state=active running=true) and the handoffitself completed (bead reassigned, branch pushed and verified); only the
belt-and-suspenders wake+nudge was lost.
gc-bwcd8 also names the mirror-case precedent: f475b68 (gc-5fdrr) fixed a
CLOSED session bead permanently reserving its alias. This is NOT that fix — here
the bead is open and the session is running, so the closed-bead release lever
does not apply.
Classification (visit gc-qcvca, merge-base ed267ad)
cmd/gc/session_resolve.go:77 and internal/api/session_resolution.go:233 raise the
message. BOTH files are fork-UNTOUCHED and upstream-STATIC since merge-base —
neither side has changed them. Genuine upstream defect in code nobody is moving:
a fix here carries essentially zero rebase-conflict risk. upstream_pr_candidate.
Correction — converse sitting 2026-08-22T14:2xZ (visit gc-qcvca, 3rd sitting). Verified independently, three findings.
1. UPSTREAM HAS ALREADY FIXED THIS. Do not author a fork fix.
upstream commit 08461bb "fix(session): recognize unique alias owner as
canonical (gastownhall#5487)", Jim Wordelman, 2026-08-21T22:11Z. Its own summary is this
bead verbatim: "Configured named sessions can now recognize their own live
session when the session is reachable only through its configured alias. After
a named session starts, commands such as gc mail send no longer report
that sole legitimate session as conflicting with itself." It fixes both the
bead-backed CLI lookup and the Info-shaped API lookup, and preserves true
collisions as errors. Our fork does NOT have it.
Standing operator decision (this subject, visit 5) applies directly: a
cherry-pick of an existing upstream commit is near-zero cost at rebase and is
ALWAYS preferred over authoring our own fix for the same defect.
2. The patch site named in "Where to look" is not where the logic lives.
Earlier triage recorded cmd/gc/session_resolve.go and
internal/api/session_resolution.go as blob-identical across merge-base / fork /
upstream with zero commits either side. That is TRUE and it is IRRELEVANT:
neither file decides canonical-vs-conflict. Both only re-raise the error that
session.LookupConfiguredNamedSession returns
(cmd/gc/session_resolve.go:77, internal/api/session_resolution.go:233).
The decision is in internal/session/named_config.go — FindCanonicalNamedSessionBead
vs FindNamedSessionConflict / BeadConflictsWithNamedSession. That file is
fork-clean (0 commits ours) but carries the 1 upstream commit above, so its blob
differs from upstream. The "zero divergence either side" reading did not cover
the file that actually needs the change.
3. It does NOT reproduce today, and that is not a fix.
2026-08-22T14:2xZ: gc session wake succeeded, exit 0, no conflict, on all three
LIVE named refinery sessions (gc-toolkit, shutupandlisten, signal-loom
/gc-toolkit.refinery). gascity has no live refinery session, so the original
2026-08-19 target could not be retested in its original state.
The code is unchanged in our fork (0 commits ours on every file that emits
"configured named session conflict"), so the defect is LATENT, not cured: per
gastownhall#5487 it fires when a session is reachable ONLY through its configured alias
(no corroborating canonical metadata). Today's session beads are not in that
state. Do not close this as no-longer-reproducing.
Implemented 2026-08-22 (polecat gc-toolkit.nux, branch polecat/gc-h4s93)
Resolution: CHERRY-PICK of upstream 08461bb ("fix(session): recognize unique
alias owner as canonical", PR gastownhall#5487), per the correction recorded in this
bead's notes and the standing operator preference for importing an existing
upstream commit over authoring a fork fix.
Commit: 836e928 (upstream authorship preserved;
-xprovenance trailer kept;subject re-titled to carry the bead ID per repo convention).
Applied cleanly, ZERO conflicts, no fork-side adaptation. Both
internal/session/named_config.go and internal/session/named_config_test.go were
verified byte-identical to the upstream commit's parent before the pick, and
the fork has 0 commits on either file since merge-base ed267ad. Third file is
the upstream release-gate doc, carried verbatim into release-gates/.
What it does: adds an alias-based canonical pass to BOTH
FindCanonicalNamedSessionBead (bead-backed CLI lookup) and
FindCanonicalNamedSessionInfo (Info-shaped API lookup), keeping the two paths
symmetric. The pass still requires the existing liveness + continuity checks
and a matching named-session template/spec, and promotes only when EXACTLY ONE
template-matching alias candidate qualifies -- so a genuine two-session
collision still falls through to conflict detection, and a reserved-name decoy
with no corroborating template metadata is not promoted.
ACCEPTANCE MET -- verified by inverted-fix test, not just by a green run:
with the fix -> all 5 new regression tests PASS
source reverted to base, new tests kept -> the 3 single-live-session cases FAIL:
TestFindCanonicalNamedSessionBead_AliasSoleLiveCandidateIsCanonical
TestFindCanonicalNamedSessionInfo_AliasSoleLiveCandidateIsCanonical
TestLookupConfiguredNamedSession_AliasOnlyLiveBeadResolvesCanonical
The 2 true-collision guards pass in both states, as intended.
This confirms the tests cover the reported defect and the fix is not inert.
Gates run (this rig configures NO test/lint/build commands, so these were run
by hand):
go build ./... PASS
go vet ./internal/session ./internal/api ./cmd/gc PASS
go test -race ./internal/session/ PASS (10.6s)
go test ./internal/api/ PASS (100.2s) [consumer]
go test ./internal/dispatch/ PASS [consumer]
go test ./cmd/gc/ -run Named PASS (targeted; never bare -- 6 shards)
gofmt clean
Push gate: .githooks/pre-push would have run
make test-fast-parallel, but theshared rig repo sets core.hooksPath=/dev/null so NO hook fires for any agent
here. Ran the gate manually instead: 9/10 jobs green. The one failure
(unit-core) was TestClientMailListAllRigsMultipleStoreSlowReturnsTyped503BeforeClientTimeout,
proven UNRELATED to this diff -- with -coverpkg=./internal/session/ that test
executes 0 of 260 coverage blocks in the changed file (6 of 4373 statements in
internal/session overall), and it passes 10/10 standalone. It is a 100ms-vs-250ms
scheduling-margin flake under parallel load.
Filed while here (both out of scope for this branch, neither fixed):
gc-38ko9 (P2) flaky: ...StoreSlowReturnsTyped503BeforeClientTimeout under
full-suite parallel load (internal/api)
gc-w89av (P1) Found: core.hooksPath=/dev/null in the shared gascity rig repo
disables the pre-push test gate for EVERY agent -- a strictly
larger hole than gc-01o2l/gc-uz8az, and it defeats their fixes
(incl. TestPrePushAnnouncesEverySkip), since the hook is never
executed at all. Needs an operator decision on intent.
Reviewer note: the alias lookup now runs BEFORE the
!includeConflictearlyreturn, so lookupConfiguredNamedSession issues one additional
listConfiguredNamedSessionBeadsByMetadata("alias", ...) store call on the
non-conflict path. That is upstream's deliberate shape (the canonical pass must
run regardless of includeConflict) and was kept unmodified.
Refinery handoff
gc-h4s93(bug, P1)polecat/gc-h4s93main5d260bdd; PR opened codex-green.