wendkeep 0.73.0 → 0.75.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.en.md +13 -9
  3. package/README.md +13 -9
  4. package/docs/en/commands/memory.md +16 -1
  5. package/docs/en/commands/observer.md +29 -11
  6. package/docs/en/commands/operating-profiles.md +1 -1
  7. package/docs/pt-BR/commands/memory.md +16 -1
  8. package/docs/pt-BR/commands/observer.md +28 -11
  9. package/docs/pt-BR/commands/operating-profiles.md +1 -1
  10. package/hooks/brain-core.mjs +2 -0
  11. package/hooks/brain-recall.mjs +5 -1
  12. package/hooks/evidence-context.mjs +41 -0
  13. package/hooks/evidence-recall.mjs +1 -0
  14. package/hooks/memory-scope.mjs +1 -0
  15. package/package.json +2 -2
  16. package/packages/cli/src/index.mjs +2 -2
  17. package/packages/integrations/src/host-hooks.mjs +1 -0
  18. package/packages/vault/src/evidence-recall.mjs +343 -0
  19. package/packages/vault/src/index.mjs +2 -0
  20. package/packages/vault/src/memory-handoff.mjs +58 -3
  21. package/packages/vault/src/memory-schema.mjs +12 -2
  22. package/packages/vault/src/memory-scope.mjs +119 -0
  23. package/packages/vault/src/memory-store.mjs +86 -24
  24. package/schema/observer/004-evidence-recall.sql +25 -0
  25. package/schema/observer/005-project-scoped-identities.sql +217 -0
  26. package/src/change.mjs +41 -1
  27. package/src/doctor.mjs +5 -0
  28. package/src/init.mjs +2 -2
  29. package/src/memory.mjs +95 -2
  30. package/src/note.mjs +8 -1
  31. package/src/observer-publish.mjs +9 -34
  32. package/src/observer-server.mjs +38 -63
  33. package/src/observer-sql-migrate.mjs +1 -1
  34. package/src/observer-sql-publish.mjs +372 -12
  35. package/src/observer-sql-store.mjs +248 -32
  36. package/src/observer-store.mjs +15 -3
  37. package/src/observer.mjs +104 -14
  38. package/src/taxonomy.mjs +4 -0
package/src/doctor.mjs CHANGED
@@ -5,6 +5,7 @@ import { checkHarness, checkVaultLinks, checkSessionActivity, checkStackedFrontm
5
5
  import { runVaultHealth } from '../hooks/vault-health.mjs';
6
6
  import { checkSyncDefs } from './sync-defs.mjs';
7
7
  import { resolveProjectVault } from './project-vault.mjs';
8
+ import { inspectObserverSqlOutbox } from './observer-sql-publish.mjs';
8
9
 
9
10
  const healthStatusLabel = (status) => ({
10
11
  healthy: 'saudável', warning: 'atenção', degraded: 'degradada', blocked: 'bloqueada', legacy: 'legado',
@@ -155,6 +156,10 @@ export function runDoctor(argv) {
155
156
  // 3e. Observabilidade materializada: schema vigente não basta sem frontier + manifest frescos.
156
157
  const observability = checkSessionObservability(vaultBase);
157
158
  process.stdout.write(`\n${renderSessionObservabilityLines(observability).join('\n')}\n`);
159
+ const observerOutbox = inspectObserverSqlOutbox(vaultBase);
160
+ const oldestSeconds = Math.round(observerOutbox.oldest_age_ms / 1000);
161
+ process.stdout.write(`[observer] outbox SQL: ${observerOutbox.batches} lote(s) · ${observerOutbox.events} evento(s) · ${observerOutbox.bytes} bytes${observerOutbox.batches ? ` · mais antigo: ${oldestSeconds}s` : ''}\n`);
162
+ if (observerOutbox.batches) process.stdout.write(' → wendkeep observer reconcile --project . (ou inicie o servidor para o hook drenar a fila)\n');
158
163
 
159
164
  // 4. Sessão: não mente "inativa" quando há atividade recente (workflow/subagente em background).
160
165
  const act = checkSessionActivity(vaultBase);
package/src/init.mjs CHANGED
@@ -323,7 +323,7 @@ const MESSAGES = {
323
323
  mcpSkipped: ' [4/5] .mcp.json ignorado (--no-mcp, sem companions MCP)',
324
324
  colorsSkipped: ' [5/5] cores ignoradas (--no-colors)',
325
325
  colors: (r) => ` [5/5] cores: ${r}`,
326
- runtimeIgnore: ' [!] ignore runtimes locais do wendkeep no Git quando o vault for versionado: .brain/.change-* .brain/runtime/flows/ .brain/observer-sql-state.json .brain/observer-sql-outbox/',
326
+ runtimeIgnore: ' [!] ignore runtimes locais do wendkeep no Git quando o vault for versionado: .brain/.change-* .brain/runtime/flows/ .brain/observer-sql-state.json .brain/observer-sql-outbox/ .brain/observer-sql-publisher.lock',
327
327
  merged: 'mesclado', created: 'criado', bakSaved: ', .bak salvo',
328
328
  nextSteps: '\nPróximos passos:',
329
329
  step1: (v) => ` 1. Abra o vault no Obsidian: "Abrir pasta como cofre" -> ${v}`,
@@ -352,7 +352,7 @@ const MESSAGES = {
352
352
  mcpSkipped: ' [4/5] .mcp.json skipped (--no-mcp, no MCP companions)',
353
353
  colorsSkipped: ' [5/5] colors skipped (--no-colors)',
354
354
  colors: (r) => ` [5/5] colors: ${r}`,
355
- runtimeIgnore: ' [!] keep local wendkeep runtimes out of Git when the vault is versioned: .brain/.change-* .brain/runtime/flows/ .brain/observer-sql-state.json .brain/observer-sql-outbox/',
355
+ runtimeIgnore: ' [!] keep local wendkeep runtimes out of Git when the vault is versioned: .brain/.change-* .brain/runtime/flows/ .brain/observer-sql-state.json .brain/observer-sql-outbox/ .brain/observer-sql-publisher.lock',
356
356
  merged: 'merged', created: 'created', bakSaved: ', .bak saved',
357
357
  nextSteps: '\nNext steps:',
358
358
  step1: (v) => ` 1. Open the vault in Obsidian: "Open folder as vault" -> ${v}`,
package/src/memory.mjs CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  } from './validate-memory.mjs';
21
21
  import { validateCore } from './validate-core.mjs';
22
22
  import { checkMemoryBundle } from '../hooks/vault-health.mjs';
23
+ import { scopeForMemoryKey } from '../hooks/memory-scope.mjs';
23
24
 
24
25
  const BRAIN = '.brain';
25
26
  const LEDGER = 'MEMORY_EVENTS.jsonl';
@@ -229,6 +230,92 @@ export function migrateMemory(vault, {
229
230
  }
230
231
  }
231
232
 
233
+ function rescopeEventId(sourceEventId, scope) {
234
+ return `mem-rescope-${hash(`${sourceEventId}\0${scope.type}\0${scope.id}`).slice(0, 24)}`;
235
+ }
236
+
237
+ /** Plan an append-only migration without choosing a winner for ambiguous keys. */
238
+ export function planScopedMemoryMigration(vault) {
239
+ const ledger = readMemoryLedger(vault);
240
+ if (ledger.status !== 'ok') throw new Error('Ledger corrompido; execute memory repair antes do rescope.');
241
+ const projection = deriveMemoryProjection(vault, ledger.events);
242
+ const ambiguous = new Set(projection.candidates.map((candidate) => candidate.record_key || candidate.memory_key));
243
+ const existingIds = new Set(ledger.events.map((event) => event.event_id));
244
+ const planned = [];
245
+
246
+ for (const [recordKey, record] of Object.entries(projection.records)) {
247
+ if (ambiguous.has(recordKey) || record.source?.scope) continue;
248
+ const source = record.source;
249
+ const sameLegacyKey = ledger.events.filter((event) => (
250
+ !event.scope && event.memory_key === source.memory_key && !event.rescopes_event_id
251
+ ));
252
+ const scope = scopeForMemoryKey(source.memory_key, {
253
+ ...source,
254
+ projectId: source.project_id,
255
+ workSessionId: source.work_session_id,
256
+ branch: source.value?.branch || source.value?.branch_name,
257
+ worktreeId: source.value?.worktree_id,
258
+ repositoryId: source.value?.repository_id,
259
+ });
260
+ const eventId = rescopeEventId(source.event_id, scope);
261
+ if (existingIds.has(eventId)) continue;
262
+ planned.push({
263
+ v: 1,
264
+ event_id: eventId,
265
+ project_id: source.project_id,
266
+ memory_key: source.memory_key,
267
+ scope,
268
+ operation: 'assert',
269
+ value: record.value,
270
+ authority: source.authority,
271
+ canonical_session_id: source.canonical_session_id || 'memory-rescope',
272
+ activation_id: source.activation_id || 'memory-rescope',
273
+ activation_epoch: Number.isInteger(source.activation_epoch) ? source.activation_epoch : 0,
274
+ turn_sequence: Number.isInteger(source.turn_sequence) ? source.turn_sequence : 0,
275
+ source_turn_id: source.source_turn_id || 'memory-rescope',
276
+ observed_at: source.observed_at || new Date(0).toISOString(),
277
+ evidence: [...new Set([...(source.evidence || []), `memory-rescope:${source.event_id}`])],
278
+ rescopes_event_id: source.event_id,
279
+ rescopes_event_ids: sameLegacyKey.map((event) => event.event_id).sort(),
280
+ });
281
+ }
282
+
283
+ return {
284
+ status: 'dry-run',
285
+ ledger_events: ledger.events.length,
286
+ planned: planned.length,
287
+ ambiguous: projection.candidates.length,
288
+ events: planned,
289
+ };
290
+ }
291
+
292
+ export function rescopeMemoryEvents(vault, { apply = false } = {}) {
293
+ projectId(vault);
294
+ const plan = planScopedMemoryMigration(vault);
295
+ if (!apply) {
296
+ return {
297
+ status: 'dry-run',
298
+ ledger_events: plan.ledger_events,
299
+ planned: plan.planned,
300
+ ambiguous: plan.ambiguous,
301
+ scopes: plan.events.map((event) => ({
302
+ event_id: event.event_id, memory_key: event.memory_key, scope: event.scope,
303
+ })),
304
+ };
305
+ }
306
+ if (!plan.events.length) {
307
+ return { status: 'unchanged', migrated: 0, ambiguous: plan.ambiguous };
308
+ }
309
+ for (const event of plan.events) enqueueMemoryEvent(vault, event);
310
+ const projection = projectMemoryOutbox(vault);
311
+ return {
312
+ status: projection.status === 'projected' ? 'migrated' : projection.status,
313
+ migrated: plan.events.length,
314
+ ambiguous: plan.ambiguous,
315
+ checkpoint: projection.checkpoint || null,
316
+ };
317
+ }
318
+
232
319
  function readCandidates(vault) {
233
320
  const path = brainPath(vault, CANDIDATES);
234
321
  const checked = checkedVaultFile(vault, path, 'MEMORY_CANDIDATES.jsonl');
@@ -266,6 +353,7 @@ function sanitizedCandidate(candidate, index) {
266
353
  reason: candidate.reason,
267
354
  status: candidate.status || 'active',
268
355
  memory_key: candidate.memory_key,
356
+ ...(candidate.scope ? { scope: candidate.scope } : {}),
269
357
  event_ids: [...eventIds].sort(lexicalCompare),
270
358
  };
271
359
  }
@@ -328,6 +416,7 @@ export function listMemoryCandidatesForCuration(vault) {
328
416
  reason: safe.reason,
329
417
  status: safe.status,
330
418
  memory_key: safe.memory_key,
419
+ ...(safe.scope ? { scope: safe.scope } : {}),
331
420
  events: safe.event_ids.map((eventId) => sanitizedCurationEvent(candidate, eventId, index)),
332
421
  };
333
422
  })
@@ -388,7 +477,7 @@ function promotedSupersedes(vault, candidate, selected) {
388
477
  const ledger = readMemoryLedger(vault);
389
478
  if (ledger.status !== 'ok') throw new Error('Ledger de memória inválido durante a promoção.');
390
479
  const projection = deriveMemoryProjection(vault, ledger.events);
391
- const current = projection.records?.[candidate.memory_key]?.source;
480
+ const current = projection.records?.[candidate.record_key || candidate.memory_key]?.source;
392
481
  if (!current?.event_id) return [...memberIds].sort();
393
482
  if (memberIds.has(current.event_id)) {
394
483
  projection.superseded
@@ -572,6 +661,9 @@ export function decideMemoryCandidate(vault, {
572
661
  event_id: `cli-${action}-${hash(`${candidateId}\0${selected?.event_id || ''}`).slice(0, 20)}`,
573
662
  project_id: projectId(vault),
574
663
  memory_key: action === 'promote' ? candidate.memory_key : `candidate.decision.${candidateId}`,
664
+ scope: action === 'promote'
665
+ ? (candidate.scope || selected?.scope || { type: 'project', id: projectId(vault) })
666
+ : { type: 'project', id: projectId(vault) },
575
667
  operation: action === 'promote' && selected ? 'replace' : 'assert',
576
668
  value: action === 'promote' ? promotedMemoryValue(selectedValue) : 'rejected',
577
669
  authority: 'verified',
@@ -2087,6 +2179,7 @@ export function runMemory(argv) {
2087
2179
  result = listMemoryCandidates(vault, { activeOnly: candidatesArgs.activeOnly });
2088
2180
  }
2089
2181
  else if (sub === 'migrate') result = migrateMemory(vault, { apply: argv.includes('--apply') });
2182
+ else if (sub === 'rescope') result = rescopeMemoryEvents(vault, { apply: argv.includes('--apply') });
2090
2183
  else if (sub === 'repair') result = repairMemory(vault);
2091
2184
  else if (sub === 'reconcile') {
2092
2185
  result = reconcileMemory(vault, {
@@ -2109,7 +2202,7 @@ export function runMemory(argv) {
2109
2202
  action: sub, candidateId: positional, ...(eventId ? { eventId } : {}),
2110
2203
  });
2111
2204
  }
2112
- else { process.stderr.write('wendkeep memory: use status | candidates [--active] | migrate [--apply] | repair | recover-attempt <session> [--apply] | reconcile <session> --by-session <session> --reason <text> [--apply] | promote <candidate> [--event <event-id>] | reject <candidate>.\n'); process.exitCode = 2; return; }
2205
+ else { process.stderr.write('wendkeep memory: use status | candidates [--active] | curate | migrate [--apply] | rescope [--apply] | repair | recover-attempt <session> [--apply] | reconcile <session> --by-session <session> --reason <text> [--apply] | promote <candidate> [--event <event-id>] | reject <candidate>.\n'); process.exitCode = 2; return; }
2113
2206
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
2114
2207
  if (sub === 'status' && argv.includes('--gate')) process.exitCode = result.status === 'blocked' ? 1 : 0;
2115
2208
  else if (sub === 'reconcile' && reconcileArgs.apply) process.exitCode = result.health?.status === 'blocked' ? 1 : 0;
package/src/note.mjs CHANGED
@@ -17,6 +17,8 @@ import { getLocale } from '../hooks/locale.mjs';
17
17
  import { buildManualBugNote, buildManualLearningNote, relinkDerivedNotes } from '../hooks/linked-notes.mjs';
18
18
  import { repairStackedFrontmatter } from '../hooks/frontmatter-repair.mjs';
19
19
  import { repairDerivedSections } from '../hooks/derived-sections.mjs';
20
+ import { enqueueObserverDocumentChange } from './observer-sql-publish.mjs';
21
+ import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
20
22
 
21
23
  const TYPES = {
22
24
  bug: { folderKey: 'bugs', prefix: 'BUG', build: buildManualBugNote },
@@ -120,6 +122,11 @@ export function runNote(argv) {
120
122
  } catch { /* sem sessão ativa — nota nasce sem backlink */ }
121
123
 
122
124
  writeFileSync(filePath, kind.build(title.trim(), { num, dateStr, sessionRel, localeId: loc.id }), 'utf8');
123
- process.stdout.write(`${toVaultRelative(vaultBase, filePath)}\n`);
125
+ const logicalPath = toVaultRelative(vaultBase, filePath);
126
+ try {
127
+ const project = readProjectForValidation(vaultBase);
128
+ if (project.ok && project.projectId) enqueueObserverDocumentChange({ vaultBase, projectId: project.projectId, logicalPath });
129
+ } catch { /* Observer é fail-open; reconcile recupera qualquer enqueue perdido. */ }
130
+ process.stdout.write(`${logicalPath}\n`);
124
131
  process.exit(0);
125
132
  }
@@ -1,8 +1,8 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { buildProjectSnapshot } from './observer-snapshot.mjs';
4
- import { publishObserverSql } from './observer-sql-publish.mjs';
3
+ import { publishObserverSqlIncremental } from './observer-sql-publish.mjs';
5
4
  import { observerAuthHeaders, resolveObserverToken } from './observer-auth.mjs';
5
+ import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
6
6
 
7
7
  const OUTBOX_REL = join('.brain', 'observer-outbox');
8
8
  const REQUEST_TIMEOUT_MS = 500;
@@ -88,51 +88,26 @@ export async function retryObserverOutbox({ vaultBase, url, token = process.env.
88
88
 
89
89
  export async function publishObserverSnapshot({
90
90
  vaultBase,
91
- projectRoot,
92
91
  url = process.env.WENDKEEP_OBSERVER_URL || '',
93
92
  now = new Date(),
94
93
  input = {},
95
94
  token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
96
95
  } = {}) {
97
96
  try {
98
- const event = buildProjectSnapshot({ vaultBase, projectRoot, now });
99
- const sql = await publishObserverSql({
97
+ const project = readProjectForValidation(vaultBase);
98
+ if (!project.ok || !project.projectId) throw new Error(project.errors?.join(' ') || 'PROJECT.json inválido.');
99
+ const sql = await publishObserverSqlIncremental({
100
100
  vaultBase,
101
- projectId: event.project_id,
101
+ projectId: project.projectId,
102
102
  url,
103
103
  input,
104
104
  now,
105
105
  token,
106
106
  });
107
- if (!url) return { ok: sql.ok, skipped: true, queued: sql.queued, hookExitCode: 0, event_id: event.event_id, sql };
108
-
109
- await retryObserverOutbox({ vaultBase, url, token });
110
- // SQL is the live authority. Keep the legacy-shaped `memory` field for
111
- // older integrations while reporting the real SQL publication separately.
107
+ // SQLite is the only live authority. The legacy snapshot store remains
108
+ // readable solely as a migration source for pre-SQL installations.
112
109
  const memory = { ok: sql.ok, queued: sql.queued, changed: sql.changed, pending: sql.pending, authority: 'sqlite' };
113
- try {
114
- const response = await postSnapshot(url, event, resolveObserverToken(token));
115
- return {
116
- ok: true,
117
- queued: false,
118
- hookExitCode: 0,
119
- event_id: event.event_id,
120
- duplicate: response.duplicate === true,
121
- memory,
122
- sql,
123
- };
124
- } catch (error) {
125
- queueOutbox(vaultBase, event);
126
- return {
127
- ok: false,
128
- queued: true,
129
- hookExitCode: 0,
130
- event_id: event.event_id,
131
- error: error.message,
132
- memory,
133
- sql,
134
- };
135
- }
110
+ return { ok: sql.ok, queued: sql.queued, hookExitCode: 0, memory, sql };
136
111
  } catch (error) {
137
112
  return { ok: false, queued: false, hookExitCode: 0, error: error.message };
138
113
  }
@@ -4,17 +4,11 @@ import { readFileSync } from 'node:fs';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { gunzipSync } from 'node:zlib';
6
6
  import {
7
- appendObserverEvent,
8
- getObserverProject,
9
7
  listRegisteredObserverProjects,
10
- readObserverIndex,
11
- registerObserverProject,
8
+ readObserverIndexSource,
12
9
  } from './observer-store.mjs';
13
10
  import { MAX_SNAPSHOT_BYTES, validateObserverSnapshot } from './observer-snapshot.mjs';
14
- import {
15
- setMemoryMode,
16
- validateMemoryEvent,
17
- } from './observer-memory.mjs';
11
+ import { validateMemoryEvent } from './observer-memory.mjs';
18
12
  import {
19
13
  OBSERVER_SQL_FILE,
20
14
  OBSERVER_SQL_SCHEMA_VERSION,
@@ -22,6 +16,8 @@ import {
22
16
  ingestObserverEvents,
23
17
  migrateObserverDatabase,
24
18
  readSqlProject,
19
+ readSqlProjectOverview,
20
+ readSqlProjectSnapshot,
25
21
  listSqlProjects,
26
22
  readSqlDocument,
27
23
  readSqlSync,
@@ -33,6 +29,7 @@ import {
33
29
  readUsageCalls,
34
30
  readUsageSummary,
35
31
  registerSqlProject,
32
+ upsertSqlProjectSnapshot,
36
33
  } from './observer-sql-store.mjs';
37
34
  import { migrateObserverContainerData } from './observer-sql-migrate.mjs';
38
35
 
@@ -182,8 +179,8 @@ function ensureSqlProjectRegistration(dataDir, sqlDb, projectId) {
182
179
  } catch (error) {
183
180
  if (error?.code !== 'project_not_registered') throw error;
184
181
  }
185
- const legacy = getObserverProject(dataDir, projectId)
186
- || listRegisteredObserverProjects(dataDir).find((item) => item.projectId === projectId);
182
+ const legacy = listRegisteredObserverProjects(dataDir).find((item) => item.projectId === projectId)
183
+ || readObserverIndexSource(dataDir).projects.find((item) => item.projectId === projectId);
187
184
  if (!legacy) return false;
188
185
  registerSqlProject(sqlDb, {
189
186
  projectId: legacy.projectId,
@@ -234,13 +231,16 @@ export async function startObserverServer({
234
231
  const legacyMigration = migrateObserverContainerData(dataDir, { database: sqlDb });
235
232
  const registered = [
236
233
  ...listRegisteredObserverProjects(dataDir),
237
- ...readObserverIndex(dataDir).projects.map((item) => ({
234
+ ...readObserverIndexSource(dataDir).projects.map((item) => ({
238
235
  projectId: item.projectId,
239
236
  projectName: item.projectName,
240
237
  wendkeepVersion: item.snapshot?.wendkeep_version || '',
241
238
  })),
242
239
  ];
243
240
  for (const project of registered) registerSqlProject(sqlDb, project);
241
+ for (const project of readObserverIndexSource(dataDir).projects) {
242
+ if (project.snapshot) upsertSqlProjectSnapshot(sqlDb, project.snapshot);
243
+ }
244
244
  const server = createServer(async (req, res) => {
245
245
  try {
246
246
  const pathname = new URL(req.url || '/', 'http://127.0.0.1').pathname;
@@ -286,21 +286,11 @@ export async function startObserverServer({
286
286
  }
287
287
 
288
288
  if (parts.length === 2 && req.method === 'GET') {
289
- const index = readObserverIndex(dataDir);
290
- const legacy = new Map(index.projects.map(({ snapshot, ...summary }) => [summary.projectId, summary]));
291
- for (const project of listSqlProjects(sqlDb)) {
292
- const current = legacy.get(project.project_id) || {};
293
- legacy.set(project.project_id, {
294
- ...current,
295
- projectId: project.project_id,
296
- projectName: current.projectName || project.project_name,
297
- wendkeepVersion: current.wendkeepVersion || project.wendkeep_version,
298
- registeredAt: current.registeredAt || project.registered_at,
299
- });
300
- }
301
289
  json(res, 200, {
302
- schema_version: index.schema_version,
303
- projects: [...legacy.values()].sort((a, b) => a.projectId.localeCompare(b.projectId)),
290
+ schema_version: 1,
291
+ projects: listSqlProjects(sqlDb)
292
+ .map((project) => readSqlProjectOverview(sqlDb, project.project_id))
293
+ .sort((a, b) => a.projectId.localeCompare(b.projectId)),
304
294
  });
305
295
  return;
306
296
  }
@@ -386,11 +376,11 @@ export async function startObserverServer({
386
376
  return;
387
377
  }
388
378
  const body = parseJson(await readBody(req));
389
- try {
390
- json(res, 200, setMemoryMode(dataDir, projectId, body.mode));
391
- } catch (error) {
392
- errorResponse(res, 400, error?.code || 'invalid_memory_mode', error?.message || 'modo inválido.');
379
+ if (body.mode && body.mode !== 'container-authority') {
380
+ errorResponse(res, 409, 'sql_authority_fixed', 'O Observer SQL é a autoridade única; modos legados são somente fonte de migração.');
381
+ return;
393
382
  }
383
+ json(res, 200, { ...readSqlSync(sqlDb, projectId), mode: 'container-authority', compatibility_noop: true });
394
384
  return;
395
385
  }
396
386
 
@@ -449,19 +439,8 @@ export async function startObserverServer({
449
439
  }
450
440
 
451
441
  if (parts.length === 3 && req.method === 'GET') {
452
- let project = getObserverProject(dataDir, projectId);
453
- if (!project) {
454
- try {
455
- const sqlProject = readSqlProject(sqlDb, projectId);
456
- project = {
457
- projectId: sqlProject.project_id,
458
- projectName: sqlProject.project_name,
459
- wendkeepVersion: sqlProject.wendkeep_version,
460
- registeredAt: sqlProject.registered_at,
461
- eventCount: 0,
462
- };
463
- } catch { /* handled as not found below */ }
464
- }
442
+ let project;
443
+ try { project = readSqlProjectOverview(sqlDb, projectId); } catch { /* handled below */ }
465
444
  if (!project) {
466
445
  errorResponse(res, 404, 'project_not_found', `projeto não encontrado: ${projectId}`);
467
446
  return;
@@ -476,7 +455,7 @@ export async function startObserverServer({
476
455
  errorResponse(res, 400, 'project_mismatch', 'project_id do corpo não corresponde à rota.');
477
456
  return;
478
457
  }
479
- const result = registerObserverProject(dataDir, {
458
+ const result = registerSqlProject(sqlDb, {
480
459
  projectId,
481
460
  projectName: body.project_name,
482
461
  wendkeepVersion: body.wendkeep_version,
@@ -485,30 +464,23 @@ export async function startObserverServer({
485
464
  errorResponse(res, 400, 'invalid_project', result.errors.join(' '));
486
465
  return;
487
466
  }
488
- registerSqlProject(sqlDb, {
489
- projectId,
490
- projectName: body.project_name,
491
- wendkeepVersion: body.wendkeep_version,
467
+ json(res, 201, {
468
+ projectId: result.project.project_id,
469
+ projectName: result.project.project_name,
470
+ wendkeepVersion: result.project.wendkeep_version,
471
+ registeredAt: result.project.registered_at,
492
472
  });
493
- json(res, 201, result.project);
494
473
  return;
495
474
  }
496
475
 
497
476
  if (parts.length === 4 && parts[3] === 'changes' && req.method === 'GET') {
498
- const project = getObserverProject(dataDir, projectId);
499
- if (!project) {
500
- let sqlProject;
501
- try { sqlProject = readSqlProject(sqlDb, projectId); } catch (error) {
502
- if (error?.code !== 'project_not_registered') throw error;
503
- }
504
- if (!sqlProject) {
505
- errorResponse(res, 404, 'project_not_found', `projeto não encontrado: ${projectId}`);
506
- return;
507
- }
508
- json(res, 200, { project_id: projectId, changes: [] });
477
+ let snapshot;
478
+ try { snapshot = readSqlProjectSnapshot(sqlDb, projectId); } catch (error) {
479
+ if (error?.code !== 'project_not_registered') throw error;
480
+ errorResponse(res, 404, 'project_not_found', `projeto não encontrado: ${projectId}`);
509
481
  return;
510
482
  }
511
- json(res, 200, { project_id: projectId, changes: project.snapshot?.changes || [] });
483
+ json(res, 200, { project_id: projectId, changes: snapshot?.changes || [] });
512
484
  return;
513
485
  }
514
486
 
@@ -519,14 +491,17 @@ export async function startObserverServer({
519
491
  errorResponse(res, 400, 'invalid_snapshot', validation.errors.join(' '));
520
492
  return;
521
493
  }
522
- const result = appendObserverEvent(dataDir, body);
494
+ const result = upsertSqlProjectSnapshot(sqlDb, body);
523
495
  if (!result.accepted && result.duplicate) {
524
496
  json(res, 200, { accepted: false, duplicate: true, event_id: body.event_id });
525
497
  return;
526
498
  }
527
499
  if (!result.accepted) {
528
- const unregistered = result.errors.some((item) => /não registrado/.test(item));
529
- errorResponse(res, unregistered ? 409 : 400, unregistered ? 'project_not_registered' : 'invalid_event', result.errors.join(' '));
500
+ if (result.stale) {
501
+ json(res, 200, { accepted: false, stale: true, event_id: body.event_id });
502
+ return;
503
+ }
504
+ errorResponse(res, 400, 'invalid_event', 'snapshot não aceito.');
530
505
  return;
531
506
  }
532
507
  json(res, 201, { accepted: true, duplicate: false, event_id: body.event_id });
@@ -41,7 +41,7 @@ function walk(root, relativeRoot, out) {
41
41
  for (const entry of readdirSync(root, { withFileTypes: true })) {
42
42
  const logicalPath = relativeRoot ? `${relativeRoot}/${entry.name}` : entry.name;
43
43
  if (entry.name.endsWith('.tmp') || entry.name.endsWith('.lock')
44
- || ['observer-memory-outbox', 'observer-outbox', 'observer-sql-outbox', 'observer-sql-state.json', 'observer-memory-state.json'].includes(entry.name)) continue;
44
+ || ['observer-memory-outbox', 'observer-outbox', 'observer-sql-outbox', 'observer-sql-state.json', 'observer-sql-publisher.lock', 'observer-memory-state.json'].includes(entry.name)) continue;
45
45
  const absolute = join(root, entry.name);
46
46
  if (entry.isDirectory()) walk(absolute, logicalPath, out);
47
47
  else if (entry.isFile()) out.push({ absolute, logicalPath });