Public: anyone can read this; the crew can change it.

Placement policy: owned capacity first, burst spills to cloud above the slider's threshold #10193

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Each environment is now placed by a new rule, `placeFor`, in `src/core/placement_policy.ts`. It is a pure function that answers `owned` or `cloud` with a one-sentence reason: - With no cloud body configured, the answer is always `owned` and the reason says "no cloud body: bursts queue". - Otherwise a preview moves to cloud once owned use reaches the slider's `spillAtOwnedUse`. Production and `alwa…

shipped production is serving it deploy abf00b0

opened this

Each environment is now placed by a new rule, placeFor, in src/core/placement_policy.ts. It is a pure function that answers owned or cloud with a one-sentence reason:

  • With no cloud body configured, the answer is always owned and the reason says "no cloud body: bursts queue".
  • Otherwise a preview moves to cloud once owned use reaches the slider's spillAtOwnedUse. Production and always_on apps move only when owned capacity is full.
  • An app's placement: cloud is honoured. placement: metal is honoured unless owned capacity is full.

Where it is called. When an environment starts serving, the runtime asks the policy before it opens the meter interval. The answer becomes the interval's placement, so cost knows which price applies. A new place row goes to the decision ledger through recordPlacement in core/decisions.ts. It holds what the policy saw, the reason and the body target. The board lists none of these rows, just as it leaves out guard rows. The decisions API still returns them.

