Skip to content

refactor(machine): Terminal→Machine naming coherence sweep (part 2) - #2012

Merged
2witstudios merged 7 commits into
masterfrom
pu/machine-rename-part2
Jul 11, 2026
Merged

refactor(machine): Terminal→Machine naming coherence sweep (part 2)#2012
2witstudios merged 7 commits into
masterfrom
pu/machine-rename-part2

Conversation

@2witstudios

@2witstudios 2witstudios commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Finishes the rename #1992 deliberately stopped halfway through. Before this PR a PageType.MACHINE page was rendered by TerminalView.tsx out of a directory literally named page-views/terminal/, useTerminalWorkspaceStore was keyed by machineId, terminal_sessions stored Machine substrate state, and TerminalSession was an exact type-name collision — the DB substrate row and the realtime PTY session shared one name.

One vocabulary now: Machine = the persistent page/Sprite substrate. Terminal = an interactive PTY pane inside a Machine.

Ships as one atomic PR: a half-done rename breaks the build, which is exactly what #1993 had to hotfix.

What changed (the 6 groups)

  1. Page-view surfacegit mv page-views/terminal/ → machine/ (history preserved); TerminalView → MachineView; TerminalWorkspace → MachineWorkspace; TerminalKeepAliveHost → MachineKeepAliveHost + terminal-keepalive.ts → machine-keepalive.ts. uiComponent string and the componentName === 'TerminalView' check in CenterPanel.tsx updated together.
  2. Workspace storestores/terminal-workspace/ → machine-workspace/; useTerminalWorkspaceStore → useMachineWorkspaceStore.
  3. DBterminal_sessions → machine_sessions (table + indexes + pkey). This resolves the TerminalSession collision. The two *Access columns on live tables are deliberately NOT renamed — see the deploy-safety note below.
  4. Substrate servicesterminal-session-manager → machine-session-manager, terminal-storage-{reconcile,billing,measure} → machine-storage-*, terminal-payer → machine-payer, terminal-pricing → machine-pricing, cron route reconcile-terminal-storage → reconcile-machine-storage.
  5. User-facing rebrand — the page type now displays as Machine ("A persistent sandboxed machine — clone repos, run code, and open terminals"); "Terminal Access" → "Machine Access"; billing card "Terminals" → "Machines". QuickCreatePalette and PageTypeIcon are config-driven, so they rebrand from page-types.config.ts with no code change.
  6. Residual terminalId — gone from all code; only immutable historical migrations still contain it.

One name I had to choose

machine-session-manager.ts's low-level acquireTerminalSandbox could not become acquireMachineSandboxmachine-session.ts already exports an acquireMachineSandbox (the authorize-then-acquire wrapper, Bucket A). The low-level one is now acquireMachineSession, which also matches its module. machine-projects.ts's pre-existing acquireMachineSandbox deps property is untouched.

Bucket B — same-word hits deliberately LEFT ALONE

These are genuine terminal/PTY concepts. Renaming them would be a bug, so please confirm the boundary holds:

Kept Why
XtermTerminal, TerminalPanes, TerminalPaneState, TerminalColumnState, OpenTerminalScope, openTerminal pane-level PTY state
The "Terminal" tab + TerminalTab.tsx (and TerminalList, useNodeTerminals) Terminal survives as one tab name inside a Machine
apps/realtime/src/terminal/ dir, terminal-session-map.ts and its TerminalSession type the PTY session — the other half of the collision
agent-terminals.ts / -store / -types, agent-terminal-handler.ts, agent-terminal:* socket events, buildAgentTerminalSessionKey, agent-terminals-runtime.ts, terminal-activity.ts agent terminals
machine_agent_terminals table Bucket A/B — already correctly named
SquareTerminal / TerminalSquare lucide icon names
compliance TERMINAL status, isTerminalStatus, AI TerminalReason/terminalReason, GIT_TERMINAL_PROMPT unrelated sense of the word ("final")

Behaviour-bearing strings I did NOT rename (deliberate)

  • NAMESPACE_VERSION = 'terminal-session:v1' (machine-session-manager.ts) — HMAC input for every session key. Renaming it re-derives every key and orphans the warm Sprite behind each live machine_sessions row. Annotated in place so nobody "tidies" it later.
  • Usage-log data valuessource: 'terminal', metadata.type: 'terminal_machine' | 'terminal_storage', model: 'terminal-machine-storage'. These are persisted billing-attribution values; rewriting them without a data backfill would silently split historical usage across two labels. Out of scope for a rename PR.

(SandboxSurface's 'terminal' member was renamed to 'machine' — it's a pure in-memory discriminator that never reaches the DB or the emitted egress tag.)

⚠️ Operator-facing: Machine billing env vars renamed

Every Machine-billing env var is now MACHINE_* (was TERMINAL_*):

Old New
TERMINAL_HOLD_ESTIMATE_CENTS MACHINE_HOLD_ESTIMATE_CENTS
TERMINAL_MARKUP_BPS MACHINE_MARKUP_BPS
TERMINAL_MAX_INFLIGHT MACHINE_MAX_INFLIGHT
TERMINAL_MARKUP_FLOOR_BPS MACHINE_MARKUP_FLOOR_BPS
TERMINAL_USD_PER_CPU_HOUR MACHINE_USD_PER_CPU_HOUR
TERMINAL_USD_PER_MEM_GB_HOUR MACHINE_USD_PER_MEM_GB_HOUR
TERMINAL_ASSUMED_CPUS MACHINE_ASSUMED_CPUS
TERMINAL_ASSUMED_MEMORY_GB MACHINE_ASSUMED_MEMORY_GB
TERMINAL_STORAGE_USD_PER_GB_MONTH MACHINE_STORAGE_USD_PER_GB_MONTH
TERMINAL_STORAGE_MEASURE_THROTTLE_MS MACHINE_STORAGE_MEASURE_THROTTLE_MS

