instar 1.3.1585 → 1.3.1587

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.
Files changed (64) hide show
  1. package/dashboard/console/console.css +11 -0
  2. package/dashboard/console/sessions-model.js +26 -0
  3. package/dashboard/console/sessions.js +63 -3
  4. package/dist/commands/server.d.ts.map +1 -1
  5. package/dist/commands/server.js +20 -2
  6. package/dist/commands/server.js.map +1 -1
  7. package/dist/core/AutonomousRunStore.d.ts +7 -0
  8. package/dist/core/AutonomousRunStore.d.ts.map +1 -1
  9. package/dist/core/AutonomousRunStore.js +8 -1
  10. package/dist/core/AutonomousRunStore.js.map +1 -1
  11. package/dist/core/AutonomousStartAdmission.d.ts +94 -0
  12. package/dist/core/AutonomousStartAdmission.d.ts.map +1 -0
  13. package/dist/core/AutonomousStartAdmission.js +170 -0
  14. package/dist/core/AutonomousStartAdmission.js.map +1 -0
  15. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  16. package/dist/core/PostUpdateMigrator.js +6 -0
  17. package/dist/core/PostUpdateMigrator.js.map +1 -1
  18. package/dist/core/QueueDrainLoop.d.ts +10 -2
  19. package/dist/core/QueueDrainLoop.d.ts.map +1 -1
  20. package/dist/core/QueueDrainLoop.js +28 -6
  21. package/dist/core/QueueDrainLoop.js.map +1 -1
  22. package/dist/core/SessionRouter.d.ts +21 -3
  23. package/dist/core/SessionRouter.d.ts.map +1 -1
  24. package/dist/core/SessionRouter.js +58 -6
  25. package/dist/core/SessionRouter.js.map +1 -1
  26. package/dist/core/SessionTranscriptReader.d.ts +25 -0
  27. package/dist/core/SessionTranscriptReader.d.ts.map +1 -0
  28. package/dist/core/SessionTranscriptReader.js +143 -0
  29. package/dist/core/SessionTranscriptReader.js.map +1 -0
  30. package/dist/core/types.d.ts +8 -0
  31. package/dist/core/types.d.ts.map +1 -1
  32. package/dist/core/types.js.map +1 -1
  33. package/dist/data/standards-guard-index.json +1 -1
  34. package/dist/data/standards-guard-index.meta.json +2 -2
  35. package/dist/data/standards-registry.meta.json +1 -1
  36. package/dist/native/instar-desktop-host +0 -0
  37. package/dist/scaffold/templates.d.ts.map +1 -1
  38. package/dist/scaffold/templates.js +1 -0
  39. package/dist/scaffold/templates.js.map +1 -1
  40. package/dist/server/AgentServer.d.ts +4 -0
  41. package/dist/server/AgentServer.d.ts.map +1 -1
  42. package/dist/server/AgentServer.js +26 -0
  43. package/dist/server/AgentServer.js.map +1 -1
  44. package/dist/server/routes.d.ts.map +1 -1
  45. package/dist/server/routes.js +102 -2
  46. package/dist/server/routes.js.map +1 -1
  47. package/dist/testing/selfActionRegistry.d.ts.map +1 -1
  48. package/dist/testing/selfActionRegistry.js +37 -0
  49. package/dist/testing/selfActionRegistry.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/data/builtin-manifest.json +64 -64
  52. package/src/data/standards-guard-index.json +1 -1
  53. package/src/data/standards-guard-index.meta.json +2 -2
  54. package/src/data/standards-registry.meta.json +1 -1
  55. package/src/scaffold/templates.ts +1 -0
  56. package/upgrades/1.3.1586.md +28 -0
  57. package/upgrades/1.3.1587.md +27 -0
  58. package/upgrades/side-effects/autonomous-start-admission.eli16.md +27 -0
  59. package/upgrades/side-effects/autonomous-start-admission.md +141 -0
  60. package/upgrades/side-effects/console-session-history.eli16.md +14 -0
  61. package/upgrades/side-effects/console-session-history.md +32 -0
  62. package/upgrades/side-effects/ecosystem-idea-pipeline-spec.md +44 -0
  63. package/upgrades/side-effects/spawn-handoff-ack.md +117 -0
  64. package/upgrades/spawn-handoff-ack.eli16.md +43 -0
