@monoes/monomindcli 1.12.0 → 1.14.0

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 (61) hide show
  1. package/.claude/agents/generated/churn-analyst.md +53 -0
  2. package/.claude/agents/generated/code-reviewer.md +55 -0
  3. package/.claude/agents/generated/code-validator.md +57 -0
  4. package/.claude/agents/generated/complexity-scanner.md +56 -0
  5. package/.claude/agents/generated/devbot-orchestrator.md +58 -0
  6. package/.claude/agents/generated/devbot-planner.md +63 -0
  7. package/.claude/agents/generated/impact-assessor.md +54 -0
  8. package/.claude/commands/mastermind/master.md +88 -24
  9. package/.claude/helpers/control-start.cjs +60 -1
  10. package/.claude/helpers/event-logger.cjs +43 -2
  11. package/.claude/helpers/handlers/capture-handler.cjs +336 -0
  12. package/.claude/helpers/handlers/route-handler.cjs +20 -13
  13. package/.claude/helpers/handlers/session-restore-handler.cjs +14 -8
  14. package/.claude/helpers/hook-handler.cjs +57 -1
  15. package/.claude/helpers/intelligence.cjs +129 -57
  16. package/.claude/helpers/memory-palace.cjs +461 -0
  17. package/.claude/helpers/memory.cjs +134 -15
  18. package/.claude/helpers/metrics-db.mjs +87 -0
  19. package/.claude/helpers/router.cjs +296 -41
  20. package/.claude/helpers/session.cjs +107 -32
  21. package/.claude/helpers/statusline.cjs +138 -2
  22. package/.claude/helpers/toggle-statusline.cjs +73 -0
  23. package/.claude/helpers/token-tracker.cjs +934 -0
  24. package/.claude/helpers/utils/monograph.cjs +39 -4
  25. package/.claude/helpers/utils/telemetry.cjs +3 -3
  26. package/.claude/skills/mastermind/createorg.md +227 -16
  27. package/.claude/skills/mastermind/idea.md +15 -3
  28. package/.claude/skills/mastermind/runorg.md +2 -1
  29. package/dist/src/commands/doctor.d.ts.map +1 -1
  30. package/dist/src/commands/doctor.js +96 -4
  31. package/dist/src/commands/doctor.js.map +1 -1
  32. package/dist/src/commands/index.js +2 -0
  33. package/dist/src/commands/org.d.ts +4 -0
  34. package/dist/src/commands/org.d.ts.map +1 -0
  35. package/dist/src/commands/org.js +93 -0
  36. package/dist/src/commands/org.js.map +1 -0
  37. package/dist/src/mcp-tools/memory-tools.js +6 -6
  38. package/dist/src/mcp-tools/memory-tools.js.map +1 -1
  39. package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
  40. package/dist/src/mcp-tools/monograph-tools.js +329 -37
  41. package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
  42. package/dist/src/mcp-tools/session-tools.d.ts.map +1 -1
  43. package/dist/src/mcp-tools/session-tools.js +9 -10
  44. package/dist/src/mcp-tools/session-tools.js.map +1 -1
  45. package/dist/src/mcp-tools/task-tools.d.ts.map +1 -1
  46. package/dist/src/mcp-tools/task-tools.js +7 -8
  47. package/dist/src/mcp-tools/task-tools.js.map +1 -1
  48. package/dist/src/mcp-tools/types.d.ts +1 -0
  49. package/dist/src/mcp-tools/types.d.ts.map +1 -1
  50. package/dist/src/mcp-tools/types.js +49 -0
  51. package/dist/src/mcp-tools/types.js.map +1 -1
  52. package/dist/src/services/worker-daemon.d.ts.map +1 -1
  53. package/dist/src/services/worker-daemon.js +295 -5
  54. package/dist/src/services/worker-daemon.js.map +1 -1
  55. package/dist/src/transfer/serialization/cfp.js +1 -1
  56. package/dist/src/transfer/serialization/cfp.js.map +1 -1
  57. package/dist/src/ui/dashboard.html +2235 -178
  58. package/dist/src/ui/orgs.html +1 -0
  59. package/dist/src/ui/server.mjs +532 -133
  60. package/dist/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +1 -1
@@ -302,16 +302,46 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
302
302
  .odh-pill { font-size: 11px; padding: 2px 8px; border-radius: 8px; background: var(--surface); color: var(--text-lo); border: 1px solid var(--border); }
303
303
  .odh-right { margin-left: auto; display: flex; gap: 6px; }
304
304
 
305
- #org-detail-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); flex-shrink: 0; padding: 0 16px; }
305
+ #org-detail-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); flex-shrink: 0; padding: 0 16px; overflow-x: auto; overflow-y: visible; scrollbar-width: none; }
306
+ #org-detail-tabs::-webkit-scrollbar { display: none; }
306
307
  .odt-btn { font-size: 12px; padding: 8px 12px; background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; cursor: pointer; color: var(--text-lo); font-family: var(--sans); transition: color 0.1s; }
307
308
  .odt-btn:hover { color: var(--text-hi); }
308
- .odt-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
309
+ .odt-btn.active { color: var(--accent); border-bottom-color: var(--accent); opacity: 1; }
310
+ .odt-btn.secondary { opacity: 0.6; }
311
+ .odt-btn.secondary.active { opacity: 1; }
312
+ .odt-tab-sep { width: 1px; background: var(--border); margin: 6px 4px; flex-shrink: 0; align-self: stretch; }
309
313
 
310
314
  #org-detail-body { flex: 1; overflow-y: auto; padding: 18px 20px; }
311
315
  #org-detail-body::-webkit-scrollbar { width: 3px; }
312
316
  #org-detail-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
313
317
  .odt-pane { display: none; }
314
318
  .odt-pane.active { display: block; }
319
+ #odt-chat { display: none; }
320
+ #odt-chat.active { display: flex; flex-direction: column; }
321
+ #odt-chat-bar { display:flex; align-items:center; gap:8px; padding:10px 16px 8px; border-bottom:1px solid var(--border); flex-shrink:0; flex-wrap:wrap; }
322
+ #odt-chat-sess-lbl { font-size:8px; letter-spacing:2px; color:var(--text-xs); flex-shrink:0; }
323
+ #odt-chat-sel { background:var(--surface); color:var(--text-mid); border:1px solid var(--border); border-radius:4px; font-size:10px; font-family:var(--mono); padding:3px 7px; cursor:pointer; max-width:320px; }
324
+ #odt-chat-sel:focus { outline:none; border-color:var(--accent); }
325
+ #odt-chat-live-dot { width:5px; height:5px; border-radius:50%; background:var(--text-xs); flex-shrink:0; margin-left:auto; transition:background 0.4s; }
326
+ #odt-chat-live-dot.on { background:oklch(68% 0.20 150); animation:livepulse-cv 2.2s ease-in-out infinite; }
327
+ #odt-chat-live-lbl { font-size:9px; color:var(--text-lo); }
328
+ #odt-chat-agent-bar { display:flex; align-items:center; gap:6px; padding:5px 16px 6px; border-bottom:1px solid var(--border); flex-shrink:0; flex-wrap:wrap; }
329
+ #odt-chat-agent-lbl { font-size:8px; letter-spacing:2px; color:var(--text-xs); flex-shrink:0; }
330
+ .odt-agent-pill { font-size:8px; padding:2px 8px; border-radius:10px; border:1px solid var(--border); color:var(--text-lo); background:transparent; cursor:pointer; letter-spacing:0.3px; font-family:var(--mono); transition:border-color 0.15s,color 0.15s; }
331
+ .odt-agent-pill:hover { border-color:oklch(62% 0.20 186 / 0.4); color:var(--text-mid); }
332
+ .odt-agent-pill.dim { opacity:0.35; }
333
+ .odt-agent-pill.dim:hover { opacity:0.6; }
334
+ .odt-agent-pill.active { border-color:oklch(62% 0.20 186); color:oklch(62% 0.20 186); background:oklch(62% 0.20 186 / 0.07); }
335
+ #odt-chat-feed { flex:1; overflow-y:auto; padding:10px 16px; display:flex; flex-direction:column; gap:5px; scrollbar-width:thin; scrollbar-color:var(--border) transparent; }
336
+ #odt-chat-feed::-webkit-scrollbar { width:4px; }
337
+ #odt-chat-feed::-webkit-scrollbar-thumb { background:var(--border); border-radius:2px; }
338
+ #odt-chat-empty { font-size:11px; color:var(--text-lo); text-align:center; padding:32px 0; line-height:2; }
339
+ .cv-excerpt-banner { flex-shrink:0; padding:7px 16px 8px; background:oklch(13% 0.01 295); border-bottom:1px solid var(--border); display:none; }
340
+ .cv-excerpt-banner.visible { display:block; }
341
+ .cv-excerpt-label { font-size:8px; letter-spacing:2px; color:var(--text-xs); text-transform:uppercase; margin-bottom:4px; }
342
+ .cv-excerpt-items { display:flex; flex-direction:column; gap:2px; }
343
+ .cv-excerpt-item { font-size:10px; color:var(--text-lo); font-family:var(--mono); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%; }
344
+ .cv-excerpt-item .cv-ex-tag { font-size:8px; color:var(--text-xs); margin-right:5px; display:inline-block; min-width:30px; }
315
345
 
316
346
  /* agent detail drawer (slides in from right within the org detail pane) */
317
347
  #org-agent-drawer {
@@ -693,6 +723,18 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
693
723
  #chat-v-feed::-webkit-scrollbar { width:4px; }
694
724
  #chat-v-feed::-webkit-scrollbar-thumb { background:var(--border); border-radius:2px; }
695
725
  #chat-v-empty { font-size:11px; color:var(--text-lo); text-align:center; padding:32px 0; line-height:2; }
726
+ #chat-v-excerpt { flex-shrink:0; padding:7px 18px 8px; background:oklch(13% 0.01 295); border-bottom:1px solid var(--border); display:none; }
727
+ #chat-v-excerpt.visible { display:block; }
728
+ #chat-v-agent-bar { display:flex; align-items:center; gap:6px; padding:6px 18px 7px; border-bottom:1px solid var(--border); flex-shrink:0; flex-wrap:wrap; }
729
+ #chat-v-agent-lbl { font-size:8px; letter-spacing:2px; color:var(--text-xs); flex-shrink:0; }
730
+ .cv-agent-pill { font-size:8px; padding:2px 8px; border-radius:10px; border:1px solid var(--border); color:var(--text-lo); background:transparent; cursor:pointer; letter-spacing:0.3px; font-family:var(--mono); transition:border-color 0.15s,color 0.15s; }
731
+ .cv-agent-pill:hover { border-color:oklch(62% 0.20 186 / 0.4); color:var(--text-mid); }
732
+ .cv-agent-pill.active { border-color:oklch(62% 0.20 186); color:oklch(62% 0.20 186); background:oklch(62% 0.20 186 / 0.07); }
733
+ .cv-session-hdr { padding:10px 18px 8px; border-bottom:1px solid var(--border); margin-bottom:4px; background:oklch(13% 0.01 295); }
734
+ .cv-shdr-prompt { font-size:12px; color:var(--text-mid); margin-bottom:4px; font-weight:500; }
735
+ .cv-shdr-meta { display:flex; flex-wrap:wrap; gap:6px; align-items:center; font-size:9px; color:var(--text-lo); font-family:var(--mono); }
736
+ .cv-shdr-id { color:var(--text-xs); font-size:8px; }
737
+ .cv-shdr-sep { color:var(--border); }
696
738
  .cv-msg { display:flex; flex-direction:column; max-width:90%; }
697
739
  .cv-msg.cv-sys { align-self:center; max-width:100%; }
698
740
  .cv-msg.cv-agent { align-self:flex-start; }
@@ -1367,9 +1409,6 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1367
1409
  <div class="nav-item" data-view="monograph">
1368
1410
  <span class="ico">⬡</span><span class="lbl">Monograph</span>
1369
1411
  </div>
1370
- <div class="nav-item" data-view="chat">
1371
- <span class="ico">⌘</span><span class="lbl">Agent Chat</span>
1372
- </div>
1373
1412
  </div>
1374
1413
  </div>
1375
1414
  <div class="nav-no-proj" id="nav-no-proj-hint">Select a project above</div>
@@ -1377,13 +1416,16 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1377
1416
  <div class="nav-item" data-view="global">
1378
1417
  <span class="ico">⊕</span><span class="lbl">Global Feed</span>
1379
1418
  </div>
1380
- <div class="nav-item" data-view="global-loops" title="Global Loops — loops across all projects">
1419
+ <div class="nav-item" data-view="global-loops" title="Loops across all projects">
1381
1420
  <span class="ico">↺</span><span class="lbl">Global Loops</span>
1382
1421
  <span class="bdg" id="bdg-global-loops">—</span>
1383
1422
  </div>
1384
- <div class="nav-item" data-view="global-tokens" title="Global Tokens — token usage across all projects">
1423
+ <div class="nav-item" data-view="global-tokens" title="Token usage across all projects">
1385
1424
  <span class="ico">$</span><span class="lbl">Global Tokens</span>
1386
1425
  </div>
1426
+ <div class="nav-item" data-view="chat" title="All agent sessions across projects">
1427
+ <span class="ico">⌘</span><span class="lbl">Global Agent Chat</span>
1428
+ </div>
1387
1429
  </div>
1388
1430
  </div>
1389
1431
  <div id="sb-footer">
@@ -1796,72 +1838,53 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1796
1838
  <span class="odh-pill" id="odh-topo">—</span>
1797
1839
  <span class="odh-pill" id="odh-roles">0 roles</span>
1798
1840
  <div class="odh-right">
1841
+ <button class="btn" onclick="v2ExportOrg()" title="Export org as JSON file" style="color:var(--text-lo);border-color:var(--border)">Export</button>
1842
+ <button class="btn" onclick="v2ImportOrgTrigger()" title="Import org from JSON file" style="color:var(--text-lo);border-color:var(--border)">Import</button>
1799
1843
  <button class="btn" id="org-copy-btn" onclick="v2ShowCopyOrgDialog()" style="color:var(--accent);border-color:var(--accent)">Copy to…</button>
1800
1844
  <button class="btn" id="org-stop-btn" onclick="v2StopOrg()" style="display:none;color:var(--red);border-color:oklch(60% 0.18 25 / 0.4)">Stop</button>
1845
+ <button class="btn" id="org-delete-btn" onclick="v2DeleteOrg()" title="Permanently delete this org and all its data" style="color:var(--red);border-color:oklch(60% 0.18 25 / 0.4)">Delete</button>
1801
1846
  </div>
1802
1847
  </div>
1803
1848
  <div id="org-detail-tabs">
1804
1849
  <button class="odt-btn active" data-tab="chart" onclick="v2SwitchOrgTab('chart')">Chart</button>
1805
- <button class="odt-btn" data-tab="activity" onclick="v2SwitchOrgTab('activity')">Activity</button>
1806
- <button class="odt-btn" data-tab="health" onclick="v2SwitchOrgTab('health')">Health</button>
1807
- <button class="odt-btn" data-tab="approvals" onclick="v2SwitchOrgTab('approvals')">Approvals</button>
1808
- <button class="odt-btn" data-tab="budgets" onclick="v2SwitchOrgTab('budgets')">Budgets</button>
1809
- <button class="odt-btn" data-tab="charts" onclick="v2SwitchOrgTab('charts')">Charts</button>
1810
- <button class="odt-btn" data-tab="skills" onclick="v2SwitchOrgTab('skills')">Skills</button>
1850
+ <button class="odt-btn" data-tab="chat" onclick="v2SwitchOrgTab('chat')">Chat</button>
1851
+ <button class="odt-btn" data-tab="agents-full" onclick="v2SwitchOrgTab('agents-full')">Agents</button>
1852
+ <button class="odt-btn" data-tab="activity" onclick="v2SwitchOrgTab('activity')">Activity</button>
1853
+ <div class="odt-tab-sep" title="Secondary tabs"></div>
1854
+ <button class="odt-btn secondary" data-tab="skills" onclick="v2SwitchOrgTab('skills')">Skills</button>
1855
+
1856
+ <button class="odt-btn secondary" data-tab="costs" onclick="v2SwitchOrgTab('costs')">Costs</button>
1857
+ <button class="odt-btn secondary" data-tab="config" onclick="v2SwitchOrgTab('config')">Config</button>
1858
+ <button class="odt-btn secondary" data-tab="files" onclick="v2SwitchOrgTab('files')">Files</button>
1811
1859
  </div>
1812
1860
  <div id="org-detail-body">
1813
1861
  <div class="odt-pane active" id="odt-chart"></div>
1814
- <div class="odt-pane" id="odt-roles"></div>
1862
+ <div class="odt-pane" id="odt-agents-full"></div>
1815
1863
  <div class="odt-pane" id="odt-activity"></div>
1816
- <div class="odt-pane" id="odt-health"></div>
1817
- <div class="odt-pane" id="odt-heartbeats"></div>
1818
- <div class="odt-pane" id="odt-tasks"></div>
1819
- <div class="odt-pane" id="odt-costs"></div>
1820
- <div class="odt-pane" id="odt-members"></div>
1821
- <div class="odt-pane" id="odt-goals"></div>
1822
- <div class="odt-pane" id="odt-board"></div>
1823
- <div class="odt-pane" id="odt-live"></div>
1824
- <div class="odt-pane" id="odt-approvals"></div>
1825
- <div class="odt-pane" id="odt-secrets"></div>
1826
- <div class="odt-pane" id="odt-settings"></div>
1827
- <div class="odt-pane" id="odt-routines"></div>
1828
- <div class="odt-pane" id="odt-myissues"></div>
1829
- <div class="odt-pane" id="odt-budgets"></div>
1830
- <div class="odt-pane" id="odt-plugins"></div>
1831
- <div class="odt-pane" id="odt-charts"></div>
1832
- <div class="odt-pane" id="odt-projects"></div>
1864
+ <div class="odt-pane" id="odt-chat" style="flex-direction:column;height:100%;overflow:hidden;"></div>
1833
1865
  <div class="odt-pane" id="odt-skills"></div>
1834
- <div class="odt-pane" id="odt-workspaces"></div>
1835
- <div class="odt-pane" id="odt-invites"></div>
1836
- <div class="odt-pane" id="odt-agents-full"></div>
1837
- <div class="odt-pane" id="odt-environments"></div>
1838
- <div class="odt-pane" id="odt-access"></div>
1839
- <div class="odt-pane" id="odt-issues-full"></div>
1840
- <div class="odt-pane" id="odt-join-requests"></div>
1841
- <div class="odt-pane" id="odt-threads"></div>
1866
+ <div class="odt-pane" id="odt-costs"></div>
1867
+ <div class="odt-pane" id="odt-config" style="overflow-y:auto"></div>
1868
+ <div class="odt-pane" id="odt-files" style="overflow-y:auto"></div>
1842
1869
  </div>
1843
1870
  </div>
1844
- <div id="org-copy-dialog" style="display:none;position:absolute;inset:0;background:rgba(0,0,0,0.55);z-index:90;align-items:center;justify-content:center">
1845
- <div style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;width:320px;display:flex;flex-direction:column;gap:14px">
1871
+ <div id="org-copy-dialog" style="display:none;position:absolute;inset:0;background:rgba(0,0,0,0.72);z-index:90;align-items:center;justify-content:center">
1872
+ <div style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;width:380px;max-width:96vw;display:flex;flex-direction:column;gap:12px">
1846
1873
  <div style="font-size:13px;font-weight:600;color:var(--text-hi)">Copy org to project</div>
1847
- <div><input id="org-copy-dest" class="filter-input" placeholder="Destination project path…" style="width:100%"></div>
1874
+ <div style="font-size:10px;color:var(--text-lo);letter-spacing:.05em;margin-bottom:2px">SELECT PROJECT</div>
1875
+ <div id="org-copy-proj-list" style="max-height:160px;overflow-y:auto;border:1px solid var(--border);border-radius:4px;background:var(--bg)"></div>
1876
+ <div style="font-size:10px;color:var(--text-lo);letter-spacing:.05em;margin-top:2px">OR ENTER PATH</div>
1877
+ <input id="org-copy-dest" class="filter-input" placeholder="Destination project path…" style="width:100%;box-sizing:border-box"
1878
+ oninput="document.querySelectorAll('#org-copy-proj-list .ocp-row').forEach(r=>r.classList.remove('ocp-sel'))"
1879
+ onkeydown="if(event.key==='Enter')v2DoCopyOrg();if(event.key==='Escape')document.getElementById('org-copy-dialog').style.display='none'">
1880
+ <div id="org-copy-status" style="font-size:11px;min-height:16px"></div>
1848
1881
  <div style="display:flex;gap:8px;justify-content:flex-end">
1849
1882
  <button class="btn" onclick="document.getElementById('org-copy-dialog').style.display='none'">Cancel</button>
1850
- <button class="btn" onclick="v2DoCopyOrg()" style="color:var(--accent);border-color:var(--accent)">Copy</button>
1883
+ <button class="btn" id="org-copy-confirm-btn" onclick="v2DoCopyOrg()" style="color:var(--accent);border-color:var(--accent)">Copy</button>
1851
1884
  </div>
1852
1885
  </div>
1853
1886
  </div>
1854
- <!-- agent detail drawer (node-click / role-card-click) -->
1855
- <div id="org-copy-dialog" style="display:none;position:absolute;inset:0;background:rgba(0,0,0,0.55);z-index:90;align-items:center;justify-content:center">
1856
- <div style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;width:320px;display:flex;flex-direction:column;gap:14px">
1857
- <div style="font-size:13px;font-weight:600;color:var(--text-hi)">Copy org to project</div>
1858
- <input id="org-copy-dest" class="filter-input" placeholder="Destination project path…" style="width:100%">
1859
- <div style="display:flex;gap:8px;justify-content:flex-end">
1860
- <button class="btn" onclick="document.getElementById('org-copy-dialog').style.display='none'">Cancel</button>
1861
- <button class="btn" onclick="v2DoCopyOrg()" style="color:var(--accent);border-color:var(--accent)">Copy</button>
1862
- </div>
1863
- </div>
1864
- </div>
1887
+ <div id="org-import-input-wrap" style="display:none"><input type="file" id="org-import-file" accept=".json" style="display:none"></div>
1865
1888
  <div id="org-agent-drawer" aria-hidden="true">
1866
1889
  <div id="oad-head"></div>
1867
1890
  <div id="oad-body"></div>
@@ -2100,9 +2123,13 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
2100
2123
  <select id="chat-v-sel" onchange="chatVSelectSession(this.value)">
2101
2124
  <option value="">— select a session —</option>
2102
2125
  </select>
2126
+ <select id="chat-v-org-run-sel" style="display:none" onchange="chatVSelectOrgRun(this.value)" title="Select org run to replay communications">
2127
+ <option value="">— org run history —</option>
2128
+ </select>
2103
2129
  <div id="chat-v-live-dot"></div>
2104
2130
  <span id="chat-v-live-lbl">OFFLINE</span>
2105
2131
  </div>
2132
+ <div id="chat-v-excerpt" class="cv-excerpt-banner"></div>
2106
2133
  <div id="chat-v-feed">
2107
2134
  <div id="chat-v-empty">Select a session above to see agent communications.<br>Agent messages, intercom signals, and system events appear here in real time.</div>
2108
2135
  </div>
@@ -2241,23 +2268,66 @@ document.getElementById('feed-scroll').addEventListener('scroll', () => {
2241
2268
  userScrolled = document.getElementById('feed-scroll').scrollTop > 50;
2242
2269
  });
2243
2270
 