No action required, and no compat fallback, because there is nothing to fall back to: none of these is set in any repo config (.env*, fly.toml, docker, infra) or as a Fly secret on any deployed app (pagespace-web/realtime/cron/processor/admin — all verified via fly secrets list, which reports names even though values are digest-only). Every deployment already runs on the documented defaults, so renaming the keys changes no computed hold, markup or rate. Machine is also still gated behind CODE_EXECUTION_ENABLED, and repo policy is a hard cutover for unreleased features. If you ever set one of these, use the new name.

Caught by codex review (P2): the codemod had silently rewritten two of these keys as a side effect of renaming their TS constants, leaving the family half-renamed. The rest of the diff was re-audited for the same class of bug — those two were the only external-contract strings touched (no socket event names, no storage keys, no other process.env reads).

Deploy safety: why no live column is renamed

An earlier revision of this PR also renamed two columns on live tables
(pages."terminalAccess", global_assistant_config.terminal_access). That was a production regression and has been reverted.

Deploys run migrations in a separate Fly machine before the new app image takes traffic (docker-images.yml: "Run migrations" line 155, "Deploy web" line 326). Both of those columns are read and written by endpoints that are not behind CODE_EXECUTION_ENABLED and serve live traffic today (api/pages/[pageId]/agent-config, api/user/assistant-config). Renaming them would drop the column out from under the still-running old image and 500 every one of those requests until the new image went live several steps later. A column rename has no safe deploy order — only an expand/contract across two releases does.

So the columns stay put and the code stays coherent, using drizzle's field/column decoupling:

machineAccess: boolean('terminalAccess')   // core.ts        (physical column unchanged)
machineAccess: boolean('terminal_access')  // integrations.ts (physical column unchanged)

The TS field is machineAccess everywhere; only the physical column names remain legacy. Verified against a real Postgres that the mapping round-trips: reading pages.machineAccess returns the legacy column's value, and writing machineAccess: false lands in pages."terminalAccess".

machine_sessions is renamed, because — unlike those two — it is read exclusively behind CODE_EXECUTION_ENABLED (off), so no running code touches it during the window.

Those physical column names are the only residual terminal_* left in the schema: a deliberate trade (zero-downtime beats cosmetic column naming) and a clean expand/contract follow-up.

Verification

  • db:generate produced a pure ALTER TABLE ... RENAME migration (0200) — no rebuild, no DROP TABLE/DROP COLUMN, no data rewrite — and now reports "No schema changes" (snapshot chain intact).
  • 0201 renames the leftover terminal_sessions_pkey constraint. Postgres doesn't rename a pkey with its table and drizzle doesn't track pkey names, so db:generate will never emit it. Hand-authored, following the 0195_migrate_machine_refs_* precedent from refactor(machines): rename TERMINAL page type → MACHINE, terminalId FK → machineId #1992; guarded with an existence check and metadata-only.
  • ✅ Full 201-migration chain applies clean to a fresh Postgres: machine_sessions exists with machine_sessions_page_id_idx, machine_sessions_last_active_at_idx, machine_sessions_pkey, both FKs and the unique constraint — zero residual terminal_session* objects.
  • Upgrade-with-data path tested too (the one a deployed DB actually takes, and where a rename can lose rows): migrated a throwaway PG to master's schema, seeded a live terminal_sessions row + both boolean columns, then applied only 0200+0201. Every value survived (sessionKey/sandboxId/storageMeasuredBytes, machineAccess=true, machine_access=true), and ON DELETE CASCADE still fires after the FK drop/recreate — so a warm Sprite keeps its sessionKey → sandboxId mapping and reattaches instead of being orphaned. Full output in this comment.
  • bun run typecheck green monorepo-wide (16/16); bun run build green (incl. apps/web + apps/realtime); bun run lint clean.
  • ✅ Tests: 1483 packages/lib + 641 apps/realtime + 2058 apps/web passing.
  • TerminalSession now resolves to exactly two meanings by file — MachineSession in packages/db/packages/lib, PTY TerminalSession only in apps/realtime/src/terminal/ — and no file references both.

Notes for the reviewer

  • The renamed cron route (/api/cron/reconcile-machine-storage) is not registered in docker/cron/crontab — it never was. So renaming its path is zero-risk, but the route is still unscheduled; that gap predates this PR.
  • scripts/fix-wrong-imports.mjs still listed isTerminalPage, an export refactor(machines): rename TERMINAL page type → MACHINE, terminalId FK → machineId #1992 deleted. Fixed to isMachinePage.
  • Deploy safety (important): the migration contains zero live-table DDL. See the section above for why.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J2dVpQa811gvgVtZuY1866

#1992 renamed the substrate (PageType.MACHINE, machineId, machine_* tables)
but stopped there to stay atomic. This finishes the job: a MACHINE page is no
longer rendered by TerminalView.tsx from a directory named page-views/terminal/,
and TerminalSession is no longer an exact type-name collision between the DB
substrate row and the realtime PTY session.

One vocabulary: Machine = the persistent page/Sprite substrate. Terminal = an
interactive PTY pane inside a Machine (kept, deliberately — see PR body).

- Page-view surface: page-views/terminal/ → machine/; TerminalView → MachineView;
  TerminalWorkspace → MachineWorkspace; TerminalKeepAliveHost → MachineKeepAliveHost.
- Store: stores/terminal-workspace/ → machine-workspace/; useTerminalWorkspaceStore
  → useMachineWorkspaceStore. Pane-level types (TerminalPaneState, OpenTerminalScope)
  kept — they are genuine PTY concepts.
- DB: terminal_sessions → machine_sessions, pages."terminalAccess" → "machineAccess",
  global_assistant_config.terminal_access → machine_access. All ALTER ... RENAME;
  no table rebuild, no data rewrite.
- Substrate services: terminal-session-manager → machine-session-manager (low-level
  acquire renamed acquireMachineSession to avoid colliding with machine-session.ts's
  pre-existing acquireMachineSandbox wrapper), terminal-storage-* → machine-storage-*,
  terminal-payer → machine-payer, terminal-pricing → machine-pricing, cron route
  reconcile-terminal-storage → reconcile-machine-storage.
- User-facing: page type displays as "Machine"; "Terminal Access" → "Machine Access";
  billing card "Terminals" → "Machines".

Verified: db:generate emits a pure RENAME migration and then reports no drift; the
full chain applies clean to a fresh Postgres with machine_sessions + renamed indexes,
constraints and pkey; typecheck, build and lint green monorepo-wide; 1483 lib + 641
realtime + 2058 web tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2dVpQa811gvgVtZuY1866
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@2witstudios, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d6cff548-eacf-49e9-8a8c-3bd89314be39

📥 Commits

Reviewing files that changed from the base of the PR and between 4365f35 and c2915e5.

📒 Files selected for processing (135)
  • apps/realtime/src/__tests__/index.test.ts
  • apps/realtime/src/index.ts
  • apps/realtime/src/terminal/__tests__/agent-terminal-access.test.ts
  • apps/realtime/src/terminal/__tests__/agent-terminal-session-key.test.ts
  • apps/realtime/src/terminal/__tests__/cold-connect-sprite-reads.test.ts
  • apps/realtime/src/terminal/agent-terminal-access.ts
  • apps/realtime/src/terminal/agent-terminal-session-key.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/__tests__/route.test.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/config/__tests__/route.test.ts
  • apps/web/src/app/api/cron/reconcile-machine-storage/__tests__/route.test.ts
  • apps/web/src/app/api/cron/reconcile-machine-storage/route.ts
  • apps/web/src/app/api/integrations/github/repos/route.ts
  • apps/web/src/app/api/pages/[pageId]/agent-config/__tests__/route.test.ts
  • apps/web/src/app/api/pages/[pageId]/agent-config/route.ts
  • apps/web/src/app/api/tasks/__tests__/route.test.ts
  • apps/web/src/app/api/user/assistant-config/__tests__/route.test.ts
  • apps/web/src/app/api/user/assistant-config/route.ts
  • apps/web/src/app/settings/ai/MachineAccessCard.tsx
  • apps/web/src/app/settings/ai/__tests__/MachineAccessCard.test.tsx
  • apps/web/src/app/settings/ai/page.tsx
  • apps/web/src/app/settings/usage/page.tsx
  • apps/web/src/components/ai/page-agents/PageAgentSettingsTab.tsx
  • apps/web/src/components/billing/MachineUsageCard.tsx
  • apps/web/src/components/layout/middle-content/CenterPanel.tsx
  • apps/web/src/components/layout/middle-content/MachineKeepAliveHost.tsx
  • apps/web/src/components/layout/middle-content/__tests__/machine-keepalive.test.ts
  • apps/web/src/components/layout/middle-content/machine-keepalive.ts
  • apps/web/src/components/layout/middle-content/page-views/machine/MachineView.test.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/MachineView.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/XtermTerminal.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/tabs/CodeFilePane.test.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/tabs/CodeFilePane.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/tabs/CodeTab.test.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/tabs/CodeTab.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/tabs/DiffFileCard.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/tabs/DiffTab.test.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/tabs/DiffTab.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/tabs/SettingsTab.test.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/tabs/SettingsTab.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/tabs/TabSidebar.test.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/tabs/TabSidebar.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/tabs/TerminalTab.test.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/tabs/TerminalTab.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/tabs/checkout-states.ts
  • apps/web/src/components/layout/middle-content/page-views/machine/tabs/tab-states.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/workspace/ConfirmRemoveDialog.test.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/workspace/ConfirmRemoveDialog.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/workspace/MachineFileTree.test.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/workspace/MachineFileTree.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/workspace/MachineTree.test.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/workspace/MachineTree.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/workspace/MachineWorkspace.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/workspace/RemoveButton.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/workspace/TerminalPanes.test.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/workspace/TerminalPanes.tsx
  • apps/web/src/components/layout/middle-content/page-views/machine/workspace/deriveProjectFieldsFromRepo.test.ts
  • apps/web/src/components/layout/right-sidebar/index.tsx
  • apps/web/src/hooks/useMachineDiff.ts
  • apps/web/src/hooks/useMachineSettings.ts
  • apps/web/src/lib/ai/tools/__tests__/sandbox-git-tools.test.ts
  • apps/web/src/lib/ai/tools/__tests__/sandbox-tools-runtime.test.ts
  • apps/web/src/lib/ai/tools/__tests__/sandbox-tools.test.ts
  • apps/web/src/lib/ai/tools/sandbox-tools-runtime.ts
  • apps/web/src/lib/ai/tools/sandbox-tools.ts
  • apps/web/src/lib/machines/agent-terminals-runtime.ts
  • apps/web/src/lib/machines/machine-branches-runtime.ts
  • apps/web/src/lib/machines/machine-projects-runtime.ts
  • apps/web/src/lib/machines/machine-settings-runtime.ts
  • apps/web/src/lib/repositories/__tests__/global-machine-config-repository.test.ts
  • apps/web/src/lib/repositories/__tests__/page-agent-repository.test.ts
  • apps/web/src/lib/repositories/global-machine-config-repository.ts
  • apps/web/src/lib/repositories/page-agent-repository.ts
  • apps/web/src/lib/subscription/__tests__/usage-breakdown.test.ts
  • apps/web/src/lib/subscription/usage-breakdown-query.ts
  • apps/web/src/lib/subscription/usage-breakdown.ts
  • apps/web/src/stores/machine-workspace/__tests__/useMachineWorkspaceStore.test.ts
  • apps/web/src/stores/machine-workspace/__tests__/workspace-reducer.test.ts
  • apps/web/src/stores/machine-workspace/useMachineWorkspaceStore.ts
  • apps/web/src/stores/machine-workspace/workspace-reducer.ts
  • docs/sprites/services-adoption-design.md
  • packages/db/drizzle/0200_spicy_whiplash.sql
  • packages/db/drizzle/0201_rename_machine_sessions_pkey.sql
  • packages/db/drizzle/meta/0200_snapshot.json
  • packages/db/drizzle/meta/0201_snapshot.json
  • packages/db/drizzle/meta/_journal.json
  • packages/db/package.json
  • packages/db/src/schema.ts
  • packages/db/src/schema/core.ts
  • packages/db/src/schema/integrations.ts
  • packages/db/src/schema/machine-agent-terminals.ts
  • packages/db/src/schema/machine-projects.ts
  • packages/db/src/schema/machine-sessions.ts
  • packages/lib/package.json
  • packages/lib/src/billing/__tests__/credit-backfill.test.ts
  • packages/lib/src/billing/__tests__/credit-pricing.test.ts
  • packages/lib/src/billing/__tests__/credits-flow.integration.test.ts
  • packages/lib/src/billing/__tests__/machine-payer.test.ts
  • packages/lib/src/billing/credit-backfill.ts
  • packages/lib/src/billing/credit-consume.ts
  • packages/lib/src/billing/credit-pricing.ts
  • packages/lib/src/billing/machine-payer.ts
  • packages/lib/src/content/__tests__/page-types-machine.test.ts
  • packages/lib/src/content/page-types.config.ts
  • packages/lib/src/integrations/repositories/config-repository.ts
  • packages/lib/src/monitoring/__tests__/machine-pricing.test.ts
  • packages/lib/src/monitoring/__tests__/terminal-pricing.test.ts
  • packages/lib/src/monitoring/ai-monitoring.ts
  • packages/lib/src/monitoring/machine-pricing.ts
  • packages/lib/src/services/machines/__tests__/machine-access.test.ts
  • packages/lib/src/services/machines/agent-terminals.ts
  • packages/lib/src/services/machines/branch-session.ts
  • packages/lib/src/services/machines/machine-branches.ts
  • packages/lib/src/services/machines/machine-projects-store.ts
  • packages/lib/src/services/machines/machine-projects.ts
  • packages/lib/src/services/sandbox/__tests__/egress-ip.test.ts
  • packages/lib/src/services/sandbox/__tests__/env-reads.test.ts
  • packages/lib/src/services/sandbox/__tests__/machine-billing.test.ts
  • packages/lib/src/services/sandbox/__tests__/machine-session-manager.test.ts
  • packages/lib/src/services/sandbox/__tests__/machine-session.test.ts
  • packages/lib/src/services/sandbox/__tests__/machine-storage-billing.test.ts
  • packages/lib/src/services/sandbox/__tests__/machine-storage-measure.test.ts
  • packages/lib/src/services/sandbox/__tests__/machine-storage-reconcile.test.ts
  • packages/lib/src/services/sandbox/__tests__/network-options.test.ts
  • packages/lib/src/services/sandbox/__tests__/persistent-machine-fs.test.ts
  • packages/lib/src/services/sandbox/machine-billing.ts
  • packages/lib/src/services/sandbox/machine-session-manager.ts
  • packages/lib/src/services/sandbox/machine-session.ts
  • packages/lib/src/services/sandbox/machine-storage-billing.ts
  • packages/lib/src/services/sandbox/machine-storage-measure.ts
  • packages/lib/src/services/sandbox/machine-storage-reconcile.ts
  • packages/lib/src/services/sandbox/network-options.ts
  • packages/lib/src/services/sandbox/sandbox-client/machine-host-adapter.ts
  • packages/lib/src/services/sandbox/sandbox-client/types.ts
  • packages/lib/src/services/sandbox/tool-runners.ts
  • scripts/fix-wrong-imports.mjs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pu/machine-rename-part2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b761a20097

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* overdraw is bounded by TERMINAL_MAX_INFLIGHT instead. Tune via env.
*/
export const TERMINAL_HOLD_ESTIMATE_CENTS = envInt('TERMINAL_HOLD_ESTIMATE_CENTS', 2);
export const MACHINE_HOLD_ESTIMATE_CENTS = envInt('MACHINE_HOLD_ESTIMATE_CENTS', 2);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep legacy terminal env overrides working

On deployments that already customized terminal billing via TERMINAL_HOLD_ESTIMATE_CENTS or TERMINAL_MARKUP_BPS, this naming sweep silently ignores those variables and falls back to the new defaults (MACHINE_MARKUP_BPS below has the same issue). That can change credit holds or reduce a deliberately higher markup without any config error; please either keep reading the old env names as fallbacks or coordinate a config migration.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch, and it was worse than the symptom you saw — thank you.

