Skip to content

feat(provider): add official xAI Grok CLI support - #596

Open
thuanlm215 wants to merge 15 commits into
awslabs:mainfrom
thuanlm215:feat/grok-cli-provider-pr
Open

feat(provider): add official xAI Grok CLI support#596
thuanlm215 wants to merge 15 commits into
awslabs:mainfrom
thuanlm215:feat/grok-cli-provider-pr

Conversation

@thuanlm215

@thuanlm215 thuanlm215 commented Aug 12, 2026

Copy link
Copy Markdown

Closes #578.

Adds the official xAI Grok Build CLI as a first-class CAO provider.

What changed

  • Adds grok_cli provider registration across the CLI, API, provider manager, workspace access, and web provider selector.
  • Launches Grok with isolated per-terminal GROK_HOME, auth symlinking, atomic MCP TOML configuration, CAO terminal identity, profile/model/rules support, and cleanup.
  • Maps CAO tool restrictions to Grok native tools, including hard --deny rules and --disable-web-search when web_fetch is unavailable.
  • Detects idle, processing, completion, permission/login, telemetry, and error states from real Grok 1.0.0 terminal output, including ANSI/CUP redraws and multi-turn stale-buffer guards.
  • Adds documentation, sanitized live fixtures, unit coverage, and the provider E2E matrix.

Orchestration verification

The Grok tmux E2E matrix passed 14/14 locally. It covers:

  • restricted and unrestricted tools
  • assign with callbacks
  • handoff and second-turn behavior
  • send_message inbox delivery
  • runtime skill injection
  • supervisor handoff/assign
  • supervisor parallel assignment to 3 data analysts plus the report-generator flow

This exercises the examples/assign topology requested in #578: analysts complete their delegated work and communicate results back, while the supervisor coordinates and produces the report rather than doing the analyst work itself.

Additional verification

  • Focused Python suite: 340 passed
  • Markdown link tests: 25 passed
  • Fixture PII guard: passed
  • Black, isort, Grok provider mypy, and git diff --check: passed
  • Frontend tests were not run locally because Node/npm are unavailable in this environment; CI should exercise them.

Scope

This PR targets the official xAI Grok CLI only, as agreed in #578. It does not add a provider-specific CI workflow or include the separate experimental Herdr work.

@codecov-commenter

codecov-commenter commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 14.71471% with 568 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@c64c9fa). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/cli_agent_orchestrator/providers/grok_cli.py 15.57% 488 Missing ⚠️
...li_agent_orchestrator/services/terminal_service.py 0.00% 21 Missing ⚠️
src/cli_agent_orchestrator/providers/manager.py 10.00% 18 Missing ⚠️
src/cli_agent_orchestrator/utils/tool_mapping.py 0.00% 11 Missing ⚠️
src/cli_agent_orchestrator/api/main.py 0.00% 9 Missing ⚠️
...rc/cli_agent_orchestrator/cli/commands/shutdown.py 0.00% 8 Missing ⚠️
src/cli_agent_orchestrator/mcp_server/server.py 0.00% 7 Missing ⚠️
.../cli_agent_orchestrator/services/status_monitor.py 28.57% 5 Missing ⚠️
src/cli_agent_orchestrator/providers/base.py 66.66% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #596   +/-   ##
=======================================
  Coverage        ?   20.40%           
=======================================
  Files           ?      139           
  Lines           ?    18088           
  Branches        ?        0           
=======================================
  Hits            ?     3690           
  Misses          ?    14398           
  Partials        ?        0           
Flag Coverage Δ
unittests 20.40% <14.71%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@fanhongy fanhongy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

The provider registration, command construction, restrictions, MCP configuration, and focused tests are in place. I found two reproducible P2 defects in multi-turn status handling and restart cleanup.

Findings

P2: Long turns with the same elapsed-time marker can never complete

In src/cli_agent_orchestrator/providers/grok_cli.py:438, when the current query has fallen outside the 8,192-character status tail, turn_start is set to the completion marker itself. The fingerprint at lines 440-447 then contains only text such as Worked for 2.0s. If a later long turn reports the same elapsed duration, its otherwise valid completion is treated as the previous turn and get_status() returns PROCESSING indefinitely.

