Skip to content

Commit 1d44443

Browse files
FUDCoclaude
andcommitted
fix: Recover orphan message when no seq state exists
Scan for message at seq 1 even when getRemoteSeqState returns undefined. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f6dbfc5 commit 1d44443

2 files changed

Lines changed: 48 additions & 1 deletion

File tree

packages/ocap-kernel/src/remotes/RemoteHandle.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,39 @@ describe('RemoteHandle', () => {
11011101
expect(seqState?.nextSendSeq).toBe(2);
11021102
});
11031103

1104+
it('recovers orphan message when no seq state exists', async () => {
1105+
// Simulate crash during first enqueue: message written but NO seq state
1106+
// persisted at all. This can happen if crash occurs after setPendingMessage
1107+
// but before setRemoteStartSeq.
1108+
mockKernelStore.setPendingMessage(mockRemoteId, 1, '{"seq":1}');
1109+
// No seq state set - getRemoteSeqState will return undefined
1110+
1111+
// Create RemoteHandle - should scan and find orphan message at seq 1
1112+
const remote = makeRemote();
1113+
1114+
// Next message should get seq 2 (recovered seq 1, then +1)
1115+
const promiseRRef = 'rp+3';
1116+
const resolutions: VatOneResolution[] = [
1117+
[promiseRRef, false, { body: '"resolved value"', slots: [] }],
1118+
];
1119+
await remote.deliverNotify(resolutions);
1120+
1121+
const sentString = vi.mocked(mockRemoteComms.sendRemoteMessage).mock
1122+
.calls[0]?.[1];
1123+
expect(sentString).toBeDefined();
1124+
const parsed = JSON.parse(sentString as string);
1125+
expect(parsed.seq).toBe(2);
1126+
1127+
// Verify state is correct: seq state recovered and 2 pending messages
1128+
const seqState = mockKernelStore.getRemoteSeqState(mockRemoteId);
1129+
expect(seqState?.startSeq).toBe(1);
1130+
expect(seqState?.nextSendSeq).toBe(2);
1131+
// Original orphan message still exists
1132+
expect(mockKernelStore.getPendingMessage(mockRemoteId, 1)).toBe(
1133+
'{"seq":1}',
1134+
);
1135+
});
1136+
11041137
it('ignores orphan messages (seq < startSeq) on recovery', () => {
11051138
// Simulate crash during ACK: startSeq updated but message not deleted
11061139
mockKernelStore.setRemoteNextSendSeq(mockRemoteId, 3);

packages/ocap-kernel/src/remotes/RemoteHandle.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,22 @@ export class RemoteHandle implements EndpointHandle {
200200
*/
201201
#restorePersistedState(): void {
202202
const seqState = this.#kernelStore.getRemoteSeqState(this.remoteId);
203+
203204
if (!seqState) {
204-
// No persisted state - this is a fresh remote or pre-persistence remote
205+
// No persisted seq state. Check for crash during first message enqueue:
206+
// Message may have been written but no seq state persisted yet.
207+
// First message always has seq 1 (since #nextSendSeq starts at 0, +1 = 1)
208+
if (this.#kernelStore.getPendingMessage(this.remoteId, 1)) {
209+
// Found orphan message - recover by setting up state
210+
this.#startSeq = 1;
211+
this.#nextSendSeq = 1;
212+
this.#kernelStore.setRemoteStartSeq(this.remoteId, 1);
213+
this.#kernelStore.setRemoteNextSendSeq(this.remoteId, 1);
214+
this.#logger.log(
215+
`${this.#peerId.slice(0, 8)}:: recovered orphan message at seq 1 from crash during first enqueue`,
216+
);
217+
this.#startAckTimeout();
218+
}
205219
return;
206220
}
207221

0 commit comments

Comments
 (0)