@researai/deepscientist 1.5.16 → 1.6.0

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 (896) hide show
  1. package/AGENTS.md +309 -130
  2. package/AISB/catalog/aisb.b1.agentic_coding.yaml +244 -0
  3. package/AISB/catalog/aisb.b10.climate_earth.yaml +235 -0
  4. package/AISB/catalog/aisb.b11.model_efficiency.yaml +231 -0
  5. package/AISB/catalog/aisb.b12.embodied_ai.yaml +238 -0
  6. package/AISB/catalog/aisb.b2.agent_systems.yaml +229 -0
  7. package/AISB/catalog/aisb.b3.self_evolving_rl.yaml +237 -0
  8. package/AISB/catalog/aisb.b4.lm_reasoning.yaml +240 -0
  9. package/AISB/catalog/aisb.b5.math_proof.yaml +235 -0
  10. package/AISB/catalog/aisb.b6.research_process.yaml +243 -0
  11. package/AISB/catalog/aisb.b7.multimodal_fusion.yaml +232 -0
  12. package/AISB/catalog/aisb.b8.lifesci_drug.yaml +275 -0
  13. package/AISB/catalog/aisb.b9.material_science.yaml +237 -0
  14. package/AISB/catalog/aisb.t3.001_savvy.yaml +159 -0
  15. package/AISB/catalog/aisb.t3.001_savvy.zh.yaml +121 -0
  16. package/AISB/catalog/aisb.t3.002_pinet.yaml +189 -0
  17. package/AISB/catalog/aisb.t3.002_pinet.zh.yaml +130 -0
  18. package/AISB/catalog/aisb.t3.004_decentralattn.yaml +184 -0
  19. package/AISB/catalog/aisb.t3.004_decentralattn.zh.yaml +153 -0
  20. package/AISB/catalog/aisb.t3.005_tsae.yaml +193 -0
  21. package/AISB/catalog/aisb.t3.005_tsae.zh.yaml +139 -0
  22. package/AISB/catalog/aisb.t3.006_physense.yaml +194 -0
  23. package/AISB/catalog/aisb.t3.006_physense.zh.yaml +118 -0
  24. package/AISB/catalog/aisb.t3.007_reasoningiqa.yaml +169 -0
  25. package/AISB/catalog/aisb.t3.007_reasoningiqa.zh.yaml +133 -0
  26. package/AISB/catalog/aisb.t3.008_meanflows.yaml +188 -0
  27. package/AISB/catalog/aisb.t3.008_meanflows.zh.yaml +140 -0
  28. package/AISB/catalog/aisb.t3.009_scoremissing.yaml +179 -0
  29. package/AISB/catalog/aisb.t3.009_scoremissing.zh.yaml +119 -0
  30. package/AISB/catalog/aisb.t3.010_suitabilityfilter.yaml +221 -0
  31. package/AISB/catalog/aisb.t3.010_suitabilityfilter.zh.yaml +141 -0
  32. package/AISB/catalog/aisb.t3.011_osd.yaml +206 -0
  33. package/AISB/catalog/aisb.t3.011_osd.zh.yaml +163 -0
  34. package/AISB/catalog/aisb.t3.012_efficientqat.yaml +206 -0
  35. package/AISB/catalog/aisb.t3.012_efficientqat.zh.yaml +159 -0
  36. package/AISB/catalog/aisb.t3.013_appl.yaml +152 -0
  37. package/AISB/catalog/aisb.t3.013_appl.zh.yaml +126 -0
  38. package/AISB/catalog/aisb.t3.014_piguard.yaml +207 -0
  39. package/AISB/catalog/aisb.t3.014_piguard.zh.yaml +164 -0
  40. package/AISB/catalog/aisb.t3.015_frspec.yaml +209 -0
  41. package/AISB/catalog/aisb.t3.015_frspec.zh.yaml +163 -0
  42. package/AISB/catalog/aisb.t3.016_mathfusion.yaml +166 -0
  43. package/AISB/catalog/aisb.t3.016_mathfusion.zh.yaml +145 -0
  44. package/AISB/catalog/aisb.t3.017_multimodalglp.yaml +171 -0
  45. package/AISB/catalog/aisb.t3.017_multimodalglp.zh.yaml +122 -0
  46. package/AISB/catalog/aisb.t3.018_cotsynth.yaml +206 -0
  47. package/AISB/catalog/aisb.t3.018_cotsynth.zh.yaml +162 -0
  48. package/AISB/catalog/aisb.t3.019_dyscaleut.yaml +211 -0
  49. package/AISB/catalog/aisb.t3.019_dyscaleut.zh.yaml +148 -0
  50. package/AISB/catalog/aisb.t3.020_aristotle.yaml +173 -0
  51. package/AISB/catalog/aisb.t3.020_aristotle.zh.yaml +119 -0
  52. package/AISB/catalog/aisb.t3.021_tokenrecycling.yaml +160 -0
  53. package/AISB/catalog/aisb.t3.021_tokenrecycling.zh.yaml +129 -0
  54. package/AISB/catalog/aisb.t3.022_chainofreasoning.yaml +204 -0
  55. package/AISB/catalog/aisb.t3.022_chainofreasoning.zh.yaml +161 -0
  56. package/AISB/catalog/aisb.t3.023_guidedembed.yaml +211 -0
  57. package/AISB/catalog/aisb.t3.023_guidedembed.zh.yaml +189 -0
  58. package/AISB/catalog/aisb.t3.024_outputcentric.yaml +148 -0
  59. package/AISB/catalog/aisb.t3.024_outputcentric.zh.yaml +131 -0
  60. package/AISB/catalog/aisb.t3.025_deeper.yaml +143 -0
  61. package/AISB/catalog/aisb.t3.025_deeper.zh.yaml +116 -0
  62. package/AISB/catalog/aisb.t3.026_gartkg.yaml +195 -0
  63. package/AISB/catalog/aisb.t3.026_gartkg.zh.yaml +127 -0
  64. package/AISB/catalog/aisb.t3.027_citeeval.yaml +182 -0
  65. package/AISB/catalog/aisb.t3.027_citeeval.zh.yaml +135 -0
  66. package/AISB/catalog/aisb.t3.028_sbam.yaml +206 -0
  67. package/AISB/catalog/aisb.t3.028_sbam.zh.yaml +166 -0
  68. package/AISB/catalog/aisb.t3.029_cdqgeoembed.yaml +224 -0
  69. package/AISB/catalog/aisb.t3.029_cdqgeoembed.zh.yaml +142 -0
  70. package/AISB/catalog/aisb.t3.030_processrm.yaml +211 -0
  71. package/AISB/catalog/aisb.t3.030_processrm.zh.yaml +166 -0
  72. package/AISB/catalog/aisb.t3.031_circuitstability.yaml +172 -0
  73. package/AISB/catalog/aisb.t3.031_circuitstability.zh.yaml +134 -0
  74. package/AISB/catalog/aisb.t3.032_ptsolver.yaml +169 -0
  75. package/AISB/catalog/aisb.t3.032_ptsolver.zh.yaml +135 -0
  76. package/AISB/catalog/aisb.t3.033_gcse.yaml +144 -0
  77. package/AISB/catalog/aisb.t3.033_gcse.zh.yaml +126 -0
  78. package/AISB/catalog/aisb.t3.034_ensemblewm.yaml +183 -0
  79. package/AISB/catalog/aisb.t3.034_ensemblewm.zh.yaml +146 -0
  80. package/AISB/catalog/aisb.t3.035_moralvalueswa.yaml +207 -0
  81. package/AISB/catalog/aisb.t3.035_moralvalueswa.zh.yaml +165 -0
  82. package/AISB/catalog/aisb.t3.036_weakstrongpref.yaml +210 -0
  83. package/AISB/catalog/aisb.t3.036_weakstrongpref.zh.yaml +194 -0
  84. package/AISB/catalog/aisb.t3.037_dementiamask.yaml +172 -0
  85. package/AISB/catalog/aisb.t3.037_dementiamask.zh.yaml +132 -0
  86. package/AISB/catalog/aisb.t3.038_tinysam.yaml +284 -0
  87. package/AISB/catalog/aisb.t3.038_tinysam.zh.yaml +240 -0
  88. package/AISB/catalog/aisb.t3.039_calf.yaml +224 -0
  89. package/AISB/catalog/aisb.t3.039_calf.zh.yaml +194 -0
  90. package/AISB/catalog/aisb.t3.040_graniteguardian.yaml +199 -0
  91. package/AISB/catalog/aisb.t3.040_graniteguardian.zh.yaml +174 -0
  92. package/AISB/catalog/aisb.t3.041_amdm.yaml +149 -0
  93. package/AISB/catalog/aisb.t3.041_amdm.zh.yaml +137 -0
  94. package/AISB/catalog/aisb.t3.042_xpatch.yaml +216 -0
  95. package/AISB/catalog/aisb.t3.042_xpatch.zh.yaml +182 -0
  96. package/AISB/catalog/aisb.t3.043_vhm.yaml +268 -0
  97. package/AISB/catalog/aisb.t3.043_vhm.zh.yaml +193 -0
  98. package/AISB/catalog/aisb.t3.044_rgvi.yaml +224 -0
  99. package/AISB/catalog/aisb.t3.044_rgvi.zh.yaml +176 -0
  100. package/AISB/catalog/aisb.t3.045_pslstm.yaml +203 -0
  101. package/AISB/catalog/aisb.t3.045_pslstm.zh.yaml +179 -0
  102. package/AISB/catalog/aisb.t3.046_nonstatts.yaml +208 -0
  103. package/AISB/catalog/aisb.t3.046_nonstatts.zh.yaml +194 -0
  104. package/AISB/catalog/aisb.t3.047_timepfn.yaml +156 -0
  105. package/AISB/catalog/aisb.t3.047_timepfn.zh.yaml +124 -0
  106. package/AISB/catalog/aisb.t3.048_proxyspex.yaml +148 -0
  107. package/AISB/catalog/aisb.t3.048_proxyspex.zh.yaml +125 -0
  108. package/AISB/catalog/aisb.t3.049_hogwildinference.yaml +183 -0
  109. package/AISB/catalog/aisb.t3.049_hogwildinference.zh.yaml +138 -0
  110. package/AISB/catalog/aisb.t3.050_causalpfn.yaml +214 -0
  111. package/AISB/catalog/aisb.t3.050_causalpfn.zh.yaml +190 -0
  112. package/AISB/catalog/aisb.t3.051_flashtp.yaml +169 -0
  113. package/AISB/catalog/aisb.t3.051_flashtp.zh.yaml +124 -0
  114. package/AISB/catalog/aisb.t3.052_nsdiff.yaml +155 -0
  115. package/AISB/catalog/aisb.t3.052_nsdiff.zh.yaml +138 -0
  116. package/AISB/catalog/aisb.t3.053_k2vae.yaml +158 -0
  117. package/AISB/catalog/aisb.t3.053_k2vae.zh.yaml +132 -0
  118. package/AISB/catalog/aisb.t3.054_timebase.yaml +178 -0
  119. package/AISB/catalog/aisb.t3.054_timebase.zh.yaml +158 -0
  120. package/AISB/catalog/aisb.t3.055_csbrain.yaml +238 -0
  121. package/AISB/catalog/aisb.t3.055_csbrain.zh.yaml +184 -0
  122. package/AISB/catalog/aisb.t3.056_infosam.yaml +224 -0
  123. package/AISB/catalog/aisb.t3.056_infosam.zh.yaml +189 -0
  124. package/AISB/catalog/aisb.t3.057_mdreid.yaml +129 -0
  125. package/AISB/catalog/aisb.t3.057_mdreid.zh.yaml +117 -0
  126. package/AISB/catalog/aisb.t3.058_mindglitch.yaml +171 -0
  127. package/AISB/catalog/aisb.t3.058_mindglitch.zh.yaml +145 -0
  128. package/AISB/catalog/aisb.t3.059_selfsupervised.yaml +154 -0
  129. package/AISB/catalog/aisb.t3.059_selfsupervised.zh.yaml +125 -0
  130. package/AISB/catalog/aisb.t3.060_iaggad.yaml +121 -0
  131. package/AISB/catalog/aisb.t3.060_iaggad.zh.yaml +100 -0
  132. package/AISB/catalog/aisb.t3.061_hsgkn.yaml +136 -0
  133. package/AISB/catalog/aisb.t3.061_hsgkn.zh.yaml +113 -0
  134. package/AISB/catalog/aisb.t3.062_visionts.yaml +237 -0
  135. package/AISB/catalog/aisb.t3.062_visionts.zh.yaml +216 -0
  136. package/AISB/catalog/aisb.t3.063_tsrag.yaml +162 -0
  137. package/AISB/catalog/aisb.t3.063_tsrag.zh.yaml +138 -0
  138. package/AISB/catalog/aisb.t3.064_pir.yaml +221 -0
  139. package/AISB/catalog/aisb.t3.064_pir.zh.yaml +197 -0
  140. package/AISB/catalog/aisb.t3.065_proteinbinding.yaml +234 -0
  141. package/AISB/catalog/aisb.t3.065_proteinbinding.zh.yaml +167 -0
  142. package/AISB/catalog/aisb.t3.066_tropicalattention.yaml +267 -0
  143. package/AISB/catalog/aisb.t3.066_tropicalattention.zh.yaml +229 -0
  144. package/AISB/catalog/aisb.t3.067_kanad.yaml +193 -0
  145. package/AISB/catalog/aisb.t3.067_kanad.zh.yaml +167 -0
  146. package/AISB/catalog/aisb.t3.068_sempo.yaml +187 -0
  147. package/AISB/catalog/aisb.t3.068_sempo.zh.yaml +148 -0
  148. package/AISB/catalog/aisb.t3.069_treehfd.yaml +129 -0
  149. package/AISB/catalog/aisb.t3.069_treehfd.zh.yaml +111 -0
  150. package/AISB/catalog/aisb.t3.070_certifiedunlearning.yaml +224 -0
  151. package/AISB/catalog/aisb.t3.070_certifiedunlearning.zh.yaml +171 -0
  152. package/AISB/catalog/aisb.t3.071_neuralmjd.yaml +142 -0
  153. package/AISB/catalog/aisb.t3.071_neuralmjd.zh.yaml +120 -0
  154. package/AISB/catalog/aisb.t3.072_fedgmt.yaml +181 -0
  155. package/AISB/catalog/aisb.t3.072_fedgmt.zh.yaml +158 -0
  156. package/AISB/catalog/aisb.t3.073_rld.yaml +161 -0
  157. package/AISB/catalog/aisb.t3.073_rld.zh.yaml +129 -0
  158. package/AISB/catalog/aisb.t3.074_lsvi.yaml +163 -0
  159. package/AISB/catalog/aisb.t3.074_lsvi.zh.yaml +129 -0
  160. package/AISB/catalog/aisb.t3.075_treeslicedentropy.yaml +201 -0
  161. package/AISB/catalog/aisb.t3.075_treeslicedentropy.zh.yaml +148 -0
  162. package/AISB/catalog/aisb.t3.076_aanet.yaml +169 -0
  163. package/AISB/catalog/aisb.t3.076_aanet.zh.yaml +129 -0
  164. package/AISB/catalog/aisb.t3.077_cmnn.yaml +199 -0
  165. package/AISB/catalog/aisb.t3.077_cmnn.zh.yaml +165 -0
  166. package/AISB/catalog/aisb.t3.078_conformalanomaly.yaml +146 -0
  167. package/AISB/catalog/aisb.t3.078_conformalanomaly.zh.yaml +117 -0
  168. package/AISB/catalog/aisb.t3.079_dpfkmeans.yaml +131 -0
  169. package/AISB/catalog/aisb.t3.079_dpfkmeans.zh.yaml +104 -0
  170. package/AISB/catalog/aisb.t3.080_latentscorereweight.yaml +169 -0
  171. package/AISB/catalog/aisb.t3.080_latentscorereweight.zh.yaml +123 -0
  172. package/AISB/catalog/aisb.t3.081_qmamba.yaml +150 -0
  173. package/AISB/catalog/aisb.t3.081_qmamba.zh.yaml +117 -0
  174. package/AISB/catalog/aisb.t3.082_onlinellmrouting.yaml +160 -0
  175. package/AISB/catalog/aisb.t3.082_onlinellmrouting.zh.yaml +133 -0
  176. package/AISB/catalog/aisb.t3.083_starformer.yaml +178 -0
  177. package/AISB/catalog/aisb.t3.083_starformer.zh.yaml +140 -0
  178. package/AISB/catalog/aisb.t3.084_ift.yaml +139 -0
  179. package/AISB/catalog/aisb.t3.084_ift.zh.yaml +111 -0
  180. package/AISB/catalog/aisb.t3.085_neuralsurv.yaml +183 -0
  181. package/AISB/catalog/aisb.t3.085_neuralsurv.zh.yaml +143 -0
  182. package/AISB/catalog/aisb.t3.086_stella.yaml +197 -0
  183. package/AISB/catalog/aisb.t3.086_stella.zh.yaml +142 -0
  184. package/AISB/catalog/aisb.t3.087_moses.yaml +167 -0
  185. package/AISB/catalog/aisb.t3.087_moses.zh.yaml +132 -0
  186. package/AISB/catalog/aisb.t3.088_channelnorm.yaml +140 -0
  187. package/AISB/catalog/aisb.t3.088_channelnorm.zh.yaml +109 -0
  188. package/AISB/catalog/aisb.t3.089_causalvelocity.yaml +730 -0
  189. package/AISB/catalog/aisb.t3.089_causalvelocity.zh.yaml +668 -0
  190. package/AISB/catalog/aisb.t3.090_rstib.yaml +144 -0
  191. package/AISB/catalog/aisb.t3.090_rstib.zh.yaml +109 -0
  192. package/AISB/catalog/aisb.t3.091_timeawarecausal.yaml +132 -0
  193. package/AISB/catalog/aisb.t3.091_timeawarecausal.zh.yaml +107 -0
  194. package/AISB/catalog/aisb.t3.092_kmeanslocalopt.yaml +138 -0
  195. package/AISB/catalog/aisb.t3.092_kmeanslocalopt.zh.yaml +110 -0
  196. package/AISB/catalog/aisb.t3.093_fedwmsam.yaml +134 -0
  197. package/AISB/catalog/aisb.t3.093_fedwmsam.zh.yaml +106 -0
  198. package/AISB/catalog/aisb.t3.094_boundre.yaml +147 -0
  199. package/AISB/catalog/aisb.t3.094_boundre.zh.yaml +114 -0
  200. package/AISB/catalog/aisb.t3.095_fastfeaturecp.yaml +153 -0
  201. package/AISB/catalog/aisb.t3.095_fastfeaturecp.zh.yaml +118 -0
  202. package/AISB/catalog/aisb.t3.096_m3svm.yaml +189 -0
  203. package/AISB/catalog/aisb.t3.096_m3svm.zh.yaml +149 -0
  204. package/AISB/catalog/aisb.t3.097_wassersteintl.yaml +212 -0
  205. package/AISB/catalog/aisb.t3.097_wassersteintl.zh.yaml +169 -0
  206. package/AISB/catalog/aisb.t3.098_xmahalanobis.yaml +171 -0
  207. package/AISB/catalog/aisb.t3.098_xmahalanobis.zh.yaml +127 -0
  208. package/AISB/catalog/aisb.t3.099_ollalanding.yaml +248 -0
  209. package/AISB/catalog/aisb.t3.099_ollalanding.zh.yaml +182 -0
  210. package/AISB/catalog/aisb.t3.100_invmissingdata.yaml +179 -0
  211. package/AISB/catalog/aisb.t3.100_invmissingdata.zh.yaml +150 -0
  212. package/AISB/catalog/aisb.t3.101_acia.yaml +164 -0
  213. package/AISB/catalog/aisb.t3.101_acia.zh.yaml +109 -0
  214. package/AISB/catalog/aisb.t3.102_stochasticff.yaml +178 -0
  215. package/AISB/catalog/aisb.t3.102_stochasticff.zh.yaml +130 -0
  216. package/AISB/catalog/aisb.t3.103_qdcp.yaml +150 -0
  217. package/AISB/catalog/aisb.t3.103_qdcp.zh.yaml +116 -0
  218. package/AISB/catalog/aisb.t3.104_balancedactiveinf.yaml +137 -0
  219. package/AISB/catalog/aisb.t3.104_balancedactiveinf.zh.yaml +104 -0
  220. package/AISB/catalog/aisb.t3.105_binaryclasseval.yaml +161 -0
  221. package/AISB/catalog/aisb.t3.105_binaryclasseval.zh.yaml +130 -0
  222. package/AISB/image/001_aisb.t3.001_savvy.jpg +0 -0
  223. package/AISB/image/002_aisb.t3.002_pinet.jpg +0 -0
  224. package/AISB/image/003_aisb.t3.003_dmsqd.jpg +0 -0
  225. package/AISB/image/004_aisb.t3.004_decentralattn.jpg +0 -0
  226. package/AISB/image/005_aisb.t3.005_tsae.jpg +0 -0
  227. package/AISB/image/006_aisb.t3.006_physense.jpg +0 -0
  228. package/AISB/image/007_aisb.t3.007_reasoningiqa.jpg +0 -0
  229. package/AISB/image/008_aisb.t3.008_meanflows.jpg +0 -0
  230. package/AISB/image/009_aisb.t3.009_scoremissing.jpg +0 -0
  231. package/AISB/image/010_aisb.t3.010_suitabilityfilter.jpg +0 -0
  232. package/AISB/image/011_aisb.t3.011_osd.jpg +0 -0
  233. package/AISB/image/012_aisb.t3.012_efficientqat.jpg +0 -0
  234. package/AISB/image/013_aisb.t3.013_appl.jpg +0 -0
  235. package/AISB/image/014_aisb.t3.014_piguard.jpg +0 -0
  236. package/AISB/image/015_aisb.t3.015_frspec.jpg +0 -0
  237. package/AISB/image/016_aisb.t3.016_mathfusion.jpg +0 -0
  238. package/AISB/image/017_aisb.t3.017_multimodalglp.jpg +0 -0
  239. package/AISB/image/018_aisb.t3.018_cotsynth.jpg +0 -0
  240. package/AISB/image/019_aisb.t3.019_dyscaleut.jpg +0 -0
  241. package/AISB/image/020_aisb.t3.020_aristotle.jpg +0 -0
  242. package/AISB/image/021_aisb.t3.021_tokenrecycling.jpg +0 -0
  243. package/AISB/image/022_aisb.t3.022_chainofreasoning.jpg +0 -0
  244. package/AISB/image/023_aisb.t3.023_guidedembed.jpg +0 -0
  245. package/AISB/image/024_aisb.t3.024_outputcentric.jpg +0 -0
  246. package/AISB/image/025_aisb.t3.025_deeper.jpg +0 -0
  247. package/AISB/image/026_aisb.t3.026_gartkg.jpg +0 -0
  248. package/AISB/image/027_aisb.t3.027_citeeval.jpg +0 -0
  249. package/AISB/image/028_aisb.t3.028_sbam.jpg +0 -0
  250. package/AISB/image/029_aisb.t3.029_cdqgeoembed.jpg +0 -0
  251. package/AISB/image/030_aisb.t3.030_processrm.jpg +0 -0
  252. package/AISB/image/031_aisb.t3.031_circuitstability.jpg +0 -0
  253. package/AISB/image/032_aisb.t3.032_ptsolver.jpg +0 -0
  254. package/AISB/image/033_aisb.t3.033_gcse.jpg +0 -0
  255. package/AISB/image/034_aisb.t3.034_ensemblewm.jpg +0 -0
  256. package/AISB/image/035_aisb.t3.035_moralvalueswa.jpg +0 -0
  257. package/AISB/image/036_aisb.t3.036_weakstrongpref.jpg +0 -0
  258. package/AISB/image/037_aisb.t3.037_dementiamask.jpg +0 -0
  259. package/AISB/image/038_aisb.t3.038_tinysam.jpg +0 -0
  260. package/AISB/image/039_aisb.t3.039_calf.jpg +0 -0
  261. package/AISB/image/040_aisb.t3.040_graniteguardian.jpg +0 -0
  262. package/AISB/image/041_aisb.t3.041_amdm.jpg +0 -0
  263. package/AISB/image/042_aisb.t3.042_xpatch.jpg +0 -0
  264. package/AISB/image/043_aisb.t3.043_vhm.jpg +0 -0
  265. package/AISB/image/044_aisb.t3.044_rgvi.jpg +0 -0
  266. package/AISB/image/045_aisb.t3.045_pslstm.jpg +0 -0
  267. package/AISB/image/046_aisb.t3.046_nonstatts.jpg +0 -0
  268. package/AISB/image/047_aisb.t3.047_timepfn.jpg +0 -0
  269. package/AISB/image/048_aisb.t3.048_proxyspex.jpg +0 -0
  270. package/AISB/image/049_aisb.t3.049_hogwildinference.jpg +0 -0
  271. package/AISB/image/050_aisb.t3.050_causalpfn.jpg +0 -0
  272. package/AISB/image/051_aisb.t3.051_flashtp.jpg +0 -0
  273. package/AISB/image/052_aisb.t3.052_nsdiff.jpg +0 -0
  274. package/AISB/image/053_aisb.t3.053_k2vae.jpg +0 -0
  275. package/AISB/image/054_aisb.t3.054_timebase.jpg +0 -0
  276. package/AISB/image/055_aisb.t3.055_csbrain.jpg +0 -0
  277. package/AISB/image/056_aisb.t3.056_infosam.jpg +0 -0
  278. package/AISB/image/057_aisb.t3.057_mdreid.jpg +0 -0
  279. package/AISB/image/058_aisb.t3.058_mindglitch.jpg +0 -0
  280. package/AISB/image/059_aisb.t3.059_selfsupervised.jpg +0 -0
  281. package/AISB/image/060_aisb.t3.060_iaggad.jpg +0 -0
  282. package/AISB/image/061_aisb.t3.061_hsgkn.jpg +0 -0
  283. package/AISB/image/062_aisb.t3.062_visionts.jpg +0 -0
  284. package/AISB/image/063_aisb.t3.063_tsrag.jpg +0 -0
  285. package/AISB/image/064_aisb.t3.064_pir.jpg +0 -0
  286. package/AISB/image/065_aisb.t3.065_proteinbinding.jpg +0 -0
  287. package/AISB/image/066_aisb.t3.066_tropicalattention.jpg +0 -0
  288. package/AISB/image/067_aisb.t3.067_kanad.jpg +0 -0
  289. package/AISB/image/068_aisb.t3.068_sempo.jpg +0 -0
  290. package/AISB/image/069_aisb.t3.069_treehfd.jpg +0 -0
  291. package/AISB/image/070_aisb.t3.070_certifiedunlearning.jpg +0 -0
  292. package/AISB/image/071_aisb.t3.071_neuralmjd.jpg +0 -0
  293. package/AISB/image/072_aisb.t3.072_fedgmt.jpg +0 -0
  294. package/AISB/image/073_aisb.t3.073_rld.jpg +0 -0
  295. package/AISB/image/074_aisb.t3.074_lsvi.jpg +0 -0
  296. package/AISB/image/075_aisb.t3.075_treeslicedentropy.jpg +0 -0
  297. package/AISB/image/076_aisb.t3.076_aanet.jpg +0 -0
  298. package/AISB/image/077_aisb.t3.077_cmnn.jpg +0 -0
  299. package/AISB/image/078_aisb.t3.078_conformalanomaly.jpg +0 -0
  300. package/AISB/image/079_aisb.t3.079_dpfkmeans.jpg +0 -0
  301. package/AISB/image/080_aisb.t3.080_latentscorereweight.jpg +0 -0
  302. package/AISB/image/081_aisb.t3.081_qmamba.jpg +0 -0
  303. package/AISB/image/082_aisb.t3.082_onlinellmrouting.jpg +0 -0
  304. package/AISB/image/083_aisb.t3.083_starformer.jpg +0 -0
  305. package/AISB/image/084_aisb.t3.084_ift.jpg +0 -0
  306. package/AISB/image/085_aisb.t3.085_neuralsurv.jpg +0 -0
  307. package/AISB/image/086_aisb.t3.086_stella.jpg +0 -0
  308. package/AISB/image/087_aisb.t3.087_moses.jpg +0 -0
  309. package/AISB/image/088_aisb.t3.088_channelnorm.jpg +0 -0
  310. package/AISB/image/089_aisb.t3.089_causalvelocity.jpg +0 -0
  311. package/AISB/image/090_aisb.t3.090_rstib.jpg +0 -0
  312. package/AISB/image/091_aisb.t3.091_timeawarecausal.jpg +0 -0
  313. package/AISB/image/092_aisb.t3.092_kmeanslocalopt.jpg +0 -0
  314. package/AISB/image/093_aisb.t3.093_fedwmsam.jpg +0 -0
  315. package/AISB/image/094_aisb.t3.094_boundre.jpg +0 -0
  316. package/AISB/image/095_aisb.t3.095_fastfeaturecp.jpg +0 -0
  317. package/AISB/image/096_aisb.t3.096_m3svm.jpg +0 -0
  318. package/AISB/image/097_aisb.t3.097_wassersteintl.jpg +0 -0
  319. package/AISB/image/098_aisb.t3.098_xmahalanobis.jpg +0 -0
  320. package/AISB/image/099_aisb.t3.099_ollalanding.jpg +0 -0
  321. package/AISB/image/100_aisb.t3.100_invmissingdata.jpg +0 -0
  322. package/AISB/image/101_aisb.t3.101_acia.jpg +0 -0
  323. package/AISB/image/102_aisb.t3.102_stochasticff.jpg +0 -0
  324. package/AISB/image/103_aisb.t3.103_qdcp.jpg +0 -0
  325. package/AISB/image/104_aisb.t3.104_balancedactiveinf.jpg +0 -0
  326. package/AISB/image/105_aisb.t3.105_binaryclasseval.jpg +0 -0
  327. package/AISB/image/106_aisb.t1.reasoning_lite.jpg +0 -0
  328. package/AISB/image/107_aisb.t2.paper_audit.jpg +0 -0
  329. package/AISB/image/108_aisb.t3.multi_gpu_search.jpg +0 -0
  330. package/AISB/image/109_aisb.t3.tdc_admet.jpg +0 -0
  331. package/AISB/image/aisb.b1.agentic_coding.svg +16 -0
  332. package/AISB/image/aisb.b10.climate_earth.svg +16 -0
  333. package/AISB/image/aisb.b11.model_efficiency.svg +16 -0
  334. package/AISB/image/aisb.b12.embodied_ai.svg +16 -0
  335. package/AISB/image/aisb.b2.agent_systems.svg +16 -0
  336. package/AISB/image/aisb.b3.self_evolving_rl.svg +16 -0
  337. package/AISB/image/aisb.b4.lm_reasoning.svg +16 -0
  338. package/AISB/image/aisb.b5.math_proof.svg +16 -0
  339. package/AISB/image/aisb.b6.research_process.svg +16 -0
  340. package/AISB/image/aisb.b7.multimodal_fusion.svg +16 -0
  341. package/AISB/image/aisb.b8.lifesci_drug.svg +16 -0
  342. package/AISB/image/aisb.b9.material_science.svg +16 -0
  343. package/README.md +196 -32
  344. package/bin/ds.js +924 -66
  345. package/docs/en/00_QUICK_START.md +195 -18
  346. package/docs/en/01_SETTINGS_REFERENCE.md +468 -96
  347. package/docs/en/02_START_RESEARCH_GUIDE.md +26 -5
  348. package/docs/en/03_QQ_CONNECTOR_GUIDE.md +14 -3
  349. package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +2 -0
  350. package/docs/en/05_TUI_GUIDE.md +171 -2
  351. package/docs/en/07_MEMORY_AND_MCP.md +38 -2
  352. package/docs/en/09_DOCTOR.md +78 -7
  353. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +38 -1
  354. package/docs/en/11_LICENSE_AND_RISK.md +4 -0
  355. package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +15 -0
  356. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +9 -0
  357. package/docs/en/15_CODEX_PROVIDER_SETUP.md +624 -180
  358. package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +14 -0
  359. package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +14 -0
  360. package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +14 -0
  361. package/docs/en/21_LOCAL_MODEL_BACKENDS_GUIDE.md +386 -0
  362. package/docs/en/22_BENCHSTORE_YAML_REFERENCE.md +469 -0
  363. package/docs/en/23_BENCHSTORE_GITHUB_RELEASES_SPEC.md +316 -0
  364. package/docs/en/24_CLAUDE_CODE_PROVIDER_SETUP.md +469 -0
  365. package/docs/en/25_OPENCODE_PROVIDER_SETUP.md +653 -0
  366. package/docs/en/26_CITATION_AND_ATTRIBUTION.md +119 -0
  367. package/docs/en/27_KIMI_CODE_PROVIDER_SETUP.md +180 -0
  368. package/docs/en/28_DISCORD_CONNECTOR_GUIDE.md +61 -0
  369. package/docs/en/29_SLACK_CONNECTOR_GUIDE.md +60 -0
  370. package/docs/en/30_SETTINGS_CONTROL_CENTER_GUIDE.md +371 -0
  371. package/docs/en/{19_LOCAL_BROWSER_AUTH.md → 31_LOCAL_BROWSER_AUTH.md} +1 -1
  372. package/docs/en/32_WINDOWS_WSL2_DEPLOYMENT_GUIDE.md +273 -0
  373. package/docs/en/33_WORKSPACE_EXPLORER_QA.md +121 -0
  374. package/docs/en/91_DEVELOPMENT.md +266 -0
  375. package/docs/en/99_ACKNOWLEDGEMENTS.md +24 -19
  376. package/docs/en/README.md +48 -7
  377. package/docs/images/admin/admin-connectors-health-en.png +0 -0
  378. package/docs/images/admin/admin-controllers-en.png +0 -0
  379. package/docs/images/admin/admin-diagnostics-en.png +0 -0
  380. package/docs/images/admin/admin-errors-en.png +0 -0
  381. package/docs/images/admin/admin-issues-en.png +0 -0
  382. package/docs/images/admin/admin-logs-en.png +0 -0
  383. package/docs/images/admin/admin-quest-detail-en.png +0 -0
  384. package/docs/images/admin/admin-quests-en.png +0 -0
  385. package/docs/images/admin/admin-repairs-en.png +0 -0
  386. package/docs/images/admin/admin-runtime-en.png +0 -0
  387. package/docs/images/admin/admin-search-en.png +0 -0
  388. package/docs/images/admin/admin-stats-en.png +0 -0
  389. package/docs/images/admin/admin-summary-en.png +0 -0
  390. package/docs/images/connectors/connector-discord-en.png +0 -0
  391. package/docs/images/connectors/connector-feishu-en.png +0 -0
  392. package/docs/images/connectors/connector-lingzhu-en.png +0 -0
  393. package/docs/images/connectors/connector-qq-en.png +0 -0
  394. package/docs/images/connectors/connector-slack-en.png +0 -0
  395. package/docs/images/connectors/connector-telegram-en.png +0 -0
  396. package/docs/images/connectors/connector-weixin-en.png +0 -0
  397. package/docs/images/connectors/connector-whatsapp-en.png +0 -0
  398. package/docs/images/settings/settings-baselines-en.png +0 -0
  399. package/docs/images/settings/settings-config-en.png +0 -0
  400. package/docs/images/settings/settings-connectors-overview-en.png +0 -0
  401. package/docs/images/settings/settings-deepxiv-en.png +0 -0
  402. package/docs/images/settings/settings-mcp-servers-en.png +0 -0
  403. package/docs/images/settings/settings-plugins-en.png +0 -0
  404. package/docs/images/settings/settings-runners-en.png +0 -0
  405. package/docs/zh/00_QUICK_START.md +142 -18
  406. package/docs/zh/01_SETTINGS_REFERENCE.md +219 -98
  407. package/docs/zh/02_START_RESEARCH_GUIDE.md +26 -5
  408. package/docs/zh/05_TUI_GUIDE.md +171 -2
  409. package/docs/zh/07_MEMORY_AND_MCP.md +29 -2
  410. package/docs/zh/09_DOCTOR.md +54 -8
  411. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +24 -1
  412. package/docs/zh/11_LICENSE_AND_RISK.md +4 -0
  413. package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +15 -0
  414. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +9 -0
  415. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +552 -181
  416. package/docs/zh/21_LOCAL_MODEL_BACKENDS_GUIDE.md +384 -0
  417. package/docs/zh/22_BENCHSTORE_YAML_REFERENCE.md +459 -0
  418. package/docs/zh/23_BENCHSTORE_GITHUB_RELEASES_SPEC.md +287 -0
  419. package/docs/zh/23_CLAUDE_RUNNER_GUIDE.md +103 -0
  420. package/docs/zh/24_CLAUDE_CODE_PROVIDER_SETUP.md +460 -0
  421. package/docs/zh/25_OPENCODE_PROVIDER_SETUP.md +660 -0
  422. package/docs/zh/26_CITATION_AND_ATTRIBUTION.md +102 -0
  423. package/docs/zh/27_KIMI_CODE_PROVIDER_SETUP.md +51 -0
  424. package/docs/zh/{19_LOCAL_BROWSER_AUTH.md → 31_LOCAL_BROWSER_AUTH.md} +1 -1
  425. package/docs/zh/32_WINDOWS_WSL2_DEPLOYMENT_GUIDE.md +264 -0
  426. package/docs/zh/33_WORKSPACE_EXPLORER_QA.md +127 -0
  427. package/docs/zh/99_ACKNOWLEDGEMENTS.md +23 -19
  428. package/docs/zh/README.md +33 -7
  429. package/install.sh +168 -20
  430. package/package.json +5 -1
  431. package/pyproject.toml +2 -1
  432. package/src/deepscientist/__init__.py +1 -1
  433. package/src/deepscientist/acp/envelope.py +13 -0
  434. package/src/deepscientist/admin/__init__.py +3 -0
  435. package/src/deepscientist/admin/charts.py +681 -0
  436. package/src/deepscientist/admin/logs.py +119 -0
  437. package/src/deepscientist/admin/repairs.py +217 -0
  438. package/src/deepscientist/admin/service.py +1310 -0
  439. package/src/deepscientist/admin/system_info.py +700 -0
  440. package/src/deepscientist/admin/tasks.py +465 -0
  441. package/src/deepscientist/admin/tool_metrics.py +600 -0
  442. package/src/deepscientist/artifact/guidance.py +8 -4
  443. package/src/deepscientist/artifact/schemas.py +115 -0
  444. package/src/deepscientist/artifact/service.py +4268 -260
  445. package/src/deepscientist/bash_exec/monitor.py +30 -3
  446. package/src/deepscientist/bash_exec/service.py +134 -1
  447. package/src/deepscientist/benchstore/__init__.py +4 -0
  448. package/src/deepscientist/benchstore/prompt_builder.py +224 -0
  449. package/src/deepscientist/benchstore/service.py +1716 -0
  450. package/src/deepscientist/bridges/connectors.py +8 -2
  451. package/src/deepscientist/channels/weixin_ilink.py +8 -1
  452. package/src/deepscientist/cli.py +92 -17
  453. package/src/deepscientist/codex_cli_compat.py +187 -74
  454. package/src/deepscientist/config/models.py +82 -11
  455. package/src/deepscientist/config/service.py +1077 -93
  456. package/src/deepscientist/connector/weixin_support.py +48 -17
  457. package/src/deepscientist/daemon/api/handlers.py +827 -235
  458. package/src/deepscientist/daemon/api/router.py +81 -1
  459. package/src/deepscientist/daemon/app.py +1512 -85
  460. package/src/deepscientist/diagnostics/__init__.py +6 -0
  461. package/src/deepscientist/diagnostics/runner_failures.py +277 -0
  462. package/src/deepscientist/doctor.py +407 -56
  463. package/src/deepscientist/evidence_packets.py +590 -0
  464. package/src/deepscientist/home.py +52 -4
  465. package/src/deepscientist/kimi_cli_compat.py +50 -0
  466. package/src/deepscientist/latex_runtime.py +2 -2
  467. package/src/deepscientist/mcp/context.py +2 -0
  468. package/src/deepscientist/mcp/schemas.py +114 -0
  469. package/src/deepscientist/mcp/server.py +1566 -126
  470. package/src/deepscientist/memory/service.py +203 -16
  471. package/src/deepscientist/process_control.py +8 -1
  472. package/src/deepscientist/prompts/builder.py +850 -88
  473. package/src/deepscientist/quest/__init__.py +2 -2
  474. package/src/deepscientist/quest/layout.py +12 -1
  475. package/src/deepscientist/quest/node_traces.py +10 -0
  476. package/src/deepscientist/quest/service.py +1852 -161
  477. package/src/deepscientist/quest/stage_views.py +1 -1
  478. package/src/deepscientist/runners/__init__.py +18 -0
  479. package/src/deepscientist/runners/base.py +89 -1
  480. package/src/deepscientist/runners/builtins.py +13 -1
  481. package/src/deepscientist/runners/claude.py +391 -0
  482. package/src/deepscientist/runners/codex.py +480 -35
  483. package/src/deepscientist/runners/codex_telemetry.py +127 -0
  484. package/src/deepscientist/runners/kimi.py +334 -0
  485. package/src/deepscientist/runners/metadata.py +68 -0
  486. package/src/deepscientist/runners/opencode.py +414 -0
  487. package/src/deepscientist/runners/runtime_overrides.py +100 -0
  488. package/src/deepscientist/runners/simple_cli.py +538 -0
  489. package/src/deepscientist/runtime_storage.py +303 -0
  490. package/src/deepscientist/shared.py +80 -16
  491. package/src/deepscientist/skills/installer.py +37 -0
  492. package/src/deepscientist/skills/registry.py +2 -0
  493. package/src/deepscientist/tinytex.py +2 -2
  494. package/src/deepscientist/tui.py +10 -3
  495. package/src/prompts/benchstore/system.md +77 -0
  496. package/src/prompts/connectors/qq.md +33 -2
  497. package/src/prompts/connectors/weixin.md +208 -23
  498. package/src/prompts/contracts/admin_ops.md +74 -0
  499. package/src/prompts/contracts/admin_ops_knowledge.md +138 -0
  500. package/src/prompts/contracts/shared_interaction.md +5 -10
  501. package/src/prompts/start_setup/system.md +422 -0
  502. package/src/prompts/system.md +411 -304
  503. package/src/prompts/system_copilot.md +89 -0
  504. package/src/skills/analysis-campaign/SKILL.md +239 -578
  505. package/src/skills/analysis-campaign/references/artifact-flow-examples.md +102 -0
  506. package/src/skills/analysis-campaign/references/boundary-cases.md +98 -0
  507. package/src/skills/analysis-campaign/references/campaign-checklist-template.md +39 -24
  508. package/src/skills/analysis-campaign/references/campaign-design.md +26 -10
  509. package/src/skills/analysis-campaign/references/campaign-plan-template.md +53 -54
  510. package/src/skills/analysis-campaign/references/operational-guidance.md +97 -0
  511. package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +10 -20
  512. package/src/skills/baseline/SKILL.md +183 -461
  513. package/src/skills/baseline/references/artifact-flow-examples.md +106 -0
  514. package/src/skills/baseline/references/artifact-payload-examples.md +1 -1
  515. package/src/skills/baseline/references/baseline-checklist-template.md +27 -35
  516. package/src/skills/baseline/references/baseline-plan-template.md +37 -76
  517. package/src/skills/baseline/references/boundary-cases.md +86 -0
  518. package/src/skills/baseline/references/codebase-audit-checklist.md +2 -6
  519. package/src/skills/baseline/references/comparability-contract.md +7 -12
  520. package/src/skills/baseline/references/operational-guidance.md +56 -0
  521. package/src/skills/baseline/references/route-selection.md +5 -25
  522. package/src/skills/decision/SKILL.md +113 -306
  523. package/src/skills/decision/references/checkpoint-memory-template.md +47 -0
  524. package/src/skills/decision/references/operational-guidance.md +94 -0
  525. package/src/skills/decision/references/research-route-criteria.md +7 -8
  526. package/src/skills/decision/references/strategic-decision-template.md +13 -26
  527. package/src/skills/experiment/SKILL.md +132 -670
  528. package/src/skills/experiment/references/execution-playbook.md +374 -0
  529. package/src/skills/experiment/references/main-experiment-checklist-template.md +26 -2
  530. package/src/skills/experiment/references/main-experiment-plan-template.md +28 -17
  531. package/src/skills/experiment/references/operational-guidance.md +108 -0
  532. package/src/skills/finalize/SKILL.md +62 -0
  533. package/src/skills/finalize/references/checkpoint-memory-template.md +49 -0
  534. package/src/skills/finalize/references/resume-packet-template.md +7 -0
  535. package/src/skills/idea/SKILL.md +228 -15
  536. package/src/skills/idea/references/controlled-brainstorming-playbook.md +78 -0
  537. package/src/skills/idea/references/current-board-packet-template.md +61 -0
  538. package/src/skills/idea/references/high-value-idea-sourcing.md +119 -0
  539. package/src/skills/idea/references/idea-generation-playbook.md +21 -0
  540. package/src/skills/idea/references/idea-thinking-flow.md +6 -0
  541. package/src/skills/idea/references/literature-survey-template.md +3 -0
  542. package/src/skills/idea/references/objective-contract-template.md +54 -0
  543. package/src/skills/idea/references/outline-seeding-example.md +56 -0
  544. package/src/skills/idea/references/pre-idea-draft-template.md +105 -0
  545. package/src/skills/idea/references/related-work-playbook.md +75 -2
  546. package/src/skills/idea/references/research-history-playbook.md +114 -0
  547. package/src/skills/idea/references/selection-gate.md +58 -6
  548. package/src/skills/intake-audit/SKILL.md +43 -2
  549. package/src/skills/intake-audit/references/state-audit-template.md +10 -0
  550. package/src/skills/nature-data/SKILL.md +128 -0
  551. package/src/skills/nature-data/UPSTREAM_LICENSE.txt +21 -0
  552. package/src/skills/nature-data/agents/openai.yaml +4 -0
  553. package/src/skills/nature-data/references/chinese-author-alignment.md +84 -0
  554. package/src/skills/nature-data/references/fair-metadata-checklist.md +105 -0
  555. package/src/skills/nature-data/references/policy-principles.md +103 -0
  556. package/src/skills/nature-data/references/repository-and-identifiers.md +96 -0
  557. package/src/skills/nature-data/references/source-basis.md +54 -0
  558. package/src/skills/nature-data/references/statement-patterns.md +153 -0
  559. package/src/skills/nature-figure/SKILL.md +197 -0
  560. package/src/skills/nature-figure/UPSTREAM_LICENSE.txt +21 -0
  561. package/src/skills/nature-figure/agents/openai.yaml +4 -0
  562. package/src/skills/nature-figure/evals/evals.json +37 -0
  563. package/src/skills/nature-figure/references/api.md +428 -0
  564. package/src/skills/nature-figure/references/backend-selection.md +100 -0
  565. package/src/skills/nature-figure/references/chart-types.md +281 -0
  566. package/src/skills/nature-figure/references/common-patterns.md +349 -0
  567. package/src/skills/nature-figure/references/design-theory.md +436 -0
  568. package/src/skills/nature-figure/references/figure-contract.md +93 -0
  569. package/src/skills/nature-figure/references/nature-2026-observations.md +112 -0
  570. package/src/skills/nature-figure/references/qa-contract.md +119 -0
  571. package/src/skills/nature-figure/references/r-template-index.md +66 -0
  572. package/src/skills/nature-figure/references/r-workflow.md +161 -0
  573. package/src/skills/nature-figure/references/tutorials.md +250 -0
  574. package/src/skills/nature-paper2ppt/SKILL.md +507 -0
  575. package/src/skills/nature-paper2ppt/UPSTREAM_LICENSE.txt +21 -0
  576. package/src/skills/nature-paper2ppt/agents/openai.yaml +4 -0
  577. package/src/skills/nature-polishing/SKILL.md +385 -0
  578. package/src/skills/nature-polishing/UPSTREAM_LICENSE.txt +21 -0
  579. package/src/skills/nature-polishing/agents/openai.yaml +4 -0
  580. package/src/skills/nature-polishing/references/phrasebank-playbook.md +162 -0
  581. package/src/skills/nature-polishing/references/section-moves.md +240 -0
  582. package/src/skills/nature-polishing/references/style-guardrails.md +94 -0
  583. package/src/skills/nature-polishing/references/writing-strategy.md +148 -0
  584. package/src/skills/optimize/SKILL.md +177 -1568
  585. package/src/skills/optimize/references/brief-shaping-playbook.md +95 -0
  586. package/src/skills/optimize/references/candidate-board-template.md +13 -0
  587. package/src/skills/optimize/references/candidate-ranking-template.md +51 -0
  588. package/src/skills/optimize/references/codegen-route-playbook.md +50 -0
  589. package/src/skills/optimize/references/debug-response-template.md +29 -0
  590. package/src/skills/optimize/references/frontier-review-template.md +32 -0
  591. package/src/skills/optimize/references/fusion-playbook.md +36 -0
  592. package/src/skills/optimize/references/method-brief-template.md +73 -0
  593. package/src/skills/optimize/references/operational-guidance.md +621 -0
  594. package/src/skills/optimize/references/optimization-memory-template.md +30 -0
  595. package/src/skills/optimize/references/optimize-checklist-template.md +18 -0
  596. package/src/skills/optimize/references/plateau-response-playbook.md +28 -0
  597. package/src/skills/optimize/references/prompt-patterns.md +49 -0
  598. package/src/skills/paper-outline/SKILL.md +227 -0
  599. package/src/skills/paper-outline/references/outline-patterns.md +87 -0
  600. package/src/skills/paper-plot/SKILL.md +79 -0
  601. package/src/skills/paper-plot/agents/openai.yaml +4 -0
  602. package/src/skills/paper-plot/references/bar_grouped_hatch.md +96 -0
  603. package/src/skills/paper-plot/references/bar_paired_delta.md +72 -0
  604. package/src/skills/paper-plot/references/line_confidence_band.md +75 -0
  605. package/src/skills/paper-plot/references/line_loss_with_inset.md +65 -0
  606. package/src/skills/paper-plot/references/line_training_curve.md +44 -0
  607. package/src/skills/paper-plot/references/radar_dual_series.md +59 -0
  608. package/src/skills/paper-plot/references/scatter_broken_axis.md +59 -0
  609. package/src/skills/paper-plot/references/scatter_tsne_cluster.md +72 -0
  610. package/src/skills/paper-plot/scripts/bar_memevolve.py +109 -0
  611. package/src/skills/paper-plot/scripts/bar_spice.py +166 -0
  612. package/src/skills/paper-plot/scripts/line_aime.py +94 -0
  613. package/src/skills/paper-plot/scripts/line_loss_inset.py +157 -0
  614. package/src/skills/paper-plot/scripts/line_selfdistill.py +168 -0
  615. package/src/skills/paper-plot/scripts/radar_dora.py +151 -0
  616. package/src/skills/paper-plot/scripts/scatter_break.py +169 -0
  617. package/src/skills/paper-plot/scripts/scatter_tsne.py +133 -0
  618. package/src/skills/rebuttal/SKILL.md +9 -0
  619. package/src/skills/references/tool-usage-by-stage.md +438 -0
  620. package/src/skills/review/SKILL.md +105 -7
  621. package/src/skills/science/PROVENANCE.md +44 -0
  622. package/src/skills/science/SKILL.md +137 -0
  623. package/src/skills/science/references/artifact-science-tool.md +110 -0
  624. package/src/skills/science/references/claim-type-discipline.md +56 -0
  625. package/src/skills/science/references/domain-index.md +422 -0
  626. package/src/skills/science/references/hpc-via-bash-exec.md +42 -0
  627. package/src/skills/science/references/package-check-playbook.md +64 -0
  628. package/src/skills/science/references/package-index.min.json +3616 -0
  629. package/src/skills/science/references/packages/abinit.md +80 -0
  630. package/src/skills/science/references/packages/acts.md +73 -0
  631. package/src/skills/science/references/packages/aiida-core.md +80 -0
  632. package/src/skills/science/references/packages/alamode.md +80 -0
  633. package/src/skills/science/references/packages/amuse.md +88 -0
  634. package/src/skills/science/references/packages/anndata.md +88 -0
  635. package/src/skills/science/references/packages/arbor.md +80 -0
  636. package/src/skills/science/references/packages/arc.md +73 -0
  637. package/src/skills/science/references/packages/astropy.md +88 -0
  638. package/src/skills/science/references/packages/astroquery.md +88 -0
  639. package/src/skills/science/references/packages/atomate2.md +80 -0
  640. package/src/skills/science/references/packages/atomsmltr.md +73 -0
  641. package/src/skills/science/references/packages/awkward.md +73 -0
  642. package/src/skills/science/references/packages/batman.md +88 -0
  643. package/src/skills/science/references/packages/biopython.md +88 -0
  644. package/src/skills/science/references/packages/bloqade.md +73 -0
  645. package/src/skills/science/references/packages/brian2.md +73 -0
  646. package/src/skills/science/references/packages/bullet3.md +73 -0
  647. package/src/skills/science/references/packages/calculix.md +80 -0
  648. package/src/skills/science/references/packages/cantera.md +73 -0
  649. package/src/skills/science/references/packages/cavity-md-ipi.md +80 -0
  650. package/src/skills/science/references/packages/ccdproc.md +88 -0
  651. package/src/skills/science/references/packages/celerite2.md +88 -0
  652. package/src/skills/science/references/packages/cellrank.md +73 -0
  653. package/src/skills/science/references/packages/cesm.md +80 -0
  654. package/src/skills/science/references/packages/chemicals.md +73 -0
  655. package/src/skills/science/references/packages/chempy.md +73 -0
  656. package/src/skills/science/references/packages/cirq.md +73 -0
  657. package/src/skills/science/references/packages/coffea.md +73 -0
  658. package/src/skills/science/references/packages/cp2k.md +88 -0
  659. package/src/skills/science/references/packages/custodian.md +80 -0
  660. package/src/skills/science/references/packages/dart.md +73 -0
  661. package/src/skills/science/references/packages/datamol.md +88 -0
  662. package/src/skills/science/references/packages/dd4hep.md +73 -0
  663. package/src/skills/science/references/packages/dealii.md +80 -0
  664. package/src/skills/science/references/packages/deepchem.md +88 -0
  665. package/src/skills/science/references/packages/delphes.md +73 -0
  666. package/src/skills/science/references/packages/devito.md +80 -0
  667. package/src/skills/science/references/packages/dftb.md +88 -0
  668. package/src/skills/science/references/packages/dftd4.md +88 -0
  669. package/src/skills/science/references/packages/dftk-jl.md +80 -0
  670. package/src/skills/science/references/packages/dolfinx.md +80 -0
  671. package/src/skills/science/references/packages/drake.md +73 -0
  672. package/src/skills/science/references/packages/dumux.md +73 -0
  673. package/src/skills/science/references/packages/elk.md +80 -0
  674. package/src/skills/science/references/packages/elmerfem.md +80 -0
  675. package/src/skills/science/references/packages/enzo-e.md +88 -0
  676. package/src/skills/science/references/packages/espresso.md +80 -0
  677. package/src/skills/science/references/packages/exoplanet.md +88 -0
  678. package/src/skills/science/references/packages/fairroot.md +73 -0
  679. package/src/skills/science/references/packages/fbpic.md +80 -0
  680. package/src/skills/science/references/packages/fdtdbath-meep.md +80 -0
  681. package/src/skills/science/references/packages/geant4.md +73 -0
  682. package/src/skills/science/references/packages/geosx.md +80 -0
  683. package/src/skills/science/references/packages/gprmax.md +80 -0
  684. package/src/skills/science/references/packages/gromacs.md +80 -0
  685. package/src/skills/science/references/packages/gwaslab.md +73 -0
  686. package/src/skills/science/references/packages/gz-sim.md +73 -0
  687. package/src/skills/science/references/packages/hail.md +88 -0
  688. package/src/skills/science/references/packages/hiphive.md +80 -0
  689. package/src/skills/science/references/packages/hoomd-blue.md +80 -0
  690. package/src/skills/science/references/packages/itensor.md +73 -0
  691. package/src/skills/science/references/packages/itensors-jl.md +73 -0
  692. package/src/skills/science/references/packages/jdftx.md +73 -0
  693. package/src/skills/science/references/packages/jobflow.md +80 -0
  694. package/src/skills/science/references/packages/kadanoffbaym-jl.md +73 -0
  695. package/src/skills/science/references/packages/kite.md +80 -0
  696. package/src/skills/science/references/packages/kratos.md +80 -0
  697. package/src/skills/science/references/packages/kwant.md +73 -0
  698. package/src/skills/science/references/packages/lammps.md +80 -0
  699. package/src/skills/science/references/packages/lightkurve.md +88 -0
  700. package/src/skills/science/references/packages/limix.md +73 -0
  701. package/src/skills/science/references/packages/maxwelllink.md +80 -0
  702. package/src/skills/science/references/packages/mcdc.md +73 -0
  703. package/src/skills/science/references/packages/meep.md +80 -0
  704. package/src/skills/science/references/packages/mfem.md +80 -0
  705. package/src/skills/science/references/packages/mitgcm.md +73 -0
  706. package/src/skills/science/references/packages/modflow6.md +73 -0
  707. package/src/skills/science/references/packages/molecool.md +73 -0
  708. package/src/skills/science/references/packages/mom6.md +73 -0
  709. package/src/skills/science/references/packages/moose.md +80 -0
  710. package/src/skills/science/references/packages/mpas-model.md +73 -0
  711. package/src/skills/science/references/packages/mujoco.md +73 -0
  712. package/src/skills/science/references/packages/mumax3.md +73 -0
  713. package/src/skills/science/references/packages/nekrs.md +80 -0
  714. package/src/skills/science/references/packages/nessi.md +73 -0
  715. package/src/skills/science/references/packages/nest-simulator.md +73 -0
  716. package/src/skills/science/references/packages/netket.md +73 -0
  717. package/src/skills/science/references/packages/neuron.md +73 -0
  718. package/src/skills/science/references/packages/nextflow.md +88 -0
  719. package/src/skills/science/references/packages/nwchem.md +88 -0
  720. package/src/skills/science/references/packages/openbabel.md +88 -0
  721. package/src/skills/science/references/packages/openems.md +80 -0
  722. package/src/skills/science/references/packages/openff-toolkit.md +88 -0
  723. package/src/skills/science/references/packages/openfoam-dev.md +80 -0
  724. package/src/skills/science/references/packages/openmc.md +73 -0
  725. package/src/skills/science/references/packages/openmm.md +80 -0
  726. package/src/skills/science/references/packages/openmoc.md +73 -0
  727. package/src/skills/science/references/packages/openmx.md +80 -0
  728. package/src/skills/science/references/packages/opensees.md +80 -0
  729. package/src/skills/science/references/packages/opensn.md +80 -0
  730. package/src/skills/science/references/packages/opm-simulators.md +73 -0
  731. package/src/skills/science/references/packages/oqupy.md +73 -0
  732. package/src/skills/science/references/packages/packmol.md +80 -0
  733. package/src/skills/science/references/packages/palabos.md +80 -0
  734. package/src/skills/science/references/packages/parflow.md +80 -0
  735. package/src/skills/science/references/packages/pennylane.md +88 -0
  736. package/src/skills/science/references/packages/perceval.md +73 -0
  737. package/src/skills/science/references/packages/phono3py.md +73 -0
  738. package/src/skills/science/references/packages/phonopy.md +73 -0
  739. package/src/skills/science/references/packages/photutils.md +88 -0
  740. package/src/skills/science/references/packages/picongpu.md +80 -0
  741. package/src/skills/science/references/packages/plink-ng.md +88 -0
  742. package/src/skills/science/references/packages/precice.md +73 -0
  743. package/src/skills/science/references/packages/psc.md +80 -0
  744. package/src/skills/science/references/packages/psi4.md +88 -0
  745. package/src/skills/science/references/packages/pybinding.md +73 -0
  746. package/src/skills/science/references/packages/pyfr.md +80 -0
  747. package/src/skills/science/references/packages/pyhf.md +73 -0
  748. package/src/skills/science/references/packages/pyiron_base.md +80 -0
  749. package/src/skills/science/references/packages/pylcp.md +73 -0
  750. package/src/skills/science/references/packages/pylith.md +80 -0
  751. package/src/skills/science/references/packages/pynbody.md +88 -0
  752. package/src/skills/science/references/packages/pysam.md +88 -0
  753. package/src/skills/science/references/packages/pyscf.md +88 -0
  754. package/src/skills/science/references/packages/q-e.md +73 -0
  755. package/src/skills/science/references/packages/qibo.md +73 -0
  756. package/src/skills/science/references/packages/qiskit.md +73 -0
  757. package/src/skills/science/references/packages/quantica-jl.md +73 -0
  758. package/src/skills/science/references/packages/quantumoptics-jl.md +73 -0
  759. package/src/skills/science/references/packages/quimb.md +73 -0
  760. package/src/skills/science/references/packages/qulacs.md +73 -0
  761. package/src/skills/science/references/packages/qutip.md +73 -0
  762. package/src/skills/science/references/packages/rdkit.md +88 -0
  763. package/src/skills/science/references/packages/rmg-py.md +73 -0
  764. package/src/skills/science/references/packages/root.md +73 -0
  765. package/src/skills/science/references/packages/scanpy.md +88 -0
  766. package/src/skills/science/references/packages/scikit-allel.md +88 -0
  767. package/src/skills/science/references/packages/scikit-bio.md +88 -0
  768. package/src/skills/science/references/packages/scqubits.md +73 -0
  769. package/src/skills/science/references/packages/scuff-em.md +80 -0
  770. package/src/skills/science/references/packages/scvi-tools.md +73 -0
  771. package/src/skills/science/references/packages/seissol.md +73 -0
  772. package/src/skills/science/references/packages/sfepy.md +80 -0
  773. package/src/skills/science/references/packages/sisl.md +73 -0
  774. package/src/skills/science/references/packages/smilei.md +80 -0
  775. package/src/skills/science/references/packages/snakemake.md +88 -0
  776. package/src/skills/science/references/packages/specfem3d-globe.md +80 -0
  777. package/src/skills/science/references/packages/specutils.md +88 -0
  778. package/src/skills/science/references/packages/spglib.md +80 -0
  779. package/src/skills/science/references/packages/squidpy.md +88 -0
  780. package/src/skills/science/references/packages/starry.md +88 -0
  781. package/src/skills/science/references/packages/strawberryfields.md +73 -0
  782. package/src/skills/science/references/packages/su2.md +80 -0
  783. package/src/skills/science/references/packages/sunny-jl.md +73 -0
  784. package/src/skills/science/references/packages/sw4.md +73 -0
  785. package/src/skills/science/references/packages/swift.md +88 -0
  786. package/src/skills/science/references/packages/tdnegf.md +73 -0
  787. package/src/skills/science/references/packages/tenpy.md +73 -0
  788. package/src/skills/science/references/packages/thermo.md +73 -0
  789. package/src/skills/science/references/packages/tkwant.md +73 -0
  790. package/src/skills/science/references/packages/tvb-root.md +73 -0
  791. package/src/skills/science/references/packages/uproot5.md +73 -0
  792. package/src/skills/science/references/packages/vampire.md +80 -0
  793. package/src/skills/science/references/packages/wannier_tools.md +73 -0
  794. package/src/skills/science/references/packages/warpx.md +80 -0
  795. package/src/skills/science/references/packages/wrf.md +73 -0
  796. package/src/skills/science/references/packages/xtb.md +88 -0
  797. package/src/skills/science/references/packages/yt.md +73 -0
  798. package/src/skills/science/references/science-task-brief-template.md +71 -0
  799. package/src/skills/scout/SKILL.md +83 -425
  800. package/src/skills/scout/references/literature-scout-template.md +5 -24
  801. package/src/skills/scout/references/operational-guidance.md +191 -0
  802. package/src/skills/scout/references/paper-triage-playbook.md +11 -35
  803. package/src/skills/write/SKILL.md +744 -1246
  804. package/src/skills/write/references/experiments_analysis_patterns.md +129 -0
  805. package/src/skills/write/references/oral_package_patterns.md +252 -0
  806. package/src/skills/write/references/oral_writing_principles.md +291 -0
  807. package/src/skills/write/references/section_rewrite_checklist.md +234 -0
  808. package/src/tui/dist/app/AppContainer.js +1314 -27
  809. package/src/tui/dist/components/Composer.js +26 -1
  810. package/src/tui/dist/components/ConfigScreen.js +2 -1
  811. package/src/tui/dist/components/InputPrompt.js +25 -9
  812. package/src/tui/dist/components/MainContent.js +18 -3
  813. package/src/tui/dist/components/QuestScreen.js +3 -2
  814. package/src/tui/dist/components/UtilityScreen.js +37 -0
  815. package/src/tui/dist/hooks/useSafeInput.js +10 -0
  816. package/src/tui/dist/index.js +13 -1
  817. package/src/tui/dist/layouts/DefaultAppLayout.js +11 -8
  818. package/src/tui/dist/lib/api.js +89 -1
  819. package/src/tui/package.json +1 -1
  820. package/src/ui/dist/assets/{AnalysisPlugin-DnSm0GZn.js → AnalysisPlugin-CA94NGmI.js} +1 -1
  821. package/src/ui/dist/assets/CliPlugin-DHBzphZU.js +79 -0
  822. package/src/ui/dist/assets/CodeEditorPlugin-BOFwD2rn.js +2 -0
  823. package/src/ui/dist/assets/{CodeViewerPlugin-itb0tltR.js → CodeViewerPlugin-CqDpgjik.js} +4 -4
  824. package/src/ui/dist/assets/{DocViewerPlugin-DqKkiCI6.js → DocViewerPlugin-UDBgt8-4.js} +3 -3
  825. package/src/ui/dist/assets/GitCommitViewerPlugin-BmHtZ0bZ.js +6 -0
  826. package/src/ui/dist/assets/{GitDiffViewerPlugin-DxL2ezFG.js → GitDiffViewerPlugin-CAxjNorQ.js} +2 -2
  827. package/src/ui/dist/assets/{GitSnapshotViewer-B_RQm1YZ.js → GitSnapshotViewer-CweA6VON.js} +2 -2
  828. package/src/ui/dist/assets/{ImageViewerPlugin-tHqlXY3n.js → ImageViewerPlugin-C8wHGvGN.js} +5 -5
  829. package/src/ui/dist/assets/LabPlugin-COyyLUol.js +32 -0
  830. package/src/ui/dist/assets/{LatexPlugin-B495DTXC.js → LatexPlugin-BQjAaA5J.js} +4 -4
  831. package/src/ui/dist/assets/{MarkdownViewerPlugin-DG28-61B.js → MarkdownViewerPlugin-Dy1NE2dI.js} +3 -3
  832. package/src/ui/dist/assets/{MarketplacePlugin-BiOGT-Kj.js → MarketplacePlugin-DMIZtEJ2.js} +2 -2
  833. package/src/ui/dist/assets/NotebookEditor-CFHMq_Qt.js +91 -0
  834. package/src/ui/dist/assets/{NotebookEditor-CVsj8h_T.js → NotebookEditor-WFyd8Ybt.js} +23 -23
  835. package/src/ui/dist/assets/{PdfLoader-CASDQmxJ.js → PdfLoader-CLE5u5TS.js} +3 -3
  836. package/src/ui/dist/assets/{PdfMarkdownPlugin-BFhwoKsY.js → PdfMarkdownPlugin-_iNK_H83.js} +1 -1
  837. package/src/ui/dist/assets/PdfViewerPlugin-DgWsbInT.js +22 -0
  838. package/src/ui/dist/assets/SearchPlugin-DrZmn5iw.js +11 -0
  839. package/src/ui/dist/assets/{TextViewerPlugin-CB4DYfWO.js → TextViewerPlugin-D1-T3aC7.js} +4 -4
  840. package/src/ui/dist/assets/branding/runner-claude.svg +107 -0
  841. package/src/ui/dist/assets/branding/runner-codex.svg +10 -0
  842. package/src/ui/dist/assets/branding/runner-kimi.svg +14 -0
  843. package/src/ui/dist/assets/branding/runner-opencode.svg +7 -0
  844. package/src/ui/dist/assets/cli-store-CoZ-x5Ip.js +1 -0
  845. package/src/ui/dist/assets/{code-DLC6G24T.js → code-DbsmSd3Y.js} +1 -1
  846. package/src/ui/dist/assets/file-diff-panel-DsvyRz47.js +1 -0
  847. package/src/ui/dist/assets/{wrap-text-CwMn-iqb.js → file-jump-queue-DeQBikaw.js} +3 -3
  848. package/src/ui/dist/assets/{file-socket-Cu4Qln7Y.js → file-socket-DA5XIx88.js} +1 -1
  849. package/src/ui/dist/assets/fonts/ds-fonts.css +50 -4
  850. package/src/ui/dist/assets/images/deepxiv/register-guide.png +0 -0
  851. package/src/ui/dist/assets/index-39vY9LmZ.js +1 -0
  852. package/src/ui/dist/assets/{index-wQ7RIIRd.js → index-BsO46tJA.js} +1 -1
  853. package/src/ui/dist/assets/index-CHzJ2xtB.js +3530 -0
  854. package/src/ui/dist/assets/index-DH-zxoZ3.css +33 -0
  855. package/src/ui/dist/assets/{plugin-notebook-HbW2K-1c.js → plugin-notebook-JRhysCqj.js} +2 -2
  856. package/src/ui/dist/assets/{project-sync-CsX08Qno.js → project-sync-DPmWKmKD.js} +1 -1
  857. package/src/ui/dist/assets/{zoom-out-R-GWEhzS.js → zoom-out-DAukFWen.js} +3 -3
  858. package/src/ui/dist/index.html +3 -3
  859. package/src/skills/analysis-campaign/references/artifact-orchestration.md +0 -58
  860. package/src/skills/baseline/references/memory-playbook.md +0 -40
  861. package/src/skills/baseline/references/publishable-baseline-package.md +0 -30
  862. package/src/skills/write/references/outline-evidence-contract-example.md +0 -107
  863. package/src/skills/write/references/paper-experiment-matrix-template.md +0 -131
  864. package/src/skills/write/references/paper-section-playbook.md +0 -64
  865. package/src/skills/write/references/reviewer-first-writing.md +0 -64
  866. package/src/skills/write/references/revision-checklist.md +0 -70
  867. package/src/skills/write/references/section-contracts.md +0 -82
  868. package/src/skills/write/references/sentence-level-proofing.md +0 -49
  869. package/src/ui/dist/assets/AiManusChatView-COFACy7V.js +0 -204
  870. package/src/ui/dist/assets/CliPlugin-CvwCmDQ5.js +0 -109
  871. package/src/ui/dist/assets/CodeEditorPlugin-cOqSa0xq.js +0 -2
  872. package/src/ui/dist/assets/GitCommitViewerPlugin-DVgNHBCS.js +0 -1
  873. package/src/ui/dist/assets/LabCopilotPanel-ClMbq5Yu.js +0 -14
  874. package/src/ui/dist/assets/LabPlugin-L_SuE8ow.js +0 -22
  875. package/src/ui/dist/assets/NotebookEditor-C-4Kt1p9.js +0 -81
  876. package/src/ui/dist/assets/PdfViewerPlugin-DcOzU9vd.js +0 -17
  877. package/src/ui/dist/assets/SearchPlugin-CHj7M58O.js +0 -16
  878. package/src/ui/dist/assets/VNCViewer-CjlbyCB3.js +0 -11
  879. package/src/ui/dist/assets/bot-CFkZY-JP.js +0 -6
  880. package/src/ui/dist/assets/chevron-up-Dq5ofbht.js +0 -6
  881. package/src/ui/dist/assets/file-content-Dv4LoZec.js +0 -1
  882. package/src/ui/dist/assets/file-diff-panel-Denq-lC3.js +0 -1
  883. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +0 -1
  884. package/src/ui/dist/assets/git-commit-horizontal-BUh6G52n.js +0 -6
  885. package/src/ui/dist/assets/image-B9HUUddG.js +0 -6
  886. package/src/ui/dist/assets/index-B2B1sg-M.js +0 -1
  887. package/src/ui/dist/assets/index-Cgla8biy.css +0 -33
  888. package/src/ui/dist/assets/index-DRyx7vAc.js +0 -1
  889. package/src/ui/dist/assets/index-Gbl53BNp.js +0 -2496
  890. package/src/ui/dist/assets/pdf-effect-queue-ZtnHFCAi.js +0 -6
  891. package/src/ui/dist/assets/popover-DL6h35vr.js +0 -1
  892. package/src/ui/dist/assets/select-DvmXt1yY.js +0 -11
  893. package/src/ui/dist/assets/sigma-7jpXazui.js +0 -6
  894. package/src/ui/dist/assets/trash-xA7kFt8i.js +0 -11
  895. package/src/ui/dist/assets/useCliAccess-DsMwDjOp.js +0 -1
  896. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +0 -1
@@ -3,10 +3,12 @@ from __future__ import annotations
3
3
  import base64
4
4
  from collections import deque
5
5
  from dataclasses import dataclass
6
+ import errno
6
7
  import faulthandler
7
8
  import hashlib
8
9
  import hmac
9
10
  import json
11
+ import logging
10
12
  import mimetypes
11
13
  import os
12
14
  import re
@@ -28,8 +30,12 @@ from urllib.request import Request
28
30
 
29
31
  from .. import __version__
30
32
  from ..annotations import AnnotationService
33
+ from ..admin import AdminTaskService
34
+ from ..admin.repairs import AdminRepairService
35
+ from ..admin.service import AdminService
31
36
  from ..acp import build_session_update
32
37
  from ..artifact import ArtifactService
38
+ from ..benchstore import BenchStoreService
33
39
  from ..bash_exec import BashExecService
34
40
  from ..bash_exec.models import TerminalClient
35
41
  from ..bash_exec.service import DEFAULT_TERMINAL_SESSION_ID
@@ -55,6 +61,7 @@ from ..connector.connector_profiles import (
55
61
  from ..connector_runtime import conversation_identity_key, format_conversation_id, normalize_conversation_id, parse_conversation_id
56
62
  from ..config import ConfigManager
57
63
  from ..config.models import SYSTEM_CONNECTOR_NAMES
64
+ from ..diagnostics import FailureDiagnosis, diagnose_runner_failure
58
65
  from ..home import repo_root
59
66
  from ..memory import MemoryService
60
67
  from ..network import urlopen_with_proxy as urlopen
@@ -77,10 +84,11 @@ from ..connector.lingzhu_support import (
77
84
  from ..prompts import PromptBuilder
78
85
  from ..prompts.builder import classify_turn_intent, current_standard_skills
79
86
  from ..connector.qq_profiles import list_qq_profiles, merge_qq_profile_config, normalize_qq_connector_config
80
- from ..quest import QuestService
81
- from ..runners import CodexRunner, RunRequest, get_runner_factory, register_builtin_runners
87
+ from ..quest import AUTONOMOUS_BLOCKING_WAIT_REASONS, QuestService
88
+ from ..runners import ClaudeRunner, CodexRunner, KimiRunner, OpenCodeRunner, RunRequest, get_runner_factory, register_builtin_runners
89
+ from ..runners.metadata import get_runner_metadata
82
90
  from ..runtime_logs import JsonlLogger
83
- from ..shared import append_jsonl, ensure_dir, generate_id, iter_jsonl, read_json, read_jsonl, read_jsonl_tail, read_text, resolve_within, run_command, slugify, utc_now, which, write_json
91
+ from ..shared import append_jsonl, ensure_dir, generate_id, iter_jsonl, read_json, read_jsonl, read_jsonl_tail, read_text, resolve_within, run_command, slugify, utc_now, utf8_text_subprocess_kwargs, which, write_json
84
92
  from ..skills import SkillInstaller
85
93
  from ..team import SingleTeamService
86
94
  from ..connector.weixin_support import (
@@ -105,7 +113,12 @@ TERMINAL_STREAM_IDLE_SLEEP_SECONDS = 0.02
105
113
  _AUTO_CONTINUE_DELAY_SECONDS = 240.0
106
114
  _AUTO_CONTINUE_ACTIVE_WORK_DELAY_SECONDS = 0.2
107
115
  _TERMINAL_PREWARM_DEBOUNCE_SECONDS = 20.0
108
- CODEX_RETRY_DEFAULT_MAX_ATTEMPTS = 5
116
+ _STALLED_RUNNING_TURN_INACTIVITY_SECONDS = 30 * 60
117
+ _STALLED_RUNNING_TURN_INTERRUPT_TIMEOUT_SECONDS = 5.0
118
+ _IMMEDIATE_READ_INTERRUPT_TIMEOUT_SECONDS = 5.0
119
+ _RESUME_CONTINUE_TEXT = "Continue"
120
+ CODEX_RETRY_DEFAULT_MAX_ATTEMPTS = 7
121
+ PREVIOUS_CODEX_RETRY_DEFAULT_MAX_ATTEMPTS = 5
109
122
  CODEX_RETRY_DEFAULT_INITIAL_BACKOFF_SEC = 10.0
110
123
  CODEX_RETRY_DEFAULT_BACKOFF_MULTIPLIER = 6.0
111
124
  CODEX_RETRY_DEFAULT_MAX_BACKOFF_SEC = 1800.0
@@ -199,6 +212,7 @@ class DaemonApp:
199
212
  self.memory_service = MemoryService(home)
200
213
  self.annotation_service = AnnotationService(home)
201
214
  self.artifact_service = ArtifactService(home)
215
+ self.benchstore_service = BenchStoreService(home, repo_root=self.repo_root)
202
216
  self.bash_exec_service = BashExecService(home)
203
217
  self.team_service = SingleTeamService(home)
204
218
  self.cloud_service = CloudLinkService(home)
@@ -210,6 +224,16 @@ class DaemonApp:
210
224
  sync_existing_quests_enabled=bool(skill_config.get("sync_quest_on_open", True)),
211
225
  )
212
226
  self.logger = JsonlLogger(home / "logs", level=config.get("logging", {}).get("level", "info"))
227
+ self.admin_task_service = AdminTaskService(
228
+ home,
229
+ repo_root=self.repo_root,
230
+ logger=self.logger,
231
+ system_update_status_fn=self.system_update_status,
232
+ system_update_action_fn=self.request_system_update,
233
+ )
234
+ self.admin_task_service.logger = self.logger
235
+ self.admin_repair_service = AdminRepairService(self)
236
+ self.admin_service = AdminService(self)
213
237
  self.reconciled_quests = self.quest_service.reconcile_runtime_state()
214
238
  for item in self.reconciled_quests:
215
239
  self.logger.log(
@@ -233,11 +257,41 @@ class DaemonApp:
233
257
  prompt_builder=self.prompt_builder,
234
258
  artifact_service=self.artifact_service,
235
259
  )
236
- register_builtin_runners(codex_runner=self.codex_runner)
260
+ self.claude_runner = ClaudeRunner(
261
+ home=home,
262
+ repo_root=self.repo_root,
263
+ binary=self.runners_config.get("claude", {}).get("binary", "claude"),
264
+ logger=self.logger,
265
+ prompt_builder=self.prompt_builder,
266
+ artifact_service=self.artifact_service,
267
+ )
268
+ self.kimi_runner = KimiRunner(
269
+ home=home,
270
+ repo_root=self.repo_root,
271
+ binary=self.runners_config.get("kimi", {}).get("binary", "kimi"),
272
+ logger=self.logger,
273
+ prompt_builder=self.prompt_builder,
274
+ artifact_service=self.artifact_service,
275
+ )
276
+ self.opencode_runner = OpenCodeRunner(
277
+ home=home,
278
+ repo_root=self.repo_root,
279
+ binary=self.runners_config.get("opencode", {}).get("binary", "opencode"),
280
+ logger=self.logger,
281
+ prompt_builder=self.prompt_builder,
282
+ artifact_service=self.artifact_service,
283
+ )
284
+ register_builtin_runners(
285
+ codex_runner=self.codex_runner,
286
+ claude_runner=self.claude_runner,
287
+ kimi_runner=self.kimi_runner,
288
+ opencode_runner=self.opencode_runner,
289
+ )
237
290
  register_builtin_connector_bridges()
238
291
  register_builtin_channels(home=home, connectors_config=self.connectors_config)
239
292
  self.runners = {
240
- "codex": self._create_runner("codex"),
293
+ name: self._create_runner(name)
294
+ for name in ("codex", "claude", "kimi", "opencode")
241
295
  }
242
296
  self.channels = {name: self._create_channel(name) for name in list_channel_names()}
243
297
  self.sessions = SessionStore()
@@ -755,7 +809,23 @@ class DaemonApp:
755
809
  if int(snapshot.get("pending_user_message_count") or 0) > 0
756
810
  else "auto_continue"
757
811
  )
758
- scheduled = self.schedule_turn(quest_id, reason=reason)
812
+ retry_delay_seconds = self._recovery_retry_delay_seconds(snapshot) if reason == "auto_continue" else None
813
+ if retry_delay_seconds is not None and retry_delay_seconds > 0:
814
+ self._schedule_turn_later(
815
+ quest_id,
816
+ reason=reason,
817
+ delay_seconds=retry_delay_seconds,
818
+ )
819
+ scheduled = {
820
+ "scheduled": True,
821
+ "started": False,
822
+ "queued": True,
823
+ "reason": reason,
824
+ "delayed": True,
825
+ "delay_seconds": retry_delay_seconds,
826
+ }
827
+ else:
828
+ scheduled = self.schedule_turn(quest_id, reason=reason)
759
829
  event = {
760
830
  "event_id": generate_id("evt"),
761
831
  "type": "quest.runtime_auto_resumed",
@@ -767,6 +837,8 @@ class DaemonApp:
767
837
  "scheduled": bool(scheduled.get("scheduled")),
768
838
  "started": bool(scheduled.get("started")),
769
839
  "queued": bool(scheduled.get("queued")),
840
+ "delayed": bool(scheduled.get("delayed")),
841
+ "delay_seconds": scheduled.get("delay_seconds"),
770
842
  "created_at": utc_now(),
771
843
  }
772
844
  append_jsonl(self.home / "quests" / quest_id / ".ds" / "events.jsonl", event)
@@ -781,6 +853,8 @@ class DaemonApp:
781
853
  scheduled=bool(scheduled.get("scheduled")),
782
854
  started=bool(scheduled.get("started")),
783
855
  queued=bool(scheduled.get("queued")),
856
+ delayed=bool(scheduled.get("delayed")),
857
+ delay_seconds=scheduled.get("delay_seconds"),
784
858
  )
785
859
  self._recovered_quest_ids.add(quest_id)
786
860
  resumed.append(
@@ -834,6 +908,63 @@ class DaemonApp:
834
908
  count += 1
835
909
  return count
836
910
 
911
+ def _recovery_retry_delay_seconds(self, snapshot: dict[str, Any]) -> float | None:
912
+ retry_state = snapshot.get("retry_state") if isinstance(snapshot.get("retry_state"), dict) else None
913
+ if not retry_state:
914
+ return None
915
+ next_retry_at = str(retry_state.get("next_retry_at") or "").strip()
916
+ if not next_retry_at:
917
+ return None
918
+ parsed = self._parse_event_timestamp(next_retry_at)
919
+ if parsed is None:
920
+ return None
921
+ return max((parsed - datetime.now(UTC)).total_seconds(), 0.0)
922
+
923
+ def _resume_retry_state(
924
+ self,
925
+ snapshot: dict[str, Any],
926
+ *,
927
+ max_attempts: int,
928
+ ) -> tuple[int, str | None, dict[str, Any] | None]:
929
+ retry_state = snapshot.get("retry_state") if isinstance(snapshot.get("retry_state"), dict) else None
930
+ resume_source = str(snapshot.get("last_resume_source") or "").strip()
931
+ if not retry_state or not resume_source.startswith("auto:daemon-recovery"):
932
+ return 1, None, None
933
+
934
+ try:
935
+ recorded_attempt = int(retry_state.get("attempt_index") or 0)
936
+ except (TypeError, ValueError):
937
+ recorded_attempt = 0
938
+ if recorded_attempt <= 0:
939
+ return 1, None, None
940
+
941
+ next_retry_at = str(retry_state.get("next_retry_at") or "").strip()
942
+ start_attempt = recorded_attempt + 1 if next_retry_at else recorded_attempt
943
+ if start_attempt > max_attempts:
944
+ start_attempt = max_attempts
945
+ if start_attempt <= 1:
946
+ return 1, None, None
947
+
948
+ turn_id = str(retry_state.get("turn_id") or "").strip() or None
949
+ previous_run_id = str(retry_state.get("last_run_id") or "").strip() or None
950
+ failure_summary = str(retry_state.get("last_error") or "").strip() or None
951
+ retry_context = {
952
+ "turn_id": turn_id,
953
+ "attempt_index": recorded_attempt,
954
+ "max_attempts": max_attempts,
955
+ "previous_run_id": previous_run_id,
956
+ "failure_kind": "daemon_recovery",
957
+ "failure_summary": failure_summary or "Recovered retry state after daemon restart.",
958
+ "previous_exit_code": None,
959
+ "previous_output_text": "",
960
+ "stderr_tail": "",
961
+ "recent_messages": [],
962
+ "tool_progress": [],
963
+ "workspace_summary": {},
964
+ "recent_artifacts": [],
965
+ }
966
+ return start_attempt, turn_id, retry_context
967
+
837
968
  def _record_auto_resume_suppressed(
838
969
  self,
839
970
  *,
@@ -948,10 +1079,10 @@ class DaemonApp:
948
1079
  command,
949
1080
  cwd=str(self.repo_root),
950
1081
  capture_output=True,
951
- text=True,
952
1082
  timeout=8,
953
1083
  check=False,
954
1084
  env=os.environ.copy(),
1085
+ **utf8_text_subprocess_kwargs(),
955
1086
  **_windows_hidden_subprocess_kwargs(),
956
1087
  )
957
1088
  except subprocess.TimeoutExpired as exc:
@@ -996,10 +1127,10 @@ class DaemonApp:
996
1127
  command,
997
1128
  cwd=str(self.repo_root),
998
1129
  capture_output=True,
999
- text=True,
1000
1130
  timeout=8,
1001
1131
  check=False,
1002
1132
  env=os.environ.copy(),
1133
+ **utf8_text_subprocess_kwargs(),
1003
1134
  **_windows_hidden_subprocess_kwargs(),
1004
1135
  )
1005
1136
  except subprocess.TimeoutExpired as exc:
@@ -1014,6 +1145,24 @@ class DaemonApp:
1014
1145
  raise RuntimeError("DeepScientist update request returned an invalid payload.")
1015
1146
  return payload
1016
1147
 
1148
+ def start_benchstore_install(self, entry_id: str) -> dict[str, Any]:
1149
+ entry_payload = self.benchstore_service.get_entry(entry_id)
1150
+ entry = entry_payload.get("entry") if isinstance(entry_payload.get("entry"), dict) else {}
1151
+ normalized_id = str(entry.get("id") or entry_id).strip() or str(entry_id).strip()
1152
+ entry_name = str(entry.get("name") or normalized_id).strip() or normalized_id
1153
+ return self.admin_task_service.start_task(
1154
+ "benchstore_install",
1155
+ metadata={
1156
+ "entry_id": normalized_id,
1157
+ "entry_name": entry_name,
1158
+ },
1159
+ target=lambda reporter, current_task: self.benchstore_service.run_install_task(
1160
+ entry_id=normalized_id,
1161
+ reporter=reporter,
1162
+ task_id=str(current_task.get("task_id") or ""),
1163
+ ),
1164
+ )
1165
+
1017
1166
  def _process_terminal_attach_request(
1018
1167
  self,
1019
1168
  connection: ServerConnection,
@@ -1358,6 +1507,9 @@ class DaemonApp:
1358
1507
  def _start_terminal_attach_server(self, host: str, port: int) -> None:
1359
1508
  if self._terminal_attach_server is not None:
1360
1509
  return
1510
+ terminal_ws_logger = logging.getLogger("deepscientist.terminal_attach.websocket")
1511
+ terminal_ws_logger.setLevel(logging.CRITICAL + 1)
1512
+ terminal_ws_logger.propagate = False
1361
1513
  candidates: list[int] = []
1362
1514
  if port > 0 and port < 65535:
1363
1515
  candidates.append(port + 1)
@@ -1373,6 +1525,7 @@ class DaemonApp:
1373
1525
  compression=None,
1374
1526
  max_size=None,
1375
1527
  max_queue=None,
1528
+ logger=terminal_ws_logger,
1376
1529
  )
1377
1530
  self._terminal_attach_server = server
1378
1531
  self._terminal_attach_host = host
@@ -1486,19 +1639,31 @@ class DaemonApp:
1486
1639
 
1487
1640
  def reload_runners_config(self) -> dict[str, object]:
1488
1641
  self.runners_config = self.config_manager.load_runners_config()
1489
- codex_config = self.runners_config.get("codex", {})
1490
- if isinstance(codex_config, dict):
1491
- self.codex_runner.binary = str(codex_config.get("binary") or "codex")
1492
- return {
1642
+ runner_instances = {
1643
+ "codex": self.codex_runner,
1644
+ "claude": self.claude_runner,
1645
+ "kimi": self.kimi_runner,
1646
+ "opencode": self.opencode_runner,
1647
+ }
1648
+ payload: dict[str, object] = {
1493
1649
  "ok": True,
1494
1650
  "runners": sorted(name for name, config in self.runners_config.items() if isinstance(config, dict)),
1495
- "codex": {
1496
- "binary": self.codex_runner.binary,
1497
- "approval_policy": codex_config.get("approval_policy") if isinstance(codex_config, dict) else None,
1498
- "sandbox_mode": codex_config.get("sandbox_mode") if isinstance(codex_config, dict) else None,
1499
- "mcp_tool_timeout_sec": codex_config.get("mcp_tool_timeout_sec") if isinstance(codex_config, dict) else None,
1500
- },
1501
1651
  }
1652
+ for runner_name, instance in runner_instances.items():
1653
+ runner_config = self.runners_config.get(runner_name, {})
1654
+ if isinstance(runner_config, dict):
1655
+ instance.binary = str(runner_config.get("binary") or getattr(instance, "binary", runner_name))
1656
+ payload[runner_name] = {
1657
+ "binary": getattr(instance, "binary", None),
1658
+ "enabled": runner_config.get("enabled") if isinstance(runner_config, dict) else None,
1659
+ "model": runner_config.get("model") if isinstance(runner_config, dict) else None,
1660
+ "config_dir": runner_config.get("config_dir") if isinstance(runner_config, dict) else None,
1661
+ }
1662
+ if runner_name == "codex" and isinstance(runner_config, dict):
1663
+ payload[runner_name]["approval_policy"] = runner_config.get("approval_policy")
1664
+ payload[runner_name]["sandbox_mode"] = runner_config.get("sandbox_mode")
1665
+ payload[runner_name]["mcp_tool_timeout_sec"] = runner_config.get("mcp_tool_timeout_sec")
1666
+ return payload
1502
1667
 
1503
1668
  def _preferred_locale(self) -> str:
1504
1669
  return str(self.runtime_config.get("default_locale") or "en-US").lower()
@@ -1506,6 +1671,14 @@ class DaemonApp:
1506
1671
  def _polite_copy(self, *, zh: str, en: str) -> str:
1507
1672
  return zh if self._preferred_locale().startswith("zh") else en
1508
1673
 
1674
+ @staticmethod
1675
+ def _resume_continue_source(source: str) -> str:
1676
+ normalized = normalize_conversation_id(source)
1677
+ parsed = parse_conversation_id(normalized)
1678
+ if parsed is not None and str(parsed.get("connector") or "").strip().lower() != "local":
1679
+ return normalized
1680
+ return "local"
1681
+
1509
1682
  def submit_user_message(
1510
1683
  self,
1511
1684
  quest_id: str,
@@ -1543,7 +1716,36 @@ class DaemonApp:
1543
1716
  )
1544
1717
  turn_state = self._refresh_turn_worker_state(quest_id)
1545
1718
  has_live_turn = bool(turn_state.get("running"))
1546
- if runtime_status == "running" and has_live_turn:
1719
+ retry_wait_details = self._retry_waiting_turn_details(
1720
+ quest_id,
1721
+ snapshot=snapshot,
1722
+ turn_state=turn_state,
1723
+ turn_reason="user_message",
1724
+ )
1725
+ stalled_details = self._stalled_running_turn_details(
1726
+ quest_id,
1727
+ snapshot=snapshot,
1728
+ turn_state=turn_state,
1729
+ turn_reason="user_message",
1730
+ )
1731
+ if retry_wait_details is not None:
1732
+ restart = self._interrupt_retry_wait_for_new_user_message(
1733
+ quest_id,
1734
+ snapshot=snapshot,
1735
+ turn_state=turn_state,
1736
+ details=retry_wait_details,
1737
+ source=source,
1738
+ )
1739
+ if restart is not None:
1740
+ scheduled = restart
1741
+ else:
1742
+ scheduled = {
1743
+ "scheduled": True,
1744
+ "started": False,
1745
+ "queued": True,
1746
+ "reason": "queued_for_artifact_interact",
1747
+ }
1748
+ elif runtime_status == "running" and has_live_turn and stalled_details is None:
1547
1749
  scheduled = {
1548
1750
  "scheduled": True,
1549
1751
  "started": False,
@@ -1559,6 +1761,151 @@ class DaemonApp:
1559
1761
  **scheduled,
1560
1762
  }
1561
1763
 
1764
+ def _retry_waiting_turn_details(
1765
+ self,
1766
+ quest_id: str,
1767
+ *,
1768
+ snapshot: dict | None,
1769
+ turn_state: dict[str, object] | None,
1770
+ turn_reason: str,
1771
+ ) -> dict[str, Any] | None:
1772
+ if str(turn_reason or "").strip() != "user_message":
1773
+ return None
1774
+ snapshot = dict(snapshot or self.quest_service.snapshot(quest_id))
1775
+ runtime_status = str(snapshot.get("runtime_status") or snapshot.get("status") or "").strip().lower()
1776
+ if runtime_status != "running":
1777
+ return None
1778
+ state = dict(turn_state or self._refresh_turn_worker_state(quest_id))
1779
+ if not state.get("running"):
1780
+ return None
1781
+ retry_state = snapshot.get("retry_state") if isinstance(snapshot.get("retry_state"), dict) else None
1782
+ if not retry_state:
1783
+ return None
1784
+ next_retry_at = str(retry_state.get("next_retry_at") or "").strip()
1785
+ if not next_retry_at:
1786
+ return None
1787
+ if str(snapshot.get("active_run_id") or "").strip():
1788
+ return None
1789
+ pending_user_count = int(snapshot.get("pending_user_message_count") or 0)
1790
+ if pending_user_count <= 0:
1791
+ return None
1792
+ try:
1793
+ attempt_index = int(retry_state.get("attempt_index") or 0)
1794
+ except (TypeError, ValueError):
1795
+ attempt_index = 0
1796
+ try:
1797
+ max_attempts = int(retry_state.get("max_attempts") or 0)
1798
+ except (TypeError, ValueError):
1799
+ max_attempts = 0
1800
+ delay_seconds = self._recovery_retry_delay_seconds(snapshot) or 0.0
1801
+ return {
1802
+ "retry_state": dict(retry_state),
1803
+ "attempt_index": attempt_index,
1804
+ "max_attempts": max_attempts,
1805
+ "pending_user_count": pending_user_count,
1806
+ "delay_seconds": max(0.0, float(delay_seconds)),
1807
+ }
1808
+
1809
+ def _interrupt_retry_wait_for_new_user_message(
1810
+ self,
1811
+ quest_id: str,
1812
+ *,
1813
+ snapshot: dict[str, Any],
1814
+ turn_state: dict[str, object],
1815
+ details: dict[str, Any],
1816
+ source: str,
1817
+ ) -> dict[str, Any] | None:
1818
+ try:
1819
+ restart = self._quiet_interrupt_for_turn_restart(quest_id, source=source)
1820
+ except RuntimeError as exc:
1821
+ self.logger.log(
1822
+ "warning",
1823
+ "runner.turn_retry_interrupt_failed",
1824
+ quest_id=quest_id,
1825
+ source=source,
1826
+ error=str(exc),
1827
+ pending_user_message_count=int(details.get("pending_user_count") or 0),
1828
+ )
1829
+ return None
1830
+
1831
+ retry_state = details.get("retry_state") if isinstance(details.get("retry_state"), dict) else {}
1832
+ turn_id = str(retry_state.get("turn_id") or "").strip() or None
1833
+ previous_run_id = str(retry_state.get("last_run_id") or "").strip() or None
1834
+ attempt_index = max(0, int(details.get("attempt_index") or 0))
1835
+ max_attempts = max(0, int(details.get("max_attempts") or 0))
1836
+ pending_user_count = max(0, int(details.get("pending_user_count") or 0))
1837
+ delay_seconds = max(0.0, float(details.get("delay_seconds") or 0.0))
1838
+ runner_name = self._runner_name_for(snapshot)
1839
+ skill_id = self._turn_skill_for(snapshot, None, turn_reason="user_message", turn_mode=self._turn_mode_for(snapshot, None, turn_reason="user_message"))
1840
+ model = str((self.runners_config.get(runner_name) or {}).get("model", "gpt-5.4"))
1841
+ summary = (
1842
+ f"Retry wait aborted after attempt {attempt_index}/{max_attempts or attempt_index or 1} "
1843
+ f"because a newer user message arrived. "
1844
+ f"Pending queued user messages: {pending_user_count}. "
1845
+ f"Skipped remaining backoff wait: {delay_seconds:.1f}s."
1846
+ )
1847
+ self.quest_service.update_runtime_state(
1848
+ quest_root=self.quest_service._quest_root(quest_id),
1849
+ retry_state=None,
1850
+ )
1851
+ self._append_retry_event(
1852
+ quest_id,
1853
+ event_type="runner.turn_retry_aborted",
1854
+ runner_name=runner_name,
1855
+ run_id=previous_run_id,
1856
+ turn_id=turn_id,
1857
+ skill_id=skill_id,
1858
+ model=model,
1859
+ attempt_index=attempt_index,
1860
+ max_attempts=max_attempts,
1861
+ summary=summary,
1862
+ failure_summary=str(retry_state.get("last_error") or "").strip() or None,
1863
+ previous_run_id=previous_run_id,
1864
+ )
1865
+ self._relay_quest_message_to_bound_connectors(
1866
+ quest_id,
1867
+ message=self._polite_copy(
1868
+ zh="我收到新的用户消息,已中断上一轮等待中的自动重试,先优先处理最新要求。",
1869
+ en="A new user message arrived, so I interrupted the pending retry wait and will handle the latest request first.",
1870
+ ),
1871
+ kind="system",
1872
+ response_phase="update",
1873
+ importance="normal",
1874
+ attachments=[
1875
+ {
1876
+ "kind": "retry_interrupted",
1877
+ "run_id": previous_run_id,
1878
+ "runner": runner_name,
1879
+ "turn_id": turn_id,
1880
+ }
1881
+ ],
1882
+ )
1883
+ return self.schedule_turn(quest_id, reason="user_message")
1884
+
1885
+ def submit_web_user_message(
1886
+ self,
1887
+ quest_id: str,
1888
+ *,
1889
+ text: str,
1890
+ source: str,
1891
+ attachment_draft_ids: list[str],
1892
+ reply_to_interaction_id: str | None = None,
1893
+ client_message_id: str | None = None,
1894
+ ) -> dict:
1895
+ finalized_attachments = self.quest_service.finalize_chat_attachment_drafts(
1896
+ quest_id,
1897
+ draft_ids=attachment_draft_ids,
1898
+ client_message_id=client_message_id,
1899
+ )
1900
+ return self.submit_user_message(
1901
+ quest_id,
1902
+ text=text,
1903
+ source=source,
1904
+ attachments=finalized_attachments,
1905
+ reply_to_interaction_id=reply_to_interaction_id,
1906
+ client_message_id=client_message_id,
1907
+ )
1908
+
1562
1909
  def create_quest(
1563
1910
  self,
1564
1911
  *,
@@ -1578,10 +1925,13 @@ class DaemonApp:
1578
1925
  normalized_requested_bindings = self._normalize_requested_connector_bindings(requested_connector_bindings)
1579
1926
  if len(normalized_requested_bindings) > 1:
1580
1927
  raise ValueError("A quest may bind at most one external connector target.")
1928
+ configured = self.config_manager.load_named("config")
1929
+ default_runner = str(configured.get("default_runner") or "codex").strip().lower() or "codex"
1581
1930
  snapshot = self.quest_service.create(
1582
1931
  goal=goal,
1583
1932
  title=title,
1584
1933
  quest_id=quest_id,
1934
+ runner=default_runner,
1585
1935
  requested_baseline_ref=dict(requested_baseline_ref) if isinstance(requested_baseline_ref, dict) else None,
1586
1936
  startup_contract=dict(startup_contract) if isinstance(startup_contract, dict) else None,
1587
1937
  )
@@ -1712,18 +2062,30 @@ class DaemonApp:
1712
2062
  return snapshot
1713
2063
 
1714
2064
  def schedule_turn(self, quest_id: str, *, reason: str = "user_message") -> dict:
2065
+ snapshot = self.quest_service.snapshot(quest_id)
2066
+ snapshot = self._reconcile_stale_active_turn(quest_id, snapshot=snapshot)
2067
+ recovery = self._recover_stalled_running_turn(quest_id, snapshot=snapshot, turn_reason=reason)
2068
+ snapshot = dict(recovery.get("snapshot") or snapshot)
2069
+ if recovery.get("blocked"):
2070
+ return {
2071
+ "scheduled": True,
2072
+ "started": False,
2073
+ "queued": True,
2074
+ "reason": "stalled_turn_recovery_pending",
2075
+ }
1715
2076
  self._refresh_turn_worker_state(quest_id)
1716
2077
  with self._turn_lock:
1717
2078
  state = self._turn_state.setdefault(quest_id, {"running": False, "pending": False})
1718
2079
  state["pending"] = True
1719
2080
  state["stop_requested"] = False
2081
+ state.pop("recovery_pending", None)
1720
2082
  state["reason"] = reason
1721
2083
  if state.get("running"):
1722
2084
  return {
1723
2085
  "scheduled": True,
1724
2086
  "started": False,
1725
2087
  "queued": True,
1726
- "reason": reason,
2088
+ "reason": "queued_for_artifact_interact" if reason == "user_message" else reason,
1727
2089
  }
1728
2090
  state["running"] = True
1729
2091
  worker = threading.Thread(
@@ -1804,6 +2166,110 @@ class DaemonApp:
1804
2166
  state.pop("worker", None)
1805
2167
  return dict(state)
1806
2168
 
2169
+ def _wait_for_turn_worker_exit(self, quest_id: str, *, timeout_seconds: float) -> dict[str, object]:
2170
+ deadline = time.monotonic() + max(0.0, float(timeout_seconds))
2171
+ state = self._refresh_turn_worker_state(quest_id)
2172
+ while state.get("running") and time.monotonic() < deadline:
2173
+ time.sleep(0.05)
2174
+ state = self._refresh_turn_worker_state(quest_id)
2175
+ return state
2176
+
2177
+ def _ensure_recovery_resume_watch(self, quest_id: str, *, turn_reason: str) -> None:
2178
+ with self._turn_lock:
2179
+ state = self._turn_state.setdefault(quest_id, {"running": False, "pending": False})
2180
+ if state.get("recovery_watch_active"):
2181
+ return
2182
+ state["recovery_watch_active"] = True
2183
+ watcher = threading.Thread(
2184
+ target=self._wait_and_resume_recovered_turn,
2185
+ args=(quest_id, turn_reason),
2186
+ daemon=True,
2187
+ name=f"deepscientist-recovery-watch-{quest_id}",
2188
+ )
2189
+ watcher.start()
2190
+
2191
+ def _wait_and_resume_recovered_turn(self, quest_id: str, turn_reason: str) -> None:
2192
+ try:
2193
+ while True:
2194
+ state = self._refresh_turn_worker_state(quest_id)
2195
+ if not state.get("recovery_pending"):
2196
+ return
2197
+ if not state.get("running"):
2198
+ break
2199
+ time.sleep(0.1)
2200
+
2201
+ snapshot = self.quest_service.snapshot(quest_id)
2202
+ runtime_status = str(snapshot.get("runtime_status") or snapshot.get("status") or "").strip().lower()
2203
+ if runtime_status in {"paused", "stopped", "completed", "error"}:
2204
+ with self._turn_lock:
2205
+ state = self._turn_state.setdefault(quest_id, {"running": False, "pending": False})
2206
+ state.pop("recovery_pending", None)
2207
+ state["stop_requested"] = runtime_status in {"paused", "stopped"}
2208
+ return
2209
+ pending_user_count = int(snapshot.get("pending_user_message_count") or 0)
2210
+ if pending_user_count > 0:
2211
+ with self._turn_lock:
2212
+ state = self._turn_state.setdefault(quest_id, {"running": False, "pending": False})
2213
+ state.pop("recovery_pending", None)
2214
+ self.schedule_turn(quest_id, reason=turn_reason)
2215
+ return
2216
+
2217
+ with self._turn_lock:
2218
+ state = self._turn_state.setdefault(quest_id, {"running": False, "pending": False})
2219
+ state.pop("recovery_pending", None)
2220
+ state["stop_requested"] = False
2221
+ except Exception as exc:
2222
+ self.logger.log(
2223
+ "warning",
2224
+ "quest.turn_state_recovery_watch_failed",
2225
+ quest_id=quest_id,
2226
+ reason=turn_reason,
2227
+ error=str(exc),
2228
+ )
2229
+ finally:
2230
+ with self._turn_lock:
2231
+ state = self._turn_state.setdefault(quest_id, {"running": False, "pending": False})
2232
+ state.pop("recovery_watch_active", None)
2233
+
2234
+ def _stalled_running_turn_details(
2235
+ self,
2236
+ quest_id: str,
2237
+ *,
2238
+ snapshot: dict | None = None,
2239
+ turn_state: dict[str, object] | None = None,
2240
+ turn_reason: str,
2241
+ ) -> dict[str, int] | None:
2242
+ if str(turn_reason or "").strip() not in {"user_message", "queued_user_messages"}:
2243
+ return None
2244
+ snapshot = dict(snapshot or self.quest_service.snapshot(quest_id))
2245
+ runtime_status = str(snapshot.get("runtime_status") or snapshot.get("status") or "").strip().lower()
2246
+ active_run_id = str(snapshot.get("active_run_id") or "").strip()
2247
+ if runtime_status != "running" or not active_run_id:
2248
+ return None
2249
+ state = dict(turn_state or self._refresh_turn_worker_state(quest_id))
2250
+ if not state.get("running"):
2251
+ return None
2252
+ pending_user_count = int(snapshot.get("pending_user_message_count") or 0)
2253
+ if pending_user_count <= 0:
2254
+ return None
2255
+ counts = snapshot.get("counts") if isinstance(snapshot.get("counts"), dict) else {}
2256
+ if int(counts.get("bash_running_count") or 0) > 0:
2257
+ return None
2258
+ silent_seconds = snapshot.get("seconds_since_last_tool_activity")
2259
+ if silent_seconds is None:
2260
+ watchdog = snapshot.get("interaction_watchdog") if isinstance(snapshot.get("interaction_watchdog"), dict) else {}
2261
+ silent_seconds = watchdog.get("seconds_since_last_tool_activity")
2262
+ try:
2263
+ silent_seconds_int = int(silent_seconds or 0)
2264
+ except (TypeError, ValueError):
2265
+ return None
2266
+ if silent_seconds_int < _STALLED_RUNNING_TURN_INACTIVITY_SECONDS:
2267
+ return None
2268
+ return {
2269
+ "pending_user_count": pending_user_count,
2270
+ "silent_seconds": silent_seconds_int,
2271
+ }
2272
+
1807
2273
  def _reconcile_stale_active_turn(self, quest_id: str, *, snapshot: dict | None = None) -> dict:
1808
2274
  snapshot = dict(snapshot or self.quest_service.snapshot(quest_id))
1809
2275
  active_run_id = str(snapshot.get("active_run_id") or "").strip()
@@ -1855,6 +2321,139 @@ class DaemonApp:
1855
2321
  )
1856
2322
  return self.quest_service.mark_turn_finished(quest_id, status=normalized_status)
1857
2323
 
2324
+ def _recover_stalled_running_turn(
2325
+ self,
2326
+ quest_id: str,
2327
+ *,
2328
+ snapshot: dict | None = None,
2329
+ turn_reason: str,
2330
+ ) -> dict[str, object]:
2331
+ snapshot = dict(snapshot or self.quest_service.snapshot(quest_id))
2332
+ turn_state = self._refresh_turn_worker_state(quest_id)
2333
+ with self._turn_lock:
2334
+ state = self._turn_state.setdefault(quest_id, {"running": False, "pending": False})
2335
+ if state.get("recovery_pending"):
2336
+ return {
2337
+ "snapshot": snapshot,
2338
+ "blocked": True,
2339
+ }
2340
+ details = self._stalled_running_turn_details(
2341
+ quest_id,
2342
+ snapshot=snapshot,
2343
+ turn_state=turn_state,
2344
+ turn_reason=turn_reason,
2345
+ )
2346
+ if details is None:
2347
+ return {
2348
+ "snapshot": snapshot,
2349
+ "blocked": False,
2350
+ }
2351
+
2352
+ active_run_id = str(snapshot.get("active_run_id") or "").strip()
2353
+ runner_name = self._runner_name_for(snapshot)
2354
+ with self._turn_lock:
2355
+ state = self._turn_state.setdefault(quest_id, {"running": False, "pending": False})
2356
+ if state.get("recovery_pending"):
2357
+ return {
2358
+ "snapshot": snapshot,
2359
+ "blocked": True,
2360
+ }
2361
+ state["pending"] = False
2362
+ state["stop_requested"] = True
2363
+ state["recovery_pending"] = True
2364
+ interrupted = False
2365
+ try:
2366
+ try:
2367
+ runner = self.get_runner(runner_name)
2368
+ except KeyError:
2369
+ runner = None
2370
+ if runner is not None and hasattr(runner, "interrupt"):
2371
+ interrupted = bool(getattr(runner, "interrupt")(quest_id))
2372
+ stopped_bash_session_ids = self._stop_active_bash_exec_sessions(
2373
+ quest_id,
2374
+ run_id=active_run_id or None,
2375
+ reason="stalled_turn_recovery",
2376
+ user_id="auto:stalled-turn-recovery",
2377
+ )
2378
+ turn_state = self._wait_for_turn_worker_exit(
2379
+ quest_id,
2380
+ timeout_seconds=_STALLED_RUNNING_TURN_INTERRUPT_TIMEOUT_SECONDS,
2381
+ )
2382
+ if turn_state.get("running"):
2383
+ self._ensure_recovery_resume_watch(quest_id, turn_reason="queued_user_messages")
2384
+ self.logger.log(
2385
+ "warning",
2386
+ "quest.turn_state_recovery_pending",
2387
+ quest_id=quest_id,
2388
+ abandoned_run_id=active_run_id or None,
2389
+ reason=turn_reason,
2390
+ silent_seconds=int(details.get("silent_seconds") or 0),
2391
+ pending_user_message_count=int(details.get("pending_user_count") or 0),
2392
+ interrupted=interrupted,
2393
+ )
2394
+ return {
2395
+ "snapshot": snapshot,
2396
+ "blocked": True,
2397
+ }
2398
+
2399
+ previous_status = (
2400
+ str(snapshot.get("runtime_status") or snapshot.get("status") or snapshot.get("display_status") or "running").strip()
2401
+ or "running"
2402
+ )
2403
+ normalized_status = "active" if previous_status == "running" else previous_status
2404
+ summary = (
2405
+ f"Recovered stalled running turn `{active_run_id}` after "
2406
+ f"{int(details.get('silent_seconds') or 0)} seconds without tool activity while "
2407
+ f"{int(details.get('pending_user_count') or 0)} queued user message(s) were waiting."
2408
+ )
2409
+ if interrupted:
2410
+ summary = f"{summary} The active runner process was interrupted."
2411
+ if stopped_bash_session_ids:
2412
+ summary = f"{summary} Stopped {len(stopped_bash_session_ids)} bash_exec session(s)."
2413
+ quest_root = self.quest_service._quest_root(quest_id)
2414
+ append_jsonl(
2415
+ quest_root / ".ds" / "events.jsonl",
2416
+ {
2417
+ "event_id": generate_id("evt"),
2418
+ "type": "quest.turn_state_reconciled",
2419
+ "quest_id": quest_id,
2420
+ "abandoned_run_id": active_run_id or None,
2421
+ "previous_status": previous_status,
2422
+ "status": normalized_status,
2423
+ "completed_at": None,
2424
+ "exit_code": None,
2425
+ "summary": summary,
2426
+ "recovery_kind": "stalled_live_turn",
2427
+ "interrupted": interrupted,
2428
+ "stopped_bash_session_ids": stopped_bash_session_ids,
2429
+ "created_at": utc_now(),
2430
+ },
2431
+ )
2432
+ self.logger.log(
2433
+ "warning",
2434
+ "quest.turn_state_reconciled",
2435
+ quest_id=quest_id,
2436
+ abandoned_run_id=active_run_id or None,
2437
+ previous_status=previous_status,
2438
+ status=normalized_status,
2439
+ recovery_kind="stalled_live_turn",
2440
+ interrupted=interrupted,
2441
+ stopped_bash_session_count=len(stopped_bash_session_ids),
2442
+ )
2443
+ snapshot = self.quest_service.mark_turn_finished(quest_id, status=normalized_status)
2444
+ with self._turn_lock:
2445
+ state = self._turn_state.setdefault(quest_id, {"running": False, "pending": False})
2446
+ state.pop("recovery_pending", None)
2447
+ return {
2448
+ "snapshot": snapshot,
2449
+ "blocked": False,
2450
+ }
2451
+ except Exception:
2452
+ with self._turn_lock:
2453
+ state = self._turn_state.setdefault(quest_id, {"running": False, "pending": False})
2454
+ state.pop("recovery_pending", None)
2455
+ raise
2456
+
1858
2457
  def control_quest(self, quest_id: str, *, action: str, source: str = "local") -> dict:
1859
2458
  normalized_action = str(action or "").strip().lower()
1860
2459
  if normalized_action == "pause":
@@ -1927,25 +2526,319 @@ class DaemonApp:
1927
2526
  )
1928
2527
  notice = self._announce_control_state(
1929
2528
  quest_id,
1930
- action=action,
2529
+ action=action,
2530
+ source=source,
2531
+ snapshot=snapshot,
2532
+ interrupted=interrupted,
2533
+ cancelled_pending_user_message_count=cancelled_count,
2534
+ previous_snapshot=previous_snapshot,
2535
+ )
2536
+ return {
2537
+ "ok": True,
2538
+ "quest_id": quest_id,
2539
+ "action": action,
2540
+ "interrupted": interrupted,
2541
+ "cancelled_pending_user_message_count": cancelled_count,
2542
+ "stopped_bash_session_ids": stopped_bash_session_ids,
2543
+ "snapshot": snapshot,
2544
+ "message": summary,
2545
+ "event": event,
2546
+ "notice": notice,
2547
+ }
2548
+
2549
+ def _quiet_interrupt_for_turn_restart(self, quest_id: str, *, source: str) -> dict[str, Any]:
2550
+ snapshot = self.quest_service.snapshot(quest_id)
2551
+ snapshot = self._reconcile_stale_active_turn(quest_id, snapshot=snapshot)
2552
+ runner_name = self._runner_name_for(snapshot)
2553
+ active_run_id = str(snapshot.get("active_run_id") or "").strip() or None
2554
+ turn_state = self._refresh_turn_worker_state(quest_id)
2555
+ runtime_status = str(snapshot.get("runtime_status") or snapshot.get("status") or "").strip().lower()
2556
+ if not active_run_id and not turn_state.get("running"):
2557
+ if runtime_status in {"stopped", "paused", "completed", "error"}:
2558
+ snapshot = self.quest_service.set_status(quest_id, "active")
2559
+ return {
2560
+ "snapshot": snapshot,
2561
+ "interrupted": False,
2562
+ "stopped_bash_session_ids": [],
2563
+ }
2564
+
2565
+ interrupted = False
2566
+ try:
2567
+ runner = self.get_runner(runner_name)
2568
+ except KeyError:
2569
+ runner = None
2570
+ with self._turn_lock:
2571
+ state = self._turn_state.setdefault(quest_id, {"running": False, "pending": False})
2572
+ state["pending"] = False
2573
+ state["stop_requested"] = True
2574
+ runner_supports_interrupt = runner is not None and hasattr(runner, "interrupt")
2575
+ if runner_supports_interrupt:
2576
+ interrupted = bool(getattr(runner, "interrupt")(quest_id))
2577
+ stopped_bash_session_ids = self._stop_active_bash_exec_sessions(
2578
+ quest_id,
2579
+ run_id=active_run_id,
2580
+ reason="immediate_read",
2581
+ user_id=source,
2582
+ )
2583
+ turn_state = self._wait_for_turn_worker_exit(
2584
+ quest_id,
2585
+ timeout_seconds=_IMMEDIATE_READ_INTERRUPT_TIMEOUT_SECONDS,
2586
+ )
2587
+ if turn_state.get("running"):
2588
+ detail_parts = [
2589
+ f"runner={runner_name or 'unknown'}",
2590
+ f"interrupt_attempted={runner_supports_interrupt}",
2591
+ f"interrupt_returned={interrupted}",
2592
+ f"active_run_id={active_run_id or 'none'}",
2593
+ f"runtime_status={runtime_status or 'unknown'}",
2594
+ f"timeout_seconds={_IMMEDIATE_READ_INTERRUPT_TIMEOUT_SECONDS:g}",
2595
+ ]
2596
+ worker = turn_state.get("worker")
2597
+ if isinstance(worker, threading.Thread):
2598
+ detail_parts.append(f"worker_alive={worker.is_alive()}")
2599
+ detail_parts.append(f"worker_name={worker.name}")
2600
+ stopped_count = len(stopped_bash_session_ids)
2601
+ if stopped_count > 0:
2602
+ detail_parts.append(f"stopped_bash_sessions={stopped_count}")
2603
+ if runner is None:
2604
+ detail_parts.append("reason=runner_not_available")
2605
+ elif not runner_supports_interrupt:
2606
+ detail_parts.append("reason=runner_has_no_interrupt")
2607
+ elif not interrupted:
2608
+ detail_parts.append("reason=runner_interrupt_returned_false")
2609
+ else:
2610
+ detail_parts.append("reason=worker_did_not_exit_before_timeout")
2611
+ raise RuntimeError(
2612
+ "The active agent turn could not be interrupted for immediate read. "
2613
+ + "; ".join(detail_parts)
2614
+ )
2615
+ normalized_status = "active" if runtime_status == "running" else (runtime_status or "active")
2616
+ if normalized_status in {"stopped", "paused", "completed", "error"}:
2617
+ snapshot = self.quest_service.set_status(quest_id, "active")
2618
+ else:
2619
+ snapshot = self.quest_service.mark_turn_finished(quest_id, status=normalized_status)
2620
+ with self._turn_lock:
2621
+ state = self._turn_state.setdefault(quest_id, {"running": False, "pending": False})
2622
+ state["stop_requested"] = False
2623
+ state["running"] = False
2624
+ state.pop("worker", None)
2625
+ return {
2626
+ "snapshot": snapshot,
2627
+ "interrupted": interrupted,
2628
+ "stopped_bash_session_ids": stopped_bash_session_ids,
2629
+ }
2630
+
2631
+ def read_queued_user_messages_now(
2632
+ self,
2633
+ quest_id: str,
2634
+ *,
2635
+ message_id: str | None = None,
2636
+ client_message_id: str | None = None,
2637
+ source: str = "local",
2638
+ ) -> dict[str, Any]:
2639
+ snapshot = self.quest_service.snapshot(quest_id)
2640
+ snapshot = self._reconcile_stale_active_turn(quest_id, snapshot=snapshot)
2641
+ quest_root = self.quest_service._quest_root(quest_id)
2642
+ target_message_id = str(message_id or "").strip() or None
2643
+ target_client_message_id = str(client_message_id or "").strip() or None
2644
+ if target_message_id or target_client_message_id:
2645
+ status_payload = self.quest_service.pending_user_message_status(
2646
+ quest_root,
2647
+ message_id=target_message_id,
2648
+ client_message_id=target_client_message_id,
2649
+ )
2650
+ queue_state = str(status_payload.get("queue_state") or "missing")
2651
+ current_message_state = (
2652
+ dict(status_payload.get("message_state") or {})
2653
+ if isinstance(status_payload.get("message_state"), dict)
2654
+ else None
2655
+ )
2656
+ if queue_state == "read":
2657
+ return {
2658
+ "ok": True,
2659
+ "status": "already_read",
2660
+ "quest_id": quest_id,
2661
+ "message": "This message was already sent to the agent.",
2662
+ "message_ids": [target_message_id] if target_message_id else [],
2663
+ "client_message_ids": [target_client_message_id] if target_client_message_id else [],
2664
+ "current_message_state": current_message_state,
2665
+ "snapshot": snapshot,
2666
+ }
2667
+ if queue_state == "withdrawn":
2668
+ return {
2669
+ "ok": False,
2670
+ "status": "withdrawn",
2671
+ "quest_id": quest_id,
2672
+ "message": "This message was already withdrawn from the waiting queue.",
2673
+ "message_ids": [target_message_id] if target_message_id else [],
2674
+ "client_message_ids": [target_client_message_id] if target_client_message_id else [],
2675
+ "current_message_state": current_message_state,
2676
+ "snapshot": snapshot,
2677
+ }
2678
+ if queue_state == "missing":
2679
+ target_label = target_message_id or f"client:{target_client_message_id}"
2680
+ return {
2681
+ "ok": False,
2682
+ "status": "missing",
2683
+ "quest_id": quest_id,
2684
+ "message": f"Message `{target_label}` is not waiting in the queue.",
2685
+ "message_ids": [target_message_id] if target_message_id else [],
2686
+ "client_message_ids": [target_client_message_id] if target_client_message_id else [],
2687
+ "current_message_state": current_message_state,
2688
+ "snapshot": snapshot,
2689
+ }
2690
+ queue_payload = self.quest_service._read_message_queue(quest_root)
2691
+ pending = [dict(item) for item in (queue_payload.get("pending") or [])]
2692
+ runtime_status = str(snapshot.get("runtime_status") or snapshot.get("status") or "").strip().lower()
2693
+ self.logger.log(
2694
+ "info",
2695
+ "quest.user_message.read_now.requested",
2696
+ quest_id=quest_id,
2697
+ source=source,
2698
+ message_id=target_message_id,
2699
+ client_message_id=target_client_message_id,
2700
+ pending_user_message_count=len(pending),
2701
+ runtime_status=runtime_status or None,
2702
+ )
2703
+ if not pending:
2704
+ return {
2705
+ "ok": False,
2706
+ "status": "empty",
2707
+ "quest_id": quest_id,
2708
+ "message": "No unread user message is waiting in the queue.",
2709
+ "snapshot": snapshot,
2710
+ }
2711
+
2712
+ try:
2713
+ restart = self._quiet_interrupt_for_turn_restart(quest_id, source=source)
2714
+ except RuntimeError as exc:
2715
+ failed_message_ids = (
2716
+ [target_message_id]
2717
+ if target_message_id
2718
+ else [
2719
+ str(item.get("message_id") or "").strip()
2720
+ for item in pending
2721
+ if str(item.get("message_id") or "").strip()
2722
+ ]
2723
+ )
2724
+ self.logger.log(
2725
+ "warning",
2726
+ "quest.user_message.read_now.interrupt_failed",
2727
+ quest_id=quest_id,
2728
+ source=source,
2729
+ message_id=target_message_id,
2730
+ client_message_id=target_client_message_id,
2731
+ pending_user_message_count=len(pending),
2732
+ error=str(exc),
2733
+ )
2734
+ return {
2735
+ "ok": False,
2736
+ "status": "interrupt_failed",
2737
+ "quest_id": quest_id,
2738
+ "message": str(exc),
2739
+ "message_ids": failed_message_ids,
2740
+ "snapshot": snapshot,
2741
+ }
2742
+ mailbox_payload = self.quest_service.consume_pending_user_messages(
2743
+ quest_root,
2744
+ interaction_id=None,
2745
+ delivery_reason="immediate_read",
2746
+ )
2747
+ recent_inbound_messages = [
2748
+ dict(item)
2749
+ for item in (mailbox_payload.get("recent_inbound_messages") or [])
2750
+ if isinstance(item, dict)
2751
+ ]
2752
+ if not recent_inbound_messages:
2753
+ return {
2754
+ "ok": False,
2755
+ "quest_id": quest_id,
2756
+ "message": "No unread user message remained after the restart.",
2757
+ }
2758
+
2759
+ self.quest_service.set_turn_message_override(
2760
+ quest_root,
2761
+ turn_reason="immediate_read",
2762
+ message=str(mailbox_payload.get("agent_instruction") or "").strip(),
2763
+ message_ids=[
2764
+ str(item.get("message_id") or "").strip()
2765
+ for item in recent_inbound_messages
2766
+ if str(item.get("message_id") or "").strip()
2767
+ ],
2768
+ delivery_batch_id=str(((mailbox_payload.get("delivery_batch") or {}).get("batch_id") or "")).strip() or None,
2769
+ source=source,
2770
+ )
2771
+ post_snapshot = self.quest_service.snapshot(quest_id)
2772
+ runtime_status = str(post_snapshot.get("runtime_status") or post_snapshot.get("status") or "").strip().lower()
2773
+ if runtime_status in {"stopped", "paused", "completed", "error"}:
2774
+ post_snapshot = self.quest_service.set_status(quest_id, "active")
2775
+ scheduling = self.schedule_turn(quest_id, reason="immediate_read")
2776
+ message_states = [
2777
+ dict(item)
2778
+ for item in (mailbox_payload.get("message_states") or [])
2779
+ if isinstance(item, dict)
2780
+ ]
2781
+ self.logger.log(
2782
+ "info",
2783
+ "quest.user_message.read_now.scheduled",
2784
+ quest_id=quest_id,
2785
+ source=source,
2786
+ message_ids=[
2787
+ str(item.get("message_id") or "").strip()
2788
+ for item in recent_inbound_messages
2789
+ if str(item.get("message_id") or "").strip()
2790
+ ],
2791
+ client_message_ids=[
2792
+ str(item.get("client_message_id") or "").strip()
2793
+ for item in recent_inbound_messages
2794
+ if str(item.get("client_message_id") or "").strip()
2795
+ ],
2796
+ scheduled=bool(scheduling.get("scheduled")),
2797
+ started=bool(scheduling.get("started")),
2798
+ queued=bool(scheduling.get("queued")),
2799
+ interrupted=bool(restart.get("interrupted")),
2800
+ )
2801
+ return {
2802
+ "ok": True,
2803
+ "status": "scheduled",
2804
+ "quest_id": quest_id,
2805
+ "snapshot": self.quest_service.snapshot(quest_id),
2806
+ "message_ids": [
2807
+ str(item.get("message_id") or "").strip()
2808
+ for item in recent_inbound_messages
2809
+ if str(item.get("message_id") or "").strip()
2810
+ ],
2811
+ "client_message_ids": [
2812
+ str(item.get("client_message_id") or "").strip()
2813
+ for item in recent_inbound_messages
2814
+ if str(item.get("client_message_id") or "").strip()
2815
+ ],
2816
+ "message_states": message_states,
2817
+ "current_message_state": message_states[0] if message_states else None,
2818
+ "delivery_batch": mailbox_payload.get("delivery_batch"),
2819
+ "recent_inbound_messages": recent_inbound_messages,
2820
+ "interrupted": bool(restart.get("interrupted")),
2821
+ "stopped_bash_session_ids": list(restart.get("stopped_bash_session_ids") or []),
2822
+ "message": "Queued user messages were forced into an immediate-read turn.",
2823
+ **scheduling,
2824
+ }
2825
+
2826
+ def withdraw_queued_user_message(
2827
+ self,
2828
+ quest_id: str,
2829
+ *,
2830
+ message_id: str,
2831
+ source: str = "local",
2832
+ ) -> dict[str, Any]:
2833
+ quest_root = self.quest_service._quest_root(quest_id)
2834
+ result = self.quest_service.withdraw_pending_user_message(
2835
+ quest_id,
2836
+ message_id=message_id,
1931
2837
  source=source,
1932
- snapshot=snapshot,
1933
- interrupted=interrupted,
1934
- cancelled_pending_user_message_count=cancelled_count,
1935
- previous_snapshot=previous_snapshot,
1936
2838
  )
1937
- return {
1938
- "ok": True,
1939
- "quest_id": quest_id,
1940
- "action": action,
1941
- "interrupted": interrupted,
1942
- "cancelled_pending_user_message_count": cancelled_count,
1943
- "stopped_bash_session_ids": stopped_bash_session_ids,
1944
- "snapshot": snapshot,
1945
- "message": summary,
1946
- "event": event,
1947
- "notice": notice,
1948
- }
2839
+ result.setdefault("quest_id", quest_id)
2840
+ result["snapshot"] = self.quest_service.snapshot(quest_id)
2841
+ return result
1949
2842
 
1950
2843
  def _stop_active_bash_exec_sessions(
1951
2844
  self,
@@ -2010,6 +2903,23 @@ class DaemonApp:
2010
2903
  last_recovery_abandoned_run_id=recovery_abandoned_run_id,
2011
2904
  last_recovery_summary=recovery_summary,
2012
2905
  )
2906
+ scheduling: dict[str, Any] | None = None
2907
+ if not source.startswith("auto:"):
2908
+ if int(snapshot.get("pending_user_message_count") or 0) > 0:
2909
+ scheduling = self.schedule_turn(quest_id, reason="queued_user_messages")
2910
+ else:
2911
+ continue_payload = self.submit_user_message(
2912
+ quest_id,
2913
+ text=_RESUME_CONTINUE_TEXT,
2914
+ source=self._resume_continue_source(source),
2915
+ )
2916
+ scheduling = {
2917
+ "scheduled": bool(continue_payload.get("scheduled")),
2918
+ "started": bool(continue_payload.get("started")),
2919
+ "queued": bool(continue_payload.get("queued")),
2920
+ "reason": str(continue_payload.get("reason") or "user_message"),
2921
+ "resume_trigger_message": continue_payload.get("message"),
2922
+ }
2013
2923
  summary = f"Quest {quest_id} resumed."
2014
2924
  event = self._append_control_event(
2015
2925
  quest_id,
@@ -2038,6 +2948,7 @@ class DaemonApp:
2038
2948
  "message": summary,
2039
2949
  "event": event,
2040
2950
  "notice": notice,
2951
+ **(dict(scheduling or {})),
2041
2952
  }
2042
2953
 
2043
2954
  def _append_control_event(
@@ -2249,8 +3160,17 @@ class DaemonApp:
2249
3160
  runtime_status = str(snapshot.get("runtime_status") or snapshot.get("status") or "").strip()
2250
3161
  if runtime_status in {"stopped", "paused", "completed", "error"} and not snapshot.get("active_run_id"):
2251
3162
  return
2252
- latest_user_message = self._latest_user_message(quest_id)
2253
- if turn_reason != "auto_continue" and latest_user_message is None:
3163
+ quest_root = Path(snapshot["quest_root"])
3164
+ turn_override = (
3165
+ self.quest_service.consume_turn_message_override(
3166
+ quest_root,
3167
+ expected_turn_reason=turn_reason,
3168
+ )
3169
+ if turn_reason == "immediate_read"
3170
+ else None
3171
+ )
3172
+ latest_user_message = self._latest_actionable_user_message(quest_id, turn_reason=turn_reason)
3173
+ if turn_reason not in {"auto_continue", "immediate_read"} and latest_user_message is None:
2254
3174
  return
2255
3175
 
2256
3176
  runner_name = self._runner_name_for(snapshot)
@@ -2262,7 +3182,11 @@ class DaemonApp:
2262
3182
  model = str(runner_cfg.get("model", "gpt-5.4"))
2263
3183
  run_message = ""
2264
3184
  claimed_message_id: str | None = None
2265
- if turn_reason != "auto_continue":
3185
+ if turn_reason == "immediate_read":
3186
+ run_message = str((turn_override or {}).get("message") or "").strip()
3187
+ if not run_message:
3188
+ return
3189
+ elif turn_reason != "auto_continue":
2266
3190
  run_message = str((latest_user_message or {}).get("content") or "").strip()
2267
3191
  claimed_message_id = str((latest_user_message or {}).get("id") or "").strip() or None
2268
3192
  if not run_message:
@@ -2321,12 +3245,14 @@ class DaemonApp:
2321
3245
  )
2322
3246
  retry_policy = self._runner_retry_policy(runner_name, runner_cfg if isinstance(runner_cfg, dict) else {})
2323
3247
  max_attempts = int(retry_policy.get("max_attempts") or 1)
2324
- turn_id = generate_id("turn")
2325
- retry_context: dict[str, Any] | None = None
2326
- quest_root = Path(snapshot["quest_root"])
3248
+ resumed_start_attempt, resumed_turn_id, retry_context = self._resume_retry_state(
3249
+ snapshot,
3250
+ max_attempts=max_attempts,
3251
+ )
3252
+ turn_id = resumed_turn_id or generate_id("turn")
2327
3253
  worktree_root = Path(str(snapshot["current_workspace_root"])) if snapshot.get("current_workspace_root") else None
2328
3254
 
2329
- for attempt_index in range(1, max_attempts + 1):
3255
+ for attempt_index in range(resumed_start_attempt, max_attempts + 1):
2330
3256
  current_run_id = run_id if attempt_index == 1 else generate_id("run")
2331
3257
  if attempt_index > 1:
2332
3258
  self._append_retry_event(
@@ -2395,6 +3321,31 @@ class DaemonApp:
2395
3321
  previous_output_text="",
2396
3322
  stderr_text=str(exc),
2397
3323
  )
3324
+ diagnosis = self._non_retryable_failure_diagnosis(
3325
+ runner_name=runner_name,
3326
+ summary=failure_summary,
3327
+ stderr_text=str(exc),
3328
+ output_text="",
3329
+ )
3330
+ if diagnosis is not None:
3331
+ self.quest_service.update_runtime_state(
3332
+ quest_root=quest_root,
3333
+ continuation_policy="wait_for_user_or_resume",
3334
+ continuation_reason="non_retryable_runner_error",
3335
+ continuation_updated_at=utc_now(),
3336
+ )
3337
+ self._record_turn_error(
3338
+ quest_id=quest_id,
3339
+ runner_name=runner_name,
3340
+ run_id=current_run_id,
3341
+ skill_id=skill_id,
3342
+ model=model,
3343
+ summary=f"{diagnosis.problem} {failure_summary}".strip(),
3344
+ retry_state=None,
3345
+ diagnosis_code=diagnosis.code,
3346
+ guidance=list(diagnosis.guidance),
3347
+ )
3348
+ return
2398
3349
  if bool(retry_policy.get("enabled")) and attempt_index < max_attempts:
2399
3350
  delay_seconds = self._retry_delay_seconds(retry_policy, attempt_index=attempt_index + 1)
2400
3351
  next_retry_at = self._retry_next_timestamp(delay_seconds)
@@ -2444,6 +3395,19 @@ class DaemonApp:
2444
3395
  )
