Skip to content

Resolve goal loops that have no predicate (#346) - #350

Merged
scgopi merged 3 commits into
mainfrom
feat/346-goal-resolution
Sep 13, 2026
Merged

scgopi merged 3 commits into
mainfrom
feat/346-goal-resolution

Conversation

@scgopi

@scgopi scgopi commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Closes #346.

A goal loop without --predicate only resolved when the app's pane saw its process exit, which an interactive agent almost never does — so a finished graph sat at IDLE, its edges never fired, and its sessions stayed resident. A turn ending is still never treated as success: loops waiting on mail, CI or their children end turns too.

What changes

How a loop resolved is recorded. The card says whether a predicate passed, the backend recorded its goal as met, the loop reported done, or a human marked it done. A verdict resolves once; a stale report cannot overturn it.

Backends' own goal verdicts. GraphCode already launches goal loops with /goal. The daemon now reads the verdict each backend records, dated so an earlier goal's verdict never counts for a replacement:

Backend Signal
Claude Code transcript goal_status with met: true
Codex the thread's goal status becomes complete
Copilot CLI the autopilot objective becomes completed
OpenCode, pi none — graphcode node done

graphcode node done <path> <id> [result…]. The completion report every backend can send, locally and from remote hosts. A predicate, when set, still decides.

Leaders wait for their workers. A leader that reports done while loops it created are unresolved shows WAITING and resolves once they all succeed. If one did not, the report is discarded and the leader is told why.

Finished sessions are freed, loops are kept. After a loop resolves, its session is ended once it is confirmed idle with no terminal attached (Settings → End a finished loop's session: 1 min / 10 min / 1 hour / Never; default 10 minutes). The transcript stays. Opening the loop resumes the conversation and never starts the met goal again. Giving it a new goal reopens it without re-firing its edges.

Review

Independently reviewed by a Codex loop and a Claude Code loop before this was opened. Every finding was fixed; the five failing reproductions from the Codex review are kept as GoalResolutionReviewTests.

Known limits

  • Attribution for node done comes from $ZMX_SESSION; a worker without it is treated as a human.
  • Remote sessions are not ended automatically (their attached-terminal count cannot be read yet).
  • Loops inside composites keep their sessions.

scgopi and others added 3 commits September 13, 2026 12:35
A goal loop without --predicate only resolved when the app's pane saw its
process exit, which an interactive agent almost never does, so finished
graphs sat at IDLE and never fired their edges.

- Record how a loop resolved (predicate, backend goal verdict, reported
  done, human, session exit, workers) and show it on the card. A verdict
  resolves once; a stale surface report cannot overturn one.
- Read each backend's own verdict on the /goal it was launched with:
  Claude Code's goal_status met, Codex's thread goal complete, Copilot's
  objective completed. A turn ending is never treated as success.
- Add `graphcode node done`, the completion report every backend can
  send (the only one for OpenCode and pi). A predicate still decides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- A loop that reports done, or whose backend records its goal met, while
  loops it created are still unresolved is held and shows WAITING; it
  resolves once the last of them does. A human's `node done` is not held.
- Once a loop resolves and its final report has gone out, its session is
  ended after a grace set in Settings (default 10 minutes, or never). The
  transcript is kept, a mid-turn session gets another grace, and opening
  the loop resumes the conversation instead of starting the goal again.
- Giving a resolved goal loop a new goal reopens it: the earlier goal
  stays recorded, the session resumes with the new one, and its edges are
  not re-fired. A loop cannot hand itself a new goal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Bind every completion to the goal it was recorded against: backend
  verdicts carry their record time and are revalidated after the read, a
  replaced goal ignores earlier verdicts, and a done report sent before a
  reopened loop's new goal reaches its session is refused.
- A held completion is discarded when the goal or predicate changes, and
  is applied only if every created loop succeeded; otherwise the leader
  is told why.
- End a resolved session only on a reported idle, confirmed a second
  time, with no terminal attached; re-arm the ends after a restart.
- Resume a resolved loop's session when it is opened (Codex, remote and
  unbanked panes wait for the daemon), and deliver a reopened goal once.
- Run the predicate of a done report off the command stream; match the
  remote shim's done output; a dead leader is not shown waiting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@scgopi
scgopi merged commit c8a05af into main Sep 13, 2026
1 check passed
scgopi added a commit that referenced this pull request Sep 13, 2026
PR #350 landed with five [LineLength] violations, so `make check` exited 2 on
main. Formatter output only — five line wraps, no semantic change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016n4hSXVu6wLR4RCfnt6PPG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Predicate-less goal loops never resolve: a finished graph shows six IDLE cards and keeps six sessions resident

1 participant