wendkeep 0.38.0 → 0.38.2
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 +19 -0
- package/hooks/brain-inject.mjs +74 -74
- package/hooks/obsidian-common.mjs +586 -586
- package/hooks/pricing.json +53 -53
- package/hooks/session-ensure.mjs +377 -377
- package/hooks/session-identity.mjs +22 -0
- package/hooks/session-observability.mjs +1 -1
- package/hooks/session-start.mjs +299 -299
- package/hooks/session-stop.mjs +1132 -1132
- package/hooks/subagent-stop.mjs +2 -2
- package/hooks/task-log.mjs +2 -2
- package/hooks/token-usage.mjs +949 -944
- package/hooks/vault-health.mjs +186 -186
- package/package.json +7 -2
- package/src/release-changelog.mjs +38 -0
|
@@ -43,6 +43,28 @@ export function resolveSessionIdentity(vaultBase, input = {}, provider = detectP
|
|
|
43
43
|
const transcriptPath = input.transcript_path || input.transcriptPath || '';
|
|
44
44
|
const inspected = inspectTranscriptIdentity(transcriptPath);
|
|
45
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
|
+
|
|
46
68
|
if (!transcriptPath || !inspected.canonicalConversationId) {
|
|
47
69
|
return { state: 'deferred', provider, transcriptPath, diagnostics: ['transcript ausente ou sem identidade canônica'] };
|
|
48
70
|
}
|
|
@@ -11,7 +11,7 @@ const usd = (n) => `$${(Number(n) || 0).toFixed(4)}`;
|
|
|
11
11
|
const round4 = (n) => Math.round((Number(n) || 0) * 10000) / 10000;
|
|
12
12
|
const effort = (value) => {
|
|
13
13
|
const normalized = String(value || '').trim().toLowerCase();
|
|
14
|
-
return ['none', 'low', 'medium', 'high', 'xhigh'].includes(normalized) ? normalized : (normalized || 'unknown');
|
|
14
|
+
return ['none', 'low', 'medium', 'high', 'xhigh', 'thinking'].includes(normalized) ? normalized : (normalized || 'unknown');
|
|
15
15
|
};
|
|
16
16
|
const usageTotal = (u = {}) => Number(u.total || 0) || (Number(u.input || 0) + Number(u.cached || 0) + Number(u.cacheWrite || 0) + Number(u.output || 0));
|
|
17
17
|
|
package/hooks/session-start.mjs
CHANGED
|
@@ -1,156 +1,156 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, writeFileSync } from 'fs';
|
|
3
|
-
import { basename, join } from 'path';
|
|
4
|
-
import { pathToFileURL } from 'url';
|
|
5
|
-
import {
|
|
6
|
-
controlPath,
|
|
7
|
-
ensureDir,
|
|
8
|
-
findActiveSessionByTranscript,
|
|
9
|
-
formatDate,
|
|
10
|
-
formatHourMinute,
|
|
11
|
-
formatLocalIso,
|
|
12
|
-
formatTime,
|
|
13
|
-
getVaultBase,
|
|
14
|
-
warnIfDefaultVault,
|
|
15
|
-
providerMeta,
|
|
16
|
-
readControl,
|
|
17
|
-
readHookInput,
|
|
18
|
-
readSessionRegistry,
|
|
19
|
-
sessionFileName,
|
|
20
|
-
sessionFolderRel,
|
|
21
|
-
sessionSummaryFromInput,
|
|
22
|
-
shouldReuseActiveSession,
|
|
23
|
-
sweepStaleSessionsFile,
|
|
24
|
-
toVaultRelative,
|
|
25
|
-
uniquePath,
|
|
26
|
-
upsertSessionRegistry,
|
|
27
|
-
VAULT_COMPLEMENT_RULES,
|
|
28
|
-
wikilinkFromRel,
|
|
29
|
-
writeControl,
|
|
30
|
-
writeHookOutput,
|
|
31
|
-
yamlQuote,
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, writeFileSync } from 'fs';
|
|
3
|
+
import { basename, join } from 'path';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
5
|
+
import {
|
|
6
|
+
controlPath,
|
|
7
|
+
ensureDir,
|
|
8
|
+
findActiveSessionByTranscript,
|
|
9
|
+
formatDate,
|
|
10
|
+
formatHourMinute,
|
|
11
|
+
formatLocalIso,
|
|
12
|
+
formatTime,
|
|
13
|
+
getVaultBase,
|
|
14
|
+
warnIfDefaultVault,
|
|
15
|
+
providerMeta,
|
|
16
|
+
readControl,
|
|
17
|
+
readHookInput,
|
|
18
|
+
readSessionRegistry,
|
|
19
|
+
sessionFileName,
|
|
20
|
+
sessionFolderRel,
|
|
21
|
+
sessionSummaryFromInput,
|
|
22
|
+
shouldReuseActiveSession,
|
|
23
|
+
sweepStaleSessionsFile,
|
|
24
|
+
toVaultRelative,
|
|
25
|
+
uniquePath,
|
|
26
|
+
upsertSessionRegistry,
|
|
27
|
+
VAULT_COMPLEMENT_RULES,
|
|
28
|
+
wikilinkFromRel,
|
|
29
|
+
writeControl,
|
|
30
|
+
writeHookOutput,
|
|
31
|
+
yamlQuote,
|
|
32
32
|
} from './obsidian-common.mjs';
|
|
33
33
|
import { resolveSessionIdentity } from './session-identity.mjs';
|
|
34
|
-
|
|
35
|
-
export function buildSessionContent({ relPath, now, summary = 'session', provider: providerId, sessionId = '' }) {
|
|
36
|
-
const date = formatDate(now);
|
|
37
|
-
const startedAt = formatLocalIso(now);
|
|
38
|
-
const titleTime = formatTime(now).slice(0, 5);
|
|
39
|
-
const objective = summary === 'session' ? 'Preencher durante a sessão.' : summary;
|
|
40
|
-
// Import passes the transcript's provider so a Codex note is tagged codex (not the
|
|
41
|
-
// ambient default); undefined falls back to providerMeta's own detection.
|
|
42
|
-
const provider = providerMeta(providerId);
|
|
43
|
-
|
|
44
|
-
return `---
|
|
45
|
-
type: session
|
|
46
|
-
date: ${date}
|
|
47
|
-
started_at: ${startedAt}
|
|
48
|
-
ended_at:
|
|
49
|
-
provider: ${provider.id}
|
|
50
|
-
session_id: ${sessionId ? yamlQuote(sessionId) : ''}
|
|
51
|
-
status: active
|
|
52
|
-
summary: ${yamlQuote(summary)}
|
|
53
|
-
cssclasses:
|
|
54
|
-
- topic-session
|
|
55
|
-
tags:
|
|
56
|
-
- sessao
|
|
57
|
-
- ${provider.tag}
|
|
58
|
-
- llm
|
|
59
|
-
source: ${provider.source}
|
|
60
|
-
related:
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
# ${titleTime} - ${summary}
|
|
64
|
-
|
|
65
|
-
## Metadados
|
|
66
|
-
|
|
67
|
-
- **Provider:** ${provider.label}
|
|
68
|
-
- **Início:** ${startedAt}
|
|
69
|
-
- **Fim:**
|
|
70
|
-
- **Status:** active
|
|
71
|
-
- **Arquivo:** \`${relPath}\`
|
|
72
|
-
|
|
73
|
-
## Objetivo da sessão
|
|
74
|
-
|
|
75
|
-
> ${objective}
|
|
76
|
-
|
|
77
|
-
## Resumo vivo
|
|
78
|
-
|
|
79
|
-
> Esta seção pode ser atualizada ao longo da sessão, mas o histórico de iterações deve ser preservado.
|
|
80
|
-
|
|
81
|
-
## Iterações
|
|
82
|
-
|
|
83
|
-
### ${titleTime} - Início da sessão
|
|
84
|
-
|
|
85
|
-
Sessão iniciada automaticamente pelo hook de início (${provider.label}).
|
|
86
|
-
|
|
87
|
-
## Decisões geradas nesta sessão
|
|
88
|
-
|
|
89
|
-
Nenhuma decisão registrada ainda.
|
|
90
|
-
|
|
91
|
-
## Bugs gerados nesta sessão
|
|
92
|
-
|
|
93
|
-
Nenhum bug registrado ainda.
|
|
94
|
-
|
|
95
|
-
## Aprendizados gerados nesta sessão
|
|
96
|
-
|
|
97
|
-
Nenhum aprendizado registrado ainda.
|
|
98
|
-
|
|
99
|
-
## Arquivos consultados
|
|
100
|
-
|
|
101
|
-
Nenhum arquivo registrado ainda.
|
|
102
|
-
|
|
103
|
-
## Arquivos criados ou alterados
|
|
104
|
-
|
|
105
|
-
Nenhum arquivo registrado ainda.
|
|
106
|
-
|
|
107
|
-
## Pendências
|
|
108
|
-
|
|
109
|
-
- [ ] Revisar resumo da sessão
|
|
110
|
-
- [ ] Verificar se houve decisões a registrar
|
|
111
|
-
- [ ] Verificar se houve bugs a registrar
|
|
112
|
-
- [ ] Verificar se houve aprendizados a registrar
|
|
113
|
-
|
|
114
|
-
## Encerramento
|
|
115
|
-
|
|
116
|
-
Sessão ainda em andamento.
|
|
117
|
-
`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function allocateSessionPath(vaultBase, now, summary = 'session') {
|
|
121
|
-
const folderRel = sessionFolderRel(now, vaultBase);
|
|
122
|
-
const folderAbs = join(vaultBase, folderRel);
|
|
123
|
-
ensureDir(folderAbs);
|
|
124
|
-
|
|
125
|
-
const baseName = sessionFileName(now, summary);
|
|
126
|
-
const filePath = uniquePath(join(folderAbs, baseName));
|
|
127
|
-
return {
|
|
128
|
-
absPath: filePath,
|
|
129
|
-
relPath: toVaultRelative(vaultBase, filePath),
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function buildAdditionalContext({ relPath, startedAt, vaultBase }) {
|
|
134
|
-
const controlRel = toVaultRelative(vaultBase, controlPath(vaultBase));
|
|
135
|
-
return [
|
|
136
|
-
'<obsidian_session>',
|
|
137
|
-
`Sessão Obsidian ativa: ${relPath}`,
|
|
138
|
-
`Controle atualizado: ${controlRel}`,
|
|
139
|
-
`Início: ${startedAt}`,
|
|
140
|
-
'',
|
|
141
|
-
'Use a sessão ativa como log desta conversa.',
|
|
142
|
-
'Antes de registrar informações, leia `.brain/CURRENT_SESSION.md` no vault.',
|
|
143
|
-
'Nunca sobrescreva o histórico anterior. Registre cada iteração como `### HH:MM - Título` DENTRO da seção `## Iterações` (logo antes de `## Decisões geradas nesta sessão`). Cada iteração deve trazer contexto conversado suficiente: pedido do usuário, investigação/ações, evidências relevantes e estado final. NUNCA escreva iterações após `## Encerramento`.',
|
|
144
|
-
'',
|
|
145
|
-
...VAULT_COMPLEMENT_RULES,
|
|
146
|
-
'Não registre chaves, tokens, senhas ou segredos; substitua por `[REDACTED_SECRET]`.',
|
|
147
|
-
`Wikilink da sessão: ${wikilinkFromRel(relPath)}`,
|
|
148
|
-
'</obsidian_session>',
|
|
149
|
-
].join('\n');
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function main() {
|
|
153
|
-
const input = readHookInput();
|
|
34
|
+
|
|
35
|
+
export function buildSessionContent({ relPath, now, summary = 'session', provider: providerId, sessionId = '' }) {
|
|
36
|
+
const date = formatDate(now);
|
|
37
|
+
const startedAt = formatLocalIso(now);
|
|
38
|
+
const titleTime = formatTime(now).slice(0, 5);
|
|
39
|
+
const objective = summary === 'session' ? 'Preencher durante a sessão.' : summary;
|
|
40
|
+
// Import passes the transcript's provider so a Codex note is tagged codex (not the
|
|
41
|
+
// ambient default); undefined falls back to providerMeta's own detection.
|
|
42
|
+
const provider = providerMeta(providerId);
|
|
43
|
+
|
|
44
|
+
return `---
|
|
45
|
+
type: session
|
|
46
|
+
date: ${date}
|
|
47
|
+
started_at: ${startedAt}
|
|
48
|
+
ended_at:
|
|
49
|
+
provider: ${provider.id}
|
|
50
|
+
session_id: ${sessionId ? yamlQuote(sessionId) : ''}
|
|
51
|
+
status: active
|
|
52
|
+
summary: ${yamlQuote(summary)}
|
|
53
|
+
cssclasses:
|
|
54
|
+
- topic-session
|
|
55
|
+
tags:
|
|
56
|
+
- sessao
|
|
57
|
+
- ${provider.tag}
|
|
58
|
+
- llm
|
|
59
|
+
source: ${provider.source}
|
|
60
|
+
related:
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
# ${titleTime} - ${summary}
|
|
64
|
+
|
|
65
|
+
## Metadados
|
|
66
|
+
|
|
67
|
+
- **Provider:** ${provider.label}
|
|
68
|
+
- **Início:** ${startedAt}
|
|
69
|
+
- **Fim:**
|
|
70
|
+
- **Status:** active
|
|
71
|
+
- **Arquivo:** \`${relPath}\`
|
|
72
|
+
|
|
73
|
+
## Objetivo da sessão
|
|
74
|
+
|
|
75
|
+
> ${objective}
|
|
76
|
+
|
|
77
|
+
## Resumo vivo
|
|
78
|
+
|
|
79
|
+
> Esta seção pode ser atualizada ao longo da sessão, mas o histórico de iterações deve ser preservado.
|
|
80
|
+
|
|
81
|
+
## Iterações
|
|
82
|
+
|
|
83
|
+
### ${titleTime} - Início da sessão
|
|
84
|
+
|
|
85
|
+
Sessão iniciada automaticamente pelo hook de início (${provider.label}).
|
|
86
|
+
|
|
87
|
+
## Decisões geradas nesta sessão
|
|
88
|
+
|
|
89
|
+
Nenhuma decisão registrada ainda.
|
|
90
|
+
|
|
91
|
+
## Bugs gerados nesta sessão
|
|
92
|
+
|
|
93
|
+
Nenhum bug registrado ainda.
|
|
94
|
+
|
|
95
|
+
## Aprendizados gerados nesta sessão
|
|
96
|
+
|
|
97
|
+
Nenhum aprendizado registrado ainda.
|
|
98
|
+
|
|
99
|
+
## Arquivos consultados
|
|
100
|
+
|
|
101
|
+
Nenhum arquivo registrado ainda.
|
|
102
|
+
|
|
103
|
+
## Arquivos criados ou alterados
|
|
104
|
+
|
|
105
|
+
Nenhum arquivo registrado ainda.
|
|
106
|
+
|
|
107
|
+
## Pendências
|
|
108
|
+
|
|
109
|
+
- [ ] Revisar resumo da sessão
|
|
110
|
+
- [ ] Verificar se houve decisões a registrar
|
|
111
|
+
- [ ] Verificar se houve bugs a registrar
|
|
112
|
+
- [ ] Verificar se houve aprendizados a registrar
|
|
113
|
+
|
|
114
|
+
## Encerramento
|
|
115
|
+
|
|
116
|
+
Sessão ainda em andamento.
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function allocateSessionPath(vaultBase, now, summary = 'session') {
|
|
121
|
+
const folderRel = sessionFolderRel(now, vaultBase);
|
|
122
|
+
const folderAbs = join(vaultBase, folderRel);
|
|
123
|
+
ensureDir(folderAbs);
|
|
124
|
+
|
|
125
|
+
const baseName = sessionFileName(now, summary);
|
|
126
|
+
const filePath = uniquePath(join(folderAbs, baseName));
|
|
127
|
+
return {
|
|
128
|
+
absPath: filePath,
|
|
129
|
+
relPath: toVaultRelative(vaultBase, filePath),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildAdditionalContext({ relPath, startedAt, vaultBase }) {
|
|
134
|
+
const controlRel = toVaultRelative(vaultBase, controlPath(vaultBase));
|
|
135
|
+
return [
|
|
136
|
+
'<obsidian_session>',
|
|
137
|
+
`Sessão Obsidian ativa: ${relPath}`,
|
|
138
|
+
`Controle atualizado: ${controlRel}`,
|
|
139
|
+
`Início: ${startedAt}`,
|
|
140
|
+
'',
|
|
141
|
+
'Use a sessão ativa como log desta conversa.',
|
|
142
|
+
'Antes de registrar informações, leia `.brain/CURRENT_SESSION.md` no vault.',
|
|
143
|
+
'Nunca sobrescreva o histórico anterior. Registre cada iteração como `### HH:MM - Título` DENTRO da seção `## Iterações` (logo antes de `## Decisões geradas nesta sessão`). Cada iteração deve trazer contexto conversado suficiente: pedido do usuário, investigação/ações, evidências relevantes e estado final. NUNCA escreva iterações após `## Encerramento`.',
|
|
144
|
+
'',
|
|
145
|
+
...VAULT_COMPLEMENT_RULES,
|
|
146
|
+
'Não registre chaves, tokens, senhas ou segredos; substitua por `[REDACTED_SECRET]`.',
|
|
147
|
+
`Wikilink da sessão: ${wikilinkFromRel(relPath)}`,
|
|
148
|
+
'</obsidian_session>',
|
|
149
|
+
].join('\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function main() {
|
|
153
|
+
const input = readHookInput();
|
|
154
154
|
const vaultBase = getVaultBase(input);
|
|
155
155
|
warnIfDefaultVault(input);
|
|
156
156
|
const now = new Date();
|
|
@@ -169,172 +169,172 @@ function main() {
|
|
|
169
169
|
const sessionId = identity.canonicalConversationId;
|
|
170
170
|
const transcriptPath = identity.transcriptPath;
|
|
171
171
|
const control = readControl(vaultBase);
|
|
172
|
-
|
|
173
|
-
// Fecha sessões `active` órfãs (sem evento de fim — janela fechada/crash) antes
|
|
174
|
-
// de seguir. Preserva a deste transcript: pode ser reaproveitada logo abaixo.
|
|
175
|
-
try {
|
|
172
|
+
|
|
173
|
+
// Fecha sessões `active` órfãs (sem evento de fim — janela fechada/crash) antes
|
|
174
|
+
// de seguir. Preserva a deste transcript: pode ser reaproveitada logo abaixo.
|
|
175
|
+
try {
|
|
176
176
|
sweepStaleSessionsFile(vaultBase, now, undefined, transcriptPath);
|
|
177
|
-
} catch (error) {
|
|
178
|
-
process.stderr.write(`[wendkeep] sweep de sessões falhou: ${error.message}\n`);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Reuso da nota apontada pelo CURRENT_SESSION só quando é a MESMA conversa
|
|
182
|
-
// (session_id idêntico). NÃO reusar por janela de tempo: o ponteiro global é
|
|
183
|
-
// racy e uma conversa concorrente recente faria esta adotar a nota da outra.
|
|
184
|
-
// Resume/compactação (session_id novo, mesmo transcript) é tratado abaixo por
|
|
185
|
-
// identidade de transcript (findActiveSessionByTranscript).
|
|
186
|
-
if (control.status === 'active' && control.session_file && control.session_id === sessionId) {
|
|
187
|
-
const activePath = join(vaultBase, control.session_file);
|
|
188
|
-
if (existsSync(activePath)) {
|
|
177
|
+
} catch (error) {
|
|
178
|
+
process.stderr.write(`[wendkeep] sweep de sessões falhou: ${error.message}\n`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Reuso da nota apontada pelo CURRENT_SESSION só quando é a MESMA conversa
|
|
182
|
+
// (session_id idêntico). NÃO reusar por janela de tempo: o ponteiro global é
|
|
183
|
+
// racy e uma conversa concorrente recente faria esta adotar a nota da outra.
|
|
184
|
+
// Resume/compactação (session_id novo, mesmo transcript) é tratado abaixo por
|
|
185
|
+
// identidade de transcript (findActiveSessionByTranscript).
|
|
186
|
+
if (control.status === 'active' && control.session_file && control.session_id === sessionId) {
|
|
187
|
+
const activePath = join(vaultBase, control.session_file);
|
|
188
|
+
if (existsSync(activePath)) {
|
|
189
189
|
upsertSessionRegistry(vaultBase, sessionId, {
|
|
190
|
-
session_file: control.session_file,
|
|
191
|
-
status: 'active',
|
|
192
|
-
started_at: control.started_at,
|
|
190
|
+
session_file: control.session_file,
|
|
191
|
+
status: 'active',
|
|
192
|
+
started_at: control.started_at,
|
|
193
193
|
ended_at: '',
|
|
194
194
|
provider: provider.id,
|
|
195
195
|
transcript_path: transcriptPath,
|
|
196
196
|
transcript_id: identity.transcriptId,
|
|
197
197
|
});
|
|
198
|
-
writeHookOutput({
|
|
199
|
-
hookSpecificOutput: {
|
|
200
|
-
hookEventName: 'SessionStart',
|
|
201
|
-
additionalContext: buildAdditionalContext({
|
|
202
|
-
relPath: control.session_file,
|
|
203
|
-
startedAt: control.started_at,
|
|
204
|
-
vaultBase,
|
|
205
|
-
}),
|
|
206
|
-
},
|
|
207
|
-
});
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Reuso DETERMINISTICO por session_id: o id e estavel em todo o ciclo da conversa
|
|
213
|
-
// (inclusive resume/compactacao apos a janela de 10min e virada de dia). O ponteiro
|
|
214
|
-
// CURRENT_SESSION e racy — sessoes Codex intercaladas o clobberam — entao olhamos o
|
|
215
|
-
// registry direto pelo session_id antes de criar nota nova. Previne o split (mesma
|
|
216
|
-
// sessao em duas notas). Recria o esqueleto no MESMO caminho se a nota sumiu.
|
|
217
|
-
if (sessionId) {
|
|
218
|
-
const known = readSessionRegistry(vaultBase).sessions?.[sessionId];
|
|
219
|
-
if (known && known.status === 'active' && known.session_file) {
|
|
220
|
-
const knownAbs = join(vaultBase, known.session_file);
|
|
221
|
-
if (!existsSync(knownAbs)) {
|
|
222
|
-
ensureDir(join(vaultBase, known.session_file.split('/').slice(0, -1).join('/')));
|
|
223
|
-
writeFileSync(knownAbs, buildSessionContent({ relPath: known.session_file, now, summary: sessionSummaryFromInput(input), sessionId }), 'utf-8');
|
|
224
|
-
}
|
|
225
|
-
const startedAt = known.started_at || control.started_at || formatLocalIso(now);
|
|
226
|
-
writeControl(vaultBase, {
|
|
227
|
-
status: 'active',
|
|
228
|
-
session_file: known.session_file,
|
|
229
|
-
last_session_file: known.session_file,
|
|
230
|
-
started_at: startedAt,
|
|
231
|
-
ended_at: '',
|
|
232
|
-
session_id: sessionId,
|
|
233
|
-
last_logged_turn_id: control.last_logged_turn_id || '',
|
|
234
|
-
});
|
|
235
|
-
upsertSessionRegistry(vaultBase, sessionId, {
|
|
236
|
-
session_file: known.session_file,
|
|
237
|
-
status: 'active',
|
|
238
|
-
started_at: startedAt,
|
|
239
|
-
ended_at: '',
|
|
198
|
+
writeHookOutput({
|
|
199
|
+
hookSpecificOutput: {
|
|
200
|
+
hookEventName: 'SessionStart',
|
|
201
|
+
additionalContext: buildAdditionalContext({
|
|
202
|
+
relPath: control.session_file,
|
|
203
|
+
startedAt: control.started_at,
|
|
204
|
+
vaultBase,
|
|
205
|
+
}),
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Reuso DETERMINISTICO por session_id: o id e estavel em todo o ciclo da conversa
|
|
213
|
+
// (inclusive resume/compactacao apos a janela de 10min e virada de dia). O ponteiro
|
|
214
|
+
// CURRENT_SESSION e racy — sessoes Codex intercaladas o clobberam — entao olhamos o
|
|
215
|
+
// registry direto pelo session_id antes de criar nota nova. Previne o split (mesma
|
|
216
|
+
// sessao em duas notas). Recria o esqueleto no MESMO caminho se a nota sumiu.
|
|
217
|
+
if (sessionId) {
|
|
218
|
+
const known = readSessionRegistry(vaultBase).sessions?.[sessionId];
|
|
219
|
+
if (known && known.status === 'active' && known.session_file) {
|
|
220
|
+
const knownAbs = join(vaultBase, known.session_file);
|
|
221
|
+
if (!existsSync(knownAbs)) {
|
|
222
|
+
ensureDir(join(vaultBase, known.session_file.split('/').slice(0, -1).join('/')));
|
|
223
|
+
writeFileSync(knownAbs, buildSessionContent({ relPath: known.session_file, now, summary: sessionSummaryFromInput(input), sessionId }), 'utf-8');
|
|
224
|
+
}
|
|
225
|
+
const startedAt = known.started_at || control.started_at || formatLocalIso(now);
|
|
226
|
+
writeControl(vaultBase, {
|
|
227
|
+
status: 'active',
|
|
228
|
+
session_file: known.session_file,
|
|
229
|
+
last_session_file: known.session_file,
|
|
230
|
+
started_at: startedAt,
|
|
231
|
+
ended_at: '',
|
|
232
|
+
session_id: sessionId,
|
|
233
|
+
last_logged_turn_id: control.last_logged_turn_id || '',
|
|
234
|
+
});
|
|
235
|
+
upsertSessionRegistry(vaultBase, sessionId, {
|
|
236
|
+
session_file: known.session_file,
|
|
237
|
+
status: 'active',
|
|
238
|
+
started_at: startedAt,
|
|
239
|
+
ended_at: '',
|
|
240
240
|
transcript_path: transcriptPath || known.transcript_path || '',
|
|
241
241
|
transcript_id: identity.transcriptId,
|
|
242
242
|
provider: provider.id,
|
|
243
|
-
});
|
|
244
|
-
writeHookOutput({
|
|
245
|
-
hookSpecificOutput: {
|
|
246
|
-
hookEventName: 'SessionStart',
|
|
247
|
-
additionalContext: buildAdditionalContext({ relPath: known.session_file, startedAt, vaultBase }),
|
|
248
|
-
},
|
|
249
|
-
});
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Re-init da conversa (compactação/resume) traz um session_id novo e cai fora
|
|
255
|
-
// da janela de reuso; o transcript continua o mesmo. Reaproveita a sessão ativa
|
|
256
|
-
// desse transcript em vez de criar um placeholder `HH-MM-codex`.
|
|
243
|
+
});
|
|
244
|
+
writeHookOutput({
|
|
245
|
+
hookSpecificOutput: {
|
|
246
|
+
hookEventName: 'SessionStart',
|
|
247
|
+
additionalContext: buildAdditionalContext({ relPath: known.session_file, startedAt, vaultBase }),
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Re-init da conversa (compactação/resume) traz um session_id novo e cai fora
|
|
255
|
+
// da janela de reuso; o transcript continua o mesmo. Reaproveita a sessão ativa
|
|
256
|
+
// desse transcript em vez de criar um placeholder `HH-MM-codex`.
|
|
257
257
|
if (transcriptPath) {
|
|
258
|
-
const match = findActiveSessionByTranscript(vaultBase, transcriptPath);
|
|
259
|
-
if (match) {
|
|
260
|
-
// fail-safe: a nota do registro pode ter sumido do disco (git stash/checkout/
|
|
261
|
-
// sync removeram o arquivo). Em vez de mintar uma nota nova (split de sessão),
|
|
262
|
-
// recria o esqueleto no MESMO caminho e segue reaproveitando.
|
|
263
|
-
const matchAbs = join(vaultBase, match.session_file);
|
|
264
|
-
if (!existsSync(matchAbs)) {
|
|
265
|
-
ensureDir(join(vaultBase, match.session_file.split('/').slice(0, -1).join('/')));
|
|
266
|
-
writeFileSync(matchAbs, buildSessionContent({ relPath: match.session_file, now, summary: sessionSummaryFromInput(input), sessionId: sessionId || match.sessionId }), 'utf-8');
|
|
267
|
-
}
|
|
268
|
-
const startedAt = match.started_at || control.started_at || formatLocalIso(now);
|
|
269
|
-
writeControl(vaultBase, {
|
|
270
|
-
status: 'active',
|
|
271
|
-
session_file: match.session_file,
|
|
272
|
-
last_session_file: match.session_file,
|
|
273
|
-
started_at: startedAt,
|
|
274
|
-
ended_at: '',
|
|
275
|
-
session_id: sessionId || match.sessionId,
|
|
276
|
-
last_logged_turn_id: control.last_logged_turn_id || '',
|
|
277
|
-
});
|
|
278
|
-
upsertSessionRegistry(vaultBase, sessionId || match.sessionId, {
|
|
279
|
-
session_file: match.session_file,
|
|
280
|
-
status: 'active',
|
|
281
|
-
started_at: startedAt,
|
|
282
|
-
ended_at: '',
|
|
258
|
+
const match = findActiveSessionByTranscript(vaultBase, transcriptPath);
|
|
259
|
+
if (match) {
|
|
260
|
+
// fail-safe: a nota do registro pode ter sumido do disco (git stash/checkout/
|
|
261
|
+
// sync removeram o arquivo). Em vez de mintar uma nota nova (split de sessão),
|
|
262
|
+
// recria o esqueleto no MESMO caminho e segue reaproveitando.
|
|
263
|
+
const matchAbs = join(vaultBase, match.session_file);
|
|
264
|
+
if (!existsSync(matchAbs)) {
|
|
265
|
+
ensureDir(join(vaultBase, match.session_file.split('/').slice(0, -1).join('/')));
|
|
266
|
+
writeFileSync(matchAbs, buildSessionContent({ relPath: match.session_file, now, summary: sessionSummaryFromInput(input), sessionId: sessionId || match.sessionId }), 'utf-8');
|
|
267
|
+
}
|
|
268
|
+
const startedAt = match.started_at || control.started_at || formatLocalIso(now);
|
|
269
|
+
writeControl(vaultBase, {
|
|
270
|
+
status: 'active',
|
|
271
|
+
session_file: match.session_file,
|
|
272
|
+
last_session_file: match.session_file,
|
|
273
|
+
started_at: startedAt,
|
|
274
|
+
ended_at: '',
|
|
275
|
+
session_id: sessionId || match.sessionId,
|
|
276
|
+
last_logged_turn_id: control.last_logged_turn_id || '',
|
|
277
|
+
});
|
|
278
|
+
upsertSessionRegistry(vaultBase, sessionId || match.sessionId, {
|
|
279
|
+
session_file: match.session_file,
|
|
280
|
+
status: 'active',
|
|
281
|
+
started_at: startedAt,
|
|
282
|
+
ended_at: '',
|
|
283
283
|
transcript_path: transcriptPath,
|
|
284
284
|
transcript_id: identity.transcriptId,
|
|
285
285
|
provider: provider.id,
|
|
286
|
-
});
|
|
287
|
-
writeHookOutput({
|
|
288
|
-
hookSpecificOutput: {
|
|
289
|
-
hookEventName: 'SessionStart',
|
|
290
|
-
additionalContext: buildAdditionalContext({ relPath: match.session_file, startedAt, vaultBase }),
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const summary = sessionSummaryFromInput(input);
|
|
298
|
-
const { absPath, relPath } = allocateSessionPath(vaultBase, now, summary);
|
|
299
|
-
const startedAt = formatLocalIso(now);
|
|
286
|
+
});
|
|
287
|
+
writeHookOutput({
|
|
288
|
+
hookSpecificOutput: {
|
|
289
|
+
hookEventName: 'SessionStart',
|
|
290
|
+
additionalContext: buildAdditionalContext({ relPath: match.session_file, startedAt, vaultBase }),
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const summary = sessionSummaryFromInput(input);
|
|
298
|
+
const { absPath, relPath } = allocateSessionPath(vaultBase, now, summary);
|
|
299
|
+
const startedAt = formatLocalIso(now);
|
|
300
300
|
writeFileSync(absPath, buildSessionContent({ relPath, now, summary, sessionId, provider: provider.id }), 'utf-8');
|
|
301
|
-
writeControl(vaultBase, {
|
|
302
|
-
status: 'active',
|
|
303
|
-
session_file: relPath,
|
|
304
|
-
last_session_file: relPath,
|
|
305
|
-
started_at: startedAt,
|
|
306
|
-
session_id: sessionId,
|
|
307
|
-
});
|
|
308
|
-
upsertSessionRegistry(vaultBase, sessionId, {
|
|
309
|
-
session_file: relPath,
|
|
310
|
-
status: 'active',
|
|
311
|
-
started_at: startedAt,
|
|
301
|
+
writeControl(vaultBase, {
|
|
302
|
+
status: 'active',
|
|
303
|
+
session_file: relPath,
|
|
304
|
+
last_session_file: relPath,
|
|
305
|
+
started_at: startedAt,
|
|
306
|
+
session_id: sessionId,
|
|
307
|
+
});
|
|
308
|
+
upsertSessionRegistry(vaultBase, sessionId, {
|
|
309
|
+
session_file: relPath,
|
|
310
|
+
status: 'active',
|
|
311
|
+
started_at: startedAt,
|
|
312
312
|
ended_at: '',
|
|
313
313
|
provider: provider.id,
|
|
314
314
|
transcript_path: transcriptPath,
|
|
315
315
|
transcript_id: identity.transcriptId,
|
|
316
316
|
});
|
|
317
|
-
|
|
318
|
-
writeHookOutput({
|
|
319
|
-
hookSpecificOutput: {
|
|
320
|
-
hookEventName: 'SessionStart',
|
|
321
|
-
additionalContext: buildAdditionalContext({ relPath, startedAt, vaultBase }),
|
|
322
|
-
},
|
|
323
|
-
systemMessage: [
|
|
317
|
+
|
|
318
|
+
writeHookOutput({
|
|
319
|
+
hookSpecificOutput: {
|
|
320
|
+
hookEventName: 'SessionStart',
|
|
321
|
+
additionalContext: buildAdditionalContext({ relPath, startedAt, vaultBase }),
|
|
322
|
+
},
|
|
323
|
+
systemMessage: [
|
|
324
324
|
`Sessão ${provider.label} criada em ${relPath}.`,
|
|
325
|
-
`${basename(controlPath(vaultBase))} atualizado.`,
|
|
326
|
-
'Iterações devem ser anexadas, nunca sobrescritas.',
|
|
327
|
-
].join(' '),
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
332
|
-
try {
|
|
333
|
-
main();
|
|
334
|
-
} catch (error) {
|
|
335
|
-
process.stderr.write(`[wendkeep] SessionStart falhou: ${error.message}\n`);
|
|
336
|
-
writeHookOutput({
|
|
337
|
-
systemMessage: `[wendkeep] Não foi possível criar a sessão Obsidian: ${error.message}`,
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
}
|
|
325
|
+
`${basename(controlPath(vaultBase))} atualizado.`,
|
|
326
|
+
'Iterações devem ser anexadas, nunca sobrescritas.',
|
|
327
|
+
].join(' '),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
332
|
+
try {
|
|
333
|
+
main();
|
|
334
|
+
} catch (error) {
|
|
335
|
+
process.stderr.write(`[wendkeep] SessionStart falhou: ${error.message}\n`);
|
|
336
|
+
writeHookOutput({
|
|
337
|
+
systemMessage: `[wendkeep] Não foi possível criar a sessão Obsidian: ${error.message}`,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|