@yeaft/webchat-agent 1.0.504 → 1.0.506

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 (100) hide show
  1. package/conversation.js +4 -2
  2. package/local-runtime/server/handlers/agent-conversation.js +11 -9
  3. package/local-runtime/server/handlers/client-conversation.js +28 -2
  4. package/local-runtime/version.json +1 -1
  5. package/local-runtime/web/app.bundle.js +99 -94
  6. package/local-runtime/web/app.bundle.js.gz +0 -0
  7. package/local-runtime/web/index.html +4 -3
  8. package/local-runtime/web/style.bundle.css +1 -1
  9. package/local-runtime/web/style.bundle.css.gz +0 -0
  10. package/local-runtime/web/vendor/katex/LICENSE +21 -0
  11. package/local-runtime/web/vendor/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
  12. package/local-runtime/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
  13. package/local-runtime/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  14. package/local-runtime/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  15. package/local-runtime/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  16. package/local-runtime/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  17. package/local-runtime/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  18. package/local-runtime/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  19. package/local-runtime/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  20. package/local-runtime/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  21. package/local-runtime/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  22. package/local-runtime/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  23. package/local-runtime/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  24. package/local-runtime/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  25. package/local-runtime/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  26. package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
  27. package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Bold.woff +0 -0
  28. package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  29. package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  30. package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  31. package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  32. package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
  33. package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Italic.woff +0 -0
  34. package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  35. package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
  36. package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Regular.woff +0 -0
  37. package/local-runtime/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  38. package/local-runtime/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  39. package/local-runtime/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  40. package/local-runtime/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  41. package/local-runtime/web/vendor/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
  42. package/local-runtime/web/vendor/katex/fonts/KaTeX_Math-Italic.woff +0 -0
  43. package/local-runtime/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  44. package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  45. package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  46. package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  47. package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  48. package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  49. package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  50. package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  51. package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  52. package/local-runtime/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  53. package/local-runtime/web/vendor/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
  54. package/local-runtime/web/vendor/katex/fonts/KaTeX_Script-Regular.woff +0 -0
  55. package/local-runtime/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  56. package/local-runtime/web/vendor/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
  57. package/local-runtime/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
  58. package/local-runtime/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  59. package/local-runtime/web/vendor/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
  60. package/local-runtime/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
  61. package/local-runtime/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  62. package/local-runtime/web/vendor/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
  63. package/local-runtime/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
  64. package/local-runtime/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  65. package/local-runtime/web/vendor/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
  66. package/local-runtime/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
  67. package/local-runtime/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  68. package/local-runtime/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  69. package/local-runtime/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  70. package/local-runtime/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  71. package/local-runtime/web/vendor/katex/katex.min.css +1 -0
  72. package/local-runtime/web/vendor.bundle.js +1 -0
  73. package/local-runtime/web/vendor.bundle.js.gz +0 -0
  74. package/package.json +1 -1
  75. package/yeaft/cli.js +3 -0
  76. package/yeaft/config-api.js +16 -5
  77. package/yeaft/config.js +11 -2
  78. package/yeaft/conversation/history-index-worker.js +151 -0
  79. package/yeaft/conversation/history-index.js +96 -1
  80. package/yeaft/conversation/persist.js +21 -10
  81. package/yeaft/conversation/recall-relevance.js +103 -0
  82. package/yeaft/conversation/search.js +2 -1
  83. package/yeaft/debug-trace.js +15 -4
  84. package/yeaft/effort.js +159 -6
  85. package/yeaft/engine.js +180 -41
  86. package/yeaft/history-window.js +430 -5
  87. package/yeaft/llm/adapter.js +3 -2
  88. package/yeaft/llm/anthropic.js +97 -25
  89. package/yeaft/llm/openai-responses.js +71 -7
  90. package/yeaft/llm/provider-state.js +242 -0
  91. package/yeaft/llm/router.js +14 -2
  92. package/yeaft/llm/usage-accounting.js +3 -0
  93. package/yeaft/models.js +4 -1
  94. package/yeaft/snapshot-filter.js +1 -0
  95. package/yeaft/sub-agent/output-log.js +4 -0
  96. package/yeaft/sub-agent/prompt-queue.js +3 -0
  97. package/yeaft/sub-agent/runner.js +13 -2
  98. package/yeaft/tools/agent.js +3 -0
  99. package/yeaft/tools/send-message.js +2 -0
  100. package/yeaft/web-bridge.js +24 -17
package/yeaft/engine.js CHANGED
@@ -33,7 +33,9 @@ import {
33
33
  DEFAULT_PROJECT_DOC_MAX_BYTES,
34
34
  } from './sessions/project-doc.js';
35
35
  import { archiveToolResults } from './archive/tool-results.js';
36
- import { trimSnapshotForBudget } from './history-window.js';
36
+ import { trimSnapshotForBudget, estimateMessageTokens, buildHistoryBuckets } from './history-window.js';
37
+ import { recallConversationTurns } from './conversation/history-index.js';
38
+ import { parseSeqFromId } from './conversation/persist.js';
37
39
  import { isVpForeign, readContent as readScopeContent } from './memory/store.js';
