mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
fix(native-chat): honour notify on the relabel path and emit the stored snapshot
The startup sweep republishes every restored session with notify: false, but the relabel branch never consulted it and emitted a full tab list per named session — the desktop renderer is a real subscriber on that feed. It also emitted the pre-store candidate, although the store can hand back a different object, so a client mirror could retain a replaced tab under an identical snapshotVersion; the sibling replace path already got this right. Also corrects the merge comment that claimed a background republish cannot re-surface a lost mirror, moves the doc block that landed between an existing comment and the field it described, and routes the replacement tab's placeholder through defaultAgentChatLabel.
This commit is contained in:
@@ -233,6 +233,9 @@ export class OrcaRuntimeWithGetWorktreePs extends OrcaRuntimeWithStructuredAgent
|
||||
workspaceId: string
|
||||
sessionId: string
|
||||
conversationName: string | null
|
||||
/** False for a background republish, which must store the name without
|
||||
* pushing a full tab list at every live subscriber. */
|
||||
notify?: boolean
|
||||
}): void {
|
||||
const existing = this.mobileSessionTabsByWorktree.get(input.workspaceId)
|
||||
const id = `agent-session:${input.sessionId}`
|
||||
@@ -249,8 +252,13 @@ export class OrcaRuntimeWithGetWorktreePs extends OrcaRuntimeWithStructuredAgent
|
||||
snapshotVersion: existing.snapshotVersion + 1,
|
||||
tabs: existing.tabs.map((tab) => (tab.id === id ? { ...tab, title } : tab))
|
||||
}
|
||||
this.storeMobileSessionSnapshot(input.workspaceId, snapshot)
|
||||
this.emitMobileSessionTabsSnapshot(snapshot)
|
||||
// Emit what was STORED, not the local candidate: the store may hand back a
|
||||
// different object, and a client mirror would then hold a replaced tab under
|
||||
// an identical snapshotVersion. Matches replaceStructuredAgentSessionTab.
|
||||
const stored = this.storeMobileSessionSnapshot(input.workspaceId, snapshot)
|
||||
if (input.notify !== false) {
|
||||
this.emitMobileSessionTabsSnapshot(stored)
|
||||
}
|
||||
}
|
||||
|
||||
protected createStructuredAgentSessionHandoffTransport(): StructuredAgentSessionHandoffTransport {
|
||||
|
||||
@@ -116,9 +116,9 @@ export class OrcaRuntimeWithRestoreStructuredAgentSessionTabsOnce extends OrcaRu
|
||||
const id = `agent-session:${input.sessionId}`
|
||||
if (existing?.tabs.some((tab) => tab.id === id)) {
|
||||
const conversationName = title
|
||||
// A background re-publish only relabels an existing tab — it never re-adds one — so it
|
||||
// cannot re-surface a client whose mirror lost the tab; healing one needs `activate` or an
|
||||
// explicit republish.
|
||||
// A background re-publish only relabels an existing tab — it never re-adds one — and with
|
||||
// `notify: false` it emits nothing at all, so it cannot re-surface a client whose mirror
|
||||
// lost the tab; healing one needs `activate` or an explicit republish.
|
||||
if (!input.activate) {
|
||||
// Republishing an already-open tab is how a restored session hands over
|
||||
// the name it was persisted with; the rest of the snapshot is unchanged.
|
||||
@@ -126,7 +126,10 @@ export class OrcaRuntimeWithRestoreStructuredAgentSessionTabsOnce extends OrcaRu
|
||||
this.applyStructuredAgentSessionConversationName({
|
||||
workspaceId: input.workspaceId,
|
||||
sessionId: input.sessionId,
|
||||
conversationName
|
||||
conversationName,
|
||||
// The startup sweep runs once per session; without this each named
|
||||
// one would push the whole tab list at every live subscriber.
|
||||
...(input.notify !== undefined ? { notify: input.notify } : {})
|
||||
})
|
||||
}
|
||||
return
|
||||
|
||||
@@ -437,6 +437,76 @@ describe('structured session cold restoration', () => {
|
||||
expect(after.snapshotVersion).toBeGreaterThan(before.snapshotVersion ?? 0)
|
||||
})
|
||||
|
||||
it('stores a background relabel without pushing a snapshot at every subscriber', async () => {
|
||||
const runtime = new OrcaRuntimeService()
|
||||
await runtime.publishStructuredAgentSessionTab({
|
||||
workspaceId: 'workspace-1',
|
||||
sessionId: 'session-1',
|
||||
agent: 'codex',
|
||||
activate: true
|
||||
})
|
||||
const emit = vi.fn()
|
||||
;(
|
||||
runtime as unknown as { emitMobileSessionTabsSnapshot: unknown }
|
||||
).emitMobileSessionTabsSnapshot = emit
|
||||
|
||||
// The startup sweep's own call shape, once per restored session.
|
||||
await runtime.publishStructuredAgentSessionTab({
|
||||
workspaceId: 'workspace-1',
|
||||
sessionId: 'session-1',
|
||||
agent: 'codex',
|
||||
activate: false,
|
||||
notify: false,
|
||||
title: 'Fix the lease probe'
|
||||
})
|
||||
|
||||
// The desktop renderer subscribes to this feed, so a sweep landing after it
|
||||
// connects would fan out one full tab list per named session.
|
||||
expect(emit).not.toHaveBeenCalled()
|
||||
const after = await runtime.listMobileSessionTabs('id:workspace-1')
|
||||
expect(after.tabs[0]).toMatchObject({ title: 'Fix the lease probe' })
|
||||
})
|
||||
|
||||
it('emits the snapshot the store kept, not the candidate handed to it', async () => {
|
||||
const runtime = new OrcaRuntimeService()
|
||||
await runtime.publishStructuredAgentSessionTab({
|
||||
workspaceId: 'workspace-1',
|
||||
sessionId: 'session-1',
|
||||
agent: 'codex',
|
||||
activate: true
|
||||
})
|
||||
const emitted: unknown[] = []
|
||||
;(
|
||||
runtime as unknown as { emitMobileSessionTabsSnapshot: unknown }
|
||||
).emitMobileSessionTabsSnapshot = (snapshot: unknown) => emitted.push(snapshot)
|
||||
// A pending conversation replacement rewrites every snapshot on its way into
|
||||
// the store, so the candidate and the stored object are not the same object.
|
||||
setStructuredAgentSessionHost({
|
||||
conversationReplacements: () => [
|
||||
{
|
||||
workspaceId: 'workspace-1',
|
||||
sourceSessionId: 'session-1',
|
||||
sessionId: 'session-2',
|
||||
agent: 'codex'
|
||||
}
|
||||
]
|
||||
} as never)
|
||||
|
||||
runtime.applyStructuredAgentSessionConversationName({
|
||||
workspaceId: 'workspace-1',
|
||||
sessionId: 'session-1',
|
||||
conversationName: 'Fix the lease probe'
|
||||
})
|
||||
|
||||
// A client mirror fed the pre-store candidate would keep a replaced tab
|
||||
// under an identical snapshotVersion.
|
||||
const kept = (
|
||||
runtime as unknown as { mobileSessionTabsByWorktree: Map<string, unknown> }
|
||||
).mobileSessionTabsByWorktree.get('workspace-1')
|
||||
expect(emitted).toHaveLength(1)
|
||||
expect(emitted[0]).toBe(kept)
|
||||
})
|
||||
|
||||
it('publishes nothing when the name it was given is the one already shown', async () => {
|
||||
const runtime = new OrcaRuntimeService()
|
||||
await runtime.publishStructuredAgentSessionTab({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultAgentChatLabel } from '../../shared/agent-session-chat-label'
|
||||
import type { RuntimeMobileSessionTabsSnapshot } from '../../shared/runtime-types'
|
||||
import type { ConversationReplacement } from '../native-chat/agent-session-wire/structured-conversation-command'
|
||||
|
||||
@@ -30,7 +31,7 @@ export function replaceConversationInSnapshot(
|
||||
id,
|
||||
sessionId: replacement.sessionId,
|
||||
agent: replacement.agent,
|
||||
title: replacement.agent === 'claude' ? 'Claude Chat' : 'Codex Chat',
|
||||
title: defaultAgentChatLabel(replacement.agent),
|
||||
replacesSessionId: replacement.sourceSessionId
|
||||
}
|
||||
: tab
|
||||
|
||||
@@ -166,12 +166,12 @@ export type AgentStatusEntry = {
|
||||
/** Provider-owned conversation/session id captured from hook payloads.
|
||||
* Used only for exact CLI resume; Orca terminal ids are not agent-session ids. */
|
||||
providerSession?: AgentProviderSessionMetadata
|
||||
/** False when the status belongs to a non-terminal owner that restores itself. */
|
||||
/** A structured chat's settled name, already resolved between the user's rename
|
||||
* and the provider's. Carried apart from `terminalTitle` because that field is
|
||||
* laundered through heuristics built for OSC titles scraped off a pty, which
|
||||
* null a legitimate name like `auth/login`. This one is authoritative. */
|
||||
conversationName?: string
|
||||
/** False when the status belongs to a non-terminal owner that restores itself. */
|
||||
terminalResumeEligible?: false
|
||||
/** Live-only Command Code turn boundary key; not persisted to last-status.json. */
|
||||
promptInteractionKey?: string
|
||||
|
||||
Reference in New Issue
Block a user