@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
package/src/memory/retrieval.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Provenance-first memory retrieval.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Injects bounded memory context from explicit, inspectable sources:
|
|
5
|
+
* - confirmed project and global preferences
|
|
6
|
+
* - recent project lessons
|
|
7
|
+
* - recent failure-avoidance notes from error observations
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* Generic observations remain queryable for inspection, but are no longer the
|
|
10
|
+
* primary injected memory contract.
|
|
10
11
|
*
|
|
11
12
|
* @module
|
|
12
13
|
*/
|
|
@@ -16,9 +17,10 @@ import { CHARS_PER_TOKEN, DEFAULT_INJECTION_BUDGET } from "./constants";
|
|
|
16
17
|
import { getMemoryDb } from "./database";
|
|
17
18
|
import { computeRelevanceScore } from "./decay";
|
|
18
19
|
import {
|
|
19
|
-
|
|
20
|
-
getObservationsByProject,
|
|
20
|
+
getConfirmedPreferencesForProject,
|
|
21
21
|
getProjectByPath,
|
|
22
|
+
getRecentFailureObservations,
|
|
23
|
+
listRelevantLessons,
|
|
22
24
|
updateAccessCount,
|
|
23
25
|
} from "./repository";
|
|
24
26
|
import type { Observation, Preference } from "./types";
|
|
@@ -48,200 +50,113 @@ export function scoreAndRankObservations(
|
|
|
48
50
|
.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
/**
|
|
52
|
-
* Type-to-section header mapping for Layer 1 grouping.
|
|
53
|
-
*/
|
|
54
|
-
const SECTION_HEADERS: Readonly<Record<string, string>> = Object.freeze({
|
|
55
|
-
decision: "### Key Decisions",
|
|
56
|
-
pattern: "### Patterns",
|
|
57
|
-
error: "### Recent Errors",
|
|
58
|
-
preference: "### Learned Preferences",
|
|
59
|
-
context: "### Context Notes",
|
|
60
|
-
tool_usage: "### Tool Usage Patterns",
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
/** Section display order (most valuable first). */
|
|
64
|
-
const SECTION_ORDER = ["decision", "pattern", "error", "preference", "context", "tool_usage"];
|
|
65
|
-
|
|
66
|
-
/** Max observations per group in Layer 1. */
|
|
67
|
-
const MAX_PER_GROUP = 5;
|
|
68
|
-
|
|
69
|
-
/** Minimum chars remaining to include Layer 2. */
|
|
70
|
-
const LAYER_2_THRESHOLD = 500;
|
|
71
|
-
|
|
72
|
-
/** Minimum chars remaining to include Layer 3. */
|
|
73
|
-
const LAYER_3_THRESHOLD = 1000;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Options for building memory context.
|
|
77
|
-
*/
|
|
78
53
|
interface BuildMemoryContextOptions {
|
|
79
54
|
readonly projectName: string;
|
|
80
55
|
readonly lastSessionDate: string | null;
|
|
81
|
-
readonly observations: readonly ScoredObservation[];
|
|
82
56
|
readonly preferences: readonly Preference[];
|
|
57
|
+
readonly lessons: readonly {
|
|
58
|
+
readonly content: string;
|
|
59
|
+
readonly domain: string;
|
|
60
|
+
readonly extractedAt: string;
|
|
61
|
+
readonly sourcePhase: string;
|
|
62
|
+
}[];
|
|
63
|
+
readonly recentFailures: readonly ScoredObservation[];
|
|
83
64
|
readonly tokenBudget?: number;
|
|
84
65
|
}
|
|
85
66
|
|
|
67
|
+
function appendSection(
|
|
68
|
+
parts: string[],
|
|
69
|
+
totalChars: number,
|
|
70
|
+
charBudget: number,
|
|
71
|
+
section: string,
|
|
72
|
+
): number {
|
|
73
|
+
if (totalChars + section.length > charBudget) {
|
|
74
|
+
return totalChars;
|
|
75
|
+
}
|
|
76
|
+
parts.push(section);
|
|
77
|
+
return totalChars + section.length;
|
|
78
|
+
}
|
|
79
|
+
|
|
86
80
|
/**
|
|
87
81
|
* Build a markdown memory context string within token budget.
|
|
88
|
-
*
|
|
89
|
-
* Uses 3-layer progressive disclosure:
|
|
90
|
-
* - Layer 1: Summaries grouped by type (always included if budget allows)
|
|
91
|
-
* - Layer 2: Recent Activity timeline (if remaining budget > 500 chars)
|
|
92
|
-
* - Layer 3: Full content for top observations (if remaining budget > 1000 chars)
|
|
93
82
|
*/
|
|
94
83
|
export function buildMemoryContext(options: BuildMemoryContextOptions): string {
|
|
95
84
|
const {
|
|
96
85
|
projectName,
|
|
97
86
|
lastSessionDate,
|
|
98
|
-
observations,
|
|
99
87
|
preferences,
|
|
88
|
+
lessons,
|
|
89
|
+
recentFailures,
|
|
100
90
|
tokenBudget = DEFAULT_INJECTION_BUDGET,
|
|
101
91
|
} = options;
|
|
102
92
|
|
|
103
|
-
if (
|
|
93
|
+
if (preferences.length === 0 && lessons.length === 0 && recentFailures.length === 0) {
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
104
96
|
|
|
105
97
|
const charBudget = tokenBudget * CHARS_PER_TOKEN;
|
|
106
98
|
let totalChars = 0;
|
|
107
99
|
const parts: string[] = [];
|
|
108
100
|
|
|
109
|
-
// Header
|
|
110
101
|
const header = `## Project Memory (auto-injected)\n**Project:** ${projectName}\n**Last session:** ${lastSessionDate ?? "first session"}\n`;
|
|
111
|
-
if (
|
|
102
|
+
if (header.length > charBudget) {
|
|
112
103
|
return header.slice(0, charBudget);
|
|
113
104
|
}
|
|
114
105
|
parts.push(header);
|
|
115
106
|
totalChars += header.length;
|
|
116
107
|
|
|
117
|
-
// --- Layer 1: Grouped summaries ---
|
|
118
|
-
// Sort by relevance within the function to ensure highest-first in each group
|
|
119
|
-
const sorted = [...observations].sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
120
|
-
const grouped = groupByType(sorted);
|
|
121
|
-
|
|
122
|
-
for (const type of SECTION_ORDER) {
|
|
123
|
-
const group = grouped.get(type);
|
|
124
|
-
if (!group || group.length === 0) continue;
|
|
125
|
-
|
|
126
|
-
const sectionHeader = SECTION_HEADERS[type] ?? `### ${type}`;
|
|
127
|
-
const items = group.slice(0, MAX_PER_GROUP);
|
|
128
|
-
const lines = items.map((obs) => `- ${obs.summary} (confidence: ${obs.confidence})`);
|
|
129
|
-
const section = `\n${sectionHeader}\n${lines.join("\n")}\n`;
|
|
130
|
-
|
|
131
|
-
if (totalChars + section.length > charBudget) break;
|
|
132
|
-
parts.push(section);
|
|
133
|
-
totalChars += section.length;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Preferences section
|
|
137
108
|
if (preferences.length > 0) {
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
109
|
+
const projectPreferences = preferences.filter((preference) => preference.scope === "project");
|
|
110
|
+
const globalPreferences = preferences.filter((preference) => preference.scope === "global");
|
|
111
|
+
|
|
112
|
+
if (projectPreferences.length > 0) {
|
|
113
|
+
const section = `\n### Confirmed Project Preferences\n${projectPreferences
|
|
114
|
+
.map(
|
|
115
|
+
(preference) =>
|
|
116
|
+
`- **${preference.key}:** ${preference.value} (confidence: ${preference.confidence}, evidence: ${preference.evidenceCount})`,
|
|
117
|
+
)
|
|
118
|
+
.join("\n")}\n`;
|
|
119
|
+
totalChars = appendSection(parts, totalChars, charBudget, section);
|
|
144
120
|
}
|
|
145
|
-
}
|
|
146
121
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
totalChars += timelineSection.length;
|
|
156
|
-
}
|
|
122
|
+
if (globalPreferences.length > 0) {
|
|
123
|
+
const section = `\n### Confirmed User Preferences\n${globalPreferences
|
|
124
|
+
.map(
|
|
125
|
+
(preference) =>
|
|
126
|
+
`- **${preference.key}:** ${preference.value} (confidence: ${preference.confidence}, evidence: ${preference.evidenceCount})`,
|
|
127
|
+
)
|
|
128
|
+
.join("\n")}\n`;
|
|
129
|
+
totalChars = appendSection(parts, totalChars, charBudget, section);
|
|
157
130
|
}
|
|
158
131
|
}
|
|
159
132
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const detail = `**${obs.type}:** ${obs.content}`;
|
|
170
|
-
const cost = detail.length + 1;
|
|
171
|
-
if (cost > linesBudget) break;
|
|
172
|
-
detailLines.push(detail);
|
|
173
|
-
linesBudget -= cost;
|
|
174
|
-
}
|
|
133
|
+
if (lessons.length > 0) {
|
|
134
|
+
const section = `\n### Recent Lessons\n${lessons
|
|
135
|
+
.map(
|
|
136
|
+
(lesson) =>
|
|
137
|
+
`- ${lesson.content} (${lesson.domain}, ${lesson.sourcePhase.toLowerCase()}, ${lesson.extractedAt.split("T")[0]})`,
|
|
138
|
+
)
|
|
139
|
+
.join("\n")}\n`;
|
|
140
|
+
totalChars = appendSection(parts, totalChars, charBudget, section);
|
|
141
|
+
}
|
|
175
142
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
143
|
+
if (recentFailures.length > 0) {
|
|
144
|
+
const sortedFailures = [...recentFailures].sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
145
|
+
const section = `\n### Failure Avoidance Notes\n${sortedFailures
|
|
146
|
+
.map(
|
|
147
|
+
(observation) =>
|
|
148
|
+
`- ${observation.summary} (confidence: ${observation.confidence}, ${observation.createdAt.split("T")[0]})`,
|
|
149
|
+
)
|
|
150
|
+
.join("\n")}\n`;
|
|
151
|
+
totalChars = appendSection(parts, totalChars, charBudget, section);
|
|
183
152
|
}
|
|
184
153
|
|
|
185
154
|
const result = parts.join("");
|
|
186
|
-
// Final safety truncation
|
|
187
155
|
return result.length > charBudget ? result.slice(0, charBudget) : result;
|
|
188
156
|
}
|
|
189
157
|
|
|
190
|
-
/**
|
|
191
|
-
* Group scored observations by type, preserving relevance order within groups.
|
|
192
|
-
*/
|
|
193
|
-
function groupByType(
|
|
194
|
-
observations: readonly ScoredObservation[],
|
|
195
|
-
): ReadonlyMap<string, readonly ScoredObservation[]> {
|
|
196
|
-
const groups = new Map<string, ScoredObservation[]>();
|
|
197
|
-
|
|
198
|
-
for (const obs of observations) {
|
|
199
|
-
const existing = groups.get(obs.type);
|
|
200
|
-
if (existing) {
|
|
201
|
-
existing.push(obs);
|
|
202
|
-
} else {
|
|
203
|
-
groups.set(obs.type, [obs]);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return groups;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Build a brief timeline of recent sessions from observations.
|
|
212
|
-
*/
|
|
213
|
-
function buildTimeline(observations: readonly ScoredObservation[]): string {
|
|
214
|
-
// Group by session, take last 5 sessions
|
|
215
|
-
const sessions = new Map<string, { date: string; count: number }>();
|
|
216
|
-
|
|
217
|
-
for (const obs of observations) {
|
|
218
|
-
const existing = sessions.get(obs.sessionId);
|
|
219
|
-
if (existing) {
|
|
220
|
-
existing.count++;
|
|
221
|
-
if (new Date(obs.createdAt).getTime() > new Date(existing.date).getTime()) {
|
|
222
|
-
existing.date = obs.createdAt;
|
|
223
|
-
}
|
|
224
|
-
} else {
|
|
225
|
-
sessions.set(obs.sessionId, { date: obs.createdAt, count: 1 });
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const sorted = [...sessions.values()]
|
|
230
|
-
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
|
231
|
-
.slice(0, 5);
|
|
232
|
-
|
|
233
|
-
return sorted
|
|
234
|
-
.map((s) => {
|
|
235
|
-
const dateStr = s.date.split("T")[0];
|
|
236
|
-
return `- ${dateStr}: ${s.count} observation${s.count !== 1 ? "s" : ""}`;
|
|
237
|
-
})
|
|
238
|
-
.join("\n");
|
|
239
|
-
}
|
|
240
|
-
|
|
241
158
|
/**
|
|
242
159
|
* Convenience function: retrieve memory context for a project path.
|
|
243
|
-
*
|
|
244
|
-
* Ties together: project lookup, observation retrieval, scoring, preferences, and context building.
|
|
245
160
|
*/
|
|
246
161
|
export function retrieveMemoryContext(
|
|
247
162
|
projectPath: string,
|
|
@@ -252,25 +167,24 @@ export function retrieveMemoryContext(
|
|
|
252
167
|
const project = getProjectByPath(projectPath, db);
|
|
253
168
|
if (!project) return "";
|
|
254
169
|
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
const
|
|
170
|
+
const preferences = getConfirmedPreferencesForProject(project.id, db);
|
|
171
|
+
const lessons = listRelevantLessons(project.id, 5, db);
|
|
172
|
+
const failures = scoreAndRankObservations(
|
|
173
|
+
getRecentFailureObservations(project.id, 5, db),
|
|
174
|
+
halfLifeDays,
|
|
175
|
+
);
|
|
258
176
|
|
|
259
177
|
const context = buildMemoryContext({
|
|
260
178
|
projectName: project.name,
|
|
261
179
|
lastSessionDate: project.lastUpdated,
|
|
262
|
-
observations: scored,
|
|
263
180
|
preferences,
|
|
181
|
+
lessons,
|
|
182
|
+
recentFailures: failures,
|
|
264
183
|
tokenBudget,
|
|
265
184
|
});
|
|
266
185
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// Best-effort: failures are swallowed to avoid blocking retrieval.
|
|
270
|
-
const maxInContext = MAX_PER_GROUP * SECTION_ORDER.length;
|
|
271
|
-
const idsToUpdate = scored
|
|
272
|
-
.slice(0, maxInContext)
|
|
273
|
-
.map((obs) => obs.id)
|
|
186
|
+
const idsToUpdate = failures
|
|
187
|
+
.map((observation) => observation.id)
|
|
274
188
|
.filter((id): id is number => id !== undefined);
|
|
275
189
|
if (idsToUpdate.length > 0) {
|
|
276
190
|
try {
|
package/src/memory/schemas.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { projectRecordSchema } from "../projects/schemas";
|
|
2
3
|
import { OBSERVATION_TYPES } from "./constants";
|
|
3
4
|
|
|
4
5
|
export const observationTypeSchema = z.enum(OBSERVATION_TYPES);
|
|
@@ -16,19 +17,50 @@ export const observationSchema = z.object({
|
|
|
16
17
|
lastAccessed: z.string(),
|
|
17
18
|
});
|
|
18
19
|
|
|
19
|
-
export const projectSchema =
|
|
20
|
-
id: z.string(),
|
|
21
|
-
path: z.string(),
|
|
22
|
-
name: z.string(),
|
|
23
|
-
lastUpdated: z.string(),
|
|
24
|
-
});
|
|
20
|
+
export const projectSchema = projectRecordSchema;
|
|
25
21
|
|
|
26
|
-
|
|
22
|
+
const preferenceBaseSchema = z.object({
|
|
27
23
|
id: z.string(),
|
|
28
24
|
key: z.string().min(1).max(200),
|
|
29
25
|
value: z.string().min(1).max(2000),
|
|
30
26
|
confidence: z.number().min(0).max(1).default(0.5),
|
|
27
|
+
scope: z.enum(["global", "project"]).default("global"),
|
|
28
|
+
projectId: z.string().nullable().default(null),
|
|
29
|
+
status: z.enum(["candidate", "confirmed", "rejected"]).default("confirmed"),
|
|
30
|
+
evidenceCount: z.number().int().min(0).default(0),
|
|
31
31
|
sourceSession: z.string().nullable().default(null),
|
|
32
32
|
createdAt: z.string(),
|
|
33
33
|
lastUpdated: z.string(),
|
|
34
34
|
});
|
|
35
|
+
|
|
36
|
+
function validatePreferenceScope(value: {
|
|
37
|
+
scope: "global" | "project";
|
|
38
|
+
projectId: string | null;
|
|
39
|
+
}): boolean {
|
|
40
|
+
return (
|
|
41
|
+
(value.scope === "global" && value.projectId === null) ||
|
|
42
|
+
(value.scope === "project" && value.projectId !== null)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const preferenceSchema = preferenceBaseSchema.refine(validatePreferenceScope, {
|
|
47
|
+
message: "projectId must be set for project scope and null for global scope",
|
|
48
|
+
path: ["projectId"],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const preferenceRecordSchema = preferenceBaseSchema.refine(validatePreferenceScope, {
|
|
52
|
+
message: "projectId must be set for project scope and null for global scope",
|
|
53
|
+
path: ["projectId"],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const preferenceEvidenceSchema = z.object({
|
|
57
|
+
id: z.string(),
|
|
58
|
+
preferenceId: z.string().min(1),
|
|
59
|
+
sessionId: z.string().nullable().default(null),
|
|
60
|
+
runId: z.string().nullable().default(null),
|
|
61
|
+
statement: z.string().min(1).max(4000),
|
|
62
|
+
statementHash: z.string().min(1).max(128),
|
|
63
|
+
confidence: z.number().min(0).max(1).default(0.5),
|
|
64
|
+
confirmed: z.boolean().default(false),
|
|
65
|
+
createdAt: z.string(),
|
|
66
|
+
});
|
package/src/memory/types.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { z } from "zod";
|
|
|
2
2
|
import type {
|
|
3
3
|
observationSchema,
|
|
4
4
|
observationTypeSchema,
|
|
5
|
+
preferenceEvidenceSchema,
|
|
6
|
+
preferenceRecordSchema,
|
|
5
7
|
preferenceSchema,
|
|
6
8
|
projectSchema,
|
|
7
9
|
} from "./schemas";
|
|
@@ -10,3 +12,5 @@ export type ObservationType = z.infer<typeof observationTypeSchema>;
|
|
|
10
12
|
export type Observation = z.infer<typeof observationSchema>;
|
|
11
13
|
export type Project = z.infer<typeof projectSchema>;
|
|
12
14
|
export type Preference = z.infer<typeof preferenceSchema>;
|
|
15
|
+
export type PreferenceRecord = z.infer<typeof preferenceRecordSchema>;
|
|
16
|
+
export type PreferenceEvidence = z.infer<typeof preferenceEvidenceSchema>;
|
|
@@ -190,11 +190,13 @@ export function createObservabilityEventHandler(deps: ObservabilityHandlerDeps)
|
|
|
190
190
|
const sessionId = extractSessionId(properties);
|
|
191
191
|
if (!sessionId) return;
|
|
192
192
|
|
|
193
|
-
//
|
|
194
|
-
const sessionData = eventStore.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
193
|
+
// Persist only new events since the last flush.
|
|
194
|
+
const sessionData = eventStore.getUnpersistedSession(sessionId);
|
|
195
|
+
if (sessionData && sessionData.events.length > 0) {
|
|
196
|
+
writeSessionLog(sessionData).catch((err) => {
|
|
197
|
+
console.error("[opencode-autopilot]", err);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
198
200
|
return;
|
|
199
201
|
}
|
|
200
202
|
|
|
@@ -202,11 +204,21 @@ export function createObservabilityEventHandler(deps: ObservabilityHandlerDeps)
|
|
|
202
204
|
const sessionId = extractSessionId(properties);
|
|
203
205
|
if (!sessionId) return;
|
|
204
206
|
|
|
207
|
+
eventStore.appendEvent(sessionId, {
|
|
208
|
+
type: "session_end",
|
|
209
|
+
timestamp: new Date().toISOString(),
|
|
210
|
+
sessionId,
|
|
211
|
+
durationMs: 0,
|
|
212
|
+
totalCost: 0,
|
|
213
|
+
});
|
|
214
|
+
|
|
205
215
|
// Final flush — session is done, remove from store
|
|
206
216
|
const sessionData = eventStore.flush(sessionId);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
217
|
+
if (sessionData && sessionData.events.length > 0) {
|
|
218
|
+
writeSessionLog(sessionData).catch((err) => {
|
|
219
|
+
console.error("[opencode-autopilot]", err);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
210
222
|
|
|
211
223
|
// Clean up context monitor
|
|
212
224
|
contextMonitor.cleanup(sessionId);
|
|
@@ -219,21 +231,20 @@ export function createObservabilityEventHandler(deps: ObservabilityHandlerDeps)
|
|
|
219
231
|
|
|
220
232
|
// Append compaction decision event (not session_start)
|
|
221
233
|
const compactEvent: ObservabilityEvent = Object.freeze({
|
|
222
|
-
type: "
|
|
234
|
+
type: "compacted" as const,
|
|
223
235
|
timestamp: new Date().toISOString(),
|
|
224
236
|
sessionId,
|
|
225
|
-
|
|
226
|
-
agent: "system",
|
|
227
|
-
decision: "Session compacted",
|
|
228
|
-
rationale: "Context window compaction triggered",
|
|
237
|
+
trigger: "context_window",
|
|
229
238
|
});
|
|
230
239
|
eventStore.appendEvent(sessionId, compactEvent);
|
|
231
240
|
|
|
232
241
|
// Snapshot to disk — session continues after compaction
|
|
233
|
-
const sessionData = eventStore.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
242
|
+
const sessionData = eventStore.getUnpersistedSession(sessionId);
|
|
243
|
+
if (sessionData && sessionData.events.length > 0) {
|
|
244
|
+
writeSessionLog(sessionData).catch((err) => {
|
|
245
|
+
console.error("[opencode-autopilot]", err);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
237
248
|
return;
|
|
238
249
|
}
|
|
239
250
|
|
|
@@ -81,6 +81,12 @@ export type ObservabilityEvent =
|
|
|
81
81
|
readonly fromPhase: string;
|
|
82
82
|
readonly toPhase: string;
|
|
83
83
|
}
|
|
84
|
+
| {
|
|
85
|
+
readonly type: "compacted";
|
|
86
|
+
readonly timestamp: string;
|
|
87
|
+
readonly sessionId: string;
|
|
88
|
+
readonly trigger: string;
|
|
89
|
+
}
|
|
84
90
|
| { readonly type: "session_start"; readonly timestamp: string; readonly sessionId: string }
|
|
85
91
|
| {
|
|
86
92
|
readonly type: "session_end";
|
|
@@ -118,6 +124,7 @@ export interface SessionEvents {
|
|
|
118
124
|
interface MutableSessionData {
|
|
119
125
|
sessionId: string;
|
|
120
126
|
events: ObservabilityEvent[];
|
|
127
|
+
persistedEventCount: number;
|
|
121
128
|
tokens: TokenAggregate;
|
|
122
129
|
toolMetrics: Map<string, ToolMetrics>;
|
|
123
130
|
currentPhase: string | null;
|
|
@@ -138,6 +145,7 @@ export class SessionEventStore {
|
|
|
138
145
|
this.sessions.set(sessionId, {
|
|
139
146
|
sessionId,
|
|
140
147
|
events: [],
|
|
148
|
+
persistedEventCount: 0,
|
|
141
149
|
tokens: createEmptyTokenAggregate(),
|
|
142
150
|
toolMetrics: new Map(),
|
|
143
151
|
currentPhase: null,
|
|
@@ -213,11 +221,31 @@ export class SessionEventStore {
|
|
|
213
221
|
};
|
|
214
222
|
}
|
|
215
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Returns only events not yet persisted and marks them as persisted.
|
|
226
|
+
*/
|
|
227
|
+
getUnpersistedSession(sessionId: string): SessionEvents | undefined {
|
|
228
|
+
const session = this.sessions.get(sessionId);
|
|
229
|
+
if (!session) return undefined;
|
|
230
|
+
|
|
231
|
+
const events = session.events.slice(session.persistedEventCount);
|
|
232
|
+
session.persistedEventCount = session.events.length;
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
sessionId: session.sessionId,
|
|
236
|
+
events,
|
|
237
|
+
tokens: session.tokens,
|
|
238
|
+
toolMetrics: new Map(session.toolMetrics),
|
|
239
|
+
currentPhase: session.currentPhase,
|
|
240
|
+
startedAt: session.startedAt,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
216
244
|
/**
|
|
217
245
|
* Returns session data and removes it from the store (for disk flush).
|
|
218
246
|
*/
|
|
219
247
|
flush(sessionId: string): SessionEvents | undefined {
|
|
220
|
-
const session = this.
|
|
248
|
+
const session = this.getUnpersistedSession(sessionId);
|
|
221
249
|
if (session) {
|
|
222
250
|
this.sessions.delete(sessionId);
|
|
223
251
|
}
|