@researai/deepscientist 1.5.15 → 1.5.17

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 (202) hide show
  1. package/README.md +385 -104
  2. package/bin/ds.js +1241 -110
  3. package/docs/en/00_QUICK_START.md +100 -19
  4. package/docs/en/01_SETTINGS_REFERENCE.md +34 -1
  5. package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
  6. package/docs/en/05_TUI_GUIDE.md +6 -0
  7. package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
  8. package/docs/en/09_DOCTOR.md +25 -8
  9. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  10. package/docs/en/15_CODEX_PROVIDER_SETUP.md +37 -11
  11. package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  12. package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
  13. package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
  14. package/docs/en/21_LOCAL_MODEL_BACKENDS_GUIDE.md +283 -0
  15. package/docs/en/91_DEVELOPMENT.md +237 -0
  16. package/docs/en/README.md +24 -2
  17. package/docs/zh/00_QUICK_START.md +89 -19
  18. package/docs/zh/01_SETTINGS_REFERENCE.md +34 -1
  19. package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
  20. package/docs/zh/05_TUI_GUIDE.md +6 -0
  21. package/docs/zh/09_DOCTOR.md +26 -9
  22. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  23. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +37 -11
  24. package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  25. package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
  26. package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
  27. package/docs/zh/21_LOCAL_MODEL_BACKENDS_GUIDE.md +281 -0
  28. package/docs/zh/README.md +24 -2
  29. package/install.sh +46 -4
  30. package/package.json +2 -1
  31. package/pyproject.toml +1 -1
  32. package/src/deepscientist/__init__.py +1 -1
  33. package/src/deepscientist/acp/envelope.py +6 -0
  34. package/src/deepscientist/artifact/service.py +647 -22
  35. package/src/deepscientist/bash_exec/service.py +234 -9
  36. package/src/deepscientist/bridges/connectors.py +8 -2
  37. package/src/deepscientist/cli.py +115 -19
  38. package/src/deepscientist/codex_cli_compat.py +367 -22
  39. package/src/deepscientist/config/models.py +2 -1
  40. package/src/deepscientist/config/service.py +183 -13
  41. package/src/deepscientist/daemon/api/handlers.py +255 -31
  42. package/src/deepscientist/daemon/api/router.py +9 -0
  43. package/src/deepscientist/daemon/app.py +1146 -105
  44. package/src/deepscientist/diagnostics/__init__.py +6 -0
  45. package/src/deepscientist/diagnostics/runner_failures.py +130 -0
  46. package/src/deepscientist/doctor.py +207 -3
  47. package/src/deepscientist/gitops/__init__.py +10 -1
  48. package/src/deepscientist/gitops/diff.py +129 -0
  49. package/src/deepscientist/gitops/service.py +4 -1
  50. package/src/deepscientist/mcp/server.py +39 -0
  51. package/src/deepscientist/prompts/builder.py +275 -34
  52. package/src/deepscientist/quest/layout.py +15 -2
  53. package/src/deepscientist/quest/service.py +707 -55
  54. package/src/deepscientist/quest/stage_views.py +6 -1
  55. package/src/deepscientist/runners/codex.py +143 -43
  56. package/src/deepscientist/shared.py +19 -0
  57. package/src/deepscientist/skills/__init__.py +2 -2
  58. package/src/deepscientist/skills/installer.py +196 -5
  59. package/src/deepscientist/skills/registry.py +66 -0
  60. package/src/prompts/connectors/qq.md +18 -8
  61. package/src/prompts/connectors/weixin.md +16 -6
  62. package/src/prompts/contracts/shared_interaction.md +14 -2
  63. package/src/prompts/system.md +23 -5
  64. package/src/prompts/system_copilot.md +56 -0
  65. package/src/skills/analysis-campaign/SKILL.md +1 -0
  66. package/src/skills/baseline/SKILL.md +8 -0
  67. package/src/skills/decision/SKILL.md +8 -0
  68. package/src/skills/experiment/SKILL.md +8 -0
  69. package/src/skills/figure-polish/SKILL.md +1 -0
  70. package/src/skills/finalize/SKILL.md +1 -0
  71. package/src/skills/idea/SKILL.md +1 -0
  72. package/src/skills/intake-audit/SKILL.md +8 -0
  73. package/src/skills/mentor/SKILL.md +217 -0
  74. package/src/skills/mentor/references/correction-rules.md +210 -0
  75. package/src/skills/mentor/references/knowledge-profile.md +91 -0
  76. package/src/skills/mentor/references/persona-profile.md +138 -0
  77. package/src/skills/mentor/references/taste-profile.md +128 -0
  78. package/src/skills/mentor/references/thought-style-profile.md +138 -0
  79. package/src/skills/mentor/references/work-profile.md +289 -0
  80. package/src/skills/mentor/references/workflow-profile.md +240 -0
  81. package/src/skills/optimize/SKILL.md +1 -0
  82. package/src/skills/rebuttal/SKILL.md +1 -0
  83. package/src/skills/review/SKILL.md +1 -0
  84. package/src/skills/scout/SKILL.md +8 -0
  85. package/src/skills/write/SKILL.md +1 -0
  86. package/src/tui/dist/app/AppContainer.js +19 -11
  87. package/src/tui/dist/index.js +4 -1
  88. package/src/tui/dist/lib/api.js +33 -3
  89. package/src/tui/package.json +1 -1
  90. package/src/ui/dist/assets/AiManusChatView-Bv-Z8YpU.js +204 -0
  91. package/src/ui/dist/assets/AnalysisPlugin-BCKAfjba.js +1 -0
  92. package/src/ui/dist/assets/CliPlugin-BCKcpc35.js +109 -0
  93. package/src/ui/dist/assets/CodeEditorPlugin-DbOfSJ8K.js +2 -0
  94. package/src/ui/dist/assets/CodeViewerPlugin-CbaFRrUU.js +270 -0
  95. package/src/ui/dist/assets/DocViewerPlugin-DAjLVeQD.js +7 -0
  96. package/src/ui/dist/assets/GitCommitViewerPlugin-CIUqbUDO.js +1 -0
  97. package/src/ui/dist/assets/GitDiffViewerPlugin-CQACjoAA.js +6 -0
  98. package/src/ui/dist/assets/GitSnapshotViewer-0r4nLPke.js +30 -0
  99. package/src/ui/dist/assets/ImageViewerPlugin-nBOmI2v_.js +26 -0
  100. package/src/ui/dist/assets/LabCopilotPanel-BHxOxF4z.js +14 -0
  101. package/src/ui/dist/assets/LabPlugin-BKoZGs95.js +22 -0
  102. package/src/ui/dist/assets/LatexPlugin-ZwtV8pIp.js +25 -0
  103. package/src/ui/dist/assets/MarkdownViewerPlugin-DKqVfKyW.js +128 -0
  104. package/src/ui/dist/assets/MarketplacePlugin-BwxStZ9D.js +13 -0
  105. package/src/ui/dist/assets/NotebookEditor-BEQhaQbt.js +81 -0
  106. package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
  107. package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
  108. package/src/ui/dist/assets/NotebookEditor-DB9N_T9q.js +361 -0
  109. package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
  110. package/src/ui/dist/assets/PdfLoader-eWBONbQP.js +16 -0
  111. package/src/ui/dist/assets/PdfMarkdownPlugin-D22YOZL3.js +1 -0
  112. package/src/ui/dist/assets/PdfViewerPlugin-c-RK9DLM.js +17 -0
  113. package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
  114. package/src/ui/dist/assets/SearchPlugin-CxF9ytAx.js +16 -0
  115. package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
  116. package/src/ui/dist/assets/TextViewerPlugin-C5xqeeUH.js +54 -0
  117. package/src/ui/dist/assets/VNCViewer-BoLGLnHz.js +11 -0
  118. package/src/ui/dist/assets/bot-DREQOxzP.js +6 -0
  119. package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
  120. package/src/ui/dist/assets/chevron-up-C9Qpx4DE.js +6 -0
  121. package/src/ui/dist/assets/code-WlFHE7z_.js +6 -0
  122. package/src/ui/dist/assets/file-content-BZMz3RYp.js +1 -0
  123. package/src/ui/dist/assets/file-diff-panel-CQhw0jS2.js +1 -0
  124. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
  125. package/src/ui/dist/assets/file-socket-CfQPKQKj.js +1 -0
  126. package/src/ui/dist/assets/git-commit-horizontal-DxZ8DCZh.js +6 -0
  127. package/src/ui/dist/assets/image-Bgl4VIyx.js +6 -0
  128. package/src/ui/dist/assets/index-BpV6lusQ.css +33 -0
  129. package/src/ui/dist/assets/index-CBNVuWcP.js +2496 -0
  130. package/src/ui/dist/assets/index-CwNu1aH4.js +11 -0
  131. package/src/ui/dist/assets/index-DrUnlf6K.js +1 -0
  132. package/src/ui/dist/assets/index-NW-h8VzN.js +1 -0
  133. package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
  134. package/src/ui/dist/assets/pdf-effect-queue-J8OnM0jE.js +6 -0
  135. package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
  136. package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
  137. package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
  138. package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
  139. package/src/ui/dist/assets/popover-CLc0pPP8.js +1 -0
  140. package/src/ui/dist/assets/project-sync-C9IdzdZW.js +1 -0
  141. package/src/ui/dist/assets/select-Cs2PmzwL.js +11 -0
  142. package/src/ui/dist/assets/sigma-ClKcHAXm.js +6 -0
  143. package/src/ui/dist/assets/trash-DwpbFr3w.js +11 -0
  144. package/src/ui/dist/assets/useCliAccess-NQ8m0Let.js +1 -0
  145. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
  146. package/src/ui/dist/assets/wrap-text-BC-Hltpd.js +11 -0
  147. package/src/ui/dist/assets/zoom-out-E_gaeAxL.js +11 -0
  148. package/src/ui/dist/index.html +5 -2
  149. package/src/ui/dist/assets/AiManusChatView-DDjbFnbt.js +0 -26597
  150. package/src/ui/dist/assets/AnalysisPlugin-Yb5IdmaU.js +0 -123
  151. package/src/ui/dist/assets/CliPlugin-e64sreyu.js +0 -31037
  152. package/src/ui/dist/assets/CodeEditorPlugin-C4D2TIkU.js +0 -427
  153. package/src/ui/dist/assets/CodeViewerPlugin-BVoNZIvC.js +0 -905
  154. package/src/ui/dist/assets/DocViewerPlugin-CLChbllo.js +0 -278
  155. package/src/ui/dist/assets/GitDiffViewerPlugin-C4xeFyFQ.js +0 -2661
  156. package/src/ui/dist/assets/ImageViewerPlugin-OiMUAcLi.js +0 -500
  157. package/src/ui/dist/assets/LabCopilotPanel-BjD2ThQF.js +0 -4104
  158. package/src/ui/dist/assets/LabPlugin-DQPg-NrB.js +0 -2677
  159. package/src/ui/dist/assets/LatexPlugin-CI05XAV9.js +0 -1792
  160. package/src/ui/dist/assets/MarkdownViewerPlugin-DpeBLYZf.js +0 -308
  161. package/src/ui/dist/assets/MarketplacePlugin-DolE58Q2.js +0 -413
  162. package/src/ui/dist/assets/NotebookEditor-7Qm2rSWD.js +0 -4214
  163. package/src/ui/dist/assets/NotebookEditor-C1kWaxKi.js +0 -84873
  164. package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
  165. package/src/ui/dist/assets/PdfLoader-BfOHw8Zw.js +0 -25468
  166. package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
  167. package/src/ui/dist/assets/PdfMarkdownPlugin-BulDREv1.js +0 -409
  168. package/src/ui/dist/assets/PdfViewerPlugin-C-daaOaL.js +0 -3095
  169. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
  170. package/src/ui/dist/assets/SearchPlugin-CjpaiJ3A.js +0 -741
  171. package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
  172. package/src/ui/dist/assets/TextViewerPlugin-BxIyqPQC.js +0 -472
  173. package/src/ui/dist/assets/VNCViewer-HAg9mF7M.js +0 -18821
  174. package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
  175. package/src/ui/dist/assets/bot-0DYntytV.js +0 -21
  176. package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
  177. package/src/ui/dist/assets/code-B20Slj_w.js +0 -17
  178. package/src/ui/dist/assets/file-content-DT24KFma.js +0 -377
  179. package/src/ui/dist/assets/file-diff-panel-DK13YPql.js +0 -92
  180. package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
  181. package/src/ui/dist/assets/file-socket-B4T2o4nR.js +0 -58
  182. package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
  183. package/src/ui/dist/assets/image-DSeR_sDS.js +0 -18
  184. package/src/ui/dist/assets/index-BrFje2Uk.js +0 -120
  185. package/src/ui/dist/assets/index-BwRJaoTl.js +0 -25
  186. package/src/ui/dist/assets/index-D_E4281X.js +0 -221322
  187. package/src/ui/dist/assets/index-DnYB3xb1.js +0 -159
  188. package/src/ui/dist/assets/index-G7AcWcMu.css +0 -12594
  189. package/src/ui/dist/assets/monaco-LExaAN3Y.js +0 -623
  190. package/src/ui/dist/assets/pdf-effect-queue-BJk5okWJ.js +0 -47
  191. package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
  192. package/src/ui/dist/assets/popover-D3Gg_FoV.js +0 -476
  193. package/src/ui/dist/assets/project-sync-C_ygLlVU.js +0 -297
  194. package/src/ui/dist/assets/select-CpAK6uWm.js +0 -1690
  195. package/src/ui/dist/assets/sigma-DEccaSgk.js +0 -22
  196. package/src/ui/dist/assets/square-check-big-uUfyVsbD.js +0 -17
  197. package/src/ui/dist/assets/trash-CXvwwSe8.js +0 -32
  198. package/src/ui/dist/assets/useCliAccess-Bnop4mgR.js +0 -957
  199. package/src/ui/dist/assets/useFileDiffOverlay-B8eUAX0I.js +0 -53
  200. package/src/ui/dist/assets/wrap-text-9vbOBpkW.js +0 -35
  201. package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
  202. package/src/ui/dist/assets/zoom-out-BgVMmOW4.js +0 -34
@@ -32,6 +32,8 @@ DEFAULT_POLL_INTERVAL_SECONDS = 0.35
32
32
  TERMINAL_STATUSES = {"completed", "failed", "terminated"}
33
33
  DEFAULT_TERMINAL_SESSION_ID = "terminal-main"
34
34
  BASH_WATCHDOG_AFTER_SECONDS = 1800
35
+ SUMMARY_RECENT_SESSION_LIMIT = 256
36
+ SUMMARY_RUNNING_SESSION_LIMIT = 64
35
37
  INPUT_ESCAPE_SEQUENCE_RE = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b[@-_]")
36
38
 
37
39
 
@@ -373,6 +375,65 @@ class BashExecService:
373
375
  str(session.get("bash_id") or ""),
374
376
  )
375
377
 
378
+ @classmethod
379
+ def _normalize_summary_session_list(
380
+ cls,
381
+ sessions: Any,
382
+ *,
383
+ limit: int,
384
+ ) -> list[dict[str, Any]]:
385
+ max_items = max(0, int(limit or 0))
386
+ if max_items <= 0:
387
+ return []
388
+ normalized: dict[str, dict[str, Any]] = {}
389
+ for raw in sessions or []:
390
+ if not isinstance(raw, dict):
391
+ continue
392
+ compact = cls._summary_session_payload(raw)
393
+ bash_id = _normalize_string(compact.get("bash_id"))
394
+ if not bash_id:
395
+ continue
396
+ normalized[bash_id] = compact
397
+ ordered = sorted(normalized.values(), key=cls._summary_sort_key, reverse=True)
398
+ return ordered[:max_items]
399
+
400
+ @classmethod
401
+ def _merge_summary_session_list(
402
+ cls,
403
+ sessions: Any,
404
+ compact: dict[str, Any],
405
+ *,
406
+ limit: int,
407
+ ) -> list[dict[str, Any]]:
408
+ bash_id = _normalize_string(compact.get("bash_id"))
409
+ merged = cls._normalize_summary_session_list(sessions, limit=max(1, int(limit or 0)) + 1)
410
+ merged = [
411
+ item
412
+ for item in merged
413
+ if _normalize_string(item.get("bash_id")) != bash_id
414
+ ]
415
+ if bash_id:
416
+ merged.append(cls._summary_session_payload(compact))
417
+ merged.sort(key=cls._summary_sort_key, reverse=True)
418
+ return merged[: max(1, int(limit or 0))]
419
+
420
+ @classmethod
421
+ def _remove_summary_session(
422
+ cls,
423
+ sessions: Any,
424
+ bash_id: str,
425
+ *,
426
+ limit: int,
427
+ ) -> list[dict[str, Any]]:
428
+ normalized_bash_id = _normalize_string(bash_id)
429
+ if not normalized_bash_id:
430
+ return cls._normalize_summary_session_list(sessions, limit=limit)
431
+ return [
432
+ item
433
+ for item in cls._normalize_summary_session_list(sessions, limit=limit)
434
+ if _normalize_string(item.get("bash_id")) != normalized_bash_id
435
+ ][: max(1, int(limit or 0))]
436
+
376
437
  @staticmethod
377
438
  def _is_active_status(value: object) -> bool:
378
439
  return _coerce_session_status(value) in {"running", "terminating"}
@@ -382,9 +443,31 @@ class BashExecService:
382
443
  "session_count": 0,
383
444
  "running_count": 0,
384
445
  "latest_session": None,
446
+ "recent_sessions": [],
447
+ "running_sessions": [],
385
448
  "updated_at": utc_now(),
386
449
  }
387
450
 
451
+ def _normalize_summary_payload(self, summary: Any) -> dict[str, Any]:
452
+ merged = {**self._default_summary(), **(summary if isinstance(summary, dict) else {})}
453
+ latest_session = merged.get("latest_session")
454
+ if isinstance(latest_session, dict):
455
+ compact_latest = self._summary_session_payload(latest_session)
456
+ merged["latest_session"] = compact_latest if _normalize_string(compact_latest.get("bash_id")) else None
457
+ else:
458
+ merged["latest_session"] = None
459
+ merged["session_count"] = max(0, int(merged.get("session_count") or 0))
460
+ merged["running_count"] = max(0, int(merged.get("running_count") or 0))
461
+ merged["recent_sessions"] = self._normalize_summary_session_list(
462
+ merged.get("recent_sessions"),
463
+ limit=SUMMARY_RECENT_SESSION_LIMIT,
464
+ )
465
+ merged["running_sessions"] = self._normalize_summary_session_list(
466
+ merged.get("running_sessions"),
467
+ limit=SUMMARY_RUNNING_SESSION_LIMIT,
468
+ )
469
+ return merged
470
+
388
471
  def _refresh_summary_cache(self, quest_root: Path, summary: dict[str, Any]) -> dict[str, Any]:
389
472
  path = self.summary_path(quest_root)
390
473
  cache_key = str(path.resolve())
@@ -397,7 +480,7 @@ class BashExecService:
397
480
  )
398
481
  else:
399
482
  state = None
400
- payload = dict(summary)
483
+ payload = self._normalize_summary_payload(summary)
401
484
  with self._summary_cache_lock:
402
485
  self._summary_cache[cache_key] = {
403
486
  "state": state,
@@ -423,38 +506,109 @@ class BashExecService:
423
506
  summary = read_json(path, None)
424
507
  if not isinstance(summary, dict):
425
508
  return None
426
- merged = {**self._default_summary(), **summary}
509
+ merged = self._normalize_summary_payload(summary)
427
510
  return self._refresh_summary_cache(quest_root, merged)
428
511
 
429
512
  def _write_summary(self, quest_root: Path, summary: dict[str, Any]) -> dict[str, Any]:
430
- normalized = {**self._default_summary(), **summary, "updated_at": utc_now()}
513
+ normalized = self._normalize_summary_payload(summary)
514
+ normalized["updated_at"] = utc_now()
431
515
  _atomic_write_json(self.summary_path(quest_root), normalized)
432
516
  return self._refresh_summary_cache(quest_root, normalized)
433
517
 
518
+ def _hydrate_summary_from_index(
519
+ self,
520
+ quest_root: Path,
521
+ summary: dict[str, Any],
522
+ ) -> dict[str, Any]:
523
+ needs_recent_sessions = not bool(summary.get("recent_sessions"))
524
+ needs_running_sessions = int(summary.get("running_count") or 0) > 0 and not bool(summary.get("running_sessions"))
525
+ if not needs_recent_sessions and not needs_running_sessions:
526
+ return summary
527
+
528
+ index_path = self.index_path(quest_root)
529
+ if not index_path.exists():
530
+ return summary
531
+
532
+ candidate_ids: list[str] = []
533
+ seen_ids: set[str] = set()
534
+ max_candidates = max(SUMMARY_RECENT_SESSION_LIMIT, SUMMARY_RUNNING_SESSION_LIMIT * 4)
535
+ for entry in reversed(read_jsonl(index_path)):
536
+ bash_id = _normalize_string((entry or {}).get("bash_id") if isinstance(entry, dict) else "")
537
+ if not bash_id or bash_id in seen_ids:
538
+ continue
539
+ seen_ids.add(bash_id)
540
+ candidate_ids.append(bash_id)
541
+ if len(candidate_ids) >= max_candidates:
542
+ break
543
+
544
+ if not candidate_ids:
545
+ return summary
546
+
547
+ recent_sessions: list[dict[str, Any]] = []
548
+ running_sessions: list[dict[str, Any]] = []
549
+ for bash_id in candidate_ids:
550
+ meta = read_json(self.meta_path(quest_root, bash_id), {})
551
+ if not isinstance(meta, dict) or not meta:
552
+ continue
553
+ compact = self._summary_session_payload(meta)
554
+ recent_sessions.append(compact)
555
+ if self._is_active_status(meta.get("status")):
556
+ running_sessions.append(compact)
557
+
558
+ if not recent_sessions and not running_sessions:
559
+ return summary
560
+
561
+ updated_summary = dict(summary)
562
+ if needs_recent_sessions and recent_sessions:
563
+ updated_summary["recent_sessions"] = self._normalize_summary_session_list(
564
+ recent_sessions,
565
+ limit=SUMMARY_RECENT_SESSION_LIMIT,
566
+ )
567
+ if updated_summary["recent_sessions"] and not isinstance(updated_summary.get("latest_session"), dict):
568
+ updated_summary["latest_session"] = updated_summary["recent_sessions"][0]
569
+ if needs_running_sessions:
570
+ updated_summary["running_sessions"] = self._normalize_summary_session_list(
571
+ running_sessions,
572
+ limit=SUMMARY_RUNNING_SESSION_LIMIT,
573
+ )
574
+ return self._write_summary(quest_root, updated_summary)
575
+
434
576
  def _rebuild_summary(self, quest_root: Path) -> dict[str, Any]:
435
577
  summary = self._default_summary()
436
578
  latest_session: dict[str, Any] | None = None
437
579
  session_count = 0
438
580
  running_count = 0
581
+ recent_sessions: list[dict[str, Any]] = []
582
+ running_sessions: list[dict[str, Any]] = []
439
583
  for meta_path in self.sessions_root(quest_root).glob("*/meta.json"):
440
584
  meta = read_json(meta_path, {})
441
585
  if not isinstance(meta, dict) or not meta:
442
586
  continue
443
587
  session_count += 1
588
+ compact = self._summary_session_payload(meta)
589
+ recent_sessions.append(compact)
444
590
  if self._is_active_status(meta.get("status")):
445
591
  running_count += 1
446
- compact = self._summary_session_payload(meta)
592
+ running_sessions.append(compact)
447
593
  if latest_session is None or self._summary_sort_key(compact) >= self._summary_sort_key(latest_session):
448
594
  latest_session = compact
449
595
  summary["session_count"] = session_count
450
596
  summary["running_count"] = running_count
451
597
  summary["latest_session"] = latest_session
598
+ summary["recent_sessions"] = self._normalize_summary_session_list(
599
+ recent_sessions,
600
+ limit=SUMMARY_RECENT_SESSION_LIMIT,
601
+ )
602
+ summary["running_sessions"] = self._normalize_summary_session_list(
603
+ running_sessions,
604
+ limit=SUMMARY_RUNNING_SESSION_LIMIT,
605
+ )
452
606
  return self._write_summary(quest_root, summary)
453
607
 
454
608
  def summary(self, quest_root: Path) -> dict[str, Any]:
455
609
  loaded = self._load_summary_from_disk(quest_root)
456
610
  if loaded is not None:
457
- return loaded
611
+ return self._hydrate_summary_from_index(quest_root, loaded)
458
612
  return self._rebuild_summary(quest_root)
459
613
 
460
614
  def _write_meta(self, quest_root: Path, bash_id: str, meta: dict[str, Any]) -> dict[str, Any]:
@@ -481,6 +635,23 @@ class BashExecService:
481
635
  or self._summary_sort_key(compact) >= self._summary_sort_key(latest_session)
482
636
  ):
483
637
  summary["latest_session"] = compact
638
+ summary["recent_sessions"] = self._merge_summary_session_list(
639
+ summary.get("recent_sessions"),
640
+ compact,
641
+ limit=SUMMARY_RECENT_SESSION_LIMIT,
642
+ )
643
+ if new_running:
644
+ summary["running_sessions"] = self._merge_summary_session_list(
645
+ summary.get("running_sessions"),
646
+ compact,
647
+ limit=SUMMARY_RUNNING_SESSION_LIMIT,
648
+ )
649
+ else:
650
+ summary["running_sessions"] = self._remove_summary_session(
651
+ summary.get("running_sessions"),
652
+ str(compact.get("bash_id") or ""),
653
+ limit=SUMMARY_RUNNING_SESSION_LIMIT,
654
+ )
484
655
  return self._write_summary(quest_root, summary)
485
656
 
486
657
  def reconcile_session(self, quest_root: Path, bash_id: str) -> dict[str, Any]:
@@ -540,10 +711,59 @@ class BashExecService:
540
711
  normalized_agent_instance_ids = {item for item in (agent_instance_ids or []) if item}
541
712
  normalized_agent_ids = {item for item in (agent_ids or []) if item}
542
713
  normalized_chat_session = _normalize_string(chat_session_id)
543
- if normalized_status in {"running", "terminating"}:
544
- summary = self.summary(quest_root)
545
- if int(summary.get("running_count") or 0) <= 0:
546
- return []
714
+ summary = self.summary(quest_root)
715
+ if normalized_status in {"running", "terminating"} and int(summary.get("running_count") or 0) <= 0:
716
+ return []
717
+ can_use_summary_fast_path = (
718
+ not normalized_agent_instance_ids
719
+ and not normalized_agent_ids
720
+ and not normalized_chat_session
721
+ )
722
+ if can_use_summary_fast_path:
723
+ candidate_compacts: list[dict[str, Any]] | None = None
724
+ if normalized_status in {"running", "terminating"}:
725
+ running_sessions = self._normalize_summary_session_list(
726
+ summary.get("running_sessions"),
727
+ limit=SUMMARY_RUNNING_SESSION_LIMIT,
728
+ )
729
+ filtered_running = [
730
+ item
731
+ for item in running_sessions
732
+ if (not normalized_kind or _normalize_string(item.get("kind")).lower() == normalized_kind)
733
+ and (not normalized_status or _normalize_string(item.get("status")).lower() == normalized_status)
734
+ ]
735
+ running_count = int(summary.get("running_count") or 0)
736
+ if len(filtered_running) >= max(1, limit) or running_count <= len(running_sessions):
737
+ candidate_compacts = filtered_running
738
+ elif not normalized_status:
739
+ recent_sessions = self._normalize_summary_session_list(
740
+ summary.get("recent_sessions"),
741
+ limit=SUMMARY_RECENT_SESSION_LIMIT,
742
+ )
743
+ if not normalized_kind:
744
+ if len(recent_sessions) >= max(1, limit) or int(summary.get("session_count") or 0) <= len(recent_sessions):
745
+ candidate_compacts = recent_sessions
746
+ else:
747
+ filtered_recent = [
748
+ item
749
+ for item in recent_sessions
750
+ if _normalize_string(item.get("kind")).lower() == normalized_kind
751
+ ]
752
+ if len(filtered_recent) >= max(1, limit) or int(summary.get("session_count") or 0) <= len(recent_sessions):
753
+ candidate_compacts = filtered_recent
754
+ if candidate_compacts is not None:
755
+ resolved_sessions: list[dict[str, Any]] = []
756
+ for compact in candidate_compacts:
757
+ bash_id = _normalize_string(compact.get("bash_id"))
758
+ if not bash_id:
759
+ continue
760
+ try:
761
+ resolved_sessions.append(self.reconcile_session(quest_root, bash_id))
762
+ except FileNotFoundError:
763
+ continue
764
+ if len(resolved_sessions) >= max(1, limit):
765
+ break
766
+ return resolved_sessions[: max(1, limit)]
547
767
  sessions: list[dict[str, Any]] = []
548
768
  for bash_id in self._list_session_ids(quest_root):
549
769
  try:
@@ -571,6 +791,11 @@ class BashExecService:
571
791
  if self.meta_path(quest_root, normalized).exists():
572
792
  return normalized
573
793
  raise FileNotFoundError(f"Unknown bash session `{normalized}`.")
794
+ summary = self.summary(quest_root)
795
+ latest_session = summary.get("latest_session")
796
+ latest_bash_id = _normalize_string((latest_session or {}).get("bash_id") if isinstance(latest_session, dict) else "")
797
+ if latest_bash_id and self.meta_path(quest_root, latest_bash_id).exists():
798
+ return latest_bash_id
574
799
  sessions = self.list_sessions(quest_root, limit=1)
575
800
  if not sessions:
576
801
  raise FileNotFoundError("No bash session found.")
@@ -1021,8 +1021,13 @@ class WeixinConnectorBridge(BaseConnectorBridge):
1021
1021
  @classmethod
1022
1022
  def _retry_delays_for_item(cls, item: dict[str, Any], exc: Exception) -> tuple[float, ...]:
1023
1023
  message = str(exc or "").strip().lower()
1024
- if "ret=-2" in message and cls._item_type(item) in {4, 5}:
1024
+ if "ret=-2" not in message:
1025
+ return ()
1026
+ item_type = cls._item_type(item)
1027
+ if item_type in {4, 5}:
1025
1028
  return cls._MEDIA_SEND_RETRY_DELAYS_SECONDS
1029
+ if item_type == 1:
1030
+ return cls._TEXT_SEND_RETRY_DELAYS_SECONDS
1026
1031
  return ()
1027
1032
 
1028
1033
  def _send_items(
@@ -1044,7 +1049,8 @@ class WeixinConnectorBridge(BaseConnectorBridge):
1044
1049
  if media_item:
1045
1050
  time.sleep(self._MEDIA_SEND_INITIAL_DELAY_SECONDS)
1046
1051
  retry_delays: tuple[float, ...] = ()
1047
- for attempt in range(1 + len(self._MEDIA_SEND_RETRY_DELAYS_SECONDS)):
1052
+ max_retries = max(len(self._TEXT_SEND_RETRY_DELAYS_SECONDS), len(self._MEDIA_SEND_RETRY_DELAYS_SECONDS))
1053
+ for attempt in range(1 + max_retries):
1048
1054
  client_id = self._next_client_id()
1049
1055
  try:
1050
1056
  send_weixin_message(
@@ -25,23 +25,78 @@ from .registries import BaselineRegistry
25
25
  from .runners import CodexRunner, RunRequest, get_runner_factory, register_builtin_runners
26
26
  from .runtime_tools import RuntimeToolService
27
27
  from .runtime_logs import JsonlLogger
28
- from .shared import ensure_dir, read_yaml
28
+ from .shared import ensure_dir, read_json, read_yaml
29
29
  from .skills import SkillInstaller
30
30
  from .tui import watch_tui
31
31
 
32
32
 
33
+ class DeepScientistArgumentParser(argparse.ArgumentParser):
34
+ def error(self, message: str) -> None:
35
+ self.print_usage(sys.stderr)
36
+ self.exit(2, f"DeepScientist argument error: {message}\nRun `{self.prog} --help` for usage.\n")
37
+
38
+
33
39
  def _local_ui_url(host: str, port: int) -> str:
34
- connect_host = "0.0.0.0" if host in {"0.0.0.0", "::", ""} else host
35
- return f"http://{connect_host}:{port}"
40
+ normalized = str(host or "").strip()
41
+ connect_host = "127.0.0.1" if normalized in {"0.0.0.0", "::", "[::]", ""} else normalized
42
+ if connect_host.startswith("[") and connect_host.endswith("]"):
43
+ rendered_host = connect_host
44
+ elif ":" in connect_host:
45
+ rendered_host = f"[{connect_host}]"
46
+ else:
47
+ rendered_host = connect_host
48
+ return f"http://{rendered_host}:{port}"
49
+
50
+
51
+ def _parse_optional_bool(value: object) -> bool | None:
52
+ if isinstance(value, bool):
53
+ return value
54
+ normalized = str(value or "").strip().lower()
55
+ if not normalized:
56
+ return None
57
+ if normalized in {"1", "true", "yes", "on"}:
58
+ return True
59
+ if normalized in {"0", "false", "no", "off"}:
60
+ return False
61
+ return None
62
+
63
+
64
+ def _daemon_request_headers(home: Path) -> dict[str, str]:
65
+ headers = {"Content-Type": "application/json"}
66
+ state = read_json(home / "runtime" / "daemon.json", {})
67
+ if not isinstance(state, dict):
68
+ return headers
69
+ if bool(state.get("auth_enabled")):
70
+ token = str(state.get("auth_token") or "").strip()
71
+ if token:
72
+ headers["Authorization"] = f"Bearer {token}"
73
+ return headers
74
+
75
+
76
+ def _daemon_launch_url(home: Path, *, host: str, port: int) -> str:
77
+ state = read_json(home / "runtime" / "daemon.json", {})
78
+ if isinstance(state, dict):
79
+ launch_url = str(state.get("launch_url") or "").strip()
80
+ if launch_url:
81
+ return launch_url
82
+ return _local_ui_url(host, port)
36
83
 
37
84
 
38
85
  def build_parser() -> argparse.ArgumentParser:
39
- parser = argparse.ArgumentParser(prog="ds", description="DeepScientist Core skeleton")
86
+ parser = DeepScientistArgumentParser(
87
+ prog="ds",
88
+ description="DeepScientist Core skeleton",
89
+ allow_abbrev=False,
90
+ )
40
91
  parser.add_argument("--home", default=None, help="Override DeepScientist home")
41
92
  parser.add_argument("--proxy", default=None, help="Explicit outbound HTTP/WS proxy, for example `http://127.0.0.1:7890`.")
42
93
  parser.add_argument("--codex", default=None, help="Override the Codex executable path for this invocation.")
43
94
 
44
- subparsers = parser.add_subparsers(dest="command", required=True)
95
+ subparsers = parser.add_subparsers(
96
+ dest="command",
97
+ required=True,
98
+ parser_class=DeepScientistArgumentParser,
99
+ )
45
100
 
46
101
  subparsers.add_parser("init")
47
102
 
@@ -60,12 +115,24 @@ def build_parser() -> argparse.ArgumentParser:
60
115
  daemon_parser = subparsers.add_parser("daemon")
61
116
  daemon_parser.add_argument("--host", default=None)
62
117
  daemon_parser.add_argument("--port", type=int, default=None)
118
+ daemon_parser.add_argument("--auth", default=None)
119
+ daemon_parser.add_argument("--auth-token", default=None)
120
+ daemon_parser.add_argument(
121
+ "--prompt-version",
122
+ default=None,
123
+ help="Use `latest` managed prompts, an official historical prompt version such as `1.5.13`, or an exact backup id from `.codex/prompt_versions/` for this daemon session.",
124
+ )
63
125
 
64
126
  run_parser = subparsers.add_parser("run")
65
127
  run_parser.add_argument("skill_id")
66
128
  run_parser.add_argument("--quest-id", required=True)
67
129
  run_parser.add_argument("--message", required=True)
68
130
  run_parser.add_argument("--model", default=None)
131
+ run_parser.add_argument(
132
+ "--prompt-version",
133
+ default=None,
134
+ help="Use `latest` managed prompts, an official historical prompt version such as `1.5.13`, or an exact backup id from `.codex/prompt_versions/` for this one-off run.",
135
+ )
69
136
 
70
137
  ui_parser = subparsers.add_parser("ui")
71
138
  ui_parser.add_argument("--mode", choices=("web", "tui", "both"), default="web")
@@ -186,7 +253,6 @@ def resume_command(home: Path, quest_id: str) -> int:
186
253
  print(json.dumps(snapshot, ensure_ascii=False, indent=2))
187
254
  return 0
188
255
 
189
-
190
256
  def _daemon_control_quest(home: Path, quest_id: str, *, action: str) -> dict | None:
191
257
  config = ConfigManager(home).load_named("config", create_optional=False)
192
258
  ui_config = config.get("ui", {})
@@ -194,7 +260,7 @@ def _daemon_control_quest(home: Path, quest_id: str, *, action: str) -> dict | N
194
260
  request = Request(
195
261
  url,
196
262
  data=json.dumps({"action": action, "source": "cli"}).encode("utf-8"),
197
- headers={"Content-Type": "application/json"},
263
+ headers=_daemon_request_headers(home),
198
264
  method="POST",
199
265
  )
200
266
  try:
@@ -211,7 +277,7 @@ def _daemon_create_quest(home: Path, *, goal: str, quest_id: str | None) -> dict
211
277
  request = Request(
212
278
  url,
213
279
  data=json.dumps({"goal": goal, "quest_id": quest_id, "source": "cli"}).encode("utf-8"),
214
- headers={"Content-Type": "application/json"},
280
+ headers=_daemon_request_headers(home),
215
281
  method="POST",
216
282
  )
217
283
  try:
@@ -221,18 +287,37 @@ def _daemon_create_quest(home: Path, *, goal: str, quest_id: str | None) -> dict
221
287
  return None
222
288
 
223
289
 
224
- def daemon_command(home: Path, host: str | None, port: int | None) -> int:
290
+ def daemon_command(
291
+ home: Path,
292
+ host: str | None,
293
+ port: int | None,
294
+ auth: str | None,
295
+ auth_token: str | None,
296
+ prompt_version: str | None,
297
+ ) -> int:
225
298
  ensure_home_layout(home)
226
299
  config_manager = ConfigManager(home)
227
300
  config_manager.ensure_files()
228
301
  config = config_manager.load_named("config")
229
302
  ui_config = config.get("ui", {})
230
- daemon = DaemonApp(home)
303
+ daemon = DaemonApp(
304
+ home,
305
+ browser_auth_enabled=_parse_optional_bool(auth),
306
+ browser_auth_token=str(auth_token or "").strip() or None,
307
+ prompt_version_selection=str(prompt_version or "").strip() or None,
308
+ )
231
309
  daemon.serve(host or ui_config.get("host", "0.0.0.0"), port or ui_config.get("port", 20999))
232
310
  return 0
233
311
 
234
312
 
235
- def run_command(home: Path, quest_id: str, skill_id: str, message: str, model: str | None) -> int:
313
+ def run_command(
314
+ home: Path,
315
+ quest_id: str,
316
+ skill_id: str,
317
+ message: str,
318
+ model: str | None,
319
+ prompt_version: str | None,
320
+ ) -> int:
236
321
  ensure_home_layout(home)
237
322
  config_manager = ConfigManager(home)
238
323
  config_manager.ensure_files()
@@ -246,7 +331,11 @@ def run_command(home: Path, quest_id: str, skill_id: str, message: str, model: s
246
331
  repo_root=repo_root(),
247
332
  binary=codex_cfg.get("binary", "codex"),
248
333
  logger=logger,
249
- prompt_builder=PromptBuilder(repo_root(), home),
334
+ prompt_builder=PromptBuilder(
335
+ repo_root(),
336
+ home,
337
+ prompt_version_selection=str(prompt_version or "").strip() or None,
338
+ ),
250
339
  artifact_service=ArtifactService(home),
251
340
  )
252
341
  register_builtin_runners(codex_runner=runner)
@@ -322,19 +411,26 @@ def launch_ink_tui(home: Path, url: str) -> int:
322
411
  )
323
412
  )
