feat: add graph inspection and loop detection#29
Conversation
Verification update
This leaves the full repository test, Playwright, and build suite unverified by hosted CI. The observed failure is at job startup rather than in a reported test or build command. |
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
integrate-your-mind
left a comment
There was a problem hiding this comment.
Review result: changes requested
The graph data model and SCC implementation are clean and deterministic, but the current event semantics make the main loop metrics unreliable in normal sessions. I would not merge this version until the blocking correctness issues below are fixed.
Blocking
- Segment transitions by run/turn and exclude lifecycle/meta events. A normal second prompt currently closes a graph cycle with the prior turn, so routine multi-turn chats become reported loops.
- Derive loop state from current loop activity, not the whole agent's current state copied onto every historical phase.
- Either retain Claude hook events in
AgentSnapshot.eventsor narrow the provider-neutral claims. Claude currently produces agent nodes but no phase or transition graph. - Do not expose raw session identities in the versioned JSON graph. Codex identities can contain the full session path; URL encoding is reversible and does not satisfy the default redaction promise.
Medium
- Live
consensus graphcalls the full scanner, which may auto-startopencode serve. That conflicts with the command's read-only contract. AgentGraphLoop.observationsis the sum of all internal edge observations, not completed loop traversals. Rename it to make that explicit or compute iterations before using it for loop health, budgets, or alerts.
Required regression coverage
- Two independent turns:
prompt -> model -> turn.completed, thenprompt -> model, must not produce a loop. - A completed historical
model -> tool -> modelloop must not become active merely because the agent is active in a later phase. - A Claude hook sequence must produce normalized phase nodes, or the output/docs must clearly mark Claude graph history unavailable.
- JSON output with redaction enabled must not contain a reversible home or session path.
The existing focused tests cover the graph algorithm, but not these integration boundaries with the actual provider event stores.
| addContainsEdge(edges, rootNodeId, currentStepNodeId, event.ts); | ||
| if (previousStepNodeId && previousStepNodeId !== currentStepNodeId) { | ||
| addTransitionEdge(edges, previousStepNodeId, currentStepNodeId, event.ts); | ||
| } |
There was a problem hiding this comment.
Blocking correctness: previousStepNodeId spans the entire retained session, so this creates edges across independent turns. Existing Codex tails retain turn.completed; when the next user prompt arrives, the chain contains ... -> turn.completed -> prompt, while an earlier prompt -> ... path already exists. Tarjan then reports a loop for an ordinary two-turn conversation. Segment by turnId/run, or at minimum filter lifecycle events and reset the chain before a new prompt. Add a two-turn false-positive regression test.
There was a problem hiding this comment.
Fixed in the current head. Step nodes and transition edges are now scoped to observed turn segments; lifecycle/transport events are filtered, every prompt starts a new segment, and no edge crosses a segment boundary. Added regressions for both explicit turn.completed boundaries and providers that omit lifecycle events. The focused suite now passes 29/29 tests.
| label: phase, | ||
| state: event.isError ? "error" : agent.state, | ||
| agentId: agent.id, | ||
| agentIdentity: identity, |
There was a problem hiding this comment.
Blocking correctness: every historical phase inherits the agent's current state. If an agent is active on a later edit, all old model/tool nodes become active and every historical SCC is counted in activeLoops; an unrelated current error similarly marks old loops as errors. Only the current/latest phase should inherit live state, or loop state needs its own timestamp/in-flight calculation.
There was a problem hiding this comment.
Fixed. Historical step nodes remain idle, and only the latest observed step receives the agent’s live state. Because steps are also turn-scoped, a later active phase cannot reactivate a prior turn’s SCC. Added a regression where a completed model -> tool -> model loop stays idle while the agent is active in a later turn.
| let previousStepNodeId: string | undefined; | ||
| for (const event of sortedEvents(agent.events)) { | ||
| retainedEventTimes.push(event.ts); | ||
| const eventType = normalizedEventType(event); |
There was a problem hiding this comment.
Provider gap: the graph can only build steps from agent.events, but the Claude branch in scan.ts does not attach an events array. Claude therefore always produces an agent node with zero steps/edges, despite the README and graph docs describing provider-neutral graphs across Codex, OpenCode, and Claude Code. Retain normalized Claude hook events or narrow the claim until that path exists.
There was a problem hiding this comment.
Fixed without narrowing the provider claim. Claude hooks now retain bounded metadata-only event history, persist it locally for cross-process one-shot scans, hydrate it into Claude AgentSnapshot.events, and build prompt/model/tool phases. The adapter follows the current hook contract for MessageDisplay, PostToolBatch, PermissionDenied, task events, StopFailure, and slash-command expansion. Tests cover persistence, hydration, phase construction, and error state.
| for (const agent of agents) { | ||
| const identity = identityForAgent(agent); | ||
| const provider = providerForKind(agent.kind); | ||
| const rootNodeId = agentNodeId(identity); |
There was a problem hiding this comment.
PII/export issue: Codex agent.identity is built from the raw session path. This value is copied into agentIdentity and, through agentNodeId, into the versioned JSON IDs. encodeURIComponent is reversible, so consensus graph --json can expose a user's home/session path even with redaction enabled. Use an opaque stable hash/id and avoid emitting the raw identity.
There was a problem hiding this comment.
Fixed. Graph agent relationships now use truncated SHA-256 opaque keys derived from provider plus local identity; the raw agent.identity and reversible URL-encoded session path are not emitted in node or loop identifiers. Added a JSON regression that rejects both the raw path and its encoded form. Claude persisted history also stores only an opaque cwd key, not the raw directory.
| if (!snapshotPath) { | ||
| return scanCodexProcesses({ mode: "full" }); | ||
| } | ||
|
|
There was a problem hiding this comment.
Contract mismatch: live graph mode calls the full scanner. That scanner invokes ensureOpenCodeServer, which can spawn opencode serve when an OpenCode process exists and its API is unavailable. The new docs call this command read-only and say it does not start processes. Add a scan option that disables helper autostart for graph inspection, or state the side effect explicitly.
There was a problem hiding this comment.
Fixed. Live graph loading now sets CONSENSUS_OPENCODE_AUTOSTART=0 before dynamically importing the scan wrapper, then restores the prior environment value. This prevents import-time autostart capture and prevents ensureOpenCodeServer from spawning opencode serve. Added regressions for both a pre-existing env value and an initially unset value.
Review follow-upAll six review clusters are addressed on head
The provider contracts and public docs were rechecked on July 18, 2026 against the current Codex configuration reference, OpenCode server reference, Claude Code hooks reference, and OpenClaw agent-loop reference. Focused verificationCoverage includes two-turn false-loop prevention, historical loop state, opaque IDs, malformed snapshots, OpenCode autostart suppression, Claude hook normalization, metadata-only persistence, hydration, and Claude phase graph construction. Hosted CIGitHub Actions run 87 failed after two seconds before executing a workflow step. The API returns an empty step list and no job-log blob, so there is still no hosted test, Playwright, or build failure to diagnose. The full hosted suite remains unverified rather than code-red. |
Backlog dependency updateCommit ancestry confirms #29 and #6 diverged directly from the same Current integration order:
This PR remains ready for review as previously requested, but it is labeled |
What
consensus graph,--json, and--snapshot <path|->Why
On July 17, 2026, OpenClaw creator Peter Steinberger asked: “Are we still talking loops or did we shift to graphs yet?”
The useful answer is both: a broader execution graph can contain the model/tool loop, while an agent can create task paths at runtime. OpenClaw’s documented runtime still describes a serialized per-session loop covering intake, context assembly, model inference, tool execution, streaming, and persistence.
Consensus now represents a loop as a cycle inside one observed turn segment of a larger execution graph.
Review fixes
consensus graphinspectiontransitionObservations; it does not claim completed loop iterationsProvider contract check — July 18, 2026
notifyis a command array in user-level config; project-local config ignores notification keysparentIDand/session/:id/children; version 1 reserves these for later explicit parent/subagent edges instead of inferring causalityMessageDisplay,PostToolBatch,PermissionDenied, task lifecycle events, andStopFailureMessageDisplaycan fire in batches; Consensus retains only its final marker and never storesdeltaUserPromptSubmitis the retained turn boundary;UserPromptExpansionis activity-only so direct slash-command expansion does not create a second prompt segmentSources:
Privacy and product boundary
This remains read-only, local-first observability. It does not start, stop, retry, route, approve, or otherwise control agent work. Live graph inspection disables OpenCode autostart.
Claude cross-process history stores only event type, session ID, timestamp, an opaque working-directory key, and small metadata labels such as tool or agent type. It excludes prompt text, assistant text, message deltas, tool input/output, transcript paths, task descriptions, turn/message IDs, and error details. The file defaults to
~/.consensus/claude-events.jsonl, mode0600, a 1 MiB bound, and can be disabled withCONSENSUS_CLAUDE_EVENT_LOG=0.Version 1 does not claim parent, delegation, approval, retry, handoff, or cross-agent causal edges.
Verification
a2541485c889754a2c113e647c4c225bb0482fbf