2445
3396
  return
2446
3397
  exhausted_summary = f"{failure_summary} Retry budget exhausted after {attempt_index} attempt(s)."
3398
+ diagnosis = self._runner_failure_diagnosis(
3399
+ runner_name=runner_name,
3400
+ summary=exhausted_summary,
3401
+ stderr_text=str(exc),
3402
+ output_text="",
3403
+ )
3404
+ if diagnosis is not None and diagnosis.retriable:
3405
+ self.quest_service.update_runtime_state(
3406
+ quest_root=quest_root,
3407
+ continuation_policy="wait_for_user_or_resume",
3408
+ continuation_reason=self._retry_exhausted_continuation_reason(diagnosis),
3409
+ continuation_updated_at=utc_now(),
3410
+ )
2447
3411
  self._append_retry_event(
2448
3412
  quest_id,
2449
3413
  event_type="runner.turn_retry_exhausted",
@@ -2456,6 +3420,7 @@ class DaemonApp:
2456
3420
  max_attempts=max_attempts,
2457
3421
  summary=exhausted_summary,
2458
3422
  failure_summary=failure_summary,
3423
+ diagnosis=diagnosis,
2459
3424
  )
2460
3425
  self._record_turn_error(
2461
3426
  quest_id=quest_id,
@@ -2465,6 +3430,8 @@ class DaemonApp:
2465
3430
  model=model,
2466
3431
  summary=exhausted_summary,
2467
3432
  retry_state=None,
3433
+ diagnosis_code=diagnosis.code if diagnosis is not None else None,
3434
+ guidance=list(diagnosis.guidance) if diagnosis is not None else None,
2468
3435
  )
2469
3436
  return
2470
3437
 
@@ -2543,6 +3510,31 @@ class DaemonApp:
2543
3510
  previous_output_text=result.output_text,
2544
3511
  stderr_text=result.stderr_text,
2545
3512
  )
3513
+ diagnosis = self._non_retryable_failure_diagnosis(
3514
+ runner_name=runner_name,
3515
+ summary=failure_summary,
3516
+ stderr_text=result.stderr_text,
3517
+ output_text=result.output_text,
3518
+ )
3519
+ if diagnosis is not None:
3520
+ self.quest_service.update_runtime_state(
3521
+ quest_root=quest_root,
3522
+ continuation_policy="wait_for_user_or_resume",
3523
+ continuation_reason="non_retryable_runner_error",
3524
+ continuation_updated_at=utc_now(),
3525
+ )
3526
+ self._record_turn_error(
3527
+ quest_id=quest_id,
3528
+ runner_name=runner_name,
3529
+ run_id=result.run_id,
3530
+ skill_id=skill_id,
3531
+ model=model,
3532
+ summary=f"{diagnosis.problem} {failure_summary}".strip(),
3533
+ retry_state=None,
3534
+ diagnosis_code=diagnosis.code,
3535
+ guidance=list(diagnosis.guidance),
3536
+ )
3537
+ return
2546
3538
  if bool(retry_policy.get("enabled")) and attempt_index < max_attempts:
2547
3539
  delay_seconds = self._retry_delay_seconds(retry_policy, attempt_index=attempt_index + 1)
2548
3540
  next_retry_at = self._retry_next_timestamp(delay_seconds)
@@ -2593,6 +3585,19 @@ class DaemonApp:
2593
3585
  return
2594
3586
 
2595
3587
  exhausted_summary = f"{failure_summary} Retry budget exhausted after {attempt_index} attempt(s)."
3588
+ diagnosis = self._runner_failure_diagnosis(
3589
+ runner_name=runner_name,
3590
+ summary=exhausted_summary,
3591
+ stderr_text=result.stderr_text,
3592
+ output_text=result.output_text,
3593
+ )
3594
+ if diagnosis is not None and diagnosis.retriable:
3595
+ self.quest_service.update_runtime_state(
3596
+ quest_root=quest_root,
3597
+ continuation_policy="wait_for_user_or_resume",
3598
+ continuation_reason=self._retry_exhausted_continuation_reason(diagnosis),
3599
+ continuation_updated_at=utc_now(),
3600
+ )
2596
3601
  self._append_retry_event(
2597
3602
  quest_id,
2598
3603
  event_type="runner.turn_retry_exhausted",
@@ -2605,6 +3610,7 @@ class DaemonApp:
2605
3610
  max_attempts=max_attempts,
2606
3611
  summary=exhausted_summary,
2607
3612
  failure_summary=failure_summary,
3613
+ diagnosis=diagnosis,
2608
3614
  )
2609
3615
  self._record_turn_error(
2610
3616
  quest_id=quest_id,
@@ -2614,6 +3620,8 @@ class DaemonApp:
2614
3620
  model=model,
2615
3621
  summary=exhausted_summary,
2616
3622
  retry_state=None,
3623
+ diagnosis_code=diagnosis.code if diagnosis is not None else None,
3624
+ guidance=list(diagnosis.guidance) if diagnosis is not None else None,
2617
3625
  )
2618
3626
  return
2619
3627
  finally:
@@ -2624,8 +3632,37 @@ class DaemonApp:
2624
3632
  )
2625
3633
 
2626
3634
  def _runner_name_for(self, snapshot: dict) -> str:
3635
+ return self._resolve_enabled_runner_name(snapshot)
3636
+
3637
+ def _resolve_enabled_runner_name(self, snapshot: dict, requested_runner: str | None = None) -> str:
2627
3638
  configured = self.config_manager.load_named("config")
2628
- return str(snapshot.get("runner") or configured.get("default_runner", "codex")).strip().lower()
3639
+ candidates = [
3640
+ requested_runner,
3641
+ snapshot.get("runner"),
3642
+ snapshot.get("default_runner"),
3643
+ configured.get("default_runner", "codex"),
3644
+ "codex",
3645
+ ]
3646
+ enabled: list[str] = []
3647
+ seen_enabled: set[str] = set()
3648
+ for name, cfg in self.runners_config.items():
3649
+ normalized = str(name or "").strip().lower()
3650
+ if not normalized or normalized in seen_enabled:
3651
+ continue
3652
+ seen_enabled.add(normalized)
3653
+ if isinstance(cfg, dict) and cfg.get("enabled") is not False:
3654
+ enabled.append(normalized)
3655
+
3656
+ checked: set[str] = set()
3657
+ for raw in candidates:
3658
+ normalized = str(raw or "").strip().lower()
3659
+ if not normalized or normalized in checked:
3660
+ continue
3661
+ checked.add(normalized)
3662
+ cfg = self.runners_config.get(normalized, {})
3663
+ if isinstance(cfg, dict) and cfg.get("enabled") is not False:
3664
+ return normalized
3665
+ return enabled[0] if enabled else "codex"
2629
3666
 
2630
3667
  @staticmethod
2631
3668
  def _stage_state_fingerprint(snapshot: dict) -> str:
@@ -2699,14 +3736,54 @@ class DaemonApp:
2699
3736
  return value
2700
3737
  return "autonomous"
2701
3738
 
3739
+ @staticmethod
3740
+ def _decision_policy_for(snapshot: dict) -> str:
3741
+ startup_contract = snapshot.get("startup_contract")
3742
+ if isinstance(startup_contract, dict):
3743
+ value = str(startup_contract.get("decision_policy") or "").strip().lower()
3744
+ if value in {"autonomous", "user_gated"}:
3745
+ return value
3746
+ return "user_gated"
3747
+
3748
+ def _auto_resume_wait_if_allowed(self, quest_id: str, snapshot: dict) -> tuple[dict, bool]:
3749
+ if str(snapshot.get("continuation_policy") or "").strip().lower() != "wait_for_user_or_resume":
3750
+ return snapshot, False
3751
+ if self._decision_policy_for(snapshot) != "autonomous":
3752
+ return snapshot, False
3753
+ reason = str(snapshot.get("continuation_reason") or "").strip() or "wait_for_user_or_resume"
3754
+ if reason in AUTONOMOUS_BLOCKING_WAIT_REASONS:
3755
+ return snapshot, False
3756
+ quest_root = self.quest_service._quest_root(quest_id)
3757
+ message = self.quest_service.localized_copy(
3758
+ quest_root=quest_root,
3759
+ zh=f"【自动继续】系统检测到当前为无需询问模式,本次“等待反馈”已自动转换为继续执行。原因:{reason}。",
3760
+ en=f"[Auto-resumed] This quest is in autonomous decision mode, so the waiting state was converted back to automatic continuation. Reason: {reason}.",
3761
+ )
3762
+ notice = {
3763
+ "status": "auto_resumed",
3764
+ "reason": reason,
3765
+ "message": message,
3766
+ "decision_policy": "autonomous",
3767
+ "label": self.quest_service.localized_copy(quest_root=quest_root, zh="自动继续", en="Auto-resumed"),
3768
+ "created_at": utc_now(),
3769
+ }
3770
+ self.quest_service.update_runtime_state(
3771
+ quest_root=quest_root,
3772
+ continuation_policy="auto",
3773
+ continuation_reason=f"{reason}_auto_resumed",
3774
+ waiting_notice=notice,
3775
+ )
3776
+ snapshot = self.quest_service.snapshot(quest_id)
3777
+ return snapshot, True
3778
+
2702
3779
  def _resolve_continuation_policy(self, snapshot: dict, *, current_policy: str) -> tuple[str, str]:
2703
3780
  normalized = str(current_policy or "auto").strip().lower() or "auto"
2704
3781
  if normalized != "auto":
2705
3782
  return normalized, str(snapshot.get("continuation_reason") or "").strip() or "explicit_continuation_policy"
2706
- if self._workspace_mode_for(snapshot) == "copilot":
2707
- return "wait_for_user_or_resume", "copilot_mode"
2708
3783
  if self._has_external_progress(snapshot):
2709
3784
  return "when_external_progress", "background_external_progress_active"
3785
+ if self._workspace_mode_for(snapshot) == "copilot":
3786
+ return "wait_for_user_or_resume", "copilot_mode"
2710
3787
  return "auto", "autonomous_prepare_or_launch_long_run"
2711
3788
 
2712
3789
  @staticmethod
@@ -2736,6 +3813,29 @@ class DaemonApp:
2736
3813
 
2737
3814
  return skill
2738
3815
 
3816
+ @staticmethod
3817
+ def _direct_user_turn_skill(snapshot: dict) -> str:
3818
+ available_stage_skills = current_standard_skills(repo_root())
3819
+ workspace_mode = DaemonApp._workspace_mode_for(snapshot)
3820
+ has_explicit_baseline_context = bool(
3821
+ isinstance(snapshot.get("confirmed_baseline_ref"), dict)
3822
+ or isinstance(snapshot.get("requested_baseline_ref"), dict)
3823
+ or str(snapshot.get("active_baseline_id") or "").strip()
3824
+ )
3825
+ for candidate in (
3826
+ str(snapshot.get("active_anchor") or "").strip(),
3827
+ str(snapshot.get("continuation_anchor") or "").strip(),
3828
+ ):
3829
+ if candidate in available_stage_skills and candidate != "decision":
3830
+ if workspace_mode == "copilot" and candidate == "baseline" and not has_explicit_baseline_context:
3831
+ continue
3832
+ return DaemonApp._turn_skill_stage_gate(snapshot, candidate)
3833
+ if workspace_mode == "copilot" and "scout" in available_stage_skills:
3834
+ fallback = "scout"
3835
+ else:
3836
+ fallback = "baseline" if "baseline" in available_stage_skills else "scout"
3837
+ return DaemonApp._turn_skill_stage_gate(snapshot, fallback)
3838
+
2739
3839
  @staticmethod
2740
3840
  def _turn_skill_for(
2741
3841
  snapshot: dict,
@@ -2747,16 +3847,6 @@ class DaemonApp:
2747
3847
  available_stage_skills = current_standard_skills(repo_root())
2748
3848
  workspace_mode = DaemonApp._workspace_mode_for(snapshot)
2749
3849
 
2750
- def copilot_default_skill() -> str:
2751
- active_anchor = str(snapshot.get("active_anchor") or "").strip()
2752
- if active_anchor in available_stage_skills and active_anchor != "decision":
2753
- return DaemonApp._turn_skill_stage_gate(snapshot, active_anchor)
2754
- continuation_anchor = str(snapshot.get("continuation_anchor") or "").strip()
2755
- if continuation_anchor in available_stage_skills and continuation_anchor != "decision":
2756
- return DaemonApp._turn_skill_stage_gate(snapshot, continuation_anchor)
2757
- fallback = "baseline" if "baseline" in available_stage_skills else "scout"
2758
- return DaemonApp._turn_skill_stage_gate(snapshot, fallback)
2759
-
2760
3850
  reply_target = str((latest_user_message or {}).get("reply_to_interaction_id") or "").strip()
2761
3851
  if reply_target:
2762
3852
  for item in (snapshot.get("active_interactions") or []):
@@ -2783,8 +3873,8 @@ class DaemonApp:
2783
3873
  ):
2784
3874
  return "decision"
2785
3875
  if str(item.get("reply_mode") or "") == "threaded":
2786
- if workspace_mode == "copilot":
2787
- return copilot_default_skill()
3876
+ if workspace_mode == "copilot" or turn_mode in {"answering", "command_execution"}:
3877
+ return DaemonApp._direct_user_turn_skill(snapshot)
2788
3878
  return DaemonApp._turn_skill_stage_gate(
2789
3879
  snapshot,
2790
3880
  DaemonApp._continuation_anchor_for(snapshot),
@@ -2792,9 +3882,9 @@ class DaemonApp:
2792
3882
  if turn_mode == "recovering":
2793
3883
  return "decision"
2794
3884
  if workspace_mode == "copilot" and latest_user_message is not None:
2795
- return copilot_default_skill()
3885
+ return DaemonApp._direct_user_turn_skill(snapshot)
2796
3886
  if turn_mode in {"answering", "command_execution"}:
2797
- return "decision"
3887
+ return DaemonApp._direct_user_turn_skill(snapshot)
2798
3888
  if str(turn_reason or "").strip() == "auto_continue" or latest_user_message is None:
2799
3889
  return DaemonApp._turn_skill_stage_gate(
2800
3890
  snapshot,
@@ -2818,6 +3908,14 @@ class DaemonApp:
2818
3908
  return item
2819
3909
  return None
2820
3910
 
3911
+ def _latest_actionable_user_message(self, quest_id: str, *, turn_reason: str) -> dict | None:
3912
+ normalized_reason = str(turn_reason or "").strip() or "user_message"
3913
+ if normalized_reason in {"user_message", "queued_user_messages"}:
3914
+ pending_message = self.quest_service.latest_pending_user_message(quest_id)
3915
+ if pending_message is not None:
3916
+ return pending_message
3917
+ return self._latest_user_message(quest_id)
3918
+
2821
3919
  @staticmethod
2822
3920
  def _runner_binary_issue(runner_name: str, runner: object) -> str | None:
2823
3921
  binary = getattr(runner, "binary", None)
@@ -2896,6 +3994,14 @@ class DaemonApp:
2896
3994
  initial_backoff = CODEX_RETRY_DEFAULT_INITIAL_BACKOFF_SEC
2897
3995
  multiplier = CODEX_RETRY_DEFAULT_BACKOFF_MULTIPLIER
2898
3996
  max_backoff = CODEX_RETRY_DEFAULT_MAX_BACKOFF_SEC
3997
+ if (
3998
+ runner_name == "codex"
3999
+ and max_attempts == PREVIOUS_CODEX_RETRY_DEFAULT_MAX_ATTEMPTS
4000
+ and self._float_matches(initial_backoff, CODEX_RETRY_DEFAULT_INITIAL_BACKOFF_SEC)
4001
+ and self._float_matches(multiplier, CODEX_RETRY_DEFAULT_BACKOFF_MULTIPLIER)
4002
+ and self._float_matches(max_backoff, CODEX_RETRY_DEFAULT_MAX_BACKOFF_SEC)
4003
+ ):
4004
+ max_attempts = CODEX_RETRY_DEFAULT_MAX_ATTEMPTS
2899
4005
  return {
2900
4006
  "enabled": enabled,
2901
4007
  "max_attempts": max_attempts,
@@ -2954,6 +4060,7 @@ class DaemonApp:
2954
4060
  backoff_seconds: float | None = None,
2955
4061
  next_attempt_index: int | None = None,
2956
4062
  previous_run_id: str | None = None,
4063
+ diagnosis: FailureDiagnosis | None = None,
2957
4064
  ) -> dict[str, Any]:
2958
4065
  payload = {
2959
4066
  "event_id": generate_id("evt"),
@@ -2977,6 +4084,9 @@ class DaemonApp:
2977
4084
  payload["next_attempt_index"] = next_attempt_index
2978
4085
  if previous_run_id:
2979
4086
  payload["previous_run_id"] = previous_run_id
4087
+ if diagnosis is not None:
4088
+ payload["diagnosis_code"] = diagnosis.code
4089
+ payload["diagnosis"] = self._failure_diagnosis_payload(diagnosis)
2980
4090
  append_jsonl(self.home / "quests" / quest_id / ".ds" / "events.jsonl", payload)
2981
4091
  self.logger.log(
2982
4092
  "warning" if "scheduled" in event_type or "exhausted" in event_type else "info",
@@ -2990,6 +4100,7 @@ class DaemonApp:
2990
4100
  backoff_seconds=backoff_seconds,
2991
4101
  next_attempt_index=next_attempt_index,
2992
4102
  previous_run_id=previous_run_id,
4103
+ diagnosis_code=diagnosis.code if diagnosis is not None else None,
2993
4104
  )
2994
4105
  return payload
2995
4106
 
@@ -3108,8 +4219,11 @@ class DaemonApp:
3108
4219
  summary: str,
3109
4220
  display_status: str = "error",
3110
4221
  retry_state: dict[str, Any] | None = None,
4222
+ diagnosis_code: str | None = None,
4223
+ guidance: list[str] | None = None,
3111
4224
  ) -> None:
3112
4225
  quest_root = self.home / "quests" / quest_id
4226
+ normalized_guidance = [str(line) for line in (guidance or []) if str(line).strip()]
3113
4227
  append_jsonl(
3114
4228
  quest_root / ".ds" / "events.jsonl",
3115
4229
  {
@@ -3121,6 +4235,8 @@ class DaemonApp:
3121
4235
  "skill_id": skill_id,
3122
4236
  "model": model,
3123
4237
  "summary": summary,
4238
+ "diagnosis_code": str(diagnosis_code or "").strip() or None,
4239
+ "guidance": normalized_guidance,
3124
4240
  "created_at": utc_now(),
3125
4241
  },
3126
4242
  )
@@ -3131,6 +4247,23 @@ class DaemonApp:
3131
4247
  active_run_id=None,
3132
4248
  retry_state=retry_state,
3133
4249
  )
4250
+ runner_label = self._connector_runner_label(runner_name)
4251
+ user_notice = self._polite_copy(
4252
+ zh=f"DeepScientist 意外停止运行,请检查你所绑定的{runner_label}是否能够顺利连接。",
4253
+ en=f"DeepScientist stopped unexpectedly. Please check whether the bound {runner_label} can connect normally.",
4254
+ )
4255
+ notice_message = "\n".join([user_notice, "", summary]).strip()
4256
+ if normalized_guidance:
4257
+ notice_message = "\n".join(
4258
+ [
4259
+ user_notice,
4260
+ "",
4261
+ summary,
4262
+ "",
4263
+ "Suggested fix:",
4264
+ *[f"- {line}" for line in normalized_guidance[:3]],
4265
+ ]
4266
+ ).strip()
3134
4267
  self.logger.log(
3135
4268
  "error",
3136
4269
  "runner.turn_error",
@@ -3143,7 +4276,7 @@ class DaemonApp:
3143
4276
  )
3144
4277
  self._relay_quest_message_to_bound_connectors(
3145
4278
  quest_id,
3146
- message=summary,
4279
+ message=notice_message,
3147
4280
  kind="error",
3148
4281
  response_phase="final",
3149
4282
  importance="warning",
@@ -3154,10 +4287,63 @@ class DaemonApp:
3154
4287
  "skill_id": skill_id,
3155
4288
  "runner": runner_name,
3156
4289
  "model": model,
4290
+ "diagnosis_code": str(diagnosis_code or "").strip() or None,
3157
4291
  }
3158
4292
  ],
3159
4293
  )
3160
4294
 
