@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
@@ -448,7 +448,7 @@ def run_monitor(session_dir: Path) -> int:
448
448
  _terminate_process(process, process_group_id)
449
449
  stop_requested = True
450
450
 
451
- if session_kind == "terminal" and output_fd is not None and process.poll() is None:
451
+ if output_fd is not None and process.poll() is None:
452
452
  cursor_payload = read_json(input_cursor_path, {}) or {}
453
453
  offset = int(cursor_payload.get("offset") or 0)
454
454
  input_entries = read_jsonl(input_path)
@@ -555,6 +555,7 @@ class BashExecService:
555
555
  before_seq: int | None = None,
556
556
  after_seq: int | None = None,
557
557
  order: str = "asc",
558
+ prefer_visible: bool = False,
558
559
  ) -> tuple[list[dict[str, Any]], dict[str, Any]]:
559
560
  if not self.meta_path(quest_root, bash_id).exists():
560
561
  raise FileNotFoundError(f"Unknown bash session `{bash_id}`.")
@@ -579,9 +580,16 @@ class BashExecService:
579
580
  entries = [entry for entry in entries if int(entry.get("seq") or 0) > normalized_after]
580
581
  if normalized_before is not None:
581
582
  entries = [entry for entry in entries if int(entry.get("seq") or 0) < normalized_before]
583
+ selection_pool = entries
584
+ if prefer_visible:
585
+ visible_entries = [
586
+ entry for entry in entries if str(entry.get("stream") or "") not in {"system", "prompt"}
587
+ ]
588
+ if visible_entries:
589
+ selection_pool = visible_entries
582
590
  normalized_limit = max(1, limit)
583
- truncated = len(entries) > normalized_limit
584
- selected = entries[-normalized_limit:]
591
+ truncated = len(selection_pool) > normalized_limit
592
+ selected = selection_pool[-normalized_limit:]
585
593
  if order == "desc":
586
594
  selected = list(reversed(selected))
587
595
  tail_start_seq = int(selected[0].get("seq") or 0) if selected else None
@@ -1025,15 +1033,14 @@ class BashExecService:
1025
1033
  if not normalized_data:
1026
1034
  raise ValueError("terminal_input_required")
1027
1035
  session = self.reconcile_session(quest_root, bash_id)
1028
- if _normalize_string(session.get("kind")).lower() != "terminal":
1029
- raise ValueError("not_terminal_session")
1030
1036
  status = _normalize_string(session.get("status")).lower()
1031
1037
  if status in TERMINAL_STATUSES:
1032
1038
  raise ValueError("terminal_session_inactive")
1033
1039
  runtime = self._terminal_runtime_manager.get_runtime(quest_root, bash_id)
1034
- if runtime is None:
1040
+ if runtime is None and _normalize_string(session.get("kind")).lower() == "terminal":
1035
1041
  raise ValueError("terminal_runtime_inactive")
1036
- runtime.write_input(normalized_data)
1042
+ if runtime is not None:
1043
+ runtime.write_input(normalized_data)
1037
1044
 
1038
1045
  entry = {
1039
1046
  "input_id": generate_id("tin"),
@@ -1081,9 +1088,10 @@ class BashExecService:
1081
1088
  return True
1082
1089
 
1083
1090
  def issue_terminal_attach_token(self, quest_root: Path, bash_id: str, *, ttl_seconds: int = 60) -> dict[str, Any]:
1084
- runtime = self._terminal_runtime_manager.get_runtime(quest_root, bash_id)
1085
- if runtime is None:
1086
- raise ValueError("terminal_runtime_inactive")
1091
+ session = self.reconcile_session(quest_root, bash_id)
1092
+ status = _normalize_string(session.get("status")).lower()
1093
+ if status in TERMINAL_STATUSES:
1094
+ raise ValueError("terminal_session_inactive")
1087
1095
  token = self._terminal_runtime_manager.issue_attach_token(
1088
1096
  quest_root,
1089
1097
  bash_id,
@@ -293,6 +293,20 @@ class QQRelayChannel(BaseChannel):
293
293
  def matches_profile(item: dict[str, Any], profile_id: str) -> bool:
294
294
  item_profile_id = str(item.get("profile_id") or "").strip()
295
295
  return item_profile_id == profile_id or (not item_profile_id and len(profiles) == 1)
296
+
297
+ def count_profile_records(path: Path, profile_id: str) -> int:
298
+ total = 0
299
+ for raw in read_jsonl(path):
300
+ if not isinstance(raw, dict):
301
+ continue
302
+ record_profile_id = str(raw.get("profile_id") or "").strip()
303
+ if not record_profile_id:
304
+ parsed = parse_conversation_id(str(raw.get("conversation_id") or "").strip())
305
+ record_profile_id = str((parsed or {}).get("profile_id") or "").strip()
306
+ if record_profile_id == profile_id or (not record_profile_id and len(profiles) == 1):
307
+ total += 1
308
+ return total
309
+
296
310
  profile_snapshots = []
297
311
  for profile in profiles:
298
312
  profile_id = str(profile.get("profile_id") or "").strip()
@@ -342,6 +356,9 @@ class QQRelayChannel(BaseChannel):
342
356
  "discovered_targets": profile_targets,
343
357
  "recent_conversations": profile_recent_conversations,
344
358
  "bindings": profile_bindings,
359
+ "inbox_count": count_profile_records(self.inbox_path, profile_id),
360
+ "outbox_count": count_profile_records(self.outbox_path, profile_id),
361
+ "ignored_count": count_profile_records(self.ignored_path, profile_id),
345
362
  "target_count": len(profile_targets),
346
363
  "binding_count": len(profile_bindings),
347
364
  "last_error": gateway_state.get("last_error") if isinstance(gateway_state, dict) else None,
@@ -282,6 +282,19 @@ class GenericRelayChannel(BaseChannel):
282
282
  item_profile_id = str(item.get("profile_id") or "").strip()
283
283
  return item_profile_id == profile_id or (not item_profile_id and len(profiles) == 1)
284
284
 
285
+ def count_profile_records(path: Path, profile_id: str) -> int:
286
+ total = 0
287
+ for raw in read_jsonl(path):
288
+ if not isinstance(raw, dict):
289
+ continue
290
+ record_profile_id = str(raw.get("profile_id") or "").strip()
291
+ if not record_profile_id:
292
+ parsed = parse_conversation_id(str(raw.get("conversation_id") or "").strip())
293
+ record_profile_id = str((parsed or {}).get("profile_id") or "").strip()
294
+ if record_profile_id == profile_id or (not record_profile_id and len(profiles) == 1):
295
+ total += 1
296
+ return total
297
+
285
298
  profile_snapshots = []
286
299
  for profile in profiles:
287
300
  profile_id = str(profile.get("profile_id") or "").strip()
@@ -322,6 +335,9 @@ class GenericRelayChannel(BaseChannel):
322
335
  "discovered_targets": profile_targets,
323
336
  "recent_conversations": profile_recent_conversations,
324
337
  "bindings": profile_bindings,
338
+ "inbox_count": count_profile_records(self.inbox_path, profile_id),
339
+ "outbox_count": count_profile_records(self.outbox_path, profile_id),
340
+ "ignored_count": count_profile_records(self.ignored_path, profile_id),
325
341
  "target_count": len(profile_targets),
326
342
  "binding_count": len(profile_bindings),
327
343
  "last_error": profile_runtime_state.get("last_error") if isinstance(profile_runtime_state, dict) else None,
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  CONFIG_NAMES = ("config", "runners", "connectors", "plugins", "mcp_servers")
7
7
  REQUIRED_CONFIG_NAMES = ("config", "runners", "connectors")
8
8
  OPTIONAL_CONFIG_NAMES = ("plugins", "mcp_servers")
9
+ SYSTEM_CONNECTOR_NAMES = ("qq", "telegram", "discord", "slack", "feishu", "whatsapp", "lingzhu")
9
10
 
10
11
 
11
12
  @dataclass(frozen=True)
@@ -20,6 +21,10 @@ def config_filename(name: str) -> str:
20
21
  return f"{name}.yaml"
21
22
 
22
23
 
24
+ def default_system_enabled_connectors() -> dict[str, bool]:
25
+ return {name: name == "qq" for name in SYSTEM_CONNECTOR_NAMES}
26
+
27
+
23
28
  def default_config(home: Path) -> dict:
24
29
  return {
25
30
  "home": str(home),
@@ -65,6 +70,7 @@ def default_config(home: Path) -> dict:
65
70
  "auto_ack": True,
66
71
  "milestone_push": True,
67
72
  "direct_chat_enabled": True,
73
+ "system_enabled": default_system_enabled_connectors(),
68
74
  },
69
75
  "cloud": {
70
76
  "enabled": False,
@@ -37,6 +37,7 @@ from .models import (
37
37
  OPTIONAL_CONFIG_NAMES,
38
38
  REQUIRED_CONFIG_NAMES,
39
39
  ConfigFileInfo,
40
+ SYSTEM_CONNECTOR_NAMES,
40
41
  config_filename,
41
42
  default_payload,
42
43
  )
@@ -95,6 +96,33 @@ class ConfigManager:
95
96
  def load_runners_config(self) -> dict:
96
97
  return apply_runners_runtime_overrides(self.load_named_normalized("runners"))
97
98
 
99
+ def load_runtime_config(self) -> dict:
100
+ return self.load_named_normalized("config")
101
+
102
+ def system_connector_gates(self) -> dict[str, bool]:
103
+ config = self.load_runtime_config()
104
+ connectors = config.get("connectors") if isinstance(config.get("connectors"), dict) else {}
105
+ system_enabled = connectors.get("system_enabled") if isinstance(connectors.get("system_enabled"), dict) else {}
106
+ return {
107
+ name: self._coerce_bool(system_enabled.get(name), default=name == "qq")
108
+ for name in SYSTEM_CONNECTOR_NAMES
109
+ }
110
+
111
+ def system_enabled_connector_names(self) -> list[str]:
112
+ gates = self.system_connector_gates()
113
+ return [name for name in SYSTEM_CONNECTOR_NAMES if gates.get(name, False)]
114
+
115
+ def is_connector_system_enabled(self, name: str) -> bool:
116
+ normalized = str(name or "").strip().lower()
117
+ if not normalized:
118
+ return False
119
+ if normalized == "local":
120
+ return True
121
+ gates = self.system_connector_gates()
122
+ if normalized in gates:
123
+ return gates[normalized]
124
+ return True
125
+
98
126
  def load_named_text(self, name: str, create_optional: bool = False) -> str:
99
127
  path = self.path_for(name)
100
128
  if create_optional and name in OPTIONAL_CONFIG_NAMES and not path.exists():
@@ -1047,6 +1075,12 @@ Use **Test** when the file exposes runtime dependencies.
1047
1075
  checked_at = utc_now()
1048
1076
  binary = str(config.get("binary") or "codex").strip() or "codex"
1049
1077
  resolved_binary = resolve_runner_binary(binary, runner_name="codex")
1078
+ raw_reasoning_effort = config.get("model_reasoning_effort")
1079
+ reasoning_effort = (
1080
+ str(raw_reasoning_effort).strip()
1081
+ if raw_reasoning_effort is not None and str(raw_reasoning_effort).strip()
1082
+ else ("xhigh" if raw_reasoning_effort is None else None)
1083
+ )
1050
1084
  details: dict[str, object] = {
1051
1085
  "binary": binary,
1052
1086
  "resolved_binary": resolved_binary,
@@ -1054,7 +1088,7 @@ Use **Test** when the file exposes runtime dependencies.
1054
1088
  "model": str(config.get("model") or "gpt-5.4"),
1055
1089
  "approval_policy": str(config.get("approval_policy") or "on-request"),
1056
1090
  "sandbox_mode": str(config.get("sandbox_mode") or "workspace-write"),
1057
- "reasoning_effort": str(config.get("model_reasoning_effort") or "xhigh"),
1091
+ "reasoning_effort": reasoning_effort,
1058
1092
  "checked_at": checked_at,
1059
1093
  }
1060
1094
  if not resolved_binary:
@@ -1087,7 +1121,6 @@ Use **Test** when the file exposes runtime dependencies.
1087
1121
  approval_policy = str(config.get("approval_policy") or "on-request").strip()
1088
1122
  if approval_policy:
1089
1123
  command.extend(["-c", f'approval_policy="{approval_policy}"'])
1090
- reasoning_effort = str(config.get("model_reasoning_effort") or "xhigh").strip()
1091
1124
  if reasoning_effort:
1092
1125
  command.extend(["-c", f'model_reasoning_effort="{reasoning_effort}"'])
1093
1126
  sandbox_mode = str(config.get("sandbox_mode") or "workspace-write").strip()
@@ -1378,6 +1411,8 @@ Use **Test** when the file exposes runtime dependencies.
1378
1411
  normalized = self._deep_merge(defaults, payload)
1379
1412
  bootstrap = normalized.get("bootstrap") if isinstance(normalized.get("bootstrap"), dict) else {}
1380
1413
  raw_bootstrap = payload.get("bootstrap") if isinstance(payload.get("bootstrap"), dict) else {}
1414
+ connectors = normalized.get("connectors") if isinstance(normalized.get("connectors"), dict) else {}
1415
+ raw_connectors = payload.get("connectors") if isinstance(payload.get("connectors"), dict) else {}
1381
1416
  default_locale = str(defaults.get("default_locale") or "").strip()
1382
1417
  current_locale = str(normalized.get("default_locale") or "").strip()
1383
1418
  locale_source = str(raw_bootstrap.get("locale_source") or "").strip().lower()
@@ -1401,6 +1436,25 @@ Use **Test** when the file exposes runtime dependencies.
1401
1436
  bootstrap["locale_initialized_at"] = bootstrap.get("locale_initialized_at")
1402
1437
  bootstrap["locale_initialized_browser_locale"] = bootstrap.get("locale_initialized_browser_locale")
1403
1438
  normalized["bootstrap"] = bootstrap
1439
+ raw_system_enabled = raw_connectors.get("system_enabled") if isinstance(raw_connectors.get("system_enabled"), dict) else {}
1440
+ default_system_enabled = (
1441
+ defaults.get("connectors", {}).get("system_enabled")
1442
+ if isinstance(defaults.get("connectors"), dict)
1443
+ else {}
1444
+ )
1445
+ current_system_enabled = (
1446
+ connectors.get("system_enabled")
1447
+ if isinstance(connectors.get("system_enabled"), dict)
1448
+ else {}
1449
+ )
1450
+ connectors["system_enabled"] = {
1451
+ name: self._coerce_bool(
1452
+ raw_system_enabled.get(name, current_system_enabled.get(name)),
1453
+ default=bool(default_system_enabled.get(name, False)),
1454
+ )
1455
+ for name in SYSTEM_CONNECTOR_NAMES
1456
+ }
1457
+ normalized["connectors"] = connectors
1404
1458
  return normalized
1405
1459
 
1406
1460
  @staticmethod
@@ -1413,6 +1467,20 @@ Use **Test** when the file exposes runtime dependencies.
1413
1467
  return False
1414
1468
  return abs(initial - 1.0) < 1e-9 and abs(multiplier - 2.0) < 1e-9 and abs(max_backoff - 8.0) < 1e-9
1415
1469
 
1470
+ @staticmethod
1471
+ def _coerce_bool(value: object, *, default: bool = False) -> bool:
1472
+ if value is None:
1473
+ return default
1474
+ if isinstance(value, bool):
1475
+ return value
1476
+ if isinstance(value, str):
1477
+ normalized = value.strip().lower()
1478
+ if normalized in {"1", "true", "yes", "on", "y"}:
1479
+ return True
1480
+ if normalized in {"0", "false", "no", "off", "n", ""}:
1481
+ return False
1482
+ return bool(value)
1483
+
1416
1484
  def _normalize_plugins_payload(self, payload: dict) -> dict:
1417
1485
  normalized = deepcopy(payload)
1418
1486
  if "load_paths" not in normalized and isinstance(normalized.get("search_paths"), list):