fix: address PR review feedback for session audit trail
This commit is contained in:
parent
28ce462ef0
commit
1cbf6579c2
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
"@aoagents/ao-core": patch
|
||||
"@aoagents/ao-cli": patch
|
||||
"@aoagents/ao-web": patch
|
||||
---
|
||||
|
||||
Split orchestrator-only detail views from worker detail views, add an auditable history for `ao acknowledge` / `ao report`, and preserve canonical `needs_input` / `stuck` lifecycle states when polling only has weak or unchanged evidence.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# Session UI, Report Audit, and Lifecycle Stability
|
||||
|
||||
## What Changed
|
||||
|
||||
- Split worker session detail from orchestrator detail so orchestrator pages no longer show worker-only PR and lifecycle detail panels.
|
||||
- Added a persistent audit trail for `ao acknowledge` and `ao report`, including actor, command, note, acceptance/rejection, and before/after lifecycle state.
|
||||
- Exposed that audit trail on worker session detail pages in the dashboard.
|
||||
- Fixed lifecycle fallback so `needs_input` and `stuck` do not bounce back to `working` when the poll cycle only has weak or unchanged evidence.
|
||||
- Removed the web app's runtime dependency on Google Fonts during `next build` by using local CSS font variables instead of `next/font/google`.
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type {
|
||||
CanonicalSessionLifecycle,
|
||||
|
|
@ -26,7 +27,7 @@ import type {
|
|||
SessionStatus,
|
||||
} from "./types.js";
|
||||
import { updateCanonicalLifecycle, updateMetadata, readMetadataRaw } from "./metadata.js";
|
||||
import { deriveLegacyStatus, parseCanonicalLifecycle } from "./lifecycle-state.js";
|
||||
import { deriveLegacyStatus } from "./lifecycle-state.js";
|
||||
import { validateStatus } from "./utils/validation.js";
|
||||
|
||||
/**
|
||||
|
|
@ -215,7 +216,16 @@ function buildAuditDir(dataDir: string): string {
|
|||
return join(dataDir, ".agent-report-audit");
|
||||
}
|
||||
|
||||
const VALID_AUDIT_SESSION_ID = /^[a-zA-Z0-9_-]+$/;
|
||||
|
||||
function validateAuditSessionId(sessionId: SessionId): void {
|
||||
if (!VALID_AUDIT_SESSION_ID.test(sessionId)) {
|
||||
throw new Error(`Invalid session ID: ${sessionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
function buildAuditFilePath(dataDir: string, sessionId: SessionId): string {
|
||||
validateAuditSessionId(sessionId);
|
||||
return join(buildAuditDir(dataDir), `${sessionId}.ndjson`);
|
||||
}
|
||||
|
||||
|
|
@ -256,7 +266,23 @@ export function readAgentReportAuditTrail(
|
|||
return [];
|
||||
}
|
||||
|
||||
return readFileSync(auditFilePath, "utf8")
|
||||
return parseAgentReportAuditTrail(readFileSync(auditFilePath, "utf8"));
|
||||
}
|
||||
|
||||
export async function readAgentReportAuditTrailAsync(
|
||||
dataDir: string,
|
||||
sessionId: SessionId,
|
||||
): Promise<AgentReportAuditEntry[]> {
|
||||
const auditFilePath = buildAuditFilePath(dataDir, sessionId);
|
||||
if (!existsSync(auditFilePath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return parseAgentReportAuditTrail(await readFile(auditFilePath, "utf8"));
|
||||
}
|
||||
|
||||
function parseAgentReportAuditTrail(content: string): AgentReportAuditEntry[] {
|
||||
return content
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
|
|
@ -299,40 +325,38 @@ export function applyAgentReport(
|
|||
throw new Error(`Session not found: ${sessionId}`);
|
||||
}
|
||||
|
||||
validateAuditSessionId(sessionId);
|
||||
const now = (input.now ?? new Date()).toISOString();
|
||||
const source = input.source ?? "report";
|
||||
const actor = normalizeActor(input.actor);
|
||||
const trimmedNote = input.note?.trim() || undefined;
|
||||
const currentLifecycle = parseCanonicalLifecycle(raw, {
|
||||
sessionId,
|
||||
status: validateStatus(raw["status"]),
|
||||
});
|
||||
const previousLegacyStatus = validateStatus(raw["status"]);
|
||||
const before = buildAuditSnapshot(currentLifecycle, previousLegacyStatus);
|
||||
const validation = validateAgentReportTransition(currentLifecycle, input.state);
|
||||
if (!validation.ok) {
|
||||
appendAgentReportAuditEntry(dataDir, sessionId, {
|
||||
timestamp: now,
|
||||
actor,
|
||||
source,
|
||||
reportState: input.state,
|
||||
note: trimmedNote,
|
||||
accepted: false,
|
||||
rejectionReason: validation.reason ?? "transition rejected",
|
||||
before,
|
||||
after: before,
|
||||
});
|
||||
throw new Error(validation.reason ?? "transition rejected");
|
||||
}
|
||||
|
||||
let before: AgentReportAuditSnapshot | null = null;
|
||||
let previousState: CanonicalSessionState | null = null;
|
||||
let nextState: CanonicalSessionState | null = null;
|
||||
let legacyStatus: SessionStatus | null = null;
|
||||
let previousLegacyStatus: SessionStatus | null = null;
|
||||
|
||||
const nextLifecycle = updateCanonicalLifecycle(
|
||||
dataDir,
|
||||
sessionId,
|
||||
(current) => {
|
||||
previousLegacyStatus = deriveLegacyStatus(current, validateStatus(raw["status"]));
|
||||
before = buildAuditSnapshot(current, previousLegacyStatus);
|
||||
const validation = validateAgentReportTransition(current, input.state);
|
||||
if (!validation.ok) {
|
||||
appendAgentReportAuditEntry(dataDir, sessionId, {
|
||||
timestamp: now,
|
||||
actor,
|
||||
source,
|
||||
reportState: input.state,
|
||||
note: trimmedNote,
|
||||
accepted: false,
|
||||
rejectionReason: validation.reason ?? "transition rejected",
|
||||
before,
|
||||
after: before,
|
||||
});
|
||||
throw new Error(validation.reason ?? "transition rejected");
|
||||
}
|
||||
const mapped = mapAgentReportToLifecycle(input.state);
|
||||
previousState = current.session.state;
|
||||
nextState = mapped.sessionState;
|
||||
|
|
@ -347,7 +371,7 @@ export function applyAgentReport(
|
|||
},
|
||||
);
|
||||
|
||||
if (!nextLifecycle || !previousState || !nextState || !legacyStatus) {
|
||||
if (!nextLifecycle || !before || !previousState || !nextState || !legacyStatus) {
|
||||
throw new Error(`Failed to apply agent report for session ${sessionId}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export {
|
|||
applyAgentReport,
|
||||
readAgentReport,
|
||||
readAgentReportAuditTrail,
|
||||
readAgentReportAuditTrailAsync,
|
||||
isAgentReportFresh,
|
||||
mapAgentReportToLifecycle,
|
||||
normalizeAgentReportedState,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { type NextRequest } from "next/server";
|
||||
import { getSessionsDir, readAgentReportAuditTrail } from "@aoagents/ao-core";
|
||||
import { getSessionsDir, readAgentReportAuditTrailAsync } from "@aoagents/ao-core";
|
||||
import { getServices, getSCM } from "@/lib/services";
|
||||
import {
|
||||
sessionToDashboard,
|
||||
|
|
@ -25,7 +25,10 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
|
|||
const project = resolveProject(coreSession, config.projects);
|
||||
if (project) {
|
||||
const sessionsDir = getSessionsDir(config.configPath, project.path);
|
||||
dashboardSession.agentReportAudit = readAgentReportAuditTrail(sessionsDir, coreSession.id);
|
||||
dashboardSession.agentReportAudit = await readAgentReportAuditTrailAsync(
|
||||
sessionsDir,
|
||||
coreSession.id,
|
||||
);
|
||||
}
|
||||
|
||||
// Enrich metadata (issue labels, agent summaries, issue titles)
|
||||
|
|
|
|||
|
|
@ -246,9 +246,9 @@ function SessionReportAuditPanel({
|
|||
Agent Report Audit
|
||||
</p>
|
||||
<div className="mt-3 space-y-3">
|
||||
{entries.map((entry) => (
|
||||
{entries.map((entry, index) => (
|
||||
<div
|
||||
key={`${entry.timestamp}-${entry.reportState}-${entry.actor}`}
|
||||
key={`${entry.timestamp}-${entry.reportState}-${entry.actor}-${entry.source}-${String(entry.accepted)}-${index}`}
|
||||
className="rounded-[16px] border border-[var(--color-border-muted)] bg-[var(--color-bg-base)] px-3 py-3"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import {
|
|||
type CanonicalRuntimeState,
|
||||
type CanonicalRuntimeReason,
|
||||
} from "@aoagents/ao-core/types";
|
||||
import type { AgentReportedState } from "@aoagents/ao-core";
|
||||
|
||||
// Re-export for use in client components
|
||||
export { CI_STATUS, TERMINAL_STATUSES, TERMINAL_ACTIVITIES, NON_RESTORABLE_STATUSES };
|
||||
|
|
@ -106,7 +107,7 @@ export interface DashboardAgentReportAuditEntry {
|
|||
timestamp: string;
|
||||
actor: string;
|
||||
source: "acknowledge" | "report";
|
||||
reportState: string;
|
||||
reportState: AgentReportedState;
|
||||
note?: string;
|
||||
accepted: boolean;
|
||||
rejectionReason?: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue