dsh-deepmoyu 2.7.2 → 2.8.1

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.
Files changed (46) hide show
  1. package/AGENTS.md +9 -0
  2. package/CHANGELOG.md +36 -0
  3. package/CONTRIBUTING.md +1 -0
  4. package/README.md +12 -3
  5. package/client.js +430 -293
  6. package/docs/ACTIVITY_TUNING.md +50 -0
  7. package/docs/CONTINUOUS_BROWSING.md +35 -0
  8. package/docs/DEFAULTS_2026_11_13.md +48 -0
  9. package/docs/FUN_FEATURE_REFERENCES.md +128 -0
  10. package/docs/LOCAL_COMPAT_AUDIT_2026-11-12.md +58 -0
  11. package/docs/NATURAL_AUTONOMY.md +91 -0
  12. package/docs/PRIVACY.md +7 -0
  13. package/docs/RELEASE_2_8_0.md +27 -0
  14. package/docs/RELEASE_2_8_1.md +29 -0
  15. package/docs/RELEASE_AUDIT.md +22 -0
  16. package/docs/TEST_MATRIX.md +9 -0
  17. package/docs/WORKFLOW_REFERENCES.md +57 -0
  18. package/docs/tool-evidence.json +6 -1
  19. package/index.js +693 -241
  20. package/lib/background-tools.mjs +1 -1
  21. package/lib/browse-preferences.mjs +154 -0
  22. package/lib/browse-progress.mjs +165 -0
  23. package/lib/comment.mjs +29 -46
  24. package/lib/config-policy.mjs +2 -1
  25. package/lib/curiosity.mjs +153 -0
  26. package/lib/learn-review.mjs +2 -2
  27. package/lib/learn-store.mjs +7 -0
  28. package/lib/learn.mjs +234 -69
  29. package/lib/mcp.mjs +5 -2
  30. package/lib/memory-center.mjs +21 -19
  31. package/lib/mode.mjs +23 -9
  32. package/lib/model-relay-adapter.mjs +1 -1
  33. package/lib/model-relay.mjs +15 -2
  34. package/lib/pace.mjs +15 -7
  35. package/lib/persona.mjs +44 -29
  36. package/lib/product-status.mjs +38 -2
  37. package/lib/prompts.mjs +54 -39
  38. package/lib/pv.py +26 -12
  39. package/lib/pv_editorial.py +5 -2
  40. package/lib/run-settings.mjs +5 -5
  41. package/lib/session-continuation.mjs +47 -0
  42. package/package.json +1 -1
  43. package/presets/deepmoyu.patch.yml +10 -11
  44. package/prompts/duty.md +35 -37
  45. package/vendor/bilibili-mcp/mcp_server.py +88 -15
  46. package/vendor/bilibili-mcp/runtime_guard.py +132 -25
@@ -1,7 +1,8 @@
1
1
  """Durable, cross-process deduplication for both Bilibili MCP entry paths.
2
2
 
3
- Only state.json and seen.md are read for migration. No credential or message body
4
- is read into this database. Send reservations never expire automatically: after a
3
+ Only state.json and seen.md are read for migration. Public subtitle snapshots are
4
+ cached separately from permanent history. No credentials or private messages are
5
+ read into this database. Send reservations never expire automatically: after a
5
6
  crash or an ambiguous network outcome, retrying could publish a second message.
6
7
  """
7
8
  from __future__ import annotations
@@ -22,14 +23,16 @@ from pathlib import Path
22
23
  BV_RE = re.compile(r"^BV[0-9A-Za-z]{10}$")
23
24
  READ_LEASE_SECONDS = 600
24
25
  READ_TIMEOUT_SECONDS = 180
26
+ SUBTITLE_CACHE_MAX = 256
27
+ SUBTITLE_CACHE_BYTES = 2_000_000
25
28
 
26
29
  # Same limits as lib/pace.mjs; SQLite is the common floor for GUI and native MCP.
27
30
  PACE = {
28
- "comment": {"gap": 120, "jitter": 60, "hour": 8},
29
- "reply": {"gap": 60, "jitter": 30, "hour": 15},
30
- "opusReply": {"gap": 20, "jitter": 10, "hour": 30},
31
- "dm": {"gap": 30, "jitter": 15, "hour": 20},
32
- "moment": {"gap": 600, "jitter": 300, "hour": 1},
31
+ "comment": {"gap": 120, "jitter": 60, "hour": 16},
32
+ "reply": {"gap": 60, "jitter": 30, "hour": 30},
33
+ "opusReply": {"gap": 20, "jitter": 10, "hour": 40},
34
+ "dm": {"gap": 30, "jitter": 15, "hour": 30},
35
+ "moment": {"gap": 600, "jitter": 300, "hour": 2},
33
36
  }
34
37
  BURST_WINDOW, BURST_MAX, BURST_EXTRA = 600, 10, 180
35
38
  PACE_KEEP = 7200
@@ -45,6 +48,10 @@ def _finite(value, default=0):
45
48
  return float(default)
46
49
 
47
50
 
51
+ def _pace_token(value):
52
+ return value.lower() if isinstance(value, str) and re.fullmatch(r"[0-9a-fA-F]{32}", value) else ""
53
+
54
+
48
55
  def pace_kind(key):
49
56
  prefix = str(key).split(":", 1)[0]
50
57
  return {"nu": "comment", "video": "reply", "opus": "opusReply",
@@ -238,6 +245,10 @@ class RuntimeGuard:
238
245
  conn.execute("CREATE INDEX IF NOT EXISTS guard_pace_kind_at ON guard_pace_events(kind,at)")
239
246
  conn.execute("CREATE TABLE IF NOT EXISTS guard_pace_state ("
240
247
  "name TEXT PRIMARY KEY, value TEXT NOT NULL)")
248
+ conn.execute("CREATE TABLE IF NOT EXISTS subtitle_cache ("
249
+ "bvid TEXT PRIMARY KEY, fetched REAL NOT NULL, payload TEXT NOT NULL)")
250
+ conn.execute("CREATE TABLE IF NOT EXISTS subtitle_reads ("
251
+ "bvid TEXT PRIMARY KEY, token TEXT NOT NULL, expires REAL NOT NULL)")
241
252
  finally:
242
253
  conn.close()
243
254
 
@@ -305,17 +316,19 @@ class RuntimeGuard:
305
316
  if isinstance(row, dict):
306
317
  at = _finite(row.get("at"))
307
318
  nxt = _finite(row.get("next"))
308
- stored.append((kind, at, nxt if nxt > 0 else None))
319
+ stored.append((kind, at, nxt if nxt > 0 else None, row.get("token") if "token" in row else None))
309
320
  else:
310
- stored.append((kind, _finite(row), None))
321
+ stored.append((kind, _finite(row), None, None))
311
322
 
312
- def recorded_next(kind, at):
313
- for s_kind, s_at, s_next in stored:
314
- if s_kind == kind and s_next and abs(s_at - at) <= 2000:
323
+ def recorded_next(kind, at, token):
324
+ for s_kind, s_at, s_next, s_token in stored:
325
+ same_event = (_pace_token(token) == _pace_token(s_token) and bool(_pace_token(token))
326
+ if token is not None else s_token is None and abs(s_at - at) <= 2000)
327
+ if s_kind == kind and s_next and same_event:
315
328
  return s_next
316
329
  return None
317
330
 
318
- def event(kind, stamp, next_stamp=None, pinned=False):
331
+ def event(kind, stamp, next_stamp=None, pinned=False, token=None):
319
332
  at = _finite(stamp) / 1000
320
333
  if kind not in PACE or at <= 0:
321
334
  return
@@ -323,12 +336,24 @@ class RuntimeGuard:
323
336
  free = max(at, free)
324
337
  if at < now - PACE_KEEP and free <= now:
325
338
  return
326
- # GUI records success just after Python does. Reconcile that one event,
327
- # not a second send; distinct pre-upgrade seed records remain distinct.
328
- native = conn.execute("SELECT event_key,next FROM guard_pace_events "
329
- "WHERE kind=? AND status='sent' AND token<>'' AND ABS(at-?)<=2 "
330
- "ORDER BY ABS(at-?) LIMIT 1", (kind, at, at)).fetchone()
339
+ # The host may finish its state lock seconds after Python committed. A receipt
340
+ # token identifies that exact send without consuming another hourly/burst slot.
341
+ # Only old records with no token use the legacy two-second migration heuristic.
342
+ valid_token = _pace_token(token)
343
+ if valid_token:
344
+ native = conn.execute("SELECT event_key,next FROM guard_pace_events "
345
+ "WHERE kind=? AND status='sent' AND token=? LIMIT 1",
346
+ (kind, valid_token)).fetchone()
347
+ elif token is None:
348
+ native = conn.execute("SELECT event_key,next FROM guard_pace_events "
349
+ "WHERE kind=? AND status='sent' AND token<>'' AND ABS(at-?)<=2 "
350
+ "ORDER BY ABS(at-?) LIMIT 1", (kind, at, at)).fetchone()
351
+ else:
352
+ native = None
331
353
  if native:
354
+ if valid_token:
355
+ conn.execute("DELETE FROM guard_pace_events WHERE kind=? AND status='seed' AND token=?",
356
+ (kind, valid_token))
332
357
  # The host already pinned this interval (one random draw, shown in the GUI).
333
358
  # Python's own independent draw must not silently lengthen it — otherwise the
334
359
  # UI says "40s left" while the native guard still waits 90s.
@@ -339,19 +364,20 @@ class RuntimeGuard:
339
364
  conn.execute("UPDATE guard_pace_events SET next=MAX(next,?) WHERE event_key=?",
340
365
  (free, native["event_key"]))
341
366
  return
342
- key = f"seed:{kind}:{int(round(at * 1000))}"
367
+ key = f"seed:{kind}:token:{valid_token}" if valid_token else f"seed:{kind}:{int(round(at * 1000))}"
343
368
  conn.execute("INSERT INTO guard_pace_events(event_key,kind,at,next,status,token) "
344
- "VALUES (?,?,?,?,'seed','') ON CONFLICT(event_key) DO UPDATE SET next=MAX(next,excluded.next)",
345
- (key, kind, at, free))
369
+ "VALUES (?,?,?,?,'seed',?) ON CONFLICT(event_key) DO UPDATE SET next=MAX(next,excluded.next)",
370
+ (key, kind, at, free, valid_token))
346
371
 
347
372
  recent = raw.get("recent")
348
373
  for row in (recent[-200:] if isinstance(recent, list) else []):
349
374
  if isinstance(row, dict):
350
375
  kind, at = str(row.get("k") or ""), _finite(row.get("t"))
351
- nxt = recorded_next(kind, at)
352
- event(kind, at, nxt, pinned=nxt is not None)
353
- for kind, at, nxt in stored:
354
- event(kind, at, nxt, pinned=nxt is not None)
376
+ token = row.get("token") if "token" in row else None
377
+ nxt = recorded_next(kind, at, token)
378
+ event(kind, at, nxt, pinned=nxt is not None, token=token)
379
+ for kind, at, nxt, token in stored:
380
+ event(kind, at, nxt, pinned=nxt is not None, token=token)
355
381
  book = self._pace_state(conn)
356
382
  cool = _finite(raw.get("cooldownUntil")) / 1000
357
383
  if cool > book["cooldown_until"]:
@@ -594,6 +620,81 @@ class RuntimeGuard:
594
620
  raise ValueError("invalid bvid")
595
621
  return self._reserve("seen:" + bvid, "read")
596
622
 
623
+ def cached_subtitle(self, bvid):
624
+ """Read a bounded public transcript; history alone is never transcript evidence."""
625
+ bvid = str(bvid).strip()
626
+ if not BV_RE.fullmatch(bvid):
627
+ raise ValueError("invalid bvid")
628
+ conn = self._connect()
629
+ try:
630
+ row = conn.execute("SELECT payload FROM subtitle_cache WHERE bvid=?", (bvid,)).fetchone()
631
+ if row is None or len(row["payload"].encode("utf-8")) > SUBTITLE_CACHE_BYTES:
632
+ return None
633
+ try:
634
+ payload = json.loads(row["payload"])
635
+ except (ValueError, TypeError):
636
+ return None
637
+ if (not isinstance(payload, dict) or payload.get("bvid") != bvid
638
+ or payload.get("error") or payload.get("message")
639
+ or not str(payload.get("text") or "").strip()):
640
+ return None
641
+ return payload
642
+ finally:
643
+ conn.close()
644
+
645
+ def reserve_subtitle(self, bvid):
646
+ """Single-flight fetch lease independent of permanent seen/send reservations."""
647
+ bvid = str(bvid).strip()
648
+ if not BV_RE.fullmatch(bvid):
649
+ raise ValueError("invalid bvid")
650
+ now = self._clock()
651
+ with self._transaction() as conn:
652
+ self._import_legacy(conn)
653
+ legacy = conn.execute("SELECT * FROM guard_entries WHERE key=? AND kind='read'",
654
+ ("seen:" + bvid,)).fetchone()
655
+ if legacy is not None and legacy["status"] == "pending" and (legacy["expires"] or 0) > now:
656
+ return {"allowed": False, "status": "pending", "kind": "read",
657
+ "retryAt": _iso(legacy["expires"]), "at": _iso(now)}
658
+ row = conn.execute("SELECT * FROM subtitle_reads WHERE bvid=?", (bvid,)).fetchone()
659
+ if row is not None and row["expires"] > now:
660
+ return {"allowed": False, "status": "pending", "kind": "read",
661
+ "retryAt": _iso(row["expires"]), "at": _iso(now)}
662
+ token = uuid.uuid4().hex
663
+ conn.execute("INSERT INTO subtitle_reads(bvid,token,expires) VALUES (?,?,?) "
664
+ "ON CONFLICT(bvid) DO UPDATE SET token=excluded.token,expires=excluded.expires",
665
+ (bvid, token, now + self.read_lease_seconds))
666
+ return {"allowed": True, "kind": "read", "subtitle": True, "bvid": bvid, "token": token}
667
+
668
+ def commit_subtitle(self, reservation, payload):
669
+ """Publish cache + first-seen record atomically; eviction never removes history."""
670
+ bvid = str(reservation.get("bvid") or "")
671
+ if not reservation.get("subtitle") or not BV_RE.fullmatch(bvid):
672
+ raise ValueError("invalid subtitle reservation")
673
+ if (not isinstance(payload, dict) or payload.get("bvid") != bvid
674
+ or payload.get("error") or payload.get("message")
675
+ or not str(payload.get("text") or "").strip()):
676
+ raise ValueError("subtitle content required")
677
+ packed = json.dumps(payload, ensure_ascii=False)
678
+ now = self._clock()
679
+ with self._transaction() as conn:
680
+ changed = conn.execute("DELETE FROM subtitle_reads WHERE bvid=? AND token=?",
681
+ (bvid, reservation["token"])).rowcount
682
+ if changed != 1:
683
+ raise RuntimeError("subtitle fetch lease was lost")
684
+ if len(packed.encode("utf-8")) <= SUBTITLE_CACHE_BYTES:
685
+ conn.execute("INSERT INTO subtitle_cache(bvid,fetched,payload) VALUES (?,?,?) "
686
+ "ON CONFLICT(bvid) DO UPDATE SET fetched=excluded.fetched,payload=excluded.payload",
687
+ (bvid, now, packed))
688
+ conn.execute("DELETE FROM subtitle_cache WHERE bvid NOT IN "
689
+ "(SELECT bvid FROM subtitle_cache ORDER BY fetched DESC,bvid LIMIT ?)",
690
+ (SUBTITLE_CACHE_MAX,))
691
+ # INSERT OR IGNORE keeps the original first-seen timestamp and its provenance.
692
+ conn.execute("DELETE FROM guard_entries WHERE key=? AND kind='read' AND status='pending' AND expires<=?",
693
+ ("seen:" + bvid, now))
694
+ self._import_one(conn, "seen:" + bvid, "read",
695
+ {"bvid": bvid, "title": payload.get("title", ""), "at": _iso(now)})
696
+ return len(packed.encode("utf-8")) <= SUBTITLE_CACHE_BYTES
697
+
597
698
  def reserve_send(self, key, legacy_keys=()):
598
699
  if not key:
599
700
  raise ValueError("empty send key")
@@ -630,6 +731,9 @@ class RuntimeGuard:
630
731
  def release(self, reservation):
631
732
  """Only use for failed reads or a send proven not to have started."""
632
733
  with self._transaction() as conn:
734
+ if reservation.get("subtitle"):
735
+ return conn.execute("DELETE FROM subtitle_reads WHERE bvid=? AND token=?",
736
+ (reservation["bvid"], reservation["token"])).rowcount == 1
633
737
  changed = conn.execute("DELETE FROM guard_entries WHERE key=? AND token=? AND status='pending'",
634
738
  (reservation["key"], reservation["token"])).rowcount == 1
635
739
  if changed:
@@ -710,6 +814,9 @@ def guard_snapshot(home=None, bvids=None, limit=200):
710
814
  result["recentSends"] = [_public(row) for row in conn.execute(
711
815
  "SELECT * FROM guard_entries WHERE kind='send' ORDER BY updated DESC LIMIT ?", (limit,))]
712
816
  tables = {row["name"] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")}
817
+ if "subtitle_reads" in tables:
818
+ leases = conn.execute("SELECT bvid FROM subtitle_reads WHERE expires>? LIMIT ?", (now, limit)).fetchall()
819
+ result["activeReads"] = list(dict.fromkeys(result["activeReads"] + [row["bvid"] for row in leases]))[:limit]
713
820
  if {"guard_pace_events", "guard_pace_state"} <= tables:
714
821
  result["pace"] = {action: RuntimeGuard._pace_check(conn, action, now) for action in PACE}
715
822
  finally: