openhorse 0.2.12 → 0.2.15

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 (143) hide show
  1. package/dist/commands/index.d.ts.map +1 -1
  2. package/dist/commands/index.js +814 -61
  3. package/dist/commands/index.js.map +1 -1
  4. package/dist/commands/types.d.ts +7 -2
  5. package/dist/commands/types.d.ts.map +1 -1
  6. package/dist/commands/types.js.map +1 -1
  7. package/dist/core/checkpoint.d.ts +8 -0
  8. package/dist/core/checkpoint.d.ts.map +1 -1
  9. package/dist/core/checkpoint.js +69 -10
  10. package/dist/core/checkpoint.js.map +1 -1
  11. package/dist/core/cost-tracker.d.ts.map +1 -1
  12. package/dist/core/cost-tracker.js +11 -0
  13. package/dist/core/cost-tracker.js.map +1 -1
  14. package/dist/core/tool-artifacts.d.ts +10 -0
  15. package/dist/core/tool-artifacts.d.ts.map +1 -1
  16. package/dist/core/tool-artifacts.js +81 -3
  17. package/dist/core/tool-artifacts.js.map +1 -1
  18. package/dist/framework/index.d.ts +2 -2
  19. package/dist/framework/index.d.ts.map +1 -1
  20. package/dist/framework/index.js +5 -1
  21. package/dist/framework/index.js.map +1 -1
  22. package/dist/framework/query.d.ts +87 -2
  23. package/dist/framework/query.d.ts.map +1 -1
  24. package/dist/framework/query.js +281 -14
  25. package/dist/framework/query.js.map +1 -1
  26. package/dist/framework/tool-scheduler.d.ts +9 -0
  27. package/dist/framework/tool-scheduler.d.ts.map +1 -1
  28. package/dist/framework/tool-scheduler.js +34 -0
  29. package/dist/framework/tool-scheduler.js.map +1 -1
  30. package/dist/ink-ui/components/PromptInput.d.ts.map +1 -1
  31. package/dist/ink-ui/components/PromptInput.js +5 -3
  32. package/dist/ink-ui/components/PromptInput.js.map +1 -1
  33. package/dist/ink-ui/components/StatusLine.d.ts.map +1 -1
  34. package/dist/ink-ui/components/StatusLine.js +7 -1
  35. package/dist/ink-ui/components/StatusLine.js.map +1 -1
  36. package/dist/ink-ui/screens/ReplScreen.d.ts +3 -1
  37. package/dist/ink-ui/screens/ReplScreen.d.ts.map +1 -1
  38. package/dist/ink-ui/screens/ReplScreen.js +40 -93
  39. package/dist/ink-ui/screens/ReplScreen.js.map +1 -1
  40. package/dist/print-ui/launch.d.ts.map +1 -1
  41. package/dist/print-ui/launch.js +1 -0
  42. package/dist/print-ui/launch.js.map +1 -1
  43. package/dist/runtime/agent-runtime-controller.d.ts +5 -0
  44. package/dist/runtime/agent-runtime-controller.d.ts.map +1 -1
  45. package/dist/runtime/agent-runtime-controller.js +15 -3
  46. package/dist/runtime/agent-runtime-controller.js.map +1 -1
  47. package/dist/runtime/agent-runtime-protocol.d.ts +13 -1
  48. package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
  49. package/dist/runtime/agent-runtime-protocol.js +24 -0
  50. package/dist/runtime/agent-runtime-protocol.js.map +1 -1
  51. package/dist/runtime/agent-status.d.ts +5 -0
  52. package/dist/runtime/agent-status.d.ts.map +1 -1
  53. package/dist/runtime/agent-status.js +28 -2
  54. package/dist/runtime/agent-status.js.map +1 -1
  55. package/dist/runtime/chat-controller.d.ts +7 -0
  56. package/dist/runtime/chat-controller.d.ts.map +1 -1
  57. package/dist/runtime/chat-controller.js +1084 -47
  58. package/dist/runtime/chat-controller.js.map +1 -1
  59. package/dist/runtime/loop-budget.d.ts +5 -0
  60. package/dist/runtime/loop-budget.d.ts.map +1 -0
  61. package/dist/runtime/loop-budget.js +77 -0
  62. package/dist/runtime/loop-budget.js.map +1 -0
  63. package/dist/runtime/turn-controller.d.ts +4 -0
  64. package/dist/runtime/turn-controller.d.ts.map +1 -1
  65. package/dist/runtime/turn-controller.js +8 -0
  66. package/dist/runtime/turn-controller.js.map +1 -1
  67. package/dist/runtime/ui-events.d.ts +41 -1
  68. package/dist/runtime/ui-events.d.ts.map +1 -1
  69. package/dist/runtime/ui-events.js.map +1 -1
  70. package/dist/runtime/ui-view-model.d.ts +287 -0
  71. package/dist/runtime/ui-view-model.d.ts.map +1 -0
  72. package/dist/runtime/ui-view-model.js +642 -0
  73. package/dist/runtime/ui-view-model.js.map +1 -0
  74. package/dist/services/config-dir.d.ts +2 -0
  75. package/dist/services/config-dir.d.ts.map +1 -1
  76. package/dist/services/config-dir.js +5 -0
  77. package/dist/services/config-dir.js.map +1 -1
  78. package/dist/services/config.d.ts +4 -2
  79. package/dist/services/config.d.ts.map +1 -1
  80. package/dist/services/config.js +19 -0
  81. package/dist/services/config.js.map +1 -1
  82. package/dist/services/file-glob.d.ts +6 -2
  83. package/dist/services/file-glob.d.ts.map +1 -1
  84. package/dist/services/file-glob.js +10 -4
  85. package/dist/services/file-glob.js.map +1 -1
  86. package/dist/services/global-config.d.ts +17 -0
  87. package/dist/services/global-config.d.ts.map +1 -1
  88. package/dist/services/global-config.js.map +1 -1
  89. package/dist/services/llm.d.ts +16 -0
  90. package/dist/services/llm.d.ts.map +1 -1
  91. package/dist/services/llm.js +56 -11
  92. package/dist/services/llm.js.map +1 -1
  93. package/dist/services/model-catalog.d.ts +14 -0
  94. package/dist/services/model-catalog.d.ts.map +1 -0
  95. package/dist/services/model-catalog.js +73 -0
  96. package/dist/services/model-catalog.js.map +1 -0
  97. package/dist/services/model-context.d.ts.map +1 -1
  98. package/dist/services/model-context.js +3 -0
  99. package/dist/services/model-context.js.map +1 -1
  100. package/dist/services/redaction.d.ts +2 -0
  101. package/dist/services/redaction.d.ts.map +1 -0
  102. package/dist/services/redaction.js +27 -0
  103. package/dist/services/redaction.js.map +1 -0
  104. package/dist/services/session-index.d.ts.map +1 -1
  105. package/dist/services/session-index.js +7 -3
  106. package/dist/services/session-index.js.map +1 -1
  107. package/dist/services/session-storage.d.ts +99 -0
  108. package/dist/services/session-storage.d.ts.map +1 -1
  109. package/dist/services/session-storage.js +153 -2
  110. package/dist/services/session-storage.js.map +1 -1
  111. package/dist/services/verification-profile.d.ts +51 -0
  112. package/dist/services/verification-profile.d.ts.map +1 -0
  113. package/dist/services/verification-profile.js +306 -0
  114. package/dist/services/verification-profile.js.map +1 -0
  115. package/dist/services/workspace-state.d.ts +25 -0
  116. package/dist/services/workspace-state.d.ts.map +1 -0
  117. package/dist/services/workspace-state.js +115 -0
  118. package/dist/services/workspace-state.js.map +1 -0
  119. package/dist/terminal-ui/completion.d.ts.map +1 -1
  120. package/dist/terminal-ui/completion.js +9 -6
  121. package/dist/terminal-ui/completion.js.map +1 -1
  122. package/dist/terminal-ui/launch.d.ts +15 -1
  123. package/dist/terminal-ui/launch.d.ts.map +1 -1
  124. package/dist/terminal-ui/launch.js +300 -56
  125. package/dist/terminal-ui/launch.js.map +1 -1
  126. package/dist/terminal-ui/raw-editor.d.ts +4 -0
  127. package/dist/terminal-ui/raw-editor.d.ts.map +1 -1
  128. package/dist/terminal-ui/raw-editor.js +29 -0
  129. package/dist/terminal-ui/raw-editor.js.map +1 -1
  130. package/dist/tui-ui/launch.d.ts.map +1 -1
  131. package/dist/tui-ui/launch.js +1 -0
  132. package/dist/tui-ui/launch.js.map +1 -1
  133. package/dist/tui-ui/layout.d.ts.map +1 -1
  134. package/dist/tui-ui/layout.js +21 -28
  135. package/dist/tui-ui/layout.js.map +1 -1
  136. package/dist/tui-ui/pickers.d.ts.map +1 -1
  137. package/dist/tui-ui/pickers.js +21 -62
  138. package/dist/tui-ui/pickers.js.map +1 -1
  139. package/dist/tui-ui/state.d.ts +2 -4
  140. package/dist/tui-ui/state.d.ts.map +1 -1
  141. package/dist/tui-ui/state.js +13 -11
  142. package/dist/tui-ui/state.js.map +1 -1
  143. package/package.json +1 -1
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.InkChatController = exports.AgentChatController = void 0;
4
37
  exports.sessionMessagesToTranscriptEntries = sessionMessagesToTranscriptEntries;
@@ -8,17 +41,29 @@ exports.loadSessionIntoRuntime = loadSessionIntoRuntime;
8
41
  exports.closeSession = closeSession;
9
42
  const commands_1 = require("../commands");
10
43
  const parser_1 = require("../commands/parser");
44
+ const path = __importStar(require("path"));
11
45
  const session_storage_1 = require("../services/session-storage");
12
46
  const config_1 = require("../services/config");
13
47
  const framework_1 = require("../framework");
14
48
  const harness_1 = require("../harness");
15
49
  const tools_1 = require("../tools");
50
+ const tool_serializer_1 = require("../framework/tool-serializer");
51
+ const tool_artifacts_1 = require("../core/tool-artifacts");
52
+ const checkpoint_1 = require("../core/checkpoint");
16
53
  const skills_1 = require("../skills");
17
54
  const file_context_1 = require("../services/file-context");
18
55
  const prompt_context_1 = require("../services/prompt-context");
56
+ const format_1 = require("../services/format");
57
+ const workspace_state_1 = require("../services/workspace-state");
58
+ const verification_profile_1 = require("../services/verification-profile");
19
59
  const ui_events_1 = require("./ui-events");
60
+ const ui_view_model_1 = require("./ui-view-model");
20
61
  const agent_status_1 = require("./agent-status");
62
+ const loop_budget_1 = require("./loop-budget");
21
63
  const ANSI_PATTERN = /\x1b\[[0-9;?]*[A-Za-z]/g;
64
+ const LOCAL_FAST_PATH_INLINE_OUTPUT_BYTES = 2048;
65
+ const TRACE_ARGS_ARTIFACT_THRESHOLD_BYTES = 160;
66
+ const TOOL_TRANSCRIPT_ARG_BUDGET = 512;
22
67
  function stripAnsi(text) {
23
68
  return text.replace(ANSI_PATTERN, '');
24
69
  }
@@ -30,6 +75,29 @@ function isAbortError(error, abortSignal) {
30
75
  }
31
76
  return false;
32
77
  }