4295
+ @staticmethod
4296
+ def _failure_diagnosis_payload(diagnosis: FailureDiagnosis) -> dict[str, Any]:
4297
+ fix = [str(line) for line in diagnosis.guidance if str(line).strip()]
4298
+ return {
4299
+ "code": diagnosis.code,
4300
+ "problem": diagnosis.problem,
4301
+ "why": diagnosis.why,
4302
+ "fix": fix,
4303
+ "guidance": fix,
4304
+ "retriable": bool(diagnosis.retriable),
4305
+ "matched_text": diagnosis.matched_text,
4306
+ }
4307
+
4308
+ @staticmethod
4309
+ def _runner_failure_diagnosis(
4310
+ *,
4311
+ runner_name: str,
4312
+ summary: str,
4313
+ stderr_text: str,
4314
+ output_text: str,
4315
+ ) -> FailureDiagnosis | None:
4316
+ return diagnose_runner_failure(
4317
+ runner_name=runner_name,
4318
+ summary=summary,
4319
+ stderr_text=stderr_text,
4320
+ output_text=output_text,
4321
+ )
4322
+
4323
+ @staticmethod
4324
+ def _non_retryable_failure_diagnosis(
4325
+ *,
4326
+ runner_name: str,
4327
+ summary: str,
4328
+ stderr_text: str,
4329
+ output_text: str,
4330
+ ) -> FailureDiagnosis | None:
4331
+ diagnosis = DaemonApp._runner_failure_diagnosis(
4332
+ runner_name=runner_name,
4333
+ summary=summary,
4334
+ stderr_text=stderr_text,
4335
+ output_text=output_text,
4336
+ )
4337
+ if diagnosis is None or diagnosis.retriable:
4338
+ return None
4339
+ return diagnosis
4340
+
4341
+ @staticmethod
4342
+ def _retry_exhausted_continuation_reason(diagnosis: FailureDiagnosis) -> str:
4343
+ if diagnosis.code == "codex_upstream_provider_error":
4344
+ return "external_codex_upstream_provider_error"
4345
+ return "runner_retry_budget_exhausted"
4346
+
3161
4347
  def _record_turn_postprocess_warning(
3162
4348
  self,
3163
4349
  *,
@@ -3289,11 +4475,13 @@ class DaemonApp:
3289
4475
  self.schedule_turn(quest_id, reason="queued_user_messages")
3290
4476
  else:
3291
4477
  continuation_policy = str(snapshot.get("continuation_policy") or "auto").strip().lower() or "auto"
4478
+ resolved_external_progress = False
3292
4479
  if continuation_policy == "auto":
3293
4480
  continuation_policy, continuation_reason = self._resolve_continuation_policy(
3294
4481
  snapshot,
3295
4482
  current_policy=continuation_policy,
3296
4483
  )
4484
+ resolved_external_progress = continuation_policy == "when_external_progress"
3297
4485
  self.quest_service.update_runtime_state(
3298
4486
  quest_root=self.quest_service._quest_root(quest_id),
3299
4487
  continuation_policy=continuation_policy,
@@ -3311,12 +4499,22 @@ class DaemonApp:
3311
4499
  if int(snapshot.get("pending_user_message_count") or 0) > 0:
3312
4500
  self.schedule_turn(quest_id, reason="queued_user_messages")
3313
4501
  return
4502
+ snapshot, auto_resumed_wait = self._auto_resume_wait_if_allowed(quest_id, snapshot)
4503
+ if auto_resumed_wait:
4504
+ self._schedule_turn_later(
4505
+ quest_id,
4506
+ reason="auto_continue",
4507
+ delay_seconds=self._auto_continue_delay_for_policy("auto"),
4508
+ )
4509
+ return
3314
4510
  continuation_policy = str(snapshot.get("continuation_policy") or "auto").strip().lower() or "auto"
4511
+ resolved_external_progress = False
3315
4512
  if continuation_policy == "auto":
3316
4513
  continuation_policy, continuation_reason = self._resolve_continuation_policy(
3317
4514
  snapshot,
3318
4515
  current_policy=continuation_policy,
3319
4516
  )
4517
+ resolved_external_progress = continuation_policy == "when_external_progress"
3320
4518
  self.quest_service.update_runtime_state(
3321
4519
  quest_root=self.quest_service._quest_root(quest_id),
3322
4520
  continuation_policy=continuation_policy,
@@ -3329,7 +4527,7 @@ class DaemonApp:
3329
4527
  if continuation_policy == "wait_for_user_or_resume":
3330
4528
  return
3331
4529
  if continuation_policy == "when_external_progress":
3332
- if not self._has_external_progress(snapshot):
4530
+ if not resolved_external_progress and not self._has_external_progress(snapshot):
3333
4531
  next_policy = "wait_for_user_or_resume" if self._workspace_mode_for(snapshot) == "copilot" else "auto"
3334
4532
  next_reason = "external_progress_finished" if next_policy == "wait_for_user_or_resume" else "external_progress_finished_continue_autonomous"
3335
4533
  self.quest_service.update_runtime_state(
@@ -3361,11 +4559,13 @@ class DaemonApp:
3361
4559
  if status in {"completed", "paused", "stopped", "error", "waiting_for_user"}:
3362
4560
  return
3363
4561
  continuation_policy = str(snapshot.get("continuation_policy") or "auto").strip().lower() or "auto"
4562
+ resolved_external_progress = False
3364
4563
  if continuation_policy == "auto":
3365
4564
  continuation_policy, continuation_reason = self._resolve_continuation_policy(
3366
4565
  snapshot,
3367
4566
  current_policy=continuation_policy,
3368
4567
  )
4568
+ resolved_external_progress = continuation_policy == "when_external_progress"
3369
4569
  self.quest_service.update_runtime_state(
3370
4570
  quest_root=self.quest_service._quest_root(quest_id),
3371
4571
  continuation_policy=continuation_policy,
@@ -3373,10 +4573,15 @@ class DaemonApp:
3373
4573
  continuation_updated_at=utc_now(),
3374
4574
  )
3375
4575
  snapshot = self.quest_service.snapshot(quest_id)
4576
+ snapshot, auto_resumed_wait = self._auto_resume_wait_if_allowed(quest_id, snapshot)
4577
+ if auto_resumed_wait:
4578
+ self.schedule_turn(quest_id, reason=reason)
4579
+ return
4580
+ continuation_policy = str(snapshot.get("continuation_policy") or "auto").strip().lower() or "auto"
3376
4581
  if continuation_policy in {"none", "wait_for_user_or_resume"}:
3377
4582
  return
3378
4583
  if continuation_policy == "when_external_progress":
3379
- if not self._has_external_progress(snapshot):
4584
+ if not resolved_external_progress and not self._has_external_progress(snapshot):
3380
4585
  next_policy = "wait_for_user_or_resume" if self._workspace_mode_for(snapshot) == "copilot" else "auto"
3381
4586
  next_reason = "external_progress_finished" if next_policy == "wait_for_user_or_resume" else "external_progress_finished_continue_autonomous"
3382
4587
  self.quest_service.update_runtime_state(
@@ -3400,16 +4605,18 @@ class DaemonApp:
3400
4605
  quest_id = str(snapshot.get("quest_id") or "").strip()
3401
4606
  if not quest_id:
3402
4607
  return False
4608
+ counts = snapshot.get("counts") if isinstance(snapshot.get("counts"), dict) else {}
3403
4609
  try:
3404
4610
  quest_root = self.quest_service._quest_root(quest_id)
3405
4611
  except FileNotFoundError:
3406
- return False
4612
+ return int(counts.get("bash_running_count") or 0) > 0
3407
4613
  try:
3408
4614
  sessions = self.bash_exec_service.list_sessions(quest_root, limit=200)
3409
- return any(str(item.get("status") or "").strip().lower() == "running" for item in sessions if isinstance(item, dict))
4615
+ if any(str(item.get("status") or "").strip().lower() == "running" for item in sessions if isinstance(item, dict)):
4616
+ return True
3410
4617
  except Exception:
3411
- counts = snapshot.get("counts") if isinstance(snapshot.get("counts"), dict) else {}
3412
- return int(counts.get("bash_running_count") or 0) > 0
4618
+ pass
4619
+ return int(counts.get("bash_running_count") or 0) > 0
3413
4620
 
3414
4621
  def _relay_quest_message_to_bound_connectors(
3415
4622
  self,
@@ -3467,6 +4674,7 @@ class DaemonApp:
3467
4674
  self._shutdown_requested.set()
3468
4675
  self._stop_background_connectors()
3469
4676
  self._stop_terminal_attach_server()
4677
+ self.admin_service.system_monitor.stop()
3470
4678
  self.bash_exec_service.shutdown()
3471
4679
  self.logger.log(
3472
4680
  "info",
@@ -4215,10 +5423,21 @@ class DaemonApp:
4215
5423
  removed_conversations = self._unbind_external_bindings(quest_id)
4216
5424
  self.sessions.forget(quest_id)
4217
5425
 
4218
- try:
4219
- shutil.rmtree(quest_root)
4220
- except FileNotFoundError:
4221
- return {"ok": True, "quest_id": quest_id, "deleted": False}
5426
+ last_delete_error: OSError | None = None
5427
+ for attempt in range(5):
5428
+ try:
5429
+ shutil.rmtree(quest_root)
5430
+ last_delete_error = None
5431
+ break
5432
+ except FileNotFoundError:
5433
+ return {"ok": True, "quest_id": quest_id, "deleted": False}
5434
+ except OSError as exc:
5435
+ last_delete_error = exc
5436
+ if exc.errno not in {errno.ENOTEMPTY, errno.EBUSY} or attempt == 4:
5437
+ raise
5438
+ time.sleep(0.05 * (attempt + 1))
5439
+ if last_delete_error is not None and quest_root.exists():
5440
+ raise last_delete_error
4222
5441
 
4223
5442
  self.logger.log(
4224
5443
  "info",
@@ -4333,11 +5552,17 @@ class DaemonApp:
4333
5552
  exclude_conversation_id=conversation_id,
4334
5553
  )
4335
5554
  self.update_quest_binding(created["quest_id"], binding_conversation_id, force=True)
4336
- self.submit_user_message(
5555
+ startup = self.submit_user_message(
4337
5556
  created["quest_id"],
4338
5557
  text=goal_text,
4339
5558
  source=conversation_id,
4340
5559
  )
5560
+ created_snapshot = self.quest_service.snapshot(created["quest_id"])
5561
+ startup_message = self._connector_turn_start_message(
5562
+ quest_id=created["quest_id"],
5563
+ startup=startup,
5564
+ snapshot=created_snapshot,
5565
+ )
4341
5566
  return channel.send(
4342
5567
  {
4343
5568
  "conversation_id": conversation_id,
@@ -4350,7 +5575,9 @@ class DaemonApp:
4350
5575
  quest_id=created["quest_id"],
4351
5576
  goal=goal_text,
4352
5577
  previous_quest_id=previous_quest_id,
4353
- ),
5578
+ )
5579
+ + "\n"
5580
+ + startup_message,
4354
5581
  ),
4355
5582
  }
4356
5583
  )
@@ -4759,7 +5986,7 @@ class DaemonApp:
4759
5986
  message_id=str(message.get("message_id") or "").strip() or None,
4760
5987
  attachments=[dict(item) for item in (message.get("attachments") or []) if isinstance(item, dict)],
4761
5988
  )
4762
- self.submit_user_message(
5989
+ startup = self.submit_user_message(
4763
5990
  quest_id,
4764
5991
  text=self._connector_message_text_with_attachment_notice(
4765
5992
  original_text=text,
@@ -4768,18 +5995,18 @@ class DaemonApp:
4768
5995
  source=conversation_id,
4769
5996
  attachments=materialized_attachments,
4770
5997
  )
5998
+ snapshot = self.quest_service.snapshot(quest_id)
5999
+ startup_message = self._connector_turn_start_message(
6000
+ quest_id=quest_id,
6001
+ startup=startup,
6002
+ snapshot=snapshot,
6003
+ )
4771
6004
  return channel.send(
4772
6005
  {
4773
6006
  "conversation_id": conversation_id,
4774
6007
  "quest_id": quest_id,
4775
6008
  "kind": "ack",
4776
- "message": self._with_qq_main_chat_notice(
4777
- message,
4778
- self._polite_copy(
4779
- zh=f"老师,已收到您的消息,当前会话已绑定到 {quest_id}。我会继续推进,并通过后续消息同步进展。",
4780
- en=f"Received. This conversation is bound to {quest_id}; I’ll continue the work and keep you updated with the next progress checkpoint.",
4781
- ),
4782
- ),
6009
+ "message": self._with_qq_main_chat_notice(message, startup_message),
4783
6010
  }
4784
6011
  )
4785
6012
 
@@ -5772,6 +6999,78 @@ class DaemonApp:
5772
6999
  en="I will clarify the current task first, then keep moving. ✨",
5773
7000
  )
5774
7001
 
7002
+ def _connector_runner_label(self, runner_name: str) -> str:
7003
+ normalized = str(runner_name or "codex").strip().lower() or "codex"
7004
+ if normalized == "claude":
7005
+ return "Claude Code"
7006
+ if normalized == "opencode":
7007
+ return "Open Code"
7008
+ try:
7009
+ metadata = get_runner_metadata(normalized)
7010
+ except Exception:
7011
+ return runner_name or "Codex"
7012
+ return str(metadata.label or runner_name or "Codex")
7013
+
7014
+ def _connector_runner_readiness(self, runner_name: str) -> dict[str, Any]:
7015
+ try:
7016
+ state = self.config_manager.runner_bootstrap_state(runner_name)
7017
+ except Exception:
7018
+ return {"runner": runner_name, "ready": True, "last_result": {}}
7019
+ if not isinstance(state, dict):
7020
+ return {"runner": runner_name, "ready": True, "last_result": {}}
7021
+ return state
7022
+
7023
+ def _connector_turn_start_message(
7024
+ self,
7025
+ *,
7026
+ quest_id: str,
7027
+ startup: dict[str, Any],
7028
+ snapshot: dict[str, Any],
7029
+ ) -> str:
7030
+ runner_name = self._runner_name_for(snapshot)
7031
+ runner_label = self._connector_runner_label(runner_name)
7032
+ readiness = self._connector_runner_readiness(runner_name)
7033
+ readiness_result = readiness.get("last_result") if isinstance(readiness.get("last_result"), dict) else {}
7034
+ readiness_summary = str(readiness_result.get("summary") or "").strip()
7035
+ readiness_errors = [str(item).strip() for item in (readiness_result.get("errors") or []) if str(item).strip()]
7036
+ readiness_guidance = [str(item).strip() for item in (readiness_result.get("guidance") or []) if str(item).strip()]
7037
+ ready = bool(readiness.get("ready", True))
7038
+ reason = str(startup.get("reason") or "").strip().lower()
7039
+ started = bool(startup.get("started"))
7040
+ queued = bool(startup.get("queued"))
7041
+ auto_resumed = bool(startup.get("auto_resumed"))
7042
+ has_explicit_runner_failure = bool(readiness_summary or readiness_errors or readiness_guidance)
7043
+ if reason == "stalled_turn_recovery_pending":
7044
+ return self._polite_copy(
7045
+ zh=f"正在启动 DeepScientist,当前会先恢复 `{quest_id}` 里停滞的运行,再继续处理您的新消息。",
7046
+ en=f"DeepScientist is starting up. I’m recovering the stalled run in `{quest_id}` first, then I’ll continue with your new message.",
7047
+ )
7048
+ if not ready and has_explicit_runner_failure:
7049
+ status_line = f" 当前状态:{readiness_summary}" if readiness_summary else ""
7050
+ return self._polite_copy(
7051
+ zh=(
7052
+ f"DeepScientist 仍然不在线哟,请检查你所绑定的{runner_label}是否能够顺利连接。{status_line}"
7053
+ ),
7054
+ en=(
7055
+ f"DeepScientist is still offline. Please check whether the bound {runner_label} can connect normally."
7056
+ f"{(' Current status: ' + readiness_summary) if readiness_summary else ''}"
7057
+ ),
7058
+ )
7059
+ if started or auto_resumed:
7060
+ return self._polite_copy(
7061
+ zh=f"已经成功收到消息,DeepScientist 已经启动并开始处理 `{quest_id}` 啦。",
7062
+ en=f"Message received successfully. DeepScientist has started and is now processing `{quest_id}`.",
7063
+ )
7064
+ if queued:
7065
+ return self._polite_copy(
7066
+ zh=f"已经成功收到消息,`{quest_id}` 当前正在运行中,这条消息也已经排进队列了。",
7067
+ en=f"Message received successfully. `{quest_id}` is already running, and this message has been queued.",
7068
+ )
7069
+ return self._polite_copy(
7070
+ zh=f"已经成功收到消息,我会继续推进 `{quest_id}` 并同步后续进展。",
7071
+ en=f"Message received successfully. I’ll continue `{quest_id}` and keep you updated.",
7072
+ )
7073
+
5775
7074
  def _quest_created_connector_message(
5776
7075
  self,
5777
7076
  connector_name: str,
@@ -6828,6 +8127,10 @@ class DaemonApp:
6828
8127
  "event_id": event.get("event_id") or f"evt-{quest_id}-{current_cursor}",
6829
8128
  **event,
6830
8129
  }
8130
+ enriched_event = self.quest_service.enrich_conversation_message_event(
8131
+ quest_id,
8132
+ enriched_event,
8133
+ )
6831
8134
  update = build_session_update(
6832
8135
  enriched_event,
6833
8136
  quest_id=quest_id,
@@ -6908,6 +8211,75 @@ class DaemonApp:
6908
8211
  except (BrokenPipeError, ConnectionResetError, TimeoutError):
6909
8212
  return
6910
8213
 
8214
+ def stream_admin_task(
8215
+ self,
8216
+ handler: BaseHTTPRequestHandler,
8217
+ *,
8218
+ task_id: str,
8219
+ headers: dict[str, str] | None = None,
8220
+ extra_headers: dict[str, str] | None = None,
8221
+ ) -> None:
8222
+ try:
8223
+ task = self.admin_task_service.get_task(task_id)
8224
+ except FileNotFoundError:
8225
+ self._write_handler_response(
8226
+ handler,
8227
+ code=404,
8228
+ content=json.dumps(
8229
+ {"ok": False, "message": f"Unknown admin task `{task_id}`."},
8230
+ ensure_ascii=False,
8231
+ ).encode("utf-8"),
8232
+ content_type="application/json; charset=utf-8",
8233
+ extra_headers=extra_headers,
8234
+ )
8235
+ return
8236
+
8237
+ last_event_raw = str((headers or {}).get("Last-Event-ID") or (headers or {}).get("last-event-id") or "").strip()
8238
+ cursor = int(last_event_raw) if last_event_raw.isdigit() else 0
8239
+
8240
+ handler.send_response(200)
8241
+ handler.send_header("Content-Type", "text/event-stream; charset=utf-8")
8242
+ handler.send_header("Cache-Control", "no-cache, no-transform")
8243
+ handler.send_header("Connection", "keep-alive")
8244
+ handler.send_header("X-Accel-Buffering", "no")
8245
+ for key, value in (extra_headers or {}).items():
8246
+ handler.send_header(key, value)
8247
+ handler.end_headers()
8248
+ handler.wfile.write(b"retry: 1000\n\n")
8249
+ handler.wfile.flush()
8250
+
8251
+ self._write_sse_event(
8252
+ handler,
8253
+ event="task.snapshot",
8254
+ data={"task": task},
8255
+ )
8256
+ heartbeat_at = time.monotonic()
8257
+ terminal_statuses = {"completed", "failed", "cancelled"}
8258
+ try:
8259
+ while True:
8260
+ current_task = self.admin_task_service.get_task(task_id)
8261
+ events = self.admin_task_service.read_events(task_id, after=cursor, limit=200)
8262
+ if events:
8263
+ for item in events:
8264
+ cursor = max(cursor, int(item.get("seq") or 0))
8265
+ self._write_sse_event(
8266
+ handler,
8267
+ event=str(item.get("event") or "task.log"),
8268
+ data=item,
8269
+ event_id=str(cursor),
8270
+ )
8271
+ heartbeat_at = time.monotonic()
8272
+ elif time.monotonic() - heartbeat_at >= 10:
8273
+ handler.wfile.write(b": keep-alive\n\n")
8274
+ handler.wfile.flush()
8275
+ heartbeat_at = time.monotonic()
8276
+ current_status = str(current_task.get("status") or "").strip().lower()
8277
+ if current_status in terminal_statuses and cursor >= int(current_task.get("last_event_seq") or 0):
8278
+ return
8279
+ time.sleep(0.2 if current_status == "running" else 0.6)
8280
+ except (BrokenPipeError, ConnectionResetError, TimeoutError):
8281
+ return
8282
+
6911
8283
  def stream_bash_sessions(
6912
8284
  self,
6913
8285
  handler: BaseHTTPRequestHandler,
@@ -7227,6 +8599,13 @@ class DaemonApp:
7227
8599
  app = self
7228
8600
 
7229
8601
  class RequestHandler(BaseHTTPRequestHandler):
8602
+ def handle(self) -> None:
8603
+ try:
8604
+ super().handle()
8605
+ except (BrokenPipeError, ConnectionResetError, TimeoutError):
8606
+ self.close_connection = True
8607
+ return
8608
+
7230
8609
  def log_message(self, format: str, *args) -> None:
7231
8610
  return
7232
8611
 
@@ -7270,6 +8649,18 @@ class DaemonApp:
7270
8649
  },
7271
8650
  )
7272
8651
  return
8652
+ if route_name in {"admin_task_stream", "system_task_stream"}:
8653
+ try:
8654
+ app.stream_admin_task(self, **params, headers=request_headers, extra_headers=auth_headers)
8655
+ except Exception as exc:
8656
+ app.logger.log(
8657
+ "error",
8658
+ "http.stream_admin_task_failed",
8659
+ path=self.path,
8660
+ error=str(exc),
8661
+ )
8662
+ self.close_connection = True
8663
+ return
7273
8664
  if route_name == "quest_events" and app._wants_event_stream(self.path, request_headers):
7274
8665
  try:
7275
8666
  app.stream_quest_events(self, **params, path=self.path, headers=request_headers, extra_headers=auth_headers)
@@ -7341,6 +8732,7 @@ class DaemonApp:
7341
8732
 
7342
8733
  try:
7343
8734
  result = getattr(app.handlers, route_name)
8735
+ route_alias = route_name.removeprefix("admin_").removeprefix("system_")
7344
8736
  if route_name == "asset":
7345
8737
  status, headers, content = result(**params)
7346
8738
  app._write_handler_response(
@@ -7350,7 +8742,17 @@ class DaemonApp:
7350
8742
  extra_headers=app._merge_response_headers(headers, auth_headers),
7351
8743
  )
7352
8744
  return
7353
- if route_name in {
8745
+ if route_alias in {
8746
+ "quests",
8747
+ "runtime_sessions",
8748
+ "log_tail",
8749
+ "failures",
8750
+ "errors",
8751
+ "audit",
8752
+ "search",
8753
+ "repairs",
8754
+ "tasks",
8755
+ } or route_name in {
7354
8756
  "quest_events",
7355
8757
  "bash_sessions",
7356
8758
  "bash_logs",
@@ -7373,9 +8775,28 @@ class DaemonApp:
7373
8775
  "annotations_project",
7374
8776
  }:
7375
8777
  payload = result(**params, path=self.path)
8778
+ elif route_name in {
8779
+ "benchstore_entries",
8780
+ "benchstore_entry",
8781
+ "benchstore_entry_image",
8782
+ "benchstore_entry_setup_packet",
8783
+ }:
8784
+ payload = result(**params, path=self.path) if params else result(self.path)
7376
8785
  elif method == "GET":
7377
8786
  payload = result(**params) if params else result()
7378
- 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", "weixin_login_qr_start", "weixin_login_qr_wait", "arxiv_import", "annotation_create", "auth_login", "auth_rotate"}:
8787
+ elif route_alias in {
8788
+ "shutdown",
8789
+ "chart_query",
8790
+ "task_doctor_start",
8791
+ "task_system_update_check_start",
8792
+ "task_system_update_action_start",
8793
+ "issue_draft",
8794
+ "controller_run",
8795
+ "controller_toggle",
8796
+ "repair_create",
8797
+ "repair_close",
8798
+ "hardware_update",
8799
+ } or route_name in {"document_open", "document_asset_upload", "quest_file_create_folder", "quest_file_upload", "quest_file_rename", "quest_file_move", "quest_file_delete", "chat_upload_create", "chat_upload_delete", "chat", "command", "quest_control", "quest_message_read_now", "quest_message_withdraw", "config_save", "quest_create", "quest_baseline_binding", "run_create", "qq_inbound", "connector_inbound", "docs_open", "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", "weixin_login_qr_start", "weixin_login_qr_wait", "arxiv_import", "annotation_create", "auth_login", "auth_rotate"}:
7379
8800
  payload = result(**params, body=body)
7380
8801
  elif route_name == "config_validate":
7381
8802
  payload = result(body)
@@ -7385,6 +8806,8 @@ class DaemonApp:
7385
8806
  payload = result(**params, body=body)
7386
8807
  elif route_name == "memory":
7387
8808
  payload = result(app.handlers.parse_query(self.path))
8809
+ elif route_name == "benchstore_entry_launch":
8810
+ payload = result(**params, path=self.path, body=body)
7388
8811
  else:
7389
8812
  payload = result(**params) if params else result()
7390
8813
  except Exception as exc:
@@ -7432,6 +8855,8 @@ class DaemonApp:
7432
8855
  self._serve_host = host
7433
8856
  self._serve_port = port
7434
8857
  self._shutdown_requested.clear()
8858
+ self.admin_service.system_monitor.start()
8859
+ self.admin_service.metrics_collector.start()
7435
8860
  self._start_terminal_attach_server(host, port)
7436
8861
  self._start_background_connectors()
7437
8862
  self._resume_reconciled_quests()
@@ -7453,6 +8878,8 @@ class DaemonApp:
7453
8878
  finally:
7454
8879
  self._stop_background_connectors()
7455
8880
  self._stop_terminal_attach_server()
8881
+ self.admin_service.metrics_collector.stop()
8882
+ self.admin_service.system_monitor.stop()
7456
8883
  self.bash_exec_service.shutdown()
7457
8884
  self._server = None
7458
8885
  self._serve_host = None