create-walle 0.9.25 → 0.9.26

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 (179) hide show
  1. package/README.md +8 -0
  2. package/bin/create-walle.js +815 -45
  3. package/package.json +2 -2
  4. package/template/bin/ctm-dev-cleanup.js +90 -4
  5. package/template/bin/ctm-launch.sh +49 -1
  6. package/template/bin/dev.sh +45 -1
  7. package/template/bin/ensure-stable-node.js +132 -0
  8. package/template/bin/install-service.sh +9 -0
  9. package/template/claude-task-manager/api-prompts.js +899 -119
  10. package/template/claude-task-manager/approval-agent.js +360 -40
  11. package/template/claude-task-manager/bin/ctm-disclaim.c +42 -0
  12. package/template/claude-task-manager/bin/ctm-hotkey.swift +67 -81
  13. package/template/claude-task-manager/bin/ctm-screen-auth.swift +37 -0
  14. package/template/claude-task-manager/bin/install-hotkey.sh +97 -49
  15. package/template/claude-task-manager/bin/restart-ctm.sh +14 -0
  16. package/template/claude-task-manager/db.js +399 -48
  17. package/template/claude-task-manager/docs/approval-hook-sandbox.md +84 -0
  18. package/template/claude-task-manager/docs/codex-app-server-approvals.md +72 -0
  19. package/template/claude-task-manager/docs/codex-native-sandbox.md +47 -0
  20. package/template/claude-task-manager/docs/prompt-editing-tree-design.md +18 -1
  21. package/template/claude-task-manager/lib/approval-hook.js +200 -0
  22. package/template/claude-task-manager/lib/approval-self-adapt.js +1 -0
  23. package/template/claude-task-manager/lib/auth-rules.js +11 -0
  24. package/template/claude-task-manager/lib/background-llm.js +32 -4
  25. package/template/claude-task-manager/lib/codesign-identity.js +140 -0
  26. package/template/claude-task-manager/lib/codex-app-server-client.js +119 -0
  27. package/template/claude-task-manager/lib/codex-approval-bridge.js +118 -0
  28. package/template/claude-task-manager/lib/codex-history-terminal-renderer.js +571 -0
  29. package/template/claude-task-manager/lib/codex-paths.js +73 -0
  30. package/template/claude-task-manager/lib/codex-rollout-snapshot.js +164 -0
  31. package/template/claude-task-manager/lib/codex-rollout-tail.js +72 -0
  32. package/template/claude-task-manager/lib/codex-sandbox-args.js +47 -0
  33. package/template/claude-task-manager/lib/coding-agent-models.js +118 -71
  34. package/template/claude-task-manager/lib/command-targets.js +163 -0
  35. package/template/claude-task-manager/lib/conversation-tail-merge.js +61 -19
  36. package/template/claude-task-manager/lib/db-owner-worker-client.js +29 -1
  37. package/template/claude-task-manager/lib/escalation-review.js +80 -3
  38. package/template/claude-task-manager/lib/flow-control.js +52 -0
  39. package/template/claude-task-manager/lib/fs-watcher.js +24 -15
  40. package/template/claude-task-manager/lib/ingest-cooldown.js +68 -0
  41. package/template/claude-task-manager/lib/jsonl-conversation-parser.js +8 -4
  42. package/template/claude-task-manager/lib/launchd-recovery.js +92 -0
  43. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +207 -52
  44. package/template/claude-task-manager/lib/mobile-push-store.js +7 -0
  45. package/template/claude-task-manager/lib/model-overview-brain-fallback.js +102 -1
  46. package/template/claude-task-manager/lib/model-overview-cache.js +1 -0
  47. package/template/claude-task-manager/lib/oauth-proxy-supervisor.js +2 -1
  48. package/template/claude-task-manager/lib/perf-tracker.js +29 -2
  49. package/template/claude-task-manager/lib/permission-match.js +146 -16
  50. package/template/claude-task-manager/lib/project-slug.js +33 -0
  51. package/template/claude-task-manager/lib/prompt-intent.js +51 -4
  52. package/template/claude-task-manager/lib/read-pool-client.js +48 -3
  53. package/template/claude-task-manager/lib/real-node.js +73 -0
  54. package/template/claude-task-manager/lib/runtime-work-registry.js +131 -14
  55. package/template/claude-task-manager/lib/session-content-backfill.js +24 -5
  56. package/template/claude-task-manager/lib/session-diagnostics-batch.js +87 -0
  57. package/template/claude-task-manager/lib/session-history.js +5 -7
  58. package/template/claude-task-manager/lib/session-host-manager.js +19 -0
  59. package/template/claude-task-manager/lib/session-jobs.js +6 -0
  60. package/template/claude-task-manager/lib/session-message-response-cache.js +89 -0
  61. package/template/claude-task-manager/lib/session-messages-page.js +211 -0
  62. package/template/claude-task-manager/lib/session-messages-projection.js +170 -0
  63. package/template/claude-task-manager/lib/session-standup.js +8 -0
  64. package/template/claude-task-manager/lib/session-timeline-summary.js +16 -2
  65. package/template/claude-task-manager/lib/session-token-usage.js +30 -8
  66. package/template/claude-task-manager/lib/session-workspace-binding.js +29 -15
  67. package/template/claude-task-manager/lib/storage-migration.js +2 -1
  68. package/template/claude-task-manager/lib/transcript-store.js +179 -12
  69. package/template/claude-task-manager/lib/walle-ctm-history.js +298 -11
  70. package/template/claude-task-manager/lib/walle-permission-reply.js +49 -0
  71. package/template/claude-task-manager/lib/walle-session-cache.js +22 -1
  72. package/template/claude-task-manager/lib/walle-supervisor.js +42 -3
  73. package/template/claude-task-manager/package.json +5 -2
  74. package/template/claude-task-manager/prompt-harvest.js +31 -11
  75. package/template/claude-task-manager/providers/claude-code.js +29 -1
  76. package/template/claude-task-manager/providers/codex.js +13 -1
  77. package/template/claude-task-manager/public/css/setup.css +11 -0
  78. package/template/claude-task-manager/public/css/walle-session.css +132 -4
  79. package/template/claude-task-manager/public/css/walle.css +89 -0
  80. package/template/claude-task-manager/public/icon-16.png +0 -0
  81. package/template/claude-task-manager/public/icon-32.png +0 -0
  82. package/template/claude-task-manager/public/icon-512.png +0 -0
  83. package/template/claude-task-manager/public/index.html +2483 -165
  84. package/template/claude-task-manager/public/js/activation-render-check.js +55 -0
  85. package/template/claude-task-manager/public/js/flow-control-policy.js +52 -0
  86. package/template/claude-task-manager/public/js/message-renderer.js +60 -1
  87. package/template/claude-task-manager/public/js/prompts.js +13 -1
  88. package/template/claude-task-manager/public/js/session-status-precedence.js +9 -3
  89. package/template/claude-task-manager/public/js/setup.js +54 -10
  90. package/template/claude-task-manager/public/js/stream-resize-policy.js +80 -0
  91. package/template/claude-task-manager/public/js/stream-view.js +78 -0
  92. package/template/claude-task-manager/public/js/terminal-reconciler.js +52 -2
  93. package/template/claude-task-manager/public/js/tool-state.js +155 -0
  94. package/template/claude-task-manager/public/js/walle-session.js +887 -326
  95. package/template/claude-task-manager/public/js/walle.js +306 -195
  96. package/template/claude-task-manager/public/m/app.css +1 -0
  97. package/template/claude-task-manager/public/m/app.js +33 -3
  98. package/template/claude-task-manager/queue-engine.js +45 -1
  99. package/template/claude-task-manager/server.js +3367 -540
  100. package/template/claude-task-manager/workers/approval-blocklist.js +130 -17
  101. package/template/claude-task-manager/workers/db-owner-worker.js +31 -1
  102. package/template/claude-task-manager/workers/read-pool-worker.js +92 -5
  103. package/template/claude-task-manager/workers/session-host-process.js +10 -0
  104. package/template/claude-task-manager/workers/state-detectors/codex.js +58 -7
  105. package/template/package.json +2 -3
  106. package/template/shared/icons/AppIcon-ctm.icns +0 -0
  107. package/template/shared/icons/AppIcon-walle.icns +0 -0
  108. package/template/wall-e/agent.js +139 -18
  109. package/template/wall-e/api-walle.js +201 -22
  110. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +1981 -0
  111. package/template/wall-e/brain.js +1049 -39
  112. package/template/wall-e/chat.js +427 -86
  113. package/template/wall-e/coding/acceptance-contract.js +26 -1
  114. package/template/wall-e/coding/action-memory-policy.js +353 -0
  115. package/template/wall-e/coding/action-memory-store.js +814 -0
  116. package/template/wall-e/coding/initial-messages.js +197 -0
  117. package/template/wall-e/coding/no-progress-guard.js +327 -0
  118. package/template/wall-e/coding/permission-service.js +88 -22
  119. package/template/wall-e/coding/session-workspaces.js +81 -0
  120. package/template/wall-e/coding/shell-sandbox.js +124 -0
  121. package/template/wall-e/coding/stream-processor.js +63 -2
  122. package/template/wall-e/coding/tool-execution-controller.js +14 -1
  123. package/template/wall-e/coding/tool-registry.js +1 -1
  124. package/template/wall-e/coding/transcript-writer.js +3 -0
  125. package/template/wall-e/coding-orchestrator.js +636 -35
  126. package/template/wall-e/coding-prompts.js +51 -2
  127. package/template/wall-e/docs/model-routing-policy.md +59 -0
  128. package/template/wall-e/docs/walle-shell-sandbox.md +61 -0
  129. package/template/wall-e/extraction/knowledge-extractor.js +76 -23
  130. package/template/wall-e/http/chat-api.js +30 -12
  131. package/template/wall-e/http/model-admin.js +93 -1
  132. package/template/wall-e/lib/background-lanes.js +133 -0
  133. package/template/wall-e/lib/boot-profile.js +11 -0
  134. package/template/wall-e/lib/brain-owner-worker-client.js +324 -0
  135. package/template/wall-e/lib/brain-read-pool-client.js +311 -0
  136. package/template/wall-e/lib/diagnostics-flags.js +87 -0
  137. package/template/wall-e/lib/event-loop-monitor.js +74 -3
  138. package/template/wall-e/lib/mcp-integration.js +7 -1
  139. package/template/wall-e/lib/real-node.js +98 -0
  140. package/template/wall-e/lib/runtime-health.js +206 -0
  141. package/template/wall-e/lib/runtime-worker-pool.js +101 -0
  142. package/template/wall-e/lib/scheduler-worker-jobs.js +231 -0
  143. package/template/wall-e/lib/scheduler.js +446 -17
  144. package/template/wall-e/lib/service-health.js +61 -2
  145. package/template/wall-e/lib/service-readiness.js +258 -0
  146. package/template/wall-e/lib/usage.js +152 -0
  147. package/template/wall-e/lib/worker-thread-pool.js +389 -0
  148. package/template/wall-e/llm/client.js +81 -4
  149. package/template/wall-e/llm/default-fallback.js +54 -8
  150. package/template/wall-e/llm/mlx.js +536 -73
  151. package/template/wall-e/llm/mlx.plugin.json +1 -1
  152. package/template/wall-e/llm/ollama.js +342 -43
  153. package/template/wall-e/llm/provider-error.js +18 -1
  154. package/template/wall-e/llm/provider-health-state.js +176 -0
  155. package/template/wall-e/llm/routing-policy.js +796 -0
  156. package/template/wall-e/llm/supported-models.js +5 -0
  157. package/template/wall-e/loops/tasks.js +60 -14
  158. package/template/wall-e/loops/think.js +89 -24
  159. package/template/wall-e/mcp-server.js +192 -28
  160. package/template/wall-e/server.js +32 -7
  161. package/template/wall-e/skills/script-skill-runner.js +8 -1
  162. package/template/wall-e/skills/skill-planner.js +64 -1
  163. package/template/wall-e/tools/builtin-middleware.js +67 -2
  164. package/template/wall-e/tools/local-tools.js +116 -26
  165. package/template/wall-e/tools/permission-checker.js +52 -4
  166. package/template/wall-e/tools/permission-rules.js +36 -0
  167. package/template/wall-e/tools/shell-analyzer.js +46 -1
  168. package/template/wall-e/training/gemma-e4b-qlora.js +314 -0
  169. package/template/wall-e/training/real-trajectory-miner.js +2617 -0
  170. package/template/wall-e/training/replay-eval-analysis.js +151 -0
  171. package/template/wall-e/training/run-shell-command-selector.js +277 -0
  172. package/template/wall-e/training/tool-sft-dataset.js +312 -0
  173. package/template/wall-e/training/tool-sft-renderers.js +144 -0
  174. package/template/wall-e/training/tool-trace-harvester.js +1440 -0
  175. package/template/wall-e/training/trajectory-action-selector.js +364 -0
  176. package/template/wall-e/weather-runtime.js +232 -0
  177. package/template/wall-e/workers/brain-owner-worker.js +162 -0
  178. package/template/wall-e/workers/brain-read-worker.js +148 -0
  179. package/template/wall-e/workers/runtime-worker.js +145 -0
