wendkeep 0.36.0 → 0.37.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 +15 -6
- package/README.md +2 -0
- package/README.pt-BR.md +2 -0
- package/hooks/import-sessions.mjs +3 -7
- package/hooks/session-observability.mjs +162 -0
- package/hooks/session-stop.mjs +7 -19
- package/hooks/subagent-stop.mjs +4 -5
- package/hooks/subagent-usage.mjs +8 -4
- package/hooks/token-usage.mjs +57 -30
- package/hooks/vault-health.mjs +9 -4
- package/package.json +1 -1
- package/src/rebuild-costs.mjs +2 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to **wendkeep** are documented here. Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.37.0] — 2026-07-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Observabilidade consolidada em `## Agentes, tokens e custos`: Stop, SubagentStop, importação e rebuild usam um único writer atômico para main + subagents.
|
|
12
|
+
- Ledger por modelo/origem com reasoning tokens e effort, sem alterar a regra de preço do modelo.
|
|
13
|
+
- Novo `wendkeep cost rebuild`, dry-run por padrão; `--apply` reconstrói sessões antigas via `SESSION_REGISTRY` e grava `.brain/COST_REBUILD.json`.
|
|
14
|
+
- Preços API-equivalentes de GPT-5.6 Sol, Terra e Luna.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Custos de subagents são atribuídos ao modelo que realmente os executou, em vez do modelo principal.
|
|
19
|
+
- Migração remove os headings legados sem perder reaberturas ou iterações mal posicionadas.
|
|
20
|
+
- Totais combinados de tokens/custo são atualizados também no `SubagentStop` e persistidos em campos compatíveis com os dashboards existentes.
|
|
21
|
+
|
|
7
22
|
## [0.35.0] — 2026-07-11
|
|
8
23
|
|
|
9
24
|
### Fixed
|
|
@@ -870,9 +885,3 @@ Initial release — the capture engine, extracted from a system in daily product
|
|
|
870
885
|
[0.2.7]: https://www.npmjs.com/package/wendkeep/v/0.2.7
|
|
871
886
|
[0.2.0]: https://www.npmjs.com/package/wendkeep/v/0.2.0
|
|
872
887
|
[0.1.0]: https://www.npmjs.com/package/wendkeep/v/0.1.0
|
|
873
|
-
# 0.36.0 — 2026-07-11
|
|
874
|
-
|
|
875
|
-
- Custos de subagents agora são atribuídos ao modelo que realmente os executou por meio de um ledger por sessão; campos legados continuam compatíveis.
|
|
876
|
-
- Novo `wendkeep cost rebuild`, dry-run por padrão, reconstrói sessões antigas via `SESSION_REGISTRY` e, com `--apply`, grava um manifesto auditável em `.brain/COST_REBUILD.json`.
|
|
877
|
-
- Totais combinados de tokens/custo passam a ser persistidos explicitamente nas notas.
|
|
878
|
-
- Preços API-equivalentes de GPT-5.6 Sol, Terra e Luna adicionados à tabela editável e ao fallback interno.
|
package/README.md
CHANGED
|
@@ -119,6 +119,8 @@ No re‑copying, no snapshot to re‑sync — the package is the single source o
|
|
|
119
119
|
| `wendkeep cost [--since d]` | Aggregate AI-coding spend across the vault's sessions — total, by model, by day (`--json`). |
|
|
120
120
|
| `wendkeep import [opts]` | **Retroactive memory** — backfill past **Claude + Codex** sessions into the vault (deduped by `session_id`). `--source all\|claude\|codex` / `--from <dir>` / `--codex-from <dir>` / `--since d` / `--limit n` / `--dry-run` / `--json`. |
|
|
121
121
|
| `wendkeep cost rebuild [opts]` | Recalcula custos históricos do transcript principal e subagents usando `SESSION_REGISTRY`. Dry-run por padrão; `--apply` atualiza notas e grava `.brain/COST_REBUILD.json`. Aceita `--session`, `--limit` e `--json`. |
|
|
122
|
+
|
|
123
|
+
Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source.
|
|
122
124
|
| `wendkeep lesson add "t" "l"` | Record a project-local lesson (injected at the next SessionStart). |
|
|
123
125
|
| `wendkeep sync-defs` | Copy `.brain/agents\|skills` into `.codex/agents`, `.claude/skills`, `.agents/skills`; `--check` detects drift. |
|
|
124
126
|
| `wendkeep validate-memory [path]` | Validate `.brain/CORE.md` (cap 25, 3 sections, no secrets/PII). |
|
package/README.pt-BR.md
CHANGED
|
@@ -116,6 +116,8 @@ Sem recopiar, sem snapshot pra re‑sincronizar — o pacote é a única fonte d
|
|
|
116
116
|
| `wendkeep sensors <sub>` | `list` / `add <id> "<comando>"` — vê/edita `wendkeep.sensors.json` (JSON Schema incluso). |
|
|
117
117
|
| `wendkeep cost [opts]` | Agrega o gasto de IA nas sessões do cofre — total, por modelo, por dia · `--top [N]` · `--trend [day\|week\|month]` (+ projeção) · `--write` (gera `00-Custo.md`) · `--json`. |
|
|
118
118
|
| `wendkeep cost rebuild [opts]` | Reconstrói custos históricos do transcript principal e subagents via `SESSION_REGISTRY`. Dry-run por padrão; `--apply` grava notas e `.brain/COST_REBUILD.json`. |
|
|
119
|
+
|
|
120
|
+
As notas de sessão usam um único snapshot vivo `## Agentes, tokens e custos`. Os hooks do agente principal e dos subagents recompõem o bloco atomicamente, incluindo custo, dimensões de tokens, reasoning e effort por modelo/origem.
|
|
119
121
|
| `wendkeep stats [--vault P]` | Uma linha compartilhável: sessões · prompts · gasto · período · modelos (`--json`). |
|
|
120
122
|
| `wendkeep import [opts]` | **Memória retroativa** — importa sessões passadas de **Claude + Codex** pro cofre (dedup por `session_id`). `--source all\|claude\|codex` / `--from <dir>` / `--codex-from <dir>` / `--stamp-ids` / `--since d` / `--limit n` / `--dry-run` / `--json`. |
|
|
121
123
|
| `wendkeep dashboard [--force]` | (Re)gera os Bases filtrados por pasta + o MOC `00-Dashboard`. |
|
|
@@ -16,8 +16,7 @@ import {
|
|
|
16
16
|
} from './session-stop.mjs';
|
|
17
17
|
import { buildSessionContent, allocateSessionPath } from './session-start.mjs';
|
|
18
18
|
import { createLinkedNotes } from './linked-notes.mjs';
|
|
19
|
-
import {
|
|
20
|
-
import { upsertSubagentUsage } from './subagent-usage.mjs';
|
|
19
|
+
import { updateSessionObservability } from './session-observability.mjs';
|
|
21
20
|
import { readSessionRegistry, upsertSessionRegistry, formatLocalIso, formatDate, providerMeta } from './obsidian-common.mjs';
|
|
22
21
|
import { getLocale } from './locale.mjs';
|
|
23
22
|
import { captureProseDecisions } from './decision-capture.mjs';
|
|
@@ -211,11 +210,8 @@ export function importSession(vaultBase, txPath, opts = {}) {
|
|
|
211
210
|
|
|
212
211
|
// Cost + subagent telemetry, exactly like the live Stop hook. Fail-open.
|
|
213
212
|
try {
|
|
214
|
-
|
|
215
|
-
} catch { /*
|
|
216
|
-
try {
|
|
217
|
-
upsertSubagentUsage(absPath, txPath);
|
|
218
|
-
} catch { /* subagent telemetry is best-effort */ }
|
|
213
|
+
updateSessionObservability({ sessionPath: absPath, transcriptPath: txPath });
|
|
214
|
+
} catch { /* observability is best-effort */ }
|
|
219
215
|
|
|
220
216
|
// Finalize: derived notes + closing section + ended_at from the last turn.
|
|
221
217
|
const endedAt = formatLocalIso(endDate);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Single atomic writer for session usage, models, reasoning/effort and subagents.
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { collectSessionUsage } from './token-usage.mjs';
|
|
4
|
+
import { collectSubagentUsage, sessionDirFromTranscript } from './subagent-usage.mjs';
|
|
5
|
+
|
|
6
|
+
const HEADING = '## Agentes, tokens e custos';
|
|
7
|
+
const LEGACY_HEADINGS = ['## Uso de tokens e custos', '## Subagents & Workflows'];
|
|
8
|
+
const fmt = (n) => Math.trunc(Number(n) || 0).toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
|
9
|
+
const usd = (n) => `$${(Number(n) || 0).toFixed(4)}`;
|
|
10
|
+
const round4 = (n) => Math.round((Number(n) || 0) * 10000) / 10000;
|
|
11
|
+
const effort = (value) => {
|
|
12
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
13
|
+
return ['none', 'low', 'medium', 'high', 'xhigh'].includes(normalized) ? normalized : (normalized || 'unknown');
|
|
14
|
+
};
|
|
15
|
+
const usageTotal = (u = {}) => Number(u.total || 0) || (Number(u.input || 0) + Number(u.cached || 0) + Number(u.cacheWrite || 0) + Number(u.output || 0));
|
|
16
|
+
|
|
17
|
+
function setFrontmatterField(content, key, value) {
|
|
18
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
19
|
+
if (!match) return content;
|
|
20
|
+
const re = new RegExp(`^${key}:.*$`, 'm');
|
|
21
|
+
const line = `${key}: ${value}`;
|
|
22
|
+
const body = re.test(match[1]) ? match[1].replace(re, line) : `${match[1]}\n${line}`;
|
|
23
|
+
return content.replace(match[0], `---\n${body}\n---`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function removeSection(content, heading, { preserveOrphanIterations = false } = {}) {
|
|
27
|
+
const start = content.indexOf(`\n${heading}`);
|
|
28
|
+
if (start < 0) return content;
|
|
29
|
+
const next = content.indexOf('\n## ', start + heading.length + 1);
|
|
30
|
+
const body = next < 0 ? content.slice(start) : content.slice(start, next);
|
|
31
|
+
const orphanAt = preserveOrphanIterations ? body.search(/\n### \d{2}:\d{2} - /) : -1;
|
|
32
|
+
const preserved = orphanAt >= 0 ? body.slice(orphanAt).trim() : '';
|
|
33
|
+
const rest = next < 0 ? '' : content.slice(next + 1).trimStart();
|
|
34
|
+
return [content.slice(0, start).trimEnd(), preserved, rest].filter(Boolean).join('\n\n').trimEnd() + '\n';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function upsertObservabilitySection(content, section) {
|
|
38
|
+
let base = content;
|
|
39
|
+
base = removeSection(base, HEADING);
|
|
40
|
+
base = removeSection(base, LEGACY_HEADINGS[0], { preserveOrphanIterations: true });
|
|
41
|
+
base = removeSection(base, LEGACY_HEADINGS[1]);
|
|
42
|
+
const anchors = ['\n## Pendências', '\n## Issues Linear', '\n## Encerramento'];
|
|
43
|
+
const indexes = anchors.map((a) => base.indexOf(a)).filter((i) => i >= 0).sort((a, b) => a - b);
|
|
44
|
+
if (!indexes.length) return `${base.trimEnd()}\n\n${section.trimEnd()}\n`;
|
|
45
|
+
const at = indexes[0];
|
|
46
|
+
return `${base.slice(0, at).trimEnd()}\n\n${section.trimEnd()}\n\n${base.slice(at).trimStart()}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function mainLedger(main) {
|
|
50
|
+
return (main.summary.modelRows || []).map((row) => ({
|
|
51
|
+
provider: row.provider || 'unknown', model: row.model || 'unknown', source: 'main',
|
|
52
|
+
effort: effort(main.summary.pensamento), calls: row.calls || 0,
|
|
53
|
+
input: row.usage.input || 0, cacheWrite: row.usage.cacheWrite || 0, cached: row.usage.cached || 0,
|
|
54
|
+
output: row.usage.output || 0, reasoning: row.usage.reasoning || 0, total: usageTotal(row.usage),
|
|
55
|
+
cost: round4(row.costs?.model || 0),
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function subagentLedger(collected) {
|
|
60
|
+
return (collected?.aggregate.modelRows || []).map((row) => ({
|
|
61
|
+
provider: row.provider || 'unknown', model: row.model || 'unknown', source: 'subagent',
|
|
62
|
+
effort: effort(row.effort), calls: row.calls || 0,
|
|
63
|
+
input: row.usage?.input || 0, cacheWrite: row.usage?.cacheWrite || 0, cached: row.usage?.cached || 0,
|
|
64
|
+
output: row.usage?.output || 0, reasoning: row.usage?.reasoning || 0, total: usageTotal(row.usage || row),
|
|
65
|
+
cost: round4(row.cost || 0),
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renderLedger(rows) {
|
|
70
|
+
if (!rows.length) return 'Nenhum modelo registrado.';
|
|
71
|
+
return ['| Modelo | Provider | Origem | Effort | Chamadas | Input | Cache W | Cache R | Output | Reasoning | Total | Custo |',
|
|
72
|
+
'|---|---|---|---|---:|---:|---:|---:|---:|---:|---:|---:|',
|
|
73
|
+
...rows.map((r) => `| ${r.model} | ${r.provider} | ${r.source} | ${r.effort} | ${fmt(r.calls)} | ${fmt(r.input)} | ${fmt(r.cacheWrite)} | ${fmt(r.cached)} | ${fmt(r.output)} | ${fmt(r.reasoning)} | ${fmt(r.total)} | ${usd(r.cost)} |`),
|
|
74
|
+
].join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderHistory(entries) {
|
|
78
|
+
if (!entries.length) return 'Nenhuma reabertura registrada.';
|
|
79
|
+
return ['| Transcript | Modelo(s) | Effort | Input | Cache W | Cache R | Output | Reasoning | Total | Custo | Atualizado |',
|
|
80
|
+
'|---|---|---|---:|---:|---:|---:|---:|---:|---:|---|',
|
|
81
|
+
...entries.map((e) => `| ${String(e.transcript_id).slice(0, 12)}… | ${(e.modelos || []).join(' + ')} | ${effort(e.pensamento)} | ${fmt(e.input)} | ${fmt(e.cache_write)} | ${fmt(e.cache_read)} | ${fmt(e.output)} | ${fmt(e.reasoning)} | ${fmt(e.total)} | ${usd(e.custo_usd)} | ${e.atualizado_em || ''} |`),
|
|
82
|
+
].join('\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderSubagents(collected) {
|
|
86
|
+
if (!collected) return '### Subagents e workflows\n\nNenhum subagent registrado.';
|
|
87
|
+
const a = collected.aggregate;
|
|
88
|
+
const workflows = collected.workflows.length
|
|
89
|
+
? collected.workflows.map((w) => `${w.name} (${w.runId}${w.status ? ` · ${w.status}` : ''} · ${w.agents} agentes · ${usd(w.cost)})`).join('; ')
|
|
90
|
+
: '(nenhum)';
|
|
91
|
+
const rows = collected.subagents.map((s) => `| ${s.id} | ${s.agentType || '-'} | ${s.workflow || '-'} | ${s.model} | ${effort(s.effort)} | ${s.tools} | ${fmt(s.tokens)} | ${usd(s.cost)} |`).join('\n');
|
|
92
|
+
return `### Subagents e workflows
|
|
93
|
+
|
|
94
|
+
- **Subagents:** ${a.count} · ${a.calls} chamadas · ${fmt(a.tokens)} tokens · ${usd(a.cost)}
|
|
95
|
+
- **Workflows:** ${workflows}
|
|
96
|
+
- **Tools:** ${(a.tools || []).join(', ') || '(nenhuma)'}${a.wasted ? `\n- **Desperdiçado:** ${usd(a.wasted)}` : ''}
|
|
97
|
+
|
|
98
|
+
#### Por subagent (${a.count})
|
|
99
|
+
|
|
100
|
+
| Agent | Tipo | Workflow | Modelo | Effort | Tools | Tokens | Custo |
|
|
101
|
+
|---|---|---|---|---|---:|---:|---:|
|
|
102
|
+
${rows}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function renderSessionObservability(snapshot) {
|
|
106
|
+
const { main, subagents, ledger } = snapshot;
|
|
107
|
+
const sub = subagents?.aggregate || { count: 0, tokens: 0, cost: 0 };
|
|
108
|
+
const combinedTokens = main.aggregate.total + sub.tokens;
|
|
109
|
+
const combinedCost = round4(main.aggregate.custo + sub.cost);
|
|
110
|
+
return `${HEADING}
|
|
111
|
+
|
|
112
|
+
> Estimativa API-equivalente baseada nos transcripts locais. Reasoning e effort são observacionais e não acrescentam tarifa separada.
|
|
113
|
+
|
|
114
|
+
| Métrica | Principal | Subagents | Total |
|
|
115
|
+
|---|---:|---:|---:|
|
|
116
|
+
| Chamadas com uso | ${fmt(main.aggregate.calls)} | ${fmt(sub.calls)} | ${fmt(main.aggregate.calls + (sub.calls || 0))} |
|
|
117
|
+
| Input tokens | ${fmt(main.aggregate.input)} | ${fmt(sub.usage?.input)} | ${fmt(main.aggregate.input + (sub.usage?.input || 0))} |
|
|
118
|
+
| Cache write | ${fmt(main.aggregate.cacheWrite)} | ${fmt(sub.usage?.cacheWrite)} | ${fmt(main.aggregate.cacheWrite + (sub.usage?.cacheWrite || 0))} |
|
|
119
|
+
| Cache read | ${fmt(main.aggregate.cached)} | ${fmt(sub.usage?.cached)} | ${fmt(main.aggregate.cached + (sub.usage?.cached || 0))} |
|
|
120
|
+
| Output tokens | ${fmt(main.aggregate.output)} | ${fmt(sub.usage?.output)} | ${fmt(main.aggregate.output + (sub.usage?.output || 0))} |
|
|
121
|
+
| Reasoning tokens | ${fmt(main.aggregate.reasoning)} | ${fmt(sub.usage?.reasoning)} | ${fmt(main.aggregate.reasoning + (sub.usage?.reasoning || 0))} |
|
|
122
|
+
| Total tokens | ${fmt(main.aggregate.total)} | ${fmt(sub.tokens)} | ${fmt(combinedTokens)} |
|
|
123
|
+
| Custo estimado | ${usd(main.aggregate.custo)} | ${usd(sub.cost)} | ${usd(combinedCost)} |
|
|
124
|
+
|
|
125
|
+
### Por modelo e origem
|
|
126
|
+
|
|
127
|
+
${renderLedger(ledger)}
|
|
128
|
+
|
|
129
|
+
### Por reabertura
|
|
130
|
+
|
|
131
|
+
${renderHistory(main.entries)}
|
|
132
|
+
|
|
133
|
+
${renderSubagents(subagents)}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function buildSessionObservability({ sessionContent, transcriptPath }) {
|
|
137
|
+
const main = collectSessionUsage({ sessionContent, transcriptPath });
|
|
138
|
+
if (!main) return null;
|
|
139
|
+
const subagents = collectSubagentUsage(sessionDirFromTranscript(transcriptPath));
|
|
140
|
+
const ledger = [...mainLedger(main), ...subagentLedger(subagents)];
|
|
141
|
+
const sub = subagents?.aggregate || { count: 0, tokens: 0, cost: 0, wasted: 0, tools: [] };
|
|
142
|
+
let content = main.content;
|
|
143
|
+
content = setFrontmatterField(content, 'subagents_count', sub.count || 0);
|
|
144
|
+
content = setFrontmatterField(content, 'subagents_tokens_total', sub.tokens || 0);
|
|
145
|
+
content = setFrontmatterField(content, 'subagents_custo_usd', sub.cost || 0);
|
|
146
|
+
content = setFrontmatterField(content, 'subagents_tools', `"${(sub.tools || []).join(', ')}"`);
|
|
147
|
+
content = setFrontmatterField(content, 'subagents_wasted_usd', sub.wasted || 0);
|
|
148
|
+
content = setFrontmatterField(content, 'tokens_total_incl_subagents', main.aggregate.total + (sub.tokens || 0));
|
|
149
|
+
content = setFrontmatterField(content, 'custo_total_incl_subagents_usd', round4(main.aggregate.custo + (sub.cost || 0)));
|
|
150
|
+
content = setFrontmatterField(content, 'observability_schema', 1);
|
|
151
|
+
content = setFrontmatterField(content, 'custo_por_modelo_json', `'${JSON.stringify(ledger).replaceAll("'", "''")}'`);
|
|
152
|
+
const snapshot = { version: 1, main, subagents, ledger };
|
|
153
|
+
return { snapshot, content: upsertObservabilitySection(content, renderSessionObservability(snapshot)) };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function updateSessionObservability({ sessionPath, transcriptPath }) {
|
|
157
|
+
if (!sessionPath || !existsSync(sessionPath)) return null;
|
|
158
|
+
const result = buildSessionObservability({ sessionContent: readFileSync(sessionPath, 'utf8'), transcriptPath });
|
|
159
|
+
if (!result) return null;
|
|
160
|
+
writeFileSync(sessionPath, result.content, 'utf8');
|
|
161
|
+
return result.snapshot;
|
|
162
|
+
}
|
package/hooks/session-stop.mjs
CHANGED
|
@@ -4,11 +4,11 @@ import { join } from 'path';
|
|
|
4
4
|
import { request } from 'http';
|
|
5
5
|
import { pathToFileURL } from 'url';
|
|
6
6
|
import { createLinkedNotes } from './linked-notes.mjs';
|
|
7
|
-
import { addUsage, costBreakdown, emptyTokenUsage, normalizeClaudeUsage, normalizeCodexUsage, priceForModel
|
|
7
|
+
import { addUsage, costBreakdown, emptyTokenUsage, normalizeClaudeUsage, normalizeCodexUsage, priceForModel } from './token-usage.mjs';
|
|
8
8
|
import { buildBrainDigest, buildBrainIndex } from './brain-core.mjs';
|
|
9
9
|
import { activeChangeLink, pruneChangeSentinels } from './change-core.mjs';
|
|
10
10
|
import { getLocale } from './locale.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import { updateSessionObservability } from './session-observability.mjs';
|
|
12
12
|
import {
|
|
13
13
|
ensureDir,
|
|
14
14
|
findActiveSessionByTranscript,
|
|
@@ -704,8 +704,9 @@ function applyDedicatedSections(content, tx) {
|
|
|
704
704
|
function insertIntoIteracoes(content, block) {
|
|
705
705
|
const iter = content.indexOf('\n## Iterações');
|
|
706
706
|
if (iter !== -1) {
|
|
707
|
-
const anchors = [
|
|
708
|
-
'\n##
|
|
707
|
+
const anchors = [
|
|
708
|
+
'\n## Agentes, tokens e custos',
|
|
709
|
+
'\n## Uso de tokens e custos',
|
|
709
710
|
'\n## Decisões geradas nesta sessão',
|
|
710
711
|
'\n## Bugs gerados nesta sessão',
|
|
711
712
|
'\n## Aprendizados gerados nesta sessão',
|
|
@@ -855,7 +856,7 @@ function replacePendingSection(content, pending) {
|
|
|
855
856
|
const end = content.indexOf(closingMarker, start + marker.length);
|
|
856
857
|
if (end === -1) return content;
|
|
857
858
|
|
|
858
|
-
// Preserva seções que outros writers inseriram dentro do span (
|
|
859
|
+
// Preserva seções que outros writers inseriram dentro do span (observabilidade,
|
|
859
860
|
// ## Progresso do plano, ## Mudanças…) — só o texto das Pendências em si é regenerado.
|
|
860
861
|
const span = content.slice(start + marker.length, end);
|
|
861
862
|
const innerIdx = span.indexOf('\n## ');
|
|
@@ -1053,24 +1054,11 @@ function main() {
|
|
|
1053
1054
|
}
|
|
1054
1055
|
|
|
1055
1056
|
try {
|
|
1056
|
-
|
|
1057
|
-
vaultBase,
|
|
1058
|
-
sessionRel,
|
|
1059
|
-
sessionPath,
|
|
1060
|
-
transcriptPath,
|
|
1061
|
-
});
|
|
1057
|
+
updateSessionObservability({ sessionPath, transcriptPath });
|
|
1062
1058
|
} catch (error) {
|
|
1063
1059
|
process.stderr.write(`[wendkeep] Token usage falhou: ${error.message}\n`);
|
|
1064
1060
|
}
|
|
1065
1061
|
|
|
1066
|
-
// Subagent/workflow telemetry (0.10.0): fold sibling subagent transcripts into the note.
|
|
1067
|
-
// Provider-gated by structure + fail-open — never derruba o Stop.
|
|
1068
|
-
try {
|
|
1069
|
-
upsertSubagentUsage(sessionPath, transcriptPath);
|
|
1070
|
-
} catch (error) {
|
|
1071
|
-
process.stderr.write(`[wendkeep] Subagent usage falhou: ${error.message}\n`);
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
1062
|
if (!shouldFinalizeSession()) {
|
|
1075
1063
|
writeControl(vaultBase, {
|
|
1076
1064
|
...control,
|
package/hooks/subagent-stop.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// SubagentStop hook: refresh this session's subagent/workflow telemetry the MOMENT a subagent
|
|
3
|
-
// finishes — not only at the main Stop.
|
|
4
|
-
//
|
|
5
|
-
// hook runs, so the output is identical; it just runs earlier + incrementally. Fail-open.
|
|
3
|
+
// finishes — not only at the main Stop. It recomposes the complete main + subagent snapshot
|
|
4
|
+
// through the same atomic writer used by Stop/import/rebuild. Fail-open.
|
|
6
5
|
//
|
|
7
6
|
// Model choice for subagents stays the harness's job (agent frontmatter `model:` / the Task/
|
|
8
7
|
// workflow `model` param). wendkeep OBSERVES (this telemetry) rather than dictating a routing rule.
|
|
@@ -10,7 +9,7 @@ import { existsSync } from 'fs';
|
|
|
10
9
|
import { join } from 'path';
|
|
11
10
|
import { pathToFileURL } from 'url';
|
|
12
11
|
import { readHookInput, writeHookOutput, getVaultBase, findActiveSessionByTranscript, readControl } from './obsidian-common.mjs';
|
|
13
|
-
import {
|
|
12
|
+
import { updateSessionObservability } from './session-observability.mjs';
|
|
14
13
|
|
|
15
14
|
export function refreshSubagents(vaultBase, input) {
|
|
16
15
|
const transcriptPath = input.transcript_path || input.transcriptPath || '';
|
|
@@ -19,7 +18,7 @@ export function refreshSubagents(vaultBase, input) {
|
|
|
19
18
|
if (!sessionRel) return false;
|
|
20
19
|
const sessionPath = join(vaultBase, sessionRel);
|
|
21
20
|
if (!existsSync(sessionPath)) return false;
|
|
22
|
-
|
|
21
|
+
updateSessionObservability({ sessionPath, transcriptPath });
|
|
23
22
|
return true;
|
|
24
23
|
}
|
|
25
24
|
|
package/hooks/subagent-usage.mjs
CHANGED
|
@@ -69,7 +69,7 @@ function readWorkflowRuns(sessionDir) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function tokensTotal(t = {}) {
|
|
72
|
-
return (t.input || 0) + (t.cached || 0) + (t.cacheWrite || 0) + (t.output || 0);
|
|
72
|
+
return Number(t.total || 0) || ((t.input || 0) + (t.cached || 0) + (t.cacheWrite || 0) + (t.output || 0));
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const round4 = (n) => Math.round((Number(n) || 0) * 10000) / 10000;
|
|
@@ -87,7 +87,7 @@ export function collectSubagentUsage(sessionDir) {
|
|
|
87
87
|
const subagents = [];
|
|
88
88
|
const wf = {};
|
|
89
89
|
const allTools = new Set();
|
|
90
|
-
const usageAgg = { input: 0, cached: 0, cacheWrite: 0, output: 0 };
|
|
90
|
+
const usageAgg = { input: 0, cached: 0, cacheWrite: 0, output: 0, reasoning: 0, total: 0 };
|
|
91
91
|
let count = 0;
|
|
92
92
|
let calls = 0;
|
|
93
93
|
let cost = 0;
|
|
@@ -108,6 +108,7 @@ export function collectSubagentUsage(sessionDir) {
|
|
|
108
108
|
agentType,
|
|
109
109
|
workflow,
|
|
110
110
|
model: summary.models[0] || '?',
|
|
111
|
+
effort: summary.pensamento || '',
|
|
111
112
|
tools: summary.tools.length,
|
|
112
113
|
toolNames: summary.tools,
|
|
113
114
|
calls: summary.calls,
|
|
@@ -117,11 +118,14 @@ export function collectSubagentUsage(sessionDir) {
|
|
|
117
118
|
});
|
|
118
119
|
|
|
119
120
|
for (const row of summary.modelRows || []) {
|
|
120
|
-
const
|
|
121
|
-
const
|
|
121
|
+
const rowEffort = summary.pensamento || '';
|
|
122
|
+
const key = `${row.provider || '?'}\u0000${row.model || '?'}\u0000${rowEffort}`;
|
|
123
|
+
const current = modelMap.get(key) || { provider: row.provider || '?', model: row.model || '?', effort: rowEffort, calls: 0, tokens: 0, cost: 0,
|
|
124
|
+
usage: { input: 0, cached: 0, cacheWrite: 0, output: 0, reasoning: 0, total: 0 } };
|
|
122
125
|
current.calls += row.calls || 0;
|
|
123
126
|
current.tokens += tokensTotal(row.usage);
|
|
124
127
|
current.cost += row.costs?.model || 0;
|
|
128
|
+
for (const k of Object.keys(current.usage)) current.usage[k] += row.usage?.[k] || 0;
|
|
125
129
|
modelMap.set(key, current);
|
|
126
130
|
}
|
|
127
131
|
|
package/hooks/token-usage.mjs
CHANGED
|
@@ -153,7 +153,16 @@ const MANAGED_FRONTMATTER_KEYS = new Set([
|
|
|
153
153
|
'custo_modelo_label',
|
|
154
154
|
'custo_modelo_usd',
|
|
155
155
|
'custo_por_modelo',
|
|
156
|
-
'usage_por_transcript',
|
|
156
|
+
'usage_por_transcript',
|
|
157
|
+
'subagents_count',
|
|
158
|
+
'subagents_tokens_total',
|
|
159
|
+
'subagents_custo_usd',
|
|
160
|
+
'subagents_tools',
|
|
161
|
+
'subagents_wasted_usd',
|
|
162
|
+
'tokens_total_incl_subagents',
|
|
163
|
+
'custo_total_incl_subagents_usd',
|
|
164
|
+
'observability_schema',
|
|
165
|
+
'custo_por_modelo_json',
|
|
157
166
|
// Legado: chaves antigas removidas ao reprocessar a sessão.
|
|
158
167
|
'custo_estimado_gpt55_usd',
|
|
159
168
|
'custo_estimado_opus47_usd',
|
|
@@ -442,8 +451,9 @@ function parseClaudeLines(lines, result) {
|
|
|
442
451
|
result.provider = 'anthropic';
|
|
443
452
|
const seenUsage = new Set();
|
|
444
453
|
const seenTools = new Set();
|
|
445
|
-
const seenThinking = new Set();
|
|
446
|
-
let thinkingChars = 0;
|
|
454
|
+
const seenThinking = new Set();
|
|
455
|
+
let thinkingChars = 0;
|
|
456
|
+
const thinkingCharsByModel = new Map();
|
|
447
457
|
let latestPrompt = '';
|
|
448
458
|
|
|
449
459
|
for (const line of lines) {
|
|
@@ -476,8 +486,9 @@ function parseClaudeLines(lines, result) {
|
|
|
476
486
|
if (block?.type === 'thinking' && block.thinking) {
|
|
477
487
|
const thinkKey = `${msg.id || ''}:${block.thinking.slice(0, 60)}`;
|
|
478
488
|
if (!seenThinking.has(thinkKey)) {
|
|
479
|
-
seenThinking.add(thinkKey);
|
|
480
|
-
thinkingChars += block.thinking.length;
|
|
489
|
+
seenThinking.add(thinkKey);
|
|
490
|
+
thinkingChars += block.thinking.length;
|
|
491
|
+
thinkingCharsByModel.set(model, (thinkingCharsByModel.get(model) || 0) + block.thinking.length);
|
|
481
492
|
}
|
|
482
493
|
}
|
|
483
494
|
}
|
|
@@ -502,10 +513,14 @@ function parseClaudeLines(lines, result) {
|
|
|
502
513
|
// Thinking estimado: ~3,5 chars por token. Distribuído no total como informação à parte
|
|
503
514
|
// (já contido em output_tokens — não somar de novo).
|
|
504
515
|
const thinkingTokens = Math.round(thinkingChars / 3.5);
|
|
505
|
-
if (thinkingTokens > 0) {
|
|
506
|
-
result.totals.reasoning = thinkingTokens;
|
|
507
|
-
result.pensamento = `thinking ~${formatTokensShort(thinkingTokens)}`;
|
|
508
|
-
|
|
516
|
+
if (thinkingTokens > 0) {
|
|
517
|
+
result.totals.reasoning = thinkingTokens;
|
|
518
|
+
result.pensamento = `thinking ~${formatTokensShort(thinkingTokens)}`;
|
|
519
|
+
for (const [model, chars] of thinkingCharsByModel) {
|
|
520
|
+
const entry = result.byModel.get(`anthropic:${model}`);
|
|
521
|
+
if (entry) entry.usage.reasoning = Math.round(chars / 3.5);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
509
524
|
|
|
510
525
|
return result;
|
|
511
526
|
}
|
|
@@ -896,40 +911,52 @@ function legacyEntryFromNote(content, summary) {
|
|
|
896
911
|
};
|
|
897
912
|
}
|
|
898
913
|
|
|
899
|
-
export function
|
|
900
|
-
if (!
|
|
901
|
-
return null;
|
|
902
|
-
}
|
|
914
|
+
export function collectSessionUsage({ sessionContent, transcriptPath }) {
|
|
915
|
+
if (!transcriptPath || !existsSync(transcriptPath)) {
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
903
918
|
|
|
904
919
|
const parsed = parseTokenUsageFromTranscript(transcriptPath);
|
|
905
920
|
const summary = summarizeTokenUsage(parsed);
|
|
906
921
|
if (!summary.calls) return null;
|
|
907
922
|
|
|
908
|
-
const
|
|
909
|
-
const fmMatch = sessionContent.match(/^---\n([\s\S]*?)\n---/);
|
|
923
|
+
const fmMatch = sessionContent.match(/^---\n([\s\S]*?)\n---/);
|
|
910
924
|
const existingEntries = fmMatch ? parseUsageHistory(fmMatch[1]) : [];
|
|
911
925
|
|
|
912
|
-
const transcriptId = transcriptIdFromPath(transcriptPath);
|
|
913
|
-
|
|
926
|
+
const transcriptId = transcriptIdFromPath(transcriptPath);
|
|
927
|
+
const previous = existingEntries.find((entry) => entry.transcript_id === transcriptId);
|
|
928
|
+
const current = entryFromSummary(summary, transcriptId);
|
|
929
|
+
if (previous) {
|
|
930
|
+
const comparable = (entry) => JSON.stringify({ ...entry, atualizado_em: undefined });
|
|
931
|
+
if (comparable(previous) === comparable(current)) current.atualizado_em = previous.atualizado_em;
|
|
932
|
+
}
|
|
933
|
+
let entries = existingEntries.filter((e) => e.transcript_id !== transcriptId);
|
|
914
934
|
|
|
915
935
|
if (!existingEntries.length) {
|
|
916
936
|
const legacy = legacyEntryFromNote(sessionContent, summary);
|
|
917
937
|
if (legacy) entries.push(legacy);
|
|
918
938
|
}
|
|
919
939
|
|
|
920
|
-
entries.push(
|
|
921
|
-
|
|
922
|
-
const agg = aggregateEntries(entries);
|
|
923
|
-
const withFrontmatter = upsertSessionFrontmatter(sessionContent, agg, entries);
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
}
|
|
940
|
+
entries.push(current);
|
|
941
|
+
|
|
942
|
+
const agg = aggregateEntries(entries);
|
|
943
|
+
const withFrontmatter = upsertSessionFrontmatter(sessionContent, agg, entries);
|
|
944
|
+
return {
|
|
945
|
+
summary,
|
|
946
|
+
aggregate: agg,
|
|
947
|
+
entries,
|
|
948
|
+
content: withFrontmatter,
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
export function updateSessionUsage({ vaultBase, sessionRel, sessionPath, transcriptPath }) {
|
|
953
|
+
if (!sessionPath || !existsSync(sessionPath)) return null;
|
|
954
|
+
const result = collectSessionUsage({ sessionContent: readFileSync(sessionPath, 'utf-8'), transcriptPath });
|
|
955
|
+
if (!result) return null;
|
|
956
|
+
const withSection = upsertUsageSection(result.content, buildUsageSection(result.aggregate, result.entries, result.summary));
|
|
957
|
+
writeFileSync(sessionPath, withSection, 'utf-8');
|
|
958
|
+
return result;
|
|
959
|
+
}
|
|
933
960
|
|
|
934
961
|
function parseCliArgs(argv) {
|
|
935
962
|
const args = {};
|
package/hooks/vault-health.mjs
CHANGED
|
@@ -72,7 +72,9 @@ function hasDefaultPending(content) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
function usageSectionIsPlaced(content, { active = false } = {}) {
|
|
75
|
-
const
|
|
75
|
+
const unified = content.indexOf('\n## Agentes, tokens e custos');
|
|
76
|
+
const legacy = content.indexOf('\n## Uso de tokens e custos');
|
|
77
|
+
const usage = unified !== -1 ? unified : legacy;
|
|
76
78
|
if (usage === -1) return true;
|
|
77
79
|
const changed = content.indexOf('\n## Arquivos criados ou alterados');
|
|
78
80
|
const pending = content.indexOf('\n## Pendências');
|
|
@@ -108,9 +110,12 @@ function checkSession({ vaultBase, sessionRel, control, registry }) {
|
|
|
108
110
|
metrics.turnMarkers = (content.match(/<!-- (?:wk-turn|codex-turn):/g) || []).length;
|
|
109
111
|
metrics.duplicateTurnMarkers = duplicates.length;
|
|
110
112
|
|
|
111
|
-
if (duplicates.length) failures.push(`Marcadores de turno duplicados: ${duplicates.join(', ')}`);
|
|
112
|
-
if (hasHeadingAfterClosing(content)) failures.push('Há headings/iterações após ## Encerramento.');
|
|
113
|
-
if (!usageSectionIsPlaced(content, { active: activeSession })) failures.push('
|
|
113
|
+
if (duplicates.length) failures.push(`Marcadores de turno duplicados: ${duplicates.join(', ')}`);
|
|
114
|
+
if (hasHeadingAfterClosing(content)) failures.push('Há headings/iterações após ## Encerramento.');
|
|
115
|
+
if (!usageSectionIsPlaced(content, { active: activeSession })) failures.push('A seção de agentes, tokens e custos está fora da posição esperada.');
|
|
116
|
+
if (content.includes('\n## Agentes, tokens e custos') && (content.includes('\n## Uso de tokens e custos') || content.includes('\n## Subagents & Workflows'))) {
|
|
117
|
+
failures.push('A sessão mistura observabilidade consolidada e seções legadas.');
|
|
118
|
+
}
|
|
114
119
|
if (hasDefaultPending(content)) warnings.push('Pendências ainda contém placeholders padrão.');
|
|
115
120
|
|
|
116
121
|
const registryEntry = registry.sessions?.[control.session_id];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/rebuild-costs.mjs
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { readSessionRegistry } from '../hooks/obsidian-common.mjs';
|
|
6
|
-
import {
|
|
7
|
-
import { upsertSubagentUsage } from '../hooks/subagent-usage.mjs';
|
|
6
|
+
import { updateSessionObservability } from '../hooks/session-observability.mjs';
|
|
8
7
|
|
|
9
8
|
export function rebuildSessionCosts(vaultBase, { apply = false, session = '', limit = 0 } = {}) {
|
|
10
9
|
const registry = readSessionRegistry(vaultBase);
|
|
@@ -22,8 +21,7 @@ export function rebuildSessionCosts(vaultBase, { apply = false, session = '', li
|
|
|
22
21
|
}
|
|
23
22
|
const before = readFileSync(note, 'utf8');
|
|
24
23
|
try {
|
|
25
|
-
|
|
26
|
-
upsertSubagentUsage(note, entry.transcript_path);
|
|
24
|
+
updateSessionObservability({ sessionPath: note, transcriptPath: entry.transcript_path });
|
|
27
25
|
const after = readFileSync(note, 'utf8');
|
|
28
26
|
const changed = before !== after;
|
|
29
27
|
if (changed) report.changed += 1; else report.unchanged += 1;
|