Skip to content

Conversation

@danisharora099
Copy link
Collaborator

@danisharora099 danisharora099 commented Nov 14, 2025

Problem / Description

Message history is currently stored only in memory, causing SDS to start recovery afresh every time.

Solution

This PR introduces persistent message history that survives application restarts using localStorage (browser), with an option to provide custom storage providers.

Changes

  • MemLocalHistory now optionally sets up Storage
  • Storage abstraction with automatic localStorage usage in browsers with serialisation support
    • Optionally can provide custom Storage providers
  • Backwards compatible MessageChannel with persistent storage support, by default
  • New tests

Notes

  • Persistent storage by default (if available)
    • Can opt out of persistent storage
    • Non-browser environments require custom storage provider, otherwise memory-only
  • Supports both browsers and NodeJS
  • Backwards compatibility maintained

Checklist

  • Code changes are covered by unit tests
  • Code changes are covered by e2e tests, if applicable
  • Dogfooding has been performed, if feasible
  • A test version has been published, if required
  • All CI checks pass successfully

*
* If no storage backend is available, this behaves like {@link MemLocalHistory}.
*/
export class PersistentHistory extends MemLocalHistory {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why are you extending another class instead of the interface? Avoid abstractions as a rule of thumb

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

extended so that we could get the implementations for the following functions without needing to reimplement:

 push(...items: ContentMessage[]): number;
  some(
    predicate: (
      value: ContentMessage,
      index: number,
      array: ContentMessage[]
    ) => unknown,
    thisArg?: any
  ): boolean;
  slice(start?: number, end?: number): ContentMessage[];
  find(
    predicate: (
      value: ContentMessage,
      index: number,
      obj: ContentMessage[]
    ) => unknown,
    thisArg?: any
  ): ContentMessage | undefined;
  findIndex(
    predicate: (
      value: ContentMessage,
      index: number,
      obj: ContentMessage[]
    ) => unknown,
    thisArg?: any
  ): number;

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes but you already know you want to get rid of some of that with the optimisation so I would avoid the abstraction usage.

You can also extract those logic if necessary. Saving code is not always the best way forward. See my new comments where naming is confusing.

this.restore();
}

public override push(...items: ContentMessage[]): number {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, don't use abstraction. Its not worth the indirection you are bringing in

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

switched to composition!

}

