@researai/deepscientist 1.5.17 → 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.
- package/AGENTS.md +309 -130
- package/AISB/catalog/aisb.b1.agentic_coding.yaml +244 -0
- package/AISB/catalog/aisb.b10.climate_earth.yaml +235 -0
- package/AISB/catalog/aisb.b11.model_efficiency.yaml +231 -0
- package/AISB/catalog/aisb.b12.embodied_ai.yaml +238 -0
- package/AISB/catalog/aisb.b2.agent_systems.yaml +229 -0
- package/AISB/catalog/aisb.b3.self_evolving_rl.yaml +237 -0
- package/AISB/catalog/aisb.b4.lm_reasoning.yaml +240 -0
- package/AISB/catalog/aisb.b5.math_proof.yaml +235 -0
- package/AISB/catalog/aisb.b6.research_process.yaml +243 -0
- package/AISB/catalog/aisb.b7.multimodal_fusion.yaml +232 -0
- package/AISB/catalog/aisb.b8.lifesci_drug.yaml +275 -0
- package/AISB/catalog/aisb.b9.material_science.yaml +237 -0
- package/AISB/catalog/aisb.t3.001_savvy.yaml +159 -0
- package/AISB/catalog/aisb.t3.001_savvy.zh.yaml +121 -0
- package/AISB/catalog/aisb.t3.002_pinet.yaml +189 -0
- package/AISB/catalog/aisb.t3.002_pinet.zh.yaml +130 -0
- package/AISB/catalog/aisb.t3.004_decentralattn.yaml +184 -0
- package/AISB/catalog/aisb.t3.004_decentralattn.zh.yaml +153 -0
- package/AISB/catalog/aisb.t3.005_tsae.yaml +193 -0
- package/AISB/catalog/aisb.t3.005_tsae.zh.yaml +139 -0
- package/AISB/catalog/aisb.t3.006_physense.yaml +194 -0
- package/AISB/catalog/aisb.t3.006_physense.zh.yaml +118 -0
- package/AISB/catalog/aisb.t3.007_reasoningiqa.yaml +169 -0
- package/AISB/catalog/aisb.t3.007_reasoningiqa.zh.yaml +133 -0
- package/AISB/catalog/aisb.t3.008_meanflows.yaml +188 -0
- package/AISB/catalog/aisb.t3.008_meanflows.zh.yaml +140 -0
- package/AISB/catalog/aisb.t3.009_scoremissing.yaml +179 -0
- package/AISB/catalog/aisb.t3.009_scoremissing.zh.yaml +119 -0
- package/AISB/catalog/aisb.t3.010_suitabilityfilter.yaml +221 -0
- package/AISB/catalog/aisb.t3.010_suitabilityfilter.zh.yaml +141 -0
- package/AISB/catalog/aisb.t3.011_osd.yaml +206 -0
- package/AISB/catalog/aisb.t3.011_osd.zh.yaml +163 -0
- package/AISB/catalog/aisb.t3.012_efficientqat.yaml +206 -0
- package/AISB/catalog/aisb.t3.012_efficientqat.zh.yaml +159 -0
- package/AISB/catalog/aisb.t3.013_appl.yaml +152 -0
- package/AISB/catalog/aisb.t3.013_appl.zh.yaml +126 -0
- package/AISB/catalog/aisb.t3.014_piguard.yaml +207 -0
- package/AISB/catalog/aisb.t3.014_piguard.zh.yaml +164 -0
- package/AISB/catalog/aisb.t3.015_frspec.yaml +209 -0
- package/AISB/catalog/aisb.t3.015_frspec.zh.yaml +163 -0
- package/AISB/catalog/aisb.t3.016_mathfusion.yaml +166 -0
- package/AISB/catalog/aisb.t3.016_mathfusion.zh.yaml +145 -0
- package/AISB/catalog/aisb.t3.017_multimodalglp.yaml +171 -0
- package/AISB/catalog/aisb.t3.017_multimodalglp.zh.yaml +122 -0
- package/AISB/catalog/aisb.t3.018_cotsynth.yaml +206 -0
- package/AISB/catalog/aisb.t3.018_cotsynth.zh.yaml +162 -0
- package/AISB/catalog/aisb.t3.019_dyscaleut.yaml +211 -0
- package/AISB/catalog/aisb.t3.019_dyscaleut.zh.yaml +148 -0
- package/AISB/catalog/aisb.t3.020_aristotle.yaml +173 -0
- package/AISB/catalog/aisb.t3.020_aristotle.zh.yaml +119 -0
- package/AISB/catalog/aisb.t3.021_tokenrecycling.yaml +160 -0
- package/AISB/catalog/aisb.t3.021_tokenrecycling.zh.yaml +129 -0
- package/AISB/catalog/aisb.t3.022_chainofreasoning.yaml +204 -0
- package/AISB/catalog/aisb.t3.022_chainofreasoning.zh.yaml +161 -0
- package/AISB/catalog/aisb.t3.023_guidedembed.yaml +211 -0
- package/AISB/catalog/aisb.t3.023_guidedembed.zh.yaml +189 -0
- package/AISB/catalog/aisb.t3.024_outputcentric.yaml +148 -0
- package/AISB/catalog/aisb.t3.024_outputcentric.zh.yaml +131 -0
- package/AISB/catalog/aisb.t3.025_deeper.yaml +143 -0
- package/AISB/catalog/aisb.t3.025_deeper.zh.yaml +116 -0
- package/AISB/catalog/aisb.t3.026_gartkg.yaml +195 -0
- package/AISB/catalog/aisb.t3.026_gartkg.zh.yaml +127 -0
- package/AISB/catalog/aisb.t3.027_citeeval.yaml +182 -0
- package/AISB/catalog/aisb.t3.027_citeeval.zh.yaml +135 -0
- package/AISB/catalog/aisb.t3.028_sbam.yaml +206 -0
- package/AISB/catalog/aisb.t3.028_sbam.zh.yaml +166 -0
- package/AISB/catalog/aisb.t3.029_cdqgeoembed.yaml +224 -0
- package/AISB/catalog/aisb.t3.029_cdqgeoembed.zh.yaml +142 -0
- package/AISB/catalog/aisb.t3.030_processrm.yaml +211 -0
- package/AISB/catalog/aisb.t3.030_processrm.zh.yaml +166 -0
- package/AISB/catalog/aisb.t3.031_circuitstability.yaml +172 -0
- package/AISB/catalog/aisb.t3.031_circuitstability.zh.yaml +134 -0
- package/AISB/catalog/aisb.t3.032_ptsolver.yaml +169 -0
- package/AISB/catalog/aisb.t3.032_ptsolver.zh.yaml +135 -0
- package/AISB/catalog/aisb.t3.033_gcse.yaml +144 -0
- package/AISB/catalog/aisb.t3.033_gcse.zh.yaml +126 -0
- package/AISB/catalog/aisb.t3.034_ensemblewm.yaml +183 -0
- package/AISB/catalog/aisb.t3.034_ensemblewm.zh.yaml +146 -0
- package/AISB/catalog/aisb.t3.035_moralvalueswa.yaml +207 -0
- package/AISB/catalog/aisb.t3.035_moralvalueswa.zh.yaml +165 -0
- package/AISB/catalog/aisb.t3.036_weakstrongpref.yaml +210 -0
- package/AISB/catalog/aisb.t3.036_weakstrongpref.zh.yaml +194 -0
- package/AISB/catalog/aisb.t3.037_dementiamask.yaml +172 -0
- package/AISB/catalog/aisb.t3.037_dementiamask.zh.yaml +132 -0
- package/AISB/catalog/aisb.t3.038_tinysam.yaml +284 -0
- package/AISB/catalog/aisb.t3.038_tinysam.zh.yaml +240 -0
- package/AISB/catalog/aisb.t3.039_calf.yaml +224 -0
- package/AISB/catalog/aisb.t3.039_calf.zh.yaml +194 -0
- package/AISB/catalog/aisb.t3.040_graniteguardian.yaml +199 -0
- package/AISB/catalog/aisb.t3.040_graniteguardian.zh.yaml +174 -0
- package/AISB/catalog/aisb.t3.041_amdm.yaml +149 -0
- package/AISB/catalog/aisb.t3.041_amdm.zh.yaml +137 -0
- package/AISB/catalog/aisb.t3.042_xpatch.yaml +216 -0
- package/AISB/catalog/aisb.t3.042_xpatch.zh.yaml +182 -0
- package/AISB/catalog/aisb.t3.043_vhm.yaml +268 -0
- package/AISB/catalog/aisb.t3.043_vhm.zh.yaml +193 -0
- package/AISB/catalog/aisb.t3.044_rgvi.yaml +224 -0
- package/AISB/catalog/aisb.t3.044_rgvi.zh.yaml +176 -0
- package/AISB/catalog/aisb.t3.045_pslstm.yaml +203 -0
- package/AISB/catalog/aisb.t3.045_pslstm.zh.yaml +179 -0
- package/AISB/catalog/aisb.t3.046_nonstatts.yaml +208 -0
- package/AISB/catalog/aisb.t3.046_nonstatts.zh.yaml +194 -0
- package/AISB/catalog/aisb.t3.047_timepfn.yaml +156 -0
- package/AISB/catalog/aisb.t3.047_timepfn.zh.yaml +124 -0
- package/AISB/catalog/aisb.t3.048_proxyspex.yaml +148 -0
- package/AISB/catalog/aisb.t3.048_proxyspex.zh.yaml +125 -0
- package/AISB/catalog/aisb.t3.049_hogwildinference.yaml +183 -0
- package/AISB/catalog/aisb.t3.049_hogwildinference.zh.yaml +138 -0
- package/AISB/catalog/aisb.t3.050_causalpfn.yaml +214 -0
- package/AISB/catalog/aisb.t3.050_causalpfn.zh.yaml +190 -0
- package/AISB/catalog/aisb.t3.051_flashtp.yaml +169 -0
- package/AISB/catalog/aisb.t3.051_flashtp.zh.yaml +124 -0
- package/AISB/catalog/aisb.t3.052_nsdiff.yaml +155 -0
- package/AISB/catalog/aisb.t3.052_nsdiff.zh.yaml +138 -0
- package/AISB/catalog/aisb.t3.053_k2vae.yaml +158 -0
- package/AISB/catalog/aisb.t3.053_k2vae.zh.yaml +132 -0
- package/AISB/catalog/aisb.t3.054_timebase.yaml +178 -0
- package/AISB/catalog/aisb.t3.054_timebase.zh.yaml +158 -0
- package/AISB/catalog/aisb.t3.055_csbrain.yaml +238 -0
- package/AISB/catalog/aisb.t3.055_csbrain.zh.yaml +184 -0
- package/AISB/catalog/aisb.t3.056_infosam.yaml +224 -0
- package/AISB/catalog/aisb.t3.056_infosam.zh.yaml +189 -0
- package/AISB/catalog/aisb.t3.057_mdreid.yaml +129 -0
- package/AISB/catalog/aisb.t3.057_mdreid.zh.yaml +117 -0
- package/AISB/catalog/aisb.t3.058_mindglitch.yaml +171 -0
- package/AISB/catalog/aisb.t3.058_mindglitch.zh.yaml +145 -0
- package/AISB/catalog/aisb.t3.059_selfsupervised.yaml +154 -0
- package/AISB/catalog/aisb.t3.059_selfsupervised.zh.yaml +125 -0
- package/AISB/catalog/aisb.t3.060_iaggad.yaml +121 -0
- package/AISB/catalog/aisb.t3.060_iaggad.zh.yaml +100 -0
- package/AISB/catalog/aisb.t3.061_hsgkn.yaml +136 -0
- package/AISB/catalog/aisb.t3.061_hsgkn.zh.yaml +113 -0
- package/AISB/catalog/aisb.t3.062_visionts.yaml +237 -0
- package/AISB/catalog/aisb.t3.062_visionts.zh.yaml +216 -0
- package/AISB/catalog/aisb.t3.063_tsrag.yaml +162 -0
- package/AISB/catalog/aisb.t3.063_tsrag.zh.yaml +138 -0
- package/AISB/catalog/aisb.t3.064_pir.yaml +221 -0
- package/AISB/catalog/aisb.t3.064_pir.zh.yaml +197 -0
- package/AISB/catalog/aisb.t3.065_proteinbinding.yaml +234 -0
- package/AISB/catalog/aisb.t3.065_proteinbinding.zh.yaml +167 -0
- package/AISB/catalog/aisb.t3.066_tropicalattention.yaml +267 -0
- package/AISB/catalog/aisb.t3.066_tropicalattention.zh.yaml +229 -0
- package/AISB/catalog/aisb.t3.067_kanad.yaml +193 -0
- package/AISB/catalog/aisb.t3.067_kanad.zh.yaml +167 -0
- package/AISB/catalog/aisb.t3.068_sempo.yaml +187 -0
- package/AISB/catalog/aisb.t3.068_sempo.zh.yaml +148 -0
- package/AISB/catalog/aisb.t3.069_treehfd.yaml +129 -0
- package/AISB/catalog/aisb.t3.069_treehfd.zh.yaml +111 -0
- package/AISB/catalog/aisb.t3.070_certifiedunlearning.yaml +224 -0
- package/AISB/catalog/aisb.t3.070_certifiedunlearning.zh.yaml +171 -0
- package/AISB/catalog/aisb.t3.071_neuralmjd.yaml +142 -0
- package/AISB/catalog/aisb.t3.071_neuralmjd.zh.yaml +120 -0
- package/AISB/catalog/aisb.t3.072_fedgmt.yaml +181 -0
- package/AISB/catalog/aisb.t3.072_fedgmt.zh.yaml +158 -0
- package/AISB/catalog/aisb.t3.073_rld.yaml +161 -0
- package/AISB/catalog/aisb.t3.073_rld.zh.yaml +129 -0
- package/AISB/catalog/aisb.t3.074_lsvi.yaml +163 -0
- package/AISB/catalog/aisb.t3.074_lsvi.zh.yaml +129 -0
- package/AISB/catalog/aisb.t3.075_treeslicedentropy.yaml +201 -0
- package/AISB/catalog/aisb.t3.075_treeslicedentropy.zh.yaml +148 -0
- package/AISB/catalog/aisb.t3.076_aanet.yaml +169 -0
- package/AISB/catalog/aisb.t3.076_aanet.zh.yaml +129 -0
- package/AISB/catalog/aisb.t3.077_cmnn.yaml +199 -0
- package/AISB/catalog/aisb.t3.077_cmnn.zh.yaml +165 -0
- package/AISB/catalog/aisb.t3.078_conformalanomaly.yaml +146 -0
- package/AISB/catalog/aisb.t3.078_conformalanomaly.zh.yaml +117 -0
- package/AISB/catalog/aisb.t3.079_dpfkmeans.yaml +131 -0
- package/AISB/catalog/aisb.t3.079_dpfkmeans.zh.yaml +104 -0
- package/AISB/catalog/aisb.t3.080_latentscorereweight.yaml +169 -0
- package/AISB/catalog/aisb.t3.080_latentscorereweight.zh.yaml +123 -0
- package/AISB/catalog/aisb.t3.081_qmamba.yaml +150 -0
- package/AISB/catalog/aisb.t3.081_qmamba.zh.yaml +117 -0
- package/AISB/catalog/aisb.t3.082_onlinellmrouting.yaml +160 -0
- package/AISB/catalog/aisb.t3.082_onlinellmrouting.zh.yaml +133 -0
- package/AISB/catalog/aisb.t3.083_starformer.yaml +178 -0
- package/AISB/catalog/aisb.t3.083_starformer.zh.yaml +140 -0
- package/AISB/catalog/aisb.t3.084_ift.yaml +139 -0
- package/AISB/catalog/aisb.t3.084_ift.zh.yaml +111 -0
- package/AISB/catalog/aisb.t3.085_neuralsurv.yaml +183 -0
- package/AISB/catalog/aisb.t3.085_neuralsurv.zh.yaml +143 -0
- package/AISB/catalog/aisb.t3.086_stella.yaml +197 -0
- package/AISB/catalog/aisb.t3.086_stella.zh.yaml +142 -0
- package/AISB/catalog/aisb.t3.087_moses.yaml +167 -0
- package/AISB/catalog/aisb.t3.087_moses.zh.yaml +132 -0
- package/AISB/catalog/aisb.t3.088_channelnorm.yaml +140 -0
- package/AISB/catalog/aisb.t3.088_channelnorm.zh.yaml +109 -0
- package/AISB/catalog/aisb.t3.089_causalvelocity.yaml +730 -0
- package/AISB/catalog/aisb.t3.089_causalvelocity.zh.yaml +668 -0
- package/AISB/catalog/aisb.t3.090_rstib.yaml +144 -0
- package/AISB/catalog/aisb.t3.090_rstib.zh.yaml +109 -0
- package/AISB/catalog/aisb.t3.091_timeawarecausal.yaml +132 -0
- package/AISB/catalog/aisb.t3.091_timeawarecausal.zh.yaml +107 -0
- package/AISB/catalog/aisb.t3.092_kmeanslocalopt.yaml +138 -0
- package/AISB/catalog/aisb.t3.092_kmeanslocalopt.zh.yaml +110 -0
- package/AISB/catalog/aisb.t3.093_fedwmsam.yaml +134 -0
- package/AISB/catalog/aisb.t3.093_fedwmsam.zh.yaml +106 -0
- package/AISB/catalog/aisb.t3.094_boundre.yaml +147 -0
- package/AISB/catalog/aisb.t3.094_boundre.zh.yaml +114 -0
- package/AISB/catalog/aisb.t3.095_fastfeaturecp.yaml +153 -0
- package/AISB/catalog/aisb.t3.095_fastfeaturecp.zh.yaml +118 -0
- package/AISB/catalog/aisb.t3.096_m3svm.yaml +189 -0
- package/AISB/catalog/aisb.t3.096_m3svm.zh.yaml +149 -0
- package/AISB/catalog/aisb.t3.097_wassersteintl.yaml +212 -0
- package/AISB/catalog/aisb.t3.097_wassersteintl.zh.yaml +169 -0
- package/AISB/catalog/aisb.t3.098_xmahalanobis.yaml +171 -0
- package/AISB/catalog/aisb.t3.098_xmahalanobis.zh.yaml +127 -0
- package/AISB/catalog/aisb.t3.099_ollalanding.yaml +248 -0
- package/AISB/catalog/aisb.t3.099_ollalanding.zh.yaml +182 -0
- package/AISB/catalog/aisb.t3.100_invmissingdata.yaml +179 -0
- package/AISB/catalog/aisb.t3.100_invmissingdata.zh.yaml +150 -0
- package/AISB/catalog/aisb.t3.101_acia.yaml +164 -0
- package/AISB/catalog/aisb.t3.101_acia.zh.yaml +109 -0
- package/AISB/catalog/aisb.t3.102_stochasticff.yaml +178 -0
- package/AISB/catalog/aisb.t3.102_stochasticff.zh.yaml +130 -0
- package/AISB/catalog/aisb.t3.103_qdcp.yaml +150 -0
- package/AISB/catalog/aisb.t3.103_qdcp.zh.yaml +116 -0
- package/AISB/catalog/aisb.t3.104_balancedactiveinf.yaml +137 -0
- package/AISB/catalog/aisb.t3.104_balancedactiveinf.zh.yaml +104 -0
- package/AISB/catalog/aisb.t3.105_binaryclasseval.yaml +161 -0
- package/AISB/catalog/aisb.t3.105_binaryclasseval.zh.yaml +130 -0
- package/AISB/image/001_aisb.t3.001_savvy.jpg +0 -0
- package/AISB/image/002_aisb.t3.002_pinet.jpg +0 -0
- package/AISB/image/003_aisb.t3.003_dmsqd.jpg +0 -0
- package/AISB/image/004_aisb.t3.004_decentralattn.jpg +0 -0
- package/AISB/image/005_aisb.t3.005_tsae.jpg +0 -0
- package/AISB/image/006_aisb.t3.006_physense.jpg +0 -0
- package/AISB/image/007_aisb.t3.007_reasoningiqa.jpg +0 -0
- package/AISB/image/008_aisb.t3.008_meanflows.jpg +0 -0
- package/AISB/image/009_aisb.t3.009_scoremissing.jpg +0 -0
- package/AISB/image/010_aisb.t3.010_suitabilityfilter.jpg +0 -0
- package/AISB/image/011_aisb.t3.011_osd.jpg +0 -0
- package/AISB/image/012_aisb.t3.012_efficientqat.jpg +0 -0
- package/AISB/image/013_aisb.t3.013_appl.jpg +0 -0
- package/AISB/image/014_aisb.t3.014_piguard.jpg +0 -0
- package/AISB/image/015_aisb.t3.015_frspec.jpg +0 -0
- package/AISB/image/016_aisb.t3.016_mathfusion.jpg +0 -0
- package/AISB/image/017_aisb.t3.017_multimodalglp.jpg +0 -0
- package/AISB/image/018_aisb.t3.018_cotsynth.jpg +0 -0
- package/AISB/image/019_aisb.t3.019_dyscaleut.jpg +0 -0
- package/AISB/image/020_aisb.t3.020_aristotle.jpg +0 -0
- package/AISB/image/021_aisb.t3.021_tokenrecycling.jpg +0 -0
- package/AISB/image/022_aisb.t3.022_chainofreasoning.jpg +0 -0
- package/AISB/image/023_aisb.t3.023_guidedembed.jpg +0 -0
- package/AISB/image/024_aisb.t3.024_outputcentric.jpg +0 -0
- package/AISB/image/025_aisb.t3.025_deeper.jpg +0 -0
- package/AISB/image/026_aisb.t3.026_gartkg.jpg +0 -0
- package/AISB/image/027_aisb.t3.027_citeeval.jpg +0 -0
- package/AISB/image/028_aisb.t3.028_sbam.jpg +0 -0
- package/AISB/image/029_aisb.t3.029_cdqgeoembed.jpg +0 -0
- package/AISB/image/030_aisb.t3.030_processrm.jpg +0 -0
- package/AISB/image/031_aisb.t3.031_circuitstability.jpg +0 -0
- package/AISB/image/032_aisb.t3.032_ptsolver.jpg +0 -0
- package/AISB/image/033_aisb.t3.033_gcse.jpg +0 -0
- package/AISB/image/034_aisb.t3.034_ensemblewm.jpg +0 -0
- package/AISB/image/035_aisb.t3.035_moralvalueswa.jpg +0 -0
- package/AISB/image/036_aisb.t3.036_weakstrongpref.jpg +0 -0
- package/AISB/image/037_aisb.t3.037_dementiamask.jpg +0 -0
- package/AISB/image/038_aisb.t3.038_tinysam.jpg +0 -0
- package/AISB/image/039_aisb.t3.039_calf.jpg +0 -0
- package/AISB/image/040_aisb.t3.040_graniteguardian.jpg +0 -0
- package/AISB/image/041_aisb.t3.041_amdm.jpg +0 -0
- package/AISB/image/042_aisb.t3.042_xpatch.jpg +0 -0
- package/AISB/image/043_aisb.t3.043_vhm.jpg +0 -0
- package/AISB/image/044_aisb.t3.044_rgvi.jpg +0 -0
- package/AISB/image/045_aisb.t3.045_pslstm.jpg +0 -0
- package/AISB/image/046_aisb.t3.046_nonstatts.jpg +0 -0
- package/AISB/image/047_aisb.t3.047_timepfn.jpg +0 -0
- package/AISB/image/048_aisb.t3.048_proxyspex.jpg +0 -0
- package/AISB/image/049_aisb.t3.049_hogwildinference.jpg +0 -0
- package/AISB/image/050_aisb.t3.050_causalpfn.jpg +0 -0
- package/AISB/image/051_aisb.t3.051_flashtp.jpg +0 -0
- package/AISB/image/052_aisb.t3.052_nsdiff.jpg +0 -0
- package/AISB/image/053_aisb.t3.053_k2vae.jpg +0 -0
- package/AISB/image/054_aisb.t3.054_timebase.jpg +0 -0
- package/AISB/image/055_aisb.t3.055_csbrain.jpg +0 -0
- package/AISB/image/056_aisb.t3.056_infosam.jpg +0 -0
- package/AISB/image/057_aisb.t3.057_mdreid.jpg +0 -0
- package/AISB/image/058_aisb.t3.058_mindglitch.jpg +0 -0
- package/AISB/image/059_aisb.t3.059_selfsupervised.jpg +0 -0
- package/AISB/image/060_aisb.t3.060_iaggad.jpg +0 -0
- package/AISB/image/061_aisb.t3.061_hsgkn.jpg +0 -0
- package/AISB/image/062_aisb.t3.062_visionts.jpg +0 -0
- package/AISB/image/063_aisb.t3.063_tsrag.jpg +0 -0
- package/AISB/image/064_aisb.t3.064_pir.jpg +0 -0
- package/AISB/image/065_aisb.t3.065_proteinbinding.jpg +0 -0
- package/AISB/image/066_aisb.t3.066_tropicalattention.jpg +0 -0
- package/AISB/image/067_aisb.t3.067_kanad.jpg +0 -0
- package/AISB/image/068_aisb.t3.068_sempo.jpg +0 -0
- package/AISB/image/069_aisb.t3.069_treehfd.jpg +0 -0
- package/AISB/image/070_aisb.t3.070_certifiedunlearning.jpg +0 -0
- package/AISB/image/071_aisb.t3.071_neuralmjd.jpg +0 -0
- package/AISB/image/072_aisb.t3.072_fedgmt.jpg +0 -0
- package/AISB/image/073_aisb.t3.073_rld.jpg +0 -0
- package/AISB/image/074_aisb.t3.074_lsvi.jpg +0 -0
- package/AISB/image/075_aisb.t3.075_treeslicedentropy.jpg +0 -0
- package/AISB/image/076_aisb.t3.076_aanet.jpg +0 -0
- package/AISB/image/077_aisb.t3.077_cmnn.jpg +0 -0
- package/AISB/image/078_aisb.t3.078_conformalanomaly.jpg +0 -0
- package/AISB/image/079_aisb.t3.079_dpfkmeans.jpg +0 -0
- package/AISB/image/080_aisb.t3.080_latentscorereweight.jpg +0 -0
- package/AISB/image/081_aisb.t3.081_qmamba.jpg +0 -0
- package/AISB/image/082_aisb.t3.082_onlinellmrouting.jpg +0 -0
- package/AISB/image/083_aisb.t3.083_starformer.jpg +0 -0
- package/AISB/image/084_aisb.t3.084_ift.jpg +0 -0
- package/AISB/image/085_aisb.t3.085_neuralsurv.jpg +0 -0
- package/AISB/image/086_aisb.t3.086_stella.jpg +0 -0
- package/AISB/image/087_aisb.t3.087_moses.jpg +0 -0
- package/AISB/image/088_aisb.t3.088_channelnorm.jpg +0 -0
- package/AISB/image/089_aisb.t3.089_causalvelocity.jpg +0 -0
- package/AISB/image/090_aisb.t3.090_rstib.jpg +0 -0
- package/AISB/image/091_aisb.t3.091_timeawarecausal.jpg +0 -0
- package/AISB/image/092_aisb.t3.092_kmeanslocalopt.jpg +0 -0
- package/AISB/image/093_aisb.t3.093_fedwmsam.jpg +0 -0
- package/AISB/image/094_aisb.t3.094_boundre.jpg +0 -0
- package/AISB/image/095_aisb.t3.095_fastfeaturecp.jpg +0 -0
- package/AISB/image/096_aisb.t3.096_m3svm.jpg +0 -0
- package/AISB/image/097_aisb.t3.097_wassersteintl.jpg +0 -0
- package/AISB/image/098_aisb.t3.098_xmahalanobis.jpg +0 -0
- package/AISB/image/099_aisb.t3.099_ollalanding.jpg +0 -0
- package/AISB/image/100_aisb.t3.100_invmissingdata.jpg +0 -0
- package/AISB/image/101_aisb.t3.101_acia.jpg +0 -0
- package/AISB/image/102_aisb.t3.102_stochasticff.jpg +0 -0
- package/AISB/image/103_aisb.t3.103_qdcp.jpg +0 -0
- package/AISB/image/104_aisb.t3.104_balancedactiveinf.jpg +0 -0
- package/AISB/image/105_aisb.t3.105_binaryclasseval.jpg +0 -0
- package/AISB/image/106_aisb.t1.reasoning_lite.jpg +0 -0
- package/AISB/image/107_aisb.t2.paper_audit.jpg +0 -0
- package/AISB/image/108_aisb.t3.multi_gpu_search.jpg +0 -0
- package/AISB/image/109_aisb.t3.tdc_admet.jpg +0 -0
- package/AISB/image/aisb.b1.agentic_coding.svg +16 -0
- package/AISB/image/aisb.b10.climate_earth.svg +16 -0
- package/AISB/image/aisb.b11.model_efficiency.svg +16 -0
- package/AISB/image/aisb.b12.embodied_ai.svg +16 -0
- package/AISB/image/aisb.b2.agent_systems.svg +16 -0
- package/AISB/image/aisb.b3.self_evolving_rl.svg +16 -0
- package/AISB/image/aisb.b4.lm_reasoning.svg +16 -0
- package/AISB/image/aisb.b5.math_proof.svg +16 -0
- package/AISB/image/aisb.b6.research_process.svg +16 -0
- package/AISB/image/aisb.b7.multimodal_fusion.svg +16 -0
- package/AISB/image/aisb.b8.lifesci_drug.svg +16 -0
- package/AISB/image/aisb.b9.material_science.svg +16 -0
- package/README.md +132 -11
- package/bin/ds.js +376 -49
- package/docs/en/00_QUICK_START.md +135 -18
- package/docs/en/01_SETTINGS_REFERENCE.md +468 -96
- package/docs/en/02_START_RESEARCH_GUIDE.md +26 -5
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +14 -3
- package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +2 -0
- package/docs/en/05_TUI_GUIDE.md +171 -2
- package/docs/en/07_MEMORY_AND_MCP.md +38 -2
- package/docs/en/09_DOCTOR.md +64 -4
- package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +38 -1
- package/docs/en/11_LICENSE_AND_RISK.md +4 -0
- package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +15 -0
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +9 -0
- package/docs/en/15_CODEX_PROVIDER_SETUP.md +622 -187
- package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +14 -0
- package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +14 -0
- package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +14 -0
- package/docs/en/21_LOCAL_MODEL_BACKENDS_GUIDE.md +105 -2
- package/docs/en/22_BENCHSTORE_YAML_REFERENCE.md +469 -0
- package/docs/en/23_BENCHSTORE_GITHUB_RELEASES_SPEC.md +316 -0
- package/docs/en/24_CLAUDE_CODE_PROVIDER_SETUP.md +469 -0
- package/docs/en/25_OPENCODE_PROVIDER_SETUP.md +653 -0
- package/docs/en/26_CITATION_AND_ATTRIBUTION.md +119 -0
- package/docs/en/27_KIMI_CODE_PROVIDER_SETUP.md +180 -0
- package/docs/en/28_DISCORD_CONNECTOR_GUIDE.md +61 -0
- package/docs/en/29_SLACK_CONNECTOR_GUIDE.md +60 -0
- package/docs/en/30_SETTINGS_CONTROL_CENTER_GUIDE.md +371 -0
- package/docs/en/{19_LOCAL_BROWSER_AUTH.md → 31_LOCAL_BROWSER_AUTH.md} +1 -1
- package/docs/en/32_WINDOWS_WSL2_DEPLOYMENT_GUIDE.md +273 -0
- package/docs/en/33_WORKSPACE_EXPLORER_QA.md +121 -0
- package/docs/en/91_DEVELOPMENT.md +29 -0
- package/docs/en/99_ACKNOWLEDGEMENTS.md +24 -19
- package/docs/en/README.md +44 -7
- package/docs/images/admin/admin-connectors-health-en.png +0 -0
- package/docs/images/admin/admin-controllers-en.png +0 -0
- package/docs/images/admin/admin-diagnostics-en.png +0 -0
- package/docs/images/admin/admin-errors-en.png +0 -0
- package/docs/images/admin/admin-issues-en.png +0 -0
- package/docs/images/admin/admin-logs-en.png +0 -0
- package/docs/images/admin/admin-quest-detail-en.png +0 -0
- package/docs/images/admin/admin-quests-en.png +0 -0
- package/docs/images/admin/admin-repairs-en.png +0 -0
- package/docs/images/admin/admin-runtime-en.png +0 -0
- package/docs/images/admin/admin-search-en.png +0 -0
- package/docs/images/admin/admin-stats-en.png +0 -0
- package/docs/images/admin/admin-summary-en.png +0 -0
- package/docs/images/connectors/connector-discord-en.png +0 -0
- package/docs/images/connectors/connector-feishu-en.png +0 -0
- package/docs/images/connectors/connector-lingzhu-en.png +0 -0
- package/docs/images/connectors/connector-qq-en.png +0 -0
- package/docs/images/connectors/connector-slack-en.png +0 -0
- package/docs/images/connectors/connector-telegram-en.png +0 -0
- package/docs/images/connectors/connector-weixin-en.png +0 -0
- package/docs/images/connectors/connector-whatsapp-en.png +0 -0
- package/docs/images/settings/settings-baselines-en.png +0 -0
- package/docs/images/settings/settings-config-en.png +0 -0
- package/docs/images/settings/settings-connectors-overview-en.png +0 -0
- package/docs/images/settings/settings-deepxiv-en.png +0 -0
- package/docs/images/settings/settings-mcp-servers-en.png +0 -0
- package/docs/images/settings/settings-plugins-en.png +0 -0
- package/docs/images/settings/settings-runners-en.png +0 -0
- package/docs/zh/00_QUICK_START.md +92 -17
- package/docs/zh/01_SETTINGS_REFERENCE.md +219 -98
- package/docs/zh/02_START_RESEARCH_GUIDE.md +26 -5
- package/docs/zh/05_TUI_GUIDE.md +171 -2
- package/docs/zh/07_MEMORY_AND_MCP.md +29 -2
- package/docs/zh/09_DOCTOR.md +39 -4
- package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +24 -1
- package/docs/zh/11_LICENSE_AND_RISK.md +4 -0
- package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +15 -0
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +9 -0
- package/docs/zh/15_CODEX_PROVIDER_SETUP.md +550 -188
- package/docs/zh/21_LOCAL_MODEL_BACKENDS_GUIDE.md +105 -2
- package/docs/zh/22_BENCHSTORE_YAML_REFERENCE.md +459 -0
- package/docs/zh/23_BENCHSTORE_GITHUB_RELEASES_SPEC.md +287 -0
- package/docs/zh/23_CLAUDE_RUNNER_GUIDE.md +103 -0
- package/docs/zh/24_CLAUDE_CODE_PROVIDER_SETUP.md +460 -0
- package/docs/zh/25_OPENCODE_PROVIDER_SETUP.md +660 -0
- package/docs/zh/26_CITATION_AND_ATTRIBUTION.md +102 -0
- package/docs/zh/27_KIMI_CODE_PROVIDER_SETUP.md +51 -0
- package/docs/zh/{19_LOCAL_BROWSER_AUTH.md → 31_LOCAL_BROWSER_AUTH.md} +1 -1
- package/docs/zh/32_WINDOWS_WSL2_DEPLOYMENT_GUIDE.md +264 -0
- package/docs/zh/33_WORKSPACE_EXPLORER_QA.md +127 -0
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +23 -19
- package/docs/zh/README.md +29 -7
- package/install.sh +122 -16
- package/package.json +4 -1
- package/pyproject.toml +2 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/acp/envelope.py +13 -0
- package/src/deepscientist/admin/__init__.py +3 -0
- package/src/deepscientist/admin/charts.py +681 -0
- package/src/deepscientist/admin/logs.py +119 -0
- package/src/deepscientist/admin/repairs.py +217 -0
- package/src/deepscientist/admin/service.py +1310 -0
- package/src/deepscientist/admin/system_info.py +700 -0
- package/src/deepscientist/admin/tasks.py +465 -0
- package/src/deepscientist/admin/tool_metrics.py +600 -0
- package/src/deepscientist/artifact/guidance.py +8 -4
- package/src/deepscientist/artifact/schemas.py +115 -0
- package/src/deepscientist/artifact/service.py +4268 -260
- package/src/deepscientist/bash_exec/monitor.py +30 -3
- package/src/deepscientist/bash_exec/service.py +134 -1
- package/src/deepscientist/benchstore/__init__.py +4 -0
- package/src/deepscientist/benchstore/prompt_builder.py +224 -0
- package/src/deepscientist/benchstore/service.py +1716 -0
- package/src/deepscientist/channels/weixin_ilink.py +8 -1
- package/src/deepscientist/cli.py +92 -17
- package/src/deepscientist/codex_cli_compat.py +2 -2
- package/src/deepscientist/config/models.py +82 -11
- package/src/deepscientist/config/service.py +927 -91
- package/src/deepscientist/connector/weixin_support.py +48 -17
- package/src/deepscientist/daemon/api/handlers.py +697 -210
- package/src/deepscientist/daemon/api/router.py +76 -1
- package/src/deepscientist/daemon/app.py +1054 -51
- package/src/deepscientist/diagnostics/runner_failures.py +147 -0
- package/src/deepscientist/doctor.py +212 -65
- package/src/deepscientist/evidence_packets.py +590 -0
- package/src/deepscientist/home.py +52 -4
- package/src/deepscientist/kimi_cli_compat.py +50 -0
- package/src/deepscientist/latex_runtime.py +2 -2
- package/src/deepscientist/mcp/context.py +2 -0
- package/src/deepscientist/mcp/schemas.py +114 -0
- package/src/deepscientist/mcp/server.py +1566 -126
- package/src/deepscientist/memory/service.py +203 -16
- package/src/deepscientist/process_control.py +8 -1
- package/src/deepscientist/prompts/builder.py +836 -92
- package/src/deepscientist/quest/__init__.py +2 -2
- package/src/deepscientist/quest/layout.py +12 -1
- package/src/deepscientist/quest/node_traces.py +10 -0
- package/src/deepscientist/quest/service.py +1430 -139
- package/src/deepscientist/quest/stage_views.py +1 -1
- package/src/deepscientist/runners/__init__.py +18 -0
- package/src/deepscientist/runners/base.py +89 -1
- package/src/deepscientist/runners/builtins.py +13 -1
- package/src/deepscientist/runners/claude.py +391 -0
- package/src/deepscientist/runners/codex.py +421 -21
- package/src/deepscientist/runners/codex_telemetry.py +127 -0
- package/src/deepscientist/runners/kimi.py +334 -0
- package/src/deepscientist/runners/metadata.py +68 -0
- package/src/deepscientist/runners/opencode.py +414 -0
- package/src/deepscientist/runners/runtime_overrides.py +100 -0
- package/src/deepscientist/runners/simple_cli.py +538 -0
- package/src/deepscientist/runtime_storage.py +303 -0
- package/src/deepscientist/shared.py +61 -16
- package/src/deepscientist/skills/installer.py +37 -0
- package/src/deepscientist/skills/registry.py +2 -0
- package/src/deepscientist/tinytex.py +2 -2
- package/src/deepscientist/tui.py +10 -3
- package/src/prompts/benchstore/system.md +77 -0
- package/src/prompts/connectors/qq.md +33 -2
- package/src/prompts/connectors/weixin.md +208 -23
- package/src/prompts/contracts/admin_ops.md +74 -0
- package/src/prompts/contracts/admin_ops_knowledge.md +138 -0
- package/src/prompts/contracts/shared_interaction.md +5 -11
- package/src/prompts/start_setup/system.md +422 -0
- package/src/prompts/system.md +409 -315
- package/src/prompts/system_copilot.md +88 -12
- package/src/skills/analysis-campaign/SKILL.md +239 -578
- package/src/skills/analysis-campaign/references/artifact-flow-examples.md +102 -0
- package/src/skills/analysis-campaign/references/boundary-cases.md +98 -0
- package/src/skills/analysis-campaign/references/campaign-checklist-template.md +39 -24
- package/src/skills/analysis-campaign/references/campaign-design.md +26 -10
- package/src/skills/analysis-campaign/references/campaign-plan-template.md +53 -54
- package/src/skills/analysis-campaign/references/operational-guidance.md +97 -0
- package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +10 -20
- package/src/skills/baseline/SKILL.md +183 -461
- package/src/skills/baseline/references/artifact-flow-examples.md +106 -0
- package/src/skills/baseline/references/artifact-payload-examples.md +1 -1
- package/src/skills/baseline/references/baseline-checklist-template.md +27 -35
- package/src/skills/baseline/references/baseline-plan-template.md +37 -76
- package/src/skills/baseline/references/boundary-cases.md +86 -0
- package/src/skills/baseline/references/codebase-audit-checklist.md +2 -6
- package/src/skills/baseline/references/comparability-contract.md +7 -12
- package/src/skills/baseline/references/operational-guidance.md +56 -0
- package/src/skills/baseline/references/route-selection.md +5 -25
- package/src/skills/decision/SKILL.md +113 -306
- package/src/skills/decision/references/checkpoint-memory-template.md +47 -0
- package/src/skills/decision/references/operational-guidance.md +94 -0
- package/src/skills/decision/references/research-route-criteria.md +7 -8
- package/src/skills/decision/references/strategic-decision-template.md +13 -26
- package/src/skills/experiment/SKILL.md +132 -670
- package/src/skills/experiment/references/execution-playbook.md +374 -0
- package/src/skills/experiment/references/main-experiment-checklist-template.md +26 -2
- package/src/skills/experiment/references/main-experiment-plan-template.md +28 -17
- package/src/skills/experiment/references/operational-guidance.md +108 -0
- package/src/skills/finalize/SKILL.md +62 -0
- package/src/skills/finalize/references/checkpoint-memory-template.md +49 -0
- package/src/skills/finalize/references/resume-packet-template.md +7 -0
- package/src/skills/idea/SKILL.md +228 -15
- package/src/skills/idea/references/controlled-brainstorming-playbook.md +78 -0
- package/src/skills/idea/references/current-board-packet-template.md +61 -0
- package/src/skills/idea/references/high-value-idea-sourcing.md +119 -0
- package/src/skills/idea/references/idea-generation-playbook.md +21 -0
- package/src/skills/idea/references/idea-thinking-flow.md +6 -0
- package/src/skills/idea/references/literature-survey-template.md +3 -0
- package/src/skills/idea/references/objective-contract-template.md +54 -0
- package/src/skills/idea/references/outline-seeding-example.md +56 -0
- package/src/skills/idea/references/pre-idea-draft-template.md +105 -0
- package/src/skills/idea/references/related-work-playbook.md +75 -2
- package/src/skills/idea/references/research-history-playbook.md +114 -0
- package/src/skills/idea/references/selection-gate.md +58 -6
- package/src/skills/intake-audit/SKILL.md +43 -2
- package/src/skills/intake-audit/references/state-audit-template.md +10 -0
- package/src/skills/nature-data/SKILL.md +128 -0
- package/src/skills/nature-data/UPSTREAM_LICENSE.txt +21 -0
- package/src/skills/nature-data/agents/openai.yaml +4 -0
- package/src/skills/nature-data/references/chinese-author-alignment.md +84 -0
- package/src/skills/nature-data/references/fair-metadata-checklist.md +105 -0
- package/src/skills/nature-data/references/policy-principles.md +103 -0
- package/src/skills/nature-data/references/repository-and-identifiers.md +96 -0
- package/src/skills/nature-data/references/source-basis.md +54 -0
- package/src/skills/nature-data/references/statement-patterns.md +153 -0
- package/src/skills/nature-figure/SKILL.md +197 -0
- package/src/skills/nature-figure/UPSTREAM_LICENSE.txt +21 -0
- package/src/skills/nature-figure/agents/openai.yaml +4 -0
- package/src/skills/nature-figure/evals/evals.json +37 -0
- package/src/skills/nature-figure/references/api.md +428 -0
- package/src/skills/nature-figure/references/backend-selection.md +100 -0
- package/src/skills/nature-figure/references/chart-types.md +281 -0
- package/src/skills/nature-figure/references/common-patterns.md +349 -0
- package/src/skills/nature-figure/references/design-theory.md +436 -0
- package/src/skills/nature-figure/references/figure-contract.md +93 -0
- package/src/skills/nature-figure/references/nature-2026-observations.md +112 -0
- package/src/skills/nature-figure/references/qa-contract.md +119 -0
- package/src/skills/nature-figure/references/r-template-index.md +66 -0
- package/src/skills/nature-figure/references/r-workflow.md +161 -0
- package/src/skills/nature-figure/references/tutorials.md +250 -0
- package/src/skills/nature-paper2ppt/SKILL.md +507 -0
- package/src/skills/nature-paper2ppt/UPSTREAM_LICENSE.txt +21 -0
- package/src/skills/nature-paper2ppt/agents/openai.yaml +4 -0
- package/src/skills/nature-polishing/SKILL.md +385 -0
- package/src/skills/nature-polishing/UPSTREAM_LICENSE.txt +21 -0
- package/src/skills/nature-polishing/agents/openai.yaml +4 -0
- package/src/skills/nature-polishing/references/phrasebank-playbook.md +162 -0
- package/src/skills/nature-polishing/references/section-moves.md +240 -0
- package/src/skills/nature-polishing/references/style-guardrails.md +94 -0
- package/src/skills/nature-polishing/references/writing-strategy.md +148 -0
- package/src/skills/optimize/SKILL.md +177 -1568
- package/src/skills/optimize/references/brief-shaping-playbook.md +95 -0
- package/src/skills/optimize/references/candidate-board-template.md +13 -0
- package/src/skills/optimize/references/candidate-ranking-template.md +51 -0
- package/src/skills/optimize/references/codegen-route-playbook.md +50 -0
- package/src/skills/optimize/references/debug-response-template.md +29 -0
- package/src/skills/optimize/references/frontier-review-template.md +32 -0
- package/src/skills/optimize/references/fusion-playbook.md +36 -0
- package/src/skills/optimize/references/method-brief-template.md +73 -0
- package/src/skills/optimize/references/operational-guidance.md +621 -0
- package/src/skills/optimize/references/optimization-memory-template.md +30 -0
- package/src/skills/optimize/references/optimize-checklist-template.md +18 -0
- package/src/skills/optimize/references/plateau-response-playbook.md +28 -0
- package/src/skills/optimize/references/prompt-patterns.md +49 -0
- package/src/skills/paper-outline/SKILL.md +227 -0
- package/src/skills/paper-outline/references/outline-patterns.md +87 -0
- package/src/skills/paper-plot/SKILL.md +79 -0
- package/src/skills/paper-plot/agents/openai.yaml +4 -0
- package/src/skills/paper-plot/references/bar_grouped_hatch.md +96 -0
- package/src/skills/paper-plot/references/bar_paired_delta.md +72 -0
- package/src/skills/paper-plot/references/line_confidence_band.md +75 -0
- package/src/skills/paper-plot/references/line_loss_with_inset.md +65 -0
- package/src/skills/paper-plot/references/line_training_curve.md +44 -0
- package/src/skills/paper-plot/references/radar_dual_series.md +59 -0
- package/src/skills/paper-plot/references/scatter_broken_axis.md +59 -0
- package/src/skills/paper-plot/references/scatter_tsne_cluster.md +72 -0
- package/src/skills/paper-plot/scripts/bar_memevolve.py +109 -0
- package/src/skills/paper-plot/scripts/bar_spice.py +166 -0
- package/src/skills/paper-plot/scripts/line_aime.py +94 -0
- package/src/skills/paper-plot/scripts/line_loss_inset.py +157 -0
- package/src/skills/paper-plot/scripts/line_selfdistill.py +168 -0
- package/src/skills/paper-plot/scripts/radar_dora.py +151 -0
- package/src/skills/paper-plot/scripts/scatter_break.py +169 -0
- package/src/skills/paper-plot/scripts/scatter_tsne.py +133 -0
- package/src/skills/rebuttal/SKILL.md +9 -0
- package/src/skills/references/tool-usage-by-stage.md +438 -0
- package/src/skills/review/SKILL.md +105 -7
- package/src/skills/science/PROVENANCE.md +44 -0
- package/src/skills/science/SKILL.md +137 -0
- package/src/skills/science/references/artifact-science-tool.md +110 -0
- package/src/skills/science/references/claim-type-discipline.md +56 -0
- package/src/skills/science/references/domain-index.md +422 -0
- package/src/skills/science/references/hpc-via-bash-exec.md +42 -0
- package/src/skills/science/references/package-check-playbook.md +64 -0
- package/src/skills/science/references/package-index.min.json +3616 -0
- package/src/skills/science/references/packages/abinit.md +80 -0
- package/src/skills/science/references/packages/acts.md +73 -0
- package/src/skills/science/references/packages/aiida-core.md +80 -0
- package/src/skills/science/references/packages/alamode.md +80 -0
- package/src/skills/science/references/packages/amuse.md +88 -0
- package/src/skills/science/references/packages/anndata.md +88 -0
- package/src/skills/science/references/packages/arbor.md +80 -0
- package/src/skills/science/references/packages/arc.md +73 -0
- package/src/skills/science/references/packages/astropy.md +88 -0
- package/src/skills/science/references/packages/astroquery.md +88 -0
- package/src/skills/science/references/packages/atomate2.md +80 -0
- package/src/skills/science/references/packages/atomsmltr.md +73 -0
- package/src/skills/science/references/packages/awkward.md +73 -0
- package/src/skills/science/references/packages/batman.md +88 -0
- package/src/skills/science/references/packages/biopython.md +88 -0
- package/src/skills/science/references/packages/bloqade.md +73 -0
- package/src/skills/science/references/packages/brian2.md +73 -0
- package/src/skills/science/references/packages/bullet3.md +73 -0
- package/src/skills/science/references/packages/calculix.md +80 -0
- package/src/skills/science/references/packages/cantera.md +73 -0
- package/src/skills/science/references/packages/cavity-md-ipi.md +80 -0
- package/src/skills/science/references/packages/ccdproc.md +88 -0
- package/src/skills/science/references/packages/celerite2.md +88 -0
- package/src/skills/science/references/packages/cellrank.md +73 -0
- package/src/skills/science/references/packages/cesm.md +80 -0
- package/src/skills/science/references/packages/chemicals.md +73 -0
- package/src/skills/science/references/packages/chempy.md +73 -0
- package/src/skills/science/references/packages/cirq.md +73 -0
- package/src/skills/science/references/packages/coffea.md +73 -0
- package/src/skills/science/references/packages/cp2k.md +88 -0
- package/src/skills/science/references/packages/custodian.md +80 -0
- package/src/skills/science/references/packages/dart.md +73 -0
- package/src/skills/science/references/packages/datamol.md +88 -0
- package/src/skills/science/references/packages/dd4hep.md +73 -0
- package/src/skills/science/references/packages/dealii.md +80 -0
- package/src/skills/science/references/packages/deepchem.md +88 -0
- package/src/skills/science/references/packages/delphes.md +73 -0
- package/src/skills/science/references/packages/devito.md +80 -0
- package/src/skills/science/references/packages/dftb.md +88 -0
- package/src/skills/science/references/packages/dftd4.md +88 -0
- package/src/skills/science/references/packages/dftk-jl.md +80 -0
- package/src/skills/science/references/packages/dolfinx.md +80 -0
- package/src/skills/science/references/packages/drake.md +73 -0
- package/src/skills/science/references/packages/dumux.md +73 -0
- package/src/skills/science/references/packages/elk.md +80 -0
- package/src/skills/science/references/packages/elmerfem.md +80 -0
- package/src/skills/science/references/packages/enzo-e.md +88 -0
- package/src/skills/science/references/packages/espresso.md +80 -0
- package/src/skills/science/references/packages/exoplanet.md +88 -0
- package/src/skills/science/references/packages/fairroot.md +73 -0
- package/src/skills/science/references/packages/fbpic.md +80 -0
- package/src/skills/science/references/packages/fdtdbath-meep.md +80 -0
- package/src/skills/science/references/packages/geant4.md +73 -0
- package/src/skills/science/references/packages/geosx.md +80 -0
- package/src/skills/science/references/packages/gprmax.md +80 -0
- package/src/skills/science/references/packages/gromacs.md +80 -0
- package/src/skills/science/references/packages/gwaslab.md +73 -0
- package/src/skills/science/references/packages/gz-sim.md +73 -0
- package/src/skills/science/references/packages/hail.md +88 -0
- package/src/skills/science/references/packages/hiphive.md +80 -0
- package/src/skills/science/references/packages/hoomd-blue.md +80 -0
- package/src/skills/science/references/packages/itensor.md +73 -0
- package/src/skills/science/references/packages/itensors-jl.md +73 -0
- package/src/skills/science/references/packages/jdftx.md +73 -0
- package/src/skills/science/references/packages/jobflow.md +80 -0
- package/src/skills/science/references/packages/kadanoffbaym-jl.md +73 -0
- package/src/skills/science/references/packages/kite.md +80 -0
- package/src/skills/science/references/packages/kratos.md +80 -0
- package/src/skills/science/references/packages/kwant.md +73 -0
- package/src/skills/science/references/packages/lammps.md +80 -0
- package/src/skills/science/references/packages/lightkurve.md +88 -0
- package/src/skills/science/references/packages/limix.md +73 -0
- package/src/skills/science/references/packages/maxwelllink.md +80 -0
- package/src/skills/science/references/packages/mcdc.md +73 -0
- package/src/skills/science/references/packages/meep.md +80 -0
- package/src/skills/science/references/packages/mfem.md +80 -0
- package/src/skills/science/references/packages/mitgcm.md +73 -0
- package/src/skills/science/references/packages/modflow6.md +73 -0
- package/src/skills/science/references/packages/molecool.md +73 -0
- package/src/skills/science/references/packages/mom6.md +73 -0
- package/src/skills/science/references/packages/moose.md +80 -0
- package/src/skills/science/references/packages/mpas-model.md +73 -0
- package/src/skills/science/references/packages/mujoco.md +73 -0
- package/src/skills/science/references/packages/mumax3.md +73 -0
- package/src/skills/science/references/packages/nekrs.md +80 -0
- package/src/skills/science/references/packages/nessi.md +73 -0
- package/src/skills/science/references/packages/nest-simulator.md +73 -0
- package/src/skills/science/references/packages/netket.md +73 -0
- package/src/skills/science/references/packages/neuron.md +73 -0
- package/src/skills/science/references/packages/nextflow.md +88 -0
- package/src/skills/science/references/packages/nwchem.md +88 -0
- package/src/skills/science/references/packages/openbabel.md +88 -0
- package/src/skills/science/references/packages/openems.md +80 -0
- package/src/skills/science/references/packages/openff-toolkit.md +88 -0
- package/src/skills/science/references/packages/openfoam-dev.md +80 -0
- package/src/skills/science/references/packages/openmc.md +73 -0
- package/src/skills/science/references/packages/openmm.md +80 -0
- package/src/skills/science/references/packages/openmoc.md +73 -0
- package/src/skills/science/references/packages/openmx.md +80 -0
- package/src/skills/science/references/packages/opensees.md +80 -0
- package/src/skills/science/references/packages/opensn.md +80 -0
- package/src/skills/science/references/packages/opm-simulators.md +73 -0
- package/src/skills/science/references/packages/oqupy.md +73 -0
- package/src/skills/science/references/packages/packmol.md +80 -0
- package/src/skills/science/references/packages/palabos.md +80 -0
- package/src/skills/science/references/packages/parflow.md +80 -0
- package/src/skills/science/references/packages/pennylane.md +88 -0
- package/src/skills/science/references/packages/perceval.md +73 -0
- package/src/skills/science/references/packages/phono3py.md +73 -0
- package/src/skills/science/references/packages/phonopy.md +73 -0
- package/src/skills/science/references/packages/photutils.md +88 -0
- package/src/skills/science/references/packages/picongpu.md +80 -0
- package/src/skills/science/references/packages/plink-ng.md +88 -0
- package/src/skills/science/references/packages/precice.md +73 -0
- package/src/skills/science/references/packages/psc.md +80 -0
- package/src/skills/science/references/packages/psi4.md +88 -0
- package/src/skills/science/references/packages/pybinding.md +73 -0
- package/src/skills/science/references/packages/pyfr.md +80 -0
- package/src/skills/science/references/packages/pyhf.md +73 -0
- package/src/skills/science/references/packages/pyiron_base.md +80 -0
- package/src/skills/science/references/packages/pylcp.md +73 -0
- package/src/skills/science/references/packages/pylith.md +80 -0
- package/src/skills/science/references/packages/pynbody.md +88 -0
- package/src/skills/science/references/packages/pysam.md +88 -0
- package/src/skills/science/references/packages/pyscf.md +88 -0
- package/src/skills/science/references/packages/q-e.md +73 -0
- package/src/skills/science/references/packages/qibo.md +73 -0
- package/src/skills/science/references/packages/qiskit.md +73 -0
- package/src/skills/science/references/packages/quantica-jl.md +73 -0
- package/src/skills/science/references/packages/quantumoptics-jl.md +73 -0
- package/src/skills/science/references/packages/quimb.md +73 -0
- package/src/skills/science/references/packages/qulacs.md +73 -0
- package/src/skills/science/references/packages/qutip.md +73 -0
- package/src/skills/science/references/packages/rdkit.md +88 -0
- package/src/skills/science/references/packages/rmg-py.md +73 -0
- package/src/skills/science/references/packages/root.md +73 -0
- package/src/skills/science/references/packages/scanpy.md +88 -0
- package/src/skills/science/references/packages/scikit-allel.md +88 -0
- package/src/skills/science/references/packages/scikit-bio.md +88 -0
- package/src/skills/science/references/packages/scqubits.md +73 -0
- package/src/skills/science/references/packages/scuff-em.md +80 -0
- package/src/skills/science/references/packages/scvi-tools.md +73 -0
- package/src/skills/science/references/packages/seissol.md +73 -0
- package/src/skills/science/references/packages/sfepy.md +80 -0
- package/src/skills/science/references/packages/sisl.md +73 -0
- package/src/skills/science/references/packages/smilei.md +80 -0
- package/src/skills/science/references/packages/snakemake.md +88 -0
- package/src/skills/science/references/packages/specfem3d-globe.md +80 -0
- package/src/skills/science/references/packages/specutils.md +88 -0
- package/src/skills/science/references/packages/spglib.md +80 -0
- package/src/skills/science/references/packages/squidpy.md +88 -0
- package/src/skills/science/references/packages/starry.md +88 -0
- package/src/skills/science/references/packages/strawberryfields.md +73 -0
- package/src/skills/science/references/packages/su2.md +80 -0
- package/src/skills/science/references/packages/sunny-jl.md +73 -0
- package/src/skills/science/references/packages/sw4.md +73 -0
- package/src/skills/science/references/packages/swift.md +88 -0
- package/src/skills/science/references/packages/tdnegf.md +73 -0
- package/src/skills/science/references/packages/tenpy.md +73 -0
- package/src/skills/science/references/packages/thermo.md +73 -0
- package/src/skills/science/references/packages/tkwant.md +73 -0
- package/src/skills/science/references/packages/tvb-root.md +73 -0
- package/src/skills/science/references/packages/uproot5.md +73 -0
- package/src/skills/science/references/packages/vampire.md +80 -0
- package/src/skills/science/references/packages/wannier_tools.md +73 -0
- package/src/skills/science/references/packages/warpx.md +80 -0
- package/src/skills/science/references/packages/wrf.md +73 -0
- package/src/skills/science/references/packages/xtb.md +88 -0
- package/src/skills/science/references/packages/yt.md +73 -0
- package/src/skills/science/references/science-task-brief-template.md +71 -0
- package/src/skills/scout/SKILL.md +83 -425
- package/src/skills/scout/references/literature-scout-template.md +5 -24
- package/src/skills/scout/references/operational-guidance.md +191 -0
- package/src/skills/scout/references/paper-triage-playbook.md +11 -35
- package/src/skills/write/SKILL.md +744 -1246
- package/src/skills/write/references/experiments_analysis_patterns.md +129 -0
- package/src/skills/write/references/oral_package_patterns.md +252 -0
- package/src/skills/write/references/oral_writing_principles.md +291 -0
- package/src/skills/write/references/section_rewrite_checklist.md +234 -0
- package/src/tui/dist/app/AppContainer.js +1314 -27
- package/src/tui/dist/components/Composer.js +26 -1
- package/src/tui/dist/components/ConfigScreen.js +2 -1
- package/src/tui/dist/components/InputPrompt.js +25 -9
- package/src/tui/dist/components/MainContent.js +18 -3
- package/src/tui/dist/components/QuestScreen.js +3 -2
- package/src/tui/dist/components/UtilityScreen.js +37 -0
- package/src/tui/dist/hooks/useSafeInput.js +10 -0
- package/src/tui/dist/index.js +13 -1
- package/src/tui/dist/layouts/DefaultAppLayout.js +11 -8
- package/src/tui/dist/lib/api.js +89 -1
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AnalysisPlugin-BCKAfjba.js → AnalysisPlugin-CA94NGmI.js} +1 -1
- package/src/ui/dist/assets/CliPlugin-DHBzphZU.js +79 -0
- package/src/ui/dist/assets/CodeEditorPlugin-BOFwD2rn.js +2 -0
- package/src/ui/dist/assets/{CodeViewerPlugin-CbaFRrUU.js → CodeViewerPlugin-CqDpgjik.js} +4 -4
- package/src/ui/dist/assets/{DocViewerPlugin-DAjLVeQD.js → DocViewerPlugin-UDBgt8-4.js} +3 -3
- package/src/ui/dist/assets/GitCommitViewerPlugin-BmHtZ0bZ.js +6 -0
- package/src/ui/dist/assets/{GitDiffViewerPlugin-CQACjoAA.js → GitDiffViewerPlugin-CAxjNorQ.js} +2 -2
- package/src/ui/dist/assets/{GitSnapshotViewer-0r4nLPke.js → GitSnapshotViewer-CweA6VON.js} +2 -2
- package/src/ui/dist/assets/{ImageViewerPlugin-nBOmI2v_.js → ImageViewerPlugin-C8wHGvGN.js} +5 -5
- package/src/ui/dist/assets/LabPlugin-COyyLUol.js +32 -0
- package/src/ui/dist/assets/{LatexPlugin-ZwtV8pIp.js → LatexPlugin-BQjAaA5J.js} +4 -4
- package/src/ui/dist/assets/{MarkdownViewerPlugin-DKqVfKyW.js → MarkdownViewerPlugin-Dy1NE2dI.js} +3 -3
- package/src/ui/dist/assets/{MarketplacePlugin-BwxStZ9D.js → MarketplacePlugin-DMIZtEJ2.js} +2 -2
- package/src/ui/dist/assets/NotebookEditor-CFHMq_Qt.js +91 -0
- package/src/ui/dist/assets/{NotebookEditor-DB9N_T9q.js → NotebookEditor-WFyd8Ybt.js} +3 -3
- package/src/ui/dist/assets/{PdfLoader-eWBONbQP.js → PdfLoader-CLE5u5TS.js} +3 -3
- package/src/ui/dist/assets/{PdfMarkdownPlugin-D22YOZL3.js → PdfMarkdownPlugin-_iNK_H83.js} +1 -1
- package/src/ui/dist/assets/PdfViewerPlugin-DgWsbInT.js +22 -0
- package/src/ui/dist/assets/SearchPlugin-DrZmn5iw.js +11 -0
- package/src/ui/dist/assets/{TextViewerPlugin-C5xqeeUH.js → TextViewerPlugin-D1-T3aC7.js} +4 -4
- package/src/ui/dist/assets/branding/runner-claude.svg +107 -0
- package/src/ui/dist/assets/branding/runner-codex.svg +10 -0
- package/src/ui/dist/assets/branding/runner-kimi.svg +14 -0
- package/src/ui/dist/assets/branding/runner-opencode.svg +7 -0
- package/src/ui/dist/assets/cli-store-CoZ-x5Ip.js +1 -0
- package/src/ui/dist/assets/{code-WlFHE7z_.js → code-DbsmSd3Y.js} +1 -1
- package/src/ui/dist/assets/file-diff-panel-DsvyRz47.js +1 -0
- package/src/ui/dist/assets/{wrap-text-BC-Hltpd.js → file-jump-queue-DeQBikaw.js} +3 -3
- package/src/ui/dist/assets/{file-socket-CfQPKQKj.js → file-socket-DA5XIx88.js} +1 -1
- package/src/ui/dist/assets/fonts/ds-fonts.css +50 -4
- package/src/ui/dist/assets/images/deepxiv/register-guide.png +0 -0
- package/src/ui/dist/assets/index-39vY9LmZ.js +1 -0
- package/src/ui/dist/assets/{index-CwNu1aH4.js → index-BsO46tJA.js} +1 -1
- package/src/ui/dist/assets/index-CHzJ2xtB.js +3530 -0
- package/src/ui/dist/assets/index-DH-zxoZ3.css +33 -0
- package/src/ui/dist/assets/{plugin-notebook-HbW2K-1c.js → plugin-notebook-JRhysCqj.js} +2 -2
- package/src/ui/dist/assets/{project-sync-C9IdzdZW.js → project-sync-DPmWKmKD.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-E_gaeAxL.js → zoom-out-DAukFWen.js} +3 -3
- package/src/ui/dist/index.html +3 -3
- package/src/skills/analysis-campaign/references/artifact-orchestration.md +0 -58
- package/src/skills/baseline/references/memory-playbook.md +0 -40
- package/src/skills/baseline/references/publishable-baseline-package.md +0 -30
- package/src/skills/write/references/outline-evidence-contract-example.md +0 -107
- package/src/skills/write/references/paper-experiment-matrix-template.md +0 -131
- package/src/skills/write/references/paper-section-playbook.md +0 -64
- package/src/skills/write/references/reviewer-first-writing.md +0 -64
- package/src/skills/write/references/revision-checklist.md +0 -70
- package/src/skills/write/references/section-contracts.md +0 -82
- package/src/skills/write/references/sentence-level-proofing.md +0 -49
- package/src/ui/dist/assets/AiManusChatView-Bv-Z8YpU.js +0 -204
- package/src/ui/dist/assets/CliPlugin-BCKcpc35.js +0 -109
- package/src/ui/dist/assets/CodeEditorPlugin-DbOfSJ8K.js +0 -2
- package/src/ui/dist/assets/GitCommitViewerPlugin-CIUqbUDO.js +0 -1
- package/src/ui/dist/assets/LabCopilotPanel-BHxOxF4z.js +0 -14
- package/src/ui/dist/assets/LabPlugin-BKoZGs95.js +0 -22
- package/src/ui/dist/assets/NotebookEditor-BEQhaQbt.js +0 -81
- package/src/ui/dist/assets/PdfViewerPlugin-c-RK9DLM.js +0 -17
- package/src/ui/dist/assets/SearchPlugin-CxF9ytAx.js +0 -16
- package/src/ui/dist/assets/VNCViewer-BoLGLnHz.js +0 -11
- package/src/ui/dist/assets/bot-DREQOxzP.js +0 -6
- package/src/ui/dist/assets/chevron-up-C9Qpx4DE.js +0 -6
- package/src/ui/dist/assets/file-content-BZMz3RYp.js +0 -1
- package/src/ui/dist/assets/file-diff-panel-CQhw0jS2.js +0 -1
- package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +0 -1
- package/src/ui/dist/assets/git-commit-horizontal-DxZ8DCZh.js +0 -6
- package/src/ui/dist/assets/image-Bgl4VIyx.js +0 -6
- package/src/ui/dist/assets/index-BpV6lusQ.css +0 -33
- package/src/ui/dist/assets/index-CBNVuWcP.js +0 -2496
- package/src/ui/dist/assets/index-DrUnlf6K.js +0 -1
- package/src/ui/dist/assets/index-NW-h8VzN.js +0 -1
- package/src/ui/dist/assets/pdf-effect-queue-J8OnM0jE.js +0 -6
- package/src/ui/dist/assets/popover-CLc0pPP8.js +0 -1
- package/src/ui/dist/assets/select-Cs2PmzwL.js +0 -11
- package/src/ui/dist/assets/sigma-ClKcHAXm.js +0 -6
- package/src/ui/dist/assets/trash-DwpbFr3w.js +0 -11
- package/src/ui/dist/assets/useCliAccess-NQ8m0Let.js +0 -1
- package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +0 -1
|
@@ -32,6 +32,7 @@ from ..skills import SkillInstaller
|
|
|
32
32
|
from ..web_search import extract_web_search_payload
|
|
33
33
|
from .layout import (
|
|
34
34
|
QUEST_DIRECTORIES,
|
|
35
|
+
default_active_anchor,
|
|
35
36
|
gitignore,
|
|
36
37
|
initial_brief,
|
|
37
38
|
initial_plan,
|
|
@@ -44,21 +45,34 @@ from .stage_views import QuestStageViewBuilder
|
|
|
44
45
|
|
|
45
46
|
_UNSET = object()
|
|
46
47
|
_NUMERIC_QUEST_ID_PATTERN = re.compile(r"^\d{1,10}$")
|
|
48
|
+
_SYSTEM_NUMERIC_QUEST_ID_PATTERN = re.compile(r"^(?P<prefix>[SB])-(?P<number>\d{1,10})$", re.IGNORECASE)
|
|
47
49
|
_MAX_NUMERIC_QUEST_ID_VALUE = 9_999_999_999
|
|
48
50
|
_NUMERIC_QUEST_ID_PAD_WIDTH = 3
|
|
49
51
|
_CRASH_AUTO_RESUME_WINDOW = timedelta(hours=24)
|
|
50
52
|
_JSONL_CACHE_MAX_BYTES = 4 * 1024 * 1024
|
|
51
53
|
_CODEX_HISTORY_TAIL_LIMIT = 400
|
|
52
54
|
_JSONL_STREAM_CHUNK_BYTES = 64 * 1024
|
|
55
|
+
_JSONL_LINE_COUNT_CACHE_SUFFIX = ".linecount.json"
|
|
53
56
|
_EVENTS_OVERSIZED_LINE_BYTES = 8 * 1024 * 1024
|
|
54
57
|
_OVERSIZED_EVENT_PREFIX_BYTES = 4096
|
|
55
58
|
_PROJECTION_SCHEMA_VERSION = 1
|
|
56
59
|
_PROJECTION_BUILD_TOTAL_STEPS = 3
|
|
57
60
|
_PROJECTION_REFRESH_THROTTLE_SECONDS = 1.0
|
|
61
|
+
_SHARED_MEMORY_DOCUMENT_PREFIX = "sharedmemory::"
|
|
58
62
|
_EVENT_TYPE_BYTES_RE = re.compile(rb'"(?:type|event_type)"\s*:\s*"([^"]+)"')
|
|
59
63
|
_EVENT_TOOL_NAME_BYTES_RE = re.compile(rb'"tool_name"\s*:\s*"([^"]+)"')
|
|
60
64
|
_EVENT_RUN_ID_BYTES_RE = re.compile(rb'"run_id"\s*:\s*"([^"]+)"')
|
|
61
65
|
CONTINUATION_POLICIES = {"auto", "when_external_progress", "wait_for_user_or_resume", "none"}
|
|
66
|
+
AUTONOMOUS_BLOCKING_WAIT_REASONS = {
|
|
67
|
+
"completion_approval",
|
|
68
|
+
"credential_required",
|
|
69
|
+
"privacy_or_data_export_boundary",
|
|
70
|
+
"large_cost_or_external_paid_api",
|
|
71
|
+
"user_gated_decision_request",
|
|
72
|
+
}
|
|
73
|
+
_CHAT_ATTACHMENT_TEXT_EXTENSIONS = {".txt", ".md", ".markdown", ".mdx", ".json", ".csv", ".log", ".yaml", ".yml"}
|
|
74
|
+
_CHAT_ATTACHMENT_TEXT_MIME_PREFIXES = ("text/",)
|
|
75
|
+
_CHAT_ATTACHMENT_TEXT_MIME_TYPES = {"application/json", "application/x-yaml", "text/csv"}
|
|
62
76
|
|
|
63
77
|
|
|
64
78
|
def _oversized_event_placeholder(*, prefix: bytes, line_bytes: int) -> dict[str, Any]:
|
|
@@ -101,6 +115,7 @@ def _iter_jsonl_records_safely(
|
|
|
101
115
|
prefix = bytearray()
|
|
102
116
|
current_bytes = 0
|
|
103
117
|
oversized = False
|
|
118
|
+
cursor = 0
|
|
104
119
|
while True:
|
|
105
120
|
chunk = handle.read(_JSONL_STREAM_CHUNK_BYTES)
|
|
106
121
|
if not chunk:
|
|
@@ -114,7 +129,8 @@ def _iter_jsonl_records_safely(
|
|
|
114
129
|
if oversized:
|
|
115
130
|
current_bytes += len(segment)
|
|
116
131
|
if has_newline:
|
|
117
|
-
|
|
132
|
+
cursor += 1
|
|
133
|
+
yield cursor, _oversized_event_placeholder(prefix=bytes(prefix), line_bytes=current_bytes)
|
|
118
134
|
prefix = bytearray()
|
|
119
135
|
current_bytes = 0
|
|
120
136
|
oversized = False
|
|
@@ -133,7 +149,8 @@ def _iter_jsonl_records_safely(
|
|
|
133
149
|
current_bytes = next_bytes
|
|
134
150
|
oversized = True
|
|
135
151
|
if has_newline:
|
|
136
|
-
|
|
152
|
+
cursor += 1
|
|
153
|
+
yield cursor, _oversized_event_placeholder(prefix=bytes(prefix), line_bytes=current_bytes)
|
|
137
154
|
prefix = bytearray()
|
|
138
155
|
current_bytes = 0
|
|
139
156
|
oversized = False
|
|
@@ -148,28 +165,27 @@ def _iter_jsonl_records_safely(
|
|
|
148
165
|
buffer.clear()
|
|
149
166
|
line_bytes = current_bytes
|
|
150
167
|
current_bytes = 0
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
yield payload
|
|
168
|
+
cursor += 1
|
|
169
|
+
payload = _parse_jsonl_record_line_safely(
|
|
170
|
+
raw,
|
|
171
|
+
oversized_line_bytes=oversized_line_bytes,
|
|
172
|
+
)
|
|
173
|
+
yield cursor, payload
|
|
158
174
|
start = newline_index + 1
|
|
159
175
|
continue
|
|
160
176
|
break
|
|
161
177
|
|
|
162
178
|
if oversized:
|
|
163
|
-
|
|
179
|
+
cursor += 1
|
|
180
|
+
yield cursor, _oversized_event_placeholder(prefix=bytes(prefix), line_bytes=current_bytes)
|
|
164
181
|
elif buffer:
|
|
165
182
|
raw = bytes(buffer).strip()
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
yield payload
|
|
183
|
+
cursor += 1
|
|
184
|
+
payload = _parse_jsonl_record_line_safely(
|
|
185
|
+
raw,
|
|
186
|
+
oversized_line_bytes=oversized_line_bytes,
|
|
187
|
+
)
|
|
188
|
+
yield cursor, payload
|
|
173
189
|
|
|
174
190
|
|
|
175
191
|
def _parse_jsonl_record_line_safely(
|
|
@@ -188,11 +204,69 @@ def _parse_jsonl_record_line_safely(
|
|
|
188
204
|
)
|
|
189
205
|
try:
|
|
190
206
|
payload = json.loads(raw)
|
|
191
|
-
except json.JSONDecodeError:
|
|
207
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
192
208
|
return None
|
|
193
209
|
return payload if isinstance(payload, dict) else None
|
|
194
210
|
|
|
195
211
|
|
|
212
|
+
def _jsonl_line_count_cache_path(path: Path) -> Path:
|
|
213
|
+
return path.with_name(f".{path.name}{_JSONL_LINE_COUNT_CACHE_SUFFIX}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _jsonl_path_state(path: Path) -> tuple[int, int, int] | None:
|
|
217
|
+
if not path.exists():
|
|
218
|
+
return None
|
|
219
|
+
stat = path.stat()
|
|
220
|
+
return (
|
|
221
|
+
stat.st_ino,
|
|
222
|
+
getattr(stat, "st_mtime_ns", int(stat.st_mtime * 1_000_000_000)),
|
|
223
|
+
stat.st_size,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _jsonl_line_count_from_cache(path: Path) -> int | None:
|
|
228
|
+
current_state = _jsonl_path_state(path)
|
|
229
|
+
if current_state is None:
|
|
230
|
+
return 0
|
|
231
|
+
|
|
232
|
+
cache_path = _jsonl_line_count_cache_path(path)
|
|
233
|
+
payload = read_json(cache_path, {})
|
|
234
|
+
if not isinstance(payload, dict):
|
|
235
|
+
payload = {}
|
|
236
|
+
raw_state = payload.get("state")
|
|
237
|
+
raw_total = payload.get("total")
|
|
238
|
+
if not isinstance(raw_state, (list, tuple)) or len(raw_state) != 3:
|
|
239
|
+
return None
|
|
240
|
+
try:
|
|
241
|
+
cached_state = tuple(int(item) for item in raw_state)
|
|
242
|
+
cached_total = int(raw_total)
|
|
243
|
+
except (TypeError, ValueError):
|
|
244
|
+
return None
|
|
245
|
+
if cached_total < 0:
|
|
246
|
+
return None
|
|
247
|
+
if cached_state == current_state:
|
|
248
|
+
return cached_total
|
|
249
|
+
|
|
250
|
+
# If the file only grew through append-only writes, count just the delta.
|
|
251
|
+
if cached_state[0] == current_state[0] and current_state[2] > cached_state[2]:
|
|
252
|
+
appended_count = 0
|
|
253
|
+
for relative_cursor, _payload in _iter_jsonl_records_from_offset_safely(
|
|
254
|
+
path,
|
|
255
|
+
start_offset=cached_state[2],
|
|
256
|
+
):
|
|
257
|
+
appended_count = relative_cursor
|
|
258
|
+
total = cached_total + appended_count
|
|
259
|
+
write_json(
|
|
260
|
+
cache_path,
|
|
261
|
+
{
|
|
262
|
+
"state": list(current_state),
|
|
263
|
+
"total": total,
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
return total
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
|
|
196
270
|
def _tail_jsonl_records_safely(
|
|
197
271
|
path: Path,
|
|
198
272
|
*,
|
|
@@ -225,6 +299,9 @@ def _tail_jsonl_records_safely(
|
|
|
225
299
|
def _count_jsonl_lines_fast(path: Path, *, chunk_size: int = 1024 * 1024) -> int:
|
|
226
300
|
if not path.exists():
|
|
227
301
|
return 0
|
|
302
|
+
cached_total = _jsonl_line_count_from_cache(path)
|
|
303
|
+
if cached_total is not None:
|
|
304
|
+
return cached_total
|
|
228
305
|
total = 0
|
|
229
306
|
last_byte = b""
|
|
230
307
|
with path.open("rb") as handle:
|
|
@@ -235,9 +312,18 @@ def _count_jsonl_lines_fast(path: Path, *, chunk_size: int = 1024 * 1024) -> int
|
|
|
235
312
|
total += chunk.count(b"\n")
|
|
236
313
|
last_byte = chunk[-1:]
|
|
237
314
|
if total == 0 and last_byte:
|
|
238
|
-
|
|
239
|
-
|
|
315
|
+
total = 1
|
|
316
|
+
elif last_byte not in {b"", b"\n"}:
|
|
240
317
|
total += 1
|
|
318
|
+
state = _jsonl_path_state(path)
|
|
319
|
+
if state is not None:
|
|
320
|
+
write_json(
|
|
321
|
+
_jsonl_line_count_cache_path(path),
|
|
322
|
+
{
|
|
323
|
+
"state": list(state),
|
|
324
|
+
"total": total,
|
|
325
|
+
},
|
|
326
|
+
)
|
|
241
327
|
return total
|
|
242
328
|
|
|
243
329
|
|
|
@@ -284,13 +370,12 @@ def _iter_jsonl_records_from_offset_safely(
|
|
|
284
370
|
return
|
|
285
371
|
with path.open("rb") as handle:
|
|
286
372
|
handle.seek(max(int(start_offset or 0), 0))
|
|
287
|
-
for raw_line in handle:
|
|
373
|
+
for relative_cursor, raw_line in enumerate(handle, start=1):
|
|
288
374
|
payload = _parse_jsonl_record_line_safely(
|
|
289
375
|
raw_line,
|
|
290
376
|
oversized_line_bytes=oversized_line_bytes,
|
|
291
377
|
)
|
|
292
|
-
|
|
293
|
-
yield payload
|
|
378
|
+
yield relative_cursor, payload
|
|
294
379
|
|
|
295
380
|
|
|
296
381
|
class QuestService:
|
|
@@ -319,6 +404,33 @@ class QuestService:
|
|
|
319
404
|
self._quest_projection_refresh_lock = threading.Lock()
|
|
320
405
|
self._quest_projection_refresh_at: dict[str, float] = {}
|
|
321
406
|
|
|
407
|
+
def _configured_default_runner(self) -> str:
|
|
408
|
+
config = ConfigManager(self.home).load_named("config")
|
|
409
|
+
return self._resolve_enabled_runner_name(config.get("default_runner"))
|
|
410
|
+
|
|
411
|
+
def _resolve_enabled_runner_name(self, *candidates: Any) -> str:
|
|
412
|
+
runners = ConfigManager(self.home).load_runners_config()
|
|
413
|
+
seen: set[str] = set()
|
|
414
|
+
enabled: list[str] = []
|
|
415
|
+
for name, cfg in runners.items():
|
|
416
|
+
normalized = str(name or "").strip().lower()
|
|
417
|
+
if not normalized or normalized in seen:
|
|
418
|
+
continue
|
|
419
|
+
seen.add(normalized)
|
|
420
|
+
if isinstance(cfg, dict) and cfg.get("enabled") is not False:
|
|
421
|
+
enabled.append(normalized)
|
|
422
|
+
|
|
423
|
+
checked: set[str] = set()
|
|
424
|
+
for raw in [*candidates, "codex"]:
|
|
425
|
+
normalized = str(raw or "").strip().lower()
|
|
426
|
+
if not normalized or normalized in checked:
|
|
427
|
+
continue
|
|
428
|
+
checked.add(normalized)
|
|
429
|
+
cfg = runners.get(normalized)
|
|
430
|
+
if isinstance(cfg, dict) and cfg.get("enabled") is not False:
|
|
431
|
+
return normalized
|
|
432
|
+
return enabled[0] if enabled else "codex"
|
|
433
|
+
|
|
322
434
|
def _quest_root(self, quest_id: str) -> Path:
|
|
323
435
|
return self.quests_root / quest_id
|
|
324
436
|
|
|
@@ -330,31 +442,33 @@ class QuestService:
|
|
|
330
442
|
|
|
331
443
|
def _normalized_binding_sources(self, sources: list[Any] | None) -> list[str]:
|
|
332
444
|
local_present = False
|
|
333
|
-
|
|
445
|
+
external_sources: list[str] = []
|
|
446
|
+
external_index: dict[str, int] = {}
|
|
334
447
|
for raw in sources or []:
|
|
335
448
|
normalized = self._normalize_binding_source(raw)
|
|
336
449
|
if not normalized:
|
|
337
450
|
continue
|
|
338
|
-
if normalized == "local:default":
|
|
339
|
-
local_present = True
|
|
340
|
-
continue
|
|
341
451
|
parsed = parse_conversation_id(normalized)
|
|
342
452
|
connector = str((parsed or {}).get("connector") or "").strip().lower()
|
|
343
|
-
if connector == "local":
|
|
453
|
+
if normalized == "local:default" or connector == "local":
|
|
344
454
|
local_present = True
|
|
345
455
|
continue
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
456
|
+
identity = conversation_identity_key(normalized)
|
|
457
|
+
existing_index = external_index.get(identity)
|
|
458
|
+
if existing_index is None:
|
|
459
|
+
external_index[identity] = len(external_sources)
|
|
460
|
+
external_sources.append(normalized)
|
|
461
|
+
else:
|
|
462
|
+
external_sources[existing_index] = normalized
|
|
349
463
|
if local_present:
|
|
350
|
-
return ["local:default"]
|
|
351
|
-
return
|
|
464
|
+
return ["local:default", *external_sources]
|
|
465
|
+
return external_sources
|
|
352
466
|
|
|
353
467
|
def _binding_sources_payload(self, quest_root: Path) -> dict[str, list[str]]:
|
|
354
468
|
bindings_path = quest_root / ".ds" / "bindings.json"
|
|
355
|
-
payload = read_json(bindings_path, {"sources": [
|
|
356
|
-
raw_sources = payload.get("sources") if isinstance(payload, dict) else [
|
|
357
|
-
sources = self._normalized_binding_sources(raw_sources if isinstance(raw_sources, list) else [
|
|
469
|
+
payload = read_json(bindings_path, {"sources": []})
|
|
470
|
+
raw_sources = payload.get("sources") if isinstance(payload, dict) else []
|
|
471
|
+
sources = self._normalized_binding_sources(raw_sources if isinstance(raw_sources, list) else [])
|
|
358
472
|
return {"sources": sources}
|
|
359
473
|
|
|
360
474
|
def preferred_locale(self, quest_root: Path | None = None) -> str:
|
|
@@ -400,11 +514,21 @@ class QuestService:
|
|
|
400
514
|
if not isinstance(payload, dict):
|
|
401
515
|
payload = {}
|
|
402
516
|
normalized = dict(payload)
|
|
403
|
-
normalized.
|
|
517
|
+
startup_contract = dict(normalized.get("startup_contract") or {}) if isinstance(normalized.get("startup_contract"), dict) else None
|
|
518
|
+
normalized.setdefault("startup_contract", startup_contract)
|
|
404
519
|
normalized.setdefault("baseline_gate", "pending")
|
|
405
520
|
normalized.setdefault("confirmed_baseline_ref", None)
|
|
406
521
|
normalized.setdefault("requested_baseline_ref", None)
|
|
407
|
-
normalized.
|
|
522
|
+
active_anchor = str(normalized.get("active_anchor") or "").strip()
|
|
523
|
+
if not active_anchor:
|
|
524
|
+
normalized["active_anchor"] = default_active_anchor(startup_contract)
|
|
525
|
+
elif (
|
|
526
|
+
active_anchor == "baseline"
|
|
527
|
+
and str((startup_contract or {}).get("workspace_mode") or "").strip().lower() == "copilot"
|
|
528
|
+
and not isinstance(normalized.get("confirmed_baseline_ref"), dict)
|
|
529
|
+
and not isinstance(normalized.get("requested_baseline_ref"), dict)
|
|
530
|
+
):
|
|
531
|
+
normalized["active_anchor"] = "scout"
|
|
408
532
|
return normalized
|
|
409
533
|
|
|
410
534
|
@staticmethod
|
|
@@ -882,7 +1006,7 @@ class QuestService:
|
|
|
882
1006
|
if not artifacts_root.exists():
|
|
883
1007
|
continue
|
|
884
1008
|
for folder in sorted(artifacts_root.iterdir()):
|
|
885
|
-
if not folder.is_dir():
|
|
1009
|
+
if not folder.is_dir() or folder.name == "graphs":
|
|
886
1010
|
continue
|
|
887
1011
|
for path in sorted(folder.glob("*.json")):
|
|
888
1012
|
item = self._read_cached_json(path, {})
|
|
@@ -1515,16 +1639,18 @@ class QuestService:
|
|
|
1515
1639
|
for artifact in recent_artifacts:
|
|
1516
1640
|
payload = artifact.get("payload") if isinstance(artifact.get("payload"), dict) else {}
|
|
1517
1641
|
artifact_path = artifact.get("path")
|
|
1642
|
+
artifact_kind = str(payload.get("kind") or artifact.get("kind") or "").strip()
|
|
1518
1643
|
entries.append(
|
|
1519
1644
|
{
|
|
1520
1645
|
"id": f"artifact:{payload.get('artifact_id') or artifact_path}",
|
|
1521
1646
|
"kind": "artifact",
|
|
1522
|
-
"title": str(payload.get("artifact_id") or artifact.get("kind") or "artifact"),
|
|
1647
|
+
"title": str(payload.get("title") or payload.get("artifact_id") or artifact.get("kind") or "artifact"),
|
|
1523
1648
|
"summary": payload.get("summary") or payload.get("message") or payload.get("reason") or "Artifact updated.",
|
|
1524
1649
|
"status": payload.get("status"),
|
|
1525
1650
|
"reason": payload.get("reason"),
|
|
1526
1651
|
"created_at": payload.get("updated_at") or payload.get("created_at"),
|
|
1527
1652
|
"paths": list((payload.get("paths") or {}).values()) + ([str(artifact_path)] if artifact_path else []),
|
|
1653
|
+
"stage_key": "science" if artifact_kind.startswith("science.") else payload.get("stage_key"),
|
|
1528
1654
|
}
|
|
1529
1655
|
)
|
|
1530
1656
|
add_file(str(artifact_path) if artifact_path else None, source="artifact")
|
|
@@ -1832,6 +1958,37 @@ class QuestService:
|
|
|
1832
1958
|
best_root = paper_root
|
|
1833
1959
|
return best_root
|
|
1834
1960
|
|
|
1961
|
+
@staticmethod
|
|
1962
|
+
def _paper_line_state_from_root(paper_root: Path) -> dict[str, Any]:
|
|
1963
|
+
path = paper_root / "paper_line_state.json"
|
|
1964
|
+
payload = read_json(path, {})
|
|
1965
|
+
return payload if isinstance(payload, dict) else {}
|
|
1966
|
+
|
|
1967
|
+
@staticmethod
|
|
1968
|
+
def _filter_paper_evidence_items(
|
|
1969
|
+
items: list[dict[str, Any]],
|
|
1970
|
+
*,
|
|
1971
|
+
selected_outline_ref: str | None = None,
|
|
1972
|
+
paper_line_id: str | None = None,
|
|
1973
|
+
) -> list[dict[str, Any]]:
|
|
1974
|
+
normalized_outline = str(selected_outline_ref or "").strip() or None
|
|
1975
|
+
normalized_line = str(paper_line_id or "").strip() or None
|
|
1976
|
+
filtered: list[dict[str, Any]] = []
|
|
1977
|
+
for item in items:
|
|
1978
|
+
if not isinstance(item, dict):
|
|
1979
|
+
continue
|
|
1980
|
+
item_outline = str(item.get("selected_outline_ref") or "").strip() or None
|
|
1981
|
+
item_line = str(item.get("paper_line_id") or "").strip() or None
|
|
1982
|
+
if normalized_line:
|
|
1983
|
+
if item_line and item_line != normalized_line:
|
|
1984
|
+
continue
|
|
1985
|
+
if not item_line and normalized_outline and item_outline and item_outline != normalized_outline:
|
|
1986
|
+
continue
|
|
1987
|
+
elif normalized_outline and item_outline and item_outline != normalized_outline:
|
|
1988
|
+
continue
|
|
1989
|
+
filtered.append(dict(item))
|
|
1990
|
+
return filtered
|
|
1991
|
+
|
|
1835
1992
|
def _outline_record_from_paper_root(self, paper_root: Path) -> dict[str, Any]:
|
|
1836
1993
|
outline_root = paper_root / "outline"
|
|
1837
1994
|
manifest_path = outline_root / "manifest.json"
|
|
@@ -1893,52 +2050,49 @@ class QuestService:
|
|
|
1893
2050
|
return payload if isinstance(payload, dict) else {}
|
|
1894
2051
|
|
|
1895
2052
|
def _paper_evidence_payload(self, quest_root: Path, workspace_root: Path) -> dict[str, Any] | None:
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
)
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
"
|
|
1936
|
-
"
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
},
|
|
1940
|
-
}
|
|
1941
|
-
return best_payload
|
|
2053
|
+
paper_root = self._best_paper_root(quest_root, workspace_root)
|
|
2054
|
+
if paper_root is None:
|
|
2055
|
+
return None
|
|
2056
|
+
ledger_json_path = paper_root / "evidence_ledger.json"
|
|
2057
|
+
if not ledger_json_path.exists():
|
|
2058
|
+
return None
|
|
2059
|
+
payload = read_json(ledger_json_path, {})
|
|
2060
|
+
if not isinstance(payload, dict) or not payload:
|
|
2061
|
+
return None
|
|
2062
|
+
selected_outline_ref = str(payload.get("selected_outline_ref") or "").strip() or None
|
|
2063
|
+
line_state = self._paper_line_state_from_root(paper_root)
|
|
2064
|
+
paper_line_id = str(line_state.get("paper_line_id") or "").strip() or None
|
|
2065
|
+
items = self._filter_paper_evidence_items(
|
|
2066
|
+
[dict(item) for item in (payload.get("items") or []) if isinstance(item, dict)],
|
|
2067
|
+
selected_outline_ref=selected_outline_ref,
|
|
2068
|
+
paper_line_id=paper_line_id,
|
|
2069
|
+
)
|
|
2070
|
+
return {
|
|
2071
|
+
"paper_root": str(paper_root),
|
|
2072
|
+
"workspace_root": str(paper_root.parent),
|
|
2073
|
+
"selected_outline_ref": selected_outline_ref,
|
|
2074
|
+
"paper_line_id": paper_line_id,
|
|
2075
|
+
"item_count": len(items),
|
|
2076
|
+
"main_text_ready_count": sum(
|
|
2077
|
+
1
|
|
2078
|
+
for item in items
|
|
2079
|
+
if str(item.get("paper_role") or "").strip() == "main_text"
|
|
2080
|
+
and str(item.get("status") or "").strip().lower() in {"ready", "completed", "analyzed", "written", "recorded", "supported"}
|
|
2081
|
+
),
|
|
2082
|
+
"appendix_item_count": sum(
|
|
2083
|
+
1 for item in items if str(item.get("paper_role") or "").strip() == "appendix"
|
|
2084
|
+
),
|
|
2085
|
+
"unmapped_item_count": sum(
|
|
2086
|
+
1
|
|
2087
|
+
for item in items
|
|
2088
|
+
if not str(item.get("section_id") or "").strip() or not str(item.get("paper_role") or "").strip()
|
|
2089
|
+
),
|
|
2090
|
+
"items": items[:40],
|
|
2091
|
+
"paths": {
|
|
2092
|
+
"ledger_json": str(ledger_json_path),
|
|
2093
|
+
"ledger_md": str(paper_root / "evidence_ledger.md") if (paper_root / "evidence_ledger.md").exists() else None,
|
|
2094
|
+
},
|
|
2095
|
+
}
|
|
1942
2096
|
|
|
1943
2097
|
def _paper_contract_payload(self, quest_root: Path, workspace_root: Path) -> dict[str, Any] | None:
|
|
1944
2098
|
paper_root = self._best_paper_root(quest_root, workspace_root)
|
|
@@ -1958,6 +2112,7 @@ class QuestService:
|
|
|
1958
2112
|
bundle_manifest = bundle_manifest if isinstance(bundle_manifest, dict) else {}
|
|
1959
2113
|
experiment_matrix_path = paper_root / "paper_experiment_matrix.md"
|
|
1960
2114
|
experiment_matrix_json_path = paper_root / "paper_experiment_matrix.json"
|
|
2115
|
+
manuscript_coverage_path = paper_root / "manuscript_coverage.json"
|
|
1961
2116
|
claim_map_path = paper_root / "claim_evidence_map.json"
|
|
1962
2117
|
paper_line_state_path = paper_root / "paper_line_state.json"
|
|
1963
2118
|
evidence_ledger = self._paper_evidence_payload(quest_root, workspace_root)
|
|
@@ -2008,11 +2163,14 @@ class QuestService:
|
|
|
2008
2163
|
return {
|
|
2009
2164
|
"paper_root": str(paper_root),
|
|
2010
2165
|
"workspace_root": str(paper_root.parent),
|
|
2166
|
+
"paper_line_id": str(self._paper_line_state_from_root(paper_root).get("paper_line_id") or "").strip() or None,
|
|
2011
2167
|
"paper_branch": str(bundle_manifest.get("paper_branch") or "").strip() or current_branch(paper_root.parent),
|
|
2012
2168
|
"source_branch": str(bundle_manifest.get("source_branch") or "").strip() or None,
|
|
2013
2169
|
"selected_outline_ref": str(selected_outline.get("outline_id") or bundle_manifest.get("selected_outline_ref") or "").strip() or None,
|
|
2014
2170
|
"title": str(selected_outline.get("title") or bundle_manifest.get("title") or "").strip() or None,
|
|
2015
2171
|
"story": str(selected_outline.get("story") or "").strip() or None,
|
|
2172
|
+
"paper_view": selected_outline.get("paper_view") if isinstance(selected_outline.get("paper_view"), dict) else None,
|
|
2173
|
+
"evidence_view": selected_outline.get("evidence_view") if isinstance(selected_outline.get("evidence_view"), dict) else None,
|
|
2016
2174
|
"research_questions": detailed_outline.get("research_questions") if isinstance(detailed_outline.get("research_questions"), list) else [],
|
|
2017
2175
|
"experimental_designs": detailed_outline.get("experimental_designs") if isinstance(detailed_outline.get("experimental_designs"), list) else [],
|
|
2018
2176
|
"contributions": detailed_outline.get("contributions") if isinstance(detailed_outline.get("contributions"), list) else [],
|
|
@@ -2030,6 +2188,7 @@ class QuestService:
|
|
|
2030
2188
|
"outline_manifest": str(outline_manifest_path) if outline_manifest_path.exists() else None,
|
|
2031
2189
|
"experiment_matrix": str(experiment_matrix_path) if experiment_matrix_path.exists() else None,
|
|
2032
2190
|
"experiment_matrix_json": str(experiment_matrix_json_path) if experiment_matrix_json_path.exists() else None,
|
|
2191
|
+
"manuscript_coverage": str(manuscript_coverage_path) if manuscript_coverage_path.exists() else None,
|
|
2033
2192
|
"bundle_manifest": str(bundle_manifest_path) if bundle_manifest_path.exists() else None,
|
|
2034
2193
|
"claim_evidence_map": str(claim_map_path) if claim_map_path.exists() else None,
|
|
2035
2194
|
"paper_line_state": str(paper_line_state_path) if paper_line_state_path.exists() else None,
|
|
@@ -2423,11 +2582,25 @@ class QuestService:
|
|
|
2423
2582
|
evidence_items = [
|
|
2424
2583
|
dict(item) for item in ((paper_evidence or {}).get("items") or []) if isinstance(item, dict)
|
|
2425
2584
|
]
|
|
2426
|
-
ledger_by_item = {
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
if
|
|
2430
|
-
|
|
2585
|
+
ledger_by_item: dict[str, list[dict[str, Any]]] = {}
|
|
2586
|
+
for item in evidence_items:
|
|
2587
|
+
item_id = str(item.get("item_id") or "").strip()
|
|
2588
|
+
if item_id:
|
|
2589
|
+
ledger_by_item.setdefault(item_id, []).append(item)
|
|
2590
|
+
|
|
2591
|
+
def ready_ledger_item(item_id: str) -> dict[str, Any] | None:
|
|
2592
|
+
candidates = ledger_by_item.get(item_id) or []
|
|
2593
|
+
ready_statuses = {"ready", "completed", "analyzed", "written", "recorded", "supported"}
|
|
2594
|
+
ready = [
|
|
2595
|
+
item
|
|
2596
|
+
for item in candidates
|
|
2597
|
+
if str(item.get("status") or "").strip().lower() in ready_statuses
|
|
2598
|
+
]
|
|
2599
|
+
if ready:
|
|
2600
|
+
main = [item for item in ready if str(item.get("paper_role") or "").strip() == "main_text"]
|
|
2601
|
+
return main[0] if main else ready[0]
|
|
2602
|
+
return candidates[0] if candidates else None
|
|
2603
|
+
|
|
2431
2604
|
unresolved_required_items: list[dict[str, Any]] = []
|
|
2432
2605
|
ready_section_count = 0
|
|
2433
2606
|
for section in paper_contract.get("sections") or []:
|
|
@@ -2436,7 +2609,7 @@ class QuestService:
|
|
|
2436
2609
|
required_items = [str(item).strip() for item in (section.get("required_items") or []) if str(item).strip()]
|
|
2437
2610
|
section_ready = True
|
|
2438
2611
|
for item_id in required_items:
|
|
2439
|
-
ledger_item =
|
|
2612
|
+
ledger_item = ready_ledger_item(item_id)
|
|
2440
2613
|
status = str((ledger_item or {}).get("status") or "").strip().lower()
|
|
2441
2614
|
if status not in {"ready", "completed", "analyzed", "written", "recorded", "supported"}:
|
|
2442
2615
|
unresolved_required_items.append(
|
|
@@ -2523,6 +2696,18 @@ class QuestService:
|
|
|
2523
2696
|
if isinstance(paper_contract.get("bundle_manifest"), dict)
|
|
2524
2697
|
else {}
|
|
2525
2698
|
)
|
|
2699
|
+
package_type = str(bundle_manifest.get("package_type") or "draft_checkpoint").strip().lower().replace("-", "_")
|
|
2700
|
+
if package_type in {"", "draft", "memo", "checkpoint", "paper_memo"}:
|
|
2701
|
+
package_type = "draft_checkpoint"
|
|
2702
|
+
elif package_type in {"review", "review_bundle"}:
|
|
2703
|
+
package_type = "review_package"
|
|
2704
|
+
elif package_type in {"final", "final_bundle", "submission", "submission_bundle"}:
|
|
2705
|
+
package_type = "submission_package"
|
|
2706
|
+
elif package_type not in {"draft_checkpoint", "review_package", "submission_package"}:
|
|
2707
|
+
package_type = "draft_checkpoint"
|
|
2708
|
+
coverage_path = str(((paper_contract.get("paths") or {}).get("manuscript_coverage") or "")).strip()
|
|
2709
|
+
manuscript_coverage = read_json(Path(coverage_path), {}) if coverage_path else {}
|
|
2710
|
+
manuscript_coverage = manuscript_coverage if isinstance(manuscript_coverage, dict) else {}
|
|
2526
2711
|
submission_checklist_path = str(((paper_contract.get("paths") or {}).get("submission_checklist") or "")).strip()
|
|
2527
2712
|
submission_checklist = read_json(Path(submission_checklist_path), {}) if submission_checklist_path else {}
|
|
2528
2713
|
submission_checklist = submission_checklist if isinstance(submission_checklist, dict) else {}
|
|
@@ -2536,9 +2721,23 @@ class QuestService:
|
|
|
2536
2721
|
closure_state = "bundle_not_ready"
|
|
2537
2722
|
delivery_state = "not_ready"
|
|
2538
2723
|
keep_bundle_fixed_by_default = False
|
|
2539
|
-
|
|
2724
|
+
evidence_ready = contract_ok
|
|
2725
|
+
analysis_ready = writing_ready
|
|
2726
|
+
academic_outline_ready = bool(manuscript_coverage.get("academic_outline_ready"))
|
|
2727
|
+
analysis_plan_ready = bool(manuscript_coverage.get("analysis_plan_ready"))
|
|
2728
|
+
language_firewall_ok = bool(manuscript_coverage.get("language_firewall_ok"))
|
|
2729
|
+
draft_checkpoint_ready = bool(active_line.get("draft_checkpoint_ready")) or draft_status == "present" or bundle_status == "present"
|
|
2730
|
+
manuscript_ready = bool(active_line.get("manuscript_ready")) or bool(manuscript_coverage.get("manuscript_ready"))
|
|
2731
|
+
submission_ready = bool(active_line.get("submission_ready")) or bool(manuscript_coverage.get("submission_ready"))
|
|
2732
|
+
if submission_ready:
|
|
2540
2733
|
closure_state = "delivery_ready"
|
|
2541
|
-
delivery_state = "
|
|
2734
|
+
delivery_state = "submission_ready"
|
|
2735
|
+
elif manuscript_ready:
|
|
2736
|
+
closure_state = "review_before_submission"
|
|
2737
|
+
delivery_state = "manuscript_ready"
|
|
2738
|
+
elif draft_checkpoint_ready:
|
|
2739
|
+
closure_state = "draft_checkpoint_continue_writing"
|
|
2740
|
+
delivery_state = "draft_checkpoint_ready"
|
|
2542
2741
|
if delivered_at or "delivered" in overall_status:
|
|
2543
2742
|
delivery_state = "delivered"
|
|
2544
2743
|
closure_state = "delivered_continue_research" if "continue" in overall_status else "delivered_parked"
|
|
@@ -2547,15 +2746,30 @@ class QuestService:
|
|
|
2547
2746
|
if unmapped_completed_items:
|
|
2548
2747
|
recommended_next_stage = "write"
|
|
2549
2748
|
recommended_action = "sync_paper_contract"
|
|
2749
|
+
elif manuscript_coverage and not academic_outline_ready:
|
|
2750
|
+
recommended_next_stage = "write"
|
|
2751
|
+
recommended_action = "repair_academic_outline_with_paper_outline"
|
|
2752
|
+
elif manuscript_coverage and not analysis_plan_ready:
|
|
2753
|
+
recommended_next_stage = "write"
|
|
2754
|
+
recommended_action = "repair_analysis_plan_with_paper_outline"
|
|
2550
2755
|
elif unresolved_required_items or blocking_pending_slices:
|
|
2551
2756
|
recommended_next_stage = "analysis-campaign"
|
|
2552
2757
|
recommended_action = "complete_required_supplementary"
|
|
2758
|
+
elif manuscript_coverage and not language_firewall_ok and draft_checkpoint_ready:
|
|
2759
|
+
recommended_next_stage = "write"
|
|
2760
|
+
recommended_action = "repair_manuscript_language"
|
|
2553
2761
|
elif draft_status != "present":
|
|
2554
2762
|
recommended_next_stage = "write"
|
|
2555
2763
|
recommended_action = "draft_paper"
|
|
2556
2764
|
elif bundle_status != "present":
|
|
2557
2765
|
recommended_next_stage = "write"
|
|
2558
|
-
recommended_action = "
|
|
2766
|
+
recommended_action = "submit_draft_checkpoint"
|
|
2767
|
+
elif not manuscript_ready:
|
|
2768
|
+
recommended_next_stage = "write"
|
|
2769
|
+
recommended_action = "expand_manuscript_and_figures"
|
|
2770
|
+
elif not submission_ready:
|
|
2771
|
+
recommended_next_stage = "review"
|
|
2772
|
+
recommended_action = "prepare_submission_package"
|
|
2559
2773
|
else:
|
|
2560
2774
|
recommended_next_stage = "finalize"
|
|
2561
2775
|
recommended_action = "finalize_paper_line"
|
|
@@ -2574,7 +2788,16 @@ class QuestService:
|
|
|
2574
2788
|
"selected_outline_ref": selected_outline_ref,
|
|
2575
2789
|
"contract_ok": contract_ok,
|
|
2576
2790
|
"writing_ready": writing_ready,
|
|
2577
|
-
"
|
|
2791
|
+
"evidence_ready": evidence_ready,
|
|
2792
|
+
"analysis_ready": analysis_ready,
|
|
2793
|
+
"academic_outline_ready": academic_outline_ready,
|
|
2794
|
+
"analysis_plan_ready": analysis_plan_ready,
|
|
2795
|
+
"language_firewall_ok": language_firewall_ok,
|
|
2796
|
+
"draft_checkpoint_ready": draft_checkpoint_ready,
|
|
2797
|
+
"manuscript_ready": manuscript_ready,
|
|
2798
|
+
"submission_ready": submission_ready,
|
|
2799
|
+
"finalize_ready": submission_ready,
|
|
2800
|
+
"package_type": package_type,
|
|
2578
2801
|
"closure_state": closure_state,
|
|
2579
2802
|
"delivery_state": delivery_state,
|
|
2580
2803
|
"delivered_at": delivered_at,
|
|
@@ -2603,6 +2826,27 @@ class QuestService:
|
|
|
2603
2826
|
"draft_status": draft_status,
|
|
2604
2827
|
"bundle_status": bundle_status,
|
|
2605
2828
|
"blocking_reasons": blocking_reasons,
|
|
2829
|
+
"manuscript_blocking_reasons": list(
|
|
2830
|
+
active_line.get("manuscript_blocking_reasons")
|
|
2831
|
+
or manuscript_coverage.get("manuscript_blockers")
|
|
2832
|
+
or []
|
|
2833
|
+
),
|
|
2834
|
+
"manuscript_warning_reasons": list(
|
|
2835
|
+
active_line.get("manuscript_warning_reasons")
|
|
2836
|
+
or manuscript_coverage.get("manuscript_warnings")
|
|
2837
|
+
or []
|
|
2838
|
+
),
|
|
2839
|
+
"submission_blocking_reasons": list(
|
|
2840
|
+
active_line.get("submission_blocking_reasons")
|
|
2841
|
+
or manuscript_coverage.get("submission_blockers")
|
|
2842
|
+
or []
|
|
2843
|
+
),
|
|
2844
|
+
"submission_warning_reasons": list(
|
|
2845
|
+
active_line.get("submission_warning_reasons")
|
|
2846
|
+
or manuscript_coverage.get("submission_warnings")
|
|
2847
|
+
or []
|
|
2848
|
+
),
|
|
2849
|
+
"manuscript_coverage": manuscript_coverage or None,
|
|
2606
2850
|
"recommended_next_stage": recommended_next_stage,
|
|
2607
2851
|
"recommended_action": recommended_action,
|
|
2608
2852
|
"unresolved_required_items": unresolved_required_items[:12],
|
|
@@ -2635,6 +2879,40 @@ class QuestService:
|
|
|
2635
2879
|
return text
|
|
2636
2880
|
return text.zfill(_NUMERIC_QUEST_ID_PAD_WIDTH)
|
|
2637
2881
|
|
|
2882
|
+
@staticmethod
|
|
2883
|
+
def _parse_reserved_numeric_quest_id(value: str | None) -> int | None:
|
|
2884
|
+
numeric_value = QuestService._parse_numeric_quest_id(value)
|
|
2885
|
+
if numeric_value is not None:
|
|
2886
|
+
return numeric_value
|
|
2887
|
+
raw = str(value or "").strip()
|
|
2888
|
+
match = _SYSTEM_NUMERIC_QUEST_ID_PATTERN.fullmatch(raw)
|
|
2889
|
+
if match is None:
|
|
2890
|
+
return None
|
|
2891
|
+
return QuestService._parse_numeric_quest_id(match.group("number"))
|
|
2892
|
+
|
|
2893
|
+
@staticmethod
|
|
2894
|
+
def _quest_class_for(
|
|
2895
|
+
*,
|
|
2896
|
+
quest_id: str | None,
|
|
2897
|
+
startup_contract: dict[str, Any] | None = None,
|
|
2898
|
+
) -> str:
|
|
2899
|
+
normalized_quest_id = str(quest_id or "").strip()
|
|
2900
|
+
match = _SYSTEM_NUMERIC_QUEST_ID_PATTERN.fullmatch(normalized_quest_id)
|
|
2901
|
+
if match is not None:
|
|
2902
|
+
prefix = str(match.group("prefix") or "").strip().upper()
|
|
2903
|
+
if prefix == "S":
|
|
2904
|
+
return "settings"
|
|
2905
|
+
if prefix == "B":
|
|
2906
|
+
return "benchstore"
|
|
2907
|
+
|
|
2908
|
+
contract = startup_contract if isinstance(startup_contract, dict) else {}
|
|
2909
|
+
custom_profile = str(contract.get("custom_profile") or "").strip().lower()
|
|
2910
|
+
if custom_profile in {"admin_ops", "settings_issue"}:
|
|
2911
|
+
return "settings"
|
|
2912
|
+
if isinstance(contract.get("benchstore_context"), dict) or isinstance(contract.get("start_setup_session"), dict):
|
|
2913
|
+
return "benchstore"
|
|
2914
|
+
return "research"
|
|
2915
|
+
|
|
2638
2916
|
@contextmanager
|
|
2639
2917
|
def _quest_id_state_lock(self):
|
|
2640
2918
|
lock_path = self._quest_id_lock_path()
|
|
@@ -2720,7 +2998,7 @@ class QuestService:
|
|
|
2720
2998
|
return self._format_numeric_quest_id(next_numeric_id)
|
|
2721
2999
|
|
|
2722
3000
|
def _reserve_numeric_quest_id(self, quest_id: str) -> None:
|
|
2723
|
-
numeric_value = self.
|
|
3001
|
+
numeric_value = self._parse_reserved_numeric_quest_id(quest_id)
|
|
2724
3002
|
if numeric_value is None:
|
|
2725
3003
|
return
|
|
2726
3004
|
with self._quest_id_state_lock():
|
|
@@ -2730,24 +3008,28 @@ class QuestService:
|
|
|
2730
3008
|
self._write_quest_id_state_locked(next_numeric_id)
|
|
2731
3009
|
|
|
2732
3010
|
def _normalize_quest_id(self, quest_id: str | None) -> tuple[str, bool]:
|
|
2733
|
-
raw = str(quest_id or "").strip()
|
|
3011
|
+
raw = str(quest_id or "").strip()
|
|
2734
3012
|
if not raw:
|
|
2735
3013
|
return self._allocate_next_numeric_quest_id(), True
|
|
2736
|
-
slug = re.sub(r"[^
|
|
3014
|
+
slug = re.sub(r"[^A-Za-z0-9._-]+", "-", raw).strip("._-")
|
|
2737
3015
|
if not slug:
|
|
2738
3016
|
return self._allocate_next_numeric_quest_id(), True
|
|
3017
|
+
system_match = _SYSTEM_NUMERIC_QUEST_ID_PATTERN.fullmatch(slug)
|
|
3018
|
+
if system_match is not None:
|
|
3019
|
+
slug = f"{str(system_match.group('prefix') or '').upper()}-{system_match.group('number')}"
|
|
2739
3020
|
return slug[:80], False
|
|
2740
3021
|
|
|
2741
3022
|
def create(
|
|
2742
3023
|
self,
|
|
2743
3024
|
goal: str,
|
|
2744
3025
|
quest_id: str | None = None,
|
|
2745
|
-
runner: str =
|
|
3026
|
+
runner: str | None = None,
|
|
2746
3027
|
title: str | None = None,
|
|
2747
3028
|
*,
|
|
2748
3029
|
requested_baseline_ref: dict[str, Any] | None = None,
|
|
2749
3030
|
startup_contract: dict[str, Any] | None = None,
|
|
2750
3031
|
) -> dict:
|
|
3032
|
+
resolved_runner = str(runner or self._configured_default_runner()).strip().lower() or "codex"
|
|
2751
3033
|
quest_id, auto_generated = self._normalize_quest_id(quest_id)
|
|
2752
3034
|
quest_root = self._quest_root(quest_id)
|
|
2753
3035
|
if quest_root.exists():
|
|
@@ -2763,7 +3045,7 @@ class QuestService:
|
|
|
2763
3045
|
quest_id,
|
|
2764
3046
|
goal,
|
|
2765
3047
|
quest_root,
|
|
2766
|
-
|
|
3048
|
+
resolved_runner,
|
|
2767
3049
|
title=title,
|
|
2768
3050
|
requested_baseline_ref=dict(requested_baseline_ref) if isinstance(requested_baseline_ref, dict) else None,
|
|
2769
3051
|
startup_contract=dict(startup_contract) if isinstance(startup_contract, dict) else None,
|
|
@@ -2794,8 +3076,9 @@ class QuestService:
|
|
|
2794
3076
|
*,
|
|
2795
3077
|
title: str | None = None,
|
|
2796
3078
|
goal: str | None = None,
|
|
2797
|
-
runner: str =
|
|
3079
|
+
runner: str | None = None,
|
|
2798
3080
|
) -> dict[str, Any]:
|
|
3081
|
+
resolved_runner = str(runner or self._configured_default_runner()).strip().lower() or "codex"
|
|
2799
3082
|
quest_root = self._quest_root(quest_id)
|
|
2800
3083
|
if not quest_root.exists():
|
|
2801
3084
|
raise FileNotFoundError(f"Unknown quest `{quest_id}`.")
|
|
@@ -2815,7 +3098,7 @@ class QuestService:
|
|
|
2815
3098
|
quest_id,
|
|
2816
3099
|
restored_goal,
|
|
2817
3100
|
quest_root,
|
|
2818
|
-
|
|
3101
|
+
resolved_runner,
|
|
2819
3102
|
title=restored_title,
|
|
2820
3103
|
),
|
|
2821
3104
|
)
|
|
@@ -3021,9 +3304,16 @@ class QuestService:
|
|
|
3021
3304
|
|
|
3022
3305
|
bash_summary = BashExecService(self.home).summary(quest_root)
|
|
3023
3306
|
interaction_watchdog = self.artifact_interaction_watchdog_status(quest_root)
|
|
3307
|
+
quest_class = self._quest_class_for(
|
|
3308
|
+
quest_id=str(quest_yaml.get("quest_id") or quest_id).strip(),
|
|
3309
|
+
startup_contract=quest_yaml.get("startup_contract") if isinstance(quest_yaml.get("startup_contract"), dict) else None,
|
|
3310
|
+
)
|
|
3311
|
+
workspace_mode = str(research_state.get("workspace_mode") or "quest").strip().lower() or "quest"
|
|
3312
|
+
listed_in_projects = quest_class == "research" and workspace_mode in {"copilot", "autonomous"}
|
|
3024
3313
|
payload = {
|
|
3025
3314
|
"quest_id": quest_yaml.get("quest_id", quest_id),
|
|
3026
3315
|
"title": quest_yaml.get("title", quest_id),
|
|
3316
|
+
"goal": quest_yaml.get("goal"),
|
|
3027
3317
|
"quest_root": str(quest_root.resolve()),
|
|
3028
3318
|
"status": runtime_state.get("display_status") or runtime_state.get("status") or quest_yaml.get("status", "idle"),
|
|
3029
3319
|
"runtime_status": runtime_state.get("status") or quest_yaml.get("status", "idle"),
|
|
@@ -3039,7 +3329,9 @@ class QuestService:
|
|
|
3039
3329
|
"research_head_worktree_root": research_state.get("research_head_worktree_root"),
|
|
3040
3330
|
"current_workspace_branch": research_state.get("current_workspace_branch"),
|
|
3041
3331
|
"current_workspace_root": research_state.get("current_workspace_root"),
|
|
3042
|
-
"workspace_mode":
|
|
3332
|
+
"workspace_mode": workspace_mode,
|
|
3333
|
+
"quest_class": quest_class,
|
|
3334
|
+
"listed_in_projects": listed_in_projects,
|
|
3043
3335
|
"active_idea_id": research_state.get("active_idea_id"),
|
|
3044
3336
|
"active_baseline_id": active_baseline_id,
|
|
3045
3337
|
"active_baseline_variant_id": active_baseline_variant_id,
|
|
@@ -3048,6 +3340,7 @@ class QuestService:
|
|
|
3048
3340
|
"continuation_anchor": runtime_state.get("continuation_anchor"),
|
|
3049
3341
|
"continuation_reason": runtime_state.get("continuation_reason"),
|
|
3050
3342
|
"continuation_updated_at": runtime_state.get("continuation_updated_at"),
|
|
3343
|
+
"waiting_notice": runtime_state.get("waiting_notice"),
|
|
3051
3344
|
"last_resume_source": runtime_state.get("last_resume_source"),
|
|
3052
3345
|
"last_resume_at": runtime_state.get("last_resume_at"),
|
|
3053
3346
|
"last_recovery_abandoned_run_id": runtime_state.get("last_recovery_abandoned_run_id"),
|
|
@@ -3142,18 +3435,18 @@ class QuestService:
|
|
|
3142
3435
|
self._jsonl_tail_cache.pop(cache_key, None)
|
|
3143
3436
|
return [], 0, False
|
|
3144
3437
|
if normalized_limit <= 0:
|
|
3145
|
-
total =
|
|
3438
|
+
total = _count_jsonl_lines_fast(path)
|
|
3146
3439
|
return [], total, False
|
|
3147
3440
|
|
|
3148
3441
|
if before is not None:
|
|
3149
|
-
stop_cursor = max(int(before) - 1, 0)
|
|
3150
3442
|
window: deque[tuple[int, dict[str, Any]]] = deque(maxlen=normalized_limit)
|
|
3151
3443
|
total = 0
|
|
3152
|
-
for payload in _iter_jsonl_records_safely(path):
|
|
3153
|
-
total
|
|
3154
|
-
if
|
|
3444
|
+
for cursor, payload in _iter_jsonl_records_safely(path):
|
|
3445
|
+
total = cursor
|
|
3446
|
+
if cursor >= before:
|
|
3155
3447
|
break
|
|
3156
|
-
|
|
3448
|
+
if isinstance(payload, dict):
|
|
3449
|
+
window.append((cursor, payload))
|
|
3157
3450
|
has_more = bool(window and window[0][0] > 1)
|
|
3158
3451
|
return list(window), total, has_more
|
|
3159
3452
|
|
|
@@ -3173,6 +3466,17 @@ class QuestService:
|
|
|
3173
3466
|
window = cached_records[-normalized_limit:]
|
|
3174
3467
|
has_more = cached_total > len(window)
|
|
3175
3468
|
return window, cached_total, has_more
|
|
3469
|
+
if cached_limit < normalized_limit:
|
|
3470
|
+
window, total = _tail_jsonl_records_safely(path, limit=normalized_limit)
|
|
3471
|
+
with self._jsonl_cache_lock:
|
|
3472
|
+
self._jsonl_tail_cache[cache_key] = {
|
|
3473
|
+
"state": state,
|
|
3474
|
+
"limit": normalized_limit,
|
|
3475
|
+
"total": total,
|
|
3476
|
+
"records": list(window),
|
|
3477
|
+
}
|
|
3478
|
+
has_more = total > len(window)
|
|
3479
|
+
return list(window), total, has_more
|
|
3176
3480
|
|
|
3177
3481
|
if (
|
|
3178
3482
|
cached_tail
|
|
@@ -3196,11 +3500,11 @@ class QuestService:
|
|
|
3196
3500
|
)
|
|
3197
3501
|
)
|
|
3198
3502
|
if appended_records:
|
|
3199
|
-
|
|
3200
|
-
for payload in appended_records:
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3503
|
+
total = cached_total
|
|
3504
|
+
for relative_cursor, payload in appended_records:
|
|
3505
|
+
total = cached_total + relative_cursor
|
|
3506
|
+
if isinstance(payload, dict):
|
|
3507
|
+
window.append((total, payload))
|
|
3204
3508
|
else:
|
|
3205
3509
|
total = cached_total
|
|
3206
3510
|
stored_records = list(window)
|
|
@@ -3211,6 +3515,13 @@ class QuestService:
|
|
|
3211
3515
|
"total": total,
|
|
3212
3516
|
"records": stored_records,
|
|
3213
3517
|
}
|
|
3518
|
+
write_json(
|
|
3519
|
+
_jsonl_line_count_cache_path(path),
|
|
3520
|
+
{
|
|
3521
|
+
"state": list(state),
|
|
3522
|
+
"total": total,
|
|
3523
|
+
},
|
|
3524
|
+
)
|
|
3214
3525
|
selected = stored_records[-normalized_limit:]
|
|
3215
3526
|
has_more = total > len(selected)
|
|
3216
3527
|
return selected, total, has_more
|
|
@@ -3230,12 +3541,14 @@ class QuestService:
|
|
|
3230
3541
|
total = 0
|
|
3231
3542
|
saw_more = False
|
|
3232
3543
|
normalized_after = max(int(after or 0), 0)
|
|
3233
|
-
for payload in _iter_jsonl_records_safely(path):
|
|
3234
|
-
total
|
|
3235
|
-
if
|
|
3544
|
+
for cursor, payload in _iter_jsonl_records_safely(path):
|
|
3545
|
+
total = cursor
|
|
3546
|
+
if cursor <= normalized_after:
|
|
3547
|
+
continue
|
|
3548
|
+
if not isinstance(payload, dict):
|
|
3236
3549
|
continue
|
|
3237
3550
|
if len(collected) < normalized_limit:
|
|
3238
|
-
collected.append((
|
|
3551
|
+
collected.append((cursor, payload))
|
|
3239
3552
|
continue
|
|
3240
3553
|
saw_more = True
|
|
3241
3554
|
return collected, total, saw_more
|
|
@@ -3514,6 +3827,7 @@ class QuestService:
|
|
|
3514
3827
|
payload = {
|
|
3515
3828
|
"quest_id": quest_yaml.get("quest_id", quest_id),
|
|
3516
3829
|
"title": quest_yaml.get("title", quest_id),
|
|
3830
|
+
"goal": quest_yaml.get("goal"),
|
|
3517
3831
|
"quest_root": str(quest_root.resolve()),
|
|
3518
3832
|
"status": runtime_state.get("display_status") or runtime_state.get("status") or quest_yaml.get("status", "idle"),
|
|
3519
3833
|
"runtime_status": runtime_state.get("status") or quest_yaml.get("status", "idle"),
|
|
@@ -3552,6 +3866,7 @@ class QuestService:
|
|
|
3552
3866
|
"continuation_anchor": runtime_state.get("continuation_anchor"),
|
|
3553
3867
|
"continuation_reason": runtime_state.get("continuation_reason"),
|
|
3554
3868
|
"continuation_updated_at": runtime_state.get("continuation_updated_at"),
|
|
3869
|
+
"waiting_notice": runtime_state.get("waiting_notice"),
|
|
3555
3870
|
"last_resume_source": runtime_state.get("last_resume_source"),
|
|
3556
3871
|
"last_resume_at": runtime_state.get("last_resume_at"),
|
|
3557
3872
|
"last_recovery_abandoned_run_id": runtime_state.get("last_recovery_abandoned_run_id"),
|
|
@@ -3850,9 +4165,12 @@ class QuestService:
|
|
|
3850
4165
|
def bind_source(self, quest_id: str, source: str) -> dict:
|
|
3851
4166
|
quest_root = self._quest_root(quest_id)
|
|
3852
4167
|
bindings_path = quest_root / ".ds" / "bindings.json"
|
|
4168
|
+
existing_payload = read_json(bindings_path, {"sources": []})
|
|
4169
|
+
existing_sources = existing_payload.get("sources") if isinstance(existing_payload, dict) else []
|
|
3853
4170
|
bindings = self._binding_sources_payload(quest_root)
|
|
3854
4171
|
normalized_source = self._normalize_binding_source(source)
|
|
3855
|
-
|
|
4172
|
+
seed_sources = list(existing_sources) if isinstance(existing_sources, list) else list(bindings.get("sources") or [])
|
|
4173
|
+
next_sources = self._normalized_binding_sources([*seed_sources, normalized_source])
|
|
3856
4174
|
changed = list(bindings.get("sources") or []) != next_sources
|
|
3857
4175
|
if changed:
|
|
3858
4176
|
bindings["sources"] = next_sources
|
|
@@ -3911,6 +4229,7 @@ class QuestService:
|
|
|
3911
4229
|
active_anchor: str | None = None,
|
|
3912
4230
|
default_runner: str | None = None,
|
|
3913
4231
|
workspace_mode: str | None = None,
|
|
4232
|
+
decision_policy: str | None = None,
|
|
3914
4233
|
) -> dict:
|
|
3915
4234
|
quest_root = self._quest_root(quest_id)
|
|
3916
4235
|
quest_yaml_path = self._quest_yaml_path(quest_root)
|
|
@@ -3954,6 +4273,13 @@ class QuestService:
|
|
|
3954
4273
|
if normalized_runner not in available_runners:
|
|
3955
4274
|
allowed = ", ".join(sorted(available_runners))
|
|
3956
4275
|
raise ValueError(f"Unsupported runner `{normalized_runner}`. Available runners: {allowed}.")
|
|
4276
|
+
runners = ConfigManager(self.home).load_runners_config()
|
|
4277
|
+
runner_cfg = runners.get(normalized_runner, {}) if isinstance(runners.get(normalized_runner), dict) else {}
|
|
4278
|
+
if runner_cfg.get("enabled") is False:
|
|
4279
|
+
fallback = self._resolve_enabled_runner_name(normalized_runner)
|
|
4280
|
+
if fallback == normalized_runner:
|
|
4281
|
+
raise ValueError(f"Runner `{normalized_runner}` is disabled and no enabled fallback runner is available.")
|
|
4282
|
+
normalized_runner = fallback
|
|
3957
4283
|
if quest_data.get("default_runner") != normalized_runner:
|
|
3958
4284
|
quest_data["default_runner"] = normalized_runner
|
|
3959
4285
|
changed = True
|
|
@@ -3980,6 +4306,33 @@ class QuestService:
|
|
|
3980
4306
|
"copilot_mode" if normalized_workspace_mode == "copilot" else "autonomous_mode"
|
|
3981
4307
|
)
|
|
3982
4308
|
|
|
4309
|
+
if decision_policy is not None:
|
|
4310
|
+
normalized_decision_policy = str(decision_policy).strip().lower()
|
|
4311
|
+
if normalized_decision_policy not in {"autonomous", "user_gated"}:
|
|
4312
|
+
raise ValueError("Unsupported decision policy. Allowed values: autonomous, user_gated.")
|
|
4313
|
+
startup_contract = (
|
|
4314
|
+
dict(quest_data.get("startup_contract") or {})
|
|
4315
|
+
if isinstance(quest_data.get("startup_contract"), dict)
|
|
4316
|
+
else {}
|
|
4317
|
+
)
|
|
4318
|
+
if str(startup_contract.get("decision_policy") or "").strip().lower() != normalized_decision_policy:
|
|
4319
|
+
startup_contract["decision_policy"] = normalized_decision_policy
|
|
4320
|
+
quest_data["startup_contract"] = startup_contract
|
|
4321
|
+
changed = True
|
|
4322
|
+
effective_workspace_mode = str(
|
|
4323
|
+
research_state_updates.get("workspace_mode")
|
|
4324
|
+
or self.read_research_state(quest_root).get("workspace_mode")
|
|
4325
|
+
or startup_contract.get("workspace_mode")
|
|
4326
|
+
or ""
|
|
4327
|
+
).strip().lower()
|
|
4328
|
+
if normalized_decision_policy == "autonomous" and effective_workspace_mode == "autonomous":
|
|
4329
|
+
runtime_state = self._read_runtime_state(quest_root)
|
|
4330
|
+
current_policy = str(runtime_state.get("continuation_policy") or "").strip().lower()
|
|
4331
|
+
current_reason = str(runtime_state.get("continuation_reason") or "").strip()
|
|
4332
|
+
if current_policy == "wait_for_user_or_resume" and current_reason not in AUTONOMOUS_BLOCKING_WAIT_REASONS:
|
|
4333
|
+
runtime_state_updates["continuation_policy"] = "auto"
|
|
4334
|
+
runtime_state_updates["continuation_reason"] = "autonomous_decision_policy"
|
|
4335
|
+
|
|
3983
4336
|
if changed:
|
|
3984
4337
|
quest_data["updated_at"] = utc_now()
|
|
3985
4338
|
write_yaml(quest_yaml_path, quest_data)
|
|
@@ -4088,11 +4441,26 @@ class QuestService:
|
|
|
4088
4441
|
active_run_id=active_run_id or None,
|
|
4089
4442
|
last_transition_at=last_transition_at,
|
|
4090
4443
|
)
|
|
4444
|
+
# Reconcile continuation_policy with current workspace_mode so that
|
|
4445
|
+
# a mode switch that happened before/during the crash is respected.
|
|
4446
|
+
research_state = self.read_research_state(quest_root)
|
|
4447
|
+
workspace_mode = str(research_state.get("workspace_mode") or "").strip().lower()
|
|
4448
|
+
current_policy = str(runtime_state.get("continuation_policy") or "").strip().lower()
|
|
4449
|
+
reconciled_policy_updates: dict[str, Any] = {}
|
|
4450
|
+
if workspace_mode == "autonomous" and current_policy == "wait_for_user_or_resume":
|
|
4451
|
+
reconciled_policy_updates["continuation_policy"] = "auto"
|
|
4452
|
+
reconciled_policy_updates["continuation_reason"] = "autonomous_mode_reconciled"
|
|
4453
|
+
reconciled_policy_updates["continuation_updated_at"] = utc_now()
|
|
4454
|
+
elif workspace_mode == "copilot" and current_policy == "auto":
|
|
4455
|
+
reconciled_policy_updates["continuation_policy"] = "wait_for_user_or_resume"
|
|
4456
|
+
reconciled_policy_updates["continuation_reason"] = "copilot_mode_reconciled"
|
|
4457
|
+
reconciled_policy_updates["continuation_updated_at"] = utc_now()
|
|
4091
4458
|
self.update_runtime_state(
|
|
4092
4459
|
quest_root=quest_root,
|
|
4093
4460
|
status="stopped",
|
|
4094
4461
|
active_run_id=None,
|
|
4095
4462
|
stop_reason="crash_recovered",
|
|
4463
|
+
**reconciled_policy_updates,
|
|
4096
4464
|
)
|
|
4097
4465
|
summary = (
|
|
4098
4466
|
f"Recovered quest from stale runtime state; previous status `{previous_status}`"
|
|
@@ -4232,8 +4600,15 @@ class QuestService:
|
|
|
4232
4600
|
|
|
4233
4601
|
def node_traces(self, quest_id: str, *, selection_type: str | None = None) -> dict:
|
|
4234
4602
|
quest_root = self._quest_root(quest_id)
|
|
4235
|
-
workflow = self.workflow(quest_id)
|
|
4236
4603
|
snapshot = self.snapshot(quest_id)
|
|
4604
|
+
try:
|
|
4605
|
+
workflow = self._build_details_projection_payload(
|
|
4606
|
+
quest_root,
|
|
4607
|
+
source_signature=self._projection_source_signature(quest_root, "details"),
|
|
4608
|
+
update_progress=lambda *_args, **_kwargs: None,
|
|
4609
|
+
)
|
|
4610
|
+
except Exception:
|
|
4611
|
+
workflow = self.workflow(quest_id)
|
|
4237
4612
|
payload = QuestNodeTraceManager(quest_root).materialize(
|
|
4238
4613
|
quest_id=quest_id,
|
|
4239
4614
|
workflow=workflow,
|
|
@@ -4474,6 +4849,7 @@ class QuestService:
|
|
|
4474
4849
|
mode: str | None = None,
|
|
4475
4850
|
profile: str | None = None,
|
|
4476
4851
|
) -> dict:
|
|
4852
|
+
profile = str(profile or "").strip().lower() or None
|
|
4477
4853
|
if revision:
|
|
4478
4854
|
return self._revision_explorer(quest_id, revision=revision, mode=mode or "ref")
|
|
4479
4855
|
|
|
@@ -4504,7 +4880,7 @@ class QuestService:
|
|
|
4504
4880
|
}
|
|
4505
4881
|
|
|
4506
4882
|
def search_files(self, quest_id: str, term: str, limit: int = 50) -> dict[str, Any]:
|
|
4507
|
-
query =
|
|
4883
|
+
query = self._normalize_explorer_search_query(term)
|
|
4508
4884
|
normalized_query = query.casefold()
|
|
4509
4885
|
workspace_root = self.active_workspace_root(self._require_initialized_quest_root(quest_id))
|
|
4510
4886
|
resolved_limit = max(1, min(limit, 200))
|
|
@@ -4530,6 +4906,41 @@ class QuestService:
|
|
|
4530
4906
|
except OSError:
|
|
4531
4907
|
continue
|
|
4532
4908
|
|
|
4909
|
+
relative = path.relative_to(workspace_root).as_posix()
|
|
4910
|
+
scope, writable = self._classify_path_scope(workspace_root, path)
|
|
4911
|
+
path_haystack = relative.casefold()
|
|
4912
|
+
name_haystack = path.name.casefold()
|
|
4913
|
+
if normalized_query in path_haystack or normalized_query in name_haystack:
|
|
4914
|
+
haystack = path_haystack
|
|
4915
|
+
match_spans: list[dict[str, int]] = []
|
|
4916
|
+
start = 0
|
|
4917
|
+
while True:
|
|
4918
|
+
found = haystack.find(normalized_query, start)
|
|
4919
|
+
if found < 0:
|
|
4920
|
+
break
|
|
4921
|
+
match_spans.append({"start": found, "end": found + len(query)})
|
|
4922
|
+
start = found + max(1, len(query))
|
|
4923
|
+
renderer_hint, mime_type = self._renderer_hint_for(path)
|
|
4924
|
+
items.append(
|
|
4925
|
+
{
|
|
4926
|
+
"id": f"{relative}:path",
|
|
4927
|
+
"document_id": f"path::{relative}",
|
|
4928
|
+
"title": path.name,
|
|
4929
|
+
"path": relative,
|
|
4930
|
+
"scope": scope,
|
|
4931
|
+
"writable": writable,
|
|
4932
|
+
"line_number": 0,
|
|
4933
|
+
"line_text": relative,
|
|
4934
|
+
"snippet": relative[:320],
|
|
4935
|
+
"match_spans": match_spans,
|
|
4936
|
+
"open_kind": renderer_hint,
|
|
4937
|
+
"mime_type": mime_type,
|
|
4938
|
+
}
|
|
4939
|
+
)
|
|
4940
|
+
if len(items) >= resolved_limit:
|
|
4941
|
+
truncated = True
|
|
4942
|
+
break
|
|
4943
|
+
|
|
4533
4944
|
renderer_hint, mime_type = self._renderer_hint_for(path)
|
|
4534
4945
|
if not self._is_text_document(path, mime_type, renderer_hint):
|
|
4535
4946
|
continue
|
|
@@ -4552,8 +4963,6 @@ class QuestService:
|
|
|
4552
4963
|
continue
|
|
4553
4964
|
|
|
4554
4965
|
files_scanned += 1
|
|
4555
|
-
relative = path.relative_to(workspace_root).as_posix()
|
|
4556
|
-
scope, writable = self._classify_path_scope(workspace_root, path)
|
|
4557
4966
|
|
|
4558
4967
|
for line_index, line in enumerate(content.splitlines(), start=1):
|
|
4559
4968
|
haystack = line.casefold()
|
|
@@ -4600,6 +5009,15 @@ class QuestService:
|
|
|
4600
5009
|
"files_scanned": files_scanned,
|
|
4601
5010
|
}
|
|
4602
5011
|
|
|
5012
|
+
@staticmethod
|
|
5013
|
+
def _normalize_explorer_search_query(term: str) -> str:
|
|
5014
|
+
query = str(term or "").strip()
|
|
5015
|
+
if len(query) >= 2 and query.startswith("*") and query.endswith("*"):
|
|
5016
|
+
inner = query.strip("*").strip()
|
|
5017
|
+
if inner and not any(marker in inner for marker in "*?[]"):
|
|
5018
|
+
return inner
|
|
5019
|
+
return query
|
|
5020
|
+
|
|
4603
5021
|
def open_document(self, quest_id: str, document_id: str) -> dict:
|
|
4604
5022
|
quest_root = self._require_initialized_quest_root(quest_id)
|
|
4605
5023
|
workspace_root = self.active_workspace_root(quest_root)
|
|
@@ -4637,12 +5055,16 @@ class QuestService:
|
|
|
4637
5055
|
},
|
|
4638
5056
|
}
|
|
4639
5057
|
|
|
5058
|
+
shared_source_quest_id = None
|
|
5059
|
+
parsed_shared_memory = self._parse_shared_memory_document_id(document_id)
|
|
5060
|
+
if parsed_shared_memory is not None:
|
|
5061
|
+
shared_source_quest_id = parsed_shared_memory[0]
|
|
4640
5062
|
path, writable, scope, source_kind = self.resolve_document(quest_id, document_id)
|
|
4641
5063
|
renderer_hint, mime_type = self._renderer_hint_for(path)
|
|
4642
5064
|
is_text = self._is_text_document(path, mime_type, renderer_hint)
|
|
4643
5065
|
content = read_text(path) if is_text else ""
|
|
4644
5066
|
revision = f"sha256:{sha256_text(content)}" if is_text else f"sha256:{hashlib.sha256(path.read_bytes()).hexdigest()}"
|
|
4645
|
-
|
|
5067
|
+
payload = {
|
|
4646
5068
|
"document_id": document_id,
|
|
4647
5069
|
"quest_id": quest_id,
|
|
4648
5070
|
"title": path.name if "::" in document_id else document_id,
|
|
@@ -4664,9 +5086,17 @@ class QuestService:
|
|
|
4664
5086
|
"renderer_hint": renderer_hint,
|
|
4665
5087
|
},
|
|
4666
5088
|
}
|
|
5089
|
+
if shared_source_quest_id:
|
|
5090
|
+
payload["source_quest_id"] = shared_source_quest_id
|
|
5091
|
+
if isinstance(payload.get("meta"), dict):
|
|
5092
|
+
payload["meta"]["source_quest_id"] = shared_source_quest_id
|
|
5093
|
+
payload["meta"]["shared"] = True
|
|
5094
|
+
return payload
|
|
4667
5095
|
|
|
4668
5096
|
def resolve_document(self, quest_id: str, document_id: str) -> tuple[Path, bool, str, str]:
|
|
4669
5097
|
quest_root = self._require_initialized_quest_root(quest_id)
|
|
5098
|
+
if document_id.startswith(_SHARED_MEMORY_DOCUMENT_PREFIX):
|
|
5099
|
+
return self._resolve_shared_memory_document(document_id)
|
|
4670
5100
|
workspace_root = self.active_workspace_root(quest_root)
|
|
4671
5101
|
resolution_root = self._document_resolution_root(
|
|
4672
5102
|
quest_root=quest_root,
|
|
@@ -4683,6 +5113,32 @@ class QuestService:
|
|
|
4683
5113
|
return self._resolve_document(quest_root, f"questpath::{legacy_relative}")
|
|
4684
5114
|
raise
|
|
4685
5115
|
|
|
5116
|
+
def _resolve_shared_memory_document(self, document_id: str) -> tuple[Path, bool, str, str]:
|
|
5117
|
+
parsed = self._parse_shared_memory_document_id(document_id)
|
|
5118
|
+
if parsed is None:
|
|
5119
|
+
raise FileNotFoundError(f"Unknown shared memory document `{document_id}`.")
|
|
5120
|
+
source_quest_id, relative = parsed
|
|
5121
|
+
source_quest_root = self._require_initialized_quest_root(source_quest_id)
|
|
5122
|
+
root = (source_quest_root / "memory").resolve()
|
|
5123
|
+
path = (root / relative).resolve()
|
|
5124
|
+
if path != root and root not in path.parents:
|
|
5125
|
+
raise ValueError("Document ID escapes shared quest memory.")
|
|
5126
|
+
if not path.exists() or not path.is_file():
|
|
5127
|
+
raise FileNotFoundError(f"Unknown shared quest memory `{source_quest_id}:{relative}`.")
|
|
5128
|
+
return path, False, "shared_quest_memory", "shared_quest_memory"
|
|
5129
|
+
|
|
5130
|
+
@staticmethod
|
|
5131
|
+
def _parse_shared_memory_document_id(document_id: str) -> tuple[str, str] | None:
|
|
5132
|
+
raw = str(document_id or "").strip()
|
|
5133
|
+
if not raw.startswith(_SHARED_MEMORY_DOCUMENT_PREFIX):
|
|
5134
|
+
return None
|
|
5135
|
+
_prefix, source_quest_id, relative = (raw.split("::", 2) + ["", "", ""])[:3]
|
|
5136
|
+
source_quest_id = source_quest_id.strip()
|
|
5137
|
+
relative = relative.lstrip("/")
|
|
5138
|
+
if not source_quest_id or not relative:
|
|
5139
|
+
return None
|
|
5140
|
+
return source_quest_id, relative
|
|
5141
|
+
|
|
4686
5142
|
def save_document(self, quest_id: str, document_id: str, content: str, previous_revision: str | None = None) -> dict:
|
|
4687
5143
|
current = self.open_document(quest_id, document_id)
|
|
4688
5144
|
if not current.get("writable", False):
|
|
@@ -4859,6 +5315,246 @@ class QuestService:
|
|
|
4859
5315
|
"saved_at": utc_now(),
|
|
4860
5316
|
}
|
|
4861
5317
|
|
|
5318
|
+
def save_chat_attachment_draft(
|
|
5319
|
+
self,
|
|
5320
|
+
quest_id: str,
|
|
5321
|
+
*,
|
|
5322
|
+
file_name: str,
|
|
5323
|
+
mime_type: str | None,
|
|
5324
|
+
content: bytes,
|
|
5325
|
+
draft_id: str | None = None,
|
|
5326
|
+
) -> dict[str, Any]:
|
|
5327
|
+
quest_root = self._quest_root(quest_id)
|
|
5328
|
+
normalized_draft_id = slugify(str(draft_id or generate_id("draft")), default=generate_id("draft"))
|
|
5329
|
+
draft_root = self._chat_attachment_draft_root(quest_root, normalized_draft_id)
|
|
5330
|
+
original_name = Path(file_name).name or "attachment.bin"
|
|
5331
|
+
suffix = Path(original_name).suffix.lower()
|
|
5332
|
+
if not suffix:
|
|
5333
|
+
guessed_suffix = mimetypes.guess_extension(mime_type or "", strict=False) or ""
|
|
5334
|
+
suffix = ".jpg" if guessed_suffix == ".jpe" else guessed_suffix
|
|
5335
|
+
safe_stem = slugify(Path(original_name).stem or "attachment", default="attachment")
|
|
5336
|
+
stored_name = f"{safe_stem}{suffix or '.bin'}"
|
|
5337
|
+
asset_path = resolve_within(draft_root, stored_name)
|
|
5338
|
+
if draft_root.exists():
|
|
5339
|
+
for child in draft_root.iterdir():
|
|
5340
|
+
if child.is_file():
|
|
5341
|
+
child.unlink(missing_ok=True)
|
|
5342
|
+
elif child.is_dir():
|
|
5343
|
+
shutil.rmtree(child, ignore_errors=True)
|
|
5344
|
+
ensure_dir(draft_root)
|
|
5345
|
+
asset_path.write_bytes(content)
|
|
5346
|
+
quest_relative_path = asset_path.relative_to(quest_root).as_posix()
|
|
5347
|
+
asset_document_id = f"path::{quest_relative_path}"
|
|
5348
|
+
attachment = self._chat_attachment_payload(
|
|
5349
|
+
quest_id=quest_id,
|
|
5350
|
+
name=original_name,
|
|
5351
|
+
stored_name=stored_name,
|
|
5352
|
+
mime_type=mime_type,
|
|
5353
|
+
asset_path=asset_path,
|
|
5354
|
+
draft_id=normalized_draft_id,
|
|
5355
|
+
)
|
|
5356
|
+
attachment["status"] = "success"
|
|
5357
|
+
write_json(
|
|
5358
|
+
draft_root / "manifest.json",
|
|
5359
|
+
{
|
|
5360
|
+
"draft_id": normalized_draft_id,
|
|
5361
|
+
"quest_id": quest_id,
|
|
5362
|
+
"created_at": utc_now(),
|
|
5363
|
+
"attachment": attachment,
|
|
5364
|
+
"asset_document_id": asset_document_id,
|
|
5365
|
+
},
|
|
5366
|
+
)
|
|
5367
|
+
return {
|
|
5368
|
+
"ok": True,
|
|
5369
|
+
"quest_id": quest_id,
|
|
5370
|
+
"draft_id": normalized_draft_id,
|
|
5371
|
+
**attachment,
|
|
5372
|
+
}
|
|
5373
|
+
|
|
5374
|
+
def delete_chat_attachment_draft(self, quest_id: str, *, draft_id: str) -> dict[str, Any]:
|
|
5375
|
+
self._quest_root(quest_id)
|
|
5376
|
+
normalized_draft_id = slugify(str(draft_id or "").strip(), default="")
|
|
5377
|
+
if not normalized_draft_id:
|
|
5378
|
+
return {"ok": False, "message": "`draft_id` is required."}
|
|
5379
|
+
draft_root = self._chat_attachment_draft_root(self._quest_root(quest_id), normalized_draft_id)
|
|
5380
|
+
if not draft_root.exists():
|
|
5381
|
+
return {
|
|
5382
|
+
"ok": True,
|
|
5383
|
+
"status": "already_deleted",
|
|
5384
|
+
"quest_id": quest_id,
|
|
5385
|
+
"draft_id": normalized_draft_id,
|
|
5386
|
+
}
|
|
5387
|
+
shutil.rmtree(draft_root, ignore_errors=True)
|
|
5388
|
+
return {
|
|
5389
|
+
"ok": True,
|
|
5390
|
+
"status": "deleted",
|
|
5391
|
+
"quest_id": quest_id,
|
|
5392
|
+
"draft_id": normalized_draft_id,
|
|
5393
|
+
}
|
|
5394
|
+
|
|
5395
|
+
def import_chat_attachment_drafts(
|
|
5396
|
+
self,
|
|
5397
|
+
target_quest_id: str,
|
|
5398
|
+
*,
|
|
5399
|
+
source_quest_id: str,
|
|
5400
|
+
attachments: list[dict[str, Any]],
|
|
5401
|
+
) -> dict[str, Any]:
|
|
5402
|
+
target_quest_root = self._quest_root(target_quest_id)
|
|
5403
|
+
source_quest_root = self._quest_root(source_quest_id)
|
|
5404
|
+
imported: list[dict[str, Any]] = []
|
|
5405
|
+
for index, raw_attachment in enumerate(attachments, start=1):
|
|
5406
|
+
if not isinstance(raw_attachment, dict):
|
|
5407
|
+
continue
|
|
5408
|
+
quest_relative_path = str(raw_attachment.get("quest_relative_path") or "").strip()
|
|
5409
|
+
absolute_path = str(raw_attachment.get("path") or "").strip()
|
|
5410
|
+
source_path: Path | None = None
|
|
5411
|
+
if quest_relative_path:
|
|
5412
|
+
source_path = resolve_within(source_quest_root, quest_relative_path)
|
|
5413
|
+
elif absolute_path:
|
|
5414
|
+
candidate = Path(absolute_path).resolve()
|
|
5415
|
+
if candidate == source_quest_root or source_quest_root in candidate.parents:
|
|
5416
|
+
source_path = candidate
|
|
5417
|
+
if source_path is None or not source_path.exists() or not source_path.is_file():
|
|
5418
|
+
continue
|
|
5419
|
+
file_name = str(
|
|
5420
|
+
raw_attachment.get("name")
|
|
5421
|
+
or raw_attachment.get("file_name")
|
|
5422
|
+
or source_path.name
|
|
5423
|
+
or f"attachment-{index:03d}"
|
|
5424
|
+
).strip() or f"attachment-{index:03d}"
|
|
5425
|
+
mime_type = str(raw_attachment.get("content_type") or raw_attachment.get("mime_type") or "").strip() or None
|
|
5426
|
+
created = self.save_chat_attachment_draft(
|
|
5427
|
+
target_quest_root.name,
|
|
5428
|
+
file_name=file_name,
|
|
5429
|
+
mime_type=mime_type,
|
|
5430
|
+
content=source_path.read_bytes(),
|
|
5431
|
+
)
|
|
5432
|
+
imported.append(created)
|
|
5433
|
+
return {
|
|
5434
|
+
"ok": True,
|
|
5435
|
+
"quest_id": target_quest_id,
|
|
5436
|
+
"source_quest_id": source_quest_id,
|
|
5437
|
+
"imported_count": len(imported),
|
|
5438
|
+
"attachments": imported,
|
|
5439
|
+
}
|
|
5440
|
+
|
|
5441
|
+
def finalize_chat_attachment_drafts(
|
|
5442
|
+
self,
|
|
5443
|
+
quest_id: str,
|
|
5444
|
+
*,
|
|
5445
|
+
draft_ids: list[str],
|
|
5446
|
+
client_message_id: str | None,
|
|
5447
|
+
) -> list[dict[str, Any]]:
|
|
5448
|
+
quest_root = self._quest_root(quest_id)
|
|
5449
|
+
normalized_draft_ids = [
|
|
5450
|
+
slugify(str(item or "").strip(), default="")
|
|
5451
|
+
for item in draft_ids
|
|
5452
|
+
if str(item or "").strip()
|
|
5453
|
+
]
|
|
5454
|
+
if not normalized_draft_ids:
|
|
5455
|
+
return []
|
|
5456
|
+
batch_slug = slugify(
|
|
5457
|
+
str(client_message_id or generate_id("userfile")).strip(),
|
|
5458
|
+
default=generate_id("userfile"),
|
|
5459
|
+
)
|
|
5460
|
+
batch_root = ensure_dir(quest_root / "userfiles" / "web" / batch_slug)
|
|
5461
|
+
materialized: list[dict[str, Any]] = []
|
|
5462
|
+
for index, normalized_draft_id in enumerate(normalized_draft_ids, start=1):
|
|
5463
|
+
draft_root = self._chat_attachment_draft_root(quest_root, normalized_draft_id)
|
|
5464
|
+
manifest = read_json(draft_root / "manifest.json", {})
|
|
5465
|
+
attachment = dict(manifest.get("attachment") or {})
|
|
5466
|
+
source_path = Path(str(attachment.get("path") or "").strip())
|
|
5467
|
+
if not draft_root.exists() or not source_path.exists():
|
|
5468
|
+
raise FileNotFoundError(f"Unknown chat attachment draft `{normalized_draft_id}`.")
|
|
5469
|
+
target_name = self._dedupe_attachment_filename(
|
|
5470
|
+
batch_root,
|
|
5471
|
+
str(attachment.get("stored_name") or source_path.name or f"attachment-{index:03d}.bin"),
|
|
5472
|
+
)
|
|
5473
|
+
target_path = resolve_within(batch_root, target_name)
|
|
5474
|
+
ensure_dir(target_path.parent)
|
|
5475
|
+
shutil.move(str(source_path), str(target_path))
|
|
5476
|
+
finalized = self._chat_attachment_payload(
|
|
5477
|
+
quest_id=quest_id,
|
|
5478
|
+
name=str(attachment.get("name") or target_name),
|
|
5479
|
+
stored_name=target_name,
|
|
5480
|
+
mime_type=str(attachment.get("content_type") or "").strip() or None,
|
|
5481
|
+
asset_path=target_path,
|
|
5482
|
+
draft_id=None,
|
|
5483
|
+
)
|
|
5484
|
+
finalized["source_path"] = str(source_path)
|
|
5485
|
+
finalized["materialized"] = True
|
|
5486
|
+
finalized["uploaded_at"] = str(attachment.get("uploaded_at") or utc_now())
|
|
5487
|
+
materialized.append(finalized)
|
|
5488
|
+
shutil.rmtree(draft_root, ignore_errors=True)
|
|
5489
|
+
write_json(
|
|
5490
|
+
batch_root / "manifest.json",
|
|
5491
|
+
{
|
|
5492
|
+
"quest_id": quest_id,
|
|
5493
|
+
"client_message_id": str(client_message_id or "").strip() or None,
|
|
5494
|
+
"materialized_at": utc_now(),
|
|
5495
|
+
"attachments": materialized,
|
|
5496
|
+
},
|
|
5497
|
+
)
|
|
5498
|
+
return materialized
|
|
5499
|
+
|
|
5500
|
+
@staticmethod
|
|
5501
|
+
def _chat_attachment_draft_root(quest_root: Path, draft_id: str) -> Path:
|
|
5502
|
+
return quest_root / "userfiles" / "web" / "_staging" / draft_id
|
|
5503
|
+
|
|
5504
|
+
@staticmethod
|
|
5505
|
+
def _dedupe_attachment_filename(batch_root: Path, file_name: str) -> str:
|
|
5506
|
+
base_name = Path(file_name).name or "attachment.bin"
|
|
5507
|
+
stem = Path(base_name).stem or "attachment"
|
|
5508
|
+
suffix = Path(base_name).suffix
|
|
5509
|
+
candidate = base_name
|
|
5510
|
+
counter = 2
|
|
5511
|
+
while (batch_root / candidate).exists():
|
|
5512
|
+
candidate = f"{stem}-{counter}{suffix}"
|
|
5513
|
+
counter += 1
|
|
5514
|
+
return candidate
|
|
5515
|
+
|
|
5516
|
+
@staticmethod
|
|
5517
|
+
def _is_readable_chat_attachment(path: Path, mime_type: str | None) -> bool:
|
|
5518
|
+
normalized_mime = str(mime_type or "").strip().lower()
|
|
5519
|
+
if any(normalized_mime.startswith(prefix) for prefix in _CHAT_ATTACHMENT_TEXT_MIME_PREFIXES):
|
|
5520
|
+
return True
|
|
5521
|
+
if normalized_mime in _CHAT_ATTACHMENT_TEXT_MIME_TYPES:
|
|
5522
|
+
return True
|
|
5523
|
+
return path.suffix.lower() in _CHAT_ATTACHMENT_TEXT_EXTENSIONS
|
|
5524
|
+
|
|
5525
|
+
def _chat_attachment_payload(
|
|
5526
|
+
self,
|
|
5527
|
+
*,
|
|
5528
|
+
quest_id: str,
|
|
5529
|
+
name: str,
|
|
5530
|
+
stored_name: str,
|
|
5531
|
+
mime_type: str | None,
|
|
5532
|
+
asset_path: Path,
|
|
5533
|
+
draft_id: str | None,
|
|
5534
|
+
) -> dict[str, Any]:
|
|
5535
|
+
quest_root = self._quest_root(quest_id)
|
|
5536
|
+
resolved_path = asset_path.resolve()
|
|
5537
|
+
content_type = mimetypes.guess_type(resolved_path.name)[0] or mime_type or "application/octet-stream"
|
|
5538
|
+
quest_relative_path = resolved_path.relative_to(quest_root).as_posix()
|
|
5539
|
+
payload: dict[str, Any] = {
|
|
5540
|
+
"kind": "image" if str(content_type).startswith("image/") else "path",
|
|
5541
|
+
"name": name,
|
|
5542
|
+
"file_name": stored_name,
|
|
5543
|
+
"content_type": content_type,
|
|
5544
|
+
"path": str(resolved_path),
|
|
5545
|
+
"quest_relative_path": quest_relative_path,
|
|
5546
|
+
"asset_document_id": f"path::{quest_relative_path}",
|
|
5547
|
+
"asset_url": f"/api/quests/{quest_id}/documents/asset?document_id={quote(f'path::{quest_relative_path}', safe='')}",
|
|
5548
|
+
"size_bytes": resolved_path.stat().st_size if resolved_path.exists() else 0,
|
|
5549
|
+
"uploaded_at": utc_now(),
|
|
5550
|
+
"upload_origin": "web",
|
|
5551
|
+
}
|
|
5552
|
+
if draft_id:
|
|
5553
|
+
payload["draft_id"] = draft_id
|
|
5554
|
+
if self._is_readable_chat_attachment(resolved_path, content_type):
|
|
5555
|
+
payload["extracted_text_path"] = quest_relative_path
|
|
5556
|
+
return payload
|
|
5557
|
+
|
|
4862
5558
|
@staticmethod
|
|
4863
5559
|
def _normalize_workspace_relative_path(
|
|
4864
5560
|
relative: str | None,
|
|
@@ -5274,9 +5970,10 @@ class QuestService:
|
|
|
5274
5970
|
@staticmethod
|
|
5275
5971
|
def _default_message_queue() -> dict[str, Any]:
|
|
5276
5972
|
return {
|
|
5277
|
-
"version":
|
|
5973
|
+
"version": 2,
|
|
5278
5974
|
"pending": [],
|
|
5279
5975
|
"completed": [],
|
|
5976
|
+
"message_states": {},
|
|
5280
5977
|
}
|
|
5281
5978
|
|
|
5282
5979
|
def _default_runtime_state(
|
|
@@ -5307,6 +6004,7 @@ class QuestService:
|
|
|
5307
6004
|
"continuation_anchor": None,
|
|
5308
6005
|
"continuation_reason": None,
|
|
5309
6006
|
"continuation_updated_at": None,
|
|
6007
|
+
"waiting_notice": None,
|
|
5310
6008
|
"last_resume_source": None,
|
|
5311
6009
|
"last_resume_at": None,
|
|
5312
6010
|
"last_recovery_abandoned_run_id": None,
|
|
@@ -5318,6 +6016,7 @@ class QuestService:
|
|
|
5318
6016
|
"last_delivered_batch_id": None,
|
|
5319
6017
|
"last_delivered_at": None,
|
|
5320
6018
|
"retry_state": None,
|
|
6019
|
+
"turn_message_override": None,
|
|
5321
6020
|
}
|
|
5322
6021
|
|
|
5323
6022
|
def _default_agent_status(self, quest_root: Path) -> dict[str, Any]:
|
|
@@ -5358,9 +6057,11 @@ class QuestService:
|
|
|
5358
6057
|
payload = self._read_cached_json(self._message_queue_path(quest_root), self._default_message_queue())
|
|
5359
6058
|
if not isinstance(payload, dict):
|
|
5360
6059
|
payload = self._default_message_queue()
|
|
5361
|
-
payload.setdefault("version",
|
|
6060
|
+
payload.setdefault("version", 2)
|
|
5362
6061
|
payload.setdefault("pending", [])
|
|
5363
6062
|
payload.setdefault("completed", [])
|
|
6063
|
+
message_states = payload.get("message_states")
|
|
6064
|
+
payload["message_states"] = dict(message_states) if isinstance(message_states, dict) else {}
|
|
5364
6065
|
return payload
|
|
5365
6066
|
|
|
5366
6067
|
def _write_message_queue(self, quest_root: Path, payload: dict[str, Any]) -> None:
|
|
@@ -5388,6 +6089,7 @@ class QuestService:
|
|
|
5388
6089
|
merged["continuation_anchor"] = str(merged.get("continuation_anchor") or "").strip() or None
|
|
5389
6090
|
merged["continuation_reason"] = str(merged.get("continuation_reason") or "").strip() or None
|
|
5390
6091
|
merged["continuation_updated_at"] = str(merged.get("continuation_updated_at") or "").strip() or None
|
|
6092
|
+
merged["waiting_notice"] = dict(merged.get("waiting_notice") or {}) if isinstance(merged.get("waiting_notice"), dict) else None
|
|
5391
6093
|
merged["last_resume_source"] = str(merged.get("last_resume_source") or "").strip() or None
|
|
5392
6094
|
merged["last_resume_at"] = str(merged.get("last_resume_at") or "").strip() or None
|
|
5393
6095
|
merged["last_recovery_abandoned_run_id"] = str(merged.get("last_recovery_abandoned_run_id") or "").strip() or None
|
|
@@ -5396,6 +6098,11 @@ class QuestService:
|
|
|
5396
6098
|
merged["last_stage_fingerprint_at"] = str(merged.get("last_stage_fingerprint_at") or "").strip() or None
|
|
5397
6099
|
merged["same_fingerprint_auto_turn_count"] = int(merged.get("same_fingerprint_auto_turn_count") or 0)
|
|
5398
6100
|
merged["retry_state"] = dict(merged.get("retry_state") or {}) if isinstance(merged.get("retry_state"), dict) else None
|
|
6101
|
+
merged["turn_message_override"] = (
|
|
6102
|
+
dict(merged.get("turn_message_override") or {})
|
|
6103
|
+
if isinstance(merged.get("turn_message_override"), dict)
|
|
6104
|
+
else None
|
|
6105
|
+
)
|
|
5399
6106
|
return merged
|
|
5400
6107
|
|
|
5401
6108
|
def _write_runtime_state(self, quest_root: Path, payload: dict[str, Any]) -> None:
|
|
@@ -5418,6 +6125,7 @@ class QuestService:
|
|
|
5418
6125
|
continuation_anchor: str | None | object = _UNSET,
|
|
5419
6126
|
continuation_reason: str | None | object = _UNSET,
|
|
5420
6127
|
continuation_updated_at: str | None | object = _UNSET,
|
|
6128
|
+
waiting_notice: dict[str, Any] | None | object = _UNSET,
|
|
5421
6129
|
last_resume_source: str | None | object = _UNSET,
|
|
5422
6130
|
last_resume_at: str | None | object = _UNSET,
|
|
5423
6131
|
last_recovery_abandoned_run_id: str | None | object = _UNSET,
|
|
@@ -5430,6 +6138,7 @@ class QuestService:
|
|
|
5430
6138
|
last_delivered_at: str | None | object = _UNSET,
|
|
5431
6139
|
display_status: str | None | object = _UNSET,
|
|
5432
6140
|
retry_state: dict[str, Any] | None | object = _UNSET,
|
|
6141
|
+
turn_message_override: dict[str, Any] | None | object = _UNSET,
|
|
5433
6142
|
) -> dict[str, Any]:
|
|
5434
6143
|
with self._runtime_state_lock(quest_root):
|
|
5435
6144
|
state = self._read_runtime_state(quest_root)
|
|
@@ -5486,6 +6195,8 @@ class QuestService:
|
|
|
5486
6195
|
state["continuation_updated_at"] = str(continuation_updated_at or "").strip() or None
|
|
5487
6196
|
elif continuation_changed:
|
|
5488
6197
|
state["continuation_updated_at"] = now
|
|
6198
|
+
if waiting_notice is not _UNSET:
|
|
6199
|
+
state["waiting_notice"] = dict(waiting_notice) if isinstance(waiting_notice, dict) else None
|
|
5489
6200
|
if last_resume_source is not _UNSET:
|
|
5490
6201
|
state["last_resume_source"] = str(last_resume_source or "").strip() or None
|
|
5491
6202
|
if last_resume_at is not _UNSET:
|
|
@@ -5508,6 +6219,12 @@ class QuestService:
|
|
|
5508
6219
|
state["last_delivered_at"] = last_delivered_at
|
|
5509
6220
|
if retry_state is not _UNSET:
|
|
5510
6221
|
state["retry_state"] = dict(retry_state) if isinstance(retry_state, dict) else None
|
|
6222
|
+
if turn_message_override is not _UNSET:
|
|
6223
|
+
state["turn_message_override"] = (
|
|
6224
|
+
dict(turn_message_override)
|
|
6225
|
+
if isinstance(turn_message_override, dict)
|
|
6226
|
+
else None
|
|
6227
|
+
)
|
|
5511
6228
|
if last_transition_at is not _UNSET:
|
|
5512
6229
|
state["last_transition_at"] = last_transition_at
|
|
5513
6230
|
elif status_changed or run_changed:
|
|
@@ -5549,11 +6266,369 @@ class QuestService:
|
|
|
5549
6266
|
continuation_reason=reason,
|
|
5550
6267
|
)
|
|
5551
6268
|
|
|
6269
|
+
@staticmethod
|
|
6270
|
+
def _normalize_message_read_state(value: object, *, default: str = "read") -> str:
|
|
6271
|
+
normalized = str(value or "").strip().lower() or default
|
|
6272
|
+
return normalized if normalized in {"read", "unread"} else default
|
|
6273
|
+
|
|
6274
|
+
def _update_message_read_state(
|
|
6275
|
+
self,
|
|
6276
|
+
queue_payload: dict[str, Any],
|
|
6277
|
+
*,
|
|
6278
|
+
message_id: str | None,
|
|
6279
|
+
client_message_id: str | None = None,
|
|
6280
|
+
source: str | None = None,
|
|
6281
|
+
conversation_id: str | None = None,
|
|
6282
|
+
created_at: str | None = None,
|
|
6283
|
+
read_state: str,
|
|
6284
|
+
read_reason: str | None = None,
|
|
6285
|
+
read_at: str | None = None,
|
|
6286
|
+
read_interaction_id: str | None = None,
|
|
6287
|
+
read_run_id: str | None = None,
|
|
6288
|
+
) -> dict[str, Any] | None:
|
|
6289
|
+
normalized_message_id = str(message_id or "").strip()
|
|
6290
|
+
normalized_client_message_id = str(client_message_id or "").strip() or None
|
|
6291
|
+
if not normalized_message_id and not normalized_client_message_id:
|
|
6292
|
+
return None
|
|
6293
|
+
states = dict(queue_payload.get("message_states") or {})
|
|
6294
|
+
key = normalized_message_id or f"client:{normalized_client_message_id}"
|
|
6295
|
+
current = dict(states.get(key) or {})
|
|
6296
|
+
current["message_id"] = normalized_message_id or current.get("message_id")
|
|
6297
|
+
if normalized_client_message_id:
|
|
6298
|
+
current["client_message_id"] = normalized_client_message_id
|
|
6299
|
+
if source:
|
|
6300
|
+
current["source"] = str(source)
|
|
6301
|
+
if conversation_id:
|
|
6302
|
+
current["conversation_id"] = str(conversation_id)
|
|
6303
|
+
if created_at:
|
|
6304
|
+
current["created_at"] = str(created_at)
|
|
6305
|
+
current["read_state"] = self._normalize_message_read_state(read_state)
|
|
6306
|
+
current["read_reason"] = str(read_reason or "").strip() or None
|
|
6307
|
+
current["read_at"] = str(read_at or "").strip() or None
|
|
6308
|
+
current["read_interaction_id"] = str(read_interaction_id or "").strip() or None
|
|
6309
|
+
current["read_run_id"] = str(read_run_id or "").strip() or None
|
|
6310
|
+
current["updated_at"] = utc_now()
|
|
6311
|
+
states[key] = current
|
|
6312
|
+
if normalized_message_id:
|
|
6313
|
+
for existing_key, item in list(states.items()):
|
|
6314
|
+
if existing_key == key or not isinstance(item, dict):
|
|
6315
|
+
continue
|
|
6316
|
+
if str(item.get("message_id") or "").strip() == normalized_message_id:
|
|
6317
|
+
states.pop(existing_key, None)
|
|
6318
|
+
if normalized_client_message_id:
|
|
6319
|
+
states[normalized_message_id] = current
|
|
6320
|
+
if key != normalized_message_id:
|
|
6321
|
+
states.pop(key, None)
|
|
6322
|
+
key = normalized_message_id
|
|
6323
|
+
queue_payload["message_states"] = states
|
|
6324
|
+
return dict(states.get(key) or current)
|
|
6325
|
+
|
|
6326
|
+
def _message_read_state(
|
|
6327
|
+
self,
|
|
6328
|
+
quest_root: Path,
|
|
6329
|
+
*,
|
|
6330
|
+
message_id: str | None = None,
|
|
6331
|
+
client_message_id: str | None = None,
|
|
6332
|
+
) -> dict[str, Any] | None:
|
|
6333
|
+
queue_payload = self._read_message_queue(quest_root)
|
|
6334
|
+
states = queue_payload.get("message_states")
|
|
6335
|
+
if not isinstance(states, dict):
|
|
6336
|
+
return None
|
|
6337
|
+
normalized_message_id = str(message_id or "").strip()
|
|
6338
|
+
normalized_client_message_id = str(client_message_id or "").strip()
|
|
6339
|
+
if normalized_message_id:
|
|
6340
|
+
direct = states.get(normalized_message_id)
|
|
6341
|
+
if isinstance(direct, dict):
|
|
6342
|
+
return dict(direct)
|
|
6343
|
+
for item in states.values():
|
|
6344
|
+
if not isinstance(item, dict):
|
|
6345
|
+
continue
|
|
6346
|
+
if normalized_message_id and str(item.get("message_id") or "").strip() == normalized_message_id:
|
|
6347
|
+
return dict(item)
|
|
6348
|
+
if normalized_client_message_id and str(item.get("client_message_id") or "").strip() == normalized_client_message_id:
|
|
6349
|
+
return dict(item)
|
|
6350
|
+
return None
|
|
6351
|
+
|
|
6352
|
+
@staticmethod
|
|
6353
|
+
def _find_message_queue_entry(
|
|
6354
|
+
items: list[dict[str, Any]],
|
|
6355
|
+
*,
|
|
6356
|
+
message_id: str | None = None,
|
|
6357
|
+
client_message_id: str | None = None,
|
|
6358
|
+
) -> tuple[int, dict[str, Any]] | tuple[None, None]:
|
|
6359
|
+
normalized_message_id = str(message_id or "").strip()
|
|
6360
|
+
normalized_client_message_id = str(client_message_id or "").strip()
|
|
6361
|
+
if not normalized_message_id and not normalized_client_message_id:
|
|
6362
|
+
return None, None
|
|
6363
|
+
for index in range(len(items) - 1, -1, -1):
|
|
6364
|
+
item = items[index]
|
|
6365
|
+
if normalized_message_id and str(item.get("message_id") or "").strip() == normalized_message_id:
|
|
6366
|
+
return index, dict(item)
|
|
6367
|
+
if normalized_client_message_id and str(item.get("client_message_id") or "").strip() == normalized_client_message_id:
|
|
6368
|
+
return index, dict(item)
|
|
6369
|
+
return None, None
|
|
6370
|
+
|
|
6371
|
+
def pending_user_message_status(
|
|
6372
|
+
self,
|
|
6373
|
+
quest_root: Path,
|
|
6374
|
+
*,
|
|
6375
|
+
message_id: str | None = None,
|
|
6376
|
+
client_message_id: str | None = None,
|
|
6377
|
+
) -> dict[str, Any]:
|
|
6378
|
+
queue_payload = self._read_message_queue(quest_root)
|
|
6379
|
+
pending = [dict(item) for item in (queue_payload.get("pending") or []) if isinstance(item, dict)]
|
|
6380
|
+
completed = [dict(item) for item in (queue_payload.get("completed") or []) if isinstance(item, dict)]
|
|
6381
|
+
pending_index, pending_item = self._find_message_queue_entry(
|
|
6382
|
+
pending,
|
|
6383
|
+
message_id=message_id,
|
|
6384
|
+
client_message_id=client_message_id,
|
|
6385
|
+
)
|
|
6386
|
+
state_record = self._message_read_state(
|
|
6387
|
+
quest_root,
|
|
6388
|
+
message_id=message_id,
|
|
6389
|
+
client_message_id=client_message_id,
|
|
6390
|
+
)
|
|
6391
|
+
completed_index, completed_item = self._find_message_queue_entry(
|
|
6392
|
+
completed,
|
|
6393
|
+
message_id=message_id,
|
|
6394
|
+
client_message_id=client_message_id,
|
|
6395
|
+
)
|
|
6396
|
+
queue_state = "missing"
|
|
6397
|
+
if pending_index is not None and pending_item is not None:
|
|
6398
|
+
queue_state = "pending"
|
|
6399
|
+
elif state_record:
|
|
6400
|
+
read_reason = str(state_record.get("read_reason") or "").strip()
|
|
6401
|
+
queue_state = "withdrawn" if read_reason == "withdrawn_by_user" else "read"
|
|
6402
|
+
elif completed_index is not None and completed_item is not None:
|
|
6403
|
+
completed_status = str(completed_item.get("status") or "").strip()
|
|
6404
|
+
queue_state = "withdrawn" if completed_status == "withdrawn_by_user" else "read"
|
|
6405
|
+
return {
|
|
6406
|
+
"queue_state": queue_state,
|
|
6407
|
+
"pending_index": pending_index,
|
|
6408
|
+
"pending_item": pending_item,
|
|
6409
|
+
"completed_index": completed_index,
|
|
6410
|
+
"completed_item": completed_item,
|
|
6411
|
+
"message_state": state_record,
|
|
6412
|
+
}
|
|
6413
|
+
|
|
6414
|
+
def latest_pending_user_message(self, quest_id: str) -> dict[str, Any] | None:
|
|
6415
|
+
quest_root = self._quest_root(quest_id)
|
|
6416
|
+
queue_payload = self._read_message_queue(quest_root)
|
|
6417
|
+
pending = [dict(item) for item in (queue_payload.get("pending") or []) if isinstance(item, dict)]
|
|
6418
|
+
if not pending:
|
|
6419
|
+
return None
|
|
6420
|
+
latest = dict(pending[-1])
|
|
6421
|
+
return {
|
|
6422
|
+
"id": latest.get("message_id"),
|
|
6423
|
+
"message_id": latest.get("message_id"),
|
|
6424
|
+
"client_message_id": latest.get("client_message_id"),
|
|
6425
|
+
"role": "user",
|
|
6426
|
+
"source": latest.get("source"),
|
|
6427
|
+
"content": latest.get("content") or "",
|
|
6428
|
+
"created_at": latest.get("created_at"),
|
|
6429
|
+
"reply_to_interaction_id": latest.get("reply_to_interaction_id"),
|
|
6430
|
+
"attachments": [dict(item) for item in (latest.get("attachments") or []) if isinstance(item, dict)],
|
|
6431
|
+
}
|
|
6432
|
+
|
|
6433
|
+
def withdraw_pending_user_message(
|
|
6434
|
+
self,
|
|
6435
|
+
quest_id: str,
|
|
6436
|
+
*,
|
|
6437
|
+
message_id: str | None,
|
|
6438
|
+
source: str,
|
|
6439
|
+
) -> dict[str, Any]:
|
|
6440
|
+
normalized_message_id = str(message_id or "").strip()
|
|
6441
|
+
if not normalized_message_id:
|
|
6442
|
+
return {
|
|
6443
|
+
"ok": False,
|
|
6444
|
+
"status": "invalid_request",
|
|
6445
|
+
"message": "Message id is required.",
|
|
6446
|
+
}
|
|
6447
|
+
quest_root = self._quest_root(quest_id)
|
|
6448
|
+
queue_payload = self._read_message_queue(quest_root)
|
|
6449
|
+
status_payload = self.pending_user_message_status(
|
|
6450
|
+
quest_root,
|
|
6451
|
+
message_id=normalized_message_id,
|
|
6452
|
+
)
|
|
6453
|
+
pending_index = status_payload.get("pending_index")
|
|
6454
|
+
pending_item = dict(status_payload.get("pending_item") or {}) if isinstance(status_payload.get("pending_item"), dict) else None
|
|
6455
|
+
if pending_index is None or pending_item is None:
|
|
6456
|
+
queue_state = str(status_payload.get("queue_state") or "missing")
|
|
6457
|
+
message_state = (
|
|
6458
|
+
dict(status_payload.get("message_state") or {})
|
|
6459
|
+
if isinstance(status_payload.get("message_state"), dict)
|
|
6460
|
+
else None
|
|
6461
|
+
)
|
|
6462
|
+
if queue_state == "read":
|
|
6463
|
+
return {
|
|
6464
|
+
"ok": False,
|
|
6465
|
+
"status": "already_read",
|
|
6466
|
+
"message": "Withdrawal failed because this message was already sent to the agent.",
|
|
6467
|
+
"message_id": normalized_message_id,
|
|
6468
|
+
"current_message_state": message_state,
|
|
6469
|
+
}
|
|
6470
|
+
if queue_state == "withdrawn":
|
|
6471
|
+
return {
|
|
6472
|
+
"ok": True,
|
|
6473
|
+
"status": "already_withdrawn",
|
|
6474
|
+
"message": "This message was already withdrawn from the waiting queue.",
|
|
6475
|
+
"message_id": normalized_message_id,
|
|
6476
|
+
"current_message_state": message_state,
|
|
6477
|
+
}
|
|
6478
|
+
return {
|
|
6479
|
+
"ok": False,
|
|
6480
|
+
"status": "missing",
|
|
6481
|
+
"message": f"Message `{normalized_message_id}` is not waiting in the queue.",
|
|
6482
|
+
"message_id": normalized_message_id,
|
|
6483
|
+
"current_message_state": message_state,
|
|
6484
|
+
}
|
|
6485
|
+
|
|
6486
|
+
pending = [dict(item) for item in (queue_payload.get("pending") or []) if isinstance(item, dict)]
|
|
6487
|
+
completed = [dict(item) for item in (queue_payload.get("completed") or []) if isinstance(item, dict)]
|
|
6488
|
+
withdrawn_at = utc_now()
|
|
6489
|
+
withdrawn = {
|
|
6490
|
+
**pending.pop(int(pending_index)),
|
|
6491
|
+
"status": "withdrawn_by_user",
|
|
6492
|
+
"withdrawn_at": withdrawn_at,
|
|
6493
|
+
"withdrawn_by_source": source,
|
|
6494
|
+
}
|
|
6495
|
+
queue_payload["pending"] = pending
|
|
6496
|
+
queue_payload["completed"] = [*completed, withdrawn][-200:]
|
|
6497
|
+
state_record = self._update_message_read_state(
|
|
6498
|
+
queue_payload,
|
|
6499
|
+
message_id=str(withdrawn.get("message_id") or "").strip() or None,
|
|
6500
|
+
client_message_id=str(withdrawn.get("client_message_id") or "").strip() or None,
|
|
6501
|
+
source=str(withdrawn.get("source") or "").strip() or None,
|
|
6502
|
+
conversation_id=str(withdrawn.get("conversation_id") or "").strip() or None,
|
|
6503
|
+
created_at=str(withdrawn.get("created_at") or "").strip() or None,
|
|
6504
|
+
read_state="read",
|
|
6505
|
+
read_reason="withdrawn_by_user",
|
|
6506
|
+
read_at=withdrawn_at,
|
|
6507
|
+
)
|
|
6508
|
+
self._write_message_queue(quest_root, queue_payload)
|
|
6509
|
+
self.update_runtime_state(
|
|
6510
|
+
quest_root=quest_root,
|
|
6511
|
+
pending_user_message_count=len(pending),
|
|
6512
|
+
)
|
|
6513
|
+
self.append_message_read_state_event(
|
|
6514
|
+
quest_id,
|
|
6515
|
+
message_id=str(withdrawn.get("message_id") or "").strip() or None,
|
|
6516
|
+
client_message_id=str(withdrawn.get("client_message_id") or "").strip() or None,
|
|
6517
|
+
read_state=str((state_record or {}).get("read_state") or "read"),
|
|
6518
|
+
read_reason=str((state_record or {}).get("read_reason") or "withdrawn_by_user"),
|
|
6519
|
+
read_at=str((state_record or {}).get("read_at") or withdrawn_at),
|
|
6520
|
+
)
|
|
6521
|
+
append_jsonl(
|
|
6522
|
+
self._interaction_journal_path(quest_root),
|
|
6523
|
+
{
|
|
6524
|
+
"event_id": generate_id("evt"),
|
|
6525
|
+
"type": "user_message_withdrawn",
|
|
6526
|
+
"quest_id": quest_id,
|
|
6527
|
+
"message_id": normalized_message_id,
|
|
6528
|
+
"source": source,
|
|
6529
|
+
"created_at": withdrawn_at,
|
|
6530
|
+
},
|
|
6531
|
+
)
|
|
6532
|
+
self._write_active_user_requirements(quest_root, latest_requirement=None)
|
|
6533
|
+
return {
|
|
6534
|
+
"ok": True,
|
|
6535
|
+
"status": "withdrawn",
|
|
6536
|
+
"message": "The message was removed from the waiting queue.",
|
|
6537
|
+
"message_id": normalized_message_id,
|
|
6538
|
+
"pending_user_message_count": len(pending),
|
|
6539
|
+
"current_message_state": state_record,
|
|
6540
|
+
}
|
|
6541
|
+
|
|
6542
|
+
def enrich_conversation_message_event(self, quest_id: str, event: dict[str, Any]) -> dict[str, Any]:
|
|
6543
|
+
if str(event.get("type") or "").strip() != "conversation.message":
|
|
6544
|
+
return dict(event)
|
|
6545
|
+
quest_root = self._quest_root(quest_id)
|
|
6546
|
+
read_state = self._message_read_state(
|
|
6547
|
+
quest_root,
|
|
6548
|
+
message_id=str(event.get("message_id") or "").strip() or None,
|
|
6549
|
+
client_message_id=str(event.get("client_message_id") or "").strip() or None,
|
|
6550
|
+
)
|
|
6551
|
+
enriched = dict(event)
|
|
6552
|
+
if read_state:
|
|
6553
|
+
enriched["read_state"] = read_state.get("read_state")
|
|
6554
|
+
enriched["read_reason"] = read_state.get("read_reason")
|
|
6555
|
+
enriched["read_at"] = read_state.get("read_at")
|
|
6556
|
+
return enriched
|
|
6557
|
+
|
|
6558
|
+
def append_message_read_state_event(
|
|
6559
|
+
self,
|
|
6560
|
+
quest_id: str,
|
|
6561
|
+
*,
|
|
6562
|
+
message_id: str | None,
|
|
6563
|
+
client_message_id: str | None = None,
|
|
6564
|
+
read_state: str,
|
|
6565
|
+
read_reason: str | None = None,
|
|
6566
|
+
read_at: str | None = None,
|
|
6567
|
+
) -> dict[str, Any]:
|
|
6568
|
+
payload = {
|
|
6569
|
+
"event_id": generate_id("evt"),
|
|
6570
|
+
"type": "conversation.message_state",
|
|
6571
|
+
"quest_id": quest_id,
|
|
6572
|
+
"message_id": str(message_id or "").strip() or None,
|
|
6573
|
+
"client_message_id": str(client_message_id or "").strip() or None,
|
|
6574
|
+
"read_state": self._normalize_message_read_state(read_state),
|
|
6575
|
+
"read_reason": str(read_reason or "").strip() or None,
|
|
6576
|
+
"read_at": str(read_at or "").strip() or None,
|
|
6577
|
+
"created_at": utc_now(),
|
|
6578
|
+
}
|
|
6579
|
+
append_jsonl(self._quest_root(quest_id) / ".ds" / "events.jsonl", payload)
|
|
6580
|
+
return payload
|
|
6581
|
+
|
|
6582
|
+
def set_turn_message_override(
|
|
6583
|
+
self,
|
|
6584
|
+
quest_root: Path,
|
|
6585
|
+
*,
|
|
6586
|
+
turn_reason: str,
|
|
6587
|
+
message: str,
|
|
6588
|
+
message_ids: list[str] | None = None,
|
|
6589
|
+
delivery_batch_id: str | None = None,
|
|
6590
|
+
source: str | None = None,
|
|
6591
|
+
) -> dict[str, Any]:
|
|
6592
|
+
payload = {
|
|
6593
|
+
"turn_reason": str(turn_reason or "").strip() or "user_message",
|
|
6594
|
+
"message": str(message or "").strip(),
|
|
6595
|
+
"message_ids": [str(item).strip() for item in (message_ids or []) if str(item).strip()],
|
|
6596
|
+
"delivery_batch_id": str(delivery_batch_id or "").strip() or None,
|
|
6597
|
+
"source": str(source or "").strip() or None,
|
|
6598
|
+
"created_at": utc_now(),
|
|
6599
|
+
}
|
|
6600
|
+
self.update_runtime_state(
|
|
6601
|
+
quest_root=quest_root,
|
|
6602
|
+
turn_message_override=payload,
|
|
6603
|
+
)
|
|
6604
|
+
return payload
|
|
6605
|
+
|
|
6606
|
+
def consume_turn_message_override(
|
|
6607
|
+
self,
|
|
6608
|
+
quest_root: Path,
|
|
6609
|
+
*,
|
|
6610
|
+
expected_turn_reason: str | None = None,
|
|
6611
|
+
) -> dict[str, Any] | None:
|
|
6612
|
+
with self._runtime_state_lock(quest_root):
|
|
6613
|
+
state = self._read_runtime_state(quest_root)
|
|
6614
|
+
override = dict(state.get("turn_message_override") or {}) if isinstance(state.get("turn_message_override"), dict) else None
|
|
6615
|
+
if not override:
|
|
6616
|
+
return None
|
|
6617
|
+
if expected_turn_reason:
|
|
6618
|
+
normalized_expected = str(expected_turn_reason or "").strip()
|
|
6619
|
+
normalized_actual = str(override.get("turn_reason") or "").strip()
|
|
6620
|
+
if normalized_expected and normalized_actual != normalized_expected:
|
|
6621
|
+
return None
|
|
6622
|
+
state["turn_message_override"] = None
|
|
6623
|
+
self._write_runtime_state(quest_root, state)
|
|
6624
|
+
return override
|
|
6625
|
+
|
|
5552
6626
|
def _enqueue_user_message(self, quest_root: Path, record: dict[str, Any]) -> dict[str, Any]:
|
|
5553
6627
|
queue_payload = self._read_message_queue(quest_root)
|
|
5554
6628
|
source = str(record.get("source") or "local")
|
|
5555
6629
|
queue_record = {
|
|
5556
6630
|
"message_id": record.get("id"),
|
|
6631
|
+
"client_message_id": str(record.get("client_message_id") or "").strip() or None,
|
|
5557
6632
|
"source": source,
|
|
5558
6633
|
"conversation_id": self._normalize_binding_source(source),
|
|
5559
6634
|
"content": record.get("content") or "",
|
|
@@ -5563,6 +6638,17 @@ class QuestService:
|
|
|
5563
6638
|
"status": "queued",
|
|
5564
6639
|
}
|
|
5565
6640
|
queue_payload["pending"] = [*list(queue_payload.get("pending") or []), queue_record]
|
|
6641
|
+
self._update_message_read_state(
|
|
6642
|
+
queue_payload,
|
|
6643
|
+
message_id=str(queue_record.get("message_id") or "").strip() or None,
|
|
6644
|
+
client_message_id=str(queue_record.get("client_message_id") or "").strip() or None,
|
|
6645
|
+
source=source,
|
|
6646
|
+
conversation_id=str(queue_record.get("conversation_id") or "").strip() or None,
|
|
6647
|
+
created_at=str(queue_record.get("created_at") or "").strip() or None,
|
|
6648
|
+
read_state="unread",
|
|
6649
|
+
read_reason="queued",
|
|
6650
|
+
read_at=None,
|
|
6651
|
+
)
|
|
5566
6652
|
self._write_message_queue(quest_root, queue_payload)
|
|
5567
6653
|
self.update_runtime_state(
|
|
5568
6654
|
quest_root=quest_root,
|
|
@@ -5592,7 +6678,27 @@ class QuestService:
|
|
|
5592
6678
|
for item in read_jsonl(quest_root / ".ds" / "conversations" / "main.jsonl")
|
|
5593
6679
|
if str(item.get("role") or "") == "user"
|
|
5594
6680
|
]
|
|
5595
|
-
|
|
6681
|
+
filtered_user_messages = []
|
|
6682
|
+
for item in user_messages:
|
|
6683
|
+
state = self._message_read_state(
|
|
6684
|
+
quest_root,
|
|
6685
|
+
message_id=str(item.get("id") or "").strip() or None,
|
|
6686
|
+
client_message_id=str(item.get("client_message_id") or "").strip() or None,
|
|
6687
|
+
)
|
|
6688
|
+
if str((state or {}).get("read_reason") or "").strip() == "withdrawn_by_user":
|
|
6689
|
+
continue
|
|
6690
|
+
filtered_user_messages.append(item)
|
|
6691
|
+
latest = latest_requirement
|
|
6692
|
+
if latest is not None:
|
|
6693
|
+
latest_state = self._message_read_state(
|
|
6694
|
+
quest_root,
|
|
6695
|
+
message_id=str(latest.get("id") or "").strip() or None,
|
|
6696
|
+
client_message_id=str(latest.get("client_message_id") or "").strip() or None,
|
|
6697
|
+
)
|
|
6698
|
+
if str((latest_state or {}).get("read_reason") or "").strip() == "withdrawn_by_user":
|
|
6699
|
+
latest = None
|
|
6700
|
+
if latest is None:
|
|
6701
|
+
latest = filtered_user_messages[-1] if filtered_user_messages else None
|
|
5596
6702
|
lines = [
|
|
5597
6703
|
"# Active User Requirements",
|
|
5598
6704
|
"",
|
|
@@ -5611,12 +6717,17 @@ class QuestService:
|
|
|
5611
6717
|
"",
|
|
5612
6718
|
]
|
|
5613
6719
|
if latest:
|
|
6720
|
+
latest_rendered = self._agent_visible_user_message_content(
|
|
6721
|
+
quest_root,
|
|
6722
|
+
content=str(latest.get("content") or ""),
|
|
6723
|
+
attachments=[dict(item) for item in (latest.get("attachments") or []) if isinstance(item, dict)],
|
|
6724
|
+
)
|
|
5614
6725
|
lines.extend(
|
|
5615
6726
|
[
|
|
5616
6727
|
f"- source: {latest.get('source') or 'local'}",
|
|
5617
6728
|
f"- created_at: {latest.get('created_at') or utc_now()}",
|
|
5618
6729
|
"",
|
|
5619
|
-
|
|
6730
|
+
latest_rendered or "No latest requirement text was captured.",
|
|
5620
6731
|
"",
|
|
5621
6732
|
]
|
|
5622
6733
|
)
|
|
@@ -5633,11 +6744,15 @@ class QuestService:
|
|
|
5633
6744
|
"",
|
|
5634
6745
|
]
|
|
5635
6746
|
)
|
|
5636
|
-
if
|
|
5637
|
-
for index, item in enumerate(
|
|
6747
|
+
if filtered_user_messages:
|
|
6748
|
+
for index, item in enumerate(filtered_user_messages[-12:], start=1):
|
|
5638
6749
|
source = str(item.get("source") or "local").strip() or "local"
|
|
5639
6750
|
created_at = str(item.get("created_at") or "").strip() or "unknown"
|
|
5640
|
-
content =
|
|
6751
|
+
content = self._agent_visible_user_message_content(
|
|
6752
|
+
quest_root,
|
|
6753
|
+
content=str(item.get("content") or ""),
|
|
6754
|
+
attachments=[dict(value) for value in (item.get("attachments") or []) if isinstance(value, dict)],
|
|
6755
|
+
) or "(empty)"
|
|
5641
6756
|
lines.append(f"{index}. [{source}] [{created_at}] {content}")
|
|
5642
6757
|
else:
|
|
5643
6758
|
lines.append("1. No user messages yet.")
|
|
@@ -5679,11 +6794,31 @@ class QuestService:
|
|
|
5679
6794
|
}
|
|
5680
6795
|
queue_payload["pending"] = pending
|
|
5681
6796
|
queue_payload["completed"] = [*list(queue_payload.get("completed") or []), claimed][-200:]
|
|
6797
|
+
state_record = self._update_message_read_state(
|
|
6798
|
+
queue_payload,
|
|
6799
|
+
message_id=str(claimed.get("message_id") or "").strip() or None,
|
|
6800
|
+
client_message_id=str(claimed.get("client_message_id") or "").strip() or None,
|
|
6801
|
+
source=str(claimed.get("source") or "").strip() or None,
|
|
6802
|
+
conversation_id=str(claimed.get("conversation_id") or "").strip() or None,
|
|
6803
|
+
created_at=str(claimed.get("created_at") or "").strip() or None,
|
|
6804
|
+
read_state="read",
|
|
6805
|
+
read_reason="accepted_by_run",
|
|
6806
|
+
read_at=now,
|
|
6807
|
+
read_run_id=run_id,
|
|
6808
|
+
)
|
|
5682
6809
|
self._write_message_queue(quest_root, queue_payload)
|
|
5683
6810
|
self.update_runtime_state(
|
|
5684
6811
|
quest_root=quest_root,
|
|
5685
6812
|
pending_user_message_count=len(pending),
|
|
5686
6813
|
)
|
|
6814
|
+
self.append_message_read_state_event(
|
|
6815
|
+
quest_id,
|
|
6816
|
+
message_id=str(claimed.get("message_id") or "").strip() or None,
|
|
6817
|
+
client_message_id=str(claimed.get("client_message_id") or "").strip() or None,
|
|
6818
|
+
read_state=str((state_record or {}).get("read_state") or "read"),
|
|
6819
|
+
read_reason=str((state_record or {}).get("read_reason") or "accepted_by_run"),
|
|
6820
|
+
read_at=str((state_record or {}).get("read_at") or now),
|
|
6821
|
+
)
|
|
5687
6822
|
append_jsonl(
|
|
5688
6823
|
self._interaction_journal_path(quest_root),
|
|
5689
6824
|
{
|
|
@@ -5733,6 +6868,26 @@ class QuestService:
|
|
|
5733
6868
|
]
|
|
5734
6869
|
queue_payload["pending"] = []
|
|
5735
6870
|
queue_payload["completed"] = [*list(queue_payload.get("completed") or []), *cancelled][-200:]
|
|
6871
|
+
for item in cancelled:
|
|
6872
|
+
self._update_message_read_state(
|
|
6873
|
+
queue_payload,
|
|
6874
|
+
message_id=str(item.get("message_id") or "").strip() or None,
|
|
6875
|
+
client_message_id=str(item.get("client_message_id") or "").strip() or None,
|
|
6876
|
+
source=str(item.get("source") or "").strip() or None,
|
|
6877
|
+
conversation_id=str(item.get("conversation_id") or "").strip() or None,
|
|
6878
|
+
created_at=str(item.get("created_at") or "").strip() or None,
|
|
6879
|
+
read_state="read",
|
|
6880
|
+
read_reason=reason,
|
|
6881
|
+
read_at=now,
|
|
6882
|
+
)
|
|
6883
|
+
self.append_message_read_state_event(
|
|
6884
|
+
quest_id,
|
|
6885
|
+
message_id=str(item.get("message_id") or "").strip() or None,
|
|
6886
|
+
client_message_id=str(item.get("client_message_id") or "").strip() or None,
|
|
6887
|
+
read_state="read",
|
|
6888
|
+
read_reason=reason,
|
|
6889
|
+
read_at=now,
|
|
6890
|
+
)
|
|
5736
6891
|
self._write_message_queue(quest_root, queue_payload)
|
|
5737
6892
|
append_jsonl(
|
|
5738
6893
|
self._interaction_journal_path(quest_root),
|
|
@@ -5892,11 +7047,13 @@ class QuestService:
|
|
|
5892
7047
|
*,
|
|
5893
7048
|
interaction_id: str | None,
|
|
5894
7049
|
limit: int = 10,
|
|
7050
|
+
delivery_reason: str = "artifact_mailbox",
|
|
5895
7051
|
) -> dict[str, Any]:
|
|
5896
7052
|
queue_payload = self._read_message_queue(quest_root)
|
|
5897
7053
|
pending = [dict(item) for item in (queue_payload.get("pending") or [])]
|
|
5898
7054
|
recent_records = self.latest_artifact_interaction_records(quest_root, limit=max(limit, 10))
|
|
5899
7055
|
delivered_messages: list[dict[str, Any]] = []
|
|
7056
|
+
delivered_state_records: list[dict[str, Any]] = []
|
|
5900
7057
|
delivery_batch = None
|
|
5901
7058
|
now = utc_now()
|
|
5902
7059
|
|
|
@@ -5913,6 +7070,29 @@ class QuestService:
|
|
|
5913
7070
|
delivered_messages.append(delivered)
|
|
5914
7071
|
queue_payload["pending"] = []
|
|
5915
7072
|
queue_payload["completed"] = [*list(queue_payload.get("completed") or []), *delivered_messages][-200:]
|
|
7073
|
+
for item in delivered_messages:
|
|
7074
|
+
state_record = self._update_message_read_state(
|
|
7075
|
+
queue_payload,
|
|
7076
|
+
message_id=str(item.get("message_id") or "").strip() or None,
|
|
7077
|
+
client_message_id=str(item.get("client_message_id") or "").strip() or None,
|
|
7078
|
+
source=str(item.get("source") or "").strip() or None,
|
|
7079
|
+
conversation_id=str(item.get("conversation_id") or "").strip() or None,
|
|
7080
|
+
created_at=str(item.get("created_at") or "").strip() or None,
|
|
7081
|
+
read_state="read",
|
|
7082
|
+
read_reason=delivery_reason,
|
|
7083
|
+
read_at=now,
|
|
7084
|
+
read_interaction_id=interaction_id,
|
|
7085
|
+
)
|
|
7086
|
+
self.append_message_read_state_event(
|
|
7087
|
+
quest_root.name,
|
|
7088
|
+
message_id=str(item.get("message_id") or "").strip() or None,
|
|
7089
|
+
client_message_id=str(item.get("client_message_id") or "").strip() or None,
|
|
7090
|
+
read_state=str((state_record or {}).get("read_state") or "read"),
|
|
7091
|
+
read_reason=str((state_record or {}).get("read_reason") or delivery_reason),
|
|
7092
|
+
read_at=str((state_record or {}).get("read_at") or now),
|
|
7093
|
+
)
|
|
7094
|
+
if state_record:
|
|
7095
|
+
delivered_state_records.append(dict(state_record))
|
|
5916
7096
|
self._write_message_queue(quest_root, queue_payload)
|
|
5917
7097
|
append_jsonl(
|
|
5918
7098
|
self._interaction_journal_path(quest_root),
|
|
@@ -5935,6 +7115,8 @@ class QuestService:
|
|
|
5935
7115
|
delivery_batch = {
|
|
5936
7116
|
"batch_id": batch_id,
|
|
5937
7117
|
"message_ids": [item.get("message_id") for item in delivered_messages],
|
|
7118
|
+
"client_message_ids": [item.get("client_message_id") for item in delivered_messages],
|
|
7119
|
+
"delivered_at": now,
|
|
5938
7120
|
}
|
|
5939
7121
|
else:
|
|
5940
7122
|
self.update_runtime_state(
|
|
@@ -5942,15 +7124,45 @@ class QuestService:
|
|
|
5942
7124
|
pending_user_message_count=0,
|
|
5943
7125
|
)
|
|
5944
7126
|
|
|
7127
|
+
state_by_message_id = {
|
|
7128
|
+
str(item.get("message_id") or "").strip(): dict(item)
|
|
7129
|
+
for item in delivered_state_records
|
|
7130
|
+
if str(item.get("message_id") or "").strip()
|
|
7131
|
+
}
|
|
7132
|
+
state_by_client_message_id = {
|
|
7133
|
+
str(item.get("client_message_id") or "").strip(): dict(item)
|
|
7134
|
+
for item in delivered_state_records
|
|
7135
|
+
if str(item.get("client_message_id") or "").strip()
|
|
7136
|
+
}
|
|
5945
7137
|
recent_inbound_messages = [
|
|
5946
7138
|
{
|
|
5947
7139
|
"message_id": item.get("message_id"),
|
|
7140
|
+
"client_message_id": item.get("client_message_id"),
|
|
5948
7141
|
"source": str(item.get("conversation_id") or item.get("source") or "local").split(":", 1)[0],
|
|
5949
7142
|
"conversation_id": item.get("conversation_id") or self._normalize_binding_source(str(item.get("source") or "local")),
|
|
5950
7143
|
"sender": "user",
|
|
5951
7144
|
"created_at": item.get("created_at"),
|
|
7145
|
+
"read_state": (
|
|
7146
|
+
state_by_message_id.get(str(item.get("message_id") or "").strip())
|
|
7147
|
+
or state_by_client_message_id.get(str(item.get("client_message_id") or "").strip())
|
|
7148
|
+
or {}
|
|
7149
|
+
).get("read_state") or "read",
|
|
7150
|
+
"read_reason": (
|
|
7151
|
+
state_by_message_id.get(str(item.get("message_id") or "").strip())
|
|
7152
|
+
or state_by_client_message_id.get(str(item.get("client_message_id") or "").strip())
|
|
7153
|
+
or {}
|
|
7154
|
+
).get("read_reason") or delivery_reason,
|
|
7155
|
+
"read_at": (
|
|
7156
|
+
state_by_message_id.get(str(item.get("message_id") or "").strip())
|
|
7157
|
+
or state_by_client_message_id.get(str(item.get("client_message_id") or "").strip())
|
|
7158
|
+
or {}
|
|
7159
|
+
).get("read_at") or now,
|
|
5952
7160
|
"text": item.get("content") or "",
|
|
5953
|
-
"content":
|
|
7161
|
+
"content": self._agent_visible_user_message_content(
|
|
7162
|
+
quest_root,
|
|
7163
|
+
content=str(item.get("content") or ""),
|
|
7164
|
+
attachments=[dict(attachment) for attachment in (item.get("attachments") or []) if isinstance(attachment, dict)],
|
|
7165
|
+
),
|
|
5954
7166
|
"attachments": [dict(attachment) for attachment in (item.get("attachments") or []) if isinstance(attachment, dict)],
|
|
5955
7167
|
"reply_to_interaction_id": item.get("reply_to_interaction_id"),
|
|
5956
7168
|
}
|
|
@@ -5998,7 +7210,12 @@ class QuestService:
|
|
|
5998
7210
|
]
|
|
5999
7211
|
for index, item in enumerate(delivered_messages, start=1):
|
|
6000
7212
|
source = str(item.get("conversation_id") or item.get("source") or "local")
|
|
6001
|
-
|
|
7213
|
+
rendered_content = self._agent_visible_user_message_content(
|
|
7214
|
+
quest_root,
|
|
7215
|
+
content=str(item.get("content") or ""),
|
|
7216
|
+
attachments=[dict(attachment) for attachment in (item.get("attachments") or []) if isinstance(attachment, dict)],
|
|
7217
|
+
)
|
|
7218
|
+
lines.append(f"{index}. [{source}] {rendered_content}")
|
|
6002
7219
|
agent_instruction = "\n".join(lines).strip()
|
|
6003
7220
|
else:
|
|
6004
7221
|
lines = [
|
|
@@ -6039,12 +7256,60 @@ class QuestService:
|
|
|
6039
7256
|
return {
|
|
6040
7257
|
"delivery_batch": delivery_batch,
|
|
6041
7258
|
"recent_inbound_messages": recent_inbound_messages,
|
|
7259
|
+
"message_states": delivered_state_records,
|
|
6042
7260
|
"recent_interaction_records": recent_records[-10:],
|
|
6043
7261
|
"agent_instruction": agent_instruction,
|
|
6044
7262
|
"queued_message_count_before_delivery": len(pending),
|
|
6045
7263
|
"queued_message_count_after_delivery": len(queue_payload.get("pending") or []),
|
|
6046
7264
|
}
|
|
6047
7265
|
|
|
7266
|
+
def _agent_visible_user_message_content(
|
|
7267
|
+
self,
|
|
7268
|
+
quest_root: Path,
|
|
7269
|
+
*,
|
|
7270
|
+
content: str,
|
|
7271
|
+
attachments: list[dict[str, Any]] | None = None,
|
|
7272
|
+
) -> str:
|
|
7273
|
+
base = str(content or "").strip()
|
|
7274
|
+
normalized_attachments = [dict(item) for item in (attachments or []) if isinstance(item, dict)]
|
|
7275
|
+
if not normalized_attachments:
|
|
7276
|
+
return base
|
|
7277
|
+
lines: list[str] = []
|
|
7278
|
+
if base:
|
|
7279
|
+
lines.extend([base, ""])
|
|
7280
|
+
lines.append(
|
|
7281
|
+
self.localized_copy(
|
|
7282
|
+
quest_root=quest_root,
|
|
7283
|
+
zh="系统提示:用户刚刚发送了附件。请优先阅读这些 quest 本地文件,再继续处理这条请求:",
|
|
7284
|
+
en="System note: the user just sent attachments. Read these quest-local files first before continuing this request:",
|
|
7285
|
+
)
|
|
7286
|
+
)
|
|
7287
|
+
for index, item in enumerate(normalized_attachments, start=1):
|
|
7288
|
+
label = str(
|
|
7289
|
+
item.get("name")
|
|
7290
|
+
or item.get("file_name")
|
|
7291
|
+
or item.get("quest_relative_path")
|
|
7292
|
+
or item.get("path")
|
|
7293
|
+
or item.get("url")
|
|
7294
|
+
or f"attachment-{index}"
|
|
7295
|
+
).strip()
|
|
7296
|
+
content_type = str(item.get("content_type") or item.get("mime_type") or "").strip()
|
|
7297
|
+
location = str(
|
|
7298
|
+
item.get("extracted_text_path")
|
|
7299
|
+
or item.get("ocr_text_path")
|
|
7300
|
+
or item.get("archive_manifest_path")
|
|
7301
|
+
or item.get("quest_relative_path")
|
|
7302
|
+
or item.get("url")
|
|
7303
|
+
or ("hidden" if str(item.get("path") or "").strip() else "unavailable")
|
|
7304
|
+
).strip()
|
|
7305
|
+
error = str(item.get("download_error") or item.get("path_error") or "").strip()
|
|
7306
|
+
suffix = f" ({content_type})" if content_type else ""
|
|
7307
|
+
if error:
|
|
7308
|
+
lines.append(f"- {label}{suffix}: {location} | attachment_error={error}")
|
|
7309
|
+
else:
|
|
7310
|
+
lines.append(f"- {label}{suffix}: {location}")
|
|
7311
|
+
return "\n".join(lines).strip()
|
|
7312
|
+
|
|
6048
7313
|
@staticmethod
|
|
6049
7314
|
def _document_resolution_root(quest_root: Path, workspace_root: Path, document_id: str) -> Path:
|
|
6050
7315
|
if document_id.startswith(("questpath::", "memory::")):
|
|
@@ -6406,19 +7671,36 @@ class QuestService:
|
|
|
6406
7671
|
return True
|
|
6407
7672
|
if relative.startswith(".ds/worktrees/"):
|
|
6408
7673
|
return True
|
|
7674
|
+
heavy_runtime_roots = {
|
|
7675
|
+
".ds/bash_exec",
|
|
7676
|
+
".ds/runs",
|
|
7677
|
+
".ds/codex_history",
|
|
7678
|
+
".ds/codex_homes",
|
|
7679
|
+
".ds/claude-home",
|
|
7680
|
+
".ds/opencode-home",
|
|
7681
|
+
".ds/evidence_packets",
|
|
7682
|
+
".ds/slim_backups",
|
|
7683
|
+
".ds/cold_archive",
|
|
7684
|
+
}
|
|
7685
|
+
normalized = relative.strip("/")
|
|
7686
|
+
if any(normalized == root or normalized.startswith(f"{root}/") for root in heavy_runtime_roots):
|
|
7687
|
+
return True
|
|
6409
7688
|
parts = PurePosixPath(relative).parts
|
|
6410
7689
|
return "__pycache__" in parts or ".pytest_cache" in parts
|
|
6411
7690
|
|
|
6412
7691
|
@staticmethod
|
|
6413
7692
|
def _skip_explorer_profile_relative(relative: str, profile: str | None) -> bool:
|
|
6414
|
-
|
|
7693
|
+
profile = str(profile or "").strip().lower()
|
|
7694
|
+
if profile not in {"mobile", "workspace"}:
|
|
6415
7695
|
return False
|
|
6416
7696
|
normalized = relative.strip("/")
|
|
6417
7697
|
if not normalized:
|
|
6418
7698
|
return False
|
|
6419
7699
|
parts = PurePosixPath(normalized).parts
|
|
6420
7700
|
top = parts[0] if parts else normalized
|
|
6421
|
-
if top in {".codex", ".claude", ".
|
|
7701
|
+
if top in {".codex", ".claude", ".kimi", ".opencode", ".ds", "tmp", "userfiles"}:
|
|
7702
|
+
return True
|
|
7703
|
+
if profile == "mobile" and top == "artifacts":
|
|
6422
7704
|
return True
|
|
6423
7705
|
if top.startswith(".") and normalized not in {".gitignore"}:
|
|
6424
7706
|
return True
|
|
@@ -6426,7 +7708,8 @@ class QuestService:
|
|
|
6426
7708
|
|
|
6427
7709
|
@staticmethod
|
|
6428
7710
|
def _truncate_explorer_directory(relative: str, *, profile: str | None, depth: int) -> bool:
|
|
6429
|
-
|
|
7711
|
+
profile = str(profile or "").strip().lower()
|
|
7712
|
+
if profile not in {"mobile", "workspace"}:
|
|
6430
7713
|
return False
|
|
6431
7714
|
normalized = relative.strip("/")
|
|
6432
7715
|
if not normalized:
|
|
@@ -6435,11 +7718,19 @@ class QuestService:
|
|
|
6435
7718
|
top = parts[0] if parts else normalized
|
|
6436
7719
|
if top == "memory":
|
|
6437
7720
|
return False
|
|
7721
|
+
if profile == "mobile":
|
|
7722
|
+
if top == "baselines":
|
|
7723
|
+
return depth >= 1
|
|
7724
|
+
if top in {"literature", "paper", "experiments", "handoffs"}:
|
|
7725
|
+
return depth >= 2
|
|
7726
|
+
return depth >= 1
|
|
6438
7727
|
if top == "baselines":
|
|
7728
|
+
return depth >= 2
|
|
7729
|
+
if top == "artifacts":
|
|
6439
7730
|
return depth >= 1
|
|
6440
7731
|
if top in {"literature", "paper", "experiments", "handoffs"}:
|
|
6441
|
-
return depth >=
|
|
6442
|
-
return depth >=
|
|
7732
|
+
return depth >= 3
|
|
7733
|
+
return depth >= 2
|
|
6443
7734
|
|
|
6444
7735
|
@staticmethod
|
|
6445
7736
|
def _classify_path_scope(quest_root: Path, path: Path) -> tuple[str, bool]:
|