mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
Client — the `changed` gate meant an unchanged poll skipped setWorktrees / setLastKnownWorktrees / setCachedWorktrees, so optimistic local edits (togglePin, handleDeleteWorktree's failure re-add) and the #8498 cache guard were no longer repaired while the host catalog was stable. The gate bought nothing: setCachedWorktrees is an in-memory Map write and areWorktreeListsEqual already ran every poll, so the steady state still short-circuits on array identity. All wire savings are unaffected. admit() now just returns the confirmed rows and HostScreen applies them exactly as it did pre-PR. Also on the client: - a stale response from a superseded client/host no longer clears the token the current client/host just established - discriminate on `worktrees` rather than on `'unchanged' in response`, so a future catalog field named `unchanged` can't reclassify a full response - useRef over useMemo for the snapshot client; React may discard memoized values - hoist WORKTREE_PS_FULL_LIMIT so the truncates-at-200 rationale travels with it Host — replace the per-limit snapshot cache with a content-addressed id (ETag semantics). Ownership lives in the id, so concurrent clients, differing limits, and runtime restarts are correct by construction; this drops the LRU, the eviction policy, the per-runtime WeakMap, and the retention of up to 8 full catalogs. The remaining cache is a pure memo: because ids derive from content, dropping or thrashing it costs CPU and nothing else. Keeping the memo also keeps the measured steady-state cost — hashing every poll instead measured 2.24ms vs 0.75ms for the compare on a 310KB catalog. Verified: mobile 2784 passed / 3 skipped, src/main/runtime/rpc 1064 passed, node + mobile typechecks, oxlint, oxfmt, max-lines ratchet.
289 lines
11 KiB
TypeScript
289 lines
11 KiB
TypeScript
import {
|
|
finishAutomationWorkspaceProvenanceRequest,
|
|
releaseAutomationWorkspaceProvenanceRequest,
|
|
resolveAutomationWorkspaceProvenance
|
|
} from '../../../automations/workspace-provenance'
|
|
import { buildCliWorkspaceProvenance } from '../../../../shared/cli-workspace-provenance'
|
|
import { defineMethod, type RpcMethod } from '../core'
|
|
import { resolveWorktreeCatalogSnapshot } from '../worktree-catalog-snapshot'
|
|
import { resolveRuntimeNavigationTarget } from '../../../../shared/runtime-navigation'
|
|
import {
|
|
WorktreeCreate,
|
|
WorktreeDetectedListParams,
|
|
WorktreeActivate,
|
|
WorktreeForceDeleteBranch,
|
|
WorktreeListParams,
|
|
WorktreePrefetchCreateBase,
|
|
WorktreePsParams,
|
|
WorktreeRemove,
|
|
WorktreeResolveMrBase,
|
|
WorktreeResolvePrBase,
|
|
WorktreeSelector,
|
|
WorktreeSet,
|
|
WorktreeSortOrder,
|
|
WorktreeTeardownMissingTerminalsParams
|
|
} from './worktree-schemas'
|
|
|
|
export const WORKTREE_METHODS: RpcMethod[] = [
|
|
defineMethod({
|
|
name: 'worktree.ps',
|
|
params: WorktreePsParams,
|
|
handler: async (params, { runtime }) => {
|
|
const result = await runtime.getWorktreePs(params.limit)
|
|
// Why: callers that never send the field get the byte-exact legacy response.
|
|
if (params.afterSnapshotId === undefined) {
|
|
return result
|
|
}
|
|
return resolveWorktreeCatalogSnapshot(result, params.afterSnapshotId)
|
|
}
|
|
}),
|
|
defineMethod({
|
|
name: 'worktree.list',
|
|
params: WorktreeListParams,
|
|
handler: async (params, { runtime }) => runtime.listManagedWorktrees(params.repo, params.limit)
|
|
}),
|
|
defineMethod({
|
|
name: 'worktree.detectedList',
|
|
params: WorktreeDetectedListParams,
|
|
handler: async (params, { runtime }) => runtime.listDetectedManagedWorktrees(params.repo)
|
|
}),
|
|
defineMethod({
|
|
name: 'worktree.teardownMissingTerminals',
|
|
params: WorktreeTeardownMissingTerminalsParams,
|
|
handler: async (params, { runtime }) =>
|
|
runtime.teardownMissingManagedWorktreeTerminals(
|
|
params.repo,
|
|
params.worktreeIds,
|
|
params.connectionId
|
|
)
|
|
}),
|
|
defineMethod({
|
|
name: 'worktree.lineageList',
|
|
params: null,
|
|
handler: async (_params, { runtime }) => ({
|
|
lineage: await runtime.listWorktreeLineage(),
|
|
workspaceLineage: await runtime.listWorkspaceLineage()
|
|
})
|
|
}),
|
|
defineMethod({
|
|
name: 'worktree.show',
|
|
params: WorktreeSelector,
|
|
handler: async (params, { runtime }) => ({
|
|
worktree: await runtime.showManagedWorktree(params.worktree)
|
|
})
|
|
}),
|
|
defineMethod({
|
|
name: 'worktree.sleep',
|
|
params: WorktreeSelector,
|
|
handler: async (params, { runtime }) => runtime.sleepManagedWorktree(params.worktree)
|
|
}),
|
|
defineMethod({
|
|
name: 'worktree.activate',
|
|
params: WorktreeActivate,
|
|
handler: async (params, { runtime, clientKind }) =>
|
|
// Why: clientKind ('mobile'|'runtime') scopes the host-renderer slept-agent
|
|
// wake to phones so web/desktop activation behavior is unchanged.
|
|
runtime.activateManagedWorktree(params.worktree, {
|
|
notifyClients: params.notifyClients !== false,
|
|
clientKind,
|
|
navigation: resolveRuntimeNavigationTarget({
|
|
navigation: params.navigation,
|
|
notifyClients: params.notifyClients,
|
|
clientKind
|
|
})
|
|
})
|
|
}),
|
|
defineMethod({
|
|
name: 'worktree.create',
|
|
params: WorktreeCreate,
|
|
handler: async (params, { runtime }) =>
|
|
// Why: a mobile create interrupted by a connection migration is retried with
|
|
// the same clientMutationId; dedupe so the host returns the in-flight/created
|
|
// worktree instead of spawning a duplicate. No key (desktop/CLI) runs plainly.
|
|
runtime.dedupeWorktreeCreate(params.repo, params.clientMutationId, async () => {
|
|
const repo = await runtime.showRepo(params.repo)
|
|
const automationProvenance = resolveAutomationWorkspaceProvenance({
|
|
authority: runtime,
|
|
repoSelector: params.repo,
|
|
repo,
|
|
request: params.automationProvenanceRequest
|
|
})
|
|
// Why: provenance tokens are reserved before creation so retries can recover,
|
|
// but failed create attempts must release the reservation for a safe retry.
|
|
try {
|
|
const result = await runtime.createManagedWorktree({
|
|
repoSelector: params.repo,
|
|
name: params.name ?? '',
|
|
baseBranch: params.baseBranch,
|
|
compareBaseRef: params.compareBaseRef,
|
|
branchNameOverride: params.branchNameOverride,
|
|
linkedIssue: params.linkedIssue,
|
|
linkedPR: params.linkedPR,
|
|
linkedLinearIssue: params.linkedLinearIssue,
|
|
linkedLinearIssueWorkspaceId: params.linkedLinearIssueWorkspaceId,
|
|
linkedLinearIssueOrganizationUrlKey: params.linkedLinearIssueOrganizationUrlKey,
|
|
linkedGitLabMR: params.linkedGitLabMR,
|
|
linkedGitLabIssue: params.linkedGitLabIssue,
|
|
linkedBitbucketPR: params.linkedBitbucketPR,
|
|
linkedAzureDevOpsPR: params.linkedAzureDevOpsPR,
|
|
linkedGiteaPR: params.linkedGiteaPR,
|
|
linkedWorkItem: params.linkedWorkItem,
|
|
linkedTaskSourceContext: params.linkedTaskSourceContext,
|
|
comment: params.comment,
|
|
displayName: params.displayName,
|
|
telemetrySource: params.telemetrySource,
|
|
workspaceStatus: params.workspaceStatus,
|
|
manualOrder: params.manualOrder,
|
|
sparseCheckout: params.sparseCheckout,
|
|
pushTarget: params.pushTarget,
|
|
runHooks: params.runHooks === true,
|
|
activate: params.activate === true,
|
|
setupDecision: params.setupDecision,
|
|
createdWithAgent: params.createdWithAgent ?? params.startupAgent,
|
|
automationProvenance,
|
|
cliProvenance: buildCliWorkspaceProvenance(params.cliProvenanceRequest, {
|
|
startupAgent: params.startupAgent ?? params.createdWithAgent,
|
|
createdAt: Date.now()
|
|
}),
|
|
startup: params.startupCommand
|
|
? {
|
|
command: params.startupCommand,
|
|
...(params.startupEnv ? { env: params.startupEnv } : {}),
|
|
...(params.startupLaunchConfig
|
|
? { launchConfig: params.startupLaunchConfig }
|
|
: {}),
|
|
...(params.startupCommandDelivery
|
|
? { startupCommandDelivery: params.startupCommandDelivery }
|
|
: {})
|
|
}
|
|
: undefined,
|
|
...(params.startupAgent ? { startupAgent: params.startupAgent } : {}),
|
|
...(params.startupPrompt !== undefined ? { startupPrompt: params.startupPrompt } : {}),
|
|
startupDraft: params.startupDraft,
|
|
lineage: {
|
|
parentWorkspace: params.parentWorkspace,
|
|
envParentWorkspace: params.envParentWorkspace,
|
|
parentWorktree: params.parentWorktree,
|
|
...(params.cwdParentWorktree ? { cwdParentWorktree: params.cwdParentWorktree } : {}),
|
|
noParent: params.noParent === true,
|
|
callerTerminalHandle: params.callerTerminalHandle,
|
|
orchestrationContext: params.orchestrationContext
|
|
}
|
|
})
|
|
finishAutomationWorkspaceProvenanceRequest(params.automationProvenanceRequest)
|
|
// Why: agent callers need a stable dispatch target without traversing
|
|
// terminal-list layout duplicates after creating the worktree.
|
|
return params.startupAgent && result.startupTerminal?.handle
|
|
? { ...result, agentTerminalHandle: result.startupTerminal.handle }
|
|
: result
|
|
} catch (error) {
|
|
releaseAutomationWorkspaceProvenanceRequest(params.automationProvenanceRequest)
|
|
throw error
|
|
}
|
|
})
|
|
}),
|
|
defineMethod({
|
|
name: 'worktree.prefetchCreateBase',
|
|
params: WorktreePrefetchCreateBase,
|
|
handler: async (params, { runtime }) => {
|
|
await runtime.prefetchManagedWorktreeCreateBase({
|
|
repoSelector: params.repo,
|
|
baseBranch: params.baseBranch
|
|
})
|
|
return null
|
|
}
|
|
}),
|
|
defineMethod({
|
|
name: 'worktree.set',
|
|
params: WorktreeSet,
|
|
handler: async (params, { runtime }) => ({
|
|
worktree: await runtime.updateManagedWorktreeMeta(params.worktree, {
|
|
displayName: params.displayName,
|
|
linkedIssue: params.linkedIssue,
|
|
linkedPR: params.linkedPR,
|
|
linkedLinearIssue: params.linkedLinearIssue,
|
|
linkedLinearIssueWorkspaceId: params.linkedLinearIssueWorkspaceId,
|
|
linkedLinearIssueOrganizationUrlKey: params.linkedLinearIssueOrganizationUrlKey,
|
|
linkedGitLabMR: params.linkedGitLabMR,
|
|
linkedGitLabIssue: params.linkedGitLabIssue,
|
|
linkedBitbucketPR: params.linkedBitbucketPR,
|
|
linkedAzureDevOpsPR: params.linkedAzureDevOpsPR,
|
|
linkedGiteaPR: params.linkedGiteaPR,
|
|
linkedWorkItem: params.linkedWorkItem,
|
|
linkedTaskSourceContext: params.linkedTaskSourceContext,
|
|
comment: params.comment,
|
|
isArchived: params.isArchived,
|
|
isUnread: params.isUnread,
|
|
isPinned: params.isPinned,
|
|
sortOrder: params.sortOrder,
|
|
manualOrder: params.manualOrder,
|
|
lastActivityAt: params.lastActivityAt,
|
|
createdAt: params.createdAt,
|
|
sparseDirectories: params.sparseDirectories,
|
|
sparseBaseRef: params.sparseBaseRef,
|
|
sparsePresetId: params.sparsePresetId,
|
|
baseRef: params.baseRef,
|
|
workspaceStatus: params.workspaceStatus,
|
|
pushTarget: params.pushTarget,
|
|
diffComments: params.diffComments,
|
|
mobileDiffReview: params.mobileDiffReview,
|
|
lineage:
|
|
params.parentWorktree || params.noParent === true
|
|
? {
|
|
parentWorktree: params.parentWorktree,
|
|
noParent: params.noParent === true
|
|
}
|
|
: undefined
|
|
} as Parameters<typeof runtime.updateManagedWorktreeMeta>[1])
|
|
})
|
|
}),
|
|
defineMethod({
|
|
name: 'worktree.persistSortOrder',
|
|
params: WorktreeSortOrder,
|
|
handler: async (params, { runtime }) =>
|
|
runtime.persistManagedWorktreeSortOrder(params.orderedIds)
|
|
}),
|
|
defineMethod({
|
|
name: 'worktree.resolvePrBase',
|
|
params: WorktreeResolvePrBase,
|
|
handler: async (params, { runtime }) =>
|
|
runtime.resolveManagedPrBase({
|
|
repoSelector: params.repo,
|
|
prNumber: params.prNumber,
|
|
headRefName: params.headRefName,
|
|
baseRefName: params.baseRefName,
|
|
isCrossRepository: params.isCrossRepository
|
|
})
|
|
}),
|
|
defineMethod({
|
|
name: 'worktree.resolveMrBase',
|
|
params: WorktreeResolveMrBase,
|
|
handler: async (params, { runtime }) =>
|
|
runtime.resolveManagedMrBase({
|
|
repoSelector: params.repo,
|
|
mrIid: params.mrIid,
|
|
sourceBranch: params.sourceBranch,
|
|
targetBranch: params.targetBranch,
|
|
isCrossRepository: params.isCrossRepository
|
|
})
|
|
}),
|
|
defineMethod({
|
|
name: 'worktree.rm',
|
|
params: WorktreeRemove,
|
|
handler: async (params, { runtime }) => {
|
|
const result = await runtime.removeManagedWorktree(
|
|
params.worktree,
|
|
params.force === true,
|
|
params.runHooks === true
|
|
)
|
|
return { removed: true, ...result }
|
|
}
|
|
}),
|
|
defineMethod({
|
|
name: 'worktree.forceDeleteBranch',
|
|
params: WorktreeForceDeleteBranch,
|
|
handler: async (params, { runtime }) =>
|
|
runtime.forceDeletePreservedBranch(params.worktree, params.branchName, params.expectedHead)
|
|
})
|
|
]
|