mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
fix(ssh): fold the pane incarnation with the binding it fences
Found by adversarial review. `persistPtyBinding` writes the binding and its incarnation into the SAME partition, and the incarnation is what its CAS compares. Step P's load fold moved only `ptyIdsByLeafId`, so after upgrade a pane whose incarnation had been written to `ssh:<target>` kept the guard in the partition nothing reads: `resolvePersistedStablePaneOwner` read `undefined` from `local`, the CAS then compared undefined against undefined, and the incarnation half of the fence passed for any value until the next write healed it. Not data loss and not a wrong-shell bind — the ptyId half still held — but a guard silently weakened by a migration is the exact shape this program keeps finding, so it is closed rather than noted. Mutation proof: disabling the incarnation half of the fold reddens the new clause; the other eight stay green. One clause in persistence.test.ts asserted the incarnation survives a reload in the SSH partition. Its subject is that the reconciled value is preserved, not which partition holds it, so the assertion follows the binding to its one home and additionally pins that the ssh partition no longer keeps a copy.
This commit is contained in:
@@ -9258,10 +9258,21 @@ describe('Store', () => {
|
||||
expect(store.getWorkspaceSession(hostId).terminalPtyIncarnationsByPaneKey?.[paneKey]).toBe(
|
||||
'inc-live'
|
||||
)
|
||||
// The in-session assertion above is unchanged. Across a RELOAD the SSH case now resolves in
|
||||
// the local partition: STA-3077 step P gave an SSH pane binding one home, and the load fold
|
||||
// carries the incarnation with the binding it fences — leaving it behind would strand the CAS
|
||||
// guard in a partition no reader consults. The preserved value is what this pins, not which
|
||||
// partition holds it, so the assertion follows the binding.
|
||||
const reloaded = await createStore()
|
||||
expect(reloaded.getWorkspaceSession(hostId).terminalPtyIncarnationsByPaneKey?.[paneKey]).toBe(
|
||||
'inc-live'
|
||||
)
|
||||
const foldedHostId = hostId?.startsWith('ssh:') ? undefined : hostId
|
||||
expect(
|
||||
reloaded.getWorkspaceSession(foldedHostId).terminalPtyIncarnationsByPaneKey?.[paneKey]
|
||||
).toBe('inc-live')
|
||||
if (hostId?.startsWith('ssh:')) {
|
||||
expect(
|
||||
reloaded.getWorkspaceSession(hostId).terminalPtyIncarnationsByPaneKey?.[paneKey]
|
||||
).toBeUndefined()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -7290,6 +7290,18 @@ export class Store {
|
||||
layout.ptyIdsByLeafId = {}
|
||||
changed = true
|
||||
}
|
||||
// The incarnation is the other half of the same fence — `persistPtyBinding` writes it into
|
||||
// whichever partition it wrote the binding to, so leaving it behind would move the binding
|
||||
// while its CAS guard stayed in the partition nothing reads.
|
||||
const incarnations = host?.terminalPtyIncarnationsByPaneKey
|
||||
if (incarnations && Object.keys(incarnations).length > 0) {
|
||||
local.terminalPtyIncarnationsByPaneKey = {
|
||||
...incarnations,
|
||||
...local.terminalPtyIncarnationsByPaneKey
|
||||
}
|
||||
host.terminalPtyIncarnationsByPaneKey = {}
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
@@ -251,6 +251,30 @@ describe('STA-3077 step P: the pane binding has one home', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('STA-3077 step P: the fold carries the whole fence', () => {
|
||||
// `persistPtyBinding` writes the binding and its incarnation into the SAME partition, and the
|
||||
// incarnation is what its CAS compares. Folding only the binding would move the guard's subject
|
||||
// to `local` while the guard itself stayed in the partition nothing reads — the CAS would then
|
||||
// compare undefined against undefined and pass for any incarnation.
|
||||
it('folds the pane incarnation along with the binding it fences', async () => {
|
||||
const sshSession = paneSession('pty-1') as unknown as {
|
||||
terminalPtyIncarnationsByPaneKey: Record<string, string>
|
||||
}
|
||||
sshSession.terminalPtyIncarnationsByPaneKey = { [`${TAB}:${LEAF}`]: 'inc-pty-1' }
|
||||
const store = await createStore({
|
||||
workspaceSession: paneSession('pty-1'),
|
||||
workspaceSessionsByHostId: { [SSH_PARTITION]: sshSession }
|
||||
})
|
||||
|
||||
expect(store.getWorkspaceSession().terminalPtyIncarnationsByPaneKey?.[`${TAB}:${LEAF}`]).toBe(
|
||||
'inc-pty-1'
|
||||
)
|
||||
expect(
|
||||
store.getWorkspaceSession(SSH_PARTITION).terminalPtyIncarnationsByPaneKey?.[`${TAB}:${LEAF}`]
|
||||
).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('STA-3077 step P: every production caller names that one home', () => {
|
||||
const summarize = (source: string): string => source.replace(/\s+/g, ' ').trim().slice(0, 90)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user