blun-king-cli 9.1.486 → 9.1.489
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/CHANGELOG.md +14 -0
- package/LIESMICH.txt +28 -7
- package/README.md +16 -3
- package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -0
- package/agent-spine-plugin/.claude-plugin/plugin.json +14 -0
- package/agent-spine-plugin/.codex-plugin/plugin.json +34 -0
- package/agent-spine-plugin/.mcp.json +8 -0
- package/agent-spine-plugin/CHANGELOG.md +277 -0
- package/agent-spine-plugin/LICENSE +186 -0
- package/agent-spine-plugin/README.md +342 -0
- package/agent-spine-plugin/assets/agentspine-banner.svg +32 -0
- package/agent-spine-plugin/bin/agentspine-mcp.js +4 -0
- package/agent-spine-plugin/bin/agentspine.js +7 -0
- package/agent-spine-plugin/blun.plugin.json +33 -0
- package/agent-spine-plugin/docs/acceptance.md +61 -0
- package/agent-spine-plugin/docs/architecture.md +183 -0
- package/agent-spine-plugin/docs/attention.md +121 -0
- package/agent-spine-plugin/docs/automatic-continuity.md +79 -0
- package/agent-spine-plugin/docs/channel-runtime.md +92 -0
- package/agent-spine-plugin/docs/coordination.md +138 -0
- package/agent-spine-plugin/docs/feed-transport.md +99 -0
- package/agent-spine-plugin/docs/gateway-runtime.md +109 -0
- package/agent-spine-plugin/docs/harness-reference.md +45 -0
- package/agent-spine-plugin/docs/host-integration.md +129 -0
- package/agent-spine-plugin/docs/https-transport.md +116 -0
- package/agent-spine-plugin/docs/learning.md +98 -0
- package/agent-spine-plugin/docs/object-transport.md +93 -0
- package/agent-spine-plugin/docs/peer-transport.md +88 -0
- package/agent-spine-plugin/docs/preservation-contract.md +53 -0
- package/agent-spine-plugin/docs/quality-gates.md +50 -0
- package/agent-spine-plugin/docs/relationships.md +67 -0
- package/agent-spine-plugin/docs/releasing.md +83 -0
- package/agent-spine-plugin/docs/roadmap.md +307 -0
- package/agent-spine-plugin/docs/selfstarter.md +88 -0
- package/agent-spine-plugin/docs/session-briefing.md +74 -0
- package/agent-spine-plugin/docs/shared-memory.md +259 -0
- package/agent-spine-plugin/docs/source-roots.md +86 -0
- package/agent-spine-plugin/docs/sqlite-transport.md +76 -0
- package/agent-spine-plugin/hooks/hooks.json +95 -0
- package/agent-spine-plugin/hooks/version.json +5 -0
- package/agent-spine-plugin/package.json +69 -0
- package/agent-spine-plugin/scripts/check-hosts.js +193 -0
- package/agent-spine-plugin/scripts/check-install.js +551 -0
- package/agent-spine-plugin/scripts/check-syntax.js +29 -0
- package/agent-spine-plugin/scripts/github-actions.js +11 -0
- package/agent-spine-plugin/scripts/release-check.js +127 -0
- package/agent-spine-plugin/scripts/run-acceptance.js +19 -0
- package/agent-spine-plugin/scripts/run-checks.js +46 -0
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +73 -0
- package/agent-spine-plugin/skill/SKILL.md +76 -0
- package/agent-spine-plugin/skills/agent-spine/SKILL.md +84 -0
- package/agent-spine-plugin/spine-example/1-identity.md +12 -0
- package/agent-spine-plugin/spine-example/2-voice.md +6 -0
- package/agent-spine-plugin/spine-example/3-conduct.md +8 -0
- package/agent-spine-plugin/spine-example/4-history.md +4 -0
- package/agent-spine-plugin/src/cli.js +950 -0
- package/agent-spine-plugin/src/hook.js +646 -0
- package/agent-spine-plugin/src/index.js +84 -0
- package/agent-spine-plugin/src/lib/acceptance.js +294 -0
- package/agent-spine-plugin/src/lib/attention.js +755 -0
- package/agent-spine-plugin/src/lib/audit.js +308 -0
- package/agent-spine-plugin/src/lib/authentication.js +513 -0
- package/agent-spine-plugin/src/lib/briefing.js +305 -0
- package/agent-spine-plugin/src/lib/catalog.js +142 -0
- package/agent-spine-plugin/src/lib/channel-runtime.js +665 -0
- package/agent-spine-plugin/src/lib/context.js +154 -0
- package/agent-spine-plugin/src/lib/continuity.js +333 -0
- package/agent-spine-plugin/src/lib/coordination.js +577 -0
- package/agent-spine-plugin/src/lib/documents.js +199 -0
- package/agent-spine-plugin/src/lib/feed-transport.js +497 -0
- package/agent-spine-plugin/src/lib/filesystem-retry.js +27 -0
- package/agent-spine-plugin/src/lib/gateway-runtime.js +760 -0
- package/agent-spine-plugin/src/lib/graph.js +284 -0
- package/agent-spine-plugin/src/lib/https-transport.js +392 -0
- package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -0
- package/agent-spine-plugin/src/lib/indexed-memory.js +281 -0
- package/agent-spine-plugin/src/lib/learning.js +645 -0
- package/agent-spine-plugin/src/lib/object-transport.js +206 -0
- package/agent-spine-plugin/src/lib/paths.js +60 -0
- package/agent-spine-plugin/src/lib/peer-transport.js +283 -0
- package/agent-spine-plugin/src/lib/persona-runtime.js +503 -0
- package/agent-spine-plugin/src/lib/runtime.js +13 -0
- package/agent-spine-plugin/src/lib/selfstarter.js +819 -0
- package/agent-spine-plugin/src/lib/sharing.js +969 -0
- package/agent-spine-plugin/src/lib/source-roots.js +451 -0
- package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -0
- package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -0
- package/agent-spine-plugin/src/lib/voice-runtime.js +39 -0
- package/agent-spine-plugin/src/mcp.js +526 -0
- package/agent-spine-plugin/src/version.js +1 -0
- package/agent-spine-plugin/src/worker.js +178 -0
- package/bin/identity-context-policy.cjs +51 -1
- package/bin/launcher-runtime.js +42 -1
- package/bin/plugin-bootstrap.js +68 -27
- package/bin/repeated-user-message-projection.cjs +6 -0
- package/bin/turn-thinking-policy.cjs +5 -4
- package/bin/turn-tool-performance-policy.cjs +16 -2
- package/blun.mjs +187 -35
- package/package.json +3 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { watch } from "node:fs";
|
|
4
|
+
import { realpath } from "node:fs/promises";
|
|
5
|
+
import { isAbsolute, resolve } from "node:path";
|
|
6
|
+
import { claimGatewayWork, completeGatewayRun, deliverPrepared, failGatewayRun, loadGatewayRuntime, reconcileGateway, updateGatewayHealth } from "./lib/gateway-runtime.js";
|
|
7
|
+
import { createTelegramAdapter } from "./lib/telegram-adapter.js";
|
|
8
|
+
import { acknowledgeChannelDelivery, loadChannelRuntime } from "./lib/channel-runtime.js";
|
|
9
|
+
import { loadPersonaRuntime, syncPersonaRosterFromEnvironment } from "./lib/persona-runtime.js";
|
|
10
|
+
import { isMainModule } from "./lib/runtime.js";
|
|
11
|
+
|
|
12
|
+
const MAX_FRAME = 64 * 1024;
|
|
13
|
+
const WAKE_FILES = new Set(["attention.json", "channel-runtime.json", "gateway-policy.json", "persona-policy.json", "persona-runtime.json"]);
|
|
14
|
+
|
|
15
|
+
export async function waitForGatewayWake(root, delayMs, { watchFactory = watch, realpathFactory = realpath } = {}) {
|
|
16
|
+
const delay = Math.max(250, Math.min(60000, Number(delayMs) || 60000));
|
|
17
|
+
const { directory } = await loadGatewayRuntime(root);
|
|
18
|
+
let watchPath;
|
|
19
|
+
try { watchPath = await realpathFactory(directory); } catch { return "watch-unavailable"; }
|
|
20
|
+
return new Promise((resolvePromise) => {
|
|
21
|
+
let settled = false; let watcher;
|
|
22
|
+
const finish = (reason) => {
|
|
23
|
+
if (settled) return;
|
|
24
|
+
settled = true; clearTimeout(timer);
|
|
25
|
+
watcher?.close();
|
|
26
|
+
resolvePromise(reason);
|
|
27
|
+
};
|
|
28
|
+
const timer = setTimeout(() => finish("timer"), delay);
|
|
29
|
+
try {
|
|
30
|
+
watcher = watchFactory(watchPath, { persistent: true }, (_eventType, filename) => {
|
|
31
|
+
const name = filename === null ? null : String(filename);
|
|
32
|
+
if (name === null || WAKE_FILES.has(name)) finish("event");
|
|
33
|
+
});
|
|
34
|
+
watcher.on?.("error", () => finish("watch-error"));
|
|
35
|
+
} catch { finish("watch-unavailable"); }
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function exactRunner(env) {
|
|
40
|
+
const runner = env.AGENTSPINE_HOST_RUNNER;
|
|
41
|
+
if (!runner || !isAbsolute(runner)) throw new Error("AGENTSPINE_HOST_RUNNER must be an absolute locally approved executable path");
|
|
42
|
+
return resolve(runner);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function invokeHostRunner(item, { env = process.env, timeoutMs = 300000, spawnProcess = spawn } = {}) {
|
|
46
|
+
const executable = exactRunner(env);
|
|
47
|
+
return new Promise((resolvePromise, reject) => {
|
|
48
|
+
const child = spawnProcess(executable, [], {
|
|
49
|
+
shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"],
|
|
50
|
+
env: Object.fromEntries(["PATH", "Path", "HOME", "USERPROFILE", "SystemRoot", "SYSTEMROOT", "WINDIR",
|
|
51
|
+
"AGENTSPINE_STATE_DIR", "CLAUDE_CONFIG_DIR", "CODEX_HOME"].filter((key) => env[key] !== undefined).map((key) => [key, env[key]]))
|
|
52
|
+
});
|
|
53
|
+
let stdout = Buffer.alloc(0); let stderr = Buffer.alloc(0); let settled = false;
|
|
54
|
+
const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); error ? reject(error) : resolvePromise(value); };
|
|
55
|
+
child.stdout.on("data", (chunk) => { stdout = Buffer.concat([stdout, chunk]); if (stdout.length > MAX_FRAME) child.kill(); });
|
|
56
|
+
child.stderr.on("data", (chunk) => { stderr = Buffer.concat([stderr, chunk]).subarray(0, 2048); });
|
|
57
|
+
child.on("error", (error) => finish(error));
|
|
58
|
+
child.on("close", (code) => {
|
|
59
|
+
if (stdout.length > MAX_FRAME) return finish(new Error("host runner response exceeds 64 KiB"));
|
|
60
|
+
if (code !== 0) return finish(new Error("host runner failed: " + stderr.toString("utf8").slice(0, 2048)));
|
|
61
|
+
try { finish(null, JSON.parse(stdout.toString("utf8"))); } catch { finish(new Error("host runner returned invalid JSON")); }
|
|
62
|
+
});
|
|
63
|
+
const timer = setTimeout(() => { child.kill(); finish(new Error("host runner exceeded its bounded timeout")); }, timeoutMs);
|
|
64
|
+
child.stdin.end(JSON.stringify({ schema: "agentspine.run-request/v1", item, authority: "execution-state-only" }) + "\n");
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function hostWorkItem(root, item) {
|
|
69
|
+
const [{ policy }, personas, channel] = await Promise.all([
|
|
70
|
+
loadGatewayRuntime(root), loadPersonaRuntime(root), loadChannelRuntime(root)
|
|
71
|
+
]);
|
|
72
|
+
const persona = personas.runtime.personas.find((entry) => entry.personaId === item.agentId && entry.status === "active");
|
|
73
|
+
const identity = personas.policy.bindings.find((entry) => entry.id === persona?.bindingId && entry.active);
|
|
74
|
+
if (!persona || !identity) throw new Error("claimed work lost its authenticated agent identity");
|
|
75
|
+
const goal = item.goalId === null ? null : policy.goals.find((entry) => entry.goalId === item.goalId) || null;
|
|
76
|
+
const event = item.channelEventId === null ? null
|
|
77
|
+
: channel.runtime.events.find((entry) => entry.eventId === item.channelEventId) || null;
|
|
78
|
+
if (item.channelEventId && !event) throw new Error("claimed channel work lost its exact event");
|
|
79
|
+
return {
|
|
80
|
+
...structuredClone(item), host: identity.host, profileId: identity.profileId, projectRoot: root,
|
|
81
|
+
goal: goal ? structuredClone(goal) : null,
|
|
82
|
+
hostEnvironment: {
|
|
83
|
+
AGENTSPINE_GATEWAY_CONTEXT: "agentspine.gateway-start/v1",
|
|
84
|
+
AGENTSPINE_ENTITY_ID: item.agentId,
|
|
85
|
+
AGENTSPINE_PROJECT_ID: item.projectId,
|
|
86
|
+
...(item.groupId === null ? {} : { AGENTSPINE_GROUP_ID: item.groupId }),
|
|
87
|
+
...(event ? {
|
|
88
|
+
AGENTSPINE_CHANNEL_EVENT_ID: event.eventId,
|
|
89
|
+
AGENTSPINE_CHANNEL_PROVIDER: event.provider
|
|
90
|
+
} : {})
|
|
91
|
+
},
|
|
92
|
+
channelStart: event ? {
|
|
93
|
+
agent_spine_scope: { entity_id: event.agentId, project_id: event.projectId, group_id: event.groupId },
|
|
94
|
+
agent_spine_channel_event: { event_id: event.eventId, provider: event.provider },
|
|
95
|
+
session_key: event.sessionKey,
|
|
96
|
+
authority: "explicit-local-channel-policy"
|
|
97
|
+
} : null
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function runWorkerTick({ root = process.cwd(), workerId = "gateway-worker:local", now = new Date(),
|
|
102
|
+
hostRunner = invokeHostRunner, adapter = null, env = process.env } = {}) {
|
|
103
|
+
const deliveryAdapter = adapter || createTelegramAdapter({ root, env });
|
|
104
|
+
await syncPersonaRosterFromEnvironment({ root, env, now });
|
|
105
|
+
const initial = await loadGatewayRuntime(root);
|
|
106
|
+
if (!initial.policy.enabled || initial.policy.killSwitch) return { status: "stopped", processed: false };
|
|
107
|
+
if (typeof deliveryAdapter.poll === "function") {
|
|
108
|
+
try { await deliveryAdapter.poll(); }
|
|
109
|
+
catch (error) {
|
|
110
|
+
await updateGatewayHealth({ root, worker: "degraded", adapter: "failed", now });
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
await reconcileGateway({ root, now });
|
|
115
|
+
await updateGatewayHealth({ root, worker: "healthy", adapter: "healthy", now });
|
|
116
|
+
const afterReconcile = await loadGatewayRuntime(root);
|
|
117
|
+
const dueOutbox = afterReconcile.runtime.outbox.filter((item) => ["prepared", "failed"].includes(item.status)
|
|
118
|
+
&& new Date(item.nextAttemptAt) <= new Date(now)).sort((a, b) => a.nextAttemptAt.localeCompare(b.nextAttemptAt)
|
|
119
|
+
|| a.createdAt.localeCompare(b.createdAt) || a.outboxId.localeCompare(b.outboxId))[0] || null;
|
|
120
|
+
if (dueOutbox) {
|
|
121
|
+
const delivery = await deliverPrepared({ root, outboxId: dueOutbox.outboxId, adapter: deliveryAdapter, now });
|
|
122
|
+
if (delivery.outbox.status === "delivered") await acknowledgeChannelDelivery({ root,
|
|
123
|
+
eventId: delivery.outbox.eventId, bindingId: delivery.outbox.bindingId,
|
|
124
|
+
deliveryReceiptId: delivery.receipt.id, now });
|
|
125
|
+
return { status: delivery.outbox.status, processed: true, outboxId: delivery.outbox.outboxId,
|
|
126
|
+
recoveredDelivery: true };
|
|
127
|
+
}
|
|
128
|
+
const claim = await claimGatewayWork({ root, workerId, now });
|
|
129
|
+
if (!claim.item) return { status: claim.reason, processed: false };
|
|
130
|
+
let result;
|
|
131
|
+
try { result = await hostRunner(await hostWorkItem(root, claim.item), { env }); }
|
|
132
|
+
catch (error) {
|
|
133
|
+
const failed = await failGatewayRun({ root, queueId: claim.item.queueId, workerId,
|
|
134
|
+
error: "Host runtime unavailable: " + String(error.message).slice(0, 400), now });
|
|
135
|
+
return { status: failed.item.status, processed: true, queueId: failed.item.queueId, retryAt: failed.item.availableAt };
|
|
136
|
+
}
|
|
137
|
+
const completed = await completeGatewayRun({ root, queueId: claim.item.queueId, workerId, result, now });
|
|
138
|
+
if (!completed.outbox) return { status: completed.item.status, processed: true, queueId: completed.item.queueId };
|
|
139
|
+
const delivery = await deliverPrepared({ root, outboxId: completed.outbox.outboxId,
|
|
140
|
+
adapter: deliveryAdapter, now });
|
|
141
|
+
if (delivery.outbox.status === "delivered") await acknowledgeChannelDelivery({ root,
|
|
142
|
+
eventId: delivery.outbox.eventId, bindingId: delivery.outbox.bindingId,
|
|
143
|
+
deliveryReceiptId: delivery.receipt.id, now });
|
|
144
|
+
return { status: delivery.outbox.status, processed: true, queueId: completed.item.queueId, outboxId: delivery.outbox.outboxId };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function runWorker({ root = process.cwd(), once = false, env = process.env,
|
|
148
|
+
waitForWake = waitForGatewayWake } = {}) {
|
|
149
|
+
const workerId = "gateway-worker:" + process.pid;
|
|
150
|
+
do {
|
|
151
|
+
const result = await runWorkerTick({ root, workerId, env });
|
|
152
|
+
if (once) return result;
|
|
153
|
+
const { policy, runtime } = await loadGatewayRuntime(root);
|
|
154
|
+
if (!policy.enabled || policy.killSwitch) return { status: "stopped", processed: false };
|
|
155
|
+
const pending = [
|
|
156
|
+
...runtime.queue.filter((item) => item.status === "pending").map((item) => new Date(item.availableAt).getTime()),
|
|
157
|
+
...runtime.outbox.filter((item) => ["prepared", "failed"].includes(item.status))
|
|
158
|
+
.map((item) => new Date(item.nextAttemptAt).getTime())
|
|
159
|
+
];
|
|
160
|
+
const delay = pending.length ? Math.max(250, Math.min(60000, Math.min(...pending) - Date.now())) : 60000;
|
|
161
|
+
await waitForWake(root, delay);
|
|
162
|
+
} while (true);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (isMainModule(import.meta.url)) {
|
|
166
|
+
const args = process.argv.slice(2); const once = args.includes("--once");
|
|
167
|
+
const rootIndex = args.indexOf("--root");
|
|
168
|
+
if (rootIndex >= 0 && (!args[rootIndex + 1] || args[rootIndex + 1].startsWith("--"))) throw new Error("--root requires a path");
|
|
169
|
+
const root = rootIndex >= 0 ? args[rootIndex + 1] : process.cwd();
|
|
170
|
+
const rosterIndex = args.indexOf("--persona-roster");
|
|
171
|
+
if (rosterIndex >= 0 && (!args[rosterIndex + 1] || args[rosterIndex + 1].startsWith("--"))) {
|
|
172
|
+
throw new Error("--persona-roster requires a path");
|
|
173
|
+
}
|
|
174
|
+
const env = { ...process.env };
|
|
175
|
+
if (rosterIndex >= 0) env.AGENTSPINE_PERSONA_ROSTER_FILE = args[rosterIndex + 1];
|
|
176
|
+
runWorker({ root, once, env }).then((result) => process.stdout.write(JSON.stringify(result) + "\n"))
|
|
177
|
+
.catch((error) => { process.stderr.write("AgentSpine worker: " + String(error.message).slice(0, 2048) + "\n"); process.exitCode = 1; });
|
|
178
|
+
}
|
|
@@ -21,6 +21,7 @@ const ACTIVITY_FILE_RE = /^\d{4}-\d{2}-\d{2}\.json$/u;
|
|
|
21
21
|
const RELATIONSHIP_CANDIDATE_FILE_RE = /^[a-f0-9]{32}\.json$/u;
|
|
22
22
|
const ACTOR_FILE_RE = /^([A-Za-z0-9][A-Za-z0-9._-]{0,127})\.json$/u;
|
|
23
23
|
const LONG_GAP_MS = 7 * 24 * 60 * 60 * 1000;
|
|
24
|
+
const ESTABLISHED_INTERACTION_DAYS = 3;
|
|
24
25
|
const MAX_COLLEAGUES_IN_CONTEXT = 12;
|
|
25
26
|
|
|
26
27
|
function autoGraphEnabled(env) {
|
|
@@ -468,6 +469,52 @@ function recordInteractionActivity(root, agentId, actorId, interactedAt) {
|
|
|
468
469
|
);
|
|
469
470
|
}
|
|
470
471
|
|
|
472
|
+
function trustedInteractionDayCount(root, agentId, actorId, limit) {
|
|
473
|
+
if (!actorId) return 0;
|
|
474
|
+
const activityRoot = path.resolve(root, 'agents', agentId, 'activity', actorId);
|
|
475
|
+
if (!isInside(root, activityRoot)) return 0;
|
|
476
|
+
try {
|
|
477
|
+
const stat = fs.lstatSync(activityRoot);
|
|
478
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return 0;
|
|
479
|
+
if (!isInside(root, fs.realpathSync(activityRoot))) return 0;
|
|
480
|
+
let count = 0;
|
|
481
|
+
for (const name of fs.readdirSync(activityRoot).filter((entry) => ACTIVITY_FILE_RE.test(entry)).sort()) {
|
|
482
|
+
const entry = readJson(root, ['agents', agentId, 'activity', actorId, name]);
|
|
483
|
+
const interactedAt = trustedTimestamp(entry?.interacted_at);
|
|
484
|
+
if (entry?.version !== 1
|
|
485
|
+
|| entry?.actor_id !== actorId
|
|
486
|
+
|| entry?.source !== 'trusted_channel_metadata'
|
|
487
|
+
|| !interactedAt
|
|
488
|
+
|| activityDate(interactedAt) !== name.slice(0, 10)) continue;
|
|
489
|
+
count += 1;
|
|
490
|
+
if (count >= limit) break;
|
|
491
|
+
}
|
|
492
|
+
return count;
|
|
493
|
+
} catch {
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function promoteReturningRelationship(root, agentId, actorId) {
|
|
499
|
+
const relationship = readJson(root, ['agents', agentId, 'relationships', `${actorId}.json`]);
|
|
500
|
+
if (relationship?.version !== 1
|
|
501
|
+
|| relationship.actor_id !== actorId
|
|
502
|
+
|| relationship.status !== 'new') return false;
|
|
503
|
+
if (trustedInteractionDayCount(root, agentId, actorId, ESTABLISHED_INTERACTION_DAYS)
|
|
504
|
+
< ESTABLISHED_INTERACTION_DAYS) return false;
|
|
505
|
+
return updateJsonAtomically(
|
|
506
|
+
root,
|
|
507
|
+
['agents', agentId, 'relationships', `${actorId}.json`],
|
|
508
|
+
(relationship) => (
|
|
509
|
+
relationship.version === 1
|
|
510
|
+
&& relationship.actor_id === actorId
|
|
511
|
+
&& relationship.status === 'new'
|
|
512
|
+
? { ...relationship, status: 'established' }
|
|
513
|
+
: undefined
|
|
514
|
+
),
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
471
518
|
function previousInteractionAfterLongGap(root, agentId, actorId) {
|
|
472
519
|
if (!actorId) return undefined;
|
|
473
520
|
const activityRoot = path.resolve(root, 'agents', agentId, 'activity', actorId);
|
|
@@ -598,7 +645,10 @@ function recordChannelIdentity(envelope, env = process.env) {
|
|
|
598
645
|
messageId: safeId(meta.message_id),
|
|
599
646
|
receivedAt,
|
|
600
647
|
});
|
|
601
|
-
if (firstSeenAt)
|
|
648
|
+
if (firstSeenAt) {
|
|
649
|
+
recordInteractionActivity(root, agentId, actorId, firstSeenAt);
|
|
650
|
+
if (promoteReturningRelationship(root, agentId, actorId)) updated.push('relationship_status');
|
|
651
|
+
}
|
|
602
652
|
let relationshipMemory;
|
|
603
653
|
let relationshipMemoryUnavailable = false;
|
|
604
654
|
let relationshipNotes = [];
|
package/bin/launcher-runtime.js
CHANGED
|
@@ -18,7 +18,10 @@ const {
|
|
|
18
18
|
launcherHelpText,
|
|
19
19
|
launcherModeFromArgv,
|
|
20
20
|
} = require('./launcher-mode');
|
|
21
|
-
const {
|
|
21
|
+
const {
|
|
22
|
+
installManagedAgentSpinePlugin,
|
|
23
|
+
installManagedTelegramPlugin,
|
|
24
|
+
} = require('./plugin-bootstrap');
|
|
22
25
|
const {
|
|
23
26
|
availableStandardToolNames,
|
|
24
27
|
seedConfiguredMnemoMcp,
|
|
@@ -275,6 +278,42 @@ function installTelegramForProfile(packageRoot, blunDir) {
|
|
|
275
278
|
}
|
|
276
279
|
}
|
|
277
280
|
|
|
281
|
+
function installAgentSpineForProfile(packageRoot, blunDir) {
|
|
282
|
+
installManagedAgentSpinePlugin({ packageRoot, blunDir });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function installAgentSpineForExistingProfiles(packageRoot, sharedHome, currentProfileHome) {
|
|
286
|
+
const profileRoot = path.join(sharedHome, 'profile');
|
|
287
|
+
const profileHomes = new Map();
|
|
288
|
+
const addProfileHome = (profileHome) => {
|
|
289
|
+
const resolved = path.resolve(profileHome);
|
|
290
|
+
const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
291
|
+
profileHomes.set(key, resolved);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
addProfileHome(currentProfileHome);
|
|
295
|
+
if (fs.existsSync(profileRoot)) {
|
|
296
|
+
for (const entry of fs.readdirSync(profileRoot, { withFileTypes: true })) {
|
|
297
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
298
|
+
let profileHome;
|
|
299
|
+
try {
|
|
300
|
+
profileHome = resolveProfilePaths(sharedHome, entry.name).home;
|
|
301
|
+
} catch {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const stat = fs.lstatSync(profileHome);
|
|
305
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
|
|
306
|
+
addProfileHome(profileHome);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const profileHome of profileHomes.values()) {
|
|
311
|
+
ensurePrivateDirectory(profileHome);
|
|
312
|
+
installAgentSpineForProfile(packageRoot, profileHome);
|
|
313
|
+
}
|
|
314
|
+
return [...profileHomes.values()];
|
|
315
|
+
}
|
|
316
|
+
|
|
278
317
|
function createManagedNodeEnvironment(binary, env = process.env, platform = process.platform) {
|
|
279
318
|
const pathImpl = platform === 'win32' ? path.win32 : path.posix;
|
|
280
319
|
const separator = platform === 'win32' ? path.win32.delimiter : path.posix.delimiter;
|
|
@@ -504,6 +543,7 @@ async function runLauncher(options = {}) {
|
|
|
504
543
|
});
|
|
505
544
|
seedConfiguredMnemoMcp({ blunDir, env: process.env });
|
|
506
545
|
seedStandardSkills({ packageRoot: PKG, blunDir: privatePaths.sharedHome });
|
|
546
|
+
installAgentSpineForExistingProfiles(PKG, privatePaths.sharedHome, blunDir);
|
|
507
547
|
if (mode === LAUNCHER_MODES.KING) installTelegramForProfile(PKG, blunDir);
|
|
508
548
|
|
|
509
549
|
const nativeModules = repairConfiguredNativeModules({
|
|
@@ -547,6 +587,7 @@ async function runLauncher(options = {}) {
|
|
|
547
587
|
module.exports = {
|
|
548
588
|
clearStartupUpdateViewport,
|
|
549
589
|
createManagedNodeEnvironment,
|
|
590
|
+
installAgentSpineForExistingProfiles,
|
|
550
591
|
resolveLauncherPrivatePaths,
|
|
551
592
|
runLauncher,
|
|
552
593
|
shouldResumeAfterStartupUpdate,
|
package/bin/plugin-bootstrap.js
CHANGED
|
@@ -75,14 +75,14 @@ function pluginTreeDigest(root) {
|
|
|
75
75
|
return hash.digest('hex');
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function
|
|
79
|
-
const records = installed.plugins.filter((plugin) => plugin.id ===
|
|
78
|
+
function hasCurrentPluginRecord(installed, pluginRoot, pluginId, mcpServerName) {
|
|
79
|
+
const records = installed.plugins.filter((plugin) => plugin.id === pluginId);
|
|
80
80
|
if (records.length !== 1) return false;
|
|
81
|
-
const [
|
|
82
|
-
return
|
|
83
|
-
&&
|
|
84
|
-
&&
|
|
85
|
-
&&
|
|
81
|
+
const [plugin] = records;
|
|
82
|
+
return plugin.root === pluginRoot
|
|
83
|
+
&& plugin.source === 'local-path'
|
|
84
|
+
&& plugin.enabled === true
|
|
85
|
+
&& plugin.capabilities?.mcpServers?.[mcpServerName]?.enabled === true;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
function readInstalledPlugins(filePath) {
|
|
@@ -96,16 +96,16 @@ function readInstalledPlugins(filePath) {
|
|
|
96
96
|
return parsed;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
function updatedInstalledPlugins(installed, pluginRoot, now) {
|
|
100
|
-
const previous = installed.plugins.find((plugin) => plugin.id ===
|
|
99
|
+
function updatedInstalledPlugins(installed, pluginRoot, now, pluginId, mcpServerName) {
|
|
100
|
+
const previous = installed.plugins.find((plugin) => plugin.id === pluginId);
|
|
101
101
|
const previousCapabilities = isObject(previous?.capabilities) ? previous.capabilities : {};
|
|
102
102
|
const previousMcpServers = isObject(previousCapabilities.mcpServers)
|
|
103
103
|
? previousCapabilities.mcpServers : {};
|
|
104
|
-
const
|
|
105
|
-
? previousMcpServers
|
|
106
|
-
const
|
|
104
|
+
const previousMcp = isObject(previousMcpServers[mcpServerName])
|
|
105
|
+
? previousMcpServers[mcpServerName] : {};
|
|
106
|
+
const updatedPlugin = {
|
|
107
107
|
...previous,
|
|
108
|
-
id:
|
|
108
|
+
id: pluginId,
|
|
109
109
|
root: pluginRoot,
|
|
110
110
|
source: 'local-path',
|
|
111
111
|
enabled: true,
|
|
@@ -116,7 +116,7 @@ function updatedInstalledPlugins(installed, pluginRoot, now) {
|
|
|
116
116
|
...previousCapabilities,
|
|
117
117
|
mcpServers: {
|
|
118
118
|
...previousMcpServers,
|
|
119
|
-
|
|
119
|
+
[mcpServerName]: { ...previousMcp, enabled: true },
|
|
120
120
|
},
|
|
121
121
|
},
|
|
122
122
|
};
|
|
@@ -124,14 +124,14 @@ function updatedInstalledPlugins(installed, pluginRoot, now) {
|
|
|
124
124
|
const plugins = [];
|
|
125
125
|
let replaced = false;
|
|
126
126
|
for (const plugin of installed.plugins) {
|
|
127
|
-
if (plugin.id !==
|
|
127
|
+
if (plugin.id !== pluginId) {
|
|
128
128
|
plugins.push(plugin);
|
|
129
129
|
} else if (!replaced) {
|
|
130
|
-
plugins.push(
|
|
130
|
+
plugins.push(updatedPlugin);
|
|
131
131
|
replaced = true;
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
-
if (!replaced) plugins.push(
|
|
134
|
+
if (!replaced) plugins.push(updatedPlugin);
|
|
135
135
|
return { ...installed, version: 1, plugins };
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -149,11 +149,19 @@ function writeInstalledPlugins(filePath, installed) {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
function
|
|
153
|
-
|
|
152
|
+
function installManagedPlugin({
|
|
153
|
+
packageRoot,
|
|
154
|
+
blunDir,
|
|
155
|
+
now = new Date().toISOString(),
|
|
156
|
+
pluginId,
|
|
157
|
+
sourceDirectory,
|
|
158
|
+
mcpServerName,
|
|
159
|
+
displayName,
|
|
160
|
+
}) {
|
|
161
|
+
const sourceRoot = path.join(packageRoot, sourceDirectory);
|
|
154
162
|
const pluginsDir = path.join(blunDir, 'plugins');
|
|
155
163
|
const managedDir = path.join(pluginsDir, 'managed');
|
|
156
|
-
const legacyPluginRoot = path.join(managedDir,
|
|
164
|
+
const legacyPluginRoot = path.join(managedDir, pluginId);
|
|
157
165
|
const installedFile = path.join(pluginsDir, 'installed.json');
|
|
158
166
|
|
|
159
167
|
ensurePrivateDirectory(pluginsDir);
|
|
@@ -162,26 +170,38 @@ function installManagedTelegramPlugin({ packageRoot, blunDir, now = new Date().t
|
|
|
162
170
|
ensurePrivateDirectory(managedDir);
|
|
163
171
|
if (fs.existsSync(legacyPluginRoot)
|
|
164
172
|
&& pluginTreesAreIdentical(sourceRoot, legacyPluginRoot)) {
|
|
165
|
-
if (!
|
|
166
|
-
const updatedInstalled = updatedInstalledPlugins(
|
|
173
|
+
if (!hasCurrentPluginRecord(installed, legacyPluginRoot, pluginId, mcpServerName)) {
|
|
174
|
+
const updatedInstalled = updatedInstalledPlugins(
|
|
175
|
+
installed,
|
|
176
|
+
legacyPluginRoot,
|
|
177
|
+
now,
|
|
178
|
+
pluginId,
|
|
179
|
+
mcpServerName,
|
|
180
|
+
);
|
|
167
181
|
writeInstalledPlugins(installedFile, updatedInstalled);
|
|
168
182
|
}
|
|
169
183
|
return { installedFile, pluginRoot: legacyPluginRoot, reused: true };
|
|
170
184
|
}
|
|
171
185
|
const sourceDigest = pluginTreeDigest(sourceRoot);
|
|
172
|
-
const pluginRoot = path.join(managedDir,
|
|
173
|
-
const updatedInstalled = updatedInstalledPlugins(
|
|
186
|
+
const pluginRoot = path.join(managedDir, `${pluginId}-${sourceDigest}`);
|
|
187
|
+
const updatedInstalled = updatedInstalledPlugins(
|
|
188
|
+
installed,
|
|
189
|
+
pluginRoot,
|
|
190
|
+
now,
|
|
191
|
+
pluginId,
|
|
192
|
+
mcpServerName,
|
|
193
|
+
);
|
|
174
194
|
if (fs.existsSync(pluginRoot)) {
|
|
175
195
|
if (!pluginTreesAreIdentical(sourceRoot, pluginRoot)) {
|
|
176
|
-
throw new Error(`Managed
|
|
196
|
+
throw new Error(`Managed ${displayName} plugin digest mismatch: ${pluginRoot}`);
|
|
177
197
|
}
|
|
178
|
-
if (!
|
|
198
|
+
if (!hasCurrentPluginRecord(installed, pluginRoot, pluginId, mcpServerName)) {
|
|
179
199
|
writeInstalledPlugins(installedFile, updatedInstalled);
|
|
180
200
|
}
|
|
181
201
|
return { installedFile, pluginRoot, reused: true };
|
|
182
202
|
}
|
|
183
203
|
const nonce = `${process.pid}-${randomUUID()}`;
|
|
184
|
-
const stagingRoot = path.join(managedDir,
|
|
204
|
+
const stagingRoot = path.join(managedDir, `.${pluginId}-${nonce}.tmp`);
|
|
185
205
|
let installedNewPlugin = false;
|
|
186
206
|
try {
|
|
187
207
|
fs.cpSync(sourceRoot, stagingRoot, { recursive: true, force: true });
|
|
@@ -201,7 +221,28 @@ function installManagedTelegramPlugin({ packageRoot, blunDir, now = new Date().t
|
|
|
201
221
|
return { installedFile, pluginRoot, reused: false };
|
|
202
222
|
}
|
|
203
223
|
|
|
224
|
+
function installManagedTelegramPlugin(options) {
|
|
225
|
+
return installManagedPlugin({
|
|
226
|
+
...options,
|
|
227
|
+
pluginId: 'telegram',
|
|
228
|
+
sourceDirectory: 'telegram-plugin',
|
|
229
|
+
mcpServerName: 'telegram',
|
|
230
|
+
displayName: 'Telegram',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function installManagedAgentSpinePlugin(options) {
|
|
235
|
+
return installManagedPlugin({
|
|
236
|
+
...options,
|
|
237
|
+
pluginId: 'agent-spine',
|
|
238
|
+
sourceDirectory: 'agent-spine-plugin',
|
|
239
|
+
mcpServerName: 'agent-spine',
|
|
240
|
+
displayName: 'AgentSpine',
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
204
244
|
module.exports = {
|
|
245
|
+
installManagedAgentSpinePlugin,
|
|
205
246
|
installManagedTelegramPlugin,
|
|
206
247
|
readInstalledPlugins,
|
|
207
248
|
};
|
|
@@ -68,6 +68,12 @@ class TelegramDeliveryLedger {
|
|
|
68
68
|
return identity;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
forget(input, origin) {
|
|
72
|
+
const identity = this.identity(input, origin);
|
|
73
|
+
if (!identity) return false;
|
|
74
|
+
return this.identities.delete(identity);
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
snapshot() {
|
|
72
78
|
return [...this.identities];
|
|
73
79
|
}
|
|
@@ -38,10 +38,11 @@ function shouldEnableThinkingForTurn(text, originKind) {
|
|
|
38
38
|
return selectThinkingEffortForTurn(text, originKind) !== "off";
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function selectThinkingEffortForWorkStep(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
function selectThinkingEffortForWorkStep(_stepNumber, _previousToolOutcome) {
|
|
42
|
+
// A successful tool call does not make the next agent decision routine.
|
|
43
|
+
// Preserve the turn-selected effort; short chat and lightweight steers are
|
|
44
|
+
// classified independently before the work loop starts.
|
|
45
|
+
return undefined;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
function capCompletionBudgetForAdaptiveEffort(budget, thinkingEffort) {
|
|
@@ -17,6 +17,12 @@ const TOOL_SEARCH_INTENT_WORDS = new Set([
|
|
|
17
17
|
'find', 'fetch', 'get', 'holen', 'list', 'read', 'search', 'show', 'anzeigen', 'lesen', 'suchen',
|
|
18
18
|
]);
|
|
19
19
|
const TOOL_SEARCH_RELATED_TERM_GROUPS = Object.freeze([
|
|
20
|
+
Object.freeze([
|
|
21
|
+
'checklist', 'list', 'liste', 'listen',
|
|
22
|
+
]),
|
|
23
|
+
Object.freeze([
|
|
24
|
+
'aufgabe', 'aufgaben', 'aufgabenliste', 'todo', 'todos',
|
|
25
|
+
]),
|
|
20
26
|
Object.freeze([
|
|
21
27
|
'current', 'inbox', 'latest', 'message', 'messages', 'nachricht', 'nachrichten', 'queue', 'queued',
|
|
22
28
|
'recent', 'letzte', 'letzten', 'letzter', 'vergangen', 'update', 'updates',
|
|
@@ -40,6 +46,7 @@ const CORE_TOOL_NAMES = Object.freeze([
|
|
|
40
46
|
'Grep',
|
|
41
47
|
'Write',
|
|
42
48
|
'Glob',
|
|
49
|
+
'TodoList',
|
|
43
50
|
'mcp__plugin-telegram_telegram__reply',
|
|
44
51
|
]);
|
|
45
52
|
|
|
@@ -195,7 +202,10 @@ function rankingTokens(value) {
|
|
|
195
202
|
function toolRankingDocument(tool) {
|
|
196
203
|
const name = String(tool?.name || '');
|
|
197
204
|
const leaf = name.split(/__|:/).at(-1) || name;
|
|
198
|
-
const
|
|
205
|
+
const splitToolName = (value) => String(value || '')
|
|
206
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
207
|
+
.replaceAll('_', ' ');
|
|
208
|
+
const nameTokens = rankingTokens(`${splitToolName(name)} ${splitToolName(leaf)}`);
|
|
199
209
|
const descriptionTokens = rankingTokens(tool?.description);
|
|
200
210
|
const parameterText = [];
|
|
201
211
|
const properties = tool?.parameters?.properties;
|
|
@@ -247,14 +257,18 @@ function rankedToolNamesForTurnText(tools, value, options = {}) {
|
|
|
247
257
|
let score = 0;
|
|
248
258
|
let matchedTokens = 0;
|
|
249
259
|
let exactNameMatches = 0;
|
|
260
|
+
let relatedNameMatches = 0;
|
|
250
261
|
for (const token of queryTokens) {
|
|
251
262
|
const tokenScore = tokenMatchScore(document, token);
|
|
252
263
|
if (tokenScore === 0) continue;
|
|
253
264
|
score += tokenScore;
|
|
254
265
|
matchedTokens += 1;
|
|
255
266
|
if (document.nameTokens.has(token)) exactNameMatches += 1;
|
|
267
|
+
else if (relatedSearchTerms(token).some((term) => document.nameTokens.has(term))) relatedNameMatches += 1;
|
|
256
268
|
}
|
|
257
|
-
const highConfidence = matchedTokens >= 2
|
|
269
|
+
const highConfidence = matchedTokens >= 2
|
|
270
|
+
|| (exactNameMatches >= 1 && score >= 10)
|
|
271
|
+
|| (relatedNameMatches >= 1 && score >= 5);
|
|
258
272
|
return highConfidence ? { name: document.name, score, matchedTokens, exactNameMatches } : null;
|
|
259
273
|
}).filter(Boolean).sort((left, right) => (
|
|
260
274
|
right.matchedTokens - left.matchedTokens
|