openhorse 0.2.12 → 0.2.13

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 (79) hide show
  1. package/dist/commands/index.d.ts.map +1 -1
  2. package/dist/commands/index.js +597 -12
  3. package/dist/commands/index.js.map +1 -1
  4. package/dist/commands/types.d.ts +3 -1
  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 +1 -0
  8. package/dist/core/checkpoint.d.ts.map +1 -1
  9. package/dist/core/checkpoint.js +59 -10
  10. package/dist/core/checkpoint.js.map +1 -1
  11. package/dist/core/tool-artifacts.d.ts +10 -0
  12. package/dist/core/tool-artifacts.d.ts.map +1 -1
  13. package/dist/core/tool-artifacts.js +81 -3
  14. package/dist/core/tool-artifacts.js.map +1 -1
  15. package/dist/framework/index.d.ts +2 -2
  16. package/dist/framework/index.d.ts.map +1 -1
  17. package/dist/framework/index.js +5 -1
  18. package/dist/framework/index.js.map +1 -1
  19. package/dist/framework/query.d.ts +87 -2
  20. package/dist/framework/query.d.ts.map +1 -1
  21. package/dist/framework/query.js +281 -14
  22. package/dist/framework/query.js.map +1 -1
  23. package/dist/framework/tool-scheduler.d.ts +9 -0
  24. package/dist/framework/tool-scheduler.d.ts.map +1 -1
  25. package/dist/framework/tool-scheduler.js +34 -0
  26. package/dist/framework/tool-scheduler.js.map +1 -1
  27. package/dist/ink-ui/components/StatusLine.d.ts.map +1 -1
  28. package/dist/ink-ui/components/StatusLine.js +7 -1
  29. package/dist/ink-ui/components/StatusLine.js.map +1 -1
  30. package/dist/runtime/agent-runtime-protocol.d.ts +13 -1
  31. package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
  32. package/dist/runtime/agent-runtime-protocol.js +24 -0
  33. package/dist/runtime/agent-runtime-protocol.js.map +1 -1
  34. package/dist/runtime/chat-controller.d.ts +3 -0
  35. package/dist/runtime/chat-controller.d.ts.map +1 -1
  36. package/dist/runtime/chat-controller.js +907 -2
  37. package/dist/runtime/chat-controller.js.map +1 -1
  38. package/dist/runtime/loop-budget.d.ts +5 -0
  39. package/dist/runtime/loop-budget.d.ts.map +1 -0
  40. package/dist/runtime/loop-budget.js +77 -0
  41. package/dist/runtime/loop-budget.js.map +1 -0
  42. package/dist/runtime/ui-events.d.ts +34 -1
  43. package/dist/runtime/ui-events.d.ts.map +1 -1
  44. package/dist/runtime/ui-events.js.map +1 -1
  45. package/dist/services/config-dir.d.ts +2 -0
  46. package/dist/services/config-dir.d.ts.map +1 -1
  47. package/dist/services/config-dir.js +5 -0
  48. package/dist/services/config-dir.js.map +1 -1
  49. package/dist/services/config.d.ts +4 -2
  50. package/dist/services/config.d.ts.map +1 -1
  51. package/dist/services/config.js +19 -0
  52. package/dist/services/config.js.map +1 -1
  53. package/dist/services/global-config.d.ts +17 -0
  54. package/dist/services/global-config.d.ts.map +1 -1
  55. package/dist/services/global-config.js.map +1 -1
  56. package/dist/services/llm.d.ts +16 -0
  57. package/dist/services/llm.d.ts.map +1 -1
  58. package/dist/services/llm.js +56 -11
  59. package/dist/services/llm.js.map +1 -1
  60. package/dist/services/redaction.d.ts +2 -0
  61. package/dist/services/redaction.d.ts.map +1 -0
  62. package/dist/services/redaction.js +27 -0
  63. package/dist/services/redaction.js.map +1 -0
  64. package/dist/services/session-index.d.ts.map +1 -1
  65. package/dist/services/session-index.js +7 -3
  66. package/dist/services/session-index.js.map +1 -1
  67. package/dist/services/session-storage.d.ts +98 -0
  68. package/dist/services/session-storage.d.ts.map +1 -1
  69. package/dist/services/session-storage.js +153 -2
  70. package/dist/services/session-storage.js.map +1 -1
  71. package/dist/services/verification-profile.d.ts +36 -0
  72. package/dist/services/verification-profile.d.ts.map +1 -0
  73. package/dist/services/verification-profile.js +239 -0
  74. package/dist/services/verification-profile.js.map +1 -0
  75. package/dist/services/workspace-state.d.ts +25 -0
  76. package/dist/services/workspace-state.d.ts.map +1 -0
  77. package/dist/services/workspace-state.js +115 -0
  78. package/dist/services/workspace-state.js.map +1 -0
  79. package/package.json +1 -1
@@ -40,8 +40,11 @@ const doctor_1 = require("../services/doctor");
40
40
  const model_context_1 = require("../services/model-context");
41
41
  const workspace_diff_1 = require("../services/workspace-diff");
42
42
  const commit_plan_1 = require("../services/commit-plan");
43
+ const tool_artifacts_1 = require("../core/tool-artifacts");
44
+ const checkpoint_1 = require("../core/checkpoint");
43
45
  const storage_maintenance_1 = require("../services/storage-maintenance");
44
46
  const agent_status_1 = require("../runtime/agent-status");
47
+ const loop_budget_1 = require("../runtime/loop-budget");
45
48
  // ============================================================================
46
49
  // 颜色常量
47
50
  // ============================================================================
@@ -75,6 +78,112 @@ function formatTokenCount(tokens) {
75
78
  function formatThreshold(value) {
76
79
  return `${Math.round(value * 100)}%`;
77
80
  }
81
+ function formatDurationMs(ms) {
82
+ if (ms >= 1000)
83
+ return `${(ms / 1000).toFixed(1)}s`;
84
+ return `${Math.round(ms)}ms`;
85
+ }
86
+ function formatLoopBudgetSource(stats) {
87
+ const source = stats.loopBudgetSource ?? 'unknown';
88
+ if (source === 'config' && stats.loopBudgetBaseProfile) {
89
+ return `config over ${stats.loopBudgetBaseProfile}`;
90
+ }
91
+ return source;
92
+ }
93
+ function formatLoopStatsLines(stats, detail = false) {
94
+ const lines = [
95
+ `Finish ${stats.finishReason}`,
96
+ `Requests ${stats.llmRequests} LLM / ${stats.turnsStarted} turns`,
97
+ `Tools ${stats.toolCalls} total (${stats.readOnlyToolCalls} read-only, ${stats.unsafeToolCalls} unsafe)`,
98
+ `Tool bytes ${(0, format_1.formatBytes)(stats.modelVisibleToolBytes)} model-visible / ${(0, format_1.formatBytes)(stats.toolResultBytes)} total`,
99
+ ];
100
+ if (stats.summarizedBytes > 0) {
101
+ lines.push(`Saved ${(0, format_1.formatBytes)(stats.summarizedBytes)} from model context`);
102
+ }
103
+ if (stats.compactTrigger) {
104
+ lines.push(`Compact ${stats.compactTrigger}`);
105
+ }
106
+ if (stats.localFastPathUsed) {
107
+ lines.push('Fast path yes');
108
+ }
109
+ if (stats.budgetExceededReason) {
110
+ lines.push(`Budget ${stats.budgetExceededReason}`);
111
+ }
112
+ if (stats.continuationActions && stats.continuationActions.length > 0) {
113
+ lines.push(`Next ${stats.continuationActions.join(', ')}`);
114
+ }
115
+ if ((stats.providerRetryCount ?? 0) > 0) {
116
+ const retryParts = [
117
+ `${stats.providerRetryCount} retries`,
118
+ `delay ${formatDurationMs(stats.providerRetryDelayMs ?? 0)}`,
119
+ stats.providerLastRetryErrorType
120
+ ? `last ${stats.providerLastRetryErrorType}${stats.providerLastRetryStatus ? `/${stats.providerLastRetryStatus}` : ''}`
121
+ : undefined,
122
+ ].filter(Boolean);
123
+ lines.push(`Provider ${retryParts.join(', ')}`);
124
+ }
125
+ if ((stats.providerFallbackCount ?? 0) > 0 || stats.providerUsingFallback) {
126
+ const fallbackPath = stats.providerFallbackFromModel && stats.providerFallbackToModel
127
+ ? `${stats.providerFallbackFromModel} -> ${stats.providerFallbackToModel}`
128
+ : stats.providerFinalModel
129
+ ? `final ${stats.providerFinalModel}`
130
+ : 'active';
131
+ lines.push(`Fallback ${fallbackPath}`);
132
+ }
133
+ if (typeof stats.verificationClaimAllowed === 'boolean') {
134
+ const verificationParts = [
135
+ stats.verificationProfile ?? 'unknown',
136
+ `required=${stats.verificationRequired ? 'yes' : 'no'}`,
137
+ `passed=${stats.verificationPassedCommands?.length ?? 0}`,
138
+ `failed=${stats.verificationFailedCommands?.length ?? 0}`,
139
+ `missing=${stats.verificationMissingCommands?.length ?? 0}`,
140
+ `claim=${stats.verificationClaimAllowed ? 'yes' : 'no'}`,
141
+ ];
142
+ lines.push(`Verify ${verificationParts.join(' ')}`);
143
+ }
144
+ if (typeof stats.loopBudgetMaxLlmRequests === 'number'
145
+ || typeof stats.loopBudgetMaxToolCalls === 'number'
146
+ || typeof stats.loopBudgetMaxModelVisibleBytes === 'number') {
147
+ const caps = [
148
+ typeof stats.loopBudgetMaxLlmRequests === 'number'
149
+ ? `${stats.llmRequests}/${stats.loopBudgetMaxLlmRequests} LLM`
150
+ : undefined,
151
+ typeof stats.loopBudgetMaxToolCalls === 'number'
152
+ ? `${stats.toolCalls}/${stats.loopBudgetMaxToolCalls} tools`
153
+ : undefined,
154
+ typeof stats.loopBudgetMaxModelVisibleBytes === 'number'
155
+ ? `${(0, format_1.formatBytes)(stats.modelVisibleToolBytes)}/${(0, format_1.formatBytes)(stats.loopBudgetMaxModelVisibleBytes)} visible`
156
+ : undefined,
157
+ typeof stats.loopBudgetMaxReadOnlyFragmentation === 'number'
158
+ ? `fragment ${stats.singleReadOnlyStreak}/${stats.loopBudgetMaxReadOnlyFragmentation}`
159
+ : undefined,
160
+ ].filter(Boolean);
161
+ lines.push(`Budget cap ${caps.join(', ')} (${formatLoopBudgetSource(stats)})`);
162
+ }
163
+ if (stats.singleReadOnlyStreak > 0 || stats.batchReadSuggestionCount > 0) {
164
+ lines.push(`Read-only streak ${stats.singleReadOnlyStreak}, batch_read hints ${stats.batchReadSuggestionCount}`);
165
+ }
166
+ if (detail) {
167
+ lines.push(`Unsafe ${stats.unsafeToolCalls}`);
168
+ if (stats.providerRetryErrorTypes && stats.providerRetryErrorTypes.length > 0) {
169
+ lines.push(`Retry type ${stats.providerRetryErrorTypes.join(', ')}`);
170
+ }
171
+ if (stats.verificationFailedCommands && stats.verificationFailedCommands.length > 0) {
172
+ lines.push(`Failed ${stats.verificationFailedCommands.join(' && ')}`);
173
+ }
174
+ if (stats.verificationMissingCommands && stats.verificationMissingCommands.length > 0) {
175
+ lines.push(`Missing ${stats.verificationMissingCommands.join(' && ')}`);
176
+ }
177
+ if (stats.verificationSkippedReason) {
178
+ lines.push(`Verify why ${stats.verificationSkippedReason}`);
179
+ }
180
+ if (stats.continuationHint) {
181
+ lines.push(`Next why ${stats.continuationHint}`);
182
+ }
183
+ lines.push(`Budget hit ${stats.finishReason === 'budget_exceeded' ? 'yes' : 'no'}`);
184
+ }
185
+ return lines;
186
+ }
78
187
  const CATEGORY_LABELS = {
79
188
  workflow: 'Workflow',
80
189
  session: 'Session',
@@ -203,15 +312,8 @@ function showStatus(ctx) {
203
312
  const stats = snapshot.lastLoopStats;
204
313
  console.log();
205
314
  console.log(` Last loop:`);
206
- console.log(` Finish ${stats.finishReason}`);
207
- console.log(` Requests ${stats.llmRequests} LLM / ${stats.turnsStarted} turns`);
208
- console.log(` Tools ${stats.toolCalls} total (${stats.readOnlyToolCalls} read-only, ${stats.unsafeToolCalls} unsafe)`);
209
- console.log(` Tool bytes ${(0, format_1.formatBytes)(stats.modelVisibleToolBytes)} model-visible / ${(0, format_1.formatBytes)(stats.toolResultBytes)} total`);
210
- if (stats.summarizedBytes > 0) {
211
- console.log(` Saved ${(0, format_1.formatBytes)(stats.summarizedBytes)} from model context`);
212
- }
213
- if (stats.compactTrigger) {
214
- console.log(` Compact ${stats.compactTrigger}`);
315
+ for (const line of formatLoopStatsLines(stats)) {
316
+ console.log(` ${line}`);
215
317
  }
216
318
  }
217
319
  const harnessState = snapshot.harnessState;
@@ -232,6 +334,413 @@ function showStatus(ctx) {
232
334
  console.log();
233
335
  return { success: true };
234
336
  }
337
+ function handleLoopStats(ctx) {
338
+ const stats = ctx.store.getSnapshot().lastLoopStats;
339
+ if (!stats) {
340
+ console.log('No agent-loop stats recorded yet.');
341
+ return { success: true };
342
+ }
343
+ console.log(HEADER('Agent Loop Stats'));
344
+ console.log();
345
+ for (const line of formatLoopStatsLines(stats, true)) {
346
+ console.log(` ${line}`);
347
+ }
348
+ console.log();
349
+ console.log(DIM('Use this to spot excessive LLM requests, fragmented read-only tool calls, and local fast-path hits.'));
350
+ return { success: true };
351
+ }
352
+ function formatTraceEventLine(event) {
353
+ const time = new Date(event.timestamp).toLocaleTimeString();
354
+ const prefix = `${DIM(time)} ${ACCENT(event.type)}`;
355
+ switch (event.type) {
356
+ case 'turn_start':
357
+ return `${prefix} input=${(0, format_1.formatBytes)(event.inputBytes ?? 0)}${event.localFastPathUsed ? ' fast-path' : ''}${event.note ? ` ${DIM(event.note)}` : ''}`;
358
+ case 'request_start':
359
+ return `${prefix} model=${event.model ?? 'unknown'} iteration=${event.turn ?? '?'}`;
360
+ case 'provider_retry': {
361
+ const parts = [
362
+ `count=${event.providerRetryCount ?? 0}`,
363
+ `delay=${formatDurationMs(event.providerRetryDelayMs ?? 0)}`,
364
+ event.providerLastRetryErrorType
365
+ ? `last=${event.providerLastRetryErrorType}${event.providerLastRetryStatus ? `/${event.providerLastRetryStatus}` : ''}`
366
+ : '',
367
+ event.providerRetryErrorTypes?.length ? `types=${event.providerRetryErrorTypes.join(',')}` : '',
368
+ event.providerFinalModel ? `final=${event.providerFinalModel}` : '',
369
+ ].filter(Boolean);
370
+ return `${prefix} ${parts.join(' ')}`;
371
+ }
372
+ case 'provider_fallback': {
373
+ const path = event.providerFallbackFromModel && event.providerFallbackToModel
374
+ ? `${event.providerFallbackFromModel}->${event.providerFallbackToModel}`
375
+ : event.providerFinalModel ?? 'active';
376
+ const parts = [
377
+ `count=${event.providerFallbackCount ?? 0}`,
378
+ `path=${path}`,
379
+ `using=${event.providerUsingFallback ? 'yes' : 'no'}`,
380
+ ];
381
+ return `${prefix} ${parts.join(' ')}`;
382
+ }
383
+ case 'prompt_assembly': {
384
+ const parts = [
385
+ `model=${event.promptModelId ?? 'unknown'}`,
386
+ `tokens=${event.promptEstimatedTokens ?? 0}/${event.promptBudgetTokens ?? 0}`,
387
+ `core=${event.promptCoreTokens ?? 0}`,
388
+ `evidenceBudget=${event.promptEvidenceBudgetTokens ?? 0}`,
389
+ `recentBudget=${event.promptRecentTurnBudgetTokens ?? 0}`,
390
+ `included=${event.promptIncludedEvidenceCount ?? 0}`,
391
+ `omitted=${event.promptOmittedEvidenceCount ?? 0}`,
392
+ ];
393
+ const sections = event.promptSections?.length
394
+ ? ` sections=${event.promptSections.join(',')}`
395
+ : '';
396
+ const evidence = event.promptIncludedEvidence?.length
397
+ ? ` evidence=${event.promptIncludedEvidence.slice(0, 6).join(', ')}${event.promptIncludedEvidence.length > 6 ? ', ...' : ''}`
398
+ : '';
399
+ return `${prefix} ${parts.join(' ')}${sections ? DIM(sections) : ''}${evidence ? ` ${DIM(evidence)}` : ''}`;
400
+ }
401
+ case 'assistant_tool_calls':
402
+ return `${prefix} calls=${event.toolCallCount ?? 0} assistant=${(0, format_1.formatBytes)(event.contentBytes ?? 0)}`;
403
+ case 'checkpoint': {
404
+ const files = event.checkpointFiles?.length
405
+ ? ` files=${event.checkpointFiles.slice(0, 6).join(', ')}${event.checkpointFiles.length > 6 ? ', ...' : ''}`
406
+ : '';
407
+ const targets = event.workspaceFiles?.length && !files
408
+ ? ` targets=${event.workspaceFiles.slice(0, 6).join(', ')}${event.workspaceFiles.length > 6 ? ', ...' : ''}`
409
+ : '';
410
+ return `${prefix} id=${event.checkpointId ?? 'unknown'} saved=${event.checkpointFileCount ?? 0}${files}${targets}${event.note ? ` ${DIM(event.note)}` : ''}`;
411
+ }
412
+ case 'tool_call':
413
+ return `${prefix} ${event.name ?? 'tool'}${event.argsSummary ? ` ${DIM(event.argsSummary)}` : ''}${event.argsArtifactId ? ` ${DIM(`args=/artifacts show ${event.argsArtifactId} --full${event.argsBytes ? ` (${(0, format_1.formatBytes)(event.argsBytes)})` : ''}`)}` : ''}${event.callId ? ` ${DIM(event.callId)}` : ''}`;
414
+ case 'permission_decision': {
415
+ const status = event.permissionApproved ? SUCCESS('approved') : ERROR('denied');
416
+ const parts = [
417
+ `source=${event.permissionSource ?? 'unknown'}`,
418
+ `behavior=${event.permissionBehavior ?? 'unknown'}`,
419
+ ];
420
+ if (typeof event.permissionDuration === 'number') {
421
+ parts.push(`${event.permissionDuration}ms`);
422
+ }
423
+ return `${prefix} ${status} ${event.name ?? 'tool'} ${DIM(parts.join(' '))}${event.permissionReason ? ` ${DIM(event.permissionReason)}` : ''}`;
424
+ }
425
+ case 'tool_result': {
426
+ const status = event.success === false ? ERROR('error') : SUCCESS('ok');
427
+ const bytes = [
428
+ `output=${(0, format_1.formatBytes)(event.outputBytes ?? 0)}`,
429
+ `model=${(0, format_1.formatBytes)(event.modelVisibleBytes ?? 0)}`,
430
+ ];
431
+ if (event.artifactId)
432
+ bytes.push(`artifact=${event.artifactId}`);
433
+ return `${prefix} ${status} ${event.name ?? 'tool'} ${DIM(`${event.duration ?? 0}ms`)} ${DIM(bytes.join(' '))}${event.error ? ` ${ERROR(event.error)}` : ''}`;
434
+ }
435
+ case 'message':
436
+ return `${prefix} assistant=${(0, format_1.formatBytes)(event.contentBytes ?? 0)}`;
437
+ case 'strategy_exhausted':
438
+ return `${prefix}${event.note ? ` ${WARN(event.note)}` : ''}`;
439
+ case 'complete': {
440
+ const stats = [
441
+ `finish=${event.finishReason ?? 'unknown'}`,
442
+ `llm=${typeof event.llmRequests === 'number' ? event.llmRequests : 'unknown'}`,
443
+ `tools=${typeof event.toolCalls === 'number' ? event.toolCalls : 'unknown'}`,
444
+ ];
445
+ if (event.loopBudgetSource
446
+ || typeof event.loopBudgetMaxLlmRequests === 'number'
447
+ || typeof event.loopBudgetMaxToolCalls === 'number'
448
+ || typeof event.loopBudgetMaxModelVisibleBytes === 'number') {
449
+ const source = event.loopBudgetSource === 'config' && event.loopBudgetBaseProfile
450
+ ? `config/${event.loopBudgetBaseProfile}`
451
+ : event.loopBudgetSource ?? 'unknown';
452
+ const caps = [
453
+ typeof event.loopBudgetMaxLlmRequests === 'number'
454
+ ? `${typeof event.llmRequests === 'number' ? event.llmRequests : '?'}/${event.loopBudgetMaxLlmRequests}llm`
455
+ : undefined,
456
+ typeof event.loopBudgetMaxToolCalls === 'number'
457
+ ? `${typeof event.toolCalls === 'number' ? event.toolCalls : '?'}/${event.loopBudgetMaxToolCalls}tools`
458
+ : undefined,
459
+ typeof event.loopBudgetMaxModelVisibleBytes === 'number'
460
+ ? `${(0, format_1.formatBytes)(event.loopBudgetMaxModelVisibleBytes)}visible`
461
+ : undefined,
462
+ typeof event.loopBudgetMaxReadOnlyFragmentation === 'number'
463
+ ? `frag=${event.loopBudgetMaxReadOnlyFragmentation}`
464
+ : undefined,
465
+ event.loopBudgetConfigOverride ? 'override=yes' : undefined,
466
+ ].filter(Boolean);
467
+ stats.push(`budgetProfile=${source}${caps.length ? `(${caps.join(',')})` : ''}`);
468
+ }
469
+ if (event.budgetExceededReason)
470
+ stats.push(`budget=${event.budgetExceededReason}`);
471
+ if (event.continuationActions?.length)
472
+ stats.push(`next=${event.continuationActions.join(',')}`);
473
+ if (event.continuationHint)
474
+ stats.push(`hint=${event.continuationHint}`);
475
+ if (event.localFastPathUsed)
476
+ stats.push('fast-path=yes');
477
+ return `${prefix} ${stats.join(' ')}`;
478
+ }
479
+ case 'local_fast_path':
480
+ return `${prefix} ${event.name ?? 'tool'}${event.argsSummary ? ` ${DIM(event.argsSummary)}` : ''}${event.argsArtifactId ? ` ${DIM(`args=/artifacts show ${event.argsArtifactId} --full${event.argsBytes ? ` (${(0, format_1.formatBytes)(event.argsBytes)})` : ''}`)}` : ''}${event.note ? ` ${DIM(event.note)}` : ''}`;
481
+ case 'workspace_snapshot': {
482
+ const state = event.workspaceGitAvailable === false
483
+ ? WARN('not-git')
484
+ : event.workspaceDirty ? WARN('dirty') : SUCCESS('clean');
485
+ const files = event.workspaceFiles?.length
486
+ ? ` files=${event.workspaceFiles.slice(0, 6).join(', ')}${event.workspaceFiles.length > 6 ? ', ...' : ''}`
487
+ : '';
488
+ return `${prefix} ${event.workspacePhase ?? 'unknown'} ${state} count=${event.workspaceFileCount ?? 0}${event.workspaceBranch ? ` branch=${event.workspaceBranch}` : ''}${files}${event.error ? ` ${ERROR(event.error)}` : ''}`;
489
+ }
490
+ case 'workspace_delta': {
491
+ const added = event.workspaceNewByTurn ?? [];
492
+ const changed = event.workspaceChangedByTurn ?? [];
493
+ const modifiedPreExisting = event.workspaceModifiedPreExistingByTurn ?? [];
494
+ const resolved = event.workspaceResolvedByTurn ?? [];
495
+ const parts = [
496
+ `after=${event.workspaceFileCount ?? 0}`,
497
+ `new=${added.length}`,
498
+ `changed=${changed.length}`,
499
+ `pre-existing-modified=${modifiedPreExisting.length}`,
500
+ `resolved=${resolved.length}`,
501
+ ];
502
+ const details = [
503
+ added.length ? `new: ${added.slice(0, 6).join(', ')}${added.length > 6 ? ', ...' : ''}` : '',
504
+ changed.length ? `changed: ${changed.slice(0, 6).join(', ')}${changed.length > 6 ? ', ...' : ''}` : '',
505
+ modifiedPreExisting.length ? `pre-existing modified: ${modifiedPreExisting.slice(0, 6).join(', ')}${modifiedPreExisting.length > 6 ? ', ...' : ''}` : '',
506
+ resolved.length ? `resolved: ${resolved.slice(0, 6).join(', ')}${resolved.length > 6 ? ', ...' : ''}` : '',
507
+ ].filter(Boolean).join(' | ');
508
+ return `${prefix} ${parts.join(' ')}${details ? ` ${DIM(details)}` : ''}${event.note ? ` ${DIM(event.note)}` : ''}`;
509
+ }
510
+ case 'verification_profile': {
511
+ const commands = event.verificationCommands ?? [];
512
+ const files = event.verificationChangedFiles ?? [];
513
+ const details = [
514
+ `profile=${event.verificationProfile ?? 'unknown'}`,
515
+ `required=${event.verificationRequired === false ? 'no' : 'yes'}`,
516
+ `commands=${commands.length}`,
517
+ `files=${files.length}`,
518
+ ];
519
+ const commandPreview = commands.length
520
+ ? ` cmds: ${commands.slice(0, 4).join(' && ')}${commands.length > 4 ? ' && ...' : ''}`
521
+ : '';
522
+ return `${prefix} ${details.join(' ')}${commandPreview ? ` ${DIM(commandPreview)}` : ''}${event.note ? ` ${DIM(event.note)}` : ''}`;
523
+ }
524
+ case 'verification_result': {
525
+ const status = event.verificationPassed === true ? SUCCESS('passed') : ERROR('failed');
526
+ const command = event.verificationCommand ?? 'unknown';
527
+ return `${prefix} ${status} ${command}${typeof event.outputBytes === 'number' ? ` ${DIM((0, format_1.formatBytes)(event.outputBytes))}` : ''}${event.error ? ` ${ERROR(event.error)}` : ''}`;
528
+ }
529
+ case 'verification_summary': {
530
+ const passed = event.verificationPassedCommands?.length ?? 0;
531
+ const failed = event.verificationFailedCommands?.length ?? 0;
532
+ const missing = event.verificationMissingCommands?.length ?? 0;
533
+ const parts = [
534
+ `profile=${event.verificationProfile ?? 'unknown'}`,
535
+ `required=${event.verificationRequired === false ? 'no' : 'yes'}`,
536
+ `passed=${passed}`,
537
+ `failed=${failed}`,
538
+ `missing=${missing}`,
539
+ `claimAllowed=${event.verificationClaimAllowed ? 'yes' : 'no'}`,
540
+ ];
541
+ const missingPreview = missing > 0
542
+ ? ` missing: ${(event.verificationMissingCommands ?? []).slice(0, 4).join(' && ')}${missing > 4 ? ' && ...' : ''}`
543
+ : '';
544
+ return `${prefix} ${parts.join(' ')}${missingPreview ? ` ${DIM(missingPreview)}` : ''}${event.note ? ` ${DIM(event.note)}` : ''}`;
545
+ }
546
+ case 'aborted':
547
+ return `${prefix}${event.note ? ` ${WARN(event.note)}` : ''}`;
548
+ case 'error':
549
+ return `${prefix} ${ERROR(event.error ?? 'unknown error')}`;
550
+ default:
551
+ return prefix;
552
+ }
553
+ }
554
+ function handleTrace(ctx, args = '') {
555
+ const session = ctx.getSession?.() ?? (ctx.sessionId ? (0, session_storage_1.loadSessionMeta)(ctx.sessionId) : null);
556
+ if (!session) {
557
+ console.log(ERROR('No active session.'));
558
+ console.log(DIM('Use /resume <session-id> first, or start a chat turn to create a session.'));
559
+ return { success: false };
560
+ }
561
+ const events = (0, session_storage_1.readSessionTraceEvents)(session.id);
562
+ if (events.length === 0) {
563
+ console.log(DIM(`No trace events recorded for session ${session.id.slice(0, 8)} yet.`));
564
+ return { success: true };
565
+ }
566
+ const ref = args.trim();
567
+ const requestedTurnId = ref && ref !== 'latest' ? ref : events[events.length - 1].turnId;
568
+ const turnEvents = events.filter(event => event.turnId === requestedTurnId);
569
+ if (turnEvents.length === 0) {
570
+ const recentTurnIds = [...new Set(events.map(event => event.turnId))].slice(-8).reverse();
571
+ console.log(ERROR(`Trace turn not found: ${requestedTurnId}`));
572
+ console.log(DIM(`Recent turns: ${recentTurnIds.join(', ')}`));
573
+ return { success: false };
574
+ }
575
+ console.log(HEADER(`Trace ${requestedTurnId}`));
576
+ console.log(DIM(`Session ${session.id} Events ${turnEvents.length}`));
577
+ console.log(DIM('─'.repeat(40)));
578
+ for (const event of turnEvents) {
579
+ console.log(` ${formatTraceEventLine(event)}`);
580
+ }
581
+ console.log();
582
+ console.log(DIM('Trace stores metadata only; full transcript and tool output stay in session/artifacts.'));
583
+ return { success: true };
584
+ }
585
+ function formatDateTime(timestamp) {
586
+ return new Date(timestamp).toLocaleString();
587
+ }
588
+ function parseArtifactArgs(args) {
589
+ const parts = args.trim().split(/\s+/).filter(Boolean);
590
+ let full = false;
591
+ let limit = 20;
592
+ const positional = [];
593
+ for (let i = 0; i < parts.length; i++) {
594
+ const part = parts[i];
595
+ if (part === '--full') {
596
+ full = true;
597
+ continue;
598
+ }
599
+ if ((part === '--limit' || part === '-n') && parts[i + 1]) {
600
+ const parsed = Number(parts[i + 1]);
601
+ if (Number.isInteger(parsed) && parsed > 0) {
602
+ limit = Math.min(parsed, 200);
603
+ }
604
+ i++;
605
+ continue;
606
+ }
607
+ positional.push(part);
608
+ }
609
+ if (positional[0] === 'show' || positional[0] === 'cat') {
610
+ return { action: 'show', ref: positional[1], full, limit };
611
+ }
612
+ if (positional[0]) {
613
+ return { action: 'show', ref: positional[0], full, limit };
614
+ }
615
+ return { action: 'list', full, limit };
616
+ }
617
+ function printArtifactPreview(content, full) {
618
+ const maxPreviewBytes = 16 * 1024;
619
+ if (full || Buffer.byteLength(content, 'utf8') <= maxPreviewBytes) {
620
+ console.log(content);
621
+ return;
622
+ }
623
+ const preview = Buffer.from(content, 'utf8').subarray(0, maxPreviewBytes).toString('utf8');
624
+ console.log(preview);
625
+ console.log();
626
+ console.log(DIM(`... preview truncated at ${(0, format_1.formatBytes)(maxPreviewBytes)}. Use /artifacts show <id> --full for full output.`));
627
+ }
628
+ function formatArtifactPathForDisplay(artifactPath) {
629
+ return (0, session_storage_1.redactTraceText)(artifactPath);
630
+ }
631
+ function handleArtifacts(ctx, args = '') {
632
+ const parsed = parseArtifactArgs(args);
633
+ if (parsed.action === 'list') {
634
+ const artifacts = (0, tool_artifacts_1.listArtifacts)(ctx.cwd, parsed.limit);
635
+ console.log(HEADER('Artifacts'));
636
+ console.log(DIM('─'.repeat(40)));
637
+ if (artifacts.length === 0) {
638
+ console.log(DIM('No artifacts found for this project.'));
639
+ return { success: true };
640
+ }
641
+ for (const artifact of artifacts) {
642
+ console.log(`${ACCENT(artifact.id)} ${DIM(artifact.toolName)} ${(0, format_1.formatBytes)(artifact.outputBytes)}`);
643
+ console.log(` ${DIM(formatDateTime(artifact.modifiedAt))}`);
644
+ console.log(` ${DIM(formatArtifactPathForDisplay(artifact.path))}`);
645
+ }
646
+ console.log();
647
+ console.log(DIM('Use /artifacts show <id|prefix> to preview, or add --full for full output.'));
648
+ return { success: true };
649
+ }
650
+ if (!parsed.ref) {
651
+ console.log(ERROR('Usage: /artifacts show <id|prefix> [--full]'));
652
+ return { success: false };
653
+ }
654
+ const artifact = (0, tool_artifacts_1.findArtifact)(ctx.cwd, parsed.ref);
655
+ if (!artifact) {
656
+ console.log(ERROR(`Artifact not found or ambiguous: ${parsed.ref}`));
657
+ console.log(DIM('Run /artifacts to list available artifact ids.'));
658
+ return { success: false };
659
+ }
660
+ const content = (0, tool_artifacts_1.retrieveArtifact)(artifact.path);
661
+ if (content == null) {
662
+ console.log(ERROR(`Artifact exists but cannot be read: ${artifact.id}`));
663
+ console.log(DIM(formatArtifactPathForDisplay(artifact.path)));
664
+ return { success: false };
665
+ }
666
+ console.log(HEADER(`Artifact ${artifact.id}`));
667
+ console.log(DIM(`Tool ${artifact.toolName} Size ${(0, format_1.formatBytes)(artifact.outputBytes)} Modified ${formatDateTime(artifact.modifiedAt)}`));
668
+ console.log(DIM(formatArtifactPathForDisplay(artifact.path)));
669
+ console.log(DIM('─'.repeat(40)));
670
+ printArtifactPreview(content, parsed.full);
671
+ return { success: true };
672
+ }
673
+ function findCheckpointByRef(ctx, ref) {
674
+ const checkpoints = (0, checkpoint_1.listCheckpoints)(ctx.cwd);
675
+ const exact = checkpoints.find(checkpoint => checkpoint.turnId === ref);
676
+ if (exact)
677
+ return { checkpoint: exact, ambiguous: false };
678
+ const matches = checkpoints.filter(checkpoint => checkpoint.turnId.startsWith(ref));
679
+ return {
680
+ checkpoint: matches.length === 1 ? matches[0] : undefined,
681
+ ambiguous: matches.length > 1,
682
+ };
683
+ }
684
+ function handleCheckpoint(ctx, args = '') {
685
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
686
+ const action = tokens[0] ?? 'list';
687
+ if (action === 'list') {
688
+ const checkpoints = (0, checkpoint_1.listCheckpoints)(ctx.cwd);
689
+ console.log(HEADER('Checkpoints'));
690
+ console.log(DIM('─'.repeat(40)));
691
+ if (checkpoints.length === 0) {
692
+ console.log(DIM('No checkpoints found for this project.'));
693
+ return { success: true };
694
+ }
695
+ for (const checkpoint of checkpoints.slice(0, 20)) {
696
+ console.log(`${ACCENT(checkpoint.turnId)} ${DIM(formatDateTime(checkpoint.createdAt))}`);
697
+ const files = checkpoint.files.map(file => file.path).slice(0, 8).join(', ');
698
+ console.log(` ${DIM(`${checkpoint.files.length} file(s)${files ? `: ${files}` : ''}`)}`);
699
+ }
700
+ console.log();
701
+ console.log(DIM('Use /checkpoint restore <turn-id|prefix> to preview, then add --yes to restore.'));
702
+ return { success: true };
703
+ }
704
+ if (action !== 'restore') {
705
+ console.log(ERROR('Usage: /checkpoint [list|restore <turn-id|prefix> [--yes]]'));
706
+ return { success: false };
707
+ }
708
+ const ref = tokens.find(token => token !== 'restore' && token !== '--yes');
709
+ const confirmed = tokens.includes('--yes');
710
+ if (!ref) {
711
+ console.log(ERROR('Usage: /checkpoint restore <turn-id|prefix> [--yes]'));
712
+ return { success: false };
713
+ }
714
+ const { checkpoint, ambiguous } = findCheckpointByRef(ctx, ref);
715
+ if (!checkpoint) {
716
+ console.log(ERROR(ambiguous
717
+ ? `Checkpoint prefix is ambiguous: ${ref}`
718
+ : `Checkpoint not found: ${ref}`));
719
+ console.log(DIM('Run /checkpoint to list available checkpoint ids.'));
720
+ return { success: false };
721
+ }
722
+ if (!confirmed) {
723
+ console.log(HEADER(`Checkpoint ${checkpoint.turnId}`));
724
+ console.log(DIM(`${formatDateTime(checkpoint.createdAt)} ${checkpoint.files.length} file(s)`));
725
+ for (const file of checkpoint.files.slice(0, 20)) {
726
+ console.log(` ${file.path} ${DIM((0, format_1.formatBytes)(file.sizeBytes))}`);
727
+ }
728
+ console.log();
729
+ console.log(WARN(`This will overwrite current files from checkpoint ${checkpoint.turnId}.`));
730
+ console.log(DIM(`Run /checkpoint restore ${checkpoint.turnId} --yes to restore.`));
731
+ return { success: true };
732
+ }
733
+ const result = (0, checkpoint_1.restoreCheckpoint)(ctx.cwd, checkpoint.turnId);
734
+ if (result.error) {
735
+ console.log(ERROR(result.error));
736
+ return { success: false, error: result.error };
737
+ }
738
+ console.log(SUCCESS(`Restored ${result.restored.length} file(s) from checkpoint ${checkpoint.turnId}.`));
739
+ for (const file of result.restored.slice(0, 20)) {
740
+ console.log(` ${file}`);
741
+ }
742
+ return { success: true, output: result.restored.join('\n') };
743
+ }
235
744
  function snapshotCurrentModel(ctx) {
236
745
  return ctx.store.getSnapshot().currentModel || ctx.config.model;
237
746
  }
@@ -917,6 +1426,7 @@ async function handleChat(ctx, input) {
917
1426
  abortSignal: ctx.abortSignal,
918
1427
  harness,
919
1428
  input,
1429
+ loopBudget: (0, loop_budget_1.resolveRuntimeLoopBudget)(input, ctx.config, harness.toJSON()),
920
1430
  })) {
921
1431
  switch (event.type) {
922
1432
  case 'request_start':
@@ -1398,7 +1908,7 @@ async function handleCompact(ctx, args) {
1398
1908
  const percent = Math.round((reduction / history.length) * 100);
1399
1909
  const sessionId = ctx.getSession?.()?.id ?? ctx.sessionId;
1400
1910
  if (sessionId) {
1401
- (0, session_storage_1.markSessionTranscriptDisplayStart)(sessionId);
1911
+ (0, session_storage_1.persistSessionCompactHistory)(sessionId, compacted);
1402
1912
  }
1403
1913
  console.log(SUCCESS(`✔ Compacted ${history.length} → ${compacted.length} messages`));
1404
1914
  console.log(DIM(` Reduced by ${reduction} messages (${percent}%)`));
@@ -1676,16 +2186,32 @@ function restoreSession(ctx, session, isLast) {
1676
2186
  const history = (0, session_storage_1.loadSessionHistory)(resumed.id);
1677
2187
  if (history.length > 0) {
1678
2188
  const summary = generateHistorySummary(history);
2189
+ const eventSummary = generateRestoredSessionEventSummary(history);
1679
2190
  console.log(DIM(` Summary: ${summary}`));
1680
2191
  console.log();
1681
2192
  ctx.store.setState({ conversationHistory: history });
1682
2193
  ctx.store.setState({ harnessState: (0, session_storage_1.loadSessionHarnessState)(resumed.id) ?? resumed.harnessState });
1683
2194
  (0, framework_1.resetToolState)();
2195
+ ctx.sessionRestored?.({
2196
+ sessionId: resumed.id,
2197
+ projectPath: resumed.projectPath,
2198
+ model: resumed.model,
2199
+ restoredMessages: history.length,
2200
+ messageCount: resumed.messageCount,
2201
+ summary: eventSummary,
2202
+ });
1684
2203
  console.log(SUCCESS(`✔ Restored ${history.length} messages`));
1685
2204
  }
1686
2205
  else {
1687
2206
  console.log();
1688
2207
  console.log(DIM(' No messages in session'));
2208
+ ctx.sessionRestored?.({
2209
+ sessionId: resumed.id,
2210
+ projectPath: resumed.projectPath,
2211
+ model: resumed.model,
2212
+ restoredMessages: 0,
2213
+ messageCount: resumed.messageCount,
2214
+ });
1689
2215
  }
1690
2216
  console.log();
1691
2217
  return { success: true };
@@ -1809,13 +2335,26 @@ async function handleEditPreview(ctx) {
1809
2335
  return { success: parsed.success === true };
1810
2336
  }
1811
2337
  /** Generate a brief summary of conversation history */
2338
+ function truncateRedactedSummary(text, maxLength) {
2339
+ const redacted = (0, session_storage_1.redactTraceText)(text);
2340
+ if (redacted.length <= maxLength)
2341
+ return redacted;
2342
+ let truncated = redacted.slice(0, maxLength);
2343
+ for (const marker of ['[REDACTED_SECRET]']) {
2344
+ const markerStart = truncated.indexOf(marker.slice(0, 6));
2345
+ if (markerStart >= 0 && !truncated.includes(marker)) {
2346
+ truncated = `${truncated.slice(0, markerStart)}${marker}`;
2347
+ break;
2348
+ }
2349
+ }
2350
+ return `${truncated}...`;
2351
+ }
1812
2352
  function generateHistorySummary(messages) {
1813
2353
  const userMsgs = messages.filter(m => m.role === 'user' && m.content);
1814
2354
  const assistantMsgsWithTools = messages.filter(m => m.role === 'assistant' && m.tool_calls && m.tool_calls.length > 0);
1815
2355
  // Extract topics from first few user messages
1816
2356
  const topics = userMsgs.slice(0, 3).map(m => {
1817
- const content = m.content || '';
1818
- return content.length > 40 ? content.slice(0, 40) + '...' : content;
2357
+ return truncateRedactedSummary(m.content || '', 40);
1819
2358
  });
1820
2359
  // Extract tools used
1821
2360
  const toolsUsed = assistantMsgsWithTools.flatMap(m => m.tool_calls?.map(tc => tc.function.name) || []);
@@ -1833,6 +2372,14 @@ function generateHistorySummary(messages) {
1833
2372
  }
1834
2373
  return parts.join('. ');
1835
2374
  }
2375
+ function generateRestoredSessionEventSummary(messages) {
2376
+ const assistantMsgsWithTools = messages.filter(m => m.role === 'assistant' && m.tool_calls && m.tool_calls.length > 0);
2377
+ const toolsUsed = assistantMsgsWithTools.flatMap(m => m.tool_calls?.map(tc => tc.function.name) || []);
2378
+ const uniqueTools = [...new Set(toolsUsed)].slice(0, 8);
2379
+ if (uniqueTools.length === 0)
2380
+ return undefined;
2381
+ return `Tools: ${uniqueTools.join(', ')}`;
2382
+ }
1836
2383
  function continueAsSlashChat(name, args) {
1837
2384
  const trimmed = args.trim();
1838
2385
  return {
@@ -2115,6 +2662,44 @@ const COMMANDS = [
2115
2662
  type: 'builtin',
2116
2663
  execute: (ctx) => handleUsage(ctx),
2117
2664
  },
2665
+ {
2666
+ name: 'loop-stats',
2667
+ aliases: ['loop'],
2668
+ description: 'Show detailed agent-loop budget and efficiency diagnostics',
2669
+ category: 'diagnostics',
2670
+ priority: 12,
2671
+ type: 'builtin',
2672
+ execute: (ctx) => handleLoopStats(ctx),
2673
+ },
2674
+ {
2675
+ name: 'trace',
2676
+ description: 'Show structured event timeline for the latest or selected turn',
2677
+ argumentHint: '[latest|turn-id]',
2678
+ category: 'diagnostics',
2679
+ priority: 14,
2680
+ type: 'builtin',
2681
+ execute: (ctx, args) => handleTrace(ctx, args),
2682
+ },
2683
+ {
2684
+ name: 'artifacts',
2685
+ aliases: ['artifact'],
2686
+ description: 'List or inspect saved full tool outputs for this project',
2687
+ argumentHint: '[show <id|prefix> --full]',
2688
+ category: 'diagnostics',
2689
+ priority: 16,
2690
+ type: 'builtin',
2691
+ execute: (ctx, args) => handleArtifacts(ctx, args),
2692
+ },
2693
+ {
2694
+ name: 'checkpoint',
2695
+ aliases: ['checkpoints'],
2696
+ description: 'List or restore file checkpoints created before agent edits',
2697
+ argumentHint: '[list|restore <turn-id|prefix> --yes]',
2698
+ category: 'diagnostics',
2699
+ priority: 18,
2700
+ type: 'builtin',
2701
+ execute: (ctx, args) => handleCheckpoint(ctx, args),
2702
+ },
2118
2703
  {
2119
2704
  name: 'cost',
2120
2705
  description: 'Show session token usage',