@alisio/core 0.1.0-alpha.8 → 0.1.0

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.
Files changed (189) hide show
  1. package/README.md +52 -7
  2. package/dist/agents/active.d.ts +99 -0
  3. package/dist/agents/active.js +218 -0
  4. package/dist/agents/capabilities.d.ts +26 -0
  5. package/dist/agents/capabilities.js +120 -0
  6. package/dist/agents/create-agent-skill.d.ts +11 -0
  7. package/dist/agents/create-agent-skill.js +60 -0
  8. package/dist/agents/definitions.d.ts +88 -0
  9. package/dist/agents/definitions.js +404 -0
  10. package/dist/agents/draft.d.ts +41 -0
  11. package/dist/agents/draft.js +133 -0
  12. package/dist/agents/service.d.ts +57 -0
  13. package/dist/agents/service.js +86 -0
  14. package/dist/agents/templates.d.ts +8 -0
  15. package/dist/agents/templates.js +102 -0
  16. package/dist/analysis/capabilities.d.ts +48 -0
  17. package/dist/analysis/capabilities.js +90 -0
  18. package/dist/analysis/data/csv.d.ts +46 -0
  19. package/dist/analysis/data/csv.js +240 -0
  20. package/dist/analysis/data/datasets.d.ts +132 -0
  21. package/dist/analysis/data/datasets.js +441 -0
  22. package/dist/analysis/data/describe.d.ts +27 -0
  23. package/dist/analysis/data/describe.js +103 -0
  24. package/dist/analysis/data/engine-client.d.ts +58 -0
  25. package/dist/analysis/data/engine-client.js +304 -0
  26. package/dist/analysis/data/engine-entry.d.ts +1 -0
  27. package/dist/analysis/data/engine-entry.js +59 -0
  28. package/dist/analysis/data/engine-source.d.ts +4 -0
  29. package/dist/analysis/data/engine-source.js +6 -0
  30. package/dist/analysis/data/engine-types.d.ts +86 -0
  31. package/dist/analysis/data/engine-types.js +7 -0
  32. package/dist/analysis/data/infer.d.ts +42 -0
  33. package/dist/analysis/data/infer.js +118 -0
  34. package/dist/analysis/data/ingest-worker.d.ts +17 -0
  35. package/dist/analysis/data/ingest-worker.js +365 -0
  36. package/dist/analysis/data/json.d.ts +19 -0
  37. package/dist/analysis/data/json.js +44 -0
  38. package/dist/analysis/data/query-worker.d.ts +12 -0
  39. package/dist/analysis/data/query-worker.js +75 -0
  40. package/dist/analysis/data/rows.d.ts +39 -0
  41. package/dist/analysis/data/rows.js +120 -0
  42. package/dist/analysis/data/sql-guard.d.ts +20 -0
  43. package/dist/analysis/data/sql-guard.js +129 -0
  44. package/dist/analysis/data/stats.d.ts +18 -0
  45. package/dist/analysis/data/stats.js +77 -0
  46. package/dist/analysis/install-hints.d.ts +33 -0
  47. package/dist/analysis/install-hints.js +144 -0
  48. package/dist/analysis/janitor.d.ts +60 -0
  49. package/dist/analysis/janitor.js +264 -0
  50. package/dist/analysis/jobs.d.ts +94 -0
  51. package/dist/analysis/jobs.js +165 -0
  52. package/dist/analysis/oci.d.ts +129 -0
  53. package/dist/analysis/oci.js +330 -0
  54. package/dist/analysis/python/sources.d.ts +2 -0
  55. package/dist/analysis/python/sources.js +10 -0
  56. package/dist/analysis/requirements.d.ts +32 -0
  57. package/dist/analysis/requirements.js +73 -0
  58. package/dist/analysis/rerun.d.ts +40 -0
  59. package/dist/analysis/rerun.js +136 -0
  60. package/dist/analysis/runtime-manager.d.ts +146 -0
  61. package/dist/analysis/runtime-manager.js +472 -0
  62. package/dist/analysis/status.d.ts +21 -0
  63. package/dist/analysis/status.js +52 -0
  64. package/dist/application.d.ts +118 -3
  65. package/dist/application.js +510 -19
  66. package/dist/artifacts/kinds.d.ts +35 -0
  67. package/dist/artifacts/kinds.js +177 -0
  68. package/dist/artifacts/publisher.d.ts +7 -0
  69. package/dist/artifacts/publisher.js +29 -0
  70. package/dist/artifacts/store.d.ts +119 -0
  71. package/dist/artifacts/store.js +581 -0
  72. package/dist/background/janitor.d.ts +22 -0
  73. package/dist/background/janitor.js +62 -0
  74. package/dist/background/mirror.d.ts +14 -0
  75. package/dist/background/mirror.js +41 -0
  76. package/dist/background/notify.d.ts +58 -0
  77. package/dist/background/notify.js +172 -0
  78. package/dist/background/output.d.ts +46 -0
  79. package/dist/background/output.js +179 -0
  80. package/dist/background/service.d.ts +137 -0
  81. package/dist/background/service.js +410 -0
  82. package/dist/background/store.d.ts +103 -0
  83. package/dist/background/store.js +195 -0
  84. package/dist/changelog/data.d.ts +6 -0
  85. package/dist/changelog/data.js +293 -0
  86. package/dist/changelog/index.d.ts +5 -0
  87. package/dist/changelog/index.js +7 -0
  88. package/dist/changelog/parse.d.ts +53 -0
  89. package/dist/changelog/parse.js +160 -0
  90. package/dist/commands/builtins.d.ts +10 -0
  91. package/dist/commands/builtins.js +161 -0
  92. package/dist/commands/catalog.d.ts +86 -0
  93. package/dist/commands/catalog.js +304 -0
  94. package/dist/config.d.ts +252 -5
  95. package/dist/config.js +514 -13
  96. package/dist/core/compaction.js +13 -2
  97. package/dist/core/contracts.d.ts +180 -3
  98. package/dist/core/opt-in.d.ts +12 -0
  99. package/dist/core/opt-in.js +12 -0
  100. package/dist/core/output-limit.d.ts +34 -0
  101. package/dist/core/output-limit.js +39 -0
  102. package/dist/core/run-stats.d.ts +16 -0
  103. package/dist/core/run-stats.js +8 -0
  104. package/dist/core/runner.d.ts +152 -4
  105. package/dist/core/runner.js +792 -138
  106. package/dist/core/timeout.d.ts +15 -0
  107. package/dist/core/timeout.js +61 -0
  108. package/dist/core/truncation.d.ts +30 -0
  109. package/dist/core/truncation.js +83 -0
  110. package/dist/goal/blockers.d.ts +45 -0
  111. package/dist/goal/blockers.js +51 -0
  112. package/dist/goal/budget.d.ts +24 -0
  113. package/dist/goal/budget.js +53 -0
  114. package/dist/goal/command.d.ts +36 -0
  115. package/dist/goal/command.js +98 -0
  116. package/dist/goal/format.d.ts +19 -0
  117. package/dist/goal/format.js +102 -0
  118. package/dist/goal/host.d.ts +29 -0
  119. package/dist/goal/host.js +42 -0
  120. package/dist/goal/index.d.ts +9 -0
  121. package/dist/goal/index.js +9 -0
  122. package/dist/goal/machine.d.ts +186 -0
  123. package/dist/goal/machine.js +344 -0
  124. package/dist/goal/prompts.d.ts +10 -0
  125. package/dist/goal/prompts.js +75 -0
  126. package/dist/goal/service.d.ts +113 -0
  127. package/dist/goal/service.js +273 -0
  128. package/dist/goal/store.d.ts +59 -0
  129. package/dist/goal/store.js +184 -0
  130. package/dist/index.d.ts +51 -6
  131. package/dist/index.js +49 -4
  132. package/dist/mcp/connector.js +38 -6
  133. package/dist/mcp/rich.d.ts +41 -0
  134. package/dist/mcp/rich.js +362 -0
  135. package/dist/permissions/modes.d.ts +72 -0
  136. package/dist/permissions/modes.js +98 -0
  137. package/dist/plan/exit-plan.d.ts +25 -0
  138. package/dist/plan/exit-plan.js +130 -0
  139. package/dist/plan/state.d.ts +102 -0
  140. package/dist/plan/state.js +165 -0
  141. package/dist/plugins/host.d.ts +39 -1
  142. package/dist/plugins/host.js +150 -9
  143. package/dist/providers/settings.d.ts +15 -0
  144. package/dist/providers/settings.js +38 -0
  145. package/dist/reload.d.ts +75 -0
  146. package/dist/reload.js +204 -0
  147. package/dist/resources/context.d.ts +5 -0
  148. package/dist/resources/context.js +13 -1
  149. package/dist/runtime/access.d.ts +62 -0
  150. package/dist/runtime/access.js +115 -0
  151. package/dist/runtime/blobs.d.ts +51 -0
  152. package/dist/runtime/blobs.js +177 -0
  153. package/dist/runtime/diff.d.ts +23 -0
  154. package/dist/runtime/diff.js +154 -0
  155. package/dist/runtime/ids.d.ts +8 -0
  156. package/dist/runtime/ids.js +32 -0
  157. package/dist/runtime/paths.d.ts +11 -0
  158. package/dist/runtime/paths.js +17 -1
  159. package/dist/runtime/process.d.ts +22 -0
  160. package/dist/runtime/process.js +100 -15
  161. package/dist/runtime/sqlite.d.ts +2 -0
  162. package/dist/runtime/sqlite.js +18 -0
  163. package/dist/runtime/store.d.ts +91 -3
  164. package/dist/runtime/store.js +465 -11
  165. package/dist/runtime/zip.d.ts +24 -0
  166. package/dist/runtime/zip.js +126 -0
  167. package/dist/sessions/children.js +17 -1
  168. package/dist/sessions/side-questions.d.ts +57 -0
  169. package/dist/sessions/side-questions.js +161 -0
  170. package/dist/startup/screen.js +6 -5
  171. package/dist/tools/analysis.d.ts +37 -0
  172. package/dist/tools/analysis.js +573 -0
  173. package/dist/tools/artifacts.d.ts +14 -0
  174. package/dist/tools/artifacts.js +143 -0
  175. package/dist/tools/background.d.ts +7 -0
  176. package/dist/tools/background.js +165 -0
  177. package/dist/tools/data.d.ts +7 -0
  178. package/dist/tools/data.js +160 -0
  179. package/dist/tools/execute.d.ts +5 -0
  180. package/dist/tools/execute.js +15 -3
  181. package/dist/tools/goal.d.ts +19 -0
  182. package/dist/tools/goal.js +126 -0
  183. package/dist/tools/search.d.ts +17 -3
  184. package/dist/tools/search.js +153 -0
  185. package/dist/tools/standard.d.ts +4 -1
  186. package/dist/tools/standard.js +99 -18
  187. package/dist/version.d.ts +5 -3
  188. package/dist/version.js +35 -6
  189. package/package.json +2 -2
