Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
6a14d9f
fix(gc): restore storage binding dispatch (ga-w9wu4.1)
julianknutsen Aug 4, 2026
01e70f1
Merge pull request #4980 from gastownhall/fix/ga-w9wu4-1-storage-binding
julianknutsen Aug 4, 2026
b1406d7
fix(lint): keep module checksums read-only
julianknutsen Aug 4, 2026
2361fad
fix(lint): fail closed on unresolved changed packages
julianknutsen Aug 4, 2026
79fb208
fix(hook): rank cross-store hook discovery by tier+priority instead o…
jacobhausler Aug 4, 2026
49aec96
Merge pull request #4982 from gastownhall/fix/ga-w9wu4-2-clean-promotion
julianknutsen Aug 4, 2026
b4eb670
fix(version): preserve SemVer build metadata in normalizeVersion (#4757)
jacobhausler Aug 4, 2026
2dcc735
fix: keep quality gates module readonly
julianknutsen Aug 4, 2026
ed947cd
Merge pull request #4985 from gastownhall/fix/ga-w9wu4-quality-goflags
julianknutsen Aug 4, 2026
c1ed8f9
fix(reconciler): gate WakeWork's assigned-work cause on the same bloc…
jacobhausler Aug 4, 2026
7197644
fix: skip managed Dolt for storage bindings
julianknutsen Aug 4, 2026
2c99b57
fix(hooks): stop build_desired_state staging from double-writing reco…
wbern Aug 4, 2026
24fe9aa
Merge pull request #4986 from gastownhall/fix/ga-w9wu4-storage-bindin…
julianknutsen Aug 4, 2026
97bdcbe
fix(session): persist claude resume key from its SessionStart hook (#…
wbern Aug 4, 2026
cd2aeaf
feat(bd): refuse a `gc bd update` whose --set-metadata pairs bd would…
wbern Aug 4, 2026
237386a
fix(orders): persist renudge-stale-human-gates ledger per send, not o…
jacobhausler Aug 4, 2026
a585e07
fix(session): exclude session's own mol-do-work drain step from the c…
jacobhausler Aug 4, 2026
8038caf
fix(sling): restamp gc.routed_to on formula-attach and disclose it in…
jacobhausler Jul 28, 2026
d1e9584
test(sling): red — regression coverage for #4763 fix plan (refs ga-f4…
Aug 4, 2026
23f6f1a
fix(sling): fix default-formula skip message and scope dry-run wisp-r…
Aug 4, 2026
a0ef020
fix(metrics): try the free uploader lock before opening the contentio…
julianknutsen Aug 4, 2026
4c64f32
fix(convergence): preserve GC_HOME for gate subprocesses (#4992)
julianknutsen Aug 4, 2026
13362a5
test(sling): drop legacy formulatest coupling from graph.v2 dry-run t…
Aug 4, 2026
18094cc
feat(orders): guard bulk order-tracking deletion (#4958)
sjarmak Aug 4, 2026
ad4d0ab
fix(usage): sweep model usage from live sessions, not only at retirem…
julianknutsen Aug 4, 2026
f3d1d70
chore: release gate PASS for sling formula-attach routing fix
Aug 4, 2026
3db4ed2
merge(sling): formula-attach execution routing metadata (ga-mwrstg)
quad341 Aug 4, 2026
f834897
fix(dispatch): fold typed coordinator outcome instead of retrying it
test22345 Aug 2, 2026
f04a0f5
fix(dispatch): harden typed deliverable-close validation (review P1/P2)
test22345 Aug 2, 2026
7f4dd78
fix(dispatch): reject trailing data after the typed-close envelope
test22345 Aug 2, 2026
696cece
refactor(dispatch): simplify typed-close recovery
test22345 Aug 5, 2026
84464bd
fix(dispatch): validate typed-close passing verdicts
test22345 Aug 5, 2026
5dc5897
Respect hold:mayor in control-ready pool routing (#4787)
quad341 Aug 5, 2026
2236676
Merge pull request #5014 from gastownhall/recovery/gc-e2xqk-20260804
sjarmak Aug 5, 2026
2e1a9cf
fix(session): align bead actor with canonical alias (#4981)
A3Ackerman Aug 5, 2026
2ea5c3c
fix(session): let a live singleton pool session reclaim its alias fro…
jacobhausler Aug 5, 2026
a48bce4
fix(dispatch): retry temporarily blocked workflow finalize (#5020)
sjarmak Aug 5, 2026
cb6560d
fix(mail): preserve typed session IDs (#5008)
test22345 Aug 5, 2026
141573f
feat(order): bound gc order history with --limit/--since (+ order-fir…
brandonmartin Aug 5, 2026
3166fe6
fix(maintenance): run disk pre-flight before snapshot in the store-ma…
brandonmartin Aug 5, 2026
55005d7
fix(builtin/claude): map auto-edit to acceptEdits for current Claude …
vishnujayvel Aug 5, 2026
08b2c75
feat(events): publish native step lifecycle facts (#5022)
julianknutsen Aug 5, 2026
bc7342d
test(productmetrics): stabilize DisableAndPurge races under -p=N load…
vishnujayvel Aug 5, 2026
94baf2d
Merge pull request #5023 from gastownhall/fix/5008-preserve-mail-sess…
sjarmak Aug 5, 2026
52d1f86
fix: upgrade Beads schema catalog
julianknutsen Aug 5, 2026
9f2e1a1
fix: preserve Beads v59 native schema
julianknutsen Aug 5, 2026
e938a19
fix(controller): close terminal workflow residue (#5026)
sjarmak Aug 5, 2026
4833948
fix: drop stale gc x/net waivers
julianknutsen Aug 5, 2026
cfee938
fix: NudgeSession must not discard the confirmed bool from submitEnte…
jacobhausler Aug 5, 2026
2e8fe2f
fix: align CI with pinned Beads source
julianknutsen Aug 5, 2026
b893763
fix: resolve integration Beads version from go.mod
julianknutsen Aug 5, 2026
d71338b
feat(nudge): make the tmux submit-key sequence declarative per provid…
jacobhausler Aug 5, 2026
96d7b2a
fix(supervisor): back off structural init failures far longer than tr…
rjgeng Aug 5, 2026
824d4ea
fix: waive kubectl x/text vulnerability
julianknutsen Aug 5, 2026
c3c006c
fix(orders): drop open-work gate for gate-less cooldown probes (NoWor…
bourgois Aug 5, 2026
7cd9986
fix: keep CI Beads harness version-aligned
julianknutsen Aug 5, 2026
99a3606
fix: update MCP mail image security floors
julianknutsen Aug 5, 2026
f4546f1
Merge pull request #5030 from gastownhall/fix/beads-v59-native-store
julianknutsen Aug 5, 2026
3f4173e
fix(runtime): refresh demand snapshots for routed work (#3667)
duncan4123 Aug 5, 2026
8500b9b
fix(events): reconcile graph step completions on patrol
julianknutsen Aug 5, 2026
cc036a7
fix(runtime): add missing hash/fnv import to city_runtime.go (main is…
remuscazacu Aug 5, 2026
ff376df
Merge pull request #5039 from gastownhall/fix/ga-usd3k-step-completed
julianknutsen Aug 5, 2026
6936579
fix(events): reconcile rig graph completions
julianknutsen Aug 5, 2026
69161ac
Merge pull request #5040 from gastownhall/fix/ga-step-completed-rig-s…
julianknutsen Aug 5, 2026
5069b81
fix(events): batch completion reconciliation facts
julianknutsen Aug 5, 2026
b19a3da
Merge pull request #5041 from gastownhall/fix/ga-completion-reconcile…
julianknutsen Aug 5, 2026
d18328c
fix(events): preserve completion reconciliation through rotation
julianknutsen Aug 5, 2026
4893092
Merge pull request #5043 from gastownhall/fix/completion-reconcile-ar…
julianknutsen Aug 5, 2026
d39b855
Merge upstream/main into fork — 2026-08-06 resync (68 commits)
bourgois Aug 6, 2026
09bcdcb
fix(resync): repair merge-resolution defects found by review + CI
bourgois Aug 6, 2026
e1090b1
chore(security): extend .trivyignore review horizon to 2026-09-07
bourgois Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,8 @@ __pycache__/
# Beads / Dolt files (added by bd init)
.beads/proxieddb/
.beads.backup-*

# oh-my-claudecode operational session state (never repo content;
# .omc/skills/ is the sole committable exception per OMC conventions)
.omc/*
!.omc/skills/
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ version: "2"
# self-deadlock where the commit that adds this very setting was blocked by it.
run:
allow-parallel-runners: true
modules-download-mode: readonly

severity:
default: error
Expand Down
95 changes: 46 additions & 49 deletions .trivyignore.yaml

Large diffs are not rendered by default.

31 changes: 19 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ LINT_BASE ?= origin/main
LINT_CHANGED_REF ?= HEAD
LINT_CHANGED_SCOPE ?= worktree
LINT_FLAGS ?=
QUALITY_GATE_GOFLAGS = $$(go env GOFLAGS | sed -E 's/(^|[[:space:]])-mod=[^[:space:]]+//g') -mod=readonly
CI_STATIC_SELECT := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))scripts/ci-static-select
CI_STATIC_GO ?= go

Expand All @@ -311,15 +312,16 @@ lint: lint-full

## lint-full: run golangci-lint across all packages
lint-full: $(GOLANGCI_LINT)
$(GOLANGCI_LINT) run $(LINT_FLAGS) ./...
GOFLAGS="$(QUALITY_GATE_GOFLAGS)" $(GOLANGCI_LINT) run $(LINT_FLAGS) ./...

## lint-new: run golangci-lint for issues introduced since LINT_BASE
lint-new: $(GOLANGCI_LINT)
$(GOLANGCI_LINT) run $(LINT_FLAGS) --new-from-merge-base=$(LINT_BASE) --whole-files ./...
GOFLAGS="$(QUALITY_GATE_GOFLAGS)" $(GOLANGCI_LINT) run $(LINT_FLAGS) --new-from-merge-base=$(LINT_BASE) --whole-files ./...

## lint-changed: run golangci-lint only for packages touched by changed Go files
lint-changed: $(GOLANGCI_LINT)
@case "$(LINT_CHANGED_SCOPE)" in \
@export GOFLAGS="$(QUALITY_GATE_GOFLAGS)"; \
case "$(LINT_CHANGED_SCOPE)" in \
staged) \
files="$$(git diff --cached --name-only --diff-filter=ACMRT -- '*.go')"; \
;; \
Expand All @@ -342,10 +344,15 @@ lint-changed: $(GOLANGCI_LINT)
echo "lint-changed: no changed Go files"; \
exit 0; \
fi; \
pkgs="$$(printf '%s\n' "$$files" | sed '/^$$/d' | sort -u | while IFS= read -r file; do dirname "$$file"; done | sort -u | while IFS= read -r dir; do \
dirs="$$(printf '%s\n' "$$files" | sed '/^$$/d' | sort -u | while IFS= read -r file; do dirname "$$file"; done | sort -u)"; \
pkgs="$$(for dir in $$dirs; do \
if [ "$$dir" = "." ]; then pkg="."; else pkg="./$$dir"; fi; \
if go list "$$pkg" >/dev/null 2>&1; then printf '%s\n' "$$pkg"; fi; \
done | sort -u)"; \
if ! go list "$$pkg" >/dev/null; then \
echo "lint-changed: unable to load $$pkg" >&2; \
exit 1; \
fi; \
printf '%s\n' "$$pkg"; \
done)" || exit $$?; \
if [ -z "$$pkgs" ]; then \
echo "lint-changed: no lintable Go packages"; \
exit 0; \
Expand All @@ -355,23 +362,23 @@ lint-changed: $(GOLANGCI_LINT)

## lint-affected: lint packages affected by changed Go build inputs or embedded files
lint-affected: $(GOLANGCI_LINT)
@"$(CI_STATIC_SELECT)" lint-affected "$(GOLANGCI_LINT)" "$(CI_STATIC_GO)" $(LINT_FLAGS)
@GOFLAGS="$(QUALITY_GATE_GOFLAGS)" "$(CI_STATIC_SELECT)" lint-affected "$(GOLANGCI_LINT)" "$(CI_STATIC_GO)" $(LINT_FLAGS)

## fmt-check: fail if formatting would change files
fmt-check: $(GOLANGCI_LINT)
$(GOLANGCI_LINT) fmt --diff ./...
GOFLAGS="$(QUALITY_GATE_GOFLAGS)" $(GOLANGCI_LINT) fmt --diff ./...

## fmt-check-changed: fail if formatting would change a regular changed Go file
fmt-check-changed: $(GOLANGCI_LINT)
@"$(CI_STATIC_SELECT)" fmt-check-changed "$(GOLANGCI_LINT)"
@GOFLAGS="$(QUALITY_GATE_GOFLAGS)" "$(CI_STATIC_SELECT)" fmt-check-changed "$(GOLANGCI_LINT)"

## fmt: auto-fix formatting
fmt: $(GOLANGCI_LINT)
$(GOLANGCI_LINT) fmt ./...

## vet: run go vet
vet:
go vet ./...
GOFLAGS="$(QUALITY_GATE_GOFLAGS)" go vet ./...

## TEST_ENV: env -i wrapper for `go test` invocations. Strips host env so
## agent-session vars (GC_CITY, GC_HOME, GC_SESSION_ID, ...) cannot leak into
Expand Down Expand Up @@ -450,7 +457,7 @@ test-ci-policy:
## cache input hashes over local working files.
## Wrapped in $(TEST_ENV) — see comment above for why.
test: test-fsys-darwin-compile
$(TEST_ENV) GC_FAST_UNIT=1 scripts/go-test-observable test -- -p=4 -count=1 -timeout 15m ./...
$(TEST_ENV) GOFLAGS="$(QUALITY_GATE_GOFLAGS)" GC_FAST_UNIT=1 scripts/go-test-observable test -- -p=4 -count=1 -timeout 15m ./...

# MAC_UNIT_PKGS excludes cmd/gc from the Mac unit sweep; cmd/gc runs
# sharded via the mac-cmd-gc-process CI matrix job instead.
Expand All @@ -471,7 +478,7 @@ test-fast-parallel:
test-fsys-darwin-compile:
@tmp=$$(mktemp -d); \
trap 'rm -rf "$$tmp"' EXIT; \
$(TEST_ENV) GOOS=darwin GOARCH=arm64 go test -c -o "$$tmp/fsys.test" ./internal/fsys
$(TEST_ENV) GOFLAGS="$(QUALITY_GATE_GOFLAGS)" GOOS=darwin GOARCH=arm64 go test -c -o "$$tmp/fsys.test" ./internal/fsys

## test-pack-registry-live: run the opt-in gascity-packs registry canary
test-pack-registry-live:
Expand Down
8 changes: 4 additions & 4 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,9 +451,9 @@ all-source audit while staying outside untagged and Small debt.
<!-- BEGIN CHECKED TEST RESOURCE LEDGER -->
| Ledger kind | Source scope | Resource baseline | Tracking owner | Invariant / resource owner | Migration | Expiry |
| --- | --- | --- | --- | --- | --- | --- |
| Audit baseline | all tracked test source | fixed_sleep: 432 calls / 160 files (historical regex census: 447 / 157) | ga-80po0c.2 | tracked test source totals remain visible as audit evidence; ga-80po0c.2 owns this point-in-time source census | P0.4a | 2026-10-01 |
| Audit baseline | all tracked test source | fixed_sleep: 437 calls / 161 files (historical regex census: 447 / 157) | ga-80po0c.2 | tracked test source totals remain visible as audit evidence; ga-80po0c.2 owns this point-in-time source census | P0.4a | 2026-10-01 |
| Audit baseline | all tracked test source | listener_helper: 58 calls / 23 files | ga-80po0c.2.2.3 | all-source listener-helper call/file totals cannot drift without an explicit checked policy update; ga-80po0c.2.2.3 owns this all-source audit; tagged calls stay Large and receive no Medium exemption | P0.4c-listener-helper | 2026-10-01 |
| Audit baseline | all tracked test source | subprocess: 552 calls / 168 files (historical regex census: 495 / 135) | ga-80po0c.2 | tracked test source totals remain visible as audit evidence; ga-80po0c.2 owns this point-in-time source census | P0.4a | 2026-10-01 |
| Audit baseline | all tracked test source | subprocess: 555 calls / 168 files (historical regex census: 495 / 135) | ga-80po0c.2 | tracked test source totals remain visible as audit evidence; ga-80po0c.2 owns this point-in-time source census | P0.4a | 2026-10-01 |
| Medium owner | `cmd/gc` package `main` | TestMain: environment, tmux | ga-80po0c.2.1 | cmd/gc TestMain is the checked package-level Medium owner for process environment and tmux namespace setup; only declared environment and tmux calls lexically inside TestMain leave Small debt | P0.4b/P0.4c-tmux | 2026-10-01 |
| Medium owner | `internal/api` package `api` | TestEveryEmittedErrorCodeIsRegistered: subprocess | ga-80po0c.2.1 | internal/api tracked-source error URN guard is a checked Medium owner; only the git ls-files call lexically inside TestEveryEmittedErrorCodeIsRegistered leaves Small debt | P0.4b | 2026-10-01 |
| Medium owner | `internal/doctor` package `doctor` | TestCustomTypesCheck_TableDrift: subprocess | ga-80po0c.2.1 | doctor custom-types config-CSV-vs-table drift detect+heal proof is a checked Medium owner; the bd and dolt subprocesses are confined to TestCustomTypesCheck_TableDrift, which manufactures and heals real table drift against a throwaway store | P0.4b | 2026-10-01 |
Expand All @@ -472,7 +472,7 @@ all-source audit while staying outside untagged and Small debt.
| Small debt ratchet | all untagged test source | net_listen: 93 calls / 35 files (historical regex census: 92 / 34) | ga-80po0c.2.2.2 | untagged Small stream-listener call/file totals cannot grow; reductions must lower this baseline; non-Medium lexical owners move stream-listener tests to exact Medium ownership or replace the listener | P0.4c-listener | 2026-10-01 |
| Small debt ratchet | all untagged test source | net_listen_config: 1 calls / 1 files | ga-80po0c.2.2.2 | untagged Small net.ListenConfig listener call/file totals cannot grow; reductions must lower this baseline; non-Medium lexical owners move ListenConfig-backed tests to exact Medium ownership or replace the listener | P0.4c-listener | 2026-10-01 |
| Small debt ratchet | all untagged test source | net_listen_packet: 3 calls / 2 files | ga-80po0c.2.2.2 | untagged Small packet-listener call/file totals cannot grow; reductions must lower this baseline; non-Medium lexical owners move packet-listener tests to exact Medium ownership or replace the listener | P0.4c-listener | 2026-10-01 |
| Small debt ratchet | all untagged test source | subprocess: 407 calls / 114 files (historical regex census: 394 / 105) | ga-80po0c.2.1 | untagged Small subprocess call/file totals cannot grow; reductions must lower this baseline; non-Medium lexical owners remove or replace each process call site | D1/D2/D5/D6/E6 | 2026-10-01 |
| Small debt ratchet | all untagged test source | subprocess: 408 calls / 114 files (historical regex census: 394 / 105) | ga-80po0c.2.1 | untagged Small subprocess call/file totals cannot grow; reductions must lower this baseline; non-Medium lexical owners remove or replace each process call site | D1/D2/D5/D6/E6 | 2026-10-01 |
| Small debt ratchet | all untagged test source | syscall_listen: 1 calls / 1 files | ga-80po0c.2.2 | untagged Small syscall.Listen call/file totals cannot grow; reductions must lower this baseline; non-Medium lexical owners move syscall-backed listener tests to exact Medium ownership or replace the listener | P0.4c | 2026-10-01 |
| Small debt ratchet | all untagged test source | tmux: 0 calls / 0 files | ga-80po0c.2.2.1 | untagged Small tmux dependency call/file totals cannot grow; reductions must lower this baseline; non-Medium lexical owners replace tmux with a fake executor or declare exact isolated ownership | P0.4c-tmux | 2026-10-01 |
| Source debt ratchet | `cmd/gc` untagged test source | cwd: 174 calls / 16 files (historical regex census: 98 / 13) | ga-80po0c.2.3 | untagged cmd/gc cwd call/file totals cannot grow; reductions must lower this baseline; cmd/gc callers restore or eliminate every recognized cwd mutation | D5/D6 | 2026-10-01 |
Expand All @@ -484,7 +484,7 @@ all-source audit while staying outside untagged and Small debt.
| Source debt ratchet | all untagged test source | net_listen: 95 calls / 36 files (historical regex census: 92 / 34) | ga-80po0c.2.2.2 | untagged stream-listener call/file totals cannot grow; reductions must lower this baseline; each owning test closes its stream listener and removes duplicate listener-backed coverage | P0.4c-listener | 2026-10-01 |
| Source debt ratchet | all untagged test source | net_listen_config: 1 calls / 1 files | ga-80po0c.2.2.2 | untagged net.ListenConfig listener call/file totals cannot grow; reductions must lower this baseline; each owning test closes its configured listener and removes duplicate listener-backed coverage | P0.4c-listener | 2026-10-01 |
| Source debt ratchet | all untagged test source | net_listen_packet: 3 calls / 2 files | ga-80po0c.2.2.2 | untagged packet-listener call/file totals cannot grow; reductions must lower this baseline; each owning test closes its packet listener and removes duplicate listener-backed coverage | P0.4c-listener | 2026-10-01 |
| Source debt ratchet | all untagged test source | subprocess: 413 calls / 117 files (historical regex census: 380 / 98) | ga-80po0c.2 | untagged subprocess call/file totals cannot grow; reductions must lower this baseline; each process-owning test removes or replaces its source call site | D1/D2/D5/D6/E6 | 2026-10-01 |
| Source debt ratchet | all untagged test source | subprocess: 414 calls / 117 files (historical regex census: 380 / 98) | ga-80po0c.2 | untagged subprocess call/file totals cannot grow; reductions must lower this baseline; each process-owning test removes or replaces its source call site | D1/D2/D5/D6/E6 | 2026-10-01 |
| Source debt ratchet | all untagged test source | syscall_listen: 1 calls / 1 files | ga-80po0c.2.2 | untagged syscall.Listen call/file totals cannot grow; reductions must lower this baseline; each owning test closes its listening file descriptor and removes duplicate listener-backed coverage | P0.4c | 2026-10-01 |
| Source debt ratchet | all untagged test source | tmux: 6 calls / 2 files | ga-80po0c.2.2.1 | untagged tmux dependency call/file totals cannot grow; reductions must lower this baseline; each owning test confines tmux processes and sockets to its isolated namespace and cleanup | P0.4c-tmux | 2026-10-01 |

Expand Down
85 changes: 85 additions & 0 deletions cmd/gc/api_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"os"
"path/filepath"
"reflect"
"sort"
"strconv"
"strings"
"sync"
Expand All @@ -25,6 +26,7 @@ import (
"github.com/gastownhall/gascity/internal/configedit"
"github.com/gastownhall/gascity/internal/emergency"
"github.com/gastownhall/gascity/internal/events"
"github.com/gastownhall/gascity/internal/executionevent"
"github.com/gastownhall/gascity/internal/extmsg"
"github.com/gastownhall/gascity/internal/fsys"
"github.com/gastownhall/gascity/internal/git"
Expand Down Expand Up @@ -448,6 +450,11 @@ func (cs *controllerState) startBeadEventWatcher(ctx context.Context) {
if ep == nil {
return
}
// A controller can crash after the durable bead.closed journal append but
// before its best-effort lifecycle append. The normal watcher intentionally
// begins at the boot-time journal head, so reconcile closed graph.v2 steps
// before tailing to repair that otherwise permanent gap.
cs.reconcileExecutionCompletions()
seq := cs.beadEventStartSeq
// A captured seq of 0 with OK=true means the log was genuinely empty at
// construction — Watch(0) then replays exactly the prime-window events and
Expand Down Expand Up @@ -499,6 +506,77 @@ func (cs *controllerState) startBeadEventWatcher(ctx context.Context) {
}()
}

// reconcileExecutionCompletions repairs graph.v2 completion facts from the
// authoritative graph store. It is safe to call at startup and on patrol ticks:
// ReconcileCompleted uses the event journal's exact fact as its idempotency
// record, so repeated passes do not duplicate lifecycle events.
func (cs *controllerState) reconcileExecutionCompletions() {
ep := cs.EventProvider()
if ep == nil {
return
}

// Graph coordination may be relocated from the city work store, while
// graph.v2 executions normally live in the individual rig work stores.
// Scan both surfaces in stable order, collapsing wrappers first so aliases
// are not scanned more than once.
cs.mu.RLock()
stores := []beads.Store{
resolveGraphStore(cs.cityBeadStore, cs.cfg, cs.cityPath, cs.eventProv),
cs.cityBeadStore,
}
rigStores := make(map[string]beads.Store, len(cs.beadStores))
for name, store := range cs.beadStores {
rigStores[name] = store
}
cs.mu.RUnlock()

rigNames := make([]string, 0, len(rigStores))
for name := range rigStores {
rigNames = append(rigNames, name)
}
sort.Strings(rigNames)
for _, name := range rigNames {
stores = append(stores, rigStores[name])
}

seen := make(map[uintptr]struct{}, len(stores))
graphStores := make([]beads.GraphStore, 0, len(stores))
for _, store := range stores {
store = uncachedBeadStore(store)
if store == nil {
continue
}
if key, ok := storePointerKey(store); ok {
if _, duplicate := seen[key]; duplicate {
continue
}
seen[key] = struct{}{}
}
graphStores = append(graphStores, beads.GraphStore{Store: store})
}
executionevent.ReconcileCompletedStores(ep, graphStores, "execution-reconcile")
}

// uncachedBeadStore peels the controller's policy/cache read layers so a
// recovery projection can inspect closed authoritative rows. The normal active
// cache prime need not include closed beads, and therefore cannot safely drive
// lifecycle gap repair.
func uncachedBeadStore(store beads.Store) beads.Store {
for range 8 {
if base, _, ok := unwrapBeadPolicyStore(store); ok {
store = base
continue
}
cached, ok := store.(*beads.CachingStore)
if !ok || cached == nil || cached.Backing() == nil {
return store
}
store = cached.Backing()
}
return store
}

// startMaintenanceLoop launches the periodic Dolt store maintenance
// loop when [maintenance.dolt] enabled=true in city.toml. When the
// section is omitted or enabled=false, this is a no-op — the caller
Expand Down Expand Up @@ -578,6 +656,13 @@ func (cs *controllerState) applyBeadEventToStores(evt events.Event) {
cs.Poke()
}
if evt.Type == events.BeadClosed && evt.Subject != "" && len(stores) > 0 {
rec := events.Discard
cs.mu.RLock()
if cs.eventProv != nil {
rec = cs.eventProv
}
cs.mu.RUnlock()
executionevent.EmitCompletedFromClosedNotification(rec, cs.GraphBeadStore().Store, evt.Payload, evt.Actor)
cs.runBeadCloseAutoclose(evt.Subject, stores[0], storeRef)
}
}
Expand Down
Loading
Loading