@kodrunhq/opencode-autopilot 1.15.2 → 1.17.0
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/bin/cli.ts +5 -0
- package/bin/inspect.ts +337 -0
- package/package.json +1 -1
- package/src/agents/autopilot.ts +7 -15
- package/src/config/index.ts +29 -0
- package/src/config/migrations.ts +196 -0
- package/src/config/v7.ts +45 -0
- package/src/config.ts +3 -3
- package/src/health/checks.ts +126 -4
- package/src/health/types.ts +1 -1
- package/src/index.ts +128 -13
- package/src/inspect/formatters.ts +225 -0
- package/src/inspect/repository.ts +882 -0
- package/src/kernel/database.ts +45 -0
- package/src/kernel/migrations.ts +62 -0
- package/src/kernel/repository.ts +571 -0
- package/src/kernel/schema.ts +122 -0
- package/src/kernel/transaction.ts +48 -0
- package/src/kernel/types.ts +65 -0
- package/src/logging/domains.ts +39 -0
- package/src/logging/forensic-writer.ts +177 -0
- package/src/logging/index.ts +4 -0
- package/src/logging/logger.ts +44 -0
- package/src/logging/performance.ts +59 -0
- package/src/logging/rotation.ts +261 -0
- package/src/logging/types.ts +33 -0
- package/src/memory/capture-utils.ts +149 -0
- package/src/memory/capture.ts +82 -67
- package/src/memory/database.ts +74 -12
- package/src/memory/decay.ts +11 -2
- package/src/memory/index.ts +17 -1
- package/src/memory/injector.ts +4 -1
- package/src/memory/lessons.ts +85 -0
- package/src/memory/observations.ts +177 -0
- package/src/memory/preferences.ts +718 -0
- package/src/memory/project-key.ts +6 -0
- package/src/memory/projects.ts +83 -0
- package/src/memory/repository.ts +52 -216
- package/src/memory/retrieval.ts +88 -170
- package/src/memory/schemas.ts +39 -7
- package/src/memory/types.ts +4 -0
- package/src/observability/context-display.ts +8 -0
- package/src/observability/event-handlers.ts +69 -20
- package/src/observability/event-store.ts +29 -1
- package/src/observability/forensic-log.ts +167 -0
- package/src/observability/forensic-schemas.ts +77 -0
- package/src/observability/forensic-types.ts +10 -0
- package/src/observability/index.ts +21 -27
- package/src/observability/log-reader.ts +161 -111
- package/src/observability/log-writer.ts +41 -83
- package/src/observability/retention.ts +2 -2
- package/src/observability/session-logger.ts +36 -57
- package/src/observability/summary-generator.ts +31 -19
- package/src/observability/types.ts +12 -24
- package/src/orchestrator/contracts/invariants.ts +14 -0
- package/src/orchestrator/contracts/legacy-result-adapter.ts +8 -20
- package/src/orchestrator/error-context.ts +24 -0
- package/src/orchestrator/fallback/event-handler.ts +47 -3
- package/src/orchestrator/handlers/architect.ts +2 -1
- package/src/orchestrator/handlers/build-utils.ts +118 -0
- package/src/orchestrator/handlers/build.ts +42 -219
- package/src/orchestrator/handlers/retrospective.ts +2 -2
- package/src/orchestrator/handlers/types.ts +0 -1
- package/src/orchestrator/lesson-memory.ts +36 -11
- package/src/orchestrator/orchestration-logger.ts +53 -24
- package/src/orchestrator/phase.ts +8 -4
- package/src/orchestrator/progress.ts +63 -0
- package/src/orchestrator/state.ts +79 -17
- package/src/projects/database.ts +47 -0
- package/src/projects/repository.ts +264 -0
- package/src/projects/resolve.ts +301 -0
- package/src/projects/schemas.ts +30 -0
- package/src/projects/types.ts +12 -0
- package/src/review/memory.ts +39 -11
- package/src/review/parse-findings.ts +116 -0
- package/src/review/pipeline.ts +3 -107
- package/src/review/selection.ts +38 -4
- package/src/scoring/time-provider.ts +23 -0
- package/src/tools/doctor.ts +28 -4
- package/src/tools/forensics.ts +7 -12
- package/src/tools/logs.ts +38 -11
- package/src/tools/memory-preferences.ts +157 -0
- package/src/tools/memory-status.ts +17 -96
- package/src/tools/orchestrate.ts +108 -90
- package/src/tools/pipeline-report.ts +3 -2
- package/src/tools/quick.ts +2 -2
- package/src/tools/replay.ts +42 -0
- package/src/tools/review.ts +46 -7
- package/src/tools/session-stats.ts +3 -2
- package/src/tools/summary.ts +43 -0
- package/src/utils/paths.ts +20 -1
- package/src/utils/random.ts +33 -0
- package/src/ux/session-summary.ts +56 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { appendForensicEventsToKernel, loadForensicEventsFromKernel } from "../kernel/repository";
|
|
5
|
+
import { isEnoentError } from "../utils/fs-helpers";
|
|
6
|
+
import { getProjectArtifactDir } from "../utils/paths";
|
|
7
|
+
import { forensicEventSchema } from "./forensic-schemas";
|
|
8
|
+
import type { ForensicEvent, ForensicEventDomain, ForensicEventType } from "./forensic-types";
|
|
9
|
+
|
|
10
|
+
const FORENSIC_LOG_FILE = "orchestration.jsonl";
|
|
11
|
+
let forensicWriteWarned = false;
|
|
12
|
+
let forensicMirrorWarned = false;
|
|
13
|
+
|
|
14
|
+
interface ForensicEventInput {
|
|
15
|
+
readonly timestamp?: string;
|
|
16
|
+
readonly projectRoot: string;
|
|
17
|
+
readonly domain: ForensicEventDomain;
|
|
18
|
+
readonly runId?: string | null;
|
|
19
|
+
readonly sessionId?: string | null;
|
|
20
|
+
readonly parentSessionId?: string | null;
|
|
21
|
+
readonly phase?: string | null;
|
|
22
|
+
readonly dispatchId?: string | null;
|
|
23
|
+
readonly taskId?: number | null;
|
|
24
|
+
readonly agent?: string | null;
|
|
25
|
+
readonly type: ForensicEventType;
|
|
26
|
+
readonly code?: string | null;
|
|
27
|
+
readonly message?: string | null;
|
|
28
|
+
readonly payload?: Record<string, string | number | boolean | null | readonly unknown[] | object>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function redactMessage(message: string | null | undefined): string | null {
|
|
32
|
+
if (message == null) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return message.replace(/[/\\][^\s"']+/g, "[PATH]");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toProjectRootFromArtifactDir(artifactDir: string): string {
|
|
39
|
+
return basename(artifactDir) === ".opencode-autopilot" ? dirname(artifactDir) : artifactDir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function appendValidatedForensicEvent(artifactDir: string, event: ForensicEvent): void {
|
|
43
|
+
mkdirSync(artifactDir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
appendForensicEventsToKernel(artifactDir, [event]);
|
|
47
|
+
} catch (kernelError) {
|
|
48
|
+
if (!forensicWriteWarned) {
|
|
49
|
+
forensicWriteWarned = true;
|
|
50
|
+
console.warn("[opencode-autopilot] forensic log write failed:", kernelError);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const logPath = join(artifactDir, FORENSIC_LOG_FILE);
|
|
56
|
+
appendFileSync(logPath, `${JSON.stringify(event)}\n`, "utf-8");
|
|
57
|
+
} catch (mirrorError) {
|
|
58
|
+
if (!forensicMirrorWarned) {
|
|
59
|
+
forensicMirrorWarned = true;
|
|
60
|
+
console.warn("[opencode-autopilot] forensic JSONL mirror write failed:", mirrorError);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getForensicLogPath(projectRoot: string): string {
|
|
66
|
+
return join(getProjectArtifactDir(projectRoot), FORENSIC_LOG_FILE);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createForensicEvent(input: ForensicEventInput): ForensicEvent {
|
|
70
|
+
return forensicEventSchema.parse({
|
|
71
|
+
schemaVersion: 1,
|
|
72
|
+
timestamp: input.timestamp ?? new Date().toISOString(),
|
|
73
|
+
projectRoot: input.projectRoot,
|
|
74
|
+
domain: input.domain,
|
|
75
|
+
runId: input.runId ?? null,
|
|
76
|
+
sessionId: input.sessionId ?? null,
|
|
77
|
+
parentSessionId: input.parentSessionId ?? null,
|
|
78
|
+
phase: input.phase ?? null,
|
|
79
|
+
dispatchId: input.dispatchId ?? null,
|
|
80
|
+
taskId: input.taskId ?? null,
|
|
81
|
+
agent: input.agent ?? null,
|
|
82
|
+
type: input.type,
|
|
83
|
+
code: input.code ?? null,
|
|
84
|
+
message: redactMessage(input.message),
|
|
85
|
+
payload: input.payload ?? {},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function appendForensicEvent(projectRoot: string, event: ForensicEventInput): void {
|
|
90
|
+
try {
|
|
91
|
+
const validated = createForensicEvent(event);
|
|
92
|
+
const artifactDir = getProjectArtifactDir(projectRoot);
|
|
93
|
+
appendValidatedForensicEvent(artifactDir, validated);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (!forensicWriteWarned) {
|
|
96
|
+
forensicWriteWarned = true;
|
|
97
|
+
console.warn("[opencode-autopilot] forensic log write failed:", error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function appendForensicEventForArtifactDir(
|
|
103
|
+
artifactDir: string,
|
|
104
|
+
event: Omit<ForensicEventInput, "projectRoot">,
|
|
105
|
+
): void {
|
|
106
|
+
try {
|
|
107
|
+
const validated = createForensicEvent({
|
|
108
|
+
...event,
|
|
109
|
+
projectRoot: toProjectRootFromArtifactDir(artifactDir),
|
|
110
|
+
});
|
|
111
|
+
appendValidatedForensicEvent(artifactDir, validated);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (!forensicWriteWarned) {
|
|
114
|
+
forensicWriteWarned = true;
|
|
115
|
+
console.warn("[opencode-autopilot] forensic log write failed:", error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function readForensicEvents(projectRoot: string): Promise<readonly ForensicEvent[]> {
|
|
121
|
+
const kernelEvents = loadForensicEventsFromKernel(getProjectArtifactDir(projectRoot));
|
|
122
|
+
if (kernelEvents.length > 0) {
|
|
123
|
+
return kernelEvents;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const raw = await readFile(getForensicLogPath(projectRoot), "utf-8");
|
|
128
|
+
const events: ForensicEvent[] = [];
|
|
129
|
+
for (const line of raw.split("\n")) {
|
|
130
|
+
const trimmed = line.trim();
|
|
131
|
+
if (!trimmed) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
events.push(forensicEventSchema.parse(JSON.parse(trimmed)));
|
|
136
|
+
} catch {
|
|
137
|
+
// Ignore malformed forensic lines so one bad append does not hide the rest.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return Object.freeze(events);
|
|
141
|
+
} catch (error: unknown) {
|
|
142
|
+
if (isEnoentError(error)) {
|
|
143
|
+
return Object.freeze([]);
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function listProjectsWithForensicLogs(logsRoot?: string): Promise<readonly string[]> {
|
|
150
|
+
const root = logsRoot;
|
|
151
|
+
if (!root) {
|
|
152
|
+
return Object.freeze([]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
157
|
+
const projects = entries
|
|
158
|
+
.filter((entry) => entry.isDirectory())
|
|
159
|
+
.map((entry) => join(root, entry.name));
|
|
160
|
+
return Object.freeze(projects);
|
|
161
|
+
} catch (error: unknown) {
|
|
162
|
+
if (isEnoentError(error)) {
|
|
163
|
+
return Object.freeze([]);
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const forensicEventTypeSchema = z.enum([
|
|
4
|
+
"run_started",
|
|
5
|
+
"dispatch",
|
|
6
|
+
"dispatch_multi",
|
|
7
|
+
"result_applied",
|
|
8
|
+
"phase_transition",
|
|
9
|
+
"complete",
|
|
10
|
+
"decision",
|
|
11
|
+
"error",
|
|
12
|
+
"loop_detected",
|
|
13
|
+
"failure_recorded",
|
|
14
|
+
"warning",
|
|
15
|
+
"session_start",
|
|
16
|
+
"session_end",
|
|
17
|
+
"fallback",
|
|
18
|
+
"model_switch",
|
|
19
|
+
"context_warning",
|
|
20
|
+
"tool_complete",
|
|
21
|
+
"compacted",
|
|
22
|
+
"info",
|
|
23
|
+
"debug",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export const forensicEventDomainSchema = z.enum([
|
|
27
|
+
"session",
|
|
28
|
+
"orchestrator",
|
|
29
|
+
"contract",
|
|
30
|
+
"system",
|
|
31
|
+
"review",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
export type JsonValue =
|
|
35
|
+
| null
|
|
36
|
+
| boolean
|
|
37
|
+
| number
|
|
38
|
+
| string
|
|
39
|
+
| readonly JsonValue[]
|
|
40
|
+
| { readonly [key: string]: JsonValue };
|
|
41
|
+
|
|
42
|
+
export const jsonValueSchema: z.ZodType<JsonValue> = z.lazy(() =>
|
|
43
|
+
z.union([
|
|
44
|
+
z.null(),
|
|
45
|
+
z.boolean(),
|
|
46
|
+
z.number(),
|
|
47
|
+
z.string(),
|
|
48
|
+
z.array(jsonValueSchema),
|
|
49
|
+
z.record(z.string(), jsonValueSchema),
|
|
50
|
+
]),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
export const forensicEventSchema = z.object({
|
|
54
|
+
schemaVersion: z.literal(1),
|
|
55
|
+
timestamp: z.string().max(128),
|
|
56
|
+
projectRoot: z.string().max(4096),
|
|
57
|
+
domain: forensicEventDomainSchema,
|
|
58
|
+
runId: z.string().max(128).nullable().default(null),
|
|
59
|
+
sessionId: z.string().max(256).nullable().default(null),
|
|
60
|
+
parentSessionId: z.string().max(256).nullable().default(null),
|
|
61
|
+
phase: z.string().max(128).nullable().default(null),
|
|
62
|
+
dispatchId: z.string().max(128).nullable().default(null),
|
|
63
|
+
taskId: z.number().int().positive().nullable().default(null),
|
|
64
|
+
agent: z.string().max(128).nullable().default(null),
|
|
65
|
+
type: forensicEventTypeSchema,
|
|
66
|
+
code: z.string().max(128).nullable().default(null),
|
|
67
|
+
message: z.string().max(4096).nullable().default(null),
|
|
68
|
+
payload: z.record(z.string(), jsonValueSchema).default({}),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export const forensicEventDefaults = forensicEventSchema.parse({
|
|
72
|
+
schemaVersion: 1,
|
|
73
|
+
timestamp: new Date(0).toISOString(),
|
|
74
|
+
projectRoot: "/unknown",
|
|
75
|
+
domain: "orchestrator",
|
|
76
|
+
type: "error",
|
|
77
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type {
|
|
3
|
+
forensicEventDomainSchema,
|
|
4
|
+
forensicEventSchema,
|
|
5
|
+
forensicEventTypeSchema,
|
|
6
|
+
} from "./forensic-schemas";
|
|
7
|
+
|
|
8
|
+
export type ForensicEventDomain = z.infer<typeof forensicEventDomainSchema>;
|
|
9
|
+
export type ForensicEventType = z.infer<typeof forensicEventTypeSchema>;
|
|
10
|
+
export type ForensicEvent = z.infer<typeof forensicEventSchema>;
|
|
@@ -2,14 +2,25 @@
|
|
|
2
2
|
* Observability module barrel export.
|
|
3
3
|
*
|
|
4
4
|
* Re-exports all public APIs from the session observability system:
|
|
5
|
-
* - Schemas and types for structured events
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* - Summary generator for human-readable markdown reports
|
|
10
|
-
* - Retention for time-based log pruning
|
|
5
|
+
* - Schemas and types for structured forensic events
|
|
6
|
+
* - Forensic log writer/reader for durable project-local evidence
|
|
7
|
+
* - Summary generator for human-readable reports
|
|
8
|
+
* - Retention for time-based pruning
|
|
11
9
|
*/
|
|
12
10
|
|
|
11
|
+
export {
|
|
12
|
+
appendForensicEvent,
|
|
13
|
+
appendForensicEventForArtifactDir,
|
|
14
|
+
createForensicEvent,
|
|
15
|
+
getForensicLogPath,
|
|
16
|
+
readForensicEvents,
|
|
17
|
+
} from "./forensic-log";
|
|
18
|
+
export {
|
|
19
|
+
forensicEventDefaults,
|
|
20
|
+
forensicEventDomainSchema,
|
|
21
|
+
forensicEventSchema,
|
|
22
|
+
forensicEventTypeSchema,
|
|
23
|
+
} from "./forensic-schemas";
|
|
13
24
|
export type { EventSearchFilters, SessionLogEntry } from "./log-reader";
|
|
14
25
|
export {
|
|
15
26
|
listSessionLogs,
|
|
@@ -17,20 +28,8 @@ export {
|
|
|
17
28
|
readSessionLog,
|
|
18
29
|
searchEvents,
|
|
19
30
|
} from "./log-reader";
|
|
20
|
-
export {
|
|
31
|
+
export { getLogsDir, writeSessionLog } from "./log-writer";
|
|
21
32
|
export { pruneOldLogs } from "./retention";
|
|
22
|
-
export {
|
|
23
|
-
baseEventSchema,
|
|
24
|
-
decisionEventSchema,
|
|
25
|
-
errorEventSchema,
|
|
26
|
-
fallbackEventSchema,
|
|
27
|
-
loggingConfigSchema,
|
|
28
|
-
loggingDefaults,
|
|
29
|
-
modelSwitchEventSchema,
|
|
30
|
-
sessionDecisionSchema,
|
|
31
|
-
sessionEventSchema,
|
|
32
|
-
sessionLogSchema,
|
|
33
|
-
} from "./schemas";
|
|
34
33
|
export { getLogsDir as getEventLogsDir, getSessionLog, logEvent } from "./session-logger";
|
|
35
34
|
|
|
36
35
|
export {
|
|
@@ -40,14 +39,9 @@ export {
|
|
|
40
39
|
generateSessionSummary,
|
|
41
40
|
} from "./summary-generator";
|
|
42
41
|
export type {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
FallbackEvent,
|
|
47
|
-
LoggingConfig,
|
|
48
|
-
ModelSwitchEvent,
|
|
42
|
+
ForensicEvent,
|
|
43
|
+
ForensicEventDomain,
|
|
44
|
+
ForensicEventType,
|
|
49
45
|
SessionDecision,
|
|
50
|
-
SessionEvent,
|
|
51
|
-
SessionEventType,
|
|
52
46
|
SessionLog,
|
|
53
47
|
} from "./types";
|
|
@@ -1,27 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
* Session log reading, listing, searching, and filtering.
|
|
3
|
-
*
|
|
4
|
-
* Provides query capabilities over persisted session logs:
|
|
5
|
-
* - Read a specific session by ID
|
|
6
|
-
* - List all sessions sorted by startedAt (newest first)
|
|
7
|
-
* - Read the most recent session
|
|
8
|
-
* - Search/filter events within a session by type and time range
|
|
9
|
-
*
|
|
10
|
-
* All functions handle missing directories gracefully (D-16).
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
14
2
|
import { join } from "node:path";
|
|
15
3
|
import { isEnoentError } from "../utils/fs-helpers";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import type {
|
|
4
|
+
import { getProjectRootFromArtifactDir } from "../utils/paths";
|
|
5
|
+
import { readForensicEvents } from "./forensic-log";
|
|
6
|
+
import type { ForensicEvent } from "./forensic-types";
|
|
7
|
+
|
|
8
|
+
export interface SessionDecision {
|
|
9
|
+
readonly timestamp: string | null;
|
|
10
|
+
readonly phase: string;
|
|
11
|
+
readonly agent: string;
|
|
12
|
+
readonly decision: string;
|
|
13
|
+
readonly rationale: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SessionLog {
|
|
17
|
+
readonly schemaVersion: 1;
|
|
18
|
+
readonly sessionId: string;
|
|
19
|
+
readonly projectRoot: string;
|
|
20
|
+
readonly startedAt: string;
|
|
21
|
+
readonly endedAt: string | null;
|
|
22
|
+
readonly events: readonly ForensicEvent[];
|
|
23
|
+
readonly decisions: readonly SessionDecision[];
|
|
24
|
+
readonly errorSummary: Readonly<Record<string, number>>;
|
|
25
|
+
}
|
|
19
26
|
|
|
20
|
-
/**
|
|
21
|
-
* Summary entry for session listing (lightweight, no full event data).
|
|
22
|
-
*/
|
|
23
27
|
export interface SessionLogEntry {
|
|
24
28
|
readonly sessionId: string;
|
|
29
|
+
readonly projectRoot: string;
|
|
25
30
|
readonly startedAt: string;
|
|
26
31
|
readonly endedAt: string | null;
|
|
27
32
|
readonly eventCount: number;
|
|
@@ -29,124 +34,169 @@ export interface SessionLogEntry {
|
|
|
29
34
|
readonly errorCount: number;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
/**
|
|
33
|
-
* Filters for searching events within a session log.
|
|
34
|
-
*/
|
|
35
37
|
export interface EventSearchFilters {
|
|
36
38
|
readonly type?: string;
|
|
37
39
|
readonly after?: string;
|
|
38
40
|
readonly before?: string;
|
|
41
|
+
readonly domain?: string;
|
|
42
|
+
readonly subsystem?: string;
|
|
43
|
+
/** Matches against event.type for semantic severity (e.g. "error", "warning"),
|
|
44
|
+
* or against event.payload.severity / event.payload.level for explicit severity fields. */
|
|
45
|
+
readonly severity?: string;
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* Returns null on: non-existent file, malformed JSON, invalid schema.
|
|
45
|
-
* Never throws for expected failure modes.
|
|
46
|
-
*/
|
|
47
|
-
export async function readSessionLog(
|
|
48
|
-
sessionId: string,
|
|
49
|
-
logsDir?: string,
|
|
50
|
-
): Promise<SessionLog | null> {
|
|
51
|
-
const dir = logsDir ?? getLogsDir();
|
|
52
|
-
const logPath = join(dir, `${sessionId}.json`);
|
|
48
|
+
function _isSessionForProject(event: Readonly<ForensicEvent>, sessionId: string): boolean {
|
|
49
|
+
return event.domain === "session" && event.sessionId === sessionId;
|
|
50
|
+
}
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
function buildErrorSummary(events: readonly ForensicEvent[]): Readonly<Record<string, number>> {
|
|
53
|
+
const summary: Record<string, number> = {};
|
|
54
|
+
for (const event of events) {
|
|
55
|
+
if (event.type !== "error") {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const code =
|
|
59
|
+
event.code ?? (typeof event.payload.errorType === "string" ? event.payload.errorType : null);
|
|
60
|
+
const key = code ?? "unknown";
|
|
61
|
+
summary[key] = (summary[key] ?? 0) + 1;
|
|
64
62
|
}
|
|
63
|
+
return Object.freeze(summary);
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
66
|
+
function buildDecisions(events: readonly ForensicEvent[]): readonly SessionDecision[] {
|
|
67
|
+
return Object.freeze(
|
|
68
|
+
events
|
|
69
|
+
.filter((event) => event.type === "decision")
|
|
70
|
+
.map((event) => ({
|
|
71
|
+
timestamp: event.timestamp,
|
|
72
|
+
phase: event.phase ?? "UNKNOWN",
|
|
73
|
+
agent: event.agent ?? "unknown",
|
|
74
|
+
decision:
|
|
75
|
+
typeof event.payload.decision === "string"
|
|
76
|
+
? event.payload.decision
|
|
77
|
+
: (event.message ?? "decision recorded"),
|
|
78
|
+
rationale:
|
|
79
|
+
typeof event.payload.rationale === "string"
|
|
80
|
+
? event.payload.rationale
|
|
81
|
+
: (event.message ?? ""),
|
|
82
|
+
})),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const result = sessionLogSchema.safeParse(parsed);
|
|
93
|
-
|
|
94
|
-
if (result.success) {
|
|
95
|
-
const log = result.data;
|
|
96
|
-
const errorCount = log.events.filter((e) => e.type === "error").length;
|
|
97
|
-
|
|
98
|
-
entries.push({
|
|
99
|
-
sessionId: log.sessionId,
|
|
100
|
-
startedAt: log.startedAt,
|
|
101
|
-
endedAt: log.endedAt,
|
|
102
|
-
eventCount: log.events.length,
|
|
103
|
-
decisionCount: log.decisions.length,
|
|
104
|
-
errorCount,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
} catch (innerError: unknown) {
|
|
108
|
-
// SyntaxError = malformed JSON, expected and skipped.
|
|
109
|
-
// I/O errors (permissions, etc.) are unexpected — log them.
|
|
110
|
-
if (!(innerError instanceof SyntaxError) && !isEnoentError(innerError)) {
|
|
111
|
-
console.error("[opencode-autopilot] Unexpected error reading log file:", file, innerError);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
86
|
+
function buildSessionLog(
|
|
87
|
+
projectRoot: string,
|
|
88
|
+
sessionId: string,
|
|
89
|
+
events: readonly ForensicEvent[],
|
|
90
|
+
): SessionLog | null {
|
|
91
|
+
if (events.length === 0) {
|
|
92
|
+
return null;
|
|
114
93
|
}
|
|
115
94
|
|
|
116
|
-
|
|
117
|
-
|
|
95
|
+
const sessionEvents = events
|
|
96
|
+
.filter((event) => event.sessionId === sessionId)
|
|
97
|
+
.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
98
|
+
if (sessionEvents.length === 0) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
118
101
|
|
|
119
|
-
|
|
102
|
+
const started = sessionEvents.find((event) => event.type === "session_start");
|
|
103
|
+
const ended = [...sessionEvents].reverse().find((event) => event.type === "session_end");
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
schemaVersion: 1,
|
|
107
|
+
sessionId,
|
|
108
|
+
projectRoot,
|
|
109
|
+
startedAt: started?.timestamp ?? sessionEvents[0].timestamp,
|
|
110
|
+
endedAt: ended?.timestamp ?? null,
|
|
111
|
+
events: Object.freeze(sessionEvents),
|
|
112
|
+
decisions: buildDecisions(sessionEvents),
|
|
113
|
+
errorSummary: buildErrorSummary(sessionEvents),
|
|
114
|
+
};
|
|
120
115
|
}
|
|
121
116
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
117
|
+
export async function readSessionLog(
|
|
118
|
+
sessionId: string,
|
|
119
|
+
artifactDirOrProjectRoot: string,
|
|
120
|
+
): Promise<SessionLog | null> {
|
|
121
|
+
const projectRoot = getProjectRootFromArtifactDir(artifactDirOrProjectRoot);
|
|
122
|
+
const events = await readForensicEvents(projectRoot);
|
|
123
|
+
return buildSessionLog(projectRoot, sessionId, events);
|
|
124
|
+
}
|
|
129
125
|
|
|
130
|
-
|
|
126
|
+
export async function listSessionLogs(
|
|
127
|
+
artifactDirOrProjectRoot: string,
|
|
128
|
+
): Promise<readonly SessionLogEntry[]> {
|
|
129
|
+
const projectRoot = getProjectRootFromArtifactDir(artifactDirOrProjectRoot);
|
|
130
|
+
const events = await readForensicEvents(projectRoot);
|
|
131
|
+
const sessionIds = [
|
|
132
|
+
...new Set(events.map((event) => event.sessionId).filter((value): value is string => !!value)),
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const entries = sessionIds
|
|
136
|
+
.map((sessionId) => buildSessionLog(projectRoot, sessionId, events))
|
|
137
|
+
.filter((entry): entry is SessionLog => entry !== null)
|
|
138
|
+
.map((log) => ({
|
|
139
|
+
sessionId: log.sessionId,
|
|
140
|
+
projectRoot: log.projectRoot,
|
|
141
|
+
startedAt: log.startedAt,
|
|
142
|
+
endedAt: log.endedAt,
|
|
143
|
+
eventCount: log.events.length,
|
|
144
|
+
decisionCount: log.decisions.length,
|
|
145
|
+
errorCount: Object.values(log.errorSummary).reduce((sum, count) => sum + count, 0),
|
|
146
|
+
}))
|
|
147
|
+
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
148
|
+
|
|
149
|
+
return Object.freeze(entries);
|
|
150
|
+
}
|
|
131
151
|
|
|
132
|
-
|
|
133
|
-
|
|
152
|
+
export async function readLatestSessionLog(
|
|
153
|
+
artifactDirOrProjectRoot: string,
|
|
154
|
+
): Promise<SessionLog | null> {
|
|
155
|
+
const entries = await listSessionLogs(artifactDirOrProjectRoot);
|
|
156
|
+
if (entries.length === 0) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return readSessionLog(entries[0].sessionId, artifactDirOrProjectRoot);
|
|
134
160
|
}
|
|
135
161
|
|
|
136
|
-
/**
|
|
137
|
-
* Filters events by type and/or time range.
|
|
138
|
-
*
|
|
139
|
-
* Pure function (no I/O, no side effects).
|
|
140
|
-
* Accepts a readonly array of events and returns a filtered copy.
|
|
141
|
-
*/
|
|
142
162
|
export function searchEvents(
|
|
143
|
-
events: readonly
|
|
163
|
+
events: readonly ForensicEvent[],
|
|
144
164
|
filters: EventSearchFilters,
|
|
145
|
-
): readonly
|
|
165
|
+
): readonly ForensicEvent[] {
|
|
146
166
|
return events.filter((event) => {
|
|
147
167
|
if (filters.type && event.type !== filters.type) return false;
|
|
168
|
+
if (filters.domain && event.domain !== filters.domain) return false;
|
|
148
169
|
if (filters.after && event.timestamp <= filters.after) return false;
|
|
149
170
|
if (filters.before && event.timestamp >= filters.before) return false;
|
|
171
|
+
if (filters.subsystem) {
|
|
172
|
+
const subsystem = event.payload.subsystem;
|
|
173
|
+
if (typeof subsystem !== "string" || subsystem !== filters.subsystem) return false;
|
|
174
|
+
}
|
|
175
|
+
if (filters.severity) {
|
|
176
|
+
const payloadSeverity =
|
|
177
|
+
typeof event.payload.severity === "string"
|
|
178
|
+
? event.payload.severity
|
|
179
|
+
: typeof event.payload.level === "string"
|
|
180
|
+
? event.payload.level
|
|
181
|
+
: null;
|
|
182
|
+
const matchesSemantic = event.type === filters.severity;
|
|
183
|
+
const matchesPayload = payloadSeverity === filters.severity;
|
|
184
|
+
if (!matchesSemantic && !matchesPayload) return false;
|
|
185
|
+
}
|
|
150
186
|
return true;
|
|
151
187
|
});
|
|
152
188
|
}
|
|
189
|
+
|
|
190
|
+
export async function listProjectRootsWithLogs(rootDir: string): Promise<readonly string[]> {
|
|
191
|
+
try {
|
|
192
|
+
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
193
|
+
return Object.freeze(
|
|
194
|
+
entries.filter((entry) => entry.isDirectory()).map((entry) => join(rootDir, entry.name)),
|
|
195
|
+
);
|
|
196
|
+
} catch (error: unknown) {
|
|
197
|
+
if (isEnoentError(error)) {
|
|
198
|
+
return Object.freeze([]);
|
|
199
|
+
}
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|