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.
- devin_internals/__init__.py +20 -0
- devin_internals/cli.py +339 -0
- devin_internals/commits.py +100 -0
- devin_internals/contract.py +231 -0
- devin_internals/fixtures.py +365 -0
- devin_internals/parsers/__init__.py +35 -0
- devin_internals/parsers/_common.py +37 -0
- devin_internals/parsers/acp_messages.py +123 -0
- devin_internals/parsers/sessions.py +282 -0
- devin_internals/parsers/state_vscdb.py +74 -0
- devin_internals/parsers/vscdb_scan.py +108 -0
- devin_internals/projects.py +79 -0
- devin_internals/schema.py +111 -0
- devin_internals_spec-0.3.0.dist-info/METADATA +217 -0
- devin_internals_spec-0.3.0.dist-info/RECORD +19 -0
- devin_internals_spec-0.3.0.dist-info/WHEEL +5 -0
- devin_internals_spec-0.3.0.dist-info/entry_points.txt +2 -0
- devin_internals_spec-0.3.0.dist-info/licenses/LICENSE +21 -0
- devin_internals_spec-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
}
|