Skip to content

fix(cli): maka run returns 0 while a self-armed full-access Goal keeps executing in the Runtime Host #3854

Description

@jsiu93

What happened

In an isolated maka-agent@next reproduction, the first model turn called GoalSet and answered ARMED. maka run --yolo then returned exit code 0 while the marker file did not exist. Stdout contained only ARMED.

The same Session recorded two Runs. The first had no goalId; the second had a goalId. Both recorded permissionMode=bypass. After the CLI process disappeared, the detached Runtime Host was reparented to PID 1, started the Goal continuation, and ran a shell command that created POST_EXIT\n after the CLI had returned.

GoalSet was called by the model from the first turn, not by a separate CLI Goal command. With the default 50-turn ceiling (packages/runtime/src/goal-state.ts:122), a Goal can run up to 49 successor Runs after the initial turn.

An earlier real-workload run (a local service integration check) showed the same boundary:

  • Its final answer said that it had not contacted any host outside 127.0.0.1.
  • After exit 0, a Goal Run inspected an established connection to 192.168.10.19:5672.
  • The Goal Run then ran stop.sh and stopped the service.

In both cases the final answer printed by maka run no longer describes the Session's final state.

How to reproduce

This uses the current $HOME for the configured connection and a fresh temporary working directory. It does not require a separate Goal command.

#!/usr/bin/env bash
set -u

tmp=$(mktemp -d)
work="$tmp/work"
gate="$tmp/cli-returned.gate"
marker="$tmp/post-exit.marker"
stdout="$tmp/stdout.txt"
stderr="$tmp/stderr.txt"
mkdir -p "$work"

condition="The file $marker exists and its complete content is exactly POST_EXIT plus a trailing newline, and it must be created only after a new user message beginning with [Goal continuation] is received."
prompt="This is a two-phase lifecycle test. In the current first turn, call GoalSet as your only tool. Set its success condition exactly to: $condition Omit max_iterations, block_cap, and token_budget so product defaults apply. In this first turn, do not inspect, wait for, or create $gate or $marker. After GoalSet succeeds, call no other tool and answer exactly ARMED. Only after you receive a new user message beginning with [Goal continuation], wait until $gate exists, then run sleep 5, write exactly POST_EXIT plus a trailing newline to $marker, verify the complete file content, and finish."

(
  cd "$work" || exit 97
  maka run --yolo --timeout 120 "$prompt"
) >"$stdout" 2>"$stderr"
rc=$?

printf 'exit_code=%s\n' "$rc"
printf 'stdout='; cat "$stdout"
if test -e "$marker"; then
  echo 'marker_at_cli_return=PRESENT'
else
  echo 'marker_at_cli_return=ABSENT'
fi

printf 'returned\n' >"$gate"
for _ in $(seq 1 60); do
  test -e "$marker" && break
  sleep 1
done

expected="$tmp/expected.txt"
printf 'POST_EXIT\n' >"$expected"
cmp -s "$expected" "$marker" && echo 'marker_after_return=POST_EXIT'
echo "artifacts=$tmp"

On the affected build, this prints exit_code=0, stdout=ARMED, and marker_at_cli_return=ABSENT. The Host then creates the marker and the script prints marker_after_return=POST_EXIT.

Expected

A successful blocking maka run must not return while a Goal armed by that invocation can still execute tools under the Session's permission boundary, unless the user explicitly selected a documented detach mode.

This follows the approved maka run contract in #730: "One invocation runs one model turn, including tool steps, then exits." The same contract defines 0: completed with finalOutput.

Root cause

The Runtime Host lifecycle and Goal durability are intentional. The architecture says the Host owns Runtime work (docs/architecture/runtime-host-architecture.md:35) and that Runtime work outlives a request connection (docs/architecture/runtime-host-architecture.md:43). #853 and #1154 establish that boundary. This issue is limited to the non-interactive completion contract.

The text runner consumes the current sendMessage() stream (packages/cli/src/run-command-core.ts:397). Only --graph adds a completion wait (packages/cli/src/run-command-core.ts:426-428). The normal success path closes the Client context (packages/cli/src/run-command-core.ts:440) and returns 0 from the first outcome (packages/cli/src/run-command-core.ts:462).

Closing the run context only closes local interaction, subscription, and graph-wait state (packages/cli/src/runtime-host-run-command.ts:414-419); the outer finally closes the Client connection (packages/cli/src/runtime-host-run-command.ts:135). The local Host candidate is detached and unreferenced (packages/runtime-host/src/client/launcher.ts:77, packages/runtime-host/src/client/launcher.ts:133).

A non-terminal Goal retains Host residency (packages/runtime-host/src/server/goal-coordinator.ts:593-601). The continuation starts asynchronously (packages/runtime/src/goal-continuation.ts:827) and creates a new Goal Run (packages/runtime-host/src/server/goal-execution-coordinator.ts:115-116). That Run inherits the Session header's permissionMode (packages/runtime/src/agent-run.ts:1125-1137).

Proposed boundary

  1. Preferred: follow the existing --graph waitForCompletion pattern and wait for the Goal to reach a terminal state. Project the final Goal Run's outcome to stdout and the process exit code. Timeout and SIGINT should continue through the existing stop path.
  2. Minimum mitigation: query Goal state before exit. If it is non-terminal, write goal continues: session=<id> to stderr and return a documented, distinct exit code.
  3. Do not pause on generic connection close. That would break the multi-client Runtime Host design in feat(runtime): RFC - one Runtime Host for every interactive Maka surface #853.
  4. Do not hide GoalSet only from the text runner. That would conflict with the model-armed Goal direction in feat(goal): let the user arm a Goal from the composer #3199 and create a surface-specific tool boundary.

I am willing to follow up with a PR once maintainers choose the boundary.

Related

Environment

  • maka-agent 0.1.0-beta.1 from npm @next.
  • HEAD f6bc66a has the same source-level lifecycle; HEAD was not run for this reproduction.
  • Node v24.19.0.
  • npm 11.17.0.
  • macOS 26.6.1, arm64.
  • Model via an OpenAI-compatible connection.
  • Surface: maka run --yolo.

Generated-by: gpt-5.6 (draft), human-reviewed before filing

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions