@aexol/spectral 0.9.13 → 0.9.14

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.
@@ -1,22 +1,17 @@
1
1
  /**
2
2
  * Spectral Vision Extension
3
3
  *
4
- * Automatically switches to a vision-capable model for image processing
5
- * when images are attached. This allows non-vision models to "see" images
6
- * and saves main agent context by using a potentially smaller/cheaper
7
- * vision model for image understanding.
4
+ * Converts attached images into text analysis before the main agent call.
8
5
  *
9
- * Logic:
10
- * - If the main model supports vision natively, images pass through
11
- * directly no interception, no text descriptions.
12
- * - When the main model does NOT support vision, falls back to the
13
- * admin-configured default vision model (isVisionDefault flag from
14
- * backend via SettingsManager) for image descriptions.
15
- * - If no admin default is configured, falls back to the best
16
- * available vision-capable model by provider priority.
17
- *
18
- * Hooks into the `context` event to intercept ALL images before they reach
19
- * the LLM (covers user-attached images and tool-result images alike).
6
+ * Pipeline:
7
+ * - If the current main model can process images, use it as the analyzer.
8
+ * - Otherwise use the admin-configured default vision model.
9
+ * - Otherwise use the best available authenticated image-capable model.
10
+ * - Replace image blocks with the text analysis before persistence when
11
+ * possible (`message_end`), so image data is deleted from conversation
12
+ * history and is not re-analyzed on later turns.
13
+ * - Keep a `context` hook as a defense-in-depth fallback for older sessions
14
+ * or messages that entered context before the replacement hook existed.
20
15
  */
21
16
  import type { ExtensionAPI } from "../sdk/coding-agent/index.js";
22
17
  export default function spectralVisionExtension(ext: ExtensionAPI): void;
@@ -1 +1 @@
1
- {"version":3,"file":"spectral-vision-fallback.d.ts","sourceRoot":"","sources":["../../src/extensions/spectral-vision-fallback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAuNjE,MAAM,CAAC,OAAO,UAAU,uBAAuB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAmFvE"}
1
+ {"version":3,"file":"spectral-vision-fallback.d.ts","sourceRoot":"","sources":["../../src/extensions/spectral-vision-fallback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAsWjE,MAAM,CAAC,OAAO,UAAU,uBAAuB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CA6DvE"}
@@ -1,27 +1,47 @@
1
1
  /**
2
2
  * Spectral Vision Extension
3
3
  *
4
- * Automatically switches to a vision-capable model for image processing
5
- * when images are attached. This allows non-vision models to "see" images
6
- * and saves main agent context by using a potentially smaller/cheaper
7
- * vision model for image understanding.
4
+ * Converts attached images into text analysis before the main agent call.
8
5
  *
9
- * Logic:
10
- * - If the main model supports vision natively, images pass through
11
- * directly no interception, no text descriptions.
12
- * - When the main model does NOT support vision, falls back to the
13
- * admin-configured default vision model (isVisionDefault flag from
14
- * backend via SettingsManager) for image descriptions.
15
- * - If no admin default is configured, falls back to the best
16
- * available vision-capable model by provider priority.
17
- *
18
- * Hooks into the `context` event to intercept ALL images before they reach
19
- * the LLM (covers user-attached images and tool-result images alike).
6
+ * Pipeline:
7
+ * - If the current main model can process images, use it as the analyzer.
8
+ * - Otherwise use the admin-configured default vision model.
9
+ * - Otherwise use the best available authenticated image-capable model.
10
+ * - Replace image blocks with the text analysis before persistence when
11
+ * possible (`message_end`), so image data is deleted from conversation
12
+ * history and is not re-analyzed on later turns.
13
+ * - Keep a `context` hook as a defense-in-depth fallback for older sessions
14
+ * or messages that entered context before the replacement hook existed.
20
15
  */
21
16
  import { streamSimple } from "../sdk/ai/index.js";
22
17
  // ---------------------------------------------------------------------------
23
18
  // Helpers
24
19
  // ---------------------------------------------------------------------------
20
+ const EXTENSION_NAME = "spectral-vision";
21
+ const IMAGE_DESCRIPTION_PATTERN = /^\[Image analysis from /;
22
+ const MAX_CACHE_ENTRIES = 500;
23
+ function withImageInput(model) {
24
+ if (model.input.includes("image"))
25
+ return model;
26
+ return {
27
+ ...model,
28
+ input: [...model.input, "image"],
29
+ };
30
+ }
31
+ function sameModel(a, b) {
32
+ return !!a && !!b && a.provider === b.provider && a.id === b.id;
33
+ }
34
+ function getCurrentImageAnalyzer(ctx) {
35
+ const currentModel = ctx.model;
36
+ if (!currentModel)
37
+ return undefined;
38
+ if (!ctx.modelRegistry.hasConfiguredAuth(currentModel))
39
+ return undefined;
40
+ if (currentModel.input.includes("image")) {
41
+ return { model: withImageInput(currentModel), source: "main" };
42
+ }
43
+ return undefined;
44
+ }
25
45
  /**
26
46
  * Check if the SettingsManager has an admin-configured default vision model
27
47
  * with working auth. Returns the model if found and auth-ready, or undefined.
@@ -36,75 +56,126 @@ function getAdminVisionModel(ctx) {
36
56
  return undefined;
37
57
  const allModels = ctx.modelRegistry.getAll();
38
58
  const match = allModels.find((m) => m.provider === defaultVisionProvider && m.id === defaultVisionModel);
39
- if (!match || !match.input.includes("image"))
59
+ if (!match)
40
60
  return undefined;
41
61
  if (!ctx.modelRegistry.hasConfiguredAuth(match))
42
62
  return undefined;
43
- return match;
63
+ // Trust the admin's explicit choice: if they marked this model as the
64
+ // default vision model, it should be used even when the backend's
65
+ // capabilities data doesn't report image support.
66
+ return withImageInput(match);
44
67
  }
45
- /**
46
- * Resolve the best vision model to use for image descriptions.
47
- *
48
- * Priority:
49
- * 1. Current main model if it supports images — passthrough, no interception
50
- * 2. Admin-configured default vision model (isVisionDefault from backend)
51
- * 3. Best available vision model by provider priority
52
- * 4. First available vision model
53
- */
54
- function resolveVisionModel(ctx) {
55
- // 1. If the main model supports vision natively, skip interception entirely
56
- // and let the main model process images directly.
57
- const currentModel = ctx.model;
58
- if (currentModel?.input.includes("image") && ctx.modelRegistry.hasConfiguredAuth(currentModel)) {
59
- process.stderr.write(`[spectral-vision] Main model supports vision — passing images through natively (${currentModel.provider}/${currentModel.id})\n`);
60
- return undefined;
61
- }
62
- // 2. Admin-configured default vision model (fallback for non-vision main models)
68
+ function getFallbackVisionModel(ctx, exclude) {
63
69
  const adminModel = getAdminVisionModel(ctx);
64
- if (adminModel) {
65
- process.stderr.write(`[spectral-vision] Using admin-configured default: ${adminModel.provider}/${adminModel.id}\n`);
66
- return adminModel;
70
+ if (adminModel && !sameModel(adminModel, exclude)) {
71
+ return { model: adminModel, source: "default" };
67
72
  }
68
- // 3. Fall back to best available vision model by provider priority
69
73
  const allModels = ctx.modelRegistry.getAll();
70
- const visionModels = allModels.filter((m) => m.input.includes("image") && ctx.modelRegistry.hasConfiguredAuth(m));
74
+ const visionModels = allModels.filter((m) => m.input.includes("image") &&
75
+ ctx.modelRegistry.hasConfiguredAuth(m) &&
76
+ !sameModel(m, exclude));
71
77
  if (visionModels.length === 0)
72
78
  return undefined;
73
79
  const providerPriority = ["anthropic", "openai", "google", "openrouter"];
74
80
  for (const provider of providerPriority) {
75
81
  const match = visionModels.find((m) => m.provider === provider);
76
- if (match) {
77
- process.stderr.write(`[spectral-vision] Using fallback vision model: ${match.provider}/${match.id}\n`);
78
- return match;
79
- }
82
+ if (match)
83
+ return { model: match, source: "fallback" };
80
84
  }
81
- // 4. First available vision model
82
- const fallback = visionModels[0];
83
- process.stderr.write(`[spectral-vision] Using first available vision model: ${fallback.provider}/${fallback.id}\n`);
84
- return fallback;
85
+ return { model: visionModels[0], source: "fallback" };
86
+ }
87
+ function resolvePrimaryAnalyzer(ctx) {
88
+ const currentAnalyzer = getCurrentImageAnalyzer(ctx);
89
+ if (currentAnalyzer) {
90
+ process.stderr.write(`[${EXTENSION_NAME}] Using main model for image analysis: ${currentAnalyzer.model.provider}/${currentAnalyzer.model.id}\n`);
91
+ return currentAnalyzer;
92
+ }
93
+ const fallback = getFallbackVisionModel(ctx);
94
+ if (fallback) {
95
+ const label = fallback.source === "default" ? "admin-configured default" : "fallback vision model";
96
+ process.stderr.write(`[${EXTENSION_NAME}] Using ${label}: ${fallback.model.provider}/${fallback.model.id}\n`);
97
+ return fallback;
98
+ }
99
+ return undefined;
85
100
  }
86
101
  /** Check if any content block in an array is an image */
87
102
  function hasImageContent(content) {
88
103
  return Array.isArray(content) && content.some((c) => c?.type === "image");
89
104
  }
105
+ function removeImageContent(content) {
106
+ return content.filter((block) => block.type === "text");
107
+ }
90
108
  /** Count images in a message */
91
109
  function countImages(msg) {
92
110
  if (!Array.isArray(msg.content))
93
111
  return 0;
94
112
  return msg.content.filter((c) => c.type === "image").length;
95
113
  }
114
+ function isProcessableImageMessage(msg) {
115
+ return (msg.role === "user" || msg.role === "toolResult") && Array.isArray(msg.content) && hasImageContent(msg.content);
116
+ }
117
+ /**
118
+ * Check if a message has already been processed by our vision fallback.
119
+ */
120
+ function isAlreadyDescribed(msg) {
121
+ if (!Array.isArray(msg.content))
122
+ return false;
123
+ return msg.content.some((c) => c.type === "text" && IMAGE_DESCRIPTION_PATTERN.test(String(c.text)));
124
+ }
125
+ function contextTextFor(content) {
126
+ return content
127
+ .filter((c) => c.type === "text")
128
+ .map((c) => c.text)
129
+ .join(" ")
130
+ .substring(0, 500);
131
+ }
132
+ function imageMessageCacheKey(msg) {
133
+ const content = Array.isArray(msg.content) ? msg.content : [];
134
+ const text = content
135
+ .filter((c) => c.type === "text")
136
+ .map((c) => String(c.text))
137
+ .join("\n")
138
+ .slice(0, 1000);
139
+ const images = content
140
+ .filter((c) => c.type === "image")
141
+ .map((c) => `${c.mimeType ?? "image"}:${String(c.data ?? "").length}:${String(c.data ?? "").slice(0, 64)}`)
142
+ .join("|");
143
+ return `${msg.role}:${msg.timestamp ?? "no-ts"}:${text}:${images}`;
144
+ }
145
+ function rememberAnalysis(cache, key, content) {
146
+ cache.set(key, content);
147
+ if (cache.size <= MAX_CACHE_ENTRIES)
148
+ return;
149
+ const first = cache.keys().next().value;
150
+ if (first)
151
+ cache.delete(first);
152
+ }
153
+ function omittedContent(msg, reason) {
154
+ const textBlocks = Array.isArray(msg.content)
155
+ ? msg.content.filter((c) => c.type === "text")
156
+ : [];
157
+ const imageCount = countImages(msg);
158
+ return [
159
+ ...textBlocks,
160
+ {
161
+ type: "text",
162
+ text: `[${imageCount} image(s) omitted — ${reason}]`,
163
+ },
164
+ ];
165
+ }
96
166
  // ---------------------------------------------------------------------------
97
167
  // Core: call vision model to describe images
98
168
  // ---------------------------------------------------------------------------
99
169
  async function describeImages(visionModel, content, contextText, ctx) {
170
+ const model = withImageInput(visionModel);
100
171
  const userContent = [
101
172
  {
102
173
  type: "text",
103
- text: `Please describe the following image(s). ` +
174
+ text: `Please analyze the following image(s). ` +
104
175
  (contextText ? `Context: ${contextText}. ` : "") +
105
176
  "Focus on what is visually visible — text content, UI elements, diagrams, " +
106
- "code structure, layout, colors, etc. Be concise but thorough. " +
107
- "If multiple images are provided, describe each one separately with a heading.",
177
+ "code structure, layout, colors, errors, and any details needed to answer the user's request. " +
178
+ "Be concise but thorough. If multiple images are provided, analyze each one separately with a heading.",
108
179
  },
109
180
  ];
110
181
  const textPrefixBlocks = [];
@@ -120,115 +191,141 @@ async function describeImages(visionModel, content, contextText, ctx) {
120
191
  }
121
192
  if (imageCount === 0)
122
193
  return content;
123
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(visionModel);
194
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
124
195
  if (!auth.ok) {
125
- process.stderr.write(`[spectral-vision] No API key for vision model ${visionModel.provider}/${visionModel.id}\n`);
196
+ throw new Error(`No API key for vision model ${model.provider}/${model.id}: ${auth.error}`);
197
+ }
198
+ const result = await streamSimple(model, {
199
+ systemPrompt: "You are an image analysis assistant. Analyze images accurately and concisely. Return only the visual analysis, not commentary about being an assistant.",
200
+ messages: [{ role: "user", content: userContent, timestamp: Date.now() }],
201
+ tools: [],
202
+ }, {
203
+ apiKey: auth.apiKey,
204
+ headers: auth.headers,
205
+ });
206
+ let description = "";
207
+ for await (const event of result) {
208
+ if (event.type === "text_delta") {
209
+ description += event.delta;
210
+ }
211
+ }
212
+ const textNote = description.trim()
213
+ ? `[Image analysis from ${model.provider}/${model.id} (${imageCount} image(s)):\n${description.trim()}\n]`
214
+ : `[${imageCount} image(s) — vision model returned empty analysis]`;
215
+ try {
216
+ ctx.ui.notify(`[${EXTENSION_NAME}] Analyzed ${imageCount} image(s) with ${model.provider}/${model.id}`, "info");
217
+ }
218
+ catch {
219
+ // UI context may not be available in headless modes.
220
+ }
221
+ return [
222
+ ...textPrefixBlocks,
223
+ { type: "text", text: textNote },
224
+ ];
225
+ }
226
+ async function analyzeMessageImages(msg, ctx, cache) {
227
+ const key = imageMessageCacheKey(msg);
228
+ const cached = cache.get(key);
229
+ if (cached)
230
+ return cached;
231
+ if (isAlreadyDescribed(msg)) {
232
+ const withoutImages = removeImageContent(msg.content);
233
+ rememberAnalysis(cache, key, withoutImages);
234
+ return withoutImages;
235
+ }
236
+ const primary = resolvePrimaryAnalyzer(ctx);
237
+ if (!primary) {
238
+ const content = omittedContent(msg, "no authenticated vision-capable model is configured");
239
+ rememberAnalysis(cache, key, content);
126
240
  return content;
127
241
  }
242
+ const contextText = contextTextFor(msg.content);
243
+ const imageCount = countImages(msg);
128
244
  try {
129
- const result = await streamSimple(visionModel, {
130
- systemPrompt: "You are an image description assistant. Describe images accurately and concisely.",
131
- messages: [{ role: "user", content: userContent, timestamp: Date.now() }],
132
- tools: [],
133
- }, {
134
- apiKey: auth.apiKey,
135
- headers: auth.headers,
136
- });
137
- let description = "";
138
- for await (const event of result) {
139
- if (event.type === "text_delta") {
140
- description += event.delta;
141
- }
142
- }
143
- const textNote = description.trim()
144
- ? `[Image description from ${visionModel.provider}/${visionModel.id} (${imageCount} image(s)):\n${description}\n]`
145
- : `[${imageCount} image(s) — vision model returned empty description]`;
146
- // Notify the frontend that the vision extension analyzed the image(s)
147
- try {
148
- ctx.ui.notify(`[spectral-vision] Analyzed ${imageCount} image(s) with ${visionModel.provider}/${visionModel.id}`, "info");
149
- }
150
- catch {
151
- // UI context may not be available in headless modes
152
- }
153
- return [
154
- ...textPrefixBlocks,
155
- { type: "text", text: textNote },
156
- ];
245
+ const described = await describeImages(primary.model, msg.content, contextText, ctx);
246
+ const withoutImages = removeImageContent(described);
247
+ rememberAnalysis(cache, key, withoutImages);
248
+ return withoutImages;
157
249
  }
158
250
  catch (err) {
159
- const msg = err instanceof Error ? err.message : String(err);
160
- process.stderr.write(`[spectral-vision] Vision call failed: ${msg}\n`);
161
- // Notify frontend about the failure
251
+ const message = err instanceof Error ? err.message : String(err);
252
+ process.stderr.write(`[${EXTENSION_NAME}] Image analysis with ${primary.model.provider}/${primary.model.id} failed: ${message}\n`);
253
+ const fallback = primary.source === "main" ? getFallbackVisionModel(ctx, primary.model) : undefined;
254
+ if (fallback) {
255
+ process.stderr.write(`[${EXTENSION_NAME}] Retrying image analysis with ${fallback.model.provider}/${fallback.model.id}\n`);
256
+ try {
257
+ const described = await describeImages(fallback.model, msg.content, contextText, ctx);
258
+ const withoutImages = removeImageContent(described);
259
+ rememberAnalysis(cache, key, withoutImages);
260
+ return withoutImages;
261
+ }
262
+ catch (fallbackErr) {
263
+ const fallbackMessage = fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr);
264
+ process.stderr.write(`[${EXTENSION_NAME}] Fallback image analysis with ${fallback.model.provider}/${fallback.model.id} failed: ${fallbackMessage}\n`);
265
+ try {
266
+ ctx.ui.notify(`[${EXTENSION_NAME}] Failed to analyze ${imageCount} image(s): ${fallbackMessage}`, "warning");
267
+ }
268
+ catch {
269
+ // UI context may not be available in headless modes.
270
+ }
271
+ const content = omittedContent(msg, `vision analysis failed: ${fallbackMessage}`);
272
+ rememberAnalysis(cache, key, content);
273
+ return content;
274
+ }
275
+ }
162
276
  try {
163
- ctx.ui.notify(`[spectral-vision] Failed to analyze ${imageCount} image(s): ${msg}`, "warning");
277
+ ctx.ui.notify(`[${EXTENSION_NAME}] Failed to analyze ${imageCount} image(s): ${message}`, "warning");
164
278
  }
165
279
  catch {
166
- // UI context may not be available in headless modes
280
+ // UI context may not be available in headless modes.
167
281
  }
168
- return [
169
- ...textPrefixBlocks,
170
- {
171
- type: "text",
172
- text: `[${imageCount} image(s) omitted — vision fallback failed: ${msg}]`,
173
- },
174
- ];
282
+ const content = omittedContent(msg, `vision analysis failed: ${message}`);
283
+ rememberAnalysis(cache, key, content);
284
+ return content;
175
285
  }
176
286
  }
177
287
  // ---------------------------------------------------------------------------
178
288
  // Extension entry point
179
289
  // ---------------------------------------------------------------------------
180
290
  export default function spectralVisionExtension(ext) {
181
- let visionModel;
291
+ const analysisCache = new Map();
182
292
  ext.on("session_start", (_event, ctx) => {
183
- visionModel = resolveVisionModel(ctx);
184
- if (visionModel) {
185
- process.stderr.write(`[spectral-vision] Ready using ${visionModel.provider}/${visionModel.id} for image descriptions.\n`);
186
- }
187
- else if (ctx.model?.input.includes("image")) {
188
- process.stderr.write(`[spectral-vision] Ready — main model ${ctx.model.provider}/${ctx.model.id} handles vision natively, passthrough enabled.\n`);
293
+ const analyzer = resolvePrimaryAnalyzer(ctx);
294
+ if (analyzer) {
295
+ const label = analyzer.source === "main" ? "main model" : analyzer.source === "default" ? "default vision model" : "fallback vision model";
296
+ process.stderr.write(`[${EXTENSION_NAME}] Ready — ${label} ${analyzer.model.provider}/${analyzer.model.id} will analyze images before agent calls.\n`);
189
297
  }
190
298
  else {
191
- process.stderr.write("[spectral-vision] No vision model with auth configured. Image description disabled.\n");
299
+ process.stderr.write(`[${EXTENSION_NAME}] No vision model with auth configured. Images will be removed with an explanatory note.\n`);
192
300
  }
193
301
  });
302
+ // Primary path: replace user/tool-result image blocks before AgentSession
303
+ // persists the message. This deletes raw base64 image data from conversation
304
+ // history and prevents repeat analysis on later turns.
305
+ ext.on("message_end", async (event, ctx) => {
306
+ const msg = event.message;
307
+ if (!isProcessableImageMessage(msg))
308
+ return;
309
+ process.stderr.write(`[${EXTENSION_NAME}] Analyzing ${countImages(msg)} image(s) before persisting ${msg.role} message...\n`);
310
+ const content = await analyzeMessageImages(msg, ctx, analysisCache);
311
+ return { message: { ...msg, content } };
312
+ });
313
+ // Defense-in-depth path: handle images still present in context from older
314
+ // sessions or from messages that bypassed message_end replacement. Results are
315
+ // cached by message/image fingerprint so old images are not re-analyzed every
316
+ // turn; cached analysis is substituted instead.
194
317
  ext.on("context", async (event, ctx) => {
195
318
  const messages = event.messages;
196
- // Check if any message contains images
197
- let totalImages = 0;
198
- for (const msg of messages) {
199
- totalImages += countImages(msg);
200
- }
201
- if (totalImages === 0)
319
+ const imageMessages = messages.filter(isProcessableImageMessage);
320
+ if (imageMessages.length === 0)
202
321
  return;
203
- // Re-resolve vision model each time images are encountered.
204
- // If the main model supports vision natively, resolveVisionModel
205
- // returns undefined and we pass images through without interception.
206
- visionModel = resolveVisionModel(ctx);
207
- if (!visionModel) {
208
- // Check if we're in passthrough mode (main model has native vision)
209
- if (ctx.model?.input.includes("image")) {
210
- process.stderr.write(`[spectral-vision] Passthrough — ${totalImages} image(s) flowing to main model natively\n`);
211
- }
212
- else {
213
- process.stderr.write("[spectral-vision] No vision model available, images will be omitted\n");
214
- }
215
- return;
216
- }
217
- process.stderr.write(`[spectral-vision] Describing ${totalImages} image(s) with ${visionModel.provider}/${visionModel.id}...\n`);
218
- // Process each message — intercept both user messages and tool results
219
- // that contain images (e.g. read tool returning a screenshot).
322
+ const totalImages = imageMessages.reduce((sum, msg) => sum + countImages(msg), 0);
323
+ process.stderr.write(`[${EXTENSION_NAME}] Context contains ${totalImages} image(s); replacing with analysis text before provider request...\n`);
220
324
  const processed = await Promise.all(messages.map(async (msg) => {
221
- if ((msg.role !== "user" && msg.role !== "toolResult") || !Array.isArray(msg.content) || !hasImageContent(msg.content)) {
325
+ if (!isProcessableImageMessage(msg))
222
326
  return msg;
223
- }
224
- // Extract context from preceding text blocks
225
- const textBlocks = msg.content
226
- .filter((c) => c.type === "text")
227
- .map((c) => c.text)
228
- .join(" ");
229
- const contextText = textBlocks.substring(0, 500); // limit context
230
- const described = await describeImages(visionModel, msg.content, contextText, ctx);
231
- return { ...msg, content: described };
327
+ const content = await analyzeMessageImages(msg, ctx, analysisCache);
328
+ return { ...msg, content };
232
329
  }));
233
330
  return { messages: processed };
234
331
  });
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/agent-core/harness/compaction/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,kEAAkE;AAClE,MAAM,WAAW,cAAc;IAC9B,+CAA+C;IAC/C,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAClB,mDAAmD;IACnD,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrB,yCAAyC;IACzC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACpB;AAED,kDAAkD;AAClD,wBAAgB,aAAa,IAAI,cAAc,CAM9C;AAED,uEAAuE;AACvE,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CA2B9F;AAED,oFAAoF;AACpF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,GAAG;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAA;CAAE,CAK1G;AAED,kDAAkD;AAClD,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,MAAM,CAUzF;AAkBD,sEAAsE;AACtE,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAqDjE"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/agent-core/harness/compaction/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,kEAAkE;AAClE,MAAM,WAAW,cAAc;IAC9B,+CAA+C;IAC/C,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAClB,mDAAmD;IACnD,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrB,yCAAyC;IACzC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACpB;AAED,kDAAkD;AAClD,wBAAgB,aAAa,IAAI,cAAc,CAM9C;AAED,uEAAuE;AACvE,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CA2B9F;AAED,oFAAoF;AACpF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,GAAG;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAA;CAAE,CAK1G;AAED,kDAAkD;AAClD,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,MAAM,CAUzF;AAiDD,sEAAsE;AACtE,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAuDjE"}
@@ -59,6 +59,33 @@ export function formatFileOperations(readFiles, modifiedFiles) {
59
59
  return `\n\n${sections.join("\n\n")}`;
60
60
  }
61
61
  const TOOL_RESULT_MAX_CHARS = 2000;
62
+ const VISION_DESCRIPTION_PREFIX = "[Image description from ";
63
+ const VISION_EMPTY_PREFIX_RE = /^\[\d+ image\(s\) — vision model returned empty description\]$/m;
64
+ const VISION_FAILED_PREFIX_RE = /^\[\d+ image\(s\) omitted — vision fallback failed: .*\]$/m;
65
+ function stripVisionDescriptionBlocks(text) {
66
+ if (!text.includes(VISION_DESCRIPTION_PREFIX) && !VISION_EMPTY_PREFIX_RE.test(text) && !VISION_FAILED_PREFIX_RE.test(text)) {
67
+ return text;
68
+ }
69
+ const lines = text.split("\n");
70
+ const kept = [];
71
+ let skippingVisionBlock = false;
72
+ for (const line of lines) {
73
+ if (!skippingVisionBlock && line.startsWith(VISION_DESCRIPTION_PREFIX)) {
74
+ skippingVisionBlock = true;
75
+ continue;
76
+ }
77
+ if (skippingVisionBlock) {
78
+ if (line === "]")
79
+ skippingVisionBlock = false;
80
+ continue;
81
+ }
82
+ if (VISION_EMPTY_PREFIX_RE.test(line) || VISION_FAILED_PREFIX_RE.test(line)) {
83
+ continue;
84
+ }
85
+ kept.push(line);
86
+ }
87
+ return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
88
+ }
62
89
  function safeJsonStringify(value) {
63
90
  try {
64
91
  return JSON.stringify(value) ?? "undefined";
@@ -78,12 +105,13 @@ export function serializeConversation(messages) {
78
105
  const parts = [];
79
106
  for (const msg of messages) {
80
107
  if (msg.role === "user") {
81
- const content = typeof msg.content === "string"
108
+ const rawContent = typeof msg.content === "string"
82
109
  ? msg.content
83
110
  : msg.content
84
111
  .filter((c) => c.type === "text")
85
112
  .map((c) => c.text)
86
113
  .join("");
114
+ const content = stripVisionDescriptionBlocks(rawContent);
87
115
  if (content)
88
116
  parts.push(`[User]: ${content}`);
89
117
  }
@@ -117,10 +145,11 @@ export function serializeConversation(messages) {
117
145
  }
118
146
  }
119
147
  else if (msg.role === "toolResult") {
120
- const content = msg.content
148
+ const rawContent = msg.content
121
149
  .filter((c) => c.type === "text")
122
150
  .map((c) => c.text)
123
151
  .join("");
152
+ const content = stripVisionDescriptionBlocks(rawContent);
124
153
  if (content) {
125
154
  parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
126
155
  }
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/compaction/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAMpD,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAClB,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrB,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACpB;AAED,wBAAgB,aAAa,IAAI,cAAc,CAM9C;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CA2B9F;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,GAAG;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAA;CAAE,CAK1G;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,MAAM,CAUzF;AAmBD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAqDjE;AAMD,eAAO,MAAM,2BAA2B,oUAEmF,CAAC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../../src/sdk/coding-agent/core/compaction/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAMpD,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAClB,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrB,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACpB;AAED,wBAAgB,aAAa,IAAI,cAAc,CAM9C;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CA2B9F;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,GAAG;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAA;CAAE,CAK1G;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,MAAM,CAUzF;AA2DD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAuDjE;AAMD,eAAO,MAAM,2BAA2B,oUAEmF,CAAC"}
@@ -72,6 +72,41 @@ export function formatFileOperations(readFiles, modifiedFiles) {
72
72
  // ============================================================================
73
73
  /** Maximum characters for a tool result in serialized summaries. */
74
74
  const TOOL_RESULT_MAX_CHARS = 2000;
75
+ const VISION_DESCRIPTION_PREFIX = "[Image description from ";
76
+ const VISION_EMPTY_PREFIX_RE = /^\[\d+ image\(s\) — vision model returned empty description\]$/m;
77
+ const VISION_FAILED_PREFIX_RE = /^\[\d+ image\(s\) omitted — vision fallback failed: .*\]$/m;
78
+ /**
79
+ * Remove synthetic vision-fallback description blocks from summarized text.
80
+ *
81
+ * These blocks are transport glue for non-vision main models. They should help
82
+ * the active turn see images, but they should not become durable conversation
83
+ * memory or dominate compaction summaries.
84
+ */
85
+ function stripVisionDescriptionBlocks(text) {
86
+ if (!text.includes(VISION_DESCRIPTION_PREFIX) && !VISION_EMPTY_PREFIX_RE.test(text) && !VISION_FAILED_PREFIX_RE.test(text)) {
87
+ return text;
88
+ }
89
+ const lines = text.split("\n");
90
+ const kept = [];
91
+ let skippingVisionBlock = false;
92
+ for (const line of lines) {
93
+ if (!skippingVisionBlock && line.startsWith(VISION_DESCRIPTION_PREFIX)) {
94
+ skippingVisionBlock = true;
95
+ continue;
96
+ }
97
+ if (skippingVisionBlock) {
98
+ if (line === "]") {
99
+ skippingVisionBlock = false;
100
+ }
101
+ continue;
102
+ }
103
+ if (VISION_EMPTY_PREFIX_RE.test(line) || VISION_FAILED_PREFIX_RE.test(line)) {
104
+ continue;
105
+ }
106
+ kept.push(line);
107
+ }
108
+ return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
109
+ }
75
110
  /**
76
111
  * Truncate text to a maximum character length for summarization.
77
112
  * Keeps the beginning and appends a truncation marker.
@@ -94,12 +129,13 @@ export function serializeConversation(messages) {
94
129
  const parts = [];
95
130
  for (const msg of messages) {
96
131
  if (msg.role === "user") {
97
- const content = typeof msg.content === "string"
132
+ const rawContent = typeof msg.content === "string"
98
133
  ? msg.content
99
134
  : msg.content
100
135
  .filter((c) => c.type === "text")
101
136
  .map((c) => c.text)
102
137
  .join("");
138
+ const content = stripVisionDescriptionBlocks(rawContent);
103
139
  if (content)
104
140
  parts.push(`[User]: ${content}`);
105
141
  }
@@ -133,10 +169,11 @@ export function serializeConversation(messages) {
133
169
  }
134
170
  }
135
171
  else if (msg.role === "toolResult") {
136
- const content = msg.content
172
+ const rawContent = msg.content
137
173
  .filter((c) => c.type === "text")
138
174
  .map((c) => c.text)
139
175
  .join("");
176
+ const content = stripVisionDescriptionBlocks(rawContent);
140
177
  if (content) {
141
178
  parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
142
179
  }
@@ -168,6 +168,19 @@ export declare class AgentBridge {
168
168
  * The `id` we register is the raw `modelId` (e.g. `claude-3-5-haiku-latest`),
169
169
  * which is exactly what the backend expects in `body.model`.
170
170
  */
171
+ /**
172
+ * Determine the input modalities for a model.
173
+ *
174
+ * When the admin explicitly marks a model as the default vision model
175
+ * (isVisionDefault = true), we always include "image" in the input array
176
+ * regardless of what the backend's capabilities-derived `supportsImages`
177
+ * flag reports. Custom/built-in models may lack proper modality metadata,
178
+ * but the admin knows this model can handle images.
179
+ *
180
+ * If `supportsImages` is explicitly false, the vision extension will use
181
+ * the configured default/fallback vision model before the main model call.
182
+ */
183
+ private modelInput;
171
184
  private registerSyntheticProviders;
172
185
  /**
173
186
  * Apply a sticky model selection to the underlying spectral session, if it
@@ -1 +1 @@
1
- {"version":3,"file":"agent-bridge.d.ts","sourceRoot":"","sources":["../../src/server/agent-bridge.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent-bridge.d.ts","sourceRoot":"","sources":["../../src/server/agent-bridge.ts"],"names":[],"mappings":"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"}
@@ -688,6 +688,26 @@ export class AgentBridge {
688
688
  * The `id` we register is the raw `modelId` (e.g. `claude-3-5-haiku-latest`),
689
689
  * which is exactly what the backend expects in `body.model`.
690
690
  */
691
+ /**
692
+ * Determine the input modalities for a model.
693
+ *
694
+ * When the admin explicitly marks a model as the default vision model
695
+ * (isVisionDefault = true), we always include "image" in the input array
696
+ * regardless of what the backend's capabilities-derived `supportsImages`
697
+ * flag reports. Custom/built-in models may lack proper modality metadata,
698
+ * but the admin knows this model can handle images.
699
+ *
700
+ * If `supportsImages` is explicitly false, the vision extension will use
701
+ * the configured default/fallback vision model before the main model call.
702
+ */
703
+ modelInput(m) {
704
+ if (m.isVisionDefault || m.supportsImages !== false) {
705
+ return ["text", "image"];
706
+ }
707
+ console.info(`[agent-bridge] Model ${m.modelId} (${m.provider}) reports supportsImages: false ` +
708
+ `— not adding image to input modalities (vision extension will analyze images before provider requests)`);
709
+ return ["text"];
710
+ }
691
711
  registerSyntheticProviders(allowedModels) {
692
712
  if (!this.modelRegistry)
693
713
  return;
@@ -714,7 +734,7 @@ export class AgentBridge {
714
734
  provider: SPECTRAL_PROXY_ANTHROPIC,
715
735
  baseUrl,
716
736
  reasoning: m.supportsReasoning ?? false,
717
- input: m.supportsImages !== false ? ["text", "image"] : ["text"],
737
+ input: this.modelInput(m),
718
738
  cost: getModelCost(m),
719
739
  contextWindow: m.contextWindow ?? 0,
720
740
  maxTokens: 0,
@@ -740,7 +760,7 @@ export class AgentBridge {
740
760
  provider: SPECTRAL_PROXY_OPENAI,
741
761
  baseUrl,
742
762
  reasoning: m.supportsReasoning ?? false,
743
- input: m.supportsImages !== false ? ["text", "image"] : ["text"],
763
+ input: this.modelInput(m),
744
764
  cost: getModelCost(m),
745
765
  contextWindow: m.contextWindow ?? 0,
746
766
  maxTokens: 0,
@@ -768,7 +788,7 @@ export class AgentBridge {
768
788
  provider: SPECTRAL_PROXY_USER_MODEL,
769
789
  baseUrl,
770
790
  reasoning: m.supportsReasoning ?? false,
771
- input: m.supportsImages !== false ? ["text", "image"] : ["text"],
791
+ input: this.modelInput(m),
772
792
  cost: getModelCost(m),
773
793
  contextWindow: m.contextWindow ?? 0,
774
794
  maxTokens: 0,
@@ -1393,6 +1413,7 @@ export class AgentBridge {
1393
1413
  // Forward compaction lifecycle events to the wire so the browser
1394
1414
  // can surface a "Compacting…" indicator and track compaction
1395
1415
  // activity.
1416
+ this.memoryPhase = "compacting";
1396
1417
  this.opts.emit({
1397
1418
  type: "compaction_start",
1398
1419
  reason: ev.reason,
@@ -1410,6 +1431,7 @@ export class AgentBridge {
1410
1431
  return;
1411
1432
  }
1412
1433
  case "compaction_end": {
1434
+ this.memoryPhase = "idle";
1413
1435
  this.opts.emit({
1414
1436
  type: "compaction_end",
1415
1437
  summary: ev.result?.summary ?? "",
@@ -1 +1 @@
1
- {"version":3,"file":"session-stream.d.ts","sourceRoot":"","sources":["../../src/server/session-stream.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session-stream.d.ts","sourceRoot":"","sources":["../../src/server/session-stream.ts"],"names":[],"mappings":"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"}
@@ -1240,12 +1240,19 @@ export class SessionStreamManager {
1240
1240
  ? `Compaction complete${tokensMsg}. Summary: ${event.summary}`
1241
1241
  : `Compaction complete${tokensMsg}.`;
1242
1242
  try {
1243
- this.store.appendMessage(stream.sessionId, {
1244
- role: "system",
1245
- content: sysMsg,
1246
- eventsJsonl: "",
1247
- createdAt: Date.now(),
1248
- });
1243
+ // Update the existing "compacting…" system message in-place rather than
1244
+ // appending a second system row. This keeps history replay at one entry
1245
+ // per compaction instead of two (start + end). Falls back to append if
1246
+ // no system message exists (shouldn't happen, but defensive).
1247
+ const updated = this.store.updateLastSystemMessage(stream.sessionId, sysMsg);
1248
+ if (!updated) {
1249
+ this.store.appendMessage(stream.sessionId, {
1250
+ role: "system",
1251
+ content: sysMsg,
1252
+ eventsJsonl: "",
1253
+ createdAt: Date.now(),
1254
+ });
1255
+ }
1249
1256
  }
1250
1257
  catch {
1251
1258
  // best-effort
@@ -94,6 +94,7 @@ export declare class SessionStore {
94
94
  private stmtAppendMessage;
95
95
  private stmtDeleteMessage;
96
96
  private stmtDeleteMessagesBySession;
97
+ private stmtUpdateLastSystemMessage;
97
98
  private stmtTouchSession;
98
99
  private stmtRenameSession;
99
100
  private stmtSessionWithCount;
@@ -156,6 +157,15 @@ export declare class SessionStore {
156
157
  /** Delete all persisted messages for a session. */
157
158
  deleteMessagesBySession(sessionId: string): void;
158
159
  appendMessage(sessionId: string, msg: AppendMessageInput): StoredMessage;
160
+ /**
161
+ * Update the content of the most-recent system message for a session.
162
+ * Used by `compaction_end` to update the initial "compacting…" message in
163
+ * place rather than appending a second system row, so history replay shows
164
+ * a single compaction entry instead of two.
165
+ *
166
+ * Returns true if a row was updated, false if no system message exists yet.
167
+ */
168
+ updateLastSystemMessage(sessionId: string, content: string): boolean;
159
169
  /**
160
170
  * Rename a session. Bumps `updated_at`. Returns the updated summary, or
161
171
  * null if the session doesn't exist. Title is trimmed; an empty title falls
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/server/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAUH,OAAO,KAAK,EACV,eAAe,EACf,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAGjE,sEAAsE;AACtE,MAAM,MAAM,aAAa,GAAG,WAAW,CAAC;AAExC,uCAAuC;AACvC,MAAM,WAAW,kBAAkB;IACjC,wDAAwD;IACxD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,wDAAwD;IACxD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,sBAAsB,CAAC;IAChC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AA0LD,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,YAAY;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,CAAgB;IAC1B,OAAO,CAAC,MAAM,CAAS;IAGvB,OAAO,CAAC,gBAAgB,CAA2C;IACnE,OAAO,CAAC,cAAc,CAAwC;IAC9D,OAAO,CAAC,iBAAiB,CAA4D;IACrF,OAAO,CAAC,iBAAiB,CAAoD;IAC7E,OAAO,CAAC,iBAAiB,CAA4B;IACrD,OAAO,CAAC,yBAAyB,CAAiD;IAClF,OAAO,CAAC,2BAA2B,CAA4C;IAG/E,OAAO,CAAC,cAAc,CAAwC;IAC9D,OAAO,CAAC,iBAAiB,CAA4D;IACrF,OAAO,CAAC,iBAAiB,CAA4B;IACrD,OAAO,CAAC,gBAAgB,CAAwC;IAChE,OAAO,CAAC,iBAAiB,CAEvB;IACF,OAAO,CAAC,iBAAiB,CAA4B;IACrD,OAAO,CAAC,2BAA2B,CAA4B;IAC/D,OAAO,CAAC,gBAAgB,CAAoC;IAC5D,OAAO,CAAC,iBAAiB,CAA4C;IACrE,OAAO,CAAC,oBAAoB,CAAiD;IAC7E,OAAO,CAAC,4BAA4B,CAAsD;IAC1F,OAAO,CAAC,+BAA+B,CAA4D;IACnG,OAAO,CAAC,+BAA+B,CAA4B;IAEnE,OAAO,CAAC,mBAAmB,CAAyD;IACpF,OAAO,CAAC,mBAAmB,CAA2C;IACtE,OAAO,CAAC,6BAA6B,CAAiE;IACtG,OAAO,CAAC,6BAA6B,CAA2C;IAEhF,OAAO,CAAC,wBAAwB,CAA2C;IAC3E,OAAO,CAAC,wBAAwB,CAAuE;IAEvG,OAAO,CAAC,oBAAoB,CAAoE;IAChG,OAAO,CAAC,oBAAoB,CAA2D;IACvF,OAAO,CAAC,mBAAmB,CAA4C;IAGvE,OAAO,CAAC,iBAAiB,CAA4D;IACrF,OAAO,CAAC,kBAAkB,CAAsC;IAChE,OAAO,CAAC,iBAAiB,CAAsC;IAC/D,OAAO,CAAC,mBAAmB,CAAoC;IAC/D,OAAO,CAAC,qBAAqB,CAA4B;IACzD,OAAO,CAAC,kBAAkB,CAA4C;gBAE1D,IAAI,EAAE,MAAM;IAgPxB,8DAA8D;IAC9D,UAAU,IAAI,MAAM,EAAE;IAWtB,YAAY,IAAI,WAAW,EAAE;IAc7B,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,WAAW;IAgBrD,iCAAiC;IACjC,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAgB1C;;;;OAIG;IACH,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,GAAG,WAAW,GAAG,IAAI;IAYxE;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,EAAE,CAAA;KAAE;IAMrE,4DAA4D;IAC5D,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAe9D,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,kBAAkB;IAyB5D,iCAAiC;IACjC,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAoBhD,oEAAoE;IACpE,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAK9C,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,EAAE;IAc/C;;;OAGG;IACH,kEAAkE;IAClE,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAY/B,mDAAmD;IACnD,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAYhD,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,kBAAkB,GAAG,aAAa;IAwBxE;;;;OAIG;IACH,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI;IAiBnE,+DAA+D;IAC/D,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IASlC,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,qBAAqB,GAAG,IAAI;IAqBzE,2BAA2B,CACzB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,IAAI,CAAC,qBAAqB,EAAE,WAAW,GAAG,WAAW,CAAC,GAC/D,IAAI;IAWP,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAQpD;;;;;;;OAOG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAKjD;;;;;;OAMG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAIhE;;;;OAIG;IACH,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAK3D;;;;OAIG;IACH,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAQzE;;;;;OAKG;IACH,WAAW,CACT,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,GAC/C,kBAAkB;IAoDrB;;;OAGG;IACH,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAKtD;;;OAGG;IACH,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAQ/C;;;OAGG;IACH,yBAAyB,CACvB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,EACvE,SAAS,EAAE,MAAM,GAChB,IAAI;IAUP;;OAEG;IACH,yBAAyB,CACvB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,KAAK,SAAK,GACT,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAWtF;;;OAGG;IACH,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAS3C;;;OAGG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,QAAQ;IAW3D;;;;OAIG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IAYjD;;OAEG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,EAAE;IAI7C;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAUrD;;OAEG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAIzC,KAAK,IAAI,IAAI;CAKd;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAkB/D"}
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/server/storage.ts"],"names":[],"mappings":"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"}
@@ -180,6 +180,7 @@ export class SessionStore {
180
180
  stmtAppendMessage;
181
181
  stmtDeleteMessage;
182
182
  stmtDeleteMessagesBySession;
183
+ stmtUpdateLastSystemMessage;
183
184
  stmtTouchSession;
184
185
  stmtRenameSession;
185
186
  stmtSessionWithCount;
@@ -317,6 +318,13 @@ export class SessionStore {
317
318
  VALUES (?, ?, ?, ?, ?, ?, ?)`);
318
319
  this.stmtDeleteMessage = this.db.prepare(`DELETE FROM messages WHERE id = ?`);
319
320
  this.stmtDeleteMessagesBySession = this.db.prepare(`DELETE FROM messages WHERE session_id = ?`);
321
+ this.stmtUpdateLastSystemMessage = this.db.prepare(`UPDATE messages SET content = ?
322
+ WHERE id = (
323
+ SELECT id FROM messages
324
+ WHERE session_id = ? AND role = 'system'
325
+ ORDER BY created_at DESC
326
+ LIMIT 1
327
+ )`);
320
328
  this.stmtTouchSession = this.db.prepare(`UPDATE sessions SET updated_at = ? WHERE id = ?`);
321
329
  this.stmtRenameSession = this.db.prepare(`UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?`);
322
330
  this.stmtSessionWithCount = this.db.prepare(`
@@ -561,6 +569,23 @@ export class SessionStore {
561
569
  ...(msg.images && msg.images.length > 0 ? { images: msg.images } : {}),
562
570
  };
563
571
  }
572
+ /**
573
+ * Update the content of the most-recent system message for a session.
574
+ * Used by `compaction_end` to update the initial "compacting…" message in
575
+ * place rather than appending a second system row, so history replay shows
576
+ * a single compaction entry instead of two.
577
+ *
578
+ * Returns true if a row was updated, false if no system message exists yet.
579
+ */
580
+ updateLastSystemMessage(sessionId, content) {
581
+ try {
582
+ const info = this.stmtUpdateLastSystemMessage.run(content, sessionId);
583
+ return info.changes > 0;
584
+ }
585
+ catch {
586
+ return false;
587
+ }
588
+ }
564
589
  /**
565
590
  * Rename a session. Bumps `updated_at`. Returns the updated summary, or
566
591
  * null if the session doesn't exist. Title is trimmed; an empty title falls
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.9.13",
3
+ "version": "0.9.14",
4
4
  "description": "AI coding agent for Aexol with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,