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.
- package/CHANGELOG.md +66 -0
- package/README.en.md +13 -9
- package/README.md +13 -9
- package/docs/en/commands/memory.md +16 -1
- package/docs/en/commands/observer.md +29 -11
- package/docs/en/commands/operating-profiles.md +1 -1
- package/docs/pt-BR/commands/memory.md +16 -1
- package/docs/pt-BR/commands/observer.md +28 -11
- package/docs/pt-BR/commands/operating-profiles.md +1 -1
- package/hooks/brain-core.mjs +2 -0
- package/hooks/brain-recall.mjs +5 -1
- package/hooks/evidence-context.mjs +41 -0
- package/hooks/evidence-recall.mjs +1 -0
- package/hooks/memory-scope.mjs +1 -0
- package/package.json +2 -2
- package/packages/cli/src/index.mjs +2 -2
- package/packages/integrations/src/host-hooks.mjs +1 -0
- package/packages/vault/src/evidence-recall.mjs +343 -0
- package/packages/vault/src/index.mjs +2 -0
- package/packages/vault/src/memory-handoff.mjs +58 -3
- package/packages/vault/src/memory-schema.mjs +12 -2
- package/packages/vault/src/memory-scope.mjs +119 -0
- package/packages/vault/src/memory-store.mjs +86 -24
- package/schema/observer/004-evidence-recall.sql +25 -0
- package/schema/observer/005-project-scoped-identities.sql +217 -0
- package/src/change.mjs +41 -1
- package/src/doctor.mjs +5 -0
- package/src/init.mjs +2 -2
- package/src/memory.mjs +95 -2
- package/src/note.mjs +8 -1
- package/src/observer-publish.mjs +9 -34
- package/src/observer-server.mjs +38 -63
- package/src/observer-sql-migrate.mjs +1 -1
- package/src/observer-sql-publish.mjs +372 -12
- package/src/observer-sql-store.mjs +248 -32
- package/src/observer-store.mjs +15 -3
- package/src/observer.mjs +104 -14
- 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
|
-
|
|
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
|
}
|
package/src/observer-publish.mjs
CHANGED
|
@@ -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 {
|
|
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
|
|
99
|
-
|
|
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:
|
|
101
|
+
projectId: project.projectId,
|
|
102
102
|
url,
|
|
103
103
|
input,
|
|
104
104
|
now,
|
|
105
105
|
token,
|
|
106
106
|
});
|
|
107
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/observer-server.mjs
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
186
|
-
||
|
|
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
|
-
...
|
|
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:
|
|
303
|
-
projects:
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
|
453
|
-
|
|
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 =
|
|
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
|
-
|
|
489
|
-
projectId,
|
|
490
|
-
projectName:
|
|
491
|
-
wendkeepVersion:
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
529
|
-
|
|
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 });
|