@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
package/src/review/selection.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|
package/src/tools/doctor.ts
CHANGED
|
@@ -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
|
|
44
|
-
legacyResultParserSeen
|
|
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
|
});
|
package/src/tools/forensics.ts
CHANGED
|
@@ -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(
|
|
25
|
+
async function readRecentContractEvents(projectRoot: string): Promise<readonly string[]> {
|
|
27
26
|
try {
|
|
28
|
-
const
|
|
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
|
|
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 (
|
|
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(
|
|
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(
|
|
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,
|
|
96
|
-
: await readLatestSessionLog(
|
|
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,
|
|
121
|
-
: await readLatestSessionLog(
|
|
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
|
|
6
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|