mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
* fix(native-chat): resolve every registered agent's floating chat to its pinned folder Drop the unused showStructuredAgentSessionChoice and say what the built-in agent check means. * fix(native-chat): launch floating OpenCode, Grok, OMP and Pi chats in their pinned folder The renderer now resolves every floating chat's links, images and skills against the folder the chat was pinned to. The ACP (OpenCode/Grok/OMP) and Pi launchers still started the agent in the current floating folder setting, so after the setting moved and the agent relaunched, the agent ran in one folder while links resolved against another (and Pi forked its conversation into the new folder). Both launchers now use resolveAgentSessionLaunchDirectory, the helper Claude and Codex already use: launch in the pin, refuse with the existing "folder this chat ran in no longer exists" message when it is gone, and pin an unpinned floating record on first launch. Worktree and folder chats are unchanged. The tab-directory reader now calls the status bridge's own isStructuredTab instead of restating its predicate inline.
81 lines
2.7 KiB
TypeScript
81 lines
2.7 KiB
TypeScript
import { sha256 } from './sha256'
|
|
import type { StructuredAgentSessionFirstMessage } from './structured-agent-session-create'
|
|
import type { StructuredAgentId } from './agent-session-provider-handle'
|
|
|
|
function canonicalize(value: unknown): string {
|
|
if (value === null || typeof value !== 'object') {
|
|
return JSON.stringify(value ?? null)
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return `[${value.map(canonicalize).join(',')}]`
|
|
}
|
|
const entries = Object.entries(value as Record<string, unknown>)
|
|
.filter(([, entry]) => entry !== undefined)
|
|
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalize(entry)}`).join(',')}}`
|
|
}
|
|
|
|
export function structuredAgentSessionPayloadFingerprint(input: {
|
|
method: string
|
|
sessionId: string
|
|
fields: Record<string, unknown>
|
|
}): string {
|
|
const bytes = sha256(
|
|
new TextEncoder().encode(
|
|
canonicalize({ method: input.method, sessionId: input.sessionId, fields: input.fields })
|
|
)
|
|
)
|
|
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')
|
|
}
|
|
|
|
export function structuredAgentSessionDomainFingerprint(input: {
|
|
domain: string
|
|
sessionId: string
|
|
fields: Record<string, unknown>
|
|
}): string {
|
|
return structuredAgentSessionPayloadFingerprint({
|
|
method: input.domain,
|
|
sessionId: input.sessionId,
|
|
fields: input.fields
|
|
})
|
|
}
|
|
|
|
export function structuredAgentSessionCreateFingerprint(input: {
|
|
sessionId: string
|
|
worktree: string
|
|
agent: StructuredAgentId
|
|
resumeFrom?: { providerSessionId: string }
|
|
tabId?: string
|
|
firstMessage?: StructuredAgentSessionFirstMessage
|
|
options?: Readonly<Record<string, string>>
|
|
}): string {
|
|
return structuredAgentSessionPayloadFingerprint({
|
|
method: 'agentSession.create',
|
|
sessionId: input.sessionId,
|
|
fields: {
|
|
worktree: input.worktree,
|
|
agent: input.agent,
|
|
// `canonicalize` drops undefined, so a plain create keeps the digest it has always had.
|
|
// Adopting a conversation is a different intent and must not replay as a blank create.
|
|
resumeFrom: input.resumeFrom,
|
|
// The host digests the same field; a retry naming another tab still replays with the
|
|
// recorded one, since the host owns the id.
|
|
tabId: input.tabId,
|
|
firstMessage: input.firstMessage,
|
|
options: input.options
|
|
}
|
|
})
|
|
}
|
|
|
|
export function createStructuredAgentSessionOperationId(
|
|
randomUuid: () => string,
|
|
now: number = Date.now()
|
|
): string {
|
|
const timestamp = Math.trunc(now).toString()
|
|
const entropy = randomUuid().replaceAll('-', '').toLowerCase()
|
|
if (!/^\d{13}$/.test(timestamp) || !/^[0-9a-f]{32}$/.test(entropy)) {
|
|
throw new Error('Unable to create a durable operation id')
|
|
}
|
|
return `${timestamp}-${entropy}`
|
|
}
|