nexus-prime 7.9.27 → 7.9.29

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 (45) hide show
  1. package/README.md +12 -10
  2. package/dist/agents/adapters/mcp/definitions.js +3 -2
  3. package/dist/agents/adapters/mcp/dispatch.js +9 -1
  4. package/dist/agents/adapters/mcp/envelope.js +2 -0
  5. package/dist/agents/adapters/mcp/handlers/orchestration.js +60 -18
  6. package/dist/agents/adapters/mcp/helpers.js +2 -2
  7. package/dist/agents/adapters/mcp/types.d.ts +1 -1
  8. package/dist/dashboard/app/api.js +30 -11
  9. package/dist/dashboard/app/index.html +28 -3
  10. package/dist/dashboard/app/main.js +34 -20
  11. package/dist/dashboard/app/state.js +4 -0
  12. package/dist/dashboard/app/styles/board.css +75 -0
  13. package/dist/dashboard/app/styles/context-log.css +154 -3
  14. package/dist/dashboard/app/styles/learning.css +204 -0
  15. package/dist/dashboard/app/styles/memory.css +169 -1
  16. package/dist/dashboard/app/styles/tokens.css +5 -0
  17. package/dist/dashboard/app/views/board.js +91 -23
  18. package/dist/dashboard/app/views/context-log.js +130 -7
  19. package/dist/dashboard/app/views/learning.js +200 -0
  20. package/dist/dashboard/app/views/memory.js +139 -26
  21. package/dist/dashboard/app/views/repo.js +168 -5
  22. package/dist/dashboard/routes/events.js +71 -3
  23. package/dist/dashboard/routes/graph.js +50 -0
  24. package/dist/dashboard/routes/learning.d.ts +2 -0
  25. package/dist/dashboard/routes/learning.js +213 -0
  26. package/dist/dashboard/routes/run-artifacts.d.ts +5 -0
  27. package/dist/dashboard/routes/run-artifacts.js +107 -0
  28. package/dist/dashboard/routes/runtime.js +98 -38
  29. package/dist/dashboard/routes/surfaces.js +2 -1
  30. package/dist/dashboard/selectors/operate-selector.js +105 -12
  31. package/dist/dashboard/server.js +2 -0
  32. package/dist/dashboard/types.d.ts +4 -1
  33. package/dist/engines/index.d.ts +2 -0
  34. package/dist/engines/index.js +1 -0
  35. package/dist/engines/learning/index.d.ts +1 -0
  36. package/dist/engines/learning/index.js +1 -0
  37. package/dist/engines/learning-network.d.ts +150 -0
  38. package/dist/engines/learning-network.js +452 -0
  39. package/dist/engines/orchestrator/decision-spine.d.ts +30 -3
  40. package/dist/engines/orchestrator/decision-spine.js +195 -23
  41. package/dist/index.js +3 -1
  42. package/dist/licensing/enforcement.js +18 -1
  43. package/dist/phantom/runtime.d.ts +15 -1
  44. package/dist/phantom/runtime.js +144 -4
  45. package/package.json +1 -1
package/README.md CHANGED
@@ -38,9 +38,9 @@
38
38
  <a href="https://www.producthunt.com/products/nexus-prime?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-nexus-prime" target="_blank" rel="noopener noreferrer"><img alt="Nexus-Prime — Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1096831&theme=dark&t=1773345508816"></a>
39
39
  </div>
40
40
 
41
- > **Coding agents were never built to remember. Nexus Prime gives them a memory.**
41
+ > **Stop restarting your coding agents from zero.**
42
42
  >
43
- > Install once. Every coding agent on your machine gets smarter together.
43
+ > Nexus Prime gives Claude Code, Codex, Cursor, Windsurf, OpenCode, Aider, and the rest of your local agent stack one shared memory, one proof trail, and one dashboard for what actually happened.
44
44
 
45
45
  ---
46
46
 
@@ -74,14 +74,15 @@ Agents should start with `nexus_session_bootstrap`, then route the raw request t
74
74
 
75
75
  ---
76
76
 
77
- ## Provenance and social proof
77
+ ## Why teams install it
78
78
 
79
- Nexus Prime now treats provenance as a product surface, not a footer afterthought.
79
+ Nexus Prime is the local-first control plane for serious AI-assisted coding.
80
80
 
81
- - Every Nexus-enabled PR can carry a **Nexus Prime Presence** block that links back to https://nexus-prime.cfd/ and states whether Nexus observed, verified, assisted, authored, or published the change.
82
- - Nexus-generated commits use the repo-local Git-native `Co-Authored-By` trailer for the Nexus Prime GitHub identity; human-only commits are not falsely claimed.
83
- - Generated artifacts can include a Nexus Prime signature with run ID, verification state, human-review state, and website proof.
84
- - Opt-in telemetry can send privacy-safe PLG events such as `pr_presence_added`, `commit_attributed`, `document_signed`, `release_verified`, and `proof_badge_viewed` to the configured Supabase endpoint. Raw prompts, code, repo names, branch names, file paths, secrets, and tokens are redacted by default.
81
+ - **Shared memory for every agent.** Your tools stop acting like strangers and start carrying useful context across sessions.
82
+ - **Lower token waste.** Nexus routes agents toward the files and facts that matter instead of rereading the repo every turn.
83
+ - **Runtime truth you can inspect.** The dashboard shows runs, memory, token savings, and agent activity instead of hiding the messy parts.
84
+ - **Safer multi-file work.** Planning, verification, and handoff state stay attached to the repo, so interrupted work is easier to resume and review.
85
+ - **Local by default.** Your code and agent memory stay on your machine unless you explicitly opt into something else.
85
86
 
86
87
  ---
87
88
 
@@ -95,7 +96,7 @@ One tracked session was measured at **100,000,000 tokens**. Ninety-nine point fo
95
96
 
96
97
  This is **agent amnesia** — and it's the quiet tax on every hour you spend with AI-assisted coding.
97
98
 
98
- **Nexus Prime ends it.**
99
+ **Nexus Prime turns that cold start into a remembered workspace.**
99
100
 
100
101
  ---
101
102
 
@@ -458,7 +459,8 @@ Nexus Prime was designed privacy-first, because the code on your machine is your
458
459
  |---------|-------------------|
