wendkeep 0.69.0 → 0.71.1
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 +43 -0
- package/README.en.md +5 -3
- package/README.md +5 -3
- package/docs/en/commands/changes-and-verification.md +4 -0
- package/docs/en/commands/costs-and-observability.md +11 -2
- package/docs/en/commands/observer.md +130 -0
- package/docs/pt-BR/commands/changes-and-verification.md +4 -0
- package/docs/pt-BR/commands/costs-and-observability.md +12 -2
- package/docs/pt-BR/commands/observer.md +131 -0
- package/hooks/harness-doctor.mjs +21 -7
- package/hooks/observer-publish.mjs +21 -0
- package/hooks/pricing.json +10 -1
- package/hooks/token-usage.mjs +13 -0
- package/package.json +4 -3
- package/packages/cli/src/index.mjs +8 -1
- package/packages/integrations/src/host-hooks.mjs +4 -0
- package/src/observer-memory-publish.mjs +334 -0
- package/src/observer-memory.mjs +308 -0
- package/src/observer-publish.mjs +134 -0
- package/src/observer-server.mjs +333 -0
- package/src/observer-snapshot.mjs +153 -0
- package/src/observer-store.mjs +155 -0
- package/src/observer.mjs +146 -0
- package/src/taxonomy.mjs +2 -0
- package/web/observer/app.mjs +611 -0
- package/web/observer/favicon.svg +5 -0
- package/web/observer/index.html +125 -0
- package/web/observer/styles.css +229 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
appendFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { dirname, join, relative, sep } from 'node:path';
|
|
12
|
+
|
|
13
|
+
export const MEMORY_SCHEMA_VERSION = 1;
|
|
14
|
+
export const MEMORY_EVENTS_FILE = 'MEMORY_EVENTS.jsonl';
|
|
15
|
+
export const MEMORY_INDEX_FILE = 'MEMORY_INDEX.json';
|
|
16
|
+
export const MEMORY_ROOT = 'memory';
|
|
17
|
+
export const MAX_MEMORY_CONTENT_BYTES = 2 * 1024 * 1024;
|
|
18
|
+
export const MEMORY_MODES = new Set(['mirror', 'container-read', 'container-authority']);
|
|
19
|
+
|
|
20
|
+
const ENTITY_TYPES = new Set(['session', 'decision', 'bug', 'learning', 'spec', 'change', 'memory']);
|
|
21
|
+
const OPERATIONS = new Set(['upsert', 'delete']);
|
|
22
|
+
const PROJECT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,120}$/;
|
|
23
|
+
const ROOTS = [
|
|
24
|
+
'02-Sessões/',
|
|
25
|
+
'04-Decisões/',
|
|
26
|
+
'05-Bugs/',
|
|
27
|
+
'06-Aprendizados/',
|
|
28
|
+
'07-Specs/',
|
|
29
|
+
'08-Mudanças/',
|
|
30
|
+
'.brain/',
|
|
31
|
+
];
|
|
32
|
+
const ROOT_FILES = new Set(['CORE.md', 'DIGEST.md', 'SHARED_MEMORY.md']);
|
|
33
|
+
|
|
34
|
+
function hashContent(content) {
|
|
35
|
+
return createHash('sha256').update(content).digest('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function atomicJson(path, value) {
|
|
39
|
+
const temp = path + '.' + process.pid + '.' + Date.now() + '.tmp';
|
|
40
|
+
writeFileSync(temp, JSON.stringify(value, null, 2) + '\n', 'utf8');
|
|
41
|
+
renameSync(temp, path);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readJson(path, fallback) {
|
|
45
|
+
if (!existsSync(path)) return fallback;
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
48
|
+
} catch {
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ensureDataDir(dataDir) {
|
|
54
|
+
if (!dataDir) throw new Error('dataDir é obrigatório.');
|
|
55
|
+
mkdirSync(dataDir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function projectIdValid(projectId) {
|
|
59
|
+
return typeof projectId === 'string' && PROJECT_ID_RE.test(projectId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizedLogicalPath(value) {
|
|
63
|
+
const original = String(value ?? '');
|
|
64
|
+
const normalized = original.replaceAll('\\', '/').replace(/^\/+/, '');
|
|
65
|
+
if (!normalized || /^[A-Za-z]:\//.test(normalized) || original.startsWith('/') || original.startsWith('\\')) {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
const parts = normalized.split('/');
|
|
69
|
+
if (parts.some((part) => !part || part === '.' || part === '..')) return '';
|
|
70
|
+
if (!(ROOT_FILES.has(normalized) || ROOTS.some((root) => normalized.startsWith(root)))) return '';
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function memoryFilePath(dataDir, projectId, logicalPath) {
|
|
75
|
+
const base = join(dataDir, MEMORY_ROOT, projectId);
|
|
76
|
+
const target = join(base, ...logicalPath.split('/'));
|
|
77
|
+
const rel = relative(base, target);
|
|
78
|
+
if (rel.startsWith('..' + sep) || rel === '..' || /^[A-Za-z]:/.test(rel)) {
|
|
79
|
+
throw new Error('logical_path fora do projeto.');
|
|
80
|
+
}
|
|
81
|
+
return target;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function defaultIndex() {
|
|
85
|
+
return {
|
|
86
|
+
schema_version: MEMORY_SCHEMA_VERSION,
|
|
87
|
+
generated_at: new Date().toISOString(),
|
|
88
|
+
projects: {},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function loadIndex(dataDir) {
|
|
93
|
+
ensureDataDir(dataDir);
|
|
94
|
+
const index = readJson(join(dataDir, MEMORY_INDEX_FILE), null);
|
|
95
|
+
if (index?.schema_version === MEMORY_SCHEMA_VERSION && index.projects && typeof index.projects === 'object') {
|
|
96
|
+
return index;
|
|
97
|
+
}
|
|
98
|
+
return defaultIndex();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function saveIndex(dataDir, index) {
|
|
102
|
+
index.generated_at = new Date().toISOString();
|
|
103
|
+
atomicJson(join(dataDir, MEMORY_INDEX_FILE), index);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readEventLines(dataDir) {
|
|
107
|
+
const path = join(dataDir, MEMORY_EVENTS_FILE);
|
|
108
|
+
if (!existsSync(path)) return [];
|
|
109
|
+
return readFileSync(path, 'utf8')
|
|
110
|
+
.replace(/\r\n/g, '\n')
|
|
111
|
+
.split('\n')
|
|
112
|
+
.filter((line) => line.trim())
|
|
113
|
+
.flatMap((line) => {
|
|
114
|
+
try { return [JSON.parse(line)]; } catch { return []; }
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function projectState(index, projectId) {
|
|
119
|
+
if (!index.projects[projectId]) {
|
|
120
|
+
index.projects[projectId] = {
|
|
121
|
+
project_id: projectId,
|
|
122
|
+
mode: 'mirror',
|
|
123
|
+
documents: {},
|
|
124
|
+
event_count: 0,
|
|
125
|
+
conflict_count: 0,
|
|
126
|
+
last_event_at: '',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return index.projects[projectId];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function eventPayload(event) {
|
|
133
|
+
const clone = { ...event };
|
|
134
|
+
delete clone.event_id;
|
|
135
|
+
return JSON.stringify(clone);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function validateMemoryEvent(event) {
|
|
139
|
+
const errors = [];
|
|
140
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)) {
|
|
141
|
+
return { ok: false, errors: ['evento deve ser um objeto JSON.'] };
|
|
142
|
+
}
|
|
143
|
+
if (event.schema_version !== MEMORY_SCHEMA_VERSION) errors.push('schema_version incompatível.');
|
|
144
|
+
if (typeof event.event_id !== 'string' || !event.event_id.trim()) errors.push('event_id ausente.');
|
|
145
|
+
if (!projectIdValid(event.project_id)) errors.push('project_id inválido.');
|
|
146
|
+
if (!ENTITY_TYPES.has(event.entity_type)) errors.push('entity_type inválido.');
|
|
147
|
+
const path = normalizedLogicalPath(event.logical_path);
|
|
148
|
+
if (!path) errors.push('logical_path inválido ou fora das raízes autorizadas.');
|
|
149
|
+
if (!OPERATIONS.has(event.operation)) errors.push('operation inválida.');
|
|
150
|
+
if (!Number.isInteger(event.revision) || event.revision < 1) errors.push('revision inválida.');
|
|
151
|
+
if (typeof event.content_hash !== 'string' || !/^[a-f0-9]{64}$/.test(event.content_hash)) {
|
|
152
|
+
errors.push('content_hash inválido.');
|
|
153
|
+
}
|
|
154
|
+
if (typeof event.captured_at !== 'string' || Number.isNaN(Date.parse(event.captured_at))) {
|
|
155
|
+
errors.push('captured_at inválido.');
|
|
156
|
+
}
|
|
157
|
+
if (event.operation === 'upsert') {
|
|
158
|
+
if (typeof event.content !== 'string') errors.push('content ausente.');
|
|
159
|
+
else {
|
|
160
|
+
if (Buffer.byteLength(event.content, 'utf8') > MAX_MEMORY_CONTENT_BYTES) errors.push('content excede o limite.');
|
|
161
|
+
if (event.content_hash !== hashContent(event.content)) errors.push('content_hash não corresponde ao conteúdo.');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { ok: errors.length === 0, errors, logical_path: path };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function applyMemoryEvent(dataDir, event) {
|
|
168
|
+
ensureDataDir(dataDir);
|
|
169
|
+
const validation = validateMemoryEvent(event);
|
|
170
|
+
if (!validation.ok) return { accepted: false, errors: validation.errors };
|
|
171
|
+
const index = loadIndex(dataDir);
|
|
172
|
+
const state = projectState(index, event.project_id);
|
|
173
|
+
const existingEvent = readEventLines(dataDir).find((item) => item.event_id === event.event_id);
|
|
174
|
+
if (existingEvent) {
|
|
175
|
+
if (eventPayload(existingEvent) === eventPayload(event)) {
|
|
176
|
+
return { accepted: false, duplicate: true, event_id: event.event_id };
|
|
177
|
+
}
|
|
178
|
+
state.conflict_count += 1;
|
|
179
|
+
saveIndex(dataDir, index);
|
|
180
|
+
return { accepted: false, conflict: true, errors: ['event_id reutilizado com payload diferente.'] };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const current = state.documents[validation.logical_path];
|
|
184
|
+
if (current && event.revision < current.revision) {
|
|
185
|
+
state.conflict_count += 1;
|
|
186
|
+
saveIndex(dataDir, index);
|
|
187
|
+
return { accepted: false, conflict: true, stale: true, errors: ['revisão antiga não pode substituir a atual.'] };
|
|
188
|
+
}
|
|
189
|
+
if (current && event.revision === current.revision && current.content_hash !== event.content_hash) {
|
|
190
|
+
state.conflict_count += 1;
|
|
191
|
+
saveIndex(dataDir, index);
|
|
192
|
+
return { accepted: false, conflict: true, errors: ['revisão já possui conteúdo diferente.'] };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const path = memoryFilePath(dataDir, event.project_id, validation.logical_path);
|
|
196
|
+
if (event.operation === 'delete') {
|
|
197
|
+
rmSync(path, { force: true });
|
|
198
|
+
delete state.documents[validation.logical_path];
|
|
199
|
+
} else {
|
|
200
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
201
|
+
const temp = path + '.' + process.pid + '.' + Date.now() + '.tmp';
|
|
202
|
+
writeFileSync(temp, event.content, 'utf8');
|
|
203
|
+
renameSync(temp, path);
|
|
204
|
+
state.documents[validation.logical_path] = {
|
|
205
|
+
project_id: event.project_id,
|
|
206
|
+
logical_path: validation.logical_path,
|
|
207
|
+
entity_type: event.entity_type,
|
|
208
|
+
content_hash: event.content_hash,
|
|
209
|
+
revision: event.revision,
|
|
210
|
+
source_session_id: String(event.source_session_id || ''),
|
|
211
|
+
source_turn_id: String(event.source_turn_id || ''),
|
|
212
|
+
captured_at: event.captured_at,
|
|
213
|
+
bytes: Buffer.byteLength(event.content, 'utf8'),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
appendFileSync(join(dataDir, MEMORY_EVENTS_FILE), JSON.stringify(event) + '\n', 'utf8');
|
|
217
|
+
state.event_count += 1;
|
|
218
|
+
state.last_event_at = event.captured_at;
|
|
219
|
+
saveIndex(dataDir, index);
|
|
220
|
+
return { accepted: true, duplicate: false, event_id: event.event_id, document: state.documents[validation.logical_path] || null };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function readMemoryTree(dataDir, projectId, prefix = '') {
|
|
224
|
+
const index = loadIndex(dataDir);
|
|
225
|
+
const state = index.projects[projectId] || projectState(index, projectId);
|
|
226
|
+
const normalizedPrefix = String(prefix || '').replaceAll('\\', '/').replace(/^\/+|\/+$/g, '');
|
|
227
|
+
const documents = Object.values(state.documents)
|
|
228
|
+
.filter((item) => !normalizedPrefix || item.logical_path.startsWith(normalizedPrefix + '/') || item.logical_path === normalizedPrefix)
|
|
229
|
+
.sort((a, b) => a.logical_path.localeCompare(b.logical_path));
|
|
230
|
+
return {
|
|
231
|
+
schema_version: MEMORY_SCHEMA_VERSION,
|
|
232
|
+
project_id: projectId,
|
|
233
|
+
documents,
|
|
234
|
+
document_count: documents.length,
|
|
235
|
+
categories: [...new Set(documents.map((item) => item.logical_path.split('/')[0]))].sort(),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function setMemoryMode(dataDir, projectId, mode) {
|
|
240
|
+
if (!MEMORY_MODES.has(mode)) {
|
|
241
|
+
const error = new Error('modo de memória inválido.');
|
|
242
|
+
error.code = 'invalid_memory_mode';
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
const index = loadIndex(dataDir);
|
|
246
|
+
const state = projectState(index, projectId);
|
|
247
|
+
state.mode = mode;
|
|
248
|
+
saveIndex(dataDir, index);
|
|
249
|
+
return readMemorySync(dataDir, projectId);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function readMemoryDocument(dataDir, projectId, logicalPath) {
|
|
253
|
+
const path = normalizedLogicalPath(logicalPath);
|
|
254
|
+
if (!projectIdValid(projectId) || !path) {
|
|
255
|
+
const error = new Error('documento inválido.');
|
|
256
|
+
error.code = 'invalid_memory_path';
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
const index = loadIndex(dataDir);
|
|
260
|
+
const metadata = index.projects[projectId]?.documents?.[path];
|
|
261
|
+
if (!metadata) {
|
|
262
|
+
const error = new Error('documento não encontrado.');
|
|
263
|
+
error.code = 'memory_not_found';
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
return { ...metadata, content: readFileSync(memoryFilePath(dataDir, projectId, path), 'utf8') };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function exportMemoryBundle(dataDir, projectId) {
|
|
270
|
+
const tree = readMemoryTree(dataDir, projectId);
|
|
271
|
+
return {
|
|
272
|
+
schema_version: MEMORY_SCHEMA_VERSION,
|
|
273
|
+
project_id: projectId,
|
|
274
|
+
mode: readMemorySync(dataDir, projectId).mode,
|
|
275
|
+
documents: tree.documents.map((metadata) => ({
|
|
276
|
+
...metadata,
|
|
277
|
+
content: readMemoryDocument(dataDir, projectId, metadata.logical_path).content,
|
|
278
|
+
})),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function searchMemory(dataDir, projectId, query) {
|
|
283
|
+
const term = String(query || '').trim().toLowerCase();
|
|
284
|
+
if (!term) return [];
|
|
285
|
+
return readMemoryTree(dataDir, projectId).documents.flatMap((metadata) => {
|
|
286
|
+
let content;
|
|
287
|
+
try { content = readFileSync(memoryFilePath(dataDir, projectId, metadata.logical_path), 'utf8'); } catch { return []; }
|
|
288
|
+
const haystack = metadata.logical_path + '\n' + content;
|
|
289
|
+
const at = haystack.toLowerCase().indexOf(term);
|
|
290
|
+
if (at < 0) return [];
|
|
291
|
+
const start = Math.max(0, at - 80);
|
|
292
|
+
return [{ ...metadata, excerpt: haystack.slice(start, start + 240).replace(/\s+/g, ' ').trim() }];
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function readMemorySync(dataDir, projectId) {
|
|
297
|
+
const index = loadIndex(dataDir);
|
|
298
|
+
const state = index.projects[projectId] || projectState(index, projectId);
|
|
299
|
+
return {
|
|
300
|
+
project_id: projectId,
|
|
301
|
+
mode: state.mode || 'mirror',
|
|
302
|
+
document_count: Object.keys(state.documents).length,
|
|
303
|
+
event_count: Number(state.event_count || 0),
|
|
304
|
+
conflict_count: Number(state.conflict_count || 0),
|
|
305
|
+
pending_count: 0,
|
|
306
|
+
last_event_at: state.last_event_at || '',
|
|
307
|
+
};
|
|
308
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { buildProjectSnapshot } from './observer-snapshot.mjs';
|
|
4
|
+
import { publishObserverMemory } from './observer-memory-publish.mjs';
|
|
5
|
+
|
|
6
|
+
const OUTBOX_REL = join('.brain', 'observer-outbox');
|
|
7
|
+
const REQUEST_TIMEOUT_MS = 500;
|
|
8
|
+
|
|
9
|
+
function outboxDir(vaultBase) {
|
|
10
|
+
return join(vaultBase, OUTBOX_REL);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function eventPath(vaultBase, eventId) {
|
|
14
|
+
if (!/^obs-[a-f0-9]{24}$/.test(eventId)) throw new Error('event_id inválido para outbox.');
|
|
15
|
+
return join(outboxDir(vaultBase), `${eventId}.json`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function atomicWrite(path, value) {
|
|
19
|
+
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
20
|
+
writeFileSync(temp, `${JSON.stringify(value)}\n`, 'utf8');
|
|
21
|
+
renameSync(temp, path);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function listOutbox(vaultBase) {
|
|
25
|
+
const dir = outboxDir(vaultBase);
|
|
26
|
+
if (!existsSync(dir)) return [];
|
|
27
|
+
return readdirSync(dir)
|
|
28
|
+
.filter((name) => /^obs-[a-f0-9]{24}\.json$/.test(name))
|
|
29
|
+
.sort()
|
|
30
|
+
.flatMap((name) => {
|
|
31
|
+
try { return [JSON.parse(readFileSync(join(dir, name), 'utf8'))]; }
|
|
32
|
+
catch { return []; }
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function queueOutbox(vaultBase, event) {
|
|
37
|
+
const dir = outboxDir(vaultBase);
|
|
38
|
+
mkdirSync(dir, { recursive: true });
|
|
39
|
+
const path = eventPath(vaultBase, event.event_id);
|
|
40
|
+
if (!existsSync(path)) atomicWrite(path, event);
|
|
41
|
+
return path;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function removeOutbox(vaultBase, eventId) {
|
|
45
|
+
const path = eventPath(vaultBase, eventId);
|
|
46
|
+
if (existsSync(path)) unlinkSync(path);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function postSnapshot(url, event) {
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
52
|
+
try {
|
|
53
|
+
const response = await fetch(`${String(url).replace(/\/$/, '')}/v1/projects/${encodeURIComponent(event.project_id)}/snapshot`, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'content-type': 'application/json',
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify(event),
|
|
59
|
+
signal: controller.signal,
|
|
60
|
+
});
|
|
61
|
+
const text = await response.text();
|
|
62
|
+
let body = {};
|
|
63
|
+
try { body = JSON.parse(text); } catch { /* server error remains deterministic below */ }
|
|
64
|
+
if (!response.ok || !(body.accepted === true || body.duplicate === true)) {
|
|
65
|
+
throw new Error(`Observer respondeu HTTP ${response.status}.`);
|
|
66
|
+
}
|
|
67
|
+
return body;
|
|
68
|
+
} finally {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function retryObserverOutbox({ vaultBase, url } = {}) {
|
|
74
|
+
if (!url) return { attempted: 0, confirmed: 0, pending: listOutbox(vaultBase).length };
|
|
75
|
+
let attempted = 0;
|
|
76
|
+
let confirmed = 0;
|
|
77
|
+
for (const event of listOutbox(vaultBase)) {
|
|
78
|
+
attempted += 1;
|
|
79
|
+
try {
|
|
80
|
+
await postSnapshot(url, event);
|
|
81
|
+
removeOutbox(vaultBase, event.event_id);
|
|
82
|
+
confirmed += 1;
|
|
83
|
+
} catch { /* preserve the event for a later retry */ }
|
|
84
|
+
}
|
|
85
|
+
return { attempted, confirmed, pending: listOutbox(vaultBase).length };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function publishObserverSnapshot({
|
|
89
|
+
vaultBase,
|
|
90
|
+
projectRoot,
|
|
91
|
+
url = process.env.WENDKEEP_OBSERVER_URL || '',
|
|
92
|
+
now = new Date(),
|
|
93
|
+
} = {}) {
|
|
94
|
+
try {
|
|
95
|
+
const event = buildProjectSnapshot({ vaultBase, projectRoot, now });
|
|
96
|
+
if (!url) return { ok: true, skipped: true, queued: false, hookExitCode: 0, event_id: event.event_id };
|
|
97
|
+
|
|
98
|
+
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
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const response = await postSnapshot(url, event);
|
|
112
|
+
return {
|
|
113
|
+
ok: true,
|
|
114
|
+
queued: false,
|
|
115
|
+
hookExitCode: 0,
|
|
116
|
+
event_id: event.event_id,
|
|
117
|
+
duplicate: response.duplicate === true,
|
|
118
|
+
memory,
|
|
119
|
+
};
|
|
120
|
+
} catch (error) {
|
|
121
|
+
queueOutbox(vaultBase, event);
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
queued: true,
|
|
125
|
+
hookExitCode: 0,
|
|
126
|
+
event_id: event.event_id,
|
|
127
|
+
error: error.message,
|
|
128
|
+
memory,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return { ok: false, queued: false, hookExitCode: 0, error: error.message };
|
|
133
|
+
}
|
|
134
|
+
}
|