instar 1.3.492 → 1.3.493

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.
@@ -1381,6 +1381,7 @@
1381
1381
  .toast.success { background: #166534; border: 1px solid #22c55e; }
1382
1382
  .toast.error { background: #7f1d1d; border: 1px solid #ef4444; }
1383
1383
  .toast.warning { background: #713f12; border: 1px solid #eab308; }
1384
+ .toast.info { background: #1e3a8a; border: 1px solid #60a5fa; }
1384
1385
 
1385
1386
  @keyframes toastIn {
1386
1387
  from { opacity: 0; transform: translateY(8px); }
@@ -1583,6 +1584,15 @@
1583
1584
  background: rgba(239, 68, 68, 0.1);
1584
1585
  }
1585
1586
 
1587
+ /* In-flight close (REMOTE-SESSION-CLOSE-SPEC §2.2): a relayed close can
1588
+ take up to ~5s against a dead peer — show pending, not an inert button
1589
+ the user double-clicks into parallel relays. !important outranks the
1590
+ hover/active opacity rules above. */
1591
+ .session-close-btn:disabled {
1592
+ opacity: 0.35 !important;
1593
+ cursor: wait;
1594
+ }
1595
+
1586
1596
  .session-name-row {
1587
1597
  display: flex;
1588
1598
  align-items: center;
@@ -4023,7 +4033,7 @@
4023
4033
  el.innerHTML = `
4024
4034
  <div class="session-name-row">
4025
4035
  <div class="session-name">${escapeHtml(session.platformName || session.name)}</div>
4026
- ${session.remote ? '' : `<button class="session-close-btn" data-tmux="${escapeHtml(session.tmuxSession)}" data-name="${escapeHtml(session.name)}" onclick="event.stopPropagation();closeSession(this.dataset.tmux,this.dataset.name)" title="Close session">&times;</button>`}
4036
+ <button class="session-close-btn" data-tmux="${escapeHtml(session.tmuxSession)}" data-name="${escapeHtml(session.name)}"${session.remote ? ` data-remote="1" data-uuid="${escapeHtml(session.id || '')}" data-machine-id="${escapeHtml(session.machineId || '')}" data-machine-nickname="${escapeHtml(session.machineNickname || '')}"` : ''} data-protected="${session.protected === true ? '1' : session.protected === false ? '0' : ''}"${closesInFlight.has(closeKey(session.remote ? session.machineId : null, session.tmuxSession)) ? ' disabled' : ''} onclick="event.stopPropagation();closeSessionFromBtn(this)" title="Close session">&times;</button>
4027
4037
  </div>
4028
4038
  <div class="session-meta">
4029
4039
  <span class="type-badge ${sType}">${sType}</span>
@@ -4421,16 +4431,101 @@
4421
4431
  }
4422
4432
 
4423
4433
  // ── Close Session ─────────────────────────────────────────
4424
- async function closeSession(tmuxSession, sessionName) {
4425
- if (!confirm(`Close session "${sessionName}"?`)) return;
4434
+ // Remote-session close (REMOTE-SESSION-CLOSE-SPEC §2.2): remote tiles
4435
+ // render the same × as local tiles, relayed via
4436
+ // POST /sessions/:name/remote-close. The confirm is the informed-consent
4437
+ // surface — it names the machine and flags a PROTECTED session (close
4438
+ // still proceeds on confirm; operator-origin semantics, there is no
4439
+ // server-side refusal). A flag-less remote row (pre-feature peer) says
4440
+ // "protection status unknown" rather than silently implying unprotected.
4441
+ const closesInFlight = new Set(); // tile keys with a close pending — survives re-renders
4442
+
4443
+ function closeKey(machineId, tmuxSession) {
4444
+ return (machineId || 'local') + ':' + tmuxSession;
4445
+ }
4446
+
4447
+ function closeSessionFromBtn(btn) {
4448
+ const d = btn.dataset;
4449
+ closeSession(d.tmux, d.name, {
4450
+ remote: d.remote === '1',
4451
+ sessionUuid: d.uuid || undefined,
4452
+ machineId: d.machineId || undefined,
4453
+ machineNickname: d.machineNickname || undefined,
4454
+ protected: d.protected === '1' ? true : d.protected === '0' ? false : undefined,
4455
+ button: btn,
4456
+ });
4457
+ }
4458
+
4459
+ async function closeSession(tmuxSession, sessionName, opts) {
4460
+ opts = opts || {}; // backward-compatible: local callers may pass (tmux, name) only
4461
+ const nickname = opts.machineNickname || opts.machineId || 'another machine';
4462
+
4463
+ let prompt;
4464
+ if (opts.protected === true) {
4465
+ // Informed consent (spec §2.0/§2.2): protected sessions ARE closeable —
4466
+ // the safety is that the operator closes them knowingly. The local ×
4467
+ // gains this warning too (deliberate UX improvement, spec AC#6).
4468
+ prompt = opts.remote
4469
+ ? `"${sessionName}" is a PROTECTED session on ${nickname} — close it anyway?`
4470
+ : `"${sessionName}" is a PROTECTED session — close it anyway?`;
4471
+ } else {
4472
+ prompt = opts.remote
4473
+ ? `Close session "${sessionName}" on ${nickname}?`
4474
+ : `Close session "${sessionName}"?`;
4475
+ }
4476
+ if (opts.remote && opts.protected === undefined) {
4477
+ // Version skew: a pre-feature peer's records omit the additive
4478
+ // `protected` flag — say so rather than implying unprotected.
4479
+ prompt += ' (protection status unknown — machine needs update)';
4480
+ }
4481
+ if (!confirm(prompt)) return;
4482
+
4483
+ const key = closeKey(opts.remote ? opts.machineId : null, tmuxSession);
4484
+ if (closesInFlight.has(key)) return; // a close for this tile is already in flight
4485
+ closesInFlight.add(key);
4486
+ if (opts.button) opts.button.disabled = true; // pending — no double-click into parallel relays
4426
4487
 
4427
4488
  try {
4428
- const resp = await fetch(`/sessions/${encodeURIComponent(tmuxSession)}`, {
4429
- method: 'DELETE',
4430
- headers: { 'Authorization': `Bearer ${token}` },
4431
- });
4489
+ let resp;
4490
+ if (opts.remote) {
4491
+ resp = await fetch(`/sessions/${encodeURIComponent(sessionName)}/remote-close`, {
4492
+ method: 'POST',
4493
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
4494
+ body: JSON.stringify({ machineId: opts.machineId, sessionUuid: opts.sessionUuid }),
4495
+ });
4496
+ } else {
4497
+ resp = await fetch(`/sessions/${encodeURIComponent(tmuxSession)}`, {
4498
+ method: 'DELETE',
4499
+ headers: { 'Authorization': `Bearer ${token}` },
4500
+ });
4501
+ }
4432
4502
  const data = await resp.json();
4433
4503
 
4504
+ if (opts.remote) {
4505
+ if (resp.ok && data.alreadyClosed) {
4506
+ // Calm path (spec §2.3): a stale tile whose session already ended
4507
+ // is a close that just won, not an error — simultaneous dashboard
4508
+ // viewers are normal.
4509
+ showToast(`Session "${sessionName}" already closed — refreshing`, 'success');
4510
+ } else if (resp.ok) {
4511
+ showToast(`Session "${sessionName}" closed on ${nickname}`, 'success');
4512
+ } else if (data.outcomeUnknown) {
4513
+ // Delivery honesty (spec §2.3): the peer may be mid-kill — report
4514
+ // OBSERVED state via a refresh, never "closed nothing".
4515
+ showToast(`Session "${sessionName}" — outcome unknown — refreshing`, 'info');
4516
+ refreshPoolSessions();
4517
+ return;
4518
+ } else {
4519
+ showToast(data.error || 'Failed to close remote session', 'error');
4520
+ return;
4521
+ }
4522
+ refreshPoolSessions();
4523
+ if (activeSession === tmuxSession && activeMachineId === (opts.machineId || null)) {
4524
+ goBack();
4525
+ }
4526
+ return;
4527
+ }
4528
+
4434
4529
  if (!resp.ok) {
4435
4530
  showToast(data.error || 'Failed to close session', 'error');
4436
4531
  return;
@@ -4444,6 +4539,9 @@
4444
4539
  }
4445
4540
  } catch (err) {
4446
4541
  showToast('Network error: ' + (err.message || 'Cannot reach server'), 'error');
4542
+ } finally {
4543
+ closesInFlight.delete(key);
4544
+ if (opts.button && opts.button.isConnected) opts.button.disabled = false;
4447
4545
  }
4448
4546
  }
4449
4547
 
@@ -4451,7 +4549,7 @@
4451
4549
  if (!activeSession) return;
4452
4550
  const session = sessions.find(s => s.tmuxSession === activeSession);
4453
4551
  const name = session ? session.name : activeSession;
4454
- closeSession(activeSession, name);
4552
+ closeSession(activeSession, name, session ? { protected: session.protected } : undefined);
4455
4553
  }
4456
4554
 
4457
4555
  // ── Helpers ───────────────────────────────────────────────
@@ -4485,26 +4583,28 @@
4485
4583
  // multi-machine pool, poll the aggregated view so every session — on every
4486
4584
  // machine — shows in the list, each tagged with the machine it runs on.
4487
4585
  let poolPollTimer = null;
4586
+ // Callable outside the poll loop too: a remote close triggers an immediate
4587
+ // refresh so the list shows OBSERVED state (REMOTE-SESSION-CLOSE-SPEC §2.2).
4588
+ async function refreshPoolSessions() {
4589
+ try {
4590
+ const r = await fetch('/sessions?scope=pool', { headers: { 'Authorization': `Bearer ${token}` } });
4591
+ if (!r.ok) return;
4592
+ const j = await r.json();
4593
+ selfMachineNickname = (j.pool && j.pool.selfMachineNickname) || null;
4594
+ // LIVE remote sessions only: a peer's plain /sessions returns its FULL
4595
+ // registry (completed/killed records included), while the local sidebar
4596
+ // is built from listRunningSessions(). Without this status filter,
4597
+ // long-dead peer sessions render as live "click to stream" tiles
4598
+ // (2026-06-11: five closed Mac Mini sessions reappeared on the laptop
4599
+ // dashboard hours after they were closed).
4600
+ remoteSessions = (j.sessions || []).filter(s => s.remote === true && (s.status === 'running' || s.status === 'starting'));
4601
+ renderSessionList();
4602
+ } catch { /* best-effort — peers may be offline; next tick retries */ }
4603
+ }
4488
4604
  function startPoolSessionsPolling() {
4489
4605
  if (poolPollTimer) return;
4490
- const poll = async () => {
4491
- try {
4492
- const r = await fetch('/sessions?scope=pool', { headers: { 'Authorization': `Bearer ${token}` } });
4493
- if (!r.ok) return;
4494
- const j = await r.json();
4495
- selfMachineNickname = (j.pool && j.pool.selfMachineNickname) || null;
4496
- // LIVE remote sessions only: a peer's plain /sessions returns its FULL
4497
- // registry (completed/killed records included), while the local sidebar
4498
- // is built from listRunningSessions(). Without this status filter,
4499
- // long-dead peer sessions render as live "click to stream" tiles
4500
- // (2026-06-11: five closed Mac Mini sessions reappeared on the laptop
4501
- // dashboard hours after they were closed).
4502
- remoteSessions = (j.sessions || []).filter(s => s.remote === true && (s.status === 'running' || s.status === 'starting'));
4503
- renderSessionList();
4504
- } catch { /* best-effort — peers may be offline; next tick retries */ }
4505
- };
4506
- poll();
4507
- poolPollTimer = setInterval(poll, 15000);
4606
+ refreshPoolSessions();
4607
+ poolPollTimer = setInterval(refreshPoolSessions, 15000);
4508
4608
  }
4509
4609
 
4510
4610
  function startWaPolling() {
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAuBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAyG7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAmyCD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CAwVN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA2uWtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAuBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAyG7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAmyCD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CAwVN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA8uWtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
@@ -5446,6 +5446,9 @@ export async function startServer(options) {
5446
5446
  reason: e.reason,
5447
5447
  disposition: e.disposition,
5448
5448
  origin: e.origin,
5449
+ // Untrusted provenance claim from a relayed close (spec §2.3) — a
5450
+ // signal in the trail, never an authority input.
5451
+ ...(e.via ? { viaClaim: e.via } : {}),
5449
5452
  // Positive-lane observability (june15-headless-spawn-reroute O4): record
5450
5453
  // which billing lane the reaped session ran on so the soak can confirm
5451
5454
  // rerouted sessions reach their completion from the reap-log too.