Most coding agents follow the same rhythm: you ask, they answer. You prompt again, they do another step. The loop between your intent and the finished work is held together by your own attention span.
SolonCode takes a different approach. Its Loop engine introduces two long-running task modes — Heartbeat (scheduled, recurring) and Goal (autonomous, until done). Goal mode is the interesting one: you state an objective once, and the agent keeps working through successive rounds — checking evidence, running validators, and only stopping when the goal is actually achieved (or a budget runs out).
This post walks through how Goal loops work under the hood: the state machine, the model-facing tools (goal_get / goal_update), the runtime safety nets, and the budget-aware prompt that keeps the agent honest.
Two Loop Modes
The Loop panel in SolonCode's Web UI (or the /loop command in the CLI) gives you two flavors of recurring tasks:
- Heartbeat — run a prompt on a fixed interval or a cron expression. Think "check CI status every 30 minutes" or "scan for TODO comments daily at 22:00."
- Goal-driven — no schedule at all. The AI auto-loops until the goal description is achieved.
Under the hood these map to LoopTask.TaskType.HEARTBEAT and LoopTask.TaskType.GOAL. A heartbeat task is registered with either a fixed intervalMinutes or a 7-field cron expression. The /goal and /loop goal CLI paths create a Goal with intervalMinutes = 0, no cron expression, runNow = true, and a prompt that is literally your goal description; the scheduler maps that zero interval to a 5-second fixed-delay fallback.
In the CLI you can skip the form entirely:
/goal fix the auth module flaky tests
/loop goal fix the auth module flaky tests
/goal migrate the config service --max-tokens:200000 --max-duration:60
--max-duration is specified in minutes and converted to milliseconds internally. If you don't pass budgets, the scheduler falls back to the defaults configured in the Loop Goal settings (more on that below).
One useful CLI guard: /goal and /loop goal refuse to create a new Goal when they find an existing PURSUING Goal in the same session. This check belongs to the CLI creation path rather than a scheduler-wide invariant; paused and blocked Goals are not counted by it.
The Goal State Machine
A goal is not a boolean. SolonCode models it as a state machine with six states:
| State | Meaning | Resumable through /loop resume? |
|---|---|---|
PURSUING |
Active — the scheduler keeps submitting rounds | — |
PAUSED |
Paused by the user (/loop pause <id>) |
✅ |
BLOCKED |
Model declared a blocker via goal_update(blocked)
|
✅ |
ACHIEVED |
Goal verified complete | ❌ |
BUDGET_LIMITED |
Token/time budget exhausted | ❌ |
ITERATION_LIMITED |
Hit the max-rounds cap | ❌ |
Only PURSUING is "active" from the scheduler's point of view. PAUSED and BLOCKED are resumable — /loop resume <id> flips either state back to PURSUING and re-registers scheduling. The current resume path does not reset the task's stagnation or consecutive-error counters.
The transitions are enforced in GoalState:
public enum Status {
PURSUING, PAUSED, BLOCKED, ACHIEVED,
BUDGET_LIMITED, ITERATION_LIMITED;
public boolean isActive() { return this == PURSUING; }
public boolean isResumable() { return this == PAUSED || this == BLOCKED; }
}
When a goal hits its token or time limit, it isn't cut off immediately. The scheduler runs one final wrap-up turn (more on this later), then marks it BUDGET_LIMITED unless that turn completes the goal. GoalState has an internal extendBudget(...) transition, but the current CLI does not expose a budget-extension command.
The Model-Facing Tools: goal_get and goal_update
The agent doesn't have to infer all Goal state from conversation text — it gets two dedicated tools. They are exposed when the session has a pursuing Goal or a resumable paused/blocked Goal; state-changing updates are accepted only while the Goal is PURSUING.
goal_get — "where am I?"
Returns the full picture of the current goal, wrapped in a Codex-compatible envelope:
{
"goal": {
"taskId": "g-3f9a",
"objective": "fix the auth module flaky tests",
"status": "pursuing",
"iteration": 7,
"elapsedSeconds": 1842,
"consumedTokens": 142880,
"maxTokens": 200000,
"maxDurationMs": 3600000,
"remainingDurationSeconds": 1758,
"budgetExceeded": false
},
"remaining_tokens": 57120,
"completionBudgetReport": null
}
The remaining_tokens and completionBudgetReport fields mirror Codex's goal API shape, so agent prompts written for Codex-style goals feel familiar.
goal_update — "I'm done" (or "I'm stuck")
goal_update accepts exactly two status values:
-
complete— the agent claims the goal is finished. This is where the completion gates kick in (see next section). -
blocked— the agent admits it's stuck. The prompt guidance tells the model to only use this after genuinely trying the same obstacle 3 times. Declaring blocked pauses scheduling and waits for a humanresume.
Three Gates Before "Done" Means Done
SolonCode is suspicious of an agent that just says it finished. When the model calls goal_update(complete), three checks run in sequence:
1. Action evidence. For create/modify/run-type objectives, completion is rejected when the current execution path explicitly reports that the round used no non-Goal tool. In the normal tracked path, goal_get and goal_update themselves don't count as evidence. Some fallback execution paths can lack explicit evidence and use a text-based heuristic, so this is a practical guardrail rather than universal proof of action. When a tracked round reports no action, the scheduler tells the agent to keep working:
完成声明被拒绝:本轮没有实际工具执行证据,请继续完成并验证目标。
2. TODO checklist cleared. If the goal's workspace still has unfinished TODO items (- [ ] or - [/]), completion is rejected with a TODO_UNFINISHED error — the agent must finish or update the checklist first.
3. Objective validator. If validators are enabled, ValidatorFactory.forCondition(...) selects one heuristically from keywords in the objective. The test validator runs a detected test command and rejects completion on failure. The build validator currently performs a Maven or Gradle compile check; it deliberately passes when no supported build tool is detected or the validation command cannot be executed. Objectives that match no validator use NoopValidator.
Only after the applicable gates pass does the state flip to ACHIEVED and its scheduled job get removed. The task record remains persisted for status/history display.
gs.achieve();
scheduler.clearGoal(__sessionId, task.getId());
Runtime Safety Nets
Autonomy without guardrails is just a runaway bill. The LoopScheduler wraps every round with several layers of protection:
Guard conditions before each round. Disabled or cancelled tasks are prevented from running and their scheduled jobs are removed; explicit task deletion is handled separately. If the session is busy (a human is chatting), the round is skipped. Time budget, token budget, and iteration caps are all checked before starting a new round.
Stagnation detection. After each round, the scheduler computes a "fingerprint" of the result — whether tools were called, plus bucketed result length (200 chars/bucket) and line count (10 lines/bucket):
String toolDim = result.isHasToolCalls() ? "1" : "0";
int lenBucket = text.length() / 200;
int lineBucket = countLines(text) / 10;
return toolDim + ":" + lenBucket + ":" + lineBucket;
An unchanged fingerprint increments the stagnation counter. Because this fingerprint measures coarse output shape rather than semantic content, it is a heuristic signal — not proof that no progress occurred. Past the stagnation threshold, the prompt injects a "Stagnation Check" section asking the model to change strategy or declare blocked.
Error triage and circuit breaking. Errors are classified (SSL, NETWORK, HTTP_4XX, HTTP_5XX, TOOL_EXECUTION, OTHER). Non-recoverable errors (SSL, HTTP 4xx) repeated twice trip a fast circuit breaker straight to BLOCKED. The same error type 3 times in a row, or reaching the configured consecutive-error threshold, also flips the goal to BLOCKED. Below those thresholds, the scheduler retries with linear backoff — 5s, 10s, 15s...
Budget wrap-up instead of a cliff. When a budget is exhausted, the agent isn't cut off mid-thought. It gets one final wrap-up turn with a budget_limit prompt (mirroring Codex's budget_limit.md), asking it to summarize progress and remaining work. The agent can even use that last turn to goal_update(complete) if it realizes the work is genuinely done — in which case the goal lands on ACHIEVED rather than BUDGET_LIMITED.
Event-driven continuation. After a successful round that didn't terminate the goal, the scheduler submits the next round automatically — but only if the goal is still active, the budget isn't exceeded, and the session isn't busy. A 1-second minimum cooldown prevents tight-loop spinning.
Budget-Aware Prompting
Long autonomous runs drift. SolonCode fights this by injecting a structured "goal continuation" preamble into every round, and it adapts the prompt size to the remaining token budget:
| Remaining token budget | Prompt mode |
|---|---|
| > 30% | Full guidance |
| 15% – 30% | Compact guidance |
| < 15% | Minimal single-paragraph guidance |
The full mode includes sections like Evidence-Based ("don't rely on memory — check files, test output, build results"), Goal Fidelity ("don't narrow the goal or lower the bar; no placeholders, no TODOs, no stubs"), Audit Check ("for each objective item, verify with objective evidence: tests passing, build succeeding, files existing with correct content"), and Blocked Audit ("only declare blocked after 3 attempts at the same obstacle").
This is the same philosophy as Codex's goal loop: budget pressure changes how the model should behave, not just whether it should stop.
Tuning in the Settings Panel
The Loop Goal behavior is configurable from Settings → General → Loop Goal:
-
Default Token Budget / Default Time Budget —
0means unlimited. - Stagnation Threshold — rounds without progress before the prompt starts questioning the agent.
- Consecutive Error Threshold — errors in a row before auto-blocking.
-
Budget Warning % / Budget Critical % — configure warning and critical indicators in
GoalState. Prompt compression currently uses fixed remaining-token thresholds of 30% and 15% rather than reading these settings. -
Enable Validator — enable keyword-selected compile/test checks. Unmatched objectives use
NoopValidator, and the compile validator is intentionally fail-open when it cannot detect or execute a supported build command.
Persistence and Restoration
Core task and Goal state is persisted to loop-tasks.json inside the session directory after scheduling and most round transitions. CLI and Web/Desktop entry paths restore persisted tasks when a session or the Loop listing is initialized. In the current restore path, PAUSED and BLOCKED Goals are automatically changed back to PURSUING; they are not preserved as paused. The iteration-limit check runs again before scheduling. Some transient safety-net fields — including the last output fingerprint and same-error classification — are not restored across restarts. A shutdown hook pauses running Goals before process exit.
Heartbeat vs Goal: When to Use Which
- Heartbeat is for recurring, time-driven work: "every 30 minutes, check whether the staging deploy is healthy and report anomalies." It's a scheduled job with an AI brain.
- Goal is for bounded, outcome-driven work: "refactor the payment service to the new SDK and make all tests pass." You hand over an objective, the agent iterates with action checks, TODO state, and any matched validator, then stops when the Goal reaches a terminal state or a budget says stop.
The two compose nicely too: a heartbeat task can nudge you about pending work, while a goal runs deep on one focused objective.
SolonCode's Loop engine turns the coding agent from a "respond to prompts" tool into a "take an objective and keep iterating" worker. The Goal state machine, completion guardrails, stagnation heuristic, and token-budget-aware prompting make that autonomy observable and bounded.
If you want to try it: install SolonCode, open the Web UI, switch to the Loop panel, and set a goal. Then walk away — the agent will tell you when it's done (or when it's honestly stuck).
SolonCode is open source at github.com/opensolon/soloncode. Happy looping!




Top comments (0)