devin-internals-spec 0.3.0__py3-none-any.whl

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.
@@ -0,0 +1,20 @@
1
+ __version__ = "0.3.0"
2
+
3
+ from devin_internals.schema import (
4
+ LATEST_KNOWN_SCHEMA,
5
+ MIN_SUPPORTED_SCHEMA,
6
+ SchemaDetectionError,
7
+ SchemaError,
8
+ UnknownSchemaVersionError,
9
+ detect_schema_version,
10
+ )
11
+
12
+ __all__ = [
13
+ "LATEST_KNOWN_SCHEMA",
14
+ "MIN_SUPPORTED_SCHEMA",
15
+ "SchemaDetectionError",
16
+ "SchemaError",
17
+ "UnknownSchemaVersionError",
18
+ "detect_schema_version",
19
+ "__version__",
20
+ ]
devin_internals/cli.py ADDED
@@ -0,0 +1,339 @@
1
+ """``devin-inspect`` — thin CLI wrapper; all logic lives in the library.
2
+
3
+ Subcommands (all read-only, all accept ``--json``):
4
+
5
+ - ``schema <path>`` print the schema-detection contract as JSON
6
+ - ``sessions <sessions.db>`` list sessions (id, title, cwd, created_at, status)
7
+ - ``health <devin-data-dir>`` locate the three stores, report versions/counts
8
+ - ``make-fixture <out>`` write deterministic synthetic stores for tests
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import sqlite3
16
+ import sys
17
+ from dataclasses import asdict
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+ from typing import Any, Sequence
21
+
22
+ from devin_internals import fixtures
23
+ from devin_internals.contract import check_contract
24
+ from devin_internals.parsers import (
25
+ AcpMessagesStore,
26
+ SessionsStore,
27
+ StateVscdbStore,
28
+ )
29
+ from devin_internals.parsers.vscdb_scan import scan as vscdb_scan
30
+ from devin_internals.schema import SchemaError, detect_schema_version
31
+
32
+
33
+ def _print_json(payload: Any) -> None:
34
+ print(json.dumps(payload, indent=2, sort_keys=False))
35
+
36
+
37
+ def _ms_to_iso(ts: int | None) -> str:
38
+ if ts is None:
39
+ return "-"
40
+ return (
41
+ datetime.fromtimestamp(ts / 1000, tz=timezone.utc)
42
+ .isoformat(timespec="seconds")
43
+ .replace("+00:00", "Z")
44
+ )
45
+
46
+
47
+ def _fmt_table(headers: list[str], rows: list[list[str]]) -> str:
48
+ widths = [
49
+ max([len(h), *(len(r[i]) for r in rows)])
50
+ for i, h in enumerate(headers)
51
+ ]
52
+ line = " ".join(h.ljust(widths[i]) for i, h in enumerate(headers))
53
+ sep = " ".join("-" * widths[i] for i in range(len(headers)))
54
+ body = [
55
+ " ".join(r[i].ljust(widths[i]) for i in range(len(headers)))
56
+ for r in rows
57
+ ]
58
+ return "\n".join([line, sep, *body])
59
+
60
+
61
+ def _truncate(text: str, width: int) -> str:
62
+ return text if len(text) <= width else text[: width - 1] + "…"
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # subcommands
67
+ # ---------------------------------------------------------------------------
68
+
69
+
70
+ def cmd_schema(args: argparse.Namespace) -> int:
71
+ info = detect_schema_version(args.path)
72
+ _print_json(info)
73
+ return 0
74
+
75
+
76
+ def cmd_sessions(args: argparse.Namespace) -> int:
77
+ with SessionsStore(args.path) as store:
78
+ sessions = store.sessions(limit=args.limit)
79
+ if args.json:
80
+ _print_json(
81
+ [
82
+ {**asdict(s), "status": s.status}
83
+ for s in sessions
84
+ ]
85
+ )
86
+ return 0
87
+ rows = [
88
+ [
89
+ _truncate(s.id, 36),
90
+ _truncate(s.title or "-", 30),
91
+ _truncate(s.working_directory, 40),
92
+ _ms_to_iso(s.created_at),
93
+ s.status,
94
+ ]
95
+ for s in sessions
96
+ ]
97
+ if rows:
98
+ print(_fmt_table(["ID", "TITLE", "CWD", "CREATED_AT", "STATUS"], rows))
99
+ else:
100
+ print("no sessions")
101
+ return 0
102
+
103
+
104
+ def _health_sessions_db(path: Path) -> dict[str, Any]:
105
+ entry: dict[str, Any] = {"path": str(path), "exists": path.is_file()}
106
+ if not entry["exists"]:
107
+ return entry
108
+ try:
109
+ info = detect_schema_version(path)
110
+ with SessionsStore(path) as store:
111
+ entry["counts"] = store.counts()
112
+ entry["schema_version"] = info["schema_version"]
113
+ entry["supported"] = info["supported"]
114
+ except SchemaError as exc:
115
+ entry["error"] = str(exc)
116
+ return entry
117
+
118
+
119
+ def _health_acp_messages(dir_path: Path) -> dict[str, Any]:
120
+ entry: dict[str, Any] = {"dir": str(dir_path), "exists": dir_path.is_dir()}
121
+ if not entry["exists"]:
122
+ return entry
123
+ dbs = []
124
+ for db in sorted(dir_path.glob("*.db")):
125
+ item: dict[str, Any] = {"name": db.name, "path": str(db)}
126
+ try:
127
+ with AcpMessagesStore(db) as store:
128
+ item["counts"] = store.counts()
129
+ except SchemaError as exc:
130
+ item["error"] = str(exc)
131
+ dbs.append(item)
132
+ entry["dbs"] = dbs
133
+ entry["count"] = len(dbs)
134
+ return entry
135
+
136
+
137
+ def _health_state_vscdb(path: Path) -> dict[str, Any]:
138
+ entry: dict[str, Any] = {"path": str(path), "exists": path.is_file()}
139
+ if not entry["exists"]:
140
+ return entry
141
+ try:
142
+ with StateVscdbStore(path) as store:
143
+ entry["counts"] = store.counts()
144
+ entry["windsurf_keys"] = len(store.list_prefix())
145
+ except SchemaError as exc:
146
+ entry["error"] = str(exc)
147
+ return entry
148
+
149
+
150
+ def cmd_vscdb_scan(args: argparse.Namespace) -> int:
151
+ with StateVscdbStore(Path(args.path).expanduser()) as store:
152
+ report = vscdb_scan(store)
153
+ if args.json:
154
+ print(json.dumps(report, ensure_ascii=False, indent=2))
155
+ else:
156
+ print(f"keys: {report['total_keys']} · flagged: {report['flagged_keys']}")
157
+ for head, n in report["key_prefixes"].items():
158
+ print(f" {head}: {n}")
159
+ for k in report["keys"]:
160
+ if k.get("risk_flags"):
161
+ print(f" FLAG {k['key']} {k['value_kind']} "
162
+ f"len={k['value_len']} {','.join(k['risk_flags'])}")
163
+ print(f"({report['note']})")
164
+ return 1 if (report["flagged_keys"] and args.fail_on_flags) else 0
165
+
166
+
167
+ def cmd_health(args: argparse.Namespace) -> int:
168
+ root = Path(args.devin_data_dir).expanduser()
169
+ report = {
170
+ "root": str(root),
171
+ "exists": root.is_dir(),
172
+ "stores": {
173
+ "sessions_db": _health_sessions_db(root / "cli" / "sessions.db"),
174
+ "acp_messages": _health_acp_messages(
175
+ root / "User" / "acp-messages"
176
+ ),
177
+ "state_vscdb": _health_state_vscdb(
178
+ root / "User" / "globalStorage" / "state.vscdb"
179
+ ),
180
+ },
181
+ }
182
+ stores = report["stores"]
183
+ ok = report["exists"] and all(
184
+ s["exists"] and "error" not in s for s in stores.values()
185
+ )
186
+ report["ok"] = ok
187
+
188
+ if args.json:
189
+ _print_json(report)
190
+ else:
191
+ s = stores["sessions_db"]
192
+ detail = (
193
+ f"v{s['schema_version']} {s['counts']}"
194
+ if s.get("schema_version")
195
+ else s.get("error", "missing")
196
+ )
197
+ print(f"sessions.db {'OK' if s['exists'] else 'MISSING'} {detail}")
198
+ a = stores["acp_messages"]
199
+ detail = (
200
+ f"{a['count']} db file(s)"
201
+ if a.get("count") is not None
202
+ else a.get("error", "missing")
203
+ )
204
+ print(f"acp-messages {'OK' if a['exists'] else 'MISSING'} {detail}")
205
+ v = stores["state_vscdb"]
206
+ detail = (
207
+ f"{v['counts']} windsurf_keys={v['windsurf_keys']}"
208
+ if v.get("counts")
209
+ else v.get("error", "missing")
210
+ )
211
+ print(f"state.vscdb {'OK' if v['exists'] else 'MISSING'} {detail}")
212
+ print(f"overall: {'OK' if ok else 'DEGRADED'}")
213
+ return 0 if ok else 1
214
+
215
+
216
+ def cmd_contract(args: argparse.Namespace) -> int:
217
+ report = check_contract(args.devin_data_dir)
218
+ _print_json(report)
219
+ return 0 if report["status"] == "ok" else 1
220
+
221
+
222
+ def cmd_make_fixture(args: argparse.Namespace) -> int:
223
+ out = Path(args.output).expanduser()
224
+ created: dict[str, Any]
225
+ if args.kind == "dir":
226
+ created = fixtures.create_devin_data_dir(
227
+ out,
228
+ seed=args.seed,
229
+ n_acp_dbs=args.n_acp_dbs,
230
+ schema_version=args.schema_version,
231
+ n_sessions=args.n_sessions,
232
+ )
233
+ elif args.kind == "sessions":
234
+ created = {
235
+ "sessions_db": fixtures.create_sessions_db(
236
+ out,
237
+ schema_version=args.schema_version,
238
+ seed=args.seed,
239
+ n_sessions=args.n_sessions,
240
+ )
241
+ }
242
+ elif args.kind == "acp-messages":
243
+ created = {"acp_messages_db": fixtures.create_acp_messages_db(out, seed=args.seed)}
244
+ else:
245
+ created = {"state_vscdb": fixtures.create_state_vscdb(out, seed=args.seed)}
246
+ created["synthetic"] = True
247
+ created["seed"] = args.seed
248
+ _print_json({k: [str(p) for p in v] if isinstance(v, list) else str(v)
249
+ for k, v in created.items()})
250
+ return 0
251
+
252
+
253
+ # ---------------------------------------------------------------------------
254
+ # entry point
255
+ # ---------------------------------------------------------------------------
256
+
257
+
258
+ def build_parser() -> argparse.ArgumentParser:
259
+ parser = argparse.ArgumentParser(
260
+ prog="devin-inspect",
261
+ description="Read-only inspection of Devin's local session stores.",
262
+ )
263
+ sub = parser.add_subparsers(dest="command", required=True)
264
+
265
+ p = sub.add_parser("schema", help="print the schema-detection contract")
266
+ p.add_argument("path", help="path to a sessions.db")
267
+ p.add_argument("--json", action="store_true", help="JSON output (default)")
268
+ p.set_defaults(func=cmd_schema)
269
+
270
+ p = sub.add_parser("sessions", help="list sessions in a sessions.db")
271
+ p.add_argument("path", help="path to a sessions.db")
272
+ p.add_argument("--limit", type=int, default=None, metavar="N",
273
+ help="show at most N sessions")
274
+ p.add_argument("--json", action="store_true", help="JSON output")
275
+ p.set_defaults(func=cmd_sessions)
276
+
277
+ p = sub.add_parser("health", help="check a Devin data directory")
278
+ p.add_argument("devin_data_dir",
279
+ help="Devin data root (contains cli/ and User/)")
280
+ p.add_argument("--json", action="store_true", help="JSON output")
281
+ p.set_defaults(func=cmd_health)
282
+
283
+ p = sub.add_parser(
284
+ "make-fixture",
285
+ help="write deterministic synthetic Devin stores (for tests/demos)",
286
+ )
287
+ p.add_argument("output",
288
+ help="output dir (kind=dir) or file path (other kinds)")
289
+ p.add_argument("--kind", choices=["dir", "sessions", "acp-messages", "state-vscdb"],
290
+ default="dir", help="what to generate (default: full data dir)")
291
+ p.add_argument("--seed", type=int, default=fixtures.DEFAULT_SEED,
292
+ help="deterministic seed (default: %(default)s)")
293
+ p.add_argument("--schema-version", type=int,
294
+ default=fixtures.LATEST_KNOWN_SCHEMA,
295
+ help="sessions.db schema version; >17 exercises the "
296
+ "unknown-version path (default: %(default)s)")
297
+ p.add_argument("--n-sessions", type=int, default=3,
298
+ help="sessions to generate (default: %(default)s)")
299
+ p.add_argument("--n-acp-dbs", type=int, default=2,
300
+ help="acp-messages DBs in kind=dir (default: %(default)s)")
301
+ p.set_defaults(func=cmd_make_fixture)
302
+
303
+ p = sub.add_parser(
304
+ "vscdb-scan",
305
+ help="shape-only audit of a state.vscdb — key names, value "
306
+ "types/lengths, risk flags; never prints values (IS-1)",
307
+ )
308
+ p.add_argument("path", help="path to state.vscdb")
309
+ p.add_argument("--json", action="store_true", help="JSON output")
310
+ p.add_argument("--fail-on-flags", action="store_true",
311
+ help="exit 1 when any key has risk flags (CI mode)")
312
+ p.set_defaults(func=cmd_vscdb_scan)
313
+
314
+ p = sub.add_parser(
315
+ "contract",
316
+ help="unified drift contract: schema version, acp-messages meta, "
317
+ "usage shape — one report for all stores",
318
+ )
319
+ p.add_argument("devin_data_dir",
320
+ help="Devin data root (contains cli/ and User/)")
321
+ p.set_defaults(func=cmd_contract)
322
+
323
+ return parser
324
+
325
+
326
+ def main(argv: Sequence[str] | None = None) -> int:
327
+ args = build_parser().parse_args(argv)
328
+ try:
329
+ return args.func(args)
330
+ except SchemaError as exc:
331
+ print(f"error: {exc}", file=sys.stderr)
332
+ return 1
333
+ except (sqlite3.Error, OSError) as exc:
334
+ print(f"error: {exc}", file=sys.stderr)
335
+ return 1
336
+
337
+
338
+ if __name__ == "__main__":
339
+ raise SystemExit(main())
@@ -0,0 +1,100 @@
1
+ """Extract git commit references from ``tool_call_state`` payloads.
2
+
3
+ Devin sessions record shell/tool traffic as JSON blobs in
4
+ ``tool_call_state.tool_call_json`` (the call) and
5
+ ``tool_call_state.tool_call_update_json`` (streaming updates / results).
6
+ Commits a session touched — created, inspected, pushed, linked — surface
7
+ as 40-char hex SHAs or ``<host>/<owner>/<repo>/commit/<sha>`` URLs inside
8
+ those blobs.
9
+
10
+ Only full 40-hex SHAs are extracted. Short SHAs (7+) are ambiguous
11
+ (config hashes, truncated ids, random hex) and are deliberately ignored —
12
+ precision over recall, because these refs feed the knowledge graph.
13
+
14
+ Everything is pure stdlib, read-only over the parsed rows — pass in
15
+ ``SessionsStore.tool_call_state(...)`` results.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ from dataclasses import dataclass, field
22
+
23
+ # 40-hex git SHA-1 (also matches SHA-256 object ids — same length, hex).
24
+ _SHA_RE = re.compile(r"\b[0-9a-f]{40}\b")
25
+
26
+ # well-known /commit/<sha> or /commits/<sha> URL fragments carry the same
27
+ # 40-hex capture the plain regex already finds, but also name the repo.
28
+ _COMMIT_URL_RE = re.compile(
29
+ r"https?://[^\s\"'<>)]+?/commits?/([0-9a-f]{40})\b"
30
+ )
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class CommitRef:
35
+ """One commit reference found inside a tool-call payload."""
36
+
37
+ session_id: str
38
+ tool_call_id: str
39
+ field: str # "call" | "update"
40
+ sha: str
41
+ repo_url: str | None = None # e.g. "github.com/owner/repo" when a URL
42
+
43
+
44
+ def _refs_in_text(
45
+ text: str | None, session_id: str, tool_call_id: str, field_name: str
46
+ ) -> list[CommitRef]:
47
+ if not text:
48
+ return []
49
+ refs: list[CommitRef] = []
50
+ # URLs first so the SHA can carry its repo context
51
+ url_shas: dict[str, str] = {}
52
+ for m in _COMMIT_URL_RE.finditer(text):
53
+ sha = m.group(1)
54
+ repo = m.group(0).split("/commit")[0].split("://", 1)[-1]
55
+ url_shas.setdefault(sha, repo)
56
+ refs.append(
57
+ CommitRef(session_id, tool_call_id, field_name, sha, repo)
58
+ )
59
+ for m in _SHA_RE.finditer(text):
60
+ sha = m.group(0)
61
+ if sha in url_shas:
62
+ continue # already captured with repo context
63
+ refs.append(CommitRef(session_id, tool_call_id, field_name, sha))
64
+ return refs
65
+
66
+
67
+ def commit_references(tool_calls) -> list[CommitRef]:
68
+ """All commit refs in a ``ToolCallState`` iterable.
69
+
70
+ Accepts anything with ``session_id``, ``tool_call_id``,
71
+ ``tool_call_json`` and ``tool_call_update_json`` attributes — typically
72
+ ``SessionsStore.tool_call_state()`` output. Results are deduplicated
73
+ on ``(sha, tool_call_id)`` keeping the richer (URL-carrying) record.
74
+ """
75
+ seen: dict[tuple[str, str], CommitRef] = {}
76
+ for tc in tool_calls:
77
+ for ref in (
78
+ _refs_in_text(
79
+ tc.tool_call_json, tc.session_id, tc.tool_call_id, "call"
80
+ )
81
+ + _refs_in_text(
82
+ tc.tool_call_update_json,
83
+ tc.session_id,
84
+ tc.tool_call_id,
85
+ "update",
86
+ )
87
+ ):
88
+ key = (ref.sha, ref.tool_call_id)
89
+ prev = seen.get(key)
90
+ if prev is None or (prev.repo_url is None and ref.repo_url):
91
+ seen[key] = ref
92
+ return sorted(seen.values(), key=lambda r: (r.session_id, r.sha))
93
+
94
+
95
+ def commits_by_session(tool_calls) -> dict[str, set[str]]:
96
+ """Convenience rollup: ``session_id -> {sha, ...}``."""
97
+ out: dict[str, set[str]] = {}
98
+ for ref in commit_references(tool_calls):
99
+ out.setdefault(ref.session_id, set()).add(ref.sha)
100
+ return out
@@ -0,0 +1,231 @@
1
+ """Unified drift contract (F4): one call, every known contract boundary.
2
+
3
+ Checks, in one pass over a Devin data directory:
4
+
5
+ - ``sessions.db`` schema version against the supported range
6
+ - ``acp-messages/*.db`` meta ``schema_version`` (observed: 6) and the
7
+ declared ``info``/``truncated`` keys
8
+ - usage-shape probe — what token/cost signals actually persist locally.
9
+ Verified on a real install (2026-10): ``tool_call_state`` carries **no**
10
+ cost/usage columns; ``message_nodes.metadata`` carries only
11
+ ``num_tokens_preceding`` (context-window tokens, cumulative); per-turn
12
+ cost exists **only** in the live ACP session meta
13
+ (``total_credit_cost``/``total_acu_cost``) and is never written to disk.
14
+ - ``state.vscdb`` presence
15
+
16
+ Every check returns ``ok``/``drift``/``unknown`` — a ``drift`` means the
17
+ stored shape differs from what downstream tools assume and they should
18
+ fail loudly rather than misread.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import sqlite3
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ from devin_internals.schema import (
29
+ SchemaError,
30
+ detect_schema_version,
31
+ )
32
+
33
+ # Observed on a real install (2026-10): 47 DBs at v6, 2 legacy DBs at v1.
34
+ KNOWN_ACP_SCHEMA_VERSIONS = {1, 6}
35
+
36
+ _EXPECTED_META_KEYS = {"info", "message_count", "schema_version", "truncated"}
37
+
38
+ # Verified on a real v17 install: tool_call_state has no cost/usage keys and
39
+ # message_nodes.metadata carries only these documented keys.
40
+ _KNOWN_MN_METADATA_KEYS = {
41
+ "summarized_from",
42
+ "num_tokens_preceding",
43
+ "is_system_prefix",
44
+ "extensions",
45
+ }
46
+ _COST_HINT_KEYS = ("cost", "usage", "acu", "credit", "price", "billing")
47
+
48
+
49
+ def _ro(path: Path) -> sqlite3.Connection:
50
+ return sqlite3.connect(f"file:{path.as_posix()}?mode=ro", uri=True)
51
+
52
+
53
+ def _check_sessions_db(path: Path) -> dict[str, Any]:
54
+ if not path.is_file():
55
+ return {"status": "missing", "path": str(path)}
56
+ try:
57
+ info = detect_schema_version(path)
58
+ except SchemaError as exc:
59
+ return {"status": "drift", "path": str(path), "error": str(exc)}
60
+ return {
61
+ "status": "ok" if info["supported"] else "drift",
62
+ "path": str(path),
63
+ "schema_version": info["schema_version"],
64
+ "supported_range": [info["min_supported"], info["max_supported"]],
65
+ }
66
+
67
+
68
+ def _check_acp_messages(dir_path: Path) -> dict[str, Any]:
69
+ if not dir_path.is_dir():
70
+ return {"status": "missing", "dir": str(dir_path)}
71
+ dbs = sorted(dir_path.glob("*.db"))
72
+ versions: dict[str, int] = {}
73
+ meta_keys: dict[str, int] = {}
74
+ for db in dbs:
75
+ try:
76
+ con = _ro(db)
77
+ for key, value in con.execute("SELECT key, value FROM meta"):
78
+ meta_keys[key] = meta_keys.get(key, 0) + 1
79
+ if key == "schema_version":
80
+ try:
81
+ v = int(value)
82
+ except (TypeError, ValueError):
83
+ v = value
84
+ versions[v] = versions.get(v, 0) + 1
85
+ con.close()
86
+ except sqlite3.Error:
87
+ continue
88
+ unknown_versions = sorted(set(versions) - KNOWN_ACP_SCHEMA_VERSIONS,
89
+ key=str)
90
+ # ``fixture.*`` keys are the declared self-identification marker of
91
+ # synthetic stores — expected, not drift.
92
+ unexpected_meta = sorted(
93
+ k for k in meta_keys
94
+ if k not in _EXPECTED_META_KEYS and not k.startswith("fixture.")
95
+ )
96
+ status = "ok"
97
+ if unknown_versions or unexpected_meta:
98
+ status = "drift"
99
+ if not dbs:
100
+ status = "missing"
101
+ return {
102
+ "status": status,
103
+ "dir": str(dir_path),
104
+ "db_count": len(dbs),
105
+ "schema_versions": {str(k): v for k, v in versions.items()},
106
+ "unknown_schema_versions": unknown_versions,
107
+ "unexpected_meta_keys": unexpected_meta,
108
+ }
109
+
110
+
111
+ def _cost_keys_in(obj: Any, out: set[str]) -> None:
112
+ if isinstance(obj, dict):
113
+ for key, value in obj.items():
114
+ lowered = key.lower()
115
+ if any(h in lowered for h in _COST_HINT_KEYS):
116
+ out.add(key)
117
+ _cost_keys_in(value, out)
118
+ elif isinstance(obj, list):
119
+ for item in obj[:8]:
120
+ _cost_keys_in(item, out)
121
+
122
+
123
+ def _check_usage_shape(sessions_db: Path) -> dict[str, Any]:
124
+ """What token/cost signals actually persist — verified 2026-10 on v17."""
125
+ if not sessions_db.is_file():
126
+ return {"status": "missing"}
127
+ con = _ro(sessions_db)
128
+ try:
129
+ tables = {
130
+ r[0]
131
+ for r in con.execute(
132
+ "SELECT name FROM sqlite_master WHERE type='table'"
133
+ )
134
+ }
135
+ cost_keys: set[str] = set()
136
+ if "tool_call_state" in tables:
137
+ for (tcc,) in con.execute(
138
+ "SELECT tool_call_update_json FROM tool_call_state "
139
+ "WHERE tool_call_update_json IS NOT NULL LIMIT 500"
140
+ ):
141
+ try:
142
+ _cost_keys_in(json.loads(tcc), cost_keys)
143
+ except (TypeError, json.JSONDecodeError):
144
+ continue
145
+ metadata_keys: set[str] = set()
146
+ num_tokens = 0
147
+ if "message_nodes" in tables:
148
+ for (md,) in con.execute(
149
+ "SELECT metadata FROM message_nodes "
150
+ "WHERE metadata IS NOT NULL LIMIT 1000"
151
+ ):
152
+ try:
153
+ keys = set(json.loads(md))
154
+ except (TypeError, json.JSONDecodeError):
155
+ continue
156
+ metadata_keys |= keys
157
+ if "num_tokens_preceding" in keys:
158
+ num_tokens += 1
159
+ # "synthetic" is the declared marker written by make-fixture.
160
+ unexpected = sorted(
161
+ metadata_keys - _KNOWN_MN_METADATA_KEYS - {"synthetic"}
162
+ )
163
+ return {
164
+ "status": "drift" if cost_keys or unexpected else "ok",
165
+ "cost_keys_in_tool_calls": sorted(cost_keys),
166
+ "cost_persisted": bool(cost_keys),
167
+ "num_tokens_preceding_rows": num_tokens,
168
+ "unexpected_metadata_keys": unexpected,
169
+ "note": (
170
+ "per-turn cost lives only in the live ACP session meta "
171
+ "(total_credit_cost/total_acu_cost); local stores persist "
172
+ "num_tokens_preceding as the sole token signal"
173
+ ),
174
+ }
175
+ finally:
176
+ con.close()
177
+
178
+
179
+ def _gui_root(data_root: Path) -> Path:
180
+ """Locate the ``User/`` tree.
181
+
182
+ Fixture-style layouts keep ``User/`` under the data root; real Linux
183
+ installs split stores across ``~/.local/share/devin/cli/`` (data) and
184
+ ``~/.config/Devin/User/`` (GUI). Windows/macOS keep them under
185
+ ``%APPDATA%``/``~/Library/Application Support``.
186
+ """
187
+ if (data_root / "User").is_dir():
188
+ return data_root
189
+ import os
190
+ import sys
191
+
192
+ if sys.platform.startswith("win"):
193
+ base = Path(os.environ.get("APPDATA", "")) / "Devin"
194
+ elif sys.platform == "darwin":
195
+ base = Path.home() / "Library" / "Application Support" / "Devin"
196
+ else:
197
+ base = Path(
198
+ os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
199
+ ) / "Devin"
200
+ return base
201
+
202
+
203
+ def check_contract(devin_data_dir: str | Path) -> dict[str, Any]:
204
+ """Run every contract check and return the unified drift report."""
205
+ root = Path(devin_data_dir).expanduser()
206
+ gui_root = _gui_root(root)
207
+ sessions_db = root / "cli" / "sessions.db"
208
+ checks = {
209
+ "sessions_db": _check_sessions_db(sessions_db),
210
+ "acp_messages": _check_acp_messages(gui_root / "User" / "acp-messages"),
211
+ "usage_shape": _check_usage_shape(sessions_db),
212
+ "state_vscdb": {
213
+ "status": (
214
+ "ok"
215
+ if (gui_root / "User" / "globalStorage" / "state.vscdb").is_file()
216
+ else "missing"
217
+ ),
218
+ "path": str(gui_root / "User" / "globalStorage" / "state.vscdb"),
219
+ },
220
+ }
221
+ worst = "ok"
222
+ if any(c["status"] == "drift" for c in checks.values()):
223
+ worst = "drift"
224
+ elif any(c["status"] == "missing" for c in checks.values()):
225
+ worst = "missing"
226
+ return {
227
+ "root": str(root),
228
+ "gui_root": str(gui_root),
229
+ "status": worst,
230
+ "checks": checks,
231
+ }