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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "monomind",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Monomind - Power toolkit for Claude Code. Autonomous agent orgs, codebase knowledge graph, keyword-routed specialist prompts, and 80+ slash commands. One init, then walk away.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -60,15 +60,20 @@
|
|
|
60
60
|
"v1:security": "npm run security:audit && npm run security:test"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.207",
|
|
63
64
|
"@monoes/monobrowse": "^1.0.3",
|
|
64
65
|
"@monoes/monograph": "^1.4.0",
|
|
66
|
+
"@noble/ed25519": "^2.1.0",
|
|
65
67
|
"mammoth": "^1.12.0",
|
|
66
68
|
"pdf-parse": "^2.4.5",
|
|
67
|
-
"semver": "^7.6.0"
|
|
69
|
+
"semver": "^7.6.0",
|
|
70
|
+
"ws": "^8.21.0",
|
|
71
|
+
"yaml": "^2.8.0",
|
|
72
|
+
"zod": "^3.25.0"
|
|
68
73
|
},
|
|
69
74
|
"overrides": {
|
|
70
75
|
"hono": ">=4.12.21",
|
|
71
|
-
"ws": "
|
|
76
|
+
"ws": "$ws",
|
|
72
77
|
"axios": ">=1.16.0",
|
|
73
78
|
"qs": ">=6.15.2",
|
|
74
79
|
"esbuild": ">=0.28.1",
|
|
@@ -137,8 +142,7 @@
|
|
|
137
142
|
"tsx": "^4.21.0",
|
|
138
143
|
"typescript": "^5.0.0",
|
|
139
144
|
"vite": ">=8.0.16",
|
|
140
|
-
"vitest": "^4.1.4"
|
|
141
|
-
"zod": "^4.3.6"
|
|
145
|
+
"vitest": "^4.1.4"
|
|
142
146
|
},
|
|
143
147
|
"engines": {
|
|
144
148
|
"node": ">=20.0.0"
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const { spawn } = require('child_process');
|
|
15
|
+
const { claimLock, releaseLock } = require('./utils/fs-helpers.cjs');
|
|
15
16
|
|
|
16
17
|
const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
17
18
|
const STATUS_FILE = path.join(CWD, '.monomind', 'control.json');
|
|
@@ -111,31 +112,23 @@ const LOCK_FILE = path.join(CWD, '.monomind', 'control.lock');
|
|
|
111
112
|
* Atomically claim the spawn lock. Concurrent hook events (a busy session can
|
|
112
113
|
* fire dozens of control-starts at once) must not each spawn a server — the
|
|
113
114
|
* loser processes exit and let the winner's server come up. A stale lock
|
|
114
|
-
* (
|
|
115
|
+
* (older than 30s) is broken and re-claimed.
|
|
116
|
+
*
|
|
117
|
+
* P2-25: previously this broke a stale lock via unlink-then-writeFileSync(wx),
|
|
118
|
+
* which is a TOCTOU race — a second process that also decided the lock was
|
|
119
|
+
* stale could unlink the FIRST process's freshly-claimed (non-stale) lock
|
|
120
|
+
* between that process's unlink and its own write, letting a third process
|
|
121
|
+
* then also claim it. `claimLock` (utils/fs-helpers.cjs) fixes this by
|
|
122
|
+
* breaking stale locks with an atomic rename-to-claim instead: only one
|
|
123
|
+
* racing process can win the rename, and a loser retries from the top
|
|
124
|
+
* rather than proceeding as if it owns the lock.
|
|
115
125
|
*/
|
|
116
126
|
function claimSpawnLock() {
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
fs.writeFileSync(LOCK_FILE, String(process.pid), { flag: 'wx' });
|
|
120
|
-
return true;
|
|
121
|
-
} catch {
|
|
122
|
-
try {
|
|
123
|
-
const stat = fs.statSync(LOCK_FILE);
|
|
124
|
-
const holder = Number(fs.readFileSync(LOCK_FILE, 'utf-8'));
|
|
125
|
-
if (Date.now() - stat.mtimeMs < 30_000 && isPidAlive(holder)) return false;
|
|
126
|
-
fs.unlinkSync(LOCK_FILE);
|
|
127
|
-
fs.writeFileSync(LOCK_FILE, String(process.pid), { flag: 'wx' });
|
|
128
|
-
return true;
|
|
129
|
-
} catch {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
127
|
+
return claimLock(LOCK_FILE, 30_000);
|
|
133
128
|
}
|
|
134
129
|
|
|
135
130
|
function releaseSpawnLock() {
|
|
136
|
-
|
|
137
|
-
if (Number(fs.readFileSync(LOCK_FILE, 'utf-8')) === process.pid) fs.unlinkSync(LOCK_FILE);
|
|
138
|
-
} catch { /* ignore */ }
|
|
131
|
+
releaseLock(LOCK_FILE);
|
|
139
132
|
}
|
|
140
133
|
|
|
141
134
|
async function main() {
|
|
@@ -22,15 +22,36 @@ safety.unref();
|
|
|
22
22
|
|
|
23
23
|
const MAX_STDIN = 1024 * 1024; // 1 MiB
|
|
24
24
|
|
|
25
|
+
// P3-16: mirrors hook-handler.cjs's readStdin — byte-counted, discard-whole-
|
|
26
|
+
// payload-on-overflow. The old version did `if (data.length < MAX_STDIN)
|
|
27
|
+
// data += c`, which silently DROPPED characters once the cap was hit instead
|
|
28
|
+
// of aborting the read, so an oversized payload produced truncated, invalid
|
|
29
|
+
// JSON that then failed JSON.parse downstream with the whole event (and its
|
|
30
|
+
// toolName/sessionId attribution) discarded anyway — just with extra steps
|
|
31
|
+
// and no signal as to why. Discarding the entire payload up front (with a
|
|
32
|
+
// logged, flagged event) is equivalent in outcome but honest about it.
|
|
25
33
|
function readStdin() {
|
|
26
34
|
if (process.stdin.isTTY) return Promise.resolve('');
|
|
27
35
|
return new Promise((resolve) => {
|
|
28
36
|
let data = '';
|
|
29
|
-
|
|
37
|
+
let byteCount = 0;
|
|
38
|
+
let truncated = false;
|
|
39
|
+
const t = setTimeout(() => { process.stdin.pause(); resolve({ data, truncated }); }, 800);
|
|
30
40
|
process.stdin.setEncoding('utf8');
|
|
31
|
-
process.stdin.on('data', c => {
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
process.stdin.on('data', c => {
|
|
42
|
+
if (truncated) return;
|
|
43
|
+
byteCount += Buffer.byteLength(c, 'utf8');
|
|
44
|
+
if (byteCount > MAX_STDIN) {
|
|
45
|
+
truncated = true;
|
|
46
|
+
process.stdin.pause();
|
|
47
|
+
clearTimeout(t);
|
|
48
|
+
resolve({ data: '', truncated: true }); // discard oversized input, same as hook-handler.cjs
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
data += c;
|
|
52
|
+
});
|
|
53
|
+
process.stdin.on('end', () => { clearTimeout(t); resolve({ data, truncated }); });
|
|
54
|
+
process.stdin.on('error', () => { clearTimeout(t); resolve({ data, truncated }); });
|
|
34
55
|
process.stdin.resume();
|
|
35
56
|
});
|
|
36
57
|
}
|
|
@@ -60,7 +81,14 @@ function forwardToDashboard(entry) {
|
|
|
60
81
|
}
|
|
61
82
|
|
|
62
83
|
async function main() {
|
|
63
|
-
const
|
|
84
|
+
const stdinResult = await readStdin();
|
|
85
|
+
const raw = stdinResult.data;
|
|
86
|
+
const stdinTruncated = stdinResult.truncated;
|
|
87
|
+
if (stdinTruncated) {
|
|
88
|
+
// Oversized payload was discarded whole (see readStdin) — log it so the
|
|
89
|
+
// drop is visible instead of silently vanishing as a JSON.parse failure.
|
|
90
|
+
console.error('[event-logger] stdin payload exceeded ' + MAX_STDIN + ' bytes; discarded (event=' + eventType + ')');
|
|
91
|
+
}
|
|
64
92
|
let payload = {};
|
|
65
93
|
try { payload = JSON.parse(raw); } catch {}
|
|
66
94
|
|
|
@@ -80,6 +108,7 @@ async function main() {
|
|
|
80
108
|
toolResult: payload.toolResult || payload.tool_result || undefined,
|
|
81
109
|
message: payload.message || payload.notification || payload.prompt || undefined,
|
|
82
110
|
raw: (raw.length < 4096) ? payload : undefined,
|
|
111
|
+
truncated: stdinTruncated || undefined,
|
|
83
112
|
};
|
|
84
113
|
|
|
85
114
|
// Remove undefined keys
|
|
@@ -113,6 +142,13 @@ async function main() {
|
|
|
113
142
|
fs.appendFileSync(allLog, JSON.stringify(entry) + '\n');
|
|
114
143
|
} catch {}
|
|
115
144
|
|
|
145
|
+
// Rotation — event logs (daily + per-session) accumulate forever with no
|
|
146
|
+
// cleanup otherwise. Cheap: single readdir + stat pass, only on session-start
|
|
147
|
+
// (once per session, not per-event) so normal hook latency is unaffected.
|
|
148
|
+
if (eventType === 'session-start') {
|
|
149
|
+
rotateEventLogs(eventsDir);
|
|
150
|
+
}
|
|
151
|
+
|
|
116
152
|
// Write to per-session log
|
|
117
153
|
if (sessionId !== 'unknown') {
|
|
118
154
|
try {
|
|
@@ -148,8 +184,75 @@ async function main() {
|
|
|
148
184
|
forwardToDashboard(entry);
|
|
149
185
|
}
|
|
150
186
|
|
|
187
|
+
// Detect route changes and worker metric updates by mtime and forward them too.
|
|
188
|
+
// These aren't Claude Code hook events — they're file writes from route-handler.cjs
|
|
189
|
+
// and the @monomind/hooks workers respectively — so we poll cheaply on every hook invocation.
|
|
190
|
+
forwardFileChanges(monoDir, CWD);
|
|
191
|
+
|
|
151
192
|
clearTimeout(safety);
|
|
152
193
|
process.exit(0);
|
|
153
194
|
}
|
|
154
195
|
|
|
155
|
-
|
|
196
|
+
function forwardFileChanges(monoDir, workDir) {
|
|
197
|
+
try {
|
|
198
|
+
const cachePath = path.join(monoDir, 'dashboard-watch-cache.json');
|
|
199
|
+
let cache = {};
|
|
200
|
+
try { cache = JSON.parse(fs.readFileSync(cachePath, 'utf8')); } catch {}
|
|
201
|
+
let changed = false;
|
|
202
|
+
|
|
203
|
+
// Route changes (.monomind/last-route.json, written every prompt by route-handler.cjs)
|
|
204
|
+
const routePath = path.join(workDir, '.monomind', 'last-route.json');
|
|
205
|
+
try {
|
|
206
|
+
const routeMtime = fs.statSync(routePath).mtimeMs;
|
|
207
|
+
if (routeMtime !== cache.routeMtime) {
|
|
208
|
+
cache.routeMtime = routeMtime;
|
|
209
|
+
changed = true;
|
|
210
|
+
let route = null;
|
|
211
|
+
try { route = JSON.parse(fs.readFileSync(routePath, 'utf8')); } catch {}
|
|
212
|
+
forwardToDashboard({ hookType: 'route-change', ts: Date.now(), route });
|
|
213
|
+
}
|
|
214
|
+
} catch {}
|
|
215
|
+
|
|
216
|
+
// Worker-complete: newest mtime among .monomind/metrics/*.json (written by @monomind/hooks workers)
|
|
217
|
+
const metricsDir = path.join(workDir, '.monomind', 'metrics');
|
|
218
|
+
try {
|
|
219
|
+
const files = fs.readdirSync(metricsDir).filter(f => f.endsWith('.json') && !f.startsWith('.'));
|
|
220
|
+
let latestFile = null;
|
|
221
|
+
let latestMtime = cache.metricsMtime || 0;
|
|
222
|
+
for (const f of files) {
|
|
223
|
+
const mtime = fs.statSync(path.join(metricsDir, f)).mtimeMs;
|
|
224
|
+
if (mtime > latestMtime) { latestMtime = mtime; latestFile = f; }
|
|
225
|
+
}
|
|
226
|
+
if (latestFile) {
|
|
227
|
+
cache.metricsMtime = latestMtime;
|
|
228
|
+
changed = true;
|
|
229
|
+
forwardToDashboard({ hookType: 'daemon-complete', ts: Date.now(), metric: latestFile.replace(/\.json$/, '') });
|
|
230
|
+
}
|
|
231
|
+
} catch {}
|
|
232
|
+
|
|
233
|
+
if (changed) fs.writeFileSync(cachePath, JSON.stringify(cache));
|
|
234
|
+
} catch {}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Delete any daily or per-session event log whose mtime is older than
|
|
238
|
+
// maxAgeMs (default 14 days). Single readdir + stat pass — no recursion.
|
|
239
|
+
function rotateEventLogs(eventsDir, maxAgeMs) {
|
|
240
|
+
maxAgeMs = maxAgeMs || 14 * 24 * 60 * 60 * 1000;
|
|
241
|
+
try {
|
|
242
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
243
|
+
const entries = fs.readdirSync(eventsDir).filter(f => !f.startsWith('._'));
|
|
244
|
+
for (const f of entries) {
|
|
245
|
+
if (!f.endsWith('.jsonl')) continue;
|
|
246
|
+
const full = path.join(eventsDir, f);
|
|
247
|
+
try {
|
|
248
|
+
if (fs.statSync(full).mtimeMs < cutoff) fs.unlinkSync(full);
|
|
249
|
+
} catch {}
|
|
250
|
+
}
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = { rotateEventLogs };
|
|
255
|
+
|
|
256
|
+
if (require.main === module) {
|
|
257
|
+
main().catch(() => process.exit(0));
|
|
258
|
+
}
|
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const { spawn } = require('child_process');
|
|
7
7
|
const { pathToFileURL } = require('url');
|
|
8
|
+
const { claimLock } = require('./utils/fs-helpers.cjs');
|
|
8
9
|
|
|
9
10
|
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
10
11
|
const graphDir = path.join(projectDir, '.monomind', 'graph');
|
|
@@ -91,20 +92,18 @@ if (fs.existsSync(dbPath)) {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
// Skip if another build is already in progress (avoids SQLite BUSY on concurrent init + session-start)
|
|
95
|
+
// P2-24: claim atomically (wx-create) instead of statSync-then-writeFileSync
|
|
96
|
+
// — two concurrent freshen triggers can both cross the same write-count
|
|
97
|
+
// threshold from parallel hook events, and a plain read-check-write lets
|
|
98
|
+
// both pass the check and both spawn a detached rebuild child. claimLock
|
|
99
|
+
// uses the same TOCTOU-safe stale-lock-break (atomic rename-to-claim) as
|
|
100
|
+
// control-start.cjs's spawn lock (see P2-25) — a lock older than 5 minutes
|
|
101
|
+
// is treated as abandoned and safely reclaimed.
|
|
94
102
|
const lockPath = path.join(graphDir, 'build.lock');
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (now - stat.mtimeMs < 5 * 60 * 1000) {
|
|
100
|
-
console.log('[graph] build already in progress — skipping');
|
|
101
|
-
process.exit(0);
|
|
102
|
-
}
|
|
103
|
-
fs.unlinkSync(lockPath);
|
|
104
|
-
} catch { /* lock does not exist — proceed */ }
|
|
105
|
-
|
|
106
|
-
// Write lock file; the build process removes it on completion
|
|
107
|
-
try { fs.writeFileSync(lockPath, String(process.pid)); } catch { /* non-fatal */ }
|
|
103
|
+
if (!claimLock(lockPath, 5 * 60 * 1000)) {
|
|
104
|
+
console.log('[graph] build already in progress — skipping');
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
108
107
|
|
|
109
108
|
// Spawn a detached node process to run buildAsync from @monoes/monograph (ESM).
|
|
110
109
|
// After the build, VACUUM the DB if it has >50% bloat (reclaim space from
|
|
@@ -113,18 +112,21 @@ const dbPathStr = JSON.stringify(path.join(projectDir, '.monomind', 'monograph.d
|
|
|
113
112
|
const script = `
|
|
114
113
|
import { buildAsync } from ${JSON.stringify(pathToFileURL(entryPoint).href)};
|
|
115
114
|
import { unlinkSync, statSync } from 'fs';
|
|
116
|
-
import {
|
|
115
|
+
import { execFileSync } from 'child_process';
|
|
117
116
|
try {
|
|
118
117
|
await buildAsync(${JSON.stringify(projectDir)});
|
|
119
118
|
// Vacuum if bloat ratio is high — keeps openDb fast over time.
|
|
120
119
|
try {
|
|
121
120
|
const dbPath = ${dbPathStr};
|
|
122
121
|
const fileMB = statSync(dbPath).size / 1024 / 1024;
|
|
122
|
+
// execFileSync with array argv — no shell involved, so a project
|
|
123
|
+
// directory name containing '"' or '$(...)' cannot break out and
|
|
124
|
+
// execute arbitrary commands (P3-8).
|
|
123
125
|
const liveMB = parseInt(
|
|
124
|
-
|
|
125
|
-
{ encoding: 'utf-8' }).trim(), 10);
|
|
126
|
+
execFileSync('sqlite3', [dbPath, 'SELECT SUM(pgsize)/1024/1024 FROM dbstat;'],
|
|
127
|
+
{ encoding: 'utf-8', timeout: 30000 }).trim(), 10);
|
|
126
128
|
if (fileMB > 100 && liveMB / fileMB < 0.5) {
|
|
127
|
-
|
|
129
|
+
execFileSync('sqlite3', [dbPath, 'VACUUM;'], { timeout: 120000 });
|
|
128
130
|
}
|
|
129
131
|
} catch (_) {}
|
|
130
132
|
} finally {
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const fs = require('fs');
|
|
7
|
+
const { purgeStaleRegistrations } = require('../utils/agent-registrations.cjs');
|
|
8
|
+
const { cleanEntries, atomicWriteFileSync, withLock } = require('../utils/fs-helpers.cjs');
|
|
7
9
|
|
|
8
10
|
module.exports = {
|
|
9
11
|
handle: async function(hCtx) {
|
|
@@ -18,41 +20,65 @@ module.exports = {
|
|
|
18
20
|
fs.mkdirSync(regDir, { recursive: true });
|
|
19
21
|
const id = Date.now() + '-' + Math.random().toString(36).slice(2, 6);
|
|
20
22
|
const regFile = path.join(regDir, 'agent-' + id + '.json');
|
|
23
|
+
// P3-15: also stamp the agent's type/slug so handlePostTask (task-handler.cjs)
|
|
24
|
+
// can correlate a completion event back to THIS registration instead of
|
|
25
|
+
// blindly popping the oldest file — the registration previously carried no
|
|
26
|
+
// identity beyond a random id, so post-task had nothing to match against.
|
|
27
|
+
// Same field precedence as the last-dispatch.json write below, computed
|
|
28
|
+
// early so both writers agree on the agent's identity.
|
|
29
|
+
const MAX_TYPE_LEN = 128;
|
|
30
|
+
const agentType = String(hookInput.subagent_type || hookInput.agentType || hookInput.agent_type || hookInput.agentSlug || 'unknown').slice(0, MAX_TYPE_LEN);
|
|
21
31
|
fs.writeFileSync(regFile, JSON.stringify({
|
|
22
32
|
agentId: id,
|
|
33
|
+
agentType: agentType,
|
|
23
34
|
startedAt: new Date().toISOString(),
|
|
24
35
|
pid: process.pid,
|
|
25
36
|
}));
|
|
37
|
+
// Purge stragglers older than 30 min on every agent-start too — not just
|
|
38
|
+
// on TeammateIdle/TaskCompleted (task-handler.cjs) — since sessions that
|
|
39
|
+
// never emit those events would otherwise leak registrations forever.
|
|
40
|
+
const activeAfterPurge = purgeStaleRegistrations(regDir);
|
|
26
41
|
// Refresh swarm-activity.json within the 5-min staleness window.
|
|
27
42
|
const activityDir = path.join(CWD, '.monomind', 'metrics');
|
|
28
43
|
fs.mkdirSync(activityDir, { recursive: true });
|
|
29
44
|
const activityPath = path.join(activityDir, 'swarm-activity.json');
|
|
30
45
|
const MAX_AGENTS = 1000;
|
|
31
|
-
const active = Math.min(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const active = Math.min(
|
|
47
|
+
activeAfterPurge != null ? activeAfterPurge : cleanEntries(regDir, f => f.endsWith('.json')).length,
|
|
48
|
+
MAX_AGENTS
|
|
49
|
+
);
|
|
50
|
+
// P2-21: swarm-activity.json is written by both this handler and
|
|
51
|
+
// task-handler.cjs's handlePostTask, concurrently (Claude Code fires
|
|
52
|
+
// hook events for parallel tool calls in one message). The old
|
|
53
|
+
// read-prevLastActive-then-write was a lost-update race — two
|
|
54
|
+
// processes could both read the same prevLastActive before either
|
|
55
|
+
// wrote, so whichever wrote last silently erased the other's peak.
|
|
56
|
+
// Share one lock file with task-handler.cjs so the read-merge-write
|
|
57
|
+
// cycle is a single critical section across both writers.
|
|
58
|
+
withLock(activityPath + '.lock', function() {
|
|
59
|
+
// Preserve lastActive (peak) so statusline shows non-zero after completion.
|
|
60
|
+
let prevLastActive = 0;
|
|
61
|
+
try {
|
|
62
|
+
const MAX_ACTIVITY = 64 * 1024; // 64 KiB
|
|
63
|
+
var actStat = fs.statSync(activityPath);
|
|
64
|
+
if (actStat.size <= MAX_ACTIVITY) {
|
|
65
|
+
prevLastActive = (JSON.parse(fs.readFileSync(activityPath, 'utf-8'))?.swarm?.lastActive) || 0;
|
|
66
|
+
}
|
|
67
|
+
} catch { /* ignore */ }
|
|
68
|
+
atomicWriteFileSync(activityPath, JSON.stringify({
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
swarm: {
|
|
71
|
+
active: active > 0,
|
|
72
|
+
agent_count: active,
|
|
73
|
+
coordination_active: active > 0,
|
|
74
|
+
lastActive: Math.max(active, prevLastActive),
|
|
75
|
+
},
|
|
76
|
+
}));
|
|
77
|
+
}, 5000);
|
|
50
78
|
|
|
51
79
|
// Write last-dispatch.json so the route handler can suppress redundant
|
|
52
80
|
// suggestions on the next turn when the same agent type is recommended.
|
|
53
|
-
const MAX_TYPE_LEN = 128;
|
|
54
81
|
const MAX_DESC_LEN = 500;
|
|
55
|
-
const agentType = String(hookInput.subagent_type || hookInput.agentType || hookInput.agent_type || hookInput.agentSlug || 'unknown').slice(0, MAX_TYPE_LEN);
|
|
56
82
|
const agentDesc = String(hookInput.description || hookInput.prompt_description || '').slice(0, MAX_DESC_LEN);
|
|
57
83
|
fs.writeFileSync(
|
|
58
84
|
path.join(CWD, '.monomind', 'last-dispatch.json'),
|
|
@@ -87,13 +113,19 @@ module.exports = {
|
|
|
87
113
|
if (_hooksModule && _hooksModule.executeHooks && _hooksModule.HookEvent) {
|
|
88
114
|
var agentTypeBridge = String(hookInput.subagent_type || hookInput.agentType || hookInput.agent_type || hookInput.agentSlug || 'unknown');
|
|
89
115
|
var agentDescBridge = String(hookInput.description || hookInput.prompt_description || '');
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
116
|
+
// Must be awaited (bounded by runWithTimeout, 1500ms) — hook-handler.cjs's
|
|
117
|
+
// main().finally(() => process.exit(...)) reaps any unawaited fire-and-forget
|
|
118
|
+
// promise before it does real I/O. See session-restore-handler.cjs's
|
|
119
|
+
// SessionStart bridge for the reference pattern this mirrors.
|
|
120
|
+
await hCtx.runWithTimeout(function() {
|
|
121
|
+
return _hooksModule.executeHooks(_hooksModule.HookEvent.AgentSpawn, {
|
|
122
|
+
agent: {
|
|
123
|
+
id: hookInput.agentId || hookInput.agent_id || (agentTypeBridge + '-' + Date.now()),
|
|
124
|
+
type: agentTypeBridge,
|
|
125
|
+
description: agentDescBridge.slice(0, 500),
|
|
126
|
+
},
|
|
127
|
+
}, { continueOnError: true, timeout: 1500 });
|
|
128
|
+
}, '@monomind/hooks.AgentSpawn');
|
|
97
129
|
}
|
|
98
130
|
} catch (e) { /* non-fatal */ }
|
|
99
131
|
},
|
|
@@ -21,9 +21,39 @@ const fs = require('fs');
|
|
|
21
21
|
const path = require('path');
|
|
22
22
|
const http = require('http');
|
|
23
23
|
const os = require('os');
|
|
24
|
+
const crypto = require('crypto');
|
|
25
|
+
const { appendJsonlWithRotation } = require('../utils/fs-helpers.cjs');
|
|
24
26
|
|
|
25
27
|
const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
26
28
|
const CAPTURE_DIR = path.join(CWD, '.monomind', 'capture');
|
|
29
|
+
const SNAP_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour — orphaned snapshots are cleaned up this old
|
|
30
|
+
|
|
31
|
+
// P2-23: derive a stable key for a subagent invocation from whatever
|
|
32
|
+
// identifying info Claude Code actually gives us in the hook payload.
|
|
33
|
+
// `transcript_path` is the one field that's both present on SubagentStart
|
|
34
|
+
// and SubagentStop AND unique per-subagent (each subagent gets its own
|
|
35
|
+
// transcript file, distinct from the parent session's) — session_id alone
|
|
36
|
+
// is shared by every concurrently-running subagent in the same team, so it
|
|
37
|
+
// can't disambiguate which stop event belongs to which start.
|
|
38
|
+
function subagentKey(hookInput) {
|
|
39
|
+
var raw = hookInput && (hookInput.transcript_path || hookInput.transcriptPath);
|
|
40
|
+
if (!raw) return null;
|
|
41
|
+
return crypto.createHash('md5').update(String(raw)).digest('hex').slice(0, 16);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Delete snap-*.json files older than SNAP_MAX_AGE_MS — orphaned when a
|
|
45
|
+
// SubagentStop never arrived (crash, forced-stop) or was matched to a
|
|
46
|
+
// different snapshot before keyed matching existed.
|
|
47
|
+
function cleanupStaleSnaps() {
|
|
48
|
+
try {
|
|
49
|
+
var files = fs.readdirSync(CAPTURE_DIR).filter(f => f.startsWith('snap-') && f.endsWith('.json') && !f.startsWith('._'));
|
|
50
|
+
var now = Date.now();
|
|
51
|
+
for (var i = 0; i < files.length; i++) {
|
|
52
|
+
var p = path.join(CAPTURE_DIR, files[i]);
|
|
53
|
+
try { if (now - fs.statSync(p).mtimeMs > SNAP_MAX_AGE_MS) fs.unlinkSync(p); } catch { /* ignore */ }
|
|
54
|
+
}
|
|
55
|
+
} catch { /* CAPTURE_DIR may not exist yet */ }
|
|
56
|
+
}
|
|
27
57
|
|
|
28
58
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
29
59
|
|
|
@@ -46,7 +76,7 @@ function snapshotJSONLFiles() {
|
|
|
46
76
|
if (!fs.existsSync(claudeDir)) return [];
|
|
47
77
|
try {
|
|
48
78
|
return fs.readdirSync(claudeDir)
|
|
49
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
79
|
+
.filter(f => f.endsWith('.jsonl') && !f.startsWith('._'))
|
|
50
80
|
.map(f => {
|
|
51
81
|
try { return { name: f, size: fs.statSync(path.join(claudeDir, f)).size }; }
|
|
52
82
|
catch { return { name: f, size: 0 }; }
|
|
@@ -170,6 +200,7 @@ function emitEvent(event) {
|
|
|
170
200
|
|
|
171
201
|
async function handleSubagentStart(hookInput) {
|
|
172
202
|
fs.mkdirSync(CAPTURE_DIR, { recursive: true });
|
|
203
|
+
cleanupStaleSnaps();
|
|
173
204
|
|
|
174
205
|
const snapshot = snapshotJSONLFiles();
|
|
175
206
|
const agentType = String(
|
|
@@ -190,10 +221,19 @@ async function handleSubagentStart(hookInput) {
|
|
|
190
221
|
}
|
|
191
222
|
const activeSess = getActiveSession(activeRun);
|
|
192
223
|
|
|
193
|
-
//
|
|
224
|
+
// P2-23: key the snapshot filename by transcript_path when available so
|
|
225
|
+
// SubagentStop can look up the EXACT matching snapshot instead of
|
|
226
|
+
// FIFO-popping the lexicographically-oldest one — under real concurrency
|
|
227
|
+
// (multiple subagents stopping in close succession) FIFO pop can match a
|
|
228
|
+
// stop event to the wrong subagent's snapshot. Falls back to the old
|
|
229
|
+
// timestamp+random naming when transcript_path isn't present in the
|
|
230
|
+
// payload, so SubagentStop's FIFO fallback path still has something to
|
|
231
|
+
// match against.
|
|
232
|
+
const subKey = subagentKey(hookInput);
|
|
194
233
|
const snapFile = path.join(
|
|
195
234
|
CAPTURE_DIR,
|
|
196
|
-
|
|
235
|
+
subKey ? 'snap-' + subKey + '.json'
|
|
236
|
+
: 'snap-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6) + '.json'
|
|
197
237
|
);
|
|
198
238
|
const leanModePath = path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), '.monomind/state/monolean-mode');
|
|
199
239
|
let leanMode = null;
|
|
@@ -235,18 +275,31 @@ async function handleSubagentStart(hookInput) {
|
|
|
235
275
|
|
|
236
276
|
async function handleSubagentStop(hookInput) {
|
|
237
277
|
fs.mkdirSync(CAPTURE_DIR, { recursive: true });
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
278
|
+
cleanupStaleSnaps();
|
|
279
|
+
|
|
280
|
+
// P2-23: prefer the exact keyed snapshot (see subagentKey/handleSubagentStart)
|
|
281
|
+
// over FIFO-popping the oldest snap-*.json — FIFO misattributes the stop
|
|
282
|
+
// event to the wrong subagent whenever multiple subagents stop in close
|
|
283
|
+
// succession. Fall back to FIFO only when this event's payload carries no
|
|
284
|
+
// transcript_path (older Claude Code versions / unexpected payload shape).
|
|
285
|
+
const subKey = subagentKey(hookInput);
|
|
286
|
+
let snapPath = null;
|
|
287
|
+
if (subKey) {
|
|
288
|
+
const keyedPath = path.join(CAPTURE_DIR, 'snap-' + subKey + '.json');
|
|
289
|
+
if (fs.existsSync(keyedPath)) snapPath = keyedPath;
|
|
290
|
+
}
|
|
291
|
+
if (!snapPath) {
|
|
292
|
+
const snapFiles = fs.readdirSync(CAPTURE_DIR)
|
|
293
|
+
.filter(f => f.startsWith('snap-') && f.endsWith('.json') && !f.startsWith('._'))
|
|
294
|
+
.sort(); // lexicographic = timestamp order (FIFO fallback)
|
|
295
|
+
if (snapFiles.length === 0) {
|
|
296
|
+
console.log('[CAPTURE:stop] No snapshot to diff — skipping');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
snapPath = path.join(CAPTURE_DIR, snapFiles[0]);
|
|
300
|
+
console.log('[CAPTURE:stop] no transcript_path/keyed snapshot — falling back to FIFO match');
|
|
247
301
|
}
|
|
248
302
|
|
|
249
|
-
const snapPath = path.join(CAPTURE_DIR, snapFiles[0]);
|
|
250
303
|
let snap;
|
|
251
304
|
try { snap = JSON.parse(fs.readFileSync(snapPath, 'utf8')); } catch {
|
|
252
305
|
try { fs.unlinkSync(snapPath); } catch {}
|
|
@@ -294,11 +347,13 @@ async function handleSubagentStop(hookInput) {
|
|
|
294
347
|
|
|
295
348
|
if (!org && !session) {
|
|
296
349
|
// No active org or session — log to general capture file only
|
|
350
|
+
// P2-26: rotate — was a pure appendFileSync with no cap, unlike
|
|
351
|
+
// intelligence-outcomes.jsonl (500-line cap).
|
|
297
352
|
const genLog = path.join(CAPTURE_DIR, 'unattributed.jsonl');
|
|
298
|
-
|
|
353
|
+
appendJsonlWithRotation(genLog, JSON.stringify({
|
|
299
354
|
ts: Date.now(), agentType, tokens_in: totalTin, tokens_out: totalTout,
|
|
300
355
|
cost_usd: costUsd, capturedFiles, summary: summary.slice(0, 200),
|
|
301
|
-
})
|
|
356
|
+
}), 500);
|
|
302
357
|
return;
|
|
303
358
|
}
|
|
304
359
|
|
|
@@ -355,9 +410,11 @@ async function handleSubagentStop(hookInput) {
|
|
|
355
410
|
// ── Persist transcript reference to run directory ─────────────────────────
|
|
356
411
|
if (capturedFiles.length > 0 && org && runId) {
|
|
357
412
|
const runDir = path.join(CWD, '.monomind', 'orgs', org, 'runs');
|
|
358
|
-
fs.mkdirSync(runDir, { recursive: true });
|
|
359
413
|
const capLog = path.join(runDir, runId + '-captures.jsonl');
|
|
360
|
-
|
|
414
|
+
// P2-26: rotate — was a pure appendFileSync with no cap. A long-running
|
|
415
|
+
// org loop can spawn far more than 500 agents over its lifetime, so
|
|
416
|
+
// this uses the same 500-line cap convention as the other JSONL logs.
|
|
417
|
+
appendJsonlWithRotation(capLog, JSON.stringify({
|
|
361
418
|
type: 'agent:capture',
|
|
362
419
|
org, runId, session,
|
|
363
420
|
agentType,
|
|
@@ -368,7 +425,7 @@ async function handleSubagentStop(hookInput) {
|
|
|
368
425
|
capturedFiles,
|
|
369
426
|
claudeProjectDir: claudeDir,
|
|
370
427
|
ts: Date.now(),
|
|
371
|
-
})
|
|
428
|
+
}), 500);
|
|
372
429
|
}
|
|
373
430
|
|
|
374
431
|
// ── Wire captured interaction into EpisodicStore ────
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const fs = require('fs');
|
|
8
|
+
const { claimLock } = require('../utils/fs-helpers.cjs');
|
|
8
9
|
|
|
9
10
|
module.exports = {
|
|
10
11
|
handle: async function(hCtx) {
|
|
@@ -84,12 +85,23 @@ module.exports = {
|
|
|
84
85
|
var codeExts = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|cs|cpp|c|rb|swift|php)$/i;
|
|
85
86
|
if (editedFile2 && codeExts.test(editedFile2)) {
|
|
86
87
|
var lockFile = path.join(CWD, '.monomind', 'graph', '.rebuild-lock');
|
|
87
|
-
var now = Date.now();
|
|
88
|
-
var lastBuild = 0;
|
|
89
|
-
try { var _lkSt = fs.statSync(lockFile); if (_lkSt.size <= 32) { lastBuild = parseInt(fs.readFileSync(lockFile, 'utf-8').trim(), 10) || 0; } } catch (e) {}
|
|
90
88
|
var COOLDOWN_MS = 5000; // 5-second debounce
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
// P3-17: on a fresh project (no full `monograph build` has ever run),
|
|
90
|
+
// .monomind/graph/ doesn't exist yet, and claimLock's writeFileSync
|
|
91
|
+
// below throws ENOENT — caught by this try/catch and silently
|
|
92
|
+
// swallowed, so the incremental rebuild (and blast-radius display)
|
|
93
|
+
// never fires until some other code path happens to create the dir.
|
|
94
|
+
try { fs.mkdirSync(path.dirname(lockFile), { recursive: true }); } catch (e) { /* non-fatal */ }
|
|
95
|
+
// P2-22: claim the cooldown lock atomically (wx-create) instead of
|
|
96
|
+
// statSync-then-writeFileSync — under real Claude Code concurrency a
|
|
97
|
+
// multi-file Edit in one message fires several PostToolUse hook
|
|
98
|
+
// processes near-simultaneously, and a plain read-check-write lets
|
|
99
|
+
// ALL of them read the same stale lastBuild timestamp before any of
|
|
100
|
+
// them writes a fresh one, so every one of them passes the cooldown
|
|
101
|
+
// check and spawns its own rebuild + ua-enrich child (see the
|
|
102
|
+
// 896-orphan-process incident fixed in control-start.cjs). claimLock
|
|
103
|
+
// mirrors that same TOCTOU-safe stale-lock-breaking pattern.
|
|
104
|
+
if (claimLock(lockFile, COOLDOWN_MS)) {
|
|
93
105
|
var { spawn: spawnRebuild } = require('child_process');
|
|
94
106
|
var rebuildScript = "import { buildAsync } from '@monoes/monograph'; await buildAsync(" + JSON.stringify(CWD) + ");";
|
|
95
107
|
var graphDir = path.join(CWD, '.monomind', 'graph');
|
|
@@ -148,10 +160,16 @@ module.exports = {
|
|
|
148
160
|
try {
|
|
149
161
|
var editFileBridge = hookInput.file_path || toolInput.file_path
|
|
150
162
|
|| process.env.TOOL_INPUT_file_path || args[0] || '';
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
// Must be awaited (bounded by runWithTimeout, 1500ms) — hook-handler.cjs's
|
|
164
|
+
// main().finally(() => process.exit(...)) reaps any unawaited fire-and-forget
|
|
165
|
+
// promise before it does real I/O. See session-restore-handler.cjs's
|
|
166
|
+
// SessionStart bridge for the reference pattern this mirrors.
|
|
167
|
+
await hCtx.runWithTimeout(function() {
|
|
168
|
+
return _hooksModule.executeHooks(_hooksModule.HookEvent.PostEdit, {
|
|
169
|
+
file: { path: editFileBridge, operation: 'modify' },
|
|
170
|
+
duration: 0,
|
|
171
|
+
}, { continueOnError: true, timeout: 1500 });
|
|
172
|
+
}, '@monomind/hooks.PostEdit');
|
|
155
173
|
} catch (e) { /* non-fatal */ }
|
|
156
174
|
}
|
|
157
175
|
|