Skip to content

Add time-based session maintenance modes#56575

Closed
JeremyGuo wants to merge 3 commits into
openclaw:mainfrom
JeremyGuo:codex/time-based-session-maintenance
Closed

Add time-based session maintenance modes#56575
JeremyGuo wants to merge 3 commits into
openclaw:mainfrom
JeremyGuo:codex/time-based-session-maintenance

Conversation

@JeremyGuo

Copy link
Copy Markdown

Summary

  • add time-based session maintenance settings using sessionCacheTtl plus timeBasedContextCompact
  • support explicit compact and reset maintenance modes instead of implicitly coupling both behaviors together
  • track session idle/cache timestamps and run gateway maintenance sweeps to apply the selected policy

Why

This change adds two time-based session maintenance workflows for long-lived sessions:

  1. compact

    • Purpose: reduce the chance of a cold cache miss after a long idle period by proactively compacting an idle session shortly before the configured timeout.
    • Good fit: long research or coding threads where the user may come back later and continue from the same context.
  2. reset

    • Purpose: aggressively clear stale sessions once they have been idle past the configured timeout instead of carrying old context forward.
    • Good fit: workflows where stale context is more harmful than helpful, or where keeping expired sessions around only increases cost and surprise.

The new behavior is controlled explicitly per model:

  • sessionCacheTtl defines the timeout window
  • timeBasedContextCompact chooses the maintenance algorithm: none, compact, or reset

cacheRetention remains a model prompt-cache setting and is no longer used to enable or disable these new maintenance features.

Implementation notes

  • keep Anthropic API-key smart defaults at cacheRetention: "short"
  • add sessionCacheTtl: "1h" as a defaulted timeout value without auto-enabling maintenance mode
  • warn when sessionCacheTtl exceeds an explicitly configured model cache TTL
  • record lastUserMessageAt, lastAssistantMessageAt, lastCacheTouchAt, and lastIdleCompactionForCacheTouchAt in session state
  • run the maintenance sweep from gateway server maintenance without overlapping runs

Validation

  • pnpm exec vitest run src/gateway/session-cache-maintenance.test.ts src/gateway/server-maintenance.test.ts src/config/config.pruning-defaults.test.ts src/agents/pi-embedded-runner/cache-ttl.test.ts src/agents/pi-embedded-runner/run/attempt.spawn-workspace.cache-ttl.test.ts src/agents/pi-embedded-runner/extra-params.cache-retention-default.test.ts src/commands/agent/session-store.test.ts
  • NODE_OPTIONS=--max-old-space-size=8192 pnpm exec tsc -p tsconfig.json --noEmit

Copilot AI review requested due to automatic review settings March 28, 2026 19:13
@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime agents Agent runtime and tooling size: L labels Mar 28, 2026

Copilot AI 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.

Pull request overview

Adds configurable, time-based session maintenance workflows (idle compaction “cache warming” and idle reset) driven by per-model TTL settings, and wires a periodic maintenance sweep into the gateway so long-lived sessions can be compacted or reset based on inactivity.

Changes:

  • Introduces sessionCacheTtl + timeBasedContextCompact (none/compact/reset) and resolves them from model extra params.
  • Records new session timing fields (lastUserMessageAt, lastAssistantMessageAt, lastCacheTouchAt, lastIdleCompactionForCacheTouchAt) and updates them during runs.
  • Adds a non-overlapping gateway maintenance sweep that applies the selected reset/compact policy and includes new unit tests.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/gateway/session-cache-maintenance.ts Implements policy resolution + sweep logic for idle compaction/reset.
