mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 00:02:38 +00:00
refactor(jcode): fold three reverse-scan copies into one, reuse the shared hook POST
The jcode journal reader, the Claude transcript reader and the Command Code transcript reader each carried their own copy of the same reverse chunked line scan; they now share one tested helper. jcode's managed hook script drops its hand-rolled curl for buildPosixAgentHookPostCommand, which also gains it the raw-JSON transport and the --noproxy guard the bespoke copy was missing. Co-authored-by: czzczz <chanzrz_zbf@foxmail.com>
This commit is contained in:
committed by
Neil
co-authored by
czzczz
parent
7fb201f60f
commit
0a45ad5ec4
@@ -3,11 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
const { spawnProcessMock } = vi.hoisted(() => ({ spawnProcessMock: vi.fn() }))
|
||||
vi.mock('../../shared/child-process/run-process', () => ({ spawnProcess: spawnProcessMock }))
|
||||
|
||||
import {
|
||||
prewarmJcodeDaemon,
|
||||
resetJcodeDaemonPrewarmForTests,
|
||||
shouldPrewarmJcodeDaemon
|
||||
} from './daemon-prewarm'
|
||||
import { prewarmJcodeDaemon, resetJcodeDaemonPrewarmForTests } from './daemon-prewarm'
|
||||
|
||||
function stubChild() {
|
||||
return { unref: vi.fn(), on: vi.fn() }
|
||||
@@ -59,12 +55,13 @@ describe('jcode daemon pre-warm', () => {
|
||||
|
||||
it('stays out of the way on Windows, which has no runtime dir', () => {
|
||||
expect(
|
||||
shouldPrewarmJcodeDaemon({
|
||||
prewarmJcodeDaemon({
|
||||
launchAgent: 'jcode',
|
||||
runtimeDir: 'C:/tmp/orca-jcode/abc',
|
||||
platform: 'win32'
|
||||
})
|
||||
).toBe(false)
|
||||
expect(spawnProcessMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports failure instead of throwing when the binary is missing', () => {
|
||||
|
||||
@@ -9,33 +9,27 @@
|
||||
import { spawnProcess } from '../../shared/child-process/run-process'
|
||||
import { getTuiAgentLaunchCommand, TUI_AGENT_CONFIG } from '../../shared/tui-agent-config'
|
||||
|
||||
/** Runtime dirs already pre-warmed in this Orca process; the daemon outlives one pane. */
|
||||
const prewarmedRuntimeDirs = new Set<string>()
|
||||
export type JcodeDaemonPrewarm = {
|
||||
launchAgent?: string
|
||||
runtimeDir?: string
|
||||
cwd?: string
|
||||
env?: Record<string, string>
|
||||
platform?: NodeJS.Platform
|
||||
}
|
||||
|
||||
/** Runtime dirs already warmed in this Orca process; the daemon outlives one pane. */
|
||||
const warmed = new Set<string>()
|
||||
|
||||
export function resetJcodeDaemonPrewarmForTests(): void {
|
||||
prewarmedRuntimeDirs.clear()
|
||||
warmed.clear()
|
||||
}
|
||||
|
||||
/** The runtime dir to warm, or null when this pane is not a local jcode launch. */
|
||||
export function resolveJcodePrewarmRuntimeDir(args: {
|
||||
launchAgent?: string
|
||||
runtimeDir?: string
|
||||
platform?: NodeJS.Platform
|
||||
}): string | null {
|
||||
// Why non-Windows only: the runtime dir is a unix-socket directory, and Orca
|
||||
// only stamps it off Windows (see shouldInjectJcodeRuntimeDir).
|
||||
if (args.launchAgent !== 'jcode' || (args.platform ?? process.platform) === 'win32') {
|
||||
return null
|
||||
}
|
||||
return args.runtimeDir !== undefined && args.runtimeDir.length > 0 ? args.runtimeDir : null
|
||||
}
|
||||
|
||||
export function shouldPrewarmJcodeDaemon(args: {
|
||||
launchAgent?: string
|
||||
runtimeDir?: string
|
||||
platform?: NodeJS.Platform
|
||||
}): boolean {
|
||||
return resolveJcodePrewarmRuntimeDir(args) !== null
|
||||
/** The runtime dir to warm, or null when this pane is not a local jcode launch.
|
||||
* Why non-Windows only: the runtime dir is a unix-socket directory, and Orca only
|
||||
* stamps it off Windows (see shouldInjectJcodeRuntimeDir). */
|
||||
function prewarmTarget({ launchAgent, runtimeDir, platform }: JcodeDaemonPrewarm): string | null {
|
||||
const unsupported = launchAgent !== 'jcode' || (platform ?? process.platform) === 'win32'
|
||||
return unsupported || !runtimeDir ? null : runtimeDir
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,39 +39,31 @@ export function shouldPrewarmJcodeDaemon(args: {
|
||||
* listening, so a pre-warm that fails costs nothing beyond the cold start Orca
|
||||
* already had. Never throws, and never blocks the spawn path.
|
||||
*/
|
||||
export function prewarmJcodeDaemon(args: {
|
||||
launchAgent?: string
|
||||
runtimeDir?: string
|
||||
cwd?: string
|
||||
env?: Record<string, string>
|
||||
platform?: NodeJS.Platform
|
||||
}): boolean {
|
||||
const runtimeDir = resolveJcodePrewarmRuntimeDir(args)
|
||||
if (runtimeDir === null || prewarmedRuntimeDirs.has(runtimeDir)) {
|
||||
export function prewarmJcodeDaemon(args: JcodeDaemonPrewarm): boolean {
|
||||
const runtimeDir = prewarmTarget(args)
|
||||
if (runtimeDir === null || warmed.has(runtimeDir)) {
|
||||
return false
|
||||
}
|
||||
prewarmedRuntimeDirs.add(runtimeDir)
|
||||
warmed.add(runtimeDir)
|
||||
// A missing binary or a spawn refusal just means no head start; forget the dir so
|
||||
// the next pane on it can try again.
|
||||
const giveUp = (): boolean => (warmed.delete(runtimeDir), false)
|
||||
try {
|
||||
const child = spawnProcess({
|
||||
program: getTuiAgentLaunchCommand(TUI_AGENT_CONFIG.jcode, args.platform ?? process.platform),
|
||||
// Why --no-update: an update check on the pre-warm path would delay the very
|
||||
// socket the client is about to wait on.
|
||||
// Why --no-update: an update check here would delay the very socket the client
|
||||
// is about to wait on. Why stdio ignore + unref: the daemon is jcode's to own
|
||||
// and must outlive this spawn, so Orca keeps no handle on it.
|
||||
args: ['--no-update', 'serve'],
|
||||
cwd: args.cwd,
|
||||
env: { ...args.env, JCODE_RUNTIME_DIR: runtimeDir },
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
})
|
||||
// Why unref: the daemon is jcode's to own and must outlive this spawn; keeping a
|
||||
// handle would tie Orca's event loop to it.
|
||||
child.unref()
|
||||
child.on('error', () => {
|
||||
// A missing binary or a spawn refusal just means no head start.
|
||||
prewarmedRuntimeDirs.delete(runtimeDir)
|
||||
})
|
||||
child.on('error', giveUp)
|
||||
return true
|
||||
} catch {
|
||||
prewarmedRuntimeDirs.delete(runtimeDir)
|
||||
return false
|
||||
return giveUp()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,10 +105,10 @@ describe.runIf(process.platform !== 'win32')('jcode managed hook as jcode runs i
|
||||
const script = readFileSync(scriptPath, 'utf8')
|
||||
const gateBranch = script.slice(script.indexOf('if [ "$JCODE_HOOK_EVENT" = pre_tool ]'))
|
||||
expect(gateBranch).toContain('orca_post_jcode_event >/dev/null 2>&1 &')
|
||||
// The observer path keeps the plain call, so a slow POST cannot be lost to
|
||||
// a script that exited first.
|
||||
// The observer path keeps the foreground call, so a slow POST cannot be lost
|
||||
// to a script that exited first.
|
||||
expect(gateBranch).toContain('orca_post_jcode_event >/dev/null 2>&1 || :')
|
||||
expect(script.trimEnd().endsWith('exit 0')).toBe(true)
|
||||
expect(script).toContain('\norca_post_jcode_event\n')
|
||||
} finally {
|
||||
cleanup()
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('JcodeHookService', () => {
|
||||
expect(script).toContain('payload@-')
|
||||
// Why: the payload is jcode's own JCODE_HOOK_PAYLOAD, forwarded verbatim.
|
||||
expect(script).toContain('$JCODE_HOOK_PAYLOAD')
|
||||
expect(script).toContain('hook_event_name=${JCODE_HOOK_EVENT}')
|
||||
expect(script).toContain('payload="$JCODE_HOOK_PAYLOAD"')
|
||||
})
|
||||
|
||||
it('preserves unrelated config tables when installing hooks', () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
writeManagedScript
|
||||
} from '../agent-hooks/installer-utils'
|
||||
import { refreshManagedScriptIfPresent } from '../agent-hooks/managed-hook-script-refresh'
|
||||
import { buildPosixAgentHookPostCommand } from '../agent-hooks/hook-post-command'
|
||||
import {
|
||||
readTextFileRemote,
|
||||
writeManagedScriptRemote,
|
||||
@@ -58,43 +59,31 @@ function getManagedScript(target: 'local' | 'posix' = 'local'): string {
|
||||
'if [ -n "$ORCA_AGENT_HOOK_ENDPOINT" ] && [ -r "$ORCA_AGENT_HOOK_ENDPOINT" ]; then',
|
||||
' . "$ORCA_AGENT_HOOK_ENDPOINT" 2>/dev/null || :',
|
||||
'fi',
|
||||
// Why: pre_tool is jcode's gate. It writes the tool input to our stdin and
|
||||
// waits for us, so drain stdin before any exit — a tool input larger than
|
||||
// the pipe buffer would otherwise stall the agent mid-write.
|
||||
// Why before the env guard: pre_tool is jcode's gate — it writes the tool input to
|
||||
// our stdin and waits for us, so stdin must be drained before ANY exit path or a
|
||||
// tool input larger than the pipe buffer stalls the agent mid-write.
|
||||
'if [ "$JCODE_HOOK_EVENT" = pre_tool ]; then',
|
||||
` ${POSIX_HOOK_STDIN_DRAIN_COMMAND}`,
|
||||
'fi',
|
||||
'if [ -z "$ORCA_AGENT_HOOK_PORT" ] || [ -z "$ORCA_AGENT_HOOK_TOKEN" ] || [ -z "$ORCA_PANE_KEY" ]; then',
|
||||
' exit 0',
|
||||
'fi',
|
||||
// Why: jcode already supplies JCODE_HOOK_PAYLOAD as a JSON object (capped
|
||||
// at 16 KB), so Orca forwards it verbatim instead of hand-building JSON in
|
||||
// shell (unsafe for arbitrary text). The event name is also posted as a
|
||||
// top-level form field for old payloads that omit it.
|
||||
// Why the env var rather than a stdin capture: jcode hands the hook its payload as
|
||||
// a ready JSON object (capped at 16 KB), so Orca forwards it verbatim instead of
|
||||
// hand-building JSON in shell, which is unsafe for arbitrary text.
|
||||
'payload="$JCODE_HOOK_PAYLOAD"',
|
||||
'orca_post_jcode_event() {',
|
||||
' printf \'%s\' "$JCODE_HOOK_PAYLOAD" | curl -sS -X POST "http://127.0.0.1:${ORCA_AGENT_HOOK_PORT}/hook/jcode" \\',
|
||||
' --connect-timeout 0.5 --max-time 1.5 \\',
|
||||
' -H "Content-Type: application/x-www-form-urlencoded" \\',
|
||||
' -H "X-Orca-Agent-Hook-Token: ${ORCA_AGENT_HOOK_TOKEN}" \\',
|
||||
' --data-urlencode "paneKey=${ORCA_PANE_KEY}" \\',
|
||||
' --data-urlencode "tabId=${ORCA_TAB_ID}" \\',
|
||||
' --data-urlencode "launchToken=${ORCA_AGENT_LAUNCH_TOKEN}" \\',
|
||||
' --data-urlencode "worktreeId=${ORCA_WORKTREE_ID}" \\',
|
||||
' --data-urlencode "env=${ORCA_AGENT_HOOK_ENV}" \\',
|
||||
' --data-urlencode "version=${ORCA_AGENT_HOOK_VERSION}" \\',
|
||||
' --data-urlencode "hook_event_name=${JCODE_HOOK_EVENT}" \\',
|
||||
' --data-urlencode "session_id=${JCODE_HOOK_SESSION_ID}" \\',
|
||||
' --data-urlencode "cwd=${JCODE_HOOK_CWD}" \\',
|
||||
' --data-urlencode "payload@-" >/dev/null 2>&1 || true',
|
||||
...buildPosixAgentHookPostCommand('jcode').map((line) => ` ${line}`),
|
||||
'}',
|
||||
// Why: jcode reads this gate's stderr to EOF before releasing the tool call, so
|
||||
// the POST runs detached with both pipes closed. Orca observes the tool live and
|
||||
// adds no latency; the gate always allows (Orca never blocks a jcode tool).
|
||||
// Why detached on the gate: jcode reads this script's stderr to EOF before it
|
||||
// releases the tool call, so an inherited pipe would hold the tool open for as long
|
||||
// as the POST ran. Orca observes the tool live and adds no latency; the gate always
|
||||
// allows, because Orca never blocks a jcode tool.
|
||||
'if [ "$JCODE_HOOK_EVENT" = pre_tool ]; then',
|
||||
' orca_post_jcode_event >/dev/null 2>&1 &',
|
||||
' exit 0',
|
||||
'else',
|
||||
' orca_post_jcode_event >/dev/null 2>&1 || :',
|
||||
'fi',
|
||||
'orca_post_jcode_event',
|
||||
'exit 0',
|
||||
''
|
||||
].join('\n')
|
||||
|
||||
Reference in New Issue
Block a user