Skip to content

feat(openclaw): JSONL-backed retain queue for external API resilience#740

Merged
nicoloboschi merged 2 commits intovectorize-io:mainfrom
akhater:feat/openclaw-retain-queue
Apr 1, 2026
Merged

feat(openclaw): JSONL-backed retain queue for external API resilience#740
nicoloboschi merged 2 commits intovectorize-io:mainfrom
akhater:feat/openclaw-retain-queue

Conversation

@akhater
Copy link
Copy Markdown
Contributor

@akhater akhater commented Mar 28, 2026

Summary

  • When the external Hindsight API is unreachable, retain requests are now buffered in a local SQLite database (~/.openclaw/data/hindsight-retain-queue.db) instead of being silently lost
  • Queue automatically flushes when connectivity is restored — either on next successful retain or via a periodic timer (default: every 60s)
  • Queue persists across process restarts, so no memories are lost during outages or Azure App Service restarts
  • Only active in external API mode — local daemon mode handles its own persistence, so the queue is never created and better-sqlite3 is never loaded

New config options

Option Type Default Description
retainQueuePath string ~/.openclaw/data/hindsight-retain-queue.db SQLite DB path
retainQueueMaxAgeMs number -1 (forever) Max age for queued items
retainQueueFlushIntervalMs number 60000 (1 min) Periodic flush interval

How it works

  1. Retain HTTP call fails → request stored in SQLite with bank ID, full payload, metadata, and timestamp
  2. Next successful retain triggers a flush attempt for queued items
  3. Periodic timer also attempts flush every 60s
  4. On flush: items sent FIFO, removed on success, stops on first failure (API still down)
  5. On shutdown: queue closes cleanly, pending items preserved for next startup

Logging

  • API unreachable — retain queued (3 pending, bank: max-openclaw): connect ECONNREFUSED
  • Retain queue: 3 items pending from previous session, will flush shortly
  • Queue flush: 3 queued retains delivered, queue empty

Test plan

  • All 62 existing unit tests pass
  • Builds cleanly with TypeScript
  • better-sqlite3 lazy-loaded — daemon mode users unaffected
  • Integration test: stop API, send messages, restart API, verify queue flushes

🤖 Generated with Claude Code

Heads up: PR includes the Claude Code attribution line — just letting you know before you see it.

@akhater akhater force-pushed the feat/openclaw-retain-queue branch from cfadeb9 to b9a3bef Compare March 28, 2026 16:31
Copy link
Copy Markdown
Collaborator

@nicoloboschi nicoloboschi left a comment

Choose a reason for hiding this comment

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

Hey this might be a good idea, but let's use normal files, not SQLite. What we really need is to persist Json lines as payload to send once the API is active again

@akhater akhater changed the title feat(openclaw): SQLite-backed retain queue for external API resilience feat(openclaw): JSONL-backed retain queue for external API resilience Mar 28, 2026
@akhater
Copy link
Copy Markdown
Contributor Author

akhater commented Mar 28, 2026

Makes total sense — swapped to a simple JSONL file. No more SQLite, no native deps. Each line is the exact JSON payload that would've gone to the API. Same queue logic (enqueue on failure, periodic flush, FIFO delivery, max age cleanup). Should be in the latest push.

@akhater akhater requested a review from nicoloboschi March 29, 2026 19:29
@akhater
Copy link
Copy Markdown
Contributor Author

akhater commented Mar 29, 2026

@nicoloboschi Updated to use JSONL files as requested. Ready for re-review!

Copy link
Copy Markdown
Collaborator

@nicoloboschi nicoloboschi left a comment

Choose a reason for hiding this comment

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

Two blockers — the O(n²) file I/O during flush and a dead-code guard that silently breaks flushing.

const clientGlobal = (global as any).__hindsightClient;
if (!clientGlobal) break;

let bankClient = clientsByBankId.get(item.bankId);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bug: O(n² file I/Oremove(item.id) calls readAll() + writeAll() on every iteration, so flushing 50 items means 50 full file reads and 50 full file rewrites.

Collect the successfully-flushed IDs into an array, then do a single bulk removal after the loop:

const flushedIds: string[] = [];
for (const item of items) {
  // ... send ...
  flushedIds.push(item.id);
}
if (flushedIds.length > 0) retainQueue.removeMany(flushedIds);

Same concern applies to size() (called on every successful retain and on the timer) — it parses the entire file each time. Worth caching the count in memory and updating it on enqueue/remove.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — flush now collects flushed IDs and calls removeMany() for a single file rewrite at the end. Also cached the item count in memory so size() is O(1) — updated on every enqueue(), writeAll(), and removeMany(). @nicoloboschi

try {
// Cleanup expired items first
retainQueue.cleanup();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Bug: clientGlobal is read but never used. This checks (global as any).__hindsightClient and breaks out of the loop if it is falsy, but the variable is never referenced again — the code below uses clientsByBankId / clientOptions instead.

This means the flush silently does nothing whenever __hindsightClient happens to be unset, even if valid per-bank clients exist. Either:

  • Remove this check entirely (it guards nothing), or
  • Replace it with a check on what is actually needed (if (!clientOptions) break;)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — removed the clientGlobal check entirely and replaced it with if (!clientOptions) return which guards what's actually needed. Moved it before cleanup() too since there's no point expiring items if we can't flush anyway. @nicoloboschi

@akhater akhater force-pushed the feat/openclaw-retain-queue branch from d78f22d to 654ac71 Compare March 30, 2026 09:15
When the external Hindsight API is unreachable, retain requests are
buffered as JSON lines in a local file and automatically flushed once
connectivity is restored. Queue survives process restarts.

- Only active in external API mode (local daemon handles its own persistence)
- Zero dependencies — uses only Node built-ins (fs, crypto)
- Bulk removal via removeMany() for O(1) file rewrites during flush
- Cached item count so size() is O(1)
- Configurable: retainQueuePath, retainQueueMaxAgeMs (-1 = forever),
  retainQueueFlushIntervalMs (default 60s)
- Flushes on successful retain and on a periodic timer
- All logging routed through structured logger (api.logger)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@akhater akhater force-pushed the feat/openclaw-retain-queue branch from 654ac71 to b04b8c8 Compare March 30, 2026 09:20
@akhater akhater requested a review from nicoloboschi March 30, 2026 09:45
Copy link
Copy Markdown
Collaborator

@nicoloboschi nicoloboschi left a comment

Choose a reason for hiding this comment

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

can you resolve conflicts?

…rvice.start()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@akhater
Copy link
Copy Markdown
Contributor Author

akhater commented Mar 31, 2026

@nicoloboschi Conflicts resolved — ready to merge!

@nicoloboschi nicoloboschi merged commit 087545c into vectorize-io:main Apr 1, 2026
38 of 39 checks passed
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.

2 participants