Q135: remove the thread replay that only digested events and reconstructed nothing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 21:47:48 -04:00
1 parent 403c57785c
commit f8f852c326
21 files changed
+17 -288

No files matched your search

-124
View File
@@ -1,36 +1,6 @@
import { createHash } from "node:crypto";
import type { DatabaseSync } from "node:sqlite";
import type { CreateAuditReplayInput } from "../../shared/commands";
import { createId } from "../../shared/ids";
interface AuditEventRow {
id: string;
org_id: string;
entity_type: string;
entity_id: string;
actor_type: string;
actor_id: string | null;
action: string;
payload_json: string;
created_at: string;
}
interface ChatThreadRow {
id: string;
root_message_id: string | null;
}
interface ReplayEvent {
id: string;
entityType: string;
entityId: string;
actorType: string;
actorId: string | null;
action: string;
payload: Record<string, unknown>;
createdAt: string;
}
export class AuditService {
constructor(private readonly db: DatabaseSync) {}
@@ -66,98 +36,4 @@ export class AuditService {
const actor = this.db.prepare(`select id from ${table} where id = ? and org_id = ?`).get(actorId, orgId);
if (!actor) throw new Error(`No ${actorType} "${actorId}" in this organization.`);
}
createThreadReplay(input: CreateAuditReplayInput): string {
const thread = this.requireThread(input.orgId, input.threadId);
this.requireEmployee(input.orgId, input.createdByEmployeeId);
const messageIds = this.threadMessageIds(input.orgId, input.threadId, thread.root_message_id);
const events = this.threadEvents(input.orgId, input.threadId, messageIds);
if (events.length === 0) throw new Error("Thread replay requires audit events.");
const replay = {
threadId: input.threadId,
rootMessageId: thread.root_message_id,
messageIds: [...messageIds].sort(),
events,
};
const replayJson = JSON.stringify(replay);
const eventDigest = createHash("sha256").update(replayJson).digest("hex");
const id = createId("arp");
const now = new Date().toISOString();
this.db.prepare(`
insert into audit_replay_runs(
id, org_id, thread_id, event_count, event_digest, replay_json,
created_by_employee_id, created_at
) values (?, ?, ?, ?, ?, ?, ?, ?)
`).run(id, input.orgId, input.threadId, events.length, eventDigest, replayJson, input.createdByEmployeeId, now);
this.record({
orgId: input.orgId,
entityType: "audit_replay_run",
entityId: id,
actorType: "employee",
actorId: input.createdByEmployeeId,
action: "created",
payload: { threadId: input.threadId, eventCount: events.length, eventDigest },
});
return id;
}
private requireThread(orgId: string, threadId: string): ChatThreadRow {
const thread = this.db.prepare("select id, root_message_id from chat_threads where id = ? and org_id = ?")
.get(threadId, orgId) as ChatThreadRow | undefined;
if (!thread) throw new Error("Chat thread was not found.");
return thread;
}
private requireEmployee(orgId: string, employeeId: string): void {
const employee = this.db.prepare("select id from employees where id = ? and org_id = ?").get(employeeId, orgId);
if (!employee) throw new Error("Audit replay employee was not found.");
}
private threadMessageIds(orgId: string, threadId: string, rootMessageId: string | null): Set<string> {
const ids = new Set<string>();
if (rootMessageId) ids.add(rootMessageId);
const rows = this.db.prepare("select id from chat_messages where org_id = ? and thread_id = ? order by created_at asc")
.all(orgId, threadId) as Array<{ id: string }>;
for (const row of rows) ids.add(row.id);
return ids;
}
private threadEvents(orgId: string, threadId: string, messageIds: Set<string>): ReplayEvent[] {
const rows = this.db.prepare("select * from audit_events where org_id = ? order by created_at asc, id asc")
.all(orgId) as unknown as AuditEventRow[];
return rows
.filter((row) => eventMatchesThread(row, threadId, messageIds))
.map((row) => ({
id: row.id,
entityType: row.entity_type,
entityId: row.entity_id,
actorType: row.actor_type,
actorId: row.actor_id,
action: row.action,
payload: parsePayload(row.payload_json),
createdAt: row.created_at,
}));
}
}
function eventMatchesThread(row: AuditEventRow, threadId: string, messageIds: Set<string>): boolean {
if (row.entity_type === "audit_replay_run") return false;
if (row.entity_type === "chat_thread" && row.entity_id === threadId) return true;
if (row.entity_type === "chat_message" && messageIds.has(row.entity_id)) return true;
const payload = parsePayload(row.payload_json);
return stringField(payload, "threadId") === threadId
|| stringField(payload, "sourceThreadId") === threadId
|| messageIds.has(stringField(payload, "messageId"))
|| messageIds.has(stringField(payload, "sourceMessageId"));
}
function parsePayload(payloadJson: string): Record<string, unknown> {
const parsed = JSON.parse(payloadJson) as unknown;
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("Audit event payload is invalid.");
return parsed as Record<string, unknown>;
}
function stringField(payload: Record<string, unknown>, key: string): string {
const value = payload[key];
return typeof value === "string" ? value : "";
}
-2
View File
@@ -200,8 +200,6 @@ export class ComplianceService {
.run(orgId, subjectId);
this.db.prepare("update retention_freezes set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?")
.run(orgId, subjectId);
this.db.prepare("update audit_replay_runs set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?")
.run(orgId, subjectId);
this.db.prepare("update rollback_snapshots set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?")
.run(orgId, subjectId);
this.db.prepare("update migration_reconciliation_items set resolved_by_employee_id = null where org_id = ? and resolved_by_employee_id = ?")
-4
View File
@@ -70,7 +70,6 @@ const LEGACY_TABLES = [
"legal_holds",
"retention_freezes",
"search_documents",
"audit_replay_runs",
"rollback_snapshots",
"migration_cutover_events",
"migration_reconciliation_items",
@@ -239,9 +238,6 @@ const RELATION_CHECKS: RelationCheck[] = [
{ table: "legal_holds", column: "created_by_employee_id", referencedTable: "employees", referencedColumn: "id", reason: "dangling_creator_employee_id" },
{ table: "retention_freezes", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
{ table: "retention_freezes", column: "created_by_employee_id", referencedTable: "employees", referencedColumn: "id", reason: "dangling_creator_employee_id" },
{ table: "audit_replay_runs", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
{ table: "audit_replay_runs", column: "thread_id", referencedTable: "chat_threads", referencedColumn: "id", reason: "dangling_thread_id" },
{ table: "audit_replay_runs", column: "created_by_employee_id", referencedTable: "employees", referencedColumn: "id", reason: "dangling_creator_employee_id" },
{ table: "rollback_snapshots", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" },
{ table: "rollback_snapshots", column: "migration_report_id", referencedTable: "migration_reports", referencedColumn: "id", reason: "dangling_migration_report_id" },
{ table: "rollback_snapshots", column: "created_by_employee_id", referencedTable: "employees", referencedColumn: "id", reason: "dangling_creator_employee_id" },
-1
View File
@@ -156,7 +156,6 @@ export class SnapshotService {
featureFlags,
sloTargets: list(this.db, "slo_targets", "signal_key asc"),
operationalEvents,
auditReplayRuns: list(this.db, "audit_replay_runs", "created_at desc"),
auditEvents: recentList<AppSnapshot["auditEvents"][number]>(this.db, "audit_events", "created_at desc", 200),
documents: list(this.db, "documents", "updated_at desc"),
knowledgeEntries: list(this.db, "knowledge_entries", "updated_at desc"),