The codemod renamed the TS constants, and because the env keys are string literals matching those constant names, it silently rewrote the keys inside envInt(...) too. Env names are an operator-facing contract (same category as the terminal-session:v1 HMAC namespace, which I deliberately left alone for exactly this reason), so that change should never have been implicit. It also left the file half-renamed: MACHINE_MARKUP_BPS sitting next to TERMINAL_MAX_INFLIGHT, TERMINAL_RATES, TERMINAL_ASSUMED_*.

What I did (d0c42b1): rather than patch the two, I finished the family, so the rename is intentional and complete instead of accidental and partial. All nine Machine-billing env vars are now MACHINE_* (MACHINE_MAX_INFLIGHT, MACHINE_MARKUP_FLOOR_BPS, MACHINE_RATES, MACHINE_USD_PER_CPU_HOUR, MACHINE_USD_PER_MEM_GB_HOUR, MACHINE_ASSUMED_CPUS, MACHINE_ASSUMED_MEMORY_GB, MACHINE_STORAGE_USD_PER_GB_MONTH, MACHINE_STORAGE_MEASURE_THROTTLE_MS).

On the fallback specifically — I went with a hard cutover, and here's the evidence that nothing can break:

None of these variables is set anywhere. Not in any repo config (.env*, fly.toml, docker, infra — zero hits), and not as a Fly secret on any deployed app:

pagespace-web:       no TERMINAL_*/MACHINE_* secrets
pagespace-realtime:  no TERMINAL_*/MACHINE_* secrets
pagespace-cron:      no TERMINAL_*/MACHINE_* secrets
pagespace-processor: no TERMINAL_*/MACHINE_* secrets
pagespace-admin:     no TERMINAL_*/MACHINE_* secrets

(fly secrets list reports names even though values are digest-only, so this is a definitive check.) Every deployment is therefore already running on the documented defaults, which means renaming the key changes no computed hold, markup, or rate — the concrete risk you identified (a deliberately higher markup being silently lowered) has no instance. Combined with the repo's hard-cutover policy for unreleased features (Machine is gated behind CODE_EXECUTION_ENABLED), a permanent envInt('MACHINE_X', envInt('TERMINAL_X', default)) shim would be dead code on every host that exists. If a deployment had set one, I'd have taken your fallback suggestion instead.

Generalizing your finding: I re-audited the whole diff for the same class of bug — codemod rewriting a string that's actually an external contract. These two env keys were the only ones. No socket event names, no storage keys, no other process.env reads changed. The persisted billing values (source: 'terminal', metadata.type) and the HMAC namespace were already explicitly excluded and are documented in the PR body.

Verified: build + typecheck green monorepo-wide, 1318 packages/lib billing/monitoring/sandbox tests pass (including the MACHINE_MARKUP_BPS override tests, which exercise the renamed key end to end).

Leaving this thread open for your verification.

2witstudios and others added 3 commits July 11, 2026 15:13
…w: codex P2)

The part-2 codemod rewrote two env-var STRING keys as a side effect of renaming
their TS constants — `TERMINAL_HOLD_ESTIMATE_CENTS` and `TERMINAL_MARKUP_BPS`
became `MACHINE_*` inside their `envInt(...)` calls. Env names are an
operator-facing contract (same category as the `terminal-session:v1` HMAC
namespace), so that was an accidental, silent, and half-applied change: it left
credit-pricing.ts with MACHINE_MARKUP_BPS sitting next to TERMINAL_MAX_INFLIGHT,
TERMINAL_RATES and TERMINAL_ASSUMED_*.

Rather than patch the two, finish the family so the rename is intentional and
complete. Every Machine-billing env var is now MACHINE_*:

  TERMINAL_MAX_INFLIGHT                 -> MACHINE_MAX_INFLIGHT
  TERMINAL_MARKUP_FLOOR_BPS             -> MACHINE_MARKUP_FLOOR_BPS
  TERMINAL_RATES                        -> MACHINE_RATES
  TERMINAL_USD_PER_CPU_HOUR             -> MACHINE_USD_PER_CPU_HOUR
  TERMINAL_USD_PER_MEM_GB_HOUR          -> MACHINE_USD_PER_MEM_GB_HOUR
  TERMINAL_ASSUMED_CPUS                 -> MACHINE_ASSUMED_CPUS
  TERMINAL_ASSUMED_MEMORY_GB            -> MACHINE_ASSUMED_MEMORY_GB
  TERMINAL_STORAGE_USD_PER_GB_MONTH     -> MACHINE_STORAGE_USD_PER_GB_MONTH
  TERMINAL_STORAGE_MEASURE_THROTTLE_MS  -> MACHINE_STORAGE_MEASURE_THROTTLE_MS

No compat fallback, because there is nothing to fall back to: none of these vars
is set in any repo config (.env*, fly.toml, docker, infra) or as a Fly secret on
pagespace-web/realtime/cron/processor/admin — verified against every deployed
app. Every deployment therefore runs on the documented defaults, so renaming the
key changes no computed hold, markup or rate. Repo policy is a hard cutover for
unreleased features (Machine is gated behind CODE_EXECUTION_ENABLED), so a
permanent fallback shim would be dead code.

Audited the rest of the diff for the same class of bug: these two were the ONLY
external-contract strings the codemod touched. No socket event names, no
storage keys, no other process.env reads changed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2dVpQa811gvgVtZuY1866
Self-review leftover: machine-access.test.ts (which tests canViewMachine /
canEditMachine — pure Machine substrate) still seeded its page id from a
TERMINAL_ID = 'terminal-1' fixture. Now MACHINE_ID = 'machine-1'.

Also confirmed the remaining `terminal` hits under services/{machines,sandbox},
billing and monitoring are all intentional: they track the persisted
`source: 'terminal'` usage-log value (the markup-override map is keyed BY that
source, so it must match) and the unrelated "terminal status" sense.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2dVpQa811gvgVtZuY1866
The rename left one silent-failure coupling untested: `uiComponent:
'MachineView'` (page-types.config.ts, packages/lib) has to match the hard-coded
string CenterPanel.tsx dispatches on (`componentName === 'MachineView'`,
apps/web). Two literals, two packages, nothing tying them together — rename one
side alone and a Machine page renders NOTHING rather than failing to compile.
A Terminal->Machine sweep is exactly the change that drifts them apart.

Also locks the user-facing rebrand: QuickCreatePalette and PageTypeIcon are
config-driven, so `displayName: 'Machine'` is the single value that makes the
create palette and page tree say Machine instead of Terminal — which is what
the epic's manual-verification checklist asks a human to eyeball.

Mutation-checked: flipping uiComponent back to 'TerminalView' fails the test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2dVpQa811gvgVtZuY1866
@2witstudios

Copy link
Copy Markdown
Owner Author

Additional verification: the migration was tested as an upgrade with live data, not just on a fresh DB

The PR body originally claimed only "applies clean to a fresh Postgres". That's the easy path — it exercises CREATE TABLE machine_sessions and never actually runs the rename against real rows. The path every deployed database will take is an upgrade, and that's where a table/column rename can silently lose data or drop a constraint. So I tested it properly:

  1. Migrated a throwaway Postgres to master's schema (through 0199) — i.e. pre-rename, with terminal_sessions, pages."terminalAccess", global_assistant_config.terminal_access.
  2. Seeded a real object graph: user → drive → MACHINE page → a live terminal_sessions row (sessionKey, sandboxId, storageMeasuredBytes), plus both boolean columns set to true.
  3. Applied only 0200 + 0201 — exactly what a deployed DB receives.

Result:

machine_sessions row: ts1 | sessionKey=key-abc | sandboxId=sprite-xyz | bytes=12345
pages.machineAccess       = true
gac.machine_access        = true
row counts: machine_sessions=1, pages(machineAccess)=1, gac(machine_access)=1

Every value survived. And the constraint that mattered most still works: 0200 drops and re-adds the FKs (that's how drizzle re-points them at the renamed table), so the real risk was silently losing ON DELETE CASCADE. Deleting the page cascades the session row exactly as before:

DELETE FROM pages WHERE id='p1';  -->  machine_sessions after page delete = 0

Final object state — zero residual terminal_*, including the pkey that 0201 fixes:

machine_sessions_pageId_pages_id_fk
machine_sessions_pkey
machine_sessions_sessionKey_unique
machine_sessions_userId_users_id_fk
0 residual terminal_session* objects

Practical upshot: a live Machine with a warm Sprite keeps its sessionKeysandboxId mapping across this deploy, so it reattaches rather than orphaning the VM. That's the same invariant the untouched terminal-session:v1 HMAC namespace protects — the two together mean no warm Sprite is stranded by this PR.

…id-deploy

Self-review caught a production regression I introduced by exceeding the epic's
stated scope. The epic scoped the DB migration to `terminal_sessions` ONLY; I
had also renamed two columns on LIVE tables:

  pages."terminalAccess"                    -> "machineAccess"
  global_assistant_config.terminal_access   -> machine_access

Deploys run migrations in a separate Fly machine BEFORE the new app image takes
traffic (.github/workflows/docker-images.yml — "Run migrations" at line 155,
"Deploy web" at line 326). `pages."terminalAccess"` is read AND written by
api/pages/[pageId]/agent-config, and global_assistant_config by
api/user/assistant-config — neither is behind CODE_EXECUTION_ENABLED, both serve
live traffic today. So the migration would drop the column out from under the
still-running old image and 500 every agent-config request until the new image
went live several steps later. A column rename has no safe deploy order; only an
expand/contract across two releases does.

Fix: keep the physical columns, keep the code coherent. Drizzle decouples the
field name from the column name, so the TS field stays `machineAccess`
everywhere while mapping to the legacy column:

  machineAccess: boolean('terminalAccess')   // core.ts
  machineAccess: boolean('terminal_access')  // integrations.ts

Regenerated 0200: it now contains ZERO live-table DDL — only the
`terminal_sessions` -> `machine_sessions` rename, which is safe because that
table is read exclusively behind CODE_EXECUTION_ENABLED (off). 0201 (pkey
rename) unchanged. `db:generate` reports no drift.

Verified against a real Postgres: reading `pages.machineAccess` returns the
legacy column's value, and writing `machineAccess: false` lands in
pages."terminalAccess" — the mapping round-trips both directions.

The physical column names are now the only residual `terminal_*` in the schema;
that's a deliberate, documented trade (zero-downtime beats cosmetic column
naming) and is a clean expand/contract follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2dVpQa811gvgVtZuY1866
@2witstudios

Copy link
Copy Markdown
Owner Author

⚠️ Self-review found a production regression in this PR — fixed in 47c0227

Worth calling out explicitly, because it changes what the migration does.

An earlier revision renamed two columns on live tables:

pages."terminalAccess"                  -> "machineAccess"
global_assistant_config.terminal_access -> machine_access

That would have broken production on deploy. Deploys run migrations in a separate Fly machine before the new app image takes traffic (docker-images.yml: "Run migrations" line 155, "Deploy web" line 326). Both columns are read and written by api/pages/[pageId]/agent-config and api/user/assistant-config — neither is behind CODE_EXECUTION_ENABLED; both serve live traffic today. The migration would have dropped the column out from under the still-running old image, 500ing every agent-config request until the new image landed several steps later.

