@researai/deepscientist 1.5.16 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +196 -32
- package/bin/ds.js +924 -66
- package/docs/en/00_QUICK_START.md +195 -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 +78 -7
- 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 +624 -180
- 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 +386 -0
- 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 +266 -0
- package/docs/en/99_ACKNOWLEDGEMENTS.md +24 -19
- package/docs/en/README.md +48 -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 +142 -18
- 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 +54 -8
- 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 +552 -181
- package/docs/zh/21_LOCAL_MODEL_BACKENDS_GUIDE.md +384 -0
- 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 +33 -7
- package/install.sh +168 -20
- package/package.json +5 -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/bridges/connectors.py +8 -2
- package/src/deepscientist/channels/weixin_ilink.py +8 -1
- package/src/deepscientist/cli.py +92 -17
- package/src/deepscientist/codex_cli_compat.py +187 -74
- package/src/deepscientist/config/models.py +82 -11
- package/src/deepscientist/config/service.py +1077 -93
- package/src/deepscientist/connector/weixin_support.py +48 -17
- package/src/deepscientist/daemon/api/handlers.py +827 -235
- package/src/deepscientist/daemon/api/router.py +81 -1
- package/src/deepscientist/daemon/app.py +1512 -85
- package/src/deepscientist/diagnostics/__init__.py +6 -0
- package/src/deepscientist/diagnostics/runner_failures.py +277 -0
- package/src/deepscientist/doctor.py +407 -56
- 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 +850 -88
- 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 +1852 -161
- 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 +480 -35
- 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 +80 -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 -10
- package/src/prompts/start_setup/system.md +422 -0
- package/src/prompts/system.md +411 -304
- package/src/prompts/system_copilot.md +89 -0
- 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-DnSm0GZn.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-itb0tltR.js → CodeViewerPlugin-CqDpgjik.js} +4 -4
- package/src/ui/dist/assets/{DocViewerPlugin-DqKkiCI6.js → DocViewerPlugin-UDBgt8-4.js} +3 -3
- package/src/ui/dist/assets/GitCommitViewerPlugin-BmHtZ0bZ.js +6 -0
- package/src/ui/dist/assets/{GitDiffViewerPlugin-DxL2ezFG.js → GitDiffViewerPlugin-CAxjNorQ.js} +2 -2
- package/src/ui/dist/assets/{GitSnapshotViewer-B_RQm1YZ.js → GitSnapshotViewer-CweA6VON.js} +2 -2
- package/src/ui/dist/assets/{ImageViewerPlugin-tHqlXY3n.js → ImageViewerPlugin-C8wHGvGN.js} +5 -5
- package/src/ui/dist/assets/LabPlugin-COyyLUol.js +32 -0
- package/src/ui/dist/assets/{LatexPlugin-B495DTXC.js → LatexPlugin-BQjAaA5J.js} +4 -4
- package/src/ui/dist/assets/{MarkdownViewerPlugin-DG28-61B.js → MarkdownViewerPlugin-Dy1NE2dI.js} +3 -3
- package/src/ui/dist/assets/{MarketplacePlugin-BiOGT-Kj.js → MarketplacePlugin-DMIZtEJ2.js} +2 -2
- package/src/ui/dist/assets/NotebookEditor-CFHMq_Qt.js +91 -0
- package/src/ui/dist/assets/{NotebookEditor-CVsj8h_T.js → NotebookEditor-WFyd8Ybt.js} +23 -23
- package/src/ui/dist/assets/{PdfLoader-CASDQmxJ.js → PdfLoader-CLE5u5TS.js} +3 -3
- package/src/ui/dist/assets/{PdfMarkdownPlugin-BFhwoKsY.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-CB4DYfWO.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-DLC6G24T.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-CwMn-iqb.js → file-jump-queue-DeQBikaw.js} +3 -3
- package/src/ui/dist/assets/{file-socket-Cu4Qln7Y.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-wQ7RIIRd.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-CsX08Qno.js → project-sync-DPmWKmKD.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-R-GWEhzS.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-COFACy7V.js +0 -204
- package/src/ui/dist/assets/CliPlugin-CvwCmDQ5.js +0 -109
- package/src/ui/dist/assets/CodeEditorPlugin-cOqSa0xq.js +0 -2
- package/src/ui/dist/assets/GitCommitViewerPlugin-DVgNHBCS.js +0 -1
- package/src/ui/dist/assets/LabCopilotPanel-ClMbq5Yu.js +0 -14
- package/src/ui/dist/assets/LabPlugin-L_SuE8ow.js +0 -22
- package/src/ui/dist/assets/NotebookEditor-C-4Kt1p9.js +0 -81
- package/src/ui/dist/assets/PdfViewerPlugin-DcOzU9vd.js +0 -17
- package/src/ui/dist/assets/SearchPlugin-CHj7M58O.js +0 -16
- package/src/ui/dist/assets/VNCViewer-CjlbyCB3.js +0 -11
- package/src/ui/dist/assets/bot-CFkZY-JP.js +0 -6
- package/src/ui/dist/assets/chevron-up-Dq5ofbht.js +0 -6
- package/src/ui/dist/assets/file-content-Dv4LoZec.js +0 -1
- package/src/ui/dist/assets/file-diff-panel-Denq-lC3.js +0 -1
- package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +0 -1
- package/src/ui/dist/assets/git-commit-horizontal-BUh6G52n.js +0 -6
- package/src/ui/dist/assets/image-B9HUUddG.js +0 -6
- package/src/ui/dist/assets/index-B2B1sg-M.js +0 -1
- package/src/ui/dist/assets/index-Cgla8biy.css +0 -33
- package/src/ui/dist/assets/index-DRyx7vAc.js +0 -1
- package/src/ui/dist/assets/index-Gbl53BNp.js +0 -2496
- package/src/ui/dist/assets/pdf-effect-queue-ZtnHFCAi.js +0 -6
- package/src/ui/dist/assets/popover-DL6h35vr.js +0 -1
- package/src/ui/dist/assets/select-DvmXt1yY.js +0 -11
- package/src/ui/dist/assets/sigma-7jpXazui.js +0 -6
- package/src/ui/dist/assets/trash-xA7kFt8i.js +0 -11
- package/src/ui/dist/assets/useCliAccess-DsMwDjOp.js +0 -1
- package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +0 -1
|
@@ -5,10 +5,10 @@ from collections import deque
|
|
|
5
5
|
from contextlib import contextmanager
|
|
6
6
|
from datetime import UTC, datetime, timedelta
|
|
7
7
|
import hashlib
|
|
8
|
-
import subprocess
|
|
9
8
|
import json
|
|
10
9
|
import mimetypes
|
|
11
10
|
import re
|
|
11
|
+
import shutil
|
|
12
12
|
import threading
|
|
13
13
|
import time
|
|
14
14
|
from pathlib import Path, PurePosixPath
|
|
@@ -27,11 +27,12 @@ from ..file_lock import advisory_file_lock
|
|
|
27
27
|
from ..gitops import current_branch, export_git_graph, head_commit, init_repo, list_branch_canvas, list_commit_canvas
|
|
28
28
|
from ..home import repo_root
|
|
29
29
|
from ..registries import BaselineRegistry
|
|
30
|
-
from ..shared import append_jsonl, ensure_dir, generate_id, iter_jsonl, read_json, read_jsonl, read_jsonl_tail, read_text, read_yaml, resolve_within, run_command, sha256_text, slugify, utc_now, write_json, write_text, write_yaml
|
|
30
|
+
from ..shared import append_jsonl, ensure_dir, generate_id, iter_jsonl, read_json, read_jsonl, read_jsonl_tail, read_text, read_yaml, resolve_within, run_command, run_command_bytes, sha256_text, slugify, utc_now, write_json, write_text, write_yaml
|
|
31
31
|
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,36 +404,71 @@ 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
|
|
|
437
|
+
def _require_initialized_quest_root(self, quest_id: str) -> Path:
|
|
438
|
+
quest_root = self._quest_root(quest_id)
|
|
439
|
+
if not quest_root.exists() or not self._quest_yaml_path(quest_root).exists():
|
|
440
|
+
raise FileNotFoundError(f"Unknown quest `{quest_id}`.")
|
|
441
|
+
return quest_root
|
|
442
|
+
|
|
325
443
|
def _normalized_binding_sources(self, sources: list[Any] | None) -> list[str]:
|
|
326
444
|
local_present = False
|
|
327
|
-
|
|
445
|
+
external_sources: list[str] = []
|
|
446
|
+
external_index: dict[str, int] = {}
|
|
328
447
|
for raw in sources or []:
|
|
329
448
|
normalized = self._normalize_binding_source(raw)
|
|
330
449
|
if not normalized:
|
|
331
450
|
continue
|
|
332
|
-
if normalized == "local:default":
|
|
333
|
-
local_present = True
|
|
334
|
-
continue
|
|
335
451
|
parsed = parse_conversation_id(normalized)
|
|
336
452
|
connector = str((parsed or {}).get("connector") or "").strip().lower()
|
|
337
|
-
if connector == "local":
|
|
453
|
+
if normalized == "local:default" or connector == "local":
|
|
338
454
|
local_present = True
|
|
339
455
|
continue
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
343
463
|
if local_present:
|
|
344
|
-
return ["local:default"]
|
|
345
|
-
return
|
|
464
|
+
return ["local:default", *external_sources]
|
|
465
|
+
return external_sources
|
|
346
466
|
|
|
347
467
|
def _binding_sources_payload(self, quest_root: Path) -> dict[str, list[str]]:
|
|
348
468
|
bindings_path = quest_root / ".ds" / "bindings.json"
|
|
349
|
-
payload = read_json(bindings_path, {"sources": [
|
|
350
|
-
raw_sources = payload.get("sources") if isinstance(payload, dict) else [
|
|
351
|
-
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 [])
|
|
352
472
|
return {"sources": sources}
|
|
353
473
|
|
|
354
474
|
def preferred_locale(self, quest_root: Path | None = None) -> str:
|
|
@@ -394,11 +514,21 @@ class QuestService:
|
|
|
394
514
|
if not isinstance(payload, dict):
|
|
395
515
|
payload = {}
|
|
396
516
|
normalized = dict(payload)
|
|
397
|
-
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)
|
|
398
519
|
normalized.setdefault("baseline_gate", "pending")
|
|
399
520
|
normalized.setdefault("confirmed_baseline_ref", None)
|
|
400
521
|
normalized.setdefault("requested_baseline_ref", None)
|
|
401
|
-
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"
|
|
402
532
|
return normalized
|
|
403
533
|
|
|
404
534
|
@staticmethod
|
|
@@ -876,7 +1006,7 @@ class QuestService:
|
|
|
876
1006
|
if not artifacts_root.exists():
|
|
877
1007
|
continue
|
|
878
1008
|
for folder in sorted(artifacts_root.iterdir()):
|
|
879
|
-
if not folder.is_dir():
|
|
1009
|
+
if not folder.is_dir() or folder.name == "graphs":
|
|
880
1010
|
continue
|
|
881
1011
|
for path in sorted(folder.glob("*.json")):
|
|
882
1012
|
item = self._read_cached_json(path, {})
|
|
@@ -1509,16 +1639,18 @@ class QuestService:
|
|
|
1509
1639
|
for artifact in recent_artifacts:
|
|
1510
1640
|
payload = artifact.get("payload") if isinstance(artifact.get("payload"), dict) else {}
|
|
1511
1641
|
artifact_path = artifact.get("path")
|
|
1642
|
+
artifact_kind = str(payload.get("kind") or artifact.get("kind") or "").strip()
|
|
1512
1643
|
entries.append(
|
|
1513
1644
|
{
|
|
1514
1645
|
"id": f"artifact:{payload.get('artifact_id') or artifact_path}",
|
|
1515
1646
|
"kind": "artifact",
|
|
1516
|
-
"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"),
|
|
1517
1648
|
"summary": payload.get("summary") or payload.get("message") or payload.get("reason") or "Artifact updated.",
|
|
1518
1649
|
"status": payload.get("status"),
|
|
1519
1650
|
"reason": payload.get("reason"),
|
|
1520
1651
|
"created_at": payload.get("updated_at") or payload.get("created_at"),
|
|
1521
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"),
|
|
1522
1654
|
}
|
|
1523
1655
|
)
|
|
1524
1656
|
add_file(str(artifact_path) if artifact_path else None, source="artifact")
|
|
@@ -1826,6 +1958,37 @@ class QuestService:
|
|
|
1826
1958
|
best_root = paper_root
|
|
1827
1959
|
return best_root
|
|
1828
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
|
+
|
|
1829
1992
|
def _outline_record_from_paper_root(self, paper_root: Path) -> dict[str, Any]:
|
|
1830
1993
|
outline_root = paper_root / "outline"
|
|
1831
1994
|
manifest_path = outline_root / "manifest.json"
|
|
@@ -1887,52 +2050,49 @@ class QuestService:
|
|
|
1887
2050
|
return payload if isinstance(payload, dict) else {}
|
|
1888
2051
|
|
|
1889
2052
|
def _paper_evidence_payload(self, quest_root: Path, workspace_root: Path) -> dict[str, Any] | None:
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
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
|
-
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
|
+
}
|
|
1936
2096
|
|
|
1937
2097
|
def _paper_contract_payload(self, quest_root: Path, workspace_root: Path) -> dict[str, Any] | None:
|
|
1938
2098
|
paper_root = self._best_paper_root(quest_root, workspace_root)
|
|
@@ -1952,6 +2112,7 @@ class QuestService:
|
|
|
1952
2112
|
bundle_manifest = bundle_manifest if isinstance(bundle_manifest, dict) else {}
|
|
1953
2113
|
experiment_matrix_path = paper_root / "paper_experiment_matrix.md"
|
|
1954
2114
|
experiment_matrix_json_path = paper_root / "paper_experiment_matrix.json"
|
|
2115
|
+
manuscript_coverage_path = paper_root / "manuscript_coverage.json"
|
|
1955
2116
|
claim_map_path = paper_root / "claim_evidence_map.json"
|
|
1956
2117
|
paper_line_state_path = paper_root / "paper_line_state.json"
|
|
1957
2118
|
evidence_ledger = self._paper_evidence_payload(quest_root, workspace_root)
|
|
@@ -2002,11 +2163,14 @@ class QuestService:
|
|
|
2002
2163
|
return {
|
|
2003
2164
|
"paper_root": str(paper_root),
|
|
2004
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,
|
|
2005
2167
|
"paper_branch": str(bundle_manifest.get("paper_branch") or "").strip() or current_branch(paper_root.parent),
|
|
2006
2168
|
"source_branch": str(bundle_manifest.get("source_branch") or "").strip() or None,
|
|
2007
2169
|
"selected_outline_ref": str(selected_outline.get("outline_id") or bundle_manifest.get("selected_outline_ref") or "").strip() or None,
|
|
2008
2170
|
"title": str(selected_outline.get("title") or bundle_manifest.get("title") or "").strip() or None,
|
|
2009
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,
|
|
2010
2174
|
"research_questions": detailed_outline.get("research_questions") if isinstance(detailed_outline.get("research_questions"), list) else [],
|
|
2011
2175
|
"experimental_designs": detailed_outline.get("experimental_designs") if isinstance(detailed_outline.get("experimental_designs"), list) else [],
|
|
2012
2176
|
"contributions": detailed_outline.get("contributions") if isinstance(detailed_outline.get("contributions"), list) else [],
|
|
@@ -2024,6 +2188,7 @@ class QuestService:
|
|
|
2024
2188
|
"outline_manifest": str(outline_manifest_path) if outline_manifest_path.exists() else None,
|
|
2025
2189
|
"experiment_matrix": str(experiment_matrix_path) if experiment_matrix_path.exists() else None,
|
|
2026
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,
|
|
2027
2192
|
"bundle_manifest": str(bundle_manifest_path) if bundle_manifest_path.exists() else None,
|
|
2028
2193
|
"claim_evidence_map": str(claim_map_path) if claim_map_path.exists() else None,
|
|
2029
2194
|
"paper_line_state": str(paper_line_state_path) if paper_line_state_path.exists() else None,
|
|
@@ -2417,11 +2582,25 @@ class QuestService:
|
|
|
2417
2582
|
evidence_items = [
|
|
2418
2583
|
dict(item) for item in ((paper_evidence or {}).get("items") or []) if isinstance(item, dict)
|
|
2419
2584
|
]
|
|
2420
|
-
ledger_by_item = {
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
if
|
|
2424
|
-
|
|
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
|
+
|
|
2425
2604
|
unresolved_required_items: list[dict[str, Any]] = []
|
|
2426
2605
|
ready_section_count = 0
|
|
2427
2606
|
for section in paper_contract.get("sections") or []:
|
|
@@ -2430,7 +2609,7 @@ class QuestService:
|
|
|
2430
2609
|
required_items = [str(item).strip() for item in (section.get("required_items") or []) if str(item).strip()]
|
|
2431
2610
|
section_ready = True
|
|
2432
2611
|
for item_id in required_items:
|
|
2433
|
-
ledger_item =
|
|
2612
|
+
ledger_item = ready_ledger_item(item_id)
|
|
2434
2613
|
status = str((ledger_item or {}).get("status") or "").strip().lower()
|
|
2435
2614
|
if status not in {"ready", "completed", "analyzed", "written", "recorded", "supported"}:
|
|
2436
2615
|
unresolved_required_items.append(
|
|
@@ -2517,6 +2696,18 @@ class QuestService:
|
|
|
2517
2696
|
if isinstance(paper_contract.get("bundle_manifest"), dict)
|
|
2518
2697
|
else {}
|
|
2519
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 {}
|
|
2520
2711
|
submission_checklist_path = str(((paper_contract.get("paths") or {}).get("submission_checklist") or "")).strip()
|
|
2521
2712
|
submission_checklist = read_json(Path(submission_checklist_path), {}) if submission_checklist_path else {}
|
|
2522
2713
|
submission_checklist = submission_checklist if isinstance(submission_checklist, dict) else {}
|
|
@@ -2530,9 +2721,23 @@ class QuestService:
|
|
|
2530
2721
|
closure_state = "bundle_not_ready"
|
|
2531
2722
|
delivery_state = "not_ready"
|
|
2532
2723
|
keep_bundle_fixed_by_default = False
|
|
2533
|
-
|
|
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:
|
|
2534
2733
|
closure_state = "delivery_ready"
|
|
2535
|
-
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"
|
|
2536
2741
|
if delivered_at or "delivered" in overall_status:
|
|
2537
2742
|
delivery_state = "delivered"
|
|
2538
2743
|
closure_state = "delivered_continue_research" if "continue" in overall_status else "delivered_parked"
|
|
@@ -2541,15 +2746,30 @@ class QuestService:
|
|
|
2541
2746
|
if unmapped_completed_items:
|
|
2542
2747
|
recommended_next_stage = "write"
|
|
2543
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"
|
|
2544
2755
|
elif unresolved_required_items or blocking_pending_slices:
|
|
2545
2756
|
recommended_next_stage = "analysis-campaign"
|
|
2546
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"
|
|
2547
2761
|
elif draft_status != "present":
|
|
2548
2762
|
recommended_next_stage = "write"
|
|
2549
2763
|
recommended_action = "draft_paper"
|
|
2550
2764
|
elif bundle_status != "present":
|
|
2551
2765
|
recommended_next_stage = "write"
|
|
2552
|
-
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"
|
|
2553
2773
|
else:
|
|
2554
2774
|
recommended_next_stage = "finalize"
|
|
2555
2775
|
recommended_action = "finalize_paper_line"
|
|
@@ -2568,7 +2788,16 @@ class QuestService:
|
|
|
2568
2788
|
"selected_outline_ref": selected_outline_ref,
|
|
2569
2789
|
"contract_ok": contract_ok,
|
|
2570
2790
|
"writing_ready": writing_ready,
|
|
2571
|
-
"
|
|
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,
|
|
2572
2801
|
"closure_state": closure_state,
|
|
2573
2802
|
"delivery_state": delivery_state,
|
|
2574
2803
|
"delivered_at": delivered_at,
|
|
@@ -2597,6 +2826,27 @@ class QuestService:
|
|
|
2597
2826
|
"draft_status": draft_status,
|
|
2598
2827
|
"bundle_status": bundle_status,
|
|
2599
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,
|
|
2600
2850
|
"recommended_next_stage": recommended_next_stage,
|
|
2601
2851
|
"recommended_action": recommended_action,
|
|
2602
2852
|
"unresolved_required_items": unresolved_required_items[:12],
|
|
@@ -2629,6 +2879,40 @@ class QuestService:
|
|
|
2629
2879
|
return text
|
|
2630
2880
|
return text.zfill(_NUMERIC_QUEST_ID_PAD_WIDTH)
|
|
2631
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
|
+
|
|
2632
2916
|
@contextmanager
|
|
2633
2917
|
def _quest_id_state_lock(self):
|
|
2634
2918
|
lock_path = self._quest_id_lock_path()
|
|
@@ -2714,7 +2998,7 @@ class QuestService:
|
|
|
2714
2998
|
return self._format_numeric_quest_id(next_numeric_id)
|
|
2715
2999
|
|
|
2716
3000
|
def _reserve_numeric_quest_id(self, quest_id: str) -> None:
|
|
2717
|
-
numeric_value = self.
|
|
3001
|
+
numeric_value = self._parse_reserved_numeric_quest_id(quest_id)
|
|
2718
3002
|
if numeric_value is None:
|
|
2719
3003
|
return
|
|
2720
3004
|
with self._quest_id_state_lock():
|
|
@@ -2724,24 +3008,28 @@ class QuestService:
|
|
|
2724
3008
|
self._write_quest_id_state_locked(next_numeric_id)
|
|
2725
3009
|
|
|
2726
3010
|
def _normalize_quest_id(self, quest_id: str | None) -> tuple[str, bool]:
|
|
2727
|
-
raw = str(quest_id or "").strip()
|
|
3011
|
+
raw = str(quest_id or "").strip()
|
|
2728
3012
|
if not raw:
|
|
2729
3013
|
return self._allocate_next_numeric_quest_id(), True
|
|
2730
|
-
slug = re.sub(r"[^
|
|
3014
|
+
slug = re.sub(r"[^A-Za-z0-9._-]+", "-", raw).strip("._-")
|
|
2731
3015
|
if not slug:
|
|
2732
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')}"
|
|
2733
3020
|
return slug[:80], False
|
|
2734
3021
|
|
|
2735
3022
|
def create(
|
|
2736
3023
|
self,
|
|
2737
3024
|
goal: str,
|
|
2738
3025
|
quest_id: str | None = None,
|
|
2739
|
-
runner: str =
|
|
3026
|
+
runner: str | None = None,
|
|
2740
3027
|
title: str | None = None,
|
|
2741
3028
|
*,
|
|
2742
3029
|
requested_baseline_ref: dict[str, Any] | None = None,
|
|
2743
3030
|
startup_contract: dict[str, Any] | None = None,
|
|
2744
3031
|
) -> dict:
|
|
3032
|
+
resolved_runner = str(runner or self._configured_default_runner()).strip().lower() or "codex"
|
|
2745
3033
|
quest_id, auto_generated = self._normalize_quest_id(quest_id)
|
|
2746
3034
|
quest_root = self._quest_root(quest_id)
|
|
2747
3035
|
if quest_root.exists():
|
|
@@ -2757,7 +3045,7 @@ class QuestService:
|
|
|
2757
3045
|
quest_id,
|
|
2758
3046
|
goal,
|
|
2759
3047
|
quest_root,
|
|
2760
|
-
|
|
3048
|
+
resolved_runner,
|
|
2761
3049
|
title=title,
|
|
2762
3050
|
requested_baseline_ref=dict(requested_baseline_ref) if isinstance(requested_baseline_ref, dict) else None,
|
|
2763
3051
|
startup_contract=dict(startup_contract) if isinstance(startup_contract, dict) else None,
|
|
@@ -2782,6 +3070,87 @@ class QuestService:
|
|
|
2782
3070
|
self._initialize_runtime_files(quest_root)
|
|
2783
3071
|
return self.snapshot(quest_id)
|
|
2784
3072
|
|
|
3073
|
+
def repair_orphaned_quest_scaffold(
|
|
3074
|
+
self,
|
|
3075
|
+
quest_id: str,
|
|
3076
|
+
*,
|
|
3077
|
+
title: str | None = None,
|
|
3078
|
+
goal: str | None = None,
|
|
3079
|
+
runner: str | None = None,
|
|
3080
|
+
) -> dict[str, Any]:
|
|
3081
|
+
resolved_runner = str(runner or self._configured_default_runner()).strip().lower() or "codex"
|
|
3082
|
+
quest_root = self._quest_root(quest_id)
|
|
3083
|
+
if not quest_root.exists():
|
|
3084
|
+
raise FileNotFoundError(f"Unknown quest `{quest_id}`.")
|
|
3085
|
+
quest_yaml_path = self._quest_yaml_path(quest_root)
|
|
3086
|
+
if quest_yaml_path.exists():
|
|
3087
|
+
raise FileExistsError(f"Quest `{quest_id}` already has a scaffold.")
|
|
3088
|
+
|
|
3089
|
+
restored_goal = str(goal or f"Recovered quest {quest_id}").strip() or f"Recovered quest {quest_id}"
|
|
3090
|
+
restored_title = str(title or quest_id).strip() or quest_id
|
|
3091
|
+
|
|
3092
|
+
for relative in QUEST_DIRECTORIES:
|
|
3093
|
+
ensure_dir(quest_root / relative)
|
|
3094
|
+
|
|
3095
|
+
write_yaml(
|
|
3096
|
+
quest_yaml_path,
|
|
3097
|
+
initial_quest_yaml(
|
|
3098
|
+
quest_id,
|
|
3099
|
+
restored_goal,
|
|
3100
|
+
quest_root,
|
|
3101
|
+
resolved_runner,
|
|
3102
|
+
title=restored_title,
|
|
3103
|
+
),
|
|
3104
|
+
)
|
|
3105
|
+
write_text(
|
|
3106
|
+
quest_root / "brief.md",
|
|
3107
|
+
"\n".join(
|
|
3108
|
+
[
|
|
3109
|
+
"# Quest Brief",
|
|
3110
|
+
"",
|
|
3111
|
+
"## Recovery Note",
|
|
3112
|
+
"",
|
|
3113
|
+
"This quest scaffold was recreated because the core quest files were missing.",
|
|
3114
|
+
"Existing runtime traces under `.ds/` were preserved.",
|
|
3115
|
+
"",
|
|
3116
|
+
"## Goal",
|
|
3117
|
+
"",
|
|
3118
|
+
restored_goal,
|
|
3119
|
+
"",
|
|
3120
|
+
]
|
|
3121
|
+
),
|
|
3122
|
+
)
|
|
3123
|
+
write_text(
|
|
3124
|
+
quest_root / "plan.md",
|
|
3125
|
+
"\n".join(
|
|
3126
|
+
[
|
|
3127
|
+
"# Plan",
|
|
3128
|
+
"",
|
|
3129
|
+
"- [ ] Inspect preserved runtime traces under `.ds/`",
|
|
3130
|
+
"- [ ] Re-establish the baseline context",
|
|
3131
|
+
"- [ ] Recreate any missing durable files or artifacts",
|
|
3132
|
+
"",
|
|
3133
|
+
]
|
|
3134
|
+
),
|
|
3135
|
+
)
|
|
3136
|
+
write_text(
|
|
3137
|
+
quest_root / "status.md",
|
|
3138
|
+
"# Status\n\nRecovered scaffold. Review preserved runtime state before continuing.\n",
|
|
3139
|
+
)
|
|
3140
|
+
write_text(
|
|
3141
|
+
quest_root / "SUMMARY.md",
|
|
3142
|
+
"# Summary\n\nRecovered quest scaffold. Original top-level quest files were missing.\n",
|
|
3143
|
+
)
|
|
3144
|
+
write_text(quest_root / ".gitignore", gitignore())
|
|
3145
|
+
self._write_active_user_requirements(
|
|
3146
|
+
quest_root,
|
|
3147
|
+
latest_requirement=None,
|
|
3148
|
+
)
|
|
3149
|
+
if not (quest_root / ".git").exists():
|
|
3150
|
+
init_repo(quest_root)
|
|
3151
|
+
self._initialize_runtime_files(quest_root)
|
|
3152
|
+
return self.snapshot(quest_id)
|
|
3153
|
+
|
|
2785
3154
|
def list_quests(self) -> list[dict]:
|
|
2786
3155
|
items: list[dict] = []
|
|
2787
3156
|
if not self.quests_root.exists():
|
|
@@ -2879,7 +3248,7 @@ class QuestService:
|
|
|
2879
3248
|
)
|
|
2880
3249
|
|
|
2881
3250
|
def summary_compact(self, quest_id: str) -> dict[str, Any]:
|
|
2882
|
-
quest_root = self.
|
|
3251
|
+
quest_root = self._require_initialized_quest_root(quest_id)
|
|
2883
3252
|
cache_key = f"compact:{self._cache_key_for_path(quest_root)}"
|
|
2884
3253
|
state = self._compact_summary_state(quest_root)
|
|
2885
3254
|
with self._snapshot_cache_lock:
|
|
@@ -2935,9 +3304,16 @@ class QuestService:
|
|
|
2935
3304
|
|
|
2936
3305
|
bash_summary = BashExecService(self.home).summary(quest_root)
|
|
2937
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"}
|
|
2938
3313
|
payload = {
|
|
2939
3314
|
"quest_id": quest_yaml.get("quest_id", quest_id),
|
|
2940
3315
|
"title": quest_yaml.get("title", quest_id),
|
|
3316
|
+
"goal": quest_yaml.get("goal"),
|
|
2941
3317
|
"quest_root": str(quest_root.resolve()),
|
|
2942
3318
|
"status": runtime_state.get("display_status") or runtime_state.get("status") or quest_yaml.get("status", "idle"),
|
|
2943
3319
|
"runtime_status": runtime_state.get("status") or quest_yaml.get("status", "idle"),
|
|
@@ -2953,7 +3329,9 @@ class QuestService:
|
|
|
2953
3329
|
"research_head_worktree_root": research_state.get("research_head_worktree_root"),
|
|
2954
3330
|
"current_workspace_branch": research_state.get("current_workspace_branch"),
|
|
2955
3331
|
"current_workspace_root": research_state.get("current_workspace_root"),
|
|
2956
|
-
"workspace_mode":
|
|
3332
|
+
"workspace_mode": workspace_mode,
|
|
3333
|
+
"quest_class": quest_class,
|
|
3334
|
+
"listed_in_projects": listed_in_projects,
|
|
2957
3335
|
"active_idea_id": research_state.get("active_idea_id"),
|
|
2958
3336
|
"active_baseline_id": active_baseline_id,
|
|
2959
3337
|
"active_baseline_variant_id": active_baseline_variant_id,
|
|
@@ -2962,6 +3340,7 @@ class QuestService:
|
|
|
2962
3340
|
"continuation_anchor": runtime_state.get("continuation_anchor"),
|
|
2963
3341
|
"continuation_reason": runtime_state.get("continuation_reason"),
|
|
2964
3342
|
"continuation_updated_at": runtime_state.get("continuation_updated_at"),
|
|
3343
|
+
"waiting_notice": runtime_state.get("waiting_notice"),
|
|
2965
3344
|
"last_resume_source": runtime_state.get("last_resume_source"),
|
|
2966
3345
|
"last_resume_at": runtime_state.get("last_resume_at"),
|
|
2967
3346
|
"last_recovery_abandoned_run_id": runtime_state.get("last_recovery_abandoned_run_id"),
|
|
@@ -3056,18 +3435,18 @@ class QuestService:
|
|
|
3056
3435
|
self._jsonl_tail_cache.pop(cache_key, None)
|
|
3057
3436
|
return [], 0, False
|
|
3058
3437
|
if normalized_limit <= 0:
|
|
3059
|
-
total =
|
|
3438
|
+
total = _count_jsonl_lines_fast(path)
|
|
3060
3439
|
return [], total, False
|
|
3061
3440
|
|
|
3062
3441
|
if before is not None:
|
|
3063
|
-
stop_cursor = max(int(before) - 1, 0)
|
|
3064
3442
|
window: deque[tuple[int, dict[str, Any]]] = deque(maxlen=normalized_limit)
|
|
3065
3443
|
total = 0
|
|
3066
|
-
for payload in _iter_jsonl_records_safely(path):
|
|
3067
|
-
total
|
|
3068
|
-
if
|
|
3444
|
+
for cursor, payload in _iter_jsonl_records_safely(path):
|
|
3445
|
+
total = cursor
|
|
3446
|
+
if cursor >= before:
|
|
3069
3447
|
break
|
|
3070
|
-
|
|
3448
|
+
if isinstance(payload, dict):
|
|
3449
|
+
window.append((cursor, payload))
|
|
3071
3450
|
has_more = bool(window and window[0][0] > 1)
|
|
3072
3451
|
return list(window), total, has_more
|
|
3073
3452
|
|
|
@@ -3087,6 +3466,17 @@ class QuestService:
|
|
|
3087
3466
|
window = cached_records[-normalized_limit:]
|
|
3088
3467
|
has_more = cached_total > len(window)
|
|
3089
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
|
|
3090
3480
|
|
|
3091
3481
|
if (
|
|
3092
3482
|
cached_tail
|
|
@@ -3110,11 +3500,11 @@ class QuestService:
|
|
|
3110
3500
|
)
|
|
3111
3501
|
)
|
|
3112
3502
|
if appended_records:
|
|
3113
|
-
|
|
3114
|
-
for payload in appended_records:
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
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))
|
|
3118
3508
|
else:
|
|
3119
3509
|
total = cached_total
|
|
3120
3510
|
stored_records = list(window)
|
|
@@ -3125,6 +3515,13 @@ class QuestService:
|
|
|
3125
3515
|
"total": total,
|
|
3126
3516
|
"records": stored_records,
|
|
3127
3517
|
}
|
|
3518
|
+
write_json(
|
|
3519
|
+
_jsonl_line_count_cache_path(path),
|
|
3520
|
+
{
|
|
3521
|
+
"state": list(state),
|
|
3522
|
+
"total": total,
|
|
3523
|
+
},
|
|
3524
|
+
)
|
|
3128
3525
|
selected = stored_records[-normalized_limit:]
|
|
3129
3526
|
has_more = total > len(selected)
|
|
3130
3527
|
return selected, total, has_more
|
|
@@ -3144,12 +3541,14 @@ class QuestService:
|
|
|
3144
3541
|
total = 0
|
|
3145
3542
|
saw_more = False
|
|
3146
3543
|
normalized_after = max(int(after or 0), 0)
|
|
3147
|
-
for payload in _iter_jsonl_records_safely(path):
|
|
3148
|
-
total
|
|
3149
|
-
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):
|
|
3150
3549
|
continue
|
|
3151
3550
|
if len(collected) < normalized_limit:
|
|
3152
|
-
collected.append((
|
|
3551
|
+
collected.append((cursor, payload))
|
|
3153
3552
|
continue
|
|
3154
3553
|
saw_more = True
|
|
3155
3554
|
return collected, total, saw_more
|
|
@@ -3254,7 +3653,7 @@ class QuestService:
|
|
|
3254
3653
|
return self._snapshot(quest_id)
|
|
3255
3654
|
|
|
3256
3655
|
def _snapshot(self, quest_id: str) -> dict:
|
|
3257
|
-
quest_root = self.
|
|
3656
|
+
quest_root = self._require_initialized_quest_root(quest_id)
|
|
3258
3657
|
cache_key = f"snapshot:{self._cache_key_for_path(quest_root)}"
|
|
3259
3658
|
state = self._snapshot_state(quest_root)
|
|
3260
3659
|
with self._snapshot_cache_lock:
|
|
@@ -3428,6 +3827,7 @@ class QuestService:
|
|
|
3428
3827
|
payload = {
|
|
3429
3828
|
"quest_id": quest_yaml.get("quest_id", quest_id),
|
|
3430
3829
|
"title": quest_yaml.get("title", quest_id),
|
|
3830
|
+
"goal": quest_yaml.get("goal"),
|
|
3431
3831
|
"quest_root": str(quest_root.resolve()),
|
|
3432
3832
|
"status": runtime_state.get("display_status") or runtime_state.get("status") or quest_yaml.get("status", "idle"),
|
|
3433
3833
|
"runtime_status": runtime_state.get("status") or quest_yaml.get("status", "idle"),
|
|
@@ -3466,6 +3866,7 @@ class QuestService:
|
|
|
3466
3866
|
"continuation_anchor": runtime_state.get("continuation_anchor"),
|
|
3467
3867
|
"continuation_reason": runtime_state.get("continuation_reason"),
|
|
3468
3868
|
"continuation_updated_at": runtime_state.get("continuation_updated_at"),
|
|
3869
|
+
"waiting_notice": runtime_state.get("waiting_notice"),
|
|
3469
3870
|
"last_resume_source": runtime_state.get("last_resume_source"),
|
|
3470
3871
|
"last_resume_at": runtime_state.get("last_resume_at"),
|
|
3471
3872
|
"last_recovery_abandoned_run_id": runtime_state.get("last_recovery_abandoned_run_id"),
|
|
@@ -3764,9 +4165,12 @@ class QuestService:
|
|
|
3764
4165
|
def bind_source(self, quest_id: str, source: str) -> dict:
|
|
3765
4166
|
quest_root = self._quest_root(quest_id)
|
|
3766
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 []
|
|
3767
4170
|
bindings = self._binding_sources_payload(quest_root)
|
|
3768
4171
|
normalized_source = self._normalize_binding_source(source)
|
|
3769
|
-
|
|
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])
|
|
3770
4174
|
changed = list(bindings.get("sources") or []) != next_sources
|
|
3771
4175
|
if changed:
|
|
3772
4176
|
bindings["sources"] = next_sources
|
|
@@ -3824,6 +4228,8 @@ class QuestService:
|
|
|
3824
4228
|
title: str | None = None,
|
|
3825
4229
|
active_anchor: str | None = None,
|
|
3826
4230
|
default_runner: str | None = None,
|
|
4231
|
+
workspace_mode: str | None = None,
|
|
4232
|
+
decision_policy: str | None = None,
|
|
3827
4233
|
) -> dict:
|
|
3828
4234
|
quest_root = self._quest_root(quest_id)
|
|
3829
4235
|
quest_yaml_path = self._quest_yaml_path(quest_root)
|
|
@@ -3832,6 +4238,8 @@ class QuestService:
|
|
|
3832
4238
|
|
|
3833
4239
|
quest_data = self.read_quest_yaml(quest_root)
|
|
3834
4240
|
changed = False
|
|
4241
|
+
research_state_updates: dict[str, Any] = {}
|
|
4242
|
+
runtime_state_updates: dict[str, Any] = {}
|
|
3835
4243
|
|
|
3836
4244
|
if title is not None:
|
|
3837
4245
|
normalized_title = str(title).strip()
|
|
@@ -3865,13 +4273,73 @@ class QuestService:
|
|
|
3865
4273
|
if normalized_runner not in available_runners:
|
|
3866
4274
|
allowed = ", ".join(sorted(available_runners))
|
|
3867
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
|
|
3868
4283
|
if quest_data.get("default_runner") != normalized_runner:
|
|
3869
4284
|
quest_data["default_runner"] = normalized_runner
|
|
3870
4285
|
changed = True
|
|
3871
4286
|
|
|
4287
|
+
if workspace_mode is not None:
|
|
4288
|
+
normalized_workspace_mode = str(workspace_mode).strip().lower()
|
|
4289
|
+
if normalized_workspace_mode not in {"copilot", "autonomous"}:
|
|
4290
|
+
raise ValueError("Unsupported workspace mode. Allowed values: copilot, autonomous.")
|
|
4291
|
+
startup_contract = (
|
|
4292
|
+
dict(quest_data.get("startup_contract") or {})
|
|
4293
|
+
if isinstance(quest_data.get("startup_contract"), dict)
|
|
4294
|
+
else {}
|
|
4295
|
+
)
|
|
4296
|
+
if str(startup_contract.get("workspace_mode") or "").strip().lower() != normalized_workspace_mode:
|
|
4297
|
+
startup_contract["workspace_mode"] = normalized_workspace_mode
|
|
4298
|
+
quest_data["startup_contract"] = startup_contract
|
|
4299
|
+
changed = True
|
|
4300
|
+
if str(self.read_research_state(quest_root).get("workspace_mode") or "").strip().lower() != normalized_workspace_mode:
|
|
4301
|
+
research_state_updates["workspace_mode"] = normalized_workspace_mode
|
|
4302
|
+
runtime_state_updates["continuation_policy"] = (
|
|
4303
|
+
"wait_for_user_or_resume" if normalized_workspace_mode == "copilot" else "auto"
|
|
4304
|
+
)
|
|
4305
|
+
runtime_state_updates["continuation_reason"] = (
|
|
4306
|
+
"copilot_mode" if normalized_workspace_mode == "copilot" else "autonomous_mode"
|
|
4307
|
+
)
|
|
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
|
+
|
|
3872
4336
|
if changed:
|
|
3873
4337
|
quest_data["updated_at"] = utc_now()
|
|
3874
4338
|
write_yaml(quest_yaml_path, quest_data)
|
|
4339
|
+
if research_state_updates:
|
|
4340
|
+
self.update_research_state(quest_root, **research_state_updates)
|
|
4341
|
+
if runtime_state_updates:
|
|
4342
|
+
self.update_runtime_state(quest_root=quest_root, **runtime_state_updates)
|
|
3875
4343
|
|
|
3876
4344
|
return self.snapshot(quest_id)
|
|
3877
4345
|
|
|
@@ -3973,11 +4441,26 @@ class QuestService:
|
|
|
3973
4441
|
active_run_id=active_run_id or None,
|
|
3974
4442
|
last_transition_at=last_transition_at,
|
|
3975
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()
|
|
3976
4458
|
self.update_runtime_state(
|
|
3977
4459
|
quest_root=quest_root,
|
|
3978
4460
|
status="stopped",
|
|
3979
4461
|
active_run_id=None,
|
|
3980
4462
|
stop_reason="crash_recovered",
|
|
4463
|
+
**reconciled_policy_updates,
|
|
3981
4464
|
)
|
|
3982
4465
|
summary = (
|
|
3983
4466
|
f"Recovered quest from stale runtime state; previous status `{previous_status}`"
|
|
@@ -4117,8 +4600,15 @@ class QuestService:
|
|
|
4117
4600
|
|
|
4118
4601
|
def node_traces(self, quest_id: str, *, selection_type: str | None = None) -> dict:
|
|
4119
4602
|
quest_root = self._quest_root(quest_id)
|
|
4120
|
-
workflow = self.workflow(quest_id)
|
|
4121
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)
|
|
4122
4612
|
payload = QuestNodeTraceManager(quest_root).materialize(
|
|
4123
4613
|
quest_id=quest_id,
|
|
4124
4614
|
workflow=workflow,
|
|
@@ -4308,7 +4798,7 @@ class QuestService:
|
|
|
4308
4798
|
return payload
|
|
4309
4799
|
|
|
4310
4800
|
def list_documents(self, quest_id: str) -> list[dict]:
|
|
4311
|
-
quest_root = self.
|
|
4801
|
+
quest_root = self._require_initialized_quest_root(quest_id)
|
|
4312
4802
|
workspace_root = self.active_workspace_root(quest_root)
|
|
4313
4803
|
documents = []
|
|
4314
4804
|
for relative in ("brief.md", "plan.md", "status.md", "SUMMARY.md"):
|
|
@@ -4359,10 +4849,11 @@ class QuestService:
|
|
|
4359
4849
|
mode: str | None = None,
|
|
4360
4850
|
profile: str | None = None,
|
|
4361
4851
|
) -> dict:
|
|
4852
|
+
profile = str(profile or "").strip().lower() or None
|
|
4362
4853
|
if revision:
|
|
4363
4854
|
return self._revision_explorer(quest_id, revision=revision, mode=mode or "ref")
|
|
4364
4855
|
|
|
4365
|
-
quest_root = self.
|
|
4856
|
+
quest_root = self._require_initialized_quest_root(quest_id)
|
|
4366
4857
|
workspace_root = self.active_workspace_root(quest_root)
|
|
4367
4858
|
git_status = self._git_status_map(workspace_root)
|
|
4368
4859
|
|
|
@@ -4389,9 +4880,9 @@ class QuestService:
|
|
|
4389
4880
|
}
|
|
4390
4881
|
|
|
4391
4882
|
def search_files(self, quest_id: str, term: str, limit: int = 50) -> dict[str, Any]:
|
|
4392
|
-
query =
|
|
4883
|
+
query = self._normalize_explorer_search_query(term)
|
|
4393
4884
|
normalized_query = query.casefold()
|
|
4394
|
-
workspace_root = self.active_workspace_root(self.
|
|
4885
|
+
workspace_root = self.active_workspace_root(self._require_initialized_quest_root(quest_id))
|
|
4395
4886
|
resolved_limit = max(1, min(limit, 200))
|
|
4396
4887
|
if not normalized_query:
|
|
4397
4888
|
return {
|
|
@@ -4415,6 +4906,41 @@ class QuestService:
|
|
|
4415
4906
|
except OSError:
|
|
4416
4907
|
continue
|
|
4417
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
|
+
|
|
4418
4944
|
renderer_hint, mime_type = self._renderer_hint_for(path)
|
|
4419
4945
|
if not self._is_text_document(path, mime_type, renderer_hint):
|
|
4420
4946
|
continue
|
|
@@ -4437,8 +4963,6 @@ class QuestService:
|
|
|
4437
4963
|
continue
|
|
4438
4964
|
|
|
4439
4965
|
files_scanned += 1
|
|
4440
|
-
relative = path.relative_to(workspace_root).as_posix()
|
|
4441
|
-
scope, writable = self._classify_path_scope(workspace_root, path)
|
|
4442
4966
|
|
|
4443
4967
|
for line_index, line in enumerate(content.splitlines(), start=1):
|
|
4444
4968
|
haystack = line.casefold()
|
|
@@ -4485,8 +5009,17 @@ class QuestService:
|
|
|
4485
5009
|
"files_scanned": files_scanned,
|
|
4486
5010
|
}
|
|
4487
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
|
+
|
|
4488
5021
|
def open_document(self, quest_id: str, document_id: str) -> dict:
|
|
4489
|
-
quest_root = self.
|
|
5022
|
+
quest_root = self._require_initialized_quest_root(quest_id)
|
|
4490
5023
|
workspace_root = self.active_workspace_root(quest_root)
|
|
4491
5024
|
if document_id.startswith("git::"):
|
|
4492
5025
|
revision, relative = self._parse_git_document_id(document_id)
|
|
@@ -4522,12 +5055,16 @@ class QuestService:
|
|
|
4522
5055
|
},
|
|
4523
5056
|
}
|
|
4524
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]
|
|
4525
5062
|
path, writable, scope, source_kind = self.resolve_document(quest_id, document_id)
|
|
4526
5063
|
renderer_hint, mime_type = self._renderer_hint_for(path)
|
|
4527
5064
|
is_text = self._is_text_document(path, mime_type, renderer_hint)
|
|
4528
5065
|
content = read_text(path) if is_text else ""
|
|
4529
5066
|
revision = f"sha256:{sha256_text(content)}" if is_text else f"sha256:{hashlib.sha256(path.read_bytes()).hexdigest()}"
|
|
4530
|
-
|
|
5067
|
+
payload = {
|
|
4531
5068
|
"document_id": document_id,
|
|
4532
5069
|
"quest_id": quest_id,
|
|
4533
5070
|
"title": path.name if "::" in document_id else document_id,
|
|
@@ -4549,9 +5086,17 @@ class QuestService:
|
|
|
4549
5086
|
"renderer_hint": renderer_hint,
|
|
4550
5087
|
},
|
|
4551
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
|
|
4552
5095
|
|
|
4553
5096
|
def resolve_document(self, quest_id: str, document_id: str) -> tuple[Path, bool, str, str]:
|
|
4554
|
-
quest_root = self.
|
|
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)
|
|
4555
5100
|
workspace_root = self.active_workspace_root(quest_root)
|
|
4556
5101
|
resolution_root = self._document_resolution_root(
|
|
4557
5102
|
quest_root=quest_root,
|
|
@@ -4568,6 +5113,32 @@ class QuestService:
|
|
|
4568
5113
|
return self._resolve_document(quest_root, f"questpath::{legacy_relative}")
|
|
4569
5114
|
raise
|
|
4570
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
|
+
|
|
4571
5142
|
def save_document(self, quest_id: str, document_id: str, content: str, previous_revision: str | None = None) -> dict:
|
|
4572
5143
|
current = self.open_document(quest_id, document_id)
|
|
4573
5144
|
if not current.get("writable", False):
|
|
@@ -4744,20 +5315,545 @@ class QuestService:
|
|
|
4744
5315
|
"saved_at": utc_now(),
|
|
4745
5316
|
}
|
|
4746
5317
|
|
|
4747
|
-
def
|
|
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]:
|
|
4748
5327
|
quest_root = self._quest_root(quest_id)
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
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
|
+
)
|
|
4757
5367
|
return {
|
|
5368
|
+
"ok": True,
|
|
4758
5369
|
"quest_id": quest_id,
|
|
4759
|
-
"
|
|
4760
|
-
|
|
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
|
+
|
|
5558
|
+
@staticmethod
|
|
5559
|
+
def _normalize_workspace_relative_path(
|
|
5560
|
+
relative: str | None,
|
|
5561
|
+
*,
|
|
5562
|
+
field_name: str,
|
|
5563
|
+
allow_root: bool = True,
|
|
5564
|
+
) -> str | None:
|
|
5565
|
+
if relative is None:
|
|
5566
|
+
if allow_root:
|
|
5567
|
+
return None
|
|
5568
|
+
raise ValueError(f"`{field_name}` is required.")
|
|
5569
|
+
raw = str(relative).strip().replace("\\", "/")
|
|
5570
|
+
if not raw:
|
|
5571
|
+
if allow_root:
|
|
5572
|
+
return None
|
|
5573
|
+
raise ValueError(f"`{field_name}` is required.")
|
|
5574
|
+
normalized = raw.lstrip("/").rstrip("/")
|
|
5575
|
+
if normalized in {"", "."}:
|
|
5576
|
+
if allow_root:
|
|
5577
|
+
return None
|
|
5578
|
+
raise ValueError(f"`{field_name}` must point to a workspace entry.")
|
|
5579
|
+
return normalized
|
|
5580
|
+
|
|
5581
|
+
@staticmethod
|
|
5582
|
+
def _normalize_workspace_entry_name(name: str | None, *, field_name: str) -> str:
|
|
5583
|
+
raw = str(name or "").strip().replace("\\", "/")
|
|
5584
|
+
if not raw:
|
|
5585
|
+
raise ValueError(f"`{field_name}` is required.")
|
|
5586
|
+
if "/" in raw:
|
|
5587
|
+
raise ValueError(f"`{field_name}` must be a single path segment.")
|
|
5588
|
+
candidate = Path(raw).name
|
|
5589
|
+
if candidate != raw or candidate in {"", ".", ".."}:
|
|
5590
|
+
raise ValueError(f"`{field_name}` must be a valid file or folder name.")
|
|
5591
|
+
if candidate == ".git":
|
|
5592
|
+
raise ValueError("`.git` cannot be created or renamed from the explorer.")
|
|
5593
|
+
return candidate
|
|
5594
|
+
|
|
5595
|
+
@staticmethod
|
|
5596
|
+
def _normalize_workspace_path_list(paths: Any, *, field_name: str) -> list[str]:
|
|
5597
|
+
if not isinstance(paths, list) or not paths:
|
|
5598
|
+
raise ValueError(f"`{field_name}` must be a non-empty list.")
|
|
5599
|
+
normalized: list[str] = []
|
|
5600
|
+
seen: set[str] = set()
|
|
5601
|
+
for raw in paths:
|
|
5602
|
+
item = QuestService._normalize_workspace_relative_path(
|
|
5603
|
+
raw,
|
|
5604
|
+
field_name=field_name,
|
|
5605
|
+
allow_root=False,
|
|
5606
|
+
)
|
|
5607
|
+
if not item or item in seen:
|
|
5608
|
+
continue
|
|
5609
|
+
seen.add(item)
|
|
5610
|
+
normalized.append(item)
|
|
5611
|
+
if not normalized:
|
|
5612
|
+
raise ValueError(f"`{field_name}` must include at least one valid path.")
|
|
5613
|
+
return normalized
|
|
5614
|
+
|
|
5615
|
+
@staticmethod
|
|
5616
|
+
def _filter_nested_workspace_paths(paths: list[str]) -> list[str]:
|
|
5617
|
+
kept: list[str] = []
|
|
5618
|
+
for path in paths:
|
|
5619
|
+
if any(path == parent or path.startswith(f"{parent}/") for parent in kept):
|
|
5620
|
+
continue
|
|
5621
|
+
kept.append(path)
|
|
5622
|
+
return kept
|
|
5623
|
+
|
|
5624
|
+
def _workspace_entry_payload(self, workspace_root: Path, path: Path) -> dict:
|
|
5625
|
+
if path.is_dir():
|
|
5626
|
+
return self._directory_node(
|
|
5627
|
+
workspace_root,
|
|
5628
|
+
path=path,
|
|
5629
|
+
children=[],
|
|
5630
|
+
git_status={},
|
|
5631
|
+
changed_paths={},
|
|
5632
|
+
)
|
|
5633
|
+
payload = self._file_node(
|
|
5634
|
+
workspace_root,
|
|
5635
|
+
path=path,
|
|
5636
|
+
git_status={},
|
|
5637
|
+
changed_paths={},
|
|
5638
|
+
)
|
|
5639
|
+
if payload is None:
|
|
5640
|
+
raise FileNotFoundError(f"Unknown workspace entry `{path}`.")
|
|
5641
|
+
return payload
|
|
5642
|
+
|
|
5643
|
+
def create_workspace_folder(
|
|
5644
|
+
self,
|
|
5645
|
+
quest_id: str,
|
|
5646
|
+
*,
|
|
5647
|
+
name: str | None,
|
|
5648
|
+
parent_path: str | None = None,
|
|
5649
|
+
) -> dict:
|
|
5650
|
+
workspace_root = self.active_workspace_root(self._require_initialized_quest_root(quest_id))
|
|
5651
|
+
normalized_parent = self._normalize_workspace_relative_path(
|
|
5652
|
+
parent_path,
|
|
5653
|
+
field_name="parent_path",
|
|
5654
|
+
allow_root=True,
|
|
5655
|
+
)
|
|
5656
|
+
folder_name = self._normalize_workspace_entry_name(name, field_name="name")
|
|
5657
|
+
parent = resolve_within(workspace_root, normalized_parent) if normalized_parent else workspace_root
|
|
5658
|
+
if not parent.exists() or not parent.is_dir():
|
|
5659
|
+
raise FileNotFoundError(
|
|
5660
|
+
f"Unknown destination folder `{normalized_parent or '.'}`."
|
|
5661
|
+
)
|
|
5662
|
+
target = resolve_within(parent, folder_name)
|
|
5663
|
+
if target.exists():
|
|
5664
|
+
raise FileExistsError(
|
|
5665
|
+
f"`{target.relative_to(workspace_root).as_posix()}` already exists."
|
|
5666
|
+
)
|
|
5667
|
+
ensure_dir(target)
|
|
5668
|
+
return {
|
|
5669
|
+
"ok": True,
|
|
5670
|
+
"quest_id": quest_id,
|
|
5671
|
+
"parent_path": normalized_parent,
|
|
5672
|
+
"item": self._workspace_entry_payload(workspace_root, target),
|
|
5673
|
+
"saved_at": utc_now(),
|
|
5674
|
+
}
|
|
5675
|
+
|
|
5676
|
+
def upload_workspace_file(
|
|
5677
|
+
self,
|
|
5678
|
+
quest_id: str,
|
|
5679
|
+
*,
|
|
5680
|
+
file_name: str | None,
|
|
5681
|
+
content: bytes,
|
|
5682
|
+
mime_type: str | None = None,
|
|
5683
|
+
parent_path: str | None = None,
|
|
5684
|
+
) -> dict:
|
|
5685
|
+
workspace_root = self.active_workspace_root(self._require_initialized_quest_root(quest_id))
|
|
5686
|
+
normalized_parent = self._normalize_workspace_relative_path(
|
|
5687
|
+
parent_path,
|
|
5688
|
+
field_name="parent_path",
|
|
5689
|
+
allow_root=True,
|
|
5690
|
+
)
|
|
5691
|
+
safe_name = self._normalize_workspace_entry_name(file_name, field_name="file_name")
|
|
5692
|
+
parent = resolve_within(workspace_root, normalized_parent) if normalized_parent else workspace_root
|
|
5693
|
+
if not parent.exists() or not parent.is_dir():
|
|
5694
|
+
raise FileNotFoundError(
|
|
5695
|
+
f"Unknown destination folder `{normalized_parent or '.'}`."
|
|
5696
|
+
)
|
|
5697
|
+
target = resolve_within(parent, safe_name)
|
|
5698
|
+
if target.exists():
|
|
5699
|
+
raise FileExistsError(
|
|
5700
|
+
f"`{target.relative_to(workspace_root).as_posix()}` already exists."
|
|
5701
|
+
)
|
|
5702
|
+
ensure_dir(target.parent)
|
|
5703
|
+
target.write_bytes(content)
|
|
5704
|
+
payload = self._workspace_entry_payload(workspace_root, target)
|
|
5705
|
+
guessed_mime = mimetypes.guess_type(target.name)[0] or mime_type or "application/octet-stream"
|
|
5706
|
+
payload["mime_type"] = guessed_mime
|
|
5707
|
+
return {
|
|
5708
|
+
"ok": True,
|
|
5709
|
+
"quest_id": quest_id,
|
|
5710
|
+
"parent_path": normalized_parent,
|
|
5711
|
+
"item": payload,
|
|
5712
|
+
"saved_at": utc_now(),
|
|
5713
|
+
}
|
|
5714
|
+
|
|
5715
|
+
def rename_workspace_entry(
|
|
5716
|
+
self,
|
|
5717
|
+
quest_id: str,
|
|
5718
|
+
*,
|
|
5719
|
+
path: str | None,
|
|
5720
|
+
new_name: str | None,
|
|
5721
|
+
) -> dict:
|
|
5722
|
+
workspace_root = self.active_workspace_root(self._require_initialized_quest_root(quest_id))
|
|
5723
|
+
normalized_path = self._normalize_workspace_relative_path(
|
|
5724
|
+
path,
|
|
5725
|
+
field_name="path",
|
|
5726
|
+
allow_root=False,
|
|
5727
|
+
)
|
|
5728
|
+
source = resolve_within(workspace_root, normalized_path)
|
|
5729
|
+
if not source.exists():
|
|
5730
|
+
raise FileNotFoundError(f"Unknown workspace entry `{normalized_path}`.")
|
|
5731
|
+
safe_name = self._normalize_workspace_entry_name(new_name, field_name="new_name")
|
|
5732
|
+
target = resolve_within(source.parent, safe_name)
|
|
5733
|
+
if target.exists() and target != source:
|
|
5734
|
+
raise FileExistsError(
|
|
5735
|
+
f"`{target.relative_to(workspace_root).as_posix()}` already exists."
|
|
5736
|
+
)
|
|
5737
|
+
if target != source:
|
|
5738
|
+
source.rename(target)
|
|
5739
|
+
payload = self._workspace_entry_payload(workspace_root, target)
|
|
5740
|
+
return {
|
|
5741
|
+
"ok": True,
|
|
5742
|
+
"quest_id": quest_id,
|
|
5743
|
+
"previous_path": normalized_path,
|
|
5744
|
+
"item": payload,
|
|
5745
|
+
"saved_at": utc_now(),
|
|
5746
|
+
}
|
|
5747
|
+
|
|
5748
|
+
def move_workspace_entries(
|
|
5749
|
+
self,
|
|
5750
|
+
quest_id: str,
|
|
5751
|
+
*,
|
|
5752
|
+
paths: Any,
|
|
5753
|
+
target_parent_path: str | None = None,
|
|
5754
|
+
) -> dict:
|
|
5755
|
+
workspace_root = self.active_workspace_root(self._require_initialized_quest_root(quest_id))
|
|
5756
|
+
normalized_paths = self._filter_nested_workspace_paths(
|
|
5757
|
+
self._normalize_workspace_path_list(paths, field_name="paths")
|
|
5758
|
+
)
|
|
5759
|
+
normalized_target_parent = self._normalize_workspace_relative_path(
|
|
5760
|
+
target_parent_path,
|
|
5761
|
+
field_name="target_parent_path",
|
|
5762
|
+
allow_root=True,
|
|
5763
|
+
)
|
|
5764
|
+
target_parent = (
|
|
5765
|
+
resolve_within(workspace_root, normalized_target_parent)
|
|
5766
|
+
if normalized_target_parent
|
|
5767
|
+
else workspace_root
|
|
5768
|
+
)
|
|
5769
|
+
if not target_parent.exists() or not target_parent.is_dir():
|
|
5770
|
+
raise FileNotFoundError(
|
|
5771
|
+
f"Unknown destination folder `{normalized_target_parent or '.'}`."
|
|
5772
|
+
)
|
|
5773
|
+
|
|
5774
|
+
moves: list[tuple[str, Path, Path]] = []
|
|
5775
|
+
destination_keys: set[str] = set()
|
|
5776
|
+
target_parent_resolved = target_parent.resolve()
|
|
5777
|
+
for normalized_path in normalized_paths:
|
|
5778
|
+
source = resolve_within(workspace_root, normalized_path)
|
|
5779
|
+
if not source.exists():
|
|
5780
|
+
raise FileNotFoundError(f"Unknown workspace entry `{normalized_path}`.")
|
|
5781
|
+
source_resolved = source.resolve()
|
|
5782
|
+
if source_resolved == target_parent_resolved or source_resolved in target_parent_resolved.parents:
|
|
5783
|
+
raise ValueError(
|
|
5784
|
+
f"`{normalized_path}` cannot be moved into itself or one of its descendants."
|
|
5785
|
+
)
|
|
5786
|
+
destination = resolve_within(target_parent, source.name)
|
|
5787
|
+
if destination.exists() and destination.resolve() != source_resolved:
|
|
5788
|
+
raise FileExistsError(
|
|
5789
|
+
f"`{destination.relative_to(workspace_root).as_posix()}` already exists."
|
|
5790
|
+
)
|
|
5791
|
+
destination_key = str(destination.resolve())
|
|
5792
|
+
if destination_key in destination_keys and destination != source:
|
|
5793
|
+
raise FileExistsError(
|
|
5794
|
+
f"`{destination.relative_to(workspace_root).as_posix()}` would conflict with another moved entry."
|
|
5795
|
+
)
|
|
5796
|
+
destination_keys.add(destination_key)
|
|
5797
|
+
moves.append((normalized_path, source, destination))
|
|
5798
|
+
|
|
5799
|
+
items: list[dict] = []
|
|
5800
|
+
for _normalized_path, source, destination in moves:
|
|
5801
|
+
if destination != source:
|
|
5802
|
+
source.rename(destination)
|
|
5803
|
+
items.append(self._workspace_entry_payload(workspace_root, destination))
|
|
5804
|
+
return {
|
|
5805
|
+
"ok": True,
|
|
5806
|
+
"quest_id": quest_id,
|
|
5807
|
+
"target_parent_path": normalized_target_parent,
|
|
5808
|
+
"items": items,
|
|
5809
|
+
"saved_at": utc_now(),
|
|
5810
|
+
}
|
|
5811
|
+
|
|
5812
|
+
def delete_workspace_entries(
|
|
5813
|
+
self,
|
|
5814
|
+
quest_id: str,
|
|
5815
|
+
*,
|
|
5816
|
+
paths: Any,
|
|
5817
|
+
) -> dict:
|
|
5818
|
+
workspace_root = self.active_workspace_root(self._require_initialized_quest_root(quest_id))
|
|
5819
|
+
normalized_paths = self._filter_nested_workspace_paths(
|
|
5820
|
+
self._normalize_workspace_path_list(paths, field_name="paths")
|
|
5821
|
+
)
|
|
5822
|
+
sources: list[Path] = []
|
|
5823
|
+
items: list[dict] = []
|
|
5824
|
+
for normalized_path in normalized_paths:
|
|
5825
|
+
source = resolve_within(workspace_root, normalized_path)
|
|
5826
|
+
if not source.exists():
|
|
5827
|
+
raise FileNotFoundError(f"Unknown workspace entry `{normalized_path}`.")
|
|
5828
|
+
sources.append(source)
|
|
5829
|
+
items.append(self._workspace_entry_payload(workspace_root, source))
|
|
5830
|
+
|
|
5831
|
+
for source in sorted(sources, key=lambda item: len(item.parts), reverse=True):
|
|
5832
|
+
if source.is_dir():
|
|
5833
|
+
shutil.rmtree(source)
|
|
5834
|
+
else:
|
|
5835
|
+
source.unlink()
|
|
5836
|
+
return {
|
|
5837
|
+
"ok": True,
|
|
5838
|
+
"quest_id": quest_id,
|
|
5839
|
+
"items": items,
|
|
5840
|
+
"saved_at": utc_now(),
|
|
5841
|
+
}
|
|
5842
|
+
|
|
5843
|
+
def _revision_explorer(self, quest_id: str, *, revision: str, mode: str) -> dict:
|
|
5844
|
+
quest_root = self._quest_root(quest_id)
|
|
5845
|
+
if not self._git_revision_exists(quest_root, revision):
|
|
5846
|
+
raise FileNotFoundError(f"Unknown git revision `{revision}`.")
|
|
5847
|
+
|
|
5848
|
+
snapshot_paths = self._git_snapshot_paths(quest_root, revision)
|
|
5849
|
+
snapshot_tree = self._build_snapshot_tree(snapshot_paths)
|
|
5850
|
+
root_nodes = self._snapshot_children(snapshot_tree, revision=revision, prefix="")
|
|
5851
|
+
sections = self._group_explorer_sections(root_nodes)
|
|
5852
|
+
|
|
5853
|
+
return {
|
|
5854
|
+
"quest_id": quest_id,
|
|
5855
|
+
"quest_root": str(quest_root.resolve()),
|
|
5856
|
+
"view": {
|
|
4761
5857
|
"mode": mode,
|
|
4762
5858
|
"revision": revision,
|
|
4763
5859
|
"label": revision,
|
|
@@ -4874,9 +5970,10 @@ class QuestService:
|
|
|
4874
5970
|
@staticmethod
|
|
4875
5971
|
def _default_message_queue() -> dict[str, Any]:
|
|
4876
5972
|
return {
|
|
4877
|
-
"version":
|
|
5973
|
+
"version": 2,
|
|
4878
5974
|
"pending": [],
|
|
4879
5975
|
"completed": [],
|
|
5976
|
+
"message_states": {},
|
|
4880
5977
|
}
|
|
4881
5978
|
|
|
4882
5979
|
def _default_runtime_state(
|
|
@@ -4907,6 +6004,7 @@ class QuestService:
|
|
|
4907
6004
|
"continuation_anchor": None,
|
|
4908
6005
|
"continuation_reason": None,
|
|
4909
6006
|
"continuation_updated_at": None,
|
|
6007
|
+
"waiting_notice": None,
|
|
4910
6008
|
"last_resume_source": None,
|
|
4911
6009
|
"last_resume_at": None,
|
|
4912
6010
|
"last_recovery_abandoned_run_id": None,
|
|
@@ -4918,6 +6016,7 @@ class QuestService:
|
|
|
4918
6016
|
"last_delivered_batch_id": None,
|
|
4919
6017
|
"last_delivered_at": None,
|
|
4920
6018
|
"retry_state": None,
|
|
6019
|
+
"turn_message_override": None,
|
|
4921
6020
|
}
|
|
4922
6021
|
|
|
4923
6022
|
def _default_agent_status(self, quest_root: Path) -> dict[str, Any]:
|
|
@@ -4936,6 +6035,8 @@ class QuestService:
|
|
|
4936
6035
|
}
|
|
4937
6036
|
|
|
4938
6037
|
def _initialize_runtime_files(self, quest_root: Path) -> None:
|
|
6038
|
+
if not self._quest_yaml_path(quest_root).exists():
|
|
6039
|
+
raise FileNotFoundError(f"Unknown quest `{quest_root.name}`.")
|
|
4939
6040
|
queue_path = self._message_queue_path(quest_root)
|
|
4940
6041
|
if not queue_path.exists():
|
|
4941
6042
|
write_json(queue_path, self._default_message_queue())
|
|
@@ -4956,9 +6057,11 @@ class QuestService:
|
|
|
4956
6057
|
payload = self._read_cached_json(self._message_queue_path(quest_root), self._default_message_queue())
|
|
4957
6058
|
if not isinstance(payload, dict):
|
|
4958
6059
|
payload = self._default_message_queue()
|
|
4959
|
-
payload.setdefault("version",
|
|
6060
|
+
payload.setdefault("version", 2)
|
|
4960
6061
|
payload.setdefault("pending", [])
|
|
4961
6062
|
payload.setdefault("completed", [])
|
|
6063
|
+
message_states = payload.get("message_states")
|
|
6064
|
+
payload["message_states"] = dict(message_states) if isinstance(message_states, dict) else {}
|
|
4962
6065
|
return payload
|
|
4963
6066
|
|
|
4964
6067
|
def _write_message_queue(self, quest_root: Path, payload: dict[str, Any]) -> None:
|
|
@@ -4986,6 +6089,7 @@ class QuestService:
|
|
|
4986
6089
|
merged["continuation_anchor"] = str(merged.get("continuation_anchor") or "").strip() or None
|
|
4987
6090
|
merged["continuation_reason"] = str(merged.get("continuation_reason") or "").strip() or None
|
|
4988
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
|
|
4989
6093
|
merged["last_resume_source"] = str(merged.get("last_resume_source") or "").strip() or None
|
|
4990
6094
|
merged["last_resume_at"] = str(merged.get("last_resume_at") or "").strip() or None
|
|
4991
6095
|
merged["last_recovery_abandoned_run_id"] = str(merged.get("last_recovery_abandoned_run_id") or "").strip() or None
|
|
@@ -4994,6 +6098,11 @@ class QuestService:
|
|
|
4994
6098
|
merged["last_stage_fingerprint_at"] = str(merged.get("last_stage_fingerprint_at") or "").strip() or None
|
|
4995
6099
|
merged["same_fingerprint_auto_turn_count"] = int(merged.get("same_fingerprint_auto_turn_count") or 0)
|
|
4996
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
|
+
)
|
|
4997
6106
|
return merged
|
|
4998
6107
|
|
|
4999
6108
|
def _write_runtime_state(self, quest_root: Path, payload: dict[str, Any]) -> None:
|
|
@@ -5016,6 +6125,7 @@ class QuestService:
|
|
|
5016
6125
|
continuation_anchor: str | None | object = _UNSET,
|
|
5017
6126
|
continuation_reason: str | None | object = _UNSET,
|
|
5018
6127
|
continuation_updated_at: str | None | object = _UNSET,
|
|
6128
|
+
waiting_notice: dict[str, Any] | None | object = _UNSET,
|
|
5019
6129
|
last_resume_source: str | None | object = _UNSET,
|
|
5020
6130
|
last_resume_at: str | None | object = _UNSET,
|
|
5021
6131
|
last_recovery_abandoned_run_id: str | None | object = _UNSET,
|
|
@@ -5028,6 +6138,7 @@ class QuestService:
|
|
|
5028
6138
|
last_delivered_at: str | None | object = _UNSET,
|
|
5029
6139
|
display_status: str | None | object = _UNSET,
|
|
5030
6140
|
retry_state: dict[str, Any] | None | object = _UNSET,
|
|
6141
|
+
turn_message_override: dict[str, Any] | None | object = _UNSET,
|
|
5031
6142
|
) -> dict[str, Any]:
|
|
5032
6143
|
with self._runtime_state_lock(quest_root):
|
|
5033
6144
|
state = self._read_runtime_state(quest_root)
|
|
@@ -5084,6 +6195,8 @@ class QuestService:
|
|
|
5084
6195
|
state["continuation_updated_at"] = str(continuation_updated_at or "").strip() or None
|
|
5085
6196
|
elif continuation_changed:
|
|
5086
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
|
|
5087
6200
|
if last_resume_source is not _UNSET:
|
|
5088
6201
|
state["last_resume_source"] = str(last_resume_source or "").strip() or None
|
|
5089
6202
|
if last_resume_at is not _UNSET:
|
|
@@ -5106,6 +6219,12 @@ class QuestService:
|
|
|
5106
6219
|
state["last_delivered_at"] = last_delivered_at
|
|
5107
6220
|
if retry_state is not _UNSET:
|
|
5108
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
|
+
)
|
|
5109
6228
|
if last_transition_at is not _UNSET:
|
|
5110
6229
|
state["last_transition_at"] = last_transition_at
|
|
5111
6230
|
elif status_changed or run_changed:
|
|
@@ -5147,11 +6266,369 @@ class QuestService:
|
|
|
5147
6266
|
continuation_reason=reason,
|
|
5148
6267
|
)
|
|
5149
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
|
+
|
|
5150
6626
|
def _enqueue_user_message(self, quest_root: Path, record: dict[str, Any]) -> dict[str, Any]:
|
|
5151
6627
|
queue_payload = self._read_message_queue(quest_root)
|
|
5152
6628
|
source = str(record.get("source") or "local")
|
|
5153
6629
|
queue_record = {
|
|
5154
6630
|
"message_id": record.get("id"),
|
|
6631
|
+
"client_message_id": str(record.get("client_message_id") or "").strip() or None,
|
|
5155
6632
|
"source": source,
|
|
5156
6633
|
"conversation_id": self._normalize_binding_source(source),
|
|
5157
6634
|
"content": record.get("content") or "",
|
|
@@ -5161,6 +6638,17 @@ class QuestService:
|
|
|
5161
6638
|
"status": "queued",
|
|
5162
6639
|
}
|
|
5163
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
|
+
)
|
|
5164
6652
|
self._write_message_queue(quest_root, queue_payload)
|
|
5165
6653
|
self.update_runtime_state(
|
|
5166
6654
|
quest_root=quest_root,
|
|
@@ -5190,7 +6678,27 @@ class QuestService:
|
|
|
5190
6678
|
for item in read_jsonl(quest_root / ".ds" / "conversations" / "main.jsonl")
|
|
5191
6679
|
if str(item.get("role") or "") == "user"
|
|
5192
6680
|
]
|
|
5193
|
-
|
|
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
|
|
5194
6702
|
lines = [
|
|
5195
6703
|
"# Active User Requirements",
|
|
5196
6704
|
"",
|
|
@@ -5209,12 +6717,17 @@ class QuestService:
|
|
|
5209
6717
|
"",
|
|
5210
6718
|
]
|
|
5211
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
|
+
)
|
|
5212
6725
|
lines.extend(
|
|
5213
6726
|
[
|
|
5214
6727
|
f"- source: {latest.get('source') or 'local'}",
|
|
5215
6728
|
f"- created_at: {latest.get('created_at') or utc_now()}",
|
|
5216
6729
|
"",
|
|
5217
|
-
|
|
6730
|
+
latest_rendered or "No latest requirement text was captured.",
|
|
5218
6731
|
"",
|
|
5219
6732
|
]
|
|
5220
6733
|
)
|
|
@@ -5231,11 +6744,15 @@ class QuestService:
|
|
|
5231
6744
|
"",
|
|
5232
6745
|
]
|
|
5233
6746
|
)
|
|
5234
|
-
if
|
|
5235
|
-
for index, item in enumerate(
|
|
6747
|
+
if filtered_user_messages:
|
|
6748
|
+
for index, item in enumerate(filtered_user_messages[-12:], start=1):
|
|
5236
6749
|
source = str(item.get("source") or "local").strip() or "local"
|
|
5237
6750
|
created_at = str(item.get("created_at") or "").strip() or "unknown"
|
|
5238
|
-
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)"
|
|
5239
6756
|
lines.append(f"{index}. [{source}] [{created_at}] {content}")
|
|
5240
6757
|
else:
|
|
5241
6758
|
lines.append("1. No user messages yet.")
|
|
@@ -5277,11 +6794,31 @@ class QuestService:
|
|
|
5277
6794
|
}
|
|
5278
6795
|
queue_payload["pending"] = pending
|
|
5279
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
|
+
)
|
|
5280
6809
|
self._write_message_queue(quest_root, queue_payload)
|
|
5281
6810
|
self.update_runtime_state(
|
|
5282
6811
|
quest_root=quest_root,
|
|
5283
6812
|
pending_user_message_count=len(pending),
|
|
5284
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
|
+
)
|
|
5285
6822
|
append_jsonl(
|
|
5286
6823
|
self._interaction_journal_path(quest_root),
|
|
5287
6824
|
{
|
|
@@ -5331,6 +6868,26 @@ class QuestService:
|
|
|
5331
6868
|
]
|
|
5332
6869
|
queue_payload["pending"] = []
|
|
5333
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
|
+
)
|
|
5334
6891
|
self._write_message_queue(quest_root, queue_payload)
|
|
5335
6892
|
append_jsonl(
|
|
5336
6893
|
self._interaction_journal_path(quest_root),
|
|
@@ -5490,11 +7047,13 @@ class QuestService:
|
|
|
5490
7047
|
*,
|
|
5491
7048
|
interaction_id: str | None,
|
|
5492
7049
|
limit: int = 10,
|
|
7050
|
+
delivery_reason: str = "artifact_mailbox",
|
|
5493
7051
|
) -> dict[str, Any]:
|
|
5494
7052
|
queue_payload = self._read_message_queue(quest_root)
|
|
5495
7053
|
pending = [dict(item) for item in (queue_payload.get("pending") or [])]
|
|
5496
7054
|
recent_records = self.latest_artifact_interaction_records(quest_root, limit=max(limit, 10))
|
|
5497
7055
|
delivered_messages: list[dict[str, Any]] = []
|
|
7056
|
+
delivered_state_records: list[dict[str, Any]] = []
|
|
5498
7057
|
delivery_batch = None
|
|
5499
7058
|
now = utc_now()
|
|
5500
7059
|
|
|
@@ -5511,6 +7070,29 @@ class QuestService:
|
|
|
5511
7070
|
delivered_messages.append(delivered)
|
|
5512
7071
|
queue_payload["pending"] = []
|
|
5513
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))
|
|
5514
7096
|
self._write_message_queue(quest_root, queue_payload)
|
|
5515
7097
|
append_jsonl(
|
|
5516
7098
|
self._interaction_journal_path(quest_root),
|
|
@@ -5533,6 +7115,8 @@ class QuestService:
|
|
|
5533
7115
|
delivery_batch = {
|
|
5534
7116
|
"batch_id": batch_id,
|
|
5535
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,
|
|
5536
7120
|
}
|
|
5537
7121
|
else:
|
|
5538
7122
|
self.update_runtime_state(
|
|
@@ -5540,15 +7124,45 @@ class QuestService:
|
|
|
5540
7124
|
pending_user_message_count=0,
|
|
5541
7125
|
)
|
|
5542
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
|
+
}
|
|
5543
7137
|
recent_inbound_messages = [
|
|
5544
7138
|
{
|
|
5545
7139
|
"message_id": item.get("message_id"),
|
|
7140
|
+
"client_message_id": item.get("client_message_id"),
|
|
5546
7141
|
"source": str(item.get("conversation_id") or item.get("source") or "local").split(":", 1)[0],
|
|
5547
7142
|
"conversation_id": item.get("conversation_id") or self._normalize_binding_source(str(item.get("source") or "local")),
|
|
5548
7143
|
"sender": "user",
|
|
5549
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,
|
|
5550
7160
|
"text": item.get("content") or "",
|
|
5551
|
-
"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
|
+
),
|
|
5552
7166
|
"attachments": [dict(attachment) for attachment in (item.get("attachments") or []) if isinstance(attachment, dict)],
|
|
5553
7167
|
"reply_to_interaction_id": item.get("reply_to_interaction_id"),
|
|
5554
7168
|
}
|
|
@@ -5596,7 +7210,12 @@ class QuestService:
|
|
|
5596
7210
|
]
|
|
5597
7211
|
for index, item in enumerate(delivered_messages, start=1):
|
|
5598
7212
|
source = str(item.get("conversation_id") or item.get("source") or "local")
|
|
5599
|
-
|
|
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}")
|
|
5600
7219
|
agent_instruction = "\n".join(lines).strip()
|
|
5601
7220
|
else:
|
|
5602
7221
|
lines = [
|
|
@@ -5637,12 +7256,60 @@ class QuestService:
|
|
|
5637
7256
|
return {
|
|
5638
7257
|
"delivery_batch": delivery_batch,
|
|
5639
7258
|
"recent_inbound_messages": recent_inbound_messages,
|
|
7259
|
+
"message_states": delivered_state_records,
|
|
5640
7260
|
"recent_interaction_records": recent_records[-10:],
|
|
5641
7261
|
"agent_instruction": agent_instruction,
|
|
5642
7262
|
"queued_message_count_before_delivery": len(pending),
|
|
5643
7263
|
"queued_message_count_after_delivery": len(queue_payload.get("pending") or []),
|
|
5644
7264
|
}
|
|
5645
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
|
+
|
|
5646
7313
|
@staticmethod
|
|
5647
7314
|
def _document_resolution_root(quest_root: Path, workspace_root: Path, document_id: str) -> Path:
|
|
5648
7315
|
if document_id.startswith(("questpath::", "memory::")):
|
|
@@ -5835,12 +7502,10 @@ class QuestService:
|
|
|
5835
7502
|
|
|
5836
7503
|
@staticmethod
|
|
5837
7504
|
def _read_git_bytes(quest_root: Path, revision: str, relative: str) -> bytes:
|
|
5838
|
-
result =
|
|
7505
|
+
result = run_command_bytes(
|
|
5839
7506
|
["git", "show", f"{revision}:{relative}"],
|
|
5840
|
-
cwd=
|
|
7507
|
+
cwd=quest_root,
|
|
5841
7508
|
check=False,
|
|
5842
|
-
text=False,
|
|
5843
|
-
capture_output=True,
|
|
5844
7509
|
)
|
|
5845
7510
|
if result.returncode != 0:
|
|
5846
7511
|
raise FileNotFoundError(f"File `{relative}` does not exist at `{revision}`.")
|
|
@@ -6006,19 +7671,36 @@ class QuestService:
|
|
|
6006
7671
|
return True
|
|
6007
7672
|
if relative.startswith(".ds/worktrees/"):
|
|
6008
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
|
|
6009
7688
|
parts = PurePosixPath(relative).parts
|
|
6010
7689
|
return "__pycache__" in parts or ".pytest_cache" in parts
|
|
6011
7690
|
|
|
6012
7691
|
@staticmethod
|
|
6013
7692
|
def _skip_explorer_profile_relative(relative: str, profile: str | None) -> bool:
|
|
6014
|
-
|
|
7693
|
+
profile = str(profile or "").strip().lower()
|
|
7694
|
+
if profile not in {"mobile", "workspace"}:
|
|
6015
7695
|
return False
|
|
6016
7696
|
normalized = relative.strip("/")
|
|
6017
7697
|
if not normalized:
|
|
6018
7698
|
return False
|
|
6019
7699
|
parts = PurePosixPath(normalized).parts
|
|
6020
7700
|
top = parts[0] if parts else normalized
|
|
6021
|
-
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":
|
|
6022
7704
|
return True
|
|
6023
7705
|
if top.startswith(".") and normalized not in {".gitignore"}:
|
|
6024
7706
|
return True
|
|
@@ -6026,7 +7708,8 @@ class QuestService:
|
|
|
6026
7708
|
|
|
6027
7709
|
@staticmethod
|
|
6028
7710
|
def _truncate_explorer_directory(relative: str, *, profile: str | None, depth: int) -> bool:
|
|
6029
|
-
|
|
7711
|
+
profile = str(profile or "").strip().lower()
|
|
7712
|
+
if profile not in {"mobile", "workspace"}:
|
|
6030
7713
|
return False
|
|
6031
7714
|
normalized = relative.strip("/")
|
|
6032
7715
|
if not normalized:
|
|
@@ -6035,11 +7718,19 @@ class QuestService:
|
|
|
6035
7718
|
top = parts[0] if parts else normalized
|
|
6036
7719
|
if top == "memory":
|
|
6037
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
|
|
6038
7727
|
if top == "baselines":
|
|
7728
|
+
return depth >= 2
|
|
7729
|
+
if top == "artifacts":
|
|
6039
7730
|
return depth >= 1
|
|
6040
7731
|
if top in {"literature", "paper", "experiments", "handoffs"}:
|
|
6041
|
-
return depth >=
|
|
6042
|
-
return depth >=
|
|
7732
|
+
return depth >= 3
|
|
7733
|
+
return depth >= 2
|
|
6043
7734
|
|
|
6044
7735
|
@staticmethod
|
|
6045
7736
|
def _classify_path_scope(quest_root: Path, path: Path) -> tuple[str, bool]:
|