@alisio/core 0.5.1 → 0.6.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 (169) hide show
  1. package/dist/analysis/janitor.d.ts +48 -0
  2. package/dist/analysis/janitor.js +170 -18
  3. package/dist/analysis/jobs.d.ts +11 -2
  4. package/dist/analysis/jobs.js +13 -0
  5. package/dist/analysis/oci.d.ts +11 -2
  6. package/dist/analysis/oci.js +20 -4
  7. package/dist/analysis/python/sources.js +2 -1
  8. package/dist/analysis/rerun.d.ts +1 -1
  9. package/dist/analysis/status.d.ts +3 -1
  10. package/dist/analysis/status.js +3 -0
  11. package/dist/application.d.ts +38 -2
  12. package/dist/application.js +213 -4
  13. package/dist/changelog/data.js +64 -0
  14. package/dist/config.d.ts +110 -2
  15. package/dist/config.js +188 -6
  16. package/dist/core/call-internals.d.ts +22 -0
  17. package/dist/core/call-internals.js +9 -0
  18. package/dist/core/contracts.d.ts +2 -2
  19. package/dist/core/runner.js +111 -38
  20. package/dist/forge/broker.d.ts +51 -0
  21. package/dist/forge/broker.js +244 -0
  22. package/dist/forge/budget.d.ts +27 -0
  23. package/dist/forge/budget.js +60 -0
  24. package/dist/forge/catalog-view.d.ts +24 -0
  25. package/dist/forge/catalog-view.js +43 -0
  26. package/dist/forge/errors.d.ts +26 -0
  27. package/dist/forge/errors.js +49 -0
  28. package/dist/forge/guides/javascript.d.ts +25 -0
  29. package/dist/forge/guides/javascript.js +90 -0
  30. package/dist/forge/guides/provider.d.ts +42 -0
  31. package/dist/forge/guides/provider.js +96 -0
  32. package/dist/forge/guides/python-environment.d.ts +15 -0
  33. package/dist/forge/guides/python-environment.js +81 -0
  34. package/dist/forge/guides/python.d.ts +38 -0
  35. package/dist/forge/guides/python.js +170 -0
  36. package/dist/forge/javascript/engine.d.ts +44 -0
  37. package/dist/forge/javascript/engine.js +229 -0
  38. package/dist/forge/javascript/hosts/factory.d.ts +25 -0
  39. package/dist/forge/javascript/hosts/factory.js +21 -0
  40. package/dist/forge/javascript/hosts/framing.d.ts +17 -0
  41. package/dist/forge/javascript/hosts/framing.js +49 -0
  42. package/dist/forge/javascript/hosts/isolate-main.d.ts +9 -0
  43. package/dist/forge/javascript/hosts/isolate-main.js +63 -0
  44. package/dist/forge/javascript/hosts/process-entry.d.ts +1 -0
  45. package/dist/forge/javascript/hosts/process-entry.js +3 -0
  46. package/dist/forge/javascript/hosts/process-host.d.ts +33 -0
  47. package/dist/forge/javascript/hosts/process-host.js +160 -0
  48. package/dist/forge/javascript/hosts/worker-entry.d.ts +1 -0
  49. package/dist/forge/javascript/hosts/worker-entry.js +3 -0
  50. package/dist/forge/javascript/hosts/worker-host.d.ts +18 -0
  51. package/dist/forge/javascript/hosts/worker-host.js +101 -0
  52. package/dist/forge/javascript/prelude.d.ts +2 -0
  53. package/dist/forge/javascript/prelude.js +333 -0
  54. package/dist/forge/javascript/probe.d.ts +2 -0
  55. package/dist/forge/javascript/probe.js +53 -0
  56. package/dist/forge/javascript/runtime-manifest.d.ts +14 -0
  57. package/dist/forge/javascript/runtime-manifest.js +14 -0
  58. package/dist/forge/javascript/vm-runner.d.ts +21 -0
  59. package/dist/forge/javascript/vm-runner.js +301 -0
  60. package/dist/forge/javascript/wasm-asset.d.ts +8 -0
  61. package/dist/forge/javascript/wasm-asset.js +8 -0
  62. package/dist/forge/javascript/wasm.d.ts +15 -0
  63. package/dist/forge/javascript/wasm.js +54 -0
  64. package/dist/forge/journal.d.ts +105 -0
  65. package/dist/forge/journal.js +174 -0
  66. package/dist/forge/kernel.d.ts +76 -0
  67. package/dist/forge/kernel.js +338 -0
  68. package/dist/forge/limits.d.ts +38 -0
  69. package/dist/forge/limits.js +73 -0
  70. package/dist/forge/nested.d.ts +96 -0
  71. package/dist/forge/nested.js +214 -0
  72. package/dist/forge/ports.d.ts +120 -0
  73. package/dist/forge/ports.js +1 -0
  74. package/dist/forge/progress.d.ts +24 -0
  75. package/dist/forge/progress.js +76 -0
  76. package/dist/forge/protocol.d.ts +110 -0
  77. package/dist/forge/protocol.js +171 -0
  78. package/dist/forge/python/backend-selector.d.ts +38 -0
  79. package/dist/forge/python/backend-selector.js +111 -0
  80. package/dist/forge/python/backend.d.ts +89 -0
  81. package/dist/forge/python/backend.js +1 -0
  82. package/dist/forge/python/engine.d.ts +84 -0
  83. package/dist/forge/python/engine.js +279 -0
  84. package/dist/forge/python/frame-parser.d.ts +24 -0
  85. package/dist/forge/python/frame-parser.js +107 -0
  86. package/dist/forge/python/frame-transport.d.ts +40 -0
  87. package/dist/forge/python/frame-transport.js +101 -0
  88. package/dist/forge/python/import-scan.d.ts +22 -0
  89. package/dist/forge/python/import-scan.js +133 -0
  90. package/dist/forge/python/managed-backend.d.ts +30 -0
  91. package/dist/forge/python/managed-backend.js +111 -0
  92. package/dist/forge/python/oci-backend.d.ts +27 -0
  93. package/dist/forge/python/oci-backend.js +84 -0
  94. package/dist/forge/python/pyodide-backend.d.ts +40 -0
  95. package/dist/forge/python/pyodide-backend.js +289 -0
  96. package/dist/forge/python/pyodide-install.d.ts +41 -0
  97. package/dist/forge/python/pyodide-install.js +167 -0
  98. package/dist/forge/python/pyodide-manifest.d.ts +42 -0
  99. package/dist/forge/python/pyodide-manifest.js +376 -0
  100. package/dist/forge/python/pyodide-runner-source.d.ts +2 -0
  101. package/dist/forge/python/pyodide-runner-source.js +3 -0
  102. package/dist/forge/python/rss.d.ts +2 -0
  103. package/dist/forge/python/rss.js +33 -0
  104. package/dist/forge/python/scratch-copy.d.ts +6 -0
  105. package/dist/forge/python/scratch-copy.js +61 -0
  106. package/dist/forge/python/settle.d.ts +6 -0
  107. package/dist/forge/python/settle.js +21 -0
  108. package/dist/forge/python/status.d.ts +8 -0
  109. package/dist/forge/python/status.js +15 -0
  110. package/dist/forge/python/wasm-node.d.ts +59 -0
  111. package/dist/forge/python/wasm-node.js +220 -0
  112. package/dist/forge/python/wiring.d.ts +39 -0
  113. package/dist/forge/python/wiring.js +47 -0
  114. package/dist/forge/registry.d.ts +26 -0
  115. package/dist/forge/registry.js +83 -0
  116. package/dist/forge/result.d.ts +58 -0
  117. package/dist/forge/result.js +86 -0
  118. package/dist/forge/routing-rule.d.ts +19 -0
  119. package/dist/forge/routing-rule.js +31 -0
  120. package/dist/forge/runtime.d.ts +64 -0
  121. package/dist/forge/runtime.js +88 -0
  122. package/dist/forge/scratch-files.d.ts +36 -0
  123. package/dist/forge/scratch-files.js +94 -0
  124. package/dist/forge/scratch-provider.d.ts +41 -0
  125. package/dist/forge/scratch-provider.js +79 -0
  126. package/dist/forge/scratch-quota.d.ts +8 -0
  127. package/dist/forge/scratch-quota.js +25 -0
  128. package/dist/forge/selector.d.ts +35 -0
  129. package/dist/forge/selector.js +51 -0
  130. package/dist/forge/session-dir.d.ts +59 -0
  131. package/dist/forge/session-dir.js +239 -0
  132. package/dist/forge/session-path.d.ts +31 -0
  133. package/dist/forge/session-path.js +61 -0
  134. package/dist/forge/slots.d.ts +16 -0
  135. package/dist/forge/slots.js +47 -0
  136. package/dist/forge/state-delta.d.ts +54 -0
  137. package/dist/forge/state-delta.js +99 -0
  138. package/dist/forge/state-machine.d.ts +20 -0
  139. package/dist/forge/state-machine.js +40 -0
  140. package/dist/forge/state-store.d.ts +25 -0
  141. package/dist/forge/state-store.js +50 -0
  142. package/dist/forge/task-log.d.ts +9 -0
  143. package/dist/forge/task-log.js +42 -0
  144. package/dist/forge/verdict.d.ts +20 -0
  145. package/dist/forge/verdict.js +28 -0
  146. package/dist/forge-isolate.d.ts +6 -0
  147. package/dist/forge-isolate.js +6 -0
  148. package/dist/index.d.ts +7 -0
  149. package/dist/index.js +7 -0
  150. package/dist/permissions/decide.d.ts +42 -0
  151. package/dist/permissions/decide.js +24 -0
  152. package/dist/resources/context.d.ts +2 -1
  153. package/dist/resources/context.js +2 -1
  154. package/dist/runtime/access.d.ts +34 -0
  155. package/dist/runtime/access.js +80 -0
  156. package/dist/runtime/process.d.ts +2 -1
  157. package/dist/runtime/process.js +5 -5
  158. package/dist/runtime/store.d.ts +11 -0
  159. package/dist/runtime/store.js +103 -0
  160. package/dist/tools/analysis.d.ts +25 -2
  161. package/dist/tools/analysis.js +315 -267
  162. package/dist/tools/execute.js +6 -7
  163. package/dist/tools/forge-guide.d.ts +6 -0
  164. package/dist/tools/forge-guide.js +60 -0
  165. package/dist/tools/js-run.d.ts +19 -0
  166. package/dist/tools/js-run.js +126 -0
  167. package/dist/tools/standard.d.ts +7 -1
  168. package/dist/tools/standard.js +44 -43
  169. package/package.json +7 -2
