Commit ee6787bade6a8dc0b38659f4624b9f6c12ac5b1f
1 parent
838667ec
U-020: p_usage_limit_wait full acceptance (PH2-S3 complete)
Re-checking REQ-14's boundary case against the letter of its text found a real functional gap, not just a testing gap: U-013's opus handling only did "skip backoff" -- it never actually retried on a downgraded model, just failed immediately. REQ-14 boundary says "不走退避轮询,直接降级到非 Opus 模型重试" (skip backoff, retry immediately on a non-opus model) -- the retry was the part that was missing. Fixed: run_fn's signature is now run_fn(cwd, model=None); an opus quota signal retries immediately (no backoff) with opus_downgrade_model (default "claude-sonnet-5"). If the downgraded model somehow also reports an opus signal (shouldn't happen -- it's not using Opus), this doesn't loop forever chasing it, raises UsageLimitWaitTimeoutError instead. work_graph_execute.py's own opus handling (U-012) deliberately NOT changed to match: REQ-14's coverage only names c_work_node_run/c_org_node_trigger, and that mechanism (a multi-agent dynamic workflow, no single top-level --agent identity) has no one "the model" to downgrade -- a deeper A-1 question, documented as out of scope rather than silently left inconsistent without explanation. Also filled in a real test gap: quota_limit_type=weekly (detection + recovery, and the 9-day timeout) was never tested despite the mechanism already supporting it since U-013. 11 new/changed stdlib unittest cases across work_node_run.py's test suite (all existing run_fn lambdas updated for the new signature). 178 total across U-001-020, all passing. This closes PH2-S3 alongside U-019. REQ-14 is now genuinely complete, not just "mostly right." Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
5 changed files
with
123 additions
and
24 deletions
docs/development-plan.md
| ... | ... | @@ -193,6 +193,7 @@ PH2-S1/S2/S3/S4 之间除标注的依赖外相互独立,可并行推进。 |
| 193 | 193 | - **U-020 [Risk Gate]** `p_usage_limit_wait` 完整验收(`session`/`weekly`/`opus` 三种 `quota_limit_type` + 超时兜底) |
| 194 | 194 | - 依赖:U-012(骨架已在 U-012 实现,本 Unit 补齐边界/失败场景) |
| 195 | 195 | - Done:REQ-14 全部用例通过(含超时转 `blocked_pending_human, blocked_reason='usage_limit_wait_timeout'`) |
| 196 | + - **实现补充(2026-07-31)**:这次不只是补测试——重新对照 REQ-14 boundary 用例原文发现,U-013 时 `work_node_run.py` 的 opus 处理只做到"不退避",没做到"直接降级到非 Opus 模型重试"(原来是探测到 opus 额度直接抛错,从没真正重试过)。补上了:`run_fn` 签名改成 `run_fn(cwd, model=None)`,探测到 opus 信号后立即(不等待)用 `opus_downgrade_model`(默认 `claude-sonnet-5`)重试一次;如果降级后还是收到 opus 信号(不合理,非 opus 模型不该撞 opus 专属额度),直接判超时,不会无限重试。顺手把之前漏测的 `weekly` 额度类型(探测恢复 + 9 天超时)也补齐了。`work_graph_execute.py`(U-012)里同名的 opus 立即失败逻辑**没有**跟着改——REQ-14 覆盖列表只写了 `c_work_node_run`/`c_org_node_trigger`,没有 `c_work_graph_execute`,而且那条路径是多 Agent 动态工作流,没有一个单一的"the model"可以整体降级,属于更深的 A-1 动态工作流问题,本 Unit 不动 | |
| 196 | 197 | - 覆盖:04-P `p_usage_limit_wait`;REQ-14 |
| 197 | 198 | |
| 198 | 199 | ### PH2-S4 断点恢复 | ... | ... |
docs/sdd/unit_matrix.json
| ... | ... | @@ -33,7 +33,7 @@ |
| 33 | 33 | {"id": "U-017", "phase": "PH2", "sub_phase": "PH2-S1", "name": "人为植入 bug 完整闭环验证(见 development-plan.md 2026-07-31 实现补充)", "classification": "Risk Gate", "depends_on": ["U-016"], "coverage": {"model": [], "view": [], "controller": [], "process": [], "requirement": []}, "done": "架构文档 §11 验收标准第 4 条达成:run_fix_loop 串联 U-015 判定 + U-016 路由 + 真实 Coder 修复 + 真实 Validator 复核"}, |
| 34 | 34 | {"id": "U-018", "phase": "PH2", "sub_phase": "PH2-S2", "name": "c_skill_update + c_skill_rollback(见 development-plan.md 2026-07-31 实现补充)", "classification": "Risk Gate", "depends_on": ["U-015"], "coverage": {"model": ["skill", "skill_iteration"], "view": [], "controller": ["c_skill_update", "c_skill_rollback"], "process": [], "requirement": ["REQ-08"]}, "done": "REQ-08 全部通过;顺带解决了 U-015 的 skill_defect 缺口(bug_record.skill_iteration_suggestion_id 由 update_skill 回填)"}, |
| 35 | 35 | {"id": "U-019", "phase": "PH2", "sub_phase": "PH2-S3", "name": "budget_guardrail_config + c_budget_check(见 development-plan.md 2026-07-31 实现补充)", "classification": "Risk Gate", "depends_on": ["U-012"], "coverage": {"model": ["budget_guardrail_config"], "view": [], "controller": ["c_budget_check"], "process": ["p_budget_guardrail"], "requirement": ["REQ-10"]}, "done": "REQ-10 happy/boundary 通过;boundary 复用 U-016 manual_resume,完整 c_budget_config_update UI 是 U-030 的事"}, |
| 36 | - {"id": "U-020", "phase": "PH2", "sub_phase": "PH2-S3", "name": "p_usage_limit_wait 完整验收", "classification": "Risk Gate", "depends_on": ["U-012"], "coverage": {"model": [], "view": [], "controller": [], "process": ["p_usage_limit_wait"], "requirement": ["REQ-14"]}, "done": "REQ-14 全部通过(含超时兜底)"}, | |
| 36 | + {"id": "U-020", "phase": "PH2", "sub_phase": "PH2-S3", "name": "p_usage_limit_wait 完整验收(见 development-plan.md 2026-07-31 实现补充)", "classification": "Risk Gate", "depends_on": ["U-012"], "coverage": {"model": [], "view": [], "controller": [], "process": ["p_usage_limit_wait"], "requirement": ["REQ-14"]}, "done": "REQ-14 全部通过(含超时兜底);补上了 opus 真实降级重试(U-013 时只做到不退避,没做到真重试)+ 之前漏测的 weekly 类型"}, | |
| 37 | 37 | {"id": "U-021", "phase": "PH2", "sub_phase": "PH2-S4", "name": "Graph Engine 持久化 + 断点恢复", "classification": "Risk Gate", "depends_on": ["U-012", "U-013"], "coverage": {"model": ["work_graph_run", "work_node_execution"], "view": [], "controller": [], "process": ["p_work_graph_execution"], "requirement": ["REQ-06"]}, "done": "REQ-06 happy/boundary/failure 通过"}, |
| 38 | 38 | |
| 39 | 39 | {"id": "U-022", "phase": "PH3", "sub_phase": "PH3-S1", "name": "v_project_setup + c_pick_folder 全平台", "classification": "Value Path", "depends_on": ["U-007", "U-008"], "coverage": {"model": [], "view": ["v_project_setup"], "controller": ["c_pick_folder"], "process": [], "requirement": ["REQ-11"]}, "done": "REQ-11 全部通过"}, | ... | ... |
src/test_work_node_run.py
| ... | ... | @@ -65,7 +65,7 @@ class InputValidationTest(WorkNodeRunTestBase): |
| 65 | 65 | def test_invalid_input_rejected_before_execution_starts(self): |
| 66 | 66 | with self.assertRaises(work_node_run.InvalidInputError): |
| 67 | 67 | work_node_run.run_work_node( |
| 68 | - self.conn, "node-1", {}, _IN_SCHEMA, _OUT_SCHEMA, run_fn=lambda cwd: {"value": 1} | |
| 68 | + self.conn, "node-1", {}, _IN_SCHEMA, _OUT_SCHEMA, run_fn=lambda cwd, model=None: {"value": 1} | |
| 69 | 69 | ) |
| 70 | 70 | status = self.conn.execute("SELECT status FROM work_node_execution WHERE node_exec_id = 'node-1'").fetchone()[0] |
| 71 | 71 | self.assertEqual(status, "pending") |
| ... | ... | @@ -76,7 +76,7 @@ class InputValidationTest(WorkNodeRunTestBase): |
| 76 | 76 | class HappyPathTest(WorkNodeRunTestBase): |
| 77 | 77 | def test_valid_output_marks_node_done(self): |
| 78 | 78 | output = work_node_run.run_work_node( |
| 79 | - self.conn, "node-1", {"goal": "x"}, _IN_SCHEMA, _OUT_SCHEMA, run_fn=lambda cwd: {"value": 42} | |
| 79 | + self.conn, "node-1", {"goal": "x"}, _IN_SCHEMA, _OUT_SCHEMA, run_fn=lambda cwd, model=None: {"value": 42} | |
| 80 | 80 | ) |
| 81 | 81 | self.assertEqual(output, {"value": 42}) |
| 82 | 82 | row = self.conn.execute( |
| ... | ... | @@ -89,7 +89,7 @@ class HappyPathTest(WorkNodeRunTestBase): |
| 89 | 89 | def test_execution_log_file_created_active_then_closed(self): |
| 90 | 90 | seen_status_during_run = [] |
| 91 | 91 | |
| 92 | - def run_fn(cwd): | |
| 92 | + def run_fn(cwd, model=None): | |
| 93 | 93 | seen_status_during_run.append( |
| 94 | 94 | self.conn.execute("SELECT status FROM execution_log_file").fetchone()[0] |
| 95 | 95 | ) |
| ... | ... | @@ -101,7 +101,7 @@ class HappyPathTest(WorkNodeRunTestBase): |
| 101 | 101 | self.assertEqual(final_status, "closed") |
| 102 | 102 | |
| 103 | 103 | def test_log_file_closed_even_when_run_fn_raises(self): |
| 104 | - def boom(cwd): | |
| 104 | + def boom(cwd, model=None): | |
| 105 | 105 | raise RuntimeError("node blew up") |
| 106 | 106 | |
| 107 | 107 | with self.assertRaises(RuntimeError): |
| ... | ... | @@ -114,7 +114,7 @@ class RetryOnSchemaFailureTest(WorkNodeRunTestBase): |
| 114 | 114 | def test_retries_then_succeeds(self): |
| 115 | 115 | attempts = {"n": 0} |
| 116 | 116 | |
| 117 | - def run_fn(cwd): | |
| 117 | + def run_fn(cwd, model=None): | |
| 118 | 118 | attempts["n"] += 1 |
| 119 | 119 | if attempts["n"] < 3: |
| 120 | 120 | return {"wrong_key": True} |
| ... | ... | @@ -133,7 +133,7 @@ class RetryOnSchemaFailureTest(WorkNodeRunTestBase): |
| 133 | 133 | with self.assertRaises(work_node_run.SchemaValidationFailedError): |
| 134 | 134 | work_node_run.run_work_node( |
| 135 | 135 | self.conn, "node-1", {"goal": "x"}, _IN_SCHEMA, _OUT_SCHEMA, |
| 136 | - run_fn=lambda cwd: {"wrong_key": True}, max_retries=2, | |
| 136 | + run_fn=lambda cwd, model=None: {"wrong_key": True}, max_retries=2, | |
| 137 | 137 | ) |
| 138 | 138 | row = self.conn.execute( |
| 139 | 139 | "SELECT status, retry_count FROM work_node_execution WHERE node_exec_id = 'node-1'" |
| ... | ... | @@ -157,7 +157,7 @@ class UsageLimitWaitTest(WorkNodeRunTestBase): |
| 157 | 157 | def test_session_limit_then_recovers(self): |
| 158 | 158 | attempts = {"n": 0} |
| 159 | 159 | |
| 160 | - def run_fn(cwd): | |
| 160 | + def run_fn(cwd, model=None): | |
| 161 | 161 | attempts["n"] += 1 |
| 162 | 162 | if attempts["n"] == 1: |
| 163 | 163 | work_node_run.raise_if_usage_limited("Error: You've hit your session limit") |
| ... | ... | @@ -179,8 +179,46 @@ class UsageLimitWaitTest(WorkNodeRunTestBase): |
| 179 | 179 | ).fetchone() |
| 180 | 180 | self.assertEqual(event[0], "quota_wait_started") |
| 181 | 181 | |
| 182 | - def test_opus_quota_raises_immediately_no_backoff(self): | |
| 183 | - def run_fn(cwd): | |
| 182 | + def test_opus_quota_retries_immediately_on_downgraded_model_no_backoff(self): | |
| 183 | + # REQ-14 boundary: "不走退避轮询,直接降级到非 Opus 模型重试" -- an actual | |
| 184 | + # retry with a different model, not just detect-and-fail. | |
| 185 | + calls = [] | |
| 186 | + | |
| 187 | + def run_fn(cwd, model=None): | |
| 188 | + calls.append(model) | |
| 189 | + if len(calls) == 1: | |
| 190 | + work_node_run.raise_if_usage_limited("You've hit your Opus limit") | |
| 191 | + return {"value": "recovered on " + str(model)} | |
| 192 | + | |
| 193 | + output = work_node_run.run_work_node( | |
| 194 | + self.conn, "node-1", {"goal": "x"}, _IN_SCHEMA, _OUT_SCHEMA, run_fn=run_fn, | |
| 195 | + sleep=self._sleep, now=self._now, | |
| 196 | + ) | |
| 197 | + self.assertEqual(calls, [None, "claude-sonnet-5"]) | |
| 198 | + self.assertEqual(output, {"value": "recovered on claude-sonnet-5"}) | |
| 199 | + self.assertEqual(self.sleeps, []) # no backoff wait for opus | |
| 200 | + status = self.conn.execute("SELECT status FROM work_node_execution WHERE node_exec_id = 'node-1'").fetchone()[0] | |
| 201 | + self.assertEqual(status, "done") | |
| 202 | + | |
| 203 | + def test_opus_quota_custom_downgrade_model_is_used(self): | |
| 204 | + calls = [] | |
| 205 | + | |
| 206 | + def run_fn(cwd, model=None): | |
| 207 | + calls.append(model) | |
| 208 | + if len(calls) == 1: | |
| 209 | + work_node_run.raise_if_usage_limited("You've hit your Opus limit") | |
| 210 | + return {"value": "ok"} | |
| 211 | + | |
| 212 | + work_node_run.run_work_node( | |
| 213 | + self.conn, "node-1", {"goal": "x"}, _IN_SCHEMA, _OUT_SCHEMA, run_fn=run_fn, | |
| 214 | + sleep=self._sleep, now=self._now, opus_downgrade_model="claude-fable-5", | |
| 215 | + ) | |
| 216 | + self.assertEqual(calls, [None, "claude-fable-5"]) | |
| 217 | + | |
| 218 | + def test_opus_quota_persisting_after_downgrade_raises_timeout(self): | |
| 219 | + # a non-opus model shouldn't hit an opus-specific quota again -- if it | |
| 220 | + # somehow does, this doesn't loop forever chasing it | |
| 221 | + def run_fn(cwd, model=None): | |
| 184 | 222 | work_node_run.raise_if_usage_limited("You've hit your Opus limit") |
| 185 | 223 | |
| 186 | 224 | with self.assertRaises(work_node_run.UsageLimitWaitTimeoutError): |
| ... | ... | @@ -191,7 +229,7 @@ class UsageLimitWaitTest(WorkNodeRunTestBase): |
| 191 | 229 | self.assertEqual(self.sleeps, []) |
| 192 | 230 | |
| 193 | 231 | def test_session_timeout_after_8_hours_blocks_node(self): |
| 194 | - def run_fn(cwd): | |
| 232 | + def run_fn(cwd, model=None): | |
| 195 | 233 | work_node_run.raise_if_usage_limited("You've hit your session limit") |
| 196 | 234 | |
| 197 | 235 | with self.assertRaises(work_node_run.UsageLimitWaitTimeoutError): |
| ... | ... | @@ -203,6 +241,40 @@ class UsageLimitWaitTest(WorkNodeRunTestBase): |
| 203 | 241 | status = self.conn.execute("SELECT status FROM work_node_execution WHERE node_exec_id = 'node-1'").fetchone()[0] |
| 204 | 242 | self.assertEqual(status, "blocked") |
| 205 | 243 | |
| 244 | + def test_weekly_limit_then_recovers(self): | |
| 245 | + # previously untested quota_limit_type -- session/opus were covered, | |
| 246 | + # weekly wasn't, even though the detection regex and timeout table | |
| 247 | + # both already supported it | |
| 248 | + attempts = {"n": 0} | |
| 249 | + | |
| 250 | + def run_fn(cwd, model=None): | |
| 251 | + attempts["n"] += 1 | |
| 252 | + if attempts["n"] == 1: | |
| 253 | + work_node_run.raise_if_usage_limited("Error: You've hit your weekly limit") | |
| 254 | + return {"value": "recovered"} | |
| 255 | + | |
| 256 | + work_node_run.run_work_node( | |
| 257 | + self.conn, "node-1", {"goal": "x"}, _IN_SCHEMA, _OUT_SCHEMA, run_fn=run_fn, | |
| 258 | + sleep=self._sleep, now=self._now, | |
| 259 | + ) | |
| 260 | + quota_type = self.conn.execute( | |
| 261 | + "SELECT quota_limit_type FROM work_node_execution WHERE node_exec_id = 'node-1'" | |
| 262 | + ).fetchone()[0] | |
| 263 | + self.assertEqual(quota_type, "weekly") | |
| 264 | + | |
| 265 | + def test_weekly_timeout_after_9_days_blocks_node(self): | |
| 266 | + def run_fn(cwd, model=None): | |
| 267 | + work_node_run.raise_if_usage_limited("You've hit your weekly limit") | |
| 268 | + | |
| 269 | + with self.assertRaises(work_node_run.UsageLimitWaitTimeoutError): | |
| 270 | + work_node_run.run_work_node( | |
| 271 | + self.conn, "node-1", {"goal": "x"}, _IN_SCHEMA, _OUT_SCHEMA, run_fn=run_fn, | |
| 272 | + sleep=self._sleep, now=self._now, | |
| 273 | + ) | |
| 274 | + self.assertGreaterEqual(self.clock[0], 9 * 24 * 3600) | |
| 275 | + status = self.conn.execute("SELECT status FROM work_node_execution WHERE node_exec_id = 'node-1'").fetchone()[0] | |
| 276 | + self.assertEqual(status, "blocked") | |
| 277 | + | |
| 206 | 278 | |
| 207 | 279 | class RealGitWorktreeIsolationTest(WorkNodeRunTestBase): |
| 208 | 280 | """REQ-03 boundary: two independent nodes, each in its own git worktree, |
| ... | ... | @@ -221,7 +293,7 @@ class RealGitWorktreeIsolationTest(WorkNodeRunTestBase): |
| 221 | 293 | self.conn.commit() |
| 222 | 294 | |
| 223 | 295 | def _write_file_run_fn(self, filename, content): |
| 224 | - def run_fn(cwd): | |
| 296 | + def run_fn(cwd, model=None): | |
| 225 | 297 | with open(os.path.join(cwd, filename), "w") as f: |
| 226 | 298 | f.write(content) |
| 227 | 299 | subprocess.run(["git", "add", filename], cwd=cwd, check=True, capture_output=True) | ... | ... |
src/work_graph_execute.py
| ... | ... | @@ -141,11 +141,20 @@ def execute_work_graph_with_usage_limit_retry( |
| 141 | 141 | """Wraps execute_work_graph with p_usage_limit_wait (04-P §4.4.1): a |
| 142 | 142 | session/weekly quota hit backs off 5->10->20->30min (repeating at 30) |
| 143 | 143 | and retries until that type's timeout, then raises |
| 144 | - UsageLimitWaitTimeoutError. An opus quota hit skips backoff entirely | |
| 145 | - (04-P: "直接...降级到非 Opus 模型重试" -- the actual model swap is a | |
| 146 | - caller-level decision, not this function's; it raises immediately so the | |
| 147 | - caller can decide). ClassifierBlockedError is never retried (03-C: | |
| 148 | - not retryable, needs human review) and propagates immediately. | |
| 144 | + UsageLimitWaitTimeoutError. ClassifierBlockedError is never retried | |
| 145 | + (03-C: not retryable, needs human review) and propagates immediately. | |
| 146 | + | |
| 147 | + An opus quota hit skips backoff and raises immediately, unlike | |
| 148 | + src/work_node_run.py's run_work_node (U-020), which actually retries on | |
| 149 | + a downgraded model. Deliberately not matched here: REQ-14's coverage | |
| 150 | + (06 节) only names c_work_node_run/c_org_node_trigger, not | |
| 151 | + c_work_graph_execute, and this function's underlying mechanism (a | |
| 152 | + dynamic-workflow script with no single top-level --agent identity, per | |
| 153 | + 03-C §3.5) has no one "the model" to swap the way a single-agent call | |
| 154 | + does -- individual agent() calls inside the workflow script would each | |
| 155 | + need their own override, a deeper A-1 dynamic-workflow question this | |
| 156 | + Unit doesn't attempt. Caller still has to decide what "retry on a | |
| 157 | + non-opus model" means here. | |
| 149 | 158 | |
| 150 | 159 | `sleep`/`now` are injectable so tests don't block on real 5-30 minute waits. |
| 151 | 160 | """ | ... | ... |
src/work_node_run.py
| ... | ... | @@ -124,10 +124,13 @@ def run_work_node( |
| 124 | 124 | repo_path=None, |
| 125 | 125 | sleep=time.sleep, |
| 126 | 126 | now=time.monotonic, |
| 127 | + opus_downgrade_model="claude-sonnet-5", | |
| 127 | 128 | ): |
| 128 | - """Runs one Work Node to contract. `run_fn(cwd) -> output_payload` is the | |
| 129 | - caller-supplied actual work (a real `claude -p` call in production, a | |
| 130 | - fake in tests). Returns the validated output_payload. | |
| 129 | + """Runs one Work Node to contract. `run_fn(cwd, model) -> output_payload` | |
| 130 | + is the caller-supplied actual work (a real `claude -p` call in | |
| 131 | + production, a fake in tests); `model` is None unless this call is an | |
| 132 | + opus-quota-triggered retry, in which case it's `opus_downgrade_model`. | |
| 133 | + Returns the validated output_payload. | |
| 131 | 134 | |
| 132 | 135 | Raises InvalidInputError (input schema), UsageLimitWaitTimeoutError |
| 133 | 136 | (p_usage_limit_wait exhausted), or leaves work_node_execution.status |
| ... | ... | @@ -169,9 +172,11 @@ def run_work_node( |
| 169 | 172 | |
| 170 | 173 | retry_count = 0 |
| 171 | 174 | quota_wait_start = None |
| 175 | + current_model = None | |
| 176 | + opus_already_downgraded = False | |
| 172 | 177 | while True: |
| 173 | 178 | try: |
| 174 | - output_payload = run_fn(cwd) | |
| 179 | + output_payload = run_fn(cwd, model=current_model) | |
| 175 | 180 | except _UsageLimitSignal as signal: |
| 176 | 181 | quota_type = signal.quota_limit_type |
| 177 | 182 | if quota_wait_start is None: |
| ... | ... | @@ -188,9 +193,21 @@ def run_work_node( |
| 188 | 193 | ) |
| 189 | 194 | conn.commit() |
| 190 | 195 | if quota_type == "opus": |
| 191 | - raise UsageLimitWaitTimeoutError( | |
| 192 | - "opus quota exhausted for {} -- caller should retry on a non-opus model".format(node_exec_id) | |
| 193 | - ) from signal | |
| 196 | + # 04-P §4.4.1: "不用等待退避——直接把该节点这次调用降级到非 | |
| 197 | + # Opus 模型重试", not just detect-and-fail. Only one | |
| 198 | + # downgrade attempt -- if the downgraded model *also* | |
| 199 | + # reports an opus-quota signal, something's wrong (a | |
| 200 | + # non-opus model shouldn't hit an opus-specific quota), | |
| 201 | + # so this doesn't loop forever chasing it. | |
| 202 | + if opus_already_downgraded: | |
| 203 | + raise UsageLimitWaitTimeoutError( | |
| 204 | + "opus quota signal persisted for {} even after downgrading to {}".format( | |
| 205 | + node_exec_id, opus_downgrade_model | |
| 206 | + ) | |
| 207 | + ) from signal | |
| 208 | + current_model = opus_downgrade_model | |
| 209 | + opus_already_downgraded = True | |
| 210 | + continue | |
| 194 | 211 | elapsed = now() - quota_wait_start |
| 195 | 212 | if elapsed >= _TIMEOUT_SECONDS[quota_type]: |
| 196 | 213 | conn.execute( | ... | ... |