@@ -60,6 +60,77 @@ function getLlmClient() { if (!_llmClient) _llmClient = require('./llm/client');
60
60
  function getLocalTools() { if (!_localTools) _localTools = require('./tools/local-tools'); return _localTools; }
61
61
  function getActivityLog() { if (!_activityLog) _activityLog = require('./lib/activity-log'); return _activityLog; }
62
62
 
63
+ function createActionMemoryPolicyForRun(opts = {}, { logger = console } = {}) {
64
+ if (opts.actionMemoryPolicy !== undefined) return opts.actionMemoryPolicy;
65
+ const mode = String(
66
+ opts.actionMemoryMode
67
+ || (opts.actionMemoryStore ? 'audit' : process.env.WALL_E_ACTION_MEMORY_MODE)
68
+ || 'off'
69
+ ).trim().toLowerCase();
70
+ if (!mode || mode === 'off' || mode === '0' || mode === 'false') return null;
71
+ const { ActionMemoryPolicy } = require('./coding/action-memory-policy');
72
+ const { ActionMemoryStore } = require('./coding/action-memory-store');
73
+ const store = opts.actionMemoryStore || new ActionMemoryStore({
74
+ dbPath: opts.actionMemoryDbPath,
75
+ artifactsDir: opts.actionMemoryArtifactsDir,
76
+ logger,
77
+ });
78
+ return new ActionMemoryPolicy({
79
+ store,
80
+ mode,
81
+ selector: opts.actionMemorySelector || process.env.WALL_E_ACTION_MEMORY_SELECTOR,
82
+ minScore: opts.actionMemoryMinScore || process.env.WALL_E_ACTION_MEMORY_MIN_SCORE,
83
+ minMargin: opts.actionMemoryMinMargin || process.env.WALL_E_ACTION_MEMORY_MIN_MARGIN,
84
+ limit: opts.actionMemoryLimit || process.env.WALL_E_ACTION_MEMORY_LIMIT,
85
+ maxCandidates: opts.actionMemoryMaxCandidates || process.env.WALL_E_ACTION_MEMORY_MAX_CANDIDATES,
86
+ memoryLimit: opts.actionMemoryMemoryLimit || process.env.WALL_E_ACTION_MEMORY_MEMORY_LIMIT,
87
+ logger,
88
+ });
89
+ }
90
+
91
+ function stringifyIntent(intent) {
92
+ if (!intent) return '';
93
+ if (typeof intent === 'string') return intent;
94
+ return String(intent.id || intent.intent || intent.name || intent.type || '');
95
+ }
96
+
97
+ function callableDecisionTool(decision = {}) {
98
+ return decision.selectedEntryId || 'no candidate';
99
+ }
100
+
101
+ function recentShellCommandsForActionMemory(toolCallHistory = [], { limit = 6 } = {}) {
102
+ return (toolCallHistory || [])
103
+ .filter((item) => callName(item) === 'run_shell')
104
+ .slice(-Math.max(1, Number(limit) || 6))
105
+ .map((item) => {
106
+ const command = shellCommandFromEvidence(item);
107
+ if (!command) return '';
108
+ const status = item.ok === false ? 'tool_error' : 'tool_result';
109
+ return `${status}: ${command}`;
110
+ })
111
+ .filter(Boolean)
112
+ .join(', ');
113
+ }
114
+
115
+ function knownFailuresForActionMemory(toolCallHistory = []) {
116
+ const failures = [];
117
+ if ((toolCallHistory || []).some((item) => callName(item) === 'run_shell' && item.ok === false)) {
118
+ failures.push('run_shell tool error');
119
+ }
120
+ return failures.join(', ');
121
+ }
122
+
123
+ function shellCommandFromEvidence(item = {}) {
124
+ const input = item.input || item.args || {};
125
+ if (input.command || input.cmd) return String(input.command || input.cmd).trim();
126
+ try {
127
+ const parsed = JSON.parse(String(item.inputHash || ''));
128
+ return String(parsed.command || parsed.cmd || '').trim();
129
+ } catch {
130
+ return '';
131
+ }
132
+ }
133
+
63
134
  // Prompt builders — operating-manual content lives in coding-prompts.js
64
135
  // for testability and to apply the Claude Code prompt patterns
65
136
  // (edit-vs-create, no-comments, no premature abstractions, verification-
@@ -88,10 +159,18 @@ const {
88
159
  } = require('./coding/capability-router');
89
160
  const { createCodingTranscript } = require('./coding/transcript-writer');
90
161
  const { createCodingCapabilities } = require('./coding/capability-broker');
162
+ const { gitRootFor } = require('./chat/code-review-context');
163
+ const { recordSessionWorkspace, recordSessionAnchor } = require('./coding/session-workspaces');
91
164
  const {
92
165
  CompactionService,
93
166
  DEFAULT_CONTEXT_WINDOW,
94
167
  } = require('./coding/compaction-service');
168
+ const {
169
+ createNoProgressGuard,
170
+ buildBlockedToolExecution,
171
+ buildNoProgressNudge,
172
+ compactRepeatedInspectionToolResults,
173
+ } = require('./coding/no-progress-guard');
95
174
  const {
96
175
  emitAgentRunContextWarnings,
97
176
  } = require('./runtime/agent-run-context');
@@ -99,6 +178,7 @@ const {
99
178
  resolveWallERuntimeProfile,
100
179
  } = require('./runtime/walle-runtime');
101
180
  const { estimateTokens, estimateMessagesTokens } = require('./context/token-counter');
181
+ const usageUtil = require('./lib/usage');
102
182
  const { recoverAllowedTextToolCalls } = require('./llm/text-tool-calls');
103
183
 
104
184
  const MAX_CUMULATIVE_CONTEXT = 4000;
@@ -359,7 +439,7 @@ const CODING_TOOLS = [
359
439
  },
360
440
  {
361
441
  name: 'edit_file',
362
- description: 'Make a targeted edit to a file by replacing a string match. Uses a 9-strategy fuzzy matching chain — tolerates minor whitespace, indentation, and Unicode differences. More efficient than write_file for modifying existing files.',
442
+ description: 'Make a targeted edit by replacing old_string with new_string; for additive changes, include the original old_string inside new_string plus the inserted sibling code so unrelated code is preserved. Uses a 9-strategy fuzzy matching chain — tolerates minor whitespace, indentation, and Unicode differences. More efficient than write_file for modifying existing files.',
363
443
  input_schema: {
364
444
  type: 'object',
365
445
  properties: {
@@ -1134,6 +1214,27 @@ function resolveRunTimeoutMs(opts = {}) {
1134
1214
  return isInteractive ? 0 : 300000;
1135
1215
  }
1136
1216
 
1217
+ // Compact-and-continue gate (pure, so it's unit-testable). Returns true when an
1218
+ // interactive coding run that is STILL making progress has reached its turn-budget
1219
+ // edge and should be extended (instead of force-stripping tools into a "summarize
1220
+ // what's left" dead-end). Extends only when: enabled + interactive, at the cap edge,
1221
+ // extension + absolute-ceiling budget remains, recent tool activity actually succeeded,
1222
+ // and it isn't spinning on one identical call (the doom-loop detector owns that case).
1223
+ function shouldExtendInteractiveBudget({
1224
+ enabled, interactive, turnIndex, turns, turnExtensions,
1225
+ maxExtensions, absoluteMax, recent = [], doomThreshold = 3,
1226
+ } = {}) {
1227
+ if (!enabled || !interactive) return false;
1228
+ if (turnIndex < turns - 1) return false;
1229
+ if (turnExtensions >= maxExtensions) return false;
1230
+ if (turns >= absoluteMax) return false;
1231
+ if (!recent.some(c => c && c.ok === true)) return false;
1232
+ const looping = recent.length >= doomThreshold
1233
+ && recent.every(c => c.name === recent[0].name && c.inputHash === recent[0].inputHash);
1234
+ if (looping) return false;
1235
+ return true;
1236
+ }
1237
+
1137
1238
  function providerSupportsToolCalls(provider) {
1138
1239
  if (!provider) return true;
1139
1240
  if (provider.capabilities?.tools === false) return false;
@@ -1179,10 +1280,87 @@ function createInitialLlmParams(opts = {}, maxTokens = 4096) {
1179
1280
  if (Object.prototype.hasOwnProperty.call(opts, 'temperature')) params.temperature = opts.temperature;
1180
1281
  if (Object.prototype.hasOwnProperty.call(opts, 'thinking')) params.thinking = opts.thinking;
1181
1282
  if (Object.prototype.hasOwnProperty.call(opts, 'reasoningEffort')) params.reasoningEffort = opts.reasoningEffort;
1283
+ if (Object.prototype.hasOwnProperty.call(opts, 'keepAlive')) params.keepAlive = opts.keepAlive;
1182
1284
  if (opts.options && typeof opts.options === 'object') params.options = { ...opts.options };
1183
1285
  return params;
1184
1286
  }
1185
1287
 
1288
+ function providerTypeOf(provider) {
1289
+ if (!provider) return '';
1290
+ if (typeof provider === 'string') return provider;
1291
+ return provider.type || provider.provider || provider.id || '';
1292
+ }
1293
+
1294
+ function isFastGemma4ModelId(model) {
1295
+ return /^gemma4:e4b(?:$|[-:])/i.test(String(model || ''));
1296
+ }
1297
+
1298
+ function ollamaKeepAliveForRun(opts = {}) {
1299
+ if (Object.prototype.hasOwnProperty.call(opts, 'keepAlive')) return opts.keepAlive;
1300
+ return process.env.WALLE_OLLAMA_CODING_KEEP_ALIVE || process.env.OLLAMA_KEEP_ALIVE || '30m';
1301
+ }
1302
+
1303
+ async function maybePrewarmLocalOllama({ provider, model, opts = {}, emitProgress }) {
1304
+ if (providerTypeOf(provider) !== 'ollama') return null;
1305
+ if (!isFastGemma4ModelId(model)) return null;
1306
+ if (opts.prewarm === false || opts.ollamaPrewarm === false) return null;
1307
+ if (typeof provider?.prewarm !== 'function') return null;
1308
+
1309
+ const started = Date.now();
1310
+ const keepAlive = ollamaKeepAliveForRun(opts);
1311
+ emitProgress?.({
1312
+ phase: opts.mode || 'executing',
1313
+ step: -1,
1314
+ message: `Prewarming Ollama model ${model}...`,
1315
+ detail: { provider: 'ollama', model, keepAlive },
1316
+ });
1317
+ try {
1318
+ const result = await provider.prewarm({
1319
+ model,
1320
+ keepAlive,
1321
+ timeoutMs: opts.prewarmTimeoutMs || opts.ollamaPrewarmTimeoutMs || 120000,
1322
+ });
1323
+ emitProgress?.({
1324
+ phase: opts.mode || 'executing',
1325
+ step: -1,
1326
+ message: `Prewarmed Ollama model ${model} in ${((Date.now() - started) / 1000).toFixed(1)}s`,
1327
+ detail: { provider: 'ollama', model, keepAlive, result },
1328
+ });
1329
+ return result;
1330
+ } catch (err) {
1331
+ emitProgress?.({
1332
+ phase: opts.mode || 'executing',
1333
+ step: -1,
1334
+ message: `Ollama prewarm failed: ${String(err?.message || err).slice(0, 120)}`,
1335
+ detail: { provider: 'ollama', model, keepAlive },
1336
+ });
1337
+ return null;
1338
+ }
1339
+ }
1340
+
1341
+ function emitOllamaTurnTiming({ emitProgress, provider, model, phase, step, startedAt, usage, params, toolsCount }) {
1342
+ if (providerTypeOf(provider) !== 'ollama') return;
1343
+ const latencyMs = Date.now() - startedAt;
1344
+ emitProgress?.({
1345
+ phase,
1346
+ step,
1347
+ message: `Ollama turn completed in ${(latencyMs / 1000).toFixed(1)}s`,
1348
+ detail: {
1349
+ provider: 'ollama',
1350
+ model,
1351
+ latencyMs,
1352
+ inputTokens: usage?.input || 0,
1353
+ outputTokens: usage?.output || 0,
1354
+ genTokPerSec: usage?.genTokPerSec ?? null,
1355
+ prefillTokPerSec: usage?.prefillTokPerSec ?? null,
1356
+ numCtx: params?.options?.num_ctx ?? null,
1357
+ maxTokens: params?.maxTokens ?? null,
1358
+ keepAlive: params?.keepAlive ?? null,
1359
+ toolsCount: toolsCount ?? null,
1360
+ },
1361
+ });
1362
+ }
1363
+
1186
1364
  function createCodingCompactionService(provider, modelId, opts = {}) {
1187
1365
  if (opts.autoCompact === false || opts.compaction === false || opts.disableCompaction === true) return null;
1188
1366
  if (String(process.env.WALLE_CODING_AUTO_COMPACT || '').trim() === '0') return null;
@@ -2101,6 +2279,18 @@ async function runAgentLoop(prompt, opts = {}) {
2101
2279
  }
2102
2280
 
2103
2281
  // Build system prompt with project context.
2282
+ // Ground the agent in its directory every turn, and surface every root this
2283
+ // session has worked in (so a session reused across repos doesn't lose track of
2284
+ // where it built things — session c3f3af97).
2285
+ let gitRoot = '';
2286
+ try { gitRoot = gitRootFor(resolvedCwd) || ''; } catch { gitRoot = ''; }
2287
+ const groundingSessionKey = opts.chatSessionId || opts.session_id || sid;
2288
+ const sessionWorkspaces = recordSessionWorkspace(opts.brain, groundingSessionKey, resolvedCwd);
2289
+ // Anchor the session to its first project root (git root, else cwd) so it has a
2290
+ // stable "home" even when a later turn is pointed at a different directory — the
2291
+ // agent resolves "the project / the site / what you built" to the anchor instead
2292
+ // of whatever cwd this turn happens to carry (session c3f3af97).
2293
+ const sessionAnchor = recordSessionAnchor(opts.brain, groundingSessionKey, gitRoot || resolvedCwd);
2104
2294
  const systemPrompt = buildAgentSystemPrompt({
2105
2295
  resolvedCwd,
2106
2296
  projectInfo: projectInfo && projectInfo.type !== 'unknown' ? projectInfo : null,
@@ -2115,11 +2305,17 @@ async function runAgentLoop(prompt, opts = {}) {
2115
2305
  promptCapabilities,
2116
2306
  artifactCapabilities,
2117
2307
  userTask: prompt,
2308
+ environment: { gitRoot, isGitRepo: !!gitRoot, platform: process.platform },
2309
+ sessionWorkspaces,
2310
+ sessionAnchor,
2118
2311
  },
2119
2312
  });
2120
2313
 
2121
- // Resume support: use restored messages if resuming from checkpoint
2122
- const messages = opts._resumeMessages || [{ role: 'user', content: prompt }];
2314
+ // Resume support: use restored messages if resuming from checkpoint.
2315
+ // For a normal turn the seed is built from the prior conversation + tool trail
2316
+ // (opts.contextMessages) once the compaction service exists (see below); this
2317
+ // placeholder is only used if there is no context to seed from.
2318
+ let messages = opts._resumeMessages || [{ role: 'user', content: prompt }];
2123
2319
  let finalOutput = '';
2124
2320
  let finalAnswerDelivered = false;
2125
2321
  let totalInput = 0;
@@ -2130,6 +2326,147 @@ async function runAgentLoop(prompt, opts = {}) {
2130
2326
  // Doom loop detection (6a)
2131
2327
  const toolCallHistory = []; // { name, inputHash }
2132
2328
  const DOOM_LOOP_THRESHOLD = 3;
2329
+ const noProgressGuard = createNoProgressGuard({
2330
+ cwd: resolvedCwd,
2331
+ enabled: opts.noProgressGuard !== false && process.env.WALLE_NO_PROGRESS_GUARD !== '0',
2332
+ threshold: opts.noProgressGuardThreshold || Number(process.env.WALLE_NO_PROGRESS_THRESHOLD) || DOOM_LOOP_THRESHOLD,
2333
+ });
2334
+ const actionMemoryPolicy = createActionMemoryPolicyForRun(opts, { logger: console });
2335
+ function actionMemoryContextForCall(call = {}) {
2336
+ const recentSequence = toolCallHistory.slice(-6).map((item) => callName(item)).filter(Boolean);
2337
+ const last = recentSequence[recentSequence.length - 1] || '';
2338
+ const shellHistory = recentShellCommandsForActionMemory(toolCallHistory);
2339
+ const failures = knownFailuresForActionMemory(toolCallHistory);
2340
+ return {
2341
+ sessionId: sid,
2342
+ cwd: resolvedCwd,
2343
+ repo: opts.actionMemoryRepo || opts.repo || gitRoot || resolvedCwd,
2344
+ familyId: opts.actionMemoryFamilyId || opts.workflowFamilyId || runtimeMode.id,
2345
+ intent: opts.actionMemoryIntent || stringifyIntent(codingIntent) || opts.mode || runtimeMode.id,
2346
+ intendedTool: callName(call),
2347
+ taskText: String(prompt || ''),
2348
+ sequence: recentSequence.join(' > '),
2349
+ lastTool: last,
2350
+ recentShellCommands: shellHistory,
2351
+ knownFailures: failures,
2352
+ verificationStatus: hasFailedVerificationAttempt(toolCallHistory)
2353
+ ? 'failed'
2354
+ : (hasVerificationEvidence(toolCallHistory) ? 'passed' : ''),
2355
+ toolCatalogId: opts.actionMemoryToolCatalogId || `${llm.type || 'provider'}:${runtimeMode.id}`,
2356
+ metadata: {
2357
+ mode: opts.mode || '',
2358
+ runtimeMode: runtimeMode.id,
2359
+ projectType: projectInfo?.type || '',
2360
+ },
2361
+ };
2362
+ }
2363
+ function actionMemoryModelContext() {
2364
+ return {
2365
+ provider: llm.type || '',
2366
+ modelId,
2367
+ toolFormat: opts.actionMemoryToolFormat || '',
2368
+ };
2369
+ }
2370
+ async function executeWithActionMemoryPolicy({ call, context, execute, allowRewrite = true }) {
2371
+ if (!actionMemoryPolicy) return execute(call, { mode: 'off', action: 'skip', accepted: false });
2372
+ if (!allowRewrite && actionMemoryPolicy.mode !== 'audit') {
2373
+ return execute(call, {
2374
+ mode: actionMemoryPolicy.mode,
2375
+ action: 'skip',
2376
+ accepted: false,
2377
+ reason: 'stream_permission_prechecked',
2378
+ });
2379
+ }
2380
+ const { executeToolCallWithActionMemoryPolicy } = require('./coding/action-memory-policy');
2381
+ return executeToolCallWithActionMemoryPolicy({
2382
+ call,
2383
+ context,
2384
+ model: actionMemoryModelContext(),
2385
+ sessionId: sid,
2386
+ prompt,
2387
+ policy: actionMemoryPolicy,
2388
+ execute,
2389
+ });
2390
+ }
2391
+ function emitActionMemoryDecision(execution, step) {
2392
+ const decision = execution?.actionMemory?.decision;
2393
+ if (!decision || decision.action === 'skip') return;
2394
+ const detail = {
2395
+ ...decision,
2396
+ selectedEntryId: decision.selectedEntryId || '',
2397
+ candidateCount: execution.actionMemory.candidateCount || 0,
2398
+ };
2399
+ emitProgress({
2400
+ type: 'action_memory',
2401
+ phase: opts.mode || 'executing',
2402
+ step,
2403
+ message: `Action memory ${decision.action}: ${callableDecisionTool(decision)}`,
2404
+ detail,
2405
+ });
2406
+ events.emit('action_memory.decision', { sessionId: sid, ...detail });
2407
+ }
2408
+ function compactNoProgressState(step, reason) {
2409
+ if (opts.noProgressGuard === false || process.env.WALLE_NO_PROGRESS_GUARD === '0') return;
2410
+ const compacted = compactRepeatedInspectionToolResults(messages, { cwd: resolvedCwd });
2411
+ if (compacted.compacted > 0) {
2412
+ emitProgress({
2413
+ phase: opts.mode || 'executing',
2414
+ step,
2415
+ message: `Compacted ${compacted.compacted} repeated inspection result(s)`,
2416
+ detail: { reason, groups: compacted.groups },
2417
+ });
2418
+ }
2419
+ }
2420
+
2421
+ // Compact-and-continue (interactive coding): a fixed turn cap that force-strips
2422
+ // tools and makes the model "summarize what's left" turns "out of steps" into a
2423
+ // dead-end ("I investigated; confirm and I'll apply it"). Interactive runs already
2424
+ // have NO wall-clock deadline (run until the agent finishes or the user stops, like
2425
+ // Claude Code / opencode) — the turn counter is the last hard ceiling contradicting
2426
+ // that. When an interactive run is STILL making progress at the cap edge, extend the
2427
+ // budget (bounded) + nudge it to land the edit, instead of forcing the wrap-up.
2428
+ // Headless/benchmark runs keep the hard cap (CI/background must not hang).
2429
+ const codingTurnContinueEnabled = process.env.WALLE_CODING_TURN_CONTINUE !== '0';
2430
+ const ABSOLUTE_MAX_TURNS = Math.max(turns, 200);
2431
+ const TURN_EXTENSION_BATCH = 10;
2432
+ const MAX_TURN_EXTENSIONS = 12;
2433
+ let turnExtensions = 0;
2434
+ // Called at the top of a turn, BEFORE tools get stripped on the final budgeted
2435
+ // round. When an interactive run is still making progress at the cap edge, bump
2436
+ // `turns`, append a directive nudging the model to land the edit (appended to the
2437
+ // last message's content, not a new user message — consecutive user messages are
2438
+ // rejected by the API), and return true. Returns false to let the normal wrap-up run.
2439
+ function tryExtendInteractiveTurnBudget(turnIndex) {
2440
+ if (!shouldExtendInteractiveBudget({
2441
+ enabled: codingTurnContinueEnabled,
2442
+ interactive: interactiveRun,
2443
+ turnIndex,
2444
+ turns,
2445
+ turnExtensions,
2446
+ maxExtensions: MAX_TURN_EXTENSIONS,
2447
+ absoluteMax: ABSOLUTE_MAX_TURNS,
2448
+ recent: toolCallHistory.slice(-DOOM_LOOP_THRESHOLD),
2449
+ doomThreshold: DOOM_LOOP_THRESHOLD,
2450
+ })) return false;
2451
+ turns = Math.min(ABSOLUTE_MAX_TURNS, turns + TURN_EXTENSION_BATCH);
2452
+ turnExtensions += 1;
2453
+ emitProgress({
2454
+ phase: opts.mode || 'executing',
2455
+ step: turnIndex,
2456
+ message: `Coding budget extended (${turnExtensions}/${MAX_TURN_EXTENSIONS}) — still making progress`,
2457
+ detail: { turns },
2458
+ });
2459
+ const directive = '[System: You still have budget. You have gathered enough context — STOP investigating and APPLY the change now with edit_file/multi_edit/apply_patch, then verify. Do not ask the user to confirm before applying; apply, then report what changed.]';
2460
+ const lastMsg = messages[messages.length - 1];
2461
+ if (lastMsg && Array.isArray(lastMsg.content)) {
2462
+ lastMsg.content.push({ type: 'text', text: directive });
2463
+ } else if (lastMsg && lastMsg.role === 'user' && typeof lastMsg.content === 'string') {
2464
+ lastMsg.content += '\n\n' + directive;
2465
+ } else {
2466
+ messages.push({ role: 'user', content: directive });
2467
+ }
2468
+ return true;
2469
+ }
2133
2470
 
2134
2471
  // Screenshots captured by browser_screenshot calls. Drives the
2135
2472
  // visual review pass in coding-review.js.
@@ -2138,6 +2475,59 @@ async function runAgentLoop(prompt, opts = {}) {
2138
2475
  // Per-step cost/token tracking (6q)
2139
2476
  const turnCosts = [];
2140
2477
  let budgetUsed = 0;
2478
+ let totalCostUsd = 0; // cumulative $ (accurate, cache-discounted)
2479
+ let lastTurnRawUsage = null; // most recent turn's usage → context occupancy
2480
+
2481
+ // Emit a live token/cost snapshot to any watching client (per-session + top-bar
2482
+ // counters). Same shape coding + chat use; see lib/usage.js. `type:'usage'` flows
2483
+ // through emitProgress → CTM emitWalleProgress → browser handleProgress(ev.type).
2484
+ let _usageRequestSeq = 0;
2485
+ function emitUsageSnapshot(rawUsage) {
2486
+ if (rawUsage) lastTurnRawUsage = rawUsage;
2487
+ try {
2488
+ const snap = usageUtil.usageSnapshot({
2489
+ rawUsage: lastTurnRawUsage,
2490
+ cumulativeInput: totalInput,
2491
+ cumulativeOutput: totalOutput,
2492
+ costUsd: totalCostUsd,
2493
+ model: modelId,
2494
+ provider: llm?.type || '',
2495
+ });
2496
+ emitProgress({ type: 'usage', usage: snap });
2497
+ } catch { /* never let telemetry break the run */ }
2498
+ }
2499
+ // Usage fields for the final transcript assistant message — Claude-Code parity:
2500
+ // `usage` mirrors its per-message `message.usage`; `tokens`/`cost` stay for compat.
2501
+ function transcriptUsageFields() {
2502
+ const usage = lastTurnRawUsage
2503
+ ? usageUtil.normalizeUsage(lastTurnRawUsage, { model: modelId, provider: llm?.type || '' })
2504
+ : null;
2505
+ return {
2506
+ tokens: { input: totalInput, output: totalOutput, cost_usd: totalCostUsd },
2507
+ usage,
2508
+ cost: totalCostUsd,
2509
+ };
2510
+ }
2511
+ // Record one turn's usage to the queryable ledger (parity with chat.js, which the
2512
+ // coding agent previously skipped). Deduped per turn via a synthetic requestId.
2513
+ function recordTurnUsageLedger(rawUsage, turnIndex) {
2514
+ if (!rawUsage) return;
2515
+ try {
2516
+ require('./brain').recordModelUsage?.({
2517
+ source: 'wall-e.coding',
2518
+ feature: 'coding_turn',
2519
+ sessionId: sid,
2520
+ providerType: llm?.type || '',
2521
+ modelId,
2522
+ requestId: `${sid}:${turnIndex}:${_usageRequestSeq++}`,
2523
+ usage: usageUtil.normalizeUsage(rawUsage, { model: modelId, provider: llm?.type || '' }),
2524
+ stopReason: rawUsage.stopReason || null,
2525
+ costUsd: usageUtil.costFor(rawUsage, modelId, llm?.type || ''),
2526
+ costSource: 'wall-e.usage',
2527
+ metadata: { turn: turnIndex, runtimeMode: opts.mode || 'executing' },
2528
+ });
2529
+ } catch { /* ledger is best-effort */ }
2530
+ }
2141
2531
 
2142
2532
  // In-flight todo tracking (6m)
2143
2533
  let currentTodos = [];
@@ -2165,6 +2555,7 @@ async function runAgentLoop(prompt, opts = {}) {
2165
2555
  });
2166
2556
  const { AgentCatalog } = require('./coding/agent-catalog');
2167
2557
  const agentCatalog = opts.agentCatalog || new AgentCatalog({ projectRoot: resolvedCwd });
2558
+ await maybePrewarmLocalOllama({ provider: llm, model: modelId, opts, emitProgress });
2168
2559
  const defaultTaskRunner = async ({ prompt: childPrompt, agent, taskId, ctx }) => {
2169
2560
  const childTools = agentCatalog.toolsForAgent(requestedTools, agent);
2170
2561
  const childResult = await runAgentLoop(childPrompt, {
@@ -2222,6 +2613,20 @@ async function runAgentLoop(prompt, opts = {}) {
2222
2613
  // Inspired by OpenCode Question service (packages/opencode/src/question/index.ts)
2223
2614
  const questionManager = opts.questionManager || new QuestionManager(events);
2224
2615
  const compactionService = createCodingCompactionService(llm, modelId, opts);
2616
+
2617
+ // Seed the turn from the prior conversation + tool trail so the agent reads
2618
+ // its own history (a bare "go ahead." now has the proposed plan to refer to,
2619
+ // and the agent does not re-investigate what it already explored). Skipped on
2620
+ // a checkpoint resume (those messages are authoritative). Bounded to the
2621
+ // context window via the compaction service just created above.
2622
+ if (!opts._resumeMessages) {
2623
+ const { buildInitialCodingMessages } = require('./coding/initial-messages');
2624
+ messages = buildInitialCodingMessages({
2625
+ prompt,
2626
+ contextMessages: opts.contextMessages,
2627
+ compactionService,
2628
+ });
2629
+ }
2225
2630
  const { RuntimeEventWriter } = require('./coding/runtime-events');
2226
2631
  const {
2227
2632
  appendPromptManifest,
@@ -2314,6 +2719,7 @@ async function runAgentLoop(prompt, opts = {}) {
2314
2719
  model: modelId,
2315
2720
  mode: opts.mode || '',
2316
2721
  runtimeMode: runtimeMode.id,
2722
+ goal: String(prompt || ''),
2317
2723
  headless: Boolean(opts.headless),
2318
2724
  benchmark: Boolean(opts.benchmark),
2319
2725
  userHooks,
@@ -2353,6 +2759,22 @@ async function runAgentLoop(prompt, opts = {}) {
2353
2759
  provider: llm,
2354
2760
  model: modelId,
2355
2761
  transcript,
2762
+ prepareToolCall: actionMemoryPolicy
2763
+ ? async (call) => {
2764
+ const evaluated = await actionMemoryPolicy.evaluateToolCall({
2765
+ call,
2766
+ context: actionMemoryContextForCall(call),
2767
+ model: actionMemoryModelContext(),
2768
+ sessionId: sid,
2769
+ prompt,
2770
+ });
2771
+ return {
2772
+ call: evaluated.call,
2773
+ decision: evaluated.decision,
2774
+ candidateCount: evaluated.candidates.length,
2775
+ };
2776
+ }
2777
+ : null,
2356
2778
  snapshotService: opts.snapshotService || new SnapshotService({
2357
2779
  cwd: resolvedCwd,
2358
2780
  // Whole-worktree step snapshots + restart-surviving boundaries for
@@ -2362,7 +2784,32 @@ async function runAgentLoop(prompt, opts = {}) {
2362
2784
  }),
2363
2785
  permissionService,
2364
2786
  headless: Boolean(opts.headless || opts.benchmark),
2365
- toolExecutor: async (call) => {
2787
+ goal: String(prompt || ''),
2788
+ toolExecutor: async (call, runtimeCtx = {}) => {
2789
+ const guardBlock = noProgressGuard.check(call, { cwd: resolvedCwd });
2790
+ if (guardBlock) {
2791
+ const blockedExecution = buildBlockedToolExecution(call, guardBlock);
2792
+ noProgressGuard.record({ name: call.name, input: blockedExecution.evidenceInput }, {
2793
+ ok: false,
2794
+ result: blockedExecution.result,
2795
+ blocked: true,
2796
+ cwd: resolvedCwd,
2797
+ });
2798
+ emitProgress({
2799
+ phase: opts.mode || 'executing',
2800
+ step: 0,
2801
+ message: `No-progress guard blocked repeated ${call.name}`,
2802
+ detail: { tool: call.name, target: guardBlock.target, repeatCount: guardBlock.repeatCount },
2803
+ });
2804
+ events.emit('no_progress.detected', {
2805
+ tool: call.name,
2806
+ target: guardBlock.target,
2807
+ repeatCount: guardBlock.repeatCount,
2808
+ threshold: guardBlock.threshold,
2809
+ sessionId: sid,
2810
+ });
2811
+ return blockedExecution.result;
2812
+ }
2366
2813
  const execution = await streamToolExecutionController.execute(call, {
2367
2814
  sessionId: sid,
2368
2815
  cwd: resolvedCwd,
@@ -2373,7 +2820,26 @@ async function runAgentLoop(prompt, opts = {}) {
2373
2820
  runtimeMode: runtimeMode.id,
2374
2821
  interactive: opts.interactive,
2375
2822
  onTodos: (todos) => { currentTodos = todos; },
2823
+ // Forward the turn's abort signal so user Stop kills an in-flight
2824
+ // run_shell child, and a heartbeat so a long command shows live motion.
2825
+ signal: runtimeCtx.signal,
2826
+ onProgress: (info) => emitProgress({
2827
+ phase: opts.mode || 'executing',
2828
+ step: 0,
2829
+ message: info?.message || 'still running…',
2830
+ detail: info,
2831
+ }),
2832
+ });
2833
+ const guardRecord = noProgressGuard.record({ name: call.name, input: execution.evidenceInput || call.input }, {
2834
+ ok: execution.ok,
2835
+ result: execution.result,
2836
+ blocked: execution.blocked,
2837
+ cwd: resolvedCwd,
2376
2838
  });
2839
+ const guardNudge = buildNoProgressNudge(guardRecord);
2840
+ if (guardNudge && execution.result && typeof execution.result === 'object' && !execution.result.error) {
2841
+ execution.result._walleNoProgressAdvice = guardNudge.trim();
2842
+ }
2377
2843
  return execution.result;
2378
2844
  },
2379
2845
  });
@@ -2389,6 +2855,7 @@ async function runAgentLoop(prompt, opts = {}) {
2389
2855
  // card and waiting state without a reload.
2390
2856
  if (evt && [
2391
2857
  'tool_call', 'tool_result', 'tool_done', 'skill_loaded', 'skill_load_failed',
2858
+ 'action_memory',
2392
2859
  'permission_request', 'permission_resolved', 'permission_denied', 'lane_event',
2393
2860
  ].includes(evt.type)) {
2394
2861
  emitProgress(evt);
@@ -2408,6 +2875,9 @@ async function runAgentLoop(prompt, opts = {}) {
2408
2875
  const streamErrors = [];
2409
2876
  let streamProviderError = null;
2410
2877
  let streamHadEdit = false;
2878
+ let completionReportRequested = false;
2879
+ const maxNoProgressGuardBlocks = opts.noProgressGuardMaxBlocks || Number(process.env.WALLE_NO_PROGRESS_MAX_BLOCKS) || 2;
2880
+ let noProgressGuardBlockStreak = 0;
2411
2881
  for (let turnIndex = opts._resumeTurn || 0; turnIndex < turns; turnIndex++) {
2412
2882
  throwIfExternalAbort();
2413
2883
  const remaining = deadline - Date.now();
@@ -2417,6 +2887,10 @@ async function runAgentLoop(prompt, opts = {}) {
2417
2887
  finalOutput += '\n[Timeout reached]';
2418
2888
  break;
2419
2889
  }
2890
+ // Compact-and-continue: if we're at the budget edge but still making progress,
2891
+ // extend the turn budget (+ nudge to land the edit) BEFORE tools get stripped
2892
+ // below, instead of forcing the model into a tool-less "summarize what's left".
2893
+ tryExtendInteractiveTurnBudget(turnIndex);
2420
2894
  emitProgress({
2421
2895
  phase: opts.mode || 'executing',
2422
2896
  step: turnIndex,
@@ -2429,7 +2903,9 @@ async function runAgentLoop(prompt, opts = {}) {
2429
2903
  let turn;
2430
2904
  let toolsForTurn = [];
2431
2905
  try {
2432
- toolsForTurn = turnIndex >= turns - 1
2906
+ // No tools on the last budgeted round, and none on the completion-report
2907
+ // round (the model must answer, not start new work).
2908
+ toolsForTurn = (turnIndex >= turns - 1 || completionReportRequested)
2433
2909
  ? []
2434
2910
  : await toolRegistry.getDefinitions({
2435
2911
  provider: llm.type,
@@ -2459,21 +2935,35 @@ async function runAgentLoop(prompt, opts = {}) {
2459
2935
  reason: 'stream_pre_turn',
2460
2936
  opts,
2461
2937
  });
2938
+ compactNoProgressState(turnIndex, 'stream_pre_turn');
2939
+ const requestKeepAlive = llmCtx.params.keepAlive ?? ollamaKeepAliveForRun(opts);
2940
+ const llmStartedAt = Date.now();
2462
2941
  turn = await processor.runTurn({
2463
2942
  sessionId: sid,
2464
2943
  cwd: resolvedCwd,
2465
2944
  system: llmCtx.system,
2466
2945
  messages,
2467
2946
  tools: toolsForTurn,
2468
- maxTokens: taskFileHints.length >= 4 ? 8192 : 4096,
2469
2947
  signal: ac.signal,
2470
2948
  maxTokens: llmCtx.params.maxTokens,
2471
2949
  temperature: llmCtx.params.temperature,
2472
2950
  thinking: llmCtx.params.thinking,
2473
2951
  reasoningEffort: llmCtx.params.reasoningEffort,
2474
2952
  options: llmCtx.params.options,
2953
+ keepAlive: requestKeepAlive,
2475
2954
  promptCache: true,
2476
2955
  });
2956
+ emitOllamaTurnTiming({
2957
+ emitProgress,
2958
+ provider: llm,
2959
+ model: modelId,
2960
+ phase: opts.mode || 'executing',
2961
+ step: turnIndex,
2962
+ startedAt: llmStartedAt,
2963
+ usage: turn.usage,
2964
+ params: { ...llmCtx.params, keepAlive: requestKeepAlive },
2965
+ toolsCount: toolsForTurn.length,
2966
+ });
2477
2967
  } finally {
2478
2968
  clearTimeout(timer);
2479
2969
  unlinkExternalAbort();
@@ -2481,6 +2971,13 @@ async function runAgentLoop(prompt, opts = {}) {
2481
2971
 
2482
2972
  totalInput += turn.usage?.input || 0;
2483
2973
  totalOutput += turn.usage?.output || 0;
2974
+ if (turn.usage) {
2975
+ // Display/ledger only — the stream path has no budget-enforcement check,
2976
+ // so leave budgetUsed's prior semantics unchanged.
2977
+ totalCostUsd += usageUtil.costFor(turn.usage, modelId, llm?.type || '');
2978
+ recordTurnUsageLedger(turn.raw?.usage || turn.usage, turnIndex);
2979
+ emitUsageSnapshot(turn.usage);
2980
+ }
2484
2981
  streamStatus = turn.status;
2485
2982
  streamStopReason = turn.stopReason || streamStopReason;
2486
2983
  streamModel = turn.model || streamModel;
@@ -2489,6 +2986,10 @@ async function runAgentLoop(prompt, opts = {}) {
2489
2986
  if (turn.text) finalOutput += turn.text;
2490
2987
  const streamToolCalls = (turn.toolCalls || []).map(tc => ({ name: tc.name, input: tc.input }));
2491
2988
  const streamToolResults = turn.toolResults || [];
2989
+ const noProgressBlocks = streamToolResults.filter((record) => {
2990
+ const result = record?.result || record;
2991
+ return result?.noProgressGuard === true;
2992
+ });
2492
2993
  toolCallHistory.push(...streamToolCalls.map((tc, index) => {
2493
2994
  const resultRecord = streamToolResults[index] || {};
2494
2995
  return normalizeToolCallEvidence(tc, resultRecord.result || resultRecord);
@@ -2509,6 +3010,30 @@ async function runAgentLoop(prompt, opts = {}) {
2509
3010
  });
2510
3011
  if (turn.hadEdit) streamHadEdit = true;
2511
3012
 
3013
+ if (noProgressBlocks.length > 0) {
3014
+ noProgressGuardBlockStreak += noProgressBlocks.length;
3015
+ const latestBlock = noProgressBlocks[noProgressBlocks.length - 1]?.result || noProgressBlocks[noProgressBlocks.length - 1] || {};
3016
+ if (noProgressGuardBlockStreak >= maxNoProgressGuardBlocks) {
3017
+ const message = `No-progress guard stopped repeated ${latestBlock.tool || 'inspection'} loop after ${noProgressGuardBlockStreak} blocked tool calls.`;
3018
+ streamStatus = 'error';
3019
+ streamErrors.push(message);
3020
+ finalOutput += `\n[${message}]`;
3021
+ emitProgress({
3022
+ phase: opts.mode || 'executing',
3023
+ step: turnIndex,
3024
+ message,
3025
+ detail: {
3026
+ tool: latestBlock.tool || '',
3027
+ target: latestBlock.target || '',
3028
+ repeatCount: latestBlock.repeatCount || noProgressGuardBlockStreak,
3029
+ },
3030
+ });
3031
+ break;
3032
+ }
3033
+ } else if (streamToolCalls.length > 0 || turn.hadEdit) {
3034
+ noProgressGuardBlockStreak = 0;
3035
+ }
3036
+
2512
3037
  if (turn.status === 'error') break;
2513
3038
  if ((turn.toolCalls || []).length === 0) {
2514
3039
  const continuation = getNoActionContinuation({
@@ -2547,7 +3072,25 @@ async function runAgentLoop(prompt, opts = {}) {
2547
3072
  }
2548
3073
  if (turn.assistantMessage) messages.push(turn.assistantMessage);
2549
3074
  if (turn.toolResultMessage) messages.push(turn.toolResultMessage);
2550
- if (turn.verified && streamHadEdit) break;
3075
+ if (turn.verified && streamHadEdit && !completionReportRequested) {
3076
+ // Verification succeeded after edits — but breaking here ends the run
3077
+ // before the model has seen the verification result, so the persisted
3078
+ // reply is accumulated narration that stops mid-thought ("finished
3079
+ // without an answer", session c3f3af97). When a human is reading the
3080
+ // reply, spend ONE no-tools round on the final completion report, then
3081
+ // stop. Headless/benchmark runs keep the fast exit — their consumers
3082
+ // score outcomes, not prose.
3083
+ if (interactiveRun && turnIndex + 1 < turns) {
3084
+ completionReportRequested = true;
3085
+ messages.push({
3086
+ role: 'user',
3087
+ content: '[System: Verification succeeded and your edits are in place. Provide the final completion report now — what changed, how it was verified, and anything the user should know. Do not call more tools.]',
3088
+ });
3089
+ emitProgress({ phase: opts.mode || 'executing', step: turnIndex, message: 'Verified after edits — requesting final completion report' });
3090
+ continue;
3091
+ }
3092
+ break;
3093
+ }
2551
3094
  if (turn.next !== 'continue') break;
2552
3095
  }
2553
3096
 
@@ -2586,7 +3129,7 @@ async function runAgentLoop(prompt, opts = {}) {
2586
3129
  chatSessionId: opts.chatSessionId || '',
2587
3130
  model: streamModel,
2588
3131
  provider: llm.type || '',
2589
- tokens: { input: totalInput, output: totalOutput },
3132
+ ...transcriptUsageFields(),
2590
3133
  });
2591
3134
  }
2592
3135
 
@@ -2604,6 +3147,7 @@ async function runAgentLoop(prompt, opts = {}) {
2604
3147
  exitCode: streamStatus === 'error' ? -1 : 0,
2605
3148
  log,
2606
3149
  usage: { input: totalInput, output: totalOutput },
3150
+ costUsd: totalCostUsd,
2607
3151
  provider: llm.type,
2608
3152
  model: streamModel,
2609
3153
  next: 'stop',
@@ -2635,6 +3179,9 @@ async function runAgentLoop(prompt, opts = {}) {
2635
3179
  break;
2636
3180
  }
2637
3181
 
3182
+ // Compact-and-continue: extend the budget (+ nudge to land the edit) at the cap
3183
+ // edge while still making progress, BEFORE turnsRemaining drives the wrap-up below.
3184
+ tryExtendInteractiveTurnBudget(turn);
2638
3185
  emitProgress({
2639
3186
  phase: opts.mode || 'executing',
2640
3187
  step: turn,
@@ -2675,6 +3222,7 @@ async function runAgentLoop(prompt, opts = {}) {
2675
3222
  reason: 'legacy_pre_turn',
2676
3223
  opts,
2677
3224
  });
3225
+ compactNoProgressState(turn, 'legacy_pre_turn');
2678
3226
 
2679
3227
  // Graceful max-steps degradation (6n)
2680
3228
  // Note: warnings are appended to the LAST message's content (not as separate
@@ -2705,6 +3253,8 @@ async function runAgentLoop(prompt, opts = {}) {
2705
3253
 
2706
3254
  let response;
2707
3255
  try {
3256
+ const requestKeepAlive = llmCtx.params.keepAlive ?? ollamaKeepAliveForRun(opts);
3257
+ const llmStartedAt = Date.now();
2708
3258
  response = await llm.chat({
2709
3259
  model: modelId,
2710
3260
  system: llmCtx.system,
@@ -2716,9 +3266,21 @@ async function runAgentLoop(prompt, opts = {}) {
2716
3266
  thinking: llmCtx.params.thinking,
2717
3267
  reasoningEffort: llmCtx.params.reasoningEffort,
2718
3268
  options: llmCtx.params.options,
3269
+ keepAlive: requestKeepAlive,
2719
3270
  promptCache: true,
2720
3271
  signal: ac.signal,
2721
3272
  });
3273
+ emitOllamaTurnTiming({
3274
+ emitProgress,
3275
+ provider: llm,
3276
+ model: modelId,
3277
+ phase: opts.mode || 'executing',
3278
+ step: turn,
3279
+ startedAt: llmStartedAt,
3280
+ usage: response.usage,
3281
+ params: { ...llmCtx.params, keepAlive: requestKeepAlive },
3282
+ toolsCount: adaptedTools.length,
3283
+ });
2722
3284
  } finally {
2723
3285
  clearTimeout(timer);
2724
3286
  unlinkExternalAbort();
@@ -2749,23 +3311,20 @@ async function runAgentLoop(prompt, opts = {}) {
2749
3311
  totalOutput += response.usage.output || 0;
2750
3312
  }
2751
3313
 
2752
- // Per-step cost tracking (6q)
3314
+ // Per-step cost tracking (6q): centralized pricing (cache-discounted) via
3315
+ // lib/usage so coding + chat agree; also persist to the queryable ledger and
3316
+ // push a live token/cost snapshot to watching clients.
2753
3317
  if (response.usage) {
2754
3318
  const inputTokens = response.usage.input || 0;
2755
3319
  const outputTokens = response.usage.output || 0;
2756
- // Cache hits cost 0.1x input price, cache writes 1.25x (Anthropic).
2757
3320
  const cacheRead = response.usage.cacheRead || 0;
2758
3321
  const cacheWrite = response.usage.cacheWrite || 0;
2759
- const effectiveInput = Math.max(0, inputTokens - cacheRead - cacheWrite)
2760
- + cacheRead * 0.1 + cacheWrite * 1.25;
2761
- // Cost estimate: rough pricing per 1M tokens
2762
- const costPer1M = {
2763
- input: modelId.includes('haiku') ? 0.25 : modelId.includes('sonnet') ? 3.0 : 15.0,
2764
- output: modelId.includes('haiku') ? 1.25 : modelId.includes('sonnet') ? 15.0 : 75.0,
2765
- };
2766
- const turnCost = (effectiveInput * costPer1M.input + outputTokens * costPer1M.output) / 1_000_000;
3322
+ const turnCost = usageUtil.costFor(response.usage, modelId, llm?.type || '');
2767
3323
  turnCosts.push({ turn, inputTokens, outputTokens, cacheRead, cacheWrite, cost: turnCost });
2768
3324
  budgetUsed += turnCost;
3325
+ totalCostUsd += turnCost;
3326
+ recordTurnUsageLedger(response.raw?.usage || response.usage, turn);
3327
+ emitUsageSnapshot(response.usage);
2769
3328
  if (opts.budgetUsd && budgetUsed > opts.budgetUsd) {
2770
3329
  finalOutput += '\n[Budget exceeded]';
2771
3330
  break;
@@ -2841,18 +3400,42 @@ async function runAgentLoop(prompt, opts = {}) {
2841
3400
  detail: { tool: tc.name, input: tc.input },
2842
3401
  });
2843
3402
 
2844
- const execution = await toolExecutionController.execute(tc, {
2845
- sessionId: sid,
2846
- cwd: resolvedCwd,
2847
- projectRoot: resolvedCwd,
2848
- model: modelId,
2849
- provider: llm.type,
2850
- mode: opts.mode || '',
2851
- runtimeMode: runtimeMode.id,
2852
- llmCtx,
2853
- interactive: opts.interactive,
2854
- onTodos: (todos) => { currentTodos = todos; },
2855
- });
3403
+ const guardBlock = noProgressGuard.check(tc, { cwd: resolvedCwd });
3404
+ let execution;
3405
+ if (guardBlock) {
3406
+ execution = buildBlockedToolExecution(tc, guardBlock);
3407
+ emitProgress({
3408
+ phase: opts.mode || 'executing',
3409
+ step: turn,
3410
+ message: `No-progress guard blocked repeated ${tc.name}`,
3411
+ detail: { tool: tc.name, target: guardBlock.target, repeatCount: guardBlock.repeatCount },
3412
+ });
3413
+ events.emit('no_progress.detected', {
3414
+ tool: tc.name,
3415
+ target: guardBlock.target,
3416
+ repeatCount: guardBlock.repeatCount,
3417
+ threshold: guardBlock.threshold,
3418
+ sessionId: sid,
3419
+ });
3420
+ } else {
3421
+ execution = await executeWithActionMemoryPolicy({
3422
+ call: tc,
3423
+ context: actionMemoryContextForCall(tc),
3424
+ execute: (preparedCall) => toolExecutionController.execute(preparedCall, {
3425
+ sessionId: sid,
3426
+ cwd: resolvedCwd,
3427
+ projectRoot: resolvedCwd,
3428
+ model: modelId,
3429
+ provider: llm.type,
3430
+ mode: opts.mode || '',
3431
+ runtimeMode: runtimeMode.id,
3432
+ llmCtx,
3433
+ interactive: opts.interactive,
3434
+ onTodos: (todos) => { currentTodos = todos; },
3435
+ }),
3436
+ });
3437
+ emitActionMemoryDecision(execution, turn);
3438
+ }
2856
3439
  const result = execution.result;
2857
3440
  const evidenceInput = execution.evidenceInput || tc.input || {};
2858
3441
  if (!execution.ok) turnHadError = true;
@@ -2883,11 +3466,19 @@ async function runAgentLoop(prompt, opts = {}) {
2883
3466
  detail: { tool: tc.name, resultLength: resultStr.length },
2884
3467
  });
2885
3468
 
2886
- const capped = truncated.content;
3469
+ const evidence = normalizeToolCallEvidence({ name: tc.name, input: evidenceInput }, result);
3470
+ const guardRecord = noProgressGuard.record({ name: tc.name, input: evidenceInput }, {
3471
+ ok: execution.ok,
3472
+ result,
3473
+ blocked: execution.blocked,
3474
+ cwd: resolvedCwd,
3475
+ });
3476
+ const guardNudge = buildNoProgressNudge(guardRecord);
3477
+ let capped = truncated.content;
3478
+ if (guardNudge) capped += guardNudge;
2887
3479
  toolResults.push({ type: 'tool_result', tool_use_id: tc.id, content: capped });
2888
3480
 
2889
3481
  log[log.length - 1].toolResults = log[log.length - 1].toolResults || [];
2890
- const evidence = normalizeToolCallEvidence({ name: tc.name, input: evidenceInput }, result);
2891
3482
  log[log.length - 1].toolResults.push({
2892
3483
  name: tc.name,
2893
3484
  resultLength: resultStr.length,
@@ -2905,7 +3496,7 @@ async function runAgentLoop(prompt, opts = {}) {
2905
3496
  if (recent.every(c => c.name === recent[0].name && c.inputHash === recent[0].inputHash)) {
2906
3497
  // Progressive recovery (Phase 6): suggest alternative tools before forcing wrap-up
2907
3498
  const altTools = {
2908
- read_file: 'grep_files or glob',
3499
+ read_file: 'edit_file now if the needed location is visible; do not call read_file or list_directory again for the same target',
2909
3500
  edit_file: 'write_file or apply_patch',
2910
3501
  run_shell: 'read the error output carefully and try a different command',
2911
3502
  grep_files: 'glob or read_file on a specific path',
@@ -2915,6 +3506,10 @@ async function runAgentLoop(prompt, opts = {}) {
2915
3506
  `${DOOM_LOOP_THRESHOLD} times. Try ${suggestion}, or use ask_user if you're stuck.`;
2916
3507
  const lastResult = toolResults[toolResults.length - 1];
2917
3508
  if (lastResult) lastResult.content += doomWarning;
3509
+ if (lastResult && recent[0].name === 'read_file') {
3510
+ const readPath = recent[0].input?.file_path || recent[0].input?.path || 'the file you just read';
3511
+ lastResult.content += `\n[SYSTEM] Next action contract for local models: if the requested change belongs in ${readPath}, call edit_file with a non-identical old_string/new_string. Re-reading or listing is no progress.`;
3512
+ }
2918
3513
  // Force wrap-up: cap remaining turns
2919
3514
  const turnsLeft = turns - turn;
2920
3515
  if (turnsLeft > 3) turns = turn + 3;
@@ -2992,7 +3587,7 @@ async function runAgentLoop(prompt, opts = {}) {
2992
3587
  chatSessionId: opts.chatSessionId || '',
2993
3588
  model: modelId,
2994
3589
  provider: llm?.type || '',
2995
- tokens: { input: totalInput, output: totalOutput },
3590
+ ...transcriptUsageFields(),
2996
3591
  });
2997
3592
  }
2998
3593
 
@@ -3051,6 +3646,7 @@ async function runAgentLoop(prompt, opts = {}) {
3051
3646
  exitCode: -1,
3052
3647
  log,
3053
3648
  usage: { input: totalInput, output: totalOutput },
3649
+ costUsd: totalCostUsd,
3054
3650
  provider: llm?.type,
3055
3651
  model: modelId,
3056
3652
  runtimeMode: runtimeMode.id,
@@ -3102,7 +3698,7 @@ async function runAgentLoop(prompt, opts = {}) {
3102
3698
  chatSessionId: opts.chatSessionId || '',
3103
3699
  model: modelId,
3104
3700
  provider: llm?.type || '',
3105
- tokens: { input: totalInput, output: totalOutput },
3701
+ ...transcriptUsageFields(),
3106
3702
  });
3107
3703
  }
3108
3704
 
@@ -3122,6 +3718,7 @@ async function runAgentLoop(prompt, opts = {}) {
3122
3718
  exitCode: 0,
3123
3719
  log,
3124
3720
  usage: { input: totalInput, output: totalOutput },
3721
+ costUsd: totalCostUsd,
3125
3722
  provider: llm?.type,
3126
3723
  model: modelId,
3127
3724
  runtimeMode: runtimeMode.id,
@@ -3263,6 +3860,7 @@ async function runPlannerPrompt(prompt, {
3263
3860
  thinking,
3264
3861
  reasoningEffort,
3265
3862
  options,
3863
+ keepAlive,
3266
3864
  temperature,
3267
3865
  maxTokens,
3268
3866
  onProgress,
@@ -3287,6 +3885,7 @@ async function runPlannerPrompt(prompt, {
3287
3885
  thinking,
3288
3886
  reasoningEffort,
3289
3887
  options,
3888
+ keepAlive,
3290
3889
  temperature: temperature ?? 0,
3291
3890
  }, maxTokens || 4096);
3292
3891
  const response = await llm.chat({
@@ -3300,6 +3899,7 @@ async function runPlannerPrompt(prompt, {
3300
3899
  thinking: params.thinking,
3301
3900
  reasoningEffort: params.reasoningEffort,
3302
3901
  options: params.options,
3902
+ keepAlive: params.keepAlive ?? ollamaKeepAliveForRun(keepAlive !== undefined ? { keepAlive } : {}),
3303
3903
  signal: ac.signal,
3304
3904
  });
3305
3905
  const output = contentToText(response?.content || '');
@@ -4439,6 +5039,7 @@ module.exports = {
4439
5039
  isTimeoutOnlyOutput,
4440
5040
  resolveRunTimeoutMs,
4441
5041
  isInteractiveRun,
5042
+ shouldExtendInteractiveBudget,
4442
5043
  isActionRequiredPrompt,
4443
5044
  isPrematureActionResponse,
4444
5045
  getNoActionContinuation,