monomind 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -5
- package/packages/@monomind/cli/.claude/commands/mastermind/runorg.md +4 -0
- package/packages/@monomind/cli/.claude/helpers/control-start.cjs +50 -5
- package/packages/@monomind/cli/.claude/helpers/event-logger.cjs +109 -6
- package/packages/@monomind/cli/.claude/helpers/graphify-freshen.cjs +19 -17
- package/packages/@monomind/cli/.claude/helpers/handlers/agent-start-handler.cjs +60 -28
- package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +75 -18
- package/packages/@monomind/cli/.claude/helpers/handlers/edit-handler.cjs +27 -9
- package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +16 -7
- package/packages/@monomind/cli/.claude/helpers/handlers/loops-status-handler.cjs +1 -1
- package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +4 -4
- package/packages/@monomind/cli/.claude/helpers/handlers/session-handler.cjs +40 -22
- package/packages/@monomind/cli/.claude/helpers/handlers/session-restore-handler.cjs +0 -12
- package/packages/@monomind/cli/.claude/helpers/handlers/task-handler.cjs +53 -22
- package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +54 -4
- package/packages/@monomind/cli/.claude/helpers/intelligence.cjs +94 -15
- package/packages/@monomind/cli/.claude/helpers/session.cjs +6 -1
- package/packages/@monomind/cli/.claude/helpers/statusline.cjs +10 -10
- package/packages/@monomind/cli/.claude/helpers/token-tracker.cjs +3 -3
- package/packages/@monomind/cli/.claude/helpers/utils/agent-registrations.cjs +41 -0
- package/packages/@monomind/cli/.claude/helpers/utils/fs-helpers.cjs +183 -0
- package/packages/@monomind/cli/.claude/helpers/utils/micro-agents.cjs +10 -6
- package/packages/@monomind/cli/.claude/helpers/utils/telemetry.cjs +35 -19
- package/packages/@monomind/cli/bin/cli.js +10 -1
- package/packages/@monomind/cli/dist/src/commands/agent-ops.js +9 -15
- package/packages/@monomind/cli/dist/src/commands/cleanup.js +20 -4
- package/packages/@monomind/cli/dist/src/commands/completions.js +66 -261
- package/packages/@monomind/cli/dist/src/commands/doctor.js +7 -2
- package/packages/@monomind/cli/dist/src/commands/hooks-workers.js +7 -0
- package/packages/@monomind/cli/dist/src/commands/index.js +3 -0
- package/packages/@monomind/cli/dist/src/commands/mcp.js +11 -6
- package/packages/@monomind/cli/dist/src/commands/memory-admin.js +12 -6
- package/packages/@monomind/cli/dist/src/commands/memory-transfer.js +6 -13
- package/packages/@monomind/cli/dist/src/commands/monograph.js +3 -3
- package/packages/@monomind/cli/dist/src/commands/org.d.ts +3 -1
- package/packages/@monomind/cli/dist/src/commands/org.js +306 -129
- package/packages/@monomind/cli/dist/src/commands/start.js +142 -37
- package/packages/@monomind/cli/dist/src/commands/tokens.js +21 -3
- package/packages/@monomind/cli/dist/src/config-adapter.js +37 -17
- package/packages/@monomind/cli/dist/src/index.js +64 -28
- package/packages/@monomind/cli/dist/src/init/claudemd-generator.js +44 -11
- package/packages/@monomind/cli/dist/src/init/executor.js +63 -49
- package/packages/@monomind/cli/dist/src/mcp-client.d.ts +8 -3
- package/packages/@monomind/cli/dist/src/mcp-client.js +37 -3
- package/packages/@monomind/cli/dist/src/mcp-server.js +8 -2
- package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.d.ts +20 -0
- package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.js +10 -5
- package/packages/@monomind/cli/dist/src/mcp-tools/github-tools.js +30 -8
- package/packages/@monomind/cli/dist/src/mcp-tools/graphify-tools.js +11 -2
- package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +51 -34
- package/packages/@monomind/cli/dist/src/mcp-tools/knowledge-tools.js +9 -1
- package/packages/@monomind/cli/dist/src/mcp-tools/monograph-compat.js +56 -14
- package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +65 -43
- package/packages/@monomind/cli/dist/src/mcp-tools/swarm-tools.js +6 -4
- package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +4 -4
- package/packages/@monomind/cli/dist/src/mcp-tools/types.d.ts +20 -0
- package/packages/@monomind/cli/dist/src/mcp-tools/types.js +36 -1
- package/packages/@monomind/cli/dist/src/memory/hnsw-operations.d.ts +13 -1
- package/packages/@monomind/cli/dist/src/memory/hnsw-operations.js +66 -14
- package/packages/@monomind/cli/dist/src/memory/intelligence.js +99 -3
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +7 -0
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +20 -0
- package/packages/@monomind/cli/dist/src/orgrt/broker.d.ts +24 -0
- package/packages/@monomind/cli/dist/src/orgrt/broker.js +68 -0
- package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +30 -0
- package/packages/@monomind/cli/dist/src/orgrt/daemon.js +96 -7
- package/packages/@monomind/cli/dist/src/orgrt/forwarder.d.ts +10 -5
- package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +105 -8
- package/packages/@monomind/cli/dist/src/orgrt/live.html +56 -0
- package/packages/@monomind/cli/dist/src/orgrt/provider.js +1 -0
- package/packages/@monomind/cli/dist/src/orgrt/scheduler.d.ts +13 -0
- package/packages/@monomind/cli/dist/src/orgrt/scheduler.js +48 -0
- package/packages/@monomind/cli/dist/src/orgrt/server.d.ts +8 -0
- package/packages/@monomind/cli/dist/src/orgrt/server.js +72 -0
- package/packages/@monomind/cli/dist/src/orgrt/test-loop.d.ts +12 -0
- package/packages/@monomind/cli/dist/src/orgrt/test-loop.js +108 -0
- package/packages/@monomind/cli/dist/src/output.js +12 -5
- package/packages/@monomind/cli/dist/src/parser.d.ts +32 -0
- package/packages/@monomind/cli/dist/src/parser.js +130 -5
- package/packages/@monomind/cli/dist/src/routing/embed-worker.js +13 -1
- package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +31 -0
- package/packages/@monomind/cli/dist/src/services/config-file-manager.d.ts +21 -0
- package/packages/@monomind/cli/dist/src/services/config-file-manager.js +60 -7
- package/packages/@monomind/cli/dist/src/services/crash-reporter.js +2 -1
- package/packages/@monomind/cli/dist/src/ui/dashboard.html +40 -40
- package/packages/@monomind/cli/dist/src/ui/orgs.html +110 -11
- package/packages/@monomind/cli/dist/src/ui/server.mjs +304 -133
- package/packages/@monomind/cli/dist/src/utils/input-guards.d.ts +9 -0
- package/packages/@monomind/cli/dist/src/utils/input-guards.js +39 -5
- package/packages/@monomind/cli/package.json +6 -2
|
@@ -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
|
-
|
|
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);
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
export declare function registerOrg(name: string, url: string, dir?: string): void;
|
|
9
|
+
/** Remove this process's registration for `name` (best effort). */
|
|
10
|
+
export declare function unregisterOrg(name: string, dir?: string): void;
|
|
11
|
+
/** Find which process (if any) currently hosts org `name`. Null if never registered or the registration is stale (owner crashed without cleanup). */
|
|
12
|
+
export declare function lookupOrg(name: string, dir?: string, staleMs?: number): BrokerEntry | null;
|
|
13
|
+
/** Keeps a broker registration alive with periodic heartbeats until stop() is called. */
|
|
14
|
+
export declare class BrokerLease {
|
|
15
|
+
private name;
|
|
16
|
+
private url;
|
|
17
|
+
private dir;
|
|
18
|
+
private intervalMs;
|
|
19
|
+
private timer;
|
|
20
|
+
constructor(name: string, url: string, dir?: string, intervalMs?: number);
|
|
21
|
+
start(): void;
|
|
22
|
+
stop(): void;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=broker.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
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 } 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
|
+
export function registerOrg(name, url, dir = defaultRegistryDir()) {
|
|
20
|
+
mkdirSync(dir, { recursive: true });
|
|
21
|
+
const entry = { url, pid: process.pid, updatedAt: Date.now() };
|
|
22
|
+
writeFileSync(entryPath(name, dir), JSON.stringify(entry));
|
|
23
|
+
}
|
|
24
|
+
/** Remove this process's registration for `name` (best effort). */
|
|
25
|
+
export function unregisterOrg(name, dir = defaultRegistryDir()) {
|
|
26
|
+
try {
|
|
27
|
+
unlinkSync(entryPath(name, dir));
|
|
28
|
+
}
|
|
29
|
+
catch { /* already gone */ }
|
|
30
|
+
}
|
|
31
|
+
/** Find which process (if any) currently hosts org `name`. Null if never registered or the registration is stale (owner crashed without cleanup). */
|
|
32
|
+
export function lookupOrg(name, dir = defaultRegistryDir(), staleMs = DEFAULT_STALE_MS) {
|
|
33
|
+
try {
|
|
34
|
+
const entry = JSON.parse(readFileSync(entryPath(name, dir), 'utf8'));
|
|
35
|
+
if (Date.now() - entry.updatedAt >= staleMs)
|
|
36
|
+
return null;
|
|
37
|
+
return entry;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Keeps a broker registration alive with periodic heartbeats until stop() is called. */
|
|
44
|
+
export class BrokerLease {
|
|
45
|
+
name;
|
|
46
|
+
url;
|
|
47
|
+
dir;
|
|
48
|
+
intervalMs;
|
|
49
|
+
timer = null;
|
|
50
|
+
constructor(name, url, dir = defaultRegistryDir(), intervalMs = 20_000) {
|
|
51
|
+
this.name = name;
|
|
52
|
+
this.url = url;
|
|
53
|
+
this.dir = dir;
|
|
54
|
+
this.intervalMs = intervalMs;
|
|
55
|
+
}
|
|
56
|
+
start() {
|
|
57
|
+
registerOrg(this.name, this.url, this.dir);
|
|
58
|
+
this.timer = setInterval(() => registerOrg(this.name, this.url, this.dir), this.intervalMs);
|
|
59
|
+
this.timer.unref?.();
|
|
60
|
+
}
|
|
61
|
+
stop() {
|
|
62
|
+
if (this.timer)
|
|
63
|
+
clearInterval(this.timer);
|
|
64
|
+
this.timer = null;
|
|
65
|
+
unregisterOrg(this.name, this.dir);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=broker.js.map
|
|
@@ -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,13 +22,28 @@ 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;
|
|
22
36
|
}
|
|
23
37
|
export declare class OrgDaemon {
|
|
24
38
|
private root;
|
|
25
39
|
private opts;
|
|
26
40
|
private orgs;
|
|
27
41
|
private globalSubscribers;
|
|
42
|
+
private leases;
|
|
43
|
+
private forwarders;
|
|
28
44
|
constructor(root: string, opts?: DaemonOpts);
|
|
45
|
+
/** Publish this daemon's inbox so orgs started AFTER this call register with the broker. */
|
|
46
|
+
setInboxUrl(url: string): void;
|
|
29
47
|
/** subscribe to events from ALL running orgs (dashboard server uses this) */
|
|
30
48
|
subscribe(fn: (e: BusEvent) => void): () => void;
|
|
31
49
|
listOrgs(): RunningOrg[];
|
|
@@ -33,6 +51,18 @@ export declare class OrgDaemon {
|
|
|
33
51
|
startOrg(name: string, taskOverride?: string): Promise<RunningOrg>;
|
|
34
52
|
/** Route a message. to = "role" (same org) or "org:role" (cross-org). Returns a receipt string. */
|
|
35
53
|
deliver(fromOrg: string, fromRole: string, to: string, subject: string, body: string): Promise<string>;
|
|
54
|
+
/** Cross-process leg of deliver(): ask the machine-local broker who hosts targetOrgName, then POST over HTTP. */
|
|
55
|
+
private deliverRemote;
|
|
56
|
+
/** Inbound handler for cross-process delivery — called by the server's POST /api/xdeliver route
|
|
57
|
+
* when ANOTHER process's deliverRemote() reaches this daemon. Pushes straight into the target
|
|
58
|
+
* agent's mailbox; the agent picks it up on its own next turn (see Mailbox — never interrupts). */
|
|
59
|
+
receiveRemote(toOrg: string, toRole: string, fromQualified: string, subject: string, body: string): {
|
|
60
|
+
ok: true;
|
|
61
|
+
receipt: string;
|
|
62
|
+
} | {
|
|
63
|
+
ok: false;
|
|
64
|
+
error: string;
|
|
65
|
+
};
|
|
36
66
|
stopOrg(name: string): Promise<void>;
|
|
37
67
|
stopAll(): Promise<void>;
|
|
38
68
|
private persistState;
|
|
@@ -7,16 +7,21 @@ import { PolicyEngine } from './policy.js';
|
|
|
7
7
|
import { Mailbox } from './mailbox.js';
|
|
8
8
|
import { runAgentSession } from './session.js';
|
|
9
9
|
import { attachForwarder } from './forwarder.js';
|
|
10
|
+
import { BrokerLease, lookupOrg } from './broker.js';
|
|
10
11
|
import { OrgDefSchema, ORG_DIR } from './types.js';
|
|
11
12
|
export class OrgDaemon {
|
|
12
13
|
root;
|
|
13
14
|
opts;
|
|
14
15
|
orgs = new Map();
|
|
15
16
|
globalSubscribers = new Set();
|
|
17
|
+
leases = new Map();
|
|
18
|
+
forwarders = new Map();
|
|
16
19
|
constructor(root, opts = {}) {
|
|
17
20
|
this.root = root;
|
|
18
21
|
this.opts = opts;
|
|
19
22
|
}
|
|
23
|
+
/** Publish this daemon's inbox so orgs started AFTER this call register with the broker. */
|
|
24
|
+
setInboxUrl(url) { this.opts.inboxUrl = url; }
|
|
20
25
|
/** subscribe to events from ALL running orgs (dashboard server uses this) */
|
|
21
26
|
subscribe(fn) {
|
|
22
27
|
this.globalSubscribers.add(fn);
|
|
@@ -29,7 +34,9 @@ export class OrgDaemon {
|
|
|
29
34
|
throw new Error(`org ${name} already running`);
|
|
30
35
|
const defPath = join(this.root, ORG_DIR, `${name}.json`);
|
|
31
36
|
const def = OrgDefSchema.parse(JSON.parse(readFileSync(defPath, 'utf8')));
|
|
32
|
-
|
|
37
|
+
// random suffix: second-precision stamps collide across processes (two CLI
|
|
38
|
+
// invocations in the same second would share a run dir and its bus.jsonl)
|
|
39
|
+
const run = `run-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
33
40
|
const dir = join(this.root, ORG_DIR, name, run);
|
|
34
41
|
mkdirSync(dir, { recursive: true });
|
|
35
42
|
const cwd = join(this.root, ORG_DIR, name, 'workspace');
|
|
@@ -39,35 +46,63 @@ export class OrgDaemon {
|
|
|
39
46
|
bus.subscribe(e => { collected.push(e); for (const fn of this.globalSubscribers)
|
|
40
47
|
fn(e); });
|
|
41
48
|
if (this.opts.forward !== false)
|
|
42
|
-
attachForwarder(bus, this.opts.controlJson ?? join(this.root, '.monomind/control.json'));
|
|
49
|
+
this.forwarders.set(name, attachForwarder(bus, this.opts.controlJson ?? join(this.root, '.monomind/control.json')));
|
|
43
50
|
const running = { def, run, bus, agents: new Map(), busEvents: () => [...collected] };
|
|
44
51
|
this.orgs.set(name, running);
|
|
45
52
|
const perRoleBudget = Math.floor((def.run_config.budget_tokens ?? 1_000_000) / def.roles.length);
|
|
46
53
|
for (const role of def.roles) {
|
|
47
54
|
const mailbox = new Mailbox();
|
|
48
55
|
const policy = new PolicyEngine(role.id, { maxTokens: perRoleBudget, ...(role.policy ?? {}) }, bus, cwd);
|
|
49
|
-
const
|
|
56
|
+
const runtime = { mailbox, policy, status: 'running', done: Promise.resolve() };
|
|
57
|
+
runtime.done = runAgentSession({
|
|
50
58
|
org: name, role, bus, policy, mailbox, cwd, def,
|
|
51
59
|
maxTurns: def.run_config.max_turns_per_message,
|
|
52
60
|
deliver: (from, to, subject, body) => this.deliver(name, from, to, subject, body),
|
|
53
61
|
queryFn: this.opts.queryFn,
|
|
54
|
-
}).
|
|
55
|
-
|
|
62
|
+
}).then(() => { runtime.status = 'ended'; })
|
|
63
|
+
.catch((err) => {
|
|
64
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
65
|
+
runtime.status = 'crashed';
|
|
66
|
+
runtime.error = message;
|
|
67
|
+
// runAgentSession already emits a 'status' event for the raw error; emit an
|
|
68
|
+
// 'audit' event too so dashboards/alerts that filter on actionable failures
|
|
69
|
+
// (not routine status chatter) can surface a dead agent instead of a run that
|
|
70
|
+
// silently never progresses.
|
|
71
|
+
bus.emit({
|
|
72
|
+
type: 'audit', from: role.id,
|
|
73
|
+
msg: `agent "${role.id}" crashed: ${message}`,
|
|
74
|
+
reason: 'agent-session-crash',
|
|
75
|
+
data: { agentId: role.id, error: message },
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
running.agents.set(role.id, runtime);
|
|
56
79
|
}
|
|
57
80
|
const boss = def.roles.find(r => r.type === 'boss' || r.reports_to === null) ?? def.roles[0];
|
|
58
81
|
running.agents.get(boss.id).mailbox.push(`Org "${name}" started (run ${run}).\nGoal: ${taskOverride ?? def.goal}\n` +
|
|
59
82
|
`Coordinate your team via org_send. Report completion by ending your turn.`);
|
|
60
83
|
bus.emit({ type: 'status', msg: `org started (${def.roles.length} agents)`, data: { goal: taskOverride ?? def.goal } });
|
|
61
84
|
this.persistState(name, 'running', run);
|
|
85
|
+
if (this.opts.crossProcess && this.opts.inboxUrl) {
|
|
86
|
+
const lease = new BrokerLease(name, this.opts.inboxUrl, this.opts.brokerDir);
|
|
87
|
+
lease.start();
|
|
88
|
+
this.leases.set(name, lease);
|
|
89
|
+
}
|
|
62
90
|
return running;
|
|
63
91
|
}
|
|
64
92
|
/** Route a message. to = "role" (same org) or "org:role" (cross-org). Returns a receipt string. */
|
|
65
93
|
async deliver(fromOrg, fromRole, to, subject, body) {
|
|
66
|
-
|
|
67
|
-
|
|
94
|
+
let cross = to.includes(':');
|
|
95
|
+
let [targetOrgName, targetRole] = cross ? to.split(':', 2) : [fromOrg, to];
|
|
96
|
+
// "own-org:role" is intra-org — agents often self-prefix; don't tag it xorg
|
|
97
|
+
if (cross && targetOrgName === fromOrg) {
|
|
98
|
+
cross = false;
|
|
99
|
+
to = targetRole;
|
|
100
|
+
}
|
|
68
101
|
const targetOrg = this.orgs.get(targetOrgName);
|
|
69
102
|
const src = this.orgs.get(fromOrg);
|
|
70
103
|
if (!targetOrg || !targetOrg.agents.has(targetRole)) {
|
|
104
|
+
if (cross && this.opts.crossProcess)
|
|
105
|
+
return this.deliverRemote(fromOrg, fromRole, targetOrgName, targetRole, to, subject, body, src);
|
|
71
106
|
src?.bus.emit({ type: 'audit', from: fromRole, to, msg: `undeliverable: ${subject}`, reason: 'unknown recipient' });
|
|
72
107
|
return `ERROR: unknown recipient "${to}" (known: ${[...(targetOrg?.agents.keys() ?? this.orgs.keys())].join(', ')})`;
|
|
73
108
|
}
|
|
@@ -78,15 +113,69 @@ export class OrgDaemon {
|
|
|
78
113
|
targetOrg.agents.get(targetRole).mailbox.push(`[message from ${evt.from}] subject: ${subject}\n\n${body}`);
|
|
79
114
|
return `delivered to ${to}`;
|
|
80
115
|
}
|
|
116
|
+
/** Cross-process leg of deliver(): ask the machine-local broker who hosts targetOrgName, then POST over HTTP. */
|
|
117
|
+
async deliverRemote(fromOrg, fromRole, targetOrgName, targetRole, to, subject, body, src) {
|
|
118
|
+
const remote = lookupOrg(targetOrgName, this.opts.brokerDir);
|
|
119
|
+
if (!remote) {
|
|
120
|
+
src?.bus.emit({ type: 'audit', from: fromRole, to, msg: `undeliverable: ${subject}`, reason: 'unknown recipient' });
|
|
121
|
+
return `ERROR: unknown recipient "${to}" (no local org, and no process on this machine has org "${targetOrgName}" registered)`;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const res = await fetch(`${remote.url}/api/xdeliver`, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
body: JSON.stringify({ fromOrg, fromRole, toOrg: targetOrgName, toRole: targetRole, subject, body }),
|
|
128
|
+
signal: AbortSignal.timeout(10_000),
|
|
129
|
+
});
|
|
130
|
+
const data = await res.json().catch(() => ({}));
|
|
131
|
+
if (res.ok && data.ok) {
|
|
132
|
+
src?.bus.emit({ type: 'xorg', from: `${fromOrg}:${fromRole}`, to, subject, msg: body });
|
|
133
|
+
return data.receipt ?? `delivered to ${to} (remote)`;
|
|
134
|
+
}
|
|
135
|
+
src?.bus.emit({ type: 'audit', from: fromRole, to, msg: `remote delivery rejected: ${data.error ?? res.status}`, reason: 'remote-delivery-rejected' });
|
|
136
|
+
return `ERROR: remote org "${to}" rejected delivery: ${data.error ?? res.status}`;
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
140
|
+
src?.bus.emit({ type: 'audit', from: fromRole, to, msg: `remote delivery failed: ${message}`, reason: 'remote-delivery-failed' });
|
|
141
|
+
return `ERROR: remote org "${targetOrgName}" unreachable: ${message}`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Inbound handler for cross-process delivery — called by the server's POST /api/xdeliver route
|
|
145
|
+
* when ANOTHER process's deliverRemote() reaches this daemon. Pushes straight into the target
|
|
146
|
+
* agent's mailbox; the agent picks it up on its own next turn (see Mailbox — never interrupts). */
|
|
147
|
+
receiveRemote(toOrg, toRole, fromQualified, subject, body) {
|
|
148
|
+
const org = this.orgs.get(toOrg);
|
|
149
|
+
if (!org)
|
|
150
|
+
return { ok: false, error: `org "${toOrg}" not hosted here` };
|
|
151
|
+
const agent = org.agents.get(toRole);
|
|
152
|
+
if (!agent)
|
|
153
|
+
return { ok: false, error: `role "${toRole}" not found in org "${toOrg}"` };
|
|
154
|
+
org.bus.emit({ type: 'xorg', from: fromQualified, to: `${toOrg}:${toRole}`, subject, msg: body });
|
|
155
|
+
agent.mailbox.push(`[message from ${fromQualified}] subject: ${subject}\n\n${body}`);
|
|
156
|
+
return { ok: true, receipt: `delivered to ${toOrg}:${toRole} (remote)` };
|
|
157
|
+
}
|
|
81
158
|
async stopOrg(name) {
|
|
82
159
|
const org = this.orgs.get(name);
|
|
83
160
|
if (!org)
|
|
84
161
|
return;
|
|
162
|
+
this.leases.get(name)?.stop();
|
|
163
|
+
this.leases.delete(name);
|
|
85
164
|
for (const a of org.agents.values())
|
|
86
165
|
a.mailbox.close();
|
|
87
166
|
await Promise.allSettled([...org.agents.values()].map(a => a.done));
|
|
88
167
|
org.bus.emit({ type: 'status', msg: 'org stopped' });
|
|
89
168
|
await org.bus.flush();
|
|
169
|
+
// the "org stopped" event above triggers the forwarder's final org:complete /
|
|
170
|
+
// session:complete POST — without waiting for it here, the CLI process can exit
|
|
171
|
+
// (and kill the in-flight fetch) before that last event reaches the dashboard,
|
|
172
|
+
// leaving the run stuck showing "running" forever.
|
|
173
|
+
const forwarder = this.forwarders.get(name);
|
|
174
|
+
if (forwarder) {
|
|
175
|
+
await forwarder.settle();
|
|
176
|
+
forwarder.unsubscribe();
|
|
177
|
+
this.forwarders.delete(name);
|
|
178
|
+
}
|
|
90
179
|
this.persistState(name, 'stopped', org.run);
|
|
91
180
|
this.orgs.delete(name);
|
|
92
181
|
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import type { OrgBus } from './bus.js';
|
|
2
|
-
|
|
3
|
-
* Forwards every bus event to the running mastermind control server
|
|
4
|
-
* (dist/src/ui/server.mjs, POST /api/mastermind/event) so the existing
|
|
5
|
-
* dashboard SSE stream shows org activity. Best-effort: failures are dropped.
|
|
6
|
-
*/
|
|
2
|
+
import type { BusEvent } from './types.js';
|
|
7
3
|
export declare function attachForwarder(bus: OrgBus, controlJsonPath?: string): {
|
|
8
4
|
settle: () => Promise<void>;
|
|
9
5
|
unsubscribe: () => void;
|
|
10
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* Companion dashboard events a single bus event needs beyond its primary
|
|
9
|
+
* translate() mapping. Currently: org-started -> session:start (registers
|
|
10
|
+
* the run in the client's session list) and org-stopped -> session:complete.
|
|
11
|
+
* Exported for tests.
|
|
12
|
+
*/
|
|
13
|
+
export declare function companionEvents(e: BusEvent): Record<string, unknown>[];
|
|
14
|
+
/** BusEvent → dashboard-native mastermind event. Exported for tests. */
|
|
15
|
+
export declare function translate(e: BusEvent): Record<string, unknown>;
|
|
11
16
|
//# sourceMappingURL=forwarder.d.ts.map
|
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
// packages/@monomind/cli/src/orgrt/forwarder.ts
|
|
2
2
|
import { readFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { basename, extname } from 'node:path';
|
|
3
4
|
/**
|
|
4
5
|
* Forwards every bus event to the running mastermind control server
|
|
5
6
|
* (dist/src/ui/server.mjs, POST /api/mastermind/event) so the existing
|
|
6
|
-
* dashboard
|
|
7
|
+
* dashboard shows org activity. Best-effort: failures are dropped.
|
|
8
|
+
*
|
|
9
|
+
* Events are translated into the dashboard's native vocabulary
|
|
10
|
+
* (org:start / org:comms / org:agent:online|offline / org:artifact /
|
|
11
|
+
* org:checkpoint / org:complete) so the Orgs panel and Chat tab render
|
|
12
|
+
* them; every payload carries org + runId so the server routes it to the
|
|
13
|
+
* run file. Event kinds without a native rendering (tool/usage/audit) are
|
|
14
|
+
* forwarded as raw org:<type> — they still land in the run file and SSE.
|
|
15
|
+
*
|
|
16
|
+
* companionEvents() additionally emits session:start/session:complete: the
|
|
17
|
+
* dashboard's client-side Chat tab only lists a run in its session dropdown
|
|
18
|
+
* once it has seen a session:start for that session id — org:start alone is
|
|
19
|
+
* NOT enough (verified against dist/src/ui/orgs.html's handleMmEvent, which
|
|
20
|
+
* creates the chatSessions entry solely on session:start). Companions are
|
|
21
|
+
* sent before the primary translate() payload for the same bus event so the
|
|
22
|
+
* client-side session record exists before anything tries to append to it.
|
|
23
|
+
*
|
|
24
|
+
* sessionId() below joins org/run with "__", not ":" — server.mjs's
|
|
25
|
+
* per-session persistence (data/sessions/<id>.jsonl + _index.json, the store
|
|
26
|
+
* GET /api/mastermind/sessions reads on page load) validates the session id
|
|
27
|
+
* against /^(?!.*\.\.)[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/, which rejects colons. A
|
|
28
|
+
* colon-joined id silently fails that check — the event still lands in the
|
|
29
|
+
* raw mastermind-events.jsonl log, but the run never persists to the session
|
|
30
|
+
* index, so a fresh dashboard load never lists it (only a client that was
|
|
31
|
+
* already connected via SSE when session:start fired would show it).
|
|
7
32
|
*/
|
|
33
|
+
function sessionId(org, run) { return `${org}__${run}`; }
|
|
8
34
|
export function attachForwarder(bus, controlJsonPath = '.monomind/control.json') {
|
|
9
35
|
let chain = Promise.resolve();
|
|
10
36
|
const baseUrl = () => {
|
|
@@ -18,20 +44,91 @@ export function attachForwarder(bus, controlJsonPath = '.monomind/control.json')
|
|
|
18
44
|
return null;
|
|
19
45
|
}
|
|
20
46
|
};
|
|
47
|
+
const post = async (url, payload) => {
|
|
48
|
+
await fetch(`${url}/api/mastermind/event`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify(payload),
|
|
52
|
+
signal: AbortSignal.timeout(3000),
|
|
53
|
+
}).then(r => { r.body?.cancel(); }).catch(() => { });
|
|
54
|
+
};
|
|
21
55
|
const unsubscribe = bus.subscribe((e) => {
|
|
22
56
|
chain = chain.then(async () => {
|
|
23
57
|
const url = baseUrl();
|
|
24
58
|
if (!url)
|
|
25
59
|
return;
|
|
26
|
-
const payload
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
headers: { 'Content-Type': 'application/json' },
|
|
30
|
-
body: JSON.stringify(payload),
|
|
31
|
-
signal: AbortSignal.timeout(3000),
|
|
32
|
-
}).then(r => { r.body?.cancel(); }).catch(() => { });
|
|
60
|
+
for (const payload of companionEvents(e))
|
|
61
|
+
await post(url, payload);
|
|
62
|
+
await post(url, translate(e));
|
|
33
63
|
}).catch(() => { });
|
|
34
64
|
});
|
|
35
65
|
return { settle: () => chain, unsubscribe };
|
|
36
66
|
}
|
|
67
|
+
const TEXTUAL_EXT = new Set(['.md', '.txt', '.json', '.ts', '.tsx', '.js', '.jsx', '.mjs',
|
|
68
|
+
'.py', '.sh', '.yaml', '.yml', '.html', '.css', '.xml', '.log', '.csv']);
|
|
69
|
+
function guessMimeType(path) {
|
|
70
|
+
if (!path)
|
|
71
|
+
return 'application/octet-stream';
|
|
72
|
+
return TEXTUAL_EXT.has(extname(path).toLowerCase()) ? 'text/plain' : 'application/octet-stream';
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Companion dashboard events a single bus event needs beyond its primary
|
|
76
|
+
* translate() mapping. Currently: org-started -> session:start (registers
|
|
77
|
+
* the run in the client's session list) and org-stopped -> session:complete.
|
|
78
|
+
* Exported for tests.
|
|
79
|
+
*/
|
|
80
|
+
export function companionEvents(e) {
|
|
81
|
+
if (e.type !== 'status')
|
|
82
|
+
return [];
|
|
83
|
+
const base = { session: sessionId(e.org, e.run), org: e.org, ts: e.ts };
|
|
84
|
+
const msg = e.msg ?? '';
|
|
85
|
+
if (msg.startsWith('org started')) {
|
|
86
|
+
const goal = e.data?.goal;
|
|
87
|
+
return [{ ...base, type: 'session:start', prompt: goal && goal.length ? goal : e.org }];
|
|
88
|
+
}
|
|
89
|
+
if (msg === 'org stopped') {
|
|
90
|
+
return [{ ...base, type: 'session:complete', status: 'complete', domains: ['ops'] }];
|
|
91
|
+
}
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
/** BusEvent → dashboard-native mastermind event. Exported for tests. */
|
|
95
|
+
export function translate(e) {
|
|
96
|
+
const base = {
|
|
97
|
+
org: e.org,
|
|
98
|
+
runId: e.run,
|
|
99
|
+
session: sessionId(e.org, e.run),
|
|
100
|
+
domain: 'ops',
|
|
101
|
+
ts: e.ts,
|
|
102
|
+
};
|
|
103
|
+
switch (e.type) {
|
|
104
|
+
case 'chat':
|
|
105
|
+
return { ...base, type: 'org:comms', from: e.from, to: 'all', msg: e.msg };
|
|
106
|
+
case 'message':
|
|
107
|
+
case 'xorg':
|
|
108
|
+
return {
|
|
109
|
+
...base, type: 'org:comms', from: e.from, to: e.to,
|
|
110
|
+
msg: e.subject ? `[${e.subject}] ${e.msg ?? ''}` : e.msg,
|
|
111
|
+
};
|
|
112
|
+
case 'asset':
|
|
113
|
+
return {
|
|
114
|
+
...base, type: 'org:artifact', from: e.from,
|
|
115
|
+
artifact: { label: basename(e.path ?? 'asset'), type: 'file', path: e.path, mimeType: guessMimeType(e.path) },
|
|
116
|
+
};
|
|
117
|
+
case 'status': {
|
|
118
|
+
const msg = e.msg ?? '';
|
|
119
|
+
if (msg.startsWith('org started'))
|
|
120
|
+
return { ...base, type: 'org:start', goal: e.data?.goal ?? '' };
|
|
121
|
+
if (msg === 'org stopped')
|
|
122
|
+
return { ...base, type: 'org:complete' };
|
|
123
|
+
if (msg === 'session starting')
|
|
124
|
+
return { ...base, type: 'org:agent:online', role: e.from, title: e.from, agent_type: e.from };
|
|
125
|
+
if (msg === 'session ended' || msg.startsWith('session error'))
|
|
126
|
+
return { ...base, type: 'org:agent:offline', from: e.from, reason: msg };
|
|
127
|
+
return { ...base, type: 'org:checkpoint', progress: msg, from: e.from };
|
|
128
|
+
}
|
|
129
|
+
default:
|
|
130
|
+
// tool / usage / audit — no native widget; keep raw for run file + SSE
|
|
131
|
+
return { ...e, ...base, type: `org:${e.type}` };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
37
134
|
//# sourceMappingURL=forwarder.js.map
|