mirror of
https://github.com/stablyai/orca.git
synced 2026-10-12 16:03:03 +00:00
* fix(ai-vault): discover and parse Devin sessions on Windows, restore workspace mapping Devin sessions never appeared in the AI Vault on Windows, and parsed nearly empty elsewhere: - The transcripts root hardcoded the XDG layout (~/.local/share/devin/cli/transcripts), but Devin CLI writes under %APPDATA%/devin/cli/transcripts on Windows. The root is now platform-aware (APPDATA on win32, XDG_DATA_HOME elsewhere) for both local scans and win32 remote hosts, and APPDATA joins the scanner child's env allowlist so relocated AppData resolves. - The parser read metadata.is_user_input / created_at / metrics, which real ATIF-v1.7 transcripts don't carry. It now also accepts the ATIF step shape (source, timestamp, step-level metrics/model_name, plain-string message) while keeping the legacy shape. - ATIF transcripts carry no working_directory, so sessions couldn't group under a workspace. The sibling sessions.db index is now merged through the existing sidecar seam: it fills cwd/title/model/ timestamps, honors the db's hidden flag, and re-merges on db-only changes without re-reading transcripts. * fix(ai-vault): inline Devin transcripts root, harden parser/db edge cases - Resolve the platform-aware Devin cli dir in agent-sources instead of importing the shared devin-cli-data-dir module, which is not part of this change (broke typecheck). - Exclude source:'system' steps unconditionally, even when legacy metadata fields would classify them as user/assistant messages. - Guard unix-seconds conversion against out-of-range values so a single bad sessions.db row cannot mark the whole index unreadable. * fix(ai-vault): watch sessions.db-wal so live Devin metadata cannot go stale In WAL mode, committed rows sit in sessions.db-wal while sessions.db keeps its stat until checkpoint, so keying the dependency on the db alone could serve a stale index. The dependency now observes the wal when one exists; the reader still opens sessions.db itself. * fix(ai-vault): probe sessions.db-wal through the WSL-gated stat existsSync bypasses wslGatedStat and can hang a scan on a stalled 9P mount; the fs-import guard forbids it in session-scanner modules. The dependency path resolution is now async and probes through the gate. * fix(ai-vault): honor zero metrics and array messages in Devin steps - firstDevinMetricValue skipped explicit numeric zeros, letting a lower-priority positive metric win and overstating token totals. - ATIF allows step.message as an array of content parts; route it through extractContentText so those steps still feed title/preview. * test(ai-vault): cover array-valued ATIF message extraction The extractDevinStepText fallback that routes an array-valued step.message through extractContentText shipped without a fixture that produces that shape, so a future refactor could silently drop the branch. Pin that an array of text parts feeds the step's title and preview. * fix(ai-vault): invalidate old Devin caches and bound database retries * Discover current Devin ATIF exports alongside legacy transcripts * Recognize drawn geometry in the browser markup contract test * Deduplicate Devin exports across transcript roots * Account for the workspace sleep-state reader in scan budget * Align OMP integration tests with recorded-path resume * fix: update scan benchmarks and await relay environment test --------- Co-authored-by: Neil <neil@stably.ai>
126 lines
4.8 KiB
TypeScript
126 lines
4.8 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { createAgentStatusExtensionHarness } from '../../src/main/pi/agent-status-extension-test-harness'
|
|
import { normalizeHookPayload } from '../../src/shared/agent-hook-listener'
|
|
import { createHookListenerState } from '../../src/shared/agent-hook-listener/listener-state'
|
|
import { getAgentResumeArgv } from '../../src/shared/agent-session-resume'
|
|
import { buildAgentResumeStartupPlan } from '../../src/shared/tui-agent-resume-startup'
|
|
import { sleepingAgentSessionsByPaneKeySchema } from '../../src/shared/workspace-session-sleeping-agents'
|
|
import { createTestStore, makeTab } from '../../src/renderer/src/store/slices/store-test-helpers'
|
|
|
|
const PANE = 'tab-1:11111111-1111-4111-8111-111111111111'
|
|
const ROOT_SESSION = '22222222-2222-4222-8222-222222222222'
|
|
const ROOT_TRANSCRIPT_PATH = '/sessions/root.jsonl'
|
|
const CHILD_SESSION = '33333333-3333-4333-8333-333333333333'
|
|
|
|
async function rootWithActiveChild(): Promise<ReturnType<typeof createTestStore>> {
|
|
const store = createTestStore()
|
|
store.setState({ tabsByWorktree: { 'wt-1': [makeTab({ id: 'tab-1', worktreeId: 'wt-1' })] } })
|
|
const listener = createHookListenerState()
|
|
const harness = createAgentStatusExtensionHarness({
|
|
kind: 'omp',
|
|
env: {
|
|
ORCA_PANE_KEY: PANE,
|
|
ORCA_WORKTREE_ID: 'wt-1',
|
|
ORCA_AGENT_HOOK_VERSION: '1',
|
|
ORCA_AGENT_HOOK_ENV: 'production'
|
|
},
|
|
fetchImpl: async (_url, init) => {
|
|
const event = normalizeHookPayload(
|
|
listener,
|
|
'omp',
|
|
JSON.parse(String(init?.body)),
|
|
'production'
|
|
)
|
|
if (!event) {
|
|
throw new Error('Expected a normalized OMP hook')
|
|
}
|
|
store
|
|
.getState()
|
|
.setAgentStatus(
|
|
PANE,
|
|
event.payload,
|
|
'OMP',
|
|
{ updatedAt: Date.now(), stateStartedAt: Date.now() },
|
|
{ tabId: 'tab-1', worktreeId: 'wt-1' },
|
|
{ providerSession: event.providerSession, launchToken: event.launchToken }
|
|
)
|
|
return { ok: true }
|
|
}
|
|
})
|
|
const emit = async (
|
|
name: string,
|
|
event: unknown,
|
|
sessionManager: {
|
|
getSessionId: () => string
|
|
getSessionFile: () => string
|
|
}
|
|
): Promise<void> => {
|
|
await harness.callHook(name, event, { sessionManager })
|
|
for (let i = 0; i < 10; i++) {
|
|
await Promise.resolve()
|
|
}
|
|
}
|
|
const root = { getSessionId: () => ROOT_SESSION, getSessionFile: () => ROOT_TRANSCRIPT_PATH }
|
|
await emit('session_start', {}, root)
|
|
await emit('before_agent_start', { prompt: 'ROOT distinctive user request' }, root)
|
|
await emit(
|
|
'message_end',
|
|
{ message: { role: 'assistant', content: 'ROOT assistant preview' } },
|
|
root
|
|
)
|
|
expect(store.getState().agentStatusByPaneKey[PANE]?.providerSession?.id).toBe(ROOT_SESSION)
|
|
|
|
// OMP binds each task's extension factory to a different SessionManager in the same process.
|
|
harness.reload()
|
|
const child = {
|
|
getSessionId: () => CHILD_SESSION,
|
|
getSessionFile: () => '/sessions/root/worker.jsonl'
|
|
}
|
|
await emit('session_start', {}, child)
|
|
await emit('before_agent_start', { prompt: 'CHILD delegated bootstrap' }, child)
|
|
await emit(
|
|
'message_end',
|
|
{ message: { role: 'assistant', content: 'CHILD assistant preview' } },
|
|
child
|
|
)
|
|
return store
|
|
}
|
|
|
|
describe('OMP child lifecycle recovery boundaries', () => {
|
|
it('retains the normalized root prompt and assistant preview while its child works (#9348)', async () => {
|
|
const store = await rootWithActiveChild()
|
|
expect(store.getState().agentStatusByPaneKey[PANE]).toMatchObject({
|
|
prompt: 'ROOT distinctive user request',
|
|
lastAssistantMessage: 'ROOT assistant preview',
|
|
state: 'working',
|
|
providerSession: { id: ROOT_SESSION }
|
|
})
|
|
})
|
|
|
|
it('captures and hydrates the root identity into resume startup after child hooks (#16353)', async () => {
|
|
const store = await rootWithActiveChild()
|
|
store.getState().captureAllSleepingAgentSessions('quit')
|
|
const serialized = JSON.stringify(store.getState().sleepingAgentSessionsByPaneKey)
|
|
const hydrated = sleepingAgentSessionsByPaneKeySchema.parse(JSON.parse(serialized))
|
|
const record = hydrated?.[PANE]
|
|
if (!record) {
|
|
throw new Error('Expected the root sleeping record to survive hydration')
|
|
}
|
|
expect(record).toMatchObject({ origin: 'quit', providerSession: { id: ROOT_SESSION } })
|
|
expect(getAgentResumeArgv(record.agent, record.providerSession)).toEqual([
|
|
'omp',
|
|
'--resume',
|
|
ROOT_TRANSCRIPT_PATH
|
|
])
|
|
const startup = buildAgentResumeStartupPlan({
|
|
agent: record.agent,
|
|
providerSession: record.providerSession,
|
|
cmdOverrides: {},
|
|
platform: 'linux',
|
|
...record.launchConfig
|
|
})
|
|
expect(startup?.launchCommand).toBe(`omp '--resume' '${ROOT_TRANSCRIPT_PATH}'`)
|
|
expect(serialized).not.toContain(CHILD_SESSION)
|
|
})
|
|
})
|