Skip to content

fix(dolt): surface + auto-clear stale compact quarantines; label backup-sync timeouts (gc-h7mc0tz) - #1

Open
vbtcl wants to merge 1 commit into
mainfrom
fix/dolt-compact-quarantine-autoclear
Open

vbtcl wants to merge 1 commit into
mainfrom
fix/dolt-compact-quarantine-autoclear

Conversation

@vbtcl

@vbtcl vbtcl commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Why

A transient post-flatten value-hash compact-quarantine marker silently disables ALL compaction/GC for a DB with no alert and no auto-staleness, letting the noms journal grow toward the corrupted-journal city-down threshold. beads_hq sat quarantined 16 days (journal 5.1G, data dir 13G) — the same path that ends in a city-wide Dolt outage. This is a recurring incident class (see gc-h7mc0tz).

What

Three improvements close the gap (commit b5116b0, examples/dolt source — the deployed .gc/system/packs/dolt is reconciler-managed so the fix must originate here):

  1. mol-dog-doctor: emit a [HIGH] health advisory whenever an active compact-quarantine marker exists. Counts only valid-db-name markers (matching the compactor's own has_compact_marker lookup) so operator archives like beads_hq.stale-cleared-* do not false-alarm.
  2. compact auto-clear: clear a quarantine marker older than GC_DOLT_COMPACT_QUARANTINE_STALE_SECS (default 6h) only once the DB reads clean (row counts) and is quiescent (whole-DB value hash stable across two probes a settle apart), then retry. The post-flatten re-verification re-quarantines on real drift — so this is a supervised retry that never bypasses integrity enforcement.
  3. Label backup-sync timeouts distinctly from hard failures.

Tests

7 new hermetic tests + full examples/dolt suite green (2 pre-existing env-specific failures unrelated).

Open for reviewer

  • Merge target: opened against vbtcl/gascity:main (the branch base). Retarget to upstream gastownhall/gascity if that's where the release is cut.
  • Release/deploy: tracked in gc-sffnhkx (P2) — needs a gc release + city reinstall after merge.
  • Independent: gc-o2n5yzz (P3) flags a dolt-2.0.7 ANSI-color leak in compact's remote-HEAD parsing, worth a look.

Filed by gastown.mayor on behalf of claude-1 (gc-wisp-ncb1). Refs gc-h7mc0tz, gc-sffnhkx.

…up sync timeouts

A transient post-flatten value-hash quarantine silently disabled ALL
compaction/GC for a DB with no alert and no auto-staleness, letting the
noms journal grow toward the corrupted-journal city-down threshold
(beads_hq sat quarantined 16 days; journal 5.1G, data dir 13G). Three
improvements close the gap:

1. mol-dog-doctor: emit a [HIGH] health advisory whenever an active
   compact-quarantine marker exists (it silently disables GC on critical
   infra). Counts only valid-db-name markers, matching the compactor's own
   has_compact_marker lookup, so operator archives like
   beads_hq.stale-cleared-20260607 do not false-alarm.

2. compact: auto-clear a quarantine marker older than
   GC_DOLT_COMPACT_QUARANTINE_STALE_SECS (default 6h) once the DB reads
   clean (row counts) and is quiescent (whole-DB value hash stable across
   two probes a settle apart), then retry compaction. The post-flatten
   re-verification re-quarantines if real drift remains, so auto-clear is
   a supervised retry that never bypasses integrity enforcement or GCs
   unverified data. Kill switch: GC_DOLT_COMPACT_QUARANTINE_AUTOCLEAR=0.

3. mol-dog-backup: distinguish a sync timeout (run_bounded rc 124 ->
   "sync timed out >120s; likely journal bloat/size", surfaced in the mail
   subject) from a generic sync error ("sync failed rc=N"), so journal
   bloat is diagnosable from the advisory.

Tests: 7 new hermetic tests in dog_exec_scripts_test.go (auto-clear when
quiescent, keep-fresh, keep-when-writer-active, kill switch, backup
timeout vs error, doctor advisory) plus a quarantine_writer_active fake
mode. Full examples/dolt suite green except two pre-existing,
environment-specific failures unrelated to this change
(TestCompactScriptRealDoltRemotePush: dolt 2.0.7 ANSI color in remote-HEAD
parse; TestRuntimeScriptManagedStateBeatsStaleEnvPort: port-resolve env).

Refs gc-h7mc0tz.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vbtcl pushed a commit that referenced this pull request Jun 16, 2026
…(ga-c4w) (gastownhall#3103)

## Summary

Makes the mouse wheel drive **tmux copy-mode scrollback** in interactive
`gc`
sessions instead of leaking the wheel to the focused TUI (Claude Code's
own
history, a pager, or the shell) — durably and out-of-the-box — while
**headless
agent sessions stay mouse-off** (controller-poll safety). This is the
proper
in-source fix that supersedes the portharbour city-local `po-vtg2`
`set-hook`
stopgap.

Two facts made the wheel inert before this change, so the fix has two
parts:

- **Part B — runtime default (`internal/api/session_runtime.go`).**
  `sessionCreateHints` now sets `MouseOn: true`. The runtime skips
  `disableMouseAndActivity` only when `MouseOn` is true
(`internal/runtime/tmux/adapter.go:930`), so the `mouse on` set at
session
create (`tmux-theme.sh`) survives and the wheel binding can fire. This
seam
  flips exactly the two human-interactive callers — provider-adhoc
(`session_resolved_config.go`) and named sessions
(`session_resolution.go`).
The headless agent path resolves `MouseOn` from
`cmd/gc/template_resolve.go`
  (`cfgAgent.MouseModeOn()`) and is **not** involved → stays mouse-off.

- **Part A — pack binding
(`examples/gastown/.../tmux-keybindings.sh`).**
Adds root-table `WheelUpPane → copy-mode -e` / `WheelDownPane →
send-keys -M`
bindings (forces copy-mode even over mouse-reporting apps so scrollback
wins;
  Shift+wheel keeps native terminal selection). **No** `client-attached`
  `set-hook` stopgap — the `MouseOn` default replaces the prototype's.

> Why `sessionCreateHints` and not the bead's suggested
`mouse_mode='on'`
> template default: provider/named sessions build their runtime hints
solely via
> `sessionCreateHints`; their synthetic `&config.Agent{}` is discarded
after
> provider resolution, so a template `mouse_mode` would never reach
them. The
> hints builder is the minimal correct seam, and it keeps the change off
the
> agent-template path entirely (guaranteeing headless behavior is
unchanged).

## Micro-tasks (TDD red→green, per-task commits)

| task | commit | test |
| --- | --- | --- |
| T-001/T-002 interactive mouse-on default | `19d6a9cdf` |
`TestSessionCreateHintsEnablesMouse` |
| T-003 headless stays mouse-off (guard) | `fe1c2149f` |
`TestResolveTemplateHeadlessAgentStaysMouseOff` |
| T-004/T-005 pack wheel binding + no stopgap | `6bc2d400a` |
`TestTmuxKeybindingsScrollWheel` |
| T-006 build + targeted tests + CHANGELOG | `0745b53d8` | — |

## Testing

Run under the hermetic `env -i` wrapper (Makefile `TEST_ENV`) +
`icu4c@78` CGO flags.

- `go build ./...` → **Success**
- `go test ./internal/api/... ./examples/gastown/...` → **1635 passed**
- `go test ./cmd/gc/...` → all ga-c4w tests pass.

**Pre-existing, unrelated failures (not introduced here):**
`TestBdRuntimeEnvManagedCityProjectsHostOverride` and
`TestBdRuntimeEnvForRigInheritedManagedCityProjectsHostOverride` fail
**identically on base `dd3ee8524`** with none of this branch's changes
present
(managed-Dolt host-override port resolution; the local sandbox's
proxied-server
setup does not produce the override).
`TestProbeDetachedWork_TmuxExitStatus`
timeouts were host-env flakes that pass under the hermetic `env -i`
wrapper.

## Manual verification (acceptance #1, gastownhall#3 — not unit-testable)

After merge + pack roll, in a fresh interactive `gc session new
<provider>`:
1. Wheel-up in a Claude pane enters copy-mode scrollback; wheel-down
scrolls
   down and exits at the bottom.
2. Mouse pane-select, drag-resize, the `MouseDown1StatusRight` mail
popup, and
   Shift+wheel native selection all still work.
3. A headless agent session shows `mouse off`
   (`tmux show-options -t <sess> mouse`).

## For the reviewer (open questions, downstream-resolvable)

1. **`monitor-activity` side-effect.** `MouseOn=true` skips the whole
   `disableMouseAndActivity`, so interactive sessions also keep
`monitor-activity on` — same as `mouse_mode=on` agents already get,
benign
for a human-attended session. Split the helper (mouse conditional,
activity
always) only if you want activity off regardless. Out of scope unless
flagged.
2. **`WheelDownPane send-keys -M` at bottom of scrollback** — exit-clean
is
   covered by manual verification #1.

## Compliance

- **GDPR:** no-op. Governs tmux mouse-mode / key bindings for dev
tooling; no
personal or special-category data read, written, transmitted, or logged.
- **MDR Class I:** no-op. Outside the voxmemo → voxist-api clinical
pipeline.

## Follow-up (separate, not this PR)

Removing the portharbour city-local `po-vtg2` stopgap is a separate
city-store
task to file once this ships and the gastown pack is rolled.

Refs: ga-c4w (supersedes po-vtg2). Plan:
`docs/plans/durable-mouse-wheel-scrollback.md`.

---------

Co-authored-by: Eric Cestari <eric@escapevelocity.fr>
vbtcl pushed a commit that referenced this pull request Jun 16, 2026
…ession) (gastownhall#3139)

## Summary

Post-merge regression fix for **ga-c4w / PR gastownhall#3103**. `internal/api`
`sessionResumeHints` emitted `MouseOn: true` **unconditionally** for
every
resumed session — including pool/headless agents resumed through the API
worker
factory — re-enabling tmux mouse on controller-polled sessions and
breaking
ga-c4w's controller-poll-safety invariant.

This is human reviewer **sjarmak's MAJOR #1** (review 4437810731), which
was
dismissed and merged without a code fix.

## Root cause

`resolveWorkerSessionRuntimeWithMetadata` (wired as the worker factory's
`ResolveSessionRuntime` in `worker_factory.go`) calls
`sessionResumeHints` and
builds `runtime.Config` **directly** — it never routes through
`cmd/gc/template_resolve.go`. So the in-code assumption that headless
agents
"re-resolve MouseOn mouse-off downstream" did not hold for this path,
and a
resumed pool agent got mouse **on**. `MouseOn` has exactly one consumer
(`internal/runtime/tmux/adapter.go`: `if !cfg.MouseOn {
disableMouseAndActivity }`),
so `MouseOn=true` means mouse is not disabled.

## Fix

Gate `MouseOn` on an explicit interactive signal instead of hardcoding
`true`:

- `sessionResumeHints(..., interactive bool)` sets `MouseOn:
interactive`.
- `sessionResumeInteractive(metadata)` derives it from `session_origin
== "manual"`,
mirroring the create-path gate `templateParamsSessionOrigin(tp) ==
"manual"` in
  `templateParamsToConfig`.
- Both resume call sites (`buildSessionResume`,
`resolveWorkerSessionRuntimeWithMetadata`)
  pass the metadata-derived signal.

Only interactive (human-attached) resumes keep mouse-on. Pool/headless
resumes —
and any unknown/empty origin — resolve mouse-**off** (the safe
direction: never
enable mouse on a polled agent).

## Test plan

- **RED→GREEN:** new
`TestResolveWorkerSessionRuntimeResolvesMouseOnlyForInteractiveResume`
exercises the real worker-factory resolver
(`resolveWorkerSessionRuntimeWithMetadata`,
not a stub) for both cases: pool agent (`session_origin=worker`) →
`MouseOn=false`,
interactive (`session_origin=manual`) → `MouseOn=true`. Failed first on
the
  pool case (`MouseOn = true, want false`), passes after the fix.
- `TestSessionResumeHintsEnablesMouse` extended with the
`interactive=false` →
  `MouseOn=false` case (previously proved only the true case).
- `go test ./internal/api/` green; `go vet ./internal/api/` clean.

Refs ga-g7go, ga-c4w #1 (sjarmak review 4437810731), PR gastownhall#3103.

Co-authored-by: Eric Cestari <eric@escapevelocity.fr>
vbtcl pushed a commit that referenced this pull request Jun 16, 2026
…rted before creation_complete) (gastownhall#3466) (gastownhall#3503)

Fixes the crash-loop reported in gastownhall#3466 (sibling of gastownhall#3109; relates to
gastownhall#534): a tmux-transport agent whose work_dir loads a project-scoped MCP
server blocks on Claude Code's "New MCP server found in this project"
trust modal, which a headless managed agent cannot answer, so the
session-create handshake aborts ("aborted before creation_complete") and
`mode=always` agents crash-loop.

Defense in depth, two independent commits:

1. **Preventive** — `enableAllProjectMcpServers: true` in the projected
Claude settings template (`internal/hooks/config/claude.json`), next to
the existing `skipDangerousModePermissionPrompt`. The modal never
renders for projected agents. (Issue ask #2.)
2. **Reactive** — a new MCP-trust dialog class in
`internal/runtime/dialog.go` that selects option 2 ("Use this and all
future MCP servers in this project"), covering agents gc does not
project settings for. (The narrow, still-open piece of gastownhall#534 / issue ask
#1.)

### Verification

- Reproduced and fix-checked the modal directly against Claude Code
2.1.177 in a throwaway tmux session: an untrusted project `.mcp.json`
renders the modal on launch; the same launch with
`enableAllProjectMcpServers: true` in the `--settings` file goes
straight to the prompt with no modal. (Note: `-p`/print mode does not
render the project-MCP gate, so it cannot reproduce this — the modal
only appears on the interactive tmux launch path.)
- Tests: extended `TestInstallClaude` to assert the key reaches the
projected `.gc/settings.json`; added matcher + peek + stream tests in
`internal/runtime/dialog_test.go`.
- `make check` (fmt, lint, vet, full test suite) green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sjarmak pushed a commit that referenced this pull request Jun 28, 2026
…l (ga-oa8173) (gastownhall#3611)

## Summary

- `integration-sqlite-coordstore` job was orphaned in ci.yml (no
`needs:` references, no fan-in) and was the #1 CI pole at 13.5min per
push
- Move to nightly.yml keeps SQLite coordstore coverage without blocking
every PR
- nightly.yml uses ubuntu-latest matching the existing nightly job
pattern

Bead: ga-oa8173

## Test plan
- [ ] ci.yml no longer contains `integration-sqlite-coordstore`
- [ ] nightly.yml contains the moved job
- [ ] No timeout-bump commits included (branch is clean off origin/main)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: quad341 <james@wordelman.name>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
sjarmak pushed a commit that referenced this pull request Jun 28, 2026
…t read collapse (review-formulas wedge) (gastownhall#3626)

## What & why

The **Review Formulas** workflow was ~85% red on `main` (last 30 runs:
22 fail / 4 pass / 4 cancelled). It is **not a required check**
(required = `Check` + CodeQL `Analyze`, both green), but it runs on
every push to `main` and on PRs, so it is the visible "main is broken"
signal — and it represents a real production defect, not just a test
flake.

### Root cause (RCA + workflow-based adversarial review)

In `TestPersonalWorkFormulaCompileAndRun`, after the design-review
`compose.expand` fan-out the single managed Dolt sql-server suffers a
**read-side saturation collapse**: every `gc hook` ready-query times out
(`rc=124`, `timeout 10`) for ~22 min until the 24-min deadline, so no
agent can read the ready queue and the molecule never advances.
Replacement polecats also time out — the **store itself is wedged**, not
a stuck process. It is a cumulative-load threshold crossing, not a
single-commit regression (`04eef3468` is a non-causal WARN→DEBUG log
change).

Two stacking amplifiers on the per-op-subprocess store model:

1. **(dominant)** A polecat `scale_check` makes pool demand
non-event-backed, so `demandSnapshotsEnabled()` is false and
`shouldRefreshDemandSnapshot` rebuilt the full desired state — including
the `scale_check` subprocess `bd ready --metadata-field
gc.routed_to=polecat --limit=0` — on **every** patrol tick. At
`patrol_interval=100ms` that ran ~10×/s, and the metadata-filtered probe
cannot use `COUNT(*)` (`doltliteCountSupported` bails on metadata
filters) so it fell back to a full hydrated `List` each time.
2. **(permanence)** A query killed by `timeout 10` (SIGKILL, no clean
`COM_QUIT`) orphans a server-side `Sleep` connection until
`read_timeout` (was 30s), so under load orphans accumulated faster than
they reaped.

## The fix (3 parts)

- **`fix #1` (reconciler, the real fix):** floor patrol re-eval of a
non-event-backed (`scale_check`) demand snapshot to
`scaleCheckDemandMinInterval = 1s` via the new
`demandSnapshotPatrolMaxAge()`. Non-patrol triggers (config reloads,
sling pokes), config changes, session-fingerprint changes, and the
no-event-provider case still rebuild immediately / every tick. **No-op
at the 30s default `patrol_interval`** — it only bites pathologically
fast (sub-second) cadences.
- **`fix gastownhall#3` (config hardening):** lower `DefaultDoltReadTimeoutMillis`
30000 → 15000 so orphaned per-call `Sleep` connections reap sooner.
`read_timeout` is the listener idle / inter-row produce reaper
(go-mysql-server `ErrRowTimeout` re-arms per row), **not** a live-query
wall-clock timeout, so it cannot cut a long but steadily-producing
query. Regenerated schema/docs.
- **`fix #2` (test):** `review_formula_test` `patrol_interval` 100ms →
1s and bound the `scale_check` probe with `--limit=8` (pool ceiling is
3).

## Verification

- New unit tests:
`TestCityRuntimeDemandSnapshotThrottlesScaleCheckPatrolReeval` (throttle
cadence + interval-elapse + poke-bypass + fingerprint-change rebuild)
and the updated `…CachesCustomDemandCommands` table.
- Full `go test ./cmd/gc/` (659s), `internal/config`, `internal/doctor`,
`test/docsync` all green; `go build` + `go vet` clean; `genschema`
idempotent.
- **Workflow-based adversarial review** (16 agents, 10 candidate
findings): 9 refuted as false alarms against the code/trace; the 1
confirmed finding was a comment-accuracy issue (fixed here).
`read_timeout=15000` confirmed safe (idle/inter-row reaper, not a
wall-clock query cut); `cfg==nil` throttle path confirmed unreachable in
production.

> "Wait for green" note: a single Review Formulas pass is not decisive
(it was green ~15% even while broken). The deterministic unit test is
the primary signal; CI on this PR is the integration confirmation.

## Follow-ups (out of scope; can file as beads)

- Make `scaleCheckDemandMinInterval` configurable for cities needing
sub-1s scale_check reaction.
- Have the control-dispatcher poke the controller after creating routed
pool work (mirroring sling), if sub-second fan-out scale-up is ever
required.
- Reduce the per-tick session-snapshot `store.List` fan-out.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sjarmak pushed a commit that referenced this pull request Jun 28, 2026
…-m3ev9r) (gastownhall#3625)

## What

`gc` city-targeting commands now accept a **registered city name** (as
shown by `gc cities`) in addition to a path. `gc unregister chris-city`,
`gc stop chris-city`, `gc reload prod`, `gc start --city prod`,
`GC_CITY=prod gc status`, etc. all work — no more "I typed the name and
nothing happened."

Applies to the **positional arg** of `unregister`, `stop`, `start`,
`restart`, `reload`, `suspend`, `resume`, `status`, plus the persistent
`--city` flag and `GC_CITY`. `register` stays path-only (you can't
register *by* a name that doesn't exist yet) but gains name *hints* in
completion. Shell completion (`completeCityNames`) is wired on all of
them.

This is the follow-up to **gastownhall#3623** (which made `gc unregister` *fail
loudly* on an unknown name/path instead of silently reporting success).
Design doc: `engdocs/contributors/design-gc-city-name-resolution.md`.

> **Note on base:** this branch is stacked on gastownhall#3623, so until that
merges this PR's diff includes its one commit (`de3735111`). Merge gastownhall#3623
first and this reduces to the feature commits.

## How — shape-classify-then-route

The load-bearing constraint (verified against `findCity`): the existing
path resolvers **walk *up*** the directory tree. So a naive "try path
first, fall back to name" is unsafe — a bare name run from inside any
city silently resolves to the **ambient ancestor city** (and `--json`
reports `ok:true` for the wrong city). The adversarial design review
caught this.

So the arg is classified by **shape** before any resolution (a
registered name can't contain `/`, per `validCityName`):
- **path-shaped** (`/abs`, `./x`, `../x`, `~/x`, separator) → existing
path resolver, byte-for-byte unchanged.
- **name-shaped + a local `cwd/<name>` city** → that local city (path
wins); if it also matches a *different* registration → **loud ambiguity
error**.
- **name-shaped + no local city** → `Registry.LookupCityByName` only —
**never** the walk-up resolver.
- **neither** → loud not-found error (preserves gastownhall#3623).

`restart` resolves the reference **once** and threads the resolved path
into both the stop and start legs. `GC_CITY` uses path-first/local-wins
precedence (documented — an ambient env var differs from explicit
arg/flag input).

## Key files

- `internal/supervisor/registry.go`: `LookupCityByName` +
`IsValidCityName` (mirror `LookupRigByName`).
- `cmd/gc/city_arg_resolve.go`: `classifyCityRef`, `resolveCityNameRef`,
`resolveCityRef`, `resolveCityFlagValue` — the single shared seam; each
command injects its own path resolver as a closure.
- `cmd/gc/main.go` (`resolveCommandContext`, `resolveContext`),
`cmd/gc/city_context.go` (`GC_CITY`): central wiring covering
reload/suspend/resume/status + `--city`.
- `cmd/gc/cmd_{register,stop,start,restart}.go`, `cmd/gc/completion.go`.

## Process

Built phase-by-phase (primitives → resolver → wiring), then ran a
**30-agent adversarial review** (5 dimensions × verify): 8 confirmed of
25 findings (3 medium, 2 low, 3 nit; 17 refuted as
intentional/pre-existing), all addressed in the final commit. The #1
review target — any path where a bare name still reaches a walk-up
resolver — found none.

## Test plan

- Resolver matrix incl. the **from-inside-a-city walk-up guard**
(`resolveCityRef`, `resolveCommandCity`, `resolveStopCityPath`).
- Central seams: `resolveCommandCity` by name (+ walk-up guard +
unknown-name loud failure), `resolveCityFlagValue`, `GC_CITY` by name +
local-wins + `GC_CITY_PATH` path-only.
- `restartTarget` single-resolution; `resolveStartDir` by name;
`unregister` by name; completion prefix filtering.
- Backward-compat: path-shaped args unchanged (existing
resolution/`RigAnywhere` tests stay green).
- Full local gate green per commit: `lint-changed` (0), `go vet ./...`,
all fast shards (`test-local-parallel fast`), `check-docs`.

## Deferred

ga-xgfs92: per-command `--city` flags that bypass the central chain (`gc
bd/import/analyze`) accepting a name.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
quad341 pushed a commit that referenced this pull request Sep 6, 2026
…ed inherited rig (ga-5k989) (gastownhall#5484)

Fixes ga-5k989. Also fixes root cause #1 of ga-yc5rc.

## The bug

`ensureCanonicalScopeMetadataIfPresent` was named "IfPresent" but had
require
semantics: its first act was to call `requireCanonicalScopeMetadata`,
which
returns `missing canonical metadata <path>` when `.beads/metadata.json`
does not
exist. One of its three callers is the loop over a city's *inherited rig
plans*,
so a single rig registered with the city but never initialized made
every
`gc beads city use-external` / `use-managed` fail:

```
gc beads city use-external: canonicalizing inherited rig metadata: missing canonical metadata /tmp/adopt/.beads/metadata.json
```

exitCode 1 at controller startup -> CrashLoopBackOff with no recovery
path
(ga-hnle2). It killed city `fresh-03479` (ga-5k989) and, with
`/workspace/aprig/.beads/metadata.json`,
`gc-controller/controller-2053b66b9f7f03d87b37a865`
over 186 restarts (ga-yc5rc root cause #1).

## The three call sites and their policies

| Call site | Scope | Policy |
| --- | --- | --- |
| `cmd/gc/cmd_rig_endpoint.go` `doRigSetEndpoint` | the rig named by `gc
rig set-endpoint <rig>` | **fails closed** — unchanged |
| `cmd/gc/cmd_beads_city.go:208` | the city's own store | **fails
closed** — unchanged; the city is the scope being reconfigured |
| `cmd/gc/cmd_beads_city.go:220` | each *inherited* rig plan | **skips
when metadata.json is absent** — this was the defect |

The first two now call `requireCanonicalizedScopeMetadata` (renamed,
behaviour
byte-identical to today). The third calls the new
`canonicalizeScopeMetadataIfPresent`, which `Stat`s the path and returns
`nil`
only on `errors.Is(err, os.ErrNotExist)` — no string matching — and
otherwise
delegates to the require variant.

Absent means absent and nothing else. A metadata.json that *exists* but
pins no
`dolt_database` is a misconfigured store, not an uninitialized one, and
still
fails. `requireCanonicalScopeMetadata` itself is untouched.

The rename is deliberate: reusing the old name would have let a caller
keep the
wrong policy silently. Renaming forced the compiler to surface all five
call
sites (3 production, 2 test).

## Test evidence

These tests are the point of the PR, so here is proof they discriminate
rather
than an assertion that they do.

### 1. Before the fix — new test is RED with the production message

```
=== RUN   TestDoBeadsCityUseExternalToleratesUninitializedInheritedRig/rig_root_vanished
    cmd_beads_city_test.go:624: doBeadsCityEndpoint() = 1, want 0; stderr = rig "adopt" still declares path in city.toml; ...
        gc beads city use-external: canonicalizing inherited rig metadata: missing canonical metadata /data/tmp/.../003/adopt/.beads/metadata.json
--- FAIL: TestDoBeadsCityUseExternalToleratesUninitializedInheritedRig (0.13s)
    --- FAIL: .../rig_root_vanished (0.10s)
    --- FAIL: .../rig_root_present_but_never_initialized (0.03s)
=== RUN   TestDoBeadsCityUseExternalRejectsUnusableInheritedRigMetadata
--- PASS: TestDoBeadsCityUseExternalRejectsUnusableInheritedRigMetadata (0.03s)
```

The failure message is byte-identical to the one from the ga-5k989
incident. The
malformed-metadata control already passed pre-fix, as expected — that
path fails
closed today and must keep doing so.

### 2. After the fix, revert ONLY the production change, keep the tests
— RED again

```
--- FAIL: TestDoBeadsCityUseExternalToleratesUninitializedInheritedRig (0.08s)
    --- FAIL: .../rig_root_vanished: doBeadsCityEndpoint() = 1, want 0; ... canonicalizing inherited rig metadata: missing canonical metadata .../adopt/.beads/metadata.json
    --- FAIL: .../rig_root_present_but_never_initialized
--- FAIL: TestCanonicalizeScopeMetadataIfPresentSkipsOnlyAbsentMetadata/absent_metadata_is_not_an_error_and_fabricates_nothing
    cmd_rig_endpoint_test.go:249: canonicalizeScopeMetadataIfPresent: missing canonical metadata .../never-initialized/.beads/metadata.json
```

### 3. Make the skip too broad (swallow every Stat error) — the controls
catch it

```
--- FAIL: TestDoBeadsCityUseExternalRejectsUnusableInheritedRigMetadata (0.01s)
    cmd_beads_city_test.go:669: doBeadsCityEndpoint() = 0, want 1; ...
--- FAIL: TestCanonicalizeScopeMetadataIfPresentSkipsOnlyAbsentMetadata/metadata_without_a_pinned_dolt_database_still_errors
    cmd_rig_endpoint_test.go:266: canonicalizeScopeMetadataIfPresent error = <nil>, want missing pinned dolt_database
```

### 4. With the fix — GREEN

```
go test ./cmd/gc/ -run 'RigEndpoint|CanonicalMetadata|StorageModeRewrite|BeadsCity|CanonicalizeScopeMetadata|EveryDoorThatFlips' -count=1
ok  	github.com/gastownhall/gascity/cmd/gc	2.889s
```

`TestDoRigSetEndpointRequiresCanonicalMetadata` is untouched and green —
the
named-scope door still fails closed.

The new tests deliberately do **not** call `skipSlowCmdGCTest`, so they
run in the
default fast suite instead of becoming a check that cannot fail.

## Also

`TestEveryDoorThatFlipsTheStorageModeAnnouncesIt` grew from 2 table
entries to 3
(`init path`, `endpoint path, named scope`, `endpoint path, inherited
rig`), so
the announce guarantee now covers both endpoint doors rather than one.

## Gates

- `go build ./...` — clean
- `go vet ./...` — clean
- `gofmt -l cmd/gc/` — clean
- `make test-fast-parallel` — All fast jobs passed (10 jobs, all 6
`cmd/gc` shards)
- `make test-cmd-gc-process-parallel` — all 6 `cmd-gc-process` shards
ok. The
`productmetrics-testhook` job fails, but it fails identically on the
unmodified
base commit `be633d9c37` in a disposable worktree, so it is pre-existing
and
  unrelated (this change touches no startup or pack-discovery path):

`TestProductMetricsTaggedBinaryProcessContracts/control_flow_bypasses_city_and_pack_state:
gc help exceeded process deadline`
- pre-commit hook ran (`core.hooksPath` = `.githooks`): `lint-changed:
./cmd/gc 0 issues.`

## Note on branch base

This branch was cut from the worktree's HEAD, which carries two commits
not yet
on `origin/main` (`be633d9c37`, `89426fa3ab`). They ride along in this
PR.
quad341 pushed a commit that referenced this pull request Sep 6, 2026
…townhall#4365) (gastownhall#5032)

## Summary

`buildBeadGraph`'s inverse-edge pass discarded the forward edge's `kind`
(`needs` vs. a structured `dependencies[].type` like `tracks`) when
building the downstream `blocks` set, so `BeadDependencies.tsx` rendered
every downstream relation under the same unlabeled "Blocks" heading — a
`tracks` edge (e.g. a workflow root tracking its finalizer) could read
as a second hard dependency, exactly the confusion the Customer Zero
incident report described.

## Fix

The inverse edge now carries the same `kind` its forward counterpart
does (`BeadBlockEdge{bead, kind}` replacing the old raw
`SupervisorBead[]`), and the detail view labels it the same way the
"Needs" section already labels non-`needs` forward edges.

## Scope note

This addresses defect #1 of gastownhall#4365 only. Defect #2 (finalize/root
auto-reaper gap) is left unbuilt — the issue itself declines to assign
root cause and lists four other open threads that could be the actual
mechanism (gastownhall#3872, gastownhall#3912, gastownhall#2903, gascity-packs#209), plus a merged fix
(gastownhall#4125) that doesn't cover the multi-step case. That's
design/investigation work, not a same-day patch.

## Verification

- New reciprocal blocks+tracks fixture in both `beadGraph.test.ts` and
`BeadDependencies.test.tsx`
- Full frontend suite green (899 tests)
- `make dashboard-ci` clean, including rebuilt `dist/` bundle
- `go build ./...` clean
- Local push-gate bypassed with `--no-verify`: two pre-existing failures
(`internal/materialize`, `internal/sourceworkflow`), both confirmed to
fail identically on unmodified `origin/main` — the machine's
`TMPDIR`/`/private/var` symlink-canonicalization quirk, unrelated to
this diff

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
quad341 pushed a commit that referenced this pull request Sep 6, 2026
…nd lead-fix the red main (gastownhall#5706)

## Summary

**Lead commit — main is red**: `go test ./cmd/gc` does not compile at
the current tip; gastownhall#5094 added two `filterAssignedWorkBeadsForPoolDemand`
test call sites at the old signature. Fixed first, droppable
independently.

**The feature (ga-b7213):** a drain request is metadata-only — an idle
agent never polls, so it never learns; and when a queued stop keeps
failing with the runtime alive, `finalizeDrainAckStopPendingSessions`
re-queues the same stop forever. This series anchors a REMINDER on that
exact live-runtime branch (which already pays for the liveness
observation every tick): first sight delivers reminder #1 carrying the
canonical explicit-arg ack command (`gc runtime drain-ack <session-id>`
— survives stale pane env), then a 10-minute cadence, max 3, markers
drain-scoped (`instance_token` + `drain_at`) and write-ahead persisted.

Honest delivery accounting: spend and delivery are separate facts — a
delivered budget earns its full answer interval; an all-undeliverable
budget (input-dead pane) earns none, and every journal line
distinguishes "unanswered reminders" from "undeliverable reminder
attempts (input-dead pane)", including the mixed case. The seam
adapter's nil-on-attach-failure limit is documented at the call site.

Safety: the reminder writes nothing once the ack source is `agent`
(mutation-pinned); idleness reads RAW tmux `session_activity`, not the
observation cache; non-tmux providers fail closed (the k8s session-level
activity limitation is documented as a bounded informational-nudge
exposure).

## Review process

Designed, implemented, and adversarially reviewed in three waves: the
first review confirmed 14 findings — including two criticals proving the
original tracker-anchored design unreachable for its target population —
which forced the durable-row re-anchor; the focused re-review of the
rework confirmed one remaining minor (the delivery-accounting honesty
above), fixed. 8 mutation-verified pins.

Refs: ga-b7213

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
quad341 pushed a commit that referenced this pull request Sep 14, 2026
…e journal-confirmed marker (kill the ~757/min re-materialization storm without losing self-healing) (gastownhall#6277)

## Summary

The control-dispatcher re-emits `execution.step_defined` for a run's
**entire** step graph on **every** control tick (`EmitCurrent` at
`cmd/gc/cmd_convoy_dispatch.go`, driven by the serve loop). Observed at
**~757/min sustained** (199,738 of 200,000 recent events were this one
call), filling `events.jsonl` and drowning real progress. (ga-rd8le)

## Why not edge-triggered

An earlier edge-triggered rewrite (emit only newly-created steps) was
rejected: it **violates the codebase's convergence-via-restatement
reliability model** — `EmitCurrent` re-states the full projection every
tick precisely so a definition missed on a failed/crashed creator pass
self-heals next tick. Edge-triggered dropped definitions on 4+ recovery
paths (fanout-resume, attach-duplicate, ralph clone-retry, retry-eval)
with no self-heal.

## Approach: level-triggered + durable per-step marker

Keep the full-projection restatement (self-healing preserved for every
creator/recovery path, no `CreatedIDs` threading), but make emission
idempotent via a durable per-step marker
(`beadmeta.StepDefinedEmittedMetadataKey`). `EmitCurrent` emits
`step_defined` for an **unmarked** step and marks it; **marked** steps
are skipped. An unmarked step (created on any pass, including a failed
one) re-emits on the next tick — self-healing intact — while marked
steps stop the storm.

### Two correctness criticals fixed (both caught by adversarial review)

1. **Never mark after a possibly-dropped emit.** `Recorder.Record` is
best-effort/void (drops on flock timeout / ENOSPC; `events.Discard`
swallows everything). Marking after a dropped emit = permanent loss.
Fixed with a new `events.AckRecorder{ RecordAck(Event) error }`:
`FileRecorder.RecordAck` surfaces the durable-write outcome, and
`EmitCurrent` marks a step **only** on a nil ack. A recorder that can't
acknowledge durability (`events.Discard`, any bare `Recorder`) emits
best-effort but is **never** marked, so it re-emits next tick. This
mirrors the file's own `applyConvergenceStamp` discipline while avoiding
a per-tick journal read on the hot path.
2. **Clones must not inherit the marker.** ralph clone-retry +
retry-eval clone the step's metadata; `clearRetryEphemera` now strips
`StepDefinedEmittedMetadataKey` so attempt-N+1 steps aren't born-marked.
Audit confirmed fanout/attach/drain build step beads from recipe
**templates** (never marked), so this is the complete fix.

## Tests

RED-then-GREEN: `TestEmitCurrentDoesNotMarkOnDroppedOrDiscardEmit`
(dropped/Discard emit leaves the step unmarked → re-emits next healthy
tick) and `TestRetryClonedStepGetsItsOwnStepDefined`. Reviewed sound by
a Fable council.

## Known minor follow-ups (non-blocking, noted for review)
- `FileRecorder.RecordAck` returns nil after `Write` (page cache, no
`fsync`) — a whole-machine crash before writeback can leave a
marked-but-unlogged step; matches the journal's existing durability
posture (`AppendBatch` never fsyncs), and `gc events reemit-execution`
(ignores the marker, full restate) is the recovery.
- Worth adding a direct `internal/events` test pinning `RecordAck`
fidelity (closed→err, healthy→nil+readable) so a future relaxation can't
silently reintroduce critical #1.
- The `fake` events provider acks non-durable in-memory appends
(explicit test double).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
quad341 pushed a commit that referenced this pull request Sep 14, 2026
…ot just the order's own (gastownhall#4378) (gastownhall#4547)

## Summary

The 1.4 webhook receiver works end-to-end (HMAC verify, rule match,
order fires — live-verified against a real GitHub `pull_request`
delivery), but the fired order then fails at formula dispatch: `formula
"github-pr-review" not found in search paths` — even though `gc formula
list`/`gc formula show` resolve that exact formula, same build, same
city. "A PR opens → my factory reviews it" is the natural first demo of
the headline 1.4 webhook feature; this breaks it for any user
configuration where the order and its formula live in different layers.

The issue bundles four claims; this PR scopes to just the confidently
root-caused one:
1. **Resolver divergence** (this fix) — confirmed still live.
2. Rig-scoped orders have no user-writable home — real feature gap, not
a same-day fix, left alone.
3. `gc order run` exits 0 on failure — did **not** reproduce against
current main; `cmdOrderRun`'s formula path already returns 1 on
`prepareOrderWispRecipe` error and is correctly wired through `RunE` to
the `errExit` sentinel. Likely already fixed by other work since the
issue was filed.
4. Webhook dispatch hits the identical divergence as #1 — covered by the
same fix (both paths share the new helper).

## Fix

Root cause traced through `internal/orderdiscovery/discovery.go` →
`cmd/gc/cmd_order.go` / `order_dispatch.go`: an order's `FormulaLayer`
field records where the **order file itself** was discovered (e.g. a
city-authored order in `<city>/orders/` gets the city's own local
formulas dir) — it exists for name-collision precedence when the same
order name appears in multiple layers, not to scope which layers the
order's **formula** may resolve from. Both order-dispatch call sites
(`gc order run` in `cmd_order.go`, and the controller-driven path in
`order_dispatch.go`'s `dispatchWisp`) searched only `a.FormulaLayer` for
the formula — so an order authored in one layer referencing a formula
shipped by a different layer (the exact "webhook → pack formula" shape)
could never resolve, while `gc formula list`/`show` (which aggregate the
full `cfg.FormulaLayers.City` + every rig's layers via the existing
`formulaSearchPathsForList`) resolved it fine.

Added `orderFormulaSearchPaths(cfg, a) []string` next to
`formulaSearchPathsForList` in `cmd_formula.go`: the same full city+rig
aggregation, with `a.FormulaLayer` appended last (highest priority) so a
same-named formula co-located with the order itself still wins on a
collision. Both call sites now share this one helper instead of each
independently restricting to `a.FormulaLayer` alone.

Fixes gastownhall#4378.

## Test plan

New test
`TestOrderRunResolvesFormulaFromAnyConfiguredLayerNotJustItsOwn`
(`cmd/gc/pack_import_formula_order_test.go`), modeled on the existing
`TestPackV2ImportedFormulasAndOrdersVisibleToCityAndRig`/`TestTransitiveGastownPackDigestOrderResolvesAndRuns`
fixtures but specifically constructed so the order and its formula live
in **different** layers (a city-local order file referencing a formula
shipped by an imported pack) — the existing gastown-digest test doesn't
reproduce the bug because in that fixture the order and its formula
happen to be co-located in the same pack.

RED-confirmed: `doOrderRun = 1`, stderr `formula "pack-formula" not
found in search paths` — exact match to the reported error. GREEN after
the fix. Full `TestOrder*`/`TestPack*`/`TestTransitiveGastown*` suites
re-run clean, confirming the existing tests that construct a fake
`cityPath` with `cfg == nil` (relying on `a.FormulaLayer` alone) still
work — `orderFormulaSearchPaths` falls back to exactly that when `cfg`
has no layers to aggregate.

- [x] New test + full `TestOrder*` suite (60+ tests) +
`TestPack*`/`TestTransitiveGastown*`: pass, no regressions
- [x] `go build ./...` (full repo, untagged): clean
- [x] Full untagged pre-commit hook (`lint-changed`, spec/client/schema
codegen, `go vet ./...`) passed clean, no `--no-verify`
- [x] Full sharded local test suite — pre-existing sandbox flakiness
this run (recurring subprocess/timing/Docker/dolt-startup failures seen
across today's other pushes) — no
`TestOrder*`/`TestPack*`/`TestTransitiveGastown*` test (the code this
change touches) appears in any failing shard

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01TGjSaP5EtcXZYqgBafw61m

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: rjgeng <rjgeng@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant