wendkeep 0.68.1 → 0.68.5
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 +51 -0
- package/README.en.md +14 -6
- package/README.md +14 -6
- package/docs/en/commands/changes-and-verification.md +12 -0
- package/docs/en/commands/memory.md +3 -1
- package/docs/en/commands/operating-profiles.md +11 -0
- package/docs/en/commands/sessions-and-import.md +15 -0
- package/docs/pt-BR/commands/changes-and-verification.md +12 -0
- package/docs/pt-BR/commands/memory.md +3 -1
- package/docs/pt-BR/commands/operating-profiles.md +11 -0
- package/docs/pt-BR/commands/sessions-and-import.md +15 -0
- package/hooks/brain-core.mjs +159 -159
- package/hooks/brain-recall.mjs +32 -32
- package/hooks/brain-reindex.mjs +13 -13
- package/hooks/change-guard.mjs +142 -103
- package/hooks/git-snapshot.mjs +25 -6
- package/hooks/obsidian-common.mjs +40 -0
- package/hooks/project-scope.mjs +435 -0
- package/hooks/session-backfill.mjs +1 -1
- package/hooks/session-ensure.mjs +23 -4
- package/hooks/session-iteration-outcome.mjs +143 -0
- package/hooks/session-start.mjs +15 -0
- package/hooks/session-stop.mjs +263 -42
- package/hooks/token-usage.mjs +13 -11
- package/package.json +2 -2
- package/packages/integrations/src/host-hooks.mjs +7 -7
- package/packages/integrations/src/prompt-content.mjs +12 -0
- package/packages/integrations/src/transcripts.mjs +37 -8
- package/packages/vault/src/memory-store.mjs +63 -1
- package/src/init.mjs +3 -0
- package/src/memory.mjs +10 -3
package/hooks/session-start.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
readControl,
|
|
18
18
|
readHookInput,
|
|
19
19
|
readSessionRegistry,
|
|
20
|
+
resolveVault,
|
|
20
21
|
sessionFileName,
|
|
21
22
|
sessionFolderRel,
|
|
22
23
|
sessionSummaryFromInput,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
yamlQuote,
|
|
33
34
|
} from './obsidian-common.mjs';
|
|
34
35
|
import { resolveSessionIdentity } from './session-identity.mjs';
|
|
36
|
+
import { captureProjectScope, projectScopePatch } from './project-scope.mjs';
|
|
35
37
|
|
|
36
38
|
export function buildSessionContent({ relPath, now, summary = 'session', provider: providerId, sessionId = '' }) {
|
|
37
39
|
const date = formatDate(now);
|
|
@@ -153,6 +155,7 @@ function buildAdditionalContext({ relPath, startedAt, vaultBase }) {
|
|
|
153
155
|
function main() {
|
|
154
156
|
const input = readHookInput();
|
|
155
157
|
const vaultBase = getVaultBase(input);
|
|
158
|
+
const projectResolution = resolveVault(input);
|
|
156
159
|
warnIfDefaultVault(input);
|
|
157
160
|
const now = new Date();
|
|
158
161
|
const provider = providerMeta();
|
|
@@ -169,11 +172,23 @@ function main() {
|
|
|
169
172
|
}
|
|
170
173
|
const sessionId = identity.canonicalConversationId;
|
|
171
174
|
const transcriptPath = identity.transcriptPath;
|
|
175
|
+
const initialScope = captureProjectScope({
|
|
176
|
+
input,
|
|
177
|
+
projectRoot: projectResolution.projectRoot,
|
|
178
|
+
projectId: projectResolution.projectId,
|
|
179
|
+
provider: provider.id,
|
|
180
|
+
sessionId,
|
|
181
|
+
});
|
|
182
|
+
const scopePatchFor = (canonicalSessionId) => projectScopePatch(
|
|
183
|
+
readSessionRegistry(vaultBase).sessions?.[canonicalSessionId]?.project_scope,
|
|
184
|
+
{ ...initialScope, sessionId: canonicalSessionId },
|
|
185
|
+
);
|
|
172
186
|
const control = readControl(vaultBase);
|
|
173
187
|
const activationId = input.activation_id || input.activationId || randomUUID();
|
|
174
188
|
const activationStartedAt = formatLocalIso(now);
|
|
175
189
|
const registerActivation = (canonicalSessionId, patch) => upsertSessionRegistry(vaultBase, canonicalSessionId, {
|
|
176
190
|
...patch,
|
|
191
|
+
...scopePatchFor(canonicalSessionId),
|
|
177
192
|
activation_id: activationId,
|
|
178
193
|
activation_started_at: activationStartedAt,
|
|
179
194
|
last_turn_sequence: 0,
|
package/hooks/session-stop.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
readObservabilityStore,
|
|
17
17
|
} from './session-observability-store.mjs';
|
|
18
18
|
import { mutateSessionNote } from './session-note-io.mjs';
|
|
19
|
+
import { appendIterationOutcome } from './session-iteration-outcome.mjs';
|
|
19
20
|
import { applyDerivedSections, provenanceSessions } from './derived-sections.mjs';
|
|
20
21
|
import { buildSessionMemoryEvents, collectLifecycleEvidence } from './memory-handoff.mjs';
|
|
21
22
|
import { enqueueMemoryEvent, projectMemoryOutbox } from './memory-store.mjs';
|
|
@@ -33,7 +34,15 @@ import {
|
|
|
33
34
|
parseTranscriptContent,
|
|
34
35
|
resolveTurnIdentity,
|
|
35
36
|
} from '../packages/integrations/src/transcripts.mjs';
|
|
36
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
isSyntheticTranscriptText,
|
|
39
|
+
sanitizeAssistantMessage,
|
|
40
|
+
} from '../packages/integrations/src/prompt-content.mjs';
|
|
41
|
+
|
|
42
|
+
const UNRESOLVED_SESSION_ID = 'unresolved';
|
|
43
|
+
const UNRESOLVED_TURN_ID = 'unresolved-turn';
|
|
44
|
+
const ABORTED_TURN_NOTICE = 'wendkeep: Stop ignorado; o turno foi abortado no transcript.';
|
|
45
|
+
|
|
37
46
|
export { resolveTurnIdentity };
|
|
38
47
|
import {
|
|
39
48
|
ensureDir,
|
|
@@ -43,7 +52,6 @@ import {
|
|
|
43
52
|
formatLocalIso,
|
|
44
53
|
getNextAdrNumber,
|
|
45
54
|
getVaultBase,
|
|
46
|
-
isBootstrapPrompt,
|
|
47
55
|
warnIfDefaultVault,
|
|
48
56
|
listMarkdownFiles,
|
|
49
57
|
readControl,
|
|
@@ -61,28 +69,15 @@ import {
|
|
|
61
69
|
hasTurnMarker,
|
|
62
70
|
normalizeTurnMarkers,
|
|
63
71
|
mutateSessionRegistry,
|
|
72
|
+
closeSessionActivation,
|
|
64
73
|
resolveRegisteredTurnSequence,
|
|
65
74
|
resolveStopActivation,
|
|
66
75
|
applyStopActivation,
|
|
67
76
|
} from './obsidian-common.mjs';
|
|
68
77
|
|
|
69
|
-
// Tags injetadas pelo harness (não são fala humana): notificações de task,
|
|
70
|
-
// reminders do sistema, stdout de comando local, wrappers de slash-command e
|
|
71
|
-
// contexto da IDE. Nunca devem virar título/Pedido/Usuário de iteração no Vault.
|
|
72
|
-
const SYNTHETIC_EVENT_TAG = /^<\/?(?:task-notification|system-reminder|local-command-stdout|local-command-stderr|command-message|command-name|command-args|user-prompt-submit-hook|ide_selection|ide_opened_file|environment_context)\b/i;
|
|
73
|
-
|
|
74
78
|
function shouldIgnoreUserText(text) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// prefix list and the copies drifted — <recommended_plugins> made it into one and not the
|
|
78
|
-
// other, so the title came out clean while the same block still showed as "Usuário" in the
|
|
79
|
-
// conversation context. One filter, one place to add the next injected block.
|
|
80
|
-
return SYNTHETIC_EVENT_TAG.test(trimmed)
|
|
81
|
-
|| isBootstrapPrompt(trimmed)
|
|
82
|
-
// Harness utility meta-prompts (title generation, classifiers) — not real user turns; they
|
|
83
|
-
// were leaking into note titles/summaries on import.
|
|
84
|
-
|| /^Generate a concise( UI)? title/i.test(trimmed)
|
|
85
|
-
|| /^You are a helpful assistant\. You will be presented with a user prompt/i.test(trimmed);
|
|
79
|
+
// Tags injetadas pelo harness não são fala humana e nunca viram título/Pedido/Usuário.
|
|
80
|
+
return isSyntheticTranscriptText(text);
|
|
86
81
|
}
|
|
87
82
|
|
|
88
83
|
function addUnique(list, value) {
|
|
@@ -486,11 +481,13 @@ function relocateOrphanIterations(content) {
|
|
|
486
481
|
|
|
487
482
|
export function insertIteration(sessionPath, block, turnId, tx, vaultBase = '') {
|
|
488
483
|
let inserted = false;
|
|
484
|
+
let duplicate = false;
|
|
489
485
|
// Sob lock: outro hook (subagent-stop) pode estar reescrevendo a mesma nota agora.
|
|
490
|
-
mutateSessionNote(sessionPath, (original) => {
|
|
486
|
+
const mutation = mutateSessionNote(sessionPath, (original) => {
|
|
491
487
|
// Self-heal: migrate any legacy `codex-turn` markers to the neutral name on this write.
|
|
492
488
|
let content = normalizeTurnMarkers(original);
|
|
493
489
|
if (hasTurnMarker(content, turnId)) {
|
|
490
|
+
duplicate = true;
|
|
494
491
|
// Turno já registrado: ainda assim repara órfãos e seções dedicadas.
|
|
495
492
|
return applyDedicatedSections(relocateOrphanIterations(content), tx);
|
|
496
493
|
}
|
|
@@ -499,7 +496,24 @@ export function insertIteration(sessionPath, block, turnId, tx, vaultBase = '')
|
|
|
499
496
|
inserted = true;
|
|
500
497
|
return applyDedicatedSections(content, tx);
|
|
501
498
|
}, { vaultBase });
|
|
502
|
-
|
|
499
|
+
const result = duplicate
|
|
500
|
+
? 'duplicate'
|
|
501
|
+
: mutation.written && inserted
|
|
502
|
+
? 'inserted'
|
|
503
|
+
: mutation.reason === 'busy'
|
|
504
|
+
? 'busy'
|
|
505
|
+
: 'failed';
|
|
506
|
+
return {
|
|
507
|
+
inserted: result === 'inserted',
|
|
508
|
+
confirmed: result === 'inserted' || result === 'duplicate',
|
|
509
|
+
written: Boolean(mutation.written),
|
|
510
|
+
result,
|
|
511
|
+
reason: mutation.reason || 'unknown',
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function confirmedLoggedTurnId(currentTurnId, candidateTurnId, projection) {
|
|
516
|
+
return projection?.confirmed ? candidateTurnId : currentTurnId;
|
|
503
517
|
}
|
|
504
518
|
|
|
505
519
|
function shouldFinalizeSession() {
|
|
@@ -993,10 +1007,14 @@ export async function refreshStopObservability({
|
|
|
993
1007
|
readStore = readObservabilityStore,
|
|
994
1008
|
resolveRoots = resolveObservabilityRoots,
|
|
995
1009
|
materialize = materializeStopObservability,
|
|
1010
|
+
returnDetails = false,
|
|
996
1011
|
} = {}) {
|
|
997
|
-
|
|
1012
|
+
const outcome = (ok, status, reason = '') => (
|
|
1013
|
+
returnDetails ? { ok, status, reason } : ok
|
|
1014
|
+
);
|
|
1015
|
+
if (causalStop && !causalStop.canPromoteMemory) return outcome(false, 'skipped', 'causal-stop-not-promotable');
|
|
998
1016
|
const deadlineAt = hookStartedAt + STOP_OBSERVABILITY_DEADLINE_MS;
|
|
999
|
-
if (now() >= deadlineAt) return false;
|
|
1017
|
+
if (now() >= deadlineAt) return outcome(false, 'stale', 'deadline-before-refresh');
|
|
1000
1018
|
|
|
1001
1019
|
const expected = expectedStopCausalSnapshot(entry, causalStop, turnSequence);
|
|
1002
1020
|
const fresh = resolveEntry(vaultBase, input, entry?.provider);
|
|
@@ -1004,18 +1022,24 @@ export async function refreshStopObservability({
|
|
|
1004
1022
|
|| fresh.identity.canonicalConversationId !== sessionId
|
|
1005
1023
|
|| !fresh.entry?.session_file
|
|
1006
1024
|
|| fresh.entry.session_file !== entry?.session_file
|
|
1007
|
-
|| !sameStopCausalSnapshot(expected, stopEntryCausalSnapshot(fresh.entry)))
|
|
1025
|
+
|| !sameStopCausalSnapshot(expected, stopEntryCausalSnapshot(fresh.entry))) {
|
|
1026
|
+
return outcome(false, 'stale', 'causal-snapshot-changed');
|
|
1027
|
+
}
|
|
1008
1028
|
|
|
1009
1029
|
const runtime = readStore(vaultBase, sessionId);
|
|
1010
1030
|
const signalSequence = Number(runtime?.observability_signal_sequence || 0);
|
|
1011
1031
|
if (expectedSignalSequence !== undefined
|
|
1012
|
-
&& signalSequence !== Number(expectedSignalSequence))
|
|
1013
|
-
|
|
1032
|
+
&& signalSequence !== Number(expectedSignalSequence)) {
|
|
1033
|
+
return outcome(false, 'stale', 'signal-sequence-changed');
|
|
1034
|
+
}
|
|
1035
|
+
if (now() >= deadlineAt) return outcome(false, 'stale', 'deadline-before-materialize');
|
|
1014
1036
|
|
|
1015
1037
|
const roots = fresh.identity.provider === 'codex'
|
|
1016
1038
|
? resolveRoots(fresh.entry)
|
|
1017
1039
|
: stopClaudeRoots(fresh.entry);
|
|
1018
|
-
if (roots?.state !== 'complete' || !roots.rootPaths?.length)
|
|
1040
|
+
if (roots?.state !== 'complete' || !roots.rootPaths?.length) {
|
|
1041
|
+
return outcome(false, 'missing', 'observability-roots-missing');
|
|
1042
|
+
}
|
|
1019
1043
|
|
|
1020
1044
|
const frontier = {
|
|
1021
1045
|
canonical_session_id: sessionId,
|
|
@@ -1106,7 +1130,64 @@ export async function refreshStopObservability({
|
|
|
1106
1130
|
withPublicationGuard,
|
|
1107
1131
|
writeRegistryCheckpoint,
|
|
1108
1132
|
}));
|
|
1109
|
-
|
|
1133
|
+
const status = result?.status || (result?.ok === false ? 'failed' : 'published');
|
|
1134
|
+
return outcome(!['stale', 'conflict', 'degraded', 'missing'].includes(status), status);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
export function recordStopOutcome(vaultBase, {
|
|
1138
|
+
sessionId = UNRESOLVED_SESSION_ID,
|
|
1139
|
+
transcriptId = '',
|
|
1140
|
+
turnId = UNRESOLVED_TURN_ID,
|
|
1141
|
+
turnSequence = 0,
|
|
1142
|
+
hook = 'Stop',
|
|
1143
|
+
stage = 'iteration',
|
|
1144
|
+
result = 'failed',
|
|
1145
|
+
lockStatus = 'unknown',
|
|
1146
|
+
durationMs = 0,
|
|
1147
|
+
reason = '',
|
|
1148
|
+
} = {}) {
|
|
1149
|
+
try {
|
|
1150
|
+
return appendIterationOutcome(vaultBase, {
|
|
1151
|
+
session_id: sessionId,
|
|
1152
|
+
transcript_id: transcriptId,
|
|
1153
|
+
turn_id: turnId,
|
|
1154
|
+
turn_sequence: turnSequence,
|
|
1155
|
+
hook,
|
|
1156
|
+
stage,
|
|
1157
|
+
result,
|
|
1158
|
+
lock_status: lockStatus,
|
|
1159
|
+
duration_ms: durationMs,
|
|
1160
|
+
occurred_at: new Date().toISOString(),
|
|
1161
|
+
reason,
|
|
1162
|
+
});
|
|
1163
|
+
} catch {
|
|
1164
|
+
// O ledger é diagnóstico: um problema nele nunca deve bloquear a sessão nem publicar o erro.
|
|
1165
|
+
return { written: false, result: 'failed', reason: 'outcome-ledger-write-failed' };
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
export function finalizeSessionRegistry(vaultBase, {
|
|
1170
|
+
sessionId = '',
|
|
1171
|
+
activationId = '',
|
|
1172
|
+
turnId = '',
|
|
1173
|
+
endedAt = '',
|
|
1174
|
+
} = {}) {
|
|
1175
|
+
let disposition = 'ambiguous';
|
|
1176
|
+
mutateSessionRegistry(vaultBase, (registry) => {
|
|
1177
|
+
const closed = closeSessionActivation(registry, {
|
|
1178
|
+
session_id: sessionId,
|
|
1179
|
+
activation_id: activationId,
|
|
1180
|
+
turn_id: turnId,
|
|
1181
|
+
ended_at: endedAt,
|
|
1182
|
+
});
|
|
1183
|
+
disposition = closed.stopDisposition;
|
|
1184
|
+
if (closed.stopDisposition === 'finalized') {
|
|
1185
|
+
registry.version = closed.registry.version;
|
|
1186
|
+
registry.sessions = closed.registry.sessions;
|
|
1187
|
+
}
|
|
1188
|
+
return null;
|
|
1189
|
+
});
|
|
1190
|
+
return disposition;
|
|
1110
1191
|
}
|
|
1111
1192
|
|
|
1112
1193
|
export async function main({
|
|
@@ -1128,6 +1209,14 @@ export async function main({
|
|
|
1128
1209
|
const { identity, entry } = resolveSessionEntry(vaultBase, input);
|
|
1129
1210
|
if (identity.state !== 'resolved' || !entry?.session_file) {
|
|
1130
1211
|
const why = identity.diagnostics?.join('; ') || 'sessão não registrada';
|
|
1212
|
+
recordStopOutcome(vaultBase, {
|
|
1213
|
+
sessionId: identity.canonicalConversationId || input.session_id || 'unresolved',
|
|
1214
|
+
transcriptId: identity.transcriptId || input.transcript_id || '',
|
|
1215
|
+
turnId: input.turn_id || input.turnId || 'unresolved-turn',
|
|
1216
|
+
hook: input.hook_event_name || 'Stop',
|
|
1217
|
+
result: 'ambiguous',
|
|
1218
|
+
reason: `identity-unresolved: ${why}`,
|
|
1219
|
+
});
|
|
1131
1220
|
process.stderr.write(`[wendkeep] Stop sem identidade segura: ${why}\n`);
|
|
1132
1221
|
// stderr alone is a black hole here: Codex discards it, which is how an entire session of
|
|
1133
1222
|
// lost turns produced no signal at all. systemMessage is what the UI actually shows.
|
|
@@ -1141,6 +1230,14 @@ export async function main({
|
|
|
1141
1230
|
// no global (contaminaria nota alheia): pulamos e o backfill recupera depois.
|
|
1142
1231
|
const sessionRel = entry.session_file;
|
|
1143
1232
|
if (!sessionRel) {
|
|
1233
|
+
recordStopOutcome(vaultBase, {
|
|
1234
|
+
sessionId: identity.canonicalConversationId,
|
|
1235
|
+
transcriptId: identity.transcriptId,
|
|
1236
|
+
turnId: input.turn_id || input.turnId || 'unresolved-turn',
|
|
1237
|
+
hook: input.hook_event_name || 'Stop',
|
|
1238
|
+
result: 'ambiguous',
|
|
1239
|
+
reason: 'session-note-not-registered',
|
|
1240
|
+
});
|
|
1144
1241
|
writeHookOutput({});
|
|
1145
1242
|
return;
|
|
1146
1243
|
}
|
|
@@ -1149,6 +1246,14 @@ export async function main({
|
|
|
1149
1246
|
expectedType: 'file', label: 'nota de sessão do Stop',
|
|
1150
1247
|
});
|
|
1151
1248
|
if (!checkedSession.exists) {
|
|
1249
|
+
recordStopOutcome(vaultBase, {
|
|
1250
|
+
sessionId: identity.canonicalConversationId,
|
|
1251
|
+
transcriptId: identity.transcriptId,
|
|
1252
|
+
turnId: input.turn_id || input.turnId || 'unresolved-turn',
|
|
1253
|
+
hook: input.hook_event_name || 'Stop',
|
|
1254
|
+
result: 'failed',
|
|
1255
|
+
reason: 'session-note-missing-on-disk',
|
|
1256
|
+
});
|
|
1152
1257
|
writeHookOutput({});
|
|
1153
1258
|
return;
|
|
1154
1259
|
}
|
|
@@ -1173,12 +1278,36 @@ export async function main({
|
|
|
1173
1278
|
observedAt: new Date(0).toISOString(),
|
|
1174
1279
|
});
|
|
1175
1280
|
}
|
|
1281
|
+
recordStopOutcome(vaultBase, {
|
|
1282
|
+
sessionId,
|
|
1283
|
+
transcriptId: identity.transcriptId,
|
|
1284
|
+
turnId: requestedTurnId || 'unresolved-turn',
|
|
1285
|
+
turnSequence: Number(entry.last_turn_sequence || 0),
|
|
1286
|
+
hook: input.hook_event_name || 'Stop',
|
|
1287
|
+
result: 'ambiguous',
|
|
1288
|
+
reason: 'turn-not-proven-by-transcript',
|
|
1289
|
+
});
|
|
1176
1290
|
const message = 'wendkeep: Stop ambiguous; o turno solicitado não foi provado pelo transcript.';
|
|
1177
1291
|
process.stderr.write(`[wendkeep] ${message}\n`);
|
|
1178
1292
|
writeHookOutput({ systemMessage: message });
|
|
1179
1293
|
return;
|
|
1180
1294
|
}
|
|
1181
1295
|
const turnId = turnIdentity.id;
|
|
1296
|
+
const parsedTurn = tx.turns.find((turn) => turn.turnId === turnId);
|
|
1297
|
+
if (parsedTurn?.status === 'aborted') {
|
|
1298
|
+
recordStopOutcome(vaultBase, {
|
|
1299
|
+
sessionId,
|
|
1300
|
+
transcriptId: identity.transcriptId,
|
|
1301
|
+
turnId,
|
|
1302
|
+
turnSequence: turnIdentity.order,
|
|
1303
|
+
hook: input.hook_event_name || 'Stop',
|
|
1304
|
+
result: 'aborted',
|
|
1305
|
+
reason: 'transcript-turn-aborted',
|
|
1306
|
+
});
|
|
1307
|
+
process.stderr.write(`[wendkeep] ${ABORTED_TURN_NOTICE}\n`);
|
|
1308
|
+
writeHookOutput({ systemMessage: ABORTED_TURN_NOTICE });
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1182
1311
|
const now = finalizing ? new Date() : null;
|
|
1183
1312
|
const endedAt = finalizing ? formatLocalIso(now) : '';
|
|
1184
1313
|
const causalStop = finalizing
|
|
@@ -1269,6 +1398,17 @@ export async function main({
|
|
|
1269
1398
|
}
|
|
1270
1399
|
if (shouldAbortStopAfterStaging(causalStop, memoryAttempt)) {
|
|
1271
1400
|
const disposition = memoryAttempt?.disposition || causalStop?.stopDisposition || 'ambiguous';
|
|
1401
|
+
recordStopOutcome(vaultBase, {
|
|
1402
|
+
sessionId,
|
|
1403
|
+
transcriptId: identity.transcriptId,
|
|
1404
|
+
turnId,
|
|
1405
|
+
turnSequence: stopTurnSequence,
|
|
1406
|
+
hook: input.hook_event_name || 'Stop',
|
|
1407
|
+
result: disposition === 'duplicate' && memoryAttempt?.state === 'duplicate'
|
|
1408
|
+
? 'duplicate'
|
|
1409
|
+
: 'skipped',
|
|
1410
|
+
reason: `staging-${disposition}`,
|
|
1411
|
+
});
|
|
1272
1412
|
if (disposition === 'duplicate' && memoryAttempt?.state === 'duplicate') {
|
|
1273
1413
|
writeHookOutput({});
|
|
1274
1414
|
return;
|
|
@@ -1278,7 +1418,19 @@ export async function main({
|
|
|
1278
1418
|
writeHookOutput({ systemMessage: message });
|
|
1279
1419
|
return;
|
|
1280
1420
|
}
|
|
1421
|
+
const iterationStartedAt = clock();
|
|
1281
1422
|
const logged = insertIteration(sessionPath, buildIterationBlock(tx, input), turnId, tx, vaultBase);
|
|
1423
|
+
recordStopOutcome(vaultBase, {
|
|
1424
|
+
sessionId,
|
|
1425
|
+
transcriptId: identity.transcriptId,
|
|
1426
|
+
turnId,
|
|
1427
|
+
turnSequence: stopTurnSequence,
|
|
1428
|
+
hook: input.hook_event_name || 'Stop',
|
|
1429
|
+
result: logged.result,
|
|
1430
|
+
lockStatus: logged.result === 'busy' ? 'busy' : logged.confirmed ? 'acquired' : 'unknown',
|
|
1431
|
+
durationMs: Math.max(0, clock() - iterationStartedAt),
|
|
1432
|
+
reason: `note-${logged.reason}`,
|
|
1433
|
+
});
|
|
1282
1434
|
|
|
1283
1435
|
try {
|
|
1284
1436
|
applyLinearLinks(sessionPath, tx, vaultBase, sessionRel);
|
|
@@ -1287,7 +1439,7 @@ export async function main({
|
|
|
1287
1439
|
}
|
|
1288
1440
|
|
|
1289
1441
|
try {
|
|
1290
|
-
await refreshObservability({
|
|
1442
|
+
const refreshed = await refreshObservability({
|
|
1291
1443
|
vaultBase,
|
|
1292
1444
|
input,
|
|
1293
1445
|
sessionPath,
|
|
@@ -1298,9 +1450,33 @@ export async function main({
|
|
|
1298
1450
|
hookStartedAt,
|
|
1299
1451
|
}, {
|
|
1300
1452
|
now: clock,
|
|
1453
|
+
returnDetails: true,
|
|
1454
|
+
});
|
|
1455
|
+
const observability = typeof refreshed === 'boolean'
|
|
1456
|
+
? { status: refreshed ? 'published' : 'failed', reason: 'legacy-boolean-result' }
|
|
1457
|
+
: (refreshed || { status: 'missing', reason: 'empty-result' });
|
|
1458
|
+
recordStopOutcome(vaultBase, {
|
|
1459
|
+
sessionId,
|
|
1460
|
+
transcriptId: identity.transcriptId,
|
|
1461
|
+
turnId,
|
|
1462
|
+
turnSequence: stopTurnSequence,
|
|
1463
|
+
hook: input.hook_event_name || 'Stop',
|
|
1464
|
+
stage: 'observability',
|
|
1465
|
+
result: observability.status,
|
|
1466
|
+
reason: observability.reason || 'observability-refresh',
|
|
1301
1467
|
});
|
|
1302
1468
|
} catch (error) {
|
|
1303
|
-
|
|
1469
|
+
recordStopOutcome(vaultBase, {
|
|
1470
|
+
sessionId,
|
|
1471
|
+
transcriptId: identity.transcriptId,
|
|
1472
|
+
turnId,
|
|
1473
|
+
turnSequence: stopTurnSequence,
|
|
1474
|
+
hook: input.hook_event_name || 'Stop',
|
|
1475
|
+
stage: 'observability',
|
|
1476
|
+
result: 'failed',
|
|
1477
|
+
reason: 'observability-refresh-threw',
|
|
1478
|
+
});
|
|
1479
|
+
process.stderr.write(`[wendkeep] Observabilidade falhou: ${error.message}\n`);
|
|
1304
1480
|
}
|
|
1305
1481
|
|
|
1306
1482
|
if (!finalizing) {
|
|
@@ -1310,7 +1486,7 @@ export async function main({
|
|
|
1310
1486
|
session_file: sessionRel,
|
|
1311
1487
|
last_session_file: control.last_session_file || sessionRel,
|
|
1312
1488
|
session_id: sessionId,
|
|
1313
|
-
last_logged_turn_id:
|
|
1489
|
+
last_logged_turn_id: confirmedLoggedTurnId(control.last_logged_turn_id, turnId, logged),
|
|
1314
1490
|
});
|
|
1315
1491
|
upsertSessionRegistry(vaultBase, sessionId, {
|
|
1316
1492
|
session_file: sessionRel,
|
|
@@ -1319,7 +1495,7 @@ export async function main({
|
|
|
1319
1495
|
// (definido no SessionStart). Usar control.started_at contaminava com o
|
|
1320
1496
|
// started_at de sessões concorrentes que sobrescrevem o ponteiro global.
|
|
1321
1497
|
ended_at: '',
|
|
1322
|
-
last_turn_id:
|
|
1498
|
+
last_turn_id: confirmedLoggedTurnId(control.last_logged_turn_id, turnId, logged),
|
|
1323
1499
|
transcript_path: transcriptPath,
|
|
1324
1500
|
transcript_id: identity.transcriptId,
|
|
1325
1501
|
provider: identity.provider,
|
|
@@ -1333,7 +1509,6 @@ export async function main({
|
|
|
1333
1509
|
createLinkedNotes(vaultBase, formatDate(now), sessionRel, tx),
|
|
1334
1510
|
findLinkedDerivedNotes(vaultBase, sessionRel),
|
|
1335
1511
|
);
|
|
1336
|
-
finalizeSessionFile(sessionPath, tx, created, endedAt, vaultBase);
|
|
1337
1512
|
// Link durável sessão↔change: uma seção "Mudanças" ANTES de `## Encerramento`. O append antigo
|
|
1338
1513
|
// (após o Encerramento) era apagado a cada reopen por stripClosingSection, perdendo a aresta do
|
|
1339
1514
|
// grafo quando a change fechava antes do turno seguinte. Aqui sobrevive ao reopen e acumula toda
|
|
@@ -1349,16 +1524,6 @@ export async function main({
|
|
|
1349
1524
|
), { vaultBase });
|
|
1350
1525
|
}
|
|
1351
1526
|
} catch { /* nunca derruba o Stop */ }
|
|
1352
|
-
writeControl(vaultBase, {
|
|
1353
|
-
status: 'inactive',
|
|
1354
|
-
session_file: '',
|
|
1355
|
-
last_session_file: sessionRel,
|
|
1356
|
-
started_at: control.started_at,
|
|
1357
|
-
ended_at: endedAt,
|
|
1358
|
-
session_id: sessionId,
|
|
1359
|
-
last_logged_turn_id: turnId,
|
|
1360
|
-
});
|
|
1361
|
-
|
|
1362
1527
|
const memoryResult = projectStopMemoryAttempt(vaultBase, memoryAttempt);
|
|
1363
1528
|
if (memoryResult.status === 'legacy') {
|
|
1364
1529
|
mutateSessionRegistry(vaultBase, (registry) => {
|
|
@@ -1378,6 +1543,62 @@ export async function main({
|
|
|
1378
1543
|
recordStopMemoryOutcome(vaultBase, memoryAttempt, memoryResult);
|
|
1379
1544
|
}
|
|
1380
1545
|
|
|
1546
|
+
// A memória compartilhada é um consumidor causal do fechamento. Se o projetor estiver
|
|
1547
|
+
// ocupado ou falhar, o outbox é a autoridade de retry e a sessão não pode ser marcada como
|
|
1548
|
+
// encerrada: fechar aqui criaria uma sessão `done` sem a publicação que o fechamento promete.
|
|
1549
|
+
if (memoryResult.status === 'degraded') {
|
|
1550
|
+
upsertSessionRegistry(vaultBase, sessionId, {
|
|
1551
|
+
session_file: sessionRel,
|
|
1552
|
+
status: 'active',
|
|
1553
|
+
ended_at: '',
|
|
1554
|
+
last_turn_id: confirmedLoggedTurnId(control.last_logged_turn_id, turnId, logged),
|
|
1555
|
+
transcript_path: transcriptPath,
|
|
1556
|
+
transcript_id: identity.transcriptId,
|
|
1557
|
+
provider: identity.provider,
|
|
1558
|
+
});
|
|
1559
|
+
writeControl(vaultBase, {
|
|
1560
|
+
...control,
|
|
1561
|
+
status: 'active',
|
|
1562
|
+
session_file: sessionRel,
|
|
1563
|
+
last_session_file: sessionRel,
|
|
1564
|
+
session_id: sessionId,
|
|
1565
|
+
ended_at: '',
|
|
1566
|
+
last_logged_turn_id: confirmedLoggedTurnId(control.last_logged_turn_id, turnId, logged),
|
|
1567
|
+
});
|
|
1568
|
+
pingObsidianVault(input.obsidian_api_key);
|
|
1569
|
+
writeHookOutput({
|
|
1570
|
+
systemMessage: 'wendkeep: memória compartilhada degradada; outbox preservado para retry e sessão mantida ativa.',
|
|
1571
|
+
});
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
finalizeSessionFile(sessionPath, tx, created, endedAt, vaultBase);
|
|
1576
|
+
|
|
1577
|
+
// Só fecha a activation depois do último consumidor causal (memória compartilhada) ter lido
|
|
1578
|
+
// a activation ainda ativa. O registry fechado alimenta a visão CURRENT_SESSION abaixo.
|
|
1579
|
+
let registryFinalization = 'ambiguous';
|
|
1580
|
+
try {
|
|
1581
|
+
registryFinalization = finalizeSessionRegistry(vaultBase, {
|
|
1582
|
+
sessionId,
|
|
1583
|
+
activationId: causalStop?.activationId || '',
|
|
1584
|
+
turnId,
|
|
1585
|
+
endedAt,
|
|
1586
|
+
});
|
|
1587
|
+
} catch (error) {
|
|
1588
|
+
process.stderr.write(`[wendkeep] fechamento do registry falhou: ${error.message}\n`);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
const registryClosed = registryFinalization === 'finalized' || registryFinalization === 'duplicate';
|
|
1592
|
+
writeControl(vaultBase, {
|
|
1593
|
+
status: registryClosed ? 'inactive' : 'active',
|
|
1594
|
+
session_file: registryClosed ? '' : sessionRel,
|
|
1595
|
+
last_session_file: sessionRel,
|
|
1596
|
+
started_at: control.started_at,
|
|
1597
|
+
ended_at: registryClosed ? endedAt : '',
|
|
1598
|
+
session_id: sessionId,
|
|
1599
|
+
last_logged_turn_id: confirmedLoggedTurnId(control.last_logged_turn_id, turnId, logged),
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1381
1602
|
// Reconstrói índice (camada fria) + digest (camada quente) ao finalizar. Nunca derruba o Stop.
|
|
1382
1603
|
try {
|
|
1383
1604
|
const rows = buildBrainIndex(vaultBase);
|
package/hooks/token-usage.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
normalizeClaudeUsage,
|
|
15
15
|
normalizeCodexUsage,
|
|
16
16
|
} from '../packages/integrations/src/transcript-usage.mjs';
|
|
17
|
+
import { isSyntheticTranscriptText } from '../packages/integrations/src/prompt-content.mjs';
|
|
17
18
|
export {
|
|
18
19
|
addUsage,
|
|
19
20
|
emptyTokenUsage,
|
|
@@ -277,17 +278,7 @@ function extractTextContent(content) {
|
|
|
277
278
|
}
|
|
278
279
|
|
|
279
280
|
function shouldIgnoreUserText(text) {
|
|
280
|
-
return
|
|
281
|
-
|| text.startsWith('<environment_context>')
|
|
282
|
-
|| text.startsWith('<permissions instructions>')
|
|
283
|
-
|| text.startsWith('<system-reminder>')
|
|
284
|
-
|| text.startsWith('<local-command-caveat>')
|
|
285
|
-
|| text.startsWith('<command-name>')
|
|
286
|
-
|| text.startsWith('<ide_')
|
|
287
|
-
|| text.startsWith('## Memory')
|
|
288
|
-
|| text.includes('You are Codex, a coding agent')
|
|
289
|
-
|| /^Generate a concise( UI)? title/i.test(text)
|
|
290
|
-
|| /^You are a helpful assistant\. You will be presented with a user prompt/i.test(text);
|
|
281
|
+
return isSyntheticTranscriptText(text);
|
|
291
282
|
}
|
|
292
283
|
|
|
293
284
|
function emptyParseResult(transcriptPath) {
|
|
@@ -371,6 +362,17 @@ function parseCodexLines(lines, result) {
|
|
|
371
362
|
continue;
|
|
372
363
|
}
|
|
373
364
|
|
|
365
|
+
if (event.type === 'response_item' && payload.type === 'custom_tool_call') {
|
|
366
|
+
result.toolCalls += 1;
|
|
367
|
+
addUnique(result.tools, payload.name || payload.tool_name || payload.tool || 'custom_tool_call');
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (event.type === 'response_item' && payload.type === 'custom_tool_call_output') {
|
|
372
|
+
// The output closes the call; it is not a second tool invocation and never a prompt.
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
374
376
|
if (event.type === 'response_item' && payload.type === 'tool_search_call') {
|
|
375
377
|
result.toolCalls += 1;
|
|
376
378
|
addUnique(result.tools, 'tool_search');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.68.
|
|
3
|
+
"version": "0.68.5",
|
|
4
4
|
"description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"node": ">=18"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
|
-
"check": "node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
|
|
43
|
+
"check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
|
|
44
44
|
"test": "node --test --test-concurrency=2",
|
|
45
45
|
"release": "node scripts/release.mjs",
|
|
46
46
|
"release:dry": "node scripts/release.mjs --dry-run",
|
|
@@ -41,7 +41,7 @@ export function hookCommandLocalLegacy(name) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Hooks do lifecycle de change (0.31.0) — enforcement do loop a2. Nudges (contexto/aviso/
|
|
44
|
-
// cobrança/captura de plano) e gate (deny/ask no Bash). Separados em dois grupos para
|
|
44
|
+
// cobrança/captura de plano) e gate (deny/ask no Bash/PreToolUse). Separados em dois grupos para
|
|
45
45
|
// preservar a opção futura de gates opt-in; hoje o init wira TODOS por default.
|
|
46
46
|
// preferLocal: alta frequência → invocação node-direta quando houver instalação local.
|
|
47
47
|
export const CHANGE_NUDGE_HOOKS = [
|
|
@@ -54,9 +54,9 @@ export const CHANGE_NUDGE_HOOKS = [
|
|
|
54
54
|
];
|
|
55
55
|
|
|
56
56
|
export const CHANGE_GATE_HOOKS = [
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
{ event: 'PreToolUse', matcher: 'Bash', name: 'change-guard', timeout: 10, order: 10, preferLocal: true, statusMessage: 'wendkeep: change gate' },
|
|
57
|
+
// The adapter accepts Codex's object, raw-string and argv forms and fails closed when a
|
|
58
|
+
// mutable target cannot be proven. Keep the matcher narrow to the payloads covered by tests.
|
|
59
|
+
{ event: 'PreToolUse', matcher: 'Bash|exec_command|apply_patch|mcp__.*', name: 'change-guard', timeout: 10, order: 10, preferLocal: true, codex: true, statusMessage: 'wendkeep: change gate' },
|
|
60
60
|
];
|
|
61
61
|
|
|
62
62
|
// --- Codex projection ---------------------------------------------------------
|
|
@@ -65,9 +65,9 @@ export const CHANGE_GATE_HOOKS = [
|
|
|
65
65
|
// rest carry a `// codex:` comment above them saying why. Three deltas from Claude, each
|
|
66
66
|
// verified against codex-rs and each silent when wrong: the timeout key is `timeoutSec`
|
|
67
67
|
// (`timeout` is not a field and falls through to a 600s default), there is no
|
|
68
|
-
// ${CLAUDE_PROJECT_DIR} so `preferLocal` never applies, and matcher is only
|
|
69
|
-
//
|
|
70
|
-
export const CODEX_MATCHER_EVENTS = new Set(['SessionStart']);
|
|
68
|
+
// ${CLAUDE_PROJECT_DIR} so `preferLocal` never applies, and matcher is honoured only for
|
|
69
|
+
// events whose host contract is covered by tests.
|
|
70
|
+
export const CODEX_MATCHER_EVENTS = new Set(['SessionStart', 'PreToolUse']);
|
|
71
71
|
|
|
72
72
|
export function codexHookSpecs(specs) {
|
|
73
73
|
return specs.filter((h) => h.codex === true && !h.command);
|
|
@@ -8,6 +8,18 @@ export function isBootstrapPrompt(text = '') {
|
|
|
8
8
|
|| clean.startsWith('## Memory');
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
const SYNTHETIC_EVENT_TAG = /^<\/?(?:task-notification|system-reminder|local-command-stdout|local-command-stderr|local-command-caveat|command-message|command-name|command-args|user-prompt-submit-hook|subagent_notification|subagent-notification|ide_[A-Za-z0-9_-]+|environment_context)\b/i;
|
|
12
|
+
|
|
13
|
+
// Shared by transcript and token-usage parsers. These wrappers are harness metadata, not human
|
|
14
|
+
// prompts; keeping the filter here prevents the two import paths from drifting.
|
|
15
|
+
export function isSyntheticTranscriptText(text = '') {
|
|
16
|
+
const trimmed = String(text || '').trim();
|
|
17
|
+
return SYNTHETIC_EVENT_TAG.test(trimmed)
|
|
18
|
+
|| isBootstrapPrompt(trimmed)
|
|
19
|
+
|| /^Generate a concise( UI)? title/i.test(trimmed)
|
|
20
|
+
|| /^You are a helpful assistant\. You will be presented with a user prompt/i.test(trimmed);
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
export function redactSecrets(text) {
|
|
12
24
|
if (!text) return '';
|
|
13
25
|
return String(text)
|