wendkeep 0.66.5 → 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.
@@ -14,9 +14,9 @@ deliberado e executa as validações e gates próprios daquele comando.
14
14
 
15
15
  ## Quando usar
16
16
 
17
- Use `profile` para consultar ou selecionar explicitamente um Perfil de Operação. Use `FLOW` para
18
- manutenção local, reversível e com `spec_impact:none` que caiba num microcontrato Executar
19
- Validar, sem change.
17
+ Use `profile use` para uma seleção humana persistente e `profile route` para o harness registrar a
18
+ rota temporária da implementação atual. Use `FLOW` para manutenção local, reversível e com
19
+ `spec_impact:none` que caiba num microcontrato Executar → Validar, sem change.
20
20
 
21
21
  ## Quando não usar
22
22
 
@@ -28,7 +28,8 @@ gates/policies do WendKeep; promova o trabalho para uma change.
28
28
  ## Pré-requisitos
29
29
 
30
30
  - Projeto inicializado, com `.wendkeep.json` vinculado ao Vault correto.
31
- - Para override de sessão, uma sessão inequívoca no `SESSION_REGISTRY.json`.
31
+ - Para override ou rota temporária, uma sessão inequívoca no `SESSION_REGISTRY.json`; `route`
32
+ também exige que o prompt atual já tenha fronteira causal registrada.
32
33
  - Para FLOW, repositório Git, allowlist de paths, motivo e ao menos um sensor existente em
33
34
  `wendkeep.sensors.json`.
34
35
 
@@ -37,6 +38,7 @@ gates/policies do WendKeep; promova o trabalho para uma change.
37
38
  ```bash
38
39
  npx wendkeep profile status [--project <path>] [--vault <path>] [--session <id>] [--json]
39
40
  npx wendkeep profile use <perfil> [--project <path>] [--vault <path>] [--session <id>] [--json]
41
+ npx wendkeep profile route <FLOW|GUIDE|GOVERN|ASSURE> --session <id> --reason <texto> [--project <path>] [--vault <path>] [--json]
40
42
  npx wendkeep flow start <slug> --allow <path> [--allow <path>...] --sensor <id> [--sensor <id>...] --reason <texto> [--session <id>]
41
43
  npx wendkeep flow status [<id>]
42
44
  npx wendkeep flow show <id> [--session <id>]
@@ -76,15 +78,42 @@ independentes.
76
78
  | `GOVERN` | P → R → E → V | Loop a2 atual e fallback conservador. |
77
79
  | `ASSURE` | P → R → E → V → C | Governança acrescida de confirmação e handoff. |
78
80
 
79
- - A resolução segue override explícito da sessão → `harness.profile` do projeto → `GOVERN`.
80
- Heurística, tamanho do diff, texto do prompt, variável de ambiente ou erro de leitura nunca
81
+ ### Legenda da rota
82
+
83
+ As letras são etapas do trabalho, não comandos individuais:
84
+
85
+ - `P` = **Planejar/Propor** — entender o pedido, delimitar o escopo e registrar a abordagem.
86
+ - `R` = **Revisar** — revisar proposta/design antes da execução; é a revisão formal do loop a2.
87
+ - `E` = **Executar** — editar os paths e artefatos permitidos.
88
+ - `V` = **Validar** — rodar testes, sensores e verificações e registrar evidência.
89
+ - `C` = **Confirmar/entregar** — obter confirmação explícita e fazer handoff.
90
+
91
+ Logo, `P → R → E → V` é “planejar/propor, revisar, executar e validar”. `FLOW` começa no
92
+ microcontrato de execução/validação; `OFF` não impõe rota Wend automática e entrega o processo ao
93
+ harness nativo da LLM.
94
+
95
+ - O harness da LLM classifica semanticamente o pedido e registra `profile route`; o Wend Runtime
96
+ não classifica texto, tamanho do diff, heurística ou variável de ambiente. Ele valida e aplica a
97
+ lease determinística.
98
+ - Para correção local, reversível e sem contrato/spec, escolha `FLOW`. Para mudança compacta de
99
+ comportamento que precisa de change sem revisão formal, escolha `GUIDE`. Em dúvida, risco,
100
+ segurança, contrato público, dependências, CI/release ou policy, escolha `GOVERN`. Use `ASSURE`
101
+ quando confirmação e handoff forem parte do contrato.
102
+ - `OFF` nunca pode ser uma rota adaptativa; somente uma pessoa o persiste explicitamente por
103
+ `profile use OFF`. Uma base `OFF` ainda pode receber uma elevação temporária para rota Wend.
104
+
105
+ - A resolução segue lease ativa do prompt → override persistente da sessão →
106
+ `harness.profile` do projeto → `GOVERN`. Lease inválida/expirada e erro de leitura nunca
81
107
  selecionam `OFF`.
82
- - `profile status` mostra perfil efetivo e origem; `--json` produz saída estruturada. Quando um
83
- Vault explícito preserva a escolha apesar de binding corrompido, a saída inclui `binding_error`
84
- e o diagnóstico também vai para stderr.
108
+ - `profile status` mostra perfil efetivo e origem. Com `--session`, a saída humana acrescenta
109
+ `base=<perfil>/<origem>` e `lease=<estado>`; `--json` produz os mesmos dados estruturados. Quando
110
+ um Vault explícito preserva a escolha apesar de binding corrompido, a saída inclui
111
+ `binding_error` e o diagnóstico também vai para stderr.
85
112
  - `profile use` valida nome e flags estritamente; opção singleton duplicada, incompleta ou com
86
113
  valor iniciado por `--` falha antes de I/O. Sem `--session`, altera atomicamente o binding do
87
114
  projeto; com `--session`, grava override, origem e timestamp sem trocar a identidade da sessão.
115
+ - `profile route` exige `--session` e `--reason`, aceita somente os quatro perfis adaptativos e
116
+ grava `lease_id`, motivo, turno/sequência e timestamp sem tocar no perfil persistente.
88
117
  - `.wendkeep.json` continua em `schemaVersion: 1`; o campo aditivo usa, por exemplo,
89
118
  `"harness": { "profile": "GOVERN" }`. Binding legado sem o campo também resolve `GOVERN`.
90
119
  - Binding corrompido nunca equivale a `OFF`. Quando o payload ou a integração legada identifica
@@ -126,13 +155,39 @@ independentes.
126
155
  - Exit `0` indica consulta ou transição concluída; exit `1` indica política/sensor vermelho; exit
127
156
  `2` indica perfil, sessão, flow ou argumentos inválidos, sem mutação parcial.
128
157
 
158
+ ### Escopo da escolha
159
+
160
+ Sem `--session`, `profile use` grava `harness.profile` no `.wendkeep.json` e muda o padrão do
161
+ projeto para as conversas/hooks que não tenham override de sessão. Com `--session <id>`, grava o
162
+ override somente no `SESSION_REGISTRY.json` daquela sessão e preserva o padrão do projeto. Por
163
+ isso, `profile use OFF` sem `--session` não é um teste isolado; se o `.wendkeep.json` for commitado,
164
+ essa escolha também será compartilhada com outros checkouts.
165
+
166
+ `profile route` cria uma lease apenas para a solicitação atual. Um `Stop` aceito a consome por
167
+ CAS; se o cleanup não rodar, o próximo `UserPromptSubmit` avança a sequência e a lease deixa de ser
168
+ efetiva. Stop bloqueado preserva a lease para o retry do mesmo pedido. Não há TTL de relógio que
169
+ interrompa trabalho longo. Sessão ainda sem prompt causal registrado (turno ausente, sequência
170
+ zero, mapa causal ausente ou divergente) é rejeitada antes de qualquer mutação. `status --session`
171
+ inclui o perfil-base e o estado da lease tanto na saída humana quanto em `--json`; neste, os campos
172
+ são `base_profile`, `base_source` e `task_lease.state` (`active`, `consumed`, `expired`, `invalid` ou
173
+ `absent`).
174
+
175
+ ```bash
176
+ npx wendkeep profile status # padrão do projeto
177
+ npx wendkeep profile use GUIDE # altera o padrão do projeto
178
+ npx wendkeep profile use FLOW --session <id> # altera somente uma sessão
179
+ npx wendkeep profile route FLOW --session <id> --reason "ajuste local" # pedido atual
180
+ npx wendkeep profile status --session <id> # consulta a sessão efetiva
181
+ ```
182
+
129
183
  ## Exemplos
130
184
 
131
- Consultar o padrão efetivo e aplicar override somente à sessão atual:
185
+ Consultar o padrão efetivo e rotear somente a implementação atual:
132
186
 
133
187
  ```bash
