Skip to content

feat: persistent message store with crash recovery - #17

Merged
akurinnoy merged 4 commits into
eclipse-che:mainfrom
akurinnoy:main
Jul 9, 2026
Merged

feat: persistent message store with crash recovery#17
akurinnoy merged 4 commits into
eclipse-che:mainfrom
akurinnoy:main

Conversation

@akurinnoy

@akurinnoy akurinnoy commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add in-memory message store with file-backed persistence and crash recovery
  • Add send_message and receive_messages MCP tools for inter-agent communication
  • Add PVC manifest and DATA_DIR config constant for persistent storage
  • Thread-based messaging with automatic cleanup

Changes

  • src/messaging/store.ts — message store with file persistence and thread management
  • src/tools/send-message.ts — MCP tool to send messages between agents
  • src/tools/receive-messages.ts — MCP tool to receive messages from a thread
  • src/tools.ts — register new tools
  • src/config.ts — add DATA_DIR constant
  • deploy/pvc.yaml — PersistentVolumeClaim for message storage
  • deploy/deployment.yaml — volume mount for data directory

Test plan

  • Unit tests for message store (tests/messaging/store.test.ts)
  • Persistence tests with crash recovery (tests/messaging/store-persistence.test.ts)
  • Tool handler tests (tests/tools/send-message.test.ts, tests/tools/receive-messages.test.ts)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added message send/receive tools with optional thread-based retrieval, using UUID message/thread identifiers.
    • Enabled persistent in-memory inbox storage so queued messages survive restarts.
    • Added Kubernetes persistent volume support for message data.
  • Bug Fixes
    • Improved message consumption behavior on read and accurate unread counts for empty/unknown sessions.
  • Tests
    • Added coverage for inbox persistence, consumption semantics, and the new send/receive tools.

