diff --git a/services/gastown/container/plugin/index.ts b/services/gastown/container/plugin/index.ts index d9f495ea1..51184bfcb 100644 --- a/services/gastown/container/plugin/index.ts +++ b/services/gastown/container/plugin/index.ts @@ -105,9 +105,17 @@ export const GastownPlugin: Plugin = async ({ client }) => { // Best-effort logging — never let telemetry failures break tool execution async function log(level: 'info' | 'error', message: string) { console.log(`${SERVICE} ${level}: ${message}`); + const townId = process.env.GASTOWN_TOWN_ID; try { - await client.app.log({ body: { service: SERVICE, level, message } }); + await client.app.log({ + body: { + service: SERVICE, + level, + message, + ...(townId ? { extra: { townId } } : {}), + }, + }); } catch { // Swallow — logging is non-critical } diff --git a/services/gastown/container/src/agent-runner.ts b/services/gastown/container/src/agent-runner.ts index 17d226f1a..88db51671 100644 --- a/services/gastown/container/src/agent-runner.ts +++ b/services/gastown/container/src/agent-runner.ts @@ -580,6 +580,7 @@ export async function runAgent(originalRequest: StartAgentRequest): Promise { const activeAgents = listAgents().filter(a => a.status === 'running' || a.status === 'starting'); log.info('refresh_token.received', { + townId: process.env.GASTOWN_TOWN_ID ?? null, agentCount: activeAgents.length, agentIds: activeAgents.map(a => a.agentId), }); @@ -299,6 +300,7 @@ app.post('/refresh-token', async c => { const results = await refreshTokenForAllAgents(); const successCount = results.filter(r => r.success).length; log.info('refresh_token.completed', { + townId: process.env.GASTOWN_TOWN_ID ?? null, agentCount: results.length, successCount, failureCount: results.length - successCount, @@ -829,6 +831,7 @@ app.post('/agents/:agentId/pty', async c => { const reuseAgent = getAgentStatus(agentId); if (reuseAgent) { log.info('agent.pty_connected', { + townId: reuseAgent.townId, agentId, containerUptimeMs: getUptime(), agentUptimeMs: Date.now() - new Date(reuseAgent.startedAt).getTime(), @@ -889,6 +892,7 @@ app.post('/agents/:agentId/pty', async c => { ); if (createResp.ok) { log.info('agent.pty_connected', { + townId: agent.townId, agentId, containerUptimeMs: getUptime(), agentUptimeMs: Date.now() - new Date(agent.startedAt).getTime(), diff --git a/services/gastown/container/src/main.ts b/services/gastown/container/src/main.ts index 07b0ecb23..225860942 100644 --- a/services/gastown/container/src/main.ts +++ b/services/gastown/container/src/main.ts @@ -76,6 +76,7 @@ setInterval(() => { }); } catch (err) { log.warn('container.memory_usage_failed', { + townId: townIdForLogs(), error: err instanceof Error ? err.message : String(err), }); } diff --git a/services/gastown/container/src/process-manager.ts b/services/gastown/container/src/process-manager.ts index 923f8b587..7def77d2d 100644 --- a/services/gastown/container/src/process-manager.ts +++ b/services/gastown/container/src/process-manager.ts @@ -71,6 +71,7 @@ const IDLE_TIMER_IGNORE_EVENTS = new Set([ let nextPort = 4096; const startTime = Date.now(); +const TOWN_ID = process.env.GASTOWN_TOWN_ID ?? null; // Set to true when drainAll() starts — prevents new agent starts and // lets the drain loop nudge agents that transition to running mid-drain. @@ -163,6 +164,7 @@ function markMayorReadyOnce(): void { if (mayorReadyAt !== null) return; mayorReadyAt = new Date().toISOString(); log.info('mayor.ready', { + townId: TOWN_ID, containerUptimeMs: getUptime(), mayorReadyAt, }); @@ -405,6 +407,7 @@ async function saveDbSnapshot( ); log.error('mayor.snapshot_failed', { event: 'mayor.snapshot_failed', + townId, agentId, role, durationMs: Date.now() - t0, @@ -430,6 +433,7 @@ async function saveDbSnapshot( console.warn(`${MANAGER_LOG} Failed to save DB snapshot for ${agentId}: ${resp.status}`); log.error('mayor.snapshot_failed', { event: 'mayor.snapshot_failed', + townId, agentId, role, durationMs: Date.now() - t0, @@ -444,6 +448,7 @@ async function saveDbSnapshot( ); log.info('mayor.snapshot_saved', { event: 'mayor.snapshot_saved', + townId, agentId, role, durationMs: Date.now() - t0, @@ -458,6 +463,7 @@ async function saveDbSnapshot( console.warn(`${MANAGER_LOG} DB snapshot save failed for agent ${agentId}:`, err); log.error('mayor.snapshot_failed', { event: 'mayor.snapshot_failed', + townId, agentId, role, durationMs: Date.now() - t0, @@ -973,6 +979,7 @@ async function subscribeToEvents( const exitAgent = () => { if (agent.status !== 'running') return; log.info('agent.exit', { + townId: agent.townId, agentId: agent.agentId, name: agent.name, reason: 'completed', @@ -1073,6 +1080,7 @@ async function subscribeToEvents( } catch (err) { if (!controller.signal.aborted) { log.error('agent.stream_error', { + townId: agent.townId, agentId: agent.agentId, error: err instanceof Error ? err.message : String(err), }); @@ -1197,6 +1205,7 @@ async function startAgentImpl( } const tDbDone = Date.now(); log.info('agent.startup_phase', { + townId: request.townId, agentId: request.agentId, phase: 'db_hydrated', elapsedMs: tDbDone - t0, @@ -1214,6 +1223,7 @@ async function startAgentImpl( agent.serverPort = port; const tSdkDone = Date.now(); log.info('agent.startup_phase', { + townId: request.townId, agentId: request.agentId, phase: 'sdk_ready', elapsedMs: tSdkDone - t0, @@ -1276,6 +1286,7 @@ async function startAgentImpl( agent.sessionId = sessionId; const tSessionDone = Date.now(); log.info('agent.startup_phase', { + townId: request.townId, agentId: request.agentId, phase: 'session_created', elapsedMs: tSessionDone - t0, @@ -1352,6 +1363,7 @@ async function startAgentImpl( agent.messageCount = 1; log.info('agent.start', { + townId: request.townId, agentId: request.agentId, role: request.role, name: request.name, @@ -1360,6 +1372,7 @@ async function startAgentImpl( }); log.info('agent.startup_complete', { + townId: request.townId, agentId: request.agentId, totalMs: Date.now() - t0, containerUptimeMs: getUptime(), @@ -1462,6 +1475,7 @@ export async function stopAgent(agentId: string): Promise { } } catch (err) { log.warn('agent.stop_failed', { + townId: agent.townId, agentId, error: err instanceof Error ? err.message : String(err), }); @@ -1469,7 +1483,12 @@ export async function stopAgent(agentId: string): Promise { agent.status = 'exited'; agent.exitReason = 'stopped'; - log.info('agent.exit', { agentId, reason: 'stopped', exitReason: 'stopped' }); + log.info('agent.exit', { + townId: agent.townId, + agentId, + reason: 'stopped', + exitReason: 'stopped', + }); broadcastEvent(agentId, 'agent.exited', { reason: 'stopped' }); syncRegistry(); @@ -1504,6 +1523,7 @@ export async function sendMessage(agentId: string, prompt: string): Promise { @@ -1926,11 +1952,13 @@ export async function refreshTokenForAllAgents(): Promise< orphan.server.close(); } catch (closeErr) { log.warn('refresh_token.orphan_close_failed', { + townId: reapTownId, agentId: reapAgentId, error: closeErr instanceof Error ? closeErr.message : String(closeErr), }); } log.warn('refresh_token.orphan_reaped', { + townId: reapTownId, agentId: reapAgentId, orphanPort, }); @@ -1942,6 +1970,7 @@ export async function refreshTokenForAllAgents(): Promise< ); } log.error('refresh_token.agent_restarted', { + townId: agent.townId, agentId: agent.agentId, role: agent.role, name: agent.name, @@ -2537,6 +2566,7 @@ export async function drainAll(): Promise { console.error(`${DRAIN_LOG} snapshot timeout/failure for ${agent.agentId}:`, err); log.error('mayor.snapshot_failed', { event: 'mayor.snapshot_failed', + townId: agent.townId, agentId: agent.agentId, role: agent.role, error: err instanceof Error ? err.message : String(err), @@ -2613,6 +2643,7 @@ export async function stopAll(): Promise { console.error(`[stop-all] snapshot timeout/failure for ${agent.agentId}:`, err); log.error('mayor.snapshot_failed', { event: 'mayor.snapshot_failed', + townId: agent.townId, agentId: agent.agentId, role: agent.role, error: err instanceof Error ? err.message : String(err), diff --git a/services/gastown/container/src/token-refresh.ts b/services/gastown/container/src/token-refresh.ts index 5d7d74b3a..df0877814 100644 --- a/services/gastown/container/src/token-refresh.ts +++ b/services/gastown/container/src/token-refresh.ts @@ -54,6 +54,7 @@ export async function fetchFreshContainerToken(): Promise { if (!apiUrl || !townId || !currentToken) { log.warn('token_refresh.skipped_missing_env', { + townId: townId ?? null, hasApiUrl: !!apiUrl, hasTownId: !!townId, hasCurrentToken: !!currentToken, @@ -75,6 +76,7 @@ export async function fetchFreshContainerToken(): Promise { if (!resp.ok) { const text = await resp.text().catch(() => ''); log.warn('token_refresh.fetch_failed', { + townId, status: resp.status, durationMs: Date.now() - t0, body: text.slice(0, 200), @@ -87,14 +89,15 @@ export async function fetchFreshContainerToken(): Promise { ? (body as { data?: { token?: unknown } }).data?.token : undefined; if (typeof token !== 'string' || token.length === 0) { - log.warn('token_refresh.invalid_response', { durationMs: Date.now() - t0 }); + log.warn('token_refresh.invalid_response', { townId, durationMs: Date.now() - t0 }); return null; } process.env.GASTOWN_CONTAINER_TOKEN = token; - log.info('token_refresh.succeeded', { durationMs: Date.now() - t0 }); + log.info('token_refresh.succeeded', { townId, durationMs: Date.now() - t0 }); return token; } catch (err) { log.warn('token_refresh.network_error', { + townId, error: err instanceof Error ? err.message : String(err), durationMs: Date.now() - t0, }); @@ -124,6 +127,7 @@ export async function refreshTokenIfNearExpiry(thresholdMs = 30 * 60_000): Promi return; } log.info('token_refresh.boot_near_expiry', { + townId: process.env.GASTOWN_TOWN_ID ?? null, msUntilExpiry: msLeft, thresholdMs, }); diff --git a/services/gastown/src/dos/Town.do.ts b/services/gastown/src/dos/Town.do.ts index 3d96e78bf..88a99eecc 100644 --- a/services/gastown/src/dos/Town.do.ts +++ b/services/gastown/src/dos/Town.do.ts @@ -3515,6 +3515,7 @@ export class TownDO extends DurableObject { ) .catch(err => console.warn(`${TOWN_LOG} slingConvoy: createConvoyBranch failed (non-fatal)`, { + townId: this.townId, error: err instanceof Error ? err.message : String(err), }) ); @@ -3997,7 +3998,7 @@ export class TownDO extends DurableObject { // ══════════════════════════════════════════════════════════════════ async alarm(): Promise { - return withLogTags({ source: 'Town.do' }, async () => { + return withLogTags({ source: 'Town.do', tags: { townId: this.townId } }, async () => { await this._alarm(); }); } diff --git a/services/gastown/src/dos/town/container-idle-stop.ts b/services/gastown/src/dos/town/container-idle-stop.ts index 03a020544..f9edaa88a 100644 --- a/services/gastown/src/dos/town/container-idle-stop.ts +++ b/services/gastown/src/dos/town/container-idle-stop.ts @@ -54,6 +54,7 @@ export async function stopContainerIfIdle(deps: IdleStopDeps): Promise { state = await stub.getState(); } catch (err) { logger.warn('stopContainerIfIdle: getState() failed', { + townId, error: err instanceof Error ? err.message : String(err), }); return; @@ -73,6 +74,7 @@ export async function stopContainerIfIdle(deps: IdleStopDeps): Promise { deps.writeEventFn({ event: 'container.idle_stop', townId, reason }); } catch (err) { logger.warn('stopContainerIfIdle: stop() failed', { + townId, error: err instanceof Error ? err.message : String(err), }); deps.writeEventFn({ diff --git a/services/gastown/src/gastown.worker.ts b/services/gastown/src/gastown.worker.ts index 86b6732a7..c4e6cf37e 100644 --- a/services/gastown/src/gastown.worker.ts +++ b/services/gastown/src/gastown.worker.ts @@ -10,7 +10,7 @@ import { getTownDOStub } from './dos/Town.do'; import { TownConfigUpdateSchema } from './types'; import { resError } from './util/res.util'; import { writeEvent } from './util/analytics.util'; -import { logger } from './util/log.util'; +import { logger, withLogTags } from './util/log.util'; import { authMiddleware, agentOnlyMiddleware, @@ -173,6 +173,11 @@ export type GastownEnv = { const app = new Hono(); const LOCAL_DEV_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]']); +function getTownIdFromPath(pathname: string): string | undefined { + const match = pathname.match(/(?:^|\/)towns\/([^/]+)/); + return match?.[1]; +} + async function cfAccessDebugMiddleware(c: Context, next: () => Promise) { const hostname = new URL(c.req.url).hostname; if (c.env.ENVIRONMENT === 'development' && LOCAL_DEV_HOSTNAMES.has(hostname)) { @@ -185,8 +190,10 @@ async function cfAccessDebugMiddleware(c: Context, next: () => Promi audience: c.env.CF_ACCESS_AUD, }); } catch (e) { - console.warn(`CF Access validation failed ${e instanceof Error ? e.message : 'unknown'}`, { - error: e, + const townId = getTownIdFromPath(new URL(c.req.url).pathname); + logger.warn(`CF Access validation failed ${e instanceof Error ? e.message : 'unknown'}`, { + ...(townId ? { townId } : {}), + error: e instanceof Error ? e.message : String(e), }); return c.json({ success: false, error: 'Unauthorized' }, 401); } @@ -218,6 +225,11 @@ app.use('/api/towns/:townId/*', async (c, next) => { if (townId) logger.setTags({ townId }); await next(); }); +app.use('/debug/towns/:townId/*', async (c, next) => { + const townId = c.req.param('townId'); + if (townId) logger.setTags({ townId }); + await next(); +}); app.use('/api/mayor/:townId/*', async (c, next) => { const townId = c.req.param('townId'); if (townId) logger.setTags({ townId }); @@ -263,6 +275,11 @@ app.use('/api/mayor/:townId/tools/rigs/:rigId/agents/:agentId/*', async (c, next if (agentId) logger.setTags({ agentId }); await next(); }); +app.use('/api/towns/:townId/container/agents/:agentId/*', async (c, next) => { + const agentId = c.req.param('agentId'); + if (agentId) logger.setTags({ agentId }); + await next(); +}); // ── CORS ──────────────────────────────────────────────────────────────── // Allow browser requests from the main Kilo app. In development, allow @@ -1357,7 +1374,10 @@ app.use( orgMemberships: c.get('kiloOrgMemberships') ?? [], }), onError: ({ error, path }: { error: Error; path?: string }) => { - console.error(`[gastown-trpc] error on ${path ?? 'unknown'}:`, error.message); + logger.error('[gastown-trpc] error', { + path: path ?? 'unknown', + error: error.message, + }); if (!(error instanceof TRPCError)) { Sentry.captureException(error); } @@ -1370,7 +1390,12 @@ app.use( app.notFound(c => c.json(resError('Not found'), 404)); app.onError((err, c) => { - console.error('Unhandled error', { error: err.message, stack: err.stack }); + const townId = getTownIdFromPath(new URL(c.req.url).pathname); + logger.error('Unhandled error', { + ...(townId ? { townId } : {}), + error: err.message, + stack: err.stack, + }); Sentry.captureException(err); return c.json(resError('Internal server error'), 500); }); @@ -1409,9 +1434,11 @@ export default withSentry( if (streamMatch) { const townId = streamMatch[1]; const agentId = streamMatch[2]; - console.log(`[gastown-worker] WS upgrade (stream): townId=${townId} agentId=${agentId}`); - const stub = getTownContainerStub(env, townId); - return stub.fetch(request); + return withLogTags({ source: 'gastown-worker', tags: { townId, agentId } }, async () => { + logger.info('WS upgrade stream'); + const stub = getTownContainerStub(env, townId); + return stub.fetch(request); + }); } // PTY terminal connection @@ -1420,20 +1447,22 @@ export default withSentry( const townId = ptyMatch[1]; const agentId = ptyMatch[2]; const ptyId = ptyMatch[3]; - console.log( - `[gastown-worker] WS upgrade (pty): townId=${townId} agentId=${agentId} ptyId=${ptyId}` - ); - const stub = getTownContainerStub(env, townId); - return stub.fetch(request); + return withLogTags({ source: 'gastown-worker', tags: { townId, agentId } }, async () => { + logger.info('WS upgrade pty', { ptyId }); + const stub = getTownContainerStub(env, townId); + return stub.fetch(request); + }); } // Town alarm status (real-time push) const statusMatch = url.pathname.match(WS_STATUS_PATTERN); if (statusMatch) { const townId = statusMatch[1]; - console.log(`[gastown-worker] WS upgrade (status): townId=${townId}`); - const stub = getTownDOStub(env, townId); - return stub.fetch(request); + return withLogTags({ source: 'gastown-worker', tags: { townId } }, async () => { + logger.info('WS upgrade status'); + const stub = getTownDOStub(env, townId); + return stub.fetch(request); + }); } } diff --git a/services/gastown/src/trpc/init.ts b/services/gastown/src/trpc/init.ts index bea8bb9b6..ce6c640fd 100644 --- a/services/gastown/src/trpc/init.ts +++ b/services/gastown/src/trpc/init.ts @@ -1,5 +1,7 @@ import { initTRPC, TRPCError } from '@trpc/server'; +import { z } from 'zod'; import { writeEvent } from '../util/analytics.util'; +import { logger } from '../util/log.util'; import type { JwtOrgMembership } from '../middleware/auth.middleware'; @@ -17,12 +19,23 @@ const t = initTRPC.context().create(); export const router = t.router; +const RawInputWithTownId = z.object({ townId: z.string().uuid() }).passthrough(); + +function getTownIdFromInput(input: unknown): string | undefined { + const parsed = RawInputWithTownId.safeParse(input); + return parsed.success ? parsed.data.townId : undefined; +} + /** * Analytics middleware — wraps every tRPC procedure to emit an analytics * event with timing and error capture. Runs before auth so even rejected * requests are tracked. */ -const analyticsProcedure = t.procedure.use(async ({ ctx, path, type, next }) => { +const analyticsProcedure = t.procedure.use(async ({ ctx, getRawInput, path, type, next }) => { + const rawInput = await getRawInput(); + const townId = getTownIdFromInput(rawInput); + if (townId) logger.setTags({ townId }); + const start = performance.now(); let error: string | undefined; try { diff --git a/services/gastown/src/trpc/router.ts b/services/gastown/src/trpc/router.ts index 5ccd60541..562c8b324 100644 --- a/services/gastown/src/trpc/router.ts +++ b/services/gastown/src/trpc/router.ts @@ -20,6 +20,7 @@ import { writeEvent } from '../util/analytics.util'; import { TownConfigSchema, TownConfigUpdateSchema, RigOverrideConfigSchema } from '../types'; import { resolveModel } from '../dos/town/config'; import type { UserRigRecord } from '../db/tables/user-rigs.table'; +import { logger } from '../util/log.util'; import { RpcTownOutput, RpcRigOutput, @@ -64,7 +65,7 @@ async function refreshGitCredentials( const result = await env.GIT_TOKEN_SERVICE.getTokenForRepo({ githubRepo, userId, orgId }); if (!result.success) { - console.warn(`[gastown-trpc] git credential refresh failed: ${result.reason}`); + logger.warn('[gastown-trpc] git credential refresh failed', { townId, reason: result.reason }); return; } @@ -113,7 +114,10 @@ async function refreshFirstGithubRigCredentials(params: { durationMs: Date.now() - start, error: err instanceof Error ? err.message : String(err), }); - console.warn('[gastown-trpc] ensureMayor: git credential refresh failed', err); + logger.warn('[gastown-trpc] ensureMayor: git credential refresh failed', { + townId: params.townId, + error: err instanceof Error ? err.message : String(err), + }); } } @@ -348,9 +352,15 @@ async function verifyRigOwnership(env: Env, ctx: TRPCContext, rigId: string, tow throw new TRPCError({ code: 'NOT_FOUND', message: 'Rig not found' }); } -async function mintKilocodeToken(env: Env, user: { id: string; api_token_pepper: string | null }) { +async function mintKilocodeToken( + env: Env, + user: { id: string; api_token_pepper: string | null }, + townId?: string +) { if (!env.NEXTAUTH_SECRET) { - console.error('[mintKilocodeToken] NEXTAUTH_SECRET not configured'); + logger.error('[mintKilocodeToken] NEXTAUTH_SECRET not configured', { + ...(townId ? { townId } : {}), + }); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Internal server error', @@ -358,7 +368,9 @@ async function mintKilocodeToken(env: Env, user: { id: string; api_token_pepper: } const secret = await resolveSecret(env.NEXTAUTH_SECRET); if (!secret) { - console.error('[mintKilocodeToken] failed to resolve NEXTAUTH_SECRET from Secrets Store'); + logger.error('[mintKilocodeToken] failed to resolve NEXTAUTH_SECRET from Secrets Store', { + ...(townId ? { townId } : {}), + }); throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Internal server error', @@ -381,7 +393,7 @@ export const gastownRouter = router({ const town = await userStub.createTown({ name: input.name, owner_user_id: user.id }); // Store kilocode token so agents can auth with the Kilo LLM gateway - const kilocodeToken = await mintKilocodeToken(ctx.env, user); + const kilocodeToken = await mintKilocodeToken(ctx.env, user, town.id); const townStub = getTownDOStub(ctx.env, town.id); await townStub.setTownId(town.id); await townStub.updateTownConfig({ @@ -523,7 +535,7 @@ export const gastownRouter = router({ // a non-owner member adds a rig, keep the existing town token. let kilocodeToken: string | undefined; if (credentialUserId === user.id) { - kilocodeToken = await mintKilocodeToken(ctx.env, user); + kilocodeToken = await mintKilocodeToken(ctx.env, user, input.townId); await townStub.updateTownConfig({ kilocode_token: kilocodeToken }); } @@ -537,7 +549,10 @@ export const gastownRouter = router({ townConfig.organization_id ); } catch (err) { - console.warn('[gastown-trpc] createRig: git credential refresh failed', err); + logger.warn('[gastown-trpc] createRig: git credential refresh failed', { + townId: input.townId, + error: err instanceof Error ? err.message : String(err), + }); } const rig = await ownerStub.createRig({ @@ -567,10 +582,11 @@ export const gastownRouter = router({ defaultBranch: input.defaultBranch, }); } catch (err) { - console.error( - `[gastown-trpc] createRig: Town DO configure FAILED for rig ${rig.id}, rolling back:`, - err - ); + logger.error('[gastown-trpc] createRig: Town DO configure FAILED, rolling back', { + townId: input.townId, + rigId: rig.id, + error: err instanceof Error ? err.message : String(err), + }); try { await ownerStub.deleteRig(rig.id); } catch { @@ -878,7 +894,10 @@ export const gastownRouter = router({ townConfig.organization_id ); } catch (err) { - console.warn('[gastown-trpc] sling: git credential refresh failed', err); + logger.warn('[gastown-trpc] sling: git credential refresh failed', { + townId: rig.town_id, + error: err instanceof Error ? err.message : String(err), + }); } const townStub = getTownDOStub(ctx.env, rig.town_id); @@ -927,9 +946,12 @@ export const gastownRouter = router({ // Mayor notification can start a container — use waitUntil so the Worker // stays alive until the RPC completes without blocking the HTTP response. ctx.executionCtx.waitUntil( - townStub - .notifyMayorOfNewBead(bead.bead_id, rig.id, input.title, input.body) - .catch(err => console.warn('[gastown-trpc] createBead: mayor notification failed', err)) + townStub.notifyMayorOfNewBead(bead.bead_id, rig.id, input.title, input.body).catch(err => + logger.warn('[gastown-trpc] createBead: mayor notification failed', { + townId: rig.town_id, + error: err instanceof Error ? err.message : String(err), + }) + ) ); return bead; @@ -1255,7 +1277,10 @@ export const gastownRouter = router({ try { await townStub.syncConfigToContainer(); } catch (err) { - console.warn('[gastown-trpc] updateTownConfig: syncConfigToContainer failed:', err); + logger.warn('[gastown-trpc] updateTownConfig: syncConfigToContainer failed', { + townId: input.townId, + error: err instanceof Error ? err.message : String(err), + }); } // Rewrite the mayor's AGENTS.md when custom instructions change so the @@ -1266,7 +1291,10 @@ export const gastownRouter = router({ try { await townStub.updateMayorSystemPrompt(); } catch (err) { - console.warn('[gastown-trpc] updateTownConfig: updateMayorSystemPrompt failed:', err); + logger.warn('[gastown-trpc] updateTownConfig: updateMayorSystemPrompt failed', { + townId: input.townId, + error: err instanceof Error ? err.message : String(err), + }); } } @@ -1287,7 +1315,10 @@ export const gastownRouter = router({ try { await townStub.updateMayorModel(newMayorModel, result.small_model ?? undefined); } catch (err) { - console.warn('[gastown-trpc] updateTownConfig: updateMayorModel failed:', err); + logger.warn('[gastown-trpc] updateTownConfig: updateMayorModel failed', { + townId: input.townId, + error: err instanceof Error ? err.message : String(err), + }); } } @@ -1344,7 +1375,7 @@ export const gastownRouter = router({ tokenUser = userFromCtx(ctx); } } - const newKilocodeToken = await mintKilocodeToken(ctx.env, tokenUser); + const newKilocodeToken = await mintKilocodeToken(ctx.env, tokenUser, input.townId); await townStub.updateTownConfig({ kilocode_token: newKilocodeToken }); await townStub.syncConfigToContainer(); }), @@ -1557,7 +1588,7 @@ export const gastownRouter = router({ // Mint kilocode token so the mayor can start without waiting for rig creation const user = userFromCtx(ctx); - const kilocodeToken = await mintKilocodeToken(ctx.env, user); + const kilocodeToken = await mintKilocodeToken(ctx.env, user, town.id); const townStub = getTownDOStub(ctx.env, town.id); await townStub.setTownId(town.id); @@ -1587,10 +1618,10 @@ export const gastownRouter = router({ const townStub = getTownDOStub(ctx.env, input.townId); await townStub.destroy(); } catch (err) { - console.error( - `[gastown-trpc] deleteOrgTown: failed to destroy Town DO for ${input.townId}:`, - err - ); + logger.error('[gastown-trpc] deleteOrgTown: failed to destroy Town DO', { + townId: input.townId, + error: err instanceof Error ? err.message : String(err), + }); } await stub.deleteTown(input.townId); @@ -1638,7 +1669,7 @@ export const gastownRouter = router({ const credentialUserId = townConfig.owner_user_id ?? ctx.userId; let kilocodeToken: string | undefined; if (credentialUserId === ctx.userId) { - kilocodeToken = await mintKilocodeToken(ctx.env, userFromCtx(ctx)); + kilocodeToken = await mintKilocodeToken(ctx.env, userFromCtx(ctx), input.townId); await townStub.updateTownConfig({ kilocode_token: kilocodeToken }); } @@ -1652,7 +1683,10 @@ export const gastownRouter = router({ townConfig.organization_id ); } catch (err) { - console.warn('[gastown-trpc] createOrgRig: git credential refresh failed', err); + logger.warn('[gastown-trpc] createOrgRig: git credential refresh failed', { + townId: input.townId, + error: err instanceof Error ? err.message : String(err), + }); } const rig = await orgStub.createRig({ @@ -1680,10 +1714,11 @@ export const gastownRouter = router({ defaultBranch: input.defaultBranch, }); } catch (err) { - console.error( - `[gastown-trpc] createOrgRig: Town DO configure FAILED for rig ${rig.id}, rolling back:`, - err - ); + logger.error('[gastown-trpc] createOrgRig: Town DO configure FAILED, rolling back', { + townId: input.townId, + rigId: rig.id, + error: err instanceof Error ? err.message : String(err), + }); try { await orgStub.deleteRig(rig.id); } catch {