src/gateway/session-cache-maintenance.test.ts Unit tests for idle compaction/reset decision helpers.
src/gateway/server-maintenance.ts Schedules maintenance sweep with an in-flight guard to prevent overlap.
src/config/sessions/types.ts Adds persisted session timing fields used by the new maintenance logic.
src/config/defaults.ts Defaults model params for cacheRetention and sessionCacheTtl.
src/config/config.pruning-defaults.test.ts Verifies new pruning defaults behavior and non-enablement of mode by default.
src/auto-reply/reply/session.ts Initializes lastUserMessageAt and clears timing fields for new sessions.
src/auto-reply/reply/session-usage.ts Updates assistant/cache-touch timestamps when assistant activity is recorded.
src/auto-reply/reply/followup-runner.ts Marks followup runs as assistant activity for timestamping.
src/auto-reply/reply/agent-runner.ts Avoids recording assistant activity for heartbeat runs.
src/agents/pi-embedded-runner/cache-ttl.ts Adds TTL/mode resolution (resolveCacheTtlMs, resolveTimeBasedContextCompactMode) + mismatch warning.
src/agents/pi-embedded-runner/cache-ttl.test.ts Adds tests for TTL resolution and mismatch warning behavior.
src/agents/command/session-store.ts Records assistant/cache-touch timestamps after agent runs.
src/agents/agent-command.ts Records user-turn timestamp when persisting command overrides.

Comment on lines +233 to +258
const patch: Partial<SessionEntry> = {
lastIdleCompactionForCacheTouchAt: cacheTouchAt,
};
if (result.ok && result.compacted) {
patch.compactionCount = (params.entry.compactionCount ?? 0) + 1;
patch.lastCacheTouchAt = params.now;
if (typeof result.result?.tokensAfter === "number" && result.result.tokensAfter > 0) {
patch.totalTokens = result.result.tokensAfter;
patch.totalTokensFresh = true;
}
log.info("idle cache-warming compaction completed", {
sessionKey: params.sessionKey,
provider,
model,
tokensBefore: params.totalTokens,
tokensAfter: result.result?.tokensAfter,
});
} else {
log.info("idle cache-warming compaction skipped", {
sessionKey: params.sessionKey,
provider,
model,
reason: result.reason ?? "not_compacted",
tokens: params.totalTokens,
});
}

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

lastIdleCompactionForCacheTouchAt is persisted even when compactEmbeddedPiSession returns ok: false (error case). That marks the cache-touch as “serviced” and prevents any retry on subsequent sweeps, so transient compaction failures would permanently disable cache-warming for that idle window. Consider only setting lastIdleCompactionForCacheTouchAt when the compaction attempt completed successfully (result.ok === true), or otherwise recording a separate failure timestamp/backoff while still allowing retries.

