apache / apache/maka

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

Open
#3,854 2 comments 0 reactions 1 assignee Claimed by @cat0825 View on GitHub
bug
Dominant language
TypeScript
Stars
5.4k
Forks
502
Avg merge
1d 2h
Merged PRs (30d)
715

Description

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

```bash
#!/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=` 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 #853.
4. Do not hide `GoalSet` only from the text runner. That would conflict with the model-armed Goal direction in #3199 and create a surface-specific tool boundary.

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

## Related

- #730 defines the one-turn stdout and exit-code contract but predates Runtime Host Goal continuation.
- #853 defines Client-independent Runtime Host work but does not define blocking `maka run` completion.
- #3022 adds Goal visibility to the TUI, not to non-interactive `maka run`.
- #3023 adds Goal controls to the TUI, not a text-run wait or detach contract.
- #3194 covers durable Goal consumer-loop correctness, not Client stdout, exit codes, or post-exit tools.
- #3244 discusses durable ownership after an `npx` shell exits, not the success boundary of an installed blocking CLI.

## 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

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.