Skip to content

fix(sessions): preserve terminal status after agent run completes#60290

Closed
jwchmodx wants to merge 5 commits into
openclaw:mainfrom
jwchmodx:fix/session-status-clobbered-after-run
Closed

fix(sessions): preserve terminal status after agent run completes#60290
jwchmodx wants to merge 5 commits into
openclaw:mainfrom
jwchmodx:fix/session-status-clobbered-after-run

Conversation

@jwchmodx

@jwchmodx jwchmodx commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Problem

After a run completes, a session can remain persisted with status: "running" even though endedAt and runtimeMs are already set. This wedges the session: new inbound messages are not accepted and stop/abort commands do not work until the store row is manually edited.

Root cause

persistGatewaySessionLifecycleEvent is called with void (fire-and-forget) in the agent event listener. It enqueues a write to set the terminal status on disk. Immediately after, updateSessionStoreAfterAgentRun enqueues its own write.

The lock queue runs them in order:

  1. Lifecycle end handler: reads disk, writes { status: "done", endedAt: X, runtimeMs: Y }
  2. updateSessionStoreAfterAgentRun: reads disk (has "done"), but patches from the in-memory sessionStore which was loaded during session initialisation — before the run — and still carries status: "running". mergeSessionEntry(diskState, next) spreads next.status = "running" over the disk's "done", leaving endedAt/runtimeMs intact (they were never in next).

Final persisted state: { status: "running", endedAt: X, runtimeMs: Y } — the exact stuck pattern described in the issue.

Fix

Omit status from the patch passed to mergeSessionEntry inside updateSessionStoreAfterAgentRun. This function is responsible for token/cost/model fields only; lifecycle status is exclusively owned by persistGatewaySessionLifecycleEvent.

Test

Added a regression test in src/commands/agent/session-store.test.ts:

  • Disk pre-seeded with { status: "done", endedAt: X, runtimeMs: 1234 } (as if lifecycle end already ran)
  • In-memory store has { status: "running" } (stale)
  • After updateSessionStoreAfterAgentRun, asserts persisted.status === "done" and endedAt/runtimeMs preserved

Closes #60250

🤖 Generated with Claude Code

jwchmodx and others added 5 commits April 3, 2026 15:45
…ostReplyRootId

Direct messages in Mattermost were creating threads even with
replyToMode=off because resolveMattermostReplyRootId would fall back
to payload.replyToId regardless of chat kind. When a downstream payload
carried a replyToId (e.g. from block-streaming delivery), this
bypassed the earlier DM threading guard and set root_id on the outbound
post, making DM replies appear as threads instead of in the channel body.

Fix: pass kind through all three delivery call sites and hard-return
undefined inside resolveMattermostReplyRootId for kind="direct",
mirroring the resolveMattermostEffectiveReplyToId guard that already
existed for session-key resolution.

Fixes openclaw#59981
…hreshold

The generic_repeat detector only checked warningThreshold and always
returned level "warning", making criticalThreshold effectively a no-op
for the most common runaway loop pattern (same tool + same args).

Add a critical-threshold check before the warning check, consistent
with how known_poll_no_progress and ping_pong already behave.

Fixes openclaw#60111

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…aining-only fields

MiniMax's usage_percent / usagePercent fields report the *remaining* quota
as a percentage, not the consumed quota. When count fields (prompt_limit /
prompt_remain) are also present, fromCounts already computed the correct
usedPercent and the inverted value was silently ignored. But when only
usage_percent is returned (no count fields), the code treated it as a
used-percent and passed it through unchanged, causing the menu bar to show
"2% left" instead of "98% left".

Move usage_percent and usagePercent from PERCENT_KEYS to a new
REMAINING_PERCENT_KEYS array. deriveUsedPercent now inverts remaining-percent
values to obtain usedPercent, matching the behaviour already validated by the
existing "prefers count-based usage when percent looks inverted" test. Count-
based fromCounts still takes priority over both key groups.

Fixes openclaw#60193

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
MiMo reasoning models (mimo-v2-pro, mimo-v2-omni) output their full
response to reasoning_content with an empty content field. This causes
OpenClaw to emit no visible text since the pi-ai stream emits
reasoning_content deltas as thinking blocks, not as main content.

Setting enable_thinking: false in the request payload directs the model
to write its reply to the standard content field instead.

Closes openclaw#60261

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
persistGatewaySessionLifecycleEvent writes the terminal status to disk
as a fire-and-forget write. updateSessionStoreAfterAgentRun runs after
it, but reads the in-memory sessionStore (loaded before the run) which
still carries status: "running". The stale status was being spread into
the merge patch, clobbering the "done"/"failed"/"killed" status on disk.

Result: sessions could remain stuck as "running" even though endedAt and
runtimeMs were correctly set, blocking new inbound messages and
stop/abort commands until the store row was edited manually.

Fix: omit the status field from the patch in updateSessionStoreAfterAgentRun
so that the lifecycle-managed terminal status on disk is always preserved.

Closes openclaw#60250

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@greptile-apps

greptile-apps Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR bundles five independent fixes: the primary fix strips status from the patch in updateSessionStoreAfterAgentRun so stale in-memory \"running\" cannot overwrite a terminal status already persisted by persistGatewaySessionLifecycleEvent; the other four changes fix Mattermost DM threading, escalate generic tool-loop repeats to \"critical\" at criticalThreshold, invert the MiniMax usage_percent (remaining→used), and inject enable_thinking: false for Xiaomi reasoning models. All five changes are well-targeted with regression tests.

Confidence Score: 5/5

Safe to merge — all five fixes are targeted, well-tested, and carry no P0/P1 issues.

All remaining findings are P2 or lower. The primary session-status fix is logically correct and covered by a regression test. The four accompanying fixes (Mattermost DM threading, tool-loop critical escalation, MiniMax percent inversion, Xiaomi reasoning-model payload) are each independently sound and tested. No data integrity, security, or reliability concerns were found.

No files require special attention.

Reviews (1): Last reviewed commit: "fix(sessions): preserve terminal status ..." | Re-trigger Greptile

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c230d1d0e8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +51 to +54
// MiniMax's usage_percent / usagePercent fields report the remaining quota
// as a percentage, not the consumed quota. Treat them as "remaining percent"
// and invert to get usedPercent. Count-based fromCounts always takes priority.
const REMAINING_PERCENT_KEYS = ["usage_percent", "usagePercent"] as const;

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.

P2 Badge Score remaining-percent keys when selecting MiniMax usage records

REMAINING_PERCENT_KEYS is introduced for usage_percent, but scoreUsageRecord still only awards score for PERCENT_KEYS. As a result, nested usage records that only expose usage_percent/usagePercent (and no count fields) can be filtered out of collectUsageCandidates, causing fetchMinimaxUsage to return Unsupported response shape even though valid usage data exists. This regression appears in payloads where the remaining-percent field is present only in nested objects.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Close as superseded: the central session-status bug is already handled on current main, the Mattermost/MiniMax/generic-loop portions are implemented or tracked by narrower work, and the remaining Xiaomi behavior belongs on the canonical Xiaomi PR rather than this stale bundled branch.

So I’m closing this here and keeping the remaining discussion on the canonical linked item.

Review details

Best possible solution:

Close this stale bundled PR, keep the shipped/current-main fixes, and route the remaining Xiaomi MiMo behavior to #60304.

Do we have a high-confidence way to reproduce the issue?

Yes for the review finding by source inspection: the PR moves usage_percent into REMAINING_PERCENT_KEYS but never scores those keys for nested MiniMax usage candidates. The central session bug no longer reproduces on current main because lifecycle-owned fields are stripped before the after-run merge.

Is this the best way to solve the issue?

No. The session-store direction is implemented on current main, but this conflicting bundled branch is not the narrow maintainable path; the remaining useful work is already split into shipped fixes or canonical narrower PRs.

Security review:

Security review cleared: The PR diff does not change workflows, lockfiles, package metadata, install scripts, permissions, credential handling, or dependency sources.

What I checked:

  • Live PR state: The PR is still open, unmerged, merge-conflicting/dirty, authored by a contributor, has no protected labels, and closes the already-closed session issue while bundling five separate commits/filesets. (c230d1d0e8e1)
  • Session fix present on current main: Current main removes lifecycle-owned fields from the after-run metadata patch before merging against the persisted session row, so stale in-memory status/startedAt/endedAt/runtimeMs cannot clobber terminal lifecycle state. (src/agents/command/session-store.ts:39, 164031f73f9e)
  • Session regression covered on current main: Current main includes a regression test that seeds disk with status done and terminal timing fields, passes a stale running in-memory snapshot, and expects the terminal lifecycle state to be preserved. (src/agents/command/session-store.test.ts:395, 164031f73f9e)
  • Linked session issue already closed as implemented: The linked session issue was closed with maintainer proof that lifecycle terminal-state persistence was implemented and shipped, so this PR is no longer the canonical path for that report.
  • Mattermost portion already implemented: Current main makes resolveMattermostReplyRootId chat-kind aware and returns undefined for direct chats before considering threadRootId or replyToId; the canonical Mattermost issue was later closed as implemented through replacement work. (extensions/mattermost/src/mattermost/monitor.ts:249, 164031f73f9e)
  • Generic loop portion superseded by current behavior: Current main has a no-progress-gated generic_repeat critical branch, which is safer than the PR's count-only generic-repeat escalation; the narrower loop PR remains the canonical discussion if follow-up is needed. (src/agents/tool-loop-detection.ts:612, 164031f73f9e)

Likely related people:

  • stainlu: Recent merged history touches session-store preservation behavior in the central session-store files. (role: recent session-store contributor; confidence: medium; commits: 24b915ed413e; files: src/agents/command/session-store.ts, src/agents/command/session-store.test.ts)
  • vincentkoc: Current history includes the Mattermost DM threading replacement fix and PR discussion shows recent repair work on the generic loop detector branch. (role: recent Mattermost and loop-detection repair contributor; confidence: high; commits: 59fb5fd3a7a4, 9fd4b85d9a4b; files: extensions/mattermost/src/mattermost/monitor.ts, extensions/mattermost/src/mattermost/monitor.test.ts, src/agents/tool-loop-detection.ts)
  • jwchmodx: The author also has merged current-main MiniMax remaining-percent parsing work, so they are connected beyond only opening this PR. (role: source PR author and MiniMax current-main contributor; confidence: medium; commits: 28021a03257f, c230d1d0e8e1; files: src/infra/provider-usage.fetch.minimax.ts, src/infra/provider-usage.fetch.minimax.test.ts, src/agents/command/session-store.ts)
  • steipete: Recent current-main history includes MiniMax usage parsing and provider/plugin SDK work adjacent to the MiniMax and Xiaomi surfaces. (role: adjacent provider/usage owner; confidence: medium; commits: 9eb8184f368a, 956fe72b39e6; files: src/infra/provider-usage.fetch.minimax.ts, extensions/xiaomi/index.ts, src/plugin-sdk/provider-entry.ts)
  • shakkernerd: Recent history moved Xiaomi model metadata into the plugin manifest that defines the affected MiMo reasoning models. (role: recent Xiaomi catalog contributor; confidence: medium; commits: 2d7b16e0dbb5; files: extensions/xiaomi/openclaw.plugin.json, extensions/xiaomi/provider-catalog.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 164031f73f9e.

@clawsweeper clawsweeper Bot closed this May 11, 2026
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.

BUG: Completed run can remain persisted as running, blocking new input and stop

1 participant