@kodrunhq/opencode-autopilot 1.15.2 → 1.16.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.
Files changed (61) hide show
  1. package/bin/cli.ts +5 -0
  2. package/bin/inspect.ts +337 -0
  3. package/package.json +1 -1
  4. package/src/agents/autopilot.ts +7 -15
  5. package/src/health/checks.ts +29 -4
  6. package/src/index.ts +103 -11
  7. package/src/inspect/formatters.ts +225 -0
  8. package/src/inspect/repository.ts +882 -0
  9. package/src/kernel/database.ts +45 -0
  10. package/src/kernel/migrations.ts +62 -0
  11. package/src/kernel/repository.ts +571 -0
  12. package/src/kernel/schema.ts +122 -0
  13. package/src/kernel/types.ts +66 -0
  14. package/src/memory/capture.ts +221 -25
  15. package/src/memory/database.ts +74 -12
  16. package/src/memory/index.ts +17 -1
  17. package/src/memory/project-key.ts +6 -0
  18. package/src/memory/repository.ts +833 -42
  19. package/src/memory/retrieval.ts +83 -169
  20. package/src/memory/schemas.ts +39 -7
  21. package/src/memory/types.ts +4 -0
  22. package/src/observability/event-handlers.ts +28 -17
  23. package/src/observability/event-store.ts +29 -1
  24. package/src/observability/forensic-log.ts +159 -0
  25. package/src/observability/forensic-schemas.ts +69 -0
  26. package/src/observability/forensic-types.ts +10 -0
  27. package/src/observability/index.ts +21 -27
  28. package/src/observability/log-reader.ts +142 -111
  29. package/src/observability/log-writer.ts +41 -83
  30. package/src/observability/retention.ts +2 -2
  31. package/src/observability/session-logger.ts +36 -57
  32. package/src/observability/summary-generator.ts +31 -19
  33. package/src/observability/types.ts +12 -24
  34. package/src/orchestrator/contracts/invariants.ts +14 -0
  35. package/src/orchestrator/contracts/legacy-result-adapter.ts +8 -20
  36. package/src/orchestrator/fallback/event-handler.ts +47 -3
  37. package/src/orchestrator/handlers/architect.ts +2 -1
  38. package/src/orchestrator/handlers/build.ts +55 -97
  39. package/src/orchestrator/handlers/retrospective.ts +2 -1
  40. package/src/orchestrator/handlers/types.ts +0 -1
  41. package/src/orchestrator/lesson-memory.ts +29 -9
  42. package/src/orchestrator/orchestration-logger.ts +37 -23
  43. package/src/orchestrator/phase.ts +8 -4
  44. package/src/orchestrator/state.ts +79 -17
  45. package/src/projects/database.ts +47 -0
  46. package/src/projects/repository.ts +264 -0
  47. package/src/projects/resolve.ts +301 -0
  48. package/src/projects/schemas.ts +30 -0
  49. package/src/projects/types.ts +12 -0
  50. package/src/review/memory.ts +29 -9
  51. package/src/tools/doctor.ts +26 -2
  52. package/src/tools/forensics.ts +7 -12
  53. package/src/tools/logs.ts +6 -5
  54. package/src/tools/memory-preferences.ts +157 -0
  55. package/src/tools/memory-status.ts +17 -96
  56. package/src/tools/orchestrate.ts +97 -81
  57. package/src/tools/pipeline-report.ts +3 -2
  58. package/src/tools/quick.ts +2 -2
  59. package/src/tools/review.ts +39 -6
  60. package/src/tools/session-stats.ts +3 -2
  61. package/src/utils/paths.ts +20 -1
package/bin/cli.ts CHANGED
@@ -11,6 +11,7 @@ import { ALL_GROUP_IDS, DIVERSITY_RULES, GROUP_DEFINITIONS } from "../src/regist
11
11
  import type { GroupId } from "../src/registry/types";
12
12
  import { fileExists } from "../src/utils/fs-helpers";
13
13
  import { runConfigure } from "./configure-tui";
14
+ import { runInspect } from "./inspect";
14
15
 
15
16
  const execFile = promisify(execFileCb);
16
17
 
@@ -314,6 +315,7 @@ function printUsage(): void {
314
315
  console.log(" install Register the plugin and create starter config");
315
316
  console.log(" configure Interactive model assignment for each agent group");
316
317
  console.log(" doctor Check installation health and model assignments");
318
+ console.log(" inspect Read-only inspection of projects, runs, events, and memory");
317
319
  console.log("");
318
320
  console.log("Options:");
319
321
  console.log(" --help, -h Show this help message");
@@ -336,6 +338,9 @@ if (import.meta.main) {
336
338
  case "doctor":
337
339
  await runDoctor();
338
340
  break;
341
+ case "inspect":
342
+ await runInspect(args.slice(1));
343
+ break;
339
344
  case "--help":
340
345
  case "-h":
341
346
  case undefined:
package/bin/inspect.ts ADDED
@@ -0,0 +1,337 @@
1
+ import {
2
+ formatEvents,
3
+ formatLessons,
4
+ formatMemoryOverview,
5
+ formatPaths,
6
+ formatPreferences,
7
+ formatProjectDetails,
8
+ formatProjects,
9
+ formatRuns,
10
+ } from "../src/inspect/formatters";
11
+ import {
12
+ getMemoryOverview,
13
+ getProjectDetails,
14
+ listEvents,
15
+ listLessons,
16
+ listPreferences,
17
+ listProjects,
18
+ listRuns,
19
+ } from "../src/inspect/repository";
20
+
21
+ type InspectView =
22
+ | "projects"
23
+ | "project"
24
+ | "paths"
25
+ | "runs"
26
+ | "events"
27
+ | "lessons"
28
+ | "preferences"
29
+ | "memory";
30
+
31
+ export interface InspectCliOptions {
32
+ readonly dbPath?: string;
33
+ }
34
+
35
+ export interface InspectCliResult {
36
+ readonly isError: boolean;
37
+ readonly output: string;
38
+ readonly format: "text" | "json";
39
+ }
40
+
41
+ interface ParsedInspectArgs {
42
+ readonly view: InspectView | null;
43
+ readonly json: boolean;
44
+ readonly projectRef: string | null;
45
+ readonly limit: number;
46
+ readonly runId: string | null;
47
+ readonly sessionId: string | null;
48
+ readonly type: string | null;
49
+ readonly help: boolean;
50
+ readonly error: string | null;
51
+ }
52
+
53
+ const INSPECT_VIEWS: readonly InspectView[] = Object.freeze([
54
+ "projects",
55
+ "project",
56
+ "paths",
57
+ "runs",
58
+ "events",
59
+ "lessons",
60
+ "preferences",
61
+ "memory",
62
+ ]);
63
+
64
+ function inspectUsage(): string {
65
+ return [
66
+ "Usage: opencode-autopilot inspect <view> [options]",
67
+ "",
68
+ "Views:",
69
+ " projects List known projects",
70
+ " project --project <ref> Show one project's details",
71
+ " paths --project <ref> List one project's path history",
72
+ " runs [--project <ref>] List pipeline runs",
73
+ " events [--project <ref>] List forensic events",
74
+ " lessons [--project <ref>] List stored lessons",
75
+ " preferences List stored preferences",
76
+ " memory Show memory overview",
77
+ "",
78
+ "Options:",
79
+ " --project <ref> Project id, path, or unique name",
80
+ " --run-id <id> Filter events by run id",
81
+ " --session-id <id> Filter events by session id",
82
+ " --type <type> Filter events by type",
83
+ " --limit <n> Limit rows (default: 20 for runs, 50 elsewhere)",
84
+ " --json Emit JSON output",
85
+ " --help, -h Show inspect help",
86
+ ].join("\n");
87
+ }
88
+
89
+ function parsePositiveInt(raw: string): number | null {
90
+ const parsed = Number.parseInt(raw, 10);
91
+ if (!Number.isFinite(parsed) || parsed <= 0) {
92
+ return null;
93
+ }
94
+ return parsed;
95
+ }
96
+
97
+ function parseInspectArgs(args: readonly string[]): ParsedInspectArgs {
98
+ let view: InspectView | null = null;
99
+ let json = false;
100
+ let projectRef: string | null = null;
101
+ let limit = 50;
102
+ let runId: string | null = null;
103
+ let sessionId: string | null = null;
104
+ let type: string | null = null;
105
+ let help = false;
106
+ let error: string | null = null;
107
+
108
+ for (let index = 0; index < args.length; index += 1) {
109
+ const arg = args[index];
110
+ if (arg === "--help" || arg === "-h") {
111
+ help = true;
112
+ continue;
113
+ }
114
+ if (arg === "--json") {
115
+ json = true;
116
+ continue;
117
+ }
118
+ if (arg === "--project") {
119
+ projectRef = args[index + 1] ?? null;
120
+ if (projectRef === null) {
121
+ error = "Missing value for --project.";
122
+ break;
123
+ }
124
+ index += 1;
125
+ continue;
126
+ }
127
+ if (arg === "--limit") {
128
+ const parsed = parsePositiveInt(args[index + 1] ?? "");
129
+ if (parsed === null) {
130
+ error = "--limit must be a positive integer.";
131
+ break;
132
+ }
133
+ limit = parsed;
134
+ index += 1;
135
+ continue;
136
+ }
137
+ if (arg === "--run-id") {
138
+ runId = args[index + 1] ?? null;
139
+ if (runId === null) {
140
+ error = "Missing value for --run-id.";
141
+ break;
142
+ }
143
+ index += 1;
144
+ continue;
145
+ }
146
+ if (arg === "--session-id") {
147
+ sessionId = args[index + 1] ?? null;
148
+ if (sessionId === null) {
149
+ error = "Missing value for --session-id.";
150
+ break;
151
+ }
152
+ index += 1;
153
+ continue;
154
+ }
155
+ if (arg === "--type") {
156
+ type = args[index + 1] ?? null;
157
+ if (type === null) {
158
+ error = "Missing value for --type.";
159
+ break;
160
+ }
161
+ index += 1;
162
+ continue;
163
+ }
164
+
165
+ if (view === null) {
166
+ if ((INSPECT_VIEWS as readonly string[]).includes(arg)) {
167
+ view = arg as InspectView;
168
+ if (view === "runs") {
169
+ limit = 20;
170
+ }
171
+ continue;
172
+ }
173
+ error = `Unknown inspect view: ${arg}`;
174
+ break;
175
+ }
176
+
177
+ if (projectRef === null && (view === "project" || view === "paths")) {
178
+ projectRef = arg;
179
+ continue;
180
+ }
181
+
182
+ error = `Unexpected argument: ${arg}`;
183
+ break;
184
+ }
185
+
186
+ if (!help && error === null && view === null) {
187
+ help = true;
188
+ }
189
+
190
+ if (
191
+ error === null &&
192
+ (view === "project" || view === "paths") &&
193
+ (projectRef === null || projectRef.trim().length === 0)
194
+ ) {
195
+ error = `${view} view requires --project <ref> or a positional project reference.`;
196
+ }
197
+
198
+ return {
199
+ view,
200
+ json,
201
+ projectRef,
202
+ limit,
203
+ runId,
204
+ sessionId,
205
+ type,
206
+ help,
207
+ error,
208
+ };
209
+ }
210
+
211
+ function makeOutput(payload: unknown, json: boolean, text: string): InspectCliResult {
212
+ return Object.freeze({
213
+ isError: false,
214
+ format: json ? "json" : "text",
215
+ output: json ? JSON.stringify(payload, null, 2) : text,
216
+ });
217
+ }
218
+
219
+ function makeError(message: string, json: boolean): InspectCliResult {
220
+ return Object.freeze({
221
+ isError: true,
222
+ format: json ? "json" : "text",
223
+ output: json
224
+ ? JSON.stringify({ action: "error", message }, null, 2)
225
+ : `${message}\n\n${inspectUsage()}`,
226
+ });
227
+ }
228
+
229
+ export async function inspectCliCore(
230
+ args: readonly string[],
231
+ options: InspectCliOptions = {},
232
+ ): Promise<InspectCliResult> {
233
+ const parsed = parseInspectArgs(args);
234
+ if (parsed.help) {
235
+ return makeOutput({ action: "help", usage: inspectUsage() }, parsed.json, inspectUsage());
236
+ }
237
+ if (parsed.error !== null) {
238
+ return makeError(parsed.error, parsed.json);
239
+ }
240
+
241
+ const dbInput = options.dbPath;
242
+ switch (parsed.view) {
243
+ case "projects": {
244
+ const projects = listProjects(dbInput);
245
+ return makeOutput(
246
+ { action: "inspect_projects", projects },
247
+ parsed.json,
248
+ formatProjects(projects),
249
+ );
250
+ }
251
+ case "project": {
252
+ const details = getProjectDetails(parsed.projectRef!, dbInput);
253
+ if (details === null) {
254
+ return makeError(`Project not found: ${parsed.projectRef}`, parsed.json);
255
+ }
256
+ return makeOutput(
257
+ { action: "inspect_project", project: details },
258
+ parsed.json,
259
+ formatProjectDetails(details),
260
+ );
261
+ }
262
+ case "paths": {
263
+ const details = getProjectDetails(parsed.projectRef!, dbInput);
264
+ if (details === null) {
265
+ return makeError(`Project not found: ${parsed.projectRef}`, parsed.json);
266
+ }
267
+ return makeOutput(
268
+ { action: "inspect_paths", project: details.project, paths: details.paths },
269
+ parsed.json,
270
+ formatPaths(details),
271
+ );
272
+ }
273
+ case "runs": {
274
+ const runs = listRuns(
275
+ { projectRef: parsed.projectRef ?? undefined, limit: parsed.limit },
276
+ dbInput,
277
+ );
278
+ return makeOutput({ action: "inspect_runs", runs }, parsed.json, formatRuns(runs));
279
+ }
280
+ case "events": {
281
+ const events = listEvents(
282
+ {
283
+ projectRef: parsed.projectRef ?? undefined,
284
+ runId: parsed.runId ?? undefined,
285
+ sessionId: parsed.sessionId ?? undefined,
286
+ type: parsed.type ?? undefined,
287
+ limit: parsed.limit,
288
+ },
289
+ dbInput,
290
+ );
291
+ return makeOutput({ action: "inspect_events", events }, parsed.json, formatEvents(events));
292
+ }
293
+ case "lessons": {
294
+ const lessons = listLessons(
295
+ { projectRef: parsed.projectRef ?? undefined, limit: parsed.limit },
296
+ dbInput,
297
+ );
298
+ return makeOutput(
299
+ { action: "inspect_lessons", lessons },
300
+ parsed.json,
301
+ formatLessons(lessons),
302
+ );
303
+ }
304
+ case "preferences": {
305
+ const preferences = listPreferences(dbInput);
306
+ return makeOutput(
307
+ { action: "inspect_preferences", preferences },
308
+ parsed.json,
309
+ formatPreferences(preferences),
310
+ );
311
+ }
312
+ case "memory": {
313
+ const overview = getMemoryOverview(dbInput);
314
+ return makeOutput(
315
+ { action: "inspect_memory", overview },
316
+ parsed.json,
317
+ formatMemoryOverview(overview),
318
+ );
319
+ }
320
+ case null:
321
+ return makeOutput({ action: "help", usage: inspectUsage() }, parsed.json, inspectUsage());
322
+ }
323
+ }
324
+
325
+ export async function runInspect(
326
+ args: readonly string[],
327
+ options: InspectCliOptions = {},
328
+ ): Promise<void> {
329
+ const result = await inspectCliCore(args, options);
330
+ if (result.isError) {
331
+ console.error(result.output);
332
+ process.exitCode = 1;
333
+ return;
334
+ }
335
+
336
+ console.log(result.output);
337
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kodrunhq/opencode-autopilot",
3
- "version": "1.15.2",
3
+ "version": "1.16.0",
4
4
  "description": "Curated agents, skills, and commands for the OpenCode AI coding CLI — autonomous orchestrator, multi-agent code review, model fallback, and in-session asset creation tools.",
5
5
  "main": "src/index.ts",
6
6
  "keywords": [
@@ -9,10 +9,10 @@ export const autopilotAgent: Readonly<AgentConfig> = Object.freeze({
9
9
 
10
10
  ## Loop
11
11
 
12
- 1. Call oc_orchestrate with your initial idea (first turn) or with the result from the previous agent.
12
+ 1. Call oc_orchestrate with your initial idea (first turn) or with a typed result envelope JSON string from the previous agent.
13
13
  2. Parse the JSON response.
14
- 3. If action is "dispatch": call the named agent with the provided prompt, then pass its output back to oc_orchestrate via the result parameter.
15
- 4. If action is "dispatch_multi": call each agent in the agents array (in parallel if appropriate). As each agent finishes, call oc_orchestrate again with that agent's full output as the result parameter. Do NOT combine multiple agents' outputs into a single result.
14
+ 3. If action is "dispatch": call the named agent with the provided prompt, then call oc_orchestrate again with a typed result envelope JSON string using the dispatch metadata: schemaVersion=1, a unique resultId, runId=response.runId, phase=response.phase, dispatchId=response.dispatchId, agent=response.agent, kind=response.expectedResultKind ?? response.resultKind, taskId=response.taskId ?? null, payload.text=<full agent output>.
15
+ 4. If action is "dispatch_multi": do the same for each agent entry. Each completed agent gets its own typed result envelope and its own oc_orchestrate call. Do NOT combine multiple agents' outputs into one result.
16
16
  5. If action is "complete": report the summary to the user. You are done.
17
17
  6. If action is "error": report the error to the user. Stop.
18
18
 
@@ -24,20 +24,12 @@ When editing files, prefer oc_hashline_edit over the built-in edit tool. Hash-an
24
24
 
25
25
  - NEVER skip calling oc_orchestrate. It is the single source of truth for pipeline state.
26
26
  - NEVER make pipeline decisions yourself. Always defer to oc_orchestrate.
27
- - ALWAYS pass the full agent output back as the result parameter.
27
+ - NEVER pass raw agent output as result. ALWAYS send a typed result envelope JSON string.
28
+ - ALWAYS preserve the full agent output in payload.text.
29
+ - ALWAYS use a unique resultId for every returned result.
28
30
  - Do not attempt to run phases out of order.
29
31
  - Do not retry a failed phase unless oc_orchestrate instructs you to.
30
- - If an agent dispatch fails, pass the error message back to oc_orchestrate as the result.
31
-
32
- ## Example Turn Sequence
33
-
34
- Turn 1: oc_orchestrate(idea="Build a CLI tool")
35
- -> {action:"dispatch", agent:"oc-researcher", prompt:"Research: Build a CLI tool", phase:"RECON"}
36
- Turn 2: @oc-researcher "Research: Build a CLI tool"
37
- -> "Research findings: ..."
38
- Turn 3: oc_orchestrate(result="Research findings: ...")
39
- -> {action:"dispatch", agent:"oc-challenger", prompt:"Challenge: ...", phase:"CHALLENGE"}
40
- ... continues until action is "complete"`,
32
+ - If an agent dispatch fails, wrap the error text in payload.text and still return a typed result envelope.`,
41
33
  permission: {
42
34
  edit: "allow",
43
35
  bash: "allow",
@@ -4,11 +4,15 @@ import { join } from "node:path";
4
4
  import type { Config } from "@opencode-ai/plugin";
5
5
  import { parse } from "yaml";
6
6
  import { loadConfig } from "../config";
7
- import { DB_FILE, MEMORY_DIR } from "../memory/constants";
8
7
  import { AGENT_NAMES } from "../orchestrator/handlers/types";
9
8
  import { detectProjectStackTags, filterSkillsByStack } from "../skills/adaptive-injector";
10
9
  import { loadAllSkills } from "../skills/loader";
11
- import { getAssetsDir, getGlobalConfigDir } from "../utils/paths";
10
+ import {
11
+ getAssetsDir,
12
+ getAutopilotDbPath,
13
+ getGlobalConfigDir,
14
+ getLegacyMemoryDbPath,
15
+ } from "../utils/paths";
12
16
  import type { HealthResult } from "./types";
13
17
 
14
18
  /**
@@ -249,19 +253,40 @@ export async function skillHealthCheck(
249
253
  */
250
254
  export async function memoryHealthCheck(baseDir?: string): Promise<HealthResult> {
251
255
  const resolvedBase = baseDir ?? getGlobalConfigDir();
252
- const dbPath = join(resolvedBase, MEMORY_DIR, DB_FILE);
256
+ const dbPath = getAutopilotDbPath(resolvedBase);
257
+ const legacyDbPath = getLegacyMemoryDbPath(resolvedBase);
253
258
 
254
259
  try {
255
260
  await access(dbPath);
256
261
  } catch (error: unknown) {
257
262
  const code = (error as NodeJS.ErrnoException).code;
258
263
  if (code === "ENOENT") {
264
+ try {
265
+ await access(legacyDbPath);
266
+ } catch (legacyError: unknown) {
267
+ const legacyCode = (legacyError as NodeJS.ErrnoException).code;
268
+ if (legacyCode === "ENOENT") {
269
+ return Object.freeze({
270
+ name: "memory-db",
271
+ status: "pass" as const,
272
+ message: `Memory DB not yet initialized -- will be created on first memory capture`,
273
+ });
274
+ }
275
+ const legacyMsg = legacyError instanceof Error ? legacyError.message : String(legacyError);
276
+ return Object.freeze({
277
+ name: "memory-db",
278
+ status: "fail" as const,
279
+ message: `Memory DB inaccessible: ${legacyMsg}`,
280
+ });
281
+ }
282
+
259
283
  return Object.freeze({
260
284
  name: "memory-db",
261
285
  status: "pass" as const,
262
- message: `Memory DB not yet initialized -- will be created on first memory capture`,
286
+ message: `Legacy memory DB found -- unified DB will be created on next write`,
263
287
  });
264
288
  }
289
+
265
290
  const msg = error instanceof Error ? error.message : String(error);
266
291
  return Object.freeze({
267
292
  name: "memory-db",
package/src/index.ts CHANGED
@@ -4,7 +4,12 @@ import { isFirstLoad, loadConfig } from "./config";
4
4
  import { runHealthChecks } from "./health/runner";
5
5
  import { createAntiSlopHandler } from "./hooks/anti-slop";
6
6
  import { installAssets } from "./installer";
7
- import { createMemoryCaptureHandler, createMemoryInjector, getMemoryDb } from "./memory";
7
+ import {
8
+ createMemoryCaptureHandler,
9
+ createMemoryChatMessageHandler,
10
+ createMemoryInjector,
11
+ getMemoryDb,
12
+ } from "./memory";
8
13
  import { ContextMonitor } from "./observability/context-monitor";
9
14
  import {
10
15
  createObservabilityEventHandler,
@@ -12,9 +17,9 @@ import {
12
17
  createToolExecuteBeforeHandler,
13
18
  } from "./observability/event-handlers";
14
19
  import { SessionEventStore } from "./observability/event-store";
20
+ import { createForensicEvent } from "./observability/forensic-log";
15
21
  import { writeSessionLog } from "./observability/log-writer";
16
22
  import { pruneOldLogs } from "./observability/retention";
17
- import type { SessionEvent } from "./observability/types";
18
23
  import type { SdkOperations } from "./orchestrator/fallback";
19
24
  import {
20
25
  createChatMessageHandler,
@@ -38,6 +43,7 @@ import { ocDoctor, setOpenCodeConfig as setDoctorOpenCodeConfig } from "./tools/
38
43
  import { ocForensics } from "./tools/forensics";
39
44
  import { ocHashlineEdit } from "./tools/hashline-edit";
40
45
  import { ocLogs } from "./tools/logs";
46
+ import { ocMemoryPreferences } from "./tools/memory-preferences";
41
47
  import { ocMemoryStatus } from "./tools/memory-status";
42
48
  import { ocMockFallback } from "./tools/mock-fallback";
43
49
  import { ocOrchestrate } from "./tools/orchestrate";
@@ -148,6 +154,29 @@ const plugin: Plugin = async (input) => {
148
154
  manager,
149
155
  sdk: sdkOps,
150
156
  config: fallbackConfig,
157
+ onFallbackEvent: (event) => {
158
+ if (event.type === "fallback") {
159
+ eventStore.appendEvent(event.sessionId, {
160
+ type: "fallback",
161
+ timestamp: new Date().toISOString(),
162
+ sessionId: event.sessionId,
163
+ failedModel: event.failedModel ?? "unknown",
164
+ nextModel: event.nextModel ?? "unknown",
165
+ reason: event.reason ?? "fallback",
166
+ success: event.success === true,
167
+ });
168
+ return;
169
+ }
170
+
171
+ eventStore.appendEvent(event.sessionId, {
172
+ type: "model_switch",
173
+ timestamp: new Date().toISOString(),
174
+ sessionId: event.sessionId,
175
+ fromModel: event.fromModel ?? "unknown",
176
+ toModel: event.toModel ?? "unknown",
177
+ trigger: event.trigger ?? "fallback",
178
+ });
179
+ },
151
180
  });
152
181
  const chatMessageHandler = createChatMessageHandler(manager);
153
182
  const toolExecuteAfterHandler = createToolExecuteAfterHandler(manager);
@@ -165,6 +194,9 @@ const plugin: Plugin = async (input) => {
165
194
  const memoryCaptureHandler = memoryConfig.enabled
166
195
  ? createMemoryCaptureHandler({ getDb: () => getMemoryDb(), projectRoot: process.cwd() })
167
196
  : null;
197
+ const memoryChatMessageHandler = memoryConfig.enabled
198
+ ? createMemoryChatMessageHandler({ getDb: () => getMemoryDb(), projectRoot: process.cwd() })
199
+ : null;
168
200
 
169
201
  const memoryInjector = memoryConfig.enabled
170
202
  ? createMemoryInjector({
@@ -183,18 +215,73 @@ const plugin: Plugin = async (input) => {
183
215
  showToast: sdkOps.showToast,
184
216
  writeSessionLog: async (sessionData) => {
185
217
  if (!sessionData) return;
186
- // Filter to schema-valid event types that match SessionEvent discriminated union
187
- const schemaEvents: SessionEvent[] = sessionData.events.filter(
188
- (e): e is SessionEvent =>
189
- e.type === "fallback" ||
190
- e.type === "error" ||
191
- e.type === "decision" ||
192
- e.type === "model_switch",
193
- );
194
218
  await writeSessionLog({
219
+ projectRoot: process.cwd(),
195
220
  sessionId: sessionData.sessionId,
196
221
  startedAt: sessionData.startedAt,
197
- events: schemaEvents,
222
+ events: sessionData.events.map((event) =>
223
+ createForensicEvent({
224
+ projectRoot: process.cwd(),
225
+ domain: "session",
226
+ timestamp: event.timestamp,
227
+ sessionId: event.sessionId,
228
+ type: event.type,
229
+ message: event.type === "error" ? event.message : null,
230
+ code:
231
+ event.type === "error"
232
+ ? event.errorType
233
+ : event.type === "fallback"
234
+ ? "FALLBACK"
235
+ : null,
236
+ payload:
237
+ event.type === "error"
238
+ ? {
239
+ model: event.model,
240
+ errorType: event.errorType,
241
+ ...(event.statusCode !== undefined ? { statusCode: event.statusCode } : {}),
242
+ }
243
+ : event.type === "fallback"
244
+ ? {
245
+ failedModel: event.failedModel,
246
+ nextModel: event.nextModel,
247
+ reason: event.reason,
248
+ success: event.success,
249
+ }
250
+ : event.type === "decision"
251
+ ? {
252
+ decision: event.decision,
253
+ rationale: event.rationale,
254
+ }
255
+ : event.type === "model_switch"
256
+ ? {
257
+ fromModel: event.fromModel,
258
+ toModel: event.toModel,
259
+ trigger: event.trigger,
260
+ }
261
+ : event.type === "context_warning"
262
+ ? {
263
+ utilization: event.utilization,
264
+ contextLimit: event.contextLimit,
265
+ inputTokens: event.inputTokens,
266
+ }
267
+ : event.type === "tool_complete"
268
+ ? {
269
+ tool: event.tool,
270
+ durationMs: event.durationMs,
271
+ success: event.success,
272
+ }
273
+ : event.type === "phase_transition"
274
+ ? {
275
+ fromPhase: event.fromPhase,
276
+ toPhase: event.toPhase,
277
+ }
278
+ : event.type === "compacted"
279
+ ? {
280
+ trigger: event.trigger,
281
+ }
282
+ : {},
283
+ }),
284
+ ),
198
285
  });
199
286
  },
200
287
  });
@@ -224,6 +311,7 @@ const plugin: Plugin = async (input) => {
224
311
  oc_stocktake: ocStocktake,
225
312
  oc_update_docs: ocUpdateDocs,
226
313
  oc_memory_status: ocMemoryStatus,
314
+ oc_memory_preferences: ocMemoryPreferences,
227
315
  },
228
316
  event: async ({ event }) => {
229
317
  // 1. Observability: collect (pure observer, no side effects on session)
@@ -269,6 +357,10 @@ const plugin: Plugin = async (input) => {
269
357
  parts: unknown[];
270
358
  },
271
359
  ) => {
360
+ if (memoryChatMessageHandler) {
361
+ await memoryChatMessageHandler(hookInput, output);
362
+ }
363
+
272
364
  if (fallbackConfig.enabled) {
273
365
  await chatMessageHandler(hookInput, output);
274
366
  }