@dotdrelle/wiki-manager 0.15.76 → 0.15.78

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdrelle/wiki-manager",
3
- "version": "0.15.76",
3
+ "version": "0.15.78",
4
4
  "description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "0.15.76",
3
- "commit": "2504dc9"
2
+ "version": "0.15.78",
3
+ "commit": "6c0c150"
4
4
  }
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.76';
4
+ const WIKI_MANAGER_VERSION = '0.15.78';
5
5
 
6
6
  function envValue(key) {
7
7
  const filePath = managerEnvFile();
@@ -272,7 +272,14 @@ function clarifyToolDescription(_serverName, _toolName, description) {
272
272
 
273
273
  async function listMcpTools(endpoint) {
274
274
  if (!endpoint.url) throw new Error('missing endpoint URL');
275
- const payload = await mcpRequest(endpoint, 'tools/list', {});
275
+ // Opt-in only, unlike callMcpTool: this probe runs inside discoverMcpTools's
276
+ // Promise.all on every re-scan/delegation, so an endpoint with no explicit
277
+ // `retry` in mcp.endpoints.json must keep costing exactly one 8s timeout
278
+ // when unreachable, not silently inherit the global 2-attempt default and
279
+ // double that cost for every unconfigured server. Only an endpoint that
280
+ // explicitly declares `retry` gets more than one attempt here.
281
+ const retry = endpoint.retry ? resolveRetryPolicy(endpoint) : { maxAttempts: 1, backoffMs: 0 };
282
+ const payload = await withRetry(() => mcpRequest(endpoint, 'tools/list', {}), retry);
276
283
  return payload?.result?.tools ?? [];
277
284
  }
278
285
 
@@ -177,6 +177,17 @@ export function isDispatchPlumbingLine(line) {
177
177
  return DISPATCH_PLUMBING_LINE.test(String(line ?? '').replace(/^runtime\s+/, ''));
178
178
  }
179
179
 
180
+ // The agent's own reasoning traces — "Agent: planning next action…",
181
+ // "Agent: classified input as …", "Agent: streaming final answer…" — describe
182
+ // how the agent works, not what the business run does. They belong with the
183
+ // dispatch plumbing in the Agent status tab; the Runtime tab keeps the
184
+ // business flow (plan, task transitions, failures).
185
+ const AGENT_TRACE_LINE = /^(?:runtime\s+)?(?:\d{1,2}:\d{2}(?::\d{2})?\s*(?:·\s*)?)?Agent:\s/;
186
+
187
+ export function isAgentTraceLine(line) {
188
+ return AGENT_TRACE_LINE.test(String(line ?? ''));
189
+ }
190
+
180
191
  export function runtimeLogMatchesFilter(line, filter = '') {
181
192
  const query = String(filter ?? '').trim();
182
193
  if (!query) return true;
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
3
 
4
4
  import { createAgentEvent, dispatchAgentEvent } from './agentEvents.js';
5
- import { compactRuntimeLogForDisplay, formatRuntimeLogPayload, isDispatchPlumbingLine, shortLogId } from './runtimeLog.js';
5
+ import { compactRuntimeLogForDisplay, formatRuntimeLogPayload, isAgentTraceLine, isDispatchPlumbingLine, shortLogId } from './runtimeLog.js';
6
6
  import { emitRuntimeLog } from '../runtime/supervisor.js';
7
7
 
8
8
  const CYCLE_EVENTS = [
@@ -156,6 +156,34 @@ test('isDispatchPlumbingLine recognises the shell-tagged "runtime " lines for th
156
156
  assert.equal(isDispatchPlumbingLine('runtime 14:42:22 Plan validated for run r1'), false);
157
157
  });
158
158
 
159
+ test('isAgentTraceLine recognises the "Agent:" traces for the Agent status tab', () => {
160
+ // The agent's own reasoning traces were rendered in the Runtime tab,
161
+ // indistinguishable from the business flow. They describe the agent's
162
+ // working, so the Agent status tab shows them with the dispatch plumbing.
163
+ for (const line of [
164
+ '14:42:18 Agent: planning next action…',
165
+ '14:42:19 Agent: classified input as enqueue',
166
+ '14:42:20 Agent: streaming final answer…',
167
+ 'runtime 14:42:18 Agent: planning next action…',
168
+ ]) {
169
+ assert.equal(isAgentTraceLine(line), true, `expected an agent trace: ${line}`);
170
+ }
171
+ });
172
+
173
+ test('isAgentTraceLine leaves every other line for the Runtime tab', () => {
174
+ for (const line of [
175
+ '14:42:18 ▸ Polish proposition — started (knowledge.polish → agent-production)',
176
+ '14:42:19 ✓ Polish proposition — done (1 output)',
177
+ '14:42:21 Run failed: No agent provides capability workspace.restore.',
178
+ '14:42:22 Plan validated for run r1',
179
+ '14:42:23 Agentic runtime: no capabilities available',
180
+ '14:42:24 Plan: 3 task(s) declared from production fragment',
181
+ 'runtime 14:42:21 Run failed: Agent was not ready',
182
+ ]) {
183
+ assert.equal(isAgentTraceLine(line), false, `expected business flow: ${line}`);
184
+ }
185
+ });
186
+
159
187
  test('shortLogId caps an over-long task slug while shortening embedded UUIDs', () => {
160
188
  const long = `${'x'.repeat(48)}-deadbeef`;
161
189
  assert.match(shortLogId(long), /…$/);
@@ -6,7 +6,7 @@ import { promisify } from 'node:util';
6
6
  import YAML from 'yaml';
7
7
  import { checkMissingDockerImages } from './dockerImages.js';
8
8
  import { loadWikircProfile, patchWikircProfile } from './wikirc.js';
9
- import { buildInheritedWikircPatch, copyCmeCredentials } from './workspaceInherit.js';
9
+ import { buildInheritedWikircPatch } from './workspaceInherit.js';
10
10
  import { resolveAgentsComposeContext } from './agentsCompose.js';
11
11
  import { managerEnvFile, managerMcpEndpointsFile, resolveAgentsDataDir } from './env.js';
12
12
  import { createWorkspace, findWorkspace, isValidWorkspaceName, listWorkspaces, managerRoot, workspacesDir } from './workspaces.js';
@@ -245,9 +245,11 @@ export async function createNewWorkspace(name, targetPath, options = {}) {
245
245
  }
246
246
 
247
247
  /**
248
- * @param options.inheritFrom name of the workspace to copy LLM config and CME
249
- * credentials from — normally the session's current workspace. Omitted, or
250
- * unknown, means "scaffold defaults only", the previous behaviour.
248
+ * @param options.inheritFrom name of the workspace to copy LLM config from —
249
+ * normally the session's current workspace. Omitted, or unknown, means
250
+ * "scaffold defaults only", the previous behaviour. Confluence credentials
251
+ * are NOT inherited: agent-cme stores them agent-wide (shared across all
252
+ * workspaces), so a new workspace already sees them.
251
253
  */
252
254
  export async function finalizeCreatedWorkspace(name, options = {}) {
253
255
  const workspace = findWorkspace(name);
@@ -282,13 +284,6 @@ export async function inheritWorkspaceSetup(workspace, sourceName) {
282
284
  // Unreadable or absent profile on either side — nothing to inherit.
283
285
  }
284
286
 
285
- try {
286
- const copied = await copyCmeCredentials(resolveAgentsDataDir(), source.name, workspace.name);
287
- if (copied) inherited.push('cme.app_data.json');
288
- } catch {
289
- // Credentials are a convenience, not a prerequisite.
290
- }
291
-
292
287
  return inherited;
293
288
  }
294
289
 
@@ -1,5 +1,5 @@
1
1
  import assert from 'node:assert/strict';
2
- import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import test from 'node:test';
@@ -300,6 +300,9 @@ test('finalizeCreatedWorkspace seeds a new workspace from the one in use', async
300
300
  '',
301
301
  ].join('\n'));
302
302
 
303
+ // Confluence credentials are agent-wide now — a stale per-workspace
304
+ // app_data.json from a legacy layout must NOT be copied into the new
305
+ // workspace, and must not be reported as inherited.
303
306
  mkdirSync(join(agentsData, 'cme', 'acme', 'cme'), { recursive: true });
304
307
  writeFileSync(join(agentsData, 'cme', 'acme', 'cme', 'app_data.json'), '{"pat":"x"}', 'utf8');
305
308
 
@@ -318,11 +321,8 @@ test('finalizeCreatedWorkspace seeds a new workspace from the one in use', async
318
321
  // The workspace keeps ITS own MCP credential, written just before.
319
322
  assert.equal(parsed.mcp.accessKey, token);
320
323
  assert.ok(inherited.includes('llm.baseUrl'));
321
- assert.ok(inherited.includes('cme.app_data.json'));
322
- assert.equal(
323
- readFileSync(join(agentsData, 'cme', 'fresh', 'cme', 'app_data.json'), 'utf8'),
324
- '{"pat":"x"}',
325
- );
324
+ assert.ok(!inherited.includes('cme.app_data.json'));
325
+ assert.equal(existsSync(join(agentsData, 'cme', 'fresh', 'cme', 'app_data.json')), false);
326
326
  } finally {
327
327
  if (previousDir === undefined) delete process.env.WIKI_WORKSPACES_DIR;
328
328
  else process.env.WIKI_WORKSPACES_DIR = previousDir;
@@ -1,5 +1,3 @@
1
- import { copyFile, mkdir } from 'node:fs/promises';
2
- import { existsSync } from 'node:fs';
3
1
  import { join } from 'node:path';
4
2
 
5
3
  /**
@@ -121,29 +119,3 @@ export function buildInheritedWikircPatch(sourceConfig, targetConfig) {
121
119
  if (Object.keys(vector).length > 0) patch.retrieval = { vector };
122
120
  return { patch, inherited };
123
121
  }
124
-
125
- export function cmeCredentialsPath(agentsDataDir, workspaceName) {
126
- return join(agentsDataDir, 'cme', workspaceName, 'cme', 'app_data.json');
127
- }
128
-
129
- /**
130
- * Carry the Confluence credentials of an existing workspace over to a new one.
131
- *
132
- * `app_data.json` only — NOT `sources-manifest.yaml`. The credentials are a
133
- * property of the operator's Confluence account and are identical everywhere;
134
- * which spaces and pages a workspace exports is precisely what makes it a
135
- * different workspace, and copying that would silently re-export someone
136
- * else's scope on the first run.
137
- */
138
- export async function copyCmeCredentials(agentsDataDir, sourceWorkspace, targetWorkspace) {
139
- if (!agentsDataDir || !sourceWorkspace || !targetWorkspace) return null;
140
- if (sourceWorkspace === targetWorkspace) return null;
141
- const from = cmeCredentialsPath(agentsDataDir, sourceWorkspace);
142
- const to = cmeCredentialsPath(agentsDataDir, targetWorkspace);
143
- if (!existsSync(from)) return null;
144
- // An existing target file is a real configuration; never clobber it.
145
- if (existsSync(to)) return null;
146
- await mkdir(join(agentsDataDir, 'cme', targetWorkspace, 'cme'), { recursive: true });
147
- await copyFile(from, to);
148
- return to;
149
- }
@@ -1,14 +1,10 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
4
- import { tmpdir } from 'node:os';
5
- import { join } from 'node:path';
3
+ import { readFileSync } from 'node:fs';
6
4
  import { fileURLToPath } from 'node:url';
7
5
  import {
8
6
  PLACEHOLDER_VALUE_RE,
9
7
  buildInheritedWikircPatch,
10
- cmeCredentialsPath,
11
- copyCmeCredentials,
12
8
  isRealValue,
13
9
  } from './workspaceInherit.js';
14
10
 
@@ -144,38 +140,14 @@ test('nothing to inherit yields an empty patch', () => {
144
140
  assert.deepEqual(inherited, []);
145
141
  });
146
142
 
147
- test('CME credentials are copied, and the source manifest is not', async () => {
148
- const root = mkdtempSync(join(tmpdir(), 'cme-inherit-'));
149
- const sourceDir = join(root, 'cme', 'acme', 'cme');
150
- mkdirSync(sourceDir, { recursive: true });
151
- writeFileSync(join(sourceDir, 'app_data.json'), '{"auth":{"pat":"secret"}}', 'utf8');
152
- // Export scope is what makes a workspace different — it must NOT travel.
153
- writeFileSync(join(root, 'cme', 'acme', 'sources-manifest.yaml'), 'sources: []\n', 'utf8');
154
-
155
- const copied = await copyCmeCredentials(root, 'acme', 'fresh');
156
-
157
- assert.equal(copied, cmeCredentialsPath(root, 'fresh'));
158
- assert.equal(readFileSync(copied, 'utf8'), '{"auth":{"pat":"secret"}}');
159
- assert.equal(existsSync(join(root, 'cme', 'fresh', 'sources-manifest.yaml')), false);
160
- });
161
-
162
- test('existing CME credentials on the target are never clobbered', async () => {
163
- const root = mkdtempSync(join(tmpdir(), 'cme-inherit-keep-'));
164
- mkdirSync(join(root, 'cme', 'acme', 'cme'), { recursive: true });
165
- mkdirSync(join(root, 'cme', 'fresh', 'cme'), { recursive: true });
166
- writeFileSync(join(root, 'cme', 'acme', 'cme', 'app_data.json'), '{"from":"source"}', 'utf8');
167
- writeFileSync(join(root, 'cme', 'fresh', 'cme', 'app_data.json'), '{"from":"target"}', 'utf8');
168
-
169
- assert.equal(await copyCmeCredentials(root, 'acme', 'fresh'), null);
170
- assert.equal(
171
- readFileSync(cmeCredentialsPath(root, 'fresh'), 'utf8'),
172
- '{"from":"target"}',
143
+ test('Confluence credentials are not part of workspace inheritance', () => {
144
+ // agent-cme stores credentials agent-wide since the shared-config change:
145
+ // a new workspace sees them without any copy. Inheritance must not smuggle
146
+ // an app_data.json (or a sources manifest) into per-workspace state.
147
+ const { patch, inherited } = buildInheritedWikircPatch(
148
+ { llm: WORKING_LLM },
149
+ { llm: SCAFFOLD_LLM },
173
150
  );
174
- });
175
-
176
- test('copying is a no-op without a source, a target, or a source file', async () => {
177
- const root = mkdtempSync(join(tmpdir(), 'cme-inherit-noop-'));
178
- assert.equal(await copyCmeCredentials(root, 'absent', 'fresh'), null);
179
- assert.equal(await copyCmeCredentials(root, null, 'fresh'), null);
180
- assert.equal(await copyCmeCredentials(root, 'acme', 'acme'), null);
151
+ assert.equal(patch['cme.app_data.json'], undefined);
152
+ assert.ok(!inherited.some((key) => key.toLowerCase().includes('cme')));
181
153
  });
@@ -72,8 +72,19 @@ export function createAgentRegistry({
72
72
  const discovered = [];
73
73
  const endpoints = Object.entries(session?.mcp ?? {});
74
74
  const activeServers = new Set(endpoints.map(([serverName]) => serverName));
75
- for (const [serverName, endpoint] of endpoints) {
76
- const agent = await discoverServerAgent(session, serverName, endpoint, { callTool, signal, now });
75
+ // Each probe is an independent network call (agent_describe against one
76
+ // server); none reads another server's result. Probing them concurrently
77
+ // turns the wall-clock cost from the SUM of every server's probe latency
78
+ // into the latency of the SLOWEST one — the sequential version made every
79
+ // delegation get linearly slower as more MCP servers were connected.
80
+ // registerAgent still runs afterward in the original endpoint order, one
81
+ // at a time, so event ordering and the shared-map mutations it performs
82
+ // are unchanged.
83
+ const probed = await Promise.all(
84
+ endpoints.map(([serverName, endpoint]) =>
85
+ discoverServerAgent(session, serverName, endpoint, { callTool, signal, now })),
86
+ );
87
+ for (const agent of probed) {
77
88
  discovered.push(registerAgent(session, agent, { agentsByInstance, instanceByServer, lastProbeFailed }));
78
89
  }
79
90
  for (const [serverName, instanceId] of instanceByServer) {
@@ -22,35 +22,37 @@ export async function discoverRuntimeProviderAgents(runtimeProviders) {
22
22
  const providers = Array.isArray(runtimeProviders)
23
23
  ? runtimeProviders
24
24
  : (runtimeProviders?.list?.() ?? []);
25
- const agents = [];
26
- const unavailable = [];
27
- // Configured-but-not-served capabilities, per runtime (deepagents providers
28
- // fill `lastDiscovery`). A drift is not an outage: the runtime stays
29
- // available with the capabilities it really serves, and the difference is
30
- // reported so an operator can see WHY agent.research is not routable.
31
- const drift = [];
32
25
 
33
- for (const entry of providers) {
26
+ // One provider's describe()/discoverCapabilities() never reads another
27
+ // provider's result, so probe them concurrently (same reasoning as
28
+ // agentRegistry.discover()) instead of paying the sum of every runtime's
29
+ // latency in sequence. Each entry still produces its own ordered
30
+ // {agents, unavailable, drift} slice, flattened below in the original
31
+ // `providers` order so output ordering is unchanged.
32
+ const results = await Promise.all(providers.map(async (entry) => {
34
33
  const provider = entry?.provider ?? entry;
35
34
  const runtimeId = String(entry?.id ?? provider?.runtime ?? 'external-runtime');
35
+ const agents = [];
36
+ const unavailable = [];
37
+ const drift = [];
36
38
  let description;
37
39
  try {
38
40
  assertRuntimeProvider(provider);
39
41
  description = await provider.describe();
40
42
  } catch (error) {
41
43
  unavailable.push({ runtimeId, error: error instanceof Error ? error.message : String(error) });
42
- continue;
44
+ return { agents, unavailable, drift };
43
45
  }
44
46
  if (description?.health === 'unavailable') {
45
47
  unavailable.push({ runtimeId, error: description?.error ?? 'runtime reports unavailable' });
46
- continue;
48
+ return { agents, unavailable, drift };
47
49
  }
48
50
  let capabilities;
49
51
  try {
50
52
  capabilities = await provider.discoverCapabilities();
51
53
  } catch (error) {
52
54
  unavailable.push({ runtimeId, error: error instanceof Error ? error.message : String(error) });
53
- continue;
55
+ return { agents, unavailable, drift };
54
56
  }
55
57
  const health = ['available', 'degraded'].includes(description?.health)
56
58
  ? description.health
@@ -62,7 +64,16 @@ export async function discoverRuntimeProviderAgents(runtimeProviders) {
62
64
  if (discovery && Array.isArray(discovery.missing) && discovery.missing.length > 0) {
63
65
  drift.push({ runtimeId, missing: [...discovery.missing], served: [...(discovery.served ?? [])] });
64
66
  }
65
- }
67
+ return { agents, unavailable, drift };
68
+ }));
69
+
70
+ // Configured-but-not-served capabilities, per runtime (deepagents providers
71
+ // fill `lastDiscovery`). A drift is not an outage: the runtime stays
72
+ // available with the capabilities it really serves, and the difference is
73
+ // reported so an operator can see WHY agent.research is not routable.
74
+ const agents = results.flatMap((r) => r.agents);
75
+ const unavailable = results.flatMap((r) => r.unavailable);
76
+ const drift = results.flatMap((r) => r.drift);
66
77
 
67
78
  return { agents, unavailable, drift };
68
79
  }
@@ -1,6 +1,6 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
  import { createMemo, createSignal, Index, Show } from 'solid-js';
3
- import { compactRuntimeLogForDisplay, filterRuntimeLogs, isDispatchPlumbingLine } from '../core/runtimeLog.js';
3
+ import { compactRuntimeLogForDisplay, filterRuntimeLogs, isAgentTraceLine, isDispatchPlumbingLine } from '../core/runtimeLog.js';
4
4
  import { fit } from './textFit';
5
5
 
6
6
  type PlanStep = { step: number; description: string; status: string };
@@ -409,13 +409,14 @@ export function LogPanel(props: { logs: string[]; width: number; filter?: string
409
409
  const [activeLogTab, setActiveLogTab] = createSignal<'flow' | 'agent-status'>('flow');
410
410
  const lineWidth = () => Math.max(8, props.width - 2);
411
411
  // "Agent status" collects the dispatch plumbing — capability resolution,
412
- // agent selection, agent_execute/agent_status polling, job acceptance — so
413
- // the "Runtime" tab is left with the readable business flow (plan + the
414
- // ▸/✓/✗/↻ task lines). isDispatchPlumbingLine (shared with agentEvents'
415
- // dedup) recognises the formatRuntimeLogPayload shape structurally; the old
416
- // token enumeration only ever matched agent_status/agent_execute and missed
417
- // every dotted event ('job.accepted' → 'ACCEPTED', …).
418
- const isAgentStatus = (line: string) => isDispatchPlumbingLine(line);
412
+ // agent selection, agent_execute/agent_status polling, job acceptance — and
413
+ // the agent's own reasoning traces ("Agent: …"), so the "Runtime" tab is
414
+ // left with the readable business flow (plan + the ▸/✓/✗/↻ task lines).
415
+ // isDispatchPlumbingLine (shared with agentEvents' dedup) recognises the
416
+ // formatRuntimeLogPayload shape structurally; the old token enumeration
417
+ // only ever matched agent_status/agent_execute and missed every dotted
418
+ // event ('job.accepted' → 'ACCEPTED', …).
419
+ const isAgentStatus = (line: string) => isDispatchPlumbingLine(line) || isAgentTraceLine(line);
419
420
  // Filtering preserves the runtime history order. logRenderLines performs
420
421
  // the single block-level reversal shared by both tabs.
421
422
  const filteredLogs = () => filterRuntimeLogs(props.logs, props.filter ?? '')
@@ -101,6 +101,13 @@ test('ShellUI renders newest-first order in both Runtime and Agent status', asyn
101
101
  assert.match(filteredLogs, /activeLogTab\(\) === 'agent-status'/);
102
102
  assert.match(filteredLogs, /isAgentStatus\(line\)/);
103
103
  assert.doesNotMatch(filteredLogs, /\.reverse\(\)/);
104
+ const logPanelBody = source.slice(
105
+ source.indexOf('export function LogPanel'),
106
+ source.indexOf('const filteredLogs ='),
107
+ );
108
+ // Agent reasoning traces route to the Agent status tab with the dispatch
109
+ // plumbing — the Runtime tab keeps the business flow.
110
+ assert.match(logPanelBody, /isDispatchPlumbingLine\(line\) \|\| isAgentTraceLine\(line\)/);
104
111
  const renderedLogs = source.slice(
105
112
  source.indexOf('function logRenderLines'),
106
113
  source.indexOf('function logEntryLines'),