@vascend/talos 0.1.4 → 0.1.6

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 (251) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +37 -12
  3. package/dist/core/agent.d.ts +16 -3
  4. package/dist/core/agent.js +170 -70
  5. package/dist/core/background.d.ts +10 -3
  6. package/dist/core/background.js +55 -9
  7. package/dist/core/bootstrap.js +1 -0
  8. package/dist/core/compaction.js +1 -1
  9. package/dist/core/config-storage.d.ts +5 -0
  10. package/dist/core/config-storage.js +83 -0
  11. package/dist/core/config.d.ts +11 -2
  12. package/dist/core/config.js +24 -8
  13. package/dist/core/context-ledger.js +2 -1
  14. package/dist/core/context-policy.d.ts +1 -1
  15. package/dist/core/context-policy.js +10 -7
  16. package/dist/core/credentials.js +29 -30
  17. package/dist/core/effort.d.ts +1 -1
  18. package/dist/core/effort.js +5 -1
  19. package/dist/core/execution-guidance.d.ts +1 -1
  20. package/dist/core/execution-guidance.js +2 -0
  21. package/dist/core/file-coordination.d.ts +3 -0
  22. package/dist/core/file-coordination.js +28 -0
  23. package/dist/core/file-lease-store.d.ts +3 -0
  24. package/dist/core/file-lease-store.js +28 -12
  25. package/dist/core/fusion.d.ts +24 -0
  26. package/dist/core/fusion.js +80 -0
  27. package/dist/core/model-limits.d.ts +1 -0
  28. package/dist/core/model-limits.js +5 -1
  29. package/dist/core/permission-queue.js +15 -15
  30. package/dist/core/request-plan.js +4 -3
  31. package/dist/core/session-naming.d.ts +3 -0
  32. package/dist/core/session-naming.js +47 -0
  33. package/dist/core/session-usage.js +4 -1
  34. package/dist/core/session.d.ts +5 -2
  35. package/dist/core/session.js +22 -7
  36. package/dist/core/settings.d.ts +2 -0
  37. package/dist/core/settings.js +5 -16
  38. package/dist/core/step-policy.d.ts +18 -0
  39. package/dist/core/step-policy.js +6 -0
  40. package/dist/core/storage-health.d.ts +24 -0
  41. package/dist/core/storage-health.js +72 -0
  42. package/dist/core/tool-artifacts.d.ts +2 -0
  43. package/dist/core/tool-artifacts.js +21 -3
  44. package/dist/core/tool-result-index.js +29 -33
  45. package/dist/core/tool-result-store.js +6 -5
  46. package/dist/core/types.d.ts +26 -2
  47. package/dist/core/user-inbox.d.ts +9 -0
  48. package/dist/core/user-inbox.js +22 -3
  49. package/dist/core/user-input.js +1 -1
  50. package/dist/fusion/autopilot-fast.d.ts +59 -0
  51. package/dist/fusion/autopilot-fast.js +213 -0
  52. package/dist/fusion/autopilot.d.ts +75 -0
  53. package/dist/fusion/autopilot.js +308 -0
  54. package/dist/fusion/focus.d.ts +30 -0
  55. package/dist/fusion/focus.js +122 -0
  56. package/dist/fusion/gate.d.ts +30 -0
  57. package/dist/fusion/gate.js +95 -0
  58. package/dist/fusion/jev.d.ts +71 -0
  59. package/dist/fusion/jev.js +164 -0
  60. package/dist/fusion/receipts.d.ts +25 -0
  61. package/dist/fusion/receipts.js +34 -0
  62. package/dist/fusion/route.d.ts +25 -0
  63. package/dist/fusion/route.js +84 -0
  64. package/dist/fusion/run.d.ts +4 -0
  65. package/dist/fusion/run.js +186 -0
  66. package/dist/horizon/engine.d.ts +20 -1
  67. package/dist/horizon/engine.js +57 -0
  68. package/dist/horizon/index.d.ts +2 -2
  69. package/dist/horizon/index.js +1 -1
  70. package/dist/horizon/types.d.ts +29 -0
  71. package/dist/index.js +12 -0
  72. package/dist/providers/anthropic-oauth.d.ts +8 -3
  73. package/dist/providers/anthropic-oauth.js +13 -10
  74. package/dist/providers/anthropic.d.ts +9 -2
  75. package/dist/providers/anthropic.js +64 -21
  76. package/dist/providers/cli-models.d.ts +30 -0
  77. package/dist/providers/cli-models.js +194 -0
  78. package/dist/providers/model-effort.js +1 -1
  79. package/dist/providers/openai-oauth.d.ts +8 -3
  80. package/dist/providers/openai-oauth.js +9 -3
  81. package/dist/providers/openrouter-manual-fallback.d.ts +3 -1
  82. package/dist/providers/openrouter-manual-fallback.js +20 -20
  83. package/dist/providers/registry.js +3 -2
  84. package/dist/providers/retry.d.ts +3 -0
  85. package/dist/providers/retry.js +44 -1
  86. package/dist/remote/cifratura.d.ts +12 -0
  87. package/dist/remote/cifratura.js +66 -0
  88. package/dist/remote/index.d.ts +15 -0
  89. package/dist/remote/index.js +65 -0
  90. package/dist/remote/relay-client.d.ts +58 -0
  91. package/dist/remote/relay-client.js +315 -0
  92. package/dist/remote/sessione-remota.d.ts +60 -0
  93. package/dist/remote/sessione-remota.js +266 -0
  94. package/dist/sdk.d.ts +7 -2
  95. package/dist/sdk.js +3 -1
  96. package/dist/tools/builtin.js +41 -20
  97. package/dist/tools/capabilities.d.ts +3 -1
  98. package/dist/tools/capabilities.js +5 -2
  99. package/dist/tools/computer-use.js +2 -1
  100. package/dist/tools/pagination.js +1 -1
  101. package/dist/tools/program.js +34 -21
  102. package/dist/tools/team-tools.js +11 -6
  103. package/dist/tui/account-entry.js +14 -9
  104. package/dist/tui/agent-label.d.ts +2 -0
  105. package/dist/tui/agent-label.js +5 -0
  106. package/dist/tui/agent-sessions-screen.d.ts +35 -0
  107. package/dist/tui/agent-sessions-screen.js +178 -0
  108. package/dist/tui/agent-sessions.d.ts +53 -0
  109. package/dist/tui/agent-sessions.js +236 -0
  110. package/dist/tui/bottom-input.d.ts +57 -2
  111. package/dist/tui/bottom-input.js +449 -54
  112. package/dist/tui/branding.js +8 -4
  113. package/dist/tui/capability-lists.d.ts +10 -0
  114. package/dist/tui/capability-lists.js +41 -0
  115. package/dist/tui/command-output-registry.js +4 -0
  116. package/dist/tui/command-transcript.d.ts +1 -1
  117. package/dist/tui/command-transcript.js +17 -21
  118. package/dist/tui/commands.js +6 -2
  119. package/dist/tui/compaction-status.d.ts +7 -0
  120. package/dist/tui/compaction-status.js +39 -0
  121. package/dist/tui/config-recovery.d.ts +12 -0
  122. package/dist/tui/config-recovery.js +54 -0
  123. package/dist/tui/context-meter.d.ts +3 -0
  124. package/dist/tui/context-meter.js +35 -8
  125. package/dist/tui/diff.d.ts +9 -0
  126. package/dist/tui/diff.js +43 -19
  127. package/dist/tui/fusion-menu.d.ts +5 -0
  128. package/dist/tui/fusion-menu.js +89 -0
  129. package/dist/tui/i18n-catalog.js +351 -2
  130. package/dist/tui/live-command.d.ts +1 -0
  131. package/dist/tui/live-command.js +9 -0
  132. package/dist/tui/live-output.d.ts +1 -0
  133. package/dist/tui/live-output.js +11 -0
  134. package/dist/tui/logo.js +3 -0
  135. package/dist/tui/model-details.js +13 -12
  136. package/dist/tui/model-picker.d.ts +2 -0
  137. package/dist/tui/model-picker.js +59 -0
  138. package/dist/tui/model-selection.js +19 -10
  139. package/dist/tui/native-scrollback.d.ts +2 -1
  140. package/dist/tui/native-scrollback.js +31 -6
  141. package/dist/tui/prompt-queue-store.d.ts +1 -0
  142. package/dist/tui/prompt-queue-store.js +4 -0
  143. package/dist/tui/prompt-queue.d.ts +3 -0
  144. package/dist/tui/prompt-queue.js +19 -1
  145. package/dist/tui/provider-label.d.ts +2 -0
  146. package/dist/tui/provider-label.js +9 -0
  147. package/dist/tui/provider-menu.js +25 -24
  148. package/dist/tui/queue-screen.js +2 -1
  149. package/dist/tui/queued-questions.js +2 -4
  150. package/dist/tui/quick-help.d.ts +1 -1
  151. package/dist/tui/quick-help.js +3 -0
  152. package/dist/tui/remote-command.d.ts +7 -0
  153. package/dist/tui/remote-command.js +79 -0
  154. package/dist/tui/repl.d.ts +11 -2
  155. package/dist/tui/repl.js +457 -327
  156. package/dist/tui/review-store.d.ts +8 -1
  157. package/dist/tui/review-store.js +64 -11
  158. package/dist/tui/review-team.js +4 -3
  159. package/dist/tui/runtime-error.d.ts +2 -0
  160. package/dist/tui/runtime-error.js +26 -0
  161. package/dist/tui/scrollback-archive.d.ts +16 -0
  162. package/dist/tui/scrollback-archive.js +90 -0
  163. package/dist/tui/selector-mouse.d.ts +1 -0
  164. package/dist/tui/selector-mouse.js +1 -1
  165. package/dist/tui/selector.d.ts +11 -0
  166. package/dist/tui/selector.js +28 -4
  167. package/dist/tui/session-controls.d.ts +26 -0
  168. package/dist/tui/session-controls.js +65 -0
  169. package/dist/tui/session-naming-notice.d.ts +14 -0
  170. package/dist/tui/session-naming-notice.js +59 -0
  171. package/dist/tui/session-picker.d.ts +2 -0
  172. package/dist/tui/session-picker.js +32 -0
  173. package/dist/tui/session-replay.d.ts +1 -0
  174. package/dist/tui/session-replay.js +101 -32
  175. package/dist/tui/session-transcript-layout.d.ts +9 -0
  176. package/dist/tui/session-transcript-layout.js +41 -0
  177. package/dist/tui/startup.js +63 -33
  178. package/dist/tui/status-line.d.ts +1 -1
  179. package/dist/tui/status-line.js +20 -12
  180. package/dist/tui/storage-warning.d.ts +2 -0
  181. package/dist/tui/storage-warning.js +35 -0
  182. package/dist/tui/talos-art-data.d.ts +1 -0
  183. package/dist/tui/talos-art-data.js +2 -0
  184. package/dist/tui/talos-art.d.ts +1 -0
  185. package/dist/tui/talos-art.js +9 -0
  186. package/dist/tui/task-labels.d.ts +1 -0
  187. package/dist/tui/task-labels.js +8 -0
  188. package/dist/tui/terminal-text.d.ts +2 -1
  189. package/dist/tui/terminal-text.js +28 -2
  190. package/dist/tui/theme.js +2 -2
  191. package/dist/tui/tool-presentation.d.ts +3 -0
  192. package/dist/tui/tool-presentation.js +98 -0
  193. package/dist/tui/tool-style.js +4 -0
  194. package/dist/tui/transcript-store.d.ts +25 -0
  195. package/dist/tui/transcript-store.js +151 -0
  196. package/dist/tui/usage-footer.d.ts +1 -1
  197. package/dist/tui/usage-footer.js +3 -1
  198. package/dist/tui/vascend-id.d.ts +49 -0
  199. package/dist/tui/vascend-id.js +321 -0
  200. package/dist/tui/web-login-screen.js +4 -3
  201. package/dist/tui/web-login.d.ts +7 -15
  202. package/dist/tui/web-login.js +38 -78
  203. package/dist/tui/worker-transcripts.d.ts +31 -0
  204. package/dist/tui/worker-transcripts.js +91 -0
  205. package/dist/vascend/candidate-collector.d.ts +6 -5
  206. package/dist/vascend/candidate-collector.js +44 -43
  207. package/dist/vascend/context-feedback-loop.js +2 -0
  208. package/dist/vascend/context-lint.js +32 -2
  209. package/dist/vascend/context-market-selection.js +12 -5
  210. package/dist/vascend/context-pack-render.js +10 -4
  211. package/dist/vascend/context-packet-mirrors.d.ts +8 -0
  212. package/dist/vascend/context-packet-mirrors.js +24 -0
  213. package/dist/vascend/context-plan-events.d.ts +4 -0
  214. package/dist/vascend/context-plan-events.js +44 -0
  215. package/dist/vascend/context-snapshot.d.ts +1 -0
  216. package/dist/vascend/context-snapshot.js +8 -5
  217. package/dist/vascend/context-store-reader.d.ts +20 -0
  218. package/dist/vascend/context-store-reader.js +46 -0
  219. package/dist/vascend/context-window-compiler.d.ts +1 -0
  220. package/dist/vascend/context-window-compiler.js +1 -0
  221. package/dist/vascend/file-lock.d.ts +1 -0
  222. package/dist/vascend/file-lock.js +26 -0
  223. package/dist/vascend/granular-kingdom.d.ts +1 -0
  224. package/dist/vascend/granular-kingdom.js +52 -17
  225. package/dist/vascend/kingdom-bus.d.ts +1 -0
  226. package/dist/vascend/kingdom-bus.js +3 -0
  227. package/dist/vascend/kingdom-channel.d.ts +2 -0
  228. package/dist/vascend/kingdom-channel.js +3 -0
  229. package/dist/vascend/kingdom-live.d.ts +32 -4
  230. package/dist/vascend/kingdom-live.js +149 -13
  231. package/dist/vascend/kingdom-run.d.ts +5 -1
  232. package/dist/vascend/kingdom-run.js +6 -2
  233. package/dist/vascend/kingdom.d.ts +3 -0
  234. package/dist/vascend/kingdom.js +22 -11
  235. package/dist/vascend/memory-packet.d.ts +2 -1
  236. package/dist/vascend/memory-packet.js +31 -44
  237. package/dist/vascend/memory-store.d.ts +1 -0
  238. package/dist/vascend/memory-store.js +12 -4
  239. package/dist/vascend/plan-store.d.ts +3 -1
  240. package/dist/vascend/plan-store.js +6 -4
  241. package/dist/vascend/room-memory.d.ts +13 -2
  242. package/dist/vascend/room-memory.js +48 -6
  243. package/dist/vascend/runtime.d.ts +3 -11
  244. package/dist/vascend/runtime.js +11 -123
  245. package/dist/vascend/session-search.js +2 -0
  246. package/dist/vascend/team-session.js +3 -2
  247. package/dist/vascend/vision.d.ts +3 -1
  248. package/dist/vascend/vision.js +6 -4
  249. package/dist/vascend/worker-session.d.ts +20 -0
  250. package/dist/vascend/worker-session.js +83 -0
  251. package/package.json +2 -2
@@ -1,11 +1,13 @@
1
1
  import { contextLearningIdentity } from "../vascend/context-learning-identity.js";
2
2
  import { trackSessionUsage } from "./session-usage.js";
3
+ import { nameSession } from "./session-naming.js";
3
4
  import { quotaResetAt } from "../providers/quota-limits.js";
4
5
  import { RequestBudget } from "./request-plan.js";
5
- import { projectToolResult, visibleOutputPages } from "./tool-artifacts.js";
6
+ import { maxInputFor, resolveMaxTokens } from "./model-limits.js";
7
+ import { projectToolResult, visibleOutputPages, boundHistoricalToolResults } from "./tool-artifacts.js";
6
8
  import { contextWindowFor, estimateMessagesTokens, estimateTokens, estimateToolDefinitionTokens, IMAGE_TOKEN_ESTIMATE, planCompaction, planCompactionMove, protectedExcerpts, repairInstructions, summarizeMessages, verifySummary, withProtectedExcerpts, } from "./compaction.js";
7
9
  import { RoomMemory, formatRoomRecord } from "../vascend/room-memory.js";
8
- import {} from "./context-ledger.js";
10
+ import { buildContextLedger } from "./context-ledger.js";
9
11
  import { applyMediaEviction, applyToolResultDemotion, mediaBudgetPolicy, planMediaEviction, planToolResultDemotion, slimMessageForSession, toolIndexPolicy, EMPTY_PLAN, } from "./tool-result-index.js";
10
12
  import { createToolExecutor } from "./tool-executor.js";
11
13
  import { ToolOutputStore } from "./tool-result-store.js";
@@ -25,6 +27,7 @@ import { DecisionStore, promoteDecisions, decisionToRoomRecord } from "../vascen
25
27
  import { ContractStore, promoteContractChanges, contractToRoomRecord } from "../vascend/contract-memory.js";
26
28
  import { VascendPlanStore } from "../vascend/plan-store.js";
27
29
  import { pendingMembers, teamForSession, waitForTeamChange } from "../vascend/team-session.js";
30
+ import { resolveKingdomBus } from "../vascend/kingdom-channel.js";
28
31
  import { getKingdomScribe } from "../vascend/kingdom-scribe.js";
29
32
  import { toolCompressOptions } from "./tool-compress.js";
30
33
  import { routeEffort, isRoutineBatch, effectiveToolName, effortRoutingEnabled } from "./effort.js";
@@ -38,14 +41,6 @@ import { RunTiming } from "./run-timing.js";
38
41
  import { queuedConfirmation } from "./permission-queue.js";
39
42
  import { workspaceMessageText, workspacePeersContext } from "./workspace-sessions.js";
40
43
  const CONTEXT_READ_TOOLS = new Set(["read_file", "glob", "grep", "list_dir", "recall", "output_page", "vascend_status", "ram_status", "file_activity", "workspace_sessions", "tool_search", "tool_list", "tool_describe", "bash_output"]);
41
- // Tetto di sicurezza ai turni di un singolo giro (anti-loop). Alto di default
42
- // (i goal Danilov multi-castello richiedono molti turni) e configurabile via
43
- // TALOS_MAX_TURNS. Al raggiungimento NON e' un errore fatale: il giro termina e
44
- // la validazione del regno sveglia l'agente sulle stanze ancora al buio.
45
- function maxTurns(env) {
46
- const n = Number(env["TALOS_MAX_TURNS"]);
47
- return Number.isFinite(n) && n > 0 ? n : 200;
48
- }
49
44
  // Quante volte auto-continuare un output troncato (stop=max_tokens) prima di
50
45
  // arrendersi e chiedere di spezzare il compito. Configurabile via
51
46
  // TALOS_MAX_AUTOCONTINUE. 0 = disabilita l'auto-continuazione.
@@ -152,9 +147,13 @@ function coerceParam(raw) {
152
147
  return raw.replace(/^\n/, "").replace(/\n$/, "");
153
148
  }
154
149
  // Esegue un giro completo: dato l'input utente, alterna chiamate al modello
155
- // ed esecuzione di tool finche' il modello non si ferma (o si raggiunge il
156
- // limite di turni / l'utente interrompe via signal).
150
+ // ed esecuzione di tool finche' il modello non si ferma, si verifica un errore
151
+ // o l'utente interrompe via signal. Nessun tetto al numero di turni.
157
152
  export async function runTurn(deps, userInput, sink, signal) {
153
+ if (deps.config.fusion && !deps.kingdom) {
154
+ const { runFusionTurn } = await import("../fusion/run.js");
155
+ return runFusionTurn(deps, userInput, sink, signal, runTurn);
156
+ }
158
157
  // Un collaboratore costruito copiando AgentDeps non assume l'identita'
159
158
  // della sessione principale: il team ha gia' il proprio canale dedicato.
160
159
  const channel = deps.workspaceSession?.owns(deps.config.cwd, deps.session.id, deps.executionTarget)
@@ -165,17 +164,31 @@ export async function runTurn(deps, userInput, sink, signal) {
165
164
  const timing = new RunTiming(deps.config.sessionDir, deps.session.id, deps.env ?? process.env);
166
165
  const releaseStorage = protectSessionStorage(deps.config.sessionDir, [deps.session.id, deps.config.roomStoreId, deps.config.planStoreId].filter((value) => Boolean(value)), deps.env ?? process.env, deps.config.cwd);
167
166
  let failed = false;
167
+ const namingController = new AbortController();
168
+ const abortNaming = () => namingController.abort();
169
+ signal.addEventListener("abort", abortNaming, { once: true });
170
+ if (signal.aborted)
171
+ abortNaming();
172
+ const naming = (deps.env ?? process.env)["TALOS_AUTO_TITLE"] === "1" && !deps.kingdom
173
+ ? nameSession(deps, text, sink, namingController.signal) : Promise.resolve();
168
174
  try {
169
175
  await runAgentLoop({ ...deps, workspaceSession: channel,
170
176
  provider: trackSessionUsage(timing.provider(deps.provider), deps.config.sessionDir, deps.session.id, sink),
171
177
  confirm: (action, detail) => timing.wait("permission", () => queuedConfirmation(deps.confirm, action, detail, signal)),
172
- }, userInput, event => { timing.event(event); sink(event); }, signal);
178
+ }, userInput, event => {
179
+ if (event.type === "status" && event.label.startsWith("Limite sessione di 5 ore raggiunto"))
180
+ abortNaming();
181
+ timing.event(event);
182
+ sink(event);
183
+ }, signal);
173
184
  }