134
188
  npx wendkeep profile status
135
- npx wendkeep profile use OFF --session 019abc-session-id --json
189
+ npx wendkeep profile route FLOW --session 019abc-session-id --reason "corrige typo local" --json
190
+ npx wendkeep profile status --session 019abc-session-id --json
136
191
  ```
137
192
 
138
193
  Executar uma manutenção FLOW capturando o `flow_id` retornado por `start`:
@@ -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. Se encontrar um registry legado
42
- com o epoch fechado, abre exatamente uma activation de recuperação; repetir o mesmo prompt não
43
- abre outra.
44
- - No Codex, `session_id`, o `turn_id` nativo e a ordem observada no transcript bastam para resolver
45
- o turno. O payload do hook não precisa inventar `activation_id` nem `turn_sequence`.
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**. Sinais que chegam na janela de **250 ms**
52
- são coalescidos: somente a maior sequência recompõe/publica, sem perder o último filho.
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
 
@@ -80,7 +95,13 @@ Cada sessão canônica aponta para provider, transcript, arquivo de nota e custo
80
95
  O registry mantém um epoch de `SessionStart` por activation e o turno nativo mais recente; vários
81
96
  `Stop` podem confirmar turnos do mesmo epoch sem fechá-lo. Importações repetidas do mesmo
82
97
  `session_id` são deduplicadas; o foco humano não encerra nem altera a identidade dos hooks. Cada
83
- iteração automática permanece Markdown válido mesmo quando uma fala precisa ser truncada. Hooks
98
+ iteração automática permanece Markdown válido mesmo quando uma fala precisa ser truncada.
99
+ Metadados internos terminais completos ou truncados são removidos somente das mensagens do
100
+ assistente; uma reprodução escrita pelo usuário permanece no transcript. Na nota, tags XML-like
101
+ são codificadas como texto visível — inclusive placeholders como `<session>` — sem alterar
102
+ autolinks `<https://...>`. Reimport e `SessionStop` compartilham o mesmo normalizador idempotente;
103
+ ao finalizar uma nota antiga, somente campos gerados reconhecíveis em `Iterações` e
104
+ `Encerramento` são migrados, sem reescrever a prosa autoral. Hooks
84
105
  duplicados/stale convergem no mesmo frontier, e importações podem atualizar só a observabilidade
85
106
  sem criar um novo bloco de turno.
86
107
 
@@ -91,6 +112,10 @@ sem criar um novo bloco de turno.
91
112
  compatível foi encontrada; o attempt fica observável, mas não publica memória.
92
113
  - Stop atrasado aparece como `stale_turn`/`superseded`: o epoch mais novo e seu checkpoint são
93
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.
94
119
  - Duplicatas de forks: limite por fonte/data e revise `forked_from_id`/`source.subagent`.
95
120
  - Codex não captura: aprove os hooks e reinicie a sessão após `sync`.
96
121
  - Custo contaminado: valide `session_id → session_file → transcript_path → provider`.
@@ -74,15 +74,18 @@ npx wendkeep memory status --gate --vault .MeuApp-vault
74
74
  ## Resultado esperado
75
75
 
76
76
  `evidencia.json` contém resultados dos sensores e um selo liga a prova ao hash atual de
77
- `tarefas.md`. No deep, o pacote contém requisitos, tarefas e evidência suficientes para revisão
78
- read-only; o verdict cobre cada `[req:]` antes do archive.
77
+ `tarefas.md`. Quando um sensor fica vermelho, sua entrada recebe somente um diagnóstico local
78
+ sanitizado e limitado a 2.000 caracteres; stdout/stderr de sensores verdes não é persistido. No
79
+ deep, o pacote contém requisitos, tarefas e evidência suficientes para revisão read-only; o
80
+ verdict cobre cada `[req:]` antes do archive.
79
81
 
80
82
  ## Erros comuns e diagnóstico
81
83
 