@@ -985,6 +985,17 @@ button.run-mark:focus-visible { outline: 2px solid var(--accent); outline-offset
985
985
  .sess-term-wrap { position: relative; flex: 1; min-height: 320px; display: flex; }
986
986
  .sess-term { flex: 1; min-width: 0; min-height: 320px; height: 52dvh; background: #0a0e17; border: 1px solid var(--hairline); border-radius: var(--r-md); padding: 6px; overflow: hidden; }
987
987
  .sess-term.plain { overflow: auto; }
988
+ .sess-tabs { display: flex; gap: var(--s-2); }
989
+ .sess-tab[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); background: var(--accent-wash); }
990
+ .sess-history { flex: 1; min-width: 0; min-height: 320px; height: 52dvh; overflow-y: auto; padding: var(--s-3); background: var(--surface); border: 1px solid var(--hairline); border-radius: var(--r-md); display: flex; flex-direction: column; gap: var(--s-3); }
991
+ .sess-history[hidden], .sess-term[hidden] { display: none; }
992
+ .sess-history-note { margin: 0; text-align: center; font-size: 13px; color: var(--text-muted); }
993
+ .sess-h { max-width: 92%; }
994
+ .sess-h-user { align-self: flex-end; background: var(--accent-wash); border: 1px solid var(--hairline); border-radius: var(--r-md); padding: var(--s-2) var(--s-3); }
995
+ .sess-h-assistant { align-self: flex-start; }
996
+ .sess-h-who { font-size: 12px; color: var(--text-faint); margin-bottom: 2px; }
997
+ .sess-h-text { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 14.5px; line-height: 1.5; color: var(--text); }
998
+ .sess-h-tool { margin: 0; font-family: var(--font-mono); font-size: 12px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
988
999
  .sess-pre { margin: 0; font-family: var(--font-mono); font-size: 12px; line-height: 1.4; color: #d6dbe4; white-space: pre-wrap; overflow-wrap: anywhere; }
989
1000
  .sess-resume { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); z-index: 2; }
990
1001
  .sess-composer { display: flex; gap: var(--s-2); align-items: flex-end; position: sticky; bottom: 0; padding: var(--s-2) 0; background: var(--bg); }
@@ -121,6 +121,32 @@ export function isInternalSession(s) {
121
121
  return (s.platform || 'headless') === 'headless' && s.triggeredBy !== OPERATOR_CREATED;
122
122
  }
123
123
 
124
+ /** The History read for a session: its conversation record, a page at a time. A session
125
+ * on another machine is read through this machine, which asks the owner. */
126
+ export const HISTORY_PAGE = 150;
127
+ export function historyUrl(s, before = null) {
128
+ const q = new URLSearchParams({ limit: String(HISTORY_PAGE) });
129
+ if (before != null) q.set('before', String(before));
130
+ if (s.remote && s.machineId) q.set('machineId', s.machineId);
131
+ return `/sessions/${encodeURIComponent(s.tmuxSession)}/transcript?${q}`;
132
+ }
133
+
134
+ /** Words for a failed History read: never "no history" when the read failed. */
135
+ export function historyErrorWords(res) {
136
+ const d = (res && res.data) || {};
137
+ if (d.reason === 'no-transcript') return 'This session has no conversation record yet.';
138
+ if (d.reason === 'peer-unreachable' || d.reason === 'peer-unknown') return `Can’t tell: ${d.error || 'the session’s machine did not answer'}.`;
139
+ const why = d.error || (res && res.reason) || (res && res.status ? `the server answered ${res.status}` : 'the server could not be reached');
140
+ return `Can’t tell: ${String(why).replace(/\.$/, '')}.`;
141
+ }
142
+
143
+ /** A user line from a chat bridge carries a routing prefix; show only what was said. */
144
+ export function historyText(e) {
145
+ let t = String((e && e.text) || '');
146
+ if (e && e.role === 'user') t = t.replace(/^\[(telegram|slack|whatsapp):[^\]]*\]\s*/, '');
147
+ return t;
148
+ }
149
+
124
150
  export function buildSessions(sessionsRes, { ws = null, now = Date.now(), topicNames = {}, showInternal = false } = {}) {
125
151
  if (!sessionsRes || !sessionsRes.ok) return { ok: false, reason: `Couldn’t list sessions: ${(sessionsRes && sessionsRes.reason) || 'no answer'}.` };
126
152
  const data = sessionsRes.data || {};
@@ -11,7 +11,7 @@ import {
11
11
  buildSessions, sessionsHeadline, sessionsFacts, partialWords, titleLine, closeRequest, closeConfirmWords,
12
12
  closeResultWords, doorChoices, modelChoices, doorModelWords, regimeWords, machineChoices, PLATFORMS,
13
13
  platformAllowsDoor, createBody, createResultWords, defaultWords, profileSourceWords, switchLanded,
14
- canSwitchDoor, attachmentMessage, KEYS, STOP_KEY, frame, nextHistoryBatch, streamErrorWords, composerRows, modelWords, HISTORY_FIRST,
14
+ canSwitchDoor, attachmentMessage, historyUrl, historyErrorWords, historyText, KEYS, STOP_KEY, frame, nextHistoryBatch, streamErrorWords, composerRows, modelWords, HISTORY_FIRST,
15
15
  } from './sessions-model.js';
16
16
  import { el, setKids, pageHead, sheet, closeSheet, story, details, note, cantTell } from './ui.js';
17
17
  import { legacyHref } from './nav-model.js';
@@ -266,7 +266,13 @@ function openView(box, row) {
266
266
  attachBtn.addEventListener('click', () => filePick.click());
267
267
  filePick.addEventListener('change', () => { addFiles([...filePick.files]); filePick.value = ''; });
268
268
  const tray = el('div', { class: 'keys-tray', hidden: true });
269
- const view = { key: row.key, s, titleSub, statusLine, termBox, resume, following: true, pending: null, loaded: HISTORY_FIRST, exhausted: false, loading: false, term: null, fit: null, pre: null };
269
+ // Live (the terminal) or History (the conversation record, which goes back to the start:
270
+ // a full-screen session keeps no terminal scrollback).
271
+ const historyBox = el('div', { class: 'sess-history', hidden: true, 'aria-label': 'Conversation history' });
272
+ const liveTab = el('button', { class: 'chip sess-tab', type: 'button', text: 'Live', 'aria-pressed': 'true', 'data-tab': 'live' });
273
+ const historyTab = el('button', { class: 'chip sess-tab', type: 'button', text: 'History', 'aria-pressed': 'false', 'data-tab': 'history' });
274
+ const tabs = el('div', { class: 'sess-tabs', role: 'group', 'aria-label': 'View' }, liveTab, historyTab);
275
+ const view = { key: row.key, s, titleSub, statusLine, termBox, historyBox, history: null, resume, following: true, pending: null, loaded: HISTORY_FIRST, exhausted: false, loading: false, term: null, fit: null, pre: null };
270
276
  st.view = view;
271
277
 
272
278
  // Keys: the ordinary ones, then Stop separated, red, and asked once.
@@ -316,14 +322,24 @@ function openView(box, row) {
316
322
  input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); sendText(); } });
