wendkeep 0.11.0 → 0.13.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 +25 -0
- package/hooks/subagent-usage.mjs +66 -8
- package/package.json +1 -1
- package/src/cost.mjs +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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.13.0] — 2026-07-06
|
|
8
|
+
|
|
9
|
+
Cost intelligence: waste + average.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Wasted-spend tracking:** a killed/failed workflow run's subagent cost is now recorded per
|
|
13
|
+
session (`subagents_wasted_usd` + a line in the note's `## Subagents & Workflows`) and rolled
|
|
14
|
+
up by `wendkeep cost` (`desperdiçado (runs killed/failed): $X`). Money burned on aborted runs
|
|
15
|
+
was invisible before.
|
|
16
|
+
- **`wendkeep cost` per-session average** (`$/sessão`) alongside the vault total.
|
|
17
|
+
|
|
18
|
+
## [0.12.0] — 2026-07-06
|
|
19
|
+
|
|
20
|
+
Deeper subagent/workflow telemetry.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **Workflow run metadata** in the `## Subagents & Workflows` section: each run now shows its
|
|
24
|
+
**status** (completed / killed / …), phase titles, duration and agent count — read from the
|
|
25
|
+
authoritative `workflows/wf_*.json`. On a real session this surfaced a **killed** run that
|
|
26
|
+
still cost $2.50 next to the completed $5.76 one — wasted spend you couldn't see before.
|
|
27
|
+
- **Subagent tools rollup:** the distinct tools the subagents used, shown in the section and a
|
|
28
|
+
new `subagents_tools` frontmatter field.
|
|
29
|
+
|
|
7
30
|
## [0.11.0] — 2026-07-06
|
|
8
31
|
|
|
9
32
|
Vault-wide cost.
|
|
@@ -249,6 +272,8 @@ Initial release — the capture engine, extracted from a system in daily product
|
|
|
249
272
|
- `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
|
|
250
273
|
|
|
251
274
|
<!-- Only v0.4.0+ is tagged in git (history starts here); older versions link to npm. -->
|
|
275
|
+
[0.13.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.13.0
|
|
276
|
+
[0.12.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.12.0
|
|
252
277
|
[0.11.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.11.0
|
|
253
278
|
[0.10.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.10.0
|
|
254
279
|
[0.9.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.9.1
|
package/hooks/subagent-usage.mjs
CHANGED
|
@@ -45,6 +45,29 @@ function runIdOfPath(file) {
|
|
|
45
45
|
return m ? m[1] : null;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// workflows/wf_<rid>.json carries authoritative run metadata (name/status/phases/duration).
|
|
49
|
+
function readWorkflowRuns(sessionDir) {
|
|
50
|
+
const runs = {};
|
|
51
|
+
let names;
|
|
52
|
+
try { names = readdirSync(join(sessionDir, 'workflows')); } catch { return runs; }
|
|
53
|
+
for (const n of names) {
|
|
54
|
+
if (!/^wf_.*\.json$/i.test(n)) continue;
|
|
55
|
+
try {
|
|
56
|
+
const o = JSON.parse(readFileSync(join(sessionDir, 'workflows', n), 'utf8'));
|
|
57
|
+
const runId = o.runId || n.replace(/\.json$/i, '');
|
|
58
|
+
runs[runId] = {
|
|
59
|
+
name: o.workflowName || runId,
|
|
60
|
+
status: o.status || '',
|
|
61
|
+
agentCount: o.agentCount || 0,
|
|
62
|
+
totalTokens: o.totalTokens || 0,
|
|
63
|
+
durationMs: o.durationMs || 0,
|
|
64
|
+
phases: (o.phases || []).map((p) => p && p.title).filter(Boolean),
|
|
65
|
+
};
|
|
66
|
+
} catch { /* skip bad run json */ }
|
|
67
|
+
}
|
|
68
|
+
return runs;
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
function tokensTotal(t = {}) {
|
|
49
72
|
return (t.input || 0) + (t.cached || 0) + (t.cacheWrite || 0) + (t.output || 0);
|
|
50
73
|
}
|
|
@@ -60,8 +83,10 @@ export function collectSubagentUsage(sessionDir) {
|
|
|
60
83
|
if (!files.length) return null;
|
|
61
84
|
|
|
62
85
|
const nameMap = workflowNameMap(sessionDir);
|
|
86
|
+
const runs = readWorkflowRuns(sessionDir);
|
|
63
87
|
const subagents = [];
|
|
64
88
|
const wf = {};
|
|
89
|
+
const allTools = new Set();
|
|
65
90
|
const usageAgg = { input: 0, cached: 0, cacheWrite: 0, output: 0 };
|
|
66
91
|
let count = 0;
|
|
67
92
|
let calls = 0;
|
|
@@ -71,10 +96,11 @@ export function collectSubagentUsage(sessionDir) {
|
|
|
71
96
|
const summary = summarizeTokenUsage(parseTokenUsageFromTranscript(f));
|
|
72
97
|
if (!summary.calls) continue;
|
|
73
98
|
const runId = runIdOfPath(f);
|
|
74
|
-
const workflow = runId ? nameMap[runId] || runId : null;
|
|
99
|
+
const workflow = runId ? (runs[runId] && runs[runId].name) || nameMap[runId] || runId : null;
|
|
75
100
|
let agentType = '';
|
|
76
101
|
try { agentType = JSON.parse(readFileSync(f.replace(/\.jsonl$/i, '.meta.json'), 'utf8')).agentType || ''; } catch { /* no meta */ }
|
|
77
102
|
const tokens = tokensTotal(summary.totals);
|
|
103
|
+
for (const t of summary.tools) allTools.add(t);
|
|
78
104
|
|
|
79
105
|
subagents.push({
|
|
80
106
|
id: basename(f).replace(/^agent-/, '').replace(/\.jsonl$/i, '').slice(0, 12),
|
|
@@ -82,6 +108,7 @@ export function collectSubagentUsage(sessionDir) {
|
|
|
82
108
|
workflow,
|
|
83
109
|
model: summary.models[0] || '?',
|
|
84
110
|
tools: summary.tools.length,
|
|
111
|
+
toolNames: summary.tools,
|
|
85
112
|
calls: summary.calls,
|
|
86
113
|
tokens,
|
|
87
114
|
cost: round4(summary.costs.model),
|
|
@@ -99,31 +126,60 @@ export function collectSubagentUsage(sessionDir) {
|
|
|
99
126
|
}
|
|
100
127
|
if (!count) return null;
|
|
101
128
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
129
|
+
// Merge transcript-derived cost with the authoritative run metadata (status/phases/duration).
|
|
130
|
+
const runIds = new Set([...Object.keys(wf), ...Object.keys(runs)]);
|
|
131
|
+
const workflows = [...runIds].map((runId) => {
|
|
132
|
+
const t = wf[runId] || { agents: 0, cost: 0 };
|
|
133
|
+
const r = runs[runId] || {};
|
|
134
|
+
return {
|
|
135
|
+
runId,
|
|
136
|
+
name: r.name || nameMap[runId] || runId,
|
|
137
|
+
status: r.status || '',
|
|
138
|
+
agents: r.agentCount || t.agents,
|
|
139
|
+
cost: round4(t.cost),
|
|
140
|
+
totalTokens: r.totalTokens || 0,
|
|
141
|
+
durationMs: r.durationMs || 0,
|
|
142
|
+
phases: r.phases || [],
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Wasted spend: cost of workflow runs that did not complete (killed/failed/…). The
|
|
147
|
+
// subagents that ran before the kill still cost money — this makes that visible.
|
|
148
|
+
const WASTE = /^(killed|failed|error|aborted|cancel(l)?ed)$/i;
|
|
149
|
+
const wasted = round4(workflows.filter((w) => WASTE.test(w.status)).reduce((s, w) => s + w.cost, 0));
|
|
105
150
|
|
|
106
151
|
return {
|
|
107
152
|
subagents,
|
|
108
153
|
workflows,
|
|
109
|
-
aggregate: { count, calls, tokens: tokensTotal(usageAgg), cost: round4(cost), usage: usageAgg },
|
|
154
|
+
aggregate: { count, calls, tokens: tokensTotal(usageAgg), cost: round4(cost), wasted, usage: usageAgg, tools: [...allTools] },
|
|
110
155
|
};
|
|
111
156
|
}
|
|
112
157
|
|
|
158
|
+
function workflowLine(w) {
|
|
159
|
+
const parts = [w.runId];
|
|
160
|
+
if (w.status) parts.push(w.status);
|
|
161
|
+
parts.push(`${w.agents} agentes`);
|
|
162
|
+
if (w.phases && w.phases.length) parts.push(`fases: ${w.phases.join(', ')}`);
|
|
163
|
+
if (w.durationMs) parts.push(`${Math.round(w.durationMs / 1000)}s`);
|
|
164
|
+
parts.push(usd(w.cost));
|
|
165
|
+
return `${w.name} (${parts.join(' · ')})`;
|
|
166
|
+
}
|
|
167
|
+
|
|
113
168
|
export function renderSubagentSection(c) {
|
|
114
169
|
const a = c.aggregate;
|
|
115
|
-
const wf = c.workflows.length
|
|
116
|
-
|
|
117
|
-
: '(nenhum)';
|
|
170
|
+
const wf = c.workflows.length ? c.workflows.map(workflowLine).join('; ') : '(nenhum)';
|
|
171
|
+
const tools = a.tools && a.tools.length ? a.tools.join(', ') : '(nenhuma)';
|
|
118
172
|
const rows = c.subagents
|
|
119
173
|
.map((s) => `| ${s.id} | ${s.agentType || '-'} | ${s.workflow || '-'} | ${s.model} | ${s.tools} | ${fmt(s.tokens)} | ${usd(s.cost)} |`)
|
|
120
174
|
.join('\n');
|
|
175
|
+
const wasteLine = a.wasted ? `\n- **Desperdiçado (runs killed/failed):** ${usd(a.wasted)}` : '';
|
|
121
176
|
return `## Subagents & Workflows
|
|
122
177
|
|
|
123
178
|
> Custo de subagents/workflows desta sessão — NÃO incluído no total principal acima.
|
|
124
179
|
|
|
125
180
|
- **Subagents:** ${a.count} · ${a.calls} chamadas · ${fmt(a.tokens)} tokens · ${usd(a.cost)}
|
|
126
181
|
- **Workflows:** ${wf}
|
|
182
|
+
- **Tools (subagents):** ${tools}${wasteLine}
|
|
127
183
|
|
|
128
184
|
<details><summary>Por subagent (${a.count})</summary>
|
|
129
185
|
|
|
@@ -169,6 +225,8 @@ export function upsertSubagentUsage(sessionPath, transcriptPath) {
|
|
|
169
225
|
content = setFrontmatterField(content, 'subagents_count', a.count);
|
|
170
226
|
content = setFrontmatterField(content, 'subagents_tokens_total', a.tokens);
|
|
171
227
|
content = setFrontmatterField(content, 'subagents_custo_usd', a.cost);
|
|
228
|
+
content = setFrontmatterField(content, 'subagents_tools', `"${(a.tools || []).join(', ')}"`);
|
|
229
|
+
content = setFrontmatterField(content, 'subagents_wasted_usd', a.wasted || 0);
|
|
172
230
|
content = setFrontmatterField(content, 'tokens_total_incl_subagents', frontmatterNumber(content, 'tokens_total') + a.tokens);
|
|
173
231
|
content = upsertSection(content, '## Subagents & Workflows', renderSubagentSection(collected));
|
|
174
232
|
writeFileSync(sessionPath, content, 'utf8');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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/cost.mjs
CHANGED
|
@@ -21,6 +21,7 @@ export function parseSessionCost(content) {
|
|
|
21
21
|
model: fmValue(content, 'custo_modelo_label') || fmValue(content, 'modelo') || '?',
|
|
22
22
|
mainCost: Number(fmValue(content, 'custo_modelo_usd')) || 0,
|
|
23
23
|
subCost: Number(fmValue(content, 'subagents_custo_usd')) || 0,
|
|
24
|
+
wasted: Number(fmValue(content, 'subagents_wasted_usd')) || 0,
|
|
24
25
|
tokens: Number(fmValue(content, 'tokens_total')) || 0,
|
|
25
26
|
subTokens: Number(fmValue(content, 'subagents_tokens_total')) || 0,
|
|
26
27
|
};
|
|
@@ -31,19 +32,22 @@ export function aggregateCosts(entries) {
|
|
|
31
32
|
const byModel = {};
|
|
32
33
|
let main = 0;
|
|
33
34
|
let sub = 0;
|
|
35
|
+
let wasted = 0;
|
|
34
36
|
let tokens = 0;
|
|
35
37
|
let subTokens = 0;
|
|
36
38
|
for (const e of entries) {
|
|
37
|
-
main += e.mainCost; sub += e.subCost; tokens += e.tokens; subTokens += e.subTokens;
|
|
39
|
+
main += e.mainCost; sub += e.subCost; wasted += e.wasted || 0; tokens += e.tokens; subTokens += e.subTokens;
|
|
38
40
|
const d = e.date || '?';
|
|
39
41
|
(byDay[d] = byDay[d] || { cost: 0, count: 0 }).cost += e.mainCost + e.subCost;
|
|
40
42
|
byDay[d].count += 1;
|
|
41
43
|
(byModel[e.model] = byModel[e.model] || { cost: 0, count: 0 }).cost += e.mainCost + e.subCost;
|
|
42
44
|
byModel[e.model].count += 1;
|
|
43
45
|
}
|
|
46
|
+
const total = main + sub;
|
|
44
47
|
return {
|
|
45
48
|
count: entries.length,
|
|
46
|
-
main: round4(main), sub: round4(sub), total: round4(
|
|
49
|
+
main: round4(main), sub: round4(sub), total: round4(total), wasted: round4(wasted),
|
|
50
|
+
avg: round4(entries.length ? total / entries.length : 0),
|
|
47
51
|
tokens, subTokens,
|
|
48
52
|
byDay: Object.entries(byDay).sort().map(([date, v]) => ({ date, cost: round4(v.cost), count: v.count })),
|
|
49
53
|
byModel: Object.entries(byModel).sort((a, b) => b[1].cost - a[1].cost).map(([model, v]) => ({ model, cost: round4(v.cost), count: v.count })),
|
|
@@ -93,8 +97,9 @@ export function runCost(argv) {
|
|
|
93
97
|
|
|
94
98
|
if (argv.includes('--json')) { process.stdout.write(`${JSON.stringify(agg, null, 2)}\n`); process.exit(0); }
|
|
95
99
|
|
|
96
|
-
process.stdout.write(`Custo total (vault): ${usd(agg.total)} — ${agg.count} sessão(ões)\n`);
|
|
100
|
+
process.stdout.write(`Custo total (vault): ${usd(agg.total)} — ${agg.count} sessão(ões) · ${usd(agg.avg)}/sessão\n`);
|
|
97
101
|
process.stdout.write(` main: ${usd(agg.main)} · subagents: ${usd(agg.sub)}\n`);
|
|
102
|
+
if (agg.wasted) process.stdout.write(` desperdiçado (runs killed/failed): ${usd(agg.wasted)}\n`);
|
|
98
103
|
if (agg.byModel.length) {
|
|
99
104
|
process.stdout.write('\nPor modelo:\n');
|
|
100
105
|
for (const m of agg.byModel) process.stdout.write(` ${m.model.padEnd(20)} ${usd(m.cost)} (${m.count})\n`);
|