Skip to content

Commit 7d3d58d

Browse files
committed
fix(chat): don't revive an aged-out chip handoff when accumulating
Cursor Bugbot: pendingContexts merged a prior chip-only handoff without checking its age, while store stamps a fresh timestamp on every write. An abandoned handoff that had already passed max-age would therefore ride along on the next 'Add to chat' and fire on the following navigation as if current. Introduced by the accumulation behavior added earlier in this PR. Applied the same freshness bar consume uses, and hoisted the 60s window into a single MAX_AGE_MS constant so the two paths cannot drift.
1 parent 598791e commit 7d3d58d

2 files changed

Lines changed: 44 additions & 3 deletions

File tree

apps/sim/lib/core/utils/browser-storage.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,31 @@ describe('MothershipHandoffStorage', () => {
8080
expect(MothershipHandoffStorage.consume(WS)).toEqual({ contexts: [first, second] })
8181
})
8282

83+
it('does not revive chips from a handoff that already aged out', () => {
84+
vi.useFakeTimers()
85+
try {
86+
vi.setSystemTime(new Date('2026-01-01T00:00:00Z'))
87+
const abandoned = chipContext()
88+
MothershipHandoffStorage.store({ contexts: [abandoned] }, WS)
89+
90+
// The user walks away; the handoff expires in place. A later "Add to chat"
91+
// stamps a fresh timestamp, which must not carry the dead chip forward.
92+
vi.advanceTimersByTime(61 * 1000)
93+
const fresh: ChatContext = {
94+
kind: 'table_selection',
95+
tableId: 't1',
96+
tableName: 'T',
97+
label: 'T (1 row)',
98+
rowIds: ['r'],
99+
}
100+
MothershipHandoffStorage.store({ contexts: [fresh] }, WS)
101+
102+
expect(MothershipHandoffStorage.consume(WS)).toEqual({ contexts: [fresh] })
103+
} finally {
104+
vi.useRealTimers()
105+
}
106+
})
107+
83108
it('does not accumulate chips onto a message handoff, or across workspaces', () => {
84109
const chip = chipContext()
85110
MothershipHandoffStorage.store({ contexts: [chip] }, WS)

apps/sim/lib/core/utils/browser-storage.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,9 @@ interface StoredHandoff extends MothershipHandoff {
330330
export class MothershipHandoffStorage {
331331
private static readonly KEY = STORAGE_KEYS.MOTHERSHIP_HANDOFF
332332

333+
/** How long a stored handoff stays eligible to fire, in milliseconds. */
334+
static readonly MAX_AGE_MS = 60 * 1000
335+
333336
/**
334337
* Store a handoff for the next home-surface mount, scoped to the workspace it
335338
* targets so a different workspace never claims it. Chip-only handoffs
@@ -355,10 +358,20 @@ export class MothershipHandoffStorage {
355358
})
356359
}
357360

358-
/** Contexts of an un-consumed chip-only handoff for `workspaceId`, else empty. */
361+
/**
362+
* Contexts of an un-consumed chip-only handoff for `workspaceId`, else empty.
363+
*
364+
* Applies the same freshness bar as {@link consume}: accumulating carries the
365+
* old contexts onto a write that stamps a new `timestamp`, so without this an
366+
* abandoned handoff that had already aged out would ride along on the next
367+
* "Add to chat" and reappear as if it were current.
368+
*/
359369
private static pendingContexts(workspaceId: string): ChatContext[] {
360370
const data = BrowserStorage.getItem<StoredHandoff | null>(MothershipHandoffStorage.KEY, null)
361371
if (!data || data.message || data.workspaceId !== workspaceId) return []
372+
if (!data.timestamp || Date.now() - data.timestamp > MothershipHandoffStorage.MAX_AGE_MS) {
373+
return []
374+
}
362375
return Array.isArray(data.contexts) ? data.contexts : []
363376
}
364377

@@ -368,9 +381,12 @@ export class MothershipHandoffStorage {
368381
* only resolves in its own workspace, so misfiring it elsewhere would drop the
369382
* context. The owner (and any legacy/corrupt entry) is tombstoned via `clear`
370383
* before the validity/expiry checks so it fires at most once and never lingers.
371-
* @param maxAge - Maximum age in milliseconds (default: 60 seconds)
384+
* @param maxAge - Maximum age in milliseconds (default: {@link MAX_AGE_MS})
372385
*/
373-
static consume(workspaceId: string, maxAge: number = 60 * 1000): MothershipHandoff | null {
386+
static consume(
387+
workspaceId: string,
388+
maxAge: number = MothershipHandoffStorage.MAX_AGE_MS
389+
): MothershipHandoff | null {
374390
const data = BrowserStorage.getItem<StoredHandoff | null>(MothershipHandoffStorage.KEY, null)
375391

376392
if (!data) {

0 commit comments

Comments
 (0)