@dotdrelle/wiki-manager 0.6.47 → 0.7.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.
@@ -80,8 +80,8 @@ async function main() {
80
80
  // Fallback for already-bootstrapped direct invocations; the shell wrapper
81
81
  // exports these before Bun starts.
82
82
  if (parsed.cacert) Object.assign(process.env, cacertEnvVars(parsed.cacert));
83
- await import('@opentui/solid/preload');
84
- const interactive = process.stdout.isTTY && process.stdin.isTTY && !argv.includes('--setup-wizard') && !argv.includes('--headless') && !argv.includes('--once') && !argv.includes('--version') && !argv.includes('-v') && !argv.includes('--help') && !argv.includes('-h');
83
+ const interactive = process.stdout.isTTY && process.stdin.isTTY && argv[0] !== 'runtime' && !argv.includes('--setup-wizard') && !argv.includes('--headless') && !argv.includes('--once') && !argv.includes('--version') && !argv.includes('-v') && !argv.includes('--help') && !argv.includes('-h');
84
+ if (interactive || argv.includes('--setup-wizard')) await import('@opentui/solid/preload');
85
85
  if (interactive) process.stdout.write('Starting wiki-manager…\r');
86
86
  const { runCli } = await import('../src/cli/wiki-manager.js');
87
87
  await runCli(argv);
@@ -62,6 +62,8 @@ services:
62
62
  - DOCUMENT_MAX_UPLOAD_BYTES=${DOCUMENT_MAX_UPLOAD_BYTES:-52428800}
63
63
  - WIKI_MCP_PROXY_URL=http://host.docker.internal:${WIKI_MCP_PORT:-3101}/mcp
64
64
  - PRODUCTION_MCP_PROXY_URL=http://host.docker.internal:${PRODUCTION_MCP_PORT:-3102}/mcp/
65
+ - WIKI_MANAGER_RUNTIME_URL=http://host.docker.internal:7788
66
+ - WIKI_MANAGER_RUNTIME_TOKEN=${WIKI_MANAGER_RUNTIME_TOKEN:-}
65
67
  # HTTPS — set paths inside the container (e.g. /certs/server.crt) and uncomment the volume above
66
68
  #- WIKI_SERVE_TLS_CERT_PATH=/certs/server.crt
67
69
  #- WIKI_SERVE_TLS_KEY_PATH=/certs/server.key
@@ -95,6 +97,25 @@ services:
95
97
  - host.docker.internal:host-gateway
96
98
  restart: unless-stopped
97
99
 
100
+ # ── wiki-manager runtime ─────────────────────────────────────────────────
101
+
102
+ agent-runtime:
103
+ image: dotdrelle/llm-wiki-manager:latest
104
+ labels:
105
+ wiki-manager.description: "Agentic runtime — runs, plan, activities, queue."
106
+ command: runtime --host 0.0.0.0 --port 7788 --state-dir /state
107
+ volumes:
108
+ - ./.wiki-manager:/state
109
+ - ${WIKI_WORKSPACES_DIR:-./workspaces}:/workspaces
110
+ environment:
111
+ - WIKI_WORKSPACES_DIR=/workspaces
112
+ - WIKI_MANAGER_RUNTIME_TOKEN=${WIKI_MANAGER_RUNTIME_TOKEN:-}
113
+ ports:
114
+ - '127.0.0.1:7788:7788'
115
+ extra_hosts:
116
+ - host.docker.internal:host-gateway
117
+ restart: unless-stopped
118
+
98
119
  # ── agent-wiki-production ─────────────────────────────────────────────────
99
120
 
100
121
  production-mcp:
@@ -119,7 +140,7 @@ services:
119
140
  x-wiki-manager:
120
141
  service-aliases:
121
142
  all:
122
- targets: [serve, mcp-http, production-mcp]
143
+ targets: [serve, mcp-http, agent-runtime, production-mcp]
123
144
  description: "Full workspace service set."
124
145
  ui:
125
146
  targets: [serve]
@@ -130,6 +151,9 @@ x-wiki-manager:
130
151
  mcp:
131
152
  targets: [mcp-http]
132
153
  description: "Alias for mcp-http: wiki MCP server."
154
+ runtime:
155
+ targets: [agent-runtime]
156
+ description: "Alias for agent-runtime: agentic runtime."
133
157
  production:
134
158
  targets: [production-mcp]
135
159
  description: "Alias for production-mcp: production jobs."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdrelle/wiki-manager",
3
- "version": "0.6.47",
3
+ "version": "0.7.3",
4
4
  "description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "author": "dotrelle",
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "scripts": {
13
13
  "start": "bun ./bin/wiki-manager.js",
14
- "test": "node --test src/core/activity.test.js src/core/agentEvents.test.js src/core/plan.test.js src/core/mcp.test.js src/core/documentIntake.test.js src/core/wikirc.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/commands/slash.test.js",
14
+ "test": "node --test src/core/activity.test.js src/core/agentEvents.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/wikirc.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/commands/slash.test.js src/shell/repl.test.js src/runtime/store.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/auth.test.js",
15
15
  "check": "bun ./bin/wiki-manager.js --version && bun ./bin/wiki-manager.js --help && bun ./bin/wiki-manager.js --once \"verifie le mode agent\""
16
16
  },
17
17
  "engines": {
@@ -1,17 +1,21 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { readFileSync } from 'node:fs';
2
3
  import { mkdir, writeFile } from 'node:fs/promises';
3
4
  import { dirname, join, resolve } from 'node:path';
4
5
  import { fileURLToPath } from 'node:url';
5
6
  import { loadManagerEnv } from '../core/env.js';
6
7
  loadManagerEnv();
7
- import { createAgentGraph, buildAgentSystemPrompt, buildLimitedAgentResponse } from '../agent/graph.js';
8
+ import { createAgentGraph } from '../agent/graph.js';
8
9
  import { handleSlashCommand, printHelp, printVersion } from '../commands/slash.js';
9
10
  import { runShell } from '../shell/repl.js';
10
11
  import { runChecks } from '../core/startupCheck.js';
11
12
  import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
12
- import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
13
- import { extractHeadlessPlan, syncActivitiesToPlan, formatPlanStatus, formatCompletedActivities } from '../core/plan.js';
13
+ import { extractActivity, parseJsonText, sessionActivities, terminalFailures } from '../core/activity.js';
14
+ import { syncActivitiesToPlan, formatPlanStatus } from '../core/plan.js';
14
15
  import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
16
+ import { runAgentTurn, runAgenticLoop } from '../core/agentLoop.js';
17
+ // Runtime modules use node:sqlite (Node.js built-in unavailable in Bun).
18
+ // They are imported dynamically so the shell / TUI path never loads them.
15
19
 
16
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
21
  const packageJsonPath = resolve(__dirname, '../../package.json');
@@ -39,6 +43,7 @@ function createSession() {
39
43
  packageJson,
40
44
  conversations: { __global__: [] },
41
45
  activities: {},
46
+ jobQueue: [],
42
47
  productionActivity: null,
43
48
  headlessPlan: null,
44
49
  };
@@ -57,20 +62,6 @@ async function writeHeadlessLog(session, lines, explicitPath) {
57
62
  return logPath;
58
63
  }
59
64
 
60
- function activitySnapshot(session) {
61
- return new Set(sessionActivities(session).map((a) => a.key));
62
- }
63
-
64
- function newNonTerminalActivities(snapshotBefore, session) {
65
- return sessionActivities(session).filter((a) => !snapshotBefore.has(a.key) && !a.terminal);
66
- }
67
-
68
- function terminalFailures(activities) {
69
- return activities.filter(
70
- (a) => a.terminal && ['failed', 'error', 'cancelled', 'canceled'].includes(String(a.status).toLowerCase()),
71
- );
72
- }
73
-
74
65
 
75
66
  async function runHeadlessActivityLoop(session, log, { wait, timeoutMs }) {
76
67
  if (!wait) return { exitCode: 0, completed: [], timedOut: false };
@@ -150,118 +141,54 @@ async function runHeadlessActivityLoop(session, log, { wait, timeoutMs }) {
150
141
  return { exitCode: 1, completed, timedOut: true };
151
142
  }
152
143
 
153
- async function runHeadlessAgentTurn(agent, session, input, log, messages = []) {
154
- session.packageJson = packageJson;
155
- let streamedContent = '';
156
- session._onStream = (delta) => { streamedContent += delta; };
157
- session._onStreamReset = () => { streamedContent = ''; };
158
- let result;
159
- try {
160
- result = await agent.invoke({ input, session, messages });
161
- } finally {
162
- delete session._onStream;
163
- delete session._onStreamReset;
164
- }
165
- if (result.streamedInline) {
166
- return streamedContent.trim() || buildLimitedAgentResponse({ input, session }, 'LLM stream ended without content');
167
- }
168
- if (result.response != null) return result.response;
169
- if (result.readyToStream && session.llm?.stream) {
170
- const { system, messages: streamMessages = [] } = result.streamContext ?? {};
171
- let content = '';
172
- for await (const delta of session.llm.stream({
173
- system: system ?? buildAgentSystemPrompt({ input, session }),
174
- messages: streamMessages,
175
- })) {
176
- content += delta;
177
- }
178
- return content.trim() || buildLimitedAgentResponse({ input, session }, 'LLM stream ended without content');
179
- }
180
- return buildLimitedAgentResponse({ input, session });
181
- }
182
-
183
144
  async function runHeadlessAgenticLoop(agent, session, initialInput, log, { timeoutMs, maxTurns }) {
184
- const conversationHistory = [];
185
- let currentInput = initialInput;
186
-
187
- for (let turn = 1; turn <= maxTurns; turn++) {
188
- log.push(`agentic-loop: turn ${turn}/${maxTurns}`);
189
- console.log(`[headless] Agent turn ${turn}/${maxTurns}…`);
190
-
191
- const snapshot = activitySnapshot(session);
192
-
193
- const response = await runHeadlessAgentTurn(agent, session, currentInput, log, conversationHistory);
194
- log.push(`agentic-loop: turn ${turn} response:`);
195
- log.push(response);
196
- console.log(response);
197
-
198
- conversationHistory.push(
199
- { role: 'user', content: currentInput },
200
- { role: 'assistant', content: response },
201
- );
202
-
203
- // session.headlessPlan is set authoritatively by wiki__plan_set tool call.
204
- // Fall back to text extraction only if the agent didn't call the tool.
205
- if (turn === 1 && session.headlessPlan === null) {
206
- const extractedPlan = extractHeadlessPlan(response);
207
- if (extractedPlan) {
208
- dispatchAgentEvent(session, createAgentEvent('plan_set', {
209
- origin: 'llm',
210
- payload: { steps: extractedPlan },
211
- }));
212
- log.push(`agentic-loop: plan extracted from text (${session.headlessPlan.length} steps, fallback)`);
213
- }
214
- } else if (turn === 1 && session.headlessPlan) {
215
- log.push(`agentic-loop: plan set via tool (${session.headlessPlan.length} steps)`);
216
- }
217
-
218
- const newPending = newNonTerminalActivities(snapshot, session);
219
- if (newPending.length === 0) {
220
- const pendingSteps = (session.headlessPlan ?? []).filter((s) => s.status === 'pending');
221
- if (pendingSteps.length === 0) {
222
- log.push('agentic-loop: no new non-terminal activities — plan complete');
223
- console.log('[headless] Plan complete.');
224
- return { exitCode: 0 };
225
- }
145
+ const result = await runAgenticLoop(agent, session, initialInput, {
146
+ timeoutMs,
147
+ maxTurns,
148
+ waitForActivities: async (turnSession, _startedActivities, waitOptions) => {
149
+ const waitResult = await runHeadlessActivityLoop(turnSession, log, { wait: true, timeoutMs: waitOptions.timeoutMs });
150
+ return {
151
+ ok: waitResult.exitCode === 0 && !waitResult.timedOut,
152
+ ...waitResult,
153
+ };
154
+ },
155
+ onTurnStart: ({ turn, maxTurns: totalTurns }) => {
156
+ log.push(`agentic-loop: turn ${turn}/${totalTurns}`);
157
+ console.log(`[headless] Agent turn ${turn}/${totalTurns}…`);
158
+ },
159
+ onTurnResponse: ({ turn, response }) => {
160
+ log.push(`agentic-loop: turn ${turn} response:`);
161
+ log.push(response);
162
+ console.log(response);
163
+ },
164
+ onPlanExtracted: ({ steps }) => {
165
+ log.push(`agentic-loop: plan extracted from text (${steps.length} steps, fallback)`);
166
+ },
167
+ onPlanAlreadySet: ({ steps }) => {
168
+ log.push(`agentic-loop: plan set via tool (${steps.length} steps)`);
169
+ },
170
+ onComplete: () => {
171
+ log.push('agentic-loop: no new non-terminal activities — plan complete');
172
+ console.log('[headless] Plan complete.');
173
+ },
174
+ onPendingSteps: ({ pendingSteps }) => {
226
175
  log.push(`agentic-loop: no new async activity, ${pendingSteps.length} pending step(s) remain`);
227
176
  if (session.headlessPlan) log.push(`agentic-loop: plan status:\n${formatPlanStatus(session.headlessPlan)}`);
228
- currentInput = [
229
- 'Original task:',
230
- initialInput,
231
- '',
232
- `Plan status:\n${formatPlanStatus(session.headlessPlan)}`,
233
- '',
234
- 'No new background activity was started in the previous turn.',
235
- 'Continue the original plan. Start the next pending step only.',
236
- 'If required information is missing and cannot be inferred, stop with a clear blocker.',
237
- ].join('\n');
238
- continue;
239
- }
240
-
241
- log.push(`agentic-loop: ${newPending.length} new job(s) started, waiting…`);
242
- const { exitCode, completed, timedOut } = await runHeadlessActivityLoop(session, log, { wait: true, timeoutMs });
243
-
244
- if (timedOut || exitCode !== 0) return { exitCode };
245
-
246
- const summary = formatCompletedActivities(completed);
247
- log.push(`agentic-loop: completed activities:\n${summary}`);
248
- if (session.headlessPlan) log.push(`agentic-loop: plan status:\n${formatPlanStatus(session.headlessPlan)}`);
249
- currentInput = [
250
- 'Original task:',
251
- initialInput,
252
- '',
253
- session.headlessPlan ? `Plan status:\n${formatPlanStatus(session.headlessPlan)}\n` : null,
254
- 'Completed activities:',
255
- summary,
256
- '',
257
- 'Continue the original plan. Start the next required step only.',
258
- 'If all steps are complete, provide the final summary.',
259
- ].filter(Boolean).join('\n');
260
- }
261
-
262
- log.push(`agentic-loop: max turns (${maxTurns}) reached without completing`);
263
- console.error(`[headless] Max agent turns (${maxTurns}) reached.`);
264
- return { exitCode: 1 };
177
+ },
178
+ onActivitiesStarted: ({ activities }) => {
179
+ log.push(`agentic-loop: ${activities.length} new job(s) started, waiting…`);
180
+ },
181
+ onActivitiesCompleted: ({ summary }) => {
182
+ log.push(`agentic-loop: completed activities:\n${summary}`);
183
+ if (session.headlessPlan) log.push(`agentic-loop: plan status:\n${formatPlanStatus(session.headlessPlan)}`);
184
+ },
185
+ onMaxTurns: ({ maxTurns: totalTurns }) => {
186
+ log.push(`agentic-loop: max turns (${totalTurns}) reached without completing`);
187
+ console.error(`[headless] Max agent turns (${totalTurns}) reached.`);
188
+ },
189
+ });
190
+
191
+ return { exitCode: result.ok ? 0 : (result.waitResult?.exitCode ?? 1) };
265
192
  }
266
193
 
267
194
  async function runHeadless(argv, agent) {
@@ -326,7 +253,7 @@ async function runHeadless(argv, agent) {
326
253
  if (useAgenticLoop) {
327
254
  ({ exitCode } = await runHeadlessAgenticLoop(agent, session, input, log, { timeoutMs, maxTurns }));
328
255
  } else {
329
- const response = await runHeadlessAgentTurn(agent, session, input, log);
256
+ const response = await runAgentTurn(agent, session, input);
330
257
  log.push('response:');
331
258
  log.push(response);
332
259
  console.log(response);
@@ -345,7 +272,171 @@ async function runHeadless(argv, agent) {
345
272
  }
346
273
  }
347
274
 
275
+ async function runRuntime(argv, agent) {
276
+ if (argv.includes('--help') || argv.includes('-h')) {
277
+ console.log([
278
+ 'Usage: wiki-manager runtime [--host <host>] [--port <port>] [--state-dir <dir>]',
279
+ '',
280
+ 'Runs the local agentic runtime used by wiki-manager Shell and llm-wiki serve.',
281
+ '',
282
+ 'Defaults:',
283
+ ' --host 0.0.0.0',
284
+ ' --port 7788',
285
+ ' --state-dir .wiki-manager',
286
+ ].join('\n'));
287
+ return;
288
+ }
289
+ const { defaultRuntimeStateDir, openRuntimeStore } = await import('../runtime/store.js');
290
+ const { startRuntimeServer } = await import('../runtime/server.js');
291
+ const { emitRuntimeLog, startActivitySupervisor } = await import('../runtime/supervisor.js');
292
+ const { resolveRuntimeAuthToken } = await import('../runtime/auth.js');
293
+ const { createSqliteQueueStore } = await import('../runtime/queueStore.js');
294
+ const { runRuntimeAgenticLoop } = await import('../runtime/runner.js');
295
+
296
+ const host = valueAfter(argv, '--host') ?? process.env.WIKI_MANAGER_RUNTIME_HOST ?? '0.0.0.0';
297
+ const port = Number(valueAfter(argv, '--port') ?? process.env.WIKI_MANAGER_RUNTIME_PORT ?? 7788);
298
+ const stateDir = valueAfter(argv, '--state-dir') ?? defaultRuntimeStateDir();
299
+ const auth = resolveRuntimeAuthToken({ host, stateDir });
300
+ if (auth.token) process.env.WIKI_MANAGER_RUNTIME_TOKEN = auth.token;
301
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
302
+ throw new Error(`Invalid runtime port: ${port}`);
303
+ }
304
+
305
+ const session = createSession();
306
+ session.headless = true;
307
+ session.chatMode = false;
308
+ session.packageJson = packageJson;
309
+
310
+ const store = openRuntimeStore({ stateDir });
311
+ store.hydrateSession(session);
312
+ session.queueStore = createSqliteQueueStore(store, session);
313
+
314
+ let serverHandle = null;
315
+ session._onAgentEvent = (event) => {
316
+ store.persistEvent(event);
317
+ serverHandle?.publish(event);
318
+ };
319
+ session._onRuntimeError = (err) => {
320
+ const message = err instanceof Error ? err.message : String(err);
321
+ dispatchAgentEvent(session, createAgentEvent('run_error', {
322
+ origin: 'runtime',
323
+ payload: { message },
324
+ }));
325
+ };
326
+
327
+ let supervisor = null;
328
+
329
+ async function executeRun(body, { signal } = {}) {
330
+ const input = String(body.input ?? body.prompt ?? '').trim();
331
+ const workspace = body.workspace ? String(body.workspace).trim() : null;
332
+ const timeoutMs = (Number.isFinite(Number(body.timeout)) ? Math.max(1, Number(body.timeout)) : 3600) * 1000;
333
+ const maxTurns = Number.isFinite(Number(body.maxTurns)) ? Math.max(1, Number(body.maxTurns)) : 20;
334
+ const runId = randomUUID();
335
+ try {
336
+ if (workspace && session.workspace !== workspace) {
337
+ const result = await handleSlashCommand(`/use ${workspace}`, { packageJson, session });
338
+ if (!session.workspacePath) throw new Error(result.output || `Workspace not loaded: ${workspace}`);
339
+ }
340
+ dispatchAgentEvent(session, createAgentEvent('run_started', {
341
+ origin: 'runtime',
342
+ runId,
343
+ payload: { input, workspace },
344
+ }));
345
+ dispatchAgentEvent(session, createAgentEvent('user_message', {
346
+ origin: 'user',
347
+ runId,
348
+ payload: { content: input },
349
+ }));
350
+ session._abortSignal = signal ?? null;
351
+ supervisor?.setRunSignal(signal);
352
+ session._onStep = (message) => emitRuntimeLog(session, message);
353
+ const result = await runRuntimeAgenticLoop(agent, session, input, {
354
+ signal,
355
+ timeoutMs,
356
+ maxTurns,
357
+ runId,
358
+ pollBusy: supervisor?.pollBusy,
359
+ });
360
+ if (!result.ok) {
361
+ dispatchAgentEvent(session, createAgentEvent('run_error', {
362
+ origin: 'runtime',
363
+ runId,
364
+ payload: {
365
+ runId,
366
+ message: result.timedOut
367
+ ? 'Runtime agentic loop timed out.'
368
+ : result.maxTurns
369
+ ? `Runtime agentic loop reached max turns (${maxTurns}).`
370
+ : 'Runtime agentic loop failed.',
371
+ },
372
+ }));
373
+ return;
374
+ }
375
+ dispatchAgentEvent(session, createAgentEvent('run_done', {
376
+ origin: 'runtime',
377
+ runId,
378
+ payload: { runId },
379
+ }));
380
+ } catch (err) {
381
+ if (err?.name === 'AbortError') {
382
+ dispatchAgentEvent(session, createAgentEvent('run_cancelled', {
383
+ origin: 'runtime',
384
+ runId,
385
+ payload: {
386
+ runId,
387
+ message: 'Agent run cancelled.',
388
+ },
389
+ }));
390
+ return;
391
+ }
392
+ dispatchAgentEvent(session, createAgentEvent('run_error', {
393
+ origin: 'runtime',
394
+ runId,
395
+ payload: {
396
+ runId,
397
+ message: err instanceof Error ? err.message : String(err),
398
+ },
399
+ }));
400
+ } finally {
401
+ supervisor?.setRunSignal(null);
402
+ delete session._abortSignal;
403
+ delete session._onStep;
404
+ }
405
+ }
406
+
407
+ serverHandle = await startRuntimeServer({
408
+ host,
409
+ port,
410
+ store,
411
+ session,
412
+ run: executeRun,
413
+ cancel: () => emitRuntimeLog(session, 'runtime: cancel requested'),
414
+ token: auth.token,
415
+ });
416
+ supervisor = startActivitySupervisor(session);
417
+
418
+ console.log(`wiki-manager runtime listening on http://${host}:${port}`);
419
+ console.log(`runtime state: ${store.dbPath}`);
420
+ if (auth.tokenPath) console.log(`runtime token: ${auth.tokenPath}`);
421
+
422
+ const shutdown = async () => {
423
+ supervisor?.stop();
424
+ await serverHandle.close();
425
+ store.close();
426
+ process.exit(0);
427
+ };
428
+ process.once('SIGINT', shutdown);
429
+ process.once('SIGTERM', shutdown);
430
+ await new Promise(() => {});
431
+ }
432
+
348
433
  export async function runCli(argv) {
434
+ if (argv[0] === 'runtime') {
435
+ const agent = createAgentGraph();
436
+ await runRuntime(argv.slice(1), agent);
437
+ return;
438
+ }
439
+
349
440
  if (argv.includes('--setup-wizard')) {
350
441
  if (!process.versions.bun) {
351
442
  throw new Error('Setup wizard requires Bun. Run: bun ./bin/wiki-manager.js --setup-wizard');
@@ -396,9 +487,25 @@ export async function runCli(argv) {
396
487
  const { runOpenTuiShell, runStartupWizard } = await import('../shell/tui.tsx');
397
488
  const gaps = await runChecks();
398
489
  if (gaps.length > 0) await runStartupWizard(gaps);
399
- await runOpenTuiShell({ agent, packageJson });
490
+ let runtime = null;
491
+ try {
492
+ const { ensureRuntime } = await import('../runtime/lifecycle.js');
493
+ runtime = await ensureRuntime();
494
+ } catch (err) {
495
+ console.error(`Runtime unavailable: ${err instanceof Error ? err.message : String(err)}`);
496
+ }
497
+ await runOpenTuiShell({ agent, packageJson, runtime });
400
498
  return;
401
499
  }
402
500
 
403
- await runShell({ agent, packageJson });
501
+ let runtime = null;
502
+ if (process.stdin.isTTY && process.stdout.isTTY) {
503
+ try {
504
+ const { ensureRuntime } = await import('../runtime/lifecycle.js');
505
+ runtime = await ensureRuntime();
506
+ } catch (err) {
507
+ console.error(`Runtime unavailable: ${err instanceof Error ? err.message : String(err)}`);
508
+ }
509
+ }
510
+ await runShell({ agent, packageJson, runtime });
404
511
  }
@@ -222,6 +222,20 @@ export function formatActivitySummary(source, action, resultText) {
222
222
  return usefulLine ? `${source}.${action}: ${usefulLine.slice(0, 120)}` : null;
223
223
  }
224
224
 
225
+ export function activitySnapshot(session) {
226
+ return new Set(sessionActivities(session).map((a) => a.key));
227
+ }
228
+
229
+ export function newNonTerminalActivities(snapshotBefore, session) {
230
+ return sessionActivities(session).filter((a) => !snapshotBefore.has(a.key) && !a.terminal);
231
+ }
232
+
233
+ export function terminalFailures(activities) {
234
+ return activities.filter(
235
+ (a) => a.terminal && ['failed', 'error', 'cancelled', 'canceled'].includes(String(a.status).toLowerCase()),
236
+ );
237
+ }
238
+
225
239
  export function formatActivityError(source, action, err) {
226
240
  const message = err instanceof Error ? err.message : String(err);
227
241
  const lines = message
@@ -7,6 +7,18 @@ const SESSION_PROJECTION_EVENTS = new Set([
7
7
  'plan_step_updated',
8
8
  'activity_upserted',
9
9
  'run_error',
10
+ 'run_cancelled',
11
+ 'runtime_log',
12
+ ]);
13
+
14
+ // Events that can mutate state.plan in applyEvent() — only these warrant the
15
+ // before/after plan comparison below (runtime_log fires far more often and
16
+ // never touches the plan).
17
+ const PLAN_MUTATING_EVENTS = new Set([
18
+ 'run_started',
19
+ 'plan_set',
20
+ 'plan_step_updated',
21
+ 'activity_upserted',
10
22
  ]);
11
23
 
12
24
  export function createAgentEvent(type, { origin = 'system', payload = {}, runId = null, turnId = null } = {}) {
@@ -23,7 +35,8 @@ export function createAgentEvent(type, { origin = 'system', payload = {}, runId
23
35
 
24
36
  export function dispatchAgentEvent(session, event) {
25
37
  const normalized = event.id && event.ts ? event : createAgentEvent(event.type, event);
26
- const previousPlan = JSON.stringify(session.headlessPlan ?? null);
38
+ const tracksPlan = PLAN_MUTATING_EVENTS.has(normalized.type);
39
+ const previousPlan = tracksPlan ? JSON.stringify(session.headlessPlan ?? null) : null;
27
40
  session.agentEvents ??= [];
28
41
  session.agentEvents.push(normalized);
29
42
  session._agentProjectionState ??= createProjectionState();
@@ -31,10 +44,11 @@ export function dispatchAgentEvent(session, event) {
31
44
  session.agentProjection = publicProjection(session._agentProjectionState);
32
45
  if (SESSION_PROJECTION_EVENTS.has(normalized.type)) {
33
46
  applyAgentProjectionToSession(session, session.agentProjection);
34
- if (JSON.stringify(session.headlessPlan ?? null) !== previousPlan) {
47
+ if (tracksPlan && JSON.stringify(session.headlessPlan ?? null) !== previousPlan) {
35
48
  session._onPlanUpdate?.();
36
49
  }
37
50
  }
51
+ session._onAgentEvent?.(normalized, session.agentProjection);
38
52
  return normalized;
39
53
  }
40
54
 
@@ -133,10 +147,18 @@ function applyEvent(state, event) {
133
147
  case 'run_done':
134
148
  state.status = 'done';
135
149
  return;
150
+ case 'run_cancelled':
151
+ state.status = 'cancelled';
152
+ state.logs.push(String(event.payload?.message ?? 'Agent run cancelled.'));
153
+ return;
136
154
  case 'run_error':
137
155
  state.status = 'error';
138
156
  state.logs.push(String(event.payload?.message ?? 'Agent run failed.'));
139
157
  return;
158
+ case 'runtime_log':
159
+ state.logs.push(String(event.payload?.message ?? ''));
160
+ state.logs = state.logs.slice(-200);
161
+ return;
140
162
  default:
141
163
  return;
142
164
  }