mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 08:02:31 +00:00
fix(claude): bound task generation history
This commit is contained in:
@@ -1,6 +1,44 @@
|
||||
import { isSettledBackgroundTaskState } from '../../shared/native-chat-background-task-row'
|
||||
import type { ClaudeBackgroundTaskRow } from './claude-background-task-row-lifecycle'
|
||||
|
||||
const MAX_GENERATION_ENTRIES = 512
|
||||
|
||||
/** Bounded run identity ledger. Once old ids fall out, a monotonic sequence
|
||||
* keeps a reused id from colliding with a durable row already in the journal. */
|
||||
export class ClaudeBackgroundTaskGenerationLedger {
|
||||
private readonly entries = new Map<string, number>()
|
||||
private nextUniqueGeneration = 1
|
||||
private evicted = false
|
||||
|
||||
next(id: string): number {
|
||||
const previous = this.entries.get(id)
|
||||
const generation =
|
||||
previous === undefined ? (this.evicted ? this.nextUniqueGeneration++ : 1) : previous + 1
|
||||
this.entries.delete(id)
|
||||
this.entries.set(id, generation)
|
||||
this.nextUniqueGeneration = Math.max(this.nextUniqueGeneration, generation + 1)
|
||||
while (this.entries.size > MAX_GENERATION_ENTRIES) {
|
||||
const oldest = this.entries.keys().next()
|
||||
if (oldest.done || oldest.value === id) {
|
||||
break
|
||||
}
|
||||
this.entries.delete(oldest.value)
|
||||
this.evicted = true
|
||||
}
|
||||
return generation
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.entries.size
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.entries.clear()
|
||||
this.nextUniqueGeneration = 1
|
||||
this.evicted = false
|
||||
}
|
||||
}
|
||||
|
||||
export function rememberBoundedClaudeTaskSet(ids: Set<string>, id: string, maxSize: number): void {
|
||||
ids.delete(id)
|
||||
ids.add(id)
|
||||
|
||||
@@ -586,6 +586,39 @@ describe('claude background task rows', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('bounds generation history without reusing an evicted durable identity', () => {
|
||||
const { rows, keys, latest } = harness([FORWARDED_TOOL, 'toolu_second'])
|
||||
for (let index = 0; index < 513; index += 1) {
|
||||
const taskId = `generation-${index}`
|
||||
rows.observe({ ...START_BASH, task_id: taskId })
|
||||
rows.observe({
|
||||
type: 'system',
|
||||
subtype: 'task_notification',
|
||||
task_id: taskId,
|
||||
status: 'completed'
|
||||
})
|
||||
}
|
||||
|
||||
rows.observe({
|
||||
...START_BASH,
|
||||
task_id: 'generation-0',
|
||||
tool_use_id: 'toolu_second',
|
||||
description: 'reused after ledger eviction'
|
||||
})
|
||||
|
||||
const identities = new Set(keys())
|
||||
expect(identities).toContain('claude-background-task:generation-0')
|
||||
expect(identities).toContain('claude-background-task:generation-0#2')
|
||||
expect(latest()).toMatchObject({
|
||||
taskId: 'generation-0',
|
||||
state: 'working',
|
||||
label: 'reused after ledger eviction'
|
||||
})
|
||||
|
||||
const generations = Reflect.get(rows, 'generations')
|
||||
expect(generations.size).toBeLessThanOrEqual(512)
|
||||
})
|
||||
|
||||
it('declines coverage so the fallback still reports when every row slot is live', () => {
|
||||
// The row map is bounded. A task that cannot be admitted for lack of a slot
|
||||
// is not silently swallowed: coverage is declined so the generic fallback
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
type ClaudeBackgroundTaskRow
|
||||
} from './claude-background-task-row-lifecycle'
|
||||
import {
|
||||
ClaudeBackgroundTaskGenerationLedger,
|
||||
ensureClaudeBackgroundTaskRowSlot,
|
||||
rememberBoundedClaudeTaskMap,
|
||||
rememberBoundedClaudeTaskSet,
|
||||
@@ -64,7 +65,7 @@ export class ClaudeBackgroundTaskRows {
|
||||
private readonly rows = new Map<string, ClaudeBackgroundTaskRow>()
|
||||
/** Runs seen per task id, so a reused id opens a new row instead of
|
||||
* overwriting the finished one. Survives the row being evicted. */
|
||||
private readonly generations = new Map<string, number>()
|
||||
private readonly generations = new ClaudeBackgroundTaskGenerationLedger()
|
||||
private readonly foreign = new Map<string, ForeignOwner>()
|
||||
/** Tasks that were declined because every typed row slot was live. Their
|
||||
* later frames must remain visible through the generic fallback. */
|
||||
@@ -184,7 +185,7 @@ export class ClaudeBackgroundTaskRows {
|
||||
return true
|
||||
}
|
||||
if (shouldRestartClaudeBackgroundTaskRow(existing, message)) {
|
||||
this.openRow(id, message, existing.generation + 1)
|
||||
this.openRow(id, message)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -208,14 +209,14 @@ export class ClaudeBackgroundTaskRows {
|
||||
return true
|
||||
}
|
||||
if (!ensureClaudeBackgroundTaskRowSlot(this.rows, MAX_TASK_ROWS)) {
|
||||
this.rememberFallbackTaskId(id)
|
||||
rememberBoundedClaudeTaskSet(this.fallbackTaskIds, id, MAX_FALLBACK_TASK_IDS)
|
||||
return false
|
||||
}
|
||||
if (restartedTerminal) {
|
||||
this.terminalTaskIds.delete(id)
|
||||
this.terminalToolUseIds.delete(id)
|
||||
}
|
||||
this.openRow(id, message, (this.generations.get(id) ?? 0) + 1)
|
||||
this.openRow(id, message)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -232,8 +233,8 @@ export class ClaudeBackgroundTaskRows {
|
||||
return toolUseId === undefined || this.deps.isForwardedParentTool(toolUseId)
|
||||
}
|
||||
|
||||
private openRow(id: string, message: Record<string, unknown>, generation: number): void {
|
||||
this.generations.set(id, generation)
|
||||
private openRow(id: string, message: Record<string, unknown>): void {
|
||||
const generation = this.generations.next(id)
|
||||
this.rows.set(id, newClaudeBackgroundTaskRow(id, message, this.now(), generation))
|
||||
this.write(id)
|
||||
}
|
||||
@@ -349,10 +350,6 @@ export class ClaudeBackgroundTaskRows {
|
||||
rememberBoundedClaudeTaskMap(this.foreign, id, owner, MAX_FOREIGN_TASK_ROWS)
|
||||
}
|
||||
|
||||
private rememberFallbackTaskId(id: string): void {
|
||||
rememberBoundedClaudeTaskSet(this.fallbackTaskIds, id, MAX_FALLBACK_TASK_IDS)
|
||||
}
|
||||
|
||||
private rememberTerminal(id: string, toolUseId?: string): void {
|
||||
rememberClaudeBackgroundTaskTerminal(
|
||||
this.terminalTaskIds,
|
||||
|
||||
Reference in New Issue
Block a user