@claude-flow/cli 3.71.3 → 3.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/helpers/auto-memory-hook.mjs +17 -0
- package/.claude/helpers/helpers.manifest.json +6 -6
- package/.claude/helpers/hook-handler.cjs +23 -10
- package/.claude/helpers/intelligence.cjs +23 -1
- package/.claude/helpers/statusline.cjs +29 -14
- package/README.md +18 -3
- package/bin/cli.js +13 -18
- package/bin/mcp-server.js +13 -18
- package/catalog-manifest.json +2 -2
- package/dist/src/appliance/rvfa-distribution.js +2 -2
- package/dist/src/benchmarks/gaia-critic.d.ts +1 -7
- package/dist/src/benchmarks/gaia-critic.js +1 -7
- package/dist/src/benchmarks/gaia-decomposer.d.ts +1 -8
- package/dist/src/benchmarks/gaia-decomposer.js +1 -8
- package/dist/src/commands/agent-wasm.js +2 -2
- package/dist/src/commands/analyze.js +1 -1
- package/dist/src/commands/daemon.js +1 -1
- package/dist/src/commands/doctor.js +3 -2
- package/dist/src/commands/guidance.js +9 -9
- package/dist/src/commands/hooks.js +13 -8
- package/dist/src/commands/init.js +14 -16
- package/dist/src/commands/memory.js +4 -3
- package/dist/src/commands/neural.js +8 -6
- package/dist/src/commands/route.js +1 -1
- package/dist/src/commands/ruvector/backup.js +7 -5
- package/dist/src/commands/ruvector/benchmark.js +6 -1
- package/dist/src/commands/ruvector/import.js +19 -5
- package/dist/src/commands/ruvector/init.js +1 -1
- package/dist/src/commands/start.js +1 -1
- package/dist/src/commands/status.js +1 -1
- package/dist/src/commands/task.js +18 -1
- package/dist/src/commands/update.js +1 -1
- package/dist/src/funnel/local-signals.js +7 -2
- package/dist/src/init/helpers-generator.js +16 -1
- package/dist/src/mcp-protocol.d.ts +90 -0
- package/dist/src/mcp-protocol.js +179 -0
- package/dist/src/mcp-server.d.ts +1 -0
- package/dist/src/mcp-server.js +20 -22
- package/dist/src/mcp-tools/agent-execute-core.js +9 -1
- package/dist/src/mcp-tools/agent-tools.d.ts +13 -0
- package/dist/src/mcp-tools/agent-tools.js +63 -12
- package/dist/src/mcp-tools/agentbbs-federation.d.ts +18 -1
- package/dist/src/mcp-tools/agentbbs-federation.js +23 -7
- package/dist/src/mcp-tools/browser-argv.d.ts +27 -0
- package/dist/src/mcp-tools/browser-argv.js +92 -0
- package/dist/src/mcp-tools/browser-session-tools.js +4 -0
- package/dist/src/mcp-tools/browser-tools.d.ts +1 -1
- package/dist/src/mcp-tools/browser-tools.js +25 -8
- package/dist/src/mcp-tools/coordination-tools.js +5 -4
- package/dist/src/mcp-tools/embeddings-tools.js +12 -4
- package/dist/src/mcp-tools/glob-exclusions.d.ts +6 -0
- package/dist/src/mcp-tools/glob-exclusions.js +81 -0
- package/dist/src/mcp-tools/hive-mind-tools.js +104 -35
- package/dist/src/mcp-tools/hooks-tools.js +12 -5
- package/dist/src/mcp-tools/import-limits.d.ts +4 -0
- package/dist/src/mcp-tools/import-limits.js +28 -0
- package/dist/src/mcp-tools/memory-tools.js +68 -72
- package/dist/src/mcp-tools/neural-tools.js +8 -2
- package/dist/src/mcp-tools/path-containment.d.ts +34 -0
- package/dist/src/mcp-tools/path-containment.js +133 -0
- package/dist/src/mcp-tools/performance-tools.js +7 -2
- package/dist/src/mcp-tools/record-store.d.ts +15 -0
- package/dist/src/mcp-tools/record-store.js +117 -3
- package/dist/src/mcp-tools/security-tools.d.ts +9 -0
- package/dist/src/mcp-tools/security-tools.js +39 -3
- package/dist/src/mcp-tools/session-tools.js +9 -41
- package/dist/src/mcp-tools/swarm-tools.js +36 -14
- package/dist/src/mcp-tools/task-tools.js +68 -5
- package/dist/src/mcp-tools/terminal-tools.js +16 -2
- package/dist/src/mcp-tools/validate-input.d.ts +2 -0
- package/dist/src/mcp-tools/validate-input.js +14 -0
- package/dist/src/mcp-tools/workflow-tools.js +13 -12
- package/dist/src/mcp-tools/x-federation-join.js +55 -14
- package/dist/src/memory/intelligence.d.ts +11 -0
- package/dist/src/memory/intelligence.js +84 -6
- package/dist/src/memory/memory-initializer.d.ts +6 -2
- package/dist/src/memory/memory-initializer.js +15 -6
- package/dist/src/ruvector/model-router.d.ts +6 -1
- package/dist/src/ruvector/model-router.js +16 -4
- package/dist/src/services/worker-daemon.js +30 -12
- package/dist/src/transfer/storage/gcs.js +3 -2
- package/package.json +1 -1
- package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP protocol-version handling for the stdio servers (ADR-524).
|
|
3
|
+
*
|
|
4
|
+
* Ruflo ships three stdio entry points (`bin/cli.js` auto-detect, `bin/mcp-server.js`,
|
|
5
|
+
* and `MCPServerManager` in mcp-server.ts). All three answered `initialize` with a
|
|
6
|
+
* hardcoded `2024-11-05` and knew nothing of the 2026-07-28 revision, which drops the
|
|
7
|
+
* handshake and carries the version on every request. This module is the one place
|
|
8
|
+
* that decides which revision a message speaks, so the three handlers cannot drift.
|
|
9
|
+
*
|
|
10
|
+
* Dual-era behaviour (spec: basic/versioning):
|
|
11
|
+
* - `initialize` selects legacy semantics; the requested legacy version is echoed.
|
|
12
|
+
* - a request carrying `_meta["io.modelcontextprotocol/protocolVersion"]` is served
|
|
13
|
+
* statelessly: `resultType` and `serverInfo` on every result, cache hints on lists,
|
|
14
|
+
* `-32602` for an unknown resource, `-32022` for an unsupported version.
|
|
15
|
+
* - `server/discover` is always answered, so a dual-era client's stdio probe succeeds.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MODERN_PROTOCOL_VERSIONS: readonly ["2026-07-28"];
|
|
18
|
+
export declare const LEGACY_PROTOCOL_VERSIONS: readonly ["2025-11-25", "2025-06-18", "2024-11-05"];
|
|
19
|
+
export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly string[];
|
|
20
|
+
/** Offered to a legacy client whose requested version we do not know. */
|
|
21
|
+
export declare const LATEST_LEGACY_PROTOCOL_VERSION: "2025-11-25";
|
|
22
|
+
export declare const META_PROTOCOL_VERSION = "io.modelcontextprotocol/protocolVersion";
|
|
23
|
+
export declare const META_CLIENT_CAPABILITIES = "io.modelcontextprotocol/clientCapabilities";
|
|
24
|
+
export declare const META_SERVER_INFO = "io.modelcontextprotocol/serverInfo";
|
|
25
|
+
export declare const ERROR_INVALID_PARAMS = -32602;
|
|
26
|
+
export declare const ERROR_UNSUPPORTED_PROTOCOL_VERSION = -32022;
|
|
27
|
+
/** 2025-11-25 and earlier; replaced by -32602 in 2026-07-28. */
|
|
28
|
+
export declare const ERROR_LEGACY_RESOURCE_NOT_FOUND = -32002;
|
|
29
|
+
/** Tool catalogue is fixed for the life of a stdio process; five minutes is a conservative hint. */
|
|
30
|
+
export declare const TOOLS_LIST_TTL_MS = 300000;
|
|
31
|
+
export declare const DISCOVER_TTL_MS = 3600000;
|
|
32
|
+
export interface ServerIdentity {
|
|
33
|
+
name: string;
|
|
34
|
+
version: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ProtocolContext {
|
|
37
|
+
/** True when the request used 2026-07-28 per-request metadata. */
|
|
38
|
+
modern: boolean;
|
|
39
|
+
/** Version the request declared, or undefined for legacy requests. */
|
|
40
|
+
version?: string;
|
|
41
|
+
/** Validated W3C traceparent, if the client propagated one. */
|
|
42
|
+
traceparent?: string;
|
|
43
|
+
tracestate?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface JsonRpcMessage {
|
|
46
|
+
jsonrpc?: string;
|
|
47
|
+
id?: string | number | null;
|
|
48
|
+
method?: string;
|
|
49
|
+
params?: unknown;
|
|
50
|
+
}
|
|
51
|
+
export interface JsonRpcResponse {
|
|
52
|
+
jsonrpc: '2.0';
|
|
53
|
+
id?: string | number | null;
|
|
54
|
+
result?: Record<string, unknown>;
|
|
55
|
+
error?: {
|
|
56
|
+
code: number;
|
|
57
|
+
message: string;
|
|
58
|
+
data?: unknown;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export type Preflight = {
|
|
62
|
+
response: JsonRpcResponse;
|
|
63
|
+
context?: undefined;
|
|
64
|
+
} | {
|
|
65
|
+
response?: undefined;
|
|
66
|
+
context: ProtocolContext;
|
|
67
|
+
};
|
|
68
|
+
/** The CLI package version, read once from package.json (dist/src/ → ../../). */
|
|
69
|
+
export declare function rufloPackageVersion(): string;
|
|
70
|
+
export declare function defaultServerIdentity(): ServerIdentity;
|
|
71
|
+
/** Pick the legacy version to answer `initialize` with (spec: echo if supported, else our latest). */
|
|
72
|
+
export declare function negotiateLegacyVersion(requested: unknown): string;
|
|
73
|
+
export declare function isValidTraceparent(value: unknown): value is string;
|
|
74
|
+
/**
|
|
75
|
+
* Decide how to serve one inbound message before the handler's own switch runs.
|
|
76
|
+
* Returns a complete response for `initialize`, `server/discover` and protocol
|
|
77
|
+
* errors; otherwise returns the context the handler passes to `finalizeResponse`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function protocolPreflight(message: JsonRpcMessage, capabilities: Record<string, unknown>, identity?: ServerIdentity): Preflight;
|
|
80
|
+
/** The resource-not-found code the request's revision expects. */
|
|
81
|
+
export declare function resourceNotFoundCode(context: ProtocolContext): number;
|
|
82
|
+
/** Stable, name-ordered copy so `tools/list` is byte-identical across calls (prompt-cache friendly). */
|
|
83
|
+
export declare function sortToolsDeterministically<T extends {
|
|
84
|
+
name: string;
|
|
85
|
+
}>(tools: readonly T[]): T[];
|
|
86
|
+
/** Tool-call context fields to forward, so handlers and logs can join the caller's trace. */
|
|
87
|
+
export declare function traceContextFields(context: ProtocolContext): Record<string, string>;
|
|
88
|
+
/** Add the 2026-07-28 result fields to a handler's response. Legacy responses pass through untouched. */
|
|
89
|
+
export declare function finalizeResponse(response: JsonRpcResponse | null, method: string | undefined, context: ProtocolContext, identity?: ServerIdentity): JsonRpcResponse | null;
|
|
90
|
+
//# sourceMappingURL=mcp-protocol.d.ts.map
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP protocol-version handling for the stdio servers (ADR-524).
|
|
3
|
+
*
|
|
4
|
+
* Ruflo ships three stdio entry points (`bin/cli.js` auto-detect, `bin/mcp-server.js`,
|
|
5
|
+
* and `MCPServerManager` in mcp-server.ts). All three answered `initialize` with a
|
|
6
|
+
* hardcoded `2024-11-05` and knew nothing of the 2026-07-28 revision, which drops the
|
|
7
|
+
* handshake and carries the version on every request. This module is the one place
|
|
8
|
+
* that decides which revision a message speaks, so the three handlers cannot drift.
|
|
9
|
+
*
|
|
10
|
+
* Dual-era behaviour (spec: basic/versioning):
|
|
11
|
+
* - `initialize` selects legacy semantics; the requested legacy version is echoed.
|
|
12
|
+
* - a request carrying `_meta["io.modelcontextprotocol/protocolVersion"]` is served
|
|
13
|
+
* statelessly: `resultType` and `serverInfo` on every result, cache hints on lists,
|
|
14
|
+
* `-32602` for an unknown resource, `-32022` for an unsupported version.
|
|
15
|
+
* - `server/discover` is always answered, so a dual-era client's stdio probe succeeds.
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
export const MODERN_PROTOCOL_VERSIONS = ['2026-07-28'];
|
|
19
|
+
// 2025-03-26 is deliberately absent: it is the one revision that allowed JSON-RPC batching, which these
|
|
20
|
+
// line-at-a-time handlers do not accept. A client asking for it is offered 2025-11-25 instead.
|
|
21
|
+
export const LEGACY_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2024-11-05'];
|
|
22
|
+
export const SUPPORTED_PROTOCOL_VERSIONS = [
|
|
23
|
+
...MODERN_PROTOCOL_VERSIONS,
|
|
24
|
+
...LEGACY_PROTOCOL_VERSIONS,
|
|
25
|
+
];
|
|
26
|
+
/** Offered to a legacy client whose requested version we do not know. */
|
|
27
|
+
export const LATEST_LEGACY_PROTOCOL_VERSION = LEGACY_PROTOCOL_VERSIONS[0];
|
|
28
|
+
export const META_PROTOCOL_VERSION = 'io.modelcontextprotocol/protocolVersion';
|
|
29
|
+
export const META_CLIENT_CAPABILITIES = 'io.modelcontextprotocol/clientCapabilities';
|
|
30
|
+
export const META_SERVER_INFO = 'io.modelcontextprotocol/serverInfo';
|
|
31
|
+
export const ERROR_INVALID_PARAMS = -32602;
|
|
32
|
+
export const ERROR_UNSUPPORTED_PROTOCOL_VERSION = -32022;
|
|
33
|
+
/** 2025-11-25 and earlier; replaced by -32602 in 2026-07-28. */
|
|
34
|
+
export const ERROR_LEGACY_RESOURCE_NOT_FOUND = -32002;
|
|
35
|
+
/** Tool catalogue is fixed for the life of a stdio process; five minutes is a conservative hint. */
|
|
36
|
+
export const TOOLS_LIST_TTL_MS = 300_000;
|
|
37
|
+
export const DISCOVER_TTL_MS = 3_600_000;
|
|
38
|
+
const LIST_METHODS = new Set(['tools/list', 'prompts/list', 'resources/list', 'resources/templates/list']);
|
|
39
|
+
// W3C Trace Context: version-traceid-parentid-flags, lowercase hex, ids not all zero, version != ff.
|
|
40
|
+
const TRACEPARENT = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
41
|
+
let cachedVersion;
|
|
42
|
+
/** The CLI package version, read once from package.json (dist/src/ → ../../). */
|
|
43
|
+
export function rufloPackageVersion() {
|
|
44
|
+
if (cachedVersion)
|
|
45
|
+
return cachedVersion;
|
|
46
|
+
for (const rel of ['../../package.json', '../package.json']) {
|
|
47
|
+
try {
|
|
48
|
+
const pkg = JSON.parse(readFileSync(new URL(rel, import.meta.url), 'utf8'));
|
|
49
|
+
if (pkg.name === '@claude-flow/cli' && typeof pkg.version === 'string') {
|
|
50
|
+
cachedVersion = pkg.version;
|
|
51
|
+
return cachedVersion;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// try the next candidate
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
cachedVersion = '0.0.0';
|
|
59
|
+
return cachedVersion;
|
|
60
|
+
}
|
|
61
|
+
export function defaultServerIdentity() {
|
|
62
|
+
return { name: 'ruflo', version: rufloPackageVersion() };
|
|
63
|
+
}
|
|
64
|
+
/** Pick the legacy version to answer `initialize` with (spec: echo if supported, else our latest). */
|
|
65
|
+
export function negotiateLegacyVersion(requested) {
|
|
66
|
+
return typeof requested === 'string' && LEGACY_PROTOCOL_VERSIONS.includes(requested)
|
|
67
|
+
? requested
|
|
68
|
+
: LATEST_LEGACY_PROTOCOL_VERSION;
|
|
69
|
+
}
|
|
70
|
+
export function isValidTraceparent(value) {
|
|
71
|
+
if (typeof value !== 'string')
|
|
72
|
+
return false;
|
|
73
|
+
const m = TRACEPARENT.exec(value);
|
|
74
|
+
if (!m)
|
|
75
|
+
return false;
|
|
76
|
+
const [, version, traceId, parentId] = m;
|
|
77
|
+
return version !== 'ff' && !/^0+$/.test(traceId) && !/^0+$/.test(parentId);
|
|
78
|
+
}
|
|
79
|
+
function metaOf(params) {
|
|
80
|
+
if (!params || typeof params !== 'object')
|
|
81
|
+
return undefined;
|
|
82
|
+
const meta = params._meta;
|
|
83
|
+
return meta && typeof meta === 'object' && !Array.isArray(meta) ? meta : undefined;
|
|
84
|
+
}
|
|
85
|
+
function error(id, code, message, data) {
|
|
86
|
+
return { jsonrpc: '2.0', id: id ?? null, error: data === undefined ? { code, message } : { code, message, data } };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Decide how to serve one inbound message before the handler's own switch runs.
|
|
90
|
+
* Returns a complete response for `initialize`, `server/discover` and protocol
|
|
91
|
+
* errors; otherwise returns the context the handler passes to `finalizeResponse`.
|
|
92
|
+
*/
|
|
93
|
+
export function protocolPreflight(message, capabilities, identity = defaultServerIdentity()) {
|
|
94
|
+
const meta = metaOf(message.params);
|
|
95
|
+
const traceparent = isValidTraceparent(meta?.traceparent) ? meta.traceparent : undefined;
|
|
96
|
+
const tracestate = traceparent && typeof meta?.tracestate === 'string' && meta.tracestate.length <= 512
|
|
97
|
+
? meta.tracestate
|
|
98
|
+
: undefined;
|
|
99
|
+
const declared = meta?.[META_PROTOCOL_VERSION];
|
|
100
|
+
const isNotification = message.id === undefined || message.id === null;
|
|
101
|
+
if (message.method === 'initialize') {
|
|
102
|
+
const requested = message.params?.protocolVersion;
|
|
103
|
+
return {
|
|
104
|
+
response: {
|
|
105
|
+
jsonrpc: '2.0',
|
|
106
|
+
id: message.id,
|
|
107
|
+
result: { protocolVersion: negotiateLegacyVersion(requested), serverInfo: identity, capabilities },
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (declared !== undefined && !isNotification) {
|
|
112
|
+
if (typeof declared !== 'string' || !SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
|
|
113
|
+
return {
|
|
114
|
+
response: error(message.id, ERROR_UNSUPPORTED_PROTOCOL_VERSION, 'Unsupported protocol version', {
|
|
115
|
+
supported: [...SUPPORTED_PROTOCOL_VERSIONS],
|
|
116
|
+
requested: declared,
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const modern = MODERN_PROTOCOL_VERSIONS.includes(declared);
|
|
121
|
+
if (modern) {
|
|
122
|
+
const caps = meta?.[META_CLIENT_CAPABILITIES];
|
|
123
|
+
if (message.method !== 'server/discover' && (!caps || typeof caps !== 'object' || Array.isArray(caps))) {
|
|
124
|
+
return { response: error(message.id, ERROR_INVALID_PARAMS, `Missing required _meta field: ${META_CLIENT_CAPABILITIES}`) };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (message.method === 'server/discover')
|
|
128
|
+
return { response: discoverResponse(message.id, capabilities, identity) };
|
|
129
|
+
return { context: { modern, version: declared, traceparent, tracestate } };
|
|
130
|
+
}
|
|
131
|
+
if (message.method === 'server/discover')
|
|
132
|
+
return { response: discoverResponse(message.id, capabilities, identity) };
|
|
133
|
+
return { context: { modern: false, traceparent, tracestate } };
|
|
134
|
+
}
|
|
135
|
+
function discoverResponse(id, capabilities, identity) {
|
|
136
|
+
return {
|
|
137
|
+
jsonrpc: '2.0',
|
|
138
|
+
id,
|
|
139
|
+
result: {
|
|
140
|
+
resultType: 'complete',
|
|
141
|
+
supportedVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
|
|
142
|
+
capabilities,
|
|
143
|
+
_meta: { [META_SERVER_INFO]: identity },
|
|
144
|
+
ttlMs: DISCOVER_TTL_MS,
|
|
145
|
+
cacheScope: 'private',
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** The resource-not-found code the request's revision expects. */
|
|
150
|
+
export function resourceNotFoundCode(context) {
|
|
151
|
+
return context.modern ? ERROR_INVALID_PARAMS : ERROR_LEGACY_RESOURCE_NOT_FOUND;
|
|
152
|
+
}
|
|
153
|
+
/** Stable, name-ordered copy so `tools/list` is byte-identical across calls (prompt-cache friendly). */
|
|
154
|
+
export function sortToolsDeterministically(tools) {
|
|
155
|
+
return [...tools].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
156
|
+
}
|
|
157
|
+
/** Tool-call context fields to forward, so handlers and logs can join the caller's trace. */
|
|
158
|
+
export function traceContextFields(context) {
|
|
159
|
+
const out = {};
|
|
160
|
+
if (context.traceparent)
|
|
161
|
+
out.traceparent = context.traceparent;
|
|
162
|
+
if (context.tracestate)
|
|
163
|
+
out.tracestate = context.tracestate;
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
/** Add the 2026-07-28 result fields to a handler's response. Legacy responses pass through untouched. */
|
|
167
|
+
export function finalizeResponse(response, method, context, identity = defaultServerIdentity()) {
|
|
168
|
+
if (!response || !context.modern || !response.result)
|
|
169
|
+
return response;
|
|
170
|
+
const result = { resultType: 'complete', ...response.result };
|
|
171
|
+
const priorMeta = result._meta && typeof result._meta === 'object' ? result._meta : {};
|
|
172
|
+
result._meta = { ...priorMeta, [META_SERVER_INFO]: identity };
|
|
173
|
+
if (method && LIST_METHODS.has(method)) {
|
|
174
|
+
result.ttlMs = TOOLS_LIST_TTL_MS;
|
|
175
|
+
result.cacheScope = 'private';
|
|
176
|
+
}
|
|
177
|
+
return { ...response, result };
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=mcp-protocol.js.map
|
package/dist/src/mcp-server.d.ts
CHANGED
package/dist/src/mcp-server.js
CHANGED
|
@@ -26,6 +26,7 @@ import * as os from 'os';
|
|
|
26
26
|
import { fileURLToPath } from 'url';
|
|
27
27
|
import { dirname } from 'path';
|
|
28
28
|
import { trackRequest } from './mcp-tools/request-tracker.js';
|
|
29
|
+
import { defaultServerIdentity, finalizeResponse, protocolPreflight, resourceNotFoundCode, sortToolsDeterministically, traceContextFields, } from './mcp-protocol.js';
|
|
29
30
|
import { isPolicyEnforcementEnabled, loadMcpPolicy, evaluateToolCall, } from './mcp-tools/policy-enforcer.js';
|
|
30
31
|
/** A failed task can be valid data; only an explicit error denotes tool failure. */
|
|
31
32
|
function hasToolError(result) {
|
|
@@ -275,6 +276,11 @@ export function assessMcpSchemaOverhead(tools, contextWindowTokens) {
|
|
|
275
276
|
: 'normal',
|
|
276
277
|
};
|
|
277
278
|
}
|
|
279
|
+
/** Capabilities every ruflo stdio server advertises (initialize and server/discover). */
|
|
280
|
+
const MCP_STDIO_CAPABILITIES = {
|
|
281
|
+
tools: { listChanged: true },
|
|
282
|
+
resources: {},
|
|
283
|
+
};
|
|
278
284
|
/**
|
|
279
285
|
* MCP Server Manager
|
|
280
286
|
*
|
|
@@ -616,7 +622,7 @@ export class MCPServerManager extends EventEmitter {
|
|
|
616
622
|
});
|
|
617
623
|
// Import the tool registry
|
|
618
624
|
const { listMCPTools, callMCPTool, hasTool } = await import('./mcp-client.js');
|
|
619
|
-
const VERSION =
|
|
625
|
+
const VERSION = defaultServerIdentity().version;
|
|
620
626
|
const sessionId = `mcp-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
621
627
|
// Log to stderr to not corrupt stdout
|
|
622
628
|
console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Starting in stdio mode`);
|
|
@@ -663,10 +669,7 @@ export class MCPServerManager extends EventEmitter {
|
|
|
663
669
|
serverInfo: {
|
|
664
670
|
name: 'ruflo',
|
|
665
671
|
version: VERSION,
|
|
666
|
-
capabilities:
|
|
667
|
-
tools: { listChanged: true },
|
|
668
|
-
resources: {},
|
|
669
|
-
},
|
|
672
|
+
capabilities: MCP_STDIO_CAPABILITIES,
|
|
670
673
|
},
|
|
671
674
|
},
|
|
672
675
|
});
|
|
@@ -722,7 +725,6 @@ export class MCPServerManager extends EventEmitter {
|
|
|
722
725
|
* Handle incoming MCP message
|
|
723
726
|
*/
|
|
724
727
|
async handleMCPMessage(message, sessionId) {
|
|
725
|
-
const { listMCPTools, callMCPTool, hasTool } = await import('./mcp-client.js');
|
|
726
728
|
if (!message.method) {
|
|
727
729
|
return {
|
|
728
730
|
jsonrpc: '2.0',
|
|
@@ -730,24 +732,20 @@ export class MCPServerManager extends EventEmitter {
|
|
|
730
732
|
error: { code: -32600, message: 'Invalid Request: missing method' },
|
|
731
733
|
};
|
|
732
734
|
}
|
|
735
|
+
// ADR-524: one decision point for which MCP revision this message speaks.
|
|
736
|
+
const preflight = protocolPreflight(message, MCP_STDIO_CAPABILITIES);
|
|
737
|
+
if (preflight.response)
|
|
738
|
+
return preflight.response;
|
|
739
|
+
const response = await this.dispatchMCPMessage(message, sessionId, preflight.context);
|
|
740
|
+
return finalizeResponse(response, message.method, preflight.context);
|
|
741
|
+
}
|
|
742
|
+
async dispatchMCPMessage(message, sessionId, protocol) {
|
|
743
|
+
const { listMCPTools, callMCPTool, hasTool } = await import('./mcp-client.js');
|
|
733
744
|
const params = (message.params || {});
|
|
734
745
|
try {
|
|
735
746
|
switch (message.method) {
|
|
736
|
-
case 'initialize':
|
|
737
|
-
return {
|
|
738
|
-
jsonrpc: '2.0',
|
|
739
|
-
id: message.id,
|
|
740
|
-
result: {
|
|
741
|
-
protocolVersion: '2024-11-05',
|
|
742
|
-
serverInfo: { name: 'ruflo', version: '3.0.0' },
|
|
743
|
-
capabilities: {
|
|
744
|
-
tools: { listChanged: true },
|
|
745
|
-
resources: {},
|
|
746
|
-
},
|
|
747
|
-
},
|
|
748
|
-
};
|
|
749
747
|
case 'tools/list':
|
|
750
|
-
const tools = filterAdvertisedMcpTools(listMCPTools(), this.options.tools);
|
|
748
|
+
const tools = sortToolsDeterministically(filterAdvertisedMcpTools(listMCPTools(), this.options.tools));
|
|
751
749
|
return {
|
|
752
750
|
jsonrpc: '2.0',
|
|
753
751
|
id: message.id,
|
|
@@ -781,7 +779,7 @@ export class MCPServerManager extends EventEmitter {
|
|
|
781
779
|
}
|
|
782
780
|
}
|
|
783
781
|
try {
|
|
784
|
-
const result = await callMCPTool(toolName, toolParams, { sessionId, transport: 'stdio' });
|
|
782
|
+
const result = await callMCPTool(toolName, toolParams, { sessionId, transport: 'stdio', ...traceContextFields(protocol) });
|
|
785
783
|
const isError = hasToolError(result);
|
|
786
784
|
trackRequest(toolName, !isError);
|
|
787
785
|
return {
|
|
@@ -808,7 +806,7 @@ export class MCPServerManager extends EventEmitter {
|
|
|
808
806
|
case 'resources/templates/list':
|
|
809
807
|
return { jsonrpc: '2.0', id: message.id, result: { resourceTemplates: [] } };
|
|
810
808
|
case 'resources/read':
|
|
811
|
-
return { jsonrpc: '2.0', id: message.id, error: { code:
|
|
809
|
+
return { jsonrpc: '2.0', id: message.id, error: { code: resourceNotFoundCode(protocol), message: 'Resource not found' } };
|
|
812
810
|
case 'notifications/initialized':
|
|
813
811
|
// Client notification - no response needed
|
|
814
812
|
console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Client initialized`);
|
|
@@ -147,10 +147,18 @@ export async function callAnthropicMessages(input) {
|
|
|
147
147
|
// checked in that order — matching callOllamaCompat's own precedence).
|
|
148
148
|
const resolvedBaseUrl = process.env.OLLAMA_BASE_URL || persistedOllama?.baseUrl;
|
|
149
149
|
const isSelfHosted = !!resolvedBaseUrl && !/^https:\/\/ollama\.com\/?$/i.test(resolvedBaseUrl);
|
|
150
|
+
// The persisted baseUrl comes from project config, which a cloned repo
|
|
151
|
+
// can commit. Never hand the environment's OLLAMA_API_KEY to an endpoint
|
|
152
|
+
// the project chose: that endpoint gets only the key saved beside it (or
|
|
153
|
+
// none). The env key goes to OLLAMA_BASE_URL, or to Ollama Cloud itself.
|
|
154
|
+
const baseFromProject = !process.env.OLLAMA_BASE_URL && !!persistedOllama?.baseUrl && isSelfHosted;
|
|
155
|
+
const apiKey = baseFromProject
|
|
156
|
+
? persistedOllama?.apiKey || 'local'
|
|
157
|
+
: ollamaKey || persistedOllama?.apiKey || 'local';
|
|
150
158
|
if (ollamaKey || persistedOllama?.apiKey || isSelfHosted) {
|
|
151
159
|
return callOllamaCompat({
|
|
152
160
|
...input,
|
|
153
|
-
apiKey
|
|
161
|
+
apiKey,
|
|
154
162
|
baseUrl: resolvedBaseUrl,
|
|
155
163
|
model: input.model,
|
|
156
164
|
// Local default for tier aliases / Anthropic ids (see
|
|
@@ -5,5 +5,18 @@
|
|
|
5
5
|
* Includes model routing integration for intelligent model selection.
|
|
6
6
|
*/
|
|
7
7
|
import { type MCPTool } from './types.js';
|
|
8
|
+
/** Live canonical registry members, excluding stale or duplicate swarm IDs. */
|
|
9
|
+
export declare function getLiveAgentIds(agentIds: string[]): string[];
|
|
10
|
+
export declare const AGENT_STATUSES: readonly ["idle", "busy", "terminated"];
|
|
11
|
+
type AgentLifecycleStatus = (typeof AGENT_STATUSES)[number];
|
|
12
|
+
/** Canonical lifecycle status (trimmed, any case, known aliases), or null. */
|
|
13
|
+
export declare function agentStatusOf(value: unknown): AgentLifecycleStatus | null;
|
|
14
|
+
/**
|
|
15
|
+
* Statuses `agent_list` should keep. `null` means no status filter:
|
|
16
|
+
* omitted, empty, whitespace, or `all` (any case, including inside a comma list).
|
|
17
|
+
* The caller still hides terminated agents unless includeTerminated is set.
|
|
18
|
+
*/
|
|
19
|
+
export declare function agentListStatusFilter(status: unknown): string[] | null;
|
|
8
20
|
export declare const agentTools: MCPTool[];
|
|
21
|
+
export {};
|
|
9
22
|
//# sourceMappingURL=agent-tools.d.ts.map
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
|
-
import { readRecordStore } from './record-store.js';
|
|
9
|
+
import { nullPrototypeRecords, readRecordStore } from './record-store.js';
|
|
10
10
|
import { getProjectCwd } from './types.js';
|
|
11
11
|
import { validateIdentifier, validateText, validateAgentSpawn } from './validate-input.js';
|
|
12
12
|
import { executeAgentTask } from './agent-execute-core.js';
|
|
@@ -36,6 +36,11 @@ function ensureAgentDir() {
|
|
|
36
36
|
function loadAgentStore() {
|
|
37
37
|
return readRecordStore(getAgentPath(), 'agents', () => ({ agents: {}, version: '3.0.0' }));
|
|
38
38
|
}
|
|
39
|
+
/** Live canonical registry members, excluding stale or duplicate swarm IDs. */
|
|
40
|
+
export function getLiveAgentIds(agentIds) {
|
|
41
|
+
const { agents } = loadAgentStore();
|
|
42
|
+
return [...new Set(agentIds)].filter(id => agents[id] && agents[id].status !== 'terminated');
|
|
43
|
+
}
|
|
39
44
|
function saveAgentStore(store) {
|
|
40
45
|
ensureAgentDir();
|
|
41
46
|
writeFileSync(getAgentPath(), JSON.stringify(store, null, 2), 'utf-8');
|
|
@@ -50,14 +55,14 @@ function loadHiveAgents() {
|
|
|
50
55
|
if (existsSync(path)) {
|
|
51
56
|
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
52
57
|
if (data && typeof data.agents === 'object' && data.agents) {
|
|
53
|
-
return data.agents;
|
|
58
|
+
return nullPrototypeRecords(data.agents);
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
62
|
catch {
|
|
58
63
|
// Ignore — hive store is optional/best-effort.
|
|
59
64
|
}
|
|
60
|
-
return
|
|
65
|
+
return nullPrototypeRecords(undefined);
|
|
61
66
|
}
|
|
62
67
|
/**
|
|
63
68
|
* #1916: merged view of every tracked agent — the canonical agent store
|
|
@@ -65,7 +70,41 @@ function loadHiveAgents() {
|
|
|
65
70
|
* wins (it carries model-routing + lastResult that the hive store omits).
|
|
66
71
|
*/
|
|
67
72
|
function loadAllAgents() {
|
|
68
|
-
return
|
|
73
|
+
return Object.assign(nullPrototypeRecords(undefined), loadHiveAgents(), loadAgentStore().agents);
|
|
74
|
+
}
|
|
75
|
+
export const AGENT_STATUSES = ['idle', 'busy', 'terminated'];
|
|
76
|
+
// Spellings callers write for the three stored states. v2 `agent/list` used
|
|
77
|
+
// "active" for a working agent; hive liveness folds running/working into busy.
|
|
78
|
+
const AGENT_STATUS_ALIASES = {
|
|
79
|
+
active: 'busy', running: 'busy', working: 'busy',
|
|
80
|
+
ready: 'idle', waiting: 'idle',
|
|
81
|
+
stopped: 'terminated', inactive: 'terminated', offline: 'terminated',
|
|
82
|
+
};
|
|
83
|
+
/** Canonical lifecycle status (trimmed, any case, known aliases), or null. */
|
|
84
|
+
export function agentStatusOf(value) {
|
|
85
|
+
if (typeof value !== 'string')
|
|
86
|
+
return null;
|
|
87
|
+
const key = value.trim().toLowerCase();
|
|
88
|
+
if (AGENT_STATUSES.includes(key))
|
|
89
|
+
return key;
|
|
90
|
+
return Object.hasOwn(AGENT_STATUS_ALIASES, key) ? AGENT_STATUS_ALIASES[key] : null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Statuses `agent_list` should keep. `null` means no status filter:
|
|
94
|
+
* omitted, empty, whitespace, or `all` (any case, including inside a comma list).
|
|
95
|
+
* The caller still hides terminated agents unless includeTerminated is set.
|
|
96
|
+
*/
|
|
97
|
+
export function agentListStatusFilter(status) {
|
|
98
|
+
if (typeof status !== 'string' || status.trim() === '')
|
|
99
|
+
return null;
|
|
100
|
+
const canonical = status.split(',').map(part => part.trim()).filter(Boolean).map(part => {
|
|
101
|
+
if (part.toLowerCase() === 'all')
|
|
102
|
+
return 'all';
|
|
103
|
+
return agentStatusOf(part) ?? part.toLowerCase();
|
|
104
|
+
});
|
|
105
|
+
if (canonical.length === 0 || canonical.includes('all'))
|
|
106
|
+
return null;
|
|
107
|
+
return canonical;
|
|
69
108
|
}
|
|
70
109
|
// Default model mappings for agent types (can be overridden)
|
|
71
110
|
const AGENT_TYPE_MODEL_DEFAULTS = {
|
|
@@ -568,17 +607,24 @@ export const agentTools = [
|
|
|
568
607
|
inputSchema: {
|
|
569
608
|
type: 'object',
|
|
570
609
|
properties: {
|
|
571
|
-
status: { type: 'string', description: 'Filter by status' },
|
|
610
|
+
status: { type: 'string', description: 'Filter by status (idle, busy, terminated; case-insensitive, aliases such as active/running/stopped). "all" or empty applies no status filter. Comma-separated values match any of them.' },
|
|
572
611
|
domain: { type: 'string', description: 'Filter by domain' },
|
|
573
612
|
agentType: { type: 'string', description: 'Filter by agent type' },
|
|
574
613
|
includeTerminated: { type: 'boolean', description: 'Include terminated agents' },
|
|
575
614
|
},
|
|
576
615
|
},
|
|
577
616
|
handler: async (input) => {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
617
|
+
// Validate each status token. Commas separate values (`active,idle`);
|
|
618
|
+
// the whole string is not one identifier.
|
|
619
|
+
if (typeof input.status === 'string' && input.status.trim() !== '') {
|
|
620
|
+
for (const part of input.status.split(',')) {
|
|
621
|
+
const token = part.trim();
|
|
622
|
+
if (!token || token.toLowerCase() === 'all')
|
|
623
|
+
continue;
|
|
624
|
+
const v = validateIdentifier(token, 'status');
|
|
625
|
+
if (!v.valid)
|
|
626
|
+
return { agents: [], total: 0, error: `Input validation failed: ${v.error}` };
|
|
627
|
+
}
|
|
582
628
|
}
|
|
583
629
|
if (input.domain) {
|
|
584
630
|
const v = validateIdentifier(input.domain, 'domain');
|
|
@@ -591,9 +637,14 @@ export const agentTools = [
|
|
|
591
637
|
return { agents: [], total: 0, error: `Input validation failed: ${v.error}` };
|
|
592
638
|
}
|
|
593
639
|
let agents = Object.values(loadAllAgents()); // #1916: includes hive-mind-spawned workers
|
|
594
|
-
// Filter by status
|
|
595
|
-
|
|
596
|
-
|
|
640
|
+
// Filter by status. `all` and empty mean no status filter (#4732),
|
|
641
|
+
// matching task_list: comma-separated, case-insensitive, known aliases.
|
|
642
|
+
const statuses = agentListStatusFilter(input.status);
|
|
643
|
+
if (statuses) {
|
|
644
|
+
agents = agents.filter(a => {
|
|
645
|
+
const stored = agentStatusOf(a.status) ?? String(a.status).trim().toLowerCase();
|
|
646
|
+
return statuses.includes(stored);
|
|
647
|
+
});
|
|
597
648
|
}
|
|
598
649
|
else if (!input.includeTerminated) {
|
|
599
650
|
agents = agents.filter(a => a.status !== 'terminated');
|
|
@@ -143,6 +143,20 @@ export interface MergeResult {
|
|
|
143
143
|
skippedUnverified: number;
|
|
144
144
|
skippedOversize: number;
|
|
145
145
|
skippedHopLimit: number;
|
|
146
|
+
/** Envelopes whose `origin` names a node the signature does not belong to. */
|
|
147
|
+
skippedForeignOrigin: number;
|
|
148
|
+
/** Highest `seq` among envelopes merged or already held; the next sync starts after it. */
|
|
149
|
+
acceptedMaxSeq: number;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Who may have signed an envelope. `origin` is inside the signature, but a
|
|
153
|
+
* signature only proves which key signed, not which node the envelope names.
|
|
154
|
+
* An envelope is accepted only when its origin is the syncing peer itself or
|
|
155
|
+
* another pinned peer, and it verifies against that origin's pinned key.
|
|
156
|
+
*/
|
|
157
|
+
export interface OriginKeys {
|
|
158
|
+
peerNodeId: string;
|
|
159
|
+
pinned: ReadonlyMap<string, string>;
|
|
146
160
|
}
|
|
147
161
|
/**
|
|
148
162
|
* Union-merge verified envelopes from a peer into the local room log.
|
|
@@ -150,8 +164,11 @@ export interface MergeResult {
|
|
|
150
164
|
* Idempotent: `envelopeId` is the merge key, so replaying the same batch is a
|
|
151
165
|
* no-op. Anything that fails verification is dropped and counted rather than
|
|
152
166
|
* quarantined — a receiver has no use for an envelope it cannot attribute.
|
|
167
|
+
*
|
|
168
|
+
* `hops` is not signed (relays change it), so it only limits propagation and
|
|
169
|
+
* is never trusted for anything else.
|
|
153
170
|
*/
|
|
154
|
-
export declare function mergeEnvelopes(basePath: string, roomId: string, incoming: SignedEnvelope[], peerPublicKey: string): Promise<MergeResult>;
|
|
171
|
+
export declare function mergeEnvelopes(basePath: string, roomId: string, incoming: SignedEnvelope[], peerPublicKey: string, origins?: OriginKeys): Promise<MergeResult>;
|
|
155
172
|
/** Pull one room from one peer and merge what verifies. */
|
|
156
173
|
export declare function syncRoomFromPeer(basePath: string, peer: FederationPeer, roomId: string, fetchImpl?: typeof fetch): Promise<MergeResult & {
|
|
157
174
|
peerNodeId: string;
|
|
@@ -315,15 +315,19 @@ export function validateRoomId(roomId) {
|
|
|
315
315
|
* Idempotent: `envelopeId` is the merge key, so replaying the same batch is a
|
|
316
316
|
* no-op. Anything that fails verification is dropped and counted rather than
|
|
317
317
|
* quarantined — a receiver has no use for an envelope it cannot attribute.
|
|
318
|
+
*
|
|
319
|
+
* `hops` is not signed (relays change it), so it only limits propagation and
|
|
320
|
+
* is never trusted for anything else.
|
|
318
321
|
*/
|
|
319
|
-
export async function mergeEnvelopes(basePath, roomId, incoming, peerPublicKey) {
|
|
322
|
+
export async function mergeEnvelopes(basePath, roomId, incoming, peerPublicKey, origins) {
|
|
320
323
|
validateRoomId(roomId);
|
|
321
324
|
ensureDir(basePath);
|
|
322
325
|
const result = {
|
|
323
326
|
merged: 0, skippedDuplicate: 0, skippedUnverified: 0, skippedOversize: 0, skippedHopLimit: 0,
|
|
327
|
+
skippedForeignOrigin: 0, acceptedMaxSeq: 0,
|
|
324
328
|
};
|
|
325
329
|
const existing = readEnvelopes(basePath, roomId);
|
|
326
|
-
const seen = new
|
|
330
|
+
const seen = new Map(existing.map(e => [e.envelopeId, Number(e.seq) || 0]));
|
|
327
331
|
const logPath = roomLogPath(basePath, roomId);
|
|
328
332
|
const batch = incoming.slice(0, MAX_ENVELOPES_PER_SYNC);
|
|
329
333
|
for (const env of batch) {
|
|
@@ -337,6 +341,7 @@ export async function mergeEnvelopes(basePath, roomId, incoming, peerPublicKey)
|
|
|
337
341
|
}
|
|
338
342
|
if (seen.has(env.envelopeId)) {
|
|
339
343
|
result.skippedDuplicate++;
|
|
344
|
+
result.acceptedMaxSeq = Math.max(result.acceptedMaxSeq, seen.get(env.envelopeId) ?? 0);
|
|
340
345
|
continue;
|
|
341
346
|
}
|
|
342
347
|
if (JSON.stringify(env).length > MAX_ENVELOPE_BYTES) {
|
|
@@ -347,12 +352,22 @@ export async function mergeEnvelopes(basePath, roomId, incoming, peerPublicKey)
|
|
|
347
352
|
result.skippedHopLimit++;
|
|
348
353
|
continue;
|
|
349
354
|
}
|
|
350
|
-
|
|
355
|
+
let signerKey = peerPublicKey;
|
|
356
|
+
if (origins) {
|
|
357
|
+
const key = env.origin === origins.peerNodeId ? peerPublicKey : typeof env.origin === 'string' ? origins.pinned.get(env.origin) : undefined;
|
|
358
|
+
if (!key) {
|
|
359
|
+
result.skippedForeignOrigin++;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
signerKey = key;
|
|
363
|
+
}
|
|
364
|
+
if (!(await verifyEnvelope(env, signerKey))) {
|
|
351
365
|
result.skippedUnverified++;
|
|
352
366
|
continue;
|
|
353
367
|
}
|
|
354
368
|
appendFileSync(logPath, JSON.stringify({ ...env, hops: (env.hops ?? 0) + 1 }) + '\n');
|
|
355
|
-
seen.
|
|
369
|
+
seen.set(env.envelopeId, Number(env.seq) || 0);
|
|
370
|
+
result.acceptedMaxSeq = Math.max(result.acceptedMaxSeq, Number(env.seq) || 0);
|
|
356
371
|
result.merged++;
|
|
357
372
|
}
|
|
358
373
|
return result;
|
|
@@ -378,14 +393,15 @@ export async function syncRoomFromPeer(basePath, peer, roomId, fetchImpl = fetch
|
|
|
378
393
|
clearTimeout(timer);
|
|
379
394
|
}
|
|
380
395
|
const envelopes = Array.isArray(body?.envelopes) ? body.envelopes : [];
|
|
381
|
-
const merged = await mergeEnvelopes(basePath, roomId, envelopes, peer.publicKey);
|
|
382
396
|
const peers = readPeers(basePath);
|
|
397
|
+
const pinned = new Map(peers.filter(p => typeof p.publicKey === 'string').map(p => [p.nodeId, p.publicKey]));
|
|
398
|
+
const merged = await mergeEnvelopes(basePath, roomId, envelopes, peer.publicKey, { peerNodeId: peer.nodeId, pinned });
|
|
383
399
|
const rec = peers.find(p => p.nodeId === peer.nodeId);
|
|
384
400
|
if (rec) {
|
|
385
401
|
rec.lastSyncedAt = new Date().toISOString();
|
|
386
402
|
rec.lastSeq = rec.lastSeq ?? {};
|
|
387
|
-
|
|
388
|
-
rec.lastSeq[roomId] =
|
|
403
|
+
// Advance only past envelopes that verified; an unverifiable batch must not skip the cursor forward.
|
|
404
|
+
rec.lastSeq[roomId] = Math.max(since, merged.acceptedMaxSeq);
|
|
389
405
|
writePeers(basePath, peers);
|
|
390
406
|
}
|
|
391
407
|
return { ...merged, peerNodeId: peer.nodeId, roomId };
|