monomind 2.7.6 → 2.7.8
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.
- package/package.json +5 -4
- package/packages/@monomind/cli/.claude/agents/core/coder.md +9 -0
- package/packages/@monomind/cli/.claude/agents/core/coordinator.md +62 -0
- package/packages/@monomind/cli/.claude/agents/core/planner.md +9 -0
- package/packages/@monomind/cli/.claude/agents/core/reviewer.md +9 -0
- package/packages/@monomind/cli/.claude/agents/core/tester.md +8 -0
- package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +180 -47
- package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +32 -3
- package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +55 -3
- package/packages/@monomind/cli/.claude/helpers/intelligence.cjs +3 -1
- package/packages/@monomind/cli/.claude/helpers/statusline.cjs +27 -3
- package/packages/@monomind/cli/.claude/helpers/utils/monograph.cjs +104 -18
- package/packages/@monomind/cli/.claude/settings.json +1 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-server.mjs +38 -1
- package/packages/@monomind/cli/dist/src/browser/dashboard/server.js +6 -1
- package/packages/@monomind/cli/dist/src/capabilities/index.d.ts +0 -1
- package/packages/@monomind/cli/dist/src/capabilities/index.js +8 -1
- package/packages/@monomind/cli/dist/src/commands/agent-lifecycle.js +5 -1
- package/packages/@monomind/cli/dist/src/commands/agent-ops.js +32 -2
- package/packages/@monomind/cli/dist/src/commands/browse-workflow.js +30 -0
- package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.js +8 -1
- package/packages/@monomind/cli/dist/src/commands/guidance.js +8 -2
- package/packages/@monomind/cli/dist/src/commands/hooks-extended-commands.js +10 -2
- package/packages/@monomind/cli/dist/src/commands/init.js +18 -4
- package/packages/@monomind/cli/dist/src/commands/memory-crud.js +40 -3
- package/packages/@monomind/cli/dist/src/commands/org-observe.js +67 -13
- package/packages/@monomind/cli/dist/src/commands/org.d.ts +11 -0
- package/packages/@monomind/cli/dist/src/commands/org.js +162 -15
- package/packages/@monomind/cli/dist/src/commands/performance.js +31 -5
- package/packages/@monomind/cli/dist/src/commands/security-misc.js +18 -3
- package/packages/@monomind/cli/dist/src/commands/security-scan.d.ts +30 -1
- package/packages/@monomind/cli/dist/src/commands/security-scan.js +182 -69
- package/packages/@monomind/cli/dist/src/commands/swarm.js +41 -14
- package/packages/@monomind/cli/dist/src/consensus/audit-writer.js +11 -10
- package/packages/@monomind/cli/dist/src/init/executor.js +138 -22
- package/packages/@monomind/cli/dist/src/init/settings-generator.js +4 -1
- package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.d.ts +17 -0
- package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.js +62 -3
- package/packages/@monomind/cli/dist/src/mcp-client.js +78 -0
- package/packages/@monomind/cli/dist/src/mcp-tools/embeddings-tools.js +16 -8
- package/packages/@monomind/cli/dist/src/mcp-tools/knowledge-tools.js +27 -13
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +5 -1
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +26 -3
- package/packages/@monomind/cli/dist/src/memory/memory-read.d.ts +9 -0
- package/packages/@monomind/cli/dist/src/memory/memory-read.js +13 -2
- package/packages/@monomind/cli/dist/src/monovector/diff-classifier.js +25 -6
- package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +21 -1
- package/packages/@monomind/cli/dist/src/orgrt/daemon.js +82 -11
- package/packages/@monomind/cli/dist/src/orgrt/inbox.js +53 -20
- package/packages/@monomind/cli/dist/src/parser.d.ts +4 -2
- package/packages/@monomind/cli/dist/src/parser.js +61 -25
- package/packages/@monomind/cli/dist/src/routing/route-layer-factory.d.ts +15 -0
- package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +44 -2
- package/packages/@monomind/cli/dist/src/services/config-file-manager.d.ts +10 -2
- package/packages/@monomind/cli/dist/src/services/config-file-manager.js +10 -2
- package/packages/@monomind/cli/dist/src/ui/collector.mjs +43 -3
- package/packages/@monomind/cli/dist/src/ui/dashboard.html +27 -8
- package/packages/@monomind/cli/dist/src/ui/server.mjs +144 -14
- package/packages/@monomind/cli/package.json +7 -6
- package/packages/@monomind/cli/dist/src/capabilities/watcher.d.ts +0 -18
- package/packages/@monomind/cli/dist/src/capabilities/watcher.js +0 -107
- package/packages/@monomind/cli/dist/src/config-adapter.d.ts +0 -16
- package/packages/@monomind/cli/dist/src/config-adapter.js +0 -220
|
@@ -406,7 +406,15 @@ export async function bridgeSearchEntries(options) {
|
|
|
406
406
|
const startTime = Date.now();
|
|
407
407
|
let results = [];
|
|
408
408
|
let searchMethod = 'keyword';
|
|
409
|
+
// Reported to callers so "(semantic)" can never be printed over keyword hits.
|
|
410
|
+
// The two reasons for skipping the vector path are distinct and must not be
|
|
411
|
+
// conflated: a healthy model given an empty query is not a missing model.
|
|
412
|
+
let fallbackReason = !_embedder ? 'no-embedding-model'
|
|
413
|
+
: queryStr.length === 0 ? 'empty-query'
|
|
414
|
+
: undefined;
|
|
415
|
+
let semanticAttempted = false;
|
|
409
416
|
if (_embedder && queryStr.length > 0) {
|
|
417
|
+
semanticAttempted = true;
|
|
410
418
|
try {
|
|
411
419
|
const queryEmbedding = await _embedder(queryStr);
|
|
412
420
|
const searchResults = await backend.search(queryEmbedding, {
|
|
@@ -431,8 +439,14 @@ export async function bridgeSearchEntries(options) {
|
|
|
431
439
|
};
|
|
432
440
|
}).sort((a, b) => b.score - a.score);
|
|
433
441
|
searchMethod = 'semantic';
|
|
442
|
+
fallbackReason = undefined;
|
|
443
|
+
}
|
|
444
|
+
catch (e) {
|
|
445
|
+
// fall through to keyword search — but never claim this was semantic
|
|
446
|
+
fallbackReason = 'embedding-failed';
|
|
447
|
+
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
448
|
+
console.error('[memory-bridge] semantic search failed — falling back to keyword matching:', e);
|
|
434
449
|
}
|
|
435
|
-
catch { /* fall through to keyword search */ }
|
|
436
450
|
}
|
|
437
451
|
// Keyword fallback — scan all entries in namespace (not just first 100)
|
|
438
452
|
// to avoid missing documents that were ingested later in the batch.
|
|
@@ -463,14 +477,22 @@ export async function bridgeSearchEntries(options) {
|
|
|
463
477
|
id: e.id,
|
|
464
478
|
key: e.key,
|
|
465
479
|
content: e.content || '',
|
|
466
|
-
|
|
480
|
+
// Raw token-overlap fraction, NOT rescaled to look like a cosine.
|
|
481
|
+
// The old `min(0.9, 0.3 + score*0.6)` floor/ceiling made a weak
|
|
482
|
+
// keyword hit outrank a genuine cosine match (0.90 vs 0.63 for the
|
|
483
|
+
// same entry) and fed cosine-calibrated gates (memory-kg dedup)
|
|
484
|
+
// scores that never came from a vector.
|
|
485
|
+
score,
|
|
467
486
|
namespace: e.namespace,
|
|
468
487
|
provenance: `keyword:${score.toFixed(2)}`,
|
|
469
488
|
tags: e.tags ?? [],
|
|
470
489
|
_createdAt: e.createdAt || 0,
|
|
471
490
|
}));
|
|
472
491
|
}
|
|
473
|
-
|
|
492
|
+
// The vector path ran and simply matched nothing — still not semantic.
|
|
493
|
+
searchMethod = semanticAttempted ? 'keyword-fallback' : 'keyword';
|
|
494
|
+
if (semanticAttempted && !fallbackReason)
|
|
495
|
+
fallbackReason = 'no-semantic-matches';
|
|
474
496
|
}
|
|
475
497
|
// Filter stale entries based on automem config — skip for knowledge
|
|
476
498
|
// namespaces (documents should remain searchable indefinitely)
|
|
@@ -494,6 +516,7 @@ export async function bridgeSearchEntries(options) {
|
|
|
494
516
|
results,
|
|
495
517
|
searchTime: Date.now() - startTime,
|
|
496
518
|
searchMethod,
|
|
519
|
+
...(searchMethod === 'semantic' ? {} : { fallbackReason }),
|
|
497
520
|
};
|
|
498
521
|
}
|
|
499
522
|
catch {
|
|
@@ -25,6 +25,15 @@ export declare function searchEntries(options: {
|
|
|
25
25
|
namespace: string;
|
|
26
26
|
}[];
|
|
27
27
|
searchTime: number;
|
|
28
|
+
/** What actually ran — propagated from the bridge so callers can report the
|
|
29
|
+
* real method instead of echoing the requested one.
|
|
30
|
+
*
|
|
31
|
+
* 'hash-vector'/'hash-hybrid' are the sql.js paths running on
|
|
32
|
+
* generateEmbedding()'s deterministic hash fallback: a vector search was
|
|
33
|
+
* performed, but over hashes with no semantic content, so neither may be
|
|
34
|
+
* reported as 'semantic'/'hybrid'. */
|
|
35
|
+
searchMethod?: 'semantic' | 'keyword' | 'keyword-fallback' | 'hybrid' | 'hash-vector' | 'hash-hybrid';
|
|
36
|
+
fallbackReason?: string;
|
|
28
37
|
error?: string;
|
|
29
38
|
}>;
|
|
30
39
|
/**
|
|
@@ -59,6 +59,11 @@ export async function searchEntries(options) {
|
|
|
59
59
|
}
|
|
60
60
|
const queryEmb = await generateEmbedding(query);
|
|
61
61
|
const queryEmbedding = queryEmb.embedding;
|
|
62
|
+
// generateEmbedding() never fails — when no ONNX model can be loaded it
|
|
63
|
+
// silently returns generateHashEmbedding() output tagged 'hash-fallback'.
|
|
64
|
+
// Cosine over those hashes is a deterministic lexical trick with no
|
|
65
|
+
// semantic content, so the methods below must not claim otherwise.
|
|
66
|
+
const realVectors = queryEmb.model !== 'hash-fallback';
|
|
62
67
|
// Try HNSW search first (150x faster)
|
|
63
68
|
const hnswResults = await searchHNSWIndex(queryEmbedding, { k: limit, namespace: effectiveNamespace });
|
|
64
69
|
if (hnswResults && hnswResults.length > 0) {
|
|
@@ -66,7 +71,9 @@ export async function searchEntries(options) {
|
|
|
66
71
|
return {
|
|
67
72
|
success: true,
|
|
68
73
|
results: filtered,
|
|
69
|
-
searchTime: Date.now() - startTime
|
|
74
|
+
searchTime: Date.now() - startTime,
|
|
75
|
+
searchMethod: realVectors ? 'semantic' : 'hash-vector',
|
|
76
|
+
...(realVectors ? {} : { fallbackReason: 'no-embedding-model' })
|
|
70
77
|
};
|
|
71
78
|
}
|
|
72
79
|
// Fall back to brute-force SQLite search
|
|
@@ -127,7 +134,11 @@ export async function searchEntries(options) {
|
|
|
127
134
|
return {
|
|
128
135
|
success: true,
|
|
129
136
|
results: results.slice(0, limit),
|
|
130
|
-
searchTime: Date.now() - startTime
|
|
137
|
+
searchTime: Date.now() - startTime,
|
|
138
|
+
// Per-row: cosine when the entry had a vector, keyword overlap otherwise.
|
|
139
|
+
// With hash-fallback vectors the cosine half is not semantic either.
|
|
140
|
+
searchMethod: realVectors ? 'hybrid' : 'hash-hybrid',
|
|
141
|
+
...(realVectors ? {} : { fallbackReason: 'no-embedding-model' })
|
|
131
142
|
};
|
|
132
143
|
}
|
|
133
144
|
catch (error) {
|
|
@@ -352,12 +352,15 @@ export function getGitDiffNumstat(ref = 'HEAD') {
|
|
|
352
352
|
try {
|
|
353
353
|
// SECURITY: Use execFileSync with args array instead of shell string
|
|
354
354
|
// This prevents command injection via the ref parameter
|
|
355
|
+
// stdio must be fully piped: execFileSync otherwise forwards git's stderr
|
|
356
|
+
// straight to the parent's terminal AND leaves err.stderr null, so the
|
|
357
|
+
// thrown error below would carry no explanation of what git objected to.
|
|
355
358
|
const numstatOutput = execFileSync('git', [
|
|
356
359
|
'diff', '--numstat', '--diff-filter=ACDMRTUXB', ref
|
|
357
|
-
], { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
|
360
|
+
], { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
358
361
|
const statusOutput = execFileSync('git', [
|
|
359
362
|
'diff', '--name-status', ref
|
|
360
|
-
], { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
|
363
|
+
], { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
361
364
|
const output = numstatOutput + '---STATUS---' + statusOutput;
|
|
362
365
|
const [numstatPart, statusPart] = output.split('---STATUS---');
|
|
363
366
|
// Parse status (usually smaller, parse first)
|
|
@@ -411,10 +414,25 @@ export function getGitDiffNumstat(ref = 'HEAD') {
|
|
|
411
414
|
diffCache.set(cacheKey, { files, timestamp: Date.now() });
|
|
412
415
|
return files;
|
|
413
416
|
}
|
|
414
|
-
catch {
|
|
415
|
-
return []
|
|
417
|
+
catch (e) {
|
|
418
|
+
// Do NOT return [] here. An empty array is indistinguishable from a genuinely
|
|
419
|
+
// clean diff, so a broken git invocation (not a repo, bad ref, git missing)
|
|
420
|
+
// used to surface as "low risk, 0 files changed". Callers (analyze_* MCP
|
|
421
|
+
// tools) already convert a throw into an explicit error response.
|
|
422
|
+
throw new Error(`git diff failed for ref "${ref}": ${describeGitError(e)}`);
|
|
416
423
|
}
|
|
417
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Extract a useful message from an execFile error, preferring git's own stderr.
|
|
427
|
+
*/
|
|
428
|
+
function describeGitError(e) {
|
|
429
|
+
const err = e;
|
|
430
|
+
const stderr = err && typeof err.stderr === 'string' ? err.stderr.trim() : '';
|
|
431
|
+
if (stderr)
|
|
432
|
+
return stderr.split('\n')[0].slice(0, 300);
|
|
433
|
+
const msg = err && typeof err.message === 'string' ? err.message : String(e);
|
|
434
|
+
return msg.slice(0, 300);
|
|
435
|
+
}
|
|
418
436
|
/**
|
|
419
437
|
* Async version of getGitDiffNumstat for non-blocking operation
|
|
420
438
|
*/
|
|
@@ -480,8 +498,9 @@ export async function getGitDiffNumstatAsync(ref = 'HEAD') {
|
|
|
480
498
|
diffCache.set(cacheKey, { files, timestamp: Date.now() });
|
|
481
499
|
return files;
|
|
482
500
|
}
|
|
483
|
-
catch {
|
|
484
|
-
|
|
501
|
+
catch (e) {
|
|
502
|
+
// See getGitDiffNumstat: a git failure must not look like a clean diff.
|
|
503
|
+
throw new Error(`git diff failed for ref "${ref}": ${describeGitError(e)}`);
|
|
485
504
|
}
|
|
486
505
|
}
|
|
487
506
|
/**
|
|
@@ -53,6 +53,9 @@ export declare class OrgDaemon {
|
|
|
53
53
|
subscribe(fn: (e: BusEvent) => void): () => void;
|
|
54
54
|
listOrgs(): RunningOrg[];
|
|
55
55
|
getOrg(name: string): RunningOrg | undefined;
|
|
56
|
+
/** Names of the orgs this daemon currently has running. Snapshot — safe to
|
|
57
|
+
* iterate while stopOrg() mutates the underlying map. */
|
|
58
|
+
listRunning(): string[];
|
|
56
59
|
startOrg(name: string, taskOverride?: string): Promise<RunningOrg>;
|
|
57
60
|
/** A role that failed its resource gate at boot isn't abandoned — keep polling
|
|
58
61
|
* for capacity in the background (bounded) and spawn it the moment resources
|
|
@@ -96,7 +99,16 @@ export declare class OrgDaemon {
|
|
|
96
99
|
* running, pushes straight into the role's live mailbox (picked up on its very next
|
|
97
100
|
* generator tick — see Mailbox.stream()). If the org has since stopped, queues the
|
|
98
101
|
* answer via the same offline fallback deliver()/receiveRemote() already use
|
|
99
|
-
* (inbox.ts + autoWake) and it's delivered when the org next starts.
|
|
102
|
+
* (inbox.ts + autoWake) and it's delivered when the org next starts.
|
|
103
|
+
*
|
|
104
|
+
* PERSIST-AFTER-DELIVERY: questions.json is only marked answered once delivery has
|
|
105
|
+
* actually happened (mailbox push, or the message landing in inbox.jsonl). Marking it
|
|
106
|
+
* first meant a rejected delivery — unknown role, mailbox closed mid-shutdown, a
|
|
107
|
+
* queueMessage that threw on a full/read-only disk — left the question recorded as
|
|
108
|
+
* answered while nobody ever received the answer, and the `already answered` guard
|
|
109
|
+
* then refused every retry. The answer was simply gone. The inverse failure (crash
|
|
110
|
+
* between delivery and the write) merely re-shows the question as pending, which a
|
|
111
|
+
* human can act on; a silently swallowed answer is not recoverable. */
|
|
100
112
|
answerQuestion(org: string, role: string, questionId: string, answer: string): Promise<{
|
|
101
113
|
ok: true;
|
|
102
114
|
} | {
|
|
@@ -150,6 +162,14 @@ export declare class OrgDaemon {
|
|
|
150
162
|
* future runs) can find it by meaning, not just recency. Best-effort. */
|
|
151
163
|
private storeRunMemory;
|
|
152
164
|
private persistState;
|
|
165
|
+
/** Mark every currently-running org as crashed in runtime.json.
|
|
166
|
+
* Called from process-level crash handlers — must be synchronous and best-effort. */
|
|
167
|
+
persistCrashStateAll(): void;
|
|
168
|
+
private heartbeatPath;
|
|
169
|
+
/** Write a heartbeat file so `org status` can distinguish "daemon alive" from
|
|
170
|
+
* "daemon gone" even when runtime.json still says running. */
|
|
171
|
+
writeHeartbeat(): void;
|
|
172
|
+
clearHeartbeat(): void;
|
|
153
173
|
}
|
|
154
174
|
export {};
|
|
155
175
|
//# sourceMappingURL=daemon.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// packages/@monomind/cli/src/orgrt/daemon.ts
|
|
2
2
|
// monolean: single-process inter-org — upgrade path = daemon-to-daemon HTTP when multi-host is real
|
|
3
|
-
import { readFileSync, mkdirSync, writeFileSync, existsSync, renameSync } from 'node:fs';
|
|
3
|
+
import { readFileSync, mkdirSync, writeFileSync, existsSync, renameSync, unlinkSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { reapOrphanedSdkProcesses } from '../utils/resource-governor.js';
|
|
6
6
|
import { OrgBus } from './bus.js';
|
|
@@ -36,6 +36,9 @@ export class OrgDaemon {
|
|
|
36
36
|
}
|
|
37
37
|
listOrgs() { return [...this.orgs.values()]; }
|
|
38
38
|
getOrg(name) { return this.orgs.get(name); }
|
|
39
|
+
/** Names of the orgs this daemon currently has running. Snapshot — safe to
|
|
40
|
+
* iterate while stopOrg() mutates the underlying map. */
|
|
41
|
+
listRunning() { return [...this.orgs.keys()]; }
|
|
39
42
|
async startOrg(name, taskOverride) {
|
|
40
43
|
if (this.orgs.has(name))
|
|
41
44
|
throw new Error(`org ${name} already running`);
|
|
@@ -500,7 +503,16 @@ export class OrgDaemon {
|
|
|
500
503
|
* running, pushes straight into the role's live mailbox (picked up on its very next
|
|
501
504
|
* generator tick — see Mailbox.stream()). If the org has since stopped, queues the
|
|
502
505
|
* answer via the same offline fallback deliver()/receiveRemote() already use
|
|
503
|
-
* (inbox.ts + autoWake) and it's delivered when the org next starts.
|
|
506
|
+
* (inbox.ts + autoWake) and it's delivered when the org next starts.
|
|
507
|
+
*
|
|
508
|
+
* PERSIST-AFTER-DELIVERY: questions.json is only marked answered once delivery has
|
|
509
|
+
* actually happened (mailbox push, or the message landing in inbox.jsonl). Marking it
|
|
510
|
+
* first meant a rejected delivery — unknown role, mailbox closed mid-shutdown, a
|
|
511
|
+
* queueMessage that threw on a full/read-only disk — left the question recorded as
|
|
512
|
+
* answered while nobody ever received the answer, and the `already answered` guard
|
|
513
|
+
* then refused every retry. The answer was simply gone. The inverse failure (crash
|
|
514
|
+
* between delivery and the write) merely re-shows the question as pending, which a
|
|
515
|
+
* human can act on; a silently swallowed answer is not recoverable. */
|
|
504
516
|
async answerQuestion(org, role, questionId, answer) {
|
|
505
517
|
const data = this.readQuestions(org);
|
|
506
518
|
const idx = data.questions.findIndex(q => q.questionId === questionId);
|
|
@@ -508,8 +520,19 @@ export class OrgDaemon {
|
|
|
508
520
|
return { ok: false, error: `question "${questionId}" not found for org "${org}"` };
|
|
509
521
|
if (data.questions[idx].answer !== null)
|
|
510
522
|
return { ok: false, error: `question "${questionId}" already answered` };
|
|
511
|
-
|
|
512
|
-
|
|
523
|
+
const question = data.questions[idx].question;
|
|
524
|
+
// Applied to questions.json ONLY after the delivery below succeeds.
|
|
525
|
+
const markAnswered = () => {
|
|
526
|
+
// Re-read so a question the daemon appended (or answered) meanwhile isn't
|
|
527
|
+
// clobbered by this stale snapshot — merge by questionId, never replace.
|
|
528
|
+
const fresh = this.readQuestions(org);
|
|
529
|
+
const fIdx = fresh.questions.findIndex(q => q.questionId === questionId);
|
|
530
|
+
if (fIdx === -1)
|
|
531
|
+
fresh.questions.push({ ...data.questions[idx], answer, answeredAt: Date.now() });
|
|
532
|
+
else
|
|
533
|
+
fresh.questions[fIdx] = { ...fresh.questions[fIdx], answer, answeredAt: Date.now() };
|
|
534
|
+
this.writeQuestions(org, fresh);
|
|
535
|
+
};
|
|
513
536
|
const running = this.orgs.get(org);
|
|
514
537
|
if (running) {
|
|
515
538
|
// Org IS running — deliver or report a real error, but never fall through to the
|
|
@@ -523,20 +546,32 @@ export class OrgDaemon {
|
|
|
523
546
|
return { ok: false, error: `role "${role}" not found in org "${org}"` };
|
|
524
547
|
if (agent.mailbox.isClosed)
|
|
525
548
|
return { ok: false, error: `role "${role}" in org "${org}" is shutting down — answer not delivered` };
|
|
549
|
+
try {
|
|
550
|
+
agent.mailbox.push(`[answer from human] question: ${question}\n\nanswer: ${answer}`);
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
return { ok: false, error: `delivery to "${role}" in org "${org}" failed — answer not recorded (${err instanceof Error ? err.message : String(err)})` };
|
|
554
|
+
}
|
|
555
|
+
markAnswered();
|
|
526
556
|
running.bus.emit({ type: 'status', from: role, msg: 'question answered', data: { questionId } });
|
|
527
|
-
agent.mailbox.push(`[answer from human] question: ${data.questions[idx].question}\n\nanswer: ${answer}`);
|
|
528
557
|
return { ok: true };
|
|
529
558
|
}
|
|
530
559
|
// Org not running at all — queue for delivery on next start, matching deliver()'s
|
|
531
560
|
// existing offline fallback exactly (inbox.ts + autoWake).
|
|
532
561
|
if (!this.hasOrgDef(org))
|
|
533
562
|
return { ok: false, error: `org "${org}" not found (no saved definition)` };
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
563
|
+
try {
|
|
564
|
+
queueMessage(this.root, org, {
|
|
565
|
+
fromQualified: 'human', toRole: role,
|
|
566
|
+
subject: `answer:${questionId}`,
|
|
567
|
+
body: `question: ${question}\n\nanswer: ${answer}`,
|
|
568
|
+
ts: Date.now(),
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
catch (err) {
|
|
572
|
+
return { ok: false, error: `could not queue answer for org "${org}" — answer not recorded (${err instanceof Error ? err.message : String(err)})` };
|
|
573
|
+
}
|
|
574
|
+
markAnswered();
|
|
540
575
|
this.autoWake(org);
|
|
541
576
|
return { ok: true };
|
|
542
577
|
}
|
|
@@ -890,5 +925,41 @@ export class OrgDaemon {
|
|
|
890
925
|
const p = join(this.root, ORG_DIR, name, 'runtime.json');
|
|
891
926
|
writeFileSync(p, JSON.stringify({ status, run, pid: process.pid, updated: new Date().toISOString() }, null, 2));
|
|
892
927
|
}
|
|
928
|
+
/** Mark every currently-running org as crashed in runtime.json.
|
|
929
|
+
* Called from process-level crash handlers — must be synchronous and best-effort. */
|
|
930
|
+
persistCrashStateAll() {
|
|
931
|
+
for (const [name, org] of this.orgs) {
|
|
932
|
+
try {
|
|
933
|
+
const p = join(this.root, ORG_DIR, name, 'runtime.json');
|
|
934
|
+
writeFileSync(p, JSON.stringify({
|
|
935
|
+
status: 'crashed', run: org.run, pid: process.pid,
|
|
936
|
+
updated: new Date().toISOString(), closedBy: 'crash-handler',
|
|
937
|
+
}, null, 2));
|
|
938
|
+
}
|
|
939
|
+
catch { /* best effort — filesystem may be unavailable */ }
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
heartbeatPath() {
|
|
943
|
+
return join(this.root, '.monomind', 'serve-heartbeat.json');
|
|
944
|
+
}
|
|
945
|
+
/** Write a heartbeat file so `org status` can distinguish "daemon alive" from
|
|
946
|
+
* "daemon gone" even when runtime.json still says running. */
|
|
947
|
+
writeHeartbeat() {
|
|
948
|
+
try {
|
|
949
|
+
const p = this.heartbeatPath();
|
|
950
|
+
mkdirSync(join(this.root, '.monomind'), { recursive: true });
|
|
951
|
+
writeFileSync(p, JSON.stringify({
|
|
952
|
+
pid: process.pid, updatedAt: new Date().toISOString(),
|
|
953
|
+
running: this.listRunning(),
|
|
954
|
+
}, null, 2));
|
|
955
|
+
}
|
|
956
|
+
catch { /* best effort */ }
|
|
957
|
+
}
|
|
958
|
+
clearHeartbeat() {
|
|
959
|
+
try {
|
|
960
|
+
unlinkSync(this.heartbeatPath());
|
|
961
|
+
}
|
|
962
|
+
catch { /* already gone or never written */ }
|
|
963
|
+
}
|
|
893
964
|
}
|
|
894
965
|
//# sourceMappingURL=daemon.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// packages/@monomind/cli/src/orgrt/inbox.ts
|
|
2
2
|
// Persistent message queue for offline orgs. Messages that can't be delivered
|
|
3
3
|
// (target org not running) are spooled here and drained when the org starts.
|
|
4
|
-
import { appendFileSync, readFileSync,
|
|
4
|
+
import { appendFileSync, readFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { ORG_DIR } from './types.js';
|
|
7
7
|
function inboxPath(root, orgName) {
|
|
@@ -12,37 +12,70 @@ export function queueMessage(root, orgName, msg) {
|
|
|
12
12
|
mkdirSync(dir, { recursive: true });
|
|
13
13
|
appendFileSync(inboxPath(root, orgName), JSON.stringify(msg) + '\n');
|
|
14
14
|
}
|
|
15
|
+
function parseLines(raw) {
|
|
16
|
+
const msgs = [];
|
|
17
|
+
for (const line of raw.trim().split('\n')) {
|
|
18
|
+
if (!line)
|
|
19
|
+
continue;
|
|
20
|
+
try {
|
|
21
|
+
msgs.push(JSON.parse(line));
|
|
22
|
+
}
|
|
23
|
+
catch { /* skip corrupt lines */ }
|
|
24
|
+
}
|
|
25
|
+
return msgs;
|
|
26
|
+
}
|
|
15
27
|
export function drainInbox(root, orgName) {
|
|
16
28
|
const path = inboxPath(root, orgName);
|
|
29
|
+
const draining = `${path}.draining`;
|
|
30
|
+
const msgs = [];
|
|
31
|
+
// Recover a .draining file left behind by a mid-drain crash. Without this the
|
|
32
|
+
// rename below would overwrite it and lose exactly the messages the rename-then-read
|
|
33
|
+
// scheme exists to protect.
|
|
34
|
+
if (existsSync(draining)) {
|
|
35
|
+
try {
|
|
36
|
+
msgs.push(...parseLines(readFileSync(draining, 'utf8')));
|
|
37
|
+
unlinkSync(draining);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
41
|
+
console.error('[inbox] drainInbox recovery of .draining failed:', e);
|
|
42
|
+
return msgs; // don't rename over a file we couldn't consume
|
|
43
|
+
}
|
|
44
|
+
}
|
|
17
45
|
if (!existsSync(path))
|
|
18
|
-
return
|
|
46
|
+
return msgs;
|
|
19
47
|
// Rename-then-read: if the process crashes after rename but before we finish
|
|
20
|
-
// reading, the .draining file survives
|
|
21
|
-
// read-then-truncate would lose messages on a mid-drain crash.
|
|
22
|
-
const draining = `${path}.draining`;
|
|
48
|
+
// reading, the .draining file survives (and is recovered above on the next
|
|
49
|
+
// drain). A plain read-then-truncate would lose messages on a mid-drain crash.
|
|
23
50
|
try {
|
|
24
51
|
renameSync(path, draining);
|
|
25
52
|
}
|
|
26
53
|
catch (e) {
|
|
27
54
|
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
28
55
|
console.error('[inbox] drainInbox rename failed:', e);
|
|
29
|
-
return
|
|
56
|
+
return msgs;
|
|
30
57
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
renameSync(draining, path);
|
|
35
|
-
return [];
|
|
58
|
+
let raw = '';
|
|
59
|
+
try {
|
|
60
|
+
raw = readFileSync(draining, 'utf8');
|
|
36
61
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
catch (e) {
|
|
63
|
+
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
64
|
+
console.error('[inbox] drainInbox read failed:', e);
|
|
65
|
+
return msgs;
|
|
66
|
+
}
|
|
67
|
+
msgs.push(...parseLines(raw));
|
|
68
|
+
// Unlink the drained snapshot rather than truncating it and renaming it back over
|
|
69
|
+
// `path`. A sender that appended between the rename above and this point created a
|
|
70
|
+
// fresh `path` (queueMessage mkdir+appends), and renaming the emptied snapshot back
|
|
71
|
+
// would clobber it — destroying messages whose sender already got a "queued" receipt.
|
|
72
|
+
// Those messages now simply stay in `path` and are picked up by the next drain.
|
|
73
|
+
try {
|
|
74
|
+
unlinkSync(draining);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
if (process.env.DEBUG || process.env.MONOMIND_DEBUG)
|
|
78
|
+
console.error('[inbox] drainInbox unlink failed:', e);
|
|
46
79
|
}
|
|
47
80
|
return msgs;
|
|
48
81
|
}
|
|
@@ -74,12 +74,14 @@ export declare class CommandParser {
|
|
|
74
74
|
* fixing collisions where multiple subcommands use the same short flag (e.g. -t).
|
|
75
75
|
*/
|
|
76
76
|
private buildScopedAliases;
|
|
77
|
+
/** Normalize a single command or a resolved chain into an array. */
|
|
78
|
+
private static asChain;
|
|
77
79
|
/**
|
|
78
|
-
* Get boolean flags scoped to a specific command/subcommand.
|
|
80
|
+
* Get boolean flags scoped to a specific command/subcommand chain.
|
|
79
81
|
*/
|
|
80
82
|
private getScopedBooleanFlags;
|
|
81
83
|
/**
|
|
82
|
-
* Get flags declared `type: 'array'`, scoped to a specific command/subcommand.
|
|
84
|
+
* Get flags declared `type: 'array'`, scoped to a specific command/subcommand chain.
|
|
83
85
|
*/
|
|
84
86
|
private getScopedArrayFlags;
|
|
85
87
|
private getBooleanFlags;
|
|
@@ -166,26 +166,51 @@ export class CommandParser {
|
|
|
166
166
|
positional: [],
|
|
167
167
|
raw: [...args]
|
|
168
168
|
};
|
|
169
|
-
// Pass 1: Identify command and subcommand (skip flags)
|
|
169
|
+
// Pass 1: Identify the command and its subcommand chain (skip flags).
|
|
170
|
+
//
|
|
171
|
+
// This walks the WHOLE tree, not just one subcommand level. It used to
|
|
172
|
+
// stop after depth 1 (`resolvedCmd` + `resolvedSub`), so options declared
|
|
173
|
+
// on a subcommand nested 2+ levels deep were invisible to the alias and
|
|
174
|
+
// boolean scans below: `hooks worker run -n audit` resolved `-n` from the
|
|
175
|
+
// global last-write-wins alias pool (where it happened to mean `--limit`)
|
|
176
|
+
// instead of `run`'s own `-n, --name`, and the command then failed with
|
|
177
|
+
// "Worker name is required" while `--name audit` worked.
|
|
178
|
+
const scopeChain = [];
|
|
170
179
|
let resolvedCmd;
|
|
171
|
-
let resolvedSub;
|
|
172
180
|
for (const arg of args) {
|
|
173
181
|
if (arg.startsWith('-'))
|
|
174
182
|
continue;
|
|
175
|
-
if (!resolvedCmd
|
|
176
|
-
|
|
183
|
+
if (!resolvedCmd) {
|
|
184
|
+
const top = this.commands.get(arg);
|
|
185
|
+
if (top) {
|
|
186
|
+
resolvedCmd = top;
|
|
187
|
+
scopeChain.push(top);
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
177
190
|
}
|
|
178
|
-
|
|
179
|
-
|
|
191
|
+
const next = resolvedCmd.subcommands?.find(sc => sc.name === arg || sc.aliases?.includes(arg));
|
|
192
|
+
if (next) {
|
|
193
|
+
resolvedCmd = next;
|
|
194
|
+
scopeChain.push(next);
|
|
180
195
|
}
|
|
181
196
|
}
|
|
182
|
-
// Pass 2: Build aliases scoped to the resolved subcommand
|
|
183
|
-
// Subcommand-specific aliases take priority over global ones
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
197
|
+
// Pass 2: Build aliases scoped to the resolved subcommand chain.
|
|
198
|
+
// Subcommand-specific aliases take priority over global ones, and the
|
|
199
|
+
// deepest subcommand wins over its ancestors. The top-level command is
|
|
200
|
+
// excluded when it has a resolved subcommand (preserving the previous
|
|
201
|
+
// `resolvedSub || resolvedCmd` precedence exactly for depth <= 1).
|
|
202
|
+
const scope = scopeChain.length > 1 ? scopeChain.slice(1) : scopeChain;
|
|
203
|
+
const aliases = this.buildScopedAliases(scope);
|
|
204
|
+
const booleanFlags = this.getScopedBooleanFlags(scope);
|
|
205
|
+
const arrayFlags = this.getScopedArrayFlags(scope);
|
|
187
206
|
let i = 0;
|
|
188
207
|
let parsingFlags = true;
|
|
208
|
+
// Once a non-flag token has been seen that is NOT a registered command,
|
|
209
|
+
// no later token may be promoted to "the command". Without this, an
|
|
210
|
+
// unrecognised first token was silently discarded and the SECOND token
|
|
211
|
+
// dispatched instead — `monomind typo status` ran `status`. A mistyped
|
|
212
|
+
// command must never run something the user did not ask for.
|
|
213
|
+
let commandSearchClosed = false;
|
|
189
214
|
while (i < args.length) {
|
|
190
215
|
const arg = args[i];
|
|
191
216
|
// Check for end of flags marker
|
|
@@ -208,7 +233,7 @@ export class CommandParser {
|
|
|
208
233
|
continue;
|
|
209
234
|
}
|
|
210
235
|
// Handle positional arguments
|
|
211
|
-
if (result.command.length === 0 && this.commands.has(arg)) {
|
|
236
|
+
if (result.command.length === 0 && !commandSearchClosed && this.commands.has(arg)) {
|
|
212
237
|
// This is a command
|
|
213
238
|
result.command.push(arg);
|
|
214
239
|
// Check for subcommand (level 1)
|
|
@@ -245,7 +270,11 @@ export class CommandParser {
|
|
|
245
270
|
}
|
|
246
271
|
}
|
|
247
272
|
else {
|
|
248
|
-
// Positional argument
|
|
273
|
+
// Positional argument. If we haven't resolved a command yet, this
|
|
274
|
+
// token was the user's attempt at one — close the search so a later
|
|
275
|
+
// token can't be silently dispatched in its place.
|
|
276
|
+
if (result.command.length === 0)
|
|
277
|
+
commandSearchClosed = true;
|
|
249
278
|
result.positional.push(arg);
|
|
250
279
|
result.flags._.push(arg);
|
|
251
280
|
}
|
|
@@ -444,12 +473,13 @@ export class CommandParser {
|
|
|
444
473
|
* The resolved command's short flags take priority over global ones,
|
|
445
474
|
* fixing collisions where multiple subcommands use the same short flag (e.g. -t).
|
|
446
475
|
*/
|
|
447
|
-
buildScopedAliases(
|
|
476
|
+
buildScopedAliases(scope) {
|
|
448
477
|
// Start with global aliases as base
|
|
449
478
|
const aliases = this.buildAliases();
|
|
450
|
-
// Override with the resolved
|
|
451
|
-
|
|
452
|
-
|
|
479
|
+
// Override with the resolved chain's own options (these take priority);
|
|
480
|
+
// deepest subcommand last, so it wins over its ancestors.
|
|
481
|
+
for (const cmd of CommandParser.asChain(scope)) {
|
|
482
|
+
for (const opt of cmd.options ?? []) {
|
|
453
483
|
if (opt.short) {
|
|
454
484
|
aliases[opt.short] = opt.name;
|
|
455
485
|
}
|
|
@@ -457,13 +487,19 @@ export class CommandParser {
|
|
|
457
487
|
}
|
|
458
488
|
return aliases;
|
|
459
489
|
}
|
|
490
|
+
/** Normalize a single command or a resolved chain into an array. */
|
|
491
|
+
static asChain(scope) {
|
|
492
|
+
if (!scope)
|
|
493
|
+
return [];
|
|
494
|
+
return Array.isArray(scope) ? scope : [scope];
|
|
495
|
+
}
|
|
460
496
|
/**
|
|
461
|
-
* Get boolean flags scoped to a specific command/subcommand.
|
|
497
|
+
* Get boolean flags scoped to a specific command/subcommand chain.
|
|
462
498
|
*/
|
|
463
|
-
getScopedBooleanFlags(
|
|
499
|
+
getScopedBooleanFlags(scope) {
|
|
464
500
|
const flags = this.getBooleanFlags();
|
|
465
|
-
|
|
466
|
-
for (const opt of
|
|
501
|
+
for (const cmd of CommandParser.asChain(scope)) {
|
|
502
|
+
for (const opt of cmd.options ?? []) {
|
|
467
503
|
if (opt.type === 'boolean') {
|
|
468
504
|
flags.add(this.normalizeKey(opt.name));
|
|
469
505
|
}
|
|
@@ -472,12 +508,12 @@ export class CommandParser {
|
|
|
472
508
|
return flags;
|
|
473
509
|
}
|
|
474
510
|
/**
|
|
475
|
-
* Get flags declared `type: 'array'`, scoped to a specific command/subcommand.
|
|
511
|
+
* Get flags declared `type: 'array'`, scoped to a specific command/subcommand chain.
|
|
476
512
|
*/
|
|
477
|
-
getScopedArrayFlags(
|
|
513
|
+
getScopedArrayFlags(scope) {
|
|
478
514
|
const flags = new Set();
|
|
479
|
-
|
|
480
|
-
for (const opt of
|
|
515
|
+
for (const cmd of CommandParser.asChain(scope)) {
|
|
516
|
+
for (const opt of cmd.options ?? []) {
|
|
481
517
|
if (opt.type === 'array') {
|
|
482
518
|
flags.add(this.normalizeKey(opt.name));
|
|
483
519
|
}
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pick the argv prefix used to spawn the embedding worker.
|
|
3
|
+
*
|
|
4
|
+
* dist/: `embed-worker.js` exists → `node embed-worker.js <task>`, byte-identical
|
|
5
|
+
* to the original behaviour, no extra flags, no extra resolution.
|
|
6
|
+
*
|
|
7
|
+
* src/ (tsx / vitest): only `embed-worker.ts` exists. Node's
|
|
8
|
+
* `--experimental-strip-types` cannot run it — the worker imports
|
|
9
|
+
* `./embedder.js`, and type-stripping does no `.js`→`.ts` extension rewriting,
|
|
10
|
+
* so it dies with ERR_MODULE_NOT_FOUND. tsx does rewrite, so the source path
|
|
11
|
+
* runs through tsx's CLI (a devDependency, never needed by dist/).
|
|
12
|
+
*
|
|
13
|
+
* Exported for tests.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveWorkerArgv(): string[];
|
|
1
16
|
type RouteResult = any;
|
|
2
17
|
export interface ConfiguredRouteLayer {
|
|
3
18
|
route: (taskDescription: string) => Promise<RouteResult>;
|