@theihtisham/agent-shadow-brain 6.0.2 → 6.2.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 (74) hide show
  1. package/CHANGELOG.md +372 -0
  2. package/README.md +330 -358
  3. package/dist/brain/auto-update.js +1 -1
  4. package/dist/brain/brain-bus.d.ts +40 -0
  5. package/dist/brain/brain-bus.d.ts.map +1 -0
  6. package/dist/brain/brain-bus.js +94 -0
  7. package/dist/brain/brain-bus.js.map +1 -0
  8. package/dist/brain/brain-portability.js +3 -3
  9. package/dist/brain/brain-portability.js.map +1 -1
  10. package/dist/brain/collision-detective.d.ts.map +1 -1
  11. package/dist/brain/collision-detective.js +16 -0
  12. package/dist/brain/collision-detective.js.map +1 -1
  13. package/dist/brain/context-continuity.d.ts +78 -0
  14. package/dist/brain/context-continuity.d.ts.map +1 -0
  15. package/dist/brain/context-continuity.js +341 -0
  16. package/dist/brain/context-continuity.js.map +1 -0
  17. package/dist/brain/dream-engine.d.ts.map +1 -1
  18. package/dist/brain/dream-engine.js +12 -0
  19. package/dist/brain/dream-engine.js.map +1 -1
  20. package/dist/brain/forgetting-curve.d.ts.map +1 -1
  21. package/dist/brain/forgetting-curve.js +19 -0
  22. package/dist/brain/forgetting-curve.js.map +1 -1
  23. package/dist/brain/global-brain.d.ts +4 -0
  24. package/dist/brain/global-brain.d.ts.map +1 -1
  25. package/dist/brain/global-brain.js +61 -9
  26. package/dist/brain/global-brain.js.map +1 -1
  27. package/dist/brain/live-brain.d.ts +119 -0
  28. package/dist/brain/live-brain.d.ts.map +1 -0
  29. package/dist/brain/live-brain.js +405 -0
  30. package/dist/brain/live-brain.js.map +1 -0
  31. package/dist/brain/mcp-server.d.ts +4 -0
  32. package/dist/brain/mcp-server.d.ts.map +1 -1
  33. package/dist/brain/mcp-server.js +81 -5
  34. package/dist/brain/mcp-server.js.map +1 -1
  35. package/dist/brain/memory-graph.d.ts +51 -0
  36. package/dist/brain/memory-graph.d.ts.map +1 -0
  37. package/dist/brain/memory-graph.js +267 -0
  38. package/dist/brain/memory-graph.js.map +1 -0
  39. package/dist/brain/memory-seeder.d.ts +78 -0
  40. package/dist/brain/memory-seeder.d.ts.map +1 -0
  41. package/dist/brain/memory-seeder.js +349 -0
  42. package/dist/brain/memory-seeder.js.map +1 -0
  43. package/dist/brain/orchestrator.js +1 -1
  44. package/dist/brain/subagent-bridge.d.ts.map +1 -1
  45. package/dist/brain/subagent-bridge.js +12 -0
  46. package/dist/brain/subagent-bridge.js.map +1 -1
  47. package/dist/brain/token-economy.d.ts +2 -0
  48. package/dist/brain/token-economy.d.ts.map +1 -1
  49. package/dist/brain/token-economy.js +80 -24
  50. package/dist/brain/token-economy.js.map +1 -1
  51. package/dist/cli.js +240 -31
  52. package/dist/cli.js.map +1 -1
  53. package/dist/dashboard/server.d.ts +24 -0
  54. package/dist/dashboard/server.d.ts.map +1 -1
  55. package/dist/dashboard/server.js +524 -7
  56. package/dist/dashboard/server.js.map +1 -1
  57. package/dist/dashboard/v6-dashboard-html.d.ts +1 -1
  58. package/dist/dashboard/v6-dashboard-html.d.ts.map +1 -1
  59. package/dist/dashboard/v6-dashboard-html.js +2070 -213
  60. package/dist/dashboard/v6-dashboard-html.js.map +1 -1
  61. package/dist/index.d.ts +4 -0
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +4 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/types.d.ts +17 -12
  66. package/dist/types.d.ts.map +1 -1
  67. package/docs/launch/README.md +61 -0
  68. package/docs/launch/RELEASE_CHECKLIST_v6.0.3.md +102 -0
  69. package/docs/launch/RELEASE_NOTES_v6.0.3.md +111 -0
  70. package/docs/launch/RELEASE_NOTES_v6.1.0.md +38 -0
  71. package/docs/launch/RELEASE_NOTES_v6.2.0.md +54 -0
  72. package/package.json +18 -4
  73. package/tools/export-resume-pack.mjs +56 -0
  74. package/tools/record-global-memory.mjs +72 -0
