wendkeep 0.67.0 → 0.67.1
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 +24 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/docs/en/commands/sessions-and-import.md +27 -8
- package/docs/pt-BR/commands/sessions-and-import.md +26 -7
- package/hooks/obsidian-common.mjs +7 -0
- package/hooks/session-backfill.mjs +26 -1
- package/hooks/session-ensure.mjs +22 -0
- package/hooks/session-stop.mjs +8 -1
- package/hooks/subagent-stop.mjs +35 -3
- package/package.json +2 -2
- package/packages/cli/src/index.mjs +3 -3
- package/packages/integrations/src/transcripts.mjs +10 -0
- package/src/sync-defs.mjs +14 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ 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.67.1] — 2026-08-02
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Turnos de subagents não bloqueiam mais a captura da sessão-mãe.** `UserPromptSubmit` de rollout
|
|
12
|
+
filho registra o path para observabilidade sem avançar a sequência principal, e `SessionStop`
|
|
13
|
+
prefere o mapeamento causal `turn_sequences[turn_id]` antes da ordem local do transcript. Isso
|
|
14
|
+
preserva os marcadores `wk-turn`, o encerramento e a memória mesmo com filhos intercalados.
|
|
15
|
+
- **`SubagentStop` consome o transcript correto do payload Codex.** O hook resolve o filho por
|
|
16
|
+
`agent_transcript_path`/`agentTranscriptPath`, valida identidade, sessão e `parent_thread_id`
|
|
17
|
+
contra um root comprovado e mantém `transcript_path` como contexto da sessão-mãe, permitindo
|
|
18
|
+
preencher `Subagents e workflows` no último filho sem aceitar sinais de outra cadeia.
|
|
19
|
+
- **O backfill não grava o turno Codex ainda em execução.** `hook session-backfill` exige
|
|
20
|
+
`task_complete`, separa `missingTurns` de `incompleteTurns`, permanece dry-run por padrão e aplica
|
|
21
|
+
somente turnos concluídos de forma idempotente. A CLI agora encaminha `--session`, `--vault`,
|
|
22
|
+
`--write` e demais argumentos ao hook executado. README e guias PT-BR/EN documentam o reparo.
|
|
23
|
+
- **O bloco gerenciado do `AGENTS.md` exige roteamento adaptativo antes de editar.** O harness
|
|
24
|
+
inspeciona o perfil-base, classifica a solicitação, registra uma lease temporária entre `FLOW`,
|
|
25
|
+
`GUIDE`, `GOVERN` e `ASSURE` e confirma o perfil efetivo; `OFF` nunca é escolhido pela LLM e a
|
|
26
|
+
escolha explícita do usuário prevalece.
|
|
27
|
+
- **O scanner de privacidade distingue configuração pública de identificadores privados.** Comandos
|
|
28
|
+
longos legítimos em `wendkeep.sensors.json` deixam de gerar falso positivo, sem permitir que UUID,
|
|
29
|
+
hash, token compacto, campo desconhecido ou path absoluto sejam ocultados pela exceção.
|
|
30
|
+
|
|
7
31
|
## [0.67.0] — 2026-08-01
|
|
8
32
|
|
|
9
33
|
### Added
|
package/README.en.md
CHANGED
|
@@ -385,7 +385,7 @@ acknowledgement pending on 0.66.4 or later, first run the targeted dry run
|
|
|
385
385
|
registry/checkpoint. See syntax, preconditions, and fail-closed behavior in
|
|
386
386
|
[memory and curation](docs/en/commands/memory.md).
|
|
387
387
|
|
|
388
|
-
Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source. Every hook that rewrites a session note takes a per-file lock and writes through a temp file + rename, so the `SubagentStop` fan-out (one hook run per subagent) can never leave a note half-written; a note whose frontmatter reads back damaged is left untouched rather than patched.
|
|
388
|
+
Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source. On Codex, subagent prompts register their rollout for observability without advancing the main agent's sequence; `SubagentStop` reads the child from `agent_transcript_path` and persists its signal only when `parent_thread_id` matches a validated session root. The main Stop uses the registry's causal `turn_id` mapping before falling back to local transcript order. To recover missing markers while a conversation is open, `hook session-backfill` is a dry-run by default and never writes a Codex turn without `task_complete`. Every hook that rewrites a session note takes a per-file lock and writes through a temp file + rename, so the `SubagentStop` fan-out (one hook run per subagent) can never leave a note half-written; a note whose frontmatter reads back damaged is left untouched rather than patched.
|
|
389
389
|
|
|
390
390
|
## Retroactive memory (`import`) — install today, remember yesterday
|
|
391
391
|
|
package/README.md
CHANGED
|
@@ -385,7 +385,7 @@ acknowledgement pending on 0.66.4 or later, first run the targeted dry run
|
|
|
385
385
|
registry/checkpoint. See syntax, preconditions, and fail-closed behavior in
|
|
386
386
|
[memory and curation](docs/en/commands/memory.md).
|
|
387
387
|
|
|
388
|
-
Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source. Every hook that rewrites a session note takes a per-file lock and writes through a temp file + rename, so the `SubagentStop` fan-out (one hook run per subagent) can never leave a note half-written; a note whose frontmatter reads back damaged is left untouched rather than patched.
|
|
388
|
+
Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source. On Codex, subagent prompts register their rollout for observability without advancing the main agent's sequence; `SubagentStop` reads the child from `agent_transcript_path` and persists its signal only when `parent_thread_id` matches a validated session root. The main Stop uses the registry's causal `turn_id` mapping before falling back to local transcript order. To recover missing markers while a conversation is open, `hook session-backfill` is a dry-run by default and never writes a Codex turn without `task_complete`. Every hook that rewrites a session note takes a per-file lock and writes through a temp file + rename, so the `SubagentStop` fan-out (one hook run per subagent) can never leave a note half-written; a note whose frontmatter reads back damaged is left untouched rather than patched.
|
|
389
389
|
|
|
390
390
|
## Retroactive memory (`import`) — install today, remember yesterday
|
|
391
391
|
|
|
@@ -30,6 +30,7 @@ npx wendkeep hook <name>
|
|
|
30
30
|
npx wendkeep session list
|
|
31
31
|
npx wendkeep session show <id>
|
|
32
32
|
npx wendkeep session use <id>
|
|
33
|
+
npx wendkeep hook session-backfill --session <id> [--write]
|
|
33
34
|
npx wendkeep import [options]
|
|
34
35
|
```
|
|
35
36
|
|
|
@@ -38,19 +39,27 @@ npx wendkeep import [options]
|
|
|
38
39
|
- `wendkeep hook <name>` reads the agent payload from stdin; valid names are listed by `--help`.
|
|
39
40
|
- `SessionStart` opens an activation: an epoch that remains active across multiple `Stop` events;
|
|
40
41
|
only a new `SessionStart` supersedes the previous epoch.
|
|
41
|
-
- `UserPromptSubmit` advances the active activation's native turn.
|
|
42
|
-
with
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
- Main-agent `UserPromptSubmit` advances the active activation's native turn. A Codex rollout
|
|
43
|
+
prompt with `source.subagent` only registers its path for observability: it does not advance the
|
|
44
|
+
sequence, enter `turn_sequences`, or replace the main transcript. If a main prompt finds a
|
|
45
|
+
legacy registry with a closed epoch, it opens exactly one recovery activation; replaying the
|
|
46
|
+
same prompt does not open another one.
|
|
47
|
+
- On Codex, `session_id` and the native `turn_id` are enough to resolve the turn. Stop proves the
|
|
48
|
+
ID in the transcript and prefers `SESSION_REGISTRY.turn_sequences[turn_id]`; local transcript
|
|
49
|
+
order is the legacy-registry fallback. Interleaved subagent turns therefore cannot make the
|
|
50
|
+
parent's Stop artificially `stale_turn`. Hook payloads need no invented `activation_id` or
|
|
51
|
+
`turn_sequence` fields.
|
|
46
52
|
- `Stop` accepts only a transcript-proven turn from the compatible active activation. Duplicates
|
|
47
53
|
are no-ops; stale/superseded Stops neither publish memory nor overwrite a newer epoch's
|
|
48
54
|
checkpoint.
|
|
49
55
|
- `Stop` receives an absolute **45 s** deadline from hook entry. Reads check the clock between
|
|
50
56
|
rollouts and on every chunk; reaching the limit returns `degraded` before the host timeout.
|
|
51
|
-
- `SubagentStop` receives an absolute **15 s** deadline
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
- `SubagentStop` receives an absolute **15 s** deadline and resolves the child rollout from Codex's
|
|
58
|
+
official `agent_transcript_path` field (`agentTranscriptPath` is also accepted);
|
|
59
|
+
`transcript_path` continues to identify the parent session. Before any write, `source.subagent`,
|
|
60
|
+
ID, canonical session, and `parent_thread_id` are validated; the parent must match a proven
|
|
61
|
+
session root. Signals arriving within the **250 ms** window are coalesced: only the highest
|
|
62
|
+
sequence recomposes/publishes, without losing the last child.
|
|
54
63
|
- Observability is tri-state: `complete` publishes the full snapshot; `none` means zero proven by
|
|
55
64
|
a causal Stop or stable offline scan; `degraded` preserves the previous snapshot and allowlisted
|
|
56
65
|
diagnostics. An isolated `SubagentStop` never publishes `none`.
|
|
@@ -64,6 +73,10 @@ npx wendkeep import [options]
|
|
|
64
73
|
- `import` reconciles observability even when no `wk-turn` is missing: a legacy schema, stale
|
|
65
74
|
frontier, or unproven manifest triggers recomposition without duplicating iterations. A fresh
|
|
66
75
|
checkpoint remains byte-identical; `degraded` is reported and does not change the note.
|
|
76
|
+
- `hook session-backfill` recovers missing `wk-turn` markers for the selected session. Without
|
|
77
|
+
`--write`, it only reports. On Codex, `missingTurns` contains only turns with `task_complete`;
|
|
78
|
+
open turns appear under `incompleteTurns` and are never written. `--write` applies only completed
|
|
79
|
+
candidates, and a second run is idempotent.
|
|
67
80
|
- Exit `0` means consistent processing; non-zero reports invalid source/config/write instead of
|
|
68
81
|
presenting silent partial success.
|
|
69
82
|
|
|
@@ -72,6 +85,8 @@ npx wendkeep import [options]
|
|
|
72
85
|
```bash
|
|
73
86
|
npx wendkeep session list
|
|
74
87
|
npx wendkeep session show 019abc-session-id
|
|
88
|
+
npx wendkeep hook session-backfill --session 019abc-session-id
|
|
89
|
+
npx wendkeep hook session-backfill --session 019abc-session-id --write
|
|
75
90
|
npx wendkeep import --source codex --since 2026-07-01 --dry-run --json
|
|
76
91
|
```
|
|
77
92
|
|
|
@@ -98,6 +113,10 @@ without creating a new turn block.
|
|
|
98
113
|
was found; the attempt remains observable but does not publish memory.
|
|
99
114
|
- A late Stop reports `stale_turn`/`superseded`: the newer epoch and checkpoint are preserved; do
|
|
100
115
|
not force the old payload to apply.
|
|
116
|
+
- `session-backfill` lists `incompleteTurns`: wait for that turn's `task_complete`/Stop and retry;
|
|
117
|
+
do not edit the note or force the partial turn. Once that turn receives `task_complete`, the next
|
|
118
|
+
run may treat it as eligible. Claude transcripts preserve their historical contract and do not
|
|
119
|
+
require this Codex event.
|
|
101
120
|
- Fork duplicates: bound source/date and inspect `forked_from_id`/`source.subagent`.
|
|
102
121
|
- Codex does not capture: approve hooks and start a new session after `sync`.
|
|
103
122
|
- Contaminated cost: validate `session_id → session_file → transcript_path → provider`.
|
|
@@ -30,6 +30,7 @@ npx wendkeep hook <nome>
|
|
|
30
30
|
npx wendkeep session list
|
|
31
31
|
npx wendkeep session show <id>
|
|
32
32
|
npx wendkeep session use <id>
|
|
33
|
+
npx wendkeep hook session-backfill --session <id> [--write]
|
|
33
34
|
npx wendkeep import [opções]
|
|
34
35
|
```
|
|
35
36
|
|
|
@@ -38,18 +39,26 @@ npx wendkeep import [opções]
|
|
|
38
39
|
- `wendkeep hook <name>` lê o payload do agente em stdin; nomes válidos aparecem em `--help`.
|
|
39
40
|
- `SessionStart` abre uma activation, isto é, um epoch que continua ativo por vários `Stop`; só um
|
|
40
41
|
novo `SessionStart` torna o epoch anterior superseded.
|
|
41
|
-
- `UserPromptSubmit` avança o turno nativo da activation ativa.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
- `UserPromptSubmit` do agente principal avança o turno nativo da activation ativa. Um prompt de
|
|
43
|
+
rollout Codex com `source.subagent` apenas registra o path para observabilidade: não avança a
|
|
44
|
+
sequência, não entra em `turn_sequences` e não substitui o transcript principal. Se o prompt
|
|
45
|
+
principal encontra um registry legado com o epoch fechado, abre exatamente uma activation de
|
|
46
|
+
recuperação; repetir o mesmo prompt não abre outra.
|
|
47
|
+
- No Codex, `session_id` e o `turn_id` nativo bastam para resolver o turno. O Stop prova o ID no
|
|
48
|
+
transcript e prefere `SESSION_REGISTRY.turn_sequences[turn_id]`; a ordem local do transcript é
|
|
49
|
+
fallback para registry legado. Assim, turnos intercalados de subagents não tornam o Stop da mãe
|
|
50
|
+
artificialmente `stale_turn`. O payload não precisa inventar `activation_id` ou `turn_sequence`.
|
|
46
51
|
- `Stop` aceita somente o turno comprovado pelo transcript e pela activation ativa compatível.
|
|
47
52
|
Duplicatas são no-op; Stops stale/superseded não publicam memória nem sobrescrevem o checkpoint
|
|
48
53
|
de um epoch mais novo.
|
|
49
54
|
- `Stop` recebe deadline absoluto de **45 s** desde a entrada do hook. A leitura verifica o relógio
|
|
50
55
|
entre rollouts e a cada chunk; ao atingir o limite, devolve `degraded` antes do timeout do host.
|
|
51
|
-
- `SubagentStop` recebe deadline absoluto de **15 s
|
|
52
|
-
|
|
56
|
+
- `SubagentStop` recebe deadline absoluto de **15 s** e resolve o rollout filho pelo campo oficial
|
|
57
|
+
Codex `agent_transcript_path` (também aceita `agentTranscriptPath`); `transcript_path` continua
|
|
58
|
+
identificando a sessão-mãe. Antes de qualquer escrita, `source.subagent`, ID, sessão canônica e
|
|
59
|
+
`parent_thread_id` são validados; o pai deve corresponder a um root comprovado da sessão. Sinais
|
|
60
|
+
que chegam na janela de **250 ms** são coalescidos: somente a maior sequência
|
|
61
|
+
recompõe/publica, sem perder o último filho.
|
|
53
62
|
- A observabilidade usa tri-state: `complete` publica o snapshot integral; `none` representa zero
|
|
54
63
|
comprovado por Stop causal ou scan offline estável; `degraded` preserva o snapshot anterior e
|
|
55
64
|
diagnostics allowlisted. `SubagentStop` isolado nunca publica `none`.
|
|
@@ -63,6 +72,10 @@ npx wendkeep import [opções]
|
|
|
63
72
|
- `import` reconcilia a observabilidade mesmo quando nenhum `wk-turn` está ausente: schema legado,
|
|
64
73
|
frontier stale ou manifest não comprovado disparam recomposição sem duplicar iterações. Um
|
|
65
74
|
checkpoint fresco permanece byte-idêntico; `degraded` é reportado e não altera a nota.
|
|
75
|
+
- `hook session-backfill` recupera `wk-turn` ausente da sessão selecionada. Sem `--write`, apenas
|
|
76
|
+
relata. Em Codex, `missingTurns` contém somente turnos com `task_complete`; turnos ainda abertos
|
|
77
|
+
aparecem em `incompleteTurns` e nunca são gravados. `--write` aplica apenas os candidatos
|
|
78
|
+
concluídos e uma segunda execução é idempotente.
|
|
66
79
|
- Exit `0` indica processamento consistente; exit não zero indica configuração, fonte ou escrita
|
|
67
80
|
inválida sem transformar isso em sucesso parcial silencioso.
|
|
68
81
|
|
|
@@ -71,6 +84,8 @@ npx wendkeep import [opções]
|
|
|
71
84
|
```bash
|
|
72
85
|
npx wendkeep session list
|
|
73
86
|
npx wendkeep session show 019abc-session-id
|
|
87
|
+
npx wendkeep hook session-backfill --session 019abc-session-id
|
|
88
|
+
npx wendkeep hook session-backfill --session 019abc-session-id --write
|
|
74
89
|
npx wendkeep import --source codex --since 2026-07-01 --dry-run --json
|
|
75
90
|
```
|
|
76
91
|
|
|
@@ -97,6 +112,10 @@ sem criar um novo bloco de turno.
|
|
|
97
112
|
compatível foi encontrada; o attempt fica observável, mas não publica memória.
|
|
98
113
|
- Stop atrasado aparece como `stale_turn`/`superseded`: o epoch mais novo e seu checkpoint são
|
|
99
114
|
preservados; não force a reaplicação do payload antigo.
|
|
115
|
+
- `session-backfill` lista `incompleteTurns`: aguarde o `task_complete`/Stop desse turno e repita;
|
|
116
|
+
não edite a nota nem force o turno parcial. Quando o mesmo turno recebe `task_complete`, a
|
|
117
|
+
próxima execução passa a considerá-lo elegível. Transcripts Claude preservam o contrato
|
|
118
|
+
histórico e não exigem esse evento Codex.
|
|
100
119
|
- Duplicatas de forks: limite por fonte/data e revise `forked_from_id`/`source.subagent`.
|
|
101
120
|
- Codex não captura: aprove os hooks e reinicie a sessão após `sync`.
|
|
102
121
|
- Custo contaminado: valide `session_id → session_file → transcript_path → provider`.
|
|
@@ -321,6 +321,13 @@ function nonNegativeSequence(value, fallback = null) {
|
|
|
321
321
|
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
export function resolveRegisteredTurnSequence(entry = {}, turnId = '', fallback = 0) {
|
|
325
|
+
const registered = turnId
|
|
326
|
+
? nonNegativeSequence(entry?.turn_sequences?.[String(turnId)])
|
|
327
|
+
: null;
|
|
328
|
+
return registered === null ? nonNegativeSequence(fallback, 0) : registered;
|
|
329
|
+
}
|
|
330
|
+
|
|
324
331
|
function cloneRegistry(registry = {}) {
|
|
325
332
|
const sessions = Object.fromEntries(Object.entries(registry.sessions || {}).map(([id, entry]) => [
|
|
326
333
|
id,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
insertIteration,
|
|
8
8
|
parseTranscript,
|
|
9
9
|
} from './session-stop.mjs';
|
|
10
|
+
import { completedCodexTurnIdsContent } from '../packages/integrations/src/transcripts.mjs';
|
|
10
11
|
import {
|
|
11
12
|
getVaultBase,
|
|
12
13
|
readControl,
|
|
@@ -63,6 +64,7 @@ export function backfillSessions({ vaultBase, write = false, limit = 0, session
|
|
|
63
64
|
scanned: 0,
|
|
64
65
|
candidates: 0,
|
|
65
66
|
inserted: 0,
|
|
67
|
+
incomplete: 0,
|
|
66
68
|
skipped: 0,
|
|
67
69
|
missing: [],
|
|
68
70
|
sessions: [],
|
|
@@ -85,13 +87,35 @@ export function backfillSessions({ vaultBase, write = false, limit = 0, session
|
|
|
85
87
|
continue;
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
const transcriptContent = readFileSync(entry.transcript_path, 'utf-8');
|
|
88
91
|
const tx = parseTranscript(entry.transcript_path);
|
|
89
|
-
const
|
|
92
|
+
const allTurns = tx.turns.filter((turn) => turn.turnId && turn.userPrompts.length);
|
|
93
|
+
const completedCodexTurns = tx.provider === 'codex'
|
|
94
|
+
? completedCodexTurnIdsContent(transcriptContent)
|
|
95
|
+
: null;
|
|
96
|
+
const turns = completedCodexTurns
|
|
97
|
+
? allTurns.filter((turn) => completedCodexTurns.has(turn.turnId))
|
|
98
|
+
: allTurns;
|
|
90
99
|
const content = readFileSync(sessionPath, 'utf-8');
|
|
91
100
|
const missingTurns = turns.filter((turn) => !hasTurnMarker(content, turn.turnId));
|
|
101
|
+
const incompleteTurns = completedCodexTurns
|
|
102
|
+
? allTurns.filter((turn) => (
|
|
103
|
+
!completedCodexTurns.has(turn.turnId) && !hasTurnMarker(content, turn.turnId)
|
|
104
|
+
))
|
|
105
|
+
: [];
|
|
106
|
+
report.incomplete += incompleteTurns.length;
|
|
92
107
|
|
|
93
108
|
if (!missingTurns.length) {
|
|
94
109
|
report.skipped += 1;
|
|
110
|
+
if (incompleteTurns.length) {
|
|
111
|
+
report.sessions.push({
|
|
112
|
+
session: entry.session_file,
|
|
113
|
+
transcript: entry.transcript_path,
|
|
114
|
+
missingTurns: [],
|
|
115
|
+
incompleteTurns: incompleteTurns.map((turn) => turn.turnId),
|
|
116
|
+
inserted: 0,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
95
119
|
continue;
|
|
96
120
|
}
|
|
97
121
|
|
|
@@ -100,6 +124,7 @@ export function backfillSessions({ vaultBase, write = false, limit = 0, session
|
|
|
100
124
|
session: entry.session_file,
|
|
101
125
|
transcript: entry.transcript_path,
|
|
102
126
|
missingTurns: missingTurns.map((turn) => turn.turnId),
|
|
127
|
+
incompleteTurns: incompleteTurns.map((turn) => turn.turnId),
|
|
103
128
|
inserted: 0,
|
|
104
129
|
};
|
|
105
130
|
|
package/hooks/session-ensure.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
yamlQuote,
|
|
33
33
|
} from './obsidian-common.mjs';
|
|
34
34
|
import { resolveSessionIdentity } from './session-identity.mjs';
|
|
35
|
+
import { readCodexRolloutMeta } from './codex-rollout-meta.mjs';
|
|
35
36
|
import { mutateSessionNote } from './session-note-io.mjs';
|
|
36
37
|
|
|
37
38
|
function sessionIdFromInput(input) {
|
|
@@ -54,6 +55,12 @@ function causalTurnPatch(input, now) {
|
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
function isCodexSubagentTranscript(identity) {
|
|
59
|
+
if (identity?.provider !== 'codex' || !identity.transcriptPath) return false;
|
|
60
|
+
const inspected = readCodexRolloutMeta(identity.transcriptPath);
|
|
61
|
+
return Boolean(inspected?.ok && inspected.meta?.source?.subagent);
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
function buildSessionContent({ relPath, now, summary = 'session', sessionId = '', reason = 'Sessão criada automaticamente pelo hook UserPromptSubmit.' }) {
|
|
58
65
|
const date = formatDate(now);
|
|
59
66
|
const startedAt = formatLocalIso(now);
|
|
@@ -341,6 +348,21 @@ function main() {
|
|
|
341
348
|
}
|
|
342
349
|
const sessionId = identity.canonicalConversationId;
|
|
343
350
|
|
|
351
|
+
// UserPromptSubmit also fires inside Codex subagents. The child belongs to the parent's
|
|
352
|
+
// observability graph, but it is not a new main turn: registering it through causalTurnPatch
|
|
353
|
+
// permanently inflated the parent's sequence and made every later main Stop stale.
|
|
354
|
+
if (isCodexSubagentTranscript(identity)) {
|
|
355
|
+
const registered = readSessionRegistry(vaultBase).sessions?.[sessionId];
|
|
356
|
+
if (registered) {
|
|
357
|
+
upsertSessionRegistry(vaultBase, sessionId, {
|
|
358
|
+
transcript_paths: [identity.transcriptPath],
|
|
359
|
+
provider: identity.provider,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
writeHookOutput({});
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
344
366
|
// Fast path: skip all writes if control file touched < 5 min ago and session matches
|
|
345
367
|
try {
|
|
346
368
|
const ctrlPath = controlPath(vaultBase);
|
package/hooks/session-stop.mjs
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
hasTurnMarker,
|
|
62
62
|
normalizeTurnMarkers,
|
|
63
63
|
mutateSessionRegistry,
|
|
64
|
+
resolveRegisteredTurnSequence,
|
|
64
65
|
resolveStopActivation,
|
|
65
66
|
applyStopActivation,
|
|
66
67
|
} from './obsidian-common.mjs';
|
|
@@ -1180,9 +1181,13 @@ export async function main({
|
|
|
1180
1181
|
const turnId = turnIdentity.id;
|
|
1181
1182
|
const now = finalizing ? new Date() : null;
|
|
1182
1183
|
const endedAt = finalizing ? formatLocalIso(now) : '';
|
|
1183
|
-
const stopTurnSequence = turnIdentity.order;
|
|
1184
1184
|
const causalStop = finalizing
|
|
1185
1185
|
? mutateSessionRegistry(vaultBase, (registry) => {
|
|
1186
|
+
const stopTurnSequence = resolveRegisteredTurnSequence(
|
|
1187
|
+
registry.sessions?.[sessionId],
|
|
1188
|
+
turnId,
|
|
1189
|
+
turnIdentity.order,
|
|
1190
|
+
);
|
|
1186
1191
|
const activationId = resolveStopActivation(registry, {
|
|
1187
1192
|
session_id: sessionId,
|
|
1188
1193
|
activation_id: input.activation_id || input.activationId || '',
|
|
@@ -1214,9 +1219,11 @@ export async function main({
|
|
|
1214
1219
|
activation,
|
|
1215
1220
|
stopDisposition: cas.stopDisposition,
|
|
1216
1221
|
canPromoteMemory: cas.canPromoteMemory,
|
|
1222
|
+
turnSequence: stopTurnSequence,
|
|
1217
1223
|
};
|
|
1218
1224
|
})
|
|
1219
1225
|
: null;
|
|
1226
|
+
const stopTurnSequence = causalStop?.turnSequence ?? turnIdentity.order;
|
|
1220
1227
|
let memoryHandoff = null;
|
|
1221
1228
|
let memoryAttempt = null;
|
|
1222
1229
|
if (finalizing) {
|
package/hooks/subagent-stop.mjs
CHANGED
|
@@ -82,6 +82,32 @@ function claudeRoots(entry) {
|
|
|
82
82
|
return { state: 'complete', rootPaths: [...paths], descendantPaths: [], diagnostics: [] };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
export function subagentIdentityInput(input = {}) {
|
|
86
|
+
const agentTranscriptPath = input.agent_transcript_path || input.agentTranscriptPath || '';
|
|
87
|
+
if (!agentTranscriptPath) return input;
|
|
88
|
+
return {
|
|
89
|
+
...input,
|
|
90
|
+
transcript_path: agentTranscriptPath,
|
|
91
|
+
transcriptPath: agentTranscriptPath,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validatedCodexRootIds(entry, canonicalConversationId, { resolveRoots, readMeta }) {
|
|
96
|
+
const roots = resolveRoots(entry, { readMeta });
|
|
97
|
+
if (roots?.state !== 'complete' || !roots.rootPaths?.length) return null;
|
|
98
|
+
|
|
99
|
+
const ids = new Set();
|
|
100
|
+
for (const rootPath of roots.rootPaths) {
|
|
101
|
+
const result = readMeta(rootPath);
|
|
102
|
+
const meta = result?.meta;
|
|
103
|
+
const rootId = String(meta?.id || '');
|
|
104
|
+
if (!result?.ok || !rootId || meta?.source?.subagent) return null;
|
|
105
|
+
if (meta.session_id && meta.session_id !== canonicalConversationId) return null;
|
|
106
|
+
ids.add(rootId);
|
|
107
|
+
}
|
|
108
|
+
return ids;
|
|
109
|
+
}
|
|
110
|
+
|
|
85
111
|
export async function refreshSubagents(vaultBase, input, {
|
|
86
112
|
now = Date.now,
|
|
87
113
|
hookStartedAt = now(),
|
|
@@ -100,7 +126,8 @@ export async function refreshSubagents(vaultBase, input, {
|
|
|
100
126
|
} = {}) {
|
|
101
127
|
const deadlineAt = hookStartedAt + deadlineMs;
|
|
102
128
|
const provider = providerMeta(input.provider).id;
|
|
103
|
-
const
|
|
129
|
+
const identityInput = subagentIdentityInput(input);
|
|
130
|
+
const { identity, entry } = resolveEntry(vaultBase, identityInput, provider);
|
|
104
131
|
if (identity.state !== 'resolved') return false;
|
|
105
132
|
const childTranscriptPath = identity.transcriptPath;
|
|
106
133
|
const sessionRel = entry?.session_file || '';
|
|
@@ -118,6 +145,11 @@ export async function refreshSubagents(vaultBase, input, {
|
|
|
118
145
|
|| childMeta.meta.source?.subagent?.thread_spawn?.parent_thread_id
|
|
119
146
|
|| '',
|
|
120
147
|
);
|
|
148
|
+
const rootIds = validatedCodexRootIds(entry, identity.canonicalConversationId, {
|
|
149
|
+
resolveRoots,
|
|
150
|
+
readMeta,
|
|
151
|
+
});
|
|
152
|
+
if (!childParentThreadId || !rootIds?.has(childParentThreadId)) return false;
|
|
121
153
|
}
|
|
122
154
|
|
|
123
155
|
const observed = causalSnapshot(entry);
|
|
@@ -168,7 +200,7 @@ export async function refreshSubagents(vaultBase, input, {
|
|
|
168
200
|
|
|
169
201
|
try {
|
|
170
202
|
if (now() >= deadlineAt) return true;
|
|
171
|
-
const fresh = resolveEntry(vaultBase,
|
|
203
|
+
const fresh = resolveEntry(vaultBase, identityInput, provider);
|
|
172
204
|
if (fresh.identity?.state !== 'resolved'
|
|
173
205
|
|| fresh.identity.canonicalConversationId !== identity.canonicalConversationId
|
|
174
206
|
|| !fresh.entry?.session_file
|
|
@@ -197,7 +229,7 @@ export async function refreshSubagents(vaultBase, input, {
|
|
|
197
229
|
const currentEntry = guardContext?.entry;
|
|
198
230
|
const currentResolved = currentEntry
|
|
199
231
|
? { identity: { state: 'resolved', canonicalConversationId: identity.canonicalConversationId }, entry: currentEntry }
|
|
200
|
-
: resolveEntry(vaultBase,
|
|
232
|
+
: resolveEntry(vaultBase, identityInput, provider);
|
|
201
233
|
if (currentResolved.identity?.state !== 'resolved'
|
|
202
234
|
|| currentResolved.identity.canonicalConversationId !== identity.canonicalConversationId
|
|
203
235
|
|| !currentResolved.entry) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.67.
|
|
3
|
+
"version": "0.67.1",
|
|
4
4
|
"description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -70,6 +70,6 @@
|
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"acorn": "^8.18.0",
|
|
73
|
-
"wendkeep": "^0.
|
|
73
|
+
"wendkeep": "^0.67.0"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -117,7 +117,7 @@ Usage:
|
|
|
117
117
|
wendkeep --help Show this help.
|
|
118
118
|
`;
|
|
119
119
|
|
|
120
|
-
function runHook(name) {
|
|
120
|
+
function runHook(name, args = []) {
|
|
121
121
|
if (!name) {
|
|
122
122
|
process.stderr.write('wendkeep hook: missing hook name\n');
|
|
123
123
|
process.exit(2);
|
|
@@ -133,7 +133,7 @@ function runHook(name) {
|
|
|
133
133
|
}
|
|
134
134
|
// Spawn exactly as the agent would run `node <hook>.mjs`: stdio inherited so the
|
|
135
135
|
// hook's stdin (agent JSON) and stdout (hookSpecificOutput) pass through untouched.
|
|
136
|
-
const r = spawnSync(process.execPath, [file], { stdio: 'inherit' });
|
|
136
|
+
const r = spawnSync(process.execPath, [file, ...args], { stdio: 'inherit' });
|
|
137
137
|
process.exit(r.status ?? 0);
|
|
138
138
|
}
|
|
139
139
|
|
|
@@ -207,7 +207,7 @@ async function main(argv) {
|
|
|
207
207
|
break;
|
|
208
208
|
}
|
|
209
209
|
case 'hook':
|
|
210
|
-
runHook(rest[0]);
|
|
210
|
+
runHook(rest[0], rest.slice(1));
|
|
211
211
|
break;
|
|
212
212
|
case 'doctor': {
|
|
213
213
|
const { runDoctor } = await import('../../../src/doctor.mjs');
|
|
@@ -170,6 +170,16 @@ function jsonLines(content) {
|
|
|
170
170
|
}).filter(Boolean);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
export function completedCodexTurnIdsContent(content = '') {
|
|
174
|
+
const completed = new Set();
|
|
175
|
+
for (const event of jsonLines(content)) {
|
|
176
|
+
if (event.type !== 'event_msg' || event.payload?.type !== 'task_complete') continue;
|
|
177
|
+
const turnId = String(event.payload?.turn_id || event.turn_id || '').trim();
|
|
178
|
+
if (turnId) completed.add(turnId);
|
|
179
|
+
}
|
|
180
|
+
return completed;
|
|
181
|
+
}
|
|
182
|
+
|
|
173
183
|
export function parseCodexTranscriptContent(content, options = {}) {
|
|
174
184
|
const result = createResult('codex');
|
|
175
185
|
const eventUserPrompts = [];
|
package/src/sync-defs.mjs
CHANGED
|
@@ -64,10 +64,20 @@ This project uses the [wendkeep](https://github.com/rogersialves/wendkeep) harne
|
|
|
64
64
|
in every profile: Vault, session, identity, memory, lessons, and persistence integrations.
|
|
65
65
|
The persistent profile is selected explicitly; missing or invalid configuration uses **GOVERN as the default**.
|
|
66
66
|
|
|
67
|
-
Before
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
Before every implementation, the native LLM harness **MUST run the routing gate**:
|
|
68
|
+
1. Inspect \`wendkeep profile status\`.
|
|
69
|
+
2. Classify the request and choose FLOW, GUIDE, GOVERN, or ASSURE from its scope and risk.
|
|
70
|
+
3. Register the choice with
|
|
71
|
+
\`wendkeep profile route <FLOW|GUIDE|GOVERN|ASSURE> --session <id> --reason <text>\`.
|
|
72
|
+
4. Re-check \`wendkeep profile status\` and follow the effective profile before editing.
|
|
73
|
+
|
|
74
|
+
The persistent profile is only the base restored after the task lease; it is not an immutable
|
|
75
|
+
decision for the current task. A base OFF does not block a temporary FLOW/GUIDE/GOVERN/ASSURE
|
|
76
|
+
lease, while a base GOVERN does not prevent choosing FLOW or GUIDE for bounded work. An explicit
|
|
77
|
+
user choice for the request takes precedence. If no causal session is available or routing fails,
|
|
78
|
+
do not invent lease state: follow the effective profile and use GOVERN only as the conservative
|
|
79
|
+
fallback when configuration is missing or invalid. The lease expires when the request ends.
|
|
80
|
+
**OFF is never selected adaptively**; only a human can persist \`profile use OFF\`.
|
|
71
81
|
|
|
72
82
|
Route work by the effective profile:
|
|
73
83
|
- **OFF** — Wend Runtime is disabled and governance belongs to the native LLM harness; Keep Core stays active.
|