Skip to content

feat: add graph inspection and loop detection#29

Open
integrate-your-mind wants to merge 72 commits into
mainfrom
agent/graphs-and-loops-core
Open

feat: add graph inspection and loop detection#29
integrate-your-mind wants to merge 72 commits into
mainfrom
agent/graphs-and-loops-core

Conversation

@integrate-your-mind

@integrate-your-mind integrate-your-mind commented Jul 18, 2026

Copy link
Copy Markdown
Owner

What

  • add a versioned, provider-neutral execution graph derived from retained Codex, OpenCode, and Claude Code events
  • scope step nodes and transition edges to observed turns
  • detect self-loops and multi-node cycles with strongly connected components
  • add consensus graph, --json, and --snapshot <path|->
  • retain bounded, metadata-only Claude hook history for one-shot scan and graph inspection
  • refocus the README, CLI docs, architecture, configuration, data inventory, threat model, changelog, and roadmap on graphs and loops

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

  • segment transitions by turn and exclude lifecycle/transport records, preventing ordinary multi-turn sessions from becoming false loops
  • apply live state only to the latest observed step so historical loops remain idle
  • add Claude hook history to snapshot graph inputs instead of emitting empty Claude graphs
  • replace reversible session-path identities with opaque graph keys
  • disable OpenCode server autostart during live consensus graph inspection
  • rename loop evidence to transitionObservations; it does not claim completed loop iterations
  • strictly validate saved snapshot agents, provider kinds, states, timestamps, and retained events

Provider contract check — July 18, 2026

  • Codex notify is a command array in user-level config; project-local config ignores notification keys
  • OpenCode exposes parentID and /session/:id/children; version 1 reserves these for later explicit parent/subagent edges instead of inferring causality
  • Claude Code hooks now include MessageDisplay, PostToolBatch, PermissionDenied, task lifecycle events, and StopFailure
  • MessageDisplay can fire in batches; Consensus retains only its final marker and never stores delta
  • UserPromptSubmit is the retained turn boundary; UserPromptExpansion is activity-only so direct slash-command expansion does not create a second prompt segment

Sources:

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, mode 0600, a 1 MiB bound, and can be disabled with CONSENSUS_CLAUDE_EVENT_LOG=0.

Version 1 does not claim parent, delegation, approval, retry, handoff, or cross-agent causal edges.

Verification

  • focused TypeScript compile passed
  • 29 focused tests passed, 0 failed
  • coverage includes two-turn false-loop prevention, historical loop state, opaque graph IDs, strict snapshot validation, OpenCode autostart suppression, Claude hook normalization, metadata-only persistence, hydration, and Claude phase graph construction
  • remote branch is one commit ahead of the reviewed fix head at a2541485c889754a2c113e647c4c225bb0482fbf
  • GitHub Actions run 87 failed after two seconds before executing any workflow step; GitHub returned an empty step list and no job-log blob, so the hosted full test/UI/build suite remains unverified rather than reporting a code-level failure

Copy link
Copy Markdown
Owner Author

Verification update

  • focused graph-core and CLI TypeScript compiles pass
  • all 9 graph unit tests pass
  • sample snapshot reports model -> tool -> model
  • GitHub Actions run 85 failed twice before executing any workflow step; both attempts returned an empty step list and no job log blob

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.

@integrate-your-mind
integrate-your-mind marked this pull request as ready for review July 18, 2026 19:20
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@integrate-your-mind integrate-your-mind left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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

  1. 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.
  2. Derive loop state from current loop activity, not the whole agent's current state copied onto every historical phase.
  3. Either retain Claude hook events in AgentSnapshot.events or narrow the provider-neutral claims. Claude currently produces agent nodes but no phase or transition graph.
  4. 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

  1. Live consensus graph calls the full scanner, which may auto-start opencode serve. That conflicts with the command's read-only contract.
  2. AgentGraphLoop.observations is 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, then prompt -> model, must not produce a loop.
  • A completed historical model -> tool -> model loop 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.

Comment thread src/graph.ts
addContainsEdge(edges, rootNodeId, currentStepNodeId, event.ts);
if (previousStepNodeId && previousStepNodeId !== currentStepNodeId) {
addTransitionEdge(edges, previousStepNodeId, currentStepNodeId, event.ts);
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Comment thread src/graph.ts Outdated
label: phase,
state: event.isError ? "error" : agent.state,
agentId: agent.id,
agentIdentity: identity,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Comment thread src/graph.ts Outdated
let previousStepNodeId: string | undefined;
for (const event of sortedEvents(agent.events)) {
retainedEventTimes.push(event.ts);
const eventType = normalizedEventType(event);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Comment thread src/graph.ts Outdated
for (const agent of agents) {
const identity = identityForAgent(agent);
const provider = providerForKind(agent.kind);
const rootNodeId = agentNodeId(identity);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Comment thread src/cli/graph.ts
if (!snapshotPath) {
return scanCodexProcesses({ mode: "full" });
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Owner Author

Review follow-up

All six review clusters are addressed on head a2541485c889754a2c113e647c4c225bb0482fbf:

  • turn-scoped transitions and lifecycle filtering
  • current-state isolation for historical loops
  • Claude Code phase history through bounded metadata-only persistence and hydration
  • opaque graph identity keys
  • read-only graph scans with OpenCode autostart disabled
  • transitionObservations naming plus strict snapshot validation

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 verification

TypeScript compile: passed
Tests: 29 passed, 0 failed

Coverage 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 CI

GitHub 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.

Copy link
Copy Markdown
Owner Author

Backlog dependency update

Commit ancestry confirms #29 and #6 diverged directly from the same main commit; neither contains the other. #29 is 72 commits ahead of the merge base and 40 commits behind #6's activity branch, so merging either one first will require a real rebase of the other.

Current integration order:

  1. Fix the live Codex expiry blocker on fix(activity): stabilize codex and opencode states #6 and merge fix(activity): stabilize codex and opencode states #6.
  2. Retarget and merge the one-commit UI/tail cleanup in fix(ui): event count + relative time in lane; evict stale tail states #26.
  3. Rebase this branch onto the resulting main, resolve scanner/server/types/docs conflicts, then rerun the graph and multi-harness verification.

This PR remains ready for review as previously requested, but it is labeled blocked-by-6 and needs-rebase; it should not merge from its current base.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant