feat(FN-1416): merge fusion/fn-1416

This commit is contained in:
gsxdsm
2026-04-11 08:31:32 -07:00
parent 98ad63abcb
commit e48e034754
2 changed files with 259 additions and 0 deletions

153
AGENTS.md
View File

@@ -2417,3 +2417,156 @@ When you add a template:
1. The template data is copied to a new workflow step (templates themselves are immutable) 1. The template data is copied to a new workflow step (templates themselves are immutable)
2. The new step is enabled by default 2. The new step is enabled by default
3. You can edit the step after creation to customize the prompt 3. You can edit the step after creation to customize the prompt
## Run Audit
The run-audit system provides complete traceability for agent runs by recording every mutation performed by the engine across three domains: git operations, database changes, and filesystem writes. Each event is tied to a specific run ID, enabling operators to map one agent execution to concrete changes.
### Data Model
**`RunAuditEvent`** — A persisted audit record:
| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | UUID for the event |
| `timestamp` | `string` | ISO-8601 when the event occurred |
| `runId` | `string` | Heartbeat run ID (or synthetic ID for executor/merger) |
| `agentId` | `string` | Agent that performed the mutation |
| `taskId` | `string?` | Associated task (inferred from target when it looks like FN-*, KB-*) |
| `domain` | `RunAuditDomain` | `"database"` \| `"git"` \| `"filesystem"` |
| `mutationType` | `string` | What changed (e.g., `task:update`, `git:commit`, `file:write`) |
| `target` | `string` | What was affected (task ID, branch name, file path) |
| `metadata` | `Record<string, unknown>?` | Additional context (phase, source, mutation-specific details) |
### Mutation Domains
**Database mutations** — TaskStore operations:
- `task:create`, `task:update`, `task:move`, `task:log-entry`
- `task:comment:add`, `task:steering-comment:add`
- `task:assign`, `task:checkout`, `task:release`, `task:pause`, `task:unpause`
- `task:dependency:add`, `document:write`, `workflow-step:result`
**Git mutations** — Repository operations:
- `worktree:create`, `worktree:remove`, `worktree:reuse`
- `branch:create`, `branch:delete`, `branch:checkout`
- `commit:create`, `commit:amend`, `reset:hard`
- `merge:start`, `merge:resolve`, `stash:push`, `stash:pop`
**Filesystem mutations** — File system operations:
- `file:write`, `file:delete`, `file:capture-modified`
- `attachment:create`, `attachment:delete`
- `prompt:write`, `prompt:update`, `session:write`, `session:delete`
### Run Context
Every active run has an `EngineRunContext` that enables correlation:
```typescript
interface EngineRunContext {
runId: string; // Stable identifier (heartbeat run ID or synthetic)
agentId: string; // Agent performing mutations
taskId?: string; // Task being operated on
phase?: string; // "heartbeat" | "execute" | "merge" | "merge-attempt-N"
source?: string; // "timer" | "on_demand" | "assignment"
}
```
The engine creates synthetic run IDs for executor and merger operations (e.g., `exec-FN-001-1712345678-a1b2`).
### Ordering Semantics
Events are ordered by `timestamp DESC, rowid DESC`. When multiple events share the same millisecond timestamp, the `rowid` (auto-increment) provides a stable tiebreaker. This ensures deterministic ordering across repeated queries.
### API Endpoints
**`GET /api/agents/:id/runs/:runId/audit`** — Fetch audit events for a run
Query parameters:
- `taskId` — Filter by task ID
- `domain` — Filter by domain (`database`, `git`, `filesystem`)
- `startTime` — Start of time range (ISO-8601, inclusive)
- `endTime` — End of time range (ISO-8601, inclusive)
- `limit` — Maximum events (default 100, max 1000)
Response:
```typescript
interface RunAuditResponse {
runId: string;
events: NormalizedRunAuditEvent[];
filters: { taskId?, domain?, startTime?, endTime? };
totalCount: number;
hasMore: boolean;
}
```
**`GET /api/agents/:id/runs/:runId/timeline`** — Correlated timeline with logs
Combines audit events with agent logs into a unified chronological view. Query parameters same as `/audit`, plus:
- `includeLogs` — Include agent logs (default true)
Response:
```typescript
interface RunTimelineResponse {
run: { id, agentId, startedAt, endedAt, status, taskId? };
auditByDomain: { database: [], git: [], filesystem: [] };
counts: { auditEvents: number; logEntries: number };
timeline: TimelineEntry[];
}
```
### Tracing a Run End-to-End
**Step 1: Identify the run** — Get the run ID from:
- Agent detail modal → Runs tab
- Task activity log → `agent:run:started` event
- Heartbeat log entries
**Step 2: Fetch audit events** — Use the audit endpoint:
```
GET /api/agents/agent-001/runs/run-abc123/audit
```
**Step 3: Map mutations to evidence** — Each event type maps to concrete evidence:
| Domain | Mutation | Evidence |
|--------|----------|----------|
| `git` | `worktree:create` | Directory `.worktrees/{task-id}` exists |
| `git` | `commit:create` | `git log --oneline` shows the commit |
| `git` | `merge:resolve` | PR merged in GitHub, commit in repo |
| `database` | `task:update` | `task.json` reflects the changes |
| `database` | `task:log-entry` | Activity log shows the entry |
| `filesystem` | `file:write` | File exists at the target path |
**Step 4: View full context** — For combined audit + logs:
```
GET /api/agents/agent-001/runs/run-abc123/timeline?includeLogs=true
```
### Troubleshooting
**Missing `contextSnapshot.taskId`**: Legacy runs may not have task context in their snapshot. Use the `taskId` query parameter explicitly when querying audit events:
```
GET /api/agents/:id/runs/:runId/audit?taskId=FN-001
```
**Unknown run ID**: Verify the run exists first:
```
GET /api/agents/:id/runs/:runId # Returns 404 if not found
```
**Empty audit results**: Possible causes:
- Run predates run-audit feature (pre-schema-v29)
- No mutations occurred during the run
- Wrong domain filter — try without `domain` parameter
**Timestamps appear out of order**: Check for millisecond-precision collisions. Events within the same millisecond are ordered by `rowid DESC` (most recently inserted first). Re-query with `?limit=10` to see the latest events first.
**Executor/merger runs have synthetic IDs**: Look for patterns like `exec-{taskId}-{timestamp}-{random}` or `merge-{taskId}-{timestamp}`. These correlate to the original heartbeat run via the `runId` field in the agent's run records.
### Backward Compatibility
The auditor no-ops cleanly when:
- No run context exists (manual/non-run operations)
- TaskStore doesn't have `recordRunAuditEvent` method
This ensures legacy code paths are unaffected. Database operations without an explicit `runContext` parameter skip audit recording but still succeed.

View File

@@ -863,3 +863,109 @@ await network.initialize();
### Error handling model ### Error handling model
All managers use defensive async APIs and silent fallback handling for unsupported environments (for example, browser development/test runs without native Capacitor bindings). This keeps startup resilient across web, simulator, and device contexts. All managers use defensive async APIs and silent fallback handling for unsupported environments (for example, browser development/test runs without native Capacitor bindings). This keeps startup resilient across web, simulator, and device contexts.
## Run Audit
The dashboard provides run-audit API clients for tracing agent runs across git, database, and filesystem mutations.
### API Client Functions
**`fetchAgentRunAudit(agentId, runId, filters?, projectId?)`** — Fetch audit events for a run
```typescript
import { fetchAgentRunAudit } from "./api";
const response = await fetchAgentRunAudit("agent-001", "run-abc123", {
domain: "git", // Optional: filter by domain
startTime: "2025-01-01T00:00:00Z", // Optional: time range
limit: 100, // Optional: max events
}, "project-xyz");
```
**`fetchAgentRunTimeline(agentId, runId, options?, projectId?)`** — Fetch correlated timeline with logs
```typescript
import { fetchAgentRunTimeline } from "./api";
const response = await fetchAgentRunTimeline("agent-001", "run-abc123", {
domain: "filesystem", // Optional: filter by domain
includeLogs: true, // Include agent log entries
limit: 50, // Max audit events
}, "project-xyz");
```
### Response Shapes
**`RunAuditResponse`** — From `fetchAgentRunAudit`:
```typescript
interface RunAuditResponse {
runId: string;
events: NormalizedRunAuditEvent[];
filters: { taskId?, domain?, startTime?, endTime? };
totalCount: number;
hasMore: boolean;
}
```
**`RunTimelineResponse`** — From `fetchAgentRunTimeline`:
```typescript
interface RunTimelineResponse {
run: { id, agentId, startedAt, endedAt?, status, taskId? };
auditByDomain: { database: [], git: [], filesystem: [] };
counts: { auditEvents: number; logEntries: number };
timeline: TimelineEntry[];
}
```
### Debugging Recipe: Map Run ID to Mutations
**Problem**: An agent run completed but you need to verify what changed.
1. **Get the run ID** from the Runs tab in the agent detail modal
2. **Fetch audit events** to see all mutations:
```typescript
const audit = await fetchAgentRunAudit(agentId, runId);
console.log(audit.events.map(e => `${e.domain}:${e.mutationType} → ${e.target}`));
```
3. **Check git mutations** for code changes:
```typescript
const timeline = await fetchAgentRunTimeline(agentId, runId, { domain: "git" });
timeline.auditByDomain.git.forEach(e => {
console.log(`${e.mutationType}: ${e.target}`, e.metadata);
});
```
4. **Verify database changes**:
```typescript
const dbEvents = audit.events.filter(e => e.domain === "database");
dbEvents.forEach(e => {
// e.summary contains a human-readable description
console.log(`[${e.timestamp}] ${e.summary}`);
});
```
5. **Trace filesystem changes**:
```typescript
const fsEvents = timeline.auditByDomain.filesystem;
fsEvents.forEach(e => {
if (e.mutationType === "file:write") {
// File was written at e.target
console.log(`Wrote: ${e.target} (${e.metadata?.size} bytes)`);
}
});
```
### TypeScript Types
Import types from `api.ts`:
```typescript
import type {
RunAuditFilters,
RunAuditResponse,
RunTimelineResponse,
NormalizedRunAuditEvent,
TimelineEntry,
} from "./api";
```