private persist(): void {
if (!this.storage) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hum, does it make sense for it to be constructed without storage?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we want the class to behave like MemLocalHistory if no storage is provided, right?

Copy link
Collaborator Author

@danisharora099 danisharora099 Nov 27, 2025

Choose a reason for hiding this comment

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

or perhaps, that should be handled one level above where MessageChannel chooses between MemLocalHistory and PersistentHistory -- suggestions?

Copy link
Collaborator

Choose a reason for hiding this comment

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

we want the class to behave like MemLocalHistory if no storage is provided, right?

So you are saying that if we instantiate a persistent local history, but there is no persistence to it, then we want it to behave like memory local history? Think about the footgun you are setting up for developers.

If there is no way to persist the history, then the class handling persisting history should not be instantiable.

Copy link
Collaborator Author

@danisharora099 danisharora099 Dec 9, 2025

Choose a reason for hiding this comment

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

Indeed:

if (storage instanceof PersistentStorage) {
      this.storage = storage;
      log.info("Using explicit persistent storage");
    } else if (typeof storage === "string") {
      this.storage = PersistentStorage.create(storage);
      log.info("Creating persistent storage for channel", storage);
    } else {
      this.storage = undefined;
      log.info("Using in-memory storage");
    }

@danisharora099 danisharora099 force-pushed the feat/persistent_history branch from d7aa504 to 92dd0ba Compare November 20, 2025 21:27
@github-actions
Copy link

github-actions bot commented Nov 20, 2025

size-limit report 📦

Path Size Loading time (3g) Running time (snapdragon) Total time
Waku node 96.31 KB (+0.08% 🔺) 2 s (+0.08% 🔺) 693 ms (+0.27% 🔺) 2.7 s
Waku Simple Light Node 147.63 KB (+0.03% 🔺) 3 s (+0.03% 🔺) 739 ms (+14.37% 🔺) 3.7 s
ECIES encryption 22.62 KB (0%) 453 ms (0%) 584 ms (+113.69% 🔺) 1.1 s
Symmetric encryption 22 KB (0%) 440 ms (0%) 457 ms (+171.08% 🔺) 897 ms
DNS discovery 52.17 KB (0%) 1.1 s (0%) 400 ms (+17.71% 🔺) 1.5 s
Peer Exchange discovery 52.91 KB (0%) 1.1 s (0%) 406 ms (+87.39% 🔺) 1.5 s
Peer Cache Discovery 46.64 KB (0%) 933 ms (0%) 398 ms (-8.35% 🔽) 1.4 s
Privacy preserving protocols 77.26 KB (-0.07% 🔽) 1.6 s (-0.07% 🔽) 338 ms (-46.59% 🔽) 1.9 s
Waku Filter 79.76 KB (-0.08% 🔽) 1.6 s (-0.08% 🔽) 662 ms (+27.57% 🔺) 2.3 s
Waku LightPush 78.06 KB (+0.11% 🔺) 1.6 s (+0.11% 🔺) 710 ms (+59.55% 🔺) 2.3 s
History retrieval protocols 83.73 KB (-0.02% 🔽) 1.7 s (-0.02% 🔽) 640 ms (-19.23% 🔽) 2.4 s
Deterministic Message Hashing 28.98 KB (0%) 580 ms (0%) 171 ms (-0.33% 🔽) 751 ms

@danisharora099
Copy link
Collaborator Author

danisharora099 commented Nov 27, 2025

Restructured the PR:
instead of keeping PersistentHistory as a module over MemLocalHistory, now setup PersistentStorage as a separate class that can be optionally used as a storage in MemLocalHistory -- perhaps a followup will be to update the name MemLocalHistory to just History

@danisharora099 danisharora099 changed the title feat: persistent history for SDS feat(sds): persistent storage for history Nov 27, 2025
@danisharora099 danisharora099 force-pushed the feat/persistent_history branch from df7d56b to 3e3c511 Compare November 27, 2025 03:40
@danisharora099 danisharora099 marked this pull request as ready for review November 27, 2025 04:07
@danisharora099 danisharora099 requested a review from a team as a code owner November 27, 2025 04:07
Comment on lines 22 to 50
export interface ILocalHistory {
length: number;
push(...items: ContentMessage[]): number;
some(
predicate: (
value: ContentMessage,
index: number,
array: ContentMessage[]
) => unknown,
thisArg?: any
): boolean;
slice(start?: number, end?: number): ContentMessage[];
find(
predicate: (
value: ContentMessage,
index: number,
obj: ContentMessage[]
) => unknown,
thisArg?: any
): ContentMessage | undefined;
findIndex(
predicate: (
value: ContentMessage,
index: number,
obj: ContentMessage[]
) => unknown,
thisArg?: any
): number;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does not seem to be the right place to define this interface. Also, it is the same as before (Pick<Array...).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

tackled here: #2745

Copy link
Collaborator

@fryorcraken fryorcraken left a comment

Choose a reason for hiding this comment

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

The naming is very confusing. It is unclear what is actually related to local history, vs the local storage interfaces.

Not sure how the message channel is supposed to use the persistent storage when there is no persistent storage implementing ILocalHistory.

* at next push.
*/
export class MemLocalHistory {
export interface ILocalHistory {
Copy link
Collaborator

Choose a reason for hiding this comment

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

define the interface alongside the class that needs this interface, aka, message channel.

It is odd to define the interface along side one of the implementations.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ILocalHistory is being implemented by LocalHistory (prev MemLocalHistory), so it needs it as well. Would you suggest moving it to MessageChannel?

Since the concept for LocalHistory belongs in this file, I believed it's good design to keep it close to the implementation. The interface exists because of the class

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Based on your observation that it's only being used once, removed it.

}

export type MemLocalHistoryOptions = {
storage?: ChannelId | PersistentStorage;
Copy link
Collaborator

Choose a reason for hiding this comment

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

What are you doing here? Why would you use persistent storage for the Memory implementation?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok, no I got it, see my recent comment on the PR.

I would not have this kind of type switching here.

You can have a storagePrefix string that is applied in any case (whether you use browser localStorage or fs). It is relevant to both.

Then, for PersistentStorage, could we instead use the package.json browser feature and have 2 files:

browser-localStorage.ts
localStorage.ts

Both of them would expert a LocalStorage (what you did with PersistentStorage class, except that for the browser one, the LocalStorage class is just a thin wrap on the browser localStorage

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Okay, interesting, that's probably a neater solution.

this.incomingBuffer = [];
this.localHistory = localHistory;
this.localHistory =
localHistory ?? new MemLocalHistory({ storage: channelId });
Copy link
Collaborator

Choose a reason for hiding this comment

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

Considering that this is not an API we actually want to expose, (ReliableChannel is), then it's fine to not have default.

Comment on lines 10 to 13
export interface HistoryStorage {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
removeItem(key: string): void;
Copy link
Collaborator

Choose a reason for hiding this comment

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

what is that? it does not seem to be "History" right? It's supposed to be the interface for local storage, right? Call it ILocalStorage then

@fryorcraken
Copy link
Collaborator

After more review, I now better understand what is done here:

  ┌─────────────────────────────────────────────────────────────┐
  │                     MessageChannel                          │
  │  (defaults to MemLocalHistory with channelId for storage)   │
  └──────────────────────────┬──────────────────────────────────┘
                             │
                             ▼
  ┌─────────────────────────────────────────────────────────────┐
  │                    MemLocalHistory                          │
  │  implements ILocalHistory                                   │
  │  - Manages in-memory message array                          │
  │  - Delegates persistence to PersistentStorage               │
  └──────────────────────────┬──────────────────────────────────┘
                             │ optional
                             ▼
  ┌─────────────────────────────────────────────────────────────┐
  │                   PersistentStorage                         │
  │  - Serializes ContentMessage → JSON with hex encoding       │
  │  - Uses HistoryStorage interface (localStorage-compatible)  │
  └─────────────────────────────────────────────────────────────┘

This is not the architecture I originally had in mind, which is fine. It just means that I did some pre-optimisation that now needs to be trashed.

In this proposed architecture, there is only one implementation of ILocalHistory, hence, the interface can be trashed.

MemLocalHistory persist when possible -> so it's a not a Mem history anymore and can just be called LocalHistory.

Then the last part Uses HistoryStorage interface (localStorage-compatible) -> this is confusing. the interface should be called ILocalStorage and not have the history word in it, which is a term use in the SDS domain, and local storage is not SDS-aware.

@danisharora099 danisharora099 force-pushed the feat/persistent_history branch from 434728f to 96ec51f Compare December 9, 2025 22:00
@danisharora099
Copy link
Collaborator Author

danisharora099 commented Dec 9, 2025

After more review, I now better understand what is done here:

  ┌─────────────────────────────────────────────────────────────┐
  │                     MessageChannel                          │
  │  (defaults to MemLocalHistory with channelId for storage)   │
  └──────────────────────────┬──────────────────────────────────┘
                             │
                             ▼
  ┌─────────────────────────────────────────────────────────────┐
  │                    MemLocalHistory                          │
  │  implements ILocalHistory                                   │
  │  - Manages in-memory message array                          │
  │  - Delegates persistence to PersistentStorage               │
  └──────────────────────────┬──────────────────────────────────┘
                             │ optional
                             ▼
  ┌─────────────────────────────────────────────────────────────┐
  │                   PersistentStorage                         │
  │  - Serializes ContentMessage → JSON with hex encoding       │
  │  - Uses HistoryStorage interface (localStorage-compatible)  │
  └─────────────────────────────────────────────────────────────┘

Ah, I see where the confusion came from. Thanks for the diagram (note to self to include it wherever possible), I can see how it may have been confusing since the structure changed.

MemLocalHistory persist when possible -> so it's a not a Mem history anymore and can just be called LocalHistory.

Agreed, see my comment here: #2741 (comment)

Then the last part Uses HistoryStorage interface (localStorage-compatible) -> this is confusing. the interface should be called ILocalStorage and not have the history word in it, which is a term use in the SDS domain, and local storage is not SDS-aware.

Valid point, thanks! One caveat: the class is more than just "local storage" as it allows users to pass their custom storage providers, so IStorage, perhaps?

@danisharora099 danisharora099 force-pushed the feat/persistent_history branch from 75e8e12 to e396c3b Compare December 10, 2025 20:21
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces persistent message history storage for SDS (Scalable Data Sync), allowing message history to survive application restarts. The implementation provides automatic localStorage usage in browsers and file-based storage in Node.js, with support for custom storage providers. The changes maintain backward compatibility by making persistent storage optional - applications can opt out or continue using the in-memory-only mode.

Key Changes:

  • Renamed MemLocalHistory to LocalHistory with optional persistent storage support via a new Storage abstraction
  • Added platform-specific storage implementations: browser (localStorage) and Node.js (file system)
  • Updated MessageChannel to use persistent storage by default when a channelId is provided
  • Added message serialization/deserialization utilities to convert between ContentMessage objects and storable JSON format

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
packages/sds/src/message_channel/storage/node.ts New file: Node.js file-based storage implementation using fs module
packages/sds/src/message_channel/storage/browser.ts New file: Browser localStorage wrapper for persistent storage
packages/sds/src/message_channel/storage/message_serializer.ts New file: Serialization utilities for ContentMessage and HistoryEntry objects
packages/sds/src/message_channel/storage/index.ts New file: Export configuration with browser field mapping for platform selection
packages/sds/src/message_channel/local_history.ts Renamed from MemLocalHistory, added storage integration with save/load methods
packages/sds/src/message_channel/local_history.spec.ts Updated tests to use new LocalHistory class and constructor API
packages/sds/src/message_channel/message_channel.ts Updated to create LocalHistory with persistent storage by default using channelId as prefix
packages/sds/src/message_channel/message_channel.spec.ts Added createTestChannel helper for in-memory testing, added localStorage persistence tests
packages/sds/src/message_channel/persistent_storage.spec.ts New test suite for storage persistence and corruption handling
packages/sds/src/message_channel/repair/repair.ts Updated type references from ILocalHistory to LocalHistory
packages/sds/package.json Added browser field mapping to swap Node.js storage for browser storage
packages/sds/karma.conf.cjs Updated to configure webpack aliases for browser storage in test environment
.gitignore Added entries for allure-results directories
Comments suppressed due to low confidence (3)

packages/sds/src/message_channel/local_history.ts:87

  • Calling save() on every push operation could be a performance bottleneck for high-frequency message scenarios, especially since it serializes and writes the entire message history to storage each time. Consider implementing a debounced or batched save mechanism to reduce I/O operations while still maintaining data persistence. This is particularly important when multiple messages are pushed in rapid succession.
    packages/sds/src/message_channel/local_history.ts:57
  • When both customInstance and prefix are provided in the storage options, the prefix is silently ignored. Consider either: (1) adding validation to throw an error when both are provided, (2) documenting this precedence clearly in the LocalHistoryOptions type documentation, or (3) logging a warning when prefix is provided but ignored due to customInstance being present.
    packages/sds/src/message_channel/local_history.ts:28
  • The class documentation still says "In-Memory implementation" but the class now supports optional persistent storage via localStorage (browser) or file system (Node.js). Update the documentation to reflect that this class can use persistent storage when configured with a storage prefix or custom storage instance, and only falls back to in-memory storage when no storage configuration is provided.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +71 to +79
private static deserializeCausalEntry(
entry: StoredCausalEntry
): HistoryEntry {
return {
messageId: entry.messageId,
retrievalHint: entry.retrievalHint
? hexToBytes(entry.retrievalHint)
: undefined
};
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The deserializeCausalEntry method is missing the senderId field from the deserialization. This field needs to be included to properly reconstruct HistoryEntry objects when loading from storage. Without it, deserialized causal history entries will be incomplete.

Copilot uses AI. Check for mistakes.
private readonly filePath: string;

public constructor(storagePrefix: string, basePath: string = ".waku") {
this.filePath = join(basePath, `${storagePrefix}.json`);
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The storagePrefix parameter is used directly in file path construction without validation or sanitization. If user-controlled input is used as the storagePrefix (e.g., from a channelId), it could contain path traversal sequences like "../" that allow writing files outside the intended basePath directory. Consider validating or sanitizing the storagePrefix to ensure it doesn't contain path separators or other potentially dangerous characters.

Suggested change
this.filePath = join(basePath, `${storagePrefix}.json`);
// Sanitize storagePrefix to prevent path traversal and invalid characters
const safePrefix = storagePrefix.replace(/[^a-zA-Z0-9_-]/g, "_");
this.filePath = join(basePath, `${safePrefix}.json`);

Copilot uses AI. Check for mistakes.
);
localStorage.setItem(this.storageKey, payload);
} catch (error) {
log.error("Failed to save messages to storage:", error);
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The error logging for localStorage.setItem should provide more context about the specific error type, particularly for QuotaExceededError which is common when localStorage is full. Consider logging a more specific message when this error occurs (e.g., "localStorage quota exceeded - consider reducing message history size") to help users understand and resolve the issue.

Suggested change
log.error("Failed to save messages to storage:", error);
if (
error &&
(error.name === "QuotaExceededError" ||
error.name === "NS_ERROR_DOM_QUOTA_REACHED" || // Firefox
error.code === 22 || // Chrome, Safari
error.code === 1014) // Firefox
) {
log.error(
"localStorage quota exceeded - consider reducing message history size.",
error
);
} else {
log.error("Failed to save messages to storage:", error);
}

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +16
import path from "path";

module.exports = config;
import baseConfig from "../../karma.conf.cjs";

export default function (config) {
baseConfig(config);

const storageDir = path.resolve(__dirname, "src/message_channel/storage");

// Swap node storage for browser storage in webpack builds
config.webpack.resolve.alias = {
...config.webpack.resolve.alias,
[path.join(storageDir, "node.ts")]: path.join(storageDir, "browser.ts"),
[path.join(storageDir, "node.js")]: path.join(storageDir, "browser.ts")
};
}
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

This file uses ES module syntax (import/export default) but has a .cjs extension which indicates CommonJS. Additionally, it uses __dirname which is not available in ES modules without importing it from a special module.

Either change the file extension to .mjs or use CommonJS syntax with require() and module.exports. If using ES modules, you'll need to derive __dirname from import.meta.url.

Copilot uses AI. Check for mistakes.
Comment on lines +1253 to +1314
describe("localStorage persistence", function () {
// LocalStorage specific tests (browser)
before(function () {
if (typeof localStorage === "undefined") {
this.skip();
}
});

it("should restore messages from localStorage on channel recreation", async () => {
const persistentChannelId = "persistent-channel";

const channel1 = new MessageChannel(persistentChannelId, "alice");

await sendMessage(channel1, utf8ToBytes("msg-1"), callback);
await sendMessage(channel1, utf8ToBytes("msg-2"), callback);

expect(channel1["localHistory"].length).to.equal(2);

// Recreate channel with same storage - should load history
const channel2 = new MessageChannel(persistentChannelId, "alice");

expect(channel2["localHistory"].length).to.equal(2);
expect(
channel2["localHistory"].slice(0).map((m) => m.messageId)
).to.deep.equal([
MessageChannel.getMessageId(utf8ToBytes("msg-1")),
MessageChannel.getMessageId(utf8ToBytes("msg-2"))
]);
});

it("should include persisted messages in causal history after restart", async () => {
const persistentChannelId = "persistent-causal";

const channel1 = new MessageChannel(persistentChannelId, "alice", {
causalHistorySize: 2
});

await sendMessage(channel1, utf8ToBytes("msg-1"), callback);
await sendMessage(channel1, utf8ToBytes("msg-2"), callback);
await sendMessage(channel1, utf8ToBytes("msg-3"), callback);

const channel2 = new MessageChannel(persistentChannelId, "alice", {
causalHistorySize: 2
});

let capturedMessage: ContentMessage | null = null;
await sendMessage(channel2, utf8ToBytes("msg-4"), async (message) => {
capturedMessage = message;
return { success: true };
});

expect(capturedMessage).to.not.be.null;
expect(capturedMessage!.causalHistory).to.have.lengthOf(2);
// Should reference the last 2 messages (msg-2 and msg-3)
expect(capturedMessage!.causalHistory[0].messageId).to.equal(
MessageChannel.getMessageId(utf8ToBytes("msg-2"))
);
expect(capturedMessage!.causalHistory[1].messageId).to.equal(
MessageChannel.getMessageId(utf8ToBytes("msg-3"))
);
});
});
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

Missing localStorage cleanup after tests. The test suite creates localStorage entries with keys like "waku:sds:storage:persistent-channel" and "waku:sds:storage:persistent-causal" but doesn't clean them up in an afterEach hook. This could lead to test pollution affecting other tests. Add an afterEach hook to remove these localStorage entries similar to what's done in persistent_storage.spec.ts.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +8
export type StoredCausalEntry = {
messageId: string;
retrievalHint?: string;
};
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The StoredCausalEntry type is missing the senderId field. Based on the code in message_channel.ts (lines 456-457, 678-679), HistoryEntry objects contain messageId, retrievalHint, and senderId fields. The senderId field needs to be included in the serialization and deserialization logic to properly preserve causal history entries. This will cause data loss when messages are persisted and restored from storage.

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +68
private static serializeCausalEntry(entry: HistoryEntry): StoredCausalEntry {
return {
messageId: entry.messageId,
retrievalHint: entry.retrievalHint
? bytesToHex(entry.retrievalHint)
: undefined
};
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The serializeCausalEntry method is missing the senderId field from the serialization. This field needs to be included to match the HistoryEntry structure used throughout the codebase. Without it, the senderId information will be lost when messages are persisted.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +32
it("persists and restores messages", () => {
const history1 = new LocalHistory({ storage: { prefix: channelId } });
history1.push(createMessage("msg-1", 1));
history1.push(createMessage("msg-2", 2));

const history2 = new LocalHistory({ storage: { prefix: channelId } });

expect(history2.length).to.equal(2);
expect(history2.slice(0).map((msg) => msg.messageId)).to.deep.equal([
"msg-1",
"msg-2"
]);
});
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The test "persists and restores messages" doesn't verify that causal history entries with their senderId fields are properly persisted and restored. Add test coverage to ensure that messages with non-empty causal history (including senderId fields) are correctly serialized and deserialized. This is important because the HistoryEntry type includes senderId, retrievalHint, and messageId fields.

Copilot uses AI. Check for mistakes.
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.

3 participants