@@ -16,7 +16,7 @@ export interface RerunPlan {
16
16
  manifest: unknown[];
17
17
  extras: Array<"analysis" | "science">;
18
18
  /** The runtime of the original execution (a rerun uses the configured runtime now). */
19
- runtime: "managed" | "oci";
19
+ runtime: "managed" | "oci" | "wasm";
20
20
  }
21
21
  export type RerunPreparation = {
22
22
  ok: true;
@@ -3,7 +3,7 @@
3
3
  * Settings page: mode, interpreter and extras, container engine (probed only when it matters),
4
4
  * limits and retention. One shape (`AnalysisStatus` of the SDK) for every surface.
5
5
  */
6
- import type { AnalysisStatus } from "@alisio/sdk";
6
+ import type { AnalysisStatus, EngineDescriptor } from "@alisio/sdk";
7
7
  import type { Config } from "../config.ts";
8
8
  import type { AnalysisJanitor } from "./janitor.ts";
9
9
  import type { OciRuntime } from "./oci.ts";
@@ -14,6 +14,8 @@ export declare function analysisStatus(input: {
14
14
  /** Builds the container runtime of the current settings (probed on demand). */
15
15
  oci?: () => OciRuntime;
16
16
  janitor?: Pick<AnalysisJanitor, "lastSweep">;
17
+ /** The live state of the Python backends (isolation, readiness, profiles); omitted = no table. */
18
+ backends?: (signal?: AbortSignal) => Promise<EngineDescriptor[]>;
17
19
  /** python_run is registered (analysis enabled and not `--read-only`). */
18
20
  enabled: boolean;
19
21
  readOnly: boolean;
@@ -40,10 +40,13 @@ export async function analysisStatus(input) {
40
40
  oci.reason = status.reason;
41
41
  }
42
42
  const lastSweep = await input.janitor?.lastSweep();
43
+ const backends = input.backends ? await input.backends(input.signal) : undefined;
43
44
  return {
44
45
  enabled: input.enabled,
45
46
  readOnly: input.readOnly,
46
47
  mode: config.runtime,
48
+ isolation: config.isolation,
49
+ ...(backends ? { backends } : {}),
47
50
  python,
48
51
  oci,
49
52
  limits: { timeoutMs: config.limits.timeoutMs },
@@ -2,7 +2,7 @@ import type { AvailableProviderModel, ModelInfo, ModelProvider, Plugin, PluginCa
2
2
  import { AgentDefinitionService } from "./agents/service.ts";
3
3
  import { CapabilityGrants } from "./analysis/capabilities.ts";
4
4
  import { DatasetService } from "./analysis/data/datasets.ts";
5
- import { AnalysisJanitor } from "./analysis/janitor.ts";
5
+ import { AnalysisJanitor, ScratchJanitor } from "./analysis/janitor.ts";
6
6
  import { AnalysisJobs } from "./analysis/jobs.ts";
7
7
  import { OciRuntime } from "./analysis/oci.ts";
8
8
  import { AnalysisRerun } from "./analysis/rerun.ts";
@@ -15,6 +15,8 @@ import type { ApprovalHandler } from "./core/contracts.ts";
15
15
  import { ToolRegistry } from "./core/registry.ts";
16
16
  import { AgentRunner } from "./core/runner.ts";
17
17
  import { DecisionMetrics, DecisionRegistry, DecisionService } from "./decisions/index.ts";
18
+ import { SqliteExecutionJournal } from "./forge/journal.ts";
19
+ import { SqliteStateStore } from "./forge/state-store.ts";
18
20
  import { GoalService } from "./goal/service.ts";
19
21
  import { HerdrBridge } from "./integrations/herdr.ts";
20
22
  import { McpConnector } from "./mcp/connector.ts";
@@ -188,12 +190,36 @@ export declare function createApplication(options?: AppOptions): Promise<{
188
190
  jobs: AnalysisJobs;
189
191
  /** Container runtime of the current configuration (status, pull); mode `managed` ignores it. */
190
192
  oci: {
191
- mode: () => "managed" | "oci";
193
+ mode: () => "auto" | "managed" | "oci" | "wasm";
192
194
  runtime: () => OciRuntime;
193
195
  };
196
+ /** The live state of every Python backend (isolation, ready, profiles, reason). */
197
+ backends: (signal?: AbortSignal) => Promise<import("@alisio/sdk").EngineDescriptor[]>;
194
198
  rerun: AnalysisRerun;
195
199
  janitor: AnalysisJanitor;
196
200
  };
201
+ /**
202
+ * Forja services for the engines (Phase 3b): the execution journal, the cross-execution
203
+ * state, the scratchpad of a session (undefined while the JavaScript engine is off) and the
204
+ * retention sweep of the volatile session folders.
205
+ */
206
+ forge: {
207
+ journal: SqliteExecutionJournal;
208
+ state: SqliteStateStore;
209
+ /** The scratchpad, created on first use; under the same gate as the write root. */
210
+ scratchpad: ((session: string) => Promise<string | undefined>) | undefined;
211
+ /**
212
+ * What the HTTP surface sees (L2): `available` follows the gate of the write root (engine
213
+ * verified, not `--read-only`); the folder is only looked up, never created.
214
+ */
215
+ scratchpadView: ((session: string) => Promise<{
216
+ available: boolean;
217
+ path?: string;
218
+ }>) | undefined;
219
+ scratchJanitor: ScratchJanitor;
220
+ /** The JavaScript engine: descriptor and effective limits (`alisio forge status`), probe. */
221
+ runtime: import("./forge/runtime.ts").ForgeRuntime;
222
+ };
197
223
  /** Keys a project layer set that only the user layer decides (ignored on load). */
198
224
  configDiagnostics: string[];
199
225
  /** Tabular datasets of every session (ingestion, read-only queries, pages). */
@@ -320,6 +346,16 @@ export declare function createApplication(options?: AppOptions): Promise<{
320
346
  resolveModel: (reference: string, signal?: AbortSignal) => Promise<ResolvedProviderModel>;
321
347
  /** Creates and binds a fresh session to an optional configured model selector. */
322
348
  createSession(reference?: string): Promise<import("./index.ts").Session>;
349
+ /**
350
+ * Re-binds an existing session to the provider that owns `model` (a configured profile), else
351
+ * to the active provider. The session keeps a single provider+model binding, so choosing a
352
+ * model of the now-active provider makes it usable even when the session's original provider
353
+ * is gone. The persisted transcript is provider-neutral: no new session is needed.
354
+ */
355
+ rebindSession(sessionId: string, model?: string): Promise<{
356
+ provider: string;
357
+ model: string;
358
+ }>;
323
359
  /** Persists and activates only after construction succeeds; programmatic overrides stay fixed. */
324
360
  activateProvider(id: string, profile: Record<string, import("@alisio/sdk").ProviderConfigurationValue>, credentials: Record<string, string>, model: string, profileName?: string): Promise<void>;
325
361
  /** Activates a stored profile without exposing its credentials. Returns false for a no-op. */
@@ -4,7 +4,7 @@ import { dirname, join, resolve } from "node:path";
4
4
  import { AgentDefinitionService } from "./agents/service.js";
5
5
  import { CapabilityGrants } from "./analysis/capabilities.js";
6
6
  import { DatasetService } from "./analysis/data/datasets.js";
7
- import { AnalysisJanitor } from "./analysis/janitor.js";
7
+ import { AnalysisJanitor, ScratchJanitor } from "./analysis/janitor.js";
8
8
  import { AnalysisJobs } from "./analysis/jobs.js";
9
9
  import { OciRuntime } from "./analysis/oci.js";
10
10
  import { AnalysisRerun } from "./analysis/rerun.js";
@@ -21,6 +21,12 @@ import { DEFAULT_MAX_OUTPUT_TOKENS } from "./core/output-limit.js";
21
21
  import { ToolRegistry } from "./core/registry.js";
22
22
  import { AgentRunner } from "./core/runner.js";
23
23
  import { bindDecisions, DecisionMetrics, DecisionRegistry, DecisionService, } from "./decisions/index.js";
24
+ import { reconcileInterruptedExecutions, SqliteExecutionJournal } from "./forge/journal.js";
25
+ import { describePythonBackends } from "./forge/python/status.js";
26
+ import { createPythonBackends } from "./forge/python/wiring.js";
27
+ import { createForgeRuntime } from "./forge/runtime.js";
28
+ import { createScratchpadLookup, createScratchpadProvider, scratchSweepRoots, } from "./forge/scratch-provider.js";
29
+ import { SqliteStateStore } from "./forge/state-store.js";
24
30
  import { GoalService } from "./goal/service.js";
25
31
  import { GoalStore } from "./goal/store.js";
26
32
  import { HerdrBridge } from "./integrations/herdr.js";
@@ -35,7 +41,7 @@ import { Skills, skillRoots } from "./resources/skills.js";
35
41
  import { PathAccess } from "./runtime/access.js";
36
42
  import { BlobStore } from "./runtime/blobs.js";
37
43
  import { isPathSpec } from "./runtime/modules.js";
38
- import { findWorkspace } from "./runtime/paths.js";
44
+ import { findWorkspace, workspaceKey } from "./runtime/paths.js";
39
45
  import { SQLiteStore } from "./runtime/store.js";
40
46
  import { ChildSessions } from "./sessions/children.js";
41
47
  import { SideQuestions } from "./sessions/side-questions.js";
@@ -100,6 +106,24 @@ export async function createApplication(options = {}) {
100
106
  }
101
107
  });
102
108
  const askExternalDirectory = options.approveExternalDirectory;
109
+ const customStore = !!options.db;
110
+ const scratchStateRoot = options.db && options.db !== ":memory:" ? dirname(options.db) : stateHome();
111
+ // Forja's scratchpad (spec §9.4): an authorized write root only while the JavaScript engine is
112
+ // enabled, so a build with Forja off behaves exactly as before. `store` is read lazily, on the
113
+ // first path outside the workspace.
114
+ const scratchOptions = {
115
+ rootOf: (session) => store.rootOf(session),
116
+ workspaceOf: (rootSessionId) => store.get(rootSessionId).workspace,
117
+ workspaceKey,
118
+ stateHome: scratchStateRoot,
119
+ configRoot: () => config.forge.scratch.root,
120
+ };
121
+ const scratchEnabled = config.forge.javascript.enabled && config.forge.scratch.enabled;
122
+ const scratchpad = scratchEnabled ? createScratchpadProvider(scratchOptions) : undefined;
123
+ const scratchpadLookup = scratchEnabled ? createScratchpadLookup(scratchOptions) : undefined;
124
+ // The scratchpad is a free write root only once the engine verified itself (`jsReady`, known after
125
+ // `createForgeRuntime` below): a failed self-check leaves it an ordinary directory (L9).
126
+ let scratchReady = false;
103
127
  const pathAccess = new PathAccess({
104
128
  workspace,
105
129
  extraRoots: [...config.additionalDirectories, ...addDirs],
@@ -109,8 +133,20 @@ export async function createApplication(options = {}) {
109
133
  }
110
134
  : {}),
111
135
  readOnly: !!options.readOnly,
136
+ ...(scratchpad
137
+ ? {
138
+ scratchpad: (session) => scratchReady ? scratchpad(session) : Promise.resolve(undefined),
139
+ scratchMaxBytes: () => config.forge.scratch.maxBytes,
140
+ }
141
+ : {}),
112
142
  });
113
143
  const store = new SQLiteStore(options.db ?? join(stateHome(), "sessions.sqlite"));
144
+ // Executions of a process that is gone cannot be running: close them (nothing is relaunched).
145
+ reconcileInterruptedExecutions(store.db);
146
+ const forgeJournal = new SqliteExecutionJournal(store.db);
147
+ const forgeState = new SqliteStateStore(store.db, {
148
+ maxTotalBytes: () => config.forge.state.maxTotalBytes,
149
+ });
114
150
  // Content-addressed attachment bytes next to the session database (no I/O until `put`).
115
151
  const blobs = new BlobStore({
116
152
  root: options.db && options.db !== ":memory:"
@@ -352,6 +388,14 @@ export async function createApplication(options = {}) {
352
388
  }));
353
389
  context.extras.push(async () => skills.catalog(), ...plugins.contexts);
354
390
  const { registerStandard } = await import("./tools/standard.js");
391
+ // Forja's JavaScript engine verifies itself here (the WASM against its pinned hash). Enabled
392
+ // means `js_run` replaces `execute` for good: a failed check leaves neither (no fallback).
393
+ const forgeRuntime = await createForgeRuntime({
394
+ forge: () => config.forge,
395
+ journal: forgeJournal,
396
+ state: forgeState,
397
+ });
398
+ scratchReady = forgeRuntime.jsReady;
355
399
  // `runner` is referenced lazily (constructed just below): safe because these accessors only
356
400
  // run once the tool is actually called, well after the runner exists.
357
401
  registerStandard(registry, workspace, skills, context, plugins.ui, {
@@ -359,7 +403,27 @@ export async function createApplication(options = {}) {
359
403
  }, {
360
404
  config: config.websearch,
361
405
  resolveExtension: () => plugins.extensions.resolve("websearch"),
362
- }, pathAccess);
406
+ }, pathAccess, { execute: !config.forge.javascript.enabled });
407
+ // `--read-only` has no scratchpad root, so `codePath` is unavailable there.
408
+ const sessionScratchpad = options.readOnly ? async () => undefined : scratchpad;
409
+ if (forgeRuntime.jsReady) {
410
+ const { registerJsRun } = await import("./tools/js-run.js");
411
+ const { buildRoutingRule } = await import("./forge/routing-rule.js");
412
+ registerJsRun(registry, {
413
+ kernel: forgeRuntime.kernel,
414
+ rootOf: (session) => store.rootOf(session),
415
+ scratchpad: async (session) => sessionScratchpad?.(session),
416
+ policy: () => runner.policy,
417
+ });
418
+ // Layer 0 of the context (spec §8.1): present only because `js_run` is, per session.
419
+ context.extras.push(async (sessionId) => buildRoutingRule({
420
+ javascript: true,
421
+ python: registry.list().some((tool) => tool.name === "python_run"),
422
+ // Registered later (after `python_run`), so it is looked up when the prompt is built.
423
+ guide: registry.list().some((tool) => tool.name === "forge_guide"),
424
+ scratchpad: sessionId ? await sessionScratchpad?.(sessionId) : undefined,
425
+ }));
426
+ }
363
427
  // Python analysis and artifacts live next to the session database (like attachment blobs).
364
428
  const artifacts = new ArtifactStore({
365
429
  root: stateRoot,
@@ -395,6 +459,13 @@ export async function createApplication(options = {}) {
395
459
  return ociRuntime.runtime;
396
460
  },
397
461
  };
462
+ // The Python backends as surfaces report them (`GET /api/analysis`, `GET /api/forge`); the tool
463
+ // builds its own for running. Read live: the settings can change between calls.
464
+ const pythonBackends = createPythonBackends({
465
+ runtime: analysisRuntime,
466
+ oci,
467
+ wasm: { stateRoot, settings: () => config.analysis.wasm },
468
+ });
398
469
  // Tabular datasets (one SQLite file each); XLSX needs the discovered Python.
399
470
  const datasets = new DatasetService({
400
471
  root: stateRoot,
@@ -416,6 +487,28 @@ export async function createApplication(options = {}) {
416
487
  ? { initialDelayMs: options.analysisSweepDelayMs }
417
488
  : {}),
418
489
  });
490
+ // Volatile Forja session folders: same schedule as the analysis sweep, gone with their session.
491
+ const scratchJanitor = new ScratchJanitor({
492
+ stateRoot,
493
+ roots: () => scratchSweepRoots({
494
+ customStore,
495
+ stateHome: scratchStateRoot,
496
+ configRoot: config.forge.scratch.root,
497
+ }),
498
+ sessionExists: (rootSessionId) => {
499
+ try {
500
+ store.get(rootSessionId);
501
+ return true;
502
+ }
503
+ catch {
504
+ return false;
505
+ }
506
+ },
507
+ retentionDays: () => config.forge.scratch.retentionDays,
508
+ ...(options.analysisSweepDelayMs !== undefined
509
+ ? { initialDelayMs: options.analysisSweepDelayMs }
510
+ : {}),
511
+ });
419
512
  // Background tasks: a service over the shared database (state change = compare-and-set), logs
420
513
  // under `<state>/tasks/`. Tasks of a dead owner process (a previous run) become `lost` now.
421
514
  const tasks = new BackgroundTasks({
@@ -473,6 +566,15 @@ export async function createApplication(options = {}) {
473
566
  runtime: analysisRuntime,
474
567
  datasets,
475
568
  oci,
569
+ // Read live: the policy and the Pyodide settings are global-only and may change.
570
+ isolation: () => config.analysis.isolation,
571
+ wasm: { stateRoot, settings: () => config.analysis.wasm },
572
+ // Python runs through the same kernel as `js_run`: one journal, one state store.
573
+ forge: { kernel: forgeRuntime.kernel, engines: forgeRuntime.engines },
574
+ // `codePath` and the read-only exposure exist only where `js_run` has its scratchpad too.
575
+ ...(scratchReady && sessionScratchpad
576
+ ? { scratchpad: async (session) => sessionScratchpad(session) }
577
+ : {}),
476
578
  rerun: analysisRerun,
477
579
  rootOf: (id) => store.rootOf(id),
478
580
  // Read live: a changed `analysis.limits.*` setting applies to the next call.
@@ -485,6 +587,8 @@ export async function createApplication(options = {}) {
485
587
  },
486
588
  },
487
589
  startup: { candidates: candidates.length > 0, extras: active?.extras ?? [] },
590
+ // Error results point at `forge_guide` only while that tool exists (spec §8.4).
591
+ guideHints: () => registry.list().some((tool) => tool.name === "forge_guide"),
488
592
  });
489
593
  const { registerArtifactTools } = await import("./tools/artifacts.js");
490
594
  registerArtifactTools(registry, { store: artifacts, rootOf: (id) => store.rootOf(id) });
@@ -496,6 +600,42 @@ export async function createApplication(options = {}) {
496
600
  context.extras.push(async () => DASHBOARD_PROMPT_RULE);
497
601
  }
498
602
  }
603
+ // Layer 2 of the context (spec §8.3): `forge_guide`. Registered only with Forja on and a ready
604
+ // engine for at least one language; the `python.*` topics need `python_run`.
605
+ if (config.forge.guides.enabled && config.forge.javascript.enabled) {
606
+ const [{ GuideProvider }, { registerForgeGuide }, { probePythonEnvironment }] = await Promise.all([
607
+ import("./forge/guides/provider.js"),
608
+ import("./tools/forge-guide.js"),
609
+ import("./forge/guides/python-environment.js"),
610
+ ]);
611
+ const hasPython = registry.list().some((tool) => tool.name === "python_run");
612
+ registerForgeGuide(registry, new GuideProvider({
613
+ ...(forgeRuntime.jsReady
614
+ ? {
615
+ javascript: async () => {
616
+ const status = await forgeRuntime.status();
617
+ return {
618
+ version: status.javascript.version,
619
+ isolation: status.engines[0]?.isolation ?? "wasm-isolate",
620
+ host: status.javascript.host,
621
+ limits: status.javascript.limits,
622
+ };
623
+ },
624
+ }
625
+ : {}),
626
+ ...(hasPython
627
+ ? {
628
+ python: () => probePythonEnvironment({
629
+ backends: pythonBackends,
630
+ runtime: () => config.analysis.runtime,
631
+ isolation: () => config.analysis.isolation,
632
+ managedExtras: async () => (await analysisRuntime.activeRuntime())?.extras ?? [],
633
+ smartDashboard: () => config.analysis.smartDashboard,
634
+ }),
635
+ }
636
+ : {}),
637
+ }));
638
+ }
499
639
  // `exit_plan` (opt-in: only the plan agent's run sees it): the plan review hand-over.
500
640
  const { registerExitPlan } = await import("./plan/exit-plan.js");
501
641
  registerExitPlan(registry, {
@@ -957,8 +1097,10 @@ export async function createApplication(options = {}) {
957
1097
  if (analysisEnabled && options.db !== ":memory:")
958
1098
  janitor.start();
959
1099
  // Retention is maintenance that deletes files: never under --read-only, never for `:memory:`.
960
- if (options.db !== ":memory:" && !options.readOnly)
1100
+ if (options.db !== ":memory:" && !options.readOnly) {
961
1101
  taskJanitor.start();
1102
+ scratchJanitor.start();
1103
+ }
962
1104
  return {
963
1105
  workspace,
964
1106
  pathAccess,
@@ -983,9 +1125,39 @@ export async function createApplication(options = {}) {
983
1125
  jobs: analysisJobs,
984
1126
  /** Container runtime of the current configuration (status, pull); mode `managed` ignores it. */
985
1127
  oci,
1128
+ /** The live state of every Python backend (isolation, ready, profiles, reason). */
1129
+ backends: (signal) => describePythonBackends(pythonBackends, signal),
986
1130
  rerun: analysisRerun,
987
1131
  janitor,
988
1132
  },
1133
+ /**
1134
+ * Forja services for the engines (Phase 3b): the execution journal, the cross-execution
1135
+ * state, the scratchpad of a session (undefined while the JavaScript engine is off) and the
1136
+ * retention sweep of the volatile session folders.
1137
+ */
1138
+ forge: {
1139
+ journal: forgeJournal,
1140
+ state: forgeState,
1141
+ /** The scratchpad, created on first use; under the same gate as the write root. */
1142
+ scratchpad: scratchpad
1143
+ ? (session) => scratchReady && !options.readOnly ? scratchpad(session) : Promise.resolve(undefined)
1144
+ : undefined,
1145
+ /**
1146
+ * What the HTTP surface sees (L2): `available` follows the gate of the write root (engine
1147
+ * verified, not `--read-only`); the folder is only looked up, never created.
1148
+ */
1149
+ scratchpadView: scratchpadLookup
1150
+ ? async (session) => {
1151
+ if (!scratchReady || options.readOnly)
1152
+ return { available: false };
1153
+ const path = await scratchpadLookup(session);
1154
+ return path ? { available: true, path } : { available: true };
1155
+ }
1156
+ : undefined,
1157
+ scratchJanitor,
1158
+ /** The JavaScript engine: descriptor and effective limits (`alisio forge status`), probe. */
1159
+ runtime: forgeRuntime,
1160
+ },
989
1161
  /** Keys a project layer set that only the user layer decides (ignored on load). */
990
1162
  configDiagnostics: configProvenance.ignored ?? [],
991
1163
  /** Tabular datasets of every session (ingestion, read-only queries, pages). */
@@ -1306,6 +1478,18 @@ export async function createApplication(options = {}) {
1306
1478
  config.tasks = { ...config.tasks, [leaf]: Number(value) };
1307
1479
  break;
1308
1480
  }
1481
+ case "forge.javascript.enabled":
1482
+ // The tool set (`js_run` or `execute`) is chosen when the application starts: the
1483
+ // change applies to the next application (the web recycles the workspace application).
1484
+ config.forge = {
1485
+ ...config.forge,
1486
+ javascript: { ...config.forge.javascript, enabled: value === true },
1487
+ };
1488
+ break;
1489
+ case "forge.guides.enabled":
1490
+ // Like `forge.javascript.enabled`: `forge_guide` is registered at start.
1491
+ config.forge = { ...config.forge, guides: { enabled: value === true } };
1492
+ break;
1309
1493
  case "web.iconTheme":
1310
1494
  // Read live by the icon-theme routes on the next request; `"none"` clears the theme.
1311
1495
  config.web = { ...config.web, iconTheme: String(value) };
@@ -1400,6 +1584,30 @@ export async function createApplication(options = {}) {
1400
1584
  sessionProviders.set(session.id, runtime);
1401
1585
  return session;
1402
1586
  },
1587
+ /**
1588
+ * Re-binds an existing session to the provider that owns `model` (a configured profile), else
1589
+ * to the active provider. The session keeps a single provider+model binding, so choosing a
1590
+ * model of the now-active provider makes it usable even when the session's original provider
1591
+ * is gone. The persisted transcript is provider-neutral: no new session is needed.
1592
+ */
1593
+ async rebindSession(sessionId, model) {
1594
+ const requested = model?.trim();
1595
+ if (requested) {
1596
+ const target = await resolveModel(requested).catch(() => undefined);
1597
+ if (target) {
1598
+ const runtime = await runtimeFor(target);
1599
+ store.updateBinding(sessionId, runtime.id, target.model.id);
1600
+ sessionProviders.set(sessionId, runtime);
1601
+ return { provider: runtime.id, model: target.model.id };
1602
+ }
1603
+ }
1604
+ // No configured profile owns it (or none requested): keep the active provider.
1605
+ const next = requested || provider.model;
1606
+ store.updateBinding(sessionId, provider.id, next);
1607
+ sessionProviders.set(sessionId, provider.currentProvider);
1608
+ retainedProviders.add(provider.currentProvider);
1609
+ return { provider: provider.id, model: next };
1610
+ },
1403
1611
  /** Persists and activates only after construction succeeds; programmatic overrides stay fixed. */
1404
1612
  async activateProvider(id, profile, credentials, model, profileName = id) {
1405
1613
  if (options.provider)
@@ -1468,6 +1676,7 @@ export async function createApplication(options = {}) {
1468
1676
  try {
1469
1677
  janitor.stop();
1470
1678
  taskJanitor.stop();
1679
+ scratchJanitor.stop();
1471
1680
  datasets?.close();
1472
1681
  // Tasks first in the same stage: they end with Alisio (process groups killed).
1473
1682
  await capped(Promise.allSettled([herdr.close(), mcp.close(), tasks.close()]));
@@ -2,6 +2,70 @@
2
2
  export const CHANGELOG_DATA = {
3
3
  schema: 1,
4
4
  entries: [
5
+ {
6
+ version: "0.6.0",
7
+ date: "2026-10-12",
8
+ sections: [
9
+ {
10
+ title: "Added",
11
+ items: [
12
+ "`js_run`: the model writes a short JavaScript script that composes tools, runs them in parallel and returns a small result. It runs in an isolated QuickJS-WebAssembly VM with limits for time, memory, calls and output; every nested call passes your permissions and a script cannot ask for approval.",
13
+ "`forge_guide`: a short guide for `js_run` and `python_run`, generated from the live limits, tools and Python backend, so the model learns the real environment on demand (`forge.guides.enabled`).",
14
+ "`python_run` backends `auto`, `managed`, `oci` and `wasm`, with an isolation policy (`analysis.isolation`: `preferred`, `required` or `strict`). The new `wasm` backend runs Pyodide in a restricted Node process with no Docker (`alisio analysis setup --wasm`); the result names the engine and why it was chosen. `alisio forge status` shows every engine and why one is not ready.",
15
+ "A per-session scratchpad where the model keeps scripts and intermediate files and runs them with `codePath` (`forge.scratch.*`: 7 days of retention and a 256 MiB quota by default). Python sees it read only through `ALISIO_SCRATCH_DIR`.",
16
+ "Web UI: a Forja settings page, a `js_run` card with the code, the nested calls and a progress bar, an isolation badge and the reason on every `python_run`, and a Scratch tab to preview, download and empty the scratchpad. The terminal shows nested-call counts and the Python backend.",
17
+ "Database migrations v9 and v10 (the execution journal and the shared script state) and `alisio_runtime` 4 with `alisio_runtime.forge`; they apply automatically.",
18
+ ],
19
+ },
20
+ {
21
+ title: "Changed",
22
+ items: [
23
+ "`analysis.runtime` defaults to `auto` (before: `managed`): Alisio uses the most isolated backend that is ready (container, then WebAssembly, then your Python). Set `managed` to keep the old behavior; an explicit value is honored or fails, never replaced.",
24
+ "While `forge.javascript.enabled` is on (the default), `execute` is not registered: `js_run` takes its place. Set it to `false` to get `execute` and the previous prompt back; saved sessions with `execute` calls still open.",
25
+ "The script's own progress now reaches the web UI next to the nested-call events instead of being replaced by them.",
26
+ "Isolation has limits you should know: the `wasm` backend does not block the network on Node 22 and 24, its memory limit is soft and a hostile script can reach the runner's JavaScript; `managed` is not a sandbox. The Forja documentation page lists them.",
27
+ ],
28
+ },
29
+ ],
30
+ },
31
+ {
32
+ version: "0.5.2",
33
+ date: "2026-10-08",
34
+ sections: [
35
+ {
36
+ title: "Fixed",
37
+ items: [
38
+ "Changing the model in a chat re-binds the session to the model's provider, so you can pick any model of the current provider and keep using the same conversation even when the session's original provider is gone, without starting a new chat.",
39
+ ],
40
+ },
41
+ ],
42
+ },
43
+ {
44
+ version: "0.5.1",
45
+ date: "2026-10-08",
46
+ sections: [
47
+ {
48
+ title: "Fixed",
49
+ items: [
50
+ 'The file panel previews PDFs again: the server no longer blocks the same-origin frame, which showed "refused to connect" instead of the document.',
51
+ ],
52
+ },
53
+ ],
54
+ },
55
+ {
56
+ version: "0.5.0",
57
+ date: "2026-10-07",
58
+ sections: [
59
+ {
60
+ title: "Added",
61
+ items: [
62
+ "The Trajectory tab offers a Table/Timeline switch: the timeline is an execution trace with lanes for the model, the tools and the user waits, plus tokens per turn and time per tool.",
63
+ "The file panel adds a right-click menu to copy a file's absolute path, relative path or name, and previews PDFs and HTML (with a source toggle for HTML).",
64
+ "Icon-theme plugins: choose one in Settings, Appearance (for example the Material Icon Theme plugin).",
65
+ ],
66
+ },
67
+ ],
68
+ },
5
69
  {
6
70
  version: "0.4.5",
7
71
  date: "2026-10-07",