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 +20 -0
- package/README.md +1 -1
- package/docs/SKILL.md +7 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/analysis_dashboard.py +2 -0
- package/src/batch_cli.py +2 -0
- package/src/granular/common.py +1 -1
- package/src/server.py +84 -10
- package/src/utils/actor_identity.py +46 -0
- package/src/utils/brain_edits.py +4 -3
- package/src/utils/resolve_ai_ledger.py +6 -4
- package/src/utils/timeline_brain_db.py +15 -1
- package/src/utils/timeline_versioning.py +4 -3
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
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
4
4
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
5
5
|
[](docs/reference/api-coverage.md)
|
|
6
6
|
[-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={...})
|
|
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
|
+
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
|
@@ -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"):
|
package/src/granular/common.py
CHANGED
|
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
|
|
|
80
80
|
handlers=[logging.StreamHandler()],
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
VERSION = "2.
|
|
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.
|
|
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
|
|
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} —
|
|
14612
|
-
set_ai_governance(preset
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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()}"
|
package/src/utils/brain_edits.py
CHANGED
|
@@ -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 =
|
|
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
|