davinci-resolve-mcp 2.205.2 → 2.207.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,109 @@
2
2
 
3
3
  Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
4
4
 
5
+ ## What's New in v2.207.0 — execution audit report exports
6
+
7
+ Contributed in PR #185.
8
+
9
+ ### Added
10
+
11
+ - **Execution traces can now be exported as reviewable audit reports.**
12
+ `resolve_control(action="export_execution_report")` writes the latest trace,
13
+ or a named `execution_id`, as Markdown or JSON. The report carries the
14
+ request, status, start/end timestamps, duration, tool summary, semantic
15
+ deltas, verification rollup, warnings, notes, and optional per-step table.
16
+ - **Reports default beside the trace log.** When no path is passed, reports are
17
+ written under `logs/execution-reports/<execution_id>.md` or `.json`.
18
+ `RESOLVE_MCP_TRACE_REPORT_DIR` can move that default destination without
19
+ changing where append-only trace events are logged.
20
+ - **Exports are observer-safe.** Creating a report is exempt from execution-step
21
+ recording, just like querying traces, so inspecting or exporting a trace
22
+ cannot mutate the trace being reviewed.
23
+ - **Existing files are protected by default.** A caller must pass
24
+ `overwrite=true` to replace a report at the chosen path.
25
+
26
+ ### Notes
27
+
28
+ - The export is built from the existing trace summary fields, not raw tool
29
+ arguments or raw tool results. It is meant for review and audit, not a replay
30
+ script.
31
+ - Added focused unit and server integration coverage for Markdown export, JSON
32
+ export, step omission, invalid formats, overwrite protection, observer
33
+ isolation, and `resolve_control` dispatch.
34
+
35
+ ### Fixed on the way in
36
+
37
+ - **An unverified run no longer reports itself as passed.** The verification
38
+ rollup collapsed "nothing reported any evidence" into `passed: True`, and the
39
+ report printed it verbatim — so a workflow where nothing was checked produced
40
+ an audit document reading `Status: unverified` on one line and `Passed: yes`
41
+ on the next. Those sit inches apart and only one of them gets scanned. The
42
+ rollup now carries `None` for the unknown case and the Passed row renders
43
+ "not established — no checks recorded"; a real pass still says yes and a real
44
+ failure still says no. This is the same distinction v2.206.0 documented for
45
+ `verification.status`, and the export is exactly where it stops being a
46
+ nuance and starts being a claim on paper.
47
+ - **The Simplified Chinese README was carrying a false version line.** Its
48
+ badge and "本翻译对应 vX.Y.Z 版 README" line were bumped to 2.207.0 without the
49
+ section itself, which is the specific failure the release process calls out —
50
+ a lagging translation whose version line asserts otherwise. Translated.
51
+ - `path` is documented as honoured-as-given, creating directories to reach the
52
+ destination: deliberate, since a conform's paperwork belongs beside the
53
+ conform rather than in `logs/`, but worth stating next to a source-media
54
+ safety policy. The `execution_id` route is sanitised to a bare filename and
55
+ cannot escape the report directory — verified.
56
+
57
+ ## What's New in v2.206.0 — agent execution traces
58
+
59
+ Adapted from the design contributed in PR #183.
60
+
61
+ ### Added
62
+
63
+ - **Execution traces answer "why did the editor do this?"** v2.205.0's
64
+ `_operation` envelope describes one call; a real editorial pass is a loop of
65
+ them. When an agent removes 17 pauses, seventeen individual returns each show
66
+ one deletion. A trace correlates them into a single execution carrying the
67
+ request that started it, the tools invoked and how often, cumulative
68
+ `duration_ms`, the summed semantic deltas (`items_deleted: 17`), and a
69
+ verification rollup that keeps a contradiction distinct.
70
+ - **Six actions on `resolve_control`** — `begin_execution`, `end_execution`,
71
+ `get_execution_trace`, `get_execution`, `list_recent_executions`,
72
+ `clear_executions` — with the compound tool count unchanged at 36. Any call
73
+ passing an explicit `execution_id` is correlated automatically; queries are
74
+ exempt from step recording, so observing a trace cannot alter it.
75
+ - **`duration_ms` on the `_operation` envelope**, measured with
76
+ `time.perf_counter()` around the call.
77
+
78
+ ### Notes on the adaptation
79
+
80
+ - **The trace log is anchored to the repo, not the working directory.** It was
81
+ derived from `os.getcwd()` and returned None when `./logs` did not exist —
82
+ and the generated client configs set no `cwd`, so on a standard install
83
+ persistence silently did nothing, with no signal either way. It now sits
84
+ beside `server.log`, the way `media-analysis-preferences.json` and
85
+ `server-preferences.json` already do, and the directory is created on first
86
+ write rather than being a precondition.
87
+ - **`list_recent_executions` reports where the log is and whether it is
88
+ writable.** The append is best-effort and must never fail a real edit, which
89
+ means a broken destination is otherwise invisible — "the file is empty" and
90
+ "nothing is being written" looked identical from the caller's side.
91
+ - **The log rotates at 8 MB, keeping one generation.** The in-memory ring was
92
+ capped at 100 executions; the file had no bound at all, at one append per
93
+ tool call, on machines that run for months.
94
+ - **The persistence is described accurately.** It is a synchronous buffered
95
+ append on the calling thread — measured at ~0.07ms per call, immaterial
96
+ beside any Resolve round-trip, but "non-blocking" was the wrong word for it.
97
+ It runs outside the lock, so a slow filesystem cannot serialize concurrent
98
+ tool calls.
99
+ - **What is recorded is now documented**: tool, action, timing, status,
100
+ semantic deltas, verification — no parameters, no file paths, no clip or
101
+ project names. The one free-text field is the `request` passed to
102
+ `begin_execution`, which on client work deserves the care of a commit
103
+ message.
104
+ - Verified through the real stdio JSON-RPC tool layer: 36 tools register, a
105
+ begin/call/end cycle produces one correlated trace, and the reported
106
+ persistence path is the one actually written.
107
+
5
108
  ## What's New in v2.205.2 — #184: background analysis actually starts
