1 Critical Finding 2 Cascading Effects ARK Diagnostic Report

OpenClaw #114234 — Usage-Cost Refresh Lock Never Releasable Under Container PID Reuse

Generated by ARK (Agent Reliability Kit) · 2026-07-28 · Target: OpenClaw 2026.5.28 (current main affected), Debian container on Kubernetes, Node 24 · Original issue

1 · Symptom Summary

IDSeveritySymptom
F-1CriticalUsage-cost refresh lock orphaned by an unclean gateway exit is honoured forever when the restarted gateway reoccupies the same PID (e.g. PID 9 under tini). Cache frozen for 12 days.
F-2CascadingFrozen cache forces every consumer to recompute from transcripts; recompute set grows ~288 transcripts/day without bound (224 MB re-parsed per call at time of report).
F-3CascadingGrowing recompute pushes isolated-cron setup past the fixed 60s watchdog — failure rate climbed 0% → 2% → 20.8% → 37.9% over 13 days, masquerading as the unrelated symptom in #82662.

2 · Root Cause Analysis

F-1: PID liveness is not lock ownership

The lock file records {pid, startedAt, token}, but staleness is judged with isProcessRunning(lock.pid) alone. That test answers "is some process alive at this PID?" — not "is the process that wrote this lock alive?". In any environment where PIDs are stable across restarts (containers under tini, minimal PID namespaces, supervisors that respawn into low PIDs), the two questions have different answers, and the code conflates them:

lock payload owner : pid=9  startedAt=2026-07-15 10:35:21 CST
ACTUAL pid 9 start : 2026-07-24 12:58:39 CST   # container restarted; PID reused
delta              : 9.10 days                 # lock still honoured

The bitter irony: parseRefreshLock() already parses and validates startedAt, and the writer already records it. The one comparison that would detect reuse — recorded startedAt vs. actual process start time — is simply never made. This is a classic identity-vs-address bug: a PID is an address, not an identity.

F-2 / F-3: Silent degradation, loud failure elsewhere

The frozen cache never throws. It degrades every consumer into a full transcript recompute whose cost grows daily, until an unrelated fixed timeout (the 60s cron setup watchdog) starts firing. The error message points at cron, not at the lock — which is why the reporter's failure-rate table is such a valuable forensic artifact: the very first failure is the next scheduled run after the lock appeared.

3 · Fix Plan (with code)

F-1 → Compare identity, not just liveness

// staleness check: the lock's startedAt must be >= the current
// occupant's process start time, else the writer is a dead incarnation
function isLockLive(lock) {
  if (!isProcessRunning(lock.pid)) return false;
  const procStart = getProcessStartTime(lock.pid);   // /proc/<pid>/stat field 22
  if (procStart > lock.startedAt) return false;      // PID reused by a newer process
  return true;
}

Alternative shapes, strongest first:

Candidate upstream fix

PR #114254 (by the issue reporter) targets exactly this ownership-comparison route and is the canonical candidate per ClawSweeper's review. The fix shape matches what the on-disk lock format already records — no new state needed.

4 · Stopgap Checklist (until patched)

5 · Prevention — the ARK pattern

This failure chain belongs to classes ARK guards against in agent systems:

FindingFailure classARK guard
F-1Ownership proven by address, not identityIdempotencyGuard — bind every lock/handle to an incarnation token, not a reusable ID
F-2Silent unbounded work growthOutputValidator — verify cache freshness after mutation; stale reads are failures, not fallbacks
F-3Fixed timeout meets growing workloadCircuitBreaker — surface latency-budget breaches at the source, before a downstream watchdog misattributes them
pip install ark-trust        # Python
npm install @feilunxitong/arkit   # TypeScript

Does your agent have hidden crash risks like these?

Idempotency guards · retry policy · logging — free 30-second check.

Run the free ARK diagnostic →