82
84
  - `no change`: isso é exit 2 e estado ocioso válido; crie/use uma change ou não rode verify.
83
85
  - Zero/sensores ausentes: confira todas as tags na mesma linha e `sensors list`; várias tags na
84
86
  mesma tarefa são válidas e todas entram no gate.
85
- - Gate vermelho: corrija a causa e repita; não use `archive --force` por conta própria.
87
+ - Gate vermelho: consulte o campo `note` limitado da entrada em `evidencia.json`, corrija a causa
88
+ e repita; não use `archive --force` por conta própria.
86
89
  - Verdict stale/ausente: regenere `--deep` e peça novo passe independente.
87
90
  - Mutantes sobreviventes: fortaleça o teste discriminante; após três rodadas, revise manualmente.
88
91
 
@@ -13,6 +13,7 @@ import {
13
13
  profileSentinelId,
14
14
  resolveHookOperatingProfile,
15
15
  } from './operating-profile-runtime.mjs';
16
+ import { consumeSessionTaskOperatingProfile } from './operating-profile-task-store.mjs';
16
17
 
17
18
  export function nagDecision(input, vaultBase, { profile = 'GOVERN' } = {}) {
18
19
  if (input && input.stop_hook_active) return null; // anti-loop: sempre primeiro
@@ -39,6 +40,13 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
39
40
  : runtime.bindingError
40
41
  ? { decision: 'block', reason: profileRuntimeError(runtime.bindingError) }
41
42
  : nagDecision(input, runtime.vaultBase, { profile: runtime.profile });
43
+ if (!decision && runtime.taskLease?.state === 'active') {
44
+ consumeSessionTaskOperatingProfile(
45
+ runtime.vaultBase,
46
+ runtime.identity?.canonicalConversationId || input?.session_id || input?.sessionId || '',
47
+ runtime.taskLease.lease_id,
48
+ );
49
+ }
42
50
  writeHookOutput(decision || {});
43
51
  } catch {
44
52
  writeHookOutput({});
@@ -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,
@@ -3,6 +3,7 @@ import { resolve } from 'node:path';
3
3
 
4
4
  import {
5
5
  DEFAULT_OPERATING_PROFILE,
6
+ evaluateTaskOperatingProfileLease,
6
7
  normalizeOperatingProfile,
7
8
  operatingProfilePolicy,
8
9
  resolveOperatingProfile,
@@ -68,6 +69,22 @@ function sessionOverride(entry) {
68
69
  }
69
70
  }
70
71
 
72
+ function nonNegativeSequence(value, fallback = null) {
73
+ const parsed = Number(value);
74
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : fallback;
75
+ }
76
+
77
+ function taskLeaseContext(entry, input, sessionId) {
78
+ return {
79
+ sessionId,
80
+ turnId: input?.turn_id || input?.turnId || entry?.last_prompt_turn_id || '',
81
+ turnSequence: nonNegativeSequence(
82
+ input?.turn_sequence ?? input?.turnSequence,
83
+ nonNegativeSequence(entry?.last_turn_sequence),
84
+ ),
85
+ };
86
+ }
87
+
71
88
  function canonicalPath(value) {
72
89
  const path = resolve(value).replaceAll('\\', '/');
73
90
  return process.platform === 'win32' ? path.toLowerCase() : path;
@@ -105,7 +122,8 @@ function matchingProjectBinding(vaultResolution, input) {
105
122
  }
106
123
  }
107
124
 
108
- // Resolution precedence for hooks: explicit session override -> project binding -> GOVERN.
125
+ // Resolution precedence for hooks: active request lease -> explicit session override
126
+ // -> project binding -> GOVERN.
109
127
  // Binding corruption is never interpreted as OFF: an authoritative Vault keeps the Keep Core
110
128
  // alive under GOVERN and carries a visible diagnostic to each entrypoint.
111
129
  export function resolveHookOperatingProfile({
@@ -134,7 +152,20 @@ export function resolveHookOperatingProfile({
134
152
  const entry = identity.state === 'resolved'
135
153
  ? readSessionRegistry(vaultResolution.base).sessions?.[identity.canonicalConversationId] || null
136
154
  : null;
137
- const selected = sessionOverride(entry) || project;
155
+ const base = sessionOverride(entry) || project;
156
+ const taskLease = evaluateTaskOperatingProfileLease(
157
+ entry?.operating_profile_task,
158
+ taskLeaseContext(entry, input, identity.canonicalConversationId || ''),
159
+ );
160
+ const selected = taskLease.state === 'active'
161
+ ? {
162
+ profile: taskLease.profile,
163
+ source: 'task-lease',
164
+ valid: true,
165
+ configured: true,
166
+ raw: taskLease.profile,
167
+ }
168
+ : base;
138
169
  return {
139
170
  ...selected,
140
171
  policy: operatingProfilePolicy(selected.profile),
@@ -142,6 +173,9 @@ export function resolveHookOperatingProfile({
142
173
  projectRoot: vaultResolution.projectRoot,
143
174
  identity,
144
175
  entry,
176
+ baseProfile: base.profile,
177
+ baseSource: base.source,
178
+ taskLease,
145
179
  resolution: vaultResolution,
146
180
  ...(bindingError ? { bindingError } : {}),
147
181
  };
@@ -0,0 +1,77 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import {
4
+ createTaskOperatingProfileLease,
5
+ } from '../src/operating-profile.mjs';
6
+ import { mutateSessionRegistry } from './obsidian-common.mjs';
7
+
8
+ function isoTimestamp(now) {
9
+ if (typeof now === 'string') return now;
10
+ if (now instanceof Date) return now.toISOString();
11
+ return new Date().toISOString();
12
+ }
13
+
14
+ function missingSessionError(sessionId) {
15
+ const error = new Error(`sessão não encontrada: ${sessionId}`);
16
+ error.code = 'WENDKEEP_SESSION_NOT_FOUND';
17
+ return error;
18
+ }
19
+
20
+ export function setSessionTaskOperatingProfile(vaultBase, sessionId, profile, {
21
+ reason,
22
+ leaseId = randomUUID(),
23
+ now,
24
+ } = {}) {
25
+ const issuedAt = isoTimestamp(now);
26
+ return mutateSessionRegistry(vaultBase, (registry) => {
27
+ const sessions = registry.sessions || (registry.sessions = {});
28
+ if (!Object.hasOwn(sessions, sessionId)) throw missingSessionError(sessionId);
29
+ const current = sessions[sessionId];
30
+ const turnId = typeof current.last_prompt_turn_id === 'string'
31
+ ? current.last_prompt_turn_id.trim()
32
+ : '';
33
+ const hasRegisteredTurn = Boolean(
34
+ turnId
35
+ && current.turn_sequences
36
+ && Object.hasOwn(current.turn_sequences, turnId)
37
+ && current.turn_sequences[turnId] === current.last_turn_sequence
38
+ );
39
+ const lease = createTaskOperatingProfileLease({
40
+ profile,
41
+ reason,
42
+ sessionId,
43
+ turnId,
44
+ turnSequence: hasRegisteredTurn ? current.last_turn_sequence : undefined,
45
+ leaseId,
46
+ issuedAt,
47
+ });
48
+ sessions[sessionId] = {
49
+ ...current,
50
+ operating_profile_task: lease,
51
+ updated_at: issuedAt,
52
+ };
53
+ return lease;
54
+ });
55
+ }
56
+
57
+ export function consumeSessionTaskOperatingProfile(vaultBase, sessionId, leaseId, {
58
+ now,
59
+ } = {}) {
60
+ if (!sessionId || !leaseId) return false;
61
+ const consumedAt = isoTimestamp(now);
62
+ return mutateSessionRegistry(vaultBase, (registry) => {
63
+ const current = registry.sessions?.[sessionId];
64
+ const lease = current?.operating_profile_task;
65
+ if (!lease || lease.state !== 'active' || lease.lease_id !== leaseId) return false;
66
+ registry.sessions[sessionId] = {
67
+ ...current,
68
+ operating_profile_task: {
69
+ ...lease,
70
+ state: 'consumed',
71
+ consumed_at: consumedAt,
72
+ },
73
+ updated_at: consumedAt,
74
+ };
75
+ return true;
76
+ });
77
+ }
@@ -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 turns = tx.turns.filter((turn) => turn.turnId && turn.userPrompts.length);
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
 
@@ -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);