317
323
  more.addEventListener('click', () => moreSheet(view));
318
324
  resume.addEventListener('click', () => backToLive());
325
+ const showTab = (which) => {
326
+ const hist = which === 'history';
327
+ liveTab.setAttribute('aria-pressed', hist ? 'false' : 'true'); historyTab.setAttribute('aria-pressed', hist ? 'true' : 'false');
328
+ termBox.hidden = hist; historyBox.hidden = !hist; if (hist) resume.hidden = true;
329
+ if (hist && !view.history) openHistory(view);
330
+ if (!hist && view.fit) requestAnimationFrame(() => { try { view.fit.fit(); } catch { /* not visible yet */ } });
331
+ };
332
+ liveTab.addEventListener('click', () => showTab('live'));
333
+ historyTab.addEventListener('click', () => showTab('history'));
319
334
 
320
335
  setKids(box, el('div', { class: 'sess-open', 'data-open': row.key },
321
336
  el('div', { class: 'sess-bar' },
322
337
  el('a', { class: 'chip icon-btn sess-back', href: listHref, 'aria-label': 'All sessions', text: '‹' }),
323
338
  el('div', { class: 'sess-titles' }, el('h1', { class: 'sess-title', text: row.title }), titleSub),
324
339
  more),
340
+ tabs,
325
341
  statusLine,
326
- el('div', { class: 'sess-term-wrap' }, termBox, resume),
342
+ el('div', { class: 'sess-term-wrap' }, termBox, historyBox, resume),
327
343
  chips,
328
344
  composer,
329
345
  tray));
@@ -416,6 +432,50 @@ function sendKey(key) {
416
432
  if (!wsSend(frame('key', st.view.s, { key }))) setStatus('Not sent: live updates are paused.');
417
433
  }
418
434
 
435
+ // ── History: the conversation record, newest at the bottom, older on scroll-up ──
436
+ async function openHistory(view) {
437
+ view.history = { before: null, done: false, loading: false, count: 0 };
438
+ view.historyBox.addEventListener('scroll', () => { if (view.historyBox.scrollTop < 200) loadHistory(view); });
439
+ await loadHistory(view);
440
+ }
441
+
442
+ async function loadHistory(view) {
443
+ const h = view.history;
444
+ if (!h || h.loading || h.done || st.view !== view) return;
445
+ h.loading = true;
446
+ const box = view.historyBox;
447
+ const marker = el('p', { class: 'sess-history-note', text: h.count ? 'Loading earlier messages…' : 'Loading the conversation…' });
448
+ box.prepend(marker);
449
+ const res = await send('GET', historyUrl(view.s, h.before));
450
+ marker.remove();
451
+ h.loading = false;
452
+ if (st.view !== view) return;
453
+ if (!res.ok || !res.data || !Array.isArray(res.data.entries)) {
454
+ h.done = true;
455
+ box.prepend(el('p', { class: 'sess-history-note', text: historyErrorWords(res) }));
456
+ return;
457
+ }
458
+ const first = !h.count;
459
+ const prevHeight = box.scrollHeight, prevTop = box.scrollTop;
460
+ const frag = document.createDocumentFragment();
461
+ for (const e of res.data.entries) {
462
+ const text = historyText(e);
463
+ if (!text) continue;
464
+ const when = e.at ? new Date(e.at).toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : null;
465
+ frag.append(e.role === 'tool'
466
+ ? el('p', { class: 'sess-h sess-h-tool', text: `→ ${text}` })
467
+ : el('div', { class: `sess-h sess-h-${e.role}` }, el('div', { class: 'sess-h-who', text: [e.role === 'user' ? 'Message in' : 'Session', when].filter(Boolean).join(' · ') }), el('div', { class: 'sess-h-text', text })));
468
+ }
469
+ h.count += res.data.entries.length;
470
+ h.before = res.data.nextBefore;
471
+ if (h.before == null) { h.done = true; frag.prepend(el('p', { class: 'sess-history-note', text: h.count ? 'The start of this conversation.' : 'Nothing has been said in this session yet.' })); }
472
+ box.prepend(frag);
473
+ if (first) box.scrollTop = box.scrollHeight;
474
+ else box.scrollTop = prevTop + (box.scrollHeight - prevHeight);
475
+ // A short first page may not fill the box: keep loading until it scrolls.
476
+ if (!h.done && box.scrollHeight <= box.clientHeight + 200) loadHistory(view);
477
+ }
478
+
419
479
  function leaveView() {
420
480
  const v = st.view;
421
481
  if (!v) return;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"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"}
@@ -26536,11 +26536,29 @@ export async function startServer(options) {
26536
26536
  }
26537
26537
  throw new Error(`deliverMessage rejected: ${res.status} ${res.reason ?? ''}`);
26538
26538
  },
26539
- spawnOnMachine: async (machineId, msg) => {
26539
+ // The placement hop is a deliverMessage at the epoch the CAS just
26540
+ // landed, read exactly like a forward. It used to send epoch 0 and
26541
+ // discard the reply: a receiver holding an older record for the
26542
+ // topic NACKed it stale and the router still logged 'spawned'.
26543
+ spawnOnMachine: async (machineId, msg, ownershipEpoch) => {
26540
26544
  const url = peerUrl(machineId);
26541
26545
  if (!url)
26542
26546
  throw new Error(`no peer url for ${machineId}`);
26543
- await meshClient.send({ machineId, url }, { type: 'deliverMessage', session: msg.sessionKey, messageId: msg.messageId, payload: msg.payload, ownershipEpoch: 0 }, 0);
26547
+ const res = await meshClient.send({ machineId, url }, { type: 'deliverMessage', session: msg.sessionKey, messageId: msg.messageId, payload: msg.payload, ownershipEpoch, ...(msg.senderEnvelope ? { senderEnvelope: msg.senderEnvelope } : {}) }, ownershipEpoch);
26548
+ if (res.ok && res.result && typeof res.result === 'object' && 'accepted' in res.result) {
26549
+ return res.result;
26550
+ }
26551
+ throw new Error(`spawn deliverMessage rejected: ${res.status} ${res.reason ?? ''}`);
26552
+ },
26553
+ // Take back a placement whose spawn never landed (see SessionRouter):
26554
+ // only the still-placing record this router put on `placedOn`.
26555
+ reclaimPlacement: (sk, placedOn) => {
26556
+ const cur = ownReg.read(sk);
26557
+ if (!cur || cur.status !== 'placing' || cur.ownerMachineId !== placedOn)
26558
+ return false;
26559
+ const r = ownReg.cas({ type: 'force-claim', machineId: meshSelfId }, { sessionKey: sk, sender: meshSelfId, nonce: `${meshSelfId}:rc:${++routerNonce}` });
26560
+ emitPlacement(sk, r, 'failover', placedOn);
26561
+ return r.ok;
26544
26562
  },
26545
26563
  handleLocally: async () => { },
26546
26564
  // Durable Inbound Message Queue §2.2: tri-state custody taking.