Add Qoder session history and search (#24614)

* Add Qoder session history and search with real CLI coverage

* Allow the real Qoder marker file to end with a newline

* Keep Qoder tool output out of history previews and search

* Keep Qoder search pages readable by older clients

* Verify persisted Qoder history after a real generated and resumed task

* Negotiate Qoder filters before searching an older execution host

* Combine search client imports for the CI plugin gate

* Keep the relay search oracle aligned with legacy agent filtering

* test(qoder): align search capability contracts and pin old-host fencing
This commit is contained in:
Neil
2026-10-02 17:23:11 -07:00
committed by GitHub
parent 65deb4bdbe
commit f97ca2a49d
32 changed files with 985 additions and 28 deletions
+4 -1
View File
@@ -77,11 +77,14 @@ function formatDebug(debug: SessionSearchResults['debug']): string[] {
}
function formatUnavailable(
reason: 'disabled' | 'not-ready' | 'no-service' | 'scope-unknown'
reason: 'disabled' | 'not-ready' | 'no-service' | 'scope-unknown' | 'unsupported-agent'
): string {
if (reason === 'disabled') {
return 'Session search is off on this host.'
}
if (reason === 'unsupported-agent') {
return 'This host does not support history search for the selected agent. Update Orca on that host or select another agent.'
}
// The CLI scopes with --path, never with an identity, so this only reaches a
// caller that built a request by hand.
if (reason === 'scope-unknown') {
+9 -2
View File
@@ -102,7 +102,11 @@ describe('orca search over the runtime RPC', () => {
const { call } = await runSearch([['query', 'resize race']])
expect(call).toHaveBeenCalledTimes(1)
expect(call).toHaveBeenCalledWith('aiVault.searchSessions', { query: 'resize race', limit: 20 })
expect(call).toHaveBeenCalledWith('aiVault.searchSessions', {
query: 'resize race',
limit: 20,
supportsQoderHistory: true
})
})
const flagCases: [string, CliFlags, Record<string, unknown>][] = [
@@ -157,7 +161,10 @@ describe('orca search over the runtime RPC', () => {
it.each(flagCases)('sends %s', async (_name, flags, params) => {
const { call } = await runSearch(flags)
expect(call).toHaveBeenCalledWith('aiVault.searchSessions', params)
expect(call).toHaveBeenCalledWith('aiVault.searchSessions', {
...params,
supportsQoderHistory: true
})
})
it('calls the status RPC for --index-status', async () => {
+7 -1
View File
@@ -126,10 +126,16 @@ describe('orca search argument parsing', () => {
it('rejects an unknown --agent and names the known ones', () => {
expect(() => parseSearch(['search', 'q', '--agent', 'claude', '--agent', 'bogus'])).toThrow(
/Unknown --agent "bogus"\. Known agents: claude, codebuddy, codex, /
/Unknown --agent "bogus"\. Known agents: claude, codebuddy, qoder, codex, /
)
})
it('accepts Qoder without changing another agent filter', () => {
expect(
request(['search', 'q', '--agent', 'qoder', '--agent', 'codex']).filters?.agents
).toEqual(['qoder', 'codex'])
})
it('rejects more --path values than the contract accepts', () => {
const paths = Array.from({ length: 65 }, (_, index) => ['--path', `/p${index}`]).flat()
expect(() => parseSearch(['search', 'q', ...paths])).toThrow('Too many --path values (65)')