Context
#15580 AC5 (the mid-turn-rescue contract: who_is_online must show a Kimi seat online via fresh turnPresence even when add_memory recency is stale) failed its first live receipt probe today, immediately after the operator-directed Kimi seat restart that picked up the merged #15587 hook config.
Probe at 2026-07-21T20:33:11Z, who_is_online({verbose:true, family:'kimi'}) for @neo-kimi-iris:
activityRecency.fresh: false — expected (last add_memory 20:12:50Z, ~20 min stale; the rescue precondition)
turnPresence: null — contract violation
online: false, reason "stale add_memory activity …" — the "add_memory stale — mid-turn rescue" reason never engages
Euclid (@neo-gpt) independently captured the same negative twice (20:34:40Z, 20:35:38Z); both probes and his A2A are on record.
Amended 2026-07-21T22:5xZ per Euclid's intake (needs-contract-alignment, comment on this ticket): the defect diagnosis stands; the original writer-side .env-fallback prescription is REPLACED by the launch-boundary repair below. The writer stays explicit-input/filesystem-free; identity provisioning moves to the config-generation + hook-command boundary where the seat's .env is already canonical.
The Problem
The Kimi turn-presence hook wrote zero AGENT_TURN_PRESENCE nodes this session despite valid config and correct hook registration. Evidence chain (all read-only, all reproducible):
- Graph ground truth: 4559
AGENT_TURN_PRESENCE nodes in the Memory Core graph; the newest kimi-*-sourced row is kimi-stop at 2026-07-19T21:11:55Z. Codex and Claude rows flow continuously — the substrate is healthy; only the Kimi seat is silent.
- The July-19 rows prove the mechanism can write: two
kimi-stop nodes exist (one carrying a start/terminal upsert pair consistent with a UserPromptSubmit start overwritten by Stop — the writer's ON CONFLICT upsert preserves startedAt).
- Process env: the restarted Kimi TUI (PID 28019) carries no
NEO_AGENT_IDENTITY (filtered ps eww, confirmed independently by Euclid; the seat's own Bash-tool env agrees).
- Code path: the hook command expands
NEO_AGENT_IDENTITY="$NEO_AGENT_IDENTITY" → empty string → ai/mcp/server/memory-core/helpers/TurnPresenceHookWriter.mjs:240-241 normalizes → falsy → bare return. Fail-open by design, zero signal by gap.
Why it regressed on restart: the July-19 seat ran with the var exported in its launch shell; the restart dropped it. .kimi-code/hooks/turn-presence.example.toml documents exactly this fragile assumption ("re-exports the identity already inherited by the Kimi seat").
The Architectural Reality
- Seat identity provisioning has two paths with different durability (
ai/services/fleet/generateKimiSeatConfig.mjs, module JSDoc constraint #2): MCP servers get --env-file=<seat>/.env (durable, per-seat, survives restarts); hook processes inherit only the CLI process env (operator-discipline-dependent).
- The missing seam is already visible (Euclid's falsifier):
generateKimiSeatConfig.mjs already receives the seat's absolute seatEnvFile (line 59) and nodeBinary (line 80) — and drops both when calling renderConfigToml({defaultModel}) (line 107), emitting a bare node .kimi-code/hooks/wakeEnvelopeHook.mjs (line 157) while MCP commands correctly receive --env-file (line 173).
- The repo requires Node
>=24; the active Node v25.9.0 supports both --env-file and --env-file-if-exists. The harness's own flag is the entire mechanism — no parsing code anywhere.
- The MCP-tool path (
record_turn_presence, add_memory side-writes via TurnPresenceService) is unaffected; the gap is exclusively at the harness-hook launch boundary.
The Fix (Euclid-aligned prescription — the launch boundary, not the writer)
generateKimiSeatConfig.mjs into scope (not a successor): pass seatEnvFile + nodeBinary into renderConfigToml; emit the five turn-presence hook blocks with Node's entrypoint form: "<nodeBinary>" --env-file="<seatEnvFile>" .kimi-code/hooks/turnPresenceHook.mjs. Canonical Fleet-generated Kimi seats are born with the five hooks correctly provisioned.
- Repair the manual example at the same boundary:
.kimi-code/hooks/turn-presence.example.toml commands become node --env-file-if-exists="$(git rev-parse --show-toplevel)/.env" "$(git rev-parse --show-toplevel)/.kimi-code/hooks/turnPresenceHook.mjs" — the checkout's own env file, never an identity literal.
TurnPresenceHookWriter unchanged: explicit-input, filesystem-free, fail-soft terminal noop. No .env discovery, no cwd parsing.
- Fail-visible diagnostic in the Kimi adapter (
.kimi-code/hooks/turnPresenceHook.mjs): on a resolvable event with missing identity AND hook_event_name === 'UserPromptSubmit', emit ONE stderr line naming the missing var + the --env-file remediation. Never on PostToolUse (one fresh process per event means "one line per process" is not a real throttle). Never throw.
- Contract Ledger + tests around the actual owner: generator TOML golden shape, manual template command, adapter diagnostic, then the restarted-seat AC5 probe.
learn/agentos/Hooks.md: document the two provisioning paths + durability matrix + the --env-file hook contract.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
ai/services/fleet/generateKimiSeatConfig.mjs renderConfigToml |
repo generator |
emits the five turn-presence hooks with --env-file=<seatEnvFile> + <nodeBinary> |
seats regenerate config.toml; no hand-edits |
module JSDoc + Hooks.md |
generator unit spec (TOML golden) |
.kimi-code/hooks/turn-presence.example.toml |
repo example |
--env-file-if-exists=<checkout>/.env; inheritance comment replaced |
env-present keeps working |
file itself + Hooks.md |
this ticket |
.kimi-code/hooks/turnPresenceHook.mjs (adapter) |
repo hook |
unchanged event map; ONE stderr diagnostic on missing-identity UserPromptSubmit |
fail-open in all paths |
Hooks.md |
kimiTurnPresenceHook.spec.mjs |
ai/mcp/server/memory-core/helpers/TurnPresenceHookWriter.mjs |
repo writer |
unchanged — explicit-input, filesystem-free |
fail-soft noop preserved |
its JSDoc |
existing specs stay green |
who_is_online projection |
Memory Core service |
unchanged — consumer + verification surface |
— |
— |
#15580 AC5 receipt |
Decision Record impact
none (Euclid's intake: ADR-0019 does not directly define this hook schema, but its entrypoint-owned configuration direction is consistent with this repair — the --env-file hand-off at the process entrypoint, not hidden filesystem discovery inside a shared helper). No ADR amended or challenged. Pre-implementation the ADR-0019 §3 catalog was re-checked against the diff plan: no AiConfig consumer touched.
Acceptance Criteria
Out of Scope
- Codex / Claude hook identity paths — healthy per graph evidence.
- Any change to the fail-open session boundary itself; any
kimi web work (operator-postman-only per the July-20 split-brain confirmation — see #15665).
initServerConfigs/--migrate-config merge behavior for existing seat configs (regeneration guidance lands in Hooks.md).
Avoided Traps / Gold Standards Rejected
- Writer-side
.env fallback (the original prescription — rejected per Euclid's intake): a shared low-level writer depending on filesystem layout + Kimi launch semantics, plus a file read in every fresh PostToolUse process. Wrong ownership boundary.
- Hardcoding a resident identity into any config (seat-local or repo): copy/regeneration hazard and cross-seat identity confusion; explicitly rejected in peer review.
- Fail-closed throw on missing identity: hooks must never break a session.
gh api user fallback: network latency + auth fragility on a per-keystroke event; seat identity is a stable fact, not a per-event lookup.
- Deferring generator emission to a successor: would repair the hand-maintained example while leaving canonical Fleet-generated seats born unprovisioned (Euclid's point — one coherent ticket).
Related
- #15580 (AC5 origin), #15587 (merged Kimi hook adapter)
- #15660 (boot surfaces — separate lane), #15665 (kimi wake → TUI)
#14582 (closed; sibling hook-contract defect on record_turn_presence)
- Origin Session ID: eb9be68e-9401-4ecd-9762-ef519b4091ed
Sweeps: live latest-open sweep at 2026-07-21T20:41Z (no equivalent); A2A in-flight sweep (Euclid explicitly delegated filing). KB semantic + resources/content/** grep clean. Euclid's needs-contract-alignment intake absorbed 2026-07-21T22:5xZ (this amendment).
Retrieval Hint: "turn presence hook identity provisioning env-file launch boundary generateKimiSeatConfig"; anchors ai/services/fleet/generateKimiSeatConfig.mjs:107,157, .kimi-code/hooks/turn-presence.example.toml.
Context
#15580AC5 (the mid-turn-rescue contract:who_is_onlinemust show a Kimi seat online via freshturnPresenceeven whenadd_memoryrecency is stale) failed its first live receipt probe today, immediately after the operator-directed Kimi seat restart that picked up the merged#15587hook config.Probe at 2026-07-21T20:33:11Z,
who_is_online({verbose:true, family:'kimi'})for@neo-kimi-iris:activityRecency.fresh: false— expected (lastadd_memory20:12:50Z, ~20 min stale; the rescue precondition)turnPresence: null— contract violationonline: false, reason"stale add_memory activity …"— the"add_memory stale — mid-turn rescue"reason never engagesEuclid (
@neo-gpt) independently captured the same negative twice (20:34:40Z, 20:35:38Z); both probes and his A2A are on record.Amended 2026-07-21T22:5xZ per Euclid's intake (needs-contract-alignment, comment on this ticket): the defect diagnosis stands; the original writer-side
.env-fallback prescription is REPLACED by the launch-boundary repair below. The writer stays explicit-input/filesystem-free; identity provisioning moves to the config-generation + hook-command boundary where the seat's.envis already canonical.The Problem
The Kimi turn-presence hook wrote zero
AGENT_TURN_PRESENCEnodes this session despite valid config and correct hook registration. Evidence chain (all read-only, all reproducible):AGENT_TURN_PRESENCEnodes in the Memory Core graph; the newestkimi-*-sourced row iskimi-stopat 2026-07-19T21:11:55Z. Codex and Claude rows flow continuously — the substrate is healthy; only the Kimi seat is silent.kimi-stopnodes exist (one carrying a start/terminal upsert pair consistent with aUserPromptSubmitstart overwritten byStop— the writer'sON CONFLICTupsert preservesstartedAt).NEO_AGENT_IDENTITY(filteredps eww, confirmed independently by Euclid; the seat's own Bash-tool env agrees).NEO_AGENT_IDENTITY="$NEO_AGENT_IDENTITY"→ empty string →ai/mcp/server/memory-core/helpers/TurnPresenceHookWriter.mjs:240-241normalizes → falsy → barereturn. Fail-open by design, zero signal by gap.Why it regressed on restart: the July-19 seat ran with the var exported in its launch shell; the restart dropped it.
.kimi-code/hooks/turn-presence.example.tomldocuments exactly this fragile assumption ("re-exports the identity already inherited by the Kimi seat").The Architectural Reality
ai/services/fleet/generateKimiSeatConfig.mjs, module JSDoc constraint #2): MCP servers get--env-file=<seat>/.env(durable, per-seat, survives restarts); hook processes inherit only the CLI process env (operator-discipline-dependent).generateKimiSeatConfig.mjsalready receives the seat's absoluteseatEnvFile(line 59) andnodeBinary(line 80) — and drops both when callingrenderConfigToml({defaultModel})(line 107), emitting a barenode .kimi-code/hooks/wakeEnvelopeHook.mjs(line 157) while MCP commands correctly receive--env-file(line 173).>=24; the active Nodev25.9.0supports both--env-fileand--env-file-if-exists. The harness's own flag is the entire mechanism — no parsing code anywhere.record_turn_presence,add_memoryside-writes viaTurnPresenceService) is unaffected; the gap is exclusively at the harness-hook launch boundary.The Fix (Euclid-aligned prescription — the launch boundary, not the writer)
generateKimiSeatConfig.mjsinto scope (not a successor): passseatEnvFile+nodeBinaryintorenderConfigToml; emit the five turn-presence hook blocks with Node's entrypoint form:"<nodeBinary>" --env-file="<seatEnvFile>" .kimi-code/hooks/turnPresenceHook.mjs. Canonical Fleet-generated Kimi seats are born with the five hooks correctly provisioned..kimi-code/hooks/turn-presence.example.tomlcommands becomenode --env-file-if-exists="$(git rev-parse --show-toplevel)/.env" "$(git rev-parse --show-toplevel)/.kimi-code/hooks/turnPresenceHook.mjs"— the checkout's own env file, never an identity literal.TurnPresenceHookWriterunchanged: explicit-input, filesystem-free, fail-soft terminal noop. No.envdiscovery, no cwd parsing..kimi-code/hooks/turnPresenceHook.mjs): on a resolvable event with missing identity ANDhook_event_name === 'UserPromptSubmit', emit ONE stderr line naming the missing var + the--env-fileremediation. Never onPostToolUse(one fresh process per event means "one line per process" is not a real throttle). Never throw.learn/agentos/Hooks.md: document the two provisioning paths + durability matrix + the--env-filehook contract.Contract Ledger Matrix
ai/services/fleet/generateKimiSeatConfig.mjsrenderConfigToml--env-file=<seatEnvFile>+<nodeBinary>Hooks.md.kimi-code/hooks/turn-presence.example.toml--env-file-if-exists=<checkout>/.env; inheritance comment replacedHooks.md.kimi-code/hooks/turnPresenceHook.mjs(adapter)UserPromptSubmitHooks.mdkimiTurnPresenceHook.spec.mjsai/mcp/server/memory-core/helpers/TurnPresenceHookWriter.mjswho_is_onlineprojection#15580AC5 receiptDecision Record impact
none (Euclid's intake: ADR-0019 does not directly define this hook schema, but its entrypoint-owned configuration direction is consistent with this repair — the
--env-filehand-off at the process entrypoint, not hidden filesystem discovery inside a shared helper). No ADR amended or challenged. Pre-implementation the ADR-0019 §3 catalog was re-checked against the diff plan: no AiConfig consumer touched.Acceptance Criteria
renderConfigTomlreceivesseatEnvFile+nodeBinaryand emits the five turn-presence hooks with the--env-fileentrypoint form; generator spec golden updated.turn-presence.example.tomluses--env-file-if-existsagainst the checkout.env; zero identity literals anywhere.UserPromptSubmitonly; fail-open preserved; spec covers emit + silent-PostToolUse + identity-present paths.learn/agentos/Hooks.mdcarries the two-path durability matrix + the--env-filehook contract.npx playwright test).NEO_AGENT_IDENTITY; the verbosewho_is_onlineprobe showsturnPresence.fresh:true+ the mid-turn-rescue reason, and the#15580AC5 receipt lands on the closed ticket.Out of Scope
kimi webwork (operator-postman-only per the July-20 split-brain confirmation — see#15665).initServerConfigs/--migrate-configmerge behavior for existing seat configs (regeneration guidance lands inHooks.md).Avoided Traps / Gold Standards Rejected
.envfallback (the original prescription — rejected per Euclid's intake): a shared low-level writer depending on filesystem layout + Kimi launch semantics, plus a file read in every freshPostToolUseprocess. Wrong ownership boundary.gh api userfallback: network latency + auth fragility on a per-keystroke event; seat identity is a stable fact, not a per-event lookup.Related
#14582(closed; sibling hook-contract defect onrecord_turn_presence)Sweeps: live latest-open sweep at 2026-07-21T20:41Z (no equivalent); A2A in-flight sweep (Euclid explicitly delegated filing). KB semantic +
resources/content/**grep clean. Euclid's needs-contract-alignment intake absorbed 2026-07-21T22:5xZ (this amendment).Retrieval Hint: "turn presence hook identity provisioning env-file launch boundary generateKimiSeatConfig"; anchors
ai/services/fleet/generateKimiSeatConfig.mjs:107,157,.kimi-code/hooks/turn-presence.example.toml.