324
413
  return 1
325
- return subprocess.call([node_binary, str(entry), "--base-url", url])
414
+ state = read_json(home / "runtime" / "daemon.json", {})
415
+ args = [node_binary, str(entry), "--base-url", url]
416
+ if isinstance(state, dict) and bool(state.get("auth_enabled")):
417
+ token = str(state.get("auth_token") or "").strip()
418
+ if token:
419
+ args.extend(["--auth-token", token])
420
+ return subprocess.call(args)
326
421
 
327
422
 
328
423
  def ui_command(home: Path, mode: str) -> int:
329
424
  config = ConfigManager(home).load_named("config", create_optional=False)
330
425
  host = config.get("ui", {}).get("host", "0.0.0.0")
331
426
  port = config.get("ui", {}).get("port", 20999)
332
- url = _local_ui_url(host, port)
427
+ base_url = _local_ui_url(str(host), int(port))
428
+ launch_url = _daemon_launch_url(home, host=str(host), port=int(port))
333
429
  if mode in {"web", "both"}:
334
- webbrowser.open(url)
335
- print(f"Opened {url}")
430
+ webbrowser.open(launch_url)
431
+ print(f"Opened {launch_url}")
336
432
  if mode in {"tui", "both"}:
337
- return launch_ink_tui(home, url)
433
+ return launch_ink_tui(home, base_url)
338
434
  return 0
339
435
 
340
436
 
@@ -492,9 +588,9 @@ def main(argv: list[str] | None = None) -> int:
492
588
  if args.command == "resume":
493
589
  return resume_command(home, args.quest_id)
494
590
  if args.command == "daemon":
495
- return daemon_command(home, args.host, args.port)
591
+ return daemon_command(home, args.host, args.port, args.auth, args.auth_token, args.prompt_version)
496
592
  if args.command == "run":
497
- return run_command(home, args.quest_id, args.skill_id, args.message, args.model)
593
+ return run_command(home, args.quest_id, args.skill_id, args.message, args.model, args.prompt_version)
498
594
  if args.command == "ui":
499
595
  return ui_command(home, args.mode)
500
596
  if args.command == "note":