diff --git a/cloudflare-gastown/src/dos/Town.do.ts b/cloudflare-gastown/src/dos/Town.do.ts index 0cf7c974a7..6fdcc33692 100644 --- a/cloudflare-gastown/src/dos/Town.do.ts +++ b/cloudflare-gastown/src/dos/Town.do.ts @@ -30,7 +30,7 @@ import * as scheduling from './town/scheduling'; import * as events from './town/events'; import * as reconciler from './town/reconciler'; import { applyAction } from './town/actions'; -import type { ApplyActionContext } from './town/actions'; +import type { Action, ApplyActionContext } from './town/actions'; import { buildRefinerySystemPrompt } from '../prompts/refinery-system.prompt'; import { GitHubPRStatusSchema, GitLabMRStatusSchema } from '../util/platform-pr.util'; @@ -3684,6 +3684,31 @@ export class TownDO extends DurableObject { }; } + // DEBUG: dry-run the reconciler against current state, returning actions + // it would emit without applying them. Side-effect-free — reconcile() + // only reads SQLite state; applyAction() is never called. + async debugDryRun(): Promise<{ + actions: Action[]; + metrics: Pick< + reconciler.ReconcilerMetrics, + 'actionsEmitted' | 'actionsByType' | 'pendingEventCount' + >; + }> { + const actions = reconciler.reconcile(this.sql); + const actionsByType: Record = {}; + for (const a of actions) { + actionsByType[a.type] = (actionsByType[a.type] ?? 0) + 1; + } + return { + actions, + metrics: { + actionsEmitted: actions.length, + actionsByType, + pendingEventCount: events.pendingEventCount(this.sql), + }, + }; + } + // DEBUG: concise non-terminal bead summary — remove after debugging async debugBeadSummary(): Promise { return [ diff --git a/cloudflare-gastown/src/gastown.worker.ts b/cloudflare-gastown/src/gastown.worker.ts index 6c8efab713..1a1634f0d2 100644 --- a/cloudflare-gastown/src/gastown.worker.ts +++ b/cloudflare-gastown/src/gastown.worker.ts @@ -206,6 +206,14 @@ app.get('/debug/towns/:townId/status', async c => { return c.json({ alarmStatus, agentMeta, beadSummary }); }); +app.post('/debug/towns/:townId/reconcile-dry-run', async c => { + const townId = c.req.param('townId'); + const town = getTownDOStub(c.env, townId); + // eslint-disable-next-line @typescript-eslint/await-thenable -- DO RPC returns promise at runtime + const result = await town.debugDryRun(); + return c.json(result); +}); + // ── Town ID + Auth ────────────────────────────────────────────────────── // All rig routes live under /api/towns/:townId/rigs/:rigId so the townId // is always available from the URL path.