monomind 2.0.3 → 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/helpers/control-start.cjs +13 -20
- 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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!-- packages/@monomind/cli/src/orgrt/live.html -->
|
|
2
|
+
<!doctype html>
|
|
3
|
+
<html><head><meta charset="utf-8"><title>org live</title>
|
|
4
|
+
<style>
|
|
5
|
+
:root { color-scheme: dark; }
|
|
6
|
+
body { margin:0; font:13px/1.5 ui-monospace,monospace; background:#0d1117; color:#e6edf3;
|
|
7
|
+
display:grid; grid-template-columns:220px 1fr 1fr; grid-template-rows:auto 1fr 1fr; gap:1px; height:100vh; }
|
|
8
|
+
header { grid-column:1/4; padding:8px 14px; background:#161b22; font-weight:700; }
|
|
9
|
+
section { background:#161b22; overflow:auto; padding:10px; }
|
|
10
|
+
h2 { font-size:11px; text-transform:uppercase; letter-spacing:.1em; color:#7d8590; margin:0 0 8px; }
|
|
11
|
+
#agents { grid-row:2/4; }
|
|
12
|
+
.agent { padding:6px 8px; border-left:3px solid #2f81f7; margin-bottom:6px; background:#0d1117; }
|
|
13
|
+
.agent small { color:#7d8590; display:block; }
|
|
14
|
+
.row { padding:4px 6px; border-bottom:1px solid #21262d; word-break:break-word; }
|
|
15
|
+
.from { color:#2f81f7; font-weight:700; } .to { color:#a371f7; }
|
|
16
|
+
.deny { color:#f85149; } .allow { color:#3fb950; } .xorg { color:#d29922; font-weight:700; }
|
|
17
|
+
.ts { color:#484f58; font-size:11px; margin-right:6px; }
|
|
18
|
+
</style></head>
|
|
19
|
+
<body>
|
|
20
|
+
<header>org live — <span id="conn">connecting…</span></header>
|
|
21
|
+
<section id="agents"><h2>Agents</h2><div id="agents-list"></div></section>
|
|
22
|
+
<section id="chat"><h2>Chats & Messages</h2><div id="chat-list"></div></section>
|
|
23
|
+
<section id="tools"><h2>Tool Activity & Assets</h2><div id="tools-list"></div></section>
|
|
24
|
+
<script>
|
|
25
|
+
const $ = id => document.getElementById(id);
|
|
26
|
+
const fmt = ts => new Date(ts).toLocaleTimeString();
|
|
27
|
+
const add = (listId, html, cap = 500) => {
|
|
28
|
+
const el = document.createElement('div'); el.className = 'row'; el.innerHTML = html;
|
|
29
|
+
const list = $(listId); list.prepend(el);
|
|
30
|
+
while (list.children.length > cap) list.lastChild.remove();
|
|
31
|
+
};
|
|
32
|
+
const esc = s => String(s ?? '').replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c]));
|
|
33
|
+
const decisionClass = d => (d === 'allow' || d === 'deny') ? d : 'unknown';
|
|
34
|
+
function render(e) {
|
|
35
|
+
const t = `<span class="ts">${fmt(e.ts)}</span>`;
|
|
36
|
+
if (e.type === 'chat') add('chat-list', `${t}<span class="from">${esc(e.org)}/${esc(e.from)}</span>: ${esc(e.msg)}`);
|
|
37
|
+
else if (e.type === 'message') add('chat-list', `${t}<span class="from">${esc(e.from)}</span> → <span class="to">${esc(e.to)}</span> [${esc(e.subject||'')}] ${esc(e.msg)}`);
|
|
38
|
+
else if (e.type === 'xorg') add('chat-list', `${t}<span class="xorg">⇄ ${esc(e.from)} → ${esc(e.to)}</span> [${esc(e.subject||'')}] ${esc(e.msg)}`);
|
|
39
|
+
else if (e.type === 'tool') add('tools-list', `${t}<span class="from">${esc(e.from)}</span> ${esc(e.tool)} <span class="${decisionClass(e.decision)}">${esc(e.decision)}</span>${e.reason ? ' — ' + esc(e.reason) : ''}`);
|
|
40
|
+
else if (e.type === 'asset') add('tools-list', `${t}<span class="from">${esc(e.from)}</span> 📄 <b>${esc(e.path)}</b>`);
|
|
41
|
+
else if (e.type === 'status' || e.type === 'usage') add('tools-list', `${t}<i>${esc(e.org)}/${esc(e.from ?? 'org')}: ${esc(e.msg ?? JSON.stringify(e.data))}</i>`);
|
|
42
|
+
}
|
|
43
|
+
async function refreshAgents() {
|
|
44
|
+
const orgs = await fetch('/api/orgs').then(r => r.json()).catch(() => []);
|
|
45
|
+
$('agents-list').innerHTML = orgs.map(o =>
|
|
46
|
+
`<div class="agent"><b>${esc(o.name)}</b><small>${esc(o.run)}</small>` +
|
|
47
|
+
o.agents.map(a => `<div>${a.closed ? '⚫' : '🟢'} ${esc(a.id)} <small>${esc(a.usage)} tok</small></div>`).join('') +
|
|
48
|
+
`</div>`).join('') || '<i>no running orgs</i>';
|
|
49
|
+
}
|
|
50
|
+
const ws = new WebSocket(`ws://${location.host}/ws`);
|
|
51
|
+
ws.onopen = () => { $('conn').textContent = 'live'; refreshAgents(); };
|
|
52
|
+
ws.onclose = () => { $('conn').textContent = 'disconnected'; };
|
|
53
|
+
ws.onmessage = ev => { render(JSON.parse(ev.data)); };
|
|
54
|
+
setInterval(refreshAgents, 5000);
|
|
55
|
+
</script>
|
|
56
|
+
</body></html>
|
|
@@ -23,6 +23,7 @@ export function resolveProviderEnv(cfg, parentEnv = process.env) {
|
|
|
23
23
|
if (!key)
|
|
24
24
|
throw new Error(`provider api-key: env var ${name} is not set`);
|
|
25
25
|
env[KEY_VAR] = key;
|
|
26
|
+
delete env.ANTHROPIC_AUTH_TOKEN; // leftover parent token would override the key in the engine
|
|
26
27
|
break;
|
|
27
28
|
}
|
|
28
29
|
case 'base-url': {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** "15m" | "2h" | "45s" | minutes as number | null → interval ms or null */
|
|
2
|
+
export declare function parseSchedule(s: string | number | null | undefined): number | null;
|
|
3
|
+
/** Fires runFn(name, intervalMs) every intervalMs per org. Real timer loop — no ScheduleWakeup, no prompts. */
|
|
4
|
+
export declare class OrgScheduler {
|
|
5
|
+
private runFn;
|
|
6
|
+
private timers;
|
|
7
|
+
private running;
|
|
8
|
+
constructor(runFn: (name: string, intervalMs: number) => Promise<void>);
|
|
9
|
+
add(name: string, intervalMs: number): void;
|
|
10
|
+
remove(name: string): void;
|
|
11
|
+
stop(): void;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// packages/@monomind/cli/src/orgrt/scheduler.ts
|
|
2
|
+
/** "15m" | "2h" | "45s" | minutes as number | null → interval ms or null */
|
|
3
|
+
export function parseSchedule(s) {
|
|
4
|
+
if (s == null)
|
|
5
|
+
return null;
|
|
6
|
+
if (typeof s === 'number')
|
|
7
|
+
return s * 60_000;
|
|
8
|
+
const m = /^(\d+)\s*(s|m|h)$/.exec(s.trim());
|
|
9
|
+
if (!m)
|
|
10
|
+
return null;
|
|
11
|
+
const n = Number(m[1]);
|
|
12
|
+
return m[2] === 's' ? n * 1000 : m[2] === 'm' ? n * 60_000 : n * 3_600_000;
|
|
13
|
+
}
|
|
14
|
+
/** Fires runFn(name, intervalMs) every intervalMs per org. Real timer loop — no ScheduleWakeup, no prompts. */
|
|
15
|
+
export class OrgScheduler {
|
|
16
|
+
runFn;
|
|
17
|
+
timers = new Map();
|
|
18
|
+
running = new Set();
|
|
19
|
+
constructor(runFn) {
|
|
20
|
+
this.runFn = runFn;
|
|
21
|
+
}
|
|
22
|
+
add(name, intervalMs) {
|
|
23
|
+
this.remove(name);
|
|
24
|
+
this.timers.set(name, setInterval(async () => {
|
|
25
|
+
if (this.running.has(name))
|
|
26
|
+
return; // skip if previous iteration still running
|
|
27
|
+
this.running.add(name);
|
|
28
|
+
try {
|
|
29
|
+
await this.runFn(name, intervalMs);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
console.error(`[org-scheduler] ${name}: scheduled run failed:`, err);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
this.running.delete(name);
|
|
36
|
+
}
|
|
37
|
+
}, intervalMs));
|
|
38
|
+
}
|
|
39
|
+
remove(name) {
|
|
40
|
+
const t = this.timers.get(name);
|
|
41
|
+
if (t)
|
|
42
|
+
clearInterval(t);
|
|
43
|
+
this.timers.delete(name);
|
|
44
|
+
}
|
|
45
|
+
stop() { for (const name of [...this.timers.keys()])
|
|
46
|
+
this.remove(name); }
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OrgDaemon } from './daemon.js';
|
|
2
|
+
export interface OrgServer {
|
|
3
|
+
port: number;
|
|
4
|
+
close: () => void;
|
|
5
|
+
}
|
|
6
|
+
/** Daemon-owned live view: WS fanout of every bus event + tiny REST surface. */
|
|
7
|
+
export declare function startOrgServer(daemon: OrgDaemon, port: number): Promise<OrgServer>;
|
|
8
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// packages/@monomind/cli/src/orgrt/server.ts
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
7
|
+
/** Daemon-owned live view: WS fanout of every bus event + tiny REST surface. */
|
|
8
|
+
export async function startOrgServer(daemon, port) {
|
|
9
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const htmlPath = ['live.html', '../orgrt/live.html']
|
|
11
|
+
.map(p => join(here, p)).find(existsSync) ?? join(here, 'live.html');
|
|
12
|
+
const server = http.createServer((req, res) => {
|
|
13
|
+
const url = req.url ?? '/';
|
|
14
|
+
if (req.method === 'GET' && url === '/') {
|
|
15
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
16
|
+
res.end(readFileSync(htmlPath, 'utf8'));
|
|
17
|
+
}
|
|
18
|
+
else if (req.method === 'GET' && url === '/api/orgs') {
|
|
19
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
20
|
+
res.end(JSON.stringify(daemon.listOrgs().map(o => ({
|
|
21
|
+
name: o.def.name, run: o.run, goal: o.def.goal,
|
|
22
|
+
agents: [...o.agents.entries()].map(([id, a]) => ({
|
|
23
|
+
id, usage: a.policy.usage, closed: a.mailbox.isClosed,
|
|
24
|
+
status: a.status, error: a.error,
|
|
25
|
+
})),
|
|
26
|
+
}))));
|
|
27
|
+
}
|
|
28
|
+
else if (req.method === 'GET' && url.startsWith('/api/history/')) {
|
|
29
|
+
const name = decodeURIComponent(url.slice('/api/history/'.length));
|
|
30
|
+
const org = daemon.getOrg(name);
|
|
31
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
32
|
+
res.end(JSON.stringify(org ? org.busEvents() : []));
|
|
33
|
+
}
|
|
34
|
+
else if (req.method === 'POST' && url === '/api/xdeliver') {
|
|
35
|
+
// inbound cross-process delivery — see OrgDaemon.deliverRemote() on the sending side
|
|
36
|
+
let body = '';
|
|
37
|
+
req.on('data', c => { body += c; });
|
|
38
|
+
req.on('end', () => {
|
|
39
|
+
try {
|
|
40
|
+
const payload = JSON.parse(body || '{}');
|
|
41
|
+
if (!payload.toOrg || !payload.toRole || !payload.fromOrg || !payload.fromRole) {
|
|
42
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
43
|
+
res.end(JSON.stringify({ ok: false, error: 'toOrg, toRole, fromOrg, fromRole are required' }));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const result = daemon.receiveRemote(payload.toOrg, payload.toRole, `${payload.fromOrg}:${payload.fromRole}`, payload.subject ?? '', payload.body ?? '');
|
|
47
|
+
res.writeHead(result.ok ? 200 : 404, { 'Content-Type': 'application/json' });
|
|
48
|
+
res.end(JSON.stringify(result));
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
52
|
+
res.end(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : 'bad request' }));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
res.writeHead(404);
|
|
58
|
+
res.end('not found');
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
const wss = new WebSocketServer({ server, path: '/ws' });
|
|
62
|
+
const unsubscribe = daemon.subscribe(e => {
|
|
63
|
+
const line = JSON.stringify(e);
|
|
64
|
+
for (const c of wss.clients)
|
|
65
|
+
if (c.readyState === WebSocket.OPEN)
|
|
66
|
+
c.send(line);
|
|
67
|
+
});
|
|
68
|
+
await new Promise(r => server.listen(port, r));
|
|
69
|
+
const actual = server.address().port;
|
|
70
|
+
return { port: actual, close: () => { unsubscribe(); wss.close(); server.close(); } };
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface IterationResult {
|
|
2
|
+
checks: Record<string, boolean>;
|
|
3
|
+
events: number;
|
|
4
|
+
}
|
|
5
|
+
export interface LoopReport {
|
|
6
|
+
iterations: IterationResult[];
|
|
7
|
+
failed: number;
|
|
8
|
+
summary: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function runTestLoop(root: string, times: number): Promise<LoopReport>;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=test-loop.d.ts.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// packages/@monomind/cli/src/orgrt/test-loop.ts
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import WebSocket from 'ws';
|
|
5
|
+
import { OrgDaemon } from './daemon.js';
|
|
6
|
+
import { startOrgServer } from './server.js';
|
|
7
|
+
import { OrgBus } from './bus.js';
|
|
8
|
+
import { ORG_DIR } from './types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Scripted fake SDK used by the verification loop (no API cost, deterministic).
|
|
11
|
+
* boss: on kickoff, delegates to coder, then pings the partner org's boss.
|
|
12
|
+
* coder: "writes" a report (Write allowed by policy), attempts Bash (denied), replies to boss.
|
|
13
|
+
* It drives the SAME production code paths via the _orgTest seam:
|
|
14
|
+
* callTool → policy.decide → bus; deliver → daemon.deliver → mailboxes + bus;
|
|
15
|
+
* assistant/result → chat/usage events.
|
|
16
|
+
*/
|
|
17
|
+
const scriptedQuery = (roleId) => ({ prompt, options }) => (async function* () {
|
|
18
|
+
const seam = options._orgTest;
|
|
19
|
+
for await (const m of prompt) {
|
|
20
|
+
const text = String(m.message.content);
|
|
21
|
+
// Trigger ONLY on the daemon kickoff message (starts with `Org "`). A bare
|
|
22
|
+
// includes('started') also matches the xorg body "alpha started its run",
|
|
23
|
+
// making the partner boss re-deliver to itself forever — an unbroken
|
|
24
|
+
// microtask chain that starves the event loop (waitFor's timer never fires).
|
|
25
|
+
if (roleId === 'boss' && text.startsWith('Org "') && text.includes('started')) {
|
|
26
|
+
yield { type: 'assistant', message: { content: [{ type: 'text', text: 'Kicking off: delegating to coder.' }] } };
|
|
27
|
+
await seam.deliver('coder', 'task', 'produce out/report.md');
|
|
28
|
+
await seam.deliver('partner:boss', 'fyi', 'alpha started its run');
|
|
29
|
+
}
|
|
30
|
+
else if (roleId === 'coder') {
|
|
31
|
+
await seam.callTool('Write', { file_path: join(options.cwd, 'out/report.md'), content: '# report' });
|
|
32
|
+
await seam.callTool('Bash', { command: 'echo should-be-denied' }); // policy MUST deny
|
|
33
|
+
yield { type: 'assistant', message: { content: [{ type: 'text', text: 'Report written.' }] } };
|
|
34
|
+
await seam.deliver('boss', 're: task', 'done — out/report.md');
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
yield { type: 'assistant', message: { content: [{ type: 'text', text: `ack: ${text.slice(0, 40)}` }] } };
|
|
38
|
+
}
|
|
39
|
+
yield { type: 'result', subtype: 'success', usage: { input_tokens: 5, output_tokens: 5 } };
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
42
|
+
function writeFixtures(root) {
|
|
43
|
+
const dir = join(root, ORG_DIR);
|
|
44
|
+
mkdirSync(dir, { recursive: true });
|
|
45
|
+
writeFileSync(join(dir, 'alpha.json'), JSON.stringify({
|
|
46
|
+
name: 'alpha', goal: 'produce a report',
|
|
47
|
+
roles: [
|
|
48
|
+
{ id: 'boss', title: 'Boss', type: 'boss', reports_to: null },
|
|
49
|
+
{ id: 'coder', title: 'Coder', type: 'specialist', reports_to: 'boss',
|
|
50
|
+
policy: { denyTools: ['Bash'], fileWrite: ['out/**'] } },
|
|
51
|
+
],
|
|
52
|
+
}));
|
|
53
|
+
writeFileSync(join(dir, 'partner.json'), JSON.stringify({
|
|
54
|
+
name: 'partner', goal: 'receive handoffs',
|
|
55
|
+
roles: [{ id: 'boss', title: 'Boss', type: 'boss', reports_to: null }],
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
async function waitFor(pred, ms = 5000) {
|
|
59
|
+
const t0 = Date.now();
|
|
60
|
+
while (Date.now() - t0 < ms) {
|
|
61
|
+
if (pred())
|
|
62
|
+
return true;
|
|
63
|
+
await new Promise(r => setTimeout(r, 50));
|
|
64
|
+
}
|
|
65
|
+
return pred();
|
|
66
|
+
}
|
|
67
|
+
export async function runTestLoop(root, times) {
|
|
68
|
+
writeFixtures(root);
|
|
69
|
+
const iterations = [];
|
|
70
|
+
for (let i = 0; i < times; i++) {
|
|
71
|
+
const queryFn = (args) => {
|
|
72
|
+
const roleId = /You are agent "([^"]+)"/.exec(args.options.systemPrompt)?.[1] ?? 'unknown';
|
|
73
|
+
return scriptedQuery(roleId)(args);
|
|
74
|
+
};
|
|
75
|
+
const daemon = new OrgDaemon(root, { queryFn: queryFn, forward: false });
|
|
76
|
+
const srv = await startOrgServer(daemon, 0);
|
|
77
|
+
const wsEvents = [];
|
|
78
|
+
const ws = new WebSocket(`ws://127.0.0.1:${srv.port}/ws`);
|
|
79
|
+
ws.on('message', d => wsEvents.push(JSON.parse(d.toString())));
|
|
80
|
+
await new Promise(r => ws.on('open', r));
|
|
81
|
+
const alpha = await daemon.startOrg('alpha');
|
|
82
|
+
await daemon.startOrg('partner');
|
|
83
|
+
await waitFor(() => alpha.busEvents().some(e => e.type === 'message' && e.from === 'coder' && e.to === 'boss'));
|
|
84
|
+
await daemon.stopAll();
|
|
85
|
+
ws.close();
|
|
86
|
+
srv.close();
|
|
87
|
+
const evs = alpha.busEvents();
|
|
88
|
+
const has = (pred) => evs.some(pred);
|
|
89
|
+
const persistedCount = OrgBus.readHistory(join(root, ORG_DIR, 'alpha', alpha.run)).length;
|
|
90
|
+
const checks = {
|
|
91
|
+
chat: has(e => e.type === 'chat'),
|
|
92
|
+
message: has(e => e.type === 'message' && e.from === 'boss' && e.to === 'coder'),
|
|
93
|
+
tool: has(e => e.type === 'tool' && e.decision === 'allow' && e.tool === 'Write'),
|
|
94
|
+
policyDeny: has(e => e.type === 'tool' && e.decision === 'deny' && e.tool === 'Bash'),
|
|
95
|
+
asset: has(e => e.type === 'asset' && (e.path ?? '').endsWith('out/report.md')),
|
|
96
|
+
xorg: has(e => e.type === 'xorg' && e.to === 'partner:boss'),
|
|
97
|
+
usage: has(e => e.type === 'usage'),
|
|
98
|
+
wsDelivery: wsEvents.length > 0 && wsEvents.some(e => e.type === 'chat'),
|
|
99
|
+
persisted: persistedCount === evs.length,
|
|
100
|
+
};
|
|
101
|
+
iterations.push({ checks, events: evs.length });
|
|
102
|
+
}
|
|
103
|
+
const failed = iterations.filter(it => Object.values(it.checks).some(v => !v)).length;
|
|
104
|
+
const summary = `org e2e: ${times - failed}/${times} passed` +
|
|
105
|
+
(failed ? ` — failing checks: ${JSON.stringify(iterations.filter(it => Object.values(it.checks).some(v => !v)).map(it => it.checks))}` : '');
|
|
106
|
+
return { iterations, failed, summary };
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=test-loop.js.map
|
|
@@ -77,9 +77,12 @@ export class OutputFormatter {
|
|
|
77
77
|
// Check for NO_COLOR environment variable
|
|
78
78
|
if (process.env.NO_COLOR !== undefined)
|
|
79
79
|
return false;
|
|
80
|
-
// Check for FORCE_COLOR environment variable
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
// Check for FORCE_COLOR environment variable.
|
|
81
|
+
// Per the supports-color convention, FORCE_COLOR=0 (or "false") forces
|
|
82
|
+
// color OFF; any other defined, non-empty value forces color ON.
|
|
83
|
+
if (process.env.FORCE_COLOR !== undefined) {
|
|
84
|
+
return process.env.FORCE_COLOR !== '0' && process.env.FORCE_COLOR !== 'false';
|
|
85
|
+
}
|
|
83
86
|
// Check if stdout is a TTY
|
|
84
87
|
return process.stdout.isTTY ?? false;
|
|
85
88
|
}
|
|
@@ -149,14 +152,18 @@ export class OutputFormatter {
|
|
|
149
152
|
if (this.verbosity === 'quiet')
|
|
150
153
|
return;
|
|
151
154
|
const icon = this.color('[WARN]', 'yellow', 'bold');
|
|
152
|
-
|
|
155
|
+
// Write to stderr (not stdout) so it never interleaves with structured
|
|
156
|
+
// stdout output (e.g. --format json | jq .)
|
|
157
|
+
this.writeErrorln(`${icon} ${message}`);
|
|
153
158
|
}
|
|
154
159
|
printInfo(message) {
|
|
155
160
|
// Info suppressed in quiet mode
|
|
156
161
|
if (this.verbosity === 'quiet')
|
|
157
162
|
return;
|
|
158
163
|
const icon = this.color('[INFO]', 'blue', 'bold');
|
|
159
|
-
|
|
164
|
+
// Write to stderr (not stdout) so it never interleaves with structured
|
|
165
|
+
// stdout output (e.g. --format json | jq .)
|
|
166
|
+
this.writeErrorln(`${icon} ${message}`);
|
|
160
167
|
}
|
|
161
168
|
printDebug(message) {
|
|
162
169
|
// Debug only shows in verbose/debug mode
|
|
@@ -35,7 +35,35 @@ export declare class CommandParser {
|
|
|
35
35
|
*/
|
|
36
36
|
private static readonly RESERVED_FLAG_KEYS;
|
|
37
37
|
private setFlagSafe;
|
|
38
|
+
/**
|
|
39
|
+
* Merge a single parsed flag (or set of `_` positionals) into the
|
|
40
|
+
* accumulated result flags, collecting repeats into an array instead of
|
|
41
|
+
* overwriting. Declared `type: 'array'` options always end up as an array
|
|
42
|
+
* (even a single occurrence); any other repeated flag also becomes an
|
|
43
|
+
* array on its second occurrence rather than silently dropping the first
|
|
44
|
+
* value.
|
|
45
|
+
*/
|
|
46
|
+
private mergeParsedFlags;
|
|
38
47
|
parse(args: string[]): ParseResult;
|
|
48
|
+
/**
|
|
49
|
+
* Convert a camelCase key to kebab-case (inverse of normalizeKey).
|
|
50
|
+
*/
|
|
51
|
+
private camelToKebab;
|
|
52
|
+
/**
|
|
53
|
+
* Ensure every flag is reachable under both its camelCase and
|
|
54
|
+
* kebab-case spelling. Runs once, after all flags (including
|
|
55
|
+
* defaults) have been merged into the result.
|
|
56
|
+
*/
|
|
57
|
+
private mirrorFlagKeys;
|
|
58
|
+
/**
|
|
59
|
+
* True when `value` looks like a space-separated negative number
|
|
60
|
+
* (`-0.5`, `-42`) rather than a new flag. Scoped narrowly to "leading `-`
|
|
61
|
+
* immediately followed by a digit" — a legitimate flag name never starts
|
|
62
|
+
* with a digit, so this can't misfire on a genuine following flag while
|
|
63
|
+
* still letting `--threshold -0.5` consume `-0.5` as the value instead of
|
|
64
|
+
* being misparsed as a new (bogus) flag.
|
|
65
|
+
*/
|
|
66
|
+
private looksLikeNegativeNumber;
|
|
39
67
|
private parseFlag;
|
|
40
68
|
private parseValue;
|
|
41
69
|
private normalizeKey;
|
|
@@ -50,6 +78,10 @@ export declare class CommandParser {
|
|
|
50
78
|
* Get boolean flags scoped to a specific command/subcommand.
|
|
51
79
|
*/
|
|
52
80
|
private getScopedBooleanFlags;
|
|
81
|
+
/**
|
|
82
|
+
* Get flags declared `type: 'array'`, scoped to a specific command/subcommand.
|
|
83
|
+
*/
|
|
84
|
+
private getScopedArrayFlags;
|
|
53
85
|
private getBooleanFlags;
|
|
54
86
|
private applyDefaults;
|
|
55
87
|
validateFlags(flags: ParsedFlags, command?: Command): string[];
|
|
@@ -72,6 +72,12 @@ export class CommandParser {
|
|
|
72
72
|
description: 'Enable interactive mode',
|
|
73
73
|
type: 'boolean',
|
|
74
74
|
default: true
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'update',
|
|
78
|
+
description: 'Check for updates on startup (use --no-update to disable)',
|
|
79
|
+
type: 'boolean',
|
|
80
|
+
default: true
|
|
75
81
|
}
|
|
76
82
|
];
|
|
77
83
|
}
|
|
@@ -111,6 +117,48 @@ export class CommandParser {
|
|
|
111
117
|
return;
|
|
112
118
|
flags[key] = value;
|
|
113
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Merge a single parsed flag (or set of `_` positionals) into the
|
|
122
|
+
* accumulated result flags, collecting repeats into an array instead of
|
|
123
|
+
* overwriting. Declared `type: 'array'` options always end up as an array
|
|
124
|
+
* (even a single occurrence); any other repeated flag also becomes an
|
|
125
|
+
* array on its second occurrence rather than silently dropping the first
|
|
126
|
+
* value.
|
|
127
|
+
*/
|
|
128
|
+
mergeParsedFlags(into, from, arrayFlags, booleanFlags) {
|
|
129
|
+
for (const key of Object.keys(from)) {
|
|
130
|
+
if (key === '_') {
|
|
131
|
+
into._.push(...from._);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (CommandParser.RESERVED_FLAG_KEYS.has(key))
|
|
135
|
+
continue;
|
|
136
|
+
const incoming = from[key];
|
|
137
|
+
if (Array.isArray(into[key])) {
|
|
138
|
+
into[key].push(...[].concat(incoming).map(String));
|
|
139
|
+
}
|
|
140
|
+
else if (into[key] !== undefined && booleanFlags?.has(key)) {
|
|
141
|
+
// Declared boolean flag repeated (e.g. `--dry-run --dry-run`) — last
|
|
142
|
+
// value wins instead of promoting to an array. Callers throughout the
|
|
143
|
+
// codebase check booleans with `flags['x'] === true`; an array value
|
|
144
|
+
// would silently fail that check (e.g. guidance.ts's --dry-run,
|
|
145
|
+
// which guards a destructive write).
|
|
146
|
+
into[key] = incoming;
|
|
147
|
+
}
|
|
148
|
+
else if (into[key] !== undefined) {
|
|
149
|
+
// Repeated flag not previously an array — promote to array so the
|
|
150
|
+
// earlier value isn't lost.
|
|
151
|
+
into[key] = [String(into[key]), String(incoming)];
|
|
152
|
+
}
|
|
153
|
+
else if (arrayFlags.has(key)) {
|
|
154
|
+
// First occurrence of a declared array flag — still wrap in an array.
|
|
155
|
+
into[key] = [String(incoming)];
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
into[key] = incoming;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
114
162
|
parse(args) {
|
|
115
163
|
const result = {
|
|
116
164
|
command: [],
|
|
@@ -135,6 +183,7 @@ export class CommandParser {
|
|
|
135
183
|
// Subcommand-specific aliases take priority over global ones
|
|
136
184
|
const aliases = this.buildScopedAliases(resolvedSub || resolvedCmd);
|
|
137
185
|
const booleanFlags = this.getScopedBooleanFlags(resolvedSub || resolvedCmd);
|
|
186
|
+
const arrayFlags = this.getScopedArrayFlags(resolvedSub || resolvedCmd);
|
|
138
187
|
let i = 0;
|
|
139
188
|
let parsingFlags = true;
|
|
140
189
|
while (i < args.length) {
|
|
@@ -148,8 +197,13 @@ export class CommandParser {
|
|
|
148
197
|
// Handle flags
|
|
149
198
|
if (parsingFlags && arg.startsWith('-')) {
|
|
150
199
|
const parseResult = this.parseFlag(args, i, aliases, booleanFlags);
|
|
151
|
-
//
|
|
152
|
-
|
|
200
|
+
// Merge into result flags. A plain Object.assign here would silently
|
|
201
|
+
// drop earlier values whenever the same flag is passed more than once
|
|
202
|
+
// (`-p a=1 -p b=2` kept only `b=2`) — options declared `type: 'array'`
|
|
203
|
+
// (e.g. browse-action.ts's --params/-p, swarm.ts) are meant to
|
|
204
|
+
// collect every occurrence, and even a flag NOT declared array
|
|
205
|
+
// shouldn't silently lose data on repetition.
|
|
206
|
+
this.mergeParsedFlags(result.flags, parseResult.flags, arrayFlags, booleanFlags);
|
|
153
207
|
i = parseResult.nextIndex;
|
|
154
208
|
continue;
|
|
155
209
|
}
|
|
@@ -201,8 +255,55 @@ export class CommandParser {
|
|
|
201
255
|
// shadow same-name global options (e.g. `browse screenshot --format` is an
|
|
202
256
|
// image format, not the global text|json|table output format).
|
|
203
257
|
this.applyDefaults(result.flags, resolvedCmd);
|
|
258
|
+
// Dual-write flag keys: every flag ends up stored under BOTH its
|
|
259
|
+
// camelCase form (`keepConfig`) and its original kebab-case form
|
|
260
|
+
// (`keep-config`). Historically only the camelCase key was ever set,
|
|
261
|
+
// but dozens of command action functions across the codebase read
|
|
262
|
+
// `ctx.flags['kebab-case']` directly — which was always `undefined`,
|
|
263
|
+
// silently disabling those flags (see AUDIT-BACKLOG P0-10). Running
|
|
264
|
+
// this as a single pass over the fully-merged flags object (long
|
|
265
|
+
// flags, `--no-x` negation, short-flag aliases, and defaults are all
|
|
266
|
+
// merged into `result.flags` by this point) covers every entry point
|
|
267
|
+
// in one place instead of touching each call site individually.
|
|
268
|
+
this.mirrorFlagKeys(result.flags);
|
|
204
269
|
return result;
|
|
205
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Convert a camelCase key to kebab-case (inverse of normalizeKey).
|
|
273
|
+
*/
|
|
274
|
+
camelToKebab(key) {
|
|
275
|
+
return key.replace(/[A-Z]/g, (letter) => '-' + letter.toLowerCase());
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Ensure every flag is reachable under both its camelCase and
|
|
279
|
+
* kebab-case spelling. Runs once, after all flags (including
|
|
280
|
+
* defaults) have been merged into the result.
|
|
281
|
+
*/
|
|
282
|
+
mirrorFlagKeys(flags) {
|
|
283
|
+
for (const key of Object.keys(flags)) {
|
|
284
|
+
if (key === '_' || CommandParser.RESERVED_FLAG_KEYS.has(key))
|
|
285
|
+
continue;
|
|
286
|
+
const camelKey = this.normalizeKey(key);
|
|
287
|
+
if (camelKey !== key && flags[camelKey] === undefined && !CommandParser.RESERVED_FLAG_KEYS.has(camelKey)) {
|
|
288
|
+
flags[camelKey] = flags[key];
|
|
289
|
+
}
|
|
290
|
+
const kebabKey = this.camelToKebab(key);
|
|
291
|
+
if (kebabKey !== key && flags[kebabKey] === undefined && !CommandParser.RESERVED_FLAG_KEYS.has(kebabKey)) {
|
|
292
|
+
flags[kebabKey] = flags[key];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* True when `value` looks like a space-separated negative number
|
|
298
|
+
* (`-0.5`, `-42`) rather than a new flag. Scoped narrowly to "leading `-`
|
|
299
|
+
* immediately followed by a digit" — a legitimate flag name never starts
|
|
300
|
+
* with a digit, so this can't misfire on a genuine following flag while
|
|
301
|
+
* still letting `--threshold -0.5` consume `-0.5` as the value instead of
|
|
302
|
+
* being misparsed as a new (bogus) flag.
|
|
303
|
+
*/
|
|
304
|
+
looksLikeNegativeNumber(value) {
|
|
305
|
+
return /^-\d/.test(value);
|
|
306
|
+
}
|
|
206
307
|
parseFlag(args, index, aliases, booleanFlags) {
|
|
207
308
|
const flags = { _: [] };
|
|
208
309
|
const arg = args[index];
|
|
@@ -239,7 +340,8 @@ export class CommandParser {
|
|
|
239
340
|
if (booleanFlags.has(normalizedKey)) {
|
|
240
341
|
this.setFlagSafe(flags, normalizedKey, true);
|
|
241
342
|
}
|
|
242
|
-
else if (nextIndex < args.length &&
|
|
343
|
+
else if (nextIndex < args.length &&
|
|
344
|
+
(!args[nextIndex].startsWith('-') || this.looksLikeNegativeNumber(args[nextIndex]))) {
|
|
243
345
|
this.setFlagSafe(flags, normalizedKey, this.parseValue(args[nextIndex]));
|
|
244
346
|
nextIndex++;
|
|
245
347
|
}
|
|
@@ -258,7 +360,8 @@ export class CommandParser {
|
|
|
258
360
|
if (booleanFlags.has(normalizedKey)) {
|
|
259
361
|
this.setFlagSafe(flags, normalizedKey, true);
|
|
260
362
|
}
|
|
261
|
-
else if (nextIndex < args.length &&
|
|
363
|
+
else if (nextIndex < args.length &&
|
|
364
|
+
(!args[nextIndex].startsWith('-') || this.looksLikeNegativeNumber(args[nextIndex]))) {
|
|
262
365
|
this.setFlagSafe(flags, normalizedKey, this.parseValue(args[nextIndex]));
|
|
263
366
|
nextIndex++;
|
|
264
367
|
}
|
|
@@ -368,6 +471,20 @@ export class CommandParser {
|
|
|
368
471
|
}
|
|
369
472
|
return flags;
|
|
370
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Get flags declared `type: 'array'`, scoped to a specific command/subcommand.
|
|
476
|
+
*/
|
|
477
|
+
getScopedArrayFlags(resolvedCmd) {
|
|
478
|
+
const flags = new Set();
|
|
479
|
+
if (resolvedCmd?.options) {
|
|
480
|
+
for (const opt of resolvedCmd.options) {
|
|
481
|
+
if (opt.type === 'array') {
|
|
482
|
+
flags.add(this.normalizeKey(opt.name));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return flags;
|
|
487
|
+
}
|
|
371
488
|
getBooleanFlags() {
|
|
372
489
|
const flags = new Set();
|
|
373
490
|
for (const opt of this.globalOptions) {
|
|
@@ -471,7 +588,15 @@ export class CommandParser {
|
|
|
471
588
|
}
|
|
472
589
|
// Check for unknown flags if not allowed
|
|
473
590
|
if (!this.options.allowUnknownFlags) {
|
|
474
|
-
|
|
591
|
+
// Include both the camelCase and original (kebab-case) spelling of
|
|
592
|
+
// each option name — parse() now mirrors flags under both forms, so
|
|
593
|
+
// validation must recognize both or the mirrored key would be
|
|
594
|
+
// flagged as an unknown option.
|
|
595
|
+
const knownFlags = new Set();
|
|
596
|
+
for (const opt of allOptions) {
|
|
597
|
+
knownFlags.add(this.normalizeKey(opt.name));
|
|
598
|
+
knownFlags.add(opt.name);
|
|
599
|
+
}
|
|
475
600
|
knownFlags.add('_'); // Positional args
|
|
476
601
|
for (const key of Object.keys(flags)) {
|
|
477
602
|
if (!knownFlags.has(key) && key !== '_') {
|
|
@@ -50,7 +50,19 @@ async function main() {
|
|
|
50
50
|
const result = await layer.route(task);
|
|
51
51
|
// Emit on its own line, prefixed, so the parent can unambiguously locate the
|
|
52
52
|
// result even if the model loader wrote stray bytes to stdout earlier.
|
|
53
|
-
|
|
53
|
+
//
|
|
54
|
+
// Fix (P1-3): onnxruntime's native thread pool keeps the event loop alive
|
|
55
|
+
// even after this function returns, so the process never exits on its own
|
|
56
|
+
// and the parent's `close` listener waits until it force-kills us at the
|
|
57
|
+
// timeout — discarding the result we already wrote. Force-exit once the
|
|
58
|
+
// write is flushed (same onnx-teardown class as memory.ts's #1428 fix).
|
|
59
|
+
const wroteSynchronously = process.stdout.write(`\n__ROUTE_RESULT__${JSON.stringify(result)}\n`);
|
|
60
|
+
if (wroteSynchronously) {
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
process.stdout.once('drain', () => process.exit(0));
|
|
65
|
+
}
|
|
54
66
|
}
|
|
55
67
|
main().catch((err) => {
|
|
56
68
|
process.stderr.write(`embed-worker: ${err instanceof Error ? err.message : String(err)}\n`);
|