diff --git a/pm-stack/aggregator/README.md b/pm-stack/aggregator/README.md index 20eb84c37..ca68a886a 100644 --- a/pm-stack/aggregator/README.md +++ b/pm-stack/aggregator/README.md @@ -580,14 +580,17 @@ tick visible on /quota. `scripts/record-claude-launch.ts`). The aggregator merges them with live laptop records into its durable registry. -`scripts/push-fleet-feed.sh` pushes the feed to the laptop over the -tailnet proxy route; a systemd timer on the sandbox runs it every -~5 minutes (`systemd/fleet-feed-collector.{service,timer}`). The -aggregator states the feed's age in the page provenance; a stale -feed keeps rendering. `/etc/systemd/system` does not survive -sandbox recycles, so `~/tools/init.sh` restores the units on boot -via `scripts/install-collector-units.sh` whenever the timer is not -active (#903). +The collector runs on the laptop: the `fleet-feed-collector` user +timer (declared in the pm-stack NixOS module, #3363/#3368) runs +`scripts/collect-fleet-feed.ts` every ~5 minutes and writes the feed +straight to the aggregator's state dir. The aggregator states the +feed's age in the page provenance; a stale feed keeps rendering. +The collector originally ran on the sandbox and pushed the feed +over the tailnet proxy route (`scripts/push-fleet-feed.sh`); that +sandbox timer is retired (#3362) — a stale sandbox copy kept +overwriting the laptop feed (its AA section 403'd against the +retired muse-dev project). The push script remains for manual use +only. **Deployment (laptop):** `install.sh` copies the package to `~/pm-stack/aggregator`, renders `.env` from @@ -706,12 +709,11 @@ src/ opencode-tools/ session-identity.ts OpenCode tool: a session reports its own id (#906) scripts/ - collect-fleet-feed.ts sandbox collector: quota + sandbox attention - + dns + registry records -> feed - push-fleet-feed.sh collect + push the feed to the laptop (~5 min timer) + collect-fleet-feed.ts collector: quota + attention + dns + registry + records -> feed (runs on the laptop, ~5 min timer) + push-fleet-feed.sh RETIRED sandbox producer: collect + push the feed + to the laptop (manual use only, #3362) record-claude-launch.ts append a Muse launch record to the JSONL (#906) -systemd/ - fleet-feed-collector.{service,timer} sandbox-side collector units test/ render.test.ts unit derivations + render smoke boot.test.ts boots the server, serves all views from fixtures diff --git a/pm-stack/aggregator/scripts/install-collector-units.sh b/pm-stack/aggregator/scripts/install-collector-units.sh deleted file mode 100755 index cf586027b..000000000 --- a/pm-stack/aggregator/scripts/install-collector-units.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -# install-collector-units.sh — idempotent installer for the sandbox-side -# fleet-feed collector systemd units (fleet-feed-collector.{service,timer}). -# -# /etc/systemd/system does not survive sandbox recycles, so ~/tools/init.sh -# calls this on every boot where the collector timer is not active (the -# same restore pattern as the fleet-units and airthings installers). -# Source of truth for the units is this package's systemd/ directory, as -# checked out in the feed clone (~/git/pm-feed). Safe to re-run any time. -set -eu -DIR="$(cd "$(dirname "$0")/.." && pwd)" # the aggregator package dir - -cp "$DIR/systemd/fleet-feed-collector.service" /etc/systemd/system/fleet-feed-collector.service -cp "$DIR/systemd/fleet-feed-collector.timer" /etc/systemd/system/fleet-feed-collector.timer -systemctl daemon-reload -systemctl enable --now fleet-feed-collector.timer -systemctl is-active --quiet fleet-feed-collector.timer && - echo "fleet-feed-collector.timer active" || - { - echo "FAILED to activate fleet-feed-collector.timer" - exit 1 - } diff --git a/pm-stack/aggregator/scripts/push-fleet-feed.sh b/pm-stack/aggregator/scripts/push-fleet-feed.sh index b7fc47d4c..94af41ebf 100755 --- a/pm-stack/aggregator/scripts/push-fleet-feed.sh +++ b/pm-stack/aggregator/scripts/push-fleet-feed.sh @@ -18,7 +18,11 @@ # Guard: scripts/push-fleet-feed.guard.sh fails if either pattern # regresses. # -# Cadence: ~5 min via fleet-feed-collector.timer (systemd, sandbox). +# RETIRED as a scheduled producer (code#3362/#3363): the collector moved +# to the laptop on 2026-10-31 and runs there as a user timer declared in +# the pm-stack NixOS module; the sandbox fleet-feed-collector timer that +# ran this script every ~5 min is deleted. Kept for manual/emergency use +# only -- running it overwrites the laptop feed with this host's collection. set -euo pipefail # Systemd system services get a minimal PATH (and no HOME); bun lives diff --git a/pm-stack/aggregator/src/feed.ts b/pm-stack/aggregator/src/feed.ts index 3d0fc112f..38630566b 100644 --- a/pm-stack/aggregator/src/feed.ts +++ b/pm-stack/aggregator/src/feed.ts @@ -1,8 +1,11 @@ -// The fleet feed: a JSON file produced on the sandbox host by -// scripts/collect-fleet-feed.ts and pushed to the laptop (the sandbox -// OpenCode server binds its own loopback, and the quota sources of -// record live on the sandbox). The aggregator renders the feed as-is; -// a stale feed keeps rendering with its age stated in provenance. +// The fleet feed: a JSON file produced on the laptop by +// scripts/collect-fleet-feed.ts, run by the laptop's fleet-feed-collector +// user timer (declared in the pm-stack NixOS module, #3363/#3368) and +// written straight to the aggregator's state dir. (The collector +// originally ran on the sandbox and pushed the feed here; that sandbox +// timer is retired — a stale sandbox copy overwriting this file was +// code#3362.) The aggregator renders the feed as-is; a stale feed keeps +// rendering with its age stated in provenance. import { readFile } from "node:fs/promises"; import type { UnmatchedCharge } from "./charges-match.ts"; import type { CheckoutDrift } from "./checkout-drift.ts"; diff --git a/pm-stack/aggregator/src/topology.ts b/pm-stack/aggregator/src/topology.ts index 1fe199dff..f8fc074b8 100644 --- a/pm-stack/aggregator/src/topology.ts +++ b/pm-stack/aggregator/src/topology.ts @@ -175,9 +175,9 @@ export function buildServices(cfg: Config): ServiceDef[] { name: "Fleet-feed collector", port: null, bind: "— (no listener)", - runsAs: "sandbox systemd timer (fleet-feed-collector, ~5 min cadence)", + runsAs: "laptop systemd user timer (fleet-feed-collector, ~5 min cadence; pm-stack NixOS module)", probe: "feed", - consumers: "the aggregator — most live facets read the feed it pushes", + consumers: "the aggregator — most live facets read the feed it writes", breaksIfDown: "feed-backed facets go stale, then fall back to fixtures", }, { diff --git a/pm-stack/aggregator/src/watchdog.ts b/pm-stack/aggregator/src/watchdog.ts index cefeda00c..7e87bef5e 100644 --- a/pm-stack/aggregator/src/watchdog.ts +++ b/pm-stack/aggregator/src/watchdog.ts @@ -126,7 +126,7 @@ export function watchdogTierItems(inputs: WatchdogInputs): TierItem[] { "buildHealth diagnoses a feed generatedAt more than 60s in the future as a failed, never-fresh feed (#1148 watchdog); the clamped age of 0 must not silence the watchdog for the whole skew.", now, now, - "Check the fleet-feed collector's clock and the feed file it pushes; the card clears itself when a feed with a sane generatedAt lands.", + "Check the fleet-feed collector's clock and the feed file it writes; the card clears itself when a feed with a sane generatedAt lands.", ), ); } else if (inputs.feedAgeMs === null) { @@ -138,7 +138,7 @@ export function watchdogTierItems(inputs: WatchdogInputs): TierItem[] { "The fleet-feed collector's output cannot be read, so every feed-backed facet is running on its last good data or fixtures. Diagnosed by the aggregator's health assembly (#1148 watchdog).", now, now, - "Check the fleet-feed collector (sandbox timer fleet-feed-collector) and the feed file it pushes; the card clears itself when a readable feed lands.", + "Check the fleet-feed collector (laptop user timer fleet-feed-collector) and the feed file it writes; the card clears itself when a readable feed lands.", ), ); } else if (inputs.feedAgeMs > WATCHDOG_FEED_STALE_MS) { @@ -151,7 +151,7 @@ export function watchdogTierItems(inputs: WatchdogInputs): TierItem[] { `Feed age ${ageMsText(inputs.feedAgeMs)} is past the 15m freshness threshold (#1148 watchdog); feed-backed facets are flipping stale over the dead feed.`, (Number.isNaN(generatedMs) ? now - inputs.feedAgeMs : generatedMs) + WATCHDOG_FEED_STALE_MS, now, - "Check the fleet-feed collector (sandbox timer fleet-feed-collector, ~5 min cadence); the card clears itself when the feed is fresh again.", + "Check the fleet-feed collector (laptop user timer fleet-feed-collector, ~5 min cadence); the card clears itself when the feed is fresh again.", ), ); } diff --git a/pm-stack/aggregator/systemd/fleet-feed-collector.service b/pm-stack/aggregator/systemd/fleet-feed-collector.service deleted file mode 100644 index ad814e9a3..000000000 --- a/pm-stack/aggregator/systemd/fleet-feed-collector.service +++ /dev/null @@ -1,18 +0,0 @@ -[Unit] -Description=Fleet feed collector for the PM aggregator (sandbox host) -After=network.target - -[Service] -Type=oneshot -# The sandbox's agent processes run as root with HOME=/home/hatch; -# the quota sources, the OpenCode server password file, and the -# laptop SSH key all live under that home, so HOME is pinned here. -Environment=HOME=/home/hatch -# AA section source: muse-dev2 (the original muse-dev project's -# Postgres has been down since 2026-10-30; the collector's bws -# management token is muse-dev2-scoped, so the ref must match it -# or the AA query 403s and the explorer's AA section goes absent. -Environment=SUPABASE_PROJECT_REF=keihfchslgjetkmnmzra -# Runs from the feed clone (~hatch/git/pm-feed): collector + push over -# the tailnet proxy route. Proxy credential from mcpx-daemon.env. -ExecStart=/home/hatch/git/pm-feed/pm-stack/aggregator/scripts/push-fleet-feed.sh diff --git a/pm-stack/aggregator/systemd/fleet-feed-collector.timer b/pm-stack/aggregator/systemd/fleet-feed-collector.timer deleted file mode 100644 index beded27a0..000000000 --- a/pm-stack/aggregator/systemd/fleet-feed-collector.timer +++ /dev/null @@ -1,10 +0,0 @@ -[Unit] -Description=Push the fleet feed to the laptop aggregator every ~5 minutes - -[Timer] -OnBootSec=2min -OnUnitActiveSec=5min -Unit=fleet-feed-collector.service - -[Install] -WantedBy=timers.target