mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
Support modern Qoder commands and verify authenticated resume
Start and resume Qoder through the existing execution-host selector when only the documented qoder command exists. Preserve legacy qodercli preference, explicit commands, quoting and session identity; disconnected SSH execution refuses without local fallback. Mobile history uses command-at-create only with the optional owned-create capability and an existing stable mutation identity. Reconcile authoritative execution-host inventory before retrying creation, adopt the same surviving operation, preserve WSL/incarnation metadata and restore only missing original launch metadata. Changed retry settings cannot replace original capture. Bounded evidence expires after 15 minutes; unavailable inventory or original evidence refuses recovery and leaves surviving execution untouched. Authoritative inventory proving absence preserves the existing recreation behavior; this is not a durable exactly-once ledger for completed one-shot side effects. Older hosts retain the acknowledged create-then-send path. Scope mobile launch authority to the current committed host/client generation, and recheck operation ownership after asynchronous preparation before later sends. Retire the mutation once the host acknowledges the resume; later ownership changes stop navigation without reporting a completed resume as failed or reusing a cached legacy pane. Preserve genuinely interrupted mutation identity. Preserve current-main OpenCode validation and merged Pi/Cursor behavior. Correct unchanged-main editor test fixtures to their production insertion-range and store contracts while retaining original assertions and production behavior. Credit: Neil Parker (@nwparker); Soperf and jyang for Qoder integration/history groundwork in #24614; actual Pullfrog and CodeRabbit reviews and the independent Source reviewer for the mobile retry, launch capture, evidence lifetime and connection ownership findings. Conservative positional process recognition and existing folder-history matching-worktree limitations remain documented.
This commit is contained in:
@@ -171,7 +171,8 @@ export function isExpectedAgentProcess(
|
||||
normalizedProcess.startsWith(`${normalizedExpected}.`) ||
|
||||
(['qoderclicn', 'qodercn'].includes(normalizedExpected) &&
|
||||
/^(?:qoderclicn|qodercn)(?:-\d.*)?$/.test(normalizedProcess)) ||
|
||||
(normalizedExpected === 'qodercli' && /^qodercli-\d/.test(normalizedProcess)) ||
|
||||
(['qoder', 'qodercli'].includes(normalizedExpected) &&
|
||||
/^(?:qoder|qodercli(?:-\d.*)?)$/.test(normalizedProcess)) ||
|
||||
(normalizedExpected === 'muse' && normalizedProcess.startsWith('muse-bin-'))
|
||||
)
|
||||
}
|
||||
@@ -200,6 +201,10 @@ export function recognizeAgentProcessFromCommandLine(
|
||||
const tokens = tokenizeCommandLine(commandLine)
|
||||
const firstNormalized = normalizeProcessName(tokens[0])
|
||||
let direct = recognizedAgentForProcess(firstNormalized)
|
||||
// Qoder's public dispatcher routes these commands to the IDE rather than the agent CLI.
|
||||
if (firstNormalized === 'qoder' && tokens[1] && !tokens[1].startsWith('-')) {
|
||||
return null
|
||||
}
|
||||
// Why: the generic Orca CLI is not an agent; only this subcommand launches its TUI mode.
|
||||
if (direct?.agent === 'claude-agent-teams' && tokens[1]?.toLowerCase() !== 'claude-teams') {
|
||||
direct = null
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AGENT_SESSION_RESUME_RUNTIME_CAPABILITIES } from './agent-session-resume-runtime-capabilities'
|
||||
import { QODER_OWNED_TERMINAL_CREATE_CAPABILITY } from './qoder-terminal-create-capability'
|
||||
export {
|
||||
AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY,
|
||||
@@ -354,6 +355,7 @@ export const AGENT_SESSION_CREATE_TAB_ID_RUNTIME_CAPABILITY =
|
||||
'agentSession.create.tab-id.v1' as const
|
||||
|
||||
export const RUNTIME_CAPABILITIES = [
|
||||
QODER_OWNED_TERMINAL_CREATE_CAPABILITY,
|
||||
...AGENT_SESSION_STOP_RUNTIME_CAPABILITIES,
|
||||
AGENT_SESSION_CREATE_TAB_ID_RUNTIME_CAPABILITY,
|
||||
ANTIGRAVITY_CONFIGURED_MODEL_RUNTIME_CAPABILITY,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
normalizeTerminalTitle,
|
||||
detectAgentStatusFromTitle
|
||||
} from './agent-detection'
|
||||
import { buildAgentStartupPlan } from './tui-agent-startup'
|
||||
import { buildAgentStartupPlan, buildAgentResumeStartupPlan } from './tui-agent-startup'
|
||||
import { getAgentResumeArgv } from './agent-session-resume'
|
||||
import { createHookListenerState } from './agent-hook-listener/listener-state'
|
||||
import { normalizeAndAccept } from './agent-hook-listener-test-harness'
|
||||
@@ -19,12 +19,16 @@ import { normalizeAndAccept } from './agent-hook-listener-test-harness'
|
||||
describe('Qoder agent identity and lifecycle', () => {
|
||||
it('recognizes the installed versioned binary on native platforms and excludes headless runs', () => {
|
||||
for (const command of [
|
||||
'qoder',
|
||||
'/home/dev/.qoder/entry/qoder',
|
||||
'C:\\Qoder\\qoder.cmd',
|
||||
'qodercli',
|
||||
'/home/dev/.qoder/bin/qodercli/qodercli-1.1.64',
|
||||
'C:\\Qoder\\qodercli-1.1.64.exe'
|
||||
]) {
|
||||
expect(recognizeAgentProcessFromCommandLine(command)?.agent).toBe('qoder')
|
||||
expect(isExpectedAgentProcess(command, 'qodercli')).toBe(true)
|
||||
expect(isExpectedAgentProcess(command, 'qoder')).toBe(true)
|
||||
}
|
||||
expect(recognizeAgentProcessFromCommandLine('qodercli --print hello')).toBeNull()
|
||||
for (const args of [
|
||||
@@ -40,8 +44,42 @@ describe('Qoder agent identity and lifecycle', () => {
|
||||
'qoder'
|
||||
)
|
||||
expect(recognizeAgentProcessFromCommandLine('qodercli-unrelated')).toBeNull()
|
||||
expect(recognizeAgentProcessFromCommandLine('qoder-unrelated')).toBeNull()
|
||||
})
|
||||
|
||||
it.each(['qoder', 'qodercli'])(
|
||||
'excludes headless %s and preserves interactive resume',
|
||||
(command) => {
|
||||
for (const args of [
|
||||
'--print hello',
|
||||
'-p hello',
|
||||
'--remote task',
|
||||
'--list-sessions',
|
||||
'--input-format stream-json',
|
||||
'--output-format stream-json',
|
||||
'--remote-control session'
|
||||
]) {
|
||||
expect(recognizeAgentProcessFromCommandLine(`${command} ${args}`)).toBeNull()
|
||||
}
|
||||
expect(
|
||||
recognizeAgentProcessFromCommandLine(`${command} --resume proof --prompt-interactive hello`)
|
||||
?.agent
|
||||
).toBe('qoder')
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['ide', 'chat', 'serve-web', 'tunnel', './project', '/home/dev/project', 'C:\\project'])(
|
||||
'does not identify qoder %s as an agent',
|
||||
(subcommand) => {
|
||||
expect(recognizeAgentProcessFromCommandLine(`qoder ${subcommand}`)).toBeNull()
|
||||
expect(
|
||||
recognizeAgentProcessFromCommandLine(`qoder ${subcommand}`, {
|
||||
includeHeadlessOneShot: true
|
||||
})
|
||||
).toBeNull()
|
||||
}
|
||||
)
|
||||
|
||||
it.each([
|
||||
['◇ Qoder CLI | Ready', 'idle'],
|
||||
['✦ Implement feature | Thinking', 'working'],
|
||||
@@ -144,6 +182,31 @@ it('does not rewrite DSH titles containing a pipe as Qoder', () => {
|
||||
expect(detectAgentStatusFromTitle(title)).not.toBe('working')
|
||||
})
|
||||
|
||||
it.each(['darwin', 'linux', 'win32'] as const)(
|
||||
'starts and resumes a configured modern Qoder entry on %s',
|
||||
(platform) => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'qoder',
|
||||
prompt: 'proof',
|
||||
cmdOverrides: { qoder: 'qoder' },
|
||||
platform,
|
||||
isRemote: platform === 'linux'
|
||||
})
|
||||
expect(plan?.launchCommand).toBe("qoder --prompt-interactive 'proof'")
|
||||
expect(plan?.launchConfig.agentCommand).toBe('qoder')
|
||||
const resume = buildAgentResumeStartupPlan({
|
||||
agent: 'qoder',
|
||||
providerSession: { key: 'session_id', id: 'proof-session' },
|
||||
cmdOverrides: {},
|
||||
agentCommand: plan?.launchConfig.agentCommand,
|
||||
platform,
|
||||
isRemote: platform === 'linux'
|
||||
})
|
||||
expect(resume?.launchCommand).toBe("qoder '--resume' 'proof-session'")
|
||||
expect(isExpectedAgentProcess('qodercli-1.1.65', resume?.expectedProcess ?? '')).toBe(true)
|
||||
}
|
||||
)
|
||||
|
||||
it('settles manual compaction without interrupting automatic compaction', () => {
|
||||
const state = createHookListenerState()
|
||||
const send = (hook_event_name: string, extra = {}) =>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const QODER_OWNED_TERMINAL_CREATE_CAPABILITY = 'session.tabs.qoderOwnedCreate.v1'
|
||||
@@ -149,6 +149,8 @@ const TUI_AGENT_CONFIG_SOURCE: Record<TuiAgent, TuiAgentConfigSource> = {
|
||||
},
|
||||
qoder: {
|
||||
detectCmd: 'qodercli',
|
||||
// The documented `qoder` dispatcher still launches the legacy CLI binary.
|
||||
detectCmdAliases: ['qoder'],
|
||||
promptInjectionMode: 'flag-prompt-interactive',
|
||||
preflightTrust: 'qoder'
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user