mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +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>
198 lines
6.7 KiB
JavaScript
198 lines
6.7 KiB
JavaScript
#!/usr/bin/env node
|
|
// git show <base>:src/main/ai-vault/session-scanner.ts | node config/scripts/session-scan-cutoff-benchmark.mjs
|
|
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { performance } from 'node:perf_hooks'
|
|
import ts from 'typescript-api'
|
|
import { build } from 'esbuild'
|
|
import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs'
|
|
|
|
const baselineSource = ts.createSourceFile(
|
|
'session-scanner.ts',
|
|
readFileSync(0, 'utf8'),
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.TS
|
|
)
|
|
const baselineFunction = baselineSource.statements.find(
|
|
(node) => ts.isFunctionDeclaration(node) && node.name?.text === 'canStopParsingSessions'
|
|
)
|
|
assert(baselineFunction, 'Pipe the baseline session-scanner.ts on stdin')
|
|
|
|
async function load(contents) {
|
|
const result = await build({
|
|
stdin: { contents, resolveDir: path.resolve('src/main/ai-vault'), loader: 'ts' },
|
|
platform: 'node',
|
|
format: 'esm',
|
|
bundle: true,
|
|
write: false
|
|
})
|
|
const encoded = Buffer.from(result.outputFiles[0].text).toString('base64')
|
|
return import(`data:text/javascript;base64,${encoded}`)
|
|
}
|
|
const [baselineModule, currentModule] = await Promise.all([
|
|
load(`import { sessionSortTime } from './session-scanner-accumulator';
|
|
export ${baselineFunction.getText(baselineSource)}`),
|
|
load(`export { canStopParsingSessions } from './session-scan-cutoff';
|
|
export { ScannedSessionCollection } from './session-root-dedup';`)
|
|
])
|
|
const baseline = baselineModule.canStopParsingSessions
|
|
const current = currentModule.canStopParsingSessions
|
|
const { ScannedSessionCollection } = currentModule
|
|
|
|
let randomState = 91114
|
|
function random(bound) {
|
|
randomState = (Math.imul(randomState, 1664525) + 1013904223) >>> 0
|
|
return Math.floor((randomState / 2 ** 32) * bound)
|
|
}
|
|
function session(index, overrides = {}) {
|
|
return Object.freeze({
|
|
agent: 'claude',
|
|
executionHostId: 'local',
|
|
sessionId: `session-${index}`,
|
|
filePath: `/home/ada/.codex/sessions/rollout-${index}.jsonl`,
|
|
codexHome: null,
|
|
updatedAt: new Date(index).toISOString(),
|
|
modifiedAt: new Date(0).toISOString(),
|
|
...overrides
|
|
})
|
|
}
|
|
function collection(rows) {
|
|
const result = new ScannedSessionCollection()
|
|
for (const row of rows) {
|
|
result.add(row)
|
|
}
|
|
return result
|
|
}
|
|
function check(sessions, limit, next) {
|
|
const rows = [...sessions.values()]
|
|
assert.equal(current(sessions, limit, next), baseline(sessions, limit, next))
|
|
assert.deepEqual([...sessions.values()], rows)
|
|
}
|
|
|
|
const dates = [
|
|
null,
|
|
'',
|
|
'invalid',
|
|
'1970-01-01T00:00:00Z',
|
|
'1970-01-01T00:00:02+00:00',
|
|
'-000001-01-01T00:00:00Z',
|
|
'+010000-01-01T00:00:00Z',
|
|
'-271821-04-20T00:00:00.000Z'
|
|
]
|
|
const limits = [0, -1, -3, 0.5, 1.5, Number.NaN, Infinity, -Infinity]
|
|
const nextTimes = [undefined, Number.NaN, Infinity, -Infinity, 0, 1, 2, 2000]
|
|
let comparisons = 0
|
|
for (let trial = 0; trial < 4_000; trial += 1) {
|
|
const sessions = new ScannedSessionCollection()
|
|
const admitted = []
|
|
for (let batch = 0; batch < 10; batch += 1) {
|
|
const count = random(8)
|
|
for (let index = 0; index < count; index += 1) {
|
|
const id = random(12)
|
|
const row =
|
|
admitted.length && random(5) === 0
|
|
? admitted[random(admitted.length)]
|
|
: session(id, {
|
|
agent: random(3) ? 'codex' : 'claude',
|
|
executionHostId: random(4) ? 'local' : 'ssh:dev',
|
|
codexHome: random(2) ? null : '/custom',
|
|
updatedAt: random(3)
|
|
? new Date(random(5000) - 2500).toISOString()
|
|
: dates[random(dates.length)],
|
|
modifiedAt: random(4) ? new Date(random(5000)).toISOString() : 'invalid'
|
|
})
|
|
admitted.push(row)
|
|
sessions.add(row)
|
|
}
|
|
const limit = random(3) ? 1 + random(40) : limits[random(limits.length)]
|
|
const next = random(2) ? random(5000) - 2500 : nextTimes[random(nextTimes.length)]
|
|
check(sessions, limit, next)
|
|
comparisons += 1
|
|
}
|
|
}
|
|
console.log(`${comparisons} differential batch cutoffs passed.`)
|
|
|
|
function median(values) {
|
|
const sorted = values.toSorted((left, right) => left - right)
|
|
return (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2
|
|
}
|
|
function measure(name, run, repeats) {
|
|
const expected = run(baseline)
|
|
assert.deepEqual(run(current), expected)
|
|
const sample = (cutoff) => {
|
|
let result
|
|
const start = performance.now()
|
|
for (let index = 0; index < repeats; index += 1) {
|
|
result = run(cutoff)
|
|
}
|
|
const elapsed = (performance.now() - start) / repeats
|
|
assert.deepEqual(result, expected)
|
|
return elapsed
|
|
}
|
|
sample(baseline)
|
|
sample(current)
|
|
const samples = { baseline: [], current: [] }
|
|
for (const pair of buildCounterbalancedSchedule(8, 'baseline', 'current')) {
|
|
for (const arm of pair) {
|
|
samples[arm].push(sample(arm === 'baseline' ? baseline : current))
|
|
}
|
|
}
|
|
return { name, beforeMs: median(samples.baseline), afterMs: median(samples.current) }
|
|
}
|
|
|
|
console.log(
|
|
JSON.stringify({ node: process.version, platform: process.platform, arch: process.arch })
|
|
)
|
|
const results = []
|
|
for (const count of [8, 100, 1_000, 2_000, 10_000]) {
|
|
for (const order of ['ordered', 'shuffled']) {
|
|
const rows = Array.from({ length: count }, (_, index) => session(count - index))
|
|
if (order === 'shuffled') {
|
|
for (let index = count - 1; index > 0; index -= 1) {
|
|
const other = random(index + 1)
|
|
;[rows[index], rows[other]] = [rows[other], rows[index]]
|
|
}
|
|
}
|
|
const sessions = collection(rows)
|
|
for (const next of [0, count]) {
|
|
results.push(
|
|
measure(
|
|
`${count} ${order} / ${next === 0 ? 'stop' : 'continue'}`,
|
|
(cutoff) => cutoff(sessions, Math.ceil(count / 2), next),
|
|
Math.max(20, Math.floor(30_000 / count))
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
for (const invalidIndex of [0, 999]) {
|
|
const sessions = collection(
|
|
Array.from({ length: 1_000 }, (_, index) =>
|
|
session(index, invalidIndex === index ? { updatedAt: 'invalid' } : {})
|
|
)
|
|
)
|
|
results.push(measure(`1000 invalid at ${invalidIndex}`, (cutoff) => cutoff(sessions, 500, 0), 50))
|
|
}
|
|
const rows = Array.from({ length: 2_000 }, () => session(random(2_000)))
|
|
results.push(
|
|
measure(
|
|
'2000-candidate scan cutoff + admission / limit1000',
|
|
(cutoff) => {
|
|
const sessions = new ScannedSessionCollection()
|
|
let index = 0
|
|
while (index < rows.length && !cutoff(sessions, 1_000, 10_000)) {
|
|
const end = Math.min(rows.length, index + Math.min(8, Math.max(1, 1_000 - sessions.size)))
|
|
while (index < end) {
|
|
sessions.add(rows[index++])
|
|
}
|
|
}
|
|
return { parsed: index, sessions: sessions.size }
|
|
},
|
|
2
|
|
)
|
|
)
|
|
console.table(results)
|
|
console.log('Synthetic cutoff/admission CPU; excludes discovery, parsing, I/O and final sorting.')
|