@dotdrelle/wiki-manager 0.15.54 → 0.15.57
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/mcp.endpoints.example.json +1 -1
- package/package.json +2 -2
- package/src/agent/graph.js +21 -7
- package/src/agent/graph.test.js +21 -0
- package/src/cli/wiki-manager.js +24 -1
- package/src/commands/slash.js +46 -7
- package/src/commands/slash.test.js +57 -1
- package/src/core/buildInfo.json +2 -2
- package/src/core/currentArtifact.js +49 -0
- package/src/core/currentArtifact.test.js +62 -0
- package/src/core/env.js +1 -0
- package/src/core/mcp.js +19 -4
- package/src/core/mcp.test.js +64 -2
- package/src/orchestrator/agentRegistry.js +25 -6
- package/src/orchestrator/agentRegistry.test.js +41 -0
- package/src/orchestrator/objectiveResolver.js +72 -16
- package/src/orchestrator/objectiveResolver.test.js +142 -44
- package/src/orchestrator/scheduler.test.js +76 -0
- package/src/runtime/runner.js +89 -4
- package/src/runtime/server.test.js +9 -0
- package/src/shell/repl.js +4 -0
- package/src/shell/repl.test.js +19 -0
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"chatAccess": {
|
|
30
30
|
"maxToolIterations": 8,
|
|
31
31
|
"servers": {
|
|
32
|
-
"llm-wiki": { "allow": ["help_list", "help_read", "help_search", "wiki_workspace_status", "wiki_list_pages", "wiki_read_page", "wiki_read_pages", "wiki_search_context", "wiki_collect_context", "wiki_read_ingested_source", "wiki_outline", "template_read", "template_write", "build_context_write"] },
|
|
32
|
+
"llm-wiki": { "allow": ["help_list", "help_read", "help_search", "wiki_workspace_status", "wiki_list_pages", "wiki_read_page", "wiki_read_pages", "wiki_search_context", "wiki_collect_context", "wiki_read_ingested_source", "wiki_outline", "template_read", "template_write", "build_context_write", "wiki_read_deliverable"] },
|
|
33
33
|
"wiki-production": { "allow": ["production_job_status", "production_jobs_list"] },
|
|
34
34
|
"cme": { "allow": ["cme_status", "cme_sources_list", "cme_export_status"] }
|
|
35
35
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.57",
|
|
4
4
|
"description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"start": "bun ./bin/wiki-manager.js",
|
|
22
|
-
"test": "node --test src/core/skillInvocation.test.js src/core/skillCompiler.test.js src/runtime/skillRun.test.js src/runtime/controlDrain.test.js src/runtime/controlCancellation.test.js src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/agent/skillRecursion.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/workspaceProfile.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/skillChainView.test.js src/core/runtimeLog.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikirc.test.js src/core/workspaceInherit.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/googleGrants.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/taskStatuses.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardModality.test.js src/shell/setupWizardPlaceholders.test.js src/shell/setupWizardSuggestions.test.js src/shell/setupWizardDiscovery.test.js src/shell/wrapText.test.js src/runtime/lifecycle.test.js src/runtime/store.test.js src/runtime/workspaceIsolation.test.js src/runtime/controlMessages.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/delegation.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/skillChain.e2e.test.js src/runtime/donna-contract.test.js src/runtime/approvals.test.js src/runtime/auth.test.js",
|
|
22
|
+
"test": "node --test src/core/skillInvocation.test.js src/core/skillCompiler.test.js src/runtime/skillRun.test.js src/runtime/controlDrain.test.js src/runtime/controlCancellation.test.js src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/agent/skillRecursion.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/workspaceProfile.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/skillChainView.test.js src/core/runtimeLog.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikirc.test.js src/core/workspaceInherit.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/currentArtifact.test.js src/core/googleGrants.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/taskStatuses.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardModality.test.js src/shell/setupWizardPlaceholders.test.js src/shell/setupWizardSuggestions.test.js src/shell/setupWizardDiscovery.test.js src/shell/wrapText.test.js src/runtime/lifecycle.test.js src/runtime/store.test.js src/runtime/workspaceIsolation.test.js src/runtime/controlMessages.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/delegation.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/skillChain.e2e.test.js src/runtime/donna-contract.test.js src/runtime/approvals.test.js src/runtime/auth.test.js",
|
|
23
23
|
"check-versions": "node scripts/check-versions.js",
|
|
24
24
|
"prepack": "node scripts/check-versions.js",
|
|
25
25
|
"prepublishOnly": "node scripts/check-versions.js",
|
package/src/agent/graph.js
CHANGED
|
@@ -25,6 +25,7 @@ import { extractActivity, formatActivitySummary, parseJsonText, sessionActivitie
|
|
|
25
25
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
26
26
|
import { enqueueProductionJob, ensureJobQueue, formatQueue, productionLockBusy } from '../core/jobQueue.js';
|
|
27
27
|
import { loadWorkspaceProfile, updateWorkspaceProfilePreference } from '../core/profile.js';
|
|
28
|
+
import { artifactFromToolCall, currentArtifactFor, currentArtifactPromptLine, rememberArtifact } from '../core/currentArtifact.js';
|
|
28
29
|
import { capabilityRegistryForSession } from '../orchestrator/capabilityRegistry.js';
|
|
29
30
|
import { fetchRuntimeState, postRuntimeCancel, postRuntimeControl, postRuntimeDelegate, postRuntimeKill, postRuntimeSkill } from '../runtime/client.js';
|
|
30
31
|
import { controlLanguage } from '../runtime/controlMessages.js';
|
|
@@ -1025,11 +1026,18 @@ export function connectorConfigurationTarget(session, objective) {
|
|
|
1025
1026
|
.filter((message) => message?.role === 'user')
|
|
1026
1027
|
.map((message) => String(message?.content ?? ''))
|
|
1027
1028
|
.join(' ');
|
|
1028
|
-
const
|
|
1029
|
-
//
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
|
|
1029
|
+
const objectiveText = String(objective ?? '').trim().toLowerCase();
|
|
1030
|
+
// The configuration keyword must describe the objective being delegated NOW,
|
|
1031
|
+
// not something the user said earlier in the session. Matching it against
|
|
1032
|
+
// recent conversation context made a stale "configurer le CME" message poison
|
|
1033
|
+
// every later delegation — an unrelated `/wiki-ingest` objective was rejected
|
|
1034
|
+
// as connector setup. Recent context is used only to resolve WHICH connector
|
|
1035
|
+
// the current objective refers to. (`connect` used to match the noun
|
|
1036
|
+
// "connector" as a substring, and production skills mention an optional
|
|
1037
|
+
// messaging connector; the narrowed word-boundary form keeps that from
|
|
1038
|
+
// misclassifying a business run as connector setup.)
|
|
1039
|
+
if (!/(?:configur|\bconnect(?:ed|ing|ion|ions)?\b|authent|oauth|setup|sign[ -]?in|\bpat\b|api[ _-]?token|credential|identifiant|mot de passe|password)/i.test(objectiveText)) return null;
|
|
1040
|
+
const contextText = `${recentContext} ${objectiveText}`.trim().toLowerCase();
|
|
1033
1041
|
for (const [serverName, server] of Object.entries(session?.mcp ?? {})) {
|
|
1034
1042
|
if (server?.status !== 'connected' || !Array.isArray(server.tools) || server.tools.length === 0) continue;
|
|
1035
1043
|
const genericAliasParts = new Set([
|
|
@@ -1042,7 +1050,7 @@ export function connectorConfigurationTarget(session, objective) {
|
|
|
1042
1050
|
]
|
|
1043
1051
|
.flatMap((value) => String(value).toLowerCase().split(/[^a-z0-9]+/))
|
|
1044
1052
|
.filter((part) => part.length >= 3 && !genericAliasParts.has(part));
|
|
1045
|
-
if (!aliases.some((alias) =>
|
|
1053
|
+
if (!aliases.some((alias) => contextText.includes(alias))) continue;
|
|
1046
1054
|
const setupTool = server.tools.find((tool) => {
|
|
1047
1055
|
const name = String(tool?.name ?? '').toLowerCase();
|
|
1048
1056
|
const description = String(tool?.description ?? '').toLowerCase();
|
|
@@ -1181,7 +1189,7 @@ export function buildAgentSystemPrompt(state) {
|
|
|
1181
1189
|
skills,
|
|
1182
1190
|
'</skill_catalog>',
|
|
1183
1191
|
runningSkillStack.length > 0
|
|
1184
|
-
? `You are already executing the compiled objective of workspace skill ${JSON.stringify(runningSkillStack.at(-1))}.
|
|
1192
|
+
? `You are already executing the compiled objective of workspace skill ${JSON.stringify(runningSkillStack.at(-1))}. The current user message IS that objective: execute it directly${runningSkillExecution === 'direct' ? ' and stop after its requested direct mutation; delegation and nested skills are forbidden for this workflow' : ' by delegating it with runtime__delegate (or a matching direct tool)'}. Do not select or call that skill again, with or without a leading slash — the runtime refuses the re-invocation with skill_recursion_blocked, and that refusal means act on the objective yourself, not report an error. A skill run is not successful until its requested mutation has an affirmative tool result; never infer success from the runtime merely becoming idle or done, and never end the run with an empty reply or a bare "{}".`
|
|
1185
1193
|
: null,
|
|
1186
1194
|
'In interactive agent mode, call only tools actually provided to you. Any directly offered tool stays direct; never substitute an orchestration-contract tool yourself.',
|
|
1187
1195
|
'When the user asks for an action that can be performed with connected MCP tools or safe primitives, do not answer with future intent such as "I will call...", "I am going to run...", or "launching..." unless you also call the tool in the same turn. Either call the tool now, ask for the exact missing required arguments, or explain the concrete blocker.',
|
|
@@ -1218,6 +1226,7 @@ export function buildAgentSystemPrompt(state) {
|
|
|
1218
1226
|
'When an action fails or is refused for lack of an authorization grant or scope (rather than a missing capability), say exactly that and name the primitive that grants it. Do not describe the feature as unavailable.',
|
|
1219
1227
|
'For an action with no matching direct tool, call runtime__delegate with the user objective only. The runtime chooses the capability, operation, agent and plan, including a validated single task for executor-only agents. Never choose those identifiers yourself. Never call <provider>__agent_plan, <provider>__agent_execute, legacy production__production_start_job, wiki__plan_set, or wiki__plan_done from interactive chat.',
|
|
1220
1228
|
'Do not ask the user which sources, files, connectors, or templates to use for an ingest, build, or export: the specialized agent discovers them from the workspace. When the objective is clear (e.g. "lance une ingestion"), delegate it as stated, without a clarifying question.',
|
|
1229
|
+
'Templates are instruction-only specs and deliverables are regenerated from them. When asked to change what a generated document says, edit the underlying wiki content (wiki_write_page) or the template\'s [[INSTRUCTION: ...]] sections — never write finished prose into a template, because a build copies it verbatim and it can no longer be refreshed from the wiki. template_write refuses prose outside an instruction block, so keep every sentence inside one.',
|
|
1221
1230
|
'Promise only what the resolved capability actually exposes in its declared contract (the input schema the specialized agent publishes for that capability). When the user requests an execution parameter — a batch or chunk size, a count "N at a time", concurrency, ordering, priority, or any tuning knob — apply it only if that parameter exists in the target capability\'s published input schema. Otherwise do not confirm or promise it: delegate the objective, and if the user explicitly asked for that parameter, say plainly in one line that you started the work but do not control that aspect (the runtime and the specialized agent decide it). Never state or imply a parameter was applied when the agent contract cannot enforce it.',
|
|
1222
1231
|
'If runtime__delegate returns a blocker or no specialized provider is available, report only that concrete blocker concisely. Never replace the missing execution path with a suggested slash command, skill, MCP tool name, manual file move, administrator escalation, or alternative workflow unless the user explicitly asks for alternatives.',
|
|
1223
1232
|
'For workspace inventory and page listings, use the connected wiki MCP read tools. Never invent or call a /wiki shell command through shell__run_command. Use /workspace init <name> [path] for low-level non-interactive workspace creation; in the interactive TUI, /new <name> opens the setup wizard.',
|
|
@@ -1235,6 +1244,7 @@ export function buildAgentSystemPrompt(state) {
|
|
|
1235
1244
|
workspaceProfile
|
|
1236
1245
|
? `Workspace profile (.wiki/profile.md) — durable user preferences, apply these to every reply (tone, tutoiement/vouvoiement, formatting, etc.):\n${workspaceProfile}`
|
|
1237
1246
|
: null,
|
|
1247
|
+
currentArtifactPromptLine(currentArtifactFor(state.session)),
|
|
1238
1248
|
'Runtime control: you have runtime__status, runtime__cancel, runtime__kill and runtime__enqueue. When the user asks to stop, remove, clean or kill the current run, its jobs or the queue ("supprime le job et la queue", "arr\u00eate tout"), call runtime__kill (or runtime__cancel for a soft stop of just the run) and confirm what was stopped. When the user explicitly asks to delete, reset, abandon or replace the current plan, call runtime__kill with purge=true; never set purge=true for a simple stop. For questions about what is running or queued, call runtime__status and answer from its data. You have no approval tool: a pending approval is granted only by the user through the approval button or the /approve command. Never grant, claim or report an approval yourself; when the user asks to proceed with pending mutations, tell them to use those controls. When the user asks for a NEW action while a run is active, do not execute it: propose runtime__enqueue (run it after) or, if they insist it replaces the current work, runtime__kill then the new action.',
|
|
1239
1249
|
'When the user asks to refresh, show, or update the displayed plan or status, call runtime__status. This is a state refresh request, not a new business capability, and must never be delegated.',
|
|
1240
1250
|
'Report every runtime control outcome exactly as the tool returned it \u2014 never embellish. If runtime__kill reports 0 run(s)/0 task(s)/0 purged, say there was nothing active to stop or purge; do NOT claim a run, plan, pending approval or queue item was removed. If runtime__status returns an error or could not be read, say the runtime state could not be retrieved and do not describe a state you never obtained. Never assert that something was cleaned, cancelled, approved or purged unless that specific tool result confirms it.',
|
|
@@ -1940,6 +1950,10 @@ export function createAgentGraph(options = {}) {
|
|
|
1940
1950
|
args = withActiveWorkspaceForExternalTool(state.session, server, tool, args);
|
|
1941
1951
|
const result = await callMcpTool(state.session.mcp, server, tool, args, state.session._abortSignal);
|
|
1942
1952
|
resultText = formatMcpToolResult(result);
|
|
1953
|
+
const artifact = artifactFromToolCall(tool, args);
|
|
1954
|
+
if (artifact) {
|
|
1955
|
+
rememberArtifact(state.session, artifact);
|
|
1956
|
+
}
|
|
1943
1957
|
}
|
|
1944
1958
|
}
|
|
1945
1959
|
{
|
package/src/agent/graph.test.js
CHANGED
|
@@ -62,6 +62,27 @@ test('a connection problem still routes to connector configuration', () => {
|
|
|
62
62
|
assert.deepEqual(target, { serverName: 'acme', setupTool: 'acme_auth' });
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
test('a stale configure-connector message does not poison a later business delegation', () => {
|
|
66
|
+
const target = connectorConfigurationTarget({
|
|
67
|
+
agentProjection: {
|
|
68
|
+
conversation: [
|
|
69
|
+
{ role: 'user', content: 'donne moi la config du cme' },
|
|
70
|
+
{ role: 'user', content: 'liste les pages wiki configurer pour l’agent cme' },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
mcp: {
|
|
74
|
+
cme: {
|
|
75
|
+
status: 'connected',
|
|
76
|
+
tools: [
|
|
77
|
+
{ name: 'cme_setup', description: 'Configure Confluence credentials.' },
|
|
78
|
+
{ name: 'cme_export_run', description: 'Run export.' },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}, 'Ingest pending staged Markdown files from raw/untracked into the wiki.');
|
|
83
|
+
assert.equal(target, null);
|
|
84
|
+
});
|
|
85
|
+
|
|
65
86
|
test('Donna cannot answer an explicit action with manual instructions instead of delegating', async () => {
|
|
66
87
|
const originalFetch = globalThis.fetch;
|
|
67
88
|
let delegated = false;
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -32,6 +32,7 @@ import { resolveCapabilityConcurrency } from '../orchestrator/scheduler.js';
|
|
|
32
32
|
import { capabilityRegistryForSession } from '../orchestrator/capabilityRegistry.js';
|
|
33
33
|
import { listWorkspaces } from '../core/workspaces.js';
|
|
34
34
|
import { findSkill } from '../core/skills.js';
|
|
35
|
+
import { rememberArtifact } from '../core/currentArtifact.js';
|
|
35
36
|
// Runtime modules use node:sqlite (Node.js built-in unavailable in Bun).
|
|
36
37
|
// They are imported dynamically so the shell / TUI path never loads them.
|
|
37
38
|
|
|
@@ -245,6 +246,21 @@ export function missingRequiredArguments(schema, args) {
|
|
|
245
246
|
});
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
// After a delegated document.build, remember the targeted template as the
|
|
250
|
+
// current artifact so the next "améliore cette slide" / "relance le build"
|
|
251
|
+
// resolves the file without re-discovery. Best effort: a build of every
|
|
252
|
+
// template (no `templates` argument) leaves the artifact untouched.
|
|
253
|
+
function rememberBuildTemplateArtifact(session, selection, args) {
|
|
254
|
+
if (selection?.capability !== 'document.build') return;
|
|
255
|
+
const templates = Array.isArray(args?.templates) ? args.templates : [];
|
|
256
|
+
if (templates.length !== 1) return;
|
|
257
|
+
const name = String(templates[0] ?? '').trim().replace(/\\/g, '/');
|
|
258
|
+
if (!name) return;
|
|
259
|
+
const withExt = name.toLowerCase().endsWith('.md') ? name : `${name}.md`;
|
|
260
|
+
const pathValue = withExt.startsWith('templates/') ? withExt : `templates/${withExt.replace(/^\.?\//, '')}`;
|
|
261
|
+
rememberArtifact(session, { path: pathValue, kind: 'template' });
|
|
262
|
+
}
|
|
263
|
+
|
|
248
264
|
function safeParseArgumentObject(text) {
|
|
249
265
|
const cleaned = String(text ?? '').trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
|
|
250
266
|
if (!cleaned) return null;
|
|
@@ -297,7 +313,7 @@ export function createInteractiveSession(context, { runtimeUrl, turnId, signal =
|
|
|
297
313
|
'workspace', 'workspacePath', 'workspaceEnvFile', 'workspaceEnv',
|
|
298
314
|
'wikirc', 'wikircConfig', 'language', 'llm', 'mcp', 'commands',
|
|
299
315
|
'packageJson', 'queueStore', 'systemPrompt',
|
|
300
|
-
'_runSkillWithinRun',
|
|
316
|
+
'_runSkillWithinRun', 'currentArtifact',
|
|
301
317
|
]) {
|
|
302
318
|
if (source[key] !== undefined) session[key] = source[key];
|
|
303
319
|
}
|
|
@@ -1249,6 +1265,7 @@ async function runRuntime(argv, agent) {
|
|
|
1249
1265
|
workspace: session.workspace ?? context.workspace ?? '',
|
|
1250
1266
|
signal: session._abortSignal,
|
|
1251
1267
|
});
|
|
1268
|
+
rememberBuildTemplateArtifact(session, selection, extractedArguments);
|
|
1252
1269
|
planResult = await callMcpTool(
|
|
1253
1270
|
session.mcp,
|
|
1254
1271
|
provider.serverName,
|
|
@@ -1649,6 +1666,12 @@ async function runRuntime(argv, agent) {
|
|
|
1649
1666
|
} else {
|
|
1650
1667
|
response = await runAgentTurn(agent, ephemeral, input, { messages, signal });
|
|
1651
1668
|
}
|
|
1669
|
+
// Persist the artifact the turn may have opened/edited (template_write,
|
|
1670
|
+
// template_read, …) back onto the long-lived session, so the next /turn —
|
|
1671
|
+
// chat or agent — sees it. The ephemeral session is otherwise discarded.
|
|
1672
|
+
if (ephemeral.currentArtifact) {
|
|
1673
|
+
context.session.currentArtifact = ephemeral.currentArtifact;
|
|
1674
|
+
}
|
|
1652
1675
|
ensureInteractiveAssistantMessage(ephemeral, response, {
|
|
1653
1676
|
turnId,
|
|
1654
1677
|
workspace: context.workspace ?? null,
|
package/src/commands/slash.js
CHANGED
|
@@ -29,6 +29,7 @@ import { createWorkspace, findWorkspace, listWorkspaces } from '../core/workspac
|
|
|
29
29
|
import { findSkill, inspectSkills, listSkills } from '../core/skills.js';
|
|
30
30
|
import { extractActivity, formatActivityError, formatActivityLine, formatActivitySummary, parseJsonText } from '../core/activity.js';
|
|
31
31
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
32
|
+
import { emitRuntimeLog } from '../runtime/supervisor.js';
|
|
32
33
|
import {
|
|
33
34
|
cancelQueueItem,
|
|
34
35
|
clearFinishedQueueItems,
|
|
@@ -612,20 +613,53 @@ function publishDocumentActivity(session, activity) {
|
|
|
612
613
|
return publishPayloadActivity(session, { _activity: activity }, { server: 'documents', tool: 'documents_convert_to_markdown' });
|
|
613
614
|
}
|
|
614
615
|
|
|
615
|
-
export async function refreshMcpRuntimeStatus(session) {
|
|
616
|
-
|
|
617
|
-
|
|
616
|
+
export async function refreshMcpRuntimeStatus(session, deps = {}) {
|
|
617
|
+
const getStates = deps.serviceStates ?? serviceStates;
|
|
618
|
+
const discover = deps.discoverMcpTools ?? discoverMcpTools;
|
|
619
|
+
const previousMcp = session.mcp;
|
|
620
|
+
const base = buildMcpStatus(session);
|
|
621
|
+
if (!session.workspacePath) {
|
|
622
|
+
session.mcp = base;
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
// Build the next status in local variables and assign `session.mcp` only at
|
|
626
|
+
// the end. Assigning `buildMcpStatus` first (status "configured", no tools)
|
|
627
|
+
// and then awaiting `serviceStates`/`discoverMcpTools` exposed a window in
|
|
628
|
+
// which a background re-scan had downgraded `production` to "configured",
|
|
629
|
+
// and the dispatcher — reading `session.mcp` mid-refresh — refused the next
|
|
630
|
+
// `agent_execute` with "MCP is not connected: production" while the agent
|
|
631
|
+
// was actually up.
|
|
618
632
|
try {
|
|
619
|
-
const states = await
|
|
620
|
-
session.mcp = applyMcpRuntimeStatus(
|
|
621
|
-
session
|
|
633
|
+
const states = await getStates(session);
|
|
634
|
+
session.mcp = await discover(applyMcpRuntimeStatus(base, states), previousMcp);
|
|
635
|
+
reportNewlyDegradedMcp(session, previousMcp);
|
|
622
636
|
return states;
|
|
623
637
|
} catch {
|
|
624
|
-
session.mcp = await
|
|
638
|
+
session.mcp = await discover(base, previousMcp);
|
|
639
|
+
reportNewlyDegradedMcp(session, previousMcp);
|
|
625
640
|
return null;
|
|
626
641
|
}
|
|
627
642
|
}
|
|
628
643
|
|
|
644
|
+
/**
|
|
645
|
+
* "Degraded" (`discoverMcpTools`, mcp.js) means an endpoint is reporting
|
|
646
|
+
* "connected" only because a prior cycle was, not because Docker or the
|
|
647
|
+
* latest probe confirm it now — the preservation that endpoint's
|
|
648
|
+
* `keepConnected` branch exists for, deliberately not reverted here since a
|
|
649
|
+
* transient probe blip must not flip a live run's endpoint away. But
|
|
650
|
+
* preserving it in silence is exactly the "stale status kept forever"
|
|
651
|
+
* pattern already fixed once in agentRegistry.js: report it, edge-triggered
|
|
652
|
+
* on the transition into the degraded state, so it does not repeat every
|
|
653
|
+
* re-scan while it persists.
|
|
654
|
+
*/
|
|
655
|
+
function reportNewlyDegradedMcp(session, previousMcp) {
|
|
656
|
+
for (const [name, entry] of Object.entries(session.mcp ?? {})) {
|
|
657
|
+
if (!entry?.degraded || previousMcp?.[name]?.degraded) continue;
|
|
658
|
+
emitRuntimeLog(session, `mcp: ${name} still reports "connected" only from a prior probe`
|
|
659
|
+
+ ` (${entry.toolError ?? 'no detail'}); Docker/the latest probe no longer confirm it.`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
629
663
|
async function statusText(session) {
|
|
630
664
|
const states = await refreshMcpRuntimeStatus(session);
|
|
631
665
|
const workspaceStats = collectWorkspaceStats(session);
|
|
@@ -1557,7 +1591,12 @@ export async function handleSlashCommand(line, context) {
|
|
|
1557
1591
|
const wikiArgs = args.slice(2);
|
|
1558
1592
|
if (wikiArgs.length === 0) return { output: 'Usage: /wiki run <args...>' };
|
|
1559
1593
|
step(`Wiki: running ${wikiArgs.join(' ')}…`);
|
|
1594
|
+
// Steps that drive LLM work (taxonomy, build, ingest, export, polish)
|
|
1595
|
+
// legitimately run for minutes: the production agent gives them a
|
|
1596
|
+
// 600s LLM timeout, so the raw hatch must not cut them at 180s.
|
|
1597
|
+
const longRunning = new Set(['taxonomy', 'build', 'ingest', 'export', 'polish', 'pipeline']);
|
|
1560
1598
|
const output = await runWikiCli(context.session, wikiArgs, {
|
|
1599
|
+
timeout: longRunning.has(wikiArgs[0] ?? '') ? 600_000 : 180_000,
|
|
1561
1600
|
onOutput: (line) => step(`Wiki: ${line}`),
|
|
1562
1601
|
});
|
|
1563
1602
|
const activity = formatActivitySummary('wiki', wikiArgs[0] ?? 'run', output);
|
|
@@ -4,7 +4,7 @@ import { mkdtemp } from 'node:fs/promises';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import test from 'node:test';
|
|
7
|
-
import { agentConcurrencySections, compactBaseUrl, compactMcpStatus, handleSlashCommand, localizedOperationResult } from './slash.js';
|
|
7
|
+
import { agentConcurrencySections, compactBaseUrl, compactMcpStatus, handleSlashCommand, localizedOperationResult, refreshMcpRuntimeStatus } from './slash.js';
|
|
8
8
|
import { completionContext } from '../shell/repl.js';
|
|
9
9
|
|
|
10
10
|
test('deterministic operation results ask Donna to localize compact facts without leaking commands', () => {
|
|
@@ -437,3 +437,59 @@ test('/queue cancel reports unknown ids that are not runtime-managed', async ()
|
|
|
437
437
|
});
|
|
438
438
|
assert.match(result.output ?? '', /Unknown queue item/i);
|
|
439
439
|
});
|
|
440
|
+
|
|
441
|
+
test('refreshMcpRuntimeStatus does not expose an intermediate configured status', async () => {
|
|
442
|
+
const session = {
|
|
443
|
+
workspacePath: '/tmp/ws',
|
|
444
|
+
workspaceEnv: { PRODUCTION_MCP_PORT: '3202', PRODUCTION_MCP_AUTH_TOKEN: 'token' },
|
|
445
|
+
wikircConfig: {},
|
|
446
|
+
mcp: { production: { status: 'connected', tools: [{ name: 'production__agent_execute' }] } },
|
|
447
|
+
};
|
|
448
|
+
const originalMcp = session.mcp;
|
|
449
|
+
let resolveStates;
|
|
450
|
+
const statesPromise = new Promise((resolve) => { resolveStates = resolve; });
|
|
451
|
+
const pending = refreshMcpRuntimeStatus(session, {
|
|
452
|
+
serviceStates: () => statesPromise,
|
|
453
|
+
discoverMcpTools: async (mcp) => mcp,
|
|
454
|
+
});
|
|
455
|
+
// While serviceStates is still in flight, the dispatcher-facing status must
|
|
456
|
+
// keep the previous connected snapshot — never the fresh "configured" base.
|
|
457
|
+
assert.equal(session.mcp, originalMcp, 'session.mcp reassigned mid-refresh');
|
|
458
|
+
resolveStates({ 'production-mcp': { running: true } });
|
|
459
|
+
await pending;
|
|
460
|
+
assert.equal(session.mcp.production.status, 'connected');
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test('refreshMcpRuntimeStatus reports a degraded MCP endpoint once, not on every re-scan', async () => {
|
|
464
|
+
const session = {
|
|
465
|
+
workspacePath: '/tmp/ws',
|
|
466
|
+
workspaceEnv: { PRODUCTION_MCP_PORT: '3202', PRODUCTION_MCP_AUTH_TOKEN: 'token' },
|
|
467
|
+
wikircConfig: {},
|
|
468
|
+
mcp: { production: { status: 'connected', tools: [] } },
|
|
469
|
+
};
|
|
470
|
+
const degradedRuns = (n) => async () => ({
|
|
471
|
+
production: { status: 'connected', tools: [], toolError: `probe ${n} failed`, degraded: true },
|
|
472
|
+
});
|
|
473
|
+
const deps = { serviceStates: async () => ({}) };
|
|
474
|
+
|
|
475
|
+
await refreshMcpRuntimeStatus(session, { ...deps, discoverMcpTools: degradedRuns(1) });
|
|
476
|
+
await refreshMcpRuntimeStatus(session, { ...deps, discoverMcpTools: degradedRuns(2) });
|
|
477
|
+
await refreshMcpRuntimeStatus(session, { ...deps, discoverMcpTools: degradedRuns(3) });
|
|
478
|
+
|
|
479
|
+
const reports = (session.agentEvents ?? []).filter((event) => event.type === 'runtime_log'
|
|
480
|
+
&& String(event.payload?.message ?? '').includes('mcp: production'));
|
|
481
|
+
assert.equal(reports.length, 1, 'a degraded endpoint is reported once, not once per re-scan');
|
|
482
|
+
|
|
483
|
+
// Recovers, then degrades again: reported a second time.
|
|
484
|
+
await refreshMcpRuntimeStatus(session, {
|
|
485
|
+
...deps,
|
|
486
|
+
discoverMcpTools: async () => ({ production: { status: 'connected', tools: [], degraded: false } }),
|
|
487
|
+
});
|
|
488
|
+
await refreshMcpRuntimeStatus(session, { ...deps, discoverMcpTools: degradedRuns(4) });
|
|
489
|
+
assert.equal(
|
|
490
|
+
(session.agentEvents ?? []).filter((event) => event.type === 'runtime_log'
|
|
491
|
+
&& String(event.payload?.message ?? '').includes('mcp: production')).length,
|
|
492
|
+
2,
|
|
493
|
+
);
|
|
494
|
+
});
|
|
495
|
+
|
package/src/core/buildInfo.json
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Le « contexte d'artefact courant » — la boucle documentaire continue.
|
|
3
|
+
*
|
|
4
|
+
* Quand Donna édite ou ouvre un artefact éditable (template, build-context,
|
|
5
|
+
* page wiki), on mémorise ce chemin au niveau de la session, comme la
|
|
6
|
+
* conversation, pour qu'un tour suivant puisse dire « améliore cette slide »
|
|
7
|
+
* sans que le modèle doive deviner quel fichier est concerné.
|
|
8
|
+
*
|
|
9
|
+
* Seuls les artefacts *éditables* sont suivis : templates/, build-context/ et
|
|
10
|
+
* wiki/. Les livrables générés (deliverables/) ne le sont pas — on les régénère,
|
|
11
|
+
* on ne les édite pas.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const ARTIFACT_KIND_BY_TOOL = {
|
|
15
|
+
template_write: 'template',
|
|
16
|
+
template_read: 'template',
|
|
17
|
+
build_context_write: 'build-context',
|
|
18
|
+
wiki_write_page: 'wiki page',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function artifactFromToolCall(tool, args) {
|
|
22
|
+
const kind = ARTIFACT_KIND_BY_TOOL[String(tool ?? '')];
|
|
23
|
+
if (!kind) return null;
|
|
24
|
+
const pathValue = args && typeof args === 'object' ? args.path : null;
|
|
25
|
+
if (typeof pathValue !== 'string' || !pathValue.trim()) return null;
|
|
26
|
+
return { path: pathValue.trim(), kind };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function currentArtifactFor(session) {
|
|
30
|
+
const artifact = session?.currentArtifact;
|
|
31
|
+
if (!artifact || !artifact.path) return null;
|
|
32
|
+
if (artifact.workspace && session?.workspace && artifact.workspace !== session.workspace) return null;
|
|
33
|
+
return artifact;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function rememberArtifact(session, { path, kind }) {
|
|
37
|
+
if (!session || typeof path !== 'string' || !path.trim()) return;
|
|
38
|
+
session.currentArtifact = {
|
|
39
|
+
workspace: session.workspace ?? null,
|
|
40
|
+
path: path.trim(),
|
|
41
|
+
kind,
|
|
42
|
+
at: Date.now(),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function currentArtifactPromptLine(artifact) {
|
|
47
|
+
if (!artifact) return null;
|
|
48
|
+
return `Current artifact: the user is working on ${artifact.path} (${artifact.kind}). When the user refers to "this document", "this slide", "the template", "the page", or the document without naming the file, treat ${artifact.path} as the artifact being edited: read it first, then apply the requested edit and save it with the matching write tool.`;
|
|
49
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
artifactFromToolCall,
|
|
5
|
+
currentArtifactFor,
|
|
6
|
+
currentArtifactPromptLine,
|
|
7
|
+
rememberArtifact,
|
|
8
|
+
} from './currentArtifact.js';
|
|
9
|
+
|
|
10
|
+
test('artifactFromToolCall records editable write tools with a path', () => {
|
|
11
|
+
assert.deepEqual(artifactFromToolCall('template_write', { path: 'templates/notes/basic.md', confirm: true }), {
|
|
12
|
+
path: 'templates/notes/basic.md',
|
|
13
|
+
kind: 'template',
|
|
14
|
+
});
|
|
15
|
+
assert.deepEqual(artifactFromToolCall('build_context_write', { path: 'build-context/rules/citations.md' }), {
|
|
16
|
+
path: 'build-context/rules/citations.md',
|
|
17
|
+
kind: 'build-context',
|
|
18
|
+
});
|
|
19
|
+
assert.deepEqual(artifactFromToolCall('wiki_write_page', { path: 'wiki/flux/ingestion.md' }), {
|
|
20
|
+
path: 'wiki/flux/ingestion.md',
|
|
21
|
+
kind: 'wiki page',
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('artifactFromToolCall records template_read only when a path is given', () => {
|
|
26
|
+
assert.deepEqual(artifactFromToolCall('template_read', { path: 'templates/notes/basic.md' }), {
|
|
27
|
+
path: 'templates/notes/basic.md',
|
|
28
|
+
kind: 'template',
|
|
29
|
+
});
|
|
30
|
+
assert.equal(artifactFromToolCall('template_read', {}), null);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('artifactFromToolCall ignores read tools and tools without a path', () => {
|
|
34
|
+
assert.equal(artifactFromToolCall('wiki_read_page', { path: 'wiki/page.md' }), null);
|
|
35
|
+
assert.equal(artifactFromToolCall('wiki_search_context', { query: 'x' }), null);
|
|
36
|
+
assert.equal(artifactFromToolCall('template_write', {}), null);
|
|
37
|
+
assert.equal(artifactFromToolCall('unknown_tool', { path: 'templates/x.md' }), null);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('currentArtifactFor is workspace-scoped', () => {
|
|
41
|
+
const artifact = { workspace: 'acpi', path: 'templates/notes/basic.md', kind: 'template' };
|
|
42
|
+
assert.equal(currentArtifactFor({ workspace: 'acpi', currentArtifact: artifact }), artifact);
|
|
43
|
+
assert.equal(currentArtifactFor({ workspace: 'other', currentArtifact: artifact }), null);
|
|
44
|
+
assert.equal(currentArtifactFor({ workspace: 'acpi' }), null);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('currentArtifactPromptLine names the artifact for follow-up edits', () => {
|
|
48
|
+
const line = currentArtifactPromptLine({ path: 'templates/presentation/presentation.md', kind: 'template' });
|
|
49
|
+
assert.match(line, /templates\/presentation\/presentation\.md/);
|
|
50
|
+
assert.match(line, /this slide/);
|
|
51
|
+
assert.equal(currentArtifactPromptLine(null), null);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('rememberArtifact records a workspace-scoped artifact and ignores empty paths', () => {
|
|
55
|
+
const session = { workspace: 'acpi' };
|
|
56
|
+
rememberArtifact(session, { path: 'templates/notes/basic.md', kind: 'template' });
|
|
57
|
+
assert.equal(session.currentArtifact.path, 'templates/notes/basic.md');
|
|
58
|
+
assert.equal(session.currentArtifact.workspace, 'acpi');
|
|
59
|
+
assert.equal(session.currentArtifact.kind, 'template');
|
|
60
|
+
rememberArtifact(session, { path: ' ', kind: 'template' });
|
|
61
|
+
assert.equal(session.currentArtifact.path, 'templates/notes/basic.md');
|
|
62
|
+
});
|
package/src/core/env.js
CHANGED
package/src/core/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { managerEnvFile, managerMcpEndpointsFile, readEnvFile } from './env.js';
|
|
3
3
|
|
|
4
|
-
const WIKI_MANAGER_VERSION = '0.15.
|
|
4
|
+
const WIKI_MANAGER_VERSION = '0.15.57';
|
|
5
5
|
|
|
6
6
|
function envValue(key) {
|
|
7
7
|
const filePath = managerEnvFile();
|
|
@@ -585,7 +585,7 @@ function retryDelay(ms, signal) {
|
|
|
585
585
|
});
|
|
586
586
|
}
|
|
587
587
|
|
|
588
|
-
export async function discoverMcpTools(mcpStatus) {
|
|
588
|
+
export async function discoverMcpTools(mcpStatus, previous = null) {
|
|
589
589
|
const next = {};
|
|
590
590
|
await Promise.all(Object.entries(mcpStatus ?? {}).map(async ([name, value]) => {
|
|
591
591
|
if (value.status === 'missing') {
|
|
@@ -607,11 +607,26 @@ export async function discoverMcpTools(mcpStatus) {
|
|
|
607
607
|
};
|
|
608
608
|
} catch (err) {
|
|
609
609
|
const message = err instanceof Error ? err.message : String(err);
|
|
610
|
+
// A transient probe failure during a background re-scan must not degrade
|
|
611
|
+
// a live endpoint. Docker service state (applyMcpRuntimeStatus) is the
|
|
612
|
+
// authority on whether the container is up; this probe is only a tool
|
|
613
|
+
// refresh, so keep last-known-good status and tools instead of flipping
|
|
614
|
+
// an in-flight run's endpoint to "not connected".
|
|
615
|
+
const prior = previous?.[name];
|
|
616
|
+
const freshlyConnected = value.status === 'connected';
|
|
617
|
+
const keepConnected = freshlyConnected || prior?.status === 'connected';
|
|
618
|
+
// `degraded` marks the case Docker itself no longer confirms as
|
|
619
|
+
// connected (freshlyConnected is false) and only prior history keeps
|
|
620
|
+
// this endpoint reporting "connected" — the preservation this whole
|
|
621
|
+
// branch exists for, worth surfacing rather than masking indefinitely.
|
|
622
|
+
// `refreshMcpRuntimeStatus` (slash.js) owns the edge-triggered log, by
|
|
623
|
+
// comparing this flag against the previous cycle's.
|
|
610
624
|
next[name] = {
|
|
611
625
|
...value,
|
|
612
|
-
status:
|
|
613
|
-
tools: [],
|
|
626
|
+
status: keepConnected ? 'connected' : value.status,
|
|
627
|
+
tools: keepConnected ? (prior?.tools ?? []) : [],
|
|
614
628
|
toolError: message,
|
|
629
|
+
degraded: keepConnected && !freshlyConnected,
|
|
615
630
|
};
|
|
616
631
|
}
|
|
617
632
|
}));
|
package/src/core/mcp.test.js
CHANGED
|
@@ -501,7 +501,7 @@ test('resolveRetryPolicy supports endpoint and tool overrides', () => {
|
|
|
501
501
|
assert.deepEqual(policy, { maxAttempts: 4, backoffMs: 100 });
|
|
502
502
|
});
|
|
503
503
|
|
|
504
|
-
test('discoverMcpTools
|
|
504
|
+
test('discoverMcpTools keeps a connected endpoint connected when tool discovery fails', async () => {
|
|
505
505
|
const originalFetch = globalThis.fetch;
|
|
506
506
|
globalThis.fetch = async () => ({
|
|
507
507
|
ok: false,
|
|
@@ -519,7 +519,7 @@ test('discoverMcpTools downgrades connected endpoint when tool discovery fails',
|
|
|
519
519
|
},
|
|
520
520
|
});
|
|
521
521
|
|
|
522
|
-
assert.equal(status.wiki.status, '
|
|
522
|
+
assert.equal(status.wiki.status, 'connected');
|
|
523
523
|
assert.equal(status.wiki.tools.length, 0);
|
|
524
524
|
assert.match(status.wiki.toolError, /401/);
|
|
525
525
|
} finally {
|
|
@@ -527,6 +527,68 @@ test('discoverMcpTools downgrades connected endpoint when tool discovery fails',
|
|
|
527
527
|
}
|
|
528
528
|
});
|
|
529
529
|
|
|
530
|
+
test('discoverMcpTools preserves last-known-good status and tools across a failed re-probe', async () => {
|
|
531
|
+
const originalFetch = globalThis.fetch;
|
|
532
|
+
globalThis.fetch = async () => ({
|
|
533
|
+
ok: false,
|
|
534
|
+
status: 502,
|
|
535
|
+
headers: { get: () => null },
|
|
536
|
+
text: async () => 'Bad gateway',
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
const status = await discoverMcpTools(
|
|
541
|
+
{
|
|
542
|
+
production: {
|
|
543
|
+
status: 'configured',
|
|
544
|
+
url: 'http://127.0.0.1:3202/mcp/',
|
|
545
|
+
token: 'token',
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
production: {
|
|
550
|
+
status: 'connected',
|
|
551
|
+
tools: [{ name: 'production__agent_execute' }, { name: 'production__agent_status' }],
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
assert.equal(status.production.status, 'connected');
|
|
557
|
+
assert.deepEqual(status.production.tools.map((tool) => tool.name), [
|
|
558
|
+
'production__agent_execute',
|
|
559
|
+
'production__agent_status',
|
|
560
|
+
]);
|
|
561
|
+
assert.match(status.production.toolError, /502/);
|
|
562
|
+
// Docker (`value.status`) no longer confirms this endpoint; only prior
|
|
563
|
+
// history keeps it "connected". `degraded` is what lets the caller
|
|
564
|
+
// (refreshMcpRuntimeStatus) tell this apart from a genuinely fresh probe
|
|
565
|
+
// success, so it can report the preservation instead of masking it.
|
|
566
|
+
assert.equal(status.production.degraded, true);
|
|
567
|
+
} finally {
|
|
568
|
+
globalThis.fetch = originalFetch;
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('discoverMcpTools does not mark a fresh probe failure on an already-connected endpoint as degraded', async () => {
|
|
573
|
+
const originalFetch = globalThis.fetch;
|
|
574
|
+
globalThis.fetch = async () => ({
|
|
575
|
+
ok: false,
|
|
576
|
+
status: 401,
|
|
577
|
+
headers: { get: () => null },
|
|
578
|
+
text: async () => '{"error":"invalid or missing bearer token"}',
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
try {
|
|
582
|
+
const status = await discoverMcpTools({
|
|
583
|
+
wiki: { status: 'connected', url: 'http://127.0.0.1:3201/mcp', token: 'token' },
|
|
584
|
+
});
|
|
585
|
+
assert.equal(status.wiki.status, 'connected');
|
|
586
|
+
assert.equal(status.wiki.degraded, false);
|
|
587
|
+
} finally {
|
|
588
|
+
globalThis.fetch = originalFetch;
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
530
592
|
// A stateful server may reject any cold request, and every SDK words that
|
|
531
593
|
// rejection differently ("No valid session" for the Node SDK, "Missing session
|
|
532
594
|
// ID" for the Python one CME runs). The client must never read that prose: it
|