@@ -10,7 +10,26 @@ export declare class DashboardServer {
10
10
  private clients;
11
11
  private orchestrator;
12
12
  private options;
13
+ private brainBusUnsub;
14
+ private brainWatcher;
15
+ private brainDbPath;
16
+ private seenEntryIds;
17
+ private brainFileDebounce;
13
18
  constructor(orchestrator: Orchestrator, options?: DashboardOptions);
19
+ /** Forward every realtime brain-bus event straight to all WebSocket clients. */
20
+ private subscribeToBrainBus;
21
+ /**
22
+ * Cross-process realtime: an agent in another terminal writes to the shared
23
+ * ~/.shadow-brain/global.json. That write lands in a different process's
24
+ * GlobalBrain instance, so the in-process bus never sees it. We watch the
25
+ * file and republish any entry id we have not already broadcast. The bus
26
+ * dedups by composite id, so our own in-process memory-writes are ignored.
27
+ */
28
+ private startBrainFileWatcher;
29
+ private primeSeenEntryIds;
30
+ /** Re-read the on-disk brain and publish any entries we haven't seen. */
31
+ private scanBrainFile;
32
+ private toEpoch;
14
33
  private handleRequest;
15
34
  private handleV6Request;
16
35
  /** Aggregate recent activity from all v6 modules into a single unified stream. */
@@ -18,9 +37,14 @@ export declare class DashboardServer {
18
37
  private readJsonBody;
19
38
  private getModulesData;
20
39
  private getMemoryData;
40
+ private getRuntimeContext;
21
41
  private getToolsData;
22
42
  private getTimelineData;
23
43
  private getFirewallDemo;
44
+ private getDefaultWorkflowProjectDir;
45
+ private pathExists;
46
+ private getWorkflowAgentStatus;
47
+ private getWorkflowWarnings;
24
48
  private setupWebSocket;
25
49
  private subscribeToOrchestrator;
26
50
  private send;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAMxD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAwYD,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,GAAG,CAAkB;IAC7B,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,OAAO,CAA6B;gBAEhC,YAAY,EAAE,YAAY,EAAE,OAAO,GAAE,gBAAqB;IAiBtE,OAAO,CAAC,aAAa;YAmDP,eAAe;IAwgB7B,kFAAkF;YACpE,kBAAkB;YAoElB,YAAY;IAY1B,OAAO,CAAC,cAAc;IA+CtB,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,YAAY;IA0BpB,OAAO,CAAC,eAAe;IAcvB,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,cAAc;IAwCtB,OAAO,CAAC,uBAAuB;IA2C/B,OAAO,CAAC,IAAI;IAMZ,OAAO,CAAC,SAAS;IASX,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAUxB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAY3B,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAQxD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAmYD,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,GAAG,CAAkB;IAC7B,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,YAAY,CAA0B;IAC9C,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,YAAY,CAA0B;IAC9C,OAAO,CAAC,iBAAiB,CAA+B;gBAE5C,YAAY,EAAE,YAAY,EAAE,OAAO,GAAE,gBAAqB;IAmBtE,gFAAgF;IAChF,OAAO,CAAC,mBAAmB;IAM3B;;;;;;OAMG;IACH,OAAO,CAAC,qBAAqB;IAoB7B,OAAO,CAAC,iBAAiB;IAQzB,yEAAyE;IACzE,OAAO,CAAC,aAAa;IAuDrB,OAAO,CAAC,OAAO;IAMf,OAAO,CAAC,aAAa;YAmDP,eAAe;IAuvB7B,kFAAkF;YACpE,kBAAkB;YAkFlB,YAAY;IAY1B,OAAO,CAAC,cAAc;IA+CtB,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,iBAAiB;IAkDzB,OAAO,CAAC,YAAY;IA0BpB,OAAO,CAAC,eAAe;IAcvB,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,4BAA4B;YAQtB,UAAU;YASV,sBAAsB;IAuCpC,OAAO,CAAC,mBAAmB;IAoB3B,OAAO,CAAC,cAAc;IA4CtB,OAAO,CAAC,uBAAuB;IA2C/B,OAAO,CAAC,IAAI;IAMZ,OAAO,CAAC,SAAS;IASX,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAUxB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB3B,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
@@ -2,9 +2,13 @@
2
2
  // v5.0.1 — Infinite Intelligence Edition
3
3
  // 8-panel layout: Health, Insights, AI Tools, Memory Tiers, Modules, Fixes, Controls, Activity
4
4
  import * as http from 'http';
5
+ import * as fs from 'fs';
6
+ import chokidar from 'chokidar';
5
7
  import { WebSocketServer, WebSocket } from 'ws';
6
8
  import { AgentFirewall } from '../brain/agent-firewall.js';
9
+ import { analyzeMemoryQuality, generateContextResumePack } from '../brain/context-continuity.js';
7
10
  import { getGlobalBrain, GlobalBrain } from '../brain/global-brain.js';
11
+ import { getBrainBus } from '../brain/brain-bus.js';
8
12
  import { V6_DASHBOARD_HTML } from './v6-dashboard-html.js';
9
13
  // ── Embedded HTML Client ──────────────────────────────────────────────────────
10
14
  const HTML_CLIENT = `<!DOCTYPE html>
@@ -212,11 +216,6 @@ function ts(){return new Date().toLocaleTimeString('en-US',{hour12:false})}
212
216
  function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
213
217
  function sc(v){return v>=85?'var(--green)':v>=70?'var(--yellow)':v>=50?'var(--orange)':'var(--red)'}
214
218
 
215
- // Log to insights panel feedback via activity
216
- function logActivity(msg,type){
217
- // We no longer have a dedicated log panel, so we use a minimal approach
218
- }
219
-
220
219
  function renderHealth(s){
221
220
  if(!s)return;
222
221
  const c=sc(s.overall);
@@ -398,6 +397,11 @@ connect();
398
397
  export class DashboardServer {
399
398
  constructor(orchestrator, options = {}) {
400
399
  this.clients = new Set();
400
+ this.brainBusUnsub = null;
401
+ this.brainWatcher = null;
402
+ this.brainDbPath = '';
403
+ this.seenEntryIds = new Set();
404
+ this.brainFileDebounce = null;
401
405
  this.orchestrator = orchestrator;
402
406
  this.options = {
403
407
  port: options.port ?? 7341,
@@ -410,6 +414,120 @@ export class DashboardServer {
410
414
  this.wss = new WebSocketServer({ server: this.server });
411
415
  this.setupWebSocket();
412
416
  this.subscribeToOrchestrator();
417
+ this.subscribeToBrainBus();
418
+ this.startBrainFileWatcher();
419
+ }
420
+ /** Forward every realtime brain-bus event straight to all WebSocket clients. */
421
+ subscribeToBrainBus() {
422
+ this.brainBusUnsub = getBrainBus().subscribe((event) => {
423
+ this.broadcast({ type: 'activity', event });
424
+ });
425
+ }
426
+ /**
427
+ * Cross-process realtime: an agent in another terminal writes to the shared
428
+ * ~/.shadow-brain/global.json. That write lands in a different process's
429
+ * GlobalBrain instance, so the in-process bus never sees it. We watch the
430
+ * file and republish any entry id we have not already broadcast. The bus
431
+ * dedups by composite id, so our own in-process memory-writes are ignored.
432
+ */
433
+ startBrainFileWatcher() {
434
+ try {
435
+ const brain = getGlobalBrain();
436
+ this.brainDbPath = brain.dbPath;
437
+ // Seed the seen-set with current contents so we never replay history.
438
+ this.primeSeenEntryIds();
439
+ this.brainWatcher = chokidar.watch(this.brainDbPath, {
440
+ persistent: true,
441
+ ignoreInitial: true,
442
+ awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 },
443
+ });
444
+ const onChange = () => {
445
+ if (this.brainFileDebounce)
446
+ clearTimeout(this.brainFileDebounce);
447
+ this.brainFileDebounce = setTimeout(() => this.scanBrainFile(), 400);
448
+ };
449
+ this.brainWatcher.on('add', onChange).on('change', onChange);
450
+ }
451
+ catch { /* watcher is best-effort; in-process events still flow */ }
452
+ }
453
+ primeSeenEntryIds() {
454
+ try {
455
+ const raw = fs.readFileSync(this.brainDbPath, 'utf-8');
456
+ const parsed = JSON.parse(raw);
457
+ for (const e of parsed.entries ?? [])
458
+ this.seenEntryIds.add(e.id);
459
+ }
460
+ catch { /* file may not exist yet */ }
461
+ }
462
+ /** Re-read the on-disk brain and publish any entries we haven't seen. */
463
+ scanBrainFile() {
464
+ let parsed;
465
+ let raw = '';
466
+ try {
467
+ raw = fs.readFileSync(this.brainDbPath, 'utf-8');
468
+ }
469
+ catch {
470
+ return;
471
+ }
472
+ // Very large stores: skip the per-entry diff, emit one generic pulse.
473
+ if (raw.length > 8000000) {
474
+ getBrainBus().publish({
475
+ id: `brain-updated:${Date.now()}`,
476
+ type: 'brain-updated',
477
+ source: 'global-brain',
478
+ detail: 'brain store updated (large store — details suppressed)',
479
+ });
480
+ return;
481
+ }
482
+ try {
483
+ parsed = JSON.parse(raw);
484
+ }
485
+ catch {
486
+ return;
487
+ }
488
+ const entries = parsed.entries ?? [];
489
+ let fresh = 0;
490
+ for (const e of entries) {
491
+ const id = String(e.id ?? '');
492
+ if (!id || this.seenEntryIds.has(id))
493
+ continue;
494
+ this.seenEntryIds.add(id);
495
+ fresh++;
496
+ // Cap how many we replay in a single burst to avoid flooding the UI.
497
+ if (fresh > 100)
498
+ continue;
499
+ getBrainBus().publish({
500
+ id: `memory-write:${id}`,
501
+ ts: this.toEpoch(e.createdAt),
502
+ type: 'memory-write',
503
+ source: String(e.agentTool ?? 'agent'),
504
+ detail: `[${String(e.category ?? '')}] ${String(e.content ?? '').slice(0, 220)}`,
505
+ meta: {
506
+ category: e.category,
507
+ importance: e.importance,
508
+ projectName: e.projectName,
509
+ crossProcess: true,
510
+ },
511
+ });
512
+ }
513
+ if (fresh > 100) {
514
+ getBrainBus().publish({
515
+ id: `brain-updated:${Date.now()}`,
516
+ type: 'brain-updated',
517
+ source: 'global-brain',
518
+ detail: `${fresh} memories written (showing first 100)`,
519
+ });
520
+ }
521
+ }
522
+ toEpoch(v) {
523
+ if (typeof v === 'number')
524
+ return v;
525
+ if (typeof v === 'string') {
526
+ const t = Date.parse(v);
527
+ if (!Number.isNaN(t))
528
+ return t;
529
+ }
530
+ return Date.now();
413
531
  }
414
532
  handleRequest(req, res) {
415
533
  const url = req.url || '/';
@@ -508,9 +626,11 @@ export class DashboardServer {
508
626
  const brain = getGlobalBrain();
509
627
  await brain.init();
510
628
  const tokenReport = await tokens.report();
629
+ const runtime = this.getRuntimeContext();
511
630
  return send({
512
- version: '6.0.0',
631
+ version: '6.0.3',
513
632
  localFirst: true,
633
+ runtime,
514
634
  totalAgentsConnected: brain.getStats().totalAgents,
515
635
  totalMemoriesStored: brain.getStats().totalEntries,
516
636
  modules: {
@@ -526,6 +646,171 @@ export class DashboardServer {
526
646
  generatedAt: new Date(),
527
647
  });
528
648
  }
649
+ case '/api/v6/ww2br-ops': {
650
+ const projectDir = String(qs.get('projectDir') || body.projectDir || this.getDefaultWorkflowProjectDir());
651
+ const brain = getGlobalBrain();
652
+ await brain.init();
653
+ const stats = brain.getStats();
654
+ const projectId = GlobalBrain.projectIdFor(projectDir);
655
+ let timeline = brain.timeline({ projectId, limit: 18 });
656
+ let scoped = true;
657
+ if (!timeline.length) {
658
+ scoped = false;
659
+ timeline = brain.timeline({ limit: 18 });
660
+ }
661
+ const memoryEntries = brain.recall({
662
+ projectId: scoped ? projectId : undefined,
663
+ minImportance: 0,
664
+ limit: 500,
665
+ });
666
+ const quality = analyzeMemoryQuality(memoryEntries);
667
+ const resumePack = generateContextResumePack({
668
+ projectDir,
669
+ projectName: projectDir.split(/[\\/]/).filter(Boolean).pop() ?? projectDir,
670
+ projectId,
671
+ entries: memoryEntries,
672
+ maxMemories: 12,
673
+ });
674
+ const agents = await this.getWorkflowAgentStatus(projectDir);
675
+ const categories = {};
676
+ for (const event of timeline)
677
+ categories[event.category] = (categories[event.category] ?? 0) + 1;
678
+ return send({
679
+ projectDir,
680
+ projectName: projectDir.split(/[\\/]/).filter(Boolean).pop() ?? projectDir,
681
+ projectExists: await this.pathExists(projectDir),
682
+ projectScoped: scoped,
683
+ stats,
684
+ agents,
685
+ warnings: this.getWorkflowWarnings(projectDir, agents, timeline, scoped),
686
+ recent: timeline,
687
+ categories,
688
+ quality,
689
+ resumePack: {
690
+ generatedAt: resumePack.generatedAt,
691
+ stats: resumePack.stats,
692
+ warnings: resumePack.warnings,
693
+ suggestedRecallQueries: resumePack.suggestedRecallQueries,
694
+ blockers: resumePack.blockers.slice(0, 5),
695
+ topMemories: resumePack.topMemories.slice(0, 5),
696
+ },
697
+ generatedAt: new Date(),
698
+ });
699
+ }
700
+ case '/api/v6/ww2br-recall': {
701
+ const projectDir = String(qs.get('projectDir') || body.projectDir || this.getDefaultWorkflowProjectDir());
702
+ const query = String(qs.get('q') || qs.get('query') || (body.query ?? '')).trim();
703
+ const brain = getGlobalBrain();
704
+ await brain.init();
705
+ const projectId = GlobalBrain.projectIdFor(projectDir);
706
+ const common = {
707
+ agentTool: qs.get('agent') || body.agent || undefined,
708
+ category: qs.get('category') || body.category || undefined,
709
+ keywords: query ? query.split(/\s+/).filter(Boolean) : undefined,
710
+ minImportance: Number(qs.get('minImportance') ?? body.minImportance ?? 0),
711
+ limit: Number(qs.get('limit') ?? body.limit ?? 12),
712
+ };
713
+ const projectResults = brain.recall({ projectId, ...common });
714
+ const results = projectResults.length ? projectResults : brain.recall(common);
715
+ return send({
716
+ projectDir,
717
+ projectScoped: projectResults.length > 0,
718
+ query,
719
+ results,
720
+ generatedAt: new Date(),
721
+ });
722
+ }
723
+ case '/api/v6/resume-pack': {
724
+ const projectDir = String(qs.get('projectDir') || body.projectDir || this.getDefaultWorkflowProjectDir());
725
+ const format = String(qs.get('format') || body.format || 'json').toLowerCase();
726
+ const limit = Number(qs.get('limit') ?? body.limit ?? 12);
727
+ const brain = getGlobalBrain();
728
+ await brain.init();
729
+ const projectId = GlobalBrain.projectIdFor(projectDir);
730
+ let entries = brain.recall({ projectId, minImportance: 0, limit: 500 });
731
+ let projectScoped = true;
732
+ if (!entries.length) {
733
+ projectScoped = false;
734
+ entries = brain.recall({ minImportance: 0, limit: 500 });
735
+ }
736
+ const pack = generateContextResumePack({
737
+ projectDir,
738
+ projectName: projectDir.split(/[\\/]/).filter(Boolean).pop() ?? projectDir,
739
+ projectId,
740
+ entries,
741
+ maxMemories: Number.isFinite(limit) ? Math.max(1, Math.min(30, limit)) : 12,
742
+ });
743
+ if (format === 'markdown' || format === 'md')
744
+ return send(pack.markdown, 200, 'text/markdown; charset=utf-8');
745
+ return send({ ...pack, projectScoped });
746
+ }
747
+ case '/api/v6/live-brain': {
748
+ const { getLiveBrain } = await import('../brain/live-brain.js');
749
+ const projectDir = String(qs.get('projectDir') || body.projectDir || this.getDefaultWorkflowProjectDir());
750
+ const format = String(qs.get('format') || body.format || 'json').toLowerCase();
751
+ const queriesRaw = String(qs.get('queries') || body.queries || '').trim();
752
+ const queries = queriesRaw ? queriesRaw.split('|').map(q => q.trim()).filter(Boolean) : undefined;
753
+ const report = await getLiveBrain().report({
754
+ projectDir,
755
+ maxMemories: Number(qs.get('limit') ?? body.limit ?? 12),
756
+ queries,
757
+ globalFallback: body.globalFallback !== false && qs.get('globalFallback') !== 'false',
758
+ });
759
+ if (format === 'markdown' || format === 'md')
760
+ return send(report.agentBriefing, 200, 'text/markdown; charset=utf-8');
761
+ return send(report);
762
+ }
763
+ case '/api/v6/seed-drafts': {
764
+ const { getMemorySeeder } = await import('../brain/memory-seeder.js');
765
+ const limit = Number(qs.get('limit') ?? body.limit ?? 30);
766
+ const seeder = getMemorySeeder();
767
+ await seeder.init();
768
+ return send(seeder.list(limit));
769
+ }
770
+ case '/api/v6/seed-draft': {
771
+ const { getMemorySeeder } = await import('../brain/memory-seeder.js');
772
+ const seeder = getMemorySeeder();
773
+ return send(await seeder.createDraft({
774
+ topic: body.topic,
775
+ objective: body.objective,
776
+ projectDir: body.projectDir || this.getDefaultWorkflowProjectDir(),
777
+ category: body.category,
778
+ tags: body.tags,
779
+ importance: body.importance,
780
+ researchAgent: body.researchAgent,
781
+ approverAgent: body.approverAgent,
782
+ approvalMode: body.approvalMode,
783
+ sources: body.sources,
784
+ content: body.content,
785
+ }));
786
+ }
787
+ case '/api/v6/seed-approve': {
788
+ const { getMemorySeeder } = await import('../brain/memory-seeder.js');
789
+ return send(await getMemorySeeder().approve(body.id, body.approvedBy || 'user', body.note));
790
+ }
791
+ case '/api/v6/seed-reject': {
792
+ const { getMemorySeeder } = await import('../brain/memory-seeder.js');
793
+ return send(await getMemorySeeder().reject(body.id, body.rejectedBy || 'user', body.reason || 'Rejected from dashboard'));
794
+ }
795
+ case '/api/v6/seed-commit': {
796
+ const { getMemorySeeder } = await import('../brain/memory-seeder.js');
797
+ return send(await getMemorySeeder().seed(body.id, body.seededBy || 'user'));
798
+ }
799
+ case '/api/v6/agent-handoff': {
800
+ const { AgentHandoff } = await import('../brain/agent-handoff.js');
801
+ const projectDir = String(body.projectDir || qs.get('projectDir') || this.getDefaultWorkflowProjectDir());
802
+ const packet = await new AgentHandoff().generate({
803
+ fromAgent: (body.fromAgent || 'user'),
804
+ toAgent: (body.toAgent || 'claude-code'),
805
+ projectDir,
806
+ task: body.task,
807
+ limit: body.limit ?? 12,
808
+ });
809
+ const format = String(body.format || qs.get('format') || 'json').toLowerCase();
810
+ if (format === 'markdown' || format === 'md')
811
+ return send(packet.markdown, 200, 'text/markdown; charset=utf-8');
812
+ return send(packet);
813
+ }
529
814
  case '/api/v6/subagent-sliver': {
530
815
  const { getSubAgentBridge } = await import('../brain/subagent-bridge.js');
531
816
  const b = getSubAgentBridge();
@@ -620,7 +905,8 @@ export class DashboardServer {
620
905
  }
621
906
  case '/api/v6/tokens-report': {
622
907
  const { getTokenEconomy } = await import('../brain/token-economy.js');
623
- return send(await getTokenEconomy().report());
908
+ const report = await getTokenEconomy().report();
909
+ return send({ ...report, runtime: this.getRuntimeContext() });
624
910
  }
625
911
  case '/api/v6/forget-consolidate': {
626
912
  const { getForgettingCurve } = await import('../brain/forgetting-curve.js');
@@ -968,6 +1254,75 @@ export class DashboardServer {
968
1254
  return send({});
969
1255
  }
970
1256
  }
1257
+ case '/api/v6/memory-intelligence': {
1258
+ const brain = getGlobalBrain();
1259
+ await brain.init();
1260
+ const limit = Number(qs.get('limit') ?? body.limit ?? 500);
1261
+ const events = brain.timeline({ limit: Number.isFinite(limit) ? Math.max(50, Math.min(2000, limit)) : 500 });
1262
+ const now = Date.now();
1263
+ const byCategory = {};
1264
+ const byAgent = {};
1265
+ const topTags = {};
1266
+ let sourceBacked = 0;
1267
+ let recent24h = 0;
1268
+ let recent7d = 0;
1269
+ let stale30d = 0;
1270
+ let highImportance = 0;
1271
+ for (const event of events) {
1272
+ const createdAt = new Date(event.createdAt).getTime();
1273
+ const ageMs = now - createdAt;
1274
+ byCategory[event.category] = (byCategory[event.category] ?? 0) + 1;
1275
+ byAgent[event.agentTool] = (byAgent[event.agentTool] ?? 0) + 1;
1276
+ if (event.importance >= 0.75)
1277
+ highImportance++;
1278
+ if (ageMs <= 24 * 60 * 60 * 1000)
1279
+ recent24h++;
1280
+ if (ageMs <= 7 * 24 * 60 * 60 * 1000)
1281
+ recent7d++;
1282
+ if (ageMs > 30 * 24 * 60 * 60 * 1000)
1283
+ stale30d++;
1284
+ const sources = Array.isArray(event.metadata?.sources) ? event.metadata.sources : [];
1285
+ if (sources.length > 0)
1286
+ sourceBacked++;
1287
+ const tags = Array.isArray(event.metadata?.tags) ? event.metadata.tags : [];
1288
+ for (const tag of tags) {
1289
+ const key = String(tag).trim();
1290
+ if (key)
1291
+ topTags[key] = (topTags[key] ?? 0) + 1;
1292
+ }
1293
+ }
1294
+ const sortRecord = (record) => Object.entries(record)
1295
+ .sort((a, b) => b[1] - a[1])
1296
+ .slice(0, 12)
1297
+ .map(([name, count]) => ({ name, count }));
1298
+ let seedDrafts = [];
1299
+ try {
1300
+ const { getMemorySeeder } = await import('../brain/memory-seeder.js');
1301
+ const seeder = getMemorySeeder();
1302
+ await seeder.init();
1303
+ seedDrafts = seeder.list(100);
1304
+ }
1305
+ catch {
1306
+ seedDrafts = [];
1307
+ }
1308
+ return send({
1309
+ totalEntries: brain.getStats().totalEntries,
1310
+ sampledEntries: events.length,
1311
+ sourceBacked,
1312
+ sourceCoverage: events.length ? sourceBacked / events.length : 0,
1313
+ recent24h,
1314
+ recent7d,
1315
+ stale30d,
1316
+ highImportance,
1317
+ byCategory: sortRecord(byCategory),
1318
+ byAgent: sortRecord(byAgent),
1319
+ topTags: sortRecord(topTags),
1320
+ openSeedDrafts: seedDrafts.filter(d => d.status === 'draft' || d.status === 'approved').length,
1321
+ pendingApprovals: seedDrafts.filter(d => d.status === 'draft').length,
1322
+ recent: events.slice(0, 12),
1323
+ generatedAt: new Date(),
1324
+ });
1325
+ }
971
1326
  case '/api/v6/memory-browser': {
972
1327
  const brain = getGlobalBrain();
973
1328
  await brain.init();
@@ -990,6 +1345,21 @@ export class DashboardServer {
990
1345
  }));
991
1346
  return send(results);
992
1347
  }
1348
+ case '/api/v6/memory-graph': {
1349
+ const { getMemoryGraph } = await import('../brain/memory-graph.js');
1350
+ const projectDirRaw = String(qs.get('projectDir') || body.projectDir || '').trim();
1351
+ const limit = Number(qs.get('limit') ?? body.limit ?? 400);
1352
+ const maxNodes = Number(qs.get('maxNodes') ?? body.maxNodes ?? 180);
1353
+ const minImportance = Number(qs.get('minImportance') ?? body.minImportance ?? 0);
1354
+ const projectId = projectDirRaw ? GlobalBrain.projectIdFor(projectDirRaw) : undefined;
1355
+ const snapshot = await getMemoryGraph().build({
1356
+ projectId,
1357
+ limit: Number.isFinite(limit) ? limit : 400,
1358
+ maxNodes: Number.isFinite(maxNodes) ? maxNodes : 180,
1359
+ minImportance: Number.isFinite(minImportance) ? minImportance : 0,
1360
+ });
1361
+ return send(snapshot);
1362
+ }
993
1363
  case '/api/v6/topology': {
994
1364
  // Assemble a real-time topology snapshot: agents + sub-agents + recent signal events
995
1365
  const projectDir = body.projectDir || process.cwd();
@@ -1082,6 +1452,20 @@ export class DashboardServer {
1082
1452
  }
1083
1453
  }
1084
1454
  catch { /* empty */ }
1455
+ try {
1456
+ const { getMemorySeeder } = await import('../brain/memory-seeder.js');
1457
+ const seeder = getMemorySeeder();
1458
+ await seeder.init();
1459
+ for (const draft of seeder.list(30)) {
1460
+ events.push({
1461
+ timestamp: new Date(draft.updatedAt || draft.createdAt),
1462
+ source: `seed:${draft.researchAgent}`,
1463
+ type: draft.status === 'seeded' ? 'memory-seeded' : 'seed-draft',
1464
+ detail: `[${draft.status}] ${draft.topic} | approval: ${draft.approvalMode}`,
1465
+ });
1466
+ }
1467
+ }
1468
+ catch { /* empty */ }
1085
1469
  try {
1086
1470
  const { getSubAgentBridge } = await import('../brain/subagent-bridge.js');
1087
1471
  const bridge = getSubAgentBridge();
@@ -1212,6 +1596,48 @@ export class DashboardServer {
1212
1596
  return { raw: 0, summary: 0, pattern: 0, principle: 0, turboCompression: 0 };
1213
1597
  }
1214
1598
  }
1599
+ getRuntimeContext() {
1600
+ const status = this.orchestrator.getStatus();
1601
+ const agents = (status.agents || []).map(a => a.toLowerCase());
1602
+ let currentAgentTool = null;
1603
+ let source = 'orchestrator';
1604
+ if (process.env.CODEX_SHELL || process.env.CODEX_THREAD_ID || process.env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE) {
1605
+ currentAgentTool = 'codex';
1606
+ source = 'environment';
1607
+ }
1608
+ else if (agents.some(a => a.includes('codex'))) {
1609
+ currentAgentTool = 'codex';
1610
+ }
1611
+ else if (agents.some(a => a.includes('claude'))) {
1612
+ currentAgentTool = 'claude-code';
1613
+ }
1614
+ else if (agents.some(a => a.includes('cursor'))) {
1615
+ currentAgentTool = 'cursor';
1616
+ }
1617
+ const labels = {
1618
+ codex: 'Codex',
1619
+ 'claude-code': 'Claude Code',
1620
+ cursor: 'Cursor',
1621
+ cline: 'Cline',
1622
+ copilot: 'Copilot',
1623
+ windsurf: 'Windsurf',
1624
+ 'kilo-code': 'Kilo Code',
1625
+ 'roo-code': 'Roo Code',
1626
+ opencode: 'OpenCode',
1627
+ aider: 'Aider',
1628
+ };
1629
+ const statusProvider = typeof status.provider === 'string' ? status.provider : '';
1630
+ const provider = statusProvider || (process.env.OPENAI_BASE_URL ? 'openai-compatible' : 'ollama');
1631
+ const model = typeof status.model === 'string' && status.model.trim() ? status.model : null;
1632
+ return {
1633
+ currentAgentTool,
1634
+ currentAgentDisplay: currentAgentTool ? labels[currentAgentTool] ?? currentAgentTool : 'Unknown agent',
1635
+ provider,
1636
+ model,
1637
+ source,
1638
+ note: 'Current agent is runtime context. Token spend rows are historical records and may include other agents or models.',
1639
+ };
1640
+ }
1215
1641
  getToolsData() {
1216
1642
  try {
1217
1643
  const status = this.orchestrator.getStatus();
@@ -1262,6 +1688,80 @@ export class DashboardServer {
1262
1688
  return { allowed: true, findings: [], riskScore: 0, summary: 'Firewall unavailable' };
1263
1689
  }
1264
1690
  }
1691
+ getDefaultWorkflowProjectDir() {
1692
+ const status = this.orchestrator.getStatus();
1693
+ const configured = String(status.projectDir || '').trim();
1694
+ const ww2br = 'D:\\New Game';
1695
+ if (configured)
1696
+ return configured;
1697
+ return ww2br;
1698
+ }
1699
+ async pathExists(pathToCheck) {
1700
+ try {
1701
+ const fs = await import('node:fs');
1702
+ return fs.existsSync(pathToCheck);
1703
+ }
1704
+ catch {
1705
+ return false;
1706
+ }
1707
+ }
1708
+ async getWorkflowAgentStatus(projectDir) {
1709
+ const knownAgents = [
1710
+ { name: 'claude-code', displayName: 'Claude Code' },
1711
+ { name: 'cursor', displayName: 'Cursor' },
1712
+ { name: 'cline', displayName: 'Cline' },
1713
+ { name: 'codex', displayName: 'Codex' },
1714
+ { name: 'copilot', displayName: 'GitHub Copilot' },
1715
+ { name: 'windsurf', displayName: 'Windsurf' },
1716
+ { name: 'kilo-code', displayName: 'Kilo Code' },
1717
+ { name: 'roo-code', displayName: 'Roo Code' },
1718
+ { name: 'opencode', displayName: 'OpenCode' },
1719
+ { name: 'aider', displayName: 'Aider' },
1720
+ ];
1721
+ let detected = [];
1722
+ let hooks = [];
1723
+ try {
1724
+ const { detectRunningAgents } = await import('../adapters/index.js');
1725
+ detected = await detectRunningAgents(projectDir);
1726
+ }
1727
+ catch {
1728
+ detected = [];
1729
+ }
1730
+ try {
1731
+ const { getHookInstaller } = await import('../brain/session-hooks.js');
1732
+ hooks = await getHookInstaller().audit(projectDir);
1733
+ }
1734
+ catch {
1735
+ hooks = [];
1736
+ }
1737
+ return knownAgents.map(agent => {
1738
+ const det = detected.find((d) => d.name === agent.name);
1739
+ const hook = hooks.find((h) => h.agent === agent.name);
1740
+ return {
1741
+ ...agent,
1742
+ detected: !!det,
1743
+ hookInstalled: !!hook,
1744
+ hookPath: hook?.installPath,
1745
+ };
1746
+ });
1747
+ }
1748
+ getWorkflowWarnings(projectDir, agents, timeline, projectScoped) {
1749
+ const warnings = [];
1750
+ if (!projectDir)
1751
+ warnings.push('No active project path is configured for the workflow view.');
1752
+ if (!agents.some(a => a.detected || a.hookInstalled)) {
1753
+ warnings.push('No agent integrations are detected or hooked for this project path.');
1754
+ }
1755
+ else if (!agents.some(a => a.hookInstalled)) {
1756
+ warnings.push('Agent tools may be present, but Shadow Brain hooks are not installed for this project path.');
1757
+ }
1758
+ if (!projectScoped && timeline.length > 0) {
1759
+ warnings.push('No project-scoped memories found; showing the latest global brain memories instead.');
1760
+ }
1761
+ if (!timeline.length)
1762
+ warnings.push('No memories are available yet. Run agents with Shadow Brain attached to build the timeline.');
1763
+ return warnings;
1764
+ }
1265
1765
  setupWebSocket() {
1266
1766
  this.wss.on('connection', (ws) => {
1267
1767
  this.clients.add(ws);
@@ -1293,6 +1793,9 @@ export class DashboardServer {
1293
1793
  evolutionGen: status.evolutionGeneration ?? 0,
1294
1794
  swarmConv: status.swarmConvergence ?? 0,
1295
1795
  });
1796
+ // Replay recent realtime activity so a late-connecting client's live
1797
+ // feed is populated immediately instead of waiting for the next event.
1798
+ this.send(ws, { type: 'activity-backlog', events: getBrainBus().recent(50) });
1296
1799
  ws.on('close', () => { this.clients.delete(ws); });
1297
1800
  ws.on('error', () => { this.clients.delete(ws); });
1298
1801
  });
@@ -1356,6 +1859,20 @@ export class DashboardServer {
1356
1859
  }
1357
1860
  async stop() {
1358
1861
  return new Promise((resolve) => {
1862
+ try {
1863
+ this.brainBusUnsub?.();
1864
+ }
1865
+ catch { /* ignore */ }
1866
+ this.brainBusUnsub = null;
1867
+ if (this.brainFileDebounce) {
1868
+ clearTimeout(this.brainFileDebounce);
1869
+ this.brainFileDebounce = null;
1870
+ }
1871
+ try {
1872
+ this.brainWatcher?.close();
1873
+ }
1874
+ catch { /* ignore */ }
1875
+ this.brainWatcher = null;
1359
1876
  for (const client of this.clients) {
1360
1877
  try {
1361
1878
  client.close();