feat: attribute workspace checkpoint changes (#1116)

This commit is contained in:
Brad Groux 2026-07-26 02:11:11 -05:00 committed by GitHub
parent 7b61b9a778
commit 2578fde055
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 752 additions and 16 deletions

View file

@ -35,7 +35,9 @@ Captures are serialized per attempt and chained through `parentCheckpointId`. Ev
Direct parent-to-child checkpoints can be compared without touching the worktree. The bounded comparison reports affected captured files, line-numbered unified hunks, content digests, mode changes, and whether HEAD, branch, index, or Git status changed. Comparisons fail closed if either checkpoint is missing, the checkpoints are not directly chained, or their worktree ownership evidence differs.
This foundation does not yet claim hunk attribution, rewind conflict analysis, restore, or retention cleanup. Those layers must consume the immutable repository and remain preview-first. Rewind cannot write until current HEAD, index, file hashes, worktree ownership, external changes, and approval evidence all match the checkpoint descendant it intends to replace.
Provider event mappers normalize bounded relative file paths and tool names into the causal journal. The attribution service considers only evidence between the two checkpoint-created events. Explicit provider file events and known path-bearing write tools are agent evidence; operator file events are operator evidence; system file events are external evidence. Every changed hunk inherits the conservative file-window attribution. Missing or mixed evidence is `unknown`, and missing checkpoint event boundaries mark the evidence window incomplete.
This foundation does not yet claim exact overlapping-hunk attribution, rewind conflict analysis, restore, or retention cleanup. Those layers must consume the immutable repository and remain preview-first. Rewind cannot write until current HEAD, index, file hashes, worktree ownership, external changes, and approval evidence all match the checkpoint descendant it intends to replace.
## Code
@ -44,4 +46,5 @@ This foundation does not yet claim hunk attribution, rewind conflict analysis, r
- File repository: `server/src/storage/workspace-checkpoint-repository.ts`
- Ownership and boundary coordination: `server/src/services/workspace-checkpoint-service.ts`
- Read-only comparison: `server/src/services/workspace-checkpoint-diff-service.ts`
- Focused verification: `server/src/__tests__/workspace-checkpoint-repository.test.ts` and `server/src/__tests__/workspace-checkpoint-diff-service.test.ts`
- Conservative causal attribution: `server/src/services/workspace-checkpoint-attribution-service.ts`
- Focused verification: `server/src/__tests__/workspace-checkpoint-repository.test.ts`, `server/src/__tests__/workspace-checkpoint-diff-service.test.ts`, and `server/src/__tests__/workspace-checkpoint-attribution-service.test.ts`

View file

@ -156,6 +156,9 @@ describe('RunEventJournalService', () => {
const oversized = appendInput({
providerEventId: 'provider_oversized_1',
payload: {
providerType: 'item.completed',
tool: 'Write',
paths: ['server/src/index.ts'],
chunks: Array.from(
{ length: 10 },
(_, index) => `${index}:${'bounded provider output '.repeat(360)}`
@ -169,6 +172,9 @@ describe('RunEventJournalService', () => {
expect(result.event.payload).toMatchObject({
spilled: true,
originalPayloadBytes: expect.any(Number),
providerType: 'item.completed',
tool: 'Write',
paths: ['server/src/index.ts'],
});
expect(result.event.payload.outputArtifact).toMatchObject({
schemaVersion: 'run-output-preview/v1',
@ -459,6 +465,36 @@ describe('provider run event mappers', () => {
kind: 'tool.started',
providerEventId: 'tool_1',
});
expect(
getProviderRunEventMapper('codex-cli').mapEvent('item.completed', {
item: {
id: 'file_1',
type: 'file_change',
file_path: './server/src/index.ts',
ignored: { path: '../outside.ts' },
},
})
).toMatchObject({
kind: 'file.changed',
payload: { paths: ['server/src/index.ts'] },
});
expect(
getProviderRunEventMapper('claude-code').mapEvent('assistant.tool_use', {
type: 'assistant',
message: {
content: [
{
type: 'tool_use',
name: 'Write',
input: { file_path: 'docs/guide.md' },
},
],
},
})
).toMatchObject({
kind: 'tool.started',
payload: { tool: 'Write', paths: ['docs/guide.md'] },
});
});
it('deduplicates retries without collapsing distinct phases that share one item ID', () => {

View file

@ -0,0 +1,234 @@
import { describe, expect, it, vi } from 'vitest';
import type {
RunEventEnvelope,
RunEventSource,
WorkspaceCheckpointDiff,
WorkspaceCheckpointFileDiff,
} from '@veritas-kanban/shared';
import { WorkspaceCheckpointAttributionService } from '../services/workspace-checkpoint-attribution-service.js';
const fromCheckpointId = 'checkpoint_from1234567890123456';
const toCheckpointId = 'checkpoint_to123456789012345678';
const input = {
workspaceId: 'workspace-872',
taskId: 'task-872',
attemptId: 'attempt-872',
fromCheckpointId,
toCheckpointId,
};
function changedFile(path: string): WorkspaceCheckpointFileDiff {
return {
path,
kind: 'modified',
source: 'tracked',
fromState: 'present',
toState: 'present',
additions: 1,
deletions: 1,
hunks: [
{
header: '@@ -1 +1 @@',
oldStart: 1,
oldLines: 1,
newStart: 1,
newLines: 1,
lines: [
{ kind: 'deletion', content: 'old', oldLineNumber: 1 },
{ kind: 'addition', content: 'new', newLineNumber: 1 },
],
},
],
};
}
function diff(paths: string[]): WorkspaceCheckpointDiff {
return {
schemaVersion: 'workspace-checkpoint-diff/v1',
workspaceId: input.workspaceId,
taskId: input.taskId,
attemptId: input.attemptId,
fromCheckpoint: {
id: fromCheckpointId,
boundary: 'before-user-turn',
createdAt: '2026-07-26T06:00:00.000Z',
digest: `sha256:${'1'.repeat(64)}`,
},
toCheckpoint: {
id: toCheckpointId,
boundary: 'before-user-turn',
createdAt: '2026-07-26T06:05:00.000Z',
digest: `sha256:${'2'.repeat(64)}`,
},
directParent: true,
git: {
headChanged: false,
branchChanged: false,
indexChanged: true,
statusChanged: true,
},
summary: { filesChanged: paths.length, additions: paths.length, deletions: paths.length },
files: paths.map(changedFile),
};
}
function event(
sequence: number,
kind: string,
source: RunEventSource,
payload: RunEventEnvelope['payload']
): RunEventEnvelope {
return {
schemaVersion: 'run-event/v1',
eventId: `event-${sequence}`,
taskId: input.taskId,
runId: input.attemptId,
attemptId: input.attemptId,
sequence,
receivedAt: `2026-07-26T06:00:0${sequence}.000Z`,
kind,
source,
redaction: { status: 'none', fields: [], originalBytes: 0, persistedBytes: 0 },
payload,
payloadHash: `${sequence}`,
};
}
const systemSource = { provider: 'system', adapter: 'workspace-checkpoint' } as const;
const codexSource = { provider: 'codex-cli', adapter: 'codex-cli', agent: 'CODEX' } as const;
const claudeSource = {
provider: 'claude-code',
adapter: 'claude-code',
agent: 'CLAUDE',
} as const;
const operatorSource = { provider: 'operator', adapter: 'operator-file-editor' } as const;
describe('WorkspaceCheckpointAttributionService', () => {
it('attributes only explicit path-bearing evidence inside checkpoint boundaries', async () => {
const paths = [
'agent.ts',
'operator.ts',
'external.ts',
'write-tool.ts',
'mixed.ts',
'unknown.ts',
];
const events = [
event(1, 'workspace.checkpoint.created', systemSource, {
checkpointId: fromCheckpointId,
}),
event(2, 'file.changed', codexSource, { paths: ['agent.ts'] }),
event(3, 'file.changed', operatorSource, { paths: ['operator.ts'] }),
event(
4,
'file.changed',
{ provider: 'system', adapter: 'filesystem-watcher' },
{
paths: ['external.ts'],
}
),
event(5, 'tool.started', claudeSource, {
tool: 'Write',
paths: ['write-tool.ts'],
}),
event(6, 'file.changed', codexSource, { paths: ['mixed.ts'] }),
event(7, 'file.changed', operatorSource, { paths: ['mixed.ts'] }),
event(8, 'tool.started', claudeSource, { tool: 'Read', paths: ['unknown.ts'] }),
event(9, 'workspace.checkpoint.created', systemSource, { checkpointId: toCheckpointId }),
];
const service = new WorkspaceCheckpointAttributionService({
diffs: { compare: vi.fn(async () => diff(paths)) },
events: {
list: vi.fn(async () => ({
schemaVersion: 'run-event/v1',
taskId: input.taskId,
attemptId: input.attemptId,
events,
nextCursor: 9,
hasMore: false,
})),
},
});
const result = await service.compare(input);
const attribution = Object.fromEntries(
result.files.map((file) => [file.path, file.attribution])
);
expect(result.attribution).toEqual({
evidenceComplete: true,
fromEventSequence: 1,
toEventSequence: 9,
eventsConsidered: 7,
});
expect(attribution['agent.ts']).toMatchObject({
source: 'agent-tool',
confidence: 'high',
basis: 'provider-file-event',
scope: 'checkpoint-file-window',
provider: 'codex-cli',
agent: 'CODEX',
});
expect(attribution['operator.ts']).toMatchObject({
source: 'operator',
basis: 'operator-file-event',
});
expect(attribution['external.ts']).toMatchObject({
source: 'external',
basis: 'filesystem-file-event',
});
expect(attribution['write-tool.ts']).toMatchObject({
source: 'agent-tool',
basis: 'write-tool-event',
tool: 'Write',
});
expect(attribution['mixed.ts']).toEqual({
source: 'unknown',
confidence: 'ambiguous',
basis: 'mixed-file-evidence',
scope: 'checkpoint-file-window',
evidenceEventIds: ['event-6', 'event-7'],
});
expect(attribution['unknown.ts']).toEqual({
source: 'unknown',
confidence: 'none',
basis: 'no-file-evidence',
scope: 'checkpoint-file-window',
evidenceEventIds: [],
});
expect(result.files[0].hunks[0].attribution).toEqual(result.files[0].attribution);
});
it('marks all attribution unknown when the event boundary window is incomplete', async () => {
const service = new WorkspaceCheckpointAttributionService({
diffs: { compare: vi.fn(async () => diff(['agent.ts'])) },
events: {
list: vi.fn(async () => ({
schemaVersion: 'run-event/v1',
taskId: input.taskId,
attemptId: input.attemptId,
events: [
event(1, 'workspace.checkpoint.created', systemSource, {
checkpointId: fromCheckpointId,
}),
event(2, 'workspace.checkpoint.created', codexSource, {
checkpointId: toCheckpointId,
}),
event(3, 'file.changed', codexSource, { paths: ['agent.ts'] }),
],
nextCursor: 3,
hasMore: false,
})),
},
});
const result = await service.compare(input);
expect(result.attribution).toEqual({
evidenceComplete: false,
fromEventSequence: 1,
eventsConsidered: 0,
});
expect(result.files[0].attribution?.source).toBe('unknown');
});
});

View file

@ -0,0 +1,117 @@
const FILE_PATH_KEYS = new Set([
'file',
'file_path',
'filePath',
'filepath',
'notebook_path',
'notebookPath',
'path',
'relative_path',
'relativePath',
]);
export function extractProviderEventPaths(value: unknown): string[] {
const paths = new Set<string>();
const seen = new Set<object>();
const visit = (candidate: unknown, key: string | undefined, depth: number): void => {
if (depth > 8 || paths.size >= 100) return;
if (typeof candidate === 'string') {
if (!key || !FILE_PATH_KEYS.has(key)) return;
const normalized = normalizeWorkspaceEvidencePath(candidate);
if (normalized) paths.add(normalized);
return;
}
if (!candidate || typeof candidate !== 'object' || seen.has(candidate)) return;
seen.add(candidate);
if (Array.isArray(candidate)) {
for (const entry of candidate.slice(0, 100)) visit(entry, key, depth + 1);
return;
}
for (const [childKey, childValue] of Object.entries(candidate as Record<string, unknown>).slice(
0,
256
)) {
visit(childValue, childKey, depth + 1);
if (paths.size >= 100) break;
}
};
visit(value, undefined, 0);
return [...paths].sort((left, right) => left.localeCompare(right));
}
export function extractProviderEventToolName(value: unknown): string | undefined {
const seen = new Set<object>();
const visit = (candidate: unknown, depth: number): string | undefined => {
if (depth > 8 || !candidate || typeof candidate !== 'object' || seen.has(candidate)) {
return undefined;
}
seen.add(candidate);
if (Array.isArray(candidate)) {
for (const entry of candidate.slice(0, 100)) {
const nested = visit(entry, depth + 1);
if (nested) return nested;
}
return undefined;
}
const record = candidate as Record<string, unknown>;
const type = typeof record.type === 'string' ? record.type.toLowerCase() : '';
for (const key of ['tool', 'tool_name', 'toolName', 'name']) {
const name = record[key];
if (
typeof name === 'string' &&
name.length <= 240 &&
!hasControlCharacters(name) &&
(key !== 'name' || type.includes('tool') || type.includes('file'))
) {
return name;
}
}
for (const child of Object.values(record).slice(0, 256)) {
const nested = visit(child, depth + 1);
if (nested) return nested;
}
return undefined;
};
return visit(value, 0);
}
export function isWriteCapableProviderTool(toolName: string | undefined): boolean {
if (!toolName) return false;
const normalized = toolName
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_');
return /(?:^|_)(?:write|edit|multi_edit|notebook_edit|apply_patch|applypatch|patch|replace|create_file|delete_file|rename_file|move_file)(?:_|$)/.test(
normalized
);
}
export function normalizeWorkspaceEvidencePath(value: string): string | undefined {
const trimmed = value.trim().replaceAll('\\', '/');
if (
!trimmed ||
trimmed.length > 2_048 ||
hasControlCharacters(trimmed) ||
trimmed.startsWith('/') ||
/^[A-Za-z]:\//.test(trimmed) ||
/^[a-z][a-z0-9+.-]*:/i.test(trimmed)
) {
return undefined;
}
const segments = trimmed.replace(/^(?:\.\/)+/, '').split('/');
if (
segments.length === 0 ||
segments.some((segment) => !segment || segment === '.' || segment === '..')
) {
return undefined;
}
return segments.join('/');
}
function hasControlCharacters(value: string): boolean {
for (const character of value) {
const code = character.codePointAt(0) ?? 0;
if (code <= 31 || code === 127) return true;
}
return false;
}

View file

@ -1,5 +1,9 @@
import { createHash } from 'node:crypto';
import type { ExecutableAgentProvider, RunEventKind } from '@veritas-kanban/shared';
import {
extractProviderEventPaths,
extractProviderEventToolName,
} from './provider-event-evidence.js';
export interface ProviderMappedRunEvent {
kind: RunEventKind;
@ -51,6 +55,25 @@ function nestedRecord(value: unknown): Record<string, unknown> | undefined {
: undefined;
}
function providerPayload(
providerType: string,
event: Record<string, unknown>,
summary: string | undefined,
kind: RunEventKind
): Record<string, unknown> {
const carriesFileEvidence =
kind === 'file.changed' || kind === 'tool.started' || kind === 'tool.completed';
const paths = carriesFileEvidence ? extractProviderEventPaths(event) : [];
const tool = carriesFileEvidence ? extractProviderEventToolName(event) : undefined;
return {
providerType,
summary,
...(paths.length > 0 ? { paths } : {}),
...(tool ? { tool } : {}),
raw: event,
};
}
function eventIdentity(event: Record<string, unknown>): {
providerEventId?: string;
sessionId?: string;
@ -166,15 +189,12 @@ function codexMapper(
},
mapEvent(providerType, event, summary) {
const identity = eventIdentity(event);
const kind = itemKind(providerType, event);
return {
...identity,
kind: itemKind(providerType, event),
kind,
dedupeKey: providerDedupeKey(provider, providerType, identity.providerEventId),
payload: {
providerType,
summary,
raw: event,
},
payload: providerPayload(providerType, event, summary, kind),
};
},
};
@ -189,11 +209,12 @@ const HERMES_MAPPER: ProviderRunEventMapper = {
},
mapEvent(providerType, event, summary) {
const identity = eventIdentity(event);
const kind = itemKind(providerType, event);
return {
...identity,
kind: itemKind(providerType, event),
kind,
dedupeKey: providerDedupeKey('hermes-cli', providerType, identity.providerEventId),
payload: { providerType, summary, raw: event },
payload: providerPayload(providerType, event, summary, kind),
};
},
};
@ -230,11 +251,12 @@ const CLAUDE_CODE_MAPPER: ProviderRunEventMapper = {
},
mapEvent(providerType, event, summary) {
const identity = eventIdentity(event);
const kind = claudeCodeKind(providerType);
return {
...identity,
kind: claudeCodeKind(providerType),
kind,
dedupeKey: providerDedupeKey('claude-code', providerType, identity.providerEventId),
payload: { providerType, summary, raw: event },
payload: providerPayload(providerType, event, summary, kind),
};
},
};
@ -248,11 +270,12 @@ const OPENCLAW_MAPPER: ProviderRunEventMapper = {
},
mapEvent(providerType, event, summary) {
const identity = eventIdentity(event);
const kind = itemKind(providerType, event);
return {
...identity,
kind: itemKind(providerType, event),
kind,
dedupeKey: providerDedupeKey('openclaw', providerType, identity.providerEventId),
payload: { providerType, summary, raw: event },
payload: providerPayload(providerType, event, summary, kind),
};
},
};
@ -266,11 +289,12 @@ const ACP_MAPPER: ProviderRunEventMapper = {
},
mapEvent(providerType, event, summary) {
const identity = eventIdentity(event);
const kind = itemKind(providerType, event);
return {
...identity,
kind: itemKind(providerType, event),
kind,
dedupeKey: providerDedupeKey('acp-stdio', providerType, identity.providerEventId),
payload: { providerType, summary, raw: event },
payload: providerPayload(providerType, event, summary, kind),
};
},
};

View file

@ -196,6 +196,18 @@ function spillSourceName(payload: Record<string, unknown>): string | undefined {
return candidate ? redactString(candidate).slice(0, 240) : undefined;
}
function spillRoutingEvidence(payload: Record<string, unknown>): Record<string, unknown> {
const evidence: Record<string, unknown> = {};
if (typeof payload.providerType === 'string') evidence.providerType = payload.providerType;
if (typeof payload.tool === 'string') evidence.tool = payload.tool;
if (Array.isArray(payload.paths)) {
evidence.paths = payload.paths
.filter((entry): entry is string => typeof entry === 'string')
.slice(0, 100);
}
return evidence;
}
function deterministicArtifactId(input: RunEventAppendInput, eventId: string): string {
const identity =
input.dedupeKey ??
@ -261,6 +273,7 @@ export class RunEventJournalService {
: undefined;
const eventPayload = spillPreview
? {
...sanitizePayload(spillRoutingEvidence(input.payload)).payload,
spilled: true,
originalPayloadBytes: countBytes(serialized ?? ''),
outputArtifact: spillPreview,

View file

@ -0,0 +1,278 @@
import type {
RunEventEnvelope,
RunEventPage,
RunEventQuery,
WorkspaceCheckpointDiff,
WorkspaceCheckpointFileDiff,
WorkspaceCheckpointHunkAttribution,
} from '@veritas-kanban/shared';
import { ConflictError } from '../middleware/error-handler.js';
import {
extractProviderEventPaths,
extractProviderEventToolName,
isWriteCapableProviderTool,
normalizeWorkspaceEvidencePath,
} from './provider-event-evidence.js';
import { RunEventJournalService } from './run-event-journal-service.js';
import {
WorkspaceCheckpointDiffService,
type WorkspaceCheckpointDiffInput,
} from './workspace-checkpoint-diff-service.js';
const MAX_EVENT_PAGES = 100;
export interface WorkspaceCheckpointEventSource {
list(query: RunEventQuery): Promise<RunEventPage>;
}
export interface WorkspaceCheckpointAttributionServiceOptions {
diffs?: Pick<WorkspaceCheckpointDiffService, 'compare'>;
events?: WorkspaceCheckpointEventSource;
}
interface CheckpointEventWindow {
evidenceComplete: boolean;
fromEventSequence?: number;
toEventSequence?: number;
events: RunEventEnvelope[];
}
interface AttributionEvidence {
source: Exclude<WorkspaceCheckpointHunkAttribution['source'], 'unknown'>;
basis: Exclude<
WorkspaceCheckpointHunkAttribution['basis'],
'mixed-file-evidence' | 'no-file-evidence'
>;
event: RunEventEnvelope;
tool?: string;
}
export class WorkspaceCheckpointAttributionService {
private readonly diffs: Pick<WorkspaceCheckpointDiffService, 'compare'>;
private readonly events: WorkspaceCheckpointEventSource;
constructor(options: WorkspaceCheckpointAttributionServiceOptions = {}) {
this.diffs = options.diffs ?? new WorkspaceCheckpointDiffService();
this.events = options.events ?? new RunEventJournalService();
}
async compare(input: WorkspaceCheckpointDiffInput): Promise<WorkspaceCheckpointDiff> {
const diff = await this.diffs.compare(input);
const window = await this.loadEventWindow(input);
const evidenceByPath = this.indexEvidence(window.events);
const files = diff.files.map((file) =>
this.attributeFile(file, evidenceByPath.get(file.path) ?? [])
);
return {
...diff,
attribution: {
evidenceComplete: window.evidenceComplete,
...(window.fromEventSequence === undefined
? {}
: { fromEventSequence: window.fromEventSequence }),
...(window.toEventSequence === undefined
? {}
: { toEventSequence: window.toEventSequence }),
eventsConsidered: window.events.length,
},
files,
};
}
private async loadEventWindow(
input: WorkspaceCheckpointDiffInput
): Promise<CheckpointEventWindow> {
let cursor = 0;
let fromEventSequence: number | undefined;
let toEventSequence: number | undefined;
const events: RunEventEnvelope[] = [];
for (let pageNumber = 0; pageNumber < MAX_EVENT_PAGES; pageNumber += 1) {
const page = await this.events.list({
taskId: input.taskId,
attemptId: input.attemptId,
afterSequence: cursor,
limit: 500,
});
for (const event of page.events) {
const checkpointId = trustedCheckpointEventId(event);
if (checkpointId === input.fromCheckpointId) {
fromEventSequence = event.sequence;
events.length = 0;
continue;
}
if (checkpointId === input.toCheckpointId) {
if (fromEventSequence === undefined) {
throw new ConflictError('Workspace checkpoint event boundaries are out of order.');
}
toEventSequence = event.sequence;
break;
}
if (fromEventSequence !== undefined) events.push(event);
}
if (toEventSequence !== undefined) break;
if (!page.hasMore) break;
if (page.nextCursor <= cursor || page.events.length === 0) {
throw new ConflictError('Workspace checkpoint event replay did not advance.');
}
cursor = page.nextCursor;
}
if (
fromEventSequence !== undefined &&
toEventSequence !== undefined &&
fromEventSequence >= toEventSequence
) {
throw new ConflictError('Workspace checkpoint event boundaries are out of order.');
}
if (fromEventSequence === undefined || toEventSequence === undefined) {
return {
evidenceComplete: false,
fromEventSequence,
toEventSequence,
events: [],
};
}
return {
evidenceComplete: true,
fromEventSequence,
toEventSequence,
events,
};
}
private attributeFile(
file: WorkspaceCheckpointFileDiff,
evidence: AttributionEvidence[]
): WorkspaceCheckpointFileDiff {
const attribution = summarizeAttribution(evidence);
return {
...file,
attribution,
hunks: file.hunks.map((hunk) => ({ ...hunk, attribution })),
};
}
private indexEvidence(events: RunEventEnvelope[]): Map<string, AttributionEvidence[]> {
const byPath = new Map<string, AttributionEvidence[]>();
for (const event of events) {
const evidence = this.evidenceForEvent(event);
if (!evidence) continue;
for (const filePath of normalizedEventPaths(event)) {
const existing = byPath.get(filePath) ?? [];
existing.push(evidence);
byPath.set(filePath, existing);
}
}
return byPath;
}
private evidenceForEvent(event: RunEventEnvelope): AttributionEvidence | undefined {
if (event.kind === 'file.changed') {
if (event.source.provider === 'operator') {
return { source: 'operator', basis: 'operator-file-event', event };
}
if (event.source.provider === 'system') {
return { source: 'external', basis: 'filesystem-file-event', event };
}
return { source: 'agent-tool', basis: 'provider-file-event', event };
}
if (event.kind === 'tool.started' || event.kind === 'tool.completed') {
const tool = normalizedEventTool(event);
if (!isWriteCapableProviderTool(tool)) return undefined;
if (event.source.provider === 'operator') {
return { source: 'operator', basis: 'operator-file-event', event, tool };
}
if (event.source.provider === 'system') {
return { source: 'external', basis: 'filesystem-file-event', event, tool };
}
return { source: 'agent-tool', basis: 'write-tool-event', event, tool };
}
return undefined;
}
}
function normalizedEventPaths(event: RunEventEnvelope): string[] {
const normalized = event.payload.paths;
if (Array.isArray(normalized)) {
return normalized.flatMap((entry) => {
if (typeof entry !== 'string') return [];
const path = normalizeWorkspaceEvidencePath(entry);
return path ? [path] : [];
});
}
return extractProviderEventPaths(event.payload);
}
function normalizedEventTool(event: RunEventEnvelope): string | undefined {
return typeof event.payload.tool === 'string'
? event.payload.tool
: extractProviderEventToolName(event.payload);
}
function summarizeAttribution(evidence: AttributionEvidence[]): WorkspaceCheckpointHunkAttribution {
if (evidence.length === 0) {
return {
source: 'unknown',
confidence: 'none',
basis: 'no-file-evidence',
scope: 'checkpoint-file-window',
evidenceEventIds: [],
};
}
const sources = new Set(evidence.map((entry) => entry.source));
const eventIds = [...new Set(evidence.map((entry) => entry.event.eventId))].slice(0, 100);
if (sources.size !== 1) {
return {
source: 'unknown',
confidence: 'ambiguous',
basis: 'mixed-file-evidence',
scope: 'checkpoint-file-window',
evidenceEventIds: eventIds,
};
}
const source = evidence[0].source;
const bases = new Set(evidence.map((entry) => entry.basis));
const providers = new Set(evidence.map((entry) => entry.event.source.provider));
const agents = new Set(
evidence
.map((entry) => entry.event.source.agent)
.filter((entry): entry is string => Boolean(entry))
);
const tools = new Set(
evidence.map((entry) => entry.tool).filter((entry): entry is string => Boolean(entry))
);
const provider = [...providers][0];
const agent = [...agents][0];
const tool = [...tools][0];
return {
source,
confidence: 'high',
basis: bases.size === 1 ? evidence[0].basis : sourceBasis(source),
scope: 'checkpoint-file-window',
evidenceEventIds: eventIds,
...(providers.size === 1 && provider ? { provider } : {}),
...(agents.size === 1 && agent ? { agent } : {}),
...(tools.size === 1 && tool ? { tool } : {}),
};
}
function trustedCheckpointEventId(event: RunEventEnvelope): string | undefined {
return event.kind === 'workspace.checkpoint.created' &&
event.source.provider === 'system' &&
event.source.adapter === 'workspace-checkpoint' &&
typeof event.payload.checkpointId === 'string'
? event.payload.checkpointId
: undefined;
}
function sourceBasis(
source: Exclude<WorkspaceCheckpointHunkAttribution['source'], 'unknown'>
): Exclude<
WorkspaceCheckpointHunkAttribution['basis'],
'mixed-file-evidence' | 'no-file-evidence'
> {
if (source === 'operator') return 'operator-file-event';
if (source === 'external') return 'filesystem-file-event';
return 'provider-file-event';
}

View file

@ -92,8 +92,10 @@
"stream.stdout",
"stream.stderr",
"command.started",
"command.detached",
"command.completed",
"file.changed",
"workspace.checkpoint.created",
"tool.started",
"tool.completed",
"approval.requested",

View file

@ -93,6 +93,27 @@ export interface WorkspaceCheckpoint {
export type WorkspaceCheckpointDiffLineKind = 'context' | 'addition' | 'deletion';
export type WorkspaceCheckpointFileChangeKind = 'added' | 'modified' | 'deleted' | 'mode-changed';
export type WorkspaceCheckpointAttributionSource =
'agent-tool' | 'operator' | 'external' | 'unknown';
export type WorkspaceCheckpointAttributionConfidence = 'high' | 'ambiguous' | 'none';
export type WorkspaceCheckpointAttributionBasis =
| 'provider-file-event'
| 'write-tool-event'
| 'operator-file-event'
| 'filesystem-file-event'
| 'mixed-file-evidence'
| 'no-file-evidence';
export interface WorkspaceCheckpointHunkAttribution {
source: WorkspaceCheckpointAttributionSource;
confidence: WorkspaceCheckpointAttributionConfidence;
basis: WorkspaceCheckpointAttributionBasis;
scope: 'checkpoint-file-window';
evidenceEventIds: string[];
provider?: string;
agent?: string;
tool?: string;
}
export interface WorkspaceCheckpointDiffLine {
kind: WorkspaceCheckpointDiffLineKind;
@ -108,6 +129,7 @@ export interface WorkspaceCheckpointDiffHunk {
newStart: number;
newLines: number;
lines: WorkspaceCheckpointDiffLine[];
attribution?: WorkspaceCheckpointHunkAttribution;
}
export interface WorkspaceCheckpointFileDiff {
@ -123,6 +145,7 @@ export interface WorkspaceCheckpointFileDiff {
additions: number;
deletions: number;
hunks: WorkspaceCheckpointDiffHunk[];
attribution?: WorkspaceCheckpointHunkAttribution;
}
export interface WorkspaceCheckpointDiff {
@ -144,5 +167,11 @@ export interface WorkspaceCheckpointDiff {
additions: number;
deletions: number;
};
attribution?: {
evidenceComplete: boolean;
fromEventSequence?: number;
toEventSequence?: number;
eventsConsidered: number;
};
files: WorkspaceCheckpointFileDiff[];
}