Skip to content

Commit bee729c

Browse files
authored
feat(daemon): Persist messages over across restarts (including promise/resolver formulas) (#3074)
Refs: #2074 ## Description This PR makes mailbox requests and their resolutions durable across restarts. This required introducing promise/resolver formulas and persisting request resolution state. This necessitates allowing petnames to include numbers, since messages are indexed by sequence of arrival. ```console ❯ endo purge -f ❯ endo start ❯ endo send SELF 'Hello, World!' ❯ endo inbox 0. you sent yourself "Hello, World!" at "2026-02-05T11:52:26.171Z" ❯ endo restart ❯ endo inbox 0. you sent yourself "Hello, World!" at "2026-02-05T11:52:26.171Z" ``` ### Security Considerations - Promise resolution is captured once in durable storage; resolvers ignore subsequent writes. ### Scaling Considerations - Each request now creates two formulas (promise + resolver) and writes a status record on resolve/reject. - Mailbox persistence includes two additional IDs per request, increasing mailbox storage and I/O. - Promise status tracking adds extra pet-store updates and listener activity proportional to outstanding requests. ### Documentation Considerations None ### Testing Considerations - Added restart durability tests for mailbox persistence and rehydrated request resolution. ### Compatibility Considerations Existing daemon databases must be purged. This is the current norm. ### Upgrade Considerations None
2 parents ad25f00 + d8f1b2a commit bee729c

File tree

15 files changed

+1514
-172
lines changed

15 files changed

+1514
-172
lines changed

packages/cli/src/commands/adopt.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import os from 'os';
33
import { E } from '@endo/far';
44
import { withEndoAgent } from '../context.js';
55
import { parsePetNamePath } from '../pet-name.js';
6-
import { parseNumber } from '../number-parse.js';
6+
import { parseBigint } from '../number-parse.js';
77

88
export const adoptCommand = async ({
99
messageNumberText,
@@ -13,7 +13,7 @@ export const adoptCommand = async ({
1313
}) =>
1414
withEndoAgent(agentNames, { os, process }, async ({ agent }) => {
1515
await E(agent).adopt(
16-
parseNumber(messageNumberText),
16+
parseBigint(messageNumberText),
1717
edgeName,
1818
parsePetNamePath(name),
1919
);

packages/cli/src/commands/dismiss.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
import os from 'os';
33
import { E } from '@endo/far';
44
import { withEndoAgent } from '../context.js';
5+
import { parseBigint } from '../number-parse.js';
56

67
export const dismissCommand = async ({ messageNumberText, agentNames }) =>
78
withEndoAgent(agentNames, { os, process }, async ({ agent }) => {
8-
// TODO less bad number parsing.
9-
const messageNumber = Number(messageNumberText);
10-
await E(agent).dismiss(messageNumber);
9+
await E(agent).dismiss(parseBigint(messageNumberText));
1110
});

packages/cli/src/commands/reject.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
import os from 'os';
33
import { E } from '@endo/far';
44
import { withEndoAgent } from '../context.js';
5-
import { parseNumber } from '../number-parse.js';
5+
import { parseBigint } from '../number-parse.js';
66

77
export const rejectCommand = async ({
88
requestNumberText,
99
message,
1010
agentNames,
1111
}) =>
1212
withEndoAgent(agentNames, { os, process }, async ({ agent }) => {
13-
await E(agent).reject(parseNumber(requestNumberText), message);
13+
await E(agent).reject(parseBigint(requestNumberText), message);
1414
});

packages/cli/src/commands/resolve.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
import os from 'os';
33
import { E } from '@endo/far';
44
import { withEndoAgent } from '../context.js';
5-
import { parseNumber } from '../number-parse.js';
5+
import { parseBigint } from '../number-parse.js';
66

77
export const resolveCommand = async ({
88
requestNumberText,
99
resolutionName,
1010
agentNames,
1111
}) =>
1212
withEndoAgent(agentNames, { os, process }, async ({ agent }) => {
13-
await E(agent).resolve(parseNumber(requestNumberText), resolutionName);
13+
await E(agent).resolve(parseBigint(requestNumberText), resolutionName);
1414
});

packages/cli/src/number-parse.js

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
/**
2-
* Parses the input and returns it as a number if it's valid, otherwise throws error.
2+
* Parses the input and returns it as a bigint if it's a valid integer.
33
*
44
* @param {string} input
5-
* @returns {number}
5+
* @returns {bigint}
66
*/
7-
export const parseNumber = input => {
8-
const result = /[0-9]/.test(input || '') ? Number(input) : NaN;
9-
10-
if (Number.isNaN(result)) {
7+
export const parseBigint = (input = '') => {
8+
const trimmed = input.trim();
9+
if (!/^(0|[1-9][0-9]*)$/.test(trimmed)) {
1110
throw new Error(`Invalid number: ${input}`);
1211
}
13-
14-
return result;
12+
return BigInt(trimmed);
1513
};
Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,24 @@
11
import test from 'ava';
2-
import { parseNumber } from '../src/number-parse.js';
2+
import { parseBigint } from '../src/number-parse.js';
33

4-
test('returns the number when input is a valid numeric string', t => {
5-
t.is(parseNumber('42'), 42);
6-
t.is(parseNumber('-40.0'), -40.0);
7-
t.is(parseNumber('.5'), 0.5);
8-
t.is(parseNumber('1.337e3'), 1337);
9-
t.is(parseNumber(' +1 '), 1);
10-
t.is(parseNumber('0B111'), 7);
11-
t.is(parseNumber('0o040'), 32);
12-
t.is(parseNumber('0xf00'), 3840);
13-
t.is(parseNumber('0xF00'), 3840);
14-
t.is(parseNumber(' -40.0 '), -40.0);
15-
// Test for binary, octal and hexadecimal
16-
t.is(parseNumber('0B111'), 7);
17-
t.is(parseNumber('0o040'), 32);
18-
t.is(parseNumber('0xf00'), 3840);
4+
test('returns a bigint when input is a valid integer string', t => {
5+
t.is(parseBigint('42'), 42n);
6+
t.is(parseBigint('0'), 0n);
7+
t.is(parseBigint(' 9007199254740993 '), 9007199254740993n);
8+
t.is(
9+
parseBigint('123456789012345678901234567890'),
10+
123456789012345678901234567890n,
11+
);
1912
});
2013

21-
test('throws an error when input is not a valid numeric string', t => {
14+
test('throws an error when input is not a valid integer string', t => {
2215
const badStrings = [
2316
'f00',
2417
'F00',
18+
'+1',
19+
'-1',
20+
'1.0',
21+
'01',
2522
'NaN',
2623
'Infinity',
2724
'-Infinity',
@@ -31,6 +28,6 @@ test('throws an error when input is not a valid numeric string', t => {
3128
' ',
3229
];
3330
for (const bad of badStrings) {
34-
t.throws(() => parseNumber(bad), { message: `Invalid number: ${bad}` });
31+
t.throws(() => parseBigint(bad), { message: `Invalid number: ${bad}` });
3532
}
3633
});

0 commit comments

Comments
 (0)