…) (#2)

* feat(messaging): add in-memory message store

Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com>

* fix(messaging): fix import ordering and add thread cleanup test

Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com>

* feat(messaging): add send_message MCP tool

Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com>

* feat(messaging): add receive_messages MCP tool

Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com>

* feat: add DATA_DIR constant to config.ts for persistent message store (issue eclipse-che#8)

* feat: add PVC manifest and register in kustomization (issue eclipse-che#8)

* feat: mount PVC and set CHE_MCP_DATA_DIR env var in deployment (issue eclipse-che#8)

* feat: replace in-memory Map with file-backed persistent store (issue eclipse-che#8)

* test: add crash-recovery persistence tests for message store (issue eclipse-che#8)

* test: update tool adapter tests to use initStore with file-backed store

* fix: lint and type-check fixups after persistence store integration

---------

Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com>
@akurinnoy
akurinnoy requested a review from tolusha as a code owner July 8, 2026 12:11
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 806d9bfa-1816-4978-a6dd-2f338d43a005

📥 Commits

Reviewing files that changed from the base of the PR and between a70e333 and f9a556b.

📒 Files selected for processing (1)
  • src/messaging/store.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/messaging/store.ts

📝 Walkthrough

Walkthrough

This PR adds a persistent messaging inbox store with send/receive functionality, exposed as new MCP tools (send_message, receive_messages). Deployment manifests are updated to provision a PersistentVolumeClaim mounted at /data. A Node stream import specifier is also updated.

Changes

Messaging Store Feature

Layer / File(s) Summary
DATA_DIR configuration
src/config.ts
Adds an exported DATA_DIR constant reading CHE_MCP_DATA_DIR with /data fallback.
Message store core implementation
src/messaging/store.ts
Introduces the message store state, disk-backed init/recovery, send/receive operations, unread counts, inbox clearing, atomic flushes, and auto-initialization.
Tool wrapper functions
src/tools/send-message.ts, src/tools/receive-messages.ts, tests/tools/*
Adds sendMessageTool and receiveMessagesTool wrappers delegating to the store, with accompanying unit tests.
MCP tool registration
src/tools.ts
Registers send_message and receive_messages MCP tools with Zod schemas and error handling.
Message store unit and persistence tests
tests/messaging/store.test.ts, tests/messaging/store-persistence.test.ts
Adds tests for message semantics, thread filtering, unread counts, and disk persistence across restarts and clears.
Deployment storage wiring
deploy/pvc.yaml, deploy/kustomization.yaml, deploy/deployment.yaml
Adds a PersistentVolumeClaim, references it in kustomization resources, and mounts it at /data with matching CHE_MCP_DATA_DIR.
Node stream import update
src/kube/exec.ts
Updates the stream import to use the node:stream module specifier.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SendMessageTool
  participant ReceiveMessagesTool
  participant MessageStore
  participant Disk

  Client->>SendMessageTool: send_message(from, to, body, thread_id)
  SendMessageTool->>MessageStore: sendMessage(from, to, body, thread_id)
  MessageStore->>Disk: flushToDisk (write temp, rename)
  MessageStore-->>SendMessageTool: message_id, thread_id
  SendMessageTool-->>Client: message_id, thread_id

  Client->>ReceiveMessagesTool: receive_messages(session_id, thread_id)
  ReceiveMessagesTool->>MessageStore: receiveMessages(session_id, thread_id)
  MessageStore->>Disk: flushToDisk (update inbox)
  MessageStore-->>ReceiveMessagesTool: messages
  ReceiveMessagesTool-->>Client: messages
Loading

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: a persistent message store with crash recovery.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/tools.ts (1)

593-634: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding destructiveHint to receive_messages.

receive_messages consumes (deletes) messages after reading, which is a destructive operation. Adding { destructiveHint: true } as the fourth argument to server.tool() would signal this to MCP clients, consistent with how delete_workspace and stop_agent are annotated.

♻️ Proposed fix
     async ({ session_id, thread_id }) => {
       try {
         const result = receiveMessagesTool({ session_id, thread_id });
         return { content: [{ type: 'text', text: JSON.stringify(result) }] };
       } catch (error) {
         return toolError(error);
       }
     },
+    { destructiveHint: true },
   );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tools.ts` around lines 593 - 634, The receive_messages tool in tools.ts
is destructive because it consumes inbox messages after reading, so update the
server.tool() registration for receive_messages to include the destructiveHint
flag. Use the existing send_message/receive_messages definitions as the anchor,
and add the hint in the tool metadata consistently with other destructive tools
like delete_workspace and stop_agent.
deploy/deployment.yaml (1)

28-32: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Volume and env wiring is correct.

Cross-file verification passed: PVC name che-mcp-server-data matches deploy/pvc.yaml, mount path /data matches CHE_MCP_DATA_DIR, and volume name data is consistent between volumeMounts and volumes.

Two pre-existing concerns worth noting (not introduced by this PR):

  1. Missing securityContext (Checkov CKV_K8S_20, CKV_K8S_23): The container runs without allowPrivilegeEscalation: false or a non-root runAsUser. Consider adding a securityContext block to the container spec.

  2. Replica scaling constraint: replicas: 1 with a ReadWriteOnce PVC means scaling beyond 1 replica will cause additional pods to fail PVC mounting. If horizontal scaling is planned, consider ReadWriteMany (requires a supporting storage class) or switching to a shared data store.

Also applies to: 45-48

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/deployment.yaml` around lines 28 - 32, The deployment wiring is fine,
but the review calls out two existing hardening/scaling gaps in the container
spec: add a securityContext to the workload defined under the Deployment’s
container template (for example, enforce non-root/disable privilege escalation),
and avoid relying on replicas: 1 with a ReadWriteOnce PVC if future horizontal
scaling is expected. Update the Deployment definition and related storage
settings only if you plan to support more than one replica.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/messaging/store.ts`:
- Around line 19-35: `initStore` needs stronger crash-recovery and reload
handling. Always clear `inboxes` before loading from `dataFile`, then wrap the
restore path in validation/error handling so a corrupted `dataFile` or promoted
`tmpFile` cannot crash startup. In `initStore`, only `renameSync(tmpFile,
dataFile)` after confirming the tmp contents are valid JSON, and guard the
`JSON.parse` load path with a fallback that clears state instead of throwing.
Use the existing `dataFile`, `tmpFile`, and `inboxes` flow in `initStore` to
keep the fix localized.

---

Nitpick comments:
In `@deploy/deployment.yaml`:
- Around line 28-32: The deployment wiring is fine, but the review calls out two
existing hardening/scaling gaps in the container spec: add a securityContext to
the workload defined under the Deployment’s container template (for example,
enforce non-root/disable privilege escalation), and avoid relying on replicas: 1
with a ReadWriteOnce PVC if future horizontal scaling is expected. Update the
Deployment definition and related storage settings only if you plan to support
more than one replica.

In `@src/tools.ts`:
- Around line 593-634: The receive_messages tool in tools.ts is destructive
because it consumes inbox messages after reading, so update the server.tool()
registration for receive_messages to include the destructiveHint flag. Use the
existing send_message/receive_messages definitions as the anchor, and add the
hint in the tool metadata consistently with other destructive tools like
delete_workspace and stop_agent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ca2fed9-2b54-45a5-89f5-1ad08aaf124a

📥 Commits

Reviewing files that changed from the base of the PR and between d42239c and 92c493a.

📒 Files selected for processing (13)
  • deploy/deployment.yaml
  • deploy/kustomization.yaml
  • deploy/pvc.yaml
  • src/config.ts
  • src/kube/exec.ts
  • src/messaging/store.ts
  • src/tools.ts
  • src/tools/receive-messages.ts
  • src/tools/send-message.ts
  • tests/messaging/store-persistence.test.ts
  • tests/messaging/store.test.ts
  • tests/tools/receive-messages.test.ts
  • tests/tools/send-message.test.ts

Comment thread src/messaging/store.ts
akurinnoy and others added 2 commits July 8, 2026 15:45
- initStore: clear inboxes before load, validate tmp file JSON before
  promoting, handle corrupted data file gracefully (start empty)
- receive_messages: add destructiveHint since it consumes messages
- deployment: add securityContext (allowPrivilegeEscalation: false,
  runAsNonRoot: true)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com>
server.tool() signature is (name, desc, schema, extras, handler),
not (name, desc, schema, handler, extras).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/messaging/store.ts (1)

112-117: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

flushToDisk failure leaves in-memory state inconsistent with disk.

sendMessage and receiveMessages mutate inboxes before calling flushToDisk. If writeFileSync or renameSync throws (disk full, I/O error), the in-memory state is already updated but the disk retains stale data. The exception propagates to the caller, but on the next restart initStore loads the old disk state — silently losing the messages that were "sent" or "received" in-memory.

Consider wrapping the flush in a try/catch and rolling back the in-memory mutation on failure, or restructuring so the serialized state is computed before mutation.

🔧 Suggested approach: snapshot before mutate, rollback on flush failure
 function flushToDisk(): void {
   if (dataFile === '') return;
   const json = JSON.stringify(Array.from(inboxes.entries()));
-  writeFileSync(tmpFile, json, 'utf8');
-  renameSync(tmpFile, dataFile);
+  try {
+    writeFileSync(tmpFile, json, 'utf8');
+    renameSync(tmpFile, dataFile);
+  } catch (e) {
+    // Disk write failed — in-memory state is now ahead of disk.
+    // Log the error so operators can investigate; the next successful
+    // flush will reconcile disk with memory.
+    console.error('flushToDisk failed:', e);
+  }
 }

Alternatively, snapshot the inbox state in sendMessage/receiveMessages before mutation and restore it if flushToDisk throws:

export function sendMessage(from: string, to: string, body: string, thread_id?: string) {
  // ... build message ...
  const prevInbox = [...(inboxes.get(to) ?? [])];
  inbox.push(message);
  inboxes.set(to, inbox);
  try {
    flushToDisk();
  } catch (e) {
    inboxes.set(to, prevInbox); // rollback
    throw e;
  }
  return { message_id, thread_id: resolvedThreadId };
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/messaging/store.ts` around lines 112 - 117, flushToDisk currently updates
disk after in-memory mutations in sendMessage and receiveMessages, so a
write/rename failure leaves inboxes diverged from persisted state. Update the
sendMessage/receiveMessages flow to snapshot the prior inbox state (or otherwise
stage the change) before mutating, then wrap the flushToDisk call in try/catch
and restore the snapshot on failure before rethrowing. Keep the fix centered
around the existing flushToDisk helper and the sendMessage/receiveMessages
mutations to ensure in-memory state only commits when persistence succeeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/messaging/store.ts`:
- Around line 112-117: flushToDisk currently updates disk after in-memory
mutations in sendMessage and receiveMessages, so a write/rename failure leaves
inboxes diverged from persisted state. Update the sendMessage/receiveMessages
flow to snapshot the prior inbox state (or otherwise stage the change) before
mutating, then wrap the flushToDisk call in try/catch and restore the snapshot
on failure before rethrowing. Keep the fix centered around the existing
flushToDisk helper and the sendMessage/receiveMessages mutations to ensure
in-memory state only commits when persistence succeeds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f0dfda1f-7659-4da1-80b1-3773a38b7789

📥 Commits

Reviewing files that changed from the base of the PR and between 92c493a and a70e333.

📒 Files selected for processing (3)
  • deploy/deployment.yaml
  • src/messaging/store.ts
  • src/tools.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • deploy/deployment.yaml
  • src/tools.ts

Wrap writeFileSync/renameSync in try/catch so a disk failure
doesn't propagate to callers. In-memory state stays current;
the next successful flush reconciles disk.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com>
@akurinnoy

Copy link
Copy Markdown
Contributor Author

Addressed the flushToDisk review comment in commit f9a556b — wrapped writeFileSync/renameSync in try/catch with console.error. In-memory state stays current; the next successful flush reconciles disk.

Chose the simpler approach (log and continue) over snapshot-and-rollback — this is a messaging store where eventual consistency on next flush is acceptable, and rollback would add complexity to both sendMessage and receiveMessages.

Re: image build skipped — the build-image job has if: github.event.pull_request.head.repo.fork == false, which correctly skips for fork PRs (secrets aren't available). The image will build when merged to main.

@akurinnoy
akurinnoy merged commit 31266cd into eclipse-che:main Jul 9, 2026
3 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.

1 participant