mono-pilot 0.2.9 → 0.2.12
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/README.md +270 -7
- package/dist/src/agents-paths.js +36 -0
- package/dist/src/brief/blocks.js +83 -0
- package/dist/src/brief/defaults.js +60 -0
- package/dist/src/brief/frontmatter.js +53 -0
- package/dist/src/brief/paths.js +10 -0
- package/dist/src/brief/reflection.js +27 -0
- package/dist/src/cli.js +62 -5
- package/dist/src/cluster/bus.js +102 -0
- package/dist/src/cluster/follower.js +137 -0
- package/dist/src/cluster/init.js +182 -0
- package/dist/src/cluster/leader.js +97 -0
- package/dist/src/cluster/log.js +49 -0
- package/dist/src/cluster/protocol.js +34 -0
- package/dist/src/cluster/services/bus.js +243 -0
- package/dist/src/cluster/services/embedding.js +12 -0
- package/dist/src/cluster/socket.js +86 -0
- package/dist/src/cluster/test-bus.js +175 -0
- package/dist/src/cluster_v2/connection-lifecycle.js +31 -0
- package/dist/src/cluster_v2/connection-lifecycle.test.js +24 -0
- package/dist/src/cluster_v2/connection.js +159 -0
- package/dist/src/cluster_v2/connection.test.js +55 -0
- package/dist/src/cluster_v2/events.js +102 -0
- package/dist/src/cluster_v2/index.js +2 -0
- package/dist/src/cluster_v2/observability.js +99 -0
- package/dist/src/cluster_v2/observability.test.js +46 -0
- package/dist/src/cluster_v2/rpc.js +389 -0
- package/dist/src/cluster_v2/rpc.test.js +110 -0
- package/dist/src/cluster_v2/runtime.failover.integration.test.js +156 -0
- package/dist/src/cluster_v2/runtime.js +531 -0
- package/dist/src/cluster_v2/runtime.lease-compromise.integration.test.js +91 -0
- package/dist/src/cluster_v2/runtime.lifecycle.integration.test.js +225 -0
- package/dist/src/cluster_v2/services/bus.integration.test.js +140 -0
- package/dist/src/cluster_v2/services/bus.js +450 -0
- package/dist/src/cluster_v2/services/discord/auth-store.js +82 -0
- package/dist/src/cluster_v2/services/discord/collector.js +569 -0
- package/dist/src/cluster_v2/services/discord/index.js +1 -0
- package/dist/src/cluster_v2/services/discord/oauth.js +87 -0
- package/dist/src/cluster_v2/services/discord/rpc-client.js +325 -0
- package/dist/src/cluster_v2/services/embedding.js +66 -0
- package/dist/src/cluster_v2/services/registry-cache.js +107 -0
- package/dist/src/cluster_v2/services/registry-cache.test.js +66 -0
- package/dist/src/cluster_v2/services/registry.js +36 -0
- package/dist/src/cluster_v2/services/twitter/collector.js +1055 -0
- package/dist/src/cluster_v2/services/twitter/index.js +1 -0
- package/dist/src/config/digest.js +78 -0
- package/dist/src/config/discord.js +143 -0
- package/dist/src/config/image-gen.js +48 -0
- package/dist/src/config/mono-pilot.js +31 -0
- package/dist/src/config/twitter.js +100 -0
- package/dist/src/extensions/cluster.js +311 -0
- package/dist/src/extensions/commands/build-memory.js +76 -0
- package/dist/src/extensions/commands/digest/backfill.js +779 -0
- package/dist/src/extensions/commands/digest/index.js +1133 -0
- package/dist/src/extensions/commands/image-model.js +214 -0
- package/dist/src/extensions/game/bus-injection.js +47 -0
- package/dist/src/extensions/game/identity.js +83 -0
- package/dist/src/extensions/game/mailbox.js +61 -0
- package/dist/src/extensions/game/system-prompt.js +134 -0
- package/dist/src/extensions/game/tools.js +28 -0
- package/dist/src/extensions/lifecycle.js +337 -0
- package/dist/src/extensions/mode-runtime.js +26 -2
- package/dist/src/extensions/mono-game.js +66 -0
- package/dist/src/extensions/mono-pilot.js +100 -18
- package/dist/src/extensions/nvim.js +47 -0
- package/dist/src/extensions/session-hints.js +60 -35
- package/dist/src/extensions/sftp.js +897 -0
- package/dist/src/extensions/status.js +676 -0
- package/dist/src/extensions/system-events.js +478 -0
- package/dist/src/extensions/system-prompt.js +24 -14
- package/dist/src/extensions/user-message.js +94 -50
- package/dist/src/lsp/client.js +235 -0
- package/dist/src/lsp/index.js +165 -0
- package/dist/src/lsp/runtime.js +67 -0
- package/dist/src/lsp/server.js +242 -0
- package/dist/src/mcp/config.js +112 -0
- package/dist/src/{utils/mcp-client.js → mcp/protocol.js} +1 -100
- package/dist/src/mcp/servers.js +90 -0
- package/dist/src/memory/build-memory.js +103 -0
- package/dist/src/memory/config/defaults.js +55 -0
- package/dist/src/memory/config/loader.js +29 -0
- package/dist/src/memory/config/paths.js +9 -0
- package/dist/src/memory/config/resolve.js +90 -0
- package/dist/src/memory/config/types.js +1 -0
- package/dist/src/memory/embeddings/batch-runner.js +39 -0
- package/dist/src/memory/embeddings/cache.js +47 -0
- package/dist/src/memory/embeddings/chunk-limits.js +26 -0
- package/dist/src/memory/embeddings/input-limits.js +48 -0
- package/dist/src/memory/embeddings/local.js +108 -0
- package/dist/src/memory/embeddings/types.js +1 -0
- package/dist/src/memory/index-manager.js +552 -0
- package/dist/src/memory/indexing/embeddings.js +67 -0
- package/dist/src/memory/indexing/files.js +180 -0
- package/dist/src/memory/indexing/index-file.js +105 -0
- package/dist/src/memory/log.js +38 -0
- package/dist/src/memory/paths.js +15 -0
- package/dist/src/memory/runtime/index.js +299 -0
- package/dist/src/memory/runtime/thread.js +116 -0
- package/dist/src/memory/search/fts.js +57 -0
- package/dist/src/memory/search/hybrid.js +50 -0
- package/dist/src/memory/search/text.js +30 -0
- package/dist/src/memory/search/vector.js +43 -0
- package/dist/src/memory/session/content-hash.js +7 -0
- package/dist/src/memory/session/entry.js +33 -0
- package/dist/src/memory/session/flush-policy.js +34 -0
- package/dist/src/memory/session/hook.js +191 -0
- package/dist/src/memory/session/paths.js +15 -0
- package/dist/src/memory/session/session-reader.js +88 -0
- package/dist/src/memory/session/transcript/content-hash.js +7 -0
- package/dist/src/memory/session/transcript/entry.js +28 -0
- package/dist/src/memory/session/transcript/flush.js +56 -0
- package/dist/src/memory/session/transcript/paths.js +28 -0
- package/dist/src/memory/session/transcript/reader.js +112 -0
- package/dist/src/memory/session/transcript/state.js +31 -0
- package/dist/src/memory/store/schema.js +89 -0
- package/dist/src/memory/store/sqlite.js +89 -0
- package/dist/src/memory/types.js +1 -0
- package/dist/src/memory/warm.js +25 -0
- package/dist/src/rules/discovery.js +41 -0
- package/dist/{tools → src/tools}/README.md +29 -3
- package/dist/{tools → src/tools}/apply-patch-description.md +8 -2
- package/dist/{tools → src/tools}/apply-patch.js +174 -104
- package/dist/{tools → src/tools}/apply-patch.test.js +52 -1
- package/dist/{tools/ask-question.js → src/tools/ask-user-question.js} +3 -3
- package/dist/src/tools/ast-grep.js +357 -0
- package/dist/src/tools/brief-write.js +122 -0
- package/dist/src/tools/bus-send.js +100 -0
- package/dist/{tools → src/tools}/call-mcp-tool.js +40 -124
- package/dist/src/tools/codex-apply-patch-description.md +52 -0
- package/dist/src/tools/codex-apply-patch.js +540 -0
- package/dist/{tools → src/tools}/delete.js +24 -0
- package/dist/src/tools/exit-plan-mode.js +83 -0
- package/dist/{tools → src/tools}/fetch-mcp-resource.js +56 -100
- package/dist/src/tools/generate-image.js +567 -0
- package/dist/{tools → src/tools}/glob.js +55 -1
- package/dist/{tools → src/tools}/list-mcp-resources.js +46 -57
- package/dist/{tools → src/tools}/list-mcp-tools.js +52 -63
- package/dist/src/tools/ls.js +48 -0
- package/dist/src/tools/lsp-diagnostics.js +67 -0
- package/dist/src/tools/lsp-symbols.js +54 -0
- package/dist/src/tools/mailbox.js +85 -0
- package/dist/src/tools/memory-get.js +90 -0
- package/dist/src/tools/memory-search.js +180 -0
- package/dist/{tools → src/tools}/plan-mode-reminder.md +3 -4
- package/dist/{tools → src/tools}/read-file.js +8 -19
- package/dist/{tools → src/tools}/rg.js +10 -20
- package/dist/{tools → src/tools}/shell.js +19 -42
- package/dist/{tools → src/tools}/subagent.js +255 -6
- package/dist/{tools → src/tools}/switch-mode.js +37 -6
- package/dist/{tools → src/tools}/web-fetch.js +105 -7
- package/dist/{tools → src/tools}/web-search.js +29 -1
- package/package.json +21 -9
- /package/dist/{tools → src/tools}/ask-mode-reminder.md +0 -0
- /package/dist/{tools → src/tools}/rg.test.js +0 -0
- /package/dist/{tools → src/tools}/semantic-search-description.md +0 -0
- /package/dist/{tools → src/tools}/semantic-search.js +0 -0
- /package/dist/{tools → src/tools}/shell-description.md +0 -0
- /package/dist/{tools → src/tools}/subagent-description.md +0 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
import lockfile from "proper-lockfile";
|
|
6
|
+
function normalizeScope(scope) {
|
|
7
|
+
const raw = scope?.trim() || "default";
|
|
8
|
+
return raw.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
9
|
+
}
|
|
10
|
+
const CLUSTER_ROOT = join(homedir(), ".mono-pilot");
|
|
11
|
+
function socketPathFor(scope) {
|
|
12
|
+
return join(CLUSTER_ROOT, `cluster-v2.${scope}.sock`);
|
|
13
|
+
}
|
|
14
|
+
function lockPathFor(scope) {
|
|
15
|
+
return join(CLUSTER_ROOT, `cluster-v2.${scope}.leader`);
|
|
16
|
+
}
|
|
17
|
+
export function getSocketPath(scope) {
|
|
18
|
+
return socketPathFor(normalizeScope(scope));
|
|
19
|
+
}
|
|
20
|
+
async function isSocketAlive(socketPath) {
|
|
21
|
+
if (!existsSync(socketPath)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const socket = net.createConnection(socketPath);
|
|
26
|
+
const timer = setTimeout(() => {
|
|
27
|
+
socket.destroy();
|
|
28
|
+
resolve(false);
|
|
29
|
+
}, 500);
|
|
30
|
+
socket.on("connect", () => {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
socket.end();
|
|
33
|
+
resolve(true);
|
|
34
|
+
});
|
|
35
|
+
socket.on("error", () => {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
resolve(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export function tryConnect(scope, timeoutMs = 2000) {
|
|
42
|
+
const socketPath = getSocketPath(scope);
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const socket = net.createConnection(socketPath);
|
|
45
|
+
const timeout = setTimeout(() => {
|
|
46
|
+
socket.destroy();
|
|
47
|
+
resolve(null);
|
|
48
|
+
}, timeoutMs);
|
|
49
|
+
socket.on("connect", () => {
|
|
50
|
+
clearTimeout(timeout);
|
|
51
|
+
resolve(socket);
|
|
52
|
+
});
|
|
53
|
+
socket.on("error", () => {
|
|
54
|
+
clearTimeout(timeout);
|
|
55
|
+
resolve(null);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Acquire leader lease and listen on scoped socket.
|
|
61
|
+
* Returns null if lease is held by another process or listening fails.
|
|
62
|
+
*/
|
|
63
|
+
export async function tryListen(options) {
|
|
64
|
+
const scope = normalizeScope(options?.scope);
|
|
65
|
+
const socketPath = socketPathFor(scope);
|
|
66
|
+
const lockPath = lockPathFor(scope);
|
|
67
|
+
const stale = options?.staleMs ?? 10_000;
|
|
68
|
+
const update = options?.updateMs ?? Math.max(1_000, Math.floor(stale / 2));
|
|
69
|
+
mkdirSync(CLUSTER_ROOT, { recursive: true });
|
|
70
|
+
writeFileSync(lockPath, "", { flag: "a" });
|
|
71
|
+
let releaseLock = null;
|
|
72
|
+
try {
|
|
73
|
+
releaseLock = await lockfile.lock(lockPath, {
|
|
74
|
+
stale,
|
|
75
|
+
update,
|
|
76
|
+
retries: 0,
|
|
77
|
+
onCompromised: (error) => {
|
|
78
|
+
const compromisedError = error instanceof Error ? error : new Error(String(error));
|
|
79
|
+
options?.onLeaseCompromised?.(compromisedError);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (existsSync(socketPath)) {
|
|
87
|
+
const alive = await isSocketAlive(socketPath);
|
|
88
|
+
if (alive) {
|
|
89
|
+
await releaseLock();
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
unlinkSync(socketPath);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
await releaseLock();
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const server = net.createServer();
|
|
101
|
+
const connections = new Set();
|
|
102
|
+
server.on("connection", (socket) => {
|
|
103
|
+
connections.add(socket);
|
|
104
|
+
socket.on("close", () => {
|
|
105
|
+
connections.delete(socket);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
const listened = await new Promise((resolve) => {
|
|
109
|
+
server.once("error", () => resolve(false));
|
|
110
|
+
server.listen(socketPath, () => resolve(true));
|
|
111
|
+
});
|
|
112
|
+
if (!listened) {
|
|
113
|
+
try {
|
|
114
|
+
server.close();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// no-op
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await releaseLock();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// no-op
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
let closed = false;
|
|
128
|
+
const close = async () => {
|
|
129
|
+
if (closed) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
closed = true;
|
|
133
|
+
for (const socket of connections) {
|
|
134
|
+
socket.destroy();
|
|
135
|
+
}
|
|
136
|
+
await new Promise((resolve) => {
|
|
137
|
+
server.close(() => resolve());
|
|
138
|
+
});
|
|
139
|
+
try {
|
|
140
|
+
unlinkSync(socketPath);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// no-op
|
|
144
|
+
}
|
|
145
|
+
if (releaseLock) {
|
|
146
|
+
try {
|
|
147
|
+
await releaseLock();
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// no-op
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
socketPath,
|
|
156
|
+
server,
|
|
157
|
+
close,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { tryListen } from "./connection.js";
|
|
4
|
+
function uniqueScope(prefix) {
|
|
5
|
+
return `${prefix}-${process.pid}-${Date.now()}-${Math.floor(Math.random() * 100000)}`;
|
|
6
|
+
}
|
|
7
|
+
async function sleep(ms) {
|
|
8
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
async function connectFollower(socketPath) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const socket = net.createConnection(socketPath);
|
|
13
|
+
socket.once("connect", () => resolve(socket));
|
|
14
|
+
socket.once("error", reject);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
describe("cluster_v2 connection lease", () => {
|
|
18
|
+
it("allows only one active leader per scope at a time", async () => {
|
|
19
|
+
const scope = uniqueScope("leader-scope");
|
|
20
|
+
const first = await tryListen({ scope });
|
|
21
|
+
expect(first).not.toBeNull();
|
|
22
|
+
if (!first)
|
|
23
|
+
return;
|
|
24
|
+
const second = await tryListen({ scope });
|
|
25
|
+
expect(second).toBeNull();
|
|
26
|
+
await first.close();
|
|
27
|
+
const third = await tryListen({ scope });
|
|
28
|
+
expect(third).not.toBeNull();
|
|
29
|
+
if (third) {
|
|
30
|
+
await third.close();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
it("closes promptly even with active follower sockets", async () => {
|
|
34
|
+
const scope = uniqueScope("leader-close");
|
|
35
|
+
const leader = await tryListen({ scope });
|
|
36
|
+
expect(leader).not.toBeNull();
|
|
37
|
+
if (!leader)
|
|
38
|
+
return;
|
|
39
|
+
const follower = await connectFollower(leader.socketPath);
|
|
40
|
+
const followerClosed = new Promise((resolve) => {
|
|
41
|
+
follower.once("close", () => resolve());
|
|
42
|
+
});
|
|
43
|
+
const closeResult = await Promise.race([
|
|
44
|
+
leader.close().then(() => "closed"),
|
|
45
|
+
sleep(1000).then(() => "timeout"),
|
|
46
|
+
]);
|
|
47
|
+
expect(closeResult).toBe("closed");
|
|
48
|
+
await Promise.race([
|
|
49
|
+
followerClosed.then(() => "closed"),
|
|
50
|
+
sleep(1000).then(() => "timeout"),
|
|
51
|
+
]).then((result) => {
|
|
52
|
+
expect(result).toBe("closed");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const leaderOfflineHandlers = new Set();
|
|
2
|
+
const leaderRecoveredHandlers = new Set();
|
|
3
|
+
const discordChannelBatchHandlers = new Set();
|
|
4
|
+
const twitterPullBatchHandlers = new Set();
|
|
5
|
+
const twitterPullFailedHandlers = new Set();
|
|
6
|
+
const twitterCollectorStartupFailedHandlers = new Set();
|
|
7
|
+
export function onClusterV2LeaderOffline(handler) {
|
|
8
|
+
leaderOfflineHandlers.add(handler);
|
|
9
|
+
return () => {
|
|
10
|
+
leaderOfflineHandlers.delete(handler);
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function onClusterV2LeaderRecovered(handler) {
|
|
14
|
+
leaderRecoveredHandlers.add(handler);
|
|
15
|
+
return () => {
|
|
16
|
+
leaderRecoveredHandlers.delete(handler);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function onClusterV2DiscordChannelBatch(handler) {
|
|
20
|
+
discordChannelBatchHandlers.add(handler);
|
|
21
|
+
return () => {
|
|
22
|
+
discordChannelBatchHandlers.delete(handler);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function onClusterV2TwitterPullBatch(handler) {
|
|
26
|
+
twitterPullBatchHandlers.add(handler);
|
|
27
|
+
return () => {
|
|
28
|
+
twitterPullBatchHandlers.delete(handler);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function onClusterV2TwitterPullFailed(handler) {
|
|
32
|
+
twitterPullFailedHandlers.add(handler);
|
|
33
|
+
return () => {
|
|
34
|
+
twitterPullFailedHandlers.delete(handler);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function onClusterV2TwitterCollectorStartupFailed(handler) {
|
|
38
|
+
twitterCollectorStartupFailedHandlers.add(handler);
|
|
39
|
+
return () => {
|
|
40
|
+
twitterCollectorStartupFailedHandlers.delete(handler);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function emitClusterV2LeaderOffline(event) {
|
|
44
|
+
for (const handler of leaderOfflineHandlers) {
|
|
45
|
+
try {
|
|
46
|
+
handler(event);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Best-effort notification only.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function emitClusterV2LeaderRecovered(event) {
|
|
54
|
+
for (const handler of leaderRecoveredHandlers) {
|
|
55
|
+
try {
|
|
56
|
+
handler(event);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Best-effort notification only.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function emitClusterV2DiscordChannelBatch(event) {
|
|
64
|
+
for (const handler of discordChannelBatchHandlers) {
|
|
65
|
+
try {
|
|
66
|
+
handler(event);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Best-effort notification only.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function emitClusterV2TwitterPullBatch(event) {
|
|
74
|
+
for (const handler of twitterPullBatchHandlers) {
|
|
75
|
+
try {
|
|
76
|
+
handler(event);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Best-effort notification only.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function emitClusterV2TwitterPullFailed(event) {
|
|
84
|
+
for (const handler of twitterPullFailedHandlers) {
|
|
85
|
+
try {
|
|
86
|
+
handler(event);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Best-effort notification only.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function emitClusterV2TwitterCollectorStartupFailed(event) {
|
|
94
|
+
for (const handler of twitterCollectorStartupFailedHandlers) {
|
|
95
|
+
try {
|
|
96
|
+
handler(event);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Best-effort notification only.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { closeClusterV2, getActiveClusterV2Service, initClusterV2, reelectClusterV2, stepdownClusterV2Leader, } from "./runtime.js";
|
|
2
|
+
export { onClusterV2DiscordChannelBatch, onClusterV2LeaderOffline, onClusterV2LeaderRecovered, onClusterV2TwitterCollectorStartupFailed, onClusterV2TwitterPullBatch, onClusterV2TwitterPullFailed, } from "./events.js";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const LOGS_DIR = join(homedir(), ".mono-pilot", "logs");
|
|
5
|
+
const STDIO_ENV = "MONO_PILOT_CLUSTER_V2_LOG_STDIO";
|
|
6
|
+
let writeQueue = Promise.resolve();
|
|
7
|
+
function getLogPath(date = new Date()) {
|
|
8
|
+
const stamp = date.toISOString().slice(0, 10);
|
|
9
|
+
return join(LOGS_DIR, `cluster_v2.${stamp}.log`);
|
|
10
|
+
}
|
|
11
|
+
function shouldMirrorToStdout() {
|
|
12
|
+
if (process.env.NODE_ENV === "test") {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
return process.env[STDIO_ENV] === "1";
|
|
16
|
+
}
|
|
17
|
+
function enqueueLogLine(line) {
|
|
18
|
+
writeQueue = writeQueue
|
|
19
|
+
.then(async () => {
|
|
20
|
+
await mkdir(LOGS_DIR, { recursive: true });
|
|
21
|
+
await appendFile(getLogPath(), `${line}\n`, { encoding: "utf-8" });
|
|
22
|
+
})
|
|
23
|
+
.catch(() => {
|
|
24
|
+
// Keep logging failures non-fatal.
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function mirrorToStdio(level, line) {
|
|
28
|
+
if (!shouldMirrorToStdout()) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (level === "error") {
|
|
32
|
+
console.error(line);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (level === "warn") {
|
|
36
|
+
console.warn(line);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
console.info(line);
|
|
40
|
+
}
|
|
41
|
+
export function createClusterLogContext(context) {
|
|
42
|
+
return {
|
|
43
|
+
pid: process.pid,
|
|
44
|
+
agentId: context?.agentId ?? null,
|
|
45
|
+
sessionId: context?.sessionId ?? null,
|
|
46
|
+
scope: context?.scope ?? null,
|
|
47
|
+
role: context?.role ?? null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function logClusterEvent(level, event, context, details) {
|
|
51
|
+
const record = {
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
event,
|
|
54
|
+
...context,
|
|
55
|
+
...(details ?? {}),
|
|
56
|
+
};
|
|
57
|
+
const line = `[cluster_v2] ${JSON.stringify(record)}`;
|
|
58
|
+
enqueueLogLine(line);
|
|
59
|
+
mirrorToStdio(level, line);
|
|
60
|
+
}
|
|
61
|
+
export class RequestCounters {
|
|
62
|
+
started = 0;
|
|
63
|
+
completed = 0;
|
|
64
|
+
byState = {
|
|
65
|
+
ok: 0,
|
|
66
|
+
timeout: 0,
|
|
67
|
+
error: 0,
|
|
68
|
+
aborted: 0,
|
|
69
|
+
closed: 0,
|
|
70
|
+
};
|
|
71
|
+
start() {
|
|
72
|
+
this.started++;
|
|
73
|
+
}
|
|
74
|
+
complete(state) {
|
|
75
|
+
this.completed++;
|
|
76
|
+
this.byState[state]++;
|
|
77
|
+
}
|
|
78
|
+
outstanding() {
|
|
79
|
+
return this.started - this.completed;
|
|
80
|
+
}
|
|
81
|
+
snapshot() {
|
|
82
|
+
return {
|
|
83
|
+
started: this.started,
|
|
84
|
+
completed: this.completed,
|
|
85
|
+
outstanding: this.outstanding(),
|
|
86
|
+
byState: { ...this.byState },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
assertConsistency(expectedOutstanding, context, source) {
|
|
90
|
+
const outstanding = this.outstanding();
|
|
91
|
+
if (this.completed > this.started || outstanding !== expectedOutstanding) {
|
|
92
|
+
logClusterEvent("warn", "request_counter_mismatch", context, {
|
|
93
|
+
source,
|
|
94
|
+
expectedOutstanding,
|
|
95
|
+
...this.snapshot(),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createClusterLogContext, RequestCounters } from "./observability.js";
|
|
3
|
+
describe("cluster_v2 observability", () => {
|
|
4
|
+
it("builds structured log context with required fields", () => {
|
|
5
|
+
const ctx = createClusterLogContext({
|
|
6
|
+
agentId: "agent-a",
|
|
7
|
+
sessionId: "session-a",
|
|
8
|
+
scope: "scope-a",
|
|
9
|
+
role: "test",
|
|
10
|
+
});
|
|
11
|
+
expect(ctx.pid).toBeGreaterThan(0);
|
|
12
|
+
expect(ctx.agentId).toBe("agent-a");
|
|
13
|
+
expect(ctx.sessionId).toBe("session-a");
|
|
14
|
+
expect(ctx.scope).toBe("scope-a");
|
|
15
|
+
expect(ctx.role).toBe("test");
|
|
16
|
+
});
|
|
17
|
+
it("warns on request counter mismatch", () => {
|
|
18
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {
|
|
19
|
+
// mute test logs
|
|
20
|
+
});
|
|
21
|
+
try {
|
|
22
|
+
const counters = new RequestCounters();
|
|
23
|
+
counters.start();
|
|
24
|
+
counters.assertConsistency(0, createClusterLogContext({ role: "test" }), "mismatch_case");
|
|
25
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
warnSpy.mockRestore();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
it("stays silent when request counters are consistent", () => {
|
|
32
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {
|
|
33
|
+
// mute test logs
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
const counters = new RequestCounters();
|
|
37
|
+
counters.start();
|
|
38
|
+
counters.complete("ok");
|
|
39
|
+
counters.assertConsistency(0, createClusterLogContext({ role: "test" }), "consistent_case");
|
|
40
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
warnSpy.mockRestore();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|