|
oc_chat
|
Classes | |
| class | _UserError |
| class | Finding |
| class | SessionRecord |
| class | Discovery |
Functions | |
| str | _excerpt (Any value) |
| Optional[Any] | _read_json (Path path) |
| str | _sha256 (Path path) |
| Optional[Path] | _latest_kind (Path session_dir, str kind) |
| str | _canonical_args (Any value) |
| Optional[datetime] | _parse_ts (Any value) |
| Optional[str] | _first_marker (str text, Sequence[str] markers) |
| tuple[str,...] | _plan_tokens (str plan_id) |
| bool | _name_references_plan (str name, str plan_id) |
| Optional[str] | _match_reason (str plan_id, str dir_name, Optional[dict] meta) |
| tuple[Path,...] | _event_dirs_for (str name, Optional[dict] meta, Path event_root) |
| SessionRecord | _record_for_dir (Path session_dir, str reason, Path event_root, Any meta=None, Optional[Path] meta_path=None) |
| Path | _resolve_explicit (str raw) |
| Discovery | discover_sessions (str plan_id, Path sessions_dir, Path event_root, Sequence[str] explicit=()) |
| Optional[dict] | _tool_payload (dict message) |
| Optional[Finding] | _outcome_finding (str session, str locus, str tool_name, dict payload, str raw) |
| list[Finding] | _warning_findings (str session, str locus, str tool_name, dict payload) |
| Optional[str] | _shell_workaround (str tool_name, Any arguments) |
| list[Finding] | _mine_chat_log (SessionRecord record, list messages) |
| list[Finding] | _retry_findings (str session, list[tuple[str, str, int]] sequence, str substrate) |
| str | _event_locus (Any event) |
| list[Finding] | _mine_events (SessionRecord record, list events, float gap_seconds) |
| list[Finding] | _gap_findings (str session, list events, float gap_seconds) |
| list[Finding] | mine_session (SessionRecord record, float gap_seconds=DEFAULT_GAP_SECONDS) |
| dict | build_report (str plan_id, Discovery discovery, float gap_seconds=DEFAULT_GAP_SECONDS) |
| str | render_json (dict report) |
| str | render_markdown (dict report) |
| None | _write_atomic (Path path, str text) |
| tuple[Path, Path] | emit (dict report, Path out_dir) |
| Path | default_out_dir () |
| argparse.ArgumentParser | _build_parser () |
| Path | _resolve_event_root (Optional[str] raw) |
| int | main (Optional[Sequence[str]] argv=None) |
Variables | |
| str | SCHEMA_VERSION = "postmortem/v1" |
| tuple | CATEGORIES |
| dict | CATEGORY_TITLES |
| PLAN_ID_RE = re.compile(r"^[0-9]{1,4}[a-z]?$") | |
| int | EXCERPT_MAX_CHARS = 200 |
| float | DEFAULT_GAP_SECONDS = 600.0 |
| int | _RETRY_RUN_MIN = 2 |
| _OK_OUTCOMES = frozenset({"ok", "success"}) | |
| _DENY_DECISIONS = frozenset({"deny", "denied", "reject", "rejected"}) | |
| tuple | _TRUNCATION_MARKERS |
| tuple | _DRIFT_MARKERS |
| tuple | _MISSING_TOOL_MARKERS |
| dict | _SHELL_WORKAROUND_COMMANDS |
The session miner — what actually happened in a plan's dogfood sessions.
Purpose
-------
Worker sessions produce friction evidence — errors, denials, retry loops,
truncations, workarounds — that evaporates when the session ends. This module
mines it mechanically, per the ratified design
``docs/architecture/AGENT_DISPATCH_IDENTITY_DESIGN_2026-08-26.md`` §4.7: it
reads the plan's oc_chat dogfood sessions (session-store content — turns, tool
calls and their outcomes — plus the event streams) and emits a per-plan
findings pair at ``docs/reports/postmortems/<NNN>-postmortem.md`` and
``<NNN>-postmortem.json``.
The miner never concludes and never files anything. Findings become backlog
items only through operator decision at the gate (§4.7, design decision D4);
nothing here edits roadmap source, a plan, or any document beyond its own two
output files.
Responsibilities
----------------
- :func:`discover_sessions` — resolve which sessions belong to the plan. An
explicit ``--session`` list takes precedence; otherwise the store is searched
for sessions whose directory name, ``session_name`` or ``active_plan_id``
references the plan id. What was searched and what matched is recorded in the
output, so a miss is legible rather than silent.
- :func:`mine_session` — extract findings in the eight pinned categories
(:data:`CATEGORIES`) from the chat log and the event stream.
- :func:`build_report` / :func:`render_json` / :func:`render_markdown` — the
two emitters. They carry identical findings and their totals agree by
construction: the markdown counts are read off the same ``by_category`` map
the JSON publishes.
- :func:`main` — the ``python -m oc_chat.scripts.session_postmortem <NNN>`` CLI.
Read-only contract (the plan-212 miner's rule)
----------------------------------------------
Every input is opened for reading only. The module deliberately does NOT use
:mod:`oc_chat.core.persistence`'s directory helpers (``_sessions_root``,
``_session_dir``, ``_archive_dir``) because those call ``_ensure_dir`` and so
bring a directory into being as a side effect of asking about it; the local
:func:`_latest_kind` mirrors ``_glob_latest`` without that. The ONLY writes are
the two postmortem files, each written via a temp file plus ``os.replace`` so a
crash mid-emit cannot leave a torn report — and cannot touch an input either
way.
Diagnostics
-----------
Domain: OC_CHAT.REPLAY
Shared with the offline replay reader deliberately: this is the same
read-only-analysis-over-the-event-substrate surface, and the domain table in
``.option_c/debug_config.json`` gates on exact literals — an unlisted domain
is silently threshold 0, and that file is outside this plan's scope.
Levels:
L2 — CLI invocation, discovery outcome, emit.
L3 — per-session substrate detail.
L4 — module-load coverage marker.
Contracts
---------
- Exit 0 on success including the empty case (no sessions found still emits an
honest postmortem naming what was searched); 2 on user error; 1 unexpected.
- Every category in :data:`CATEGORIES` appears in both outputs, zero included —
an empty section is information, not omission.
- Output carries no wall-clock stamp, so re-running over unchanged inputs
produces byte-identical files (a mined report is a function of its inputs).
- Findings are lenses, not a partition: one message can raise findings in more
than one category, but each (locus, signal) raises at most one finding, under
the precedence documented on each detector.
Dependencies
------------
- oc_diagnostics (structured debug logging via _dbg)
- oc_chat.core.event_bootstrap (_slug / _contained_alias_dir — reused rather
than copied because alias validation and root containment are security-
relevant path handling that must not drift)
- oc_chat.core.replay (read_events_from_dir — the tolerant NDJSON slot reader)
- oc_chat.core.persistence (DEFAULT_SESSIONS_DIR and the reserved dir names)
|
protected |
Build the ``session_postmortem`` argument parser.
Definition at line 1020 of file session_postmortem.py.
|
protected |
Return a stable string form of tool arguments for identity comparison.
Definition at line 333 of file session_postmortem.py.
|
protected |
Return the existing event directories for a session, in merge order. Mirrors ``event_bootstrap.event_read_dirs`` from the persisted metadata rather than from a live ``ChatSession``, because constructing one to ask a question about a stored session would be a write-capable object in a read-only tool.
Definition at line 402 of file session_postmortem.py.
|
protected |
Return a stable, readable locus string for one event.
Definition at line 718 of file session_postmortem.py.
|
protected |
Collapse *value* to one bounded line, verbatim apart from whitespace.
Definition at line 279 of file session_postmortem.py.
|
protected |
Return the first marker in *markers* present in *text*, else ``None``.
Definition at line 357 of file session_postmortem.py.
|
protected |
Return one finding per pair of consecutive events separated by blocked time.
Definition at line 798 of file session_postmortem.py.
|
protected |
Return the current ``*_{kind}.json`` in *session_dir*, or ``None``.
Mirrors ``persistence._glob_latest`` (most-recently-modified wins) WITHOUT
its raise-on-miss and, crucially, without the ``_ensure_dir`` side effect
the surrounding persistence path helpers carry.
Definition at line 319 of file session_postmortem.py.
|
protected |
Return why this session matches *plan_id*, or ``None`` when it does not.
Definition at line 383 of file session_postmortem.py.
|
protected |
Extract findings from one session's chat-log message list.
Definition at line 635 of file session_postmortem.py.
|
protected |
Extract findings from one session's ordered event stream.
Definition at line 724 of file session_postmortem.py.
|
protected |
True when *name* references *plan_id* as a token, not as a substring. ``2240`` and ``1224`` must not answer for plan ``224``, so the id has to sit on a token boundary — the separators a session name actually uses are hyphen, underscore, space and dot.
Definition at line 372 of file session_postmortem.py.
|
protected |
Return the ONE outcome-family finding for a tool result, if any. Precedence, most specific first: a missing dependency is a missing-tool workaround; an error or a non-zero return code is drift when its text names the anchor class of failure, otherwise a tool-call failure; a pending confirmation is a denial in the same sense the design means it (the call did not run).
Definition at line 559 of file session_postmortem.py.
|
protected |
Parse an RFC3339 timestamp; return ``None`` for anything unparseable.
Definition at line 346 of file session_postmortem.py.
|
protected |
Return the spellings of *plan_id* a session name or metadata may carry.
Definition at line 367 of file session_postmortem.py.
|
protected |
Read *path* as JSON, tolerating a BOM. Returns ``None`` when unreadable.
Definition at line 288 of file session_postmortem.py.
|
protected |
Build a :class:`SessionRecord` for an existing session directory. *meta* / *meta_path* let a caller that has already read the metadata (the store search does, to decide the match) hand it over rather than have the file opened a second time.
Definition at line 447 of file session_postmortem.py.
|
protected |
Resolve the event root: the flag, else the env override, else the default.
Definition at line 1039 of file session_postmortem.py.
|
protected |
Resolve one ``--session`` argument to a session directory. Accepts the directory itself or either of its two sidecar files, because a caller who has just been reading a ``*_meta.json`` should be able to paste that path.
Definition at line 478 of file session_postmortem.py.
|
protected |
Return one finding per maximal run of >= 2 identical consecutive calls.
Definition at line 696 of file session_postmortem.py.
|
protected |
Return the hex SHA-256 of *path*, or ``"unreadable"`` when it cannot be read.
Definition at line 305 of file session_postmortem.py.
|
protected |
Return the native tool a shelled-out command duplicates, else ``None``.
Definition at line 615 of file session_postmortem.py.
|
protected |
Return the parsed dict a ``role:"tool"`` message carries, if it is one.
Definition at line 547 of file session_postmortem.py.
|
protected |
Return the warning-family findings for a tool result. A declared ``warnings`` entry is one finding each; stderr text alongside a SUCCESSFUL command is the undeclared form of the same thing (a failing command's stderr is already reported by the outcome family, so counting it here too would double-report one event).
Definition at line 595 of file session_postmortem.py.
|
protected |
Write *text* to *path* via a temp file + ``os.replace``. A crash mid-emit must not leave a torn report. Inputs are untouched either way — the miner only ever writes here.
Definition at line 989 of file session_postmortem.py.
| dict oc_chat.scripts.session_postmortem.build_report | ( | str | plan_id, |
| Discovery | discovery, | ||
| float | gap_seconds = DEFAULT_GAP_SECONDS ) |
Mine every discovered session and assemble the report structure.
Definition at line 846 of file session_postmortem.py.
| Path oc_chat.scripts.session_postmortem.default_out_dir | ( | ) |
Return the committed postmortem home, resolved from this file's location.
Definition at line 1015 of file session_postmortem.py.
| Discovery oc_chat.scripts.session_postmortem.discover_sessions | ( | str | plan_id, |
| Path | sessions_dir, | ||
| Path | event_root, | ||
| Sequence[str] | explicit = () ) |
Resolve the sessions for *plan_id*, recording what was searched. An explicit list wins outright: the operator naming sessions is a stronger signal than any name heuristic, and honouring it silently alongside a store search would make the report's provenance ambiguous.
Definition at line 493 of file session_postmortem.py.
| tuple[Path, Path] oc_chat.scripts.session_postmortem.emit | ( | dict | report, |
| Path | out_dir ) |
Write the markdown + JSON pair for *report*; return both paths.
Definition at line 1003 of file session_postmortem.py.
| int oc_chat.scripts.session_postmortem.main | ( | Optional[Sequence[str]] | argv = None | ) |
Entry point. Returns the process exit code (0 ok, 1 error, 2 user error).
Definition at line 1047 of file session_postmortem.py.
| list[Finding] oc_chat.scripts.session_postmortem.mine_session | ( | SessionRecord | record, |
| float | gap_seconds = DEFAULT_GAP_SECONDS ) |
Mine one session across both substrates, in category-stable order.
Definition at line 819 of file session_postmortem.py.
| str oc_chat.scripts.session_postmortem.render_json | ( | dict | report | ) |
Render *report* as the JSON sidecar (stable key order, trailing newline).
Definition at line 900 of file session_postmortem.py.
| str oc_chat.scripts.session_postmortem.render_markdown | ( | dict | report | ) |
Render *report* as the markdown view of the SAME findings. Counts come from ``totals.by_category`` rather than from a second pass over the findings, so the two outputs cannot disagree by construction.
Definition at line 905 of file session_postmortem.py.
|
protected |
Definition at line 158 of file session_postmortem.py.
|
protected |
Definition at line 176 of file session_postmortem.py.
|
protected |
Definition at line 189 of file session_postmortem.py.
|
protected |
Definition at line 155 of file session_postmortem.py.
|
protected |
Definition at line 152 of file session_postmortem.py.
|
protected |
Definition at line 200 of file session_postmortem.py.
|
protected |
Definition at line 163 of file session_postmortem.py.
| tuple oc_chat.scripts.session_postmortem.CATEGORIES |
Definition at line 114 of file session_postmortem.py.
| dict oc_chat.scripts.session_postmortem.CATEGORY_TITLES |
Definition at line 126 of file session_postmortem.py.
| float oc_chat.scripts.session_postmortem.DEFAULT_GAP_SECONDS = 600.0 |
Definition at line 148 of file session_postmortem.py.
| int oc_chat.scripts.session_postmortem.EXCERPT_MAX_CHARS = 200 |
Definition at line 144 of file session_postmortem.py.
| oc_chat.scripts.session_postmortem.PLAN_ID_RE = re.compile(r"^[0-9]{1,4}[a-z]?$") |
Definition at line 140 of file session_postmortem.py.
| str oc_chat.scripts.session_postmortem.SCHEMA_VERSION = "postmortem/v1" |
Definition at line 109 of file session_postmortem.py.