codebee 0.1.95 → 0.1.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/CHANGELOG.md +733 -720
  2. package/LICENSE +21 -21
  3. package/README.md +516 -509
  4. package/app/core/acceptance.py +100 -100
  5. package/app/core/agent_context.py +118 -0
  6. package/app/core/aiflavor.py +197 -197
  7. package/app/core/analytics.py +38 -38
  8. package/app/core/artifacts.py +148 -148
  9. package/app/core/assets.py +110 -110
  10. package/app/core/attachments.py +623 -623
  11. package/app/core/audit.py +30 -30
  12. package/app/core/backup.py +470 -470
  13. package/app/core/beekeeper.py +183 -183
  14. package/app/core/benchstore.py +102 -95
  15. package/app/core/board.py +246 -209
  16. package/app/core/bookmeta.py +1036 -1036
  17. package/app/core/bookmeta_catalog.py +180 -180
  18. package/app/core/branching.py +137 -137
  19. package/app/core/builtin_agent.py +2055 -1680
  20. package/app/core/capability.py +144 -144
  21. package/app/core/chaptersafe.py +32 -32
  22. package/app/core/checkpoints.py +648 -0
  23. package/app/core/cleanup.py +303 -303
  24. package/app/core/compaction.py +201 -201
  25. package/app/core/contracts.py +255 -255
  26. package/app/core/covergen.py +356 -356
  27. package/app/core/defectretro.py +43 -43
  28. package/app/core/diagnostics.py +114 -114
  29. package/app/core/dispatch.py +198 -191
  30. package/app/core/dispatch_log.py +128 -128
  31. package/app/core/env_scrub.py +85 -85
  32. package/app/core/errorlog.py +137 -137
  33. package/app/core/eval_matrix.py +82 -0
  34. package/app/core/evalbench.py +184 -91
  35. package/app/core/evaluation.py +174 -174
  36. package/app/core/failure_notify.py +290 -290
  37. package/app/core/flow_graph.py +33 -0
  38. package/app/core/goal_service.py +167 -167
  39. package/app/core/health.py +389 -389
  40. package/app/core/history.py +38 -38
  41. package/app/core/hooks.py +162 -162
  42. package/app/core/jobs.py +25 -4
  43. package/app/core/knowledge_pipeline.py +86 -0
  44. package/app/core/manager.py +2232 -2232
  45. package/app/core/mcp_client.py +200 -29
  46. package/app/core/mocks.py +64 -64
  47. package/app/core/notify.py +201 -201
  48. package/app/core/novel_quality.py +181 -189
  49. package/app/core/openapi.py +177 -154
  50. package/app/core/operations.py +228 -228
  51. package/app/core/paihang.py +145 -145
  52. package/app/core/pipeline.py +319 -59
  53. package/app/core/planner.py +17 -11
  54. package/app/core/policy.py +84 -0
  55. package/app/core/portguard.py +111 -111
  56. package/app/core/presence.py +77 -77
  57. package/app/core/preview.py +293 -293
  58. package/app/core/project_memory.py +139 -0
  59. package/app/core/prompt_guard.py +78 -78
  60. package/app/core/publish/__init__.py +7 -7
  61. package/app/core/publish/auto.py +106 -22
  62. package/app/core/publish/browser.py +694 -655
  63. package/app/core/publish/chaprows.py +138 -0
  64. package/app/core/publish/fanqie.py +212 -165
  65. package/app/core/publish/flow.py +832 -640
  66. package/app/core/publish/flows-fanqie-calibrated.json +361 -336
  67. package/app/core/publish/ledger.py +51 -2
  68. package/app/core/publish/manager.py +1150 -1118
  69. package/app/core/publish/qimao.py +176 -176
  70. package/app/core/publish/volumes.py +108 -0
  71. package/app/core/publish/ws.py +139 -139
  72. package/app/core/quality_gate.py +194 -194
  73. package/app/core/redact.py +38 -38
  74. package/app/core/repeat_guard.py +129 -129
  75. package/app/core/retrieval.py +137 -137
  76. package/app/core/router.py +398 -398
  77. package/app/core/runner.py +2239 -2238
  78. package/app/core/selfupdate.py +384 -384
  79. package/app/core/session_log.py +161 -161
  80. package/app/core/sessions.py +312 -312
  81. package/app/core/settings.py +33 -5
  82. package/app/core/settings_schema.py +282 -271
  83. package/app/core/share_page.py +114 -114
  84. package/app/core/skill_scan.py +139 -139
  85. package/app/core/skillpacks/fanqie-novel.md +8 -6
  86. package/app/core/skillpacks/market/character-bible.md +66 -66
  87. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  88. package/app/core/skillpacks/market/git-workflow.md +57 -57
  89. package/app/core/skillpacks/market/release-notes.md +72 -72
  90. package/app/core/skillpacks/market/weekly-report.md +71 -71
  91. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  92. package/app/core/skillpacks/qimao-signing.md +14 -11
  93. package/app/core/skills.py +32 -21
  94. package/app/core/step_runner.py +132 -132
  95. package/app/core/store.py +24 -0
  96. package/app/core/story_tracking.py +275 -275
  97. package/app/core/task_compile.py +199 -199
  98. package/app/core/telemetry.py +292 -292
  99. package/app/core/tlsctx.py +64 -64
  100. package/app/core/token_meter.py +147 -147
  101. package/app/core/tool_outputs.py +107 -0
  102. package/app/core/tracing.py +207 -144
  103. package/app/core/upstream.py +41 -41
  104. package/app/core/usage.py +1038 -1038
  105. package/app/core/volumes.py +389 -389
  106. package/app/core/webhooks.py +89 -0
  107. package/app/core/wxdigest.py +865 -858
  108. package/app/core/zentao.py +2692 -2692
  109. package/app/main.py +179 -10
  110. package/app/mcp_server.py +244 -244
  111. package/app/pick_dialog.py +110 -110
  112. package/app/ui/app.js +563 -21
  113. package/app/ui/artdiff.js +74 -74
  114. package/app/ui/assets/community-qr.png +0 -0
  115. package/app/ui/assets/donate-alipay.jpg +0 -0
  116. package/app/ui/assets/donate-qr.jpg +0 -0
  117. package/app/ui/assets/workbench-reference.png +0 -0
  118. package/app/ui/board.css +451 -666
  119. package/app/ui/board.html +165 -162
  120. package/app/ui/board.js +509 -305
  121. package/app/ui/hive3d.js +242 -0
  122. package/app/ui/i18n.js +125 -9
  123. package/app/ui/icons/bee.svg +79 -79
  124. package/app/ui/index.html +60 -3
  125. package/app/ui/manifest.json +16 -16
  126. package/app/ui/qrcode.js +2297 -2297
  127. package/app/ui/style.css +189 -9
  128. package/bin/tutti.js +147 -147
  129. package/package.json +49 -48
  130. package/tools/wx_reader.py +261 -261
  131. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  132. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  133. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  134. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  135. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  136. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  137. package/app/.playwright-cli/run-detail-after.png +0 -0
  138. package/app/output/_createbook_body.json +0 -1
  139. package/app/output/_probe_fq_click.py +0 -50
  140. package/app/output/_probe_fq_form.py +0 -42
  141. package/app/output/_probe_fq_modal.py +0 -55
  142. package/app/output/_probe_fq_points.py +0 -71
  143. package/app/output/_probe_fq_submit.py +0 -34
  144. package/app/output/_probe_fq_watch.py +0 -62
@@ -1,133 +1,133 @@
1
- # -*- coding: utf-8 -*-
2
- """Step 执行器:压缩触发的重试守门(复用已渲染 prompt,不重复组装)。
3
-
4
- 设计稿:docs/migration/02-context-compaction.md §1D。
5
- 参考 dsh packages/core/agent-loop/src/agent.ts:307-378:
6
- 失败重试只在「前进 surface generation」时返回 {kind:'retry'},
7
- 复用已渲染 PromptAssembly,不重跑 pre-step / 组装 / 用户消息准入。
8
-
9
- Tutti 语境:runner.run_agent 自带 prompt(已渲染);重试的风险不是重复渲染
10
- (prompt 是同一个字符串),而是「不压缩直接重试」必然再次撑爆。
11
- 本模块的守门:仅当 maybe_compact 真正前进了 surface generation 才重试一次。
12
- """
13
- from __future__ import annotations
14
-
15
- import logging
16
-
17
- from .compaction import maybe_compact, DEFAULT_PRESSURE_THRESHOLD
18
- from .error_codes import ErrorCode
19
-
20
- log = logging.getLogger(__name__)
21
-
22
- # 触发「压缩后重试」的错误码
23
- _OVERFLOW_CODES = {ErrorCode.CONTEXT_OVERFLOW, ErrorCode.MAX_TOKENS}
24
-
25
- # 事前预检阈值:最近一次调用的上下文锚点已占本步模型容量九成 → 第一个请求
26
- # 大概率被供应商拒。只拦「大概率必死」,不提前压缩平时的高压(used() 是累计
27
- # 口径,比真实上下文偏大;0.8 的响应式阈值语义与此不同,勿混用)。
28
- _PRECHECK_RATIO = 0.9
29
-
30
-
31
- def _v2_compaction_tuning():
32
- """settings_v2 orchestrator.compaction 微调 → (threshold|None, retain|None)。
33
-
34
- None = 未配置(走 maybe_compact 的模块默认);读取/注册失败静默回落。
35
- retain 允许 0(尾部不保留),与 None(未配置)语义不同,勿合并。"""
36
- try:
37
- from .settings_schema import get as ss_get, register_default_namespaces
38
- register_default_namespaces()
39
- c = (ss_get("orchestrator") or {}).get("compaction") or {}
40
- th = c.get("pressure_threshold")
41
- rt = c.get("retain_tail_tokens")
42
- return (float(th) if th else None,
43
- int(rt) if rt is not None else None)
44
- except Exception:
45
- return None, None
46
-
47
-
48
- def execute_step(session, run_agent_fn, prompt, *, model: str = "",
49
- llm_caller=None, retain_tail_tokens=None, **kwargs):
50
- """执行一次 step;撑爆时压缩并守门重试一次。
51
-
52
- 事前预检(借鉴 freebuff base-chat 每步容量重估):换将切到小窗口模型后,
53
- 响应式路径要等供应商报 CONTEXT_OVERFLOW 才压缩——报得不干净就直接卡死。
54
- 先用「最近一次调用上下文 vs 本步模型容量」判一次,超阈先压缩再发。
55
-
56
- Args:
57
- session: session_log.Session(记录 replace_generation)
58
- run_agent_fn: runner.run_agent(或测试替身),签名 (prompt, **kwargs) -> dict
59
- prompt: 已渲染的提示词(重试时原样复用,不重新渲染)
60
- model: 本步将要使用的模型名(容量与压力估算都按它算)
61
- llm_caller: 压缩摘要 LLM(None = 不压缩,永不重试)
62
- **kwargs: 透传 run_agent_fn
63
-
64
- Returns:
65
- (result, retried: bool)
66
- """
67
- v2_th, v2_rt = _v2_compaction_tuning()
68
- compact_kwargs = {}
69
- if retain_tail_tokens is not None:
70
- compact_kwargs["retain_tail_tokens"] = retain_tail_tokens
71
- elif v2_rt is not None:
72
- compact_kwargs["retain_tail_tokens"] = v2_rt
73
- if v2_th is not None:
74
- compact_kwargs["threshold"] = v2_th
75
- if llm_caller is not None and model:
76
- try:
77
- from .token_meter import token_meter
78
- ctx = token_meter.last_context(session.run_id)
79
- cap = token_meter.capacity(model)
80
- if ctx > 0 and cap > 0 and ctx > cap * _PRECHECK_RATIO:
81
- gen_p0 = session.replace_generation()
82
- if maybe_compact(session, model=model, llm_caller=llm_caller,
83
- run_id=session.run_id, **compact_kwargs) \
84
- and session.replace_generation() > gen_p0:
85
- log.info("precheck: last ctx~%d > %.0f%% of cap(%s)=%d, "
86
- "compacted before step", ctx, _PRECHECK_RATIO * 100,
87
- model, cap)
88
- except Exception:
89
- log.debug("precheck skipped", exc_info=True)
90
- gen0 = session.replace_generation()
91
- result = run_agent_fn(prompt, **kwargs)
92
- code = result.get("error_code") or ""
93
- if code not in _OVERFLOW_CODES or llm_caller is None:
94
- return result, False
95
- # 撑爆 → 尝试压缩
96
- compacted = maybe_compact(session, model=model, llm_caller=llm_caller,
97
- run_id=session.run_id, **compact_kwargs)
98
- gen1 = session.replace_generation()
99
- if not compacted or gen1 <= gen0:
100
- # 压缩未发生或未前进 generation:重试必然再次撑爆,不重试
101
- log.info("overflow but no compaction progress (gen %s->%s), no retry",
102
- gen0, gen1)
103
- return result, False
104
- # 压缩生效:surface 已折叠。**真正替换 CLI 请求上下文**(2026-09-21
105
- # 清最老待深挖项:此前重试仍发原 prompt + 续旧 resume 会话——摘要只进
106
- # 审计日志,CLI 看到的上下文一点没小,重试必然再爆):
107
- # ①重试 prompt 改用 session 派生转录(折叠摘要 + 近尾消息,体量骤降);
108
- # ②弃 resume(旧会话本体仍是膨胀态,续上就前功尽弃)。
109
- msgs = []
110
- try:
111
- msgs = session.derive_messages()
112
- except Exception:
113
- log.debug("derive_messages failed on retry", exc_info=True)
114
- kwargs2 = dict(kwargs)
115
- if msgs:
116
- transcript = "\n\n".join("[%s] %s" % (m.get("role"), m.get("content"))
117
- for m in msgs)
118
- if len(transcript) > 24_000:
119
- transcript = transcript[-24_000:]
120
- retry_prompt = transcript
121
- # 本步指令必须在场:被折叠/截断掉就显式补在尾部(缺失指令比重复严重)
122
- tail = transcript[-2000:]
123
- if prompt[:400] not in tail:
124
- retry_prompt = transcript + "\n\n" + prompt
125
- kwargs2.pop("resume", None)
126
- log.info("compaction advanced gen %s->%s, retrying with derived "
127
- "context (%d msgs, resume dropped)", gen0, gen1, len(msgs))
128
- result2 = run_agent_fn(retry_prompt, **kwargs2)
129
- else:
130
- log.info("compaction advanced gen %s->%s but no surface derived, "
131
- "retrying same prompt", gen0, gen1)
132
- result2 = run_agent_fn(prompt, **kwargs)
1
+ # -*- coding: utf-8 -*-
2
+ """Step 执行器:压缩触发的重试守门(复用已渲染 prompt,不重复组装)。
3
+
4
+ 设计稿:docs/migration/02-context-compaction.md §1D。
5
+ 参考 dsh packages/core/agent-loop/src/agent.ts:307-378:
6
+ 失败重试只在「前进 surface generation」时返回 {kind:'retry'},
7
+ 复用已渲染 PromptAssembly,不重跑 pre-step / 组装 / 用户消息准入。
8
+
9
+ Tutti 语境:runner.run_agent 自带 prompt(已渲染);重试的风险不是重复渲染
10
+ (prompt 是同一个字符串),而是「不压缩直接重试」必然再次撑爆。
11
+ 本模块的守门:仅当 maybe_compact 真正前进了 surface generation 才重试一次。
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+
17
+ from .compaction import maybe_compact, DEFAULT_PRESSURE_THRESHOLD
18
+ from .error_codes import ErrorCode
19
+
20
+ log = logging.getLogger(__name__)
21
+
22
+ # 触发「压缩后重试」的错误码
23
+ _OVERFLOW_CODES = {ErrorCode.CONTEXT_OVERFLOW, ErrorCode.MAX_TOKENS}
24
+
25
+ # 事前预检阈值:最近一次调用的上下文锚点已占本步模型容量九成 → 第一个请求
26
+ # 大概率被供应商拒。只拦「大概率必死」,不提前压缩平时的高压(used() 是累计
27
+ # 口径,比真实上下文偏大;0.8 的响应式阈值语义与此不同,勿混用)。
28
+ _PRECHECK_RATIO = 0.9
29
+
30
+
31
+ def _v2_compaction_tuning():
32
+ """settings_v2 orchestrator.compaction 微调 → (threshold|None, retain|None)。
33
+
34
+ None = 未配置(走 maybe_compact 的模块默认);读取/注册失败静默回落。
35
+ retain 允许 0(尾部不保留),与 None(未配置)语义不同,勿合并。"""
36
+ try:
37
+ from .settings_schema import get as ss_get, register_default_namespaces
38
+ register_default_namespaces()
39
+ c = (ss_get("orchestrator") or {}).get("compaction") or {}
40
+ th = c.get("pressure_threshold")
41
+ rt = c.get("retain_tail_tokens")
42
+ return (float(th) if th else None,
43
+ int(rt) if rt is not None else None)
44
+ except Exception:
45
+ return None, None
46
+
47
+
48
+ def execute_step(session, run_agent_fn, prompt, *, model: str = "",
49
+ llm_caller=None, retain_tail_tokens=None, **kwargs):
50
+ """执行一次 step;撑爆时压缩并守门重试一次。
51
+
52
+ 事前预检(借鉴 freebuff base-chat 每步容量重估):换将切到小窗口模型后,
53
+ 响应式路径要等供应商报 CONTEXT_OVERFLOW 才压缩——报得不干净就直接卡死。
54
+ 先用「最近一次调用上下文 vs 本步模型容量」判一次,超阈先压缩再发。
55
+
56
+ Args:
57
+ session: session_log.Session(记录 replace_generation)
58
+ run_agent_fn: runner.run_agent(或测试替身),签名 (prompt, **kwargs) -> dict
59
+ prompt: 已渲染的提示词(重试时原样复用,不重新渲染)
60
+ model: 本步将要使用的模型名(容量与压力估算都按它算)
61
+ llm_caller: 压缩摘要 LLM(None = 不压缩,永不重试)
62
+ **kwargs: 透传 run_agent_fn
63
+
64
+ Returns:
65
+ (result, retried: bool)
66
+ """
67
+ v2_th, v2_rt = _v2_compaction_tuning()
68
+ compact_kwargs = {}
69
+ if retain_tail_tokens is not None:
70
+ compact_kwargs["retain_tail_tokens"] = retain_tail_tokens
71
+ elif v2_rt is not None:
72
+ compact_kwargs["retain_tail_tokens"] = v2_rt
73
+ if v2_th is not None:
74
+ compact_kwargs["threshold"] = v2_th
75
+ if llm_caller is not None and model:
76
+ try:
77
+ from .token_meter import token_meter
78
+ ctx = token_meter.last_context(session.run_id)
79
+ cap = token_meter.capacity(model)
80
+ if ctx > 0 and cap > 0 and ctx > cap * _PRECHECK_RATIO:
81
+ gen_p0 = session.replace_generation()
82
+ if maybe_compact(session, model=model, llm_caller=llm_caller,
83
+ run_id=session.run_id, **compact_kwargs) \
84
+ and session.replace_generation() > gen_p0:
85
+ log.info("precheck: last ctx~%d > %.0f%% of cap(%s)=%d, "
86
+ "compacted before step", ctx, _PRECHECK_RATIO * 100,
87
+ model, cap)
88
+ except Exception:
89
+ log.debug("precheck skipped", exc_info=True)
90
+ gen0 = session.replace_generation()
91
+ result = run_agent_fn(prompt, **kwargs)
92
+ code = result.get("error_code") or ""
93
+ if code not in _OVERFLOW_CODES or llm_caller is None:
94
+ return result, False
95
+ # 撑爆 → 尝试压缩
96
+ compacted = maybe_compact(session, model=model, llm_caller=llm_caller,
97
+ run_id=session.run_id, **compact_kwargs)
98
+ gen1 = session.replace_generation()
99
+ if not compacted or gen1 <= gen0:
100
+ # 压缩未发生或未前进 generation:重试必然再次撑爆,不重试
101
+ log.info("overflow but no compaction progress (gen %s->%s), no retry",
102
+ gen0, gen1)
103
+ return result, False
104
+ # 压缩生效:surface 已折叠。**真正替换 CLI 请求上下文**(2026-09-21
105
+ # 清最老待深挖项:此前重试仍发原 prompt + 续旧 resume 会话——摘要只进
106
+ # 审计日志,CLI 看到的上下文一点没小,重试必然再爆):
107
+ # ①重试 prompt 改用 session 派生转录(折叠摘要 + 近尾消息,体量骤降);
108
+ # ②弃 resume(旧会话本体仍是膨胀态,续上就前功尽弃)。
109
+ msgs = []
110
+ try:
111
+ msgs = session.derive_messages()
112
+ except Exception:
113
+ log.debug("derive_messages failed on retry", exc_info=True)
114
+ kwargs2 = dict(kwargs)
115
+ if msgs:
116
+ transcript = "\n\n".join("[%s] %s" % (m.get("role"), m.get("content"))
117
+ for m in msgs)
118
+ if len(transcript) > 24_000:
119
+ transcript = transcript[-24_000:]
120
+ retry_prompt = transcript
121
+ # 本步指令必须在场:被折叠/截断掉就显式补在尾部(缺失指令比重复严重)
122
+ tail = transcript[-2000:]
123
+ if prompt[:400] not in tail:
124
+ retry_prompt = transcript + "\n\n" + prompt
125
+ kwargs2.pop("resume", None)
126
+ log.info("compaction advanced gen %s->%s, retrying with derived "
127
+ "context (%d msgs, resume dropped)", gen0, gen1, len(msgs))
128
+ result2 = run_agent_fn(retry_prompt, **kwargs2)
129
+ else:
130
+ log.info("compaction advanced gen %s->%s but no surface derived, "
131
+ "retrying same prompt", gen0, gen1)
132
+ result2 = run_agent_fn(prompt, **kwargs)
133
133
  return result2, True
package/app/core/store.py CHANGED
@@ -180,6 +180,13 @@ def create_task(payload):
180
180
  "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
181
181
  "status": "created",
182
182
  }
183
+ # Every new task must persist one verified execution policy. If policy
184
+ # normalization fails, do not silently replace it with a broader default.
185
+ try:
186
+ from . import policy
187
+ task["sandbox"] = policy.normalize_sandbox(payload.get("sandbox"), wd)
188
+ except Exception as exc:
189
+ raise ValueError("沙箱策略无法验证,任务未创建") from exc
183
190
  thinking = str(payload.get("thinking") or "standard").strip().lower()
184
191
  task["thinking"] = thinking if thinking in ("auto", "low", "standard", "high") else "auto"
185
192
  # 流程修订出处(借鉴 WorkDSH ADR-0010):创建时钉住当时流程定义的指纹。
@@ -1738,6 +1745,23 @@ def retry_task(task_id):
1738
1745
  "chapter_scores": scores,
1739
1746
  }
1740
1747
  break
1748
+ else:
1749
+ # 断点续跑(代码等非连载任务,A+B):上一遍失败/取消/超时 run 的
1750
+ # 实现会话(inherit_session,跨 run 续 CLI 会话)与断点简报
1751
+ # (handoff,走到哪/改了什么/还剩什么)继承给新 run——重试从断点
1752
+ # 继续而不是从零烧 token。done 的 run 不继承:主动重跑语义就是重来。
1753
+ prev_runs = sorted((r for r in _RUNS.values()
1754
+ if r.get("task_id") == task_id and r["id"] != run["id"]),
1755
+ key=lambda r: r["id"], reverse=True)
1756
+ for prev in prev_runs:
1757
+ if prev.get("status") not in ("failed", "cancelled", "timeout"):
1758
+ continue
1759
+ if prev.get("impl_session") and "inherit_session" not in run:
1760
+ run["inherit_session"] = dict(prev["impl_session"])
1761
+ if prev.get("handoff") and "handoff" not in run:
1762
+ run["handoff"] = dict(prev["handoff"])
1763
+ if "inherit_session" in run or "handoff" in run:
1764
+ break
1741
1765
  task["status"] = "queued"
1742
1766
  _save_json(paths.TASKS_DIR / (task_id + ".json"), task)
1743
1767
  _save_json(paths.RUNS_DIR / run["id"] / "run.json", run)