@@ -1,9 +1,26 @@
1
+ import { realpath } from "node:fs/promises";
1
2
  import { homedir } from "node:os";
2
3
  import { dirname, join, resolve } from "node:path";
3
- import { configFile, configHome, loadConfigWithProvenance, overridesSavedProviderProfile, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, stateHome, } from "./config.js";
4
+ import { AgentDefinitionService } from "./agents/service.js";
5
+ import { CapabilityGrants } from "./analysis/capabilities.js";
6
+ import { DatasetService } from "./analysis/data/datasets.js";
7
+ import { AnalysisJanitor } from "./analysis/janitor.js";
8
+ import { AnalysisJobs } from "./analysis/jobs.js";
9
+ import { OciRuntime } from "./analysis/oci.js";
10
+ import { AnalysisRerun } from "./analysis/rerun.js";
11
+ import { AnalysisRuntimeManager } from "./analysis/runtime-manager.js";
12
+ import { createArtifactPublisher } from "./artifacts/publisher.js";
13
+ import { ArtifactStore } from "./artifacts/store.js";
14
+ import { TaskJanitor } from "./background/janitor.js";
15
+ import { subagentTasks } from "./background/mirror.js";
16
+ import { BackgroundTasks } from "./background/service.js";
17
+ import { configFile, configHome, loadConfigWithProvenance, overridesSavedProviderProfile, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, stateHome, } from "./config.js";
4
18
  import { completeText } from "./core/compaction.js";
