nexus-prime 7.9.0 → 7.9.2

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.
@@ -15,6 +15,7 @@ export type AsyncJobStatus = 'pending' | 'running' | 'completed' | 'failed';
15
15
  export interface AsyncJob {
16
16
  runId: string;
17
17
  tool: string;
18
+ args?: Record<string, unknown>;
18
19
  status: AsyncJobStatus;
19
20
  stage: string;
20
21
  progress: number;
@@ -29,10 +30,13 @@ export interface AsyncJob {
29
30
  };
30
31
  error?: string;
31
32
  etaMs?: number;
33
+ lifecycleReportedAt?: number;
32
34
  }
33
35
  export interface AsyncGateOpts {
34
36
  /** Tool name — for job metadata */
35
37
  tool: string;
38
+ /** Original tool args, used when a poll observes final lifecycle state. */
39
+ args?: Record<string, unknown>;
36
40
  /** If handler resolves within this window, return inline. Default: 2000ms */
37
41
  maxSyncMs?: number;
38
42
  /** Human-readable hint for estimated duration */
@@ -67,6 +71,7 @@ declare class AsyncGate {
67
71
  }>;
68
72
  };
69
73
  updateStage(runId: string, stage: string, progress: number): void;
74
+ markLifecycleReported(runId: string): void;
70
75
  /** Return jobs that have been running longer than their stale threshold. */
71
76
  getStaleJobs(now: number, defaultStaleMs: number, etaMultiplier: number): AsyncJob[];
72
77
  /** Mark a running job as failed (watchdog reap). */
@@ -25,6 +25,7 @@ class AsyncGate {
25
25
  const job = {
26
26
  runId,
27
27
  tool: opts.tool,
28
+ args: opts.args,
28
29
  status: 'pending',
29
30
  stage: 'queued',
30
31
  progress: 0,
@@ -55,8 +56,12 @@ class AsyncGate {
55
56
  job.error = err instanceof Error ? err.message : String(err);
56
57
  job.completedAt = Date.now();
57
58
  nexusEventBus.emit('async_gate.failed', { runId, tool: opts.tool, error: job.error });
58
- // Do not rethrow — if deadline already won there is no downstream catcher;
59
- // job state is updated so polls return the error correctly.
59
+ return {
60
+ content: [{
61
+ type: 'text',
62
+ text: `❌ Run ${runId} failed\nTool: ${opts.tool}\nError: ${job.error ?? 'unknown'}`,
63
+ }],
64
+ };
60
65
  }
61
66
  })();
62
67
  // Suppress unhandled rejection: deadline may win while work is still in flight.
@@ -118,6 +123,11 @@ class AsyncGate {
118
123
  job.progress = Math.min(99, progress);
119
124
  }
120
125
  }
126
+ markLifecycleReported(runId) {
127
+ const job = this.jobs.get(runId);
128
+ if (job)
129
+ job.lifecycleReportedAt = Date.now();
130
+ }
121
131
  /** Return jobs that have been running longer than their stale threshold. */
122
132
  getStaleJobs(now, defaultStaleMs, etaMultiplier) {
123
133
  const stale = [];
@@ -26,6 +26,23 @@ const SLOW_TOOLS = new Set([
26
26
  'nexus_ghost_pass',
27
27
  'nexus_spawn_workers',
28
28
  'nexus_optimize_tokens',
29
+ 'nexus_search',
30
+ 'nexus_assemble_context',
31
+ 'nexus_graph_query',
32
+ 'nexus_session_search',
33
+ 'nexus_memory_export',
34
+ 'nexus_memory_backup',
35
+ 'nexus_memory_import',
36
+ 'nexus_memory_hygiene',
37
+ 'nexus_memory_audit',
38
+ 'nexus_pattern_search',
39
+ 'nexus_knowledge_provenance',
40
+ 'nexus_rag_ingest_collection',
41
+ 'nexus_workflow_run',
42
+ 'nexus_automation_run',
43
+ 'nexus_doctor',
44
+ 'nexus_autofix',
45
+ 'nexus_hypertune_max',
29
46
  ]);
30
47
  /** Estimated wall-clock duration for each slow tool (ms). */
31
48
  const TOOL_ETA_MS = {
@@ -34,6 +51,23 @@ const TOOL_ETA_MS = {
34
51
  nexus_ghost_pass: 45_000,
35
52
  nexus_spawn_workers: 30_000,
36
53
  nexus_optimize_tokens: 10_000,
54
+ nexus_search: 20_000,
55
+ nexus_assemble_context: 20_000,
56
+ nexus_graph_query: 30_000,
57
+ nexus_session_search: 20_000,
58
+ nexus_memory_export: 20_000,
59
+ nexus_memory_backup: 30_000,
60
+ nexus_memory_import: 30_000,
61
+ nexus_memory_hygiene: 30_000,
62
+ nexus_memory_audit: 30_000,
63
+ nexus_pattern_search: 20_000,
64
+ nexus_knowledge_provenance: 20_000,
65
+ nexus_rag_ingest_collection: 45_000,
66
+ nexus_workflow_run: 45_000,
67
+ nexus_automation_run: 45_000,
68
+ nexus_doctor: 20_000,
69
+ nexus_autofix: 45_000,
70
+ nexus_hypertune_max: 45_000,
37
71
  };
38
72
  /**
39
73
  * Route a tool call to the appropriate handler group.
@@ -90,7 +124,7 @@ export async function dispatchMcpToolCall(hctx, request, args, ctx) {
90
124
  const gated = await withAsyncGate(async () => {
91
125
  const r = await runHandlers();
92
126
  return r ?? { content: [{ type: 'text', text: `Tool ${toolName} returned no result` }] };
93
- }, { tool: toolName, maxSyncMs: 2000, etaMs: TOOL_ETA_MS[toolName] });
127
+ }, { tool: toolName, args, maxSyncMs: 2000, etaMs: TOOL_ETA_MS[toolName] });
94
128
  if ('queued' in gated && gated.queued) {
95
129
  // Persist orchestration run record for durable stage tracking
96
130
  if (toolName === 'nexus_orchestrate') {
@@ -4,10 +4,11 @@
4
4
  * federation_status, run_status.
5
5
  * Extracted from mcp.ts (Phase 3 split).
6
6
  */
7
- import { formatBullets, formatJsonDetails, } from '../helpers.js';
7
+ import { formatBullets, formatJsonDetails, buildAutoMemorySummary, } from '../helpers.js';
8
8
  import { nexusEventBus } from '../../../../engines/event-bus.js';
9
9
  import { getSharedLicenseManager, snapshotPCU, formatPCUStatus } from '../../../../licensing/index.js';
10
10
  import { TokenAnalyticsEngine } from '../../../../engines/token-analytics.js';
11
+ import * as path from 'path';
11
12
  import { requireRuntime } from '../util/require-runtime.js';
12
13
  import { getAsyncGate } from '../async-gate.js';
13
14
  import { getRun as getOrchRun } from '../../../../engines/orchestrator/store.js';
@@ -231,6 +232,37 @@ export async function handleRuntimeGroup(toolName, hctx, request, args, ctx) {
231
232
  // Check async gate first (fast in-memory jobs from withAsyncGate)
232
233
  const asyncJob = getAsyncGate().getJob(runId);
233
234
  if (asyncJob) {
235
+ if (!asyncJob.lifecycleReportedAt
236
+ && (asyncJob.status === 'completed' || asyncJob.status === 'failed')) {
237
+ if (asyncJob.status === 'completed') {
238
+ hctx.telemetry.observeSuccessfulToolCall(asyncJob.tool, asyncJob.args ?? {});
239
+ }
240
+ const shouldPersistSuccess = [
241
+ 'nexus_orchestrate',
242
+ 'nexus_plan_execution',
243
+ 'nexus_session_dna',
244
+ 'nexus_ghost_pass',
245
+ 'nexus_mindkit_check',
246
+ ].includes(asyncJob.tool);
247
+ try {
248
+ if (asyncJob.tool !== 'nexus_store_memory' && (asyncJob.status === 'failed' || shouldPersistSuccess)) {
249
+ const workspace = hctx.getWorkspace(asyncJob.args ?? {});
250
+ const repoName = workspace.repoName || path.basename(workspace.repoRoot || '');
251
+ const summary = asyncJob.status === 'completed'
252
+ ? `MCP ${asyncJob.tool} completed asynchronously. ${buildAutoMemorySummary(asyncJob.tool, asyncJob.args ?? {})}`
253
+ : `MCP ${asyncJob.tool} failed asynchronously: ${asyncJob.error ?? 'unknown failure'}. ${buildAutoMemorySummary(asyncJob.tool, asyncJob.args ?? {})}`;
254
+ hctx.nexusRef.storeMemory(summary.slice(0, 1200), asyncJob.status === 'completed' ? 0.55 : 0.72, [
255
+ '#mcp-result',
256
+ '#auto',
257
+ `#tool:${asyncJob.tool}`,
258
+ ...(repoName ? [`repo:${repoName}`, `#repo:${repoName}`] : []),
259
+ ...(asyncJob.status === 'completed' ? ['#workspace'] : ['#failure-mode', '#runtime-result', '#inbox']),
260
+ ]);
261
+ }
262
+ }
263
+ catch { /* best-effort lifecycle memory */ }
264
+ getAsyncGate().markLifecycleReported(runId);
265
+ }
234
266
  if (fmt === 'nxl')
235
267
  return nxlResult(asyncJob);
236
268
  return getAsyncGate().formatStatus(runId);
@@ -7,7 +7,7 @@
7
7
  * 3. Transient retries — SQLite-busy / cold runtime → one retry with backoff.
8
8
  *
9
9
  * Per-client timeouts (AI Lead refinement):
10
- * Codex 90 s · Claude Code 60 s · Cursor 30 s · default 25 s
10
+ * Codex 90 s · Claude Code 60 s · Cursor 30 s · MCP clients 60 s · default 60 s
11
11
  *
12
12
  * Types and `envelopeToMcpResponse` are canonical in ./envelope.ts;
13
13
  * re-exported from here for backward compatibility.
@@ -16,6 +16,7 @@ import type { RunHandlerOptions } from './envelope.js';
16
16
  import type { HandlerEnvelope } from './envelope.js';
17
17
  export type { HandlerErrorCode, HandlerEnvelope, RunHandlerOptions } from './envelope.js';
18
18
  export { envelopeToMcpResponse } from './envelope.js';
19
+ export declare function resolveHandlerTimeoutForTest(opts?: RunHandlerOptions): number;
19
20
  /**
20
21
  * Run a handler function with timeout, retry, and structured envelope.
21
22
  *
@@ -7,7 +7,7 @@
7
7
  * 3. Transient retries — SQLite-busy / cold runtime → one retry with backoff.
8
8
  *
9
9
  * Per-client timeouts (AI Lead refinement):
10
- * Codex 90 s · Claude Code 60 s · Cursor 30 s · default 25 s
10
+ * Codex 90 s · Claude Code 60 s · Cursor 30 s · MCP clients 60 s · default 60 s
11
11
  *
12
12
  * Types and `envelopeToMcpResponse` are canonical in ./envelope.ts;
13
13
  * re-exported from here for backward compatibility.
@@ -20,16 +20,25 @@ const CLIENT_TIMEOUTS = {
20
20
  'codex': 90_000,
21
21
  'claude-code': 60_000,
22
22
  'cursor': 30_000,
23
+ 'openclaw': 90_000,
24
+ 'opencode': 90_000,
25
+ 'windsurf': 90_000,
26
+ 'antigravity': 90_000,
27
+ 'mcp': 60_000,
23
28
  };
24
- const DEFAULT_TIMEOUT = 25_000;
29
+ const DEFAULT_TIMEOUT = 60_000;
25
30
  function resolveTimeout(opts) {
26
31
  if (opts.timeoutMs !== undefined)
27
32
  return opts.timeoutMs;
28
- if (opts.callerName && opts.callerName in CLIENT_TIMEOUTS) {
29
- return CLIENT_TIMEOUTS[opts.callerName];
33
+ const callerName = String(opts.callerName ?? '').trim().toLowerCase();
34
+ if (callerName && callerName in CLIENT_TIMEOUTS) {
35
+ return CLIENT_TIMEOUTS[callerName];
30
36
  }
31
37
  return DEFAULT_TIMEOUT;
32
38
  }
39
+ export function resolveHandlerTimeoutForTest(opts = {}) {
40
+ return resolveTimeout(opts);
41
+ }
33
42
  // ─── Error classification ─────────────────────────────────────────────────────
34
43
  function classifyError(err) {
35
44
  const msg = err instanceof Error ? err.message : String(err);
@@ -59,6 +59,8 @@ export declare class MCPAdapter implements Adapter {
59
59
  }>;
60
60
  }>;
61
61
  private executeToolRequest;
62
+ private parseAsyncReceipt;
63
+ private maybeStoreToolOutcomeMemory;
62
64
  private buildHandlerCtx;
63
65
  private handleToolCall;
64
66
  /**
@@ -818,6 +818,7 @@ export class MCPAdapter {
818
818
  const result = envelopeToMcpResponse(envelope);
819
819
  const durationMs = Date.now() - startTimeMs;
820
820
  const resultPreview = envelope.ok ? buildResultPreview(result) : null;
821
+ const asyncReceipt = envelope.ok ? this.parseAsyncReceipt(result) : null;
821
822
  nexusEventBus.emit('mcp.call.complete', {
822
823
  callId,
823
824
  serverName: 'nexus-prime',
@@ -830,27 +831,71 @@ export class MCPAdapter {
830
831
  ? { result: resultPreview }
831
832
  : { error: envelope.error.message }),
832
833
  });
833
- this.telemetry.observeSuccessfulToolCall(toolName, args);
834
- // Surface meaningful tool calls on the SSE feed without persisting them as memories.
835
- // Previously a low-priority memory was auto-stored on every non-read-only tool call,
836
- // which polluted recall with audit-log entries like "nexus_store_memory: priority=0.85,
837
- // tags=...". Recall now stays focused on user-stored knowledge.
838
- if (this.nexusRef && !isReadOnlyMcpTool(toolName)) {
834
+ if (envelope.ok && !asyncReceipt?.queued) {
835
+ this.telemetry.observeSuccessfulToolCall(toolName, args);
836
+ }
837
+ // Surface meaningful tool calls on the SSE feed; persist only high-signal outcomes.
838
+ if (this.nexusRef) {
839
839
  const repoRoot = this.nexusRef.getWorkspaceContext?.()?.repoRoot ?? '';
840
840
  const repoName = repoRoot ? path.basename(repoRoot) : '';
841
- try {
842
- nexusEventBus.emit('memory.snapshot', {
843
- kind: 'tool-invocation',
844
- toolName,
845
- repo: repoName,
846
- summary: buildAutoMemorySummary(toolName, args),
847
- });
841
+ if (!isReadOnlyMcpTool(toolName)) {
842
+ try {
843
+ nexusEventBus.emit('memory.snapshot', {
844
+ kind: 'tool-invocation',
845
+ toolName,
846
+ repo: repoName,
847
+ summary: buildAutoMemorySummary(toolName, args),
848
+ });
849
+ }
850
+ catch { /* best-effort */ }
848
851
  }
849
- catch { /* best-effort */ }
852
+ this.maybeStoreToolOutcomeMemory(toolName, args, envelope, durationMs, repoName, asyncReceipt);
850
853
  }
851
854
  const decorated = this.decorateLifecycleResponse(toolName, result);
852
855
  return this.injectMemoryContext(toolName, args, decorated);
853
856
  }