174
185
  catch (error) {
175
186
  failed = true;
176
187
  throw error;
177
188
  }
178
189
  finally {
190
+ await naming;
191
+ signal.removeEventListener("abort", abortNaming);
179
192
  timing.finish(signal.aborted, failed);
180
193
  channel?.update({ state: "idle" });
181
194
  releaseStorage();
@@ -257,6 +270,7 @@ userInput, sink, signal) {
257
270
  // recall plan, supersession). Fallback sicuro al Memory Packet grezzo se il
258
271
  // compiler fallisce per qualsiasi motivo, cosi' il turno non si rompe mai.
259
272
  const memoryScopeId = config.roomStoreId ?? session.id;
273
+ const sharedProjectMemory = env["TALOS_SHARED_PROJECT_MEMORY"] === "1";
260
274
  if (tools.compact) {
261
275
  runtimeSystem += `\n\n${COMPACT_TOOL_GUIDANCE}`;
262
276
  systemBlocks.push({ key: "tool-protocol", label: "Protocollo strumenti", text: COMPACT_TOOL_GUIDANCE, cacheable: true });
@@ -332,6 +346,10 @@ userInput, sink, signal) {
332
346
  systemBlocks.splice(stableBlockCount);
333
347
  turnBlocks.length = 0;
334
348
  turnBlockRecords.length = 0;
349
+ if (config.turnSystem) {
350
+ turnBlocks.push(config.turnSystem);
351
+ turnBlockRecords.push({ key: "host-instructions", label: "Istruzioni operative dell'host", text: config.turnSystem, cacheable: false });
352
+ }
335
353
  if (persistenceReminder)
336
354
  addTurnBlock(persistenceReminder, { key: "nudge", label: "Promemoria di persistenza" });
337
355
  // Il contesto dell'host non e' parte del system prompt ne' della cronologia:
@@ -352,12 +370,21 @@ userInput, sink, signal) {
352
370
  cwd: config.cwd,
353
371
  selection: config.vibeMode,
354
372
  userText: latestUserText,
355
- inheritProjectFrame: sessionWasEmpty,
373
+ inheritProjectFrame: sharedProjectMemory && sessionWasEmpty,
374
+ });
375
+ const inheritedFrame = fabric.frame.receipt?.inherited === true;
376
+ // Su una sessione vuota il frame appena creato deriva interamente dal
377
+ // messaggio corrente: reinviarlo come Fabric ripeterebbe lo stesso prompt
378
+ // in missione, contratto e revisioni. Il frame viene comunque persistito
379
+ // e sara' disponibile dal follow-up successivo.
380
+ const fabricBlock = horizonHost
381
+ ? renderHorizonFabricBlock(fabric)
382
+ : sessionWasEmpty && !inheritedFrame ? "" : fabric.systemBlock;
383
+ if (fabricBlock)
384
+ addTurnBlock(fabricBlock, {
385
+ key: "fabric",
386
+ label: "Frame di progetto (Context Fabric)",
356
387
  });
357
- addTurnBlock(horizonHost ? renderHorizonFabricBlock(fabric) : fabric.systemBlock, {
358
- key: "fabric",
359
- label: "Frame di progetto (Context Fabric)",
360
- });
361
388
  }
362
389
  catch {
363
390
  // Context Fabric e' un acceleratore, non un single point of failure: uno
@@ -379,6 +406,7 @@ userInput, sink, signal) {
379
406
  compileBase = {
380
407
  sessionDir: config.sessionDir,
381
408
  scopeId: memoryScopeId,
409
+ sharedProjectMemory,
382
410
  ...(config.planStoreId ? { planScopeId: config.planStoreId } : {}),
383
411
  userText: fabric?.rawUserText ?? latestUserText,
384
412
  capsuleMarket: capsuleMarketEnabled(env),
@@ -395,6 +423,7 @@ userInput, sink, signal) {
395
423
  try {
396
424
  snapshot = loadContextStoreSnapshot({
397
425
  sessionDir: config.sessionDir, scopeId: memoryScopeId,
426
+ sharedProjectMemory,
398
427
  indexed: env["TALOS_CONTEXT_INDEX"] !== "0",
399
428
  ...(config.planStoreId ? { planScopeId: config.planStoreId } : {}),
400
429
  ...(fabric ? { projectKey: fabric.frame.projectKey } : {}),
@@ -462,12 +491,12 @@ userInput, sink, signal) {
462
491
  // dal turno) e fonde la config migliore nella baseline del progetto. Effetto
463
492
  // collaterale utile: lascia lo snapshot degli store caldo per il turno dopo.
464
493
  let memoryPacket = "";
465
- if (!contextBlock || horizonHost) {
494
+ if (!contextBlock || horizonHost || sessionWasEmpty) {
466
495
  memoryPacket = compileInput.snapshot.packet(12_000);
467
496
  }
468
497
  if (!contextBlock)
469
498
  contextBlock = memoryPacket;
470
- if (horizonHost && !shouldIncludeHorizonContextPack({
499
+ if (!shouldIncludeHorizonContextPack({
471
500
  sessionWasEmpty,
472
501
  inheritedFrame: fabric?.frame.receipt?.inherited === true,
473
502
  hasCompiledEvidence: Boolean(compiledContext?.report.cleaned.length),
@@ -512,6 +541,9 @@ userInput, sink, signal) {
512
541
  // Il blocco NON viene persistito nella sessione: la cronologia su disco resta
513
542
  // pulita e il prefisso inviato al modello resta byte-identico fra i turni.
514
543
  const turnMessage = session.append({ role: "user", content: userParts }).message;
544
+ if (deps.inputMessage)
545
+ sink({ type: "user_message", id: deps.inputMessage.id, text: userText,
546
+ at: Date.now(), ...(deps.inputMessage.immediate ? { immediate: true } : {}) });
515
547
  await host?.emit("agent_start", { input: userText });
516
548
  // Prende in carico i messaggi arrivati durante il turno: li appende come
517
549
  // messaggi utente veri (quindi entrano nel contesto del giro successivo) e
@@ -519,6 +551,7 @@ userInput, sink, signal) {
519
551
  // l'interfaccia mostra come "letto". Ritorna true se ne ha consumato almeno
520
552
  // uno, cosi' il chiamante sa che c'e' lavoro nuovo da servire.
521
553
  const receivedPeerMessages = new Set();
554
+ const notifiedTeamMessages = new Set();
522
555
  const consumeInbox = () => {
523
556
  const arrivals = deps.inbox?.drain() ?? [];
524
557
  if (arrivals.length) {
@@ -553,7 +586,6 @@ userInput, sink, signal) {
553
586
  }
554
587
  return arrivals.length > 0 || peerArrivals > 0;
555
588
  };
556
- const turnCap = maxTurns(env);
557
589
  const requestBudget = new RequestBudget();
558
590
  const toolCompress = toolCompressOptions(env);
559
591
  // Archivio dei tool result: gli output voluminosi vengono indicizzati appena
@@ -577,7 +609,7 @@ userInput, sink, signal) {
577
609
  // Effort routing: i turni di routine (digestione di letture) abbassano il
578
610
  // reasoning; il primo turno e quelli con scrittura/errore tengono l'effort pieno.
579
611
  const baseEffort = (config.effort ?? "");
580
- const effortRouting = effortRoutingEnabled(env);
612
+ const effortRouting = effortRoutingEnabled(env, provider.id);
581
613
  let routineBatch = false;
582
614
  // Auto-continuazione su output troncato: se il modello si ferma per
583
615
  // max_tokens (risposta tagliata al tetto), invece di chiudere la stanza con
@@ -588,6 +620,7 @@ userInput, sink, signal) {
588
620
  const completionRepairCap = maxCompletionRepairs(env);
589
621
  let emptyResponseRepairs = 0;
590
622
  let transientGuardContext = "";
623
+ let stepDirective = null;
591
624
  let planCompletionRepairs = 0;
592
625
  let planTouchedThisRun = false;
593
626
  let delegationObserved = false;
@@ -619,7 +652,28 @@ userInput, sink, signal) {
619
652
  ...openContractSymbols(deps),
620
653
  ].filter(Boolean)),
621
654
  ].slice(0, MAX_COMPACTION_ANCHORS);
622
- for (let turn = 0; turn < turnCap; turn += 1) {
655
+ for (let turn = 0;; turn += 1) {
656
+ if (signal.aborted) {
657
+ await archiveCurrentRoom();
658
+ sink({ type: "error", message: "interrotto" });
659
+ await host?.emit("agent_end", {});
660
+ return;
661
+ }
662
+ // I comandi manuali mutano la cronologia solo dopo un batch completo di
663
+ // tool, mai durante provider/tool concorrenti. Non diventano prompt finti.
664
+ const compactions = deps.inbox?.drainCompactions?.() ?? [];
665
+ for (const request of compactions) {
666
+ try {
667
+ const compacted = await compactNow(deps, signal, sink, request.instructions, compactionAnchors());
668
+ contextDirty ||= compacted;
669
+ sink({ type: "status", category: "context", label: compacted
670
+ ? "Contesto compattato; proseguo nella stessa sessione."
671
+ : "Nessuna compattazione necessaria; contesto conservato." });
672
+ }
673
+ catch (err) {
674
+ sink({ type: "status", category: "context", label: `Compattazione non riuscita: ${err.message}; contesto conservato.` });
675
+ }
676
+ }
623
677
  if (signal.aborted) {
624
678
  await archiveCurrentRoom();
625
679
  sink({ type: "error", message: "interrotto" });
@@ -630,6 +684,7 @@ userInput, sink, signal) {
630
684
  if (env["TALOS_CONTEXT_LIVE"] !== "0") {
631
685
  try {
632
686
  const currentSnapshot = loadContextStoreSnapshot({ sessionDir: config.sessionDir, scopeId: memoryScopeId,
687
+ sharedProjectMemory,
633
688
  ...(config.planStoreId ? { planScopeId: config.planStoreId } : {}),
634
689
  ...(fabric ? { projectKey: fabric.frame.projectKey } : {}), indexed: env["TALOS_CONTEXT_INDEX"] !== "0" });
635
690
  contextDirty ||= currentSnapshot.signature !== contextSignature;
@@ -650,7 +705,7 @@ userInput, sink, signal) {
650
705
  // persisted, including nested execute calls and updates from collaborators.
651
706
  if (completed.length && config.compaction.enabled && config.compaction.perRoom
652
707
  && policy.roomCompaction !== "never") {
653
- const limits = workingContextLimits(policy, contextWindowFor(config.model, config.contextWindow), config.compaction.reserveTokens);
708
+ const limits = workingContextLimits(policy, contextWindowFor(config.model, config.contextWindow), config.compaction.reserveTokens, resolveMaxTokens(config.maxTokens, config.model), maxInputFor(config.model));
654
709
  const activeTokens = contextTokens(deps, runtimeSystem) + estimateTokens(turnContext)
655
710
  - planDemotion(session.history()).freedTokens
656
711
  - planMediaEviction({ messages: session.history(), policy: mediaBudget }).images * IMAGE_TOKEN_ESTIMATE;
@@ -683,6 +738,7 @@ userInput, sink, signal) {
683
738
  // successivo la sessione espone soltanto il riferimento testuale. Senza
684
739
  // questa proiezione ogni nuova richiesta reinviava lo stesso base64 (e gli
685
740
  // SVG storici potevano far fallire Anthropic per dimensioni nominali >8k).
741
+ messages = boundHistoricalToolResults(messages, outputStore, toolCompress);
686
742
  messages = projectHistoricalPromptImages(messages, turnMessage);
687
743
  // Budget delle immagini: le schermate vecchie lasciano la finestra. Prima
688
744
  // della retrocessione del testo, perche' e' la voce piu' pesante di tutte
@@ -753,7 +809,10 @@ userInput, sink, signal) {
753
809
  : {}),
754
810
  };
755
811
  let streamedText = "";
756
- const requestTurnContext = [turnContext, scopeGuidance, transientGuardContext, peerContext].filter(Boolean).join("\n\n");
812
+ stepDirective = deps.stepPolicy
813
+ ? await deps.stepPolicy.beforeStep({ step: turn, tools: tools.names(), signal }).catch(() => null) : null;
814
+ const requestTurnContext = [turnContext, scopeGuidance, transientGuardContext, peerContext, stepDirective?.guidance]
815
+ .filter(Boolean).join("\n\n");
757
816
  // I guard interni valgono per una sola richiesta provider e non devono
758
817
  // entrare nella cronologia come messaggi utente.
759
818
  transientGuardContext = "";
@@ -776,6 +835,7 @@ userInput, sink, signal) {
776
835
  onActivity: (label) => sink({ type: "status", label }),
777
836
  onReasoning: (delta) => sink({ type: "reasoning", delta }),
778
837
  }, ledgerInput);
838
+ sessionContextFrames.set(session, { ...ledgerInput, messages: undefined, now: undefined });
779
839
  sink({ type: "context_window", ledger: requestPlan.ledger });
780
840
  if (!requestPlan.fits) {
781
841
  sink({ type: "error", message: "Il contesto supera la finestra del modello anche dopo la compattazione. La sessione resta recuperabile." });
@@ -901,12 +961,21 @@ userInput, sink, signal) {
901
961
  // Ultimo controllo della casella prima di chiudere: se l'utente ha scritto
902
962
  // mentre il modello stava componendo la risposta, il turno prosegue invece
903
963
  // di terminare e costringerlo a un giro nuovo.
904
- if (consumeInbox())
964
+ if (consumeInbox() || deps.inbox?.pendingCompactions)
905
965
  continue;
906
966
  const activeTeam = delegationObserved ? teamForSession(session.id) : null;
907
967
  if (activeTeam && !activeTeam.done) {
908
968
  sink({ type: "status", label: `team al lavoro: ${pendingMembers(activeTeam).join(", ")}; attendo avanzamento` });
909
- await waitForTeamChange(activeTeam, signal, () => Boolean(deps.inbox?.pending));
969
+ const teamBus = resolveKingdomBus(process.env, config.sessionDir);
970
+ await waitForTeamChange(activeTeam, signal, () => {
971
+ if (deps.inbox?.pending || deps.inbox?.pendingCompactions)
972
+ return true;
973
+ const unread = teamBus.peekInbox(activeTeam.id, deps.kingdom?.self || "orchestratore")
974
+ .filter(message => !notifiedTeamMessages.has(`${activeTeam.id}:${message.seq}`));
975
+ for (const message of unread)
976
+ notifiedTeamMessages.add(`${activeTeam.id}:${message.seq}`);
977
+ return unread.length > 0;
978
+ });
910
979
  transientGuardContext = "[TALOS_TEAM_GUARD]\nIl team avviato deve ancora essere raccolto e verificato. "
911
980
  + "Leggi team_report e team_inbox, rispondi ai blocchi e verifica i risultati prima della consegna. "
912
981
  + "Non dichiarare concluso il lavoro solo perche' l'ingaggio e' partito.";
@@ -1042,6 +1111,10 @@ userInput, sink, signal) {
1042
1111
  // a indice di stanze, cosi' resta sempre compatto. Il dettaglio si
1043
1112
  // recupera col tool recall.
1044
1113
  await archiveCurrentRoom();
1114
+ // L'archiviazione puo' attendere I/O: non perdere un invio o comando
1115
+ // arrivato dopo il primo controllo della finale e prima del done.
1116
+ if (consumeInbox() || deps.inbox?.pendingCompactions)
1117
+ continue;
1045
1118
  sink({ type: "done" });
1046
1119
  await host?.emit("agent_end", {});
1047
1120
  return;
@@ -1049,6 +1122,7 @@ userInput, sink, signal) {
1049
1122
  // Esegue i tool richiesti e raccoglie i risultati in un turno "tool".
1050
1123
  const ctx = {
1051
1124
  cwd: config.cwd,
1125
+ waitInterrupted: () => Boolean(deps.inbox?.pending || deps.inbox?.pendingCompactions),
1052
1126
  ...(deps.workspaceSession ? { workspaceSession: deps.workspaceSession } : {}),
1053
1127
  ...(deps.executionTarget ? { executionTarget: deps.executionTarget } : {}),
1054
1128
  ...(deps.fsRoots !== undefined ? { fsRoots: deps.fsRoots } : {}),
@@ -1077,6 +1151,9 @@ userInput, sink, signal) {
1077
1151
  const executeCall = createToolExecutor(ctx, {
1078
1152
  onCall: (call, parentId) => sink({ type: "tool_call", call, ...(parentId ? { parentId } : {}) }),
1079
1153
  before: async (call) => {
1154
+ if (stepDirective?.admit && !stepDirective.admit(call.name)) {
1155
+ return { block: true, reason: stepDirective.rejection ?? "tool non ammesso in questo passo" };
1156
+ }
1080
1157
  let blocked;
1081
1158
  for (const output of await host?.emit("tool_call", { toolName: call.name, input: { ...call.input } }) ?? []) {
1082
1159
  if (output.block)
@@ -1127,6 +1204,9 @@ userInput, sink, signal) {
1127
1204
  results.push(part);
1128
1205
  sink({ type: "tool_result", result: part });
1129
1206
  }
1207
+ deps.stepPolicy?.afterStep?.({ step: turn, final: false,
1208
+ tools: toolCalls.map(call => effectiveToolName(call.name, call.input)),
1209
+ errors: results.filter(result => result.isError).length });
1130
1210
  if (toolCalls.some((call, index) => PLAN_COMPLETION_TOOLS.has(effectiveToolName(call.name, call.input))
1131
1211
  && results[index]?.isError !== true)) {
1132
1212
  planTouchedThisRun = true;
@@ -1196,9 +1276,6 @@ Il piano ha ${collaborators.length} filoni `
1196
1276
  transientGuardContext = [transientGuardContext, hint].filter(Boolean).join("\n\n");
1197
1277
  }
1198
1278
  }
1199
- await host?.emit("agent_end", {});
1200
- await archiveCurrentRoom();
1201
- sink({ type: "error", message: `pausa al tetto di ${turnCap} turni per questo giro (TALOS_MAX_TURNS per alzarlo); la validazione del piano riprende le attivita rimaste` });
1202
1279
  }
1203
1280
  // Archivia la stanza appena illuminata: costruisce il record Danilov dei nodi
1204
1281
  // di questo turno e lo persiste nella memoria delle stanze. L'archiviazione e'
@@ -1270,7 +1347,7 @@ async function archiveRoom(deps, roomStartHead, sink, policy, signal, anchors) {
1270
1347
  const firstKeptId = head;
1271
1348
  if (policy.roomCompaction === "threshold") {
1272
1349
  const window = contextWindowFor(config.model, config.contextWindow);
1273
- const limits = workingContextLimits(policy, window, config.compaction.reserveTokens);
1350
+ const limits = workingContextLimits(policy, window, config.compaction.reserveTokens, resolveMaxTokens(config.maxTokens, config.model), maxInputFor(config.model));
1274
1351
  if (projectedContextTokens(deps) <= limits.checkpoint)
1275
1352
  return;
1276
1353
  await compactNow(deps, signal, sink, undefined, anchors);
@@ -1622,6 +1699,9 @@ function projectCompactionMessages(deps, messages) {
1622
1699
  // Compattazione gia' avviata e non ancora conclusa, per sessione. Single-flight:
1623
1700
  // due turni ravvicinati non devono riassumere due volte la stessa cronologia.
1624
1701
  const pendingCompaction = new WeakMap();
1702
+ // Ultima composizione runtime: una compattazione idle non deve misurare solo
1703
+ // la cronologia o usare l'usage cumulativo del provider come finestra attiva.
1704
+ const sessionContextFrames = new WeakMap();
1625
1705
  // Compatta se la soglia e' superata. No-op se disabilitata o non necessario.
1626
1706
  //
1627
1707
  // La compattazione e' il nodo piu' costoso del giro: una chiamata al modello
@@ -1642,7 +1722,7 @@ savedTokens = 0,
1642
1722
  system = deps.config.system) {
1643
1723
  const { compaction } = deps.config;
1644
1724
  const inFlight = pendingCompaction.get(deps.session);
1645
- const limits = workingContextLimits(contextPolicy(deps.env ?? process.env), contextWindowFor(deps.config.model, deps.config.contextWindow), compaction.reserveTokens);
1725
+ const limits = workingContextLimits(contextPolicy(deps.env ?? process.env), contextWindowFor(deps.config.model, deps.config.contextWindow), compaction.reserveTokens, resolveMaxTokens(deps.config.maxTokens, deps.config.model), maxInputFor(deps.config.model));
1646
1726
  const move = planCompactionMove({
1647
1727
  enabled: compaction.enabled,
1648
1728
  // Il pack compilato e il contesto IDE sono effimeri e quindi non vivono
@@ -1697,8 +1777,8 @@ export async function compactNow(deps, signal, sink, instructions, anchors = [])
1697
1777
  async function performCompaction(deps, signal, sink, instructions, anchors = []) {
1698
1778
  const { session, config } = deps;
1699
1779
  const provider = trackSessionUsage(deps.provider, config.sessionDir, session.id, sink);
1700
- const fullChain = session.nodeChain();
1701
1780
  const previousCompaction = session.lastCompaction;
1781
+ const fullChain = session.nodeChain(previousCompaction?.firstKeptId);
1702
1782
  const liveStart = previousCompaction
1703
1783
  ? fullChain.findIndex((node) => node.id === previousCompaction.firstKeptId)
1704
1784
  : 0;
@@ -1727,52 +1807,72 @@ async function performCompaction(deps, signal, sink, instructions, anchors = [])
1727
1807
  signal,
1728
1808
  ...(previousCompaction?.summary && liveStart >= 0 ? { previousSummary: previousCompaction.summary } : {}),
1729
1809
  };
1730
- let summary = await summarizeMessages(provider, plan.toSummarize, {
1731
- ...base,
1732
- ...(summaryInstructions ? { instructions: summaryInstructions } : {}),
1733
- });
1734
- if (!summary)
1735
- return false;
1736
- let missing = verifySummary(summary, anchors, plan.toKeep);
1737
- if (missing.length) {
1738
- const repaired = await summarizeMessages(provider, plan.toSummarize, {
1810
+ sink({ type: "compacting" });
1811
+ try {
1812
+ let summary = await summarizeMessages(provider, plan.toSummarize, {
1739
1813
  ...base,
1740
- instructions: repairInstructions(missing, summaryInstructions),
1814
+ ...(summaryInstructions ? { instructions: summaryInstructions } : {}),
1741
1815
  });
1742
- if (repaired) {
1743
- const stillMissing = verifySummary(repaired, anchors, plan.toKeep);
1744
- // Si tiene il tentativo migliore: quello che perde meno ancore.
1745
- if (stillMissing.length <= missing.length) {
1746
- summary = repaired;
1747
- missing = stillMissing;
1748
- }
1749
- }
1750
- }
1751
- // Ultima rete: cio' che il modello continua a non dire lo diciamo noi, con
1752
- // le sue stesse parole. Nelle compattazioni successive il summary precedente
1753
- // precede la live chain: protectedExcerpts cerca dall'ultimo elemento, quindi
1754
- // il dettaglio live piu' recente vince e il summary resta il fallback storico.
1755
- if (missing.length) {
1816
+ if (!summary)
1817
+ return false;
1818
+ let missing = verifySummary(summary, anchors, plan.toKeep);
1756
1819
  const excerptSources = [
1757
1820
  ...(previousCompaction?.summary && liveStart >= 0
1758
1821
  ? [{ role: "user", content: [{ type: "text", text: previousCompaction.summary }] }]
1759
1822
  : []),
1760
1823
  ...plan.toSummarize,
1761
1824
  ];
1762
- summary = withProtectedExcerpts(summary, protectedExcerpts(excerptSources, missing));
1825
+ if (missing.length) {
1826
+ // Repair the existing summary with the missing evidence instead of
1827
+ // sending the entire archived transcript to the model a second time.
1828
+ const repairEvidence = [{ role: "user", content: [{
1829
+ type: "text", text: protectedExcerpts(excerptSources, missing).join("\n"),
1830
+ }] }];
1831
+ const repaired = await summarizeMessages(provider, repairEvidence, {
1832
+ ...base,
1833
+ previousSummary: summary,
1834
+ instructions: repairInstructions(missing, summaryInstructions),
1835
+ });
1836
+ if (repaired) {
1837
+ const stillMissing = verifySummary(repaired, anchors, plan.toKeep);
1838
+ // Si tiene il tentativo migliore: quello che perde meno ancore.
1839
+ if (stillMissing.length <= missing.length) {
1840
+ summary = repaired;
1841
+ missing = stillMissing;
1842
+ }
1843
+ }
1844
+ }
1845
+ // Ultima rete: cio' che il modello continua a non dire lo diciamo noi, con
1846
+ // le sue stesse parole. Nelle compattazioni successive il summary precedente
1847
+ // precede la live chain: protectedExcerpts cerca dall'ultimo elemento, quindi
1848
+ // il dettaglio live piu' recente vince e il summary resta il fallback storico.
1849
+ if (missing.length) {
1850
+ summary = withProtectedExcerpts(summary, protectedExcerpts(excerptSources, missing));
1851
+ }
1852
+ // A speculative summary may finish after new messages arrive. Keep that tail
1853
+ // intact, but reject a result for a branch that is no longer current.
1854
+ if (signal.aborted || session.lastCompaction !== previousCompaction
1855
+ || !session.nodeChain(snapshotHead ?? undefined).some(node => node.id === snapshotHead))
1856
+ return false;
1857
+ const checkpoint = compactionCheckpoint(checkpointTasks(config, session.id, deps.kingdom?.self), messages, plan.cutIndex);
1858
+ if (checkpoint)
1859
+ summary += `\n\n${checkpoint}`;
1860
+ session.compact(summary, firstKept.id, tokensBefore);
1861
+ const tokensAfter = projectedContextTokens(deps);
1862
+ sink({ type: "compacted", tokensBefore, tokensAfter, reason: "summary", ...(anchors.length ? { anchors: [...anchors] } : {}) });
1863
+ const frame = sessionContextFrames.get(session);
1864
+ sink({ type: "context_window", ledger: buildContextLedger({
1865
+ model: config.model, contextWindow: config.contextWindow, reserveTokens: config.compaction.reserveTokens,
1866
+ workingContextTokens: contextPolicy(deps.env ?? process.env).workingContextTokens,
1867
+ system: config.system, ...frame, tools: deps.tools.defs(),
1868
+ messages: projectCompactionMessages(deps, session.history()), demoted: null,
1869
+ compaction: { ts: session.lastCompaction.ts, tokensBefore, summary },
1870
+ }) });
1871
+ return true;
1872
+ }
1873
+ finally {
1874
+ sink({ type: "compaction_end" });
1763
1875
  }
1764
- // A speculative summary may finish after new messages arrive. Keep that tail
1765
- // intact, but reject a result for a branch that is no longer current.
1766
- if (signal.aborted || session.lastCompaction !== previousCompaction
1767
- || !session.nodeChain().some(node => node.id === snapshotHead))
1768
- return false;
1769
- const checkpoint = compactionCheckpoint(checkpointTasks(config, session.id, deps.kingdom?.self), messages, plan.cutIndex);
1770
- if (checkpoint)
1771
- summary += `\n\n${checkpoint}`;
1772
- session.compact(summary, firstKept.id, tokensBefore);
1773
- const tokensAfter = projectedContextTokens(deps);
1774
- sink({ type: "compacted", tokensBefore, tokensAfter, reason: "summary", ...(anchors.length ? { anchors: [...anchors] } : {}) });
1775
- return true;
1776
1876
  }
1777
1877
  // Modello del compattatore: riassumere e' un lavoro meccanico e non ha bisogno
1778
1878
  // del modello di punta. TALOS_COMPACT_MODEL permette di dirottarlo su uno piu'
@@ -13,14 +13,16 @@ export interface BgTask {
13
13
  }
14
14
  interface Foreground {
15
15
  readonly taskId: string;
16
+ readonly sessionId?: string;
16
17
  background(): void;
17
18
  }
18
19
  export declare class BackgroundManager {
19
20
  private readonly tasks;
20
21
  private readonly children;
21
22
  private readonly order;
23
+ private readonly completedOrder;
22
24
  private seq;
23
- private fg;
25
+ private readonly foregrounds;
24
26
  private readonly changeCbs;
25
27
  private readonly finishCbs;
26
28
  createTask(command: string): BgTask;
@@ -29,11 +31,16 @@ export declare class BackgroundManager {
29
31
  finishTask(id: string, status: BgStatus, exitCode?: number): void;
30
32
  setForeground(fg: Foreground): void;
31
33
  clearForeground(id?: string): void;
32
- hasForeground(): boolean;
34
+ private foreground;
35
+ hasForeground(sessionId?: string): boolean;
33
36
  markDetached(id: string): void;
34
- requestBackground(): BgTask | null;
37
+ requestBackground(sessionId?: string): BgTask | null;
35
38
  kill(id: string): boolean;
36
39
  get(id: string): BgTask | undefined;
40
+ waitForTask(id: string, signal: AbortSignal, options?: {
41
+ readonly timeoutMs?: number;
42
+ readonly interrupted?: () => boolean;
43
+ }): Promise<void>;
37
44
  list(): readonly BgTask[];
38
45
  running(): readonly BgTask[];
39
46
  onChange(cb: () => void): () => void;
@@ -25,8 +25,9 @@ export class BackgroundManager {
25
25
  tasks = new Map();
26
26
  children = new Map();
27
27
  order = [];
28
+ completedOrder = [];
28
29
  seq = 0;
29
- fg = null;
30
+ foregrounds = new Map();
30
31
  changeCbs = new Set();
31
32
  finishCbs = new Set();
32
33
  createTask(command) {
@@ -56,6 +57,7 @@ export class BackgroundManager {
56
57
  t.endedAt = Date.now();
57
58
  if (exitCode !== undefined)
58
59
  t.exitCode = exitCode;
60
+ this.completedOrder.push(id);
59
61
  this.children.delete(id);
60
62
  this.emitChange();
61
63
  for (const cb of this.finishCbs)
@@ -63,10 +65,17 @@ export class BackgroundManager {
63
65
  this.pruneHistory(id);
64
66
  }
65
67
  // Registra il comando in primo piano (chiamato dal tool bash all'avvio).
66
- setForeground(fg) { this.fg = fg; }
67
- clearForeground(id) { if (!id || this.fg?.taskId === id)
68
- this.fg = null; }
69
- hasForeground() { return this.fg !== null; }
68
+ setForeground(fg) { this.foregrounds.set(fg.taskId, fg); }
69
+ clearForeground(id) {
70
+ if (id)
71
+ this.foregrounds.delete(id);
72
+ else
73
+ this.foregrounds.clear();
74
+ }
75
+ foreground(sessionId) {
76
+ return [...this.foregrounds.values()].findLast(fg => sessionId === undefined || fg.sessionId === sessionId);
77
+ }
78
+ hasForeground(sessionId) { return !!this.foreground(sessionId); }
70
79
  markDetached(id) {
71
80
  const task = this.tasks.get(id);
72
81
  if (task && !task.detached) {
@@ -76,11 +85,11 @@ export class BackgroundManager {
76
85
  }
77
86
  // Ctrl+B: stacca il comando in primo piano. Ritorna il task o null se non c'e'
78
87
  // niente in primo piano.
79
- requestBackground() {
80
- const fg = this.fg;
88
+ requestBackground(sessionId) {
89
+ const fg = this.foreground(sessionId);
81
90
  if (!fg)
82
91
  return null;
83
- this.fg = null;
92
+ this.foregrounds.delete(fg.taskId);
84
93
  const t = this.tasks.get(fg.taskId);
85
94
  if (t)
86
95
  t.detached = true;
@@ -97,6 +106,41 @@ export class BackgroundManager {
97
106
  return true;
98
107
  }
99
108
  get(id) { return this.tasks.get(id); }
109
+ // Wait in the host, without asking the model to poll unchanged output.
110
+ // Keep the task reference alive even if completed history is pruned.
111
+ async waitForTask(id, signal, options = {}) {
112
+ signal.throwIfAborted();
113
+ const task = this.tasks.get(id);
114
+ if (!task || task.status !== "running" || options.interrupted?.())
115
+ return;
116
+ await new Promise((resolve, reject) => {
117
+ let timeout;
118
+ let interruptCheck;
119
+ const cleanup = () => {
120
+ unsubscribe();
121
+ signal.removeEventListener("abort", abort);
122
+ clearTimeout(timeout);
123
+ clearInterval(interruptCheck);
124
+ };
125
+ const finish = () => { cleanup(); resolve(); };
126
+ const abort = () => { cleanup(); reject(signal.reason); };
127
+ const unsubscribe = this.onFinish(completed => { if (completed.id === id)
128
+ finish(); });
129
+ signal.addEventListener("abort", abort, { once: true });
130
+ if (options.timeoutMs !== undefined)
131
+ timeout = setTimeout(finish, options.timeoutMs);
132
+ // Only check local user input; elapsed time/output never wakes the model.
133
+ if (options.interrupted)
134
+ interruptCheck = setInterval(() => {
135
+ if (options.interrupted?.())
136
+ finish();
137
+ }, 100);
138
+ if (signal.aborted)
139
+ abort();
140
+ else if (task.status !== "running")
141
+ finish();
142
+ });
143
+ }
100
144
  list() {
101
145
  // Ogni comando shell nasce come candidato al Ctrl+B, ma diventa davvero
102
146
  // un'attivita' in background soltanto quando l'utente lo stacca. Esporre
@@ -111,7 +155,7 @@ export class BackgroundManager {
111
155
  emitChange() { for (const cb of this.changeCbs)
112
156
  cb(); }
113
157
  pruneHistory(latestId) {
114
- const completedDetached = this.order.filter((id) => {
158
+ const completedDetached = this.completedOrder.filter((id) => {
115
159
  const task = this.tasks.get(id);
116
160
  return Boolean(task?.detached && task.status !== "running");
117
161
  });
@@ -134,6 +178,8 @@ export class BackgroundManager {
134
178
  }
135
179
  const retainedOrder = this.order.filter((id) => keep.has(id));
136
180
  this.order.splice(0, this.order.length, ...retainedOrder);
181
+ const retainedCompleted = this.completedOrder.filter(id => keep.has(id));
182
+ this.completedOrder.splice(0, this.completedOrder.length, ...retainedCompleted);
137
183
  }
138
184
  }
139
185
  // Istanza condivisa di processo.
@@ -278,6 +278,7 @@ export async function bootstrap(opts) {
278
278
  const runConfig = { ...config, model: resolved.modelId };
279
279
  return {
280
280
  provider: resolved.provider,
281
+ ...(config.fusion ? { fusionHarness: harness } : {}),
281
282
  tools,
282
283
  session,
283
284
  config: runConfig,