Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions cloudflare-gastown/container/src/control-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ app.post('/agents/start', async c => {
return c.json({ error: 'Invalid request body', issues: parsed.error.issues }, 400);
}

// Persist the organization ID as a standalone env var so it survives
// config rebuilds (e.g. model hot-swap). The env var is the primary
// source of truth; KILO_CONFIG_CONTENT extraction is the fallback.
process.env.GASTOWN_ORGANIZATION_ID = parsed.data.organizationId ?? '';

console.log(
`[control-server] /agents/start: role=${parsed.data.role} name=${parsed.data.name} rigId=${parsed.data.rigId} agentId=${parsed.data.agentId}`
);
Expand Down Expand Up @@ -211,6 +216,11 @@ app.patch('/agents/:agentId/model', async c => {
return c.json({ error: 'Invalid request body', issues: parsed.error.issues }, 400);
}

// Update org billing context from the request body if provided.
if (parsed.data.organizationId) {
process.env.GASTOWN_ORGANIZATION_ID = parsed.data.organizationId;
}

// Sync config-derived env vars from X-Town-Config into process.env so
// the SDK server restart picks up fresh tokens and git identity.
// The middleware already parsed the header into lastKnownTownConfig.
Expand Down Expand Up @@ -252,6 +262,12 @@ app.patch('/agents/:agentId/model', async c => {
} else {
delete process.env.GASTOWN_DISABLE_AI_COAUTHOR;
}
// organization_id — keep the standalone env var in sync with the
// town config so org billing context is never lost.
const orgId = cfg.organization_id;
if (typeof orgId === 'string' && orgId) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Stale org billing context is never cleared

If organization_id is removed from the town config, this branch leaves the previous GASTOWN_ORGANIZATION_ID in process.env. extractOrganizationId() now prefers that env var over KILO_CONFIG_CONTENT, so later model swaps will continue attributing usage to the old org instead of falling back to the current town ownership. Clear or delete the env var when organization_id is absent.

process.env.GASTOWN_ORGANIZATION_ID = orgId;
}
}

await updateAgentModel(
Expand Down
6 changes: 6 additions & 0 deletions cloudflare-gastown/container/src/process-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,12 @@ export async function sendMessage(agentId: string, prompt: string): Promise<void
* by `buildKiloConfigContent` at agent startup.
*/
function extractOrganizationId(): string | undefined {
// Primary source: standalone env var set by control-server on /agents/start
// and updated on every PATCH /model via X-Town-Config.
const envOrgId = process.env.GASTOWN_ORGANIZATION_ID;
if (envOrgId) return envOrgId;

// Fallback: extract from KILO_CONFIG_CONTENT (legacy path)
const raw = process.env.KILO_CONFIG_CONTENT;
if (!raw) return undefined;
try {
Expand Down
2 changes: 2 additions & 0 deletions cloudflare-gastown/container/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export const UpdateAgentModelRequest = z.object({
smallModel: z.string().optional(),
/** Pre-formatted conversation history to inject into the new session prompt. */
conversationHistory: z.string().optional(),
/** Organization ID — ensures org billing context is preserved across model changes. */
organizationId: z.string().optional(),
});
export type UpdateAgentModelRequest = z.infer<typeof UpdateAgentModelRequest>;

Expand Down
8 changes: 7 additions & 1 deletion cloudflare-gastown/src/dos/Town.do.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2067,14 +2067,20 @@ export class TownDO extends DurableObject<Env> {
// before restarting the SDK server (tokens, git identity, etc.).
const containerConfig = await config.buildContainerConfig(this.ctx.storage, this.env);

// Resolve townConfig to thread the organization_id into the request body
// (belt-and-suspenders: ensures org billing survives even if X-Town-Config
// header parsing fails on the container side).
const townConfig = await config.getTownConfig(this.ctx.storage);

const updated = await dispatch.updateAgentModelInContainer(
this.env,
townId,
mayor.id,
model,
smallModel,
conversationHistory || undefined,
containerConfig
containerConfig,
townConfig.organization_id
);
if (updated) {
console.log(
Expand Down
1 change: 1 addition & 0 deletions cloudflare-gastown/src/dos/town/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,5 +150,6 @@ export async function buildContainerConfig(
disable_ai_coauthor: config.disable_ai_coauthor,
kilo_api_url: env.KILO_API_URL ?? '',
gastown_api_url: env.GASTOWN_API_URL ?? '',
organization_id: config.organization_id,
};
}
4 changes: 3 additions & 1 deletion cloudflare-gastown/src/dos/town/container-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,8 @@ export async function updateAgentModelInContainer(
model: string,
smallModel?: string,
conversationHistory?: string,
containerConfig?: Record<string, unknown>
containerConfig?: Record<string, unknown>,
organizationId?: string
): Promise<boolean> {
try {
const container = getTownContainerStub(env, townId);
Expand All @@ -691,6 +692,7 @@ export async function updateAgentModelInContainer(
model,
...(smallModel ? { smallModel } : {}),
...(conversationHistory ? { conversationHistory } : {}),
...(organizationId ? { organizationId } : {}),
}),
});
return response.ok;
Expand Down
Loading