38
40
  import { ActiveMemorySet } from './memory/ams.js';
39
41
  import { cleanMemoryPromptText } from './memory/prompt-cleanup.js';
@@ -42,7 +44,8 @@ import { perfNowMs, recordAgentPerfTrace } from './perf-trace.js';
42
44
  // Default thread marker for legacy / non-group flows. Group VP runtime may
43
45
  // pass a real threadId per (sessionId, vpId, threadId) engine instance.
44
46
  const MAIN_THREAD_ID = 'main';
45
- import { pickEffort, parseEffortPrefix } from './effort.js';
47
+ import { pickEffort, parseEffortPrefix, snapshotEffortDecision } from './effort.js';
48
+ import { bindProviderState } from './llm/provider-state.js';
46
49
  import { DEFAULT_CONTEXT_WINDOW, normalizeEffort, resolveContextWindow, resolveModel } from './models.js';
47
50
  import { lookupModelLimitSync } from './llm/models-dev.js';
48
51
  import { attachRouterPlan, extractPriorPlan, stripMetaForWire } from './router/continuity.js';
@@ -90,7 +93,8 @@ const MAX_CONTINUE_TURNS = 3;
90
93
  * network, or subprocess reads. Only tools whose metadata explicitly declares
91
94
  * both read-only and concurrency-safe execution enter this lane.
92
95
  */
93
- const MAX_CONCURRENT_READ_ONLY_TOOLS = 4;
96
+ // Safe shared tools run together within the finite provider batch. Unsafe tools
97
+ // form exclusive barriers; resource-specific limits belong to the owning tool.
94
98
 
95
99
  /** Maximum silence while a visible turn waits for a result-producing task. */
96
100
  const DEFAULT_ASYNC_TASK_WAIT_TIMEOUT_MS = 120_000;
