Hmbown/CodeWhale · error · Error
This saved trace changed in another tab. Reopen it before…
Error message
This saved trace changed in another tab. Reopen it before updating, or use Save separate copy.
What it means
This is the version-conflict branch of the same optimistic lock: save(trace, expected) compares the stored record's savedAt with expected.savedAt. If another tab modified the trace (advancing savedAt by at least 1 ms) after you loaded it, the save aborts with this message so concurrent edits never silently overwrite each other.
Solutions
- Reopen the trace to load its current savedAt and reapply your edits, then save with the fresh expected reference
- Use Save separate copy (save without expected) to keep your edits under a new key without clobbering
- Diff your edit against the current stored version before re-saving to avoid losing the other tab's changes
- Avoid editing the same trace in multiple tabs, or refresh after external saves
Example fix
// before: stale expected reference
await library.save(myEdits, staleRef); // savedAt mismatch -> throws
// after: reload current version, merge, save with fresh expected
const fresh = (await library.list()).find(t => t.key === staleRef.key);
const current = await library.getEntry(fresh.key);
await library.save(merge(current.trace, myEdits), { key: fresh.key, savedAt: fresh.savedAt }); Defensive patterns
Strategy: try-catch
Validate before calling
const current = (await library.list()).find(s => s.key === expected.key);
if (current && current.savedAt !== expected.savedAt) console.warn('trace changed elsewhere; reopen before updating'); Type guard
const isChangedElsewhere = (e) => e instanceof Error && e.message.includes('changed in another tab'); Try / catch
try {
await library.save(trace, expected);
} catch (e) {
if (isChangedElsewhere(e)) {
const fresh = await library.getEntry(expected.key); // reopen to get new savedAt
await library.save(merge(fresh.trace, trace), { key: expected.key, savedAt: fresh.savedAt });
} else throw e;
} Prevention
- Always send the savedAt you loaded as `expected` so conflicts are detected
- Reopen/reload a trace before updating if it may have been edited elsewhere
- Avoid editing the same trace concurrently in multiple tabs
- Surface a merge or 'save as copy' choice when a version conflict is detected
When it happens
Trigger: Calling save(trace, expected) where traces.get(key) returns a record whose savedAt differs from expected.savedAt — i.e. the record was updated elsewhere between load and save.
Common situations: Two tabs editing the same saved trace simultaneously; one tab auto-saves while the other holds a stale copy; replaying/importing overwrites a trace in another tab; long-lived editor sessions where the stored savedAt moved on.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- This saved trace was deleted in another tab. Use Save…
- A saved trace identifier collided. Try saving again.
- a window change is already in progress
- Another pet owner is running
- Another pet recorder is using this output.
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/9a7aae498f430d72.
Report an issue: GitHub.
Appendix: source
Thrown at pet/src/ui/storage.ts:82
let result: T, failure: Error | undefined;
const fail = (error: Error) => { failure ??= error; try { tx.abort(); } catch { reject(failure); } };
tx.oncomplete = () => failure ? reject(failure) : resolve(result);
tx.onerror = () => { failure ??= problem(tx.error); };
tx.onabort = () => reject(failure ?? problem(tx.error));
try { action(tx.objectStore('traces'), tx.objectStore('summaries'), value => { result = value; }, fail, tx.objectStore('habitats')); }
catch (e) { fail(e as Error); }
});
}
async save(trace: Trace, expected?: SavedReference): Promise<SavedReference> {
await this.open(); // Origin error before secure-context-only randomUUID.
const key = expected?.key ?? crypto.randomUUID();
return this.transaction('readwrite', (traces, metas, done, fail) => {
const req = traces.get(key);
req.onsuccess = () => {
try {
const previous: SavedTrace | undefined = req.result;
if (expected && !previous) throw missing();
if (expected && previous!.savedAt !== expected.savedAt) throw changed();
if (!expected && previous) throw new Error('A saved trace identifier collided. Try saving again.');
const savedAt = this.nextTime(previous?.savedAt), item: SavedTrace = { key, savedAt, trace };
traces.put(item); metas.put(summary(item)); done({ key, savedAt });
} catch (e) { fail(e as Error); }
};
});
}
private nextTime(previous?: string): string {
const last = previous ? Date.parse(previous) : 0;
return new Date(Math.max(Date.now(), Number.isFinite(last) ? last + 1 : 0)).toISOString();
}
async list(): Promise<SavedSummary[]> {
return this.transaction('readonly', (_traces, metas, done) => {
const req = metas.getAll();
req.onsuccess = () => done((req.result as SavedSummary[]).sort((a, b) => b.savedAt.localeCompare(a.savedAt)));
});
}
async getEntry(key: string): Promise<SavedTrace> {View on GitHub (pinned to 433685b202)