instar 1.3.495 → 1.3.496

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -217,6 +217,20 @@
217
217
  already names the machine, so a slightly dimmed row is enough hint. */
218
218
  .session-item.remote { opacity: 0.9; cursor: pointer; }
219
219
  .session-item.remote:hover { opacity: 1; }
220
+ /* WS4.2: explicit per-machine state when a pool machine has no session
221
+ tiles — an idle machine must never look identical to a broken one.
222
+ Plain flex row; inherits the sidebar width (mobile-safe). */
223
+ .machine-status-row {
224
+ display: flex;
225
+ align-items: center;
226
+ gap: 8px;
227
+ padding: 8px 12px;
228
+ font-size: 11px;
229
+ color: var(--text-dim, #8b8fa3);
230
+ border-top: 1px dashed rgba(139, 143, 163, 0.25);
231
+ }
232
+ .machine-status-row.offline .machine-status-text { color: #f87171; }
233
+ .machine-status-row.online .machine-status-text { opacity: 0.85; }
220
234
 
221
235
  .type-badge {
222
236
  padding: 1px 6px;
@@ -3703,6 +3717,10 @@
3703
3717
  // Kept separate from `sessions` because the WebSocket replaces that array
3704
3718
  // with LOCAL sessions on every broadcast — merging would be wiped each tick.
3705
3719
  let remoteSessions = [];
3720
+ // WS4.2 (multi-machine-seamlessness spec, F7): pool machine inventory for the
3721
+ // per-machine empty-state strip. Populated only when the pool is enabled with
3722
+ // 2+ machines — single-machine agents never see the strip (strict no-op).
3723
+ let poolMachinesView = [];
3706
3724
  let selfMachineNickname = null;
3707
3725
  let activeSession = null;
3708
3726
  let activeMachineId = null; // non-null when the active session streams from a peer (§2.2)
@@ -3964,6 +3982,9 @@
3964
3982
  empty.style.display = 'flex';
3965
3983
  // Clear any existing session items
3966
3984
  list.querySelectorAll('.session-item').forEach(el => el.remove());
3985
+ // WS4.2: even with zero sessions anywhere, each pool machine still gets
3986
+ // an explicit state row — an idle machine must never look broken.
3987
+ renderMachineStatusStrip(list, all);
3967
3988
  return;
3968
3989
  }
3969
3990
 
@@ -4051,6 +4072,37 @@
4051
4072
  ${telemetryHtml}
4052
4073
  `;
4053
4074
  }
4075
+
4076
+ // WS4.2: per-machine state strip after the tiles.
4077
+ renderMachineStatusStrip(list, all);
4078
+ }
4079
+
4080
+ // WS4.2 (multi-machine-seamlessness spec, F7 — 2026-06-12 live incident):
4081
+ // a machine with zero running sessions used to render as NOTHING, identical
4082
+ // to a broken or unreachable machine. Render an explicit state row per pool
4083
+ // machine that has no session tiles: "online — no active sessions" when its
4084
+ // heartbeat is live, "not reachable — last seen <t>" when it is not. Pool
4085
+ // disabled or single-machine → poolMachinesView is empty → zero rows, zero
4086
+ // behavior change (single-machine strict no-op, spec invariant 6).
4087
+ function machineStatusInfo(m) {
4088
+ if (m.online) return { cls: 'online', text: 'online — no active sessions' };
4089
+ const seen = m.selfReportedLastSeen ? new Date(m.selfReportedLastSeen).getTime() : null;
4090
+ const rel = seen ? fmtRelTime(seen) : 'unknown';
4091
+ return { cls: 'offline', text: `not reachable — last seen ${rel}` };
4092
+ }
4093
+ function renderMachineStatusStrip(list, allSessions) {
4094
+ list.querySelectorAll('.machine-status-row').forEach(el => el.remove());
4095
+ if (!poolMachinesView.length) return;
4096
+ const busy = new Set(allSessions.map(s => s.machineNickname || s.machineId).filter(Boolean));
4097
+ for (const m of poolMachinesView) {
4098
+ const label = m.nickname || m.machineId || 'unnamed machine';
4099
+ if (busy.has(m.nickname) || busy.has(m.machineId)) continue; // has tiles — badge already names it
4100
+ const info = machineStatusInfo(m);
4101
+ const row = document.createElement('div');
4102
+ row.className = `machine-status-row ${info.cls}`;
4103
+ row.innerHTML = `<span class="machine-badge">${escapeHtml(label)}</span><span class="machine-status-text">${escapeHtml(info.text)}</span>`;
4104
+ list.appendChild(row);
4105
+ }
4054
4106
  }
4055
4107
 
4056
4108
  // ── Terminal ──────────────────────────────────────────────
@@ -4606,6 +4658,15 @@
4606
4658
  remoteSessions = (j.sessions || []).filter(s => s.remote === true && (s.status === 'running' || s.status === 'starting'));
4607
4659
  renderSessionList();
4608
4660
  } catch { /* best-effort — peers may be offline; next tick retries */ }
4661
+ // WS4.2: refresh the machine inventory on the same cadence so the sessions
4662
+ // view can say "online — no active sessions" vs "not reachable" per machine.
4663
+ try {
4664
+ const pool = await apiFetch('/pool');
4665
+ poolMachinesView = (pool && pool.enabled && Array.isArray(pool.machines) && pool.machines.length >= 2)
4666
+ ? pool.machines
4667
+ : [];
4668
+ renderSessionList();
4669
+ } catch { /* best-effort — strip simply stays absent until the next tick */ }
4609
4670
  }
4610
4671
  function startPoolSessionsPolling() {
4611
4672
  if (poolPollTimer) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.495",
3
+ "version": "1.3.496",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-06-12T21:22:33.499Z",
5
- "instarVersion": "1.3.495",
4
+ "generatedAt": "2026-06-12T21:27:13.599Z",
5
+ "instarVersion": "1.3.496",
6
6
  "entryCount": 201,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -9,10 +9,31 @@ The PIN-gated user→agent floor-grant route (`POST /mandate/:id/grants`) finall
9
9
 
10
10
  Agent awareness ships both ways: the CLAUDE.md template gains a "User floor-action grants are phone-first" bullet (send the operator the dashboard link, NEVER a terminal command), and a `PostUpdateMigrator` migration inserts the same bullet into existing agents' Coordination Mandate section (anchored insertion, append fallback, idempotent). `CapabilityIndex` entries updated for both routes.
11
11
 
12
+ The dashboard sessions view rendered NOTHING for a pool machine with zero running
13
+ sessions — visually identical to a machine that is offline or unreachable. Live
14
+ incident 2026-06-12 (topic 13481): minutes after the pool-tile status filter shipped,
15
+ an idle-but-healthy Mac Mini disappeared from the sessions view entirely and the
16
+ operator reasonably read it as a regression.
17
+
18
+ The sessions view now renders an explicit state row for every pool machine that has
19
+ no session tiles: **"online — no active sessions"** when its heartbeat is live, or
20
+ **"not reachable — last seen <t>"** when it is not. Data comes from the existing
21
+ `GET /pool` machine inventory on the existing 15-second pool poll — no new routes, no
22
+ server changes. Machines that have session tiles get no redundant row (their tiles
23
+ already carry the machine badge). Pool disabled or single-machine: the strip is never
24
+ rendered — strict no-op, locked by test. This is WS4.2 of the converged
25
+ multi-machine-seamlessness spec (`docs/specs/MULTI-MACHINE-SEAMLESSNESS-SPEC.md`,
26
+ 3-round convergence, 91 findings addressed), whose audit and ELI16 companion ship in
27
+ this same PR.
28
+
12
29
  ## What to Tell Your User
13
30
 
14
31
  - "You can now grant a teammate a time-boxed floor action (like a 1-hour prod-deploy) straight from the dashboard on your phone — pick the person, the action, and the duration, type your PIN, tap Grant. No terminal, no laptop."
15
32
 
33
+ - "Your dashboard's session list now tells you the state of every machine, even the
34
+ quiet ones — an idle machine says 'online — no active sessions' instead of showing
35
+ nothing, so you can always tell a resting machine from a broken one."
36
+
16
37
  ## Summary of New Capabilities
17
38
 
18
39
  | Capability | How to Use |
@@ -20,6 +41,10 @@ Agent awareness ships both ways: the CLAUDE.md template gains a "User floor-acti
20
41
  | Phone-first floor-action grants | Dashboard → Mandates tab → any active mandate → "Grant a user a floor action" |
21
42
  | Registered-user person picker | Automatic (`GET /permissions/users` feeds it) |
22
43
 
44
+ | Capability | How to Use |
45
+ |-----------|-----------|
46
+ | Per-machine empty-state in the sessions view | Automatic — open the dashboard sessions list on any multi-machine pool |
47
+
23
48
  ## Evidence
24
49
 
25
50
  - Unit: `tests/unit/dashboard-mandateGrantForm.test.ts` (12 tests — pick-don't-type rendering, PIN discipline incl. never-retained, expiry clamp, enum-drift pin, XSS, registry-failure degradation) + `tests/unit/PostUpdateMigrator-floorGrantPhoneFirst.test.ts` (4 tests — fresh install, anchored insertion, hand-edited fallback, idempotency).
@@ -27,3 +52,15 @@ Agent awareness ships both ways: the CLAUDE.md template gains a "User floor-acti
27
52
  - E2E: `tests/e2e/coordination-mandate-lifecycle.test.ts` (+3 — route alive + Bearer-gated on the production boot path; the form's EXACT payload signs a grant end-to-end; Bearer-without-PIN structurally refused).
28
53
  - All 24 pre-existing Mandates-tab tests + the full mandate-area suite (109 tests) green.
29
54
  - Side-effects artifact: `upgrades/side-effects/mandate-grant-form.md`.
55
+
56
+ - New test file `tests/unit/dashboard-machineEmptyState.test.ts` (6 tests, at-rest
57
+ HTML/JS assertions following the established dashboard test pattern) — proven
58
+ failing 6/6 against the pre-change file, all pass on the fix. Covers: the
59
+ single-machine no-op gate, both honest state strings, the zero-sessions branch,
60
+ no-duplicate-rows reconciliation, no redundant rows for busy machines, and HTML
61
+ escaping of machine-provided strings.
62
+ - `tests/integration/pool-routes.test.ts` extended to lock the `/pool` contract field
63
+ (`selfReportedLastSeen`) the strip consumes.
64
+ - Side-effects artifact: `upgrades/side-effects/multi-machine-seamlessness-ws42.md`.
65
+ - Spec: `docs/specs/MULTI-MACHINE-SEAMLESSNESS-SPEC.md` (converged + approved) with
66
+ convergence report `docs/specs/reports/multi-machine-seamlessness-convergence.md`.
@@ -0,0 +1,59 @@
1
+ # Side-Effects Review — WS4.2 per-machine empty-state strip (dashboard sessions view)
2
+
3
+ **Spec:** docs/specs/MULTI-MACHINE-SEAMLESSNESS-SPEC.md (converged 2026-06-12, 3 iterations; approved)
4
+ **Change:** dashboard/index.html (+ tests). The sessions view renders an explicit state
5
+ row for every pool machine that has no session tiles: "online — no active sessions"
6
+ when its heartbeat is live, "not reachable — last seen <t>" when not. Data: the
7
+ existing `GET /pool` machines array, fetched on the existing 15s pool poll cadence.
8
+ Closes audit finding F7 (2026-06-12 live incident: idle Mini rendered as nothing and
9
+ read as a regression).
10
+
11
+ ## 1. Over-block
12
+ No issue identified — the change blocks nothing. It is a read-only presentation
13
+ addition; no input is rejected anywhere.
14
+
15
+ ## 2. Under-block
16
+ No issue identified — there is no blocking surface. Honesty boundary worth naming:
17
+ `online:false` cannot distinguish "deliberately shut down" from "network-unreachable",
18
+ so the row says "not reachable — last seen <t>" (data-honest) rather than claiming to
19
+ know which. The spec's three-state wording maps onto the two states the data supports;
20
+ this is recorded as the deliberate build decision the converged spec's round-3
21
+ adversarial pass classified as adequately constrained.
22
+
23
+ ## 3. Level-of-abstraction fit
24
+ Right layer. The dashboard already consumes `/pool` (Machines tab) and
25
+ `/sessions?scope=pool` (tiles); the strip is pure client-side composition of data both
26
+ endpoints already serve. No new route, no server change. A server-side "empty
27
+ machines" field would duplicate client-known state at a worse layer.
28
+
29
+ ## 4. Signal vs authority compliance
30
+ Compliant by vacuity: the change introduces NO decision point — it gates no flow,
31
+ filters no message, constrains no behavior. (Phase-1 principle check recorded the
32
+ same conclusion in the build transcript.)
33
+
34
+ ## 5. Interactions
35
+ - The strip renders AFTER session tiles and clears its own rows per render — no
36
+ interference with tile add/remove reconciliation (verified by test: no duplicate
37
+ accumulation).
38
+ - The zero-sessions early-return branch now also renders the strip; the existing
39
+ #emptyState banner still shows alongside it (intended: "no sessions" + per-machine
40
+ states are complementary, not contradictory).
41
+ - A machine WITH tiles gets no row (its tiles' machine badge already names it) —
42
+ prevents double-labeling.
43
+ - The extra `GET /pool` per 15s poll tick matches the Machines tab's existing call
44
+ pattern and is auth'd identically (apiFetch). Failure is swallowed best-effort: the
45
+ strip simply stays absent until the next tick — degraded view, never an error loop.
46
+
47
+ ## 6. External surfaces
48
+ None beyond the operator's own PIN-gated dashboard. No new data is exposed: nickname,
49
+ online, lastSeen are already rendered on the Machines tab. Machine-provided strings
50
+ are escaped before DOM injection (tested). Single-machine agents: `poolMachinesView`
51
+ is populated only when the pool is enabled with 2+ machines — strict no-op, tested.
52
+
53
+ ## 7. Rollback cost
54
+ Trivial: revert the dashboard/index.html hunk (static asset; no data, no migration,
55
+ no state). Ships with the next release; rollback is a one-commit revert and re-release.
56
+
57
+ ## Second-pass review
58
+ Not required — no block/allow decisions, no session lifecycle, no
59
+ gate/sentinel/watchdog surface. (Phase-5 trigger list consulted; none match.)