@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.
Files changed (93) 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/config/index.ts +29 -0
  6. package/src/config/migrations.ts +196 -0
  7. package/src/config/v7.ts +45 -0
  8. package/src/config.ts +3 -3
  9. package/src/health/checks.ts +126 -4
  10. package/src/health/types.ts +1 -1
  11. package/src/index.ts +128 -13
  12. package/src/inspect/formatters.ts +225 -0
  13. package/src/inspect/repository.ts +882 -0
  14. package/src/kernel/database.ts +45 -0
  15. package/src/kernel/migrations.ts +62 -0
  16. package/src/kernel/repository.ts +571 -0
  17. package/src/kernel/schema.ts +122 -0
  18. package/src/kernel/transaction.ts +48 -0
  19. package/src/kernel/types.ts +65 -0
  20. package/src/logging/domains.ts +39 -0
  21. package/src/logging/forensic-writer.ts +177 -0
  22. package/src/logging/index.ts +4 -0
  23. package/src/logging/logger.ts +44 -0
  24. package/src/logging/performance.ts +59 -0
  25. package/src/logging/rotation.ts +261 -0
  26. package/src/logging/types.ts +33 -0
  27. package/src/memory/capture-utils.ts +149 -0
  28. package/src/memory/capture.ts +82 -67
  29. package/src/memory/database.ts +74 -12
  30. package/src/memory/decay.ts +11 -2
  31. package/src/memory/index.ts +17 -1
  32. package/src/memory/injector.ts +4 -1
  33. package/src/memory/lessons.ts +85 -0
  34. package/src/memory/observations.ts +177 -0
  35. package/src/memory/preferences.ts +718 -0
  36. package/src/memory/project-key.ts +6 -0
  37. package/src/memory/projects.ts +83 -0
  38. package/src/memory/repository.ts +52 -216
  39. package/src/memory/retrieval.ts +88 -170
  40. package/src/memory/schemas.ts +39 -7
  41. package/src/memory/types.ts +4 -0
  42. package/src/observability/context-display.ts +8 -0
  43. package/src/observability/event-handlers.ts +69 -20
  44. package/src/observability/event-store.ts +29 -1
  45. package/src/observability/forensic-log.ts +167 -0
  46. package/src/observability/forensic-schemas.ts +77 -0
  47. package/src/observability/forensic-types.ts +10 -0
  48. package/src/observability/index.ts +21 -27
  49. package/src/observability/log-reader.ts +161 -111
  50. package/src/observability/log-writer.ts +41 -83
  51. package/src/observability/retention.ts +2 -2
  52. package/src/observability/session-logger.ts +36 -57
  53. package/src/observability/summary-generator.ts +31 -19
  54. package/src/observability/types.ts +12 -24
  55. package/src/orchestrator/contracts/invariants.ts +14 -0
  56. package/src/orchestrator/contracts/legacy-result-adapter.ts +8 -20
  57. package/src/orchestrator/error-context.ts +24 -0
  58. package/src/orchestrator/fallback/event-handler.ts +47 -3
  59. package/src/orchestrator/handlers/architect.ts +2 -1
  60. package/src/orchestrator/handlers/build-utils.ts +118 -0
  61. package/src/orchestrator/handlers/build.ts +42 -219
  62. package/src/orchestrator/handlers/retrospective.ts +2 -2
  63. package/src/orchestrator/handlers/types.ts +0 -1
  64. package/src/orchestrator/lesson-memory.ts +36 -11
  65. package/src/orchestrator/orchestration-logger.ts +53 -24
  66. package/src/orchestrator/phase.ts +8 -4
  67. package/src/orchestrator/progress.ts +63 -0
  68. package/src/orchestrator/state.ts +79 -17
  69. package/src/projects/database.ts +47 -0
  70. package/src/projects/repository.ts +264 -0
  71. package/src/projects/resolve.ts +301 -0
  72. package/src/projects/schemas.ts +30 -0
  73. package/src/projects/types.ts +12 -0
  74. package/src/review/memory.ts +39 -11
  75. package/src/review/parse-findings.ts +116 -0
  76. package/src/review/pipeline.ts +3 -107
  77. package/src/review/selection.ts +38 -4
  78. package/src/scoring/time-provider.ts +23 -0
  79. package/src/tools/doctor.ts +28 -4
  80. package/src/tools/forensics.ts +7 -12
  81. package/src/tools/logs.ts +38 -11
  82. package/src/tools/memory-preferences.ts +157 -0
  83. package/src/tools/memory-status.ts +17 -96
  84. package/src/tools/orchestrate.ts +108 -90
  85. package/src/tools/pipeline-report.ts +3 -2
  86. package/src/tools/quick.ts +2 -2
  87. package/src/tools/replay.ts +42 -0
  88. package/src/tools/review.ts +46 -7
  89. package/src/tools/session-stats.ts +3 -2
  90. package/src/tools/summary.ts +43 -0
  91. package/src/utils/paths.ts +20 -1
  92. package/src/utils/random.ts +33 -0
  93. package/src/ux/session-summary.ts +56 -0
@@ -5,6 +5,8 @@
5
5
  * agents with non-empty relevantStacks require at least one match.
6
6
  */
7
7
 
8
+ import { createSeededRandom, deterministicShuffle } from "../utils/random";
9
+
8
10
  /** Minimal agent shape needed for selection (compatible with ReviewAgent from agents/). */
9
11
  interface SelectableAgent {
10
12
  readonly name: string;
@@ -21,6 +23,13 @@ export interface DiffAnalysisInput {
21
23
  readonly fileCount: number;
22
24
  }
23
25
 
26
+ export interface SelectionOptions {
27
+ /** Seed for reproducible agent ordering. If omitted, uses a fixed default. */
28
+ readonly seed?: string;
29
+ /** Maximum number of gated agents to select. Universal agents are always included. */
30
+ readonly limit?: number;
31
+ }
32
+
24
33
  export interface SelectionResult {
25
34
  readonly selected: readonly SelectableAgent[];
26
35
  readonly excluded: readonly { readonly agent: string; readonly reason: string }[];
@@ -32,25 +41,28 @@ export interface SelectionResult {
32
41
  * @param detectedStacks - Stack tags detected in the project (e.g., ["node", "typescript"])
33
42
  * @param diffAnalysis - Analysis of changed files
34
43
  * @param agents - All candidate agents
44
+ * @param options - Options for seeding and limiting the number of agents
35
45
  * @returns Frozen SelectionResult with selected and excluded lists
36
46
  */
37
47
  export function selectAgents(
38
48
  detectedStacks: readonly string[],
39
49
  _diffAnalysis: DiffAnalysisInput,
40
50
  agents: readonly SelectableAgent[],
51
+ options: SelectionOptions = {},
41
52
  ): SelectionResult {
42
53
  const stackSet = new Set(detectedStacks);
43
- const selected: SelectableAgent[] = [];
54
+ const universal: SelectableAgent[] = [];
55
+ const gatedCandidates: SelectableAgent[] = [];
44
56
  const excluded: { readonly agent: string; readonly reason: string }[] = [];
45
57
 
46
58
  for (const agent of agents) {
47
59
  // Pass 1: Stack gate
48
60
  if (agent.relevantStacks.length === 0) {
49
61
  // Universal agent -- always passes
50
- selected.push(agent);
62
+ universal.push(agent);
51
63
  } else if (agent.relevantStacks.some((s) => stackSet.has(s))) {
52
64
  // Gated agent with at least one matching stack
53
- selected.push(agent);
65
+ gatedCandidates.push(agent);
54
66
  } else {
55
67
  // Gated agent with no matching stack
56
68
  const stackList = detectedStacks.length > 0 ? detectedStacks.join(", ") : "none";
@@ -63,8 +75,30 @@ export function selectAgents(
63
75
  }
64
76
  }
65
77
 
78
+ const seed = options.seed ?? "default-selection-seed";
79
+ const rng = createSeededRandom(seed);
80
+
81
+ const shuffledGated = deterministicShuffle([...gatedCandidates], rng);
82
+ const finalGated =
83
+ options.limit !== undefined ? shuffledGated.slice(0, options.limit) : shuffledGated;
84
+
85
+ if (options.limit !== undefined && finalGated.length < shuffledGated.length) {
86
+ const dropped = shuffledGated.slice(options.limit);
87
+ for (const agent of dropped) {
88
+ excluded.push(
89
+ Object.freeze({
90
+ agent: agent.name,
91
+ reason: `Diversity limit: dropped to meet limit of ${options.limit}`,
92
+ }),
93
+ );
94
+ }
95
+ }
96
+
97
+ const combined = [...universal, ...finalGated];
98
+ deterministicShuffle(combined, rng);
99
+
66
100
  return Object.freeze({
67
- selected: Object.freeze(selected),
101
+ selected: Object.freeze(combined),
68
102
  excluded: Object.freeze(excluded),
69
103
  });
70
104
  }
@@ -0,0 +1,23 @@
1
+ export interface TimeProvider {
2
+ readonly now: () => number;
3
+ }
4
+
5
+ export const systemTimeProvider: TimeProvider = Object.freeze({
6
+ now: () => Date.now(),
7
+ });
8
+
9
+ export function createFixedTimeProvider(
10
+ initialTimeMs: number,
11
+ ): TimeProvider & { advance: (ms: number) => void; set: (ms: number) => void } {
12
+ let currentTime = initialTimeMs;
13
+
14
+ return Object.freeze({
15
+ now: () => currentTime,
16
+ advance: (ms: number) => {
17
+ currentTime += ms;
18
+ },
19
+ set: (ms: number) => {
20
+ currentTime = ms;
21
+ },
22
+ });
23
+ }
@@ -17,7 +17,7 @@ export function setOpenCodeConfig(config: Config | null): void {
17
17
  */
18
18
  interface DoctorCheck {
19
19
  readonly name: string;
20
- readonly status: "pass" | "fail";
20
+ readonly status: "pass" | "warn" | "fail";
21
21
  readonly message: string;
22
22
  readonly fixSuggestion: string | null;
23
23
  }
@@ -39,9 +39,33 @@ async function detectContractHealth(projectRoot?: string): Promise<ContractHealt
39
39
  const artifactDir = getProjectArtifactDir(projectRoot);
40
40
  const logPath = join(artifactDir, "orchestration.jsonl");
41
41
  const content = await readFile(logPath, "utf-8");
42
+ const entries = content
43
+ .split("\n")
44
+ .map((line) => line.trim())
45
+ .filter(Boolean)
46
+ .map((line) => {
47
+ try {
48
+ return JSON.parse(line) as Record<string, unknown>;
49
+ } catch {
50
+ return null;
51
+ }
52
+ })
53
+ .filter((entry): entry is Record<string, unknown> => entry !== null);
54
+ const legacyTasksFallbackSeen =
55
+ entries.some(
56
+ (entry) =>
57
+ typeof entry.message === "string" &&
58
+ entry.message.includes("PLAN fallback: parsed legacy tasks.md"),
59
+ ) || content.includes("PLAN fallback: parsed legacy tasks.md");
60
+ const legacyResultParserSeen =
61
+ entries.some(
62
+ (entry) =>
63
+ typeof entry.message === "string" &&
64
+ entry.message.includes("Legacy result parser path used"),
65
+ ) || content.includes("Legacy result parser path used");
42
66
  return {
43
- legacyTasksFallbackSeen: content.includes("PLAN fallback: parsed legacy tasks.md"),
44
- legacyResultParserSeen: content.includes("Legacy result parser path used"),
67
+ legacyTasksFallbackSeen,
68
+ legacyResultParserSeen,
45
69
  };
46
70
  } catch {
47
71
  return {
@@ -96,7 +120,7 @@ function formatCheck(result: HealthResult): DoctorCheck {
96
120
  */
97
121
  function buildDisplayText(checks: readonly DoctorCheck[], duration: number): string {
98
122
  const lines = checks.map((c) => {
99
- const icon = c.status === "pass" ? "OK" : "FAIL";
123
+ const icon = c.status === "pass" ? "OK" : c.status === "warn" ? "WARN" : "FAIL";
100
124
  const line = `[${icon}] ${c.name}: ${c.message}`;
101
125
  return c.fixSuggestion ? `${line}\n Fix: ${c.fixSuggestion}` : line;
102
126
  });
@@ -1,6 +1,5 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
1
  import { tool } from "@opencode-ai/plugin";
2
+ import { readForensicEvents } from "../observability/forensic-log";
4
3
  import { loadState } from "../orchestrator/state";
5
4
  import { getProjectArtifactDir } from "../utils/paths";
6
5
 
@@ -23,16 +22,12 @@ function getSuggestedAction(failedPhase: string, recoverable: boolean): "resume"
23
22
  return "resume";
24
23
  }
25
24
 
26
- async function readRecentContractEvents(artifactDir: string): Promise<readonly string[]> {
25
+ async function readRecentContractEvents(projectRoot: string): Promise<readonly string[]> {
27
26
  try {
28
- const raw = await readFile(join(artifactDir, "orchestration.jsonl"), "utf-8");
29
- const lines = raw
30
- .split("\n")
31
- .map((line) => line.trim())
32
- .filter(Boolean)
33
- .slice(-120);
27
+ const events = (await readForensicEvents(projectRoot)).slice(-120);
34
28
  const codes = new Set<string>();
35
- for (const line of lines) {
29
+ for (const event of events) {
30
+ const searchable = `${String(event.code ?? "")} ${String(event.message ?? "")} ${JSON.stringify(event.payload ?? {})}`;
36
31
  for (const code of [
37
32
  "E_INVALID_RESULT",
38
33
  "E_STALE_RESULT",
@@ -42,7 +37,7 @@ async function readRecentContractEvents(artifactDir: string): Promise<readonly s
42
37
  "E_BUILD_TASK_ID_REQUIRED",
43
38
  "E_BUILD_UNKNOWN_TASK",
44
39
  ]) {
45
- if (line.includes(code)) {
40
+ if (searchable.includes(code)) {
46
41
  codes.add(code);
47
42
  }
48
43
  }
@@ -98,7 +93,7 @@ export async function forensicsCore(
98
93
  const recoverable = isRecoverable(failureContext.failedPhase);
99
94
  const suggestedAction = getSuggestedAction(failureContext.failedPhase, recoverable);
100
95
  const phasesCompleted = state.phases.filter((p) => p.status === "DONE").map((p) => p.name);
101
- const deterministicErrorCodes = await readRecentContractEvents(artifactDir);
96
+ const deterministicErrorCodes = await readRecentContractEvents(projectRoot);
102
97
 
103
98
  return JSON.stringify({
104
99
  failedPhase: failureContext.failedPhase,
package/src/tools/logs.ts CHANGED
@@ -22,14 +22,14 @@ import {
22
22
  } from "../observability/log-reader";
23
23
  import { generateSessionSummary } from "../observability/summary-generator";
24
24
 
25
- /**
26
- * Options for logsCore search/detail modes.
27
- */
28
25
  interface LogsOptions {
29
26
  readonly sessionID?: string;
30
27
  readonly eventType?: string;
31
28
  readonly after?: string;
32
29
  readonly before?: string;
30
+ readonly domain?: string;
31
+ readonly subsystem?: string;
32
+ readonly severity?: string;
33
33
  }
34
34
 
35
35
  /**
@@ -79,9 +79,10 @@ export async function logsCore(
79
79
  options?: LogsOptions,
80
80
  logsDir?: string,
81
81
  ): Promise<string> {
82
+ const logsRoot = logsDir ?? process.cwd();
82
83
  switch (mode) {
83
84
  case "list": {
84
- const sessions = await listSessionLogs(logsDir);
85
+ const sessions = await listSessionLogs(logsRoot);
85
86
 
86
87
  return JSON.stringify({
87
88
  action: "logs_list",
@@ -92,8 +93,8 @@ export async function logsCore(
92
93
 
93
94
  case "detail": {
94
95
  const log = options?.sessionID
95
- ? await readSessionLog(options.sessionID, logsDir)
96
- : await readLatestSessionLog(logsDir);
96
+ ? await readSessionLog(options.sessionID, logsRoot)
97
+ : await readLatestSessionLog(logsRoot);
97
98
 
98
99
  if (!log) {
99
100
  const target = options?.sessionID
@@ -117,8 +118,8 @@ export async function logsCore(
117
118
 
118
119
  case "search": {
119
120
  const log = options?.sessionID
120
- ? await readSessionLog(options.sessionID, logsDir)
121
- : await readLatestSessionLog(logsDir);
121
+ ? await readSessionLog(options.sessionID, logsRoot)
122
+ : await readLatestSessionLog(logsRoot);
122
123
 
123
124
  if (!log) {
124
125
  const target = options?.sessionID
@@ -134,6 +135,9 @@ export async function logsCore(
134
135
  type: options?.eventType,
135
136
  after: options?.after,
136
137
  before: options?.before,
138
+ domain: options?.domain,
139
+ subsystem: options?.subsystem,
140
+ severity: options?.severity,
137
141
  });
138
142
 
139
143
  const displayLines = [
@@ -145,6 +149,15 @@ export async function logsCore(
145
149
  return JSON.stringify({
146
150
  action: "logs_search",
147
151
  sessionId: log.sessionId,
152
+ filters: {
153
+ eventType: options?.eventType,
154
+ after: options?.after,
155
+ before: options?.before,
156
+ domain: options?.domain,
157
+ subsystem: options?.subsystem,
158
+ severity: options?.severity,
159
+ },
160
+ matchCount: filtered.length,
148
161
  events: filtered,
149
162
  displayText: displayLines.join("\n"),
150
163
  });
@@ -157,7 +170,7 @@ export async function logsCore(
157
170
  export const ocLogs = tool({
158
171
  description:
159
172
  "View session logs. Modes: 'list' shows all sessions, 'detail' shows full log with " +
160
- "summary, 'search' filters events by type/time. Use to inspect session history and errors.",
173
+ "summary, 'search' filters events by type/time/domain/subsystem/severity. Use to inspect session history and errors.",
161
174
  args: {
162
175
  mode: z.enum(["list", "detail", "search"]).describe("View mode: list, detail, or search"),
163
176
  sessionID: z
@@ -171,8 +184,22 @@ export const ocLogs = tool({
171
184
  .string()
172
185
  .optional()
173
186
  .describe("Only events before this ISO timestamp (for search mode)"),
187
+ domain: z
188
+ .string()
189
+ .optional()
190
+ .describe("Filter events by domain (e.g. 'session', 'orchestrator') (for search mode)"),
191
+ subsystem: z
192
+ .string()
193
+ .optional()
194
+ .describe("Filter events by payload.subsystem field (for search mode)"),
195
+ severity: z
196
+ .string()
197
+ .optional()
198
+ .describe(
199
+ "Filter by severity: matches event.type (e.g. 'error', 'warning') or payload.severity/payload.level (for search mode)",
200
+ ),
174
201
  },
175
- async execute({ mode, sessionID, eventType, after, before }) {
176
- return logsCore(mode, { sessionID, eventType, after, before });
202
+ async execute({ mode, sessionID, eventType, after, before, domain, subsystem, severity }) {
203
+ return logsCore(mode, { sessionID, eventType, after, before, domain, subsystem, severity });
177
204
  },
178
205
  });
@@ -0,0 +1,157 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { tool } from "@opencode-ai/plugin";
3
+ import {
4
+ deletePreferenceRecord,
5
+ deletePreferencesByKey,
6
+ getMemoryDb,
7
+ prunePreferenceEvidence,
8
+ prunePreferences,
9
+ } from "../memory";
10
+ import { resolveProjectIdentitySync } from "../projects/resolve";
11
+
12
+ type PreferenceScopeArg = "global" | "project";
13
+ type PreferenceStatusArg = "candidate" | "confirmed" | "rejected" | "unconfirmed" | "any";
14
+
15
+ interface MemoryPreferencesArgs {
16
+ readonly subcommand: "delete" | "prune" | "prune-evidence";
17
+ readonly id?: string;
18
+ readonly key?: string;
19
+ readonly scope?: PreferenceScopeArg;
20
+ readonly olderThanDays?: number;
21
+ readonly status?: PreferenceStatusArg;
22
+ readonly keepLatestPerPreference?: number;
23
+ }
24
+
25
+ function resolveProjectId(projectRoot: string, db: Database): string | null {
26
+ const resolved = resolveProjectIdentitySync(projectRoot, {
27
+ db,
28
+ allowCreate: false,
29
+ });
30
+ return resolved.id.startsWith("project:") ? null : resolved.id;
31
+ }
32
+
33
+ export function memoryPreferencesCore(
34
+ args: MemoryPreferencesArgs,
35
+ projectRoot: string,
36
+ db?: Database,
37
+ ): string {
38
+ try {
39
+ const resolvedDb = db ?? getMemoryDb();
40
+ const scope = args.scope ?? "global";
41
+ const projectId = scope === "project" ? resolveProjectId(projectRoot, resolvedDb) : null;
42
+
43
+ if (scope === "project" && projectId === null) {
44
+ return JSON.stringify({
45
+ error: "no_project_preferences",
46
+ message: "No known project identity for current directory.",
47
+ });
48
+ }
49
+
50
+ switch (args.subcommand) {
51
+ case "delete": {
52
+ if (typeof args.id === "string" && args.id.trim().length > 0) {
53
+ return JSON.stringify({
54
+ ok: true,
55
+ subcommand: "delete",
56
+ result: deletePreferenceRecord(args.id, resolvedDb),
57
+ });
58
+ }
59
+ if (typeof args.key === "string" && args.key.trim().length > 0) {
60
+ return JSON.stringify({
61
+ ok: true,
62
+ subcommand: "delete",
63
+ result: deletePreferencesByKey(args.key, { scope, projectId }, resolvedDb),
64
+ });
65
+ }
66
+ return JSON.stringify({
67
+ error: "id_or_key_required",
68
+ message: "delete requires either id or key.",
69
+ });
70
+ }
71
+
72
+ case "prune": {
73
+ if (typeof args.olderThanDays !== "number" || args.olderThanDays <= 0) {
74
+ return JSON.stringify({
75
+ error: "older_than_days_required",
76
+ message: "prune requires olderThanDays > 0.",
77
+ });
78
+ }
79
+ return JSON.stringify({
80
+ ok: true,
81
+ subcommand: "prune",
82
+ result: prunePreferences(
83
+ {
84
+ olderThanDays: args.olderThanDays,
85
+ scope,
86
+ projectId,
87
+ status: args.status ?? "unconfirmed",
88
+ },
89
+ resolvedDb,
90
+ ),
91
+ });
92
+ }
93
+
94
+ case "prune-evidence": {
95
+ if (typeof args.olderThanDays !== "number" || args.olderThanDays <= 0) {
96
+ return JSON.stringify({
97
+ error: "older_than_days_required",
98
+ message: "prune-evidence requires olderThanDays > 0.",
99
+ });
100
+ }
101
+ return JSON.stringify({
102
+ ok: true,
103
+ subcommand: "prune-evidence",
104
+ result: prunePreferenceEvidence(
105
+ {
106
+ olderThanDays: args.olderThanDays,
107
+ keepLatestPerPreference: args.keepLatestPerPreference,
108
+ scope,
109
+ projectId,
110
+ status: args.status ?? "any",
111
+ },
112
+ resolvedDb,
113
+ ),
114
+ });
115
+ }
116
+ }
117
+ } catch (error: unknown) {
118
+ return JSON.stringify({
119
+ error: error instanceof Error ? error.message : String(error),
120
+ });
121
+ }
122
+ }
123
+
124
+ export const ocMemoryPreferences = tool({
125
+ description:
126
+ "Manage learned preferences. Supports delete by id/key and pruning stale preference records or evidence.",
127
+ args: {
128
+ subcommand: tool.schema
129
+ .enum(["delete", "prune", "prune-evidence"])
130
+ .describe("Preference maintenance operation"),
131
+ id: tool.schema.string().optional().describe("Preference record id for delete"),
132
+ key: tool.schema.string().optional().describe("Preference key for delete"),
133
+ scope: tool.schema
134
+ .enum(["global", "project"])
135
+ .default("global")
136
+ .describe("Preference scope filter"),
137
+ olderThanDays: tool.schema
138
+ .number()
139
+ .int()
140
+ .positive()
141
+ .optional()
142
+ .describe("Delete records/evidence older than this many days"),
143
+ status: tool.schema
144
+ .enum(["candidate", "confirmed", "rejected", "unconfirmed", "any"])
145
+ .optional()
146
+ .describe("Preference status filter for prune operations"),
147
+ keepLatestPerPreference: tool.schema
148
+ .number()
149
+ .int()
150
+ .min(0)
151
+ .optional()
152
+ .describe("For prune-evidence, keep this many newest evidence rows per preference"),
153
+ },
154
+ async execute(args) {
155
+ return memoryPreferencesCore(args, process.cwd());
156
+ },
157
+ });
@@ -2,20 +2,15 @@
2
2
  * oc_memory_status tool — inspect memory system state.
3
3
  *
4
4
  * Shows observation counts, storage size, recent observations,
5
- * preferences, and per-type breakdowns. Follows the *Core + tool()
6
- * pattern from create-agent.ts.
5
+ * preferences, and per-type breakdowns through the shared inspection
6
+ * query layer.
7
7
  *
8
8
  * @module
9
9
  */
10
10
 
11
- import { Database } from "bun:sqlite";
12
- import { statSync } from "node:fs";
13
- import { join } from "node:path";
11
+ import type { Database } from "bun:sqlite";
14
12
  import { tool } from "@opencode-ai/plugin";
15
- import { DB_FILE, MEMORY_DIR, OBSERVATION_TYPES } from "../memory/constants";
16
- import { getMemoryDb } from "../memory/database";
17
- import { getAllPreferences } from "../memory/repository";
18
- import { getGlobalConfigDir } from "../utils/paths";
13
+ import { getMemoryOverview } from "../inspect/repository";
19
14
 
20
15
  interface MemoryStatusResult {
21
16
  readonly stats: {
@@ -47,94 +42,22 @@ export function memoryStatusCore(
47
42
  _args: { readonly detail?: "summary" | "full" },
48
43
  dbOrPath?: Database | string,
49
44
  ): MemoryStatusResult {
50
- let ownedDb: Database | null = null;
51
45
  try {
52
- if (typeof dbOrPath === "string") {
53
- ownedDb = new Database(dbOrPath);
54
- }
55
- const db = dbOrPath instanceof Database ? dbOrPath : (ownedDb ?? getMemoryDb());
56
-
57
- // Count observations
58
- const obsCountRow = db.query("SELECT COUNT(*) as cnt FROM observations").get() as {
59
- cnt: number;
60
- };
61
- const totalObservations = obsCountRow.cnt;
62
-
63
- // Count by type
64
- const typeRows = db
65
- .query("SELECT type, COUNT(*) as cnt FROM observations GROUP BY type")
66
- .all() as Array<{ type: string; cnt: number }>;
67
-
68
- const observationsByType: Record<string, number> = {};
69
- for (const t of OBSERVATION_TYPES) {
70
- observationsByType[t] = 0;
71
- }
72
- for (const row of typeRows) {
73
- observationsByType[row.type] = row.cnt;
74
- }
75
-
76
- // Count projects
77
- const projCountRow = db.query("SELECT COUNT(*) as cnt FROM projects").get() as {
78
- cnt: number;
79
- };
80
- const totalProjects = projCountRow.cnt;
81
-
82
- // Count preferences
83
- const prefCountRow = db.query("SELECT COUNT(*) as cnt FROM preferences").get() as {
84
- cnt: number;
85
- };
86
- const totalPreferences = prefCountRow.cnt;
87
-
88
- // Storage size — derive from actual DB path, not always the global default
89
- let storageSizeKb = 0;
90
- try {
91
- const statPath =
92
- typeof dbOrPath === "string" && dbOrPath !== ":memory:"
93
- ? dbOrPath
94
- : join(getGlobalConfigDir(), MEMORY_DIR, DB_FILE);
95
- const stat = statSync(statPath);
96
- storageSizeKb = Math.round(stat.size / 1024);
97
- } catch {
98
- // DB might be in-memory or path doesn't exist
99
- }
100
-
101
- // Recent observations (last 10)
102
- const recentRows = db
103
- .query(
104
- "SELECT type, summary, created_at, confidence FROM observations ORDER BY created_at DESC LIMIT 10",
105
- )
106
- .all() as Array<{
107
- type: string;
108
- summary: string;
109
- created_at: string;
110
- confidence: number;
111
- }>;
112
-
113
- const recentObservations = recentRows.map((row) => ({
114
- type: row.type,
115
- summary: row.summary,
116
- createdAt: row.created_at,
117
- confidence: row.confidence,
118
- }));
119
-
120
- // All preferences
121
- const allPrefs = getAllPreferences(db);
122
- const preferences = allPrefs.map((p) => ({
123
- key: p.key,
124
- value: p.value,
125
- confidence: p.confidence,
126
- }));
46
+ const overview = getMemoryOverview(dbOrPath);
127
47
 
128
48
  return {
129
- stats: {
130
- totalObservations,
131
- totalProjects,
132
- totalPreferences,
133
- storageSizeKb,
134
- observationsByType,
135
- },
136
- recentObservations,
137
- preferences,
49
+ stats: overview.stats,
50
+ recentObservations: overview.recentObservations.map((row) => ({
51
+ type: row.type,
52
+ summary: row.summary,
53
+ createdAt: row.createdAt,
54
+ confidence: row.confidence,
55
+ })),
56
+ preferences: overview.preferences.map((row) => ({
57
+ key: row.key,
58
+ value: row.value,
59
+ confidence: row.confidence,
60
+ })),
138
61
  };
139
62
  } catch (err) {
140
63
  const detail = err instanceof Error ? err.message : String(err);
@@ -144,8 +67,6 @@ export function memoryStatusCore(
144
67
  preferences: [],
145
68
  error: `Memory system error: ${detail}`,
146
69
  };
147
- } finally {
148
- ownedDb?.close();
149
70
  }
150
71
  }
151
72