mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 08:03:12 +00:00
fix(claude): never let a later failed look collapse an observed live descendant into unverifiable
The reaper's single assignment site latched only 'exited', so a second reap whose table reads all missed their deadline overwrote an earlier completed verification's 'live' with 'unverifiable'. The acquisition release gate discriminates on exactly that pair, so a root exit after such a decay released the lease over a descendant that had been observed alive. The latch is now monotone in trust order: exited is final, and live is only ever raised to exited. Claude-Session: https://claude.ai/code/session_01HfdhsvSJucLw4cTZxzg2CP
This commit is contained in:
@@ -413,6 +413,27 @@ describe('claude child tree reaper', () => {
|
||||
expect(tree.treeVerdict).toBe('exited')
|
||||
})
|
||||
|
||||
it('keeps an observed live descendant when a later re-read cannot see the table', async () => {
|
||||
const child = mockChild()
|
||||
// Reap #1 completed and saw a descendant alive at its deadline; the root then
|
||||
// left on its own and the re-verification on a loaded host could not read the
|
||||
// table. "Could not look" must not erase "was seen alive": the lease release
|
||||
// gate is exactly the pair this distinguishes.
|
||||
const terminateDescendants = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce('live')
|
||||
.mockResolvedValueOnce('unverifiable')
|
||||
const tree = createClaudeChildTreeReaper(child, {
|
||||
platform: 'linux',
|
||||
captureDescendants: vi.fn(async () => snapshotOf(4243)),
|
||||
terminateDescendants
|
||||
})
|
||||
|
||||
await expect(tree.reap()).resolves.toBe('live')
|
||||
await expect(tree.reap()).resolves.toBe('unverifiable')
|
||||
expect(tree.treeVerdict).toBe('live')
|
||||
})
|
||||
|
||||
it('treats an unreadable process table as unproven and re-walks the live root', async () => {
|
||||
const child = mockChild()
|
||||
// A loaded host can miss the table's deadline; while the root still lives
|
||||
|
||||
@@ -15,6 +15,17 @@ import { terminateWindowsProcessTree } from '../windows-process-tree-kill'
|
||||
const GRACEFUL_EXIT_MS = 1_500
|
||||
const FORCED_EXIT_MS = 1_000
|
||||
|
||||
/**
|
||||
* A later reap may only raise the latched verdict. An observed exit is final, and
|
||||
* a descendant seen alive at a deadline is never forgotten by a later look that
|
||||
* could not read the table: the lease gate discriminates on exactly that pair.
|
||||
*/
|
||||
const TREE_VERDICT_TRUST: Record<DescendantTreeVerdict, number> = {
|
||||
unverifiable: 0,
|
||||
live: 1,
|
||||
exited: 2
|
||||
}
|
||||
|
||||
type ReapableChild = Pick<SpawnedProcess, 'pid' | 'kill'>
|
||||
|
||||
/** One platform's descendant tree, tagged so neither verifier can be handed the other's rows. */
|
||||
@@ -191,8 +202,8 @@ export function createClaudeChildTreeReaper(
|
||||
const attempt = judgeTree()
|
||||
.catch((): DescendantTreeVerdict => 'unverifiable')
|
||||
.then((verdict) => {
|
||||
// An observed exit is final; anything later can only be a stale re-read.
|
||||
treeVerdict = treeVerdict === 'exited' ? 'exited' : verdict
|
||||
treeVerdict =
|
||||
TREE_VERDICT_TRUST[verdict] > TREE_VERDICT_TRUST[treeVerdict] ? verdict : treeVerdict
|
||||
return verdict
|
||||
})
|
||||
inFlight = attempt
|
||||
|
||||
Reference in New Issue
Block a user