blun-king-cli 9.1.567 → 9.1.569
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/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
- package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
- package/agent-spine-plugin/.codex-plugin/plugin.json +2 -1
- package/agent-spine-plugin/CHANGELOG.md +1581 -0
- package/agent-spine-plugin/README.md +30 -4
- package/agent-spine-plugin/blun.plugin.json +3 -3
- package/agent-spine-plugin/docs/acceptance.md +61 -0
- package/agent-spine-plugin/docs/assignment-continuation.md +48 -0
- package/agent-spine-plugin/docs/host-integration.md +178 -0
- package/agent-spine-plugin/docs/preflight-recall.md +69 -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/releasing.md +85 -0
- package/agent-spine-plugin/docs/session-timeline.md +251 -0
- package/agent-spine-plugin/docs/source-roots.md +113 -0
- package/agent-spine-plugin/docs/structured-completion.md +67 -0
- package/agent-spine-plugin/docs/world-model.md +94 -0
- package/agent-spine-plugin/hooks/codex.json +2 -2
- package/agent-spine-plugin/hooks/hooks.json +1 -1
- package/agent-spine-plugin/hooks/version.json +1 -1
- package/agent-spine-plugin/package.json +13 -3
- package/agent-spine-plugin/scripts/check-codex-install.js +226 -0
- package/agent-spine-plugin/scripts/check-hosts.js +14 -5
- package/agent-spine-plugin/scripts/check-install-hook.js +226 -0
- package/agent-spine-plugin/scripts/check-install-selfstarter.js +154 -0
- package/agent-spine-plugin/scripts/check-install.js +478 -0
- package/agent-spine-plugin/scripts/check-line-budget.js +58 -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/hermetic-process.js +183 -0
- package/agent-spine-plugin/scripts/release-check.js +145 -0
- package/agent-spine-plugin/scripts/run-acceptance.js +19 -0
- package/agent-spine-plugin/scripts/run-checks.js +47 -0
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +89 -0
- package/agent-spine-plugin/skills/agent-spine/SKILL.md +16 -2
- 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-agent.js +296 -0
- package/agent-spine-plugin/src/cli-attention.js +95 -0
- package/agent-spine-plugin/src/cli-autonomy.js +36 -0
- package/agent-spine-plugin/src/cli-common.js +71 -0
- package/agent-spine-plugin/src/cli-continuity.js +116 -0
- package/agent-spine-plugin/src/cli-core.js +128 -0
- package/agent-spine-plugin/src/cli-diagnostics.js +291 -0
- package/agent-spine-plugin/src/cli-host.js +21 -0
- package/agent-spine-plugin/src/cli-learning.js +305 -0
- package/agent-spine-plugin/src/cli-premortem.js +16 -0
- package/agent-spine-plugin/src/cli-sharing.js +230 -0
- package/agent-spine-plugin/src/cli.js +40 -1350
- package/agent-spine-plugin/src/codex-reader-launcher.js +182 -0
- package/agent-spine-plugin/src/hook.js +185 -567
- package/agent-spine-plugin/src/index.js +16 -2
- package/agent-spine-plugin/src/lib/acceptance.js +113 -11
- package/agent-spine-plugin/src/lib/action-lesson-recall.js +53 -0
- package/agent-spine-plugin/src/lib/attention-context.js +167 -0
- package/agent-spine-plugin/src/lib/attention-events.js +113 -0
- package/agent-spine-plugin/src/lib/attention-privacy.js +72 -0
- package/agent-spine-plugin/src/lib/attention-schema.js +164 -0
- package/agent-spine-plugin/src/lib/attention-storage.js +93 -0
- package/agent-spine-plugin/src/lib/attention.js +9 -600
- package/agent-spine-plugin/src/lib/audit-premortem.js +223 -0
- package/agent-spine-plugin/src/lib/audit.js +56 -6
- package/agent-spine-plugin/src/lib/autonomy-policy.js +110 -0
- package/agent-spine-plugin/src/lib/autonomy-store.js +202 -0
- package/agent-spine-plugin/src/lib/autonomy.js +8 -0
- package/agent-spine-plugin/src/lib/briefing.js +67 -4
- package/agent-spine-plugin/src/lib/catalog-document-read.js +51 -0
- package/agent-spine-plugin/src/lib/catalog.js +1 -1
- package/agent-spine-plugin/src/lib/codex-installation.js +231 -0
- package/agent-spine-plugin/src/lib/codex-skill-installation.js +211 -0
- package/agent-spine-plugin/src/lib/context.js +3 -1
- package/agent-spine-plugin/src/lib/delivery-agent-usage.js +224 -0
- package/agent-spine-plugin/src/lib/delivery-assignment.js +220 -0
- package/agent-spine-plugin/src/lib/delivery-command-actions.js +453 -0
- package/agent-spine-plugin/src/lib/delivery-knowledge.js +78 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-binding.js +107 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-closure.js +171 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-codec.js +45 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-correction.js +101 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-file.js +21 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-index.js +493 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-inspection.js +37 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-recovery.js +120 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-rejection.js +65 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-results.js +28 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-session-guard.js +46 -0
- package/agent-spine-plugin/src/lib/delivery-premortem-write-ledger.js +285 -0
- package/agent-spine-plugin/src/lib/delivery-premortem.js +499 -0
- package/agent-spine-plugin/src/lib/delivery-shell-heredoc.js +115 -0
- package/agent-spine-plugin/src/lib/delivery-shell-substitutions.js +111 -0
- package/agent-spine-plugin/src/lib/delivery-shell-wrapper.js +64 -0
- package/agent-spine-plugin/src/lib/delivery-target.js +73 -0
- package/agent-spine-plugin/src/lib/delivery-verification.js +445 -0
- package/agent-spine-plugin/src/lib/documents.js +27 -5
- package/agent-spine-plugin/src/lib/filesystem-retry.js +2 -0
- package/agent-spine-plugin/src/lib/gateway-common.js +68 -0
- package/agent-spine-plugin/src/lib/gateway-control.js +302 -0
- package/agent-spine-plugin/src/lib/gateway-delivery.js +103 -0
- package/agent-spine-plugin/src/lib/gateway-execution.js +350 -0
- package/agent-spine-plugin/src/lib/gateway-host-lifecycle.js +185 -0
- package/agent-spine-plugin/src/lib/gateway-inspection.js +85 -0
- package/agent-spine-plugin/src/lib/gateway-knowledge.js +129 -0
- package/agent-spine-plugin/src/lib/gateway-policy-provenance.js +197 -0
- package/agent-spine-plugin/src/lib/gateway-premortem-disposition.js +82 -0
- package/agent-spine-plugin/src/lib/gateway-premortem.js +363 -0
- package/agent-spine-plugin/src/lib/gateway-runs.js +300 -0
- package/agent-spine-plugin/src/lib/gateway-runtime-identity.js +31 -0
- package/agent-spine-plugin/src/lib/gateway-runtime-records.js +21 -0
- package/agent-spine-plugin/src/lib/gateway-runtime.js +18 -1623
- package/agent-spine-plugin/src/lib/gateway-state-transaction.js +356 -0
- package/agent-spine-plugin/src/lib/gateway-state.js +342 -0
- package/agent-spine-plugin/src/lib/hook-artifact-guards.js +356 -0
- package/agent-spine-plugin/src/lib/hook-audit.js +16 -2
- package/agent-spine-plugin/src/lib/hook-briefing-use.js +68 -0
- package/agent-spine-plugin/src/lib/hook-context.js +424 -0
- package/agent-spine-plugin/src/lib/hook-final-message.js +26 -0
- package/agent-spine-plugin/src/lib/hook-input.js +27 -0
- package/agent-spine-plugin/src/lib/hook-output.js +140 -0
- package/agent-spine-plugin/src/lib/hook-premortem.js +257 -0
- package/agent-spine-plugin/src/lib/hook-process-advisory.js +37 -0
- package/agent-spine-plugin/src/lib/hook-protection.js +95 -0
- package/agent-spine-plugin/src/lib/hook-stop-verification.js +84 -0
- package/agent-spine-plugin/src/lib/hook-timeline.js +91 -0
- package/agent-spine-plugin/src/lib/identifier-analysis.js +446 -0
- package/agent-spine-plugin/src/lib/indexed-memory.js +23 -6
- package/agent-spine-plugin/src/lib/knowledge-evidence.js +431 -0
- package/agent-spine-plugin/src/lib/learning-applications.js +441 -0
- package/agent-spine-plugin/src/lib/learning-candidates.js +274 -0
- package/agent-spine-plugin/src/lib/learning-context.js +130 -0
- package/agent-spine-plugin/src/lib/learning-delivery-contracts.js +310 -0
- package/agent-spine-plugin/src/lib/learning-evaluation-contracts.js +350 -0
- package/agent-spine-plugin/src/lib/learning-evaluation-registration.js +372 -0
- package/agent-spine-plugin/src/lib/learning-evaluation-revocation.js +292 -0
- package/agent-spine-plugin/src/lib/learning-evidence-contracts.js +321 -0
- package/agent-spine-plugin/src/lib/learning-findings.js +458 -0
- package/agent-spine-plugin/src/lib/learning-measurement-contracts.js +285 -0
- package/agent-spine-plugin/src/lib/learning-measurements.js +265 -0
- package/agent-spine-plugin/src/lib/learning-outcome-contracts.js +165 -0
- package/agent-spine-plugin/src/lib/learning-outcomes.js +343 -0
- package/agent-spine-plugin/src/lib/learning-reconciliation.js +290 -0
- package/agent-spine-plugin/src/lib/learning-retry-contracts.js +188 -0
- package/agent-spine-plugin/src/lib/learning-schema.js +229 -0
- package/agent-spine-plugin/src/lib/learning-scope-targets.js +424 -0
- package/agent-spine-plugin/src/lib/learning-state-upgrade.js +477 -0
- package/agent-spine-plugin/src/lib/learning-status-configuration.js +476 -0
- package/agent-spine-plugin/src/lib/learning-storage.js +203 -0
- package/agent-spine-plugin/src/lib/learning-trial-recovery.js +218 -0
- package/agent-spine-plugin/src/lib/learning-validation-contracts.js +375 -0
- package/agent-spine-plugin/src/lib/learning-validation-renewal.js +298 -0
- package/agent-spine-plugin/src/lib/learning-validation-runtime.js +234 -0
- package/agent-spine-plugin/src/lib/learning.js +36 -6923
- package/agent-spine-plugin/src/lib/lesson-recall-session.js +172 -0
- package/agent-spine-plugin/src/lib/mcp-autonomy-tools.js +37 -0
- package/agent-spine-plugin/src/lib/mcp-delivery-completion.js +124 -0
- package/agent-spine-plugin/src/lib/mcp-delivery-tools.js +45 -0
- package/agent-spine-plugin/src/lib/mcp-premortem.js +68 -0
- package/agent-spine-plugin/src/lib/mcp-runtime.js +269 -0
- package/agent-spine-plugin/src/lib/mcp-source-context.js +51 -0
- package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +164 -0
- package/agent-spine-plugin/src/lib/mcp-world-tools.js +52 -0
- package/agent-spine-plugin/src/lib/owned-file-lock.js +30 -10
- package/agent-spine-plugin/src/lib/project-portfolio.js +176 -0
- package/agent-spine-plugin/src/lib/selfstarter-core.js +382 -0
- package/agent-spine-plugin/src/lib/selfstarter-jobs.js +149 -0
- package/agent-spine-plugin/src/lib/selfstarter-lease.js +204 -0
- package/agent-spine-plugin/src/lib/selfstarter-policy.js +90 -0
- package/agent-spine-plugin/src/lib/selfstarter-workspace.js +111 -0
- package/agent-spine-plugin/src/lib/selfstarter.js +11 -911
- package/agent-spine-plugin/src/lib/session-timeline-auth.js +316 -0
- package/agent-spine-plugin/src/lib/session-timeline-codex.js +58 -0
- package/agent-spine-plugin/src/lib/session-timeline-contract.js +48 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +41 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +132 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +17 -0
- package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +500 -0
- package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +157 -0
- package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +74 -0
- package/agent-spine-plugin/src/lib/session-timeline-host-receipt.js +117 -0
- package/agent-spine-plugin/src/lib/session-timeline-invocation.js +201 -0
- package/agent-spine-plugin/src/lib/session-timeline-king.js +79 -0
- package/agent-spine-plugin/src/lib/session-timeline-prior.js +59 -0
- package/agent-spine-plugin/src/lib/session-timeline-provider.js +34 -0
- package/agent-spine-plugin/src/lib/session-timeline-query.js +55 -0
- package/agent-spine-plugin/src/lib/session-timeline-results.js +31 -0
- package/agent-spine-plugin/src/lib/session-timeline-root.js +11 -0
- package/agent-spine-plugin/src/lib/session-timeline-search.js +82 -0
- package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +217 -0
- package/agent-spine-plugin/src/lib/session-timeline-source.js +83 -0
- package/agent-spine-plugin/src/lib/session-timeline-state.js +45 -0
- package/agent-spine-plugin/src/lib/session-timeline-transport.js +50 -0
- package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +148 -0
- package/agent-spine-plugin/src/lib/session-timeline.js +453 -0
- package/agent-spine-plugin/src/lib/source-roots.js +69 -136
- package/agent-spine-plugin/src/lib/source-tree-scan.js +178 -0
- package/agent-spine-plugin/src/lib/task-knowledge-context.js +78 -0
- package/agent-spine-plugin/src/lib/timeline-tool-guard.js +202 -0
- package/agent-spine-plugin/src/lib/world-knowledge.js +249 -0
- package/agent-spine-plugin/src/lib/world-model.js +278 -0
- package/agent-spine-plugin/src/mcp.js +20 -161
- package/agent-spine-plugin/src/version.js +1 -1
- package/agent-spine-plugin/src/worker.js +22 -5
- package/bin/agent-resume-snapshot.cjs +2 -2
- package/bin/agentspine-king-goal-inbox.mjs +111 -0
- package/bin/agentspine-king-goal-intake.mjs +106 -0
- package/bin/baseline-skill-performance-policy.cjs +1 -16
- package/bin/core-bootstrap.js +2 -0
- package/bin/curiosity-scout-policy.cjs +5 -1
- package/bin/input-draft-persistence.cjs +2 -2
- package/bin/king-tui-function-contract.json +33 -0
- package/bin/launcher-restart-policy.cjs +150 -0
- package/bin/launcher-runtime.js +56 -44
- package/bin/managed-context-startup-policy.cjs +27 -0
- package/bin/managed-plugin-selection.cjs +116 -0
- package/bin/mistake-relevance-policy.cjs +1 -1
- package/bin/observer-hooks.cjs +14 -0
- package/bin/oversized-context-offload-policy.cjs +86 -0
- package/bin/plugin-bootstrap.js +7 -40
- package/bin/proactive-compaction-policy.cjs +1 -1
- package/bin/provider-model-refresh-deadline.cjs +53 -0
- package/bin/provider-model-refresh-policy.cjs +107 -0
- package/bin/release-artifact-freeze-policy.cjs +30 -0
- package/bin/repeated-user-message-projection.cjs +3 -126
- package/bin/research-page-result.cjs +74 -0
- package/bin/runtime-exit-ledger.cjs +1 -0
- package/bin/session-compaction-policy.cjs +84 -0
- package/bin/skill-listing-performance-policy.cjs +2 -2
- package/bin/standard-tools-bootstrap.js +0 -37
- package/bin/subagent-skill-policy.cjs +3 -1
- package/bin/telegram-approval-relay.cjs +12 -7
- package/bin/telegram-private-conversation-policy.cjs +3 -2
- package/bin/telegram-queue-handoff-policy.cjs +24 -0
- package/bin/thinking-activity-status-policy.cjs +1 -1
- package/bin/thinking-only-guard.cjs +16 -12
- package/bin/tool-call-loop-policy.cjs +0 -2
- package/bin/tool-result-offload-policy.cjs +11 -2
- package/bin/tui-functional-contract.cjs +55 -0
- package/bin/update-notice.js +18 -14
- package/bin/user-message-offload-policy.cjs +1 -1
- package/bin/user-prompt-hook-origin-policy.cjs +34 -0
- package/bin/windows-node-crash-dump.cjs +110 -0
- package/blun.mjs +2517 -1048
- package/codebase-index/codebase_index.py +4 -3
- package/package.json +3 -17
- package/standard-skills/research-evidence/SKILL.md +39 -0
- package/standard-skills/research-evidence/references/evidence-format.md +82 -0
- package/standard-skills/research-evidence/scripts/evidence-collection.cjs +254 -0
- package/standard-skills/research-evidence/scripts/score-report.cjs +112 -0
- package/standard-skills/web-lesen/SKILL.md +37 -22
- package/standard-skills/web-lesen/scripts/crawl_public.py +376 -0
- package/telegram-plugin/DELIVERY.md +36 -0
- package/telegram-plugin/bin/telegram-approval-relay.cjs +13 -7
- package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +122 -0
- package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +3 -2
- package/telegram-plugin/bin/telegram-reply-parts.cjs +149 -0
- package/telegram-plugin/dist/bridge.mjs +7 -56
- package/telegram-plugin/dist/mcp-server.mjs +33 -4
- package/agent-spine-plugin/skill/SKILL.md +0 -76
- package/bin/mnemo-connect-heartbeat.cjs +0 -204
- package/bin/mnemo-tool-agent-policy.cjs +0 -22
- package/telegram-plugin/bin/telegram-mnemo-capture.cjs +0 -297
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MANAGED_CONTEXT_MISMATCH = 'Runtime model context does not match the configured internal BLUN model.';
|
|
4
|
+
|
|
5
|
+
function isManagedContextStartupRace(error) {
|
|
6
|
+
return error?.code === 'CONFIG_INVALID' && error?.message === MANAGED_CONTEXT_MISMATCH;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function bindManagedContextWithRetry(options) {
|
|
10
|
+
const delaysMs = Array.isArray(options?.delaysMs) ? options.delaysMs : [100, 300];
|
|
11
|
+
const wait = options?.wait || ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
|
|
12
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
13
|
+
try {
|
|
14
|
+
return await options.bind();
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (!isManagedContextStartupRace(error) || attempt >= delaysMs.length) throw error;
|
|
17
|
+
await options.refresh();
|
|
18
|
+
await wait(delaysMs[attempt]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
MANAGED_CONTEXT_MISMATCH,
|
|
25
|
+
bindManagedContextWithRetry,
|
|
26
|
+
isManagedContextStartupRace,
|
|
27
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { createHash, randomUUID } = require('node:crypto');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { ensurePrivateDirectory, securePrivateTree } = require('./private-paths');
|
|
6
|
+
const SCHEMA = 'blun.managed-plugin-selection/v1';
|
|
7
|
+
|
|
8
|
+
function snapshotPluginTree(root) {
|
|
9
|
+
const entries = [];
|
|
10
|
+
const rootStat = fs.lstatSync(root);
|
|
11
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) return null;
|
|
12
|
+
const visit = (directory, relativeDirectory = '') => {
|
|
13
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })
|
|
14
|
+
.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) {
|
|
15
|
+
const relativePath = path.join(relativeDirectory, entry.name);
|
|
16
|
+
const absolutePath = path.join(directory, entry.name);
|
|
17
|
+
const stat = fs.lstatSync(absolutePath);
|
|
18
|
+
if (stat.isSymbolicLink()) return false;
|
|
19
|
+
if (stat.isDirectory()) {
|
|
20
|
+
entries.push({ kind: 'directory', path: relativePath });
|
|
21
|
+
if (!visit(absolutePath, relativePath)) return false;
|
|
22
|
+
} else if (stat.isFile()) {
|
|
23
|
+
entries.push({ kind: 'file', path: relativePath, content: fs.readFileSync(absolutePath) });
|
|
24
|
+
} else return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
};
|
|
28
|
+
return visit(root) ? entries : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function digestPluginEntries(entries) {
|
|
32
|
+
if (entries === null) throw new Error('Invalid managed plugin tree');
|
|
33
|
+
const hash = createHash('sha256');
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
hash.update(Buffer.from(`${entry.kind}\0${entry.path}\0`, 'utf8'));
|
|
36
|
+
if (entry.kind === 'file') {
|
|
37
|
+
const length = Buffer.allocUnsafe(8);
|
|
38
|
+
length.writeBigUInt64BE(BigInt(entry.content.length));
|
|
39
|
+
hash.update(length);
|
|
40
|
+
hash.update(entry.content);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return hash.digest('hex');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function samePath(left, right) {
|
|
47
|
+
const normalize = value => process.platform === 'win32' ? path.resolve(value).toLowerCase() : path.resolve(value);
|
|
48
|
+
return normalize(left) === normalize(right);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function scopedPluginRoot(blunDir, pluginRoot) {
|
|
52
|
+
const managed = path.resolve(blunDir, 'plugins', 'managed');
|
|
53
|
+
const root = path.resolve(pluginRoot);
|
|
54
|
+
if (!samePath(path.dirname(root), managed)) throw new Error('Selected AgentSpine root is outside this profile');
|
|
55
|
+
for (const directory of [blunDir, path.dirname(managed), managed, root]) {
|
|
56
|
+
const stat = fs.lstatSync(directory);
|
|
57
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error('Selected AgentSpine root contains a linked directory');
|
|
58
|
+
}
|
|
59
|
+
if (!samePath(path.dirname(fs.realpathSync(root)), fs.realpathSync(managed))) {
|
|
60
|
+
throw new Error('Selected AgentSpine root resolves outside this profile');
|
|
61
|
+
}
|
|
62
|
+
return root;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function describeAgentSpineSelection({ blunDir, pluginRoot }) {
|
|
66
|
+
const root = scopedPluginRoot(blunDir, pluginRoot);
|
|
67
|
+
const digest = digestPluginEntries(snapshotPluginTree(root));
|
|
68
|
+
if (path.basename(root) !== `agent-spine-${digest}`) throw new Error('Selected AgentSpine digest mismatch');
|
|
69
|
+
return { schema: SCHEMA, digest };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function selectedAgentSpineRoot({ installed, blunDir }) {
|
|
73
|
+
const records = installed.plugins.filter(record => record.id === 'agent-spine');
|
|
74
|
+
if (!records.some(record => Object.hasOwn(record, 'managedSelection'))) return null;
|
|
75
|
+
if (records.length !== 1) throw new Error('Ambiguous AgentSpine selection');
|
|
76
|
+
const [record] = records;
|
|
77
|
+
const selection = record.managedSelection;
|
|
78
|
+
if (!selection || selection.schema !== SCHEMA || !/^[a-f0-9]{64}$/.test(selection.digest || '')
|
|
79
|
+
|| Object.keys(selection).sort().join(',') !== 'digest,schema' || typeof record.root !== 'string') {
|
|
80
|
+
throw new Error('Invalid AgentSpine selection');
|
|
81
|
+
}
|
|
82
|
+
const actual = describeAgentSpineSelection({ blunDir, pluginRoot: record.root });
|
|
83
|
+
if (actual.digest !== selection.digest) throw new Error('Selected AgentSpine digest mismatch');
|
|
84
|
+
return record.root;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function copySelectedAgentSpinePlugin({ blunDir, sourceRoot }) {
|
|
88
|
+
const plugins = path.resolve(blunDir, 'plugins');
|
|
89
|
+
const managed = path.join(plugins, 'managed');
|
|
90
|
+
ensurePrivateDirectory(plugins);
|
|
91
|
+
ensurePrivateDirectory(managed);
|
|
92
|
+
// Explicit installs must see current source bytes, not the bundled-source cache.
|
|
93
|
+
const digest = digestPluginEntries(snapshotPluginTree(sourceRoot));
|
|
94
|
+
const pluginRoot = path.join(managed, `agent-spine-${digest}`);
|
|
95
|
+
if (fs.existsSync(pluginRoot)) {
|
|
96
|
+
describeAgentSpineSelection({ blunDir, pluginRoot });
|
|
97
|
+
return pluginRoot;
|
|
98
|
+
}
|
|
99
|
+
const staging = path.join(managed, `.agent-spine-selected-${process.pid}-${randomUUID()}.tmp`);
|
|
100
|
+
try {
|
|
101
|
+
fs.cpSync(sourceRoot, staging, { recursive: true, errorOnExist: true, force: false });
|
|
102
|
+
securePrivateTree(staging);
|
|
103
|
+
if (digestPluginEntries(snapshotPluginTree(staging)) !== digest) throw new Error('AgentSpine source changed during installation');
|
|
104
|
+
fs.renameSync(staging, pluginRoot);
|
|
105
|
+
describeAgentSpineSelection({ blunDir, pluginRoot });
|
|
106
|
+
return pluginRoot;
|
|
107
|
+
} finally {
|
|
108
|
+
if (!samePath(path.dirname(staging), managed) || !path.basename(staging).startsWith('.agent-spine-selected-')) {
|
|
109
|
+
throw new Error('Invalid AgentSpine staging cleanup path');
|
|
110
|
+
}
|
|
111
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { snapshotPluginTree, digestPluginEntries, describeAgentSpineSelection,
|
|
116
|
+
selectedAgentSpineRoot, copySelectedAgentSpinePlugin };
|
|
@@ -32,7 +32,7 @@ const SEMANTIC_SIGNAL_RULES = Object.freeze([
|
|
|
32
32
|
['problem:protocol', /\b(?:schema|contract|payload|protocol|format|version mismatch|manifest mismatch|vertrag|protokoll|versionskonflikt)\b/iu],
|
|
33
33
|
['problem:capability', /\b(?:capabilit\w*|missing tool|not available|unsupported|fahigkeit\w*|werkzeug fehlt|nicht verfugbar)\b/iu],
|
|
34
34
|
['area:orchestration', /\b(?:agent\w*|worker\w*|queue\w*|checkpoint\w*|heartbeat\w*|delegat\w*|owner\w*|responsib\w*|task\w*|turn\w*|busy|group\w*|channel\w*|arbeiter\w*|warteschlange\w*|prufpunkt\w*|herzschlag\w*|zustandig\w*|auftrag\w*|zug\w*|beschaftigt|gruppe\w*|gruppen\w*|kanal\w*)\b/iu],
|
|
35
|
-
['area:memory', /\b(?:memory|recall|remember\w*|context|history|resume|checkpoint|
|
|
35
|
+
['area:memory', /\b(?:memory|recall|remember\w*|context|history|resume|checkpoint|gedachtnis|erinner\w*|kontext|verlauf|wiederaufnahme)\b/iu],
|
|
36
36
|
['area:release', /\b(?:release\w*|rollout\w*|update\w*|updater\w*|npm|package\w*|manifest\w*|version\w*|veroffentlich\w*|paket\w*|integrity|integritat)\b/iu],
|
|
37
37
|
['area:media', /\b(?:image\w*|video\w*|audio\w*|portrait\w*|render\w*|pixel\w*|media|bild\w*|foto\w*|gesicht\w*|stimme\w*)\b/iu],
|
|
38
38
|
['risk:validation', /\b(?:verif\w*|validat\w*|test\w*|check\w*|gate\w*|rollback\w*|canary|proof|evidence|sha(?:256|512)?|measur\w*|inspect\w*|(?:ge)?pruf\w*|gegenprobe\w*|beleg\w*|evidenz\w*|mess\w*|kontroll\w*)\b/iu],
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
async function runHookPhases(hooks, run, signal) {
|
|
4
|
+
const regular = hooks.filter(hook => hook.observer !== true);
|
|
5
|
+
const results = await Promise.all(regular.map(run));
|
|
6
|
+
// A fail-open guard error is not proof that the event was accepted.
|
|
7
|
+
if (signal?.aborted || results.some(result => result.action === 'block'
|
|
8
|
+
|| result.timedOut === true || result.exitCode !== 0)) return results;
|
|
9
|
+
await Promise.allSettled(hooks.filter(hook => hook.observer === true).map(run));
|
|
10
|
+
// Observers have no authority to deny work or add instructions to the agent.
|
|
11
|
+
return results;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { runHookPhases };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const OVERSIZED_CONTEXT_MESSAGE_MAX_CHARS = 200_000;
|
|
4
|
+
const OVERSIZED_CONTEXT_TOOL_RESULT_MAX_CHARS = 200_000;
|
|
5
|
+
|
|
6
|
+
function textContent(message) {
|
|
7
|
+
if (!Array.isArray(message?.content)) return undefined;
|
|
8
|
+
const textParts = message.content.filter(
|
|
9
|
+
(part) => part?.type === 'text' && typeof part.text === 'string',
|
|
10
|
+
);
|
|
11
|
+
if (textParts.length === 0) return undefined;
|
|
12
|
+
return textParts.map((part) => part.text).join('\n');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function oversizedItemKind(message) {
|
|
16
|
+
if (message?.role === 'tool') return 'tool-result';
|
|
17
|
+
if (message?.role === 'user') return 'user-message';
|
|
18
|
+
if (message?.role === 'assistant') return 'assistant-message';
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function oversizedItemLimit(kind) {
|
|
23
|
+
return kind === 'tool-result'
|
|
24
|
+
? OVERSIZED_CONTEXT_TOOL_RESULT_MAX_CHARS
|
|
25
|
+
: OVERSIZED_CONTEXT_MESSAGE_MAX_CHARS;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function projectOversizedContextItems(messages, options = {}) {
|
|
29
|
+
if (!Array.isArray(messages) || typeof options.persistItem !== 'function') {
|
|
30
|
+
return {
|
|
31
|
+
messages,
|
|
32
|
+
offloadedCount: 0,
|
|
33
|
+
charsBefore: 0,
|
|
34
|
+
charsAfter: 0,
|
|
35
|
+
interventions: [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const projected = messages.slice();
|
|
40
|
+
const interventions = [];
|
|
41
|
+
let charsBefore = 0;
|
|
42
|
+
let charsAfter = 0;
|
|
43
|
+
|
|
44
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
45
|
+
const message = messages[index];
|
|
46
|
+
const kind = oversizedItemKind(message);
|
|
47
|
+
if (kind === undefined) continue;
|
|
48
|
+
const text = textContent(message);
|
|
49
|
+
if (text === undefined || text.length < oversizedItemLimit(kind)) continue;
|
|
50
|
+
|
|
51
|
+
const replacement = await options.persistItem({ index, kind, message, text });
|
|
52
|
+
if (!Array.isArray(replacement?.content)) continue;
|
|
53
|
+
const replacementChars = textContent({ content: replacement.content })?.length;
|
|
54
|
+
if (!Number.isSafeInteger(replacementChars) || replacementChars >= text.length) continue;
|
|
55
|
+
|
|
56
|
+
projected[index] = {
|
|
57
|
+
...message,
|
|
58
|
+
content: replacement.content,
|
|
59
|
+
};
|
|
60
|
+
charsBefore += text.length;
|
|
61
|
+
charsAfter += replacementChars;
|
|
62
|
+
interventions.push({
|
|
63
|
+
index,
|
|
64
|
+
kind,
|
|
65
|
+
charsBefore: text.length,
|
|
66
|
+
charsAfter: replacementChars,
|
|
67
|
+
...(typeof replacement.outputPath === 'string'
|
|
68
|
+
? { outputPath: replacement.outputPath }
|
|
69
|
+
: {}),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
messages: interventions.length === 0 ? messages : projected,
|
|
75
|
+
offloadedCount: interventions.length,
|
|
76
|
+
charsBefore,
|
|
77
|
+
charsAfter,
|
|
78
|
+
interventions,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
OVERSIZED_CONTEXT_MESSAGE_MAX_CHARS,
|
|
84
|
+
OVERSIZED_CONTEXT_TOOL_RESULT_MAX_CHARS,
|
|
85
|
+
projectOversizedContextItems,
|
|
86
|
+
};
|
package/bin/plugin-bootstrap.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { randomUUID } = require('node:crypto');
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
|
+
const { snapshotPluginTree, digestPluginEntries, selectedAgentSpineRoot } = require('./managed-plugin-selection.cjs');
|
|
6
7
|
|
|
7
8
|
const {
|
|
8
9
|
ensurePrivateDirectory,
|
|
@@ -17,33 +18,6 @@ function isObject(value) {
|
|
|
17
18
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
function snapshotPluginTree(root) {
|
|
21
|
-
const entries = [];
|
|
22
|
-
const visit = (directory, relativeDirectory = '') => {
|
|
23
|
-
for (const entry of fs.readdirSync(directory, { withFileTypes: true })
|
|
24
|
-
.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) {
|
|
25
|
-
const relativePath = path.join(relativeDirectory, entry.name);
|
|
26
|
-
const absolutePath = path.join(directory, entry.name);
|
|
27
|
-
const stat = fs.lstatSync(absolutePath);
|
|
28
|
-
if (stat.isSymbolicLink()) return false;
|
|
29
|
-
if (stat.isDirectory()) {
|
|
30
|
-
entries.push({ kind: 'directory', path: relativePath });
|
|
31
|
-
if (!visit(absolutePath, relativePath)) return false;
|
|
32
|
-
} else if (stat.isFile()) {
|
|
33
|
-
entries.push({
|
|
34
|
-
kind: 'file',
|
|
35
|
-
path: relativePath,
|
|
36
|
-
content: fs.readFileSync(absolutePath),
|
|
37
|
-
});
|
|
38
|
-
} else {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return true;
|
|
43
|
-
};
|
|
44
|
-
return visit(root) ? entries : null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
21
|
function snapshotManagedPluginSource(root) {
|
|
48
22
|
const resolvedRoot = path.resolve(root);
|
|
49
23
|
const cacheKey = process.platform === 'win32' ? resolvedRoot.toLowerCase() : resolvedRoot;
|
|
@@ -72,18 +46,7 @@ function pluginTreesAreIdentical(sourceRoot, pluginRoot) {
|
|
|
72
46
|
function pluginTreeDigest(root) {
|
|
73
47
|
const entries = snapshotManagedPluginSource(root);
|
|
74
48
|
if (entries === null) throw new Error(`Invalid managed plugin tree: ${root}`);
|
|
75
|
-
|
|
76
|
-
for (const entry of entries) {
|
|
77
|
-
const metadata = Buffer.from(`${entry.kind}\0${entry.path}\0`, 'utf8');
|
|
78
|
-
hash.update(metadata);
|
|
79
|
-
if (entry.kind === 'file') {
|
|
80
|
-
const length = Buffer.allocUnsafe(8);
|
|
81
|
-
length.writeBigUInt64BE(BigInt(entry.content.length));
|
|
82
|
-
hash.update(length);
|
|
83
|
-
hash.update(entry.content);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return hash.digest('hex');
|
|
49
|
+
return digestPluginEntries(entries);
|
|
87
50
|
}
|
|
88
51
|
|
|
89
52
|
function hasCurrentPluginRecord(installed, pluginRoot, pluginId, mcpServerName) {
|
|
@@ -179,6 +142,10 @@ function installManagedPlugin({
|
|
|
179
142
|
if (fs.existsSync(installedFile)) securePrivateFile(installedFile);
|
|
180
143
|
const installed = readInstalledPlugins(installedFile);
|
|
181
144
|
ensurePrivateDirectory(managedDir);
|
|
145
|
+
if (pluginId === 'agent-spine') {
|
|
146
|
+
const selected = selectedAgentSpineRoot({ installed, blunDir });
|
|
147
|
+
if (selected) return { installedFile, pluginRoot: selected, reused: true, selected: true };
|
|
148
|
+
}
|
|
182
149
|
if (fs.existsSync(legacyPluginRoot)
|
|
183
150
|
&& pluginTreesAreIdentical(sourceRoot, legacyPluginRoot)) {
|
|
184
151
|
if (!hasCurrentPluginRecord(installed, legacyPluginRoot, pluginId, mcpServerName)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const PROACTIVE_COMPACTION_TRIGGER_FRACTION = 0.
|
|
3
|
+
const PROACTIVE_COMPACTION_TRIGGER_FRACTION = 0.9;
|
|
4
4
|
|
|
5
5
|
function proactiveCompactionEligibility({ usedTokens, budgetTokens, hasShrinkableHistory, isCompacting }) {
|
|
6
6
|
const budget = Number(budgetTokens);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_PROVIDER_MODEL_READ_TIMEOUT_MS = 10_000;
|
|
4
|
+
|
|
5
|
+
function providerModelRefreshAbortError(timedOut = false, cause) {
|
|
6
|
+
const error = new Error(timedOut ? 'Model catalog refresh timed out.' : 'Model catalog refresh was cancelled.', { cause });
|
|
7
|
+
error.code = timedOut ? 'PROVIDER_MODEL_REFRESH_TIMEOUT' : 'PROVIDER_MODEL_REFRESH_ABORTED';
|
|
8
|
+
return error;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isProviderModelReadInterrupted(error) {
|
|
12
|
+
return error?.code === 'PROVIDER_MODEL_REFRESH_TIMEOUT' || error?.code === 'PROVIDER_MODEL_REFRESH_ABORTED';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Bound only remote reads. The caller must await any config commit it has started.
|
|
16
|
+
function withProviderModelReadDeadline(operation, options = {}) {
|
|
17
|
+
const { signal, timeoutMs = DEFAULT_PROVIDER_MODEL_READ_TIMEOUT_MS } = options;
|
|
18
|
+
if (signal?.aborted) return Promise.reject(providerModelRefreshAbortError(false, signal.reason));
|
|
19
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647) {
|
|
20
|
+
return Promise.reject(new RangeError('Invalid provider model read timeout.'));
|
|
21
|
+
}
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
let settled = false;
|
|
25
|
+
let timer;
|
|
26
|
+
const finish = (complete, value) => {
|
|
27
|
+
if (settled) return;
|
|
28
|
+
settled = true;
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
signal?.removeEventListener('abort', onAbort);
|
|
31
|
+
complete(value);
|
|
32
|
+
};
|
|
33
|
+
const abort = (timedOut) => {
|
|
34
|
+
const error = providerModelRefreshAbortError(timedOut, signal?.reason);
|
|
35
|
+
controller.abort(error);
|
|
36
|
+
finish(reject, error);
|
|
37
|
+
};
|
|
38
|
+
const onAbort = () => abort(false);
|
|
39
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
40
|
+
timer = setTimeout(() => abort(true), timeoutMs);
|
|
41
|
+
if (signal?.aborted) onAbort();
|
|
42
|
+
Promise.resolve().then(() => {
|
|
43
|
+
controller.signal.throwIfAborted();
|
|
44
|
+
return operation(controller.signal);
|
|
45
|
+
}).then(
|
|
46
|
+
(value) => controller.signal.aborted ? finish(reject, controller.signal.reason) : finish(resolve, value),
|
|
47
|
+
(error) => finish(reject, error),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { DEFAULT_PROVIDER_MODEL_READ_TIMEOUT_MS, providerModelRefreshAbortError,
|
|
53
|
+
isProviderModelReadInterrupted, withProviderModelReadDeadline };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { isDeepStrictEqual } = require('node:util');
|
|
5
|
+
const providerId = 'managed:blun';
|
|
6
|
+
const serviceKeys = ['blunSearch', 'blunFetch', 'blunMedia'];
|
|
7
|
+
const writers = new Map();
|
|
8
|
+
const jsonCopy = (value) => JSON.parse(JSON.stringify(value));
|
|
9
|
+
const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
10
|
+
|
|
11
|
+
function providerModelRefreshSnapshot(config) {
|
|
12
|
+
return jsonCopy({
|
|
13
|
+
provider: config.providers?.[providerId] ?? null,
|
|
14
|
+
models: Object.fromEntries(Object.entries(config.models ?? {}).filter(([, model]) => model?.provider === providerId)),
|
|
15
|
+
defaultModel: config.defaultModel ?? null,
|
|
16
|
+
thinkingEnabled: config.thinking?.enabled ?? null,
|
|
17
|
+
services: Object.fromEntries(serviceKeys.map((key) => [key, config.services?.[key] ?? null])),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function exactKeys(value, keys) {
|
|
22
|
+
return record(value) && isDeepStrictEqual(Object.keys(value).sort(), [...keys].sort());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function checkCredentials(value) {
|
|
26
|
+
if (value?.oauth != null && (value.apiKey?.trim() || value.apiKeyFile?.trim())) {
|
|
27
|
+
throw new Error('Provider refresh cannot combine OAuth and API key credentials.');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function checkSnapshot(value) {
|
|
32
|
+
if (!exactKeys(value, ['provider', 'models', 'defaultModel', 'thinkingEnabled', 'services']) ||
|
|
33
|
+
!record(value.provider) || value.provider.type !== 'blun' || !record(value.models) ||
|
|
34
|
+
!(value.defaultModel === null || typeof value.defaultModel === 'string') ||
|
|
35
|
+
!(value.thinkingEnabled === null || typeof value.thinkingEnabled === 'boolean') ||
|
|
36
|
+
!exactKeys(value.services, serviceKeys)) throw new Error('Invalid provider refresh snapshot.');
|
|
37
|
+
checkCredentials(value.provider);
|
|
38
|
+
for (const [alias, model] of Object.entries(value.models)) {
|
|
39
|
+
if (['__proto__', 'prototype', 'constructor'].includes(alias) || !record(model) || model.provider !== providerId) {
|
|
40
|
+
throw new Error('Provider refresh contains a foreign or invalid model alias.');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const service of Object.values(value.services)) {
|
|
44
|
+
if (service !== null && !record(service)) throw new Error('Invalid provider refresh service.');
|
|
45
|
+
checkCredentials(service);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function applyProviderModelRefresh(config, envelope) {
|
|
50
|
+
if (!exactKeys(envelope, ['providerModelRefresh'])) throw new Error('Provider refresh cannot be combined with a config patch.');
|
|
51
|
+
const request = envelope.providerModelRefresh;
|
|
52
|
+
if (!exactKeys(request, ['version', 'before', 'after']) || request.version !== 1) throw new Error('Invalid provider refresh request.');
|
|
53
|
+
const { before, after } = request;
|
|
54
|
+
checkSnapshot(before);
|
|
55
|
+
checkSnapshot(after);
|
|
56
|
+
if (!Object.hasOwn(after.models, 'blun/king')) throw new Error('Provider refresh must retain the BLUN King model.');
|
|
57
|
+
const current = providerModelRefreshSnapshot(config);
|
|
58
|
+
if (!isDeepStrictEqual(current.provider, before.provider) || !isDeepStrictEqual(current.models, before.models)) {
|
|
59
|
+
throw new Error('Provider or models changed during refresh; existing configuration retained.');
|
|
60
|
+
}
|
|
61
|
+
const next = structuredClone(config);
|
|
62
|
+
next.providers[providerId] = jsonCopy(after.provider);
|
|
63
|
+
next.models ??= {};
|
|
64
|
+
// The TOML writer otherwise revives omitted fields from these old raw tables.
|
|
65
|
+
if (record(next.raw?.providers)) delete next.raw.providers[providerId];
|
|
66
|
+
for (const alias of new Set([...Object.keys(current.models), ...Object.keys(after.models)])) {
|
|
67
|
+
const existing = next.models[alias];
|
|
68
|
+
if (existing !== undefined && existing.provider !== providerId) throw new Error('Provider refresh conflicts with another provider model.');
|
|
69
|
+
delete next.models[alias];
|
|
70
|
+
if (record(next.raw?.models)) delete next.raw.models[alias];
|
|
71
|
+
}
|
|
72
|
+
Object.assign(next.models, jsonCopy(after.models));
|
|
73
|
+
if (current.defaultModel === before.defaultModel) {
|
|
74
|
+
if (after.defaultModel === null) delete next.defaultModel;
|
|
75
|
+
else next.defaultModel = after.defaultModel;
|
|
76
|
+
}
|
|
77
|
+
if (next.defaultModel !== undefined && !Object.hasOwn(next.models, next.defaultModel)) throw new Error('Provider refresh would invalidate the selected default model.');
|
|
78
|
+
if (current.defaultModel === before.defaultModel && current.thinkingEnabled === before.thinkingEnabled) {
|
|
79
|
+
if (after.thinkingEnabled === null) {
|
|
80
|
+
if (next.thinking) delete next.thinking.enabled;
|
|
81
|
+
if (record(next.raw?.thinking)) delete next.raw.thinking.enabled;
|
|
82
|
+
} else next.thinking = { ...next.thinking, enabled: after.thinkingEnabled };
|
|
83
|
+
}
|
|
84
|
+
for (const key of serviceKeys) {
|
|
85
|
+
if (!isDeepStrictEqual(current.services[key], before.services[key])) continue;
|
|
86
|
+
if (after.services[key] === null) {
|
|
87
|
+
if (next.services) delete next.services[key];
|
|
88
|
+
} else {
|
|
89
|
+
next.services ??= {};
|
|
90
|
+
next.services[key] = jsonCopy(after.services[key]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return next;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Share one read-modify-write queue across Core instances using the same file.
|
|
97
|
+
function withProviderConfigWrite(filePath, operation) {
|
|
98
|
+
const resolved = path.resolve(filePath);
|
|
99
|
+
const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
100
|
+
const run = (writers.get(key) ?? Promise.resolve()).then(operation);
|
|
101
|
+
const tail = run.then(() => undefined, () => undefined);
|
|
102
|
+
writers.set(key, tail);
|
|
103
|
+
void tail.then(() => { if (writers.get(key) === tail) writers.delete(key); });
|
|
104
|
+
return run;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { providerModelRefreshSnapshot, applyProviderModelRefresh, withProviderConfigWrite };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
function sha256(filePath) {
|
|
8
|
+
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function copyFrozenArtifact(sourcePath, destinationPath) {
|
|
12
|
+
const source = path.resolve(sourcePath);
|
|
13
|
+
const destination = path.resolve(destinationPath);
|
|
14
|
+
const sourceSha256 = sha256(source);
|
|
15
|
+
if (fs.existsSync(destination)) {
|
|
16
|
+
const destinationSha256 = sha256(destination);
|
|
17
|
+
if (destinationSha256 !== sourceSha256) {
|
|
18
|
+
throw new Error(`FROZEN_ARTIFACT_NAME_CONFLICT:${destination}`);
|
|
19
|
+
}
|
|
20
|
+
return { status: 'already-frozen', path: destination, sha256: destinationSha256 };
|
|
21
|
+
}
|
|
22
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
23
|
+
fs.copyFileSync(source, destination, fs.constants.COPYFILE_EXCL);
|
|
24
|
+
return { status: 'created', path: destination, sha256: sourceSha256 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
copyFrozenArtifact,
|
|
29
|
+
sha256,
|
|
30
|
+
};
|
|
@@ -1,131 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const LONG_DUPLICATE_MIN_CHARS = 512;
|
|
4
|
-
const TELEGRAM_DELIVERY_LEDGER_LIMIT = 2_048;
|
|
5
|
-
|
|
6
|
-
function userMessageText(message) {
|
|
7
|
-
if (
|
|
8
|
-
message?.role !== 'user'
|
|
9
|
-
|| message.origin?.kind !== 'user'
|
|
10
|
-
|| !Array.isArray(message.content)
|
|
11
|
-
) return '';
|
|
12
|
-
|
|
13
|
-
const textParts = message.content
|
|
14
|
-
.filter((part) => part?.type === 'text' && typeof part.text === 'string')
|
|
15
|
-
.map((part) => part.text);
|
|
16
|
-
return textParts.length > 0 ? textParts.join('\n') : '';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function channelAttribute(attributes, name) {
|
|
20
|
-
const match = String(attributes).match(new RegExp(`(?:^|\\s)${name}="([^"]+)"`, 'u'));
|
|
21
|
-
return match?.[1] ?? '';
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function telegramDeliveryIdentity(text) {
|
|
25
|
-
const channel = String(text).match(/<channel\b([^>]*)>/u);
|
|
26
|
-
if (channel === null) return '';
|
|
27
|
-
|
|
28
|
-
const source = channelAttribute(channel[1], 'source');
|
|
29
|
-
const chatId = channelAttribute(channel[1], 'chat_id');
|
|
30
|
-
const messageId = channelAttribute(channel[1], 'message_id');
|
|
31
|
-
if (source !== 'telegram' || !chatId || !messageId) return '';
|
|
32
|
-
return `${chatId}:${messageId}`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function telegramDeliveryIdentityFromParts(input) {
|
|
36
|
-
if (!Array.isArray(input)) return '';
|
|
37
|
-
for (const part of input) {
|
|
38
|
-
if (part?.type !== 'text' || typeof part.text !== 'string') continue;
|
|
39
|
-
const identity = telegramDeliveryIdentity(part.text);
|
|
40
|
-
if (identity) return identity;
|
|
41
|
-
}
|
|
42
|
-
return '';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
class TelegramDeliveryLedger {
|
|
46
|
-
constructor(limit = TELEGRAM_DELIVERY_LEDGER_LIMIT) {
|
|
47
|
-
this.limit = Math.max(1, Number.isInteger(limit) ? limit : TELEGRAM_DELIVERY_LEDGER_LIMIT);
|
|
48
|
-
this.identities = new Set();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
identity(input, origin) {
|
|
52
|
-
if (origin?.kind !== 'user') return '';
|
|
53
|
-
return telegramDeliveryIdentityFromParts(input);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
isDuplicate(input, origin) {
|
|
57
|
-
const identity = this.identity(input, origin);
|
|
58
|
-
return identity ? this.identities.has(identity) : false;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
remember(input, origin) {
|
|
62
|
-
const identity = this.identity(input, origin);
|
|
63
|
-
if (!identity || this.identities.has(identity)) return identity;
|
|
64
|
-
this.identities.add(identity);
|
|
65
|
-
while (this.identities.size > this.limit) {
|
|
66
|
-
this.identities.delete(this.identities.values().next().value);
|
|
67
|
-
}
|
|
68
|
-
return identity;
|
|
69
|
-
}
|
|
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
|
-
|
|
77
|
-
snapshot() {
|
|
78
|
-
return [...this.identities];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
restore(identities) {
|
|
82
|
-
this.identities.clear();
|
|
83
|
-
if (!Array.isArray(identities)) return;
|
|
84
|
-
for (const identity of identities) {
|
|
85
|
-
if (typeof identity !== 'string' || !identity || this.identities.has(identity)) continue;
|
|
86
|
-
this.identities.add(identity);
|
|
87
|
-
while (this.identities.size > this.limit) {
|
|
88
|
-
this.identities.delete(this.identities.values().next().value);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function repeatedUserMessageKey(message) {
|
|
95
|
-
const text = userMessageText(message);
|
|
96
|
-
if (!text) return '';
|
|
97
|
-
|
|
98
|
-
const telegramIdentity = telegramDeliveryIdentity(text);
|
|
99
|
-
if (telegramIdentity) return `telegram:${telegramIdentity}`;
|
|
100
|
-
return text.length >= LONG_DUPLICATE_MIN_CHARS ? `text:${text}` : '';
|
|
101
|
-
}
|
|
102
|
-
|
|
103
3
|
function dedupeRepeatedUserMessages(messages) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const lastIndexByKey = new Map();
|
|
107
|
-
const keys = messages.map((message, index) => {
|
|
108
|
-
const key = repeatedUserMessageKey(message);
|
|
109
|
-
if (key) lastIndexByKey.set(key, index);
|
|
110
|
-
return key;
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
let changed = false;
|
|
114
|
-
const projected = messages.filter((message, index) => {
|
|
115
|
-
const key = keys[index];
|
|
116
|
-
const keep = !key || lastIndexByKey.get(key) === index;
|
|
117
|
-
if (!keep) changed = true;
|
|
118
|
-
return keep;
|
|
119
|
-
});
|
|
120
|
-
return changed ? projected : messages;
|
|
4
|
+
// Repeated deliveries stay visible so the model can resolve them in context.
|
|
5
|
+
return messages;
|
|
121
6
|
}
|
|
122
7
|
|
|
123
|
-
module.exports = {
|
|
124
|
-
LONG_DUPLICATE_MIN_CHARS,
|
|
125
|
-
TELEGRAM_DELIVERY_LEDGER_LIMIT,
|
|
126
|
-
TelegramDeliveryLedger,
|
|
127
|
-
dedupeRepeatedUserMessages,
|
|
128
|
-
telegramDeliveryIdentity,
|
|
129
|
-
telegramDeliveryIdentityFromParts,
|
|
130
|
-
userMessageText,
|
|
131
|
-
};
|
|
8
|
+
module.exports = { dedupeRepeatedUserMessages };
|