2244
- function switchView(v) {
2271
+ function switchView(v, { updateHash = true } = {}) {
2245
2272
  currentView = v;
2246
2273
  document.querySelectorAll('.nav-item[data-view]').forEach(el =>
2247
2274
  el.classList.toggle('active', el.dataset.view === v));
2248
2275
  document.querySelectorAll('.view').forEach(el =>
2249
2276
  el.classList.toggle('active', el.id === 'view-' + v));
2250
- const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', tokens:'Tokens', memory:'Memory', orgs:'Orgs', monograph:'Monograph', global:'Global Feed', 'global-loops':'Global Loops', 'global-tokens':'Global Tokens', chat:'Agent Chat' };
2277
+ const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', tokens:'Tokens', memory:'Memory', orgs:'Orgs', monograph:'Monograph', global:'Global Feed', 'global-loops':'Global Loops', 'global-tokens':'Global Tokens', chat:'Global Agent Chat' };
2251
2278
  document.getElementById('view-title').textContent = titles[v] || v;
2252
2279
  const PROJECT = DIR ? shortPath(DIR) : 'monomind';
2253
2280
  const VIEW_LABELS = { now: 'Now', sessions: 'Sessions', projects: 'Projects', loops: 'Loops', tokens: 'Tokens', memory: 'Memory', orgs: 'Orgs', monograph: 'Monograph', global: 'Global Feed', 'global-loops': 'Global Loops', 'global-tokens': 'Global Tokens' };
2254
2281
  document.title = `monomind · ${PROJECT} · ${VIEW_LABELS[v] || v}`;
2282
+ if (updateHash) _pushHash();
2255
2283
  // Projects always re-fetches so onclick paths in cards stay current
2256
2284
  if (v === 'projects') { renderProjects(); return; }
2257
2285
  if (v === 'chat') { initChatView(); return; }
2258
2286
  if (!viewRendered[v]) { renderView(v); viewRendered[v] = true; }
2259
2287
  }
2260
2288
 
2289
+ // ── Hash-based routing ────────────────────────────────────────────────────────
2290
+ // Format: #<view> or #orgs/<orgName>/<tab>
2291
+ let _suppressHashChange = false;
2292
+
2293
+ function _pushHash() {
2294
+ let hash = currentView || 'now';
2295
+ if (currentView === 'orgs' && typeof _v2SelOrg !== 'undefined' && _v2SelOrg) {
2296
+ const tab = (typeof _v2OrgTab !== 'undefined' && _v2OrgTab) ? _v2OrgTab : 'chart';
2297
+ hash = 'orgs/' + encodeURIComponent(_v2SelOrg) + '/' + tab;
2298
+ }
2299
+ _suppressHashChange = true;
2300
+ location.hash = '#' + hash;
2301
+ setTimeout(() => { _suppressHashChange = false; }, 50);
2302
+ }
2303
+
2304
+ function _restoreFromHash() {
2305
+ if (_suppressHashChange) return;
2306
+ const raw = location.hash.replace(/^#/, '');
2307
+ if (!raw) { _pushHash(); return; }
2308
+ const parts = raw.split('/');
2309
+ const view = parts[0];
2310
+ if (!view) return;
2311
+ if (view === 'orgs') {
2312
+ switchView('orgs', { updateHash: false });
2313
+ const orgName = parts[1] ? decodeURIComponent(parts[1]) : null;
2314
+ const tab = parts[2] || 'chart';
2315
+ if (orgName) {
2316
+ // _v2Orgs is populated async by renderOrgs(); retry until available
2317
+ const _trySelect = (attempts) => {
2318
+ if (typeof _v2Orgs !== 'undefined' && _v2Orgs && _v2Orgs.length > 0) {
2319
+ v2SelectOrg(orgName).then(() => { if (tab !== 'chart') v2SwitchOrgTab(tab); });
2320
+ } else if (attempts > 0) {
2321
+ setTimeout(() => _trySelect(attempts - 1), 250);
2322
+ }
2323
+ };
2324
+ _trySelect(20);
2325
+ }
2326
+ } else {
2327
+ switchView(view, { updateHash: false });
2328
+ }
2329
+ }
2330
+
2261
2331
  function renderView(v) {
2262
2332
  if (v === 'now') { refreshNow(); return; }
2263
2333
  if (v === 'projects') renderProjects();
@@ -2327,6 +2397,9 @@ async function init() {
2327
2397
  await refreshNow();
2328
2398
  if (sessParam) setTimeout(() => jumpToSession(sessParam), 300);
2329
2399
  initSSE();
2400
+ // Restore view from URL hash (after initial data load so orgs list is ready)
2401
+ _restoreFromHash();
2402
+ window.addEventListener('hashchange', () => _restoreFromHash());
2330
2403
  }
2331
2404
 
2332
2405
  function _setLiveMode(mode) {
@@ -3894,42 +3967,190 @@ async function renderGlobalFeed() {
3894
3967
  }
3895
3968
 
3896
3969
  // ── agent chat view ────────────────────────────────────────
3897
- let chatVSessions = {};
3970
+ let chatVSessions = {}; // sessionId or groupId → session/group object
3971
+ let chatVGroupMap = {}; // groupId → group object
3972
+ let chatVSessionGroupMap = {}; // sessionId → groupId (for sessions inside a group)
3898
3973
  let chatVCurrentId = null;
3974
+ let chatVCurrentGroupSessions = new Set(); // session IDs belonging to the currently selected group
3899
3975
  let chatVSseSource = null;
3976
+ let chatVSeenKeys = new Set();
3900
3977
 
3901
3978
  function initChatView() {
3902
3979
  loadChatViewSessions();
3980
+ loadChatViewOrgRuns();
3903
3981
  if (!chatVSseSource) connectChatViewSSE();
3904
3982
  }
3905
3983
 
3984
+ async function loadChatViewOrgRuns() {
3985
+ // Populate the org run history selector with recent org runs
3986
+ const runSel = document.getElementById('chat-v-org-run-sel');
3987
+ if (!runSel) return;
3988
+ try {
3989
+ const dir = DIR ? `?dir=${encodeURIComponent(DIR)}` : '';
3990
+ const orgs = await apiFetch('/api/orgs' + dir).catch(() => []);
3991
+ const orgList = Array.isArray(orgs) ? orgs : (orgs.orgs || []);
3992
+ const runEntries = [];
3993
+ await Promise.all(orgList.slice(0, 10).map(async org => {
3994
+ const name = org.name || org;
3995
+ if (!name) return;
3996
+ try {
3997
+ const runs = await apiFetch(`/api/org/${encodeURIComponent(name)}/runs${dir}`);
3998
+ (Array.isArray(runs) ? runs : (runs.runs || [])).slice(0, 5).forEach(r => {
3999
+ runEntries.push({ org: name, runId: r.runId || r.id, startedAt: r.startedAt || 0 });
4000
+ });
4001
+ } catch (_) {}
4002
+ }));
4003
+ runEntries.sort((a, b) => b.startedAt - a.startedAt);
4004
+ while (runSel.options.length > 1) runSel.remove(1);
4005
+ runEntries.slice(0, 20).forEach(r => {
4006
+ const opt = document.createElement('option');
4007
+ opt.value = JSON.stringify({ org: r.org, runId: r.runId });
4008
+ const ts = r.startedAt ? new Date(r.startedAt).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : '';
4009
+ opt.textContent = r.org + '/' + (r.runId || '').slice(0, 12) + (ts ? ' ' + ts : '');
4010
+ runSel.appendChild(opt);
4011
+ });
4012
+ runSel.style.display = runEntries.length ? '' : 'none';
4013
+ } catch(e) { console.warn('chat org runs load failed', e); }
4014
+ }
4015
+
4016
+ async function chatVSelectOrgRun(value) {
4017
+ if (!value) return;
4018
+ let org, runId;
4019
+ try { ({ org, runId } = JSON.parse(value)); } catch { return; }
4020
+ const feed = document.getElementById('chat-v-feed');
4021
+ const empty = document.getElementById('chat-v-empty');
4022
+ if (!feed) return;
4023
+ feed.innerHTML = '';
4024
+ // Clear session selection — org run replay is a different mode;
4025
+ // also set chatVCurrentId=null so appendChatViewEvent session-guard doesn't filter events
4026
+ chatVCurrentId = null;
4027
+ const sessEl = document.getElementById('chat-v-sel');
4028
+ if (sessEl) sessEl.value = '';
4029
+ try {
4030
+ const dir = DIR ? `?dir=${encodeURIComponent(DIR)}` : '';
4031
+ const events = await apiFetch(`/api/org/${encodeURIComponent(org)}/runs/${encodeURIComponent(runId)}${dir}`);
4032
+ const evArr = Array.isArray(events) ? events : (events.events || []);
4033
+ if (!evArr.length) { feed.appendChild(empty || document.createTextNode('No events in this run.')); return; }
4034
+ evArr.forEach(ev => appendChatViewEvent(ev, false));
4035
+ feed.scrollTop = feed.scrollHeight;
4036
+ } catch(e) {
4037
+ feed.textContent = 'Failed to load run events: ' + e.message;
4038
+ }
4039
+ }
4040
+
3906
4041
  async function loadChatViewSessions() {
3907
4042
  try {
4043
+ // Snapshot SSE-seeded sessions before the async fetch — the API response may not yet
4044
+ // include events that arrived via SSE right before the fetch, so we preserve their
4045
+ // buffered events and merge them back after (same pattern as _odtLoadChatSessions).
4046
+ const _prevChatSessions = { ...chatVSessions };
3908
4047
  const data = await apiFetch('/api/mastermind/sessions');
3909
4048
  chatVSessions = {};
4049
+ chatVGroupMap = {};
4050
+ chatVSessionGroupMap = {};
3910
4051
  const sel = document.getElementById('chat-v-sel');
3911
4052
  const prev = sel.value;
3912
4053
  while (sel.options.length > 1) sel.remove(1);
3913
- const sessions = Object.values(data.sessions || {});
3914
- sessions.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
4054
+ // API returns a plain array (not {sessions:{...}}) — handle both shapes for safety
4055
+ const sessions = Array.isArray(data) ? data : Object.values(data.sessions || data || {});
4056
+
4057
+ // Normalize and merge SSE-seeded events before grouping
3915
4058
  sessions.forEach(s => {
4059
+ if (_prevChatSessions[s.id] && (_prevChatSessions[s.id].events || []).length > (s.events || []).length) {
4060
+ s.events = _prevChatSessions[s.id].events;
4061
+ }
4062
+ if (s.status && s.status !== 'running' && s.status !== 'complete') s.status = 'complete';
4063
+ });
4064
+
4065
+ // Group consecutive sessions with same normalized prompt + gap < 20 min → loop group
4066
+ const _CV_MAX_LOOP_GAP = 20 * 60 * 1000;
4067
+ const _cvNormPrompt = p => (p || '').replace(/^running org:\s*/i, '').toLowerCase().replace(/\s*rep\s+\d+.*$/i, '').trim().slice(0, 40);
4068
+ const sessAsc = [...sessions].sort((a, b) => (a.startedAt || a.ts || 0) - (b.startedAt || b.ts || 0));
4069
+ const cvGroups = []; // [{id, sessions, events, totalEvents, status, prompt, startedAt, _newestAt, _normKey, _isGroup}]
4070
+ sessAsc.forEach(s => {
4071
+ const key = _cvNormPrompt(s.prompt);
4072
+ const sAt = s.startedAt || s.ts || 0;
4073
+ const last = cvGroups[cvGroups.length - 1];
4074
+ if (last && last._normKey === key && key && (sAt - last._newestAt) < _CV_MAX_LOOP_GAP) {
4075
+ last.sessions.push(s);
4076
+ last._newestAt = sAt;
4077
+ last.totalEvents += (s.events || []).length;
4078
+ if (s.status === 'running') last.status = 'running';
4079
+ (s.events || []).forEach(ev => last.events.push(ev));
4080
+ } else {
4081
+ cvGroups.push({
4082
+ id: s.id,
4083
+ sessions: [s],
4084
+ _normKey: key,
4085
+ _newestAt: sAt,
4086
+ totalEvents: (s.events || []).length,
4087
+ status: s.status || 'complete',
4088
+ prompt: s.prompt,
4089
+ startedAt: sAt,
4090
+ domains: s.domains || [],
4091
+ events: [...(s.events || [])],
4092
+ _isGroup: false
4093
+ });
4094
+ }
4095
+ });
4096
+ // Finalize groups
4097
+ cvGroups.forEach(g => {
4098
+ if (g.sessions.length > 1) {
4099
+ g._isGroup = true;
4100
+ g.id = 'grp:' + g.sessions[0].id;
4101
+ g.events.sort((a, b) => (a.ts || 0) - (b.ts || 0));
4102
+ }
4103
+ });
4104
+ // Sort groups newest first, register
4105
+ cvGroups.sort((a, b) => b._newestAt - a._newestAt);
4106
+ cvGroups.forEach(g => {
4107
+ chatVSessions[g.id] = g;
4108
+ chatVGroupMap[g.id] = g;
4109
+ g.sessions.forEach(s => {
4110
+ chatVSessions[s.id] = g; // session ID also routes to group
4111
+ if (g._isGroup) chatVSessionGroupMap[s.id] = g.id;
4112
+ });
3916
4113
  const opt = document.createElement('option');
3917
- opt.value = s.id;
3918
- const ts = s.startedAt ? new Date(s.startedAt).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : '';
3919
- opt.textContent = (s.id.slice(0,16)) + (ts ? ' ' + ts : '') + (s.status === 'running' ? ' ●' : '');
4114
+ opt.value = g.id;
4115
+ const ts = g._newestAt ? new Date(g._newestAt).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : '';
4116
+ const label = (g.prompt || '').replace(/^running org:\s*/i, '').replace(/\s*rep\s+\d+.*$/i, '').trim().slice(0, 30) || g.id.slice(0, 16);
4117
+ const evCount = g.totalEvents || (g.events || []).length;
4118
+ const msgPart = evCount ? ' ' + evCount + 'ev' : '';
4119
+ const repPart = g._isGroup ? ' ' + g.sessions.length + 'reps' : '';
4120
+ opt.textContent = label + (ts ? ' ' + ts : '') + repPart + msgPart + (g.status === 'running' ? ' ●' : '');
3920
4121
  sel.appendChild(opt);
3921
- chatVSessions[s.id] = s;
3922
4122
  });
3923
- if (prev && chatVSessions[prev]) { sel.value = prev; }
3924
- else {
3925
- const running = sessions.find(s => s.status === 'running');
3926
- if (running) { sel.value = running.id; chatVSelectSession(running.id); }
4123
+
4124
+ // Re-attach SSE-only sessions not yet in the API response (arrived during fetch)
4125
+ Object.values(_prevChatSessions).forEach(ps => {
4126
+ if (!chatVSessions[ps.id]) {
4127
+ chatVSessions[ps.id] = ps;
4128
+ const opt = document.createElement('option');
4129
+ opt.value = ps.id;
4130
+ const tsVal = ps.startedAt || 0;
4131
+ const ts = tsVal ? new Date(tsVal).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : '';
4132
+ const label = (ps.prompt || '').replace(/^running org:\s*/i, '').slice(0, 40) || ps.id.slice(0, 16);
4133
+ opt.textContent = label + (ts ? ' ' + ts : '') + ' ●';
4134
+ sel.appendChild(opt);
4135
+ }
4136
+ });
4137
+
4138
+ // Resolve prev selection: may be a sessionId inside a group now
4139
+ const prevResolved = prev ? (chatVSessionGroupMap[prev] || (chatVSessions[prev] ? prev : null)) : null;
4140
+ if (prevResolved && chatVSessions[prevResolved]) {
4141
+ sel.value = prevResolved;
4142
+ if (!chatVCurrentId) chatVSelectSession(prevResolved);
4143
+ } else {
4144
+ const runningGroup = cvGroups.find(g => g.status === 'running');
4145
+ if (runningGroup) { sel.value = runningGroup.id; chatVSelectSession(runningGroup.id); }
4146
+ else if (cvGroups.length) { sel.value = cvGroups[0].id; chatVSelectSession(cvGroups[0].id); }
3927
4147
  }
3928
4148
  } catch(e) { console.warn('chat sessions load failed', e); }
3929
4149
  }
3930
4150
 
3931
4151
  function chatVSelectSession(id) {
3932
4152
  chatVCurrentId = id;
4153
+ chatVCurrentGroupSessions.clear();
3933
4154
  const feed = document.getElementById('chat-v-feed');
3934
4155
  const empty = document.getElementById('chat-v-empty');
3935
4156
  if (!id || !chatVSessions[id]) {
@@ -3939,24 +4160,118 @@ function chatVSelectSession(id) {
3939
4160
  }
3940
4161
  feed.innerHTML = '';
3941
4162
  const session = chatVSessions[id];
4163
+ // Populate group session set so live SSE events from any rep appear in feed
4164
+ if (session._isGroup && session.sessions) {
4165
+ session.sessions.forEach(s => chatVCurrentGroupSessions.add(s.id));
4166
+ }
4167
+ // Inject session metadata header
4168
+ const tsVal = session.startedAt || session.ts || 0;
4169
+ const tsStr = tsVal ? new Date(tsVal).toLocaleString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}) : '—';
4170
+ const domains = (session.domains || []);
4171
+ const agents = domains.length ? domains.join(', ') : '—';
4172
+ const evCount = session._isGroup ? (session.totalEvents || (session.events || []).length) : (session.events || []).length;
4173
+ const agentNames = (() => {
4174
+ const names = new Set();
4175
+ (session.events || []).forEach(ev => {
4176
+ if (ev.agent) names.add(ev.agent);
4177
+ if (ev.from && ev.from !== 'system') names.add(ev.from);
4178
+ });
4179
+ return names.size ? [...names].join(', ') : agents;
4180
+ })();
4181
+ const prompt = (session.prompt || '').replace(/^running org:\s*/i, '').replace(/\s*rep\s+\d+.*$/i, '').trim().slice(0, 120) || id;
4182
+ const repInfo = session._isGroup ? ' · ' + session.sessions.length + ' reps' : '';
4183
+ const statusCls = session.status === 'running' ? 'color:oklch(65% 0.20 145)' : session.status === 'complete' ? 'color:var(--text-lo)' : 'color:oklch(65% 0.20 25)';
4184
+ // Compute cost + duration from events
4185
+ const _allEvs = session.events || [];
4186
+ const _totalCost = _allEvs.reduce((acc, ev) => acc + (Number(ev.cost_usd) || 0), 0);
4187
+ const _costStr = _totalCost > 0 ? '$' + _totalCost.toFixed(4) : '';
4188
+ const _startTs = session.startedAt || session.ts || 0;
4189
+ const _endTs = session.endedAt || (_allEvs[_allEvs.length-1]?.ts) || 0;
4190
+ const _durMs = _endTs > _startTs ? _endTs - _startTs : 0;
4191
+ const _durStr = _durMs > 60000 ? Math.round(_durMs/60000) + 'm' : _durMs > 0 ? Math.round(_durMs/1000) + 's' : '';
4192
+ const hdr = document.createElement('div');
4193
+ hdr.className = 'cv-session-hdr';
4194
+ hdr.innerHTML = `<div class="cv-shdr-prompt">${esc(prompt)}${repInfo ? '<span style="color:var(--text-lo);font-size:10px"> ' + esc(repInfo) + '</span>' : ''}</div>`+
4195
+ `<div class="cv-shdr-meta">`+
4196
+ `<span class="cv-shdr-id" title="${esc(id)}">${esc(id)}</span>`+
4197
+ `<span class="cv-shdr-sep">·</span>`+
4198
+ `<span style="${statusCls}">${esc(session.status || 'unknown')}</span>`+
4199
+ `<span class="cv-shdr-sep">·</span>`+
4200
+ `<span title="started">${esc(tsStr)}</span>`+
4201
+ (_durStr ? `<span class="cv-shdr-sep">·</span><span title="duration">${esc(_durStr)}</span>` : '')+
4202
+ `<span class="cv-shdr-sep">·</span>`+
4203
+ `<span title="agents">${esc(agentNames)}</span>`+
4204
+ `<span class="cv-shdr-sep">·</span>`+
4205
+ `<span title="event count">${evCount} events</span>`+
4206
+ (_costStr ? `<span class="cv-shdr-sep">·</span><span style="color:oklch(65% 0.18 50)" title="total cost">${esc(_costStr)}</span>` : '')+
4207
+ `</div>`;
4208
+ feed.appendChild(hdr);
3942
4209
  const events = session.events || [];
3943
- events.forEach(ev => appendChatViewEvent(ev, false));
4210
+ events.forEach(ev => {
4211
+ appendChatViewEvent(ev, false);
4212
+ // Pre-seed dedup set so SSE reconnect replays of historical events are swallowed
4213
+ const _cvhk = (ev?.ts||'')+'|'+(ev?.type||'')+'|'+(ev?.from||ev?.role||ev?.agent||'')+'|'+(ev?.session||ev?.runId||'')+'|'+(ev?.result||ev?.msg||ev?.message||ev?.summary||'').slice(0,20);
4214
+ chatVSeenKeys.add(_cvhk);
4215
+ });
4216
+ // Populate excerpt banner with last activity for the selected session
4217
+ const _cvExEl = document.getElementById('chat-v-excerpt');
4218
+ if (_cvExEl) { if (events.length) renderCvExcerptBanner('chat-v-excerpt', events, session); else _cvExEl.innerHTML = ''; }
3944
4219
  feed.scrollTop = feed.scrollHeight;
3945
4220
  }
3946
4221
 
3947
4222
  function appendChatViewEvent(ev, animate) {
3948
- if (chatVCurrentId && ev.session && ev.session !== chatVCurrentId) return;
4223
+ if (chatVCurrentId && ev.session && ev.session !== chatVCurrentId && !chatVCurrentGroupSessions.has(ev.session)) return;
3949
4224
  const feed = document.getElementById('chat-v-feed');
3950
4225
  if (!feed) return;
3951
4226
  const empty = document.getElementById('chat-v-empty');
3952
4227
  if (empty && feed.contains(empty)) feed.removeChild(empty);
3953
4228
 
3954
4229
  let el;
4230
+ const atBottom = !animate || (feed.scrollHeight - feed.scrollTop - feed.clientHeight < 80);
3955
4231
  const ts = ev.ts ? new Date(ev.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
3956
- if (ev.type === 'intercom') {
4232
+ if (ev.type === 'run:start') {
4233
+ el = mkCVSys('▶ Run started' + (ev.goal ? ': ' + esc(ev.goal.slice(0,80)) : '') + (ev.bossRole ? ' · boss: ' + esc(ev.bossRole) : ''), ts);
4234
+ } else if (ev.type === 'run:cycle:complete') {
4235
+ el = mkCVSys('◎ Cycle complete' + (ev.pending != null ? ' · ' + esc(String(ev.pending)) + ' tasks pending' : ''), ts);
4236
+ } else if (ev.type === 'intercom') {
3957
4237
  el = mkCVIntercom(ev.from, ev.to, ev.msg || '', ts);
3958
- } else if (ev.type === 'agent:message' || ev.type === 'agent:spawn') {
3959
- el = mkCVAgent(ev.agent || ev.name || '?', ev.msg || ev.message || ev.role || ev.type, ts, ev.type);
4238
+ } else if (ev.type === 'org:comms') {
4239
+ const _cvCommsFrom = ev.from || ev.role || ev.org || '?';
4240
+ // Directed messages (to set, not broadcast) → intercom bubble; broadcasts → agent bubble
4241
+ if (ev.to && ev.to !== 'all') el = mkCVIntercom(_cvCommsFrom, ev.to, ev.msg || '', ts);
4242
+ else el = mkCVAgent(_cvCommsFrom, ev.msg || '', ts, 'org:comms');
4243
+ } else if (ev.type === 'org:agent:online') {
4244
+ el = mkCVSys('[' + esc(ev.org || '') + '] ' + esc(ev.role || ev.title || '?') + ' online' + (ev.title && ev.role && ev.title !== ev.role ? ' (' + esc(ev.title) + ')' : ''), ts);
4245
+ } else if (ev.type === 'org:checkpoint') {
4246
+ el = mkCVSys('[checkpoint] ' + esc(ev.summary || ev.progress || ev.msg || ''), ts);
4247
+ } else if (ev.type === 'org:start') {
4248
+ el = mkCVSys('[org:start] ' + esc(ev.org || ''), ts);
4249
+ } else if (ev.type === 'run:complete') {
4250
+ el = mkCVSys('■ Run complete' + (ev.status ? ' [' + esc(ev.status) + ']' : ''), ts);
4251
+ } else if (ev.type === 'org:complete') {
4252
+ el = mkCVSys('■ Org complete' + (ev.org ? ' — ' + esc(ev.org) : ''), ts);
4253
+ } else if (ev.type === 'agent:result') {
4254
+ el = mkCVResult(ev.agent || ev.from || '?', ev.result || ev.msg || ev.message || ev.summary || '', ts);
4255
+ } else if (ev.type === 'agent:spawn') {
4256
+ const _spawnTo = ev.to || ev.agentType || '?';
4257
+ const _spawnTask = ev.task || ev.briefing || '';
4258
+ el = mkCVSpawn(ev.from || 'orchestrator', _spawnTo, _spawnTask, ts);
4259
+ } else if (ev.type === 'agent:complete') {
4260
+ // Rich completion event from capture-handler (replaces/supplements org:comms)
4261
+ const _acAgent = ev.agentType || ev.role || ev.from || '?';
4262
+ const _acResult = ev.result || '';
4263
+ const _acCost = ev.cost_usd != null ? ' $' + Number(ev.cost_usd).toFixed(4) : '';
4264
+ const _acTok = (ev.tokens_in || ev.tokens_out) ? ' · ' + ((ev.tokens_in||0)+(ev.tokens_out||0)).toLocaleString() + ' tok' : '';
4265
+ const _acTools = (ev.toolCalls || []).length ? ' · [' + ev.toolCalls.slice(0,5).map(t => esc(t)).join(', ') + ']' : '';
4266
+ el = mkCVResult(_acAgent + _acCost + _acTok + _acTools, _acResult, ts);
4267
+ } else if (ev.type === 'agent:usage') {
4268
+ // Token/cost accounting event — show as compact system line
4269
+ const _auAgent = ev.role || ev.agentType || '?';
4270
+ const _auCost = ev.cost_usd != null ? '$' + Number(ev.cost_usd).toFixed(4) : '';
4271
+ const _auTok = (ev.tokens_in || ev.tokens_out) ? ((ev.tokens_in||0)).toLocaleString() + '↑ ' + ((ev.tokens_out||0)).toLocaleString() + '↓' : '';
4272
+ el = mkCVSys('◈ ' + esc(_auAgent) + (_auTok ? ' · ' + _auTok : '') + (_auCost ? ' · ' + _auCost : ''), ts);
4273
+ } else if (ev.type === 'agent:message') {
4274
+ el = mkCVAgent(ev.agent || ev.name || '?', ev.msg || ev.message || '', ts, ev.type);
3960
4275
  } else if (ev.type === 'session:start') {
3961
4276
  el = mkCVSys('Session started' + (ev.prompt ? ': ' + esc(ev.prompt.slice(0,80)) : ''), ts);
3962
4277
  } else if (ev.type === 'session:complete') {
@@ -3966,23 +4281,150 @@ function appendChatViewEvent(ev, animate) {
3966
4281
  } else if (ev.type === 'domain:complete') {
3967
4282
  el = mkCVSys('✓ ' + esc(ev.domain || '') + (ev.status ? ' [' + esc(ev.status) + ']' : ''), ts);
3968
4283
  } else if (ev.type === 'loop:start') {
3969
- el = mkCVSys('Loop started: ' + esc(ev.command || ''), ts);
4284
+ el = mkCVSys('Loop: ' + esc(ev.command || '') + (ev.repeat ? ' ×' + esc(String(ev.repeat)) : ''), ts);
3970
4285
  if (currentView === 'loops') renderLoops();
3971
4286
  } else if (ev.type === 'loop:complete') {
3972
- el = mkCVSys('Loop complete: ' + esc(ev.command || '') + (ev.ranReps ? ' (' + ev.ranReps + ' runs)' : ''), ts);
4287
+ el = mkCVSys('Loop done: ' + esc(ev.command || '') + (ev.ranReps ? ' (' + esc(String(ev.ranReps)) + ' runs)' : ''), ts);
3973
4288
  if (currentView === 'loops') renderLoops();
3974
4289
  } else if (ev.type === 'loop:tick') {
3975
- el = mkCVSys('Loop tick: ' + esc(ev.command || ev.id || ''), ts);
4290
+ const _tickLbl = ev.rep != null
4291
+ ? 'rep ' + esc(String(ev.rep)) + ' done' + (ev.wait != null ? ' → next in ' + esc(String(ev.wait)) + 's' : '')
4292
+ : esc(ev.command || ev.loopId || ev.id || '');
4293
+ el = mkCVSys('◷ ' + _tickLbl, ts);
4294
+ el.dataset.evType = 'loop:tick';
4295
+ if (feed.lastElementChild?.dataset?.evType === 'loop:tick') feed.lastElementChild.remove();
4296
+ if (currentView === 'loops') renderLoops();
4297
+ } else if (ev.type === 'loop:hil' || ev.type === 'loop:hil:waiting') {
4298
+ el = mkCVSys('⚠ Loop HIL: ' + esc(ev.command || ev.loopId || ev.id || ''), ts);
3976
4299
  if (currentView === 'loops') renderLoops();
3977
- } else if (ev.type === 'loop:hil') {
3978
- el = mkCVSys(' Loop HIL: ' + esc(ev.command || ev.id || ''), ts);
4300
+ } else if (ev.type === 'loop:hil:resolved') {
4301
+ el = mkCVSys(' Loop HIL resolved: ' + esc(ev.loopId || ev.command || ''), ts);
3979
4302
  if (currentView === 'loops') renderLoops();
4303
+ } else if (ev.type === 'file:write') {
4304
+ el = mkCVFileCard(ev.name || ev.path || '?', ev.path || '', ev.size || 0, ts);
4305
+ } else if (ev.type === 'org:agent:offline') {
4306
+ el = mkCVSys('◌ ' + esc(ev.title || ev.role || '?') + ' offline' + (ev.org ? ' [' + esc(ev.org) + ']' : ''), ts);
4307
+ } else if (ev.type === 'org:error') {
4308
+ el = mkCVSys('⚠ Error: ' + esc(ev.msg || ev.error || ev.message || ''), ts);
4309
+ el.classList.add('cv-err');
3980
4310
  } else {
3981
4311
  el = mkCVSys(esc(ev.type || 'event'), ts);
3982
4312
  }
3983
4313
  if (animate) el.classList.add('cv-new');
3984
4314
  feed.appendChild(el);
3985
- feed.scrollTop = feed.scrollHeight;
4315
+ if (atBottom) feed.scrollTop = feed.scrollHeight;
4316
+ }
4317
+
4318
+ function _cvExcerptText(ev) {
4319
+ const t = ev.type || '';
4320
+ if (t === 'intercom' || t === 'org:comms') return (ev.from || ev.role || '?') + ' → ' + (ev.to || '?') + ': ' + (ev.msg || ev.message || '').slice(0, 80);
4321
+ if (t === 'agent:message') return (ev.agent || ev.name || '?') + ': ' + (ev.msg || ev.message || '').slice(0, 80);
4322
+ if (t === 'agent:result') return (ev.agent || ev.from || '?') + ': ' + (ev.result || ev.msg || ev.message || ev.summary || '').slice(0, 80);
4323
+ if (t === 'agent:spawn') return 'spawned ' + (ev.to || ev.agent || ev.role || '?') + (ev.task || ev.briefing ? ': ' + (ev.task || ev.briefing || '').slice(0, 50) : '');
4324
+ if (t === 'org:agent:online') return (ev.title || ev.role || '?') + ' online';
4325
+ if (t === 'org:agent:offline') return (ev.title || ev.role || '?') + ' offline';
4326
+ if (t === 'org:error') return 'error: ' + (ev.msg || ev.error || ev.message || '').slice(0, 80);
4327
+ if (t === 'file:write') return (ev.name || ev.path || '?') + (ev.size ? ' (' + (ev.size > 1048576 ? (ev.size/1048576).toFixed(1)+'MB' : ev.size > 1024 ? (ev.size/1024).toFixed(1)+'KB' : ev.size+'B') + ')' : '');
4328
+ if (t === 'org:checkpoint') return 'checkpoint: ' + (ev.summary || ev.progress || ev.msg || '').slice(0, 80) + (ev.pending_tasks != null ? ' · ' + ev.pending_tasks + ' pending' : '');
4329
+ if (t === 'run:complete') return 'run complete' + (ev.status ? ' [' + ev.status + ']' : '');
4330
+ if (t === 'run:cycle:complete') return 'cycle done' + (ev.pending != null ? ' · ' + ev.pending + ' pending' : '');
4331
+ if (t === 'org:start') return 'org started: ' + (ev.goal || '').slice(0, 80);
4332
+ if (t === 'org:complete') return 'org complete';
4333
+ if (t === 'session:start') return 'started: ' + (ev.prompt || '').replace(/^running org:\s*/i, '').slice(0, 80);
4334
+ if (t === 'session:complete') return 'completed' + (ev.status ? ' [' + ev.status + ']' : '');
4335
+ if (t === 'domain:dispatch') return '→ ' + (ev.domain || '') + (ev.cmd ? ': ' + ev.cmd.slice(0, 60) : '');
4336
+ if (t === 'domain:complete') return '✓ ' + (ev.domain || '');
4337
+ if (t === 'loop:tick') { const _r = ev.rep ?? ev.fromRep; return _r != null ? 'rep ' + _r + ' done' : (ev.command || ev.loopId || t); }
4338
+ if (t === 'loop:start') return 'loop: ' + (ev.command || '');
4339
+ if (t === 'loop:complete') return 'loop done: ' + (ev.command || '') + (ev.ranReps ? ' (' + ev.ranReps + ' runs)' : '');
4340
+ const _content = ev.result || ev.msg || ev.message || ev.error || ev.summary || '';
4341
+ if (_content) return _content.slice(0, 80);
4342
+ return t.slice(0, 60);
4343
+ }
4344
+
4345
+ function _cvExcerptTag(ev) {
4346
+ const t = ev.type || '';
4347
+ if (t === 'intercom' || t === 'org:comms') return 'IC';
4348
+ if (t === 'agent:message') return 'MSG';
4349
+ if (t === 'agent:result') return 'RESULT';
4350
+ if (t === 'agent:spawn' || t === 'org:agent:online') return 'NEW';
4351
+ if (t === 'org:agent:offline') return 'OFF';
4352
+ if (t === 'org:error') return 'ERR';
4353
+ if (t === 'org:checkpoint') return 'CHK';
4354
+ if (t === 'file:write') return 'FILE';
4355
+ if (t.startsWith('org:')) return 'ORG';
4356
+ if (t.startsWith('run:')) return 'RUN';
4357
+ if (t.startsWith('session:')) return 'SES';
4358
+ if (t.startsWith('domain:')) return 'DOM';
4359
+ if (t.startsWith('loop:')) return 'LOOP';
4360
+ if (t.startsWith('agent:')) return 'AGT';
4361
+ return 'EVT';
4362
+ }
4363
+
4364
+ function renderCvExcerptBanner(bannerId, events, sessionMeta) {
4365
+ const banner = document.getElementById(bannerId);
4366
+ if (!banner) return;
4367
+ const SKIP = new Set(['session:start', 'run:start', 'org:start']);
4368
+ const meaningful = (events || []).filter(ev => !SKIP.has(ev.type)).slice(-5).reverse();
4369
+ if (!meaningful.length) {
4370
+ const prompt = sessionMeta && (sessionMeta.prompt || '');
4371
+ const clean = prompt.replace(/^running org:\s*/i, '').slice(0, 120);
4372
+ if (!clean) { banner.classList.remove('visible'); banner.innerHTML = ''; return; }
4373
+ banner.innerHTML = `<div class="cv-excerpt-label">SESSION GOAL</div><div class="cv-excerpt-items"><div class="cv-excerpt-item"><span class="cv-ex-tag">ORG</span>${esc(clean)}</div></div>`;
4374
+ banner.classList.add('visible');
4375
+ return;
4376
+ }
4377
+ const items = meaningful.slice(0, 3).map(ev => {
4378
+ const tag = _cvExcerptTag(ev);
4379
+ const txt = _cvExcerptText(ev);
4380
+ return `<div class="cv-excerpt-item"><span class="cv-ex-tag">${tag}</span>${esc(txt)}</div>`;
4381
+ }).join('');
4382
+ banner.innerHTML = `<div class="cv-excerpt-label">LAST ACTIVITY</div><div class="cv-excerpt-items">${items}</div>`;
4383
+ banner.classList.add('visible');
4384
+ }
4385
+
4386
+ function mkCVSpawn(from, to, task, ts) {
4387
+ const d = document.createElement('div');
4388
+ d.className = 'cv-msg cv-spawn';
4389
+ const short = task.replace(/\n/g,' ').slice(0,140);
4390
+ d.innerHTML = `<div class="cv-bub" style="border-left:2px solid oklch(65% 0.18 280)">`+
4391
+ `<span class="cv-tag" style="background:oklch(25% 0.10 280);color:oklch(80% 0.18 280)">${esc(from)}</span>`+
4392
+ `<span class="cv-arrow" style="color:oklch(65% 0.18 280)">→</span>`+
4393
+ `<span class="cv-tag" style="background:oklch(25% 0.15 160);color:oklch(75% 0.20 160)">${esc(to)}</span>`+
4394
+ `<span class="cv-etype" style="color:oklch(65% 0.18 280)">SPAWN</span>`+
4395
+ `<span class="cv-text">${esc(short)}${task.length>140?'…':''}</span>`+
4396
+ `<span class="cv-ts">${ts}</span></div>`;
4397
+ return d;
4398
+ }
4399
+
4400
+ function mkCVResult(agent, text, ts) {
4401
+ const d = document.createElement('div');
4402
+ d.className = 'cv-msg cv-result';
4403
+ const isLong = text.length > 300;
4404
+ const preview = isLong ? text.slice(0, 300) + '…' : text;
4405
+ const uid = 'res-' + Math.random().toString(36).slice(2,8);
4406
+ d.innerHTML = `<div class="cv-bub" style="border-left:2px solid oklch(68% 0.20 150)">`+
4407
+ `<span class="cv-tag" style="background:oklch(20% 0.10 150);color:oklch(72% 0.20 150)">${esc(agent)}</span>`+
4408
+ `<span class="cv-etype" style="color:oklch(68% 0.20 150)">REPORT</span>`+
4409
+ `<span class="cv-text" id="${uid}" style="white-space:pre-wrap;line-height:1.6">${esc(preview)}</span>`+
4410
+ (isLong ? `<button class="btn" style="font-size:9px;padding:1px 6px;margin-left:6px;flex-shrink:0" data-uid="${uid}" data-full="${esc(text)}" onclick="(function(b){const el=document.getElementById(b.dataset.uid);const full=b.dataset.full;const show=el.dataset.expanded;el.textContent=show?full.slice(0,300)+'…':full;el.dataset.expanded=show?'':'1';b.textContent=show?'Expand':'Collapse';})(this)">Expand</button>` : '')+
4411
+ `<span class="cv-ts">${ts}</span></div>`;
4412
+ return d;
4413
+ }
4414
+
4415
+ function mkCVFileCard(name, filePath, size, ts) {
4416
+ const d = document.createElement('div');
4417
+ d.className = 'cv-msg cv-file';
4418
+ const sizeStr = size > 1024*1024 ? (size/1024/1024).toFixed(1)+'MB' : size > 1024 ? (size/1024).toFixed(1)+'KB' : (size||'?')+'B';
4419
+ const isMd = /\.md$/i.test(name);
4420
+ d.innerHTML = `<div class="cv-bub" style="border-left:2px solid oklch(65% 0.18 50);align-items:center;gap:8px">`+
4421
+ `<span style="font-size:14px">${isMd?'📄':'📁'}</span>`+
4422
+ `<span class="cv-tag" style="background:oklch(22% 0.10 50);color:oklch(78% 0.18 50)">FILE</span>`+
4423
+ `<span class="cv-text" style="font-family:var(--mono);font-size:10px">${esc(name)}</span>`+
4424
+ `<span style="font-size:9px;color:var(--text-lo);flex-shrink:0">${sizeStr}</span>`+
4425
+ (filePath ? `<button class="btn" style="font-size:9px;padding:1px 6px;flex-shrink:0;border-color:oklch(65% 0.18 50);color:oklch(65% 0.18 50)" data-fp="${esc(filePath)}" data-fn="${esc(name)}" onclick="v2ViewOrgFile(this.dataset.fp,this.dataset.fn)">View</button>` : '')+
4426
+ `<span class="cv-ts">${ts}</span></div>`;
4427
+ return d;
3986
4428
  }
3987
4429
 
3988
4430
  function mkCVSys(html, ts) {
@@ -3996,14 +4438,14 @@ function mkCVAgent(name, text, ts, typeTag) {
3996
4438
  const d = document.createElement('div');
3997
4439
  d.className = 'cv-msg cv-agent';
3998
4440
  const tag = typeTag === 'agent:spawn' ? 'SPAWN' : 'MSG';
3999
- d.innerHTML = `<div class="cv-bub"><span class="cv-tag">${esc(name)}</span><span class="cv-etype">${tag}</span><span class="cv-text">${esc(String(text).slice(0,200))}</span><span class="cv-ts">${ts}</span></div>`;
4441
+ d.innerHTML = `<div class="cv-bub"><span class="cv-tag">${esc(name)}</span><span class="cv-etype">${tag}</span><span class="cv-text">${esc(String(text).slice(0,800))}</span><span class="cv-ts">${ts}</span></div>`;
4000
4442
  return d;
4001
4443
  }
4002
4444
 
4003
4445
  function mkCVIntercom(from, to, text, ts) {
4004
4446
  const d = document.createElement('div');
4005
4447
  d.className = 'cv-msg cv-ic';
4006
- d.innerHTML = `<div class="cv-bub"><span class="cv-tag cv-sender">${esc(from||'?')}</span><span class="cv-arrow">→</span><span class="cv-tag cv-receiver">${esc(to||'?')}</span><span class="cv-etype">IC</span><span class="cv-text">${esc(String(text).slice(0,200))}</span><span class="cv-ts">${ts}</span></div>`;
4448
+ d.innerHTML = `<div class="cv-bub"><span class="cv-tag cv-sender">${esc(from||'?')}</span><span class="cv-arrow">→</span><span class="cv-tag cv-receiver">${esc(to||'?')}</span><span class="cv-etype">IC</span><span class="cv-text">${esc(String(text).slice(0,800))}</span><span class="cv-ts">${ts}</span></div>`;
4007
4449
  return d;
4008
4450
  }
4009
4451
 
@@ -4019,24 +4461,69 @@ function connectChatViewSSE() {
4019
4461
  chatVSseSource.onerror = () => {
4020
4462
  dot && dot.classList.remove('on');
4021
4463
  lbl && (lbl.textContent = 'OFFLINE');
4022
- chatVSseSource.close();
4464
+ const _cvSrc = chatVSseSource;
4023
4465
  chatVSseSource = null;
4466
+ if (_cvSrc) _cvSrc.close();
4024
4467
  setTimeout(connectChatViewSSE, 5000);
4025
4468
  };
4026
4469
  }
4027
4470
 
4028
4471
  function handleChatViewEvent(ev) {
4029
- if (!ev || !ev.session) return;
4030
- if (!chatVSessions[ev.session]) {
4031
- chatVSessions[ev.session] = { id: ev.session, events: [], startedAt: ev.ts, status: 'running' };
4032
- loadChatViewSessions();
4033
- } else {
4034
- const s = chatVSessions[ev.session];
4035
- s.events = s.events || [];
4036
- s.events.push(ev);
4037
- if (ev.type === 'session:complete') s.status = 'complete';
4472
+ if (!ev) return;
4473
+ // Deduplicate SSE reconnect replays (server replays last 50 events on every reconnect)
4474
+ const dedupKey = (ev.ts||'') + '|' + (ev.type||'') + '|' + (ev.from||ev.role||ev.agent||'') + '|' + (ev.session||ev.runId||'') + '|' + (ev.result||ev.msg||ev.message||ev.summary||'').slice(0,20);
4475
+ if (chatVSeenKeys.has(dedupKey)) return;
4476
+ chatVSeenKeys.add(dedupKey);
4477
+ if (chatVSeenKeys.size > 2000) { const _a = [...chatVSeenKeys]; chatVSeenKeys = new Set(_a.slice(1000)); }
4478
+ // Org events (org:comms, org:agent:online, org:checkpoint, etc.) may carry a session field
4479
+ // from the org run's session ID — route them directly to the feed if the session matches,
4480
+ // or append them unconditionally when the chat view is showing the "live" placeholder.
4481
+ const isOrgEvent = ev.type && (ev.type.startsWith('org:') || ev.type.startsWith('run:') || ev.type.startsWith('loop:'));
4482
+ if (!ev.session && !isOrgEvent) return;
4483
+ if (ev.session) {
4484
+ const existingEntry = chatVSessions[ev.session]; // may be a group object
4485
+ if (!existingEntry) {
4486
+ // Seed events with the triggering event so it isn't lost if loadChatViewSessions
4487
+ // runs before the event is flushed to disk (disk write and SSE broadcast race).
4488
+ chatVSessions[ev.session] = { id: ev.session, events: [ev], startedAt: ev.ts, status: 'running', prompt: ev.prompt || '' };
4489
+ loadChatViewSessions();
4490
+ // Auto-switch to new session on session:start — after loadChatViewSessions it may be grouped
4491
+ if (ev.type === 'session:start') {
4492
+ const _cvGroupId = chatVSessionGroupMap[ev.session] || ev.session;
4493
+ const _cvSel = document.getElementById('chat-v-sel');
4494
+ if (_cvSel) { _cvSel.value = _cvGroupId; chatVSelectSession(_cvGroupId); }
4495
+ }
4496
+ } else {
4497
+ // existingEntry may be a group — push event to it and update status
4498
+ const target = existingEntry;
4499
+ target.events = target.events || [];
4500
+ target.events.push(ev);
4501
+ if (ev.type === 'session:complete') {
4502
+ // Mark this individual session complete; group stays running until all sessions done
4503
+ if (!target._isGroup) target.status = 'complete';
4504
+ else {
4505
+ // Update the underlying session in group
4506
+ const sub = (target.sessions || []).find(s => s.id === ev.session);
4507
+ if (sub) sub.status = 'complete';
4508
+ // Group stays running if any session is still running
4509
+ if (!(target.sessions || []).some(s => s.status === 'running')) target.status = 'complete';
4510
+ }
4511
+ }
4512
+ if (target._isGroup) target.totalEvents = (target.totalEvents || 0) + 1;
4513
+ }
4514
+ // Show event in feed if this session (or its group) is currently selected
4515
+ const _cvGroupId = chatVSessionGroupMap[ev.session];
4516
+ const _cvIsSelected = chatVCurrentId === ev.session || (_cvGroupId && chatVCurrentId === _cvGroupId) || chatVCurrentGroupSessions.has(ev.session);
4517
+ if (_cvIsSelected) {
4518
+ appendChatViewEvent(ev, true);
4519
+ const _cvSess = chatVSessions[_cvGroupId || ev.session];
4520
+ if (_cvSess) renderCvExcerptBanner('chat-v-excerpt', _cvSess.events || [], _cvSess);
4521
+ }
4522
+ } else if (isOrgEvent) {
4523
+ // Org events without a Claude session ID — append to feed regardless of active view;
4524
+ // #chat-v-feed is in the DOM even when hidden, so events accumulate correctly.
4525
+ appendChatViewEvent(ev, true);
4038
4526
  }
4039
- if (chatVCurrentId === ev.session) appendChatViewEvent(ev, true);
4040
4527
  }
4041
4528
 
4042
4529
  // ── feature 4: budget cap + desktop notification ───────────
@@ -4685,7 +5172,7 @@ async function renderLoops() {
4685
5172
  el.innerHTML = loops.map((l, idx) => {
4686
5173
  const maxReps = l.maxReps || 0;
4687
5174
  const curRep = l.currentRep || 0;
4688
- const isTillend = !maxReps || l.loopType === 'tillend' || String(l.command || '').includes('--tillend');
5175
+ const isTillend = !maxReps || l.type === 'tillend' || String(l.command || '').includes('--tillend');
4689
5176
  const nextAt = l.nextRunAt ? parseInt(l.nextRunAt, 10) : 0;
4690
5177
  const isExplicitlyActive = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
4691
5178
  const isOverdue = !isExplicitlyActive && nextAt > 0 && nextAt <= Date.now();
@@ -4704,7 +5191,7 @@ async function renderLoops() {
4704
5191
  const runs = curRep != null ? curRep : '—';
4705
5192
  const pct = (maxReps > 0) ? Math.min(100, Math.round(curRep / maxReps * 100)) : 0;
4706
5193
  const progBar = isTillend
4707
- ? `<div class="lp-bar" title="tillend loop"><div class="lp-fill lp-fill-inf" style="width:100%;opacity:0.3;background:linear-gradient(90deg,var(--accent),transparent)"></div><span style="position:absolute;left:6px;top:0;font-size:9px;color:var(--text-lo)">run ${curRep} / ∞${l.capReps ? ' (cap: '+l.capReps+')' : ''}</span></div>`
5194
+ ? `<div class="lp-bar" title="tillend loop"><div class="lp-fill lp-fill-inf" style="width:100%;opacity:0.3;background:linear-gradient(90deg,var(--accent),transparent)"></div><span style="position:absolute;left:6px;top:0;font-size:9px;color:var(--text-lo)">run ${curRep} / ∞${maxReps ? ' (cap: '+maxReps+')' : ''}</span></div>`
4708
5195
  : (maxReps > 0 && running)
4709
5196
  ? `<div class="lp-bar"><div class="lp-fill" style="width:${pct}%"></div></div>`
4710
5197
  : '';
@@ -4734,8 +5221,8 @@ async function renderLoops() {
4734
5221
  ${stopBtn}
4735
5222
  </div>
4736
5223
  <div class="loop-expand">
4737
- ${fullPrompt ? `<div class="le-row"><div class="le-lbl">Prompt</div><div class="le-val mono" style="cursor:pointer" onclick="navigator.clipboard.writeText(${JSON.stringify(fullPrompt)}).then(()=>showToast('Copied','','ok'))">${esc(fullPrompt.slice(0, 300))}</div></div>` : ''}
4738
- ${command && command !== fullPrompt ? `<div class="le-row"><div class="le-lbl">Command</div><div class="le-val mono" style="cursor:pointer" onclick="navigator.clipboard.writeText(${JSON.stringify(command)}).then(()=>showToast('Copied','','ok'))">${esc(command.slice(0, 200))}</div></div>` : ''}
5224
+ ${fullPrompt ? `<div class="le-row"><div class="le-lbl">Prompt</div><div class="le-val mono" style="cursor:pointer" data-cp="${esc(fullPrompt)}" onclick="navigator.clipboard.writeText(this.dataset.cp).then(()=>showToast('Copied','','ok'))">${esc(fullPrompt.slice(0, 300))}</div></div>` : ''}
5225
+ ${command && command !== fullPrompt ? `<div class="le-row"><div class="le-lbl">Command</div><div class="le-val mono" style="cursor:pointer" data-cp="${esc(command)}" onclick="navigator.clipboard.writeText(this.dataset.cp).then(()=>showToast('Copied','','ok'))">${esc(command.slice(0, 200))}</div></div>` : ''}
4739
5226
  <div class="le-row"><div class="le-lbl">Type</div><div class="le-val">${isTillend ? '∞ tillend' : '↺ repeat'}</div></div>
4740
5227
  <div class="le-row"><div class="le-lbl">Interval</div><div class="le-val">${interval}</div></div>
4741
5228
  <div class="le-row"><div class="le-lbl">Status</div><div class="le-val">${isFinished ? '✓ done' : isHil ? '⚠ hil:pending' : running ? '● running' : '○ stopped'}</div></div>
@@ -5112,7 +5599,9 @@ function v2RenderOrgList() {
5112
5599
 
5113
5600
  async function v2SelectOrg(name) {
5114
5601
  _v2SelOrg = name;
5602
+ _pushHash();
5115
5603
  if (typeof v2CloseAgent === 'function') v2CloseAgent();
5604
+ if (_orgLiveInterval) { clearInterval(_orgLiveInterval); _orgLiveInterval = null; }
5116
5605
  const requested = name;
5117
5606
  const requestedDir = DIR;
5118
5607
  document.querySelectorAll('.org-item').forEach(el => {
@@ -5147,11 +5636,14 @@ async function v2SelectOrg(name) {
5147
5636
  _v2OrgData = mainR.config ? { ...mainR, ...mainR.config, running: mainR.running } : mainR;
5148
5637
  _v2OrgData._activity = Array.isArray(actR) ? actR : [];
5149
5638
  _v2OrgData._health = healthR;
5150
- // Fetch supplemental data for new tabs
5151
- const agentsR = await fetch(`/api/org/${_enc}/agents${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
5152
- const budgetsR = await fetch(`/api/org/${_enc}/budgets${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r=>r.ok?r.json():null).catch(()=>null);
5153
- const membersR = await fetch(`/api/org/${_enc}/members${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
5154
- const issuesR = await fetch(`/api/org/${_enc}/issues${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
5639
+ // Fetch supplemental data for new tabs in parallel
5640
+ const [agentsR, budgetsR, membersR, issuesR] = await Promise.all([
5641
+ fetch(`/api/org/${_enc}/agents${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r=>r.ok?r.json():[]).catch(()=>[]),
5642
+ fetch(`/api/org/${_enc}/budgets${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r=>r.ok?r.json():null).catch(()=>null),
5643
+ fetch(`/api/org/${_enc}/members${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r=>r.ok?r.json():[]).catch(()=>[]),
5644
+ fetch(`/api/org/${_enc}/issues${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r=>r.ok?r.json():[]).catch(()=>[]),
5645
+ ]);
5646
+ if (_v2SelOrg !== requested || DIR !== requestedDir) return;
5155
5647
  _v2OrgData._agents = Array.isArray(agentsR) ? agentsR : (agentsR?.agents || []);
5156
5648
  _v2OrgData._budgets = budgetsR;
5157
5649
  _v2OrgData._members = Array.isArray(membersR) ? membersR : (membersR?.members || []);
@@ -5190,6 +5682,7 @@ window.v2SwitchOrgTab = function(tab) {
5190
5682
  });
5191
5683
  document.querySelectorAll('.odt-pane').forEach(p => p.classList.remove('active'));
5192
5684
  document.getElementById('odt-' + tab)?.classList.add('active');
5685
+ _pushHash();
5193
5686
  v2RenderOrgTab(tab);
5194
5687
  };
5195
5688
 
@@ -5228,11 +5721,15 @@ function v2RenderOrgTab(tab) {
5228
5721
  else if (tab === 'issues-full') v2RenderOrgIssuesFull();
5229
5722
  else if (tab === 'join-requests') v2RenderOrgJoinRequests();
5230
5723
  else if (tab === 'threads') v2RenderOrgThreads();
5724
+ else if (tab === 'chat') v2RenderOrgChat();
5725
+ else if (tab === 'config') v2RenderOrgConfig();
5726
+ else if (tab === 'files') v2RenderOrgFiles();
5231
5727
  }
5232
5728
 
5233
5729
  // ── org chart ───────────────────────────────
5234
5730
  function _v2OrgIsLeader(r) {
5235
- return r.id === 'boss' || r.type === 'coordinator' || r.type === 'planner';
5731
+ return r.isBoss === true || r.id === 'boss' || r.type === 'coordinator' || r.type === 'planner'
5732
+ || r.agent_type === 'coordinator' || r.agent_type === 'devbot-orchestrator';
5236
5733
  }
5237
5734
 
5238
5735
  function v2RenderOrgChart() {
@@ -5420,22 +5917,27 @@ function v2RenderOrgChart() {
5420
5917
  `<clipPath id="${nodeClipIds[i]}"><circle r="${Math.round(R * 0.52)}"/></clipPath>`
5421
5918
  ).join('');
5422
5919
 
5920
+ // Overlay live running status from in-memory agent SSE state for chart node indicators
5921
+ const _chartRunning = new Set((_v2OrgData?._agents || []).filter(a => a.status === 'running').map(a => a.id));
5423
5922
  let nodesHTML = '';
5424
5923
  roles.forEach((role, i) => {
5425
5924
  const p = pos[role.id];
5426
5925
  if (!p) return;
5427
5926
  const leader = _v2OrgIsLeader(role);
5428
5927
  const color = colorOf(role, i);
5928
+ const isRunning = _chartRunning.has(role.id);
5429
5929
  const displayName = role.name || role.title || role.id;
5430
5930
  const subType = role.type || role.agent_type || '';
5431
5931
  const MAX_LBL = Math.floor(R / 4.2);
5432
5932
  const nameText = displayName.length > MAX_LBL ? displayName.slice(0, MAX_LBL - 1) + '…' : displayName;
5433
5933
  const subTypeText = subType.length > MAX_LBL ? subType.slice(0, MAX_LBL - 1) + '…' : subType;
5434
5934
  const outerRing = leader ? `<circle r="${R + 9}" fill="none" stroke="${color}" stroke-width="0.6" opacity="0.18"/>` : '';
5935
+ const runningDot = isRunning ? `<circle class="v2-chart-running-dot" r="5.5" cx="${(R * 0.65).toFixed(1)}" cy="${(-R * 0.65).toFixed(1)}" fill="oklch(68% 0.20 150)" stroke="oklch(12% 0.008 55)" stroke-width="1.5" style="animation:blink 2s ease-in-out infinite"/>` : '';
5435
5936
  const nameY = subType ? (R * 0.55 + 2).toFixed(0) : (R * 0.55 + 8).toFixed(0);
5436
5937
  const avR = Math.round(R * 0.52);
5437
5938
  const avatarSrc = v2RoleAvatar(role);
5438
5939
  nodesHTML += `<g class="v2oc-node" data-role="${esc(role.id)}" onclick="v2OpenAgent('${String(role.id).replace(/['\\]/g,'\\$&')}')" transform="translate(${p.x.toFixed(1)},${p.y.toFixed(1)})">
5940
+ ${runningDot}
5439
5941
  <title>${esc(displayName)}${subType ? ` · ${esc(subType)}` : ''} — click for details</title>
5440
5942
  ${outerRing}
5441
5943
  <circle r="${R}" fill="oklch(12% 0.008 55)" stroke="${color}" stroke-width="${leader ? 2.5 : 1.8}" filter="url(#v2glow)"/>
@@ -5503,6 +6005,31 @@ function v2RenderOrgChart() {
5503
6005
  }
5504
6006
 
5505
6007
 
6008
+ // ── in-place running dot updater (avoids full re-render + GSAP flicker on SSE events) ──
6009
+ function v2UpdateChartRunningDots() {
6010
+ if (!_v2OrgData) return;
6011
+ const R = 42;
6012
+ const running = new Set((_v2OrgData._agents || []).filter(a => a.status === 'running').map(a => a.id));
6013
+ document.querySelectorAll('#v2oc-nodes .v2oc-node[data-role]').forEach(g => {
6014
+ const roleId = g.dataset.role;
6015
+ const existing = g.querySelector('.v2-chart-running-dot');
6016
+ if (running.has(roleId) && !existing) {
6017
+ const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
6018
+ c.setAttribute('class', 'v2-chart-running-dot');
6019
+ c.setAttribute('r', '5.5');
6020
+ c.setAttribute('cx', (R * 0.65).toFixed(1));
6021
+ c.setAttribute('cy', (-R * 0.65).toFixed(1));
6022
+ c.setAttribute('fill', 'oklch(68% 0.20 150)');
6023
+ c.setAttribute('stroke', 'oklch(12% 0.008 55)');
6024
+ c.setAttribute('stroke-width', '1.5');
6025
+ c.style.animation = 'blink 2s ease-in-out infinite';
6026
+ g.insertBefore(c, g.firstChild);
6027
+ } else if (!running.has(roleId) && existing) {
6028
+ existing.remove();
6029
+ }
6030
+ });
6031
+ }
6032
+
5506
6033
  // ── avatar lookup ────────────────────────────
5507
6034
  const _v2AvatarKnown = new Set([
5508
6035
  'coder','senior-developer','reviewer','tester','planner','researcher',
@@ -5570,9 +6097,12 @@ function v2RenderOrgRoles() {
5570
6097
  // Determine leader: explicit reports_to=undefined + type=planner/coordinator, or first role, or id=boss
5571
6098
  const leaderIds = new Set(roles.filter(r => r.id === 'boss' || r.type === 'coordinator' || r.type === 'planner').map(r => r.id));
5572
6099
  if (!leaderIds.size) leaderIds.add(roles[0]?.id);
6100
+ // Overlay live running status from in-memory agent SSE state
6101
+ const _rolesRunning = new Set((_v2OrgData?._agents || []).filter(a => a.status === 'running').map(a => a.id));
5573
6102
  pane.innerHTML = `<div class="roles-v2-grid">${roles.map((r, i) => {
5574
6103
  const color = V2_ROLE_COLORS[i % V2_ROLE_COLORS.length];
5575
6104
  const isLeader = leaderIds.has(r.id);
6105
+ const isRunning = _rolesRunning.has(r.id);
5576
6106
  const resps = Array.isArray(r.responsibilities) ? r.responsibilities : [];
5577
6107
  const displayName = r.name || r.title || r.id;
5578
6108
  const roleType = r.type || r.agent_type || '';
@@ -5581,7 +6111,7 @@ function v2RenderOrgRoles() {
5581
6111
  <div class="rv2-head">
5582
6112
  <img class="rv2-avatar" src="${avatarSrc}" alt="${esc(displayName)}" loading="lazy" onerror="this.src='data/avatars/coder.svg'"/>
5583
6113
  <div class="rv2-info">
5584
- <div class="rv2-title">${esc(displayName)}${isLeader ? ' <span style="font-size:10px;color:var(--accent);font-weight:400;opacity:0.8">lead</span>' : ''}</div>
6114
+ <div class="rv2-title">${esc(displayName)}${isLeader ? ' <span style="font-size:10px;color:var(--accent);font-weight:400;opacity:0.8">lead</span>' : ''}${isRunning ? ' <span class="live-dot" style="display:inline-block;vertical-align:middle;margin-left:4px"></span>' : ''}</div>
5585
6115
  ${roleType ? `<span class="rv2-agent">${esc(roleType)}</span>` : ''}
5586
6116
  </div>
5587
6117
  </div>
@@ -5609,16 +6139,18 @@ async function v2OpenAgent(roleId) {
5609
6139
  const bodyEl = document.getElementById('oad-body');
5610
6140
  const headEl = document.getElementById('oad-head');
5611
6141
  if (!drawer || !bodyEl || !headEl) return;
6142
+ const _agentOrg = _v2SelOrg, _agentRole = roleId;
5612
6143
  drawer.classList.add('open'); drawer.setAttribute('aria-hidden', 'false');
5613
6144
  v2HighlightAgentNode(roleId);
5614
6145
  headEl.innerHTML = '<button class="oad-close" onclick="v2CloseAgent()" aria-label="Close">✕</button>';
5615
6146
  bodyEl.innerHTML = '<div class="oad-loading">Loading agent…</div>';
5616
6147
  try {
5617
- const r = await fetch(`/api/org/${encodeURIComponent(_v2SelOrg)}/agent/${encodeURIComponent(roleId)}${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`);
6148
+ const r = await fetch(`/api/org/${encodeURIComponent(_agentOrg)}/agent/${encodeURIComponent(_agentRole)}${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`);
5618
6149
  if (!r.ok) throw new Error('not found');
6150
+ if (_v2SelOrg !== _agentOrg) return;
5619
6151
  v2RenderAgentDrawer(await r.json());
5620
6152
  } catch (_) {
5621
- bodyEl.innerHTML = '<div class="oad-loading">Could not load agent detail.</div>';
6153
+ if (_v2SelOrg === _agentOrg) bodyEl.innerHTML = '<div class="oad-loading">Could not load agent detail.</div>';
5622
6154
  }
5623
6155
  }
5624
6156
  function v2RenderAgentDrawer(data) {
@@ -5710,16 +6242,38 @@ function v2RenderMarkdown(md) {
5710
6242
  }
5711
6243
 
5712
6244
  // ── org activity ────────────────────────────
6245
+ function fmtOrgEvDetail(ev) {
6246
+ if (ev.type === 'org:comms' && (ev.from || ev.role || ev.to || ev.msg)) {
6247
+ const sender = ev.from || ev.role || '';
6248
+ const route = [sender, ev.to].filter(Boolean).join(' → ');
6249
+ return route ? route + (ev.msg ? ': ' + ev.msg : '') : ev.msg || '';
6250
+ }
6251
+ if (ev.type === 'org:checkpoint') return ev.summary || ev.progress || '';
6252
+ if (ev.type === 'agent:result') return (ev.agent || ev.from || '') + (ev.result || ev.msg || ev.message || ev.summary ? ': ' + (ev.result || ev.msg || ev.message || ev.summary) : '');
6253
+ if (ev.type === 'org:agent:online' || ev.type === 'org:agent:offline') return ev.title || ev.role || ev.agent || ev.id || ev.from || '';
6254
+ if (ev.type === 'org:error') return ev.error || ev.msg || ev.message || '';
6255
+ if (ev.type === 'file:write') return ev.path || ev.file || ev.msg || '';
6256
+ return ev.role||ev.result||ev.msg||ev.message||ev.progress||ev.agent||ev.status||'';
6257
+ }
6258
+
5713
6259
  function v2RenderOrgActivity() {
5714
6260
  if (!_v2SelOrg) return;
5715
6261
  const activity = _v2OrgData?._activity || [];
5716
6262
  const orgEvents = _v2OrgEventLog[_v2SelOrg] || [];
5717
- const events = [...activity, ...orgEvents].sort((a,b) => (b.ts||0)-(a.ts||0)).slice(0,80);
6263
+ const _actSeen = new Set();
6264
+ const events = [...activity, ...orgEvents]
6265
+ .filter(e => { const k=(e.ts||'')+'|'+(e.type||'')+'|'+(e.runId||e.session||'')+'|'+(e.from||e.role||e.agent||'')+'|'+(e.result||e.msg||e.message||e.summary||'').slice(0,20); return _actSeen.has(k)?false:(_actSeen.add(k),true); })
6266
+ .sort((a,b) => (b.ts||0)-(a.ts||0)).slice(0,80);
5718
6267
  const pane = document.getElementById('odt-activity');
5719
6268
  if (!pane) return;
5720
6269
  const fmtOrgEvType = t => {
5721
- const m={'org:start':'start','org:stop':'stop','org:complete':'done','org:create':'create','org:heartbeat':'hb','org:agent:online':'agent-on','org:comms':'comms'};
5722
- return m[t]||(t||'').replace(/^org:/,'');
6270
+ const m={
6271
+ 'org:start':'start','org:stop':'stop','org:complete':'done','org:create':'create',
6272
+ 'org:heartbeat':'hb','org:agent:online':'agent-on','org:comms':'comms',
6273
+ 'org:checkpoint':'checkpoint','run:cycle:complete':'cycle','run:complete':'run-done',
6274
+ 'org:error':'error','org:agent:offline':'agent-off',
6275
+ };
6276
+ return m[t]||(t||'').replace(/^(?:org|run):/,'');
5723
6277
  };
5724
6278
  if (!events.length) {
5725
6279
  pane.innerHTML = '<div class="empty">No activity recorded</div>';
@@ -5727,7 +6281,7 @@ function v2RenderOrgActivity() {
5727
6281
  }
5728
6282
  pane.innerHTML = `<div class="act-v2-list">${events.map(ev => {
5729
6283
  const t = ev.ts ? new Date(ev.ts).toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
5730
- const detail = ev.role||ev.msg||ev.agent||'';
6284
+ const detail = fmtOrgEvDetail(ev) || ev.role || ev.agent || '';
5731
6285
  return `<div class="av2-row"><span class="av2-time">${esc(t)}</span><span class="av2-type">${esc(fmtOrgEvType(ev.type))}</span><span class="av2-msg">${esc(detail)}</span></div>`;
5732
6286
  }).join('')}</div>`;
5733
6287
  }
@@ -5747,7 +6301,11 @@ function v2RenderOrgHealth() {
5747
6301
  <div class="hv2-cell"><div class="hv2-lbl">Topology</div><div class="hv2-val" style="font-size:14px">${esc((_v2OrgData?.topology||'—').toUpperCase())}</div></div>
5748
6302
  ${health?.agents_active!=null?`<div class="hv2-cell"><div class="hv2-lbl">Agents</div><div class="hv2-val ${health.agents_active>0?'green':''}">${health.agents_active}</div></div>`:''}
5749
6303
  ${health?.tasks_pending!=null?`<div class="hv2-cell"><div class="hv2-lbl">Tasks</div><div class="hv2-val ${health.tasks_pending>5?'amber':''}">${health.tasks_pending}</div></div>`:''}
6304
+ ${health?.run_success_rate_7d!=null?`<div class="hv2-cell"><div class="hv2-lbl">7d Success</div><div class="hv2-val ${health.run_success_rate_7d>=80?'green':health.run_success_rate_7d<50?'amber':''}">${health.run_success_rate_7d}%</div></div>`:''}
6305
+ ${health?.total_runs_7d!=null?`<div class="hv2-cell"><div class="hv2-lbl">7d Runs</div><div class="hv2-val">${health.total_runs_7d}</div></div>`:''}
6306
+ ${health?.budget_used_pct!=null?`<div class="hv2-cell"><div class="hv2-lbl">Budget Used</div><div class="hv2-val ${health.budget_used_pct>=90?'amber':''}">${health.budget_used_pct}%</div></div>`:''}
5750
6307
  </div>
6308
+ ${health?.budget_used_pct!=null&&health?.budget_max_tokens>0?`<div style="margin:10px 0 12px"><div style="height:4px;background:var(--border);border-radius:2px;overflow:hidden"><div style="width:${Math.min(100,health.budget_used_pct)}%;height:100%;background:${health.budget_used_pct>=90?'var(--red)':health.budget_used_pct>=70?'oklch(70% 0.18 60)':'var(--accent)'};border-radius:2px"></div></div><div style="font-size:10px;color:var(--text-lo);margin-top:4px">${(health.budget_used_tokens??0).toLocaleString()} / ${health.budget_max_tokens.toLocaleString()} tokens</div></div>`:''}
5751
6309
  ${health?.errors?.length?`<div style="margin-top:12px"><div class="m-group-title">Errors</div>${health.errors.slice(0,5).map(e=>`<div style="font-size:11px;color:var(--red);padding:4px 0;border-bottom:1px solid var(--border)">${esc(e)}</div>`).join('')}</div>`:`<div style="margin-top:10px;font-size:12px;color:var(--text-lo)">${listOrg.running?'Running — no errors detected':'Org is idle'}</div>`}
5752
6310
  `;
5753
6311
  }
@@ -5783,11 +6341,17 @@ function v2RenderOrgTasks() {
5783
6341
  if (!pane) return;
5784
6342
  // Normalize: API returns {todo:[],doing:[],done:[]} (columns) or a flat array
5785
6343
  const raw = _v2OrgData?.tasks;
6344
+ // Server puts completed/failed/cancelled in the 'done' column but keeps original status string
6345
+ const _normTaskStatus = (st, col) => {
6346
+ if (st === 'completed' || st === 'failed' || st === 'cancelled') return 'done';
6347
+ if (st === 'in_progress') return 'doing';
6348
+ return st || col;
6349
+ };
5786
6350
  let tasks = [];
5787
- if (Array.isArray(raw)) tasks = raw.slice();
6351
+ if (Array.isArray(raw)) tasks = raw.map(t => ({ ...t, status: _normTaskStatus(t.status, 'todo') }));
5788
6352
  else if (raw && typeof raw === 'object') {
5789
6353
  for (const [col, items] of Object.entries(raw)) {
5790
- (Array.isArray(items) ? items : []).forEach(t => tasks.push({ ...t, status: t.status || col }));
6354
+ (Array.isArray(items) ? items : []).forEach(t => tasks.push({ ...t, status: _normTaskStatus(t.status, col) }));
5791
6355
  }
5792
6356
  }
5793
6357
  if (!tasks.length) { pane.innerHTML = '<div class="empty">No tasks</div>'; return; }
@@ -5822,7 +6386,7 @@ function v2RenderOrgCosts() {
5822
6386
  ? c.map(r => ({ label: r.label ?? r.name ?? '—', cost: Number(r.value ?? r.cost ?? 0), tin: 0, tout: 0 }))
5823
6387
  : Object.entries(c).map(([k, v]) => ({ label: k, cost: Number(v) || 0, tin: 0, tout: 0 }));
5824
6388
  }
5825
- if (!rows.length) { pane.innerHTML = '<div class="empty">No cost data</div>'; return; }
6389
+ if (!rows.length) { pane.innerHTML = '<div class="empty" style="text-align:center;padding:24px 16px"><div style="font-size:13px;color:var(--text-lo);margin-bottom:6px">No cost data tracked yet</div><div style="font-size:11px;color:var(--text-lo);opacity:0.7">Agents must emit <code>agent:usage</code> events to populate this tab</div></div>'; return; }
5826
6390
  const cur = (b && b.currency) || 'USD';
5827
6391
  const period = (b && b.period) || '';
5828
6392
  const total = rows.reduce((s, r) => s + r.cost, 0);
@@ -5853,7 +6417,9 @@ function v2RenderOrgMembers() {
5853
6417
  const joinReqs = Array.isArray(_v2OrgData?._joinRequests) ? _v2OrgData._joinRequests : [];
5854
6418
  if (!list.length) { pane.innerHTML = '<div class="empty">No members</div>'; return; }
5855
6419
  const src = members.length ? 'joined members' : 'defined roles';
5856
- const active = (r) => r.running || r.active || r.status === 'active';
6420
+ // Overlay live running status from in-memory agent SSE state (updated by org:agent:online/offline events)
6421
+ const _liveRunning = new Set((_v2OrgData?._agents || []).filter(a => a.status === 'running').map(a => a.id));
6422
+ const active = (r) => r.running || r.active || r.status === 'active' || _liveRunning.has(r.id || r.name || r.user);
5857
6423
  pane.innerHTML = `<div class="m-group-title">Members (${list.length}) · ${src}</div>` +
5858
6424
  list.map(r => `<div style="display:flex;gap:10px;align-items:center;padding:6px 0;border-bottom:1px solid var(--border);font-size:12px">
5859
6425
  <span style="width:28px;height:28px;border-radius:50%;background:var(--surface-hi);display:inline-flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0">◈</span>
@@ -5878,7 +6444,7 @@ function v2RenderOrgGoals() {
5878
6444
  const indent = depth * 20;
5879
6445
  const pct = g.total > 0 ? Math.min(100, Math.round(g.filled / g.total * 100)) : 0;
5880
6446
  const status = g.status || 'pending';
5881
- const statusCls = status === 'done' ? 'on' : status === 'blocked' ? 'warn' : '';
6447
+ const statusCls = status === 'done' ? 'on' : (status === 'blocked' || status === 'in_progress') ? 'warn' : '';
5882
6448
  return `<div style="padding:8px 0;border-bottom:1px solid var(--border);padding-left:${indent}px">
5883
6449
  <div style="display:flex;align-items:center;gap:10px">
5884
6450
  <span style="flex:1;font-size:13px;color:var(--text-hi)">${esc(g.text || g.goal || g.title || '—')}</span>
@@ -5899,9 +6465,10 @@ function v2RenderOrgBoard() {
5899
6465
  if (!issues.length) { el.innerHTML = '<div class="empty">No issues</div>'; return; }
5900
6466
  const cols = ['open', 'in_progress', 'blocked', 'done', 'cancelled'];
5901
6467
  const PRIORITY = { urgent: '🔴', high: '🟠', medium: '🟡', low: '🟢' };
6468
+ const _normIssueStatus = s => (s === 'closed' || s === 'resolved') ? 'done' : (s || 'open');
5902
6469
  el.innerHTML = `<div style="display:flex;gap:10px;overflow-x:auto;padding-bottom:8px">` +
5903
6470
  cols.map(col => {
5904
- const cards = issues.filter(i => (i.status || i.state || 'open') === col);
6471
+ const cards = issues.filter(i => _normIssueStatus(i.status || i.state) === col);
5905
6472
  return `<div style="min-width:160px;flex:1">
5906
6473
  <div style="font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.07em;color:var(--text-lo);padding:6px 0;border-bottom:1px solid var(--border);margin-bottom:8px">${esc(col.replace('_', ' '))} <span style="background:var(--surface-hi);padding:1px 6px;border-radius:8px">${cards.length}</span></div>
5907
6474
  ${cards.slice(0, 15).map(i => `<div style="background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:7px 9px;margin-bottom:5px;font-size:12px">
@@ -5923,27 +6490,41 @@ function v2RenderOrgLive() {
5923
6490
  ${running.length
5924
6491
  ? running.map(a => `<div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--border)">
5925
6492
  <span class="live-dot" style="flex-shrink:0"></span>
5926
- <span style="font-size:13px;color:var(--text-hi)">${esc(a.type || a.title || a.id || '—')}</span>
5927
- <span style="margin-left:auto;font-size:11px;color:var(--text-lo)">${esc(a.adapter || '')}</span>
6493
+ <span style="font-size:13px;color:var(--text-hi)">${esc(a.title || a.id || '—')}</span>${a.adapterType?`<span style="font-size:10px;color:var(--text-lo);margin-left:4px">[${esc(a.adapterType)}]</span>`:''}
6494
+ <span style="margin-left:auto;font-size:11px;color:var(--text-lo)">${esc(a.adapterModel || a.adapter || '')}</span>
5928
6495
  </div>`).join('')
5929
6496
  : '<div style="color:var(--text-lo);font-size:13px;padding:10px 0">No agents currently running</div>'}
5930
6497
  </div>
5931
6498
  <div class="m-group-title" style="margin-bottom:6px">Activity Feed</div>
5932
6499
  <div style="max-height:240px;overflow-y:auto;font-size:11px;font-family:var(--mono)">
5933
- ${(_v2OrgData._activity || []).slice(-30).reverse().map(e => `<div style="padding:3px 0;border-bottom:1px solid var(--border);color:var(--text-lo)">
5934
- ${esc(relTime(e.ts || e.timestamp || e.created_at))}
5935
- <span style="color:var(--text-mid);margin-left:6px">${esc(e.type || e.kind || e.event || '—')}</span>
5936
- ${e.message ? `<span style="color:var(--text-hi);margin-left:6px">${esc(e.message.toString().slice(0, 80))}</span>` : ''}
5937
- </div>`).join('')}
6500
+ ${(()=>{const _la=_v2OrgData._activity||[];const _ls=_v2OrgEventLog[_v2SelOrg]||[];const _lseen=new Set();const _lmerged=[..._la,..._ls].filter(e=>{const k=(e.ts||'')+'|'+(e.type||'')+'|'+(e.runId||e.session||'')+'|'+(e.from||e.role||e.agent||'')+'|'+(e.result||e.msg||e.message||e.summary||'').slice(0,20);return _lseen.has(k)?false:(_lseen.add(k),true);}).sort((a,b)=>(a.ts||0)-(b.ts||0));return _lmerged.slice(-30).reverse().map(e=>`<div style="padding:3px 0;border-bottom:1px solid var(--border);color:var(--text-lo)">${esc(relTime(e.ts||e.timestamp||e.created_at))}<span style="color:var(--text-mid);margin-left:6px">${esc(e.type||e.kind||e.event||'—')}</span>${fmtOrgEvDetail(e)?`<span style="color:var(--text-hi);margin-left:6px">${esc(fmtOrgEvDetail(e).slice(0,80))}</span>`:''}</div>`).join('');})()}
5938
6501
  </div>`;
5939
6502
  // auto-refresh every 5s while tab is active
5940
6503
  if (!_orgLiveInterval) {
5941
6504
  _orgLiveInterval = setInterval(async () => {
5942
- if (_v2OrgTab !== 'live' || !_v2SelOrg) { clearInterval(_orgLiveInterval); _orgLiveInterval = null; return; }
5943
- const _enc = encodeURIComponent(_v2SelOrg);
6505
+ if (_v2OrgTab !== 'live' || !_v2SelOrg || currentView !== 'orgs') { clearInterval(_orgLiveInterval); _orgLiveInterval = null; return; }
6506
+ const _liveOrg = _v2SelOrg;
6507
+ const _enc = encodeURIComponent(_liveOrg);
5944
6508
  try {
5945
- const data = await fetch(`/api/org/${_enc}/activity${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []);
5946
- if (_v2OrgData) { _v2OrgData._activity = Array.isArray(data) ? data : []; v2RenderOrgLive(); }
6509
+ const [actData, agentsData] = await Promise.all([
6510
+ fetch(`/api/org/${_enc}/activity${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []),
6511
+ fetch(`/api/org/${_enc}/agents${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : {}).catch(() => {}),
6512
+ ]);
6513
+ if (_v2SelOrg !== _liveOrg || !_v2OrgData) return;
6514
+ // Merge fresh API data with SSE-pushed events to avoid clobbering live events
6515
+ // that haven't flushed to the JSONL file yet when this poll ran.
6516
+ const _freshAct = Array.isArray(actData) ? actData : [];
6517
+ const _sseAct = (_v2OrgData._activity || []).filter(e => e._sseOnly);
6518
+ const _mergeSeen = new Set(_freshAct.map(e => (e.ts||'')+'|'+(e.type||'')+'|'+(e.runId||e.session||'')));
6519
+ _sseAct.forEach(e => { const k=(e.ts||'')+'|'+(e.type||'')+'|'+(e.runId||e.session||''); if (!_mergeSeen.has(k)) _freshAct.push(e); });
6520
+ _v2OrgData._activity = _freshAct.sort((a,b)=>(a.ts||0)-(b.ts||0));
6521
+ const freshAgents = Array.isArray(agentsData) ? agentsData : (agentsData?.agents || []);
6522
+ if (freshAgents.length) {
6523
+ // Preserve SSE-driven running status: state file lags behind real-time SSE updates
6524
+ const sseRunning = new Set((_v2OrgData._agents || []).filter(a => a.status === 'running').map(a => a.id));
6525
+ _v2OrgData._agents = freshAgents.map(a => sseRunning.has(a.id) ? { ...a, status: 'running' } : a);
6526
+ }
6527
+ v2RenderOrgLive();
5947
6528
  } catch (_) {}
5948
6529
  }, 5000);
5949
6530
  }
@@ -5953,10 +6534,12 @@ function v2RenderOrgLive() {
5953
6534
  async function v2RenderOrgApprovals() {
5954
6535
  const el = document.getElementById('odt-approvals');
5955
6536
  if (!el || !_v2SelOrg) return;
6537
+ const _rendOrg = _v2SelOrg;
5956
6538
  el.innerHTML = '<div class="loading-txt">Loading…</div>';
5957
6539
  try {
5958
- const _enc = encodeURIComponent(_v2SelOrg);
6540
+ const _enc = encodeURIComponent(_rendOrg);
5959
6541
  const data = await fetch(`/api/org/${_enc}/approvals${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
6542
+ if (_v2SelOrg !== _rendOrg) return;
5960
6543
  const approvals = Array.isArray(data) ? data : (data.approvals || []);
5961
6544
  if (!approvals.length) { el.innerHTML = '<div class="empty">No pending approvals</div>'; return; }
5962
6545
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
@@ -5974,8 +6557,8 @@ async function v2RenderOrgApprovals() {
5974
6557
  <td style="padding:6px 8px;color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(a.created_at || a.ts)}</td>
5975
6558
  <td style="padding:6px 8px;white-space:nowrap">
5976
6559
  ${pending
5977
- ? `<button class="btn" style="font-size:10px;color:var(--green);border-color:var(--green)" title="Approve" aria-label="Approve" onclick="orgApprovalAction(${JSON.stringify(aid)},'approve')">✓</button>
5978
- <button class="btn" style="font-size:10px;color:var(--red);border-color:var(--red);margin-left:3px" title="Reject" aria-label="Reject" onclick="orgApprovalAction(${JSON.stringify(aid)},'reject')">✕</button>`
6560
+ ? `<button class="btn" style="font-size:10px;color:var(--green);border-color:var(--green)" title="Approve" aria-label="Approve" data-aid="${esc(aid)}" onclick="orgApprovalAction(this.dataset.aid,'approve')">✓</button>
6561
+ <button class="btn" style="font-size:10px;color:var(--red);border-color:var(--red);margin-left:3px" title="Reject" aria-label="Reject" data-aid="${esc(aid)}" onclick="orgApprovalAction(this.dataset.aid,'reject')">✕</button>`
5979
6562
  : ''}
5980
6563
  </td>
5981
6564
  </tr>`;
@@ -5987,7 +6570,7 @@ async function orgApprovalAction(id, action) {
5987
6570
  if (!confirm(action + ' this request?')) return;
5988
6571
  try {
5989
6572
  const _enc = encodeURIComponent(_v2SelOrg);
5990
- await fetch(`/api/org/${_enc}/approvals/${encodeURIComponent(id)}`, {
6573
+ await fetch(`/api/org/${_enc}/approvals/${encodeURIComponent(id)}${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`, {
5991
6574
  method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action })
5992
6575
  });
5993
6576
  showToast('Done', action + 'd', 'ok');
@@ -5999,10 +6582,12 @@ async function orgApprovalAction(id, action) {
5999
6582
  async function v2RenderOrgSecrets() {
6000
6583
  const el = document.getElementById('odt-secrets');
6001
6584
  if (!el || !_v2SelOrg) return;
6585
+ const _rendOrg = _v2SelOrg;
6002
6586
  el.innerHTML = '<div class="loading-txt">Loading…</div>';
6003
6587
  try {
6004
- const _enc = encodeURIComponent(_v2SelOrg);
6588
+ const _enc = encodeURIComponent(_rendOrg);
6005
6589
  const data = await fetch(`/api/org/${_enc}/secrets${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
6590
+ if (_v2SelOrg !== _rendOrg) return;
6006
6591
  const secrets = Array.isArray(data) ? data : (data.secrets || []);
6007
6592
  el.innerHTML = '<div style="font-size:11px;color:var(--text-lo);margin-bottom:10px">Secret values are never transmitted or displayed.</div>' +
6008
6593
  (secrets.length
@@ -6065,10 +6650,12 @@ function generateOrgSettingsCmd() {
6065
6650
  async function v2RenderOrgRoutines() {
6066
6651
  const el = document.getElementById('odt-routines');
6067
6652
  if (!el || !_v2SelOrg) return;
6653
+ const _rendOrg = _v2SelOrg;
6068
6654
  el.innerHTML = '<div class="loading-txt">Loading…</div>';
6069
6655
  try {
6070
- const _enc = encodeURIComponent(_v2SelOrg);
6656
+ const _enc = encodeURIComponent(_rendOrg);
6071
6657
  const data = await fetch(`/api/org/${_enc}/routines${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
6658
+ if (_v2SelOrg !== _rendOrg) return;
6072
6659
  const routines = Array.isArray(data) ? data : (data.routines || _v2OrgData?.config?.routines || []);
6073
6660
  if (!routines.length) { el.innerHTML = '<div class="empty">No routines configured</div>'; return; }
6074
6661
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
@@ -6078,8 +6665,8 @@ async function v2RenderOrgRoutines() {
6078
6665
  routines.map(r => `<tr style="border-top:1px solid var(--border)">
6079
6666
  <td style="padding:6px 8px;color:var(--text-hi)">${esc(r.name || '—')}</td>
6080
6667
  <td style="padding:6px 8px;font-family:var(--mono);color:var(--text-lo)">${esc(r.cron || r.schedule || '—')}</td>
6081
- <td style="padding:6px 8px;color:var(--text-lo)">${r.lastRun ? relTime(r.lastRun) : '—'}</td>
6082
- <td style="padding:6px 8px"><span class="ss-pill ${r.active || r.status === 'active' ? 'on' : ''}">${esc(r.status || '—')}</span></td>
6668
+ <td style="padding:6px 8px;color:var(--text-lo)">${(r.lastRun || r.last_run) ? relTime(r.lastRun || r.last_run) : '—'}</td>
6669
+ <td style="padding:6px 8px"><span class="ss-pill ${r.enabled || r.active || r.status === 'active' ? 'on' : ''}">${esc(r.status || (r.enabled != null ? (r.enabled ? 'active' : 'inactive') : '—'))}</span></td>
6083
6670
  </tr>`).join('') + '</tbody></table>';
6084
6671
  } catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
6085
6672
  }
@@ -6088,10 +6675,12 @@ async function v2RenderOrgRoutines() {
6088
6675
  async function v2RenderOrgMyIssues() {
6089
6676
  const el = document.getElementById('odt-myissues');
6090
6677
  if (!el || !_v2SelOrg) return;
6678
+ const _rendOrg = _v2SelOrg;
6091
6679
  el.innerHTML = '<div class="loading-txt">Loading…</div>';
6092
6680
  try {
6093
- const _enc = encodeURIComponent(_v2SelOrg);
6681
+ const _enc = encodeURIComponent(_rendOrg);
6094
6682
  const data = await fetch(`/api/org/${_enc}/my-issues${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
6683
+ if (_v2SelOrg !== _rendOrg) return;
6095
6684
  const issues = Array.isArray(data) ? data : (data.issues || []);
6096
6685
  if (!issues.length) { el.innerHTML = '<div class="empty">No issues assigned to you</div>'; return; }
6097
6686
  const PRIORITY = { urgent: '🔴', high: '🟠', medium: '🟡', low: '🟢' };
@@ -6100,7 +6689,7 @@ async function v2RenderOrgMyIssues() {
6100
6689
  <th style="padding:5px 4px">P</th><th style="padding:5px 8px">Title</th><th>Status</th><th>Updated</th>
6101
6690
  </tr></thead><tbody>` +
6102
6691
  issues.slice(0, 50).map(i => {
6103
- const cls = i.status === 'done' ? 'on' : i.status === 'blocked' ? 'warn' : '';
6692
+ const cls = i.status === 'done' || i.status === 'closed' || i.status === 'resolved' ? 'on' : (i.status === 'blocked' || i.status === 'in_progress') ? 'warn' : '';
6104
6693
  return `<tr style="border-top:1px solid var(--border)">
6105
6694
  <td style="padding:5px 4px">${PRIORITY[i.priority] || '·'}</td>
6106
6695
  <td style="padding:5px 8px;color:var(--text-hi);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc((i.title || i.description || '—').slice(0, 80))}</td>
@@ -6116,7 +6705,14 @@ function v2RenderOrgBudgets() {
6116
6705
  const el = document.getElementById('odt-budgets');
6117
6706
  if (!el || !_v2OrgData) return;
6118
6707
  const b = _v2OrgData._budgets || _v2OrgData.budgets || {};
6119
- const agents = _v2OrgData._agents || [];
6708
+ // Server returns { org_budget: {limit_tokens, limit_usd}, agents: [{id, title, tokens_in, tokens_out, total_cost_usd}] }
6709
+ const orgBudget = b.org_budget || {};
6710
+ const tokenLimit = orgBudget.limit_tokens || b.tokenLimit || null;
6711
+ const usdLimit = orgBudget.limit_usd || b.usdLimit || null;
6712
+ // Per-agent data comes from the budgets endpoint (has cost); fall back to agents roster
6713
+ const budgetAgents = Array.isArray(b.agents) && b.agents.length ? b.agents : [];
6714
+ const usedTokens = budgetAgents.reduce((s, a) => s + (a.tokens_in || 0) + (a.tokens_out || 0), 0);
6715
+ const usedUsd = budgetAgents.reduce((s, a) => s + (a.total_cost_usd || 0), 0);
6120
6716
 
6121
6717
  function fillBar(used, limit) {
6122
6718
  if (!limit) return '';
@@ -6127,22 +6723,25 @@ function v2RenderOrgBudgets() {
6127
6723
  }
6128
6724
 
6129
6725
  let html = '';
6130
- if (b.tokens != null || b.tokenLimit != null) {
6131
- html += `<div class="m-group-title">Tokens</div>${fillBar(b.tokens || 0, b.tokenLimit)}<div style="font-size:12px;color:var(--text-lo);margin:4px 0 14px">${Number(b.tokens || 0).toLocaleString()} / ${b.tokenLimit ? b.tokenLimit.toLocaleString() : '∞'}</div>`;
6726
+ if (tokenLimit != null || usedTokens > 0) {
6727
+ html += `<div class="m-group-title">Tokens</div>${fillBar(usedTokens, tokenLimit)}<div style="font-size:12px;color:var(--text-lo);margin:4px 0 14px">${usedTokens.toLocaleString()} / ${tokenLimit ? tokenLimit.toLocaleString() : '∞'}</div>`;
6132
6728
  }
6133
- if (b.usd != null || b.usdLimit != null) {
6134
- html += `<div class="m-group-title">USD Budget</div>${fillBar(b.usd || 0, b.usdLimit)}<div style="font-size:12px;color:var(--text-lo);margin:4px 0 14px">$${Number(b.usd || 0).toFixed(4)} / ${b.usdLimit ? '$' + Number(b.usdLimit).toFixed(2) : '∞'}</div>`;
6729
+ if (usdLimit != null || usedUsd > 0) {
6730
+ html += `<div class="m-group-title">USD Budget</div>${fillBar(usedUsd, usdLimit)}<div style="font-size:12px;color:var(--text-lo);margin:4px 0 14px">$${usedUsd.toFixed(4)} / ${usdLimit ? '$' + Number(usdLimit).toFixed(2) : '∞'}</div>`;
6135
6731
  }
6136
- if (agents.length) {
6732
+ if (budgetAgents.length) {
6137
6733
  html += '<div class="m-group-title" style="margin-bottom:6px">Per Agent</div>' +
6138
6734
  `<table style="width:100%;border-collapse:collapse;font-size:12px"><thead><tr style="color:var(--text-xs);text-align:left"><th style="padding:4px 8px">Agent</th><th>Tokens In</th><th>Tokens Out</th><th>Cost</th></tr></thead><tbody>` +
6139
- agents.slice(0, 20).map(a => {
6140
- const over = a.budgetLimit && ((a.tokensIn || 0) + (a.tokensOut || 0)) > a.budgetLimit;
6735
+ budgetAgents.slice(0, 20).map(a => {
6736
+ const tIn = a.tokens_in || 0, tOut = a.tokens_out || 0;
6737
+ const cost = a.total_cost_usd || 0;
6738
+ const limit = a.limit_usd || orgBudget.limit_usd || 0;
6739
+ const over = limit && cost > limit;
6141
6740
  return `<tr style="border-top:1px solid var(--border)${over ? ';color:var(--red)' : ''}">
6142
- <td style="padding:4px 8px;font-family:var(--mono);font-size:11px;color:${over ? 'var(--red)' : 'var(--text-hi)'}">${esc((a.id || a.title || '—').toString().slice(0, 14))}</td>
6143
- <td style="padding:4px 8px;color:var(--text-lo)">${Number(a.tokensIn || 0).toLocaleString()}</td>
6144
- <td style="padding:4px 8px;color:var(--text-lo)">${Number(a.tokensOut || 0).toLocaleString()}</td>
6145
- <td style="padding:4px 8px;color:var(--accent)">$${Number(a.cost || 0).toFixed(4)}${over ? ' ⚠' : ''}</td>
6741
+ <td style="padding:4px 8px;font-family:var(--mono);font-size:11px;color:${over ? 'var(--red)' : 'var(--text-hi)'}">${esc((a.title || a.id || '—').toString().slice(0, 14))}</td>
6742
+ <td style="padding:4px 8px;color:var(--text-lo)">${tIn.toLocaleString()}</td>
6743
+ <td style="padding:4px 8px;color:var(--text-lo)">${tOut.toLocaleString()}</td>
6744
+ <td style="padding:4px 8px;color:var(--accent)">$${cost.toFixed(4)}${over ? ' ⚠' : ''}</td>
6146
6745
  </tr>`;
6147
6746
  }).join('') + '</tbody></table>';
6148
6747
  }
@@ -6153,10 +6752,12 @@ function v2RenderOrgBudgets() {
6153
6752
  async function v2RenderOrgPlugins() {
6154
6753
  const el = document.getElementById('odt-plugins');
6155
6754
  if (!el || !_v2SelOrg) return;
6755
+ const _rendOrg = _v2SelOrg;
6156
6756
  el.innerHTML = '<div class="loading-txt">Loading…</div>';
6157
6757
  try {
6158
- const _enc = encodeURIComponent(_v2SelOrg);
6758
+ const _enc = encodeURIComponent(_rendOrg);
6159
6759
  const data = await fetch(`/api/org/${_enc}/plugins${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
6760
+ if (_v2SelOrg !== _rendOrg) return;
6160
6761
  const plugins = Array.isArray(data) ? data : (data.plugins || []);
6161
6762
  if (!plugins.length) { el.innerHTML = '<div class="empty">No plugins installed</div>'; return; }
6162
6763
  el.innerHTML = `<div class="proj-grid">` +
@@ -6184,7 +6785,7 @@ function v2RenderOrgCharts() {
6184
6785
  const DAY = 86400000;
6185
6786
  const buckets = Array.from({length: 14}, (_, i) => ({
6186
6787
  day: new Date(now - (13 - i) * DAY).toLocaleDateString('en', {month:'short', day:'numeric'}),
6187
- ts: now - (13 - i) * DAY,
6788
+ ts: now - (14 - i) * DAY,
6188
6789
  events: 0, errors: 0,
6189
6790
  }));
6190
6791
  activity.forEach(e => {
@@ -6244,10 +6845,12 @@ function v2RenderOrgCharts() {
6244
6845
  async function v2RenderOrgProjects() {
6245
6846
  const el = document.getElementById('odt-projects');
6246
6847
  if (!el || !_v2SelOrg) return;
6848
+ const _rendOrg = _v2SelOrg;
6247
6849
  el.innerHTML = '<div class="loading-txt">Loading…</div>';
6248
6850
  try {
6249
- const _enc = encodeURIComponent(_v2SelOrg);
6851
+ const _enc = encodeURIComponent(_rendOrg);
6250
6852
  const data = await fetch(`/api/org/${_enc}/projects${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
6853
+ if (_v2SelOrg !== _rendOrg) return;
6251
6854
  const projects = Array.isArray(data) ? data : (data.projects || []);
6252
6855
  if (!projects.length) { el.innerHTML = '<div class="empty">No projects configured</div>'; return; }
6253
6856
  el.innerHTML = `<div class="proj-grid">${projects.map(p => `<div class="proj-card">
@@ -6295,14 +6898,1211 @@ async function v2RenderOrgSkills() {
6295
6898
  </div>`).join('');
6296
6899
  }
6297
6900
 
6901
+ // ── ORG CHAT ───────────────────────────────────────────────
6902
+ let _odtChatSessions = [];
6903
+ let _odtChatCurrentId = null;
6904
+ let _odtChatCurrentAgent = 'all';
6905
+ let _odtChatSseSource = null;
6906
+ let _odtChatSeenKeys = new Set();
6907
+ let _odtChatForOrg = '';
6908
+ // Loop grouping: [S] sessions with same prompt + <20 min gap → one entry
6909
+ let _odtSessionGroups = {}; // groupId → {sessions, events, status, ts, _isGroup}
6910
+ let _odtSessionGroupMap = {}; // sessionId → groupId
6911
+ // Run grouping: [R] runs with same goal (or no goal) + <30 min gap → one entry
6912
+ let _odtRunGroups = {}; // groupId → {runs, status, ts, _isRunGroup}
6913
+ let _odtRunGroupMap = {}; // runId → groupId
6914
+
6915
+ function _odtOrgSessionMatch(s) {
6916
+ if (!_v2SelOrg) return true;
6917
+ const orgLc = _v2SelOrg.toLowerCase();
6918
+ const prompt = (s.prompt || '').toLowerCase();
6919
+ // Use a word-boundary check so "monomind-improver" doesn't match sessions for
6920
+ // "monomind-improver-state" (the shorter name is a prefix of the longer one).
6921
+ // After the matched org name, the next char must be end-of-string or a character
6922
+ // that cannot be part of an org name (not alphanumeric, hyphen, or underscore).
6923
+ const _orgBound = (str, prefix) => {
6924
+ const idx = str.indexOf(prefix);
6925
+ if (idx === -1) return false;
6926
+ const after = str[idx + prefix.length];
6927
+ return after === undefined || !/[a-z0-9_-]/.test(after);
6928
+ };
6929
+ if (_orgBound(prompt, 'running org: ' + orgLc) || _orgBound(prompt, 'org: ' + orgLc)) return true;
6930
+ return (s.events || []).some(ev => (ev.org || '').toLowerCase() === orgLc);
6931
+ }
6932
+
6933
+ function _odtChatAgentMatches(ev) {
6934
+ if (_odtChatCurrentAgent === 'all') return true;
6935
+ // Structural events always show regardless of agent filter
6936
+ const structural = new Set(['session:start','session:complete','domain:dispatch','domain:complete','file:write','run:start','run:complete','run:cycle:complete','org:start','org:complete','org:agent:online','org:agent:offline','org:checkpoint','org:error','loop:start','loop:complete','loop:tick','loop:hil','loop:hil:waiting','loop:hil:resolved']);
6937
+ if (structural.has(ev.type)) return true;
6938
+ return ev.agent === _odtChatCurrentAgent || ev.from === _odtChatCurrentAgent || ev.to === _odtChatCurrentAgent || ev.role === _odtChatCurrentAgent;
6939
+ }
6940
+
6941
+ async function v2RenderOrgChat() {
6942
+ const pane = document.getElementById('odt-chat');
6943
+ if (!pane) return;
6944
+
6945
+ // Build the pane HTML once
6946
+ if (!pane.querySelector('#odt-chat-bar')) {
6947
+ pane.innerHTML = `
6948
+ <div id="odt-chat-bar">
6949
+ <span id="odt-chat-sess-lbl">RUN</span>
6950
+ <select id="odt-chat-sel" onchange="odtChatSelectSession(this.value)">
6951
+ <option value="">— loading runs —</option>
6952
+ </select>
6953
+ <div id="odt-chat-live-dot"></div>
6954
+ <span id="odt-chat-live-lbl">OFFLINE</span>
6955
+ </div>
6956
+ <div id="odt-chat-agent-bar" style="display:none">
6957
+ <span id="odt-chat-agent-lbl">AGENT</span>
6958
+ </div>
6959
+ <div id="odt-chat-excerpt" class="cv-excerpt-banner"></div>
6960
+ <div id="odt-chat-feed">
6961
+ <div id="odt-chat-empty">Select a session to see agent communications.<br><span style="font-size:10px;opacity:0.5">Sessions appear after the first /mastermind:runorg</span></div>
6962
+ </div>`;
6963
+ }
6964
+
6965
+ // Reset state only when org changes (not on every tab-switch)
6966
+ if (_odtChatForOrg !== _v2SelOrg) {
6967
+ _odtChatForOrg = _v2SelOrg;
6968
+ _odtChatCurrentId = null;
6969
+ _odtChatCurrentAgent = 'all';
6970
+ _odtChatSeenKeys = new Set();
6971
+ // Clear sessions from the previous org so _odtLoadChatSessions doesn't snapshot them
6972
+ // into _prevSessions and risk contaminating the new org's event buffers. SSE-injected
6973
+ // sessions for the NEW org arriving during the upcoming fetch are still captured via
6974
+ // _sseDuringFetch in _odtLoadChatSessions.
6975
+ _odtChatSessions = [];
6976
+ document.getElementById('odt-chat-agent-bar').style.display = 'none';
6977
+ document.getElementById('odt-chat-feed').querySelectorAll('.cv-msg, .cv-session-hdr').forEach(e => e.remove());
6978
+ const emptyEl = document.getElementById('odt-chat-empty');
6979
+ if (emptyEl) { emptyEl.style.display = 'block'; emptyEl.textContent = 'Select a session to see agent communications.'; }
6980
+ }
6981
+
6982
+ await _odtLoadChatSessions();
6983
+ // Returning to chat tab (same org, session already selected): re-render feed to catch
6984
+ // up on events buffered in sess.events while user was on another tab. _odtLoadChatSessions
6985
+ // refreshes the run list but won't re-render the feed when _odtChatCurrentId is already set.
6986
+ if (_odtChatCurrentId) {
6987
+ const _catchSess = _odtChatSessions.find(s => s.id === _odtChatCurrentId);
6988
+ if (_catchSess && (_catchSess._eventsLoaded || !_catchSess._isOrgRun)) {
6989
+ const _catchFeed = document.getElementById('odt-chat-feed');
6990
+ if (_catchFeed) {
6991
+ _catchFeed.querySelectorAll('.cv-msg, .cv-session-hdr').forEach(e => e.remove());
6992
+ // Re-inject session header for session-based (non-_isOrgRun) sessions — was removed above
6993
+ if (!_catchSess._isOrgRun) {
6994
+ const _cTs = _catchSess.ts || _catchSess.startedAt || 0;
6995
+ const _cTsStr = _cTs ? new Date(_cTs).toLocaleString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}) : '—';
6996
+ const _cPrompt = (_catchSess.prompt || '').replace(/^running org:\s*/i, '').slice(0, 100) || _odtChatCurrentId;
6997
+ const _cSt = _catchSess.status || 'unknown';
6998
+ const _cStCls = _cSt === 'running' ? 'color:oklch(65% 0.20 145)' : _cSt === 'complete' ? 'color:var(--text-lo)' : 'color:var(--text-xs)';
6999
+ const _cHdr = document.createElement('div');
7000
+ _cHdr.className = 'cv-session-hdr';
7001
+ _cHdr.innerHTML = `<div class="cv-shdr-prompt">${esc(_cPrompt)}</div>`+
7002
+ `<div class="cv-shdr-meta">`+
7003
+ `<span class="cv-shdr-id" title="${esc(_odtChatCurrentId)}">${esc(_odtChatCurrentId.slice(0,22))}</span>`+
7004
+ `<span class="cv-shdr-sep">·</span><span style="${_cStCls}">${esc(_cSt)}</span>`+
7005
+ `<span class="cv-shdr-sep">·</span><span>${esc(_cTsStr)}</span>`+
7006
+ `<span class="cv-shdr-sep">·</span><span class="cv-shdr-evcount">${(_catchSess.events||[]).length} events</span>`+
7007
+ `</div>`;
7008
+ _catchFeed.appendChild(_cHdr);
7009
+ }
7010
+ (_catchSess.events || []).forEach(ev => {
7011
+ _odtAppendEvent(ev, false);
7012
+ const _ck = (ev?.ts||'')+'|'+(ev?.type||'')+'|'+(ev?.runId||ev?.session||'')+'|'+(ev?.from||ev?.role||ev?.agent||'')+'|'+(ev?.result||ev?.msg||ev?.message||ev?.summary||'').slice(0,20);
7013
+ _odtChatSeenKeys.add(_ck);
7014
+ });
7015
+ // Re-inject [S]→[R] link card on tab-return (same as first-select path)
7016
+ if (!_catchSess._isOrgRun) {
7017
+ const _crLinkedId = 'run-' + (_catchSess.id || '').replace(/^mm-/, '');
7018
+ const _crLinked = _odtChatSessions.find(s => s._isOrgRun && s.id === _crLinkedId);
7019
+ if (_crLinked) {
7020
+ const _crLinkEl = document.createElement('div');
7021
+ _crLinkEl.className = 'cv-msg cv-sys';
7022
+ const _crGoal = (_crLinked.prompt || '').slice(0, 60) || _crLinkedId;
7023
+ _crLinkEl.innerHTML = `<div class="cv-bub" style="cursor:pointer;border-left:2px solid var(--accent);background:oklch(15% 0.05 220)">`+
7024
+ `<span class="cv-etype" style="color:var(--accent)">RUN</span>`+
7025
+ `<span class="cv-text">→ ${esc(_crGoal)}${_crGoal.length >= 60 ? '…' : ''}</span>`+
7026
+ `<span class="cv-ts" style="color:var(--accent)">[${esc(_crLinked.status||'?')}]</span></div>`;
7027
+ _crLinkEl.onclick = () => {
7028
+ const _crSel = document.getElementById('odt-chat-sel');
7029
+ if (_crSel) { _crSel.value = _crLinkedId; odtChatSelectSession(_crLinkedId); }
7030
+ };
7031
+ _catchFeed.appendChild(_crLinkEl);
7032
+ }
7033
+ }
7034
+ _catchFeed.scrollTop = _catchFeed.scrollHeight;
7035
+ }
7036
+ // Refresh agent bar — new agents may have come online while on another tab
7037
+ const _catchOrgRoles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : [];
7038
+ _odtPopulateAgentBar(_catchSess.events || [], _catchOrgRoles);
7039
+ // Refresh excerpt banner for all session types so last-activity stays current after tab-return
7040
+ if ((_catchSess.events || []).length) {
7041
+ renderCvExcerptBanner('odt-chat-excerpt', _catchSess.events, _catchSess);
7042
+ }
7043
+ } else if (_catchSess && _catchSess._isOrgRun && !_catchSess._eventsLoaded && !_catchSess._loading) {
7044
+ // Session selected but events not yet loaded (object reference may have been replaced by
7045
+ // _odtLoadChatSessions while lazy-load was in-flight). Retrigger select to lazy-load events
7046
+ // so returning to Chat tab never shows a permanently empty feed.
7047
+ // Guard _loading: if a fetch is already in-flight (user tabbed away/back during the fetch)
7048
+ // do NOT fire a second parallel fetch — it would race and overwrite sess.events on settle.
7049
+ odtChatSelectSession(_odtChatCurrentId);
7050
+ }
7051
+ }
7052
+ _odtConnectChatSSE();
7053
+ }
7054
+
7055
+ async function _odtLoadChatSessions() {
7056
+ if (!_v2SelOrg) return;
7057
+ const _loadedForOrg = _v2SelOrg;
7058
+ // Snapshot existing sessions to preserve event caches across refreshes.
7059
+ // Do NOT reset _odtChatSessions to [] here — SSE handlers may inject new
7060
+ // sessions into the live array during the await below, and a reset would
7061
+ // cause them to be lost when we overwrite with the API response.
7062
+ const _prevSessions = _odtChatSessions.slice();
7063
+ try {
7064
+ const dirParam = (typeof DIR !== 'undefined' && DIR) ? '&dir='+encodeURIComponent(DIR) : '';
7065
+ // Primary: structured org run files
7066
+ const runs = await apiFetch('/api/org/'+encodeURIComponent(_loadedForOrg)+'/runs?x=1'+dirParam);
7067
+ if (_v2SelOrg !== _loadedForOrg) return;
7068
+ if (Array.isArray(runs) && runs.length) {
7069
+ // Capture any sessions SSE-injected DURING the await (not in pre-fetch snapshot)
7070
+ const _sseDuringFetch = _odtChatSessions.filter(s => !_prevSessions.find(p => p.id === s.id));
7071
+ _odtChatSessions = runs.map(r => {
7072
+ const prev = _prevSessions.find(s => s.id === r.runId) || _sseDuringFetch.find(s => s.id === r.runId);
7073
+ // Preserve the SSE-incremented cycleCount/eventCount when they exceed the disk-based API values.
7074
+ // The JSONL file may not have flushed the latest checkpoint yet when this fetch resolves,
7075
+ // so the API count can be lower than what SSE has already observed — causing a visible
7076
+ // backwards jump in the "Ncyc" label if we blindly replace with the API value.
7077
+ const _prevMeta = prev?._runMeta;
7078
+ const _mergedMeta = _prevMeta
7079
+ ? { ...r, cycleCount: Math.max(r.cycleCount || 0, _prevMeta.cycleCount || 0), eventCount: Math.max(r.eventCount || 0, _prevMeta.eventCount || 0) }
7080
+ : r;
7081
+ return {
7082
+ id: r.runId,
7083
+ ts: r.startedAt,
7084
+ prompt: r.goal || r.runId,
7085
+ status: r.status,
7086
+ _isOrgRun: true,
7087
+ _runMeta: _mergedMeta,
7088
+ events: prev ? prev.events : [],
7089
+ _eventsLoaded: prev ? prev._eventsLoaded : false,
7090
+ };
7091
+ });
7092
+ // Re-attach SSE-only sessions from original snapshot not in API response
7093
+ _prevSessions.forEach(ps => {
7094
+ if (!_odtChatSessions.find(s => s.id === ps.id)) _odtChatSessions.push(ps);
7095
+ });
7096
+ // Re-attach sessions SSE-injected DURING the fetch (not in snapshot, not in API)
7097
+ _sseDuringFetch.forEach(ss => {
7098
+ if (!_odtChatSessions.find(s => s.id === ss.id)) _odtChatSessions.push(ss);
7099
+ });
7100
+ // Sort most-recent-first so SSE-injected new runs (appended above) appear at top of dropdown
7101
+ _odtChatSessions.sort((a, b) => (b.ts || b.startedAt || 0) - (a.ts || a.startedAt || 0));
7102
+ _odtPopulateChatSel();
7103
+ // Also merge mastermind sessions matching this org — they are distinct from org
7104
+ // runs (no runId/JSONL file) but contain lifecycle events the user needs to see.
7105
+ // Orgs running via _repeat loops emit session:start/complete events to the mastermind
7106
+ // event store; these never appear in the run list and are otherwise invisible.
7107
+ try {
7108
+ const _mmSessR = await apiFetch('/api/mastermind/sessions');
7109
+ if (_v2SelOrg === _loadedForOrg) {
7110
+ const _mmAll = Array.isArray(_mmSessR) ? _mmSessR : Object.values(_mmSessR.sessions || {});
7111
+ const _mmMatch = _mmAll.filter(_odtOrgSessionMatch).filter(s => !_odtChatSessions.find(x => x.id === s.id));
7112
+ if (_mmMatch.length) {
7113
+ _mmMatch.forEach(s => {
7114
+ if (s.status && s.status !== 'running' && s.status !== 'complete') s.status = 'complete';
7115
+ // Mark events as loaded for complete sessions that already carry the full events[]
7116
+ // from /api/mastermind/sessions — avoids a redundant /api/mastermind/session/:id
7117
+ // fetch (and the "Loading session events…" spinner) on first select.
7118
+ // Running sessions are NOT marked loaded so they still get a fresh fetch.
7119
+ if (s.status !== 'running' && Array.isArray(s.events) && s.events.length > 0) s._eventsLoaded = true;
7120
+ });
7121
+ _odtChatSessions = _odtChatSessions.concat(_mmMatch);
7122
+ _odtChatSessions.sort((a, b) => (b.ts || b.startedAt || 0) - (a.ts || a.startedAt || 0));
7123
+ _odtPopulateChatSel();
7124
+ }
7125
+ }
7126
+ } catch(_) {}
7127
+ if (!_odtChatCurrentId && _odtChatSessions.length) {
7128
+ const first = _odtChatSessions[0];
7129
+ const sel = document.getElementById('odt-chat-sel');
7130
+ if (sel) { sel.value = first.id; odtChatSelectSession(first.id); }
7131
+ }
7132
+ return;
7133
+ }
7134
+ } catch(_) {}
7135
+ if (_v2SelOrg !== _loadedForOrg) return;
7136
+ // Fallback: mastermind lifecycle events (pre-run-file era sessions)
7137
+ try {
7138
+ const r = await apiFetch('/api/mastermind/sessions');
7139
+ if (_v2SelOrg !== _loadedForOrg) return;
7140
+ const all = Array.isArray(r) ? r : Object.values(r.sessions || {});
7141
+ _odtChatSessions = all.filter(_odtOrgSessionMatch).map(s => {
7142
+ if (s.status && s.status !== 'running' && s.status !== 'complete') s.status = 'complete';
7143
+ // Mark complete sessions with events already loaded — avoids redundant lazy-load fetch
7144
+ if (s.status !== 'running' && Array.isArray(s.events) && s.events.length > 0) s._eventsLoaded = true;
7145
+ return s;
7146
+ });
7147
+ // Re-attach SSE-injected org run placeholders that aren't in the mastermind sessions API.
7148
+ // These are created by _odtHandleLiveEvent when a run:start arrives before the JSONL file
7149
+ // flushes to disk — without this, switching to the fallback path drops them.
7150
+ _prevSessions.forEach(ps => {
7151
+ if (ps._isOrgRun && !_odtChatSessions.find(s => s.id === ps.id)) _odtChatSessions.push(ps);
7152
+ });
7153
+ _odtChatSessions.sort((a, b) => (b.ts || b.startedAt || 0) - (a.ts || a.startedAt || 0));
7154
+ _odtPopulateChatSel();
7155
+ // Auto-select most recent session when nothing is running (mirrors primary path behaviour).
7156
+ // _odtPopulateChatSel only picks running sessions; for all-complete orgs the feed stays
7157
+ // blank without this.
7158
+ if (!_odtChatCurrentId && _odtChatSessions.length) {
7159
+ const _fbFirst = _odtChatSessions[0];
7160
+ const _fbSel = document.getElementById('odt-chat-sel');
7161
+ if (_fbSel) { _fbSel.value = _fbFirst.id; odtChatSelectSession(_fbFirst.id); }
7162
+ }
7163
+ } catch(_) {}
7164
+ }
7165
+
7166
+ function _odtRebuildRunGroups() {
7167
+ // Group consecutive [R] runs within 30 min with the same goal (or both without a goal).
7168
+ const _ODT_RUN_GAP = 30 * 60 * 1000;
7169
+ const _norm = p => (p || '').toLowerCase().trim().slice(0, 40);
7170
+ _odtRunGroups = {};
7171
+ _odtRunGroupMap = {};
7172
+ const runs = _odtChatSessions.filter(s => s._isOrgRun)
7173
+ .slice().sort((a, b) => (a.ts||a.startedAt||0) - (b.ts||b.startedAt||0));
7174
+ const groups = [];
7175
+ runs.forEach(s => {
7176
+ // No-goal runs (prompt === id) get key '' — group all consecutive ones
7177
+ const key = (s.prompt && s.prompt !== s.id) ? _norm(s.prompt) : '';
7178
+ const sAt = s.ts || s.startedAt || 0;
7179
+ const last = groups[groups.length - 1];
7180
+ if (last && last._normKey === key && (sAt - last._newestAt) < _ODT_RUN_GAP) {
7181
+ last.runs.push(s);
7182
+ last._newestAt = sAt;
7183
+ if (s.status === 'running') last.status = 'running';
7184
+ else if (s.status === 'stale' && last.status !== 'running' && last.status !== 'complete') last.status = 'stale';
7185
+ else if (s.status === 'complete' && last.status !== 'running') last.status = 'complete';
7186
+ } else {
7187
+ groups.push({ id: s.id, runs: [s], _normKey: key, _newestAt: sAt,
7188
+ status: s.status || 'complete', prompt: s.prompt, ts: sAt, _isRunGroup: false });
7189
+ }
7190
+ });
7191
+ groups.forEach(g => {
7192
+ if (g.runs.length > 1) {
7193
+ g._isRunGroup = true;
7194
+ g.id = 'rgrp:' + g.runs[0].id;
7195
+ g.ts = g._newestAt;
7196
+ // Use the most informative goal among sub-runs
7197
+ const withGoal = g.runs.find(r => r.prompt && r.prompt !== r.id);
7198
+ if (withGoal) g.prompt = withGoal.prompt;
7199
+ }
7200
+ _odtRunGroups[g.id] = g;
7201
+ g.runs.forEach(r => { _odtRunGroupMap[r.id] = g.id; });
7202
+ });
7203
+ }
7204
+
7205
+ function _odtGetMergedRunGroupEvents(group) {
7206
+ const all = [];
7207
+ (group.runs || []).forEach(r => (r.events || []).forEach(ev => all.push(ev)));
7208
+ all.sort((a, b) => (a.ts||0) - (b.ts||0));
7209
+ return all;
7210
+ }
7211
+
7212
+ function _odtRebuildSessionGroups() {
7213
+ // Group [S] sessions (loop reps) that share same prompt + gap <20 min into one entry.
7214
+ // [R] runs are never grouped — each is its own run.
7215
+ const _ODT_MAX_GAP = 20 * 60 * 1000;
7216
+ const _odtNorm = p => (p || '').replace(/^running org:\s*/i, '').toLowerCase().replace(/\s*rep\s+\d+.*$/i, '').trim().slice(0, 40);
7217
+ _odtSessionGroups = {};
7218
+ _odtSessionGroupMap = {};
7219
+ // Process [S] sessions only, sorted oldest first
7220
+ const sSessions = _odtChatSessions.filter(s => !s._isOrgRun)
7221
+ .slice().sort((a, b) => (a.ts||a.startedAt||0) - (b.ts||b.startedAt||0));
7222
+ const groups = [];
7223
+ sSessions.forEach(s => {
7224
+ const key = _odtNorm(s.prompt);
7225
+ const sAt = s.ts || s.startedAt || 0;
7226
+ const last = groups[groups.length - 1];
7227
+ if (last && last._normKey === key && key && (sAt - last._newestAt) < _ODT_MAX_GAP) {
7228
+ last.sessions.push(s);
7229
+ last._newestAt = sAt;
7230
+ last.totalEvents += (s.events || []).length;
7231
+ if (s.status === 'running') last.status = 'running';
7232
+ } else {
7233
+ groups.push({ id: s.id, sessions: [s], _normKey: key, _newestAt: sAt,
7234
+ totalEvents: (s.events || []).length, status: s.status || 'complete',
7235
+ prompt: s.prompt, ts: sAt, _isGroup: false });
7236
+ }
7237
+ });
7238
+ // Finalize: multi-session groups get a virtual ID + merged events
7239
+ groups.forEach(g => {
7240
+ if (g.sessions.length > 1) {
7241
+ g._isGroup = true;
7242
+ g.id = 'grp:' + g.sessions[0].id;
7243
+ g.ts = g._newestAt; // show newest timestamp in dropdown
7244
+ }
7245
+ _odtSessionGroups[g.id] = g;
7246
+ g.sessions.forEach(s => { _odtSessionGroupMap[s.id] = g.id; });
7247
+ });
7248
+ }
7249
+
7250
+ function _odtGetMergedGroupEvents(group) {
7251
+ // Merge events from all sessions in a group, time-sorted
7252
+ const all = [];
7253
+ (group.sessions || []).forEach(s => (s.events || []).forEach(ev => all.push(ev)));
7254
+ all.sort((a, b) => (a.ts||0) - (b.ts||0));
7255
+ return all;
7256
+ }
7257
+
7258
+ function _odtPopulateChatSel() {
7259
+ const sel = document.getElementById('odt-chat-sel');
7260
+ if (!sel) return;
7261
+ if (document.activeElement === sel) return;
7262
+ // Rebuild groups from current sessions + runs
7263
+ _odtRebuildSessionGroups();
7264
+ _odtRebuildRunGroups();
7265
+ const prev = _odtChatCurrentId || sel.value;
7266
+ const runGroups = Object.values(_odtRunGroups).sort((a, b) => b.ts - a.ts);
7267
+ const sessGroups = Object.values(_odtSessionGroups).sort((a, b) => b.ts - a.ts);
7268
+ const totalEntries = runGroups.length + sessGroups.length;
7269
+ const hasRuns = runGroups.length > 0;
7270
+ const hasSess = sessGroups.length > 0;
7271
+ const entryLabel = (hasRuns && hasSess) ? 'entries' : hasRuns ? 'run' + (runGroups.length !== 1 ? 's' : '') : 'loop' + (sessGroups.length !== 1 ? 's' : '');
7272
+ sel.innerHTML = `<option value="">${totalEntries ? `— ${totalEntries} ${entryLabel} for ${_v2SelOrg} —` : '— no runs yet —'}</option>`;
7273
+
7274
+ // Render [R] run groups
7275
+ runGroups.forEach(g => {
7276
+ const d = new Date(g.ts || 0);
7277
+ const ts = d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'});
7278
+ const date = d.toLocaleDateString([],{month:'short',day:'numeric'});
7279
+ const _rawPrompt = (g.prompt || '').replace(/^running org:\s*/i, '').trim();
7280
+ const prompt = (_rawPrompt && _rawPrompt !== g.runs[0]?.id) ? _rawPrompt.slice(0, 30) : '(auto-run)';
7281
+ const runsPart = g._isRunGroup ? ` · ${g.runs.length} runs` : '';
7282
+ // Total event count from run metadata
7283
+ const totalEvs = g.runs.reduce((sum, r) => sum + ((r._runMeta?.eventCount || 0) || (r.events || []).length), 0);
7284
+ const totalCyc = g.runs.reduce((sum, r) => sum + (r._runMeta?.cycleCount || 0), 0);
7285
+ const evPart = totalEvs > 0 ? ` · ${totalCyc}cyc · ${totalEvs}ev` : '';
7286
+ const opt = document.createElement('option');
7287
+ opt.value = g.id;
7288
+ opt.textContent = `[R] ${date} ${ts} ${prompt}${_rawPrompt.length>30?'…':''}${runsPart}${evPart} [${g.status||'?'}]`;
7289
+ if (g.id === prev || g.runs.some(r => r.id === prev)) opt.selected = true;
7290
+ sel.appendChild(opt);
7291
+ });
7292
+
7293
+ // Render [S] groups (collapsed loop reps)
7294
+ groups.forEach(g => {
7295
+ const d = new Date(g.ts || 0);
7296
+ const ts = d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'});
7297
+ const date = d.toLocaleDateString([],{month:'short',day:'numeric'});
7298
+ const _rawPrompt = (g.prompt || '').replace(/^running org:\s*/i, '').replace(/\s*rep\s+\d+.*$/i, '').trim();
7299
+ const prompt = _rawPrompt.slice(0, 28) || '(loop)';
7300
+ const repPart = g._isGroup ? ` · ${g.sessions.length} reps` : '';
7301
+ const evPart = g.totalEvents > 0 ? ` · ${g.totalEvents}ev` : '';
7302
+ const opt = document.createElement('option');
7303
+ opt.value = g.id;
7304
+ opt.textContent = `[S] ${date} ${ts} ${prompt}${_rawPrompt.length>28?'…':''}${repPart}${evPart} [${g.status||'?'}]`;
7305
+ // Selection match: group ID OR any session in the group
7306
+ if (g.id === prev || g.sessions.some(s => s.id === prev)) opt.selected = true;
7307
+ sel.appendChild(opt);
7308
+ });
7309
+
7310
+ // Update label
7311
+ const _lblEl = document.getElementById('odt-chat-sess-lbl');
7312
+ if (_lblEl) _lblEl.textContent = (hasRuns && hasSess) ? 'HISTORY' : hasRuns ? 'RUN' : 'SESSION';
7313
+ // Auto-select running entry
7314
+ if (!_odtChatCurrentId) {
7315
+ const runningRunGrp = runGroups.find(g => g.status === 'running');
7316
+ const runningSessGrp = sessGroups.find(g => g.status === 'running');
7317
+ const first = runningRunGrp || runningSessGrp || runGroups[0] || sessGroups[0];
7318
+ if (first) { sel.value = first.id; odtChatSelectSession(first.id); }
7319
+ }
7320
+ }
7321
+
7322
+ window.odtChatSelectSession = async function(id) {
7323
+ _odtChatCurrentId = id;
7324
+ _odtChatCurrentAgent = 'all';
7325
+ const feed = document.getElementById('odt-chat-feed');
7326
+ const emptyEl = document.getElementById('odt-chat-empty');
7327
+ const bar = document.getElementById('odt-chat-agent-bar');
7328
+ feed.querySelectorAll('.cv-msg, .cv-session-hdr').forEach(e => e.remove());
7329
+ if (!id) {
7330
+ if (emptyEl) emptyEl.style.display = 'block';
7331
+ if (bar) bar.style.display = 'none';
7332
+ return;
7333
+ }
7334
+
7335
+ // ── Run group path: render merged events from all runs in the group ──
7336
+ if (id.startsWith('rgrp:') && _odtRunGroups[id]) {
7337
+ const rgrp = _odtRunGroups[id];
7338
+ if (emptyEl) emptyEl.style.display = 'none';
7339
+ // Lazy-load each sub-run's events that haven't been loaded yet
7340
+ for (const r of rgrp.runs) {
7341
+ if (!r._eventsLoaded && !r._loading) {
7342
+ const _ldr = Object.assign(document.createElement('div'), { id: 'odt-chat-loading', style: 'text-align:center;padding:24px 0;font-size:11px;opacity:0.5;color:var(--text-mid)' });
7343
+ _ldr.textContent = `Loading ${rgrp.runs.length} runs…`;
7344
+ if (feed) feed.appendChild(_ldr);
7345
+ r._loading = true;
7346
+ try {
7347
+ const _dp = (typeof DIR !== 'undefined' && DIR) ? '?dir='+encodeURIComponent(DIR) : '';
7348
+ const evs = await apiFetch('/api/org/'+encodeURIComponent(_v2SelOrg)+'/runs/'+encodeURIComponent(r.id)+_dp);
7349
+ r.events = Array.isArray(evs) ? evs : (r.events || []);
7350
+ r._eventsLoaded = true;
7351
+ } catch(_) { r._eventsLoaded = true; }
7352
+ r._loading = false;
7353
+ _ldr.remove();
7354
+ if (_odtChatCurrentId !== id) return;
7355
+ }
7356
+ }
7357
+ // Re-render from scratch (all runs now loaded)
7358
+ feed.querySelectorAll('.cv-msg, .cv-session-hdr').forEach(e => e.remove());
7359
+ const mergedEvs = _odtGetMergedRunGroupEvents(rgrp);
7360
+ const _rgrpLabel = (rgrp.prompt && rgrp.prompt !== rgrp.runs[0]?.id)
7361
+ ? (rgrp.prompt || '').replace(/^running org:\s*/i, '').trim().slice(0, 80)
7362
+ : '(auto-run)';
7363
+ const _rgrpTs = rgrp.ts || 0;
7364
+ const _rgrpTsStr = _rgrpTs ? new Date(_rgrpTs).toLocaleString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}) : '—';
7365
+ const _rShdr = document.createElement('div');
7366
+ _rShdr.className = 'cv-session-hdr';
7367
+ _rShdr.innerHTML = `<div class="cv-shdr-prompt">${esc(_rgrpLabel)}</div>`+
7368
+ `<div class="cv-shdr-meta">`+
7369
+ `<span class="cv-shdr-id">${rgrp.runs.length} run${rgrp.runs.length!==1?'s':''}</span>`+
7370
+ `<span class="cv-shdr-sep">·</span>`+
7371
+ `<span>${esc(_rgrpTsStr)}</span>`+
7372
+ `<span class="cv-shdr-sep">·</span>`+
7373
+ `<span class="cv-shdr-evcount">${mergedEvs.length} events</span>`+
7374
+ `</div>`;
7375
+ feed.appendChild(_rShdr);
7376
+ const orgRoles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : [];
7377
+ _odtPopulateAgentBar(mergedEvs, orgRoles);
7378
+ mergedEvs.forEach(ev => {
7379
+ _odtAppendEvent(ev, false);
7380
+ const _hk = (ev?.ts||'')+'|'+(ev?.type||'')+'|'+(ev?.runId||ev?.session||'')+'|'+(ev?.from||ev?.role||ev?.agent||'')+'|'+(ev?.result||ev?.msg||ev?.message||ev?.summary||'').slice(0,20);
7381
+ _odtChatSeenKeys.add(_hk);
7382
+ });
7383
+ const ex = document.getElementById('odt-chat-excerpt');
7384
+ if (ex && mergedEvs.length) renderCvExcerptBanner('odt-chat-excerpt', mergedEvs, rgrp);
7385
+ if (!feed.querySelector('.cv-msg') && emptyEl) { emptyEl.style.display = 'block'; emptyEl.textContent = 'No events yet.'; }
7386
+ feed.scrollTop = feed.scrollHeight;
7387
+ return;
7388
+ }
7389
+
7390
+ // ── Group path: render merged events from all sessions in the group ──
7391
+ if (id.startsWith('grp:') && _odtSessionGroups[id]) {
7392
+ const grp = _odtSessionGroups[id];
7393
+ if (emptyEl) emptyEl.style.display = 'none';
7394
+ // Lazy-load each session's events that haven't been loaded yet
7395
+ for (const s of grp.sessions) {
7396
+ if (!s._eventsLoaded) {
7397
+ const _fbLoader = Object.assign(document.createElement('div'), { id: 'odt-chat-loading', style: 'text-align:center;padding:24px 0;font-size:11px;opacity:0.5;color:var(--text-mid)' });
7398
+ _fbLoader.textContent = `Loading ${grp.sessions.length} loop sessions…`;
7399
+ if (feed) feed.appendChild(_fbLoader);
7400
+ s._loading = true;
7401
+ try {
7402
+ const _fbData = await apiFetch('/api/mastermind/session/'+encodeURIComponent(s.id));
7403
+ s.events = Array.isArray(_fbData.events) ? _fbData.events : (s.events || []);
7404
+ s._eventsLoaded = true;
7405
+ } catch(_) { s._eventsLoaded = true; }
7406
+ s._loading = false;
7407
+ _fbLoader.remove();
7408
+ if (_odtChatCurrentId !== id) return;
7409
+ break; // Load one at a time; subsequent repaint will finish others
7410
+ }
7411
+ }
7412
+ const mergedEvs = _odtGetMergedGroupEvents(grp);
7413
+ const _normKey = (grp.prompt || '').replace(/^running org:\s*/i, '').replace(/\s*rep\s+\d+.*$/i, '').trim().slice(0, 80) || id;
7414
+ const _shdrTs = grp.ts || 0;
7415
+ const _shdrTsStr = _shdrTs ? new Date(_shdrTs).toLocaleString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}) : '—';
7416
+ const _shdr = document.createElement('div');
7417
+ _shdr.className = 'cv-session-hdr';
7418
+ _shdr.innerHTML = `<div class="cv-shdr-prompt">${esc(_normKey)}</div>`+
7419
+ `<div class="cv-shdr-meta">`+
7420
+ `<span class="cv-shdr-id" title="${esc(id)}">${grp.sessions.length} reps</span>`+
7421
+ `<span class="cv-shdr-sep">·</span>`+
7422
+ `<span>${esc(_shdrTsStr)}</span>`+
7423
+ `<span class="cv-shdr-sep">·</span>`+
7424
+ `<span class="cv-shdr-evcount">${mergedEvs.length} events</span>`+
7425
+ `</div>`;
7426
+ feed.appendChild(_shdr);
7427
+ const orgRoles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : [];
7428
+ _odtPopulateAgentBar(mergedEvs, orgRoles);
7429
+ mergedEvs.forEach(ev => {
7430
+ _odtAppendEvent(ev, false);
7431
+ const _hk = (ev?.ts||'')+'|'+(ev?.type||'')+'|'+(ev?.runId||ev?.session||'')+'|'+(ev?.from||ev?.role||ev?.agent||'')+'|'+(ev?.result||ev?.msg||ev?.message||ev?.summary||'').slice(0,20);
7432
+ _odtChatSeenKeys.add(_hk);
7433
+ });
7434
+ const ex = document.getElementById('odt-chat-excerpt');
7435
+ if (ex && mergedEvs.length) renderCvExcerptBanner('odt-chat-excerpt', mergedEvs, grp);
7436
+ if (!feed.querySelector('.cv-msg') && emptyEl) { emptyEl.style.display = 'block'; emptyEl.textContent = 'No events yet.'; }
7437
+ feed.scrollTop = feed.scrollHeight;
7438
+ return;
7439
+ }
7440
+
7441
+ const sess = _odtChatSessions.find(s => s.id === id);
7442
+ if (!sess) return;
7443
+ if (emptyEl) emptyEl.style.display = 'none';
7444
+ // Lazy-load events for fallback (non-_isOrgRun) sessions from /api/mastermind/session/:id
7445
+ if (!sess._isOrgRun && !sess._eventsLoaded) {
7446
+ const _fbFeed = document.getElementById('odt-chat-feed');
7447
+ const _fbLoader = Object.assign(document.createElement('div'), { id: 'odt-chat-loading', style: 'text-align:center;padding:24px 0;font-size:11px;opacity:0.5;color:var(--text-mid)' });
7448
+ _fbLoader.textContent = 'Loading session events…';
7449
+ if (_fbFeed) _fbFeed.appendChild(_fbLoader);
7450
+ const _fbPreLoad = (sess.events || []).slice();
7451
+ sess._loading = true;
7452
+ try {
7453
+ const _fbData = await apiFetch('/api/mastermind/session/'+encodeURIComponent(id));
7454
+ const _fbSseDuring = (sess.events || []).filter(e => !_fbPreLoad.find(p => p === e));
7455
+ sess.events = Array.isArray(_fbData.events) ? _fbData.events : _fbPreLoad;
7456
+ _fbSseDuring.forEach(e => {
7457
+ if (!sess.events.find(s => s.ts === e.ts && s.type === e.type && (s.from||'') === (e.from||''))) sess.events.push(e);
7458
+ });
7459
+ sess._eventsLoaded = true;
7460
+ } catch(_) {
7461
+ sess._eventsLoaded = true;
7462
+ }
7463
+ sess._loading = false;
7464
+ // Sync back — _odtLoadChatSessions may have replaced the object reference during the await
7465
+ const _fbLive = _odtChatSessions.find(s => s.id === id);
7466
+ if (_fbLive && _fbLive !== sess) { _fbLive._eventsLoaded = true; _fbLive._loading = false; _fbLive.events = sess.events; }
7467
+ _fbLoader.remove();
7468
+ if (_odtChatCurrentId !== id) return;
7469
+ }
7470
+ // Lazy-load events for org run sessions
7471
+ if (sess._isOrgRun && !sess._eventsLoaded) {
7472
+ const feed2 = document.getElementById('odt-chat-feed');
7473
+ const loadingEl = Object.assign(document.createElement('div'), { id: 'odt-chat-loading', style: 'text-align:center;padding:24px 0;font-size:11px;opacity:0.5;color:var(--text-mid)' });
7474
+ loadingEl.textContent = 'Loading run events…';
7475
+ if (feed2) feed2.appendChild(loadingEl);
7476
+ // Guard SSE handler: prevent it from appending to the feed while fetch is in-flight.
7477
+ // Without this, SSE events get appended immediately AND replayed by the bulk render
7478
+ // below (since they were also pushed to sess.events) → duplicate messages.
7479
+ const _preLoadEvents = (sess.events || []).slice();
7480
+ sess._loading = true;
7481
+ try {
7482
+ const dirParam = (typeof DIR !== 'undefined' && DIR) ? '?dir='+encodeURIComponent(DIR) : '';
7483
+ const evs = await apiFetch('/api/org/'+encodeURIComponent(_v2SelOrg)+'/runs/'+encodeURIComponent(id)+dirParam);
7484
+ // Merge: SSE events pushed to sess.events DURING the fetch are not in the API response
7485
+ // (they haven't been flushed to disk yet). Re-attach them so they aren't overwritten.
7486
+ const _sseDuringLoad = (sess.events || []).filter(e => !_preLoadEvents.find(p => p === e));
7487
+ sess.events = Array.isArray(evs) ? evs : _preLoadEvents;
7488
+ _sseDuringLoad.forEach(e => {
7489
+ if (!sess.events.find(s => s.ts === e.ts && s.type === e.type && (s.from||'') === (e.from||''))) sess.events.push(e);
7490
+ });
7491
+ sess._eventsLoaded = true;
7492
+ } catch(_) {
7493
+ // On failure keep whatever was buffered so SSE events aren't lost
7494
+ sess._eventsLoaded = true;
7495
+ }
7496
+ sess._loading = false;
7497
+ // Sync back — _odtLoadChatSessions may have replaced the object reference during the await
7498
+ const _runLive = _odtChatSessions.find(s => s.id === id);
7499
+ if (_runLive && _runLive !== sess) { _runLive._eventsLoaded = true; _runLive._loading = false; _runLive.events = sess.events; }
7500
+ loadingEl.remove();
7501
+ if (_odtChatCurrentId !== id) return; // user switched sessions during fetch
7502
+ }
7503
+ // Agent pills from org config roles (authoritative list, not derived from events)
7504
+ const orgRoles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : [];
7505
+ _odtPopulateAgentBar(sess.events || [], orgRoles);
7506
+ // Inject session metadata header for session-based (non-_isOrgRun) sessions so the
7507
+ // user can see prompt, ID, start time, and status without opening a separate tab.
7508
+ // Org run sessions already get a summary header from the dropdown label + run meta.
7509
+ const ex = document.getElementById('odt-chat-excerpt');
7510
+ if (!sess._isOrgRun) {
7511
+ const _shdrTs = sess.ts || sess.startedAt || 0;
7512
+ const _shdrTsStr = _shdrTs ? new Date(_shdrTs).toLocaleString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}) : '—';
7513
+ const _shdrPrompt = (sess.prompt || '').replace(/^running org:\s*/i, '').slice(0, 100) || id;
7514
+ const _shdrSt = sess.status || 'unknown';
7515
+ const _shdrStCls = _shdrSt === 'running' ? 'color:oklch(65% 0.20 145)' : _shdrSt === 'complete' ? 'color:var(--text-lo)' : 'color:var(--text-xs)';
7516
+ const _shdr = document.createElement('div');
7517
+ _shdr.className = 'cv-session-hdr';
7518
+ _shdr.innerHTML = `<div class="cv-shdr-prompt">${esc(_shdrPrompt)}</div>`+
7519
+ `<div class="cv-shdr-meta">`+
7520
+ `<span class="cv-shdr-id" title="${esc(id)}">${esc(id.slice(0,22))}</span>`+
7521
+ `<span class="cv-shdr-sep">·</span>`+
7522
+ `<span style="${_shdrStCls}">${esc(_shdrSt)}</span>`+
7523
+ `<span class="cv-shdr-sep">·</span>`+
7524
+ `<span>${esc(_shdrTsStr)}</span>`+
7525
+ `<span class="cv-shdr-sep">·</span>`+
7526
+ `<span class="cv-shdr-evcount">${(sess.events || []).length} events</span>`+
7527
+ `</div>`;
7528
+ feed.appendChild(_shdr);
7529
+ // Populate excerpt banner to show last activity for historical sessions
7530
+ if (ex) { if ((sess.events || []).length) renderCvExcerptBanner('odt-chat-excerpt', sess.events, sess); else ex.innerHTML = ''; }
7531
+ } else {
7532
+ // Org run sessions: populate excerpt with last activity (mirrors non-_isOrgRun path).
7533
+ // Previous code always cleared this, leaving the banner permanently blank for org runs.
7534
+ if (ex && (sess.events || []).length > 0) renderCvExcerptBanner('odt-chat-excerpt', sess.events, sess);
7535
+ else if (ex) ex.innerHTML = '';
7536
+ }
7537
+ (sess.events || []).forEach(ev => {
7538
+ _odtAppendEvent(ev, false);
7539
+ // Pre-seed dedup set so SSE reconnect replays of these historical events are swallowed
7540
+ // rather than appended again. Without this, a reconnect replays ~50 events from the server
7541
+ // and they all pass _odtChatSeenKeys (which only tracks events that arrived via SSE).
7542
+ const _hk = (ev?.ts||'')+'|'+(ev?.type||'')+'|'+(ev?.runId||ev?.session||'')+'|'+(ev?.from||ev?.role||ev?.agent||'')+'|'+(ev?.result||ev?.msg||ev?.message||ev?.summary||'').slice(0,20);
7543
+ _odtChatSeenKeys.add(_hk);
7544
+ });
7545
+ // For [S] sessions (mastermind wrappers that triggered an org run), show a link card to the
7546
+ // corresponding [R] run so users can navigate there directly. The runId is always
7547
+ // "run-" + same timestamp suffix as the session ID (e.g. mm-20260617T235959 → run-20260617T235959).
7548
+ if (!sess._isOrgRun) {
7549
+ const _linkedRunId = 'run-' + (sess.id || '').replace(/^mm-/, '');
7550
+ const _linkedRun = _odtChatSessions.find(s => s._isOrgRun && s.id === _linkedRunId);
7551
+ if (_linkedRun) {
7552
+ const _linkEl = document.createElement('div');
7553
+ _linkEl.className = 'cv-msg cv-sys';
7554
+ const _lrGoal = (_linkedRun.prompt || '').slice(0, 60) || _linkedRunId;
7555
+ _linkEl.innerHTML = `<div class="cv-bub" style="cursor:pointer;border-left:2px solid var(--accent);background:oklch(15% 0.05 220)">`+
7556
+ `<span class="cv-etype" style="color:var(--accent)">RUN</span>`+
7557
+ `<span class="cv-text">→ ${esc(_lrGoal)}${_lrGoal.length >= 60 ? '…' : ''}</span>`+
7558
+ `<span class="cv-ts" style="color:var(--accent)">[${esc(_linkedRun.status||'?')}]</span></div>`;
7559
+ _linkEl.title = 'Click to view the org run triggered by this session';
7560
+ _linkEl.onclick = () => {
7561
+ const _lSel = document.getElementById('odt-chat-sel');
7562
+ if (_lSel) { _lSel.value = _linkedRunId; odtChatSelectSession(_linkedRunId); }
7563
+ };
7564
+ feed.appendChild(_linkEl);
7565
+ if (emptyEl) emptyEl.style.display = 'none';
7566
+ }
7567
+ }
7568
+ // If nothing was rendered (empty run or all events unknown type), show a clear empty state
7569
+ // instead of leaving the user with a blank white feed and no feedback.
7570
+ if (!feed.querySelector('.cv-msg') && emptyEl) {
7571
+ emptyEl.style.display = 'block';
7572
+ emptyEl.textContent = (sess.events && sess.events.length)
7573
+ ? 'No displayable events.'
7574
+ : 'No events yet.';
7575
+ }
7576
+ feed.scrollTop = feed.scrollHeight;
7577
+ };
7578
+
7579
+ function _odtPopulateAgentBar(events, orgRoles) {
7580
+ const bar = document.getElementById('odt-chat-agent-bar');
7581
+ if (!bar) return;
7582
+ const agentSet = new Set();
7583
+ // Org config roles are the authoritative agent list — always use them when available
7584
+ const roles = Array.isArray(orgRoles) ? orgRoles : (Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : []);
7585
+ roles.forEach(r => { const n = typeof r === 'string' ? r : (r.id || r.role || r.name || ''); if (n) agentSet.add(n); });
7586
+ // For runs without org config, derive from events (agent:online events are reliable)
7587
+ if (!agentSet.size) {
7588
+ (events || []).forEach(ev => {
7589
+ if (ev.type === 'org:agent:online' && ev.role) agentSet.add(ev.role);
7590
+ else if (ev.type === 'org:comms') {
7591
+ // Support both ev.from (new format) and ev.role (old format) as sender
7592
+ const _sf = ev.from || ev.role || '';
7593
+ if (_sf && _sf.length < 40) agentSet.add(_sf);
7594
+ if (ev.to && ev.to !== 'all' && ev.to.length < 40) agentSet.add(ev.to);
7595
+ }
7596
+ });
7597
+ }
7598
+ // Count messages per agent so pills can show activity level
7599
+ const msgCount = {};
7600
+ (events || []).forEach(ev => {
7601
+ if (ev.type === 'org:comms' || ev.type === 'agent:message') {
7602
+ const sender = ev.from || ev.role || ev.agent || '';
7603
+ if (sender) msgCount[sender] = (msgCount[sender] || 0) + 1;
7604
+ }
7605
+ });
7606
+ const agents = [...agentSet].sort();
7607
+ if (!agents.length) { bar.style.display = 'none'; return; }
7608
+ bar.style.display = 'flex';
7609
+ bar.innerHTML = '<span id="odt-chat-agent-lbl">AGENT</span>';
7610
+ ['all', ...agents].forEach(name => {
7611
+ const pill = document.createElement('button');
7612
+ const cnt = name === 'all' ? null : (msgCount[name] || 0);
7613
+ const active = _odtChatCurrentAgent === name;
7614
+ pill.className = 'odt-agent-pill' + (active ? ' active' : '') + (cnt === 0 ? ' dim' : '');
7615
+ pill.textContent = name === 'all' ? 'ALL' : (cnt > 0 ? `${name} (${cnt})` : name);
7616
+ pill.dataset.agent = name;
7617
+ pill.title = name === 'all' ? 'Show all events' : (cnt > 0 ? `${cnt} message${cnt!==1?'s':''} from ${name}` : `${name} was online but sent no messages`);
7618
+ pill.onclick = () => odtChatSelectAgent(name);
7619
+ bar.appendChild(pill);
7620
+ });
7621
+ }
7622
+
7623
+ window.odtChatSelectAgent = function(name) {
7624
+ _odtChatCurrentAgent = name;
7625
+ document.querySelectorAll('#odt-chat-agent-bar .odt-agent-pill').forEach(p => {
7626
+ p.classList.toggle('active', p.dataset.agent === name);
7627
+ });
7628
+ const feed = document.getElementById('odt-chat-feed');
7629
+ const emptyEl = document.getElementById('odt-chat-empty');
7630
+ const sess = _odtChatSessions.find(s => s.id === _odtChatCurrentId);
7631
+ feed.querySelectorAll('.cv-msg').forEach(e => e.remove());
7632
+ if (!sess) { if (emptyEl) emptyEl.style.display = 'block'; return; }
7633
+ const visible = (sess.events || []).filter(ev => _odtChatAgentMatches(ev));
7634
+ if (!visible.length) {
7635
+ if (emptyEl) { emptyEl.style.display = 'block'; emptyEl.textContent = name === 'all' ? 'No events.' : `${name} was online in this run but sent no messages.`; }
7636
+ } else {
7637
+ if (emptyEl) emptyEl.style.display = 'none';
7638
+ visible.forEach(ev => _odtAppendEvent(ev, false));
7639
+ }
7640
+ if (!sess._isOrgRun) {
7641
+ const _linkedRunId = 'run-' + (sess.id || '').replace(/^mm-/, '');
7642
+ const _linkedRun = _odtChatSessions.find(s => s._isOrgRun && s.id === _linkedRunId);
7643
+ if (_linkedRun) {
7644
+ const _linkEl = document.createElement('div');
7645
+ _linkEl.className = 'cv-msg cv-sys';
7646
+ const _lrGoal = (_linkedRun.prompt || '').slice(0, 60) || _linkedRunId;
7647
+ _linkEl.innerHTML = `<div class="cv-bub" style="cursor:pointer;border-left:2px solid var(--accent);background:oklch(15% 0.05 220)">`+
7648
+ `<span class="cv-etype" style="color:var(--accent)">RUN</span>`+
7649
+ `<span class="cv-text">→ ${esc(_lrGoal)}${_lrGoal.length >= 60 ? '…' : ''}</span>`+
7650
+ `<span class="cv-ts" style="color:var(--accent)">[${esc(_linkedRun.status||'?')}]</span></div>`;
7651
+ _linkEl.title = 'Click to view the org run triggered by this session';
7652
+ _linkEl.onclick = () => {
7653
+ const _lSel = document.getElementById('odt-chat-sel');
7654
+ if (_lSel) { _lSel.value = _linkedRunId; odtChatSelectSession(_linkedRunId); }
7655
+ };
7656
+ feed.appendChild(_linkEl);
7657
+ if (emptyEl) emptyEl.style.display = 'none';
7658
+ }
7659
+ }
7660
+ feed.scrollTop = feed.scrollHeight;
7661
+ };
7662
+
7663
+ function _odtAppendEvent(ev, animate) {
7664
+ if (!_odtChatAgentMatches(ev)) return;
7665
+ const feed = document.getElementById('odt-chat-feed');
7666
+ if (!feed) return;
7667
+ // Capture scroll position BEFORE appending: only auto-scroll for live events when
7668
+ // the user is already at (or near) the bottom. If they scrolled up to read earlier
7669
+ // messages a new event must not forcibly jump them back to the bottom.
7670
+ const atBottom = !animate || (feed.scrollHeight - feed.scrollTop - feed.clientHeight < 80);
7671
+ const emptyEl = document.getElementById('odt-chat-empty');
7672
+ if (emptyEl) emptyEl.style.display = 'none';
7673
+ const ts = ev.ts ? new Date(ev.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
7674
+ let el;
7675
+ if (ev.type === 'run:start') {
7676
+ el = mkCVSys('▶ Run started' + (ev.goal ? ': ' + esc(ev.goal.slice(0,80)) : '') + (ev.bossRole ? ' · boss: ' + esc(ev.bossRole) : ''), ts);
7677
+ } else if (ev.type === 'run:cycle:complete') {
7678
+ el = mkCVSys('◎ Cycle complete' + (ev.pending != null ? ' · ' + esc(String(ev.pending)) + ' tasks pending' : ''), ts);
7679
+ } else if (ev.type === 'run:complete') {
7680
+ el = mkCVSys('■ Run complete' + (ev.status ? ' [' + esc(ev.status) + ']' : ''), ts);
7681
+ } else if (ev.type === 'org:comms') {
7682
+ // Some older runs use ev.role instead of ev.from — normalise before rendering
7683
+ const _commsFrom = ev.from || ev.role || 'boss';
7684
+ if (ev.to && ev.to !== 'all') el = mkCVIntercom(_commsFrom, ev.to, ev.msg || '', ts);
7685
+ else el = mkCVAgent(_commsFrom, ev.msg || '', ts, 'org:comms');
7686
+ } else if (ev.type === 'org:agent:online') {
7687
+ el = mkCVSys('◉ ' + esc(ev.title || ev.role || '?') + ' online', ts);
7688
+ } else if (ev.type === 'org:checkpoint') {
7689
+ el = mkCVAgent('boss', ev.summary || ev.progress || ev.msg || '', ts, 'org:checkpoint');
7690
+ } else if (ev.type === 'org:start' || ev.type === 'org:complete') {
7691
+ el = mkCVSys(ev.type === 'org:start' ? '▶ Org started' : '■ Org complete', ts);
7692
+ } else if (ev.type === 'agent:spawn') {
7693
+ el = mkCVSpawn(ev.from || 'orchestrator', ev.to || '?', ev.task || ev.briefing || '', ts);
7694
+ } else if (ev.type === 'agent:result') {
7695
+ el = mkCVResult(ev.agent || ev.from || '?', ev.result || ev.msg || ev.message || ev.summary || '', ts);
7696
+ } else if (ev.type === 'file:write') {
7697
+ el = mkCVFileCard(ev.name || ev.path || '?', ev.path || '', ev.size || 0, ts);
7698
+ } else if (ev.type === 'intercom') {
7699
+ el = mkCVIntercom(ev.from, ev.to, ev.msg || '', ts);
7700
+ } else if (ev.type === 'agent:message') {
7701
+ el = mkCVAgent(ev.agent || ev.name || '?', ev.msg || ev.message || '', ts, ev.type);
7702
+ } else if (ev.type === 'session:start') {
7703
+ el = mkCVSys('▶ ' + esc((ev.prompt || '').replace(/^running org:\s*/i,'').slice(0,80) || 'Session started'), ts);
7704
+ } else if (ev.type === 'session:complete') {
7705
+ el = mkCVSys('■ Complete' + (ev.status ? ' — ' + esc(ev.status) : ''), ts);
7706
+ } else if (ev.type === 'domain:dispatch') {
7707
+ el = mkCVSys('→ ' + esc(ev.domain || '') + (ev.cmd ? ': ' + esc(ev.cmd.slice(0,80)) : ''), ts);
7708
+ } else if (ev.type === 'domain:complete') {
7709
+ el = mkCVSys('✓ ' + esc(ev.domain || '') + (ev.status ? ' [' + esc(ev.status) + ']' : ''), ts);
7710
+ } else if (ev.type === 'loop:start') {
7711
+ el = mkCVSys('Loop: ' + esc(ev.command || ''), ts);
7712
+ } else if (ev.type === 'loop:complete') {
7713
+ el = mkCVSys('Loop done: ' + esc(ev.command || '') + (ev.ranReps ? ' (' + ev.ranReps + ' runs)' : ''), ts);
7714
+ } else if (ev.type === 'loop:tick') {
7715
+ const _trep = ev.rep ?? ev.fromRep;
7716
+ const _tnext = ev.nextRep ?? ev.toRep;
7717
+ const tickLabel = _trep != null
7718
+ ? 'rep ' + esc(String(_trep)) + ' done' + (_tnext != null ? ' → rep ' + esc(String(_tnext)) + (ev.wait != null ? ' in ' + esc(String(ev.wait)) + 's' : '') : '')
7719
+ : esc(ev.command || ev.loopId || '');
7720
+ el = mkCVSys('◷ ' + tickLabel, ts);
7721
+ el.dataset.evType = 'loop:tick';
7722
+ // Collapse: replace the previous loop:tick if it's the last element — prevents the
7723
+ // feed from flooding with one entry per rep in long-running loops (e.g. --repeat 9999).
7724
+ if (feed.lastElementChild?.dataset?.evType === 'loop:tick') feed.lastElementChild.remove();
7725
+ } else if (ev.type === 'loop:hil' || ev.type === 'loop:hil:waiting') {
7726
+ el = mkCVSys('⚠ Loop HIL: ' + esc(ev.command || ev.loopId || ''), ts);
7727
+ } else if (ev.type === 'loop:hil:resolved') {
7728
+ el = mkCVSys('✓ Loop HIL resolved: ' + esc(ev.loopId || ''), ts);
7729
+ } else if (ev.type === 'org:error') {
7730
+ el = mkCVSys('⚠ Error: ' + esc(ev.msg || ev.error || ev.message || ''), ts);
7731
+ el.classList.add('cv-err');
7732
+ } else if (ev.type === 'org:agent:offline') {
7733
+ el = mkCVSys('◌ ' + esc(ev.title || ev.role || '?') + ' offline', ts);
7734
+ } else {
7735
+ el = mkCVSys(esc(ev.type || 'event'), ts);
7736
+ }
7737
+ if (animate) el.classList.add('cv-new');
7738
+ feed.appendChild(el);
7739
+ if (atBottom) feed.scrollTop = feed.scrollHeight;
7740
+ }
7741
+
7742
+ function _odtConnectChatSSE() {
7743
+ if (_odtChatSseSource) return;
7744
+ const dot = document.getElementById('odt-chat-live-dot');
7745
+ const lbl = document.getElementById('odt-chat-live-lbl');
7746
+ _odtChatSseSource = new EventSource('/api/mastermind-stream');
7747
+ _odtChatSseSource.onopen = () => { dot?.classList.add('on'); if (lbl) lbl.textContent = 'LIVE'; };
7748
+ _odtChatSseSource.onmessage = e => {
7749
+ try { _odtHandleLiveEvent(JSON.parse(e.data)); } catch(_) {}
7750
+ };
7751
+ _odtChatSseSource.onerror = () => {
7752
+ dot?.classList.remove('on');
7753
+ if (lbl) lbl.textContent = 'OFFLINE';
7754
+ // Close before nulling: EventSource has built-in auto-retry, so without close()
7755
+ // the browser keeps reconnecting the old source. We'd then create a second one
7756
+ // via setTimeout → two SSE connections delivering the same events simultaneously.
7757
+ const _src = _odtChatSseSource;
7758
+ _odtChatSseSource = null;
7759
+ if (_src) _src.close();
7760
+ setTimeout(_odtConnectChatSSE, 5000);
7761
+ };
7762
+ }
7763
+
7764
+ function _odtHandleLiveEvent(ev) {
7765
+ // Deduplicate SSE reconnect replays (server replays last 50 events on every reconnect)
7766
+ const _odtDedupKey = (ev?.ts||'') + '|' + (ev?.type||'') + '|' + (ev?.runId||ev?.session||'') + '|' + (ev?.from||ev?.role||ev?.agent||'') + '|' + (ev?.result||ev?.msg||ev?.message||ev?.summary||'').slice(0,20);
7767
+ if (_odtChatSeenKeys.has(_odtDedupKey)) return;
7768
+ _odtChatSeenKeys.add(_odtDedupKey);
7769
+ // Cap the dedup Set to prevent unbounded memory growth in long-running sessions.
7770
+ // Server replays only the last ~50 events on reconnect, so pruning entries older
7771
+ // than 1000 events never risks a false-pass through the dedup check.
7772
+ if (_odtChatSeenKeys.size > 2000) {
7773
+ const _arr = [..._odtChatSeenKeys];
7774
+ _odtChatSeenKeys = new Set(_arr.slice(1000));
7775
+ }
7776
+ // Route org run events (have runId + org) to the active run session
7777
+ if (ev?.org && ev?.runId && ev.org === _v2SelOrg) {
7778
+ let runSess = _odtChatSessions.find(s => s.id === ev.runId);
7779
+ if (!runSess) {
7780
+ // Create a session placeholder for any unknown runId so SSE events aren't dropped
7781
+ // while _odtLoadChatSessions fetch is still in-flight (opening Chat tab mid-run).
7782
+ // For run:start: full setup + auto-switch. For other types: minimal placeholder
7783
+ // with _eventsLoaded:false so lazy-load fetches full history when selected.
7784
+ runSess = { id: ev.runId, ts: ev.ts || Date.now(), prompt: ev.goal || '', status: 'running', _isOrgRun: true, _eventsLoaded: ev.type === 'run:start', _runMeta: { cycleCount: 0, eventCount: 1 }, events: [ev] };
7785
+ _odtChatSessions.unshift(runSess);
7786
+ // Always update the dropdown so the run is selectable even when opened mid-run
7787
+ _odtPopulateChatSel();
7788
+ if (ev.type === 'run:start') {
7789
+ // After _odtPopulateChatSel above rebuilt groups, check if the new run joined
7790
+ // the currently-selected group. If so, keep the group selected. Otherwise switch.
7791
+ const _newGrpId = _odtRunGroupMap[ev.runId];
7792
+ const _alreadySelected = _newGrpId && _odtChatCurrentId === _newGrpId;
7793
+ if (!_alreadySelected) {
7794
+ const _rsSel = document.getElementById('odt-chat-sel');
7795
+ // Switch to the run's group if it has one, otherwise to the individual run
7796
+ const _switchId = _newGrpId || ev.runId;
7797
+ if (_rsSel) { _rsSel.value = _switchId; odtChatSelectSession(_switchId); }
7798
+ }
7799
+ }
7800
+ } else if (runSess) {
7801
+ (runSess.events = runSess.events || []).push(ev);
7802
+ if (runSess._runMeta) runSess._runMeta.eventCount = (runSess._runMeta.eventCount || 0) + 1;
7803
+ if (ev.type === 'run:cycle:complete' || ev.type === 'org:checkpoint') { if (runSess._runMeta) runSess._runMeta.cycleCount = (runSess._runMeta.cycleCount || 0) + 1; _odtPopulateChatSel(); }
7804
+ // Backfill prompt from first boss directive when run:start had no goal field
7805
+ if (!runSess.prompt && ev.type === 'org:comms' && (ev.from === 'boss' || ev.role === 'boss') && ev.msg) {
7806
+ runSess.prompt = ev.msg.slice(0, 80);
7807
+ _odtPopulateChatSel();
7808
+ }
7809
+ if (ev.type === 'run:complete' || ev.type === 'org:complete') { runSess.status = 'complete'; _odtPopulateChatSel(); }
7810
+ // Check if this run's group (or the run itself) is currently selected
7811
+ const _evRunGrpId = _odtRunGroupMap[ev.runId];
7812
+ const _isSelRun = _odtChatCurrentId === ev.runId;
7813
+ const _isSelRunGrp = _evRunGrpId && _odtChatCurrentId === _evRunGrpId;
7814
+ if ((_isSelRun || _isSelRunGrp) && !runSess._loading) {
7815
+ const hasNewRunAgent = (ev.role && !document.querySelector(`#odt-chat-agent-bar [data-agent="${CSS.escape(ev.role)}"]`))
7816
+ || (ev.from && !document.querySelector(`#odt-chat-agent-bar [data-agent="${CSS.escape(ev.from)}"]`))
7817
+ || (ev.to && ev.to !== 'all' && !document.querySelector(`#odt-chat-agent-bar [data-agent="${CSS.escape(ev.to)}"]`));
7818
+ if (hasNewRunAgent) _odtPopulateAgentBar(runSess.events, Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : []);
7819
+ _odtAppendEvent(ev, true);
7820
+ renderCvExcerptBanner('odt-chat-excerpt', runSess.events, runSess);
7821
+ const _rEvCnt = document.querySelector('#odt-chat-feed .cv-session-hdr .cv-shdr-evcount');
7822
+ if (_rEvCnt) {
7823
+ const _rgrpEvs = _isSelRunGrp ? _odtGetMergedRunGroupEvents(_odtRunGroups[_evRunGrpId]) : runSess.events;
7824
+ _rEvCnt.textContent = _rgrpEvs.length + ' events';
7825
+ }
7826
+ }
7827
+ }
7828
+ return;
7829
+ }
7830
+
7831
+ if (!ev?.session) return;
7832
+ // Find existing session first — if already in our list it's been validated as belonging to
7833
+ // this org, so skip the prompt-based org check that would falsely drop events whose
7834
+ // individual payloads don't carry an ev.org field (e.g. intercom, agent:message).
7835
+ let sess = _odtChatSessions.find(s => s.id === ev.session);
7836
+ if (!sess) {
7837
+ const isOrgSess = _odtOrgSessionMatch({ id: ev.session, prompt: ev.prompt || '', events: [ev] });
7838
+ if (!isOrgSess) return;
7839
+ }
7840
+ if (!sess && ev.type === 'session:start') {
7841
+ sess = { id: ev.session, ts: ev.ts || Date.now(), prompt: ev.prompt || '', status: 'running', events: [ev] };
7842
+ _odtChatSessions.unshift(sess);
7843
+ _odtPopulateChatSel();
7844
+ // Auto-select if nothing selected (tab-guard removed: feed is in DOM regardless of active tab)
7845
+ if (!_odtChatCurrentId) {
7846
+ const sel = document.getElementById('odt-chat-sel');
7847
+ if (sel) { sel.value = ev.session; odtChatSelectSession(ev.session); }
7848
+ }
7849
+ return;
7850
+ }
7851
+ if (sess) {
7852
+ (sess.events = sess.events || []).push(ev);
7853
+ if (ev.type === 'session:complete') { sess.status = 'complete'; _odtPopulateChatSel(); }
7854
+ // Determine if a group containing this session is currently selected
7855
+ const _evGrpId = _odtSessionGroupMap[ev.session];
7856
+ const _isCurrentSess = _odtChatCurrentId === ev.session;
7857
+ const _isCurrentGrp = _evGrpId && _odtChatCurrentId === _evGrpId;
7858
+ const _notLoading = !sess._loading && (!_evGrpId || !(_odtSessionGroups[_evGrpId]?.sessions || []).some(s => s._loading));
7859
+ if ((_isCurrentSess || _isCurrentGrp) && _notLoading) {
7860
+ // sess._loading is true while odtChatSelectSession's lazy-load fetch is in-flight.
7861
+ const hasNewAgent = (ev.agent && !document.querySelector(`#odt-chat-agent-bar [data-agent="${CSS.escape(ev.agent)}"]`))
7862
+ || (ev.from && !document.querySelector(`#odt-chat-agent-bar [data-agent="${CSS.escape(ev.from)}"]`))
7863
+ || (ev.to && !document.querySelector(`#odt-chat-agent-bar [data-agent="${CSS.escape(ev.to)}"]`));
7864
+ if (hasNewAgent) _odtPopulateAgentBar(sess.events, Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : []);
7865
+ if (!sess._isJsonl) renderCvExcerptBanner('odt-chat-excerpt', sess.events, sess);
7866
+ _odtAppendEvent(ev, true);
7867
+ // Keep event count in session header current (frozen at load time otherwise)
7868
+ const _sEvCnt = document.querySelector('#odt-chat-feed .cv-session-hdr .cv-shdr-evcount');
7869
+ if (_sEvCnt) _sEvCnt.textContent = ((_isCurrentGrp ? _odtGetMergedGroupEvents(_odtSessionGroups[_evGrpId]) : sess.events).length) + ' events';
7870
+ }
7871
+ }
7872
+ }
7873
+
7874
+ // ── ORG CONFIG ─────────────────────────────────────────────
7875
+ function v2RenderOrgConfig() {
7876
+ const el = document.getElementById('odt-config');
7877
+ if (!el || !_v2OrgData || !_v2SelOrg) return;
7878
+ const d = _v2OrgData;
7879
+ const rc = d.run_config || {};
7880
+ const gov = (d.governance && typeof d.governance === 'object') ? d.governance : { policy: d.governance || 'auto' };
7881
+ const topos = ['hierarchical','hierarchical-mesh','mesh','star','ring','adaptive','hybrid'];
7882
+ const modes = ['daemon','once','scheduled'];
7883
+ const statuses = ['active','paused','archived'];
7884
+ const govPolicies = ['auto','board','strict'];
7885
+ const inp = (id,val,type,extra) => '<input class="filter-input" id="'+id+'" type="'+(type||'text')+'" value="'+esc(String(val??''))+'" '+(extra||'')+' style="width:100%;box-sizing:border-box">';
7886
+ const sel = (id,opts,cur) => '<select class="filter-input" id="'+id+'" style="width:100%;box-sizing:border-box;cursor:pointer">'+opts.map(o=>'<option'+(cur===o?' selected':'')+'>'+esc(o)+'</option>').join('')+'</select>';
7887
+ const fld = (lbl,html) => '<div style="display:flex;flex-direction:column;gap:4px"><div style="font-size:10px;color:var(--text-lo)">'+lbl+'</div>'+html+'</div>';
7888
+ const sec = (title,inner) => '<div style="margin-bottom:22px"><div style="font-size:9px;letter-spacing:2px;color:var(--text-xs);margin-bottom:10px;padding-bottom:5px;border-bottom:1px solid var(--border)">'+title+'</div><div style="display:grid;grid-template-columns:1fr 1fr;gap:11px">'+inner+'</div></div>';
7889
+ el.innerHTML = '<div style="padding:18px;overflow-y:auto;max-width:660px">'
7890
+ +sec('GENERAL',
7891
+ fld('Name', inp('oc-name', d.name||'','text','readonly style="opacity:0.5"'))
7892
+ +fld('Status', sel('oc-status', statuses, d.status||'active'))
7893
+ +'<div style="grid-column:1/-1">'+fld('Goal', '<textarea id="oc-goal" class="filter-input" rows="3" style="width:100%;box-sizing:border-box;resize:vertical;line-height:1.5">'+esc(d.goal||'')+'</textarea>')+'</div>'
7894
+ +fld('Mode', sel('oc-mode', modes, d.mode||'daemon'))
7895
+ +fld('Topology', sel('oc-topology', topos, d.topology||'hierarchical'))
7896
+ +'<div style="grid-column:1/-1">'+fld('Schedule', inp('oc-schedule', d.schedule||''))+'</div>'
7897
+ )
7898
+ +sec('RUN CONFIG',
7899
+ fld('Max Concurrent Agents', inp('oc-max-agents', rc.max_concurrent_agents??6,'number','min="1" max="32"'))
7900
+ +fld('Checkpoint Interval (min)', inp('oc-checkpoint', rc.checkpoint_interval_min??30,'number','min="5"'))
7901
+ +fld('Budget Tokens (0=unlimited)', inp('oc-budget', rc.budget_tokens??0,'number','min="0"'))
7902
+ +fld('Alert Threshold (0-1)', inp('oc-alert', rc.alert_threshold??0.8,'number','step="0.05" min="0" max="1"'))
7903
+ +fld('Boss Role ID', inp('oc-boss', rc.boss_role||''))
7904
+ +fld('Memory Namespace', inp('oc-namespace', rc.memory_namespace||('org:'+d.name)))
7905
+ +'<div style="grid-column:1/-1;display:flex;align-items:center;gap:8px;margin-top:4px"><input type="checkbox" id="oc-spawn-all" '+(rc.spawn_all_roles?'checked':'')+' style="accent-color:var(--accent);width:14px;height:14px;cursor:pointer"><label for="oc-spawn-all" style="font-size:12px;color:var(--text-mid);cursor:pointer">Spawn all roles as separate agents</label></div>'
7906
+ )
7907
+ +sec('GOVERNANCE',
7908
+ fld('Approval Policy', sel('oc-gov-policy', govPolicies, gov.policy||'auto'))
7909
+ +fld('Approvals File', inp('oc-gov-file', gov.approvals_file||('.monomind/orgs/'+d.name+'-approvals.json')))
7910
+ )
7911
+ +'<div style="display:flex;align-items:center;gap:10px"><button class="btn" style="border-color:var(--accent);color:var(--accent)" onclick="v2SaveOrgConfig()">Save Config</button><span id="oc-status-msg" style="font-size:11px;color:var(--text-lo)"></span></div>'
7912
+ +'</div>';
7913
+ }
7914
+
7915
+ async function v2SaveOrgConfig() {
7916
+ if (!_v2SelOrg || !_v2OrgData) return;
7917
+ const g = id => document.getElementById(id);
7918
+ // Only include org config fields — exclude runtime data (state, goals, routines, approvals,
7919
+ // running, tasks, config nested copy) that would corrupt the org .json file.
7920
+ const _RUNTIME_KEYS = new Set(['config','state','goals','routines','approvals','running','tasks']);
7921
+ const _baseConfig = Object.fromEntries(
7922
+ Object.entries(_v2OrgData).filter(([k]) => !k.startsWith('_') && !_RUNTIME_KEYS.has(k))
7923
+ );
7924
+ const cfg = {
7925
+ ..._baseConfig,
7926
+ status: (g('oc-status')?.value) || _v2OrgData.status,
7927
+ goal: (g('oc-goal')?.value) || _v2OrgData.goal,
7928
+ mode: (g('oc-mode')?.value) || _v2OrgData.mode,
7929
+ topology: (g('oc-topology')?.value) || _v2OrgData.topology,
7930
+ schedule: (g('oc-schedule')?.value||'').trim() || null,
7931
+ run_config: { ...(_v2OrgData.run_config||{}),
7932
+ max_concurrent_agents: parseInt(g('oc-max-agents')?.value)||6,
7933
+ checkpoint_interval_min: parseInt(g('oc-checkpoint')?.value)||30,
7934
+ budget_tokens: parseInt(g('oc-budget')?.value)||0,
7935
+ alert_threshold: parseFloat(g('oc-alert')?.value)||0.8,
7936
+ boss_role: (g('oc-boss')?.value||'').trim(),
7937
+ memory_namespace: (g('oc-namespace')?.value||'').trim()||('org:'+_v2SelOrg),
7938
+ spawn_all_roles: !!(g('oc-spawn-all')?.checked),
7939
+ },
7940
+ governance: {
7941
+ policy: (g('oc-gov-policy')?.value)||'auto',
7942
+ approvals_file: (g('oc-gov-file')?.value||'').trim()||('.monomind/orgs/'+_v2SelOrg+'-approvals.json'),
7943
+ },
7944
+ };
7945
+ const msg = document.getElementById('oc-status-msg');
7946
+ if (msg) { msg.style.color='var(--text-lo)'; msg.textContent='Saving...'; }
7947
+ try {
7948
+ const qs = (typeof DIR !== 'undefined' && DIR) ? '?dir='+encodeURIComponent(DIR) : '';
7949
+ const r = await fetch('/api/orgs'+qs, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(cfg) });
7950
+ const json = await r.json();
7951
+ if (json.ok) {
7952
+ _v2OrgData = Object.assign({}, _v2OrgData, cfg);
7953
+ if (msg) { msg.style.color='oklch(68% 0.20 150)'; msg.textContent='Saved'; setTimeout(()=>{ if(msg) msg.textContent=''; }, 2500); }
7954
+ if (typeof showToast === 'function') showToast('Config saved','','ok');
7955
+ } else { throw new Error(json.error||'Save failed'); }
7956
+ } catch(e) {
7957
+ if (msg) { msg.style.color='oklch(60% 0.22 25)'; msg.textContent='Error: '+e.message; }
7958
+ }
7959
+ }
7960
+
7961
+ // ── ORG FILES ──────────────────────────────────────────────
7962
+ async function v2RenderOrgFiles() {
7963
+ const el = document.getElementById('odt-files');
7964
+ if (!el || !_v2SelOrg) return;
7965
+ const _rendOrg = _v2SelOrg;
7966
+ el.innerHTML = '<div style="padding:20px;color:var(--text-lo)">Loading files...</div>';
7967
+ try {
7968
+ const enc = encodeURIComponent(_rendOrg);
7969
+ const qs = (typeof DIR !== 'undefined' && DIR) ? '?dir='+encodeURIComponent(DIR) : '';
7970
+ const files = await fetch('/api/org/'+enc+'/files'+qs).then(r => r.ok ? r.json() : []).catch(() => []);
7971
+ if (_v2SelOrg !== _rendOrg) return;
7972
+ if (!files.length) { el.innerHTML = '<div style="padding:20px;color:var(--text-lo)">No files found.</div>'; return; }
7973
+ const fmtSize = b => b < 1024 ? b+'B' : b < 1048576 ? (b/1024).toFixed(1)+'K' : (b/1048576).toFixed(1)+'M';
7974
+ const fmtTime = s => { const d = new Date(s); return d.toLocaleDateString([],{month:'short',day:'numeric'})+' '+d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}); };
7975
+ const typeColor = { config:'oklch(62% 0.20 186)', generated:'oklch(68% 0.20 150)', 'agent-definition':'oklch(65% 0.20 280)', approvals:'oklch(70% 0.18 60)', state:'oklch(70% 0.18 60)', goals:'oklch(70% 0.18 60)' };
7976
+ const groupOrder = ['config','generated','agent-definition','approvals','state','goals','routines','other'];
7977
+ const groups = {};
7978
+ groupOrder.forEach(k => { groups[k] = { label: k==='agent-definition'?'AGENT DEFINITIONS':k.toUpperCase().replace(/-/g,' '), items:[] }; });
7979
+ files.forEach(f => { const k = groups[f.type] ? f.type : 'other'; if (!groups[k]) groups[k]={label:f.type.toUpperCase(),items:[]}; groups[k].items.push(f); });
7980
+ const color = t => typeColor[t]||'var(--text-lo)';
7981
+ const badge = t => '<span style="font-size:9px;padding:1px 6px;border-radius:8px;border:1px solid '+color(t)+';color:'+color(t)+'">'+t+'</span>';
7982
+ el.innerHTML = '<div style="padding:16px">'+groupOrder.filter(k => groups[k]&&groups[k].items.length).map(k => {
7983
+ const g = groups[k];
7984
+ return '<div style="margin-bottom:20px"><div style="font-size:9px;letter-spacing:2px;color:var(--text-xs);margin-bottom:8px;padding-bottom:5px;border-bottom:1px solid var(--border)">'+g.label+' ('+g.items.length+')</div>'+
7985
+ '<table style="width:100%;border-collapse:collapse;font-size:12px">'+
7986
+ '<thead><tr style="color:var(--text-xs);font-size:9px;text-align:left"><th style="padding:4px 8px">File</th><th>Type</th><th>Size</th><th>Modified</th><th></th></tr></thead>'+
7987
+ '<tbody>'+g.items.map(f => {
7988
+ return '<tr style="border-top:1px solid var(--border)" data-fp="'+esc(f.path)+'" data-fn="'+esc(f.name)+'">'+
7989
+ '<td style="padding:6px 8px"><span style="font-family:var(--mono);font-size:11px;color:var(--text-hi);cursor:pointer" onclick="v2ViewOrgFile(this.closest(\'tr\').dataset.fp,this.closest(\'tr\').dataset.fn)">'+esc(f.name)+'</span></td>'+
7990
+ '<td style="padding:6px 8px">'+badge(f.type)+'</td>'+
7991
+ '<td style="padding:6px 8px;font-size:10px;color:var(--text-lo);font-family:var(--mono)">'+fmtSize(f.size||0)+'</td>'+
7992
+ '<td style="padding:6px 8px;font-size:10px;color:var(--text-lo)">'+fmtTime(f.mtime)+'</td>'+
7993
+ '<td style="padding:6px 8px"><button class="btn" style="font-size:9px;padding:2px 7px" onclick="v2ViewOrgFile(this.closest(\'tr\').dataset.fp,this.closest(\'tr\').dataset.fn)">View</button></td></tr>';
7994
+ }).join('')+'</tbody></table></div>';
7995
+ }).join('')+'</div>';
7996
+ } catch(e) { el.innerHTML = '<div style="padding:20px;color:oklch(60% 0.22 25)">Error: '+e.message+'</div>'; }
7997
+ }
7998
+
7999
+ let _ofModal = null;
8000
+ let _ofRawText = '';
8001
+
8002
+ function _ofMdToHtml(md) {
8003
+ return md
8004
+ .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
8005
+ .replace(/^#{6}\s+(.+)$/gm,'<h6 style="font-size:11px;font-weight:700;color:var(--text-hi);margin:10px 0 4px">$1</h6>')
8006
+ .replace(/^#{5}\s+(.+)$/gm,'<h5 style="font-size:12px;font-weight:700;color:var(--text-hi);margin:10px 0 4px">$1</h5>')
8007
+ .replace(/^#{4}\s+(.+)$/gm,'<h4 style="font-size:13px;font-weight:700;color:var(--text-hi);margin:12px 0 5px">$1</h4>')
8008
+ .replace(/^#{3}\s+(.+)$/gm,'<h3 style="font-size:14px;font-weight:700;color:var(--text-hi);margin:14px 0 6px">$1</h3>')
8009
+ .replace(/^#{2}\s+(.+)$/gm,'<h2 style="font-size:16px;font-weight:700;color:var(--accent);margin:16px 0 7px;padding-bottom:4px;border-bottom:1px solid var(--border)">$1</h2>')
8010
+ .replace(/^#{1}\s+(.+)$/gm,'<h1 style="font-size:18px;font-weight:700;color:var(--accent);margin:18px 0 8px;padding-bottom:5px;border-bottom:1px solid var(--border)">$1</h1>')
8011
+ .replace(/```[\w]*\n?([\s\S]*?)```/g,'<pre style="background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:10px 12px;font-size:10px;overflow-x:auto;margin:8px 0;line-height:1.6">$1</pre>')
8012
+ .replace(/`([^`]+)`/g,'<code style="background:var(--surface);border:1px solid var(--border);border-radius:3px;padding:1px 5px;font-size:10px;font-family:var(--mono)">$1</code>')
8013
+ .replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')
8014
+ .replace(/\*(.+?)\*/g,'<em>$1</em>')
8015
+ .replace(/^---+$/gm,'<hr style="border:none;border-top:1px solid var(--border);margin:12px 0">')
8016
+ .replace(/^\s*[-*]\s+(.+)$/gm,'<li style="margin:2px 0;padding-left:4px">$1</li>')
8017
+ .replace(/(<li.*<\/li>\n?)+/g,'<ul style="padding-left:18px;margin:6px 0">$&</ul>')
8018
+ .replace(/^\d+\.\s+(.+)$/gm,'<li style="margin:2px 0;padding-left:4px">$1</li>')
8019
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2" style="color:var(--accent);text-decoration:underline" target="_blank" rel="noopener">$1</a>')
8020
+ .replace(/\n\n+/g,'</p><p style="margin:0 0 8px">')
8021
+ .replace(/^(?!<[hupoli]|<pre|<hr)(.+)$/gm, '$1')
8022
+ .replace(/\n/g,'<br>');
8023
+ }
8024
+
8025
+ async function v2ViewOrgFile(filePath, fileName) {
8026
+ if (_ofModal) { _ofModal.remove(); _ofModal = null; }
8027
+ const isMd = /\.(md|markdown)$/i.test(fileName);
8028
+ const isJson = /\.json$/i.test(fileName);
8029
+ _ofRawText = '';
8030
+ _ofModal = document.createElement('div');
8031
+ _ofModal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px;box-sizing:border-box';
8032
+ _ofModal.onclick = e => { if (e.target===_ofModal) { _ofModal.remove(); _ofModal=null; } };
8033
+ _ofModal.innerHTML =
8034
+ '<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;max-width:900px;width:100%;max-height:88vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 8px 40px rgba(0,0,0,0.6)">' +
8035
+ '<div style="display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid var(--border);flex-shrink:0">' +
8036
+ '<span style="font-family:var(--mono);font-size:11px;color:var(--text-hi);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="'+esc(filePath)+'">'+esc(fileName)+'</span>' +
8037
+ (isMd ? '<button id="of-toggle" class="btn" style="font-size:9px;padding:2px 7px" onclick="_ofToggleRender()">Raw</button>' : '') +
8038
+ '<button class="btn" style="font-size:9px;padding:2px 7px" onclick="navigator.clipboard.writeText(_ofRawText).then(()=>{ if(typeof showToast===\'function\') showToast(\'Copied\',\'\',\'ok\'); })">Copy</button>' +
8039
+ '<button class="btn" style="font-size:9px;padding:2px 7px;border-color:oklch(68% 0.20 150);color:oklch(68% 0.20 150)" onclick="_ofSaveFile(\''+esc(fileName)+'\')">Save</button>' +
8040
+ '<button class="btn" style="font-size:9px;padding:2px 7px" onclick="_ofModal&&(_ofModal.remove(),_ofModal=null)">✕</button>' +
8041
+ '</div>' +
8042
+ '<div id="of-content" style="flex:1;overflow-y:auto;padding:16px 18px;font-size:12px;line-height:1.7;color:var(--text-mid);scrollbar-width:thin;scrollbar-color:var(--border) transparent">' +
8043
+ '<span style="color:var(--text-lo)">Loading…</span>' +
8044
+ '</div>' +
8045
+ '</div>';
8046
+ document.body.appendChild(_ofModal);
8047
+ try {
8048
+ const dirParam = (typeof DIR !== 'undefined' && DIR) ? '&dir='+encodeURIComponent(DIR) : '';
8049
+ const r = await fetch('/api/file-content?path='+encodeURIComponent(filePath)+dirParam);
8050
+ const el = document.getElementById('of-content');
8051
+ if (!el) return;
8052
+ if (!r.ok) { el.textContent = 'Error '+r.status+': '+r.statusText; return; }
8053
+ _ofRawText = await r.text();
8054
+ if (isMd) {
8055
+ el.innerHTML = '<div style="max-width:720px">'+_ofMdToHtml(_ofRawText)+'</div>';
8056
+ el.dataset.mode = 'rendered';
8057
+ } else if (isJson) {
8058
+ try { el.textContent = JSON.stringify(JSON.parse(_ofRawText), null, 2); } catch(_) { el.textContent = _ofRawText; }
8059
+ el.style.fontFamily = 'var(--mono)'; el.style.fontSize = '11px'; el.style.whiteSpace = 'pre-wrap';
8060
+ } else {
8061
+ el.textContent = _ofRawText;
8062
+ el.style.fontFamily = 'var(--mono)'; el.style.fontSize = '11px'; el.style.whiteSpace = 'pre-wrap';
8063
+ }
8064
+ } catch(e) {
8065
+ const el = document.getElementById('of-content');
8066
+ if (el) el.textContent = 'Error: '+e.message;
8067
+ }
8068
+ }
8069
+
8070
+ function _ofToggleRender() {
8071
+ const el = document.getElementById('of-content');
8072
+ const btn = document.getElementById('of-toggle');
8073
+ if (!el || !_ofRawText) return;
8074
+ if (el.dataset.mode === 'rendered') {
8075
+ el.textContent = _ofRawText;
8076
+ el.style.fontFamily = 'var(--mono)'; el.style.fontSize = '11px'; el.style.whiteSpace = 'pre-wrap';
8077
+ el.dataset.mode = 'raw';
8078
+ if (btn) btn.textContent = 'Preview';
8079
+ } else {
8080
+ el.innerHTML = '<div style="max-width:720px">'+_ofMdToHtml(_ofRawText)+'</div>';
8081
+ el.style.fontFamily = ''; el.style.fontSize = '12px'; el.style.whiteSpace = '';
8082
+ el.dataset.mode = 'rendered';
8083
+ if (btn) btn.textContent = 'Raw';
8084
+ }
8085
+ }
8086
+
8087
+ function _ofSaveFile(defaultName) {
8088
+ if (!_ofRawText) return;
8089
+ const a = document.createElement('a');
8090
+ a.href = URL.createObjectURL(new Blob([_ofRawText], { type: 'text/plain' }));
8091
+ a.download = defaultName || 'file.txt';
8092
+ a.click();
8093
+ URL.revokeObjectURL(a.href);
8094
+ }
8095
+
6298
8096
  // ── WORKSPACES ─────────────────────────────────────────────
6299
8097
  async function v2RenderOrgWorkspaces() {
6300
8098
  const el = document.getElementById('odt-workspaces');
6301
8099
  if (!el || !_v2SelOrg) return;
8100
+ const _rendOrg = _v2SelOrg;
6302
8101
  el.innerHTML = '<div class="loading-txt">Loading…</div>';
6303
8102
  try {
6304
- const _enc = encodeURIComponent(_v2SelOrg);
8103
+ const _enc = encodeURIComponent(_rendOrg);
6305
8104
  const data = await fetch(`/api/org/${_enc}/workspaces${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
8105
+ if (_v2SelOrg !== _rendOrg) return;
6306
8106
  const ws = Array.isArray(data) ? data : (data.workspaces || []);
6307
8107
  if (!ws.length) { el.innerHTML = '<div class="empty">No workspaces configured</div>'; return; }
6308
8108
  el.innerHTML = ws.map(w => `<div style="padding:10px 0;border-bottom:1px solid var(--border)">
@@ -6320,10 +8120,12 @@ async function v2RenderOrgWorkspaces() {
6320
8120
  async function v2RenderOrgInvites() {
6321
8121
  const el = document.getElementById('odt-invites');
6322
8122
  if (!el || !_v2SelOrg) return;
8123
+ const _rendOrg = _v2SelOrg;
6323
8124
  el.innerHTML = '<div class="loading-txt">Loading…</div>';
6324
8125
  try {
6325
- const _enc = encodeURIComponent(_v2SelOrg);
8126
+ const _enc = encodeURIComponent(_rendOrg);
6326
8127
  const data = await fetch(`/api/org/${_enc}/invites${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
8128
+ if (_v2SelOrg !== _rendOrg) return;
6327
8129
  const invites = Array.isArray(data) ? data : (data.invites || []);
6328
8130
  if (!invites.length) { el.innerHTML = '<div class="empty">No active invite tokens</div>'; return; }
6329
8131
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
@@ -6352,12 +8154,12 @@ function v2RenderOrgAgentsFull() {
6352
8154
  </tr></thead>
6353
8155
  <tbody>${agents.map(a => `<tr style="border-top:1px solid var(--border)">
6354
8156
  <td style="padding:5px 8px;color:var(--text-hi);font-family:var(--mono);font-size:11px">${esc((a.id||'—').toString().slice(0,12))}</td>
6355
- <td style="padding:5px 8px;color:var(--text-hi)">${esc(a.type||a.title||'—')}</td>
6356
- <td style="padding:5px 8px;color:var(--text-lo)">${esc(a.adapter||'—')}</td>
8157
+ <td style="padding:5px 8px;color:var(--text-hi)">${esc(a.adapterType||a.type||a.title||'—')}</td>
8158
+ <td style="padding:5px 8px;color:var(--text-lo)">${esc(a.adapterModel||a.adapter||'—')}</td>
6357
8159
  <td style="padding:5px 8px"><span class="ss-pill ${(a.status==='running'||a.running)?'on':''}">${esc(a.status||'idle')}</span></td>
6358
8160
  <td style="padding:5px 8px;color:var(--text-lo)">${a.lastHeartbeat ? relTime(a.lastHeartbeat) : '—'}</td>
6359
- <td style="padding:5px 8px;color:var(--text-lo)">${a.tokensIn != null ? Number(a.tokensIn).toLocaleString() : '—'}</td>
6360
- <td style="padding:5px 8px;color:var(--text-lo)">${a.tokensOut != null ? Number(a.tokensOut).toLocaleString() : '—'}</td>
8161
+ <td style="padding:5px 8px;color:var(--text-lo)">${(a.tokensIn ?? a.tokens_in) != null ? Number(a.tokensIn ?? a.tokens_in).toLocaleString() : '—'}</td>
8162
+ <td style="padding:5px 8px;color:var(--text-lo)">${(a.tokensOut ?? a.tokens_out) != null ? Number(a.tokensOut ?? a.tokens_out).toLocaleString() : '—'}</td>
6361
8163
  </tr>`).join('')}</tbody>
6362
8164
  </table>`;
6363
8165
  }
@@ -6366,10 +8168,12 @@ function v2RenderOrgAgentsFull() {
6366
8168
  async function v2RenderOrgEnvironments() {
6367
8169
  const el = document.getElementById('odt-environments');
6368
8170
  if (!el || !_v2SelOrg) return;
8171
+ const _rendOrg = _v2SelOrg;
6369
8172
  el.innerHTML = '<div class="loading-txt">Loading…</div>';
6370
8173
  try {
6371
- const _enc = encodeURIComponent(_v2SelOrg);
8174
+ const _enc = encodeURIComponent(_rendOrg);
6372
8175
  const data = await fetch(`/api/org/${_enc}/environments${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
8176
+ if (_v2SelOrg !== _rendOrg) return;
6373
8177
  const envs = Array.isArray(data) ? data : (data.environments || []);
6374
8178
  if (!envs.length) { el.innerHTML = '<div class="empty">No environments configured</div>'; return; }
6375
8179
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
@@ -6423,7 +8227,7 @@ function v2RenderOrgIssuesFull() {
6423
8227
  <td style="padding:5px 4px">${PRIORITY[i.priority]||'·'}</td>
6424
8228
  <td style="padding:5px 8px;color:var(--text-hi);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc((i.title||i.description||'—').slice(0,80))}</td>
6425
8229
  <td style="padding:5px 8px;color:var(--text-lo)">${esc(i.assignee||'—')}</td>
6426
- <td style="padding:5px 8px"><span class="ss-pill ${i.status==='done'?'on':i.status==='blocked'?'warn':''}">${esc(i.status||'open')}</span></td>
8230
+ <td style="padding:5px 8px"><span class="ss-pill ${i.status==='done'||i.status==='closed'||i.status==='resolved'?'on':(i.status==='blocked'||i.status==='in_progress')?'warn':''}">${esc(i.status||'open')}</span></td>
6427
8231
  <td style="padding:5px 8px;color:var(--text-xs);font-family:var(--mono);font-size:11px">${relTime(i.updated_at||i.ts)}</td>
6428
8232
  </tr>`).join('')}</tbody>
6429
8233
  </table>`;
@@ -6433,10 +8237,12 @@ function v2RenderOrgIssuesFull() {
6433
8237
  async function v2RenderOrgJoinRequests() {
6434
8238
  const el = document.getElementById('odt-join-requests');
6435
8239
  if (!el || !_v2SelOrg) return;
8240
+ const _rendOrg = _v2SelOrg;
6436
8241
  el.innerHTML = '<div class="loading-txt">Loading…</div>';
6437
8242
  try {
6438
- const _enc = encodeURIComponent(_v2SelOrg);
8243
+ const _enc = encodeURIComponent(_rendOrg);
6439
8244
  const data = await fetch(`/api/org/${_enc}/join-requests${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
8245
+ if (_v2SelOrg !== _rendOrg) return;
6440
8246
  const reqs = Array.isArray(data) ? data : (data.joinRequests || data.join_requests || []);
6441
8247
  if (!reqs.length) { el.innerHTML = '<div class="empty">No pending join requests</div>'; return; }
6442
8248
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
@@ -6457,10 +8263,12 @@ async function v2RenderOrgJoinRequests() {
6457
8263
  async function v2RenderOrgThreads() {
6458
8264
  const el = document.getElementById('odt-threads');
6459
8265
  if (!el || !_v2SelOrg) return;
8266
+ const _rendOrg = _v2SelOrg;
6460
8267
  el.innerHTML = '<div class="loading-txt">Loading…</div>';
6461
8268
  try {
6462
- const _enc = encodeURIComponent(_v2SelOrg);
8269
+ const _enc = encodeURIComponent(_rendOrg);
6463
8270
  const data = await fetch(`/api/org/${_enc}/threads${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
8271
+ if (_v2SelOrg !== _rendOrg) return;
6464
8272
  const threads = Array.isArray(data) ? data : (data.threads || []);
6465
8273
  if (!threads.length) { el.innerHTML = '<div class="empty">No threads found</div>'; return; }
6466
8274
  el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
@@ -6482,28 +8290,123 @@ async function v2RenderOrgThreads() {
6482
8290
  window.v2StopOrg = async function() {
6483
8291
  if (!_v2SelOrg) return;
6484
8292
  const stopped = _v2SelOrg;
6485
- try { await fetch(`/api/orgs/${encodeURIComponent(stopped)}/stop`, {method:'POST'}); } catch(_) {}
8293
+ try { await fetch(`/api/orgs/${encodeURIComponent(stopped)}/stop${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`, {method:'POST'}); } catch(_) {}
6486
8294
  setTimeout(async () => { await renderOrgs(); if (_v2SelOrg === stopped) v2SelectOrg(stopped); }, 600);
6487
8295
  };
6488
8296
 
6489
- window.v2ShowCopyOrgDialog = function() {
8297
+ window.v2DeleteOrg = async function() {
8298
+ if (!_v2SelOrg) return;
8299
+ const name = _v2SelOrg;
8300
+ if (!confirm(`Delete org "${name}"?\n\nThis removes the config and all associated data files. This cannot be undone.`)) return;
8301
+ const btn = document.getElementById('org-delete-btn');
8302
+ if (btn) { btn.disabled = true; btn.textContent = 'Deleting…'; }
8303
+ try {
8304
+ const r = await fetch(`/api/orgs/${encodeURIComponent(name)}${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`, { method: 'DELETE' });
8305
+ if (!r.ok) { const d = await r.json().catch(() => ({})); throw new Error(d.error || 'HTTP ' + r.status); }
8306
+ showToast('Deleted', `Org "${name}" deleted`, 'ok');
8307
+ _v2SelOrg = null;
8308
+ const inner = document.getElementById('org-detail-inner');
8309
+ if (inner) inner.style.display = 'none';
8310
+ const noSel = document.querySelector('.orgs-no-sel');
8311
+ if (noSel) noSel.style.display = '';
8312
+ await renderOrgs();
8313
+ } catch(e) {
8314
+ showToast('Error', String(e.message || e), 'err');
8315
+ if (btn) { btn.disabled = false; btn.textContent = 'Delete'; }
8316
+ }
8317
+ };
8318
+
8319
+ window.v2ShowCopyOrgDialog = async function() {
6490
8320
  const d = document.getElementById('org-copy-dialog');
6491
- if (d) { d.style.display = 'flex'; document.getElementById('org-copy-dest')?.focus(); }
8321
+ if (!d) return;
8322
+ d.style.display = 'flex';
8323
+ document.getElementById('org-copy-dest').value = '';
8324
+ document.getElementById('org-copy-status').textContent = '';
8325
+ document.getElementById('org-copy-confirm-btn').disabled = false;
8326
+ const listEl = document.getElementById('org-copy-proj-list');
8327
+ listEl.innerHTML = '<div style="padding:8px 10px;font-size:11px;color:var(--text-lo)">Loading projects…</div>';
8328
+ try {
8329
+ const data = await apiFetch('/api/projects');
8330
+ const projects = (data?.projects || []);
8331
+ if (!projects.length) { listEl.innerHTML = '<div style="padding:8px 10px;font-size:11px;color:var(--text-lo)">No projects found</div>'; }
8332
+ else {
8333
+ listEl.innerHTML = projects.map(p => {
8334
+ const label = p.name || p.slug || p.path?.split('/').pop() || p.path;
8335
+ const shortP = (p.path || '').replace(/\/Users\/[^/]+\//, '~/');
8336
+ const isCurrent = p.path === (window._orgDir || '');
8337
+ return `<div class="ocp-row${isCurrent ? ' ocp-sel' : ''}" title="${esc(p.path)}" onclick="document.querySelectorAll('.ocp-row').forEach(r=>r.classList.remove('ocp-sel'));this.classList.add('ocp-sel');document.getElementById('org-copy-dest').value='${esc(p.path).replace(/'/g,"\\'")}'" style="padding:7px 10px;font-size:12px;font-family:var(--mono);cursor:pointer;border-bottom:1px solid var(--border);display:flex;gap:8px;align-items:baseline">
8338
+ <span style="color:var(--text-hi)">${esc(label)}</span>
8339
+ <span style="color:var(--text-lo);font-size:10px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(shortP)}</span>
8340
+ </div>`;
8341
+ }).join('');
8342
+ }
8343
+ } catch(e) { listEl.innerHTML = '<div style="padding:8px;font-size:11px;color:var(--text-lo)">Could not load projects</div>'; }
8344
+ setTimeout(() => document.getElementById('org-copy-dest')?.focus(), 60);
6492
8345
  };
6493
8346
  window.v2DoCopyOrg = async function() {
6494
8347
  const dest = document.getElementById('org-copy-dest')?.value.trim();
6495
- if (!dest) { showToast('Required', 'Enter destination path', 'warn'); return; }
8348
+ const statusEl = document.getElementById('org-copy-status');
8349
+ const confirmBtn = document.getElementById('org-copy-confirm-btn');
8350
+ if (!dest) { if (statusEl) { statusEl.textContent = 'Enter or select a destination'; statusEl.style.color = 'var(--red)'; } return; }
6496
8351
  if (!_v2SelOrg) return;
8352
+ confirmBtn.disabled = true;
8353
+ if (statusEl) { statusEl.textContent = 'Copying…'; statusEl.style.color = 'var(--text-lo)'; }
6497
8354
  try {
6498
- const r = await fetch('/api/org/' + enc(_v2SelOrg) + '/copy?dir=' + enc(DIR), {
8355
+ const r = await fetch('/api/orgs/' + enc(_v2SelOrg) + '/copy?dir=' + enc(DIR), {
6499
8356
  method: 'POST', headers: { 'Content-Type': 'application/json' },
6500
8357
  body: JSON.stringify({ destination: dest }),
6501
8358
  });
6502
- if (!r.ok) throw new Error('HTTP ' + r.status);
6503
- showToast('Copied', `Org "${_v2SelOrg}" copied to ${dest}`, 'ok');
8359
+ const data = await r.json().catch(() => ({}));
8360
+ if (!r.ok) throw new Error(data.error || 'HTTP ' + r.status);
8361
+ showToast('Copied', `"${_v2SelOrg}" → ${dest}`, 'ok');
6504
8362
  document.getElementById('org-copy-dialog').style.display = 'none';
6505
- } catch (e) { showToast('Error', e.message, 'err'); }
8363
+ } catch (e) {
8364
+ if (statusEl) { statusEl.textContent = e.message; statusEl.style.color = 'var(--red)'; }
8365
+ confirmBtn.disabled = false;
8366
+ }
8367
+ };
8368
+ window.v2ExportOrg = async function() {
8369
+ if (!_v2SelOrg) return;
8370
+ try {
8371
+ const r = await fetch('/api/orgs/' + encodeURIComponent(_v2SelOrg) + (DIR ? '?dir=' + encodeURIComponent(DIR) : ''));
8372
+ if (!r.ok) throw new Error('HTTP ' + r.status);
8373
+ const data = await r.json();
8374
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
8375
+ const url = URL.createObjectURL(blob);
8376
+ const a = document.createElement('a');
8377
+ a.href = url; a.download = _v2SelOrg + '.json'; a.click();
8378
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
8379
+ showToast('Exported', `${_v2SelOrg}.json downloaded`, 'ok');
8380
+ } catch(e) { showToast('Export failed', e.message, 'err'); }
8381
+ };
8382
+ window.v2ImportOrgTrigger = function() {
8383
+ let inp = document.getElementById('org-import-file');
8384
+ if (!inp) {
8385
+ inp = document.createElement('input');
8386
+ inp.type = 'file'; inp.id = 'org-import-file'; inp.accept = '.json'; inp.style.display = 'none';
8387
+ document.body.appendChild(inp);
8388
+ }
8389
+ inp.onchange = v2HandleImportFile;
8390
+ inp.click();
6506
8391
  };
8392
+ async function v2HandleImportFile(e) {
8393
+ const file = e.target.files?.[0];
8394
+ e.target.value = '';
8395
+ if (!file) return;
8396
+ try {
8397
+ const text = await file.text();
8398
+ const data = JSON.parse(text);
8399
+ const name = data.name || file.name.replace(/\.json$/, '');
8400
+ const r = await fetch('/api/orgs', {
8401
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
8402
+ body: JSON.stringify({ ...data, name, dir: DIR || undefined }),
8403
+ });
8404
+ if (!r.ok) { const d = await r.json().catch(()=>({})); throw new Error(d.error || 'HTTP ' + r.status); }
8405
+ showToast('Imported', `Org "${name}" imported`, 'ok');
8406
+ await renderOrgs();
8407
+ if (name) v2SelectOrg(name);
8408
+ } catch(e) { showToast('Import failed', e.message, 'err'); }
8409
+ }
6507
8410
 
6508
8411
  function filterLoopList(q) {
6509
8412
  const query = q.trim().toLowerCase();
@@ -6525,13 +8428,23 @@ function filterOrgList(q) {
6525
8428
  // live SSE for org events
6526
8429
  (function v2OrgSSE() {
6527
8430
  let src;
8431
+ let seenKeys = new Set();
8432
+ let reconnectTimer = null;
6528
8433
  function connect() {
8434
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
6529
8435
  if (src) src.close();
8436
+ // Do NOT reset seenKeys here — the server replays ~50 events on every reconnect and
8437
+ // clearing the set lets all replays through, duplicating _v2OrgEventLog and _activity entries.
6530
8438
  src = new EventSource('/api/mastermind-stream');
6531
8439
  src.onmessage = e => {
6532
8440
  try {
6533
8441
  const ev = JSON.parse(e.data);
6534
- if (!ev?.org || !ev?.type?.startsWith('org:')) return;
8442
+ if (!ev?.org) return;
8443
+ if (!ev?.type?.startsWith('org:') && ev?.type !== 'run:start' && ev?.type !== 'run:complete' && ev?.type !== 'run:cycle:complete') return;
8444
+ const _sk = (ev.ts||'') + '|' + (ev.type||'') + '|' + (ev.org||'') + '|' + (ev.runId||ev.session||'') + '|' + (ev.from||ev.role||ev.agent||'') + '|' + (ev.result||ev.msg||ev.message||ev.summary||'').slice(0,20);
8445
+ if (seenKeys.has(_sk)) return;
8446
+ seenKeys.add(_sk);
8447
+ if (seenKeys.size > 2000) { const _a = [...seenKeys]; seenKeys = new Set(_a.slice(1000)); }
6535
8448
  const n = ev.org;
6536
8449
  // Filter by project dir if the event carries one; skip events from other projects.
6537
8450
  // Events without a project field are treated as belonging to the current project.
@@ -6541,7 +8454,63 @@ function filterOrgList(q) {
6541
8454
  _v2OrgEventLog[n].push(ev);
6542
8455
  if (_v2OrgEventLog[n].length > 50) _v2OrgEventLog[n].shift();
6543
8456
  if (n === _v2SelOrg && _v2OrgTab === 'activity') v2RenderOrgActivity();
6544
- if (ev.type === 'org:start' || ev.type === 'org:stop' || ev.type === 'org:delete' || ev.type === 'org:create') {
8457
+ // Update in-memory agent status so Live tab running-agents section reflects reality.
8458
+ // org:agent:online → mark that agent running; org:complete/run:complete → reset all to idle.
8459
+ if (n === _v2SelOrg && _v2OrgData && Array.isArray(_v2OrgData._agents)) {
8460
+ if (ev.type === 'org:agent:online' && ev.role) {
8461
+ const _liveAgent = _v2OrgData._agents.find(a => a.id === ev.role);
8462
+ if (_liveAgent) { _liveAgent.status = 'running'; _liveAgent.title = _liveAgent.title || ev.title || ev.role; }
8463
+ else _v2OrgData._agents.push({ id: ev.role, title: ev.title || ev.role, adapterType: ev.agent_type || null, status: 'running' });
8464
+ } else if (ev.type === 'org:agent:offline' && ev.role) {
8465
+ const _offAgent = _v2OrgData._agents.find(a => a.id === ev.role);
8466
+ if (_offAgent) _offAgent.status = 'idle';
8467
+ } else if (ev.type === 'org:complete' || ev.type === 'run:complete') {
8468
+ _v2OrgData._agents.forEach(a => { a.status = 'idle'; });
8469
+ }
8470
+ if (_v2OrgTab === 'agents-full' && (ev.type === 'org:agent:online' || ev.type === 'org:agent:offline' || ev.type === 'org:complete' || ev.type === 'run:complete')) v2RenderOrgAgentsFull();
8471
+ else if (_v2OrgTab === 'live' && (ev.type === 'org:agent:online' || ev.type === 'org:agent:offline' || ev.type === 'org:complete' || ev.type === 'run:complete')) v2RenderOrgLive();
8472
+ else if (_v2OrgTab === 'heartbeats' && (ev.type === 'org:agent:online' || ev.type === 'org:agent:offline' || ev.type === 'org:complete' || ev.type === 'run:complete')) v2RenderOrgHeartbeats();
8473
+ else if (_v2OrgTab === 'members' && (ev.type === 'org:agent:online' || ev.type === 'org:agent:offline' || ev.type === 'org:complete' || ev.type === 'run:complete')) v2RenderOrgMembers();
8474
+ else if (_v2OrgTab === 'roles' && (ev.type === 'org:agent:online' || ev.type === 'org:agent:offline' || ev.type === 'org:complete' || ev.type === 'run:complete')) v2RenderOrgRoles();
8475
+ else if (_v2OrgTab === 'chart' && (ev.type === 'org:agent:online' || ev.type === 'org:agent:offline' || ev.type === 'org:complete' || ev.type === 'run:complete')) v2UpdateChartRunningDots();
8476
+ // Health tab: update KPI counters live from SSE events so agents_active and
8477
+ // tasks_pending reflect current state without waiting for a run-event re-fetch.
8478
+ if (_v2OrgTab === 'health' && _v2OrgData._health) {
8479
+ if (ev.type === 'org:agent:online' || ev.type === 'org:agent:offline' || ev.type === 'org:complete' || ev.type === 'run:complete') {
8480
+ _v2OrgData._health.agents_active = _v2OrgData._agents.filter(a => a.status === 'running').length;
8481
+ v2RenderOrgHealth();
8482
+ }
8483
+ if (ev.type === 'org:checkpoint' && ev.pending_tasks != null) {
8484
+ _v2OrgData._health.tasks_pending = ev.pending_tasks;
8485
+ v2RenderOrgHealth();
8486
+ }
8487
+ if (ev.type === 'run:cycle:complete' && ev.pending != null) {
8488
+ _v2OrgData._health.tasks_pending = ev.pending;
8489
+ v2RenderOrgHealth();
8490
+ }
8491
+ }
8492
+ }
8493
+ // Also push live org:comms / org:checkpoint events directly into the Live tab
8494
+ // activity feed so they appear in real-time without waiting for the 5s poll.
8495
+ if (n === _v2SelOrg && _v2OrgTab === 'live' && _v2OrgData) {
8496
+ if (!Array.isArray(_v2OrgData._activity)) _v2OrgData._activity = [];
8497
+ _v2OrgData._activity.push({ ...ev, _sseOnly: true });
8498
+ v2RenderOrgLive();
8499
+ }
8500
+ // Pulse the org list dot on comms/checkpoint events so the user can see the org is active
8501
+ // without switching to the activity tab
8502
+ if (ev.type === 'org:comms' || ev.type === 'org:checkpoint') {
8503
+ const _orgDot = document.querySelector(`.org-item[data-org="${CSS.escape(n)}"] .oi-dot`);
8504
+ if (_orgDot) {
8505
+ _orgDot.classList.add('running');
8506
+ // Brief flash to indicate activity — class stays if org is running
8507
+ setTimeout(() => {
8508
+ const _listOrg = _v2Orgs.find(o => o.name === n) || {};
8509
+ if (!_listOrg.running) _orgDot.classList.remove('running');
8510
+ }, 2000);
8511
+ }
8512
+ }
8513
+ if (ev.type === 'org:start' || ev.type === 'org:stop' || ev.type === 'org:delete' || ev.type === 'org:create' || ev.type === 'run:start' || ev.type === 'run:complete') {
6545
8514
  setTimeout(async () => {
6546
8515
  await renderOrgs();
6547
8516
  if (_v2SelOrg === n) {
@@ -6556,14 +8525,102 @@ function filterOrgList(q) {
6556
8525
  const listOrg = _v2Orgs.find(o => o.name === _v2SelOrg) || {};
6557
8526
  if (_v2OrgData) _v2OrgData.running = listOrg.running;
6558
8527
  v2UpdateOrgHeader(listOrg, _v2OrgData);
6559
- if (_v2OrgTab === 'health') v2RenderOrgHealth();
8528
+ if (_v2OrgTab === 'health') {
8529
+ // Re-fetch health after run events so stats (agents_active, success_rate, total_runs) are current
8530
+ const _hEnc = encodeURIComponent(_v2SelOrg);
8531
+ fetch(`/api/org/${_hEnc}/health${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
8532
+ .then(r => r.ok ? r.json() : null)
8533
+ .then(h => { if (h && _v2OrgData && _v2SelOrg === n) { _v2OrgData._health = h; v2RenderOrgHealth(); } })
8534
+ .catch(() => { v2RenderOrgHealth(); });
8535
+ }
8536
+ if (_v2OrgTab === 'tasks') {
8537
+ // Re-fetch org data after run events so tasks (todo/doing/done) reflect current store state
8538
+ const _tEnc = encodeURIComponent(_v2SelOrg);
8539
+ fetch(`/api/org/${_tEnc}${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
8540
+ .then(r => r.ok ? r.json() : null)
8541
+ .then(d => { if (d && _v2OrgData && _v2SelOrg === n && d.tasks !== undefined) { _v2OrgData.tasks = d.tasks; v2RenderOrgTasks(); } })
8542
+ .catch(() => { v2RenderOrgTasks(); });
8543
+ }
8544
+ if (_v2OrgTab === 'budgets' || _v2OrgTab === 'costs') {
8545
+ // Re-fetch budgets after run events so token/cost counters stay current
8546
+ const _bEnc = encodeURIComponent(_v2SelOrg);
8547
+ fetch(`/api/org/${_bEnc}/budgets${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
8548
+ .then(r => r.ok ? r.json() : null)
8549
+ .then(b => { if (b && _v2OrgData && _v2SelOrg === n) { _v2OrgData._budgets = b; _v2OrgTab === 'budgets' ? v2RenderOrgBudgets() : v2RenderOrgCosts(); } })
8550
+ .catch(() => { _v2OrgTab === 'budgets' ? v2RenderOrgBudgets() : v2RenderOrgCosts(); });
8551
+ }
8552
+ if (_v2OrgTab === 'board' || _v2OrgTab === 'issues-full') {
8553
+ // Re-fetch issues after run events so board/issues-full reflect current state
8554
+ const _iEnc = encodeURIComponent(_v2SelOrg);
8555
+ fetch(`/api/org/${_iEnc}/issues${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
8556
+ .then(r => r.ok ? r.json() : null)
8557
+ .then(d => { if (d && _v2OrgData && _v2SelOrg === n) { _v2OrgData._issues = Array.isArray(d) ? d : (d?.issues || []); _v2OrgTab === 'board' ? v2RenderOrgBoard() : v2RenderOrgIssuesFull(); } })
8558
+ .catch(() => { _v2OrgTab === 'board' ? v2RenderOrgBoard() : v2RenderOrgIssuesFull(); });
8559
+ }
8560
+ if (_v2OrgTab === 'goals') {
8561
+ // Re-fetch org data after run events so goal progress (filled/total/status) reflects current state
8562
+ const _glEnc = encodeURIComponent(_v2SelOrg);
8563
+ fetch(`/api/org/${_glEnc}${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
8564
+ .then(r => r.ok ? r.json() : null)
8565
+ .then(d => { if (d && _v2OrgData && _v2SelOrg === n && d.goals !== undefined) { _v2OrgData.goals = d.goals; v2RenderOrgGoals(); } })
8566
+ .catch(() => { v2RenderOrgGoals(); });
8567
+ }
8568
+ if (_v2OrgTab === 'chat') {
8569
+ // Re-load chat run list after run events so cycleCount/eventCount in dropdown reflect disk state
8570
+ _odtLoadChatSessions().catch(() => {});
8571
+ }
8572
+ if (_v2OrgTab === 'agents-full' || _v2OrgTab === 'heartbeats') {
8573
+ // Re-fetch agents after run events to get fresh tokensIn/tokensOut/lastHeartbeat
8574
+ // Heartbeats tab derives from _v2OrgData._agents so shares this re-fetch.
8575
+ const _agEnc = encodeURIComponent(_v2SelOrg);
8576
+ fetch(`/api/org/${_agEnc}/agents${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
8577
+ .then(r => r.ok ? r.json() : null)
8578
+ .then(d => { if (d && _v2OrgData && _v2SelOrg === n) { _v2OrgData._agents = Array.isArray(d) ? d : (d?.agents || []); _v2OrgTab === 'agents-full' ? v2RenderOrgAgentsFull() : v2RenderOrgHeartbeats(); } })
8579
+ .catch(() => { _v2OrgTab === 'agents-full' ? v2RenderOrgAgentsFull() : v2RenderOrgHeartbeats(); });
8580
+ }
8581
+ if (_v2OrgTab === 'myissues') {
8582
+ // Re-render myissues after run events — it fetches its own data on every render
8583
+ v2RenderOrgMyIssues();
8584
+ }
8585
+ if (_v2OrgTab === 'charts') {
8586
+ // Re-fetch activity after run events so the 14-day heatmap reflects the completed run
8587
+ const _chEnc = encodeURIComponent(_v2SelOrg);
8588
+ fetch(`/api/org/${_chEnc}/activity${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
8589
+ .then(r => r.ok ? r.json() : [])
8590
+ .then(d => { if (_v2OrgData && _v2SelOrg === n) { _v2OrgData._activity = Array.isArray(d) ? d : []; v2RenderOrgCharts(); } })
8591
+ .catch(() => { v2RenderOrgCharts(); });
8592
+ }
6560
8593
  }
6561
8594
  }
6562
8595
  }, 400);
6563
8596
  }
8597
+ // run:cycle:complete: lightweight re-fetch for budgets/costs/tasks without renderOrgs()
8598
+ // — cycles fire frequently and only affect token counters and task state, not org-list status.
8599
+ if (ev.type === 'run:cycle:complete' && _v2SelOrg === n && _v2OrgData) {
8600
+ if (_v2OrgTab === 'budgets' || _v2OrgTab === 'costs') {
8601
+ const _bcEnc = encodeURIComponent(_v2SelOrg);
8602
+ fetch(`/api/org/${_bcEnc}/budgets${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
8603
+ .then(r => r.ok ? r.json() : null)
8604
+ .then(b => { if (b && _v2OrgData && _v2SelOrg === n) { _v2OrgData._budgets = b; _v2OrgTab === 'budgets' ? v2RenderOrgBudgets() : v2RenderOrgCosts(); } })
8605
+ .catch(() => { _v2OrgTab === 'budgets' ? v2RenderOrgBudgets() : v2RenderOrgCosts(); });
8606
+ }
8607
+ if (_v2OrgTab === 'tasks') {
8608
+ const _tcEnc = encodeURIComponent(_v2SelOrg);
8609
+ fetch(`/api/org/${_tcEnc}${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
8610
+ .then(r => r.ok ? r.json() : null)
8611
+ .then(d => { if (d && _v2OrgData && _v2SelOrg === n && d.tasks !== undefined) { _v2OrgData.tasks = d.tasks; v2RenderOrgTasks(); } })
8612
+ .catch(() => { v2RenderOrgTasks(); });
8613
+ }
8614
+ }
6564
8615
  } catch(_) {}
6565
8616
  };
6566
- src.onerror = () => setTimeout(connect, 5000);
8617
+ src.onerror = () => {
8618
+ // Close immediately to stop browser's built-in auto-retry (zombie window).
8619
+ // Guard reconnectTimer so multiple onerror firings don't stack timers that
8620
+ // would then cancel each other's freshly-created connections.
8621
+ const _s = src; src = null; if (_s) _s.close();
8622
+ if (!reconnectTimer) reconnectTimer = setTimeout(connect, 5000);
8623
+ };
6567
8624
  }
6568
8625
  connect();
6569
8626
  })();
@@ -9580,8 +11637,8 @@ function selectMem(filename) {
9580
11637
  const actions = (f.readonly || f.source === 'backend')
9581
11638
  ? '<div class="mem-actions"><span style="font-size:11px;color:var(--text-lo)">Read-only — stored in the backend memory store (not a file)</span></div>'
9582
11639
  : '<div class="mem-actions">' +
9583
- '<button class="btn" onclick="openEditMemModal(' + JSON.stringify(filename) + ')">&#x270E; Edit</button>' +
9584
- '<button class="btn" style="color:var(--red);border-color:var(--red)" onclick="deleteMem(' + JSON.stringify(filename) + ')">&#x2715; Delete</button>' +
11640
+ '<button class="btn" data-fn="' + esc(filename) + '" onclick="openEditMemModal(this.dataset.fn)">&#x270E; Edit</button>' +
11641
+ '<button class="btn" style="color:var(--red);border-color:var(--red)" data-fn="' + esc(filename) + '" onclick="deleteMem(this.dataset.fn)">&#x2715; Delete</button>' +
9585
11642
  '</div>';
9586
11643
  detail.innerHTML =
9587
11644
  '<span class="mem-badge" style="background:' + col + '22;color:' + col + '">' + esc(f.type || '?') + '</span>' + srcBadge +
@@ -10135,7 +12192,7 @@ function mmRenderOrgs(body) {
10135
12192
  if (!orgs.length) { body.innerHTML = '<div class="empty">No orgs found. Run /mastermind:createorg to create one.</div>'; return; }
10136
12193
  body.innerHTML = orgs.map(o => {
10137
12194
  const running = o.running;
10138
- return `<div class="mm-skill-item" onclick="closeMastermind();v2SelectOrg(${JSON.stringify(o.name)});switchView('orgs')">
12195
+ return `<div class="mm-skill-item" data-org="${esc(o.name)}" onclick="closeMastermind();v2SelectOrg(this.dataset.org);switchView('orgs')">
10139
12196
  <span class="mm-skill-name">${esc(o.name)}</span>
10140
12197
  <span class="mm-skill-desc">${esc((o.goal || '').slice(0, 60))} ${running ? '⬤ LIVE' : ''}</span>
10141
12198
  </div>`;
@@ -10147,7 +12204,7 @@ function mmRenderSkills(body) {
10147
12204
  const q = _mmSkillFilter.toLowerCase();
10148
12205
  const filtered = q ? _MM_SKILLS_CATALOG.filter(s => s.name.toLowerCase().includes(q) || s.desc.toLowerCase().includes(q)) : _MM_SKILLS_CATALOG;
10149
12206
  body.innerHTML = `<div class="filter-bar" style="margin-bottom:12px"><input class="filter-input" type="text" placeholder="Search skills…" value="${esc(_mmSkillFilter)}" oninput="_mmSkillFilter=this.value;mmRenderSkills(document.getElementById('mm-body'))"></div>` +
10150
- filtered.map(s => `<div class="mm-skill-item" onclick="navigator.clipboard.writeText(${JSON.stringify(s.name)}).then(()=>showToast('Copied',${JSON.stringify(s.name)},'ok'))">
12207
+ filtered.map(s => `<div class="mm-skill-item" data-sn="${esc(s.name)}" onclick="navigator.clipboard.writeText(this.dataset.sn).then(()=>showToast('Copied',this.dataset.sn,'ok'))">
10151
12208
  <span class="mm-skill-name">${esc(s.name)}</span>
10152
12209
  <span class="mm-skill-desc">${esc(s.desc)}</span>
10153
12210
  </div>`).join('');
@@ -10162,7 +12219,7 @@ async function mmRenderLoops(body) {
10162
12219
  body.innerHTML = loops.map(l => {
10163
12220
  const maxReps = l.maxReps || 0;
10164
12221
  const curRep = l.currentRep || 0;
10165
- const isTillend = !maxReps || l.loopType === 'tillend' || String(l.command || '').includes('--tillend');
12222
+ const isTillend = !maxReps || l.type === 'tillend' || String(l.command || '').includes('--tillend');
10166
12223
  const nextAt = l.nextRunAt ? parseInt(l.nextRunAt, 10) : 0;
10167
12224
  const isExplicitlyActive = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
10168
12225
  const isOverdue = !isExplicitlyActive && nextAt > 0 && nextAt <= Date.now();
@@ -10180,7 +12237,7 @@ async function mmRenderLoops(body) {
10180
12237
  <span style="font-size:13px;color:var(--text-hi);flex:1">${esc(name)}</span>
10181
12238
  ${isHil ? '<span style="color:oklch(78% 0.18 80);font-size:10px">⚠ HIL</span>' : ''}
10182
12239
  <span class="ss-pill ${running ? 'on' : ''}">${running ? 'active' : isFinished ? 'done' : 'stopped'}</span>
10183
- ${(running || isHil) ? `<button class="btn" style="font-size:10px;color:var(--red);border-color:var(--red)" onclick="stopLoop(event,${JSON.stringify(l.id||l.name||'')});mmSwitchTab('loops')">■ Stop</button>` : ''}
12240
+ ${(running || isHil) ? `<button class="btn" style="font-size:10px;color:var(--red);border-color:var(--red)" data-lid="${esc(l.id||l.name||'')}" onclick="stopLoop(event,this.dataset.lid);mmSwitchTab('loops')">■ Stop</button>` : ''}
10184
12241
  </div>
10185
12242
  <div style="font-size:11px;color:var(--text-lo);margin-top:4px;font-family:var(--mono)">${esc(fmtInterval(l.interval||l.schedule||''))} ${cdown ? '· ' + cdown : ''} ${curRep != null ? '· run ' + curRep + (isTillend ? '/∞' : maxReps ? '/'+maxReps : '') : ''}</div>
10186
12243
  </div>`;