solidity-argus 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agents/argus-prompt.ts +21 -8
- package/src/agents/scribe-prompt.ts +9 -5
- package/src/cli/index.ts +0 -0
- package/src/config/schema.ts +5 -0
- package/src/create-hooks.ts +78 -22
- package/src/features/migration/index.ts +14 -0
- package/src/features/migration/migration-adapter.ts +151 -0
- package/src/features/migration/parity-telemetry.ts +133 -0
- package/src/features/persistent-state/event-sink.ts +171 -0
- package/src/features/persistent-state/index.ts +2 -0
- package/src/features/persistent-state/run-finalizer.ts +175 -0
- package/src/features/persistent-state/run-journal.ts +1 -1
- package/src/hooks/agent-tracker.ts +15 -0
- package/src/hooks/event-hook.ts +93 -1
- package/src/hooks/tool-tracking-hook.ts +263 -33
- package/src/shared/audit-artifact-resolver.ts +74 -0
- package/src/shared/drop-diagnostics.ts +108 -0
- package/src/shared/index.ts +14 -0
- package/src/shared/report-path-resolver.ts +70 -0
- package/src/solodit-lifecycle.ts +86 -7
- package/src/state/adapters.ts +262 -0
- package/src/state/index.ts +15 -0
- package/src/state/projectors.ts +437 -0
- package/src/state/schemas.ts +356 -0
- package/src/tools/report-generator-tool.ts +569 -31
- package/src/tools/solodit-search-tool.ts +11 -24
- package/src/utils/solodit-health.ts +18 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { mkdir, rename } from "node:fs/promises"
|
|
2
|
+
import { dirname, join } from "node:path"
|
|
3
|
+
import type { AuditEvent, AuditEventType } from "../../state/schemas"
|
|
4
|
+
|
|
5
|
+
export type EventSinkErrorCode = "SEQUENCE_CONFLICT" | "INVALID_EVENT" | "IO_ERROR"
|
|
6
|
+
|
|
7
|
+
export class EventSinkError extends Error {
|
|
8
|
+
readonly code: EventSinkErrorCode
|
|
9
|
+
|
|
10
|
+
constructor(code: EventSinkErrorCode, message: string) {
|
|
11
|
+
super(message)
|
|
12
|
+
this.name = "EventSinkError"
|
|
13
|
+
this.code = code
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface EventSink {
|
|
18
|
+
append(event: AuditEvent): Promise<void>
|
|
19
|
+
readAll(): Promise<AuditEvent[]>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const VALID_EVENT_TYPES: ReadonlySet<string> = new Set<AuditEventType>([
|
|
23
|
+
"session.created",
|
|
24
|
+
"session.idle",
|
|
25
|
+
"session.deleted",
|
|
26
|
+
"tool.started",
|
|
27
|
+
"tool.completed",
|
|
28
|
+
"finding.added",
|
|
29
|
+
"phase.changed",
|
|
30
|
+
"run.finalized",
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
function createMutex() {
|
|
34
|
+
let chain = Promise.resolve()
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
async run<T>(fn: () => Promise<T>): Promise<T> {
|
|
38
|
+
const prev = chain
|
|
39
|
+
let release!: () => void
|
|
40
|
+
chain = new Promise<void>((r) => {
|
|
41
|
+
release = r
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
await prev
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
return await fn()
|
|
48
|
+
} finally {
|
|
49
|
+
release()
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildJournalPath(runId: string, projectDir: string): string {
|
|
56
|
+
return join(projectDir, ".opencode", "runs", runId, "events.jsonl")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function readRawContent(path: string): Promise<string> {
|
|
60
|
+
const file = Bun.file(path)
|
|
61
|
+
if (!(await file.exists())) {
|
|
62
|
+
return ""
|
|
63
|
+
}
|
|
64
|
+
return file.text()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseJournalLines(content: string): AuditEvent[] {
|
|
68
|
+
if (!content.trim()) return []
|
|
69
|
+
|
|
70
|
+
const lines = content.split("\n").filter(Boolean)
|
|
71
|
+
const events: AuditEvent[] = []
|
|
72
|
+
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
try {
|
|
75
|
+
events.push(JSON.parse(line) as AuditEvent)
|
|
76
|
+
} catch {
|
|
77
|
+
/* skip malformed lines */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
events.sort((a, b) => a.seq - b.seq)
|
|
82
|
+
return events
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Replay-safe stateless read — returns all events for a run sorted by seq.
|
|
87
|
+
*/
|
|
88
|
+
export async function readEvents(runId: string, projectDir: string): Promise<AuditEvent[]> {
|
|
89
|
+
const journalPath = buildJournalPath(runId, projectDir)
|
|
90
|
+
const content = await readRawContent(journalPath)
|
|
91
|
+
return parseJournalLines(content)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Append-only event sink with monotonic seq allocation, in-process mutex,
|
|
96
|
+
* and atomic temp-file-then-rename writes. Restart-safe via journal replay.
|
|
97
|
+
*/
|
|
98
|
+
export function createEventSink(runId: string, projectDir: string): EventSink {
|
|
99
|
+
const journalPath = buildJournalPath(runId, projectDir)
|
|
100
|
+
const mutex = createMutex()
|
|
101
|
+
let lastSeq = 0
|
|
102
|
+
let initialized = false
|
|
103
|
+
|
|
104
|
+
async function ensureInitialized(): Promise<void> {
|
|
105
|
+
if (initialized) return
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const content = await readRawContent(journalPath)
|
|
109
|
+
const events = parseJournalLines(content)
|
|
110
|
+
const lastEvent = events.at(-1)
|
|
111
|
+
if (lastEvent) {
|
|
112
|
+
lastSeq = lastEvent.seq
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
throw new EventSinkError("IO_ERROR", `Failed to initialize event sink: ${String(err)}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
initialized = true
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
async append(event: AuditEvent): Promise<void> {
|
|
123
|
+
return mutex.run(async () => {
|
|
124
|
+
await ensureInitialized()
|
|
125
|
+
|
|
126
|
+
if (event.run_id !== runId) {
|
|
127
|
+
throw new EventSinkError(
|
|
128
|
+
"INVALID_EVENT",
|
|
129
|
+
`Event run_id "${event.run_id}" does not match sink run_id "${runId}"`,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!event.type || !VALID_EVENT_TYPES.has(event.type)) {
|
|
134
|
+
throw new EventSinkError("INVALID_EVENT", `Invalid event type "${String(event.type)}"`)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (event.seq > 0 && event.seq <= lastSeq) {
|
|
138
|
+
throw new EventSinkError(
|
|
139
|
+
"SEQUENCE_CONFLICT",
|
|
140
|
+
`Event seq ${event.seq} conflicts with last assigned seq ${lastSeq}; must be > ${lastSeq}`,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const nextSeq = lastSeq + 1
|
|
145
|
+
const eventToWrite: AuditEvent = { ...event, seq: nextSeq }
|
|
146
|
+
|
|
147
|
+
const currentContent = await readRawContent(journalPath)
|
|
148
|
+
const newContent = `${currentContent}${JSON.stringify(eventToWrite)}\n`
|
|
149
|
+
|
|
150
|
+
await mkdir(dirname(journalPath), { recursive: true })
|
|
151
|
+
|
|
152
|
+
const suffix = `${Date.now()}.${Math.random().toString(36).slice(2)}`
|
|
153
|
+
const tempPath = `${journalPath}.${suffix}.tmp`
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await Bun.write(tempPath, newContent)
|
|
157
|
+
await rename(tempPath, journalPath)
|
|
158
|
+
} catch (err) {
|
|
159
|
+
throw new EventSinkError("IO_ERROR", `Failed to write event to journal: ${String(err)}`)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
lastSeq = nextSeq
|
|
163
|
+
})
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
async readAll(): Promise<AuditEvent[]> {
|
|
167
|
+
const content = await readRawContent(journalPath)
|
|
168
|
+
return parseJournalLines(content)
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { validateEventSequence } from "../../state/projectors"
|
|
2
|
+
import type { AuditEvent } from "../../state/schemas"
|
|
3
|
+
import { SCHEMA_VERSION } from "../../state/schemas"
|
|
4
|
+
import type { EventSink } from "./event-sink"
|
|
5
|
+
import { readEvents } from "./event-sink"
|
|
6
|
+
|
|
7
|
+
export type FinalizationResult = {
|
|
8
|
+
success: boolean
|
|
9
|
+
invariantsPassed: boolean
|
|
10
|
+
errors: string[]
|
|
11
|
+
runId: string
|
|
12
|
+
timestamp: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function hasSessionCreated(events: AuditEvent[]): boolean {
|
|
16
|
+
return events.some((event) => event.type === "session.created")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hasSessionDeleted(events: AuditEvent[]): boolean {
|
|
20
|
+
return events.some((event) => event.type === "session.deleted")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function collectOrphanedToolStarts(events: AuditEvent[]): string[] {
|
|
24
|
+
const startedCallIds = new Set<string>()
|
|
25
|
+
const completedCallIds = new Set<string>()
|
|
26
|
+
const errors: string[] = []
|
|
27
|
+
|
|
28
|
+
for (const event of events) {
|
|
29
|
+
if (event.type !== "tool.started" && event.type !== "tool.completed") {
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof event.tool_call_id !== "string" || event.tool_call_id.length === 0) {
|
|
34
|
+
errors.push(`${event.type} at seq ${event.seq} missing tool_call_id`)
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (event.type === "tool.started") {
|
|
39
|
+
startedCallIds.add(event.tool_call_id)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (event.type === "tool.completed") {
|
|
43
|
+
completedCallIds.add(event.tool_call_id)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const toolCallId of startedCallIds) {
|
|
48
|
+
if (!completedCallIds.has(toolCallId)) {
|
|
49
|
+
errors.push(`orphaned tool.started without matching tool.completed: ${toolCallId}`)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return errors
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
57
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
58
|
+
return value as Record<string, unknown>
|
|
59
|
+
}
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function collectParentChildIntegrityErrors(events: AuditEvent[]): string[] {
|
|
64
|
+
const errors: string[] = []
|
|
65
|
+
const parentByChild = new Map<string, string>()
|
|
66
|
+
const correlationByChild = new Map<string, string>()
|
|
67
|
+
|
|
68
|
+
for (const event of events) {
|
|
69
|
+
const payload = asRecord(event.payload)
|
|
70
|
+
if (!payload) {
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const childSessionId = payload.child_session_id
|
|
75
|
+
if (typeof childSessionId !== "string" || childSessionId.length === 0) {
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const parentSessionId = event.session_id
|
|
80
|
+
if (!parentSessionId) {
|
|
81
|
+
errors.push(`child session edge at seq ${event.seq} missing parent session_id`)
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (parentSessionId === childSessionId) {
|
|
86
|
+
errors.push(`child session edge at seq ${event.seq} is self-referential`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const correlationId = payload.correlation_id
|
|
90
|
+
if (typeof correlationId !== "string" || correlationId.length === 0) {
|
|
91
|
+
errors.push(`child session edge at seq ${event.seq} missing correlation_id`)
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const existingParent = parentByChild.get(childSessionId)
|
|
96
|
+
if (existingParent && existingParent !== parentSessionId) {
|
|
97
|
+
errors.push(
|
|
98
|
+
`child session ${childSessionId} mapped to multiple parents: ${existingParent}, ${parentSessionId}`,
|
|
99
|
+
)
|
|
100
|
+
} else {
|
|
101
|
+
parentByChild.set(childSessionId, parentSessionId)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const existingCorrelation = correlationByChild.get(childSessionId)
|
|
105
|
+
if (existingCorrelation && existingCorrelation !== correlationId) {
|
|
106
|
+
errors.push(
|
|
107
|
+
`child session ${childSessionId} has inconsistent correlation_id: ${existingCorrelation}, ${correlationId}`,
|
|
108
|
+
)
|
|
109
|
+
} else {
|
|
110
|
+
correlationByChild.set(childSessionId, correlationId)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return errors
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function collectInvariantErrors(events: AuditEvent[]): string[] {
|
|
118
|
+
const errors: string[] = []
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
validateEventSequence(events)
|
|
122
|
+
} catch (error) {
|
|
123
|
+
errors.push(`invalid event sequence: ${error instanceof Error ? error.message : String(error)}`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!hasSessionCreated(events)) {
|
|
127
|
+
errors.push("missing required lifecycle event: session.created")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!hasSessionDeleted(events)) {
|
|
131
|
+
errors.push("missing required lifecycle event: session.deleted")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
errors.push(...collectOrphanedToolStarts(events))
|
|
135
|
+
errors.push(...collectParentChildIntegrityErrors(events))
|
|
136
|
+
return errors
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function finalizeRun(
|
|
140
|
+
runId: string,
|
|
141
|
+
projectDir: string,
|
|
142
|
+
sink: EventSink | null,
|
|
143
|
+
): Promise<FinalizationResult> {
|
|
144
|
+
const timestamp = Date.now()
|
|
145
|
+
const events = sink ? await sink.readAll() : await readEvents(runId, projectDir)
|
|
146
|
+
const errors = collectInvariantErrors(events)
|
|
147
|
+
const invariantsPassed = errors.length === 0
|
|
148
|
+
const sessionId = events.at(-1)?.session_id ?? ""
|
|
149
|
+
|
|
150
|
+
if (sink) {
|
|
151
|
+
await sink.append({
|
|
152
|
+
type: "run.finalized",
|
|
153
|
+
run_id: runId,
|
|
154
|
+
seq: 0,
|
|
155
|
+
session_id: sessionId,
|
|
156
|
+
source: "run-finalizer",
|
|
157
|
+
schema_version: SCHEMA_VERSION,
|
|
158
|
+
timestamp,
|
|
159
|
+
payload: {
|
|
160
|
+
finalized: invariantsPassed,
|
|
161
|
+
invariantsPassed,
|
|
162
|
+
errors,
|
|
163
|
+
status: invariantsPassed ? "finalized" : "failed-finalization",
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
success: invariantsPassed,
|
|
170
|
+
invariantsPassed,
|
|
171
|
+
errors,
|
|
172
|
+
runId,
|
|
173
|
+
timestamp,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -15,7 +15,7 @@ export type JournalEvent =
|
|
|
15
15
|
findingsCount: number
|
|
16
16
|
toolsExecutedCount: number
|
|
17
17
|
}
|
|
18
|
-
| { type: "session.deleted"; timestamp: number; archived: boolean }
|
|
18
|
+
| { type: "session.deleted"; timestamp: number; archived: boolean; finalizationPassed: boolean | null }
|
|
19
19
|
| {
|
|
20
20
|
type: "tool.executed"
|
|
21
21
|
tool: string
|
|
@@ -11,6 +11,7 @@ export type AgentTracker = ReturnType<typeof createAgentTracker>
|
|
|
11
11
|
|
|
12
12
|
export function createAgentTracker() {
|
|
13
13
|
const sessions = new Map<string, string>()
|
|
14
|
+
const childSessions = new Map<string, Set<string>>()
|
|
14
15
|
|
|
15
16
|
const trackSession = (sessionID: string, agent?: string): void => {
|
|
16
17
|
if (!agent) {
|
|
@@ -49,5 +50,19 @@ export function createAgentTracker() {
|
|
|
49
50
|
getTrackedSessions: (): Map<string, string> => {
|
|
50
51
|
return sessions
|
|
51
52
|
},
|
|
53
|
+
|
|
54
|
+
trackChildSession: (parentSessionId: string, childSessionId: string): void => {
|
|
55
|
+
let children = childSessions.get(parentSessionId)
|
|
56
|
+
if (!children) {
|
|
57
|
+
children = new Set()
|
|
58
|
+
childSessions.set(parentSessionId, children)
|
|
59
|
+
}
|
|
60
|
+
children.add(childSessionId)
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
getChildSessions: (parentSessionId: string): string[] => {
|
|
64
|
+
const children = childSessions.get(parentSessionId)
|
|
65
|
+
return children ? Array.from(children) : []
|
|
66
|
+
},
|
|
52
67
|
}
|
|
53
68
|
}
|
package/src/hooks/event-hook.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import type { EventSink } from "../features/persistent-state/event-sink"
|
|
2
|
+
import { finalizeRun } from "../features/persistent-state/run-finalizer"
|
|
3
|
+
import type { FinalizationResult } from "../features/persistent-state/run-finalizer"
|
|
1
4
|
import { createLogger } from "../shared/logger"
|
|
2
5
|
import { createAuditState } from "../state/audit-state"
|
|
6
|
+
import type { AuditEvent } from "../state/schemas"
|
|
7
|
+
import { SCHEMA_VERSION } from "../state/schemas"
|
|
3
8
|
import type { AuditState } from "../state/types"
|
|
4
9
|
|
|
5
10
|
export type AuditEventType =
|
|
@@ -29,17 +34,50 @@ export function createEventHook(
|
|
|
29
34
|
hook: EventHookFn
|
|
30
35
|
getAuditState: () => AuditState | null
|
|
31
36
|
setAuditState: (state: AuditState | null) => void
|
|
37
|
+
setEventSink: (sink: EventSink | null) => void
|
|
38
|
+
getLastFinalizationResult: () => FinalizationResult | null
|
|
32
39
|
} {
|
|
33
40
|
const logger = createLogger()
|
|
34
41
|
let currentAuditState: AuditState | null = null
|
|
42
|
+
let eventSink: EventSink | null = null
|
|
43
|
+
let lastFinalizationResult: FinalizationResult | null = null
|
|
35
44
|
|
|
36
45
|
const getAuditState = (): AuditState | null => currentAuditState
|
|
37
46
|
const setAuditState = (state: AuditState | null): void => {
|
|
38
47
|
currentAuditState = state
|
|
39
48
|
}
|
|
49
|
+
const setEventSink = (sink: EventSink | null): void => {
|
|
50
|
+
eventSink = sink
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function emitToSink(
|
|
54
|
+
type: AuditEvent["type"],
|
|
55
|
+
runId: string,
|
|
56
|
+
sessionId: string | undefined,
|
|
57
|
+
payload: unknown,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
if (!eventSink) return
|
|
60
|
+
try {
|
|
61
|
+
await eventSink.append({
|
|
62
|
+
type,
|
|
63
|
+
run_id: runId,
|
|
64
|
+
seq: 0, // auto-assigned by sink
|
|
65
|
+
session_id: sessionId ?? "",
|
|
66
|
+
source: "event-hook",
|
|
67
|
+
schema_version: SCHEMA_VERSION,
|
|
68
|
+
timestamp: Date.now(),
|
|
69
|
+
payload,
|
|
70
|
+
})
|
|
71
|
+
} catch (error) {
|
|
72
|
+
logger.error(
|
|
73
|
+
`Failed to emit ${type} event to sink: ${error instanceof Error ? error.message : String(error)}`,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
40
77
|
|
|
41
78
|
const hook: EventHookFn = async (input): Promise<void> => {
|
|
42
79
|
const { type, sessionId } = input.event
|
|
80
|
+
let preDeleteState: AuditState | null = null
|
|
43
81
|
|
|
44
82
|
switch (type) {
|
|
45
83
|
case "session.created": {
|
|
@@ -73,6 +111,7 @@ export function createEventHook(
|
|
|
73
111
|
}
|
|
74
112
|
|
|
75
113
|
case "session.deleted": {
|
|
114
|
+
preDeleteState = currentAuditState
|
|
76
115
|
currentAuditState = null
|
|
77
116
|
break
|
|
78
117
|
}
|
|
@@ -93,7 +132,60 @@ export function createEventHook(
|
|
|
93
132
|
logger.error(`Sub-handler failed for event ${type}:`, error)
|
|
94
133
|
}
|
|
95
134
|
}
|
|
135
|
+
|
|
136
|
+
// Emit canonical events to sink (after sub-handlers, so sink may have been set during session.created)
|
|
137
|
+
switch (type) {
|
|
138
|
+
case "session.created": {
|
|
139
|
+
if (currentAuditState) {
|
|
140
|
+
await emitToSink("session.created", currentAuditState.sessionId, sessionId, {
|
|
141
|
+
projectDir: currentAuditState.projectDir,
|
|
142
|
+
sessionId: currentAuditState.sessionId,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
break
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
case "session.idle": {
|
|
149
|
+
if (currentAuditState) {
|
|
150
|
+
await emitToSink("session.idle", currentAuditState.sessionId, sessionId, {
|
|
151
|
+
findingsCount: currentAuditState.findings.length,
|
|
152
|
+
toolsExecutedCount: currentAuditState.toolsExecuted.length,
|
|
153
|
+
phase: currentAuditState.currentPhase,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case "session.deleted": {
|
|
160
|
+
if (preDeleteState) {
|
|
161
|
+
await emitToSink("session.deleted", preDeleteState.sessionId, sessionId, {
|
|
162
|
+
archived: true,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
if (eventSink) {
|
|
166
|
+
try {
|
|
167
|
+
lastFinalizationResult = await finalizeRun(
|
|
168
|
+
preDeleteState.sessionId,
|
|
169
|
+
preDeleteState.projectDir,
|
|
170
|
+
eventSink,
|
|
171
|
+
)
|
|
172
|
+
} catch (error) {
|
|
173
|
+
logger.error(
|
|
174
|
+
`Failed to finalize run ${preDeleteState.sessionId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
eventSink = null
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
default:
|
|
184
|
+
break
|
|
185
|
+
}
|
|
96
186
|
}
|
|
97
187
|
|
|
98
|
-
|
|
188
|
+
const getLastFinalizationResult = (): FinalizationResult | null => lastFinalizationResult
|
|
189
|
+
|
|
190
|
+
return { hook, getAuditState, setAuditState, setEventSink, getLastFinalizationResult }
|
|
99
191
|
}
|