I reproduced this with two different queries and two different 9,000-character response streams, each ending in a structurally valid raw Worked for 2.0s marker and ready footer: the first returned completed, while the second returned processing. Because send_input() clears the rolling buffer and the status monitor consumes its processing-revert arm, handoff/inbox workflows can then wait until timeout despite Grok being visibly ready.

Preserve a current-turn discriminator before the query is evicted from the tail, or otherwise tie completion evidence to the dispatch/buffer generation instead of reducing the fingerprint to the elapsed-time marker. Add a regression test with two long, distinct turns sharing the same duration.

P2: Deleting a restored terminal leaves its private Grok home behind

src/cli_agent_orchestrator/providers/grok_cli.py:509 makes cleanup a no-op whenever _grok_home is None. That field is only assigned by _prepare_grok_home() during initialization. After cao-server restarts, ProviderManager reconstructs the provider from database metadata without running initialization, so the restored provider has _grok_home=None; if no provider has yet been reconstructed, cleanup_provider() is also a no-op. Deleting that terminal therefore leaves <CAO_HOME>/grok/terminals/<id>-<hash> permanently on disk.

I reproduced the lifecycle by preparing a home, discarding the provider object to simulate restart, constructing the restored provider, and calling cleanup(); home.exists() remained true. The retained directory contains config.toml and may contain MCP headers/environment values plus the live auth.json symlink, contradicting the documented cleanup behavior.

Make Grok cleanup reconstruct the deterministic path from terminal_id and ensure terminal deletion invokes that cleanup even when the in-memory provider map was lost. Add a restart/delete regression test.

Validation

  • Read all acquisition artifacts and the complete 40-file diff at head a7c09a4ae467a09bdbe3a64875beb1ec24d32bf0.
  • Ran focused provider, manager, tool-mapping, and terminal-service tests: 130 passed.
  • Ran git diff --check: passed.
  • Ran two read-only Python reproductions for the findings above.
  • Acquisition validation reported 340 focused Python tests, 175 web tests, and the production web build passing.
  • Live Grok E2E was not run because grok is not installed in the review environment.

@gutosantos82 gutosantos82 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PR Review: #596 — feat(provider): add official xAI Grok CLI support

Summary

Adds the official xAI Grok Build CLI as a first-class grok_cli provider: registration across CLI/API/manager/web, an isolated per-terminal GROK_HOME with atomic 0600 MCP config and symlinked (never copied) auth, hard tool restrictions via native --deny rules that survive --always-approve, careful multi-turn TUI status detection with raw/rendered fixtures, and thorough docs. The implementation quality is well above the bar for a new provider — it follows the established claude_code/copilot_cli hard-enforcement pattern and the codex/antigravity auto-approve pattern, ships a 619-line unit suite plus a full e2e matrix, and the security-relevant file handling (atomic write, mode repair, cleanup on init failure) is unusually careful. Recommend approve; two questions below are worth an author response but neither is a demonstrated defect.

Important (should fix or answer)

  • [security] src/cli_agent_orchestrator/utils/tool_mapping.py (grok_cli.execute_bash)execute_bash maps to only ["Bash"], while the claude_code mapping needed Bash, BashOutput, KillShell, Task, Agent, Monitor after live escapes through subagent/background-shell tools. Grok's subagent path is closed by unconditional --no-subagents (and CAO owns the config.toml it could be re-enabled from), but please confirm Grok 1.0.0's full native tool inventory contains no other execution-capable surface (background-shell output/kill companions, monitor-style tools, or anything that can shell out) that would stay approvable under --always-approve on a restricted profile. The passing restricted-supervisor e2e covers the Bash and write paths; it can't prove the inventory is exhaustively covered. Weighted up because this is the hard-enforcement security boundary.
  • [correctness] src/cli_agent_orchestrator/providers/grok_cli.py:get_status (stale-completion fingerprint guard) — the guard hashes the last-query→Worked for slice and reports PROCESSING while the fingerprint is unchanged after a new dispatch. If two consecutive turns produce a byte-identical slice (same prompt, same response, same thought/work durations — plausible for short repeated orchestration prompts like a heartbeat "ok"), the second completion is indistinguishable from the stale frame and the turn wedges in PROCESSING until any differing frame arrives. The tests show this trade-off is deliberate and the window is narrow, but consider mixing a monotonic cue (e.g., completion-match count in the full buffer) into the guard, or documenting the failure mode and its recovery. Weighted up because it sits in provider status detection.

Nits (optional)

  • [security] grok_cli.py:_build_grok_command — the full profile system prompt plus runtime skill catalog is passed as a single --rules argv element, visible to other local users via ps and bounded by ARG_MAX for very large skill catalogs. Antigravity does the same via -i, so there's precedent, but codex's file-based developer-instructions approach is the more robust pattern if Grok supports rules-from-file.
  • [correctness] grok_cli.py:_prepare_grok_home (auth symlink) — if a future Grok build refreshes tokens by atomically replacing auth.json, the replace converts the symlink into a regular file inside the disposable home: the refreshed token is deleted at cleanup and the real ~/.grok/auth.json keeps the stale one. Works with current observed behavior (in-place write-through); worth a one-line comment so the assumption is explicit.
  • [correctness] grok_cli.py:initialize — an unauthenticated launch parks on the login picker (correctly classified WAITING_USER_ANSWER) and then surfaces as a generic "initialization timed out" error. Including a hint ("is Grok authenticated? run grok login") in the timeout message would save operators a diagnosis step; docs do cover the prerequisite.
  • [consistency] test/e2e/test_allowed_tools.py — the added Path(BASH_MARKER_FILE).unlink(missing_ok=True) cleanup in the shared helpers changes behavior for all providers' tests, not just Grok. It's a genuine leak fix and harmless, just slightly out of the PR's stated scope.

Tests

Excellent coverage for a provider PR. The 619-line unit suite exercises status detection across rendered and raw pipe-pane fixtures (idle, processing, completed, permission picker, login, telemetry banner, error, second turn, ANSI/CUP redraws), stale-marker ordering, the multi-turn fingerprint guard, message extraction with chrome/thought/timestamp stripping, command construction (flag set, model precedence, deny mapping, web kill-switch, empty allowlist), private-home isolation, atomic 0600 config writes with mode repair, auth symlinking (including custom GROK_HOME), idempotent/retryable cleanup, and async init success/timeout/cleanup paths. Registration is covered in test_constants.py, test_provider_manager_unit.py, test_terminal_service.py, API and launch tests, and the tool-mapping table in test_tool_mapping.py. The e2e matrix adds Grok classes to allowed-tools (restricted + unrestricted + a new read-only reviewer write-denial probe), assign, handoff, send_message, skills, and supervisor orchestration, all gated on a require_grok fixture that reuses auth by symlink without copying credentials. Fixtures are sanitized (login code is XXXX-XXXX; no PII observed).

Verification

Ran the focused suites in the PR worktree with the repo venv (Python 3.12):

  • ✓ VERIFIED test/providers/test_grok_cli_unit.py, test_provider_manager_unit.py, test/utils/test_tool_mapping.py, test/test_constants.py, test/services/test_terminal_service.py194 passed.
  • ✓ VERIFIED test/api/test_api_endpoints.py, test/cli/commands/test_launch.py146 passed.
  • ✓ VERIFIED the PR body's "Focused Python suite: 340 passed" claim (194 + 146 = 340, matching exactly).
  • ⁇ NOT VERIFIED: the Grok tmux e2e matrix (14/14 claimed) and live TUI marker behavior — the grok binary is not installed on this host; e2e tests skip via require_grok. Manual: install Grok Build 1.0.0, authenticate, then uv run pytest -m e2e test/e2e/ -k grok -v.
  • ⁇ NOT VERIFIED: frontend tests (Node unavailable here, same as the author's environment); the web change is a one-element fallback-list addition with a matching test update — CI should exercise it.

Verdict

Approve with nits — a carefully engineered provider addition with strong isolation, hard enforcement, and verification; the two Important items are questions/hardening in sensitive paths rather than demonstrated defects.

Comment thread examples/assign/README.md

## E2E Testing

The `data_analyst` and `report_generator` profiles from this directory are used in the E2E test suite to validate assign and handoff flows across all providers (codex, claude_code, kiro_cli, kimi_cli).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@thuanlm215 why are we crossing out the existing providers ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No intent to cross out any provider. That wording was corrected in the merge update at 4b19e7c; the README now says the examples validate the supported providers, including grok_cli. Thanks for catching it.

@haofeif haofeif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@thuanlm215 thanks for the great work! I tested this provider against the official Grok Build 1.0.0 binary, not only the fixtures. I am requesting changes for five independently reproduced P2 issues.

The previously reported long-turn status wedge and restart cleanup leak are both real. I also reproduced three additional gaps: Grok workflows can still start native subagents in CAO-controlled mode, project-local configuration stops startup at an unhandled trust screen, and SSE MCP profiles are rewritten as ordinary HTTP profiles.

I checked the separate monitor concern as well: Grok reports that --deny Bash blocks the monitor tool, so I am not treating that as a defect. The focused Python suite (340 tests), web suite (175 tests), and production web build pass at this head. I could not run the account-backed assign/handoff E2E because no authorized Grok account was available.

binary,
"--no-alt-screen",
"--always-approve",
"--no-subagents",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Make CAO-controlled mode cover workflow subagents

--no-subagents hides Grok's direct spawn_subagent tool, but it does not stop the workflow tool or /goal from starting Grok-native workers. With Grok 1.0.0 and these exact TUI flags, I had the model call an inline workflow containing one agent() step; the workflow completed and produced a separate Grok Build subagent model request. /goal also displayed Ran 1 subagent.

Those workers are invisible to CAO's terminal/group accounting and do not use CAO's role profiles or assign callbacks, so a supervisor can delegate work outside CAO even though this flag and the docs say that path is closed. Please make CAO-controlled mode actually disable workflow-backed workers. Keep native loops available through an explicit mode so /goal and provider-native workflows remain usable when the user chooses them, rather than silently mixing the two control models.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 3bcab70. Root cause: --no-subagents only covered the direct tool, not workflow-backed workers or /goal. CAO-controlled launch now also sets GROK_SUBAGENTS=0, GROK_WORKFLOWS=0, and GROK_GOAL=0; native behavior requires the typed per-profile grokNativeWorkflows: true opt-in, which sets them to 1 and omits --no-subagents. Regression tests cover default and opt-in command construction/profile validation. I also ran focused Grok 1.0.0 probes for the default and opt-in paths; the default did not start a native worker, while the explicit opt-in did. This is documented with the version caveat because these controls are not all exposed by grok --help.

await asyncio.to_thread(
get_backend().send_keys, self.session_name, self.window_name, command
)
if not await wait_until_status(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Handle Grok's directory-trust screen before waiting for ready

Every terminal gets a fresh private GROK_HOME, so it has no saved folder-trust decision. In a working directory containing only .mcp.json, official Grok 1.0.0 stops at Do you trust the contents of this directory? before it renders the composer. That screen matches neither WAITING_USER_PATTERN nor a ready state, so this wait runs until provider_init_timeout, then initialization deletes the terminal. I reproduced this in the real TUI.

Please handle this startup screen using an explicit safe policy and add a regression fixture. Simply pressing Yes unconditionally needs care because the process also runs with --always-approve, and trusting the folder enables repository MCP/hooks.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 3bcab70. Root cause: initialization waited only for ready/completed status, so the project directory-trust surface was treated as a generic timeout. The ready wait now inspects the status buffer first and raises an actionable ProviderError on the exact trust screen; initialization cleanup then removes the private home. CAO deliberately never answers Yes because that trusts repository-local MCP, LSP, and hooks under the terminal user. Added trust-screen fixture/regression coverage and the operator guidance in docs/grok-cli.md. Focused live Grok 1.0.0 trust-screen probing confirmed detection follows the safe fail-closed path.

table = f"mcp_servers.{_toml_string(name)}"
lines.extend(["", f"[{table}]"])
if config.get("url"):
lines.append(f"url = {_toml_string(config['url'])}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Keep the SSE transport in generated MCP config

This URL branch drops the profile's type field. Grok 1.0.0 requires type = "sse" for an SSE server; its own grok mcp add --transport sse ... command writes that field. Without it, Grok treats the same URL as ordinary HTTP. I rendered an SSE profile with this code and confirmed with grok mcp list --json that the generated entry was no longer SSE. Preserve the transport field and add an SSE config test.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 3bcab70. Root cause: the URL MCP rendering branch emitted only url, silently relying on Grok HTTP defaulting and thereby changing an SSE profile. Generated TOML now preserves explicit type = "http" or type = "sse" and rejects unsupported URL transports rather than emitting a changed configuration. Regression coverage asserts both HTTP and SSE output.

completion_match = completion_matches[-1]
query_matches = list(QUERY_PATTERN.finditer(tail[: completion_match.start()]))
turn_start = (
query_matches[-1].start() if query_matches else completion_match.start()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Do not identify a long turn only by its elapsed-time text

Once a response is long enough to push its query outside this 8,192-character tail, turn_start becomes the completion marker itself. The fingerprint then contains only text such as Worked for 2.0s. A later, different long turn with the same reported duration is mistaken for the old completion and remains PROCESSING forever. I reproduced two different 9,000-character turns: the first returned COMPLETED, the second returned PROCESSING. Tie completion to the current dispatch/buffer generation instead, and add that two-turn regression case.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 3bcab70. Root cause: the stale guard used a fingerprint from the 8 KiB display tail; once the query was evicted, it degraded to the shared Worked for 2.0s marker. The provider now tracks a monotonic normalized-stream position across rolling-buffer overlap and combines it with full-transcript completion identity/current-turn activity. A retained stale completion remains PROCESSING, while a later identical marker at an advanced stream position completes. Regressions cover two >9 KiB distinct turns with equal durations, raw and rendered output, byte-identical consecutive turns, and 1 KiB rolling-buffer eviction. Focused regression suite passed locally (223 tests); full static validation also passed (318 passed, 10 skipped).

def cleanup(self) -> None:
self._initialized = False
home = self._grok_home
if home is None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Remove the private Grok home after a server restart

_grok_home exists only on the provider object that ran initialization. After cao-server restarts, deletion either finds no provider in the manager or reconstructs one with _grok_home = None, so this early return leaves the deterministic terminal home on disk. I reproduced prepare -> discard provider -> reconstruct -> cleanup, and the directory remained. The retained directory can contain generated MCP environment/header values and the live auth.json symlink. Derive the path from terminal_id during cleanup and make deletion call that cleanup even when the in-memory provider map was lost.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed across 3bcab70 and 6d9e392. Root cause: cleanup depended on the initialized provider object, which is absent after a server restart. Grok homes are now deterministically reconstructed from terminal id, and ProviderManager creates a cleanup-only Grok adapter from persisted terminal metadata when its in-memory map is empty. Cleanup validates the exact managed path (including symlinked-ancestor defenses), waits for exact-home residual Grok/MCP processes to stop, then removes the home. Terminal/flow teardown now kills the owning tmux process before provider cleanup to avoid updater recreation races. Regression coverage includes restart/delete, idempotency, retryable process cleanup, and symlink-escape handling; focused live cleanup probes and the focused unit suite passed.

@thuanlm215

thuanlm215 commented Aug 13, 2026

Copy link
Copy Markdown
Author

@haofeif All five requested P2 fixes are now pushed in 3bcab70 and 6d9e392, with inline root-cause and test details on each thread.

Validation now includes:

  • focused regression/static checks: 318 passed, 10 skipped
  • full account-backed Grok E2E matrix on 6d9e392: 14 passed in 679.73s (exit code 0)

The full E2E run covered allowed-tools, assign, handoff, inbox messaging, skills, and supervisor orchestration. Could you please re-review when convenient?

@haofeif haofeif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the thorough fixes @thuanlm215 . The native-workflow opt-in, trust-screen handling, SSE transport, and the original distinct long-turn case now behave as intended. The Linux restart-cleanup happy path also works.

I found two remaining P2 correctness gaps in the new state and cleanup logic: a fast repeated turn can still stay PROCESSING forever after the real buffer clear, and cleanup permanently leaks every private Grok home when Linux /proc is unavailable (including supported macOS). Both are independently reproduced in the inline comments.

I checked this exact head (6d9e392) against official Grok Build 1.0.0 and ran the focused provider/lifecycle/profile tests (124 passed). Account-backed Grok E2E was not available in this review environment.

Comment thread src/cli_agent_orchestrator/providers/grok_cli.py
Comment thread src/cli_agent_orchestrator/providers/grok_cli.py Outdated
@haofeif

haofeif commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Think we are getting very close. Very keen to get this PR merged

@thuanlm215

Copy link
Copy Markdown
Author

@haofeif Thanks again for the careful review. I’ve addressed the two remaining P2s and pushed the fixes in 78960b2. The focused regression suite is green (658 passed), and I also rechecked the affected Grok flows live, including the repeated-turn status case, portable cleanup, restricted permissions, and the three-analyst workflow. Hopefully this gets us over the line 🙂 Would appreciate another look when you have time.

@fanhongy fanhongy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

Reviewed head 78960b2f5b9da9206b8185bba5985bff2c684473 against the PR intent and base. The provider registration, permission mapping, isolated home setup, and retryable cleanup are broadly covered, but I found two P2 behavioral defects: raw status parsing can complete a turn on ordinary prose, and deferred cleanup is still reported as a successful delete by user-facing clients.

Findings

P2: Keep raw completion ordinals aligned with the status tail

src/cli_agent_orchestrator/providers/grok_cli.py:679

Raw structural completion ordinals are counted over the full output, while clean_counts restarts at zero inside the last 8 KiB tail. Once an earlier raw Worked for 2.0s marker has scrolled outside that tail, second-turn prose containing the same phrase is ordinal zero in the tail and incorrectly matches the old marker's structural ordinal. With a retained ready footer and a preceding processing marker, get_status() then returns COMPLETED even though the current turn has emitted no structural completion marker. This can make orchestration consume or act on an incomplete response. Preserve the full-buffer ordinal offset when scanning the tail (or associate raw structural markers with absolute normalized positions), and add a regression with an old marker outside _STATUS_TAIL_CHARS plus same-duration prose in the active turn.

P2: Propagate deferred cleanup as a failed delete

src/cli_agent_orchestrator/services/terminal_service.py:1717

The new cleanup contract returns False after killing the backend window but retaining the terminal row and Grok home for a required retry. DELETE /terminals/{id} exposes that as HTTP 200 with {"success": false}, but both dashboard delete handlers ignore the payload and show success. The session path similarly records an error and omits the session from deleted (src/cli_agent_orchestrator/services/session_service.py:175), while the API still returns "success": true; the web UI and cao shutdown therefore also report deletion complete. A protected/orphaned process triggers this path, leaving the retry row and private home behind without telling the user to retry. Make deferred cleanup a non-success API result, or require every client to inspect success/errors, preserve the retry identifier, and add terminal/session client tests for the deferred case.

Validation

  • 366 passed: focused provider, manager, lifecycle, status monitor, MCP cleanup, and tool-mapping tests.
  • 2 passed: the two flow-service tests noted as failures in the acquisition context, rerun directly under the repository's normal test contract.
  • 175 passed: frontend tests (with expected jsdom canvas/error-boundary diagnostics).
  • Grok provider mypy and Markdown link validation passed.
  • A direct parser reproduction returned completed for a second turn containing no current structural completion marker.
  • Live Grok E2E was not run because grok is not installed in the review environment.

An evicted structural Worked-for marker was still ordinal 0 in the full
buffer, while the 8 KiB tail restarted that count. Same-duration prose in
the active turn then matched the old marker and returned COMPLETED.
DELETE /terminals and DELETE /sessions now return HTTP 409 when cleanup
must be retried, and the dashboard, shutdown CLI, and MCP client inspect
that result instead of reporting a successful delete.
@thuanlm215

Copy link
Copy Markdown
Author

@fanhongy thanks for catching these — both made sense.

I pushed two follow-ups on this:

  • fecc1b2 keeps the raw completion ordinals aligned with the status tail, so an old Worked for marker that has scrolled out of the tail no longer completes later prose
  • 3cfc6cc makes deferred Grok cleanup a real failed delete (409), and the dashboard / cao shutdown / MCP delete now treat it that way instead of showing success

Would you mind taking another look when you have a chance?

@fanhongy

Copy link
Copy Markdown
Collaborator

Sure. Thanks for the PR, LGTM, I have approved, pending on @haofeif

@haofeif haofeif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM. Thank you @thuanlm215 for your great contribution!

@haofeif

haofeif commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

@thuanlm215 can you please help to fix the CI errors ?

The deferred-cleanup 409 check used `session_name not in deleted`. A bool
or other non-sequence mock then raised TypeError and became HTTP 500.
@thuanlm215

Copy link
Copy Markdown
Author

Fixed the CI failure in f47d419. Waiting on the workflow run for this push.

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.

Feature request: Add Grok CLI as a first-class provider

5 participants