@dotdrelle/wiki-manager 0.15.101 → 0.16.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 (42) hide show
  1. package/mcp.endpoints.example.json +1 -1
  2. package/package.json +2 -2
  3. package/src/agent/graph.js +63 -13
  4. package/src/agent/graph.test.js +125 -1
  5. package/src/agent/llm.js +13 -4
  6. package/src/agent/llm.test.js +59 -0
  7. package/src/cli/wiki-manager.js +43 -3
  8. package/src/commands/slash.js +2 -0
  9. package/src/core/agentEvents.js +12 -2
  10. package/src/core/buildInfo.json +2 -2
  11. package/src/core/env.js +11 -2
  12. package/src/core/env.test.js +22 -6
  13. package/src/core/llmCapabilities.js +31 -0
  14. package/src/core/llmCapabilities.test.js +27 -0
  15. package/src/core/logLabel.js +9 -0
  16. package/src/core/logLabel.test.js +12 -0
  17. package/src/core/mcp.js +2 -2
  18. package/src/core/toolLoop.js +222 -20
  19. package/src/core/toolLoop.test.js +324 -0
  20. package/src/core/wikiPresearch.js +58 -0
  21. package/src/core/wikirc.js +61 -0
  22. package/src/core/wikirc.test.js +40 -1
  23. package/src/core/workflow.js +4 -1
  24. package/src/orchestrator/attemptManager.js +21 -5
  25. package/src/orchestrator/attemptManager.test.js +19 -0
  26. package/src/orchestrator/dispatcher.js +49 -8
  27. package/src/orchestrator/dispatcher.test.js +33 -1
  28. package/src/orchestrator/lockManager.js +40 -5
  29. package/src/orchestrator/resultAggregator.js +12 -1
  30. package/src/orchestrator/resultAggregator.test.js +29 -0
  31. package/src/runtime/controlClassify.test.js +85 -1
  32. package/src/runtime/conversationCompact.js +39 -0
  33. package/src/runtime/conversationCompaction.test.js +72 -0
  34. package/src/runtime/runner.e2e.test.js +49 -0
  35. package/src/runtime/runner.js +65 -1
  36. package/src/runtime/server.js +83 -75
  37. package/src/runtime/server.test.js +121 -0
  38. package/src/runtime/store.js +17 -1
  39. package/src/runtime/store.test.js +22 -0
  40. package/src/runtime/workspaceIsolation.test.js +21 -12
  41. package/src/shell/repl.js +148 -27
  42. package/src/shell/repl.test.js +182 -1
@@ -29,7 +29,7 @@
29
29
  "chatAccess": {
30
30
  "maxToolIterations": 8,
31
31
  "servers": {
32
- "llm-wiki": { "allow": ["help_list", "help_read", "help_search", "wiki_workspace_status", "wiki_list_pages", "wiki_read_page", "wiki_read_pages", "wiki_search_context", "wiki_collect_context", "wiki_read_ingested_source", "wiki_outline", "template_read", "template_write", "build_context_write", "wiki_read_deliverable", "wiki_graph_query", "wiki_graph_path"] },
32
+ "llm-wiki": { "allow": ["help_list", "help_read", "help_search", "wiki_workspace_status", "wiki_list_pages", "wiki_read_page", "wiki_read_pages", "wiki_search_context", "wiki_collect_context", "wiki_read_ingested_source", "wiki_outline", "template_read", "wiki_read_deliverable", "wiki_graph_query", "wiki_graph_path"] },
33
33
  "wiki-production": { "allow": ["production_job_status", "production_jobs_list"] },
34
34
  "cme": { "allow": ["cme_status", "cme_sources_list", "cme_export_status", "cme_confluence_search", "cme_wiki_search"] }
35
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdrelle/wiki-manager",
3
- "version": "0.15.101",
3
+ "version": "0.16.00",
4
4
  "description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "scripts": {
21
21
  "start": "bun ./bin/wiki-manager.js",
22
- "test": "node --test src/core/skillInvocation.test.js src/core/skillCompiler.test.js src/runtime/skillRun.test.js src/runtime/controlDrain.test.js src/runtime/controlCancellation.test.js src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/agent/skillRecursion.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/workspaceProfile.test.js src/core/buildInfo.test.js src/core/testGate.test.js src/core/agentEvents.test.js src/core/skillChainView.test.js src/core/runtimeLog.test.js src/core/runtimeEventAdapter.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/mcpEndpoints.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikiWorkspaceStart.test.js src/core/wikirc.test.js src/core/workspaceInherit.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/currentArtifact.test.js src/core/googleGrants.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/taskStatuses.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/proactiveReviewScheduler.test.js src/orchestrator/knowledgeSignals.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/orchestrator/providers/fakeRuntimeProvider.test.js src/orchestrator/providers/runtimeProviders.test.js src/orchestrator/providers/dispatcherExternalRuntime.test.js src/orchestrator/providers/deepAgentsProvider.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardModality.test.js src/shell/setupWizardPlaceholders.test.js src/shell/setupWizardSuggestions.test.js src/shell/setupWizardDiscovery.test.js src/shell/wrapText.test.js src/runtime/lifecycle.test.js src/runtime/store.test.js src/runtime/workspaceIsolation.test.js src/runtime/controlMessages.test.js src/runtime/controlClassify.test.js src/runtime/deltaCoalescer.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/delegation.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/skillChain.e2e.test.js src/runtime/donna-contract.test.js src/runtime/approvals.test.js src/runtime/auth.test.js src/runtime/totp.test.js src/runtime/loginSession.test.js src/runtime/loginRoutes.test.js",
22
+ "test": "node --test src/core/skillInvocation.test.js src/core/skillCompiler.test.js src/runtime/skillRun.test.js src/runtime/controlDrain.test.js src/runtime/controlCancellation.test.js src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/agent/llm.test.js src/agent/skillRecursion.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/llmCapabilities.test.js src/core/workspaceProfile.test.js src/core/buildInfo.test.js src/core/testGate.test.js src/core/agentEvents.test.js src/core/skillChainView.test.js src/core/runtimeLog.test.js src/core/runtimeEventAdapter.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/mcpEndpoints.test.js src/core/toolLoop.test.js src/core/logLabel.test.js src/runtime/conversationCompaction.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikiWorkspaceStart.test.js src/core/wikirc.test.js src/core/workspaceInherit.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/currentArtifact.test.js src/core/googleGrants.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/taskStatuses.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/proactiveReviewScheduler.test.js src/orchestrator/knowledgeSignals.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/orchestrator/providers/fakeRuntimeProvider.test.js src/orchestrator/providers/runtimeProviders.test.js src/orchestrator/providers/dispatcherExternalRuntime.test.js src/orchestrator/providers/deepAgentsProvider.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardModality.test.js src/shell/setupWizardPlaceholders.test.js src/shell/setupWizardSuggestions.test.js src/shell/setupWizardDiscovery.test.js src/shell/wrapText.test.js src/runtime/lifecycle.test.js src/runtime/store.test.js src/runtime/workspaceIsolation.test.js src/runtime/controlMessages.test.js src/runtime/controlClassify.test.js src/runtime/deltaCoalescer.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/delegation.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/skillChain.e2e.test.js src/runtime/donna-contract.test.js src/runtime/approvals.test.js src/runtime/auth.test.js src/runtime/totp.test.js src/runtime/loginSession.test.js src/runtime/loginRoutes.test.js",
23
23
  "check-versions": "node scripts/check-versions.js",
24
24
  "prepack": "node scripts/check-versions.js",
25
25
  "prepublishOnly": "node scripts/check-versions.js",
@@ -27,6 +27,8 @@ import { toolResultNote, toolStartNote } from '../core/progressNotes.js';
27
27
  import { openWikiPagesPromptLine } from '../core/openWikiPages.js';
28
28
  import { enqueueProductionJob, ensureJobQueue, formatQueue, productionLockBusy } from '../core/jobQueue.js';
29
29
  import { loadWorkspaceProfile, updateWorkspaceProfilePreference } from '../core/profile.js';
30
+ import { formatLlmConfigFact } from '../core/wikirc.js';
31
+ import { wikiSearchContextMessages } from '../core/wikiPresearch.js';
30
32
  import { artifactFromToolCall, currentArtifactFor, currentArtifactPromptLine, rememberArtifact } from '../core/currentArtifact.js';
31
33
  import { capabilityRegistryForSession } from '../orchestrator/capabilityRegistry.js';
32
34
  import { objectiveForResolution } from '../orchestrator/objectiveResolver.js';
@@ -445,7 +447,15 @@ export function bareToolCallJson(content, tools = []) {
445
447
  * reached the user and nothing ran.
446
448
  */
447
449
  export function narratedToolCallText(content) {
448
- const match = String(content ?? '').trim().match(/^([a-z][a-z0-9_-]*__[a-z][a-z0-9_-]*)\s*\{/i);
450
+ // Accept EITHER `name{...}` (bare JSON body) OR `name({...})` (function-call
451
+ // style). Some providers/gateways write the call with parentheses around the
452
+ // arguments object — `runtime__run_skill({...})` — and the old brace-only
453
+ // regex missed it, so the raw call surfaced to the user AND executed nothing.
454
+ // The parenthesis alone is NOT enough: `runtime__delegate (the delegation
455
+ // tool) hands…` is a legitimate answer about the tool, and treating it as a
456
+ // call discarded it and ended on "Action not executed". Require the `{` of an
457
+ // arguments object right after the parenthesis.
458
+ const match = String(content ?? '').trim().match(/^([a-z][a-z0-9_-]*__[a-z][a-z0-9_-]*)\s*(?:\{|\(\s*\{)/i);
449
459
  return match ? match[1] : null;
450
460
  }
451
461
 
@@ -1311,6 +1321,7 @@ export function buildAgentSystemPrompt(state) {
1311
1321
  `Reply language: ${language}.`,
1312
1322
  `Current workspace: ${workspace}.`,
1313
1323
  `Current wikirc profile: ${wikirc}.`,
1324
+ formatLlmConfigFact(state.session.wikircConfig, state.session.wikirc),
1314
1325
  `Available primitives: ${commandList(state.session)}.`,
1315
1326
  'Only announce or call slash commands that appear exactly in Available primitives. Do not invent command names, subcommands, or arguments.',
1316
1327
  'Connected MCP tools you may call directly (server__tool naming convention). Everything listed below is directly callable. When the requested action has no matching direct tool, call runtime__delegate with the original objective: the runtime resolves it against the discovered agent capability contracts, including executor-only single-task capabilities.',
@@ -1333,6 +1344,20 @@ export function buildAgentSystemPrompt(state) {
1333
1344
  'Write the way a thoughtful colleague speaks: warm, plain, and to the point. For a simple factual question, 1 to 3 sentences is the sweet spot. Stay synthetic and information-dense — use only the lines needed, and never exceed roughly 15 to 20 short lines even for a detailed answer. Never expose internal reasoning, repeated checks, tool-selection commentary, or a chronological diary. Prioritize the result, essential facts, concrete errors, and actual outputs — but say them in human language, not as a field dump.',
1334
1345
  'Call a matching direct tool when one is offered. Otherwise, for an action backed by a discovered agent capability, call runtime__delegate with the original objective. This applies to both planner agents and executor-only single-task agents. Never call an agent orchestration-contract or plan tool directly.',
1335
1346
  'Configuration is not a business run. When a connected server offers a setup or configuration tool, use it directly; never delegate configuration to an export, collect, send, build, or ingest capability. Read that server status first when existing non-secret values are needed, then ask only for required values that are still missing.',
1347
+ // Observed: « récupère le numéro de ticket depuis le wiki » delegated to
1348
+ // the external runtime's agent.answer, which burned 519k tokens and failed
1349
+ // on its budget, while one wiki search answers it in a second.
1350
+ 'A question about the subject matter of this workspace (its projects, documents, tickets, people, decisions, figures, dates) is answered from the wiki: search it FIRST with the wiki search/read tools and answer from what they return. Never runtime__delegate a question those read tools can answer; delegation is for actions, or for an analysis the user explicitly asks an agent to perform.',
1351
+ // Observed (acpi): "cherche sur internet" was delegated to the external
1352
+ // runtime, which answered it could not search the internet — while a
1353
+ // connected web-search read tool was available and offered right here. The
1354
+ // wiki stays first for workspace facts; the web tools cover the rest and
1355
+ // are never denied.
1356
+ 'A wiki-first rule covers workspace facts only. For an internet/web search the wiki cannot answer, use the connected web-search read tool directly when one is offered; otherwise delegate the objective to a research capability. Never answer that you cannot search the internet when a web-search tool is available, and never claim a search you did not run.',
1357
+ // Observed: « compare les options A et B » answered from the previous
1358
+ // answers alone — the history carries Donna's text, not the pages — and
1359
+ // option A was invented, the opposite of what the wiki says.
1360
+ 'Your earlier answers in this conversation are not evidence: they keep your text, not the pages. For each new question, search the wiki again for every fact you have not quoted from a tool result in this very turn. Never fill a gap (an acronym expansion, a missing option, a figure) from general knowledge.',
1336
1361
  'For any question about the current workspace inventory or what is waiting there, call wiki__wiki_workspace_status first and answer only from its result. This is the canonical read-only workspace state; do not reconstruct it from upload, connector, or production tools.',
1337
1362
  'Tool identifiers are private implementation details. Never print MCP tool names such as server__tool in a user-facing answer. Describe the human result instead.',
1338
1363
  'Internal data shapes are private too. Never quote raw JSON field names (e.g. pendingSources.files), internal directory paths (e.g. raw/untracked/), or config keys in a user-facing answer — translate them into plain language. Say "36 pages sources sont en attente d\'ingestion", not the field or path they came from.',
@@ -1382,7 +1407,7 @@ export function buildAgentSystemPrompt(state) {
1382
1407
  : null,
1383
1408
  openWikiPagesPromptLine(state.session.openWikiPages),
1384
1409
  currentArtifactPromptLine(currentArtifactFor(state.session)),
1385
- 'Runtime control: you have runtime__status, runtime__cancel, runtime__kill and runtime__enqueue. When the user asks to stop, remove, clean or kill the current run, its jobs or the queue ("supprime le job et la queue", "arr\u00eate tout"), call runtime__kill (or runtime__cancel for a soft stop of just the run) and confirm what was stopped. When the user explicitly asks to delete, reset, abandon or replace the current plan, call runtime__kill with purge=true; never set purge=true for a simple stop. For questions about what is running or queued, call runtime__status and answer from its data. You have no approval tool: a pending approval is granted only by the user through the approval button or the /approve command. Never grant, claim or report an approval yourself; when the user asks to proceed with pending mutations, tell them to use those controls. A request to approve, validate, confirm or accept a pending run is a human control action: NEVER call runtime__delegate (or any capability) for it — answer with the control to use and nothing else. When the user asks for a NEW action while a run is active, do not execute it: propose runtime__enqueue (run it after) or, if they insist it replaces the current work, runtime__kill then the new action.',
1410
+ 'Runtime control: you have runtime__status, runtime__cancel, runtime__kill and runtime__enqueue. When the user asks to stop, remove, clean or kill the current run, its jobs or the queue ("supprime le job et la queue", "arr\u00eate tout"), call runtime__kill (or runtime__cancel for a soft stop of just the run) and confirm what was stopped. When the user explicitly asks to delete, reset, abandon or replace the current plan, call runtime__kill with purge=true; never set purge=true for a simple stop. For questions about what is running or queued, call runtime__status and answer from its data. You have no approval tool: a pending approval is granted only by the user through the approval button or the /approve command. Never grant, claim or report an approval yourself; when the user asks to proceed with pending mutations, tell them to use those controls. A request to approve, validate, confirm or accept a pending run is a human control action: NEVER call runtime__delegate (or any capability) for it — answer with the control to use and nothing else. When the user asks for a NEW action while a run is active: if one of your direct tools performs it (write or edit a template, a build-context file or a wiki page; a read or a check), do it now — a write may be refused because the running job is using that part of the workspace, and then say so plainly and offer runtime__enqueue. Heavier work (ingest, build, export, curate, rebuilding an index) is queued with runtime__enqueue (it runs after), or, if the user insists it replaces the current work, runtime__kill then the new action.',
1386
1411
  'When the user asks to refresh, show, or update the displayed plan or status, call runtime__status. This is a state refresh request, not a new business capability, and must never be delegated.',
1387
1412
  'Report every runtime control outcome exactly as the tool returned it \u2014 never embellish. If runtime__kill reports 0 run(s)/0 task(s)/0 purged, say there was nothing active to stop or purge; do NOT claim a run, plan, pending approval or queue item was removed. If runtime__status returns an error or could not be read, say the runtime state could not be retrieved and do not describe a state you never obtained. Never assert that something was cleaned, cancelled, approved or purged unless that specific tool result confirms it.',
1388
1413
  'Durable profile updates are actions in this stabilized version: delegate them instead of writing directly.',
@@ -1433,7 +1458,14 @@ function toolsForClassification(classification, writeTools, session = null) {
1433
1458
  // suite: she can answer, approve, enqueue for later, soft-cancel or
1434
1459
  // kill — but she must not fire new MCP jobs alongside the run (that is
1435
1460
  // what runtime__enqueue is for). No canned regex answers anywhere.
1436
- return [SHELL_READ_COMMAND_TOOL, ...controlTools, ...capabilityRunTools];
1461
+ // The direct tools stay offered — reads, and unitary writes such as a
1462
+ // template or a page: without them a question sent as a run could only be
1463
+ // delegated (observed: a wiki question handed to the external runtime,
1464
+ // which spent 519k tokens on it), and a template asked for during an
1465
+ // ingest waited for the whole ingest. ordinaryDirectTools never carries a
1466
+ // job starter, and the engine refuses a write that would race with the
1467
+ // running job (plan-demandes-pendant-run.md, lot 3).
1468
+ return [SHELL_READ_COMMAND_TOOL, ...controlTools, ...capabilityRunTools, ...ordinaryDirectTools(writeTools)];
1437
1469
  }
1438
1470
  if (session?.runtime?.url) {
1439
1471
  // Offer every connected tool directly EXCEPT orchestration-bypass tools
@@ -1476,7 +1508,7 @@ export function isDonnaReadTool(item) {
1476
1508
  if (tool === 'wiki_workspace_status' || tool === 'agent_describe' || tool === 'agent_status') return true;
1477
1509
  // Match a read verb anywhere in the underscore-tokenized name, not just as
1478
1510
  // a trailing suffix — third-party MCPs don't all name tools verb-last
1479
- // (e.g. exa's "web_search_exa"/"web_fetch_exa" put the verb in the middle).
1511
+ // (e.g. a connector whose `web_search_x` puts the verb in the middle).
1480
1512
  return tool.split('_').some((segment) => DONNA_READ_VERBS.has(segment));
1481
1513
  }
1482
1514
 
@@ -1585,7 +1617,8 @@ export function createAgentGraph(options = {}) {
1585
1617
  kind: 'agent_turn',
1586
1618
  confidence: 1,
1587
1619
  reason: 'agent_mode_llm_decision',
1588
- activeRun: state.session?.agentProjection?.status === 'running'
1620
+ activeRun: state.session?._runActive === true
1621
+ || ['running', 'pending_approval'].includes(state.session?.agentProjection?.status)
1589
1622
  || sessionActivities(state.session).some((activity) => !activity.terminal),
1590
1623
  })
1591
1624
  : (state.inputClassification ?? { kind: 'modify_run', confidence: 1, reason: 'tool_iteration' });
@@ -1614,11 +1647,28 @@ export function createAgentGraph(options = {}) {
1614
1647
  : toolsForClassification(classification, writeTools, state.session);
1615
1648
  const system = buildAgentSystemPrompt(state);
1616
1649
 
1650
+ // The wiki is searched before the first model call of a turn, as in chat
1651
+ // mode (core/wikiPresearch.js): a question about the workspace was
1652
+ // otherwise answered from the previous answers, or delegated to an agent
1653
+ // that burned its whole token budget on it. Only on the user's own words —
1654
+ // never on a compiled skill objective (private material) nor on the
1655
+ // continuation prompts of a run's later turns. It lives for this turn
1656
+ // only: the next turn's history is rebuilt from the conversation.
1657
+ const runTurnId = state.session._currentRunIdentity?.turnId;
1658
+ const userWords = iterations === 0
1659
+ && normalizedSkillStack(state.session).length === 0
1660
+ && !state.session._responseSynthesisOnly
1661
+ && (!state.session._currentRunIdentity || !runTurnId || String(runTurnId).endsWith(':turn-1'));
1662
+ const userTurn = [
1663
+ ...(userWords ? await wikiSearchContextMessages(state.input, state.session, tools, state.session._onStep) : []),
1664
+ { role: 'user', content: state.input },
1665
+ ];
1666
+
1617
1667
  // On iteration 0: prior history is in state.messages, user input must be appended.
1618
1668
  // On subsequent iterations: user message was already stored in state.messages by the
1619
1669
  // iteration-0 return below, so use state.messages as-is.
1620
1670
  const conversationMessages = iterations === 0
1621
- ? [...(state.messages ?? []), { role: 'user', content: state.input }]
1671
+ ? [...(state.messages ?? []), ...userTurn]
1622
1672
  : (state.messages ?? []);
1623
1673
 
1624
1674
  try {
@@ -1655,7 +1705,7 @@ export function createAgentGraph(options = {}) {
1655
1705
  return {
1656
1706
  pendingToolCalls: null,
1657
1707
  messages: [
1658
- ...(iterations === 0 ? [{ role: 'user', content: state.input }] : []),
1708
+ ...(iterations === 0 ? userTurn : []),
1659
1709
  {
1660
1710
  role: 'user',
1661
1711
  content: 'Your previous tool call was incomplete or contained invalid JSON arguments. Call the appropriate available tool again with one complete valid JSON object. Do not narrate or reproduce the broken call.',
@@ -1689,7 +1739,7 @@ export function createAgentGraph(options = {}) {
1689
1739
  tool_calls: result.tool_calls,
1690
1740
  };
1691
1741
  const newMessages = iterations === 0
1692
- ? [{ role: 'user', content: state.input }, assistantToolMessage]
1742
+ ? [...userTurn, assistantToolMessage]
1693
1743
  : [assistantToolMessage];
1694
1744
  return {
1695
1745
  pendingToolCalls: result.tool_calls,
@@ -1722,7 +1772,7 @@ export function createAgentGraph(options = {}) {
1722
1772
  return {
1723
1773
  pendingToolCalls: null,
1724
1774
  messages: [
1725
- ...(iterations === 0 ? [{ role: 'user', content: state.input }] : []),
1775
+ ...(iterations === 0 ? userTurn : []),
1726
1776
  {
1727
1777
  role: 'user',
1728
1778
  content: `You described a call to ${bareCall} as JSON text instead of calling it. Issue a real tool call now, or answer in plain language. Never print the call as text.`,
@@ -1746,7 +1796,7 @@ export function createAgentGraph(options = {}) {
1746
1796
  return {
1747
1797
  pendingToolCalls: null,
1748
1798
  messages: [
1749
- { role: 'user', content: state.input },
1799
+ ...userTurn,
1750
1800
  result.message ?? { role: 'assistant', content: result.content ?? '' },
1751
1801
  {
1752
1802
  role: 'user',
@@ -1790,7 +1840,7 @@ export function createAgentGraph(options = {}) {
1790
1840
  return {
1791
1841
  pendingToolCalls: null,
1792
1842
  messages: [
1793
- { role: 'user', content: state.input },
1843
+ ...userTurn,
1794
1844
  result.message ?? { role: 'assistant', content: result.content ?? '' },
1795
1845
  { role: 'user', content: 'This is an action request. Call runtime__delegate now with the original objective only. Do not provide instructions or narration.' },
1796
1846
  ],
@@ -1834,7 +1884,7 @@ export function createAgentGraph(options = {}) {
1834
1884
  return {
1835
1885
  pendingToolCalls: null,
1836
1886
  messages: [
1837
- ...(iterations === 0 ? [{ role: 'user', content: state.input }] : []),
1887
+ ...(iterations === 0 ? userTurn : []),
1838
1888
  result.message ?? { role: 'assistant', content: result.content ?? '' },
1839
1889
  {
1840
1890
  role: 'user',
@@ -1863,7 +1913,7 @@ export function createAgentGraph(options = {}) {
1863
1913
  emitAgentEvent(state.session, 'assistant_message', 'llm', { content: result.content ?? '' });
1864
1914
  // Text was streamed inline via session._onStream — no second LLM call needed.
1865
1915
  const newMessages = iterations === 0
1866
- ? [{ role: 'user', content: state.input }, result.message]
1916
+ ? [...userTurn, result.message]
1867
1917
  : [result.message];
1868
1918
  return {
1869
1919
  response: null,
@@ -3,7 +3,7 @@ import test from 'node:test';
3
3
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
- import { bareToolCallJson, buildAgentSystemPrompt, connectorConfigurationTarget, createAgentGraph, invalidSuggestedSlashCommands, invalidUserFacingToolNames, isOrchestrationBypassTool, knownCapabilityIds, normalizeToolArgumentsFromSchema, nothingToDoForDonna } from './graph.js';
6
+ import { bareToolCallJson, buildAgentSystemPrompt, connectorConfigurationTarget, createAgentGraph, invalidSuggestedSlashCommands, invalidUserFacingToolNames, isOrchestrationBypassTool, knownCapabilityIds, narratedToolCallText, normalizeToolArgumentsFromSchema, nothingToDoForDonna } from './graph.js';
7
7
 
8
8
  test('user-facing response guard hides MCP identifiers generically', () => {
9
9
  const session = sessionBase();
@@ -1122,6 +1122,81 @@ test('buildAgentSystemPrompt includes .wiki/profile.md content so preferences ap
1122
1122
  }
1123
1123
  });
1124
1124
 
1125
+ test('buildAgentSystemPrompt answers workspace questions from the wiki instead of delegating them', () => {
1126
+ // Observed: a wiki question delegated to the external runtime's agent.answer
1127
+ // burned 519k tokens and failed on its budget.
1128
+ const prompt = buildAgentSystemPrompt({ session: sessionBase({}) });
1129
+ assert.match(prompt, /search it FIRST with the wiki search\/read tools/);
1130
+ assert.match(prompt, /Never runtime__delegate a question those read tools can answer/);
1131
+ });
1132
+
1133
+ async function agentFirstCallMessages(sessionOverrides, input = 'compare les options A et B') {
1134
+ const searched = [];
1135
+ const originalFetch = globalThis.fetch;
1136
+ globalThis.fetch = async (url, init) => {
1137
+ const body = JSON.parse(init?.body ?? '{}');
1138
+ let result = {};
1139
+ if (body.method === 'tools/call') {
1140
+ searched.push(body.params.arguments.question);
1141
+ result = { content: [{ type: 'text', text: 'wiki/concepts/a.md: option A, sans tracé manuel' }] };
1142
+ }
1143
+ const text = JSON.stringify({ jsonrpc: '2.0', id: body.id ?? 1, result });
1144
+ return { ok: true, status: 200, headers: { get: () => null }, text: async () => text, json: async () => JSON.parse(text) };
1145
+ };
1146
+ const seen = [];
1147
+ const session = sessionBase({
1148
+ mcp: { wiki: { status: 'connected', url: `http://agent-presearch-${Math.random()}.test/mcp`, tools: [
1149
+ { name: 'wiki_search_context', inputSchema: { type: 'object', properties: {} } },
1150
+ ] } },
1151
+ llm: { async completeWithTools({ messages }) { seen.push(messages); return { tool_calls: [], content: 'ok' }; } },
1152
+ ...sessionOverrides,
1153
+ });
1154
+ try {
1155
+ await createAgentGraph().invoke({ input, session });
1156
+ } finally {
1157
+ globalThis.fetch = originalFetch;
1158
+ }
1159
+ return { searched, first: seen[0] ?? [] };
1160
+ }
1161
+
1162
+ test('an agent turn searches the wiki before its first model call', async () => {
1163
+ const { searched, first } = await agentFirstCallMessages({});
1164
+ assert.deepEqual(searched, ['compare les options A et B']);
1165
+ const index = first.findIndex((m) => /WIKI SEARCH RESULTS/.test(m.content));
1166
+ assert.ok(index >= 0);
1167
+ assert.equal(first[index + 1].content, 'compare les options A et B');
1168
+ });
1169
+
1170
+ test('an agent turn never pre-searches a skill objective or a later run turn', async () => {
1171
+ const skill = await agentFirstCallMessages({ _skillStack: ['pipeline'] });
1172
+ assert.deepEqual(skill.searched, []);
1173
+ const laterTurn = await agentFirstCallMessages({ _currentRunIdentity: { runId: 'r1', turnId: 'r1:turn-2' } });
1174
+ assert.deepEqual(laterTurn.searched, []);
1175
+ const firstTurn = await agentFirstCallMessages({ _currentRunIdentity: { runId: 'r1', turnId: 'r1:turn-1' } });
1176
+ assert.deepEqual(firstTurn.searched, ['compare les options A et B']);
1177
+ });
1178
+
1179
+ test('an agent turn during a pending run keeps its direct tools and cannot start a second run', async () => {
1180
+ // plan-demandes-pendant-run.md, lot 3. The runtime says a run is active
1181
+ // (_runActive): a projection in `pending_approval` used to read as idle.
1182
+ let offered = [];
1183
+ const session = sessionBase({
1184
+ runtime: { url: 'http://127.0.0.1:7788' },
1185
+ _runActive: true,
1186
+ agentProjection: { status: 'pending_approval' },
1187
+ mcp: { wiki: { status: 'connected', url: 'http://x.test/mcp', tools: [
1188
+ { name: 'template_write', inputSchema: { type: 'object', properties: {} } },
1189
+ { name: 'wiki_search_context', inputSchema: { type: 'object', properties: {} } },
1190
+ ] }, production: sessionBase().mcp.production },
1191
+ llm: { async completeWithTools({ tools }) { offered = tools.map((item) => item.function.name); return { tool_calls: [], content: 'ok' }; } },
1192
+ });
1193
+ await createAgentGraph().invoke({ input: 'salut', session });
1194
+ assert.ok(offered.includes('wiki__template_write'), offered.join(','));
1195
+ assert.ok(offered.includes('runtime__enqueue'), offered.join(','));
1196
+ assert.ok(!offered.includes('runtime__delegate'), 'no second run beside the active one');
1197
+ assert.ok(!offered.includes('production__production_start_job'), 'never a job starter');
1198
+ });
1199
+
1125
1200
  test('buildAgentSystemPrompt omits the profile section when profile.md is missing or empty', () => {
1126
1201
  const workspacePath = mkdtempSync(join(tmpdir(), 'donna-profile-empty-'));
1127
1202
  try {
@@ -2612,3 +2687,52 @@ test('LOT F: the guard message stays English-only for every session language', a
2612
2687
  assert.match(await answer(undefined), /repeatedly printed an internal tool request/);
2613
2688
  assert.match(await answer('fr'), /repeatedly printed an internal tool request/);
2614
2689
  });
2690
+
2691
+ test('narratedToolCallText recognises the brace and the function-call forms', () => {
2692
+ assert.equal(narratedToolCallText('runtime__delegate{"objective":"x"}'), 'runtime__delegate');
2693
+ // Some gateways write the call with parentheses around the arguments object;
2694
+ // the brace-only regex missed this and executed nothing.
2695
+ assert.equal(narratedToolCallText('runtime__run_skill({"skillName":"deliver"})'), 'runtime__run_skill');
2696
+ assert.equal(
2697
+ narratedToolCallText(' production__production_start_job({ "type": "build" }) '),
2698
+ 'production__production_start_job',
2699
+ );
2700
+ assert.equal(narratedToolCallText('Bonjour, je peux vous aider.'), null);
2701
+ // Prose ABOUT a tool, opening on its name and a parenthesis, is an answer.
2702
+ assert.equal(
2703
+ narratedToolCallText('runtime__delegate (the delegation tool) hands an objective to the runtime.'),
2704
+ null,
2705
+ );
2706
+ assert.equal(narratedToolCallText('runtime__delegate() takes an objective.'), null);
2707
+ assert.equal(narratedToolCallText('{"name":"runtime__run_skill"}'), null);
2708
+ });
2709
+
2710
+ test('a skill call narrated with parentheses is neither executed nor shown', async () => {
2711
+ // Observed: `/deliver TechSections.export.md polish` answered with the raw
2712
+ // `runtime__run_skill({…})` text — nothing ran and the call leaked, because
2713
+ // the guard only matched `name{`.
2714
+ const raw =
2715
+ 'runtime__run_skill({"skillName":"deliver","arguments":{"deliverable":"TechSections.export.md","polish":"true"},"selectionKind":"explicit_name"})';
2716
+ const ran = [];
2717
+ let calls = 0;
2718
+ const session = sessionBase({
2719
+ language: 'fr-FR',
2720
+ runtime: { url: 'http://runtime.test' },
2721
+ _runSkillWithinRun: async (name) => { ran.push(name); return { ok: true }; },
2722
+ _onStreamReset: () => {},
2723
+ llm: {
2724
+ async completeWithTools() {
2725
+ calls += 1;
2726
+ return { content: raw, message: { role: 'assistant', content: raw }, tool_calls: null };
2727
+ },
2728
+ },
2729
+ });
2730
+
2731
+ const result = await createAgentGraph().invoke({ input: 'Lance le polish.', session });
2732
+
2733
+ assert.deepEqual(ran, [], 'a call written as text must never reach the skill runner');
2734
+ assert.doesNotMatch(result.response, /runtime__run_skill/);
2735
+ assert.doesNotMatch(result.response, /[{}]/, 'no JSON fragment may survive');
2736
+ assert.match(result.response, /repeatedly printed an internal tool request/);
2737
+ assert.ok(calls >= 2, 'the first occurrence is retried, not surfaced');
2738
+ });
package/src/agent/llm.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { supportsTemperature } from '../core/llmCapabilities.js';
2
+
1
3
  function trimTrailingSlash(value) {
2
4
  return value.replace(/\/+$/, '');
3
5
  }
@@ -25,6 +27,13 @@ export function createLlmClientFromWikiConfig(config) {
25
27
  return null;
26
28
  }
27
29
 
30
+ // A gpt-5-class model rejects `temperature` outright (HTTP 400, "Only the
31
+ // default (1) value is supported"), whether the profile sets one or not. When
32
+ // it is refused, the field is omitted whole rather than sent and rejected.
33
+ const temperatureBody = supportsTemperature(llmConfig)
34
+ ? { temperature: typeof llmConfig.temperature === 'number' ? llmConfig.temperature : 0.2 }
35
+ : {};
36
+
28
37
  return {
29
38
  async complete({ system, input, signal }) {
30
39
  const response = await fetch(`${baseUrl}/chat/completions`, {
@@ -40,7 +49,7 @@ export function createLlmClientFromWikiConfig(config) {
40
49
  { role: 'system', content: system },
41
50
  { role: 'user', content: input },
42
51
  ],
43
- temperature: typeof llmConfig.temperature === 'number' ? llmConfig.temperature : 0.2,
52
+ ...temperatureBody,
44
53
  }),
45
54
  });
46
55
 
@@ -64,7 +73,7 @@ export function createLlmClientFromWikiConfig(config) {
64
73
  const body = {
65
74
  model,
66
75
  messages: allMessages,
67
- temperature: typeof llmConfig.temperature === 'number' ? llmConfig.temperature : 0.2,
76
+ ...temperatureBody,
68
77
  };
69
78
  if (tools.length > 0) {
70
79
  body.tools = tools;
@@ -99,7 +108,7 @@ export function createLlmClientFromWikiConfig(config) {
99
108
  const body = {
100
109
  model,
101
110
  messages: allMessages,
102
- temperature: typeof llmConfig.temperature === 'number' ? llmConfig.temperature : 0.2,
111
+ ...temperatureBody,
103
112
  stream: true,
104
113
  };
105
114
  if (tools.length > 0) {
@@ -195,7 +204,7 @@ export function createLlmClientFromWikiConfig(config) {
195
204
  body: JSON.stringify({
196
205
  model,
197
206
  messages: allMessages,
198
- temperature: typeof llmConfig.temperature === 'number' ? llmConfig.temperature : 0.2,
207
+ ...temperatureBody,
199
208
  stream: true,
200
209
  }),
201
210
  });
@@ -0,0 +1,59 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createLlmClientFromWikiConfig } from './llm.js';
4
+
5
+ function captureFetch(reply) {
6
+ const calls = [];
7
+ const original = globalThis.fetch;
8
+ globalThis.fetch = async (url, init) => {
9
+ calls.push({ url, body: JSON.parse(init.body) });
10
+ return { ok: true, json: async () => reply };
11
+ };
12
+ return { calls, restore: () => { globalThis.fetch = original; } };
13
+ }
14
+
15
+ const reply = { choices: [{ message: { content: 'ok' } }] };
16
+ const gateway = {
17
+ provider: 'ai-gateway',
18
+ baseUrl: 'https://gw.example.com/v1',
19
+ apiKey: 'k',
20
+ };
21
+
22
+ test('omits temperature for a gpt-5-class model even when the profile sets one', async () => {
23
+ const { calls, restore } = captureFetch(reply);
24
+ try {
25
+ const client = createLlmClientFromWikiConfig({
26
+ llm: { ...gateway, model: 'openai/gpt-5-mini', temperature: 0.2 },
27
+ });
28
+ await client.complete({ system: 's', input: 'i' });
29
+ assert.equal('temperature' in calls[0].body, false);
30
+ } finally {
31
+ restore();
32
+ }
33
+ });
34
+
35
+ test('keeps the configured temperature for a model that accepts it', async () => {
36
+ const { calls, restore } = captureFetch(reply);
37
+ try {
38
+ const client = createLlmClientFromWikiConfig({
39
+ llm: { ...gateway, model: 'openai/gpt-4.1', temperature: 0.3 },
40
+ });
41
+ await client.complete({ system: 's', input: 'i' });
42
+ assert.equal(calls[0].body.temperature, 0.3);
43
+ } finally {
44
+ restore();
45
+ }
46
+ });
47
+
48
+ test('keeps the 0.2 default when the profile declares no temperature', async () => {
49
+ const { calls, restore } = captureFetch(reply);
50
+ try {
51
+ const client = createLlmClientFromWikiConfig({
52
+ llm: { ...gateway, model: 'openai/gpt-4.1' },
53
+ });
54
+ await client.complete({ system: 's', input: 'i' });
55
+ assert.equal(calls[0].body.temperature, 0.2);
56
+ } finally {
57
+ restore();
58
+ }
59
+ });
@@ -18,7 +18,7 @@ import { ensureManagerScaffold, loadManagerEnv } from '../core/env.js';
18
18
  loadManagerEnv();
19
19
  import { createAgentGraph } from '../agent/graph.js';
20
20
  import { handleSlashCommand, printHelp, printVersion, refreshMcpRuntimeStatus } from '../commands/slash.js';
21
- import { runShell, runHeadlessChatTurn, sanitizeOpenWikiPages } from '../shell/repl.js';
21
+ import { runShell, runHeadlessChatTurn, sanitizeOpenWikiPages, chatInputBudgetChars } from '../shell/repl.js';
22
22
  import { runPreflightChecks, withRuntimePreflight } from '../core/startupCheck.js';
23
23
  import { refreshRunningContainers } from '../core/wikiSetup.js';
24
24
  import { applySessionWikircProfile } from '../core/sessionConfig.js';
@@ -32,6 +32,7 @@ import { runAgentTurn, runAgenticLoop } from '../core/agentLoop.js';
32
32
  import { createDeltaCoalescer } from '../runtime/deltaCoalescer.js';
33
33
  import { TERMINAL_STATUS_SET } from '../orchestrator/taskStatuses.js';
34
34
  import { resolveCapabilityConcurrency } from '../orchestrator/scheduler.js';
35
+ import { workspaceLockRegistry } from '../orchestrator/lockManager.js';
35
36
  import { capabilityRegistryForSession } from '../orchestrator/capabilityRegistry.js';
36
37
  import { CapabilityUnavailableError, resolve as resolveCapability } from '../orchestrator/capabilityResolver.js';
37
38
  import { listWorkspaces } from '../core/workspaces.js';
@@ -358,6 +359,9 @@ export function createInteractiveSession(context, { runtimeUrl, turnId, signal =
358
359
  session.headlessPlan = null;
359
360
  session.turnId = turnId ?? null;
360
361
  session._abortSignal = signal;
362
+ // The SAME lock registry as the workspace's runs, never a copy: a direct
363
+ // write from this turn must see what a run holds (plan-demandes-pendant-run.md).
364
+ session._workspaceLocks = workspaceLockRegistry(source);
361
365
  return session;
362
366
  }
363
367
 
@@ -956,7 +960,8 @@ async function runRuntime(argv, agent) {
956
960
  const { resolveRuntimeAuthToken } = await import('../runtime/auth.js');
957
961
  const { createSqliteQueueStore } = await import('../runtime/queueStore.js');
958
962
  const { createApprovalManager } = await import('../runtime/approvals.js');
959
- const { conversationSeed, runRuntimeAgenticWorkflow } = await import('../runtime/runner.js');
963
+ const { conversationSeed, conversationCompactionPlan, compactionNoteForDonna, runRuntimeAgenticWorkflow } = await import('../runtime/runner.js');
964
+ const { summarizeCompactedConversation } = await import('../runtime/conversationCompact.js');
960
965
 
961
966
  // Same default as wiki-workspace runtime up and ensureRuntime: a loopback
962
967
  // bind is invisible to the serve container.
@@ -1741,13 +1746,17 @@ async function runRuntime(argv, agent) {
1741
1746
  await refreshMcpRuntimeStatus(context.session);
1742
1747
  }
1743
1748
  const ephemeral = createInteractiveSession(context, { runtimeUrl: selfRuntimeUrl, turnId, signal });
1749
+ // Whether a run is active in this workspace, from the runtime itself: the
1750
+ // projection says `pending_approval` for an ingest waiting on its approval,
1751
+ // and reading only `running` made such a turn believe the workspace idle.
1752
+ ephemeral._runActive = Boolean(context.running);
1744
1753
  // Seed from a freshly reduced COPY of persisted events. Interactive turn
1745
1754
  // events deliberately do not mutate the canonical run projection, so the
1746
1755
  // canonical session alone is not a reliable conversation-history source.
1747
1756
  const persistedProjection = reduceAgentEvents(store.listEvents({
1748
1757
  workspace: context.workspace ?? ephemeral.workspace ?? null,
1749
1758
  }));
1750
- const messages = conversationSeed({ agentProjection: persistedProjection }, input);
1759
+ let messages = conversationSeed({ agentProjection: persistedProjection }, input);
1751
1760
  // Streaming fragments are coalesced before they are persisted and pushed —
1752
1761
  // one synchronous SQLite insert (plus one SSE write) per token stalled the
1753
1762
  // event loop, freezing both chats (serve and ShellUI) while a long answer
@@ -1779,6 +1788,37 @@ async function runRuntime(argv, agent) {
1779
1788
  workspace: context.workspace ?? null,
1780
1789
  payload: { message },
1781
1790
  }));
1791
+ // Automatic memory compaction, before this turn's own message enters the
1792
+ // conversation. Not during a run (the manual gauge refuses it too: the
1793
+ // boundary must not move under a run's own messages). A compaction whose
1794
+ // summary could not be produced moves nothing — dropping messages without
1795
+ // their summary is the silent loss this exists to end. Donna, not the
1796
+ // system, tells the user it happened.
1797
+ const compaction = context.running ? null : conversationCompactionPlan(persistedProjection, {
1798
+ budgetChars: chatInputBudgetChars(context.session?.wikircConfig),
1799
+ });
1800
+ if (compaction) {
1801
+ ephemeral._onStep?.('Condensing the earlier conversation…');
1802
+ const summary = await summarizeCompactedConversation(context.session, compaction);
1803
+ if (summary && summary !== compaction.previousSummary) {
1804
+ dispatchAgentEvent(ephemeral, createAgentEvent('conversation_reset', {
1805
+ origin: 'runtime_turn',
1806
+ turnId,
1807
+ workspace: context.workspace ?? null,
1808
+ payload: { summary, keepLast: compaction.keepLast, automatic: true, reason: compaction.reason },
1809
+ }));
1810
+ const compactedProjection = reduceAgentEvents(store.listEvents({
1811
+ workspace: context.workspace ?? ephemeral.workspace ?? null,
1812
+ }));
1813
+ const count = compaction.segment.filter((message) => ['user', 'assistant'].includes(message?.role)).length;
1814
+ // Right after the summary it refers to — never last: the chat branch
1815
+ // drops a trailing user message as the current input.
1816
+ const seed = conversationSeed({ agentProjection: compactedProjection }, input);
1817
+ messages = [seed[0], { role: 'user', content: compactionNoteForDonna(count) }, ...seed.slice(1)];
1818
+ } else {
1819
+ emitRuntimeLog(context.session, 'conversation-compact: automatic compaction skipped — no new summary; the conversation window is unchanged');
1820
+ }
1821
+ }
1782
1822
  dispatchAgentEvent(ephemeral, createAgentEvent('user_message', {
1783
1823
  origin: 'runtime_turn',
1784
1824
  turnId,
@@ -144,8 +144,10 @@ function wikircSummaryText(summary) {
144
144
  `profile=${summary.profile}`,
145
145
  `file=${summary.fileName}`,
146
146
  `provider=${summary.provider ?? '-'}`,
147
+ `engine=${summary.engine ?? '-'}`,
147
148
  `model=${summary.model ?? '-'}`,
148
149
  `baseUrl=${summary.baseUrl ?? '-'}`,
150
+ `temperature=${summary.temperature ?? '-'}`,
149
151
  `language=${summary.language ?? '-'}`,
150
152
  `apiKey=${summary.hasApiKey ? 'configured' : 'missing'}`,
151
153
  `vector=${summary.vectorEnabled ? 'enabled' : 'disabled'}`,
@@ -5,6 +5,7 @@ import { applyPlanPatch, normalizePlanPatch, normalizePlanRevision, rebasePlanPa
5
5
  import { formatRuntimeLogPayload, isDispatchPlumbingLine, normalizeRuntimeLog, shortTaskLabel } from './runtimeLog.js';
6
6
  import { projectSkillChains, TERMINAL as CONTROL_TERMINAL_STATUSES } from './skillChainView.js';
7
7
  import { projectWorkflow } from './workflow.js';
8
+ import { compactLogLabel } from './logLabel.js';
8
9
  import { validateContractInDev } from '../contracts/schemas.js';
9
10
  import { isActive, isTerminal, isSuccessful, isUnknownStatus, normalizeTaskStatus } from '../orchestrator/taskStatuses.js';
10
11
 
@@ -362,7 +363,16 @@ function applyEvent(state, event) {
362
363
  // (length at reset time) instead of clearing state.conversation is what
363
364
  // keeps the two concerns apart: the gauge/seed read conversationSeedStart,
364
365
  // the display reads conversation.
365
- state.conversationSeedStart = state.conversation.length;
366
+ // `keepLast` (automatic compaction): the last exchanges stay verbatim in
367
+ // the seed — a follow-up such as « rajoute la liste » needs the previous
368
+ // answer word for word, not its summary. The manual gauge sends none.
369
+ {
370
+ const keepLast = Math.max(0, Math.floor(Number(event.payload?.keepLast) || 0));
371
+ state.conversationSeedStart = Math.max(
372
+ Number(state.conversationSeedStart) || 0,
373
+ state.conversation.length - keepLast,
374
+ );
375
+ }
366
376
  // The summary is best-effort (an LLM call the compact route makes before
367
377
  // dispatching this event): when it succeeds it REPLACES the previous one
368
378
  // — it is a rolling summary of "everything before this point", not an
@@ -1237,7 +1247,7 @@ function planTaskById(state, taskId) {
1237
1247
  function taskLabelFor(state, taskId) {
1238
1248
  const step = planTaskById(state, taskId);
1239
1249
  const label = step?.label ?? step?.description ?? null;
1240
- if (label && !/^Step \d+$/.test(label)) return label;
1250
+ if (label && !/^Step \d+$/.test(label)) return compactLogLabel(label);
1241
1251
  return shortTaskLabel(taskId) || String(taskId ?? '') || 'task';
1242
1252
  }
1243
1253