From f56b72b69564a42ccc6c7f14b407382b8648d085 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:27:03 +0800 Subject: [PATCH 1/2] fix(research): record source receipt clocks after reads Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../capabilities/external_research/README.md | 7 +++ loopx/extensions/public_github_research.py | 6 +- .../references/repair-patterns.md | 1 + .../test_public_github_evidence.py | 56 +++++++++++++++++++ 4 files changed, 67 insertions(+), 3 deletions(-) diff --git a/loopx/capabilities/external_research/README.md b/loopx/capabilities/external_research/README.md index 560889229e..c93443ee7e 100644 --- a/loopx/capabilities/external_research/README.md +++ b/loopx/capabilities/external_research/README.md @@ -202,6 +202,13 @@ cannot authorize or prove a successful read. 规划和执行均检查仓库当前公开性;不使用 token、cookie、私有仓库、分支 head、重定向、 代理凭据、原文持久化或自动采纳。保留的 ready 行本身不证明执行成功。 +Each successful source's `accessed_at` records its UTC observation after the +bounded read; receipt `completed_at` is recorded after all attempted reads, +including failures. Neither timestamp establishes publication or first availability. + +每条成功来源的 `accessed_at` 在有界读取后记录 UTC 观察时间;回执的 +`completed_at` 在全部读取尝试(包括失败)结束后记录。两者均不证明发布或首次可得时间。 + ```bash loopx external-evidence plan --public-github \ --objective "Inspect public source" --user-activity "Choose a source" \ diff --git a/loopx/extensions/public_github_research.py b/loopx/extensions/public_github_research.py index 6903c9d32e..ae7a9e60f4 100644 --- a/loopx/extensions/public_github_research.py +++ b/loopx/extensions/public_github_research.py @@ -89,7 +89,6 @@ def execute_public_github(plan: dict) -> dict: raise ValueError("public GitHub execution requires one to eight pinned sources") terms = request.get("search_terms", []) records, failures = [], [] - now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") for index, (ref, (owner, repo, revision, path)) in enumerate(zip(refs, sources), 1): try: _public_repository(owner, repo) @@ -100,6 +99,7 @@ def execute_public_github(plan: dict) -> dict: if not text.strip(): failures.append(f"Source {index}: empty source") continue + accessed_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") matches = [] for term in terms: lines = [str(index) for index, line in enumerate(text.splitlines(), 1) if term in line] @@ -112,7 +112,7 @@ def execute_public_github(plan: dict) -> dict: records.append({"source_ref": ref, "source_family": "github_repository_file", "basis": "observed", "finding": finding, "limitation": "Retrieval/literal matches only; no semantic conclusion, execution test or completeness claim.", - "publication_date": None, "accessed_at": now, + "publication_date": None, "accessed_at": accessed_at, "content_digest": "sha256:" + hashlib.sha256(raw).hexdigest()}) except HTTPError as error: failures.append(f"Source {index}: HTTP {error.code}") @@ -124,7 +124,7 @@ def execute_public_github(plan: dict) -> dict: "provider_id": PROVIDER_ID, "provider_kind": "method", "status": status, "sources": records, "summary": f"Read {len(records)} of {len(refs)} requested public sources.", "limitations": ["Original-source fallback remains available; parent admission is required.", *failures], - "completed_at": now} + "completed_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")} return {"receipt": receipt, "execution": {"provider_id": PROVIDER_ID, "source_reads_observed": len(records), "requested_source_count": len(refs), "raw_content_persisted": False, "credentials_used": False, diff --git a/skills/loopx-self-repair/references/repair-patterns.md b/skills/loopx-self-repair/references/repair-patterns.md index fe8592dcbf..887e296808 100644 --- a/skills/loopx-self-repair/references/repair-patterns.md +++ b/skills/loopx-self-repair/references/repair-patterns.md @@ -5,6 +5,7 @@ teaches a reusable control-plane lesson. | Pattern | Symptoms | Evidence To Read | Likely Root | Durable Repair | | --- | --- | --- | --- | --- | +| `public_source_receipt_clock_precedes_reads` | Multiple sources share a batch-start observation time, or a receipt reports completion before its final source read returns. | Installed provider source pin, actual execution process bounds, source/receipt timestamps, a controlled advancing-clock case and canonical readback. | The provider captures one UTC timestamp before its HTTP loop and reuses it for source observations and completion. | Record each successful source observation after its bounded read and record completion after all attempts, including partial, empty and failed results. Keep publication/first-availability unknown, retain original receipts and reuse the existing typed identity/admission/projection owner. Verify late failures cannot move the completion boundary before an earlier successful observation; do not claim authenticated execution or downstream coverage from timestamp correctness. | | `candidate_outcome_as_execution_blocker` | An evaluation repeatedly enters a bounded retry after rejecting individual candidates, although independent authorized evaluation remains executable. | Exact selected Todo, typed delivery outcome and result class, committed retry origin/due time, validated evidence delta, current vision/frontier and installed source pin. | The caller classified candidate uncertainty or rejection as blocked execution for the whole work item; a stale runtime may also explain the retry as an unrelated user gate. | Correct the existing Todo/vision and classify new validated evaluation evidence as progress only when it actually advances acceptance. Stop answered finite questions; keep pure reuse non-progress. Preserve genuine blocked retries and independent clocks. Reproduce retry versus ordinary notice through the existing quota/interaction/scheduler path, verify the installed revision contains the owner fix, and retain old receipts. Never remove cooldowns globally or manufacture progress. | | `due_monitor_display_order_starvation` | A recurring Monitor is selected on every wake while equally ranked older due Monitors never run. | Complete eligible due set, claim/profile/priority ranks, due instants, display caps, selected Todo and exact poll/Turn receipts. | Display index determines execution order before a one-row cap; the first Monitor becomes due again before the next admitted Turn. | In the existing typed planning owner, retain scope and eligibility fences, then rank equally eligible work by due instant before display index and truncation. Cover recurring reschedules, stable ties, capability/claim negatives and real provider replay. Preserve exact per-Turn settlement; fairness does not authorize same-wake loops, extra quota spend or a shorter owner cadence. | | `receipt_freshness_commit_gap` | A receipt-bound follow-up passes its first freshness check, then commits an older Turn after a newer receipt arrives during lock contention or final persistence. An immediate retry correctly reports the old Turn as stale. | Ordered receipt events, the exact Turn ID, state-lock timing, commit result, and persisted state identity. | Receipt freshness and scheduler persistence did not hold the same cross-runtime append fence, so a newer receipt could arrive after the final check. | Keep the early check for fast rejection. Acquire the scheduler-state lock before the receipt-log mutation lock, then retain both locks from the final receipt read through legacy migration and the final state rename. Make every receipt writer use the same mutation lock. Return the existing typed missing or stale failure without mutation. Prove both lock-wait and read-to-rename interleavings with real writers, then scan for reverse lock order. | diff --git a/tests/capabilities/test_public_github_evidence.py b/tests/capabilities/test_public_github_evidence.py index b490902269..8538ae55ec 100644 --- a/tests/capabilities/test_public_github_evidence.py +++ b/tests/capabilities/test_public_github_evidence.py @@ -92,6 +92,62 @@ def test_partial_reads_are_observed_not_admitted_or_complete(monkeypatch): assert rejected["retirement"]["reason"] == "parent_rejected" +@pytest.mark.parametrize("contents,status", [ + ([b"first", b"second"], "succeeded"), + ([b"first", None], "succeeded"), + ([None, None], "failed"), + ([b"", b""], "no_evidence"), + ([b"\x00binary", b"\x00binary"], "failed"), +]) +def test_source_observation_and_completion_clocks_follow_reads(monkeypatch, contents, status): + from datetime import datetime, timedelta, timezone + + from loopx.extensions.lark.presentation.message_card import build_lark_markdown_reply_card + + started = datetime(2026, 1, 1, tzinfo=timezone.utc) + current = started + source_returns = [] + + class Clock: + @staticmethod + def now(tz): + assert tz == timezone.utc + return current + + def delayed_read(url): + nonlocal current + current += timedelta(seconds=1) + if url.startswith("https://api.github.com/"): + return b'{"private":false}' + content = contents[len(source_returns)] + source_returns.append(current) + if content is None: + raise HTTPError(url, 404, "not found", {}, None) + return content + + monkeypatch.setattr(provider, "datetime", Clock) + monkeypatch.setattr(provider, "_read", delayed_read) + p = plan([REF, SECOND]) + output = provider.execute_public_github(p) + receipt = output["receipt"] + assert receipt["status"] == status + assert datetime.fromisoformat(receipt["completed_at"].replace("Z", "+00:00")) == started + timedelta(seconds=4) + expected = [(ref, at) for ref, at, content in zip([REF, SECOND], source_returns, contents) + if content and not content.startswith(b"\x00")] + assert [(s["source_ref"], datetime.fromisoformat(s["accessed_at"].replace("Z", "+00:00"))) + for s in receipt["sources"]] == expected + assert all(s["publication_date"] is None for s in receipt["sources"]) + assert output["execution"]["raw_content_persisted"] is False + assert output["execution"]["credentials_used"] is False + accepted = admission(p, receipt, "admit" if expected else "reject") + projected = readback(p, receipt, accepted) + assert projected["sources"] == receipt["sources"] + assert projected["evidence_coverage_observed"] is False + card = build_lark_markdown_reply_card(render_readback(projected)) + body = card["elements"][0]["text"]["content"] + assert all(s["accessed_at"] in body for s in receipt["sources"]) + + @pytest.mark.parametrize("mutate", ["source_refs", "search_terms", "objective"]) def test_execute_rejects_mutated_plan_before_provider_calls(tmp_path, monkeypatch, mutate): p = plan() From 5c2a74ff3cc4f2b7370d44c5a2328c462f7f3521 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:18:15 +0800 Subject: [PATCH 2/2] docs: preserve repair pattern table after conflict resolution Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- skills/loopx-self-repair/references/repair-patterns.md | 1 - 1 file changed, 1 deletion(-) diff --git a/skills/loopx-self-repair/references/repair-patterns.md b/skills/loopx-self-repair/references/repair-patterns.md index 8dc33b27aa..cc0eee102c 100644 --- a/skills/loopx-self-repair/references/repair-patterns.md +++ b/skills/loopx-self-repair/references/repair-patterns.md @@ -6,7 +6,6 @@ teaches a reusable control-plane lesson. | Pattern | Symptoms | Evidence To Read | Likely Root | Durable Repair | | --- | --- | --- | --- | --- | | `public_source_receipt_clock_precedes_reads` | Multiple sources share a batch-start observation time, or a receipt reports completion before its final source read returns. | Installed provider source pin, actual execution process bounds, source/receipt timestamps, a controlled advancing-clock case and canonical readback. | The provider captures one UTC timestamp before its HTTP loop and reuses it for source observations and completion. | Record each successful source observation after its bounded read and record completion after all attempts, including partial, empty and failed results. Keep publication/first-availability unknown, retain original receipts and reuse the existing typed identity/admission/projection owner. Verify late failures cannot move the completion boundary before an earlier successful observation; do not claim authenticated execution or downstream coverage from timestamp correctness. | - | `public_source_failure_step_erasure` | A public-source plan is ready, but execution reports only a generic unavailable read, hiding whether visibility, download or text validation failed. | Original exact-plan receipt, source index, read-only process bounds, provider source pin and shared readback. | One catch path erased the reached provider step; readiness was mistaken for successful retrieval or the generic error was guessed to mean lost permission. | Preserve the actual reached step in existing redacted limitations, without exposing exception reasons or changing admission, retry or source status. Cover visibility, download, malformed text and partial HTTP failure through the canonical CLI and shared sinks. Keep original failed receipts; diagnose a changed producer under a bounded trial rather than repeatedly fetching the same object or freezing independent work. | | `candidate_outcome_as_execution_blocker` | An evaluation repeatedly enters a bounded retry after rejecting individual candidates, although independent authorized evaluation remains executable. | Exact selected Todo, typed delivery outcome and result class, committed retry origin/due time, validated evidence delta, current vision/frontier and installed source pin. | The caller classified candidate uncertainty or rejection as blocked execution for the whole work item; a stale runtime may also explain the retry as an unrelated user gate. | Correct the existing Todo/vision and classify new validated evaluation evidence as progress only when it actually advances acceptance. Stop answered finite questions; keep pure reuse non-progress. Preserve genuine blocked retries and independent clocks. Reproduce retry versus ordinary notice through the existing quota/interaction/scheduler path, verify the installed revision contains the owner fix, and retain old receipts. Never remove cooldowns globally or manufacture progress. | | `due_monitor_display_order_starvation` | A recurring Monitor is selected on every wake while equally ranked older due Monitors never run. | Complete eligible due set, claim/profile/priority ranks, due instants, display caps, selected Todo and exact poll/Turn receipts. | Display index determines execution order before a one-row cap; the first Monitor becomes due again before the next admitted Turn. | In the existing typed planning owner, retain scope and eligibility fences, then rank equally eligible work by due instant before display index and truncation. Cover recurring reschedules, stable ties, capability/claim negatives and real provider replay. Preserve exact per-Turn settlement; fairness does not authorize same-wake loops, extra quota spend or a shorter owner cadence. |