diff --git a/loopx/capabilities/external_research/README.md b/loopx/capabilities/external_research/README.md index fd85ae5dc0..ffc8307104 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` 在全部读取尝试(包括失败)结束后记录。两者均不证明发布或首次可得时间。 + Failure limitations identify the reached step: public repository visibility, source file read, or source text validation. HTTP status codes remain visible; exception messages, response bodies and transport reasons remain redacted. diff --git a/loopx/extensions/public_github_research.py b/loopx/extensions/public_github_research.py index 3db3571400..6d2e33079f 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): read_step = "public repository visibility check" try: @@ -103,6 +102,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] @@ -115,7 +115,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} during {read_step}") @@ -127,7 +127,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 1771442adb..cc0eee102c 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. | | `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. | diff --git a/tests/capabilities/test_public_github_evidence.py b/tests/capabilities/test_public_github_evidence.py index eff900ab1f..994bc21ba3 100644 --- a/tests/capabilities/test_public_github_evidence.py +++ b/tests/capabilities/test_public_github_evidence.py @@ -92,6 +92,61 @@ 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("failed_step,expected", [ ("visibility", "public repository visibility check"), ("download", "source file read"),