@dotdrelle/wiki-manager 0.14.12 → 0.14.13

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.14.12",
3
+ "version": "0.14.13",
4
4
  "description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "author": "dotrelle",
@@ -31,6 +31,9 @@ function normalizePlanSteps(steps) {
31
31
  executor: s?.executor ?? null,
32
32
  executorQuery: s?.executorQuery ?? null,
33
33
  outputRefs: Array.isArray(s?.outputRefs) ? s.outputRefs.map(String) : [],
34
+ ...(s?.status != null ? { status: String(s.status) } : {}),
35
+ ...(s?.startedAt != null ? { startedAt: s.startedAt } : {}),
36
+ ...(s?.finishedAt != null ? { finishedAt: s.finishedAt } : {}),
34
37
  }));
35
38
  }
36
39
 
@@ -149,6 +152,7 @@ function productionActivityFromPayload(payload, context = {}) {
149
152
  step,
150
153
  ...(payload?.taskId ? { stepId: String(payload.taskId) } : {}),
151
154
  },
155
+ plan: Array.isArray(progress?.steps) ? { steps: progress.steps } : null,
152
156
  poll: jobId ? {
153
157
  server: 'production',
154
158
  tool: context.tool === 'agent_status' ? 'agent_status' : 'production_job_status',
@@ -109,6 +109,37 @@ test('extractActivity: documents conversion activity carries plan and percent',
109
109
  assert.deepEqual(activity.plan.steps.map((step) => step.id), ['resolve', 'convert', 'write']);
110
110
  });
111
111
 
112
+ test('extractActivity: agent_status carries structured processing progress', () => {
113
+ const activity = extractActivity({
114
+ jobId: 'job-build',
115
+ taskId: 'build-report',
116
+ operation: 'build',
117
+ status: 'running',
118
+ progress: {
119
+ percent: 47,
120
+ phase: 'build',
121
+ detail: 'Batch 2/4 · LLM throttled',
122
+ stepIndex: 1,
123
+ stepTotal: 2,
124
+ steps: [
125
+ { id: 'build', name: 'build', status: 'running' },
126
+ { id: 'export', name: 'export', status: 'pending' },
127
+ ],
128
+ batch: { index: 2, total: 4, status: 'running' },
129
+ throttling: { active: true, waitMs: 1200, retryAt: '2026-07-21T10:00:01Z' },
130
+ processing: { instructionCount: 8 },
131
+ },
132
+ }, { server: 'production', tool: 'agent_status' });
133
+
134
+ assert.deepEqual(activity.plan.steps.map((step) => [step.id, step.status]), [
135
+ ['build', 'running'],
136
+ ['export', 'pending'],
137
+ ]);
138
+ assert.equal(activity.progress.batch.index, 2);
139
+ assert.equal(activity.progress.throttling.active, true);
140
+ assert.equal(activity.progress.processing.instructionCount, 8);
141
+ });
142
+
112
143
  test('rememberActivity: returns normalized activity on success', () => {
113
144
  const session = {};
114
145
  const result = rememberActivity(session, { id: '1', status: 'running', source: 'x', kind: 'job' });
@@ -122,13 +122,22 @@ export function resetSessionProjection(session) {
122
122
 
123
123
  function withSessionRunIdentity(event, session) {
124
124
  const identity = session?._currentRunIdentity;
125
- if (!identity) return event;
125
+ // The workspace must be attached even OUTSIDE a run identity: the SSE
126
+ // publisher drops events whose workspace does not match the (always
127
+ // workspace-scoped) serve UI client. Events dispatched between chained
128
+ // executions — e.g. the plan_set of the next action, run evaluation,
129
+ // assistant messages — used to leave with workspace=null and never reached
130
+ // the UI, which then displayed a stale plan from the previous action.
131
+ const workspace = event.workspace ?? identity?.workspace ?? session?.workspace ?? null;
132
+ if (!identity) {
133
+ return workspace === (event.workspace ?? null) ? event : { ...event, workspace };
134
+ }
126
135
  return {
127
136
  ...event,
128
137
  runId: event.runId ?? identity.runId ?? null,
129
138
  turnId: event.turnId ?? identity.turnId ?? null,
130
139
  taskId: event.taskId ?? identity.taskId ?? null,
131
- workspace: event.workspace ?? identity.workspace ?? null,
140
+ workspace,
132
141
  };
133
142
  }
134
143
 
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "0.14.12",
3
- "commit": "621830d"
2
+ "version": "0.14.13",
3
+ "commit": "f0faf8b"
4
4
  }
package/src/core/env.js CHANGED
@@ -98,14 +98,26 @@ export function ensureManagerScaffold({ log = () => {} } = {}) {
98
98
  return created;
99
99
  }
100
100
 
101
+ // Mirrors workspacesDir() in core/workspaces.js (which imports this module,
102
+ // so it cannot be imported here without a cycle).
103
+ function workspacesRootDir() {
104
+ return process.env.WIKI_WORKSPACES_DIR
105
+ ? resolve(process.env.WIKI_WORKSPACES_DIR)
106
+ : join(userManagerDir(), 'workspaces');
107
+ }
108
+
101
109
  // Single source of truth for where `.agents-data` lives, shared by the
102
- // manager's own document intake and by the host path it mounts into agent
103
- // containers — keeping them in sync avoids the two silently drifting apart.
110
+ // manager's own document intake, the per-workspace serve stack, and the
111
+ // GLOBAL agents stack (documents/cme containers). All three must resolve the
112
+ // same host directory: serve writes an upload into its mount and the
113
+ // documents container must find it under the identical path — a divergence
114
+ // here is exactly the clean-install "Input file does not exist" bug.
115
+ // Canonical location: `<workspaces root>/.agents-data`, session or not.
104
116
  export function resolveAgentsDataDir(session = null) {
105
117
  const configured = process.env.AGENTS_DATA_DIR;
106
- if (configured) return isAbsolute(configured) ? configured : resolve(defaultRuntimeStateDir(), configured);
118
+ if (configured) return isAbsolute(configured) ? configured : resolve(managerStateDir(), configured);
107
119
  if (session?.workspacePath) return resolve(dirname(session.workspacePath), '.agents-data');
108
- return resolve(defaultRuntimeStateDir(), 'agents-data');
120
+ return resolve(workspacesRootDir(), '.agents-data');
109
121
  }
110
122
 
111
123
  function parseEnvValue(value) {
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.14.12';
4
+ const WIKI_MANAGER_VERSION = '0.14.13';
5
5
 
6
6
  function envValue(key) {
7
7
  const filePath = managerEnvFile();
@@ -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 { patchWikircProfile } from './wikirc.js';
9
- import { managerEnvFile, managerMcpEndpointsFile } from './env.js';
9
+ import { managerEnvFile, managerMcpEndpointsFile, resolveAgentsDataDir } from './env.js';
10
10
  import { createWorkspace, findWorkspace, isValidWorkspaceName, listWorkspaces, managerRoot, workspacesDir } from './workspaces.js';
11
11
 
12
12
  const execFileAsync = promisify(execFile);
@@ -68,6 +68,12 @@ export async function startAgents(options = {}) {
68
68
  // user's .env stayed empty on a fresh install.
69
69
  WIKI_MANAGER_ENV_FILE: managerEnvFile(),
70
70
  WIKI_MANAGER_ENDPOINTS_FILE: managerMcpEndpointsFile(),
71
+ // Same pinning for .agents-data: without it the script defaults to
72
+ // $PWD/.agents-data (the PACKAGE directory), while the serve stack
73
+ // mounts the workspace-root .agents-data — the documents container
74
+ // then reads an empty input dir and every conversion fails with
75
+ // "Input file does not exist" on a clean install.
76
+ AGENTS_DATA_DIR: resolveAgentsDataDir(),
71
77
  },
72
78
  timeout: options.timeout ?? 180_000,
73
79
  maxBuffer: options.maxBuffer ?? 1024 * 1024 * 8,
@@ -88,6 +94,7 @@ export async function stopAgents(options = {}) {
88
94
  env: {
89
95
  ...process.env,
90
96
  WIKI_WORKSPACES_DIR: workspacesDir(),
97
+ AGENTS_DATA_DIR: resolveAgentsDataDir(),
91
98
  },
92
99
  timeout: options.timeout ?? 120_000,
93
100
  maxBuffer: options.maxBuffer ?? 1024 * 1024 * 8,
@@ -607,8 +607,25 @@ function failedTaskActivities(session) {
607
607
  return failedKeys.size > 0 ? all.filter((activity) => failedKeys.has(activity.key)) : all;
608
608
  }
609
609
 
610
- function ensurePlanProjection(session, runId) {
611
- if (!session.headlessPlan || session.agentProjection?.plan) return;
610
+ // Identity of a plan as displayed: the ordered list of task ids. Two plans
611
+ // with the same tasks in the same order are the same plan for projection
612
+ // purposes (statuses update through plan_step_updated, not a re-projection).
613
+ function planIdentitySignature(plan) {
614
+ return Array.isArray(plan) ? plan.map(planTaskId).join('|') : '';
615
+ }
616
+
617
+ export function ensurePlanProjection(session, runId) {
618
+ if (!session.headlessPlan) return;
619
+ // Re-emit plan_set not only when the projection has no plan yet, but also
620
+ // when the session plan has CHANGED shape since the last projection. A
621
+ // chained run (e.g. /skill pipeline) sets a fresh headlessPlan for step 2
622
+ // while the projection still holds step 1's plan; the old guard
623
+ // (`agentProjection?.plan` truthy → skip) then left the UI stuck on the
624
+ // step 1 plan. Compare task identities and re-project when they differ.
625
+ const projected = session.agentProjection?.plan ?? null;
626
+ if (projected && planIdentitySignature(projected) === planIdentitySignature(session.headlessPlan)) {
627
+ return;
628
+ }
612
629
  dispatchAgentEvent(session, createAgentEvent('plan_set', {
613
630
  origin: 'runtime',
614
631
  runId,
@@ -1,7 +1,38 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
3
  import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
4
- import { evaluateRuntimeRun, finishRuntimeRun, materializeTaskInputs, replanRuntimeRun, runRuntimeAgenticWorkflow, runRuntimeParallelPlan, shouldUseParallelScheduler } from './runner.js';
4
+ import { ensurePlanProjection, evaluateRuntimeRun, finishRuntimeRun, materializeTaskInputs, replanRuntimeRun, runRuntimeAgenticWorkflow, runRuntimeParallelPlan, shouldUseParallelScheduler } from './runner.js';
5
+
6
+ test('ensurePlanProjection re-projects when the chained plan changes shape (step 2)', () => {
7
+ const session = { agentEvents: [], agentProjection: null };
8
+ // Step 1: a fresh plan is projected.
9
+ session.headlessPlan = [{ id: 'a', description: 'ingest', status: 'pending' }];
10
+ ensurePlanProjection(session, 'run-1');
11
+ assert.deepEqual(
12
+ (session.agentProjection?.plan ?? []).map((step) => step.id),
13
+ ['a'],
14
+ );
15
+
16
+ // Step 1 completes in place — same plan shape, no re-projection needed.
17
+ session.headlessPlan = [{ id: 'a', description: 'ingest', status: 'done' }];
18
+ const eventsAfterStep1 = session.agentEvents.length;
19
+ ensurePlanProjection(session, 'run-1');
20
+ assert.equal(session.agentEvents.length, eventsAfterStep1, 'unchanged shape must not re-emit plan_set');
21
+
22
+ // Step 2 adds a task — the projection must now reflect the new plan.
23
+ session.headlessPlan = [
24
+ { id: 'a', description: 'ingest', status: 'done' },
25
+ { id: 'b', description: 'build', status: 'pending' },
26
+ ];
27
+ ensurePlanProjection(session, 'run-1');
28
+ assert.deepEqual(
29
+ (session.agentProjection?.plan ?? []).map((step) => step.id),
30
+ ['a', 'b'],
31
+ 'step 2 plan must be projected instead of staying on step 1',
32
+ );
33
+ // Completed step 1 status is preserved through the re-projection.
34
+ assert.equal(session.agentProjection.plan.find((step) => step.id === 'a').status, 'done');
35
+ });
5
36
 
6
37
  test('validated tasks waiting for approval stay in the scheduler instead of falling back to Donna', () => {
7
38
  assert.equal(shouldUseParallelScheduler([
package/wiki-workspace CHANGED
@@ -210,8 +210,11 @@ agents_compose() {
210
210
  [[ -f "$agents_compose_file" ]] || die "agents compose file not found: $agents_compose_file"
211
211
 
212
212
  local agents_data_dir workspaces_root
213
- agents_data_dir="$(absolute_path "${AGENTS_DATA_DIR:-.agents-data}" "$DEFAULT_MANAGER_DIR")"
214
213
  workspaces_root="$(absolute_path "$WORKSPACES_DIR" "$DEFAULT_MANAGER_DIR")"
214
+ # Canonical .agents-data lives next to the workspaces (same rule as the
215
+ # manager's resolveAgentsDataDir): the serve stack and the agents stack MUST
216
+ # mount the same host directory or document conversions break.
217
+ agents_data_dir="$(absolute_path "${AGENTS_DATA_DIR:-$workspaces_root/.agents-data}" "$DEFAULT_MANAGER_DIR")"
215
218
 
216
219
  local compose_env_args=()
217
220
  [[ -f "$MANAGER_ENV_FILE" ]] && compose_env_args+=(--env-file "$MANAGER_ENV_FILE")
@@ -709,7 +712,8 @@ compose_for_workspace() {
709
712
  mcp_port="$(workspace_value "$workspace" WIKI_MCP_PORT 3101)"
710
713
  prod_port="$(workspace_value "$workspace" PRODUCTION_MCP_PORT 3102)"
711
714
  project="$(compose_project "$workspace")"
712
- agents_data_dir="$(absolute_path "${AGENTS_DATA_DIR:-.agents-data}" "$DEFAULT_MANAGER_DIR")"
715
+ # Same canonical rule as agents_compose: .agents-data next to the workspaces.
716
+ agents_data_dir="$(absolute_path "${AGENTS_DATA_DIR:-$(dirname "$ws_path")/.agents-data}" "$DEFAULT_MANAGER_DIR")"
713
717
 
714
718
  local compose_env_args=()
715
719
  [[ -f "$MANAGER_ENV_FILE" ]] && compose_env_args+=(--env-file "$MANAGER_ENV_FILE")
@@ -920,7 +924,8 @@ config_workspace() {
920
924
  [[ -f "$env_file" ]] && die "workspace already configured: $env_file"
921
925
 
922
926
  local port_base serve_port mcp_port prod_port wiki_token prod_token agents_data_dir
923
- agents_data_dir="$(absolute_path "${AGENTS_DATA_DIR:-.agents-data}" "$DEFAULT_MANAGER_DIR")"
927
+ # Same canonical rule as agents_compose: .agents-data next to the workspaces.
928
+ agents_data_dir="$(absolute_path "${AGENTS_DATA_DIR:-$(dirname "$target_path")/.agents-data}" "$DEFAULT_MANAGER_DIR")"
924
929
  port_base="$(find_free_workspace_port_base 3100)"
925
930
  serve_port="$port_base"
926
931
  mcp_port=$((port_base + 1))