2026-10-20 TUE UTC 12:42 PM TPE 8:42 PM Auto-updates hourly 中文 EN
LIVE WIRE
← Back to the site changelog Site Changelog

Original commit record

fix: reserve independent repair and time for final Luna fallback (#300)

SHA
8c002ae7ee67846b9cd8fd7385195db10916915a
Commit date
Author
nnneeeooo-nnneeeooo
Committer
web-flow

Changed files (2)

  • pipeline/write.py modified · +61 / −8
  • tests/test_write_offline.py modified · +101 / −0

Full diff

@@ -82,6 +82,7 @@\n MAX_GROUPS_PER_RUN = 10\n MAX_DRAFT_SECONDS_PER_RUN = 15 * 60\n MAX_MODEL_ATTEMPT_SECONDS = 60\n+PREFERRED_ATTEMPT_SECONDS_WITH_LUNA = 30\n MAX_DRAFT_SECONDS_PER_GROUP = 120\n PROVIDER_ROUTING_OVERHEAD_SECONDS = 5\n TRANSIENT_PLATFORM_FAILURE_LIMIT = 2\n@@ -1484,14 +1485,17 @@ def _normalize_reject_reason(candidate: dict) -> None:\n             f"to establish a new event. Model detail: {reason}")\n \n \n-def _provider_attempt_allowance(provider, deadline: float | None = None) -> float:\n+def _provider_attempt_allowance(provider, deadline: float | None = None,\n+                                max_attempt_seconds: float | None = None) -> float:\n     """Return this provider's bounded call budget, clipped to its story time."""\n     allowance = getattr(\n         provider, "max_attempt_seconds", MAX_MODEL_ATTEMPT_SECONDS)\n     if (isinstance(allowance, bool)\n             or not isinstance(allowance, (int, float))\n             or allowance <= 0):\n         allowance = MAX_MODEL_ATTEMPT_SECONDS\n+    if max_attempt_seconds is not None:\n+        allowance = min(allowance, max_attempt_seconds)\n     if deadline is not None:\n         allowance = min(allowance, deadline - time.monotonic())\n     return allowance\n@@ -1507,9 +1511,34 @@ def _provider_group_budget(provider) -> float:\n     return budget\n \n \n+def _is_luna_provider(provider) -> bool:\n+    return (getattr(provider, "name", None) == "openai"\n+            and getattr(provider, "model", None) == "gpt-6-luna")\n+\n+\n+def _luna_reserve_seconds(providers) -> float:\n+    """Reserve one initial draft and one independent repair for final Luna."""\n+    return sum(2 * _provider_attempt_allowance(provider)\n+               + 2 * PROVIDER_ROUTING_OVERHEAD_SECONDS\n+               for provider in providers if _is_luna_provider(provider))\n+\n+\n def _provider_chain_budget(providers) -> float:\n     """Leave room for each preferred route to time out before GPT-6 Luna."""\n     providers = tuple(providers)\n+    luna_reserve = _luna_reserve_seconds(providers)\n+    if luna_reserve:\n+        preferred = [provider for provider in providers\n+                     if not _is_luna_provider(provider)]\n+        # Existing primary/shared fallback repairs remain bounded, while\n+        # neither can spend Luna's own repair time.\n+        preferred_budget = sum(_provider_attempt_allowance(\n+            provider, max_attempt_seconds=PREFERRED_ATTEMPT_SECONDS_WITH_LUNA)\n+            for provider in preferred)\n+        if preferred:\n+            preferred_budget += 2 * PREFERRED_ATTEMPT_SECONDS_WITH_LUNA\n+        return luna_reserve + preferred_budget + (\n+            len(preferred) + 2) * PROVIDER_ROUTING_OVERHEAD_SECONDS\n     budget = max(\n         (_provider_group_budget(provider) for provider in providers),\n         default=MAX_DRAFT_SECONDS_PER_GROUP,\n@@ -1527,7 +1556,8 @@ def _provider_chain_budget(providers) -> float:\n \n def _validated_draft(provider, group: dict, tries: int, ai_calls=None,\n                      run_trace: _RunTrace | None = None,\n-                     deadline: float | None = None):\n+                     deadline: float | None = None,\n+                     max_attempt_seconds: float | None = None):\n     """Call `provider` up to `tries` times until a draft survives validation\n     and quote verification.\n \n@@ -1556,7 +1586,8 @@ def _validated_draft(provider, group: dict, tries: int, ai_calls=None,\n                                                 previous_candidate)\n                 if run_trace is not None:\n                     run_trace.add_duration("promptAssembly", prompt_started)\n-                allowance = _provider_attempt_allowance(provider, deadline)\n+                allowance = _provider_attempt_allowance(\n+                    provider, deadline, max_attempt_seconds)\n                 with draft_timeout(allowance):\n                     candidate = provider.draft(\n                         SYSTEM_PROMPT, user_prompt, DRAFT_SCHEMA)\n@@ -2863,6 +2894,10 @@ def main() -> None:\n     else:\n         print("write: provider order: "\n               + " -> ".join(p.label for p in providers))\n+        if _luna_reserve_seconds(providers):\n+            print("write: Luna final fallback policy: preferred calls capped "\n+                  f"at {PREFERRED_ATTEMPT_SECONDS_WITH_LUNA}s; independent "\n+                  "draft/repair time reserved")\n         try:\n             # Thin-story companion retrieval: excerpt-only items (e.g.\n             # Simple Flying) act as a discovery index and the pipeline\n@@ -2962,6 +2997,11 @@ def main() -> None:\n                 continue\n             if drafted >= MAX_GROUPS_PER_RUN:\n                 break  # LLM budget for this run is spent; the rest waits\n+            if draft_deadline - time.monotonic() < _luna_reserve_seconds(alive):\n+                print("write: insufficient run time for Luna draft and repair; "\n+                      "retaining remaining groups for retry")\n+                run_trace.finish("retry_pending", "retry_pending")\n+                break\n             drafted += 1\n             draft, writer, verified = None, None, None\n             final_model = None\n@@ -2980,17 +3020,30 @@ def main() -> None:\n                     # Removed mid-group by a platform-level auth/quota\n                     # death earlier in this same iteration.\n                     continue\n-                # One primary retry and one responsive-fallback repair per\n-                # group; transport failures do not spend the repair budget.\n-                tries = 2 if provider is providers[0] or not fallback_repair_used else 1\n+                is_luna = _is_luna_provider(provider)\n+                luna_reserve = _luna_reserve_seconds(alive)\n+                provider_deadline = group_deadline\n+                attempt_cap = None\n+                if luna_reserve and not is_luna:\n+                    provider_deadline -= luna_reserve\n+                    attempt_cap = PREFERRED_ATTEMPT_SECONDS_WITH_LUNA\n+                    if time.monotonic() >= provider_deadline:\n+                        print(f"write: {provider.label} skipped on group "\n+                              f"{group.get('id')}: preserving Luna draft/repair time")\n+                        continue\n+                # Luna has its own repair, independent of the shared repair\n+                # for preferred fallbacks. Transport failures still fail over.\n+                tries = 2 if (is_luna or provider is providers[0]\n+                              or not fallback_repair_used) else 1\n                 try:\n                     candidate, facts = _validated_draft(provider, group,\n                                                         tries, ai_calls,\n                                                         run_trace,\n-                                                        deadline=group_deadline)\n+                                                        deadline=provider_deadline,\n+                                                        max_attempt_seconds=attempt_cap)\n                 except DraftInvalid:\n                     content_failed = True\n-                    if provider is not providers[0]:\n+                    if provider is not providers[0] and not is_luna:\n                         fallback_repair_used = True\n                     _record_provider_response(\n                         provider, transient_failure_counts)\n@@ -16,6 +16,7 @@\n import sys\n import tempfile\n import traceback\n+from contextlib import contextmanager\n from datetime import timedelta\n from pathlib import Path\n \n@@ -975,6 +976,104 @@ def test_luna_is_used_after_preferred_service_failures():\n     print("test_luna_is_used_after_preferred_service_failures: done")\n \n \n+def test_luna_repairs_after_shared_fallback_repair_is_spent():\n+    """A responsive preferred fallback cannot consume Luna's repair."""\n+    for valid_repair in (True, False):\n+        reset_data_dir()\n+        pending = load(DATA / "pending.json")\n+        group = pending["groups"][1]\n+        pending["groups"] = [group]\n+        common.save_json(DATA / "pending.json", pending)\n+        bad = json.loads(json.dumps(DRAFT_BIZ))\n+        bad["cat"] = "not-a-cat"\n+        short = json.loads(json.dumps(DRAFT_BIZ))\n+        short["zh"]["body"] = ["短訊", "短訊"]\n+        fabricated = json.loads(json.dumps(DRAFT_BIZ))\n+        fabricated["facts"][0]["sourceQuote"] = "This quote is fabricated."\n+        primary = FakeProvider("gemini", [providers.ProviderError("HTTP 503")])\n+        fallback = FakeProvider("wechat", [bad])\n+\n+        class Luna(FakeProvider):\n+            model = "gpt-6-luna"\n+            max_attempt_seconds = 90\n+            group_budget_seconds = 180\n+\n+            def draft(self, system_prompt, user_prompt, schema):\n+                self.prompts.append(user_prompt)\n+                return super().draft(system_prompt, user_prompt, schema)\n+\n+        luna = Luna("openai", [short, DRAFT_BIZ if valid_repair else fabricated])\n+        luna.prompts = []\n+        original = write.build_providers\n+        write.build_providers = lambda: [primary, fallback, luna]\n+        try:\n+            write.main()\n+        finally:\n+            write.build_providers = original\n+        check(primary.calls == 1 and fallback.calls == 2 and luna.calls == 2,\n+              "Luna independently repairs after preferred fallback uses both calls")\n+        check("body does not meet" in luna.prompts[1],\n+              "Luna repair receives the actual failed body validation")\n+        if valid_repair:\n+            check(len(all_articles()) == 1\n+                  and all_articles()[0]["writer"] == "openai:fake",\n+                  "a valid Luna repair publishes the formerly short draft")\n+        else:\n+            check(not all_articles() and load(DATA / "pending.json")["groups"],\n+                  "fabricated Luna repair remains unpublished and retryable")\n+            check(common.norm_url(group["items"][0]["url"])\n+                  not in load(DATA / "seen.json")["urls"],\n+                  "failed Luna repair does not consume the source")\n+\n+\n+def test_preferred_timeout_preserves_luna_and_run_budget():\n+    """A clipped run deadline still leaves two full Luna calls, then stops."""\n+    reset_data_dir()\n+    pending = load(DATA / "pending.json")\n+    pending["groups"] = [pending["groups"][1], pending["groups"][0]]\n+    common.save_json(DATA / "pending.json", pending)\n+    clock, allowances, order = [0.0], [], []\n+    bad = json.loads(json.dumps(DRAFT_BIZ))\n+    bad["cat"] = "not-a-cat"\n+\n+    class TimedProvider(FakeProvider):\n+        def draft(self, *args):\n+            order.append(self.name)\n+            clock[0] += allowances[-1]\n+            return super().draft(*args)\n+\n+    first = TimedProvider("gemini", [providers.ProviderError("ReadTimeout")])\n+    skipped = TimedProvider("wechat", [DRAFT_BIZ])\n+    luna = TimedProvider("openai", [bad, DRAFT_BIZ])\n+    luna.model = "gpt-6-luna"\n+    luna.max_attempt_seconds = 90\n+    luna.group_budget_seconds = 180\n+\n+    @contextmanager\n+    def fake_timeout(seconds):\n+        allowances.append(seconds)\n+        yield\n+\n+    original = (write.build_providers, write.time.monotonic,\n+                write.draft_timeout, write.MAX_DRAFT_SECONDS_PER_RUN)\n+    write.build_providers = lambda: [first, skipped, luna]\n+    write.time.monotonic = lambda: clock[0]\n+    write.draft_timeout = fake_timeout\n+    write.MAX_DRAFT_SECONDS_PER_RUN = 220\n+    try:\n+        write.main()\n+    finally:\n+        (write.build_providers, write.time.monotonic,\n+         write.draft_timeout, write.MAX_DRAFT_SECONDS_PER_RUN) = original\n+    check(order == ["gemini", "openai", "openai"],\n+          "preferred routes yield to final Luna when its reserved time is reached")\n+    check(allowances == [30, 90, 90] and clock[0] == 210,\n+          "preferred request is capped and Luna gets two full calls within run cap")\n+    check(len(all_articles()) == 1\n+          and len(load(DATA / "pending.json")["groups"]) == 1,\n+          "completed article is saved; next story waits for a full fallback budget")\n+\n+\n def test_editorial_gate():\n     """Model reject skips the group; fabricated quotes are dropped/blocking."""\n     reset_data_dir()\n@@ -1328,6 +1427,8 @@ def main():\n              test_model_chain_and_routing_policy,\n              test_provider_specific_time_budget,\n              test_luna_is_used_after_preferred_service_failures,\n+             test_luna_repairs_after_shared_fallback_repair_is_spent,\n+             test_preferred_timeout_preserves_luna_and_run_budget,\n              test_editorial_gate,\n              test_completeness_precheck,\n              test_major_title_only_is_retained_for_material_retry,

You found a secret theme!

8-bit mode is unlocked. Refresh to add it to your theme selector.