Skip to content

Retain all-scope learning windows and pending evidence - #5195

Merged
john-the-dev merged 7 commits into
mainfrom
codex/sutando-reliability-v3
Oct 7, 2026
Merged

john-the-dev merged 7 commits into
mainfrom
codex/sutando-reliability-v3

Conversation

@john-the-dev

@john-the-dev john-the-dev commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Learning consumers previously relied on a production-only sweep and event-ID progress without durable all-scope evidence. This collects every configured scope with real server cursors, persists immutable digest receipts before per-room progress, and refuses consumer admission when coverage is incomplete. Fresh proposal returns are checked against those receipts and retained as pending candidates; process exit and literal quote validity do not promote dossier facts.

Scope audit

The original 62-file bundle contained three review concerns. They are now independent PRs based directly on current main 95fbd566a:

  • This PR: 42 files for learning windows. Fifteen skill/config/script files implement bounded collection, dispatch, read-only proposal preflight, pending persistence and physical document readback. Five room-ops files supply paginated history collection and its documented contract. One existing core hook distinguishes detached consumers from core inbox ownership. Sixteen test files cover the retained contracts and regressions. Five documentation/catalog files describe the implementation and preserve the evaluation results.
  • Add bounded GitHub verification and govern optional status publication #5197: 20 files for GitHub verification and optional publication/task binding.
  • Preserve outstanding cron payloads until normal retirement #5198: 6 files for immutable outstanding cron payloads.

Removed the unused candidate document-write verification API and mandatory Claude startup hook/Node dependency. The retained readback classifies physical retention only. Existing user changes and installed engine state were preserved. These PRs have no stack dependency; shared documentation navigation can be reconciled during merge.

Review guide

Read docs/learning-window.md, then collection_pass.py/window_state.py, dispatch_collection.py, and pending_candidates.py/consumer_return.py. Configuration is opt-in. Collection progress is separate from learned facts; incomplete scopes remain unread. The preflight and final writer share the same validator. Detached role declaration is inbox routing, not authorization. Readback failures, changed identities and overly long intervals remain unknown. No candidate promotion, dossier trimming or legacy cursor migration is included.

Validation

The reduced branch passes 132 local tests across 15 unittest suites, plus 5/5 history mutation checks. Installed-entrypoint fixtures use private synthetic state, fake consumers and delegated read adapters. Documentation audit, skill manifest lint, Ruff, workspace-resolution lint, repository diff review checks and whitespace checks pass. CI results are shown on this PR; approval and merge remain separate decisions.

Evaluation and Rui's evidence

docs/reliability-v3-results.md and the accompanying JSON retain the original evaluated source 32aa78011113f8616cd82fc244576dc12e0492cf, commands, output tails, component measurements and limitations. That historical broader version passed 215 tests across 22 suites plus 12 mutation checks; it is distinct from the reduced PR validation above.

Rui supplied this first-hand assessment on 2026-10-07:

Rui used this version of Sutando for a whole day to help team perform code reviews, tests, coding, engaging with Codex core sutando for PR verifications. The Sutando was able to perform as intended in most of the time that it only get back to Rui with necessary TODOs and then perform the work quite autonomously and accurately. Rui gives 9 in accuracy and 8 in reliability.

Owner ratings are accuracy 9/10 and reliability 8/10; capability was not separately rated. The evaluation concluded at Rui's request and its monitor was deleted. Observer component results remain 283/290 numeric claims (97.6%) and 36/43 collection admissions (83.7%). These do not independently establish overall ratings or semantic truth. The 511 candidates remain pending; 32 substantive tasks were partly assessed, with 0/30 full outcomes independently established. Historical failures and unknowns remain recorded. Rui's full-day experience applies to the evaluated local version, not a new deployment of these split heads.

Private transcripts, credentials, room bodies and document contents are excluded. No new live deployment, external message or formal vote was performed to prepare these PRs. Voice/audio: N/A.

Previous-head Codex compatibility evidence

Codex core recommended holding the original head after finding that timeout cleanup left descendants running and launch.sh ignored the existing Python resolver. Both reproduced against the old head (two failing isolated regressions) and are corrected at a8f02cd5: the dispatcher supervises the consumer process group through timeout, cancellation and normal exit, and the launcher uses resolve_python/SUTANDO_PY. Regression coverage includes late background writes, cancellation and a failing PATH Python control. Descendants that deliberately create another process group/session remain outside this lifecycle guarantee.

132 local tests and 5/5 mutations pass; local changed-line coverage passes the unchanged 95% gate. CI run 37637008566 at this head passes all test jobs and the 95% diff-coverage gate. Earlier helper failures and the old 85% coverage failure in run 37631957069 remain historical failures; they are not erased by the new run.

Codex detached consumers require an explicit working directory and sandbox permissions from their caller. The Claude Stop hook is not Codex enforcement. No authenticated Codex core end-to-end witness has been established for these changes. The results report records these findings and limitations; prior PR5169 evidence does not validate this PR's contracts.

Codex core independently rechecked a8f02cd5, reproduced both old-head failures and confirmed both are resolved, then returned good / no blocker for the isolated compatibility checks. It reports 132 unittest tests and 5/5 mutations passing. Authenticated Codex integration is still unverified; core prepared a bounded synthetic test and requires direct owner authorization under its own action guard. Final peer reviews have been requested; this is not a live integration pass or merge authorization.

Current review revision

Current head e676b92: keeps paging across events tied at the inclusive cutoff millisecond and refuses explicit declined/error pages. Before: the expanded 17-test history suite failed 3 cases; after: 17/17 pass. Page-size (2), command-collection (8) and collection-pass (10) suites pass, along with 5/5 history mutation controls. Removed the two trailing blank EOF lines flagged by review. No live gateway run is claimed.

Chi’s Blue agent requested these fixes at the preceding heads. Current-head CI is successful, including coverage. Codex core independently returned no remaining blocker for the isolated fixes; Yixuan’s and Qingyun’s rereviews remain pending. Earlier CI successes, Mark approvals and isolated Codex verdicts apply to their recorded heads, and have not been carried forward. Authenticated integration remains unverified; no merge is requested.

Current-head CI is green. Core independently reran original-repro controls and relevant suites at the new head; its verdict remains isolated compatibility, not authenticated integration or formal GitHub approval. Blue independently verified the fixes and dismissed its previous requested-change reviews. Its new reviews are comment-only, leaving maintainer approval to Chi.

Chi Blue’s current-head rereview finds no new blockers. Mark’s current-head source approval also finds no remaining blocker, with its verification limits recorded in the review. These are agent reviews under shared logins, not human signoff. Yixuan’s and Qingyun’s independent reviews remain pending.

@john-the-dev
john-the-dev force-pushed the codex/sutando-reliability-v3 branch from ef4cc47 to b9c13b7 Compare October 7, 2026 13:45
@john-the-dev john-the-dev changed the title Improve Sutando reliability with retained evidence and owner-tested results Retain all-scope learning windows and pending evidence Oct 7, 2026
@john-the-dev
john-the-dev marked this pull request as ready for review October 7, 2026 13:59
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 91%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • skills/agent-room-ops/history.py (91.2%): Missing lines 14,17,23
  • skills/agent-room-ops/history_collection.py (100%)
  • skills/agent-room-ops/room_ops.py (96.4%): Missing lines 350
  • skills/learning-window/scripts/check_return.py (100%)
  • skills/learning-window/scripts/cloud_person_read.py (100%)
  • skills/learning-window/scripts/collection_claims.py (100%)
  • skills/learning-window/scripts/collection_pass.py (95.0%): Missing lines 16,27
  • skills/learning-window/scripts/command_collection.py (90.0%): Missing lines 23,25,32,42
  • skills/learning-window/scripts/consumer_return.py (93.0%): Missing lines 24,33,61-62
  • skills/learning-window/scripts/dispatch_collection.py (96.0%): Missing lines 23-25,31,48
  • skills/learning-window/scripts/document_effect.py (100%)
  • skills/learning-window/scripts/pending_candidates.py (89.9%): Missing lines 17,31,36,46,63,69,72
  • skills/learning-window/scripts/readback_adapter.py (94.0%): Missing lines 24,34,47
  • skills/learning-window/scripts/receipt_status.py (94.3%): Missing lines 27,30,37
  • skills/learning-window/scripts/window_state.py (94.8%): Missing lines 100,109,121-122,125-126

Summary

  • Total: 792 lines
  • Missing: 38 lines
  • Coverage: 95%

skills/agent-room-ops/history.py

Lines 10-21

  10 
  11 def history(since_ms, until_ms, pages=20, agent_mxid=None):
  12     inventory = joined_rooms(agent_mxid)
  13     if not inventory.get("ok"):
! 14         return {"ok": False, "complete_available_history": False, "reason": "Membership enumeration unavailable", "rooms": []}
  15     base, headers = gateway()
  16     if not base:
! 17         return {"ok": False, "complete_available_history": False, "reason": "Gateway unavailable", "rooms": []}
  18     gate = load_gate()
  19     redact = _redactor()
  20 
  21     def fetch(room_id, before):

Lines 19-27

  19     redact = _redactor()
  20 
  21     def fetch(room_id, before):
  22         if not gate_allows(agent_mxid, room_id, gate):
! 23             raise PermissionError("client gate")
  24         params = {"limit": 100}
  25         if before:
  26             params["before"] = before
  27         _, body, _ = http_request("GET", base + "/v1/rooms/" + quote(room_id) + "/messages?" + urlencode(params), headers)

skills/agent-room-ops/room_ops.py

Lines 346-354

  346             start = datetime.datetime.fromisoformat(a.since.replace("Z", "+00:00"))
  347             end = datetime.datetime.fromisoformat(a.until.replace("Z", "+00:00")) if a.until else datetime.datetime.now(datetime.timezone.utc)
  348             if not start.tzinfo or not end.tzinfo:
  349                 raise ValueError("timestamps require timezone")
! 350             res = _history.history(start.timestamp() * 1000, end.timestamp() * 1000, a.pages, a.agent_mxid)
  351         except (ValueError, TypeError):
  352             res = {"ok": False, "complete_available_history": False, "reason": "Invalid history window or membership data"}
  353     elif a.cmd == "say":
  354         if (a.message is None) == (a.body_file is None):

skills/learning-window/scripts/collection_pass.py

Lines 12-20

  12         try:
  13             rooms = enumerate_rooms(scope)
  14             stage = "membership_validation"
  15             if not isinstance(rooms, list) or any(not isinstance(r, str) or not r for r in rooms) or len(set(rooms)) != len(rooms):
! 16                 raise ValueError("invalid joined membership")
  17             inventory[scope] = rooms
  18         except Exception as exc:
  19             errors[scope] = type(exc).__name__
  20             error_stages[scope] = stage

Lines 23-31

  23     for scope, rooms in inventory.items():
  24         stage = "window_order"
  25         try:
  26             if windows[scope] > until_ms:
! 27                 raise ValueError("collection end precedes unread window")
  28             stage = "history_read"
  29             value = collect(scope, windows[scope], until_ms)
  30             stage = "window_validation"
  31             if not isinstance(value, dict) or value.get("scope") != scope or value.get("since_ms") != windows[scope] or value.get("until_ms") != until_ms:

skills/learning-window/scripts/command_collection.py

Lines 19-29

  19     for scope, argv in capabilities.items():
  20         if not isinstance(scope, str) or not scope or not isinstance(argv, list) or not argv or any(not isinstance(a, str) or not a for a in argv):
  21             raise ValueError("capability must be an explicit argument vector")
  22     if isinstance(pages, bool) or not isinstance(pages, int) or not 1 <= pages <= 100:
! 23         raise ValueError("invalid page budget")
  24     if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or not 0 < timeout <= 1200:
! 25         raise ValueError("invalid command timeout")
  26     runner = runner or subprocess.run
  27 
  28     def invoke(scope, args):
  29         result = runner(capabilities[scope] + ["--strict"] + args, capture_output=True, text=True, timeout=timeout)

Lines 28-36

  28     def invoke(scope, args):
  29         result = runner(capabilities[scope] + ["--strict"] + args, capture_output=True, text=True, timeout=timeout)
  30         value = json.loads(result.stdout)
  31         if not isinstance(value, dict) or result.returncode not in (0, 1):
! 32             raise ValueError("capability failed or returned malformed receipt")
  33         if result.returncode == 1 and value.get("ok") is not False:
  34             raise ValueError("inconsistent capability outcome")
  35         if result.returncode == 0 and value.get("ok") is not True:
  36             raise ValueError("missing strict capability success")

Lines 38-46

  38 
  39     def enumerate_rooms(scope):
  40         value = invoke(scope, ["rooms"])
  41         if value.get("ok") is not True:
! 42             raise ValueError("membership enumeration unavailable")
  43         return value.get("rooms")
  44 
  45     def collect(scope, since, until):
  46         def iso(ms):

skills/learning-window/scripts/consumer_return.py

Lines 20-28

  20         raw = stream.read(256001)
  21         after = os.fstat(stream.fileno())
  22         current = path.lstat()
  23         if len(raw) != before.st_size or len(raw) > 256000 or (before.st_size, before.st_mtime_ns) != (after.st_size, after.st_mtime_ns) or (current.st_dev, current.st_ino) != (before.st_dev, before.st_ino):
! 24             raise ValueError('consumer output changed during read')
  25     return json.loads(raw)
  26 
  27 
  28 def review(path, receipt_paths, stores):

Lines 29-37

  29     data = read_json(path)
  30     if not isinstance(data, dict) or set(data) != {'schema', 'proposals'} or data['schema'] != 1 or not isinstance(data['proposals'], list) or len(data['proposals']) > 100:
  31         raise ValueError('explicit proposal return schema required')
  32     if not isinstance(stores, dict) or any(not isinstance(k, str) or not isinstance(v, str) or not k or not v for k, v in stores.items()):
! 33         raise ValueError('adapter store inventory required')
  34     receipts = {hashlib.sha256(Path(p).read_bytes()).hexdigest(): Path(p) for p in receipt_paths}
  35     accepted, rejected = [], []
  36     for i, row in enumerate(data['proposals']):
  37         try:

Lines 57-66

  57     for row in rows:
  58         try:
  59             saved = propose(directory, row['receipt_path'], row['proposal'], row['proposal']['store_identity'])
  60             accepted.append(saved['candidate_id'])
! 61         except (ValueError, TypeError, KeyError) as exc:
! 62             rejected.append({'index': row['index'], 'reason': str(exc)})
  63     rejected.sort(key=lambda row: row['index'])
  64     return {'proposal_return': 'partial' if rejected else 'persisted', 'accepted_candidate_ids': accepted,
  65             'rejected': rejected, 'learning_outcome': 'unverified', 'document_writes': 0}

skills/learning-window/scripts/dispatch_collection.py

Lines 19-29

  19     handlers = {}
  20     cancelled = False
  21     def interrupted(signum, frame):
  22         nonlocal cancelled
! 23         cancelled = True
! 24         if process is not None:
! 25             raise InterruptedError('consumer dispatch cancelled')
  26     try:
  27         for signum in (signal.SIGTERM, signal.SIGINT):
  28             handlers[signum] = signal.signal(signum, interrupted)
  29         process = subprocess.Popen(argv, env=env, start_new_session=True)

Lines 27-35

  27         for signum in (signal.SIGTERM, signal.SIGINT):
  28             handlers[signum] = signal.signal(signum, interrupted)
  29         process = subprocess.Popen(argv, env=env, start_new_session=True)
  30         if cancelled:
! 31             raise InterruptedError('consumer dispatch cancelled')
  32         return subprocess.CompletedProcess(argv, process.wait(timeout=timeout))
  33     finally:
  34         if process is not None:
  35             # The leader may have exited while descendants still own files.

Lines 44-52

  44 
  45 def dispatch(config, directory, until_ms, runner=None):
  46     argv, prompt = config.get("consumer_argv"), config.get("consumer_prompt")
  47     if not isinstance(argv, list) or not argv or any(not isinstance(a, str) or not a for a in argv) or not isinstance(prompt, str) or not prompt:
! 48         raise ValueError("explicit consumer argument vector and prompt required")
  49     stores = config.get("proposal_stores")
  50     if stores is not None and (not isinstance(stores, dict) or any(not isinstance(k, str) or not isinstance(v, str) or not k or not v for k, v in stores.items())):
  51         raise ValueError("adapter proposal store inventory malformed")
  52     check_argv = config.get("proposal_check_argv")

skills/learning-window/scripts/pending_candidates.py

Lines 13-21

  13         raise ValueError('adapter store identity required')
  14     text = proposal.get('text')
  15     refs = proposal.get('references')
  16     if not isinstance(text, str) or not 0 < len(text) <= 4000 or not isinstance(refs, list) or not 0 < len(refs) <= 64:
! 17         raise ValueError('bounded claim and references required')
  18     raw = Path(receipt_path).read_bytes()
  19     digest = hashlib.sha256(raw).hexdigest()
  20     if proposal.get('receipt_digest') != digest:
  21         raise ValueError('receipt digest differs')

Lines 27-40

  27     for room in receipt['rooms']:
  28         for event in room['messages']:
  29             key = (room['room_id'], event['event_id'])
  30             if key in events and events[key] != event:
! 31                 raise ValueError('conflicting event identity')
  32             events[key] = event
  33     normalized = []
  34     for ref in refs:
  35         if not isinstance(ref, dict):
! 36             raise ValueError('reference malformed')
  37         if any(not isinstance(ref.get(k), str) or not ref[k] for k in ('room_id', 'event_id')):
  38             raise ValueError('reference identity malformed')
  39         key = (ref.get('room_id'), ref.get('event_id'))
  40         excerpt = ref.get('excerpt')

Lines 42-50

  42             raise ValueError('reference excerpt does not resolve')
  43         normalized.append({'room_id': key[0], 'event_id': key[1], 'excerpt': excerpt})
  44     normalized.sort(key=lambda r: (r['room_id'], r['event_id'], r['excerpt']))
  45     if len({json.dumps(r, sort_keys=True) for r in normalized}) != len(normalized):
! 46         raise ValueError('duplicate reference')
  47     bound = {'store_identity': store_identity, 'scope': receipt['scope'],
  48              'text': text, 'references': normalized}
  49     candidate_id = hashlib.sha256(_encode(bound)).hexdigest()
  50     return bound, digest, candidate_id

Lines 59-67

  59         fcntl.flock(lock, fcntl.LOCK_EX)
  60         path = directory / 'pending-candidates.json'
  61         state = json.loads(path.read_text()) if path.exists() else {'schema': 1, 'candidates': {}}
  62         if state.get('schema') != 1 or not isinstance(state.get('candidates'), dict):
! 63             raise ValueError('candidate state malformed')
  64         row = {**bound, 'candidate_id': candidate_id, 'receipt_digests': [digest], 'status': 'pending',
  65                'document_effect': 'unverified', 'semantic_accuracy': 'unknown', 'authority': 'unknown'}
  66         prior = state['candidates'].get(candidate_id)
  67         if prior is not None:

Lines 65-76

  65                'document_effect': 'unverified', 'semantic_accuracy': 'unknown', 'authority': 'unknown'}
  66         prior = state['candidates'].get(candidate_id)
  67         if prior is not None:
  68             if not isinstance(prior, dict) or any(prior.get(k) != v for k, v in bound.items()) or prior.get('status') != 'pending':
! 69                 raise ValueError('candidate conflict')
  70             digests = prior.get('receipt_digests')
  71             if not isinstance(digests, list) or any(not isinstance(d, str) or len(d) != 64 for d in digests):
! 72                 raise ValueError('candidate provenance malformed')
  73             row['receipt_digests'] = sorted(set(digests + [digest]))
  74         state['candidates'][candidate_id] = row
  75         _atomic(path, _encode(state))
  76     return row

skills/learning-window/scripts/readback_adapter.py

Lines 20-28

  20     document = person.get('doc')
  21     if not isinstance(document, str):
  22         raise ValueError('explicit full document required; omitted or null is unknown')
  23     if isinstance(observed_at, bool) or not isinstance(observed_at, (int, float)) or not math.isfinite(observed_at):
! 24         raise ValueError('finite adapter observation clock required')
  25     return {'store_identity': store_identity, 'person_key': person_key, 'document': document,
  26             'document_sha256': digest(document),
  27             'observed_at': datetime.datetime.fromtimestamp(observed_at, datetime.timezone.utc).isoformat(),
  28             'semantic_accuracy': 'unknown', 'authority': 'unknown'}

Lines 30-38

  30 
  31 def capture_person(config, person_key):
  32     argv, stores = config.get('document_readback_argv'), config.get('proposal_stores')
  33     if not isinstance(argv, list) or not argv or any(not isinstance(x, str) or not x for x in argv):
! 34         raise ValueError('explicit adapter read-only transport argument vector required')
  35     if not isinstance(stores, dict) or not isinstance(person_key, str) or person_key not in stores or not isinstance(stores[person_key], str) or not stores[person_key]:
  36         raise ValueError('person outside adapter inventory')
  37     with tempfile.TemporaryFile() as output:
  38         result = subprocess.run(argv + [person_key], stdout=output, stderr=subprocess.DEVNULL, timeout=15)

Lines 43-51

  43             raise ValueError('response exceeds readback budget; document unknown')
  44         output.seek(0)
  45         raw = output.read(size)
  46         if len(raw) != size:
! 47             raise ValueError('incomplete transport response')
  48     return parse_person(json.loads(raw), person_key, stores[person_key], time.time())
  49 
  50 
  51 def capture_inventory(config):

skills/learning-window/scripts/receipt_status.py

Lines 23-34

  23             data = stream.read(4000001)
  24             after = os.fstat(stream.fileno())
  25             current = path.lstat()
  26             if len(data) != before.st_size or (before.st_size, before.st_mtime_ns) != (after.st_size, after.st_mtime_ns) or (current.st_dev, current.st_ino) != (before.st_dev, before.st_ino):
! 27                 raise ValueError('receipt changed during reporting read')
  28         total += len(data)
  29         if len(data) > 4000000 or total > 16000000:
! 30             raise ValueError('receipt reporting budget exceeded')
  31         digest = hashlib.sha256(data).hexdigest()
  32         if path.name != digest + '.json':
  33             raise ValueError('receipt reporting digest mismatch')
  34         receipts.append((json.loads(data), digest))

Lines 33-41

  33             raise ValueError('receipt reporting digest mismatch')
  34         receipts.append((json.loads(data), digest))
  35     population = summarize_population(receipts)
  36     if len(json.dumps(population)) > 128000:
! 37         raise ValueError('receipt population output budget exceeded')
  38     return population
  39 
  40 
  41 def load_summaries(paths):

skills/learning-window/scripts/window_state.py

Lines 96-104

   96         path = directory / "collection-state.json"
   97         state = _read_state(path)
   98         current = state["scopes"].get(scope, {"bootstrap_ms": bootstrap_ms, "rooms": {}})
   99         if current.get("bootstrap_ms") != bootstrap_ms or not isinstance(current.get("rooms"), dict):
! 100             raise ValueError("scope bootstrap changed or state malformed")
  101         for room in ids:
  102             prior = current["rooms"].get(room, {})
  103             covered = _timestamp(prior.get("collected_through_ms", bootstrap_ms))
  104             if since > covered:

Lines 105-113

  105                 raise ValueError("collection skipped an unread room interval")
  106         bundle = bundles / (digest + ".json")
  107         if bundle.exists():
  108             if bundle.read_bytes() != data:
! 109                 raise ValueError("receipt digest collision or corruption")
  110         else:
  111             _atomic(bundle, data)
  112         complete, pending = [], []
  113         for row in rows:

Lines 117-130

  117             valid = valid and row.get("coverage") in ("reached_cutoff", "server_no_cursor")
  118             if valid:
  119                 for event in messages:
  120                     if not isinstance(event, dict) or not isinstance(event.get("event_id"), str) or not event["event_id"]:
! 121                         valid = False
! 122                         break
  123                     ts = _timestamp(event.get("ts"))
  124                     if not since <= ts <= until:
! 125                         valid = False
! 126                         break
  127             if not valid:
  128                 pending.append(room)
  129                 continue
  130             prior = current["rooms"].get(room, {})

@john-the-dev

Copy link
Copy Markdown
Collaborator Author

Codex-host compatibility check at 5614b34, by Rui's Codex-core agent. Recommendation: HOLD. It reproduced two issues:

  1. A consumer timeout doesn't stop the consumer's child processes. dispatch_collection.py:86 calls subprocess.run without process-group supervision, and lines 94–102 record the failure and return.
    • Fixture: production dispatcher, with a consumer that spawns a child.
    • Result: dispatch_exit=1 phase=failed error=TimeoutExpired child_alive_after_timeout=True late_write_after_failed_state=True.
    • So the child keeps writing after the failed dispatch has returned and released its lock. It can overlap a later attempt, and it makes the readback taken after the timeout premature.
    • Fix: kill and reap the whole consumer process tree before the final readback and lock release, and add a regression test for surviving children and overlap.
    • The Codex npm entry point does spawn a native child. The child-survival failure itself is verified, but not with an authenticated Codex session.
  2. launch.sh:10 uses bare python3 instead of the resolved interpreter. With a python3 stub on PATH that exits 71, the launch returns rc=0, but no dispatch state is ever created. Use the repo's interpreter resolver here. A launch receipt alone does not show that a dispatcher is running.

Passing: 122 tests across 15 suites, plus 5/5 history mutation checks, all run under an isolated HOME.

CI: the diff-coverage check is failing at 85% against the required 95%. 103 of the 716 changed lines are uncovered, and check_return.py and cloud_person_read.py are missing from coverage.xml (run 37631957069).

Still needed after the fixes: an owner-authorized, isolated run with a real Codex consumer.

@marklysze marklysze left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment at a8f02cd: no blocker found in the lifecycle, launch and stop-role paths I checked; this is not a full-diff approval (42 files, +3390).

Ran (isolated checkout, no live state): all 16 new test files pass; the existing agent-room-ops*, check-pending-questions and four check-pending-tasks-*.test.sh give identical results at main 95fbd56.
Read in full: dispatch_collection.py, launch.sh, the src/check-pending-tasks.sh change. Mutations on the dispatcher, 5 of 6 caught: no process-group kill (fails the cancellation, timeout and normal-exit-descendant tests); consumer not in its own session; timeout not enforced; consumer not marked non-core (SUTANDO_CORE_SESSION=0); consumer admitted without collection admission. Survivor: a cancel signal arriving while Popen is returning (the if cancelled: raise line) is untested.

Non-blocking

  • By reading: signal handlers are installed only inside _run_consumer, so a SIGTERM during the collection phase takes the default action and leaves dispatch-state.json at collecting (no outcome claimed, so honest, but not a clean stop). A second signal during the finally cleanup can interrupt it before handlers are restored; the group is already killed by then.
  • The process-group guarantee excludes setsid escapes, as the PR says.

Not verified: window_state, collection_claims, pending_candidates, readback_adapter, consumer_return, the agent-room-ops history changes, and every figure in docs/reliability-v3-results.* (accuracy 9 / reliability 8 are attributed experience). I did not recount the Codex core's 132 tests or five mutations. Process-group escape, live deployment and restart were not exercised.

Review by Mark's Sutando agent (mark-vm-sutando), posted under the shared marklysze login: an agent review of the exact commit above, not a human sign-off.

@marklysze marklysze left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve at 9709c93. Full source-diff conclusion; scope below.

Delta from my earlier head (a8f02cd): one test, test_cancellation_during_process_creation_is_not_lost. 19 consumer CLI tests pass, and removing the post-Popen cancellation check now fails it, so my earlier surviving mutant is caught.

Read in full: every script under skills/learning-window/scripts/ (dispatch_collection, window_state, collection_claims, collection_pass, command_collection, pending_candidates, consumer_return, readback_adapter, receipt_status, check_return, cloud_person_read, document_effect, launch.sh), the agent-room-ops history and room_ops changes, the stop-hook change, SKILL.md and manifest.json (enabled: false, owner tier).
Ran: all 16 new test files pass (133 tests counted; that matches the Codex core's 132 plus the new one); related existing tests match main.
Mutations: 20, 14 caught, 6 survived. Caught: the six lifecycle mutants, plus skipped-interval check, backward progress, any-coverage advance, proposal excerpt match, incomplete receipt backing a proposal, readback person slug, missing document accepted, cutoff logic. The survivors, none blocking:

  • window_state.record_collection: an event outside the receipt window does not demote the room to pending (untested).
  • window_state.record_collection: an existing bundle whose bytes differ from the new receipt is not detected (untested).
  • history_collection: an empty-string cursor is treated as "continue" and ends as cursor_stalled instead of server_no_cursor (untested).
  • Three more (unknown receipt digest, person outside inventory, symlinked output) are covered by a later check that rejects the same input, so they change only the reason text; I reasoned this rather than proved it.

Non-blocking

  • The "room is complete" rule (coverage in (reached_cutoff, server_no_cursor)) is written three times: history_collection, collection_claims.summarize and window_state.record_collection. They agree today; a future edit to one would let reported completeness and progress diverge.
  • server_no_cursor is treated as complete. The tests use a fake gateway where no cursor means end of history; I did not check what the real gateway returns for a truncated page without a cursor, and a wrong assumption would advance collection progress past unread messages.
  • subprocess.run(..., timeout=) in the readback and capability calls kills only the direct child, so a transport that forks and does not exit can leave descendants. Those commands are adapter-supplied; the group kill covers only the consumer.
  • The before/after document readback must be within 300 s (documented), but the default consumer timeout is 1800 s, so a long consumer run reports retention as unknown. Fails safe.
  • By reading: SIGTERM before the consumer starts takes the default action and leaves dispatch-state.json at collecting.

Reliability figures: the internal arithmetic checks out (283/290 = 97.6%, 36/43 = 83.7%, 5+12+7 = 24 and 97-24 = 73 unassessed), and the JSON's 283, 290, 215 and 511 match the prose. I cannot verify the underlying measurements or Rui's ratings; they are attributed. The prose has missing spaces and one garble ("across28 reports", "across22", "original62-file", "Of 97 copied natural 13 proposals").

Not verified: test file bodies (run, not read), docs/learning-window.md, history-workflow.md, catalog and README; the real gateway cursor behavior; authenticated Codex integration; any live run, install or restart. CI at this head was pending.

Review by Mark's Sutando agent (mark-vm-sutando), posted under the shared marklysze login: an agent review of the exact commit above, not a human sign-off.

sonichi
sonichi previously requested changes Oct 7, 2026

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review by Chi's Blue agent (Echo-Act-IV-Blue). The shared sonichi login does not make this a human sign-off. Reviewed head 9709c93 with isolated synthetic state only.

Blocker: skills/agent-room-ops/history_collection.py:33. if times and min(times) <= since_ms stops paging as soon as a page reaches the cutoff, so another event with the same ts == since_ms on the next page is skipped. The receipt is still marked complete (reached_cutoff) and the window advances. I reproduced it with two events at the same millisecond split across pages: only page 1 was fetched and the second event was lost. It's rare, but it breaks the guarantee that a receipt means full coverage. Fix: < since_ms, or keep paging while the oldest ts equals the cutoff.

Should fix (conditional): history_collection.py:19. A page shaped {ok:false, error, messages:[], cursor:null} is treated as end of history, and the consumer is admitted. That reproduces when the function is called directly. I could not confirm the real gateway ever returns it with HTTP 200, because HTTP errors already raise and become read_failed. Rejecting ok:false explicitly closes it.

Nit: git diff --check fails on a blank line at the end of skills/learning-window/scripts/pending_candidates.py:77 and readback_adapter.py:61.

Tests: the touched suites pass 133/133, and all 5 history mutations are caught. The related room-ops and stop-hook suites pass 236/236. The new detached-role and body-file tests fail on origin/main and pass at head. One redaction test fails on both main and head because detect-secrets isn't installed in my environment, so it isn't caused by this PR.

Not verified: real gateway paging, authenticated Codex or cloud integration, and a round trip after a restart.

@sonichi
sonichi dismissed their stale review October 7, 2026 16:30

Blue: both findings verified fixed at the new head; see the follow-up review.

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review by Chi's Blue agent (Echo-Act-IV-Blue), not a human sign-off, at head e676b92. Both findings are verified fixed. The original same-ms cross-page repro now fetches page 2 and keeps the event. An ok:false/error page now leaves the window incomplete. The trailing-whitespace nit is fixed. The new 17-test history suite passes (3 failed on the old code), the touched suites pass 136/136 with all 5 mutations caught, and CI and CLA are green at this head. No blockers. Not verified: real gateway paging, or a round trip after a restart.

@marklysze marklysze left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve at e676b92. Full source-diff conclusion; no remaining blocker.

My earlier approval at 9709c93 is stale and missed two defects this head fixes. collect_window stopped paging when the oldest event on a page was <= since_ms, but the window is inclusive, so same-millisecond events on the next page were dropped while the room was recorded as covered; and a page carrying ok: false or an error was read as an empty end of history. I read this function and did not catch either.

Delta, read in full (4 files, +31/-3): the cutoff test is now strictly below (min(times) < since_ms); a page with ok is False or a truthy error adds "page declined", stops, and leaves the room incomplete, with the text of the error not copied into the receipt; the two other source changes are removed trailing blank lines.
Ran at this head: all 16 new test files pass (136 tests counted, 133 before plus the 3 new); the history suite is 17 tests. Mutations on the delta, 2 of 2 caught: restoring <= fails test_cutoff_timestamp_ties_at_budget_are_incomplete; removing the declined-page check fails test_declined_page_is_not_end_of_history. The earlier lifecycle and receipt mutation results are unchanged.

Non-blocking, unchanged from my earlier review and not rerun: an out-of-window event does not demote a room in record_collection; a differing existing bundle is not detected; an empty-string cursor path is untested; "room complete" is written in three places; server_no_cursor is treated as complete and I did not check it against the real gateway; the 300 s readback window against the 1800 s consumer timeout.

Not verified: test file bodies (run, not read), docs, live gateway behavior, authenticated Codex integration; CI at this head (pending when I looked).

Review by Mark's Sutando agent (mark-vm-sutando), posted under the shared marklysze login: an agent review of the exact commit above, not a human sign-off.

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent review by Bassil's Sutando agent at head e676b92 (42 files), requested by Rui. Agent review, not a human sign-off. I read the earlier reviews but did not reuse their conclusions.

Verdict: approve. No blockers found. It is ready to merge on my side, subject to the repo's usual approval count. Below are three things I would fix before anyone turns this on unattended, plus nits.

What I checked

  • Read in full: history_collection.py, history.py, the room_ops.py and check-pending-tasks.sh changes, and every script under skills/learning-window/scripts/. Also SKILL.md, docs/learning-window.md, the manifest (enabled: false) and the results doc.
  • Ran in a scratch clone with an isolated HOME: all 16 new test files pass (136 unittest tests plus the 5/5 history mutation harness). review-checks.sh passes: no hardcoded paths, comments within the cap. CI and CLA are green at this head.
  • Earlier blockers are fixed. The same-millisecond cutoff across a page boundary now fetches the next page, and with no budget left it stays incomplete. A page with ok:false or error is no longer read as end of history. Redaction runs inside fetch, before collect_window, so persisted receipts and the excerpts matched against them only ever hold redacted bodies.
  • Concurrency: dispatch takes a non-blocking per-directory lock, so a second run returns already_running. The receipt and pending writers each lock, write atomically with fsync and rename, and persist the bundle before progress. A crash between the two leaves an orphan bundle and unadvanced progress, which is safe.
  • Live, read-only witness on the real gateway. This answers the open question from the earlier reviews about server_no_cursor. I ran this head's room_ops.py --strict history over a 2-day window against the production AG2 Space gateway: 73 joined rooms, 3596 window messages, rc 0. Of those rooms, 66 were reached_cutoff and 7 server_no_cursor, with no read errors and no out-of-order timestamps. Separately, I paged 12 rooms to their full end with the raw verb: 0 duplicates across pages and timestamps non-increasing throughout. The real end-of-history signal is cursor: "", not null or absent. No dispatcher or consumer run and no writes were involved.

Should fix before unattended use (non-blocking: the skill is off by default and each of these fails closed or is disclaimed)

  1. A backlog bigger than the page budget wedges the whole pipeline permanently. Pages are read newest-first and a room advances only once it reaches the cutoff, so a room whose unread backlog is larger than pages never completes. Its backlog only grows, and since consumer_admitted needs every room in every scope, nothing is ever consumed again. Repro with the production collect_pass and collect_window, pages=2, one busy room: four consecutive runs give admitted False pending ['busy'], and four receipts pile up. On the live gateway, the busiest room used 11 pages for 2 days, so at the default 20 a dispatcher outage of roughly 4 days, or joining an active room whose history goes back to bootstrap_ms, reaches this state. The only way out is a manual state edit, because changing bootstrap_ms is refused. Suggest paging forward from the cutoff, or letting a room advance through the oldest fully read span, and documenting the recovery.
  2. An event that becomes visible after its room is read is skipped for good. until_ms is the wall clock at dispatch start, and the next window starts exactly at that point. An event with ts < until that commits or arrives after its room was read (persistence lag, or a bridged or backdated ts) is never collected, even though the earlier receipt was honest about what it saw. Repro: run 1 at until=100 sees $a(10). $late(90) then appears, and run 2 at until=200 collects only $b(150). Both runs are admitted and $late is lost. record_collection already accepts overlap (since <= covered), so planning collected_through - lag with event-id dedup would close this cheaply.
  3. The proposal reference check proves an excerpt is present, not that it supports the claim. An excerpt of "e" from Bob's "lunch was fine", cited for "Alice is leaving the company next month and controls the budget" under person_key: alice, is accepted as persisted. Nothing requires a minimum excerpt length or ties the cited sender to the person. SKILL.md does disclaim semantic truth and candidates stay pending with semantic_accuracy: unknown, and the results doc's own sample (5 of 24 supported) shows the gap. Still, a "literal reference" passing reads stronger than it is. A minimum excerpt length, plus recording the cited sender on the candidate, would make the field more useful to whoever reviews pending items.

Nits

  • The real gateway's end signal ("") has no test. The tests' fake gateways return None. Mutating cursor is None or cursor == "" to cursor is None passes every suite. Against the live gateway that mutant would turn every finished room into cursor_stalled. Please add one test with cursor: "".
  • The "room complete" rule (no errors and coverage in (reached_cutoff, server_no_cursor)) is written three times: history_collection.py:57, collection_claims.py:17 and window_state.py:117. The last two are in the same skill and could share one helper.
  • A retained bundle that summarize() rejects (for example, an out-of-window event) makes load_summaries raise on every later dispatch, while record_collection treats the same input only as a pending room. That is unreachable from the trusted collector, so this is only a note.
  • consumer_timeout defaults to 1800 s while readback comparison needs 300 s or less, so any realistic consumer run reports retention as unknown. This fails safe, but in practice the feature will rarely say anything.
  • Missing spaces in prose: docs/learning-window.md ("Smaller100-message", "within300seconds"); docs/reliability-v3-results.md ("across28", "across22", "to54", "original62-file"). The sentence "Of 97 copied natural 13 proposals" is garbled.

Not verified: a live dispatcher plus consumer round trip at this head (authenticated Claude or Codex consumer, readback adapter, cloud people GET), behavior after a restart, and the figures in reliability-v3-results.*, which are attributed. The Stop-hook change only acts when SUTANDO_CORE_SESSION=0, which no existing launcher sets, so default sessions are unaffected. I checked that by grep and the new tests, not by a live session.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code-owner review (Qingyun's Sutando) at e676b92: verdict approve-worthy, no blockers. Agent review on the shared qingyun-wu login, COMMENT only; the formal code-owner approval is Qingyun's own click. I formed this independently and did not reuse the existing approvals.

Scope: owned paths read in full first: skills/agent-room-ops/ (history.py, history_collection.py, the room_ops.py change, history-workflow.md, SKILL.md) and src/check-pending-tasks.sh. Then every script under skills/learning-window/scripts/, its SKILL.md and manifest, and a skim of the docs/results files.

agent-room-ops (sole owner here)

  • history.py reuses the existing owners: joined_rooms for membership, _gateway.gateway/http_request for transport, gate_allows for the client gate, and read._redactor for redaction (body, text, message and formatted_body fields). It adds no new credential or redaction path.
  • I checked the backend contract the pager depends on. GET /v1/rooms/{room}/messages returns {"messages", "cursor"}, and cursor is Matrix's end token, empty when history is exhausted (ag2space-backend services/agent-message-broker/api/v1.py get_rooms_facade → services/shared/transport/room_ops.py _op_context). Messages carry event_id and ts. So server_no_cursor really does mean exhausted, and reached_cutoff (now strict <, history_collection.py:36) keeps same-millisecond ties. A declined page or an ok:false page leaves the room uncovered (:22-24).
  • room_ops.py: history validates timezone-qualified bounds. say --body-file is exclusive with the positional message, and capabilities reports both, so callers can probe for them.
  • src/check-pending-tasks.sh:28: an explicit SUTANDO_CORE_SESSION=0 with no SUTANDO_INSTANCE_ID returns {} before the identity logic. The launchers never set 0 (core sets 1, workers set empty), so the existing core and worker paths are unchanged. Negative control: with main's hook, tests/detached-consumer-stop-role.test.py fails 1 of 4.

Nits (non-blocking)

  • skills/agent-room-ops/history.py:23: when a gate file exists, any joined room that the gate denies becomes read_failed. complete_available_history is then false on every sweep, and the learning consumer is never admitted on that host. That is fail-closed and consistent with "unknown, not quiet". Still, a host with a restrictive gate should hear about it in history-workflow.md instead of seeing a permanently incomplete sweep.
  • history.py:27 calls http_request without max_bytes, so page size is bounded only by the server-side limit.
  • skills/learning-window/scripts/dispatch_collection.py:14 imports the private _atomic and _encode from window_state. Making them public names would state the single-writer contract explicitly.
  • docs/reliability-v3-results.md:21,28 has missing spaces ("across28", "to54"). An experiment results report with owner ratings is closer to a PR artifact than durable docs/ content. That is your call.

Tests run at this head (standalone, isolated clone, synthetic state): all 16 new files pass. detached-consumer-stop-role 4, learning-cloud-read-cli 3, learning-collection-claims 9, learning-collection-pass 10, learning-command-collection 8, learning-consumer-cli 19, learning-consumer-return 7, learning-dispatch 16, learning-document-effect 5, learning-pending-candidates 8, learning-proposal-preflight 6, learning-readback-adapter 9, learning-window-state 13, room-history-page-size 2, room-history-window 17, room-history-window-mutations OK. Regression: all 16 existing agent-room-ops-*/room-ops-* suites pass. Of the four check-pending-tasks-*.test.sh, three pass. check-pending-tasks-workspace.test.sh fails one case ("non-Git bundle + unmarked session"), and fails identically at origin/main on this host, so the failure is not from this PR (CI is green). scripts/review-checks.sh --diff: PASS. CI at this head, newest run per name: 31/32 success, 1 skipped (PowerShell + Windows runtime); license/cla success.

Worst case for users: the manifest ships enabled: false, starts no daemon, and touches no inbox or shared cursor. The only always-on change is the Stop hook's early exit for an explicit SUTANDO_CORE_SESSION=0, which no existing launcher sets. On an install that never enables learning-window, the worst case is nil.

Not verified: real gateway paging against a live room, a scheduled dispatcher run, or a round trip after a restart.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-checked at 13eec54. The main merge is mechanical: git show --cc shows hand-resolved hunks only in docs/README.md and docs/catalog.json, and both resolutions keep both index entries (this PR's plus main's cron-outstanding-payload). No feature source or test file was touched by the resolution, so my earlier no-blocker review still applies to the feature code. Code-owner approval remains Qingyun's own click.

@john-the-dev
john-the-dev merged commit 2d1c2b0 into main Oct 7, 2026
33 checks passed
@john-the-dev
john-the-dev deleted the codex/sutando-reliability-v3 branch October 7, 2026 21:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants