mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 08:03:33 +00:00
Merge remote-tracking branch 'origin/main' into brennanb2025/mobile-i18n-audit
# Conflicts: # mobile/src/session/MobileTerminalLiveInputStatus.tsx # mobile/src/terminal/terminal-live-input-affordance.test.ts
This commit is contained in:
@@ -4986,6 +4986,7 @@ export default function SessionScreen() {
|
||||
<MobileTerminalLiveInputStatus
|
||||
dictation={dictation}
|
||||
isAttaching={isAttaching}
|
||||
liveInputText={liveInputCapture}
|
||||
/>
|
||||
</Pressable>
|
||||
<MobileTerminalInputActions
|
||||
|
||||
@@ -11,11 +11,13 @@ type DictationStatus = {
|
||||
type MobileTerminalLiveInputStatusProps = {
|
||||
readonly dictation: DictationStatus
|
||||
readonly isAttaching: boolean
|
||||
readonly liveInputText: string
|
||||
}
|
||||
|
||||
export function MobileTerminalLiveInputStatus({
|
||||
dictation,
|
||||
isAttaching
|
||||
isAttaching,
|
||||
liveInputText
|
||||
}: MobileTerminalLiveInputStatusProps) {
|
||||
const title = dictation.isRecording
|
||||
? t('mobileTerminalLiveInputStatus.listening')
|
||||
@@ -32,14 +34,14 @@ export function MobileTerminalLiveInputStatus({
|
||||
? t('mobileTerminalLiveInputStatus.preparing')
|
||||
: isAttaching
|
||||
? t('mobileTerminalLiveInputStatus.uploading')
|
||||
: t('mobileTerminalLiveInputStatus.tapShow')
|
||||
: liveInputText || t('mobileTerminalLiveInputStatus.tapShow')
|
||||
|
||||
return (
|
||||
<View style={styles.status}>
|
||||
<Text style={styles.title} numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text style={styles.detail} numberOfLines={1}>
|
||||
<Text style={styles.detail} numberOfLines={1} ellipsizeMode="head">
|
||||
{detail}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -37,6 +37,7 @@ describe('terminal live input affordance', () => {
|
||||
expect(block).toContain('pressed && styles.liveInputFocusTargetPressed')
|
||||
expect(block).toContain('!canSend && styles.liveInputFocusTargetDisabled')
|
||||
expect(block).toContain('showSoftInputOnFocus')
|
||||
expect(block).toContain('liveInputText={liveInputCapture}')
|
||||
expect(sessionRouteSource).toContain('useTerminalLiveInputFocus({')
|
||||
expect(sessionRouteSource).toContain('return resetLiveInputFocus')
|
||||
expect(liveInputFocusSource).toContain('focusTerminalLiveInputTarget(inputRef.current')
|
||||
@@ -50,6 +51,10 @@ describe('terminal live input affordance', () => {
|
||||
|
||||
it('makes the live keyboard target visible instead of status-only chrome', () => {
|
||||
expect(liveInputStatusSource).toContain("t('mobileTerminalLiveInputStatus.tapShow')")
|
||||
expect(liveInputStatusSource).toContain(
|
||||
"liveInputText || t('mobileTerminalLiveInputStatus.tapShow')"
|
||||
)
|
||||
expect(liveInputStatusSource).toContain('ellipsizeMode="head"')
|
||||
expect(commandInputStylesSource).toContain('backgroundColor: colors.bgRaised')
|
||||
expect(commandInputStylesSource).toContain('borderWidth: 1')
|
||||
expect(commandInputStylesSource).toContain('liveInputFocusTargetPressed')
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { sendTerminalLiveControlAfterPendingFlush } from './terminal-live-control-send-order'
|
||||
import {
|
||||
cancelTerminalLivePendingFlush,
|
||||
createTerminalLivePendingFlushState,
|
||||
queueTerminalLiveMirrorSend,
|
||||
waitForTerminalLivePendingFlush,
|
||||
type TerminalLivePendingFlushState
|
||||
waitForTerminalLivePendingFlush
|
||||
} from './terminal-live-pending-flush-state'
|
||||
|
||||
describe('terminal live pending flush state', () => {
|
||||
it('Given no in-flight flush When waiting for the barrier Then allows control input', async () => {
|
||||
// Given
|
||||
const state: TerminalLivePendingFlushState = { current: null }
|
||||
const state = createTerminalLivePendingFlushState()
|
||||
|
||||
// When / Then
|
||||
await expect(waitForTerminalLivePendingFlush(state)).resolves.toBe(true)
|
||||
@@ -22,7 +23,8 @@ describe('terminal live pending flush state', () => {
|
||||
const flushPromise = new Promise<boolean>((resolve) => {
|
||||
resolveFlush = resolve
|
||||
})
|
||||
const state: TerminalLivePendingFlushState = { current: flushPromise }
|
||||
const state = createTerminalLivePendingFlushState()
|
||||
state.current = flushPromise
|
||||
|
||||
// When
|
||||
const controlSend = sendTerminalLiveControlAfterPendingFlush(
|
||||
@@ -48,7 +50,8 @@ describe('terminal live pending flush state', () => {
|
||||
const flushPromise = new Promise<boolean>((resolve) => {
|
||||
resolveFlush = resolve
|
||||
})
|
||||
const state: TerminalLivePendingFlushState = { current: flushPromise }
|
||||
const state = createTerminalLivePendingFlushState()
|
||||
state.current = flushPromise
|
||||
|
||||
// When
|
||||
const controlSend = sendTerminalLiveControlAfterPendingFlush(
|
||||
@@ -67,17 +70,45 @@ describe('terminal live pending flush state', () => {
|
||||
})
|
||||
|
||||
describe('terminal live mirror send queue', () => {
|
||||
it('Given high RTT When more input queues Then pending bytes share one follow-up send', async () => {
|
||||
// Given
|
||||
const state = createTerminalLivePendingFlushState()
|
||||
const payloads: string[] = []
|
||||
let resolveFirstSend: (value: boolean) => void = () => {}
|
||||
const sender = async (_handle: string, payload: string): Promise<boolean> => {
|
||||
payloads.push(payload)
|
||||
if (payloads.length === 1) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
resolveFirstSend = resolve
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// When
|
||||
const first = queueTerminalLiveMirrorSend(state, 'terminal-1', 'a', sender)
|
||||
const second = queueTerminalLiveMirrorSend(state, 'terminal-1', 'b', sender)
|
||||
const third = queueTerminalLiveMirrorSend(state, 'terminal-1', 'c', sender)
|
||||
await Promise.resolve()
|
||||
|
||||
// Then
|
||||
expect(payloads).toEqual(['a'])
|
||||
resolveFirstSend(true)
|
||||
await expect(Promise.all([first, second, third])).resolves.toEqual([true, true, true])
|
||||
expect(payloads).toEqual(['a', 'bc'])
|
||||
})
|
||||
|
||||
it('Given a failed previous send When a mirror send queues Then it still runs in order', async () => {
|
||||
// Given
|
||||
const state: TerminalLivePendingFlushState = { current: null }
|
||||
const state = createTerminalLivePendingFlushState()
|
||||
const order: string[] = []
|
||||
const first = queueTerminalLiveMirrorSend(state, async () => {
|
||||
const first = queueTerminalLiveMirrorSend(state, 'terminal-1', 'first', async () => {
|
||||
order.push('first')
|
||||
return false
|
||||
})
|
||||
|
||||
// When
|
||||
const second = queueTerminalLiveMirrorSend(state, async () => {
|
||||
const second = queueTerminalLiveMirrorSend(state, 'terminal-1', 'second', async () => {
|
||||
order.push('second')
|
||||
return true
|
||||
})
|
||||
@@ -90,13 +121,13 @@ describe('terminal live mirror send queue', () => {
|
||||
|
||||
it('Given a throwing send When a mirror send queues Then the promise resolves false and the chain continues', async () => {
|
||||
// Given
|
||||
const state: TerminalLivePendingFlushState = { current: null }
|
||||
const first = queueTerminalLiveMirrorSend(state, async () => {
|
||||
const state = createTerminalLivePendingFlushState()
|
||||
const first = queueTerminalLiveMirrorSend(state, 'terminal-1', 'first', async () => {
|
||||
throw new Error('boom')
|
||||
})
|
||||
|
||||
// When
|
||||
const second = queueTerminalLiveMirrorSend(state, async () => true)
|
||||
const second = queueTerminalLiveMirrorSend(state, 'terminal-1', 'second', async () => true)
|
||||
|
||||
// Then
|
||||
await expect(first).resolves.toBe(false)
|
||||
@@ -105,13 +136,33 @@ describe('terminal live mirror send queue', () => {
|
||||
|
||||
it('Given a settled mirror send When it was the newest Then the state resets to null', async () => {
|
||||
// Given
|
||||
const state: TerminalLivePendingFlushState = { current: null }
|
||||
const state = createTerminalLivePendingFlushState()
|
||||
|
||||
// When
|
||||
await queueTerminalLiveMirrorSend(state, async () => true)
|
||||
await queueTerminalLiveMirrorSend(state, 'terminal-1', 'payload', async () => true)
|
||||
await Promise.resolve()
|
||||
|
||||
// Then
|
||||
expect(state.current).toBeNull()
|
||||
})
|
||||
|
||||
it('Given queued input When the queue is cancelled Then unsent input is dropped', async () => {
|
||||
// Given
|
||||
const state = createTerminalLivePendingFlushState()
|
||||
let resolveSend: (value: boolean) => void = () => {}
|
||||
const sender = async (): Promise<boolean> =>
|
||||
new Promise((resolve) => {
|
||||
resolveSend = resolve
|
||||
})
|
||||
const active = queueTerminalLiveMirrorSend(state, 'terminal-1', 'a', sender)
|
||||
const pending = queueTerminalLiveMirrorSend(state, 'terminal-1', 'b', sender)
|
||||
|
||||
// When
|
||||
cancelTerminalLivePendingFlush(state)
|
||||
|
||||
// Then
|
||||
await expect(Promise.all([active, pending])).resolves.toEqual([false, false])
|
||||
expect(state.current).toBeNull()
|
||||
resolveSend(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
type TerminalLiveMirrorSender = (handle: string, payload: string) => Promise<boolean>
|
||||
|
||||
type TerminalLivePendingRequest = {
|
||||
readonly resolve: (sent: boolean) => void
|
||||
}
|
||||
|
||||
type TerminalLivePendingBatch = {
|
||||
readonly handle: string
|
||||
payload: string
|
||||
readonly requests: TerminalLivePendingRequest[]
|
||||
readonly sender: TerminalLiveMirrorSender
|
||||
}
|
||||
|
||||
export type TerminalLivePendingFlushState = {
|
||||
current: Promise<boolean> | null
|
||||
activeRequests: TerminalLivePendingRequest[]
|
||||
generation: number
|
||||
pendingBatches: TerminalLivePendingBatch[]
|
||||
}
|
||||
|
||||
export function createTerminalLivePendingFlushState(): TerminalLivePendingFlushState {
|
||||
return {
|
||||
current: null,
|
||||
activeRequests: [],
|
||||
generation: 0,
|
||||
pendingBatches: []
|
||||
}
|
||||
}
|
||||
|
||||
export function waitForTerminalLivePendingFlush(
|
||||
@@ -8,26 +33,76 @@ export function waitForTerminalLivePendingFlush(
|
||||
return state.current ?? Promise.resolve(true)
|
||||
}
|
||||
|
||||
// Why: mirror payloads are erase/append deltas against the PTY echo. A skipped
|
||||
// delta desyncs every later diff, so this chain runs each send even when the
|
||||
// previous one failed. state.current should never reject; the catch keeps a
|
||||
// future raw assignment from skipping a delta.
|
||||
export function cancelTerminalLivePendingFlush(state: TerminalLivePendingFlushState): void {
|
||||
state.generation += 1
|
||||
const requests = [
|
||||
...state.activeRequests,
|
||||
...state.pendingBatches.flatMap((batch) => batch.requests)
|
||||
]
|
||||
state.activeRequests = []
|
||||
state.pendingBatches = []
|
||||
state.current = null
|
||||
requests.forEach(({ resolve }) => resolve(false))
|
||||
}
|
||||
|
||||
async function drainTerminalLiveMirrorSends(
|
||||
state: TerminalLivePendingFlushState,
|
||||
generation: number
|
||||
): Promise<boolean> {
|
||||
let allSent = true
|
||||
while (state.generation === generation) {
|
||||
const batch = state.pendingBatches.shift()
|
||||
if (!batch) {
|
||||
state.current = null
|
||||
return allSent
|
||||
}
|
||||
|
||||
state.activeRequests = batch.requests
|
||||
const sent = await batch.sender(batch.handle, batch.payload).catch(() => false)
|
||||
if (state.generation !== generation) {
|
||||
return false
|
||||
}
|
||||
|
||||
state.activeRequests = []
|
||||
batch.requests.forEach(({ resolve }) => resolve(sent))
|
||||
allSent &&= sent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Mirror deltas are ordered PTY bytes; batching pending bytes avoids one RTT per keystroke.
|
||||
export function queueTerminalLiveMirrorSend(
|
||||
state: TerminalLivePendingFlushState,
|
||||
sendMirrorPayload: () => Promise<boolean>
|
||||
handle: string,
|
||||
payload: string,
|
||||
sender: TerminalLiveMirrorSender
|
||||
): Promise<boolean> {
|
||||
const previousSend = state.current
|
||||
const sendPromise = (async () => {
|
||||
if (previousSend) {
|
||||
await previousSend.catch(() => false)
|
||||
}
|
||||
return sendMirrorPayload()
|
||||
})().catch(() => false)
|
||||
state.current = sendPromise
|
||||
void sendPromise.then(() => {
|
||||
if (state.current === sendPromise) {
|
||||
state.current = null
|
||||
}
|
||||
let resolveRequest: (sent: boolean) => void = () => {}
|
||||
const request = new Promise<boolean>((resolve) => {
|
||||
resolveRequest = resolve
|
||||
})
|
||||
return sendPromise
|
||||
const pendingTail = state.pendingBatches.at(-1)
|
||||
if (pendingTail?.handle === handle && pendingTail.sender === sender) {
|
||||
pendingTail.payload += payload
|
||||
pendingTail.requests.push({ resolve: resolveRequest })
|
||||
} else {
|
||||
state.pendingBatches.push({
|
||||
handle,
|
||||
payload,
|
||||
requests: [{ resolve: resolveRequest }],
|
||||
sender
|
||||
})
|
||||
}
|
||||
|
||||
if (!state.current) {
|
||||
const generation = state.generation
|
||||
const drain = drainTerminalLiveMirrorSends(state, generation).catch(() => false)
|
||||
state.current = drain
|
||||
void drain.then(() => {
|
||||
if (state.current === drain) {
|
||||
state.current = null
|
||||
}
|
||||
})
|
||||
}
|
||||
return request
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
TERMINAL_LIVE_HELD_SYLLABLE_COMMIT_DELAY_MS
|
||||
} from './terminal-live-hangul-mirror'
|
||||
import {
|
||||
cancelTerminalLivePendingFlush,
|
||||
createTerminalLivePendingFlushState,
|
||||
queueTerminalLiveMirrorSend,
|
||||
waitForTerminalLivePendingFlush
|
||||
} from './terminal-live-pending-flush-state'
|
||||
@@ -39,7 +41,7 @@ export function useTerminalLivePendingInputFlush<TTabType extends string>({
|
||||
setLiveInputCapture
|
||||
}: TerminalLivePendingInputFlushOptions<TTabType>): TerminalLivePendingInputFlush {
|
||||
const heldCommitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const pendingLiveInputFlushRef = useRef<Promise<boolean> | null>(null)
|
||||
const pendingLiveInputFlushRef = useRef(createTerminalLivePendingFlushState())
|
||||
const heldLiveInputTextRef = useRef('')
|
||||
const sentLiveInputTextRef = useRef('')
|
||||
const pendingLiveInputHandleRef = useRef<string | null>(null)
|
||||
@@ -56,6 +58,7 @@ export function useTerminalLivePendingInputFlush<TTabType extends string>({
|
||||
|
||||
const resetMirrorState = useCallback(() => {
|
||||
clearHeldCommitTimer()
|
||||
cancelTerminalLivePendingFlush(pendingLiveInputFlushRef.current)
|
||||
heldLiveInputTextRef.current = ''
|
||||
sentLiveInputTextRef.current = ''
|
||||
pendingLiveInputHandleRef.current = null
|
||||
@@ -68,9 +71,15 @@ export function useTerminalLivePendingInputFlush<TTabType extends string>({
|
||||
}, [liveInputRef, resetMirrorState, setLiveInputCapture])
|
||||
|
||||
const waitForPendingLiveInputFlush = useCallback(async (): Promise<boolean> => {
|
||||
return waitForTerminalLivePendingFlush(pendingLiveInputFlushRef)
|
||||
return waitForTerminalLivePendingFlush(pendingLiveInputFlushRef.current)
|
||||
}, [])
|
||||
|
||||
const sendQueuedMirrorPayload = useCallback(
|
||||
(handle: string, payload: string): Promise<boolean> =>
|
||||
sendLiveTerminalInputRef.current(handle, payload),
|
||||
[sendLiveTerminalInputRef]
|
||||
)
|
||||
|
||||
const runMirrorStep = useCallback(
|
||||
async (handle: string, fieldText: string, commitHeld: boolean): Promise<boolean> => {
|
||||
if (
|
||||
@@ -107,8 +116,11 @@ export function useTerminalLivePendingInputFlush<TTabType extends string>({
|
||||
if (payload.length === 0) {
|
||||
return waitForPendingLiveInputFlush()
|
||||
}
|
||||
return queueTerminalLiveMirrorSend(pendingLiveInputFlushRef, () =>
|
||||
sendLiveTerminalInputRef.current(handle, payload)
|
||||
return queueTerminalLiveMirrorSend(
|
||||
pendingLiveInputFlushRef.current,
|
||||
handle,
|
||||
payload,
|
||||
sendQueuedMirrorPayload
|
||||
)
|
||||
},
|
||||
[
|
||||
@@ -117,7 +129,7 @@ export function useTerminalLivePendingInputFlush<TTabType extends string>({
|
||||
clearHeldCommitTimer,
|
||||
liveInputTerminalHandlesRef,
|
||||
resetMirrorState,
|
||||
sendLiveTerminalInputRef,
|
||||
sendQueuedMirrorPayload,
|
||||
waitForPendingLiveInputFlush
|
||||
]
|
||||
)
|
||||
@@ -164,7 +176,7 @@ export function useTerminalLivePendingInputFlush<TTabType extends string>({
|
||||
heldLiveInputTextRef.current = ''
|
||||
sentLiveInputTextRef.current = ''
|
||||
pendingLiveInputHandleRef.current = null
|
||||
pendingLiveInputFlushRef.current = null
|
||||
cancelTerminalLivePendingFlush(pendingLiveInputFlushRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -220,4 +220,53 @@ describe('mobile relay pairing recovery', () => {
|
||||
expect(saved.metadata.authorizationMode).toBe('authenticated-direct')
|
||||
expect(deps.updateJournal).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
// Why: a journal stranded by a relay outage used to block every later pairing
|
||||
// with "recovery pending" forever, because recovery only ever deferred.
|
||||
it('abandons a journal once its invite expired and no credential can reconcile', async () => {
|
||||
const saved = journal()
|
||||
const unreachable = client(async () => {
|
||||
throw new Error('relay unreachable')
|
||||
})
|
||||
const deps = {
|
||||
...dependencies({ journal: saved, connectRelay: vi.fn(() => unreachable) }),
|
||||
now: () => saved.metadata.relay.inviteExpiresAt + 10 * 60 * 1000 + 1
|
||||
}
|
||||
|
||||
await expect(recoverMobileRelayPairing(deps)).resolves.toBe('abandoned')
|
||||
expect(deps.clearJournal).toHaveBeenCalledWith(saved.metadata.journalId)
|
||||
})
|
||||
|
||||
it('keeps a just-expired journal so a brief outage cannot discard it', async () => {
|
||||
const saved = journal()
|
||||
const unreachable = client(async () => {
|
||||
throw new Error('relay unreachable')
|
||||
})
|
||||
const deps = {
|
||||
...dependencies({ journal: saved, connectRelay: vi.fn(() => unreachable) }),
|
||||
now: () => saved.metadata.relay.inviteExpiresAt + 1
|
||||
}
|
||||
|
||||
await expect(recoverMobileRelayPairing(deps)).resolves.toBe('deferred')
|
||||
expect(deps.clearJournal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Why: a committed install whose local persistence failed is the one case the
|
||||
// journal must survive — it is the only record left to retry the write from.
|
||||
it('keeps a journal when the server committed but the local write failed', async () => {
|
||||
const saved = journal()
|
||||
const directInstalled = installed(saved, 'authenticated-direct')
|
||||
const committed = client(async () =>
|
||||
response(endpoints(saved, { state: 'committed', result: directInstalled }))
|
||||
)
|
||||
const deps = {
|
||||
...dependencies({ journal: saved, connectRelay: vi.fn(() => committed) }),
|
||||
now: () => saved.metadata.relay.inviteExpiresAt + 10 * 60 * 1000 + 1,
|
||||
writeCredentialBundle: vi.fn(async () => {
|
||||
throw new Error('keychain unavailable')
|
||||
})
|
||||
}
|
||||
|
||||
await expect(recoverMobileRelayPairing(deps)).resolves.toBe('deferred')
|
||||
expect(deps.clearJournal).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
import { createRecoveringPairingRelayCandidate } from './pairing-relay-candidate'
|
||||
import type { HostProfile, RpcResponse } from './types'
|
||||
|
||||
export type MobileRelayPairingRecoveryResult = 'none' | 'recovered' | 'deferred'
|
||||
export type MobileRelayPairingRecoveryResult = 'none' | 'recovered' | 'deferred' | 'abandoned'
|
||||
|
||||
type RecoveryDependencies = {
|
||||
loadJournal: typeof loadMobileRelayPairingJournal
|
||||
@@ -57,6 +57,10 @@ const defaultDependencies: RecoveryDependencies = {
|
||||
platform: Platform.OS
|
||||
}
|
||||
|
||||
// One full invite lifetime past expiry, so a momentary outage never discards a
|
||||
// journal that a later launch could still reconcile.
|
||||
const ABANDON_GRACE_MS = 10 * 60 * 1000
|
||||
|
||||
let recoveryPromise: Promise<MobileRelayPairingRecoveryResult> | null = null
|
||||
|
||||
export function recoverMobileRelayPairing(
|
||||
@@ -96,6 +100,10 @@ async function runRecovery(
|
||||
}
|
||||
|
||||
const credentials = recoveryCredentials(journal, bundle, dependencies.now())
|
||||
// Why: publishCommitted runs inside the catch below, so a failed local write
|
||||
// of an authoritatively committed install must not look like "nothing to
|
||||
// reconcile" — that journal is the only record left to retry the write from.
|
||||
let observedCommitted = false
|
||||
for (const credential of credentials) {
|
||||
let client: PairingCandidateClient | null = null
|
||||
try {
|
||||
@@ -113,6 +121,7 @@ async function runRecovery(
|
||||
})
|
||||
const endpoints = await getRecoveryStatus(client, journal, credential.kind)
|
||||
if (endpoints.installStatus?.state === 'committed') {
|
||||
observedCommitted = true
|
||||
await publishCommitted(journal, endpoints, dependencies)
|
||||
return 'recovered'
|
||||
}
|
||||
@@ -128,6 +137,7 @@ async function runRecovery(
|
||||
)
|
||||
const reconciled = await getRecoveryStatus(client, journal, 'invite')
|
||||
assertCommitted(reconciled, installed)
|
||||
observedCommitted = true
|
||||
await publishCommitted(journal, reconciled, dependencies)
|
||||
return 'recovered'
|
||||
}
|
||||
@@ -138,6 +148,19 @@ async function runRecovery(
|
||||
client?.close()
|
||||
}
|
||||
}
|
||||
// Why: past invite expiry no credential can still establish what happened, so
|
||||
// retaining the journal cannot reconcile anything — it only fails every later
|
||||
// pairing with "recovery pending" forever. Re-pairing mints a fresh device and
|
||||
// any uncommitted server-side install expires on its own. The extra invite
|
||||
// lifetime of slack keeps a brief relay outage from discarding a journal whose
|
||||
// resume credential would have reconciled it on the next launch.
|
||||
if (
|
||||
!observedCommitted &&
|
||||
journal.metadata.relay.inviteExpiresAt + ABANDON_GRACE_MS <= dependencies.now()
|
||||
) {
|
||||
await dependencies.clearJournal(journal.metadata.journalId).catch(() => {})
|
||||
return 'abandoned'
|
||||
}
|
||||
return 'deferred'
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user