Bug
On org-owned towns, after refreshing the container token and changing the mayor's model in settings, the mayor's LLM calls start billing to the user's personal account instead of the organization. The refinery (which was not restarted) continues billing correctly through the org. The mayor reports "this is a paid model and you don't have the balance" even though the org has credits.
Root Cause
The organizationId for billing is passed to the container only once — in the initial POST /agents/start request body (container-dispatch.ts:441):
organizationId: params.townConfig.organization_id,
This gets embedded into KILO_CONFIG_CONTENT via buildKiloConfigContent (agent-runner.ts:51-53):
if (organizationId) {
providerOptions.kilocodeOrganizationId = organizationId;
}
When updateMayorModel triggers a model change, the PATCH /agents/:agentId/model request does NOT include organizationId (container-dispatch.ts:687-694):
body: JSON.stringify({
model,
...(smallModel ? { smallModel } : {}),
...(conversationHistory ? { conversationHistory } : {}),
// ❌ organizationId is MISSING
}),
The container-side updateAgentModel (process-manager.ts:807-808) tries to preserve the org by extracting it from process.env.KILO_CONFIG_CONTENT:
const organizationId = extractOrganizationId();
This extraction is fragile. It fails when:
process.env.KILO_CONFIG_CONTENT was cleared or corrupted by a prior failed model update
- The token refresh caused
KILOCODE_TOKEN to be momentarily undefined, causing buildKiloConfigContent to skip the rebuild (guarded by if (kilocodeToken) at line 813)
- The PATCH handler's X-Town-Config header sync overwrote env vars without preserving the config content
- A container restart between the token refresh and model change cleared process.env entirely
Once extractOrganizationId() returns undefined, the rebuilt KILO_CONFIG_CONTENT omits kilocodeOrganizationId. All subsequent LLM calls go through the user's personal account.
Why the Refinery Is Unaffected
The refinery was started before the token refresh and model change. Its KILO_CONFIG_CONTENT still contains the original kilocodeOrganizationId from its initial dispatch. Since it was not restarted, its env was never overwritten.
Fix
Fix 1 (Recommended): Store organizationId as a standalone env var
At container startup (control-server.ts, start handler), set:
process.env.GASTOWN_ORGANIZATION_ID = request.organizationId ?? "";
In extractOrganizationId() (process-manager.ts:752-765), read from the standalone var first:
function extractOrganizationId(): string | undefined {
// Authoritative source — set once at container startup, never overwritten
if (process.env.GASTOWN_ORGANIZATION_ID) {
return process.env.GASTOWN_ORGANIZATION_ID;
}
// Fallback — extract from KILO_CONFIG_CONTENT
// ... existing extraction logic
}
This is resilient to config rebuilds, token refreshes, and model changes.
Fix 2 (Belt-and-suspenders): Pass organizationId in the model update request
Add organizationId to updateAgentModelInContainer (container-dispatch.ts:672-700):
body: JSON.stringify({
model,
...(smallModel ? { smallModel } : {}),
...(conversationHistory ? { conversationHistory } : {}),
organizationId: townConfig.organization_id, // ← ADD THIS
}),
And resolve townConfig in updateMayorModel to pass it through.
Fix 3 (Defense-in-depth): Include organization_id in buildContainerConfig
config.ts:136-154 builds the X-Town-Config payload but omits organization_id. Add it:
return {
...existing fields,
organization_id: townConfig.organization_id,
};
The PATCH handler in control-server.ts can then extract it and ensure GASTOWN_ORGANIZATION_ID stays current.
Files
| File |
Line |
Issue |
container-dispatch.ts |
687-694 |
updateAgentModelInContainer body missing organizationId |
container-dispatch.ts |
441 |
Only place organizationId is passed (initial start) |
src/dos/town/config.ts |
136-154 |
buildContainerConfig missing organization_id |
container/src/process-manager.ts |
752-765 |
extractOrganizationId — fragile, reads from KILO_CONFIG_CONTENT JSON |
container/src/process-manager.ts |
807-818 |
Model update relies on fragile extraction |
container/src/control-server.ts |
214-255 |
PATCH handler syncs config but not org ID |
src/dos/Town.do.ts |
2053-2089 |
updateMayorModel does not pass org context |
Impact
High — org customers get billed to their personal account after any model change or mayor restart. This causes confusing "insufficient balance" errors when the org has ample credits.
Bug
On org-owned towns, after refreshing the container token and changing the mayor's model in settings, the mayor's LLM calls start billing to the user's personal account instead of the organization. The refinery (which was not restarted) continues billing correctly through the org. The mayor reports "this is a paid model and you don't have the balance" even though the org has credits.
Root Cause
The
organizationIdfor billing is passed to the container only once — in the initialPOST /agents/startrequest body (container-dispatch.ts:441):This gets embedded into
KILO_CONFIG_CONTENTviabuildKiloConfigContent(agent-runner.ts:51-53):When
updateMayorModeltriggers a model change, the PATCH/agents/:agentId/modelrequest does NOT includeorganizationId(container-dispatch.ts:687-694):The container-side
updateAgentModel(process-manager.ts:807-808) tries to preserve the org by extracting it fromprocess.env.KILO_CONFIG_CONTENT:This extraction is fragile. It fails when:
process.env.KILO_CONFIG_CONTENTwas cleared or corrupted by a prior failed model updateKILOCODE_TOKENto be momentarily undefined, causingbuildKiloConfigContentto skip the rebuild (guarded byif (kilocodeToken)at line 813)Once
extractOrganizationId()returnsundefined, the rebuiltKILO_CONFIG_CONTENTomitskilocodeOrganizationId. All subsequent LLM calls go through the user's personal account.Why the Refinery Is Unaffected
The refinery was started before the token refresh and model change. Its
KILO_CONFIG_CONTENTstill contains the originalkilocodeOrganizationIdfrom its initial dispatch. Since it was not restarted, its env was never overwritten.Fix
Fix 1 (Recommended): Store
organizationIdas a standalone env varAt container startup (
control-server.ts, start handler), set:In
extractOrganizationId()(process-manager.ts:752-765), read from the standalone var first:This is resilient to config rebuilds, token refreshes, and model changes.
Fix 2 (Belt-and-suspenders): Pass
organizationIdin the model update requestAdd
organizationIdtoupdateAgentModelInContainer(container-dispatch.ts:672-700):And resolve
townConfiginupdateMayorModelto pass it through.Fix 3 (Defense-in-depth): Include
organization_idinbuildContainerConfigconfig.ts:136-154builds the X-Town-Config payload but omitsorganization_id. Add it:The PATCH handler in
control-server.tscan then extract it and ensureGASTOWN_ORGANIZATION_IDstays current.Files
container-dispatch.tsupdateAgentModelInContainerbody missingorganizationIdcontainer-dispatch.tsorganizationIdis passed (initial start)src/dos/town/config.tsbuildContainerConfigmissingorganization_idcontainer/src/process-manager.tsextractOrganizationId— fragile, reads from KILO_CONFIG_CONTENT JSONcontainer/src/process-manager.tscontainer/src/control-server.tssrc/dos/Town.do.tsupdateMayorModeldoes not pass org contextImpact
High — org customers get billed to their personal account after any model change or mayor restart. This causes confusing "insufficient balance" errors when the org has ample credits.