feat(native-chat): say terminal sessions kept running, and clear the quality gate

The modal lists stopped chats with no way to tell that CLI agents are fine, and
the true state of the world is counterintuitive: the terminal sessions survived
the restart and the chats did not. One line now says so, next to the heading
where it frames the list rather than as a footnote at the bottom.

Wording follows the app's own vocabulary rather than inventing a term: the
catalog settles on "terminal sessions" (terminalSessionCount, "Terminal sessions
are grouped by workspace", "No terminal sessions yet"), and UpdateCard already
reassures with "Your terminal sessions won't be interrupted during the update" in
the same text-xs text-muted-foreground treatment. "kept running" rather than
"were restored" -- nothing reconnected them, they never stopped, and the line
says nothing about why.

Also clears check:code-quality:changed, which I had not been running -- oxlint
alone covers neither the design-system nor the casting audit, so 18 findings had
accumulated across the branch.

  - design system (4): Button spacing hand-rolled as gap-1/px-2 is just size="xs";
    PopoverContent and DialogTitle own their typography and spacing, so the
    text-xs moved to the popover's own children and the title's icon gap moved to
    a plain wrapper.
  - casting (14): production code loses its assertions outright via Reflect.get,
    the idiom already used in managed-hook-detection-commands and
    worktree-name-retirement. The marker validator reads each field through
    Reflect.get and now checks recordedAt is a number rather than asserting it;
    the store-file parse uses the existing `file` shape instead of a second
    assertion; the runner narrows the admission error's owner with typeof.
    Test fixtures keep their assertions behind the line-specific SAFETY:
    rationale the repo mandates for exactly this case.

One trap worth recording: the audit reports an assertion at the line its
EXPRESSION OPENS, not where `as` appears, so a disable-next-line above the
closing brace of a multi-line literal is inert and silently changes nothing.

Guards unchanged; ablation re-proved 14/14 at this head.
This commit is contained in:
Brennan Benson
2026-09-16 13:24:16 -07:00
parent c05f928101
commit d047322227
7 changed files with 47 additions and 29 deletions
@@ -96,12 +96,13 @@ async function resumeOne(
})
} catch (error) {
const reason = error instanceof Error ? error.message : String(error)
const live = (error as { owner?: string }).owner
const owner =
typeof error === 'object' && error !== null ? Reflect.get(error, 'owner') : undefined
return {
sessionId,
outcome: 'refused',
reason,
...(live === undefined ? {} : { owner: live })
...(typeof owner === 'string' ? { owner } : {})
}
}
}
@@ -43,6 +43,7 @@ function turnItem(
}
function record(overrides: { chain?: AgentSessionRecord['providerHandleChain'] } = {}) {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a literal fixture standing in for a durable record; the code under test reads only lease, provider, location and providerHandleChain.
return {
schemaVersion: 2,
sessionId: SESSION,
@@ -110,6 +111,7 @@ function claudeRecord(
leafUuid: string | null,
providerSessionId = CLAUDE_PROVIDER_SESSION
): AgentSessionRecord {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the base fixture is already record-shaped; this only swaps the provider and its Claude handle chain.
return {
...record(),
provider: 'claude',
@@ -127,6 +129,7 @@ function claudeRecord(
}
function journal(items: AgentJournalRenderItem[], isReadOnly = false) {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the code under test calls only isReadOnly and snapshot(); a real AgentSessionJournal needs an on-disk SQLite store.
return { isReadOnly, snapshot: () => ({ items, submissions: [] }) } as never
}
@@ -416,7 +419,9 @@ describe('the restart-resume surface', () => {
])
return {
restartResume: createStructuredAgentSessionRestartResume(
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the collaborator reads only getRecord and resumeMarkers from the store, and supportsCreate from the adapter.
{ store, adapter: { supportsCreate: () => true } } as never,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the live-session map is read for journal, hasProviderChild and fence only.
sessions as never,
{
revealSession: async () => ({ readable: true }),
@@ -429,6 +434,7 @@ describe('the restart-resume surface', () => {
send: async ({ envelope, body }) => {
sent.push({
sessionId: envelope.sessionId,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: restartContinuationBody builds exactly one text block, which is what this assertion reads.
text: (body.blocks[0] as { text: string }).text
})
return { ok: true }
@@ -513,6 +519,7 @@ describe('the restart-resume surface', () => {
// predicate drops the session. Reporting "nothing happened" would leave the user pressing a dead
// button for a session that IS running.
it('reports a session the chat pane already re-acquired as resumed, not as nothing', async () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the base fixture is already record-shaped; only claimStatus is overridden, to model a lease the pane re-took.
const liveRecord = {
...record(),
lease: { ...record().lease, claimStatus: 'live' }
@@ -537,6 +544,7 @@ describe('the restart-resume surface', () => {
// Relaxing the lease clause must not relax the whole predicate. "Resume all" targets every
// marker, so a held-but-ineligible session would otherwise be consumed and counted as resumed.
it('refuses to settle an already-live session the predicate rejects', async () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the base fixture is already record-shaped; only claimStatus is overridden, to model a lease the pane re-took.
const liveRecord = {
...record(),
lease: { ...record().lease, claimStatus: 'live' }
@@ -102,6 +102,7 @@ function parseState(
if (typeof parsed !== 'object' || parsed === null) {
return null
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every field is read back as `unknown` and validated below before use; adding `resumeMarkers` brought this long-standing assertion into the changed-code gate.
const file = parsed as {
schemaVersion?: unknown
hostId?: unknown
@@ -110,6 +111,7 @@ function parseState(
retiredClaimKeys?: unknown
unusableRecords?: unknown
visibleSessionIds?: unknown
resumeMarkers?: unknown
}
if (
!Number.isSafeInteger(file.schemaVersion) ||
@@ -227,9 +229,7 @@ function parseState(
}
state.visibleSessionIdsIndexPresent = visibleSessionIds.present
visibleSessionIds.ids.forEach((sessionId) => state.visibleSessionIds.add(sessionId))
state.resumeMarkers = parseAgentSessionResumeMarkers(
(parsed as { resumeMarkers?: unknown }).resumeMarkers
)
state.resumeMarkers = parseAgentSessionResumeMarkers(file.resumeMarkers)
return { state, needsRewrite }
}
@@ -82,13 +82,7 @@ function ResumeCandidateRow({
<span className="shrink-0 text-[11px] tabular-nums text-muted-foreground">
{formatShortTimeAgo(candidate.recordedAt, listedAt)}
</span>
<Button
variant="ghost"
size="sm"
className="h-7 shrink-0 gap-1 px-2"
disabled={busy}
onClick={onReconnect}
>
<Button variant="ghost" size="xs" className="shrink-0" disabled={busy} onClick={onReconnect}>
<Play className="size-3" />
{translate('auto.components.NativeChatResumeOnRestartModal.resume', 'Reconnect')}
</Button>
@@ -94,20 +94,20 @@ function ContinuationExplainer(): React.JSX.Element {
<Info className="size-3.5" />
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-96 text-xs">
<p className="font-semibold">
<PopoverContent align="end" className="w-96">
<p className="text-xs font-semibold">
{translate(
'auto.components.NativeChatResumeOnRestartModal.whatIsSentTitle',
'What Orca sends'
)}
</p>
<p className="mt-1 text-muted-foreground">
<p className="mt-1 text-xs text-muted-foreground">
{translate(
'auto.components.NativeChatResumeOnRestartModal.whatIsSentBody',
'Continuing sends one short message to each agent, telling it that Orca restarted and asking it to check its last action before carrying on. Your own prompt is never re-sent.'
)}
</p>
<blockquote className="mt-2 rounded-md border bg-muted/40 p-2 text-muted-foreground">
<blockquote className="mt-2 rounded-md border bg-muted/40 p-2 text-xs text-muted-foreground">
{AGENT_SESSION_RESTART_CONTINUATION_MESSAGE}
</blockquote>
</PopoverContent>
@@ -248,12 +248,15 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null {
point, so the list scrolls inside the dialog while the header and primary action stay. */}
<DialogContent className="grid-rows-[auto_minmax(0,1fr)_auto_auto] sm:max-w-xl max-h-[85vh]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<RotateCcw className="size-4 text-muted-foreground" />
{translate(
'auto.components.NativeChatResumeOnRestartModal.title',
'Reconnect interrupted chats?'
)}
<DialogTitle>
{/* Plain wrapper owns the icon spacing; DialogTitle owns its own. */}
<span className="flex items-center gap-2">
<RotateCcw className="size-4 text-muted-foreground" />
{translate(
'auto.components.NativeChatResumeOnRestartModal.title',
'Reconnect interrupted chats?'
)}
</span>
</DialogTitle>
<DialogDescription>
{interruptedByUpdate
@@ -266,6 +269,15 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null {
'These chats were mid-turn when Orca closed. Reconnecting restores each one where it stopped, with its full context and without re-sending your prompt — the interrupted reply will not continue on its own.'
)}
</DialogDescription>
{/* The true state of things is counterintuitive — the terminal sessions survived and the
chats did not — so say so where it frames the list, not as a footnote. "kept running"
rather than "were restored": nothing reconnected them, they never stopped. */}
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.NativeChatResumeOnRestartModal.terminalSessionsUnaffected',
'Only chats are affected — your terminal sessions kept running and need nothing from you.'
)}
</p>
</DialogHeader>
<div
+1
View File
@@ -1923,6 +1923,7 @@
"title": "Reconnect interrupted chats?",
"body": "These chats were mid-turn when Orca closed. Reconnecting restores each one where it stopped, with its full context and without re-sending your prompt — the interrupted reply will not continue on its own.",
"updateBody": "These chats were mid-turn when Orca installed an update. Reconnecting restores each one where it stopped, with its full context and without re-sending your prompt — the interrupted reply will not continue on its own.",
"terminalSessionsUnaffected": "Only chats are affected — your terminal sessions kept running and need nothing from you.",
"resume": "Reconnect",
"resumeAll": "Reconnect all",
"resuming": "Reconnecting…",
+9 -7
View File
@@ -46,14 +46,16 @@ export function isAgentSessionResumeMarker(value: unknown): value is AgentSessio
if (typeof value !== 'object' || value === null) {
return false
}
const marker = value as Partial<AgentSessionResumeMarker>
const recordedAt = Reflect.get(value, 'recordedAt')
const trigger = Reflect.get(value, 'trigger')
return (
isMarkerField(marker.sessionId) &&
isMarkerField(marker.turnId) &&
isMarkerField(marker.providerHandleRoot) &&
Number.isSafeInteger(marker.recordedAt) &&
(marker.recordedAt as number) >= 0 &&
(marker.trigger === 'quit' || marker.trigger === 'update')
isMarkerField(Reflect.get(value, 'sessionId')) &&
isMarkerField(Reflect.get(value, 'turnId')) &&
isMarkerField(Reflect.get(value, 'providerHandleRoot')) &&
typeof recordedAt === 'number' &&
Number.isSafeInteger(recordedAt) &&
recordedAt >= 0 &&
(trigger === 'quit' || trigger === 'update')
)
}