wendkeep 0.57.2 → 0.58.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 +21 -0
- package/README.en.md +42 -8
- package/README.md +42 -8
- package/bin/wendkeep.mjs +16 -4
- package/hooks/brain-inject.mjs +185 -19
- package/hooks/change-core.mjs +42 -5
- package/hooks/lessons-core.mjs +8 -2
- package/hooks/memory-handoff.mjs +199 -0
- package/hooks/memory-schema.mjs +295 -0
- package/hooks/memory-store.mjs +660 -0
- package/hooks/obsidian-common.mjs +241 -6
- package/hooks/session-ensure.mjs +20 -0
- package/hooks/session-start.mjs +13 -4
- package/hooks/session-stop.mjs +134 -16
- package/hooks/vault-health.mjs +139 -1
- package/package.json +2 -2
- package/src/init.mjs +2 -0
- package/src/memory.mjs +210 -0
- package/src/taxonomy.mjs +3 -0
- package/src/validate-memory.mjs +115 -0
|
@@ -338,9 +338,12 @@ export function mutateSessionRegistry(vaultBase, mutator, { timeoutMs = 2000 } =
|
|
|
338
338
|
|
|
339
339
|
try {
|
|
340
340
|
const registry = readSessionRegistry(vaultBase);
|
|
341
|
+
const before = JSON.stringify(registry);
|
|
341
342
|
registry.version = 2;
|
|
342
343
|
const result = mutator(registry);
|
|
343
|
-
|
|
344
|
+
if (JSON.stringify(registry) !== before) {
|
|
345
|
+
writeSessionRegistry(vaultBase, registry);
|
|
346
|
+
}
|
|
344
347
|
return result;
|
|
345
348
|
} finally {
|
|
346
349
|
// rmSync recursivo não remove diretório em caminho não-ASCII no Windows — ver
|
|
@@ -350,14 +353,204 @@ export function mutateSessionRegistry(vaultBase, mutator, { timeoutMs = 2000 } =
|
|
|
350
353
|
}
|
|
351
354
|
|
|
352
355
|
function meaningfulPatch(patch = {}) {
|
|
353
|
-
const protectedNonEmpty = new Set(['session_file', 'transcript_path', 'transcript_id', 'provider', 'started_at', 'change_slug']);
|
|
356
|
+
const protectedNonEmpty = new Set(['session_file', 'transcript_path', 'transcript_id', 'provider', 'started_at', 'change_slug', 'activation_id']);
|
|
354
357
|
return Object.fromEntries(Object.entries(patch).filter(([key, value]) => {
|
|
358
|
+
if (key === 'advance_turn_sequence' || key === 'turn_sequence') return false;
|
|
355
359
|
if (value === undefined || value === null) return false;
|
|
356
360
|
if (value === '' && protectedNonEmpty.has(key)) return false;
|
|
357
361
|
return true;
|
|
358
362
|
}));
|
|
359
363
|
}
|
|
360
364
|
|
|
365
|
+
function nonNegativeSequence(value, fallback = null) {
|
|
366
|
+
const parsed = Number(value);
|
|
367
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function cloneRegistry(registry = {}) {
|
|
371
|
+
const sessions = Object.fromEntries(Object.entries(registry.sessions || {}).map(([id, entry]) => [
|
|
372
|
+
id,
|
|
373
|
+
{
|
|
374
|
+
...(entry || {}),
|
|
375
|
+
...(entry?.activations && typeof entry.activations === 'object'
|
|
376
|
+
? { activations: Object.fromEntries(Object.entries(entry.activations).map(([activationId, activation]) => [activationId, { ...(activation || {}) }])) }
|
|
377
|
+
: {}),
|
|
378
|
+
},
|
|
379
|
+
]));
|
|
380
|
+
return { ...registry, version: Math.max(2, registry.version || 1), sessions };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Pure causal helpers. Keeping them independent from filesystem I/O lets Stop perform its
|
|
384
|
+
// compare-and-swap under the same registry lock used by the existing upsert path.
|
|
385
|
+
export function openActivation(registry, session = {}, explicitActivationId = '') {
|
|
386
|
+
const next = cloneRegistry(registry);
|
|
387
|
+
const sessionId = session.session_id || session.canonical_session_id || session.id || '';
|
|
388
|
+
const activationId = explicitActivationId || session.activation_id || '';
|
|
389
|
+
if (!sessionId || !activationId) throw new TypeError('session_id and activation_id are required');
|
|
390
|
+
|
|
391
|
+
const current = next.sessions[sessionId] || {};
|
|
392
|
+
const activations = { ...(current.activations || {}) };
|
|
393
|
+
const requestedSequence = nonNegativeSequence(
|
|
394
|
+
session.turn_sequence ?? session.last_turn_sequence,
|
|
395
|
+
0,
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
if (current.active_activation_id === activationId && activations[activationId]?.status === 'active') {
|
|
399
|
+
const sequence = Math.max(nonNegativeSequence(current.last_turn_sequence, 0), requestedSequence);
|
|
400
|
+
activations[activationId] = {
|
|
401
|
+
...activations[activationId],
|
|
402
|
+
last_turn_sequence: sequence,
|
|
403
|
+
};
|
|
404
|
+
next.sessions[sessionId] = {
|
|
405
|
+
...current,
|
|
406
|
+
activation_id: activationId,
|
|
407
|
+
active_activation_id: activationId,
|
|
408
|
+
last_turn_sequence: sequence,
|
|
409
|
+
activations,
|
|
410
|
+
};
|
|
411
|
+
return next;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const previousId = current.active_activation_id || '';
|
|
415
|
+
if (previousId && activations[previousId]?.status === 'active') {
|
|
416
|
+
activations[previousId] = {
|
|
417
|
+
...activations[previousId],
|
|
418
|
+
status: 'superseded',
|
|
419
|
+
superseded_by: activationId,
|
|
420
|
+
superseded_at: session.started_at || session.activation_started_at || '',
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const epoch = Math.max(
|
|
425
|
+
nonNegativeSequence(current.activation_epoch, 0),
|
|
426
|
+
...Object.values(activations).map((activation) => nonNegativeSequence(activation?.epoch, 0)),
|
|
427
|
+
) + 1;
|
|
428
|
+
activations[activationId] = {
|
|
429
|
+
activation_id: activationId,
|
|
430
|
+
epoch,
|
|
431
|
+
status: 'active',
|
|
432
|
+
started_at: session.activation_started_at || session.started_at || '',
|
|
433
|
+
last_turn_sequence: requestedSequence,
|
|
434
|
+
...(session.transcript_id ? { transcript_id: session.transcript_id } : {}),
|
|
435
|
+
...(session.transcript_path ? { transcript_path: session.transcript_path } : {}),
|
|
436
|
+
...(session.provider ? { provider: session.provider } : {}),
|
|
437
|
+
};
|
|
438
|
+
next.sessions[sessionId] = {
|
|
439
|
+
...current,
|
|
440
|
+
activation_id: activationId,
|
|
441
|
+
active_activation_id: activationId,
|
|
442
|
+
activation_epoch: epoch,
|
|
443
|
+
activation_started_at: session.activation_started_at || session.started_at || '',
|
|
444
|
+
last_turn_sequence: requestedSequence,
|
|
445
|
+
activations,
|
|
446
|
+
};
|
|
447
|
+
return next;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function advanceActivationTurn(registry, turn = {}) {
|
|
451
|
+
const next = cloneRegistry(registry);
|
|
452
|
+
const sessionId = turn.session_id || turn.canonical_session_id || '';
|
|
453
|
+
const current = next.sessions[sessionId];
|
|
454
|
+
if (!current) return next;
|
|
455
|
+
|
|
456
|
+
const activeId = current.active_activation_id || '';
|
|
457
|
+
if (turn.activation_id && activeId && turn.activation_id !== activeId) return next;
|
|
458
|
+
const previous = nonNegativeSequence(current.last_turn_sequence, 0);
|
|
459
|
+
const explicit = nonNegativeSequence(turn.turn_sequence ?? turn.last_turn_sequence);
|
|
460
|
+
const sequence = explicit === null ? previous + 1 : Math.max(previous, explicit);
|
|
461
|
+
const activations = { ...(current.activations || {}) };
|
|
462
|
+
if (activeId && activations[activeId]) {
|
|
463
|
+
activations[activeId] = {
|
|
464
|
+
...activations[activeId],
|
|
465
|
+
last_turn_sequence: Math.max(
|
|
466
|
+
nonNegativeSequence(activations[activeId].last_turn_sequence, 0),
|
|
467
|
+
sequence,
|
|
468
|
+
),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
next.sessions[sessionId] = { ...current, last_turn_sequence: sequence, activations };
|
|
472
|
+
return next;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function resolveStopActivation(registry, stop = {}) {
|
|
476
|
+
const sessionId = stop.session_id || stop.canonical_session_id || '';
|
|
477
|
+
const entry = registry?.sessions?.[sessionId];
|
|
478
|
+
if (!entry) return '';
|
|
479
|
+
if (stop.activation_id) return String(stop.activation_id);
|
|
480
|
+
|
|
481
|
+
const transcriptId = String(stop.transcript_id || '');
|
|
482
|
+
const transcriptPath = String(stop.transcript_path || '');
|
|
483
|
+
if (!transcriptId && !transcriptPath) return '';
|
|
484
|
+
const matches = Object.entries(entry.activations || {}).filter(([, activation]) => {
|
|
485
|
+
if (!activation) return false;
|
|
486
|
+
if (transcriptId && activation.transcript_id && activation.transcript_id !== transcriptId) return false;
|
|
487
|
+
const paths = [
|
|
488
|
+
...(Array.isArray(activation.transcript_paths) ? activation.transcript_paths : []),
|
|
489
|
+
activation.transcript_path,
|
|
490
|
+
].filter(Boolean);
|
|
491
|
+
if (transcriptPath && paths.length && !paths.some((path) => transcriptsMatch(path, transcriptPath))) return false;
|
|
492
|
+
return Boolean(
|
|
493
|
+
(transcriptId && activation.transcript_id === transcriptId)
|
|
494
|
+
|| (transcriptPath && paths.some((path) => transcriptsMatch(path, transcriptPath))),
|
|
495
|
+
);
|
|
496
|
+
});
|
|
497
|
+
return matches.length === 1 ? matches[0][0] : '';
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function stopResult(registry, stopDisposition, canPromoteMemory = false) {
|
|
501
|
+
return {
|
|
502
|
+
...registry,
|
|
503
|
+
registry,
|
|
504
|
+
stopDisposition,
|
|
505
|
+
canPromoteMemory,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function applyStopActivation(registry, stop = {}) {
|
|
510
|
+
const next = cloneRegistry(registry);
|
|
511
|
+
const sessionId = stop.session_id || stop.canonical_session_id || '';
|
|
512
|
+
const current = next.sessions[sessionId];
|
|
513
|
+
const activeId = current?.active_activation_id || '';
|
|
514
|
+
const stopActivationId = stop.activation_id || '';
|
|
515
|
+
if (!current || !activeId || !stopActivationId) return stopResult(next, 'ambiguous');
|
|
516
|
+
if (current.activations?.[activeId]?.status !== 'active') return stopResult(next, 'ambiguous');
|
|
517
|
+
|
|
518
|
+
if (activeId !== stopActivationId) {
|
|
519
|
+
const activations = { ...(current.activations || {}) };
|
|
520
|
+
if (activations[stopActivationId]?.status === 'active') {
|
|
521
|
+
activations[stopActivationId] = {
|
|
522
|
+
...activations[stopActivationId],
|
|
523
|
+
status: 'superseded',
|
|
524
|
+
superseded_by: activeId,
|
|
525
|
+
};
|
|
526
|
+
next.sessions[sessionId] = { ...current, activations };
|
|
527
|
+
}
|
|
528
|
+
return stopResult(next, 'superseded');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const stopSequence = nonNegativeSequence(stop.turn_sequence ?? stop.last_turn_sequence);
|
|
532
|
+
const lastSequence = nonNegativeSequence(current.last_turn_sequence, 0);
|
|
533
|
+
if (stopSequence === null) return stopResult(next, 'ambiguous');
|
|
534
|
+
if (stopSequence < lastSequence) return stopResult(next, 'stale_turn');
|
|
535
|
+
|
|
536
|
+
const activations = { ...(current.activations || {}) };
|
|
537
|
+
activations[activeId] = {
|
|
538
|
+
...(activations[activeId] || { activation_id: activeId, epoch: current.activation_epoch }),
|
|
539
|
+
status: 'done',
|
|
540
|
+
ended_at: stop.ended_at || '',
|
|
541
|
+
last_turn_sequence: stopSequence,
|
|
542
|
+
};
|
|
543
|
+
next.sessions[sessionId] = {
|
|
544
|
+
...current,
|
|
545
|
+
status: 'done',
|
|
546
|
+
ended_at: stop.ended_at || current.ended_at || '',
|
|
547
|
+
active_activation_id: '',
|
|
548
|
+
last_turn_sequence: stopSequence,
|
|
549
|
+
activations,
|
|
550
|
+
};
|
|
551
|
+
return stopResult(next, 'applied', true);
|
|
552
|
+
}
|
|
553
|
+
|
|
361
554
|
// Remove one registry entry, but ONLY when its transcript matches the given path — this is
|
|
362
555
|
// self-healing for entries wendkeep itself mis-wrote (a subagent rollout registered as a
|
|
363
556
|
// top-level session by import <=0.46.1), never generic registry cleanup. An entry with the
|
|
@@ -382,16 +575,58 @@ export function upsertSessionRegistry(vaultBase, sessionId, patch) {
|
|
|
382
575
|
const clean = meaningfulPatch(patch);
|
|
383
576
|
const next = mutateSessionRegistry(vaultBase, (registry) => {
|
|
384
577
|
const current = registry.sessions[sessionId] || {};
|
|
578
|
+
let causalCurrent = current;
|
|
579
|
+
if (clean.activation_id) {
|
|
580
|
+
causalCurrent = openActivation(
|
|
581
|
+
{ version: registry.version, sessions: { [sessionId]: current } },
|
|
582
|
+
{
|
|
583
|
+
session_id: sessionId,
|
|
584
|
+
activation_id: clean.activation_id,
|
|
585
|
+
activation_started_at: clean.activation_started_at,
|
|
586
|
+
started_at: clean.activation_started_at || clean.started_at,
|
|
587
|
+
last_turn_sequence: clean.last_turn_sequence,
|
|
588
|
+
transcript_id: clean.transcript_id || current.transcript_id,
|
|
589
|
+
transcript_path: clean.transcript_path || current.transcript_path,
|
|
590
|
+
provider: clean.provider || current.provider,
|
|
591
|
+
},
|
|
592
|
+
).sessions[sessionId];
|
|
593
|
+
}
|
|
594
|
+
if (patch?.advance_turn_sequence === true || patch?.turn_sequence !== undefined) {
|
|
595
|
+
causalCurrent = advanceActivationTurn(
|
|
596
|
+
{ version: registry.version, sessions: { [sessionId]: causalCurrent } },
|
|
597
|
+
{
|
|
598
|
+
session_id: sessionId,
|
|
599
|
+
activation_id: causalCurrent.active_activation_id || '',
|
|
600
|
+
turn_sequence: patch.turn_sequence,
|
|
601
|
+
},
|
|
602
|
+
).sessions[sessionId];
|
|
603
|
+
}
|
|
604
|
+
const activeId = causalCurrent.active_activation_id || '';
|
|
605
|
+
if (activeId && causalCurrent.activations?.[activeId]) {
|
|
606
|
+
const active = causalCurrent.activations[activeId];
|
|
607
|
+
causalCurrent = {
|
|
608
|
+
...causalCurrent,
|
|
609
|
+
activations: {
|
|
610
|
+
...causalCurrent.activations,
|
|
611
|
+
[activeId]: {
|
|
612
|
+
...active,
|
|
613
|
+
...(!active.transcript_id && clean.transcript_id ? { transcript_id: clean.transcript_id } : {}),
|
|
614
|
+
...(!active.transcript_path && clean.transcript_path ? { transcript_path: clean.transcript_path } : {}),
|
|
615
|
+
...(!active.provider && clean.provider ? { provider: clean.provider } : {}),
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
}
|
|
385
620
|
const transcriptPaths = [...new Set([
|
|
386
|
-
...(Array.isArray(
|
|
387
|
-
|
|
621
|
+
...(Array.isArray(causalCurrent.transcript_paths) ? causalCurrent.transcript_paths : []),
|
|
622
|
+
causalCurrent.transcript_path,
|
|
388
623
|
...(Array.isArray(clean.transcript_paths) ? clean.transcript_paths : []),
|
|
389
624
|
clean.transcript_path,
|
|
390
625
|
].filter(Boolean))];
|
|
391
626
|
const value = {
|
|
392
|
-
...
|
|
627
|
+
...causalCurrent,
|
|
393
628
|
...clean,
|
|
394
|
-
...(transcriptPaths.length ? { transcript_paths: transcriptPaths, transcript_path: clean.transcript_path ||
|
|
629
|
+
...(transcriptPaths.length ? { transcript_paths: transcriptPaths, transcript_path: clean.transcript_path || causalCurrent.transcript_path || transcriptPaths.at(-1) } : {}),
|
|
395
630
|
last_seen: clean.last_seen || clean.updated_at || formatLocalIso(new Date()),
|
|
396
631
|
updated_at: clean.updated_at || formatLocalIso(new Date()),
|
|
397
632
|
};
|
package/hooks/session-ensure.mjs
CHANGED
|
@@ -37,6 +37,12 @@ function sessionIdFromInput(input) {
|
|
|
37
37
|
return input.session_id || input.sessionId || input.codex_session_id || '';
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function turnSequenceFromInput(input = {}) {
|
|
41
|
+
const value = input.turn_sequence ?? input.turnSequence;
|
|
42
|
+
const parsed = Number(value);
|
|
43
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
40
46
|
function buildSessionContent({ relPath, now, summary = 'session', sessionId = '', reason = 'Sessão criada automaticamente pelo hook UserPromptSubmit.' }) {
|
|
41
47
|
const date = formatDate(now);
|
|
42
48
|
const startedAt = formatLocalIso(now);
|
|
@@ -262,6 +268,8 @@ function activateExistingSession({ vaultBase, relPath, startedAt, sessionId, inp
|
|
|
262
268
|
transcript_path: identity.transcriptPath,
|
|
263
269
|
transcript_id: identity.transcriptId,
|
|
264
270
|
provider: identity.provider,
|
|
271
|
+
advance_turn_sequence: true,
|
|
272
|
+
turn_sequence: turnSequenceFromInput(input),
|
|
265
273
|
});
|
|
266
274
|
return true;
|
|
267
275
|
}
|
|
@@ -288,6 +296,8 @@ function createSession({ vaultBase, sessionId, input, now, identity }) {
|
|
|
288
296
|
transcript_path: identity.transcriptPath,
|
|
289
297
|
transcript_id: identity.transcriptId,
|
|
290
298
|
provider: identity.provider,
|
|
299
|
+
advance_turn_sequence: true,
|
|
300
|
+
turn_sequence: turnSequenceFromInput(input),
|
|
291
301
|
});
|
|
292
302
|
return { relPath, startedAt };
|
|
293
303
|
}
|
|
@@ -333,6 +343,14 @@ function main() {
|
|
|
333
343
|
existsSync(join(vaultBase, ctrl.session_file)) &&
|
|
334
344
|
ctrl.session_id === sessionId
|
|
335
345
|
) {
|
|
346
|
+
upsertSessionRegistry(vaultBase, sessionId, {
|
|
347
|
+
status: 'active',
|
|
348
|
+
transcript_path: identity.transcriptPath,
|
|
349
|
+
transcript_id: identity.transcriptId,
|
|
350
|
+
provider: identity.provider,
|
|
351
|
+
advance_turn_sequence: true,
|
|
352
|
+
turn_sequence: turnSequenceFromInput(input),
|
|
353
|
+
});
|
|
336
354
|
writeHookOutput({});
|
|
337
355
|
return;
|
|
338
356
|
}
|
|
@@ -375,6 +393,8 @@ function main() {
|
|
|
375
393
|
transcript_path: identity.transcriptPath,
|
|
376
394
|
transcript_id: identity.transcriptId,
|
|
377
395
|
provider: identity.provider,
|
|
396
|
+
advance_turn_sequence: true,
|
|
397
|
+
turn_sequence: turnSequenceFromInput(input),
|
|
378
398
|
});
|
|
379
399
|
writeHookOutput({});
|
|
380
400
|
return;
|
package/hooks/session-start.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, writeFileSync } from 'fs';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
3
4
|
import { basename, join } from 'path';
|
|
4
5
|
import { pathToFileURL } from 'url';
|
|
5
6
|
import {
|
|
@@ -169,6 +170,14 @@ function main() {
|
|
|
169
170
|
const sessionId = identity.canonicalConversationId;
|
|
170
171
|
const transcriptPath = identity.transcriptPath;
|
|
171
172
|
const control = readControl(vaultBase);
|
|
173
|
+
const activationId = input.activation_id || input.activationId || randomUUID();
|
|
174
|
+
const activationStartedAt = formatLocalIso(now);
|
|
175
|
+
const registerActivation = (canonicalSessionId, patch) => upsertSessionRegistry(vaultBase, canonicalSessionId, {
|
|
176
|
+
...patch,
|
|
177
|
+
activation_id: activationId,
|
|
178
|
+
activation_started_at: activationStartedAt,
|
|
179
|
+
last_turn_sequence: 0,
|
|
180
|
+
});
|
|
172
181
|
|
|
173
182
|
// Fecha sessões `active` órfãs (sem evento de fim — janela fechada/crash) antes
|
|
174
183
|
// de seguir. Preserva a deste transcript: pode ser reaproveitada logo abaixo.
|
|
@@ -186,7 +195,7 @@ function main() {
|
|
|
186
195
|
if (control.status === 'active' && control.session_file && control.session_id === sessionId) {
|
|
187
196
|
const activePath = join(vaultBase, control.session_file);
|
|
188
197
|
if (existsSync(activePath)) {
|
|
189
|
-
|
|
198
|
+
registerActivation(sessionId, {
|
|
190
199
|
session_file: control.session_file,
|
|
191
200
|
status: 'active',
|
|
192
201
|
started_at: control.started_at,
|
|
@@ -232,7 +241,7 @@ function main() {
|
|
|
232
241
|
session_id: sessionId,
|
|
233
242
|
last_logged_turn_id: control.last_logged_turn_id || '',
|
|
234
243
|
});
|
|
235
|
-
|
|
244
|
+
registerActivation(sessionId, {
|
|
236
245
|
session_file: known.session_file,
|
|
237
246
|
status: 'active',
|
|
238
247
|
started_at: startedAt,
|
|
@@ -275,7 +284,7 @@ function main() {
|
|
|
275
284
|
session_id: sessionId || match.sessionId,
|
|
276
285
|
last_logged_turn_id: control.last_logged_turn_id || '',
|
|
277
286
|
});
|
|
278
|
-
|
|
287
|
+
registerActivation(sessionId || match.sessionId, {
|
|
279
288
|
session_file: match.session_file,
|
|
280
289
|
status: 'active',
|
|
281
290
|
started_at: startedAt,
|
|
@@ -305,7 +314,7 @@ function main() {
|
|
|
305
314
|
started_at: startedAt,
|
|
306
315
|
session_id: sessionId,
|
|
307
316
|
});
|
|
308
|
-
|
|
317
|
+
registerActivation(sessionId, {
|
|
309
318
|
session_file: relPath,
|
|
310
319
|
status: 'active',
|
|
311
320
|
started_at: startedAt,
|
package/hooks/session-stop.mjs
CHANGED
|
@@ -12,6 +12,9 @@ import { updateSessionObservability } from './session-observability.mjs';
|
|
|
12
12
|
import { resolveSessionEntry } from './session-identity.mjs';
|
|
13
13
|
import { mutateSessionNote } from './session-note-io.mjs';
|
|
14
14
|
import { applyDerivedSections, provenanceSessions } from './derived-sections.mjs';
|
|
15
|
+
import { buildSessionMemoryEvents, collectLifecycleEvidence } from './memory-handoff.mjs';
|
|
16
|
+
import { enqueueMemoryEvent, projectMemoryOutbox } from './memory-store.mjs';
|
|
17
|
+
import { sanitizeMemoryText } from './memory-schema.mjs';
|
|
15
18
|
import {
|
|
16
19
|
ensureDir,
|
|
17
20
|
findActiveSessionByTranscript,
|
|
@@ -37,6 +40,9 @@ import {
|
|
|
37
40
|
turnMarker,
|
|
38
41
|
hasTurnMarker,
|
|
39
42
|
normalizeTurnMarkers,
|
|
43
|
+
mutateSessionRegistry,
|
|
44
|
+
resolveStopActivation,
|
|
45
|
+
applyStopActivation,
|
|
40
46
|
} from './obsidian-common.mjs';
|
|
41
47
|
|
|
42
48
|
function extractContentText(content) {
|
|
@@ -792,6 +798,42 @@ function shouldFinalizeSession() {
|
|
|
792
798
|
return process.env.OBSIDIAN_NO_AUTO_FINALIZE !== '1';
|
|
793
799
|
}
|
|
794
800
|
|
|
801
|
+
export function commitSessionMemory(vaultBase, handoff, { projectOptions = {} } = {}) {
|
|
802
|
+
const events = buildSessionMemoryEvents(handoff);
|
|
803
|
+
const eventIds = events.map((event) => event.event_id);
|
|
804
|
+
try {
|
|
805
|
+
for (const event of events) enqueueMemoryEvent(vaultBase, event);
|
|
806
|
+
const projection = projectMemoryOutbox(vaultBase, projectOptions);
|
|
807
|
+
if (projection.status === 'busy') {
|
|
808
|
+
return {
|
|
809
|
+
status: 'degraded',
|
|
810
|
+
error: 'memory projector busy; outbox preserved for replay',
|
|
811
|
+
eventCount: events.length,
|
|
812
|
+
eventIds,
|
|
813
|
+
checkpoint: null,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
return {
|
|
817
|
+
status: 'projected',
|
|
818
|
+
eventCount: events.length,
|
|
819
|
+
eventIds,
|
|
820
|
+
checkpoint: {
|
|
821
|
+
revision: projection.revision,
|
|
822
|
+
event_cursor: projection.eventCursor,
|
|
823
|
+
state_hash: projection.stateHash,
|
|
824
|
+
},
|
|
825
|
+
};
|
|
826
|
+
} catch (error) {
|
|
827
|
+
return {
|
|
828
|
+
status: 'degraded',
|
|
829
|
+
error: sanitizeMemoryText(error?.message || String(error)),
|
|
830
|
+
eventCount: events.length,
|
|
831
|
+
eventIds,
|
|
832
|
+
checkpoint: null,
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
795
837
|
// Só captura checkboxes de tarefa reais (`- [ ] ...`). Antes casava as palavras
|
|
796
838
|
// `todo`/`pendência`/`pendente` em prosa (ex.: "todo" dentro de "todos"), o que
|
|
797
839
|
// despejava trechos de conversa na seção Pendências.
|
|
@@ -913,9 +955,7 @@ function replaceClosingSection(content, closing) {
|
|
|
913
955
|
export function finalizeSessionFile(sessionPath, tx, created, endedAt) {
|
|
914
956
|
const pending = extractPending(tx.rawTextForDetection);
|
|
915
957
|
const links = (items) => items.length ? items.map((rel) => ` - ${wikilinkFromRel(rel)}`).join('\n') : ' - Nenhuma';
|
|
916
|
-
const summary = tx
|
|
917
|
-
? truncate(tx.latestAssistantMessage, 500)
|
|
918
|
-
: `Sessão encerrada com ${tx.userPrompts.length} prompts e ${tx.tools.length} ferramentas registradas.`;
|
|
958
|
+
const summary = sessionFinalSummary(tx);
|
|
919
959
|
|
|
920
960
|
const closing = `## Encerramento
|
|
921
961
|
|
|
@@ -943,6 +983,12 @@ ${formatPendingClosing(pending)}
|
|
|
943
983
|
));
|
|
944
984
|
}
|
|
945
985
|
|
|
986
|
+
export function sessionFinalSummary(tx) {
|
|
987
|
+
return tx.latestAssistantMessage
|
|
988
|
+
? truncate(tx.latestAssistantMessage, 500)
|
|
989
|
+
: `Sessão encerrada com ${tx.userPrompts.length} prompts e ${tx.tools.length} ferramentas registradas.`;
|
|
990
|
+
}
|
|
991
|
+
|
|
946
992
|
// --- Vínculo Sessão ↔ Issues Linear (03-Linear) -------------------------------
|
|
947
993
|
// Coleta IDs `NUT-\d+` citados na conversa, resolve as notas em 03-Linear e, ao
|
|
948
994
|
// ler cada nota, descobre NUTs conectadas (1 salto) mencionadas no corpo dela.
|
|
@@ -1085,6 +1131,53 @@ function main() {
|
|
|
1085
1131
|
const tx = parseTranscript(input.transcript_path || input.transcriptPath);
|
|
1086
1132
|
const turnId = input.turn_id || tx.latestTurnId || String(Date.now());
|
|
1087
1133
|
const sessionId = identity.canonicalConversationId;
|
|
1134
|
+
const finalizing = shouldFinalizeSession();
|
|
1135
|
+
const now = finalizing ? new Date() : null;
|
|
1136
|
+
const endedAt = finalizing ? formatLocalIso(now) : '';
|
|
1137
|
+
const stopTurnSequence = Number.isSafeInteger(Number(input.turn_sequence))
|
|
1138
|
+
? Number(input.turn_sequence)
|
|
1139
|
+
: Number(entry.last_turn_sequence || 0);
|
|
1140
|
+
const causalStop = finalizing
|
|
1141
|
+
? mutateSessionRegistry(vaultBase, (registry) => {
|
|
1142
|
+
const activationId = resolveStopActivation(registry, {
|
|
1143
|
+
session_id: sessionId,
|
|
1144
|
+
activation_id: input.activation_id || input.activationId || '',
|
|
1145
|
+
transcript_id: identity.transcriptId,
|
|
1146
|
+
transcript_path: identity.transcriptPath || transcriptPath,
|
|
1147
|
+
});
|
|
1148
|
+
const cas = applyStopActivation(registry, {
|
|
1149
|
+
session_id: sessionId,
|
|
1150
|
+
activation_id: activationId,
|
|
1151
|
+
turn_sequence: stopTurnSequence,
|
|
1152
|
+
ended_at: endedAt,
|
|
1153
|
+
});
|
|
1154
|
+
const activation = cas.registry.sessions[sessionId]?.activations?.[activationId] || null;
|
|
1155
|
+
if (cas.canPromoteMemory) {
|
|
1156
|
+
registry.version = cas.registry.version;
|
|
1157
|
+
registry.sessions = cas.registry.sessions;
|
|
1158
|
+
registry.sessions[sessionId] = {
|
|
1159
|
+
...registry.sessions[sessionId],
|
|
1160
|
+
session_file: sessionRel,
|
|
1161
|
+
last_turn_id: turnId,
|
|
1162
|
+
transcript_path: transcriptPath,
|
|
1163
|
+
transcript_id: identity.transcriptId,
|
|
1164
|
+
provider: identity.provider,
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
return {
|
|
1168
|
+
activationId,
|
|
1169
|
+
activation,
|
|
1170
|
+
stopDisposition: cas.stopDisposition,
|
|
1171
|
+
canPromoteMemory: cas.canPromoteMemory,
|
|
1172
|
+
};
|
|
1173
|
+
})
|
|
1174
|
+
: null;
|
|
1175
|
+
if (causalStop && !causalStop.canPromoteMemory) {
|
|
1176
|
+
const message = `wendkeep: Stop ${causalStop.stopDisposition}; uma activation mais nova foi preservada e a memória não foi promovida.`;
|
|
1177
|
+
process.stderr.write(`[wendkeep] ${message}\n`);
|
|
1178
|
+
writeHookOutput({ systemMessage: message });
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1088
1181
|
const logged = insertIteration(sessionPath, buildIterationBlock(tx, input), turnId, tx);
|
|
1089
1182
|
|
|
1090
1183
|
try {
|
|
@@ -1099,7 +1192,7 @@ function main() {
|
|
|
1099
1192
|
process.stderr.write(`[wendkeep] Token usage falhou: ${error.message}\n`);
|
|
1100
1193
|
}
|
|
1101
1194
|
|
|
1102
|
-
if (!
|
|
1195
|
+
if (!finalizing) {
|
|
1103
1196
|
writeControl(vaultBase, {
|
|
1104
1197
|
...control,
|
|
1105
1198
|
status: 'active',
|
|
@@ -1125,8 +1218,6 @@ function main() {
|
|
|
1125
1218
|
return;
|
|
1126
1219
|
}
|
|
1127
1220
|
|
|
1128
|
-
const now = new Date();
|
|
1129
|
-
const endedAt = formatLocalIso(now);
|
|
1130
1221
|
const created = mergeCreatedNotes(
|
|
1131
1222
|
createLinkedNotes(vaultBase, formatDate(now), sessionRel, tx),
|
|
1132
1223
|
findLinkedDerivedNotes(vaultBase, sessionRel),
|
|
@@ -1156,15 +1247,40 @@ function main() {
|
|
|
1156
1247
|
session_id: sessionId,
|
|
1157
1248
|
last_logged_turn_id: turnId,
|
|
1158
1249
|
});
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1250
|
+
|
|
1251
|
+
let projectId = '';
|
|
1252
|
+
try {
|
|
1253
|
+
projectId = JSON.parse(readFileSync(join(vaultBase, '.brain', 'PROJECT.json'), 'utf8')).projectId || '';
|
|
1254
|
+
} catch { /* store validator reports a degraded handoff below */ }
|
|
1255
|
+
const finalSummary = sessionFinalSummary(tx);
|
|
1256
|
+
const memoryEvidence = collectLifecycleEvidence(vaultBase, {
|
|
1257
|
+
changeSlug: entry.change_slug,
|
|
1258
|
+
summary: finalSummary,
|
|
1259
|
+
noteRel: sessionRel,
|
|
1260
|
+
});
|
|
1261
|
+
const memoryResult = commitSessionMemory(vaultBase, {
|
|
1262
|
+
projectId,
|
|
1263
|
+
identity,
|
|
1264
|
+
activation: {
|
|
1265
|
+
id: causalStop.activationId,
|
|
1266
|
+
epoch: Number(causalStop.activation?.epoch || entry.activation_epoch || 0),
|
|
1267
|
+
},
|
|
1268
|
+
turn: { id: turnId, sequence: stopTurnSequence },
|
|
1269
|
+
noteRel: sessionRel,
|
|
1270
|
+
observedAt: new Date().toISOString(),
|
|
1271
|
+
summary: finalSummary,
|
|
1272
|
+
evidence: memoryEvidence,
|
|
1273
|
+
});
|
|
1274
|
+
mutateSessionRegistry(vaultBase, (registry) => {
|
|
1275
|
+
const current = registry.sessions[sessionId];
|
|
1276
|
+
if (!current) return null;
|
|
1277
|
+
registry.sessions[sessionId] = {
|
|
1278
|
+
...current,
|
|
1279
|
+
memory_status: memoryResult.status,
|
|
1280
|
+
memory_activation_id: causalStop.activationId,
|
|
1281
|
+
...(memoryResult.checkpoint ? { memory_checkpoint: memoryResult.checkpoint } : {}),
|
|
1282
|
+
};
|
|
1283
|
+
return null;
|
|
1168
1284
|
});
|
|
1169
1285
|
|
|
1170
1286
|
// Reconstrói índice (camada fria) + digest (camada quente) ao finalizar. Nunca derruba o Stop.
|
|
@@ -1179,7 +1295,9 @@ function main() {
|
|
|
1179
1295
|
try { pruneChangeSentinels(vaultBase); } catch { /* bônus */ }
|
|
1180
1296
|
|
|
1181
1297
|
pingObsidianVault(input.obsidian_api_key);
|
|
1182
|
-
writeHookOutput(
|
|
1298
|
+
writeHookOutput(memoryResult.status === 'degraded'
|
|
1299
|
+
? { systemMessage: `wendkeep: sessão salva; memória compartilhada degradada (${memoryResult.error}). Outbox preservada para replay.` }
|
|
1300
|
+
: {});
|
|
1183
1301
|
}
|
|
1184
1302
|
|
|
1185
1303
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|