davinci-resolve-mcp 2.38.0 → 2.39.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,26 @@
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.39.0
6
+
7
+ Governance enforce mode and actor identity — the staged Phase 3 of the
8
+ Resolve 21 AI-ops work.
9
+
10
+ - **Added** governance `mode` for the media-creating AI ops (motion deblur,
11
+ speech generation): `advisory` (default, unchanged — confirm-preview
12
+ warnings only) or `enforce`, where an over-tier run is blocked with
13
+ `GOVERNANCE_BLOCKED` before token issuance, naming the exceeded dimensions.
14
+ Escape hatches: raise the tier, relax the mode, or pass
15
+ `override_governance=true` to consciously exceed the tier once.
16
+ `set_ai_governance` accepts `mode` (preset now optional);
17
+ `get_ai_governance` reports it.
18
+ - **Added** instance-level actor identity (per the recorded concurrency
19
+ design): each entry point declares itself — `stdio`, `network-sse`,
20
+ `network-http`, `control-panel`, `batch-cli` — and AI-ops ledger rows,
21
+ brain edits, and timeline versions now carry `actor` (`<instance>:<pid>`)
22
+ alongside `initiator`. Schema v8 (additive columns); migration verified
23
+ against a copy of a real project DB with all rows preserved.
24
+
5
25
  ## What's New in v2.38.0
6
26
 
7
27
  Busy gate for long DaVinci Resolve operations — the first piece of the
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DaVinci Resolve MCP Server
2
2
 
