mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
* feat(native-chat): record which agent run wrote each journal item Each journal item row, subagent roster entry and background-task entry now records ownerFence: the lease fence of the agent run whose output produced it. Optional, persisted, no row version bump and no migration. The journal's itemFence() returns it, so the turn-verdict rules and per-run provider activity read the producing run rather than the writer's fence. * test(native-chat): fewer provenance tests: fold reopen and unstated-owner checks into the main case Drops the epoch-replacement case (Part 2 rewind test reads the same values through the same rebuild) and the corrupt-value case (no version writes one). * test(native-chat): an unstamped roster entry from an older build reads as its row provenance Moved here from Part 4 so it sits beside stampJournalEntryProvenance. * test(native-chat): build the older-build roster row inline, after main removed its body helper Main #27947 writes one row per subagent and deleted journal-subagent-group-body; a roster row from an older build is still what this case reads. * refactor(native-chat): provenance is per item only; drop per-entry stamping Main's #27947 writes one saved row per subagent, and each background task already has its own row, so an item's own ownerFence names the run of every entry a producer still writes. Per-entry stamps inside a row mattered only for the shared roster rows saved before #27947, and no run writes into those any more, so the row's own provenance settles them correctly. Removes the entry stamping, the optional ownerFence on roster entries and background tasks, and their tests. * fix(native-chat): an item's provenance is set when it is created; revisions keep it A newer run's provider can revise an item an older run created: Pi's start and an ACP session/load both report context usage onto the newest saved turn. Restamping that turn with the newer run let the newer run's exit settle the older run's unverifiable turn as interrupted. Only creation states an owner now, as main's itemFences rule did. * test(native-chat): keep the provenance test below the production lines it covers
77 lines
2.7 KiB
TypeScript
77 lines
2.7 KiB
TypeScript
import { z } from 'zod'
|
|
import {
|
|
AgentJournalItemBodySchema,
|
|
AgentJournalProducerLinkageFields,
|
|
AgentJournalTurnScopeSchema
|
|
} from './agent-session-journal-schemas'
|
|
import { parseAgentJournalItemKey } from './agent-session-journal-item-key'
|
|
import type { AgentSessionMutationEnvelope } from './agent-session-wire'
|
|
|
|
export const AGENT_SESSION_REWIND_REASONS = [
|
|
'unsupported',
|
|
'history-not-paginated',
|
|
'busy',
|
|
'stale-epoch',
|
|
'invalid-target',
|
|
'history-limit',
|
|
'provider-refused',
|
|
'proof-mismatch',
|
|
'outcome-unknown'
|
|
] as const
|
|
export type AgentSessionRewindReason = (typeof AGENT_SESSION_REWIND_REASONS)[number]
|
|
export type AgentSessionRewindSupport =
|
|
| { supported: true }
|
|
| { supported: false; reason: AgentSessionRewindReason }
|
|
export type AgentSessionRewindParams = {
|
|
envelope: AgentSessionMutationEnvelope
|
|
itemId: string
|
|
expectedEpoch: string
|
|
}
|
|
export type AgentSessionRewindResult = {
|
|
itemId: string
|
|
epoch: string
|
|
sequence?: number
|
|
}
|
|
|
|
const Key = z.string().min(1).max(4096)
|
|
export const AgentSessionRewindRecordSchema = z.object({
|
|
operationId: Key,
|
|
callerKey: Key,
|
|
itemId: Key,
|
|
providerItemId: Key.optional(),
|
|
expectedEpoch: Key,
|
|
contextClearOperationId: Key.optional(),
|
|
contextClearSequence: z.number().int().positive().optional(),
|
|
phase: z.enum(['prepared', 'provider-succeeded', 'completed', 'refused']),
|
|
epoch: Key.optional(),
|
|
sequence: z.number().int().nonnegative().optional(),
|
|
hydrationVerified: z.boolean().optional(),
|
|
reason: z.string().min(1).max(512).optional(),
|
|
retained: z
|
|
.array(
|
|
z.object({
|
|
itemId: Key.refine((key) => parseAgentJournalItemKey(key) !== null),
|
|
body: AgentJournalItemBodySchema,
|
|
observedAt: z.number().finite(),
|
|
// Absent on records written before rows stated them: the rebuild then places the row by position.
|
|
turnScope: AgentJournalTurnScopeSchema.optional(),
|
|
// The row's execution provenance, carried into the rebuilt epoch. Absent on records written
|
|
// before rows carried it.
|
|
ownerFence: z.number().int().nonnegative().optional(),
|
|
...AgentJournalProducerLinkageFields
|
|
})
|
|
)
|
|
.max(10_000)
|
|
})
|
|
export type AgentSessionRewindRecord = z.infer<typeof AgentSessionRewindRecordSchema>
|
|
export const isAgentSessionRewindRecord = (value: unknown): value is AgentSessionRewindRecord =>
|
|
AgentSessionRewindRecordSchema.safeParse(value).success
|
|
|
|
export function isAgentSessionRewindResult(value: unknown): value is AgentSessionRewindResult {
|
|
if (!value || typeof value !== 'object') {
|
|
return false
|
|
}
|
|
const result = value as Partial<AgentSessionRewindResult>
|
|
return typeof result.itemId === 'string' && typeof result.epoch === 'string'
|
|
}
|