wendkeep 0.74.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 +35 -0
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/docs/en/commands/observer.md +22 -11
- package/docs/pt-BR/commands/observer.md +22 -11
- package/package.json +1 -1
- package/packages/cli/src/index.mjs +1 -1
- 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/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 +108 -28
- package/src/observer-store.mjs +15 -3
- package/src/observer.mjs +104 -14
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
readdirSync,
|
|
6
6
|
readFileSync,
|
|
7
7
|
renameSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
statSync,
|
|
8
10
|
unlinkSync,
|
|
9
11
|
writeFileSync,
|
|
10
12
|
} from 'node:fs';
|
|
@@ -18,6 +20,7 @@ import { sanitizeObserverContent, sanitizeObserverMetadata } from './observer-pr
|
|
|
18
20
|
|
|
19
21
|
export const SQL_OUTBOX_REL = '.brain/observer-sql-outbox';
|
|
20
22
|
export const SQL_STATE_REL = '.brain/observer-sql-state.json';
|
|
23
|
+
export const SQL_PUBLISHER_LEASE_REL = '.brain/observer-sql-publisher.lock';
|
|
21
24
|
const SQL_SCHEMA_VERSION = 1;
|
|
22
25
|
export const SQL_EVENT_BATCH_SIZE = 64;
|
|
23
26
|
export const SQL_EVENT_BATCH_BYTES = 8 * 1024 * 1024;
|
|
@@ -158,7 +161,11 @@ function transcriptCalls({ projectId, sessionId, agentId, role, provider, modelF
|
|
|
158
161
|
const tokens = tokenPayload(turn.usage);
|
|
159
162
|
if (!prompt && !response && !tokens.total) return [];
|
|
160
163
|
const model = text(turn.model || parsed.model || modelFallback, '?');
|
|
161
|
-
const
|
|
164
|
+
const stableTurn = turn.turnId || index + 1;
|
|
165
|
+
const turnRevision = turn.status === 'complete'
|
|
166
|
+
? 'complete'
|
|
167
|
+
: hash({ prompt, response, tokens, status: turn.status }).slice(0, 16);
|
|
168
|
+
const callId = eventId('call', projectId, `${transcriptId}:${agentId}:${stableTurn}:${turnRevision}`);
|
|
162
169
|
return [{
|
|
163
170
|
schema_version: 1,
|
|
164
171
|
event_id: eventId('call-event', projectId, callId),
|
|
@@ -321,6 +328,136 @@ export function buildObserverSqlEventBatch({ vaultBase, projectId, input = {}, n
|
|
|
321
328
|
return { events: dedupeEvents(events), nextState, scanned: Object.keys(nextState.files).length, changed };
|
|
322
329
|
}
|
|
323
330
|
|
|
331
|
+
function hookEventName(input = {}) {
|
|
332
|
+
return text(input.hook_event_name || input.hookEventName || input.event_name || input.event).toLowerCase();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function incrementalSessionContext(vaultBase, input = {}) {
|
|
336
|
+
const registry = readRegistry(vaultBase);
|
|
337
|
+
const requestedId = text(
|
|
338
|
+
input.session_id || input.sessionId || input.thread_id || input.threadId
|
|
339
|
+
|| input.conversation_id || input.conversationId,
|
|
340
|
+
);
|
|
341
|
+
const pair = Object.entries(registry.sessions || {}).find(([sessionId, entry]) => (
|
|
342
|
+
(requestedId && sessionId === requestedId)
|
|
343
|
+
|| (input.session_file && normalizePath(entry?.session_file) === normalizePath(input.session_file))
|
|
344
|
+
));
|
|
345
|
+
if (!pair?.[1]?.session_file) return null;
|
|
346
|
+
const logicalPath = normalizePath(pair[1].session_file);
|
|
347
|
+
if (!logicalPath || logicalPath.startsWith('/') || logicalPath.includes('..')) return null;
|
|
348
|
+
const absolute = join(vaultBase, logicalPath);
|
|
349
|
+
if (!existsSync(absolute)) return null;
|
|
350
|
+
const content = readFileSync(absolute, 'utf8');
|
|
351
|
+
return {
|
|
352
|
+
sessionId: pair[0],
|
|
353
|
+
entry: pair[1],
|
|
354
|
+
file: { logicalPath, absolute },
|
|
355
|
+
content,
|
|
356
|
+
fm: parseFrontmatter(content),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Build only the session/subagent evidence named by the current hook payload. */
|
|
361
|
+
export function buildObserverSqlIncrementalBatch({
|
|
362
|
+
vaultBase,
|
|
363
|
+
projectId,
|
|
364
|
+
input = {},
|
|
365
|
+
now = new Date(),
|
|
366
|
+
state = readState(vaultBase),
|
|
367
|
+
captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata',
|
|
368
|
+
} = {}) {
|
|
369
|
+
if (!vaultBase || !projectId) throw new Error('vaultBase e projectId são obrigatórios.');
|
|
370
|
+
const eventName = hookEventName(input);
|
|
371
|
+
const nextState = {
|
|
372
|
+
schema_version: SQL_SCHEMA_VERSION,
|
|
373
|
+
files: { ...(state.files || {}) },
|
|
374
|
+
transcripts: { ...(state.transcripts || {}) },
|
|
375
|
+
};
|
|
376
|
+
if (eventName.includes('sessionstart')) {
|
|
377
|
+
return { events: [], nextState, scanned: 0, changed: 0, scope: 'drain-only' };
|
|
378
|
+
}
|
|
379
|
+
const context = incrementalSessionContext(vaultBase, input);
|
|
380
|
+
if (!context) return { events: [], nextState, scanned: 0, changed: 0, scope: 'drain-only' };
|
|
381
|
+
|
|
382
|
+
const occurredAt = isoNow(now);
|
|
383
|
+
const resolvedCaptureLevel = normalizeObserverCaptureLevel(captureLevel);
|
|
384
|
+
const events = [];
|
|
385
|
+
let changed = 0;
|
|
386
|
+
const safeContent = sanitizeObserverContent(context.content);
|
|
387
|
+
const contentHash = hash(safeContent);
|
|
388
|
+
const previous = state.files?.[context.file.logicalPath];
|
|
389
|
+
const revision = Math.max(1, Number(previous?.revision || 0) + (previous?.content_hash === contentHash ? 0 : 1));
|
|
390
|
+
nextState.files[context.file.logicalPath] = { content_hash: contentHash, revision };
|
|
391
|
+
if (previous?.content_hash !== contentHash) {
|
|
392
|
+
changed += 1;
|
|
393
|
+
events.push(documentEvent({
|
|
394
|
+
projectId,
|
|
395
|
+
logicalPath: context.file.logicalPath,
|
|
396
|
+
content: context.content,
|
|
397
|
+
metadata: context.fm,
|
|
398
|
+
revision,
|
|
399
|
+
occurredAt,
|
|
400
|
+
}));
|
|
401
|
+
const cost = parseSessionCost(context.content)
|
|
402
|
+
|| { model: '?', mainCost: 0, subCost: 0, tokens: 0, subTokens: 0, ledger: [] };
|
|
403
|
+
const sessionProjection = sessionEvents({
|
|
404
|
+
projectId,
|
|
405
|
+
logicalPath: context.file.logicalPath,
|
|
406
|
+
content: context.content,
|
|
407
|
+
cost,
|
|
408
|
+
revision,
|
|
409
|
+
sessionId: context.sessionId,
|
|
410
|
+
}).events;
|
|
411
|
+
events.push(...sessionProjection.filter((event) => !(
|
|
412
|
+
eventName.includes('subagentstop') && event.kind === 'transcript.upsert'
|
|
413
|
+
)));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const mainAgentId = `${projectId}:${context.sessionId}:main`;
|
|
417
|
+
const provider = text(context.fm.provider);
|
|
418
|
+
const model = text(context.fm.modelo || context.fm.custo_modelo_label, '?');
|
|
419
|
+
let sources = sourceCandidates({
|
|
420
|
+
vaultBase,
|
|
421
|
+
logicalPath: context.file.logicalPath,
|
|
422
|
+
fm: context.fm,
|
|
423
|
+
input,
|
|
424
|
+
});
|
|
425
|
+
if (input.agent_transcript_path || input.agentTranscriptPath || eventName.includes('subagentstop')) {
|
|
426
|
+
sources = sources.filter((source) => source.role === 'subagent').slice(0, 1);
|
|
427
|
+
} else {
|
|
428
|
+
sources = sources.filter((source) => source.role === 'main').slice(0, 1);
|
|
429
|
+
}
|
|
430
|
+
for (const source of sources) {
|
|
431
|
+
const transcriptContent = readFileSync(source.path, 'utf8');
|
|
432
|
+
const fingerprint = hash(transcriptContent);
|
|
433
|
+
const previousTranscript = state.transcripts?.[source.transcriptId];
|
|
434
|
+
if (previousTranscript?.content_hash === fingerprint && previousTranscript?.coverage === resolvedCaptureLevel) continue;
|
|
435
|
+
const complete = completeTranscriptEvents({
|
|
436
|
+
projectId,
|
|
437
|
+
sessionId: context.sessionId,
|
|
438
|
+
mainAgentId,
|
|
439
|
+
provider,
|
|
440
|
+
model,
|
|
441
|
+
source,
|
|
442
|
+
now: occurredAt,
|
|
443
|
+
captureLevel: resolvedCaptureLevel,
|
|
444
|
+
});
|
|
445
|
+
nextState.transcripts[source.transcriptId] = {
|
|
446
|
+
content_hash: complete.fingerprint,
|
|
447
|
+
coverage: resolvedCaptureLevel,
|
|
448
|
+
};
|
|
449
|
+
events.push(...complete.events);
|
|
450
|
+
changed += 1;
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
events: dedupeEvents(events),
|
|
454
|
+
nextState,
|
|
455
|
+
scanned: 1,
|
|
456
|
+
changed,
|
|
457
|
+
scope: `session:${context.sessionId}`,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
324
461
|
function queueOutbox(vaultBase, batch) {
|
|
325
462
|
mkdirSync(outboxDir(vaultBase), { recursive: true });
|
|
326
463
|
const path = outboxPath(vaultBase, batch);
|
|
@@ -328,14 +465,184 @@ function queueOutbox(vaultBase, batch) {
|
|
|
328
465
|
return path;
|
|
329
466
|
}
|
|
330
467
|
|
|
468
|
+
function eventCoalesceKey(event) {
|
|
469
|
+
const payload = event.payload || {};
|
|
470
|
+
return [
|
|
471
|
+
event.project_id,
|
|
472
|
+
event.kind,
|
|
473
|
+
payload.logical_path || payload.session_id || payload.agent_id || payload.rollup_key
|
|
474
|
+
|| payload.call_id || payload.transcript_id || event.event_id,
|
|
475
|
+
].join('\u001f');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const WAIT_ARRAY = new Int32Array(new SharedArrayBuffer(4));
|
|
479
|
+
function waitSync(milliseconds) { Atomics.wait(WAIT_ARRAY, 0, 0, milliseconds); }
|
|
480
|
+
|
|
481
|
+
function acquireBatchFileLease(path, waitMs = 0) {
|
|
482
|
+
const lock = `${path}.lock`;
|
|
483
|
+
const deadline = Date.now() + Math.max(0, waitMs);
|
|
484
|
+
do {
|
|
485
|
+
try {
|
|
486
|
+
mkdirSync(lock);
|
|
487
|
+
return lock;
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
490
|
+
try {
|
|
491
|
+
if (Date.now() - statSync(lock).mtimeMs > 60_000) {
|
|
492
|
+
const stale = `${lock}.stale-${process.pid}-${Date.now()}`;
|
|
493
|
+
renameSync(lock, stale);
|
|
494
|
+
rmSync(stale, { recursive: true, force: true });
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
} catch { /* another process recovered or released it */ }
|
|
498
|
+
if (Date.now() >= deadline) return null;
|
|
499
|
+
waitSync(Math.min(5, Math.max(1, deadline - Date.now())));
|
|
500
|
+
}
|
|
501
|
+
} while (Date.now() <= deadline);
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function releaseBatchFileLease(lock) {
|
|
506
|
+
if (lock) rmSync(lock, { recursive: true, force: true });
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Queue a precise writer batch and replace older pending state for the same logical scope. */
|
|
510
|
+
export function enqueueObserverSqlBatch(vaultBase, batch, { scope = 'incremental', now = new Date() } = {}) {
|
|
511
|
+
if (!batch?.project_id || !Array.isArray(batch.events)) throw new Error('Batch incremental do Observer inválido.');
|
|
512
|
+
if (!batch.events.length) return { queued: false, events: 0, path: '' };
|
|
513
|
+
mkdirSync(outboxDir(vaultBase), { recursive: true });
|
|
514
|
+
const path = join(outboxDir(vaultBase), `sql-live-${hash(`${batch.project_id}:${scope}`).slice(0, 24)}.json`);
|
|
515
|
+
const lease = acquireBatchFileLease(path, 200);
|
|
516
|
+
if (!lease) {
|
|
517
|
+
const fallback = queueOutbox(vaultBase, { ...batch, enqueued_at: isoNow(now), scope });
|
|
518
|
+
return { queued: true, events: batch.events.length, path: fallback, coalesced: false };
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
const existing = readJson(path, { events: [] });
|
|
522
|
+
const merged = new Map();
|
|
523
|
+
for (const event of [...(existing.events || []), ...batch.events]) merged.set(eventCoalesceKey(event), event);
|
|
524
|
+
const timestamp = isoNow(now);
|
|
525
|
+
atomicJson(path, {
|
|
526
|
+
schema_version: SQL_SCHEMA_VERSION,
|
|
527
|
+
project_id: batch.project_id,
|
|
528
|
+
scope,
|
|
529
|
+
enqueued_at: existing.enqueued_at || timestamp,
|
|
530
|
+
updated_at: timestamp,
|
|
531
|
+
events: [...merged.values()],
|
|
532
|
+
});
|
|
533
|
+
return { queued: true, events: merged.size, path, coalesced: true };
|
|
534
|
+
} finally {
|
|
535
|
+
releaseBatchFileLease(lease);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Writer seam for note/archive commands: enqueue exactly one known logical document. */
|
|
540
|
+
export function enqueueObserverDocumentChange({
|
|
541
|
+
vaultBase,
|
|
542
|
+
projectId,
|
|
543
|
+
logicalPath,
|
|
544
|
+
deleted = false,
|
|
545
|
+
now = new Date(),
|
|
546
|
+
} = {}) {
|
|
547
|
+
const normalized = normalizePath(logicalPath);
|
|
548
|
+
if (!vaultBase || !projectId || !normalized || normalized.startsWith('/') || normalized.includes('..')) {
|
|
549
|
+
throw new Error('Documento incremental do Observer inválido.');
|
|
550
|
+
}
|
|
551
|
+
const state = readState(vaultBase);
|
|
552
|
+
const nextState = {
|
|
553
|
+
schema_version: SQL_SCHEMA_VERSION,
|
|
554
|
+
files: { ...(state.files || {}) },
|
|
555
|
+
transcripts: { ...(state.transcripts || {}) },
|
|
556
|
+
};
|
|
557
|
+
const previous = state.files?.[normalized];
|
|
558
|
+
const revision = Math.max(1, Number(previous?.revision || 0) + 1);
|
|
559
|
+
const occurredAt = isoNow(now);
|
|
560
|
+
let event;
|
|
561
|
+
if (deleted) {
|
|
562
|
+
if (!previous) return { queued: false, unchanged: true, events: 0 };
|
|
563
|
+
event = {
|
|
564
|
+
schema_version: 1,
|
|
565
|
+
event_id: eventId('document-delete', projectId, `${normalized}:${revision}`),
|
|
566
|
+
kind: 'document.delete',
|
|
567
|
+
project_id: projectId,
|
|
568
|
+
occurred_at: occurredAt,
|
|
569
|
+
payload: { logical_path: normalized, revision },
|
|
570
|
+
};
|
|
571
|
+
nextState.files[normalized] = { ...previous, revision, deleted: true };
|
|
572
|
+
} else {
|
|
573
|
+
const absolute = join(vaultBase, normalized);
|
|
574
|
+
if (!existsSync(absolute)) return { queued: false, missing: true, events: 0 };
|
|
575
|
+
const content = readFileSync(absolute, 'utf8');
|
|
576
|
+
const metadata = parseFrontmatter(content);
|
|
577
|
+
const contentHash = hash(sanitizeObserverContent(content));
|
|
578
|
+
if (previous?.content_hash === contentHash && previous?.deleted !== true) {
|
|
579
|
+
return { queued: false, unchanged: true, events: 0 };
|
|
580
|
+
}
|
|
581
|
+
event = documentEvent({ projectId, logicalPath: normalized, content, metadata, revision, occurredAt });
|
|
582
|
+
nextState.files[normalized] = { content_hash: contentHash, revision };
|
|
583
|
+
}
|
|
584
|
+
const queued = enqueueObserverSqlBatch(vaultBase, {
|
|
585
|
+
schema_version: SQL_SCHEMA_VERSION,
|
|
586
|
+
project_id: projectId,
|
|
587
|
+
events: [event],
|
|
588
|
+
}, { scope: `document:${normalized}`, now });
|
|
589
|
+
atomicJson(statePath(vaultBase), nextState);
|
|
590
|
+
return { ...queued, event_id: event.event_id, deleted };
|
|
591
|
+
}
|
|
592
|
+
|
|
331
593
|
export function listSqlOutbox(vaultBase) {
|
|
332
594
|
const dir = outboxDir(vaultBase);
|
|
333
595
|
if (!existsSync(dir)) return [];
|
|
334
|
-
return readdirSync(dir).filter((name) => /^sql-[a-f0-9]{24}\.json$/.test(name)).sort().flatMap((name) => {
|
|
596
|
+
return readdirSync(dir).filter((name) => /^sql-(?:live-)?[a-f0-9]{24}\.json$/.test(name)).sort().flatMap((name) => {
|
|
335
597
|
try { return [{ path: join(dir, name), ...JSON.parse(readFileSync(join(dir, name), 'utf8')) }]; } catch { return []; }
|
|
336
598
|
});
|
|
337
599
|
}
|
|
338
600
|
|
|
601
|
+
export function inspectObserverSqlOutbox(vaultBase, currentTime = Date.now()) {
|
|
602
|
+
const batches = listSqlOutbox(vaultBase);
|
|
603
|
+
let bytes = 0;
|
|
604
|
+
let oldestAt = '';
|
|
605
|
+
let events = 0;
|
|
606
|
+
for (const batch of batches) {
|
|
607
|
+
try { bytes += statSync(batch.path).size; } catch { /* raced with a drain */ }
|
|
608
|
+
events += Array.isArray(batch.events) ? batch.events.length : 0;
|
|
609
|
+
const candidate = text(batch.enqueued_at || batch.updated_at);
|
|
610
|
+
if (candidate && (!oldestAt || candidate < oldestAt)) oldestAt = candidate;
|
|
611
|
+
}
|
|
612
|
+
const oldestAgeMs = oldestAt ? Math.max(0, Number(currentTime) - Date.parse(oldestAt)) : 0;
|
|
613
|
+
return { batches: batches.length, events, bytes, oldest_at: oldestAt, oldest_age_ms: oldestAgeMs };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function leasePath(vaultBase) { return join(vaultBase, SQL_PUBLISHER_LEASE_REL); }
|
|
617
|
+
|
|
618
|
+
function acquirePublisherLease(vaultBase, currentTime = Date.now()) {
|
|
619
|
+
const path = leasePath(vaultBase);
|
|
620
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
621
|
+
const token = `${process.pid}-${currentTime}-${Math.random().toString(16).slice(2)}`;
|
|
622
|
+
try {
|
|
623
|
+
mkdirSync(path);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
626
|
+
let age = 0;
|
|
627
|
+
try { age = currentTime - statSync(path).mtimeMs; } catch { return null; }
|
|
628
|
+
if (age <= 60_000) return null;
|
|
629
|
+
const stale = `${path}.stale-${token}`;
|
|
630
|
+
try {
|
|
631
|
+
renameSync(path, stale);
|
|
632
|
+
rmSync(stale, { recursive: true, force: true });
|
|
633
|
+
mkdirSync(path);
|
|
634
|
+
} catch { return null; }
|
|
635
|
+
}
|
|
636
|
+
atomicJson(join(path, 'owner.json'), { token, pid: process.pid, acquired_at: new Date(currentTime).toISOString() });
|
|
637
|
+
return { path, token };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function releasePublisherLease(lease) {
|
|
641
|
+
if (!lease) return;
|
|
642
|
+
const owner = readJson(join(lease.path, 'owner.json'), {});
|
|
643
|
+
if (owner.token === lease.token) rmSync(lease.path, { recursive: true, force: true });
|
|
644
|
+
}
|
|
645
|
+
|
|
339
646
|
async function postSqlChunk({ url, projectId, events, fetchImpl = globalThis.fetch, token = '' }) {
|
|
340
647
|
const controller = new AbortController();
|
|
341
648
|
const rawBody = Buffer.from(JSON.stringify({ events }), 'utf8');
|
|
@@ -402,17 +709,65 @@ async function postSqlBatch({ url, projectId, events, fetchImpl = globalThis.fet
|
|
|
402
709
|
export async function retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl = globalThis.fetch, token = process.env.WENDKEEP_OBSERVER_TOKEN || '' } = {}) {
|
|
403
710
|
const pending = listSqlOutbox(vaultBase);
|
|
404
711
|
if (!url) return { attempted: 0, confirmed: 0, pending: pending.length };
|
|
712
|
+
const lease = acquirePublisherLease(vaultBase);
|
|
713
|
+
if (!lease) return { attempted: 0, confirmed: 0, pending: pending.length, busy: true };
|
|
405
714
|
let attempted = 0;
|
|
406
715
|
let confirmed = 0;
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
716
|
+
try {
|
|
717
|
+
for (const batch of pending) {
|
|
718
|
+
if (batch.project_id && batch.project_id !== projectId) continue;
|
|
719
|
+
const batchLease = acquireBatchFileLease(batch.path);
|
|
720
|
+
if (!batchLease) continue;
|
|
721
|
+
attempted += 1;
|
|
722
|
+
try {
|
|
723
|
+
const current = readJson(batch.path, null);
|
|
724
|
+
if (!current?.events?.length) continue;
|
|
725
|
+
await postSqlBatch({ url, projectId, events: current.events, fetchImpl, token });
|
|
726
|
+
if (existsSync(batch.path)) unlinkSync(batch.path);
|
|
727
|
+
confirmed += 1;
|
|
728
|
+
} catch { break; }
|
|
729
|
+
finally { releaseBatchFileLease(batchLease); }
|
|
730
|
+
}
|
|
731
|
+
return { attempted, confirmed, pending: listSqlOutbox(vaultBase).length, busy: false };
|
|
732
|
+
} finally {
|
|
733
|
+
releasePublisherLease(lease);
|
|
414
734
|
}
|
|
415
|
-
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** Hook path: enqueue at most one touched session/subagent, then drain under one lease. */
|
|
738
|
+
export async function publishObserverSqlIncremental({
|
|
739
|
+
vaultBase,
|
|
740
|
+
projectId,
|
|
741
|
+
url = process.env.WENDKEEP_OBSERVER_URL || '',
|
|
742
|
+
input = {},
|
|
743
|
+
now = new Date(),
|
|
744
|
+
fetchImpl = globalThis.fetch,
|
|
745
|
+
token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
|
|
746
|
+
captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata',
|
|
747
|
+
} = {}) {
|
|
748
|
+
if (!vaultBase || !projectId) throw new Error('vaultBase e projectId são obrigatórios.');
|
|
749
|
+
const state = readState(vaultBase);
|
|
750
|
+
const batch = buildObserverSqlIncrementalBatch({
|
|
751
|
+
vaultBase, projectId, input, now, state, captureLevel,
|
|
752
|
+
});
|
|
753
|
+
const queued = enqueueObserverSqlBatch(vaultBase, {
|
|
754
|
+
schema_version: SQL_SCHEMA_VERSION,
|
|
755
|
+
project_id: projectId,
|
|
756
|
+
events: batch.events,
|
|
757
|
+
}, { scope: batch.scope, now });
|
|
758
|
+
if (batch.events.length) atomicJson(statePath(vaultBase), batch.nextState);
|
|
759
|
+
const replay = await retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl, token });
|
|
760
|
+
const pending = listSqlOutbox(vaultBase).length;
|
|
761
|
+
return {
|
|
762
|
+
ok: pending === 0,
|
|
763
|
+
queued: pending > 0,
|
|
764
|
+
scanned: batch.scanned,
|
|
765
|
+
changed: batch.changed,
|
|
766
|
+
enqueued_events: queued.events || 0,
|
|
767
|
+
pending,
|
|
768
|
+
replay,
|
|
769
|
+
hookExitCode: 0,
|
|
770
|
+
};
|
|
416
771
|
}
|
|
417
772
|
|
|
418
773
|
export async function publishObserverSql({ vaultBase, projectId, url = process.env.WENDKEEP_OBSERVER_URL || '', input = {}, now = new Date(), fetchImpl = globalThis.fetch, token = process.env.WENDKEEP_OBSERVER_TOKEN || '', captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata' } = {}) {
|
|
@@ -423,17 +778,22 @@ export async function publishObserverSql({ vaultBase, projectId, url = process.e
|
|
|
423
778
|
? await readRemoteDocuments({ url, projectId, fetchImpl, token })
|
|
424
779
|
: {};
|
|
425
780
|
const batch = buildObserverSqlEventBatch({ vaultBase, projectId, input, now, state, remoteDocuments, captureLevel });
|
|
426
|
-
|
|
427
|
-
|
|
781
|
+
if (!batch.events.length) {
|
|
782
|
+
atomicJson(statePath(vaultBase), batch.nextState);
|
|
783
|
+
return { ok: true, queued: false, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase).length, replay };
|
|
784
|
+
}
|
|
428
785
|
if (!url) {
|
|
429
786
|
queueOutbox(vaultBase, { schema_version: SQL_SCHEMA_VERSION, project_id: projectId, events: batch.events });
|
|
787
|
+
atomicJson(statePath(vaultBase), batch.nextState);
|
|
430
788
|
return { ok: false, queued: true, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase).length, replay, hookExitCode: 0 };
|
|
431
789
|
}
|
|
432
790
|
try {
|
|
433
791
|
const response = await postSqlBatch({ url, projectId, events: batch.events, fetchImpl, token });
|
|
792
|
+
atomicJson(statePath(vaultBase), batch.nextState);
|
|
434
793
|
return { ok: true, queued: false, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase).length, replay, response };
|
|
435
794
|
} catch (error) {
|
|
436
795
|
queueOutbox(vaultBase, { schema_version: SQL_SCHEMA_VERSION, project_id: projectId, events: batch.events });
|
|
796
|
+
atomicJson(statePath(vaultBase), batch.nextState);
|
|
437
797
|
return { ok: false, queued: true, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase).length, replay, hookExitCode: 0, error: error.message };
|
|
438
798
|
}
|
|
439
799
|
}
|