3
- [![Version](https://img.shields.io/badge/version-2.38.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.39.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
4
4
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
5
5
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
6
6
  [![Tools](https://img.shields.io/badge/MCP%20Tools-33%20(341%20full)-blue.svg)](#server-modes)
package/docs/SKILL.md CHANGED
@@ -694,7 +694,13 @@ The two media-creating ops also have **soft governance tiers**
694
694
  deblur/speech runs, bytes, and render time. It is advisory — the confirm dialog
695
695
  warns when near/over the tier but never blocks (the ops are confirm-gated).
696
696
  Inspect/set with `media_analysis(action="get_ai_governance")` and
697
- `media_analysis(action="set_ai_governance", preset=…, overrides={...})`; the AI
697
+ `media_analysis(action="set_ai_governance", preset=…, mode=…, overrides={...})`.
698
+ Governance `mode` is `advisory` by default (preview warnings only); `enforce`
699
+ blocks an over-tier run with `GOVERNANCE_BLOCKED` until the tier is raised, the
700
+ mode is relaxed, or the op is re-called with `override_governance=true`. Ledger
701
+ rows, brain edits, and timeline versions record the acting instance
702
+ (`stdio` / `network-sse` / `network-http` / `control-panel` / `batch-cli` + pid)
703
+ in an `actor` field; the AI
698
704
  Console's Governance section offers a tier picker + consumption gauges.
699
705
 
700
706
  The caps layer:
package/install.py CHANGED
@@ -35,7 +35,7 @@ from src.utils.update_check import (
35
35
 
36
36
  # ─── Version ──────────────────────────────────────────────────────────────────
37
37
 
38
- VERSION = "2.38.0"
38
+ VERSION = "2.39.0"
39
39
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
40
40
  # Resolve's scripting bridge loads into newer interpreters on recent builds
41
41
  # (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.38.0",
3
+ "version": "2.39.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -14678,6 +14678,8 @@ def _warm_inventory_cache(project_root: str) -> None:
14678
14678
 
14679
14679
 
14680
14680
  def main() -> None:
14681
+ from src.utils import actor_identity
14682
+ actor_identity.set_instance("control-panel")
14681
14683
  args = parse_args()
14682
14684
  state = DashboardState(args.project_name, args.project_id, args.analysis_root)
14683
14685
  Handler.state = state
package/src/batch_cli.py CHANGED
@@ -526,6 +526,8 @@ _HANDLERS = {
526
526
 
527
527
 
528
528
  def main(argv: Optional[List[str]] = None) -> int:
529
+ from src.utils import actor_identity
530
+ actor_identity.set_instance("batch-cli")
529
531
  parser = _build_parser()
530
532
  args = parser.parse_args(argv)
531
533
  if not hasattr(args, "json"):
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
80
80
  handlers=[logging.StreamHandler()],
81
81
  )
82
82
 
83
- VERSION = "2.38.0"
83
+ VERSION = "2.39.0"
84
84
  logger = logging.getLogger("davinci-resolve-mcp")
85
85
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
86
86
  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 341-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.38.0"
14
+ VERSION = "2.39.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -84,7 +84,7 @@ from src.utils.media_analysis import (
84
84
  summarize_reports as summarize_media_analysis_reports,
85
85
  )
86
86
  from src.utils.sync_detection import detect_sync_events_for_records as detect_media_sync_events
87
- from src.utils import resolve_busy
87
+ from src.utils import actor_identity, resolve_busy
88
88
  from src.utils.resolve_busy import long_resolve_op
89
89
  from src.utils.media_analysis_jobs import (
90
90
  MEDIA_EXTENSIONS,
@@ -716,6 +716,54 @@ def _ai_governance_check(op: str) -> Dict[str, Any]:
716
716
  return {"applies": False}
717
717
 
718
718
 
719
+ _AI_GOVERNANCE_MODES = ("advisory", "enforce")
720
+
721
+
722
+ def _ai_governance_mode() -> str:
723
+ try:
724
+ raw = str(_read_media_analysis_preferences().get("resolve_ai_governance_mode") or "").strip().lower()
725
+ return raw if raw in _AI_GOVERNANCE_MODES else "advisory"
726
+ except Exception:
727
+ return "advisory"
728
+
729
+
730
+ def _ai_governance_gate(op: str, p: Dict[str, Any]) -> Optional[Dict[str, Any]]:
731
+ """Hard gate for a governed AI render op when governance mode is 'enforce'.
732
+
733
+ Returns None to proceed (advisory mode, within tier, or explicit
734
+ override_governance=True), otherwise a destructive_blocked error naming
735
+ the exceeded dimensions. Checked before token issuance so the agent
736
+ learns about the block without burning a confirm round-trip.
737
+ """
738
+ if _ai_governance_mode() != "enforce":
739
+ return None
740
+ if p.get("override_governance") or p.get("overrideGovernance"):
741
+ return None
742
+ check = _ai_governance_check(op)
743
+ if not check.get("applies") or not check.get("exceeded"):
744
+ return None
745
+ return _err(
746
+ f"Governance tier '{check.get('tier')}' blocks this {op} run: "
747
+ + " ".join(check.get("warnings") or ["over threshold."]),
748
+ code="GOVERNANCE_BLOCKED",
749
+ category="destructive_blocked",
750
+ retryable=False,
751
+ remediation=(
752
+ "Raise the tier or thresholds via media_analysis(action='set_ai_governance'), "
753
+ "switch governance mode back to 'advisory', or re-call with override_governance=true "
754
+ "to consciously exceed the tier this once."
755
+ ),
756
+ state={
757
+ "op": op,
758
+ "mode": "enforce",
759
+ "tier": check.get("tier"),
760
+ "usage": check.get("usage"),
761
+ "projected": check.get("projected"),
762
+ "thresholds": check.get("thresholds"),
763
+ },
764
+ )
765
+
766
+
719
767
  def _destructive_preference_provider(key: str) -> Any:
720
768
  """Reader for C6 preferences out of the existing media-analysis prefs file."""
721
769
  try:
@@ -1032,11 +1080,14 @@ def _consume_confirm_token(*, action: str, params: Optional[Dict[str, Any]]) ->
1032
1080
  rec = _CONFIRM_TOKENS.pop(token, None) # one-time use
1033
1081
  if rec is None:
1034
1082
  return _err(
1035
- "confirm_token is invalid or expired",
1083
+ "confirm_token is invalid, expired, or was issued by a different "
1084
+ "server instance (tokens are valid only on the instance that "
1085
+ "issued them — e.g. a stdio-server token is not honored by the "
1086
+ "networked server).",
1036
1087
  code="CONFIRM_TOKEN_INVALID",
1037
1088
  category="destructive_blocked",
1038
1089
  retryable=False,
1039
- remediation=f"Re-call {action} without confirm_token to receive a fresh token.",
1090
+ remediation=f"Re-call {action} without confirm_token on this instance to receive a fresh token.",
1040
1091
  )
1041
1092
  if rec.get("action") != action:
1042
1093
  return _err(
@@ -12998,6 +13049,9 @@ def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Di
12998
13049
  return _err("generate_speech requires speech_generation_settings with a 'TextInput' string. "
12999
13050
  "Optional keys: VoiceModel, CustomVoiceFile, Speed, Variation, Pitch, GenerationID, Filename, AddToTimeline, AudioTrack.")
13000
13051
  timecode = _first_param(p, "timecode", default="") or ""
13052
+ gate = _ai_governance_gate("generate_speech", p)
13053
+ if gate:
13054
+ return gate
13001
13055
  if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
13002
13056
  return _issue_confirm_token(
13003
13057
  action="project_settings.generate_speech",
@@ -14090,6 +14144,9 @@ def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
14090
14144
  if missing:
14091
14145
  return missing
14092
14146
  deblur = _first_param(p, "deblur_option", "deblurOption", default=None) or {}
14147
+ gate = _ai_governance_gate("remove_motion_blur", p)
14148
+ if gate:
14149
+ return gate
14093
14150
  if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
14094
14151
  return _issue_confirm_token(
14095
14152
  action="folder.remove_motion_blur",
@@ -14425,6 +14482,9 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
14425
14482
  if missing:
14426
14483
  return missing
14427
14484
  deblur = _first_param(p, "deblur_option", "deblurOption", default=None) or {}
14485
+ gate = _ai_governance_gate("remove_motion_blur", p)
14486
+ if gate:
14487
+ return gate
14428
14488
  if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
14429
14489
  return _issue_confirm_token(
14430
14490
  action="media_pool_item.remove_motion_blur",
@@ -14608,8 +14668,8 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
14608
14668
  set_caps_preset(preset, overrides?) -> {success, preset, overrides} — preset is minimal | standard | generous | unlimited. Overrides is a dict of {field: int|"unlimited"} that wins over the preset.
14609
14669
  get_usage(scope?, scope_key?, clip_id?, job_id?) -> {scope, usage} — raw usage rollup for one scope (clip | job | day).
14610
14670
  get_resolve_ai_usage(session_only?, op?, limit?) -> {summary, recent} — ledger of Resolve 21 local AI ops (audio classification, IntelliSearch, slate, motion-deblur, speech). Tracks invocations, wall-clock, and files/bytes created by the two media-creating ops. Separate from get_caps/get_usage (those meter Claude-side tokens; these ops don't spend them).
14611
- get_ai_governance() -> {tier, thresholds, usage, tiers_available} — soft governance tier (off|lenient|standard|strict) for the media-creating AI ops vs this session's render usage. Advisory; surfaced in the confirm preview + panel, never blocks.
14612
- set_ai_governance(preset, overrides?) -> {success, tier, overrides} — set the governance tier. Overrides keys: deblur_runs, speech_runs, render_bytes, render_wall_clock_ms (int or "unlimited").
14671
+ get_ai_governance() -> {tier, mode, thresholds, usage, tiers_available} — governance tier (off|lenient|standard|strict) for the media-creating AI ops vs this session's render usage. mode is advisory (default; preview warnings only) or enforce (over-tier runs are blocked).
14672
+ set_ai_governance(preset?, mode?, overrides?) -> {success, tier, mode, overrides} — set the tier, the mode (advisory|enforce), and/or overrides (deblur_runs, speech_runs, render_bytes, render_wall_clock_ms; int or "unlimited"). In enforce mode a blocked run returns GOVERNANCE_BLOCKED; pass override_governance=true on the op to consciously exceed the tier once.
14613
14673
  resolve_output_root(analysis_root?, source_paths?) -> {project_root}
14614
14674
  plan(target, depth?, analysis_root?, transcription?, vision?, dry_run?) -> {clips, artifacts}
14615
14675
  analyze_file(path|file_path, dry_run?, session_only?, persist?) -> {clips, manifest}
@@ -14792,19 +14852,32 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
14792
14852
  project_root=project_root, session_id=_AI_LEDGER_SESSION_ID,
14793
14853
  preset=_ai_governance_preset(), overrides=_ai_governance_overrides(),
14794
14854
  )
14795
- return {"success": True, "session_id": _AI_LEDGER_SESSION_ID, **st}
14855
+ return {"success": True, "session_id": _AI_LEDGER_SESSION_ID, "mode": _ai_governance_mode(), **st}
14796
14856
  except Exception as exc:
14797
14857
  return _err(f"{type(exc).__name__}: {exc}")
14798
14858
  if action == "set_ai_governance":
14799
14859
  preset = (p.get("preset") or "").strip().lower()
14800
- if preset not in _resolve_ai_governance.VALID_TIERS:
14860
+ mode = (p.get("mode") or "").strip().lower()
14861
+ if not preset and not mode and not isinstance(p.get("overrides"), dict):
14862
+ return _err("set_ai_governance requires at least one of: preset, mode, overrides")
14863
+ if preset and preset not in _resolve_ai_governance.VALID_TIERS:
14801
14864
  return _err(f"unknown tier '{preset}'. Valid: {sorted(_resolve_ai_governance.VALID_TIERS)}")
14865
+ if mode and mode not in _AI_GOVERNANCE_MODES:
14866
+ return _err(f"unknown mode '{mode}'. Valid: {list(_AI_GOVERNANCE_MODES)}")
14802
14867
  prefs = _read_media_analysis_preferences()
14803
- prefs["resolve_ai_governance_preset"] = preset
14868
+ if preset:
14869
+ prefs["resolve_ai_governance_preset"] = preset
14870
+ if mode:
14871
+ prefs["resolve_ai_governance_mode"] = mode
14804
14872
  if isinstance(p.get("overrides"), dict):
14805
14873
  prefs["resolve_ai_governance_overrides"] = p["overrides"]
14806
14874
  _write_media_analysis_preferences(prefs)
14807
- return {"success": True, "tier": preset, "overrides": prefs.get("resolve_ai_governance_overrides") or {}}
14875
+ return {
14876
+ "success": True,
14877
+ "tier": prefs.get("resolve_ai_governance_preset") or _resolve_ai_governance.DEFAULT_TIER,
14878
+ "mode": prefs.get("resolve_ai_governance_mode") or "advisory",
14879
+ "overrides": prefs.get("resolve_ai_governance_overrides") or {},
14880
+ }
14808
14881
  if action == "set_caps_preset":
14809
14882
  preset = (p.get("preset") or "").strip().lower()
14810
14883
  from src.utils import analysis_caps as _ac
@@ -21363,6 +21436,7 @@ if __name__ == "__main__":
21363
21436
  del sys.argv[_i:_i + 2]
21364
21437
  if transport in ("sse", "streamable-http"):
21365
21438
  from src.utils.mcp_transport import run_networked
21439
+ actor_identity.set_instance("network-sse" if transport == "sse" else "network-http")
21366
21440
  logger.info(f"Starting DaVinci Resolve MCP Server ({transport} transport)")
21367
21441
  run_networked(mcp, transport)
21368
21442
  sys.exit(0)
@@ -0,0 +1,46 @@
1
+ """Instance-level actor identity for ledger and destructive-op records.
2
+
3
+ Design decision (2026-06-09): the supported concurrency target is a single
4
+ editor running multiple clients — a stdio server, a networked server, the
5
+ control panel, and the batch CLI may all drive one Resolve. Records therefore
6
+ identify the INSTANCE that performed an op, not a per-request client; finer
7
+ per-client fingerprints can layer on in a multi-user future without changing
8
+ this surface.
9
+
10
+ Each entry point declares itself once at startup via set_instance():
11
+
12
+ stdio — the default MCP server over stdio
13
+ network-sse — the networked server (SSE transport)
14
+ network-http — the networked server (streamable-http transport)
15
+ control-panel — the analysis dashboard process
16
+ batch-cli — the headless batch runner
17
+
18
+ actor_string() is the compact form persisted in DB columns: "<instance>:<pid>".
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ from typing import Any, Dict
24
+
25
+ _instance = "stdio"
26
+
27
+ KNOWN_INSTANCES = ("stdio", "network-sse", "network-http", "control-panel", "batch-cli")
28
+
29
+
30
+ def set_instance(kind: str) -> None:
31
+ """Declare what kind of process this is. Unknown kinds are kept verbatim."""
32
+ global _instance
33
+ kind = str(kind or "").strip() or "stdio"
34
+ _instance = kind
35
+
36
+
37
+ def get_instance() -> str:
38
+ return _instance
39
+
40
+
41
+ def current_actor() -> Dict[str, Any]:
42
+ return {"instance": _instance, "pid": os.getpid()}
43
+
44
+
45
+ def actor_string() -> str:
46
+ return f"{_instance}:{os.getpid()}"
@@ -24,7 +24,7 @@ import os
24
24
  import time
25
25
  from typing import Any, Callable, Dict, List, Optional
26
26
 
27
- from src.utils import timeline_brain_db
27
+ from src.utils import actor_identity, timeline_brain_db
28
28
 
29
29
  logger = logging.getLogger("resolve-mcp.brain-edits")
30
30
 
@@ -220,14 +220,15 @@ def log_brain_edit(
220
220
  analysis_run_id, timeline_before, timeline_after, edit_type,
221
221
  tool_name, action_name, target_metric, metric_direction,
222
222
  before_value, after_value, rationale, params_json,
223
- result_summary_json, created_at, initiator
224
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
223
+ result_summary_json, created_at, initiator, actor
224
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
225
225
  """,
226
226
  (
227
227
  analysis_run_id, timeline_before, timeline_after, edit_type,
228
228
  tool_name, action_name, target_metric, metric_direction,
229
229
  before_value, after_value, rationale, params_json,
230
230
  result_json, created_at, initiator,
231
+ actor_identity.actor_string(),
231
232
  ),
232
233
  )
233
234
  row_id = cursor.lastrowid
@@ -22,7 +22,7 @@ import logging
22
22
  import time
23
23
  from typing import Any, Dict, List, Optional
24
24
 
25
- from src.utils import timeline_brain_db
25
+ from src.utils import actor_identity, timeline_brain_db
26
26
 
27
27
  logger = logging.getLogger("resolve-mcp.resolve-ai-ledger")
28
28
 
@@ -64,6 +64,7 @@ def record_op(
64
64
  output_path: Optional[str] = None,
65
65
  output_bytes: Optional[int] = None,
66
66
  error: Optional[str] = None,
67
+ actor: Optional[str] = None,
67
68
  ) -> Optional[int]:
68
69
  """Persist one ledger row. Returns the row id, or None on any failure.
69
70
 
@@ -81,8 +82,8 @@ def record_op(
81
82
  INSERT INTO resolve_ai_op_usage(
82
83
  op, op_class, clip_id, session_id, success, wall_clock_ms,
83
84
  output_path, output_bytes, extra_required, error,
84
- occurred_at, day_bucket
85
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
85
+ occurred_at, day_bucket, actor
86
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
86
87
  """,
87
88
  (
88
89
  op, meta["op_class"], clip_id, session_id, 1 if success else 0,
@@ -90,6 +91,7 @@ def record_op(
90
91
  int(output_bytes) if output_bytes is not None else None,
91
92
  meta["extra_required"], error,
92
93
  _iso(now), _day_bucket(now),
94
+ actor or actor_identity.actor_string(),
93
95
  ),
94
96
  )
95
97
  return cursor.lastrowid
@@ -177,7 +179,7 @@ def get_usage(
177
179
  rows = conn.execute(
178
180
  f"""
179
181
  SELECT op, op_class, clip_id, session_id, success, wall_clock_ms,
180
- output_path, output_bytes, extra_required, error, occurred_at
182
+ output_path, output_bytes, extra_required, error, occurred_at, actor
181
183
  FROM resolve_ai_op_usage{where}
182
184
  ORDER BY id DESC LIMIT ?
183
185
  """,
@@ -29,7 +29,7 @@ from typing import Callable, Dict, Iterator, Optional, Tuple
29
29
 
30
30
  logger = logging.getLogger("resolve-mcp.timeline-brain-db")
31
31
 
32
- SCHEMA_VERSION = 7
32
+ SCHEMA_VERSION = 8
33
33
  DB_FILENAME = "timeline_brain.sqlite"
34
34
  SOUL_DIRNAME = "_soul"
35
35
 
@@ -464,6 +464,20 @@ def _migrate_v7_resolve_ai_op_usage(conn: sqlite3.Connection) -> None:
464
464
  )
465
465
 
466
466
 
467
+ @register_migration(8)
468
+ def _migrate_v8_actor_identity(conn: sqlite3.Connection) -> None:
469
+ """Instance-level actor provenance (design decision 2026-06-09).
470
+
471
+ Stamps which process kind performed an op — "<instance>:<pid>" where
472
+ instance is stdio / network-sse / network-http / control-panel /
473
+ batch-cli. Complements `initiator` (auto vs manual) on the versioning
474
+ tables: initiator says WHY a row exists, actor says WHO wrote it.
475
+ """
476
+ for table in ("resolve_ai_op_usage", "brain_edits", "timeline_versions"):
477
+ if not _column_exists(conn, table, "actor"):
478
+ conn.execute(f"ALTER TABLE {table} ADD COLUMN actor TEXT")
479
+
480
+
467
481
  @register_migration(5)
468
482
  def _migrate_v5_analysis_token_usage(conn: sqlite3.Connection) -> None:
469
483
  """Track real vendor token + frame upload usage so caps can enforce budgets.
@@ -32,7 +32,7 @@ import time
32
32
  import uuid
33
33
  from typing import Any, Dict, List, Optional, Tuple
34
34
 
35
- from src.utils import timeline_brain_db
35
+ from src.utils import actor_identity, timeline_brain_db
36
36
 
37
37
  logger = logging.getLogger("resolve-mcp.timeline-versioning")
38
38
 
@@ -159,8 +159,8 @@ def archive_current_timeline(
159
159
  """
160
160
  INSERT INTO timeline_versions(
161
161
  timeline_name, version, created_at, analysis_run_id,
162
- archived_timeline_name, archived_bin_path, reason, initiator
163
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
162
+ archived_timeline_name, archived_bin_path, reason, initiator, actor
163
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
164
164
  """,
165
165
  (
166
166
  working_name,
@@ -171,6 +171,7 @@ def archive_current_timeline(
171
171
  _archive_bin_path(media_pool),
172
172
  reason,
173
173
  initiator,
174
+ actor_identity.actor_string(),
174
175
  ),
175
176
  )
176
177
  row_id = cursor.lastrowid