@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/memory/retrieval.ts
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
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
|
*/
|
|
13
14
|
|
|
14
15
|
import type { Database } from "bun:sqlite";
|
|
16
|
+
import { getLogger } from "../logging/domains";
|
|
15
17
|
import { CHARS_PER_TOKEN, DEFAULT_INJECTION_BUDGET } from "./constants";
|
|
16
18
|
import { getMemoryDb } from "./database";
|
|
17
19
|
import { computeRelevanceScore } from "./decay";
|
|
18
20
|
import {
|
|
19
|
-
|
|
20
|
-
getObservationsByProject,
|
|
21
|
+
getConfirmedPreferencesForProject,
|
|
21
22
|
getProjectByPath,
|
|
23
|
+
getRecentFailureObservations,
|
|
24
|
+
listRelevantLessons,
|
|
22
25
|
updateAccessCount,
|
|
23
26
|
} from "./repository";
|
|
24
27
|
import type { Observation, Preference } from "./types";
|
|
25
28
|
|
|
29
|
+
const logger = getLogger("memory", "retrieval");
|
|
30
|
+
|
|
26
31
|
/**
|
|
27
32
|
* An observation with its computed relevance score.
|
|
28
33
|
*/
|
|
@@ -48,200 +53,113 @@ export function scoreAndRankObservations(
|
|
|
48
53
|
.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
49
54
|
}
|
|
50
55
|
|
|
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
56
|
interface BuildMemoryContextOptions {
|
|
79
57
|
readonly projectName: string;
|
|
80
58
|
readonly lastSessionDate: string | null;
|
|
81
|
-
readonly observations: readonly ScoredObservation[];
|
|
82
59
|
readonly preferences: readonly Preference[];
|
|
60
|
+
readonly lessons: readonly {
|
|
61
|
+
readonly content: string;
|
|
62
|
+
readonly domain: string;
|
|
63
|
+
readonly extractedAt: string;
|
|
64
|
+
readonly sourcePhase: string;
|
|
65
|
+
}[];
|
|
66
|
+
readonly recentFailures: readonly ScoredObservation[];
|
|
83
67
|
readonly tokenBudget?: number;
|
|
84
68
|
}
|
|
85
69
|
|
|
70
|
+
function appendSection(
|
|
71
|
+
parts: string[],
|
|
72
|
+
totalChars: number,
|
|
73
|
+
charBudget: number,
|
|
74
|
+
section: string,
|
|
75
|
+
): number {
|
|
76
|
+
if (totalChars + section.length > charBudget) {
|
|
77
|
+
return totalChars;
|
|
78
|
+
}
|
|
79
|
+
parts.push(section);
|
|
80
|
+
return totalChars + section.length;
|
|
81
|
+
}
|
|
82
|
+
|
|
86
83
|
/**
|
|
87
84
|
* 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
85
|
*/
|
|
94
86
|
export function buildMemoryContext(options: BuildMemoryContextOptions): string {
|
|
95
87
|
const {
|
|
96
88
|
projectName,
|
|
97
89
|
lastSessionDate,
|
|
98
|
-
observations,
|
|
99
90
|
preferences,
|
|
91
|
+
lessons,
|
|
92
|
+
recentFailures,
|
|
100
93
|
tokenBudget = DEFAULT_INJECTION_BUDGET,
|
|
101
94
|
} = options;
|
|
102
95
|
|
|
103
|
-
if (
|
|
96
|
+
if (preferences.length === 0 && lessons.length === 0 && recentFailures.length === 0) {
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
104
99
|
|
|
105
100
|
const charBudget = tokenBudget * CHARS_PER_TOKEN;
|
|
106
101
|
let totalChars = 0;
|
|
107
102
|
const parts: string[] = [];
|
|
108
103
|
|
|
109
|
-
// Header
|
|
110
104
|
const header = `## Project Memory (auto-injected)\n**Project:** ${projectName}\n**Last session:** ${lastSessionDate ?? "first session"}\n`;
|
|
111
|
-
if (
|
|
105
|
+
if (header.length > charBudget) {
|
|
112
106
|
return header.slice(0, charBudget);
|
|
113
107
|
}
|
|
114
108
|
parts.push(header);
|
|
115
109
|
totalChars += header.length;
|
|
116
110
|
|
|
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
111
|
if (preferences.length > 0) {
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
112
|
+
const projectPreferences = preferences.filter((preference) => preference.scope === "project");
|
|
113
|
+
const globalPreferences = preferences.filter((preference) => preference.scope === "global");
|
|
114
|
+
|
|
115
|
+
if (projectPreferences.length > 0) {
|
|
116
|
+
const section = `\n### Confirmed Project Preferences\n${projectPreferences
|
|
117
|
+
.map(
|
|
118
|
+
(preference) =>
|
|
119
|
+
`- **${preference.key}:** ${preference.value} (confidence: ${preference.confidence}, evidence: ${preference.evidenceCount})`,
|
|
120
|
+
)
|
|
121
|
+
.join("\n")}\n`;
|
|
122
|
+
totalChars = appendSection(parts, totalChars, charBudget, section);
|
|
144
123
|
}
|
|
145
|
-
}
|
|
146
124
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
totalChars += timelineSection.length;
|
|
156
|
-
}
|
|
125
|
+
if (globalPreferences.length > 0) {
|
|
126
|
+
const section = `\n### Confirmed User Preferences\n${globalPreferences
|
|
127
|
+
.map(
|
|
128
|
+
(preference) =>
|
|
129
|
+
`- **${preference.key}:** ${preference.value} (confidence: ${preference.confidence}, evidence: ${preference.evidenceCount})`,
|
|
130
|
+
)
|
|
131
|
+
.join("\n")}\n`;
|
|
132
|
+
totalChars = appendSection(parts, totalChars, charBudget, section);
|
|
157
133
|
}
|
|
158
134
|
}
|
|
159
135
|
|
|
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
|
-
}
|
|
136
|
+
if (lessons.length > 0) {
|
|
137
|
+
const section = `\n### Recent Lessons\n${lessons
|
|
138
|
+
.map(
|
|
139
|
+
(lesson) =>
|
|
140
|
+
`- ${lesson.content} (${lesson.domain}, ${lesson.sourcePhase.toLowerCase()}, ${lesson.extractedAt.split("T")[0]})`,
|
|
141
|
+
)
|
|
142
|
+
.join("\n")}\n`;
|
|
143
|
+
totalChars = appendSection(parts, totalChars, charBudget, section);
|
|
144
|
+
}
|
|
175
145
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
146
|
+
if (recentFailures.length > 0) {
|
|
147
|
+
const sortedFailures = [...recentFailures].sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
148
|
+
const section = `\n### Failure Avoidance Notes\n${sortedFailures
|
|
149
|
+
.map(
|
|
150
|
+
(observation) =>
|
|
151
|
+
`- ${observation.summary} (confidence: ${observation.confidence}, ${observation.createdAt.split("T")[0]})`,
|
|
152
|
+
)
|
|
153
|
+
.join("\n")}\n`;
|
|
154
|
+
totalChars = appendSection(parts, totalChars, charBudget, section);
|
|
183
155
|
}
|
|
184
156
|
|
|
185
157
|
const result = parts.join("");
|
|
186
|
-
// Final safety truncation
|
|
187
158
|
return result.length > charBudget ? result.slice(0, charBudget) : result;
|
|
188
159
|
}
|
|
189
160
|
|
|
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
161
|
/**
|
|
242
162
|
* Convenience function: retrieve memory context for a project path.
|
|
243
|
-
*
|
|
244
|
-
* Ties together: project lookup, observation retrieval, scoring, preferences, and context building.
|
|
245
163
|
*/
|
|
246
164
|
export function retrieveMemoryContext(
|
|
247
165
|
projectPath: string,
|
|
@@ -252,25 +170,24 @@ export function retrieveMemoryContext(
|
|
|
252
170
|
const project = getProjectByPath(projectPath, db);
|
|
253
171
|
if (!project) return "";
|
|
254
172
|
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
const
|
|
173
|
+
const preferences = getConfirmedPreferencesForProject(project.id, db);
|
|
174
|
+
const lessons = listRelevantLessons(project.id, 5, db);
|
|
175
|
+
const failures = scoreAndRankObservations(
|
|
176
|
+
getRecentFailureObservations(project.id, 5, db),
|
|
177
|
+
halfLifeDays,
|
|
178
|
+
);
|
|
258
179
|
|
|
259
180
|
const context = buildMemoryContext({
|
|
260
181
|
projectName: project.name,
|
|
261
182
|
lastSessionDate: project.lastUpdated,
|
|
262
|
-
observations: scored,
|
|
263
183
|
preferences,
|
|
184
|
+
lessons,
|
|
185
|
+
recentFailures: failures,
|
|
264
186
|
tokenBudget,
|
|
265
187
|
});
|
|
266
188
|
|
|
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)
|
|
189
|
+
const idsToUpdate = failures
|
|
190
|
+
.map((observation) => observation.id)
|
|
274
191
|
.filter((id): id is number => id !== undefined);
|
|
275
192
|
if (idsToUpdate.length > 0) {
|
|
276
193
|
try {
|
|
@@ -280,8 +197,9 @@ export function retrieveMemoryContext(
|
|
|
280
197
|
updateAccessCount(id, db);
|
|
281
198
|
}
|
|
282
199
|
resolvedDb.run("COMMIT");
|
|
283
|
-
} catch {
|
|
200
|
+
} catch (err) {
|
|
284
201
|
// best-effort — access count update is non-critical
|
|
202
|
+
logger.warn("access count update failed", { error: String(err) });
|
|
285
203
|
}
|
|
286
204
|
}
|
|
287
205
|
|
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>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function getContextUtilizationString(usedTokens: number, maxTokens: number): string {
|
|
2
|
+
const safeMaxTokens = Math.max(0, maxTokens);
|
|
3
|
+
const safeUsedTokens = Math.max(0, usedTokens);
|
|
4
|
+
const utilization =
|
|
5
|
+
safeMaxTokens > 0 ? Math.min(100, Math.round((safeUsedTokens / safeMaxTokens) * 100)) : 0;
|
|
6
|
+
|
|
7
|
+
return `[${utilization}% used] ${safeUsedTokens} / ${safeMaxTokens} tokens`;
|
|
8
|
+
}
|
|
@@ -12,12 +12,17 @@
|
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { getLogger } from "../logging/domains";
|
|
15
16
|
import { classifyErrorType, getErrorMessage } from "../orchestrator/fallback/error-classifier";
|
|
17
|
+
import { generateSessionSummary } from "../ux/session-summary";
|
|
18
|
+
import { getContextUtilizationString } from "./context-display";
|
|
16
19
|
import type { ContextMonitor } from "./context-monitor";
|
|
17
20
|
import { emitErrorEvent, emitToolCompleteEvent } from "./event-emitter";
|
|
18
21
|
import type { ObservabilityEvent, SessionEventStore, SessionEvents } from "./event-store";
|
|
19
22
|
import { accumulateTokensFromMessage, createEmptyTokenAggregate } from "./token-tracker";
|
|
20
23
|
|
|
24
|
+
const logger = getLogger("session", "event-handlers");
|
|
25
|
+
|
|
21
26
|
/**
|
|
22
27
|
* Dependencies for the observability event handler.
|
|
23
28
|
*/
|
|
@@ -161,7 +166,6 @@ export function createObservabilityEventHandler(deps: ObservabilityHandlerDeps)
|
|
|
161
166
|
// Check context utilization
|
|
162
167
|
const utilResult = contextMonitor.processMessage(sessionId, info.tokens.input);
|
|
163
168
|
if (utilResult.shouldWarn) {
|
|
164
|
-
const pct = Math.round(utilResult.utilization * 100);
|
|
165
169
|
// Append context_warning event
|
|
166
170
|
const warningEvent: ObservabilityEvent = Object.freeze({
|
|
167
171
|
type: "context_warning" as const,
|
|
@@ -176,10 +180,14 @@ export function createObservabilityEventHandler(deps: ObservabilityHandlerDeps)
|
|
|
176
180
|
// Fire toast (per D-35)
|
|
177
181
|
showToast(
|
|
178
182
|
"Context Warning",
|
|
179
|
-
|
|
183
|
+
getContextUtilizationString(info.tokens.input, 200000),
|
|
180
184
|
"warning",
|
|
181
185
|
).catch((err) => {
|
|
182
|
-
|
|
186
|
+
logger.error("showToast failed for context warning", {
|
|
187
|
+
operation: "context_warning",
|
|
188
|
+
sessionId,
|
|
189
|
+
error: String(err),
|
|
190
|
+
});
|
|
183
191
|
});
|
|
184
192
|
}
|
|
185
193
|
}
|
|
@@ -190,11 +198,17 @@ export function createObservabilityEventHandler(deps: ObservabilityHandlerDeps)
|
|
|
190
198
|
const sessionId = extractSessionId(properties);
|
|
191
199
|
if (!sessionId) return;
|
|
192
200
|
|
|
193
|
-
//
|
|
194
|
-
const sessionData = eventStore.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
201
|
+
// Persist only new events since the last flush.
|
|
202
|
+
const sessionData = eventStore.getUnpersistedSession(sessionId);
|
|
203
|
+
if (sessionData && sessionData.events.length > 0) {
|
|
204
|
+
writeSessionLog(sessionData).catch((err) => {
|
|
205
|
+
logger.error("writeSessionLog failed on session.idle", {
|
|
206
|
+
operation: "session_end",
|
|
207
|
+
sessionId,
|
|
208
|
+
error: String(err),
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
}
|
|
198
212
|
return;
|
|
199
213
|
}
|
|
200
214
|
|
|
@@ -202,11 +216,43 @@ export function createObservabilityEventHandler(deps: ObservabilityHandlerDeps)
|
|
|
202
216
|
const sessionId = extractSessionId(properties);
|
|
203
217
|
if (!sessionId) return;
|
|
204
218
|
|
|
219
|
+
eventStore.appendEvent(sessionId, {
|
|
220
|
+
type: "session_end",
|
|
221
|
+
timestamp: new Date().toISOString(),
|
|
222
|
+
sessionId,
|
|
223
|
+
durationMs: 0,
|
|
224
|
+
totalCost: 0,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const summary = generateSessionSummary(eventStore.getSession(sessionId), null);
|
|
228
|
+
logger.info(`Session ended summary:\n${summary}`, {
|
|
229
|
+
operation: "session_end",
|
|
230
|
+
sessionId,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
void showToast(
|
|
234
|
+
"Session ended",
|
|
235
|
+
"Run /oc_summary to view the session summary.",
|
|
236
|
+
"info",
|
|
237
|
+
).catch((err) => {
|
|
238
|
+
logger.error("showToast failed for session end", {
|
|
239
|
+
operation: "session_end",
|
|
240
|
+
sessionId,
|
|
241
|
+
error: String(err),
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
205
245
|
// Final flush — session is done, remove from store
|
|
206
246
|
const sessionData = eventStore.flush(sessionId);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
247
|
+
if (sessionData && sessionData.events.length > 0) {
|
|
248
|
+
writeSessionLog(sessionData).catch((err) => {
|
|
249
|
+
logger.error("writeSessionLog failed on session.deleted", {
|
|
250
|
+
operation: "session_end",
|
|
251
|
+
sessionId,
|
|
252
|
+
error: String(err),
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
}
|
|
210
256
|
|
|
211
257
|
// Clean up context monitor
|
|
212
258
|
contextMonitor.cleanup(sessionId);
|
|
@@ -219,21 +265,24 @@ export function createObservabilityEventHandler(deps: ObservabilityHandlerDeps)
|
|
|
219
265
|
|
|
220
266
|
// Append compaction decision event (not session_start)
|
|
221
267
|
const compactEvent: ObservabilityEvent = Object.freeze({
|
|
222
|
-
type: "
|
|
268
|
+
type: "compacted" as const,
|
|
223
269
|
timestamp: new Date().toISOString(),
|
|
224
270
|
sessionId,
|
|
225
|
-
|
|
226
|
-
agent: "system",
|
|
227
|
-
decision: "Session compacted",
|
|
228
|
-
rationale: "Context window compaction triggered",
|
|
271
|
+
trigger: "context_window",
|
|
229
272
|
});
|
|
230
273
|
eventStore.appendEvent(sessionId, compactEvent);
|
|
231
274
|
|
|
232
275
|
// Snapshot to disk — session continues after compaction
|
|
233
|
-
const sessionData = eventStore.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
276
|
+
const sessionData = eventStore.getUnpersistedSession(sessionId);
|
|
277
|
+
if (sessionData && sessionData.events.length > 0) {
|
|
278
|
+
writeSessionLog(sessionData).catch((err) => {
|
|
279
|
+
logger.error("writeSessionLog failed on session.compacted", {
|
|
280
|
+
operation: "compacted",
|
|
281
|
+
sessionId,
|
|
282
|
+
error: String(err),
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}
|
|
237
286
|
return;
|
|
238
287
|
}
|
|
239
288
|
|
|
@@ -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
|
}
|