6
109
 
7
110
  ### Fixed
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.205.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.207.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#server-modes)
@@ -266,6 +266,35 @@ confirm gate's `status: "confirmation_required"`. `setup(action="set_defaults",
266
266
  params={"result_envelope": "pure" | "legacy"})` changes the shape, per call via
267
267
  `params={"envelope": ...}`, per process via `RESOLVE_MCP_RESULT_ENVELOPE`.
268
268
 
269
+ ### Agent execution traces ("Why did the editor do this?")
270
+
271
+ Multi-step AI operations correlate across tool calls into unified execution
272
+ traces. Each trace aggregates tool durations (`duration_ms`), call counts, cumulative
273
+ semantic deltas (`items_deleted`, `items_added`), and readback verifications.
274
+ Agents and editors can inspect workflows via `resolve_control`:
275
+ `get_execution_trace(execution_id?)`, `list_recent_executions()`, or open a
276
+ scoped execution with `begin_execution(request="...")` / `end_execution()`.
277
+ `export_execution_report(execution_id?, format="markdown"|"json")` writes a
278
+ reviewable audit artifact with the same summary, defaulting to
279
+ `logs/execution-reports/<execution_id>.md`. `path` writes it anywhere you want
280
+ it instead — alongside a conform in a dated TransferFiles folder, say — and
281
+ creates the directories to get there, so check the path before you send it.
282
+ An existing file is never replaced without `overwrite: true`.
283
+
284
+ A report for a run where nothing was verified says **"not established — no
285
+ checks recorded"**, not "passed". Absence of evidence is a question still open,
286
+ and an audit document is the last place to let a reader read it as an all-clear.
287
+
288
+ Traces live in a 100-entry in-memory ring and are appended to
289
+ `logs/execution-traces.jsonl` beside `server.log` — `RESOLVE_MCP_TRACE_FILE`
290
+ moves it. `list_recent_executions` reports that path and whether it is
291
+ writable, so "the log is empty" and "nothing is being written" are
292
+ distinguishable without reading the source. What is recorded is tool name,
293
+ action, timing, status, semantic deltas and verification — no parameters and no
294
+ file paths. The one free-text field is the `request` you pass to
295
+ `begin_execution`, so treat it the way you would a commit message on a client
296
+ project.
297
+
269
298
  ## Optional Extras
270
299
 
271
300
  The core install is deliberately small: Python, ffmpeg, and the Resolve scripting
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.205.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.207.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#服务器模式)
@@ -12,7 +12,7 @@
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
13
13
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
14
14
 
15
- > 本翻译对应 v2.205.2 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.207.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -166,6 +166,18 @@ DRX 调色写入**针对 Resolve Studio 做过实机校准**:调色参数默
166
166
 
167
167
  信封是带命名空间的,而不是平铺到顶层,因为 `status`、`operation`、`warnings`、`result` 和 `changes` 在这里本来就都是业务 key;平铺会改写后台任务的 `status: "done"` 和确认关卡的 `status: "confirmation_required"`。用 `setup(action="set_defaults", params={"result_envelope": "pure" | "legacy"})` 改变形态,单次调用用 `params={"envelope": ...}`,进程级用 `RESOLVE_MCP_RESULT_ENVELOPE`。
168
168
 
169
+ ### Agent 执行轨迹("编辑器为什么这么做?")
170
+
171
+ 多步 AI 操作会跨工具调用关联成统一的执行轨迹,聚合各工具耗时(`duration_ms`)、调用次数、累计语义增量(`items_deleted`、`items_added`)以及回读校验结果。通过 `resolve_control` 查看:`get_execution_trace(execution_id?)`、`list_recent_executions()`,或用 `begin_execution(request="...")` / `end_execution()` 圈定一段执行。
172
+
173
+ 轨迹保存在一个容量 100 条的内存环形缓冲里,并追加写入 `logs/execution-traces.jsonl`(就在 `server.log` 旁边,可用 `RESOLVE_MCP_TRACE_FILE` 改位置),文件到 8 MB 会轮转并保留一份上一代。`list_recent_executions` 会返回该路径以及是否可写,这样"日志是空的"和"根本没在写"就能区分开。记录的内容是工具名、动作、耗时、状态、语义增量和校验结果——不记录参数,也不记录文件路径。唯一的自由文本是你传给 `begin_execution` 的 `request`,在客户项目上请把它当成提交信息来写。
174
+
175
+ ### 导出执行审计报告
176
+
177
+ `export_execution_report(execution_id?, format="markdown"|"json")` 会把一条轨迹写成可供审阅的审计文件,默认落在 `logs/execution-reports/<execution_id>.md`。传 `path` 可以写到任何你想要的位置——比如跟着某次套底放进当天的 TransferFiles 文件夹——并且会自动创建沿途的目录,所以发出去之前请先确认路径。已存在的文件不会被覆盖,除非显式传 `overwrite: true`。
178
+
179
+ 如果这次运行根本没有做过校验,报告里写的是**"not established — no checks recorded"(未确立——没有记录任何检查)**,而不是"通过"。没有证据是一个仍然悬而未决的问题;审计文件恰恰是最不该让读者把它读成"一切正常"的地方。
180
+
169
181
  ## 可选增强
170
182
 
171
183
  核心安装刻意保持精简:Python、ffmpeg 和 Resolve 脚本 API。有些功能需要更多依赖,且**每一项都会诚实拒绝并给出自己的安装命令,而不是退化成瞎猜**——编造的节拍或虚构的电平会产出自信但错误的结果,比没有这个功能更糟。
package/docs/SKILL.md CHANGED
@@ -219,6 +219,99 @@ nested under `result`; `legacy` adds nothing.
219
219
 
220
220
  ---
221
221
 
222
+ ## Agent Observability: Execution Traces ("Why did the editor do this?")
223
+
224
+ Multi-step AI operations (such as detecting pauses, deleting multiple timeline
225
+ items, and verifying the result) correlate across calls into unified execution
226
+ traces. Each trace captures the user request or prompt, tool execution timing,
227
+ cumulative semantic deltas, and verification outcomes.
228
+
229
+ Example trace shape returned by `resolve_control(action="get_execution_trace")`:
230
+
231
+ ```json
232
+ {
233
+ "execution_id": "exec_8f91c7a210bc",
234
+ "request": "Remove all pauses longer than 800ms",
235
+ "status": "success",
236
+ "started_at": "2026-09-04T07:30:00Z",
237
+ "ended_at": "2026-09-04T07:30:02Z",
238
+ "duration_ms": 2845,
239
+ "tools": [
240
+ {
241
+ "tool": "media_analysis.analyze_timeline",
242
+ "count": 1,
243
+ "duration_ms": 821
244
+ },
245
+ {
246
+ "tool": "timeline.delete_item",
247
+ "count": 17,
248
+ "duration_ms": 1420
249
+ }
250
+ ],
251
+ "changes": {
252
+ "items_deleted": 17
253
+ },
254
+ "verification": {
255
+ "status": "passed",
256
+ "passed": true,
257
+ "checks": [{"check": "readback_verification", "passed": true}]
258
+ },
259
+ "warnings": []
260
+ }
261
+ ```
262
+
263
+ ### Trace Actions on `resolve_control`
264
+
265
+ - **`begin_execution(request?, execution_id?, initiator?)`**: Opens a scoped
266
+ multi-step execution. Subsequent tool calls in the session automatically thread
267
+ under this `execution_id` until ended.
268
+ - **`end_execution(execution_id?, verification?, status?, notes?)`**: Closes the
269
+ active execution, finalizes timestamps and aggregated metrics.
270
+ - **`get_execution_trace(execution_id?)`** / **`get_execution(execution_id)`**:
271
+ Fetches the trace for a specific ID, or the most recent execution if omitted.
272
+ - **`list_recent_executions(limit?)`**: Returns the recent execution traces
273
+ (newest first, default limit 20).
274
+ - **`export_execution_report(execution_id?, format?, path?, overwrite?, include_steps?)`**:
275
+ Writes a Markdown or JSON audit report for a trace. The default destination is
276
+ `logs/execution-reports/<execution_id>.md`; pass `format: "json"` for
277
+ structured output, `include_steps: false` for a shorter summary, or
278
+ `overwrite: true` to replace an existing report.
279
+ - **`clear_executions(dry_run?)`**: Clears the in-memory execution trace buffer.
280
+
281
+ Explicit correlation is also supported per-call: pass `params={"execution_id": ...}`
282
+ or `params={"trace_id": ...}` in any tool call to associate it with a specific trace.
283
+
284
+ Two things to know when a trace is not where you expect it. The buffer holds the
285
+ **100 most recent** executions and is in memory only — a server restart empties
286
+ it, and the on-disk `logs/execution-traces.jsonl` is the durable record.
287
+ `list_recent_executions` returns a `persistence` block naming that file and
288
+ whether it is writable; check it before concluding that nothing was traced,
289
+ since the append is best-effort and will never fail a real edit to report a
290
+ logging problem.
291
+
292
+ Recorded per step: tool, action, `duration_ms`, status, semantic deltas and
293
+ verification. **Not** recorded: parameters, file paths, clip or project names.
294
+ The only free text is the `request` string passed to `begin_execution`.
295
+
296
+ The file rotates at 8 MB, keeping one previous generation as
297
+ `execution-traces.jsonl.1`. Both are gitignored along with the rest of `logs/`.
298
+ Audit reports are separate point-in-time exports; `RESOLVE_MCP_TRACE_REPORT_DIR`
299
+ moves their default directory without moving the append-only trace log.
300
+
301
+ `path` is honoured as given — the report can be written anywhere, and the
302
+ directories are created to reach it. That is deliberate (a conform's paperwork
303
+ belongs beside the conform, not in `logs/`), so treat it the way you would any
304
+ other export destination and do not invent a path near source media. The
305
+ `execution_id` route cannot escape the report directory: it is sanitised to a
306
+ filename.
307
+
308
+ A report whose run recorded no verification checks renders **"not established
309
+ — no checks recorded"** in the Passed row, never "yes" — the same rule as
310
+ `verification.status: "unverified"` in the envelope. Do not report such a run
311
+ to the user as verified.
312
+
313
+ ---
314
+
222
315
  ## Two Server Modes
223
316
 
224
317
  | Mode | Entry point | Tool count | Use when |
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.205.2"
40
+ VERSION = "2.207.0"
41
41
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
42
42
  # Resolve's scripting bridge loads into newer interpreters on recent builds
43
43
  # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.205.2",
3
+ "version": "2.207.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.205.2"
90
+ VERSION = "2.207.0"
91
91
  logger = logging.getLogger("davinci-resolve-mcp")
92
92
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
93
93
  logger.info(f"Detected platform: {get_platform()}")
package/src/server.py CHANGED
@@ -11,7 +11,7 @@ Usage:
11
11
  python src/server.py --full # Start the 353-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.205.2"
14
+ VERSION = "2.207.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -66,6 +66,16 @@ from src.utils.operation_result import (
66
66
  get_envelope_mode as _get_envelope_mode,
67
67
  set_envelope_mode as _set_envelope_mode,
68
68
  )
69
+ from src.utils import execution_trace as _execution_trace
70
+ from src.utils.execution_trace import (
71
+ get_execution_trace,
72
+ get_execution,
73
+ list_recent_executions,
74
+ begin_execution,
75
+ end_execution,
76
+ clear_executions,
77
+ export_execution_report,
78
+ )
69
79
  from src.utils.render_ids import (
70
80
  render_codec_id_from_codecs as _render_codec_id_from_codecs,
71
81
  render_format_id_from_formats as _render_format_id_from_formats,
@@ -1487,6 +1497,51 @@ def _guarded_params(args, kwargs) -> Optional[Dict[str, Any]]:
1487
1497
  return None
1488
1498
 
1489
1499
 
1500
+ _TRACE_OBSERVER_ACTIONS = {
1501
+ "get_execution_trace", "get_execution", "list_recent_executions",
1502
+ "clear_executions", "begin_execution", "end_execution",
1503
+ "export_execution_report",
1504
+ }
1505
+
1506
+
1507
+ def _record_execution_step(
1508
+ tool_name: str,
1509
+ action: str,
1510
+ params: Optional[Dict[str, Any]],
1511
+ raw_result: Any,
1512
+ enveloped: Any,
1513
+ duration_ms: int,
1514
+ ) -> None:
1515
+ if tool_name == "resolve_control" and action in _TRACE_OBSERVER_ACTIONS:
1516
+ return
1517
+ try:
1518
+ env = None
1519
+ if isinstance(enveloped, dict):
1520
+ env = enveloped.get(_operation_result.ENVELOPE_KEY)
1521
+ if not env and "execution_id" in enveloped:
1522
+ env = enveloped
1523
+ exec_id = env.get("execution_id") if isinstance(env, dict) else None
1524
+ status = env.get("status") if isinstance(env, dict) else None
1525
+ verification = env.get("verification") if isinstance(env, dict) else None
1526
+ changes = env.get("changes") if isinstance(env, dict) else None
1527
+ warnings = env.get("warnings") if isinstance(env, dict) else None
1528
+
1529
+ _execution_trace.record_step(
1530
+ tool=tool_name,
1531
+ action=action,
1532
+ params=params if isinstance(params, dict) else None,
1533
+ raw_result=raw_result,
1534
+ duration_ms=duration_ms,
1535
+ execution_id=exec_id,
1536
+ status=status,
1537
+ verification=verification,
1538
+ changes=changes,
1539
+ warnings=warnings,
1540
+ )
1541
+ except Exception as exc: # pragma: no cover
1542
+ logger.debug("Failed to record execution step: %s", exc)
1543
+
1544
+
1490
1545
  def _guard_missing_params(fn):
1491
1546
  """Tool decorator: report a missing parameter instead of leaking a KeyError.
1492
1547
 
@@ -1517,22 +1572,32 @@ def _guard_missing_params(fn):
1517
1572
  @functools.wraps(fn)
1518
1573
  async def wrapper(*args, **kwargs):
1519
1574
  action = _guarded_action_name(args, kwargs)
1575
+ params = _guarded_params(args, kwargs)
1576
+ t0 = time.perf_counter()
1520
1577
  try:
1521
1578
  result = await fn(*args, **kwargs)
1522
1579
  except _MissingParam as exc:
1523
1580
  result = _missing_param_error(exc, action)
1524
- return _build_operation_envelope(
1525
- tool_name, action, _guarded_params(args, kwargs), result)
1581
+ duration_ms = max(0, int((time.perf_counter() - t0) * 1000))
1582
+ enveloped = _build_operation_envelope(
1583
+ tool_name, action, params, result, duration_ms=duration_ms)
1584
+ _record_execution_step(tool_name, action, params, result, enveloped, duration_ms)
1585
+ return enveloped
1526
1586
  else:
1527
1587
  @functools.wraps(fn)
1528
1588
  def wrapper(*args, **kwargs):
1529
1589
  action = _guarded_action_name(args, kwargs)
1590
+ params = _guarded_params(args, kwargs)
1591
+ t0 = time.perf_counter()
1530
1592
  try:
1531
1593
  result = fn(*args, **kwargs)
1532
1594
  except _MissingParam as exc:
1533
1595
  result = _missing_param_error(exc, action)
1534
- return _build_operation_envelope(
1535
- tool_name, action, _guarded_params(args, kwargs), result)
1596
+ duration_ms = max(0, int((time.perf_counter() - t0) * 1000))
1597
+ enveloped = _build_operation_envelope(
1598
+ tool_name, action, params, result, duration_ms=duration_ms)
1599
+ _record_execution_step(tool_name, action, params, result, enveloped, duration_ms)
1600
+ return enveloped
1536
1601
 
1537
1602
  wrapper.__wrapped_by_missing_param_guard__ = True
1538
1603
  return wrapper
@@ -16047,6 +16112,20 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
16047
16112
  — Captures the current Resolve UI state so it can be restored after a preview.
16048
16113
  restore_state(state_token) -> {success, restored: {...}}
16049
16114
  — Returns Resolve to a previously-saved state.
16115
+ get_execution_trace(execution_id?) -> {success, trace}
16116
+ — Correlated agent execution trace by execution_id, or most recent if omitted (no connection needed).
16117
+ get_execution(execution_id) -> {success, trace}
16118
+ — Alias for get_execution_trace.
16119
+ list_recent_executions(limit?) -> {success, executions, count}
16120
+ — List recent execution traces with aggregated tool calls, durations, and verifications (no connection needed).
16121
+ begin_execution(request?, execution_id?, initiator?) -> {success, execution_id, started_at, request}
16122
+ — Open a multi-step execution trace so subsequent tool calls thread under this execution ID.
16123
+ end_execution(execution_id?, verification?, status?, notes?) -> {success, trace}
16124
+ — Conclude an execution trace and compute final rollups.
16125
+ export_execution_report(execution_id?, format?, path?, overwrite?, include_steps?) -> {success, path, bytes}
16126
+ — Write a Markdown or JSON audit report for an execution trace (no connection needed).
16127
+ clear_executions(dry_run?) -> {success, cleared}
16128
+ — Clear the in-memory execution trace buffer.
16050
16129
  """
16051
16130
  p = _params(params)
16052
16131
 
@@ -16134,6 +16213,80 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
16134
16213
  if action == "list_jobs":
16135
16214
  return {"jobs": background_jobs.list_jobs()}
16136
16215
 
16216
+ if action in {"get_execution_trace", "get_execution"}:
16217
+ exec_id = p.get("execution_id") or p.get("id")
16218
+ trace = _execution_trace.get_execution_trace(exec_id)
16219
+ if not trace:
16220
+ return _err(f"No execution trace found for id: {exec_id or 'latest'}", code="NOT_FOUND", category="state")
16221
+ return {"success": True, "trace": trace}
16222
+ if action == "list_recent_executions":
16223
+ limit = _safe_int(p.get("limit"), 20, minimum=1, maximum=100)
16224
+ executions = _execution_trace.list_recent_executions(limit=limit)
16225
+ # Say where the on-disk log is and whether it is actually writable.
16226
+ # The traces themselves live in a 100-entry in-memory ring, so "the
16227
+ # list is short" and "the log is not being written" are different
16228
+ # facts, and a caller should not have to guess which one they have.
16229
+ return {
16230
+ "success": True,
16231
+ "executions": executions,
16232
+ "count": len(executions),
16233
+ "buffer_capacity": _execution_trace.MAX_RECENT_EXECUTIONS,
16234
+ "persistence": _execution_trace.persistence_status(),
16235
+ }
16236
+ if action == "begin_execution":
16237
+ req = p.get("request") or p.get("prompt") or p.get("reason")
16238
+ exec_id = p.get("execution_id") or p.get("id")
16239
+ res = _execution_trace.begin_execution(
16240
+ request=req,
16241
+ execution_id=exec_id,
16242
+ initiator=p.get("initiator") or "agent",
16243
+ )
16244
+ return res
16245
+ if action == "end_execution":
16246
+ exec_id = p.get("execution_id") or p.get("id")
16247
+ trace = _execution_trace.end_execution(
16248
+ execution_id=exec_id,
16249
+ verification=p.get("verification"),
16250
+ status=p.get("status"),
16251
+ notes=p.get("notes"),
16252
+ )
16253
+ if not trace:
16254
+ return _err("No active or matching execution to end", code="NOT_FOUND", category="state")
16255
+ return {"success": True, "trace": trace}
16256
+ if action == "export_execution_report":
16257
+ exec_id = p.get("execution_id") or p.get("id")
16258
+ report_format = p.get("format") or p.get("report_format") or "markdown"
16259
+ include_steps = _setup_bool(p.get("include_steps", p.get("includeSteps")), True)
16260
+ overwrite = _setup_bool(p.get("overwrite"), False)
16261
+ try:
16262
+ res = _execution_trace.export_execution_report(
16263
+ execution_id=exec_id,
16264
+ report_format=report_format,
16265
+ output_path=p.get("path") or p.get("output_path") or p.get("outputPath"),
16266
+ overwrite=overwrite,
16267
+ include_steps=include_steps,
16268
+ )
16269
+ except FileExistsError as exc:
16270
+ return _err(str(exc), code="REPORT_EXISTS", category="invalid_input")
16271
+ except ValueError as exc:
16272
+ return _err(str(exc), code="INVALID_REPORT_FORMAT", category="invalid_input")
16273
+ except OSError as exc:
16274
+ return _err(
16275
+ "Could not write execution report",
16276
+ code="REPORT_WRITE_FAILED",
16277
+ category="io",
16278
+ reason=str(exc),
16279
+ )
16280
+ if not res:
16281
+ return _err(f"No execution trace found for id: {exec_id or 'latest'}", code="NOT_FOUND", category="state")
16282
+ return res
16283
+ if action == "clear_executions":
16284
+ dry_run = _setup_bool(p.get("dry_run", p.get("dryRun")), False)
16285
+ if dry_run:
16286
+ return {"success": True, "dry_run": True, "count": len(_execution_trace.list_recent_executions(100))}
16287
+ res = _execution_trace.clear_executions()
16288
+ return res
16289
+
16137
16290
  # Control-panel actions don't require Resolve to be running.
16138
16291
  if action == "open_control_panel":
16139
16292
  return _open_control_panel(p)
@@ -16348,7 +16501,7 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
16348
16501
  if err:
16349
16502
  return _err(err)
16350
16503
  return {"success": bool(r.ExportUserPreferencesPreset(clean["name"], clean["path"]))}
16351
- return _unknown(action, ["launch","runtime_mode","get_version","api_truth","check_version_support","verification_stats","job_status","list_jobs","mcp_update_status","set_mcp_update_policy","ignore_mcp_update","snooze_mcp_update","clear_mcp_update_preferences","get_page","open_page","get_keyframe_mode","set_keyframe_mode","quit","get_fairlight_presets","set_high_priority","disable_background_tasks_for_current_session","list_user_preferences_presets","save_user_preferences_preset","load_user_preferences_preset","delete_user_preferences_preset","import_user_preferences_preset","export_user_preferences_preset","open_control_panel","control_panel_status","close_control_panel","save_state","restore_state"])
16504
+ return _unknown(action, ["launch","runtime_mode","get_version","api_truth","check_version_support","verification_stats","job_status","list_jobs","get_execution_trace","get_execution","list_recent_executions","begin_execution","end_execution","export_execution_report","clear_executions","mcp_update_status","set_mcp_update_policy","ignore_mcp_update","snooze_mcp_update","clear_mcp_update_preferences","get_page","open_page","get_keyframe_mode","set_keyframe_mode","quit","get_fairlight_presets","set_high_priority","disable_background_tasks_for_current_session","list_user_preferences_presets","save_user_preferences_preset","load_user_preferences_preset","delete_user_preferences_preset","import_user_preferences_preset","export_user_preferences_preset","open_control_panel","control_panel_status","close_control_panel","save_state","restore_state"])
16352
16505
 
16353
16506
 
16354
16507
  # ─── V2 C4: Per-field corrections with provenance + changelog ────────────────