Commit Graph
5 Commits
Author SHA1 Message Date
yhh 394da0a313 refactor: the kernel's packages live in the layer they belong to
internal/ was 141 packages side by side, so nothing but a reader's memory
said which were foundations and which assembled the rest. Each now sits in a
layer directory, and the directory is the declaration:

  base      L0  foundation with no domain in it
  contract  L1  types and interfaces the layers above agree on
  model, tools, safety, ext, state, platform
            L2  subsystems, peers of one another
  runtime   L3  the turn engine
  session   L4  the controller
  assembly  L5  the composition root
  frontend  L6  cli, serve, acp and the host adapters

Only paths change: package names, code and behaviour do not. Imports,
repolint and coverage baselines, sensitive paths, CI package lists and docs
follow the new paths; release notes keep the paths they were written with,
and sample paths inside tests and fixtures keep theirs.

repolint gains layer-order: a package may import its own layer and those
below it, read from where it lives. The 47 imports that point upward today
are recorded as that rule's opening debt (-allow-widen, a new rule's first
baseline) and can only go down. Most of it is config, which still reaches
into eight subsystems and is the next thing to take apart; the rest is
event, history and a few tools reaching into runtime for types that belong
in contract.
2026-09-23 18:28:21 -07:00
esengine 22bff74de6 bench(runtime-resume): ask what names a host-started execution
9a measures one surface and its name says so: a person typing /<skill> into a
live session, routed through Submit to the controller's slash path. The headless
counterpart is deliberately not measured beside it — caller-owned, with its own
cancellation and session ownership — because reading one for the other is how a
real number gets the wrong subject, which has happened twice in this series.

The fourth arm is queued rather than restart. Restart is an axis every arm
already has; what no other arm can answer is whether an identity exists before
the child does, and only work the scheduler is holding back asks that.

Four identities are printed apart, because this is the first surface where they
may not coincide: the synthetic dispatch id the host mints so a child's activity
can nest on screen, the store's own artifact ref, the provider-visible parent
call, and the execution a journal would open. For a model-delegated run the last
two are one string, and that has only ever held because a tool call produced
every durable execution there was.

What it found is narrower and sharper than "a slash skill records nothing". The
store is not empty: since the runner converged, a slash-started child is marked
running before it acts and carries a correct terminal afterwards — completed,
interrupted, cancelled, each measured. What is missing is the execution. Nothing
was ever opened, so nothing places that child, and the arms say exactly that:
record-only, a reader can see a child of this name was cut and cannot say which
invocation it belonged to. LOST-SILENT is reserved for the queued arm, where
work the scheduler had admitted into its domain and then refused leaves nothing
at all.

That is what makes the queued arm decide a linearization. An identity for this
class would have to exist before a child artifact does, and the three candidates
fail by exhaustion rather than by assertion: the synthetic dispatch id may never
be persisted as lineage, the child ref does not exist while the work is queued,
and the parent call must stay empty because there is none. Both oracles held in
every arm — no persisted lineage named a host event id, and every parent was
empty.

Two labels were corrected while building this, both mine. A restart that still
holds a child record is not silence, and calling it LOST-SILENT would have
reported a surviving record as a total loss; and the rebuilt column credited the
holder fleet's nodes to the invocation until it was filtered to the execution it
belongs to.

Nothing is implemented. These arms establish what the candidate identities
cannot do; what replaces them is the next cut's question.
2026-09-05 00:08:56 -07:00
esengine 6dac296ad6 bench(runtime-resume): ask what kind of execution a subagent skill is
8d-2 classifies rather than compares. Whether a runAs=subagent skill belongs to
the delegation class the task and fan-out arms established, or is an execution
class of its own, leads to opposite work — converge it onto the shared owner, or
declare an exclusion — and the answer cannot come from which plumbing happens to
exist today.

Two runners are measured apart instead of averaged. The tool a model calls
prepares a transcript, supports continuation and saves a terminal; the read-only
entry point builds a session in memory, refuses continuation and stores nothing.
Asking the second for durability would be demanding it break its own contract,
so those arms ask what it is instead, and answer in a classification —
ephemeral-seen or ephemeral-unseen — rather than a pass.

Seven dimensions, one row each, because a skill can share one domain and not
another: scheduler, graph, journal, store, cancel, restart, identity. The
scheduler has no refusal hook to read, so admission is established as an A/B —
the call is out, unanswered, the ceiling occupied and no child of its own has
reached the provider, against the same dispatch with capacity free whose child
is inside it. Only the slots check is reached that way, and the README says so
rather than letting a writer claim be read as measured.

Identity reads the two entry points apart. A model-invoked skill has a call to
join to; the controller's own drops that id deliberately, so an empty one there
is top-level provenance and not a lost join — both run in one death so the row
compares two lineages rather than a value against an expectation. The cancel row
judges the terminal against an observed cause, never wording: the child's own
context closed, which is what a cancellation does to it and a failure does not.

The queued arm is the negative control and bounds any convergence: an execution
the scheduler never admitted produced nothing, so a change that satisfied the
running arm by preparing a transcript at dispatch would break it.

Three labelling faults were caught while building this and are fixed here rather
than reported later: a journal and a store that were empty on both sides read as
persisted-direct, a restart row credited the holder fleet's interruption to the
skill, and the stop row named a guarded turn caller-owned. Absence now has its
own verdict, evidence is filtered to the execution it belongs to, and the graph
row carries the whole graph's node count so "the filter found nothing" cannot be
read as "there was nothing to find".
2026-09-04 22:52:44 -07:00
esengine 1a9076abf8 bench(runtime-resume): stop each surface with the stop that owns it
The last cut reported a cancellation defect. It was measuring the wrong owner,
and this A/B retracts it.

A turn the controller admits itself — what Send opens, and what a person's Stop
reaches — carries the cancel the gate holds. A synchronous Run is never admitted
through that gate: the controller holds no cancel for it, and the caller's own
context holds the only one. The probe had been driving Run and then calling
Controller.Cancel, which claims nothing there, and reading Running(), which
answers for a gate that was never told about the turn — so "the turn ended while
its work ran on" was two facts about two different things.

Read the right way round, the interactive surface is correct. An ordinary shell
the turn is waiting on ends the moment the stop arrives. A delegation stopped
while the scheduler still held it back settles with no store record, and one
stopped while its child was executing settles with the child's own cancelled
terminal — the table this step was aiming for, on the path a person actually
uses. On the other surface both halves hold too: the controller leaves the
caller's work alone, and the caller's own cancel ends it.

One arm is red, and it is the one that matters most. Work already handed to a
background job — which the tool result tells the caller runs across turns — is
gone the instant the parent turn is cancelled, with the probe confirming it was
live when the stop was issued. That contradicts the ownership boundary the
handoff exists to draw. The mechanism is not located here: a job's context is
rooted in the manager, not the turn, so something else in the child's chain is
carrying the cancellation, and finding which edge is the next cut's job rather
than a guess in this one.

The readings now say what surface they came from, so neither answer can be read
as the other's again.
2026-09-04 21:13:10 -07:00
esengine c6313e58d2 bench(runtime-resume): ask where a stop goes
8c left one reading unexplained: a cancelled turn ended while its foreground
delegation stayed open and its child kept running. That could have been three
different defects, so this is the probe that tells them apart rather than a fix
aimed at the first guess.

Four readings, and they localize it. A plain shell — `bash sleep 30`, a tool
that derives its context the way every tool does — is still running three
seconds after the stop, with the turn already ended. So the fault is not in how
a delegation derives its context: an ordinary tool loses the same cancellation.
The delegation arms agree from both sides of the slot: one stopped while queued
and one stopped while executing are both left open after the turn is over.

The fourth is the negative control and bounds any fix. Work already handed to a
background job keeps running when a stop is aimed at the turn — which is
correct, and is what a cancel that reached everything the session knows about
would destroy. A fix passes this arm only by cancelling what the turn owns and
nothing further.

The delegation arms now carry this as an invariant instead of a premise: once
the turn has ended, nothing it owned may still be open. An open record implies a
live owner, since nothing else closes one, so the child's liveness rides in the
reading as evidence rather than standing in as the test — a delegation still
waiting for a slot has no child at all and is just as much still owned. That
also fixes a hole in the first version of the row, which read a queued
delegation's absent child as work that had ended.

No production change: this says where to look, and the next cut says what to do
about it.
2026-09-04 21:02:39 -07:00