78
+ function errorLayerForChatError(error) {
79
+ const message = error instanceof Error ? error.message : String(error);
80
+ const lower = message.toLowerCase();
81
+ if (/NotEnoughCvError|code:\s*11210|provider|quota|rate.?limit|timeout|connection/i.test(message)) {
82
+ return 'provider';
83
+ }
84
+ if (/tool|command|exec_command|write_file|edit_file/i.test(message)) {
85
+ return 'tool';
86
+ }
87
+ if (/\bmcp\b/i.test(message))
88
+ return 'mcp';
89
+ if (/\b(session|resume|compact|harness)\b/i.test(message))
90
+ return 'session';
91
+ if (/\b(skill|skills)\b/i.test(message))
92
+ return 'skills';
93
+ if (/\b(memory|vector store|recall|forget)\b/i.test(message))
94
+ return 'memory';
95
+ if (/\b(renderer|terminal|tty|prompt|resize|scrollback)\b/i.test(message))
96
+ return 'renderer';
97
+ if (lower.includes('abort') || lower.includes('interrupted'))
98
+ return 'runtime';
99
+ return 'unknown';
100
+ }
33
101
  function formatChatError(error) {
34
102
  const message = error instanceof Error ? error.message : String(error);
35
103
  if (/NotEnoughCvError|code:\s*11210/i.test(message)) {
@@ -64,34 +132,323 @@ function compactToolArgs(args, maxLength = 160) {
64
132
  }
65
133
  return '';
66
134
  }
67
- function toolStartContent(name, args) {
135
+ function fullToolArgsForTrace(name, args) {
68
136
  if (name === 'exec_command' && typeof args.command === 'string') {
69
- return `Running ${name}\n $ ${args.command}`;
137
+ return `$ ${args.command}`;
138
+ }
139
+ try {
140
+ return JSON.stringify(args, null, 2);
141
+ }
142
+ catch {
143
+ return compactToolArgs(args, 2048);
70
144
  }
71
- const detail = compactToolArgs(args);
72
- return `Running ${name}${detail ? ` ${detail}` : ''}`;
73
145
  }
74
- function toolSummary(name, args, success, duration) {
75
- const details = compactToolArgs(args);
76
- const suffix = details ? ` ${details}` : '';
77
- return `${success ? '✓' : '✗'} ${name}${suffix} (${duration}ms)`;
146
+ function buildTraceArgsDetails(projectPath, name, args) {
147
+ const argsSummary = compactToolArgs(args);
148
+ const fullArgs = (0, session_storage_1.redactTraceText)(fullToolArgsForTrace(name, args)).trim();
149
+ const argsBytes = byteLength(fullArgs);
150
+ if (!projectPath
151
+ || !fullArgs
152
+ || fullArgs === (0, session_storage_1.redactTraceText)(argsSummary)
153
+ || argsBytes <= TRACE_ARGS_ARTIFACT_THRESHOLD_BYTES) {
154
+ return { argsSummary };
155
+ }
156
+ const artifact = (0, tool_artifacts_1.storeArtifact)(projectPath, `${name}-args`, fullArgs, argsBytes);
157
+ return artifact
158
+ ? { argsSummary, argsArtifactId: artifact.id, argsBytes }
159
+ : { argsSummary };
78
160
  }
79
- function toolFinishContent(event) {
80
- const summary = event.summary || toolSummary(event.name, event.args, event.success, event.duration);
81
- const lines = [summary];
82
- if (event.name === 'exec_command' && typeof event.args.command === 'string') {
83
- lines.push(` $ ${event.args.command}`);
161
+ function parseToolCallArgsForRuntime(toolCall) {
162
+ try {
163
+ const parsed = JSON.parse(toolCall.function.arguments || '{}');
164
+ return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
165
+ ? parsed
166
+ : null;
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ }
172
+ function resolveProjectScopedPath(cwd, filePath) {
173
+ const absolute = path.resolve(cwd, filePath);
174
+ const relative = path.relative(cwd, absolute);
175
+ if (relative.startsWith('..') || path.isAbsolute(relative))
176
+ return null;
177
+ return absolute;
178
+ }
179
+ function checkpointTargetsFromToolCalls(cwd, toolCalls) {
180
+ const targets = new Set();
181
+ for (const toolCall of toolCalls) {
182
+ const name = toolCall.function.name;
183
+ if (name !== 'write_file' && name !== 'edit_file')
184
+ continue;
185
+ const args = parseToolCallArgsForRuntime(toolCall);
186
+ if (!args || typeof args.path !== 'string')
187
+ continue;
188
+ if (name === 'edit_file' && args.preview === true)
189
+ continue;
190
+ const target = resolveProjectScopedPath(cwd, args.path);
191
+ if (target)
192
+ targets.add(target);
193
+ }
194
+ return Array.from(targets);
195
+ }
196
+ function createPreToolCheckpoint(events, sessionId, turnId, checkpointId, cwd, toolCalls) {
197
+ const targets = checkpointTargetsFromToolCalls(cwd, toolCalls);
198
+ if (targets.length === 0)
199
+ return { created: false, targetCount: 0, risky: false };
200
+ const risky = (0, checkpoint_1.shouldCreateMultiFileCheckpoint)(targets.length);
201
+ const checkpoint = (0, checkpoint_1.createCheckpoint)(cwd, checkpointId, targets);
202
+ if (!sessionId)
203
+ return { created: true, targetCount: targets.length, risky };
204
+ const relativeTargets = targets.map(target => path.relative(cwd, target));
205
+ recordTraceEvent(events, sessionId, {
206
+ turnId,
207
+ type: 'checkpoint',
208
+ checkpointId,
209
+ checkpointFileCount: checkpoint?.files.length ?? 0,
210
+ checkpointFiles: checkpoint?.files.map(file => file.path) ?? [],
211
+ workspaceFiles: relativeTargets,
212
+ note: checkpoint
213
+ ? (risky ? 'risky_multi_file_checkpoint' : 'pre_edit_checkpoint')
214
+ : 'pre_edit_checkpoint_skipped',
215
+ });
216
+ return { created: true, targetCount: targets.length, risky };
217
+ }
218
+ function byteLength(text) {
219
+ return Buffer.byteLength(text, 'utf8');
220
+ }
221
+ function traceTurnId(turnId) {
222
+ return turnId == null ? `turn-${Date.now()}` : String(turnId);
223
+ }
224
+ function compactTraceError(error) {
225
+ const message = error instanceof Error ? error.message : String(error);
226
+ return compactMiddle(message, 240);
227
+ }
228
+ function getLastRequestDiagnostics(llm) {
229
+ if (!llm)
230
+ return undefined;
231
+ const reader = llm.getLastRequestDiagnostics;
232
+ return typeof reader === 'function' ? reader.call(llm) : undefined;
233
+ }
234
+ function compactPathList(paths, maxItems = 40) {
235
+ return paths.slice(0, maxItems);
236
+ }
237
+ function formatWorkspaceFileForTrace(file) {
238
+ const metadata = [
239
+ typeof file.sizeBytes === 'number' ? `${file.sizeBytes}B` : '',
240
+ typeof file.mtimeMs === 'number' ? `mtime=${file.mtimeMs}` : '',
241
+ ].filter(Boolean).join(' ');
242
+ return `${file.status} ${file.path}${metadata ? ` (${metadata})` : ''}`;
243
+ }
244
+ function appendWorkspaceSnapshotTrace(events, sessionId, turnId, phase, snapshot) {
245
+ if (!sessionId)
246
+ return;
247
+ recordTraceEvent(events, sessionId, {
248
+ turnId,
249
+ type: 'workspace_snapshot',
250
+ workspacePhase: phase,
251
+ workspaceGitAvailable: snapshot.gitAvailable,
252
+ workspaceDirty: snapshot.dirty,
253
+ workspaceBranch: snapshot.branch,
254
+ workspaceFileCount: snapshot.fileCount,
255
+ workspaceFiles: compactPathList(snapshot.files.map(formatWorkspaceFileForTrace)),
256
+ error: snapshot.error ? compactMiddle(snapshot.error, 240) : undefined,
257
+ });
258
+ }
259
+ function appendWorkspaceDeltaTrace(events, sessionId, turnId, before, after) {
260
+ const delta = (0, workspace_state_1.diffWorkspaceSnapshots)(before, after);
261
+ if (sessionId) {
262
+ recordTraceEvent(events, sessionId, {
263
+ turnId,
264
+ type: 'workspace_delta',
265
+ workspaceFileCount: delta.filesAfterTurn.length,
266
+ workspaceFiles: compactPathList(delta.filesAfterTurn),
267
+ workspaceNewByTurn: compactPathList(delta.newFilesByTurn),
268
+ workspaceChangedByTurn: compactPathList(delta.changedByTurn),
269
+ workspaceModifiedPreExistingByTurn: compactPathList(delta.modifiedPreExistingByTurn),
270
+ workspaceResolvedByTurn: compactPathList(delta.resolvedByTurn),
271
+ note: `pre_existing=${delta.preExistingFiles.length}`,
272
+ });
273
+ }
274
+ return delta;
275
+ }
276
+ function workspaceDeltaHasTurnChanges(delta) {
277
+ return delta.newFilesByTurn.length > 0
278
+ || delta.changedByTurn.length > 0
279
+ || delta.resolvedByTurn.length > 0;
280
+ }
281
+ function formatFailureRecoveryNotice(turnId, delta, checkpointIds) {
282
+ const files = compactPathList([
283
+ ...delta.newFilesByTurn,
284
+ ...delta.changedByTurn,
285
+ ...delta.resolvedByTurn,
286
+ ], 8);
287
+ const fileText = files.length > 0
288
+ ? files.join(', ')
289
+ : 'workspace changes recorded';
290
+ const checkpointText = checkpointIds.length > 0
291
+ ? ` Checkpoints: ${checkpointIds.join(', ')}. Preview rollback with /checkpoint restore <id>; restore each listed checkpoint if multiple.`
292
+ : '';
293
+ return `Turn failed after modifying files: ${fileText}. Inspect /trace ${turnId}.${checkpointText}`;
294
+ }
295
+ function appendVerificationProfileTrace(events, sessionId, turnId, profile) {
296
+ if (!sessionId || profile.changedFiles.length === 0)
297
+ return;
298
+ recordTraceEvent(events, sessionId, {
299
+ turnId,
300
+ type: 'verification_profile',
301
+ verificationProfile: profile.profile,
302
+ verificationRequired: profile.required,
303
+ verificationRisky: (0, verification_profile_1.isRiskyEdit)(profile.changedFiles),
304
+ verificationCommands: compactPathList(profile.commands, 8),
305
+ verificationChangedFiles: compactPathList(profile.changedFiles),
306
+ note: profile.reason,
307
+ });
308
+ }
309
+ function appendVerificationResultTrace(events, sessionId, turnId, result) {
310
+ if (!sessionId)
311
+ return;
312
+ recordTraceEvent(events, sessionId, {
313
+ turnId,
314
+ type: 'verification_result',
315
+ verificationCommand: result.command,
316
+ verificationPassed: result.success,
317
+ outputBytes: result.outputBytes,
318
+ error: result.error ? compactMiddle(result.error, 240) : undefined,
319
+ });
320
+ }
321
+ function appendVerificationSummaryTrace(events, sessionId, turnId, summary, changedFiles) {
322
+ if (!sessionId || changedFiles.length === 0)
323
+ return;
324
+ recordTraceEvent(events, sessionId, {
325
+ turnId,
326
+ type: 'verification_summary',
327
+ verificationProfile: summary.profile,
328
+ verificationRequired: summary.required,
329
+ verificationCommands: compactPathList(summary.commandsRun, 12),
330
+ verificationPassedCommands: compactPathList(summary.passedCommands, 12),
331
+ verificationFailedCommands: compactPathList(summary.failedCommands, 12),
332
+ verificationMissingCommands: compactPathList(summary.missingCommands, 12),
333
+ verificationChangedFiles: compactPathList(changedFiles),
334
+ verificationClaimAllowed: summary.claimAllowed,
335
+ note: summary.skippedReason,
336
+ });
337
+ }
338
+ function compactVerificationCommands(commands, maxItems = 12) {
339
+ return commands.slice(0, maxItems).map(session_storage_1.redactTraceText);
340
+ }
341
+ function withVerificationLoopStats(stats, summary) {
342
+ return {
343
+ ...stats,
344
+ verificationProfile: summary.profile,
345
+ verificationRequired: summary.required,
346
+ verificationClaimAllowed: summary.claimAllowed,
347
+ verificationPassedCommands: compactVerificationCommands(summary.passedCommands),
348
+ verificationFailedCommands: compactVerificationCommands(summary.failedCommands),
349
+ verificationMissingCommands: compactVerificationCommands(summary.missingCommands),
350
+ verificationSkippedReason: summary.skippedReason ? (0, session_storage_1.redactTraceText)(summary.skippedReason) : undefined,
351
+ };
352
+ }
353
+ function shouldRecordVerificationLoopStats(profile, summary) {
354
+ return profile.changedFiles.length > 0
355
+ || summary.commandsRun.length > 0
356
+ || summary.passedCommands.length > 0
357
+ || summary.failedCommands.length > 0
358
+ || summary.missingCommands.length > 0;
359
+ }
360
+ function appendPostWorkspaceTrace(events, sessionId, turnId, cwd, before, verificationResults = []) {
361
+ const postWorkspace = (0, workspace_state_1.captureWorkspaceSnapshot)(cwd);
362
+ appendWorkspaceSnapshotTrace(events, sessionId, turnId, 'post_turn', postWorkspace);
363
+ const delta = appendWorkspaceDeltaTrace(events, sessionId, turnId, before, postWorkspace);
364
+ const profile = (0, verification_profile_1.selectVerificationProfile)(cwd, delta.changedByTurn);
365
+ const summary = (0, verification_profile_1.summarizeVerificationState)(profile, verificationResults);
366
+ appendVerificationProfileTrace(events, sessionId, turnId, profile);
367
+ appendVerificationSummaryTrace(events, sessionId, turnId, summary, profile.changedFiles);
368
+ return { delta, profile, summary };
369
+ }
370
+ function appendAssistantNotice(messages, notice) {
371
+ for (let index = messages.length - 1; index >= 0; index--) {
372
+ const message = messages[index];
373
+ if (message.role === 'assistant' && !message.tool_calls) {
374
+ message.content = message.content ? `${message.content}\n\n${notice}` : notice;
375
+ return;
376
+ }
84
377
  }
85
- if (event.artifactRef) {
86
- lines.push(` artifact ${event.artifactRef.id} (${event.artifactRef.outputBytes}B full output)`);
378
+ messages.push({
379
+ role: 'assistant',
380
+ content: notice,
381
+ timestamp: Date.now(),
382
+ });
383
+ }
384
+ function recordTraceEvent(events, sessionId, event) {
385
+ if (!sessionId)
386
+ return null;
387
+ const traceEvent = (0, session_storage_1.appendSessionTraceEvent)(sessionId, event);
388
+ if (traceEvent) {
389
+ events.traceEventRecorded?.(traceEvent);
87
390
  }
88
- else if (typeof event.outputBytes === 'number') {
89
- lines.push(` output ${event.outputBytes}B`);
391
+ return traceEvent;
392
+ }
393
+ function recordProviderTraceEvents(events, sessionId, turnId, stats) {
394
+ if ((stats.providerRetryCount ?? 0) > 0) {
395
+ recordTraceEvent(events, sessionId, {
396
+ turnId,
397
+ type: 'provider_retry',
398
+ providerRetryCount: stats.providerRetryCount,
399
+ providerRetryDelayMs: stats.providerRetryDelayMs,
400
+ providerRetryErrorTypes: stats.providerRetryErrorTypes,
401
+ providerLastRetryErrorType: stats.providerLastRetryErrorType,
402
+ providerLastRetryStatus: stats.providerLastRetryStatus,
403
+ providerFinalModel: stats.providerFinalModel,
404
+ providerUsingFallback: stats.providerUsingFallback,
405
+ });
90
406
  }
91
- if (event.error) {
92
- lines.push(`Error: ${event.error}`);
407
+ if ((stats.providerFallbackCount ?? 0) > 0 || stats.providerUsingFallback) {
408
+ recordTraceEvent(events, sessionId, {
409
+ turnId,
410
+ type: 'provider_fallback',
411
+ providerFallbackCount: stats.providerFallbackCount,
412
+ providerFallbackFromModel: stats.providerFallbackFromModel,
413
+ providerFallbackToModel: stats.providerFallbackToModel,
414
+ providerFinalModel: stats.providerFinalModel,
415
+ providerUsingFallback: stats.providerUsingFallback,
416
+ });
93
417
  }
94
- return lines.filter(Boolean).join('\n');
418
+ }
419
+ function toHarnessDiagnostics(state) {
420
+ const stats = state.promptAssemblyStats;
421
+ const redactOptional = (value) => typeof value === 'string' ? (0, session_storage_1.redactTraceText)(value) : undefined;
422
+ const redactList = (values) => values?.slice(0, 6).map(session_storage_1.redactTraceText);
423
+ return {
424
+ taskEpoch: state.taskEpoch,
425
+ rootObjective: redactOptional(state.rootObjective ?? state.contract?.objective),
426
+ activeInstruction: redactOptional(state.activeInstruction ?? state.contract?.userIntent),
427
+ openQuestions: redactList(state.openQuestions),
428
+ diagnostics: redactList(state.diagnostics?.slice(-6)),
429
+ ledgerSize: state.ledger?.length ?? 0,
430
+ evidenceSize: state.evidenceIndex?.length ?? 0,
431
+ turnSummaryCount: state.turnSummaries?.length ?? 0,
432
+ promptAssembly: stats
433
+ ? {
434
+ modelId: stats.modelId,
435
+ estimatedTokens: stats.estimatedTokens,
436
+ budgetTokens: stats.budgetTokens,
437
+ sections: stats.sections.slice(0, 12),
438
+ includedEvidence: stats.includedEvidence.length,
439
+ omittedEvidence: stats.omittedEvidence.length,
440
+ }
441
+ : undefined,
442
+ };
443
+ }
444
+ function emitHarnessDiagnostics(events, state) {
445
+ events.harnessDiagnosticsUpdated?.(toHarnessDiagnostics(state));
446
+ }
447
+ function toolStartContent(event) {
448
+ return (0, ui_view_model_1.formatToolActivityTranscript)((0, ui_view_model_1.toolActivityFromStarted)(event, compactToolArgs(event.args, TOOL_TRANSCRIPT_ARG_BUDGET)));
449
+ }
450
+ function toolFinishContent(event) {
451
+ return (0, ui_view_model_1.formatToolActivityTranscript)((0, ui_view_model_1.toolActivityFromFinished)(event, compactToolArgs(event.args, TOOL_TRANSCRIPT_ARG_BUDGET)));
95
452
  }
96
453
  function isSyntheticCompactContext(content) {
97
454
  return content.startsWith('[OpenHorse Context State v2]')
@@ -254,25 +611,120 @@ function createAssistantStreamPresenter(events, abortSignal) {
254
611
  activeSegmentText = content;
255
612
  ensureLiveEntry();
256
613
  },
614
+ replaceMessage(content) {
615
+ if (abortSignal?.aborted || !content)
616
+ return;
617
+ activeSegmentText = content;
618
+ ensureLiveEntry();
619
+ },
257
620
  };
258
621
  }
622
+ class LocalFastPathBlockedError extends Error {
623
+ constructor(message) {
624
+ super(message);
625
+ this.name = 'LocalFastPathBlockedError';
626
+ }
627
+ }
628
+ function parseLocalFastPath(input) {
629
+ const text = input.trim();
630
+ if (/^git\s+status$/i.test(text)) {
631
+ return { tool: 'git_status', args: {}, label: 'git status' };
632
+ }
633
+ const readMatch = /^(?:read|读取)\s+(.+)$/i.exec(text);
634
+ const readTarget = readMatch?.[1]?.trim();
635
+ const looksLikePath = Boolean(readTarget)
636
+ && !/\s/.test(readTarget)
637
+ && (/[/\\.]/.test(readTarget) || readTarget.startsWith('~'));
638
+ if (readTarget && looksLikePath) {
639
+ return { tool: 'read_file', args: { path: readTarget }, label: `read ${readTarget}` };
640
+ }
641
+ const grepMatch = /^(?:grep|搜索)\s+(.+)$/i.exec(text);
642
+ if (grepMatch?.[1]?.trim()) {
643
+ return { tool: 'grep', args: { pattern: grepMatch[1].trim() }, label: `grep ${grepMatch[1].trim()}` };
644
+ }
645
+ const runTestMatch = /^(?:run\s+test|运行测试)\s*[::]\s*(.+)$/i.exec(text);
646
+ if (runTestMatch?.[1]?.trim()) {
647
+ return { tool: 'exec_command', args: { command: runTestMatch[1].trim() }, label: `run test: ${runTestMatch[1].trim()}` };
648
+ }
649
+ return null;
650
+ }
651
+ function formatLocalFastPathAssistantContent(action, rawResult, projectPath) {
652
+ const envelope = (0, tool_serializer_1.parseToolResultEnvelope)(rawResult);
653
+ const rawOutput = typeof envelope.output === 'string' ? envelope.output : '';
654
+ const output = rawOutput.trim();
655
+ const summary = envelope.summary || `${action.tool} ${envelope.success ? 'completed' : 'failed'}`;
656
+ const lines = [
657
+ envelope.success
658
+ ? `Local fast path completed ${action.label}.`
659
+ : `Local fast path failed ${action.label}.`,
660
+ '',
661
+ summary,
662
+ ];
663
+ if (!envelope.success && envelope.error) {
664
+ lines.push(`Error: ${envelope.error}`);
665
+ }
666
+ if (!output) {
667
+ return { content: lines.join('\n'), artifactRef: envelope.artifactRef };
668
+ }
669
+ let artifactRef = envelope.artifactRef;
670
+ let preview = output;
671
+ const outputBytes = envelope.outputBytes ?? byteLength(rawOutput);
672
+ if (byteLength(rawOutput) > LOCAL_FAST_PATH_INLINE_OUTPUT_BYTES) {
673
+ if (!artifactRef) {
674
+ const artifact = (0, tool_artifacts_1.storeArtifact)(projectPath, action.tool, rawOutput, outputBytes);
675
+ artifactRef = artifact ? { id: artifact.id, outputBytes: artifact.outputBytes } : undefined;
676
+ }
677
+ preview = (0, tool_artifacts_1.truncateForContext)(output, LOCAL_FAST_PATH_INLINE_OUTPUT_BYTES);
678
+ }
679
+ if (artifactRef) {
680
+ lines.push('', `Full output: /artifacts show ${artifactRef.id} --full (${(0, format_1.formatBytes)(artifactRef.outputBytes)})`);
681
+ }
682
+ lines.push('', 'Preview:', preview);
683
+ return { content: lines.join('\n'), artifactRef };
684
+ }
259
685
  function createToolEventPresenter(events) {
260
686
  const runningToolEntries = new Map();
261
687
  return {
262
688
  start(event) {
689
+ const entryId = events.append({
690
+ role: 'tool',
691
+ title: 'tool',
692
+ content: toolStartContent(event),
693
+ });
694
+ runningToolEntries.set(event.callId, {
695
+ entryId,
696
+ name: event.name,
697
+ args: event.args,
698
+ batchCount: event.batchCount,
699
+ batchIndex: event.batchIndex,
700
+ });
263
701
  events.toolStarted?.({
264
702
  callId: event.callId,
265
703
  name: event.name,
266
704
  args: event.args,
705
+ batchCount: event.batchCount,
706
+ batchIndex: event.batchIndex,
267
707
  });
268
- const entryId = events.append({
269
- role: 'tool',
270
- title: 'tool',
271
- content: toolStartContent(event.name, event.args),
272
- });
273
- runningToolEntries.set(event.callId, entryId);
274
708
  },
275
709
  finish(event) {
710
+ const content = toolFinishContent(event);
711
+ const stored = runningToolEntries.get(event.callId);
712
+ if (stored) {
713
+ events.finalize(stored.entryId, {
714
+ role: event.success ? 'tool' : 'error',
715
+ title: 'tool',
716
+ content,
717
+ });
718
+ runningToolEntries.delete(event.callId);
719
+ }
720
+ else {
721
+ const entryId = events.append({
722
+ role: event.success ? 'tool' : 'error',
723
+ title: 'tool',
724
+ content,
725
+ });
726
+ events.finalize(entryId);
727
+ }
276
728
  events.toolFinished?.({
277
729
  callId: event.callId,
278
730
  name: event.name,
@@ -283,24 +735,30 @@ function createToolEventPresenter(events) {
283
735
  error: event.error,
284
736
  outputBytes: event.outputBytes,
285
737
  artifactRef: event.artifactRef,
738
+ batchCount: event.batchCount,
739
+ batchIndex: event.batchIndex,
286
740
  });
287
- const content = toolFinishContent(event);
288
- const existingEntryId = runningToolEntries.get(event.callId);
289
- if (existingEntryId) {
290
- events.finalize(existingEntryId, {
291
- role: event.success ? 'tool' : 'error',
741
+ },
742
+ finalizePendingAsSkipped(reason = 'permission denied') {
743
+ for (const [callId, entry] of runningToolEntries) {
744
+ events.finalize(entry.entryId, {
745
+ role: 'tool',
292
746
  title: 'tool',
293
- content,
747
+ content: `Skipped · ${reason}`,
748
+ });
749
+ events.toolFinished?.({
750
+ callId,
751
+ name: entry.name,
752
+ args: entry.args,
753
+ success: false,
754
+ skipped: true,
755
+ duration: 0,
756
+ error: reason,
757
+ batchCount: entry.batchCount,
758
+ batchIndex: entry.batchIndex,
294
759
  });
295
- runningToolEntries.delete(event.callId);
296
- return;
297
760
  }
298
- const entryId = events.append({
299
- role: event.success ? 'tool' : 'error',
300
- title: 'tool',
301
- content,
302
- });
303
- events.finalize(entryId);
761
+ runningToolEntries.clear();
304
762
  },
305
763
  };
306
764
  }
@@ -331,12 +789,21 @@ class AgentChatController {
331
789
  this.events = events;
332
790
  this.controllerOptions = controllerOptions;
333
791
  }
792
+ setLoopStats(stats) {
793
+ this.runtime.store.setLastLoopStats(stats);
794
+ this.events.loopStatsUpdated?.(stats);
795
+ }
334
796
  async runInput(input, options = {}) {
335
797
  const text = input.trim();
336
798
  if (!text)
337
799
  return;
338
800
  const parsed = (0, parser_1.parseInput)(text);
339
801
  if (!parsed.isCommand) {
802
+ const localFastPath = parseLocalFastPath(text);
803
+ if (localFastPath) {
804
+ await this.runLocalFastPath(text, localFastPath, options);
805
+ return;
806
+ }
340
807
  await this.runChat(text, options);
341
808
  return;
342
809
  }
@@ -362,6 +829,7 @@ class AgentChatController {
362
829
  content: suggestions.length > 0
363
830
  ? `Unknown command: /${parsed.name}\nDid you mean: ${suggestions.map(item => `/${item}`).join(', ')}?`
364
831
  : `Unknown command: /${parsed.name}`,
832
+ errorLayer: 'runtime',
365
833
  });
366
834
  return;
367
835
  }
@@ -386,6 +854,7 @@ class AgentChatController {
386
854
  role: 'error',
387
855
  title: `/${command.name}`,
388
856
  content: result.error,
857
+ errorLayer: 'runtime',
389
858
  });
390
859
  }
391
860
  if (result.sessionPicker) {
@@ -400,6 +869,182 @@ class AgentChatController {
400
869
  await this.runChat(result.chatInput ?? parsed.args, options);
401
870
  }
402
871
  }
872
+ async runLocalFastPath(input, action, options = {}) {
873
+ const activeSession = this.runtime.getSession() ?? this.runtime.ensureSession() ?? (0, session_storage_1.loadSessionMeta)(this.runtime.getSession()?.id ?? '');
874
+ const sessionId = activeSession?.id;
875
+ const turnId = traceTurnId(options.turnId);
876
+ const localCallId = `local-${turnId}`;
877
+ const start = Date.now();
878
+ const preWorkspace = (0, workspace_state_1.captureWorkspaceSnapshot)(this.runtime.cwd);
879
+ const traceArgs = buildTraceArgsDetails(this.runtime.cwd, action.tool, action.args);
880
+ if (sessionId) {
881
+ (0, session_storage_1.appendSessionMessage)(sessionId, {
882
+ role: 'user',
883
+ content: input,
884
+ timestamp: Date.now(),
885
+ });
886
+ recordTraceEvent(this.events, sessionId, {
887
+ turnId,
888
+ type: 'turn_start',
889
+ inputBytes: byteLength(input),
890
+ localFastPathUsed: true,
891
+ });
892
+ appendWorkspaceSnapshotTrace(this.events, sessionId, turnId, 'pre_turn', preWorkspace);
893
+ recordTraceEvent(this.events, sessionId, {
894
+ turnId,
895
+ type: 'local_fast_path',
896
+ name: action.tool,
897
+ ...traceArgs,
898
+ note: compactMiddle(action.label, 160),
899
+ });
900
+ }
901
+ this.runtime.store.addMessage({ role: 'user', content: input });
902
+ this.events.setStatus(`Running local ${action.label}...`);
903
+ try {
904
+ const tool = (0, tools_1.getRuntimeTools)().find(candidate => candidate.name === action.tool);
905
+ const toolContext = {
906
+ cwd: this.runtime.cwd,
907
+ config: {
908
+ name: this.runtime.config.name,
909
+ mode: this.runtime.config.mode,
910
+ },
911
+ sessionId,
912
+ turnId,
913
+ };
914
+ const permission = tool?.checkPermissions?.(action.args, toolContext);
915
+ if (permission?.behavior === 'deny' || tool?.isDestructive?.(action.args) === true) {
916
+ const reason = permission?.reason || 'Local fast path blocked a destructive tool request.';
917
+ throw new LocalFastPathBlockedError(reason);
918
+ }
919
+ if (permission?.behavior === 'ask') {
920
+ throw new LocalFastPathBlockedError(permission.reason || 'Local fast path requires an allow-safe command.');
921
+ }
922
+ if (sessionId) {
923
+ recordTraceEvent(this.events, sessionId, {
924
+ turnId,
925
+ type: 'tool_call',
926
+ name: action.tool,
927
+ callId: localCallId,
928
+ ...traceArgs,
929
+ });
930
+ }
931
+ const result = await (0, tools_1.executeTool)(action.tool, action.args, options.abortSignal, {
932
+ ...toolContext,
933
+ sessionId,
934
+ turnId,
935
+ });
936
+ const duration = Date.now() - start;
937
+ const envelope = (0, tool_serializer_1.parseToolResultEnvelope)(result);
938
+ const outputBytes = typeof envelope.outputBytes === 'number'
939
+ ? envelope.outputBytes
940
+ : Buffer.byteLength(result, 'utf8');
941
+ const formattedLocalResult = formatLocalFastPathAssistantContent(action, result, this.runtime.cwd);
942
+ const assistantContent = formattedLocalResult.content;
943
+ const stats = (0, framework_1.createLocalFastPathLoopStats)({
944
+ finishReason: envelope.success ? 'completed' : 'failed',
945
+ toolCalls: 1,
946
+ readOnlyToolCalls: action.tool === 'exec_command' ? 0 : 1,
947
+ unsafeToolCalls: action.tool === 'exec_command' ? 1 : 0,
948
+ toolResultBytes: outputBytes,
949
+ modelVisibleToolBytes: 0,
950
+ summarizedBytes: outputBytes,
951
+ });
952
+ this.events.append({
953
+ role: envelope.success ? 'tool' : 'error',
954
+ title: 'local',
955
+ content: toolFinishContent({
956
+ type: 'tool_result',
957
+ name: action.tool,
958
+ args: action.args,
959
+ callId: localCallId,
960
+ result,
961
+ modelVisibleResult: result,
962
+ duration,
963
+ success: envelope.success,
964
+ error: envelope.error,
965
+ summary: envelope.summary,
966
+ outputBytes,
967
+ artifactRef: formattedLocalResult.artifactRef,
968
+ }),
969
+ });
970
+ this.runtime.store.addMessage({ role: 'assistant', content: assistantContent });
971
+ this.setLoopStats(stats);
972
+ if (sessionId) {
973
+ recordTraceEvent(this.events, sessionId, {
974
+ turnId,
975
+ type: 'tool_result',
976
+ name: action.tool,
977
+ callId: localCallId,
978
+ argsSummary: traceArgs.argsSummary,
979
+ argsArtifactId: traceArgs.argsArtifactId,
980
+ argsBytes: traceArgs.argsBytes,
981
+ success: envelope.success,
982
+ duration,
983
+ outputBytes,
984
+ modelVisibleBytes: 0,
985
+ artifactId: formattedLocalResult.artifactRef?.id,
986
+ error: envelope.error ? compactMiddle(envelope.error, 240) : undefined,
987
+ });
988
+ appendPostWorkspaceTrace(this.events, sessionId, turnId, this.runtime.cwd, preWorkspace);
989
+ recordTraceEvent(this.events, sessionId, {
990
+ turnId,
991
+ type: 'complete',
992
+ finishReason: stats.finishReason,
993
+ llmRequests: stats.llmRequests,
994
+ toolCalls: stats.toolCalls,
995
+ readOnlyToolCalls: stats.readOnlyToolCalls,
996
+ unsafeToolCalls: stats.unsafeToolCalls,
997
+ localFastPathUsed: true,
998
+ });
999
+ (0, session_storage_1.appendSessionMessage)(sessionId, {
1000
+ role: 'assistant',
1001
+ content: assistantContent,
1002
+ timestamp: Date.now(),
1003
+ });
1004
+ const recordedMessages = (0, session_storage_1.readSessionMessages)(sessionId);
1005
+ if (recordedMessages.length > 0) {
1006
+ (0, session_storage_1.updateSessionSummary)(sessionId, recordedMessages);
1007
+ }
1008
+ }
1009
+ this.events.setStatus(envelope.success ? `Completed local ${action.label}` : `Failed local ${action.label}`);
1010
+ }
1011
+ catch (error) {
1012
+ const message = error instanceof Error ? error.message : String(error);
1013
+ this.events.append({ role: 'error', title: 'local', content: message, errorLayer: 'tool' });
1014
+ this.events.setStatus('Local command failed. Ready for the next input.');
1015
+ const assistantContent = `Local fast path failed for ${action.label}.\n\n${message}`;
1016
+ this.runtime.store.addMessage({ role: 'assistant', content: assistantContent });
1017
+ const finishReason = error instanceof LocalFastPathBlockedError ? 'blocked' : 'failed';
1018
+ const stats = (0, framework_1.createLocalFastPathLoopStats)({
1019
+ finishReason,
1020
+ toolCalls: 0,
1021
+ });
1022
+ this.setLoopStats(stats);
1023
+ if (sessionId) {
1024
+ recordTraceEvent(this.events, sessionId, {
1025
+ turnId,
1026
+ type: 'error',
1027
+ name: action.tool,
1028
+ error: compactTraceError(error),
1029
+ });
1030
+ appendPostWorkspaceTrace(this.events, sessionId, turnId, this.runtime.cwd, preWorkspace);
1031
+ recordTraceEvent(this.events, sessionId, {
1032
+ turnId,
1033
+ type: 'complete',
1034
+ finishReason: stats.finishReason,
1035
+ llmRequests: stats.llmRequests,
1036
+ toolCalls: stats.toolCalls,
1037
+ localFastPathUsed: true,
1038
+ note: 'local_fast_path_failed',
1039
+ });
1040
+ (0, session_storage_1.appendSessionMessage)(sessionId, {
1041
+ role: 'assistant',
1042
+ content: assistantContent,
1043
+ timestamp: Date.now(),
1044
+ });
1045
+ }
1046
+ }
1047
+ }
403
1048
  createCommandContext(abortSignal, turnId) {
404
1049
  return {
405
1050
  cwd: this.runtime.cwd,
@@ -414,6 +1059,9 @@ class AgentChatController {
414
1059
  this.runtime.setSession(session);
415
1060
  this.events.replaceTranscript(sessionMessagesToTranscriptEntries(session.id));
416
1061
  },
1062
+ sessionRestored: event => {
1063
+ this.events.sessionRestored?.(event);
1064
+ },
417
1065
  getSession: this.runtime.getSession,
418
1066
  abortSignal,
419
1067
  writeOutput: text => {
@@ -426,25 +1074,28 @@ class AgentChatController {
426
1074
  this.events.append({ role: 'system', content: text });
427
1075
  }
428
1076
  },
429
- uiCapabilities: (0, ui_events_1.resolveUiRendererCapabilities)(this.controllerOptions.uiCapabilities),
1077
+ uiRenderer: this.controllerOptions.uiRenderer ?? this.runtime.config.ui?.renderer ?? 'terminal',
1078
+ uiCapabilities: (0, ui_events_1.resolveUiRendererCapabilities)(this.controllerOptions.uiCapabilities, this.controllerOptions.uiRenderer ?? this.runtime.config.ui?.renderer),
430
1079
  };
431
1080
  }
432
1081
  async runChat(input, options = {}) {
433
1082
  if (!input) {
434
- this.events.append({ role: 'error', content: 'Usage: /chat <message>' });
1083
+ this.events.append({ role: 'error', content: 'Usage: /chat <message>', errorLayer: 'runtime' });
435
1084
  return;
436
1085
  }
437
1086
  if (!this.runtime.llm || !(0, config_1.isConfigured)(this.runtime.config)) {
438
1087
  this.events.append({
439
1088
  role: 'error',
440
1089
  content: 'LLM is not configured. Set OPENHORSE_API_KEY in ~/.openhorse/openhorse.json or environment.',
1090
+ errorLayer: 'provider',
441
1091
  });
442
1092
  return;
443
1093
  }
444
1094
  const abortSignal = options.abortSignal;
445
- const turnId = options.turnId;
1095
+ const turnId = traceTurnId(options.turnId);
446
1096
  const activeSession = this.runtime.getSession() ?? this.runtime.ensureSession() ?? (0, session_storage_1.loadSessionMeta)(this.runtime.getSession()?.id ?? '');
447
1097
  const sessionId = activeSession?.id;
1098
+ const preWorkspace = (0, workspace_state_1.captureWorkspaceSnapshot)(this.runtime.cwd);
448
1099
  const runtimeTools = (0, tools_1.getRuntimeTools)();
449
1100
  const skillResolution = (0, skills_1.resolveSkillsForTurn)({
450
1101
  cwd: this.runtime.cwd,
@@ -461,6 +1112,13 @@ class AgentChatController {
461
1112
  timestamp: Date.now(),
462
1113
  appliedSkills: appliedSkillNames.length > 0 ? appliedSkillNames : undefined,
463
1114
  });
1115
+ recordTraceEvent(this.events, sessionId, {
1116
+ turnId,
1117
+ type: 'turn_start',
1118
+ inputBytes: byteLength(input),
1119
+ note: appliedSkillNames.length > 0 ? `skills=${appliedSkillNames.join(',')}` : undefined,
1120
+ });
1121
+ appendWorkspaceSnapshotTrace(this.events, sessionId, turnId, 'pre_turn', preWorkspace);
464
1122
  }
465
1123
  this.runtime.store.addMessage({ role: 'user', content: input });
466
1124
  (0, prompt_context_1.refreshProjectInstructions)(this.runtime.store, this.runtime.cwd);
@@ -477,6 +1135,10 @@ class AgentChatController {
477
1135
  });
478
1136
  const intent = harness.updateContractFromUserInput(input);
479
1137
  harness.recordAppliedSkills(skillResolution.skills);
1138
+ // Reconcile diagnostic: when harness state is present but objective may be incomplete
1139
+ if (snapshot.harnessState && !snapshot.harnessState.rootObjective && !snapshot.harnessState.contract?.objective) {
1140
+ this.events.setStatus('Resume diagnostic: harness state restored but objective may be incomplete. Run /harness explain to review.');
1141
+ }
480
1142
  const promptCtx = {
481
1143
  cwd: this.runtime.cwd,
482
1144
  platform: process.platform,
@@ -497,9 +1159,14 @@ class AgentChatController {
497
1159
  let finalContent = '';
498
1160
  let finalUsage;
499
1161
  let finalModel = '';
1162
+ let pendingCompleteTrace = null;
1163
+ let pendingCompleteStats;
1164
+ const verificationResults = [];
500
1165
  const sessionMessagesToRecord = [];
501
1166
  const assistantStream = createAssistantStreamPresenter(this.events, abortSignal);
502
1167
  const toolEvents = createToolEventPresenter(this.events);
1168
+ let checkpointSequence = 0;
1169
+ const checkpointIds = [];
503
1170
  const streamCallbacks = {
504
1171
  onChunk: chunk => {
505
1172
  assistantStream.appendChunk(chunk);
@@ -524,6 +1191,14 @@ class AgentChatController {
524
1191
  turnId,
525
1192
  });
526
1193
  };
1194
+ const loopBudget = (0, loop_budget_1.resolveRuntimeLoopBudget)(input, this.runtime.config, harness.toJSON());
1195
+ let observedTurnsStarted = 0;
1196
+ let observedLlmRequests = 0;
1197
+ let observedToolCalls = 0;
1198
+ let observedReadOnlyToolCalls = 0;
1199
+ let observedUnsafeToolCalls = 0;
1200
+ let observedToolResultBytes = 0;
1201
+ let observedModelVisibleToolBytes = 0;
527
1202
  try {
528
1203
  for await (const event of (0, framework_1.query)({
529
1204
  messages,
@@ -547,17 +1222,86 @@ class AgentChatController {
547
1222
  abortSignal,
548
1223
  harness,
549
1224
  input,
1225
+ loopBudget,
550
1226
  })) {
551
1227
  switch (event.type) {
552
1228
  case 'request_start':
1229
+ observedTurnsStarted = Math.max(observedTurnsStarted, event.turn);
1230
+ observedLlmRequests++;
553
1231
  assistantStream.discardSegment();
554
1232
  this.events.setStatus((0, agent_status_1.agentStepStatus)(event.turn));
1233
+ if (sessionId) {
1234
+ recordTraceEvent(this.events, sessionId, {
1235
+ turnId,
1236
+ type: 'request_start',
1237
+ model: event.model,
1238
+ turn: event.turn,
1239
+ });
1240
+ }
1241
+ break;
1242
+ case 'prompt_assembly':
1243
+ if (sessionId) {
1244
+ recordTraceEvent(this.events, sessionId, {
1245
+ turnId,
1246
+ type: 'prompt_assembly',
1247
+ promptModelId: event.modelId,
1248
+ promptEstimatedTokens: event.estimatedTokens,
1249
+ promptBudgetTokens: event.budgetTokens,
1250
+ promptCoreTokens: event.coreTokens,
1251
+ promptEvidenceBudgetTokens: event.evidenceBudgetTokens,
1252
+ promptRecentTurnBudgetTokens: event.recentTurnBudgetTokens,
1253
+ promptSections: event.sections,
1254
+ promptIncludedEvidence: event.includedEvidence,
1255
+ promptOmittedEvidence: event.omittedEvidence,
1256
+ promptIncludedEvidenceCount: event.includedEvidenceCount,
1257
+ promptOmittedEvidenceCount: event.omittedEvidenceCount,
1258
+ });
1259
+ }
555
1260
  break;
556
1261
  case 'assistant_tool_calls':
557
1262
  assistantStream.ensureMessage(event.content || '');
558
1263
  assistantStream.closeSegment();
559
- if (event.toolCalls.length > 1) {
560
- this.events.setStatus((0, agent_status_1.runningToolsStatus)(event.toolCalls.length));
1264
+ this.events.setStatus((0, agent_status_1.runningToolsStatus)(event.toolCalls.length));
1265
+ {
1266
+ const batchReadOnlyCount = event.toolCalls.filter(tc => {
1267
+ const def = skillResolution.tools.find(t => t.name === tc.function.name);
1268
+ const args = parseToolCallArgsForRuntime(tc);
1269
+ return args && def?.isReadOnly?.(args) === true;
1270
+ }).length;
1271
+ const suggestion = (0, agent_status_1.batchingSuggestion)(batchReadOnlyCount);
1272
+ if (suggestion) {
1273
+ this.events.append({ role: 'status', content: suggestion });
1274
+ }
1275
+ }
1276
+ const checkpointId = checkpointSequence === 0
1277
+ ? turnId
1278
+ : `${turnId}-checkpoint-${checkpointSequence + 1}`;
1279
+ const checkpointResult = createPreToolCheckpoint(this.events, sessionId, turnId, checkpointId, this.runtime.cwd, event.toolCalls);
1280
+ if (checkpointResult.created) {
1281
+ checkpointIds.push(checkpointId);
1282
+ checkpointSequence++;
1283
+ }
1284
+ if (checkpointResult.created && checkpointResult.risky) {
1285
+ this.events.append({
1286
+ role: 'status',
1287
+ title: 'checkpoint',
1288
+ content: `Risky edit: ${checkpointResult.targetCount} files in one turn. Checkpoint ${checkpointId} created for rollback (/checkpoints restore ${checkpointId}).`,
1289
+ });
1290
+ }
1291
+ else if (checkpointResult.risky) {
1292
+ this.events.append({
1293
+ role: 'status',
1294
+ title: 'checkpoint',
1295
+ content: `Risky edit: ${checkpointResult.targetCount} files in one turn, but checkpoint creation failed. Restore any pre-existing checkpoint manually or revert via git.`,
1296
+ });
1297
+ }
1298
+ if (sessionId) {
1299
+ recordTraceEvent(this.events, sessionId, {
1300
+ turnId,
1301
+ type: 'assistant_tool_calls',
1302
+ toolCallCount: event.toolCalls.length,
1303
+ contentBytes: byteLength(event.content || ''),
1304
+ });
561
1305
  }
562
1306
  sessionMessagesToRecord.push({
563
1307
  role: 'assistant',
@@ -567,11 +1311,83 @@ class AgentChatController {
567
1311
  });
568
1312
  break;
569
1313
  case 'tool_call':
1314
+ observedToolCalls++;
1315
+ {
1316
+ const toolDefinition = skillResolution.tools.find(tool => tool.name === event.name);
1317
+ if (toolDefinition?.isReadOnly?.(event.args) === true) {
1318
+ observedReadOnlyToolCalls++;
1319
+ }
1320
+ else {
1321
+ observedUnsafeToolCalls++;
1322
+ }
1323
+ }
570
1324
  assistantStream.closeSegment();
571
1325
  toolEvents.start(event);
1326
+ if (sessionId) {
1327
+ const traceArgs = buildTraceArgsDetails(this.runtime.cwd, event.name, event.args);
1328
+ recordTraceEvent(this.events, sessionId, {
1329
+ turnId,
1330
+ type: 'tool_call',
1331
+ name: event.name,
1332
+ callId: event.callId,
1333
+ ...traceArgs,
1334
+ batchCount: event.batchCount,
1335
+ batchIndex: event.batchIndex,
1336
+ });
1337
+ }
1338
+ break;
1339
+ case 'permission_decision':
1340
+ if (sessionId) {
1341
+ recordTraceEvent(this.events, sessionId, {
1342
+ turnId,
1343
+ type: 'permission_decision',
1344
+ name: event.name,
1345
+ callId: event.callId,
1346
+ argsSummary: compactToolArgs(event.args),
1347
+ permissionBehavior: event.decision.behavior,
1348
+ permissionApproved: event.decision.approved,
1349
+ permissionSource: event.decision.source,
1350
+ permissionReason: event.decision.reason
1351
+ ? compactMiddle(event.decision.reason, 240)
1352
+ : undefined,
1353
+ permissionDuration: event.decision.duration,
1354
+ batchCount: event.batchCount,
1355
+ batchIndex: event.batchIndex,
1356
+ });
1357
+ }
572
1358
  break;
573
1359
  case 'tool_result': {
1360
+ observedToolResultBytes += event.outputBytes ?? byteLength(event.result);
1361
+ observedModelVisibleToolBytes += byteLength(event.modelVisibleResult);
574
1362
  toolEvents.finish(event);
1363
+ if (sessionId) {
1364
+ recordTraceEvent(this.events, sessionId, {
1365
+ turnId,
1366
+ type: 'tool_result',
1367
+ name: event.name,
1368
+ callId: event.callId,
1369
+ argsSummary: compactToolArgs(event.args),
1370
+ success: event.success,
1371
+ duration: event.duration,
1372
+ outputBytes: event.outputBytes,
1373
+ modelVisibleBytes: byteLength(event.modelVisibleResult),
1374
+ artifactId: event.artifactRef?.id,
1375
+ error: event.error ? compactMiddle(event.error, 240) : undefined,
1376
+ batchCount: event.batchCount,
1377
+ batchIndex: event.batchIndex,
1378
+ });
1379
+ }
1380
+ const verificationResult = (0, verification_profile_1.collectVerificationCommandResult)({
1381
+ toolName: event.name,
1382
+ args: event.args,
1383
+ success: event.success,
1384
+ outputBytes: event.outputBytes,
1385
+ error: event.error,
1386
+ });
1387
+ if (verificationResult) {
1388
+ verificationResults.push(verificationResult);
1389
+ appendVerificationResultTrace(this.events, sessionId, turnId, verificationResult);
1390
+ }
575
1391
  sessionMessagesToRecord.push({
576
1392
  role: 'tool',
577
1393
  content: event.result,
@@ -583,10 +1399,24 @@ class AgentChatController {
583
1399
  }
584
1400
  case 'strategy_exhausted':
585
1401
  this.events.append({ role: 'status', content: event.suggestion });
1402
+ if (sessionId) {
1403
+ recordTraceEvent(this.events, sessionId, {
1404
+ turnId,
1405
+ type: 'strategy_exhausted',
1406
+ note: compactMiddle(event.suggestion, 240),
1407
+ });
1408
+ }
586
1409
  break;
587
1410
  case 'message':
588
1411
  finalContent = event.content;
589
1412
  assistantStream.ensureMessage(event.content);
1413
+ if (sessionId) {
1414
+ recordTraceEvent(this.events, sessionId, {
1415
+ turnId,
1416
+ type: 'message',
1417
+ contentBytes: byteLength(event.content),
1418
+ });
1419
+ }
590
1420
  if (event.content) {
591
1421
  sessionMessagesToRecord.push({
592
1422
  role: 'assistant',
@@ -596,11 +1426,58 @@ class AgentChatController {
596
1426
  }
597
1427
  break;
598
1428
  case 'complete':
1429
+ if (event.stats?.finishReason === 'blocked') {
1430
+ toolEvents.finalizePendingAsSkipped('permission denied');
1431
+ }
1432
+ if (event.content && !finalContent) {
1433
+ if (event.stats?.finishReason === 'budget_exceeded') {
1434
+ assistantStream.replaceMessage(event.content);
1435
+ }
1436
+ else {
1437
+ assistantStream.ensureMessage(event.content);
1438
+ }
1439
+ sessionMessagesToRecord.push({
1440
+ role: 'assistant',
1441
+ content: event.content,
1442
+ timestamp: Date.now(),
1443
+ });
1444
+ }
599
1445
  finalContent = event.content;
600
1446
  finalUsage = event.usage;
601
1447
  finalModel = event.model;
602
1448
  if (event.stats) {
603
- this.runtime.store.setLastLoopStats(event.stats);
1449
+ pendingCompleteStats = event.stats;
1450
+ recordProviderTraceEvents(this.events, sessionId, turnId, event.stats);
1451
+ pendingCompleteTrace = {
1452
+ turnId,
1453
+ type: 'complete',
1454
+ model: event.model,
1455
+ contentBytes: byteLength(event.content || ''),
1456
+ finishReason: event.stats.finishReason,
1457
+ llmRequests: event.stats.llmRequests,
1458
+ toolCalls: event.stats.toolCalls,
1459
+ readOnlyToolCalls: event.stats.readOnlyToolCalls,
1460
+ unsafeToolCalls: event.stats.unsafeToolCalls,
1461
+ loopBudgetSource: event.stats.loopBudgetSource,
1462
+ loopBudgetBaseProfile: event.stats.loopBudgetBaseProfile,
1463
+ loopBudgetMaxLlmRequests: event.stats.loopBudgetMaxLlmRequests,
1464
+ loopBudgetMaxToolCalls: event.stats.loopBudgetMaxToolCalls,
1465
+ loopBudgetMaxReadOnlyFragmentation: event.stats.loopBudgetMaxReadOnlyFragmentation,
1466
+ loopBudgetMaxModelVisibleBytes: event.stats.loopBudgetMaxModelVisibleBytes,
1467
+ loopBudgetConfigOverride: event.stats.loopBudgetConfigOverride,
1468
+ budgetExceededReason: event.stats.budgetExceededReason,
1469
+ continuationActions: event.stats.continuationActions,
1470
+ continuationHint: event.stats.continuationHint,
1471
+ localFastPathUsed: event.stats.localFastPathUsed,
1472
+ };
1473
+ }
1474
+ else {
1475
+ pendingCompleteTrace = {
1476
+ turnId,
1477
+ type: 'complete',
1478
+ model: event.model,
1479
+ contentBytes: byteLength(event.content || ''),
1480
+ };
604
1481
  }
605
1482
  break;
606
1483
  }
@@ -611,11 +1488,97 @@ class AgentChatController {
611
1488
  this.events.setStatus('Interrupted.');
612
1489
  removeTrailingUserMessage(this.runtime);
613
1490
  if (sessionId) {
1491
+ const { delta } = appendPostWorkspaceTrace(this.events, sessionId, turnId, this.runtime.cwd, preWorkspace, verificationResults);
1492
+ recordTraceEvent(this.events, sessionId, {
1493
+ turnId,
1494
+ type: 'aborted',
1495
+ note: 'aborted_after_query',
1496
+ });
1497
+ const recoveryNotice = workspaceDeltaHasTurnChanges(delta)
1498
+ ? formatFailureRecoveryNotice(turnId, delta, checkpointIds)
1499
+ : undefined;
1500
+ if (recoveryNotice) {
1501
+ this.events.append({
1502
+ role: 'status',
1503
+ title: 'recovery',
1504
+ content: recoveryNotice,
1505
+ });
1506
+ }
614
1507
  (0, session_storage_1.removeLastIncompleteAssistantMessage)(sessionId);
615
1508
  }
616
1509
  return;
617
1510
  }
618
1511
  assistantStream.closeSegment();
1512
+ if (sessionId) {
1513
+ const { profile, summary } = appendPostWorkspaceTrace(this.events, sessionId, turnId, this.runtime.cwd, preWorkspace, verificationResults);
1514
+ if (profile.changedFiles.length > 0 && profile.required) {
1515
+ this.events.setStatus((0, agent_status_1.verifyingStatus)(profile.profile));
1516
+ this.controllerOptions.onVerificationStateChange?.('running');
1517
+ }
1518
+ if (shouldRecordVerificationLoopStats(profile, summary)) {
1519
+ const stats = pendingCompleteStats ?? this.runtime.store.getSnapshot().lastLoopStats;
1520
+ if (stats) {
1521
+ pendingCompleteStats = withVerificationLoopStats(stats, summary);
1522
+ }
1523
+ }
1524
+ if ((0, verification_profile_1.shouldGateCompletion)(summary)) {
1525
+ this.events.setStatus((0, agent_status_1.verificationGateStatus)(summary.skippedReason ?? 'verification checks not run'));
1526
+ this.controllerOptions.onVerificationStateChange?.('gated');
1527
+ const notice = (0, verification_profile_1.formatVerificationGateNotice)(summary);
1528
+ this.events.append({
1529
+ role: 'status',
1530
+ title: 'verification',
1531
+ content: notice,
1532
+ });
1533
+ finalContent = finalContent ? `${finalContent}\n\n${notice}` : notice;
1534
+ appendAssistantNotice(sessionMessagesToRecord, notice);
1535
+ if (pendingCompleteTrace) {
1536
+ pendingCompleteTrace.finishReason = 'completion_gate';
1537
+ pendingCompleteTrace.contentBytes = byteLength(finalContent);
1538
+ pendingCompleteTrace.note = 'verification_incomplete';
1539
+ }
1540
+ const stats = pendingCompleteStats ?? this.runtime.store.getSnapshot().lastLoopStats;
1541
+ if (stats) {
1542
+ pendingCompleteStats = {
1543
+ ...withVerificationLoopStats(stats, summary),
1544
+ finishReason: 'completion_gate',
1545
+ };
1546
+ }
1547
+ }
1548
+ else if (profile.changedFiles.length > 0 && profile.required) {
1549
+ this.controllerOptions.onVerificationStateChange?.('passed');
1550
+ }
1551
+ }
1552
+ if (pendingCompleteStats?.finishReason === 'budget_exceeded') {
1553
+ const stats = pendingCompleteStats;
1554
+ const lines = ['Loop budget reached — stopping this turn.'];
1555
+ if (stats.budgetExceededReason) {
1556
+ lines.push(`Reason: ${stats.budgetExceededReason}`);
1557
+ }
1558
+ const progressParts = [];
1559
+ if (typeof stats.loopBudgetMaxLlmRequests === 'number') {
1560
+ progressParts.push(`${stats.llmRequests ?? 0}/${stats.loopBudgetMaxLlmRequests} LLM requests`);
1561
+ }
1562
+ if (typeof stats.loopBudgetMaxToolCalls === 'number') {
1563
+ progressParts.push(`${stats.toolCalls ?? 0}/${stats.loopBudgetMaxToolCalls} tool calls`);
1564
+ }
1565
+ if (progressParts.length) {
1566
+ lines.push(`Progress: ${progressParts.join(', ')}`);
1567
+ }
1568
+ if (stats.continuationActions?.length) {
1569
+ lines.push(`Next: ${stats.continuationActions.join('; ')}`);
1570
+ }
1571
+ else if (stats.continuationHint) {
1572
+ lines.push(`Next: ${stats.continuationHint}`);
1573
+ }
1574
+ const notice = lines.join('\n');
1575
+ this.events.append({ role: 'status', title: 'budget', content: notice });
1576
+ finalContent = finalContent ? `${finalContent}\n\n${notice}` : notice;
1577
+ appendAssistantNotice(sessionMessagesToRecord, notice);
1578
+ }
1579
+ if (pendingCompleteStats) {
1580
+ this.setLoopStats(pendingCompleteStats);
1581
+ }
619
1582
  if (finalContent) {
620
1583
  this.runtime.store.addMessage({ role: 'assistant', content: finalContent });
621
1584
  }
@@ -633,7 +1596,11 @@ class AgentChatController {
633
1596
  });
634
1597
  const harnessState = harness.toJSON();
635
1598
  this.runtime.store.setState({ harnessState });
1599
+ emitHarnessDiagnostics(this.events, harnessState);
636
1600
  if (sessionId) {
1601
+ if (pendingCompleteTrace) {
1602
+ recordTraceEvent(this.events, sessionId, pendingCompleteTrace);
1603
+ }
637
1604
  (0, session_storage_1.updateSessionSkills)(sessionId, appliedSkillNames);
638
1605
  (0, session_storage_1.updateSessionHarnessState)(sessionId, harnessState);
639
1606
  const recordedMessages = (0, session_storage_1.readSessionMessages)(sessionId);
@@ -649,13 +1616,83 @@ class AgentChatController {
649
1616
  this.events.setStatus('Interrupted.');
650
1617
  removeTrailingUserMessage(this.runtime);
651
1618
  if (sessionId) {
1619
+ const { delta } = appendPostWorkspaceTrace(this.events, sessionId, turnId, this.runtime.cwd, preWorkspace, verificationResults);
1620
+ recordTraceEvent(this.events, sessionId, {
1621
+ turnId,
1622
+ type: 'aborted',
1623
+ note: 'abort_error',
1624
+ });
1625
+ const recoveryNotice = workspaceDeltaHasTurnChanges(delta)
1626
+ ? formatFailureRecoveryNotice(turnId, delta, checkpointIds)
1627
+ : undefined;
1628
+ if (recoveryNotice) {
1629
+ this.events.append({
1630
+ role: 'status',
1631
+ title: 'recovery',
1632
+ content: recoveryNotice,
1633
+ });
1634
+ }
652
1635
  (0, session_storage_1.removeLastIncompleteAssistantMessage)(sessionId);
653
1636
  }
654
1637
  return;
655
1638
  }
656
1639
  assistantStream.discardSegment();
657
- this.events.append({ role: 'error', content: formatChatError(error) });
1640
+ this.events.append({ role: 'error', content: formatChatError(error), errorLayer: errorLayerForChatError(error) });
658
1641
  this.events.setStatus('Turn failed. Ready for the next input.');
1642
+ const failedStats = error instanceof framework_1.QueryLoopError
1643
+ ? error.stats
1644
+ : (0, framework_1.createFailedLoopStats)({
1645
+ loopBudget,
1646
+ diagnostics: observedLlmRequests > 0 ? getLastRequestDiagnostics(this.runtime.llm) : undefined,
1647
+ turnsStarted: observedTurnsStarted,
1648
+ llmRequests: observedLlmRequests,
1649
+ toolCalls: observedToolCalls,
1650
+ readOnlyToolCalls: observedReadOnlyToolCalls,
1651
+ unsafeToolCalls: observedUnsafeToolCalls,
1652
+ toolResultBytes: observedToolResultBytes,
1653
+ modelVisibleToolBytes: observedModelVisibleToolBytes,
1654
+ });
1655
+ this.setLoopStats(failedStats);
1656
+ if (sessionId) {
1657
+ recordProviderTraceEvents(this.events, sessionId, turnId, failedStats);
1658
+ const { delta } = appendPostWorkspaceTrace(this.events, sessionId, turnId, this.runtime.cwd, preWorkspace, verificationResults);
1659
+ const recoveryNotice = workspaceDeltaHasTurnChanges(delta)
1660
+ ? formatFailureRecoveryNotice(turnId, delta, checkpointIds)
1661
+ : undefined;
1662
+ if (recoveryNotice) {
1663
+ this.events.append({
1664
+ role: 'status',
1665
+ title: 'recovery',
1666
+ content: recoveryNotice,
1667
+ });
1668
+ }
1669
+ recordTraceEvent(this.events, sessionId, {
1670
+ turnId,
1671
+ type: 'error',
1672
+ error: compactTraceError(error),
1673
+ note: recoveryNotice,
1674
+ });
1675
+ recordTraceEvent(this.events, sessionId, {
1676
+ turnId,
1677
+ type: 'complete',
1678
+ model: failedStats.providerFinalModel ?? this.runtime.llm.getModel(),
1679
+ contentBytes: 0,
1680
+ finishReason: failedStats.finishReason,
1681
+ llmRequests: failedStats.llmRequests,
1682
+ toolCalls: failedStats.toolCalls,
1683
+ readOnlyToolCalls: failedStats.readOnlyToolCalls,
1684
+ unsafeToolCalls: failedStats.unsafeToolCalls,
1685
+ loopBudgetSource: failedStats.loopBudgetSource,
1686
+ loopBudgetBaseProfile: failedStats.loopBudgetBaseProfile,
1687
+ loopBudgetMaxLlmRequests: failedStats.loopBudgetMaxLlmRequests,
1688
+ loopBudgetMaxToolCalls: failedStats.loopBudgetMaxToolCalls,
1689
+ loopBudgetMaxReadOnlyFragmentation: failedStats.loopBudgetMaxReadOnlyFragmentation,
1690
+ loopBudgetMaxModelVisibleBytes: failedStats.loopBudgetMaxModelVisibleBytes,
1691
+ loopBudgetConfigOverride: failedStats.loopBudgetConfigOverride,
1692
+ localFastPathUsed: failedStats.localFastPathUsed,
1693
+ });
1694
+ (0, session_storage_1.removeLastIncompleteAssistantMessage)(sessionId);
1695
+ }
659
1696
  const history = this.runtime.store.getSnapshot().conversationHistory;
660
1697
  if (history.length > 0) {
661
1698
  this.runtime.store.setState({ conversationHistory: history.slice(0, -1) });