wendkeep 0.37.0 → 0.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +2 -0
- package/README.pt-BR.md +2 -0
- package/bin/wendkeep.mjs +7 -1
- package/hooks/brain-inject.mjs +87 -80
- package/hooks/change-context.mjs +8 -4
- package/hooks/decision-capture.mjs +4 -2
- package/hooks/obsidian-common.mjs +701 -619
- package/hooks/plan-capture.mjs +8 -7
- package/hooks/pricing.json +53 -53
- package/hooks/session-ensure.mjs +413 -392
- package/hooks/session-identity.mjs +97 -0
- package/hooks/session-observability.mjs +16 -2
- package/hooks/session-start.mjs +340 -317
- package/hooks/session-stop.mjs +1153 -1141
- package/hooks/subagent-stop.mjs +7 -5
- package/hooks/task-log.mjs +5 -4
- package/hooks/token-usage.mjs +941 -941
- package/hooks/vault-health.mjs +198 -195
- package/package.json +1 -1
- package/src/change.mjs +14 -2
- package/src/rebuild-costs.mjs +5 -5
- package/src/session.mjs +37 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { basename } from 'path';
|
|
3
|
+
import { detectProvider, readSessionRegistry, transcriptsMatch } from './obsidian-common.mjs';
|
|
4
|
+
|
|
5
|
+
function parseLines(path) {
|
|
6
|
+
if (!path || !existsSync(path)) return [];
|
|
7
|
+
return readFileSync(path, 'utf-8').split('\n').filter(Boolean).map((line) => {
|
|
8
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
9
|
+
}).filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function inspectTranscriptIdentity(transcriptPath) {
|
|
13
|
+
const lines = parseLines(transcriptPath);
|
|
14
|
+
const codexMeta = lines.find((event) => event.type === 'session_meta')?.payload;
|
|
15
|
+
if (codexMeta) {
|
|
16
|
+
return {
|
|
17
|
+
transcriptProvider: 'openai',
|
|
18
|
+
provider: 'codex',
|
|
19
|
+
canonicalConversationId: codexMeta.session_id || codexMeta.id || '',
|
|
20
|
+
transcriptId: codexMeta.id || basename(transcriptPath, '.jsonl'),
|
|
21
|
+
parentConversationId: codexMeta.parent_thread_id || codexMeta.forked_from_id || '',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const claudeEvent = lines.find((event) => event.sessionId);
|
|
25
|
+
if (claudeEvent) {
|
|
26
|
+
return {
|
|
27
|
+
transcriptProvider: 'anthropic',
|
|
28
|
+
provider: 'claude',
|
|
29
|
+
canonicalConversationId: claudeEvent.sessionId,
|
|
30
|
+
transcriptId: basename(transcriptPath, '.jsonl'),
|
|
31
|
+
parentConversationId: '',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return { transcriptProvider: 'unknown', provider: 'unknown', canonicalConversationId: '', transcriptId: '', parentConversationId: '' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function compatible(provider, transcriptProvider) {
|
|
38
|
+
return (provider === 'codex' && transcriptProvider === 'openai')
|
|
39
|
+
|| (provider === 'claude' && transcriptProvider === 'anthropic');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveSessionIdentity(vaultBase, input = {}, provider = detectProvider()) {
|
|
43
|
+
const transcriptPath = input.transcript_path || input.transcriptPath || '';
|
|
44
|
+
const inspected = inspectTranscriptIdentity(transcriptPath);
|
|
45
|
+
const hookId = input.session_id || input.sessionId || '';
|
|
46
|
+
|
|
47
|
+
// Claude: input.session_id já é o id canônico e estável da conversa — idêntico
|
|
48
|
+
// ao sessionId que cada linha do transcript grava. Numa sessão nova o arquivo
|
|
49
|
+
// ainda não materializou em disco quando o hook roda, então inspectTranscriptIdentity
|
|
50
|
+
// volta vazio e o gate abaixo adiaria o 1º turno inteiro (SessionStart + 1º prompt
|
|
51
|
+
// sem nota; sessão curta nunca registrada). Não adiar: usar o hookId direto.
|
|
52
|
+
// Codex NÃO entra aqui de propósito — lá o id do hook no resume é efêmero e ≠ do
|
|
53
|
+
// thread canônico, então seguimos exigindo rollout/registry (incidente 2026-07-11,
|
|
54
|
+
// contaminação cross-provider de sessão).
|
|
55
|
+
if (provider === 'claude' && hookId && !inspected.canonicalConversationId) {
|
|
56
|
+
return {
|
|
57
|
+
state: 'resolved',
|
|
58
|
+
provider,
|
|
59
|
+
canonicalConversationId: hookId,
|
|
60
|
+
hookSessionId: hookId,
|
|
61
|
+
transcriptPath,
|
|
62
|
+
transcriptId: transcriptPath ? basename(transcriptPath, '.jsonl') : hookId,
|
|
63
|
+
parentConversationId: '',
|
|
64
|
+
diagnostics: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!transcriptPath || !inspected.canonicalConversationId) {
|
|
69
|
+
return { state: 'deferred', provider, transcriptPath, diagnostics: ['transcript ausente ou sem identidade canônica'] };
|
|
70
|
+
}
|
|
71
|
+
if (!compatible(provider, inspected.transcriptProvider)) {
|
|
72
|
+
return { state: 'deferred', provider, transcriptPath, diagnostics: [`provider ${provider} incompatível com transcript ${inspected.transcriptProvider}`] };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const registry = readSessionRegistry(vaultBase);
|
|
76
|
+
const byTranscript = Object.entries(registry.sessions || {}).find(([, entry]) => {
|
|
77
|
+
const paths = [...(Array.isArray(entry?.transcript_paths) ? entry.transcript_paths : []), entry?.transcript_path].filter(Boolean);
|
|
78
|
+
return paths.some((path) => transcriptsMatch(path, transcriptPath));
|
|
79
|
+
});
|
|
80
|
+
const canonicalConversationId = byTranscript?.[0] || inspected.canonicalConversationId;
|
|
81
|
+
return {
|
|
82
|
+
state: 'resolved',
|
|
83
|
+
provider,
|
|
84
|
+
canonicalConversationId,
|
|
85
|
+
hookSessionId: hookId,
|
|
86
|
+
transcriptPath,
|
|
87
|
+
transcriptId: inspected.transcriptId,
|
|
88
|
+
parentConversationId: inspected.parentConversationId,
|
|
89
|
+
diagnostics: [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resolveSessionEntry(vaultBase, input = {}, provider = detectProvider()) {
|
|
94
|
+
const identity = resolveSessionIdentity(vaultBase, input, provider);
|
|
95
|
+
if (identity.state !== 'resolved') return { identity, entry: null };
|
|
96
|
+
return { identity, entry: readSessionRegistry(vaultBase).sessions?.[identity.canonicalConversationId] || null };
|
|
97
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { collectSessionUsage } from './token-usage.mjs';
|
|
4
4
|
import { collectSubagentUsage, sessionDirFromTranscript } from './subagent-usage.mjs';
|
|
5
|
+
import { inspectTranscriptIdentity } from './session-identity.mjs';
|
|
5
6
|
|
|
6
7
|
const HEADING = '## Agentes, tokens e custos';
|
|
7
8
|
const LEGACY_HEADINGS = ['## Uso de tokens e custos', '## Subagents & Workflows'];
|
|
@@ -153,9 +154,22 @@ export function buildSessionObservability({ sessionContent, transcriptPath }) {
|
|
|
153
154
|
return { snapshot, content: upsertObservabilitySection(content, renderSessionObservability(snapshot)) };
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
export function updateSessionObservability({ sessionPath, transcriptPath }) {
|
|
157
|
+
export function updateSessionObservability({ sessionPath, transcriptPath, caller = 'unknown', canonicalConversationId = '' }) {
|
|
157
158
|
if (!sessionPath || !existsSync(sessionPath)) return null;
|
|
158
|
-
const
|
|
159
|
+
const sessionContent = readFileSync(sessionPath, 'utf8');
|
|
160
|
+
const noteProvider = sessionContent.match(/^provider:\s*"?([^"\n]+)"?/m)?.[1]?.trim() || 'unknown';
|
|
161
|
+
const identity = inspectTranscriptIdentity(transcriptPath);
|
|
162
|
+
if ((noteProvider === 'codex' && identity.transcriptProvider !== 'openai')
|
|
163
|
+
|| (noteProvider === 'claude' && identity.transcriptProvider !== 'anthropic')) {
|
|
164
|
+
throw new Error(`observability provider mismatch: note=${noteProvider}, transcript=${identity.transcriptProvider}`);
|
|
165
|
+
}
|
|
166
|
+
let annotated = setFrontmatterField(sessionContent, 'observability_caller', `"${caller}"`);
|
|
167
|
+
annotated = setFrontmatterField(annotated, 'observability_session_id', `"${canonicalConversationId || identity.canonicalConversationId || ''}"`);
|
|
168
|
+
annotated = setFrontmatterField(annotated, 'observability_transcript_id', `"${identity.transcriptId || ''}"`);
|
|
169
|
+
if (!/^observability_updated_at:/m.test(annotated)) {
|
|
170
|
+
annotated = setFrontmatterField(annotated, 'observability_updated_at', `"${new Date().toISOString()}"`);
|
|
171
|
+
}
|
|
172
|
+
const result = buildSessionObservability({ sessionContent: annotated, transcriptPath });
|
|
159
173
|
if (!result) return null;
|
|
160
174
|
writeFileSync(sessionPath, result.content, 'utf8');
|
|
161
175
|
return result.snapshot;
|