wendkeep 0.60.0 → 0.62.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 +44 -0
- package/README.en.md +14 -7
- package/README.md +14 -7
- package/docs/en/commands/memory.md +6 -2
- package/docs/en/commands/operating-profiles.md +26 -4
- package/docs/pt-BR/commands/memory.md +6 -2
- package/docs/pt-BR/commands/operating-profiles.md +26 -3
- package/hooks/memory-handoff.mjs +1 -199
- package/hooks/memory-mode.mjs +1 -89
- package/hooks/memory-schema.mjs +1 -310
- package/hooks/memory-store.mjs +1 -900
- package/hooks/sensors-core.mjs +1 -102
- package/package.json +6 -3
- package/packages/harness/package.json +2 -1
- package/packages/harness/src/index.mjs +2 -0
- package/packages/harness/src/operating-profile.mjs +133 -0
- package/packages/harness/src/sensors-core.mjs +102 -0
- package/packages/vault/src/index.mjs +6 -0
- package/packages/vault/src/memory-handoff.mjs +199 -0
- package/packages/vault/src/memory-mode.mjs +89 -0
- package/packages/vault/src/memory-schema.mjs +310 -0
- package/packages/vault/src/memory-store.mjs +900 -0
- package/packages/vault/src/validate-core.mjs +181 -0
- package/packages/vault/src/validate-memory.mjs +128 -0
- package/src/memory.mjs +77 -11
- package/src/operating-profile.mjs +1 -133
- package/src/validate-core.mjs +1 -181
- package/src/validate-memory.mjs +1 -128
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Memory-compaction protocol for the curated .brain/CORE.md layer.
|
|
2
|
+
// Ported from NutriGym-Vision's scripts/validate-brain-core.js to ESM:
|
|
3
|
+
// - cap 25 lines (hard), 22 (soft warning) — 1 durable item per line
|
|
4
|
+
// - 3 required sections
|
|
5
|
+
// - no secrets / no real-provider PII emails
|
|
6
|
+
// Plus the seeded skeleton and the protocol reference doc.
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
const HARD_LIMIT = 25;
|
|
12
|
+
const SOFT_LIMIT = 22;
|
|
13
|
+
|
|
14
|
+
// Bilingual (0.8.0): a CORE is valid when it carries the COMPLETE section set of either
|
|
15
|
+
// locale — pt-BR or en. Mixed/partial sets fail (the 3 sections are one contract).
|
|
16
|
+
const SECTION_SETS = {
|
|
17
|
+
'pt-BR': [
|
|
18
|
+
{ label: 'Preferências do Usuário', regex: /^##\s+Prefer[êe]ncias\s+do\s+Usu[áa]rio\s*$/im },
|
|
19
|
+
{ label: 'Padrões Ativos', regex: /^##\s+Padr[õo]es\s+Ativos\s*$/im },
|
|
20
|
+
{ label: 'Pendências Abertas', regex: /^##\s+Pend[êe]ncias\s+Abertas\s*$/im },
|
|
21
|
+
],
|
|
22
|
+
en: [
|
|
23
|
+
{ label: 'User Preferences', regex: /^##\s+User\s+Preferences\s*$/im },
|
|
24
|
+
{ label: 'Active Patterns', regex: /^##\s+Active\s+Patterns\s*$/im },
|
|
25
|
+
{ label: 'Open Items', regex: /^##\s+Open\s+Items\s*$/im },
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Secret patterns reject only "real" values (length floor); abstract mentions like
|
|
30
|
+
// `sk_*` / `whsec_*` (trailing asterisk) are allowed.
|
|
31
|
+
const SECRET_PATTERNS = [
|
|
32
|
+
{ name: 'Stripe secret key', regex: /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/ },
|
|
33
|
+
{ name: 'Stripe webhook secret', regex: /\bwhsec_[A-Za-z0-9]{20,}\b/ },
|
|
34
|
+
{ name: 'JWT token', regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
|
|
35
|
+
{ name: 'Bearer token', regex: /\bBearer\s+[A-Za-z0-9._-]{20,}\b/i },
|
|
36
|
+
{ name: 'OpenAI API key', regex: /\bsk-[A-Za-z0-9]{40,}\b/ },
|
|
37
|
+
{ name: 'Anthropic API key', regex: /\bsk-ant-[A-Za-z0-9_-]{40,}\b/ },
|
|
38
|
+
{ name: 'Google API key', regex: /\bAIza[0-9A-Za-z_-]{35}\b/ },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const PII_EMAIL_REGEX = /\b[A-Za-z0-9._%+-]+@(?!example\.(?:com|org|net)\b)(?:gmail|hotmail|yahoo|outlook|live|icloud|protonmail)\.[A-Za-z]{2,}\b/i;
|
|
42
|
+
|
|
43
|
+
// Validate CORE.md content. Returns { ok, errors, warnings, lineCount }.
|
|
44
|
+
export function validateCore(content) {
|
|
45
|
+
const text = String(content ?? '');
|
|
46
|
+
const lines = text.split('\n');
|
|
47
|
+
const lineCount = text.endsWith('\n') ? lines.length - 1 : lines.length;
|
|
48
|
+
const errors = [];
|
|
49
|
+
|
|
50
|
+
if (lineCount > HARD_LIMIT) {
|
|
51
|
+
errors.push(`Tamanho ${lineCount} > ${HARD_LIMIT} linhas (hard limit). Curar: remover itens resolvidos (detalhe vive no vault/git).`);
|
|
52
|
+
}
|
|
53
|
+
// Pick the locale set that matches best; require it to be complete.
|
|
54
|
+
const missingBySet = Object.values(SECTION_SETS).map((set) => set.filter(({ regex }) => !regex.test(text)));
|
|
55
|
+
const best = missingBySet.reduce((a, b) => (b.length < a.length ? b : a));
|
|
56
|
+
for (const { label } of best) errors.push(`Seção obrigatória ausente: ## ${label}`);
|
|
57
|
+
for (const { name, regex } of SECRET_PATTERNS) {
|
|
58
|
+
const m = text.match(regex);
|
|
59
|
+
if (m) errors.push(`Possível ${name} detectado: "${m[0].slice(0, 30)}..." — substituir por [REDACTED_SECRET].`);
|
|
60
|
+
}
|
|
61
|
+
const em = text.match(PII_EMAIL_REGEX);
|
|
62
|
+
if (em) errors.push(`Email real detectado: "${em[0]}" — usar user@example.com.`);
|
|
63
|
+
|
|
64
|
+
const warnings = [];
|
|
65
|
+
if (lineCount >= SOFT_LIMIT && lineCount <= HARD_LIMIT) {
|
|
66
|
+
warnings.push(`Tamanho ${lineCount}/${HARD_LIMIT} linhas — perto do limite; remover itens resolvidos (≥${SOFT_LIMIT}).`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { ok: errors.length === 0, errors, warnings, lineCount };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The seeded CORE.md (must pass validateCore). Bootstraps the 3 sections so the
|
|
73
|
+
// curated hot layer exists with the right shape from day one.
|
|
74
|
+
export function renderCoreSkeleton(localeId = 'pt-BR') {
|
|
75
|
+
if (localeId === 'en') {
|
|
76
|
+
return `# CORE — curated memory core (.brain)
|
|
77
|
+
|
|
78
|
+
> RULE #1 — the project's canonical memory. Hand-curated, 25-line cap (validate: \`wendkeep validate-memory\`). Volatile facts live in DIGEST.md (auto). Depth: /brain-recall <topic>.
|
|
79
|
+
|
|
80
|
+
## User Preferences
|
|
81
|
+
- (durable preferences: language, style, conventions)
|
|
82
|
+
|
|
83
|
+
## Active Patterns
|
|
84
|
+
- (active patterns/architecture another agent must know)
|
|
85
|
+
|
|
86
|
+
## Open Items
|
|
87
|
+
- (open items/decisions — remove when resolved)
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
return `# CORE — núcleo curado da memória (.brain)
|
|
91
|
+
|
|
92
|
+
> REGRA #1 — memória canônica do projeto. Curado à mão, cap 25 linhas (valide: \`wendkeep validate-memory\`). Volátil vive no DIGEST.md (auto). Profundidade: /brain-recall <tópico>.
|
|
93
|
+
|
|
94
|
+
## Preferências do Usuário
|
|
95
|
+
- (preferências duráveis: idioma, estilo, convenções)
|
|
96
|
+
|
|
97
|
+
## Padrões Ativos
|
|
98
|
+
- (padrões/arquitetura ativos que outro agente precise saber)
|
|
99
|
+
|
|
100
|
+
## Pendências Abertas
|
|
101
|
+
- (pendências/decisões em aberto — remova quando resolvidas)
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The compaction-protocol reference doc dropped into the vault.
|
|
106
|
+
export function renderCompactionProtocol() {
|
|
107
|
+
return `# Protocolo de Memória — núcleo curado + digest automático (.brain)
|
|
108
|
+
|
|
109
|
+
> Como cada agente recebe, consulta e persiste memória entre sessões no seu vault.
|
|
110
|
+
|
|
111
|
+
## 1. Duas camadas
|
|
112
|
+
|
|
113
|
+
- **QUENTE** (auto-injetada por sessão, budget ~45 linhas):
|
|
114
|
+
- \`.brain/CORE.md\` — curado à mão, **≤25 linhas** (1 item/linha): preferências, padrões, pendências.
|
|
115
|
+
- \`.brain/DIGEST.md\` — auto-gerado (0 token LLM, ≤15 linhas): decisões/sessões/bugs/aprendizados recentes.
|
|
116
|
+
- **FRIA** (sob demanda):
|
|
117
|
+
- \`.brain/index.jsonl\` — índice de todas as sessões (1/linha, frontmatter).
|
|
118
|
+
- Vault: \`02-Sessões/**\`, \`04-Decisões/**\`, \`05-Bugs/**\`, \`06-Aprendizados/**\`. Desce via \`/brain-recall <tópico>\`.
|
|
119
|
+
|
|
120
|
+
## 2. Compactação = regra de geração (sem trabalho manual)
|
|
121
|
+
|
|
122
|
+
- **DIGEST se auto-compacta**: caps determinísticos (5 decisões, 4 sessões, 2 bugs, 2 aprendizados + \`+N mais\`). O velho cai do quente sozinho e permanece no índice/vault. **NUNCA editar** \`DIGEST.md\`/\`index.jsonl\`.
|
|
123
|
+
- **CORE**: quando ≥22 linhas (soft warning), remover itens resolvidos/obsoletos — o detalhe já vive no vault e no histórico do git.
|
|
124
|
+
|
|
125
|
+
## 3. O que escrever no CORE
|
|
126
|
+
|
|
127
|
+
Só estado **durável** que outro agente precise saber — preferência, padrão ativo, pendência aberta. 1 linha por item. Nunca log de sessão (isso é automático no vault).
|
|
128
|
+
|
|
129
|
+
3 seções fixas (obrigatórias): \`## Preferências do Usuário\`, \`## Padrões Ativos\`, \`## Pendências Abertas\`.
|
|
130
|
+
|
|
131
|
+
## 4. Sem segredos / PII
|
|
132
|
+
|
|
133
|
+
\`CORE.md\` nunca contém tokens (\`sk_*\`, \`whsec_*\`, JWT, Bearer), API keys, senhas ou email/telefone real. Use \`[REDACTED_SECRET]\` / \`user@example.com\`.
|
|
134
|
+
|
|
135
|
+
## 5. Validação
|
|
136
|
+
|
|
137
|
+
\`\`\`bash
|
|
138
|
+
wendkeep validate-memory # valida <vault>/.brain/CORE.md
|
|
139
|
+
wendkeep validate-memory <path> # valida outro arquivo
|
|
140
|
+
\`\`\`
|
|
141
|
+
|
|
142
|
+
Checa: cap 25 (soft 22), 3 seções, sem segredos/PII. Exit 0 = OK, 1 = falha.
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// CLI entry for `wendkeep validate-memory [path]`. Resolves the target from an
|
|
147
|
+
// explicit path, else <vault>/.brain/CORE.md (--vault or OBSIDIAN_VAULT_PATH).
|
|
148
|
+
export function runValidateMemory(argv) {
|
|
149
|
+
let target;
|
|
150
|
+
let vault;
|
|
151
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
152
|
+
const a = argv[i];
|
|
153
|
+
if (a === '--vault') vault = argv[++i];
|
|
154
|
+
else if (a.startsWith('--vault=')) vault = a.slice(8);
|
|
155
|
+
else if (!a.startsWith('-')) target = a;
|
|
156
|
+
}
|
|
157
|
+
if (!target) {
|
|
158
|
+
const base = vault || process.env.OBSIDIAN_VAULT_PATH;
|
|
159
|
+
if (!base) {
|
|
160
|
+
process.stderr.write('wendkeep validate-memory: no target. Pass a path, --vault <path>, or set OBSIDIAN_VAULT_PATH.\n');
|
|
161
|
+
process.exit(2);
|
|
162
|
+
}
|
|
163
|
+
target = join(base, '.brain', 'CORE.md');
|
|
164
|
+
}
|
|
165
|
+
const abs = isAbsolute(target) ? target : resolve(process.cwd(), target);
|
|
166
|
+
if (!existsSync(abs)) {
|
|
167
|
+
process.stderr.write(`wendkeep validate-memory: not found: ${abs}\n`);
|
|
168
|
+
process.exit(2);
|
|
169
|
+
}
|
|
170
|
+
const res = validateCore(readFileSync(abs, 'utf8'));
|
|
171
|
+
if (!res.ok) {
|
|
172
|
+
process.stderr.write(`❌ CORE.md viola protocolo (${res.errors.length} erro${res.errors.length > 1 ? 's' : ''}):\n`);
|
|
173
|
+
for (const e of res.errors) process.stderr.write(` - ${e}\n`);
|
|
174
|
+
process.stderr.write('\nProtocolo: .brain/COMPACTION_PROTOCOL.md\n');
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
let msg = `✅ CORE.md OK (${res.lineCount} linhas, 3/3 seções, sem segredos).`;
|
|
178
|
+
for (const w of res.warnings) msg += `\n ⚠ ${w}`;
|
|
179
|
+
process.stdout.write(`${msg}\n`);
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { validateMemoryEvent, validateSharedMemory } from './memory-schema.mjs';
|
|
4
|
+
import { assertVaultPathSafe } from './vault-path-safety.mjs';
|
|
5
|
+
import { validateCore } from './validate-core.mjs';
|
|
6
|
+
|
|
7
|
+
function failedComponent(errors, extra = {}) {
|
|
8
|
+
return { ok: false, errors: Array.isArray(errors) ? errors : [errors], warnings: [], ...extra };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function readRequired(vaultBase, path, label) {
|
|
12
|
+
let checked;
|
|
13
|
+
try {
|
|
14
|
+
checked = assertVaultPathSafe(vaultBase, path, {
|
|
15
|
+
expectedType: 'file', label: `artefato ${label}`,
|
|
16
|
+
});
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return { ok: false, error: `${label} inseguro: ${error?.message || error}` };
|
|
19
|
+
}
|
|
20
|
+
if (!checked.exists) return { ok: false, error: `${label} ausente: ${path}` };
|
|
21
|
+
try {
|
|
22
|
+
// Deliberately adjacent to the open performed by readFileSync.
|
|
23
|
+
checked = assertVaultPathSafe(vaultBase, checked.target, {
|
|
24
|
+
allowMissing: false, expectedType: 'file', label: `artefato ${label}`,
|
|
25
|
+
});
|
|
26
|
+
return { ok: true, content: readFileSync(checked.target, 'utf8') };
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return { ok: false, error: `${label} ilegível: ${error?.message || error}` };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function readProjectForValidation(vaultBase) {
|
|
33
|
+
const path = join(vaultBase, '.brain', 'PROJECT.json');
|
|
34
|
+
const read = readRequired(vaultBase, path, 'PROJECT.json');
|
|
35
|
+
if (!read.ok) return failedComponent(read.error, { projectId: '', path });
|
|
36
|
+
try {
|
|
37
|
+
const marker = JSON.parse(read.content);
|
|
38
|
+
if (!marker || typeof marker.projectId !== 'string' || !marker.projectId) {
|
|
39
|
+
return failedComponent('PROJECT.json inválido: projectId ausente.', { projectId: '', path });
|
|
40
|
+
}
|
|
41
|
+
return { ok: true, errors: [], warnings: [], projectId: marker.projectId, marker, path };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return failedComponent(`PROJECT.json contém JSON inválido: ${error?.message || error}`, { projectId: '', path });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Read and validate the append-only JSONL authority without repairing or mutating it. */
|
|
48
|
+
export function readLedgerForValidation(vaultBase, { projectId } = {}) {
|
|
49
|
+
const path = join(vaultBase, '.brain', 'MEMORY_EVENTS.jsonl');
|
|
50
|
+
const read = readRequired(vaultBase, path, 'MEMORY_EVENTS.jsonl');
|
|
51
|
+
if (!read.ok) return failedComponent(read.error, { events: [], eventIds: new Set(), path });
|
|
52
|
+
|
|
53
|
+
const errors = [];
|
|
54
|
+
const warnings = [];
|
|
55
|
+
const events = [];
|
|
56
|
+
const eventIds = new Set();
|
|
57
|
+
const normalized = read.content.replace(/\r\n/g, '\n');
|
|
58
|
+
const lines = normalized.split('\n');
|
|
59
|
+
const logicalLines = normalized === '' ? [] : (normalized.endsWith('\n') ? lines.slice(0, -1) : lines);
|
|
60
|
+
logicalLines.forEach((line, index) => {
|
|
61
|
+
if (!line.trim()) {
|
|
62
|
+
errors.push(`MEMORY_EVENTS.jsonl linha ${index + 1} está vazia no meio do ledger.`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
let event;
|
|
66
|
+
try {
|
|
67
|
+
event = JSON.parse(line);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
errors.push(`MEMORY_EVENTS.jsonl linha ${index + 1} contém JSON inválido/parcial: ${error?.message || error}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const validation = validateMemoryEvent(event, projectId ? { projectId } : {});
|
|
73
|
+
for (const error of validation.errors) errors.push(`MEMORY_EVENTS.jsonl linha ${index + 1}: ${error}`);
|
|
74
|
+
for (const warning of validation.warnings) warnings.push(`MEMORY_EVENTS.jsonl linha ${index + 1}: ${warning}`);
|
|
75
|
+
if (typeof event?.event_id === 'string' && event.event_id) {
|
|
76
|
+
if (eventIds.has(event.event_id)) errors.push(`MEMORY_EVENTS.jsonl event_id duplicado: ${event.event_id}.`);
|
|
77
|
+
eventIds.add(event.event_id);
|
|
78
|
+
}
|
|
79
|
+
events.push(event);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
ok: errors.length === 0,
|
|
84
|
+
errors,
|
|
85
|
+
warnings,
|
|
86
|
+
events,
|
|
87
|
+
eventIds,
|
|
88
|
+
lineCount: logicalLines.length,
|
|
89
|
+
path,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validateCoreArtifact(vaultBase) {
|
|
94
|
+
const path = join(vaultBase, '.brain', 'CORE.md');
|
|
95
|
+
const read = readRequired(vaultBase, path, 'CORE.md');
|
|
96
|
+
if (!read.ok) return failedComponent(read.error, { lineCount: 0, path });
|
|
97
|
+
return { ...validateCore(read.content), path, content: read.content };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validateSharedArtifact(vaultBase, eventIds) {
|
|
101
|
+
const path = join(vaultBase, '.brain', 'SHARED_MEMORY.md');
|
|
102
|
+
const read = readRequired(vaultBase, path, 'SHARED_MEMORY.md');
|
|
103
|
+
if (!read.ok) return failedComponent(read.error, { path });
|
|
104
|
+
return { ...validateSharedMemory(read.content, { eventIds }), path, content: read.content };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function combineMemoryResults({ project, core, ledger, shared }) {
|
|
108
|
+
const components = { project, core, ledger, shared };
|
|
109
|
+
const errors = [];
|
|
110
|
+
const warnings = [];
|
|
111
|
+
for (const [name, result] of Object.entries(components)) {
|
|
112
|
+
for (const error of result?.errors || []) errors.push(`${name}: ${error}`);
|
|
113
|
+
for (const warning of result?.warnings || []) warnings.push(`${name}: ${warning}`);
|
|
114
|
+
}
|
|
115
|
+
return { ok: errors.length === 0, errors, warnings, ...components };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Validate the v2 local memory bundle as a read-only composition. Missing/corrupt
|
|
120
|
+
* artifacts remain explicit failures; they are never silently treated as empty.
|
|
121
|
+
*/
|
|
122
|
+
export function validateMemoryBundle(vaultBase) {
|
|
123
|
+
const project = readProjectForValidation(vaultBase);
|
|
124
|
+
const core = validateCoreArtifact(vaultBase);
|
|
125
|
+
const ledger = readLedgerForValidation(vaultBase, { projectId: project.projectId });
|
|
126
|
+
const shared = validateSharedArtifact(vaultBase, ledger.eventIds);
|
|
127
|
+
return combineMemoryResults({ project, core, ledger, shared });
|
|
128
|
+
}
|
package/src/memory.mjs
CHANGED
|
@@ -295,6 +295,14 @@ function attemptFingerprint(attempt) {
|
|
|
295
295
|
return hash(canonicalMemoryJson(attempt || null));
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
function memoryCheckpointFingerprint(entry) {
|
|
299
|
+
const present = Object.prototype.hasOwnProperty.call(entry || {}, 'memory_checkpoint');
|
|
300
|
+
return hash(canonicalMemoryJson({
|
|
301
|
+
present,
|
|
302
|
+
checkpoint: present ? (entry.memory_checkpoint ?? null) : null,
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
|
|
298
306
|
function matchingAppliedReconciliation(entry, request) {
|
|
299
307
|
const attempt = entry?.last_memory_attempt;
|
|
300
308
|
if (attempt?.memory_mode !== 'v2' || attempt?.state !== 'skipped' || attempt?.disposition !== 'superseded') return null;
|
|
@@ -723,27 +731,81 @@ function checkpointShape(checkpoint) {
|
|
|
723
731
|
&& typeof checkpoint.state_hash === 'string' && checkpoint.state_hash;
|
|
724
732
|
}
|
|
725
733
|
|
|
726
|
-
function
|
|
734
|
+
function legacyEventOrder(left, right) {
|
|
735
|
+
return (Number(left.base_revision ?? 0) - Number(right.base_revision ?? 0))
|
|
736
|
+
|| String(left.effective_at || left.observed_at).localeCompare(String(right.effective_at || right.observed_at))
|
|
737
|
+
|| Number(left.turn_sequence ?? 0) - Number(right.turn_sequence ?? 0)
|
|
738
|
+
|| String(left.event_id).localeCompare(String(right.event_id));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function historicalAssertOnlyCheckpoint(vault, attempt, authority, checkpoint) {
|
|
742
|
+
const requiredEventIds = Array.isArray(attempt?.event_ids) ? [...attempt.event_ids] : [];
|
|
743
|
+
if (!requiredEventIds.includes(checkpoint.event_cursor)) return null;
|
|
744
|
+
|
|
745
|
+
const cursorIndex = authority.ledgerEvents
|
|
746
|
+
.findIndex((event) => event.event_id === checkpoint.event_cursor);
|
|
747
|
+
if (cursorIndex < 0) return null;
|
|
748
|
+
const prefix = authority.ledgerEvents.slice(0, cursorIndex + 1);
|
|
749
|
+
const prefixIds = new Set(prefix.map((event) => event.event_id));
|
|
750
|
+
if (requiredEventIds.some((eventId) => !prefixIds.has(eventId))) return null;
|
|
751
|
+
if (prefix.some((event) => event.operation !== 'assert')) return null;
|
|
752
|
+
|
|
753
|
+
const firstByKey = [];
|
|
754
|
+
const priorByKey = new Map();
|
|
755
|
+
for (const event of [...prefix].sort(legacyEventOrder)) {
|
|
756
|
+
const prior = priorByKey.get(event.memory_key);
|
|
757
|
+
if (prior) {
|
|
758
|
+
const sameActivation = Boolean(event.canonical_session_id)
|
|
759
|
+
&& event.canonical_session_id === prior.canonical_session_id
|
|
760
|
+
&& event.activation_id === prior.activation_id;
|
|
761
|
+
if (!sameActivation || !Number.isInteger(event.turn_sequence)
|
|
762
|
+
|| event.turn_sequence <= prior.turn_sequence) return null;
|
|
763
|
+
} else {
|
|
764
|
+
firstByKey.push(event);
|
|
765
|
+
}
|
|
766
|
+
priorByKey.set(event.memory_key, event);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const legacyState = deriveMemoryProjection(vault, firstByKey);
|
|
770
|
+
const currentPrefix = deriveMemoryProjection(vault, prefix);
|
|
771
|
+
if (legacyState.candidates.length || currentPrefix.candidates.length) return null;
|
|
772
|
+
const legacyCheckpoint = {
|
|
773
|
+
revision: legacyState.revision,
|
|
774
|
+
event_cursor: currentPrefix.eventCursor,
|
|
775
|
+
state_hash: legacyState.stateHash,
|
|
776
|
+
};
|
|
777
|
+
if (!sameCheckpoint(checkpoint, legacyCheckpoint)) return null;
|
|
778
|
+
return currentPrefix.checkpoint;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function legacyCheckpointMigration(vault, sessionId, entry, authority, fullReplay) {
|
|
782
|
+
const attempt = entry?.last_memory_attempt;
|
|
727
783
|
const checkpoint = attempt?.checkpoint;
|
|
728
784
|
const requiredEventIds = Array.isArray(attempt?.event_ids) ? [...attempt.event_ids] : [];
|
|
729
785
|
if (!checkpointShape(checkpoint) || !requiredEventIds.length) return null;
|
|
786
|
+
if (entry?.memory_checkpoint !== undefined
|
|
787
|
+
&& !sameCheckpoint(entry.memory_checkpoint, checkpoint)) return null;
|
|
730
788
|
// The pre-physical format had no explicit causal_event_cursor: event_cursor itself
|
|
731
789
|
// named the reducer's causal tail while revision/hash described the full authority
|
|
732
|
-
// snapshot.
|
|
733
|
-
// fail-closed
|
|
790
|
+
// snapshot. Historical prefixes are accepted only by the narrower assert-only proof below;
|
|
791
|
+
// every other ambiguous tuple remains fail-closed for explicit human reconciliation.
|
|
734
792
|
if (checkpoint.causal_event_cursor !== undefined) return null;
|
|
735
793
|
if (requiredEventIds.some((eventId) => !authority.ledgerById.has(eventId))) return null;
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
794
|
+
const matchesFullReplay = checkpoint.revision === fullReplay.revision
|
|
795
|
+
&& checkpoint.state_hash === fullReplay.stateHash
|
|
796
|
+
&& checkpoint.event_cursor === fullReplay.eventCursor;
|
|
797
|
+
const nextCheckpoint = matchesFullReplay
|
|
798
|
+
? fullReplay.checkpoint
|
|
799
|
+
: historicalAssertOnlyCheckpoint(vault, attempt, authority, checkpoint);
|
|
800
|
+
if (!nextCheckpoint || sameCheckpoint(checkpoint, nextCheckpoint)) return null;
|
|
740
801
|
|
|
741
802
|
const proof = validateSuccessorProof({ bySessionId: sessionId }, attempt, authority);
|
|
742
803
|
return {
|
|
743
804
|
sessionId,
|
|
744
805
|
expectedFingerprint: attemptFingerprint(attempt),
|
|
806
|
+
expectedMemoryCheckpointFingerprint: memoryCheckpointFingerprint(entry),
|
|
745
807
|
originalCheckpoint: cloneJson(checkpoint),
|
|
746
|
-
checkpoint: cloneJson(
|
|
808
|
+
checkpoint: cloneJson(nextCheckpoint),
|
|
747
809
|
proof,
|
|
748
810
|
};
|
|
749
811
|
}
|
|
@@ -759,7 +821,7 @@ export function migrateLegacyMemoryCheckpoints(vault, {
|
|
|
759
821
|
const fullReplay = deriveMemoryProjection(vault, authority.ledgerEvents);
|
|
760
822
|
const plans = Object.entries(inspected.sessions || {})
|
|
761
823
|
.map(([sessionId, entry]) => legacyCheckpointMigration(
|
|
762
|
-
sessionId, entry
|
|
824
|
+
vault, sessionId, entry, authority, fullReplay,
|
|
763
825
|
))
|
|
764
826
|
.filter(Boolean);
|
|
765
827
|
assertAuthorityMatches(expectedAuthority, readMemoryAuthority(vault));
|
|
@@ -773,10 +835,14 @@ export function migrateLegacyMemoryCheckpoints(vault, {
|
|
|
773
835
|
return mutateSessionRegistry(vault, (registry) => {
|
|
774
836
|
// Validate every CAS before changing the first entry, making the batch atomic.
|
|
775
837
|
for (const plan of plans) {
|
|
776
|
-
const
|
|
838
|
+
const entry = registry.sessions?.[plan.sessionId];
|
|
839
|
+
const attempt = entry?.last_memory_attempt;
|
|
777
840
|
if (attemptFingerprint(attempt) !== plan.expectedFingerprint) {
|
|
778
841
|
throw new Error(`CAS perdido: checkpoint da sessão ${plan.sessionId} mudou durante a migração estrutural.`);
|
|
779
842
|
}
|
|
843
|
+
if (memoryCheckpointFingerprint(entry) !== plan.expectedMemoryCheckpointFingerprint) {
|
|
844
|
+
throw new Error(`CAS perdido: memory_checkpoint da sessão ${plan.sessionId} mudou durante a migração estrutural.`);
|
|
845
|
+
}
|
|
780
846
|
}
|
|
781
847
|
|
|
782
848
|
const path = registryPath(vault);
|
|
@@ -795,7 +861,7 @@ export function migrateLegacyMemoryCheckpoints(vault, {
|
|
|
795
861
|
|
|
796
862
|
for (const plan of plans) {
|
|
797
863
|
const entry = registry.sessions[plan.sessionId];
|
|
798
|
-
const reconciliationId = `memcp-${hash(`${plan.sessionId}\0${plan.expectedFingerprint}\0${canonicalMemoryJson(plan.checkpoint)}`).slice(0, 20)}`;
|
|
864
|
+
const reconciliationId = `memcp-${hash(`${plan.sessionId}\0${plan.expectedFingerprint}\0${plan.expectedMemoryCheckpointFingerprint}\0${canonicalMemoryJson(plan.checkpoint)}`).slice(0, 20)}`;
|
|
799
865
|
entry.memory_reconciliations = [
|
|
800
866
|
...(Array.isArray(entry.memory_reconciliations) ? entry.memory_reconciliations : []),
|
|
801
867
|
{
|
|
@@ -1,133 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
'OFF',
|
|
3
|
-
'FLOW',
|
|
4
|
-
'GUIDE',
|
|
5
|
-
'GOVERN',
|
|
6
|
-
'ASSURE',
|
|
7
|
-
]);
|
|
8
|
-
export const DEFAULT_OPERATING_PROFILE = 'GOVERN';
|
|
9
|
-
|
|
10
|
-
const PROFILE_SET = new Set(OPERATING_PROFILES);
|
|
11
|
-
|
|
12
|
-
function policy(profile, route, options) {
|
|
13
|
-
return Object.freeze({
|
|
14
|
-
profile,
|
|
15
|
-
route: Object.freeze(route),
|
|
16
|
-
keepCore: true,
|
|
17
|
-
...options,
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const OPERATING_PROFILE_POLICIES = Object.freeze({
|
|
22
|
-
OFF: policy('OFF', ['LLM'], {
|
|
23
|
-
harness: false,
|
|
24
|
-
contract: 'native',
|
|
25
|
-
requiresChange: false,
|
|
26
|
-
requiresReview: false,
|
|
27
|
-
requiresConfirmation: false,
|
|
28
|
-
}),
|
|
29
|
-
FLOW: policy('FLOW', ['E', 'V'], {
|
|
30
|
-
harness: true,
|
|
31
|
-
contract: 'flow',
|
|
32
|
-
requiresChange: false,
|
|
33
|
-
requiresReview: false,
|
|
34
|
-
requiresConfirmation: false,
|
|
35
|
-
}),
|
|
36
|
-
GUIDE: policy('GUIDE', ['P', 'E', 'V'], {
|
|
37
|
-
harness: true,
|
|
38
|
-
contract: 'simple-change',
|
|
39
|
-
requiresChange: true,
|
|
40
|
-
requiresReview: false,
|
|
41
|
-
requiresConfirmation: false,
|
|
42
|
-
}),
|
|
43
|
-
GOVERN: policy('GOVERN', ['P', 'R', 'E', 'V'], {
|
|
44
|
-
harness: true,
|
|
45
|
-
contract: 'change',
|
|
46
|
-
requiresChange: true,
|
|
47
|
-
requiresReview: true,
|
|
48
|
-
requiresConfirmation: false,
|
|
49
|
-
}),
|
|
50
|
-
ASSURE: policy('ASSURE', ['P', 'R', 'E', 'V', 'C'], {
|
|
51
|
-
harness: true,
|
|
52
|
-
contract: 'change',
|
|
53
|
-
requiresChange: true,
|
|
54
|
-
requiresReview: true,
|
|
55
|
-
requiresConfirmation: true,
|
|
56
|
-
}),
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
function invalidProfileError(value) {
|
|
60
|
-
const rendered = typeof value === 'string' ? `"${value}"` : String(value);
|
|
61
|
-
const error = new Error(
|
|
62
|
-
`Perfil de Operação inválido: ${rendered}. Use ${OPERATING_PROFILES.join(', ')}.`,
|
|
63
|
-
);
|
|
64
|
-
error.code = 'WENDKEEP_OPERATING_PROFILE_INVALID';
|
|
65
|
-
return error;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function canonicalProfile(value) {
|
|
69
|
-
if (typeof value !== 'string') return '';
|
|
70
|
-
return value.trim().toUpperCase();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function normalizeOperatingProfile(value, { strict = false } = {}) {
|
|
74
|
-
const normalized = canonicalProfile(value);
|
|
75
|
-
if (PROFILE_SET.has(normalized)) return normalized;
|
|
76
|
-
if (strict) throw invalidProfileError(value);
|
|
77
|
-
return DEFAULT_OPERATING_PROFILE;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function resolveOperatingProfile(config = {}) {
|
|
81
|
-
const harness = config && typeof config === 'object' && !Array.isArray(config)
|
|
82
|
-
&& config.harness && typeof config.harness === 'object' && !Array.isArray(config.harness)
|
|
83
|
-
? config.harness
|
|
84
|
-
: null;
|
|
85
|
-
const configured = !!harness && Object.prototype.hasOwnProperty.call(harness, 'profile');
|
|
86
|
-
if (!configured) {
|
|
87
|
-
return {
|
|
88
|
-
profile: DEFAULT_OPERATING_PROFILE,
|
|
89
|
-
source: 'default',
|
|
90
|
-
valid: true,
|
|
91
|
-
configured: false,
|
|
92
|
-
raw: null,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const raw = harness.profile;
|
|
97
|
-
const normalized = canonicalProfile(raw);
|
|
98
|
-
if (PROFILE_SET.has(normalized)) {
|
|
99
|
-
return {
|
|
100
|
-
profile: normalized,
|
|
101
|
-
source: 'project-binding',
|
|
102
|
-
valid: true,
|
|
103
|
-
configured: true,
|
|
104
|
-
raw,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
return {
|
|
108
|
-
profile: DEFAULT_OPERATING_PROFILE,
|
|
109
|
-
source: 'default-invalid',
|
|
110
|
-
valid: false,
|
|
111
|
-
configured: true,
|
|
112
|
-
raw,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function operatingProfilePolicy(value) {
|
|
117
|
-
return OPERATING_PROFILE_POLICIES[normalizeOperatingProfile(value)];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function setOperatingProfile(config = {}, value) {
|
|
121
|
-
const profile = normalizeOperatingProfile(value, { strict: true });
|
|
122
|
-
const base = config && typeof config === 'object' && !Array.isArray(config) ? config : {};
|
|
123
|
-
const harness = base.harness && typeof base.harness === 'object' && !Array.isArray(base.harness)
|
|
124
|
-
? base.harness
|
|
125
|
-
: {};
|
|
126
|
-
return {
|
|
127
|
-
...base,
|
|
128
|
-
harness: {
|
|
129
|
-
...harness,
|
|
130
|
-
profile,
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
}
|
|
1
|
+
export * from '../packages/harness/src/operating-profile.mjs';
|