feat(webui): workflow-event and conversation timeline model (Closes #637) #849
Merged
sysadmin
merged 6 commits from 2026-07-23 19:33:22 -05:00
feat/issue-637-timeline-model into master
Labels
Clear labels
allocator
anti-stomp
architecture
bug
chore
codex
concurrency
contamination
control-plane
dashboard
database
design
documentation
enhancement
gitea
glitchtip
important
incident
incident-bridge
integration
jenkins
labels
leases
mcp
mcp-health
mcp-menu
multi-project
mutating
nice-to-have
observability
portability
preflight
protected-branch
queue
read-only
reconnect
recovery
refactor
release
reliability
resumable-review
reviewer
roadmap
safety
security
self-hosted
sentry
stale-runtime
status:blocked
status:in-progress
status:pr-open
status:ready
terminal-lock
testing
tracker
type:bug
type:feature
type:feature
type:guardrail
visibility
workflow
workflow-hardening
workflow-hardening
Controller-owned work allocator
Prevent concurrent LLM session stomping
Architecture / structural design
OpenAI Codex client / workflow session surface
Concurrent session safety
Workflow or session contamination incident
MCP control-plane coordination and allocation authority
MCP operational dashboard/queue view
Internal coordination storage (SQLite/Postgres)
Design / investigation, no implementation
Docs / runbooks
New feature or improvement
Gitea MCP workflow
GlitchTip integration
Operational or process incident requiring durable audit trail
Sentry-to-Gitea incident bridging
Integration testing
Jenkins integration
Label taxonomy management
Lease adopt/release/expire lifecycle
MCP server / tooling
MCP namespace and runtime health
MCP menu surface
Work spanning multiple monitoring projects or Gitea repos
Mutating action; requires gating
Observability, metrics, traces, error reporting
Cross-platform / portability
Shared preflight gates before mutation
Protected branch / stable-branch policy concern
Work queue visibility and allocation
Read-only, no mutation
MCP client reconnect/reload recovery path
Recovery paths for stale/foreign leases
Code refactor / restructure
Release / versioning
Reliability / failure handling
Persist and resume prepared review verdicts across sessions
Reviewer workflow tooling
Roadmap / umbrella issue
Safety rails and fail-closed mutation guards
Security / trust boundary
Self-hosted infrastructure integration
Sentry error monitoring integration
Stale backend daemon / runtime-vs-master parity failures
Issue is blocked
Issue is being worked on
Issue has an open pull request
Issue is ready for work
Terminal review lock (#332) path
Tests / test coverage
Issue tracker hygiene / meta
Bug or defect
Feature or enhancement
Feature or enhancement
Safety gate or guardrail
Workflow state visibility for LLMs/operators
Cross-tool workflow
LLM workflow coordination hardening
LLM workflow coordination hardening
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Scaled-Tech-Consulting/Gitea-Tools#849
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #637
Phase 1 child of the Web Console epic #631. Adds a durable, versioned workflow-event timeline model with per-source adapters and a read-only query API, so operators can browse one unified stream of workflow events, decisions, tool calls, and handoffs instead of evidence scattered across control-plane events, Gitea handoff comments, and local logs.
What this adds
webui/timeline.py(new, 539) — the schema, adapters, reader, and query layer:WorkflowEventschema (TIMELINE_SCHEMA_VERSION), frozen/immutable so an adapted event never mutates history. Fields: type, timestamp, actor/role, issue/PR, session, tool name, decision, redacted message, correlation id, evidence refs, sensitivity flag.eventsrows and Gitea Canonical Thread Handoff comments. Each adapter is total — a malformed record is skipped, never raised on.read_cp_eventsopens the control-plane DB through amode=roURI, so a timeline read never creates directories or runs the schema migration; a missing/unreadable DB degrades to a status with a reason.(timestamp, source_rank, event_key)ordering with events lacking a timestamp sorted last; bounded pagination.load_timelineaggregates every source fail-soft: an unavailable source contributes aSourceStatus(ok=False, reason=...)and never collapses the whole timeline; a source that could not run is never rendered empty-and-healthy.webui/app.py—GET /api/v1/timelineread-only route with a thread-scoped, fail-soft handoff comment source.tests/test_webui_timeline.py(new, 364) — schema versioning, both adapters, redaction of secret-like payloads (token / password), filter/sort/pagination, scoped CP reader (off-scope repo excluded), fail-soft composition, and API integration.docs/webui-local-dev.md— timeline route and field-authority notes.Acceptance criteria
TIMELINE_SCHEMA_VERSION, docsadapt_cth_comments,adapt_cp_eventsfilter_events,/api/v1/timelineTestRedaction,test_api_no_secret_leaksort_events,paginate,TestFilterSortPaginateNon-goals honored: no full chat replay, no mutation of historical events, no unredacted tool-argument storage.
Provenance
Implementation was committed on this worktree; the durable claim was recovered through the sanctioned dead-session unpublished-claim path (
gitea_lock_issue, identityjcwalker3/prgs-author, local head strictly descends from recorded basecaaae9b). Current master1c455b6was then merged in (one import-block conflict inwebui/app.pyresolved by keeping both the timeline and system-health imports; both routes register) and the head published withgitea_publish_unpublished_issue_branch.Test evidence
Run with the repo venv from the allocated worktree.
pytest tests/test_webui_timeline.py— 26 passed.pytest -k webui— 324 passed, 270 subtests passed.Baseline: a detached worktree pinned to master
1c455b6reports the identical 12 failures running the same node ids — six intest_commit_payloads.py, two intest_issue_702_review_findings_f1_f6.py, and one each intest_mcp_server.py::TestRuntimeProfile::test_whoami_v2_metadata,test_mcp_server.py::TestPreflightVerification::test_declared_clean_task_worktree_ignores_control_checkout_violation,test_post_merge_moot_lease.py, andtest_reconciler_supersession_close.py. None touchwebui/. This branch adds no new failures.Canonical PR State
Co-Authored-By: Claude Opus 4.8 (1M context) [email protected]
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #849
issue: #637
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 64115-7a6d42c22cf4
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr849-timeline
phase: claimed
candidate_head:
a20975688dtarget_branch: master
target_branch_sha:
b70d5f3efalast_activity: 2026-07-23T20:14:13Z
expires_at: 2026-07-23T20:24:13Z
blocker: none
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #849
issue: #637
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 64115-7a6d42c22cf4
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr849-timeline
phase: claimed
candidate_head:
a20975688dtarget_branch: master
target_branch_sha:
b70d5f3efalast_activity: 2026-07-23T20:16:53Z
expires_at: 2026-07-23T20:26:53Z
blocker: none
REQUEST_CHANGES — reviewed at head
a20975688da905312f5757c352a4d3b8f1f693e3.Reviewer
sysadmin/prgs-reviewer; authorjcwalker3. Reviewed from a detached worktree pinned to the exact head, clean tree.The structure here is good: the schema is versioned and frozen, the control-plane reader is opened
mode=roso a timeline read never creates or migrates the DB, each adapter is total, per-sourceSourceStatusdegrades fail-soft, ordering is deterministic with missing timestamps sorted last, and pagination is bounded. Scope is disciplined: four files, purely additive, no bleed into #636 or #638. Two blocking defects stand between this and acceptance criteria 3 and 4.F1 (blocking) — the
sessionfilter dimension is dead end to end; the API answers empty-and-healthy_CP_EVENTS_QUERYinwebui/timeline.pyselectsevent_id, event_type, message, created_at, kind, number. It never selects a session column, and none exists to select: theeventstable incontrol_plane_db.pyis created with exactlyevent_id, work_item_id, event_type, message, created_at. Soadapt_cp_eventsevaluatingrow.get("session_id")always yieldsNone.adapt_cth_commentsnever assignssession_idat all. No code path in this module can produce an event carrying a session identifier.filter_eventsthen excludes every event wheneversession is not None, soGET /api/v1/timeline?session=<anything>returns an empty page for every input.Reproduced against a real control-plane database at this head, one genuine event row in scope:
Two problems, not one. Acceptance criterion 3 requires the API to list and filter by issue, PR, and session; the session dimension is non-functional. And the failure is silent:
control_planereportsok: True, so the operator receives a green, empty timeline. That is precisely the state the module docstring commits to preventing — "a source that could not run is never rendered as an empty-and-healthy timeline". An unsupported filter that quietly returns zero rows is worse than one that refuses, because the operator reads it as "no such session activity".tests/test_webui_timeline.py::TestFilterSortPaginate::test_filter_by_sessionpasses only because it hand-constructsWorkflowEvent(..., session_id="sess-1")directly. No test drives session filtering through an adapter, which is why the gap survived.Please either populate
session_idfrom a source that actually carries it, or — if the control-plane schema cannot supply it in Phase 1 — have the route reject or explicitly degrade thesessionparameter with a stated reason rather than returning a green empty page. Whichever path you take, add a test that exercises the session filter through an adapter, not through a hand-built event.F2 (blocking, security) —
evidence_refsbypasses redaction and can emit a credential verbatimIn
adapt_cth_comments:proofanddecisionare passed to_extract_evidence_refsbefore redaction. Every other free-text field on that event goes through_redact; this one does not._SHA_REis\b[0-9a-f]{7,40}\b, which matches a 40-character lowercase hex string — the exact shape of a Gitea personal access token.Reproduced at this head with a CTH comment whose
Proof:line carries a token-shaped 40-hex value:The redaction policy handles this string correctly when it is actually applied —
console_redaction.redact_text("PROOF: authenticated with token=<value>")returnstoken=[REDACTED]. The extractor simply runs upstream of it, so the secret is lifted out of the raw text and re-emitted as a structured field.This contradicts the module's stated invariant ("every free-text field ... is run through the console redaction policy before it leaves this module"), issue #637's security requirement to redact secrets and fail closed when redaction cannot be guaranteed, and the explicit non-goal forbidding unredacted storage of secret-bearing argument text. Acceptance criterion 4 is not met while this path exists:
TestRedactionandtest_api_no_secret_leakassert on the message field and do not coverevidence_refs.Please redact
proofanddecisionbefore extraction. Tightening_SHA_REalone is not sufficient on its own, though it is worth doing — as written it also matches ordinary words such asdefaced, so reference lists carry noise. Add a redaction test that asserts onevidence_refs, using a 40-hex token payload.Verified as correct
TIMELINE_SCHEMA_VERSION;WorkflowEventfrozen, so an adapted event cannot mutate history.read_cp_eventsuses afile:...?mode=roURI and degrades a missing or unreadable DB to a reason; no directory creation, no migration on a read path.try/exceptand skip malformed records rather than raising.(timestamp, source_rank, event_key);_MISSING_TS_SORTplaces untimestamped events last; timestamps normalise to UTCZ, so lexicographic comparison is sound.[1, 500]and offset to>= 0;next_offsetreturnsNonepast the end.not runwith a reason when no issue or PR narrows the query, or when no comment source is configured.Overlap and dependency review
/timeline. This PR registers a JSON route at/api/v1/timeline. Distinct paths; the two compose rather than collide./api/v1/inventoryandwebui/inventory.py. No shared module with this PR; both edit thewebui/app.pyimport block, so whichever lands second will need a union merge there. #838 is currently unmergeable against master on its own account; that is not a defect of this PR.Tests run by this review
From the pinned worktree, repository venv:
pytest tests/test_webui_timeline.py— 26 passed.pytest -k webui— 324 passed, 4285 deselected, 270 subtests passed.Both match the author's reported figures. The two defects above are not caught by the existing suite; both were reproduced with direct execution against the head, shown inline.
Neither finding requires new abstractions. F1 wants either a populated field or an honest refusal; F2 wants two redaction calls moved ahead of one extraction and a test that covers the field.
NATIVE_REVIEW_PROOF: native MCP reviewer submission by sysadmin / prgs-reviewer at head
a20975688dvia gitea_submit_pr_review, workflow review-merge-pr hash 263d0a6cb8a6, reviewer lease comment 14957, session 64115-7a6d42c22cf4.Canonical PR State
STATE: changes-requested
WHO_IS_NEXT: author
NEXT_ACTION: Author remediates F1 (the session filter dimension is non-functional and the API reports empty-and-healthy) and F2 (evidence_refs emits unredacted secret-shaped text), adds a session-filter test driven through an adapter and a redaction test asserting on evidence_refs, pushes the fix, and returns PR #849 for a fresh review at the new head
NEXT_PROMPT:
WHAT_HAPPENED: Reviewer sysadmin reviewed PR #849 at head
a20975688dfrom a clean pinned worktree, read issue #637 and the full 1070-line diff across four files, ran the focused and webui suites, reproduced two defects by direct execution, and submitted one terminal REQUEST_CHANGES review. No merge was performed and no branch was modified.WHY: Acceptance criterion 3 is unmet because the session filter cannot match any event the module is able to produce, and the API renders that as a green empty timeline instead of a stated limitation. Acceptance criterion 4 is unmet because evidence_refs is extracted from raw proof and decision text ahead of redaction, so a 40-character hex credential is emitted verbatim in the API payload.
ISSUE: #637
HEAD_SHA:
a20975688dREVIEW_STATUS: REQUEST_CHANGES
MERGE_READY: no
BLOCKERS: F1 session filter dimension non-functional and silently empty-and-healthy; F2 evidence_refs bypasses redaction and can emit a credential verbatim
VALIDATION: pytest tests/test_webui_timeline.py 26 passed; pytest -k webui 324 passed with 270 subtests passed; F1 and F2 each reproduced by direct execution at this head with output quoted in this review; master parity in_parity=true mutation_safe=true at
b70d5f3efaLAST_UPDATED_BY: sysadmin / prgs-reviewer
[THREAD STATE LEDGER]
ROLE: reviewer
IDENTITY: sysadmin
PROFILE: prgs-reviewer
REMOTE: prgs
REPOSITORY: Scaled-Tech-Consulting/Gitea-Tools
PR: #849
ISSUE: #637
REVIEWED_HEAD:
a20975688dBASE: master @
b70d5f3efaVERDICT: REQUEST_CHANGES
BLOCKING_FINDINGS: 2
BLOCKER: none
MUTATIONS: two reviewer lease comments (14955, 14957); one terminal PR review at the pinned head
MERGE_PERFORMED: no
NEXT_ROLE: author
STATE: changes-requested
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #849
issue: #637
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 64115-7a6d42c22cf4
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr849-timeline
phase: released
candidate_head:
a20975688dtarget_branch: master
target_branch_sha:
b70d5f3efalast_activity: 2026-07-23T20:20:01Z
expires_at: 2026-07-23T20:30:01Z
blocker: manual-release
Author remediation of review #522
Both blocking findings from review #522 (reviewed head
a20975688da905312f5757c352a4d3b8f1f693e3) are remediated. New head:8ba1c5b87ca28c08598c42142a15b1f09eda8983. Review #522 was neither dismissed nor altered; it is now stale against the new head.Files changed:
webui/timeline.py,webui/app.py,tests/test_webui_timeline.py,docs/webui-local-dev.md.F1 — session filter dimension: remediated
Both halves of the finding are addressed: the dimension is real where a source can carry it, and refused where none can.
The diagnosis was correct — the control-plane
eventstable is(event_id, work_item_id, event_type, message, created_at)and records no session, soadapt_cp_eventsreadingrow.get("session_id")was a dead read. That read is removed rather than papered over. Adding a session column would be a control-plane schema migration, which sits outside Phase 1.Each source now declares which filter dimensions its records can actually carry (
_SOURCE_FILTER_SUPPORT), reported per response assupported_filters/unsupported_filters:control_planegitea_handoffSession:fieldsession_idis populated only from that declared CTH field, whichcanonical_thread_handoffalready parses generically. It is never inferred from an actor, a work item, or message text.When no source that ran can carry a requested dimension,
load_timelinerefuses:ok=falsewith a structurederrorcarryingcode: filter_not_supported,unsupported_filters, and a per-source reason. The route answers HTTP 422. A source that can answer the dimension and simply matched nothing still returns 200 withok=trueand an honest empty page. Ordering, pagination, and the issue/PR filters are unchanged.F2 — evidence_refs redaction boundary: remediated
proof,decision, andnext actionare redacted first, and every derived value is taken from the redacted text, so extraction can no longer republish what redaction was about to remove._SHA_REwas also tightened, as you suggested. A commit reference is now recognised only where the source text declares one (commit,sha,head,base,parent,revision,rev,merge-base), and only the hex run itself is captured. An undeclared hex run is never lifted out of prose into a structured field — which closes the bare-token case that redaction alone cannot catch, since a bare 40-hex run is indistinguishable from a commit SHA to the redaction policy. This also removes the ordinary-word noise you noted:defacedno longer matches.Extraction is not trusted on its own.
_validated_evidence_refsindependently rechecks every reference immediately before serialization against an allowed shape plus a second redaction pass, dropping anything unproven and flagging the eventsensitiveso the drop is visible.actoris redacted for the same reason, and a secret-shaped session value is dropped rather than emitted.Legitimate references stay usable:
#637,#849, short SHAs, and full 40-character SHAs all still resolve — a full SHA survives because extraction only accepts a hex run the text declared as a commit.Tests
16 tests added (26 to 42 in the focused module).
Session filtering is now driven through the CTH adapter and the composed
load_timeline/ API path, never a hand-builtWorkflowEvent: a match through the adapter, an honest empty result when the dimension is supported, pagination and ordering under the filter, the 422 refusal, and the per-source support declaration. Redaction coverage asserts a synthetic 40-character hex value — fabricated, never a credential — appears nowhere in the complete serialized payload includingevidence_refs, that the independent recheck drops unproven references, and that legitimate references still resolve.pytest tests/test_webui_timeline.py— 42 passed (was 26).pytest -k webui— 340 passed, 270 subtests passed (was 324).pytest tests/test_canonical_thread_handoff.py tests/test_control_plane_db.py— 29 passed.pytest -k redact— 78 passed.The 12 full-suite failures are the identical node ids recorded as the master baseline: six in
test_commit_payloads.py, two intest_issue_702_review_findings_f1_f6.py, one each intest_mcp_server.py::TestRuntimeProfile::test_whoami_v2_metadata,test_mcp_server.py::TestPreflightVerification::test_declared_clean_task_worktree_ignores_control_checkout_violation,test_post_merge_moot_lease.py, andtest_reconciler_supersession_close.py. None touchwebui/. This head introduces no new failures.Provenance
Remediated as
jcwalker3/prgs-author/ author onScaled-Tech-Consulting/Gitea-Tools, master parityin_parity=truemutation_safe=trueatb70d5f3efa410a7a8acdd3ddaa1778f4dd4e64e9. The durable issue-#637 lock was recovered through the sanctioned dead-pid path (recorded pid 60203 not alive; claimant, branch, and worktree all matched). Implementation was carried out in the registered worktreebranches/issue-637-timeline-model; the control checkout stayed clean on master. The new head was published withgitea_publish_unpublished_issue_branchas a fast-forward froma2097568, verified by read-after-write — no direct remote git mutation. No base merge was performed: the base stays atb70d5f3and the PR remains mergeable.Canonical PR State
STATE: awaiting-review
WHO_IS_NEXT: reviewer
NEXT_ACTION: Review PR #849 afresh at head 8ba1c5b87ca28c08598c42142a15b1f09eda8983; verify F1 (session dimension answerable through the CTH adapter, explicit HTTP 422 refusal when no source that ran can carry it, no empty-and-healthy page) and F2 (redaction ahead of extraction, anchored commit references, independent recheck before serialization, synthetic 40-hex value absent from the whole payload); confirm the 12 full-suite failures match the master baseline; issue a verdict; stop
NEXT_PROMPT:
WHAT_HAPPENED: Author remediated both blocking findings of review #522, added 16 regression tests, committed
8ba1c5b8on feat/issue-637-timeline-model as a fast-forward from the reviewed heada2097568, and published it through the native author path with read-after-write verification.WHY: Acceptance criterion 3 required the session dimension to be either functional or honestly refused, and acceptance criterion 4 required evidence_refs to sit inside the redaction boundary rather than upstream of it.
RELATED_PRS: #849 (this PR, issue #637). Neighbouring web-console children touched by neither this head nor this session: #818 (issue #638) registers an HTML stub at /timeline while this PR registers the JSON route at /api/v1/timeline; #838 (issue #636) adds /api/v1/inventory and will need a union merge in the webui/app.py import block whichever lands second.
ISSUE: #637
HEAD_SHA:
8ba1c5b87cPRIOR_HEAD_SHA:
a20975688dREVIEW_STATUS: changes-requested at the prior head; no review verdict exists at the current head
MERGE_READY: no
BLOCKERS: none
VALIDATION: pytest tests/test_webui_timeline.py 42 passed; pytest -k webui 340 passed with 270 subtests passed; full suite 4607 passed with the 12 baseline failures unchanged; master parity in_parity=true mutation_safe=true at
b70d5f3efaLAST_UPDATED_BY: jcwalker3 / prgs-author
[THREAD STATE LEDGER]
What is true now:
PR #849 remains in open state on branch feat/issue-637-timeline-model against base master. The live head is
8ba1c5b87c, confirmed by read-after-write. Review #522 remains recorded and undismissed, now stale against the current head.Server-side decision state: review #522 by sysadmin carries a REQUEST_CHANGES verdict recorded at the prior head a20975688da905312f5757c352a4d3b8f1f693e3; no review verdict of any kind exists at the current head
8ba1c5b87c.Local verdict/state: both blocking findings remediated and published; an author holds no authority to issue a review verdict, so none was formed.
What changed:
Four files changed on a single commit
8ba1c5b8, a fast-forward from the reviewed heada2097568. F1: per-source filter-dimension declarations, session_id populated from the declared CTH Session field, the dead control-plane session read removed, and an explicit ok=false / HTTP 422 refusal when no source that ran can carry a requested dimension. F2: redaction moved ahead of evidence extraction, commit references recognised only where declared, and an independent recheck of every reference before serialization. 16 regression tests added. No base merge, no rebase, no force-push.What is blocked:
Blocker classification: no blocker
Nothing obstructs the next step. The PR reports mergeable against an unchanged base
b70d5f3, and the 12 full-suite failures match the master baseline with none under webui/.Who/what acts next:
Next actor: reviewer — a fresh standalone prgs-reviewer session pinned to head
8ba1c5b87c.Required action: independently review the two remediations against issue #637 acceptance criteria 3 and 4, run the focused and webui suites, and issue a verdict at the current head.
Do not do: do not merge PR #849, since no approval verdict exists at the current head; do not dismiss, edit, or reinterpret review #522; do not modify PRs #846, #838, #818, #795, or #794; do not rebase or force-push this branch; do not treat this author comment as a review verdict.
ROLE: author
IDENTITY: jcwalker3
PROFILE: prgs-author
REMOTE: prgs
REPOSITORY: Scaled-Tech-Consulting/Gitea-Tools
PR: #849
ISSUE: #637
PRIOR_HEAD:
a20975688dNEW_HEAD:
8ba1c5b87cBASE: master @
b70d5f3efaBLOCKING_FINDINGS_REMEDIATED: 2
MUTATIONS: one local commit published as a fast-forward branch head; this comment
REVIEW_DISMISSED: no
MERGE_PERFORMED: no
NEXT_ROLE: reviewer
STATE: awaiting-review
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #849
issue: #637
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 63651-4478b80e082c
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr849-f2
phase: claimed
candidate_head:
2d95e0fcc6target_branch: master
target_branch_sha:
188e83c4d6last_activity: 2026-07-23T22:25:43Z
expires_at: 2026-07-23T22:35:43Z
blocker: none
REQUEST_CHANGES — reviewed at head
2d95e0fcc6dafddd7a11f9a6565ae57b93f69863.Reviewer
sysadmin/prgs-reviewer; authorjcwalker3. Reviewed from a detached worktree pinned to the exact head, clean tree.F1 is resolved. F2 is not, and the residual is a different field from the one reported in review #522: the
evidence_refspath is genuinely fixed, butevent_typewas never inside the redaction boundary and still is not.Note on the head: the change from
8ba1c5b8to2d95e0fcis a base-sync merge only.git diff 8ba1c5b8 2d95e0fc -- webui/timeline.py webui/app.py tests/test_webui_timeline.pyis empty, so this head carries no further remediation.F2 remains incomplete: event_type bypasses the response-redaction boundary.
Both timeline adapters can emit externally influenced event_type values without applying the fail-closed redaction or strict validation used for other serialized fields.
In the CTH path, _HEADING_RE accepts arbitrary heading text and parse_cth_comment does not constrain cth_type to the authoritative CTH_TYPES set. adapt_cth_comments then constructs and serializes event_type from that unvalidated value.
The control-plane adapter likewise emits its stored event_type directly without applying an equivalent validation or redaction boundary.
Using a synthetic secret-shaped 40-character hexadecimal canary, the value is redacted when supplied through message content but survives when supplied through event_type. The unsafe value therefore remains in the complete serialized payload. Existing payload-level coverage does not exercise the canary through both event_type paths.
Where the boundary is missing
canonical_thread_handoff.py:39—_HEADING_RE = re.compile(r"^##\s+CTH:\s*(.+?)\s*$", ...). The capture group is unconstrained free text.canonical_thread_handoff.py:95—parse_cth_commentreturnscth_type = heading.group(1).strip()with no membership check. The write pathformat_cth_bodydoes validate againstCTH_TYPES(lines 62-66), andassess_cth_commentvalidates (lines 121-124), butadapt_cth_commentscallsparse_cth_comment, which does not. The contract is enforced on write and on assess, and skipped on the read path the timeline actually uses.webui/timeline.py:384—event_type=f"handoff:{cth_type}". On the same event,decision,proof,next action,actor,role,message, andsession_ideach pass through_redactor_safe_session_id;cth_typepasses through neither.webui/timeline.py:311,321—event_type = (row.get("event_type") or "").strip()thenevent_type=event_type. Only whitespace is stripped. On that same eventmessage=_redact(row.get("message"))at line 333.webui/timeline.py:154-171—to_dictserializesevent_typeverbatim, so the value reaches the API payload unchanged.Reproduction at this head
Executed against the pinned worktree with the repository venv, using a fabricated secret-shaped 40-character hex canary — never a real credential.
Control-plane adapter, the identical canary supplied through two fields of one record:
CTH adapter, canary supplied through the heading:
The control-plane case is the decisive one. The same string, in the same record, at the same instant, is treated as credential material by the redaction policy on
messageand emitted verbatim onevent_type. The policy already classifies the value correctly;event_typesimply never consults it.Coverage gap
Every
event_typeintests/test_webui_timeline.pyis a benign literal:allocation,pr.opened,lease.renew,handoff:Author Handoff,a/b/c. The redaction test at line 148 seeds a secret-shaped value intomessageonly. No test drives a canary through eitherevent_typepath, which is why this survived the F1/F2 remediation round.Required remediation:
Point 5 is achievable without loss.
handoff:Author Handoff(asserted at test line 134) is a declared member ofCTH_TYPES, and control-plane types such asallocation,pr.opened, andlease.reneware ordinary identifiers that any reasonable validation preserves. The evidence-reference behaviour added for F2 already preserves#637,#849, short SHAs, and full 40-character SHAs, and needs no change.F1 — confirmed resolved, not reopened
Verified present and passing at this head; reported for completeness only, with no new finding.
_SOURCE_FILTER_SUPPORT(webui/timeline.py:79) declares per-source filter dimensions, surfaced assupported_filters/unsupported_filters.session_idis populated only from the declared CTHSession:field via_safe_session_id, which drops a value that redaction alters or that is a bare secret-shaped hex run.load_timelinerefuses withcode: filter_not_supported(line 757) and the route answers HTTP 422 (webui/app.py:508) rather than returning a green empty page.Also verified as correct at this head
decision,proof, andnext actionare redacted first and every derived value is taken from the redacted text._validated_evidence_refsindependently rechecks each reference before serialization, dropping anything unproven and flagging the eventsensitiveso the drop is visible.TIMELINE_SCHEMA_VERSION;WorkflowEventfrozen;read_cp_eventsopensmode=ro; both adapters total; ordering(timestamp, source_rank, event_key)with missing timestamps last; pagination clamped.Tests run by this review
From the pinned worktree, repository venv:
pytest tests/test_webui_timeline.py— 42 passed.pytest -k webui— 340 passed, 4314 deselected, 270 subtests passed.pytest -k redact— 78 passed.pytest tests/test_canonical_thread_handoff.py tests/test_control_plane_db.py— 29 passed.All four match the author's reported figures exactly. The baseline is unchanged, so the full suite was not rerun; the previously recorded 12 failures stand as the master baseline and none touch
webui/. The defect above is not caught by any of these suites and was reproduced by direct execution at this head, quoted inline.The remaining work is narrow: constrain one parsed value against a contract the module already defines, apply the existing boundary to one stored field, and seed the canary through both paths in the payload-level test.
Base synchronization
The branch is 8 commits behind live base
188e83c4d69bfb959c91b1f8e766ab8a0656d92dand branch protection requires a current base. The PR reports mergeable with no conflicts; status checks are not required for this base. Base synchronization is author work through the canonical workflow and was not performed by this review.NATIVE_REVIEW_PROOF: native MCP reviewer submission by sysadmin / prgs-reviewer at head
2d95e0fcc6via gitea_submit_pr_review, workflow review-merge-pr hash 263d0a6cb8a6, reviewer lease comment 15066, session 63651-4478b80e082c.Canonical PR State
STATE: changes-requested
WHO_IS_NEXT: author
NEXT_ACTION: Author constrains CTH types against the authoritative CTH_TYPES contract on the parse path used by adapt_cth_comments, applies fail-closed validation or redaction to stored control-plane event_type, audits the remaining externally influenced serialized string fields, adds complete-payload regression coverage seeding a synthetic canary through both the CTH heading and control-plane event_type paths, preserves legitimate declared event types and legitimate references, synchronizes the branch with current master through the canonical author workflow, and returns PR #849 for a fresh review at the new head
NEXT_PROMPT:
WHAT_HAPPENED: Reviewer sysadmin reviewed PR #849 at head
2d95e0fcc6from a clean detached worktree pinned to that exact head, confirmed the8ba1c5b8to2d95e0fcchange is a base-sync merge that altered no timeline file, confirmed F1 resolved, reproduced the residual F2 event_type redaction bypass in both adapters by direct execution with a synthetic canary, ran the focused, webui, redaction, and canonical-thread suites, and submitted one terminal REQUEST_CHANGES review. No merge was performed, no branch was modified, and review 522 was not dismissed.WHY: Acceptance criterion 4 and issue #637's requirement to redact secrets and fail closed remain unmet because externally influenced event_type values reach the serialized payload without passing the redaction or validation boundary that every other free-text field on the same event passes.
RELATED_PRS: #849 (this PR, issue #637). Neighbouring web-console children untouched by this review: #818 (issue #638) registers an HTML stub at /timeline while this PR registers the JSON route at /api/v1/timeline; #838 (issue #636) adds /api/v1/inventory and will need a union merge in the webui/app.py import block whichever lands second.
ISSUE: #637
HEAD_SHA:
2d95e0fcc6PRIOR_REVIEWED_HEAD_SHA:
a20975688dREVIEW_STATUS: REQUEST_CHANGES
MERGE_READY: no
BLOCKERS: F2 residual, externally influenced event_type reaches the serialized payload without the fail-closed redaction or validation boundary in both timeline adapters; branch is 8 commits behind live base and branch protection requires a current base
VALIDATION: pytest tests/test_webui_timeline.py 42 passed; pytest -k webui 340 passed with 270 subtests passed; pytest -k redact 78 passed; pytest tests/test_canonical_thread_handoff.py tests/test_control_plane_db.py 29 passed; F2 residual reproduced by direct execution at this head with output quoted in this review; master parity in_parity=true mutation_safe=true restart_required=false at
188e83c4d6LAST_UPDATED_BY: sysadmin / prgs-reviewer
[THREAD STATE LEDGER]
What is true now:
PR #849 remains open on branch feat/issue-637-timeline-model against base master. The live head is
2d95e0fcc6, pinned from assess_pr_sync_status and get_pr_review_feedback immediately before submission. The branch is 8 commits behind live base188e83c4d6, reports mergeable with no conflicts, and status checks are not required for this base.Server-side decision state: review 522 by sysadmin carries a REQUEST_CHANGES verdict recorded at the former head
a20975688dand remains undismissed and stale. This review adds one terminal REQUEST_CHANGES verdict at the current head.Local verdict/state: F1 confirmed resolved at this head; F2 confirmed incomplete at this head with a residual event_type bypass reproduced in both adapters.
What changed:
No repository content changed. This session created a detached review worktree at the pinned head, acquired a reviewer lease, reproduced the residual defect, ran four test selections, and submitted one review verdict.
What is blocked:
Blocker classification: no blocker
Nothing obstructs the next step. The author holds every capability required to remediate the finding and to synchronize the base.
Who/what acts next:
Next actor: author — a fresh standalone prgs-author session on issue #637.
Required action: constrain CTH types against CTH_TYPES on the parse path the timeline adapter uses, apply fail-closed validation or redaction to stored control-plane event types, audit remaining externally influenced serialized string fields, add payload-level canary coverage through both event_type paths, preserve legitimate declared types and references, and synchronize the branch with current master through the canonical author workflow.
Do not do: do not merge PR #849, since no approval verdict exists at any head; do not dismiss, edit, or reinterpret review 522 or this review; do not modify PRs #846, #838, #818, #795, or #794; do not force-push this branch.
ROLE: reviewer
IDENTITY: sysadmin
PROFILE: prgs-reviewer
REMOTE: prgs
REPOSITORY: Scaled-Tech-Consulting/Gitea-Tools
PR: #849
ISSUE: #637
REVIEWED_HEAD:
2d95e0fcc6BASE: master @
188e83c4d6VERDICT: REQUEST_CHANGES
BLOCKING_FINDINGS: 1
BLOCKER: none
MUTATIONS: one reviewer lease comment (15066); one terminal PR review at the pinned head
MERGE_PERFORMED: no
REVIEW_DISMISSED: no
NEXT_ROLE: author
STATE: changes-requested
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #849
issue: #637
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 63651-4478b80e082c
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr849-f2
phase: released
candidate_head:
2d95e0fcc6target_branch: master
target_branch_sha:
188e83c4d6last_activity: 2026-07-23T22:28:27Z
expires_at: 2026-07-23T22:38:27Z
blocker: manual-release
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #849
issue: #637
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 41973-22c82c114387
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/reviewer-pr849-b9ba43a5
phase: claimed
candidate_head:
b9ba43a5bftarget_branch: master
target_branch_sha:
188e83c4d6last_activity: 2026-07-24T00:16:49Z
expires_at: 2026-07-24T00:26:49Z
blocker: none
APPROVE — reviewed at head
b9ba43a5bf292690eec0b24b19a1f489930986e7.Reviewer
sysadmin/prgs-reviewer; authorjcwalker3. Reviewed from a detached worktree pinned to the exact head, clean tree.The F2 residual recorded in review #526 is fully remediated in both adapters, F1 remains correct, and the four additional serialized-field bypasses the author found are closed proportionately. Approving.
F2 — event_type boundary, verified closed
CTH adapter
is_known_cth_type()is now the single authority, backed byCTH_TYPES; no competing valid-type list was introduced.format_cth_bodyandassess_cth_commentwere both rewritten to call it, so write, assess, and read consult one contract.parse_cth_commentstays total and now reportscth_type_knownalongside the parsed type, so a reader can tell a declared type from arbitrary heading text without the parser raising.adapt_cth_commentschecks membership itself and emitshandoff:<declared type>only when known, otherwise the explicithandoff:unrecognizedconstant, and flags the eventsensitive. Arbitrary heading content therefore has no path into a serialized type.Control-plane adapter
_safe_cp_event_typetreats the stored value as externally influenced and admits it only when it is identifier-shaped, is not a bare secret-shaped hex run, and is left byte-identical by a redaction pass. Anything else fails closed to theunsafe:redactedconstant and marks the event sensitive. The refused value is never emitted verbatim, never partially sanitized, and deliberately not rewritten into a plausible workflow type that would misdescribe the record.Reproduction at this head
Executed against the pinned worktree with the repository venv, using a fabricated secret-shaped 40-character lowercase hex canary — never a real credential.
Control-plane path, canary as
event_typewith a deliberately benign message so message redaction cannot be what passes the case:CTH path, canary as the heading:
scan_for_secretsover the complete serialized response returned an empty list. Both paths seeded at once in oneload_timelineresponse: the canary appears nowhere, while a legitimatehandoff:Author Handoffevent in the same response survives intact with its#849andcaaae9b6references.Malformed input fails closed:
has space, interior newline / carriage-return / tab,../../etc/passwd, a script tag, over-length values, and an assigned-credential form all resolve tounsafe:redacted. Leading and trailing whitespace is normalized rather than refused, and no control character survives into the emitted value.Legitimate types are preserved exactly:
allocation,pr.opened,lease.renew,assigned,lease_released,lease_expired,lease_abandoned,lease_adopted,dependency_edge_state_change, and all seven declaredCTH_TYPESmembers. A heading ofunsafe:redactedorhandoff:unrecognizedcannot be used to forge a declared type — both land on the fallback and are marked sensitive.The four additional serialized-field fixes
Each verified independently at this head.
event_key._safe_record_idadmits only a plain numeric id. Injection-shaped ids, path fragments, booleans, over-long digit runs, and the canary are all refused and the row is skipped rather than keyed on. A legitimate id stays deterministic:cp:42.event_keyandcorrelation_id. Normalized once up front; a scope the adapter cannot express is refused outright. Injection-shaped kinds, the canary, empty, and null produce no events;issue/prwith an integer producecth:pr:849:5andpr#849. A non-integer number is refused rather than interpolated.SourceStatus.reason. Both status constructors redact the reason. A simulated authenticated fetch exception carrying basic-auth credentials in a URL plus a bearer value emittedhandoff source failed: 401 from [REDACTED_URL] token=[REDACTED]— the canary is absent, and the snapshot degraded per-source instead of collapsing._safe_echoguardsremote,org,repo, and each filter value; a secret-shaped value becomes the placeholder while ordinary values pass through untouched.Wider audit:
message,decision,actor,role, and thenext action/statusfallback all pass_redact;session_idpasses_safe_session_id;sourceis a module constant; issue and PR numbers are integers; nested page and source values are covered by the same boundary.Evidence references remain usable and are not over-blocked.
_SHA_REnow requires an anchoring keyword, so#849, short SHAs, and full 40-character SHAs stay usable while undeclared hex is not promoted. Confirmed directly: a canary supplied bare, in prose, as an assigned credential, as a bearer header, or inside a URL's basic-auth segment yields an empty reference list and never reaches the payload._validated_evidence_refsrechecks every reference before serialization and flags the event sensitive when it drops one.F1 — confirmed still correct, not reopened
_SOURCE_FILTER_SUPPORTdeclares per-source dimensions and is surfaced assupported_filters/unsupported_filters.session_idis populated only from the declared CTHSession:field; the control-plane adapter sets none and records why inline, and the obsolete control-plane session read stays removed. An unanswerable session filter returnsok=falsewithcode: filter_not_supportedand the route answers HTTP 422. A supported filter with no matches still returns an honest successful empty page. Filtering runs before pagination; ordering is(timestamp, source_rank, event_key)with missing timestamps last; issue and PR filters behave correctly.Scope and base synchronization
The delta against master
188e83c4d69bfb959c91b1f8e766ab8a0656d92dis exactly five files:webui/timeline.py,webui/app.py,canonical_thread_handoff.py,tests/test_webui_timeline.py, anddocs/webui-local-dev.md. Thea1e5a4af8ce0fc5da4d30db2712c4407c4af43ddcommit is the security remediation;b9ba43a5is a clean merge of live master carrying no author content — every non-PR file at this head is byte-identical to master, and no master content was dropped. Issue #637 remains the only issue implemented; nothing bleeds into #636 or #638, and the JSON route stays distinct from the HTML stub at/timeline.Non-blocking observation
_BARE_SECRET_SHAPEis anchored to lowercase hex, so an uppercase 32-character-or-longer hex run is admitted by_safe_cp_event_typeand_safe_session_id. This is not the asymmetry #526 described: the redaction policy itself does not classify a bare hex run of either case as a credential —scan_for_secretsreturns empty for both — and the lowercase case is refused only by this module's own additional guard. Gitea tokens and git SHAs are lowercase, so the guard matches the real shape. Adding a case-insensitive flag to that one pattern would close the case variant at no cost to any legitimate type, whenever this area is next touched. Not blocking and not a condition of this approval.Tests run by this review
From the pinned worktree, repository venv:
pytest tests/test_webui_timeline.py— 64 passed, 15 subtests passed.pytest -k webui— 376 passed, 4321 deselected, 336 subtests passed.pytest tests/test_canonical_thread_handoff.py tests/test_control_plane_db.py— 29 passed.pytest -k redact— 78 passed, 1 failed.The first three match the author's reported figures exactly.
The
-k redactfailure istests/test_mcp_server.py::TestSubmitPrReview::test_error_message_redacts_credential. It is not caused by this PR and is not a redaction defect. The assertion never reaches the credential path: the call fails closed earlier onblocker_kind: workspace_role_binding, because the review worktree used for that run was a detached scratchpad checkout rather than one underbranches/. The identical node was run at clean master188e83c4d69bfb959c91b1f8e766ab8a0656d92din an equivalent detached worktree and failed identically with the same blocker, so it is environment-determined baseline behaviour. Counting the selections, the PR head reports 78 passed against the baseline's 68 for the same single failure, so this branch adds ten passing redaction tests and no new failure. The author's reported 79-passed figure is consistent with running from abranches/worktree.The full suite was not rerun: the focused evidence resolves both event_type paths and all four additional fields, and the only failure encountered was independently classified against current master.
New security regression coverage
TestEventTypeBoundaryseeds the canary through the control-planeevent_typeand the CTH heading separately and through both at once, asserts on the field directly and then on the complete serialized payload, and pins the benign message so message redaction provably is not doing the work.TestSerializedFieldAuditcovers the four additional fields, andTestCthTypeContractpins contract membership. This is the coverage whose absence let the residual survive the previous round.NATIVE_REVIEW_PROOF: native MCP reviewer submission by sysadmin / prgs-reviewer at head
b9ba43a5bfvia gitea_submit_pr_review, workflow review-merge-pr hash 263d0a6cb8a6, reviewer lease comment 15130, session 41973-22c82c114387.Canonical PR State
STATE: approved
WHO_IS_NEXT: merger
NEXT_ACTION: A fresh standalone prgs-merger session independently re-verifies eligibility, runtime parity, exact head
b9ba43a5bf, approval validity at that head, lease state, base synchronization, checks, and conflicts, then merges PR #849 if and only if all of its own gates passNEXT_PROMPT:
WHAT_HAPPENED: Reviewer sysadmin reviewed PR #849 at head
b9ba43a5bffrom a clean detached worktree pinned to that exact head, read issue #637 and reviews 522 and 526, classified the2d95e0fctoa1e5a4afsecurity remediation and thea1e5a4aftob9ba43a5base-sync merge separately, confirmed the merge carries no author content and the PR delta against current master is exactly five files, reproduced the closure of the F2 event_type residual in both adapters and of all four additional serialized-field bypasses by direct execution with a fabricated canary, confirmed F1 still correct, ran four test selections, independently classified the single test failure encountered against clean master, and submitted one terminal APPROVED review. No merge was performed, no branch was modified, and reviews 522 and 526 were not altered.WHY: The blocking defect recorded in review 526 is resolved at this head. Externally influenced event_type now crosses a fail-closed boundary in both adapters, verified by seeding a fabricated secret-shaped canary through the control-plane event_type and the CTH heading with a benign message so message redaction demonstrably is not masking the result, and the canary appears nowhere in the complete serialized response. The four additional serialized-field protections are correct and proportionate, legitimate declared types and legitimate issue, PR, evidence, and authoritative commit references remain usable, F1 remains correct, issue #637 acceptance criteria 1 through 5 are met, and the only test failure encountered reproduces identically at current master.
ISSUE: #637
HEAD_SHA:
b9ba43a5bfPRIOR_REVIEWED_HEAD_SHA:
2d95e0fcc6REVIEW_STATUS: APPROVED
MERGE_READY: yes — approval is recorded at the current head with base synchronized, no conflicts, and checks not required; the merger must still pass its own eligibility, runtime, lease, and conflict gates before merging
BLOCKERS: none from this review
VALIDATION: pytest tests/test_webui_timeline.py 64 passed 15 subtests; pytest -k webui 376 passed 336 subtests; canonical-thread and control-plane 29 passed; pytest -k redact 78 passed 1 failed, that failure reproduced identically at master
188e83c4d6and classified environment-determined; F2 closure and the four additional field fixes reproduced by direct execution at this head with output quoted above; master parity in_parity=true mutation_safe=true restart_required=false at188e83c4d6LAST_UPDATED_BY: sysadmin / prgs-reviewer
[THREAD STATE LEDGER]
What is true now:
PR #849 is open on branch feat/issue-637-timeline-model against base master. The live head is
b9ba43a5bf, pinned from assess_pr_sync_status and get_pr_review_feedback immediately before submission. The branch is 0 commits behind live base188e83c4d6, reports mergeable with no conflicts, and status checks are not required for this base.Server-side decision state: review 522 is dismissed and stale at former head
a20975688d. Review 526 carries REQUEST_CHANGES recorded at former head2d95e0fcc6, is undismissed, and is stale relative to the current head. This review adds one terminal APPROVED verdict at the current head.The author remediation comment for this round is absent from the thread. The author reported being unable to post it because a foreign malformed issue #850 author lock blocked further author mutations. That absence is a protocol discrepancy only; it was not treated as evidence either way, and every conclusion above rests on direct diff inspection and execution at the pinned head.
What changed:
No repository content changed. This session created a detached review worktree at the pinned head and a second detached worktree at master for baseline comparison, acquired a reviewer lease, reproduced the security behaviour with a fabricated canary, ran four test selections, and submitted one review verdict.
What is blocked:
Blocker classification: no blocker
Reviewer operations were never obstructed. The retained issue #637 author worktree and the malformed issue #850 author lock are not reviewer-owned, were left untouched, and did not interfere with reviewer lease acquisition or verdict submission.
Who/what acts next:
Next actor: merger — a fresh standalone prgs-merger session on PR #849.
Required action: independently verify its own eligibility, runtime parity, exact head, approval validity at that head, lease ownership, base synchronization, checks, and conflicts, then merge only if all pass.
Do not do: do not clear, overwrite, repoint, or repair the retained issue #637 author lease or the malformed issue #850 author lock; do not dismiss, edit, or reinterpret review 522 or review 526; do not modify PRs #846, #838, #818, #795, or #794; do not force-push this branch.
ROLE: reviewer
IDENTITY: sysadmin
PROFILE: prgs-reviewer
REMOTE: prgs
REPOSITORY: Scaled-Tech-Consulting/Gitea-Tools
PR: #849
ISSUE: #637
REVIEWED_HEAD:
b9ba43a5bfBASE: master @
188e83c4d6VERDICT: APPROVED
BLOCKING_FINDINGS: 0
BLOCKER: none
MERGE_PERFORMED: no
REVIEW_DISMISSED: no
NEXT_ROLE: merger
STATE: approved
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #849
issue: #637
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 41973-22c82c114387
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/reviewer-pr849-b9ba43a5
phase: released
candidate_head:
b9ba43a5bftarget_branch: master
target_branch_sha:
188e83c4d6last_activity: 2026-07-24T00:18:35Z
expires_at: 2026-07-24T00:28:35Z
blocker: manual-release
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #849
issue: #637
reviewer_identity: sysadmin
profile: prgs-merger
session_id: 77376-ba12f1fb83a2
worktree: /Users/jasonwalker/Development/Gitea-Tools
phase: claimed
candidate_head:
b9ba43a5bftarget_branch: master
target_branch_sha:
188e83c4d6last_activity: 2026-07-24T00:31:16Z
expires_at: 2026-07-24T00:41:16Z
blocker: none
Stale #332 review-decision lock cleanup (#594)
Status: APPLIED
sysadminprgs-merger2026-07-24T00:33:24.449903+00:00approveon PR feat(webui): workflow-event and conversation timeline model (Closes #637) (#849)closed(merged=True)9301739910df7b51ca3abcdbf46cce25cfb026e55prgs-reviewerManual deletion of session-state files is not the workflow.
This path only clears a lock when the referenced PR is merged/closed.