mirror of
https://github.com/stablyai/orca.git
synced 2026-10-04 00:02:21 +00:00
* refactor(runtime): split OrcaRuntimeService into focused modules
* test(runtime): cover admission tiers and strict worktree reconciliation
* fix(runtime): preserve owner and structured session visibility
* fix(runtime): port post-extraction compatibility fixes
* fix(runtime): preserve skill-share cancellation barrier
* test(runtime): update identity inventory after extraction
* fix(runtime): preserve hook transport environment cleanup
* fix(runtime): consolidate idle probe imports
* test(runtime): retire split file process allowlist entry
* fix(runtime): route child process types through shared boundary
* test(runtime): preserve worktree host metadata precedence
* fix(runtime): update extracted test seams
* fix(runtime): gate the split's ts-nocheck set and restore the stop-confirmed contract
Audit follow-ups for the OrcaRuntimeService split:
- Freeze the 171 @ts-nocheck files behind a ratchet so no new file can disable
type checking. The split's linear mixin chain cannot express forward
references yet, so the existing suppressions are grandfathered; the baseline
may only shrink.
- Drop the stray @ts-nocheck at the end of orca-runtime-get-status.ts. It sat
after the first statement, where TypeScript ignores it, so the module was
already checked.
- Restore `retireRejectedPty(ptyId, stopConfirmed: boolean)` as a required
argument. The split widened it to optional and patched the resulting error
with `stopConfirmed === true`; an omitted argument would have silently taken
the unverified-stop path instead of failing to compile.
- Guard that every orca-runtime-tests fragment is imported by the compatibility
entrypoint. The fragments are .spec.ts, which no Vitest include glob matches,
so one left out of the list would silently stop running.
* fix(runtime): restore four behaviors the OrcaRuntimeService split dropped
Audit findings against the refactor's true base (ad5ba2572e):
- retirePtyAgentLaunchAuthority collected pane keys after deleting the
restored-authority receipt instead of before it. collectPaneKeysForPty reads
that receipt, so a receipt-only pane lost its key and never had its agent-hook
compatibility authority retired. on-pty-exit.ts already carried a comment
naming this exact invariant.
- The PTY-exit path kept orchestrationMailboxNotifications.retirePty but lost
the loop that schedules a debounced mail-pointer repoint for the dead pty's
terminal handle and any run bound to its panes. Restores the schedule call
count to 7, matching base.
- subscribeToPtyExit lost isPtyKnownExited's leaf fallback and its
post-registration lifecycle-generation recheck. leavesByPtyId is rebuilt from
the renderer graph independently of ptysById, so a leaf can outlive its pty
record; without the fallback a caller waiting on an already-dead pty never
gets released.
- The chain root declared `[key: string]: unknown`, which base had nowhere. It
leaked through the exported runtime type into every consumer, so any misspelled
member access typechecked as unknown instead of erroring, and it accounted for
957 of the suppressed errors. Removing it costs zero type errors.
* fix(runtime): restore escalation prose and unscoped automation publication
Two more behaviors the split dropped, each with a regression test that fails
against the pre-fix code:
- The worker-exit escalation stopped deriving its title through
buildOrchestrationTaskDisplayMetadata and inlined `task.spec` instead. That
ignored an explicit task_title, dropped the single-line normalization and the
80-character bound, and turned the no-spec case into a quoted, duplicated id.
A multi-paragraph spec landed verbatim in the coordinator's banner. The
existing 11 tests all use short single-line specs, where the derived title and
the raw spec are identical, so none of them could see it.
Also reverts an added `if (!handle) return` guard: the dispatch lookup is
deliberately keyed on the pane as well, because a reminted handle no longer
matches the row while the pane identity outlives the remint.
- updateAutomation stopped going through automationChangePublications and
published `source` unconditionally while gating the fallback on a non-null
destination. A destination the store can no longer name then published only
the stale source, so subscribers scoped elsewhere kept rendering a row that
had left them — the exact case the helper documents. The helper had been left
with zero callers; all three sites use it again.
* fix(skills): stop swallowing lookup errors and hard-erroring on non-ssh hosts
Follow-ups from auditing the skill install path against the refactor's base:
- resolveWorktree wrapped showManagedWorktree in `.catch(() => null)`, so a
transient git or IO failure surfaced to the user as
skill-install-workspace-not-found with the real cause discarded. Errors
propagate again; a genuine id mismatch still returns null.
- resolveSkillSshTarget threw skill-install-workspace-host-unavailable when the
execution host was neither local nor ssh, on both the repo and folder
branches. Base gated these on connectionId, so a runtime-owned repo simply
was not an SSH install and fell through to the local path. Both return null
again, and the error code the split invented is now unreferenced.
- listManagedSkillInstalls awaited the receipt walk and the worktree resolve in
sequence. They are independent and either can hit disk, WSL, or an SSH scan,
so Promise.all is restored.
Deliberately unchanged: resolving the worktree through listResolvedWorktrees
rather than showManagedWorktree, which disambiguates a worktree id colliding
across hosts and is covered by its own test, and the SSH-folder
skill-install-ssh-dispatch-required throw, which matches the repo branch.
* fix(runtime): merge duplicate worktree-logic imports
The #17448 port added a third import from ../ipc/worktree-logic, which the
code-quality oxlint config rejects under --deny-warnings. Plain oxlint does not
flag it, so it only surfaced in CI's static analysis job.
* ci: run the ts-nocheck ratchet in PR checks
pr-workflow-lint-parity requires every leaf command in `pnpm lint` to have a
matching step in pr.yml. The ratchet was wired into lint but not the workflow,
so PR CI would not have enforced it.
* Merge remote-tracking branch 'origin/main' and retry the paired-host launch evaluate
main advanced 9 commits; none touch the orca-runtime.ts this branch splits, so
nothing needed porting.
CI failed twice on `Execution context was destroyed` thrown from
headless-paired-runtime-host's first `evaluate` after launch — a different spec
each run, which is the signature of the flake #17780 describes rather than a
regression. That commit added retryTransientMainEvaluate and adopted it in five
helpers but not this call site, even though its docblock names exactly this
case: the first evaluate after electron.launch() resolves, before the app is
ready. Wrapped it the same way.
226 lines
9.2 KiB
TypeScript
226 lines
9.2 KiB
TypeScript
// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests.
|
|
import { OrcaRuntimeWithListManagedWorktrees } from './orca-runtime-list-managed-worktrees'
|
|
import type { RuntimeNavigationTarget } from '../../shared/runtime-navigation'
|
|
import { navigationTargetsClients, navigationTargetsHost } from '../../shared/runtime-navigation'
|
|
import { getRepoExecutionHostId } from '../../shared/execution-host'
|
|
import type { Repo } from '../../shared/repo-types'
|
|
import type { TuiAgent } from '../../shared/tui-agent'
|
|
import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types'
|
|
import type {
|
|
WorktreeStartupDraftPaste,
|
|
WorktreeStartupFollowup
|
|
} from './runtime-worktree-agent-startup'
|
|
import {
|
|
buildWorktreeStartupForAgent,
|
|
buildWorktreeStartupForDraft,
|
|
markLocalWorktreeTrusted,
|
|
markRemoteWorktreeTrusted
|
|
} from './runtime-worktree-agent-startup'
|
|
import type { AgentLaunchPreferences } from '../../shared/agent-session-host-authority'
|
|
import type { Worktree } from '../../shared/worktree/types'
|
|
import type { WorktreeLineageResolution } from './runtime-worktree-lineage-resolution'
|
|
import type {
|
|
WorkspaceLineage,
|
|
WorktreeLineage,
|
|
WorktreeLineageWarning
|
|
} from '../../shared/worktree/lineage-types'
|
|
import { recordCreatedWorktreeLineage as recordCreatedWorktreeLineageState } from './runtime-worktree-lineage-recording'
|
|
import {
|
|
pasteWorktreeStartupDraftWhenReady,
|
|
sendWorktreeStartupFollowupWhenReady
|
|
} from './runtime-worktree-startup-readiness'
|
|
import type { CreateWorktreeResult } from '../../shared/worktree/create-types'
|
|
import { provisionWorktreeTerminals } from './runtime-worktree-terminal-provisioning'
|
|
|
|
export class OrcaRuntimeWithActivateManagedWorktree extends OrcaRuntimeWithListManagedWorktrees {
|
|
async activateManagedWorktree(
|
|
worktreeSelector: string,
|
|
opts: {
|
|
notifyClients?: boolean
|
|
clientKind?: 'mobile' | 'runtime'
|
|
navigation?: RuntimeNavigationTarget
|
|
} = {}
|
|
): Promise<{
|
|
repoId: string
|
|
worktreeId: string
|
|
activated: boolean
|
|
/** Mobile-scoped slept-agent wake outcome. `unsupported-headless` means no
|
|
* renderer holds the sleeping records (headless `orca serve`), so nothing
|
|
* woke — clients must not present the worktree's agents as resumed. */
|
|
sleepingAgentWake: 'requested' | 'unsupported-headless' | 'not-applicable'
|
|
}> {
|
|
this.assertGraphReady()
|
|
const worktree = await this.resolveWorktreeSelector(worktreeSelector)
|
|
const repo = this.store?.getRepo(worktree.repoId)
|
|
if (!repo) {
|
|
throw new Error('repo_not_found')
|
|
}
|
|
const navigation = opts.navigation ?? (opts.notifyClients === false ? 'caller' : 'all')
|
|
const targetsHost = navigationTargetsHost(navigation)
|
|
const targetsClients = navigationTargetsClients(navigation)
|
|
|
|
if (!targetsHost && this.store?.getWorktreeMeta(worktree.id)?.isUnread) {
|
|
// Why: mobile/web session activation intentionally bypasses renderer
|
|
// selection, so the runtime must acknowledge the unread state itself.
|
|
this.store.setWorktreeMeta(worktree.id, { isUnread: false })
|
|
this.notifyWorktreesChanged(repo.id)
|
|
}
|
|
|
|
let sleepingAgentWake: 'requested' | 'unsupported-headless' | 'not-applicable' =
|
|
'not-applicable'
|
|
if (targetsHost || targetsClients) {
|
|
// Why: inactive worktree terminal panes are renderer-owned and may not have
|
|
// live PTYs until the desktop activates the worktree and mounts them.
|
|
if (targetsHost) {
|
|
this.notifyHostActivateWorktree(repo.id, worktree.id)
|
|
}
|
|
if (targetsClients) {
|
|
this.notifyClientsActivateWorktree(repo.id, worktree.id)
|
|
}
|
|
}
|
|
if (!targetsHost) {
|
|
// Why: mobile/web selection needs fresh session surfaces without forcing
|
|
// every attached desktop renderer to navigate to the phone's workspace.
|
|
this.hydrateHeadlessMobileSessionTabsFromWorkspaceSession(worktree.id, {
|
|
allowAttachedWindow: true
|
|
})
|
|
await this.refreshMobileSessionPtyRecords()
|
|
this.notifyMobileSessionTabsChanged(worktree.id)
|
|
// Why: a phone open must also wake the worktree's slept agents (experimental
|
|
// agent sleep). Only the host renderer holds the sleeping records + wake
|
|
// authority, so fire-and-forget ask it — mobile-scoped so web/desktop are
|
|
// unaffected. Headless serve has no renderer to wake anything, so report
|
|
// that explicitly instead of letting mobile assume the agents resumed.
|
|
if (opts.clientKind === 'mobile') {
|
|
if (this.getAvailableAuthoritativeWindow()) {
|
|
this.notifier?.resumeSleepingAgents?.(worktree.id)
|
|
sleepingAgentWake = 'requested'
|
|
} else if (
|
|
// Why: sleeping records are partitioned by execution host; reading
|
|
// only the local partition would miss slept agents on SSH-host
|
|
// worktrees and skip the headless warning for them.
|
|
Object.values(
|
|
this.store?.getWorkspaceSession?.(getRepoExecutionHostId(repo))
|
|
.sleepingAgentSessionsByPaneKey ?? {}
|
|
).some((record) => record.worktreeId === worktree.id)
|
|
) {
|
|
// Why: headless is only degraded when this worktree actually has a
|
|
// persisted resume record. Ordinary mobile activation must not show
|
|
// an unsupported warning merely because no desktop window is open.
|
|
sleepingAgentWake = 'unsupported-headless'
|
|
}
|
|
}
|
|
}
|
|
return { repoId: repo.id, worktreeId: worktree.id, activated: true, sleepingAgentWake }
|
|
}
|
|
|
|
protected async buildStartupForDraft(
|
|
repo: Repo,
|
|
draft: string,
|
|
requestedAgent?: TuiAgent
|
|
): Promise<{
|
|
agent: TuiAgent
|
|
startup: WorktreeStartupLaunch
|
|
draftPaste?: WorktreeStartupDraftPaste
|
|
} | null> {
|
|
if (!this.store) {
|
|
return null
|
|
}
|
|
return buildWorktreeStartupForDraft({
|
|
repo,
|
|
draft,
|
|
...(requestedAgent ? { requestedAgent } : {}),
|
|
settings: this.store.getSettings(),
|
|
getLaunchPlatform: () => this.getAgentLaunchPlatformForRepo(repo)
|
|
})
|
|
}
|
|
|
|
protected buildStartupForAgent(
|
|
repo: Repo,
|
|
agent: TuiAgent,
|
|
prompt: string | undefined,
|
|
launchPreferences?: AgentLaunchPreferences
|
|
): { agent: TuiAgent; startup: WorktreeStartupLaunch; followup?: WorktreeStartupFollowup } {
|
|
if (!this.store) {
|
|
throw new Error('runtime_unavailable')
|
|
}
|
|
return buildWorktreeStartupForAgent({
|
|
repo,
|
|
agent,
|
|
...(prompt !== undefined ? { prompt } : {}),
|
|
...(launchPreferences ? { launchPreferences } : {}),
|
|
settings: this.store.getSettings(),
|
|
getLaunchPlatform: () => this.getAgentLaunchPlatformForRepo(repo),
|
|
toSessionOptions: (preferences) => this.toAgentSessionOptions(preferences)
|
|
})
|
|
}
|
|
|
|
protected async markLocalWorkspaceTrustedForAgent(
|
|
agent: TuiAgent,
|
|
workspacePath: string
|
|
): Promise<void> {
|
|
await markLocalWorktreeTrusted(agent, workspacePath)
|
|
}
|
|
|
|
protected async markWorkspaceTrustedForAgent(
|
|
agent: TuiAgent,
|
|
connectionId: string | null | undefined,
|
|
workspacePath: string
|
|
): Promise<void> {
|
|
if (connectionId) {
|
|
await this.markRemoteWorkspaceTrustedForAgent(agent, connectionId, workspacePath)
|
|
return
|
|
}
|
|
await this.markLocalWorkspaceTrustedForAgent(agent, workspacePath)
|
|
}
|
|
|
|
protected async markRemoteWorkspaceTrustedForAgent(
|
|
agent: TuiAgent,
|
|
connectionId: string,
|
|
workspacePath: string
|
|
): Promise<void> {
|
|
await markRemoteWorktreeTrusted(agent, connectionId, workspacePath)
|
|
}
|
|
|
|
protected recordCreatedWorktreeLineage(
|
|
worktree: Pick<Worktree, 'id' | 'instanceId'>,
|
|
lineageResolution: WorktreeLineageResolution
|
|
): {
|
|
lineage: WorktreeLineage | null
|
|
workspaceLineage: WorkspaceLineage | null
|
|
warnings: WorktreeLineageWarning[]
|
|
} {
|
|
return recordCreatedWorktreeLineageState(this.store, worktree, lineageResolution)
|
|
}
|
|
|
|
protected pasteStartupDraftWhenReady(handle: string, draft: WorktreeStartupDraftPaste): void {
|
|
pasteWorktreeStartupDraftWhenReady(this.getWorktreeStartupReadinessHost(), handle, draft)
|
|
}
|
|
|
|
protected sendStartupFollowupWhenReady(handle: string, followup: WorktreeStartupFollowup): void {
|
|
sendWorktreeStartupFollowupWhenReady(this.getWorktreeStartupReadinessHost(), handle, followup)
|
|
}
|
|
|
|
protected async provisionManagedWorktreeTerminals(args: {
|
|
worktreeSelector: string
|
|
worktreeId: string
|
|
worktreePath: string
|
|
setup?: CreateWorktreeResult['setup']
|
|
defaultTabs?: CreateWorktreeResult['defaultTabs']
|
|
primaryTerminalHandle?: string | null
|
|
hasStartupTerminal: boolean
|
|
setupCommandPlatform: 'windows' | 'posix'
|
|
observeSetupCompletion?: boolean
|
|
// Why: when the agent startup is sequenced to wait for setup
|
|
// (waitForAgentStartup), the startup PTY runs a wrapper that already embeds
|
|
// the setup command. Pass that wrapped command through so the Setup tab runs
|
|
// the same script the agent is waiting on instead of a bare runner.
|
|
wrappedSetupCommand?: string
|
|
// Why: a workspace provisioned in the background must not pull the sidebar
|
|
// to itself; the user never asked to look at these tabs.
|
|
surfaceOwner?: false
|
|
}): Promise<{ setupSpawned: boolean; setupTerminalHandle: string | null }> {
|
|
return provisionWorktreeTerminals(this.getWorktreeTerminalProvisioningHost(), args)
|
|
}
|
|
}
|