Copilot uses AI. Check for mistakes.
Comment on lines +319 to +335
const totalTokens = estimateSessionTotalTokens({ entry, target });
if (
shouldRunIdleCacheCompaction({
entry,
policy,
now,
totalTokens,
})
) {
await maybeCompactIdleSession({
cfg,
entry,
sessionKey,
target,
now,
totalTokens: totalTokens ?? 0,
});

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

estimateSessionTotalTokens() is called for every session that reaches this point, even when the policy is reset (where tokens are never used). Because the estimator can read and tokenize the session transcript from disk, this can add significant per-minute overhead. Move token estimation behind a policy.mode === "compact" check (and ideally after the cheap time/window checks) so transcripts are only read when an idle compaction is actually possible.

Suggested change
const totalTokens = estimateSessionTotalTokens({ entry, target });
if (
shouldRunIdleCacheCompaction({
entry,
policy,
now,
totalTokens,
})
) {
await maybeCompactIdleSession({
cfg,
entry,
sessionKey,
target,
now,
totalTokens: totalTokens ?? 0,
});
if (policy.mode === "compact") {
const totalTokens = estimateSessionTotalTokens({ entry, target });
if (
shouldRunIdleCacheCompaction({
entry,
policy,
now,
totalTokens,
})
) {
await maybeCompactIdleSession({
cfg,
entry,
sessionKey,
target,
now,
totalTokens: totalTokens ?? 0,
});
}

Copilot uses AI. Check for mistakes.
return false;
}
const lastUser = resolvePositiveTimestamp(entry.lastUserMessageAt);
return lastUser == null || lastAssistant > lastUser;

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

isAwaitingUserReply uses lastAssistant > lastUser to determine whether the session is idle. Several call sites write these timestamps with independent Date.now() calls; if both values land in the same millisecond, the equality case will be treated as “not awaiting user reply” and will prevent reset/compaction from ever running for that session. Consider using >= here (or otherwise ensuring monotonic timestamps when recording user/assistant turns).

Suggested change
return lastUser == null || lastAssistant > lastUser;
return lastUser == null || lastAssistant >= lastUser;

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +23
const warnedSessionCacheTtlMismatchKeys = new Set<string>();

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

warnedSessionCacheTtlMismatchKeys is a process-global Set that grows for every unique mismatch tuple (provider/model/sessionCacheTtl/explicitTtl) and never evicts entries. In long-lived gateway processes with many models/agents/config changes, this can become an unbounded memory leak. Consider bounding this structure (e.g., size cap + clear, TTL-based eviction, or keying only on provider/model to reduce cardinality).

Suggested change
const warnedSessionCacheTtlMismatchKeys = new Set<string>();
const MAX_WARNED_SESSION_CACHE_TTL_MISMATCH_KEYS = 1000;
class BoundedStringSet extends Set<string> {
private readonly maxSize: number;
constructor(maxSize: number) {
super();
this.maxSize = maxSize;
}
override add(value: string): this {
if (this.maxSize > 0 && this.size >= this.maxSize) {
// Prevent unbounded growth in long-lived processes by clearing
// previously recorded mismatch keys once the cap is reached.
this.clear();
}
return super.add(value);
}
}
const warnedSessionCacheTtlMismatchKeys = new BoundedStringSet(
MAX_WARNED_SESSION_CACHE_TTL_MISMATCH_KEYS,
);

Copilot uses AI. Check for mistakes.
Comment on lines 69 to +84
...entry,
sessionId,
updatedAt: Date.now(),
lastAssistantMessageAt: Date.now(),
contextTokens,
};
const cacheTtlMs = resolveCacheTtlMs({
config: cfg,
provider: providerUsed,
modelId: modelUsed,
agentId: resolveAgentIdFromSessionKey(sessionKey),
});
if (cacheTtlMs != null) {
next.lastCacheTouchAt = next.updatedAt;
next.lastIdleCompactionForCacheTouchAt = undefined;
}

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

updateSessionStoreAfterAgentRun calls Date.now() multiple times while constructing next (for updatedAt and lastAssistantMessageAt) and then reuses next.updatedAt for lastCacheTouchAt. Using a single now value for all related timestamps avoids same-ms ordering edge cases and keeps the new time-based maintenance logic consistent.

Copilot uses AI. Check for mistakes.
Comment on lines +535 to +540
const next: SessionEntry = {
...entry,
sessionId,
updatedAt: Date.now(),
lastUserMessageAt: Date.now(),
};

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

This session-store patch uses multiple Date.now() calls (updatedAt and lastUserMessageAt). For the new idle-detection logic (lastAssistantMessageAt vs lastUserMessageAt), keeping turn timestamps derived from a single now value per command reduces equality/ordering edge cases.

Copilot uses AI. Check for mistakes.
skillsSnapshot: params.entry.skillsSnapshot,
provider,
model,
trigger: "manual",

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

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

maybeCompactIdleSession calls compactEmbeddedPiSession with trigger: "manual". In compactEmbeddedPiSession this implies force: true and uses the threshold compaction target, which will over-compact/force-compaction during the automated idle maintenance sweep. Use a non-manual trigger (e.g. "budget") or introduce an explicit "idle" trigger so maintenance compaction remains non-forced and uses the intended target.

Suggested change
trigger: "manual",
trigger: "budget",

Copilot uses AI. Check for mistakes.

@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: 2953e42e1c

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/auto-reply/reply/session.ts Outdated
...baseEntry,
sessionId,
updatedAt: Date.now(),
lastUserMessageAt: Date.now(),

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 Avoid marking heartbeat turns as user activity

lastUserMessageAt is now updated unconditionally during session initialization, but initSessionState is also used for heartbeat-triggered runs. In heartbeat flows, recordAssistantActivity is intentionally disabled, so this write can make lastUserMessageAt >= lastAssistantMessageAt, causing isAwaitingUserReply() to return false and preventing time-based reset/compact maintenance from running for otherwise idle sessions.

Useful? React with 👍 / 👎.

Comment on lines +233 to +235
const patch: Partial<SessionEntry> = {
lastIdleCompactionForCacheTouchAt: cacheTouchAt,
};

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 Only mark idle compaction window serviced on success

maybeCompactIdleSession sets lastIdleCompactionForCacheTouchAt before checking whether compaction succeeded. If compaction fails or is skipped for a transient reason, this still marks the current cache-touch window as handled, and shouldRunIdleCacheCompaction() will suppress all retries for that window, so cache-warming can be permanently skipped until another assistant turn happens.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds two time-based session maintenance modes (compact and reset) controlled by sessionCacheTtl and timeBasedContextCompact per-model params. It tracks session activity timestamps (lastUserMessageAt, lastAssistantMessageAt, lastCacheTouchAt) and runs a maintenance sweep from the gateway server every minute, skipping active sessions and guarding against overlapping runs.

Key changes:

  • New resolveCacheTtlMs / resolveTimeBasedContextCompactMode helpers in cache-ttl.ts decode the new params and validate against cacheRetention-derived TTL.
  • session-cache-maintenance.ts implements shouldRunIdleCacheCompaction and shouldResetExpiredSession predicates, plus the sweep orchestration.
  • defaults.ts now also injects a default sessionCacheTtl: \"1h\" for Anthropic models, without enabling maintenance mode (timeBasedContextCompact must still be set explicitly).
  • Session tracking is recorded in updateSessionStoreAfterAgentRun, persistSessionUsageUpdate, initSessionState, and agentCommandInternal.

Issue found: The mock for resolveProviderCacheTtlEligibility in cache-ttl.test.ts was not updated to match the changed test expectations for moonshot. The mock still returns true for moonshot (lines 10–11), but two test cases now assert false (lines 31, 36). These tests will fail as written.

Confidence Score: 4/5

Not safe to merge until the test mock is fixed — the test suite will have failing tests as-is.

The implementation logic for session maintenance is solid and well-tested in session-cache-maintenance.test.ts. However, the isCacheTtlEligibleProvider mock in cache-ttl.test.ts was not updated to match the changed moonshot expectations, meaning two test cases are broken and the stated validation claim cannot hold. This is a concrete, verifiable test failure that blocks merge.

src/agents/pi-embedded-runner/cache-ttl.test.ts — the vi.mock at the top of the file needs to be updated so that moonshot no longer returns true, aligning the mock with the new expectations on lines 31 and 36.

Important Files Changed

Filename Overview
src/agents/pi-embedded-runner/cache-ttl.test.ts Test expectations for moonshot eligibility changed to false, but the mock still returns true for moonshot — two tests will fail.
src/gateway/session-cache-maintenance.ts New file implementing idle-compaction and expired-session-reset sweep logic; guard against overlapping runs and active sessions is sound.
src/agents/pi-embedded-runner/cache-ttl.ts Adds resolveCacheTtlMs, resolveTimeBasedContextCompactMode, and supporting helpers; logic is correct and well-guarded.
src/config/defaults.ts Correctly extends pruning defaults to also set sessionCacheTtl: '1h' alongside cacheRetention: 'short', with independent mutation guards.
src/auto-reply/reply/session-usage.ts Correctly gates lastAssistantMessageAt and lastCacheTouchAt tracking behind recordAssistantActivity, using a single Date.now() snapshot.
src/gateway/server-maintenance.ts Adds session-cache maintenance sweep inside the dedupe-cleanup interval with a correct in-flight guard to prevent overlapping sweeps.
src/agents/agent-command.ts Sets lastUserMessageAt via a second Date.now() call instead of reusing the same timestamp as updatedAt.
src/auto-reply/reply/session.ts Sets lastUserMessageAt via a second Date.now() call at line 474, same minor timestamp-drift issue as agent-command.ts.
src/config/sessions/types.ts Adds four new optional timestamp fields with clear JSDoc comments.

Comments Outside Diff (1)

  1. src/agents/pi-embedded-runner/cache-ttl.test.ts, line 10-37 (link)

    P1 Mock not updated to match new expectations — tests will fail

    The mock for resolveProviderCacheTtlEligibility still returns true for moonshot:

    if (params.context.provider === "moonshot" || params.context.provider === "zai") {
      return true;
    }

    But the two test cases now expect isCacheTtlEligibleProvider("moonshot", "kimi-k2.5") and isCacheTtlEligibleProvider("Moonshot", "Kimi-K2.5") to return false. Because isCacheTtlEligibleProvider delegates directly to resolveProviderCacheTtlEligibility and returns its result when it is not undefined, the mock value of true is what is returned — not false. Both affected tests will fail at runtime:

    • "uses provider-specific native eligibility rules" (line 31)
    • "normalizes native eligibility rules case-insensitively" (line 36)

    If the intent is that moonshot is no longer a directly eligible provider, the mock needs to be updated to return a falsy/undefined value for moonshot (while still returning true for zai). For example:

    vi.mock("../../plugins/provider-runtime.js", () => ({
      resolveProviderCacheTtlEligibility: (params: {
        context: { provider: string; modelId: string };
      }) => {
        if (params.context.provider === "anthropic") {
          return true;
        }
        if (params.context.provider === "zai") {
          return true;
        }
        if (params.context.provider === "openrouter") {
          return ["anthropic/", "moonshot/", "moonshotai/", "zai/"].some((prefix) =>
            params.context.modelId.startsWith(prefix),
          );
        }
        return undefined;
      },
    }));

    Also worth checking whether the underlying resolveProviderCacheTtlEligibility plugin implementation (in plugins/provider-runtime.js, not in this PR) already reflects the new moonshot exclusion, or whether that change is still pending.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/agents/pi-embedded-runner/cache-ttl.test.ts
    Line: 10-37
    
    Comment:
    **Mock not updated to match new expectations — tests will fail**
    
    The mock for `resolveProviderCacheTtlEligibility` still returns `true` for `moonshot`:
    
    ```typescript
    if (params.context.provider === "moonshot" || params.context.provider === "zai") {
      return true;
    }
    ```
    
    But the two test cases now expect `isCacheTtlEligibleProvider("moonshot", "kimi-k2.5")` and `isCacheTtlEligibleProvider("Moonshot", "Kimi-K2.5")` to return `false`. Because `isCacheTtlEligibleProvider` delegates directly to `resolveProviderCacheTtlEligibility` and returns its result when it is not `undefined`, the mock value of `true` is what is returned — not `false`. Both affected tests will fail at runtime:
    
    - `"uses provider-specific native eligibility rules"` (line 31)
    - `"normalizes native eligibility rules case-insensitively"` (line 36)
    
    If the intent is that `moonshot` is no longer a directly eligible provider, the mock needs to be updated to return a falsy/`undefined` value for moonshot (while still returning `true` for `zai`). For example:
    
    ```typescript
    vi.mock("../../plugins/provider-runtime.js", () => ({
      resolveProviderCacheTtlEligibility: (params: {
        context: { provider: string; modelId: string };
      }) => {
        if (params.context.provider === "anthropic") {
          return true;
        }
        if (params.context.provider === "zai") {
          return true;
        }
        if (params.context.provider === "openrouter") {
          return ["anthropic/", "moonshot/", "moonshotai/", "zai/"].some((prefix) =>
            params.context.modelId.startsWith(prefix),
          );
        }
        return undefined;
      },
    }));
    ```
    
    Also worth checking whether the underlying `resolveProviderCacheTtlEligibility` plugin implementation (in `plugins/provider-runtime.js`, not in this PR) already reflects the new moonshot exclusion, or whether that change is still pending.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/cache-ttl.test.ts
Line: 10-37

Comment:
**Mock not updated to match new expectations — tests will fail**

The mock for `resolveProviderCacheTtlEligibility` still returns `true` for `moonshot`:

```typescript
if (params.context.provider === "moonshot" || params.context.provider === "zai") {
  return true;
}
```

But the two test cases now expect `isCacheTtlEligibleProvider("moonshot", "kimi-k2.5")` and `isCacheTtlEligibleProvider("Moonshot", "Kimi-K2.5")` to return `false`. Because `isCacheTtlEligibleProvider` delegates directly to `resolveProviderCacheTtlEligibility` and returns its result when it is not `undefined`, the mock value of `true` is what is returned — not `false`. Both affected tests will fail at runtime:

- `"uses provider-specific native eligibility rules"` (line 31)
- `"normalizes native eligibility rules case-insensitively"` (line 36)

If the intent is that `moonshot` is no longer a directly eligible provider, the mock needs to be updated to return a falsy/`undefined` value for moonshot (while still returning `true` for `zai`). For example:

```typescript
vi.mock("../../plugins/provider-runtime.js", () => ({
  resolveProviderCacheTtlEligibility: (params: {
    context: { provider: string; modelId: string };
  }) => {
    if (params.context.provider === "anthropic") {
      return true;
    }
    if (params.context.provider === "zai") {
      return true;
    }
    if (params.context.provider === "openrouter") {
      return ["anthropic/", "moonshot/", "moonshotai/", "zai/"].some((prefix) =>
        params.context.modelId.startsWith(prefix),
      );
    }
    return undefined;
  },
}));
```

Also worth checking whether the underlying `resolveProviderCacheTtlEligibility` plugin implementation (in `plugins/provider-runtime.js`, not in this PR) already reflects the new moonshot exclusion, or whether that change is still pending.

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

---

This is a comment left during a code review.
Path: src/agents/agent-command.ts
Line: 538-539

Comment:
**Two separate `Date.now()` calls can drift apart**

`updatedAt` and `lastUserMessageAt` are set with two independent `Date.now()` calls. On a heavily loaded system these could differ by a millisecond or two, meaning `lastUserMessageAt > updatedAt` is possible in theory.

`session-usage.ts` (updated in this same PR) already uses the correct pattern of capturing `const now = Date.now()` once and reusing it. The same pattern is worth applying here and in `src/auto-reply/reply/session.ts` (line 473–474) where the same issue exists.

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

Reviews (1): Last reviewed commit: "feat(session): add time-based session ma..." | Re-trigger Greptile

Comment thread src/agents/agent-command.ts Outdated
Comment on lines +538 to +539
updatedAt: Date.now(),
lastUserMessageAt: Date.now(),

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 Two separate Date.now() calls can drift apart

updatedAt and lastUserMessageAt are set with two independent Date.now() calls. On a heavily loaded system these could differ by a millisecond or two, meaning lastUserMessageAt > updatedAt is possible in theory.

session-usage.ts (updated in this same PR) already uses the correct pattern of capturing const now = Date.now() once and reusing it. The same pattern is worth applying here and in src/auto-reply/reply/session.ts (line 473–474) where the same issue exists.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/agent-command.ts
Line: 538-539

Comment:
**Two separate `Date.now()` calls can drift apart**

`updatedAt` and `lastUserMessageAt` are set with two independent `Date.now()` calls. On a heavily loaded system these could differ by a millisecond or two, meaning `lastUserMessageAt > updatedAt` is possible in theory.

`session-usage.ts` (updated in this same PR) already uses the correct pattern of capturing `const now = Date.now()` once and reusing it. The same pattern is worth applying here and in `src/auto-reply/reply/session.ts` (line 473–474) where the same issue exists.

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: dcbebed44e

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +55 to +56
resolvePositiveTimestamp(entry.lastCacheTouchAt) ??
resolvePositiveTimestamp(entry.lastAssistantMessageAt)

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 Use newest activity as cache-touch baseline

resolveCacheTouchAt always prefers lastCacheTouchAt over lastAssistantMessageAt, but lastCacheTouchAt is only refreshed when resolveCacheTtlMs(...) is enabled in session write paths. If timeBasedContextCompact is disabled for a while and later re-enabled, an old lastCacheTouchAt can survive while assistant turns continue, so the sweep treats the session as already expired and may reset/compact immediately. Using the newer of the two timestamps avoids false expiry decisions.

Useful? React with 👍 / 👎.

sessionId,
updatedAt: Date.now(),
updatedAt,
lastUserMessageAt: params.isHeartbeat === true ? baseEntry?.lastUserMessageAt : updatedAt,

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 Avoid marking command-only turns as unanswered user input

This updates lastUserMessageAt for every non-heartbeat inbound turn, including directive-only commands that can return before any model run updates lastAssistantMessageAt (for example, immediate /status-style replies). In that case the session is left with lastUserMessageAt > lastAssistantMessageAt, so isAwaitingUserReply() remains false and time-based reset/compact maintenance is skipped until a later assistant model turn happens.

Useful? React with 👍 / 👎.

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

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +170 to +171
if (!isCacheTtlEligibleProvider(params.provider, params.modelId)) {
return null;

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 Apply reset TTL regardless of provider cache eligibility

resolveCacheTtlMs bails out before reading timeBasedContextCompact whenever isCacheTtlEligibleProvider(...) is false, and runSessionCacheMaintenanceSweep skips maintenance entirely when cacheTtlMs is null. That means a configured timeBasedContextCompact: "reset" policy silently never runs for non-cache-eligible providers (for example, providers without prompt-cache support), even though reset logic is provider-agnostic and only needs idle timestamps. This makes the new reset mode a no-op for those models.

Useful? React with 👍 / 👎.


for (const target of targets) {
const store = loadSessionStore(target.storePath);
for (const [sessionKey, entry] of Object.entries(store)) {

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 Deduplicate session maintenance by canonical session key

The sweep iterates raw Object.entries(store) keys and performs reset/compaction directly on each one. If a store still contains both canonical and legacy aliases for the same session key, the first alias can mutate/reset the canonical entry, but the later alias in this stale snapshot can still satisfy the old predicates and trigger another maintenance action in the same pass. Deduping by canonical key (or reloading before acting) avoids duplicate resets/compactions.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

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

Keep open for maintainer review: the branch is a plausible feature, but current main does not already provide these explicit reset/compact modes, and PR head still has session-state blockers, missing real behavior proof, and a config/API boundary conflict with existing cache-TTL pruning plus #62475.

Canonical path: Close this PR as superseded by #62179.

So I’m closing this here and keeping the remaining discussion on #62179.

Review details

Best possible solution:

Close this PR as superseded by #62179.

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

Yes for the review blockers: PR-head source inspection shows the reset provider gate, stale cache-touch baseline, raw alias iteration, command-only timestamp risk, and failing Moonshot test expectation. I did not run live compact/reset behavior in this read-only review.

Is this the best way to solve the issue?

No. The direction is plausible, but this implementation is not the safest merge path until maintainers choose the config/API boundary and the session-state defects are fixed.

Security review:

Security review cleared: No concrete security or supply-chain concern was found; the diff touches runtime TypeScript and tests without dependency, workflow, install, release, package, or secret-handling changes.

What I checked:

  • linked superseding PR: feat: expose prompt-cache runtime context to context engines #62179 (feat: expose prompt-cache runtime context to context engines) is merged at 2026-04-07T16:29:57Z.
  • cluster evidence: the durable review links that PR in the work cluster or recommended risk path.
  • no human follow-up: live comments and timeline hydrated by apply contain no non-automation activity after the ClawSweeper review.

Likely related people:

  • steipete: Commit 9f59ff3 introduced the current contextPruning.mode: "cache-ttl" path that this PR overlaps with. (role: introduced current adjacent behavior; confidence: high; commits: 9f59ff325b66; files: src/agents/pi-hooks/context-pruning/extension.ts, src/agents/pi-hooks/context-pruning/settings.ts, src/config/defaults.ts)
  • jalehman: Merged PR feat: expose prompt-cache runtime context to context engines #62179 exposed prompt-cache telemetry to context engines, which is directly adjacent to cache-aware maintenance decisions. (role: adjacent prompt-cache runtime-context owner; confidence: high; commits: e46e32b98c04; files: src/context-engine/types.ts, src/agents/pi-embedded-runner/run/attempt.ts)
  • Ayaan Zaidi: Commit f70ad92 recently fixed cache-TTL pruning behavior around thinking replay sanitization, making this person a useful routing signal for the existing pruning path. (role: recent cache-TTL pruning fixer; confidence: medium; commits: f70ad924a65d; files: src/agents/pi-hooks/context-pruning/extension.ts, src/agents/pi-hooks/context-pruning/pruner.ts)

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

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. labels May 19, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 19, 2026
@clawsweeper

clawsweeper Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat.

Where did the egg go?
  • The egg game starts only after the PR passes the real-behavior proof check.
  • Before that, no creature or rarity is rolled. The treat waits for real proof.
  • This is still just collectible flavor: proof affects review readiness, not creature quality.

@clawsweeper

clawsweeper Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

@clawsweeper clawsweeper Bot closed this May 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling gateway Gateway runtime merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: XL status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants