Repository navigation
Added
-
Codex fork refusal metadata (issue #477). A JSON-RPC error response to
initialize,thread/read,thread/turns/listorthread/forkduring 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 ismissing,empty,textoroverlong; 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 wordkherepthe 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 doctoradds theintakeLatency
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) andlastReceivedAt, 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 keepslastEventand
lastEventAt, the last hook event (SessionStart,UserPromptSubmit,
PostToolUseorStop) and its time, andlastStopAt, the time of the last
Stop; an input without a known event keeps them. The original owner's
PostToolUserefreshes only the event of an existing record, and a
subagent's call writes nothing. The pureidleEvidence()reads a record as
idle only for aStopat 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 sessionsprintslast event <event> at <time>for this node's Codex
sessions, andkherep-node doctoradds thecodexSessionscheck 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.codexForkpolicy section (enabled,sessionsby full thread id
only,platforms["darwin"]only,idleAfterSeconds30 to 3600, default
90,maxAnswerBytes256 to 8192, default 4096) is off when absent; any
malformed shape turns off only the fork reply and leaves the rest ofwake
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 attemptsuncertain
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'spermission_modebefore the recorded
one and treatslastSeenas approximate. Idle evidence now fails closed on
Stopcontinuations: the CodexStophook marks the session record with
lastStopEndedafter recording theStop, on every exit that returns no
blocking continuation, every hook event clears the mark, and
idleEvidence()requires it, so a blockingStopor a failed mark is never
idle. The attempt store skips dotfiles such as.DS_Store, and
codexFork.sessionsids are matched without case and kept in lowercase.kherep-node doctorprints a
writtenwake.codexForksection with a count of opted-in sessions and fails a
rejected one. New audit actionsfork,fork-declined,fork-failed,
fork-uncertain,fork-superseded,fork-not-idleand
fork-disabled-platformexist 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 incodexFork.sessionswith idle evidence and
an accepted exact-addressed message, the daemon starts one short-lived
codex app-serverof thecodexonPATHper message, one at a time on the
node, in its own process group and withKHEREP_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/getthat refuses an active, paused or otherwise unfinished
goal, and refusesgoals-disabledwhen 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 afterthread/goal/geton the fork
answers-32600with exactlyephemeral 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: nullincluded, fails the attempt with reasonfork-goaland a
fixed detail code. Nocodexversion or hash is pinned; the feature list is
cached per resolved path and hash.deferGoalContinuationis 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(stagesstable,experimental,under development,
deprecatedandremoved, 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-32601and fail the
attempt, as do tool items, goal notifications for the fork and turns the
helper did not start, whenever they arrive, also afterturn/completedor
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-unconfirmedwith the service admission retained.
The validated answer is stored in the attempt file
(codex-fork-attempts/<owner>/<message>.json, mode 0600) with a fixed
reasoncode 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.jsonlline; on another platform the only effect is a
fork-disabled-platformline. The daemon now marks launching or running
attemptsuncertainat start, the Codex delivery hook does nothing inside a
fork helper, andkherep-node doctoradds thecodexForkAttemptscheck
(counts per state, no ids). The JSON-RPC client of the native Queue cleanup
moved tocodex-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 optionalproxyOfonmessage.send
andmessage.delivernames the original, never together withinReplyTo,
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 sidecarinbox/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
capabilitymessaging.proxy.v1from adirectoryframe 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 andmsg inboxshow 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-relayedand the attempt'srelayedAtmean the Worker answered. The
Worker validatesproxyOfagainst the live original row only (proxy parent unknownfor 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,upgradeandrollbackwrite<configRoot>/bin/kherep-node.mjs,
a POSIX shimkherep-nodeand, on Windows,kherep-node.cmdand
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'smodules/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
launcheraswritten,unchangedorfailed. 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.shinto a managed
<!-- kherep-intercom:start -->block of~/.claude/CLAUDE.mdand into
~/.claude/teams/kherep/ROUTING.md,codex/install.mtsinto the
<!-- kherep:start -->block ofAGENTS.mdand into
orchestra/ROUTING.md. The block listsmsg sessions,msg send <node>/<session> -- <text>,--reply-to,--new ... --directive,msg inboxandmsg statuswith one example, which names this node from
node.jsonwhen 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.shandkherep-node doctordo not yet compare the rendered
files. -
Confirmed remote stop for Claude Code background tasks (issue #437, second
part).task stopandmsg stopfor 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), runsclaude stop <short id>, the
supported stop, and waits for every captured identity to end; the listing
shows the sessionstoppedwithout apidwhile its process still lives, so
the row is never the evidence. The tree is captured again from its live
members afterclaude stopreturned 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 afterclaude stop
returns (measured), and one that still lives after the grace period makes the
stopstop_failedwithprocessStaterunningwithout a signal, because the
supervisor's reaction to a signalled session is not measured.
stopConfirmed: trueonly when every captured identity ended; aclaude 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_runwith the new run recorded for the next status. Status offers the stop (stopSupported: true) for a
workingorblockedsession 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
directoryframe gives each
node an additiveintakerecord:oldestQueuedAtandqueued, its queue at
the control plane, andlastAcceptedAt, the Worker's time of the node's last
report that took one of its messages out ofqueued(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 sessionsprints anintake:line under each
node,NOT DRAININGonce its oldest queued message is older than 5 minutes,
andmsg statusnames the target's intake while a message is queued. The
newqueueDraindoctor check reports this node's queue from the last
directory,ok: false, measured: falsewhenever it was not measured. Older
nodes accept the new frame; deploy the Worker before the nodes.
Changed
wake.codexForkstaysdarwinonly (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-enabledwin32would 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-32600refusal 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; afterclaude stopthe 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): thedirectoryframe carriesworker.capabilities, and a node
refuses every fork before admission (fork-relay-unsupported) while its
current connection has not shownmessaging.proxy.v1. - Codex messaging rules allowlist the stable launcher (issue #436).
rules/kherep-control-plane.rulesholds oneprefix_rule, fornodewith
<configRoot>/bin/kherep-node.mjs(forward slashes on Windows), whose
examples are the exact intercom command.renderControlPlaneRulestakes a
list oflauncherRule()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 namenode "<configRoot>/bin/kherep-node.mjs"; elsewhere they keep
naming the running code'scli.mts. The rules file no longer carries the
rule for the installing checkout'scli.mts, andcliRule()and
controlPlaneCli()are removed;kherep-node doctornow reports a rules file
that is not exactly the launcher rule asstale. Upgrade the service to a
pin with this change first, then runcodex/install.mts. A Codex session on
a host without a managed service asks for approval permsgcommand. - Native Codex Queue pointer producer off by default (issue #429). The
exact-owner producer that submits a fixedcodex queuepointer for an
authorized Codex owner now runs only with"nativeQueue": truein the node
policywakesection; 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 rewritespolicy.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 doctorshows 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 statusfor a Claude task no longer answers process state
unknown: the target reads thepidthatclaude agents --json --alllists
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
workingorblockedsession isrunning; a session without a live
process, or a finished one whose process still runs, isidleorclosed;
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 removeslisteners/<id>.mode.json,
.turns.json,.scope.json,.stuck.json,.woke.json,.queued.json,
.busy-hint.jsonand.busy-admission.jsonof a session that a fresh,
complete listing does not show, thatknown-sessions.jsonno 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.jsonhas had no writer or reader since 2026-10-11 and
goes with the rest.kherep-node doctorreports the count as
listeners.orphaned. A session resumed after more than 7 days without a
listing arms itsSessionStartlistener 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 logslane <lane> item <kind> waited <s> s, took <s> s, the existingtookline gains, waited <s> swhen both were slow, and a round that sent or handled more than 32
frames logsround 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 sessionsandtask newis 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_IDnames a
Claude session only when no Codex thread variable is present or the Claude
Code process (CLAUDE_PID) sits between the command and a recognisedcodex
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.--frombehaves as before. Measured on macOS with Codex 0.160.1;
Windows unmeasured. -
Confluence semantic search without
~/.local/binonPATH(fixes #478).
The search behindsearch,relatedandstitchspawnedtwgby bare
name, so a macOS background job, whosePATHlacks~/.local/bin, failed
every search withspawn twg ENOENT.confluence-semantic.mtsnow resolves
the executable itself, without a shell:KHEREP_TWG_BIN, thenPATH, then
~/.local/bin/twg,/opt/homebrew/bin/twgand/usr/local/bin/twg; on
Windowstwg.exeonPATH, then the%LOCALAPPDATA%and%ProgramFiles%
install locations. The order mirrorsmodules/twg/runtime/resolve-binary.mts.
A set but unusableKHEREP_TWG_BINand a CLI found nowhere both end as a
search that did not run (UNKNOWN), as before. -
stitchidempotence (fixes #479). A secondstitch --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 aRelated pagesblock 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 isunchanged. The sibling flag was not the
cause: it only annotates thelinktrace 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
afterok: 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 launchdExitTimeOut. A macOS stop refused before
launchctl bootoutruns leaves the live service untouched and reports
restored; any other stop failure is treated as possibly effective and is
never reportedrestoredwithout proof. Anunknownrecovery releases the
drain marker only when no definition write was attempted and the daemon told
to stop is gone. Failures add a fixedstageand, for anunknownrecovery,
a fixedblocker; the error message is still never printed.service status
reportsdrain(none,active,staleorunknown) and fails on a stale
or unverifiable drain. It names the recoveryservice upgrade --source <clean checkout at <commit>>for a stale drain and for a stopped service with a dead
recorded daemon, whichupgradenow 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.deliverframe 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'sauth.okwas
handled still waits behind it. A command ack is now allocated, and the
command counted as processed in theackfield 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
watchCodexTaskspass 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 thesession.*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.
Tightenedsessionsfields apply to the next command. -
Retained admission recovery for direct native Codex Queue reconciliation
(Refs #442). After an unconfirmed shutdown, the same helper's laterexitand
closecan 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
closedwhile the recorded process
lives (issue #454). When theclaude agents --json --allrow lists nopid,
as it does right afterclaude stopwhile 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
answersidlefor an ended session (runningfor aworkingorblocked
row; a missing row answers from the task state,idlefor an ended task and
otherwiseunknown) with its run version; null or changed, the process ended and the answer
staysclosedoridleas before; unreadable, it isidentity_unknown. A
stop that endedstop_failedwith the session process alive is followed by
idle, thenclosedonce 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.codexForkopt-in or opt-out, or awake.sessionschange, 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 onewake.jsonlline per owner,
message and code: the admission code with the prefixfork-(fork-cwd-refused,
fork-permission-mode,fork-budget-spacing, ...) and the producer's own
fork-producer,fork-mcp-unknown,fork-features-unknownand
fork-relay-unsupported;fork-not-idleandfork-disabled-platformare
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 acodex execsession reportsbypassPermissions
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 awake.codexForkopt-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
asCodex 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 outinstead ofservice controller identity cannot be verified.createServiceOwnerProbelets
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 getsExitTimeOut60 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.jsonand is shown bykherep-node doctoras
runtimes.claude.lastStopInterruptedProbeAtuntil the next ready verdict.
Every daemon log line, in the managed service log and fromkherep-node daemon, starts with an ISO 8601 timestamp. A Windows kill such as
taskkill /Fstill cannot be caught; Claude probes stay attached to the
daemon there. -
Doctor hook check from a source pin (issue #445):
kherep-node doctorrun
through the stable launcher runs from the active service pin, so the Claude
deliver-hook.mtsandwake-hook.mtsentries thatinstall.shwrote 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 existingpinnedrule; the identity of each
root is read once per run. A checkout at another commit or with tracked
changes is still another checkout, anddetailnow names the reason
(commit differs,source tree differs, which side has tracked changes, or
no verified source identity).