19
+ import { DEFAULT_MAX_OUTPUT_TOKENS } from "./core/output-limit.js";
5
20
  import { ToolRegistry } from "./core/registry.js";
6
21
  import { AgentRunner } from "./core/runner.js";
22
+ import { GoalService } from "./goal/service.js";
23
+ import { GoalStore } from "./goal/store.js";
7
24
  import { HerdrBridge } from "./integrations/herdr.js";
8
25
  import { McpConnector } from "./mcp/connector.js";
9
26
  import { discoverPlugins, PluginHost } from "./plugins/host.js";
@@ -13,10 +30,21 @@ import { ProviderSettingsStore } from "./providers/settings.js";
13
30
  import { ProjectContext } from "./resources/context.js";
14
31
  import { expandSlashPrompt, loadPromptTemplates, promptSources } from "./resources/prompts.js";
15
32
  import { Skills, skillRoots } from "./resources/skills.js";
33
+ import { PathAccess } from "./runtime/access.js";
34
+ import { BlobStore } from "./runtime/blobs.js";
16
35
  import { isPathSpec } from "./runtime/modules.js";
17
36
  import { findWorkspace } from "./runtime/paths.js";
18
37
  import { SQLiteStore } from "./runtime/store.js";
19
38
  import { ChildSessions } from "./sessions/children.js";
39
+ import { SideQuestions } from "./sessions/side-questions.js";
40
+ import { registerGoalTools } from "./tools/goal.js";
41
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
42
+ /**
43
+ * Cap per teardown stage of `app.close()`: exit must never wait longer than this on a hanging
44
+ * component (MCP server, provider, plugin). Stages run in parallel where independent and
45
+ * failures are swallowed, so teardown errors never replace the caller's result.
46
+ */
47
+ const TEARDOWN_STAGE_TIMEOUT_MS = 2_500;
20
48
  /** A built-in runs unless its config sets `enabled: false` or it is disabled by id. */
