Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions packages/opencode/src/aiv/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,27 @@ import { AivPersistence } from "./persistence"

const log = Log.create({ service: "aiv" })

const MAX_TOUCHED_FILES = 500
const MAX_STRATEGY_CHANGES = 50
const IDLE_EVICTION_MS = 30 * 60 * 1000 // 30 minutes
const SWEEP_INTERVAL_MS = 5 * 60 * 1000 // 5 minutes

const intents = new Map<SessionID, AivSchema.Intent>()
const touchedFiles = new Map<SessionID, Set<string>>()

function isFilePath(value: string): boolean {
if (value.startsWith("http://") || value.startsWith("https://") || value.startsWith("//")) return false
if (value.includes("\n") || value.includes("\t")) return false
if (value.length > 500) return false
if (/^[.\/~]/.test(value) || /\/[^/]+\.\w+$/.test(value)) return true
return false
}

export namespace AivState {
export function has(sessionID: SessionID): boolean {
return intents.has(sessionID)
}

export function get(sessionID: SessionID): AivSchema.Intent {
return intents.get(sessionID) ?? AivSchema.empty(sessionID)
}
Expand Down Expand Up @@ -55,7 +72,7 @@ export namespace AivState {
to: next.workType,
timestamp: Date.now(),
}
next.strategyChanges = [...prev.strategyChanges, change]
next.strategyChanges = [...prev.strategyChanges, change].slice(-MAX_STRATEGY_CHANGES)
Bus.publish(AivEvent.StrategyChanged, { sessionID, change })
AivPersistence.appendEvent({
sessionID,
Expand Down Expand Up @@ -94,19 +111,24 @@ export namespace AivState {
})
}

function addFile(files: Set<string>, path: string) {
if (files.size >= MAX_TOUCHED_FILES) return
if (isFilePath(path)) files.add(path)
}

function handleToolPart(sessionID: SessionID, part: MessageV2.ToolPart) {
const files = getOrCreateFiles(sessionID)

// Extract file paths from tool input across all states
const input = "input" in part.state ? part.state.input : {}
if (input) {
const filePath = input.file_path ?? input.path ?? input.filename
if (typeof filePath === "string") files.add(filePath)
if (typeof filePath === "string") addFile(files, filePath)

const filePaths = input.files ?? input.paths
if (Array.isArray(filePaths)) {
for (const f of filePaths) {
if (typeof f === "string") files.add(f)
if (typeof f === "string") addFile(files, f)
}
}
}
Expand Down Expand Up @@ -144,7 +166,7 @@ export namespace AivState {

function handlePatchPart(sessionID: SessionID, files: string[]) {
const tracked = getOrCreateFiles(sessionID)
for (const f of files) tracked.add(f)
for (const f of files) addFile(tracked, f)

const allPaths = [...tracked]
updateIntent(sessionID, {
Expand Down Expand Up @@ -201,7 +223,7 @@ export namespace AivState {
const files = diff.map((d) => d.file)
if (files.length > 0) {
const tracked = getOrCreateFiles(sessionID)
for (const f of files) tracked.add(f)
for (const f of files) addFile(tracked, f)
const allPaths = [...tracked]
updateIntent(sessionID, {
location: classifyLocationFromPaths(allPaths),
Expand Down Expand Up @@ -243,9 +265,22 @@ export namespace AivState {
}),
)

// Periodic sweep to evict idle sessions
const sweepTimer = setInterval(() => {
const now = Date.now()
for (const [sessionID, intent] of intents) {
if (!intent.active && now - intent.timestamp > IDLE_EVICTION_MS) {
intents.delete(sessionID)
touchedFiles.delete(sessionID)
log.info("evicted idle session", { sessionID })
}
}
}, SWEEP_INTERVAL_MS)

log.info("AIV state subscriptions initialized")

return () => {
clearInterval(sweepTimer)
for (const unsub of unsubs) unsub()
log.info("AIV state subscriptions stopped")
}
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/server/routes/aiv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const AIVRoutes = lazy(() =>
validator("param", z.object({ sessionID: SessionID.zod })),
async (c) => {
const { sessionID } = c.req.valid("param")
if (!AivState.has(sessionID)) return c.json({ error: "Session not found" }, 404)
return c.json(AivState.get(sessionID))
},
)
Expand Down