wendkeep 0.66.4 → 0.66.5
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 +22 -0
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/docs/en/commands/costs-and-observability.md +21 -7
- package/docs/en/commands/maintenance-and-diagnostics.md +13 -1
- package/docs/en/commands/sessions-and-import.md +15 -0
- 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/sessions-and-import.md +15 -1
- 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/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 +218 -5
- package/hooks/subagent-stop.mjs +266 -12
- package/hooks/subagent-usage.mjs +65 -0
- package/hooks/token-usage.mjs +81 -4
- package/package.json +1 -1
- package/src/cost.mjs +40 -6
- package/src/doctor.mjs +4 -1
- package/src/rebuild-costs.mjs +220 -34
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export const OBSERVABILITY_SCHEMA = 2;
|
|
4
|
+
|
|
5
|
+
export const OBSERVABILITY_DIAGNOSTIC_CODES = Object.freeze([
|
|
6
|
+
'CACHE_INVALID',
|
|
7
|
+
'CHILD_META_INVALID',
|
|
8
|
+
'CHILD_MISSING',
|
|
9
|
+
'DUPLICATE_ROLLOUT_ID',
|
|
10
|
+
'FALLBACK_LIMIT_EXCEEDED',
|
|
11
|
+
'GRAPH_LIMIT_EXCEEDED',
|
|
12
|
+
'LEGACY_CHAIN_UNPROVEN',
|
|
13
|
+
'LIVE_BYTE_BUDGET_EXCEEDED',
|
|
14
|
+
'LIVE_DEADLINE_EXCEEDED',
|
|
15
|
+
'MAIN_TRANSCRIPT_UNRESOLVED',
|
|
16
|
+
'PARENT_META_INVALID',
|
|
17
|
+
'ROOT_MISMATCH',
|
|
18
|
+
'SOURCE_CHANGED_DURING_SCAN',
|
|
19
|
+
'STALE_FRONTIER',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const DIAGNOSTIC_CODE_SET = new Set(OBSERVABILITY_DIAGNOSTIC_CODES);
|
|
23
|
+
const OBSERVABILITY_STATES = new Set(['complete', 'none', 'degraded']);
|
|
24
|
+
const FRONTIER_STRING_FIELDS = [
|
|
25
|
+
'canonical_session_id',
|
|
26
|
+
'activation_id',
|
|
27
|
+
'roots_stat_hash',
|
|
28
|
+
'graph_cursor',
|
|
29
|
+
'source_manifest_hash',
|
|
30
|
+
];
|
|
31
|
+
const FRONTIER_SEQUENCE_FIELDS = [
|
|
32
|
+
'activation_epoch',
|
|
33
|
+
'turn_sequence',
|
|
34
|
+
'signal_sequence',
|
|
35
|
+
];
|
|
36
|
+
const FRONTMATTER_TO_FRONTIER = Object.freeze({
|
|
37
|
+
canonical_session_id: 'observability_session_id',
|
|
38
|
+
activation_id: 'observability_activation_id',
|
|
39
|
+
activation_epoch: 'observability_activation_epoch',
|
|
40
|
+
turn_sequence: 'observability_turn_sequence',
|
|
41
|
+
signal_sequence: 'observability_signal_sequence',
|
|
42
|
+
roots_stat_hash: 'observability_roots_stat_hash',
|
|
43
|
+
graph_cursor: 'observability_graph_cursor',
|
|
44
|
+
source_manifest_hash: 'observability_source_manifest_hash',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function diagnosticError() {
|
|
48
|
+
const error = new TypeError('diagnostic de observabilidade inválido');
|
|
49
|
+
error.code = 'OBSERVABILITY_DIAGNOSTIC_INVALID';
|
|
50
|
+
return error;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function frontierError() {
|
|
54
|
+
const error = new TypeError('frontier de observabilidade inválido');
|
|
55
|
+
error.code = 'OBSERVABILITY_FRONTIER_INVALID';
|
|
56
|
+
return error;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function checkpointError() {
|
|
60
|
+
const error = new TypeError('checkpoint de observabilidade inválido');
|
|
61
|
+
error.code = 'OBSERVABILITY_CHECKPOINT_INVALID';
|
|
62
|
+
return error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function safeNonEmptyString(value) {
|
|
66
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function safeSequence(value) {
|
|
70
|
+
if (typeof value === 'string' && /^\d+$/.test(value.trim())) value = Number(value);
|
|
71
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Reduce diagnostics to the only shape that may cross the local runtime boundary.
|
|
76
|
+
* Values are never interpolated in thrown errors so rejected private data is not echoed.
|
|
77
|
+
*/
|
|
78
|
+
export function sanitizeObservabilityDiagnostics(diagnostics = []) {
|
|
79
|
+
if (!Array.isArray(diagnostics)) throw diagnosticError();
|
|
80
|
+
const counts = new Map();
|
|
81
|
+
for (const diagnostic of diagnostics) {
|
|
82
|
+
if (!diagnostic || typeof diagnostic !== 'object' || Array.isArray(diagnostic)) {
|
|
83
|
+
throw diagnosticError();
|
|
84
|
+
}
|
|
85
|
+
const keys = Object.keys(diagnostic).sort();
|
|
86
|
+
if (keys.length !== 2 || keys[0] !== 'code' || keys[1] !== 'count') {
|
|
87
|
+
throw diagnosticError();
|
|
88
|
+
}
|
|
89
|
+
if (!DIAGNOSTIC_CODE_SET.has(diagnostic.code)
|
|
90
|
+
|| !Number.isSafeInteger(diagnostic.count)
|
|
91
|
+
|| diagnostic.count <= 0) {
|
|
92
|
+
throw diagnosticError();
|
|
93
|
+
}
|
|
94
|
+
counts.set(diagnostic.code, (counts.get(diagnostic.code) || 0) + diagnostic.count);
|
|
95
|
+
}
|
|
96
|
+
return [...counts.entries()]
|
|
97
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
98
|
+
.map(([code, count]) => ({ code, count }));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function normalizeObservabilityFrontier(input) {
|
|
102
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw frontierError();
|
|
103
|
+
const frontier = {};
|
|
104
|
+
for (const field of FRONTIER_STRING_FIELDS) {
|
|
105
|
+
const value = safeNonEmptyString(input[field]);
|
|
106
|
+
if (value === null) throw frontierError();
|
|
107
|
+
frontier[field] = value;
|
|
108
|
+
}
|
|
109
|
+
for (const field of FRONTIER_SEQUENCE_FIELDS) {
|
|
110
|
+
const value = safeSequence(input[field]);
|
|
111
|
+
if (value === null) throw frontierError();
|
|
112
|
+
frontier[field] = value;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
canonical_session_id: frontier.canonical_session_id,
|
|
116
|
+
activation_id: frontier.activation_id,
|
|
117
|
+
activation_epoch: frontier.activation_epoch,
|
|
118
|
+
turn_sequence: frontier.turn_sequence,
|
|
119
|
+
signal_sequence: frontier.signal_sequence,
|
|
120
|
+
roots_stat_hash: frontier.roots_stat_hash,
|
|
121
|
+
graph_cursor: frontier.graph_cursor,
|
|
122
|
+
source_manifest_hash: frontier.source_manifest_hash,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const buildObservabilityFrontier = normalizeObservabilityFrontier;
|
|
127
|
+
|
|
128
|
+
export function hashObservabilityFrontier(input) {
|
|
129
|
+
const frontier = normalizeObservabilityFrontier(input);
|
|
130
|
+
return createHash('sha256').update(JSON.stringify(frontier)).digest('hex');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Compare a candidate frontier with the checkpoint already materialized.
|
|
135
|
+
* The argument order is deliberately (current, candidate), matching a CAS writer.
|
|
136
|
+
*/
|
|
137
|
+
export function compareObservabilityFrontiers(currentInput, candidateInput) {
|
|
138
|
+
if (!currentInput) {
|
|
139
|
+
normalizeObservabilityFrontier(candidateInput);
|
|
140
|
+
return 'newer';
|
|
141
|
+
}
|
|
142
|
+
const current = normalizeObservabilityFrontier(currentInput);
|
|
143
|
+
const candidate = normalizeObservabilityFrontier(candidateInput);
|
|
144
|
+
if (current.canonical_session_id !== candidate.canonical_session_id) return 'conflict';
|
|
145
|
+
|
|
146
|
+
for (const field of FRONTIER_SEQUENCE_FIELDS) {
|
|
147
|
+
if (candidate[field] < current[field]) return 'stale';
|
|
148
|
+
if (candidate[field] > current[field]) return 'newer';
|
|
149
|
+
if (field === 'activation_epoch' && candidate.activation_id !== current.activation_id) {
|
|
150
|
+
return 'conflict';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const field of ['roots_stat_hash', 'graph_cursor', 'source_manifest_hash']) {
|
|
155
|
+
if (candidate[field] !== current[field]) return 'conflict';
|
|
156
|
+
}
|
|
157
|
+
return 'same';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const compareObservabilityFrontier = compareObservabilityFrontiers;
|
|
161
|
+
|
|
162
|
+
function unquoteFrontmatterValue(raw) {
|
|
163
|
+
const value = String(raw ?? '').trim();
|
|
164
|
+
if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
|
|
165
|
+
return value.slice(1, -1).replaceAll("''", "'");
|
|
166
|
+
}
|
|
167
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
168
|
+
try { return JSON.parse(value); } catch { return value.slice(1, -1); }
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function checkpointFields(input) {
|
|
174
|
+
if (typeof input !== 'string') return input;
|
|
175
|
+
const normalized = input.replaceAll('\r\n', '\n');
|
|
176
|
+
const frontmatter = normalized.match(/^---\n([\s\S]*?)\n---(?:\n|$)/)?.[1] ?? normalized;
|
|
177
|
+
const fields = {};
|
|
178
|
+
for (const line of frontmatter.split('\n')) {
|
|
179
|
+
const match = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
|
|
180
|
+
if (match) fields[match[1]] = unquoteFrontmatterValue(match[2]);
|
|
181
|
+
}
|
|
182
|
+
return fields;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function renderObservabilityCheckpoint(frontierInput, {
|
|
186
|
+
state,
|
|
187
|
+
diagnostics = [],
|
|
188
|
+
} = {}) {
|
|
189
|
+
const frontier = normalizeObservabilityFrontier(frontierInput);
|
|
190
|
+
if (!OBSERVABILITY_STATES.has(state)) throw checkpointError();
|
|
191
|
+
const safeDiagnostics = sanitizeObservabilityDiagnostics(diagnostics);
|
|
192
|
+
return {
|
|
193
|
+
observability_schema: OBSERVABILITY_SCHEMA,
|
|
194
|
+
subagents_observability_state: state,
|
|
195
|
+
observability_session_id: frontier.canonical_session_id,
|
|
196
|
+
observability_activation_id: frontier.activation_id,
|
|
197
|
+
observability_activation_epoch: frontier.activation_epoch,
|
|
198
|
+
observability_turn_sequence: frontier.turn_sequence,
|
|
199
|
+
observability_signal_sequence: frontier.signal_sequence,
|
|
200
|
+
observability_roots_stat_hash: frontier.roots_stat_hash,
|
|
201
|
+
observability_graph_cursor: frontier.graph_cursor,
|
|
202
|
+
observability_source_manifest_hash: frontier.source_manifest_hash,
|
|
203
|
+
subagents_diagnostics_json: JSON.stringify(safeDiagnostics),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function quoteFrontmatter(value) {
|
|
208
|
+
if (typeof value === 'number') return String(value);
|
|
209
|
+
return `'${String(value).replaceAll("'", "''")}'`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function renderObservabilityCheckpointLines(frontierInput, options = {}) {
|
|
213
|
+
const fields = renderObservabilityCheckpoint(frontierInput, options);
|
|
214
|
+
return Object.entries(fields).map(([key, value]) => `${key}: ${quoteFrontmatter(value)}`).join('\n');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function parseObservabilityCheckpoint(input) {
|
|
218
|
+
const fields = checkpointFields(input);
|
|
219
|
+
if (!fields || typeof fields !== 'object') return null;
|
|
220
|
+
if (safeSequence(fields.observability_schema) !== OBSERVABILITY_SCHEMA) return null;
|
|
221
|
+
if (!OBSERVABILITY_STATES.has(fields.subagents_observability_state)) return null;
|
|
222
|
+
try {
|
|
223
|
+
const source = {};
|
|
224
|
+
for (const [field, frontmatterKey] of Object.entries(FRONTMATTER_TO_FRONTIER)) {
|
|
225
|
+
source[field] = unquoteFrontmatterValue(fields[frontmatterKey]);
|
|
226
|
+
}
|
|
227
|
+
const frontier = normalizeObservabilityFrontier(source);
|
|
228
|
+
const diagnosticsRaw = unquoteFrontmatterValue(fields.subagents_diagnostics_json ?? '[]');
|
|
229
|
+
const diagnostics = sanitizeObservabilityDiagnostics(JSON.parse(diagnosticsRaw));
|
|
230
|
+
return {
|
|
231
|
+
schema: OBSERVABILITY_SCHEMA,
|
|
232
|
+
state: fields.subagents_observability_state,
|
|
233
|
+
frontier,
|
|
234
|
+
diagnostics,
|
|
235
|
+
};
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export const readObservabilityCheckpoint = parseObservabilityCheckpoint;
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
VAULT_LOCK_BUSY,
|
|
6
|
+
assertVaultPathSafe,
|
|
7
|
+
mkdirVaultPath,
|
|
8
|
+
withVaultPathLock,
|
|
9
|
+
writeVaultFileAtomic,
|
|
10
|
+
} from './vault-path-safety.mjs';
|
|
11
|
+
import { sanitizeObservabilityDiagnostics } from './session-observability-state.mjs';
|
|
12
|
+
|
|
13
|
+
const STORE_SCHEMA_VERSION = 1;
|
|
14
|
+
const STORE_DIR_PARTS = ['.brain', 'runtime', 'session-observability'];
|
|
15
|
+
const STORE_PATH_CODE = 'OBSERVABILITY_STORE_PATH_UNSAFE';
|
|
16
|
+
const DEFAULT_SIGNAL_LIMIT = 4_096;
|
|
17
|
+
const SIGNAL_KINDS = new Set(['started', 'interacted', 'interrupted']);
|
|
18
|
+
|
|
19
|
+
function storeError(code, message) {
|
|
20
|
+
const error = new Error(message);
|
|
21
|
+
error.code = code;
|
|
22
|
+
return error;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function nonEmptyString(value, code = 'OBSERVABILITY_STORE_INVALID') {
|
|
26
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
27
|
+
throw storeError(code, 'identificador de observabilidade inválido');
|
|
28
|
+
}
|
|
29
|
+
return value.trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sequence(value, fallback = null) {
|
|
33
|
+
if (typeof value === 'string' && /^\d+$/.test(value.trim())) value = Number(value);
|
|
34
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : fallback;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function timeValue(value, fallback) {
|
|
38
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function jsonClone(value) {
|
|
42
|
+
if (value === undefined) return null;
|
|
43
|
+
try { return JSON.parse(JSON.stringify(value)); }
|
|
44
|
+
catch { throw storeError('OBSERVABILITY_STORE_INVALID', 'estado de observabilidade inválido'); }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function defaultState(sessionId, {
|
|
48
|
+
reconstructed = false,
|
|
49
|
+
dirty = false,
|
|
50
|
+
diagnostics = [],
|
|
51
|
+
} = {}) {
|
|
52
|
+
return {
|
|
53
|
+
schema_version: STORE_SCHEMA_VERSION,
|
|
54
|
+
session_id: sessionId,
|
|
55
|
+
observability_signal_sequence: 0,
|
|
56
|
+
observability_checkpoint_sequence: 0,
|
|
57
|
+
observability_dirty: dirty,
|
|
58
|
+
signals: [],
|
|
59
|
+
lease: null,
|
|
60
|
+
checkpoint_frontier: null,
|
|
61
|
+
source_manifest: null,
|
|
62
|
+
graph_cache: null,
|
|
63
|
+
diagnostics: sanitizeObservabilityDiagnostics(diagnostics),
|
|
64
|
+
reconstructed,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeSignal(input) {
|
|
69
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
70
|
+
throw storeError('OBSERVABILITY_SIGNAL_INVALID', 'sinal de observabilidade inválido');
|
|
71
|
+
}
|
|
72
|
+
const rolloutId = nonEmptyString(
|
|
73
|
+
input.rollout_id ?? input.rolloutId ?? input.agent_thread_id ?? input.agentThreadId,
|
|
74
|
+
'OBSERVABILITY_SIGNAL_INVALID',
|
|
75
|
+
);
|
|
76
|
+
const signal = { rollout_id: rolloutId };
|
|
77
|
+
const transcriptPath = input.transcript_path ?? input.transcriptPath;
|
|
78
|
+
if (typeof transcriptPath === 'string' && transcriptPath.trim()) {
|
|
79
|
+
signal.transcript_path = transcriptPath.trim();
|
|
80
|
+
}
|
|
81
|
+
const parentThreadId = input.parent_thread_id
|
|
82
|
+
?? input.parentThreadId
|
|
83
|
+
?? input.parent_rollout_id
|
|
84
|
+
?? input.parentRolloutId;
|
|
85
|
+
if (typeof parentThreadId === 'string' && parentThreadId.trim()) {
|
|
86
|
+
signal.parent_thread_id = parentThreadId.trim();
|
|
87
|
+
}
|
|
88
|
+
const rawKind = input.kind ?? input.event_kind ?? input.eventKind ?? 'started';
|
|
89
|
+
if (typeof rawKind !== 'string' || !SIGNAL_KINDS.has(rawKind.trim().toLowerCase())) {
|
|
90
|
+
throw storeError('OBSERVABILITY_SIGNAL_INVALID', 'sinal de observabilidade inválido');
|
|
91
|
+
}
|
|
92
|
+
signal.kind = rawKind.trim().toLowerCase();
|
|
93
|
+
const timestamp = input.timestamp ?? input.started_at ?? input.startedAt;
|
|
94
|
+
if (typeof timestamp === 'string' && timestamp.trim()) {
|
|
95
|
+
signal.timestamp = timestamp.trim();
|
|
96
|
+
}
|
|
97
|
+
const agentPath = input.agent_path ?? input.agentPath;
|
|
98
|
+
if (typeof agentPath === 'string' && agentPath.trim()) {
|
|
99
|
+
signal.agent_path = agentPath.trim();
|
|
100
|
+
}
|
|
101
|
+
const activationId = input.activation_id ?? input.activationId;
|
|
102
|
+
if (typeof activationId === 'string' && activationId.trim()) {
|
|
103
|
+
signal.activation_id = activationId.trim();
|
|
104
|
+
}
|
|
105
|
+
for (const [target, source] of [
|
|
106
|
+
['activation_epoch', input.activation_epoch ?? input.activationEpoch],
|
|
107
|
+
['turn_sequence', input.turn_sequence ?? input.turnSequence],
|
|
108
|
+
['signal_sequence', input.signal_sequence ?? input.signalSequence],
|
|
109
|
+
]) {
|
|
110
|
+
if (source !== undefined) {
|
|
111
|
+
const normalized = sequence(source);
|
|
112
|
+
if (normalized === null) {
|
|
113
|
+
throw storeError('OBSERVABILITY_SIGNAL_INVALID', 'sinal de observabilidade inválido');
|
|
114
|
+
}
|
|
115
|
+
signal[target] = normalized;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (typeof input.observed_at === 'string' && input.observed_at.trim()) {
|
|
119
|
+
signal.observed_at = input.observed_at.trim();
|
|
120
|
+
}
|
|
121
|
+
return signal;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeLease(input) {
|
|
125
|
+
if (input == null) return null;
|
|
126
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
127
|
+
throw storeError('OBSERVABILITY_STORE_CORRUPT', 'lease de observabilidade corrompida');
|
|
128
|
+
}
|
|
129
|
+
const signalSequence = sequence(input.signal_sequence);
|
|
130
|
+
const expiresAt = timeValue(input.expires_at, null);
|
|
131
|
+
if (signalSequence === null || expiresAt === null) {
|
|
132
|
+
throw storeError('OBSERVABILITY_STORE_CORRUPT', 'lease de observabilidade corrompida');
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
owner_token: nonEmptyString(input.owner_token, 'OBSERVABILITY_STORE_CORRUPT'),
|
|
136
|
+
signal_sequence: signalSequence,
|
|
137
|
+
acquired_at: timeValue(input.acquired_at, expiresAt),
|
|
138
|
+
expires_at: expiresAt,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeStoreState(input, sessionId) {
|
|
143
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)
|
|
144
|
+
|| input.schema_version !== STORE_SCHEMA_VERSION
|
|
145
|
+
|| input.session_id !== sessionId) {
|
|
146
|
+
throw storeError('OBSERVABILITY_STORE_CORRUPT', 'store de observabilidade corrompido');
|
|
147
|
+
}
|
|
148
|
+
const signalSequence = sequence(input.observability_signal_sequence);
|
|
149
|
+
const checkpointSequence = sequence(input.observability_checkpoint_sequence);
|
|
150
|
+
if (signalSequence === null || checkpointSequence === null
|
|
151
|
+
|| checkpointSequence > signalSequence
|
|
152
|
+
|| typeof input.observability_dirty !== 'boolean'
|
|
153
|
+
|| !Array.isArray(input.signals)) {
|
|
154
|
+
throw storeError('OBSERVABILITY_STORE_CORRUPT', 'store de observabilidade corrompido');
|
|
155
|
+
}
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
const signals = input.signals.map(normalizeSignal);
|
|
158
|
+
let previousSignalSequence = 0;
|
|
159
|
+
for (const signal of signals) {
|
|
160
|
+
if (seen.has(signal.rollout_id)
|
|
161
|
+
|| !Number.isSafeInteger(signal.signal_sequence)
|
|
162
|
+
|| signal.signal_sequence <= previousSignalSequence
|
|
163
|
+
|| signal.signal_sequence > signalSequence) {
|
|
164
|
+
throw storeError('OBSERVABILITY_STORE_CORRUPT', 'store de observabilidade corrompido');
|
|
165
|
+
}
|
|
166
|
+
seen.add(signal.rollout_id);
|
|
167
|
+
previousSignalSequence = signal.signal_sequence;
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
schema_version: STORE_SCHEMA_VERSION,
|
|
171
|
+
session_id: sessionId,
|
|
172
|
+
observability_signal_sequence: signalSequence,
|
|
173
|
+
observability_checkpoint_sequence: checkpointSequence,
|
|
174
|
+
observability_dirty: input.observability_dirty,
|
|
175
|
+
signals,
|
|
176
|
+
lease: normalizeLease(input.lease),
|
|
177
|
+
checkpoint_frontier: jsonClone(input.checkpoint_frontier),
|
|
178
|
+
source_manifest: jsonClone(input.source_manifest),
|
|
179
|
+
graph_cache: jsonClone(input.graph_cache),
|
|
180
|
+
diagnostics: sanitizeObservabilityDiagnostics(input.diagnostics ?? []),
|
|
181
|
+
reconstructed: Boolean(input.reconstructed),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function serializableState(input) {
|
|
186
|
+
return {
|
|
187
|
+
schema_version: input.schema_version,
|
|
188
|
+
session_id: input.session_id,
|
|
189
|
+
observability_signal_sequence: input.observability_signal_sequence,
|
|
190
|
+
observability_checkpoint_sequence: input.observability_checkpoint_sequence,
|
|
191
|
+
observability_dirty: input.observability_dirty,
|
|
192
|
+
signals: input.signals,
|
|
193
|
+
lease: input.lease,
|
|
194
|
+
checkpoint_frontier: input.checkpoint_frontier,
|
|
195
|
+
source_manifest: input.source_manifest,
|
|
196
|
+
graph_cache: input.graph_cache,
|
|
197
|
+
diagnostics: input.diagnostics,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function serialized(input) {
|
|
202
|
+
return `${JSON.stringify(serializableState(input), null, 2)}\n`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function storeDirectory(vaultBase) {
|
|
206
|
+
return join(vaultBase, ...STORE_DIR_PARTS);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function ensureStoreDirectory(vaultBase) {
|
|
210
|
+
return mkdirVaultPath(vaultBase, storeDirectory(vaultBase), {
|
|
211
|
+
recursive: true,
|
|
212
|
+
label: 'runtime de observabilidade de sessão',
|
|
213
|
+
code: STORE_PATH_CODE,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function observabilityStorePath(vaultBase, sessionId) {
|
|
218
|
+
const id = nonEmptyString(sessionId);
|
|
219
|
+
const digest = createHash('sha256').update(id).digest('hex');
|
|
220
|
+
return join(vaultBase, ...STORE_DIR_PARTS, `${digest}.json`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function readStorePath(vaultBase, sessionId, path) {
|
|
224
|
+
const checked = assertVaultPathSafe(vaultBase, path, {
|
|
225
|
+
expectedType: 'file',
|
|
226
|
+
label: 'store de observabilidade de sessão',
|
|
227
|
+
code: STORE_PATH_CODE,
|
|
228
|
+
});
|
|
229
|
+
if (!checked.exists) return defaultState(sessionId);
|
|
230
|
+
try {
|
|
231
|
+
const raw = readFileSync(checked.target, 'utf8');
|
|
232
|
+
assertVaultPathSafe(vaultBase, checked.target, {
|
|
233
|
+
allowMissing: false,
|
|
234
|
+
expectedType: 'file',
|
|
235
|
+
label: 'store de observabilidade de sessão',
|
|
236
|
+
code: STORE_PATH_CODE,
|
|
237
|
+
});
|
|
238
|
+
return normalizeStoreState(JSON.parse(raw), sessionId);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (error?.code === STORE_PATH_CODE) throw error;
|
|
241
|
+
return defaultState(sessionId, {
|
|
242
|
+
reconstructed: true,
|
|
243
|
+
dirty: true,
|
|
244
|
+
diagnostics: [{ code: 'CACHE_INVALID', count: 1 }],
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function readObservabilityStore(vaultBase, sessionId) {
|
|
250
|
+
const id = nonEmptyString(sessionId);
|
|
251
|
+
return readStorePath(vaultBase, id, observabilityStorePath(vaultBase, id));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Serialize a small store transition under the hardened Vault path lock.
|
|
256
|
+
* The mutator may return `{ state, value }`; its `value` is returned without persistence.
|
|
257
|
+
*/
|
|
258
|
+
export function mutateObservabilityStore(vaultBase, sessionId, mutator, {
|
|
259
|
+
lockTimeoutMs = 2_000,
|
|
260
|
+
lockStaleMs = 10_000,
|
|
261
|
+
} = {}) {
|
|
262
|
+
const id = nonEmptyString(sessionId);
|
|
263
|
+
if (typeof mutator !== 'function') {
|
|
264
|
+
throw storeError('OBSERVABILITY_STORE_INVALID', 'mutator de observabilidade inválido');
|
|
265
|
+
}
|
|
266
|
+
ensureStoreDirectory(vaultBase);
|
|
267
|
+
const path = observabilityStorePath(vaultBase, id);
|
|
268
|
+
const outcome = withVaultPathLock(vaultBase, path, () => {
|
|
269
|
+
const current = readStorePath(vaultBase, id, path);
|
|
270
|
+
const transition = mutator(jsonClone(current));
|
|
271
|
+
if (transition == null) {
|
|
272
|
+
return { state: current, changed: false, value: null, reconstructed: current.reconstructed };
|
|
273
|
+
}
|
|
274
|
+
const nextInput = Object.hasOwn(transition, 'state') ? transition.state : transition;
|
|
275
|
+
const value = Object.hasOwn(transition, 'state') ? transition.value : null;
|
|
276
|
+
const next = normalizeStoreState({
|
|
277
|
+
...nextInput,
|
|
278
|
+
schema_version: STORE_SCHEMA_VERSION,
|
|
279
|
+
session_id: id,
|
|
280
|
+
}, id);
|
|
281
|
+
next.reconstructed = false;
|
|
282
|
+
const before = serialized(current);
|
|
283
|
+
const after = serialized(next);
|
|
284
|
+
if (before !== after || current.reconstructed) {
|
|
285
|
+
writeVaultFileAtomic(vaultBase, path, after, 'utf8', {
|
|
286
|
+
label: 'store de observabilidade de sessão',
|
|
287
|
+
code: STORE_PATH_CODE,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
state: next,
|
|
292
|
+
changed: before !== after || current.reconstructed,
|
|
293
|
+
value,
|
|
294
|
+
reconstructed: current.reconstructed,
|
|
295
|
+
};
|
|
296
|
+
}, {
|
|
297
|
+
timeoutMs: lockTimeoutMs,
|
|
298
|
+
staleMs: lockStaleMs,
|
|
299
|
+
code: STORE_PATH_CODE,
|
|
300
|
+
});
|
|
301
|
+
if (outcome === VAULT_LOCK_BUSY) {
|
|
302
|
+
return { state: null, changed: false, value: null, busy: true, reconstructed: false };
|
|
303
|
+
}
|
|
304
|
+
return { ...outcome, busy: false };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function recordObservabilitySignal(vaultBase, sessionId, signalInput, {
|
|
308
|
+
maxSignals = DEFAULT_SIGNAL_LIMIT,
|
|
309
|
+
...storeOptions
|
|
310
|
+
} = {}) {
|
|
311
|
+
const signal = normalizeSignal(signalInput);
|
|
312
|
+
if (!Number.isSafeInteger(maxSignals) || maxSignals <= 0) {
|
|
313
|
+
throw storeError('OBSERVABILITY_STORE_INVALID', 'limite de sinais inválido');
|
|
314
|
+
}
|
|
315
|
+
const outcome = mutateObservabilityStore(vaultBase, sessionId, (state) => {
|
|
316
|
+
const duplicate = state.signals.some((entry) => entry.rollout_id === signal.rollout_id);
|
|
317
|
+
if (duplicate) return { state, value: { duplicate: true } };
|
|
318
|
+
const nextSequence = state.observability_signal_sequence + 1;
|
|
319
|
+
state.observability_signal_sequence = nextSequence;
|
|
320
|
+
state.observability_dirty = true;
|
|
321
|
+
state.signals = [...state.signals, { ...signal, signal_sequence: nextSequence }]
|
|
322
|
+
.slice(-maxSignals);
|
|
323
|
+
return { state, value: { duplicate: false } };
|
|
324
|
+
}, storeOptions);
|
|
325
|
+
if (outcome.busy) {
|
|
326
|
+
return { recorded: false, duplicate: false, sequence: null, state: null, reason: 'store-busy' };
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
recorded: !outcome.value.duplicate,
|
|
330
|
+
duplicate: outcome.value.duplicate,
|
|
331
|
+
sequence: outcome.state.observability_signal_sequence,
|
|
332
|
+
state: outcome.state,
|
|
333
|
+
reason: outcome.value.duplicate ? 'duplicate' : 'recorded',
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function tryAcquireObservabilityLease(vaultBase, sessionId, {
|
|
338
|
+
signalSequence,
|
|
339
|
+
ownerToken = randomUUID(),
|
|
340
|
+
now = Date.now(),
|
|
341
|
+
ttlMs = 20_000,
|
|
342
|
+
...storeOptions
|
|
343
|
+
} = {}) {
|
|
344
|
+
const requestedSequence = sequence(signalSequence);
|
|
345
|
+
const token = nonEmptyString(ownerToken, 'OBSERVABILITY_LEASE_INVALID');
|
|
346
|
+
const nowMs = timeValue(now, null);
|
|
347
|
+
if (requestedSequence === null || nowMs === null || !Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
348
|
+
throw storeError('OBSERVABILITY_LEASE_INVALID', 'lease de observabilidade inválida');
|
|
349
|
+
}
|
|
350
|
+
const outcome = mutateObservabilityStore(vaultBase, sessionId, (state) => {
|
|
351
|
+
const latest = state.observability_signal_sequence;
|
|
352
|
+
if (requestedSequence < latest) {
|
|
353
|
+
return { state, value: { acquired: false, reason: 'stale-signal' } };
|
|
354
|
+
}
|
|
355
|
+
if (requestedSequence > latest) {
|
|
356
|
+
return { state, value: { acquired: false, reason: 'future-signal' } };
|
|
357
|
+
}
|
|
358
|
+
const lease = state.lease;
|
|
359
|
+
if (lease && lease.expires_at > nowMs
|
|
360
|
+
&& lease.signal_sequence === requestedSequence
|
|
361
|
+
&& lease.owner_token !== token) {
|
|
362
|
+
return { state, value: { acquired: false, reason: 'lease-busy' } };
|
|
363
|
+
}
|
|
364
|
+
if (lease && lease.expires_at > nowMs
|
|
365
|
+
&& lease.signal_sequence === requestedSequence
|
|
366
|
+
&& lease.owner_token === token) {
|
|
367
|
+
return { state, value: { acquired: true, reason: 'already-owned' } };
|
|
368
|
+
}
|
|
369
|
+
state.lease = {
|
|
370
|
+
owner_token: token,
|
|
371
|
+
signal_sequence: requestedSequence,
|
|
372
|
+
acquired_at: nowMs,
|
|
373
|
+
expires_at: nowMs + ttlMs,
|
|
374
|
+
};
|
|
375
|
+
return { state, value: { acquired: true, reason: lease ? 'superseded' : 'acquired' } };
|
|
376
|
+
}, storeOptions);
|
|
377
|
+
if (outcome.busy) {
|
|
378
|
+
return { acquired: false, reason: 'store-busy', state: null, ownerToken: token };
|
|
379
|
+
}
|
|
380
|
+
return { ...outcome.value, state: outcome.state, ownerToken: token };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function releaseObservabilityLease(vaultBase, sessionId, {
|
|
384
|
+
ownerToken,
|
|
385
|
+
signalSequence,
|
|
386
|
+
...storeOptions
|
|
387
|
+
} = {}) {
|
|
388
|
+
const token = nonEmptyString(ownerToken, 'OBSERVABILITY_LEASE_INVALID');
|
|
389
|
+
const expectedSequence = signalSequence === undefined ? null : sequence(signalSequence);
|
|
390
|
+
if (signalSequence !== undefined && expectedSequence === null) {
|
|
391
|
+
throw storeError('OBSERVABILITY_LEASE_INVALID', 'lease de observabilidade inválida');
|
|
392
|
+
}
|
|
393
|
+
const outcome = mutateObservabilityStore(vaultBase, sessionId, (state) => {
|
|
394
|
+
const owned = state.lease?.owner_token === token
|
|
395
|
+
&& (expectedSequence === null || state.lease.signal_sequence === expectedSequence);
|
|
396
|
+
if (!owned) return { state, value: { released: false, reason: 'not-owner' } };
|
|
397
|
+
state.lease = null;
|
|
398
|
+
return { state, value: { released: true, reason: 'released' } };
|
|
399
|
+
}, storeOptions);
|
|
400
|
+
if (outcome.busy) return { released: false, reason: 'store-busy', state: null };
|
|
401
|
+
return { ...outcome.value, state: outcome.state };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function markObservabilityCheckpoint(vaultBase, sessionId, {
|
|
405
|
+
checkpointSequence,
|
|
406
|
+
frontier = null,
|
|
407
|
+
sourceManifest,
|
|
408
|
+
graphCache,
|
|
409
|
+
diagnostics,
|
|
410
|
+
...storeOptions
|
|
411
|
+
} = {}) {
|
|
412
|
+
const nextCheckpoint = sequence(checkpointSequence);
|
|
413
|
+
if (nextCheckpoint === null) {
|
|
414
|
+
throw storeError('OBSERVABILITY_CHECKPOINT_INVALID', 'checkpoint de observabilidade inválido');
|
|
415
|
+
}
|
|
416
|
+
const outcome = mutateObservabilityStore(vaultBase, sessionId, (state) => {
|
|
417
|
+
if (nextCheckpoint > state.observability_signal_sequence) {
|
|
418
|
+
throw storeError('OBSERVABILITY_CHECKPOINT_INVALID', 'checkpoint de observabilidade inválido');
|
|
419
|
+
}
|
|
420
|
+
if (nextCheckpoint < state.observability_checkpoint_sequence) {
|
|
421
|
+
return { state, value: { accepted: false, reason: 'stale-checkpoint' } };
|
|
422
|
+
}
|
|
423
|
+
state.observability_checkpoint_sequence = nextCheckpoint;
|
|
424
|
+
state.observability_dirty = nextCheckpoint < state.observability_signal_sequence;
|
|
425
|
+
state.checkpoint_frontier = jsonClone(frontier);
|
|
426
|
+
if (sourceManifest !== undefined) state.source_manifest = jsonClone(sourceManifest);
|
|
427
|
+
if (graphCache !== undefined) state.graph_cache = jsonClone(graphCache);
|
|
428
|
+
if (diagnostics !== undefined) {
|
|
429
|
+
state.diagnostics = sanitizeObservabilityDiagnostics(diagnostics);
|
|
430
|
+
}
|
|
431
|
+
if (state.lease && state.lease.signal_sequence <= nextCheckpoint) state.lease = null;
|
|
432
|
+
return { state, value: { accepted: true, reason: 'checkpointed' } };
|
|
433
|
+
}, storeOptions);
|
|
434
|
+
if (outcome.busy) return null;
|
|
435
|
+
return outcome.state;
|
|
436
|
+
}
|