@bobfrankston/rmfmail 1.2.324 → 1.2.325
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.commitmsg +17 -22
- package/.llm/config-cache-crash.md +26 -0
- package/bin/mailx.js +96 -25
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +105 -16
- package/client/app.bundle.js +8 -1
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +15 -1
- package/client/app.js.map +1 -1
- package/client/app.ts +13 -1
- package/npmchanges.md +27 -0
- package/package.json +5 -5
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +7 -2
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +7 -2
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-settings/index.d.ts +15 -0
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +71 -14
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +65 -14
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/package.json +1 -1
package/.commitmsg
CHANGED
|
@@ -1,24 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
accounts.jsonc: crash-safe local cache + live reconcile instead of "restart to apply"
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
A Kernel-Power 41 crash (2026-09-13 16:36) left ~/.rmfmail/accounts.jsonc
|
|
4
|
+
zero-filled: cloudRead rewrote the local cache with a plain writeFileSync on
|
|
5
|
+
EVERY cloud read (every 3 min via pollCloud), so the file was perpetually in a
|
|
6
|
+
just-written window and NTFS committed the size but not the data. jsonc-parser
|
|
7
|
+
returned undefined for the NULs without a single log line, the daemon booted
|
|
8
|
+
with 0 accounts, and the cloud copy sat unused behind the "restart to apply"
|
|
9
|
+
banner.
|
|
5
10
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
gets no vouching.
|
|
16
|
-
|
|
17
|
-
The banner's "Stop warning about" buttons used to appear only when the
|
|
18
|
-
server's verdict was the single finding. They now appear whenever every
|
|
19
|
-
finding is one trust clears (the verdict, the redirector warning) and the
|
|
20
|
-
sender is proved — the Toast receipt carried both and so had no button
|
|
21
|
-
on the one message that needed it. A proved-but-untrusted sender's
|
|
22
|
-
redirector warning now names the action; an unproved one says plainly
|
|
23
|
-
that nothing proves who the sender is. mailx-types 0.1.99. Verified on
|
|
24
|
-
the live receipt: two cautions before, nothing after.
|
|
11
|
+
- mailx-settings 0.1.92: atomicWriteText (tmp + fsync + rename) exported and
|
|
12
|
+
used by every local-cache writer; writeTextIfChanged skips identical
|
|
13
|
+
content; readJsonc reports jsonc-parser errors instead of returning nothing.
|
|
14
|
+
- mailx-imap 0.1.185: pollCloud writes through atomicWriteText.
|
|
15
|
+
- bin/mailx.ts: reconcileAccountsFromDisk — new enabled accounts are added
|
|
16
|
+
live (addAccount + inbox check + sync + IDLE) on any accounts.jsonc change,
|
|
17
|
+
including the boot-time cloud refresh; configChanged carries restartNeeded.
|
|
18
|
+
- client: the Restart banner shows only when restartNeeded; otherwise a
|
|
19
|
+
status-bar line names what changed.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# accounts.jsonc zeroed by a kernel crash — daemon booted with 0 accounts (2026-09-13)
|
|
2
|
+
|
|
3
|
+
Session: Claude Fable 5.1, started 2026-09-13 ~17:00 EDT.
|
|
4
|
+
|
|
5
|
+
## Evidence
|
|
6
|
+
- Windows System log: Kernel-Power 41 + 6008 (unexpected shutdown) at 16:36 today.
|
|
7
|
+
- `~/.rmfmail/accounts.jsonc` (1932 bytes) came back as 1932 NUL bytes (NTFS commits size, not data).
|
|
8
|
+
- Boot 16:50:58: `settings loaded (0 accounts)`, `addAccount loop starting` / `complete` with NO accounts.
|
|
9
|
+
jsonc-parser `parse()` returns undefined on NULs (no throw) → readJsonc returned nothing, no log line.
|
|
10
|
+
- 16:51:12.489 background `loadAccountsAsync` read cloud (4 accounts), `cloudRead` rewrote the local cache
|
|
11
|
+
→ fs.watch saw NULs→JSON → `configChanged` → client banner "accounts.jsonc changed — restart to apply".
|
|
12
|
+
- Why the file was in a fresh-write window: `cloudRead` rewrites the cache with plain `writeFileSync` on
|
|
13
|
+
EVERY read, and pollCloud reads every 3 min (log: `[watch] accounts.jsonc fs.watch fired but content
|
|
14
|
+
unchanged` every 3 min all day).
|
|
15
|
+
|
|
16
|
+
## Fix (in progress)
|
|
17
|
+
1. mailx-settings `index.ts`: cloudRead / loadAccounts cache writes → skip when identical, else
|
|
18
|
+
atomic tmp+fsync+rename (`atomicWriteText`, now exported, with fsync). readJsonc logs parse errors.
|
|
19
|
+
2. mailx-imap `index.ts` pollCloud write → atomicWriteText.
|
|
20
|
+
3. bin/mailx.ts: cloud account refresh at boot APPLIES new accounts in-process (addAccount +
|
|
21
|
+
quick check + syncAll + startWatching) instead of only logging "restart to apply".
|
|
22
|
+
4. Bump mailx-settings, mailx-imap; rebuild.cmd.
|
|
23
|
+
|
|
24
|
+
## Status
|
|
25
|
+
- [x] edits - [x] tsc + bundles - [ ] rebuild.cmd (running) - [ ] Bob restarts app
|
|
26
|
+
- DONE.md entry written as v1.2.325; .commitmsg written; versions: settings 0.1.92, imap 0.1.185
|
package/bin/mailx.js
CHANGED
|
@@ -2098,7 +2098,7 @@ async function main() {
|
|
|
2098
2098
|
// for nothing — each was independent. Same for node-tcp-transport
|
|
2099
2099
|
// and mailx-store/file-store.js further down; folded in here so the
|
|
2100
2100
|
// resolution + module-init cost happens once, in parallel.
|
|
2101
|
-
const [{ MailxDB, prewarmParseWorker, storeBus, Store }, { ImapManager }, { MailxService, spawnSyncWorker }, { dispatch, setDebugEvalSink }, { loadSettings, loadAccountsAsync, loadAllowlistAsync, getConfigDir, getStorageInfo, getStorePath }, { NodeTcpTransport }, { FileMessageStore },] = await Promise.all([
|
|
2101
|
+
const [{ MailxDB, prewarmParseWorker, storeBus, Store }, { ImapManager }, { MailxService, spawnSyncWorker }, { dispatch, setDebugEvalSink }, { loadSettings, loadAccounts, loadAccountsAsync, loadAllowlistAsync, getConfigDir, getStorageInfo, getStorePath }, { NodeTcpTransport }, { FileMessageStore },] = await Promise.all([
|
|
2102
2102
|
import("@bobfrankston/mailx-store"),
|
|
2103
2103
|
import("@bobfrankston/mailx-imap"),
|
|
2104
2104
|
import("@bobfrankston/mailx-service"),
|
|
@@ -2124,29 +2124,11 @@ async function main() {
|
|
|
2124
2124
|
prewarmParseWorker();
|
|
2125
2125
|
let settings = loadSettings();
|
|
2126
2126
|
_tick(`settings loaded (${settings.accounts?.length || 0} accounts)`);
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
//
|
|
2133
|
-
// The local accounts.jsonc cache (loaded synchronously above) has the
|
|
2134
|
-
// last-known state; that's what every account+folder+message UI read
|
|
2135
|
-
// depends on. Cloud refresh's only job is "another device added an
|
|
2136
|
-
// account" — fire-and-forget, log + warn if it diverges, the user can
|
|
2137
|
-
// restart for it to take effect. Was a band-aid hidden in a sync
|
|
2138
|
-
// path; per feedback_no_bandaids, fix the real cause.
|
|
2139
|
-
void (async () => {
|
|
2140
|
-
try {
|
|
2141
|
-
const cloudAccounts = await loadAccountsAsync();
|
|
2142
|
-
if (cloudAccounts.length > 0 && cloudAccounts.length !== settings.accounts.length) {
|
|
2143
|
-
console.log(` [cloud] accounts diverged: cache=${settings.accounts.length} cloud=${cloudAccounts.length} — restart to apply`);
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
catch (e) {
|
|
2147
|
-
console.error(` [cloud] background account refresh failed: ${e?.message || e}`);
|
|
2148
|
-
}
|
|
2149
|
-
})();
|
|
2127
|
+
const cloudAccountsRefresh = loadAccountsAsync().catch((e) => {
|
|
2128
|
+
// The boot snapshot stays in force; the 3-minute cloud poll retries.
|
|
2129
|
+
console.error(` [cloud] background account refresh failed: ${e?.message || e}`);
|
|
2130
|
+
return [];
|
|
2131
|
+
});
|
|
2150
2132
|
const db = new MailxDB(getConfigDir());
|
|
2151
2133
|
_tick("DB opened");
|
|
2152
2134
|
// Store — the architectural nexus (DB + .eml files + bus + operations).
|
|
@@ -2984,8 +2966,81 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
|
|
|
2984
2966
|
imapManager.on("accountError", (accountId, error, hint, isOAuth) => {
|
|
2985
2967
|
handle.send({ _event: "accountError", type: "accountError", accountId, error, hint, isOAuth });
|
|
2986
2968
|
});
|
|
2969
|
+
/** Bring the running daemon in line with accounts.jsonc as it is on disk.
|
|
2970
|
+
* New enabled accounts are added live (addAccount + inbox check + sync +
|
|
2971
|
+
* IDLE). Anything that can't be applied to a live account — one removed,
|
|
2972
|
+
* disabled, or with changed settings — is reported with restartNeeded so
|
|
2973
|
+
* the client shows the Restart banner only when a restart really is the
|
|
2974
|
+
* fix. Idempotent: addAccount ignores ids it already has, and the boot
|
|
2975
|
+
* snapshot (settings.accounts) is updated in place.
|
|
2976
|
+
*
|
|
2977
|
+
* 2026-09-13 17:20 EDT — Claude Code (Fable 5.1), at Bob's direction.
|
|
2978
|
+
* Before this every accounts.jsonc change, including the daemon's own
|
|
2979
|
+
* repair of a crash-zeroed cache from the cloud copy, produced only
|
|
2980
|
+
* "restart to apply". */
|
|
2981
|
+
async function reconcileAccountsFromDisk() {
|
|
2982
|
+
const onDisk = loadAccounts();
|
|
2983
|
+
if (onDisk.length === 0) {
|
|
2984
|
+
// A file that reads as empty is a broken file, not a request to
|
|
2985
|
+
// drop every account (readJsonc has already logged the parse error).
|
|
2986
|
+
return { restartNeeded: false, detail: "accounts.jsonc has no accounts — keeping the running set" };
|
|
2987
|
+
}
|
|
2988
|
+
const snapshot = new Map(settings.accounts.map(a => [a.id, a]));
|
|
2989
|
+
const diskIds = new Set(onDisk.map(a => a.id));
|
|
2990
|
+
const added = onDisk.filter(a => a.enabled && !snapshot.has(a.id));
|
|
2991
|
+
const removed = settings.accounts.filter(a => !diskIds.has(a.id)).map(a => a.id);
|
|
2992
|
+
const changed = onDisk
|
|
2993
|
+
.filter(a => snapshot.has(a.id) && JSON.stringify(snapshot.get(a.id)) !== JSON.stringify(a))
|
|
2994
|
+
.map(a => a.id);
|
|
2995
|
+
if (added.length === 0 && removed.length === 0 && changed.length === 0) {
|
|
2996
|
+
return { restartNeeded: false, detail: "accounts.jsonc updated — no account changes" };
|
|
2997
|
+
}
|
|
2998
|
+
const applied = [];
|
|
2999
|
+
for (const account of added) {
|
|
3000
|
+
try {
|
|
3001
|
+
await imapManager.addAccount(account);
|
|
3002
|
+
settings.accounts.push(account);
|
|
3003
|
+
applied.push(account.label || account.name || account.id);
|
|
3004
|
+
console.log(` Account: ${account.label || account.name} (${account.id}) — added from accounts.jsonc`);
|
|
3005
|
+
imapManager.quickInboxCheckAccount(account.id).catch(e => console.error(` [startup-check] ${account.id}: ${e?.message || e}`));
|
|
3006
|
+
}
|
|
3007
|
+
catch (e) {
|
|
3008
|
+
console.error(` Failed: ${account.id}: ${e.message}`);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
if (applied.length > 0) {
|
|
3012
|
+
// syncAll is a no-op while a sync is already running; the periodic
|
|
3013
|
+
// sweep picks the new account up then. startWatching is per-config
|
|
3014
|
+
// and idempotent, so IDLE starts for the new account either way.
|
|
3015
|
+
imapManager.syncAll()
|
|
3016
|
+
.then(() => imapManager.startWatching())
|
|
3017
|
+
.catch(e => console.error(` Sync error: ${e.message}`));
|
|
3018
|
+
}
|
|
3019
|
+
const parts = [];
|
|
3020
|
+
if (applied.length)
|
|
3021
|
+
parts.push(`added ${applied.join(", ")}`);
|
|
3022
|
+
if (removed.length)
|
|
3023
|
+
parts.push(`removed ${removed.join(", ")}`);
|
|
3024
|
+
if (changed.length)
|
|
3025
|
+
parts.push(`changed ${changed.join(", ")}`);
|
|
3026
|
+
const restartNeeded = removed.length > 0 || changed.length > 0;
|
|
3027
|
+
const detail = `accounts.jsonc: ${parts.join("; ")}${restartNeeded ? " — restart to apply" : ""}`;
|
|
3028
|
+
console.log(` [accounts] ${detail}`);
|
|
3029
|
+
return { restartNeeded, detail };
|
|
3030
|
+
}
|
|
2987
3031
|
imapManager.on("configChanged", (filename) => {
|
|
2988
|
-
|
|
3032
|
+
if (filename !== "accounts.jsonc") {
|
|
3033
|
+
handle.send({ _event: "configChanged", type: "configChanged", filename });
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
reconcileAccountsFromDisk()
|
|
3037
|
+
.then(r => handle.send({ _event: "configChanged", type: "configChanged", filename, restartNeeded: r.restartNeeded, detail: r.detail }))
|
|
3038
|
+
.catch((e) => {
|
|
3039
|
+
// Reconcile failed: fall back to the old behaviour — the user can
|
|
3040
|
+
// always restart — and say why in the log.
|
|
3041
|
+
console.error(` [accounts] reconcile failed: ${e?.message || e}`);
|
|
3042
|
+
handle.send({ _event: "configChanged", type: "configChanged", filename, restartNeeded: true, detail: `accounts.jsonc changed — restart to apply (${e?.message || e})` });
|
|
3043
|
+
});
|
|
2989
3044
|
});
|
|
2990
3045
|
imapManager.on("outboxStatus", (status) => {
|
|
2991
3046
|
handle.send({ _event: "outboxStatus", type: "outboxStatus", ...status });
|
|
@@ -3369,6 +3424,22 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
|
|
|
3369
3424
|
imapManager.startOutboxWorker();
|
|
3370
3425
|
imapManager.startSentSweep();
|
|
3371
3426
|
imapManager.watchConfigFiles();
|
|
3427
|
+
// 2026-09-13 17:20 EDT — Claude Code (Fable 5.1), at Bob's direction.
|
|
3428
|
+
// Apply the background cloud refresh started before the DB was even open.
|
|
3429
|
+
// cloudRead has already written the cloud copy into the local cache; if
|
|
3430
|
+
// it differs from the boot snapshot, run it through the same configChanged
|
|
3431
|
+
// path an on-disk edit takes (MailxService drops its accounts cache, then
|
|
3432
|
+
// reconcileAccountsFromDisk adds new accounts live and tells the client).
|
|
3433
|
+
// Emitting here rather than relying on fs.watch closes the race where the
|
|
3434
|
+
// cloud read lands before watchConfigFiles() registered the watcher.
|
|
3435
|
+
void cloudAccountsRefresh.then((cloudAccounts) => {
|
|
3436
|
+
if (cloudAccounts.length === 0)
|
|
3437
|
+
return;
|
|
3438
|
+
if (JSON.stringify(cloudAccounts) === JSON.stringify(settings.accounts))
|
|
3439
|
+
return;
|
|
3440
|
+
console.log(` [cloud] accounts.jsonc differs from the boot snapshot (cache=${settings.accounts.length} cloud=${cloudAccounts.length}) — reconciling`);
|
|
3441
|
+
imapManager.emit("configChanged", "accounts.jsonc");
|
|
3442
|
+
});
|
|
3372
3443
|
// Deploy app-owned reference docs (.md per JSONC schema) to the cloud
|
|
3373
3444
|
// folder so users see them next to the .jsonc they document. Versioned
|
|
3374
3445
|
// by app version — only redeploys when the app updates. Fire-and-forget;
|