mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
fix: keep an empty proof delta distinguishable from a proof-less host
A negotiated stream now sends retiredTerminalSurfaces: [] when nothing is new instead of omitting the field. Absence is the host's "I hold no proofs" signal — which is also what a recreated worktree's fresh host entry publishes — so the client ledger forgets on absence and a successor occupant never inherits its predecessor's proofs, even when the removed frame was missed.
This commit is contained in:
@@ -45,14 +45,16 @@ describe('session tabs retirement proof delta', () => {
|
||||
expect(project(frame(2, [proof(1), proof(2)]))).toEqual(frame(2, [proof(1), proof(2)]))
|
||||
})
|
||||
|
||||
it('sends each proof once and omits the field when nothing is new', () => {
|
||||
// Why `[]` rather than omitting the field: absence is the host's "I hold no proofs" signal and
|
||||
// tells the client to forget, so a delta with nothing new must stay distinguishable from it.
|
||||
it('sends each proof once and an empty list when nothing is new', () => {
|
||||
const project = createSessionTabsRetirementProofDelta([
|
||||
SESSION_TABS_RETIREMENT_PROOF_DELTA_RUNTIME_CAPABILITY
|
||||
])
|
||||
expect(project(frame(1, [proof(1)]))).toEqual(frame(1, [proof(1)]))
|
||||
expect(project(frame(2, [proof(1)]))).toEqual(frame(2))
|
||||
expect(project(frame(2, [proof(1)]))).toEqual(frame(2, []))
|
||||
expect(project(frame(3, [proof(1), proof(2)]))).toEqual(frame(3, [proof(2)]))
|
||||
expect(project(frame(4, [proof(1), proof(2)]))).toEqual(frame(4))
|
||||
expect(project(frame(4, [proof(1), proof(2)]))).toEqual(frame(4, []))
|
||||
})
|
||||
|
||||
it('resends a proof that left the host list and came back', () => {
|
||||
@@ -143,15 +145,14 @@ describe('session.tabs.subscribe retirement proof payload', () => {
|
||||
const legacyTick = JSON.parse(legacy.tick).result
|
||||
const deltaTick = JSON.parse(delta.tick).result
|
||||
expect(legacyTick.retiredTerminalSurfaces).toHaveLength(64)
|
||||
expect(deltaTick.retiredTerminalSurfaces).toBeUndefined()
|
||||
expect(deltaTick.retiredTerminalSurfaces).toEqual([])
|
||||
// Both clients still receive the full list on the initial snapshot.
|
||||
expect(JSON.parse(legacy.initial).result.retiredTerminalSurfaces).toHaveLength(64)
|
||||
expect(JSON.parse(delta.initial).result.retiredTerminalSurfaces).toHaveLength(64)
|
||||
|
||||
// The delta tick keeps a two-byte `[]` so the client can tell "nothing new" from "no proofs".
|
||||
const proofBytes = Buffer.byteLength(JSON.stringify(proofs))
|
||||
expect(proofBytes).toBeGreaterThan(8_000)
|
||||
expect(Buffer.byteLength(legacy.tick) - Buffer.byteLength(delta.tick)).toBeGreaterThanOrEqual(
|
||||
proofBytes
|
||||
)
|
||||
expect(Buffer.byteLength(legacy.tick) - Buffer.byteLength(delta.tick)).toBe(proofBytes - 2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,10 +36,10 @@ export function createSessionTabsRetirementProofDelta(
|
||||
: frame.retiredTerminalSurfaces
|
||||
// Why: track exactly the current list, so a proof that leaves and returns is sent again.
|
||||
sentByWorktree.set(frame.worktree, new Set(frame.retiredTerminalSurfaces.map(proofKey)))
|
||||
if (fresh.length === frame.retiredTerminalSurfaces.length) {
|
||||
return frame
|
||||
}
|
||||
const { retiredTerminalSurfaces: _sentProofs, ...rest } = frame
|
||||
return (fresh.length > 0 ? { ...rest, retiredTerminalSurfaces: fresh } : rest) as TFrame
|
||||
// Why: an empty list is a real signal ("nothing new, keep yours"). Omitting the field would be
|
||||
// indistinguishable from a host that holds no proofs, which is what tells the client to forget.
|
||||
return fresh.length === frame.retiredTerminalSurfaces.length
|
||||
? frame
|
||||
: { ...frame, retiredTerminalSurfaces: fresh }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,31 @@ function frame(
|
||||
describe('web session terminal retirement proof ledger', () => {
|
||||
beforeEach(() => clearRetainedTerminalRetirementProofsForTests())
|
||||
|
||||
// Why: a recreated worktree keeps the same environment and worktree id but its fresh host entry
|
||||
// holds no proofs and omits the field. Absence must forget, so the successor occupant never
|
||||
// inherits its predecessor's proofs even when the removed frame was missed. A delta host with
|
||||
// nothing new sends `[]`, which keeps what was retained.
|
||||
it('forgets on an absent field but keeps proofs on an empty delta', () => {
|
||||
mergeRetainedTerminalRetirementProofs(
|
||||
ENVIRONMENT_ID,
|
||||
frame(1, { retiredTerminalSurfaces: [retired] })
|
||||
)
|
||||
expect(
|
||||
mergeRetainedTerminalRetirementProofs(
|
||||
ENVIRONMENT_ID,
|
||||
frame(2, { retiredTerminalSurfaces: [] })
|
||||
).retiredTerminalSurfaces
|
||||
).toEqual([retired])
|
||||
const successor = frame(3)
|
||||
expect(mergeRetainedTerminalRetirementProofs(ENVIRONMENT_ID, successor)).toBe(successor)
|
||||
expect(
|
||||
mergeRetainedTerminalRetirementProofs(
|
||||
ENVIRONMENT_ID,
|
||||
frame(4, { retiredTerminalSurfaces: [] })
|
||||
).retiredTerminalSurfaces
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('carries a proof sent once into later delta frames for the same worktree', () => {
|
||||
expect(
|
||||
mergeRetainedTerminalRetirementProofs(
|
||||
@@ -46,11 +71,17 @@ describe('web session terminal retirement proof ledger', () => {
|
||||
).retiredTerminalSurfaces
|
||||
).toEqual([retired])
|
||||
expect(
|
||||
mergeRetainedTerminalRetirementProofs(ENVIRONMENT_ID, frame(2)).retiredTerminalSurfaces
|
||||
mergeRetainedTerminalRetirementProofs(
|
||||
ENVIRONMENT_ID,
|
||||
frame(2, { retiredTerminalSurfaces: [] })
|
||||
).retiredTerminalSurfaces
|
||||
).toEqual([retired])
|
||||
expect(
|
||||
mergeRetainedTerminalRetirementProofs('other-environment', frame(3)).retiredTerminalSurfaces
|
||||
).toBeUndefined()
|
||||
mergeRetainedTerminalRetirementProofs(
|
||||
'other-environment',
|
||||
frame(3, { retiredTerminalSurfaces: [] })
|
||||
).retiredTerminalSurfaces
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('forgets a proof once the host publishes its surface live again', () => {
|
||||
@@ -146,11 +177,9 @@ describe('orphan recovery over delta frames', () => {
|
||||
|
||||
const second = await recoverWebSessionTerminalOrphansBeforeApply(
|
||||
state,
|
||||
frame(2),
|
||||
frame(2, { retiredTerminalSurfaces: [] }),
|
||||
ENVIRONMENT_ID,
|
||||
{
|
||||
call: call as never
|
||||
}
|
||||
{ call: call as never }
|
||||
)
|
||||
expect(second?.tabs).toEqual([])
|
||||
expect(second?.retiredTerminalSurfaces).toEqual([retired])
|
||||
|
||||
@@ -36,15 +36,23 @@ export function mergeRetainedTerminalRetirementProofs(
|
||||
retainedByKey.delete(key)
|
||||
return snapshot
|
||||
}
|
||||
// Why: a host that holds no proofs omits the field; a delta host with nothing new sends `[]`.
|
||||
// Absence therefore means "forget" — which is also what a recreated worktree's fresh host entry
|
||||
// publishes, so a new occupant never inherits its predecessor's proofs even if the removed
|
||||
// frame was missed.
|
||||
if (snapshot.retiredTerminalSurfaces === undefined) {
|
||||
retainedByKey.delete(key)
|
||||
return snapshot
|
||||
}
|
||||
const connectionGeneration = getRuntimeEnvironmentConnectionGeneration(environmentId)
|
||||
const cached = retainedByKey.get(key)
|
||||
const retained = cached?.connectionGeneration === connectionGeneration ? cached.proofs : undefined
|
||||
if (!retained && !snapshot.retiredTerminalSurfaces?.length) {
|
||||
if (!retained && snapshot.retiredTerminalSurfaces.length === 0) {
|
||||
retainedByKey.delete(key)
|
||||
return snapshot
|
||||
}
|
||||
const merged = dropRetirementProofsForLiveSurfaces(
|
||||
appendRetiredTerminalSurfaceProofs(retained, snapshot.retiredTerminalSurfaces ?? []),
|
||||
appendRetiredTerminalSurfaceProofs(retained, snapshot.retiredTerminalSurfaces),
|
||||
snapshot.tabs
|
||||
)
|
||||
retainedByKey.delete(key)
|
||||
|
||||
Reference in New Issue
Block a user