@researai/deepscientist 1.5.7 → 1.5.9

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 (156) hide show
  1. package/LICENSE +186 -21
  2. package/README.md +8 -4
  3. package/bin/ds.js +224 -9
  4. package/docs/en/00_QUICK_START.md +2 -2
  5. package/docs/en/07_MEMORY_AND_MCP.md +40 -3
  6. package/docs/en/99_ACKNOWLEDGEMENTS.md +1 -0
  7. package/docs/zh/00_QUICK_START.md +2 -2
  8. package/docs/zh/07_MEMORY_AND_MCP.md +40 -3
  9. package/docs/zh/99_ACKNOWLEDGEMENTS.md +1 -0
  10. package/install.sh +34 -0
  11. package/package.json +2 -2
  12. package/pyproject.toml +2 -2
  13. package/src/deepscientist/__init__.py +1 -1
  14. package/src/deepscientist/acp/envelope.py +1 -0
  15. package/src/deepscientist/artifact/metrics.py +814 -83
  16. package/src/deepscientist/artifact/schemas.py +1 -0
  17. package/src/deepscientist/artifact/service.py +2001 -229
  18. package/src/deepscientist/bash_exec/monitor.py +1 -1
  19. package/src/deepscientist/bash_exec/service.py +17 -9
  20. package/src/deepscientist/channels/qq.py +17 -0
  21. package/src/deepscientist/channels/relay.py +16 -0
  22. package/src/deepscientist/config/models.py +6 -0
  23. package/src/deepscientist/config/service.py +70 -2
  24. package/src/deepscientist/daemon/api/handlers.py +414 -14
  25. package/src/deepscientist/daemon/api/router.py +4 -0
  26. package/src/deepscientist/daemon/app.py +292 -21
  27. package/src/deepscientist/gitops/diff.py +6 -10
  28. package/src/deepscientist/mcp/server.py +191 -40
  29. package/src/deepscientist/prompts/builder.py +65 -19
  30. package/src/deepscientist/quest/node_traces.py +129 -2
  31. package/src/deepscientist/quest/service.py +140 -34
  32. package/src/deepscientist/quest/stage_views.py +175 -33
  33. package/src/deepscientist/registries/baseline.py +56 -4
  34. package/src/deepscientist/runners/codex.py +1 -1
  35. package/src/prompts/connectors/qq.md +1 -1
  36. package/src/prompts/contracts/shared_interaction.md +14 -0
  37. package/src/prompts/system.md +113 -32
  38. package/src/skills/analysis-campaign/SKILL.md +10 -14
  39. package/src/skills/baseline/SKILL.md +51 -38
  40. package/src/skills/baseline/references/baseline-plan-template.md +2 -0
  41. package/src/skills/decision/SKILL.md +12 -8
  42. package/src/skills/experiment/SKILL.md +28 -16
  43. package/src/skills/experiment/references/main-experiment-plan-template.md +2 -0
  44. package/src/skills/figure-polish/SKILL.md +1 -0
  45. package/src/skills/finalize/SKILL.md +3 -8
  46. package/src/skills/idea/SKILL.md +18 -8
  47. package/src/skills/idea/references/literature-survey-template.md +24 -0
  48. package/src/skills/idea/references/related-work-playbook.md +4 -0
  49. package/src/skills/idea/references/selection-gate.md +9 -0
  50. package/src/skills/intake-audit/SKILL.md +2 -8
  51. package/src/skills/rebuttal/SKILL.md +2 -8
  52. package/src/skills/review/SKILL.md +2 -8
  53. package/src/skills/scout/SKILL.md +2 -8
  54. package/src/skills/write/SKILL.md +53 -17
  55. package/src/skills/write/templates/DEEPSCIENTIST_NOTES.md +21 -0
  56. package/src/skills/write/templates/README.md +408 -0
  57. package/src/skills/write/templates/UPSTREAM_LICENSE.txt +21 -0
  58. package/src/skills/write/templates/aaai2026/README.md +534 -0
  59. package/src/skills/write/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
  60. package/src/skills/write/templates/aaai2026/aaai2026-unified-template.tex +952 -0
  61. package/src/skills/write/templates/aaai2026/aaai2026.bib +111 -0
  62. package/src/skills/write/templates/aaai2026/aaai2026.bst +1493 -0
  63. package/src/skills/write/templates/aaai2026/aaai2026.sty +315 -0
  64. package/src/skills/write/templates/acl/README.md +50 -0
  65. package/src/skills/write/templates/acl/acl.sty +312 -0
  66. package/src/skills/write/templates/acl/acl_latex.tex +377 -0
  67. package/src/skills/write/templates/acl/acl_lualatex.tex +101 -0
  68. package/src/skills/write/templates/acl/acl_natbib.bst +1940 -0
  69. package/src/skills/write/templates/acl/anthology.bib.txt +26 -0
  70. package/src/skills/write/templates/acl/custom.bib +70 -0
  71. package/src/skills/write/templates/acl/formatting.md +326 -0
  72. package/src/skills/write/templates/asplos2027/main.tex +459 -0
  73. package/src/skills/write/templates/asplos2027/references.bib +135 -0
  74. package/src/skills/write/templates/colm2025/README.md +3 -0
  75. package/src/skills/write/templates/colm2025/colm2025_conference.bib +11 -0
  76. package/src/skills/write/templates/colm2025/colm2025_conference.bst +1440 -0
  77. package/src/skills/write/templates/colm2025/colm2025_conference.sty +218 -0
  78. package/src/skills/write/templates/colm2025/colm2025_conference.tex +305 -0
  79. package/src/skills/write/templates/colm2025/fancyhdr.sty +485 -0
  80. package/src/skills/write/templates/colm2025/math_commands.tex +508 -0
  81. package/src/skills/write/templates/colm2025/natbib.sty +1246 -0
  82. package/src/skills/write/templates/iclr2026/fancyhdr.sty +485 -0
  83. package/src/skills/write/templates/iclr2026/iclr2026_conference.bib +24 -0
  84. package/src/skills/write/templates/iclr2026/iclr2026_conference.bst +1440 -0
  85. package/src/skills/write/templates/iclr2026/iclr2026_conference.sty +246 -0
  86. package/src/skills/write/templates/iclr2026/iclr2026_conference.tex +414 -0
  87. package/src/skills/write/templates/iclr2026/math_commands.tex +508 -0
  88. package/src/skills/write/templates/iclr2026/natbib.sty +1246 -0
  89. package/src/skills/write/templates/icml2026/algorithm.sty +79 -0
  90. package/src/skills/write/templates/icml2026/algorithmic.sty +201 -0
  91. package/src/skills/write/templates/icml2026/example_paper.bib +75 -0
  92. package/src/skills/write/templates/icml2026/example_paper.tex +662 -0
  93. package/src/skills/write/templates/icml2026/fancyhdr.sty +864 -0
  94. package/src/skills/write/templates/icml2026/icml2026.bst +1443 -0
  95. package/src/skills/write/templates/icml2026/icml2026.sty +767 -0
  96. package/src/skills/write/templates/neurips2025/Makefile +36 -0
  97. package/src/skills/write/templates/neurips2025/extra_pkgs.tex +53 -0
  98. package/src/skills/write/templates/neurips2025/main.tex +38 -0
  99. package/src/skills/write/templates/neurips2025/neurips.sty +382 -0
  100. package/src/skills/write/templates/nsdi2027/main.tex +426 -0
  101. package/src/skills/write/templates/nsdi2027/references.bib +151 -0
  102. package/src/skills/write/templates/nsdi2027/usenix-2020-09.sty +83 -0
  103. package/src/skills/write/templates/osdi2026/main.tex +429 -0
  104. package/src/skills/write/templates/osdi2026/references.bib +150 -0
  105. package/src/skills/write/templates/osdi2026/usenix-2020-09.sty +83 -0
  106. package/src/skills/write/templates/sosp2026/main.tex +532 -0
  107. package/src/skills/write/templates/sosp2026/references.bib +148 -0
  108. package/src/tui/package.json +1 -1
  109. package/src/ui/dist/assets/{AiManusChatView-BS3V4ZOk.js → AiManusChatView-BKZ103sn.js} +110 -14
  110. package/src/ui/dist/assets/{AnalysisPlugin-DLPXQsmr.js → AnalysisPlugin-mTTzGAlK.js} +1 -1
  111. package/src/ui/dist/assets/{AutoFigurePlugin-C-Fr9knQ.js → AutoFigurePlugin-C_wWw4AP.js} +5 -5
  112. package/src/ui/dist/assets/{CliPlugin-Dd8AHzFg.js → CliPlugin-BH58n3GY.js} +9 -9
  113. package/src/ui/dist/assets/{CodeEditorPlugin-Dg-RepTl.js → CodeEditorPlugin-BKGRUH7e.js} +8 -8
  114. package/src/ui/dist/assets/{CodeViewerPlugin-D2J_3nyt.js → CodeViewerPlugin-BMADwFWJ.js} +5 -5
  115. package/src/ui/dist/assets/{DocViewerPlugin-ChRLLKNb.js → DocViewerPlugin-ZOnTIHLN.js} +3 -3
  116. package/src/ui/dist/assets/{GitDiffViewerPlugin-DgHfcved.js → GitDiffViewerPlugin-CQ7h1Djm.js} +830 -86
  117. package/src/ui/dist/assets/{ImageViewerPlugin-C89GZMBy.js → ImageViewerPlugin-GVS5MsnC.js} +5 -5
  118. package/src/ui/dist/assets/{LabCopilotPanel-BUfIwUcb.js → LabCopilotPanel-BZNv1JML.js} +10 -10
  119. package/src/ui/dist/assets/{LabPlugin-zvUmQUMq.js → LabPlugin-TWcJsdQA.js} +1 -1
  120. package/src/ui/dist/assets/{LatexPlugin-C1SSNuWp.js → LatexPlugin-DIjHiR2x.js} +7 -7
  121. package/src/ui/dist/assets/{MarkdownViewerPlugin-D2Mf5tU5.js → MarkdownViewerPlugin-D3ooGAH0.js} +4 -4
  122. package/src/ui/dist/assets/{MarketplacePlugin-CF4LgiS2.js → MarketplacePlugin-DfVfE9hN.js} +3 -3
  123. package/src/ui/dist/assets/{NotebookEditor-BM7Bgwlv.js → NotebookEditor-DDl0_Mc0.js} +1 -1
  124. package/src/ui/dist/assets/{index-Be0NAmh8.js → NotebookEditor-s8JhzuX1.js} +12 -155
  125. package/src/ui/dist/assets/{PdfLoader-Bc5qfD-Z.js → PdfLoader-C2Sf6SJM.js} +1 -1
  126. package/src/ui/dist/assets/{PdfMarkdownPlugin-sh1-IRcp.js → PdfMarkdownPlugin-CXFLoIsa.js} +3 -3
  127. package/src/ui/dist/assets/{PdfViewerPlugin-C_a7CpWG.js → PdfViewerPlugin-BYTmz2fK.js} +10 -10
  128. package/src/ui/dist/assets/{SearchPlugin-L4z3HcLf.js → SearchPlugin-CjWBI1O9.js} +1 -1
  129. package/src/ui/dist/assets/{Stepper-Dk4aQ3fN.js → Stepper-B0Dd8CxK.js} +1 -1
  130. package/src/ui/dist/assets/{TextViewerPlugin-BsNtlKVo.js → TextViewerPlugin-DdOBU3-S.js} +4 -4
  131. package/src/ui/dist/assets/{VNCViewer-BpeDcZ5_.js → VNCViewer-B8HGgLwQ.js} +9 -9
  132. package/src/ui/dist/assets/{bibtex-C4QI-bbj.js → bibtex-CKaefIN2.js} +1 -1
  133. package/src/ui/dist/assets/{code-DuMINRsg.js → code-BWAY76JP.js} +1 -1
  134. package/src/ui/dist/assets/{file-content-C3N-432K.js → file-content-C1NwU5oQ.js} +1 -1
  135. package/src/ui/dist/assets/{file-diff-panel-CffQ4ZMg.js → file-diff-panel-CywslwB9.js} +1 -1
  136. package/src/ui/dist/assets/{file-socket-CRH59PCO.js → file-socket-B4kzuOBQ.js} +1 -1
  137. package/src/ui/dist/assets/{file-utils-vYGtW2mI.js → file-utils-H2fjA46S.js} +1 -1
  138. package/src/ui/dist/assets/{image-DBVGaooo.js → image-D-NZM-6P.js} +1 -1
  139. package/src/ui/dist/assets/{index-B1P6hQRJ.js → index-7Chr1g9c.js} +3734 -1862
  140. package/src/ui/dist/assets/{index-DjSFDmgB.js → index-BdM1Gqfr.js} +2 -2
  141. package/src/ui/dist/assets/{index-BpjYH9Vg.js → index-CDxNdQdz.js} +1 -1
  142. package/src/ui/dist/assets/{index-Do9N28uB.css → index-DGIYDuTv.css} +163 -34
  143. package/src/ui/dist/assets/index-DHZJ_0TI.js +159 -0
  144. package/src/ui/dist/assets/{message-square-BsPDBhiY.js → message-square-BzjLiXir.js} +1 -1
  145. package/src/ui/dist/assets/{monaco-BTkdPojV.js → monaco-Cb2uKKe6.js} +1 -1
  146. package/src/ui/dist/assets/{popover-cWjCk-vc.js → popover-Bg72DGgT.js} +1 -1
  147. package/src/ui/dist/assets/{project-sync-CXn530xb.js → project-sync-Ce_0BglY.js} +1 -1
  148. package/src/ui/dist/assets/{sigma-04Jr12jg.js → sigma-DPaACDrh.js} +1 -1
  149. package/src/ui/dist/assets/{tooltip-BdVDl0G5.js → tooltip-C_mA6R0w.js} +1 -1
  150. package/src/ui/dist/assets/{trash-CB_GlQyC.js → trash-BvTgE5__.js} +1 -1
  151. package/src/ui/dist/assets/{useCliAccess-BL932NwS.js → useCliAccess-CgPeMOwP.js} +1 -1
  152. package/src/ui/dist/assets/{useFileDiffOverlay-B2WK7Tvq.js → useFileDiffOverlay-xPhz7P5B.js} +1 -1
  153. package/src/ui/dist/assets/{wrap-text-YC68g12z.js → wrap-text-C3Un3YQr.js} +1 -1
  154. package/src/ui/dist/assets/{zoom-out-C0RJvFiJ.js → zoom-out-BgxLa0Ri.js} +1 -1
  155. package/src/ui/dist/index.html +5 -2
  156. /package/src/ui/dist/assets/{index-CccQYZjX.css → NotebookEditor-CccQYZjX.css} +0 -0
@@ -39,6 +39,7 @@ from ..connector_profiles import (
39
39
  )
40
40
  from ..connector_runtime import conversation_identity_key, format_conversation_id, normalize_conversation_id, parse_conversation_id
41
41
  from ..config import ConfigManager
42
+ from ..config.models import SYSTEM_CONNECTOR_NAMES
42
43
  from ..home import repo_root
43
44
  from ..memory import MemoryService
44
45
  from ..network import urlopen_with_proxy as urlopen
@@ -80,6 +81,7 @@ class DaemonApp:
80
81
  self.daemon_managed_by = str(os.environ.get("DS_DAEMON_MANAGED_BY") or "manual").strip() or "manual"
81
82
  self.repo_root = repo_root()
82
83
  self.config_manager = ConfigManager(home)
84
+ self.runtime_config = self.config_manager.load_runtime_config()
83
85
  self.runners_config = self.config_manager.load_runners_config()
84
86
  self.connectors_config = self.config_manager.load_named_normalized("connectors")
85
87
  self.skill_installer = SkillInstaller(self.repo_root, home)
@@ -90,7 +92,7 @@ class DaemonApp:
90
92
  self.bash_exec_service = BashExecService(home)
91
93
  self.team_service = SingleTeamService(home)
92
94
  self.cloud_service = CloudLinkService(home)