There is no safe deploy order for a column rename — migrate-first breaks the old code, deploy-first breaks the new code. Only an expand/contract across two releases is safe.

It was also scope I added: the epic scoped the DB migration to terminal_sessions only.

Fix: keep the physical columns, keep the code coherent. Drizzle decouples field name from column name:

machineAccess: boolean('terminalAccess')   // core.ts
machineAccess: boolean('terminal_access')  // integrations.ts

The TS field is machineAccess everywhere — no code changed — while the columns stay put. Regenerated 0200 now contains zero live-table DDL: only the terminal_sessions → machine_sessions rename, which is safe precisely because that table is read exclusively behind CODE_EXECUTION_ENABLED (off), so nothing running touches it during the window. db:generate reports no drift.

Verified against a real Postgres that the mapping round-trips in both directions:

read  pages.machineAccess       = true      (from legacy "terminalAccess" column)
read  gac.machineAccess         = true      (from legacy terminal_access column)
write machineAccess = false  ->  pages."terminalAccess" = false

The two physical column names are now the only residual terminal_* in the schema. That's a deliberate trade — zero-downtime beats cosmetic column naming — and a clean expand/contract follow-up whenever someone wants it.

2witstudios and others added 2 commits July 11, 2026 16:43
#2008 landed on master and modified terminal-session-manager.ts — the exact file
this branch renames to machine-session-manager.ts — plus realtime/index.ts,
agent-terminal-access.ts and agent-terminals-runtime.ts. Git followed the rename,
leaving four content conflicts.

Resolved by taking MASTER'S content wholesale for every conflicted file and
re-applying this branch's renames on top. This branch's edits to those files were
rename-only, so nothing of ours is lost, and none of #2008's semantics can be
silently dropped: its deletion of `ensureSpriteAwake` and its rewritten
reconnect/wake comments are preserved verbatim (grep-verified: ensureSpriteAwake
appears nowhere in the merged tree).

#2008's three NEW test files came in clean (no conflict) but still referenced the
pre-rename symbols (acquireTerminalSandbox, deriveTerminalSessionKey,
TerminalSessionStore, ...terminal-session-manager). Applied the same rename map to
them, so they now exercise acquireMachineSession / deriveMachineSessionKey /
MachineSessionStore against machine-session-manager.

Verified: build + typecheck green monorepo-wide; realtime 647/647 pass INCLUDING
#2008's 3 new sprite test files (50 tests); packages/lib 7570/7570 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2dVpQa811gvgVtZuY1866
@2witstudios
2witstudios merged commit 67affbb into master Jul 11, 2026
8 checks passed
@2witstudios
2witstudios deleted the pu/machine-rename-part2 branch July 11, 2026 22:20
2witstudios added a commit that referenced this pull request Jul 11, 2026
Addresses the Codex P2 on #2014: `applyEgressLockdown` was also the only thing
that recreated SANDBOX_ROOT, so applying it on fresh create only left a warm
reconnect exposed if a sandbox command had deleted /workspace — the PTY passes
cwd to createSession, the server chdirs into it before spawning, and the session
open fails outright.

Fixed where the directory is actually consumed rather than by buying the mkdir
back on every hand-back (the exec this PR exists to remove): `spawnWithSelfHealingCwd`
(pure, in the driver) wraps command+args in an `sh` that recreates and enters the
cwd, then execs the real command — preserving the PTY, signals and exit code, with
cwd/command/args as positional data args so the no-injection invariant of the
arg-array form holds. This is the shape `runCommand` already used; it is now the
single definition, shared by the batch path, MachineHost.stream, and the realtime
PTY.

Also rebased onto master's Terminal→Machine rename (#2012): machine-session-manager,
MachineSessionStore/Record, surface 'machine', and the migration renumbered to
0202 on machine_sessions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PNvzrqmNYJbT3B6XKmr9dm
2witstudios added a commit that referenced this pull request Jul 11, 2026
Keeps the branch current with the Terminal→Machine naming sweep (#2012) and the
iOS/mobile fixes. No conflicts: the sweep did not touch sprites-shell.ts,
replay-dedupe.ts, or their tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CTUvWzi9AkWgDQcKA8DwRH
2witstudios added a commit that referenced this pull request Jul 11, 2026
* perf(sandbox): apply egress lockdown on fresh create only

The Sprite network policy lives at /.sprite/policy/network.json — a persistent
file that survives pause/hibernation (docs.sprites.dev/concepts/networking).
getOrCreate nonetheless re-pushed it, plus a SANDBOX_ROOT mkdir exec, on EVERY
hand-back: every terminal connect, every tab-back, and every 60s re-auth tick
paid a control-plane round-trip plus an exec for a policy that was already in
place.

The lockdown is now applied exactly when it is not already known-good: a fresh
create (a new Sprite starts on the platform's open outbound), a hash mismatch
(the desired policy changed), or an unknown recorded state (fail closed). The
decision is a pure function — shouldApplyPolicy({fresh, appliedPolicyHash,
desiredPolicyHash}) over a canonical hashPolicy — and the shell records the
confirmed hash on the terminal_sessions row.

The crash window the old unconditional re-apply defended (a crash between
createSprite and its lockdown leaving an open-egress Sprite reachable) is closed
by ORDERING instead: the session row is written only after getOrCreate resolves,
so an unlocked Sprite is never linked to a session. A lockdown failure on fresh
create still destroys the Sprite and rejects the hand-back.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PNvzrqmNYJbT3B6XKmr9dm

* fix(sandbox): self-healing cwd on PTY open; rebase onto Machine rename

Addresses the Codex P2 on #2014: `applyEgressLockdown` was also the only thing
that recreated SANDBOX_ROOT, so applying it on fresh create only left a warm
reconnect exposed if a sandbox command had deleted /workspace — the PTY passes
cwd to createSession, the server chdirs into it before spawning, and the session
open fails outright.

Fixed where the directory is actually consumed rather than by buying the mkdir
back on every hand-back (the exec this PR exists to remove): `spawnWithSelfHealingCwd`
(pure, in the driver) wraps command+args in an `sh` that recreates and enters the
cwd, then execs the real command — preserving the PTY, signals and exit code, with
cwd/command/args as positional data args so the no-injection invariant of the
arg-array form holds. This is the shape `runCommand` already used; it is now the
single definition, shared by the batch path, MachineHost.stream, and the realtime
PTY.

Also rebased onto master's Terminal→Machine rename (#2012): machine-session-manager,
MachineSessionStore/Record, surface 'machine', and the migration renumbered to
0202 on machine_sessions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PNvzrqmNYJbT3B6XKmr9dm

* fix(sandbox): self-heal a deleted workspace on the file-write path too

Second half of the same gap the PTY fix closed. The Sprite fs API does not create
parent directories, so `writeFiles` also depended on the egress lockdown's
per-hand-back `mkdir` to guarantee SANDBOX_ROOT existed. With the lockdown now
fresh-create-only, an agent that had `rm -rf`'d /workspace would fail every
subsequent file write.

Folded into the exec the fs path was already paying for: `fsWithWakeRetry`'s
recovery exec now takes the directories the op needs, so the single exec between
a failed op and its retry BOTH wakes the VM (the reason it exists — the fs API is
a bare fetch that cannot wake a hibernated Sprite) and `mkdir -p`s the parents.
Zero extra round-trips on the happy path; the read path is unchanged (`sh -c :`).

New pure functions + tests: `parentDir` (POSIX by construction — these are paths
inside the Linux VM, so node:path would apply Win32 semantics on a Windows host)
and `fsRecoveryExec`. Directories are positional data args, never interpolated
into the script.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PNvzrqmNYJbT3B6XKmr9dm

* fix(sandbox): key the egress record on the Sprite instance, not just the policy

Addresses the Codex P1 on #2014: a policy-hash-only record could not tell one VM
from its replacement, which opened a containment hole on the vanish-and-recreate
path. Given a session row whose hash matches the desired policy but whose Sprite
has vanished, two concurrent getOrCreate calls race: A creates the replacement
(which starts on the platform's default OPEN egress) and has not yet reached its
lockdown; B then finds that new Sprite by name, sees fresh === false, and — since
the recorded hash still describes the DESTROYED Sprite's policy — skipped the
push and handed back an unlocked VM.

The record is now a LOCKDOWN TOKEN over (Sprite instance id, policy hash): proof
that a specific policy was applied to a specific VM. The SDK hydrates `id` from
the API response on both getSprite and createSprite, so reading it costs nothing.
A replacement VM has a different id, so B's token does not match and B locks it
down itself (both callers pushing the same policy is harmless; one skipping it is
not). An SDK that reports no id yields NO token — unprovable, so never recorded
as proof, and the next hand-back re-applies. Fail closed throughout.

The driver now returns the token it confirmed and the shell persists that, rather
than the shell computing what it hoped for: machine_sessions.egressPolicyToken
(migration 0202), threaded through SandboxHandle/MachineHandle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PNvzrqmNYJbT3B6XKmr9dm

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request Jul 13, 2026
* fix(cron): schedule machine-storage billing reconcile hourly

The /api/cron/reconcile-machine-storage route (PR #2012) existed with GET
and POST handlers but had no crontab entry, so machine persistent-storage
billing never reconciled. Add an hourly GET entry alongside the other
reconcile/sweep jobs; the route is idempotent via per-machine last-billed
watermarks, so hourly cadence is safe and missed runs self-correct.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWBkQUgKqCBphueo738YPo

* fix(cron): serialize machine-storage reconciles with flock

Review flagged that charge + watermark-advance in reconcileMachineStorage
are two un-transactioned writes, so two CONCURRENT invocations that both
snapshot the same storageLastBilledAt could bill the same elapsed window
twice. Wrap the crontab entry in flock -n: an overlapping tick is skipped
rather than queued, and the skipped window is caught up by the watermark
on the next hour. Install util-linux flock explicitly in the cron image
rather than relying on a busybox applet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWBkQUgKqCBphueo738YPo

* fix(cron): bound cron requests, make skips visible, honest overlap docs

Review-pass hardening of the machine-storage reconcile schedule:

- cron-curl now runs curl with -sS --connect-timeout 10 --max-time 3300:
  a wedged request could previously hold the reconcile flock forever,
  silently starving every future tick (and any entry could hang/pile up);
  now every request is bounded below the hourly tick and transport errors
  print into the entry's log instead of vanishing with -s.
- The reconcile entry logs skipped/failed ticks via || echo, so a held
  lock is distinguishable from a healthy quiet run.
- Lock moved /tmp -> /run (never bulk-cleaned; unlinking a held lock file
  would silently break mutual exclusion).
- Crontab comment no longer over-promises: flock is a best-effort guard
  for this container's own ticks; in-service serialization for ALL
  callers (manual POST, out-of-band second machine, client-disconnect
  with server-side run continuing) is tracked follow-up work.
- route.ts docblock corrected (comment-only): it claimed "overlapping or
  repeated runs never double-bill", but the charge and watermark advance
  are two un-transactioned writes — concurrent runs CAN double-bill.
  Sequential-rerun idempotency claim kept, concurrency caveat added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HWBkQUgKqCBphueo738YPo

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant