wendkeep 0.71.1 → 0.72.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.
@@ -0,0 +1,21 @@
1
+ CREATE TABLE IF NOT EXISTS transcripts (
2
+ transcript_id TEXT PRIMARY KEY,
3
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
4
+ session_id TEXT NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE,
5
+ agent_id TEXT NOT NULL REFERENCES agent_runs(agent_id) ON DELETE CASCADE,
6
+ coverage TEXT NOT NULL DEFAULT 'summary_only',
7
+ codec TEXT NOT NULL DEFAULT 'gzip',
8
+ content_gzip BLOB NOT NULL,
9
+ content_sha256 TEXT NOT NULL,
10
+ original_bytes INTEGER NOT NULL,
11
+ compressed_bytes INTEGER NOT NULL,
12
+ source TEXT NOT NULL DEFAULT '',
13
+ occurred_at TEXT NOT NULL,
14
+ metadata_json TEXT NOT NULL DEFAULT '{}'
15
+ );
16
+
17
+ CREATE INDEX IF NOT EXISTS idx_transcripts_project_session
18
+ ON transcripts(project_id, session_id, occurred_at);
19
+
20
+ CREATE INDEX IF NOT EXISTS idx_transcripts_project_coverage
21
+ ON transcripts(project_id, coverage);
@@ -68,7 +68,8 @@ function entityType(logicalPath) {
68
68
  }
69
69
 
70
70
  function shouldSkip(name, relativePath) {
71
- if (TRANSIENT_NAMES.has(name) || name.endsWith('.tmp') || name.endsWith('.lock')) return true;
71
+ if (TRANSIENT_NAMES.has(name) || name === 'observer-sql-state.json' || name === 'observer-sql-outbox'
72
+ || name.endsWith('.tmp') || name.endsWith('.lock')) return true;
72
73
  if (relativePath === MEMORY_STATE_FILE) return true;
73
74
  return false;
74
75
  }
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { buildProjectSnapshot } from './observer-snapshot.mjs';
4
- import { publishObserverMemory } from './observer-memory-publish.mjs';
4
+ import { publishObserverSql } from './observer-sql-publish.mjs';
5
5
 
6
6
  const OUTBOX_REL = join('.brain', 'observer-outbox');
7
7
  const REQUEST_TIMEOUT_MS = 500;
@@ -90,23 +90,23 @@ export async function publishObserverSnapshot({
90
90
  projectRoot,
91
91
  url = process.env.WENDKEEP_OBSERVER_URL || '',
92
92
  now = new Date(),
93
+ input = {},
93
94
  } = {}) {
94
95
  try {
95
96
  const event = buildProjectSnapshot({ vaultBase, projectRoot, now });
96
- if (!url) return { ok: true, skipped: true, queued: false, hookExitCode: 0, event_id: event.event_id };
97
+ const sql = await publishObserverSql({
98
+ vaultBase,
99
+ projectId: event.project_id,
100
+ url,
101
+ input,
102
+ now,
103
+ });
104
+ if (!url) return { ok: sql.ok, skipped: true, queued: sql.queued, hookExitCode: 0, event_id: event.event_id, sql };
97
105
 
98
106
  await retryObserverOutbox({ vaultBase, url });
99
- let memory;
100
- try {
101
- memory = await publishObserverMemory({
102
- vaultBase,
103
- projectId: event.project_id,
104
- url,
105
- now,
106
- });
107
- } catch (error) {
108
- memory = { ok: false, queued: false, error: error.message };
109
- }
107
+ // SQL is the live authority. Keep the legacy-shaped `memory` field for
108
+ // older integrations while reporting the real SQL publication separately.
109
+ const memory = { ok: sql.ok, queued: sql.queued, changed: sql.changed, pending: sql.pending, authority: 'sqlite' };
110
110
  try {
111
111
  const response = await postSnapshot(url, event);
112
112
  return {
@@ -116,6 +116,7 @@ export async function publishObserverSnapshot({
116
116
  event_id: event.event_id,
117
117
  duplicate: response.duplicate === true,
118
118
  memory,
119
+ sql,
119
120
  };
120
121
  } catch (error) {
121
122
  queueOutbox(vaultBase, event);
@@ -126,6 +127,7 @@ export async function publishObserverSnapshot({
126
127
  event_id: event.event_id,
127
128
  error: error.message,
128
129
  memory,
130
+ sql,
129
131
  };
130
132
  }
131
133
  } catch (error) {
@@ -1,6 +1,7 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { gunzipSync } from 'node:zlib';
4
5
  import {
5
6
  appendObserverEvent,
6
7
  getObserverProject,
@@ -10,19 +11,35 @@ import {
10
11
  } from './observer-store.mjs';
11
12
  import { MAX_SNAPSHOT_BYTES, validateObserverSnapshot } from './observer-snapshot.mjs';
12
13
  import {
13
- applyMemoryEvent,
14
- exportMemoryBundle,
15
- readMemoryDocument,
16
- readMemorySync,
17
- readMemoryTree,
18
14
  setMemoryMode,
19
- searchMemory,
20
15
  validateMemoryEvent,
21
16
  } from './observer-memory.mjs';
17
+ import {
18
+ OBSERVER_SQL_FILE,
19
+ OBSERVER_SQL_SCHEMA_VERSION,
20
+ ensureObserverDatabase,
21
+ ingestObserverEvents,
22
+ migrateObserverDatabase,
23
+ readSqlProject,
24
+ listSqlProjects,
25
+ readSqlDocument,
26
+ readSqlSync,
27
+ readSqlTree,
28
+ searchSqlDocuments,
29
+ exportSqlMemoryBundle,
30
+ readTranscript,
31
+ readUsageBreakdown,
32
+ readUsageCalls,
33
+ readUsageSummary,
34
+ registerSqlProject,
35
+ } from './observer-sql-store.mjs';
36
+ import { migrateObserverContainerData } from './observer-sql-migrate.mjs';
22
37
 
23
38
  const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
24
39
  const MAX_BODY_BYTES = MAX_SNAPSHOT_BYTES + 4096;
25
40
  const MAX_MEMORY_BODY_BYTES = 8 * 1024 * 1024;
41
+ const MAX_SQL_BODY_BYTES = 64 * 1024 * 1024;
42
+ const MAX_SQL_EXPANDED_BODY_BYTES = 256 * 1024 * 1024;
26
43
  const STATIC_ROOT = fileURLToPath(new URL('../web/observer/', import.meta.url));
27
44
  const STATIC_ASSETS = new Map([
28
45
  ['/index.html', { file: 'index.html', type: 'text/html; charset=utf-8' }],
@@ -67,7 +84,7 @@ function serveStatic(res, pathname) {
67
84
  return true;
68
85
  }
69
86
 
70
- function readBody(req, maxBytes = MAX_BODY_BYTES) {
87
+ function readBody(req, maxBytes = MAX_BODY_BYTES, { gunzip = false, expandedMaxBytes = maxBytes } = {}) {
71
88
  return new Promise((resolve, reject) => {
72
89
  let size = 0;
73
90
  let tooLarge = false;
@@ -85,7 +102,24 @@ function readBody(req, maxBytes = MAX_BODY_BYTES) {
85
102
  chunks.push(chunk);
86
103
  });
87
104
  req.on('end', () => {
88
- if (!tooLarge) resolve(Buffer.concat(chunks).toString('utf8'));
105
+ if (tooLarge) return;
106
+ let body = Buffer.concat(chunks);
107
+ if (gunzip && String(req.headers['content-encoding'] || '').toLowerCase() === 'gzip') {
108
+ try { body = gunzipSync(body); }
109
+ catch {
110
+ const error = new Error('corpo gzip inválido.');
111
+ error.code = 'invalid_content_encoding';
112
+ reject(error);
113
+ return;
114
+ }
115
+ if (body.length > expandedMaxBytes) {
116
+ const error = new Error('corpo expandido acima do limite.');
117
+ error.code = 'payload_too_large';
118
+ reject(error);
119
+ return;
120
+ }
121
+ }
122
+ resolve(body.toString('utf8'));
89
123
  });
90
124
  req.on('error', reject);
91
125
  });
@@ -108,11 +142,41 @@ function projectIdFrom(parts) {
108
142
  return parts[0] === 'v1' && parts[1] === 'projects' && parts[2] ? parts[2] : '';
109
143
  }
110
144
 
111
- function projectKnown(dataDir, projectId) {
112
- return Boolean(
113
- getObserverProject(dataDir, projectId)
114
- || listRegisteredObserverProjects(dataDir).some((item) => item.projectId === projectId),
115
- );
145
+ function ensureSqlProjectRegistration(dataDir, sqlDb, projectId) {
146
+ try {
147
+ if (readSqlProject(sqlDb, projectId)) return true;
148
+ } catch (error) {
149
+ if (error?.code !== 'project_not_registered') throw error;
150
+ }
151
+ const legacy = getObserverProject(dataDir, projectId)
152
+ || listRegisteredObserverProjects(dataDir).find((item) => item.projectId === projectId);
153
+ if (!legacy) return false;
154
+ registerSqlProject(sqlDb, {
155
+ projectId: legacy.projectId,
156
+ projectName: legacy.projectName,
157
+ wendkeepVersion: legacy.snapshot?.wendkeep_version || legacy.wendkeepVersion || '',
158
+ });
159
+ return true;
160
+ }
161
+
162
+ function sqlMemoryEvent(projectId, event) {
163
+ return {
164
+ schema_version: 1,
165
+ event_id: event.event_id,
166
+ kind: event.operation === 'delete' ? 'document.delete' : 'document.upsert',
167
+ project_id: projectId,
168
+ occurred_at: event.captured_at || new Date().toISOString(),
169
+ payload: {
170
+ logical_path: event.logical_path,
171
+ entity_type: event.entity_type,
172
+ content: event.content || '',
173
+ content_hash: event.content_hash || '',
174
+ revision: event.revision || 1,
175
+ source_session_id: event.source_session_id || '',
176
+ source_turn_id: event.source_turn_id || '',
177
+ metadata: event.metadata || {},
178
+ },
179
+ };
116
180
  }
117
181
 
118
182
  export async function startObserverServer({
@@ -125,11 +189,35 @@ export async function startObserverServer({
125
189
  throw new Error(`Observer HTTP aceita somente host loopback; recebido: ${host}`);
126
190
  }
127
191
  if (!dataDir) throw new Error('dataDir é obrigatório.');
192
+ const sqlDb = ensureObserverDatabase(dataDir);
193
+ const databaseMigration = migrateObserverDatabase(sqlDb);
194
+ const legacyMigration = migrateObserverContainerData(dataDir, { database: sqlDb });
195
+ const registered = [
196
+ ...listRegisteredObserverProjects(dataDir),
197
+ ...readObserverIndex(dataDir).projects.map((item) => ({
198
+ projectId: item.projectId,
199
+ projectName: item.projectName,
200
+ wendkeepVersion: item.snapshot?.wendkeep_version || '',
201
+ })),
202
+ ];
203
+ for (const project of registered) registerSqlProject(sqlDb, project);
128
204
  const server = createServer(async (req, res) => {
129
205
  try {
130
206
  const pathname = new URL(req.url || '/', 'http://127.0.0.1').pathname;
131
207
  if (req.method === 'GET' && pathname === '/healthz') {
132
- json(res, 200, { ok: true, service: 'wendkeep-observer', schema_version: 1 });
208
+ json(res, 200, {
209
+ ok: true,
210
+ service: 'wendkeep-observer',
211
+ schema_version: 1,
212
+ database: {
213
+ engine: 'sqlite',
214
+ file: OBSERVER_SQL_FILE,
215
+ schema_version: OBSERVER_SQL_SCHEMA_VERSION,
216
+ migrations: databaseMigration.applied.length,
217
+ legacy_migration: legacyMigration,
218
+ ready: true,
219
+ },
220
+ });
133
221
  return;
134
222
  }
135
223
  if (req.method === 'GET' && (pathname === '/' || STATIC_ASSETS.has(pathname))) {
@@ -148,9 +236,20 @@ export async function startObserverServer({
148
236
 
149
237
  if (parts.length === 2 && req.method === 'GET') {
150
238
  const index = readObserverIndex(dataDir);
239
+ const legacy = new Map(index.projects.map(({ snapshot, ...summary }) => [summary.projectId, summary]));
240
+ for (const project of listSqlProjects(sqlDb)) {
241
+ const current = legacy.get(project.project_id) || {};
242
+ legacy.set(project.project_id, {
243
+ ...current,
244
+ projectId: project.project_id,
245
+ projectName: current.projectName || project.project_name,
246
+ wendkeepVersion: current.wendkeepVersion || project.wendkeep_version,
247
+ registeredAt: current.registeredAt || project.registered_at,
248
+ });
249
+ }
151
250
  json(res, 200, {
152
251
  schema_version: index.schema_version,
153
- projects: index.projects.map(({ snapshot, ...summary }) => summary),
252
+ projects: [...legacy.values()].sort((a, b) => a.projectId.localeCompare(b.projectId)),
154
253
  });
155
254
  return;
156
255
  }
@@ -160,18 +259,78 @@ export async function startObserverServer({
160
259
  errorResponse(res, 404, 'not_found', 'projeto não informado.');
161
260
  return;
162
261
  }
262
+ ensureSqlProjectRegistration(dataDir, sqlDb, projectId);
263
+
264
+ if (parts.length === 4 && parts[3] === 'ingest' && req.method === 'POST') {
265
+ if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
266
+ errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
267
+ return;
268
+ }
269
+ const body = parseJson(await readBody(req, MAX_SQL_BODY_BYTES, {
270
+ gunzip: true,
271
+ expandedMaxBytes: MAX_SQL_EXPANDED_BODY_BYTES,
272
+ }));
273
+ const events = Array.isArray(body.events) ? body.events : [];
274
+ if (events.length === 0) {
275
+ errorResponse(res, 400, 'invalid_ingest_batch', 'events deve conter pelo menos um evento.');
276
+ return;
277
+ }
278
+ const result = ingestObserverEvents(sqlDb, { projectId, events });
279
+ const status = result.conflicts > 0 ? 409 : result.rejected > 0 ? 400 : result.accepted > 0 ? 201 : 200;
280
+ json(res, status, result);
281
+ return;
282
+ }
283
+
284
+ if (parts.length >= 4 && parts[3] === 'usage' && req.method === 'GET') {
285
+ if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
286
+ errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
287
+ return;
288
+ }
289
+ const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
290
+ const filters = {
291
+ from: query.get('from') || '', to: query.get('to') || '', agentId: query.get('agent_id') || '', subagentId: query.get('subagent_id') || '',
292
+ sessionId: query.get('session_id') || '', changeSlug: query.get('change') || query.get('change_slug') || '', role: query.get('role') || '',
293
+ model: query.get('model') || '', provider: query.get('provider') || '', modelProvider: query.get('model_provider') || '',
294
+ limit: query.get('limit') || 100, offset: query.get('offset') || 0,
295
+ };
296
+ if (parts[4] === 'summary') {
297
+ json(res, 200, readUsageSummary(sqlDb, projectId, filters));
298
+ return;
299
+ }
300
+ if (parts[4] === 'breakdown') {
301
+ json(res, 200, readUsageBreakdown(sqlDb, projectId, filters));
302
+ return;
303
+ }
304
+ if (parts[4] === 'calls') {
305
+ json(res, 200, readUsageCalls(sqlDb, projectId, filters));
306
+ return;
307
+ }
308
+ }
309
+
310
+ if (parts.length === 5 && parts[3] === 'transcripts' && req.method === 'GET') {
311
+ if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
312
+ errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
313
+ return;
314
+ }
315
+ try {
316
+ json(res, 200, readTranscript(sqlDb, projectId, parts[4]));
317
+ } catch (error) {
318
+ errorResponse(res, error?.code === 'transcript_not_found' ? 404 : 400, error?.code || 'transcript_error', error?.message || 'transcript indisponível.');
319
+ }
320
+ return;
321
+ }
163
322
 
164
323
  if (parts.length === 4 && parts[3] === 'sync' && req.method === 'GET') {
165
- if (!projectKnown(dataDir, projectId)) {
324
+ if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
166
325
  errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
167
326
  return;
168
327
  }
169
- json(res, 200, readMemorySync(dataDir, projectId));
328
+ json(res, 200, { ...readSqlSync(sqlDb, projectId), mode: 'container-authority' });
170
329
  return;
171
330
  }
172
331
 
173
332
  if (parts.length === 4 && parts[3] === 'sync' && req.method === 'PUT') {
174
- if (!projectKnown(dataDir, projectId)) {
333
+ if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
175
334
  errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
176
335
  return;
177
336
  }
@@ -185,20 +344,21 @@ export async function startObserverServer({
185
344
  }
186
345
 
187
346
  if (parts.length >= 4 && parts[3] === 'memory') {
188
- if (!projectKnown(dataDir, projectId)) {
347
+ if (!ensureSqlProjectRegistration(dataDir, sqlDb, projectId)) {
189
348
  errorResponse(res, 404, 'project_not_found', 'projeto não encontrado: ' + projectId);
190
349
  return;
191
350
  }
192
351
  const memoryAction = parts[4] || '';
193
352
  if (memoryAction === 'tree' && req.method === 'GET') {
194
353
  const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
195
- json(res, 200, readMemoryTree(dataDir, projectId, query.get('prefix') || ''));
354
+ const tree = readSqlTree(sqlDb, projectId, query.get('prefix') || '');
355
+ json(res, 200, { ...tree, document_count: tree.documents.length });
196
356
  return;
197
357
  }
198
358
  if (memoryAction === 'document' && req.method === 'GET') {
199
359
  const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
200
360
  try {
201
- json(res, 200, readMemoryDocument(dataDir, projectId, query.get('path') || ''));
361
+ json(res, 200, readSqlDocument(sqlDb, projectId, query.get('path') || ''));
202
362
  } catch (error) {
203
363
  const status = error?.code === 'memory_not_found' ? 404 : 400;
204
364
  errorResponse(res, status, error?.code || 'invalid_memory_path', error?.message || 'documento inválido.');
@@ -207,11 +367,11 @@ export async function startObserverServer({
207
367
  }
208
368
  if (memoryAction === 'search' && req.method === 'GET') {
209
369
  const query = new URL(req.url || '/', 'http://127.0.0.1').searchParams;
210
- json(res, 200, { project_id: projectId, query: query.get('q') || '', results: searchMemory(dataDir, projectId, query.get('q') || '') });
370
+ json(res, 200, { project_id: projectId, query: query.get('q') || '', results: searchSqlDocuments(sqlDb, projectId, query.get('q') || '') });
211
371
  return;
212
372
  }
213
373
  if (memoryAction === 'export' && req.method === 'GET') {
214
- json(res, 200, exportMemoryBundle(dataDir, projectId));
374
+ json(res, 200, { ...exportSqlMemoryBundle(sqlDb, projectId), mode: 'container-authority' });
215
375
  return;
216
376
  }
217
377
  if (memoryAction === 'events' && req.method === 'POST') {
@@ -221,30 +381,36 @@ export async function startObserverServer({
221
381
  errorResponse(res, 400, 'invalid_memory_batch', 'events deve conter pelo menos um evento.');
222
382
  return;
223
383
  }
224
- const results = [];
384
+ const validationErrors = [];
225
385
  for (const event of events) {
226
- if (event?.project_id !== projectId) {
227
- results.push({ accepted: false, errors: ['project_id do evento não corresponde à rota.'] });
228
- continue;
229
- }
230
386
  const validation = validateMemoryEvent(event);
231
- results.push(validation.ok ? applyMemoryEvent(dataDir, event) : { accepted: false, errors: validation.errors });
387
+ if (!validation.ok) validationErrors.push(...validation.errors);
232
388
  }
233
- const accepted = results.filter((item) => item.accepted).length;
234
- const duplicates = results.filter((item) => item.duplicate).length;
235
- const conflicts = results.filter((item) => item.conflict).length;
236
- const rejected = results.length - accepted - duplicates - conflicts;
237
- if (conflicts > 0 || rejected > 0) {
238
- json(res, conflicts > 0 ? 409 : 400, { accepted, duplicates, conflicts, rejected, results });
389
+ if (validationErrors.length) {
390
+ errorResponse(res, 400, 'invalid_memory_batch', validationErrors.join(' '));
239
391
  return;
240
392
  }
241
- json(res, accepted > 0 ? 201 : 200, { accepted, duplicates, conflicts, rejected, results });
393
+ const result = ingestObserverEvents(sqlDb, { projectId, events: events.map((event) => sqlMemoryEvent(projectId, event)) });
394
+ const status = result.conflicts > 0 ? 409 : result.rejected > 0 ? 400 : result.accepted > 0 ? 201 : 200;
395
+ json(res, status, result);
242
396
  return;
243
397
  }
244
398
  }
245
399
 
246
400
  if (parts.length === 3 && req.method === 'GET') {
247
- const project = getObserverProject(dataDir, projectId);
401
+ let project = getObserverProject(dataDir, projectId);
402
+ if (!project) {
403
+ try {
404
+ const sqlProject = readSqlProject(sqlDb, projectId);
405
+ project = {
406
+ projectId: sqlProject.project_id,
407
+ projectName: sqlProject.project_name,
408
+ wendkeepVersion: sqlProject.wendkeep_version,
409
+ registeredAt: sqlProject.registered_at,
410
+ eventCount: 0,
411
+ };
412
+ } catch { /* handled as not found below */ }
413
+ }
248
414
  if (!project) {
249
415
  errorResponse(res, 404, 'project_not_found', `projeto não encontrado: ${projectId}`);
250
416
  return;
@@ -268,6 +434,11 @@ export async function startObserverServer({
268
434
  errorResponse(res, 400, 'invalid_project', result.errors.join(' '));
269
435
  return;
270
436
  }
437
+ registerSqlProject(sqlDb, {
438
+ projectId,
439
+ projectName: body.project_name,
440
+ wendkeepVersion: body.wendkeep_version,
441
+ });
271
442
  json(res, 201, result.project);
272
443
  return;
273
444
  }
@@ -275,7 +446,15 @@ export async function startObserverServer({
275
446
  if (parts.length === 4 && parts[3] === 'changes' && req.method === 'GET') {
276
447
  const project = getObserverProject(dataDir, projectId);
277
448
  if (!project) {
278
- errorResponse(res, 404, 'project_not_found', `projeto não encontrado: ${projectId}`);
449
+ let sqlProject;
450
+ try { sqlProject = readSqlProject(sqlDb, projectId); } catch (error) {
451
+ if (error?.code !== 'project_not_registered') throw error;
452
+ }
453
+ if (!sqlProject) {
454
+ errorResponse(res, 404, 'project_not_found', `projeto não encontrado: ${projectId}`);
455
+ return;
456
+ }
457
+ json(res, 200, { project_id: projectId, changes: [] });
279
458
  return;
280
459
  }
281
460
  json(res, 200, { project_id: projectId, changes: project.snapshot?.changes || [] });
@@ -306,7 +485,7 @@ export async function startObserverServer({
306
485
  errorResponse(res, 404, 'not_found', 'rota não encontrada.');
307
486
  } catch (error) {
308
487
  if (res.headersSent) return;
309
- const status = error?.code === 'payload_too_large' ? 413 : error?.code === 'invalid_json' ? 400 : 500;
488
+ const status = error?.code === 'payload_too_large' ? 413 : ['invalid_json', 'invalid_content_encoding'].includes(error?.code) ? 400 : 500;
310
489
  errorResponse(res, status, error?.code || 'observer_error', error?.message || 'erro interno do Observer.');
311
490
  }
312
491
  });
@@ -328,6 +507,10 @@ export async function startObserverServer({
328
507
  return {
329
508
  server,
330
509
  address: () => server.address(),
331
- close: () => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))),
510
+ close: () => new Promise((resolve, reject) => server.close((error) => {
511
+ try { sqlDb.close(); } catch { /* already closed */ }
512
+ if (error) reject(error);
513
+ else resolve();
514
+ })),
332
515
  };
333
516
  }