21
49
  export function enabledBuiltins(available, config, disabled = []) {
22
50
  for (const id of [...Object.keys(config), ...disabled])
@@ -52,7 +80,28 @@ export async function createApplication(options = {}) {
52
80
  // a project value (see loadConfigWithProvenance), so a project cannot grant itself MCP consent.
53
81
  const mcpAllow = config.mcp.allow === true;
54
82
  let persistedMcpAllow = mcpAllow;
83
+ // Extra tool roots: config `additionalDirectories` (already canonicalized on load) plus per-run
84
+ // `--add-dir` (resolved against the current directory). Ignored entirely under --read-only, so
85
+ // the flag and the config key can never widen a locked session.
86
+ const addDirs = options.readOnly
87
+ ? []
88
+ : await Promise.all((options.addDirs ?? []).map((dir) => realpath(resolve(cwd, dir)).catch(() => resolve(cwd, dir))));
89
+ const pathAccess = new PathAccess({
90
+ workspace,
91
+ extraRoots: [...config.additionalDirectories, ...addDirs],
92
+ ...(options.approveExternalDirectory && !options.readOnly
93
+ ? { approve: options.approveExternalDirectory }
94
+ : {}),
95
+ readOnly: !!options.readOnly,
96
+ });
55
97
  const store = new SQLiteStore(options.db ?? join(stateHome(), "sessions.sqlite"));
98
+ // Content-addressed attachment bytes next to the session database (no I/O until `put`).
99
+ const blobs = new BlobStore({
100
+ root: options.db && options.db !== ":memory:"
101
+ ? join(dirname(options.db), "blobs")
102
+ : join(stateHome(), "blobs"),
103
+ db: store.db,
104
+ });
56
105
  const registry = new ToolRegistry(), skills = new Skills({ overrides: config.skillOverrides }), context = new ProjectContext(workspace, {
57
106
  globalDir: configHome(),
58
107
  cwd,
@@ -234,7 +283,150 @@ export async function createApplication(options = {}) {
234
283
  }, {
235
284
  config: config.websearch,
236
285
  resolveExtension: () => plugins.extensions.resolve("websearch"),
286
+ }, pathAccess);
287
+ // Python analysis and artifacts live next to the session database (like attachment blobs).
288
+ const stateRoot = options.db && options.db !== ":memory:" ? dirname(options.db) : stateHome();
289
+ const artifacts = new ArtifactStore({
290
+ root: stateRoot,
291
+ db: store.db,
292
+ limits: {
293
+ maxFiles: config.analysis.limits.maxFiles,
294
+ maxFileBytes: config.analysis.limits.maxFileBytes,
295
+ maxOutputBytes: config.analysis.limits.maxOutputBytes,
296
+ },
297
+ });
298
+ const capabilityGrants = new CapabilityGrants({
299
+ db: store.db,
300
+ rootOf: (id) => store.rootOf(id),
301
+ });
302
+ const analysisRuntime = options.analysisRuntime ??
303
+ new AnalysisRuntimeManager({
304
+ stateDir: stateRoot,
305
+ ...(options.python ? { python: options.python } : {}),
306
+ });
307
+ const analysisJobs = new AnalysisJobs({ root: stateRoot, db: store.db });
308
+ // Container mode (`analysis.runtime: "oci"`): read from the live config on every call.
309
+ let ociRuntime;
310
+ const oci = {
311
+ mode: () => config.analysis.runtime,
312
+ runtime: () => {
313
+ const key = JSON.stringify(config.analysis.oci);
314
+ if (ociRuntime?.key !== key)
315
+ ociRuntime = {
316
+ key,
317
+ runtime: new OciRuntime({ ...config.analysis.oci }, options.analysisOci),
318
+ };
319
+ return ociRuntime.runtime;
320
+ },
321
+ };
322
+ // Tabular datasets (one SQLite file each); XLSX needs the discovered Python.
323
+ const datasets = new DatasetService({
324
+ root: stateRoot,
325
+ db: store.db,
326
+ limits: config.analysis.data,
327
+ python: analysisRuntime,
328
+ });
329
+ const analysisRerun = new AnalysisRerun({ jobs: analysisJobs, store: artifacts, datasets });
330
+ // Retention: a sweep at most every 24 h, launched in the background after the start.
331
+ const janitor = new AnalysisJanitor({
332
+ root: stateRoot,
333
+ db: store.db,
334
+ artifacts,
335
+ datasets,
336
+ blobs,
337
+ retention: () => config.analysis.retention,
338
+ ...(options.analysisSweepDelayMs !== undefined
339
+ ? { initialDelayMs: options.analysisSweepDelayMs }
340
+ : {}),
341
+ });
342
+ // Background tasks: a service over the shared database (state change = compare-and-set), logs
343
+ // under `<state>/tasks/`. Tasks of a dead owner process (a previous run) become `lost` now.
344
+ const tasks = new BackgroundTasks({
345
+ db: store.db,
346
+ stateRoot,
347
+ rootOf: (id) => store.rootOf(id),
348
+ limits: () => config.tasks,
349
+ readOnly: !!options.readOnly,
350
+ ...(options.tasks?.wake ? { wake: options.tasks.wake } : {}),
351
+ ...(options.tasks?.onChange ? { onChange: options.tasks.onChange } : {}),
352
+ ...(options.tasks?.timing ? { timing: options.tasks.timing } : {}),
353
+ ...(options.tasks?.maxPerProcess ? { maxPerProcess: options.tasks.maxPerProcess } : {}),
354
+ ...(options.tasks?.killGraceMs ? { killGraceMs: options.tasks.killGraceMs } : {}),
355
+ mirror: (root) => subagentTasks(plugins.panels, root),
356
+ onError: (error) => process.stderr.write(`Background task error: ${error instanceof Error ? error.message : String(error)}\n`),
357
+ });
358
+ tasks.recover();
359
+ const taskJanitor = new TaskJanitor({
360
+ store: tasks.store,
361
+ stateRoot,
362
+ retentionDays: () => config.tasks.retentionDays,
363
+ ...(options.tasks?.sweepDelayMs !== undefined
364
+ ? { initialDelayMs: options.tasks.sweepDelayMs }
365
+ : {}),
237
366
  });
367
+ if (config.tasks.enabled) {
368
+ const { registerBackgroundTools } = await import("./tools/background.js");
369
+ registerBackgroundTools(registry, { tasks, rootOf: (id) => store.rootOf(id) });
370
+ }
371
+ // Session goals (`/goal`): one service over the shared database. A goal left `active` by a
372
+ // process that is gone pauses with reason `restart` now (never auto-resumed). The two goal
373
+ // tools are opt-in: a run only sees them while its session has an active goal.
374
+ const goals = new GoalService({
375
+ store: new GoalStore(store.db),
376
+ settings: () => config.goal,
377
+ ...(options.goals?.onChange ? { onChange: options.goals.onChange } : {}),
378
+ ...(options.goals?.onLog ? { onLog: options.goals.onLog } : {}),
379
+ });
380
+ if (options.goals?.recover !== false)
381
+ goals.recover();
382
+ registerGoalTools(registry, { goals });
383
+ // R4: nothing is registered with analysis disabled or under --read-only. Startup only looks
384
+ // for candidate interpreters with `stat`; no process is launched until the first call.
385
+ const analysisEnabled = config.analysis.enabled && !options.readOnly;
386
+ if (analysisEnabled) {
387
+ const [candidates, active] = await Promise.all([
388
+ analysisRuntime.candidates(),
389
+ analysisRuntime.activeRuntime(),
390
+ ]);
391
+ const { registerAnalysisTools } = await import("./tools/analysis.js");
392
+ registerAnalysisTools(registry, {
393
+ store: artifacts,
394
+ jobs: analysisJobs,
395
+ runtime: analysisRuntime,
396
+ datasets,
397
+ oci,
398
+ rerun: analysisRerun,
399
+ rootOf: (id) => store.rootOf(id),
400
+ // Read live: a changed `analysis.limits.*` setting applies to the next call.
401
+ limits: {
402
+ get timeoutMs() {
403
+ return config.analysis.limits.timeoutMs;
404
+ },
405
+ get maxLogBytes() {
406
+ return config.analysis.limits.maxLogBytes;
407
+ },
408
+ },
409
+ startup: { candidates: candidates.length > 0, extras: active?.extras ?? [] },
410
+ });
411
+ const { registerArtifactTools } = await import("./tools/artifacts.js");
412
+ registerArtifactTools(registry, { store: artifacts, rootOf: (id) => store.rootOf(id) });
413
+ }
414
+ // `exit_plan` (opt-in: only the plan agent's run sees it): the plan review hand-over.
415
+ const { registerExitPlan } = await import("./plan/exit-plan.js");
416
+ registerExitPlan(registry, { store, ui: plugins.ui, readOnly: !!options.readOnly });
417
+ // Data tools only read (effect `read`): they stay available under --read-only.
418
+ if (config.analysis.enabled) {
419
+ const { registerDataTools } = await import("./tools/data.js");
420
+ registerDataTools(registry, { datasets, rootOf: (id) => store.rootOf(id) });
421
+ }
422
+ const sessionWorkspace = (sessionId) => {
423
+ try {
424
+ return store.get(sessionId).workspace;
425
+ }
426
+ catch {
427
+ return workspace;
428
+ }
429
+ };
238
430
  const { registerPluginInstallTool } = await import("./plugins/install.js");
239
431
  registerPluginInstallTool(registry, { readOnly: !!options.readOnly });
240
432
  if ((options.allowMcp || mcpAllow) && !options.readOnly)
@@ -475,6 +667,7 @@ export async function createApplication(options = {}) {
475
667
  const runtimePolicy = {
476
668
  write: !!options.allowWrite && !options.readOnly,
477
669
  process: !!options.allowProcess && !options.readOnly,
670
+ ...(options.allowAnalysis && !options.readOnly ? { analysis: true } : {}),
478
671
  external: !options.readOnly &&
479
672
  (!!options.allowExternal ||
480
673
  !!options.allowMcp ||
@@ -483,6 +676,52 @@ export async function createApplication(options = {}) {
483
676
  plugins.externalCount > 0 ||
484
677
  registry.list().some((t) => t.name.startsWith("p_"))),
485
678
  };
679
+ /** Model and provider of a session, for the provenance of its artifacts. */
680
+ const provenanceOf = (sessionId) => {
681
+ try {
682
+ const session = store.get(sessionId);
683
+ return {
684
+ ...(session.model ? { model: session.model } : {}),
685
+ ...(session.provider ? { provider: session.provider } : {}),
686
+ };
687
+ }
688
+ catch {
689
+ return {};
690
+ }
691
+ };
692
+ /**
693
+ * The maximum output a model's catalog declares (`ModelInfo.maxOutputTokens`), for the budget
694
+ * of each request. Looks in the active provider's catalog first (loading it once, bounded, if
695
+ * the first request comes before it); then in the catalogs of the configured profiles that
696
+ * were already listed, taking the smallest declaration when several providers list the same
697
+ * model id so a derived budget never exceeds any of them. Unknown stays unknown.
698
+ */
699
+ const declaredOutputLimit = async (model) => {
700
+ if (!models.has(model)) {
701
+ const loading = ensureModels();
702
+ if (loading) {
703
+ let timer;
704
+ await Promise.race([
705
+ loading.catch(() => undefined),
706
+ new Promise((resolve) => {
707
+ timer = setTimeout(resolve, 2_000);
708
+ }),
709
+ ]);
710
+ if (timer)
711
+ clearTimeout(timer);
712
+ }
713
+ }
714
+ const own = models.get(model)?.maxOutputTokens;
715
+ if (own !== undefined)
716
+ return own;
717
+ const declared = catalogCache
718
+ ? availableProviderModels(catalogCache.value)
719
+ .filter((entry) => entry.model.id === model)
720
+ .map((entry) => entry.model.maxOutputTokens)
721
+ .filter((value) => typeof value === "number" && value > 0)
722
+ : [];
723
+ return declared.length ? Math.min(...declared) : undefined;
724
+ };
486
725
  const runner = new AgentRunner({
487
726
  provider,
488
727
  providerFor,
@@ -495,7 +734,43 @@ export async function createApplication(options = {}) {
495
734
  compaction: config.compaction,
496
735
  extensions: plugins,
497
736
  contextWindow,
737
+ modelOutputLimit: declaredOutputLimit,
738
+ fallbackMaxOutputTokens: DEFAULT_MAX_OUTPUT_TOKENS,
739
+ effortLevels: (model) => {
740
+ const known = models.get(model)?.effort;
741
+ if (!known)
742
+ void ensureModels();
743
+ return known;
744
+ },
498
745
  ...(options.approve && !options.readOnly ? { approve: options.approve } : {}),
746
+ pathAccess,
747
+ ...(analysisEnabled
748
+ ? {
749
+ capabilities: {
750
+ granted: (capability, sessionId) => capabilityGrants.granted(capability, sessionId),
751
+ record: (input) => {
752
+ capabilityGrants.record({ ...input, workspace: sessionWorkspace(input.sessionId) });
753
+ },
754
+ source: options.approvalSource ?? "tui",
755
+ get runtime() {
756
+ return config.analysis.runtime;
757
+ },
758
+ // A rerun's approval shows the saved script (the input carries only its reference).
759
+ preview: async (input, sessionId) => typeof input.rerunOf === "string"
760
+ ? analysisRerun.script(input.rerunOf, store.rootOf(sessionId))
761
+ : undefined,
762
+ },
763
+ }
764
+ : {}),
765
+ artifacts: (call, announce) => createArtifactPublisher(artifacts, {
766
+ sessionId: call.sessionId,
767
+ rootSessionId: store.rootOf(call.sessionId),
768
+ workspace: sessionWorkspace(call.sessionId),
769
+ runId: call.runId,
770
+ callId: call.callId,
771
+ // Public provenance of what produced the artifact (no paths, no keys).
772
+ provenance: provenanceOf(call.sessionId),
773
+ }, announce),
499
774
  ...(config.websearch.provider === "native"
500
775
  ? { nativeTools: [{ type: config.websearch.nativeToolType }] }
501
776
  : {}),
@@ -508,6 +783,10 @@ export async function createApplication(options = {}) {
508
783
  // Child sessions (generic delegation service for plugins). Contexts are cached per workspace
509
784
  // so a child in a git worktree reads that worktree's AGENTS.md files.
510
785
  const contexts = new Map([[workspace, context]]);
786
+ // Startup reconciliation: runs left queued/running by a dead process become `interrupted`
787
+ // (runs of this process, e.g. another Application in a server, are kept). Child sessions are
788
+ // reconciled by `interruptStale()` in the ChildSessions constructor right below.
789
+ store.interruptRuns();
511
790
  plugins.setSessions(new ChildSessions({
512
791
  store,
513
792
  runner,
@@ -574,12 +853,43 @@ export async function createApplication(options = {}) {
574
853
  }
575
854
  }
576
855
  }
856
+ // Retention runs in the background (unreferenced timer); never for in-memory test databases.
857
+ if (analysisEnabled && options.db !== ":memory:")
858
+ janitor.start();
859
+ // Retention is maintenance that deletes files: never under --read-only, never for `:memory:`.
860
+ if (options.db !== ":memory:" && !options.readOnly)
861
+ taskJanitor.start();
577
862
  return {
578
863
  workspace,
864
+ pathAccess,
579
865
  config,
580
866
  prompts,
581
867
  expandPrompt,
582
868
  store,
869
+ /** Uploaded attachment bytes; resolve a `BlobRef` with `blobs.attachment(ref)` before a run. */
870
+ blobs,
871
+ /** Published artifacts of every session (folders under `<state>/artifacts`). */
872
+ artifacts,
873
+ /** Persisted capability grants (`analysis.run`) and their audit rows. */
874
+ capabilityGrants,
875
+ /** Background tasks (`bg_*` tools, `/tasks` panels): admission, state, output, stop. */
876
+ tasks,
877
+ /** Session goals (`/goal`): state, user actions, the continuation controller. */
878
+ goals,
879
+ /** Python discovery and job folders of `python_run`; `enabled` is false under --read-only. */
880
+ analysis: {
881
+ enabled: analysisEnabled,
882
+ runtime: analysisRuntime,
883
+ jobs: analysisJobs,
884
+ /** Container runtime of the current configuration (status, pull); mode `managed` ignores it. */
885
+ oci,
886
+ rerun: analysisRerun,
887
+ janitor,
888
+ },
889
+ /** Keys a project layer set that only the user layer decides (ignored on load). */
890
+ configDiagnostics: configProvenance.ignored ?? [],
891
+ /** Tabular datasets of every session (ingestion, read-only queries, pages). */
892
+ datasets,
583
893
  registry,
584
894
  context,
585
895
  skills,
@@ -672,12 +982,15 @@ export async function createApplication(options = {}) {
672
982
  mcpStartupFailures() {
673
983
  return [...mcpStartupFailures];
674
984
  },
675
- /** Host-owned interactive consent boundary. Headless callers remain blocked unless flagged. */
985
+ /**
986
+ * Host-owned interactive consent boundary: the TUI and the web UI (`alisio serve`, after an
987
+ * explicit confirmation dialog) may grant. Headless callers remain blocked unless flagged.
988
+ */
676
989
  grantMcpRuntimePermission(request) {
677
990
  if (!request.confirmed)
678
991
  return mcpRuntimePermission;
679
- if (request.source !== "interactive-tui")
680
- throw new Error("MCP runtime permission may only be granted by the interactive TUI");
992
+ if (request.source !== "interactive-tui" && request.source !== "interactive-web")
993
+ throw new Error("MCP runtime permission may only be granted by an interactive UI");
681
994
  if (options.readOnly)
682
995
  throw new Error("MCP is unavailable under --read-only");
683
996
  if (mcpRuntimePermission === "granted")
@@ -688,12 +1001,12 @@ export async function createApplication(options = {}) {
688
1001
  return mcpRuntimePermission;
689
1002
  },
690
1003
  /** Persist global consent atomically, then grant runtime permission for this process. */
691
- async rememberGlobalMcpConsent() {
1004
+ async rememberGlobalMcpConsent(source = "interactive-tui") {
692
1005
  if (options.readOnly)
693
1006
  throw new Error("MCP is unavailable under --read-only");
694
1007
  await setGlobalMcpAllow({ allow: true });
695
1008
  persistedMcpAllow = true;
696
- this.grantMcpRuntimePermission({ source: "interactive-tui", confirmed: true });
1009
+ this.grantMcpRuntimePermission({ source, confirmed: true });
697
1010
  },
698
1011
  /**
699
1012
  * Clear the persisted global consent atomically and drop the runtime permission (disconnecting
@@ -729,6 +1042,139 @@ export async function createApplication(options = {}) {
729
1042
  : undefined);
730
1043
  return mcp.info(id);
731
1044
  },
1045
+ /**
1046
+ * Persist one user-facing setting to the GLOBAL user config (`<config home>/config.json`)
1047
+ * and apply it to this process where the value can change live. Compaction and limits land
1048
+ * in the runner (honored by the next run/compact), `context.*` lands in the project context
1049
+ * (next instructions load), `websearch.provider` mutates the shared config object the search
1050
+ * chain reads per call, `pluginHooks.timeoutMs` lands in the plugin host (next hook call),
1051
+ * and `tui.*` is persisted for the host to apply. Nothing in this set requires a restart.
1052
+ * `agents.*` (active agent id and reasoning effort) is persisted for the TUI to consume:
1053
+ * the next `/agents`/`/effort` selection and the next prompt already read the live config.
1054
+ * `mcp.allow` is deliberately not routed here: it flows through
1055
+ * `rememberGlobalMcpConsent`/`revokeGlobalMcpConsent` instead.
1056
+ * Throws under `--read-only`. Returns the written config file.
1057
+ */
1058
+ async updateSetting(key, value) {
1059
+ if (options.readOnly)
1060
+ throw new Error("Settings changes are unavailable under --read-only");
1061
+ const file = await setConfigValue({ key, value });
1062
+ const compactionLeaf = (leaf) => ({ [leaf]: value });
1063
+ const limitLeaf = (leaf) => ({ [leaf]: value });
1064
+ switch (key) {
1065
+ case "compaction.auto":
1066
+ case "compaction.threshold":
1067
+ case "compaction.keepTurns":
1068
+ case "compaction.maxOutputTokens": {
1069
+ const patch = compactionLeaf(key.slice("compaction.".length));
1070
+ config.compaction = { ...config.compaction, ...patch };
1071
+ runner.applySettings({ compaction: patch });
1072
+ break;
1073
+ }
1074
+ case "context.claudeMdFallback":
1075
+ config.context = { ...config.context, claudeMdFallback: value === true };
1076
+ context.update({ claudeMdFallback: value === true });
1077
+ break;
1078
+ case "context.maxBytes":
1079
+ config.context = { ...config.context, maxBytes: Number(value) };
1080
+ context.update({ maxBytes: Number(value) });
1081
+ break;
1082
+ case "limits.maxTurns":
1083
+ case "limits.maxOutputTokens":
1084
+ case "limits.maxContextChars":
1085
+ case "limits.firstTokenTimeoutMs":
1086
+ case "limits.firstTokenRetries":
1087
+ case "limits.truncationRecoveries":
1088
+ case "limits.timeoutMs": {
1089
+ const patch = limitLeaf(key.slice("limits.".length));
1090
+ config.limits = { ...config.limits, ...patch };
1091
+ runner.applySettings(patch);
1092
+ break;
1093
+ }
1094
+ case "pluginHooks.timeoutMs":
1095
+ config.pluginHooks = { ...config.pluginHooks, timeoutMs: Number(value) };
1096
+ plugins.applyTimeoutSettings({ hookTimeoutMs: Number(value) });
1097
+ break;
1098
+ case "tui.paddingX":
1099
+ config.tui = { ...config.tui, paddingX: Number(value) };
1100
+ break;
1101
+ case "tui.contentPaddingX":
1102
+ config.tui = { ...config.tui, contentPaddingX: Number(value) };
1103
+ break;
1104
+ case "tui.skillSlashCommands":
1105
+ config.tui = { ...config.tui, skillSlashCommands: value === true };
1106
+ break;
1107
+ case "agents.active":
1108
+ config.agents = { ...config.agents, active: String(value) };
1109
+ break;
1110
+ case "agents.effort":
1111
+ // Clearing (undefined) drops the leaf from the live config and the JSON file.
1112
+ config.agents =
1113
+ value === undefined
1114
+ ? { active: config.agents.active }
1115
+ : { ...config.agents, effort: String(value) };
1116
+ break;
1117
+ case "analysis.enabled":
1118
+ // Tools are registered when the application starts: the change applies to the next
1119
+ // application (a restart; the web recycles the workspace application).
1120
+ config.analysis = { ...config.analysis, enabled: value === true };
1121
+ break;
1122
+ case "analysis.limits.timeoutMs":
1123
+ // Read live by python_run on its next call.
1124
+ config.analysis = {
1125
+ ...config.analysis,
1126
+ limits: { ...config.analysis.limits, timeoutMs: Number(value) },
1127
+ };
1128
+ break;
1129
+ case "analysis.retention.jobsDays":
1130
+ case "analysis.retention.intermediateDays":
1131
+ case "analysis.retention.artifactsDays": {
1132
+ // Read by the next sweep.
1133
+ const leaf = key.slice("analysis.retention.".length);
1134
+ config.analysis = {
1135
+ ...config.analysis,
1136
+ retention: { ...config.analysis.retention, [leaf]: Number(value) },
1137
+ };
1138
+ break;
1139
+ }
1140
+ case "goal.enabled":
1141
+ case "goal.maxTurns":
1142
+ case "goal.maxMinutes":
1143
+ case "goal.repeatedReplyLimit":
1144
+ case "goal.noToolTurnsLimit":
1145
+ case "goal.blockedRepeats": {
1146
+ // Read live: the next continuation (or the next `/goal`) uses the new value. The caps
1147
+ // of a goal already running were snapshotted when it was created.
1148
+ const leaf = key.slice("goal.".length);
1149
+ config.goal = {
1150
+ ...config.goal,
1151
+ [leaf]: leaf === "enabled" ? value === true : Number(value),
1152
+ };
1153
+ break;
1154
+ }
1155
+ case "tasks.enabled":
1156
+ // Tools are registered when the application starts: the change applies to the next
1157
+ // application (a restart; the web recycles the workspace application).
1158
+ config.tasks = { ...config.tasks, enabled: value === true };
1159
+ break;
1160
+ case "tasks.maxPerSession":
1161
+ case "tasks.maxRunMs":
1162
+ case "tasks.maxOutputBytes":
1163
+ case "tasks.retentionDays": {
1164
+ // Read live: the next task (or the next sweep) uses the new value.
1165
+ const leaf = key.slice("tasks.".length);
1166
+ config.tasks = { ...config.tasks, [leaf]: Number(value) };
1167
+ break;
1168
+ }
1169
+ case "websearch.provider":
1170
+ // Mutated in place (same object identity) so the tool chain's per-call
1171
+ // `websearchCtx.config` sees the new provider on the very next search call.
1172
+ config.websearch.provider =
1173
+ value === undefined ? undefined : value;
1174
+ break;
1175
+ }
1176
+ return file;
1177
+ },
732
1178
  provider,
733
1179
  providers,
734
1180
  providerSettings,
@@ -739,6 +1185,40 @@ export async function createApplication(options = {}) {
739
1185
  return providerInfo;
740
1186
  },
741
1187
  runner,
1188
+ /** `/btw` side questions: tool-less answers about a session outside its conversation. */
1189
+ sideQuestions: new SideQuestions({ runner, state: store }),
1190
+ /**
1191
+ * User agent files (`<workspace>/.agents/agents`, `~/.agents/agents`). Every write asks the
1192
+ * subagents plugin (the runtime agent registry) to rediscover through its `/agents reload`
1193
+ * verb and reports whether the saved file is loaded; without the plugin nothing reloads.
1194
+ */
1195
+ agentDefinitions: new AgentDefinitionService({
1196
+ workspace,
1197
+ home: homedir(),
1198
+ registry: {
1199
+ async reload() {
1200
+ const handler = plugins.commands.get("agents");
1201
+ if (!handler)
1202
+ return false;
1203
+ await handler("reload");
1204
+ return Array.isArray(plugins.pluginState("subagents", "definitions"));
1205
+ },
1206
+ loadedPaths() {
1207
+ let state;
1208
+ try {
1209
+ state = plugins.pluginState("subagents", "definitions");
1210
+ }
1211
+ catch {
1212
+ return undefined;
1213
+ }
1214
+ if (!Array.isArray(state))
1215
+ return undefined;
1216
+ return state
1217
+ .map((entry) => entry?.path)
1218
+ .filter((path) => typeof path === "string");
1219
+ },
1220
+ },
1221
+ }),
742
1222
  herdr,
743
1223
  contextWindow,
744
1224
  /** Effective context budget + compaction point shared by the runner and the TUI bar. */
@@ -837,24 +1317,34 @@ export async function createApplication(options = {}) {
837
1317
  });
838
1318
  },
839
1319
  async close() {
1320
+ // Exit latency is user-facing: tear down herdr + MCP in parallel, then provider +
1321
+ // plugins (store last). Every stage is raced against a cap so a single hanging
1322
+ // component cannot stall /exit; teardown errors are swallowed for callers.
1323
+ const capped = (work) => Promise.race([work.catch(() => { }), delay(TEARDOWN_STAGE_TIMEOUT_MS)]);
840
1324
  try {
841
- await herdr.close();
842
- await mcp.close();
1325
+ janitor.stop();
1326
+ taskJanitor.stop();
1327
+ datasets?.close();
1328
+ // Tasks first in the same stage: they end with Alisio (process groups killed).
1329
+ await capped(Promise.allSettled([herdr.close(), mcp.close(), tasks.close()]));
843
1330
  }
844
1331
  finally {
845
1332
  try {
846
- await provider.dispose();
847
- await Promise.allSettled([
848
- ...new Set([
1333
+ await capped((async () => {
1334
+ const runtimes = new Set([
849
1335
  ...(await Promise.allSettled(targetProviders.values()))
850
1336
  .filter((x) => x.status === "fulfilled")
851
1337
  .map((x) => x.value),
852
1338
  ...retainedProviders,
853
- ]),
854
- ]
855
- .filter((runtime) => runtime !== provider.currentProvider)
856
- .map((runtime) => Promise.resolve(runtime.dispose?.())));
857
- await plugins.close();
1339
+ ]);
1340
+ await Promise.allSettled([
1341
+ provider.dispose(),
1342
+ ...[...runtimes]
1343
+ .filter((runtime) => runtime !== provider.currentProvider)
1344
+ .map((runtime) => Promise.resolve(runtime.dispose?.())),
1345
+ plugins.close(),
1346
+ ]);
1347
+ })());
858
1348
  }
859
1349
  finally {
860
1350
  store.close();
@@ -864,9 +1354,10 @@ export async function createApplication(options = {}) {
864
1354
  };
865
1355
  }
866
1356
  catch (e) {
867
- await herdr.close();
868
- await mcp.close();
869
- await plugins.close();
1357
+ await Promise.race([
1358
+ Promise.allSettled([herdr.close(), mcp.close(), plugins.close()]),
1359
+ delay(TEARDOWN_STAGE_TIMEOUT_MS),
1360
+ ]);
870
1361
  store.close();
871
1362
  throw e;
872
1363
  }