@@ -1390,6 +1394,8 @@ export class Engine {
1390
1394
  #buildToolContext(signal, vpCtx) {
1391
1395
  return {
1392
1396
  signal,
1397
+ effortDecision: snapshotEffortDecision(vpCtx?.effortDecision),
1398
+ requestIdentity: vpCtx?.requestIdentity,
1393
1399
  yeaftDir: this.#yeaftDir,
1394
1400
  managedCliReady: this.#managedCliReady,
1395
1401
  runtimePlatform: getRuntimePlatformInfo(),
@@ -1589,6 +1595,7 @@ export class Engine {
1589
1595
  if (message.toolCallId) record.toolCallId = message.toolCallId;
1590
1596
  if (Array.isArray(message.toolCalls) && message.toolCalls.length > 0) record.toolCalls = message.toolCalls;
1591
1597
  if (Array.isArray(message.thinkingBlocks) && message.thinkingBlocks.length > 0) record.thinkingBlocks = message.thinkingBlocks;
1598
+ if (message.providerState) record.providerState = message.providerState;
1592
1599
  if (message.isError) record.isError = true;
1593
1600
  if (message.imageAssetAnchor) record.imageAssetAnchor = true;
1594
1601
  if (message._reflection) record._reflection = true;
@@ -1623,7 +1630,7 @@ export class Engine {
1623
1630
  : message.content != null;
1624
1631
  const hasToolCalls = Array.isArray(message.toolCalls) && message.toolCalls.length > 0;
1625
1632
  const hasThinking = Array.isArray(message.thinkingBlocks) && message.thinkingBlocks.length > 0;
1626
- if (!hasContent && !hasToolCalls && !hasThinking && message.role !== 'tool') return null;
1633
+ if (!hasContent && !hasToolCalls && !hasThinking && !message.providerState && message.role !== 'tool') return null;
1627
1634
  return this.#conversationStore.append(this.#conversationRecord(message, context));
1628
1635
  }
1629
1636
 
@@ -1875,7 +1882,7 @@ export class Engine {
1875
1882
  }
1876
1883
  }
1877
1884
 
1878
- async *#queryLifecycle({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, projectSessionIds = null, projectInstruction = '', projectLabel = '', vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, causalRootId = null, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, prepareProviderRequest = null, startProviderRequest = null, finishProviderRequest = null, failProviderRequest = null, closePendingUserInput = null, collabToolPolicy = null } = {}) {
1885
+ async *#queryLifecycle({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', isSubAgent = false, parentEffortDecision = null, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, projectSessionIds = null, projectInstruction = '', projectLabel = '', vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, currentUserMessage = null, causalRootId = null, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, prepareProviderRequest = null, startProviderRequest = null, finishProviderRequest = null, failProviderRequest = null, closePendingUserInput = null, collabToolPolicy = null } = {}) {
1879
1886
  if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
1880
1887
  const error = new Error('prompt is required and must be a non-empty string');
1881
1888
  yield {
@@ -1967,7 +1974,7 @@ export class Engine {
1967
1974
  try {
1968
1975
  this.#currentThreadId = threadId || MAIN_THREAD_ID;
1969
1976
  this.#currentCausalRootId = effectiveCausalRootId;
1970
- yield* this.#runQuery({ prompt: effectivePrompt, promptParts: effectivePromptParts, messages, signal: runSignal, userEffort: explicitUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, projectSessionIds, projectInstruction, projectLabel, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted, causalRootId: effectiveCausalRootId, getCurrentTodos, setCurrentTodos, askUser, threadId: this.#currentThreadId, vpTurnId, drainPendingUserMessages, prepareProviderRequest, startProviderRequest, finishProviderRequest, failProviderRequest, closePendingUserInput, collabToolPolicy: effectiveCollabToolPolicy, explicitSkillName: parsedSkill.skillName, retryLifecycle });
1977
+ yield* this.#runQuery({ prompt: effectivePrompt, promptParts: effectivePromptParts, messages, signal: runSignal, userEffort: explicitUserEffort, scenario, isSubAgent, parentEffortDecision, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, projectSessionIds, projectInstruction, projectLabel, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted, currentUserMessage, causalRootId: effectiveCausalRootId, getCurrentTodos, setCurrentTodos, askUser, threadId: this.#currentThreadId, vpTurnId, drainPendingUserMessages, prepareProviderRequest, startProviderRequest, finishProviderRequest, failProviderRequest, closePendingUserInput, collabToolPolicy: effectiveCollabToolPolicy, explicitSkillName: parsedSkill.skillName, retryLifecycle });
1971
1978
  } finally {
1972
1979
  // Closing the async generator at a visible retry boundary means the
1973
1980
  // continuation never reached a provider. Keep it out of history and
@@ -2022,7 +2029,7 @@ export class Engine {
2022
2029
  * in a try/finally without indenting the whole loop.
2023
2030
  * @private
2024
2031
  */
2025
- async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, projectSessionIds = null, projectInstruction = '', projectLabel = '', vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, causalRootId = null, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, prepareProviderRequest = null, startProviderRequest = null, finishProviderRequest = null, failProviderRequest = null, closePendingUserInput = null, collabToolPolicy = null, explicitSkillName = null, retryLifecycle }) {
2032
+ async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', isSubAgent = false, parentEffortDecision = null, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, projectSessionIds = null, projectInstruction = '', projectLabel = '', vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, currentUserMessage = null, causalRootId = null, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, prepareProviderRequest = null, startProviderRequest = null, finishProviderRequest = null, failProviderRequest = null, closePendingUserInput = null, collabToolPolicy = null, explicitSkillName = null, retryLifecycle }) {
2026
2033
 
2027
2034
  const effectiveCollabToolPolicy = collabToolPolicy === COLLAB_TOOL_POLICY.SINGLE_VP || collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP
2028
2035
  ? collabToolPolicy
@@ -2047,6 +2054,15 @@ export class Engine {
2047
2054
  // standalone/CLI callers pass it per query.
2048
2055
  this.#sessionId = runtimeSessionId || null;
2049
2056
  this.#currentThreadId = runtimeThreadId;
2057
+ const requestIdentity = Object.freeze({
2058
+ instanceScope: this.#yeaftDir || '',
2059
+ ownerScope: this.#yeaftDir ? 'instance-local-owner' : '',
2060
+ sessionId: runtimeSessionId || this.#chatId || '',
2061
+ vpId: this.#vpId || senderVpId || vpPersona?.vpId || 'default',
2062
+ threadId: runtimeThreadId,
2063
+ });
2064
+ const effortConstraint = isSubAgent || scenario === 'sub_agent' || vpPersona?.subAgent
2065
+ ? Object.freeze({ parentDecision: snapshotEffortDecision(parentEffortDecision) }) : null;
2050
2066
  const queryStartedAt = Date.now();
2051
2067
  const userQuestionPreview = String(prompt || '').slice(0, 200);
2052
2068
  const queryVpId = vpPersona && typeof vpPersona === 'object'
@@ -2079,10 +2095,59 @@ export class Engine {
2079
2095
  // memory pre-flow or provider request can fail. The Web Session bridge
2080
2096
  // already writes one shared user row before multi-VP fan-out, so those
2081
2097
  // callers set userAlreadyPersisted and every VP skips this append.
2098
+ const internalTrigger = !!inboundEnvelope?.msg?.meta?.injectedBy;
2099
+ let persistedQueryUser = currentUserMessage;
2082
2100
  if (!userAlreadyPersisted) {
2083
- this.#persistConversationMessage({ role: 'user', content: prompt, userAuthored: true }, {
2084
- sessionId: runtimeSessionId,
2085
- });
2101
+ persistedQueryUser = this.#persistConversationMessage({
2102
+ role: internalTrigger ? 'assistant' : 'user', content: prompt,
2103
+ ...(internalTrigger ? { internal: true } : { userAuthored: true }),
2104
+ }, { sessionId: runtimeSessionId });
2105
+ }
2106
+ // Native Session history, including internal wakeups, never comes from
2107
+ // Dream. Work Center and scoped child agents keep their memory contracts.
2108
+ const useMessageHistory = scenario !== 'work-item' && !!runtimeSessionId && !vpPersona?.subAgent;
2109
+ const useDreamMemory = scenario === 'work-item' || !!vpPersona?.subAgent
2110
+ || (!runtimeSessionId && !internalTrigger);
2111
+ const recentTurnCap = this.#config.yeaft?.recentTurnsLimit ?? 20;
2112
+ const relatedTurnCap = this.#config.yeaft?.relatedTurnsLimit ?? 8;
2113
+ let relatedHistoryTurns = [];
2114
+ let historyRecallMeta = { source: 'messages', status: 'disabled' };
2115
+ if (useMessageHistory && !internalTrigger && this.#conversationStore?.loadRecentBySession) {
2116
+ // Bounded canonical rows, not the lossy bridge cache. Threads share the
2117
+ // Session transcript; only tool arcs and thinking are VP-private.
2118
+ const clientId = inboundEnvelope?.msg?.meta?.clientMessageId;
2119
+ if (!persistedQueryUser && clientId) {
2120
+ // Compatibility only: bridge callers carry the durable row directly.
2121
+ // A missing identity outside this bounded lookup fails closed below.
2122
+ persistedQueryUser = this.#conversationStore.loadRecentBySession(runtimeSessionId, recentTurnCap + 1)
2123
+ .find(m => m.role === 'user' && m.clientMessageId === clientId);
2124
+ }
2125
+ const beforeSeq = Number.isFinite(persistedQueryUser?.seq)
2126
+ ? persistedQueryUser.seq : parseSeqFromId(persistedQueryUser?.id);
2127
+ if (Number.isFinite(beforeSeq)) {
2128
+ const tail = this.#conversationStore.loadRecentBySession(runtimeSessionId, recentTurnCap, { beforeSeq });
2129
+ messages = tail.filter(m => parseSeqFromId(m.id) < beforeSeq
2130
+ && (m.role !== 'tool' || !queryVpId || m.speakerVpId === queryVpId))
2131
+ .map(m => {
2132
+ if (m.role !== 'assistant' || !queryVpId || m.speakerVpId === queryVpId) return m;
2133
+ const { toolCalls, thinkingBlocks, ...textOnly } = m;
2134
+ return textOnly;
2135
+ });
2136
+ if (relatedTurnCap > 0 && this.#yeaftDir) {
2137
+ try {
2138
+ const recalled = await recallConversationTurns(this.#yeaftDir, runtimeSessionId, prompt, {
2139
+ beforeSeq, limit: relatedTurnCap,
2140
+ });
2141
+ relatedHistoryTurns = recalled.turns || [];
2142
+ historyRecallMeta = { source: 'messages', status: 'ready', ...recalled.meta };
2143
+ } catch (error) {
2144
+ // Cold/stale index degrades to recent history, never a full scan.
2145
+ historyRecallMeta = { source: 'messages', status: error?.code || 'unavailable' };
2146
+ }
2147
+ }
2148
+ } else {
2149
+ historyRecallMeta = { source: 'messages', status: 'missing_current_user_fence' };
2150
+ }
2086
2151
  }
2087
2152
 
2088
2153
  const perfTraceId = typeof inboundEnvelope?._perfTraceId === 'string' && inboundEnvelope._perfTraceId.trim()
@@ -2111,7 +2176,7 @@ export class Engine {
2111
2176
  let memoryInjection = '';
2112
2177
  let recallEntryCount = 0;
2113
2178
 
2114
- const topicScopesForMemory = await this.#loadSessionTopicScopes(sessionId);
2179
+ const topicScopesForMemory = !useDreamMemory ? [] : await this.#loadSessionTopicScopes(sessionId);
2115
2180
  const projectScopesForMemory = Array.isArray(projectSessionIds)
2116
2181
  ? projectSessionIds.flatMap(id => [
2117
2182
  `sessions/${id}`,
@@ -2119,7 +2184,7 @@ export class Engine {
2119
2184
  `group/${id}`,
2120
2185
  ])
2121
2186
  : [];
2122
- const recallResult = await this.#recallMemory(prompt, {
2187
+ const recallResult = !useDreamMemory ? { entries: [], meta: {} } : await this.#recallMemory(prompt, {
2123
2188
  sessionId,
2124
2189
  vpId: vpPersona && typeof vpPersona === 'object' && typeof vpPersona.vpId === 'string'
2125
2190
  ? vpPersona.vpId
@@ -2148,7 +2213,7 @@ export class Engine {
2148
2213
 
2149
2214
  // Load canonical content only for scopes selected by ranked FTS records.
2150
2215
  // summary.md remains catalog metadata and never enters the prompt.
2151
- const summaries = await this.#loadLayerASummaries({
2216
+ const summaries = !useDreamMemory ? {} : await this.#loadLayerASummaries({
2152
2217
  sessionId,
2153
2218
  vpId: vpPersona && typeof vpPersona === 'object' && typeof vpPersona.vpId === 'string'
2154
2219
  ? vpPersona.vpId
@@ -2167,7 +2232,7 @@ export class Engine {
2167
2232
  && typeof vpPersona.vpId === 'string'
2168
2233
  ? vpPersona.vpId
2169
2234
  : (typeof senderVpId === 'string' ? senderVpId : null);
2170
- const amsContext = this.#prepareAms({
2235
+ const amsContext = !useDreamMemory ? null : this.#prepareAms({
2171
2236
  sessionId,
2172
2237
  ownVpId: ownVpIdForAms,
2173
2238
  summaries,
@@ -2179,7 +2244,8 @@ export class Engine {
2179
2244
  }
2180
2245
  const loadedMemoryForDebug = loadedMemoryDebugEntries(amsContext?.snapshot);
2181
2246
  const loadedMemoryMetaForDebug = {
2182
- recallLimit: resolveMemoryRecallLimit(this.#config),
2247
+ ...(useMessageHistory ? historyRecallMeta : {}),
2248
+ recallLimit: useMessageHistory ? relatedTurnCap : resolveMemoryRecallLimit(this.#config),
2183
2249
  recallCandidates: Number.isFinite(recallResult?.meta?.hitCount)
2184
2250
  ? recallResult.meta.hitCount
2185
2251
  : (recallResult && Array.isArray(recallResult.entries) ? recallResult.entries.length : 0),
@@ -2224,7 +2290,7 @@ export class Engine {
2224
2290
  envelope: inboundEnvelope || null,
2225
2291
  };
2226
2292
 
2227
- const projectDocSource = this.#getProjectDocBlock(workDir);
2293
+ let projectDocSource = this.#getProjectDocBlock(workDir);
2228
2294
  let projectDocLoadedPathHints = [];
2229
2295
  let projectDocContext = selectProjectDocContext(projectDocSource, {
2230
2296
  prompt,
@@ -2356,11 +2422,14 @@ export class Engine {
2356
2422
  : prompt;
2357
2423
  }
2358
2424
  const conversationMessages = [
2359
- ...trimSnapshotForBudget(messages, {
2425
+ ...(useMessageHistory ? messages : trimSnapshotForBudget(messages, {
2360
2426
  messageTokenBudget: this.#config.messageTokenBudget,
2361
2427
  language: this.#config.language,
2362
- }),
2363
- { role: 'user', content: finalUserContent },
2428
+ })),
2429
+ { role: 'user', content: finalUserContent,
2430
+ ...(persistedQueryUser?.id ? { id: persistedQueryUser.id, seq: parseSeqFromId(persistedQueryUser.id) } : {}),
2431
+ ...(persistedQueryUser?.clientMessageId ? { clientMessageId: persistedQueryUser.clientMessageId } : {}),
2432
+ },
2364
2433
  ];
2365
2434
 
2366
2435
  const groupReflectionGate = shouldAllowGroupReflection({
@@ -2620,7 +2689,9 @@ export class Engine {
2620
2689
  });
2621
2690
  };
2622
2691
  const toolCalls = [];
2623
- const thinkingBlocks = []; // task-327d: collected from adapter for round-trip
2692
+ const thinkingBlocks = []; // Legacy adapter compatibility only.
2693
+ let providerState = null;
2694
+ let requestEffortDecision = snapshotEffortDecision();
2624
2695
  let stopReason = 'end_turn';
2625
2696
  const totalUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheInputDeltaTokens: 0 };
2626
2697
  // Raw provider exchange is diagnostic source data, not model context or
@@ -2741,7 +2812,7 @@ export class Engine {
2741
2812
  // routerPlan, the VP's role default, and the global config all
2742
2813
  // outrank the scenario picker for `'high'|'max'`. UI/userEffort
2743
2814
  // is already honoured by pickEffort (highest precedence).
2744
- if (vpPersona && vpPersona.vpId) {
2815
+ if (!requestUserEffort && vpPersona && vpPersona.vpId) {
2745
2816
  const priorPlan = extractPriorPlan(conversationMessages, vpPersona.vpId);
2746
2817
  const thinkingCfg = (this.#config && this.#config.thinking) || {};
2747
2818
  // PR-I: live routerPlan.thinking — when the dispatcher passes
@@ -2784,10 +2855,28 @@ export class Engine {
2784
2855
  // query tape remain complete; no summary is generated and no history
2785
2856
  // rows are rewritten. This also protects later tool-loop requests,
2786
2857
  // not just the initial snapshot assembled by the bridge.
2787
- const requestHistory = trimSnapshotForBudget(conversationMessages, {
2858
+ const continuationCost = pendingContinuationForRequest
2859
+ ? estimateMessageTokens(pendingContinuationForRequest) : 0;
2860
+ const historyBudget = Math.max(1, Math.min(
2861
+ requestConfig.messageTokenBudget || 32768,
2862
+ Math.floor(currentContextWindow * 0.75) - estimateMessagesTokens(systemPrompt, []),
2863
+ ) - continuationCost);
2864
+ const buckets = useMessageHistory ? buildHistoryBuckets(conversationMessages, {
2865
+ prompt,
2866
+ relatedTurns: relatedHistoryTurns,
2867
+ recentTurnCap: requestConfig.yeaft?.recentTurnsLimit ?? 20,
2868
+ relatedTurnCap: requestConfig.yeaft?.relatedTurnsLimit ?? 8,
2869
+ messageTokenBudget: historyBudget,
2870
+ currentTurnStartIndex: turnStartIdx,
2871
+ language: requestConfig.language,
2872
+ }) : null;
2873
+ const requestHistory = buckets?.messages || trimSnapshotForBudget(conversationMessages, {
2788
2874
  messageTokenBudget: requestConfig.messageTokenBudget,
2789
2875
  language: requestConfig.language,
2790
2876
  });
2877
+ if (buckets) this.#trace.log?.('history_buckets', {
2878
+ sessionId: runtimeSessionId, turnId: queryTurnId, ...historyRecallMeta, ...buckets.meta,
2879
+ });
2791
2880
  let wireMessages = stripMetaForWire(pendingContinuationForRequest
2792
2881
  ? [...requestHistory, pendingContinuationForRequest]
2793
2882
  : requestHistory);
@@ -2896,6 +2985,10 @@ export class Engine {
2896
2985
  tools: toolDefs.length > 0 ? toolDefs : undefined,
2897
2986
  maxTokens: requestConfig.maxOutputTokens || 16384,
2898
2987
  effort: resolvedEffort,
2988
+ effortConstraint,
2989
+ requestIdentity,
2990
+ onEffortDecision: decision => { requestEffortDecision = snapshotEffortDecision(decision); },
2991
+ onProviderDiagnostics: info => traceRequest('llm.capabilities', { detail: info }),
2899
2992
  effortSource: requestUserEffort ? 'user' : 'auto',
2900
2993
  signal,
2901
2994
  onRawExchange: captureRawExchange,
@@ -2956,6 +3049,9 @@ export class Engine {
2956
3049
  responseText += event.text;
2957
3050
  yield event;
2958
3051
  break;
3052
+ case 'provider_state':
3053
+ providerState = event.providerState;
3054
+ break;
2959
3055
  case 'thinking_delta':
2960
3056
  yield event;
2961
3057
  break;
@@ -2993,6 +3089,9 @@ export class Engine {
2993
3089
  const cacheInputDeltaTokens = event.cacheTokensAreIncludedInInput ? 0 : cacheReadTokens + cacheWriteTokens;
2994
3090
  totalUsage.inputTokens += inputTokens;
2995
3091
  totalUsage.outputTokens += outputTokens;
3092
+ if (Number.isFinite(event.reasoningTokens) && event.reasoningTokens >= 0) {
3093
+ totalUsage.reasoningTokens = (totalUsage.reasoningTokens || 0) + event.reasoningTokens;
3094
+ }
2996
3095
  totalUsage.cacheReadTokens += cacheReadTokens;
2997
3096
  totalUsage.cacheWriteTokens += cacheWriteTokens;
2998
3097
  totalUsage.cacheInputDeltaTokens += cacheInputDeltaTokens;
@@ -3089,6 +3188,7 @@ export class Engine {
3089
3188
  usage: {
3090
3189
  inputTokens: totalUsage.inputTokens || 0,
3091
3190
  outputTokens: totalUsage.outputTokens || 0,
3191
+ ...(totalUsage.reasoningTokens !== undefined ? { reasoningTokens: totalUsage.reasoningTokens } : {}),
3092
3192
  cacheReadTokens: totalUsage.cacheReadTokens || 0,
3093
3193
  cacheWriteTokens: totalUsage.cacheWriteTokens || 0,
3094
3194
  totalInputTokens: (totalUsage.inputTokens || 0) + (totalUsage.cacheInputDeltaTokens || 0),
@@ -3335,6 +3435,7 @@ export class Engine {
3335
3435
  usage: {
3336
3436
  inputTokens: totalUsage.inputTokens || 0,
3337
3437
  outputTokens: totalUsage.outputTokens || 0,
3438
+ ...(totalUsage.reasoningTokens !== undefined ? { reasoningTokens: totalUsage.reasoningTokens } : {}),
3338
3439
  cacheReadTokens: totalUsage.cacheReadTokens || 0,
3339
3440
  cacheWriteTokens: totalUsage.cacheWriteTokens || 0,
3340
3441
  totalInputTokens: (totalUsage.inputTokens || 0) + (totalUsage.cacheInputDeltaTokens || 0),
@@ -3361,6 +3462,7 @@ export class Engine {
3361
3462
  usage: {
3362
3463
  inputTokens: totalUsage.inputTokens || 0,
3363
3464
  outputTokens: errLoopOutputTokens,
3465
+ ...(totalUsage.reasoningTokens !== undefined ? { reasoningTokens: totalUsage.reasoningTokens } : {}),
3364
3466
  cacheReadTokens: totalUsage.cacheReadTokens || 0,
3365
3467
  cacheWriteTokens: totalUsage.cacheWriteTokens || 0,
3366
3468
  totalInputTokens: errLoopInputTokens,
@@ -3438,6 +3540,7 @@ export class Engine {
3438
3540
  usage: {
3439
3541
  inputTokens: totalUsage.inputTokens || 0,
3440
3542
  outputTokens: totalUsage.outputTokens || 0,
3543
+ ...(totalUsage.reasoningTokens !== undefined ? { reasoningTokens: totalUsage.reasoningTokens } : {}),
3441
3544
  cacheReadTokens: totalUsage.cacheReadTokens || 0,
3442
3545
  cacheWriteTokens: totalUsage.cacheWriteTokens || 0,
3443
3546
  totalInputTokens: turnInputTokens,
@@ -3460,13 +3563,10 @@ export class Engine {
3460
3563
  input: tc.input,
3461
3564
  }));
3462
3565
  }
3463
- if (thinkingBlocks.length > 0) {
3464
- assistantMsg.thinkingBlocks = thinkingBlocks.map(tb => (
3465
- tb.redacted
3466
- ? { redacted: true, data: tb.data, signature: tb.signature }
3467
- : { thinking: tb.thinking, signature: tb.signature }
3468
- ));
3469
- }
3566
+ const boundProviderState = bindProviderState(providerState, assistantMsg);
3567
+ if (boundProviderState) assistantMsg.providerState = boundProviderState;
3568
+ // New private reasoning is persisted only with verified provider ownership;
3569
+ // never downgrade it to the origin-free legacy thinkingBlocks format.
3470
3570
  if (vpPersona && vpPersona.vpId) {
3471
3571
  const planForThisVp = (vpPlan && typeof vpPlan === 'object'
3472
3572
  && typeof vpPlan.vpId === 'string' && vpPlan.vpId === vpPersona.vpId)
@@ -3536,6 +3636,7 @@ export class Engine {
3536
3636
  usage: {
3537
3637
  inputTokens: totalUsage.inputTokens || 0,
3538
3638
  outputTokens: loopOutputTokens,
3639
+ ...(totalUsage.reasoningTokens !== undefined ? { reasoningTokens: totalUsage.reasoningTokens } : {}),
3539
3640
  cacheReadTokens: totalUsage.cacheReadTokens || 0,
3540
3641
  cacheWriteTokens: totalUsage.cacheWriteTokens || 0,
3541
3642
  totalInputTokens: loopInputTokens,
@@ -3841,6 +3942,8 @@ export class Engine {
3841
3942
  // We re-create the closure each iteration because endTurnRequested
3842
3943
  // is a per-query local (reset implicitly at the top of #runQuery).
3843
3944
  const toolCtx = this.#buildToolContext(signal, {
3945
+ effortDecision: snapshotEffortDecision(requestEffortDecision),
3946
+ requestIdentity,
3844
3947
  router,
3845
3948
  senderVpId,
3846
3949
  sessionId: runtimeSessionId,
@@ -4021,9 +4124,8 @@ export class Engine {
4021
4124
  && duplicatePolicyForCall(tc) !== 'suppress'
4022
4125
  && !mayMutateWorkspaceAfterReturn(this, tc.name, tc.input)) {
4023
4126
  const parallelCalls = [];
4024
- const segmentCacheKeys = new Set();
4025
4127
  for (let candidateIndex = toolCallIndex;
4026
- candidateIndex < toolCalls.length && parallelCalls.length < MAX_CONCURRENT_READ_ONLY_TOOLS;
4128
+ candidateIndex < toolCalls.length;
4027
4129
  candidateIndex += 1) {
4028
4130
  const candidate = toolCalls[candidateIndex];
4029
4131
  if (!toolAllowedForRequest(candidate)
@@ -4032,21 +4134,27 @@ export class Engine {
4032
4134
  || mayMutateWorkspaceAfterReturn(this, candidate.name, candidate.input)) break;
4033
4135
  const candidateKey = `${candidate.name}\u001f${argsHashOf(candidate.input)}`;
4034
4136
  const candidateCacheable = isCacheableTool(this, candidate.name, candidate.input);
4035
- // Keep identical cacheable reads on the serial commit path so the
4036
- // second call reuses the first result instead of duplicating I/O.
4037
- if (candidateCacheable
4038
- && (readOnlyToolResults.has(candidateKey) || segmentCacheKeys.has(candidateKey))) break;
4137
+ // Already committed reads reuse their result on the commit path.
4138
+ if (!readOnlyToolReuseDisabled && candidateCacheable
4139
+ && readOnlyToolResults.has(candidateKey)) break;
4039
4140
  parallelCalls.push(candidate);
4040
- if (candidateCacheable) segmentCacheKeys.add(candidateKey);
4041
4141
  }
4042
4142
 
4043
4143
  if (parallelCalls.length > 1) {
4044
4144
  const executions = [];
4145
+ const inFlightReads = new Map();
4045
4146
  for (const call of parallelCalls) {
4046
- if (signal?.aborted) {
4047
- abortedDuringTools = true;
4147
+ if (signal?.aborted || toolBatchBarrier) {
4148
+ if (signal?.aborted) abortedDuringTools = true;
4048
4149
  break;
4049
4150
  }
4151
+ const cacheKey = `${call.name}\u001f${argsHashOf(call.input)}`;
4152
+ const cacheable = !readOnlyToolReuseDisabled && isCacheableTool(this, call.name, call.input);
4153
+ const sharedExecution = cacheable ? inFlightReads.get(cacheKey) : null;
4154
+ if (sharedExecution) {
4155
+ executions.push(sharedExecution.then(result => ({ ...result, call, shared: true })));
4156
+ continue;
4157
+ }
4050
4158
  announcedParallelToolCalls.add(call.id);
4051
4159
  yield {
4052
4160
  type: 'tool_start',
@@ -4059,7 +4167,8 @@ export class Engine {
4059
4167
  abortedDuringTools = true;
4060
4168
  break;
4061
4169
  }
4062
- executions.push((async () => {
4170
+ if (toolBatchBarrier) break;
4171
+ const execution = (async () => {
4063
4172
  const startedAt = Date.now();
4064
4173
  const callContext = toolContextForCall(call);
4065
4174
  const toolErrorOutput = this.#toolRegistry
@@ -4073,7 +4182,9 @@ export class Engine {
4073
4182
  } catch (error) {
4074
4183
  return { call, startedAt, durationMs: Date.now() - startedAt, error, toolErrorOutput };
4075
4184
  }
4076
- })());
4185
+ })();
4186
+ executions.push(execution);
4187
+ if (cacheable) inFlightReads.set(cacheKey, execution);
4077
4188
  }
4078
4189
  const completed = await Promise.all(executions);
4079
4190
  for (const execution of completed) {
@@ -4130,7 +4241,12 @@ export class Engine {
4130
4241
  const missingProjectDocScopes = hasTool && !readOnlyTool
4131
4242
  ? projectDocWriteScopesNeedingReload(projectDocContext, toolProjectDocPathHints)
4132
4243
  : new Set();
4133
- const needsProjectDocReload = missingProjectDocScopes.size > 0;
4244
+ // A changed rule source was not present in the request that generated
4245
+ // this write, even when its scope label is unchanged. Never replay it.
4246
+ const freshProjectDocSource = hasTool && !readOnlyTool
4247
+ ? this.#getProjectDocBlock(workDir) : projectDocSource;
4248
+ const projectDocSourceChanged = freshProjectDocSource !== projectDocSource;
4249
+ const needsProjectDocReload = missingProjectDocScopes.size > 0 || projectDocSourceChanged;
4134
4250
 
4135
4251
  if (abortSkipped) {
4136
4252
  output = `Skipped ${tc.name} because the turn was aborted before this tool started.`;
@@ -4173,6 +4289,7 @@ export class Engine {
4173
4289
  isError = true;
4174
4290
  yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: true, threadId: this.currentThreadId };
4175
4291
  } else if (needsProjectDocReload) {
4292
+ projectDocSource = freshProjectDocSource;
4176
4293
  projectDocLoadedPathHints = [...new Set([
4177
4294
  ...projectDocLoadedPathHints,
4178
4295
  ...toolProjectDocPathHints,
@@ -4319,6 +4436,15 @@ export class Engine {
4319
4436
 
4320
4437
  currentToolCallForAsyncTask = null;
4321
4438
 
4439
+ // Record concrete inputs, never tool/file prose. Apply only after the
4440
+ // whole batch: a read cannot authorize a same-response write whose
4441
+ // model has not yet received the newly selected rules.
4442
+ if (hasTool && readOnlyTool && !skipped && !isError) {
4443
+ projectDocLoadedPathHints = [...new Set([
4444
+ ...projectDocLoadedPathHints, ...toolProjectDocPathHints,
4445
+ ])].slice(-128);
4446
+ }
4447
+
4322
4448
  if (!skipped && !duplicateCallSuppressed && !isError && !reusedReadOnlyResult
4323
4449
  && duplicateCallPolicy !== 'allow') {
4324
4450
  const nextDuplicateCount = successfulDuplicateCount + 1;
@@ -4446,6 +4572,19 @@ export class Engine {
4446
4572
  if (fatalToolError) throw fatalToolError;
4447
4573
  }
4448
4574
 
4575
+ // Preload rules for observed read paths before the next provider input.
4576
+ // No tool from the just-completed response can benefit retroactively.
4577
+ projectDocSource = this.#getProjectDocBlock(workDir);
4578
+ const nextProjectDocContext = selectProjectDocContext(projectDocSource, {
4579
+ prompt, messages, pathHints: projectDocLoadedPathHints,
4580
+ forcedScopes: [...projectDocContext.selectedScopes],
4581
+ language: this.#config.language || 'en',
4582
+ });
4583
+ if (nextProjectDocContext.text !== projectDocContext.text) {
4584
+ projectDocContext = nextProjectDocContext;
4585
+ systemPrompt = buildCurrentSystemPrompt();
4586
+ }
4587
+
4449
4588
  // PR-L: flush any duplicate-call reminders queued during the batch.
4450
4589
  // Pushed AFTER the for-loop so the tool_use → tool_result pairing
4451
4590
  // is intact; the next adapter.stream() will see the reminder as a