459
460
  | 💬 [**Discord**](https://discord.gg/tByGZgk5gS) | Real-time help, show-and-tell, feature ideas |
460
461
  | 🔴 [**Reddit — r/Nexus_Prime**](https://www.reddit.com/r/Nexus_Prime/) | Long-form posts, releases, community wins |
461
- | 🐦 [**X / Twitter**](https://x.com/nexusprime_ai) | Launch announcements, tips, updates |
462
+ | 🐦 [**X / Twitter**](https://x.com/getnexusprime) | Launch announcements, tips, updates |
463
+ | 📸 [**Instagram**](https://www.instagram.com/nexus_prime.cfd/) | Product clips, launch updates, behind-the-scenes |
462
464
  | 📧 [**adarsh@nexus-prime.cfd**](mailto:adarsh@nexus-prime.cfd) | License requests, upgrades, pilots, enterprise |
463
465
  | 📧 [**hello@nexus-prime.cfd**](mailto:hello@nexus-prime.cfd) | General support, press, community |
464
466
  | 🌐 [**nexus-prime.cfd**](https://nexus-prime.cfd) | Product site, demos, pricing, setup guides |
@@ -238,7 +238,7 @@ export function buildMcpToolDefinitions() {
238
238
  properties: {
239
239
  goal: { type: 'string', description: 'Raw goal or task description for this session' },
240
240
  files: { type: 'array', items: { type: 'string' }, description: 'Optional candidate file constraints' },
241
- detailLevel: { type: 'string', enum: ['compact', 'standard', 'debug'], description: 'Response verbosity; defaults to compact for MCP callers' },
241
+ detailLevel: { type: 'string', enum: ['compact', 'standard', 'debug', 'full'], description: 'Response verbosity; defaults to compact for MCP callers' },
242
242
  depth: { type: 'string', enum: ['fast', 'deep'], description: 'Optional bootstrap depth override; defaults to fast unless debug mode is requested' },
243
243
  include: { type: 'array', items: { type: 'string' }, description: 'Optional rich payload sections to include for advanced clients' },
244
244
  intent: { type: 'string', enum: ['inspect', 'plan', 'mutate'], description: 'Requested orchestration stance for the bootstrap summary' },
@@ -268,7 +268,7 @@ export function buildMcpToolDefinitions() {
268
268
  properties: {
269
269
  prompt: { type: 'string', description: 'Raw prompt or objective to orchestrate end-to-end' },
270
270
  files: { type: 'array', items: { type: 'string' }, description: 'Optional hard constraints for candidate files' },
271
- detailLevel: { type: 'string', enum: ['compact', 'standard', 'debug'], description: 'Response verbosity; defaults to compact for MCP callers' },
271
+ detailLevel: { type: 'string', enum: ['compact', 'standard', 'debug', 'full'], description: 'Response verbosity; defaults to compact for MCP callers' },
272
272
  intent: { type: 'string', enum: ['inspect', 'plan', 'mutate'], description: 'Requested orchestration stance; inspect/plan should stay read-only' },
273
273
  topology: { type: 'string', enum: ['auto', 'manager-tools', 'handoff', 'dag-pool', 'worktree-swarm'], description: 'Requested orchestration topology hint' },
274
274
  workers: { type: 'number', description: 'Optional worker override' },
@@ -282,6 +282,7 @@ export function buildMcpToolDefinitions() {
282
282
  executionPreset: { type: 'string', enum: ['fast', 'balanced', 'deep', 'release'], description: 'Optional execution preset that maps orchestration depth, verification strictness, and backend routing' },
283
283
  background: { type: 'boolean', description: 'Compatibility flag; nexus_orchestrate already returns a queued hiring preflight by default and continues in the async gate.' },
284
284
  async: { type: 'boolean', description: 'Alias for background; useful for clients that prefer explicit async orchestration.' },
285
+ wait: { type: 'boolean', description: 'Alias for inline/sync execution. Waits up to the MCP-safe bounded window and returns the final result when it completes in time.' },
285
286
  waitMs: { type: 'number', description: 'Advanced/debug bounded wait for inline execution. Clamped to 45 seconds; normal orchestrate calls return a queued preflight immediately.' },
286
287
  inline: { type: 'boolean', description: 'Advanced/debug only: wait for inline orchestrate output instead of the default fast queued preflight.' }
287
288
  },
@@ -101,7 +101,10 @@ function shouldReturnQueuedReceipt(toolName, args) {
101
101
  if (!SLOW_TOOLS.has(toolName))
102
102
  return false;
103
103
  if (toolName === 'nexus_orchestrate') {
104
- return !isTruthyFlag(args.inline) && !isTruthyFlag(args.sync) && !isTruthyFlag(args.blocking);
104
+ return !isTruthyFlag(args.inline)
105
+ && !isTruthyFlag(args.sync)
106
+ && !isTruthyFlag(args.blocking)
107
+ && !isTruthyFlag(args.wait);
105
108
  }
106
109
  return isTruthyFlag(args.background)
107
110
  || isTruthyFlag(args.async)
@@ -109,8 +112,13 @@ function shouldReturnQueuedReceipt(toolName, args) {
109
112
  || isTruthyFlag(args.detach);
110
113
  }
111
114
  function resolveMaxSyncMs(toolName, args) {
115
+ const explicitWait = isTruthyFlag(args.wait)
116
+ || isTruthyFlag(args.inline)
117
+ || isTruthyFlag(args.sync)
118
+ || isTruthyFlag(args.blocking);
112
119
  return coerceBoundedWaitMs(args.waitMs)
113
120
  ?? coerceBoundedWaitMs(args.maxSyncMs)
121
+ ?? (toolName === 'nexus_orchestrate' && explicitWait ? MAX_CLIENT_SYNC_WAIT_MS : undefined)
114
122
  ?? (toolName === 'nexus_orchestrate' ? ORCHESTRATE_DEFAULT_MAX_SYNC_MS : DEFAULT_MAX_SYNC_MS);
115
123
  }
116
124
  function asStringList(value) {
@@ -22,6 +22,8 @@ function renderTable(rows, maxRows = 20) {
22
22
  }
23
23
  /** Build a compact timing + savings footer line for tool responses. */
24
24
  function buildMcpFooter(meta) {
25
+ if (process.env.NEXUS_MCP_RESPONSE_FOOTER !== '1')
26
+ return '';
25
27
  const ms = meta.durationMs;
26
28
  const timing = ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
27
29
  return `\n\n─── nexus-prime · ${meta.toolName} · ${timing} · dashboard: http://localhost:${DASH_PORT}/#runtime ───`;
@@ -275,7 +275,8 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
275
275
  const timings = {
276
276
  totalMs: Date.now() - callStartedAt,
277
277
  };
278
- const payload = detailLevel === 'debug'
278
+ const fullBootstrapDetails = detailLevel === 'debug' || detailLevel === 'full';
279
+ const payload = fullBootstrapDetails
279
280
  ? {
280
281
  depth: bootstrap.depth,
281
282
  workspace,
@@ -390,7 +391,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
390
391
  return '';
391
392
  }
392
393
  })(),
393
- ...(detailLevel === 'debug' ? [
394
+ ...(fullBootstrapDetails ? [
394
395
  `Bootstrap depth: ${bootstrap.depth || bootstrapDepth}`,
395
396
  `Execution mode: ${bootstrap.recommendedExecutionMode || 'autonomous'}`,
396
397
  `Shortlist: ${bootstrap.shortlist?.skills?.slice(0, 3).join(', ') || 'none'} (skills), ${bootstrap.shortlist?.specialists?.slice(0, 3).join(', ') || 'none'} (specialists)`,
@@ -410,7 +411,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
410
411
  `Bootstrap status: ${bootstrap.clientBootstrapStatus?.clients?.length || 0} client manifests tracked`,
411
412
  ] : []),
412
413
  ]),
413
- detailLevel === 'debug' && bootstrap.tokenOptimization?.autoApplied && bootstrap.tokenOptimization?.plan
414
+ fullBootstrapDetails && bootstrap.tokenOptimization?.autoApplied && bootstrap.tokenOptimization?.plan
414
415
  ? `Auto token plan\n\`\`\`txt\n${bootstrap.tokenOptimization.plan}\n\`\`\``
415
416
  : '',
416
417
  formatJsonDetails('Structured details', payload),
@@ -547,7 +548,36 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
547
548
  intent: requestedIntent,
548
549
  topology: requestedTopology,
549
550
  };
551
+ const verboseDetails = detailLevel === 'debug' || detailLevel === 'full';
550
552
  const compactPayload = {
553
+ workspace,
554
+ runId: execution.runId,
555
+ state: execution.state,
556
+ summary: summarizeExecution(execution),
557
+ resultPreview: truncateText(execution.result || summarizeExecution(execution), 700),
558
+ artifactsPath: execution.artifactsPath,
559
+ planner: execution.plannerState
560
+ ? {
561
+ selectedCrew: execution.plannerState.selectedCrew?.name,
562
+ selectedSpecialists: execution.plannerState.selectedSpecialists.slice(0, 4).map((specialist) => specialist.name),
563
+ selectedWorkflows: execution.plannerState.selectedWorkflows.slice(0, 4),
564
+ }
565
+ : undefined,
566
+ worktreeHealth: runtimeUsage.worktreeHealth
567
+ ? {
568
+ repoRoot: runtimeUsage.worktreeHealth.repoRoot,
569
+ overall: runtimeUsage.worktreeHealth.overall,
570
+ brokenEntries: runtimeUsage.worktreeHealth.brokenEntries,
571
+ repairedEntries: runtimeUsage.worktreeHealth.repairedEntries,
572
+ issues: runtimeUsage.worktreeHealth.issues,
573
+ }
574
+ : undefined,
575
+ modelRoute: selectionSummary.modelRoute,
576
+ verifiedWorkers,
577
+ continuationChildren: execution.continuationChildren.length,
578
+ timings,
579
+ };
580
+ const standardPayload = {
551
581
  workspace,
552
582
  runId: execution.runId,
553
583
  state: execution.state,
@@ -616,31 +646,43 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
616
646
  timings,
617
647
  payloadRef,
618
648
  };
619
- const payload = detailLevel === 'debug' ? debugPayload : compactPayload;
649
+ const payload = verboseDetails
650
+ ? debugPayload
651
+ : detailLevel === 'standard'
652
+ ? standardPayload
653
+ : compactPayload;
654
+ const verboseResponse = detailLevel !== 'compact';
655
+ const stateLine = execution.state === 'inspected'
656
+ ? 'Orchestrated run inspected (read-only advisory; no diff expected).'
657
+ : `Orchestrated run ${execution.state}.`;
620
658
  return {
621
659
  content: [{
622
660
  type: 'text',
623
661
  text: [
624
662
  upfrontTokenNote ? `[Token plan] ${upfrontTokenNote}` : '',
625
- `Orchestrated run ${execution.state}.`,
663
+ stateLine,
626
664
  formatBullets([
627
665
  `Workspace: ${workspace.repoName} (${workspace.workspaceSource})`,
628
666
  `Run ID: ${execution.runId}`,
629
667
  `Summary: ${summarizeExecution(execution)}`,
630
668
  `Crew: ${execution.plannerState?.selectedCrew?.name || 'baseline path'}`,
631
- `Decomposition: ${selectionSummary.phaseCount} phase(s), ${selectionSummary.workerLaneCount} worker lane(s), ${selectionSummary.mode}`,
632
669
  `Hired/selected: crew ${selectionSummary.crew}; specialists ${formatSelectionList(selectionSummary.specialists)}; workflows ${formatSelectionList(selectionSummary.workflows)}; skills ${formatSelectionList(selectionSummary.skills)}`,
633
- `Model route: ${formatModelRoute(selectionSummary.modelRoute)}`,
634
- `Budget route: ${selectionSummary.budgetRoute || 'budget pending'}`,
635
- selectionSummary.agentFlow?.stages?.length
636
- ? `AgentFlow gates: ${selectionSummary.agentFlow.stages.map((stage) => `${stage.stage}:${stage.ownerRole}`).join(' -> ')}`
637
- : null,
638
- `Selection audit: ${selectionSummary.auditSelected} selected, ${selectionSummary.auditRejected} rejected`,
670
+ ...(verboseResponse ? [
671
+ `Decomposition: ${selectionSummary.phaseCount} phase(s), ${selectionSummary.workerLaneCount} worker lane(s), ${selectionSummary.mode}`,
672
+ `Model route: ${formatModelRoute(selectionSummary.modelRoute)}`,
673
+ `Budget route: ${selectionSummary.budgetRoute || 'budget pending'}`,
674
+ selectionSummary.agentFlow?.stages?.length
675
+ ? `AgentFlow gates: ${selectionSummary.agentFlow.stages.map((stage) => `${stage.stage}:${stage.ownerRole}`).join(' -> ')}`
676
+ : null,
677
+ `Selection audit: ${selectionSummary.auditSelected} selected, ${selectionSummary.auditRejected} rejected`,
678
+ ] : [
679
+ `Model route: ${formatModelRoute(selectionSummary.modelRoute)}`,
680
+ ]),
639
681
  `Verification: ${verifiedWorkers}/${execution.workerResults.length} worker(s) verified`,
640
- `Tokens: saved ${Number(execution.tokenTelemetry?.savedTokens || 0).toLocaleString()} · compression ${Number(execution.tokenTelemetry?.compressionPct || 0)}%`,
682
+ verboseResponse ? `Tokens: saved ${Number(execution.tokenTelemetry?.savedTokens || 0).toLocaleString()} · compression ${Number(execution.tokenTelemetry?.compressionPct || 0)}%` : null,
641
683
  autoTokenApplyNote || null,
642
- `Payload ref: ${workspace.stateKey} · ${detailLevel}`,
643
- ...(detailLevel === 'debug' ? [
684
+ verboseResponse ? `Payload ref: ${workspace.stateKey} · ${detailLevel}` : null,
685
+ ...(verboseDetails ? [
644
686
  `Specialists: ${execution.plannerState?.selectedSpecialists.map((specialist) => specialist.name).slice(0, 4).join(', ') || 'none selected'}`,
645
687
  `Assets: ${(execution.activeSkills || []).length} skills · ${(execution.activeWorkflows || []).length} workflows · ${(runtimeUsage.artifactSelectionAudit?.selected?.length || 0)} audited selections`,
646
688
  `Task graph: ${runtimeUsage.taskGraph?.phases?.length || execution.taskGraph?.phases?.length || 0} phases · ${runtimeUsage.workerPlan?.totalWorkers || execution.workerPlan?.totalWorkers || 0} workers`,
@@ -652,9 +694,9 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
652
694
  `Payload ref: ${workspace.stateKey} · ${detailLevel}`,
653
695
  ] : []),
654
696
  ]),
655
- detailLevel === 'debug' && execution.result ? `Result\n\`\`\`\n${execution.result}\n\`\`\`` : `Result preview\n\`\`\`\n${truncateText(execution.result || summarizeExecution(execution), 900)}\n\`\`\``,
697
+ verboseDetails && execution.result ? `Result\n\`\`\`\n${execution.result}\n\`\`\`` : `Result preview\n\`\`\`\n${truncateText(execution.result || summarizeExecution(execution), 900)}\n\`\`\``,
656
698
  formatJsonDetails('Structured details', payload),
657
- hctx.formatRemainingProtocolSteps(),
699
+ verboseResponse ? hctx.formatRemainingProtocolSteps() : '',
658
700
  ].filter(Boolean).join('\n\n'),
659
701
  }],
660
702
  };
@@ -684,7 +726,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
684
726
  runtimeUsage?.skipReasons?.length ? `Skipped stages: ${runtimeUsage.skipReasons.join(' · ')}` : 'Skipped stages: not recorded',
685
727
  ]),
686
728
  formatJsonDetails('Structured details', payload),
687
- hctx.formatRemainingProtocolSteps(),
729
+ detailLevel !== 'compact' ? hctx.formatRemainingProtocolSteps() : '',
688
730
  ].join('\n\n'),
689
731
  }],
690
732
  };
@@ -90,7 +90,7 @@ export function formatJsonDetails(label, value) {
90
90
  }
91
91
  export function normalizeDetailLevel(value) {
92
92
  const normalized = String(value ?? 'compact').toLowerCase();
93
- if (normalized === 'standard' || normalized === 'debug')
93
+ if (normalized === 'standard' || normalized === 'debug' || normalized === 'full')
94
94
  return normalized;
95
95
  return 'compact';
96
96
  }
@@ -100,7 +100,7 @@ export function normalizeBootstrapDepth(value, detailLevel) {
100
100
  return 'deep';
101
101
  if (normalized === 'fast')
102
102
  return 'fast';
103
- return detailLevel === 'debug' ? 'deep' : 'fast';
103
+ return detailLevel === 'debug' || detailLevel === 'full' ? 'deep' : 'fast';
104
104
  }
105
105
  export function normalizeResponseIntent(value) {
106
106
  const normalized = String(value ?? 'inspect').toLowerCase();
@@ -14,7 +14,7 @@ import type { SessionDNAManager } from '../../../engines/session-dna.js';
14
14
  import type { NgramIndex } from '../../../engines/ngram-index.js';
15
15
  export type McpToolProfile = 'autonomous' | 'full';
16
16
  export type LifecyclePhase = 'pre-bootstrap' | 'bootstrapped' | 'orchestrated' | 'working' | 'closing';
17
- export type ResponseDetailLevel = 'compact' | 'standard' | 'debug';
17
+ export type ResponseDetailLevel = 'compact' | 'standard' | 'debug' | 'full';
18
18
  export type ResponseIntent = 'inspect' | 'plan' | 'mutate';
19
19
  export type ResponseTopology = 'auto' | 'manager-tools' | 'handoff' | 'dag-pool' | 'worktree-swarm';
20
20
  /** Session-level telemetry tracker. Moved to types.ts so handler files can
@@ -8,6 +8,18 @@
8
8
  import { CACHE_TTL, S, bus } from './state.js';
9
9
 
10
10
  const _cache = new Map();
11
+ const DEFAULT_TIMEOUT_MS = 5_000;
12
+
13
+ function _timeoutFor(url, override) {
14
+ if (Number.isFinite(Number(override))) return Number(override);
15
+ if (url.includes('/api/tokens/')) return 1_800;
16
+ if (url.includes('/api/license')) return 2_500;
17
+ if (url.includes('/api/runtimes')) return 3_000;
18
+ if (url.includes('/api/dashboard/surface/')) return 4_000;
19
+ if (url.includes('/api/dashboard/summary')) return 4_500;
20
+ if (url.includes('/api/knowledge-topology')) return 7_000;
21
+ return DEFAULT_TIMEOUT_MS;
22
+ }
11
23
 
12
24
  function _scopedUrl(url) {
13
25
  if (typeof url !== 'string' || !url.startsWith('/api/')) return url;
@@ -27,32 +39,39 @@ function _scopedUrl(url) {
27
39
  * then kick off a background refresh. Callers get the fast synchronous hit
28
40
  * for p50 <5ms; the next render cycle gets the fresh value.
29
41
  */
30
- export async function api(url, ttl = CACHE_TTL) {
42
+ export async function api(url, ttl = CACHE_TTL, opts = {}) {
31
43
  const requestUrl = _scopedUrl(url);
32
44
  const hit = _cache.get(requestUrl);
33
45
  const fresh = !hit || (Date.now() - hit.ts >= ttl);
34
46
 
35
- // Return stale data synchronously while refresh fires in background.
36
- const refreshPromise = fresh ? _fetch(requestUrl) : Promise.resolve(hit.data);
37
-
38
- if (hit && !fresh) {
39
- // Background refresh — don't await, just schedule.
40
- _fetch(requestUrl).catch(() => {});
47
+ if (hit) {
48
+ if (fresh) {
49
+ _fetch(requestUrl, opts).catch(() => {});
50
+ bus.emit('api:stale', { url: requestUrl, ageMs: Date.now() - hit.ts });
51
+ } else {
52
+ _fetch(requestUrl, opts).catch(() => {});
53
+ }
54
+ return hit.data;
41
55
  }
42
56
 
43
- return refreshPromise;
57
+ return _fetch(requestUrl, opts);
44
58
  }
45
59
 
46
- async function _fetch(url) {
60
+ async function _fetch(url, opts = {}) {
61
+ const controller = new AbortController();
62
+ const timeout = setTimeout(() => controller.abort(), _timeoutFor(url, opts.timeoutMs));
47
63
  try {
48
- const r = await fetch(url);
64
+ const r = await fetch(url, { signal: controller.signal });
49
65
  if (!r.ok) return null;
50
66
  const d = await r.json();
51
67
  _cache.set(url, { data: d, ts: Date.now() });
52
68
  bus.emit('cache:updated', url);
53
69
  return d;
54
- } catch {
70
+ } catch (err) {
71
+ bus.emit('api:timeout', { url, error: err?.name === 'AbortError' ? 'timeout' : String(err?.message || err) });
55
72
  return null;
73
+ } finally {
74
+ clearTimeout(timeout);
56
75
  }
57
76
  }
58
77
 
@@ -12,6 +12,7 @@
12
12
  <link rel="stylesheet" href="./styles/workforce.css">
13
13
  <link rel="stylesheet" href="./styles/memory.css">
14
14
  <link rel="stylesheet" href="./styles/context-log.css">
15
+ <link rel="stylesheet" href="./styles/learning.css">
15
16
  <link rel="stylesheet" href="./styles/governance.css">
16
17
  <link rel="stylesheet" href="./styles/trust.css">
17
18
  <link rel="stylesheet" href="./styles/animation.css">
@@ -56,6 +57,7 @@ if(location.protocol==='file:'){
56
57
  <a class="nav-item" data-nav="runtime" href="#runtime">⚡ Runtime</a>
57
58
  <a class="nav-item" data-nav="workforce" href="#workforce">Workforce</a>
58
59
  <a class="nav-item" data-nav="memory" href="#memory">Memory</a>
60
+ <a class="nav-item" data-nav="learning" href="#learning">Learning</a>
59
61
  <a class="nav-item" data-nav="context-log" href="#context-log">Context Log</a>
60
62
  <a class="nav-item" data-nav="repo" href="#repo">Repo</a>
61
63
  <a class="nav-item" data-nav="knowledge" href="#knowledge">Knowledge</a>
@@ -111,6 +113,8 @@ if(location.protocol==='file:'){
111
113
  <div class="hero-stat"><div class="metric-val" id="m-ops">—</div><div class="metric-lbl">Operatives</div><canvas id="spark-ops" class="sparkline" width="80" height="24" aria-hidden="true"></canvas></div>
112
114
  </div>
113
115
 
116
+ <div id="model-router-panel" class="model-router-panel card" aria-label="Model and context router"></div>
117
+
114
118
  <!-- Live strip + kanban -->
115
119
  <div id="agents-live-strip"></div>
116
120
  <div id="kanban-board">
@@ -189,6 +193,7 @@ if(location.protocol==='file:'){
189
193
 
190
194
  <div class="memory-toolbar">
191
195
  <button class="btn btn-sm" id="mem-graph-max-btn">Maximize graph</button>
196
+ <button class="btn btn-sm" id="mem-graph-focus-btn">Focus selected</button>
192
197
  <button class="btn btn-sm" id="mem-browse-btn">Browse memories</button>
193
198
  </div>
194
199
  <div id="graph-container" class="card">
@@ -228,13 +233,33 @@ if(location.protocol==='file:'){
228
233
  <div id="context-log-view"></div>
229
234
  </section>
230
235
 
236
+ <!-- LEARNING ───────────────────────────────────────────────── -->
237
+ <section class="view-panel" data-view="learning" aria-label="Learning network">
238
+ <div id="learning-view"></div>
239
+ </section>
240
+
231
241
  <!-- REPO ───────────────────────────────────────────────────── -->
232
242
  <section class="view-panel" data-view="repo" aria-label="Repo graph">
233
243
  <div class="shd">Repo knowledge graph</div>
234
- <div id="repo-graph-header" style="display:flex;align-items:center;gap:12px;padding:0 2px 10px;font-size:var(--text-sm);color:var(--text-muted)">
244
+ <div id="repo-graph-header" class="repo-graph-header">
235
245
  <span id="repo-graph-meta">—</span>
236
- <button id="repo-build-btn" class="btn btn-sm" style="margin-left:auto">Build graph</button>
237
- <input id="repo-search-input" type="text" placeholder="Search nodes…" style="padding:3px 8px;border:1px solid var(--border);border-radius:4px;background:var(--surface-1);color:var(--text);font-size:var(--text-sm);width:180px" autocomplete="off">
246
+ <div class="repo-graph-actions">
247
+ <button id="repo-build-btn" class="btn btn-sm">Build graph</button>
248
+ <button id="repo-memory-focus-btn" class="btn btn-sm">Memory overlay</button>
249
+ <input id="repo-search-input" class="repo-graph-search" type="text" placeholder="Search nodes…" autocomplete="off">
250
+ <div class="repo-graph-controls" aria-label="Repo graph navigation controls">
251
+ <button id="repo-zoom-out-btn" class="repo-graph-control" title="Zoom out">-</button>
252
+ <button id="repo-zoom-in-btn" class="repo-graph-control" title="Zoom in">+</button>
253
+ <button id="repo-fit-btn" class="repo-graph-control repo-graph-control-wide" title="Fit graph">Fit</button>
254
+ <button id="repo-reset-btn" class="repo-graph-control repo-graph-control-wide" title="Reset view">Reset</button>
255
+ <button id="repo-pan-left-btn" class="repo-graph-control" title="Pan left">&lt;</button>
256
+ <button id="repo-pan-right-btn" class="repo-graph-control" title="Pan right">&gt;</button>
257
+ <button id="repo-pan-up-btn" class="repo-graph-control" title="Pan up">^</button>
258
+ <button id="repo-pan-down-btn" class="repo-graph-control" title="Pan down">v</button>
259
+ <button id="repo-graph-max-btn" class="repo-graph-control repo-graph-control-wide" title="Maximize graph">Max</button>
260
+ <span id="repo-viewport-badge" class="repo-viewport-badge">100%</span>
261
+ </div>
262
+ </div>
238
263
  </div>
239
264
  <div class="card" style="position:relative;overflow:hidden">
240
265
  <div id="repo-graph-container" style="width:100%;height:480px;position:relative">
@@ -15,6 +15,7 @@ import { init as initCmdBar } from './widgets/command-bar.js';
15
15
  import * as Board from './views/board.js';
16
16
  import * as Workforce from './views/workforce.js';
17
17
  import * as Memory from './views/memory.js';
18
+ import * as Learning from './views/learning.js';
18
19
  import * as ContextLog from './views/context-log.js';
19
20
  import * as Knowledge from './views/knowledge.js';
20
21
  import * as Repo from './views/repo.js';
@@ -54,6 +55,7 @@ navRegister('board', Board.load);
54
55
  navRegister('runtime', Runtime.load);
55
56
  navRegister('workforce', Workforce.load);
56
57
  navRegister('memory', Memory.load);
58
+ navRegister('learning', Learning.load);
57
59
  navRegister('context-log', ContextLog.load);
58
60
  navRegister('repo', Repo.load);
59
61
  navRegister('knowledge', Knowledge.load);
@@ -89,6 +91,10 @@ setOnEvent(evt => {
89
91
  if (tab === 'memory' && evt.category === 'memory') {
90
92
  _refreshMemorySoon();
91
93
  }
94
+ if (tab === 'learning' && (evt.category === 'memory' || String(evt.type||'').startsWith('orchestration.'))) {
95
+ bustCache('/api/learning/packets?limit=24');
96
+ Learning.load();
97
+ }
92
98
  if (tab === 'governance' && evt.category === 'darwin') {
93
99
  Governance.load();
94
100
  }
@@ -123,6 +129,9 @@ setOnEvent(evt => {
123
129
  if (tab === 'context-log') {
124
130
  ContextLog.load();
125
131
  }
132
+ if (tab === 'learning') {
133
+ Learning.load();
134
+ }
126
135
  }
127
136
  // Workspace root promoted (e.g. from bootstrap hint) — refresh header immediately.
128
137
  if (String(evt.type||'') === 'workspace.changed') {
@@ -184,8 +193,9 @@ async function loadProjects() {
184
193
  ]);
185
194
  S.projects = Array.isArray(projectsD) ? projectsD : (projectsD?.projects||[]);
186
195
  S.runtimes = Array.isArray(runtimesD) ? runtimesD : (runtimesD?.runtimes||[]);
187
- _selectDefaultRuntime();
196
+ const scopeChanged = _selectDefaultRuntime();
188
197
  _renderProjectSelectors();
198
+ return scopeChanged;
189
199
  }
190
200
 
191
201
  async function loadCompanies() {
@@ -210,7 +220,7 @@ function _renderProjectSelectors() {
210
220
  }
211
221
 
212
222
  function _selectDefaultRuntime() {
213
- if (S.scope.runtimeId || !(S.runtimes||[]).length) return;
223
+ if (S.scope.runtimeId || !(S.runtimes||[]).length) return false;
214
224
  const current = S.workspace?.repoRoot || S.workspace?.workspaceRoot || '';
215
225
  const candidates = (S.runtimes||[]).filter(r => r?.runtimeId);
216
226
  const exact = current ? candidates.find(r => r.repoIdentity?.repoRoot === current) : null;
@@ -228,7 +238,10 @@ function _selectDefaultRuntime() {
228
238
  return root && root !== '/';
229
239
  });
230
240
  const selected = (exact && !currentLooksLikeHome ? exact : null) || remoteRepo || deepestRepo || candidates[0];
231
- S.scope.runtimeId = selected?.runtimeId || null;
241
+ const nextRuntimeId = selected?.runtimeId || null;
242
+ const changed = nextRuntimeId !== S.scope.runtimeId;
243
+ S.scope.runtimeId = nextRuntimeId;
244
+ return changed;
232
245
  }
233
246
 
234
247
  /* ─────────────────── Header / ticker ─────────────────── */
@@ -344,29 +357,30 @@ async function bootstrap() {
344
357
  _renderHeader(false);
345
358
 
346
359
  await loadWorkspace();
347
- await loadProjects();
348
-
349
- // Fast first paint: board + companies + repo-tree + license
350
- const [, , , lic] = await Promise.all([
351
- Board.load(),
352
- loadCompanies(),
353
- loadRepoTree(),
354
- api('/api/license', 60000),
355
- ]);
356
- S.license = lic;
357
-
358
- // Populate agent selector
359
- const agentSel = $('ctx-agent');
360
- if (agentSel && S.synapseHealth.length) {
361
- agentSel.innerHTML = '<option value="">All agents</option>' +
362
- S.synapseHealth.map(o=>`<option value="${esc(o.id||o.operativeId)}">${esc(o.role||o.name||o.id||'agent')}</option>`).join('');
360
+ const scopeChanged = await loadProjects();
361
+ if (scopeChanged) {
362
+ bustCache('/api/workspace');
363
+ bustCache('/api/repo-tree');
364
+ await loadWorkspace();
363
365
  }
364
366
 
365
- // Wire router after first paint
367
+ // Wire router before slow side panels. Each view owns progressive loading.
366
368
  routerStart();
367
369
 
368
370
  // Connect SSE stream — SSE drives all live updates; no polling needed.
369
371
  connectSSE();
372
+
373
+ Promise.allSettled([
374
+ loadCompanies(),
375
+ loadRepoTree(),
376
+ api('/api/license', 60000, { timeoutMs: 2500 }).then(lic => { S.license = lic; }),
377
+ ]).then(() => {
378
+ const agentSel = $('ctx-agent');
379
+ if (agentSel && S.synapseHealth.length) {
380
+ agentSel.innerHTML = '<option value="">All agents</option>' +
381
+ S.synapseHealth.map(o=>`<option value="${esc(o.id||o.operativeId)}">${esc(o.role||o.name||o.id||'agent')}</option>`).join('');
382
+ }
383
+ });
370
384
  }
371
385
 
372
386
  // Start when DOM is ready
@@ -86,6 +86,10 @@ export const S = {
86
86
  contextLogRuns: [],
87
87
  contextLogSelectedRunId: null,
88
88
  contextLogSpine: null,
89
+ contextLogLearning: null,
90
+ learningSurface: null,
91
+ learningGraph: null,
92
+ learningSelectedPacketId: null,
89
93
 
90
94
  // Neural Stream HUD ring buffer (latest SSE events, restored from v3.8.0)
91
95
  neuralStream: [],