@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.
- 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/health/checks.ts +29 -4
- package/src/index.ts +103 -11
- 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/types.ts +66 -0
- package/src/memory/capture.ts +221 -25
- package/src/memory/database.ts +74 -12
- package/src/memory/index.ts +17 -1
- package/src/memory/project-key.ts +6 -0
- package/src/memory/repository.ts +833 -42
- package/src/memory/retrieval.ts +83 -169
- package/src/memory/schemas.ts +39 -7
- package/src/memory/types.ts +4 -0
- package/src/observability/event-handlers.ts +28 -17
- package/src/observability/event-store.ts +29 -1
- package/src/observability/forensic-log.ts +159 -0
- package/src/observability/forensic-schemas.ts +69 -0
- package/src/observability/forensic-types.ts +10 -0
- package/src/observability/index.ts +21 -27
- package/src/observability/log-reader.ts +142 -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/fallback/event-handler.ts +47 -3
- package/src/orchestrator/handlers/architect.ts +2 -1
- package/src/orchestrator/handlers/build.ts +55 -97
- package/src/orchestrator/handlers/retrospective.ts +2 -1
- package/src/orchestrator/handlers/types.ts +0 -1
- package/src/orchestrator/lesson-memory.ts +29 -9
- package/src/orchestrator/orchestration-logger.ts +37 -23
- package/src/orchestrator/phase.ts +8 -4
- 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 +29 -9
- package/src/tools/doctor.ts +26 -2
- package/src/tools/forensics.ts +7 -12
- package/src/tools/logs.ts +6 -5
- package/src/tools/memory-preferences.ts +157 -0
- package/src/tools/memory-status.ts +17 -96
- package/src/tools/orchestrate.ts +97 -81
- package/src/tools/pipeline-report.ts +3 -2
- package/src/tools/quick.ts +2 -2
- package/src/tools/review.ts +39 -6
- package/src/tools/session-stats.ts +3 -2
- package/src/utils/paths.ts +20 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export const KERNEL_SCHEMA_VERSION = 1;
|
|
2
|
+
|
|
3
|
+
export const KERNEL_SCHEMA_STATEMENTS: readonly string[] = Object.freeze([
|
|
4
|
+
`CREATE TABLE IF NOT EXISTS pipeline_runs (
|
|
5
|
+
project_id TEXT NOT NULL,
|
|
6
|
+
run_id TEXT PRIMARY KEY,
|
|
7
|
+
schema_version INTEGER NOT NULL,
|
|
8
|
+
status TEXT NOT NULL,
|
|
9
|
+
current_phase TEXT,
|
|
10
|
+
idea TEXT NOT NULL,
|
|
11
|
+
state_revision INTEGER NOT NULL,
|
|
12
|
+
started_at TEXT NOT NULL,
|
|
13
|
+
last_updated_at TEXT NOT NULL,
|
|
14
|
+
failure_phase TEXT,
|
|
15
|
+
failure_agent TEXT,
|
|
16
|
+
failure_message TEXT,
|
|
17
|
+
last_successful_phase TEXT,
|
|
18
|
+
state_json TEXT NOT NULL,
|
|
19
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
20
|
+
)`,
|
|
21
|
+
`CREATE INDEX IF NOT EXISTS idx_pipeline_runs_project_updated_at ON pipeline_runs(project_id, last_updated_at DESC, run_id DESC)`,
|
|
22
|
+
`CREATE TABLE IF NOT EXISTS run_phases (
|
|
23
|
+
run_id TEXT NOT NULL,
|
|
24
|
+
phase_name TEXT NOT NULL,
|
|
25
|
+
status TEXT NOT NULL,
|
|
26
|
+
completed_at TEXT,
|
|
27
|
+
confidence TEXT,
|
|
28
|
+
PRIMARY KEY (run_id, phase_name),
|
|
29
|
+
FOREIGN KEY (run_id) REFERENCES pipeline_runs(run_id) ON DELETE CASCADE
|
|
30
|
+
)`,
|
|
31
|
+
`CREATE TABLE IF NOT EXISTS run_tasks (
|
|
32
|
+
run_id TEXT NOT NULL,
|
|
33
|
+
task_id INTEGER NOT NULL,
|
|
34
|
+
title TEXT NOT NULL,
|
|
35
|
+
status TEXT NOT NULL,
|
|
36
|
+
wave INTEGER NOT NULL,
|
|
37
|
+
depends_on_json TEXT NOT NULL,
|
|
38
|
+
attempt INTEGER NOT NULL,
|
|
39
|
+
strike INTEGER NOT NULL,
|
|
40
|
+
PRIMARY KEY (run_id, task_id),
|
|
41
|
+
FOREIGN KEY (run_id) REFERENCES pipeline_runs(run_id) ON DELETE CASCADE
|
|
42
|
+
)`,
|
|
43
|
+
`CREATE TABLE IF NOT EXISTS run_pending_dispatches (
|
|
44
|
+
run_id TEXT NOT NULL,
|
|
45
|
+
dispatch_id TEXT NOT NULL,
|
|
46
|
+
phase TEXT NOT NULL,
|
|
47
|
+
agent TEXT NOT NULL,
|
|
48
|
+
issued_at TEXT NOT NULL,
|
|
49
|
+
result_kind TEXT NOT NULL,
|
|
50
|
+
task_id INTEGER,
|
|
51
|
+
PRIMARY KEY (run_id, dispatch_id),
|
|
52
|
+
FOREIGN KEY (run_id) REFERENCES pipeline_runs(run_id) ON DELETE CASCADE
|
|
53
|
+
)`,
|
|
54
|
+
`CREATE TABLE IF NOT EXISTS run_processed_results (
|
|
55
|
+
run_id TEXT NOT NULL,
|
|
56
|
+
result_id TEXT NOT NULL,
|
|
57
|
+
PRIMARY KEY (run_id, result_id),
|
|
58
|
+
FOREIGN KEY (run_id) REFERENCES pipeline_runs(run_id) ON DELETE CASCADE
|
|
59
|
+
)`,
|
|
60
|
+
`CREATE TABLE IF NOT EXISTS active_review_state (
|
|
61
|
+
project_id TEXT PRIMARY KEY,
|
|
62
|
+
stage INTEGER NOT NULL,
|
|
63
|
+
scope TEXT NOT NULL,
|
|
64
|
+
started_at TEXT NOT NULL,
|
|
65
|
+
saved_at TEXT NOT NULL,
|
|
66
|
+
state_json TEXT NOT NULL,
|
|
67
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
68
|
+
)`,
|
|
69
|
+
`CREATE TABLE IF NOT EXISTS project_review_memory (
|
|
70
|
+
project_id TEXT PRIMARY KEY,
|
|
71
|
+
schema_version INTEGER NOT NULL,
|
|
72
|
+
last_reviewed_at TEXT,
|
|
73
|
+
state_json TEXT NOT NULL,
|
|
74
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
75
|
+
)`,
|
|
76
|
+
`CREATE TABLE IF NOT EXISTS project_lesson_memory (
|
|
77
|
+
project_id TEXT PRIMARY KEY,
|
|
78
|
+
schema_version INTEGER NOT NULL,
|
|
79
|
+
last_updated_at TEXT,
|
|
80
|
+
state_json TEXT NOT NULL,
|
|
81
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
82
|
+
)`,
|
|
83
|
+
`CREATE TABLE IF NOT EXISTS project_lessons (
|
|
84
|
+
lesson_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
85
|
+
project_id TEXT NOT NULL,
|
|
86
|
+
content TEXT NOT NULL,
|
|
87
|
+
domain TEXT NOT NULL,
|
|
88
|
+
extracted_at TEXT NOT NULL,
|
|
89
|
+
source_phase TEXT NOT NULL,
|
|
90
|
+
last_updated_at TEXT,
|
|
91
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
|
92
|
+
UNIQUE(project_id, extracted_at, domain, source_phase, content)
|
|
93
|
+
)`,
|
|
94
|
+
`CREATE INDEX IF NOT EXISTS idx_project_lessons_project_extracted_at ON project_lessons(project_id, extracted_at DESC, lesson_id DESC)`,
|
|
95
|
+
`CREATE INDEX IF NOT EXISTS idx_project_lessons_domain ON project_lessons(project_id, domain, extracted_at DESC, lesson_id DESC)`,
|
|
96
|
+
`CREATE TABLE IF NOT EXISTS forensic_events (
|
|
97
|
+
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
98
|
+
project_id TEXT NOT NULL,
|
|
99
|
+
schema_version INTEGER NOT NULL,
|
|
100
|
+
timestamp TEXT NOT NULL,
|
|
101
|
+
project_root TEXT NOT NULL,
|
|
102
|
+
domain TEXT NOT NULL,
|
|
103
|
+
run_id TEXT,
|
|
104
|
+
session_id TEXT,
|
|
105
|
+
parent_session_id TEXT,
|
|
106
|
+
phase TEXT,
|
|
107
|
+
dispatch_id TEXT,
|
|
108
|
+
task_id INTEGER,
|
|
109
|
+
agent TEXT,
|
|
110
|
+
type TEXT NOT NULL,
|
|
111
|
+
code TEXT,
|
|
112
|
+
message TEXT,
|
|
113
|
+
payload_json TEXT NOT NULL,
|
|
114
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
115
|
+
)`,
|
|
116
|
+
`CREATE INDEX IF NOT EXISTS idx_forensic_events_timestamp ON forensic_events(timestamp, event_id)`,
|
|
117
|
+
`CREATE INDEX IF NOT EXISTS idx_forensic_events_project ON forensic_events(project_id, timestamp, event_id)`,
|
|
118
|
+
`CREATE INDEX IF NOT EXISTS idx_forensic_events_session ON forensic_events(session_id, timestamp, event_id)`,
|
|
119
|
+
`CREATE INDEX IF NOT EXISTS idx_forensic_events_run ON forensic_events(run_id, timestamp, event_id)`,
|
|
120
|
+
`CREATE INDEX IF NOT EXISTS idx_forensic_events_dispatch ON forensic_events(dispatch_id, timestamp, event_id)`,
|
|
121
|
+
`CREATE INDEX IF NOT EXISTS idx_forensic_events_type ON forensic_events(type, timestamp, event_id)`,
|
|
122
|
+
]);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ForensicEvent } from "../observability/forensic-types";
|
|
2
|
+
import type { LessonMemory } from "../orchestrator/lesson-types";
|
|
3
|
+
import type { PipelineState } from "../orchestrator/types";
|
|
4
|
+
import type { ReviewMemory, ReviewState } from "../review/types";
|
|
5
|
+
|
|
6
|
+
export const KERNEL_STATE_CONFLICT_CODE = "E_STATE_CONFLICT";
|
|
7
|
+
|
|
8
|
+
export interface PipelineRunRow {
|
|
9
|
+
readonly project_id: string;
|
|
10
|
+
readonly run_id: string;
|
|
11
|
+
readonly schema_version: number;
|
|
12
|
+
readonly status: PipelineState["status"];
|
|
13
|
+
readonly current_phase: string | null;
|
|
14
|
+
readonly idea: string;
|
|
15
|
+
readonly state_revision: number;
|
|
16
|
+
readonly started_at: string;
|
|
17
|
+
readonly last_updated_at: string;
|
|
18
|
+
readonly failure_phase: string | null;
|
|
19
|
+
readonly failure_agent: string | null;
|
|
20
|
+
readonly failure_message: string | null;
|
|
21
|
+
readonly last_successful_phase: string | null;
|
|
22
|
+
readonly state_json: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ActiveReviewStateRow {
|
|
26
|
+
readonly project_id: string;
|
|
27
|
+
readonly stage: ReviewState["stage"];
|
|
28
|
+
readonly scope: string;
|
|
29
|
+
readonly started_at: string;
|
|
30
|
+
readonly saved_at: string;
|
|
31
|
+
readonly state_json: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ProjectReviewMemoryRow {
|
|
35
|
+
readonly project_id: string;
|
|
36
|
+
readonly schema_version: number;
|
|
37
|
+
readonly last_reviewed_at: string | null;
|
|
38
|
+
readonly state_json: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ProjectLessonMemoryRow {
|
|
42
|
+
readonly project_id: string;
|
|
43
|
+
readonly schema_version: number;
|
|
44
|
+
readonly last_updated_at: string | null;
|
|
45
|
+
readonly state_json: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ForensicEventRow {
|
|
49
|
+
readonly event_id: number;
|
|
50
|
+
readonly project_id: string;
|
|
51
|
+
readonly schema_version: number;
|
|
52
|
+
readonly timestamp: string;
|
|
53
|
+
readonly project_root: string;
|
|
54
|
+
readonly domain: ForensicEvent["domain"];
|
|
55
|
+
readonly run_id: string | null;
|
|
56
|
+
readonly session_id: string | null;
|
|
57
|
+
readonly parent_session_id: string | null;
|
|
58
|
+
readonly phase: string | null;
|
|
59
|
+
readonly dispatch_id: string | null;
|
|
60
|
+
readonly task_id: number | null;
|
|
61
|
+
readonly agent: string | null;
|
|
62
|
+
readonly type: ForensicEvent["type"];
|
|
63
|
+
readonly code: string | null;
|
|
64
|
+
readonly message: string | null;
|
|
65
|
+
readonly payload_json: string;
|
|
66
|
+
}
|
package/src/memory/capture.ts
CHANGED
|
@@ -1,34 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Memory capture handlers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* are filtered out per Research Pitfall 4.
|
|
8
|
-
*
|
|
9
|
-
* Factory pattern matches createObservabilityEventHandler in
|
|
10
|
-
* src/observability/event-handlers.ts.
|
|
4
|
+
* Event capture remains a supporting path for project incidents and decisions.
|
|
5
|
+
* Explicit user preference capture happens on the chat.message hook where the
|
|
6
|
+
* actual outbound user-authored text parts are available.
|
|
11
7
|
*
|
|
12
8
|
* @module
|
|
13
9
|
*/
|
|
14
10
|
|
|
15
11
|
import type { Database } from "bun:sqlite";
|
|
16
12
|
import { basename } from "node:path";
|
|
13
|
+
import { resolveProjectIdentity } from "../projects/resolve";
|
|
17
14
|
import { pruneStaleObservations } from "./decay";
|
|
18
|
-
import {
|
|
19
|
-
import { insertObservation, upsertProject } from "./repository";
|
|
15
|
+
import { insertObservation, upsertPreferenceRecord, upsertProject } from "./repository";
|
|
20
16
|
import type { ObservationType } from "./types";
|
|
21
17
|
|
|
22
18
|
/**
|
|
23
|
-
* Dependencies for the memory capture
|
|
19
|
+
* Dependencies for the memory capture handlers.
|
|
24
20
|
*/
|
|
25
21
|
export interface MemoryCaptureDeps {
|
|
26
22
|
readonly getDb: () => Database;
|
|
27
23
|
readonly projectRoot: string;
|
|
28
24
|
}
|
|
29
25
|
|
|
26
|
+
interface PreferenceCandidate {
|
|
27
|
+
readonly key: string;
|
|
28
|
+
readonly value: string;
|
|
29
|
+
readonly scope: "global" | "project";
|
|
30
|
+
readonly confidence: number;
|
|
31
|
+
readonly statement: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
/**
|
|
31
|
-
* Events that produce
|
|
35
|
+
* Events that produce supporting observations.
|
|
32
36
|
*/
|
|
33
37
|
const CAPTURE_EVENT_TYPES = new Set([
|
|
34
38
|
"session.created",
|
|
@@ -38,6 +42,33 @@ const CAPTURE_EVENT_TYPES = new Set([
|
|
|
38
42
|
"app.phase_transition",
|
|
39
43
|
]);
|
|
40
44
|
|
|
45
|
+
const PROJECT_SCOPE_HINTS = [
|
|
46
|
+
"in this repo",
|
|
47
|
+
"for this repo",
|
|
48
|
+
"in this project",
|
|
49
|
+
"for this project",
|
|
50
|
+
"in this codebase",
|
|
51
|
+
"for this codebase",
|
|
52
|
+
"here ",
|
|
53
|
+
"this repo ",
|
|
54
|
+
"this project ",
|
|
55
|
+
] as const;
|
|
56
|
+
|
|
57
|
+
const EXPLICIT_PREFERENCE_PATTERNS = [
|
|
58
|
+
{
|
|
59
|
+
regex: /\b(?:please|do|always|generally)\s+(?:use|prefer|keep|run|avoid)\s+(.+?)(?:[.!?]|$)/i,
|
|
60
|
+
buildValue: (match: RegExpMatchArray) => match[1]?.trim() ?? "",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
regex: /\b(?:i|we)\s+(?:prefer|want|need|like)\s+(.+?)(?:[.!?]|$)/i,
|
|
64
|
+
buildValue: (match: RegExpMatchArray) => match[1]?.trim() ?? "",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
regex: /\b(?:don't|do not|never)\s+(.+?)(?:[.!?]|$)/i,
|
|
68
|
+
buildValue: (match: RegExpMatchArray) => `avoid ${match[1]?.trim() ?? ""}`,
|
|
69
|
+
},
|
|
70
|
+
] as const;
|
|
71
|
+
|
|
41
72
|
/**
|
|
42
73
|
* Extracts a session ID from event properties.
|
|
43
74
|
* Supports properties.sessionID, properties.info.id, properties.info.sessionID.
|
|
@@ -52,6 +83,107 @@ function extractSessionId(properties: Record<string, unknown>): string | undefin
|
|
|
52
83
|
return undefined;
|
|
53
84
|
}
|
|
54
85
|
|
|
86
|
+
function normalizePreferenceKey(value: string): string {
|
|
87
|
+
const normalized = value
|
|
88
|
+
.toLowerCase()
|
|
89
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
90
|
+
.trim()
|
|
91
|
+
.split(/\s+/)
|
|
92
|
+
.slice(0, 6)
|
|
93
|
+
.join(".");
|
|
94
|
+
return normalized.length > 0 ? normalized : "user.preference";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizePreferenceValue(value: string): string {
|
|
98
|
+
return value
|
|
99
|
+
.replace(/\s+/g, " ")
|
|
100
|
+
.trim()
|
|
101
|
+
.replace(/[.!?]+$/, "");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function inferPreferenceScope(text: string): "global" | "project" {
|
|
105
|
+
const lowerText = text.toLowerCase();
|
|
106
|
+
return PROJECT_SCOPE_HINTS.some((hint) => lowerText.includes(hint)) ? "project" : "global";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function extractTextPartContent(part: unknown): string | null {
|
|
110
|
+
if (part === null || typeof part !== "object") {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const record = part as Record<string, unknown>;
|
|
115
|
+
if (record.type !== "text") {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (typeof record.text === "string" && record.text.trim().length > 0) {
|
|
120
|
+
return record.text;
|
|
121
|
+
}
|
|
122
|
+
if (typeof record.content === "string" && record.content.trim().length > 0) {
|
|
123
|
+
return record.content;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extractExplicitPreferenceCandidates(
|
|
130
|
+
parts: readonly unknown[],
|
|
131
|
+
): readonly PreferenceCandidate[] {
|
|
132
|
+
const joinedText = parts
|
|
133
|
+
.map(extractTextPartContent)
|
|
134
|
+
.filter((value): value is string => value !== null)
|
|
135
|
+
.join("\n")
|
|
136
|
+
.trim();
|
|
137
|
+
if (joinedText.length === 0) {
|
|
138
|
+
return Object.freeze([]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const candidates: PreferenceCandidate[] = [];
|
|
142
|
+
const scope = inferPreferenceScope(joinedText);
|
|
143
|
+
const lines = joinedText
|
|
144
|
+
.split(/\n+/)
|
|
145
|
+
.flatMap((line) => line.split(/(?<=[.!?])\s+/))
|
|
146
|
+
.map((line) => line.trim())
|
|
147
|
+
.filter((line) => line.length > 0 && line.length <= 500);
|
|
148
|
+
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
for (const pattern of EXPLICIT_PREFERENCE_PATTERNS) {
|
|
151
|
+
const match = line.match(pattern.regex);
|
|
152
|
+
if (!match) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const value = normalizePreferenceValue(pattern.buildValue(match));
|
|
157
|
+
if (value.length < 6) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
candidates.push(
|
|
162
|
+
Object.freeze({
|
|
163
|
+
key: normalizePreferenceKey(value),
|
|
164
|
+
value,
|
|
165
|
+
scope,
|
|
166
|
+
confidence: 0.9,
|
|
167
|
+
statement: line,
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const seen = new Set<string>();
|
|
175
|
+
return Object.freeze(
|
|
176
|
+
candidates.filter((candidate) => {
|
|
177
|
+
const uniqueness = `${candidate.scope}:${candidate.key}:${candidate.value}`;
|
|
178
|
+
if (seen.has(uniqueness)) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
seen.add(uniqueness);
|
|
182
|
+
return true;
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
55
187
|
/**
|
|
56
188
|
* Safely truncate a string to maxLen characters.
|
|
57
189
|
*/
|
|
@@ -60,12 +192,7 @@ function truncate(s: string, maxLen: number): string {
|
|
|
60
192
|
}
|
|
61
193
|
|
|
62
194
|
/**
|
|
63
|
-
* Creates
|
|
64
|
-
*
|
|
65
|
-
* Returns an async function matching the event handler signature:
|
|
66
|
-
* `(input: { event: { type: string; [key: string]: unknown } }) => Promise<void>`
|
|
67
|
-
*
|
|
68
|
-
* Pure observer: never modifies the event or session output.
|
|
195
|
+
* Creates an event capture handler matching the plugin event hook signature.
|
|
69
196
|
*/
|
|
70
197
|
export function createMemoryCaptureHandler(deps: MemoryCaptureDeps) {
|
|
71
198
|
let currentSessionId: string | null = null;
|
|
@@ -110,7 +237,6 @@ export function createMemoryCaptureHandler(deps: MemoryCaptureDeps) {
|
|
|
110
237
|
? (rawProps as Record<string, unknown>)
|
|
111
238
|
: {};
|
|
112
239
|
|
|
113
|
-
// Skip noisy events early
|
|
114
240
|
if (!CAPTURE_EVENT_TYPES.has(event.type)) return;
|
|
115
241
|
|
|
116
242
|
switch (event.type) {
|
|
@@ -121,7 +247,10 @@ export function createMemoryCaptureHandler(deps: MemoryCaptureDeps) {
|
|
|
121
247
|
if (!info.id) return;
|
|
122
248
|
|
|
123
249
|
currentSessionId = info.id;
|
|
124
|
-
|
|
250
|
+
const resolvedProject = await resolveProjectIdentity(deps.projectRoot, {
|
|
251
|
+
db: deps.getDb(),
|
|
252
|
+
});
|
|
253
|
+
currentProjectKey = resolvedProject.id;
|
|
125
254
|
const projectName = basename(deps.projectRoot);
|
|
126
255
|
|
|
127
256
|
try {
|
|
@@ -130,6 +259,7 @@ export function createMemoryCaptureHandler(deps: MemoryCaptureDeps) {
|
|
|
130
259
|
id: currentProjectKey,
|
|
131
260
|
path: deps.projectRoot,
|
|
132
261
|
name: projectName,
|
|
262
|
+
firstSeenAt: resolvedProject.firstSeenAt,
|
|
133
263
|
lastUpdated: now(),
|
|
134
264
|
},
|
|
135
265
|
deps.getDb(),
|
|
@@ -144,12 +274,9 @@ export function createMemoryCaptureHandler(deps: MemoryCaptureDeps) {
|
|
|
144
274
|
const projectKey = currentProjectKey;
|
|
145
275
|
const db = deps.getDb();
|
|
146
276
|
|
|
147
|
-
// Reset state
|
|
148
277
|
currentSessionId = null;
|
|
149
278
|
currentProjectKey = null;
|
|
150
279
|
|
|
151
|
-
// Defer pruning to avoid blocking the session.deleted handler.
|
|
152
|
-
// Best-effort: will not run if the process exits before this microtask drains.
|
|
153
280
|
if (projectKey) {
|
|
154
281
|
queueMicrotask(() => {
|
|
155
282
|
try {
|
|
@@ -197,9 +324,8 @@ export function createMemoryCaptureHandler(deps: MemoryCaptureDeps) {
|
|
|
197
324
|
typeof properties.fromPhase === "string" ? properties.fromPhase : "unknown";
|
|
198
325
|
const toPhase = typeof properties.toPhase === "string" ? properties.toPhase : "unknown";
|
|
199
326
|
const content = `Phase transition: ${fromPhase} -> ${toPhase}`;
|
|
200
|
-
const summary = content;
|
|
201
327
|
|
|
202
|
-
safeInsert("pattern", content,
|
|
328
|
+
safeInsert("pattern", content, content, 0.6);
|
|
203
329
|
return;
|
|
204
330
|
}
|
|
205
331
|
|
|
@@ -208,3 +334,73 @@ export function createMemoryCaptureHandler(deps: MemoryCaptureDeps) {
|
|
|
208
334
|
}
|
|
209
335
|
};
|
|
210
336
|
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Creates a chat.message capture handler that records explicit user preferences.
|
|
340
|
+
*/
|
|
341
|
+
export function createMemoryChatMessageHandler(deps: MemoryCaptureDeps) {
|
|
342
|
+
return async (
|
|
343
|
+
input: { readonly sessionID: string },
|
|
344
|
+
output: { readonly parts: unknown[] },
|
|
345
|
+
): Promise<void> => {
|
|
346
|
+
try {
|
|
347
|
+
const candidates = extractExplicitPreferenceCandidates(output.parts);
|
|
348
|
+
if (candidates.length === 0) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const resolvedProject = await resolveProjectIdentity(deps.projectRoot, {
|
|
353
|
+
db: deps.getDb(),
|
|
354
|
+
});
|
|
355
|
+
const projectName = basename(deps.projectRoot);
|
|
356
|
+
const timestamp = new Date().toISOString();
|
|
357
|
+
|
|
358
|
+
upsertProject(
|
|
359
|
+
{
|
|
360
|
+
id: resolvedProject.id,
|
|
361
|
+
path: deps.projectRoot,
|
|
362
|
+
name: projectName,
|
|
363
|
+
firstSeenAt: resolvedProject.firstSeenAt,
|
|
364
|
+
lastUpdated: timestamp,
|
|
365
|
+
},
|
|
366
|
+
deps.getDb(),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
for (const candidate of candidates) {
|
|
370
|
+
upsertPreferenceRecord(
|
|
371
|
+
{
|
|
372
|
+
key: candidate.key,
|
|
373
|
+
value: candidate.value,
|
|
374
|
+
scope: candidate.scope,
|
|
375
|
+
projectId: candidate.scope === "project" ? resolvedProject.id : null,
|
|
376
|
+
status: "confirmed",
|
|
377
|
+
confidence: candidate.confidence,
|
|
378
|
+
sourceSession: input.sessionID,
|
|
379
|
+
createdAt: timestamp,
|
|
380
|
+
lastUpdated: timestamp,
|
|
381
|
+
evidence: [
|
|
382
|
+
{
|
|
383
|
+
sessionId: input.sessionID,
|
|
384
|
+
statement: candidate.statement,
|
|
385
|
+
confidence: candidate.confidence,
|
|
386
|
+
confirmed: true,
|
|
387
|
+
createdAt: timestamp,
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
},
|
|
391
|
+
deps.getDb(),
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
} catch (err) {
|
|
395
|
+
console.warn("[opencode-autopilot] explicit preference capture failed:", err);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export const memoryCaptureInternals = Object.freeze({
|
|
401
|
+
extractExplicitPreferenceCandidates,
|
|
402
|
+
extractTextPartContent,
|
|
403
|
+
inferPreferenceScope,
|
|
404
|
+
normalizePreferenceKey,
|
|
405
|
+
normalizePreferenceValue,
|
|
406
|
+
});
|
package/src/memory/database.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import { mkdirSync } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { runProjectRegistryMigrations } from "../projects/database";
|
|
5
|
+
import { getAutopilotDbPath } from "../utils/paths";
|
|
6
6
|
|
|
7
7
|
let db: Database | null = null;
|
|
8
8
|
|
|
@@ -11,12 +11,7 @@ let db: Database | null = null;
|
|
|
11
11
|
* Idempotent via IF NOT EXISTS.
|
|
12
12
|
*/
|
|
13
13
|
export function initMemoryDb(database: Database): void {
|
|
14
|
-
database
|
|
15
|
-
id TEXT PRIMARY KEY,
|
|
16
|
-
path TEXT NOT NULL UNIQUE,
|
|
17
|
-
name TEXT NOT NULL,
|
|
18
|
-
last_updated TEXT NOT NULL
|
|
19
|
-
)`);
|
|
14
|
+
runProjectRegistryMigrations(database);
|
|
20
15
|
|
|
21
16
|
database.run(`CREATE TABLE IF NOT EXISTS observations (
|
|
22
17
|
id INTEGER PRIMARY KEY,
|
|
@@ -42,6 +37,73 @@ export function initMemoryDb(database: Database): void {
|
|
|
42
37
|
last_updated TEXT NOT NULL
|
|
43
38
|
)`);
|
|
44
39
|
|
|
40
|
+
database.run(`CREATE TABLE IF NOT EXISTS preference_records (
|
|
41
|
+
id TEXT PRIMARY KEY,
|
|
42
|
+
key TEXT NOT NULL,
|
|
43
|
+
value TEXT NOT NULL,
|
|
44
|
+
scope TEXT NOT NULL CHECK(scope IN ('global', 'project')),
|
|
45
|
+
project_id TEXT,
|
|
46
|
+
status TEXT NOT NULL CHECK(status IN ('candidate', 'confirmed', 'rejected')) DEFAULT 'confirmed',
|
|
47
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
48
|
+
source_session TEXT,
|
|
49
|
+
created_at TEXT NOT NULL,
|
|
50
|
+
last_updated TEXT NOT NULL,
|
|
51
|
+
FOREIGN KEY (project_id) REFERENCES projects(id),
|
|
52
|
+
UNIQUE(key, scope, project_id)
|
|
53
|
+
)`);
|
|
54
|
+
|
|
55
|
+
database.run(`CREATE INDEX IF NOT EXISTS idx_preference_records_scope_updated
|
|
56
|
+
ON preference_records(scope, last_updated DESC, key ASC)`);
|
|
57
|
+
database.run(`CREATE INDEX IF NOT EXISTS idx_preference_records_project_updated
|
|
58
|
+
ON preference_records(project_id, last_updated DESC, key ASC)`);
|
|
59
|
+
|
|
60
|
+
database.run(`CREATE TABLE IF NOT EXISTS preference_evidence (
|
|
61
|
+
id TEXT PRIMARY KEY,
|
|
62
|
+
preference_id TEXT NOT NULL,
|
|
63
|
+
session_id TEXT,
|
|
64
|
+
run_id TEXT,
|
|
65
|
+
statement TEXT NOT NULL,
|
|
66
|
+
statement_hash TEXT NOT NULL,
|
|
67
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
68
|
+
confirmed INTEGER NOT NULL DEFAULT 0,
|
|
69
|
+
created_at TEXT NOT NULL,
|
|
70
|
+
FOREIGN KEY (preference_id) REFERENCES preference_records(id) ON DELETE CASCADE,
|
|
71
|
+
UNIQUE(preference_id, statement_hash)
|
|
72
|
+
)`);
|
|
73
|
+
|
|
74
|
+
database.run(`CREATE INDEX IF NOT EXISTS idx_preference_evidence_preference_created
|
|
75
|
+
ON preference_evidence(preference_id, created_at DESC, id DESC)`);
|
|
76
|
+
|
|
77
|
+
database.run(`INSERT INTO preference_records (
|
|
78
|
+
id,
|
|
79
|
+
key,
|
|
80
|
+
value,
|
|
81
|
+
scope,
|
|
82
|
+
project_id,
|
|
83
|
+
status,
|
|
84
|
+
confidence,
|
|
85
|
+
source_session,
|
|
86
|
+
created_at,
|
|
87
|
+
last_updated
|
|
88
|
+
)
|
|
89
|
+
SELECT
|
|
90
|
+
p.id,
|
|
91
|
+
p.key,
|
|
92
|
+
p.value,
|
|
93
|
+
'global',
|
|
94
|
+
NULL,
|
|
95
|
+
'confirmed',
|
|
96
|
+
p.confidence,
|
|
97
|
+
p.source_session,
|
|
98
|
+
p.created_at,
|
|
99
|
+
p.last_updated
|
|
100
|
+
FROM preferences p
|
|
101
|
+
WHERE NOT EXISTS (
|
|
102
|
+
SELECT 1
|
|
103
|
+
FROM preference_records pr
|
|
104
|
+
WHERE pr.id = p.id
|
|
105
|
+
)`);
|
|
106
|
+
|
|
45
107
|
database.run(`CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
|
|
46
108
|
content, summary,
|
|
47
109
|
content=observations,
|
|
@@ -79,9 +141,9 @@ export function getMemoryDb(dbPath?: string): Database {
|
|
|
79
141
|
const resolvedPath =
|
|
80
142
|
dbPath ??
|
|
81
143
|
(() => {
|
|
82
|
-
const
|
|
83
|
-
mkdirSync(
|
|
84
|
-
return
|
|
144
|
+
const runtimeDbPath = getAutopilotDbPath();
|
|
145
|
+
mkdirSync(dirname(runtimeDbPath), { recursive: true });
|
|
146
|
+
return runtimeDbPath;
|
|
85
147
|
})();
|
|
86
148
|
|
|
87
149
|
db = new Database(resolvedPath);
|
package/src/memory/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
createMemoryCaptureHandler,
|
|
3
|
+
createMemoryChatMessageHandler,
|
|
4
|
+
type MemoryCaptureDeps,
|
|
5
|
+
memoryCaptureInternals,
|
|
6
|
+
} from "./capture";
|
|
2
7
|
export * from "./constants";
|
|
3
8
|
export { closeMemoryDb, getMemoryDb, initMemoryDb } from "./database";
|
|
4
9
|
export { computeRelevanceScore, pruneStaleObservations } from "./decay";
|
|
@@ -6,13 +11,24 @@ export { createMemoryInjector, type MemoryInjectorConfig } from "./injector";
|
|
|
6
11
|
export { computeProjectKey } from "./project-key";
|
|
7
12
|
export {
|
|
8
13
|
deleteObservation,
|
|
14
|
+
deletePreferenceRecord,
|
|
15
|
+
deletePreferencesByKey,
|
|
9
16
|
getAllPreferences,
|
|
17
|
+
getConfirmedPreferencesForProject,
|
|
10
18
|
getObservationsByProject,
|
|
19
|
+
getPreferenceRecordById,
|
|
11
20
|
getProjectByPath,
|
|
21
|
+
getRecentFailureObservations,
|
|
12
22
|
insertObservation,
|
|
23
|
+
listPreferenceEvidence,
|
|
24
|
+
listPreferenceRecords,
|
|
25
|
+
listRelevantLessons,
|
|
26
|
+
prunePreferenceEvidence,
|
|
27
|
+
prunePreferences,
|
|
13
28
|
searchObservations,
|
|
14
29
|
updateAccessCount,
|
|
15
30
|
upsertPreference,
|
|
31
|
+
upsertPreferenceRecord,
|
|
16
32
|
upsertProject,
|
|
17
33
|
} from "./repository";
|
|
18
34
|
export {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Legacy path-hash project key.
|
|
5
|
+
*
|
|
6
|
+
* Kept only for migration compatibility while the runtime moves to the
|
|
7
|
+
* project registry / stable project identity model.
|
|
8
|
+
*/
|
|
3
9
|
export function computeProjectKey(projectPath: string): string {
|
|
4
10
|
return createHash("sha256").update(projectPath).digest("hex");
|
|
5
11
|
}
|