swarm-controller's `InitAgentConfigRepo` node already covers config-repo
creation, so this deletes a duplicate rather than a capability; old
`init_config` rows are skipped by `collect_lenient` with no migration, by
operator decision.
Refs #4398
- swarm_agent_state.rs: HEADROOM in swarm_term.rs is private, so the
intra-doc link can never resolve; switch to plain backticks.
- docs/swarm/README.md: move closing period inside the quoted phrase
per Microsoft.Quotes.
Refs #3802
The swarm can already tell whether an agent is alive — the `agent-status`
KV bucket republishes once a minute — but not what it is doing right now.
A header bar wants the second thing, and a minute-old answer to "is this
agent thinking" is the wrong answer most of the time it is read.
`hive-agent` now publishes a turn-state header to
`$SWARM.agent-state.<hive>.<agent>`, a core subject beside the terminal
rows it already sends. It goes out **on transition, not on a timer**: the
publisher watches the event bus, rebuilds the header, and sends only when
the serialised result differs from the last one it sent — so a second
periodic writer, which is the problem this exists to fix, is not what
replaces the bucket.
The payload is the published contract a swarm-level renderer is written
against, so the test asserts on the serialised JSON keys rather than on
Rust field names. Two fields deliberately depart from the per-agent web
UI's `StateSnapshot`: `turn_state_since` is an ISO 8601 UTC string rather
than unix seconds, matching the sibling `$SWARM.term` subject's stamp, and
`agent_state` carries the swarm's own `AgentState` vocabulary rather than
a `paused` boolean, so a reader can compare actual against wanted without
translating. `turn_state` and `agent_state` stay two separate fields:
neither vocabulary contains the other's values.
Swarm-side, `GET /api/agents/{name}/state/stream` relays the subject as
SSE, resolving the agent's hive at request time exactly as the terminal
stream does and passing the bytes through without parsing them.
The broker grant is a second `--agent-publish-subject` rather than a
widening of the existing one, so the terminal family and the header family
stay independently revocable, and a `module-eval` arm pins the rendered
flag and its argument together — the doubled dollar included, since a
single one expands to nothing in `ExecStart` and yields a grant that
matches nothing.
Refs #3802
otelNoIdentity points swarm.authelia.url at a remote authelia while
deploy.authelia.enable stays false, so ssoLocal is false and nothing
mkDefaults the forge's deploy.forgejo.sso.clientSecretFile. The forge
module's config is gated only on services.hyperhive.enable (there is
no deploy.forgejo.enable to opt out of), so its SSO assertion fired
against every fixture that lacks the secret — entirely orthogonal to
what this fixture tests. Supply the by-hand path, mirroring
otelRemoteAuthelia's own hand-delivered secret.
The swarm.otel.journaldUnits = [ "nginx" ] line added in the previous
commit targeted an assertion that was never firing: journaldUnits
already defaults non-empty, so the effective list just contained
"nginx" twice. Removed as redundant.
Refs #4374
Refs #4374
- otelNoIdentity: name journaldUnits so the fixture trips the store-
identity path it's testing instead of swarm-otel's unrelated
journald-safety assertion (an empty list with log collection on is
refused as "collect everything", not "collect nothing").
- otelNoStores: give it a bao client identity. The secret gate moved
from deployCfg.authelia.enable to a real client cert/key pair, so a
fixture meaning "no telemetry stores" now needs its own secret
identity to keep exercising the exporter/authenticator wiring it
was written for.
- docs/swarm/secrets.md: two vale fixes — a contraction, and drop a
condescending "simply".
The swarm collector's OIDC client secret only existed where authelia
did: `swarm-otel-oidc-secret.service` copied the minted plaintext out
of authelia's container tree, reachable only because the two share a
host's network namespace. A swarm that placed authelia elsewhere
delivered nothing, and the option's own description said so —
"a deployment that places authelia elsewhere points this at a file it
delivers itself." Same gap as #3853 and #4234, and this is the
swarm-otel twin of #4234's fix for Grafana.
Mirrors PR #4361 (Grafana) almost exactly:
- `swarm-bao-otel-oidc.service` reads
`swarm/services/<client-id>/oidc/client` out of the store, in every
deployment, replacing the co-located copy unit outright — one
delivery route, not two, per the ruling that landed under #4234.
- Client registration moved out of `swarm-otel.nix`'s own `config`
block (gated on this host running the collector) into
`glue-swarm-otel-oidc-client.nix` (gated on this host running
authelia), the same split `glue-grafana-oidc-client.nix` made. It
was broken the same way: a split deployment registered the client
nowhere at all, so authelia never minted a secret for the publisher
to send on.
- The publisher's `services` prefix (write grant in `swarm-bao.nix`,
hive read grant in `policy::render`) already covers any service's
path — nothing to add there. `swarm-secret-publisher.nix` only grew
`serviceClientIds` by one entry.
One judgement call, stated rather than buried: the store-reading unit
renders only where this host holds a client identity
(`deploy.bao.clientCertFile`/`clientKeyFile`), rather than asserting
it the way `swarm-grafana.nix` does. Grafana's local login form is
disabled unconditionally, so a Grafana with no OIDC secret has no way
in at all — that earns a hard refusal. This collector without a
credential still receives every hive's telemetry; only its own pushes
to the stores go out unauthenticated and get refused there, an
already-supported degrade the module's own `haveCollectorSecret` flag
named before this change. So the reading unit follows the shape
`glue-matrix-bao-token.nix` and `glue-queue-agent-credential.nix` use
for their own optional readers: no unit when the identity is absent,
not a build refusal.
Fixtures mirror #4361's: `otelBaoWithAuthelia`/`otelBaoRemoteAuthelia`
are the positive pair (co-located and split, both reading through the
store), `otelNoIdentity` is the negative — no reading unit, no
assertion firing, `clientSecretFile` left null.
Refs #4258
The identity half of giving an agent container its own way into the store.
The operator's ruling: minting an identity the agent itself can authenticate
with takes the delivery from four pieces (mint -> store -> pass -> use) to
three (mint -> store -> use), and makes the agent that much less dependent on
the hive it runs on — but only if the grant behind that identity is scoped to
that agent's secrets, read-only. Otherwise it is a second way to be everyone.
So `render_agent` is a SIBLING of `render`, not a parameter added to it. A
hive's document is wide in two of its kinds on purpose (the module header says
why each of those is a decision), and none of that breadth transfers: an
agent's path names the agent, so scoping to it costs nothing and drifts
nowhere, and an agent has no business with a service's OIDC client secret,
another agent's credentials, a hive's, or the controller's. The tests say that
as an exhaustive check over `path::Kind` rather than as a list of paths, so a
kind added later cannot be granted here by a renderer nobody re-read.
`render`'s own text is untouched, and its byte-for-byte assertion is left
exactly as it stands: a hive can still read every agent's secrets, and closing
that is the separate decision its doc comment already prices.
The object name is `hive-agent-<agent>`, one string for the policy, the
cert-auth role and the certificate subject. Inside `hive-` rather than beside
it because `sys/policies/acl/hive-*` and `auth/cert/certs/hive-*` are the whole
of what the controller may create, and the controller is the only principal
that learns an agent exists — at no cost in authority, since it already holds
create/update on `secret/data/swarm/agents/*`. It still cannot collide with
`hive-<hive>`: that needs a hive named `agent-<agent>`, and a hive name may not
contain `agent` (nix/reserved-hive-fragments.nix, the reservation the queue's
`hive-<name>-agent` client ids already lean on). A test spells that collision
out and fails if the prefix moves somewhere that guard does not cover.
Rendering only. Nothing mints a leaf, writes a role or delivers a certificate
yet, and nothing calls either new function — because nothing at the module
layer knows which agents exist: hive-c0re creates them at runtime and renders
them into its own meta flake, which is why even the gateway's per-agent vhosts
are a generated conf rather than nix. Issuance therefore has to be runtime, and
that design is the operator's to approve before it is built.
Refs #4386
The v2-conversion script carried over the local-test dashboard's own
title ('agents-full-test') instead of the real one, since it read
spec.title straight from the generated ground-truth file without
overriding it back. Caught on a diff review, not by CI — nothing
schema-level flags a wrong-but-valid string.
Confirmed empirically (real Grafana 13.0.7, a file-based provisioner
identical in shape to swarm-grafana.nix's) that the deployed version
provisions v2-schema dashboards natively, tabs included, no migration
path needed. Supersedes the row-based split from the previous commit
on this branch: same four groups (activity/cost/health/subagents), same
17 panels with identical content (targets, fieldConfig, options,
descriptions all carried over unchanged — only the document shape
around them moved from a flat panels array + row wrapper panels to the
v2 schema's spec.elements map + spec.layout TabsLayout referencing them
by id), now rendered as real browser tabs instead of accordion rows.
Generated the panel-content half of this file mechanically rather than
hand-authoring the new schema: posted the pre-conversion (v1, flat,
17-panel) dashboard to a local Grafana 13.0.7 and read it back through
the v2beta1 API, which is Grafana's own conversion of the exact same
panel content into the v2 element shape — then replaced only the
generated layout (a flat GridLayout) with a hand-authored TabsLayout
grouping the same element references into the four tabs. Verified the
result end-to-end against the same local instance: file-provisioned
(confirmed via the loaded object's own grafana.app/managedBy annotation),
and a real screenshot of each of the four tabs rendering with its
correct panels.
Cost, health (cpu/mem/disk), and subagents, plus an activity row for the
panels that fit none of the three (agent/turn counts, lines-of-code and
CLAUDE.md-size trends). Rows rather than tabs: Grafana's tab layout is a
v2/Scenes-schema feature, and this deployment's file-based provisioner
doesn't reliably load v2-schema dashboards, so classic-schema collapsible
rows are the closest available equivalent — one section open, the rest
collapsed. activity stays expanded so the dashboard opens on something
useful instead of a wall of accordion headers.
An agent container cannot dial the store's loopback listener: the bridge
to-loopback DROP rule is there precisely to stop that, and the store
authenticates every reader by client certificate, so the usual answer —
a gateway vhost — is the one shape that cannot work. A terminating proxy
strips the certificate and bao sees nginx as the client for every hive.
nginx's stream module does not terminate. `ssl_preread` reads the SNI off
the ClientHello and splices the rest of the connection through byte for
byte, so bao completes the handshake itself and authenticates the client
it actually has. That is the no-vhost rule kept, not bent.
The listener binds the bridge IP rather than every address, because bao
already holds `127.0.0.1:<port>` in the same netns and a wildcard bind
there is EADDRINUSE — nginx would fail to start, taking the gateway with
it. Nothing moves as a result: the name already resolves two ways, so a
host-side reader still goes straight to loopback and an agent goes
through the passthrough, both on one `BAO_ADDR`.
Renders only inside the store's own `deploy.bao.enable` region; a host
that runs no store grows no listener and opens no port.
Per-agent certificates and per-agent policy are separate work.
Refs #4386
validateConfigFile runs `otelcol validate` at nix build time, in a pure
sandbox where systemd's StateDirectory= has not run yet, so the
directory named by extensions.file_storage.directory does not exist.
Setting create_directory = true lets the validator create it itself,
matching what happens at runtime once StateDirectory= has acted.
Refs #4375
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mara's steer on the issue, right as the previous commit's plan was
posted: the swarm-level term isn't a separate page, it's part of the
same pwa -- selecting an agent should show a small preview (no header,
no input) below the existing detail fields. That's the whole MVP for
this issue; the full terminal (+ sending input back to the agent) is
explicit follow-up scope.
AgentTermPage -> AgentTermPreview: drops the Panel wrapper, the
"terminal" nav badge + wouter route, and the back-link -- just the
inline preview, height-capped at 12em (@hive/shared/terminal.css's own
default is a much taller 72vh/60em, sized for a dedicated page rather
than a peek).
Moves the TermMsg rendering pipeline (Row.tsx, termMsg.ts, linkify.tsx,
markdown.ts) from @hive/agent into @hive/shared, so swarm-ui becomes a
second consumer of it instead of forking a copy -- CSS was already
shared (@hive/shared/terminal.css). marked+dompurify move from
@hive/agent's deps to @hive/shared's; swarm-ui picks them up
transitively, no new direct dep there.
New swarm-ui route /agents/:name/term (AgentTermPage), linked from
AgentsPage's detail panel via a "terminal" badge next to "link matrix
account". Consumes GET /api/agents/{name}/term/stream: unlike
@hive/agent's own useLiveStream (TermEnvelope-wrapped, history/backfill
dance), the swarm relay forwards one bare TermMsg per SSE event with no
envelope and no history endpoint -- useSwarmTermStream is a much
smaller hook for that shape (EventSource -> parse -> coalesce, nothing
to buffer/dedupe/backfill against).
Verified against a live SSE mock (screenshots in /agents/iris/state/screenshots/
3801-agents-detail-panel-terminal-badge.png and
3801-agent-term-page-live-rows.png -- real rows rendering through the
shared Row component, not just a build/typecheck pass).
autheliaCfg/forgeCfg/vmCfg/vlCfg were local re-exports of
hyperhiveCfg.swarm.<subpath> — a shape that hides the full option
path from grep, the same hazard #4356 fixed for the tls alias.
Removed the six let-bindings and spelled the full
hyperhiveCfg.swarm.<subpath>.<field> path at every use site instead.
None of the read fields (url, machine, hiveClientPrefix,
agentClientSuffix, domain, port) sit on the old side of a
mkRenamedOptionModule in deploy.nix, so inlining the alias's own
current path is correct as-is.
Refs #4363
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous commit left two delivery paths and a three-way gate:
`swarm-grafana-oidc-secret.service` copied authelia's minted plaintext
out of its host tree wherever the two were co-located,
`swarm-bao-grafana-oidc.service` read the same value from the swarm
secret store wherever they were not, and `ssoConfigured && (ssoLocal ||
haveClientIdentity)` decided whether Grafana got an OIDC block at all.
Delete the co-located path. The store reader is now THE delivery unit,
in every deployment — the publisher on authelia's host writes
`swarm/services/<id>/oidc/client` whether the reader is a network away
or in the container next door. The ruling behind it: the store exists so
a host holds ONE out-of-band secret, its client certificate, and reads
everything else with it. Skipping the store when the producer happens to
be local saves a round trip and costs a second delivery unit, a second
way for the file to be wrong, and a gate to choose between them.
The gate goes too, and both of its questions become assertions, scoped
to hosts that run Grafana:
- `swarm.authelia.url` must be set. `auth.disable_login_form` is
unconditional — Grafana ships an admin/admin account on a public
vhost — so dropping the OIDC block when the swarm names no IdP
produced a container with no SSO and no password box, silently. An
eval-time refusal naming the option is the only report that reaches
anyone, the shape swarm-nats.nix already uses for the same option.
- `deploy.bao.clientCertFile` / `clientKeyFile` must be set. This
replaces a warning that nothing reads back, and its message names both
options and where the leaf comes from.
Fixtures follow. `grafanaWithAuthelia` gains the cert pair, because a
co-located host is a store reader like any other. The old
`grafanaRemoteAutheliaNoIdentity` is kept rather than deleted, renamed
`grafanaNoIdentity`: the shape is still reachable, only its deliverable
changed from silence to a refusal, and an arm now reads that refusal
back. Its mirror `grafanaNoSso` covers the other assertion, each fixture
wrong in exactly one way so an arm can name which refusal fired. Every
positive keeps an explicit negative — the one-delivery-unit arm asserts
the deleted unit is absent in both topologies rather than merely that
the store reader is present.
Refs #4234
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Grafana's OIDC client secret only existed where authelia did. One
`ssoLocal` gate — `grafana.enable && authelia.enable` — decided the
client registration, the minted secret's delivery and the whole
`auth.generic_oauth` block, so a swarm whose authelia runs on another
host got Grafana with no SSO wiring at all. The local login form is
disabled unconditionally, so that is no way in.
Split the one gate into the two questions it was conflating:
- `ssoConfigured` — does this SWARM have an identity provider
(`swarm.authelia.url`, which is swarm-wide and whose own description
makes null mean "no SSO configured"). With a delivery route present
this is what emits Grafana's OIDC block.
- `ssoLocal` — is authelia on THIS host, now spelled as the forge and
matrix modules spell it. It decides only which unit delivers the
secret.
Where authelia is elsewhere, `swarm-bao-grafana-oidc.service` reads the
secret from the swarm secret store, shaped after
glue-queue-agent-credential.nix: cert login fails loudly because a retry
fixes every state it fails on, the read degrades quietly because no
retry turns "no value there" into a value, and nothing writes a
stand-in. The producer is the publisher that already runs on authelia's
host, which gains the swarm's service clients beside the per-hive ones
at `swarm/services/<id>/oidc/client` — with the write grant in
swarm-bao.nix and the hive read grant in `policy::render` to match.
Registration moved to glue-grafana-oidc-client.nix. It has to be
declared where authelia's config is rendered, and swarm-grafana.nix's
config block hangs off this host running Grafana.
Two judgement calls stated rather than buried: a hive's read policy now
grants the whole `services` prefix, because a service's path names the
service and nothing swarm-wide records which hive runs it (cost recorded
in docs/trust-boundary/security.md); and the client is registered on any
authelia host, because no swarm-wide "this swarm has a Grafana" fact
exists to gate it on.
Refs #4234
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The journald receiver runs with --lines=0, so every collector start
only ships what's written after it starts, and a restart silently
loses whatever landed while it was down. Point it at a file_storage
extension so the read cursor survives a restart; start_at stays at
its 'end' default since the cursor now covers everything after the
first run.
Refs #3818
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports the subtle text-shadow glow @hive/agent's and the dashboard's own
headline rule already use (both independently converged on
`text-shadow: 0 0 Npx color-mix(color, transparent)`), not their whole
uppercase+letter-spacing headline typography -- that's those UIs' own
look, swarm-ui never adopted it and this issue only asked for the glow.
- Panel titles: `currentColor`-based, since a panel title has no one
fixed accent (some are dynamic, e.g. an agent's own name) -- a fixed
--purple glow on plain --fg text would read as a mismatched halo.
- Active nav tab: each item's own `accent` (NAV_ITEMS), the same value
its underline already uses, so the glow and the underline read as
one accent rather than two that happen to agree.
AgentsPage's table view (and its list+detail split) hits the same
60em readable-line-length cap the issue report's wide table already
opts out of via shell-body-wide. Adds /agents to WIDE_BODY_ROUTES --
the allowlist's own comment already anticipated this, a future wide
page reaches for the same class rather than inventing its own cap.
mara's review point on #4351: an agent isn't pinned to a hive forever
(it can move), so a URL naming one would go stale the moment it did.
Resolve the hive at request time from the agent-status bucket instead
-- the same source AgentStatusRow.hive already comes from -- rather
than trusting a caller-supplied value. Route is now
GET /api/agents/{name}/term/stream; a never-reported agent now answers
404 (no hive on record) instead of silently guessing.
hive-agent already publishes classified TermMsg rows to the core NATS
subject $SWARM.term.{hive}.{agent} -- live only, no retention, by
design. This endpoint subscribes that subject per request and relays
each row over SSE, opaque to this daemon (no TermMsg dependency, same
pass-through shape crate::status already uses for hive snapshots).
No replay/history: the publish side never grew JetStream retention, and
this route's own job (a live tail) never needed it.
extract validate_agent_names() and use it for both the parsed
agent-<name> inputs and run_meta_lock's pre-computed fanout list, so a
malformed name can't reach the new fast_forward_applied_main / lock_update
filesystem+git+forge-URL operations regardless of which of the two
sources it came from
argus + mara (PR #4339 review): the previous commit's per-agent lock
relock is a one-shot effect on the single rebuild the cascade triggers
- applied/<name> never moves, so the next relock=true rebuild trigger
(the boot sweep, most notably) re-locks against applied/<name> and
reverts straight back to whatever it was stuck on. The fix didn't
outlive the transaction it ran in.
New forge::fast_forward_applied_main(name), sibling to the existing
reseed-only fetch_config_main_into_applied: for an applied repo that
already has a .git and just needs to catch up, force-set rather than
fast-forward-gated since there's no PR to review on this path either.
Called per cascade agent right after the relock, best-effort so one
unreachable agent repo doesn't block the others.
Once applied/<name>/main has actually moved, lock_update_for_rebuild's
override (always reads current applied/<name>/main, no ?rev pin)
naturally stays in sync on any later relock=true rebuild instead of
reverting.
mara (hyperhive#4271): "whatever is on main is trusted and should be
pulled. dont make it periodic, just add it to the meta update when
choosing the agent."
A meta-input bump previously rebuilt every affected agent against
whatever applied/<name> was already locked to. Normally current, but
silently stale forever if a past deploy failed and nothing since
retried it — the manual meta-input trigger never special-cased that
either, since nothing wired it to check the config repo's live main at
all.
run_meta_lock now relocks each cascade agent's own input alongside the
originally-requested ones, against the input's declared source (the
forge URL) rather than the local applied/<name> mirror prepare_deploy
uses for a reviewed deploy — there's no PR to review on this path, so
nothing to gate. One combined lock_update call for the whole cascade:
simpler than per-agent isolation, at the cost of one broken/unreachable
agent repo failing the whole cascade relock rather than just that
agent (the separate top-level input bump above it is unaffected).
Known gap, not fixed here: applied/<name>'s own main/deployed/* tags
never advance from this path, only a real MergeConfigPr deploy does
that — so the audit trail stays as accurate as today, it just stops
being what actually got built.
fetch_total read issue_get_issue's comments field, Forgejo's running
counter on the Issue object that also increments for several
non-prose event kinds (review_request, issue_ref, comment_ref,
pull_push, ...) that issue_get_comments never returns. Diffing that
count against a fetched window reported drift that was never real: a
comment that would never be shown at any window size. Live repro on
PR #4333 (damocles's diagnosis on the issue): more_after: 2, both
events non-prose.
Fix: read the total straight off issue_get_comments's own
X-Total-Count header instead — the same endpoint that produces the
window, so the two populations can never drift apart again. Mirrors
timeline.rs's own fetch_total, which made the same move for its
endpoint first. fetch_head now captures the header from the request
it already makes (one fewer round trip in the --limit path); the
--tail/default path still probes once via page_size=1, same shape as
before but now scoped to the endpoint that actually produces the
window.
This also fixes the more serious half: fetch_tail derives its
pagination offset (which page holds the tail) from the same total, so
an inflated total didn't just skew the trailer's count, it could send
the offset math reaching for a page beyond the real thread's end,
returning fewer rows than requested or none at all when fetch_tail's
empty-page guard tripped. New test pins this against fetch_tail's
existing pagination-plan helper.
Refs #4335
hive-subagent-mcp already stamps a subagent=<name> resource attribute
(alongside the parent's own agent=<name>) on every claude_code.* sample
a spawned subagent turn emits -- the label already existed, it just
wasn't plotted anywhere. Adds five panels: subagent cost, subagent
turns, subagent cost share (of all cost), cost by subagent name, and
cost by parent agent. No backend changes needed.
The dashboard already templated an $agent variable and broke turns and
active-time out by agent, but cost and token totals only split by
model, effort and query_source -- there was no panel answering "how
much did each agent spend/use", the aggregate-by-agent view mara asked
for.
Same principle as the enum-enumeration fix, mara caught it in review:
the description said where full output lives, but format_task's own
answer already names the exact path when output was actually
truncated — the description doesn't need to say it too.
Mirrors #4333's split on the sibling hive-subagent-mcp surface (mara's
ruling there, quoted on #4336: "dont explain all the possible states
that can be returned in the tool description. instead explain the
status when returning it").
status_explanation() carries what each TaskStatus means and what to do
about it, appended to every format_task answer. The status tool's
description shrinks to what it's for; a caller only ever sees one
state at a time and now learns what it means without cross-referencing
anything else.
The `status` tool description enumerated all five states it can report, so
every caller paid for four answers it didn't get and read the explanation
in the wrong place. The description now states only what the tool is for
and that it costs nothing to call; each answer it returns carries its own
meaning and the caller's next move instead — running, starting and idle
were terse, the not-found error terser still, and they had been leaning on
the enumeration to be legible. The killed answer and the end-of-turn todo
are unchanged. `continue`'s description keeps its killed-resume sentence:
that describes what the tool does, not a state it might hand back.
Refs #4326
argus's review on #4333 flagged one real vale error (Microsoft.Contractions):
"did not" in the new "A killed turn" section reads as "didn't" instead. The
rest of the diff's new prose (docs/tools/subagent.md and the tool
descriptions in hive-subagent-mcp/src/mcp.rs) has no other instance of the
same expansion, confirmed both by a local `vale --minAlertLevel=error` run
(clean) and by grepping the added lines. Also tidies session.rs's module doc:
the two `//!` runs split to dodge the 30-line comment-block lint had a bare
trailing `//!` right before the blank separator; dropped so the first run
ends on content.
Refs #4326
A subagent whose claude process died on a signal — the kernel's OOM
killer, a stopped unit, an `interrupt` — was indistinguishable from one
that finished its turn: its entry left the `running` map, `status` fell
through to "a session exists on disk" and answered `idle`, and the
end-of-turn todo said the subagent had "finished". The usual next move
on that reading is `continue`, which resumes work that was cut mid-turn
with nothing having recorded that it was cut.
The driver already preserves how the child ended — `RunningClaude::wait`
returns `Error::Exit` carrying the `ExitStatus`, whose `signal()` is the
whole answer — so this reads it rather than having to recover it:
`classify_end` turns the outcome into `Complete` / `Killed { signal }` /
`Failed`, and `State::finish_turn` remembers a kill against the name
(cleared by the next confirmed spawn under it).
What an agent sees as a result:
- `status` reports the session killed, naming the signal, instead of idle;
- the todo the daemon pushes without being asked says the subagent was
KILLED mid-turn rather than that it finished;
- `continue` still resumes such a session, but its reply says the
previous turn was killed, so no caller carries on from cut-off work
believing it was complete.
Refs #4326
Three cases beside the existing ingest ones, since the read route has the
same trap and one property of its own.
- the route exists and carries `auth_request`;
- it has no login fallback — with the browser location as the positive
control, so a pass means the two routes differ rather than that
`error_page` and the login host are absent from the whole vhost;
- it injects no filter, on `extraConfig` and on `proxyPass` both, because
VictoriaLogs takes its filters as request parameters and those ride an
upstream URI as easily as a directive. A filter arriving later is then a
visible diff here rather than a quiet change of rule.
Each absence arm leads with a clause that proves the location resolved and
that `hasInfix` finds what is really in that string, so "not there" cannot
be read off an unreadable path.
Refs #3870