fix(gastown): address PR #544 review hardening items#689
Conversation
- Add .catch() error logging on fire-and-forget dispatchAgent in slingBead - Validate SDK session.create response with Zod schema instead of duck-typing - Add tests verifying registerAgent stores rig_id correctly - Store town config per-request via Hono context with Zod validation - Emit notification_failed bead events when escalation mayor notification fails - Wrap git credential write in try/catch during rig creation for resilience - Verify /api/gastown/git-credentials route exists (confirmed) - Return 403 (not 400) for cross-town access attempts via resolveTownId
Code Review SummaryStatus: No New Issues Found | Recommendation: Merge (after addressing prior review comments) OverviewThis PR is a solid hardening pass that addresses multiple issues from the prior review round. All 19 previously identified issues have been resolved or acknowledged. No new issues were found in this review pass. Key Changes Reviewed
Files Reviewed (17 files)
|
- Fix misleading .passthrough() comment to accurately describe z.record() - Make townConfig type optional in ContainerEnv since header may be absent - Hoist SessionResponse Zod schema to module level to avoid repeated allocation - Wrap logBeadEvent calls in try/catch inside .catch() handlers to prevent unhandled rejections if the event log write fails
Eliminates the repeated 3-line resolveTownId/error-check/extract pattern
from all 31 handler call sites across 8 files. The auth middleware now
resolves and validates townId (returning 400 or 403 as appropriate) and
sets it on the Hono context. Handlers simply call c.get('townId').
- Remove c.set('townConfig', ...) which was set but never read by any handler
- Remove ContainerEnv type that only existed for the unused variable
- getTownId and resolveTownId were already cleaned up in previous commit
townId resolution was inside authMiddleware which is skipped in dev mode,
causing c.get('townId') to return undefined for all handlers. Split into
a separate townIdMiddleware that runs unconditionally before the auth gate.
…able townIdMiddleware now only extracts the route param (no JWT dependency). The cross-town check (JWT townId vs route townId) is done in authMiddleware after the JWT is parsed, alongside the existing rigId check.
## Summary Fixes 4 compounding bugs that caused the refinery agent to be dispatched into the wrong rig's repository in multi-rig towns, fixes 3 additional issues with MR bead assignment and the review lifecycle, and adds a Related Beads DAG section to the bead drawer UI. Closes #657 > **Note:** This branch is based on `686-pr544-review-hardening` (PR #689) which is pending review. Once #689 merges to main, this PR will be clean against main. Also adds UI to show relationship between related beads (like a task bead to review bead) <img width="1567" height="1237" alt="image" src="https://github.com/user-attachments/assets/0801695b-a51a-4692-832f-07cb8d9cf2ab" /> ## The bug chain (before this fix) 1. Polecat on **rig B** finishes work -> `submitToReviewQueue` 2. MR bead created with **`rig_id = null`** and **assignee = polecat** (Bugs 3, 5) 3. Alarm fires -> `processReviewQueue` pops the entry 4. Rig resolved via **`rigList[0]`** -> gets **rig A** (Bug 2) 5. `getOrCreateAgent('refinery')` returns the **existing refinery for rig A** (Bug 1) 6. Refinery hooked to the **source bead** (not the MR bead), overwriting its assignee (Bug 6) 7. Source bead left `in_progress` instead of closed (Bug 7) ## Fixes ### Bug 1: Refinery was a town-wide singleton - **`agents.ts`**: Removed `refinery` from `singletonRoles` (now only `witness` and `mayor`) - Refinery now uses the same per-rig idle agent reuse logic as polecats, scoped by `rig_id` ### Bug 2: `processReviewQueue` hardcoded `rigList[0]` - **`Town.do.ts`**: Now reads `entry.rig_id` from the merge_request bead ### Bug 3: merge_request beads created with `rig_id = null` - **`review-queue.ts`**: `submitToReviewQueue` now writes `input.rig_id` to the bead ### Bug 4: `ReviewQueueInput` type lacked `rig_id` - **`types.ts`**: Added `rig_id: string` to both `ReviewQueueInput` and `ReviewQueueEntry` ### Bug 5: MR bead assignee set to polecat instead of left for refinery - **`review-queue.ts`**: MR bead `assignee_agent_bead_id` now null on creation (refinery claims it via `hookBead`). Source agent stored in `metadata.source_agent_id`. ### Bug 6: Refinery hooked to source bead, not MR bead - **`Town.do.ts`**: `processReviewQueue` now hooks the refinery to `entry.id` (MR bead), not `entry.bead_id` (source bead). This preserves the source bead's polecat assignee. - **`review-queue.ts`**: Refinery `agentDone` path updated — `completeReviewFromMRBead` reads the source bead from the MR's metadata instead of the old `completeReviewForSourceBead` lookup. ### Bug 7: Source bead not closed after polecat submits to review - **`review-queue.ts`**: `agentDone` (polecat path) now calls `closeBead` on the source bead after submitting to the review queue, matching upstream `gt done` behavior. ### Feature: Related Beads DAG in bead drawer - **`BeadPanel.tsx`**: New "Related Beads" section shows the bead's DAG neighborhood: - **Child beads** — beads whose `parent_bead_id` matches the current bead - **Source Work** — for MR beads, the original issue/work bead from `metadata.source_bead_id` - **Review** — for non-MR beads, any `merge_request` beads that track this bead - Each entry is clickable, pushing a new bead drawer onto the navigation stack - Computed client-side from existing `listBeads` data — no new API endpoints - Also adds a `bead_dependencies` 'tracks' row when creating MR beads to formally link the DAG ## Files changed | File | Change | |------|--------| | `cloudflare-gastown/src/types.ts` | Added `rig_id` to `ReviewQueueInput` and `ReviewQueueEntry` | | `cloudflare-gastown/src/dos/town/agents.ts` | Refinery per-rig, generalized idle agent lookup | | `cloudflare-gastown/src/dos/town/review-queue.ts` | MR bead lifecycle fixes, bead_dependencies tracking | | `cloudflare-gastown/src/dos/Town.do.ts` | Hook refinery to MR bead, resolve rig from bead | | `cloudflare-gastown/src/handlers/rig-review-queue.handler.ts` | Pass `params.rigId` to `submitToReviewQueue` | | `src/components/gastown/drawer-panels/BeadPanel.tsx` | Related Beads DAG section | | `cloudflare-gastown/test/integration/rig-do.test.ts` | Add `rig_id` to test data | | `cloudflare-gastown/test/integration/rig-alarm.test.ts` | Add `rig_id` to test data |
Summary
Addresses all 8 hardening items identified in the PR #544 code review.
Closes #686
Changes
1. Fire-and-forget
dispatchAgenterror logging.catch()withconsole.erroron thevoid this.dispatchAgent()call inslingBeadso failures are visible in logs.2. Zod validation for SDK session creation response
sessionResult.data ?? sessionResult+'id' in sessionduck-typing with a Zod schema (z.object({ id: z.string().min(1) }).passthrough()).3.
registerAgentstoresrig_id— verified + tested{ ...parsed.data, rig_id: params.rigId }→agents.registerAgentstoresinput.rig_id ?? nullin the beads tablerig_idcolumn.rig_idis stored when provided, one verifying it'snullwhen omitted.4. Thread-safe
currentTownConfigvia Hono contextz.record(z.string(), z.unknown())) for theX-Town-Configheader JSON.ContainerEnvvariables and store config per-request viac.set('townConfig', ...).lastKnownTownConfigto clarify it's a cache, not the authoritative source.5. Emit bead event on failed escalation notification
notification_failedto theBeadEventTypeenum (bothbead-events.table.tsandrig-bead-events.table.ts).routeEscalationand the re-escalation alarm now emit anotification_failedbead event with metadata (target, reason, severity) when mayor notification fails..catch(() => {}).6. Resilient rig creation + git credential write
updateTownConfiggit credential write in a try/catch so a failed credential write doesn't propagate as a rig creation failure.resolveGitCredentialsIfMissingandrefreshTownGitCredentialsserve as recovery mechanisms.7.
/api/gastown/git-credentialsroute — verifiedsrc/app/api/gastown/git-credentials/route.tswith proper auth, ownership verification, and Zod validation. No changes needed.8. Return 403 for cross-town access attempts
resolveTownId()in auth middleware returning a discriminated union:{ townId }|{ error: 'missing', status: 400 }|{ error: 'forbidden', status: 403 }.resolveTownId()and return the correct HTTP status code.getTownId()as a backward-compatible convenience wrapper.Files changed (16)
cloudflare-gastown/src/dos/Town.do.tscloudflare-gastown/container/src/process-manager.tscloudflare-gastown/container/src/control-server.tscloudflare-gastown/src/db/tables/bead-events.table.tscloudflare-gastown/src/db/tables/rig-bead-events.table.tscloudflare-gastown/src/middleware/auth.middleware.tscloudflare-gastown/src/handlers/rig-*.handler.ts(8 files)cloudflare-gastown/test/integration/rig-do.test.tssrc/routers/gastown-router.ts