Skip to content

Kherep 0.4.0

Latest

Choose a tag to compare

@cfaysal cfaysal released this 18 Oct 12:52
· 363 commits to main since this release

Added

  • Codex fork refusal metadata (issue #477). A JSON-RPC error response to
    initialize, thread/read, thread/turns/list or thread/fork during a
    fork attempt is now recorded as one content-free daemon log line:
    kherep-node: Codex fork helper refused <method> (code <code|none>, <shape>, sha256 <12 hex>, mentions <list|none>). The code appears only when it is an
    integer; the shape is missing, empty, text or overlong; the hash is
    the first 12 hex digits of the SHA-256 of the message; the mentions say
    which of the owner id, the newest turn id, the working directory, the Codex
    home and the word kherep the message contains. No text of the message
    enters the attempt, the audit, the log or the relay, no file holds it, and no
    persisted field is added: the attempt's reason code, wake.jsonl, doctor
    and the relay are unchanged. A throwing logger changes neither the reason,
    the close nor the service admission lease.

  • Intake visibility (refs #441). kherep-node doctor adds the intakeLatency
    check: for messages stored in the last two hours, observed,
    maxLatencySeconds (control-plane accept to local store), overThreshold
    (over 300 seconds; any one fails the check) and lastReceivedAt, with no
    ids, text or sender names. It sees only messages that reached the inbox, so
    it is no proof that the control-plane queue for the node drains. The daemon
    logs a lane item over 5 seconds with its kind (lane chain item command:session.start took 61.0 s), a message stored more than 60 seconds
    after the control plane accepted it with its id and lane wait, and a
    synchronous child process over 2 seconds by name (process-start,
    process-relations, owner-identity, owner-observe); never bodies,
    session names or pids.

  • Codex idle evidence (issue #408, first increment). The node's record of each
    Codex session (codex-sessions/<id>.json) now keeps lastEvent and
    lastEventAt, the last hook event (SessionStart, UserPromptSubmit,
    PostToolUse or Stop) and its time, and lastStopAt, the time of the last
    Stop; an input without a known event keeps them. The original owner's
    PostToolUse refreshes only the event of an existing record, and a
    subagent's call writes nothing. The pure idleEvidence() reads a record as
    idle only for a Stop at least 90 seconds old of a session seen within 12
    hours; a record of an older hook is never idle. Nothing acts on it yet:
    msg sessions prints last event <event> at <time> for this node's Codex
    sessions, and kherep-node doctor adds the codexSessions check with counts
    per last event and no session ids. Message delivery is unchanged.

  • Codex fork reply policy, attempt store and admission (issue #408, second
    increment; nothing is wired and no process is started). The opt-in
    wake.codexFork policy section (enabled, sessions by full thread id
    only, platforms ["darwin"] only, idleAfterSeconds 30 to 3600, default
    90, maxAnswerBytes 256 to 8192, default 4096) is off when absent; any
    malformed shape turns off only the fork reply and leaves the rest of wake
    as written. A new attempt store (codex-fork-attempts/<owner>/<message>.json)
    claims each message once with an exclusive create, moves states forward only,
    fails every read closed and turns launching or running attempts uncertain
    at recovery. A new admission decides the guards of the design in order with
    one reason code and spends the owner's turn budget only when every other
    guard passed; it refuses unless the Codex delivery hook is installed on
    PostToolUse, reads a hook payload's permission_mode before the recorded
    one and treats lastSeen as approximate. Idle evidence now fails closed on
    Stop continuations: the Codex Stop hook marks the session record with
    lastStopEnded after recording the Stop, on every exit that returns no
    blocking continuation, every hook event clears the mark, and
    idleEvidence() requires it, so a blocking Stop or a failed mark is never
    idle. The attempt store skips dotfiles such as .DS_Store, and
    codexFork.sessions ids are matched without case and kept in lowercase. kherep-node doctor prints a
    written wake.codexFork section with a count of opted-in sessions and fails a
    rejected one. New audit actions fork, fork-declined, fork-failed,
    fork-uncertain, fork-superseded, fork-not-idle and
    fork-disabled-platform exist for the later increments; nothing writes them yet.

  • Codex fork reply producer (issue #408, third increment, darwin only, behind
    wake.codexFork; nothing changes while the section is absent, except that
    relays made while it was present are still withdrawn when the owner reads and
    cleaned up after 24 hours; nothing is sent, forked or audited). For a
    wake-authorized owner listed in codexFork.sessions with idle evidence and
    an accepted exact-addressed message, the daemon starts one short-lived
    codex app-server of the codex on PATH per message, one at a time on the
    node, in its own process group and with KHEREP_CODEX_FORK=1. It reads the
    owner without loading it (thread/read, the newest turn through
    thread/turns/list, which must be completed, and a goal check through
    thread/goal/get that refuses an active, paused or otherwise unfinished
    goal, and refuses goals-disabled when goals are off in the identity's Codex
    config), forks it ephemerally and read-only at that turn with tools, hooks,
    goals, web search, request-user-input and every discovered MCP server off,
    and asks one structured answer only after thread/goal/get on the fork
    answers -32600 with exactly ephemeral thread does not support goals: <fork id> (measured on 0.160.1, 0.162.0-alpha.17.2 and 0.163.0); any other
    answer, goal: null included, fails the attempt with reason fork-goal and a
    fixed detail code. No codex version or hash is pinned; the feature list is
    cached per resolved path and hash. deferGoalContinuation is not sent: both
    measured binaries reject it with an ephemeral fork, and it copies the goal
    into a persisted fork. The feature keys are filtered through
    codex features list (stages stable, experimental, under development,
    deprecated and removed, as listed by the measured binaries; a removed key
    is not used) and the attempt is refused when that listing fails or shows any
    other line, or the MCP listing fails. Server requests are refused with -32601 and fail the
    attempt, as do tool items, goal notifications for the fork and turns the
    helper did not start, whenever they arrive, also after turn/completed or
    while the helper closes; a timed-out turn or any violation, also one detected
    while the helper closes, ends with SIGKILL of the helper's process group,
    and that kill confirms the close only when the OS reports it sent or the
    group absent; a failed or throwing group kill ends the attempt
    uncertain/close-unconfirmed with the service admission retained.
    The validated answer is stored in the attempt file
    (codex-fork-attempts/<owner>/<message>.json, mode 0600) with a fixed
    reason code for every other outcome; nothing is relayed, and the owner's
    inbox record, progress, session record and thread stay unchanged. The fork
    spends the owner's autonomous-turn budget once per attempt and writes one
    wake.jsonl line; on another platform the only effect is a
    fork-disabled-platform line. The daemon now marks launching or running
    attempts uncertain at start, the Codex delivery hook does nothing inside a
    fork helper, and kherep-node doctor adds the codexForkAttempts check
    (counts per state, no ids). The JSON-RPC client of the native Queue cleanup
    moved to codex-app-server-client.mts; it now decodes UTF-8 split across
    output chunks exactly (a split multibyte character came back as replacement
    characters before) and refuses every server request with -32601.

  • Codex fork reply relay (issue #408, fourth increment, darwin only, behind
    wake.codexFork). A completed fork answer is relayed as one labelled proxy
    answer to the original's sender: a new optional proxyOf on message.send
    and message.deliver names the original, never together with inReplyTo,
    so the original is never marked replied; the proxy is one reply hop deeper.
    The daemon's sweep writes one outbox record per completed attempt and the
    owner's local sidecar inbox/proxied/<message>.json; there is no stored
    release, and the exchange sends the record only when the relay guards pass
    at that send (attempt at most 24 hours old, the fresh policy with opt-in,
    enrollment, kill switch, fingerprint, platform and answer bound, the Worker
    capability messaging.proxy.v1 from a directory frame of the current
    authenticated connection, the original still accepted, the owner's recorded
    session idle with its task and permission guards) and the sidecar matches
    the attempt. A relay spends no turn and starts no helper. The owner reading
    or answering first withdraws the relay before any guard is consulted,
    exactly when nothing can have left the node and as unconfirmed otherwise; a
    held record without any Worker status is removed after 24 hours and the
    outcome is then recorded as unresolved, never as not delivered. The
    withdrawal and the cleanup run in every poll for every message with a
    sidecar, whatever the owner's session state, and do not depend on the
    section; a superseded relay whose sidecar is missing or names another relay
    is cleaned up as well, and a record that is not the attempt's relay is never
    removed. The owner's
    delivery block and msg inbox show the relay state (prepared, queued or
    awaiting confirmation, may have reached the control plane, queued by the
    control plane, reached the peer, could not be delivered, unresolved); only a
    proxy that reached the peer says not to answer again. The audit action
    fork-relayed and the attempt's relayedAt mean the Worker answered. The
    Worker validates proxyOf against the live original row only (proxy parent unknown for a missing row or any address mismatch, original already answered, original not waiting), refuses a proxy still queued when the
    owner replies or the owner node reports the original delivered or refused,
    and re-reads the row where a proxy delivery is pushed. A proxy already
    accepted at the peer stays (documented residual, audit
    message.proxy.late-delivery); closing it needs a reservation.

  • Stable Control Plane command (issue #436, first part). kherep-node service install, upgrade and rollback write <configRoot>/bin/kherep-node.mjs,
    a POSIX shim kherep-node and, on Windows, kherep-node.cmd and
    kherep-node.ps1. node "<configRoot>/bin/kherep-node.mjs" <args> reads
    the managed service receipt at every call and runs the receipt's Node
    executable with the active pin's modules/control-plane/node/cli.mts,
    passing the arguments, stdio and exit code through, so the command keeps
    working across upgrade and rollback without a reinstall. The files never
    name a pin and import nothing from one; they are rewritten only when their
    bytes differ, and the JSON result of the three service commands reports
    launcher as written, unchanged or failed. A missing or invalid
    receipt, a missing pin or a pin without the CLI exits 1 with a fixed message
    and no fallback. PATH is not changed, and the instructions, Codex rules and
    doctor check follow separately.

  • Intercom instructions (issue #436, second part). Both installers render the
    host's command, node "<configRoot>/bin/kherep-node.mjs", into the
    always-loaded instructions: bootstrap/install.sh into a managed
    <!-- kherep-intercom:start --> block of ~/.claude/CLAUDE.md and into
    ~/.claude/teams/kherep/ROUTING.md, codex/install.mts into the
    <!-- kherep:start --> block of AGENTS.md and into
    orchestra/ROUTING.md. The block lists msg sessions, msg send <node>/<session> -- <text>, --reply-to, --new ... --directive, msg inbox and msg status with one example, which names this node from
    node.json when it is enrolled. The sources carry the placeholders
    __KHEREP_NODE__ and __KHEREP_EXAMPLE_TARGET__, rendered by
    render-profile.mts intercom; operator text outside the Codex block keeps
    its bytes, and a config directory no shell can quote stops either installer
    before it writes. The installers do not write the launcher itself, and
    drift-check.sh and kherep-node doctor do not yet compare the rendered
    files.

  • Confirmed remote stop for Claude Code background tasks (issue #437, second
    part). task stop and msg stop for a Claude task the node started now stop
    the measured run and confirm it. The target re-measures the run, captures the
    session process and the descendants it has at that moment with their start
    times (by parent, not by process group), runs claude stop <short id>, the
    supported stop, and waits for every captured identity to end; the listing
    shows the session stopped without a pid while its process still lives, so
    the row is never the evidence. The tree is captured again from its live
    members after claude stop returned and before each signal, so a process a
    survivor started during the stop is ended and waited for too; a process whose
    ancestors all ended before a capture is not reached, the limit of the Codex
    stop (issues #231, #233). A process that outlives the grace period, in
    practice a shell command the stop does not end, is signalled by its own pid,
    SIGTERM then SIGKILL, only while it still has its captured start time; the
    session process is never signalled: it ends 1.7-1.8 s after claude stop
    returns (measured), and one that still lives after the grace period makes the
    stop stop_failed with processState running without a signal, because the
    supervisor's reaction to a signalled session is not measured.
    stopConfirmed: true only when every captured identity ended; a claude stop
    that fails, a survivor after SIGKILL or a tree past 256 processes is
    stop_failed, a session the supervisor restarted in the meantime is
    stale_run with the new run recorded for the next status. Status offers the stop (stopSupported: true) for a
    working or blocked session with a live process on macOS and Linux
    (measured on macOS). On Windows the Claude stop stays refused
    (unsupported_runtime, stopSupported: false); see issue #460 under
    Changed.

  • Sender-visible intake per node (Refs #441). The directory frame gives each
    node an additive intake record: oldestQueuedAt and queued, its queue at
    the control plane, and lastAcceptedAt, the Worker's time of the node's last
    report that took one of its messages out of queued (new column
    nodes.last_accepted_at, added on the Registry's next start). A node the
    Worker did not measure has no record and reads UNKNOWN, never an empty
    queue. The frame limit is unchanged: an oversized directory drops sessions,
    then intake, then nodes from the end, and marks itself truncated; equal node
    names list in id order. msg sessions prints an intake: line under each
    node, NOT DRAINING once its oldest queued message is older than 5 minutes,
    and msg status names the target's intake while a message is queued. The
    new queueDrain doctor check reports this node's queue from the last
    directory, ok: false, measured: false whenever it was not measured. Older
    nodes accept the new frame; deploy the Worker before the nodes.

Changed

  • wake.codexFork stays darwin only (issues #464, #477), now for measured
    reasons on a Windows node: the fork helper's process tree cannot be proven
    contained, and a forced close cannot confirm a stop once the helper has
    exited, so a hand-enabled win32 would keep the service admission lease; a
    turn timeout on a living root measured exactly that
    (uncertain/close-unconfirmed, lease kept). The fork itself completes on
    Windows (Codex CLI 0.160.1, an owner created by the Desktop app,
    0.163.0-alpha): the -32600 refusal first measured in #464 came from the
    measurement probe, which passed one MCP server override that codex rejects,
    while the production fork guards drop such names. The compared v2
    app-server schema files (thread fork, read, turns list, goal) are equal on
    Windows and macOS at Codex 0.160.1. Lifting it needs an identity-confirmed
    close on Windows and a containment capture from creation on (such as a job
    object).
  • Windows stays refused for the Claude confirmed stop (issue #460), now for a
    measured reason (Claude Code 2.1.289): the Bash tool runs a command through
    Git Bash (MSYS), whose intermediate process exits, so the command is not a
    descendant of the listed session process and is missing from the captured
    tree; after claude stop the session ended within about 2 s while the
    command and its shell chain kept running, so a confirmed stop would confirm
    too early. Lifting the refusal needs a capture independent of the parent
    chain, such as a job object or the MSYS process table. A Worker test pins
    that a confirming Claude stop result is stored like a Codex one.
  • Upgrade the Worker before the nodes for the Codex fork reply relay (issue
    #408): the directory frame carries worker.capabilities, and a node
    refuses every fork before admission (fork-relay-unsupported) while its
    current connection has not shown messaging.proxy.v1.
  • Codex messaging rules allowlist the stable launcher (issue #436).
    rules/kherep-control-plane.rules holds one prefix_rule, for node with
    <configRoot>/bin/kherep-node.mjs (forward slashes on Windows), whose
    examples are the exact intercom command. renderControlPlaneRules takes a
    list of launcherRule() commands.
  • Task prompts and delivery hook recipes name the stable launcher (issue
    #436). On a host whose managed launcher resolves, the daemon's task and
    intercom prompts and the Claude and Codex delivery hooks' reply and receive
    commands name node "<configRoot>/bin/kherep-node.mjs"; elsewhere they keep
    naming the running code's cli.mts. The rules file no longer carries the
    rule for the installing checkout's cli.mts, and cliRule() and
    controlPlaneCli() are removed; kherep-node doctor now reports a rules file
    that is not exactly the launcher rule as stale. Upgrade the service to a
    pin with this change first, then run codex/install.mts. A Codex session on
    a host without a managed service asks for approval per msg command.
  • Native Codex Queue pointer producer off by default (issue #429). The
    exact-owner producer that submits a fixed codex queue pointer for an
    authorized Codex owner now runs only with "nativeQueue": true in the node
    policy wake section; the key grants nothing by itself, and any non-boolean
    value rejects the section. Neither the built-in default nor onboarding writes
    it, and no upgrade step rewrites policy.json, so an existing node stops
    producing native Queue cards at the first daemon round after the upgrade. CP
    hints are unchanged, and pointers already submitted are still retired at
    confirming Stop and on poll rounds. kherep-node doctor shows the flag as
    checks.policy.wake.nativeQueue; it never fails the policy check.
  • Owner task status measures Claude Code background tasks (issue #437, first
    part). task status for a Claude task no longer answers process state
    unknown: the target reads the pid that claude agents --json --all lists
    for the task's own background session while its process is alive, binds a
    run version to that pid and its start time, and records both on the task
    (at launch, and again when the supervisor restarted the process). A live
    working or blocked session is running; a session without a live
    process, or a finished one whose process still runs, is idle or closed;
    a failed listing or a row that is not the task's own background session is
    identity_unknown. The stop followed in the second part (see Added).

Fixed

  • Listener state files of ended sessions are now pruned (issue #474). The
    daemon's hourly listener sweep also removes listeners/<id>.mode.json,
    .turns.json, .scope.json, .stuck.json, .woke.json, .queued.json,
    .busy-hint.json and .busy-admission.json of a session that a fresh,
    complete listing does not show, that known-sessions.json no longer holds (7
    days after its last listing), that has no lock or in-flight lock and no active
    task record, once every one of its files is older than 7 days; at most 200
    files per pass, each removed only while its bytes are unchanged, otherwise put
    back, and nothing is removed while the listing is stale or any state file is
    unreadable. .queued.json has had no writer or reader since 2026-10-11 and
    goes with the rest. kherep-node doctor reports the count as
    listeners.orphaned. A session resumed after more than 7 days without a
    listing arms its SessionStart listener from the transcript and launch check,
    as a session without a remembered mode always did. Locks and their scopes were
    already swept hourly; this changes neither that nor any guard, permission or
    wake budget.

  • Control Plane: an idle exchange round no longer re-reads the whole inbox
    (issue #487). On a Windows node with about 1000 settled messages a round read
    every record, receipt and progress file several times, about 2.6 s against
    the 2-second cadence, and one waiting message for an ended Codex task added
    one full inbox read per ended Codex task (23 of them, about 11 s). The daemon
    now keeps an in-process inbox index (node/inbox-index.mts), validated each
    round by one directory listing and one stat per record; only new or changed
    record files are read, the round's consumers share that read, and the Codex
    task resume reads the inbox once per round. Hooks and the CLI read and write
    the files as before and their writes are picked up by the next round. A
    failed directory listing still fails the round, never reads as empty; the
    index is rebuilt from the files every 10 minutes. Lane items now record their
    enqueue time: an item that waited over 5 seconds logs lane <lane> item <kind> waited <s> s, took <s> s, the existing took line gains , waited <s> s when both were slow, and a round that sent or handled more than 32
    frames logs round sent <n> status/<m> outbox frames, handled <k> inbound items. Guards, policy, receipts, progress codes, audit lines and the wire
    protocol are unchanged.

  • Control Plane: the sender of msg send, msg send --new, msg send --reply-to,
    msg inbox, msg sessions and task new is the runtime that runs the command
    (issue #471). A Codex process started from a Claude Code tool inherits
    CLAUDE_CODE_SESSION_ID; the CLI used to prefer that variable and spoke for the
    Claude session. It now decides the runtime first: CODEX_THREAD_ID, which Codex
    sets for its shell commands, names a Codex thread; CLAUDE_CODE_SESSION_ID names a
    Claude session only when no Codex thread variable is present or the Claude
    Code process (CLAUDE_PID) sits between the command and a recognised codex
    program in the process ancestry; KHEREP_SESSION_ID
    keeps naming the node's own Codex runs and is checked against the task's thread.
    When both runtimes are in the environment and the process ancestry cannot order
    them, the command refuses with a fixed reason and writes nothing instead of
    falling back. --from behaves as before. Measured on macOS with Codex 0.160.1;
    Windows unmeasured.

  • Confluence semantic search without ~/.local/bin on PATH (fixes #478).
    The search behind search, related and stitch spawned twg by bare
    name, so a macOS background job, whose PATH lacks ~/.local/bin, failed
    every search with spawn twg ENOENT. confluence-semantic.mts now resolves
    the executable itself, without a shell: KHEREP_TWG_BIN, then PATH, then
    ~/.local/bin/twg, /opt/homebrew/bin/twg and /usr/local/bin/twg; on
    Windows twg.exe on PATH, then the %LOCALAPPDATA% and %ProgramFiles%
    install locations. The order mirrors modules/twg/runtime/resolve-binary.mts.
    A set but unusable KHEREP_TWG_BIN and a CLI found nowhere both end as a
    search that did not run (UNKNOWN), as before.

  • stitch idempotence (fixes #479). A second stitch --space <key> --id <id>
    on the same orphan could write the same link again on every run: whether a
    neighbour was already linked was decided by comparing the regenerated body
    with the stored one byte for byte, so a Related pages block that came back
    in a different serialization never compared equal. The check now compares the
    page ids the block links with the ids the run would write, and a neighbour
    that already links all of them is unchanged. The sibling flag was not the
    cause: it only annotates the link trace line. The block format is
    unchanged; a renamed title of an already linked page is no longer refreshed
    on its own, only when the block is rewritten for another reason.

  • Managed service rollback (fixes #472). A rollback now starts the restored
    service whenever the current or the backed-up service was running, so a
    backup taken from a stopped service no longer leaves a running node unloaded
    after ok: true. Upgrade and rollback report success only after the native
    service reads back with the expected bytes and, when it should run, as running
    with the healthy daemon still recorded. The stop proof waits up to 75 seconds
    instead of 30, beyond the launchd ExitTimeOut. A macOS stop refused before
    launchctl bootout runs leaves the live service untouched and reports
    restored; any other stop failure is treated as possibly effective and is
    never reported restored without proof. An unknown recovery releases the
    drain marker only when no definition write was attempted and the daemon told
    to stop is gone. Failures add a fixed stage and, for an unknown recovery,
    a fixed blocker; the error message is still never printed. service status
    reports drain (none, active, stale or unknown) and fails on a stale
    or unverifiable drain. It names the recovery service upgrade --source <clean checkout at <commit>> for a stale drain and for a stopped service with a dead
    recorded daemon, which upgrade now accepts instead of refusing the stale
    record, and asks to inspect the drain owner file when its owner cannot be
    verified. Status prints no local path or pid.

  • Message intake no longer waits for runtime work (refs #441; the
    sender-visible signal for a node that does not drain is a follow-up). A
    message.deliver frame of an authenticated connection is stored on an
    intake lane of its own instead of behind the daemon's frame lane, which
    awaits CLI stops and starts, task control and listings of up to 60 seconds
    each. Policy acceptance, the store, the refusal and the no-answer-on-store-
    failure rule are unchanged; a delivery before its connection's auth.ok was
    handled still waits behind it. A command ack is now allocated, and the
    command counted as processed in the ack field of node frames, only after
    its handler: a status sent meanwhile can neither make the Worker drop the
    ack nor ack the command early, which would drop its resend after a daemon
    crash. The wire seq and ack are now stamped at the daemon's single send
    step instead of when a frame is built, so a frame of one lane sent after a
    higher-numbered frame of the other is no longer dropped by the Worker (a
    lost command result); the MCP inbox response keeps 32 bytes of margin for
    the stamp, and a Worker error frame naming no message is logged. A
    synchronous child process still blocks the event loop: one
    watchCodexTasks pass can run several in a row, so a delivery can still
    wait for their sum, each bounded by 10 seconds.

  • Node policy applied without a restart (issue #463). The daemon handed the
    Codex task wake, closed-session delivery, task requests
    (sessions.delegate.request) and the session.* commands the policy it
    loaded at start; they now use the exchange round's policy, a fresh read per
    session check, and a fresh read per command, so every section takes effect
    within one exchange round, one session check or at the next command.
    Tightened sessions fields apply to the next command.

  • Retained admission recovery for direct native Codex Queue reconciliation
    (Refs #442). After an unconfirmed shutdown, the same helper's later exit and
    close can release its lease once the operation and every held helper have
    finished. Unknown, rejected and permanent holds stay blocked. Late shutdown
    evidence does not certify Queue cleanup. All disk leases keep the same PID,
    process start time and owner fields, without helper identity. This release
    requires the creating daemon to retain the admission and proof from that exact
    child in memory. Lost evidence cannot be reconstructed from disk; existing
    positive stale-owner recovery stays unchanged.

  • Claude task status no longer answers closed while the recorded process
    lives (issue #454). When the claude agents --json --all row lists no pid,
    as it does right after claude stop while the session process still runs
    (measured), or when the row is missing, the target now reads the start time
    of the pid recorded on the task: equal, the process lives and the status
    answers idle for an ended session (running for a working or blocked
    row; a missing row answers from the task state, idle for an ended task and
    otherwise unknown) with its run version; null or changed, the process ended and the answer
    stays closed or idle as before; unreadable, it is identity_unknown. A
    stop that ended stop_failed with the session process alive is followed by
    idle, then closed once the process ended, and a stop meanwhile is
    stale_run. The confirmed stop is unchanged: every captured identity has
    ended before its post-stop check, which still detects a restart only by a
    newly listed pid.

  • Windows stale service-owner recovery (issue #442): valid process creation time
    can prove PID reuse when the SID is unreadable, using the existing two-second
    tolerance. Active or uncertain owners still block recovery. Admission waits
    identify the first blocker by opaque lease fingerprint, PID and process start.

  • Codex fork reply gate and refusal audit (issue #444). The daemon's regular
    exchange round handed the Codex queue the policy loaded at start, so a
    wake.codexFork opt-in or opt-out, or a wake.sessions change, written
    after the daemon started took effect only after a restart; the round now
    passes the policy it read for that round, as the intake-window path already
    did, and nothing changes while the section is absent. Every refusal of the
    fork producer before the claim is now one wake.jsonl line per owner,
    message and code: the admission code with the prefix fork- (fork-cwd-refused,
    fork-permission-mode, fork-budget-spacing, ...) and the producer's own
    fork-producer, fork-mcp-unknown, fork-features-unknown and
    fork-relay-unsupported; fork-not-idle and fork-disabled-platform are
    unchanged. A line carries ids and the code only. A line whose append
    failed is not marked as noted, so the next poll that meets the same
    refusal writes it again once the audit file is writable. Documentation
    states that a fork owner's recorded working directory must lie inside the
    workspace roots and that a codex exec session reports bypassPermissions
    and is therefore refused as a fork owner (measured 2026-10-16 with
    codex-cli 0.160.1). The exchange's proxy send gate was still built once at
    start from the startup policy, so a wake.codexFork opt-in written later
    left every completed proxy answer in the outbox silently (measured
    2026-10-17); the gate is now built per exchange round from the round's
    policy, and a record held by a relay guard is logged once per relay and code
    as Codex fork relay send held: <code>; nothing is audited and nothing
    changes while the section is absent.

  • Service owner probe in unit tests (issue #423): the runner deps carry the
    owner probe of a service admission (serviceOwner), and task starts,
    continues, closed-session resumes, Codex runs and Codex queue runs use it.
    The task test fixture injects a probe that starts no process, so these
    tests no longer depend on a PowerShell identity lookup that can time out
    on a loaded Windows runner. The daemon keeps the default probe.

  • Windows service identity lookup (issue #427): a PowerShell identity lookup
    or owner observation that hits its 10 s timeout runs once more; every other
    failure is still final at once. A first service admission after a boot or
    a service restart no longer fails, and the start-up readiness probe no
    longer reports a runtime as not ready for two minutes, when only the first
    PowerShell start was slow. Two timeouts in a row fail admission with
    service controller identity lookup timed out instead of service controller identity cannot be verified. createServiceOwnerProbe lets
    tests replace the platform, the spawn function and the timeout.

  • Daemon stop during a Claude probe (issue #420): a daemon stopped by
    SIGINT, SIGTERM or now SIGHUP starts no new readiness probe and waits for
    the one in flight, so a stop no longer cuts a login renewal short. A probe
    still waiting for a service drain is given up instead. A timed-out probe on
    macOS and Linux gets SIGTERM and SIGKILL only after 5 seconds; Windows keeps
    taskkill /T /F. The macOS LaunchAgent gets ExitTimeOut 60 on a fresh
    installation and on an upgrade of a definition without it. A restarted
    daemon reuses the previous daemon's ready verdict when it is younger than
    10 minutes instead of probing at once, so a restart no longer starts a
    Claude call exactly when daemons are killed. A Claude probe a killed daemon
    left behind is logged at the next start, forces the probe, is recorded in
    daemon.json and is shown by kherep-node doctor as
    runtimes.claude.lastStopInterruptedProbeAt until the next ready verdict.
    Every daemon log line, in the managed service log and from kherep-node daemon, starts with an ISO 8601 timestamp. A Windows kill such as
    taskkill /F still cannot be caught; Claude probes stay attached to the
    daemon there.

  • Doctor hook check from a source pin (issue #445): kherep-node doctor run
    through the stable launcher runs from the active service pin, so the Claude
    deliver-hook.mts and wake-hook.mts entries that install.sh wrote from a
    checkout counted as another checkout even at the same commit. A hook in a
    checkout whose source identity (commit and tree, no tracked changes) matches
    the pin the doctor runs from now counts as this checkout and is listed under
    checkouts, symmetric to the existing pinned rule; the identity of each
    root is read once per run. A checkout at another commit or with tracked
    changes is still another checkout, and detail now names the reason
    (commit differs, source tree differs, which side has tracked changes, or
    no verified source identity).