gabriel / musehub public
musehub_wire_push.py python
857 lines 33.9 KB Hotspot Blast
Raw
sha256:7d6dd8f4a89e2d1fef2d84f6e65feaff51385d382f466766b7f690a22ec18e32 fix: fall back to DB ancestry check when mpack-only fast-fo… Sonnet 4.6 patch 43 days ago
1 """Push path — wire_refs, wire_repair_*, wire_push_mpack_presign, wire_push_unpack_mpack."""
2
3 import asyncio
4 import collections
5 import hashlib
6 import logging
7 import msgpack as _msgpack
8 import time as _time_module
9 from datetime import datetime, timezone
10
11 from sqlalchemy import func, select, text as _sa_text
12 from sqlalchemy.dialects.postgresql import insert as _pg_insert
13 from sqlalchemy.ext.asyncio import AsyncSession
14
15 from musehub.db.musehub_abuse_models import MusehubBlockedHash, MusehubDailyPushBytes, MusehubPushAnomaly
16 from musehub.db.musehub_collaborator_models import MusehubCollaborator
17 from musehub.db.musehub_repo_models import (
18 MusehubBranch,
19 MusehubCommit,
20 MusehubCommitGraph,
21 MusehubCommitRef,
22 MusehubObject,
23 MusehubObjectRef,
24 MusehubMPackIndex,
25 MusehubRepo,
26 MusehubSnapshot,
27 MusehubSnapshotRef,
28 )
29 from musehub.models.wire import WireCommit, WireRefsResponse
30 from muse.core.types import blob_id, split_id
31 from musehub.core.genesis import compute_branch_id
32 from musehub.types.json_types import IntDict, JSONObject, JSONValue, StrDict
33 from musehub.config import settings
34 from musehub.storage import get_backend
35 from musehub.storage.backends import read_object_bytes
36
37 from musehub.services.musehub_wire_shared import (
38 MPackValidationError,
39 NonFastForwardError,
40 ObjectHashMismatch,
41 RepairResult,
42 _ChildMap,
43 _is_fast_forward,
44 _reconstruct_manifest,
45 _snap_row_to_wire,
46 _to_wire_commit,
47 _upsert_object_refs,
48 _utc_now,
49 _parse_iso,
50 _str_list,
51 _int_safe,
52 logger,
53 )
54
55 _ObjFetchMap = dict[str, tuple[str, str, str | None, bytes | None]]
56 _SnapshotManifest = dict[str, str]
57 _EMPTY_OID = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
58 _COMMIT_BATCH = 50
59
60
61 async def wire_refs(
62 session: AsyncSession,
63 repo_id: str,
64 ) -> WireRefsResponse | None:
65 _t0 = _time_module.perf_counter()
66 def _ms() -> float:
67 return (_time_module.perf_counter() - _t0) * 1000
68
69 logger.info("[wire_refs] START repo_id=%s", repo_id)
70
71 repo_row = await session.get(MusehubRepo, repo_id)
72 logger.info("[wire_refs] repo_row loaded found=%s t=%.1fms", repo_row is not None, _ms())
73 if repo_row is None:
74 return None
75
76 branch_rows = (
77 await session.execute(
78 select(MusehubBranch).where(MusehubBranch.repo_id == repo_id)
79 )
80 ).scalars().all()
81 logger.info("[wire_refs] branches loaded count=%d t=%.1fms", len(branch_rows), _ms())
82
83 branch_heads: StrDict = {
84 b.name: b.head_commit_id
85 for b in branch_rows
86 if b.head_commit_id
87 }
88
89 domain_meta: JSONObject = (
90 repo_row.domain_meta if isinstance(repo_row.domain_meta, dict) else {}
91 )
92 domain = str(domain_meta.get("domain", "code"))
93 default_branch = getattr(repo_row, "default_branch", None) or "main"
94
95 logger.info("[wire_refs] RETURN branches=%d TOTAL=%.1fms", len(branch_heads), _ms())
96 return WireRefsResponse(
97 repo_id=repo_id,
98 domain=domain,
99 default_branch=default_branch,
100 branch_heads=branch_heads,
101 )
102
103 async def wire_repair_object(
104 session: AsyncSession,
105 repo_id: str,
106 object_id: str,
107 content: bytes,
108 caller_id: str | None,
109 ) -> RepairResult:
110 repo_row = await session.get(MusehubRepo, repo_id)
111 if repo_row is None:
112 raise ValueError("repo not found")
113
114 if not caller_id:
115 raise PermissionError("repair rejected: unauthenticated")
116 if caller_id != repo_row.owner:
117 collab_row = (await session.execute(
118 select(MusehubCollaborator).where(
119 MusehubCollaborator.repo_id == repo_id,
120 MusehubCollaborator.identity_handle == caller_id,
121 MusehubCollaborator.accepted_at.isnot(None),
122 MusehubCollaborator.permission.in_(["write", "admin"]),
123 )
124 )).scalar_one_or_none()
125 if collab_row is None:
126 raise PermissionError("repair rejected: not authorized")
127
128 actual_id = blob_id(content)
129 if actual_id != object_id:
130 _, expected_hex = split_id(object_id)
131 _, actual = split_id(actual_id)
132 raise ObjectHashMismatch(
133 f"repair content hash mismatch for {object_id!r}: "
134 f"declared={expected_hex[:16]}… actual={actual[:16]}…"
135 )
136
137 backend = get_backend()
138 uri = await backend.put(object_id, content)
139
140 await session.merge(MusehubObject(
141 object_id=object_id, path="", size_bytes=len(content),
142 storage_uri=uri, content_cache=None,
143 ))
144 await _upsert_object_refs(session, repo_id, [object_id])
145 await session.commit()
146
147 logger.info(
148 "✅ repair-object repo=%s object_id=%s size=%d",
149 repo_id, object_id, len(content),
150 )
151 return {"repaired": True}
152
153 async def wire_fetch_objects(
154 session: AsyncSession,
155 repo_id: str,
156 object_ids: list[str],
157 ) -> list[dict]:
158 result: list[dict] = []
159 for oid in object_ids:
160 obj_row = await session.get(MusehubObject, oid)
161 if obj_row is None:
162 continue
163 content: bytes | None = await read_object_bytes(obj_row)
164 if content is None:
165 continue
166 result.append({"object_id": oid, "content": content})
167 return result
168
169 async def wire_repair_snapshot(
170 session: AsyncSession,
171 repo_id: str,
172 snapshot_id: str,
173 manifest: dict[str, str],
174 directories: list[str],
175 caller_id: str | None,
176 ) -> RepairResult:
177 from musehub.muse_cli.snapshot import compute_snapshot_id
178
179 repo_row = await session.get(MusehubRepo, repo_id)
180 if repo_row is None:
181 raise ValueError("repo not found")
182
183 if not caller_id:
184 raise PermissionError("repair rejected: unauthenticated")
185 if caller_id != repo_row.owner:
186 collab_row = (await session.execute(
187 select(MusehubCollaborator).where(
188 MusehubCollaborator.repo_id == repo_id,
189 MusehubCollaborator.identity_handle == caller_id,
190 MusehubCollaborator.accepted_at.isnot(None),
191 MusehubCollaborator.permission.in_(["write", "admin"]),
192 )
193 )).scalar_one_or_none()
194 if collab_row is None:
195 raise PermissionError("repair rejected: not authorized")
196
197 recomputed = compute_snapshot_id(manifest, directories or [])
198 if recomputed != snapshot_id:
199 raise ObjectHashMismatch(
200 f"repair snapshot hash mismatch for {snapshot_id!r}: "
201 f"declared={snapshot_id}… recomputed={recomputed}…"
202 )
203
204 import msgpack as _msgpack
205 blob = _msgpack.packb(manifest, use_bin_type=True)
206 dirs_sorted = sorted(directories) if directories else []
207 await session.merge(MusehubSnapshot(
208 snapshot_id=snapshot_id,
209 directories=dirs_sorted,
210 manifest_blob=blob,
211 entry_count=len(manifest),
212 created_at=_utc_now(),
213 ))
214 await session.execute(
215 _pg_insert(MusehubSnapshotRef)
216 .values([{"repo_id": repo_id, "snapshot_id": snapshot_id, "created_at": _utc_now()}])
217 .on_conflict_do_nothing(index_elements=["repo_id", "snapshot_id"])
218 )
219 await session.commit()
220
221 logger.info(
222 "✅ repair-snapshot repo=%s snapshot_id=%s entries=%d",
223 repo_id, snapshot_id, len(manifest),
224 )
225 return {"repaired": True}
226
227 async def _fetch_commit(
228 session: AsyncSession,
229 commit_id: str,
230 ) -> MusehubCommit | None:
231 return await session.get(MusehubCommit, commit_id)
232
233 _ObjFetchMap = dict[str, tuple[str, str, str | None, bytes | None]]
234
235 async def _fetch_object_meta(
236 session: AsyncSession,
237 repo_id: str,
238 object_ids: list[str],
239 ) -> _ObjFetchMap:
240 result: _ObjFetchMap = {}
241 if _EMPTY_OID in object_ids:
242 result[_EMPTY_OID] = (repo_id, "", None, None)
243 remaining = [oid for oid in object_ids if oid != _EMPTY_OID]
244 if remaining:
245 obj_rows_q = await session.execute(
246 select(MusehubObject).where(MusehubObject.object_id.in_(remaining))
247 )
248 result.update({
249 r.object_id: (repo_id, r.path or "", r.storage_uri, r.content_cache)
250 for r in obj_rows_q.scalars().all()
251 })
252 return result
253
254 def _topological_sort(commits: list[WireCommit]) -> list[WireCommit]:
255 if not commits:
256 return []
257 by_id = {c.commit_id: c for c in commits}
258 in_degree: IntDict = {c.commit_id: 0 for c in commits}
259 children: _ChildMap = {c.commit_id: [] for c in commits}
260
261 for c in commits:
262 for pid in filter(None, [c.parent_commit_id, c.parent2_commit_id]):
263 if pid in by_id:
264 in_degree[c.commit_id] += 1
265 children[pid].append(c.commit_id)
266
267 queue: collections.deque[str] = collections.deque(
268 cid for cid, deg in in_degree.items() if deg == 0
269 )
270 result: list[WireCommit] = []
271 while queue:
272 cid = queue.popleft()
273 result.append(by_id[cid])
274 for child_id in children.get(cid, []):
275 in_degree[child_id] -= 1
276 if in_degree[child_id] == 0:
277 queue.append(child_id)
278
279 sorted_ids = {c.commit_id for c in result}
280 for c in commits:
281 if c.commit_id not in sorted_ids:
282 result.append(c)
283
284 return result
285
286 async def record_mpack_bytes_uploaded(
287 session: AsyncSession,
288 identity_id: str,
289 size_bytes: int,
290 ) -> None:
291 import datetime as _dt
292 today = _dt.date.today()
293 await session.execute(
294 _pg_insert(MusehubDailyPushBytes)
295 .values(identity_id=identity_id, date=today, bytes_uploaded=size_bytes)
296 .on_conflict_do_update(
297 index_elements=["identity_id", "date"],
298 set_={"bytes_uploaded": MusehubDailyPushBytes.bytes_uploaded + size_bytes},
299 )
300 )
301
302
303 async def check_push_anomaly(
304 session: AsyncSession,
305 identity_id: str,
306 bytes_today: int,
307 ) -> bool:
308 import datetime as _dt
309 import uuid as _uuid
310
311 today = _dt.date.today()
312 cutoff = today - _dt.timedelta(days=30)
313
314 result = await session.execute(
315 select(func.avg(MusehubDailyPushBytes.bytes_uploaded)).where(
316 MusehubDailyPushBytes.identity_id == identity_id,
317 MusehubDailyPushBytes.date >= cutoff,
318 MusehubDailyPushBytes.date < today,
319 )
320 )
321 avg_bytes = result.scalar()
322 if avg_bytes is None or avg_bytes == 0:
323 return False
324
325 ratio = bytes_today / float(avg_bytes)
326 if ratio <= 10.0:
327 return False
328
329 anomaly_id = "sha256:" + hashlib.sha256(
330 f"{identity_id}:{today.isoformat()}:{bytes_today}".encode()
331 ).hexdigest()
332 logger.warning(
333 "[anomaly] push spike detected identity_id=%s bytes_today=%d avg_bytes=%.0f ratio=%.1fx",
334 identity_id[:20], bytes_today, avg_bytes, ratio,
335 )
336 session.add(MusehubPushAnomaly(
337 anomaly_id=anomaly_id,
338 identity_id=identity_id,
339 bytes_today=bytes_today,
340 rolling_avg_bytes=float(avg_bytes),
341 ratio=ratio,
342 ))
343 return True
344
345
346 async def wire_push_mpack_presign(
347 mpack_key: str,
348 size_bytes: int,
349 ttl_seconds: int = 3600,
350 ) -> JSONObject:
351 logger.info("[mpack-presign] START mpack_key=%s size_bytes=%d ttl=%ds", mpack_key[:20], size_bytes, ttl_seconds)
352 backend = get_backend()
353 logger.info("[mpack-presign] calling presign_mpack_put …")
354 upload_url = await backend.presign_mpack_put(mpack_key, ttl_seconds)
355 logger.info("[mpack-presign] DONE upload_url=%s…", upload_url[:60])
356 return {"upload_url": upload_url, "mpack_key": mpack_key}
357
358
359 async def wire_push_unpack_mpack(
360 session: AsyncSession,
361 repo_id: str,
362 mpack_key: str,
363 pusher_id: str | None,
364 branch: str = "main",
365 head_commit_id: str = "",
366 commits_count: int = 0,
367 objects_count: int = 0,
368 force: bool = False,
369 ) -> JSONObject:
370 t0 = _time_module.monotonic()
371 def _ms(ref: float = 0.0) -> float:
372 return (_time_module.monotonic() - (t0 if ref == 0.0 else ref)) * 1000
373
374 backend = get_backend()
375
376 logger.warning(
377 "[SERVER step 0] receive repo=%s branch=%s head=%s",
378 repo_id[:20], branch, head_commit_id[:20] if head_commit_id else "none",
379 )
380
381 # step 1: fetch mpack bytes from MinIO by mpack_key
382 wire_bytes = await backend.get_mpack(mpack_key)
383 t_got = _time_module.monotonic()
384 if not wire_bytes:
385 raise ValueError(f"mpack {mpack_key[:20]} not found in storage")
386 mpack_bytes_len = len(wire_bytes)
387 logger.warning("[SERVER step 1] fetch mpack from object store %.1fms size=%.2f MB", _ms(), mpack_bytes_len / 1_048_576)
388
389 from musehub.config import get_settings as _get_settings
390 _max_bytes = _get_settings().mpack_max_bytes
391 if mpack_bytes_len > _max_bytes:
392 raise ValueError(
393 f"mpack {mpack_key[:20]} size {mpack_bytes_len:,} bytes "
394 f"exceeds limit {_max_bytes:,} bytes"
395 )
396
397 # step 2: verify blob_id(bytes) == mpack_key
398 actual_id = blob_id(wire_bytes)
399 if actual_id != mpack_key:
400 _, expected_hex = split_id(mpack_key)
401 _, got_hex = split_id(actual_id)
402 raise ValueError(
403 f"mpack integrity failure: sha256={got_hex[:20]}… ≠ key={expected_hex[:20]}…"
404 )
405 t_verify = _time_module.monotonic()
406 logger.warning("[SERVER step 2] verify sha256(bytes)==mpack_key %.1fms", (t_verify - t_got) * 1000)
407
408 # ── 2. Inline index ───────────────────────────────────────────────────────
409 import zstandard as _zstd_sync
410
411 if wire_bytes[:4] != b"MUSE":
412 raise ValueError(f"mpack is not MUSE binary format (got {wire_bytes[:4]!r})")
413 from muse.core.mpack import parse_wire_mpack as _parse_wire_mpack
414 _mpack_sync: JSONObject = _parse_wire_mpack(wire_bytes)
415 t_parse = _time_module.monotonic()
416 _raw_objs: list[JSONValue] = _mpack_sync.get("objects") or []
417 _raw_snaps: list[JSONValue] = _mpack_sync.get("snapshots") or []
418 _raw_commits_inline: list[JSONValue] = _mpack_sync.get("commits") or []
419 logger.warning("[SERVER step 3+4] verify MUSE magic + parse %.1fms commits=%d snapshots=%d objects=%d",
420 (t_parse - t_verify) * 1000, len(_raw_commits_inline), len(_raw_snaps), len(_raw_objs))
421
422 # 2b. zstd decompress all objects — zip bomb guard runs inline
423 _dctx_sync = _zstd_sync.ZstdDecompressor()
424 _decompressed_sync: list[tuple[str, bytes]] = []
425 _total_obj_bytes = 0
426 _max_decompressed = settings.mpack_max_decompressed_bytes
427 for _obj in _raw_objs:
428 _oid = _obj["object_id"]
429 _raw = _obj.get("content") or b""
430 if _obj.get("encoding") == "zstd" and _raw:
431 _raw = _dctx_sync.decompress(_raw)
432 _total_obj_bytes += len(_raw)
433 if _total_obj_bytes > _max_decompressed:
434 _zip_err = MPackValidationError(
435 f"decompressed size {_total_obj_bytes:,} exceeds limit "
436 f"{_max_decompressed:,} — possible zip bomb"
437 )
438 await backend.quarantine_mpack(mpack_key)
439 raise _zip_err
440 _decompressed_sync.append((_oid, _raw))
441 t_decomp = _time_module.monotonic()
442 logger.warning("[SERVER step 5] decompress + zip-bomb check %.1fms objects=%d uncompressed=%.2f MB",
443 (t_decomp - t_parse) * 1000, len(_decompressed_sync), _total_obj_bytes / 1_048_576)
444
445 # step 6: check blocked hashes
446 if _decompressed_sync:
447 _all_check_oids = [_oid for _oid, _ in _decompressed_sync]
448 _blocked_oids = (await session.execute(
449 select(MusehubBlockedHash.object_id)
450 .where(MusehubBlockedHash.object_id.in_(_all_check_oids))
451 )).scalars().all()
452 if _blocked_oids:
453 _blocked_err = MPackValidationError(
454 f"mpack contains {len(_blocked_oids)} blocked object(s): "
455 + ", ".join(oid[:20] for oid in _blocked_oids[:3])
456 + ("..." if len(_blocked_oids) > 3 else "")
457 )
458 await backend.quarantine_mpack(mpack_key)
459 raise _blocked_err
460 logger.warning("[SERVER step 6] check blocked hashes objects=%d blocked=0", len(_decompressed_sync))
461
462 # step 7: write objects
463 _new_oids: list[str] = []
464 if _decompressed_sync:
465 _cc_oids = [_oid for _oid, _ in _decompressed_sync]
466
467 t_obj_sel0 = _time_module.monotonic()
468 _existing_oids = set((await session.execute(
469 _sa_text("SELECT object_id FROM musehub_objects WHERE object_id = ANY(:ids)"),
470 {"ids": _cc_oids},
471 )).scalars())
472 t_obj_sel1 = _time_module.monotonic()
473 logger.warning("[SERVER step 7a] objects: dedup SELECT %.1fms total=%d existing=%d new=%d",
474 (t_obj_sel1 - t_obj_sel0) * 1000, len(_cc_oids), len(_existing_oids),
475 len(_cc_oids) - len(_existing_oids))
476
477 session.add_all([
478 MusehubObject(object_id=_oid, path="", size_bytes=len(_data),
479 storage_uri=f"mpack://{mpack_key}", content_cache=None)
480 for _oid, _data in _decompressed_sync
481 if _oid not in _existing_oids
482 ])
483 t_obj_add = _time_module.monotonic()
484 logger.warning("[SERVER step 7b] objects: INSERT new %.1fms", (t_obj_add - t_obj_sel1) * 1000)
485
486 _new_oids = [_oid for _oid in _cc_oids if _oid not in _existing_oids]
487 t_ref0 = _time_module.monotonic()
488 await _upsert_object_refs(session, repo_id, _new_oids)
489 t_ref1 = _time_module.monotonic()
490 logger.warning("[SERVER step 7c] objects: UPSERT object_refs %.1fms oids=%d",
491 (t_ref1 - t_ref0) * 1000, len(_new_oids))
492
493 _new_oids_for_idx = [_oid for _oid in _cc_oids if _oid not in _existing_oids]
494 if _new_oids_for_idx:
495 session.add_all([
496 MusehubMPackIndex(entity_id=_oid, mpack_id=mpack_key,
497 entity_type="object", created_at=_utc_now())
498 for _oid in _new_oids_for_idx
499 ])
500 t_idx_add = _time_module.monotonic()
501 logger.warning("[SERVER step 7d] objects: INSERT mpack_index %.1fms new=%d",
502 (t_idx_add - t_ref1) * 1000, len(_new_oids_for_idx))
503 else:
504 t_idx_add = t_decomp
505
506 # step 7: write snapshots
507 _snap_ids_in_mpack = {_sd.get("snapshot_id") for _sd in _raw_snaps if _sd.get("snapshot_id")}
508 _external_parent_sids = {
509 _sd.get("parent_snapshot_id")
510 for _sd in _raw_snaps
511 if _sd.get("parent_snapshot_id") and _sd.get("parent_snapshot_id") not in _snap_ids_in_mpack
512 }
513 _parent_snap_manifests: dict[str, dict] = {}
514 t_snap_ext0 = _time_module.monotonic()
515 if _external_parent_sids:
516 _psnap_rows = (await session.execute(
517 select(MusehubSnapshot.snapshot_id, MusehubSnapshot.manifest_blob)
518 .where(MusehubSnapshot.snapshot_id.in_(_external_parent_sids))
519 )).all()
520 for _psid, _pblob in _psnap_rows:
521 if _pblob:
522 _parent_snap_manifests[_psid] = dict(_msgpack.unpackb(_pblob, raw=False))
523 t_snap_ext1 = _time_module.monotonic()
524 logger.warning("[SERVER step 8a] snapshots: fetch external parents %.1fms external=%d loaded=%d",
525 (t_snap_ext1 - t_snap_ext0) * 1000, len(_external_parent_sids), len(_parent_snap_manifests))
526
527 _head_snap_id: str | None = None
528 for _cd in _raw_commits_inline:
529 if _cd.get("commit_id") == head_commit_id:
530 _head_snap_id = _cd.get("snapshot_id") or None
531 break
532
533 _snap_resolved: dict[str, _SnapshotManifest] = {}
534 _snap_rows_inline = []
535 _n_snap_root = 0
536 _n_snap_head = 0
537 _n_snap_delta_only = 0
538 _snap_manifest_bytes_total = 0
539 _snap_delta_bytes_total = 0
540
541 t_snap_loop0 = _time_module.monotonic()
542 for _sd in _raw_snaps:
543 _sid = _sd.get("snapshot_id", "")
544 if not _sid:
545 continue
546 _parent_sid = _sd.get("parent_snapshot_id")
547 _delta_add: dict[str, str] = _sd.get("delta_add") or {}
548 _delta_remove: list[str] = _sd.get("delta_remove") or []
549 if _parent_sid and _parent_sid in _snap_resolved:
550 _base = dict(_snap_resolved[_parent_sid])
551 elif _parent_sid:
552 _base = dict(_parent_snap_manifests.get(_parent_sid) or {})
553 else:
554 _base = {}
555 _base.update(_delta_add)
556 for _path in _delta_remove:
557 _base.pop(_path, None)
558 _snap_resolved[_sid] = _base
559
560 _delta_encoded: JSONObject = {"add": _delta_add}
561 if _delta_remove:
562 _delta_encoded["rm"] = _delta_remove
563 _delta_blob = _msgpack.packb(_delta_encoded, use_bin_type=True) if (_delta_add or _delta_remove) else None
564
565 _is_root = not _parent_sid
566 _is_head = _sid == _head_snap_id
567 _manifest_blob = _msgpack.packb(_base, use_bin_type=True) if (_is_root or _is_head) else None
568
569 if _is_root: _n_snap_root += 1
570 if _is_head: _n_snap_head += 1
571 if not _is_root and not _is_head: _n_snap_delta_only += 1
572 if _manifest_blob: _snap_manifest_bytes_total += len(_manifest_blob)
573 if _delta_blob: _snap_delta_bytes_total += len(_delta_blob)
574
575 _snap_rows_inline.append({
576 "snapshot_id": _sid,
577 "directories": [],
578 "manifest_blob": _manifest_blob,
579 "entry_count": len(_base),
580 "created_at": _utc_now(),
581 "parent_snapshot_id": _parent_sid or None,
582 "delta_blob": _delta_blob,
583 })
584 t_snap_loop1 = _time_module.monotonic()
585 logger.warning(
586 "[SERVER step 8b] snapshots: replay delta chain %.1fms "
587 "total=%d root=%d head=%d delta_only=%d",
588 (t_snap_loop1 - t_snap_loop0) * 1000, len(_snap_rows_inline),
589 _n_snap_root, _n_snap_head, _n_snap_delta_only,
590 )
591
592 if _snap_rows_inline:
593 _snap_ids_to_check = [r["snapshot_id"] for r in _snap_rows_inline]
594 t_snap_sel0 = _time_module.monotonic()
595 _existing_snap_rows = (await session.execute(
596 _sa_text(
597 "SELECT snapshot_id, (delta_blob IS NOT NULL) AS has_delta "
598 "FROM musehub_snapshots WHERE snapshot_id = ANY(:ids)"
599 ),
600 {"ids": _snap_ids_to_check},
601 )).all()
602 _existing_sids: set[str] = {row[0] for row in _existing_snap_rows}
603 _sids_already_healed: set[str] = {row[0] for row in _existing_snap_rows if row[1]}
604 t_snap_sel1 = _time_module.monotonic()
605 logger.warning("[SERVER step 8c] snapshots: dedup SELECT %.1fms existing=%d new=%d",
606 (t_snap_sel1 - t_snap_sel0) * 1000, len(_existing_sids),
607 len(_snap_rows_inline) - len(_existing_sids))
608
609 _new_snap_dicts = [r for r in _snap_rows_inline if r["snapshot_id"] not in _existing_sids]
610 _heal_snap_dicts = [
611 r for r in _snap_rows_inline
612 if r["snapshot_id"] in _existing_sids
613 and r["snapshot_id"] not in _sids_already_healed
614 and r.get("delta_blob")
615 ]
616 if _new_snap_dicts:
617 session.add_all([MusehubSnapshot(**r) for r in _new_snap_dicts])
618 if _heal_snap_dicts:
619 _HEAL_CHUNK = 2000
620 for _hi in range(0, len(_heal_snap_dicts), _HEAL_CHUNK):
621 _chunk = _heal_snap_dicts[_hi : _hi + _HEAL_CHUNK]
622 _heal_stmt = _pg_insert(MusehubSnapshot).values(_chunk)
623 await session.execute(
624 _heal_stmt.on_conflict_do_update(
625 index_elements=["snapshot_id"],
626 set_={
627 "delta_blob": _heal_stmt.excluded.delta_blob,
628 "parent_snapshot_id": _heal_stmt.excluded.parent_snapshot_id,
629 },
630 where=MusehubSnapshot.delta_blob.is_(None),
631 )
632 )
633 t_snap_add = _time_module.monotonic()
634 logger.warning("[SERVER step 8d] snapshots: INSERT new %.1fms new=%d healed=%d",
635 (t_snap_add - t_snap_sel1) * 1000, len(_new_snap_dicts), len(_heal_snap_dicts))
636
637 _new_snap_rows = _new_snap_dicts
638 if _new_snap_rows:
639 await session.execute(
640 _pg_insert(MusehubSnapshotRef)
641 .values([{"repo_id": repo_id, "snapshot_id": r["snapshot_id"], "created_at": _utc_now()} for r in _new_snap_rows])
642 .on_conflict_do_nothing(index_elements=["repo_id", "snapshot_id"])
643 )
644 t_snap_ref = _time_module.monotonic()
645 logger.warning("[SERVER step 8e] snapshots: UPSERT snapshot_refs %.1fms new=%d", (t_snap_ref - t_snap_add) * 1000, len(_new_snap_rows))
646 else:
647 t_snap_ref = t_snap_loop1
648
649 # step 8: write commits
650 from muse.core.commits import CommitRecord as _CR_inline
651 _commit_rows_inline = []
652 _graph_rows_inline = []
653
654 _commit_ids_in_mpack = {
655 _cd.get("commit_id") for _cd in _raw_commits_inline if _cd.get("commit_id")
656 }
657 _external_parent_cids = {
658 pid
659 for _cd in _raw_commits_inline
660 for pid in (
661 ([_cd["parent_commit_id"]] if _cd.get("parent_commit_id") else [])
662 + ([_cd["parent2_commit_id"]] if _cd.get("parent2_commit_id") else [])
663 )
664 if pid not in _commit_ids_in_mpack
665 }
666 t_gen_sel0 = _time_module.monotonic()
667 _db_parent_gens: dict[str, int] = {}
668 if _external_parent_cids:
669 _gen_rows = (await session.execute(
670 select(MusehubCommitGraph.commit_id, MusehubCommitGraph.generation)
671 .where(MusehubCommitGraph.commit_id.in_(_external_parent_cids))
672 )).all()
673 _db_parent_gens = {_cid: _gval for _cid, _gval in _gen_rows}
674 t_gen_sel1 = _time_module.monotonic()
675 logger.warning("[SERVER step 9a] commits: fetch parent generations %.1fms external_parents=%d found=%d",
676 (t_gen_sel1 - t_gen_sel0) * 1000, len(_external_parent_cids), len(_db_parent_gens))
677
678 t_commit_parse0 = _time_module.monotonic()
679
680 _cid_set_inline = {_cd.get("commit_id") for _cd in _raw_commits_inline if _cd.get("commit_id")}
681 _cd_by_cid: dict[str, dict] = {_cd["commit_id"]: _cd for _cd in _raw_commits_inline if _cd.get("commit_id")}
682 _children: dict[str, list[dict]] = {cid: [] for cid in _cid_set_inline}
683 _in_degree: dict[str, int] = {cid: 0 for cid in _cid_set_inline}
684 for _cd in _raw_commits_inline:
685 for _pk in ("parent_commit_id", "parent2_commit_id"):
686 _p = _cd.get(_pk) or ""
687 if _p in _cid_set_inline:
688 _children[_p].append(_cd)
689 _in_degree[_cd["commit_id"]] = _in_degree.get(_cd["commit_id"], 0) + 1
690 from collections import deque as _deque
691 _queue: _deque[dict] = _deque(
692 _cd_by_cid[cid] for cid, deg in _in_degree.items() if deg == 0
693 )
694 _topo_sorted: list[dict] = []
695 while _queue:
696 _cd = _queue.popleft()
697 _topo_sorted.append(_cd)
698 for _child in _children.get(_cd.get("commit_id", ""), []):
699 _child_cid = _child.get("commit_id", "")
700 _in_degree[_child_cid] -= 1
701 if _in_degree[_child_cid] == 0:
702 _queue.append(_child)
703 _topo_seen = {_cd.get("commit_id") for _cd in _topo_sorted}
704 _topo_sorted.extend(_cd for _cd in _raw_commits_inline if _cd.get("commit_id") not in _topo_seen)
705
706 _inline_gen: dict[str, int] = {}
707 for _cd in _topo_sorted:
708 try:
709 _cr = _CR_inline.from_dict(_cd)
710 except Exception:
711 continue
712 if not _cr.commit_id:
713 continue
714 _pids: list[str] = []
715 if _cr.parent_commit_id:
716 _pids.append(_cr.parent_commit_id)
717 if _cr.parent2_commit_id:
718 _pids.append(_cr.parent2_commit_id)
719 _parent_gens: list[int] = []
720 for _pid in _pids:
721 if _pid in _inline_gen:
722 _parent_gens.append(_inline_gen[_pid])
723 elif _pid in _db_parent_gens:
724 _parent_gens.append(_db_parent_gens[_pid])
725 _gen = (max(_parent_gens) + 1) if _parent_gens else 0
726 _inline_gen[_cr.commit_id] = _gen
727 _commit_rows_inline.append({
728 "commit_id": _cr.commit_id,
729 "branch": _cr.branch or "main",
730 "message": _cr.message or "",
731 "author": _cr.author or "",
732 "timestamp": _cr.committed_at or _utc_now(),
733 "parent_ids": _pids,
734 "snapshot_id": _cr.snapshot_id or None,
735 "agent_id": _cr.agent_id or "",
736 "model_id": _cr.model_id or "",
737 "toolchain_id": _cr.toolchain_id or "",
738 "commit_branch": _cr.branch or None,
739 "signature": _cr.signature or "",
740 "signer_public_key": "",
741 "signer_key_id": _cr.signer_key_id or "",
742 "sem_ver_bump": _cr.sem_ver_bump or "none",
743 "breaking_changes": _cr.breaking_changes or [],
744 "reviewed_by": [],
745 "test_runs": 0,
746 "prompt_hash": _cr.prompt_hash or "",
747 "structured_delta": _cr.structured_delta if isinstance(_cr.structured_delta, dict) else None,
748 })
749 _graph_rows_inline.append({
750 "commit_id": _cr.commit_id,
751 "parent_ids": _pids,
752 "generation": _gen,
753 "snapshot_id": _cr.snapshot_id or None,
754 })
755 t_commit_parse1 = _time_module.monotonic()
756 _gen_values = sorted(_inline_gen.values())
757 logger.warning(
758 "[SERVER step 9b] commits: topo sort + parse %.1fms parsed=%d gen_min=%s gen_max=%s",
759 (t_commit_parse1 - t_commit_parse0) * 1000, len(_commit_rows_inline),
760 _gen_values[0] if _gen_values else "NONE",
761 _gen_values[-1] if _gen_values else "NONE",
762 )
763
764 if _commit_rows_inline:
765 t_csel0 = _time_module.monotonic()
766 _commit_ids_to_check = [r["commit_id"] for r in _commit_rows_inline]
767 _existing_cids = set((await session.execute(
768 _sa_text("SELECT commit_id FROM musehub_commits WHERE commit_id = ANY(:ids)"),
769 {"ids": _commit_ids_to_check},
770 )).scalars())
771 t_csel1 = _time_module.monotonic()
772 logger.warning("[SERVER step 9c] commits: dedup SELECT %.1fms existing=%d new=%d",
773 (t_csel1 - t_csel0) * 1000, len(_existing_cids),
774 len(_commit_rows_inline) - len(_existing_cids))
775
776 _new_commit_rows = [r for r in _commit_rows_inline if r["commit_id"] not in _existing_cids]
777 session.add_all([MusehubCommit(**r) for r in _new_commit_rows])
778 t_cadd = _time_module.monotonic()
779 logger.warning("[SERVER step 9d] commits: INSERT new %.1fms new=%d", (t_cadd - t_csel1) * 1000, len(_new_commit_rows))
780 # Upsert refs for ALL commits in this push, not just globally-new ones.
781 # A commit may already exist in musehub_commits from another repo's push
782 # but still be missing its ref for THIS repo — using _new_commit_rows here
783 # silently drops those refs and leaves commit history invisible to intel.
784 if _commit_rows_inline:
785 await session.execute(
786 _pg_insert(MusehubCommitRef)
787 .values([{"repo_id": repo_id, "commit_id": r["commit_id"], "created_at": _utc_now()} for r in _commit_rows_inline])
788 .on_conflict_do_nothing(index_elements=["repo_id", "commit_id"])
789 )
790 t_cref = _time_module.monotonic()
791 logger.warning("[SERVER step 9e] commits: UPSERT commit_refs %.1fms total=%d", (t_cref - t_cadd) * 1000, len(_commit_rows_inline))
792
793 if _graph_rows_inline:
794 _graph_stmt = _pg_insert(MusehubCommitGraph).values(_graph_rows_inline)
795 await session.execute(_graph_stmt.on_conflict_do_update(
796 index_elements=["commit_id"],
797 set_={"generation": _graph_stmt.excluded.generation,
798 "snapshot_id": _graph_stmt.excluded.snapshot_id},
799 ))
800 t_graph = _time_module.monotonic()
801 logger.warning("[SERVER step 9f] commits: UPSERT commit_graph %.1fms rows=%d",
802 (t_graph - t_cref) * 1000, len(_graph_rows_inline))
803 else:
804 t_graph = t_commit_parse1
805
806 # step 9: advance branch pointer
807 tip = head_commit_id
808 if tip and branch:
809 t_branch0 = _time_module.monotonic()
810 branch_row = (await session.execute(
811 select(MusehubBranch).where(
812 MusehubBranch.repo_id == repo_id,
813 MusehubBranch.name == branch,
814 ).with_for_update()
815 )).scalar_one_or_none()
816
817 current_head = branch_row.head_commit_id if branch_row else None
818 if not force and not _is_fast_forward(tip, current_head or "", wire_bytes):
819 raise NonFastForwardError(
820 f"push rejected: {tip[:20]}… is not a fast-forward of "
821 f"{(current_head or '')[:20]}… — use --force to override"
822 )
823
824 if branch_row is None:
825 session.add(MusehubBranch(
826 branch_id=compute_branch_id(repo_id, branch),
827 repo_id=repo_id,
828 name=branch,
829 head_commit_id=tip,
830 ))
831 else:
832 branch_row.head_commit_id = tip
833 t_branch1 = _time_module.monotonic()
834 logger.warning("[SERVER step 10] advance branch pointer (atomic CAS) %.1fms", (t_branch1 - t_branch0) * 1000)
835 else:
836 t_branch1 = t_graph
837
838 # commit db transaction
839 t_pre_commit = _time_module.monotonic()
840 await session.commit()
841 t_commit = _time_module.monotonic()
842 logger.warning("[SERVER step 10] commit db transaction %.1fms", (t_commit - t_pre_commit) * 1000)
843
844 # step 11: enqueue async jobs — TODO: not yet implemented
845
846 elapsed_ms = (t_commit - t0) * 1000
847 logger.warning("[SERVER step 11] enqueue intel jobs (TODO) 0.0ms")
848 logger.warning("[SERVER ✅] TOTAL=%.1fms branch=%s head=%s", elapsed_ms, branch, head_commit_id[:20] if head_commit_id else "none")
849 return {
850 "head": tip,
851 "branch": branch,
852 "objects_in_mpack": objects_count,
853 "commits_in_mpack": commits_count,
854 "objects_written": len(_new_oids),
855 "commits_written": len(_new_commit_rows) if _commit_rows_inline else 0,
856 "snapshots_written": len(_new_snap_dicts) if _snap_rows_inline else 0,
857 }
File History 20 commits
sha256:7d6dd8f4a89e2d1fef2d84f6e65feaff51385d382f466766b7f690a22ec18e32 fix: fall back to DB ancestry check when mpack-only fast-fo… Sonnet 4.6 patch 43 days ago
sha256:1c94edd45e7ed3bf7012c2742e755bb7608890f0964aa2ba9ea7f8cf83807013 revert: undo unauthorized wire protocol change Sonnet 4.6 patch 45 days ago
sha256:a1d7cf2d9e9c9f4edc566e56afbde3f6b3194ca712209beb02eb3c838d1f47e6 fix: fast-forward push validation falls back to DB ancestor walk Sonnet 4.6 patch 45 days ago
sha256:80c35083d2abe19ef730693f2b35ad9a9342d2abbe81e802e6f397fe9ae75eb9 merge: pull staging/dev — asyncpg batch fix + content-addre… Sonnet 4.6 patch 45 days ago
sha256:5667a3e21bf16fd2e6d6bd4a769bd1c0cf7634afa12cef6450cc77573196b7f9 asyncpg caps query parameters Human patch 45 days ago
sha256:c3e12fae919d99c0e030595f58eb0a7b7df9ae4bd4ed6ce582a46c33784a54d0 fix(mpack-index-job): write object_refs after musehub_objec… Sonnet 4.6 patch 48 days ago
sha256:8ba2515ec3cb2a4089d54422f342b5a47161df29d5db72cb7356591d128fba6a fix(wire-push): write object_refs and mpack_index for all m… Sonnet 4.6 patch 48 days ago
sha256:009b5a222314f47640a58d75ce5a1f428f1624cf0b51384dfcdfbdfab3cc42a4 feat: migration idempotency, file attribution DAG walk, mpa… Sonnet 4.6 minor 52 days ago
sha256:92528ae07d0e1239d87fd5fd1f439e8fbb49c9778a9a400bc4a736073fb28316 feat: byte-range blob reads, file attribution DAG walk, bra… Sonnet 4.6 minor 52 days ago
sha256:f3995ec2c05c9c34b0e4d6e96349a811d0117a1c51d78096d757998ccb3c0520 fix: blobs only in S3/mpack — remove commit/snapshot indivi… Sonnet 4.6 patch 54 days ago
sha256:8a7ff43f27504c1f6abba59ffbab3dc89bd1d8bcfa4c57f6e280286d1a58195a feat: resurrect process_mpack_index_job with byte-range ind… Sonnet 4.6 patch 54 days ago
sha256:ad616c6113d6c00f4efed6b2993734ca46d3e9b5bee25addd4ce8ae6b57136e5 chore: bump version to 0.2.0rc11; typing audit clean + all … Sonnet 4.6 minor 56 days ago
sha256:ecca645d94c5f39c88f4bc1283447ba0f4635ef3cbb11d0cd9b3759cba289d00 fix: compute_snapshot_id uses typed-object formula and wire… Sonnet 4.6 minor 56 days ago
sha256:e35be48854f182f7bf02dc6cc0f58d22b3de3a544b570c0e2bc53f9e75a3607d feat(phase6): remove delta_blob path, dead imports, add fal… Sonnet 4.6 minor 57 days ago
sha256:450998d182617fa93b737cbbdb3fe956c61566051739acec8c63ec5e7b4587f8 feat(phase3): write snapshot objects to S3 at all 3 write s… Sonnet 4.6 patch 57 days ago
sha256:e597c0b97ade9c3c52ac4735ceb437ee69d1b6f0db61b8d7caa6467c5866566d feat(phase2): write commit objects to S3 at all 5 write sit… Sonnet 4.6 patch 57 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f fix: use wire_bytes not mpack_bytes_raw in compute_object_b… Sonnet 4.6 patch 57 days ago
sha256:302574ddba13c9a20694c0fb051176eef4896f943b63bc458df886633b1bfcd6 feat: mpack byte-range index — store byte_offset/byte_lengt… Sonnet 4.6 minor 57 days ago
sha256:ba425baae57a4223a95f2cf4841181d790d04d02b9c98d0371f7b43046d92ff4 fix: store directories from incoming snapshot deltas instea… Sonnet 4.6 patch 57 days ago
sha256:4c039010d7b35b3bb7e9242df3e73ff50336264cb89a85a7509dc959e8516fc1 fix: store signer_public_key from incoming commits instead … Sonnet 4.6 patch 57 days ago