feat(FN-1416): merge fusion/fn-1416
This commit is contained in:
153
AGENTS.md
153
AGENTS.md
@@ -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.
|
||||||
|
|||||||
@@ -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";
|
||||||
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user