wendkeep 0.66.4 → 0.67.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 +50 -0
- package/README.en.md +78 -5
- package/README.md +78 -5
- package/docs/en/commands/costs-and-observability.md +21 -7
- package/docs/en/commands/maintenance-and-diagnostics.md +13 -1
- package/docs/en/commands/operating-profiles.md +65 -10
- package/docs/en/commands/sessions-and-import.md +22 -1
- package/docs/en/commands/verify.md +5 -3
- package/docs/pt-BR/commands/costs-and-observability.md +21 -7
- package/docs/pt-BR/commands/maintenance-and-diagnostics.md +12 -1
- package/docs/pt-BR/commands/operating-profiles.md +66 -11
- package/docs/pt-BR/commands/sessions-and-import.md +20 -0
- package/docs/pt-BR/commands/verify.md +6 -3
- package/hooks/change-nag.mjs +8 -0
- package/hooks/codex-rollout-meta.mjs +112 -0
- package/hooks/codex-subagent-graph.mjs +903 -0
- package/hooks/harness-doctor.mjs +82 -1
- package/hooks/import-sessions.mjs +185 -50
- package/hooks/operating-profile-runtime.mjs +36 -2
- package/hooks/operating-profile-task-store.mjs +77 -0
- package/hooks/session-identity.mjs +40 -5
- package/hooks/session-observability-lifecycle.mjs +129 -0
- package/hooks/session-observability-state.mjs +241 -0
- package/hooks/session-observability-store.mjs +436 -0
- package/hooks/session-observability.mjs +647 -21
- package/hooks/session-stop.mjs +339 -11
- package/hooks/subagent-stop.mjs +266 -12
- package/hooks/subagent-usage.mjs +65 -0
- package/hooks/token-usage.mjs +81 -4
- package/package.json +3 -3
- package/packages/harness/src/operating-profile.mjs +127 -0
- package/packages/harness/src/sensors-core.mjs +41 -1
- package/packages/integrations/src/prompt-content.mjs +123 -0
- package/packages/integrations/src/transcripts.mjs +16 -10
- package/src/cost.mjs +40 -6
- package/src/doctor.mjs +4 -1
- package/src/profile.mjs +95 -17
- package/src/rebuild-costs.mjs +220 -34
- package/src/skills-seed.mjs +38 -2
- package/src/sync-defs.mjs +6 -1
package/hooks/subagent-stop.mjs
CHANGED
|
@@ -8,29 +8,283 @@
|
|
|
8
8
|
import { existsSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
import { pathToFileURL } from 'url';
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
getVaultBase,
|
|
13
|
+
mutateSessionRegistry,
|
|
14
|
+
providerMeta,
|
|
15
|
+
readHookInput,
|
|
16
|
+
writeHookOutput,
|
|
17
|
+
} from './obsidian-common.mjs';
|
|
18
|
+
import { materializeSessionObservability } from './session-observability.mjs';
|
|
13
19
|
import { resolveSessionEntry } from './session-identity.mjs';
|
|
20
|
+
import { readCodexRolloutMeta } from './codex-rollout-meta.mjs';
|
|
21
|
+
import { resolveObservabilityRoots } from './session-observability-lifecycle.mjs';
|
|
22
|
+
import {
|
|
23
|
+
readObservabilityStore,
|
|
24
|
+
markObservabilityCheckpoint,
|
|
25
|
+
recordObservabilitySignal,
|
|
26
|
+
releaseObservabilityLease,
|
|
27
|
+
tryAcquireObservabilityLease,
|
|
28
|
+
} from './session-observability-store.mjs';
|
|
14
29
|
|
|
15
|
-
|
|
16
|
-
|
|
30
|
+
const SUBAGENT_COALESCE_MS = 250;
|
|
31
|
+
const SUBAGENT_DEADLINE_MS = 15_000;
|
|
32
|
+
|
|
33
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
34
|
+
|
|
35
|
+
function causalSnapshot(entry) {
|
|
36
|
+
const activationId = String(entry?.active_activation_id || '');
|
|
37
|
+
const activation = entry?.activations?.[activationId] || {};
|
|
38
|
+
return {
|
|
39
|
+
activationId,
|
|
40
|
+
activationEpoch: Number(activation.epoch || entry?.activation_epoch || 0),
|
|
41
|
+
turnSequence: Number(entry?.last_turn_sequence || activation.last_turn_sequence || 0),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sameCausalSnapshot(left, right) {
|
|
46
|
+
return left.activationId === right.activationId
|
|
47
|
+
&& left.activationEpoch === right.activationEpoch
|
|
48
|
+
&& left.turnSequence === right.turnSequence;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function defaultMaterialize(request) {
|
|
52
|
+
return materializeSessionObservability({
|
|
53
|
+
vaultBase: request.vaultBase,
|
|
54
|
+
sessionPath: request.sessionPath,
|
|
55
|
+
transcriptPath: request.transcriptPath,
|
|
56
|
+
entry: request.entry,
|
|
57
|
+
canonicalConversationId: request.canonicalConversationId,
|
|
58
|
+
frontier: request.frontier,
|
|
59
|
+
signals: request.signals,
|
|
60
|
+
cache: request.cache,
|
|
61
|
+
mode: 'live',
|
|
62
|
+
deadlineAt: request.deadlineAt,
|
|
63
|
+
now: request.now,
|
|
64
|
+
allowNone: request.allowNone,
|
|
65
|
+
readRuntimeFrontier: request.readRuntimeFrontier,
|
|
66
|
+
withPublicationGuard: request.withPublicationGuard,
|
|
67
|
+
writeRegistryCheckpoint: request.writeRegistryCheckpoint,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function claudeRoots(entry) {
|
|
72
|
+
const paths = new Set();
|
|
73
|
+
const add = (value) => {
|
|
74
|
+
if (typeof value === 'string' && value.trim()) paths.add(value.trim());
|
|
75
|
+
};
|
|
76
|
+
add(entry?.transcript_path);
|
|
77
|
+
for (const path of entry?.transcript_paths || []) add(path);
|
|
78
|
+
for (const activation of Object.values(entry?.activations || {})) {
|
|
79
|
+
add(activation?.transcript_path);
|
|
80
|
+
for (const path of activation?.transcript_paths || []) add(path);
|
|
81
|
+
}
|
|
82
|
+
return { state: 'complete', rootPaths: [...paths], descendantPaths: [], diagnostics: [] };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function refreshSubagents(vaultBase, input, {
|
|
86
|
+
now = Date.now,
|
|
87
|
+
hookStartedAt = now(),
|
|
88
|
+
sleep = wait,
|
|
89
|
+
coalesceMs = SUBAGENT_COALESCE_MS,
|
|
90
|
+
deadlineMs = SUBAGENT_DEADLINE_MS,
|
|
91
|
+
resolveEntry = resolveSessionEntry,
|
|
92
|
+
readMeta = readCodexRolloutMeta,
|
|
93
|
+
mutateRegistry = mutateSessionRegistry,
|
|
94
|
+
recordSignal = recordObservabilitySignal,
|
|
95
|
+
readStore = readObservabilityStore,
|
|
96
|
+
acquireLease = tryAcquireObservabilityLease,
|
|
97
|
+
releaseLease = releaseObservabilityLease,
|
|
98
|
+
resolveRoots = resolveObservabilityRoots,
|
|
99
|
+
materialize = defaultMaterialize,
|
|
100
|
+
} = {}) {
|
|
101
|
+
const deadlineAt = hookStartedAt + deadlineMs;
|
|
102
|
+
const provider = providerMeta(input.provider).id;
|
|
103
|
+
const { identity, entry } = resolveEntry(vaultBase, input, provider);
|
|
17
104
|
if (identity.state !== 'resolved') return false;
|
|
18
|
-
const
|
|
105
|
+
const childTranscriptPath = identity.transcriptPath;
|
|
19
106
|
const sessionRel = entry?.session_file || '';
|
|
20
107
|
if (!sessionRel) return false;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
108
|
+
|
|
109
|
+
let childParentThreadId = '';
|
|
110
|
+
if (identity.provider === 'codex') {
|
|
111
|
+
const childMeta = readMeta(childTranscriptPath);
|
|
112
|
+
if (!childMeta?.ok || !childMeta.meta?.source?.subagent) return false;
|
|
113
|
+
if (!childMeta.meta.id || childMeta.meta.id !== identity.transcriptId) return false;
|
|
114
|
+
if (childMeta.meta.session_id
|
|
115
|
+
&& childMeta.meta.session_id !== identity.canonicalConversationId) return false;
|
|
116
|
+
childParentThreadId = String(
|
|
117
|
+
childMeta.meta.parent_thread_id
|
|
118
|
+
|| childMeta.meta.source?.subagent?.thread_spawn?.parent_thread_id
|
|
119
|
+
|| '',
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const observed = causalSnapshot(entry);
|
|
124
|
+
const signal = mutateRegistry(vaultBase, (registry) => {
|
|
125
|
+
const current = registry.sessions?.[identity.canonicalConversationId];
|
|
126
|
+
if (!current || current.session_file !== sessionRel
|
|
127
|
+
|| !sameCausalSnapshot(observed, causalSnapshot(current))) return null;
|
|
128
|
+
const recorded = recordSignal(vaultBase, identity.canonicalConversationId, {
|
|
129
|
+
rollout_id: identity.transcriptId,
|
|
130
|
+
transcript_path: childTranscriptPath,
|
|
131
|
+
parent_thread_id: childParentThreadId,
|
|
132
|
+
kind: 'started',
|
|
133
|
+
activation_id: observed.activationId,
|
|
134
|
+
activation_epoch: observed.activationEpoch,
|
|
135
|
+
turn_sequence: observed.turnSequence,
|
|
136
|
+
});
|
|
137
|
+
if (!recorded?.state) return recorded;
|
|
138
|
+
registry.sessions[identity.canonicalConversationId] = {
|
|
139
|
+
...current,
|
|
140
|
+
observability_signal_sequence: recorded.sequence,
|
|
141
|
+
observability_checkpoint_sequence: Number(
|
|
142
|
+
recorded.state.observability_checkpoint_sequence
|
|
143
|
+
?? current.observability_checkpoint_sequence
|
|
144
|
+
?? 0,
|
|
145
|
+
),
|
|
146
|
+
observability_dirty: Boolean(recorded.state.observability_dirty),
|
|
147
|
+
};
|
|
148
|
+
return recorded;
|
|
149
|
+
});
|
|
150
|
+
if (!signal?.state) return false;
|
|
151
|
+
if (!signal.state.observability_dirty) return true;
|
|
152
|
+
|
|
153
|
+
await sleep(coalesceMs);
|
|
154
|
+
if (now() >= deadlineAt) return true;
|
|
155
|
+
|
|
156
|
+
const latest = readStore(vaultBase, identity.canonicalConversationId);
|
|
157
|
+
if (!latest?.observability_dirty
|
|
158
|
+
|| latest.observability_signal_sequence !== signal.sequence) return true;
|
|
159
|
+
|
|
160
|
+
const leaseNow = now();
|
|
161
|
+
if (leaseNow >= deadlineAt) return true;
|
|
162
|
+
const lease = acquireLease(vaultBase, identity.canonicalConversationId, {
|
|
163
|
+
signalSequence: signal.sequence,
|
|
164
|
+
now: leaseNow,
|
|
165
|
+
ttlMs: Math.max(1, deadlineAt - leaseNow),
|
|
26
166
|
});
|
|
27
|
-
return true;
|
|
167
|
+
if (!lease?.acquired) return true;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
if (now() >= deadlineAt) return true;
|
|
171
|
+
const fresh = resolveEntry(vaultBase, input, provider);
|
|
172
|
+
if (fresh.identity?.state !== 'resolved'
|
|
173
|
+
|| fresh.identity.canonicalConversationId !== identity.canonicalConversationId
|
|
174
|
+
|| !fresh.entry?.session_file
|
|
175
|
+
|| !sameCausalSnapshot(observed, causalSnapshot(fresh.entry))) return true;
|
|
176
|
+
|
|
177
|
+
const sessionPath = join(vaultBase, fresh.entry.session_file);
|
|
178
|
+
if (!existsSync(sessionPath)) return true;
|
|
179
|
+
const roots = identity.provider === 'codex'
|
|
180
|
+
? resolveRoots(fresh.entry)
|
|
181
|
+
: claudeRoots(fresh.entry);
|
|
182
|
+
if (roots?.state !== 'complete' || !roots.rootPaths?.length) return true;
|
|
183
|
+
|
|
184
|
+
const runtimeState = lease.state || latest;
|
|
185
|
+
const frontier = {
|
|
186
|
+
canonical_session_id: identity.canonicalConversationId,
|
|
187
|
+
activation_id: observed.activationId || 'legacy',
|
|
188
|
+
activation_epoch: observed.activationEpoch,
|
|
189
|
+
turn_sequence: observed.turnSequence,
|
|
190
|
+
signal_sequence: signal.sequence,
|
|
191
|
+
roots_stat_hash: 'pending',
|
|
192
|
+
graph_cursor: 'pending',
|
|
193
|
+
source_manifest_hash: 'pending',
|
|
194
|
+
};
|
|
195
|
+
const readRuntimeFrontier = (candidateFrontier, guardContext) => {
|
|
196
|
+
const currentRuntime = readStore(vaultBase, identity.canonicalConversationId);
|
|
197
|
+
const currentEntry = guardContext?.entry;
|
|
198
|
+
const currentResolved = currentEntry
|
|
199
|
+
? { identity: { state: 'resolved', canonicalConversationId: identity.canonicalConversationId }, entry: currentEntry }
|
|
200
|
+
: resolveEntry(vaultBase, input, provider);
|
|
201
|
+
if (currentResolved.identity?.state !== 'resolved'
|
|
202
|
+
|| currentResolved.identity.canonicalConversationId !== identity.canonicalConversationId
|
|
203
|
+
|| !currentResolved.entry) {
|
|
204
|
+
return { ...candidateFrontier, canonical_session_id: 'unresolved' };
|
|
205
|
+
}
|
|
206
|
+
const currentCausal = causalSnapshot(currentResolved.entry);
|
|
207
|
+
return {
|
|
208
|
+
...candidateFrontier,
|
|
209
|
+
activation_id: currentCausal.activationId || 'legacy',
|
|
210
|
+
activation_epoch: currentCausal.activationEpoch,
|
|
211
|
+
turn_sequence: currentCausal.turnSequence,
|
|
212
|
+
signal_sequence: Math.max(
|
|
213
|
+
Number(currentRuntime?.observability_signal_sequence || 0),
|
|
214
|
+
Number(currentResolved.entry.observability_signal_sequence || 0),
|
|
215
|
+
),
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
const withPublicationGuard = (_candidateFrontier, publishGuarded) => (
|
|
219
|
+
mutateRegistry(vaultBase, (registry) => publishGuarded({
|
|
220
|
+
registry,
|
|
221
|
+
entry: registry.sessions?.[identity.canonicalConversationId] || null,
|
|
222
|
+
}))
|
|
223
|
+
);
|
|
224
|
+
const writeRegistryCheckpoint = ({
|
|
225
|
+
frontier: checkpointFrontier,
|
|
226
|
+
state,
|
|
227
|
+
diagnostics,
|
|
228
|
+
snapshot,
|
|
229
|
+
}, guardContext) => {
|
|
230
|
+
const registry = guardContext?.registry;
|
|
231
|
+
const current = registry?.sessions?.[identity.canonicalConversationId];
|
|
232
|
+
if (!current) return null;
|
|
233
|
+
const currentSignal = Number(current.observability_signal_sequence || checkpointFrontier.signal_sequence);
|
|
234
|
+
registry.sessions[identity.canonicalConversationId] = {
|
|
235
|
+
...current,
|
|
236
|
+
observability_signal_sequence: currentSignal,
|
|
237
|
+
observability_checkpoint_sequence: checkpointFrontier.signal_sequence,
|
|
238
|
+
observability_dirty: currentSignal > checkpointFrontier.signal_sequence,
|
|
239
|
+
observability_checkpoint_frontier: checkpointFrontier,
|
|
240
|
+
subagents_observability_state: state,
|
|
241
|
+
subagents_diagnostics: diagnostics || [],
|
|
242
|
+
};
|
|
243
|
+
return markObservabilityCheckpoint(vaultBase, identity.canonicalConversationId, {
|
|
244
|
+
checkpointSequence: checkpointFrontier.signal_sequence,
|
|
245
|
+
frontier: checkpointFrontier,
|
|
246
|
+
sourceManifest: snapshot?.subagents?.sourceManifest,
|
|
247
|
+
graphCache: snapshot?.subagents?.cache,
|
|
248
|
+
diagnostics,
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
await Promise.resolve(materialize({
|
|
253
|
+
vaultBase,
|
|
254
|
+
sessionPath,
|
|
255
|
+
entry: fresh.entry,
|
|
256
|
+
rootPaths: roots.rootPaths,
|
|
257
|
+
transcriptPath: roots.rootPaths[0],
|
|
258
|
+
caller: 'subagent-stop',
|
|
259
|
+
canonicalConversationId: identity.canonicalConversationId,
|
|
260
|
+
activationId: observed.activationId,
|
|
261
|
+
activationEpoch: observed.activationEpoch,
|
|
262
|
+
turnSequence: observed.turnSequence,
|
|
263
|
+
signalSequence: signal.sequence,
|
|
264
|
+
deadlineAt,
|
|
265
|
+
allowNone: false,
|
|
266
|
+
frontier,
|
|
267
|
+
signals: runtimeState.signals || [],
|
|
268
|
+
cache: runtimeState.graph_cache || null,
|
|
269
|
+
now,
|
|
270
|
+
readRuntimeFrontier,
|
|
271
|
+
withPublicationGuard,
|
|
272
|
+
writeRegistryCheckpoint,
|
|
273
|
+
}));
|
|
274
|
+
return true;
|
|
275
|
+
} finally {
|
|
276
|
+
releaseLease(vaultBase, identity.canonicalConversationId, {
|
|
277
|
+
ownerToken: lease.ownerToken,
|
|
278
|
+
signalSequence: signal.sequence,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
28
281
|
}
|
|
29
282
|
|
|
30
283
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
31
284
|
try {
|
|
285
|
+
const hookStartedAt = Date.now();
|
|
32
286
|
const input = readHookInput();
|
|
33
|
-
refreshSubagents(getVaultBase(input), input);
|
|
287
|
+
await refreshSubagents(getVaultBase(input), input, { hookStartedAt });
|
|
34
288
|
writeHookOutput({});
|
|
35
289
|
} catch (error) {
|
|
36
290
|
process.stderr.write(`[wendkeep] subagent-stop falhou: ${error.message}\n`);
|
package/hooks/subagent-usage.mjs
CHANGED
|
@@ -29,6 +29,49 @@ function walkAgentJsonl(dir) {
|
|
|
29
29
|
return out;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function inspectAgentJsonlDirectory(dir) {
|
|
33
|
+
let names;
|
|
34
|
+
try {
|
|
35
|
+
names = readdirSync(dir);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return { state: error?.code === 'ENOENT' ? 'absent' : 'error', files: [] };
|
|
38
|
+
}
|
|
39
|
+
const files = [];
|
|
40
|
+
for (const name of names) {
|
|
41
|
+
const path = join(dir, name);
|
|
42
|
+
let stat;
|
|
43
|
+
try {
|
|
44
|
+
stat = statSync(path);
|
|
45
|
+
} catch {
|
|
46
|
+
return { state: 'error', files: [] };
|
|
47
|
+
}
|
|
48
|
+
if (stat.isDirectory()) {
|
|
49
|
+
const nested = inspectAgentJsonlDirectory(path);
|
|
50
|
+
if (nested.state === 'error' || nested.state === 'absent') return { state: 'error', files: [] };
|
|
51
|
+
files.push(...nested.files);
|
|
52
|
+
} else if (name.startsWith('agent-') && name.endsWith('.jsonl')) {
|
|
53
|
+
files.push(path);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { state: 'ok', files };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validJsonl(path) {
|
|
60
|
+
let lines;
|
|
61
|
+
try {
|
|
62
|
+
lines = readFileSync(path, 'utf8').split(/\r?\n/).filter((line) => line.trim());
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (!lines.length) return false;
|
|
67
|
+
try {
|
|
68
|
+
for (const line of lines) JSON.parse(line);
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
32
75
|
// workflows/scripts/<name>-wf_<rid>.js -> { wf_<rid>: <name> }
|
|
33
76
|
function workflowNameMap(sessionDir) {
|
|
34
77
|
const map = {};
|
|
@@ -276,6 +319,28 @@ export function collectSubagentUsage(sessionDir) {
|
|
|
276
319
|
};
|
|
277
320
|
}
|
|
278
321
|
|
|
322
|
+
// Schema-2 callers need to distinguish proven absence from read/parse failure. Keep the
|
|
323
|
+
// legacy aggregate API above intact while exposing a fail-closed state for observability.
|
|
324
|
+
export function collectClaudeSubagentUsageState(sessionDir) {
|
|
325
|
+
const scan = inspectAgentJsonlDirectory(join(sessionDir, 'subagents'));
|
|
326
|
+
if (scan.state === 'absent' || (scan.state === 'ok' && scan.files.length === 0)) {
|
|
327
|
+
return { state: 'none', diagnostics: [] };
|
|
328
|
+
}
|
|
329
|
+
if (scan.state !== 'ok' || scan.files.some((path) => !validJsonl(path))) {
|
|
330
|
+
return {
|
|
331
|
+
state: 'degraded',
|
|
332
|
+
diagnostics: [{ code: 'CHILD_META_INVALID', count: 1 }],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const collected = collectSubagentUsage(sessionDir);
|
|
336
|
+
return collected
|
|
337
|
+
? { ...collected, state: 'complete', diagnostics: [] }
|
|
338
|
+
: {
|
|
339
|
+
state: 'degraded',
|
|
340
|
+
diagnostics: [{ code: 'CHILD_META_INVALID', count: 1 }],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
279
344
|
function workflowLine(w) {
|
|
280
345
|
const parts = [w.runId];
|
|
281
346
|
if (w.status) parts.push(w.status);
|
package/hooks/token-usage.mjs
CHANGED
|
@@ -510,16 +510,19 @@ function detectTranscriptFormat(lines) {
|
|
|
510
510
|
return 'codex';
|
|
511
511
|
}
|
|
512
512
|
|
|
513
|
-
export function
|
|
513
|
+
export function parseTokenUsageFromContent(content, { transcriptPath = '' } = {}) {
|
|
514
514
|
const result = emptyParseResult(transcriptPath);
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
|
|
515
|
+
const lines = String(content || '').split('\n').filter(Boolean);
|
|
518
516
|
return detectTranscriptFormat(lines) === 'claude'
|
|
519
517
|
? parseClaudeLines(lines, result)
|
|
520
518
|
: parseCodexLines(lines, result);
|
|
521
519
|
}
|
|
522
520
|
|
|
521
|
+
export function parseTokenUsageFromTranscript(transcriptPath) {
|
|
522
|
+
if (!transcriptPath || !existsSync(transcriptPath)) return emptyParseResult(transcriptPath);
|
|
523
|
+
return parseTokenUsageFromContent(readFileSync(transcriptPath, 'utf-8'), { transcriptPath });
|
|
524
|
+
}
|
|
525
|
+
|
|
523
526
|
function modelCost(usage, model) {
|
|
524
527
|
const normalized = normalizeModelName(model);
|
|
525
528
|
const price = PRICE_REFERENCE[normalized];
|
|
@@ -935,6 +938,80 @@ export function collectSessionUsage({ sessionContent, transcriptPath }) {
|
|
|
935
938
|
};
|
|
936
939
|
}
|
|
937
940
|
|
|
941
|
+
function normalizedTranscriptKey(value) {
|
|
942
|
+
return String(value || '').replace(/\.jsonl?$/i, '').trim().toLowerCase();
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Rebuild the main bucket from every validated top-level rollout. Existing history is used
|
|
946
|
+
// only to preserve stable timestamps; an entry that is neither a known root nor a proven
|
|
947
|
+
// descendant makes the reconstruction fail closed instead of silently deleting a legitimate
|
|
948
|
+
// reopening.
|
|
949
|
+
export function collectSessionUsageForRoots({
|
|
950
|
+
sessionContent,
|
|
951
|
+
rootPaths = [],
|
|
952
|
+
descendantIds = [],
|
|
953
|
+
} = {}) {
|
|
954
|
+
const fmMatch = String(sessionContent || '').match(/^---\n([\s\S]*?)\n---/);
|
|
955
|
+
if (!fmMatch) return null;
|
|
956
|
+
|
|
957
|
+
const roots = [...new Set(rootPaths.filter(Boolean))].sort((a, b) => String(a).localeCompare(String(b)));
|
|
958
|
+
const rootKeys = new Set(roots.map((path) => normalizedTranscriptKey(transcriptIdFromPath(path))));
|
|
959
|
+
const descendantKeys = new Set(descendantIds.map(normalizedTranscriptKey).filter(Boolean));
|
|
960
|
+
const existingEntries = parseUsageHistory(fmMatch[1]);
|
|
961
|
+
const unresolved = existingEntries.filter((entry) => {
|
|
962
|
+
const key = normalizedTranscriptKey(entry.transcript_id);
|
|
963
|
+
return key && !rootKeys.has(key) && !descendantKeys.has(key);
|
|
964
|
+
});
|
|
965
|
+
if (unresolved.length) {
|
|
966
|
+
return {
|
|
967
|
+
state: 'degraded',
|
|
968
|
+
diagnostics: [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: unresolved.length }],
|
|
969
|
+
entries: existingEntries,
|
|
970
|
+
content: sessionContent,
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const entries = [];
|
|
975
|
+
const summaries = [];
|
|
976
|
+
for (const transcriptPath of roots) {
|
|
977
|
+
if (!existsSync(transcriptPath)) {
|
|
978
|
+
return {
|
|
979
|
+
state: 'degraded',
|
|
980
|
+
diagnostics: [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: 1 }],
|
|
981
|
+
entries: existingEntries,
|
|
982
|
+
content: sessionContent,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
const summary = summarizeTokenUsage(parseTokenUsageFromTranscript(transcriptPath));
|
|
986
|
+
if (!summary.calls) {
|
|
987
|
+
return {
|
|
988
|
+
state: 'degraded',
|
|
989
|
+
diagnostics: [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: 1 }],
|
|
990
|
+
entries: existingEntries,
|
|
991
|
+
content: sessionContent,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
const transcriptId = transcriptIdFromPath(transcriptPath);
|
|
995
|
+
const current = entryFromSummary(summary, transcriptId);
|
|
996
|
+
const previous = existingEntries.find((entry) => normalizedTranscriptKey(entry.transcript_id) === normalizedTranscriptKey(transcriptId));
|
|
997
|
+
if (previous && sameUsageData(previous, current)) current.atualizado_em = previous.atualizado_em;
|
|
998
|
+
entries.push(current);
|
|
999
|
+
summaries.push(summary);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const agg = aggregateEntries(entries);
|
|
1003
|
+
const content = upsertSessionFrontmatter(sessionContent, agg, entries);
|
|
1004
|
+
if (content === null) return null;
|
|
1005
|
+
return {
|
|
1006
|
+
state: 'complete',
|
|
1007
|
+
diagnostics: [],
|
|
1008
|
+
summaries,
|
|
1009
|
+
aggregate: agg,
|
|
1010
|
+
entries,
|
|
1011
|
+
content,
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
|
|
938
1015
|
export function updateSessionUsage({ vaultBase, sessionRel, sessionPath, transcriptPath, lockTimeoutMs }) {
|
|
939
1016
|
if (!sessionPath || !existsSync(sessionPath)) return null;
|
|
940
1017
|
let result = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.0",
|
|
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/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 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",
|
|
@@ -70,6 +70,6 @@
|
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"acorn": "^8.18.0",
|
|
73
|
-
"wendkeep": "^0.
|
|
73
|
+
"wendkeep": "^0.66.5"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -6,8 +6,16 @@ export const OPERATING_PROFILES = Object.freeze([
|
|
|
6
6
|
'ASSURE',
|
|
7
7
|
]);
|
|
8
8
|
export const DEFAULT_OPERATING_PROFILE = 'GOVERN';
|
|
9
|
+
export const ADAPTIVE_OPERATING_PROFILES = Object.freeze([
|
|
10
|
+
'FLOW',
|
|
11
|
+
'GUIDE',
|
|
12
|
+
'GOVERN',
|
|
13
|
+
'ASSURE',
|
|
14
|
+
]);
|
|
9
15
|
|
|
10
16
|
const PROFILE_SET = new Set(OPERATING_PROFILES);
|
|
17
|
+
const ADAPTIVE_PROFILE_SET = new Set(ADAPTIVE_OPERATING_PROFILES);
|
|
18
|
+
export const TASK_PROFILE_REASON_MAX_LENGTH = 500;
|
|
11
19
|
|
|
12
20
|
function policy(profile, route, options) {
|
|
13
21
|
return Object.freeze({
|
|
@@ -70,6 +78,125 @@ function canonicalProfile(value) {
|
|
|
70
78
|
return value.trim().toUpperCase();
|
|
71
79
|
}
|
|
72
80
|
|
|
81
|
+
function taskProfileError(code, message) {
|
|
82
|
+
const error = new Error(message);
|
|
83
|
+
error.code = code;
|
|
84
|
+
return error;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function taskProfile(value) {
|
|
88
|
+
const profile = canonicalProfile(value);
|
|
89
|
+
if (ADAPTIVE_PROFILE_SET.has(profile)) return profile;
|
|
90
|
+
throw taskProfileError(
|
|
91
|
+
'WENDKEEP_TASK_PROFILE_INVALID',
|
|
92
|
+
`Perfil temporário inválido: ${typeof value === 'string' ? `"${value}"` : String(value)}. `
|
|
93
|
+
+ `Use ${ADAPTIVE_OPERATING_PROFILES.join(', ')}; OFF exige seleção humana persistente.`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function taskReason(value) {
|
|
98
|
+
const reason = typeof value === 'string' ? value.trim() : '';
|
|
99
|
+
if (reason && reason.length <= TASK_PROFILE_REASON_MAX_LENGTH) return reason;
|
|
100
|
+
throw taskProfileError(
|
|
101
|
+
'WENDKEEP_TASK_PROFILE_REASON_INVALID',
|
|
102
|
+
`Motivo da rota temporária deve ter entre 1 e ${TASK_PROFILE_REASON_MAX_LENGTH} caracteres.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function taskSequence(value) {
|
|
107
|
+
const sequence = Number(value);
|
|
108
|
+
return Number.isSafeInteger(sequence) && sequence > 0 ? sequence : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function taskContextError() {
|
|
112
|
+
return taskProfileError(
|
|
113
|
+
'WENDKEEP_TASK_PROFILE_CONTEXT_INVALID',
|
|
114
|
+
'Rota temporária exige sessão, prompt causal, lease id e timestamp válidos.',
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function createTaskOperatingProfileLease({
|
|
119
|
+
profile,
|
|
120
|
+
reason,
|
|
121
|
+
sessionId,
|
|
122
|
+
turnId = '',
|
|
123
|
+
turnSequence,
|
|
124
|
+
leaseId,
|
|
125
|
+
issuedAt,
|
|
126
|
+
} = {}) {
|
|
127
|
+
const selected = taskProfile(profile);
|
|
128
|
+
const auditedReason = taskReason(reason);
|
|
129
|
+
const session = typeof sessionId === 'string' ? sessionId.trim() : '';
|
|
130
|
+
const requestTurnId = typeof turnId === 'string' ? turnId.trim() : '';
|
|
131
|
+
const sequence = taskSequence(turnSequence);
|
|
132
|
+
const id = typeof leaseId === 'string' ? leaseId.trim() : '';
|
|
133
|
+
const issued = typeof issuedAt === 'string' ? issuedAt.trim() : '';
|
|
134
|
+
if (!session || !requestTurnId || sequence === null || !id || !issued || !Number.isFinite(Date.parse(issued))) {
|
|
135
|
+
throw taskContextError();
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
lease_id: id,
|
|
139
|
+
state: 'active',
|
|
140
|
+
profile: selected,
|
|
141
|
+
requested_by: 'llm-harness',
|
|
142
|
+
reason: auditedReason,
|
|
143
|
+
session_id: session,
|
|
144
|
+
request_turn_id: requestTurnId,
|
|
145
|
+
request_turn_sequence: sequence,
|
|
146
|
+
issued_at: issued,
|
|
147
|
+
expires_on: 'request-stop',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function evaluateTaskOperatingProfileLease(lease, {
|
|
152
|
+
sessionId = '',
|
|
153
|
+
turnId = '',
|
|
154
|
+
turnSequence,
|
|
155
|
+
} = {}) {
|
|
156
|
+
if (lease === undefined || lease === null) return { state: 'absent' };
|
|
157
|
+
if (!lease || typeof lease !== 'object' || Array.isArray(lease)) return { state: 'invalid' };
|
|
158
|
+
|
|
159
|
+
let normalized;
|
|
160
|
+
try {
|
|
161
|
+
normalized = createTaskOperatingProfileLease({
|
|
162
|
+
profile: lease.profile,
|
|
163
|
+
reason: lease.reason,
|
|
164
|
+
sessionId: lease.session_id,
|
|
165
|
+
turnId: lease.request_turn_id,
|
|
166
|
+
turnSequence: lease.request_turn_sequence,
|
|
167
|
+
leaseId: lease.lease_id,
|
|
168
|
+
issuedAt: lease.issued_at,
|
|
169
|
+
});
|
|
170
|
+
} catch {
|
|
171
|
+
return {
|
|
172
|
+
state: 'invalid',
|
|
173
|
+
...(typeof lease.lease_id === 'string' && lease.lease_id ? { lease_id: lease.lease_id } : {}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (lease.requested_by !== 'llm-harness' || lease.expires_on !== 'request-stop') {
|
|
177
|
+
return { state: 'invalid', lease_id: normalized.lease_id };
|
|
178
|
+
}
|
|
179
|
+
if (lease.state === 'consumed' || lease.state === 'expired') {
|
|
180
|
+
return { ...lease, ...normalized, state: lease.state };
|
|
181
|
+
}
|
|
182
|
+
if (lease.state !== 'active') return { state: 'invalid', lease_id: normalized.lease_id };
|
|
183
|
+
|
|
184
|
+
const currentSession = typeof sessionId === 'string' ? sessionId.trim() : '';
|
|
185
|
+
const currentTurnId = typeof turnId === 'string' ? turnId.trim() : '';
|
|
186
|
+
const currentSequence = taskSequence(turnSequence);
|
|
187
|
+
if (!currentSession || !currentTurnId || currentSequence === null) {
|
|
188
|
+
return { ...normalized, state: 'invalid' };
|
|
189
|
+
}
|
|
190
|
+
if (normalized.session_id !== currentSession) {
|
|
191
|
+
return { ...normalized, state: 'invalid' };
|
|
192
|
+
}
|
|
193
|
+
const turnIdMismatch = normalized.request_turn_id !== currentTurnId;
|
|
194
|
+
if (turnIdMismatch || normalized.request_turn_sequence !== currentSequence) {
|
|
195
|
+
return { ...normalized, state: 'expired' };
|
|
196
|
+
}
|
|
197
|
+
return normalized;
|
|
198
|
+
}
|
|
199
|
+
|
|
73
200
|
export function normalizeOperatingProfile(value, { strict = false } = {}) {
|
|
74
201
|
const normalized = canonicalProfile(value);
|
|
75
202
|
if (PROFILE_SET.has(normalized)) return normalized;
|