monomind 2.0.3 → 2.1.3

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 (113) hide show
  1. package/README.md +74 -92
  2. package/package.json +11 -7
  3. package/packages/@monomind/cli/.claude/helpers/control-start.cjs +13 -20
  4. package/packages/@monomind/cli/.claude/helpers/event-logger.cjs +109 -6
  5. package/packages/@monomind/cli/.claude/helpers/graphify-freshen.cjs +19 -17
  6. package/packages/@monomind/cli/.claude/helpers/handlers/agent-start-handler.cjs +60 -28
  7. package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +99 -28
  8. package/packages/@monomind/cli/.claude/helpers/handlers/edit-handler.cjs +27 -9
  9. package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +18 -9
  10. package/packages/@monomind/cli/.claude/helpers/handlers/loops-status-handler.cjs +1 -1
  11. package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +33 -4
  12. package/packages/@monomind/cli/.claude/helpers/handlers/session-handler.cjs +75 -45
  13. package/packages/@monomind/cli/.claude/helpers/handlers/session-restore-handler.cjs +0 -12
  14. package/packages/@monomind/cli/.claude/helpers/handlers/task-handler.cjs +53 -22
  15. package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +75 -4
  16. package/packages/@monomind/cli/.claude/helpers/intelligence.cjs +94 -15
  17. package/packages/@monomind/cli/.claude/helpers/session.cjs +6 -1
  18. package/packages/@monomind/cli/.claude/helpers/statusline.cjs +10 -10
  19. package/packages/@monomind/cli/.claude/helpers/token-tracker.cjs +3 -3
  20. package/packages/@monomind/cli/.claude/helpers/utils/agent-registrations.cjs +41 -0
  21. package/packages/@monomind/cli/.claude/helpers/utils/fs-helpers.cjs +183 -0
  22. package/packages/@monomind/cli/.claude/helpers/utils/micro-agents.cjs +10 -6
  23. package/packages/@monomind/cli/.claude/helpers/utils/monograph.cjs +73 -2
  24. package/packages/@monomind/cli/.claude/helpers/utils/telemetry.cjs +35 -19
  25. package/packages/@monomind/cli/README.md +74 -92
  26. package/packages/@monomind/cli/bin/cli.js +10 -1
  27. package/packages/@monomind/cli/dist/src/browser/workflow/store.d.ts +2 -2
  28. package/packages/@monomind/cli/dist/src/commands/agent-ops.js +9 -15
  29. package/packages/@monomind/cli/dist/src/commands/browse.d.ts +1 -1
  30. package/packages/@monomind/cli/dist/src/commands/cleanup.js +20 -4
  31. package/packages/@monomind/cli/dist/src/commands/completions.js +66 -261
  32. package/packages/@monomind/cli/dist/src/commands/doctor-env-checks.js +5 -0
  33. package/packages/@monomind/cli/dist/src/commands/doctor.js +7 -2
  34. package/packages/@monomind/cli/dist/src/commands/hooks-workers.js +7 -0
  35. package/packages/@monomind/cli/dist/src/commands/index.js +3 -0
  36. package/packages/@monomind/cli/dist/src/commands/mcp.js +11 -6
  37. package/packages/@monomind/cli/dist/src/commands/memory-admin.js +12 -6
  38. package/packages/@monomind/cli/dist/src/commands/memory-transfer.js +6 -13
  39. package/packages/@monomind/cli/dist/src/commands/monograph.js +3 -3
  40. package/packages/@monomind/cli/dist/src/commands/org.d.ts +3 -1
  41. package/packages/@monomind/cli/dist/src/commands/org.js +321 -127
  42. package/packages/@monomind/cli/dist/src/commands/platforms.d.ts +1 -1
  43. package/packages/@monomind/cli/dist/src/commands/start.js +142 -37
  44. package/packages/@monomind/cli/dist/src/commands/tokens.js +21 -3
  45. package/packages/@monomind/cli/dist/src/config-adapter.js +37 -17
  46. package/packages/@monomind/cli/dist/src/index.js +64 -28
  47. package/packages/@monomind/cli/dist/src/init/claudemd-generator.js +44 -11
  48. package/packages/@monomind/cli/dist/src/init/executor.js +93 -55
  49. package/packages/@monomind/cli/dist/src/mcp-client.d.ts +8 -3
  50. package/packages/@monomind/cli/dist/src/mcp-client.js +37 -3
  51. package/packages/@monomind/cli/dist/src/mcp-server.js +8 -2
  52. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.d.ts +20 -0
  53. package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.js +10 -5
  54. package/packages/@monomind/cli/dist/src/mcp-tools/auto-install.d.ts +8 -8
  55. package/packages/@monomind/cli/dist/src/mcp-tools/coherence/types.d.ts +31 -31
  56. package/packages/@monomind/cli/dist/src/mcp-tools/github-tools.js +30 -8
  57. package/packages/@monomind/cli/dist/src/mcp-tools/graphify-tools.js +11 -2
  58. package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +56 -35
  59. package/packages/@monomind/cli/dist/src/mcp-tools/knowledge-tools.js +9 -1
  60. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-compat.js +56 -14
  61. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +88 -41
  62. package/packages/@monomind/cli/dist/src/mcp-tools/quality/coverage-analysis/prioritize-gaps.d.ts +36 -36
  63. package/packages/@monomind/cli/dist/src/mcp-tools/quality/security-compliance/detect-secrets.d.ts +4 -4
  64. package/packages/@monomind/cli/dist/src/mcp-tools/swarm-tools.js +6 -4
  65. package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +4 -4
  66. package/packages/@monomind/cli/dist/src/mcp-tools/types.d.ts +20 -0
  67. package/packages/@monomind/cli/dist/src/mcp-tools/types.js +36 -1
  68. package/packages/@monomind/cli/dist/src/memory/ewc-consolidation.d.ts +12 -0
  69. package/packages/@monomind/cli/dist/src/memory/hnsw-operations.d.ts +13 -1
  70. package/packages/@monomind/cli/dist/src/memory/hnsw-operations.js +66 -14
  71. package/packages/@monomind/cli/dist/src/memory/intelligence.js +99 -3
  72. package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +7 -0
  73. package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +20 -0
  74. package/packages/@monomind/cli/dist/src/memory/sona-optimizer.d.ts +12 -0
  75. package/packages/@monomind/cli/dist/src/orgrt/broker.d.ts +26 -0
  76. package/packages/@monomind/cli/dist/src/orgrt/broker.js +73 -0
  77. package/packages/@monomind/cli/dist/src/orgrt/bus.d.ts +5 -1
  78. package/packages/@monomind/cli/dist/src/orgrt/bus.js +5 -1
  79. package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +46 -0
  80. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +201 -18
  81. package/packages/@monomind/cli/dist/src/orgrt/forwarder.d.ts +10 -5
  82. package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +138 -8
  83. package/packages/@monomind/cli/dist/src/orgrt/inbox.d.ts +11 -0
  84. package/packages/@monomind/cli/dist/src/orgrt/inbox.js +56 -0
  85. package/packages/@monomind/cli/dist/src/orgrt/policy.d.ts +5 -1
  86. package/packages/@monomind/cli/dist/src/orgrt/policy.js +58 -5
  87. package/packages/@monomind/cli/dist/src/orgrt/provider.js +1 -0
  88. package/packages/@monomind/cli/dist/src/orgrt/scheduler.d.ts +13 -0
  89. package/packages/@monomind/cli/dist/src/orgrt/scheduler.js +48 -0
  90. package/packages/@monomind/cli/dist/src/orgrt/server.d.ts +9 -0
  91. package/packages/@monomind/cli/dist/src/orgrt/server.js +39 -0
  92. package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +10 -1
  93. package/packages/@monomind/cli/dist/src/orgrt/session.js +24 -1
  94. package/packages/@monomind/cli/dist/src/orgrt/test-loop.d.ts +12 -0
  95. package/packages/@monomind/cli/dist/src/orgrt/test-loop.js +108 -0
  96. package/packages/@monomind/cli/dist/src/orgrt/types.d.ts +52 -52
  97. package/packages/@monomind/cli/dist/src/output.d.ts +29 -29
  98. package/packages/@monomind/cli/dist/src/output.js +22 -7
  99. package/packages/@monomind/cli/dist/src/parser.d.ts +32 -0
  100. package/packages/@monomind/cli/dist/src/parser.js +130 -5
  101. package/packages/@monomind/cli/dist/src/routing/embed-worker.js +13 -1
  102. package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +31 -0
  103. package/packages/@monomind/cli/dist/src/services/config-file-manager.d.ts +21 -0
  104. package/packages/@monomind/cli/dist/src/services/config-file-manager.js +60 -7
  105. package/packages/@monomind/cli/dist/src/services/crash-reporter.js +2 -1
  106. package/packages/@monomind/cli/dist/src/types.d.ts +2 -2
  107. package/packages/@monomind/cli/dist/src/ui/dashboard.html +243 -49
  108. package/packages/@monomind/cli/dist/src/ui/orgs-files.js +192 -0
  109. package/packages/@monomind/cli/dist/src/ui/orgs.html +79 -11
  110. package/packages/@monomind/cli/dist/src/ui/server.mjs +346 -139
  111. package/packages/@monomind/cli/dist/src/utils/input-guards.d.ts +9 -0
  112. package/packages/@monomind/cli/dist/src/utils/input-guards.js +39 -5
  113. package/packages/@monomind/cli/package.json +15 -15
@@ -24,14 +24,59 @@ async function getBridge() {
24
24
  }
25
25
  let hnswIndex = null;
26
26
  let hnswInitializing = false;
27
+ /** Lazily import the pure-JS HNSWIndex class from the optional @monoes/memory package. */
28
+ async function loadHNSWIndexClass() {
29
+ try {
30
+ const mod = await import('@monoes/memory');
31
+ return mod?.HNSWIndex ?? null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /** Read entries with parseable embeddings of the expected dimension from the sql.js DB. */
38
+ async function loadEntriesFromDb(dbPath, dimensions) {
39
+ if (!fs.existsSync(dbPath))
40
+ return [];
41
+ const initSqlJs = (await import('sql.js')).default;
42
+ const SQL = await initSqlJs();
43
+ const fileBuffer = fs.readFileSync(dbPath);
44
+ const db = new SQL.Database(fileBuffer);
45
+ try {
46
+ const { safeParseEmbedding } = await import('./memory-bridge.js');
47
+ const stmt = db.prepare(`SELECT id, key, namespace, content, embedding FROM memory_entries WHERE status = 'active' AND embedding IS NOT NULL LIMIT 50000`);
48
+ const rows = [];
49
+ while (stmt.step())
50
+ rows.push(stmt.get());
51
+ stmt.free();
52
+ const results = [];
53
+ for (const row of rows) {
54
+ const [id, key, namespace, content, embeddingJson] = row;
55
+ const parsed = safeParseEmbedding(embeddingJson);
56
+ if (!parsed || parsed.length !== dimensions)
57
+ continue;
58
+ results.push({
59
+ id,
60
+ vector: new Float32Array(parsed),
61
+ entry: { id, key: key || id, namespace: namespace || 'default', content: content || '' },
62
+ });
63
+ }
64
+ return results;
65
+ }
66
+ finally {
67
+ db.close();
68
+ }
69
+ }
27
70
  /**
28
71
  * Get or create the HNSW index singleton
29
72
  * Lazily initializes from SQLite data on first use
30
73
  */
31
74
  export async function getHNSWIndex(options) {
32
75
  const dimensions = options?.dimensions ?? 384;
33
- // Return existing index if already initialized
34
- if (hnswIndex?.initialized && !options?.forceRebuild) {
76
+ // Return existing index if already initialized for the same dimensionality.
77
+ // A cached index built for a different embedding size must be rebuilt —
78
+ // otherwise addPoint()/search() silently fail (or corrupt) on the mismatch.
79
+ if (hnswIndex?.initialized && hnswIndex.dimensions === dimensions && !options?.forceRebuild) {
35
80
  return hnswIndex;
36
81
  }
37
82
  // Prevent concurrent initialization
@@ -50,13 +95,23 @@ export async function getHNSWIndex(options) {
50
95
  }
51
96
  hnswInitializing = true;
52
97
  try {
53
- // Native @monoes/core HNSW (WASM VectorDb) was removed in the lean teardown.
54
- // This function is kept for callers that check its return value — all callers
55
- // already handle null by falling back to the pure-JS / brute-force path.
56
- // The memory bridge (memory-bridge.ts) provides ANN search via LanceDB.
57
- // Native backend removed — return null so callers use the pure-JS fallback.
98
+ const HNSWIndexClass = await loadHNSWIndexClass();
99
+ if (!HNSWIndexClass) {
100
+ // @monoes/memory isn't installed no pure-JS fallback available;
101
+ // callers fall through to brute-force search.
102
+ hnswInitializing = false;
103
+ return null;
104
+ }
105
+ const dbPath = options?.dbPath ?? path.join(process.cwd(), '.swarm', 'memory.db');
106
+ const loaded = await loadEntriesFromDb(dbPath, dimensions);
107
+ const instance = new HNSWIndexClass({ dimensions });
108
+ await instance.rebuild(loaded.map((l) => ({ id: l.id, vector: l.vector })));
109
+ const entries = new Map();
110
+ for (const l of loaded)
111
+ entries.set(l.id, l.entry);
112
+ hnswIndex = { instance, entries, dimensions, initialized: true };
58
113
  hnswInitializing = false;
59
- return null;
114
+ return hnswIndex;
60
115
  }
61
116
  catch {
62
117
  hnswInitializing = false;
@@ -90,10 +145,7 @@ export async function addToHNSWIndex(id, embedding, entry) {
90
145
  return false;
91
146
  try {
92
147
  const vector = new Float32Array(embedding);
93
- await index.db.insert({
94
- id,
95
- vector
96
- });
148
+ await index.instance.addPoint(id, vector);
97
149
  index.entries.set(id, entry);
98
150
  // Save metadata for persistence (debounced would be better for high-volume)
99
151
  saveHNSWMetadata();
@@ -115,7 +167,7 @@ export async function searchHNSWIndex(queryEmbedding, options) {
115
167
  const vector = new Float32Array(queryEmbedding);
116
168
  const k = options?.k ?? 10;
117
169
  // HNSW search returns results with cosine distance (lower = more similar)
118
- const results = await index.db.search({ vector, k: k * 2 }); // Get extra for filtering
170
+ const results = await index.instance.search(vector, k * 2); // Get extra for filtering
119
171
  const filtered = [];
120
172
  for (const result of results) {
121
173
  const entry = index.entries.get(result.id);
@@ -127,7 +179,7 @@ export async function searchHNSWIndex(queryEmbedding, options) {
127
179
  }
128
180
  // Convert cosine distance to similarity score (1 - distance)
129
181
  // Cosine distance convention: 0 = identical, 2 = opposite
130
- const score = 1 - (result.score / 2);
182
+ const score = 1 - (result.distance / 2);
131
183
  filtered.push({
132
184
  id: entry.id.substring(0, 12),
133
185
  key: entry.key || entry.id.substring(0, 15),
@@ -10,9 +10,9 @@
10
10
  *
11
11
  * @module v1/cli/intelligence
12
12
  */
13
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
13
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync, unlinkSync } from 'node:fs';
14
14
  import { homedir } from 'node:os';
15
- import { join } from 'node:path';
15
+ import { dirname, join } from 'node:path';
16
16
  import { cosineSimilarity as sharedCosineSimilarity } from '../utils/cosine-similarity.js';
17
17
  import { readJsonFileSync, writeJsonFileAtomic } from '../utils/json-file.js';
18
18
  // ============================================================================
@@ -49,6 +49,75 @@ function ensureDataDir() {
49
49
  function getPatternsPath() {
50
50
  return join(getDataDir(), 'patterns.json');
51
51
  }
52
+ // ── multi-process-safe patterns.json flush (P1-14) ─────────────────────────
53
+ // patterns.json can be flushed by more than one process at once — e.g. the
54
+ // long-lived MCP server process and a short-lived CJS hook subprocess each
55
+ // load their own in-memory copy, then flush independently. Without a lock +
56
+ // re-read-before-write, whichever flush lands last silently erases whatever
57
+ // the other process added since its own load. flushToDisk() below re-reads
58
+ // the current on-disk file immediately before writing and merges this
59
+ // process's in-memory patterns in by `id` (keeping whichever record has the
60
+ // newer `lastUsedAt`), guarded by a short advisory lock.
61
+ function getPatternsLockPath() {
62
+ return `${getPatternsPath()}.lock`;
63
+ }
64
+ const PATTERNS_LOCK_STALE_MS = 10_000; // single JSON write — 10s is generous
65
+ function isPidAlive(pid) {
66
+ if (!Number.isInteger(pid) || pid <= 0)
67
+ return false;
68
+ try {
69
+ process.kill(pid, 0);
70
+ return true;
71
+ }
72
+ catch (e) {
73
+ return e?.code === 'EPERM';
74
+ }
75
+ }
76
+ /**
77
+ * Mirrors the stale-lock-breaking pattern in
78
+ * .claude/helpers/control-start.cjs (claimSpawnLock/releaseSpawnLock):
79
+ * wx-flag write to claim, break the lock if its holder is dead or the lock
80
+ * is older than the stale threshold, retry once, then give up and proceed
81
+ * unlocked (best-effort — a missed lock still merges via the re-read, it
82
+ * just narrows a tiny TOCTOU window further).
83
+ */
84
+ function claimPatternsLock() {
85
+ const lockPath = getPatternsLockPath();
86
+ try {
87
+ mkdirSync(dirname(lockPath), { recursive: true });
88
+ }
89
+ catch {
90
+ /* ignore */
91
+ }
92
+ try {
93
+ writeFileSync(lockPath, String(process.pid), { flag: 'wx' });
94
+ return true;
95
+ }
96
+ catch {
97
+ try {
98
+ const stat = statSync(lockPath);
99
+ const holder = Number(readFileSync(lockPath, 'utf-8'));
100
+ if (Date.now() - stat.mtimeMs < PATTERNS_LOCK_STALE_MS && isPidAlive(holder))
101
+ return false;
102
+ unlinkSync(lockPath);
103
+ writeFileSync(lockPath, String(process.pid), { flag: 'wx' });
104
+ return true;
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
110
+ }
111
+ function releasePatternsLock() {
112
+ const lockPath = getPatternsLockPath();
113
+ try {
114
+ if (Number(readFileSync(lockPath, 'utf-8')) === process.pid)
115
+ unlinkSync(lockPath);
116
+ }
117
+ catch {
118
+ /* ignore */
119
+ }
120
+ }
52
121
  /**
53
122
  * Get the stats file path
54
123
  */
@@ -412,7 +481,34 @@ class LocalReasoningBank {
412
481
  return;
413
482
  try {
414
483
  ensureDataDir();
415
- writeJsonFileAtomic(getPatternsPath(), this.patternList);
484
+ let acquired = claimPatternsLock();
485
+ if (!acquired)
486
+ acquired = claimPatternsLock(); // one retry — lock is short-lived
487
+ try {
488
+ // Re-read the current on-disk patterns and merge this process's
489
+ // in-memory changes in by id (newer lastUsedAt wins) instead of
490
+ // blindly overwriting the whole file with this.patternList — see
491
+ // the P1-14 comment above claimPatternsLock() for why.
492
+ const diskPatterns = readJsonFileSync(getPatternsPath(), []);
493
+ const merged = new Map();
494
+ if (Array.isArray(diskPatterns)) {
495
+ for (const p of diskPatterns) {
496
+ if (p && typeof p.id === 'string')
497
+ merged.set(p.id, p);
498
+ }
499
+ }
500
+ for (const p of this.patternList) {
501
+ const existing = merged.get(p.id);
502
+ if (!existing || (p.lastUsedAt ?? 0) >= (existing.lastUsedAt ?? 0)) {
503
+ merged.set(p.id, p);
504
+ }
505
+ }
506
+ writeJsonFileAtomic(getPatternsPath(), Array.from(merged.values()));
507
+ }
508
+ finally {
509
+ if (acquired)
510
+ releasePatternsLock();
511
+ }
416
512
  this.dirty = false;
417
513
  }
418
514
  catch {
@@ -7,6 +7,8 @@
7
7
  * @module v1/cli/memory-bridge
8
8
  */
9
9
  export declare function safeParseEmbedding(raw: string | null | undefined): number[] | null;
10
+ /** Resolve the real on-disk LanceDB path for a given custom path (or the default). */
11
+ export declare function bridgeGetDbPath(customPath?: string): string;
10
12
  export declare function bridgeStoreEntry(options: {
11
13
  key: string;
12
14
  value: string;
@@ -113,6 +115,11 @@ export declare function bridgeLoadEmbeddingModel(dbPath?: string): Promise<{
113
115
  modelName: string;
114
116
  loadTime?: number;
115
117
  } | null>;
118
+ export declare function bridgeGetBackendStats(dbPath?: string): Promise<{
119
+ totalEntries: number;
120
+ entriesByNamespace: Record<string, number>;
121
+ memoryUsage: number;
122
+ } | null>;
116
123
  export declare function bridgeGetHNSWStatus(dbPath?: string): Promise<{
117
124
  built: boolean;
118
125
  size: number;
@@ -88,6 +88,10 @@ function getDbPath(customPath) {
88
88
  return resolved;
89
89
  return defaultDir;
90
90
  }
91
+ /** Resolve the real on-disk LanceDB path for a given custom path (or the default). */
92
+ export function bridgeGetDbPath(customPath) {
93
+ return getDbPath(customPath);
94
+ }
91
95
  function getAutomemConfig() {
92
96
  const defaults = { dedupThreshold: 0.85, staleDays: 7 };
93
97
  try {
@@ -439,6 +443,22 @@ export async function bridgeLoadEmbeddingModel(dbPath) {
439
443
  return null;
440
444
  }
441
445
  }
446
+ export async function bridgeGetBackendStats(dbPath) {
447
+ const backend = await getBackend(dbPath);
448
+ if (!backend)
449
+ return null;
450
+ try {
451
+ const stats = await backend.getStats();
452
+ return {
453
+ totalEntries: stats?.totalEntries ?? 0,
454
+ entriesByNamespace: stats?.entriesByNamespace ?? {},
455
+ memoryUsage: stats?.memoryUsage ?? 0,
456
+ };
457
+ }
458
+ catch {
459
+ return null;
460
+ }
461
+ }
442
462
  // ===== HNSW (replaced by LanceDB ANN — stubs kept for API compat) =====
443
463
  export async function bridgeGetHNSWStatus(dbPath) {
444
464
  const backend = await getBackend(dbPath);
@@ -94,6 +94,12 @@ export declare class SONAOptimizer {
94
94
  private failedRoutings;
95
95
  private lastUpdate;
96
96
  private persistencePath;
97
+ /** Set when in-memory state diverges from disk — triggers next debounced write */
98
+ private dirty;
99
+ /** NodeJS timeout handle for debounced disk flush */
100
+ private saveTimer;
101
+ /** Debounce window for disk writes (ms) — batches rapid trajectory bursts */
102
+ private static readonly SAVE_DEBOUNCE_MS;
97
103
  constructor(options?: {
98
104
  persistencePath?: string;
99
105
  });
@@ -182,6 +188,12 @@ export declare class SONAOptimizer {
182
188
  * Load patterns from disk
183
189
  */
184
190
  private loadFromDisk;
191
+ /**
192
+ * Schedule a debounced disk flush.
193
+ * Multiple calls within SAVE_DEBOUNCE_MS coalesce into a single write,
194
+ * preventing blocking I/O on every trajectory event during swarm bursts.
195
+ */
196
+ private scheduleSave;
185
197
  /**
186
198
  * Save patterns to disk
187
199
  */
@@ -0,0 +1,26 @@
1
+ export interface BrokerEntry {
2
+ url: string;
3
+ pid: number;
4
+ updatedAt: number;
5
+ }
6
+ export declare function defaultRegistryDir(): string;
7
+ /** Publish that this process hosts org `name`, reachable via `url`. Call again periodically (heartbeat) — see BrokerLease.
8
+ * Writes via tmp+rename (same-directory rename is atomic on POSIX/NTFS) so a concurrent lookupOrg() never observes a
9
+ * partially-written entry — this file is rewritten every heartbeat (default 20s) while other processes may read it. */
10
+ export declare function registerOrg(name: string, url: string, dir?: string): void;
11
+ /** Remove this process's registration for `name` (best effort). */
12
+ export declare function unregisterOrg(name: string, dir?: string): void;
13
+ /** Find which process (if any) currently hosts org `name`. Null if never registered or the registration is stale (owner crashed without cleanup). */
14
+ export declare function lookupOrg(name: string, dir?: string, staleMs?: number): BrokerEntry | null;
15
+ /** Keeps a broker registration alive with periodic heartbeats until stop() is called. */
16
+ export declare class BrokerLease {
17
+ private name;
18
+ private url;
19
+ private dir;
20
+ private intervalMs;
21
+ private timer;
22
+ constructor(name: string, url: string, dir?: string, intervalMs?: number);
23
+ start(): void;
24
+ stop(): void;
25
+ }
26
+ //# sourceMappingURL=broker.d.ts.map
@@ -0,0 +1,73 @@
1
+ // packages/@monomind/cli/src/orgrt/broker.ts
2
+ // monolean: file-based local broker for cross-process org discovery (different
3
+ // `monomind org` processes / project directories, same machine). Upgrade path:
4
+ // a real network registry when cross-machine discovery is needed.
5
+ import { mkdirSync, writeFileSync, readFileSync, unlinkSync, renameSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { homedir } from 'node:os';
8
+ const SAFE_NAME = /^[a-z0-9][a-z0-9_-]*$/i;
9
+ const DEFAULT_STALE_MS = 90_000;
10
+ export function defaultRegistryDir() {
11
+ return process.env.MONOMIND_ORGRT_BROKER_DIR || join(homedir(), '.monomind', 'orgrt-broker');
12
+ }
13
+ function entryPath(name, dir) {
14
+ if (!SAFE_NAME.test(name))
15
+ throw new Error(`invalid org name for broker registry: ${name}`);
16
+ return join(dir, `${name}.json`);
17
+ }
18
+ /** Publish that this process hosts org `name`, reachable via `url`. Call again periodically (heartbeat) — see BrokerLease.
19
+ * Writes via tmp+rename (same-directory rename is atomic on POSIX/NTFS) so a concurrent lookupOrg() never observes a
20
+ * partially-written entry — this file is rewritten every heartbeat (default 20s) while other processes may read it. */
21
+ export function registerOrg(name, url, dir = defaultRegistryDir()) {
22
+ mkdirSync(dir, { recursive: true });
23
+ const entry = { url, pid: process.pid, updatedAt: Date.now() };
24
+ const dest = entryPath(name, dir);
25
+ const tmp = `${dest}.${process.pid}.tmp`;
26
+ writeFileSync(tmp, JSON.stringify(entry));
27
+ renameSync(tmp, dest);
28
+ }
29
+ /** Remove this process's registration for `name` (best effort). */
30
+ export function unregisterOrg(name, dir = defaultRegistryDir()) {
31
+ try {
32
+ unlinkSync(entryPath(name, dir));
33
+ }
34
+ catch { /* already gone */ }
35
+ }
36
+ /** Find which process (if any) currently hosts org `name`. Null if never registered or the registration is stale (owner crashed without cleanup). */
37
+ export function lookupOrg(name, dir = defaultRegistryDir(), staleMs = DEFAULT_STALE_MS) {
38
+ try {
39
+ const entry = JSON.parse(readFileSync(entryPath(name, dir), 'utf8'));
40
+ if (Date.now() - entry.updatedAt >= staleMs)
41
+ return null;
42
+ return entry;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ /** Keeps a broker registration alive with periodic heartbeats until stop() is called. */
49
+ export class BrokerLease {
50
+ name;
51
+ url;
52
+ dir;
53
+ intervalMs;
54
+ timer = null;
55
+ constructor(name, url, dir = defaultRegistryDir(), intervalMs = 20_000) {
56
+ this.name = name;
57
+ this.url = url;
58
+ this.dir = dir;
59
+ this.intervalMs = intervalMs;
60
+ }
61
+ start() {
62
+ registerOrg(this.name, this.url, this.dir);
63
+ this.timer = setInterval(() => registerOrg(this.name, this.url, this.dir), this.intervalMs);
64
+ this.timer.unref?.();
65
+ }
66
+ stop() {
67
+ if (this.timer)
68
+ clearInterval(this.timer);
69
+ this.timer = null;
70
+ unregisterOrg(this.name, this.dir);
71
+ }
72
+ }
73
+ //# sourceMappingURL=broker.js.map
@@ -15,7 +15,11 @@ export declare class OrgBus {
15
15
  constructor(org: string, run: string, dir: string);
16
16
  subscribe(fn: Listener): () => void;
17
17
  emit(partial: Omit<BusEvent, 'id' | 'ts' | 'org' | 'run'>): BusEvent;
18
- /** await all queued disk writes (tests, shutdown) */
18
+ /** Await all queued disk writes (tests, shutdown). Listeners registered via subscribe() run
19
+ * synchronously inside emit() — flush() has no visibility into any async work they schedule
20
+ * off of that (e.g. the forwarder's HTTP POSTs). A caller that needs an async subscriber's
21
+ * work to have settled too must await that subscriber's own completion signal separately
22
+ * (see daemon.ts stopOrg(), which awaits forwarder.settle() alongside bus.flush()). */
19
23
  flush(): Promise<void>;
20
24
  static readHistory(dir: string): BusEvent[];
21
25
  }
@@ -45,7 +45,11 @@ export class OrgBus {
45
45
  }
46
46
  return e;
47
47
  }
48
- /** await all queued disk writes (tests, shutdown) */
48
+ /** Await all queued disk writes (tests, shutdown). Listeners registered via subscribe() run
49
+ * synchronously inside emit() — flush() has no visibility into any async work they schedule
50
+ * off of that (e.g. the forwarder's HTTP POSTs). A caller that needs an async subscriber's
51
+ * work to have settled too must await that subscriber's own completion signal separately
52
+ * (see daemon.ts stopOrg(), which awaits forwarder.settle() alongside bus.flush()). */
49
53
  flush() { return this.pending; }
50
54
  static readHistory(dir) {
51
55
  const f = join(dir, 'bus.jsonl');
@@ -7,6 +7,9 @@ interface AgentRuntime {
7
7
  mailbox: Mailbox;
8
8
  policy: PolicyEngine;
9
9
  done: Promise<void>;
10
+ /** 'running' until the session promise settles; 'crashed' if it rejected (see error). */
11
+ status: 'running' | 'ended' | 'crashed';
12
+ error?: string;
10
13
  }
11
14
  export interface RunningOrg {
12
15
  def: OrgDef;
@@ -19,20 +22,63 @@ export interface DaemonOpts {
19
22
  queryFn?: typeof query;
20
23
  forward?: boolean;
21
24
  controlJson?: string;
25
+ /** Enables cross-process inter-org routing: on a local delivery miss, ask the
26
+ * machine-local broker whether another `monomind org` process (e.g. a
27
+ * different project directory) hosts the target org, and deliver over HTTP
28
+ * if so. Off by default — tests and single-process runs don't need it. */
29
+ crossProcess?: boolean;
30
+ /** Base URL at which OTHER processes can reach this daemon's inbox (see
31
+ * server.ts POST /api/xdeliver). Set this to make orgs hosted here
32
+ * discoverable; omit for outbound-only cross-process delivery. */
33
+ inboxUrl?: string;
34
+ /** Override the broker's file registry directory (tests only). */
35
+ brokerDir?: string;
36
+ /** Override how long stopOrg() waits for agent sessions before proceeding anyway (tests only; default 15000ms). */
37
+ stopWaitMs?: number;
22
38
  }
23
39
  export declare class OrgDaemon {
24
40
  private root;
25
41
  private opts;
26
42
  private orgs;
43
+ private waking;
27
44
  private globalSubscribers;
45
+ private leases;
46
+ private forwarders;
28
47
  constructor(root: string, opts?: DaemonOpts);
48
+ /** Publish this daemon's inbox so orgs started AFTER this call register with the broker. */
49
+ setInboxUrl(url: string): void;
29
50
  /** subscribe to events from ALL running orgs (dashboard server uses this) */
30
51
  subscribe(fn: (e: BusEvent) => void): () => void;
31
52
  listOrgs(): RunningOrg[];
32
53
  getOrg(name: string): RunningOrg | undefined;
33
54
  startOrg(name: string, taskOverride?: string): Promise<RunningOrg>;
55
+ /**
56
+ * Resolves an org_send `to` address ("role" for same-org, "org:role" for
57
+ * cross-org) into its parts. Centralizes the one addressing rule that
58
+ * matters (an "own-org:role" self-prefix is intra-org, not cross-org) so
59
+ * deliver()/deliverRemote() don't each re-derive it — the qualified `to`
60
+ * string returned is always the canonical display form for that address.
61
+ */
62
+ private resolveAddress;
34
63
  /** Route a message. to = "role" (same org) or "org:role" (cross-org). Returns a receipt string. */
35
64
  deliver(fromOrg: string, fromRole: string, to: string, subject: string, body: string): Promise<string>;
65
+ /** Cross-process leg of deliver(): ask the machine-local broker who hosts targetOrgName, then POST over HTTP.
66
+ * `to` here is always the fully-qualified "org:role" display form (resolveAddress already normalized it). */
67
+ private deliverRemote;
68
+ /** Inbound handler for cross-process delivery — called by the server's POST /api/xdeliver route
69
+ * when ANOTHER process's deliverRemote() reaches this daemon. Pushes straight into the target
70
+ * agent's mailbox; the agent picks it up on its own next turn (see Mailbox — never interrupts). */
71
+ receiveRemote(toOrg: string, toRole: string, fromQualified: string, subject: string, body: string): {
72
+ ok: true;
73
+ receipt: string;
74
+ } | {
75
+ ok: false;
76
+ error: string;
77
+ };
78
+ private hasOrgDef;
79
+ /** Start an offline org in the background so queued messages get drained.
80
+ * Fire-and-forget — errors are logged but don't propagate to the sender. */
81
+ private autoWake;
36
82
  stopOrg(name: string): Promise<void>;
37
83
  stopAll(): Promise<void>;
38
84
  private persistState;