@researai/deepscientist 1.5.14 → 1.5.16

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 (225) hide show
  1. package/README.md +336 -90
  2. package/assets/branding/logo-raster.png +0 -0
  3. package/bin/ds.js +816 -131
  4. package/docs/en/00_QUICK_START.md +36 -15
  5. package/docs/en/01_SETTINGS_REFERENCE.md +53 -4
  6. package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
  7. package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -0
  8. package/docs/en/05_TUI_GUIDE.md +6 -0
  9. package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
  10. package/docs/en/09_DOCTOR.md +11 -5
  11. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  12. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
  13. package/docs/en/15_CODEX_PROVIDER_SETUP.md +25 -8
  14. package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  15. package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  16. package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  17. package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  18. package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
  19. package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
  20. package/docs/en/README.md +24 -0
  21. package/docs/zh/00_QUICK_START.md +36 -15
  22. package/docs/zh/01_SETTINGS_REFERENCE.md +53 -4
  23. package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
  24. package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
  25. package/docs/zh/05_TUI_GUIDE.md +6 -0
  26. package/docs/zh/09_DOCTOR.md +11 -5
  27. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  28. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
  29. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +25 -8
  30. package/docs/zh/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  31. package/docs/zh/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  32. package/docs/zh/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  33. package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  34. package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
  35. package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
  36. package/docs/zh/README.md +24 -0
  37. package/install.sh +2 -0
  38. package/package.json +1 -1
  39. package/pyproject.toml +1 -1
  40. package/src/deepscientist/__init__.py +1 -1
  41. package/src/deepscientist/acp/envelope.py +6 -0
  42. package/src/deepscientist/artifact/charts.py +567 -0
  43. package/src/deepscientist/artifact/guidance.py +50 -10
  44. package/src/deepscientist/artifact/metrics.py +228 -5
  45. package/src/deepscientist/artifact/schemas.py +3 -0
  46. package/src/deepscientist/artifact/service.py +4276 -308
  47. package/src/deepscientist/bash_exec/models.py +23 -0
  48. package/src/deepscientist/bash_exec/monitor.py +147 -67
  49. package/src/deepscientist/bash_exec/runtime.py +218 -156
  50. package/src/deepscientist/bash_exec/service.py +309 -69
  51. package/src/deepscientist/bash_exec/shells.py +87 -0
  52. package/src/deepscientist/bridges/connectors.py +51 -2
  53. package/src/deepscientist/cli.py +115 -19
  54. package/src/deepscientist/codex_cli_compat.py +232 -0
  55. package/src/deepscientist/config/models.py +8 -4
  56. package/src/deepscientist/config/service.py +38 -11
  57. package/src/deepscientist/connector/weixin_support.py +122 -1
  58. package/src/deepscientist/daemon/api/handlers.py +199 -9
  59. package/src/deepscientist/daemon/api/router.py +5 -0
  60. package/src/deepscientist/daemon/app.py +1458 -289
  61. package/src/deepscientist/doctor.py +51 -0
  62. package/src/deepscientist/file_lock.py +48 -0
  63. package/src/deepscientist/gitops/__init__.py +10 -1
  64. package/src/deepscientist/gitops/diff.py +296 -1
  65. package/src/deepscientist/gitops/service.py +4 -1
  66. package/src/deepscientist/mcp/server.py +212 -5
  67. package/src/deepscientist/process_control.py +161 -0
  68. package/src/deepscientist/prompts/builder.py +501 -453
  69. package/src/deepscientist/quest/layout.py +15 -2
  70. package/src/deepscientist/quest/service.py +2539 -195
  71. package/src/deepscientist/quest/stage_views.py +177 -1
  72. package/src/deepscientist/runners/base.py +2 -0
  73. package/src/deepscientist/runners/codex.py +169 -31
  74. package/src/deepscientist/runners/runtime_overrides.py +17 -1
  75. package/src/deepscientist/skills/__init__.py +2 -2
  76. package/src/deepscientist/skills/installer.py +196 -5
  77. package/src/deepscientist/skills/registry.py +66 -0
  78. package/src/prompts/connectors/qq.md +18 -8
  79. package/src/prompts/connectors/weixin.md +16 -6
  80. package/src/prompts/contracts/shared_interaction.md +24 -4
  81. package/src/prompts/system.md +921 -72
  82. package/src/prompts/system_copilot.md +43 -0
  83. package/src/skills/analysis-campaign/SKILL.md +32 -2
  84. package/src/skills/analysis-campaign/references/artifact-orchestration.md +1 -1
  85. package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +65 -0
  86. package/src/skills/baseline/SKILL.md +10 -0
  87. package/src/skills/decision/SKILL.md +27 -2
  88. package/src/skills/experiment/SKILL.md +16 -2
  89. package/src/skills/figure-polish/SKILL.md +1 -0
  90. package/src/skills/finalize/SKILL.md +19 -0
  91. package/src/skills/idea/SKILL.md +79 -0
  92. package/src/skills/idea/references/idea-generation-playbook.md +100 -0
  93. package/src/skills/idea/references/outline-seeding-example.md +60 -0
  94. package/src/skills/intake-audit/SKILL.md +9 -1
  95. package/src/skills/mentor/SKILL.md +217 -0
  96. package/src/skills/mentor/references/correction-rules.md +210 -0
  97. package/src/skills/mentor/references/knowledge-profile.md +91 -0
  98. package/src/skills/mentor/references/persona-profile.md +138 -0
  99. package/src/skills/mentor/references/taste-profile.md +128 -0
  100. package/src/skills/mentor/references/thought-style-profile.md +138 -0
  101. package/src/skills/mentor/references/work-profile.md +289 -0
  102. package/src/skills/mentor/references/workflow-profile.md +240 -0
  103. package/src/skills/optimize/SKILL.md +1645 -0
  104. package/src/skills/rebuttal/SKILL.md +3 -1
  105. package/src/skills/review/SKILL.md +3 -1
  106. package/src/skills/scout/SKILL.md +8 -0
  107. package/src/skills/write/SKILL.md +81 -12
  108. package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
  109. package/src/tui/dist/app/AppContainer.js +22 -11
  110. package/src/tui/dist/index.js +4 -1
  111. package/src/tui/dist/lib/api.js +33 -3
  112. package/src/tui/package.json +1 -1
  113. package/src/ui/dist/assets/AiManusChatView-COFACy7V.js +204 -0
  114. package/src/ui/dist/assets/AnalysisPlugin-DnSm0GZn.js +1 -0
  115. package/src/ui/dist/assets/CliPlugin-CvwCmDQ5.js +109 -0
  116. package/src/ui/dist/assets/CodeEditorPlugin-cOqSa0xq.js +2 -0
  117. package/src/ui/dist/assets/CodeViewerPlugin-itb0tltR.js +270 -0
  118. package/src/ui/dist/assets/DocViewerPlugin-DqKkiCI6.js +7 -0
  119. package/src/ui/dist/assets/GitCommitViewerPlugin-DVgNHBCS.js +1 -0
  120. package/src/ui/dist/assets/GitDiffViewerPlugin-DxL2ezFG.js +6 -0
  121. package/src/ui/dist/assets/GitSnapshotViewer-B_RQm1YZ.js +30 -0
  122. package/src/ui/dist/assets/ImageViewerPlugin-tHqlXY3n.js +26 -0
  123. package/src/ui/dist/assets/LabCopilotPanel-ClMbq5Yu.js +14 -0
  124. package/src/ui/dist/assets/LabPlugin-L_SuE8ow.js +22 -0
  125. package/src/ui/dist/assets/LatexPlugin-B495DTXC.js +25 -0
  126. package/src/ui/dist/assets/MarkdownViewerPlugin-DG28-61B.js +128 -0
  127. package/src/ui/dist/assets/MarketplacePlugin-BiOGT-Kj.js +13 -0
  128. package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
  129. package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
  130. package/src/ui/dist/assets/NotebookEditor-C-4Kt1p9.js +81 -0
  131. package/src/ui/dist/assets/NotebookEditor-CVsj8h_T.js +361 -0
  132. package/src/ui/dist/assets/PdfLoader-CASDQmxJ.js +16 -0
  133. package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
  134. package/src/ui/dist/assets/PdfMarkdownPlugin-BFhwoKsY.js +1 -0
  135. package/src/ui/dist/assets/PdfViewerPlugin-DcOzU9vd.js +17 -0
  136. package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
  137. package/src/ui/dist/assets/SearchPlugin-CHj7M58O.js +16 -0
  138. package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
  139. package/src/ui/dist/assets/TextViewerPlugin-CB4DYfWO.js +54 -0
  140. package/src/ui/dist/assets/VNCViewer-CjlbyCB3.js +11 -0
  141. package/src/ui/dist/assets/bot-CFkZY-JP.js +6 -0
  142. package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
  143. package/src/ui/dist/assets/chevron-up-Dq5ofbht.js +6 -0
  144. package/src/ui/dist/assets/code-DLC6G24T.js +6 -0
  145. package/src/ui/dist/assets/file-content-Dv4LoZec.js +1 -0
  146. package/src/ui/dist/assets/file-diff-panel-Denq-lC3.js +1 -0
  147. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
  148. package/src/ui/dist/assets/file-socket-Cu4Qln7Y.js +1 -0
  149. package/src/ui/dist/assets/git-commit-horizontal-BUh6G52n.js +6 -0
  150. package/src/ui/dist/assets/image-B9HUUddG.js +6 -0
  151. package/src/ui/dist/assets/index-B2B1sg-M.js +1 -0
  152. package/src/ui/dist/assets/index-Cgla8biy.css +33 -0
  153. package/src/ui/dist/assets/index-DRyx7vAc.js +1 -0
  154. package/src/ui/dist/assets/index-Gbl53BNp.js +2496 -0
  155. package/src/ui/dist/assets/index-wQ7RIIRd.js +11 -0
  156. package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
  157. package/src/ui/dist/assets/pdf-effect-queue-ZtnHFCAi.js +6 -0
  158. package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
  159. package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
  160. package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
  161. package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
  162. package/src/ui/dist/assets/popover-DL6h35vr.js +1 -0
  163. package/src/ui/dist/assets/project-sync-CsX08Qno.js +1 -0
  164. package/src/ui/dist/assets/select-DvmXt1yY.js +11 -0
  165. package/src/ui/dist/assets/sigma-7jpXazui.js +6 -0
  166. package/src/ui/dist/assets/trash-xA7kFt8i.js +11 -0
  167. package/src/ui/dist/assets/useCliAccess-DsMwDjOp.js +1 -0
  168. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
  169. package/src/ui/dist/assets/wrap-text-CwMn-iqb.js +11 -0
  170. package/src/ui/dist/assets/zoom-out-R-GWEhzS.js +11 -0
  171. package/src/ui/dist/index.html +5 -2
  172. package/src/ui/dist/assets/AiManusChatView-DaF9Nge_.js +0 -26597
  173. package/src/ui/dist/assets/AnalysisPlugin-BSVx6dXE.js +0 -123
  174. package/src/ui/dist/assets/CliPlugin-C9gzJX41.js +0 -5905
  175. package/src/ui/dist/assets/CodeEditorPlugin-DU9G0Tox.js +0 -427
  176. package/src/ui/dist/assets/CodeViewerPlugin-DoX_fI9l.js +0 -905
  177. package/src/ui/dist/assets/DocViewerPlugin-C4FWIXuU.js +0 -278
  178. package/src/ui/dist/assets/GitDiffViewerPlugin-BgfFMgtf.js +0 -2661
  179. package/src/ui/dist/assets/ImageViewerPlugin-tcPkfY_x.js +0 -500
  180. package/src/ui/dist/assets/LabCopilotPanel-_dKV60Bf.js +0 -4104
  181. package/src/ui/dist/assets/LabPlugin-Bje0ayoC.js +0 -2677
  182. package/src/ui/dist/assets/LatexPlugin-CVsBzAln.js +0 -1792
  183. package/src/ui/dist/assets/MarkdownViewerPlugin-xjmrqv_8.js +0 -308
  184. package/src/ui/dist/assets/MarketplacePlugin-mMM2A8wP.js +0 -413
  185. package/src/ui/dist/assets/NotebookEditor-3kVDSOBo.js +0 -4214
  186. package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
  187. package/src/ui/dist/assets/NotebookEditor-SoJ8X-MO.js +0 -84873
  188. package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
  189. package/src/ui/dist/assets/PdfLoader-DElVuHl9.js +0 -25468
  190. package/src/ui/dist/assets/PdfMarkdownPlugin-Bq88XT4G.js +0 -409
  191. package/src/ui/dist/assets/PdfViewerPlugin-CsCXMo9S.js +0 -3095
  192. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
  193. package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
  194. package/src/ui/dist/assets/SearchPlugin-oUPvy19k.js +0 -741
  195. package/src/ui/dist/assets/TextViewerPlugin-CRkT9yNy.js +0 -472
  196. package/src/ui/dist/assets/VNCViewer-BgbuvWhR.js +0 -18821
  197. package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
  198. package/src/ui/dist/assets/bot-v_RASACv.js +0 -21
  199. package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
  200. package/src/ui/dist/assets/code-5hC9d0VH.js +0 -17
  201. package/src/ui/dist/assets/file-content-D1PxfOrp.js +0 -377
  202. package/src/ui/dist/assets/file-diff-panel-DG1oT_Hj.js +0 -92
  203. package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
  204. package/src/ui/dist/assets/file-socket-BmdFYQlk.js +0 -58
  205. package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
  206. package/src/ui/dist/assets/image-Dqe2X2tW.js +0 -18
  207. package/src/ui/dist/assets/index-BQG-1s2o.css +0 -12553
  208. package/src/ui/dist/assets/index-DVsMKK_y.js +0 -25
  209. package/src/ui/dist/assets/index-Duvz8Ip0.js +0 -159
  210. package/src/ui/dist/assets/index-Nt9hS4ck.js +0 -244829
  211. package/src/ui/dist/assets/index-RDlNXXx1.js +0 -120
  212. package/src/ui/dist/assets/monaco-DIXge1CP.js +0 -623
  213. package/src/ui/dist/assets/pdf-effect-queue-BBTTQaO-.js +0 -47
  214. package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
  215. package/src/ui/dist/assets/popover-BWlolyxo.js +0 -476
  216. package/src/ui/dist/assets/project-sync-BM5PkFH4.js +0 -297
  217. package/src/ui/dist/assets/select-D4dAtrA8.js +0 -1690
  218. package/src/ui/dist/assets/sigma-CKbE5jJT.js +0 -22
  219. package/src/ui/dist/assets/square-check-big-CZNGMgiB.js +0 -17
  220. package/src/ui/dist/assets/trash-DaB37xAz.js +0 -32
  221. package/src/ui/dist/assets/useCliAccess-C2OmAcWe.js +0 -957
  222. package/src/ui/dist/assets/useFileDiffOverlay-Dowd1Ij4.js +0 -53
  223. package/src/ui/dist/assets/wrap-text-BGjAhAUq.js +0 -35
  224. package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
  225. package/src/ui/dist/assets/zoom-out-dMZQMXzc.js +0 -34
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import threading
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class AttachToken:
11
+ token: str
12
+ quest_root: Path
13
+ bash_id: str
14
+ expires_at: float
15
+
16
+
17
+ @dataclass(slots=True)
18
+ class TerminalClient:
19
+ client_id: str
20
+ send_text: Any
21
+ send_binary: Any
22
+ close: Any
23
+ send_lock: threading.Lock
@@ -1,18 +1,24 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
3
4
  import codecs
4
5
  import json
5
6
  import os
6
- import pty
7
7
  import select
8
8
  import shlex
9
- import signal
10
9
  import subprocess
11
10
  import sys
11
+ import threading
12
12
  import time
13
13
  from pathlib import Path
14
14
  from typing import Any
15
15
 
16
+ if os.name != "nt": # pragma: no cover - exercised on POSIX
17
+ import pty
18
+ else: # pragma: no cover - exercised on Windows
19
+ pty = None
20
+
21
+ from ..process_control import process_session_popen_kwargs, terminate_subprocess
16
22
  from .service import (
17
23
  BASH_CARRIAGE_RETURN_PREFIX,
18
24
  BASH_PROGRESS_PREFIX,
@@ -22,6 +28,7 @@ from .service import (
22
28
  _coerce_session_status,
23
29
  _parse_progress_marker,
24
30
  )
31
+ from .shells import build_exec_shell_launch
25
32
  from ..shared import append_jsonl, ensure_dir, iter_jsonl, read_json, read_jsonl, utc_now
26
33
 
27
34
  DEFAULT_STOP_GRACE_SECONDS = 5
@@ -179,39 +186,21 @@ def _status_marker(meta: dict[str, Any], *, status: str, exit_code: int | None,
179
186
 
180
187
 
181
188
  def _terminate_process(process: subprocess.Popen[bytes], process_group_id: int | None) -> None:
182
- if process.poll() is not None:
183
- return
184
- if isinstance(process_group_id, int) and process_group_id > 0:
185
- try:
186
- os.killpg(process_group_id, signal.SIGTERM)
187
- except ProcessLookupError:
188
- return
189
- else:
190
- process.terminate()
191
- deadline = time.monotonic() + DEFAULT_STOP_GRACE_SECONDS
192
- while time.monotonic() < deadline:
193
- if process.poll() is not None:
194
- return
195
- time.sleep(0.1)
196
- if isinstance(process_group_id, int) and process_group_id > 0:
197
- try:
198
- os.killpg(process_group_id, signal.SIGKILL)
199
- except ProcessLookupError:
200
- return
201
- elif process.poll() is None:
202
- process.kill()
189
+ terminate_subprocess(
190
+ process,
191
+ process_group_id=process_group_id,
192
+ force=False,
193
+ grace_seconds=DEFAULT_STOP_GRACE_SECONDS,
194
+ )
203
195
 
204
196
 
205
197
  def _terminate_process_force(process: subprocess.Popen[bytes], process_group_id: int | None) -> None:
206
- if process.poll() is not None:
207
- return
208
- if isinstance(process_group_id, int) and process_group_id > 0:
209
- try:
210
- os.killpg(process_group_id, signal.SIGKILL)
211
- except ProcessLookupError:
212
- return
213
- else:
214
- process.kill()
198
+ terminate_subprocess(
199
+ process,
200
+ process_group_id=process_group_id,
201
+ force=True,
202
+ grace_seconds=DEFAULT_STOP_GRACE_SECONDS,
203
+ )
215
204
 
216
205
 
217
206
  def _drain_buffer(
@@ -266,6 +255,12 @@ def _parse_terminal_prompt_marker(line: str) -> dict[str, str] | None:
266
255
 
267
256
 
268
257
  def _format_terminal_prompt(meta: dict[str, Any], cwd_value: str) -> str:
258
+ cwd_b64 = str(cwd_value or "").strip()
259
+ if meta.get("shell_family") == "powershell" and cwd_b64:
260
+ try:
261
+ cwd_value = base64.b64decode(cwd_b64.encode("ascii")).decode("utf-8")
262
+ except Exception:
263
+ cwd_value = cwd_b64
269
264
  quest_root = Path(str(meta.get("quest_root") or ".")).expanduser().resolve()
270
265
  cwd_path = Path(str(cwd_value or quest_root)).expanduser().resolve()
271
266
  home = Path.home().expanduser().resolve()
@@ -274,6 +269,8 @@ def _format_terminal_prompt(meta: dict[str, Any], cwd_value: str) -> str:
274
269
  display = "~" if relative == "." else f"~/{relative}"
275
270
  except ValueError:
276
271
  display = str(cwd_path)
272
+ if str(meta.get("shell_family") or "").strip().lower() == "powershell":
273
+ return f"PS {display}> "
277
274
  return f"{display}$ "
278
275
 
279
276
 
@@ -330,7 +327,19 @@ def run_monitor(session_dir: Path) -> int:
330
327
  prompt_marker = _parse_terminal_prompt_marker(line) if session_kind == "terminal" else None
331
328
  if prompt_marker is not None:
332
329
  prompt_ts = str(prompt_marker.get("ts") or utc_now())
333
- prompt_cwd = str(prompt_marker.get("cwd") or meta.get("cwd") or cwd)
330
+ prompt_cwd_raw = str(
331
+ prompt_marker.get("cwd_b64")
332
+ or prompt_marker.get("cwd")
333
+ or meta.get("cwd")
334
+ or cwd
335
+ )
336
+ if prompt_marker.get("cwd_b64"):
337
+ try:
338
+ prompt_cwd = base64.b64decode(prompt_cwd_raw.encode("ascii")).decode("utf-8")
339
+ except Exception:
340
+ prompt_cwd = prompt_cwd_raw
341
+ else:
342
+ prompt_cwd = prompt_cwd_raw
334
343
  update_meta(cwd=prompt_cwd, last_prompt_at=prompt_ts)
335
344
  seq += 1
336
345
  _append_jsonl(
@@ -338,7 +347,7 @@ def run_monitor(session_dir: Path) -> int:
338
347
  {
339
348
  "seq": seq,
340
349
  "stream": "prompt",
341
- "line": _format_terminal_prompt(meta, prompt_cwd),
350
+ "line": _format_terminal_prompt(meta, prompt_cwd_raw),
342
351
  "timestamp": prompt_ts,
343
352
  },
344
353
  )
@@ -374,39 +383,49 @@ def run_monitor(session_dir: Path) -> int:
374
383
 
375
384
  master_fd: int | None = None
376
385
  slave_fd: int | None = None
377
- output_fd: int | None = None
386
+ output_stream: Any = None
378
387
  process: subprocess.Popen[bytes] | None = None
388
+ pipe_chunks: list[bytes] = []
389
+ pipe_chunks_lock = threading.Lock()
390
+ pipe_reader_done = threading.Event()
391
+ pipe_reader_thread: threading.Thread | None = None
379
392
  try:
380
- using_pty = True
393
+ launch_argv = [
394
+ str(item)
395
+ for item in (meta.get("launch_argv") or [])
396
+ if str(item).strip()
397
+ ] or build_exec_shell_launch(command).argv
398
+ using_pty = os.name != "nt" and pty is not None
381
399
  try:
400
+ if not using_pty:
401
+ raise OSError("pty_unavailable")
382
402
  master_fd, slave_fd = pty.openpty()
383
403
  process = subprocess.Popen(
384
- ["bash", "-lc", command],
404
+ launch_argv,
385
405
  cwd=str(cwd),
386
406
  env=env_payload,
387
407
  stdin=slave_fd,
388
408
  stdout=slave_fd,
389
409
  stderr=slave_fd,
390
- start_new_session=True,
410
+ **process_session_popen_kwargs(hide_window=True),
391
411
  )
392
412
  os.close(slave_fd)
393
413
  slave_fd = None
394
- output_fd = master_fd
395
414
  except OSError:
396
415
  using_pty = False
397
416
  process = subprocess.Popen(
398
- ["bash", "-lc", command],
417
+ launch_argv,
399
418
  cwd=str(cwd),
400
419
  env=env_payload,
401
- stdin=subprocess.DEVNULL,
420
+ stdin=subprocess.PIPE,
402
421
  stdout=subprocess.PIPE,
403
422
  stderr=subprocess.STDOUT,
404
- start_new_session=True,
423
+ **process_session_popen_kwargs(hide_window=True),
405
424
  )
406
425
  if process.stdout is None:
407
426
  raise RuntimeError("bash_exec_missing_stdout_pipe")
408
- output_fd = process.stdout.fileno()
409
- process_group_id = os.getpgid(process.pid)
427
+ output_stream = process.stdout
428
+ process_group_id = process.pid if os.name == "nt" else os.getpgid(process.pid)
410
429
  update_meta(
411
430
  monitor_pid=os.getpid(),
412
431
  process_pid=process.pid,
@@ -420,6 +439,24 @@ def run_monitor(session_dir: Path) -> int:
420
439
  buffer = ""
421
440
  deadline = time.monotonic() + int(timeout_seconds) if isinstance(timeout_seconds, int) and timeout_seconds > 0 else None
422
441
  stop_requested = False
442
+ if not using_pty and output_stream is not None:
443
+ def _pipe_reader() -> None:
444
+ try:
445
+ while True:
446
+ chunk = output_stream.read(4096)
447
+ if not chunk:
448
+ break
449
+ with pipe_chunks_lock:
450
+ pipe_chunks.append(chunk)
451
+ finally:
452
+ pipe_reader_done.set()
453
+
454
+ pipe_reader_thread = threading.Thread(
455
+ target=_pipe_reader,
456
+ name=f"bash-exec-monitor-{meta.get('bash_id')}",
457
+ daemon=True,
458
+ )
459
+ pipe_reader_thread.start()
423
460
 
424
461
  while True:
425
462
  if not stop_requested and stop_request_path.exists():
@@ -448,7 +485,7 @@ def run_monitor(session_dir: Path) -> int:
448
485
  _terminate_process(process, process_group_id)
449
486
  stop_requested = True
450
487
 
451
- if output_fd is not None and process.poll() is None:
488
+ if ((using_pty and master_fd is not None) or process.stdin is not None) and process.poll() is None:
452
489
  cursor_payload = read_json(input_cursor_path, {}) or {}
453
490
  offset = int(cursor_payload.get("offset") or 0)
454
491
  total_input_entries = sum(1 for _ in iter_jsonl(input_path))
@@ -459,7 +496,11 @@ def run_monitor(session_dir: Path) -> int:
459
496
  raw_data = str(entry.get("data") or "")
460
497
  if raw_data:
461
498
  try:
462
- os.write(output_fd, raw_data.encode("utf-8"))
499
+ if using_pty and master_fd is not None:
500
+ os.write(master_fd, raw_data.encode("utf-8"))
501
+ elif process.stdin is not None:
502
+ process.stdin.write(raw_data.encode("utf-8"))
503
+ process.stdin.flush()
463
504
  except OSError:
464
505
  break
465
506
  offset += 1
@@ -471,13 +512,28 @@ def run_monitor(session_dir: Path) -> int:
471
512
  },
472
513
  )
473
514
 
474
- ready, _unused_w, _unused_x = select.select([output_fd], [], [], TERMINAL_IO_POLL_SECONDS)
475
- if ready:
476
- try:
477
- chunk = os.read(output_fd, 4096)
478
- except OSError:
479
- chunk = b""
480
- if chunk:
515
+ if using_pty and master_fd is not None:
516
+ ready, _unused_w, _unused_x = select.select([master_fd], [], [], TERMINAL_IO_POLL_SECONDS)
517
+ if ready:
518
+ try:
519
+ chunk = os.read(master_fd, 4096)
520
+ except OSError:
521
+ chunk = b""
522
+ if chunk:
523
+ buffer += decoder.decode(chunk)
524
+ buffer = _drain_buffer(
525
+ buffer,
526
+ append_line,
527
+ flush_partial=session_kind == "terminal",
528
+ carriage_mode="stream" if session_kind == "terminal" else "marker",
529
+ )
530
+ else:
531
+ drained: list[bytes] = []
532
+ with pipe_chunks_lock:
533
+ if pipe_chunks:
534
+ drained = list(pipe_chunks)
535
+ pipe_chunks.clear()
536
+ for chunk in drained:
481
537
  buffer += decoder.decode(chunk)
482
538
  buffer = _drain_buffer(
483
539
  buffer,
@@ -485,23 +541,42 @@ def run_monitor(session_dir: Path) -> int:
485
541
  flush_partial=session_kind == "terminal",
486
542
  carriage_mode="stream" if session_kind == "terminal" else "marker",
487
543
  )
544
+ if not drained:
545
+ time.sleep(TERMINAL_IO_POLL_SECONDS)
488
546
  if process.poll() is not None:
489
547
  break
490
548
 
491
- while True:
492
- try:
493
- chunk = os.read(output_fd, 4096)
494
- except OSError:
495
- chunk = b""
496
- if not chunk:
497
- break
498
- buffer += decoder.decode(chunk)
499
- buffer = _drain_buffer(
500
- buffer,
501
- append_line,
502
- flush_partial=session_kind == "terminal",
503
- carriage_mode="stream" if session_kind == "terminal" else "marker",
504
- )
549
+ if using_pty and master_fd is not None:
550
+ while True:
551
+ try:
552
+ chunk = os.read(master_fd, 4096)
553
+ except OSError:
554
+ chunk = b""
555
+ if not chunk:
556
+ break
557
+ buffer += decoder.decode(chunk)
558
+ buffer = _drain_buffer(
559
+ buffer,
560
+ append_line,
561
+ flush_partial=session_kind == "terminal",
562
+ carriage_mode="stream" if session_kind == "terminal" else "marker",
563
+ )
564
+ else:
565
+ if pipe_reader_thread is not None:
566
+ pipe_reader_thread.join(timeout=1)
567
+ drained = []
568
+ with pipe_chunks_lock:
569
+ if pipe_chunks:
570
+ drained = list(pipe_chunks)
571
+ pipe_chunks.clear()
572
+ for chunk in drained:
573
+ buffer += decoder.decode(chunk)
574
+ buffer = _drain_buffer(
575
+ buffer,
576
+ append_line,
577
+ flush_partial=session_kind == "terminal",
578
+ carriage_mode="stream" if session_kind == "terminal" else "marker",
579
+ )
505
580
  buffer += decoder.decode(b"", final=True)
506
581
  if buffer:
507
582
  append_line(buffer, stream="partial" if session_kind == "terminal" else "stdout")
@@ -535,6 +610,11 @@ def run_monitor(session_dir: Path) -> int:
535
610
  os.close(slave_fd)
536
611
  except OSError:
537
612
  pass
613
+ if process is not None and process.stdin is not None:
614
+ try:
615
+ process.stdin.close()
616
+ except OSError:
617
+ pass
538
618
  if process is not None and process.stdout is not None:
539
619
  try:
540
620
  process.stdout.close()