93
- config = self.config_manager.load_named("config")
95
+ config = self.runtime_config
94
96
  skill_config = config.get("skills") if isinstance(config.get("skills"), dict) else {}
95
97
  self.skill_sync_summary = self.skill_installer.ensure_release_sync(
96
98
  installed_version=__version__,
@@ -145,9 +147,13 @@ class DaemonApp:
145
147
 
146
148
  def list_connector_statuses(self) -> list[dict[str, object]]:
147
149
  title_by_quest = self._quest_titles_by_id()
148
- items = [self._augment_connector_status(channel.status(), title_by_quest=title_by_quest) for channel in self.channels.values()]
150
+ items = [
151
+ self._augment_connector_status(channel.status(), title_by_quest=title_by_quest)
152
+ for name, channel in self.channels.items()
153
+ if name == "local" or self._is_connector_system_enabled(name)
154
+ ]
149
155
  lingzhu_config = self.connectors_config.get("lingzhu")
150
- if isinstance(lingzhu_config, dict):
156
+ if isinstance(lingzhu_config, dict) and self._is_connector_system_enabled("lingzhu"):
151
157
  items.append(self._augment_connector_status(self.config_manager.lingzhu_snapshot(lingzhu_config), title_by_quest=title_by_quest))
152
158
  return items
153
159
 
@@ -346,6 +352,8 @@ class DaemonApp:
346
352
  continue
347
353
  if connector_name not in self.channels:
348
354
  continue
355
+ if not self._is_connector_system_enabled(connector_name):
356
+ continue
349
357
  if parsed is not None and str(parsed.get("connector") or "").strip().lower() != connector_name:
350
358
  continue
351
359
  normalized_by_connector[connector_name] = {
@@ -459,19 +467,203 @@ class DaemonApp:
459
467
  b"Terminal attach token is invalid or expired.",
460
468
  )
461
469
  if runtime is None:
462
- return Response(
463
- 409,
464
- "Conflict",
465
- Headers({"Content-Type": "text/plain; charset=utf-8"}),
466
- b"Terminal runtime is no longer active.",
467
- )
470
+ try:
471
+ session = self.bash_exec_service.get_session(attach_token.quest_root, attach_token.bash_id)
472
+ except FileNotFoundError:
473
+ return Response(
474
+ 404,
475
+ "Not Found",
476
+ Headers({"Content-Type": "text/plain; charset=utf-8"}),
477
+ b"Terminal session is no longer available.",
478
+ )
479
+ status = str(session.get("status") or "").strip().lower()
480
+ kind = str(session.get("kind") or "").strip().lower()
481
+ if status in {"completed", "failed", "terminated"} or kind not in {"exec"}:
482
+ return Response(
483
+ 409,
484
+ "Conflict",
485
+ Headers({"Content-Type": "text/plain; charset=utf-8"}),
486
+ b"Terminal runtime is no longer active.",
487
+ )
468
488
  setattr(connection, "_ds_terminal_attach_token", token)
469
489
  return None
470
490
 
491
+ def _handle_logged_terminal_attach_connection(
492
+ self,
493
+ connection: ServerConnection,
494
+ *,
495
+ attach_token,
496
+ send_lock: threading.Lock,
497
+ ) -> None:
498
+ session = self.bash_exec_service.get_session(attach_token.quest_root, attach_token.bash_id)
499
+ stop_event = threading.Event()
500
+
501
+ with send_lock:
502
+ connection.send(
503
+ json.dumps(
504
+ {
505
+ "type": "ready",
506
+ "bash_id": attach_token.bash_id,
507
+ "status": session.get("status"),
508
+ "cwd": session.get("cwd"),
509
+ "workdir": session.get("workdir"),
510
+ },
511
+ ensure_ascii=False,
512
+ )
513
+ )
514
+
515
+ def _encode_exec_log_entry(entry: dict[str, Any]) -> bytes:
516
+ line = str(entry.get("line") or "")
517
+ stream = str(entry.get("stream") or "").strip().lower()
518
+ if line.startswith("__DS_PROGRESS__") or line.startswith("__DS_BASH_STATUS__"):
519
+ return b""
520
+ if line.startswith("__DS_BASH_CR__"):
521
+ payload = line[len("__DS_BASH_CR__") :].lstrip()
522
+ return f"\r\x1b[K{payload}".encode("utf-8", errors="replace")
523
+ if stream in {"prompt", "partial"}:
524
+ return line.encode("utf-8", errors="replace")
525
+ if stream == "system" and not line.strip():
526
+ return b""
527
+ return f"{line}\n".encode("utf-8", errors="replace")
528
+
529
+ def _relay_output() -> None:
530
+ last_seq = 0
531
+ try:
532
+ entries, meta = self.bash_exec_service.read_log_entries(
533
+ attach_token.quest_root,
534
+ attach_token.bash_id,
535
+ limit=2000,
536
+ order="asc",
537
+ )
538
+ if isinstance(meta.get("latest_seq"), int):
539
+ last_seq = int(meta["latest_seq"])
540
+ elif entries:
541
+ last_seq = max(int(item.get("seq") or 0) for item in entries)
542
+ for entry in entries:
543
+ payload = _encode_exec_log_entry(entry)
544
+ if not payload:
545
+ continue
546
+ with send_lock:
547
+ connection.send(payload)
548
+
549
+ while not stop_event.is_set():
550
+ entries, _meta = self.bash_exec_service.read_log_entries(
551
+ attach_token.quest_root,
552
+ attach_token.bash_id,
553
+ limit=400,
554
+ after_seq=last_seq,
555
+ order="asc",
556
+ )
557
+ for entry in entries:
558
+ last_seq = max(last_seq, int(entry.get("seq") or 0))
559
+ payload = _encode_exec_log_entry(entry)
560
+ if not payload:
561
+ continue
562
+ with send_lock:
563
+ connection.send(payload)
564
+ current = self.bash_exec_service.get_session(
565
+ attach_token.quest_root,
566
+ attach_token.bash_id,
567
+ )
568
+ status = str(current.get("status") or "").strip().lower()
569
+ if status in {"completed", "failed", "terminated"}:
570
+ with send_lock:
571
+ connection.send(
572
+ json.dumps(
573
+ {
574
+ "type": "exit",
575
+ "bash_id": attach_token.bash_id,
576
+ "status": current.get("status"),
577
+ "exit_code": current.get("exit_code"),
578
+ "stop_reason": current.get("stop_reason"),
579
+ "finished_at": current.get("finished_at"),
580
+ },
581
+ ensure_ascii=False,
582
+ )
583
+ )
584
+ return
585
+ time.sleep(TERMINAL_STREAM_IDLE_SLEEP_SECONDS)
586
+ except Exception as exc:
587
+ if stop_event.is_set():
588
+ return
589
+ try:
590
+ with send_lock:
591
+ connection.send(
592
+ json.dumps({"type": "error", "message": str(exc)}, ensure_ascii=False)
593
+ )
594
+ except Exception:
595
+ pass
596
+
597
+ relay_thread = threading.Thread(
598
+ target=_relay_output,
599
+ daemon=True,
600
+ name=f"exec-attach-{attach_token.bash_id}",
601
+ )
602
+ relay_thread.start()
603
+ try:
604
+ while True:
605
+ try:
606
+ message = connection.recv()
607
+ except ConnectionClosed:
608
+ break
609
+ if message is None:
610
+ break
611
+ if isinstance(message, bytes):
612
+ self.bash_exec_service.append_terminal_input(
613
+ attach_token.quest_root,
614
+ attach_token.bash_id,
615
+ data=message.decode("utf-8", errors="replace"),
616
+ source="web-pty",
617
+ )
618
+ continue
619
+ try:
620
+ payload = json.loads(message)
621
+ except json.JSONDecodeError:
622
+ continue
623
+ if not isinstance(payload, dict):
624
+ continue
625
+ message_type = str(payload.get("type") or "").strip().lower()
626
+ if message_type == "input":
627
+ self.bash_exec_service.append_terminal_input(
628
+ attach_token.quest_root,
629
+ attach_token.bash_id,
630
+ data=str(payload.get("data") or ""),
631
+ source="web-pty",
632
+ )
633
+ continue
634
+ if message_type == "binary_input":
635
+ raw = str(payload.get("data") or "")
636
+ if raw:
637
+ self.bash_exec_service.append_terminal_input(
638
+ attach_token.quest_root,
639
+ attach_token.bash_id,
640
+ data=base64.b64decode(raw).decode("utf-8", errors="replace"),
641
+ source="web-pty",
642
+ )
643
+ continue
644
+ if message_type == "resize":
645
+ cols = int(payload.get("cols") or 0)
646
+ rows = int(payload.get("rows") or 0)
647
+ self.bash_exec_service.resize_terminal_session(
648
+ attach_token.quest_root,
649
+ attach_token.bash_id,
650
+ cols=cols,
651
+ rows=rows,
652
+ )
653
+ continue
654
+ if message_type == "detach":
655
+ break
656
+ if message_type == "ping":
657
+ with send_lock:
658
+ connection.send(json.dumps({"type": "pong"}, ensure_ascii=False))
659
+ finally:
660
+ stop_event.set()
661
+ relay_thread.join(timeout=1)
662
+
471
663
  def _handle_terminal_attach_connection(self, connection: ServerConnection) -> None:
472
664
  token_value = str(getattr(connection, "_ds_terminal_attach_token", "") or "").strip()
473
665
  attach_token, runtime = self.bash_exec_service.consume_terminal_attach_token(token_value)
474
- if attach_token is None or runtime is None:
666
+ if attach_token is None:
475
667
  try:
476
668
  connection.close(code=1011, reason="terminal_attach_unavailable")
477
669
  except Exception:
@@ -479,6 +671,31 @@ class DaemonApp:
479
671
  return
480
672
 
481
673
  send_lock = threading.Lock()
674
+ if runtime is None:
675
+ try:
676
+ self._handle_logged_terminal_attach_connection(
677
+ connection,
678
+ attach_token=attach_token,
679
+ send_lock=send_lock,
680
+ )
681
+ except Exception as exc:
682
+ try:
683
+ with send_lock:
684
+ connection.send(
685
+ json.dumps(
686
+ {"type": "error", "message": str(exc)},
687
+ ensure_ascii=False,
688
+ )
689
+ )
690
+ except Exception:
691
+ pass
692
+ finally:
693
+ try:
694
+ connection.close()
695
+ except Exception:
696
+ pass
697
+ return
698
+
482
699
  client = TerminalClient(
483
700
  client_id=generate_id("tclient"),
484
701
  send_text=connection.send,
@@ -635,6 +852,43 @@ class DaemonApp:
635
852
  factory = get_channel_factory(name)
636
853
  return factory(home=self.home, app=self, config=self.connectors_config.get(name, {}))
637
854
 
855
+ def _system_enabled_connector_names(self) -> set[str]:
856
+ connectors = self.runtime_config.get("connectors") if isinstance(self.runtime_config.get("connectors"), dict) else {}
857
+ system_enabled = connectors.get("system_enabled") if isinstance(connectors.get("system_enabled"), dict) else {}
858
+ return {
859
+ name
860
+ for name in SYSTEM_CONNECTOR_NAMES
861
+ if bool(system_enabled.get(name, name == "qq"))
862
+ }
863
+
864
+ def _is_connector_system_enabled(self, connector_name: str) -> bool:
865
+ normalized = str(connector_name or "").strip().lower()
866
+ if normalized == "local":
867
+ return True
868
+ enabled = self._system_enabled_connector_names()
869
+ if normalized in enabled:
870
+ return True
871
+ if normalized in SYSTEM_CONNECTOR_NAMES:
872
+ return False
873
+ return True
874
+
875
+ def reload_runtime_config(self, *, restart_background: bool = True) -> dict[str, object]:
876
+ previous_enabled = self._system_enabled_connector_names()
877
+ self.runtime_config = self.config_manager.load_runtime_config()
878
+ logging_config = self.runtime_config.get("logging") if isinstance(self.runtime_config.get("logging"), dict) else {}
879
+ self.logger.level = str(logging_config.get("level") or "info").strip().lower() or "info"
880
+ enabled = self._system_enabled_connector_names()
881
+ restarted = False
882
+ if restart_background and self._server is not None and enabled != previous_enabled:
883
+ self._stop_background_connectors()
884
+ self._start_background_connectors()
885
+ restarted = True
886
+ return {
887
+ "ok": True,
888
+ "system_enabled_connectors": sorted(enabled),
889
+ "restarted_background_connectors": restarted,
890
+ }
891
+
638
892
  def reload_connectors_config(self, *, restart_background: bool = True) -> dict[str, object]:
639
893
  self.connectors_config = self.config_manager.load_named_normalized("connectors")
640
894
  register_builtin_channels(home=self.home, connectors_config=self.connectors_config)
@@ -650,7 +904,11 @@ class DaemonApp:
650
904
  return {
651
905
  "ok": True,
652
906
  "connectors": sorted(
653
- name for name, config in self.connectors_config.items() if not str(name).startswith("_") and isinstance(config, dict)
907
+ name
908
+ for name, config in self.connectors_config.items()
909
+ if not str(name).startswith("_")
910
+ and isinstance(config, dict)
911
+ and self._is_connector_system_enabled(str(name))
654
912
  ),
655
913
  }
656
914
 
@@ -671,8 +929,7 @@ class DaemonApp:
671
929
  }
672
930
 
673
931
  def _preferred_locale(self) -> str:
674
- config = self.config_manager.load_named("config")
675
- return str(config.get("default_locale") or "en-US").lower()
932
+ return str(self.runtime_config.get("default_locale") or "en-US").lower()
676
933
 
677
934
  def _polite_copy(self, *, zh: str, en: str) -> str:
678
935
  return zh if self._preferred_locale().startswith("zh") else en
@@ -741,7 +998,7 @@ class DaemonApp:
741
998
  exclude_conversation_id: str | None = None,
742
999
  preferred_connector_conversation_id: str | None = None,
743
1000
  requested_connector_bindings: list[dict[str, object]] | None = None,
744
- force_connector_rebind: bool = False,
1001
+ force_connector_rebind: bool = True,
745
1002
  requested_baseline_ref: dict[str, object] | None = None,
746
1003
  startup_contract: dict[str, object] | None = None,
747
1004
  ) -> dict:
@@ -2666,6 +2923,13 @@ class DaemonApp:
2666
2923
  }
2667
2924
 
2668
2925
  def handle_connector_inbound(self, connector_name: str, body: dict) -> dict:
2926
+ if not self._is_connector_system_enabled(connector_name):
2927
+ return {
2928
+ "ok": True,
2929
+ "accepted": False,
2930
+ "reason": "system_disabled",
2931
+ "message": f"Connector `{connector_name}` is disabled at the system level.",
2932
+ }
2669
2933
  channel = self._channel_with_bindings(connector_name)
2670
2934
  ingested = channel.ingest(body)
2671
2935
  if not ingested.get("accepted", False):
@@ -3330,6 +3594,8 @@ class DaemonApp:
3330
3594
  for connector_name, channel in self.channels.items():
3331
3595
  if connector_name == "local":
3332
3596
  continue
3597
+ if not self._is_connector_system_enabled(connector_name):
3598
+ continue
3333
3599
  connector_config = self.connectors_config.get(connector_name, {})
3334
3600
  if not isinstance(connector_config, dict):
3335
3601
  continue
@@ -3523,6 +3789,8 @@ class DaemonApp:
3523
3789
  }
3524
3790
 
3525
3791
  def _maybe_auto_bind_connector_conversation(self, connector_name: str, conversation_id: str) -> dict | None:
3792
+ if not self._is_connector_system_enabled(connector_name):
3793
+ return None
3526
3794
  connector_config = self.connectors_config.get(connector_name, {})
3527
3795
  if not isinstance(connector_config, dict):
3528
3796
  return None
@@ -3746,6 +4014,8 @@ class DaemonApp:
3746
4014
  )
3747
4015
 
3748
4016
  def _profiled_connector_configs(self, connector_name: str) -> list[tuple[str, str | None, dict[str, Any]]]:
4017
+ if not self._is_connector_system_enabled(connector_name):
4018
+ return []
3749
4019
  connector_config = self.connectors_config.get(connector_name, {})
3750
4020
  if not isinstance(connector_config, dict):
3751
4021
  return []
@@ -3763,7 +4033,7 @@ class DaemonApp:
3763
4033
 
3764
4034
  def _start_background_connectors(self) -> None:
3765
4035
  qq_config = self.connectors_config.get("qq", {})
3766
- if isinstance(qq_config, dict) and not self._qq_gateways:
4036
+ if self._is_connector_system_enabled("qq") and isinstance(qq_config, dict) and not self._qq_gateways:
3767
4037
  profiles = list_qq_profiles(qq_config)
3768
4038
  encode_profile_id = len(profiles) > 1
3769
4039
  for profile in profiles:
@@ -3783,7 +4053,7 @@ class DaemonApp:
3783
4053
  )
3784
4054
  if gateway.start():
3785
4055
  self._qq_gateways[profile_id] = gateway
3786
- if not self._telegram_polling:
4056
+ if self._is_connector_system_enabled("telegram") and not self._telegram_polling:
3787
4057
  for profile_id, profile_label, profile_config in self._profiled_connector_configs("telegram"):
3788
4058
  polling = TelegramPollingService(
3789
4059
  home=self.home,
@@ -3801,7 +4071,7 @@ class DaemonApp:
3801
4071
  )
3802
4072
  if polling.start():
3803
4073
  self._telegram_polling[profile_id] = polling
3804
- if not self._slack_socket:
4074
+ if self._is_connector_system_enabled("slack") and not self._slack_socket:
3805
4075
  for profile_id, profile_label, profile_config in self._profiled_connector_configs("slack"):
3806
4076
  slack = SlackSocketModeService(
3807
4077
  home=self.home,
@@ -3819,7 +4089,7 @@ class DaemonApp:
3819
4089
  )
3820
4090
  if slack.start():
3821
4091
  self._slack_socket[profile_id] = slack
3822
- if not self._discord_gateway:
4092
+ if self._is_connector_system_enabled("discord") and not self._discord_gateway:
3823
4093
  for profile_id, profile_label, profile_config in self._profiled_connector_configs("discord"):
3824
4094
  discord = DiscordGatewayService(
3825
4095
  home=self.home,
@@ -3837,7 +4107,7 @@ class DaemonApp:
3837
4107
  )
3838
4108
  if discord.start():
3839
4109
  self._discord_gateway[profile_id] = discord
3840
- if not self._feishu_long_connection:
4110
+ if self._is_connector_system_enabled("feishu") and not self._feishu_long_connection:
3841
4111
  for profile_id, profile_label, profile_config in self._profiled_connector_configs("feishu"):
3842
4112
  feishu = FeishuLongConnectionService(
3843
4113
  home=self.home,
@@ -3855,7 +4125,7 @@ class DaemonApp:
3855
4125
  )
3856
4126
  if feishu.start():
3857
4127
  self._feishu_long_connection[profile_id] = feishu
3858
- if not self._whatsapp_local_session:
4128
+ if self._is_connector_system_enabled("whatsapp") and not self._whatsapp_local_session:
3859
4129
  for profile_id, profile_label, profile_config in self._profiled_connector_configs("whatsapp"):
3860
4130
  whatsapp = WhatsAppLocalSessionService(
3861
4131
  home=self.home,
@@ -4486,6 +4756,7 @@ class DaemonApp:
4486
4756
  "git_commit",
4487
4757
  "git_diff_file",
4488
4758
  "git_commit_file",
4759
+ "file_change_diff",
4489
4760
  "explorer",
4490
4761
  "quest_search",
4491
4762
  "node_traces",
@@ -4498,7 +4769,7 @@ class DaemonApp:
4498
4769
  payload = result(**params, path=self.path)
4499
4770
  elif method == "GET":
4500
4771
  payload = result(**params) if params else result()
4501
- elif route_name in {"document_open", "document_asset_upload", "chat", "command", "quest_control", "config_save", "quest_create", "quest_baseline_binding", "run_create", "qq_inbound", "connector_inbound", "docs_open", "admin_shutdown", "bash_stop", "quest_settings", "quest_bindings", "quest_delete", "terminal_session_ensure", "terminal_attach", "terminal_input", "stage_view", "latex_init", "latex_compile", "system_update_action"}:
4772
+ elif route_name in {"document_open", "document_asset_upload", "chat", "command", "quest_control", "config_save", "quest_create", "quest_baseline_binding", "run_create", "qq_inbound", "connector_inbound", "docs_open", "admin_shutdown", "bash_stop", "quest_settings", "quest_bindings", "quest_delete", "quest_layout_update", "terminal_session_ensure", "terminal_attach", "terminal_input", "stage_view", "latex_init", "latex_compile", "system_update_action"}:
4502
4773
  payload = result(**params, body=body)
4503
4774
  elif route_name == "config_validate":
4504
4775
  payload = result(body)
@@ -4,6 +4,7 @@ from collections import defaultdict
4
4
  from pathlib import Path
5
5
  from typing import Any
6
6
 
7
+ from ..artifact.metrics import extract_latest_metric
7
8
  from ..shared import read_json, run_command
8
9
  from .service import branch_exists, current_branch, head_commit
9
10
 
@@ -308,16 +309,9 @@ def _collect_branch_state(repo: Path) -> dict[str, dict[str, Any]]:
308
309
  state["parent_branch"] = record.get("parent_branch")
309
310
  if record.get("worktree_root"):
310
311
  state["worktree_root"] = record.get("worktree_root")
311
- if isinstance(record.get("metrics_summary"), dict) and record["metrics_summary"]:
312
- key = next(iter(record["metrics_summary"]))
313
- state["latest_metric"] = {
314
- "key": ((record.get("progress_eval") or {}).get("primary_metric_id")) or key,
315
- "value": record["metrics_summary"].get(
316
- ((record.get("progress_eval") or {}).get("primary_metric_id")) or key,
317
- record["metrics_summary"].get(key),
318
- ),
319
- "delta_vs_baseline": ((record.get("progress_eval") or {}).get("delta_vs_baseline")),
320
- }
312
+ latest_metric = extract_latest_metric(record)
313
+ if latest_metric is not None:
314
+ state["latest_metric"] = latest_metric
321
315
  if record.get("kind") == "run":
322
316
  state["latest_result"] = {
323
317
  "run_id": record.get("run_id"),
@@ -365,6 +359,8 @@ def _classify_ref(ref: str, state: dict[str, Any]) -> dict[str, str]:
365
359
  return {"branch_kind": "quest", "tier": "major", "mode": "ideas"}
366
360
  if ref.startswith("idea/"):
367
361
  return {"branch_kind": "idea", "tier": "major", "mode": "ideas"}
362
+ if ref.startswith("paper/"):
363
+ return {"branch_kind": "paper", "tier": "major", "mode": "ideas"}
368
364
  if ref.startswith("analysis/") or run_id.startswith("analysis") or run_kind == "analysis-campaign":
369
365
  return {"branch_kind": "analysis", "tier": "minor", "mode": "analysis"}
370
366
  return {"branch_kind": "implementation", "tier": "major", "mode": "ideas"}