857
+ parseAsyncReceipt(result) {
858
+ const text = result.content?.[0]?.text;
859
+ if (!text)
860
+ return null;
861
+ try {
862
+ const parsed = JSON.parse(String(text).split('\n\n')[0] ?? '{}');
863
+ return parsed && typeof parsed === 'object' && parsed.queued === true ? parsed : null;
864
+ }
865
+ catch {
866
+ return null;
867
+ }
868
+ }
869
+ maybeStoreToolOutcomeMemory(toolName, args, envelope, durationMs, repoName, asyncReceipt) {
870
+ if (!this.nexusRef || toolName === 'nexus_store_memory')
871
+ return;
872
+ if (asyncReceipt?.queued)
873
+ return;
874
+ const shouldPersistSuccess = [
875
+ 'nexus_orchestrate',
876
+ 'nexus_plan_execution',
877
+ 'nexus_session_dna',
878
+ 'nexus_ghost_pass',
879
+ 'nexus_mindkit_check',
880
+ ].includes(toolName);
881
+ if (envelope.ok && !shouldPersistSuccess)
882
+ return;
883
+ const error = envelope.error;
884
+ const summary = envelope.ok
885
+ ? `MCP ${toolName} completed in ${durationMs}ms. ${buildAutoMemorySummary(toolName, args)}`
886
+ : `MCP ${toolName} failed after ${durationMs}ms: ${error?.code ?? 'error'} ${error?.message ?? 'unknown failure'}. ${buildAutoMemorySummary(toolName, args)}`;
887
+ const tags = [
888
+ '#mcp-result',
889
+ '#auto',
890
+ `#tool:${toolName}`,
891
+ ...(repoName ? [`repo:${repoName}`, `#repo:${repoName}`] : []),
892
+ ...(envelope.ok ? ['#workspace'] : ['#failure-mode', '#runtime-result', '#inbox']),
893
+ ];
894
+ try {
895
+ this.nexusRef.storeMemory(summary.slice(0, 1200), envelope.ok ? 0.55 : 0.72, tags);
896
+ }
897
+ catch { /* best-effort */ }
898
+ }
854
899
  buildHandlerCtx(args = {}) {
855
900
  // eslint-disable-next-line @typescript-eslint/no-this-alias
856
901
  const self = this;
@@ -245,12 +245,30 @@ export function initArchitects(options) {
245
245
  nexusEventBus.emit('architects.event.failed', { event: 'striketeam.deployed', strikeTeamId, error: String(err) });
246
246
  }
247
247
  };
248
+ const onStriketeamFinished = ({ strikeTeamId, intent, outcome }) => {
249
+ try {
250
+ if (intent === 'mutate' && outcome !== 'cancelled' && outcome !== 'failed')
251
+ return;
252
+ const worklistId = implicitWorklistByTeam.get(strikeTeamId);
253
+ if (!worklistId)
254
+ return;
255
+ db.prepare(`
256
+ UPDATE architects_work_items SET status='cancelled'
257
+ WHERE worklist_id=? AND status='todo'
258
+ `).run(worklistId);
259
+ }
260
+ catch (err) {
261
+ console.error('[Architects] onStriketeamFinished failed', strikeTeamId, err);
262
+ nexusEventBus.emit('architects.event.failed', { event: 'striketeam.finished', strikeTeamId, error: String(err) });
263
+ }
264
+ };
248
265
  const onStanddown = () => setConvergencePaused(true);
249
266
  const onResumed = () => setConvergencePaused(false);
250
267
  const unsubOperative = nexusEventBus.on('synapse.operative.hired', onOperativeHired);
251
268
  const unsubMission = nexusEventBus.on('synapse.mission.assigned', onMissionAssigned);
252
269
  const unsubSortie = nexusEventBus.on('synapse.sortie.completed', onSortieCompleted);
253
270
  const unsubTeam = nexusEventBus.on('synapse.striketeam.deployed', onStriketeamDeployed);
271
+ const unsubFinished = nexusEventBus.on('synapse.striketeam.finished', onStriketeamFinished);
254
272
  const unsubStanddown = nexusEventBus.on('synapse.compaction.standdown', onStanddown);
255
273
  const unsubResumed = nexusEventBus.on('synapse.compaction.resumed', onResumed);
256
274
  nexusEventBus.emit('architects.ready', { version: '5.0.0' });
@@ -321,6 +339,7 @@ export function initArchitects(options) {
321
339
  unsubMission();
322
340
  unsubSortie();
323
341
  unsubTeam();
342
+ unsubFinished();
324
343
  unsubStanddown();
325
344
  unsubResumed();
326
345
  db.close();
@@ -1,3 +1,3 @@
1
1
  import type { ArchitectsDb } from '../types.js';
2
- export declare const ARCHITECTS_SCHEMA = "\nCREATE TABLE IF NOT EXISTS architects_blueprints (\n id TEXT PRIMARY KEY,\n strike_team_id TEXT,\n title TEXT NOT NULL,\n workflow_id TEXT,\n status TEXT NOT NULL DEFAULT 'active'\n CHECK(status IN ('draft','active','converging','done','archived')),\n variables TEXT NOT NULL DEFAULT '{}',\n worklist_id TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS architects_worklists (\n id TEXT PRIMARY KEY,\n blueprint_id TEXT NOT NULL,\n title TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS architects_work_items (\n id TEXT PRIMARY KEY,\n worklist_id TEXT NOT NULL,\n title TEXT NOT NULL,\n parent_work_item_id TEXT,\n correlation_id TEXT,\n ancestry_path TEXT NOT NULL DEFAULT '[]',\n status TEXT NOT NULL DEFAULT 'todo'\n CHECK(status IN ('todo','claimed','in_progress','done','failed','blocked')),\n depends_on TEXT NOT NULL DEFAULT '[]',\n assigned_operative_id TEXT,\n construction_lock_id TEXT,\n branch TEXT,\n merged_at TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS architects_construction_locks (\n id TEXT PRIMARY KEY,\n work_item_id TEXT NOT NULL,\n operative_id TEXT NOT NULL,\n acquired_at TEXT NOT NULL DEFAULT (datetime('now')),\n released_at TEXT,\n lease_duration_ms INTEGER NOT NULL DEFAULT 300000,\n lease_renewed_at INTEGER,\n lease_expires_at INTEGER NOT NULL DEFAULT 0,\n hook_ref TEXT\n);\n\nCREATE TABLE IF NOT EXISTS architects_relay_messages (\n id TEXT PRIMARY KEY,\n from_operative_id TEXT NOT NULL,\n to_operative_id TEXT,\n strike_team_id TEXT,\n subject TEXT NOT NULL,\n body TEXT NOT NULL,\n sent_at TEXT NOT NULL DEFAULT (datetime('now')),\n read_at TEXT,\n priority TEXT NOT NULL DEFAULT 'normal'\n CHECK(priority IN ('normal','urgent'))\n);\n\nCREATE TABLE IF NOT EXISTS architects_convergence_runs (\n id TEXT PRIMARY KEY,\n worklist_id TEXT NOT NULL,\n strategy TEXT NOT NULL DEFAULT 'bisecting'\n CHECK(strategy IN ('sequential','bisecting')),\n work_item_ids TEXT NOT NULL DEFAULT '[]',\n status TEXT NOT NULL DEFAULT 'running'\n CHECK(status IN ('running','merged','failed','bisecting','deferred')),\n failed_item_id TEXT,\n started_at TEXT NOT NULL DEFAULT (datetime('now')),\n completed_at TEXT\n);\n\nCREATE TABLE IF NOT EXISTS architects_dispatch_queue (\n id TEXT PRIMARY KEY,\n operative_id TEXT NOT NULL,\n work_item_id TEXT NOT NULL,\n scheduled_at TEXT NOT NULL DEFAULT (datetime('now')),\n dispatched_at TEXT,\n status TEXT NOT NULL DEFAULT 'queued'\n CHECK(status IN ('queued','dispatched','failed'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_arch_items_worklist ON architects_work_items(worklist_id, status);\nCREATE INDEX IF NOT EXISTS idx_arch_items_operative ON architects_work_items(assigned_operative_id);\nCREATE INDEX IF NOT EXISTS idx_arch_locks_item ON architects_construction_locks(work_item_id, released_at);\nCREATE INDEX IF NOT EXISTS idx_arch_blueprints_team ON architects_blueprints(strike_team_id, worklist_id);\nCREATE INDEX IF NOT EXISTS idx_arch_relay_to_op ON architects_relay_messages(to_operative_id, read_at);\nCREATE INDEX IF NOT EXISTS idx_arch_relay_to_team ON architects_relay_messages(strike_team_id, read_at);\n";
2
+ export declare const ARCHITECTS_SCHEMA = "\nCREATE TABLE IF NOT EXISTS architects_blueprints (\n id TEXT PRIMARY KEY,\n strike_team_id TEXT,\n title TEXT NOT NULL,\n workflow_id TEXT,\n status TEXT NOT NULL DEFAULT 'active'\n CHECK(status IN ('draft','active','converging','done','archived')),\n variables TEXT NOT NULL DEFAULT '{}',\n worklist_id TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS architects_worklists (\n id TEXT PRIMARY KEY,\n blueprint_id TEXT NOT NULL,\n title TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS architects_work_items (\n id TEXT PRIMARY KEY,\n worklist_id TEXT NOT NULL,\n title TEXT NOT NULL,\n parent_work_item_id TEXT,\n correlation_id TEXT,\n ancestry_path TEXT NOT NULL DEFAULT '[]',\n status TEXT NOT NULL DEFAULT 'todo'\n CHECK(status IN ('todo','claimed','in_progress','done','failed','blocked','cancelled')),\n depends_on TEXT NOT NULL DEFAULT '[]',\n assigned_operative_id TEXT,\n construction_lock_id TEXT,\n branch TEXT,\n merged_at TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS architects_construction_locks (\n id TEXT PRIMARY KEY,\n work_item_id TEXT NOT NULL,\n operative_id TEXT NOT NULL,\n acquired_at TEXT NOT NULL DEFAULT (datetime('now')),\n released_at TEXT,\n lease_duration_ms INTEGER NOT NULL DEFAULT 300000,\n lease_renewed_at INTEGER,\n lease_expires_at INTEGER NOT NULL DEFAULT 0,\n hook_ref TEXT\n);\n\nCREATE TABLE IF NOT EXISTS architects_relay_messages (\n id TEXT PRIMARY KEY,\n from_operative_id TEXT NOT NULL,\n to_operative_id TEXT,\n strike_team_id TEXT,\n subject TEXT NOT NULL,\n body TEXT NOT NULL,\n sent_at TEXT NOT NULL DEFAULT (datetime('now')),\n read_at TEXT,\n priority TEXT NOT NULL DEFAULT 'normal'\n CHECK(priority IN ('normal','urgent'))\n);\n\nCREATE TABLE IF NOT EXISTS architects_convergence_runs (\n id TEXT PRIMARY KEY,\n worklist_id TEXT NOT NULL,\n strategy TEXT NOT NULL DEFAULT 'bisecting'\n CHECK(strategy IN ('sequential','bisecting')),\n work_item_ids TEXT NOT NULL DEFAULT '[]',\n status TEXT NOT NULL DEFAULT 'running'\n CHECK(status IN ('running','merged','failed','bisecting','deferred')),\n failed_item_id TEXT,\n started_at TEXT NOT NULL DEFAULT (datetime('now')),\n completed_at TEXT\n);\n\nCREATE TABLE IF NOT EXISTS architects_dispatch_queue (\n id TEXT PRIMARY KEY,\n operative_id TEXT NOT NULL,\n work_item_id TEXT NOT NULL,\n scheduled_at TEXT NOT NULL DEFAULT (datetime('now')),\n dispatched_at TEXT,\n status TEXT NOT NULL DEFAULT 'queued'\n CHECK(status IN ('queued','dispatched','failed'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_arch_items_worklist ON architects_work_items(worklist_id, status);\nCREATE INDEX IF NOT EXISTS idx_arch_items_operative ON architects_work_items(assigned_operative_id);\nCREATE INDEX IF NOT EXISTS idx_arch_locks_item ON architects_construction_locks(work_item_id, released_at);\nCREATE INDEX IF NOT EXISTS idx_arch_blueprints_team ON architects_blueprints(strike_team_id, worklist_id);\nCREATE INDEX IF NOT EXISTS idx_arch_relay_to_op ON architects_relay_messages(to_operative_id, read_at);\nCREATE INDEX IF NOT EXISTS idx_arch_relay_to_team ON architects_relay_messages(strike_team_id, read_at);\n";
3
3
  export declare function initArchitectsSchema(db: ArchitectsDb): void;
@@ -27,7 +27,7 @@ CREATE TABLE IF NOT EXISTS architects_work_items (
27
27
  correlation_id TEXT,
28
28
  ancestry_path TEXT NOT NULL DEFAULT '[]',
29
29
  status TEXT NOT NULL DEFAULT 'todo'
30
- CHECK(status IN ('todo','claimed','in_progress','done','failed','blocked')),
30
+ CHECK(status IN ('todo','claimed','in_progress','done','failed','blocked','cancelled')),
31
31
  depends_on TEXT NOT NULL DEFAULT '[]',
32
32
  assigned_operative_id TEXT,
33
33
  construction_lock_id TEXT,
@@ -100,10 +100,49 @@ function addColumnIfMissing(db, table, column, ddl) {
100
100
  return;
101
101
  db.prepare(ddl).run();
102
102
  }
103
+ /**
104
+ * Migrate architects_work_items to allow 'cancelled' status.
105
+ *
106
+ * SQLite cannot ALTER a CHECK constraint, so we recreate the table when the
107
+ * existing CHECK does not include 'cancelled'. Safe to call on new databases
108
+ * (the table won't exist yet — `CREATE TABLE IF NOT EXISTS` in ARCHITECTS_SCHEMA
109
+ * already includes 'cancelled', so nothing needs doing).
110
+ */
111
+ function migrateWorkItemCancelledStatus(db) {
112
+ const existing = db.prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name='architects_work_items'`).get();
113
+ if (!existing?.sql)
114
+ return; // new DB — schema string already has 'cancelled'
115
+ if (existing.sql.includes("'cancelled'"))
116
+ return; // already migrated
117
+ db.exec(`
118
+ CREATE TABLE IF NOT EXISTS architects_work_items_v2 (
119
+ id TEXT PRIMARY KEY,
120
+ worklist_id TEXT NOT NULL,
121
+ title TEXT NOT NULL,
122
+ parent_work_item_id TEXT,
123
+ correlation_id TEXT,
124
+ ancestry_path TEXT NOT NULL DEFAULT '[]',
125
+ status TEXT NOT NULL DEFAULT 'todo'
126
+ CHECK(status IN ('todo','claimed','in_progress','done','failed','blocked','cancelled')),
127
+ depends_on TEXT NOT NULL DEFAULT '[]',
128
+ assigned_operative_id TEXT,
129
+ construction_lock_id TEXT,
130
+ branch TEXT,
131
+ merged_at TEXT,
132
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
133
+ );
134
+ INSERT OR IGNORE INTO architects_work_items_v2 SELECT * FROM architects_work_items;
135
+ DROP TABLE architects_work_items;
136
+ ALTER TABLE architects_work_items_v2 RENAME TO architects_work_items;
137
+ CREATE INDEX IF NOT EXISTS idx_arch_items_worklist ON architects_work_items(worklist_id, status);
138
+ CREATE INDEX IF NOT EXISTS idx_arch_items_operative ON architects_work_items(assigned_operative_id);
139
+ `);
140
+ }
103
141
  export function initArchitectsSchema(db) {
104
142
  // File-based forward-only migrations (PRAGMA user_version scheme).
105
143
  runMigrations(db, 'architects');
106
144
  db.exec(ARCHITECTS_SCHEMA);
145
+ migrateWorkItemCancelledStatus(db);
107
146
  addColumnIfMissing(db, 'architects_work_items', 'parent_work_item_id', 'ALTER TABLE architects_work_items ADD COLUMN parent_work_item_id TEXT');
108
147
  addColumnIfMissing(db, 'architects_work_items', 'correlation_id', 'ALTER TABLE architects_work_items ADD COLUMN correlation_id TEXT');
109
148
  addColumnIfMissing(db, 'architects_work_items', 'ancestry_path', "ALTER TABLE architects_work_items ADD COLUMN ancestry_path TEXT NOT NULL DEFAULT '[]'");
@@ -5,7 +5,7 @@ import type { PersistentWorkLedger } from '../engines/work-ledger.js';
5
5
  import type { NexusNetRelay } from '../engines/nexusnet-relay.js';
6
6
  import type { EngineHealthStatus } from '../engines/runtime-health.js';
7
7
  export type ArchitectsDb = BetterSqlite3.Database;
8
- export type WorkItemStatus = 'todo' | 'claimed' | 'in_progress' | 'done' | 'failed' | 'blocked';
8
+ export type WorkItemStatus = 'todo' | 'claimed' | 'in_progress' | 'done' | 'failed' | 'blocked' | 'cancelled';
9
9
  export type BlueprintStatus = 'draft' | 'active' | 'converging' | 'done' | 'archived';
10
10
  export type ClaimScope = 'workitem' | 'goal' | 'mission' | 'crew' | 'project';
11
11
  export interface Blueprint {
@@ -2,6 +2,7 @@ import type { WorkspaceContext } from '../engines/workspace-resolver.js';
2
2
  import { type DaemonLockRecord } from './lock.js';
3
3
  export interface DaemonHealthResponse {
4
4
  ok: boolean;
5
+ ready?: boolean;
5
6
  pid: number;
6
7
  port: number;
7
8
  stateKey: string;
@@ -16,6 +16,7 @@ function readCliPkgVersion() {
16
16
  return 'unknown';
17
17
  }
18
18
  import { acquireDaemonLock, getDaemonLockPath, isProcessAlive, readDaemonLock, removeDaemonLock, } from './lock.js';
19
+ const DEFAULT_DAEMON_READY_TIMEOUT_MS = 45_000;
19
20
  function sleep(ms) {
20
21
  return new Promise((resolve) => setTimeout(resolve, ms));
21
22
  }
@@ -111,7 +112,7 @@ function spawnDaemonProcess(workspace, entrypoint) {
111
112
  child.unref();
112
113
  }
113
114
  export async function ensureDaemonReady(workspace, options = {}) {
114
- const timeoutMs = options.timeoutMs ?? 15_000;
115
+ const timeoutMs = options.timeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
115
116
  const existing = acquireDaemonLock(workspace);
116
117
  if (existing.status === 'running') {
117
118
  const currentVersion = readCliPkgVersion();
@@ -13,6 +13,7 @@ export declare class NexusDaemonServer {
13
13
  private lockRecord;
14
14
  private heartbeatTimer;
15
15
  private stopping;
16
+ private runtimeReady;
16
17
  constructor(workspace: WorkspaceContext);
17
18
  private installProcessErrorHandlers;
18
19
  /**
@@ -21,6 +22,7 @@ export declare class NexusDaemonServer {
21
22
  * holding stale references.
22
23
  */
23
24
  getLockRecord(): DaemonLockRecord | null;
25
+ private startHeartbeat;
24
26
  start(): Promise<{
25
27
  started: boolean;
26
28
  record: DaemonLockRecord;
@@ -101,6 +101,7 @@ export class NexusDaemonServer {
101
101
  lockRecord = null;
102
102
  heartbeatTimer = null;
103
103
  stopping = false;
104
+ runtimeReady = false;
104
105
  constructor(workspace) {
105
106
  this.workspace = workspace;
106
107
  this.httpServer = http.createServer((req, res) => {
@@ -156,6 +157,25 @@ export class NexusDaemonServer {
156
157
  getLockRecord() {
157
158
  return this.lockRecord;
158
159
  }
160
+ startHeartbeat() {
161
+ if (this.heartbeatTimer) {
162
+ return;
163
+ }
164
+ this.heartbeatTimer = setInterval(() => {
165
+ if (this.lockPath && this.lockRecord) {
166
+ try {
167
+ this.lockRecord = writeDaemonLock(this.lockPath, {
168
+ ...this.lockRecord,
169
+ heartbeatAt: Date.now(),
170
+ });
171
+ }
172
+ catch {
173
+ // Non-fatal; worst case the next starter will detect us as stale.
174
+ }
175
+ }
176
+ }, HEARTBEAT_INTERVAL_MS);
177
+ this.heartbeatTimer.unref?.();
178
+ }
159
179
  async start() {
160
180
  // Clear the stopping flag so a second start() after a supervisor-triggered
161
181
  // stop() doesn't silently no-op via the SIGINT/SIGTERM handler guard.
@@ -171,6 +191,17 @@ export class NexusDaemonServer {
171
191
  record: lock.record,
172
192
  };
173
193
  }
194
+ const port = await listen(this.httpServer, this.host);
195
+ this.lockRecord = writeDaemonLock(this.lockPath, {
196
+ ...lock.record,
197
+ port,
198
+ token: this.authToken,
199
+ heartbeatAt: Date.now(),
200
+ pkgVersion: DAEMON_PKG_VERSION,
201
+ });
202
+ this.startHeartbeat();
203
+ this.installSignalHandlers();
204
+ this.installProcessErrorHandlers();
174
205
  await runStartupHygiene({
175
206
  repoRoot: this.workspace.repoRoot,
176
207
  workspaceStateRoot: this.workspace.stateRoot,
@@ -183,6 +214,7 @@ export class NexusDaemonServer {
183
214
  if (!preflight.ok) {
184
215
  console.error(`[nexus-daemon] startup-error: ${preflight.code} — ${preflight.hint}`);
185
216
  console.error(`[nexus-daemon] startup-error.json written to ${this.workspace.stateRoot}`);
217
+ await this.stop('startup-error');
186
218
  return { started: false, record: lock.record };
187
219
  }
188
220
  // Integrity check: detect + auto-repair corrupt sqlite files before
@@ -196,6 +228,7 @@ export class NexusDaemonServer {
196
228
  },
197
229
  });
198
230
  await this.nexus.start();
231
+ this.runtimeReady = true;
199
232
  // Optional repo-search prewarm. It can be CPU-heavy in large workspaces, so
200
233
  // keep daemon startup and dashboard responsiveness as the default behavior.
201
234
  if (process.env.NEXUS_PREWARM_REPO_SEARCH === '1' && this.workspace.gitRoot) {
@@ -206,33 +239,6 @@ export class NexusDaemonServer {
206
239
  // Warm the invoker availability cache so /api/runtimes returns fast and
207
240
  // the dashboard runtime badges render immediately on first load.
208
241
  setImmediate(() => { invokerRegistry.warmCache(); });
209
- const port = await listen(this.httpServer, this.host);
210
- this.lockRecord = writeDaemonLock(this.lockPath, {
211
- ...lock.record,
212
- port,
213
- token: this.authToken,
214
- heartbeatAt: Date.now(),
215
- pkgVersion: DAEMON_PKG_VERSION,
216
- });
217
- // Heartbeat: keep lockfile fresh so stale-lock detection (30 s TTL) works
218
- // correctly after a SIGKILL. The interval is intentionally unref'd so it
219
- // does not prevent clean shutdown.
220
- this.heartbeatTimer = setInterval(() => {
221
- if (this.lockPath && this.lockRecord) {
222
- try {
223
- this.lockRecord = writeDaemonLock(this.lockPath, {
224
- ...this.lockRecord,
225
- heartbeatAt: Date.now(),
226
- });
227
- }
228
- catch {
229
- // Non-fatal; worst case the next starter will detect us as stale.
230
- }
231
- }
232
- }, HEARTBEAT_INTERVAL_MS);
233
- this.heartbeatTimer.unref?.();
234
- this.installSignalHandlers();
235
- this.installProcessErrorHandlers();
236
242
  return {
237
243
  started: true,
238
244
  record: this.lockRecord,
@@ -374,8 +380,11 @@ export class NexusDaemonServer {
374
380
  return;
375
381
  }
376
382
  if (req.method === 'GET' && url.pathname === '/health') {
377
- sendJson(res, 200, {
378
- ok: true,
383
+ const ready = this.runtimeReady && this.nexus !== null;
384
+ sendJson(res, ready ? 200 : 503, {
385
+ ok: ready,
386
+ ready,
387
+ error: ready ? undefined : 'runtime-initializing',
379
388
  pid: process.pid,
380
389
  port: this.lockRecord?.port ?? 0,
381
390
  stateKey: this.workspace.stateKey,
@@ -429,6 +438,7 @@ export class NexusDaemonServer {
429
438
  return;
430
439
  }
431
440
  this.stopping = true;
441
+ this.runtimeReady = false;
432
442
  nexusEventBus.emit('nexus.shutdown', { signal });
433
443
  this.cleanupTimer.unref?.();
434
444
  try {
@@ -27,6 +27,24 @@ import { mount as mountRuntimeBadges } from './widgets/runtime-badge.js';
27
27
 
28
28
  const $ = id => document.getElementById(id);
29
29
  const esc = s => s==null?'':String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
30
+ let memoryReloadTimer = null;
31
+
32
+ function _memoryApiUrl() {
33
+ return S.workspace?.repoName ? '/api/memory?repo='+encodeURIComponent(S.workspace.repoName) : '/api/memory';
34
+ }
35
+
36
+ function _refreshMemorySoon() {
37
+ bustCache(_memoryApiUrl());
38
+ bustCache('/api/memory');
39
+ bustCache('/api/memory/health');
40
+ bustCache('/api/dashboard/surface/memory');
41
+ if (S.tab !== 'memory') return;
42
+ clearTimeout(memoryReloadTimer);
43
+ memoryReloadTimer = setTimeout(() => {
44
+ memoryReloadTimer = null;
45
+ Memory.load();
46
+ }, 150);
47
+ }
30
48
 
31
49
  /* ─────────────────── Router ─────────────────── */
32
50
  navRegister('board', Board.load);
@@ -53,15 +71,19 @@ setOnEvent(evt => {
53
71
  Board.render();
54
72
  _renderTicker();
55
73
  }
56
- // Refresh tool health tile on any tool invocation event (board always refreshes)
57
- if (String(evt.type||'') === 'tool.invocation' && tab === 'board') {
58
- Board.loadToolHealth();
74
+ // Refresh tool health tile on tool lifecycle events.
75
+ if (['tool.invocation', 'mcp.handler.complete', 'mcp.handler.failed'].includes(String(evt.type||''))) {
76
+ bustCache('/api/runtime/tool-health');
77
+ bustCache('/api/dashboard/surface/operate');
78
+ if (tab === 'board') {
79
+ Board.loadToolHealth();
80
+ }
59
81
  }
60
82
  if (tab === 'workforce' && ['synapse','operative','mission'].includes(evt.category)) {
61
83
  Workforce.render();
62
84
  }
63
85
  if (tab === 'memory' && evt.category === 'memory') {
64
- Memory.render();
86
+ _refreshMemorySoon();
65
87
  }
66
88
  if (tab === 'governance' && evt.category === 'darwin') {
67
89
  Governance.load();
@@ -25,6 +25,7 @@ export declare class DashboardServer {
25
25
  private repoTreeGenerator;
26
26
  private gitUser;
27
27
  private sseBroker;
28
+ private unsubscribeWorkspaceChanged;
28
29
  constructor(options?: DashboardServerOptions);
29
30
  start(): void;
30
31
  stop(): void;
@@ -64,6 +65,8 @@ export declare class DashboardServer {
64
65
  private readJsonIfExists;
65
66
  private fileExists;
66
67
  private collectUsageSnapshot;
68
+ private normalizeDashboardRepoToken;
69
+ private dashboardMemoryMatchesRepoName;
67
70
  private listDashboardMemories;
68
71
  private listCrossProjectMemories;
69
72
  private listCrossProjectProjects;
@@ -107,6 +107,7 @@ export class DashboardServer {
107
107
  repoTreeGenerator;
108
108
  gitUser;
109
109
  sseBroker = new SseBroker();
110
+ unsubscribeWorkspaceChanged = null;
110
111
  constructor(options = {}) {
111
112
  this.runtimeProvider = options.runtimeProvider;
112
113
  this.orchestratorProvider = options.orchestratorProvider;
@@ -124,7 +125,16 @@ export class DashboardServer {
124
125
  this.repoTreeGenerator = new RepoTreeGenerator(this.repoRoot);
125
126
  this.gitUser = process.env.GIT_AUTHOR_NAME || process.env.GIT_COMMITTER_NAME || '';
126
127
  this.server = http.createServer((req, res) => {
127
- void this.requestHandler(req, res);
128
+ this.requestHandler(req, res).catch((err) => {
129
+ console.error('[Dashboard] requestHandler unhandled error:', err instanceof Error ? err.message : String(err));
130
+ if (!res.headersSent && res.writable) {
131
+ try {
132
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
133
+ res.end('Internal Server Error');
134
+ }
135
+ catch { /* socket already dead */ }
136
+ }
137
+ });
128
138
  });
129
139
  this.server.on('error', (error) => {
130
140
  if (this.dashboardMode === 'bound') {
@@ -135,7 +145,7 @@ export class DashboardServer {
135
145
  // is promoted (e.g. from a nexus_session_bootstrap file hint). Without this
136
146
  // the dashboard keeps serving the stale "/" → "workspace" fallback for
137
147
  // up to 30s after the real repo is discovered.
138
- nexusEventBus.on('workspace.changed', (payload) => {
148
+ this.unsubscribeWorkspaceChanged = nexusEventBus.on('workspace.changed', (payload) => {
139
149
  try {
140
150
  this.repoRoot = payload.repoRoot || this.repoRoot;
141
151
  this.repoTreeGenerator = new RepoTreeGenerator(this.repoRoot);
@@ -178,6 +188,10 @@ export class DashboardServer {
178
188
  controller.abort();
179
189
  }
180
190
  this.activeProbeControllers.clear();
191
+ if (this.unsubscribeWorkspaceChanged) {
192
+ this.unsubscribeWorkspaceChanged();
193
+ this.unsubscribeWorkspaceChanged = null;
194
+ }
181
195
  if (this.dashboardMode === 'bound') {
182
196
  this.sseBroker.stop();
183
197
  this.server.close();
@@ -762,6 +776,35 @@ export class DashboardServer {
762
776
  latestRun: null,
763
777
  };
764
778
  }
779
+ normalizeDashboardRepoToken(value) {
780
+ return String(value ?? '').trim().replace(/^#/, '').toLowerCase();
781
+ }
782
+ dashboardMemoryMatchesRepoName(memory, repoName) {
783
+ const expected = this.normalizeDashboardRepoToken(repoName);
784
+ if (!expected)
785
+ return true;
786
+ const tags = Array.isArray(memory.tags) ? memory.tags : [];
787
+ const provenance = memory.provenance ?? {};
788
+ const candidates = [
789
+ ...tags,
790
+ ...(Array.isArray(provenance.tags) ? provenance.tags : []),
791
+ ...(Array.isArray(provenance.containerTags) ? provenance.containerTags : []),
792
+ provenance.repoName,
793
+ provenance.repoId,
794
+ provenance.workspaceId,
795
+ provenance.projectId,
796
+ ];
797
+ return candidates.some((candidate) => {
798
+ const token = this.normalizeDashboardRepoToken(candidate);
799
+ if (!token)
800
+ return false;
801
+ if (token === expected)
802
+ return true;
803
+ if (token === `repo:${expected}`)
804
+ return true;
805
+ return token.startsWith('repo:') && token.slice(5) === expected;
806
+ });
807
+ }
765
808
  listDashboardMemories(options = {}) {
766
809
  const limit = Math.max(1, Number(options.limit || 40));
767
810
  const raw = this.getMemory()?.listSnapshots(Math.min(limit * 3, 200), {
@@ -769,21 +812,20 @@ export class DashboardServer {
769
812
  tag: options.tag,
770
813
  linkedType: options.linkedType,
771
814
  recencyMs: options.recencyMs,
815
+ state: options.includeHidden || options.lane === 'inbox' ? undefined : 'active',
772
816
  lane: options.lane,
773
817
  repoId: options.repoId,
774
818
  workspaceId: options.workspaceId,
775
819
  projectId: options.projectId,
776
820
  includeHidden: options.includeHidden,
777
821
  }) ?? [];
778
- return raw
822
+ const visible = raw
779
823
  .filter((memory) => {
780
824
  const tags = Array.isArray(memory.tags) ? memory.tags : [];
781
825
  if (!options.includeHidden && tags.includes('#system-hidden'))
782
826
  return false;
783
827
  if (!options.includeHidden && tags.includes('#auto') && Number(memory.priority ?? 0) < 0.4)
784
828
  return false;
785
- if (options.repoName && !tags.some((t) => t === `repo:${options.repoName}`))
786
- return false;
787
829
  if (tags.includes('#quarantine') && options.lane !== 'inbox')
788
830
  return false;
789
831
  if (!options.showPhantom && (tags.includes('#phantom-learning') || tags.includes('#swarm')))
@@ -793,8 +835,11 @@ export class DashboardServer {
793
835
  if (!options.includeHidden && (tags.includes('#hidden') || tags.includes('#repo-profile') || tags.includes('#system-hidden')))
794
836
  return false;
795
837
  return true;
796
- })
797
- .slice(0, limit);
838
+ });
839
+ const scoped = options.repoName
840
+ ? visible.filter((memory) => this.dashboardMemoryMatchesRepoName(memory, options.repoName))
841
+ : visible;
842
+ return (options.repoName && scoped.length === 0 ? visible : scoped).slice(0, limit);
798
843
  }
799
844
  listCrossProjectMemories(options) {
800
845
  const memory = this.getMemory();
@@ -1465,6 +1510,10 @@ export class DashboardServer {
1465
1510
  body: Buffer.concat(chunks).toString('utf8'),
1466
1511
  });
1467
1512
  });
1513
+ res.on('error', (error) => {
1514
+ cleanup();
1515
+ reject(error);
1516
+ });
1468
1517
  });
1469
1518
  req.on('timeout', () => {
1470
1519
  req.destroy(new Error('probe-timeout'));
@@ -97,6 +97,8 @@ export class SseBroker {
97
97
  'Access-Control-Allow-Origin': corsOrigin,
98
98
  });
99
99
  const safeWrite = (chunk) => {
100
+ if (!res.writable || res.destroyed)
101
+ return false;
100
102
  try {
101
103
  return res.write(chunk);
102
104
  }
@@ -1,4 +1,4 @@
1
- export type NexusEventType = 'system.boot' | 'planner.stage' | 'memory.store' | 'memory.dedup' | 'memory.recall' | 'memory.flushed' | 'memory.snapshot' | 'memory.health.tick' | 'memory.sqlite.retry' | 'pod.signal' | 'tokens.optimized' | 'tokens.searchSaved' | 'orchestration.decomposed' | 'orchestration.completed' | 'session.summaryBootstrap' | 'phantom.worker.start' | 'phantom.worker.complete' | 'phantom.merge.complete' | 'phantom.merge' | 'guardrail.check' | 'ghost.pass' | 'graph.query' | 'graph.sync.failed' | 'graph.coverage.low' | 'graph.cr.build.start' | 'graph.cr.build.complete' | 'graph.cr.build.failed' | 'darwin.cycle' | 'darwin.cycle.complete' | 'session.dna' | 'skill.register' | 'skill.deploy' | 'skill.revoke' | 'hook.deploy' | 'hook.revoke' | 'hook.fire' | 'hook.error' | 'workflow.deploy' | 'workflow.run' | 'automation.deploy' | 'automation.revoke' | 'automation.run' | 'shield.decision' | 'memory.audit' | 'federation.heartbeat' | 'client.heartbeat' | 'client.inferred' | 'client.status' | 'dashboard.action' | 'workspace.changed' | 'nexus.shutdown' | 'nexus.daemon.error' | 'orchestrator.disposed' | 'orchestrator.funnel.stage' | 'orchestrator.warn' | 'nexusnet.publish' | 'nexusnet.sync' | 'mcp.call.start' | 'mcp.call.stream' | 'mcp.call.complete' | 'mcp.handler.complete' | 'mcp.handler.failed' | 'mcp.handler.retry' | 'mcp.handler.best-effort-failed' | 'tool.invocation' | 'nexus.deprecation.used' | 'memory.tier.promoted' | 'memory.tier.promoted.failed' | 'memory.pre-compaction' | 'memory.flush-requested' | 'entanglement.create' | 'entanglement.collapse' | 'entanglement.correlate' | 'cas.encode' | 'cas.decode' | 'cas.pattern_learned' | 'kv.merge' | 'kv.adapt' | 'kv.consensus' | 'byzantine.vote.started' | 'byzantine.vote.result' | 'synapse.ready' | 'synapse.operative.hired' | 'synapse.operative.retired' | 'synapse.operative.health.changed' | 'synapse.striketeam.deployed' | 'synapse.striketeam.completed' | 'synapse.mission.assigned' | 'synapse.mission.completed' | 'synapse.sortie.started' | 'synapse.sortie.completed' | 'synapse.sortie.failed' | 'synapse.fieldreport.submitted' | 'synapse.echo.fired' | 'synapse.budget.warning' | 'synapse.budget.exceeded' | 'synapse.approval.requested' | 'synapse.approval.resolved' | 'synapse.compaction.standdown' | 'synapse.compaction.resumed' | 'synapse.coordination.publish.failed' | 'synapse.watchdog.stall' | 'synapse.watchdog.zombie' | 'architects.ready' | 'architects.blueprint.instantiated' | 'architects.worklist.created' | 'architects.workitem.claimed' | 'architects.workitem.completed' | 'architects.workitem.blocked' | 'architects.constructionlock.acquired' | 'architects.constructionlock.renewed' | 'architects.constructionlock.released' | 'architects.constructionlock.contested' | 'architects.relay.sent' | 'architects.relay.read' | 'architects.sentinel.patrol' | 'architects.sentinel.stall' | 'architects.sentinel.zombie' | 'architects.ward.patrol' | 'architects.ward.escalation' | 'architects.convergence.started' | 'architects.convergence.merged' | 'architects.convergence.failed' | 'architects.dispatch.go' | 'architects.dispatch.queued' | 'architects.relay.failed' | 'architects.event.failed' | 'ledger.duplicate-prevented' | 'nexus.circuit-open' | 'nexus.circuit-tripped' | 'license.activated' | 'license.deactivated' | 'license.expired' | 'license.cap.warning' | 'license.cap.reached' | 'dispatch.started' | 'dispatch.event' | 'dispatch.token-usage' | 'dispatch.complete' | 'dispatch.failed' | 'dispatch.cancelled' | 'dispatch.budget-exceeded' | 'mcp.auto-memory.failed' | 'daemon.self-heal.db-corrupt' | 'feature.time_to_first_bootstrap' | 'feature.time_to_first_memory' | 'feature.time_to_first_interaction' | 'feature.time_to_first_mission_complete' | 'async_gate.completed' | 'async_gate.failed' | 'install.step' | 'license.tierChanged' | 'license.upgradeNudge' | 'orchestrator.run.start' | 'orchestrator.run.complete';
1
+ export type NexusEventType = 'system.boot' | 'planner.stage' | 'memory.store' | 'memory.dedup' | 'memory.recall' | 'memory.flushed' | 'memory.snapshot' | 'memory.health.tick' | 'memory.sqlite.retry' | 'pod.signal' | 'tokens.optimized' | 'tokens.searchSaved' | 'orchestration.decomposed' | 'orchestration.completed' | 'session.summaryBootstrap' | 'phantom.worker.start' | 'phantom.worker.complete' | 'phantom.merge.complete' | 'phantom.merge' | 'guardrail.check' | 'ghost.pass' | 'graph.query' | 'graph.sync.failed' | 'graph.coverage.low' | 'graph.cr.build.start' | 'graph.cr.build.complete' | 'graph.cr.build.failed' | 'darwin.cycle' | 'darwin.cycle.complete' | 'session.dna' | 'skill.register' | 'skill.deploy' | 'skill.revoke' | 'hook.deploy' | 'hook.revoke' | 'hook.fire' | 'hook.error' | 'workflow.deploy' | 'workflow.run' | 'automation.deploy' | 'automation.revoke' | 'automation.run' | 'shield.decision' | 'memory.audit' | 'federation.heartbeat' | 'client.heartbeat' | 'client.inferred' | 'client.status' | 'dashboard.action' | 'workspace.changed' | 'nexus.shutdown' | 'nexus.daemon.error' | 'orchestrator.disposed' | 'orchestrator.funnel.stage' | 'orchestrator.warn' | 'nexusnet.publish' | 'nexusnet.sync' | 'mcp.call.start' | 'mcp.call.stream' | 'mcp.call.complete' | 'mcp.handler.complete' | 'mcp.handler.failed' | 'mcp.handler.retry' | 'mcp.handler.best-effort-failed' | 'tool.invocation' | 'nexus.deprecation.used' | 'memory.tier.promoted' | 'memory.tier.promoted.failed' | 'memory.pre-compaction' | 'memory.flush-requested' | 'entanglement.create' | 'entanglement.collapse' | 'entanglement.correlate' | 'cas.encode' | 'cas.decode' | 'cas.pattern_learned' | 'kv.merge' | 'kv.adapt' | 'kv.consensus' | 'byzantine.vote.started' | 'byzantine.vote.result' | 'synapse.ready' | 'synapse.operative.hired' | 'synapse.operative.retired' | 'synapse.operative.health.changed' | 'synapse.striketeam.deployed' | 'synapse.striketeam.completed' | 'synapse.striketeam.finished' | 'synapse.mission.assigned' | 'synapse.mission.completed' | 'synapse.sortie.started' | 'synapse.sortie.completed' | 'synapse.sortie.failed' | 'synapse.fieldreport.submitted' | 'synapse.echo.fired' | 'synapse.budget.warning' | 'synapse.budget.exceeded' | 'synapse.approval.requested' | 'synapse.approval.resolved' | 'synapse.compaction.standdown' | 'synapse.compaction.resumed' | 'synapse.coordination.publish.failed' | 'synapse.watchdog.stall' | 'synapse.watchdog.zombie' | 'architects.ready' | 'architects.blueprint.instantiated' | 'architects.worklist.created' | 'architects.workitem.claimed' | 'architects.workitem.completed' | 'architects.workitem.blocked' | 'architects.constructionlock.acquired' | 'architects.constructionlock.renewed' | 'architects.constructionlock.released' | 'architects.constructionlock.contested' | 'architects.relay.sent' | 'architects.relay.read' | 'architects.sentinel.patrol' | 'architects.sentinel.stall' | 'architects.sentinel.zombie' | 'architects.ward.patrol' | 'architects.ward.escalation' | 'architects.convergence.started' | 'architects.convergence.merged' | 'architects.convergence.failed' | 'architects.dispatch.go' | 'architects.dispatch.queued' | 'architects.relay.failed' | 'architects.event.failed' | 'ledger.duplicate-prevented' | 'nexus.circuit-open' | 'nexus.circuit-tripped' | 'license.activated' | 'license.deactivated' | 'license.expired' | 'license.cap.warning' | 'license.cap.reached' | 'dispatch.started' | 'dispatch.event' | 'dispatch.token-usage' | 'dispatch.complete' | 'dispatch.failed' | 'dispatch.cancelled' | 'dispatch.budget-exceeded' | 'mcp.auto-memory.failed' | 'daemon.self-heal.db-corrupt' | 'feature.time_to_first_bootstrap' | 'feature.time_to_first_memory' | 'feature.time_to_first_interaction' | 'feature.time_to_first_mission_complete' | 'async_gate.completed' | 'async_gate.failed' | 'install.step' | 'license.tierChanged' | 'license.upgradeNudge' | 'orchestrator.run.start' | 'orchestrator.run.complete';
2
2
  export interface NexusEventPayloads {
3
3
  'system.boot': {
4
4
  version: string;
@@ -463,6 +463,12 @@ export interface NexusEventPayloads {
463
463
  worklistId?: string | null;
464
464
  correlationId?: string | null;
465
465
  };
466
+ 'synapse.striketeam.finished': {
467
+ strikeTeamId: string;
468
+ intent: 'read' | 'orchestrate' | 'mutate';
469
+ outcome: 'completed' | 'cancelled' | 'failed';
470
+ worklistId?: string | null;
471
+ };
466
472
  'synapse.mission.assigned': {
467
473
  operativeId: string;
468
474
  missionId: string;
@@ -149,6 +149,7 @@ export declare class KnowledgeFabricEngine {
149
149
  private readonly patternRegistry;
150
150
  private readonly memory?;
151
151
  private readonly bundleCache;
152
+ private readonly snapshotCache;
152
153
  constructor(options?: {
153
154
  repoRoot?: string;
154
155
  stateRoot?: string;
@@ -200,7 +201,8 @@ export declare class KnowledgeFabricEngine {
200
201
  trace: ModelTierTrace[];
201
202
  };
202
203
  private persistBundle;
203
- private pruneRuntimeSnapshots;
204
+ private persistBundleAsync;
205
+ private pruneRuntimeSnapshotsAsync;
204
206
  private buildSourceMix;
205
207
  private allocateTokenBudget;
206
208
  private buildRecommendations;
@@ -1,4 +1,5 @@
1
1
  import * as fs from 'fs';
2
+ import { promises as fsp } from 'fs';
2
3
  import * as path from 'path';
3
4
  import { PatternRegistry } from './pattern-registry.js';
4
5
  import { RuntimeRegistry, resolveNexusStateDir, } from './runtime-registry.js';
@@ -14,6 +15,7 @@ export class KnowledgeFabricEngine {
14
15
  patternRegistry;
15
16
  memory;
16
17
  bundleCache = new Map();
18
+ snapshotCache = new Map();
17
19
  constructor(options = {}) {
18
20
  this.repoRoot = options.repoRoot ?? process.cwd();
19
21
  this.stateDir = path.join(options.stateRoot ?? resolveNexusStateDir(), 'knowledge-fabric');
@@ -22,6 +24,7 @@ export class KnowledgeFabricEngine {
22
24
  this.ragCollections = options.ragCollections ?? new RagCollectionStore(options.stateRoot);
23
25
  this.patternRegistry = options.patternRegistry ?? new PatternRegistry(options.stateRoot);
24
26
  this.memory = options.memory;
27
+ // eslint-disable-next-line no-restricted-syntax -- constructor sync mkdir is startup-only, not hot path
25
28
  fs.mkdirSync(this.stateDir, { recursive: true });
26
29
  }
27
30
  async compose(input) {
@@ -55,7 +58,7 @@ export class KnowledgeFabricEngine {
55
58
  }
56
59
  async composeStage(input, stage, cacheKey) {
57
60
  const runtimeSnapshot = input.runtimeSnapshot ?? this.runtimeRegistry.read(input.runtimeId);
58
- const fileRefs = input.candidateFiles.map((entry) => toFileRef(entry)).filter((entry) => Boolean(entry));
61
+ const fileRefs = (await Promise.all(input.candidateFiles.map((entry) => toFileRef(entry)))).filter((entry) => Boolean(entry));
59
62
  const readingPlan = fileRefs.length > 0 ? await this.tokenEngine.plan(input.task, fileRefs, input.fileBoosts) : undefined;
60
63
  const selectedFiles = readingPlan
61
64
  ? readingPlan.files.filter((file) => file.action !== 'skip').map((file) => file.file.path)
@@ -226,14 +229,13 @@ export class KnowledgeFabricEngine {
226
229
  const existingTextLabels = new Set((fullCollection?.sources ?? [])
227
230
  .filter((source) => source.kind === 'text')
228
231
  .map((source) => source.label));
229
- const seedPaths = dedupePaths([
232
+ const seedCandidates = dedupePaths([
230
233
  path.join(this.repoRoot, 'README.md'),
231
234
  path.join(this.repoRoot, 'package.json'),
232
235
  ...input.candidateFiles,
233
- ])
234
- .filter((target) => fs.existsSync(target))
235
- .filter((target) => isReadableBootstrapSeed(target))
236
- .slice(0, 6);
236
+ ]);
237
+ const seedReadable = await Promise.all(seedCandidates.map((target) => isReadableBootstrapSeed(target)));
238
+ const seedPaths = seedCandidates.filter((_, i) => seedReadable[i]).slice(0, 6);
237
239
  const ingestInputs = seedPaths
238
240
  .filter((target) => !existingLocations.has(path.resolve(target)))
239
241
  .map((target) => ({
@@ -241,7 +243,7 @@ export class KnowledgeFabricEngine {
241
243
  label: path.relative(this.repoRoot, target) || path.basename(target),
242
244
  tags: ['bootstrap-seeded', 'project-context'],
243
245
  }));
244
- const workspaceProfile = buildWorkspaceProfileSeed(this.repoRoot);
246
+ const workspaceProfile = await buildWorkspaceProfileSeed(this.repoRoot);
245
247
  if (workspaceProfile && !existingTextLabels.has('workspace-profile')) {
246
248
  ingestInputs.unshift({
247
249
  text: workspaceProfile,
@@ -265,11 +267,20 @@ export class KnowledgeFabricEngine {
265
267
  this.patternRegistry.recordOutcome(patternId, success);
266
268
  }
267
269
  getSessionSnapshot(runtimeId) {
270
+ // Serve from in-memory cache written by persistBundle — avoids disk read on hot path.
271
+ // Falls back to disk only if the snapshot was persisted by a previous process instance.
272
+ const cached = this.snapshotCache.get(runtimeId);
273
+ if (cached)
274
+ return cached;
268
275
  const target = path.join(this.stateDir, runtimeId, 'latest.json');
276
+ // eslint-disable-next-line no-restricted-syntax -- cold-start fallback, runs at most once per runtimeId per process
269
277
  if (!fs.existsSync(target))
270
278
  return undefined;
271
279
  try {
272
- return JSON.parse(fs.readFileSync(target, 'utf8'));
280
+ // eslint-disable-next-line no-restricted-syntax -- cold-start fallback, populates snapshotCache so subsequent calls are free
281
+ const snapshot = JSON.parse(fs.readFileSync(target, 'utf8'));
282
+ this.snapshotCache.set(runtimeId, snapshot);
283
+ return snapshot;
273
284
  }
274
285
  catch {
275
286
  return undefined;
@@ -289,8 +300,6 @@ export class KnowledgeFabricEngine {
289
300
  };
290
301
  }
291
302
  persistBundle(bundle) {
292
- const runtimeDir = path.join(this.stateDir, bundle.runtimeId);
293
- fs.mkdirSync(runtimeDir, { recursive: true });
294
303
  const snapshot = {
295
304
  runtimeId: bundle.runtimeId,
296
305
  sessionId: bundle.sessionId,
@@ -314,21 +323,43 @@ export class KnowledgeFabricEngine {
314
323
  provenance: bundle.provenance,
315
324
  summary: bundle.summary,
316
325
  };
317
- fs.writeFileSync(path.join(runtimeDir, 'latest.json'), JSON.stringify(snapshot, null, 2), 'utf8');
318
- fs.writeFileSync(path.join(runtimeDir, `${bundle.sessionId}.json`), JSON.stringify(snapshot, null, 2), 'utf8');
319
- this.pruneRuntimeSnapshots(runtimeDir);
320
- }
321
- pruneRuntimeSnapshots(runtimeDir) {
322
- const sessionSnapshots = fs.readdirSync(runtimeDir)
323
- .filter((entry) => entry.endsWith('.json') && entry !== 'latest.json')
324
- .map((entry) => ({
325
- entry,
326
- mtimeMs: fs.statSync(path.join(runtimeDir, entry)).mtimeMs,
327
- }))
328
- .sort((a, b) => b.mtimeMs - a.mtimeMs);
329
- for (const stale of sessionSnapshots.slice(MAX_RUNTIME_SESSION_SNAPSHOTS)) {
330
- fs.unlinkSync(path.join(runtimeDir, stale.entry));
326
+ // Update in-memory cache immediately so getSessionSnapshot has zero disk latency.
327
+ this.snapshotCache.set(bundle.runtimeId, snapshot);
328
+ // Persist to disk asynchronously — callers never await this return value.
329
+ void this.persistBundleAsync(bundle.runtimeId, bundle.sessionId, snapshot);
330
+ }
331
+ async persistBundleAsync(runtimeId, sessionId, snapshot) {
332
+ const runtimeDir = path.join(this.stateDir, runtimeId);
333
+ await fsp.mkdir(runtimeDir, { recursive: true });
334
+ const serialized = JSON.stringify(snapshot, null, 2);
335
+ await Promise.all([
336
+ fsp.writeFile(path.join(runtimeDir, 'latest.json'), serialized, 'utf8'),
337
+ fsp.writeFile(path.join(runtimeDir, `${sessionId}.json`), serialized, 'utf8'),
338
+ ]);
339
+ await this.pruneRuntimeSnapshotsAsync(runtimeDir);
340
+ }
341
+ async pruneRuntimeSnapshotsAsync(runtimeDir) {
342
+ let entries;
343
+ try {
344
+ entries = await fsp.readdir(runtimeDir);
345
+ }
346
+ catch {
347
+ return;
331
348
  }
349
+ const candidates = entries.filter((entry) => entry.endsWith('.json') && entry !== 'latest.json');
350
+ if (candidates.length <= MAX_RUNTIME_SESSION_SNAPSHOTS)
351
+ return;
352
+ const withMtimes = await Promise.all(candidates.map(async (entry) => {
353
+ try {
354
+ const stat = await fsp.stat(path.join(runtimeDir, entry));
355
+ return { entry, mtimeMs: stat.mtimeMs };
356
+ }
357
+ catch {
358
+ return { entry, mtimeMs: 0 };
359
+ }
360
+ }));
361
+ const stale = withMtimes.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(MAX_RUNTIME_SESSION_SNAPSHOTS);
362
+ await Promise.all(stale.map(({ entry }) => fsp.unlink(path.join(runtimeDir, entry)).catch(() => { })));
332
363
  }
333
364
  buildSourceMix(input) {
334
365
  // Memory quality factor: scales memory weight by avg confidence × trust, penalizes high entropy
@@ -576,25 +607,26 @@ function normalizeBudget(base, totalBudget) {
576
607
  }
577
608
  return normalized;
578
609
  }
579
- function buildWorkspaceProfileSeed(repoRoot) {
610
+ async function buildWorkspaceProfileSeed(repoRoot) {
580
611
  const repoName = path.basename(repoRoot) || 'workspace';
581
- const packageName = readPackageName(repoRoot);
582
- const topLevel = safeListDir(repoRoot)
612
+ const packageName = await readPackageName(repoRoot);
613
+ const topLevel = (await safeListDir(repoRoot))
583
614
  .slice(0, 10)
584
615
  .map((entry) => entry.name)
585
616
  .join(', ');
586
- const focusAreas = ['src/synapse', 'src/architects', 'src/dashboard', 'src/phantom', 'src/engines', 'test', 'docs']
587
- .filter((relativePath) => fs.existsSync(path.join(repoRoot, relativePath)))
588
- .join(', ');
617
+ const focusAreaCandidates = ['src/synapse', 'src/architects', 'src/dashboard', 'src/phantom', 'src/engines', 'test', 'docs'];
618
+ const focusAreaExists = await Promise.all(focusAreaCandidates.map((relativePath) => fsp.access(path.join(repoRoot, relativePath)).then(() => true, () => false)));
619
+ const focusAreas = focusAreaCandidates.filter((_, i) => focusAreaExists[i]).join(', ');
589
620
  return [
590
621
  `Workspace profile: ${repoName}${packageName && packageName !== repoName ? ` (package ${packageName})` : ''}.`,
591
622
  topLevel ? `Top-level entries: ${topLevel}.` : null,
592
623
  focusAreas ? `Key areas: ${focusAreas}.` : null,
593
624
  ].filter(Boolean).join(' ');
594
625
  }
595
- function safeListDir(target) {
626
+ async function safeListDir(target) {
596
627
  try {
597
- return fs.readdirSync(target, { withFileTypes: true })
628
+ const entries = await fsp.readdir(target, { withFileTypes: true });
629
+ return entries
598
630
  .filter((entry) => !entry.name.startsWith('.'))
599
631
  .map((entry) => ({ name: entry.name }));
600
632
  }
@@ -602,21 +634,19 @@ function safeListDir(target) {
602
634
  return [];
603
635
  }
604
636
  }
605
- function readPackageName(repoRoot) {
637
+ async function readPackageName(repoRoot) {
606
638
  const packageJsonPath = path.join(repoRoot, 'package.json');
607
- if (!fs.existsSync(packageJsonPath))
608
- return null;
609
639
  try {
610
- const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
640
+ const parsed = JSON.parse(await fsp.readFile(packageJsonPath, 'utf8'));
611
641
  return typeof parsed.name === 'string' && parsed.name.trim() ? parsed.name.trim() : null;
612
642
  }
613
643
  catch {
614
644
  return null;
615
645
  }
616
646
  }
617
- function toFileRef(filePath) {
647
+ async function toFileRef(filePath) {
618
648
  try {
619
- const stat = fs.statSync(filePath);
649
+ const stat = await fsp.stat(filePath);
620
650
  return {
621
651
  path: filePath,
622
652
  sizeBytes: stat.size,
@@ -636,9 +666,9 @@ function dedupeStrings(values) {
636
666
  function estimateTokens(value) {
637
667
  return Math.max(1, Math.ceil(String(value || '').length / 4));
638
668
  }
639
- function isReadableBootstrapSeed(target) {
669
+ async function isReadableBootstrapSeed(target) {
640
670
  try {
641
- const stat = fs.statSync(target);
671
+ const stat = await fsp.stat(target);
642
672
  if (!stat.isFile() || stat.size > 96_000) {
643
673
  return false;
644
674
  }
package/dist/index.d.ts CHANGED
@@ -281,3 +281,10 @@ export declare class NexusPrime {
281
281
  private getDefaultCapabilities;
282
282
  }
283
283
  export declare const createNexusPrime: (config?: Partial<NexusConfig>) => NexusPrime;
284
+ export { GhostPass, PhantomOrchestrator, PhantomWorker } from './phantom/index.js';
285
+ export type { GhostReport, WorkerTask, WorkerResult, MergeDecision } from './phantom/index.js';
286
+ export { MemoryEngine, createMemoryEngine } from './engines/memory.js';
287
+ export { SessionDNAManager } from './engines/session-dna.js';
288
+ export type { SessionDNA } from './engines/session-dna.js';
289
+ export { GithubBridge, getSharedGithubBridge } from './engines/github-bridge.js';
290
+ export type { GithubPromotionInput, GithubPromotionResult } from './engines/github-bridge.js';
package/dist/index.js CHANGED
@@ -1193,3 +1193,8 @@ function detectCurrentClient() {
1193
1193
  }
1194
1194
  return null;
1195
1195
  }
1196
+ // Public API surface for sibling packages (e.g., @nexus-prime/finishit)
1197
+ export { GhostPass, PhantomOrchestrator, PhantomWorker } from './phantom/index.js';
1198
+ export { MemoryEngine, createMemoryEngine } from './engines/memory.js';
1199
+ export { SessionDNAManager } from './engines/session-dna.js';
1200
+ export { GithubBridge, getSharedGithubBridge } from './engines/github-bridge.js';
@@ -166,5 +166,10 @@ export async function executeMandatePipeline(db, mandateText, providers, opts =
166
166
  });
167
167
  return getStrikeTeam(db, teamId);
168
168
  })();
169
+ nexusEventBus.emit('synapse.striketeam.finished', {
170
+ strikeTeamId: team.id,
171
+ intent: signals.complexity,
172
+ outcome: 'completed',
173
+ });
169
174
  return team;
170
175
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-prime",
3
- "version": "7.9.0",
3
+ "version": "7.9.2",
4
4
  "description": "Local-first MCP control plane for coding agents with bootstrap-orchestrate execution, memory fabric, token budgeting, and worktree-backed swarms",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",