Skip to content
Merged
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
1 change: 1 addition & 0 deletions .kilo/skills/specs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ to the user if instructions or changes will cause deviations from the original i

| Spec | Governs |
|---|---|
| `.specs/gastown-usage-based-billing.md` | Gastown container pricing, payer attribution, metering lifecycle, and budget enforcement |
| `.specs/kiloclaw-billing.md` | KiloClaw billing, pricing, invoicing, usage metering, payment flows |
| `.specs/kiloclaw-billing-lifecycle.md` | KiloClaw billing lifecycle — credit-renewal orchestration safety |
| `.specs/kiloclaw-composio.md` | KiloClaw Composio credential provisioning, injection, and sharing |
Expand Down
525 changes: 525 additions & 0 deletions .specs/gastown-usage-based-billing.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ Manage shared web env var additions and rotations with `pnpm web:env set <VARIAB
- `NEXT_PUBLIC_CLOUD_AGENT_NEXT_WS_URL` - WebSocket URL for Cloud Agent Next from the browser. [PUBLIC]
- `CLOUD_AGENT_R2_ATTACHMENTS_BUCKET_NAME` - R2 bucket for cloud agent file attachments. [SERVER]
- `GASTOWN_SERVICE_URL` - URL for the Gastown service. [SERVER]
- `GASTOWN_BILLING_ENABLED` - Enables Gastown container usage billing _enforcement_: admission checks and low-balance stops. Does not control metering — usage is always reported to the meter whenever the `CONTAINER_USAGE` binding is present. Enabled in the Gastown Wrangler development environment and defaults to `false` in production. [SERVER]
- `GASTOWN_BILLING_ANNOUNCEMENT_ENABLED` - Set to exactly `true` to show the upcoming usage-based container billing announcement on Gastown town overview pages. Always enabled when Next.js runs in development and defaults to off otherwise. [SERVER]
- `NEXT_PUBLIC_GASTOWN_URL` - Client-side base URL for Gastown. [PUBLIC]
- `O11Y_SERVICE_URL` - URL for the observability (O11Y) service. [SERVER]
- `O11Y_KILO_GATEWAY_CLIENT_SECRET` - Client secret for the O11Y Kilo Gateway. `[SECRET]`
Expand Down
18 changes: 18 additions & 0 deletions apps/web/src/app/(app)/gastown/[townId]/TownOverviewPageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
Layers,
MessageSquare,
Copy,
Info,
} from 'lucide-react';
import { toast } from 'sonner';
import { formatDistanceToNow } from 'date-fns';
Expand All @@ -38,6 +39,7 @@ import { motion, AnimatePresence } from 'motion/react';
import type { GastownOutputs } from '@/lib/gastown/trpc';
import { AdminViewingBanner } from '@/components/gastown/AdminViewingBanner';
import { DrainStatusBanner } from '@/components/gastown/DrainStatusBanner';
import { Banner } from '@/components/shared/Banner';
import { SidebarTrigger } from '@/components/ui/sidebar';
import { useConfirm } from '@/components/ui/confirm';

Expand All @@ -49,6 +51,7 @@ type TownOverviewPageClientProps = {
basePath?: string;
/** When set, integration queries use org-scoped endpoints. */
organizationId?: string;
billingAnnouncementEnabled: boolean;
};

const ROLE_ICONS: Record<string, typeof Bot> = {
Expand Down Expand Up @@ -94,6 +97,7 @@ export function TownOverviewPageClient({
townId,
basePath: basePathOverride,
organizationId,
billingAnnouncementEnabled,
}: TownOverviewPageClientProps) {
const townBasePath = basePathOverride ?? `/gastown/${townId}`;
const router = useRouter();
Expand Down Expand Up @@ -215,6 +219,20 @@ export function TownOverviewPageClient({
<AdminViewingBanner townId={townId} />
<div className="px-6">
<DrainStatusBanner townId={townId} />
{billingAnnouncementEnabled && (
<Banner color="blue" className="my-4 rounded-lg p-3">
<Banner.Icon>
<Info aria-hidden="true" />
</Banner.Icon>
<Banner.Content>
<Banner.Title>Usage-based container billing is coming</Banner.Title>
<Banner.Description>
Charges will use Kilo credits only while the town container is running. We'll share
more details before billing is activated.
</Banner.Description>
</Banner.Content>
</Banner>
)}
</div>
{/* Top bar — sticky */}
<div className="sticky top-0 z-10 flex items-center justify-between border-b border-white/[0.06] bg-[oklch(0.1_0_0)] px-6 py-3">
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/app/(app)/gastown/[townId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getUserFromAuthOrRedirect } from '@/lib/user/server';
import { notFound } from 'next/navigation';
import { isGastownEnabled } from '@/lib/gastown/feature-flags';
import { GASTOWN_BILLING_ANNOUNCEMENT_ENABLED } from '@/lib/config.server';
import { TownOverviewPageClient } from './TownOverviewPageClient';

export default async function TownOverviewPage({
Expand All @@ -15,5 +16,10 @@ export default async function TownOverviewPage({
return notFound();
}

return <TownOverviewPageClient townId={townId} />;
return (
<TownOverviewPageClient
townId={townId}
billingAnnouncementEnabled={GASTOWN_BILLING_ANNOUNCEMENT_ENABLED}
/>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OrganizationByPageLayout } from '@/components/organizations/OrganizationByPageLayout';
import { TownOverviewPageClient } from '@/app/(app)/gastown/[townId]/TownOverviewPageClient';
import { GASTOWN_BILLING_ANNOUNCEMENT_ENABLED } from '@/lib/config.server';

export default async function OrgTownOverviewPage({
params,
Expand All @@ -13,7 +14,12 @@ export default async function OrgTownOverviewPage({
params={params}
fullBleed
render={() => (
<TownOverviewPageClient townId={townId} basePath={basePath} organizationId={id} />
<TownOverviewPageClient
townId={townId}
basePath={basePath}
organizationId={id}
billingAnnouncementEnabled={GASTOWN_BILLING_ANNOUNCEMENT_ENABLED}
/>
)}
/>
);
Expand Down
19 changes: 18 additions & 1 deletion apps/web/src/components/gastown/MayorChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function MayorChat({ townId }: MayorChatProps) {
const trpc = useGastownTRPC();
const queryClient = useQueryClient();
const [collapsed, setCollapsed] = useState(false);
const billingQuery = useQuery(trpc.gastown.getBillingStatus.queryOptions({ townId }));

// Eagerly ensure mayor agent + container on mount
const ensureMayor = useMutation(
Expand Down Expand Up @@ -48,10 +49,16 @@ export function MayorChat({ townId }: MayorChatProps) {
// Reset on townId change so ensureMayor fires for each town
const ensuredTownRef = useRef<string | null>(null);
useEffect(() => {
if (
!billingQuery.isSuccess ||
billingQuery.data.enforcing ||
billingQuery.data.runPolicy === 'paused_by_user'
)
return;
if (ensuredTownRef.current === townId) return;
ensuredTownRef.current = townId;
ensureMayor.mutate({ townId });
}, [townId]);
}, [billingQuery.data, billingQuery.isSuccess, townId]);

// Poll mayor status to get agentId
const statusQuery = useQuery({
Expand All @@ -69,6 +76,16 @@ export function MayorChat({ townId }: MayorChatProps) {
const { terminalRef, connected, status, fitAddonRef } = useXtermPty({
townId,
agentId: mayorAgentId,
enabled:
billingQuery.isSuccess &&
billingQuery.data.runPolicy === 'automatic' &&
(!billingQuery.data.enforcing ||
Comment thread
jrf0110 marked this conversation as resolved.
billingQuery.data.state === 'running' ||
billingQuery.data.state === 'warning' ||
billingQuery.data.state === 'starting' ||
// `degraded` is a transient metering hiccup; the container keeps
// running per spec, so don't disconnect the terminal.
billingQuery.data.state === 'degraded'),
retries: 10,
retryDelay: 3_000,
});
Expand Down
Loading
Loading