Skip to content

fix(cron): preserve parent session updatedAt for isolated cron jobs#51026

Closed
QuinnH496 wants to merge 1 commit into
openclaw:mainfrom
QuinnH496:fix/cron-session-overwrite
Closed

fix(cron): preserve parent session updatedAt for isolated cron jobs#51026
QuinnH496 wants to merge 1 commit into
openclaw:mainfrom
QuinnH496:fix/cron-session-overwrite

Conversation

@QuinnH496

Copy link
Copy Markdown
Contributor

Problem

Isolated cron jobs with sessionKey matching the main session key were overwriting the parent session's updatedAt, preventing daily session reset from triggering. This caused users to get previous day's session with stale context instead of a fresh start.

Solution

Use mergeSessionEntryPreserveActivity when writing to the agent session key to preserve the parent session's activity timestamp. This prevents isolated cron jobs from bumping the updatedAt past the daily reset boundary.

Changes

Testing

Manual testing needed: Configure an isolated cron job with sessionKey: "agent:main:main" that runs after the daily reset hour, then verify that the daily reset still triggers correctly.

Fixes #51000

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Mar 20, 2026
@greptile-apps

greptile-apps Bot commented Mar 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR addresses two separate concerns: (1) the primary fix prevents isolated cron jobs whose sessionKey matches the parent session key from advancing updatedAt past the daily reset boundary, and (2) a secondary change adds Bailian API (OpenAI-compatible nested usage object) support in the usage normalization layer.

Key changes:

  • src/cron/isolated-agent/run.ts: persistSessionEntry now uses mergeSessionEntryPreserveActivity for writes to agentSessionKey in both in-memory and on-disk paths, correctly preserving the parent session's updatedAt without disturbing session content fields.
  • src/agents/usage.ts: normalizeUsage and hasNonzeroUsage are extended to unwrap a nested usage sub-object (raw.usage ?? raw) for Bailian API compatibility. All existing fields retain their priority via ?? chaining.
  • src/agents/usage.normalization.test.ts: Two well-written test cases cover the new nested usage path.

One minor gap noted: hasNonzeroUsage checks usageObj.prompt_tokens, usageObj.completion_tokens, and usageObj.total_tokens from the nested object, but omits usageObj.cached_tokens — inconsistent with normalizeUsage, which does extract that field as cacheRead. In practice this edge case (non-zero cache reads with zero other tokens) is unlikely, but worth aligning for consistency.

Confidence Score: 4/5

  • Safe to merge; the cron fix is logically sound and the usage normalization change is additive with no breaking impact on existing paths.
  • The primary run.ts fix is correct: mergeSessionEntryPreserveActivity properly preserves parent session updatedAt while still applying all other session fields. Edge cases (no existing entry, runSessionKey !== agentSessionKey) are handled correctly. The usage changes are purely additive. Score is 4 rather than 5 due to the minor usageObj.cached_tokens gap in hasNonzeroUsage and the lack of automated test coverage for the cron fix path.
  • No files require special attention, though src/agents/usage.ts has a minor inconsistency in hasNonzeroUsage worth addressing.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/usage.ts
Line: 93-102

Comment:
**Missing `cached_tokens` in nested usage check**

`hasNonzeroUsage` checks `usageObj.prompt_tokens`, `usageObj.completion_tokens`, and `usageObj.total_tokens` from the nested object, but omits `usageObj.cached_tokens`. This creates an inconsistency with `normalizeUsage`, which correctly maps `usageObj.cached_tokens``cacheRead`.

In the unlikely but possible case where a Bailian API response returns non-zero `cached_tokens` with zero prompt/completion/total tokens, `hasNonzeroUsage` would return `false` while `normalizeUsage` would return a non-undefined result with a non-zero `cacheRead`. Callers that guard behind `hasNonzeroUsage` (e.g. `pi-embedded-subscribe.ts:274`) would silently discard that usage.

```suggestion
  return [
    usage.input,
    usage.output,
    usage.cacheRead,
    usage.cacheWrite,
    usage.total,
    usageObj.prompt_tokens,
    usageObj.completion_tokens,
    usageObj.total_tokens,
    usageObj.cached_tokens,
  ].some(
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "fix(cron): preserve ..."

Comment thread src/agents/usage.ts Outdated
Comment on lines +93 to +102
return [
usage.input,
usage.output,
usage.cacheRead,
usage.cacheWrite,
usage.total,
usageObj.prompt_tokens,
usageObj.completion_tokens,
usageObj.total_tokens,
].some(

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 Missing cached_tokens in nested usage check

hasNonzeroUsage checks usageObj.prompt_tokens, usageObj.completion_tokens, and usageObj.total_tokens from the nested object, but omits usageObj.cached_tokens. This creates an inconsistency with normalizeUsage, which correctly maps usageObj.cached_tokenscacheRead.

In the unlikely but possible case where a Bailian API response returns non-zero cached_tokens with zero prompt/completion/total tokens, hasNonzeroUsage would return false while normalizeUsage would return a non-undefined result with a non-zero cacheRead. Callers that guard behind hasNonzeroUsage (e.g. pi-embedded-subscribe.ts:274) would silently discard that usage.

Suggested change
return [
usage.input,
usage.output,
usage.cacheRead,
usage.cacheWrite,
usage.total,
usageObj.prompt_tokens,
usageObj.completion_tokens,
usageObj.total_tokens,
].some(
return [
usage.input,
usage.output,
usage.cacheRead,
usage.cacheWrite,
usage.total,
usageObj.prompt_tokens,
usageObj.completion_tokens,
usageObj.total_tokens,
usageObj.cached_tokens,
].some(
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/usage.ts
Line: 93-102

Comment:
**Missing `cached_tokens` in nested usage check**

`hasNonzeroUsage` checks `usageObj.prompt_tokens`, `usageObj.completion_tokens`, and `usageObj.total_tokens` from the nested object, but omits `usageObj.cached_tokens`. This creates an inconsistency with `normalizeUsage`, which correctly maps `usageObj.cached_tokens``cacheRead`.

In the unlikely but possible case where a Bailian API response returns non-zero `cached_tokens` with zero prompt/completion/total tokens, `hasNonzeroUsage` would return `false` while `normalizeUsage` would return a non-undefined result with a non-zero `cacheRead`. Callers that guard behind `hasNonzeroUsage` (e.g. `pi-embedded-subscribe.ts:274`) would silently discard that usage.

```suggestion
  return [
    usage.input,
    usage.output,
    usage.cacheRead,
    usage.cacheWrite,
    usage.total,
    usageObj.prompt_tokens,
    usageObj.completion_tokens,
    usageObj.total_tokens,
    usageObj.cached_tokens,
  ].some(
```

How can I resolve this? If you propose a fix, please make it concise.

@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: d14258b5e2

ℹ️ 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 +387 to +389
cronSession.store[agentSessionKey] = mergeSessionEntryPreserveActivity(
cronSession.store[agentSessionKey],
cronSession.sessionEntry,

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.

P1 Badge Preserve activity only when writing parent session key

persistSessionEntry now always uses mergeSessionEntryPreserveActivity for agentSessionKey, which freezes updatedAt for normal cron-owned sessions too. On subsequent runs, resolveCronSession evaluates freshness from that stale timestamp, so after the daily/idle boundary it will treat an actively running cron session as stale and rotate to a new session unnecessarily. This breaks shared cron context continuity for jobs that are not targeting a parent user session key.

Useful? React with 👍 / 👎.

Comment thread src/agents/usage.ts Outdated
Comment on lines +92 to +93
const usageObj = (usage as any).usage ?? usage;
return [

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 Normalize nested usage before narrowing to NormalizedUsage

hasNonzeroUsage now returns true for nested usage.prompt_tokens-style payloads, but the function still narrows the argument to NormalizedUsage. Callers that rely on that guard then read usage.input/output/cache* and can persist zeros when the payload is nested-only, so usage/cost accounting becomes incorrect even though nonzero usage was detected. The guard should either normalize first or avoid claiming the value is already NormalizedUsage in this branch.

Useful? React with 👍 / 👎.

Isolated cron jobs with sessionKey matching the main session key were
overwriting the parent session's updatedAt, preventing daily session
reset from triggering. This fix uses mergeSessionEntryPreserveActivity
to preserve the parent session's activity timestamp when writing to
the agent session key.

Fixes #51000
@QuinnH496
QuinnH496 force-pushed the fix/cron-session-overwrite branch from d14258b to 2e9746a Compare March 20, 2026 12:39
@openclaw-barnacle openclaw-barnacle Bot added size: XS and removed agents Agent runtime and tooling size: S labels Mar 20, 2026
@steipete steipete added duplicate This issue or pull request already exists close:duplicate Closed as duplicate labels Apr 26, 2026
@steipete

Copy link
Copy Markdown
Contributor

Thanks for documenting the isolated-cron updatedAt vector. I’m closing this as superseded by #71845.

What changed in the replacement:

  • daily/idle reset freshness no longer depends on updatedAt, so cron writes cannot mask a stale session by refreshing that bookkeeping field
  • cron session handling now preserves/sets lifecycle timestamps explicitly
  • lastInteractionAt is the idle-reset authority, while sessionStartedAt is the daily-reset authority
  • the fix covers the broader duplicate family: heartbeat, cron, exec, gateway-agent, and ordinary channel-session metadata writes
  • regression tests cover cron session freshness plus the shared session-store write behavior

This PR’s root diagnosis was correct, but the replacement removes updatedAt from the reset decision entirely instead of preserving it one write site at a time. Closing in favor of #71845.

@steipete steipete closed this Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

close:duplicate Closed as duplicate duplicate This issue or pull request already exists size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Isolated cron jobs with explicit sessionKey overwrite parent session's updatedAt, preventing daily reset

2 participants