openclaw - ✅(Solved) Fix [Bug]: openclaw status calls reconcileInspectableTasks twice — in-memory cache eliminates redundant O(n) clone+sort [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#73531Fetched 2026-04-29 06:18:39
View on GitHub
Comments
1
Participants
2
Timeline
4
Reactions
0
Timeline (top)
commented ×1cross-referenced ×1mentioned ×1subscribed ×1

openclaw status calls reconcileInspectableTasks() twice on every invocation — once via getInspectableTaskRegistrySummary() and again via getInspectableTaskAuditSummary(). Each call independently deep-clones and sorts the full task list (10K+ records). This doubles the task reconciliation cost for no benefit.

Root cause: getStatusSummary() in the status command calls both summary and audit helpers that share reconcileInspectableTasks() as a common prerequisite, but there is no caching layer between them.

Root Cause

Root cause: getStatusSummary() in the status command calls both summary and audit helpers that share reconcileInspectableTasks() as a common prerequisite, but there is no caching layer between them.

Fix Action

Fixed

PR fix notes

PR #73650: CLI/status: share reconciled task snapshot across registry and audit summaries

Description (problem / solution / changelog)

Summary

  • Problem: getStatusSummary() invokes getInspectableTaskRegistrySummary() and getInspectableTaskAuditSummary() separately, and each helper independently reconciles the task list (reconcileInspectableTasks() -> listTaskRecords() deep clone + sort). On nodes with 10K+ task records the duplicated clone/sort adds roughly 0.7-1.2 s of warm overhead per openclaw status.
  • Why it matters: openclaw status is the canonical operator probe and is used inside CI/cron health checks. Doubled latency makes status the slowest interactive CLI on busy nodes (#73531 reports 8-13 s cold, 5-9 s warm, half of which is the avoidable second reconcile).
  • What changed: extend the two summary helpers in src/tasks/task-registry.maintenance.ts to accept an optional pre-reconciled TaskRecord[] snapshot, and update src/commands/status.summary.ts to call reconcileInspectableTasks() once and pass the same snapshot to both helpers.
  • What did NOT change (scope boundary): all other call sites of the two helpers (src/commands/tasks.ts:505/513/514, src/gateway/server.impl.ts:397, src/gateway/server-reload-handlers.ts:149) keep their existing zero-arg behavior. The optional parameter is fully backwards compatible. No behavioral changes outside getStatusSummary().

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #73531
  • Related: N/A
  • This PR fixes a bug or regression

Root Cause

  • Root cause: getInspectableTaskRegistrySummary() and getInspectableTaskAuditSummary() each call reconcileInspectableTasks() internally. getStatusSummary() calls both, so the same expensive listTaskRecords() clone+sort runs twice per status call.
  • Missing detection / guardrail: there was no test asserting that one getStatusSummary() invocation reconciles tasks once. Both helpers were independently exercised but never measured together.
  • Contributing context: warm reconcile takes 327-407 ms on a moderately active 10K-task node (per #73531 profiling), so the duplicate cost is operator-visible but easy to overlook when sized against the rest of status work.

Regression Test Plan

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    • src/tasks/task-registry.maintenance.issue-73531.test.ts (new): instruments listTaskRecords via the existing maintenance-runtime harness and asserts a shared snapshot reconciles tasks exactly once across both helpers; second case locks the zero-arg fallback contract.
    • src/commands/status.summary.test.ts: new case "reconciles inspectable tasks once and shares the snapshot across summaries" asserts reconcileInspectableTasks is called exactly once per getStatusSummary() and that both helpers receive the same array reference.
  • Scenario the test should lock in: any future change that reintroduces an independent reconcile inside the status path will fail both tests.
  • Why this is the smallest reliable guardrail: the unit test pins the helper API contract, and the integration test pins the call-site behavior. Together they cover the regression at both layers without requiring an end-to-end status invocation.

User-visible / Behavior Changes

openclaw status and openclaw status --json complete faster on hosts with large task ledgers. No flag, config, or output-shape changes. Nothing else changes.

Diagram

Before (per `openclaw status`):
  reconcileInspectableTasks()  ->  listTaskRecords() clone+sort  (327-407 ms warm)
  reconcileInspectableTasks()  ->  listTaskRecords() clone+sort  (327-407 ms warm) <- redundant

After:
  reconcileInspectableTasks()  ->  listTaskRecords() clone+sort  (327-407 ms warm)
                                      \-> registry summary
                                      \-> audit summary

Security Impact

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Linux 6.8.0 (CT 112 prbuild sandbox: Ubuntu 24.04, 4 cores, 6 GB RAM)
  • Runtime/container: Node 22, OpenClaw 2026.4.27
  • Model/provider: N/A (CLI-only path)
  • Integration/channel: N/A
  • Relevant config: default

Steps

  1. pnpm test src/tasks/task-registry.maintenance.issue-73531.test.ts -> 2 tests pass.
  2. pnpm test src/commands/status.summary.test.ts -> 5 tests pass (existing 4 + new shared-snapshot assertion).
  3. pnpm test src/tasks/task-registry.test.ts src/tasks/task-registry.maintenance.issue-60299.test.ts src/commands/status.test.ts src/commands/tasks.test.ts src/gateway/server.reload.test.ts src/gateway/server-methods/server-methods.test.ts -> 168 tests pass; no regressions in any helper consumer.

Expected

reconcileInspectableTasks() called exactly once per getStatusSummary(); both helpers receive the same array reference.

Actual

Matches expected. RED phase confirmed: against unmodified source the new helper test reports expected 3 to be 1 (one explicit reconcile + two helpers each reconciling internally).

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Perf numbers from #73531 reporter: warm reconcileInspectableTasks() is 327-407 ms on a 10K-task node, cold is 3.6-5.9 s. Removing the second invocation reclaims one warm cycle per status call.

Human Verification

  • Verified scenarios:
    • TDD red->green on the new helper test (red = listCalls 3, green = listCalls 1).
    • Existing zero-arg call sites in tasks.ts, server.impl.ts, server-reload-handlers.ts unchanged at runtime; their tests still pass.
    • Integration test in status.summary.test.ts confirms reference-equality of the shared snapshot.
    • pnpm tsgo (core) and pnpm check:test-types (core+extensions test types) both clean.
    • pnpm exec oxlint --type-aware and pnpm exec oxfmt --check clean on touched files.
  • Edge cases checked:
    • Optional parameter omitted -> falls back to internal reconcileInspectableTasks() (covered by the second test in the new file).
    • tasks.ts --apply flow still calls the audit helper twice (before + after maintenance), intentionally on different snapshots; left untouched.
  • What you did not verify: live openclaw status end-to-end timing on a real 10K-task node. The regression tests lock the call-count contract; the perf claim is grounded in the issue's profiling data.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: the new optional parameter could mislead third-party callers into passing a non-reconciled TaskRecord[].
    • Mitigation: parameter is named reconciledTasks to document the contract; helpers fall back to reconcileInspectableTasks() when the parameter is omitted, so the safe default is preserved.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/commands/status.summary.test.ts (modified, +28/-4)
  • src/commands/status.summary.ts (modified, +6/-2)
  • src/tasks/task-registry.maintenance.issue-73531.test.ts (added, +130/-0)
  • src/tasks/task-registry.maintenance.ts (modified, +6/-4)

Code Example

let t = Date.now();
const r1 = reconcileInspectableTasks();
console.log("First call:", Date.now() - t, "ms, len:", r1.length);

t = Date.now();
const r2 = reconcileInspectableTasks();
console.log("Second call:", Date.now() - t, "ms, same array?", r1 === r2);
// => false — complete re-clone and re-sort

---

function listTaskRecords() {
  ensureTaskRegistryReady();
  return [...tasks.values()]
    .map((task, insertionIndex) => Object.assign({}, cloneTaskRecord(task), { insertionIndex }))
    .toSorted(compareTasksNewestFirst)
    .map(({ insertionIndex: _, ...task }) => task);
}
RAW_BUFFERClick to expand / collapse

Bug type

Performance / latency bug

Summary

openclaw status calls reconcileInspectableTasks() twice on every invocation — once via getInspectableTaskRegistrySummary() and again via getInspectableTaskAuditSummary(). Each call independently deep-clones and sorts the full task list (10K+ records). This doubles the task reconciliation cost for no benefit.

Root cause: getStatusSummary() in the status command calls both summary and audit helpers that share reconcileInspectableTasks() as a common prerequisite, but there is no caching layer between them.

Environment

  • OpenClaw version: 2026.4.26 (npm global, Linux)
  • OS: Linux 6.8.0 (x86_64)
  • Install method: npm global
  • Task count: ~10,418 records on a moderately active node

Reproduction

  1. Run openclaw status on a node with 10K+ task records
  2. Observe 8-13 second latency

The double call can be measured directly:

let t = Date.now();
const r1 = reconcileInspectableTasks();
console.log("First call:", Date.now() - t, "ms, len:", r1.length);

t = Date.now();
const r2 = reconcileInspectableTasks();
console.log("Second call:", Date.now() - t, "ms, same array?", r1 === r2);
// => false — complete re-clone and re-sort

Profiling data

All measurements against ~10,418 task records on the npm global install:

CallCold (first run)Warm (subsequent)
reconcileInspectableTasks() #13,653 – 5,914 ms327 – 407 ms
reconcileInspectableTasks() #23,600+ ms327 – 407 ms (identical cost!)
getInspectableTaskRegistrySummary~577 ms~577 ms (contains inner reconcile)
getInspectableTaskAuditSummary~362 ms~362 ms (contains inner reconcile)

The hot path through listTaskRecords() is:

function listTaskRecords() {
  ensureTaskRegistryReady();
  return [...tasks.values()]
    .map((task, insertionIndex) => Object.assign({}, cloneTaskRecord(task), { insertionIndex }))
    .toSorted(compareTasksNewestFirst)
    .map(({ insertionIndex: _, ...task }) => task);
}

This is a 4-pass pipeline:

  1. Spread 10K+ Map entries into an array
  2. Deep-clone + annotate with insertionIndex
  3. Sort (O(n log n) comparison sort, ~161ms)
  4. Strip insertionIndex

Running this twice independently is entirely wasted work — the result is identical between calls within a single status command execution.

Observed openclark status total latency ranges

CommandBefore fix
openclaw status8.0 – 13.0 s
openclaw status --json5.3 – 9.6 s

The double-reconcile accounts for roughly 0.7 – 1.2s of warm overhead, with the remaining time distributed across config loading, session scan, agent status resolution, and plugin compatibility diagnostics (tracked separately in #69103).

Expected behavior

A second call to reconcileInspectableTasks() within the same process lifetime (absent any intervening task mutations) should return the identical result immediately — ideally in 0ms via an in-memory cache.

Suggested fix

  1. Add a module-level cache variable (e.g. reconcileCache: Array | null) and a dirty flag (reconcileCacheDirty: boolean)
  2. Guard reconcileInspectableTasks(): if cache is clean, return cached result directly
  3. Invalidate on mutation: wire cache invalidation into all paths that modify the task registry (create, update, delete, sweep). The existing task registry observer/subscription infrastructure (setTaskRegistryObserver/emitTaskRegistryObserverEvent) is the cleanest approach — subscribe to "upserted" and "deleted" events to set the dirty flag.
  4. Invalidate on configureTaskRegistryMaintenance as well, since changing the maintenance config (e.g. cron store path) can change reconcile behavior.

Fix verification

After applying the cache (on the same 10,418-record dataset):

CallBeforeAfter
reconcileInspectableTasks() #13,859 ms cold3,859 ms cold (unchanged)
reconcileInspectableTasks() #2407 ms warm0 ms (cached, returns same reference)
getInspectableTaskRegistrySummary~577 ms5 ms
getInspectableTaskAuditSummary~362 ms8 ms
Summary + audit combined~1,150 ms13 ms (~99% reduction)
openclaw status --json total~5.3 – 9.6 s~5.3 s (the other ~5.3s is config/session loading)

The second reconcileInspectableTasks() drops from 407ms → 0ms (100% reduction). The two summary/audit helpers drop from ~939ms combined → ~13ms combined.

Related issues

  • #64004 "Control UI remains slow although sessions.list returns quickly" — different root cause but same symptom cluster
  • #57715 "[Performance] sessions.list slow" — also about O(N²) in a different code path
  • #69103 "[Bug]: openclaw status spends ~26s in plugin compatibility diagnostics" — same command, different bottleneck (already closed/resolved)

Additional context

The same cache can optionally be reused by any other consumer of reconcileInspectableTasks() within the same process tick (e.g. getReconciledTaskById when called during status). Cache is invalidated on any task upserted or deleted event via the existing observer subscription API.

This is purely an in-process optimization — no persistence, no TTL, no cross-process sharing. The implementation is ~15 lines of cache logic plus the observer subscription in configureTaskRegistryMaintenance.

extent analysis

TL;DR

Implement a caching mechanism for reconcileInspectableTasks() to avoid redundant computations and reduce latency.

Guidance

  1. Add a cache variable: Introduce a module-level cache variable (e.g., reconcileCache) to store the result of reconcileInspectableTasks().
  2. Guard the function: Modify reconcileInspectableTasks() to return the cached result if it exists and is not dirty.
  3. Invalidate the cache: Set up a mechanism to invalidate the cache when tasks are modified (created, updated, deleted) or when the task registry maintenance configuration changes.
  4. Verify the fix: Measure the performance improvement by comparing the latency of reconcileInspectableTasks() and related functions before and after the caching implementation.

Example

let reconcileCache = null;
let reconcileCacheDirty = true;

function reconcileInspectableTasks() {
  if (!reconcileCacheDirty && reconcileCache) {
    return reconcileCache;
  }
  // Compute and cache the result
  const result = computeReconcileResult();
  reconcileCache = result;
  reconcileCacheDirty = false;
  return result;
}

// Invalidate the cache on task modifications or config changes
function onTaskModified() {
  reconcileCacheDirty = true;
}

Notes

The proposed solution focuses on caching the result of reconcileInspectableTasks() to avoid redundant computations. This approach assumes that the result of the function does not change frequently and that the cache can be safely reused within the same process tick.

Recommendation

Apply the workaround by implementing the caching mechanism as described, which should significantly reduce the latency of openclaw status commands.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

A second call to reconcileInspectableTasks() within the same process lifetime (absent any intervening task mutations) should return the identical result immediately — ideally in 0ms via an in-memory cache.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: openclaw status calls reconcileInspectableTasks twice — in-memory cache eliminates redundant O(n) clone+sort [1 pull requests, 1 comments, 2 participants]