muonroi-cli 1.6.2 → 1.6.3
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/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/mcp/__tests__/client-pool.spec.js +21 -0
- package/dist/src/mcp/client-pool.js +22 -0
- package/dist/src/mcp/runtime.d.ts +11 -0
- package/dist/src/orchestrator/message-processor.js +12 -0
- package/dist/src/pil/__tests__/layer4-gsd.test.js +10 -0
- package/dist/src/pil/layer4-gsd.js +10 -5
- package/dist/src/ui/app.js +3 -9
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const PACKAGE_VERSION = "1.6.
|
|
1
|
+
export declare const PACKAGE_VERSION = "1.6.3";
|
|
2
2
|
export declare const PACKAGE_DESCRIPTION = "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// AUTO-GENERATED by scripts/sync-version.cjs. DO NOT EDIT BY HAND.
|
|
2
2
|
// Sourced from package.json at build time so it survives bun --compile bundling.
|
|
3
|
-
export const PACKAGE_VERSION = "1.6.
|
|
3
|
+
export const PACKAGE_VERSION = "1.6.3";
|
|
4
4
|
export const PACKAGE_DESCRIPTION = "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.";
|
|
5
5
|
//# sourceMappingURL=version.js.map
|
|
@@ -109,6 +109,27 @@ describe("acquireMcpTools — cross-turn client pool", () => {
|
|
|
109
109
|
expect(results.every((r) => r === "pong")).toBe(true);
|
|
110
110
|
expect(connectOneServer).toHaveBeenCalledTimes(2); // 14 failures → exactly ONE shared reconnect
|
|
111
111
|
});
|
|
112
|
+
it("waits for a criticalServerId past the normal deadline so it lands THIS turn (session 584ba476c07a)", async () => {
|
|
113
|
+
// Normal deadline is 500ms (mock). docs connects at ~700ms — past the normal
|
|
114
|
+
// deadline but within the critical window → must be included when critical.
|
|
115
|
+
connectOneServer.mockImplementation((s) => new Promise((res) => {
|
|
116
|
+
if (s.id === "docs")
|
|
117
|
+
setTimeout(() => res(connected(s.id)), 700);
|
|
118
|
+
else
|
|
119
|
+
res(connected(s.id));
|
|
120
|
+
}));
|
|
121
|
+
const b = await acquireMcpTools([srv("docs")], { criticalServerIds: ["docs"], criticalDeadlineMs: 3000 });
|
|
122
|
+
expect(Object.keys(b.tools)).toContain("mcp_docs__ping");
|
|
123
|
+
expect(b.errors).toHaveLength(0);
|
|
124
|
+
});
|
|
125
|
+
it("without criticalServerIds, a slow server is reported still-connecting (available next turn)", async () => {
|
|
126
|
+
connectOneServer.mockImplementation((s) => new Promise((res) => {
|
|
127
|
+
setTimeout(() => res(connected(s.id)), 700);
|
|
128
|
+
}));
|
|
129
|
+
const b = await acquireMcpTools([srv("docs")]);
|
|
130
|
+
expect(Object.keys(b.tools)).not.toContain("mcp_docs__ping");
|
|
131
|
+
expect(b.errors.some((e) => /still connecting/.test(e))).toBe(true);
|
|
132
|
+
});
|
|
112
133
|
it("keys by cwd/config — a different command reconnects rather than reusing", async () => {
|
|
113
134
|
connectOneServer.mockImplementation(async (s) => connected(s.id));
|
|
114
135
|
await acquireMcpTools([
|
|
@@ -179,6 +179,28 @@ export async function acquireMcpTools(servers, opts) {
|
|
|
179
179
|
await Promise.race([Promise.allSettled(attempts), deadline]);
|
|
180
180
|
if (deadlineTimer)
|
|
181
181
|
clearTimeout(deadlineTimer);
|
|
182
|
+
// Critical-server extended wait: a turn that MUST have a specific server (e.g.
|
|
183
|
+
// muonroi-docs on an ecosystem question) waits for just that server's connect
|
|
184
|
+
// beyond the normal deadline, so a cold first-connect is included THIS turn
|
|
185
|
+
// rather than reported "still connecting → next turn". Only the named servers
|
|
186
|
+
// are awaited; everything already settled is untouched (no added latency for
|
|
187
|
+
// normal turns, which pass no criticalServerIds).
|
|
188
|
+
const critical = new Set((opts?.criticalServerIds ?? []).filter(Boolean));
|
|
189
|
+
if (critical.size > 0) {
|
|
190
|
+
const pendingIdx = enabled.map((s, i) => ({ s, i })).filter(({ s, i }) => critical.has(s.id) && !slots[i].done);
|
|
191
|
+
if (pendingIdx.length > 0) {
|
|
192
|
+
const criticalDeadlineMs = Math.max(deadlineMs, opts?.criticalDeadlineMs ?? 8000);
|
|
193
|
+
const extraMs = Math.max(0, criticalDeadlineMs - deadlineMs);
|
|
194
|
+
let extraTimer;
|
|
195
|
+
const extraDeadline = new Promise((resolve) => {
|
|
196
|
+
extraTimer = setTimeout(resolve, extraMs);
|
|
197
|
+
extraTimer.unref?.();
|
|
198
|
+
});
|
|
199
|
+
await Promise.race([Promise.allSettled(pendingIdx.map(({ i }) => attempts[i])), extraDeadline]);
|
|
200
|
+
if (extraTimer)
|
|
201
|
+
clearTimeout(extraTimer);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
182
204
|
for (let i = 0; i < slots.length; i++) {
|
|
183
205
|
const slot = slots[i];
|
|
184
206
|
if (slot.done) {
|
|
@@ -8,6 +8,17 @@ export interface McpToolBundle {
|
|
|
8
8
|
}
|
|
9
9
|
export interface McpBuildOptions {
|
|
10
10
|
onOAuthRequired?: (serverId: string, url: URL) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Server ids the CURRENT turn critically needs (e.g. muonroi-docs on an
|
|
13
|
+
* ecosystem question). acquireMcpTools waits for these specifically beyond the
|
|
14
|
+
* normal build deadline — up to `criticalDeadlineMs` — so a cold first-connect
|
|
15
|
+
* is included THIS turn instead of "ready next turn" (session 584ba476c07a:
|
|
16
|
+
* first ecosystem question missed muonroi-docs while it was still warming).
|
|
17
|
+
* Other servers are unaffected — only the named ones get the extended wait.
|
|
18
|
+
*/
|
|
19
|
+
criticalServerIds?: string[];
|
|
20
|
+
/** Extended ceiling (ms) for criticalServerIds. Default 8000. */
|
|
21
|
+
criticalDeadlineMs?: number;
|
|
11
22
|
}
|
|
12
23
|
/**
|
|
13
24
|
* Total wall-clock budget for building the MCP tool set. Servers connect in
|
|
@@ -60,6 +60,7 @@ import * as phaseTracker from "../ee/phase-tracker.js";
|
|
|
60
60
|
import { buildScope as buildScopeForVeto } from "../ee/scope.js";
|
|
61
61
|
import { fireTrajectoryEvent } from "../ee/session-trajectory.js";
|
|
62
62
|
import { getTenantId as getTenantIdForVeto } from "../ee/tenant.js";
|
|
63
|
+
import { mentionsEcosystemScope } from "../gsd/directives.js";
|
|
63
64
|
import { acquireMcpTools } from "../mcp/client-pool.js";
|
|
64
65
|
import { dropRedundantFsMcpTools, filterMcpServersByMessage } from "../mcp/smart-filter.js";
|
|
65
66
|
import { getModelInfo } from "../models/registry.js";
|
|
@@ -1022,6 +1023,16 @@ export class MessageProcessor {
|
|
|
1022
1023
|
const filteredServers = filterMcpServersByMessage(loadMcpServers(), userMessage, {
|
|
1023
1024
|
disabled: process.env.MUONROI_DISABLE_SMART_MCP === "1",
|
|
1024
1025
|
});
|
|
1026
|
+
// Ecosystem question → muonroi-docs is the authoritative source the
|
|
1027
|
+
// agent is nudged to consult FIRST. Wait for it specifically beyond the
|
|
1028
|
+
// normal deadline so a cold first-connect lands THIS turn instead of
|
|
1029
|
+
// "ready next turn" (session 584ba476c07a: first ecosystem question
|
|
1030
|
+
// missed docs while warming → agent guessed from local files).
|
|
1031
|
+
const criticalServerIds = mentionsEcosystemScope(userMessage)
|
|
1032
|
+
? filteredServers
|
|
1033
|
+
.filter((s) => /(^|[-_])docs([-_]|$)/.test(s.id) && /muonroi/i.test(s.id))
|
|
1034
|
+
.map((s) => s.id)
|
|
1035
|
+
: undefined;
|
|
1025
1036
|
// MCP non-blocking: acquireMcpTools self-bounds — it connects servers
|
|
1026
1037
|
// in parallel and returns PARTIAL results at its internal deadline
|
|
1027
1038
|
// (fast/cached servers included; slow first-connects reported in
|
|
@@ -1038,6 +1049,7 @@ export class MessageProcessor {
|
|
|
1038
1049
|
// command-injection vector the old exec() opener had.
|
|
1039
1050
|
openUrl(url);
|
|
1040
1051
|
},
|
|
1052
|
+
...(criticalServerIds && criticalServerIds.length > 0 ? { criticalServerIds } : {}),
|
|
1041
1053
|
});
|
|
1042
1054
|
}
|
|
1043
1055
|
catch (err) {
|
|
@@ -104,6 +104,16 @@ describe("layer4Gsd (gsd-native)", () => {
|
|
|
104
104
|
const result = await layer4Gsd(makeCtx({ raw, enriched: raw, taskType: "analyze", intentKind: "task", deliverableKind: "answer" }));
|
|
105
105
|
expect(result.enriched).toContain("QUESTION / explanatory");
|
|
106
106
|
});
|
|
107
|
+
it("deliverableKind='report' is informational (no council/discuss scaffold) — session 666630479c1a", async () => {
|
|
108
|
+
// "Đọc và tóm tắt kiến trúc…" classifies as deliverableKind 'report'. A
|
|
109
|
+
// report is human-facing with NO code change, so it must route to the
|
|
110
|
+
// QUESTION directive, not the heavy implement/discuss/council scaffold that
|
|
111
|
+
// over-asked with askcards on a read/summarize task.
|
|
112
|
+
const raw = "đọc và tóm tắt kiến trúc src/orchestrator, src/pil, src/mcp kèm file:line";
|
|
113
|
+
const result = await layer4Gsd(makeCtx({ raw, enriched: raw, taskType: "analyze", intentKind: "task", deliverableKind: "report" }));
|
|
114
|
+
expect(result.enriched).toContain("QUESTION / explanatory");
|
|
115
|
+
expect(result.enriched).not.toContain("MANDATORY");
|
|
116
|
+
});
|
|
107
117
|
it("Phase 2b: deliverableKind='code' is NOT informational even for a question-shaped prompt", async () => {
|
|
108
118
|
// The raw text reads as a question — the legacy regex would mark it
|
|
109
119
|
// informational. The model's deliverableKind='code' must override that so
|
|
@@ -84,15 +84,20 @@ export async function layer4Gsd(ctx) {
|
|
|
84
84
|
// into the human-facing reply as a "2-3 line plan" + process narration
|
|
85
85
|
// (session 829a83888dd2). Route them to the human-facing question directive.
|
|
86
86
|
//
|
|
87
|
-
// Phase 2b: when the model classified the deliverable, CONSUME it
|
|
88
|
-
// "answer"
|
|
89
|
-
//
|
|
90
|
-
//
|
|
87
|
+
// Phase 2b: when the model classified the deliverable, CONSUME it. Both an
|
|
88
|
+
// "answer" AND a "report" deliverable are HUMAN-FACING with no code change, so
|
|
89
|
+
// both are informational — only "code" routes through the implement/verify (and
|
|
90
|
+
// heavy discuss/council) scaffold. Treating "report" as non-informational sent
|
|
91
|
+
// read/summarize/architecture tasks (deliverableKind "report") down the heavy
|
|
92
|
+
// council + AskUserQuestion path, over-asking on a task that just wanted a
|
|
93
|
+
// written summary (session 666630479c1a: "Đọc và tóm tắt kiến trúc…" raised 2
|
|
94
|
+
// askcards + a council loop). Only when the model emitted no deliverable
|
|
95
|
+
// (deliverableKind null → legacy cascade) do we fall back to regex predicates:
|
|
91
96
|
// 1. isMetaAnalysisPrompt — self/CLI evaluation, prior-turn reflection.
|
|
92
97
|
// 2. taskType "general" classified as a real task by L1.
|
|
93
98
|
// 3. question-shaped prompt that is NOT an implementation request.
|
|
94
99
|
const informational = ctx.deliverableKind
|
|
95
|
-
? ctx.deliverableKind
|
|
100
|
+
? ctx.deliverableKind !== "code"
|
|
96
101
|
: isMetaAnalysisPrompt(ctx.raw) ||
|
|
97
102
|
(ctx.taskType === "general" && ctx.intentKind === "task") ||
|
|
98
103
|
(isQuestionLike(ctx.raw) && !isImplementationIntent(ctx.raw));
|
package/dist/src/ui/app.js
CHANGED
|
@@ -2525,7 +2525,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
2525
2525
|
applyLocalAssistantDelta(`\n⚠ [Experience] ${eeChunk.experienceWarning?.message ?? eeChunk.content ?? ""}\nWhy: ${eeChunk.experienceWarning?.why ?? ""}\n`);
|
|
2526
2526
|
}
|
|
2527
2527
|
else if (eeChunk.type === "experience_injected") {
|
|
2528
|
-
applyLocalAssistantDelta(
|
|
2528
|
+
applyLocalAssistantDelta(formatExperienceInjectedBlock(eeChunk.experienceInjected ?? {}));
|
|
2529
2529
|
}
|
|
2530
2530
|
});
|
|
2531
2531
|
for await (const chunk of agent.processMessage(text.trim(), undefined, images)) {
|
|
@@ -3472,10 +3472,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
3472
3472
|
},
|
|
3473
3473
|
];
|
|
3474
3474
|
}
|
|
3475
|
-
return [
|
|
3476
|
-
...prev,
|
|
3477
|
-
buildAssistantEntry(`💡 [Experience Injected] ${chunk.experienceInjected.pointCount} point(s)`),
|
|
3478
|
-
];
|
|
3475
|
+
return [...prev, buildAssistantEntry(formatExperienceInjectedBlock(chunk.experienceInjected))];
|
|
3479
3476
|
});
|
|
3480
3477
|
}
|
|
3481
3478
|
if (chunk.type === "halt" && chunk.haltChunk) {
|
|
@@ -3694,10 +3691,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
3694
3691
|
},
|
|
3695
3692
|
];
|
|
3696
3693
|
}
|
|
3697
|
-
return [
|
|
3698
|
-
...prev,
|
|
3699
|
-
buildAssistantEntry(`💡 [Experience Injected] ${chunk.experienceInjected.pointCount} point(s)`),
|
|
3700
|
-
];
|
|
3694
|
+
return [...prev, buildAssistantEntry(formatExperienceInjectedBlock(chunk.experienceInjected))];
|
|
3701
3695
|
});
|
|
3702
3696
|
}
|
|
3703
3697
|
if (chunk.type === "done")
|