Skip to content

SpawnClient: observe-only monitor loop on every non-mock provider (#126) - #132

Merged
scttfrdmn merged 3 commits into
mainfrom
fix/126-viewer-mode
Nov 11, 2026
Merged

scttfrdmn merged 3 commits into
mainfrom
fix/126-viewer-mode

Conversation

@scttfrdmn

@scttfrdmn scttfrdmn commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #126

Problem

SpawnClient.tick() ran spored's lifecycle engine against real EC2 with isIdle: true hardcoded, and EC2Provider sets lastActivityMs to the launch time. So any open portal tab called StopInstances on every spawn-managed instance older than its spawn:idle-timeout, however busy it was. A Go spawn launch without --ttl defaults to a 1h idle timeout, so a CLI job was stopped one hour after launch. The same loop also terminated on TTL and cost limit, duplicating spored and the reaper.

Change

New ClientOptions.lifecycle: "observe" | "simulate":

  • "observe" is the default for every provider except a MockProvider. That includes EC2 and substrate, because non-billable is not the same as simulated. The loop refreshes, emits TTL and cost warnings once each, and advances the sweeps, queues and job arrays this client started. It never takes a lifecycle action. It evaluates idle as not idle, because the browser can't see activity on the box.
  • "simulate" keeps the engine for the mock demo, where the sim clock is the activity signal. Passing it with a non-mock provider throws.
  • client.lifecycleMode reports which mode applies. setProvider re-derives it, so a mock client switched to EC2 becomes a viewer.

There is deliberately no mode that enforces against EC2.

Tests (src/core/client.viewer.test.ts)

The client is built exactly as the portal will build it: new SpawnClient({ provider: new EC2Provider({...}) }), with no lifecycle option and no endpoint, so isReal is true. It runs against a fake EC2 (EC2Client.prototype.send stubbed, no network). The fleet has every rule already due: a CLI job past its idle timeout (the audit's case), a past-TTL instance, an over-budget instance, and an on-complete hibernate-on-idle instance.

  • 40 startMonitor ticks under fake timers send at least 40 DescribeInstances and zero other commands, with no action events.
  • The same holds via step(), over a substrate endpoint, and after setProvider(mock → EC2).
  • The TTL warning is still emitted once.
  • The mock still simulates by default, and lifecycle: "observe" makes it a viewer.

I checked the guard: when the observe branch is forced off, 5 of these tests fail.

Breaking for existing consumers?

Behaviour change, noted in the CHANGELOG. A caller that relied on the loop enforcing TTL, cost or idle on EC2 no longer gets that from spawn-ts. spored and the reaper enforce them, as they always did. There is no API removal.

Not in this PR

Also in this PR

  • src/aws/ec2.integration.test.ts (live-smoke, against substrate): this test asserted that the monitor terminated an expired-TTL instance over the wire. It now asserts the opposite, observe-only, and cleans up after itself.

Merging alongside #133 / #134 / #135

There are no code conflicts. I trial-merged this PR into the tip of the #135 stack, and all 888 tests pass on the combined code. The only conflict is CHANGELOG.md, because every PR replaces Nothing yet. under Unreleased. Resolve it by keeping both entries.

The monitor loop ran the lifecycle engine against EC2 with isIdle: true
hardcoded and the launch time as last activity, so an open portal tab
stopped every spawn-managed instance older than its spawn:idle-timeout,
busy or not, and terminated on TTL/cost behind spored and the reaper.

New ClientOptions.lifecycle: "observe" (refresh, warnings, fan-out
progress; never a lifecycle action) is the default for every provider
but a MockProvider; "simulate" keeps the engine for the mock demo and
throws on anything else. setProvider re-derives the mode.

Fixes #126
…126)

The live-smoke substrate test asserted the monitor terminated an
expired-TTL instance over the wire, which #126 removes by design. It now
asserts the opposite and cleans up.

Refs #126
Keeps the barrel's client export block untouched, so this PR and the

Refs #126
@scttfrdmn
scttfrdmn merged commit 0a23624 into main Nov 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant