blun-king-cli 9.1.550 → 9.1.562
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/LIESMICH.txt +2 -2
- package/README.md +1 -1
- package/agent-spine-plugin/scripts/check-hosts.js +196 -0
- package/bin/compaction-transaction-policy.cjs +122 -0
- package/bin/default-model-output-budget-policy.cjs +28 -0
- package/bin/file-observation-policy.cjs +133 -0
- package/bin/launcher-runtime.js +0 -1
- package/bin/micro-compaction-policy.cjs +64 -0
- package/bin/mnemo-connect-heartbeat.cjs +1 -3
- package/bin/retry-checkpoint-policy.cjs +13 -0
- package/bin/session-checkpoint-policy.cjs +25 -0
- package/bin/startup-preferences.cjs +1 -0
- package/bin/tool-file-persistence.cjs +141 -0
- package/bin/tool-result-offload-policy.cjs +12 -33
- package/bin/turn-thinking-policy.cjs +2 -26
- package/bin/update-notice.js +16 -0
- package/blun.mjs +482 -170
- package/package.json +2 -1
- package/telegram-plugin/bin/telegram-mnemo-capture.cjs +1 -3
- package/bin/empty-response-retry-policy.cjs +0 -29
package/LIESMICH.txt
CHANGED
package/README.md
CHANGED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
function assert(condition, message) {
|
|
8
|
+
if (!condition) throw new Error(message);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function json(root, path) {
|
|
12
|
+
return JSON.parse(await readFile(resolve(root, path), "utf8"));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function expand(value, variable, root) {
|
|
16
|
+
assert(typeof value === "string" && value.length > 0, "host command values must be non-empty strings");
|
|
17
|
+
return value.split(`\${${variable}}`).join(root);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function validateEntrypoint(root, value) {
|
|
21
|
+
const target = resolve(value);
|
|
22
|
+
const within = relative(root, target);
|
|
23
|
+
assert(within && !within.startsWith("..") && !isAbsolute(within), "MCP entrypoint must remain inside the plugin");
|
|
24
|
+
const metadata = await stat(target);
|
|
25
|
+
assert(metadata.isFile(), "MCP entrypoint must be a regular file");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateHooks(root, hooks, { required, commandRoot }) {
|
|
29
|
+
assert(hooks && typeof hooks === "object" && !Array.isArray(hooks), "hook bundle is missing");
|
|
30
|
+
assert(Object.keys(hooks).every((key) => ["description", "hooks"].includes(key)), "hook bundle contains unsupported top-level metadata");
|
|
31
|
+
assert(hooks.description && typeof hooks.description === "string", "hook bundle description is missing");
|
|
32
|
+
for (const event of required) {
|
|
33
|
+
const registrations = hooks.hooks?.[event];
|
|
34
|
+
assert(Array.isArray(registrations) && registrations.length === 1, `${event} must have exactly one registration`);
|
|
35
|
+
assert(Array.isArray(registrations[0].hooks) && registrations[0].hooks.length === 1, `${event} must have exactly one hook command`);
|
|
36
|
+
const command = registrations[0].hooks[0];
|
|
37
|
+
assert(command.type === "command", `${event} must use a command hook`);
|
|
38
|
+
assert(command.command === `node "\${${commandRoot}}/src/hook.js"`, `${event} must use the bundled lifecycle adapter`);
|
|
39
|
+
assert(Number.isInteger(command.timeout) && command.timeout > 0 && command.timeout <= 15, `${event} timeout is unsafe`);
|
|
40
|
+
}
|
|
41
|
+
const extras = Object.keys(hooks.hooks || {}).filter((event) => !required.includes(event));
|
|
42
|
+
assert(extras.length === 0, `unknown hook events: ${extras.join(", ")}`);
|
|
43
|
+
return { events: required, commands: required.length, entrypoint: relative(root, resolve(root, "src/hook.js")) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateBlunHooks(root, hooks) {
|
|
47
|
+
const required = [
|
|
48
|
+
"SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse",
|
|
49
|
+
"PreCompact", "PostCompact", "Stop", "SubagentStop"
|
|
50
|
+
];
|
|
51
|
+
assert(Array.isArray(hooks), "BLUN hook bundle is missing");
|
|
52
|
+
assert(hooks.length === required.length, "BLUN must register exactly one command per lifecycle event");
|
|
53
|
+
assert(JSON.stringify(hooks.map(({ event }) => event)) === JSON.stringify(required),
|
|
54
|
+
"BLUN lifecycle events must remain complete and ordered");
|
|
55
|
+
for (const event of required) {
|
|
56
|
+
const registrations = hooks.filter((hook) => hook.event === event);
|
|
57
|
+
assert(registrations.length === 1, `${event} must have exactly one BLUN registration`);
|
|
58
|
+
const command = registrations[0];
|
|
59
|
+
assert(command.command === 'node "./src/hook.js"', `${event} must use the bundled BLUN lifecycle adapter`);
|
|
60
|
+
assert(Number.isInteger(command.timeout) && command.timeout > 0 && command.timeout <= 15, `${event} BLUN timeout is unsafe`);
|
|
61
|
+
}
|
|
62
|
+
return { events: required, commands: required.length, entrypoint: relative(root, resolve(root, "src/hook.js")) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function initializeServer({ label, root, variable, server, version }) {
|
|
66
|
+
assert(server && typeof server === "object" && !Array.isArray(server), `${label} MCP registration is missing`);
|
|
67
|
+
assert(server.command === "node", `${label} MCP registration must use the Node.js runtime`);
|
|
68
|
+
assert(Array.isArray(server.args) && server.args.length === 1, `${label} MCP registration must name exactly one entrypoint`);
|
|
69
|
+
const command = process.execPath;
|
|
70
|
+
const args = server.args.map((value) => {
|
|
71
|
+
const expanded = expand(value, variable, root);
|
|
72
|
+
return isAbsolute(expanded) ? expanded : resolve(root, expanded);
|
|
73
|
+
});
|
|
74
|
+
assert(!args.some((value) => value.includes("${")), `${label} MCP registration contains an unresolved variable`);
|
|
75
|
+
await validateEntrypoint(root, args[0]);
|
|
76
|
+
|
|
77
|
+
return await new Promise((resolveResult, reject) => {
|
|
78
|
+
const child = spawn(command, args, {
|
|
79
|
+
cwd: root,
|
|
80
|
+
env: { ...process.env, CLAUDE_PLUGIN_ROOT: root, PLUGIN_ROOT: root },
|
|
81
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
82
|
+
});
|
|
83
|
+
let stdout = "";
|
|
84
|
+
let stderr = "";
|
|
85
|
+
let settled = false;
|
|
86
|
+
const finish = (error, value) => {
|
|
87
|
+
if (settled) return;
|
|
88
|
+
settled = true;
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
const settle = () => {
|
|
91
|
+
if (error) reject(error);
|
|
92
|
+
else resolveResult(value);
|
|
93
|
+
};
|
|
94
|
+
if (child.exitCode !== null || child.signalCode !== null) return settle();
|
|
95
|
+
child.once("close", settle);
|
|
96
|
+
child.kill();
|
|
97
|
+
};
|
|
98
|
+
const timer = setTimeout(() => {
|
|
99
|
+
finish(new Error(`${label} MCP initialize timed out${stderr ? `: ${stderr.trim()}` : ""}`));
|
|
100
|
+
}, 3000);
|
|
101
|
+
child.stderr.setEncoding("utf8");
|
|
102
|
+
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
103
|
+
child.stdout.setEncoding("utf8");
|
|
104
|
+
child.stdout.on("data", (chunk) => {
|
|
105
|
+
stdout += chunk;
|
|
106
|
+
const newline = stdout.indexOf("\n");
|
|
107
|
+
if (newline < 0) return;
|
|
108
|
+
try {
|
|
109
|
+
const message = JSON.parse(stdout.slice(0, newline));
|
|
110
|
+
assert(message.id === 1, `${label} MCP initialize returned the wrong request id`);
|
|
111
|
+
assert(message.result?.serverInfo?.name === "agent-spine", `${label} MCP initialize returned the wrong server identity`);
|
|
112
|
+
assert(message.result?.serverInfo?.version === version, `${label} MCP initialize returned a stale server version`);
|
|
113
|
+
finish(null, { label, server: message.result.serverInfo.name, entrypoint: relative(root, args[0]) });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
finish(error);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
child.once("error", (error) => finish(error));
|
|
119
|
+
child.once("close", (code) => {
|
|
120
|
+
if (!settled) finish(new Error(`${label} MCP server exited with ${code}${stderr ? `: ${stderr.trim()}` : ""}`));
|
|
121
|
+
});
|
|
122
|
+
// A real MCP host keeps stdio open after initialization. Closing stdin
|
|
123
|
+
// here let macOS and Windows terminate the server before its queued reply.
|
|
124
|
+
child.stdin.write(`${JSON.stringify({
|
|
125
|
+
jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18" }
|
|
126
|
+
})}\n`);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function checkHosts(root = process.cwd()) {
|
|
131
|
+
root = resolve(root);
|
|
132
|
+
const [pkg, blunManifest, claudeManifest, claudeMcp, codexManifest, claudeHooks, codexHooks, hookVersion] = await Promise.all([
|
|
133
|
+
json(root, "package.json"),
|
|
134
|
+
json(root, "blun.plugin.json"),
|
|
135
|
+
json(root, ".claude-plugin/plugin.json"),
|
|
136
|
+
json(root, ".mcp.json"),
|
|
137
|
+
json(root, ".codex-plugin/plugin.json"),
|
|
138
|
+
json(root, "hooks/hooks.json"),
|
|
139
|
+
json(root, "hooks/codex.json"),
|
|
140
|
+
json(root, "hooks/version.json")
|
|
141
|
+
]);
|
|
142
|
+
assert(blunManifest.version === pkg.version && claudeManifest.version === pkg.version && codexManifest.version === pkg.version,
|
|
143
|
+
"host manifests must use the package cache version");
|
|
144
|
+
assert(hookVersion.schema === "agentspine.hook-bundle/v1" && hookVersion.version === pkg.version,
|
|
145
|
+
"hook bundle version must match the package cache version");
|
|
146
|
+
assert(hookVersion.contract === "agentspine.preflight/v2", "hook bundle preflight contract is missing");
|
|
147
|
+
assert(pkg.bin?.["agentspine-worker"] === "./src/worker.js", "package must register exactly one gateway worker entrypoint");
|
|
148
|
+
await validateEntrypoint(root, resolve(root, pkg.bin["agentspine-worker"]));
|
|
149
|
+
assert(claudeManifest.mcpServers === "./.mcp.json", "Claude manifest must explicitly reference ./.mcp.json");
|
|
150
|
+
assert(claudeManifest.hooks === undefined, "default hooks/hooks.json must not also be registered through a supplemental manifest path");
|
|
151
|
+
assert(codexManifest.hooks === undefined, "Codex manifest must omit unsupported hook registration fields");
|
|
152
|
+
assert(claudeMcp.mcpServers && Object.keys(claudeMcp.mcpServers).length === 1, "Claude MCP file must contain one mcpServers registration");
|
|
153
|
+
assert(codexManifest.mcpServers && Object.keys(codexManifest.mcpServers).length === 1, "Codex manifest must contain one MCP registration");
|
|
154
|
+
const commonEvents = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PreCompact", "PostCompact", "Stop", "SubagentStop"];
|
|
155
|
+
const claudeHookInventory = validateHooks(root, claudeHooks, {
|
|
156
|
+
required: [...commonEvents, "InstructionsLoaded"], commandRoot: "CLAUDE_PLUGIN_ROOT"
|
|
157
|
+
});
|
|
158
|
+
const codexHookInventory = validateHooks(root, codexHooks, { required: commonEvents, commandRoot: "PLUGIN_ROOT" });
|
|
159
|
+
const blunHookInventory = validateBlunHooks(root, blunManifest.hooks);
|
|
160
|
+
const registrations = await Promise.all([
|
|
161
|
+
initializeServer({ label: "blun", root, variable: "BLUN_PLUGIN_ROOT", server: blunManifest.mcpServers["agent-spine"], version: pkg.version }),
|
|
162
|
+
initializeServer({ label: "claude", root, variable: "CLAUDE_PLUGIN_ROOT", server: claudeMcp.mcpServers["agent-spine"], version: pkg.version }),
|
|
163
|
+
initializeServer({ label: "codex", root, variable: "PLUGIN_ROOT", server: codexManifest.mcpServers["agent-spine"], version: pkg.version })
|
|
164
|
+
]);
|
|
165
|
+
return {
|
|
166
|
+
ok: true, root, version: pkg.version, registrations,
|
|
167
|
+
hooks: { blun: blunHookInventory, claude: claudeHookInventory, codex: codexHookInventory },
|
|
168
|
+
hookDiscovery: {
|
|
169
|
+
blun: "plugin-manifest", claude: "default-hooks-directory", codex: "bundled-host-adapter",
|
|
170
|
+
trust: "host-user-required", liveTrustVerified: false
|
|
171
|
+
},
|
|
172
|
+
worker: { entrypoint: pkg.bin["agentspine-worker"], setsPerInstall: 1 },
|
|
173
|
+
exactlyOnce: { mcpServersPerHost: 1, hookSetsPerHost: 1, workerSetsPerInstall: 1 },
|
|
174
|
+
authority: "registration-check-only"
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function main() {
|
|
179
|
+
const args = process.argv.slice(2);
|
|
180
|
+
let root = process.cwd();
|
|
181
|
+
let pretty = false;
|
|
182
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
183
|
+
if (args[index] === "--root") root = args[++index];
|
|
184
|
+
else if (args[index] === "--json") pretty = true;
|
|
185
|
+
else throw new Error(`unknown host-check argument: ${args[index]}`);
|
|
186
|
+
}
|
|
187
|
+
process.stdout.write(`${JSON.stringify(await checkHosts(root), null, pretty ? 2 : 0)}\n`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
|
|
191
|
+
main().catch((error) => {
|
|
192
|
+
process.stderr.write(`AgentSpine host check failed: ${error.message}\n`);
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { randomUUID } = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
function positiveInteger(value, label) {
|
|
6
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
7
|
+
throw new TypeError(`${label} must be a non-negative safe integer`);
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function toolCallId(call) {
|
|
13
|
+
return typeof call?.id === 'string' && call.id.length > 0 ? call.id : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function assertBalancedToolPairs(messages, label = 'compaction history') {
|
|
17
|
+
const pending = new Set();
|
|
18
|
+
for (const message of messages) {
|
|
19
|
+
if (message?.role === 'assistant') {
|
|
20
|
+
for (const call of message.toolCalls ?? []) {
|
|
21
|
+
const id = toolCallId(call);
|
|
22
|
+
if (id === undefined) throw new Error(`${label} contains a tool call without an id`);
|
|
23
|
+
if (pending.has(id)) throw new Error(`${label} contains duplicate tool call id ${id}`);
|
|
24
|
+
pending.add(id);
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (message?.role !== 'tool') continue;
|
|
29
|
+
const id = typeof message.toolCallId === 'string' ? message.toolCallId : undefined;
|
|
30
|
+
if (id === undefined || !pending.delete(id)) {
|
|
31
|
+
throw new Error(`${label} contains an orphan tool result${id ? ` ${id}` : ''}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (pending.size > 0) {
|
|
35
|
+
throw new Error(`${label} contains ${pending.size} tool call(s) without results`);
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createCompactionTransaction({ source, turnId, tokensBefore, messageCountBefore }) {
|
|
41
|
+
const transaction = {
|
|
42
|
+
id: randomUUID(),
|
|
43
|
+
source,
|
|
44
|
+
turnId: turnId ?? null,
|
|
45
|
+
tokensBefore: positiveInteger(tokensBefore, 'tokensBefore'),
|
|
46
|
+
messageCountBefore: positiveInteger(messageCountBefore, 'messageCountBefore'),
|
|
47
|
+
state: 'started',
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
transaction,
|
|
51
|
+
record: {
|
|
52
|
+
type: 'compaction/start',
|
|
53
|
+
compaction_id: transaction.id,
|
|
54
|
+
source: transaction.source,
|
|
55
|
+
turn_id: transaction.turnId,
|
|
56
|
+
size_before: transaction.tokensBefore,
|
|
57
|
+
message_count_before: transaction.messageCountBefore,
|
|
58
|
+
unit: 'tokens',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function summarizeCompactionTransaction(transaction, input) {
|
|
64
|
+
if (transaction?.state !== 'started') {
|
|
65
|
+
throw new Error('compaction/summary requires exactly one open compaction/start');
|
|
66
|
+
}
|
|
67
|
+
if (typeof input.summary !== 'string' || input.summary.trim().length === 0) {
|
|
68
|
+
throw new Error('compaction/summary requires non-empty model-visible summary text');
|
|
69
|
+
}
|
|
70
|
+
assertBalancedToolPairs(input.historyBefore, 'history before compaction');
|
|
71
|
+
assertBalancedToolPairs(input.historyAfter, 'history after compaction');
|
|
72
|
+
transaction.state = 'summarized';
|
|
73
|
+
transaction.tokensAfter = positiveInteger(input.tokensAfter, 'tokensAfter');
|
|
74
|
+
transaction.messageCountAfter = positiveInteger(input.messageCountAfter, 'messageCountAfter');
|
|
75
|
+
return {
|
|
76
|
+
type: 'compaction/summary',
|
|
77
|
+
compaction_id: transaction.id,
|
|
78
|
+
source: transaction.source,
|
|
79
|
+
turn_id: transaction.turnId,
|
|
80
|
+
summary: input.summary,
|
|
81
|
+
shadowed_token_count: transaction.tokensBefore,
|
|
82
|
+
size_before: transaction.tokensBefore,
|
|
83
|
+
size_after: transaction.tokensAfter,
|
|
84
|
+
message_count_before: transaction.messageCountBefore,
|
|
85
|
+
message_count_after: transaction.messageCountAfter,
|
|
86
|
+
tool_pairing_balanced_before: true,
|
|
87
|
+
tool_pairing_balanced_after: true,
|
|
88
|
+
unit: 'tokens',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function endCompactionTransaction(transaction, { status, error } = {}) {
|
|
93
|
+
if (transaction?.state === 'ended') return undefined;
|
|
94
|
+
if (transaction?.state !== 'started' && transaction?.state !== 'summarized') {
|
|
95
|
+
throw new Error('compaction/end requires an open compaction/start');
|
|
96
|
+
}
|
|
97
|
+
const normalizedStatus = status ?? 'success';
|
|
98
|
+
if (normalizedStatus === 'success' && transaction.state !== 'summarized') {
|
|
99
|
+
throw new Error('successful compaction/end requires one compaction/summary');
|
|
100
|
+
}
|
|
101
|
+
if (!['success', 'cancelled', 'failed'].includes(normalizedStatus)) {
|
|
102
|
+
throw new Error(`unsupported compaction status ${normalizedStatus}`);
|
|
103
|
+
}
|
|
104
|
+
transaction.state = 'ended';
|
|
105
|
+
return {
|
|
106
|
+
type: 'compaction/end',
|
|
107
|
+
compaction_id: transaction.id,
|
|
108
|
+
source: transaction.source,
|
|
109
|
+
turn_id: transaction.turnId,
|
|
110
|
+
status: normalizedStatus,
|
|
111
|
+
...(transaction.tokensAfter === undefined ? {} : { size_after: transaction.tokensAfter }),
|
|
112
|
+
...(typeof error === 'string' && error.length > 0 ? { error } : {}),
|
|
113
|
+
unit: 'tokens',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
assertBalancedToolPairs,
|
|
119
|
+
createCompactionTransaction,
|
|
120
|
+
endCompactionTransaction,
|
|
121
|
+
summarizeCompactionTransaction,
|
|
122
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MODEL_ALIAS = 'blun/king';
|
|
4
|
+
const DEFAULT_MAX_OUTPUT_SIZE = 128000;
|
|
5
|
+
|
|
6
|
+
function applyDefaultModelOutputBudget(config) {
|
|
7
|
+
const models = config?.models;
|
|
8
|
+
const model = models?.[DEFAULT_MODEL_ALIAS];
|
|
9
|
+
|
|
10
|
+
if (!model || model.maxOutputSize !== undefined) return config;
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
...config,
|
|
14
|
+
models: {
|
|
15
|
+
...models,
|
|
16
|
+
[DEFAULT_MODEL_ALIAS]: {
|
|
17
|
+
...model,
|
|
18
|
+
maxOutputSize: DEFAULT_MAX_OUTPUT_SIZE,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
DEFAULT_MAX_OUTPUT_SIZE,
|
|
26
|
+
DEFAULT_MODEL_ALIAS,
|
|
27
|
+
applyDefaultModelOutputBudget,
|
|
28
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function versionFromStat(stat) {
|
|
4
|
+
if (!stat || typeof stat !== 'object') return null;
|
|
5
|
+
return [
|
|
6
|
+
stat.stDev ?? null,
|
|
7
|
+
stat.stIno ?? null,
|
|
8
|
+
stat.stSize ?? null,
|
|
9
|
+
stat.stMtime ?? null,
|
|
10
|
+
].join(':');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isNotFoundError(error) {
|
|
14
|
+
return error?.code === 'ENOENT' || error?.code === 'ENOTDIR';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createFileObservationPolicy(options = {}) {
|
|
18
|
+
const caseInsensitive = options.pathClass === 'win32';
|
|
19
|
+
const observations = new Map();
|
|
20
|
+
|
|
21
|
+
function key(filePath) {
|
|
22
|
+
const normalized = String(filePath).replaceAll('\\', '/');
|
|
23
|
+
return caseInsensitive ? normalized.toLowerCase() : normalized;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function recordPresent(filePath, stat) {
|
|
27
|
+
const version = versionFromStat(stat);
|
|
28
|
+
if (version === null) throw new TypeError('recordPresent requires a file stat');
|
|
29
|
+
observations.set(key(filePath), { kind: 'present', version });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function recordAbsent(filePath) {
|
|
33
|
+
observations.set(key(filePath), { kind: 'absent' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function verifyStableRead(filePath, before, after) {
|
|
37
|
+
const beforeVersion = versionFromStat(before);
|
|
38
|
+
const afterVersion = versionFromStat(after);
|
|
39
|
+
if (beforeVersion === null || afterVersion === null || beforeVersion !== afterVersion) {
|
|
40
|
+
return {
|
|
41
|
+
allowed: false,
|
|
42
|
+
code: 'FS_STALE_VERSION',
|
|
43
|
+
error: `File changed while reading "${filePath}". Read it again before editing or overwriting it.`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
recordPresent(filePath, after);
|
|
47
|
+
return { allowed: true, version: afterVersion };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function authorizeEdit(filePath, currentStat) {
|
|
51
|
+
const prior = observations.get(key(filePath));
|
|
52
|
+
if (currentStat === null) {
|
|
53
|
+
return {
|
|
54
|
+
allowed: false,
|
|
55
|
+
code: prior?.kind === 'absent' ? 'FS_NOT_FOUND' : 'FS_NOT_OBSERVED',
|
|
56
|
+
error: prior?.kind === 'absent'
|
|
57
|
+
? `Cannot edit "${filePath}": the last Read confirmed that it does not exist.`
|
|
58
|
+
: `Edit requires reading "${filePath}" first. Read the exact target path, then retry.`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (prior === undefined) {
|
|
62
|
+
return {
|
|
63
|
+
allowed: false,
|
|
64
|
+
code: 'FS_NOT_OBSERVED',
|
|
65
|
+
error: `Edit requires reading "${filePath}" first. Read the exact target path, then retry.`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (prior.kind !== 'present' || prior.version !== versionFromStat(currentStat)) {
|
|
69
|
+
return {
|
|
70
|
+
allowed: false,
|
|
71
|
+
code: 'FS_STALE_VERSION',
|
|
72
|
+
error: `"${filePath}" changed since the last Read. Read it again, then retry the Edit.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return { allowed: true, version: prior.version };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function authorizeWrite(filePath, currentStat) {
|
|
79
|
+
const prior = observations.get(key(filePath));
|
|
80
|
+
if (currentStat === null) {
|
|
81
|
+
if (prior?.kind === 'present') {
|
|
82
|
+
return {
|
|
83
|
+
allowed: false,
|
|
84
|
+
code: 'FS_STALE_VERSION',
|
|
85
|
+
error: `"${filePath}" disappeared since the last Read. Read it again before recreating it.`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return { allowed: true, version: null, create: true };
|
|
89
|
+
}
|
|
90
|
+
if (prior === undefined) {
|
|
91
|
+
return {
|
|
92
|
+
allowed: false,
|
|
93
|
+
code: 'FS_NOT_OBSERVED',
|
|
94
|
+
error: `Write would replace or append to existing file "${filePath}" without a prior Read. Read it first, then retry.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (prior.kind !== 'present' || prior.version !== versionFromStat(currentStat)) {
|
|
98
|
+
return {
|
|
99
|
+
allowed: false,
|
|
100
|
+
code: 'FS_STALE_VERSION',
|
|
101
|
+
error: `"${filePath}" changed since the last Read. Read it again, then retry the Write.`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return { allowed: true, version: prior.version, create: false };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function verifyUnchanged(filePath, expectedVersion, currentStat) {
|
|
108
|
+
const currentVersion = versionFromStat(currentStat);
|
|
109
|
+
if (expectedVersion === currentVersion) return { allowed: true };
|
|
110
|
+
return {
|
|
111
|
+
allowed: false,
|
|
112
|
+
code: 'FS_STALE_VERSION',
|
|
113
|
+
error: expectedVersion === null
|
|
114
|
+
? `"${filePath}" appeared before the new file could be created. Read it before deciding whether to overwrite it.`
|
|
115
|
+
: `"${filePath}" changed before the mutation was written. Read it again, then retry.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
authorizeEdit,
|
|
121
|
+
authorizeWrite,
|
|
122
|
+
recordAbsent,
|
|
123
|
+
recordPresent,
|
|
124
|
+
verifyStableRead,
|
|
125
|
+
verifyUnchanged,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
createFileObservationPolicy,
|
|
131
|
+
isNotFoundError,
|
|
132
|
+
versionFromStat,
|
|
133
|
+
};
|
package/bin/launcher-runtime.js
CHANGED
|
@@ -539,7 +539,6 @@ async function runLauncher(options = {}) {
|
|
|
539
539
|
env.BLUN_SHARED_HOME = privatePaths.sharedHome;
|
|
540
540
|
env.BLUN_LOG_HOME = privatePaths.sharedHome;
|
|
541
541
|
env.BLUN_PROFILE = PROFILE.profileName;
|
|
542
|
-
env.BLUN_MODEL_MAX_COMPLETION_TOKENS = env.BLUN_MODEL_MAX_COMPLETION_TOKENS || '32768';
|
|
543
542
|
const mnemoConnect = startMnemoConnectHeartbeat({
|
|
544
543
|
env,
|
|
545
544
|
profileName: PROFILE.profileName,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { createHash } = require('node:crypto');
|
|
4
|
+
|
|
3
5
|
const MICRO_COMPACTION_PRESSURE_RATIO = 0.75;
|
|
4
6
|
const MICRO_COMPACTION_MIN_ADVANCE_MESSAGES = 20;
|
|
5
7
|
const MICRO_COMPACTION_RECENT_MESSAGES = 4;
|
|
@@ -46,6 +48,66 @@ function selectMicroCompactionCutoff(options = {}) {
|
|
|
46
48
|
};
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
function redundantHistoricalToolResultIds(messages, cutoff) {
|
|
52
|
+
return new Set(redundantHistoricalToolResultReferences(messages, cutoff).keys());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function redundantHistoricalToolResultReferences(messages, cutoff) {
|
|
56
|
+
if (!Array.isArray(messages)) return new Map();
|
|
57
|
+
|
|
58
|
+
const historicalCutoff = Math.min(messages.length, nonNegativeInteger(cutoff));
|
|
59
|
+
const callSignatures = new Map();
|
|
60
|
+
for (const message of messages) {
|
|
61
|
+
if (message?.role !== 'assistant' || !Array.isArray(message.toolCalls)) continue;
|
|
62
|
+
for (const call of message.toolCalls) {
|
|
63
|
+
if (
|
|
64
|
+
typeof call?.id !== 'string'
|
|
65
|
+
|| typeof call.name !== 'string'
|
|
66
|
+
|| typeof call.arguments !== 'string'
|
|
67
|
+
) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
callSignatures.set(call.id, JSON.stringify([call.name, call.arguments]));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const resultsBySignature = new Map();
|
|
75
|
+
for (let index = 0; index < messages.length; index++) {
|
|
76
|
+
const message = messages[index];
|
|
77
|
+
if (message?.role !== 'tool' || typeof message.toolCallId !== 'string') continue;
|
|
78
|
+
const callSignature = callSignatures.get(message.toolCallId);
|
|
79
|
+
if (callSignature === undefined || !Array.isArray(message.content)) continue;
|
|
80
|
+
|
|
81
|
+
let resultSignature;
|
|
82
|
+
try {
|
|
83
|
+
resultSignature = createHash('sha256')
|
|
84
|
+
.update(callSignature)
|
|
85
|
+
.update('\0')
|
|
86
|
+
.update(JSON.stringify(message.content))
|
|
87
|
+
.digest('hex');
|
|
88
|
+
} catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const entries = resultsBySignature.get(resultSignature) ?? [];
|
|
93
|
+
entries.push({ index, toolCallId: message.toolCallId });
|
|
94
|
+
resultsBySignature.set(resultSignature, entries);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const redundantIds = new Map();
|
|
98
|
+
for (const entries of resultsBySignature.values()) {
|
|
99
|
+
if (entries.length < 2) continue;
|
|
100
|
+
const newestToolCallId = entries.at(-1)?.toolCallId;
|
|
101
|
+
if (newestToolCallId === undefined) continue;
|
|
102
|
+
for (const entry of entries.slice(0, -1)) {
|
|
103
|
+
if (entry.index < historicalCutoff) {
|
|
104
|
+
redundantIds.set(entry.toolCallId, newestToolCallId);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return redundantIds;
|
|
109
|
+
}
|
|
110
|
+
|
|
49
111
|
function finiteOr(value, fallback) {
|
|
50
112
|
const number = Number(value);
|
|
51
113
|
return Number.isFinite(number) ? number : fallback;
|
|
@@ -77,5 +139,7 @@ module.exports = {
|
|
|
77
139
|
MICRO_COMPACTION_RECENT_MESSAGES,
|
|
78
140
|
MICRO_COMPACTION_TOOL_ARGUMENTS_ENABLED,
|
|
79
141
|
MICRO_COMPACTION_TOOL_ARGUMENT_MIN_TOKENS,
|
|
142
|
+
redundantHistoricalToolResultIds,
|
|
143
|
+
redundantHistoricalToolResultReferences,
|
|
80
144
|
selectMicroCompactionCutoff,
|
|
81
145
|
};
|
|
@@ -5,7 +5,6 @@ const os = require('node:os');
|
|
|
5
5
|
|
|
6
6
|
const DEFAULT_HEARTBEAT_MS = 60_000;
|
|
7
7
|
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
8
|
-
const DEFAULT_INTERNAL_HUB_URL = 'http://100.85.21.103:7117';
|
|
9
8
|
|
|
10
9
|
function resolveMnemoHubUrl(env = process.env) {
|
|
11
10
|
const raw = [
|
|
@@ -51,7 +50,7 @@ function resolveMnemoProfileConnectConfig(configPath) {
|
|
|
51
50
|
].some((value) => typeof value === 'string' && value.trim().length > 0);
|
|
52
51
|
const baseUrl = resolveMnemoHubUrl(server.env);
|
|
53
52
|
if (configuredUrl && !baseUrl) return null;
|
|
54
|
-
return { agentName, baseUrl
|
|
53
|
+
return baseUrl ? { agentName, baseUrl } : null;
|
|
55
54
|
} catch {
|
|
56
55
|
return null;
|
|
57
56
|
}
|
|
@@ -196,7 +195,6 @@ function startMnemoConnectHeartbeat(options = {}) {
|
|
|
196
195
|
|
|
197
196
|
module.exports = {
|
|
198
197
|
DEFAULT_HEARTBEAT_MS,
|
|
199
|
-
DEFAULT_INTERNAL_HUB_URL,
|
|
200
198
|
DEFAULT_TIMEOUT_MS,
|
|
201
199
|
postMnemoTool,
|
|
202
200
|
resolveMnemoAgentName,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
async function persistRetrySchedule(input = {}) {
|
|
4
|
+
const { dispatchRetrying, flush, event, signal } = input;
|
|
5
|
+
if (typeof dispatchRetrying !== 'function') throw new TypeError('persistRetrySchedule requires dispatchRetrying');
|
|
6
|
+
if (typeof flush !== 'function') throw new TypeError('persistRetrySchedule requires flush');
|
|
7
|
+
signal?.throwIfAborted?.();
|
|
8
|
+
await dispatchRetrying(event);
|
|
9
|
+
await flush();
|
|
10
|
+
signal?.throwIfAborted?.();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = { persistRetrySchedule };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const TOOL_ABORTED_BEFORE_DISPATCH = 'TOOL_ABORTED_BEFORE_DISPATCH';
|
|
4
|
+
|
|
5
|
+
async function checkpointBeforeExternalSideEffect(options = {}) {
|
|
6
|
+
const flush = options.flush;
|
|
7
|
+
const signal = options.signal;
|
|
8
|
+
if (typeof flush !== 'function') {
|
|
9
|
+
throw new TypeError('checkpointBeforeExternalSideEffect requires flush');
|
|
10
|
+
}
|
|
11
|
+
await flush();
|
|
12
|
+
if (signal?.aborted === true) {
|
|
13
|
+
return {
|
|
14
|
+
allowed: false,
|
|
15
|
+
code: TOOL_ABORTED_BEFORE_DISPATCH,
|
|
16
|
+
error: 'Tool call aborted before dispatch while its durable checkpoint was being written.',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return { allowed: true };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
TOOL_ABORTED_BEFORE_DISPATCH,
|
|
24
|
+
checkpointBeforeExternalSideEffect,
|
|
25
|
+
};
|