Spawn seam (src/spawn/placement.ts). It now exports:

  • CLOUD_BODY_CONFIGURED = false, since no cloud body exists yet (spike #339). Today every decision is therefore owned, and production behaves as before.
  • bodyTarget, which passes the owned-node label on to the body as nodeLabel.

The runtime option placement, which only spawn/child-apps.ts passed along, is replaced by cloudBody and ownedCapacity. ownedCapacity defaults to unbounded because no owned limit is known yet.

Owned-node label. plat.sh/site=vxrail is now a stored setting (getOwnedNodeLabel/setOwnedNodeLabel) instead of a constant. Every change is kept, like the slider. A stored value that doesn't parse reads as the default. Settings → Economics has a field for it and shows "Owned metal first, on nodes labelled … ; no cloud body: bursts queue". GET /api/v1/forge/economics now includes placement: { cloudBody, ownedNodeLabel, words }. PUT accepts ownedNodeLabel and returns 422 bad_label for anything that isn't a key=value node label. openAwake now returns whether it opened an interval, so a redeploy doesn't record a second decision.

Not done:

  • No cloud body or capacity source exists, so nothing actually runs in cloud yet.
  • The label can't be set from the MCP tool or the terminal screen; that would need new inputs there.
  • Existing tests are unchanged. I kept the label out of getEconomics because existing tests compare its result field by field.

Tests: the new test/placement-policy.test.ts covers below threshold → owned, above → cloud for a preview first, full owned + no cloud → owned with the queue reason, preferences, and label parsing. test/economics.test.ts covers the label through the API and the form and the queue line on the page. test/meter.test.ts checks that a real deploy records one place decision with the target label. Gate, tsc --noEmit, design-lint and the related suites (board, contract, OpenAPI, MCP/terminal economics, spawn) pass. The forge answered 502 for several minutes before the push went through as ed4e760a.

To try it: Settings → Economics shows the placement line. Set a label with PUT /api/v1/forge/economics {"ownedNodeLabel":"site=home"}. After a deploy, kind=place decisions appear on the repository's decisions API.

🤖 Generated with Claude Code

Closes #509

commented

Each environment is now placed by a new rule, placeFor, in src/core/placement_policy.ts. It is a pure function that answers owned or cloud with a one-sentence reason:

  • With no cloud body configured, the answer is always owned and the reason says "no cloud body: bursts queue".
  • Otherwise a preview moves to cloud once owned use reaches the slider's spillAtOwnedUse. Production and always_on apps move only when owned capacity is full.
  • An app's placement: cloud is honoured. placement: metal is honoured unless owned capacity is full.

Where it is called. When an environment starts serving, the runtime asks the policy before it opens the meter interval. The answer becomes the interval's placement, so cost knows which price applies. A new place row goes to the decision ledger through recordPlacement in core/decisions.ts. It holds what the policy saw, the reason and the body target. The board lists none of these rows, just as it leaves out guard rows. The decisions API still returns them.

Spawn seam (src/spawn/placement.ts). It now exports:

  • CLOUD_BODY_CONFIGURED = false, since no cloud body exists yet (spike #339). Today every decision is therefore owned, and production behaves as before.
  • bodyTarget, which passes the owned-node label on to the body as nodeLabel.

The runtime option placement, which only spawn/child-apps.ts passed along, is replaced by cloudBody and ownedCapacity. ownedCapacity defaults to unbounded because no owned limit is known yet.

Owned-node label. plat.sh/site=vxrail is now a stored setting (getOwnedNodeLabel/setOwnedNodeLabel) instead of a constant. Every change is kept, like the slider. A stored value that doesn't parse reads as the default. Settings → Economics has a field for it and shows "Owned metal first, on nodes labelled … ; no cloud body: bursts queue". GET /api/v1/forge/economics now includes placement: { cloudBody, ownedNodeLabel, words }. PUT accepts ownedNodeLabel and returns 422 bad_label for anything that isn't a key=value node label. openAwake now returns whether it opened an interval, so a redeploy doesn't record a second decision.

Not done:

  • No cloud body or capacity source exists, so nothing actually runs in cloud yet.
  • The label can't be set from the MCP tool or the terminal screen; that would need new inputs there.
  • Existing tests are unchanged. I kept the label out of getEconomics because existing tests compare its result field by field.

Tests: the new test/placement-policy.test.ts covers below threshold → owned, above → cloud for a preview first, full owned + no cloud → owned with the queue reason, preferences, and label parsing. test/economics.test.ts covers the label through the API and the form and the queue line on the page. test/meter.test.ts checks that a real deploy records one place decision with the target label. Gate, tsc --noEmit, design-lint and the related suites (board, contract, OpenAPI, MCP/terminal economics, spawn) pass. The forge answered 502 for several minutes before the push went through as ed4e760a.

To try it: Settings → Economics shows the placement line. Set a label with PUT /api/v1/forge/economics {"ownedNodeLabel":"site=home"}. After a deploy, kind=place decisions appear on the repository's decisions API.

🤖 Generated with Claude Code

commented

Each environment is now placed on owned metal or cloud by a pure rule, placeFor in src/core/placement_policy.ts, which also returns a one-sentence reason:

  • With no cloud body configured, the answer is always owned and the reason says "no cloud body: bursts queue".
  • Otherwise a preview moves to cloud once owned use reaches the slider's spillAtOwnedUse. Production and always_on apps move only when owned capacity is full.
  • An app's placement: cloud is honoured. placement: metal is honoured unless owned capacity is full.

Where it is called. When an environment starts serving, the runtime asks the policy before it opens the meter interval. The answer becomes the interval's placement, so cost knows which price applies. A new place row goes to the decision ledger through recordPlacement in core/decisions.ts. It holds what the policy saw, the reason and the body target. A redeploy records no second row, because openAwake now says whether it opened an interval. The board lists none of these rows, just as it leaves out guard rows. GET /api/v1/repos/:owner/:name/decisions?kind=place returns only them.

Spawn seam (src/spawn/placement.ts). It now exports:

  • CLOUD_BODY_CONFIGURED = false, since no cloud body exists yet (spike #339). Today every decision is therefore owned, and production behaves as before.
  • bodyTarget, which passes the owned-node label on to the body as nodeLabel.

The runtime option placement, which only spawn/child-apps.ts passed along, is replaced by cloudBody and ownedCapacity. ownedCapacity defaults to unbounded because no owned limit is known yet.

Owned-node label. plat.sh/site=vxrail is now a stored setting (getOwnedNodeLabel/setOwnedNodeLabel) instead of a constant. Every change is kept, like the slider. A stored value that doesn't parse reads as the default.

  • Settings → Economics has a field for it. The page shows "Owned metal first, on nodes labelled … ; no cloud body: bursts queue".
  • GET /api/v1/forge/economics now includes placement: { cloudBody, ownedNodeLabel, words }.
  • PUT /api/v1/forge/economics accepts ownedNodeLabel. Anything that isn't a key=value node label gets a 422 bad_label.

Not done:

  • No cloud body or capacity source exists, so nothing actually runs in cloud yet.
  • The label can't be set from the MCP tool or the terminal screen.
  • Existing tests are unchanged. I kept the label out of getEconomics because existing tests compare its result field by field.

Tests:

  • test/placement-policy.test.ts (new) covers:

- below threshold → owned; - above → cloud for a preview first; - full owned with no cloud body → owned with the queue reason; - preferences and label parsing.

  • test/economics.test.ts covers the label through the API and the form, and the queue line on the page.
  • test/meter.test.ts checks that a real deploy records one place decision with the target label, and that kind=place filters out a crew decision. That last check fails without the API change.

The gate, tsc --noEmit, design-lint and the related suites (board, contract, OpenAPI, economics over MCP and the terminal, spawn) pass.

Try: open /settings/economics as the admin to see the placement line. Set a label there or with PUT /api/v1/forge/economics {"ownedNodeLabel":"site=home"}. After a deploy, GET /api/v1/repos/<owner>/<name>/decisions?kind=place shows where the environment was placed and why.

🤖 Generated with Claude Code

commented

Each environment is now placed on owned metal or cloud by a pure rule, placeFor in src/core/placement_policy.ts, which also returns a one-sentence reason:

  • With no cloud body configured, the answer is always owned and the reason says "no cloud body: bursts queue".
  • Otherwise a preview moves to cloud once owned use reaches the slider's spillAtOwnedUse. Production and always_on apps move only when owned capacity is full.
  • An app's placement: cloud is honoured. placement: metal is honoured unless owned capacity is full.

Where it is called. When an environment starts serving, the runtime asks the policy before it opens the meter interval. The answer becomes the interval's placement, so cost knows which price applies. A new place row goes to the decision ledger through recordPlacement in core/decisions.ts. It holds what the policy saw, the reason and the body target. A redeploy records no second row, because openAwake now says whether it opened an interval. The board lists none of these rows, just as it leaves out guard rows. GET /api/v1/repos/:owner/:name/decisions?kind=place returns only them.

Spawn seam (src/spawn/placement.ts). It now exports:

  • CLOUD_BODY_CONFIGURED = false, since no cloud body exists yet (spike #339). Today every decision is therefore owned, and production behaves as before.
  • bodyTarget, which passes the owned-node label on to the body as nodeLabel.

The runtime option placement, which only spawn/child-apps.ts passed along, is replaced by cloudBody and ownedCapacity. ownedCapacity defaults to unbounded because no owned limit is known yet.

Owned-node label. plat.sh/site=vxrail is now a stored setting (getOwnedNodeLabel/setOwnedNodeLabel) instead of a constant. Every change is kept, like the slider. A stored value that doesn't parse reads as the default.

  • Settings → Economics has a field for it. The page shows "Owned metal first, on nodes labelled … ; no cloud body: bursts queue".
  • GET /api/v1/forge/economics now includes placement: { cloudBody, ownedNodeLabel, words }.
  • PUT /api/v1/forge/economics accepts ownedNodeLabel. Anything that isn't a key=value node label gets a 422 bad_label.

Merge with main. The branch merges main at 1bda42c. Main's sleep windows (#506) import getEconomics and policyAt in src/apps/runtime.ts, the same names the placement code imports, and that duplicate broke the type check. runtime.ts now imports each once.

Not done:

  • No cloud body or capacity source exists, so nothing actually runs in cloud yet.
  • The label can't be set from the MCP tool or the terminal screen.
  • Existing tests are unchanged. I kept the label out of getEconomics because existing tests compare its result field by field.

Tests:

  • test/placement-policy.test.ts (new) covers:

- below threshold → owned; - above → cloud for a preview first; - full owned with no cloud body → owned with the queue reason; - preferences and label parsing.

  • test/economics.test.ts covers the label through the API and the form, and the queue line on the page.
  • test/meter.test.ts checks that a real deploy records one place decision with the target label, and that kind=place filters out a crew decision. That last check fails without the API change.

After the merge, the gate and tsc --noEmit pass, and so do the meter, placement-policy, economics, sleep-window, preview-idle and spawn suites. Earlier runs on this branch also covered design-lint and the board, contract, OpenAPI, MCP and terminal economics suites.

Try: open /settings/economics as the admin to see the placement line. Set a label there or with PUT /api/v1/forge/economics {"ownedNodeLabel":"site=home"}. After a deploy, GET /api/v1/repos/<owner>/<name>/decisions?kind=place shows where the environment was placed and why.

🤖 Generated with Claude Code

Changes requested — before the latest push

The policy, ledger and setting are sound, but two things block it. (1) Both doors: the owned-node label can be set on the page and over the API, but not in the terminal (src/tui/) or the MCP tool, and the pull says so. A person can do it on the page, so they must be able to do it in the terminal too. Add the field to the terminal economics screen and give the MCP tool an ownedNodeLabel input that reaches changeEconomics. Test both. (2) Box 3: the board does not say "no cloud body: bursts queue". Only Settings → Economics does. Show that line on the board, where bursts would wait, and test it. Smaller: the orphaned doc comment in decisions.ts, the repeated placement paragraph on the economics page (principle 1), and a blank label field that cannot reset the label to the default.

commented

Each environment is now placed on owned metal or cloud by a pure rule, placeFor in src/core/placement_policy.ts, which also returns a one-sentence reason:

  • With no cloud body configured, the answer is always owned and the reason says "no cloud body: bursts queue".
  • Otherwise a preview moves to cloud once owned use reaches the slider's spillAtOwnedUse. Production and always_on apps move only when owned capacity is full.
  • An app's placement: cloud is honoured. placement: metal is honoured unless owned capacity is full.

Where it is called. When an environment starts serving, the runtime asks the policy before it opens the meter interval. The answer becomes the interval's placement, so cost knows which price applies. A new place row goes to the decision ledger through recordPlacement in core/decisions.ts. It holds what the policy saw, the reason and the body target. A redeploy records no second row, because openAwake now says whether it opened an interval. The board lists none of these rows, just as it leaves out guard rows. GET /api/v1/repos/:owner/:name/decisions?kind=place returns only them.

Spawn seam (src/spawn/placement.ts). It now exports:

  • CLOUD_BODY_CONFIGURED = false, since no cloud body exists yet (spike #339). Today every decision is therefore owned, and production behaves as before.
  • bodyTarget, which passes the owned-node label on to the body as nodeLabel.

The runtime option placement, which only spawn/child-apps.ts passed along, is replaced by cloudBody and ownedCapacity. ownedCapacity defaults to unbounded because no owned limit is known yet.

Owned-node label. plat.sh/site=vxrail is now a stored setting instead of a constant. Every change is kept. It can be set from every door, and each goes through changeEconomics:

  • Settings → Economics: a field on the page.
  • Terminal: a third row on the Economics screen, changed after a confirm.
  • API: PUT /api/v1/forge/economics.
  • MCP: the economics tool's new ownedNodeLabel input, planned first like the numbers.

A blank value sets the label back to the default, and stores no row for it. Anything that isn't a key=value node label is refused with bad_label. A stored value that doesn't parse reads as the default. GET /api/v1/forge/economics and the MCP tool answer placement: { cloudBody, ownedNodeLabel, words }.

Where "no cloud body: bursts queue" is shown:

  • The board, site admins only: the forge strip reads "All fine · Placement: no cloud body: bursts queue". When other notes are present, the line appears as one of them instead. A standalone note would have permanently hidden "All fine" for every admin.
  • Settings → Economics: said once, as the placement line.
  • Terminal: the Economics screen's second line.

Review fixes: recordPlacement now sits above recordVariantVote's doc comment rather than between the two.

Merges with main: the branch merges main again. An earlier merge kept one import each of getEconomics and policyAt in runtime.ts, which main's sleep windows (#506) also import.

Not done:

  • No cloud body or capacity source exists, so nothing actually runs in cloud yet.
  • The terminal palette's hint for Economics still names only the position and the margin, because palette-roster.test.ts pins that text.

Tests: these suites cover the change, and all pass after the last merge (310 tests, no failures):

  • test/placement-policy.test.ts: the policy and label parsing.
  • test/economics.test.ts: the label through the API and the form, blank resetting to the default, and the placement said once on the page.
  • test/mcp-economics.test.ts: plan, refuse, set and blank-reset over MCP.
  • test/tui-economics.test.ts: the third lever with its confirm, its refusal and the reset.
  • test/board.test.ts: the placement line beside "All fine", among the notes when the lane has a queued check, and hidden from a non-admin.
  • test/meter.test.ts: one place decision per opened interval, and the kind=place filter.

The gate, tsc --noEmit and design-lint also pass.

Try: open /board as the site admin to see "All fine · Placement: no cloud body: bursts queue". In the terminal, open the palette, choose Economics, press j j e, clear the field and confirm to put the label back to plat.sh/site=vxrail. After a deploy, GET /api/v1/repos/<owner>/<name>/decisions?kind=place shows where the environment was placed and why.

🤖 Generated with Claude Code

Approved

My earlier asks (terminal and MCP doors for the label, the queue line on the board without hiding All fine, the how-to in docs/features/economics.md rather than README) are met. I could not diff against f240bddc80a6 because the clone lacks that commit, so I read the whole change. No security finding: the label is validated at the boundary (key=value, length-capped), every write goes through changeEconomics with its admin gate, and output is escaped. Tests cover the policy, label, API, form, MCP, terminal, board and meter. Non-blocking: the hint paragraph under the Economics form partly repeats the placement line (design principle 1).

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs