wendkeep 0.65.0 → 0.66.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 +32 -0
- package/README.en.md +15 -5
- package/README.md +15 -5
- package/docs/en/commands/getting-started.md +8 -0
- package/docs/pt-BR/commands/getting-started.md +8 -0
- package/hooks/obsidian-common.mjs +24 -124
- package/hooks/session-identity.mjs +21 -136
- package/hooks/session-stop.mjs +18 -384
- package/hooks/token-usage.mjs +12 -56
- package/package.json +13 -4
- package/packages/integrations/package.json +2 -1
- package/packages/integrations/src/hook-envelope.mjs +103 -0
- package/packages/integrations/src/host-hooks.mjs +80 -0
- package/packages/integrations/src/index.mjs +6 -0
- package/packages/integrations/src/prompt-content.mjs +20 -0
- package/packages/integrations/src/session-identity.mjs +171 -0
- package/packages/integrations/src/transcript-usage.mjs +53 -0
- package/packages/integrations/src/transcripts.mjs +423 -0
- package/src/taxonomy.mjs +24 -78
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@ All notable changes to **wendkeep** are documented here. Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.66.0] — 2026-07-29
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **O workspace privado Integrations passa a ter um kernel canônico para Claude Code e Codex.**
|
|
12
|
+
Catálogo/projeção de hooks, envelope e provider, filtros de conteúdo, normalização de uso,
|
|
13
|
+
parsers de transcript e identidade de sessão/turno agora vivem em módulos puros sob
|
|
14
|
+
`packages/integrations/src/`, sem filesystem, ambiente global, Vault ou registry no import.
|
|
15
|
+
- **O tarball instalado prova a fronteira completa fora do checkout.** O teste executa `init` e
|
|
16
|
+
um hook `session-ensure` em consumidor temporário, valida estado persistido e as projeções
|
|
17
|
+
Claude/Codex contra o kernel empacotado, e confirma que Integrations permanece interno à
|
|
18
|
+
única publicação `wendkeep`.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **As fachadas históricas agora injetam efeitos no kernel de Integrations.** Taxonomia e hooks
|
|
23
|
+
preservam assinaturas e identidade dos exports enquanto stdin/stdout, `process.env`, leitura de
|
|
24
|
+
transcripts, acesso ao Vault e ao registry continuam nas bordas; paths, schemas, configs e
|
|
25
|
+
sessões existentes não exigem migração.
|
|
26
|
+
- **Os workspaces privados deixaram de vazar por deep imports do wildcard raiz.** A allowlist de
|
|
27
|
+
exports mantém `wendkeep/harness`, `wendkeep/vault` e os caminhos históricos publicados, mas
|
|
28
|
+
bloqueia `packages/*`, inclusive variantes percent-encoded; MCP e Integrations seguem como
|
|
29
|
+
adapters irmãos sem dependência direta.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **A reconexão de sessão por transcript agora respeita o provider.** Uma entrada do registry de
|
|
34
|
+
outro host é ignorada sem substituir uma identidade canônica válida já inspecionada.
|
|
35
|
+
- **A suíte integral mantém determinismo sob carga de I/O no Windows.** O runner limita a duas as
|
|
36
|
+
files executadas em paralelo, preservando a concorrência multiprocesso dentro dos testes de
|
|
37
|
+
CAS/locks e todos os asserts, sem falsos vermelhos causados por starvation entre suítes.
|
|
38
|
+
|
|
7
39
|
## [0.65.0] — 2026-07-29
|
|
8
40
|
|
|
9
41
|
### Added
|
package/README.en.md
CHANGED
|
@@ -17,14 +17,24 @@
|
|
|
17
17
|
|
|
18
18
|
The runtime is being separated into six physical boundaries — `cli`, `harness`, `vault`, `mcp`,
|
|
19
19
|
`integrations`, and `pi` — without fragmenting installation. The private `cli`, `harness`,
|
|
20
|
-
`vault`, and `
|
|
21
|
-
sensor engine, safe binding/the Shared Project Memory v2 kernel,
|
|
22
|
-
|
|
23
|
-
`wendkeep/vault`; CLI
|
|
24
|
-
|
|
20
|
+
`vault`, `mcp`, and `integrations` workspaces now canonically own the executable runtime,
|
|
21
|
+
Operating Profiles/the sensor engine, safe binding/the Shared Project Memory v2 kernel, the MCP
|
|
22
|
+
configuration kernel, and pure Claude/Codex integration rules, respectively. The root package
|
|
23
|
+
exposes Harness and Vault through `wendkeep/harness` and `wendkeep/vault`; CLI, MCP, and
|
|
24
|
+
Integrations remain private surfaces, reached only through the binaries, the configuration effects
|
|
25
|
+
of `init`, and historical facades. Historical imports keep working through compatibility facades
|
|
25
26
|
and no session data needs migration;
|
|
26
27
|
see the [modular architecture](docs/en/architecture.md).
|
|
27
28
|
|
|
29
|
+
In the **0.66 Integrations Kernel** phase, `packages/integrations/src/` becomes the canonical
|
|
30
|
+
authority for the hook catalog and projection, the envelope/provider, transcript content and usage
|
|
31
|
+
filters and parsers, and session identity. These rules are pure: stdin/stdout, environment,
|
|
32
|
+
filesystem, Vault, and registry effects remain in the historical facades. MCP and Integrations are
|
|
33
|
+
sibling adapters with no dependency between them, and the direction remains
|
|
34
|
+
`cli/mcp/integrations/pi → Harness → Vault`. Hooks, sessions, paths, configuration, and schemas
|
|
35
|
+
remain equivalent; the private `@wendkeep/integrations` workspace stays inside the single
|
|
36
|
+
published `wendkeep` package, with no public `wendkeep/integrations` subpath. Pi is the next phase.
|
|
37
|
+
|
|
28
38
|
In the **0.65 MCP Configuration Kernel** phase, `packages/mcp/src/config.mjs` becomes the
|
|
29
39
|
canonical authority for the MCPVault entry, catalog-described server selection, and `.mcp.json`
|
|
30
40
|
merging. `src/taxonomy.mjs` supplies descriptors while `src/init.mjs` retains only filesystem
|
package/README.md
CHANGED
|
@@ -17,14 +17,24 @@
|
|
|
17
17
|
|
|
18
18
|
The runtime is being separated into six physical boundaries — `cli`, `harness`, `vault`, `mcp`,
|
|
19
19
|
`integrations`, and `pi` — without fragmenting installation. The private `cli`, `harness`,
|
|
20
|
-
`vault`, and `
|
|
21
|
-
sensor engine, safe binding/the Shared Project Memory v2 kernel,
|
|
22
|
-
|
|
23
|
-
`wendkeep/vault`; CLI
|
|
24
|
-
|
|
20
|
+
`vault`, `mcp`, and `integrations` workspaces now canonically own the executable runtime,
|
|
21
|
+
Operating Profiles/the sensor engine, safe binding/the Shared Project Memory v2 kernel, the MCP
|
|
22
|
+
configuration kernel, and pure Claude/Codex integration rules, respectively. The root package
|
|
23
|
+
exposes Harness and Vault through `wendkeep/harness` and `wendkeep/vault`; CLI, MCP, and
|
|
24
|
+
Integrations remain private surfaces, reached only through the binaries, the configuration effects
|
|
25
|
+
of `init`, and historical facades. Historical imports keep working through compatibility facades
|
|
25
26
|
and no session data needs migration;
|
|
26
27
|
see the [modular architecture](docs/en/architecture.md).
|
|
27
28
|
|
|
29
|
+
In the **0.66 Integrations Kernel** phase, `packages/integrations/src/` becomes the canonical
|
|
30
|
+
authority for the hook catalog and projection, the envelope/provider, transcript content and usage
|
|
31
|
+
filters and parsers, and session identity. These rules are pure: stdin/stdout, environment,
|
|
32
|
+
filesystem, Vault, and registry effects remain in the historical facades. MCP and Integrations are
|
|
33
|
+
sibling adapters with no dependency between them, and the direction remains
|
|
34
|
+
`cli/mcp/integrations/pi → Harness → Vault`. Hooks, sessions, paths, configuration, and schemas
|
|
35
|
+
remain equivalent; the private `@wendkeep/integrations` workspace stays inside the single
|
|
36
|
+
published `wendkeep` package, with no public `wendkeep/integrations` subpath. Pi is the next phase.
|
|
37
|
+
|
|
28
38
|
In the **0.65 MCP Configuration Kernel** phase, `packages/mcp/src/config.mjs` becomes the
|
|
29
39
|
canonical authority for the MCPVault entry, catalog-described server selection, and `.mcp.json`
|
|
30
40
|
merging. `src/taxonomy.mjs` supplies descriptors while `src/init.mjs` retains only filesystem
|
|
@@ -78,6 +78,14 @@ When MCP is enabled, `init` preserves existing properties and servers in `.mcp.j
|
|
|
78
78
|
and the reconciled proposal is written to `.mcp.json.new`. Since version 0.65, this composition is
|
|
79
79
|
owned by the private MCP kernel without changing commands, flags, or the public npm surface.
|
|
80
80
|
|
|
81
|
+
Pure rules that project Claude/Codex hooks and interpret envelopes, transcripts, usage, and
|
|
82
|
+
identity belong to the private `@wendkeep/integrations` workspace. Historical facades retain
|
|
83
|
+
stdin/stdout, environment, filesystem, Vault, and registry effects. This adds no commands or flags,
|
|
84
|
+
changes neither hooks nor sessions, and requires no Vault, configuration, path, or schema migration.
|
|
85
|
+
MCP and Integrations remain sibling adapters with no dependency between them, and the direction
|
|
86
|
+
remains `cli/mcp/integrations/pi → Harness → Vault`. Everything stays inside the single published
|
|
87
|
+
`wendkeep` package; there is no public `wendkeep/integrations` surface. Pi is the next modular phase.
|
|
88
|
+
|
|
81
89
|
## Common errors and diagnosis
|
|
82
90
|
|
|
83
91
|
- Wrong vault: inspect `.wendkeep.json` and run `wendkeep doctor --vault <path>`.
|
|
@@ -79,6 +79,14 @@ e adiciona `wendkeep-vault`. Se o JSON existente for inválido, o arquivo origin
|
|
|
79
79
|
a byte intacto e a proposta reconciliada é gravada em `.mcp.json.new`. Desde a versão 0.65, essa
|
|
80
80
|
composição pertence ao kernel MCP privado, sem alterar comandos, flags ou a superfície npm pública.
|
|
81
81
|
|
|
82
|
+
As regras puras que projetam os hooks de Claude/Codex e interpretam envelopes, transcripts, uso e
|
|
83
|
+
identidade pertencem ao workspace privado `@wendkeep/integrations`. As fachadas históricas mantêm
|
|
84
|
+
os efeitos de stdin/stdout, ambiente, filesystem, Vault e registry. Isso não acrescenta comandos ou
|
|
85
|
+
flags, não altera hooks ou sessões e não exige migração de cofre, config, paths ou schemas. MCP e
|
|
86
|
+
Integrations permanecem adapters irmãos sem dependência, e a direção continua
|
|
87
|
+
`cli/mcp/integrations/pi → Harness → Vault`. Tudo permanece dentro do único pacote publicado
|
|
88
|
+
`wendkeep`; não existe `wendkeep/integrations` público. A próxima fase modular é Pi.
|
|
89
|
+
|
|
82
90
|
## Erros comuns e diagnóstico
|
|
83
91
|
|
|
84
92
|
- Cofre errado: confira `.wendkeep.json` e rode `wendkeep doctor --vault <path>`.
|
|
@@ -7,6 +7,26 @@ import { resolveProjectVault } from '../src/project-vault.mjs';
|
|
|
7
7
|
import {
|
|
8
8
|
assertVaultPathSafe, mkdirVaultPath, writeVaultFileAtomic,
|
|
9
9
|
} from './vault-path-safety.mjs';
|
|
10
|
+
import {
|
|
11
|
+
salvageTruncatedJson,
|
|
12
|
+
parseHookInput,
|
|
13
|
+
stringifyHookOutput,
|
|
14
|
+
detectProvider as detectProviderFromEnvironment,
|
|
15
|
+
providerMeta as providerMetaFromProvider,
|
|
16
|
+
extractHookPrompt,
|
|
17
|
+
} from '../packages/integrations/src/hook-envelope.mjs';
|
|
18
|
+
import {
|
|
19
|
+
isBootstrapPrompt,
|
|
20
|
+
redactSecrets,
|
|
21
|
+
} from '../packages/integrations/src/prompt-content.mjs';
|
|
22
|
+
import { transcriptsMatch } from '../packages/integrations/src/session-identity.mjs';
|
|
23
|
+
export {
|
|
24
|
+
salvageTruncatedJson,
|
|
25
|
+
extractHookPrompt,
|
|
26
|
+
isBootstrapPrompt,
|
|
27
|
+
redactSecrets,
|
|
28
|
+
transcriptsMatch,
|
|
29
|
+
};
|
|
10
30
|
|
|
11
31
|
// Deprecated export kept for consumers that imported it before 0.39.0. Automatic
|
|
12
32
|
// hooks never use this fallback: an unbound project fails closed.
|
|
@@ -27,56 +47,12 @@ export const VAULT_COMPLEMENT_RULES = [
|
|
|
27
47
|
'Atualize `SHARED_MEMORY.md` somente quando a síntese mudar estado ativo que outro agente precise saber.',
|
|
28
48
|
];
|
|
29
49
|
|
|
30
|
-
// Codex on Windows serializes the Stop payload with `last_assistant_message` cut mid-string
|
|
31
|
-
// and never closed when the assistant text carries non-ASCII (openai/codex#23784). That field
|
|
32
|
-
// is LAST in codex-rs's StopCommandInput, so everything wendkeep consumes — session_id,
|
|
33
|
-
// turn_id, transcript_path, cwd — sits in the intact prefix.
|
|
34
|
-
//
|
|
35
|
-
// One pass, tracking quotes/escapes/depth, remembering the offset of the last top-level comma
|
|
36
|
-
// that was NOT inside a string. Re-closing there yields the well-formed prefix. Deliberately
|
|
37
|
-
// NOT a decreasing brute-force parse: this runs on every turn and the payload can be tens of
|
|
38
|
-
// KB. The truncated field is dropped, never reconstructed — half an assistant message is
|
|
39
|
-
// invented data, and it is the one field we do not need.
|
|
40
|
-
export function salvageTruncatedJson(raw) {
|
|
41
|
-
const text = String(raw || '');
|
|
42
|
-
if (text[0] !== '{') return null;
|
|
43
|
-
let inString = false;
|
|
44
|
-
let escaped = false;
|
|
45
|
-
let depth = 0;
|
|
46
|
-
let lastBoundary = -1;
|
|
47
|
-
for (let i = 0; i < text.length; i += 1) {
|
|
48
|
-
const ch = text[i];
|
|
49
|
-
if (escaped) { escaped = false; continue; }
|
|
50
|
-
if (ch === '\\') { if (inString) escaped = true; continue; }
|
|
51
|
-
if (ch === '"') { inString = !inString; continue; }
|
|
52
|
-
if (inString) continue;
|
|
53
|
-
if (ch === '{' || ch === '[') depth += 1;
|
|
54
|
-
else if (ch === '}' || ch === ']') depth -= 1;
|
|
55
|
-
else if (ch === ',' && depth === 1) lastBoundary = i;
|
|
56
|
-
}
|
|
57
|
-
if (lastBoundary === -1) return null;
|
|
58
|
-
try {
|
|
59
|
-
const parsed = JSON.parse(`${text.slice(0, lastBoundary)}}`);
|
|
60
|
-
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
61
|
-
} catch { return null; }
|
|
62
|
-
}
|
|
63
|
-
|
|
64
50
|
export function readHookInput() {
|
|
65
|
-
|
|
66
|
-
if (!raw) return {};
|
|
67
|
-
try {
|
|
68
|
-
return JSON.parse(raw);
|
|
69
|
-
} catch (error) {
|
|
70
|
-
const salvaged = salvageTruncatedJson(raw);
|
|
71
|
-
// `_wk` prefix: the object is the harness payload merged with our own metadata, and a
|
|
72
|
-
// silent key collision here would be worse than the ugly prefix.
|
|
73
|
-
if (salvaged) return { ...salvaged, _wkSalvaged: true };
|
|
74
|
-
throw error;
|
|
75
|
-
}
|
|
51
|
+
return parseHookInput(readFileSync(0, 'utf-8'));
|
|
76
52
|
}
|
|
77
53
|
|
|
78
54
|
export function writeHookOutput(payload = {}) {
|
|
79
|
-
process.stdout.write(
|
|
55
|
+
process.stdout.write(stringifyHookOutput(payload));
|
|
80
56
|
}
|
|
81
57
|
|
|
82
58
|
// Resolve from explicit hook payload or the nearest project-local binding. A legacy
|
|
@@ -118,17 +94,11 @@ export function warnIfDefaultVault(input = {}) {
|
|
|
118
94
|
// Detecta o agente real que está executando o hook. Claude Code expõe
|
|
119
95
|
// CLAUDECODE / CLAUDE_CODE_SESSION_ID / CLAUDE_PROJECT_DIR; Codex não.
|
|
120
96
|
export function detectProvider() {
|
|
121
|
-
|
|
122
|
-
return 'claude';
|
|
123
|
-
}
|
|
124
|
-
return 'codex';
|
|
97
|
+
return detectProviderFromEnvironment(process.env);
|
|
125
98
|
}
|
|
126
99
|
|
|
127
100
|
export function providerMeta(provider = detectProvider()) {
|
|
128
|
-
|
|
129
|
-
return { id: 'claude', label: 'Claude Code', tag: 'claude', source: 'claude-hook' };
|
|
130
|
-
}
|
|
131
|
-
return { id: 'codex', label: 'Codex', tag: 'codex', source: 'codex-hook' };
|
|
101
|
+
return providerMetaFromProvider(provider);
|
|
132
102
|
}
|
|
133
103
|
|
|
134
104
|
export function ensureDir(path) {
|
|
@@ -807,44 +777,6 @@ export function keysBate(a = '', b = '') {
|
|
|
807
777
|
return a === b || a.startsWith(b) || b.startsWith(a);
|
|
808
778
|
}
|
|
809
779
|
|
|
810
|
-
export function extractHookPrompt(input = {}) {
|
|
811
|
-
const candidates = [
|
|
812
|
-
input.prompt,
|
|
813
|
-
input.user_prompt,
|
|
814
|
-
input.userPrompt,
|
|
815
|
-
input.message,
|
|
816
|
-
input.input,
|
|
817
|
-
];
|
|
818
|
-
|
|
819
|
-
for (const candidate of candidates) {
|
|
820
|
-
if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
if (Array.isArray(input.messages)) {
|
|
824
|
-
const text = input.messages
|
|
825
|
-
.map((message) => message?.content || message?.text || '')
|
|
826
|
-
.filter((item) => typeof item === 'string' && item.trim())
|
|
827
|
-
.join('\n')
|
|
828
|
-
.trim();
|
|
829
|
-
if (text) return text;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
return '';
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
export function isBootstrapPrompt(text = '') {
|
|
836
|
-
const clean = String(text || '').trim();
|
|
837
|
-
return clean.startsWith('# AGENTS.md instructions')
|
|
838
|
-
|| clean.startsWith('<environment_context>')
|
|
839
|
-
|| clean.startsWith('<permissions instructions>')
|
|
840
|
-
// Codex injects the available-plugins catalogue as the first userPrompt of turn 1.
|
|
841
|
-
// Anchored with startsWith on purpose: matching the bare substring would discard a
|
|
842
|
-
// legitimate prompt that merely asks about plugins.
|
|
843
|
-
|| clean.startsWith('<recommended_plugins>')
|
|
844
|
-
|| clean.includes('You are Codex, a coding agent')
|
|
845
|
-
|| clean.startsWith('## Memory');
|
|
846
|
-
}
|
|
847
|
-
|
|
848
780
|
export function summarizePromptForTitle(text = '', fallback = 'session') {
|
|
849
781
|
const cleaned = redactSecrets(String(text || ''))
|
|
850
782
|
.replace(/\[@[^\]]+\]\([^)]+\)/g, ' ')
|
|
@@ -901,27 +833,6 @@ export function shouldReuseActiveSession(control = {}, now = new Date()) {
|
|
|
901
833
|
return now.getTime() - startedMs <= windowMinutes * 60 * 1000;
|
|
902
834
|
}
|
|
903
835
|
|
|
904
|
-
function normalizeTranscript(p) {
|
|
905
|
-
return String(p || '').replace(/\\/g, '/').toLowerCase();
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
function transcriptBasename(p) {
|
|
909
|
-
const n = normalizeTranscript(p);
|
|
910
|
-
const i = n.lastIndexOf('/');
|
|
911
|
-
return i === -1 ? n : n.slice(i + 1);
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
// Mesmo transcript apesar de caixa/separador diferentes (o Claude Code emite o
|
|
915
|
-
// slug do projeto ora `c--`, ora `C--`) ou prefixo de path diferente (WSL vs
|
|
916
|
-
// Windows). Compara normalizado e, em último caso, pelo basename
|
|
917
|
-
// (`<session_id>.jsonl`, globalmente único). Evita rupturas de sessão no restart.
|
|
918
|
-
export function transcriptsMatch(a, b) {
|
|
919
|
-
if (!a || !b) return false;
|
|
920
|
-
if (normalizeTranscript(a) === normalizeTranscript(b)) return true;
|
|
921
|
-
const ba = transcriptBasename(a);
|
|
922
|
-
return !!ba && ba === transcriptBasename(b);
|
|
923
|
-
}
|
|
924
|
-
|
|
925
836
|
// O `transcript_path` é estável dentro de uma conversa mesmo quando o
|
|
926
837
|
// SessionStart re-dispara (compactação/resume) com `session_id` novo. Achar a
|
|
927
838
|
// sessão ativa do mesmo transcript evita criar placeholders `HH-MM-codex`.
|
|
@@ -940,17 +851,6 @@ export function findActiveSessionByTranscript(vaultBase, transcriptPath) {
|
|
|
940
851
|
return best;
|
|
941
852
|
}
|
|
942
853
|
|
|
943
|
-
export function redactSecrets(text) {
|
|
944
|
-
if (!text) return '';
|
|
945
|
-
return String(text)
|
|
946
|
-
.replace(/\b(sk-[A-Za-z0-9_-]{12,})\b/g, '[REDACTED_SECRET]')
|
|
947
|
-
.replace(/\b(whsec_[A-Za-z0-9_/-]{8,})\b/g, '[REDACTED_SECRET]')
|
|
948
|
-
.replace(/\b(gh[pousr]_[A-Za-z0-9_]{12,})\b/g, '[REDACTED_SECRET]')
|
|
949
|
-
.replace(/\b(xox[baprs]-[A-Za-z0-9-]{12,})\b/g, '[REDACTED_SECRET]')
|
|
950
|
-
.replace(/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY)[A-Z0-9_]*)\s*[:=]\s*["']?[^"'\s]+/gi, '$1=[REDACTED_SECRET]')
|
|
951
|
-
.replace(/:\/\/([^:\s/@]+):([^@\s/]+)@/g, '://[REDACTED_SECRET]@');
|
|
952
|
-
}
|
|
953
|
-
|
|
954
854
|
export function truncate(text, max = 240) {
|
|
955
855
|
const clean = redactSecrets(String(text || '').replace(/\s+/g, ' ').trim());
|
|
956
856
|
if (clean.length <= max) return clean;
|
|
@@ -1,152 +1,37 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'fs';
|
|
2
2
|
import { basename } from 'path';
|
|
3
|
-
import { detectProvider, readSessionRegistry
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
try { return JSON.parse(line); } catch { return null; }
|
|
9
|
-
}).filter(Boolean);
|
|
10
|
-
}
|
|
3
|
+
import { detectProvider, readSessionRegistry } from './obsidian-common.mjs';
|
|
4
|
+
import {
|
|
5
|
+
inspectTranscriptIdentityContent,
|
|
6
|
+
resolveSessionIdentitySnapshot,
|
|
7
|
+
} from '../packages/integrations/src/session-identity.mjs';
|
|
11
8
|
|
|
12
9
|
export function inspectTranscriptIdentity(transcriptPath) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
canonicalConversationId: codexMeta.session_id || codexMeta.id || '',
|
|
20
|
-
transcriptId: codexMeta.id || basename(transcriptPath, '.jsonl'),
|
|
21
|
-
parentConversationId: codexMeta.parent_thread_id || codexMeta.forked_from_id || '',
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
const claudeEvent = lines.find((event) => event.sessionId);
|
|
25
|
-
if (claudeEvent) {
|
|
26
|
-
return {
|
|
27
|
-
transcriptProvider: 'anthropic',
|
|
28
|
-
provider: 'claude',
|
|
29
|
-
canonicalConversationId: claudeEvent.sessionId,
|
|
30
|
-
transcriptId: basename(transcriptPath, '.jsonl'),
|
|
31
|
-
parentConversationId: '',
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
return { transcriptProvider: 'unknown', provider: 'unknown', canonicalConversationId: '', transcriptId: '', parentConversationId: '' };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function compatible(provider, transcriptProvider) {
|
|
38
|
-
return (provider === 'codex' && transcriptProvider === 'openai')
|
|
39
|
-
|| (provider === 'claude' && transcriptProvider === 'anthropic');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function canonicalUuid(value = '') {
|
|
43
|
-
const id = String(value || '').trim().toLowerCase();
|
|
44
|
-
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(id) ? id : '';
|
|
10
|
+
const content = transcriptPath && existsSync(transcriptPath)
|
|
11
|
+
? readFileSync(transcriptPath, 'utf-8')
|
|
12
|
+
: '';
|
|
13
|
+
return inspectTranscriptIdentityContent(content, {
|
|
14
|
+
fallbackTranscriptId: transcriptPath ? basename(transcriptPath, '.jsonl') : '',
|
|
15
|
+
});
|
|
45
16
|
}
|
|
46
17
|
|
|
47
18
|
export function resolveSessionIdentity(vaultBase, input = {}, provider = detectProvider()) {
|
|
48
19
|
const transcriptPath = input.transcript_path || input.transcriptPath || '';
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const codexThreadId = canonicalUuid(input.codex_thread_id || input.codexThreadId || process.env.CODEX_THREAD_ID || '');
|
|
52
|
-
|
|
53
|
-
// Codex Desktop exposes the stable canonical conversation through
|
|
54
|
-
// CODEX_THREAD_ID even when SessionStart/UserPromptSubmit omit transcript_path.
|
|
55
|
-
// This is safer than the hook session_id (which may rotate on resume). Once the
|
|
56
|
-
// rollout materializes, require both canonical sources to agree.
|
|
57
|
-
const transcriptConversationId = canonicalUuid(inspected.canonicalConversationId);
|
|
58
|
-
if (provider === 'codex' && transcriptConversationId && codexThreadId
|
|
59
|
-
&& transcriptConversationId !== codexThreadId) {
|
|
60
|
-
return {
|
|
61
|
-
state: 'deferred', provider, transcriptPath,
|
|
62
|
-
diagnostics: [`CODEX_THREAD_ID diverge do session_id do transcript (${codexThreadId} != ${transcriptConversationId})`],
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
if (provider === 'codex' && !inspected.canonicalConversationId && codexThreadId) {
|
|
66
|
-
return {
|
|
67
|
-
state: 'resolved',
|
|
68
|
-
provider,
|
|
69
|
-
canonicalConversationId: codexThreadId,
|
|
70
|
-
hookSessionId: hookId,
|
|
71
|
-
transcriptPath,
|
|
72
|
-
transcriptId: transcriptPath ? basename(transcriptPath, '.jsonl') : codexThreadId,
|
|
73
|
-
parentConversationId: '',
|
|
74
|
-
diagnostics: [],
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Claude: input.session_id já é o id canônico e estável da conversa — idêntico
|
|
79
|
-
// ao sessionId que cada linha do transcript grava. Numa sessão nova o arquivo
|
|
80
|
-
// ainda não materializou em disco quando o hook roda, então inspectTranscriptIdentity
|
|
81
|
-
// volta vazio e o gate abaixo adiaria o 1º turno inteiro (SessionStart + 1º prompt
|
|
82
|
-
// sem nota; sessão curta nunca registrada). Não adiar: usar o hookId direto.
|
|
83
|
-
// Codex NÃO entra aqui de propósito — lá o id do hook no resume é efêmero e ≠ do
|
|
84
|
-
// thread canônico, então seguimos exigindo rollout/registry (incidente 2026-07-11,
|
|
85
|
-
// contaminação cross-provider de sessão).
|
|
86
|
-
if (provider === 'claude' && hookId && !inspected.canonicalConversationId) {
|
|
87
|
-
return {
|
|
88
|
-
state: 'resolved',
|
|
89
|
-
provider,
|
|
90
|
-
canonicalConversationId: hookId,
|
|
91
|
-
hookSessionId: hookId,
|
|
92
|
-
transcriptPath,
|
|
93
|
-
transcriptId: transcriptPath ? basename(transcriptPath, '.jsonl') : hookId,
|
|
94
|
-
parentConversationId: '',
|
|
95
|
-
diagnostics: [],
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Codex on Windows can deliver a Stop payload whose transcript_path never arrives (or is
|
|
100
|
-
// lost to a truncated JSON, openai/codex#23784). The registry already knows the mapping —
|
|
101
|
-
// the lookup below just sat under this gate, unreachable in the one case it solves. The
|
|
102
|
-
// comment above says we require "rollout/registry"; the registry half was never wired.
|
|
103
|
-
// Requiring the entry's provider to match preserves the cross-provider invariant from the
|
|
104
|
-
// 2026-07-11 incident: we are not minting a canonical id, we are finding an ALREADY
|
|
105
|
-
// REGISTERED session whose key is the hook's own id. A resume with a fresh id simply
|
|
106
|
-
// misses and stays deferred.
|
|
107
|
-
if (!transcriptPath && hookId) {
|
|
108
|
-
const entry = readSessionRegistry(vaultBase).sessions?.[hookId];
|
|
109
|
-
if (entry?.transcript_path && entry.provider === provider) {
|
|
110
|
-
return {
|
|
111
|
-
state: 'resolved',
|
|
112
|
-
provider,
|
|
113
|
-
canonicalConversationId: hookId,
|
|
114
|
-
hookSessionId: hookId,
|
|
115
|
-
transcriptPath: entry.transcript_path,
|
|
116
|
-
transcriptId: entry.transcript_id || basename(entry.transcript_path, '.jsonl'),
|
|
117
|
-
parentConversationId: '',
|
|
118
|
-
diagnostics: ['transcript recuperado do SESSION_REGISTRY'],
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (!transcriptPath || !inspected.canonicalConversationId) {
|
|
124
|
-
return { state: 'deferred', provider, transcriptPath, diagnostics: ['transcript ausente ou sem identidade canônica'] };
|
|
125
|
-
}
|
|
126
|
-
if (!compatible(provider, inspected.transcriptProvider)) {
|
|
127
|
-
return { state: 'deferred', provider, transcriptPath, diagnostics: [`provider ${provider} incompatível com transcript ${inspected.transcriptProvider}`] };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const registry = readSessionRegistry(vaultBase);
|
|
131
|
-
const byTranscript = Object.entries(registry.sessions || {}).find(([, entry]) => {
|
|
132
|
-
const paths = [...(Array.isArray(entry?.transcript_paths) ? entry.transcript_paths : []), entry?.transcript_path].filter(Boolean);
|
|
133
|
-
return paths.some((path) => transcriptsMatch(path, transcriptPath));
|
|
134
|
-
});
|
|
135
|
-
const canonicalConversationId = byTranscript?.[0] || inspected.canonicalConversationId;
|
|
136
|
-
return {
|
|
137
|
-
state: 'resolved',
|
|
20
|
+
return resolveSessionIdentitySnapshot({
|
|
21
|
+
input,
|
|
138
22
|
provider,
|
|
139
|
-
|
|
140
|
-
hookSessionId: hookId,
|
|
23
|
+
codexThreadId: process.env.CODEX_THREAD_ID || '',
|
|
141
24
|
transcriptPath,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
};
|
|
25
|
+
inspected: inspectTranscriptIdentity(transcriptPath),
|
|
26
|
+
registry: readSessionRegistry(vaultBase),
|
|
27
|
+
});
|
|
146
28
|
}
|
|
147
29
|
|
|
148
30
|
export function resolveSessionEntry(vaultBase, input = {}, provider = detectProvider()) {
|
|
149
31
|
const identity = resolveSessionIdentity(vaultBase, input, provider);
|
|
150
32
|
if (identity.state !== 'resolved') return { identity, entry: null };
|
|
151
|
-
return {
|
|
33
|
+
return {
|
|
34
|
+
identity,
|
|
35
|
+
entry: readSessionRegistry(vaultBase).sessions?.[identity.canonicalConversationId] || null,
|
|
36
|
+
};
|
|
152
37
|
}
|